From 40469e2e2731803d8d5aa26bf3e0240d9270ae02 Mon Sep 17 00:00:00 2001 From: Woody <88191233+evencoding@users.noreply.github.com> Date: Wed, 3 May 2023 23:03:41 +0900 Subject: [PATCH] =?UTF-8?q?[=ED=8E=98=EC=9D=B4=EB=A8=BC=EC=B8=A0=20?= =?UTF-8?q?=EB=AF=B8=EC=85=98=20Step=202]=20=EC=9A=B0=EB=94=94(=EB=A5=98?= =?UTF-8?q?=EC=A0=95=EC=9A=B0)=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 범용적인 함수 utils 폴더로 분리 * refactor: Card 컴포넌트 내부의 any 타입 제거 * refactor: nullable하지 않은 데이터에서 옵셔널 체이닝 제거 * refactor(InputBox): nullable하지 않은 데이터에서 옵셔널 체이닝 제거 * refactor: CardRegisterPage를 typescript로 마이그레이션 * refactor: 보안 코드의 에러 메세지에 left:0 스타일 적용 * refactor: 만료일 전체에 대한 유효성 검사 제거 (임시) * feat: Input, Card, Header에 대한 스토리 북 구현 * feat: 등록한 카드 정보를 기억하기 위해 useLocalStorage 훅 구현 * refactor: localStorage 관련 로직을 hook이 아닌 utils로 변경 * refactor: utils 폴더로 분리 * refactor: 타입 정리 * refactor: Year 데이터를 렌더링 할 때, 불필요한 반복문 제거 * chore: 카드 회사 로고 이미지 폴더, 파일 생성 * chore: svg 모듈 declare 작성 * feat: 모달을 열고 닫는 기능 구현 * feat: 카드 회사 정보를 담은 constants 생성 * feat: 모달을 넣어주기 위한 Portal 구현 * feat: 카드사를 선택하는 컴포넌트의 템플릿 구현 * feat: 모달을 여는 클릭 이벤트 추가 * feat: 등록할 카드 정보에 대한 상태를 다루는 context 구현 * feat: 카드사의 정보를 표시 * refactor: 모달의 초기 상태를 외부에서 주입 받도록 변경 * feat: 카드의 닉네임을 표시 * feat: cardList에 대한 context 구현 * refactor: context파일 typescript로 마이그레이션 * refactor: useModalSwitch를 useSwitch로 범용성 있게 수정 * refactor: 폴더 내 파일 이름 구체화 * feat: overflow 히든 추가 * refactor: 폴더 구조 변경 및 폴더 별 내보내기 합치기 * refactor: useForm을 범용성 있게 변경 * refactor: cardInfo, cardList에 대한 Context 적용 * refactor: 상수에 타입 부여 * feat: modal-root 태그 추가 * chore: 파일 구조 변경 및 컴포넌트 파일 이름 구체화 * chore: react-hooks 테스팅 라이브러리 설치 * test: useSwitch hook 테스트 코드 작성 * chore: 불필요한 파일 삭제 * feat: form이 렌더링되면 input에 focus되도록 구현 * feat: form 화면에서 첫 input을 focus하는 기능 구현 * refactor: 닉네임 Input에 대한 스타일을 동적으로 변경하도록 변경 * chore: 폴더 구조 변경 * story: Card 컴포넌트에 대한 스토리 구현 * stody: Header 컴포넌트에 대한 스토리 구현 * stody: SelectBank 컴포넌트에 대한 스토리 구현 * feat: Input에 max-width 추가 * story: Input 컴포넌트에 대한 스토리 구현 * chore: import 개행 정리 * story: InputBox 컴포넌트들에 대한 스토리 구현 * story: 내보내기 이름 변경 * story: Page 컴포넌트에 대한 스토리 구현 * refactor: nullable하지 않은 객체에 대한 옵셔널 연산자 제거 * refactor: context에서 상태와 actions에 대한 타입 네로잉 * refactor: 카드사의 정보가 담긴 오브젝트에 반복문을 실행할 때 entries로 key와 value를 가져오도록 변경 * refactor: 삼항 연산자 대신 or, and 연산자를 사용하도록 변경 * refactor: 로컬 스토리지 로직을 custom hook으로 구현 * refactor: set 함수를 사용할 때, 이전 상태 값을 변경하도록 수정 * feat: errorOptions의 타입 지정 * feat: props.name이 nickname이 아닐 경우 border 지정 --- package-lock.json | 19 ++ package.json | 1 + public/index.html | 29 +-- src/App.tsx | 26 +-- src/GlobalStyles.ts | 9 +- src/__tests__/useSwitch.test.js | 22 ++ src/__tests__/validator.test.js | 66 ------ src/components/Card/Card.styled.ts | 30 ++- src/components/Card/Card.tsx | 52 +++++ src/components/Card/index.tsx | 51 ----- .../CardPreview/CardPreview.styled.ts | 2 + src/components/CardPreview/CardPreview.tsx | 21 ++ src/components/CardPreview/index.tsx | 18 -- .../CardRegisterForm.styled.ts | 10 + .../CardRegisterForm/CardRegisterForm.tsx | 159 +++++++++++++++ .../Form/NicknameForm/NicknameForm.styled.ts | 23 +++ .../Form/NicknameForm/NicknameForm.tsx | 51 +++++ src/components/Form/index.ts | 4 + .../Header/{index.tsx => Header.tsx} | 4 +- .../{InputBox => }/Input/Input.styled.ts | 16 +- .../Input/index.tsx => Input/Input.tsx} | 15 +- .../{index.tsx => CardNumbers.tsx} | 19 +- .../{index.tsx => ExpirationDate.tsx} | 7 +- .../OwnerName/{index.tsx => OwnerName.tsx} | 5 +- .../Password/{index.tsx => Password.tsx} | 7 +- .../SecurityNumbers/SecurityNumbers.styled.ts | 1 + .../{index.tsx => SecurityNumbers.tsx} | 7 +- src/components/InputBox/index.ts | 13 +- .../SelectBank/SelectBank.styled.ts | 45 +++++ src/components/SelectBank/SelectBank.tsx | 37 ++++ src/components/index.ts | 7 + .../CardRegisterPage.styled.ts | 22 ++ .../CardRegisterPage/CardRegisterPage.tsx | 39 ++++ .../pages/MyCardPage/MyCardPage.styled.ts | 17 +- .../pages/MyCardPage/MyCardPage.tsx | 32 +++ src/components/pages/index.ts | 4 + src/components/portal/Modal/Modal.styled.ts | 22 ++ src/components/portal/Modal/Modal.tsx | 29 +++ src/components/portal/index.ts | 3 + src/components/styles/index.ts | 15 ++ src/constants/index.ts | 63 ++++++ src/context/CardInfoContext.tsx | 76 +++++++ src/context/CardListContext.tsx | 45 +++++ src/domain/validator.ts | 18 -- src/hooks/useForm.ts | 119 ++++++----- src/hooks/useLocalStorage.ts | 21 ++ src/hooks/useSwitch.ts | 17 ++ src/images/bc_logo.svg | 9 + src/images/hana_logo.svg | 9 + src/images/hyundai_logo.svg | 9 + src/images/index.ts | 10 + src/images/kakao_logo.svg | 9 + src/images/kookmin_logo.svg | 9 + src/images/lotte_logo.svg | 9 + src/images/sinhan_logo.svg | 9 + src/images/woori_logo.svg | 9 + src/index.tsx | 9 +- .../CardRegisterPage.styled.ts | 25 --- src/pages/CardRegisterPage/index.jsx | 189 ------------------ src/pages/MyCardPage/index.tsx | 38 ---- src/stories/CardNumberBox.stories.ts | 16 -- src/stories/Input.stories.ts | 45 ----- src/stories/components/Card.stories.ts | 82 ++++++++ src/stories/components/Header.stories.tsx | 37 ++++ src/stories/components/Input.stories.tsx | 84 ++++++++ .../InputBox/CardNumbers.stories.tsx | 100 +++++++++ .../InputBox/ExpirationDate.stories.tsx | 74 +++++++ .../components/InputBox/OwnerName.stories.tsx | 62 ++++++ .../components/InputBox/Password.stories.tsx | 71 +++++++ .../InputBox/SecurityNumbers.stories.tsx | 62 ++++++ src/stories/components/SelectBank.stories.tsx | 38 ++++ .../pages/CardRegisterPage.stories.tsx | 35 ++++ .../components/pages/MyCardPage.stories.tsx | 32 +++ src/types/InputBox.ts | 2 +- src/types/card.ts | 3 +- src/types/svg.d.ts | 1 + tsconfig.json | 11 +- 77 files changed, 1776 insertions(+), 640 deletions(-) create mode 100644 src/__tests__/useSwitch.test.js delete mode 100644 src/__tests__/validator.test.js create mode 100644 src/components/Card/Card.tsx delete mode 100644 src/components/Card/index.tsx create mode 100644 src/components/CardPreview/CardPreview.tsx delete mode 100644 src/components/CardPreview/index.tsx create mode 100644 src/components/Form/CardRegisterForm/CardRegisterForm.styled.ts create mode 100644 src/components/Form/CardRegisterForm/CardRegisterForm.tsx create mode 100644 src/components/Form/NicknameForm/NicknameForm.styled.ts create mode 100644 src/components/Form/NicknameForm/NicknameForm.tsx create mode 100644 src/components/Form/index.ts rename src/components/Header/{index.tsx => Header.tsx} (100%) rename src/components/{InputBox => }/Input/Input.styled.ts (63%) rename src/components/{InputBox/Input/index.tsx => Input/Input.tsx} (67%) rename src/components/InputBox/CardNumbers/{index.tsx => CardNumbers.tsx} (62%) rename src/components/InputBox/ExpirationDate/{index.tsx => ExpirationDate.tsx} (73%) rename src/components/InputBox/OwnerName/{index.tsx => OwnerName.tsx} (87%) rename src/components/InputBox/Password/{index.tsx => Password.tsx} (72%) rename src/components/InputBox/SecurityNumbers/{index.tsx => SecurityNumbers.tsx} (77%) create mode 100644 src/components/SelectBank/SelectBank.styled.ts create mode 100644 src/components/SelectBank/SelectBank.tsx create mode 100644 src/components/index.ts create mode 100644 src/components/pages/CardRegisterPage/CardRegisterPage.styled.ts create mode 100644 src/components/pages/CardRegisterPage/CardRegisterPage.tsx rename src/{ => components}/pages/MyCardPage/MyCardPage.styled.ts (73%) create mode 100644 src/components/pages/MyCardPage/MyCardPage.tsx create mode 100644 src/components/pages/index.ts create mode 100644 src/components/portal/Modal/Modal.styled.ts create mode 100644 src/components/portal/Modal/Modal.tsx create mode 100644 src/components/portal/index.ts create mode 100644 src/components/styles/index.ts create mode 100644 src/constants/index.ts create mode 100644 src/context/CardInfoContext.tsx create mode 100644 src/context/CardListContext.tsx create mode 100644 src/hooks/useLocalStorage.ts create mode 100644 src/hooks/useSwitch.ts create mode 100644 src/images/bc_logo.svg create mode 100644 src/images/hana_logo.svg create mode 100644 src/images/hyundai_logo.svg create mode 100644 src/images/index.ts create mode 100644 src/images/kakao_logo.svg create mode 100644 src/images/kookmin_logo.svg create mode 100644 src/images/lotte_logo.svg create mode 100644 src/images/sinhan_logo.svg create mode 100644 src/images/woori_logo.svg delete mode 100644 src/pages/CardRegisterPage/CardRegisterPage.styled.ts delete mode 100644 src/pages/CardRegisterPage/index.jsx delete mode 100644 src/pages/MyCardPage/index.tsx delete mode 100644 src/stories/CardNumberBox.stories.ts delete mode 100644 src/stories/Input.stories.ts create mode 100644 src/stories/components/Card.stories.ts create mode 100644 src/stories/components/Header.stories.tsx create mode 100644 src/stories/components/Input.stories.tsx create mode 100644 src/stories/components/InputBox/CardNumbers.stories.tsx create mode 100644 src/stories/components/InputBox/ExpirationDate.stories.tsx create mode 100644 src/stories/components/InputBox/OwnerName.stories.tsx create mode 100644 src/stories/components/InputBox/Password.stories.tsx create mode 100644 src/stories/components/InputBox/SecurityNumbers.stories.tsx create mode 100644 src/stories/components/SelectBank.stories.tsx create mode 100644 src/stories/components/pages/CardRegisterPage.stories.tsx create mode 100644 src/stories/components/pages/MyCardPage.stories.tsx create mode 100644 src/types/svg.d.ts diff --git a/package-lock.json b/package-lock.json index 69375b86c5..8ce52bfe42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3935,6 +3935,16 @@ "@types/react-dom": "^18.0.0" } }, + "@testing-library/react-hooks": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz", + "integrity": "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "react-error-boundary": "^3.1.0" + } + }, "@testing-library/user-event": { "version": "13.5.0", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", @@ -12525,6 +12535,15 @@ } } }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/package.json b/package.json index 4c5590bb8e..bd8bc942fb 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@storybook/react-webpack5": "7.0.6", "@storybook/testing-library": "^0.0.14-next.2", "@types/styled-components": "5.1.26", + "@testing-library/react-hooks": "^8.0.1", "@types/uuid": "^9.0.1", "babel-plugin-named-exports-order": "0.0.2", "eslint-config-prettier": "8.8.0", diff --git a/public/index.html b/public/index.html index aa069f27cb..276e236815 100644 --- a/public/index.html +++ b/public/index.html @@ -5,39 +5,14 @@ - + - - React App
- + diff --git a/src/App.tsx b/src/App.tsx index 020720cc84..492f91ea53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,32 +1,14 @@ -import { useState } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { v4 as uuid } from 'uuid'; -import MyCardPage from './pages/MyCardPage'; -import CardRegisterPage from './pages/CardRegisterPage'; -import { CardInfo } from './types/card'; -const App = () => { - const [cardList, setCardList] = useState([]); - - const onChangeCardList = (values: any) => { - setCardList((prev) => [ - { - id: uuid, - ...values, - }, - ...prev, - ]); - }; +import { MyCardPage, CardRegisterPage } from './components/pages'; +const App = () => { return (
- } /> - } - /> + } /> + } />
diff --git a/src/GlobalStyles.ts b/src/GlobalStyles.ts index 277e7db106..7d4bfffa9a 100644 --- a/src/GlobalStyles.ts +++ b/src/GlobalStyles.ts @@ -5,7 +5,6 @@ const GlobalStyles = createGlobalStyle` ${reset} body{ - padding:28px 24px; margin:0 auto; max-width: 600px; height:100%; @@ -33,6 +32,14 @@ const GlobalStyles = createGlobalStyle` padding: 0; cursor: pointer; } + + .App { + padding:28px 24px; + } + + .overflowHidden { + overflow: hidden; + }; `; export default GlobalStyles; diff --git a/src/__tests__/useSwitch.test.js b/src/__tests__/useSwitch.test.js new file mode 100644 index 0000000000..16fc3844bb --- /dev/null +++ b/src/__tests__/useSwitch.test.js @@ -0,0 +1,22 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-dom/test-utils'; + +import useSwitch from '../hooks/useSwitch'; + +test('useSwitch hook 테스트', () => { + // useSwitch 훅을 사용한다. 초기 상태 값을 false로 설정한다 + const { result } = renderHook(() => useSwitch(false)); + + // 상태 값의 초기 상태 값은 false이다 + expect(result.current.state).toBe(false); + + // state의 상태 값을 true로 변경한다 + act(() => result.current.turnOn()); + + expect(result.current.state).toBe(true); + + // state의 상태 값을 false로 변경한다 + act(() => result.current.turnOff()); + + expect(result.current.state).toBe(false); +}); diff --git a/src/__tests__/validator.test.js b/src/__tests__/validator.test.js deleted file mode 100644 index c90a0aa035..0000000000 --- a/src/__tests__/validator.test.js +++ /dev/null @@ -1,66 +0,0 @@ -import { - validateNumeric, - validateMonth, - validateExpirationDate, - validateValidUserName, -} from '../domain/validator'; - -describe('', () => { - it.each(['1234', '12345678', '3'])('숫자로만 이루어진 값은 에러를 반환하지 않는다.', (value) => { - expect(() => validateNumeric(value)).not.toThrow(); - }); - - it.each([' ', 'a', ''])('숫자가 아닌 값은 에러를 반환한다.', (value) => { - expect(() => validateNumeric(value)).toThrow(); - }); - - it.each([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])( - '만료일의 MM은 1 ~ 12 사이의 숫자로 이루어져 있다.', - (value) => { - expect(() => validateMonth(value)).not.toThrow(); - } - ); - - it.each([0, 13])('만료일의 MM이 1 ~ 12 외의 값이면 에러를 반환한다.', (value) => { - expect(() => validateMonth(value)).toThrow(); - }); - - it.each([ - { expirationYear: 23, expirationMonth: 4 }, - { expirationYear: 23, expirationMonth: 5 }, - { expirationYear: 23, expirationMonth: 6 }, - { expirationYear: 23, expirationMonth: 12 }, - { expirationYear: 30, expirationMonth: 1 }, - ])('만료일은 이번년도, 이번달 이상이다.', ({ expirationYear, expirationMonth }) => { - expect(() => validateExpirationDate(expirationYear, expirationMonth)).not.toThrow(); - }); - - it.each([ - { expirationYear: 23, expirationMonth: 3 }, - { expirationYear: 23, expirationMonth: 2 }, - { expirationYear: 23, expirationMonth: 1 }, - { expirationYear: 22, expirationMonth: 12 }, - { expirationYear: 22, expirationMonth: 1 }, - ])('만료일이 이번달 미만이면 에러를 반환한다.', ({ expirationYear, expirationMonth }) => { - expect(() => validateExpirationDate(expirationYear, expirationMonth)).toThrow(); - }); - - it.each(['woody', 'ice coffee', '', 'AbcdeAbcdeAbcdeAbcdeAbcdeAbcde'])( - '이름은 30글지 이하의 영문과 공백으로 이루어져 있다.', - (value) => { - expect(() => validateValidUserName(value)).not.toThrow(); - } - ); - - it.each([ - ' ice coffee', - '우디', - '아커', - 'AbcdeAbcdeAbcdeAbcdeAbcdeAbcdeA', - 'ice coffee ', - '$', - '1', - ])('올바르지 않은 이름은 에러를 반환한다.', (value) => { - expect(() => validateValidUserName(value)).toThrow(); - }); -}); diff --git a/src/components/Card/Card.styled.ts b/src/components/Card/Card.styled.ts index 9e539ac3fe..1d70a2cc1f 100644 --- a/src/components/Card/Card.styled.ts +++ b/src/components/Card/Card.styled.ts @@ -1,24 +1,25 @@ import styled from 'styled-components'; -export const Card = styled.div` +interface CardProps { + bgColor?: string; + color?: string; +} + +export const Card = styled.div` position: relative; width: 240px; height: 150px; - padding: 12px 18px; + padding: 15px 18px; - background-color: ${(props: any) => props.bgColor}; - color: white; + background-color: ${(props) => props.bgColor || 'black'}; + color: ${(props) => props.color || 'white'}; box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); border-radius: 5px; letter-spacing: 1.5px; - - &:not(:last-child) { - margin-bottom: 50px; - } `; export const Rectangle = styled.div` @@ -36,11 +37,18 @@ export const Rectangle = styled.div` export const CardInfos = styled.div` display: flex; flex-direction: column; - justify-content: flex-end; + justify-content: space-between; height: 100%; `; +export const BankName = styled.div` + font-size: 12px; + font-weight: 400; +`; + +export const Bottom = styled.div``; + export const CardNumbers = styled.div` display: flex; justify-content: space-between; @@ -67,12 +75,12 @@ export const Ellipse = styled.div` border-radius: 50%; - background-color: white; + background-color: ${(props) => props.color || 'white'}; margin-right: 5px; `; -export const CardBottomInfos = styled.div` +export const ExtraInfos = styled.div` display: flex; justify-content: space-between; align-items: end; diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx new file mode 100644 index 0000000000..d35ce7f18b --- /dev/null +++ b/src/components/Card/Card.tsx @@ -0,0 +1,52 @@ +import * as styled from './Card.styled'; + +import { BANKS } from '../../constants'; + +import { CardInfo } from '../../types/card'; + +interface CardProps { + cardInfo: CardInfo; +} + +const Card = ({ cardInfo }: CardProps) => { + return ( + + + + {BANKS[cardInfo.bank]?.name} + + +
{cardInfo.firstCardNumbers}
+
{cardInfo.secondCardNumbers}
+ + {Array.from({ length: cardInfo.thirdCardNumbers.length }).map((_, index) => ( + + ))} + + + {Array.from({ length: cardInfo.fourthCardNumbers.length }).map((_, index) => ( + + ))} + +
+ + + {cardInfo.ownerName ? cardInfo.ownerName : 'NAME'} + + + + {cardInfo.expirationMonth ? cardInfo.expirationMonth : 'MM'} + + / + + {cardInfo.expirationYear || 'YY'} + + + +
+
+
+ ); +}; + +export default Card; diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx deleted file mode 100644 index 6ff96bb9da..0000000000 --- a/src/components/Card/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { CardInfo } from '../../types/card'; -import * as styled from './Card.styled'; - -interface CardProps { - cardInfo: CardInfo; - bgColor?: string; -} - -const Card = ({ cardInfo, bgColor }: CardProps) => { - return ( - - - - -
{cardInfo?.firstCardNumbers}
-
{cardInfo?.secondCardNumbers}
- - {Array.from({ length: cardInfo?.thirdCardNumbers.length }).map((_, index) => ( - - ))} - - - {Array.from({ length: cardInfo?.fourthCardNumbers.length }).map((_, index) => ( - - ))} - -
- - - {cardInfo?.ownerName ? cardInfo?.ownerName : 'NAME'} - - - - {[cardInfo?.expirationMonth ? cardInfo?.expirationMonth : 'MM'].map((char, index) => ( - {char} - ))} - - / - - {[cardInfo?.expirationYear ? cardInfo?.expirationYear : 'YY'].map((char, index) => ( - {char} - ))} - - - -
-
- ); -}; - -export default Card; diff --git a/src/components/CardPreview/CardPreview.styled.ts b/src/components/CardPreview/CardPreview.styled.ts index af6e5ead46..b287a9a8c8 100644 --- a/src/components/CardPreview/CardPreview.styled.ts +++ b/src/components/CardPreview/CardPreview.styled.ts @@ -5,4 +5,6 @@ export const CardPreview = styled.div` justify-content: center; margin: 30px 0; + + cursor: pointer; `; diff --git a/src/components/CardPreview/CardPreview.tsx b/src/components/CardPreview/CardPreview.tsx new file mode 100644 index 0000000000..eaccccd5f7 --- /dev/null +++ b/src/components/CardPreview/CardPreview.tsx @@ -0,0 +1,21 @@ +import * as styled from './CardPreview.styled'; + +import { useCardInfoValue } from '../../context/CardInfoContext'; + +import { Card } from '../'; + +interface CardPreviewProps { + openModal: () => void; +} + +const CardPreview = ({ openModal }: CardPreviewProps) => { + const cardInfo = useCardInfoValue(); + + return ( + + + + ); +}; + +export default CardPreview; diff --git a/src/components/CardPreview/index.tsx b/src/components/CardPreview/index.tsx deleted file mode 100644 index cfaf71c256..0000000000 --- a/src/components/CardPreview/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { CardInfo } from '../../types/card'; -import Card from '../Card'; -import * as styled from './CardPreview.styled'; - -interface CardPreviewProps { - cardInfo: CardInfo; - bgColor?: string; -} - -const CardPreview = ({ cardInfo, bgColor }: CardPreviewProps) => { - return ( - - - - ); -}; - -export default CardPreview; diff --git a/src/components/Form/CardRegisterForm/CardRegisterForm.styled.ts b/src/components/Form/CardRegisterForm/CardRegisterForm.styled.ts new file mode 100644 index 0000000000..f610fdfafe --- /dev/null +++ b/src/components/Form/CardRegisterForm/CardRegisterForm.styled.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components'; +import { SubmitButton } from '../../styles'; + +export const CardRegisterForm = styled.form` + & > div:not(:last-child) { + margin-bottom: 32px; + } +`; + +export const CardRegisterButton = styled(SubmitButton)``; diff --git a/src/components/Form/CardRegisterForm/CardRegisterForm.tsx b/src/components/Form/CardRegisterForm/CardRegisterForm.tsx new file mode 100644 index 0000000000..225995719c --- /dev/null +++ b/src/components/Form/CardRegisterForm/CardRegisterForm.tsx @@ -0,0 +1,159 @@ +import * as styled from './CardRegisterForm.styled'; + +import { useCardInfoActions, useCardInfoValue } from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; + +import { Input } from '../../'; +import { CardNumbers, ExpirationDate, OwnerName, Password, SecurityNumbers } from '../../InputBox'; + +import validator from '../../../domain/validator'; +import { useEffect, useRef } from 'react'; + +interface Props { + showModal: boolean; + turnToNicknameForm: () => void; +} + +const CardRegisterForm = ({ showModal, turnToNicknameForm }: Props) => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const firstInputRef = useRef(null); + + useEffect(() => { + if (showModal) return; + + firstInputRef.current?.focus(); + }, [showModal]); + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => turnToNicknameForm(), + changeAction: (name: string, value: string) => { + setCardInfo((prev) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + firstCardNumbers: '', + secondCardNumbers: '', + thirdCardNumbers: '', + fourthCardNumbers: '', + expirationMonth: '', + expirationYear: '', + ownerName: '', + securityNumbers: '', + firstPassword: '', + secondPassword: '', + }, + validator, + }, + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + 다음 + + ); +}; + +export default CardRegisterForm; diff --git a/src/components/Form/NicknameForm/NicknameForm.styled.ts b/src/components/Form/NicknameForm/NicknameForm.styled.ts new file mode 100644 index 0000000000..85bb0b18d0 --- /dev/null +++ b/src/components/Form/NicknameForm/NicknameForm.styled.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +import { SubmitButton } from '../../styles'; + +export const NicknameForm = styled.form` + margin-top: 120px; +`; + +export const NicknameInput = styled.input` + width: 100%; + border: none; + padding-bottom: 8px; + border-bottom: 2px solid #383838; + + font-size: 22px; + font-weight: 500; + + color: #383838; + + text-align: center; +`; + +export const NicknameSubmitButton = styled(SubmitButton)``; diff --git a/src/components/Form/NicknameForm/NicknameForm.tsx b/src/components/Form/NicknameForm/NicknameForm.tsx new file mode 100644 index 0000000000..364dd336da --- /dev/null +++ b/src/components/Form/NicknameForm/NicknameForm.tsx @@ -0,0 +1,51 @@ +import { useNavigate } from 'react-router-dom'; +import { useEffect, useRef } from 'react'; + +import * as styled from './NicknameForm.styled'; + +import { useCardInfoActions, useCardInfoValue } from '../../../context/CardInfoContext'; +import { useCardListActions } from '../../../context/CardListContext'; +import useForm from '../../../hooks/useForm'; + +import { Input } from '../../'; + +const NicknameForm = () => { + const navigate = useNavigate(); + const [cardInfo, { setCardInfo, initCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + const { setCardList } = useCardListActions(); + + const nicknameInputRef = useRef(null); + + useEffect(() => { + nicknameInputRef.current?.focus(); + }, []); + + const { onChange, onSubmit } = useForm({ + submitAction: () => { + setCardList((prev) => [cardInfo, ...prev]); + navigate('/'); + initCardInfo(); + }, + changeAction: (name: string, value: string) => { + setCardInfo((prev) => ({ ...prev, [name]: value })); + }, + }); + + return ( + + + 완료 + + ); +}; + +export default NicknameForm; diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts new file mode 100644 index 0000000000..b1a6aaa254 --- /dev/null +++ b/src/components/Form/index.ts @@ -0,0 +1,4 @@ +import CardRegisterForm from './CardRegisterForm/CardRegisterForm'; +import NicknameForm from './NicknameForm/NicknameForm'; + +export { CardRegisterForm, NicknameForm }; diff --git a/src/components/Header/index.tsx b/src/components/Header/Header.tsx similarity index 100% rename from src/components/Header/index.tsx rename to src/components/Header/Header.tsx index 17611b7c93..8332228135 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/Header.tsx @@ -1,7 +1,7 @@ -import * as styled from './Header.styled'; - import { useLocation, useNavigate } from 'react-router-dom'; +import * as styled from './Header.styled'; + const Header = () => { const { pathname } = useLocation(); const navigate = useNavigate(); diff --git a/src/components/InputBox/Input/Input.styled.ts b/src/components/Input/Input.styled.ts similarity index 63% rename from src/components/InputBox/Input/Input.styled.ts rename to src/components/Input/Input.styled.ts index 68c09e612c..ed2237c5e0 100644 --- a/src/components/InputBox/Input/Input.styled.ts +++ b/src/components/Input/Input.styled.ts @@ -1,6 +1,7 @@ import styled from 'styled-components'; interface InputProps { + name: string; maxLength: number; center?: boolean; } @@ -10,26 +11,29 @@ const InputWidth: { [key: string]: string } = { 2: '70px', 3: '80px', 4: '100%', + 8: '100%', 30: '100%', }; export const Input = styled.input` width: ${(props) => InputWidth[props.maxLength]}; + max-width: calc(600px - 56px); height: 45px; - background-color: #e5e5e5; + padding: ${(props) => props.name === 'ownerName' && '0 16px'}; + + background-color: ${(props) => props.name !== 'nickname' && '#e5e5e5'}; - border-radius: 10px; border: none; + border-radius: ${(props) => props.name !== 'nickname' && '10px'}; + border-bottom: ${(props) => props.name === 'nickname' && '2px solid black'}; font-weight: 600; font-size: ${(props) => (props.type === 'password' ? '24px' : '18px')}; - letter-spacing: 2px; text-align: ${(props) => props.center && 'center'}; - - padding: ${(props) => props.name === 'ownerName' && '0 16px'}; + letter-spacing: 2px; &:focus { - border: 2px solid #0078ff; + border: ${(props) => props.name !== 'nickname' && '2px solid #0078ff;'}; } `; diff --git a/src/components/InputBox/Input/index.tsx b/src/components/Input/Input.tsx similarity index 67% rename from src/components/InputBox/Input/index.tsx rename to src/components/Input/Input.tsx index b237d7e0bc..9520924e48 100644 --- a/src/components/InputBox/Input/index.tsx +++ b/src/components/Input/Input.tsx @@ -1,19 +1,19 @@ +import React, { ChangeEvent, forwardRef } from 'react'; + import * as styled from './Input.styled'; -import { ChangeEvent, forwardRef, Ref } from 'react'; interface InputProps { name: string; + type: string; value: string; - onChange: (event: ChangeEvent) => void; maxLength: number; + onChange: (event: ChangeEvent) => void; + numeric?: boolean; placeholder?: string; center?: boolean; - type: string; - dataType: string; - dataNext: string; } -const Input = forwardRef((props: InputProps, ref: Ref): JSX.Element => { +const Input = forwardRef((props: InputProps, ref: React.Ref) => { return ( ): JSX.El placeholder={props.placeholder} center={props.center} type={props.type} - data-type={props.dataType} - data-next={props.dataNext} + data-numeric={props.numeric} /> ); }); diff --git a/src/components/InputBox/CardNumbers/index.tsx b/src/components/InputBox/CardNumbers/CardNumbers.tsx similarity index 62% rename from src/components/InputBox/CardNumbers/index.tsx rename to src/components/InputBox/CardNumbers/CardNumbers.tsx index 2b17ed0b38..b7551fa678 100644 --- a/src/components/InputBox/CardNumbers/index.tsx +++ b/src/components/InputBox/CardNumbers/CardNumbers.tsx @@ -1,6 +1,7 @@ -import { InputBoxProps } from '../../../types/InputBox'; import * as styled from './CardNumbers.styled'; +import { InputBoxProps } from '../../../types/InputBox'; + const CardNumbers = ({ children, error }: InputBoxProps) => { return ( @@ -8,15 +9,15 @@ const CardNumbers = ({ children, error }: InputBoxProps) => { 카드 번호 {children} - {(error?.firstCardNumbers || - error?.secondCardNumbers || - error?.thirdCardNumbers || - error?.fourthCardNumbers) && ( + {(error.firstCardNumbers || + error.secondCardNumbers || + error.thirdCardNumbers || + error.fourthCardNumbers) && ( - {error?.firstCardNumbers || - error?.secondCardNumbers || - error?.thirdCardNumbers || - error?.fourthCardNumbers} + {error.firstCardNumbers || + error.secondCardNumbers || + error.thirdCardNumbers || + error.fourthCardNumbers} )} diff --git a/src/components/InputBox/ExpirationDate/index.tsx b/src/components/InputBox/ExpirationDate/ExpirationDate.tsx similarity index 73% rename from src/components/InputBox/ExpirationDate/index.tsx rename to src/components/InputBox/ExpirationDate/ExpirationDate.tsx index d6f4d326b2..856f05b359 100644 --- a/src/components/InputBox/ExpirationDate/index.tsx +++ b/src/components/InputBox/ExpirationDate/ExpirationDate.tsx @@ -1,6 +1,7 @@ -import { InputBoxProps } from '../../../types/InputBox'; import * as styled from './ExpirationDate.styled'; +import { InputBoxProps } from '../../../types/InputBox'; + const ExpirationDate = ({ children, error }: InputBoxProps) => { return ( @@ -8,8 +9,8 @@ const ExpirationDate = ({ children, error }: InputBoxProps) => { 만료일 {children} - {(error?.expirationMonth || error?.expirationYear) && ( - {error?.expirationMonth || error?.expirationYear} + {(error.expirationMonth || error.expirationYear) && ( + {error.expirationMonth || error.expirationYear} )} ); diff --git a/src/components/InputBox/OwnerName/index.tsx b/src/components/InputBox/OwnerName/OwnerName.tsx similarity index 87% rename from src/components/InputBox/OwnerName/index.tsx rename to src/components/InputBox/OwnerName/OwnerName.tsx index fcdfb90c56..01730e28a0 100644 --- a/src/components/InputBox/OwnerName/index.tsx +++ b/src/components/InputBox/OwnerName/OwnerName.tsx @@ -1,6 +1,7 @@ -import { InputBoxProps } from '../../../types/InputBox'; import * as styled from './OwnerName.styled'; +import { InputBoxProps } from '../../../types/InputBox'; + interface OwnerNameProps extends InputBoxProps { ownerName: string; maxLength: number; @@ -18,7 +19,7 @@ const OwnerName = ({ ownerName, maxLength, children, error }: OwnerNameProps) => {children} - {error?.ownerName && {error?.ownerName}} + {error.ownerName && {error.ownerName}} ); }; diff --git a/src/components/InputBox/Password/index.tsx b/src/components/InputBox/Password/Password.tsx similarity index 72% rename from src/components/InputBox/Password/index.tsx rename to src/components/InputBox/Password/Password.tsx index 993e2319c2..cfa45a738c 100644 --- a/src/components/InputBox/Password/index.tsx +++ b/src/components/InputBox/Password/Password.tsx @@ -1,6 +1,7 @@ -import { InputBoxProps } from '../../../types/InputBox'; import * as styled from './Password.styled'; +import { InputBoxProps } from '../../../types/InputBox'; + const Password = ({ children, error }: InputBoxProps) => { return ( @@ -8,8 +9,8 @@ const Password = ({ children, error }: InputBoxProps) => { 카드 비밀번호 {children} - {(error?.firstPassword || error?.secondPassword) && ( - {error?.firstPassword || error?.secondPassword} + {(error.firstPassword || error.secondPassword) && ( + {error.firstPassword || error.secondPassword} )} ); diff --git a/src/components/InputBox/SecurityNumbers/SecurityNumbers.styled.ts b/src/components/InputBox/SecurityNumbers/SecurityNumbers.styled.ts index a11cdd15f1..f90b461b63 100644 --- a/src/components/InputBox/SecurityNumbers/SecurityNumbers.styled.ts +++ b/src/components/InputBox/SecurityNumbers/SecurityNumbers.styled.ts @@ -16,6 +16,7 @@ export const LabelHeader = styled.div` export const ErrorMessage = styled.span` position: absolute; bottom: -20px; + left: 0; color: red; diff --git a/src/components/InputBox/SecurityNumbers/index.tsx b/src/components/InputBox/SecurityNumbers/SecurityNumbers.tsx similarity index 77% rename from src/components/InputBox/SecurityNumbers/index.tsx rename to src/components/InputBox/SecurityNumbers/SecurityNumbers.tsx index 75f699f936..b796e132b0 100644 --- a/src/components/InputBox/SecurityNumbers/index.tsx +++ b/src/components/InputBox/SecurityNumbers/SecurityNumbers.tsx @@ -1,6 +1,7 @@ -import { InputBoxProps } from '../../../types/InputBox'; import * as styled from './SecurityNumbers.styled'; +import { InputBoxProps } from '../../../types/InputBox'; + const SecurityNumbers = ({ children, error }: InputBoxProps) => { return ( @@ -8,9 +9,7 @@ const SecurityNumbers = ({ children, error }: InputBoxProps) => { 보안 코드(CVC/CVV) {children} - {error?.securityNumbers && ( - {error?.securityNumbers} - )} + {error.securityNumbers && {error.securityNumbers}} ); }; diff --git a/src/components/InputBox/index.ts b/src/components/InputBox/index.ts index 13ec8f7bbc..5d8f94737f 100644 --- a/src/components/InputBox/index.ts +++ b/src/components/InputBox/index.ts @@ -1,8 +1,7 @@ -import Input from './Input'; -import CardNumbers from './CardNumbers'; -import ExpirationDate from './ExpirationDate'; -import OwnerName from './OwnerName'; -import Password from './Password'; -import SecurityNumbers from './SecurityNumbers'; +import CardNumbers from './CardNumbers/CardNumbers'; +import ExpirationDate from './ExpirationDate/ExpirationDate'; +import OwnerName from './OwnerName/OwnerName'; +import Password from './Password/Password'; +import SecurityNumbers from './SecurityNumbers/SecurityNumbers'; -export { Input, CardNumbers, ExpirationDate, OwnerName, Password, SecurityNumbers }; +export { CardNumbers, ExpirationDate, OwnerName, Password, SecurityNumbers }; diff --git a/src/components/SelectBank/SelectBank.styled.ts b/src/components/SelectBank/SelectBank.styled.ts new file mode 100644 index 0000000000..0c97ad053a --- /dev/null +++ b/src/components/SelectBank/SelectBank.styled.ts @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +export const SelectBank = styled.div` + position: fixed; + bottom: 0; + + display: flex; + justify-content: center; + + width: 100%; + height: 227px; + + z-index: 1; + + background-color: white; +`; + +export const Banks = styled.div` + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-row-gap: 26px; + grid-column-gap: 36px; + + padding: 34px 50px 40px 50px; + + width: 100%; + max-width: 600px; +`; + +export const Bank = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + cursor: pointer; +`; + +export const Icon = styled.div` + margin-bottom: 10px; +`; + +export const Name = styled.div` + font-size: 13px; + color: #525252; +`; diff --git a/src/components/SelectBank/SelectBank.tsx b/src/components/SelectBank/SelectBank.tsx new file mode 100644 index 0000000000..8b1bfed0ca --- /dev/null +++ b/src/components/SelectBank/SelectBank.tsx @@ -0,0 +1,37 @@ +import { MouseEvent } from 'react'; + +import * as styled from './SelectBank.styled'; + +import { useCardInfoActions } from '../../context/CardInfoContext'; + +import { BANKS } from '../../constants'; + +interface Props { + closeModal: () => void; +} + +const SelectBank = ({ closeModal }: Props) => { + const { setCardInfo } = useCardInfoActions(); + + const onClick = ({ currentTarget: { id } }: MouseEvent) => { + setCardInfo((prev) => ({ ...prev, bank: id })); + closeModal(); + }; + + return ( + + + {Object.entries(BANKS).map(([key, bank]) => ( + + + {`${bank.name}_logo`} + + {bank.name} + + ))} + + + ); +}; + +export default SelectBank; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000000..ce501dad57 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,7 @@ +import Card from './Card/Card'; +import CardPreview from './CardPreview/CardPreview'; +import Header from './Header/Header'; +import SelectBank from './SelectBank/SelectBank'; +import Input from './Input/Input'; + +export { Card, CardPreview, Header, SelectBank, Input }; diff --git a/src/components/pages/CardRegisterPage/CardRegisterPage.styled.ts b/src/components/pages/CardRegisterPage/CardRegisterPage.styled.ts new file mode 100644 index 0000000000..9313698675 --- /dev/null +++ b/src/components/pages/CardRegisterPage/CardRegisterPage.styled.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +export const CardRegisterPage = styled.div` + position: relative; + + height: calc(100vh - 56px); +`; + +export const NicknameFormContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + height: 100%; +`; + +export const Title = styled.div` + font-size: 24px; + + margin-bottom: 35px; +`; diff --git a/src/components/pages/CardRegisterPage/CardRegisterPage.tsx b/src/components/pages/CardRegisterPage/CardRegisterPage.tsx new file mode 100644 index 0000000000..c9b7a56f45 --- /dev/null +++ b/src/components/pages/CardRegisterPage/CardRegisterPage.tsx @@ -0,0 +1,39 @@ +import * as styled from './CardRegisterPage.styled'; + +import { useCardInfoValue } from '../../../context/CardInfoContext'; +import useSwitch from '../../../hooks/useSwitch'; + +import { Header, CardPreview, Card, SelectBank } from '../../'; +import { CardRegisterForm, NicknameForm } from '../../Form'; +import { Modal } from '../../portal'; + +const CardRegisterPage = () => { + const cardInfo = useCardInfoValue(); + const { state: showCardForm, turnOff: turnToNicknameForm } = useSwitch(true); + const { state: showModal, turnOn: openModal, turnOff: closeModal } = useSwitch(true); + + return ( + + {showCardForm &&
} + {showCardForm ? ( + <> + + + + ) : ( + + 곧 카드 등록이 완료됩니다! + + + + )} + {showModal && ( + + + + )} + + ); +}; + +export default CardRegisterPage; diff --git a/src/pages/MyCardPage/MyCardPage.styled.ts b/src/components/pages/MyCardPage/MyCardPage.styled.ts similarity index 73% rename from src/pages/MyCardPage/MyCardPage.styled.ts rename to src/components/pages/MyCardPage/MyCardPage.styled.ts index a85f354229..89fdfa921a 100644 --- a/src/pages/MyCardPage/MyCardPage.styled.ts +++ b/src/components/pages/MyCardPage/MyCardPage.styled.ts @@ -1,10 +1,9 @@ import styled from 'styled-components'; -import { Card } from '../../components/Card/Card.styled'; +import { Card } from '../../Card/Card.styled'; export const MyCardPage = styled.div` display: flex; flex-direction: column; - justify-content: center; align-items: center; width: 100%; @@ -14,10 +13,6 @@ export const MyCardPage = styled.div` export const CardList = styled.ul` display: flex; flex-direction: column; - - & > button:not(:last-child) { - margin-bottom: 50px; - } `; export const CardRegisterMessage = styled.p` @@ -30,6 +25,7 @@ export const CardRegisterButton = styled(Card)` justify-content: center; align-items: center; + background-color: #e5e5e5; color: black; margin-bottom: 50px; @@ -42,3 +38,12 @@ export const ButtonIcon = styled.div` opacity: 0.6; `; + +export const Nickname = styled.div` + text-align: center; + font-size: 18px; + font-weight: 600; + + margin-top: 20px; + margin-bottom: 40px; +`; diff --git a/src/components/pages/MyCardPage/MyCardPage.tsx b/src/components/pages/MyCardPage/MyCardPage.tsx new file mode 100644 index 0000000000..80dbda2077 --- /dev/null +++ b/src/components/pages/MyCardPage/MyCardPage.tsx @@ -0,0 +1,32 @@ +import { useNavigate } from 'react-router-dom'; +import { useCardListValue } from '../../../context/CardListContext'; + +import * as styled from './MyCardPage.styled'; + +import { Header, Card } from '../../'; + +const MyCardPage = () => { + const navigation = useNavigate(); + + const cardList = useCardListValue(); + + return ( + +
+ 새로운 카드를 등록해 주세요 + navigation('/register')}> + + + + + {cardList.map((cardInfo) => ( +
+ + {cardInfo.nickname} +
+ ))} +
+ + ); +}; + +export default MyCardPage; diff --git a/src/components/pages/index.ts b/src/components/pages/index.ts new file mode 100644 index 0000000000..13237b0257 --- /dev/null +++ b/src/components/pages/index.ts @@ -0,0 +1,4 @@ +import CardRegisterPage from './CardRegisterPage/CardRegisterPage'; +import MyCardPage from './MyCardPage/MyCardPage'; + +export { CardRegisterPage, MyCardPage }; diff --git a/src/components/portal/Modal/Modal.styled.ts b/src/components/portal/Modal/Modal.styled.ts new file mode 100644 index 0000000000..6412b2a2a9 --- /dev/null +++ b/src/components/portal/Modal/Modal.styled.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +export const Modal = styled.div` + position: fixed; + top: 0; + left: 0; + + display: flex; + align-items: flex-end; + + width: 100vw; + height: 100vh; +`; + +export const ModalBackground = styled.div` + position: absolute; + + width: 100%; + height: 100%; + + background-color: rgba(1, 1, 1, 0.5); +`; diff --git a/src/components/portal/Modal/Modal.tsx b/src/components/portal/Modal/Modal.tsx new file mode 100644 index 0000000000..498dbea75c --- /dev/null +++ b/src/components/portal/Modal/Modal.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import ReactDOM from 'react-dom'; + +import * as styled from './Modal.styled'; + +interface Props { + closeModal: () => void; + children: React.ReactNode; +} + +const Modal = ({ closeModal, children }: Props) => { + const modalRoot = document.querySelector('#modal-root') as HTMLElement; + + useEffect(() => { + document.body.classList.add('overflowHidden'); + + return () => document.body.classList.remove('overflowHidden'); + }, []); + + return ReactDOM.createPortal( + + + {children} + , + modalRoot + ); +}; + +export default Modal; diff --git a/src/components/portal/index.ts b/src/components/portal/index.ts new file mode 100644 index 0000000000..7d2db5cec9 --- /dev/null +++ b/src/components/portal/index.ts @@ -0,0 +1,3 @@ +import Modal from './Modal/Modal'; + +export { Modal }; diff --git a/src/components/styles/index.ts b/src/components/styles/index.ts new file mode 100644 index 0000000000..15555b4e9d --- /dev/null +++ b/src/components/styles/index.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const SubmitButton = styled.button` + position: absolute; + + bottom: 0; + right: 0; + + font-size: 16px; + font-weight: 900; + + &:focus { + padding: 10px 20px; + } +`; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000000..aab019c182 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,63 @@ +import { bc, hana, hyundai, kakao, kookmin, lotte, sinhan, woori } from '../images'; + +interface IBANKS { + [key: string]: { + name: string; + logo: any; + bgColor: string; + color: string; + }; +} + +const BANKS: IBANKS = { + bc: { + name: 'BC카드', + logo: bc, + bgColor: '#DE5356', + color: '#FFFFFF', + }, + sinhan: { + name: '신한카드', + logo: sinhan, + bgColor: '#1B45F5', + color: '#FFFFFF', + }, + kakao: { + name: '카카오뱅크', + logo: kakao, + bgColor: '#FBE74D', + color: '#333333', + }, + hyundai: { + name: '현대카드', + logo: hyundai, + bgColor: '#000000', + color: '#FFFFFF', + }, + woori: { + name: '우리카드', + logo: woori, + bgColor: '#3579C2', + color: '#FFFFFF', + }, + lotte: { + name: '롯데카드', + logo: lotte, + bgColor: '#DA3832', + color: '#FFFFFF', + }, + hana: { + name: '하나카드', + logo: hana, + bgColor: '#41928F', + color: '#FFFFFF', + }, + kookmin: { + name: '국민카드', + logo: kookmin, + bgColor: '#6D655C', + color: '#F7CF47', + }, +}; + +export { BANKS }; diff --git a/src/context/CardInfoContext.tsx b/src/context/CardInfoContext.tsx new file mode 100644 index 0000000000..6893d8a246 --- /dev/null +++ b/src/context/CardInfoContext.tsx @@ -0,0 +1,76 @@ +import { + createContext, + Dispatch, + ReactNode, + SetStateAction, + useContext, + useMemo, + useState, +} from 'react'; + +import { v4 as uuid } from 'uuid'; + +import { CardInfo } from '../types/card'; + +interface Actions { + setCardInfo: Dispatch>; + initCardInfo: () => void; +} + +interface Props { + children: ReactNode; +} + +const CardInfoValueContext = createContext(undefined); +const CardInfoActionsContext = createContext(undefined); + +export const CardInfoProvider = ({ children }: Props) => { + const getInit = () => { + return { + id: uuid(), + firstCardNumbers: '', + secondCardNumbers: '', + thirdCardNumbers: '', + fourthCardNumbers: '', + expirationMonth: '', + expirationYear: '', + ownerName: '', + securityNumbers: '', + firstPassword: '', + secondPassword: '', + bank: '', + nickname: '', + }; + }; + + const [cardInfo, setCardInfo] = useState(getInit()); + + const actions = useMemo( + () => ({ + setCardInfo, + + initCardInfo() { + setCardInfo(getInit()); + }, + }), + [] + ); + + return ( + + {children} + + ); +}; + +export const useCardInfoValue = () => { + const state = useContext(CardInfoValueContext); + if (!state) throw Error('cannot find Provider'); + return state; +}; + +export const useCardInfoActions = () => { + const actions = useContext(CardInfoActionsContext); + if (!actions?.initCardInfo || !actions?.setCardInfo) throw Error('cannot find Provider'); + return actions; +}; diff --git a/src/context/CardListContext.tsx b/src/context/CardListContext.tsx new file mode 100644 index 0000000000..a3820ba384 --- /dev/null +++ b/src/context/CardListContext.tsx @@ -0,0 +1,45 @@ +import { createContext, ReactNode, SetStateAction, useContext, useMemo } from 'react'; + +import useLocalStorage from '../hooks/useLocalStorage'; + +import { CardInfo } from '../types/card'; + +interface Actions { + setCardList: (param: SetStateAction | CardInfo[]) => void; +} + +interface Props { + children: ReactNode; +} + +const CardListValueContext = createContext([]); +const CardListActionsContext = createContext(undefined); + +export const CardListProvider = ({ children }: Props) => { + const [cardList, setCardList] = useLocalStorage('react-payments-card-info', []); + + const actions = useMemo( + () => ({ + setCardList, + }), + [setCardList] + ); + + return ( + + {children} + + ); +}; + +export const useCardListValue = () => { + const state = useContext(CardListValueContext); + if (!state) throw Error('cannot find Provider'); + return state; +}; + +export const useCardListActions = () => { + const actions = useContext(CardListActionsContext); + if (!actions?.setCardList) throw Error('cannot find Provider'); + return actions; +}; diff --git a/src/domain/validator.ts b/src/domain/validator.ts index 10a8deb976..55481e3211 100644 --- a/src/domain/validator.ts +++ b/src/domain/validator.ts @@ -18,18 +18,6 @@ const validateCardNumbers = (value: string) => { return ''; }; -let [month, year] = ['', '']; - -const validateExpirationDate = () => { - const curYear = String(new Date().getFullYear()).substring(2); - const curMonth = new Date().getMonth() + 1; - - if (year === curYear && Number(month) < curMonth) { - return ERROR_MESSAGE.EXPIRATION_DATE; - } - return ''; -}; - const validateExpirationMonth = (value: string) => { if (value.length < 2) { return ERROR_MESSAGE.EXPIRATION; @@ -39,9 +27,6 @@ const validateExpirationMonth = (value: string) => { return ERROR_MESSAGE.EXPIRATION_MONTH; } - month = value; - if (month && year) return validateExpirationDate(); - return ''; }; @@ -55,9 +40,6 @@ const validateExpirationYear = (value: string) => { return ERROR_MESSAGE.EXPIRATION_YEAR; } - year = value; - if (month && year) return validateExpirationDate(); - return ''; }; diff --git a/src/hooks/useForm.ts b/src/hooks/useForm.ts index d7f90477b5..3f0a8ccc9b 100644 --- a/src/hooks/useForm.ts +++ b/src/hooks/useForm.ts @@ -1,82 +1,97 @@ -import { ChangeEvent, RefObject, useState } from 'react'; -import { Validator } from '../types/validator'; +import { ChangeEvent, FormEvent, useState } from 'react'; -interface ValuesState { - [key: string]: string; -} - -interface ErrorState { - [key: string]: string; +interface ErrorOptions { + initState: { + [key: string]: string; + }; + validator: { + [key: string]: (value: string) => string; + }; } -interface IRefs { - [key: string]: RefObject; +interface Props { + submitAction: () => void; + changeAction: (name: string, value: string) => void; + errorOptions?: ErrorOptions; } -const useForm = (refs: IRefs, validator: Validator) => { - const refNames = Object.keys(refs); - const valueObj: ValuesState = {}; - const errorObj: ErrorState = {}; +const useForm = ({ submitAction, changeAction, errorOptions }: Props) => { + const [error, setError] = useState(errorOptions?.initState || {}); - refNames.forEach((name) => { - valueObj[name] = ''; - errorObj[name] = ''; - }); + const checkFormValidity = (elements: HTMLInputElement[]) => { + return elements.every((elem) => { + if (elem.tagName !== 'INPUT') return true; - const [values, setValues] = useState(valueObj); - const [error, setError] = useState(errorObj); - - const findError = () => { - return Object.keys(validator).some((key) => { - const errorMessage = validator[key](values[key]); + const { name, value } = elem; + const errorMessage = errorOptions?.validator && errorOptions?.validator[name]?.(value); if (errorMessage) { - setError((prev) => ({ ...prev, [key]: errorMessage })); - refs[key]?.current?.focus(); + setError((prev: any) => ({ ...prev, [name]: errorMessage })); + elem.focus(); - return true; + return false; } - return false; + return true; }); }; - const isNumeric = (value: string) => { - return /^[0-9]*$/.test(value); + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + const formElements = (e.target as HTMLFormElement).elements; + const elements = formElements ? ([...formElements] as HTMLInputElement[]) : []; + + const ok = checkFormValidity(elements); + if (!ok) return; + + submitAction(); }; - const onChange = (e: ChangeEvent) => { - e.preventDefault(); + const onChange = ({ target }: ChangeEvent) => { + const { name, value, maxLength } = target; + + if (!canChange(target)) return; + + setError((prev: any) => ({ ...prev, [name]: '' })); + if (value.length === maxLength) { + const formElements = target.form?.elements; + const elements = formElements ? ([...formElements] as HTMLInputElement[]) : []; + focusToNextFormElement(elements, target); + } + + changeAction(name, value); + }; + + const focusToNextFormElement = (elements: HTMLInputElement[], target: HTMLInputElement) => { + elements.forEach((elem, index) => { + if (elem !== target) return; + + elements[index + 1]?.focus(); + }); + }; + + const canChange = (target: HTMLInputElement) => { const { - name, value, maxLength, - dataset: { type, next }, - } = e.target; - - if (value.length > maxLength) return; - if ((type === 'number' || type === 'password') && !isNumeric(value)) return; + dataset: { numeric }, + } = target; - if (error[name]) { - setError((prev) => ({ - ...prev, - [name]: '', - })); + if (value.length > maxLength) return false; + if (numeric) { + if (!isNumeric(value)) return false; } - if (value.length >= maxLength && next) { - refs[name]?.current?.blur(); - refs[next]?.current?.focus(); - } + return true; + }; - setValues((prev) => ({ - ...prev, - [name]: value, - })); + const isNumeric = (value: string) => { + return /^[0-9]*$/.test(value); }; - return { values, onChange, error, findError }; + return { onSubmit, onChange, error }; }; export default useForm; diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000000..5538366805 --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,21 @@ +import { SetStateAction, useEffect, useState } from 'react'; + +const useLocalStorage = (key: string, initialValue: T) => { + const [localData, setLocalData] = useState(() => { + const item = localStorage.getItem(key); + + return item ? JSON.parse(item) : initialValue; + }); + + useEffect(() => { + localStorage.setItem(key, JSON.stringify(localData)); + }, [key, localData]); + + const setData = (param: SetStateAction | T) => { + setLocalData(param); + }; + + return [localData, setData] as const; +}; + +export default useLocalStorage; diff --git a/src/hooks/useSwitch.ts b/src/hooks/useSwitch.ts new file mode 100644 index 0000000000..58d968d4e4 --- /dev/null +++ b/src/hooks/useSwitch.ts @@ -0,0 +1,17 @@ +import { useState } from 'react'; + +const useSwitch = (initValue: boolean) => { + const [state, setState] = useState(initValue); + + const turnOn = () => { + setState((prev) => !prev); + }; + + const turnOff = () => { + setState((prev) => !prev); + }; + + return { state, turnOn, turnOff }; +}; + +export default useSwitch; diff --git a/src/images/bc_logo.svg b/src/images/bc_logo.svg new file mode 100644 index 0000000000..75530f6948 --- /dev/null +++ b/src/images/bc_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/hana_logo.svg b/src/images/hana_logo.svg new file mode 100644 index 0000000000..f599e42014 --- /dev/null +++ b/src/images/hana_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/hyundai_logo.svg b/src/images/hyundai_logo.svg new file mode 100644 index 0000000000..07ad7921e5 --- /dev/null +++ b/src/images/hyundai_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/index.ts b/src/images/index.ts new file mode 100644 index 0000000000..250459ad13 --- /dev/null +++ b/src/images/index.ts @@ -0,0 +1,10 @@ +import bc from './bc_logo.svg'; +import hana from './hana_logo.svg'; +import hyundai from './hyundai_logo.svg'; +import kakao from './kakao_logo.svg'; +import kookmin from './kookmin_logo.svg'; +import lotte from './lotte_logo.svg'; +import sinhan from './sinhan_logo.svg'; +import woori from './woori_logo.svg'; + +export { bc, hana, hyundai, kakao, kookmin, lotte, sinhan, woori }; diff --git a/src/images/kakao_logo.svg b/src/images/kakao_logo.svg new file mode 100644 index 0000000000..1b26a48303 --- /dev/null +++ b/src/images/kakao_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/kookmin_logo.svg b/src/images/kookmin_logo.svg new file mode 100644 index 0000000000..4ccab3a18c --- /dev/null +++ b/src/images/kookmin_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/lotte_logo.svg b/src/images/lotte_logo.svg new file mode 100644 index 0000000000..2735444021 --- /dev/null +++ b/src/images/lotte_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/sinhan_logo.svg b/src/images/sinhan_logo.svg new file mode 100644 index 0000000000..2cc5bef47b --- /dev/null +++ b/src/images/sinhan_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/woori_logo.svg b/src/images/woori_logo.svg new file mode 100644 index 0000000000..e2c5a4929b --- /dev/null +++ b/src/images/woori_logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/index.tsx b/src/index.tsx index d67e0108e8..3f27fdb6e5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,10 +3,17 @@ import ReactDOM from 'react-dom/client'; import GlobalStyles from './GlobalStyles'; import App from './App'; +import { CardInfoProvider } from './context/CardInfoContext'; +import { CardListProvider } from './context/CardListContext'; + const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); root.render( - + + + + + ); diff --git a/src/pages/CardRegisterPage/CardRegisterPage.styled.ts b/src/pages/CardRegisterPage/CardRegisterPage.styled.ts deleted file mode 100644 index ef6417871a..0000000000 --- a/src/pages/CardRegisterPage/CardRegisterPage.styled.ts +++ /dev/null @@ -1,25 +0,0 @@ -import styled from 'styled-components'; - -export const CardRegisterPage = styled.div` - height: 100%; -`; - -export const CardRegisterForm = styled.form` - & > div { - margin-bottom: 32px; - } -`; - -export const CardInfoSubmitButtonContainer = styled.div` - display: flex; - justify-content: flex-end; -`; - -export const CardInfoSubmitButton = styled.button` - font-size: 16px; - font-weight: 900; - - &:focus { - padding: 10px 20px; - } -`; diff --git a/src/pages/CardRegisterPage/index.jsx b/src/pages/CardRegisterPage/index.jsx deleted file mode 100644 index 8891adcec1..0000000000 --- a/src/pages/CardRegisterPage/index.jsx +++ /dev/null @@ -1,189 +0,0 @@ -import { useRef } from 'react'; -import { useNavigate } from 'react-router-dom'; -import useForm from '../../hooks/useForm'; -import * as styled from './CardRegisterPage.styled'; - -import validator from '../../domain/validator'; - -import CardPreview from '../../components/CardPreview'; -import Header from '../../components/Header'; -import { - Input, - CardNumbers, - ExpirationDate, - OwnerName, - Password, - SecurityNumbers, -} from '../../components/InputBox'; - -const CardRegisterPage = ({ onChangeCardList }) => { - const navigate = useNavigate(); - - const refs = { - firstCardNumbers: useRef(null), - secondCardNumbers: useRef(null), - thirdCardNumbers: useRef(null), - fourthCardNumbers: useRef(null), - expirationMonth: useRef(null), - expirationYear: useRef(null), - ownerName: useRef(null), - securityNumbers: useRef(null), - firstPassword: useRef(null), - secondPassword: useRef(null), - submitButton: useRef(null), - }; - - const { values, onChange, error, findError } = useForm(refs, validator); - - const onSubmit = (e) => { - e.preventDefault(); - - if (findError()) return; - - onChangeCardList(values); - - navigate(`/`); - }; - - return ( - -
- - - - <> - - - - - - - - <> - - - - - - - - - - - - <> - - - - - - 다음 - - - - ); -}; - -export default CardRegisterPage; diff --git a/src/pages/MyCardPage/index.tsx b/src/pages/MyCardPage/index.tsx deleted file mode 100644 index 20da2e7e66..0000000000 --- a/src/pages/MyCardPage/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import * as styled from './MyCardPage.styled'; - -import { useNavigate } from 'react-router-dom'; - -import Card from '../../components/Card'; -import Header from '../../components/Header'; -import { CardInfo } from '../../types/card'; - -interface MyCardPageProps { - cardList: CardInfo[]; -} - -const MyCardPage = ({ cardList }: MyCardPageProps) => { - const navigation = useNavigate(); - - const handleClick = () => { - navigation('/register'); - }; - - const generateCardList = (cardList: CardInfo[]) => { - return cardList.map((cardInfo) => ( - - )); - }; - - return ( - -
- 새로운 카드를 등록해 주세요 - - + - - {generateCardList(cardList)} - - ); -}; - -export default MyCardPage; diff --git a/src/stories/CardNumberBox.stories.ts b/src/stories/CardNumberBox.stories.ts deleted file mode 100644 index cfab24cd46..0000000000 --- a/src/stories/CardNumberBox.stories.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { CardNumbers } from '../components/InputBox'; - -const meta: Meta = { - title: 'Components/CardNumbers', - component: CardNumbers, - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - -export const Container: Story = { - args: {}, -}; diff --git a/src/stories/Input.stories.ts b/src/stories/Input.stories.ts deleted file mode 100644 index 82c3b6de8c..0000000000 --- a/src/stories/Input.stories.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Input } from '../components/InputBox'; - -const meta: Meta = { - title: 'Components/Input', - component: Input, - tags: ['autodocs'], -}; - -export default meta; -type Story = StoryObj; - -export const CardNumber: Story = { - args: { - value: '', - onChange: () => console.log('onChange CardNumber Input'), - type: 'number', - }, -}; - -export const OwnerName: Story = { - args: { - value: '', - onChange: () => console.log('onChange OwnerName Input'), - type: 'text', - placeholder: '카드에 표시된 이름과 동일하게 입력하세요.', - }, -}; - -export const CVC: Story = { - args: { - value: '', - onChange: () => console.log('onChange CVC Input'), - type: 'number', - }, -}; - -export const CardPassword: Story = { - args: { - value: '', - onChange: () => console.log('onChange CardPassword Input'), - type: 'password', - }, -}; diff --git a/src/stories/components/Card.stories.ts b/src/stories/components/Card.stories.ts new file mode 100644 index 0000000000..2b19bed03c --- /dev/null +++ b/src/stories/components/Card.stories.ts @@ -0,0 +1,82 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Card } from '../../components'; + +const meta: Meta = { + component: Card, + title: 'Components/Card', + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +const getCardInfo = (bank: string = '') => ({ + id: '1', + firstCardNumbers: '1234', + secondCardNumbers: '1234', + thirdCardNumbers: '1234', + fourthCardNumbers: '1234', + expirationMonth: '12', + expirationYear: '23', + ownerName: 'Woody', + securityNumbers: '123', + firstPassword: '1', + secondPassword: '2', + bank, + nickname: bank.toUpperCase(), +}); + +export const Default: Story = { + args: { + cardInfo: getCardInfo(), + }, +}; + +export const Bc: Story = { + args: { + cardInfo: getCardInfo('bc'), + }, +}; + +export const Sinhan: Story = { + args: { + cardInfo: getCardInfo('sinhan'), + }, +}; + +export const Kakao: Story = { + args: { + cardInfo: getCardInfo('kakao'), + }, +}; + +export const Hyundai: Story = { + args: { + cardInfo: getCardInfo('hyundai'), + }, +}; + +export const Woori: Story = { + args: { + cardInfo: getCardInfo('woori'), + }, +}; + +export const Lotte: Story = { + args: { + cardInfo: getCardInfo('lotte'), + }, +}; + +export const Hana: Story = { + args: { + cardInfo: getCardInfo('hana'), + }, +}; + +export const Kookmin: Story = { + args: { + cardInfo: getCardInfo('kookmin'), + }, +}; diff --git a/src/stories/components/Header.stories.tsx b/src/stories/components/Header.stories.tsx new file mode 100644 index 0000000000..269a0bbfdb --- /dev/null +++ b/src/stories/components/Header.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta } from '@storybook/react'; + +import { MemoryRouter } from 'react-router-dom'; + +import { Header } from '../../components'; + +const meta: Meta = { + component: Header, + title: 'Components/Header', + tags: ['autodocs'], +}; + +export default meta; + +export const Default = () => { + return ( + +
+ + ); +}; + +export const MyCardPage: React.FC = () => { + return ( + +
+ + ); +}; + +export const CardRegisterPage: React.FC = () => { + return ( + +
+ + ); +}; diff --git a/src/stories/components/Input.stories.tsx b/src/stories/components/Input.stories.tsx new file mode 100644 index 0000000000..dd9f481860 --- /dev/null +++ b/src/stories/components/Input.stories.tsx @@ -0,0 +1,84 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Input } from '../../components'; + +const meta: Meta = { + component: Input, + title: 'Components/Input', + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const CardNumbersText: Story = { + args: { + type: 'text', + maxLength: 4, + center: true, + }, +}; + +export const CardNumbersPassword: Story = { + args: { + type: 'text', + maxLength: 4, + center: true, + }, +}; + +export const ExpirationMonth: Story = { + args: { + type: 'text', + maxLength: 2, + center: true, + placeholder: 'MM', + }, +}; + +export const ExpirationYear: Story = { + args: { + type: 'text', + maxLength: 2, + center: true, + placeholder: 'YY', + }, +}; + +export const OwnerName: Story = { + args: { + name: 'ownerName', + type: 'text', + maxLength: 30, + placeholder: '카드에 표시된 이름과 동일하게 입력하세요.', + }, +}; + +export const SecurityNumbers: Story = { + args: { + type: 'password', + maxLength: 3, + center: true, + }, +}; + +export const Password: Story = { + args: { + type: 'password', + maxLength: 1, + center: true, + }, +}; + +export const Nickname: Story = { + args: { + name: 'nickname', + type: 'text', + maxLength: 8, + center: true, + }, +}; diff --git a/src/stories/components/InputBox/CardNumbers.stories.tsx b/src/stories/components/InputBox/CardNumbers.stories.tsx new file mode 100644 index 0000000000..095ddfd79e --- /dev/null +++ b/src/stories/components/InputBox/CardNumbers.stories.tsx @@ -0,0 +1,100 @@ +import type { Meta } from '@storybook/react'; + +import { useEffect, useRef } from 'react'; +import { + CardInfoProvider, + useCardInfoActions, + useCardInfoValue, +} from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; +import { CardNumbers as CardNumbersComponent } from '../../../components/InputBox'; +import { Input } from '../../../components'; + +import validator from '../../../domain/validator'; +import { CardInfo } from '../../../types/card'; + +const InputBoxStories = () => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const firstInputRef = useRef(null); + + useEffect(() => { + firstInputRef.current?.focus(); + }, []); + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => {}, + changeAction: (name: string, value: string) => { + setCardInfo((prev: CardInfo) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + firstCardNumbers: '', + secondCardNumbers: '', + thirdCardNumbers: '', + fourthCardNumbers: '', + }, + validator, + }, + }); + + return ( +
+ + + + + + + +
+ ); +}; + +export const CardNumbers = () => { + return ( + + + + ); +}; + +const meta: Meta = { + component: CardNumbers, + title: 'Components/InputBox', +}; + +export default meta; diff --git a/src/stories/components/InputBox/ExpirationDate.stories.tsx b/src/stories/components/InputBox/ExpirationDate.stories.tsx new file mode 100644 index 0000000000..7c583c2467 --- /dev/null +++ b/src/stories/components/InputBox/ExpirationDate.stories.tsx @@ -0,0 +1,74 @@ +import type { Meta } from '@storybook/react'; + +import { + CardInfoProvider, + useCardInfoActions, + useCardInfoValue, +} from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; +import { ExpirationDate as ExpirationDateComponent } from '../../../components/InputBox'; +import { Input } from '../../../components'; + +import validator from '../../../domain/validator'; +import { CardInfo } from '../../../types/card'; + +const InputBoxStories = () => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => {}, + changeAction: (name: string, value: string) => { + setCardInfo((prev: CardInfo) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + expirationMonth: '', + expirationYear: '', + }, + validator, + }, + }); + + return ( +
+ + + + + +
+ ); +}; + +export const ExpirationDate = () => { + return ( + + + + ); +}; + +const meta: Meta = { + component: ExpirationDate, + title: 'Components/InputBox', +}; + +export default meta; diff --git a/src/stories/components/InputBox/OwnerName.stories.tsx b/src/stories/components/InputBox/OwnerName.stories.tsx new file mode 100644 index 0000000000..82b4ab25b0 --- /dev/null +++ b/src/stories/components/InputBox/OwnerName.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta } from '@storybook/react'; + +import { + CardInfoProvider, + useCardInfoActions, + useCardInfoValue, +} from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; +import { OwnerName as OwnerNameComponent } from '../../../components/InputBox'; +import { Input } from '../../../components'; + +import validator from '../../../domain/validator'; +import { CardInfo } from '../../../types/card'; + +const InputBoxStories = () => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => {}, + changeAction: (name: string, value: string) => { + setCardInfo((prev: CardInfo) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + expirationMonth: '', + expirationYear: '', + }, + validator, + }, + }); + + return ( +
+ + + + +
+ ); +}; + +export const OwnerName = () => { + return ( + + + + ); +}; + +const meta: Meta = { + component: OwnerName, + title: 'Components/InputBox', +}; + +export default meta; diff --git a/src/stories/components/InputBox/Password.stories.tsx b/src/stories/components/InputBox/Password.stories.tsx new file mode 100644 index 0000000000..c49ecdf906 --- /dev/null +++ b/src/stories/components/InputBox/Password.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta } from '@storybook/react'; + +import { + CardInfoProvider, + useCardInfoActions, + useCardInfoValue, +} from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; +import { Password as PasswordComponent } from '../../../components/InputBox'; +import { Input } from '../../../components'; + +import validator from '../../../domain/validator'; +import { CardInfo } from '../../../types/card'; + +const InputBoxStories = () => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => {}, + changeAction: (name: string, value: string) => { + setCardInfo((prev: CardInfo) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + password: '', + }, + validator, + }, + }); + + return ( +
+ + + + + +
+ ); +}; + +export const Password = () => { + return ( + + + + ); +}; + +const meta: Meta = { + component: Password, + title: 'Components/InputBox', +}; + +export default meta; diff --git a/src/stories/components/InputBox/SecurityNumbers.stories.tsx b/src/stories/components/InputBox/SecurityNumbers.stories.tsx new file mode 100644 index 0000000000..3283850f06 --- /dev/null +++ b/src/stories/components/InputBox/SecurityNumbers.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta } from '@storybook/react'; + +import { + CardInfoProvider, + useCardInfoActions, + useCardInfoValue, +} from '../../../context/CardInfoContext'; +import useForm from '../../../hooks/useForm'; +import { SecurityNumbers as SecurityNumbersComponent } from '../../../components/InputBox'; +import { Input } from '../../../components'; + +import validator from '../../../domain/validator'; +import { CardInfo } from '../../../types/card'; + +const InputBoxStories = () => { + const [cardInfo, { setCardInfo }] = [useCardInfoValue(), useCardInfoActions()]; + + const { onSubmit, onChange, error } = useForm({ + submitAction: () => {}, + changeAction: (name: string, value: string) => { + setCardInfo((prev: CardInfo) => ({ ...prev, [name]: value })); + }, + errorOptions: { + initState: { + password: '', + }, + validator, + }, + }); + + return ( +
+ + + + +
+ ); +}; + +export const SecurityNumbers = () => { + return ( + + + + ); +}; + +const meta: Meta = { + component: SecurityNumbers, + title: 'Components/InputBox', +}; + +export default meta; diff --git a/src/stories/components/SelectBank.stories.tsx b/src/stories/components/SelectBank.stories.tsx new file mode 100644 index 0000000000..69ef164f9a --- /dev/null +++ b/src/stories/components/SelectBank.stories.tsx @@ -0,0 +1,38 @@ +import type { Meta } from '@storybook/react'; + +import { CardInfoProvider } from '../../context/CardInfoContext'; +import useSwitch from '../../hooks/useSwitch'; +import { SelectBank } from '../../components'; +import { Modal } from '../../components/portal'; + +const meta: Meta = { + component: SelectBank, + title: 'Components/SelectBank', + tags: ['autodocs'], +}; + +export default meta; + +const SelectBankStories = () => { + const { state, turnOff: closeModal } = useSwitch(true); + + return ( + <> + {state && ( + + + + + + )} + + ); +}; + +export const Default = () => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'modal-root'); + document.body.appendChild(modalRoot); + + return ; +}; diff --git a/src/stories/components/pages/CardRegisterPage.stories.tsx b/src/stories/components/pages/CardRegisterPage.stories.tsx new file mode 100644 index 0000000000..88399cc1f0 --- /dev/null +++ b/src/stories/components/pages/CardRegisterPage.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta } from '@storybook/react'; + +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { CardInfoProvider } from '../../../context/CardInfoContext'; +import { CardListProvider } from '../../../context/CardListContext'; +import { + CardRegisterPage as CardRegisterPageComponent, + MyCardPage, +} from '../../../components/pages'; + +export const CardRegisterPage = () => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'modal-root'); + document.body.appendChild(modalRoot); + + return ( + + + + + } /> + } /> + + + + + ); +}; + +const meta: Meta = { + component: CardRegisterPage, + title: 'Components/pages', +}; + +export default meta; diff --git a/src/stories/components/pages/MyCardPage.stories.tsx b/src/stories/components/pages/MyCardPage.stories.tsx new file mode 100644 index 0000000000..35634b0bc0 --- /dev/null +++ b/src/stories/components/pages/MyCardPage.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta } from '@storybook/react'; + +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { CardInfoProvider } from '../../../context/CardInfoContext'; +import { CardListProvider } from '../../../context/CardListContext'; +import { CardRegisterPage, MyCardPage as MyCardPageComponent } from '../../../components/pages'; + +export const MyCardPage = () => { + const modalRoot = document.createElement('div'); + modalRoot.setAttribute('id', 'modal-root'); + document.body.appendChild(modalRoot); + + return ( + + + + + } /> + } /> + + + + + ); +}; + +const meta: Meta = { + component: MyCardPage, + title: 'Components/pages', +}; + +export default meta; diff --git a/src/types/InputBox.ts b/src/types/InputBox.ts index 3af25fd0bf..b7ca8511a6 100644 --- a/src/types/InputBox.ts +++ b/src/types/InputBox.ts @@ -1,7 +1,7 @@ import { ReactElement } from 'react'; export interface InputBoxProps { - children: ReactElement; + children: ReactElement | ReactElement[]; error: { [key: string]: string; }; diff --git a/src/types/card.ts b/src/types/card.ts index 97706736e4..d5800bdff5 100644 --- a/src/types/card.ts +++ b/src/types/card.ts @@ -10,5 +10,6 @@ export interface CardInfo { securityNumbers: string; firstPassword: string; secondPassword: string; - submitButton: string; + bank: string; + nickname: string; } diff --git a/src/types/svg.d.ts b/src/types/svg.d.ts new file mode 100644 index 0000000000..bff94710c9 --- /dev/null +++ b/src/types/svg.d.ts @@ -0,0 +1 @@ +declare module '*.svg'; diff --git a/tsconfig.json b/tsconfig.json index a273b0cfc0..d7175ed81e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,8 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], + "downlevelIteration": true, "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +17,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"] }