Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

결제 모듈 미션 구현 #6

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6ff216e
Chore : 페이지 구성 작성
hmk20000 Nov 18, 2024
084874d
모달창 구현
hmk20000 Nov 18, 2024
a67f525
카드 기본 컴포넌트 제작
hmk20000 Nov 18, 2024
543f954
결제 메인 페이지 구현
hmk20000 Nov 22, 2024
5ca7c26
캐러셀 컴포넌트 구현
hmk20000 Nov 22, 2024
708e319
카드 입력 및 완료 구현
hmk20000 Nov 22, 2024
137e2a7
멘토링 진행하면서 변경한 코드
JunilHwang Nov 23, 2024
c7e53da
동작 오류 수정
hmk20000 Nov 27, 2024
ebb295f
feature : 테스트 코드 실행 처리
hmk20000 Nov 29, 2024
600d674
테스트 코드 작성
hmk20000 Nov 29, 2024
51e3efd
CardNumber Input 분리
hmk20000 Dec 2, 2024
8f0983f
드로어 컴포넌트 분리
hmk20000 Dec 6, 2024
5fd343a
숫자 키패드 제작
hmk20000 Dec 6, 2024
25cc486
Input 컴포넌트들 분리
hmk20000 Dec 6, 2024
fda3908
포커스 처리
hmk20000 Dec 6, 2024
fe265c4
custom hook 분리
hmk20000 Dec 13, 2024
f56f18b
Storybook 추가
hmk20000 Dec 13, 2024
51835af
스토리북에 테일윈드 적용
hmk20000 Dec 13, 2024
0b044cb
Create useToggle.ts
hmk20000 Dec 20, 2024
bd6347a
커스텀 훅 분리
hmk20000 Dec 20, 2024
3948696
스토리 파일 추가
hmk20000 Dec 20, 2024
708d4cd
빌드용 코드 정리
hmk20000 Dec 26, 2024
1910861
vite로 library export 하기
hmk20000 Dec 27, 2024
b882f23
tailwind 제거
hmk20000 Dec 27, 2024
a8e30f5
Revert "tailwind 제거"
hmk20000 Jan 3, 2025
f3db93e
tailwind 포함해서 빌드하기
hmk20000 Jan 3, 2025
1cb1802
스토리북에 index 컴포넌트 추가
hmk20000 Jan 10, 2025
d76a94c
스토리북에 컴포넌트 멀티로 생성
hmk20000 Jan 10, 2025
735f27d
창이 열릴 때 스토리지 정보로 업데이트
hmk20000 Jan 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ dist-ssr
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?
*storybook.log
22 changes: 22 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
viteFinal: async (config) => {
return {
...config,
plugins: [...(config.plugins || [])],
};
},
};
export default config;
15 changes: 15 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '../src/index.css';

const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};

export default preview;
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,43 @@ yarn dev

```bash
yarn build
```
```

## 페이지 구성

### 메인 페이지

1. 상품 목록 페이지와 **결제하기 버튼**
1. 버튼 클릭시 모달창 오픈
1. 이후 프로젝트 진행은 모달창 내부에서 진행

### 결제 페이지

1. 캐러셀로 보유 카드를 보여줌
1. 카드가 없을 경우 **+ 버튼**
1. 카드가 있고 최초 진입시 마지막 등록 카드가 선택됨
1. 선택된 카드가 없을 경우 결제하기 비활성화

### 카드 목록 페이지

1. 보유한 카드 목록
1. 최근 입력이 상단
1. 추가 버튼은 최 하단

### 카드 추가 페이지

1. 카드 정보를 입력하면 상단 카드 UI에 실시간 반영
1. 카드번호
- 16자리
- 숫자만 입력 가능
- 4자리 마다 **[-]** 삽입
- 앞 8자리는 숫자, 나머지는 **[*]** 표시

### 카드 추가 완료 페이지

## 필요 컴포넌트

1. 캐러셀
2. 카드
3. 버튼
4. 인풋
5 changes: 1 addition & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payments Mission</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/reset.min.css"
/>
<link rel="stylesheet" href="/src/index.css" />
<link href="./dist/output.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
Expand Down
70 changes: 52 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,67 @@
{
"name": "react-payments",
"private": true,
"version": "0.0.0",
"version": "0.0.9",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"prebuild": "mkdir -p dist",
"build": "yarn build:css && tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest --watch",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build:css": "tailwindcss -i ./src/index.css -o ./dist/output.css"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"vite-plugin-dts": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.3",
"@chromatic-com/storybook": "3.2.3",
"@eslint/js": "^9.17.0",
"@storybook/addon-essentials": "8.4.7",
"@storybook/addon-interactions": "8.4.7",
"@storybook/addon-onboarding": "8.4.7",
"@storybook/blocks": "8.4.7",
"@storybook/react": "8.4.7",
"@storybook/react-vite": "8.4.7",
"@storybook/test": "8.4.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.1.0",
"@types/node": "^22.10.5",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.13.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.14",
"globals": "^15.11.0",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"eslint-plugin-storybook": "^0.11.2",
"globals": "^15.14.0",
"jsdom": "^26.0.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.15",
"typescript": "~5.6.2",
"typescript-eslint": "^8.11.0",
"vite": "^5.4.10"
}
"prettier": "^3.4.2",
"storybook": "8.4.7",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.3",
"typescript-eslint": "^8.19.1",
"vite": "^6.0.7",
"vitest": "^2.1.8"
},
"eslintConfig": {
"extends": [
"plugin:storybook/recommended",
"plugin:storybook/recommended"
]
},
"files": [
"dist",
"src"
],
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
2 changes: 1 addition & 1 deletion postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export default {
tailwindcss: {},
autoprefixer: {},
},
}
};
Binary file added react-payments-v0.0.0.tgz
Binary file not shown.
Binary file added react-payments-v0.0.5.tgz
Binary file not shown.
Binary file added react-payments-v0.0.6.tgz
Binary file not shown.
Binary file added react-payments-v0.0.7.tgz
Binary file not shown.
Binary file added react-payments-v0.0.8.tgz
Binary file not shown.
Binary file added react-payments-v0.0.9.tgz
Binary file not shown.
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

28 changes: 26 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import PaymentMain from './pages/PaymentMain';
import { useModal } from './contexts/hooks/useModal.tsx';
import { colors } from './constants/color.ts';

function App() {
const { setModal } = useModal();
return (
<div className="w-full h-screen flex items-center justify-center">
<div className="max-w-screen-md">결제 모듈 미션 Init</div>
<div
style={{
maxWidth: 'screen-md',
textAlign: 'center',
}}
>
<div>결제 모듈 미션</div>
<button
style={{
backgroundColor: colors.mint,
padding: '8px',
color: 'white',
marginTop: '20px',
border: 'none',
borderRadius: '10px',
cursor: 'pointer',
}}
onClick={() => setModal(<PaymentMain />)}
>
모달 열기
</button>
</div>
);
}
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/react';
import App from '../App';
import { useModal } from '../contexts/hooks/useModal';
import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('../contexts/hooks/useModal', () => ({
useModal: vi.fn(),
}));

describe('basic test', () => {
beforeEach(() => {
(useModal as Mock).mockReturnValue({
setModal: vi.fn(),
});
});

it('sample test', () => {
render(<App />);
const element = screen.getByText('모달 열기');
expect(element).toBeDefined();
});
});
38 changes: 38 additions & 0 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CardType } from '../storage/cardType.ts';

interface CardProps {
data?: CardType;
onClick?: () => void;
}
const Card = ({ data, onClick }: CardProps) => {
// 카드번호 별표 처리
const cardNumberMasked = (cardNumber: string) => {
// 공백으로 나누고 맨 앞 2개의 블록은 유지, 나머지는 *로 변경
const blocks = cardNumber.split(' ');
return blocks.map((block, index) => (index < 2 ? block : '****')).join(' ');
};

return (
<div
className={`flex flex-col justify-between w-[208px] h-[130px] rounded-lg p-2.5 text-[#575757] text-[14px] shadow-md shadow-gray-400 ${onClick ? 'cursor-pointer' : ''}`}
style={{ backgroundColor: data ? data.cardColor : '#d5d5d5' }}
onClick={onClick}
>
{data ? (
<>
<div className="text-left">{data.cardCompany}</div>
<div className={`w-10 h-[26px] rounded bg-[#cbba64]`} />
<div className="text-center">{cardNumberMasked(data.cardNumber)}</div>
<div className="flex justify-between">
<div>{data.userName}</div>
<div>{data.expiredDate}</div>
</div>
</>
) : (
<div className="m-auto text-3xl font-bold">+</div>
)}
</div>
);
};

export default Card;
88 changes: 88 additions & 0 deletions src/components/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { ReactNode, useState } from 'react';
import ArrowIcon from './icons/ArrowIcon';
let WIDTH = 280;
let SPACING = 28;
interface CarouselProps {
children: ReactNode | ReactNode[];
width?: number;
spacing?: number;
}
const Carousel = ({
children,
width = WIDTH,
spacing = SPACING,
}: CarouselProps) => {
WIDTH = width;
SPACING = spacing;
const [currentIndex, setCurrentIndex] = useState(0);
const nodes = React.Children.toArray(children);

const addIndex = () => {
if (currentIndex === nodes.length - 1) {
return;
} else {
setCurrentIndex(currentIndex + 1);
}
};

const subIndex = () => {
if (currentIndex === 0) {
return;
} else {
setCurrentIndex(currentIndex - 1);
}
};

return (
<div className="relative">
<div
className="relative h-[200px] flex bg-gray-100 w-full m-auto overflow-hidden text-center items-center"
style={{ maxWidth: width }}
>
{nodes.map((node, i) => (
<CarouselItem key={i} index={i} currentIndex={currentIndex}>
{node}
</CarouselItem>
))}
</div>
<button
className={`absolute top-1/2 left-2 ${currentIndex === 0 && 'opacity-50 cursor-not-allowed'}`}
onClick={subIndex}
disabled={currentIndex === 0}
>
<ArrowIcon alt="이전 카드" />
</button>
<button
className={`absolute top-1/2 right-2 rotate-180 ${currentIndex === nodes.length - 1 && 'opacity-50 cursor-not-allowed'}`}
onClick={addIndex}
disabled={currentIndex === nodes.length - 1}
>
<ArrowIcon alt="다음 카드" />
</button>
</div>
);
};

const CarouselItem = ({
index,
currentIndex,
children,
}: {
index: number;
currentIndex: number;
children: ReactNode;
}) => {
return (
<div
className="absolute transition-all duration-500 ease-in-out transform items-center flex justify-center"
style={{
width: WIDTH - SPACING * 2,
left: SPACING + (index - currentIndex) * (WIDTH - SPACING * 2),
}}
>
{children}
</div>
);
};

export default Carousel;
Loading
Loading