From c4166e0716cd869a4571a91041a2ffa9b50569e6 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 13:51:49 +0900 Subject: [PATCH 001/102] =?UTF-8?q?fix:=20card=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20id=20=EB=88=84=EB=9D=BD=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardListPage.tsx | 15 +++++++-------- src/utils/applicationUtil.ts | 5 +++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pages/CardListPage.tsx b/src/pages/CardListPage.tsx index d42c2dc796..660a16b171 100644 --- a/src/pages/CardListPage.tsx +++ b/src/pages/CardListPage.tsx @@ -24,14 +24,13 @@ const CardListPage = () => { 새로운 카드를 등록해주세요. ) : ( cardList.map((card: CardType) => ( -
  • - -
  • + )) )} -

    카드 추가

    +

    카드 추가

    -
    +
    { cardPassword1={cardPassword1} cardPassword2={cardPassword2} /> -
    + ); }; diff --git a/src/pages/CardListPage.css b/src/pages/CardListPage.css index f83f3236c9..71c9b9aa73 100644 --- a/src/pages/CardListPage.css +++ b/src/pages/CardListPage.css @@ -4,7 +4,7 @@ width: 208px; padding: 49px 0; border: none; - font-family: 'Roboto'; + font-family: 'Roboto', sans-serif; font-style: normal; font-weight: 400; font-size: 30px; @@ -20,7 +20,7 @@ cursor: pointer; } -.add-card-page-body { +.card-list-page-body { display: flex; overflow-y: scroll; flex-direction: column; @@ -35,6 +35,6 @@ color: #575757; } -.add-card-page-header-title { +.card-list-page-header-title { margin-left: 36px; } diff --git a/src/pages/CardListPage.tsx b/src/pages/CardListPage.tsx index 660a16b171..fa0ba1f84e 100644 --- a/src/pages/CardListPage.tsx +++ b/src/pages/CardListPage.tsx @@ -15,11 +15,11 @@ const CardListPage = () => { }; return ( -
    +
    -

    보유카드

    +

    보유카드

    -
    +
    {cardList.length === 0 ? ( 새로운 카드를 등록해주세요. ) : ( From 58a0a69af6dc673510fbb19c2233355956c8117f Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 17:36:12 +0900 Subject: [PATCH 005/102] =?UTF-8?q?refactor:=20custom=20hook=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/{usePasswordInput.ts => useComplicateInput.ts} | 4 ++-- src/pages/AddCardPage.tsx | 4 ++-- src/stories/AddCardNumberInput.stories.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/hooks/{usePasswordInput.ts => useComplicateInput.ts} (79%) diff --git a/src/hooks/usePasswordInput.ts b/src/hooks/useComplicateInput.ts similarity index 79% rename from src/hooks/usePasswordInput.ts rename to src/hooks/useComplicateInput.ts index 6b8bac1836..8052b7ac7d 100644 --- a/src/hooks/usePasswordInput.ts +++ b/src/hooks/useComplicateInput.ts @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; -const usePasswordInput = (initialState: T) => { +const useComplicateInput = (initialState: T) => { const [value, setValue] = useState(initialState); const onChange = useCallback( @@ -16,4 +16,4 @@ const usePasswordInput = (initialState: T) => { return [value, onChange] as const; }; -export default usePasswordInput; +export default useComplicateInput; diff --git a/src/pages/AddCardPage.tsx b/src/pages/AddCardPage.tsx index 9253901da7..6da75ed9fa 100644 --- a/src/pages/AddCardPage.tsx +++ b/src/pages/AddCardPage.tsx @@ -6,7 +6,7 @@ import Card from '../components/Card'; import AddCardForm from '../components/AddCardForm'; import Header from '../components/Header'; import useInput from '../hooks/useInput'; -import usePasswordInput from '../hooks/usePasswordInput'; +import useComplicateInput from '../hooks/useComplicateInput'; import { formatExpireDate, handleNumberInput, @@ -26,7 +26,7 @@ const AddCardPage = () => { const navigate = useNavigate(); const [cardType] = useState('현대'); - const [cardNumber, onChangeCardNumber] = usePasswordInput({ + const [cardNumber, onChangeCardNumber] = useComplicateInput({ first: '', second: '', third: '', diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index 3de8777449..817f7d671d 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -2,7 +2,7 @@ import type { Meta } from '@storybook/react'; import type { CardNumber } from '../type'; import AddCardNumberInput from '../components/AddCardNumberInput'; -import usePasswordInput from '../hooks/usePasswordInput'; +import useComplicateInput from '../hooks/useComplicateInput'; import { isNumberInput } from '../utils/util'; const meta: Meta = { @@ -24,7 +24,7 @@ const meta: Meta = { export default meta; const AddHooks = () => { - const [cardNumber, onChangeCardNumber] = usePasswordInput({ + const [cardNumber, onChangeCardNumber] = useComplicateInput({ first: '', second: '', third: '', From 2ec399e69e0dd22125daa2e78217094fd7fd87f8 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 17:59:19 +0900 Subject: [PATCH 006/102] =?UTF-8?q?refactor:=20=EB=A1=9C=EC=A7=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardListPage.tsx | 4 ++-- src/utils/applicationUtil.ts | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/CardListPage.tsx b/src/pages/CardListPage.tsx index fa0ba1f84e..57d38b79ee 100644 --- a/src/pages/CardListPage.tsx +++ b/src/pages/CardListPage.tsx @@ -4,10 +4,10 @@ import type { CardType } from '../type'; import Card from '../components/Card'; import Header from '../components/Header'; import './CardListPage.css'; +import { fetchLocalStorage } from '../utils/applicationUtil'; const CardListPage = () => { - const cardList = JSON.parse(localStorage.getItem('cardList') ?? '[]'); - + const cardList = fetchLocalStorage('cardList', '[]'); const navigate = useNavigate(); const onAddButton = () => { diff --git a/src/utils/applicationUtil.ts b/src/utils/applicationUtil.ts index 0ab18b95bb..aad2871a4a 100644 --- a/src/utils/applicationUtil.ts +++ b/src/utils/applicationUtil.ts @@ -55,3 +55,7 @@ export const sumbitCard = ( }; postLocalStorage(card); }; + +export const fetchLocalStorage = (key: string, initial = '') => { + return JSON.parse(localStorage.getItem('cardList') ?? initial); +}; From cbaae5165444309e7b3586822ea3c2b4b93c6e52 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 22:42:07 +0900 Subject: [PATCH 007/102] =?UTF-8?q?refactor:=20StrictMode=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.tsx | 7 ++++++- src/router/AppRouter.tsx | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index ca543af444..546a88f842 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,12 @@ +import React from 'react'; import ReactDOM from 'react-dom/client'; import AppRouter from './router/AppRouter'; import './index.css'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); -root.render(); +root.render( + + + +); diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index ff3db3433b..c2cc6207f3 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import AddCardPage from '../pages/AddCardPage'; @@ -16,11 +15,7 @@ const router = createBrowserRouter([ ]); const AppRouter = () => { - return ( - - - - ); + return ; }; export default AppRouter; From 8a13ccf64d94de9b37cec1e8301976b714d1bbdc Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 22:51:09 +0900 Subject: [PATCH 008/102] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=A7=A4=EA=B0=9C=EB=B3=80=EC=88=98=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AddCardForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddCardForm.tsx b/src/components/AddCardForm.tsx index 548a0e0b1a..2d89055783 100644 --- a/src/components/AddCardForm.tsx +++ b/src/components/AddCardForm.tsx @@ -21,7 +21,7 @@ const AddCardForm = ({ }: FormCardAddProps) => { const navigate = useNavigate(); - const onSubmit = (e: React.FormEvent) => { + const onSubmit = (e: React.FormEvent) => { e.preventDefault(); try { sumbitCard('현대', cardNumber, cardOwner.value, cardExpire.value, securityCode.value, { From 5c14ee2d82785adf8ad87029d94c8049d54bf7f1 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 22:53:20 +0900 Subject: [PATCH 009/102] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9D=91=EC=A7=91=EB=8F=84=20=EC=A6=9D=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - try 문 안에서 처리되어야 하는 로직 합쳐줌 --- src/components/AddCardForm.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/AddCardForm.tsx b/src/components/AddCardForm.tsx index 2d89055783..8626f8cb6c 100644 --- a/src/components/AddCardForm.tsx +++ b/src/components/AddCardForm.tsx @@ -28,12 +28,11 @@ const AddCardForm = ({ first: cardPassword1.value, second: cardPassword2.value, }); + navigate('/'); } catch (error) { alert('중복된 카드 입니다.'); return; } - - navigate('/'); }; return ( From 00b1e1f5efc9c30ebf1b0ef326e4bcc2b1a61951 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 23:24:33 +0900 Subject: [PATCH 010/102] =?UTF-8?q?refactor:=20storybook=20default=20expor?= =?UTF-8?q?t=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/AddCardExpireDateInput.stories.tsx | 6 ++---- src/stories/AddCardNumberInput.stories.tsx | 6 ++---- src/stories/Card.stories.ts | 5 ++--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/stories/AddCardExpireDateInput.stories.tsx b/src/stories/AddCardExpireDateInput.stories.tsx index b72aca189d..b3f32abd1f 100644 --- a/src/stories/AddCardExpireDateInput.stories.tsx +++ b/src/stories/AddCardExpireDateInput.stories.tsx @@ -5,7 +5,7 @@ import useInput from '../hooks/useInput'; import { cardExpireCondition } from '../pages/cardInputCondition'; import { formatExpireDate } from '../utils/util'; -const meta: Meta = { +export default { title: 'AddCardExpireDateInput', component: AddCardExpireDateInput, decorators: [ @@ -19,9 +19,7 @@ const meta: Meta = {
    ), ], -}; - -export default meta; +} as Meta; const AddInputHooks = () => { const cardExpireDate = useInput('', cardExpireCondition, formatExpireDate); diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index 817f7d671d..38456cb8f6 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -5,7 +5,7 @@ import AddCardNumberInput from '../components/AddCardNumberInput'; import useComplicateInput from '../hooks/useComplicateInput'; import { isNumberInput } from '../utils/util'; -const meta: Meta = { +export default { title: 'AddCardNumberInput', component: AddCardNumberInput, decorators: [ @@ -19,9 +19,7 @@ const meta: Meta = {
    ), ], -}; - -export default meta; +} as Meta; const AddHooks = () => { const [cardNumber, onChangeCardNumber] = useComplicateInput({ diff --git a/src/stories/Card.stories.ts b/src/stories/Card.stories.ts index f0fedbfacb..287894222d 100644 --- a/src/stories/Card.stories.ts +++ b/src/stories/Card.stories.ts @@ -2,12 +2,11 @@ import type { Meta, StoryObj } from '@storybook/react'; import Card from '../components/Card'; -const meta: Meta = { +export default { title: 'Card', component: Card, -}; +} as Meta; -export default meta; type Story = StoryObj; export const Primary: Story = { From 904df4c6bfd9d68b1e5a3d119f5d8202792b5e58 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 23:33:00 +0900 Subject: [PATCH 011/102] =?UTF-8?q?feat:=20CardOwner=20=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=EB=B6=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/AddCardOwnerInput.stories.tsx | 35 +++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/stories/AddCardOwnerInput.stories.tsx diff --git a/src/stories/AddCardOwnerInput.stories.tsx b/src/stories/AddCardOwnerInput.stories.tsx new file mode 100644 index 0000000000..6d967787c5 --- /dev/null +++ b/src/stories/AddCardOwnerInput.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import AddCardOwnerInput from '../components/AddCardOwnerInput'; +import { stringToUpperCase } from '../utils/util'; +import { cardOwnerCondition } from '../pages/cardInputCondition'; +import useInput from '../hooks/useInput'; +import { APP_WIDTH } from './constants'; + +export default { + title: 'AddCardOwnerInput', + component: AddCardOwnerInput, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +} as Meta; + +type Story = StoryObj; + +const AddHooks = () => { + const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); + + return ; +}; + +export const Primary: Story = { + render: () => , +}; From 22df1f8b6bac887722335d152deff2ee353ffe13 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 23:33:40 +0900 Subject: [PATCH 012/102] =?UTF-8?q?refactor:=20decorator=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=86=8D=EC=84=B1=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/AddCardExpireDateInput.stories.tsx | 3 ++- src/stories/AddCardNumberInput.stories.tsx | 3 ++- src/stories/constants.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 src/stories/constants.ts diff --git a/src/stories/AddCardExpireDateInput.stories.tsx b/src/stories/AddCardExpireDateInput.stories.tsx index b3f32abd1f..1a78120897 100644 --- a/src/stories/AddCardExpireDateInput.stories.tsx +++ b/src/stories/AddCardExpireDateInput.stories.tsx @@ -4,6 +4,7 @@ import AddCardExpireDateInput from '../components/AddCardExpireDateInput'; import useInput from '../hooks/useInput'; import { cardExpireCondition } from '../pages/cardInputCondition'; import { formatExpireDate } from '../utils/util'; +import { APP_WIDTH } from './constants'; export default { title: 'AddCardExpireDateInput', @@ -12,7 +13,7 @@ export default { (Story) => (
    diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index 38456cb8f6..8aefa8f265 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -4,6 +4,7 @@ import type { CardNumber } from '../type'; import AddCardNumberInput from '../components/AddCardNumberInput'; import useComplicateInput from '../hooks/useComplicateInput'; import { isNumberInput } from '../utils/util'; +import { APP_WIDTH } from './constants'; export default { title: 'AddCardNumberInput', @@ -12,7 +13,7 @@ export default { (Story) => (
    diff --git a/src/stories/constants.ts b/src/stories/constants.ts new file mode 100644 index 0000000000..421c1aaebc --- /dev/null +++ b/src/stories/constants.ts @@ -0,0 +1 @@ +export const APP_WIDTH = '318px'; From 6c421cb6cc1e1f7a79be3e30d068eaf7d47941c7 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 23:37:59 +0900 Subject: [PATCH 013/102] =?UTF-8?q?feat:=20AddCardPasswordInput=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/AddCardPasswordInput.stories.tsx | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/stories/AddCardPasswordInput.stories.tsx diff --git a/src/stories/AddCardPasswordInput.stories.tsx b/src/stories/AddCardPasswordInput.stories.tsx new file mode 100644 index 0000000000..71f7909072 --- /dev/null +++ b/src/stories/AddCardPasswordInput.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import AddCardPasswordInput from '../components/AddCardPasswordInput'; +import { handleNumberInput } from '../utils/util'; +import { cardPasswordCondition } from '../pages/cardInputCondition'; +import useInput from '../hooks/useInput'; +import { APP_WIDTH } from './constants'; + +export default { + title: 'AddCardPasswordInput', + component: AddCardPasswordInput, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +} as Meta; + +type Story = StoryObj; + +const AddHooks = () => { + const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); + const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); + + return ; +}; + +export const Primary: Story = { + render: () => , +}; From ca2a22338e71672b413bf9166c6bfb4ec86e9886 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 25 Apr 2023 23:41:27 +0900 Subject: [PATCH 014/102] =?UTF-8?q?feat:=20AddCardSecurityCodeInput=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCardSecurityCodeInput.stories.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/stories/AddCardSecurityCodeInput.stories.tsx diff --git a/src/stories/AddCardSecurityCodeInput.stories.tsx b/src/stories/AddCardSecurityCodeInput.stories.tsx new file mode 100644 index 0000000000..3232e54fdd --- /dev/null +++ b/src/stories/AddCardSecurityCodeInput.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import AddCardSecurityCodeInput from '../components/AddCardSecurityCodeInput'; +import { handleNumberInput } from '../utils/util'; +import { securityCodeCondition } from '../pages/cardInputCondition'; +import useInput from '../hooks/useInput'; +import { APP_WIDTH } from './constants'; + +export default { + title: 'AddCardSecurityCodeInput', + component: AddCardSecurityCodeInput, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +} as Meta; + +type Story = StoryObj; + +const AddHooks = () => { + const securityCode = useInput('', securityCodeCondition, handleNumberInput); + + return ; +}; + +export const Primary: Story = { + render: () => , +}; From a71d5c1774b053cc6c08cfe077e8b9121420832d Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 14:40:52 +0900 Subject: [PATCH 015/102] =?UTF-8?q?fix:=20expireDateInput=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AddCardExpireDateInput.css | 16 ++++++++++--- src/components/AddCardExpireDateInput.tsx | 28 ++++++++++++++++------- src/components/AddCardForm.tsx | 21 ++++++++++++----- src/components/Card.tsx | 5 ++-- src/pages/AddCardPage.tsx | 21 +++++++++-------- src/pages/CardListPage.tsx | 3 ++- src/pages/cardInputCondition.ts | 14 ++++++++++++ src/stories/Card.stories.ts | 3 ++- src/type.d.ts | 12 ++++++---- src/utils/applicationUtil.ts | 6 +++-- src/utils/util.ts | 15 +++--------- 11 files changed, 95 insertions(+), 49 deletions(-) diff --git a/src/components/AddCardExpireDateInput.css b/src/components/AddCardExpireDateInput.css index 6b5cd0cc43..08dd7af2e2 100644 --- a/src/components/AddCardExpireDateInput.css +++ b/src/components/AddCardExpireDateInput.css @@ -1,12 +1,22 @@ .card-expired-input-container { margin-top: 20px; + width: 137px; +} + +.card-expired-input { + display: flex; + align-items: center; + justify-content: center; + background-color: #ecebf1; + width: 100%; + height: 47px; + border-radius: 7px; } .card-expired { - display: block; text-align: center; border: none; background-color: #ecebf1; - width: 137px; - border-radius: 7px; + height: 30px; + width: 30px; } diff --git a/src/components/AddCardExpireDateInput.tsx b/src/components/AddCardExpireDateInput.tsx index 4e7a3ee862..1b280dea8b 100644 --- a/src/components/AddCardExpireDateInput.tsx +++ b/src/components/AddCardExpireDateInput.tsx @@ -1,17 +1,29 @@ import { AddCardExpireDateInputProps } from '../type'; import './AddCardExpireDateInput.css'; -const AddCardExpireDateInput = ({ cardExpire }: AddCardExpireDateInputProps) => { +const AddCardExpireDateInput = ({ expireMonth, expireYear }: AddCardExpireDateInputProps) => { return (
    만료일 - +
    + + / + +
    ); }; diff --git a/src/components/AddCardForm.tsx b/src/components/AddCardForm.tsx index 8626f8cb6c..68e6d1ed5b 100644 --- a/src/components/AddCardForm.tsx +++ b/src/components/AddCardForm.tsx @@ -13,7 +13,8 @@ import './AddCardForm.css'; const AddCardForm = ({ cardNumber, onCardNumberChange, - cardExpire, + expireMonth, + expireYear, cardOwner, securityCode, cardPassword1, @@ -24,10 +25,18 @@ const AddCardForm = ({ const onSubmit = (e: React.FormEvent) => { e.preventDefault(); try { - sumbitCard('현대', cardNumber, cardOwner.value, cardExpire.value, securityCode.value, { - first: cardPassword1.value, - second: cardPassword2.value, - }); + sumbitCard( + '현대', + cardNumber, + cardOwner.value, + expireMonth.value, + expireYear.value, + securityCode.value, + { + first: cardPassword1.value, + second: cardPassword2.value, + } + ); navigate('/'); } catch (error) { alert('중복된 카드 입니다.'); @@ -38,7 +47,7 @@ const AddCardForm = ({ return (
    - + diff --git a/src/components/Card.tsx b/src/components/Card.tsx index f99bc71136..85b4ac3c7e 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -2,7 +2,7 @@ import type { CardProps } from '../type'; import { changeNumberToMask } from '../utils/util'; import './Card.css'; -const Card = ({ cardType, cardNumber, cardOwner, expired }: CardProps) => { +const Card = ({ cardType, cardNumber, cardOwner, expireYear, expireMonth }: CardProps) => { return (
    @@ -19,10 +19,11 @@ const Card = ({ cardType, cardNumber, cardOwner, expired }: CardProps) => {
    {cardOwner} - {expired} + {`${expireYear}/${expireMonth}`}
    ); }; +// TODO: 만료일 입력 로직 외부로 분리 export default Card; diff --git a/src/pages/AddCardPage.tsx b/src/pages/AddCardPage.tsx index 6da75ed9fa..880747ea65 100644 --- a/src/pages/AddCardPage.tsx +++ b/src/pages/AddCardPage.tsx @@ -7,17 +7,13 @@ import AddCardForm from '../components/AddCardForm'; import Header from '../components/Header'; import useInput from '../hooks/useInput'; import useComplicateInput from '../hooks/useComplicateInput'; +import { handleNumberInput, identity, isNumberInput, stringToUpperCase } from '../utils/util'; import { - formatExpireDate, - handleNumberInput, - isNumberInput, - stringToUpperCase, -} from '../utils/util'; -import { - cardExpireCondition, securityCodeCondition, cardOwnerCondition, cardPasswordCondition, + cardExpireMonthCondition, + cardExpireYearCondition, } from './cardInputCondition'; import BackButtonImg from '../asset/back_button.png'; import './AddCardPage.css'; @@ -32,7 +28,8 @@ const AddCardPage = () => { third: '', fourth: '', }); - const cardExpire = useInput('', cardExpireCondition, formatExpireDate); + const expireMonth = useInput('', cardExpireMonthCondition, identity); + const expireYear = useInput('', cardExpireYearCondition, identity); const securityCode = useInput('', securityCodeCondition, handleNumberInput); const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); @@ -42,6 +39,8 @@ const AddCardPage = () => { navigate('/'); }, [navigate]); + // TODO: e.target.value.length 가독성 개선 => 변수화 + // TODO: 이벤트 핸들러 리팩터링.. 네이밍 + 구조 const onCardNumberChange = useCallback( (e: React.ChangeEvent) => { const lastWord = e.target.value[e.target.value.length - 1]; @@ -67,12 +66,14 @@ const AddCardPage = () => { cardType={cardType} cardNumber={cardNumber} cardOwner={cardOwner.value} - expired={cardExpire.value} + expireMonth={expireMonth.value} + expireYear={expireYear.value} /> { cardType={card.cardType} cardNumber={card.cardNumber} cardOwner={card.cardOwner} - expired={card.expired} + expireMonth={card.expireMonth} + expireYear={card.expireYear} /> )) )} diff --git a/src/pages/cardInputCondition.ts b/src/pages/cardInputCondition.ts index 9cdc323b36..f135e51cc9 100644 --- a/src/pages/cardInputCondition.ts +++ b/src/pages/cardInputCondition.ts @@ -1,5 +1,6 @@ import { isAlphabetInput, isNumberInput } from '../utils/util'; +// TODO: 훅 분리 고민 export const cardExpireCondition = (e: React.ChangeEvent) => { const length = e.target.value.length; const lastWord = e.target.value[length - 1]; @@ -19,3 +20,16 @@ export const cardOwnerCondition = (e: React.ChangeEvent) => { export const cardPasswordCondition = (e: React.ChangeEvent) => { return e.target.value.length <= 1; }; + +export const cardExpireMonthCondition = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + const month = +inputValue; + return month >= 0 && month <= 12 && inputValue.length <= 2; +}; + +const now = new Date(); + +export const cardExpireYearCondition = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + return inputValue.length <= 2; +}; diff --git a/src/stories/Card.stories.ts b/src/stories/Card.stories.ts index 287894222d..a1273f5d6f 100644 --- a/src/stories/Card.stories.ts +++ b/src/stories/Card.stories.ts @@ -18,6 +18,7 @@ export const Primary: Story = { fourth: '1234', }, cardOwner: 'LEE', - expired: '01/01', + expireYear: '01', + expireMonth: '01', }, }; diff --git a/src/type.d.ts b/src/type.d.ts index e06c24aa27..d818dba25c 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -9,7 +9,8 @@ type CardProps = { cardType: string; cardNumber: CardNumber; cardOwner: string; - expired: string; + expireMonth: string; + expireYear: string; }; export type CardPassword = { @@ -21,7 +22,8 @@ export type CardType = { cardType: string; cardNumber: CardNumber; cardOwner: string; - expired: string; + expireMonth: string; + expireYear: string; securityCode: string; cardPassword: CardPassword; }; @@ -35,18 +37,20 @@ export type PasswordInputHook = [T, (e: React.ChangeEvent) export type FormCardAddProps = { cardNumber: CardNumber; - cardExpire: InputHook; cardOwner: InputHook; securityCode: InputHook; cardPassword1: InputHook; cardPassword2: InputHook; onCardNumberChange: (e: React.ChangeEvent) => void; + expireMonth: InputHook; + expireYear: InputHook; }; export type AddCardNumberInputProps = Pick & { onChange: (e: React.ChangeEvent) => void; }; -export type AddCardExpireDateInputProps = Pick; + +export type AddCardExpireDateInputProps = Pick; export type AddCardOwnerInputProps = Pick; export type AddCardSecurityCodeInputProps = Pick; export type AddCardPasswordInputProps = Pick; diff --git a/src/utils/applicationUtil.ts b/src/utils/applicationUtil.ts index aad2871a4a..d12205eaa6 100644 --- a/src/utils/applicationUtil.ts +++ b/src/utils/applicationUtil.ts @@ -38,7 +38,8 @@ export const sumbitCard = ( cardType: string, cardNumber: CardNumber, cardOwner: string, - cardExpire: string, + expireMonth: string, + expireYear: string, securityCode: string, cardPassword: CardPassword ) => { @@ -46,7 +47,8 @@ export const sumbitCard = ( cardType, cardNumber, cardOwner, - expired: cardExpire, + expireMonth, + expireYear, securityCode, cardPassword: { first: cardPassword.first, diff --git a/src/utils/util.ts b/src/utils/util.ts index 1583cc4c38..1da8cb490e 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,19 +1,8 @@ -const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; +export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); export const formatExpireDate = (expireDate: string): string => { - if (expireDate[0] !== '0' && expireDate[0] !== '1') return ''; - if (expireDate.length > 1 && !MONTH_DATA.includes(`${expireDate[0]}${expireDate[1]}`)) return ''; - - const nowLength = expireDate.length; - const nowString = expireDate.split(''); - if (nowLength === 3 && nowString.includes('/')) { - return expireDate.slice(0, -1); - } - if (nowLength === 3) { - return `${expireDate[0]}${expireDate[1]}/${expireDate[2]}`; - } return expireDate; }; @@ -40,3 +29,5 @@ export const changeNumberToMask = (data: string): string => { export const stringToUpperCase = (data: string): string => { return data.toUpperCase(); }; + +export const identity = (v: any) => v; From b3ab87810943b8a771c5101a3d1f103396d8ca1e Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:10:53 +0900 Subject: [PATCH 016/102] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/{ => AddCard}/AddCardPage.css | 0 src/pages/{ => AddCard}/AddCardPage.tsx | 18 +++++++++--------- .../components/AddCardExpireDateInput.css | 0 .../components/AddCardExpireDateInput.tsx | 2 +- .../AddCard}/components/AddCardForm.css | 0 .../AddCard}/components/AddCardForm.tsx | 6 +++--- .../AddCard}/components/AddCardNumberInput.css | 0 .../AddCard}/components/AddCardNumberInput.tsx | 3 ++- .../AddCard}/components/AddCardOwnerInput.css | 0 .../AddCard}/components/AddCardOwnerInput.tsx | 2 +- .../components/AddCardPasswordInput.css | 0 .../components/AddCardPasswordInput.tsx | 4 ++-- .../components/AddCardSecurityCodeInput.css | 0 .../components/AddCardSecurityCodeInput.tsx | 4 ++-- src/router/AppRouter.tsx | 2 +- src/stories/AddCardExpireDateInput.stories.tsx | 2 +- src/stories/AddCardNumberInput.stories.tsx | 2 +- src/stories/AddCardOwnerInput.stories.tsx | 2 +- src/stories/AddCardPasswordInput.stories.tsx | 2 +- .../AddCardSecurityCodeInput.stories.tsx | 2 +- 20 files changed, 26 insertions(+), 25 deletions(-) rename src/pages/{ => AddCard}/AddCardPage.css (100%) rename src/pages/{ => AddCard}/AddCardPage.tsx (85%) rename src/{ => pages/AddCard}/components/AddCardExpireDateInput.css (100%) rename src/{ => pages/AddCard}/components/AddCardExpireDateInput.tsx (92%) rename src/{ => pages/AddCard}/components/AddCardForm.css (100%) rename src/{ => pages/AddCard}/components/AddCardForm.tsx (93%) rename src/{ => pages/AddCard}/components/AddCardNumberInput.css (100%) rename src/{ => pages/AddCard}/components/AddCardNumberInput.tsx (91%) rename src/{ => pages/AddCard}/components/AddCardOwnerInput.css (100%) rename src/{ => pages/AddCard}/components/AddCardOwnerInput.tsx (90%) rename src/{ => pages/AddCard}/components/AddCardPasswordInput.css (100%) rename src/{ => pages/AddCard}/components/AddCardPasswordInput.tsx (88%) rename src/{ => pages/AddCard}/components/AddCardSecurityCodeInput.css (100%) rename src/{ => pages/AddCard}/components/AddCardSecurityCodeInput.tsx (88%) diff --git a/src/pages/AddCardPage.css b/src/pages/AddCard/AddCardPage.css similarity index 100% rename from src/pages/AddCardPage.css rename to src/pages/AddCard/AddCardPage.css diff --git a/src/pages/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx similarity index 85% rename from src/pages/AddCardPage.tsx rename to src/pages/AddCard/AddCardPage.tsx index 880747ea65..500757bfde 100644 --- a/src/pages/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -1,21 +1,21 @@ import React, { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { CardNumber } from '../type'; -import Card from '../components/Card'; -import AddCardForm from '../components/AddCardForm'; -import Header from '../components/Header'; -import useInput from '../hooks/useInput'; -import useComplicateInput from '../hooks/useComplicateInput'; -import { handleNumberInput, identity, isNumberInput, stringToUpperCase } from '../utils/util'; +import type { CardNumber } from '../../type'; +import Card from '../../components/Card'; +import AddCardForm from './components/AddCardForm'; +import Header from '../../components/Header'; +import useInput from '../../hooks/useInput'; +import useComplicateInput from '../../hooks/useComplicateInput'; +import { handleNumberInput, identity, isNumberInput, stringToUpperCase } from '../../utils/util'; import { securityCodeCondition, cardOwnerCondition, cardPasswordCondition, cardExpireMonthCondition, cardExpireYearCondition, -} from './cardInputCondition'; -import BackButtonImg from '../asset/back_button.png'; +} from '../cardInputCondition'; +import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; const AddCardPage = () => { diff --git a/src/components/AddCardExpireDateInput.css b/src/pages/AddCard/components/AddCardExpireDateInput.css similarity index 100% rename from src/components/AddCardExpireDateInput.css rename to src/pages/AddCard/components/AddCardExpireDateInput.css diff --git a/src/components/AddCardExpireDateInput.tsx b/src/pages/AddCard/components/AddCardExpireDateInput.tsx similarity index 92% rename from src/components/AddCardExpireDateInput.tsx rename to src/pages/AddCard/components/AddCardExpireDateInput.tsx index 1b280dea8b..88862beefd 100644 --- a/src/components/AddCardExpireDateInput.tsx +++ b/src/pages/AddCard/components/AddCardExpireDateInput.tsx @@ -1,4 +1,4 @@ -import { AddCardExpireDateInputProps } from '../type'; +import { AddCardExpireDateInputProps } from '../../../type'; import './AddCardExpireDateInput.css'; const AddCardExpireDateInput = ({ expireMonth, expireYear }: AddCardExpireDateInputProps) => { diff --git a/src/components/AddCardForm.css b/src/pages/AddCard/components/AddCardForm.css similarity index 100% rename from src/components/AddCardForm.css rename to src/pages/AddCard/components/AddCardForm.css diff --git a/src/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx similarity index 93% rename from src/components/AddCardForm.tsx rename to src/pages/AddCard/components/AddCardForm.tsx index 68e6d1ed5b..7fd42de5c6 100644 --- a/src/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import type { FormCardAddProps } from '../type'; -import { sumbitCard } from '../utils/applicationUtil'; +import type { FormCardAddProps } from '../../../type'; +import { sumbitCard } from '../../../utils/applicationUtil'; import { useNavigate } from 'react-router-dom'; -import AddCardNumberInput from './AddCardNumberInput'; import AddCardExpireDateInput from './AddCardExpireDateInput'; import AddCardOwnerInput from './AddCardOwnerInput'; import AddCardSecurityCodeInput from './AddCardSecurityCodeInput'; import AddCardPasswordInput from './AddCardPasswordInput'; import './AddCardForm.css'; +import AddCardNumberInput from './AddCardNumberInput'; const AddCardForm = ({ cardNumber, diff --git a/src/components/AddCardNumberInput.css b/src/pages/AddCard/components/AddCardNumberInput.css similarity index 100% rename from src/components/AddCardNumberInput.css rename to src/pages/AddCard/components/AddCardNumberInput.css diff --git a/src/components/AddCardNumberInput.tsx b/src/pages/AddCard/components/AddCardNumberInput.tsx similarity index 91% rename from src/components/AddCardNumberInput.tsx rename to src/pages/AddCard/components/AddCardNumberInput.tsx index f042b51077..aae1e6841b 100644 --- a/src/components/AddCardNumberInput.tsx +++ b/src/pages/AddCard/components/AddCardNumberInput.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import type { AddCardNumberInputProps } from '../type'; +import type { AddCardNumberInputProps } from '../../../type'; import './AddCardNumberInput.css'; const AddCardNumberInput = ({ cardNumber, onChange }: AddCardNumberInputProps) => { + // TODO: memo를 적용시키려면 객체를 내리면 안된다. const { first, second, third, fourth } = cardNumber; return (
    diff --git a/src/components/AddCardOwnerInput.css b/src/pages/AddCard/components/AddCardOwnerInput.css similarity index 100% rename from src/components/AddCardOwnerInput.css rename to src/pages/AddCard/components/AddCardOwnerInput.css diff --git a/src/components/AddCardOwnerInput.tsx b/src/pages/AddCard/components/AddCardOwnerInput.tsx similarity index 90% rename from src/components/AddCardOwnerInput.tsx rename to src/pages/AddCard/components/AddCardOwnerInput.tsx index 93ec989bf3..359fb69deb 100644 --- a/src/components/AddCardOwnerInput.tsx +++ b/src/pages/AddCard/components/AddCardOwnerInput.tsx @@ -1,4 +1,4 @@ -import type { AddCardOwnerInputProps } from '../type'; +import type { AddCardOwnerInputProps } from '../../../type'; import './AddCardOwnerInput.css'; const AddCardOwnerInput = ({ cardOwner }: AddCardOwnerInputProps) => { diff --git a/src/components/AddCardPasswordInput.css b/src/pages/AddCard/components/AddCardPasswordInput.css similarity index 100% rename from src/components/AddCardPasswordInput.css rename to src/pages/AddCard/components/AddCardPasswordInput.css diff --git a/src/components/AddCardPasswordInput.tsx b/src/pages/AddCard/components/AddCardPasswordInput.tsx similarity index 88% rename from src/components/AddCardPasswordInput.tsx rename to src/pages/AddCard/components/AddCardPasswordInput.tsx index 72e00e864a..3382e307d8 100644 --- a/src/components/AddCardPasswordInput.tsx +++ b/src/pages/AddCard/components/AddCardPasswordInput.tsx @@ -1,5 +1,5 @@ -import type { AddCardPasswordInputProps } from '../type'; -import passwordDotImg from '../asset/password_dot.png'; +import type { AddCardPasswordInputProps } from '../../../type'; +import passwordDotImg from '../../../asset/password_dot.png'; import './AddCardPasswordInput.css'; const AddCardPasswordInput = ({ cardPassword1, cardPassword2 }: AddCardPasswordInputProps) => { diff --git a/src/components/AddCardSecurityCodeInput.css b/src/pages/AddCard/components/AddCardSecurityCodeInput.css similarity index 100% rename from src/components/AddCardSecurityCodeInput.css rename to src/pages/AddCard/components/AddCardSecurityCodeInput.css diff --git a/src/components/AddCardSecurityCodeInput.tsx b/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx similarity index 88% rename from src/components/AddCardSecurityCodeInput.tsx rename to src/pages/AddCard/components/AddCardSecurityCodeInput.tsx index a1dc3cfcce..a832393c25 100644 --- a/src/components/AddCardSecurityCodeInput.tsx +++ b/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx @@ -1,5 +1,5 @@ -import type { AddCardSecurityCodeInputProps } from '../type'; -import cvcInfo from '../asset/cvc_info.png'; +import type { AddCardSecurityCodeInputProps } from '../../../type'; +import cvcInfo from '../../../asset/cvc_info.png'; import './AddCardSecurityCodeInput.css'; diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index c2cc6207f3..6e76efe2dd 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -1,6 +1,6 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import AddCardPage from '../pages/AddCardPage'; +import AddCardPage from '../pages/AddCard/AddCardPage'; import CardListPage from '../pages/CardListPage'; const router = createBrowserRouter([ diff --git a/src/stories/AddCardExpireDateInput.stories.tsx b/src/stories/AddCardExpireDateInput.stories.tsx index 1a78120897..d104e5b889 100644 --- a/src/stories/AddCardExpireDateInput.stories.tsx +++ b/src/stories/AddCardExpireDateInput.stories.tsx @@ -1,6 +1,6 @@ import type { Meta } from '@storybook/react'; -import AddCardExpireDateInput from '../components/AddCardExpireDateInput'; +import AddCardExpireDateInput from '../pages/AddCard/components/AddCardExpireDateInput'; import useInput from '../hooks/useInput'; import { cardExpireCondition } from '../pages/cardInputCondition'; import { formatExpireDate } from '../utils/util'; diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index 8aefa8f265..310f7b89d2 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react'; import type { CardNumber } from '../type'; -import AddCardNumberInput from '../components/AddCardNumberInput'; +import AddCardNumberInput from '../pages/AddCard/components/AddCardNumberInput'; import useComplicateInput from '../hooks/useComplicateInput'; import { isNumberInput } from '../utils/util'; import { APP_WIDTH } from './constants'; diff --git a/src/stories/AddCardOwnerInput.stories.tsx b/src/stories/AddCardOwnerInput.stories.tsx index 6d967787c5..685685b265 100644 --- a/src/stories/AddCardOwnerInput.stories.tsx +++ b/src/stories/AddCardOwnerInput.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardOwnerInput from '../components/AddCardOwnerInput'; +import AddCardOwnerInput from '../pages/AddCard/components/AddCardOwnerInput'; import { stringToUpperCase } from '../utils/util'; import { cardOwnerCondition } from '../pages/cardInputCondition'; import useInput from '../hooks/useInput'; diff --git a/src/stories/AddCardPasswordInput.stories.tsx b/src/stories/AddCardPasswordInput.stories.tsx index 71f7909072..1b6010c287 100644 --- a/src/stories/AddCardPasswordInput.stories.tsx +++ b/src/stories/AddCardPasswordInput.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardPasswordInput from '../components/AddCardPasswordInput'; +import AddCardPasswordInput from '../pages/AddCard/components/AddCardPasswordInput'; import { handleNumberInput } from '../utils/util'; import { cardPasswordCondition } from '../pages/cardInputCondition'; import useInput from '../hooks/useInput'; diff --git a/src/stories/AddCardSecurityCodeInput.stories.tsx b/src/stories/AddCardSecurityCodeInput.stories.tsx index 3232e54fdd..d4e3c87fbd 100644 --- a/src/stories/AddCardSecurityCodeInput.stories.tsx +++ b/src/stories/AddCardSecurityCodeInput.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardSecurityCodeInput from '../components/AddCardSecurityCodeInput'; +import AddCardSecurityCodeInput from '../pages/AddCard/components/AddCardSecurityCodeInput'; import { handleNumberInput } from '../utils/util'; import { securityCodeCondition } from '../pages/cardInputCondition'; import useInput from '../hooks/useInput'; From fcd6e018ae83a47fb7ecab0c7caa60a83f4478d1 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:11:02 +0900 Subject: [PATCH 017/102] =?UTF-8?q?docs:=20=EC=B6=94=EA=B0=80=EB=90=9C=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/REQUIREMENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 1c9d588c37..02a9f475fa 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -28,6 +28,10 @@ - [x] 카드번호 뒷 8자리는 온점으로 표시한다. - [x] "?" 버튼에 hover 상태면 cvc에 대한 설명을 보여줌. - [x] (예외) 입력값이 잘못 들어오면 오류 메시지를 띄워준다. + - [ ] 카드를 클릭하면 bottom sheet을 연다. + - [ ] bottom sheet에서는 8개의 은행을 사용자가 선택할 수 있다. 은행을 선택하면 카드 좌측 상단에 선택된 카드 회사 이름이 뜨고, 카드 회사의 메인 색깔로 변경된다. + - [ ] 카드 등록이 완료되면 카드에 별칭을 부여할 수 있다. + - [ ] 별칭은 등록할 수도 있고 안할수도 있다. ### 카드 컴포넌트 From e5ec76e7e824f35def21f495c5ca48a140711d28 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:11:22 +0900 Subject: [PATCH 018/102] =?UTF-8?q?feat:=20BottomSheet=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B8=B0=EB=B3=B8=20=ED=8B=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.tsx | 11 +++++++++++ src/type.d.ts | 1 + 2 files changed, 12 insertions(+) create mode 100644 src/components/BottomSheet.tsx diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx new file mode 100644 index 0000000000..e54c6cc239 --- /dev/null +++ b/src/components/BottomSheet.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren } from 'react'; + +type BottomSheetProps = { + isOpen: boolean; + onChangeOpen: () => void; +}; +const BottomSheet = ({ children }: PropsWithChildren) => { + return
    {children}
    ; +}; + +export default BottomSheet; diff --git a/src/type.d.ts b/src/type.d.ts index d818dba25c..3a78cc4968 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -11,6 +11,7 @@ type CardProps = { cardOwner: string; expireMonth: string; expireYear: string; + onClick?: (e: React.MouseEvent) => void; }; export type CardPassword = { From a0e4176682314ca1c2c711570ed20020757cacd8 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:11:43 +0900 Subject: [PATCH 019/102] =?UTF-8?q?refactor:=20children=20=EB=B9=8C?= =?UTF-8?q?=ED=8A=B8=EC=9D=B8=20=ED=83=80=EC=9E=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 6095da20d6..50d740a39b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,10 +2,7 @@ import React from 'react'; import './Header.css'; -type HeaderType = { - children: React.ReactNode; -}; -const Header = ({ children }: HeaderType) => { +const Header = ({ children }: React.PropsWithChildren) => { return
    {children}
    ; }; From c8f5c16d9b9d66c6906a455a939aaa1a6856235d Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:11:56 +0900 Subject: [PATCH 020/102] =?UTF-8?q?refactor:=20import=20=EC=99=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/.storybook/main.ts b/.storybook/main.ts index c4367c9ad3..5588dcb871 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,5 @@ import type { StorybookConfig } from '@storybook/react-webpack5'; + const config: StorybookConfig = { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ From 0c612357f7c9e39c5db58485d845aa6b97ae253a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:12:11 +0900 Subject: [PATCH 021/102] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20index.html=20=EC=BB=A4=EC=8A=A4=ED=85=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/index.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index b925c92cda..57d60bf1dd 100644 --- a/public/index.html +++ b/public/index.html @@ -4,14 +4,13 @@ - - + 💳 Payments - +
    From 5840ed7a66b87e59be9057f5380d5eaf99ee6ca3 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:28:24 +0900 Subject: [PATCH 022/102] =?UTF-8?q?refactor:=20=EB=B2=94=EC=9A=A9=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=93=B0=EC=9D=B4=EB=8A=94=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/constants.ts | 1 + src/utils/util.ts | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/utils/constants.ts diff --git a/src/utils/constants.ts b/src/utils/constants.ts new file mode 100644 index 0000000000..f8ebf6dcfb --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1 @@ +export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; diff --git a/src/utils/util.ts b/src/utils/util.ts index 1da8cb490e..6c3583b1c6 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,4 +1,3 @@ -export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); From 71962f766898686dd02a664edc1b91072df2d237 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:29:01 +0900 Subject: [PATCH 023/102] =?UTF-8?q?feat:=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EA=B9=8C=EC=A7=80=20=EA=B0=80=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20Custom=20Hook=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInputs.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/hooks/useInputs.ts diff --git a/src/hooks/useInputs.ts b/src/hooks/useInputs.ts new file mode 100644 index 0000000000..b3e3420420 --- /dev/null +++ b/src/hooks/useInputs.ts @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +type INPUT_STATUS = 'INIT' | 'VALID' | 'INVALID'; + +const useInputs = (formatDispatcher: (str: string) => INPUT_STATUS, init = '') => { + const [status, setStatus] = useState('INIT'); + const [value, setValue] = useState(''); + + const onChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value; + setStatus(formatDispatcher(inputValue)); + setValue(inputValue); + }; + + return { value, status, onChange }; +}; + +export default useInputs; From ac779222362c6942688fea13ca8052b5834a1948 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 18:33:56 +0900 Subject: [PATCH 024/102] =?UTF-8?q?feat:=20=EC=9E=AC=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8A=94=20InputContainer,=20ErrorMessage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ErrorMessage.tsx | 5 +++++ src/components/InputContainer.tsx | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/components/ErrorMessage.tsx create mode 100644 src/components/InputContainer.tsx diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx new file mode 100644 index 0000000000..8e07a9ce56 --- /dev/null +++ b/src/components/ErrorMessage.tsx @@ -0,0 +1,5 @@ +const ErrorMessage = () => { + return
    ; +}; + +export default ErrorMessage; diff --git a/src/components/InputContainer.tsx b/src/components/InputContainer.tsx new file mode 100644 index 0000000000..be0f6d12a9 --- /dev/null +++ b/src/components/InputContainer.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren } from 'react'; +import ErrorMessage from './ErrorMessage'; + +type InputContainerProps = { + className: string; +}; +const InputContainer = ({ children, className }: PropsWithChildren) => { + return ( +
    + {children} + +
    + ); +}; + +export default InputContainer; From 52998eef64bb7dcae8abaf1e6f30bc3d4fe74d4b Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:00:00 +0900 Subject: [PATCH 025/102] =?UTF-8?q?feat:=20ErrorMessage=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ErrorMessage.css | 4 ++++ src/components/ErrorMessage.tsx | 8 ++++++-- src/pages/AddCard/domain/domain.ts | 4 ++++ src/type.d.ts | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/components/ErrorMessage.css create mode 100644 src/pages/AddCard/domain/domain.ts diff --git a/src/components/ErrorMessage.css b/src/components/ErrorMessage.css new file mode 100644 index 0000000000..0319af3845 --- /dev/null +++ b/src/components/ErrorMessage.css @@ -0,0 +1,4 @@ +.error-message { + color: red; + font-size: 10px; +} diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx index 8e07a9ce56..94a37422af 100644 --- a/src/components/ErrorMessage.tsx +++ b/src/components/ErrorMessage.tsx @@ -1,5 +1,9 @@ -const ErrorMessage = () => { - return
    ; +import { getErrorMessage } from '../pages/AddCard/domain/domain'; +import { ErrorMessageProps } from '../type'; +import './ErrorMessage.css'; + +const ErrorMessage = ({ inputType, status }: ErrorMessageProps) => { + return
    {getErrorMessage(inputType, status)}
    ; }; export default ErrorMessage; diff --git a/src/pages/AddCard/domain/domain.ts b/src/pages/AddCard/domain/domain.ts new file mode 100644 index 0000000000..f84e294d75 --- /dev/null +++ b/src/pages/AddCard/domain/domain.ts @@ -0,0 +1,4 @@ +// TODO: 구체화 하기 +export const getErrorMessage = (inputType: string, status: InputStatus) => { + return status === 'INVALID' ? '잘못된 입력입니다.' : ''; +}; diff --git a/src/type.d.ts b/src/type.d.ts index 3a78cc4968..1b02535aab 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -55,3 +55,8 @@ export type AddCardExpireDateInputProps = Pick; export type AddCardSecurityCodeInputProps = Pick; export type AddCardPasswordInputProps = Pick; + +export type ErrorMessageProps = { + inputType: string; + status: InputStatus; +}; From e0f6fa460c5a5e6eb10984b489b7978202d1e267 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:01:00 +0900 Subject: [PATCH 026/102] =?UTF-8?q?refactor:=20Input=20Status=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EA=B3=B5=ED=86=B5=20=ED=8C=8C=EC=9D=BC=EC=97=90=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInputs.ts | 7 +++---- src/type.d.ts | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hooks/useInputs.ts b/src/hooks/useInputs.ts index b3e3420420..c7be8a186a 100644 --- a/src/hooks/useInputs.ts +++ b/src/hooks/useInputs.ts @@ -1,9 +1,8 @@ import { useState } from 'react'; +import { InputStatus } from '../type'; -type INPUT_STATUS = 'INIT' | 'VALID' | 'INVALID'; - -const useInputs = (formatDispatcher: (str: string) => INPUT_STATUS, init = '') => { - const [status, setStatus] = useState('INIT'); +const useInputs = (formatDispatcher: (str: string) => InputStatus, init = '') => { + const [status, setStatus] = useState('INIT'); const [value, setValue] = useState(''); const onChange = (e: React.ChangeEvent) => { diff --git a/src/type.d.ts b/src/type.d.ts index 1b02535aab..67e38c374d 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -34,6 +34,8 @@ export type InputHook = { onChange: (e: React.ChangeEvent) => void; }; +export type InputStatus = 'INIT' | 'VALID' | 'INVALID'; + export type PasswordInputHook = [T, (e: React.ChangeEvent) => void]; export type FormCardAddProps = { From ad10502baab8e0effe81f2689ade9260362f2d09 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:03:25 +0900 Subject: [PATCH 027/102] =?UTF-8?q?feat:=20Input=20Container=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/InputContainer.tsx | 13 ++++++++----- src/type.d.ts | 15 +++++++++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/InputContainer.tsx b/src/components/InputContainer.tsx index be0f6d12a9..2d434174aa 100644 --- a/src/components/InputContainer.tsx +++ b/src/components/InputContainer.tsx @@ -1,14 +1,17 @@ import { PropsWithChildren } from 'react'; import ErrorMessage from './ErrorMessage'; +import { InputContainerProps } from '../type'; -type InputContainerProps = { - className: string; -}; -const InputContainer = ({ children, className }: PropsWithChildren) => { +const InputContainer = ({ + children, + className, + status, + inputType, +}: PropsWithChildren) => { return (
    {children} - +
    ); }; diff --git a/src/type.d.ts b/src/type.d.ts index 67e38c374d..43a29bba58 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -29,9 +29,10 @@ export type CardType = { cardPassword: CardPassword; }; -export type InputHook = { - value: T; +export type InputHook = { + value: string; onChange: (e: React.ChangeEvent) => void; + status: InputStatus; }; export type InputStatus = 'INIT' | 'VALID' | 'INVALID'; @@ -45,8 +46,8 @@ export type FormCardAddProps = { cardPassword1: InputHook; cardPassword2: InputHook; onCardNumberChange: (e: React.ChangeEvent) => void; - expireMonth: InputHook; - expireYear: InputHook; + expireMonth: InputHook; + expireYear: InputHook; }; export type AddCardNumberInputProps = Pick & { @@ -62,3 +63,9 @@ export type ErrorMessageProps = { inputType: string; status: InputStatus; }; + +export type InputContainerProps = { + className: string; + status: InputStatus; + inputType: string; +}; From 562fecc8d2f0b6a1424a0e3d4ab69ded2218aad8 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:04:54 +0900 Subject: [PATCH 028/102] =?UTF-8?q?feat:=20expireDate=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Custom=20Hoo?= =?UTF-8?q?k=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 21 +++++++++---------- .../components/AddCardExpireDateInput.tsx | 11 ++++++---- src/pages/AddCard/domain/dispatcher.ts | 11 ++++++++++ src/pages/AddCard/domain/domain.ts | 8 +++++++ 4 files changed, 36 insertions(+), 15 deletions(-) create mode 100644 src/pages/AddCard/domain/dispatcher.ts diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 500757bfde..3139101cf4 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -7,16 +7,15 @@ import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; import useInput from '../../hooks/useInput'; import useComplicateInput from '../../hooks/useComplicateInput'; -import { handleNumberInput, identity, isNumberInput, stringToUpperCase } from '../../utils/util'; -import { - securityCodeCondition, - cardOwnerCondition, - cardPasswordCondition, - cardExpireMonthCondition, - cardExpireYearCondition, -} from '../cardInputCondition'; +import { handleNumberInput, isNumberInput, stringToUpperCase } from '../../utils/util'; +import { cardOwnerCondition, cardPasswordCondition } from '../cardInputCondition'; import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; +import useInputs from '../../hooks/useInputs'; +import { + isValidExpiredMonthFormat, + isValidExpiredYearFormat, +} from './domain/dispatcher'; const AddCardPage = () => { const navigate = useNavigate(); @@ -28,9 +27,9 @@ const AddCardPage = () => { third: '', fourth: '', }); - const expireMonth = useInput('', cardExpireMonthCondition, identity); - const expireYear = useInput('', cardExpireYearCondition, identity); - const securityCode = useInput('', securityCodeCondition, handleNumberInput); + + const expireMonth = useInputs(isValidExpiredMonthFormat); + const expireYear = useInputs(isValidExpiredYearFormat); const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); diff --git a/src/pages/AddCard/components/AddCardExpireDateInput.tsx b/src/pages/AddCard/components/AddCardExpireDateInput.tsx index 88862beefd..ccbed37dcb 100644 --- a/src/pages/AddCard/components/AddCardExpireDateInput.tsx +++ b/src/pages/AddCard/components/AddCardExpireDateInput.tsx @@ -1,9 +1,12 @@ +import InputContainer from '../../../components/InputContainer'; import { AddCardExpireDateInputProps } from '../../../type'; +import { calcMultipleStatus } from '../domain/domain'; import './AddCardExpireDateInput.css'; const AddCardExpireDateInput = ({ expireMonth, expireYear }: AddCardExpireDateInputProps) => { + const status = calcMultipleStatus([expireMonth.status, expireYear.status]); return ( -
    + 만료일
    / @@ -19,12 +22,12 @@ const AddCardExpireDateInput = ({ expireMonth, expireYear }: AddCardExpireDateIn className="card-expired" value={expireYear.value} onChange={expireYear.onChange} + maxLength={2} name="year" - type="number" required />
    -
    + ); }; diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts new file mode 100644 index 0000000000..d1804925ec --- /dev/null +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -0,0 +1,11 @@ +import { MONTH_DATA } from '../../../utils/constants'; + +export const isValidExpiredMonthFormat = (str: string) => { + return MONTH_DATA.includes(str) ? 'VALID' : 'INVALID'; +}; + +export const isValidExpiredYearFormat = (str: string) => { + const strToNum = +str; + return Number.isInteger(strToNum) && strToNum >= 0 ? 'VALID' : 'INVALID'; +}; + diff --git a/src/pages/AddCard/domain/domain.ts b/src/pages/AddCard/domain/domain.ts index f84e294d75..a1ae8a8dbb 100644 --- a/src/pages/AddCard/domain/domain.ts +++ b/src/pages/AddCard/domain/domain.ts @@ -1,3 +1,11 @@ +import { InputStatus } from '../../../type'; + +export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { + if (arr.includes('INVALID')) return 'INVALID'; + if (arr.filter((element) => element === 'VALID').length === arr.length) return 'VALID'; + return 'INIT'; +}; + // TODO: 구체화 하기 export const getErrorMessage = (inputType: string, status: InputStatus) => { return status === 'INVALID' ? '잘못된 입력입니다.' : ''; From 3a44154a1375738b2c9b57d41b4cf7d60f500069 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:05:32 +0900 Subject: [PATCH 029/102] =?UTF-8?q?feat:=20SecurityCodeInput=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Cus?= =?UTF-8?q?tom=20Hook=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 2 ++ .../AddCard/components/AddCardSecurityCodeInput.tsx | 9 +++++++-- src/pages/AddCard/domain/dispatcher.ts | 4 ++++ src/type.d.ts | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 3139101cf4..8da7f36b84 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -15,6 +15,7 @@ import useInputs from '../../hooks/useInputs'; import { isValidExpiredMonthFormat, isValidExpiredYearFormat, + isValidSecurityCode, } from './domain/dispatcher'; const AddCardPage = () => { @@ -30,6 +31,7 @@ const AddCardPage = () => { const expireMonth = useInputs(isValidExpiredMonthFormat); const expireYear = useInputs(isValidExpiredYearFormat); + const securityCode = useInputs(isValidSecurityCode); const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); diff --git a/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx b/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx index a832393c25..2079343c84 100644 --- a/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx +++ b/src/pages/AddCard/components/AddCardSecurityCodeInput.tsx @@ -2,10 +2,15 @@ import type { AddCardSecurityCodeInputProps } from '../../../type'; import cvcInfo from '../../../asset/cvc_info.png'; import './AddCardSecurityCodeInput.css'; +import InputContainer from '../../../components/InputContainer'; const AddCardSecurityCodeInput = ({ securityCode }: AddCardSecurityCodeInputProps) => { return ( -
    + 보안코드(CVC/CVV)
    카드 뒷면의 3자리 숫자입니다.

    - + ); }; diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index d1804925ec..248ac91756 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -9,3 +9,7 @@ export const isValidExpiredYearFormat = (str: string) => { return Number.isInteger(strToNum) && strToNum >= 0 ? 'VALID' : 'INVALID'; }; +export const isValidSecurityCode = (str: string) => { + const strToNum = +str; + return str.length === 3 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; +}; diff --git a/src/type.d.ts b/src/type.d.ts index 43a29bba58..3f6dd5bae3 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -42,7 +42,7 @@ export type PasswordInputHook = [T, (e: React.ChangeEvent) export type FormCardAddProps = { cardNumber: CardNumber; cardOwner: InputHook; - securityCode: InputHook; + securityCode: InputHook; cardPassword1: InputHook; cardPassword2: InputHook; onCardNumberChange: (e: React.ChangeEvent) => void; From 7e56d1a2a31aff97c053794698db303aacf8f933 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:15:41 +0900 Subject: [PATCH 030/102] =?UTF-8?q?feat:=20OwnerInput=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Custom=20Hoo?= =?UTF-8?q?k=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 7 ++++--- src/pages/AddCard/components/AddCardOwnerInput.tsx | 10 ++++++++-- src/pages/AddCard/domain/dispatcher.ts | 8 +++++++- src/type.d.ts | 2 +- src/utils/constants.ts | 2 ++ src/utils/util.ts | 3 ++- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 8da7f36b84..9a14e54cde 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -7,14 +7,15 @@ import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; import useInput from '../../hooks/useInput'; import useComplicateInput from '../../hooks/useComplicateInput'; -import { handleNumberInput, isNumberInput, stringToUpperCase } from '../../utils/util'; -import { cardOwnerCondition, cardPasswordCondition } from '../cardInputCondition'; +import { handleNumberInput, isNumberInput } from '../../utils/util'; +import { cardPasswordCondition } from '../cardInputCondition'; import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; import useInputs from '../../hooks/useInputs'; import { isValidExpiredMonthFormat, isValidExpiredYearFormat, + isValidOwnerName, isValidSecurityCode, } from './domain/dispatcher'; @@ -32,7 +33,7 @@ const AddCardPage = () => { const expireMonth = useInputs(isValidExpiredMonthFormat); const expireYear = useInputs(isValidExpiredYearFormat); const securityCode = useInputs(isValidSecurityCode); - const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); + const cardOwner = useInputs(isValidOwnerName); const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); diff --git a/src/pages/AddCard/components/AddCardOwnerInput.tsx b/src/pages/AddCard/components/AddCardOwnerInput.tsx index 359fb69deb..c2210bd438 100644 --- a/src/pages/AddCard/components/AddCardOwnerInput.tsx +++ b/src/pages/AddCard/components/AddCardOwnerInput.tsx @@ -1,9 +1,14 @@ +import InputContainer from '../../../components/InputContainer'; import type { AddCardOwnerInputProps } from '../../../type'; import './AddCardOwnerInput.css'; const AddCardOwnerInput = ({ cardOwner }: AddCardOwnerInputProps) => { return ( -
    +
    카드 소유자 이름(선택) {cardOwner.value.length}/30 @@ -13,8 +18,9 @@ const AddCardOwnerInput = ({ cardOwner }: AddCardOwnerInputProps) => { value={cardOwner.value} onChange={cardOwner.onChange} name="owner" + maxLength={30} /> -
    + ); }; diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 248ac91756..eaea48bb59 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -1,4 +1,4 @@ -import { MONTH_DATA } from '../../../utils/constants'; +import { ALPHABET, MONTH_DATA } from '../../../utils/constants'; export const isValidExpiredMonthFormat = (str: string) => { return MONTH_DATA.includes(str) ? 'VALID' : 'INVALID'; @@ -13,3 +13,9 @@ export const isValidSecurityCode = (str: string) => { const strToNum = +str; return str.length === 3 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; }; + +export const isValidOwnerName = (str: string) => { + const charList = str.split('').filter((char) => ALPHABET.includes(char)); + + return str.length <= 30 && charList.length === str.length ? 'VALID' : 'INVALID'; +}; diff --git a/src/type.d.ts b/src/type.d.ts index 3f6dd5bae3..d3b683a6be 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -41,7 +41,7 @@ export type PasswordInputHook = [T, (e: React.ChangeEvent) export type FormCardAddProps = { cardNumber: CardNumber; - cardOwner: InputHook; + cardOwner: InputHook; securityCode: InputHook; cardPassword1: InputHook; cardPassword2: InputHook; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f8ebf6dcfb..26e04b20ca 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1 +1,3 @@ export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; + +export const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); diff --git a/src/utils/util.ts b/src/utils/util.ts index 6c3583b1c6..37d82e1f33 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,5 +1,6 @@ +import { ALPHABET } from './constants'; + const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; -const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); export const formatExpireDate = (expireDate: string): string => { return expireDate; From c7efe748638d62ca7e929dc562b45eff7dfbf6b7 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:21:14 +0900 Subject: [PATCH 031/102] =?UTF-8?q?feat:=20Password=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Custom=20Hoo?= =?UTF-8?q?k=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 5 +++-- src/pages/AddCard/components/AddCardPasswordInput.tsx | 10 ++++++++-- src/pages/AddCard/domain/dispatcher.ts | 8 ++++++-- src/utils/constants.ts | 2 ++ src/utils/util.ts | 4 +--- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 9a14e54cde..2f2bae43a7 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -16,6 +16,7 @@ import { isValidExpiredMonthFormat, isValidExpiredYearFormat, isValidOwnerName, + isValidPassword, isValidSecurityCode, } from './domain/dispatcher'; @@ -29,13 +30,13 @@ const AddCardPage = () => { third: '', fourth: '', }); + const cardPassword1 = useInputs(isValidPassword); + const cardPassword2 = useInputs(isValidPassword); const expireMonth = useInputs(isValidExpiredMonthFormat); const expireYear = useInputs(isValidExpiredYearFormat); const securityCode = useInputs(isValidSecurityCode); const cardOwner = useInputs(isValidOwnerName); - const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); - const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); const onBackButtonClick = useCallback(() => { navigate('/'); diff --git a/src/pages/AddCard/components/AddCardPasswordInput.tsx b/src/pages/AddCard/components/AddCardPasswordInput.tsx index 3382e307d8..2b84794a4e 100644 --- a/src/pages/AddCard/components/AddCardPasswordInput.tsx +++ b/src/pages/AddCard/components/AddCardPasswordInput.tsx @@ -1,10 +1,16 @@ import type { AddCardPasswordInputProps } from '../../../type'; import passwordDotImg from '../../../asset/password_dot.png'; import './AddCardPasswordInput.css'; +import InputContainer from '../../../components/InputContainer'; +import { calcMultipleStatus } from '../domain/domain'; const AddCardPasswordInput = ({ cardPassword1, cardPassword2 }: AddCardPasswordInputProps) => { return ( -
    + 카드 비밀번호
    비밀번호
    -
    + ); }; diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index eaea48bb59..a6f67a2472 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -1,4 +1,4 @@ -import { ALPHABET, MONTH_DATA } from '../../../utils/constants'; +import { ALPHABET, MONTH_DATA, NUMBERS } from '../../../utils/constants'; export const isValidExpiredMonthFormat = (str: string) => { return MONTH_DATA.includes(str) ? 'VALID' : 'INVALID'; @@ -17,5 +17,9 @@ export const isValidSecurityCode = (str: string) => { export const isValidOwnerName = (str: string) => { const charList = str.split('').filter((char) => ALPHABET.includes(char)); - return str.length <= 30 && charList.length === str.length ? 'VALID' : 'INVALID'; + return str.length === 30 && charList.length === str.length ? 'VALID' : 'INVALID'; +}; + +export const isValidPassword = (str: string) => { + return str.length === 1 && NUMBERS.includes(str) ? 'VALID' : 'INVALID'; }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 26e04b20ca..02c5e4dd25 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,5 @@ export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; export const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + +export const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; diff --git a/src/utils/util.ts b/src/utils/util.ts index 37d82e1f33..fdfd143c67 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,6 +1,4 @@ -import { ALPHABET } from './constants'; - -const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +import { ALPHABET, NUMBERS } from './constants'; export const formatExpireDate = (expireDate: string): string => { return expireDate; From aebb7afd7ce1cd22404d81fba08a14dd1f866457 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:44:51 +0900 Subject: [PATCH 032/102] =?UTF-8?q?feat:=20CardNumber=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=88=EB=A1=9C=EC=9A=B4=20Custom=20Hoo?= =?UTF-8?q?k=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 19 +++++--- src/pages/AddCard/AddCardPage.tsx | 37 ++++++---------- src/pages/AddCard/components/AddCardForm.tsx | 20 +++++++-- .../AddCard/components/AddCardNumberInput.tsx | 43 +++++++++++++------ src/pages/AddCard/domain/dispatcher.ts | 5 +++ src/pages/CardListPage.tsx | 5 ++- src/type.d.ts | 26 +++++++---- 7 files changed, 99 insertions(+), 56 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 85b4ac3c7e..368c323a1b 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -2,7 +2,16 @@ import type { CardProps } from '../type'; import { changeNumberToMask } from '../utils/util'; import './Card.css'; -const Card = ({ cardType, cardNumber, cardOwner, expireYear, expireMonth }: CardProps) => { +const Card = ({ + cardType, + cardFirstNumber, + cardSecondNumber, + cardThirdNumber, + cardFourthNumber, + cardOwner, + expireYear, + expireMonth, +}: CardProps) => { return (
    @@ -12,10 +21,10 @@ const Card = ({ cardType, cardNumber, cardOwner, expireYear, expireMonth }: Card
    - {cardNumber.first} - {cardNumber.second} - {changeNumberToMask(cardNumber.third)} - {changeNumberToMask(cardNumber.fourth)} + {cardFirstNumber} + {cardSecondNumber} + {changeNumberToMask(cardThirdNumber)} + {changeNumberToMask(cardFourthNumber)}
    {cardOwner} diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 2f2bae43a7..00591ad2e8 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -13,6 +13,7 @@ import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; import useInputs from '../../hooks/useInputs'; import { + isValidCardNumber, isValidExpiredMonthFormat, isValidExpiredYearFormat, isValidOwnerName, @@ -24,15 +25,12 @@ const AddCardPage = () => { const navigate = useNavigate(); const [cardType] = useState('현대'); - const [cardNumber, onChangeCardNumber] = useComplicateInput({ - first: '', - second: '', - third: '', - fourth: '', - }); + const cardFirstNumber = useInputs(isValidCardNumber); + const cardSecondNumber = useInputs(isValidCardNumber); + const cardThirdNumber = useInputs(isValidCardNumber); + const cardFourthNumber = useInputs(isValidCardNumber); const cardPassword1 = useInputs(isValidPassword); const cardPassword2 = useInputs(isValidPassword); - const expireMonth = useInputs(isValidExpiredMonthFormat); const expireYear = useInputs(isValidExpiredYearFormat); const securityCode = useInputs(isValidSecurityCode); @@ -42,20 +40,6 @@ const AddCardPage = () => { navigate('/'); }, [navigate]); - // TODO: e.target.value.length 가독성 개선 => 변수화 - // TODO: 이벤트 핸들러 리팩터링.. 네이밍 + 구조 - const onCardNumberChange = useCallback( - (e: React.ChangeEvent) => { - const lastWord = e.target.value[e.target.value.length - 1]; - - if (e.target.value.length > 4) return; - if (e.target.value.length > 0 && !isNumberInput(lastWord)) return; - - onChangeCardNumber(e); - }, - [onChangeCardNumber] - ); - return (
    @@ -67,14 +51,19 @@ const AddCardPage = () => {
    - + diff --git a/src/pages/AddCard/components/AddCardNumberInput.tsx b/src/pages/AddCard/components/AddCardNumberInput.tsx index aae1e6841b..82c24a01a8 100644 --- a/src/pages/AddCard/components/AddCardNumberInput.tsx +++ b/src/pages/AddCard/components/AddCardNumberInput.tsx @@ -2,27 +2,44 @@ import React from 'react'; import type { AddCardNumberInputProps } from '../../../type'; import './AddCardNumberInput.css'; +import InputContainer from '../../../components/InputContainer'; +import { calcMultipleStatus } from '../domain/domain'; -const AddCardNumberInput = ({ cardNumber, onChange }: AddCardNumberInputProps) => { +const AddCardNumberInput = ({ + cardFirstNumber, + cardSecondNumber, + cardThirdNumber, + cardFourthNumber, +}: AddCardNumberInputProps) => { // TODO: memo를 적용시키려면 객체를 내리면 안된다. - const { first, second, third, fourth } = cardNumber; return ( -
    + 카드 번호
    - - @@ -30,9 +47,8 @@ const AddCardNumberInput = ({ cardNumber, onChange }: AddCardNumberInputProps) = className="input-element input-password-container card-number" type="password" maxLength={4} - minLength={4} - value={third} - onChange={onChange} + value={cardThirdNumber.value} + onChange={cardThirdNumber.onChange} name="third" required /> @@ -41,14 +57,13 @@ const AddCardNumberInput = ({ cardNumber, onChange }: AddCardNumberInputProps) = className="input-element input-password-container card-number" type="password" maxLength={4} - minLength={4} - value={fourth} - onChange={onChange} + value={cardFourthNumber.value} + onChange={cardFourthNumber.onChange} name="fourth" required />
    -
    + ); }; diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index a6f67a2472..253706ec21 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -23,3 +23,8 @@ export const isValidOwnerName = (str: string) => { export const isValidPassword = (str: string) => { return str.length === 1 && NUMBERS.includes(str) ? 'VALID' : 'INVALID'; }; + +export const isValidCardNumber = (str: string) => { + const strToNum = +str; + return str.length === 4 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; +}; diff --git a/src/pages/CardListPage.tsx b/src/pages/CardListPage.tsx index 51f1ec2235..875ebd4992 100644 --- a/src/pages/CardListPage.tsx +++ b/src/pages/CardListPage.tsx @@ -27,7 +27,10 @@ const CardListPage = () => { = [T, (e: React.ChangeEvent) => void]; export type FormCardAddProps = { - cardNumber: CardNumber; + cardFirstNumber: InputHook; + cardSecondNumber: InputHook; + cardThirdNumber: InputHook; + cardFourthNumber: InputHook; cardOwner: InputHook; securityCode: InputHook; - cardPassword1: InputHook; - cardPassword2: InputHook; - onCardNumberChange: (e: React.ChangeEvent) => void; + cardPassword1: InputHook; + cardPassword2: InputHook; expireMonth: InputHook; expireYear: InputHook; }; -export type AddCardNumberInputProps = Pick & { - onChange: (e: React.ChangeEvent) => void; -}; +export type AddCardNumberInputProps = Pick< + FormCardAddProps, + 'cardFirstNumber', + 'cardSecondNumber', + 'cardThirdNumber', + 'cardFourthNumber' +>; export type AddCardExpireDateInputProps = Pick; export type AddCardOwnerInputProps = Pick; From bb1e645583ffed08e3dff3d32b14aec220497748 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 20:58:25 +0900 Subject: [PATCH 033/102] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=ED=8E=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EA=B0=84=EB=8B=A8=ED=9E=88=20=EC=B6=95=EC=95=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 6 ----- src/pages/AddCard/components/AddCardForm.tsx | 24 +++++++++---------- ...ardNumberInput.css => CardNumberInput.css} | 0 ...ardNumberInput.tsx => CardNumberInput.tsx} | 10 ++++---- ...xpireDateInput.css => ExpireDateInput.css} | 0 ...xpireDateInput.tsx => ExpireDateInput.tsx} | 8 +++---- .../{AddCardOwnerInput.css => OwnerInput.css} | 0 .../{AddCardOwnerInput.tsx => OwnerInput.tsx} | 8 +++---- ...ardPasswordInput.css => PasswordInput.css} | 0 ...ardPasswordInput.tsx => PasswordInput.tsx} | 8 +++---- ...ityCodeInput.css => SecurityCodeInput.css} | 0 ...ityCodeInput.tsx => SecurityCodeInput.tsx} | 8 +++---- src/type.d.ts | 14 +++++------ 13 files changed, 40 insertions(+), 46 deletions(-) rename src/pages/AddCard/components/{AddCardNumberInput.css => CardNumberInput.css} (100%) rename src/pages/AddCard/components/{AddCardNumberInput.tsx => CardNumberInput.tsx} (90%) rename src/pages/AddCard/components/{AddCardExpireDateInput.css => ExpireDateInput.css} (100%) rename src/pages/AddCard/components/{AddCardExpireDateInput.tsx => ExpireDateInput.tsx} (78%) rename src/pages/AddCard/components/{AddCardOwnerInput.css => OwnerInput.css} (100%) rename src/pages/AddCard/components/{AddCardOwnerInput.tsx => OwnerInput.tsx} (76%) rename src/pages/AddCard/components/{AddCardPasswordInput.css => PasswordInput.css} (100%) rename src/pages/AddCard/components/{AddCardPasswordInput.tsx => PasswordInput.tsx} (83%) rename src/pages/AddCard/components/{AddCardSecurityCodeInput.css => SecurityCodeInput.css} (100%) rename src/pages/AddCard/components/{AddCardSecurityCodeInput.tsx => SecurityCodeInput.tsx} (79%) diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 00591ad2e8..d394ab8559 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -1,14 +1,9 @@ import React, { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { CardNumber } from '../../type'; import Card from '../../components/Card'; import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; -import useInput from '../../hooks/useInput'; -import useComplicateInput from '../../hooks/useComplicateInput'; -import { handleNumberInput, isNumberInput } from '../../utils/util'; -import { cardPasswordCondition } from '../cardInputCondition'; import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; import useInputs from '../../hooks/useInputs'; @@ -24,7 +19,6 @@ import { const AddCardPage = () => { const navigate = useNavigate(); const [cardType] = useState('현대'); - const cardFirstNumber = useInputs(isValidCardNumber); const cardSecondNumber = useInputs(isValidCardNumber); const cardThirdNumber = useInputs(isValidCardNumber); diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 581e041f94..1faba1911b 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import type { FormCardAddProps } from '../../../type'; +import type { AddCardFormProps } from '../../../type'; import { sumbitCard } from '../../../utils/applicationUtil'; import { useNavigate } from 'react-router-dom'; -import AddCardExpireDateInput from './AddCardExpireDateInput'; -import AddCardOwnerInput from './AddCardOwnerInput'; -import AddCardSecurityCodeInput from './AddCardSecurityCodeInput'; -import AddCardPasswordInput from './AddCardPasswordInput'; +import ExpireDateInput from './ExpireDateInput'; +import OwnerInput from './OwnerInput'; +import SecurityCodeInput from './SecurityCodeInput'; +import PasswordInput from './PasswordInput'; import './AddCardForm.css'; -import AddCardNumberInput from './AddCardNumberInput'; +import CardNumberInput from './CardNumberInput'; const AddCardForm = ({ cardFirstNumber, @@ -21,7 +21,7 @@ const AddCardForm = ({ securityCode, cardPassword1, cardPassword2, -}: FormCardAddProps) => { +}: AddCardFormProps) => { const navigate = useNavigate(); const onSubmit = (e: React.FormEvent) => { @@ -53,16 +53,16 @@ const AddCardForm = ({ return ( - - - - - + + + +
    diff --git a/src/pages/AddCard/components/AddCardNumberInput.css b/src/pages/AddCard/components/CardNumberInput.css similarity index 100% rename from src/pages/AddCard/components/AddCardNumberInput.css rename to src/pages/AddCard/components/CardNumberInput.css diff --git a/src/pages/AddCard/components/AddCardNumberInput.tsx b/src/pages/AddCard/components/CardNumberInput.tsx similarity index 90% rename from src/pages/AddCard/components/AddCardNumberInput.tsx rename to src/pages/AddCard/components/CardNumberInput.tsx index 82c24a01a8..215ca8b9dc 100644 --- a/src/pages/AddCard/components/AddCardNumberInput.tsx +++ b/src/pages/AddCard/components/CardNumberInput.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import type { AddCardNumberInputProps } from '../../../type'; -import './AddCardNumberInput.css'; +import type { CardNumberInputProps } from '../../../type'; +import './CardNumberInput.css'; import InputContainer from '../../../components/InputContainer'; import { calcMultipleStatus } from '../domain/domain'; -const AddCardNumberInput = ({ +const CardNumberInput = ({ cardFirstNumber, cardSecondNumber, cardThirdNumber, cardFourthNumber, -}: AddCardNumberInputProps) => { +}: CardNumberInputProps) => { // TODO: memo를 적용시키려면 객체를 내리면 안된다. return ( { +const ExpireDateInput = ({ expireMonth, expireYear }: ExpireDateInputProps) => { const status = calcMultipleStatus([expireMonth.status, expireYear.status]); return ( @@ -31,4 +31,4 @@ const AddCardExpireDateInput = ({ expireMonth, expireYear }: AddCardExpireDateIn ); }; -export default AddCardExpireDateInput; +export default ExpireDateInput; diff --git a/src/pages/AddCard/components/AddCardOwnerInput.css b/src/pages/AddCard/components/OwnerInput.css similarity index 100% rename from src/pages/AddCard/components/AddCardOwnerInput.css rename to src/pages/AddCard/components/OwnerInput.css diff --git a/src/pages/AddCard/components/AddCardOwnerInput.tsx b/src/pages/AddCard/components/OwnerInput.tsx similarity index 76% rename from src/pages/AddCard/components/AddCardOwnerInput.tsx rename to src/pages/AddCard/components/OwnerInput.tsx index c2210bd438..62488f05d6 100644 --- a/src/pages/AddCard/components/AddCardOwnerInput.tsx +++ b/src/pages/AddCard/components/OwnerInput.tsx @@ -1,8 +1,8 @@ import InputContainer from '../../../components/InputContainer'; -import type { AddCardOwnerInputProps } from '../../../type'; -import './AddCardOwnerInput.css'; +import type { OwnerInputProps } from '../../../type'; +import './OwnerInput.css'; -const AddCardOwnerInput = ({ cardOwner }: AddCardOwnerInputProps) => { +const OwnerInput = ({ cardOwner }: OwnerInputProps) => { return ( { ); }; -export default AddCardOwnerInput; +export default OwnerInput; diff --git a/src/pages/AddCard/components/AddCardPasswordInput.css b/src/pages/AddCard/components/PasswordInput.css similarity index 100% rename from src/pages/AddCard/components/AddCardPasswordInput.css rename to src/pages/AddCard/components/PasswordInput.css diff --git a/src/pages/AddCard/components/AddCardPasswordInput.tsx b/src/pages/AddCard/components/PasswordInput.tsx similarity index 83% rename from src/pages/AddCard/components/AddCardPasswordInput.tsx rename to src/pages/AddCard/components/PasswordInput.tsx index 2b84794a4e..bfec78f005 100644 --- a/src/pages/AddCard/components/AddCardPasswordInput.tsx +++ b/src/pages/AddCard/components/PasswordInput.tsx @@ -1,10 +1,10 @@ -import type { AddCardPasswordInputProps } from '../../../type'; +import type { PasswordInputProps } from '../../../type'; import passwordDotImg from '../../../asset/password_dot.png'; -import './AddCardPasswordInput.css'; +import './PasswordInput.css'; import InputContainer from '../../../components/InputContainer'; import { calcMultipleStatus } from '../domain/domain'; -const AddCardPasswordInput = ({ cardPassword1, cardPassword2 }: AddCardPasswordInputProps) => { +const PasswordInput = ({ cardPassword1, cardPassword2 }: PasswordInputProps) => { return ( { +const SecurityCodeInput = ({ securityCode }: SecurityCodeInputProps) => { return ( = [T, (e: React.ChangeEvent) => void]; -export type FormCardAddProps = { +export type AddCardFormProps = { cardFirstNumber: InputHook; cardSecondNumber: InputHook; cardThirdNumber: InputHook; @@ -56,18 +56,18 @@ export type FormCardAddProps = { expireYear: InputHook; }; -export type AddCardNumberInputProps = Pick< - FormCardAddProps, +export type CardNumberInputProps = Pick< + AddCardFormProps, 'cardFirstNumber', 'cardSecondNumber', 'cardThirdNumber', 'cardFourthNumber' >; -export type AddCardExpireDateInputProps = Pick; -export type AddCardOwnerInputProps = Pick; -export type AddCardSecurityCodeInputProps = Pick; -export type AddCardPasswordInputProps = Pick; +export type ExpireDateInputProps = Pick; +export type OwnerInputProps = Pick; +export type SecurityCodeInputProps = Pick; +export type PasswordInputProps = Pick; export type ErrorMessageProps = { inputType: string; From e3d432e2c2a27926c032fed6464e96d8a40082e1 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 23:21:34 +0900 Subject: [PATCH 034/102] =?UTF-8?q?refactor:=20deprecated=20=EB=90=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useComplicateInput.ts | 19 ----------------- src/hooks/useInput.ts | 22 ++++++++----------- src/hooks/useInputs.ts | 17 --------------- src/pages/AddCard/AddCardPage.tsx | 24 ++++++++++----------- src/pages/cardInputCondition.ts | 35 ------------------------------- 5 files changed, 21 insertions(+), 96 deletions(-) delete mode 100644 src/hooks/useComplicateInput.ts delete mode 100644 src/hooks/useInputs.ts delete mode 100644 src/pages/cardInputCondition.ts diff --git a/src/hooks/useComplicateInput.ts b/src/hooks/useComplicateInput.ts deleted file mode 100644 index 8052b7ac7d..0000000000 --- a/src/hooks/useComplicateInput.ts +++ /dev/null @@ -1,19 +0,0 @@ -import React, { useCallback, useState } from 'react'; - -const useComplicateInput = (initialState: T) => { - const [value, setValue] = useState(initialState); - - const onChange = useCallback( - (e: React.ChangeEvent) => { - setValue({ - ...value, - [e.target.name]: e.target.value, - }); - }, - [value] - ); - - return [value, onChange] as const; -}; - -export default useComplicateInput; diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index e8865c5fb7..ccc9fe0dbd 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -1,21 +1,17 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; +import { InputStatus } from '../type'; -const useInput = ( - initialState: string, - prevConditionCallback: (e: React.ChangeEvent) => boolean, - formatCallback: (data: string) => string -) => { - const [value, setValue] = useState(initialState); +const useInput = (formatDispatcher: (str: string) => InputStatus, init = '') => { + const [status, setStatus] = useState('INIT'); + const [value, setValue] = useState(''); const onChange = (e: React.ChangeEvent) => { - if (!prevConditionCallback(e)) return; - setValue(formatCallback(e.target.value)); + const inputValue = e.target.value; + setStatus(formatDispatcher(inputValue)); + setValue(inputValue); }; - return { - value, - onChange, - }; + return { value, status, onChange }; }; export default useInput; diff --git a/src/hooks/useInputs.ts b/src/hooks/useInputs.ts deleted file mode 100644 index c7be8a186a..0000000000 --- a/src/hooks/useInputs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useState } from 'react'; -import { InputStatus } from '../type'; - -const useInputs = (formatDispatcher: (str: string) => InputStatus, init = '') => { - const [status, setStatus] = useState('INIT'); - const [value, setValue] = useState(''); - - const onChange = (e: React.ChangeEvent) => { - const inputValue = e.target.value; - setStatus(formatDispatcher(inputValue)); - setValue(inputValue); - }; - - return { value, status, onChange }; -}; - -export default useInputs; diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index d394ab8559..a761214115 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; @@ -6,7 +6,7 @@ import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; import BackButtonImg from '../../asset/back_button.png'; import './AddCardPage.css'; -import useInputs from '../../hooks/useInputs'; +import useInput from '../../hooks/useInput'; import { isValidCardNumber, isValidExpiredMonthFormat, @@ -19,16 +19,16 @@ import { const AddCardPage = () => { const navigate = useNavigate(); const [cardType] = useState('현대'); - const cardFirstNumber = useInputs(isValidCardNumber); - const cardSecondNumber = useInputs(isValidCardNumber); - const cardThirdNumber = useInputs(isValidCardNumber); - const cardFourthNumber = useInputs(isValidCardNumber); - const cardPassword1 = useInputs(isValidPassword); - const cardPassword2 = useInputs(isValidPassword); - const expireMonth = useInputs(isValidExpiredMonthFormat); - const expireYear = useInputs(isValidExpiredYearFormat); - const securityCode = useInputs(isValidSecurityCode); - const cardOwner = useInputs(isValidOwnerName); + const cardFirstNumber = useInput(isValidCardNumber); + const cardSecondNumber = useInput(isValidCardNumber); + const cardThirdNumber = useInput(isValidCardNumber); + const cardFourthNumber = useInput(isValidCardNumber); + const cardPassword1 = useInput(isValidPassword); + const cardPassword2 = useInput(isValidPassword); + const expireMonth = useInput(isValidExpiredMonthFormat); + const expireYear = useInput(isValidExpiredYearFormat); + const securityCode = useInput(isValidSecurityCode); + const cardOwner = useInput(isValidOwnerName); const onBackButtonClick = useCallback(() => { navigate('/'); diff --git a/src/pages/cardInputCondition.ts b/src/pages/cardInputCondition.ts deleted file mode 100644 index f135e51cc9..0000000000 --- a/src/pages/cardInputCondition.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isAlphabetInput, isNumberInput } from '../utils/util'; - -// TODO: 훅 분리 고민 -export const cardExpireCondition = (e: React.ChangeEvent) => { - const length = e.target.value.length; - const lastWord = e.target.value[length - 1]; - return length <= 5 && length > 0 && (isNumberInput(lastWord) || lastWord === '/'); -}; - -export const securityCodeCondition = (e: React.ChangeEvent) => { - return e.target.value.length <= 3; -}; - -export const cardOwnerCondition = (e: React.ChangeEvent) => { - const length = e.target.value.length; - const lastWord = e.target.value[length - 1]; - return length <= 30 && !(length > 0 && !isAlphabetInput(lastWord.toUpperCase())); -}; - -export const cardPasswordCondition = (e: React.ChangeEvent) => { - return e.target.value.length <= 1; -}; - -export const cardExpireMonthCondition = (e: React.ChangeEvent) => { - const inputValue = e.target.value; - const month = +inputValue; - return month >= 0 && month <= 12 && inputValue.length <= 2; -}; - -const now = new Date(); - -export const cardExpireYearCondition = (e: React.ChangeEvent) => { - const inputValue = e.target.value; - return inputValue.length <= 2; -}; From f200e881c6defd20e9b3212f85a372ee284490fc Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 23:21:47 +0900 Subject: [PATCH 035/102] =?UTF-8?q?refactor:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/preview.ts | 3 +- .../AddCardExpireDateInput.stories.tsx | 33 -------------- src/stories/AddCardNumberInput.stories.tsx | 43 ++++++++----------- src/stories/AddCardOwnerInput.stories.tsx | 15 +++---- src/stories/AddCardPasswordInput.stories.tsx | 17 ++++---- .../AddCardSecurityCodeInput.stories.tsx | 15 +++---- src/stories/Card.stories.ts | 11 +++-- src/stories/ExpireDateInput.stories.tsx | 36 ++++++++++++++++ 8 files changed, 84 insertions(+), 89 deletions(-) delete mode 100644 src/stories/AddCardExpireDateInput.stories.tsx create mode 100644 src/stories/ExpireDateInput.stories.tsx diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 4502b78d9d..265b801313 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,6 @@ import type { Preview } from '@storybook/react'; -import '../src/components/AddCardForm.css'; + +import '../src/pages/AddCard/components/AddCardForm.css'; const preview: Preview = { parameters: { diff --git a/src/stories/AddCardExpireDateInput.stories.tsx b/src/stories/AddCardExpireDateInput.stories.tsx deleted file mode 100644 index d104e5b889..0000000000 --- a/src/stories/AddCardExpireDateInput.stories.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Meta } from '@storybook/react'; - -import AddCardExpireDateInput from '../pages/AddCard/components/AddCardExpireDateInput'; -import useInput from '../hooks/useInput'; -import { cardExpireCondition } from '../pages/cardInputCondition'; -import { formatExpireDate } from '../utils/util'; -import { APP_WIDTH } from './constants'; - -export default { - title: 'AddCardExpireDateInput', - component: AddCardExpireDateInput, - decorators: [ - (Story) => ( -
    - -
    - ), - ], -} as Meta; - -const AddInputHooks = () => { - const cardExpireDate = useInput('', cardExpireCondition, formatExpireDate); - - return ; -}; - -export const Primary = { - render: () => , -}; diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index 310f7b89d2..a79edb52cd 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -1,14 +1,13 @@ import type { Meta } from '@storybook/react'; -import type { CardNumber } from '../type'; -import AddCardNumberInput from '../pages/AddCard/components/AddCardNumberInput'; -import useComplicateInput from '../hooks/useComplicateInput'; -import { isNumberInput } from '../utils/util'; +import CardNumberInput from '../pages/AddCard/components/CardNumberInput'; import { APP_WIDTH } from './constants'; +import { isValidCardNumber } from '../pages/AddCard/domain/dispatcher'; +import useInput from '../hooks/useInput'; export default { - title: 'AddCardNumberInput', - component: AddCardNumberInput, + title: 'CardNumberInput', + component: CardNumberInput, decorators: [ (Story) => (
    ), ], -} as Meta; +} as Meta; const AddHooks = () => { - const [cardNumber, onChangeCardNumber] = useComplicateInput({ - first: '', - second: '', - third: '', - fourth: '', - }); - - const onChange = (e: React.ChangeEvent) => { - const lastWord = e.target.value[e.target.value.length - 1]; - - if (e.target.value.length > 4) return; - if (e.target.value.length > 0 && !isNumberInput(lastWord)) return; - - onChangeCardNumber(e); - }; - - return ; + const cardFirstNumber = useInput(isValidCardNumber); + const cardSecondNumber = useInput(isValidCardNumber); + const cardThirdNumber = useInput(isValidCardNumber); + const cardFourthNumber = useInput(isValidCardNumber); + + return ( + + ); }; export const Primary = { diff --git a/src/stories/AddCardOwnerInput.stories.tsx b/src/stories/AddCardOwnerInput.stories.tsx index 685685b265..b7a97a6fea 100644 --- a/src/stories/AddCardOwnerInput.stories.tsx +++ b/src/stories/AddCardOwnerInput.stories.tsx @@ -1,14 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardOwnerInput from '../pages/AddCard/components/AddCardOwnerInput'; -import { stringToUpperCase } from '../utils/util'; -import { cardOwnerCondition } from '../pages/cardInputCondition'; +import OwnerInput from '../pages/AddCard/components/OwnerInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from './constants'; +import { isValidOwnerName } from '../pages/AddCard/domain/dispatcher'; export default { title: 'AddCardOwnerInput', - component: AddCardOwnerInput, + component: OwnerInput, decorators: [ (Story) => (
    ), ], -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; const AddHooks = () => { - const cardOwner = useInput('', cardOwnerCondition, stringToUpperCase); + const cardOwner = useInput(isValidOwnerName); - return ; + return ; }; export const Primary: Story = { diff --git a/src/stories/AddCardPasswordInput.stories.tsx b/src/stories/AddCardPasswordInput.stories.tsx index 1b6010c287..fd1ba0ee09 100644 --- a/src/stories/AddCardPasswordInput.stories.tsx +++ b/src/stories/AddCardPasswordInput.stories.tsx @@ -1,14 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardPasswordInput from '../pages/AddCard/components/AddCardPasswordInput'; -import { handleNumberInput } from '../utils/util'; -import { cardPasswordCondition } from '../pages/cardInputCondition'; +import PasswordInput from '../pages/AddCard/components/PasswordInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from './constants'; +import { isValidPassword } from '../pages/AddCard/domain/dispatcher'; export default { title: 'AddCardPasswordInput', - component: AddCardPasswordInput, + component: PasswordInput, decorators: [ (Story) => (
    ), ], -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; const AddHooks = () => { - const cardPassword1 = useInput('', cardPasswordCondition, handleNumberInput); - const cardPassword2 = useInput('', cardPasswordCondition, handleNumberInput); + const cardPassword1 = useInput(isValidPassword); + const cardPassword2 = useInput(isValidPassword); - return ; + return ; }; export const Primary: Story = { diff --git a/src/stories/AddCardSecurityCodeInput.stories.tsx b/src/stories/AddCardSecurityCodeInput.stories.tsx index d4e3c87fbd..0381f44448 100644 --- a/src/stories/AddCardSecurityCodeInput.stories.tsx +++ b/src/stories/AddCardSecurityCodeInput.stories.tsx @@ -1,14 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react'; -import AddCardSecurityCodeInput from '../pages/AddCard/components/AddCardSecurityCodeInput'; -import { handleNumberInput } from '../utils/util'; -import { securityCodeCondition } from '../pages/cardInputCondition'; +import SecurityCodeInput from '../pages/AddCard/components/SecurityCodeInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from './constants'; +import { isValidSecurityCode } from '../pages/AddCard/domain/dispatcher'; export default { title: 'AddCardSecurityCodeInput', - component: AddCardSecurityCodeInput, + component: SecurityCodeInput, decorators: [ (Story) => (
    ), ], -} as Meta; +} as Meta; -type Story = StoryObj; +type Story = StoryObj; const AddHooks = () => { - const securityCode = useInput('', securityCodeCondition, handleNumberInput); + const securityCode = useInput(isValidSecurityCode); - return ; + return ; }; export const Primary: Story = { diff --git a/src/stories/Card.stories.ts b/src/stories/Card.stories.ts index a1273f5d6f..4f50513682 100644 --- a/src/stories/Card.stories.ts +++ b/src/stories/Card.stories.ts @@ -11,12 +11,11 @@ type Story = StoryObj; export const Primary: Story = { args: { - cardNumber: { - first: '1234', - second: '1234', - third: '1234', - fourth: '1234', - }, + cardType: '현대', + cardFirstNumber: '1234', + cardSecondNumber: '2345', + cardThirdNumber: '4456', + cardFourthNumber: '2357', cardOwner: 'LEE', expireYear: '01', expireMonth: '01', diff --git a/src/stories/ExpireDateInput.stories.tsx b/src/stories/ExpireDateInput.stories.tsx new file mode 100644 index 0000000000..3288b5789e --- /dev/null +++ b/src/stories/ExpireDateInput.stories.tsx @@ -0,0 +1,36 @@ +import type { Meta } from '@storybook/react'; + +import ExpireDateInput from '../pages/AddCard/components/ExpireDateInput'; +import { APP_WIDTH } from './constants'; +import useInput from '../hooks/useInput'; +import { + isValidExpiredMonthFormat, + isValidExpiredYearFormat, +} from '../pages/AddCard/domain/dispatcher'; + +export default { + title: 'AddCardExpireDateInput', + component: ExpireDateInput, + decorators: [ + (Story) => ( +
    + +
    + ), + ], +} as Meta; + +const AddHook = () => { + const expireMonth = useInput(isValidExpiredMonthFormat); + const expireYear = useInput(isValidExpiredYearFormat); + + const props = { expireMonth, expireYear }; + return ; +}; +export const Primary = { + render: () => , +}; From af58f3811126c7e99c25df6025ad3181379412cf Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 23:42:20 +0900 Subject: [PATCH 036/102] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/{ => CardList}/CardListPage.css | 0 src/pages/{ => CardList}/CardListPage.tsx | 8 ++++---- src/router/AppRouter.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/pages/{ => CardList}/CardListPage.css (100%) rename src/pages/{ => CardList}/CardListPage.tsx (86%) diff --git a/src/pages/CardListPage.css b/src/pages/CardList/CardListPage.css similarity index 100% rename from src/pages/CardListPage.css rename to src/pages/CardList/CardListPage.css diff --git a/src/pages/CardListPage.tsx b/src/pages/CardList/CardListPage.tsx similarity index 86% rename from src/pages/CardListPage.tsx rename to src/pages/CardList/CardListPage.tsx index 875ebd4992..f33dadc573 100644 --- a/src/pages/CardListPage.tsx +++ b/src/pages/CardList/CardListPage.tsx @@ -1,10 +1,10 @@ import { useNavigate } from 'react-router-dom'; -import type { CardType } from '../type'; -import Card from '../components/Card'; -import Header from '../components/Header'; +import type { CardType } from '../../type'; +import Card from '../../components/Card'; +import Header from '../../components/Header'; import './CardListPage.css'; -import { fetchLocalStorage } from '../utils/applicationUtil'; +import { fetchLocalStorage } from '../../utils/applicationUtil'; const CardListPage = () => { const cardList = fetchLocalStorage('cardList', '[]'); diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 6e76efe2dd..2597fe7b59 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -1,7 +1,7 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import AddCardPage from '../pages/AddCard/AddCardPage'; -import CardListPage from '../pages/CardListPage'; +import CardListPage from '../pages/CardList/CardListPage'; const router = createBrowserRouter([ { From c2e58d0ff245dbd4085eefe027aacca817ba9e9c Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 23:44:58 +0900 Subject: [PATCH 037/102] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/AddCardForm.css | 4 ++++ src/pages/AddCard/components/CardNumberInput.css | 2 +- src/pages/AddCard/components/ExpireDateInput.css | 7 +------ src/pages/AddCard/components/OwnerInput.css | 4 ---- src/pages/AddCard/components/PasswordInput.css | 3 --- src/pages/AddCard/components/SecurityCodeInput.css | 4 ---- 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/pages/AddCard/components/AddCardForm.css b/src/pages/AddCard/components/AddCardForm.css index df14163421..01a94c35d1 100644 --- a/src/pages/AddCard/components/AddCardForm.css +++ b/src/pages/AddCard/components/AddCardForm.css @@ -4,6 +4,10 @@ height: 100%; } +.add-card-form section { + width: 318px; + height: 100px; +} .form-label { color: #525252; font-size: 12px; diff --git a/src/pages/AddCard/components/CardNumberInput.css b/src/pages/AddCard/components/CardNumberInput.css index 184832c134..617182612f 100644 --- a/src/pages/AddCard/components/CardNumberInput.css +++ b/src/pages/AddCard/components/CardNumberInput.css @@ -1,6 +1,6 @@ .card-number-input-container { width: 318px; - margin-top: 20px; + margin-top: 50px; } /* 글자를 알맞게 정렬하는 방법이 뭐가 있을까? */ diff --git a/src/pages/AddCard/components/ExpireDateInput.css b/src/pages/AddCard/components/ExpireDateInput.css index 08dd7af2e2..f1149258e4 100644 --- a/src/pages/AddCard/components/ExpireDateInput.css +++ b/src/pages/AddCard/components/ExpireDateInput.css @@ -1,15 +1,10 @@ -.card-expired-input-container { - margin-top: 20px; - width: 137px; -} - .card-expired-input { display: flex; align-items: center; justify-content: center; background-color: #ecebf1; - width: 100%; height: 47px; + width: 137px; border-radius: 7px; } diff --git a/src/pages/AddCard/components/OwnerInput.css b/src/pages/AddCard/components/OwnerInput.css index e4e00b14ed..48948e74ba 100644 --- a/src/pages/AddCard/components/OwnerInput.css +++ b/src/pages/AddCard/components/OwnerInput.css @@ -1,7 +1,3 @@ -.card-owner-input-container { - margin-top: 20px; -} - .card-owner-input-container-header { display: flex; justify-content: space-between; diff --git a/src/pages/AddCard/components/PasswordInput.css b/src/pages/AddCard/components/PasswordInput.css index 4993715c11..28d40f688e 100644 --- a/src/pages/AddCard/components/PasswordInput.css +++ b/src/pages/AddCard/components/PasswordInput.css @@ -1,6 +1,3 @@ -.card-password-container { - margin-top: 20px; -} .card-password-input-box { display: flex; } diff --git a/src/pages/AddCard/components/SecurityCodeInput.css b/src/pages/AddCard/components/SecurityCodeInput.css index 545de6620b..5b99a3a3fe 100644 --- a/src/pages/AddCard/components/SecurityCodeInput.css +++ b/src/pages/AddCard/components/SecurityCodeInput.css @@ -1,7 +1,3 @@ -.card-security-code-container { - margin-top: 20px; -} - .card-security-code-box { position: relative; display: flex; From 776e0e196fac5cdf4f7b16c5ac34becd8fc350e2 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 26 Apr 2023 23:56:47 +0900 Subject: [PATCH 038/102] =?UTF-8?q?feat:=20=EC=83=81=ED=83=9C=EC=97=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=20=EC=9E=85=EB=A0=A5=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EC=98=A4=EB=A5=98=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCard/components/CardNumberInput.tsx | 2 +- .../AddCard/components/SecurityCodeInput.tsx | 2 +- src/pages/AddCard/domain/dispatcher.ts | 2 +- src/pages/AddCard/domain/domain.ts | 4 ++-- src/type.d.ts | 8 ++++---- src/utils/constants.ts | 18 ++++++++++++++++++ 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/pages/AddCard/components/CardNumberInput.tsx b/src/pages/AddCard/components/CardNumberInput.tsx index 215ca8b9dc..48bc29fd56 100644 --- a/src/pages/AddCard/components/CardNumberInput.tsx +++ b/src/pages/AddCard/components/CardNumberInput.tsx @@ -21,7 +21,7 @@ const CardNumberInput = ({ cardThirdNumber.status, cardFourthNumber.status, ])} - inputType="card-number" + inputType="cardNumber" > 카드 번호
    diff --git a/src/pages/AddCard/components/SecurityCodeInput.tsx b/src/pages/AddCard/components/SecurityCodeInput.tsx index f38c142638..dfdab0af33 100644 --- a/src/pages/AddCard/components/SecurityCodeInput.tsx +++ b/src/pages/AddCard/components/SecurityCodeInput.tsx @@ -9,7 +9,7 @@ const SecurityCodeInput = ({ securityCode }: SecurityCodeInputProps) => { 보안코드(CVC/CVV)
    diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 253706ec21..1c4dcc02cc 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -17,7 +17,7 @@ export const isValidSecurityCode = (str: string) => { export const isValidOwnerName = (str: string) => { const charList = str.split('').filter((char) => ALPHABET.includes(char)); - return str.length === 30 && charList.length === str.length ? 'VALID' : 'INVALID'; + return str.length > 0 && str.length <= 30 && charList.length === str.length ? 'VALID' : 'INVALID'; }; export const isValidPassword = (str: string) => { diff --git a/src/pages/AddCard/domain/domain.ts b/src/pages/AddCard/domain/domain.ts index a1ae8a8dbb..7a8bf07503 100644 --- a/src/pages/AddCard/domain/domain.ts +++ b/src/pages/AddCard/domain/domain.ts @@ -1,4 +1,5 @@ import { InputStatus } from '../../../type'; +import { INVALID_MESSAGE } from '../../../utils/constants'; export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { if (arr.includes('INVALID')) return 'INVALID'; @@ -6,7 +7,6 @@ export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { return 'INIT'; }; -// TODO: 구체화 하기 export const getErrorMessage = (inputType: string, status: InputStatus) => { - return status === 'INVALID' ? '잘못된 입력입니다.' : ''; + return INVALID_MESSAGE[inputType][status]; }; diff --git a/src/type.d.ts b/src/type.d.ts index 251d67b371..03ddeb6e98 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -41,8 +41,6 @@ export type InputHook = { export type InputStatus = 'INIT' | 'VALID' | 'INVALID'; -export type PasswordInputHook = [T, (e: React.ChangeEvent) => void]; - export type AddCardFormProps = { cardFirstNumber: InputHook; cardSecondNumber: InputHook; @@ -69,13 +67,15 @@ export type OwnerInputProps = Pick; export type SecurityCodeInputProps = Pick; export type PasswordInputProps = Pick; +export type InputType = 'securityCode' | 'password' | 'owner' | 'expired' | 'cardNumber'; + export type ErrorMessageProps = { - inputType: string; + inputType: InputType; status: InputStatus; }; export type InputContainerProps = { className: string; status: InputStatus; - inputType: string; + inputType: InputType; }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 02c5e4dd25..c11f6ac0c1 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,3 +3,21 @@ export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', export const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); export const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + +export const INVALID_MESSAGE: { [key: string]: { [key: string]: string } } = { + securityCode: { + INVALID: '보안카드 번호는 숫자 0~9를 사용해 3자리로 이루어져야 합니다.', + }, + password: { + INVALID: '비밀번호는 숫자 0~9를 사용해 각 1자리로 이루어져야 합니다.', + }, + owner: { + INVALID: '카드 소유자 이름은 영대문자, 30자 이내로 이루어져야 합니다.', + }, + expired: { + INVALID: '만료월/만료년은 숫자 0~9를 사용해 각 2자리로 이루어져야 합니다.', + }, + cardNumber: { + INVALID: '카드번호는 숫자 0~9를 사용해 각 4자리로 이루어져야 합니다.', + }, +}; From 31f8abfa7dfe10fc592587b3706df07015a43c10 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 13:19:38 +0900 Subject: [PATCH 039/102] =?UTF-8?q?chore:=20card=20=EC=82=AC=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/asset/bc_card.png | Bin 0 -> 16219 bytes src/asset/hana_card.png | Bin 0 -> 7978 bytes src/asset/hyundai_card.png | Bin 0 -> 2267 bytes src/asset/kakao_bank.png | Bin 0 -> 3277 bytes src/asset/kookmin_card.png | Bin 0 -> 22573 bytes src/asset/lotte_card.png | Bin 0 -> 4958 bytes src/asset/sinhan_card.png | Bin 0 -> 7708 bytes src/asset/woori_card.png | Bin 0 -> 9750 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/asset/bc_card.png create mode 100644 src/asset/hana_card.png create mode 100644 src/asset/hyundai_card.png create mode 100644 src/asset/kakao_bank.png create mode 100644 src/asset/kookmin_card.png create mode 100644 src/asset/lotte_card.png create mode 100644 src/asset/sinhan_card.png create mode 100644 src/asset/woori_card.png diff --git a/src/asset/bc_card.png b/src/asset/bc_card.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d540b5dc1835e74d1c16942739ac22a997eff2 GIT binary patch literal 16219 zcmV-hKcv8kP)@~0drDELIAGL9O(c600d`2O+f$vv5yPs>4G1wuo<4D9=oXmJ6O)_7c$xI~0 ziDSoc5O!=599s~tfI~M1FW49wvuk}qyO7lT_TIZybLyO`d+%#?x4I>@)ROvu`fYcu zI`=I9SxUfJN=G!>_F?suqjc|t38OcyhH{YE}TzcWM;EgQ{RlZ}H6n+)- z6hLW(bFTRxL`)zIQ)p%qZRC&nUWL5E+tM$>d~GF_pyNr}NW`{r?<7dKv+qMtRsu3# z$a>SKHp(fJp75$_8cKfe{w$nfbjC1L)1Axf{6g-Upd_^nn%Zi$3arQhCwlCA-kzC9 zgJ&2+@Ftd>_;x86mw+YlhMfKU~%R+^<&zZLQ07cUMOCm`N^xj~o(6 zD@nAla)Tw140V{*al}x_Mwa)?tHON^BNZfnHoGf(PC>EjUr;k zA|^m?GK`}nGnA8bhOyaWWR>WoChgtNwYY6mukGo#L=wExvaA?av!T%0{z&zPFJ)l_ z(}-m#y>=~1q!(6bNUVge_A1aU72Fd~3`OtF43JfWzj+n{u=dJ83>8z2S~wOG%&frb zOe*WsNf}BJ985GS<#v!7>%rS|MzO0N<5D_h9*WfDwQ>^#lehY zP*sslW@c%OC`u?4s9P1q5z@I0$}YJ zWg&qWIe^kZl%ZrXuL%zzoyJ!mU(3MDOamA@7X0dJfHCFa`S_ZE4F5zyLH^(&fYuhs z@7M)!s2R{f0G%s<=|N90QVKkx` zs_D*;)`=ik1s&aW+WJY-xn?2LurX^SiKC)nw>FKk%q7BYL@2O~GE*@h%$qPC#MCL^ z&zb>p{`ugaGaKZ@2`m+?5#J}pVF;rFjiH{;gW8$}y>$oVUws1#dy&a@?S-JDv(KX_ zzMD`z(syL2TkjO@waDm~;Wg7XwF|S!{mn~DHmu9S2%!fpzbZ0p`vH|I)>fxab1#rcD7r_nieA zi@@H}t@!*3>_M6JW7|5ET?GZuyZ1rx&Q9q0x9>n<%Xa2>dllN6;f7gcXuBPCv_cp| zAd2RV8wdXE>FY}V@KftWHA=2iOsAZonwH&Nr@DiOr7C31=g`xkhg0`EsS!o&#lWI9 zn$5AucrMVuE8}$;AacwYkc%#Y#LYK?x8Qt2y0x8U7H*0>v?I=V)|7wdQybtE z(kWvoy?S+3>zmnCx~X|38^QcNdq_HT)y1HnT-qC)ac(AAk{8BqlPpD>XvrmzTzVDw zwF@vDk^qjcIFGD&68pBjz)B>ZbBKYMA?C!eXY6_caPmj(-3OhY`Ev*kHv?FIR9ft+ z=q`lC(fS@aE1^Ub2ms5?1F}IatoLA~CSgI*39K-?o#6gSyYi3e*M*({U%kVOTh#^UljA zW=vmN^3d;P;UtrUlT7UoKD0u2_PngR3mG4Q!XQ3TA{tbB>INEQn-$=QR1)aW{vwmM zrUp_Uy9dfv{V^miUPxe}+eg?**P6XXLLrQ_=u5@w$g8w65JK?)(FF?~hacG0htS63 z10}Dx48-~8fcDG@Js1E@fIct8U5B=ENxKIUkA=)xKr3cp1a^Z#6 zrgENC#I-C;9KK}FXipicomeW1Q(@}eCeUr|0Bvoc4je$ZwF={LH6W9yY9~$vkxYWK z1GUnjV4=^20J^^U?@)O3Kk)cIUTx9!6$3>Co4~$z)E)S#RfEZHZy;no&gUbObE5oQA^koxi z71~2OHmObxB03Kmaizl6Euc1SgZwM6g5Liw;Y@Baz|YJ8 zF@8MA@#sOK-DW0Tzwj$)|NMH;T^)QcR-=Xw(@=*emVQImU~fcP_#}1pdm75h{M*Gt z4-I?xX4o03>E>k_ohqq^&aRBHZAAFAA3rH!Q6j7ER7Dx2Kk$A?FS`{)Wf?nPy;P$i zXYe{HNKd@9DiePT_Uwn;^FM>2@eL4r_k!AUfb_4@(gt@Nc*@6

    YmjjumK@P!yA! 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 literal 0 HcmV?d00001 diff --git a/src/asset/hyundai_card.png b/src/asset/hyundai_card.png new file mode 100644 index 0000000000000000000000000000000000000000..035cd2c652c50cffabc9b2224207bb011011c7f5 GIT binary patch literal 2267 zcmV<12qgE3P)@~0drDELIAGL9O(c600d`2O+f$vv5yP)NC5skTMAAE?pW#;2#K)C<4tvfs_ukXCO_O8A5dTr15BYn@_y{@#1fh@n~?Jq@f^nyMN~8J&Y? zAX$#vJaN(ROm*L=iS-Ca3P{ap!RTKjwiXV+uP%d+%jaqdSrv|J9HX13+SPNh}Oa+Y;Z7h*IVy{WRgjY|4BNm)SCD2S|M!`FVso8+^EP$g2tAK)+Y5u|lkqfrI zCH0iX2&6HT1J+GdMPtMwebgFChM~-ZR29iOn~_sJRY$pM0%~;CkdUM)Pr(~oqd;sx zBVp=Gn(`LFRA&>3Q(uw@rQj9f#6FJ0dqCPylJt}}Kx}0p?g!(hygJw$ZZpd0toqxPK1(hcMimMq&WYCA+^-72&KSo0RPMyWv&O6n#BLb@3kr3OVPp4pKoBs%Q~`$bfQs)J@C(MXi4h5aJR zP!~bSU_yJ(!&06Cui24!NZRWv&ch-UJ26RI`ik?g2*qNY#6`pFLRh0WLdm`E3WUT% zp{F?SHB=phB%>C5(QBwV5R#0u!56_AN`Ys0WHh9X(y|haP&E)T8f!fRK|`GdA)|4` zUkRo>b{CLQS-ZC9M<{j|kTl4(JtZqaLlKgOn*5-lNLI8xG~Ib|1O;MhY6^Sy?7`l> zd$D!v*7j=xeEISP@7}$`>({UG=FJ-vjIKV+i9ZI~7{@(z>Xdu;?p^o&`}Z!1=Iht5 z?(*`od*{v__vFcw?v^cE+({q5j)&UJOnPJH=jYw$&!4v&iX=zc?)2%?MKF~TL)9j= z0Xuf=aIamvMk8f}L@a632^y;1!C9ZgAK$cTQ#-wpY@an$0F&zI=&~A3uVuvvcQ896o#)b8~Yzc<^A~?~ffjhTFGq z!_-#z{P{C-M#+y`$l1wf&YTJT?|Sv>m0PdZ-HjVJPU=6bG?J5;OP4OWTIByHDfwffri>eZ`xZ;RY%p6`S|CcpRX_=yuI^4{^v$<$7L z?P_Y#ytl&vax|2j*?su%p=+lfJ$mGtp76YPypjq_eK|*dN3P{5QCZDK6${hp*7sfkuSu1bm6esg_qK1}jy%cLP@6Yz?t72X zDLabLmbn^=R393OWC{&MGDQrfK}a@eMktagVyM(JVH46pGv$#?5ktKNA=$tPMKWcX zh9X&_S(KA3X&b5qLb9Rdr#wa}k{0pMs(+8S4BZ_2^9^QZW-vS3{byRO7FJj3=PXc4 zwG|}C_HDwOo4C!d$Zu31RZWS9)-DdSjv{Gkd&oCb6NIFrZK&XLa%BAI3NfFOz|3&YJP1aE)DSroF^ct!O zLXvSS_<|EKk~C-xnSfFF9fZW=M(-P85lT8YS3t;c*3RyH!XlJhnr?!S!88#v+BfRH zyq`hHU_wiyL33X4+8&8Qqo)}6*)ZTs--5Gzl5l=RL?`&52qn?U0izV@MjIVP#IW%} z&o+=R+yEinNbM9krg+%AVT@8Fh=w;>>KwC`AN~h%>t& z8!(*zj6NC7gV;>unO%@1bFVhQZUkG=h!LHHBu}to@43AkkNd$mDbGh6$&>591F;D? zDf=Vthm%e>E`_<=wyr>IKx0TZK`%)k+A7@$--FnKlsP)d7m}nr9}T8H8<14@3lfqv z<@qr6RU3_%XLY@d`kZn}eaFG)N>vg6*p9>~DKGek+2L6|elq$K6NJx}s*V;>5E>{~ z!%_WeAOl7%%6nmvUT!o744O+qR+9($g`5T{8yW>Z!BWU^mUSw%9F!od$YXxxj+wGF zs;yc<4o`m03Yv;$${g8X2jyyL8|0vbX7W*>sfuPMg_5H{Q_)NrBmGnjl!>p)aN?I|;=%w_NlFb}PC+S&>fSXYfx4lKP4#*qryOWM@xn{w-5w8}ybc;Dn2 z3XE21WPMbMLV?jLi5g&3ibWkX)*xAR2A3s^ps{+5vx>r3>uUE7`gkVEGCj|T||YVQC7E66%7UNAf1J0 zG=*Xycb43PH|eu1H7Z6`4b{tRKu0>uaiAhL%8y>6{aBA= py~|#7XX%?pmdka%Q002ovPDHLkV1k3iB$WUF literal 0 HcmV?d00001 diff --git a/src/asset/kakao_bank.png b/src/asset/kakao_bank.png new file mode 100644 index 0000000000000000000000000000000000000000..33be9f66cd76dcf6e19af6ec3970f549daeb1cfc GIT binary patch literal 3277 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 LNkvXXu0mjfE?5E8 literal 0 HcmV?d00001 diff --git a/src/asset/kookmin_card.png b/src/asset/kookmin_card.png new file mode 100644 index 0000000000000000000000000000000000000000..c0fd6930ca59f3844dc8d6d1f0ec2996936d79a0 GIT binary patch literal 22573 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 literal 0 HcmV?d00001 diff --git a/src/asset/sinhan_card.png b/src/asset/sinhan_card.png new file mode 100644 index 0000000000000000000000000000000000000000..48abf32d582f04baf7eba0b68e4b2183e1b6d48e GIT binary patch literal 7708 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 WJf1?HLdjYH0000Dg|00009a7bBm001F4 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 Date: Thu, 27 Apr 2023 13:20:09 +0900 Subject: [PATCH 040/102] =?UTF-8?q?feat:=20CardSelectButton=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/custom.d.ts | 5 +++- .../AddCard/components/CardSelectButton.css | 23 +++++++++++++++++++ .../AddCard/components/CardSelectButton.tsx | 17 ++++++++++++++ src/utils/constants.ts | 20 ++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/pages/AddCard/components/CardSelectButton.css create mode 100644 src/pages/AddCard/components/CardSelectButton.tsx diff --git a/src/custom.d.ts b/src/custom.d.ts index e2937d470e..6218df03fa 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -1 +1,4 @@ -declare module '*.png'; +declare module '*.png' { + const path: string; + export default path; +} diff --git a/src/pages/AddCard/components/CardSelectButton.css b/src/pages/AddCard/components/CardSelectButton.css new file mode 100644 index 0000000000..8115ac24db --- /dev/null +++ b/src/pages/AddCard/components/CardSelectButton.css @@ -0,0 +1,23 @@ +.card-select-button { + display: flex; + padding: 0; + flex-direction: column; + align-items: center; + border: none; + width: 60px; + height: 62px; + background-color: white; + cursor: pointer; + justify-content: space-between; +} + +.card-select-button img { + width: 35px; + height: 35px; +} + +.card-select-button p { + margin: 0; + font-size: 12px; + color: #525252; +} diff --git a/src/pages/AddCard/components/CardSelectButton.tsx b/src/pages/AddCard/components/CardSelectButton.tsx new file mode 100644 index 0000000000..be69cf086e --- /dev/null +++ b/src/pages/AddCard/components/CardSelectButton.tsx @@ -0,0 +1,17 @@ +import { CARD_NAME_IMAGE_SRCS } from '../../../utils/constants'; +import './CardSelectButton.css'; + +type CardSelectButtonProps = { + cardName: string; + onClick: (e: React.MouseEvent) => void; +}; +const CardSelectButton = ({ onClick, cardName }: CardSelectButtonProps) => { + return ( + + ); +}; + +export default CardSelectButton; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index c11f6ac0c1..9b3a610140 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,3 +1,12 @@ +import BcCardIcon from '../asset/bc_card.png'; +import HanaCardIcon from '../asset/hana_card.png'; +import HyundaiCardIcon from '../asset/hyundai_card.png'; +import KakaoBankIcon from '../asset/kakao_bank.png'; +import KookminCardIcon from '../asset/kookmin_card.png'; +import LotteCardIcon from '../asset/lotte_card.png'; +import SinhanCardIcon from '../asset/sinhan_card.png'; +import WooriCardIcon from '../asset/woori_card.png'; + export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; export const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); @@ -21,3 +30,14 @@ export const INVALID_MESSAGE: { [key: string]: { [key: string]: string } } = { INVALID: '카드번호는 숫자 0~9를 사용해 각 4자리로 이루어져야 합니다.', }, }; + +export const CARD_NAME_IMAGE_SRCS: { [key: string]: string } = { + BC카드: BcCardIcon, + 하나카드: HanaCardIcon, + 현대카드: HyundaiCardIcon, + 신한카드: SinhanCardIcon, + 국민카드: KookminCardIcon, + 우리카드: WooriCardIcon, + 카카오뱅크: KakaoBankIcon, + 롯데카드: LotteCardIcon, +}; From ff3cf5766b3b7db1e3e0b2a193b4f24095df11c9 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 13:22:04 +0900 Subject: [PATCH 041/102] =?UTF-8?q?feat:=20BottomSheet=20=EC=97=B4?= =?UTF-8?q?=EA=B8=B0/=EB=8B=AB=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.css | 17 +++++++++++++++++ src/components/BottomSheet.tsx | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/components/BottomSheet.css diff --git a/src/components/BottomSheet.css b/src/components/BottomSheet.css new file mode 100644 index 0000000000..23a8cb507f --- /dev/null +++ b/src/components/BottomSheet.css @@ -0,0 +1,17 @@ +.bottom-sheet { + position: absolute; + top: 0; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +.back-drop { + background-color: rgba(0, 0, 0, 0.25); + height: 70%; +} + +.bottom-sheet-body { + background-color: white; + height: 30%; +} diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index e54c6cc239..961f3361c1 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -1,11 +1,22 @@ import { PropsWithChildren } from 'react'; +import './BottomSheet.css'; type BottomSheetProps = { isOpen: boolean; - onChangeOpen: () => void; + onToggleOpen: () => void; }; -const BottomSheet = ({ children }: PropsWithChildren) => { - return

    {children}
    ; + +const BottomSheet = ({ children, isOpen, onToggleOpen }: PropsWithChildren) => { + return ( + <> + {isOpen && ( +
    +
    +
    {children}
    +
    + )} + + ); }; export default BottomSheet; From 14472dbf015c59cbfd398aa43b27ea538c90f155 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 13:23:54 +0900 Subject: [PATCH 042/102] =?UTF-8?q?feat:=20CardNameBottomSheet=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 75 +++++++++++-------- .../components/CardNameBottomSheet.css | 9 +++ .../components/CardNameBottomSheet.tsx | 70 +++++++++++++++++ 3 files changed, 121 insertions(+), 33 deletions(-) create mode 100644 src/pages/AddCard/components/CardNameBottomSheet.css create mode 100644 src/pages/AddCard/components/CardNameBottomSheet.tsx diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index a761214115..76ba1a19d2 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -15,10 +15,11 @@ import { isValidPassword, isValidSecurityCode, } from './domain/dispatcher'; +import CardNameBottomSheet from './components/CardNameBottomSheet'; const AddCardPage = () => { const navigate = useNavigate(); - const [cardType] = useState('현대'); + const [cardType, setCardType] = useState('현대'); const cardFirstNumber = useInput(isValidCardNumber); const cardSecondNumber = useInput(isValidCardNumber); const cardThirdNumber = useInput(isValidCardNumber); @@ -29,44 +30,52 @@ const AddCardPage = () => { const expireYear = useInput(isValidExpiredYearFormat); const securityCode = useInput(isValidSecurityCode); const cardOwner = useInput(isValidOwnerName); + const [isOpen, setIsOpen] = useState(true); const onBackButtonClick = useCallback(() => { navigate('/'); }, [navigate]); return ( -
    -
    - -

    카드 추가

    -
    -
    - - -
    -
    + <> +
    +
    + +

    카드 추가

    +
    +
    + + +
    +
    + setIsOpen(!isOpen)} + setCardType={setCardType} + /> + ); }; diff --git a/src/pages/AddCard/components/CardNameBottomSheet.css b/src/pages/AddCard/components/CardNameBottomSheet.css new file mode 100644 index 0000000000..1a581c6f1a --- /dev/null +++ b/src/pages/AddCard/components/CardNameBottomSheet.css @@ -0,0 +1,9 @@ +.card-name-select { + display: grid; + height: 100%; + align-items: center; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, 1fr); + row-gap: 10px; + column-gap: 10px; +} diff --git a/src/pages/AddCard/components/CardNameBottomSheet.tsx b/src/pages/AddCard/components/CardNameBottomSheet.tsx new file mode 100644 index 0000000000..8f1ff70e63 --- /dev/null +++ b/src/pages/AddCard/components/CardNameBottomSheet.tsx @@ -0,0 +1,70 @@ +import BottomSheet from '../../../components/BottomSheet'; +import CardSelectButton from './CardSelectButton'; +import './CardNameBottomSheet.css'; + +const CardNameBottomSheet = ({ isOpen, onToggleOpen, setCardType }: any) => { + return ( + +
    + { + onToggleOpen(!isOpen); + setCardType('BC카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('신한카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('카카오뱅크'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('현대카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('우리카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('롯데카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('하나카드'); + }} + /> + { + onToggleOpen(!isOpen); + setCardType('국민카드'); + }} + /> +
    +
    + ); +}; + +export default CardNameBottomSheet; From 83ac68a6edd24552eeb2604af796267b2aa0f900 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 13:51:14 +0900 Subject: [PATCH 043/102] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EC=8B=9C=20=ED=86=A0=EA=B8=80=20=EB=8F=99=EC=9E=91?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EB=B0=8F=20=EC=84=A0=ED=83=9D=20=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.css | 34 ++++++++++++++++++++++++++++++- src/components/Card.tsx | 5 +++-- src/pages/AddCard/AddCardPage.tsx | 1 + src/type.d.ts | 2 +- src/utils/util.ts | 23 +++++++++++++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/components/Card.css b/src/components/Card.css index 17da265e48..a22fb2e170 100644 --- a/src/components/Card.css +++ b/src/components/Card.css @@ -4,7 +4,6 @@ justify-content: space-around; width: 213px; height: 133px; - background-color: #333333; border-radius: 5px; color: white; font-size: 12px; @@ -49,3 +48,36 @@ overflow: hidden; text-overflow: ellipsis; } + +.hyundai, +.default-company { + background-color: #333333; +} + +.bc { + background-color: #c2474c; +} + +.kakao { + background-color: #f5dd3f; +} + +.woori { + background-color: #457ac4; +} + +.kookmin { + background-color: darkgoldenrod; +} + +.hana { + background-color: darkcyan; +} + +.lotte { + background-color: crimson; +} + +.sinhan { + background-color: #2848fa; +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 368c323a1b..b0061a2328 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,5 +1,5 @@ import type { CardProps } from '../type'; -import { changeNumberToMask } from '../utils/util'; +import { changeNumberToMask, getBackgroundStyleByCardType } from '../utils/util'; import './Card.css'; const Card = ({ @@ -11,9 +11,10 @@ const Card = ({ cardOwner, expireYear, expireMonth, + onClick = () => {}, }: CardProps) => { return ( -
    +
    {cardType}
    diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index 76ba1a19d2..d9d79dab9e 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -55,6 +55,7 @@ const AddCardPage = () => { cardOwner={cardOwner.value} expireMonth={expireMonth.value} expireYear={expireYear.value} + onClick={() => setIsOpen(!isOpen)} /> ) => void; + onClick?: () => void; }; export type CardPassword = { diff --git a/src/utils/util.ts b/src/utils/util.ts index fdfd143c67..7e06361380 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -29,3 +29,26 @@ export const stringToUpperCase = (data: string): string => { }; export const identity = (v: any) => v; + +export const getBackgroundStyleByCardType = (cardType: string) => { + switch (cardType) { + case '현대카드': + return 'hyundai'; + case 'BC카드': + return 'bc'; + case '신한카드': + return 'sinhan'; + case '카카오뱅크': + return 'kakao'; + case '우리카드': + return 'woori'; + case '국민카드': + return 'kookmin'; + case '하나카드': + return 'hana'; + case '롯데카드': + return 'lotte'; + default: + return 'default-company'; + } +}; From 33f983e7eef706ba22bfa2939d88ee1804ce0ddd Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 16:24:30 +0900 Subject: [PATCH 044/102] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=EB=AA=85=20Card=20Component=EC=97=90=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/AddCardPage.tsx | 2 ++ src/pages/AddCard/components/AddCardForm.tsx | 3 ++- src/type.d.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/AddCardPage.tsx index d9d79dab9e..f68666e43e 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/AddCardPage.tsx @@ -32,6 +32,7 @@ const AddCardPage = () => { const cardOwner = useInput(isValidOwnerName); const [isOpen, setIsOpen] = useState(true); + // TODO: BottomSheet CustomHook 제작 const onBackButtonClick = useCallback(() => { navigate('/'); }, [navigate]); @@ -58,6 +59,7 @@ const AddCardPage = () => { onClick={() => setIsOpen(!isOpen)} /> Date: Thu, 27 Apr 2023 16:24:57 +0900 Subject: [PATCH 045/102] =?UTF-8?q?refactor:=20=ED=98=84=EC=9E=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20=EC=B9=B4=EB=93=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20Context=EB=A1=9C=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/CurrentCardProvider.tsx | 50 ++++++++++++++++++++ src/pages/AddCard/components/AddCardForm.tsx | 42 ++++++++-------- src/pages/CardAlias/index.tsx | 24 ++++++++++ src/router/AppRouter.tsx | 16 ++++++- src/utils/applicationUtil.ts | 22 +-------- 5 files changed, 113 insertions(+), 41 deletions(-) create mode 100644 src/context/CurrentCardProvider.tsx create mode 100644 src/pages/CardAlias/index.tsx diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx new file mode 100644 index 0000000000..53496088d3 --- /dev/null +++ b/src/context/CurrentCardProvider.tsx @@ -0,0 +1,50 @@ +import { + Dispatch, + PropsWithChildren, + SetStateAction, + createContext, + useContext, + useState, +} from 'react'; +import { CardType } from '../type'; + +type CurrentCard = { + currentCard: Omit; + setCurrentCard: Dispatch>>; +}; + +const CurrentCardContext = createContext(null); + +export const CurrentCardProvider = ({ children }: PropsWithChildren) => { + const [currentCard, setCurrentCard] = useState>({ + cardType: '', + cardNumber: { + first: '', + second: '', + third: '', + fourth: '', + }, + cardOwner: '', + expireMonth: '', + expireYear: '', + securityCode: '', + cardPassword: { + first: '', + second: '', + }, + }); + + return ( + + {children} + + ); +}; + +export const useCurrentCardContext = () => { + const toggleContext = useContext(CurrentCardContext); + if (toggleContext === null) { + throw new Error('Context가 존재하지 않습니다.'); + } + return toggleContext; +}; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index dca6e9f800..63418fc096 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import type { AddCardFormProps } from '../../../type'; +import type { AddCardFormProps, CardType } from '../../../type'; import { sumbitCard } from '../../../utils/applicationUtil'; import { useNavigate } from 'react-router-dom'; import ExpireDateInput from './ExpireDateInput'; @@ -9,6 +9,7 @@ import SecurityCodeInput from './SecurityCodeInput'; import PasswordInput from './PasswordInput'; import './AddCardForm.css'; import CardNumberInput from './CardNumberInput'; +import { useCurrentCardContext } from '../../../context/CurrentCardProvider'; const AddCardForm = ({ cardType, @@ -24,28 +25,31 @@ const AddCardForm = ({ cardPassword2, }: AddCardFormProps) => { const navigate = useNavigate(); + const { currentCard, setCurrentCard } = useCurrentCardContext(); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); + const submitData: Omit = { + cardType, + cardNumber: { + first: cardFirstNumber.value, + second: cardSecondNumber.value, + third: cardThirdNumber.value, + fourth: cardFourthNumber.value, + }, + cardOwner: cardOwner.value, + expireMonth: expireMonth.value, + expireYear: expireYear.value, + securityCode: securityCode.value, + cardPassword: { + first: cardPassword1.value, + second: cardPassword2.value, + }, + }; try { - sumbitCard( - cardType, - { - first: cardFirstNumber.value, - second: cardSecondNumber.value, - third: cardThirdNumber.value, - fourth: cardFourthNumber.value, - }, - cardOwner.value, - expireMonth.value, - expireYear.value, - securityCode.value, - { - first: cardPassword1.value, - second: cardPassword2.value, - } - ); - navigate('/'); + sumbitCard(submitData); + setCurrentCard(submitData); + navigate('/alias'); } catch (error) { alert('중복된 카드 입니다.'); return; diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx new file mode 100644 index 0000000000..6206788338 --- /dev/null +++ b/src/pages/CardAlias/index.tsx @@ -0,0 +1,24 @@ +import Card from '../../components/Card'; +import { useCurrentCardContext } from '../../context/CurrentCardProvider'; + +const CardAliasPage = () => { + const { currentCard } = useCurrentCardContext(); + return ( +
    +

    카드 등록이 완료되었습니다.

    + + ; +
    + ); +}; + +export default CardAliasPage; diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 2597fe7b59..42cff4310c 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -2,6 +2,8 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import AddCardPage from '../pages/AddCard/AddCardPage'; import CardListPage from '../pages/CardList/CardListPage'; +import CardAliasPage from '../pages/CardAlias'; +import { CurrentCardProvider } from '../context/CurrentCardProvider'; const router = createBrowserRouter([ { @@ -10,7 +12,19 @@ const router = createBrowserRouter([ }, { path: '/add', - element: , + element: ( + + + + ), + }, + { + path: '/alias', + element: ( + + + + ), }, ]); diff --git a/src/utils/applicationUtil.ts b/src/utils/applicationUtil.ts index d12205eaa6..2034173a73 100644 --- a/src/utils/applicationUtil.ts +++ b/src/utils/applicationUtil.ts @@ -34,27 +34,7 @@ export const postLocalStorage = (data: Omit) => { localStorage.setItem('cardList', JSON.stringify(dataToArr)); }; -export const sumbitCard = ( - cardType: string, - cardNumber: CardNumber, - cardOwner: string, - expireMonth: string, - expireYear: string, - securityCode: string, - cardPassword: CardPassword -) => { - const card: Omit = { - cardType, - cardNumber, - cardOwner, - expireMonth, - expireYear, - securityCode, - cardPassword: { - first: cardPassword.first, - second: cardPassword.second, - }, - }; +export const sumbitCard = (card: Omit) => { postLocalStorage(card); }; From 2b680dfa13fae0e2310a1cdb4fce3343f601bb7b Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 17:25:29 +0900 Subject: [PATCH 046/102] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EB=B3=84?= =?UTF-8?q?=EB=AA=85=20=EB=93=B1=EB=A1=9D=EC=B0=BD=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/domain/dispatcher.ts | 4 ++++ src/pages/CardAlias/index.tsx | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 1c4dcc02cc..03311ef617 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -28,3 +28,7 @@ export const isValidCardNumber = (str: string) => { const strToNum = +str; return str.length === 4 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; }; + +export const isValidCardAlias = (str: string) => { + return str.length !== 0 ? 'VALID' : 'INVALID'; +}; diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 6206788338..c585281f2a 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -1,8 +1,17 @@ +import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; import { useCurrentCardContext } from '../../context/CurrentCardProvider'; +import useInput from '../../hooks/useInput'; +import { isValidCardAlias } from '../AddCard/domain/dispatcher'; const CardAliasPage = () => { + const navigate = useNavigate(); const { currentCard } = useCurrentCardContext(); + const { value, onChange } = useInput(isValidCardAlias); + const onClick = () => { + // 별칭 등록 + navigate('/'); + }; return (

    카드 등록이 완료되었습니다.

    @@ -16,7 +25,8 @@ const CardAliasPage = () => { expireMonth={currentCard.expireMonth} expireYear={currentCard.expireYear} /> - ; + +
    ); }; From 5111925e6eddc8c3312ee47d0ae501fed7ac2f95 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 18:00:21 +0900 Subject: [PATCH 047/102] =?UTF-8?q?feat:=20PrivateRoute=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/CurrentCardProvider.tsx | 6 ++-- src/context/IsAccessAliasPageProvider.tsx | 33 ++++++++++++++++++++ src/pages/AddCard/components/AddCardForm.tsx | 5 ++- src/pages/CardAlias/index.tsx | 3 ++ src/router/AppRouter.tsx | 20 ++++++++---- src/router/PrivateRoute.tsx | 19 +++++++++++ 6 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/context/IsAccessAliasPageProvider.tsx create mode 100644 src/router/PrivateRoute.tsx diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index 53496088d3..045e3642c4 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -42,9 +42,9 @@ export const CurrentCardProvider = ({ children }: PropsWithChildren) => { }; export const useCurrentCardContext = () => { - const toggleContext = useContext(CurrentCardContext); - if (toggleContext === null) { + const currentCardContext = useContext(CurrentCardContext); + if (currentCardContext === null) { throw new Error('Context가 존재하지 않습니다.'); } - return toggleContext; + return currentCardContext; }; diff --git a/src/context/IsAccessAliasPageProvider.tsx b/src/context/IsAccessAliasPageProvider.tsx new file mode 100644 index 0000000000..ae1681e624 --- /dev/null +++ b/src/context/IsAccessAliasPageProvider.tsx @@ -0,0 +1,33 @@ +import { + Dispatch, + PropsWithChildren, + SetStateAction, + createContext, + useContext, + useState, +} from 'react'; + +type IsAccessAliasPage = { + isAccessAliasPage: boolean; + setIsAccessAliasPage: Dispatch>; +}; + +const IsAccessAliasPageContext = createContext(null); + +export const IsAccessAliasPageProvider = ({ children }: PropsWithChildren) => { + const [isAccessAliasPage, setIsAccessAliasPage] = useState(false); + + return ( + + {children} + + ); +}; + +export const useIsAccessAliasPageContext = () => { + const isAccessAliasPageContext = useContext(IsAccessAliasPageContext); + if (isAccessAliasPageContext === null) { + throw new Error('Context가 존재하지 않습니다.'); + } + return isAccessAliasPageContext; +}; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 63418fc096..726fc8faac 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -10,6 +10,7 @@ import PasswordInput from './PasswordInput'; import './AddCardForm.css'; import CardNumberInput from './CardNumberInput'; import { useCurrentCardContext } from '../../../context/CurrentCardProvider'; +import { useIsAccessAliasPageContext } from '../../../context/IsAccessAliasPageProvider'; const AddCardForm = ({ cardType, @@ -25,7 +26,8 @@ const AddCardForm = ({ cardPassword2, }: AddCardFormProps) => { const navigate = useNavigate(); - const { currentCard, setCurrentCard } = useCurrentCardContext(); + const { setCurrentCard } = useCurrentCardContext(); + const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -49,6 +51,7 @@ const AddCardForm = ({ try { sumbitCard(submitData); setCurrentCard(submitData); + setIsAccessAliasPage(true); navigate('/alias'); } catch (error) { alert('중복된 카드 입니다.'); diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index c585281f2a..a5b4467e1d 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -3,13 +3,16 @@ import Card from '../../components/Card'; import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import useInput from '../../hooks/useInput'; import { isValidCardAlias } from '../AddCard/domain/dispatcher'; +import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; const CardAliasPage = () => { const navigate = useNavigate(); const { currentCard } = useCurrentCardContext(); const { value, onChange } = useInput(isValidCardAlias); + const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const onClick = () => { // 별칭 등록 + setIsAccessAliasPage(false); navigate('/'); }; return ( diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 42cff4310c..a58bb56d09 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -4,6 +4,8 @@ import AddCardPage from '../pages/AddCard/AddCardPage'; import CardListPage from '../pages/CardList/CardListPage'; import CardAliasPage from '../pages/CardAlias'; import { CurrentCardProvider } from '../context/CurrentCardProvider'; +import { IsAccessAliasPageProvider } from '../context/IsAccessAliasPageProvider'; +import PrivateRoute from './PrivateRoute'; const router = createBrowserRouter([ { @@ -13,17 +15,23 @@ const router = createBrowserRouter([ { path: '/add', element: ( - - - + + + + + ), }, { path: '/alias', element: ( - - - + + + + + + + ), }, ]); diff --git a/src/router/PrivateRoute.tsx b/src/router/PrivateRoute.tsx new file mode 100644 index 0000000000..879a81a5f5 --- /dev/null +++ b/src/router/PrivateRoute.tsx @@ -0,0 +1,19 @@ +import { PropsWithChildren, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useIsAccessAliasPageContext } from '../context/IsAccessAliasPageProvider'; + +const PrivateRoute = ({ children }: PropsWithChildren) => { + const { isAccessAliasPage } = useIsAccessAliasPageContext(); + const navigate = useNavigate(); + + useEffect(() => { + if (!isAccessAliasPage) { + navigate('/'); + } + }, []); + + return <>{children}; +}; + +export default PrivateRoute; From 7c188a702d73593d9bffacf4ecb210c88db2ae4b Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 27 Apr 2023 18:56:02 +0900 Subject: [PATCH 048/102] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCard/{AddCardPage.css => index.css} | 0 .../AddCard/{AddCardPage.tsx => index.tsx} | 2 +- src/pages/CardAlias/index.tsx | 20 +++++++++++++- .../CardList/{CardListPage.css => index.css} | 0 .../CardList/{CardListPage.tsx => index.tsx} | 26 ++++++++++--------- src/router/AppRouter.tsx | 4 +-- 6 files changed, 36 insertions(+), 16 deletions(-) rename src/pages/AddCard/{AddCardPage.css => index.css} (100%) rename src/pages/AddCard/{AddCardPage.tsx => index.tsx} (99%) rename src/pages/CardList/{CardListPage.css => index.css} (100%) rename src/pages/CardList/{CardListPage.tsx => index.tsx} (64%) diff --git a/src/pages/AddCard/AddCardPage.css b/src/pages/AddCard/index.css similarity index 100% rename from src/pages/AddCard/AddCardPage.css rename to src/pages/AddCard/index.css diff --git a/src/pages/AddCard/AddCardPage.tsx b/src/pages/AddCard/index.tsx similarity index 99% rename from src/pages/AddCard/AddCardPage.tsx rename to src/pages/AddCard/index.tsx index f68666e43e..7dd629d4a8 100644 --- a/src/pages/AddCard/AddCardPage.tsx +++ b/src/pages/AddCard/index.tsx @@ -5,7 +5,7 @@ import Card from '../../components/Card'; import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; import BackButtonImg from '../../asset/back_button.png'; -import './AddCardPage.css'; +import './index.css'; import useInput from '../../hooks/useInput'; import { isValidCardNumber, diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index a5b4467e1d..40aab16eac 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -4,6 +4,24 @@ import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import useInput from '../../hooks/useInput'; import { isValidCardAlias } from '../AddCard/domain/dispatcher'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; +import { CardNumber, CardType } from '../../type'; +import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; + +const registerCardAlias = (alias: string, cardNumber: CardNumber) => { + const registerdCardNumber = getSerialNumber(cardNumber); + const cardList = fetchLocalStorage('cardList', '[]'); + const currentCard = cardList.find( + (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber + ); + const addedAliasCard = { alias, ...currentCard }; + const restCardList = cardList.filter( + (card: CardType) => getSerialNumber(card.cardNumber) !== registerdCardNumber + ); + + const newCardList = [...restCardList, addedAliasCard]; + + localStorage.setItem('cardList', JSON.stringify(newCardList)); +}; const CardAliasPage = () => { const navigate = useNavigate(); @@ -11,7 +29,7 @@ const CardAliasPage = () => { const { value, onChange } = useInput(isValidCardAlias); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const onClick = () => { - // 별칭 등록 + registerCardAlias(value, currentCard.cardNumber); setIsAccessAliasPage(false); navigate('/'); }; diff --git a/src/pages/CardList/CardListPage.css b/src/pages/CardList/index.css similarity index 100% rename from src/pages/CardList/CardListPage.css rename to src/pages/CardList/index.css diff --git a/src/pages/CardList/CardListPage.tsx b/src/pages/CardList/index.tsx similarity index 64% rename from src/pages/CardList/CardListPage.tsx rename to src/pages/CardList/index.tsx index f33dadc573..caaeb0df2a 100644 --- a/src/pages/CardList/CardListPage.tsx +++ b/src/pages/CardList/index.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import type { CardType } from '../../type'; import Card from '../../components/Card'; import Header from '../../components/Header'; -import './CardListPage.css'; +import './index.css'; import { fetchLocalStorage } from '../../utils/applicationUtil'; const CardListPage = () => { @@ -24,17 +24,19 @@ const CardListPage = () => { 새로운 카드를 등록해주세요. ) : ( cardList.map((card: CardType) => ( - +
    + +

    {card.alias}

    +
    )) )} -
    +
    {isActive && }
    ); }; From f567b368de81a1d7bcc2a3bb0c0b22c57260ae67 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 14:50:49 +0900 Subject: [PATCH 051/102] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 4 ++-- src/components/format.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 src/components/format.ts diff --git a/src/components/Card.tsx b/src/components/Card.tsx index b0061a2328..3628dfdfa2 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,5 +1,6 @@ import type { CardProps } from '../type'; import { changeNumberToMask, getBackgroundStyleByCardType } from '../utils/util'; +import { formatExpireDate } from './format'; import './Card.css'; const Card = ({ @@ -29,11 +30,10 @@ const Card = ({
    {cardOwner} - {`${expireYear}/${expireMonth}`} + {formatExpireDate(expireYear, expireMonth)}
    ); }; -// TODO: 만료일 입력 로직 외부로 분리 export default Card; diff --git a/src/components/format.ts b/src/components/format.ts new file mode 100644 index 0000000000..3b069113df --- /dev/null +++ b/src/components/format.ts @@ -0,0 +1,6 @@ +export const formatExpireDate = (year: string, month: string) => { + if (!year && !month) return ''; + if (!year) return month; + if (!month) return year; + return `${month}/${year}`; +}; From dffbe25d06a4d3a6bf7db2f14ee4a3b34d1c028a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 14:51:16 +0900 Subject: [PATCH 052/102] =?UTF-8?q?feat:=20bottom=20sheet=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20custom=20hook=EC=9C=BC=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useBottomSheet.ts | 12 ++++++++++++ src/pages/AddCard/index.tsx | 14 +++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useBottomSheet.ts diff --git a/src/hooks/useBottomSheet.ts b/src/hooks/useBottomSheet.ts new file mode 100644 index 0000000000..ac35788429 --- /dev/null +++ b/src/hooks/useBottomSheet.ts @@ -0,0 +1,12 @@ +import { useState } from 'react'; + +const useBottomSheet = (init = true) => { + const [isOpen, setIsOpen] = useState(init); + const toggleOpen = () => { + setIsOpen((isOpen) => !isOpen); + }; + + return { isOpen, toggleOpen }; +}; + +export default useBottomSheet; diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 7dd629d4a8..7010c5d273 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -16,10 +16,11 @@ import { isValidSecurityCode, } from './domain/dispatcher'; import CardNameBottomSheet from './components/CardNameBottomSheet'; +import useBottomSheet from '../../hooks/useBottomSheet'; const AddCardPage = () => { const navigate = useNavigate(); - const [cardType, setCardType] = useState('현대'); + const [cardType, setCardType] = useState(''); const cardFirstNumber = useInput(isValidCardNumber); const cardSecondNumber = useInput(isValidCardNumber); const cardThirdNumber = useInput(isValidCardNumber); @@ -30,9 +31,8 @@ const AddCardPage = () => { const expireYear = useInput(isValidExpiredYearFormat); const securityCode = useInput(isValidSecurityCode); const cardOwner = useInput(isValidOwnerName); - const [isOpen, setIsOpen] = useState(true); + const { isOpen, toggleOpen } = useBottomSheet(true); - // TODO: BottomSheet CustomHook 제작 const onBackButtonClick = useCallback(() => { navigate('/'); }, [navigate]); @@ -56,7 +56,7 @@ const AddCardPage = () => { cardOwner={cardOwner.value} expireMonth={expireMonth.value} expireYear={expireYear.value} - onClick={() => setIsOpen(!isOpen)} + onClick={toggleOpen} /> { />
    - setIsOpen(!isOpen)} - setCardType={setCardType} - /> + ); }; From 489aac6fba78f1234a585637feb0d17d6f2e2d36 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 16:05:37 +0900 Subject: [PATCH 053/102] =?UTF-8?q?refactor:=20=EC=9D=B8=EB=8D=B1=EC=8A=A4?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?Record=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/AddCardForm.tsx | 2 +- .../AddCard/components/CardSelectButton.tsx | 3 ++- src/pages/AddCard/domain/dispatcher.ts | 6 +++++- src/pages/AddCard/domain/domain.ts | 4 ++-- src/type.d.ts | 10 ++++++++++ src/utils/constants.ts | 18 +++++++++++++++--- 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 78bda18142..6974df9ca7 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import type { AddCardFormProps, CardType } from '../../../type'; import { sumbitCard } from '../../../utils/applicationUtil'; diff --git a/src/pages/AddCard/components/CardSelectButton.tsx b/src/pages/AddCard/components/CardSelectButton.tsx index be69cf086e..b44f92eef2 100644 --- a/src/pages/AddCard/components/CardSelectButton.tsx +++ b/src/pages/AddCard/components/CardSelectButton.tsx @@ -1,8 +1,9 @@ +import { CardCompany } from '../../../type'; import { CARD_NAME_IMAGE_SRCS } from '../../../utils/constants'; import './CardSelectButton.css'; type CardSelectButtonProps = { - cardName: string; + cardName: CardCompany; onClick: (e: React.MouseEvent) => void; }; const CardSelectButton = ({ onClick, cardName }: CardSelectButtonProps) => { diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 03311ef617..36dd3e74ea 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -1,10 +1,14 @@ import { ALPHABET, MONTH_DATA, NUMBERS } from '../../../utils/constants'; export const isValidExpiredMonthFormat = (str: string) => { - return MONTH_DATA.includes(str) ? 'VALID' : 'INVALID'; + if (!MONTH_DATA.includes(str)) return 'INVALID'; + + return 'VALID'; }; export const isValidExpiredYearFormat = (str: string) => { + // TODO: 유효 월 검증 + const strToNum = +str; return Number.isInteger(strToNum) && strToNum >= 0 ? 'VALID' : 'INVALID'; }; diff --git a/src/pages/AddCard/domain/domain.ts b/src/pages/AddCard/domain/domain.ts index 7a8bf07503..a52a8b72dc 100644 --- a/src/pages/AddCard/domain/domain.ts +++ b/src/pages/AddCard/domain/domain.ts @@ -1,4 +1,4 @@ -import { InputStatus } from '../../../type'; +import { CardInfoInput, InputStatus } from '../../../type'; import { INVALID_MESSAGE } from '../../../utils/constants'; export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { @@ -7,6 +7,6 @@ export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { return 'INIT'; }; -export const getErrorMessage = (inputType: string, status: InputStatus) => { +export const getErrorMessage = (inputType: CardInfoInput, status: InputStatus) => { return INVALID_MESSAGE[inputType][status]; }; diff --git a/src/type.d.ts b/src/type.d.ts index 8be3c0cc67..f512a90b95 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -41,6 +41,16 @@ export type InputHook = { }; export type InputStatus = 'INIT' | 'VALID' | 'INVALID'; +export type CardInfoInput = 'securityCode' | 'password' | 'owner' | 'expired' | 'cardNumber'; +export type CardCompany = + | 'BC카드' + | '하나카드' + | '현대카드' + | '신한카드' + | '국민카드' + | '우리카드' + | '카카오뱅크' + | '롯데카드'; export type AddCardFormProps = { cardType: string; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9b3a610140..ebf9b45556 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -6,6 +6,7 @@ import KookminCardIcon from '../asset/kookmin_card.png'; import LotteCardIcon from '../asset/lotte_card.png'; import SinhanCardIcon from '../asset/sinhan_card.png'; import WooriCardIcon from '../asset/woori_card.png'; +import { CardCompany, CardInfoInput, InputStatus } from '../type'; export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; @@ -13,25 +14,36 @@ export const ALPHABET = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); export const NUMBERS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; -export const INVALID_MESSAGE: { [key: string]: { [key: string]: string } } = { +export const INVALID_MESSAGE: Record> = { securityCode: { INVALID: '보안카드 번호는 숫자 0~9를 사용해 3자리로 이루어져야 합니다.', + VALID: '', + INIT: '', }, password: { INVALID: '비밀번호는 숫자 0~9를 사용해 각 1자리로 이루어져야 합니다.', + VALID: '', + INIT: '', }, owner: { INVALID: '카드 소유자 이름은 영대문자, 30자 이내로 이루어져야 합니다.', + VALID: '', + INIT: '', }, expired: { - INVALID: '만료월/만료년은 숫자 0~9를 사용해 각 2자리로 이루어져야 합니다.', + INVALID: + '만료월/만료년은 숫자 0~9를 사용해 각 2자리로 이루어져야 합니다. 혹은 유효기간이 지났는지 확인하세요.', + VALID: '', + INIT: '', }, cardNumber: { INVALID: '카드번호는 숫자 0~9를 사용해 각 4자리로 이루어져야 합니다.', + VALID: '', + INIT: '', }, }; -export const CARD_NAME_IMAGE_SRCS: { [key: string]: string } = { +export const CARD_NAME_IMAGE_SRCS: Record = { BC카드: BcCardIcon, 하나카드: HanaCardIcon, 현대카드: HyundaiCardIcon, From 095efb289b80ab4e8b4e4a3bb35e434583a13a47 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 16:26:18 +0900 Subject: [PATCH 054/102] =?UTF-8?q?feat:=20=EC=B9=B4=EB=93=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B4=80=EB=A6=AC=EC=97=90=20=EB=94=B0=EB=A5=B8=20cus?= =?UTF-8?q?tom=20hook=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 2 +- src/hooks/useSelectCardType.ts | 16 +++++++++++++++ src/pages/AddCard/components/AddCardForm.tsx | 3 ++- src/pages/AddCard/domain/dispatcher.ts | 7 ++++--- src/pages/AddCard/index.tsx | 21 +++++++++++++++----- src/type.d.ts | 1 + 6 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useSelectCardType.ts diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 3628dfdfa2..3620c8c33c 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -17,7 +17,7 @@ const Card = ({ return (
    - {cardType} + {cardType || '카드회사를 선택해 주세요.'}
    diff --git a/src/hooks/useSelectCardType.ts b/src/hooks/useSelectCardType.ts new file mode 100644 index 0000000000..deb1a390de --- /dev/null +++ b/src/hooks/useSelectCardType.ts @@ -0,0 +1,16 @@ +import { useState } from 'react'; +import { InputStatus } from '../type'; + +const useSelectCardType = (statusDispather: (str: string) => InputStatus, init = '') => { + const [cardCompany, setCardCompany] = useState(init); + const [status, setStatus] = useState('INIT'); + + const changeCardCompany = (str: string) => { + setStatus(statusDispather(str)); + setCardCompany(str); + }; + + return { cardCompany, status, changeCardCompany }; +}; + +export default useSelectCardType; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 6974df9ca7..66d7fec7a5 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -15,6 +15,7 @@ import useTotalStatus from '../../../hooks/useTotalStatus'; const AddCardForm = ({ cardType, + cardCompanyStatus, cardFirstNumber, cardSecondNumber, cardThirdNumber, @@ -30,6 +31,7 @@ const AddCardForm = ({ const { setCurrentCard } = useCurrentCardContext(); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const isActive = useTotalStatus([ + cardCompanyStatus, cardFirstNumber.status, cardSecondNumber.status, cardThirdNumber.status, @@ -43,7 +45,6 @@ const AddCardForm = ({ ]); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); - const submitData: Omit = { cardType, cardNumber: { diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 36dd3e74ea..32c6aff392 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -2,13 +2,14 @@ import { ALPHABET, MONTH_DATA, NUMBERS } from '../../../utils/constants'; export const isValidExpiredMonthFormat = (str: string) => { if (!MONTH_DATA.includes(str)) return 'INVALID'; - return 'VALID'; }; -export const isValidExpiredYearFormat = (str: string) => { - // TODO: 유효 월 검증 +export const isSelectCardType = (str: string) => { + return str.length === 0 ? 'INVALID' : 'VALID'; +}; +export const isValidExpiredYearFormat = (str: string) => { const strToNum = +str; return Number.isInteger(strToNum) && strToNum >= 0 ? 'VALID' : 'INVALID'; }; diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 7010c5d273..1bbf6d932d 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; @@ -8,6 +8,7 @@ import BackButtonImg from '../../asset/back_button.png'; import './index.css'; import useInput from '../../hooks/useInput'; import { + isSelectCardType, isValidCardNumber, isValidExpiredMonthFormat, isValidExpiredYearFormat, @@ -17,10 +18,15 @@ import { } from './domain/dispatcher'; import CardNameBottomSheet from './components/CardNameBottomSheet'; import useBottomSheet from '../../hooks/useBottomSheet'; +import useSelectCardType from '../../hooks/useSelectCardType'; const AddCardPage = () => { const navigate = useNavigate(); - const [cardType, setCardType] = useState(''); + const { + cardCompany, + status: cardCompanyStatus, + changeCardCompany, + } = useSelectCardType(isSelectCardType); const cardFirstNumber = useInput(isValidCardNumber); const cardSecondNumber = useInput(isValidCardNumber); const cardThirdNumber = useInput(isValidCardNumber); @@ -48,7 +54,7 @@ const AddCardPage = () => {
    { onClick={toggleOpen} /> { />
    - + ); }; diff --git a/src/type.d.ts b/src/type.d.ts index f512a90b95..08a14cd898 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -54,6 +54,7 @@ export type CardCompany = export type AddCardFormProps = { cardType: string; + cardCompanyStatus: InputStatus; cardFirstNumber: InputHook; cardSecondNumber: InputHook; cardThirdNumber: InputHook; From 2142e5b8b64821c7d00f4bb5666a5c50dea52fca Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 16:36:54 +0900 Subject: [PATCH 055/102] =?UTF-8?q?fix:=20=EB=B9=88=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EB=85=84=EB=8F=84=EB=A5=BC=20=EC=97=90=EB=9F=AC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=EC=8B=9D=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/ExpireDateInput.tsx | 2 ++ src/pages/AddCard/domain/dispatcher.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/AddCard/components/ExpireDateInput.tsx b/src/pages/AddCard/components/ExpireDateInput.tsx index c687dcc9b1..948267532f 100644 --- a/src/pages/AddCard/components/ExpireDateInput.tsx +++ b/src/pages/AddCard/components/ExpireDateInput.tsx @@ -15,6 +15,7 @@ const ExpireDateInput = ({ expireMonth, expireYear }: ExpireDateInputProps) => { onChange={expireMonth.onChange} name="month" maxLength={2} + placeholder="MM" required /> / @@ -24,6 +25,7 @@ const ExpireDateInput = ({ expireMonth, expireYear }: ExpireDateInputProps) => { onChange={expireYear.onChange} maxLength={2} name="year" + placeholder="YY" required />
    diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 32c6aff392..05458f5c17 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -11,7 +11,7 @@ export const isSelectCardType = (str: string) => { export const isValidExpiredYearFormat = (str: string) => { const strToNum = +str; - return Number.isInteger(strToNum) && strToNum >= 0 ? 'VALID' : 'INVALID'; + return Number.isInteger(strToNum) && strToNum > 0 ? 'VALID' : 'INVALID'; }; export const isValidSecurityCode = (str: string) => { From 7bccfcc0487b564154a6e12f7d7a57936f2d87b3 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 17:12:43 +0900 Subject: [PATCH 056/102] =?UTF-8?q?feat:=20css=20=EC=8A=A4=ED=83=80?= =?UTF-8?q?=EC=9D=BC=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardAlias/index.css | 42 +++++++++++++++++++++++++++++++++++ src/pages/CardAlias/index.tsx | 9 +++++--- src/pages/CardList/index.css | 5 +++++ src/pages/CardList/index.tsx | 2 +- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/pages/CardAlias/index.css diff --git a/src/pages/CardAlias/index.css b/src/pages/CardAlias/index.css new file mode 100644 index 0000000000..cff209e4ba --- /dev/null +++ b/src/pages/CardAlias/index.css @@ -0,0 +1,42 @@ +.card-alias-page { + display: flex; + flex-direction: column; + align-items: center; + height: 100vh; +} + +.card-alias-page h2 { + margin: 130px 0 40px 0; + font-size: 24px; + font-weight: 400; +} + +.card-alias-page input { + margin-top: 130px; + width: 265px; + border: none; + border-bottom: 2px solid #737373; + text-align: center; + font-size: 18px; +} + +.card-alias-page input:focus { + outline: none; + border-bottom: 3px solid #737373; +} + +.card-alias-footer button { + border: none; + background-color: white; + font-size: 14px; + font-weight: 700; + cursor: pointer; +} + +.card-alias-footer { + display: flex; + flex-direction: row-reverse; + width: 100%; + padding-right: 100px; + margin-top: 250px; +} diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 40aab16eac..7d67231ed8 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -6,6 +6,7 @@ import { isValidCardAlias } from '../AddCard/domain/dispatcher'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; import { CardNumber, CardType } from '../../type'; import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; +import './index.css'; const registerCardAlias = (alias: string, cardNumber: CardNumber) => { const registerdCardNumber = getSerialNumber(cardNumber); @@ -34,7 +35,7 @@ const CardAliasPage = () => { navigate('/'); }; return ( -
    +

    카드 등록이 완료되었습니다.

    { expireMonth={currentCard.expireMonth} expireYear={currentCard.expireYear} /> - - + +
    + +
    ); }; diff --git a/src/pages/CardList/index.css b/src/pages/CardList/index.css index 71c9b9aa73..bc0167a661 100644 --- a/src/pages/CardList/index.css +++ b/src/pages/CardList/index.css @@ -29,6 +29,11 @@ margin-bottom: 36px; } +.card-list-page-body .card-alias { + color: #525252; + text-align: center; +} + .empty-card-list-title { font-weight: 700; font-size: 14px; diff --git a/src/pages/CardList/index.tsx b/src/pages/CardList/index.tsx index caaeb0df2a..802209307c 100644 --- a/src/pages/CardList/index.tsx +++ b/src/pages/CardList/index.tsx @@ -35,7 +35,7 @@ const CardListPage = () => { expireMonth={card.expireMonth} expireYear={card.expireYear} /> -

    {card.alias}

    +

    {card.alias || card.cardType}

    )) )} From 798224df126980e2a8e7f7762b70ee4ff39ac06a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Fri, 28 Apr 2023 17:16:36 +0900 Subject: [PATCH 057/102] =?UTF-8?q?fix:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B0=B0=EC=97=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/PrivateRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/router/PrivateRoute.tsx b/src/router/PrivateRoute.tsx index 879a81a5f5..191301e7c0 100644 --- a/src/router/PrivateRoute.tsx +++ b/src/router/PrivateRoute.tsx @@ -11,7 +11,7 @@ const PrivateRoute = ({ children }: PropsWithChildren) => { if (!isAccessAliasPage) { navigate('/'); } - }, []); + }, [isAccessAliasPage, navigate]); return <>{children}; }; From 11b53f745f259e62ba35f852c5237333f7854ba0 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 14:47:17 +0900 Subject: [PATCH 058/102] =?UTF-8?q?refactor:=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/REQUIREMENTS.md | 8 ++++---- src/stories/AddCardNumberInput.stories.tsx | 2 +- src/stories/AddCardOwnerInput.stories.tsx | 2 +- src/stories/AddCardPasswordInput.stories.tsx | 2 +- src/stories/AddCardSecurityCodeInput.stories.tsx | 2 +- src/stories/ExpireDateInput.stories.tsx | 2 +- src/stories/constants.ts | 1 - src/utils/constants.ts | 2 ++ 8 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 src/stories/constants.ts diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index 02a9f475fa..d680954eba 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -28,10 +28,10 @@ - [x] 카드번호 뒷 8자리는 온점으로 표시한다. - [x] "?" 버튼에 hover 상태면 cvc에 대한 설명을 보여줌. - [x] (예외) 입력값이 잘못 들어오면 오류 메시지를 띄워준다. - - [ ] 카드를 클릭하면 bottom sheet을 연다. - - [ ] bottom sheet에서는 8개의 은행을 사용자가 선택할 수 있다. 은행을 선택하면 카드 좌측 상단에 선택된 카드 회사 이름이 뜨고, 카드 회사의 메인 색깔로 변경된다. - - [ ] 카드 등록이 완료되면 카드에 별칭을 부여할 수 있다. - - [ ] 별칭은 등록할 수도 있고 안할수도 있다. + - [x] 카드를 클릭하면 bottom sheet을 연다. + - [x] bottom sheet에서는 8개의 은행을 사용자가 선택할 수 있다. 은행을 선택하면 카드 좌측 상단에 선택된 카드 회사 이름이 뜨고, 카드 회사의 메인 색깔로 변경된다. + - [x] 카드 등록이 완료되면 카드에 별칭을 부여할 수 있다. + - [x] 별칭은 등록할 수도 있고 안할수도 있다. ### 카드 컴포넌트 diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index a79edb52cd..d0d81d63b3 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react'; import CardNumberInput from '../pages/AddCard/components/CardNumberInput'; -import { APP_WIDTH } from './constants'; +import { APP_WIDTH } from '../utils/constants'; import { isValidCardNumber } from '../pages/AddCard/domain/dispatcher'; import useInput from '../hooks/useInput'; diff --git a/src/stories/AddCardOwnerInput.stories.tsx b/src/stories/AddCardOwnerInput.stories.tsx index b7a97a6fea..96c084886d 100644 --- a/src/stories/AddCardOwnerInput.stories.tsx +++ b/src/stories/AddCardOwnerInput.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import OwnerInput from '../pages/AddCard/components/OwnerInput'; import useInput from '../hooks/useInput'; -import { APP_WIDTH } from './constants'; +import { APP_WIDTH } from '../utils/constants'; import { isValidOwnerName } from '../pages/AddCard/domain/dispatcher'; export default { diff --git a/src/stories/AddCardPasswordInput.stories.tsx b/src/stories/AddCardPasswordInput.stories.tsx index fd1ba0ee09..99d2890963 100644 --- a/src/stories/AddCardPasswordInput.stories.tsx +++ b/src/stories/AddCardPasswordInput.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import PasswordInput from '../pages/AddCard/components/PasswordInput'; import useInput from '../hooks/useInput'; -import { APP_WIDTH } from './constants'; +import { APP_WIDTH } from '../utils/constants'; import { isValidPassword } from '../pages/AddCard/domain/dispatcher'; export default { diff --git a/src/stories/AddCardSecurityCodeInput.stories.tsx b/src/stories/AddCardSecurityCodeInput.stories.tsx index 0381f44448..b27e6f8903 100644 --- a/src/stories/AddCardSecurityCodeInput.stories.tsx +++ b/src/stories/AddCardSecurityCodeInput.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import SecurityCodeInput from '../pages/AddCard/components/SecurityCodeInput'; import useInput from '../hooks/useInput'; -import { APP_WIDTH } from './constants'; +import { APP_WIDTH } from '../utils/constants'; import { isValidSecurityCode } from '../pages/AddCard/domain/dispatcher'; export default { diff --git a/src/stories/ExpireDateInput.stories.tsx b/src/stories/ExpireDateInput.stories.tsx index 3288b5789e..5c213890ff 100644 --- a/src/stories/ExpireDateInput.stories.tsx +++ b/src/stories/ExpireDateInput.stories.tsx @@ -1,7 +1,7 @@ import type { Meta } from '@storybook/react'; import ExpireDateInput from '../pages/AddCard/components/ExpireDateInput'; -import { APP_WIDTH } from './constants'; +import { APP_WIDTH } from '../utils/constants'; import useInput from '../hooks/useInput'; import { isValidExpiredMonthFormat, diff --git a/src/stories/constants.ts b/src/stories/constants.ts deleted file mode 100644 index 421c1aaebc..0000000000 --- a/src/stories/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const APP_WIDTH = '318px'; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index ebf9b45556..3690559dc8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -53,3 +53,5 @@ export const CARD_NAME_IMAGE_SRCS: Record = { 카카오뱅크: KakaoBankIcon, 롯데카드: LotteCardIcon, }; + +export const APP_WIDTH = '318px'; From e7df492a226e1b67ceef35a9d5336003b8dea685 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 14:57:44 +0900 Subject: [PATCH 059/102] =?UTF-8?q?refactor:=20=EB=B6=88=EB=B6=84=EB=AA=85?= =?UTF-8?q?=ED=95=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 8 ++++---- src/context/CurrentCardProvider.tsx | 2 +- src/hooks/useSelectCardType.ts | 4 ++-- src/pages/AddCard/components/AddCardForm.tsx | 4 ++-- .../AddCard/components/CardNameBottomSheet.tsx | 18 +++++++++--------- src/pages/AddCard/index.tsx | 8 ++++---- src/pages/CardAlias/index.tsx | 2 +- src/pages/CardList/index.tsx | 4 ++-- src/stories/Card.stories.ts | 2 +- src/type.d.ts | 6 +++--- src/utils/util.ts | 4 ++-- 11 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 3620c8c33c..9eb72f840b 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,10 +1,10 @@ import type { CardProps } from '../type'; -import { changeNumberToMask, getBackgroundStyleByCardType } from '../utils/util'; +import { changeNumberToMask, getBackgroundStyleByCardCompany } from '../utils/util'; import { formatExpireDate } from './format'; import './Card.css'; const Card = ({ - cardType, + cardCompany, cardFirstNumber, cardSecondNumber, cardThirdNumber, @@ -15,9 +15,9 @@ const Card = ({ onClick = () => {}, }: CardProps) => { return ( -
    +
    - {cardType || '카드회사를 선택해 주세요.'} + {cardCompany || '카드회사를 선택해 주세요.'}
    diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index 045e3642c4..75ab759662 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -17,7 +17,7 @@ const CurrentCardContext = createContext(null); export const CurrentCardProvider = ({ children }: PropsWithChildren) => { const [currentCard, setCurrentCard] = useState>({ - cardType: '', + cardCompany: '', cardNumber: { first: '', second: '', diff --git a/src/hooks/useSelectCardType.ts b/src/hooks/useSelectCardType.ts index deb1a390de..30237a2427 100644 --- a/src/hooks/useSelectCardType.ts +++ b/src/hooks/useSelectCardType.ts @@ -1,7 +1,7 @@ import { useState } from 'react'; import { InputStatus } from '../type'; -const useSelectCardType = (statusDispather: (str: string) => InputStatus, init = '') => { +const useSelectCardCompany = (statusDispather: (str: string) => InputStatus, init = '') => { const [cardCompany, setCardCompany] = useState(init); const [status, setStatus] = useState('INIT'); @@ -13,4 +13,4 @@ const useSelectCardType = (statusDispather: (str: string) => InputStatus, init = return { cardCompany, status, changeCardCompany }; }; -export default useSelectCardType; +export default useSelectCardCompany; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 66d7fec7a5..a2a88bc8e5 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -14,7 +14,7 @@ import { useIsAccessAliasPageContext } from '../../../context/IsAccessAliasPageP import useTotalStatus from '../../../hooks/useTotalStatus'; const AddCardForm = ({ - cardType, + cardCompany, cardCompanyStatus, cardFirstNumber, cardSecondNumber, @@ -46,7 +46,7 @@ const AddCardForm = ({ const onSubmit = (e: React.FormEvent) => { e.preventDefault(); const submitData: Omit = { - cardType, + cardCompany, cardNumber: { first: cardFirstNumber.value, second: cardSecondNumber.value, diff --git a/src/pages/AddCard/components/CardNameBottomSheet.tsx b/src/pages/AddCard/components/CardNameBottomSheet.tsx index 8f1ff70e63..a822b40964 100644 --- a/src/pages/AddCard/components/CardNameBottomSheet.tsx +++ b/src/pages/AddCard/components/CardNameBottomSheet.tsx @@ -2,7 +2,7 @@ import BottomSheet from '../../../components/BottomSheet'; import CardSelectButton from './CardSelectButton'; import './CardNameBottomSheet.css'; -const CardNameBottomSheet = ({ isOpen, onToggleOpen, setCardType }: any) => { +const CardNameBottomSheet = ({ isOpen, onToggleOpen, setCardType: setCardCompany }: any) => { return (
    @@ -10,56 +10,56 @@ const CardNameBottomSheet = ({ isOpen, onToggleOpen, setCardType }: any) => { cardName="BC카드" onClick={() => { onToggleOpen(!isOpen); - setCardType('BC카드'); + setCardCompany('BC카드'); }} /> { onToggleOpen(!isOpen); - setCardType('신한카드'); + setCardCompany('신한카드'); }} /> { onToggleOpen(!isOpen); - setCardType('카카오뱅크'); + setCardCompany('카카오뱅크'); }} /> { onToggleOpen(!isOpen); - setCardType('현대카드'); + setCardCompany('현대카드'); }} /> { onToggleOpen(!isOpen); - setCardType('우리카드'); + setCardCompany('우리카드'); }} /> { onToggleOpen(!isOpen); - setCardType('롯데카드'); + setCardCompany('롯데카드'); }} /> { onToggleOpen(!isOpen); - setCardType('하나카드'); + setCardCompany('하나카드'); }} /> { onToggleOpen(!isOpen); - setCardType('국민카드'); + setCardCompany('국민카드'); }} />
    diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 1bbf6d932d..8e96d17f82 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -18,7 +18,7 @@ import { } from './domain/dispatcher'; import CardNameBottomSheet from './components/CardNameBottomSheet'; import useBottomSheet from '../../hooks/useBottomSheet'; -import useSelectCardType from '../../hooks/useSelectCardType'; +import useSelectCardCompany from '../../hooks/useSelectCardType'; const AddCardPage = () => { const navigate = useNavigate(); @@ -26,7 +26,7 @@ const AddCardPage = () => { cardCompany, status: cardCompanyStatus, changeCardCompany, - } = useSelectCardType(isSelectCardType); + } = useSelectCardCompany(isSelectCardType); const cardFirstNumber = useInput(isValidCardNumber); const cardSecondNumber = useInput(isValidCardNumber); const cardThirdNumber = useInput(isValidCardNumber); @@ -54,7 +54,7 @@ const AddCardPage = () => {
    { onClick={toggleOpen} /> {

    카드 등록이 완료되었습니다.

    { cardList.map((card: CardType) => (
    { expireMonth={card.expireMonth} expireYear={card.expireYear} /> -

    {card.alias || card.cardType}

    +

    {card.alias || card.cardCompany}

    )) )} diff --git a/src/stories/Card.stories.ts b/src/stories/Card.stories.ts index 4f50513682..1cd2a85237 100644 --- a/src/stories/Card.stories.ts +++ b/src/stories/Card.stories.ts @@ -11,7 +11,7 @@ type Story = StoryObj; export const Primary: Story = { args: { - cardType: '현대', + cardCompany: '현대', cardFirstNumber: '1234', cardSecondNumber: '2345', cardThirdNumber: '4456', diff --git a/src/type.d.ts b/src/type.d.ts index 08a14cd898..403c141263 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -6,7 +6,7 @@ export type CardNumber = { }; type CardProps = { - cardType: string; + cardCompany: string; cardFirstNumber: string; cardSecondNumber: string; cardThirdNumber: string; @@ -25,7 +25,7 @@ export type CardPassword = { export type CardType = { id: number; alias?: string; - cardType: string; + cardCompany: string; cardNumber: CardNumber; cardOwner: string; expireMonth: string; @@ -53,7 +53,7 @@ export type CardCompany = | '롯데카드'; export type AddCardFormProps = { - cardType: string; + cardCompany: string; cardCompanyStatus: InputStatus; cardFirstNumber: InputHook; cardSecondNumber: InputHook; diff --git a/src/utils/util.ts b/src/utils/util.ts index 7e06361380..859541fc52 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -30,8 +30,8 @@ export const stringToUpperCase = (data: string): string => { export const identity = (v: any) => v; -export const getBackgroundStyleByCardType = (cardType: string) => { - switch (cardType) { +export const getBackgroundStyleByCardCompany = (cardCompany: string) => { + switch (cardCompany) { case '현대카드': return 'hyundai'; case 'BC카드': From 2818beac57605326ad24db1524bfa0dcdf136fb7 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 15:03:36 +0900 Subject: [PATCH 060/102] =?UTF-8?q?refactor:=20import=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=20=EB=B0=8F=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.tsx | 1 + src/components/InputContainer.tsx | 1 + src/context/CurrentCardProvider.tsx | 2 ++ src/context/IsAccessAliasPageProvider.tsx | 1 + src/hooks/useInput.ts | 1 + src/hooks/useSelectCardType.ts | 1 + src/hooks/useTotalStatus.ts | 1 + src/pages/AddCard/components/AddCardForm.tsx | 2 +- src/pages/AddCard/components/PasswordInput.tsx | 4 ++-- src/pages/AddCard/components/SecurityCodeInput.tsx | 3 +-- src/pages/AddCard/index.tsx | 8 ++++---- src/pages/CardAlias/index.tsx | 7 ++++--- 12 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index 961f3361c1..53b3b583e2 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -1,4 +1,5 @@ import { PropsWithChildren } from 'react'; + import './BottomSheet.css'; type BottomSheetProps = { diff --git a/src/components/InputContainer.tsx b/src/components/InputContainer.tsx index 2d434174aa..d080c7611b 100644 --- a/src/components/InputContainer.tsx +++ b/src/components/InputContainer.tsx @@ -1,4 +1,5 @@ import { PropsWithChildren } from 'react'; + import ErrorMessage from './ErrorMessage'; import { InputContainerProps } from '../type'; diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index 75ab759662..2da20653f4 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -6,8 +6,10 @@ import { useContext, useState, } from 'react'; + import { CardType } from '../type'; +// TODO: 타입 분리 type CurrentCard = { currentCard: Omit; setCurrentCard: Dispatch>>; diff --git a/src/context/IsAccessAliasPageProvider.tsx b/src/context/IsAccessAliasPageProvider.tsx index ae1681e624..2f512e9b1c 100644 --- a/src/context/IsAccessAliasPageProvider.tsx +++ b/src/context/IsAccessAliasPageProvider.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react'; +// TODO: 타입.ts로 분리 type IsAccessAliasPage = { isAccessAliasPage: boolean; setIsAccessAliasPage: Dispatch>; diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index ccc9fe0dbd..ebbd285b52 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -1,4 +1,5 @@ import { useState } from 'react'; + import { InputStatus } from '../type'; const useInput = (formatDispatcher: (str: string) => InputStatus, init = '') => { diff --git a/src/hooks/useSelectCardType.ts b/src/hooks/useSelectCardType.ts index 30237a2427..72b69d12b8 100644 --- a/src/hooks/useSelectCardType.ts +++ b/src/hooks/useSelectCardType.ts @@ -1,4 +1,5 @@ import { useState } from 'react'; + import { InputStatus } from '../type'; const useSelectCardCompany = (statusDispather: (str: string) => InputStatus, init = '') => { diff --git a/src/hooks/useTotalStatus.ts b/src/hooks/useTotalStatus.ts index 0010a00eb3..c65cec57c8 100644 --- a/src/hooks/useTotalStatus.ts +++ b/src/hooks/useTotalStatus.ts @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; + import { InputStatus } from '../type'; const useTotalStatus = (statusList: InputStatus[]): boolean => { diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index a2a88bc8e5..559b9b19e8 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; import type { AddCardFormProps, CardType } from '../../../type'; import { sumbitCard } from '../../../utils/applicationUtil'; -import { useNavigate } from 'react-router-dom'; import ExpireDateInput from './ExpireDateInput'; import OwnerInput from './OwnerInput'; import SecurityCodeInput from './SecurityCodeInput'; diff --git a/src/pages/AddCard/components/PasswordInput.tsx b/src/pages/AddCard/components/PasswordInput.tsx index bfec78f005..97ef5740f4 100644 --- a/src/pages/AddCard/components/PasswordInput.tsx +++ b/src/pages/AddCard/components/PasswordInput.tsx @@ -1,8 +1,8 @@ import type { PasswordInputProps } from '../../../type'; -import passwordDotImg from '../../../asset/password_dot.png'; -import './PasswordInput.css'; import InputContainer from '../../../components/InputContainer'; import { calcMultipleStatus } from '../domain/domain'; +import passwordDotImg from '../../../asset/password_dot.png'; +import './PasswordInput.css'; const PasswordInput = ({ cardPassword1, cardPassword2 }: PasswordInputProps) => { return ( diff --git a/src/pages/AddCard/components/SecurityCodeInput.tsx b/src/pages/AddCard/components/SecurityCodeInput.tsx index dfdab0af33..0ba04df80f 100644 --- a/src/pages/AddCard/components/SecurityCodeInput.tsx +++ b/src/pages/AddCard/components/SecurityCodeInput.tsx @@ -1,8 +1,7 @@ import type { SecurityCodeInputProps } from '../../../type'; import cvcInfo from '../../../asset/cvc_info.png'; - -import './SecurityCodeInput.css'; import InputContainer from '../../../components/InputContainer'; +import './SecurityCodeInput.css'; const SecurityCodeInput = ({ securityCode }: SecurityCodeInputProps) => { return ( diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 8e96d17f82..2dfb0d93a3 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -5,8 +5,10 @@ import Card from '../../components/Card'; import AddCardForm from './components/AddCardForm'; import Header from '../../components/Header'; import BackButtonImg from '../../asset/back_button.png'; -import './index.css'; +import CardNameBottomSheet from './components/CardNameBottomSheet'; import useInput from '../../hooks/useInput'; +import useBottomSheet from '../../hooks/useBottomSheet'; +import useSelectCardCompany from '../../hooks/useSelectCardType'; import { isSelectCardType, isValidCardNumber, @@ -16,9 +18,7 @@ import { isValidPassword, isValidSecurityCode, } from './domain/dispatcher'; -import CardNameBottomSheet from './components/CardNameBottomSheet'; -import useBottomSheet from '../../hooks/useBottomSheet'; -import useSelectCardCompany from '../../hooks/useSelectCardType'; +import './index.css'; const AddCardPage = () => { const navigate = useNavigate(); diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 6bf2f5b6c3..86e93e1b2b 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -1,10 +1,11 @@ import { useNavigate } from 'react-router-dom'; + +import { CardNumber, CardType } from '../../type'; import Card from '../../components/Card'; -import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import useInput from '../../hooks/useInput'; -import { isValidCardAlias } from '../AddCard/domain/dispatcher'; +import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; -import { CardNumber, CardType } from '../../type'; +import { isValidCardAlias } from '../AddCard/domain/dispatcher'; import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; import './index.css'; From 9972a62c27587685c1dd3f4a01f844b88aa14e21 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 15:06:30 +0900 Subject: [PATCH 061/102] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=20=EC=99=B8=EB=B6=80=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/CurrentCardProvider.tsx | 17 ++--------------- src/context/IsAccessAliasPageProvider.tsx | 15 ++------------- .../AddCard/components/CardNumberInput.tsx | 1 - src/type.d.ts | 10 ++++++++++ 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index 2da20653f4..7cd3e2761a 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -1,19 +1,6 @@ -import { - Dispatch, - PropsWithChildren, - SetStateAction, - createContext, - useContext, - useState, -} from 'react'; +import { PropsWithChildren, createContext, useContext, useState } from 'react'; -import { CardType } from '../type'; - -// TODO: 타입 분리 -type CurrentCard = { - currentCard: Omit; - setCurrentCard: Dispatch>>; -}; +import { CardType, CurrentCard } from '../type'; const CurrentCardContext = createContext(null); diff --git a/src/context/IsAccessAliasPageProvider.tsx b/src/context/IsAccessAliasPageProvider.tsx index 2f512e9b1c..068b32bd01 100644 --- a/src/context/IsAccessAliasPageProvider.tsx +++ b/src/context/IsAccessAliasPageProvider.tsx @@ -1,17 +1,6 @@ -import { - Dispatch, - PropsWithChildren, - SetStateAction, - createContext, - useContext, - useState, -} from 'react'; +import { PropsWithChildren, createContext, useContext, useState } from 'react'; -// TODO: 타입.ts로 분리 -type IsAccessAliasPage = { - isAccessAliasPage: boolean; - setIsAccessAliasPage: Dispatch>; -}; +import { IsAccessAliasPage } from '../type'; const IsAccessAliasPageContext = createContext(null); diff --git a/src/pages/AddCard/components/CardNumberInput.tsx b/src/pages/AddCard/components/CardNumberInput.tsx index 48bc29fd56..6823ca49e2 100644 --- a/src/pages/AddCard/components/CardNumberInput.tsx +++ b/src/pages/AddCard/components/CardNumberInput.tsx @@ -11,7 +11,6 @@ const CardNumberInput = ({ cardThirdNumber, cardFourthNumber, }: CardNumberInputProps) => { - // TODO: memo를 적용시키려면 객체를 내리면 안된다. return ( ; + setCurrentCard: Dispatch>>; +}; + +export type IsAccessAliasPage = { + isAccessAliasPage: boolean; + setIsAccessAliasPage: Dispatch>; +}; From ebd050a167762a16ec7febeaa807e5c6407a43b9 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 18:31:14 +0900 Subject: [PATCH 062/102] =?UTF-8?q?test:=20=EA=B0=80=EC=A7=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/utils/util.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/test/utils/util.ts diff --git a/src/test/utils/util.ts b/src/test/utils/util.ts new file mode 100644 index 0000000000..bb28b9f1f1 --- /dev/null +++ b/src/test/utils/util.ts @@ -0,0 +1,3 @@ +export const mockEventTarget = (str: string) => { + return { target: { value: str } }; +}; From cef89d9c04d50b318af581f0e816d9685473452d Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 18:32:06 +0900 Subject: [PATCH 063/102] =?UTF-8?q?test:=20useInput=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EB=B2=88=ED=98=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/test/hooks/useInput.test.ts diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts new file mode 100644 index 0000000000..8b529d03bc --- /dev/null +++ b/src/test/hooks/useInput.test.ts @@ -0,0 +1,27 @@ +import { renderHook } from '@testing-library/react'; +import useInput from '../../hooks/useInput'; +import { isValidCardNumber, isValidExpiredYearFormat } from '../../pages/AddCard/domain/dispatcher'; +import { act } from 'react-dom/test-utils'; +import { mockEventTarget } from '../utils/util'; + +describe('카드 번호 테스트', () => { + it('카드 번호 Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidCardNumber)); + expect(result.current.status).toBe('INIT'); + }); + it('유효한 카드 번호 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidCardNumber)); + act(() => { + result.current.onChange(mockEventTarget('1234') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('1234'); + }); + it('유효하지 않은 카드 번호 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidCardNumber)); + act(() => { + result.current.onChange(mockEventTarget('12354') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); +}); From 98f3572b0648588a2cf77d31f8790d41c83ce4c8 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 18:32:17 +0900 Subject: [PATCH 064/102] =?UTF-8?q?test:=20useInput=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EB=A7=8C=EB=A3=8C=EC=9D=BC(=EB=85=84=EB=8F=84)=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index 8b529d03bc..04029254da 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -25,3 +25,25 @@ describe('카드 번호 테스트', () => { expect(result.current.status).toBe('INVALID'); }); }); + +describe('만료일(년도) 테스트', () => { + it('카드 번호 Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + expect(result.current.status).toBe('INIT'); + }); + it('유효한 카드 번호 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + act(() => { + result.current.onChange(mockEventTarget('23') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('23'); + }); + it('유효하지 않은 카드 번호 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + act(() => { + result.current.onChange(mockEventTarget('a8') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); +}); From 6a66921e2e0312cfd8bddc95857436b953364916 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 19:53:21 +0900 Subject: [PATCH 065/102] =?UTF-8?q?test:=20useInput=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EB=A7=8C=EB=A3=8C=EC=9D=BC(=EC=9B=94)=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index 04029254da..1191900002 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -1,6 +1,10 @@ import { renderHook } from '@testing-library/react'; import useInput from '../../hooks/useInput'; -import { isValidCardNumber, isValidExpiredYearFormat } from '../../pages/AddCard/domain/dispatcher'; +import { + isValidCardNumber, + isValidExpiredMonthFormat, + isValidExpiredYearFormat, +} from '../../pages/AddCard/domain/dispatcher'; import { act } from 'react-dom/test-utils'; import { mockEventTarget } from '../utils/util'; @@ -27,11 +31,11 @@ describe('카드 번호 테스트', () => { }); describe('만료일(년도) 테스트', () => { - it('카드 번호 Hook의 초기 상태는 INIT를 가진다.', () => { + it('만료일(년도) Hook의 초기 상태는 INIT를 가진다.', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); expect(result.current.status).toBe('INIT'); }); - it('유효한 카드 번호 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + it('유효한 만료일(년도) 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('23') as React.ChangeEvent); @@ -39,7 +43,7 @@ describe('만료일(년도) 테스트', () => { expect(result.current.status).toBe('VALID'); expect(result.current.value).toBe('23'); }); - it('유효하지 않은 카드 번호 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + it('유효하지 않은 만료일(년도) 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('a8') as React.ChangeEvent); @@ -47,3 +51,25 @@ describe('만료일(년도) 테스트', () => { expect(result.current.status).toBe('INVALID'); }); }); + +describe('만료일(월) 테스트', () => { + it('만료일(월) Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + expect(result.current.status).toBe('INIT'); + }); + it('유효한 만료일(월) 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + act(() => { + result.current.onChange(mockEventTarget('12') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('12'); + }); + it('유효하지 않은 만료일(월) 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + act(() => { + result.current.onChange(mockEventTarget('13') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); +}); From d938f6fe071e07c12eaa384074dc480e2249792a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 20:00:29 +0900 Subject: [PATCH 066/102] =?UTF-8?q?test:=20useInput=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EC=86=8C=EC=9C=A0=EC=9E=90=20=EC=9D=B4=EB=A6=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index 1191900002..46388af1f6 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -4,6 +4,7 @@ import { isValidCardNumber, isValidExpiredMonthFormat, isValidExpiredYearFormat, + isValidOwnerName, } from '../../pages/AddCard/domain/dispatcher'; import { act } from 'react-dom/test-utils'; import { mockEventTarget } from '../utils/util'; @@ -73,3 +74,35 @@ describe('만료일(월) 테스트', () => { expect(result.current.status).toBe('INVALID'); }); }); + +describe('카드 소유자 이름 테스트', () => { + it('카드 소유자 이름 Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidOwnerName)); + expect(result.current.status).toBe('INIT'); + }); + it('카드 소유자 이름 입력이 들어오면, 카드 소유자 이름의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidOwnerName)); + act(() => { + result.current.onChange(mockEventTarget('YUNSEONG') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('YUNSEONG'); + }); + it('유효하지 않은 카드 소유자 이름 입력이 들어오면, 카드 소유자 이름의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidOwnerName)); + act(() => { + result.current.onChange(mockEventTarget('이윤성') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); + + it('소유자 이름을 작성했다가 지웠을 경우에는, 카드 소유자 이름은 선택이므로 유효한 값이다.', () => { + const { result } = renderHook(() => useInput(isValidOwnerName)); + act(() => { + result.current.onChange(mockEventTarget('YUNSEONG') as React.ChangeEvent); + result.current.onChange(mockEventTarget('') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe(''); + }); +}); From 0a894c99c83a0c20275dbd97291ea019a132c554 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 20:00:51 +0900 Subject: [PATCH 067/102] =?UTF-8?q?fix:=20=EC=86=8C=EC=9C=A0=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=B4=20=EC=97=86=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/domain/dispatcher.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/dispatcher.ts index 05458f5c17..27d0825e6f 100644 --- a/src/pages/AddCard/domain/dispatcher.ts +++ b/src/pages/AddCard/domain/dispatcher.ts @@ -22,7 +22,9 @@ export const isValidSecurityCode = (str: string) => { export const isValidOwnerName = (str: string) => { const charList = str.split('').filter((char) => ALPHABET.includes(char)); - return str.length > 0 && str.length <= 30 && charList.length === str.length ? 'VALID' : 'INVALID'; + return str.length >= 0 && str.length <= 30 && charList.length === str.length + ? 'VALID' + : 'INVALID'; }; export const isValidPassword = (str: string) => { From 993ba4e968d97bda4ab8991a4f5e5ba97db551d4 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 20:03:19 +0900 Subject: [PATCH 068/102] =?UTF-8?q?test:=20useInput=20=EB=B3=B4=EC=95=88?= =?UTF-8?q?=EB=B2=88=ED=98=B8(CVC/CVV)=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index 46388af1f6..bafca468a5 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -5,6 +5,7 @@ import { isValidExpiredMonthFormat, isValidExpiredYearFormat, isValidOwnerName, + isValidSecurityCode, } from '../../pages/AddCard/domain/dispatcher'; import { act } from 'react-dom/test-utils'; import { mockEventTarget } from '../utils/util'; @@ -106,3 +107,25 @@ describe('카드 소유자 이름 테스트', () => { expect(result.current.value).toBe(''); }); }); + +describe('보안코드(CVC/CVV) 테스트', () => { + it('보안코드(CVC/CVV) Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidSecurityCode)); + expect(result.current.status).toBe('INIT'); + }); + it('보안코드(CVC/CVV) 입력이 들어오면, 보안코드(CVC/CVV)의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidSecurityCode)); + act(() => { + result.current.onChange(mockEventTarget('123') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('123'); + }); + it('유효하지 않은 보안코드(CVC/CVV) 입력이 들어오면, 보안코드(CVC/CVV)의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidSecurityCode)); + act(() => { + result.current.onChange(mockEventTarget('13') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); +}); From e79142bb9bb98062f64f378135dfeacd57bfbcce Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 20:05:44 +0900 Subject: [PATCH 069/102] =?UTF-8?q?test:=20useInput=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index bafca468a5..1522ebd6d3 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -5,6 +5,7 @@ import { isValidExpiredMonthFormat, isValidExpiredYearFormat, isValidOwnerName, + isValidPassword, isValidSecurityCode, } from '../../pages/AddCard/domain/dispatcher'; import { act } from 'react-dom/test-utils'; @@ -129,3 +130,25 @@ describe('보안코드(CVC/CVV) 테스트', () => { expect(result.current.status).toBe('INVALID'); }); }); + +describe('카드 비밀번호 테스트', () => { + it('카드 비밀번호 Hook의 초기 상태는 INIT를 가진다.', () => { + const { result } = renderHook(() => useInput(isValidPassword)); + expect(result.current.status).toBe('INIT'); + }); + it('카드 비밀번호 입력이 들어오면, 카드 비밀번호의 상태가 유효한 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidPassword)); + act(() => { + result.current.onChange(mockEventTarget('1') as React.ChangeEvent); + }); + expect(result.current.status).toBe('VALID'); + expect(result.current.value).toBe('1'); + }); + it('유효하지 않은 카드 비밀번호 입력이 들어오면, 카드 비밀번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + const { result } = renderHook(() => useInput(isValidPassword)); + act(() => { + result.current.onChange(mockEventTarget('a') as React.ChangeEvent); + }); + expect(result.current.status).toBe('INVALID'); + }); +}); From b231ebbbfdb79a309053c928d75244be362ffca3 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Sat, 29 Apr 2023 20:09:34 +0900 Subject: [PATCH 070/102] =?UTF-8?q?test:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EB=AA=A9=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/hooks/useInput.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index 1522ebd6d3..bd3b42c971 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -16,7 +16,7 @@ describe('카드 번호 테스트', () => { const { result } = renderHook(() => useInput(isValidCardNumber)); expect(result.current.status).toBe('INIT'); }); - it('유효한 카드 번호 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + it('유효한 카드 번호 입력이 들어오면, 카드 번호의 상태가 유효한 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidCardNumber)); act(() => { result.current.onChange(mockEventTarget('1234') as React.ChangeEvent); @@ -24,7 +24,7 @@ describe('카드 번호 테스트', () => { expect(result.current.status).toBe('VALID'); expect(result.current.value).toBe('1234'); }); - it('유효하지 않은 카드 번호 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + it('유효하지 않은 카드 번호 입력이 들어오면, 카드 번호의 상태가 유효하지 않는 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidCardNumber)); act(() => { result.current.onChange(mockEventTarget('12354') as React.ChangeEvent); @@ -38,7 +38,7 @@ describe('만료일(년도) 테스트', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); expect(result.current.status).toBe('INIT'); }); - it('유효한 만료일(년도) 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + it('유효한 만료일(년도) 입력이 들어오면, 만료일(년도)의 상태가 유효한 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('23') as React.ChangeEvent); @@ -46,7 +46,7 @@ describe('만료일(년도) 테스트', () => { expect(result.current.status).toBe('VALID'); expect(result.current.value).toBe('23'); }); - it('유효하지 않은 만료일(년도) 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + it('유효하지 않은 만료일(년도) 입력이 들어오면, 만료일(년도)의 상태가 유효하지 않는 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('a8') as React.ChangeEvent); @@ -60,7 +60,7 @@ describe('만료일(월) 테스트', () => { const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); expect(result.current.status).toBe('INIT'); }); - it('유효한 만료일(월) 입력이 들어오면, 카드번호의 상태가 유효한 상태로 변경된다.', () => { + it('유효한 만료일(월) 입력이 들어오면, 만료일(월)의 상태가 유효한 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); act(() => { result.current.onChange(mockEventTarget('12') as React.ChangeEvent); @@ -68,7 +68,7 @@ describe('만료일(월) 테스트', () => { expect(result.current.status).toBe('VALID'); expect(result.current.value).toBe('12'); }); - it('유효하지 않은 만료일(월) 입력이 들어오면, 카드번호의 상태가 유효하지 않는 상태로 변경된다.', () => { + it('유효하지 않은 만료일(월) 입력이 들어오면, 만료일(월)의 상태가 유효하지 않는 상태로 변경된다.', () => { const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); act(() => { result.current.onChange(mockEventTarget('13') as React.ChangeEvent); From 1d6c28d9815d5de42e1d93a8f7e8cec921d02db2 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Mon, 1 May 2023 17:49:33 +0900 Subject: [PATCH 071/102] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/util.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/util.ts b/src/utils/util.ts index 859541fc52..f25b7eacda 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -28,8 +28,6 @@ export const stringToUpperCase = (data: string): string => { return data.toUpperCase(); }; -export const identity = (v: any) => v; - export const getBackgroundStyleByCardCompany = (cardCompany: string) => { switch (cardCompany) { case '현대카드': From b6925fb26df5cfdc8f52d5429d27d8c50e2e1144 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 10:52:53 +0900 Subject: [PATCH 072/102] =?UTF-8?q?refactor:=20=ED=95=A8=EC=88=98=EB=A5=BC?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EB=A6=AC=ED=84=B0=EB=9F=B4=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 5 +++-- src/context/CurrentCardProvider.tsx | 2 +- src/hooks/useSelectCardType.ts | 11 +++++++---- src/stories/Card.stories.ts | 2 +- src/type.d.ts | 11 ++++++----- src/utils/constants.ts | 13 +++++++++++++ src/utils/util.ts | 23 ----------------------- 7 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 9eb72f840b..507bab0284 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,6 +1,7 @@ import type { CardProps } from '../type'; -import { changeNumberToMask, getBackgroundStyleByCardCompany } from '../utils/util'; +import { changeNumberToMask } from '../utils/util'; import { formatExpireDate } from './format'; +import { CARD_COMPANY_ENG } from '../utils/constants'; import './Card.css'; const Card = ({ @@ -15,7 +16,7 @@ const Card = ({ onClick = () => {}, }: CardProps) => { return ( -
    +
    {cardCompany || '카드회사를 선택해 주세요.'}
    diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index 7cd3e2761a..da89e7423a 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -6,7 +6,7 @@ const CurrentCardContext = createContext(null); export const CurrentCardProvider = ({ children }: PropsWithChildren) => { const [currentCard, setCurrentCard] = useState>({ - cardCompany: '', + cardCompany: '카드사 선택', cardNumber: { first: '', second: '', diff --git a/src/hooks/useSelectCardType.ts b/src/hooks/useSelectCardType.ts index 72b69d12b8..490c048f25 100644 --- a/src/hooks/useSelectCardType.ts +++ b/src/hooks/useSelectCardType.ts @@ -1,12 +1,15 @@ import { useState } from 'react'; -import { InputStatus } from '../type'; +import { CardCompany, InputStatus } from '../type'; -const useSelectCardCompany = (statusDispather: (str: string) => InputStatus, init = '') => { - const [cardCompany, setCardCompany] = useState(init); +const useSelectCardCompany = ( + statusDispather: (str: CardCompany) => InputStatus, + init: CardCompany = '카드사 선택' +) => { + const [cardCompany, setCardCompany] = useState(init); const [status, setStatus] = useState('INIT'); - const changeCardCompany = (str: string) => { + const changeCardCompany = (str: CardCompany) => { setStatus(statusDispather(str)); setCardCompany(str); }; diff --git a/src/stories/Card.stories.ts b/src/stories/Card.stories.ts index 1cd2a85237..b3d7c1774e 100644 --- a/src/stories/Card.stories.ts +++ b/src/stories/Card.stories.ts @@ -11,7 +11,7 @@ type Story = StoryObj; export const Primary: Story = { args: { - cardCompany: '현대', + cardCompany: '국민카드', cardFirstNumber: '1234', cardSecondNumber: '2345', cardThirdNumber: '4456', diff --git a/src/type.d.ts b/src/type.d.ts index 129cf66349..f98452eb35 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -5,8 +5,8 @@ export type CardNumber = { fourth: string; }; -type CardProps = { - cardCompany: string; +export type CardProps = { + cardCompany: CardCompany; cardFirstNumber: string; cardSecondNumber: string; cardThirdNumber: string; @@ -25,7 +25,7 @@ export type CardPassword = { export type CardType = { id: number; alias?: string; - cardCompany: string; + cardCompany: CardCompany; cardNumber: CardNumber; cardOwner: string; expireMonth: string; @@ -50,10 +50,11 @@ export type CardCompany = | '국민카드' | '우리카드' | '카카오뱅크' - | '롯데카드'; + | '롯데카드' + | '카드사 선택'; export type AddCardFormProps = { - cardCompany: string; + cardCompany: CardCompany; cardCompanyStatus: InputStatus; cardFirstNumber: InputHook; cardSecondNumber: InputHook; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3690559dc8..000b4e6e64 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -52,6 +52,19 @@ export const CARD_NAME_IMAGE_SRCS: Record = { 우리카드: WooriCardIcon, 카카오뱅크: KakaoBankIcon, 롯데카드: LotteCardIcon, + '카드사 선택': '', +}; + +export const CARD_COMPANY_ENG: Record = { + 현대카드: 'hyundai', + BC카드: 'bc', + 신한카드: 'sinhan', + 카카오뱅크: 'kakao', + 우리카드: 'woori', + 국민카드: 'kookmin', + 하나카드: 'hana', + 롯데카드: 'lotte', + '카드사 선택': 'default-company', }; export const APP_WIDTH = '318px'; diff --git a/src/utils/util.ts b/src/utils/util.ts index f25b7eacda..76950b7be6 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -27,26 +27,3 @@ export const changeNumberToMask = (data: string): string => { export const stringToUpperCase = (data: string): string => { return data.toUpperCase(); }; - -export const getBackgroundStyleByCardCompany = (cardCompany: string) => { - switch (cardCompany) { - case '현대카드': - return 'hyundai'; - case 'BC카드': - return 'bc'; - case '신한카드': - return 'sinhan'; - case '카카오뱅크': - return 'kakao'; - case '우리카드': - return 'woori'; - case '국민카드': - return 'kookmin'; - case '하나카드': - return 'hana'; - case '롯데카드': - return 'lotte'; - default: - return 'default-company'; - } -}; From aa0d89d8b26136f602b195a079ec92f0493f0f47 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 15:21:32 +0900 Subject: [PATCH 073/102] =?UTF-8?q?refactor:=20=EC=B6=94=EB=A1=A0=ED=95=A0?= =?UTF-8?q?=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/util.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/utils/util.ts b/src/utils/util.ts index 76950b7be6..96e6b097ad 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,10 +1,11 @@ import { ALPHABET, NUMBERS } from './constants'; -export const formatExpireDate = (expireDate: string): string => { +// TODO: 구체화 하기 +export const formatExpireDate = (expireDate: string) => { return expireDate; }; -export const handleNumberInput = (data: string): string => { +export const handleNumberInput = (data: string) => { if (!NUMBERS.includes(data[data.length - 1])) { data = data.slice(0, -1); } @@ -12,18 +13,18 @@ export const handleNumberInput = (data: string): string => { return data; }; -export const isAlphabetInput = (data: string): boolean => { +export const isAlphabetInput = (data: string) => { return ALPHABET.includes(data); }; -export const isNumberInput = (data: string): boolean => { +export const isNumberInput = (data: string) => { return NUMBERS.includes(data); }; -export const changeNumberToMask = (data: string): string => { +export const changeNumberToMask = (data: string) => { return '·'.repeat(data.length); }; -export const stringToUpperCase = (data: string): string => { +export const stringToUpperCase = (data: string) => { return data.toUpperCase(); }; From 80d4b86a98a33e4f6d5bf271c8306d60bbaef518 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 15:31:49 +0900 Subject: [PATCH 074/102] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EA=B0=90=EC=8B=BC=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0.=20named=20export=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/AddCardForm.tsx | 4 ++-- src/utils/applicationUtil.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 559b9b19e8..9506cbaa93 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import type { AddCardFormProps, CardType } from '../../../type'; -import { sumbitCard } from '../../../utils/applicationUtil'; +import { postLocalStorage as submitCard } from '../../../utils/applicationUtil'; import ExpireDateInput from './ExpireDateInput'; import OwnerInput from './OwnerInput'; import SecurityCodeInput from './SecurityCodeInput'; @@ -63,7 +63,7 @@ const AddCardForm = ({ }, }; try { - sumbitCard(submitData); + submitCard(submitData); setCurrentCard(submitData); setIsAccessAliasPage(true); navigate('/alias'); diff --git a/src/utils/applicationUtil.ts b/src/utils/applicationUtil.ts index 1a3f049fb4..f362152b82 100644 --- a/src/utils/applicationUtil.ts +++ b/src/utils/applicationUtil.ts @@ -34,10 +34,6 @@ export const postLocalStorage = (data: Omit) => { localStorage.setItem('cardList', JSON.stringify(dataToArr)); }; -export const sumbitCard = (card: Omit) => { - postLocalStorage(card); -}; - export const fetchLocalStorage = (key: string, initial = '') => { return JSON.parse(localStorage.getItem('cardList') ?? initial); }; From 7ca547b3dfe3fe3215ada6e19019697518401487 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 16:01:15 +0900 Subject: [PATCH 075/102] =?UTF-8?q?refactor:=20any=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.tsx | 6 +---- src/context/CurrentCardProvider.tsx | 19 ++------------- .../components/CardNameBottomSheet.tsx | 23 +++++++++++-------- src/pages/AddCard/index.tsx | 2 +- src/type.d.ts | 9 ++++++++ src/utils/constants.ts | 20 +++++++++++++++- 6 files changed, 46 insertions(+), 33 deletions(-) diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index 53b3b583e2..e7261679bb 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -1,11 +1,7 @@ import { PropsWithChildren } from 'react'; import './BottomSheet.css'; - -type BottomSheetProps = { - isOpen: boolean; - onToggleOpen: () => void; -}; +import { BottomSheetProps } from '../type'; const BottomSheet = ({ children, isOpen, onToggleOpen }: PropsWithChildren) => { return ( diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx index da89e7423a..7f0c02a1a3 100644 --- a/src/context/CurrentCardProvider.tsx +++ b/src/context/CurrentCardProvider.tsx @@ -1,27 +1,12 @@ import { PropsWithChildren, createContext, useContext, useState } from 'react'; import { CardType, CurrentCard } from '../type'; +import { CARD_INIT } from '../utils/constants'; const CurrentCardContext = createContext(null); export const CurrentCardProvider = ({ children }: PropsWithChildren) => { - const [currentCard, setCurrentCard] = useState>({ - cardCompany: '카드사 선택', - cardNumber: { - first: '', - second: '', - third: '', - fourth: '', - }, - cardOwner: '', - expireMonth: '', - expireYear: '', - securityCode: '', - cardPassword: { - first: '', - second: '', - }, - }); + const [currentCard, setCurrentCard] = useState>(CARD_INIT); return ( diff --git a/src/pages/AddCard/components/CardNameBottomSheet.tsx b/src/pages/AddCard/components/CardNameBottomSheet.tsx index a822b40964..65a9e7a3cf 100644 --- a/src/pages/AddCard/components/CardNameBottomSheet.tsx +++ b/src/pages/AddCard/components/CardNameBottomSheet.tsx @@ -1,64 +1,69 @@ import BottomSheet from '../../../components/BottomSheet'; import CardSelectButton from './CardSelectButton'; import './CardNameBottomSheet.css'; +import { CardNameBottomSheetProps } from '../../../type'; -const CardNameBottomSheet = ({ isOpen, onToggleOpen, setCardType: setCardCompany }: any) => { +const CardNameBottomSheet = ({ + isOpen, + onToggleOpen, + setCardCompany, +}: CardNameBottomSheetProps) => { return (
    { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('BC카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('신한카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('카카오뱅크'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('현대카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('우리카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('롯데카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('하나카드'); }} /> { - onToggleOpen(!isOpen); + onToggleOpen(); setCardCompany('국민카드'); }} /> diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 2dfb0d93a3..8294bd1215 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -83,7 +83,7 @@ const AddCardPage = () => { ); diff --git a/src/type.d.ts b/src/type.d.ts index f98452eb35..8746e2a72b 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -103,3 +103,12 @@ export type IsAccessAliasPage = { isAccessAliasPage: boolean; setIsAccessAliasPage: Dispatch>; }; + +export type BottomSheetProps = { + isOpen: boolean; + onToggleOpen: () => void; +}; + +export type CardNameBottomSheetProps = BottomSheetProps & { + setCardCompany: (cardCompany: CardCompany) => void; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 000b4e6e64..1f2e0253c9 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -6,7 +6,7 @@ import KookminCardIcon from '../asset/kookmin_card.png'; import LotteCardIcon from '../asset/lotte_card.png'; import SinhanCardIcon from '../asset/sinhan_card.png'; import WooriCardIcon from '../asset/woori_card.png'; -import { CardCompany, CardInfoInput, InputStatus } from '../type'; +import { CardCompany, CardInfoInput, CardType, InputStatus } from '../type'; export const MONTH_DATA = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']; @@ -68,3 +68,21 @@ export const CARD_COMPANY_ENG: Record = { }; export const APP_WIDTH = '318px'; + +export const CARD_INIT: Omit = { + cardCompany: '카드사 선택', + cardNumber: { + first: '', + second: '', + third: '', + fourth: '', + }, + cardOwner: '', + expireMonth: '', + expireYear: '', + securityCode: '', + cardPassword: { + first: '', + second: '', + }, +}; From 83b6c3a87ede2e54a154b4c7815988c36435ae6a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 16:03:55 +0900 Subject: [PATCH 076/102] =?UTF-8?q?refactor:=20=EC=9D=B8=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=84=A0=EC=96=B8=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCard/components/CardNumberInput.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pages/AddCard/components/CardNumberInput.tsx b/src/pages/AddCard/components/CardNumberInput.tsx index 6823ca49e2..ab64272e91 100644 --- a/src/pages/AddCard/components/CardNumberInput.tsx +++ b/src/pages/AddCard/components/CardNumberInput.tsx @@ -11,17 +11,14 @@ const CardNumberInput = ({ cardThirdNumber, cardFourthNumber, }: CardNumberInputProps) => { + const status = calcMultipleStatus([ + cardFirstNumber.status, + cardSecondNumber.status, + cardThirdNumber.status, + cardFourthNumber.status, + ]); return ( - + 카드 번호
    Date: Tue, 2 May 2023 16:38:00 +0900 Subject: [PATCH 077/102] =?UTF-8?q?refactor:=20props=20=EC=A0=84=EB=8B=AC?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useSelectCardType.ts | 6 +++--- src/pages/AddCard/components/AddCardForm.tsx | 5 ++--- src/pages/AddCard/components/CardSelectButton.tsx | 1 + src/pages/AddCard/index.tsx | 11 +++-------- src/type.d.ts | 9 +++++++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/hooks/useSelectCardType.ts b/src/hooks/useSelectCardType.ts index 490c048f25..87617d64ec 100644 --- a/src/hooks/useSelectCardType.ts +++ b/src/hooks/useSelectCardType.ts @@ -6,15 +6,15 @@ const useSelectCardCompany = ( statusDispather: (str: CardCompany) => InputStatus, init: CardCompany = '카드사 선택' ) => { - const [cardCompany, setCardCompany] = useState(init); + const [value, setValue] = useState(init); const [status, setStatus] = useState('INIT'); const changeCardCompany = (str: CardCompany) => { setStatus(statusDispather(str)); - setCardCompany(str); + setValue(str); }; - return { cardCompany, status, changeCardCompany }; + return { value, status, changeCardCompany }; }; export default useSelectCardCompany; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 9506cbaa93..2350266a64 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -15,7 +15,6 @@ import useTotalStatus from '../../../hooks/useTotalStatus'; const AddCardForm = ({ cardCompany, - cardCompanyStatus, cardFirstNumber, cardSecondNumber, cardThirdNumber, @@ -31,7 +30,7 @@ const AddCardForm = ({ const { setCurrentCard } = useCurrentCardContext(); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const isActive = useTotalStatus([ - cardCompanyStatus, + cardCompany.status, cardFirstNumber.status, cardSecondNumber.status, cardThirdNumber.status, @@ -46,7 +45,7 @@ const AddCardForm = ({ const onSubmit = (e: React.FormEvent) => { e.preventDefault(); const submitData: Omit = { - cardCompany, + cardCompany: cardCompany.value, cardNumber: { first: cardFirstNumber.value, second: cardSecondNumber.value, diff --git a/src/pages/AddCard/components/CardSelectButton.tsx b/src/pages/AddCard/components/CardSelectButton.tsx index b44f92eef2..953b6d1940 100644 --- a/src/pages/AddCard/components/CardSelectButton.tsx +++ b/src/pages/AddCard/components/CardSelectButton.tsx @@ -6,6 +6,7 @@ type CardSelectButtonProps = { cardName: CardCompany; onClick: (e: React.MouseEvent) => void; }; + const CardSelectButton = ({ onClick, cardName }: CardSelectButtonProps) => { return ( ); }; diff --git a/src/type.d.ts b/src/type.d.ts index 80d5cb3984..bc075c456f 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -117,3 +117,8 @@ export type BottomSheetProps = { export type CardNameBottomSheetProps = BottomSheetProps & { setCardCompany: (cardCompany: CardCompany) => void; }; + +export type CardSelectButtonProps = { + company: CardCompany; + onClick: (e: React.MouseEvent) => void; +}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1f2e0253c9..bb161b2f5c 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -86,3 +86,14 @@ export const CARD_INIT: Omit = { second: '', }, }; + +export const CARD_COMPANYS: CardCompany[] = [ + 'BC카드', + '신한카드', + '카카오뱅크', + '현대카드', + '우리카드', + '롯데카드', + '하나카드', + '국민카드', +]; From d4306a62335976bba7856a3dd3de9410406f0a01 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 17:06:47 +0900 Subject: [PATCH 079/102] =?UTF-8?q?refactor:=20=EB=8B=A4=EC=86=8C=20?= =?UTF-8?q?=EC=9D=98=EB=AF=B8=EA=B0=80=20=EB=8B=A4=EB=A5=B8=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/domain/{dispatcher.ts => validation.ts} | 0 src/pages/AddCard/index.tsx | 2 +- src/pages/CardAlias/index.tsx | 2 +- src/stories/AddCardNumberInput.stories.tsx | 2 +- src/stories/AddCardOwnerInput.stories.tsx | 2 +- src/stories/AddCardPasswordInput.stories.tsx | 2 +- src/stories/AddCardSecurityCodeInput.stories.tsx | 2 +- src/stories/ExpireDateInput.stories.tsx | 2 +- src/test/hooks/useInput.test.ts | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) rename src/pages/AddCard/domain/{dispatcher.ts => validation.ts} (100%) diff --git a/src/pages/AddCard/domain/dispatcher.ts b/src/pages/AddCard/domain/validation.ts similarity index 100% rename from src/pages/AddCard/domain/dispatcher.ts rename to src/pages/AddCard/domain/validation.ts diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 6e4ab81dd2..110397180d 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -17,7 +17,7 @@ import { isValidOwnerName, isValidPassword, isValidSecurityCode, -} from './domain/dispatcher'; +} from './domain/validation'; import './index.css'; const AddCardPage = () => { diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 86e93e1b2b..85eaa57b6a 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -5,7 +5,7 @@ import Card from '../../components/Card'; import useInput from '../../hooks/useInput'; import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; -import { isValidCardAlias } from '../AddCard/domain/dispatcher'; +import { isValidCardAlias } from '../AddCard/domain/validation'; import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; import './index.css'; diff --git a/src/stories/AddCardNumberInput.stories.tsx b/src/stories/AddCardNumberInput.stories.tsx index d0d81d63b3..6873929ab0 100644 --- a/src/stories/AddCardNumberInput.stories.tsx +++ b/src/stories/AddCardNumberInput.stories.tsx @@ -2,7 +2,7 @@ import type { Meta } from '@storybook/react'; import CardNumberInput from '../pages/AddCard/components/CardNumberInput'; import { APP_WIDTH } from '../utils/constants'; -import { isValidCardNumber } from '../pages/AddCard/domain/dispatcher'; +import { isValidCardNumber } from '../pages/AddCard/domain/validation'; import useInput from '../hooks/useInput'; export default { diff --git a/src/stories/AddCardOwnerInput.stories.tsx b/src/stories/AddCardOwnerInput.stories.tsx index 96c084886d..4814aefb97 100644 --- a/src/stories/AddCardOwnerInput.stories.tsx +++ b/src/stories/AddCardOwnerInput.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import OwnerInput from '../pages/AddCard/components/OwnerInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from '../utils/constants'; -import { isValidOwnerName } from '../pages/AddCard/domain/dispatcher'; +import { isValidOwnerName } from '../pages/AddCard/domain/validation'; export default { title: 'AddCardOwnerInput', diff --git a/src/stories/AddCardPasswordInput.stories.tsx b/src/stories/AddCardPasswordInput.stories.tsx index 99d2890963..ad50aa46c5 100644 --- a/src/stories/AddCardPasswordInput.stories.tsx +++ b/src/stories/AddCardPasswordInput.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import PasswordInput from '../pages/AddCard/components/PasswordInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from '../utils/constants'; -import { isValidPassword } from '../pages/AddCard/domain/dispatcher'; +import { isValidPassword } from '../pages/AddCard/domain/validation'; export default { title: 'AddCardPasswordInput', diff --git a/src/stories/AddCardSecurityCodeInput.stories.tsx b/src/stories/AddCardSecurityCodeInput.stories.tsx index b27e6f8903..28b6d44ae8 100644 --- a/src/stories/AddCardSecurityCodeInput.stories.tsx +++ b/src/stories/AddCardSecurityCodeInput.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import SecurityCodeInput from '../pages/AddCard/components/SecurityCodeInput'; import useInput from '../hooks/useInput'; import { APP_WIDTH } from '../utils/constants'; -import { isValidSecurityCode } from '../pages/AddCard/domain/dispatcher'; +import { isValidSecurityCode } from '../pages/AddCard/domain/validation'; export default { title: 'AddCardSecurityCodeInput', diff --git a/src/stories/ExpireDateInput.stories.tsx b/src/stories/ExpireDateInput.stories.tsx index 5c213890ff..3f6bcb228a 100644 --- a/src/stories/ExpireDateInput.stories.tsx +++ b/src/stories/ExpireDateInput.stories.tsx @@ -6,7 +6,7 @@ import useInput from '../hooks/useInput'; import { isValidExpiredMonthFormat, isValidExpiredYearFormat, -} from '../pages/AddCard/domain/dispatcher'; +} from '../pages/AddCard/domain/validation'; export default { title: 'AddCardExpireDateInput', diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index bd3b42c971..eecda819f3 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -7,7 +7,7 @@ import { isValidOwnerName, isValidPassword, isValidSecurityCode, -} from '../../pages/AddCard/domain/dispatcher'; +} from '../../pages/AddCard/domain/validation'; import { act } from 'react-dom/test-utils'; import { mockEventTarget } from '../utils/util'; From 7e6d49b7f71e4198fa3d61a236d8770798ab0bbb Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 21:04:59 +0900 Subject: [PATCH 080/102] =?UTF-8?q?fix:=20=EB=B3=B4=EC=95=88=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=99=80=20=EC=B9=B4=EB=93=9C=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EC=97=90=20=EA=B3=B5=EB=B0=B1=EC=9D=B4=20=ED=8F=AC=ED=95=A8?= =?UTF-8?q?=EB=90=98=EC=96=B4=EB=8F=84=20=ED=86=B5=EA=B3=BC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/domain/validation.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/AddCard/domain/validation.ts b/src/pages/AddCard/domain/validation.ts index 27d0825e6f..3cb27f85ed 100644 --- a/src/pages/AddCard/domain/validation.ts +++ b/src/pages/AddCard/domain/validation.ts @@ -15,8 +15,10 @@ export const isValidExpiredYearFormat = (str: string) => { }; export const isValidSecurityCode = (str: string) => { - const strToNum = +str; - return str.length === 3 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; + const strToArr = str.split(''); + return str.length === 3 && strToArr.every((element) => NUMBERS.includes(element)) + ? 'VALID' + : 'INVALID'; }; export const isValidOwnerName = (str: string) => { @@ -32,8 +34,10 @@ export const isValidPassword = (str: string) => { }; export const isValidCardNumber = (str: string) => { - const strToNum = +str; - return str.length === 4 && Number.isInteger(strToNum) ? 'VALID' : 'INVALID'; + const strToArr = str.split(''); + return str.length === 4 && strToArr.every((element) => NUMBERS.includes(element)) + ? 'VALID' + : 'INVALID'; }; export const isValidCardAlias = (str: string) => { From a08bd83bb8d6463d256bbc93e59e91968c9f835f Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 21:49:48 +0900 Subject: [PATCH 081/102] =?UTF-8?q?fix:=20=EB=A7=8C=EB=A3=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B3=A0=EB=8F=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useValidExpireDate.ts | 32 ++++++++++++++++++++++++++ src/pages/AddCard/domain/validation.ts | 12 +--------- src/pages/AddCard/index.tsx | 4 ++-- 3 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 src/hooks/useValidExpireDate.ts diff --git a/src/hooks/useValidExpireDate.ts b/src/hooks/useValidExpireDate.ts new file mode 100644 index 0000000000..5cb0721c51 --- /dev/null +++ b/src/hooks/useValidExpireDate.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { MONTH_DATA } from '../utils/constants'; + +const YEAR = new Date().getFullYear(); +const YEAR_LAST = YEAR.toString().slice(2, 4); +const MONTH = new Date().getMonth() + 1; + +const useValidExpireDate = () => { + const [isCurrentYear, setIsCurrentYear] = useState(false); + + const isValidExpiredMonthFormat = (str: string) => { + if (!MONTH_DATA.includes(str)) return 'INVALID'; + if (isCurrentYear && MONTH >= +str) return 'INVALID'; + return 'VALID'; + }; + + const isValidExpiredYearFormat = (str: string) => { + const strToNum = +str; + + if (strToNum >= +YEAR_LAST) { + setIsCurrentYear(true); + return 'VALID'; + } + + setIsCurrentYear(false); + return 'INVALID'; + }; + + return { isValidExpiredMonthFormat, isValidExpiredYearFormat }; +}; + +export default useValidExpireDate; diff --git a/src/pages/AddCard/domain/validation.ts b/src/pages/AddCard/domain/validation.ts index 3cb27f85ed..3e2af357a9 100644 --- a/src/pages/AddCard/domain/validation.ts +++ b/src/pages/AddCard/domain/validation.ts @@ -1,19 +1,9 @@ -import { ALPHABET, MONTH_DATA, NUMBERS } from '../../../utils/constants'; - -export const isValidExpiredMonthFormat = (str: string) => { - if (!MONTH_DATA.includes(str)) return 'INVALID'; - return 'VALID'; -}; +import { ALPHABET, NUMBERS } from '../../../utils/constants'; export const isSelectCardType = (str: string) => { return str.length === 0 ? 'INVALID' : 'VALID'; }; -export const isValidExpiredYearFormat = (str: string) => { - const strToNum = +str; - return Number.isInteger(strToNum) && strToNum > 0 ? 'VALID' : 'INVALID'; -}; - export const isValidSecurityCode = (str: string) => { const strToArr = str.split(''); return str.length === 3 && strToArr.every((element) => NUMBERS.includes(element)) diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 110397180d..981151de03 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -12,15 +12,15 @@ import useSelectCardCompany from '../../hooks/useSelectCardType'; import { isSelectCardType, isValidCardNumber, - isValidExpiredMonthFormat, - isValidExpiredYearFormat, isValidOwnerName, isValidPassword, isValidSecurityCode, } from './domain/validation'; import './index.css'; +import useValidExpireDate from '../../hooks/useValidExpireDate'; const AddCardPage = () => { + const { isValidExpiredMonthFormat, isValidExpiredYearFormat } = useValidExpireDate(); const navigate = useNavigate(); const cardCompany = useSelectCardCompany(isSelectCardType); const cardFirstNumber = useInput(isValidCardNumber); From 2ed3d5c2caac0802f6bd489be184fbf847d80c85 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 21:55:24 +0900 Subject: [PATCH 082/102] =?UTF-8?q?fix:=20=EB=A7=8C=EB=A3=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B2=80=EC=A6=9D=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20import=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stories/ExpireDateInput.stories.tsx | 6 ++---- src/test/hooks/useInput.test.ts | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/stories/ExpireDateInput.stories.tsx b/src/stories/ExpireDateInput.stories.tsx index 3f6bcb228a..3c5a67dc0b 100644 --- a/src/stories/ExpireDateInput.stories.tsx +++ b/src/stories/ExpireDateInput.stories.tsx @@ -3,10 +3,7 @@ import type { Meta } from '@storybook/react'; import ExpireDateInput from '../pages/AddCard/components/ExpireDateInput'; import { APP_WIDTH } from '../utils/constants'; import useInput from '../hooks/useInput'; -import { - isValidExpiredMonthFormat, - isValidExpiredYearFormat, -} from '../pages/AddCard/domain/validation'; +import useValidExpireDate from '../hooks/useValidExpireDate'; export default { title: 'AddCardExpireDateInput', @@ -25,6 +22,7 @@ export default { } as Meta; const AddHook = () => { + const { isValidExpiredMonthFormat, isValidExpiredYearFormat } = useValidExpireDate(); const expireMonth = useInput(isValidExpiredMonthFormat); const expireYear = useInput(isValidExpiredYearFormat); diff --git a/src/test/hooks/useInput.test.ts b/src/test/hooks/useInput.test.ts index eecda819f3..305c79b67b 100644 --- a/src/test/hooks/useInput.test.ts +++ b/src/test/hooks/useInput.test.ts @@ -2,14 +2,13 @@ import { renderHook } from '@testing-library/react'; import useInput from '../../hooks/useInput'; import { isValidCardNumber, - isValidExpiredMonthFormat, - isValidExpiredYearFormat, isValidOwnerName, isValidPassword, isValidSecurityCode, } from '../../pages/AddCard/domain/validation'; import { act } from 'react-dom/test-utils'; import { mockEventTarget } from '../utils/util'; +import useValidExpireDate from '../../hooks/useValidExpireDate'; describe('카드 번호 테스트', () => { it('카드 번호 Hook의 초기 상태는 INIT를 가진다.', () => { @@ -35,11 +34,13 @@ describe('카드 번호 테스트', () => { describe('만료일(년도) 테스트', () => { it('만료일(년도) Hook의 초기 상태는 INIT를 가진다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredYearFormat)); expect(result.current.status).toBe('INIT'); }); it('유효한 만료일(년도) 입력이 들어오면, 만료일(년도)의 상태가 유효한 상태로 변경된다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('23') as React.ChangeEvent); }); @@ -47,7 +48,8 @@ describe('만료일(년도) 테스트', () => { expect(result.current.value).toBe('23'); }); it('유효하지 않은 만료일(년도) 입력이 들어오면, 만료일(년도)의 상태가 유효하지 않는 상태로 변경된다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredYearFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredYearFormat)); act(() => { result.current.onChange(mockEventTarget('a8') as React.ChangeEvent); }); @@ -57,11 +59,13 @@ describe('만료일(년도) 테스트', () => { describe('만료일(월) 테스트', () => { it('만료일(월) Hook의 초기 상태는 INIT를 가진다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredMonthFormat)); expect(result.current.status).toBe('INIT'); }); it('유효한 만료일(월) 입력이 들어오면, 만료일(월)의 상태가 유효한 상태로 변경된다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredMonthFormat)); act(() => { result.current.onChange(mockEventTarget('12') as React.ChangeEvent); }); @@ -69,7 +73,8 @@ describe('만료일(월) 테스트', () => { expect(result.current.value).toBe('12'); }); it('유효하지 않은 만료일(월) 입력이 들어오면, 만료일(월)의 상태가 유효하지 않는 상태로 변경된다.', () => { - const { result } = renderHook(() => useInput(isValidExpiredMonthFormat)); + const { result: valid } = renderHook(() => useValidExpireDate()); + const { result } = renderHook(() => useInput(valid.current.isValidExpiredMonthFormat)); act(() => { result.current.onChange(mockEventTarget('13') as React.ChangeEvent); }); From c0819217f2d6ed912b49714da6ecb95c52364fd1 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 21:55:44 +0900 Subject: [PATCH 083/102] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=EB=A1=9C?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useValidExpireDate.ts | 5 +---- src/utils/constants.ts | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hooks/useValidExpireDate.ts b/src/hooks/useValidExpireDate.ts index 5cb0721c51..88b4773c15 100644 --- a/src/hooks/useValidExpireDate.ts +++ b/src/hooks/useValidExpireDate.ts @@ -1,9 +1,6 @@ import { useState } from 'react'; -import { MONTH_DATA } from '../utils/constants'; -const YEAR = new Date().getFullYear(); -const YEAR_LAST = YEAR.toString().slice(2, 4); -const MONTH = new Date().getMonth() + 1; +import { MONTH, MONTH_DATA, YEAR_LAST } from '../utils/constants'; const useValidExpireDate = () => { const [isCurrentYear, setIsCurrentYear] = useState(false); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index bb161b2f5c..fd40b2e833 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -97,3 +97,7 @@ export const CARD_COMPANYS: CardCompany[] = [ '하나카드', '국민카드', ]; + +export const YEAR = new Date().getFullYear(); +export const YEAR_LAST = YEAR.toString().slice(2, 4); +export const MONTH = new Date().getMonth() + 1; From cd4b984824af6f6d059ab9e843abeb47eeb3c907 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:03:09 +0900 Subject: [PATCH 084/102] =?UTF-8?q?refactor:=20=EB=A9=94=EB=AA=A8=EC=9D=B4?= =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9=20(React.memo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ErrorMessage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ErrorMessage.tsx b/src/components/ErrorMessage.tsx index 94a37422af..87dab4f624 100644 --- a/src/components/ErrorMessage.tsx +++ b/src/components/ErrorMessage.tsx @@ -1,3 +1,5 @@ +import React from 'react'; + import { getErrorMessage } from '../pages/AddCard/domain/domain'; import { ErrorMessageProps } from '../type'; import './ErrorMessage.css'; @@ -6,4 +8,4 @@ const ErrorMessage = ({ inputType, status }: ErrorMessageProps) => { return
    {getErrorMessage(inputType, status)}
    ; }; -export default ErrorMessage; +export default React.memo(ErrorMessage); From 39bddef862ba56fa38a503540acbb8c52d39df71 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:16:45 +0900 Subject: [PATCH 085/102] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardAlias/index.tsx | 23 +++-------------------- src/pages/CardAlias/registerCardAlias.ts | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 src/pages/CardAlias/registerCardAlias.ts diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 85eaa57b6a..c6c3e38c09 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -1,36 +1,19 @@ import { useNavigate } from 'react-router-dom'; -import { CardNumber, CardType } from '../../type'; import Card from '../../components/Card'; import useInput from '../../hooks/useInput'; import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; import { isValidCardAlias } from '../AddCard/domain/validation'; -import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; import './index.css'; - -const registerCardAlias = (alias: string, cardNumber: CardNumber) => { - const registerdCardNumber = getSerialNumber(cardNumber); - const cardList = fetchLocalStorage('cardList', '[]'); - const currentCard = cardList.find( - (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber - ); - const addedAliasCard = { alias, ...currentCard }; - const restCardList = cardList.filter( - (card: CardType) => getSerialNumber(card.cardNumber) !== registerdCardNumber - ); - - const newCardList = [...restCardList, addedAliasCard]; - - localStorage.setItem('cardList', JSON.stringify(newCardList)); -}; +import registerCardAlias from './registerCardAlias'; const CardAliasPage = () => { const navigate = useNavigate(); const { currentCard } = useCurrentCardContext(); const { value, onChange } = useInput(isValidCardAlias); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); - const onClick = () => { + const onConfirmButtonClick = () => { registerCardAlias(value, currentCard.cardNumber); setIsAccessAliasPage(false); navigate('/'); @@ -50,7 +33,7 @@ const CardAliasPage = () => { />
    - +
    ); diff --git a/src/pages/CardAlias/registerCardAlias.ts b/src/pages/CardAlias/registerCardAlias.ts new file mode 100644 index 0000000000..17028911a7 --- /dev/null +++ b/src/pages/CardAlias/registerCardAlias.ts @@ -0,0 +1,23 @@ +import { CardNumber, CardType } from '../../type'; +import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; + +// TODO: 도메인 분리하기 ! +const registerCardAlias = (alias: string, cardNumber: CardNumber) => { + const registerdCardNumber = getSerialNumber(cardNumber); + const cardList = fetchLocalStorage('cardList', '[]'); + const currentCard = cardList.find( + (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber + ); + + const addedAliasCard = { alias, ...currentCard }; + + const restCardList = cardList.filter( + (card: CardType) => getSerialNumber(card.cardNumber) !== registerdCardNumber + ); + + const newCardList = [...restCardList, addedAliasCard]; + + localStorage.setItem('cardList', JSON.stringify(newCardList)); +}; + +export default registerCardAlias; From db7ad7539c18c55fecf7eeef999179590936f7dc Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:18:05 +0900 Subject: [PATCH 086/102] =?UTF-8?q?refactor:=20deprecated=20=EB=90=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/util.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/utils/util.ts b/src/utils/util.ts index 96e6b097ad..622bea2955 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -1,30 +1,3 @@ -import { ALPHABET, NUMBERS } from './constants'; - -// TODO: 구체화 하기 -export const formatExpireDate = (expireDate: string) => { - return expireDate; -}; - -export const handleNumberInput = (data: string) => { - if (!NUMBERS.includes(data[data.length - 1])) { - data = data.slice(0, -1); - } - - return data; -}; - -export const isAlphabetInput = (data: string) => { - return ALPHABET.includes(data); -}; - -export const isNumberInput = (data: string) => { - return NUMBERS.includes(data); -}; - export const changeNumberToMask = (data: string) => { return '·'.repeat(data.length); }; - -export const stringToUpperCase = (data: string) => { - return data.toUpperCase(); -}; From 864e185e71d40b7ed39ebe3989552f8aaf552985 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:31:23 +0900 Subject: [PATCH 087/102] =?UTF-8?q?refactor:=20=EB=84=A4=EC=9D=B4=EB=B0=8D?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/{applicationUtil.ts => applicationStorage.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/utils/{applicationUtil.ts => applicationStorage.ts} (100%) diff --git a/src/utils/applicationUtil.ts b/src/utils/applicationStorage.ts similarity index 100% rename from src/utils/applicationUtil.ts rename to src/utils/applicationStorage.ts From e898cc4e81c34eeaa8f2988f27119477b4b6b22c Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:31:59 +0900 Subject: [PATCH 088/102] =?UTF-8?q?refactor:=20=ED=91=9C=ED=98=84=EC=9D=B4?= =?UTF-8?q?=20=EC=A4=91=EC=9D=98=EC=A0=81=EC=9D=B8=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCard/components/CardNameBottomSheet.tsx | 13 +++++++------ src/pages/AddCard/components/CardSelectButton.tsx | 4 ++-- src/type.d.ts | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pages/AddCard/components/CardNameBottomSheet.tsx b/src/pages/AddCard/components/CardNameBottomSheet.tsx index 905efd71a8..740b29aa18 100644 --- a/src/pages/AddCard/components/CardNameBottomSheet.tsx +++ b/src/pages/AddCard/components/CardNameBottomSheet.tsx @@ -1,7 +1,7 @@ import BottomSheet from '../../../components/BottomSheet'; import CardSelectButton from './CardSelectButton'; import './CardNameBottomSheet.css'; -import { CardNameBottomSheetProps } from '../../../type'; +import { CardCompany, CardNameBottomSheetProps } from '../../../type'; import { CARD_COMPANYS } from '../../../utils/constants'; const CardNameBottomSheet = ({ @@ -9,7 +9,11 @@ const CardNameBottomSheet = ({ onToggleOpen, setCardCompany, }: CardNameBottomSheetProps) => { - // TODO: onClick 함수 분리 + const onCardSelectButtonClick = (cardCompany:CardCompany) => { + onToggleOpen(); + setCardCompany(cardCompany); + } + return (
    @@ -17,10 +21,7 @@ const CardNameBottomSheet = ({ { - onToggleOpen(); - setCardCompany(cardCompany); - }} + onCardSelectButtonClick={() => onCardSelectButtonClick(cardCompany)} /> ))}
    diff --git a/src/pages/AddCard/components/CardSelectButton.tsx b/src/pages/AddCard/components/CardSelectButton.tsx index e916e86f5a..89413cd236 100644 --- a/src/pages/AddCard/components/CardSelectButton.tsx +++ b/src/pages/AddCard/components/CardSelectButton.tsx @@ -2,9 +2,9 @@ import { CardSelectButtonProps } from '../../../type'; import { CARD_NAME_IMAGE_SRCS } from '../../../utils/constants'; import './CardSelectButton.css'; -const CardSelectButton = ({ onClick, company }: CardSelectButtonProps) => { +const CardSelectButton = ({ onCardSelectButtonClick, company }: CardSelectButtonProps) => { return ( - diff --git a/src/type.d.ts b/src/type.d.ts index bc075c456f..b9f6c57925 100644 --- a/src/type.d.ts +++ b/src/type.d.ts @@ -120,5 +120,5 @@ export type CardNameBottomSheetProps = BottomSheetProps & { export type CardSelectButtonProps = { company: CardCompany; - onClick: (e: React.MouseEvent) => void; + onCardSelectButtonClick: () => void; }; From cf114ef1b85325b6adb8259b950d0e904c311911 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Tue, 2 May 2023 22:34:50 +0900 Subject: [PATCH 089/102] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=A3=BC=EC=84=9D=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardAlias/registerCardAlias.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/CardAlias/registerCardAlias.ts b/src/pages/CardAlias/registerCardAlias.ts index 17028911a7..22e54e3a8e 100644 --- a/src/pages/CardAlias/registerCardAlias.ts +++ b/src/pages/CardAlias/registerCardAlias.ts @@ -1,7 +1,6 @@ import { CardNumber, CardType } from '../../type'; -import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationUtil'; +import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationStorage'; -// TODO: 도메인 분리하기 ! const registerCardAlias = (alias: string, cardNumber: CardNumber) => { const registerdCardNumber = getSerialNumber(cardNumber); const cardList = fetchLocalStorage('cardList', '[]'); From df9eb96807606eaca7a89cccbeb504e15bff54f6 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 3 May 2023 13:17:37 +0900 Subject: [PATCH 090/102] =?UTF-8?q?refactor:=20=EC=84=A0=EC=96=B8=EB=B6=80?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/AddCardForm.tsx | 65 ++++---------------- src/pages/AddCard/domain/domain.ts | 38 +++++++++++- src/pages/CardList/index.tsx | 22 +++---- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 2350266a64..1f51a252a2 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import type { AddCardFormProps, CardType } from '../../../type'; -import { postLocalStorage as submitCard } from '../../../utils/applicationUtil'; +import { postLocalStorage as submitCard } from '../../../utils/applicationStorage'; import ExpireDateInput from './ExpireDateInput'; import OwnerInput from './OwnerInput'; import SecurityCodeInput from './SecurityCodeInput'; @@ -12,55 +12,17 @@ import CardNumberInput from './CardNumberInput'; import { useCurrentCardContext } from '../../../context/CurrentCardProvider'; import { useIsAccessAliasPageContext } from '../../../context/IsAccessAliasPageProvider'; import useTotalStatus from '../../../hooks/useTotalStatus'; +import { getSubmitData, getTotalStatus } from '../domain/domain'; -const AddCardForm = ({ - cardCompany, - cardFirstNumber, - cardSecondNumber, - cardThirdNumber, - cardFourthNumber, - expireMonth, - expireYear, - cardOwner, - securityCode, - cardPassword1, - cardPassword2, -}: AddCardFormProps) => { +const AddCardForm = (props: AddCardFormProps) => { const navigate = useNavigate(); const { setCurrentCard } = useCurrentCardContext(); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); - const isActive = useTotalStatus([ - cardCompany.status, - cardFirstNumber.status, - cardSecondNumber.status, - cardThirdNumber.status, - cardFourthNumber.status, - cardOwner.status, - expireMonth.status, - expireYear.status, - securityCode.status, - cardPassword1.status, - cardPassword2.status, - ]); + const isActive = useTotalStatus(getTotalStatus(props)); + const onSubmit = (e: React.FormEvent) => { e.preventDefault(); - const submitData: Omit = { - cardCompany: cardCompany.value, - cardNumber: { - first: cardFirstNumber.value, - second: cardSecondNumber.value, - third: cardThirdNumber.value, - fourth: cardFourthNumber.value, - }, - cardOwner: cardOwner.value, - expireMonth: expireMonth.value, - expireYear: expireYear.value, - securityCode: securityCode.value, - cardPassword: { - first: cardPassword1.value, - second: cardPassword2.value, - }, - }; + const submitData: Omit = getSubmitData(props); try { submitCard(submitData); setCurrentCard(submitData); @@ -74,16 +36,11 @@ const AddCardForm = ({ return (
    - - - - - + + + + +
    {isActive && }
    ); diff --git a/src/pages/AddCard/domain/domain.ts b/src/pages/AddCard/domain/domain.ts index a52a8b72dc..5fdca1eaf6 100644 --- a/src/pages/AddCard/domain/domain.ts +++ b/src/pages/AddCard/domain/domain.ts @@ -1,4 +1,4 @@ -import { CardInfoInput, InputStatus } from '../../../type'; +import { AddCardFormProps, CardInfoInput, InputStatus } from '../../../type'; import { INVALID_MESSAGE } from '../../../utils/constants'; export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { @@ -10,3 +10,39 @@ export const calcMultipleStatus = (arr: InputStatus[]): InputStatus => { export const getErrorMessage = (inputType: CardInfoInput, status: InputStatus) => { return INVALID_MESSAGE[inputType][status]; }; + +export const getTotalStatus = (formInputs: AddCardFormProps) => { + return Object.values(formInputs).map((input) => input.status); +}; + +export const getSubmitData = ({ + cardCompany, + cardFirstNumber, + cardSecondNumber, + cardThirdNumber, + cardFourthNumber, + cardOwner, + expireMonth, + expireYear, + securityCode, + cardPassword1, + cardPassword2, +}: AddCardFormProps) => { + return { + cardCompany: cardCompany.value, + cardNumber: { + first: cardFirstNumber.value, + second: cardSecondNumber.value, + third: cardThirdNumber.value, + fourth: cardFourthNumber.value, + }, + cardOwner: cardOwner.value, + expireMonth: expireMonth.value, + expireYear: expireYear.value, + securityCode: securityCode.value, + cardPassword: { + first: cardPassword1.value, + second: cardPassword2.value, + }, + }; +}; diff --git a/src/pages/CardList/index.tsx b/src/pages/CardList/index.tsx index 00b1767fb2..9f6f280be3 100644 --- a/src/pages/CardList/index.tsx +++ b/src/pages/CardList/index.tsx @@ -4,7 +4,7 @@ import type { CardType } from '../../type'; import Card from '../../components/Card'; import Header from '../../components/Header'; import './index.css'; -import { fetchLocalStorage } from '../../utils/applicationUtil'; +import { fetchLocalStorage } from '../../utils/applicationStorage'; const CardListPage = () => { const cardList = fetchLocalStorage('cardList', '[]'); @@ -23,19 +23,17 @@ const CardListPage = () => { {cardList.length === 0 ? ( 새로운 카드를 등록해주세요. ) : ( - cardList.map((card: CardType) => ( -
    + cardList.map(({ id, cardNumber, alias, cardCompany, ...rest }: CardType) => ( +
    -

    {card.alias || card.cardCompany}

    +

    {alias || cardCompany}

    )) )} From 191d8fdadd933c702aaf2c5c28503575a2561fec Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 3 May 2023 13:17:49 +0900 Subject: [PATCH 091/102] =?UTF-8?q?feat:=20createPortal=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index e7261679bb..5070fe25a1 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -2,16 +2,20 @@ import { PropsWithChildren } from 'react'; import './BottomSheet.css'; import { BottomSheetProps } from '../type'; +import { createPortal } from 'react-dom'; const BottomSheet = ({ children, isOpen, onToggleOpen }: PropsWithChildren) => { return ( <> - {isOpen && ( -
    -
    -
    {children}
    -
    - )} + {isOpen && + createPortal( +
    +
    +
    {children}
    +
    , + document.body + )} + ) ); }; From b1941713d0003dc74d2b69aefb929d879e64d745 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 3 May 2023 13:44:03 +0900 Subject: [PATCH 092/102] =?UTF-8?q?refactor:=20=EB=94=94=EB=A0=89=ED=84=B0?= =?UTF-8?q?=EB=A6=AC=20=EA=B2=BD=EB=A1=9C=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Card.tsx | 2 +- src/components/{ => formatting}/format.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/{ => formatting}/format.ts (100%) diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 507bab0284..d99d3e52a6 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,6 +1,6 @@ import type { CardProps } from '../type'; import { changeNumberToMask } from '../utils/util'; -import { formatExpireDate } from './format'; +import { formatExpireDate } from './formatting/format'; import { CARD_COMPANY_ENG } from '../utils/constants'; import './Card.css'; diff --git a/src/components/format.ts b/src/components/formatting/format.ts similarity index 100% rename from src/components/format.ts rename to src/components/formatting/format.ts From 787c2c4dc26c0ee87af98fe305855ad1a51b6f88 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 3 May 2023 14:54:33 +0900 Subject: [PATCH 093/102] =?UTF-8?q?refactor:=20context=20api=20=EB=8D=94?= =?UTF-8?q?=20=EC=A2=81=EC=9D=80=20=EB=B2=94=EC=9C=84=EB=A1=9C=20=EC=B6=95?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/index.tsx | 37 ++++++++++++++++++++++--------------- src/router/AppRouter.tsx | 8 +------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 981151de03..c278884ff8 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; @@ -18,6 +18,8 @@ import { } from './domain/validation'; import './index.css'; import useValidExpireDate from '../../hooks/useValidExpireDate'; +import { CurrentCardProvider } from '../../context/CurrentCardProvider'; +import { IsAccessAliasPageProvider } from '../../context/IsAccessAliasPageProvider'; const AddCardPage = () => { const { isValidExpiredMonthFormat, isValidExpiredYearFormat } = useValidExpireDate(); @@ -60,19 +62,24 @@ const AddCardPage = () => { expireYear={expireYear.value} onClick={toggleOpen} /> - + + + + + +
    { ); }; -export default AddCardPage; +export default React.memo(AddCardPage); diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 6fc7d45f25..a8026341e9 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -14,13 +14,7 @@ const router = createBrowserRouter([ }, { path: '/add', - element: ( - - - - - - ), + element: , }, { path: '/alias', From fb98283e9a8a64c4e5322ddc4452335125bcd64a Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Wed, 3 May 2023 15:52:56 +0900 Subject: [PATCH 094/102] =?UTF-8?q?feat:=20=EB=A9=94=EB=AA=A8=EC=9D=B4?= =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AddCard/components/CardNumberInput.tsx | 24 +++++++++++++------ .../AddCard/components/ExpireDateInput.tsx | 6 ++++- .../AddCard/components/PasswordInput.tsx | 11 +++++---- src/pages/AddCard/index.tsx | 1 + 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/pages/AddCard/components/CardNumberInput.tsx b/src/pages/AddCard/components/CardNumberInput.tsx index ab64272e91..8b11dbe3ee 100644 --- a/src/pages/AddCard/components/CardNumberInput.tsx +++ b/src/pages/AddCard/components/CardNumberInput.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { CardNumberInputProps } from '../../../type'; import './CardNumberInput.css'; @@ -11,12 +11,22 @@ const CardNumberInput = ({ cardThirdNumber, cardFourthNumber, }: CardNumberInputProps) => { - const status = calcMultipleStatus([ - cardFirstNumber.status, - cardSecondNumber.status, - cardThirdNumber.status, - cardFourthNumber.status, - ]); + const status = useMemo( + () => + calcMultipleStatus([ + cardFirstNumber.status, + cardSecondNumber.status, + cardThirdNumber.status, + cardFourthNumber.status, + ]), + [ + cardFirstNumber.status, + cardSecondNumber.status, + cardThirdNumber.status, + cardFourthNumber.status, + ] + ); + return ( 카드 번호 diff --git a/src/pages/AddCard/components/ExpireDateInput.tsx b/src/pages/AddCard/components/ExpireDateInput.tsx index 948267532f..65f331959c 100644 --- a/src/pages/AddCard/components/ExpireDateInput.tsx +++ b/src/pages/AddCard/components/ExpireDateInput.tsx @@ -1,10 +1,14 @@ +import { useMemo } from 'react'; import InputContainer from '../../../components/InputContainer'; import { ExpireDateInputProps } from '../../../type'; import { calcMultipleStatus } from '../domain/domain'; import './ExpireDateInput.css'; const ExpireDateInput = ({ expireMonth, expireYear }: ExpireDateInputProps) => { - const status = calcMultipleStatus([expireMonth.status, expireYear.status]); + const status = useMemo( + () => calcMultipleStatus([expireMonth.status, expireYear.status]), + [expireMonth.status, expireYear.status] + ); return ( 만료일 diff --git a/src/pages/AddCard/components/PasswordInput.tsx b/src/pages/AddCard/components/PasswordInput.tsx index 97ef5740f4..8d35434f1e 100644 --- a/src/pages/AddCard/components/PasswordInput.tsx +++ b/src/pages/AddCard/components/PasswordInput.tsx @@ -3,14 +3,15 @@ import InputContainer from '../../../components/InputContainer'; import { calcMultipleStatus } from '../domain/domain'; import passwordDotImg from '../../../asset/password_dot.png'; import './PasswordInput.css'; +import { useMemo } from 'react'; const PasswordInput = ({ cardPassword1, cardPassword2 }: PasswordInputProps) => { + const status = useMemo( + () => calcMultipleStatus([cardPassword1.status, cardPassword2.status]), + [cardPassword1.status, cardPassword2.status] + ); return ( - + 카드 비밀번호
    { const { isValidExpiredMonthFormat, isValidExpiredYearFormat } = useValidExpireDate(); const navigate = useNavigate(); + // TODO: 좀 더 선언적으로 해볼까? const cardCompany = useSelectCardCompany(isSelectCardType); const cardFirstNumber = useInput(isValidCardNumber); const cardSecondNumber = useInput(isValidCardNumber); From 5970d7fd9f5a875ba0f45e4665c5ad4843adc826 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 14:13:19 +0900 Subject: [PATCH 095/102] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=AC=B8=EC=9E=90=EA=B0=80=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EB=8A=94=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BottomSheet.tsx | 1 - src/pages/AddCard/index.tsx | 31 +++++++++++++------------------ src/pages/CardAlias/index.tsx | 1 + src/router/AppRouter.tsx | 8 +++++++- src/router/PrivateRoute.tsx | 1 + 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/components/BottomSheet.tsx b/src/components/BottomSheet.tsx index 5070fe25a1..412f826456 100644 --- a/src/components/BottomSheet.tsx +++ b/src/components/BottomSheet.tsx @@ -15,7 +15,6 @@ const BottomSheet = ({ children, isOpen, onToggleOpen }: PropsWithChildren, document.body )} - ) ); }; diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index 6805ea5ed6..cebfda68bb 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -63,24 +63,19 @@ const AddCardPage = () => { expireYear={expireYear.value} onClick={toggleOpen} /> - - - - - - +
    { const { currentCard } = useCurrentCardContext(); const { value, onChange } = useInput(isValidCardAlias); const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); + const onConfirmButtonClick = () => { registerCardAlias(value, currentCard.cardNumber); setIsAccessAliasPage(false); diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index a8026341e9..6fc7d45f25 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -14,7 +14,13 @@ const router = createBrowserRouter([ }, { path: '/add', - element: , + element: ( + + + + + + ), }, { path: '/alias', diff --git a/src/router/PrivateRoute.tsx b/src/router/PrivateRoute.tsx index 191301e7c0..44beb0cf3d 100644 --- a/src/router/PrivateRoute.tsx +++ b/src/router/PrivateRoute.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { useIsAccessAliasPageContext } from '../context/IsAccessAliasPageProvider'; +// TODO: 이녀석이 진짜 필요할까? const PrivateRoute = ({ children }: PropsWithChildren) => { const { isAccessAliasPage } = useIsAccessAliasPageContext(); const navigate = useNavigate(); From ba879883917566f5cd85fcd352e1263c61a7a3cf Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 14:19:41 +0900 Subject: [PATCH 096/102] =?UTF-8?q?refactor:=20=EC=9D=91=EC=A7=91=EB=8F=84?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/CardAlias/index.tsx | 2 +- src/pages/CardAlias/registerCardAlias.ts | 22 ---------------------- src/utils/applicationStorage.ts | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 23 deletions(-) delete mode 100644 src/pages/CardAlias/registerCardAlias.ts diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 2d974aa7a5..ccfa722594 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -6,7 +6,7 @@ import { useCurrentCardContext } from '../../context/CurrentCardProvider'; import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; import { isValidCardAlias } from '../AddCard/domain/validation'; import './index.css'; -import registerCardAlias from './registerCardAlias'; +import { registerCardAlias } from '../../utils/applicationStorage'; const CardAliasPage = () => { const navigate = useNavigate(); diff --git a/src/pages/CardAlias/registerCardAlias.ts b/src/pages/CardAlias/registerCardAlias.ts deleted file mode 100644 index 22e54e3a8e..0000000000 --- a/src/pages/CardAlias/registerCardAlias.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CardNumber, CardType } from '../../type'; -import { fetchLocalStorage, getSerialNumber } from '../../utils/applicationStorage'; - -const registerCardAlias = (alias: string, cardNumber: CardNumber) => { - const registerdCardNumber = getSerialNumber(cardNumber); - const cardList = fetchLocalStorage('cardList', '[]'); - const currentCard = cardList.find( - (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber - ); - - const addedAliasCard = { alias, ...currentCard }; - - const restCardList = cardList.filter( - (card: CardType) => getSerialNumber(card.cardNumber) !== registerdCardNumber - ); - - const newCardList = [...restCardList, addedAliasCard]; - - localStorage.setItem('cardList', JSON.stringify(newCardList)); -}; - -export default registerCardAlias; diff --git a/src/utils/applicationStorage.ts b/src/utils/applicationStorage.ts index f362152b82..cf3f53cb0b 100644 --- a/src/utils/applicationStorage.ts +++ b/src/utils/applicationStorage.ts @@ -37,3 +37,21 @@ export const postLocalStorage = (data: Omit) => { export const fetchLocalStorage = (key: string, initial = '') => { return JSON.parse(localStorage.getItem('cardList') ?? initial); }; + +export const registerCardAlias = (alias: string, cardNumber: CardNumber) => { + const registerdCardNumber = getSerialNumber(cardNumber); + const cardList = fetchLocalStorage('cardList', '[]'); + const currentCard = cardList.find( + (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber + ); + + const addedAliasCard = { alias, ...currentCard }; + + const restCardList = cardList.filter( + (card: CardType) => getSerialNumber(card.cardNumber) !== registerdCardNumber + ); + + const newCardList = [...restCardList, addedAliasCard]; + + localStorage.setItem('cardList', JSON.stringify(newCardList)); +}; From 92f44d85a27ee9eaad536d0009cf87335515267e Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 14:51:00 +0900 Subject: [PATCH 097/102] =?UTF-8?q?fix:=20context=20api=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/context/CurrentCardProvider.tsx | 24 -------------------- src/context/IsAccessAliasPageProvider.tsx | 23 ------------------- src/pages/AddCard/components/AddCardForm.tsx | 6 ----- src/pages/AddCard/index.tsx | 2 -- src/pages/CardAlias/index.tsx | 17 +++++++++----- src/router/AppRouter.tsx | 21 ++--------------- src/router/PrivateRoute.tsx | 20 ---------------- 7 files changed, 13 insertions(+), 100 deletions(-) delete mode 100644 src/context/CurrentCardProvider.tsx delete mode 100644 src/context/IsAccessAliasPageProvider.tsx delete mode 100644 src/router/PrivateRoute.tsx diff --git a/src/context/CurrentCardProvider.tsx b/src/context/CurrentCardProvider.tsx deleted file mode 100644 index 7f0c02a1a3..0000000000 --- a/src/context/CurrentCardProvider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { PropsWithChildren, createContext, useContext, useState } from 'react'; - -import { CardType, CurrentCard } from '../type'; -import { CARD_INIT } from '../utils/constants'; - -const CurrentCardContext = createContext(null); - -export const CurrentCardProvider = ({ children }: PropsWithChildren) => { - const [currentCard, setCurrentCard] = useState>(CARD_INIT); - - return ( - - {children} - - ); -}; - -export const useCurrentCardContext = () => { - const currentCardContext = useContext(CurrentCardContext); - if (currentCardContext === null) { - throw new Error('Context가 존재하지 않습니다.'); - } - return currentCardContext; -}; diff --git a/src/context/IsAccessAliasPageProvider.tsx b/src/context/IsAccessAliasPageProvider.tsx deleted file mode 100644 index 068b32bd01..0000000000 --- a/src/context/IsAccessAliasPageProvider.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { PropsWithChildren, createContext, useContext, useState } from 'react'; - -import { IsAccessAliasPage } from '../type'; - -const IsAccessAliasPageContext = createContext(null); - -export const IsAccessAliasPageProvider = ({ children }: PropsWithChildren) => { - const [isAccessAliasPage, setIsAccessAliasPage] = useState(false); - - return ( - - {children} - - ); -}; - -export const useIsAccessAliasPageContext = () => { - const isAccessAliasPageContext = useContext(IsAccessAliasPageContext); - if (isAccessAliasPageContext === null) { - throw new Error('Context가 존재하지 않습니다.'); - } - return isAccessAliasPageContext; -}; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 1f51a252a2..8521305602 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -9,15 +9,11 @@ import SecurityCodeInput from './SecurityCodeInput'; import PasswordInput from './PasswordInput'; import './AddCardForm.css'; import CardNumberInput from './CardNumberInput'; -import { useCurrentCardContext } from '../../../context/CurrentCardProvider'; -import { useIsAccessAliasPageContext } from '../../../context/IsAccessAliasPageProvider'; import useTotalStatus from '../../../hooks/useTotalStatus'; import { getSubmitData, getTotalStatus } from '../domain/domain'; const AddCardForm = (props: AddCardFormProps) => { const navigate = useNavigate(); - const { setCurrentCard } = useCurrentCardContext(); - const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); const isActive = useTotalStatus(getTotalStatus(props)); const onSubmit = (e: React.FormEvent) => { @@ -25,8 +21,6 @@ const AddCardForm = (props: AddCardFormProps) => { const submitData: Omit = getSubmitData(props); try { submitCard(submitData); - setCurrentCard(submitData); - setIsAccessAliasPage(true); navigate('/alias'); } catch (error) { alert('중복된 카드 입니다.'); diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index cebfda68bb..e75a48e7aa 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -18,8 +18,6 @@ import { } from './domain/validation'; import './index.css'; import useValidExpireDate from '../../hooks/useValidExpireDate'; -import { CurrentCardProvider } from '../../context/CurrentCardProvider'; -import { IsAccessAliasPageProvider } from '../../context/IsAccessAliasPageProvider'; const AddCardPage = () => { const { isValidExpiredMonthFormat, isValidExpiredYearFormat } = useValidExpireDate(); diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index ccfa722594..7f0270deae 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -2,23 +2,28 @@ import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; import useInput from '../../hooks/useInput'; -import { useCurrentCardContext } from '../../context/CurrentCardProvider'; -import { useIsAccessAliasPageContext } from '../../context/IsAccessAliasPageProvider'; import { isValidCardAlias } from '../AddCard/domain/validation'; +import { fetchLocalStorage, registerCardAlias } from '../../utils/applicationStorage'; import './index.css'; -import { registerCardAlias } from '../../utils/applicationStorage'; +import { useEffect } from 'react'; const CardAliasPage = () => { const navigate = useNavigate(); - const { currentCard } = useCurrentCardContext(); const { value, onChange } = useInput(isValidCardAlias); - const { setIsAccessAliasPage } = useIsAccessAliasPageContext(); + const cardList = fetchLocalStorage('cardList', '[]'); + const currentCard = cardList[cardList.length - 1]; + useEffect(() => { + if (cardList.length === 0 || currentCard.alias) { + console.log(currentCard); + navigate('/'); + } + }, []); const onConfirmButtonClick = () => { registerCardAlias(value, currentCard.cardNumber); - setIsAccessAliasPage(false); navigate('/'); }; + return (

    카드 등록이 완료되었습니다.

    diff --git a/src/router/AppRouter.tsx b/src/router/AppRouter.tsx index 6fc7d45f25..e2666e23db 100644 --- a/src/router/AppRouter.tsx +++ b/src/router/AppRouter.tsx @@ -3,9 +3,6 @@ import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import AddCardPage from '../pages/AddCard'; import CardListPage from '../pages/CardList'; import CardAliasPage from '../pages/CardAlias'; -import { CurrentCardProvider } from '../context/CurrentCardProvider'; -import { IsAccessAliasPageProvider } from '../context/IsAccessAliasPageProvider'; -import PrivateRoute from './PrivateRoute'; const router = createBrowserRouter([ { @@ -14,25 +11,11 @@ const router = createBrowserRouter([ }, { path: '/add', - element: ( - - - - - - ), + element: , }, { path: '/alias', - element: ( - - - - - - - - ), + element: , }, ]); diff --git a/src/router/PrivateRoute.tsx b/src/router/PrivateRoute.tsx deleted file mode 100644 index 44beb0cf3d..0000000000 --- a/src/router/PrivateRoute.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PropsWithChildren, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; - -import { useIsAccessAliasPageContext } from '../context/IsAccessAliasPageProvider'; - -// TODO: 이녀석이 진짜 필요할까? -const PrivateRoute = ({ children }: PropsWithChildren) => { - const { isAccessAliasPage } = useIsAccessAliasPageContext(); - const navigate = useNavigate(); - - useEffect(() => { - if (!isAccessAliasPage) { - navigate('/'); - } - }, [isAccessAliasPage, navigate]); - - return <>{children}; -}; - -export default PrivateRoute; From 4c5dfab71b556631fd8d6b2166c3f4cabbadffe9 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 16:22:58 +0900 Subject: [PATCH 098/102] =?UTF-8?q?refactor:=20useEffect=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useRedirection.ts | 13 +++++++++++++ src/pages/CardAlias/index.tsx | 10 +++------- 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 src/hooks/useRedirection.ts diff --git a/src/hooks/useRedirection.ts b/src/hooks/useRedirection.ts new file mode 100644 index 0000000000..270ce8189b --- /dev/null +++ b/src/hooks/useRedirection.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const useRedirection = (condition: boolean) => { + const navigate = useNavigate(); + useEffect(() => { + if (condition) { + navigate('/'); + } + }, []); +}; + +export default useRedirection; diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 7f0270deae..60eaaa90ac 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -5,7 +5,7 @@ import useInput from '../../hooks/useInput'; import { isValidCardAlias } from '../AddCard/domain/validation'; import { fetchLocalStorage, registerCardAlias } from '../../utils/applicationStorage'; import './index.css'; -import { useEffect } from 'react'; +import useRedirection from '../../hooks/useRedirection'; const CardAliasPage = () => { const navigate = useNavigate(); @@ -13,12 +13,8 @@ const CardAliasPage = () => { const cardList = fetchLocalStorage('cardList', '[]'); const currentCard = cardList[cardList.length - 1]; - useEffect(() => { - if (cardList.length === 0 || currentCard.alias) { - console.log(currentCard); - navigate('/'); - } - }, []); + useRedirection(cardList.length === 0 || currentCard.alias); + const onConfirmButtonClick = () => { registerCardAlias(value, currentCard.cardNumber); navigate('/'); From 2e17e7322400d9e2b1d4db253e92c82ba8ddd067 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 16:55:19 +0900 Subject: [PATCH 099/102] =?UTF-8?q?refactor:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useLocalStorage.ts | 15 +++++++++++++++ src/pages/AddCard/components/AddCardForm.tsx | 6 ++++-- src/utils/applicationStorage.ts | 17 ++++++----------- 3 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/hooks/useLocalStorage.ts diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000000..a4a3fca21a --- /dev/null +++ b/src/hooks/useLocalStorage.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +const useLocalStorage = (key: string, initValue: string) => { + const fetchData = JSON.parse(localStorage.getItem(key) ?? initValue); + const [value, setValue] = useState(fetchData); + + const postLocalStorage = (data: string) => { + setValue(JSON.parse(data)); + localStorage.setItem(key, data); + }; + + return { value, postLocalStorage }; +}; + +export default useLocalStorage; diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index 8521305602..c721eee7e9 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import type { AddCardFormProps, CardType } from '../../../type'; -import { postLocalStorage as submitCard } from '../../../utils/applicationStorage'; +import { newCardList } from '../../../utils/applicationStorage'; import ExpireDateInput from './ExpireDateInput'; import OwnerInput from './OwnerInput'; import SecurityCodeInput from './SecurityCodeInput'; @@ -11,16 +11,18 @@ import './AddCardForm.css'; import CardNumberInput from './CardNumberInput'; import useTotalStatus from '../../../hooks/useTotalStatus'; import { getSubmitData, getTotalStatus } from '../domain/domain'; +import useLocalStorage from '../../../hooks/useLocalStorage'; const AddCardForm = (props: AddCardFormProps) => { const navigate = useNavigate(); const isActive = useTotalStatus(getTotalStatus(props)); + const { value: cardList, postLocalStorage } = useLocalStorage('cardList', '[]'); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); const submitData: Omit = getSubmitData(props); try { - submitCard(submitData); + postLocalStorage(newCardList(cardList, submitData)); navigate('/alias'); } catch (error) { alert('중복된 카드 입니다.'); diff --git a/src/utils/applicationStorage.ts b/src/utils/applicationStorage.ts index cf3f53cb0b..0f46b35841 100644 --- a/src/utils/applicationStorage.ts +++ b/src/utils/applicationStorage.ts @@ -10,17 +10,12 @@ export const getSerialNumber = (card: CardNumber): string => { return result; }; -export const postLocalStorage = (data: Omit) => { - const getData = localStorage.getItem('cardList'); - - if (!getData) { - localStorage.setItem('cardList', JSON.stringify([{ id: 0, ...data }])); - return; +export const newCardList = (recentList: CardType[], data: Omit) => { + if (recentList.length === 0) { + return JSON.stringify([{ id: 0, ...data }]); } - const dataToArr = JSON.parse(getData); - - const sameNumbers = dataToArr.filter((card: Omit) => { + const sameNumbers = recentList.filter((card: Omit) => { const { cardNumber } = card; let cardNumberSerial = getSerialNumber(cardNumber); let fetchCardNumberSerial = getSerialNumber(data.cardNumber); @@ -30,8 +25,8 @@ export const postLocalStorage = (data: Omit) => { }); if (sameNumbers.length > 0) throw new Error('이미 등록된 카드'); - dataToArr.push({ id: dataToArr.length, ...data }); - localStorage.setItem('cardList', JSON.stringify(dataToArr)); + recentList.push({ id: recentList.length, ...data }); + return JSON.stringify(recentList); }; export const fetchLocalStorage = (key: string, initial = '') => { From 3cc3a73f9f307ba4250487b61ef9e5244eba8322 Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 17:05:19 +0900 Subject: [PATCH 100/102] =?UTF-8?q?fix:=20deprecate=EB=90=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/AddCard/components/AddCardForm.tsx | 2 +- src/pages/CardAlias/index.tsx | 7 ++++--- src/pages/CardList/index.tsx | 4 ++-- src/utils/{applicationStorage.ts => card.ts} | 9 ++------- 4 files changed, 9 insertions(+), 13 deletions(-) rename src/utils/{applicationStorage.ts => card.ts} (80%) diff --git a/src/pages/AddCard/components/AddCardForm.tsx b/src/pages/AddCard/components/AddCardForm.tsx index c721eee7e9..1b3317d351 100644 --- a/src/pages/AddCard/components/AddCardForm.tsx +++ b/src/pages/AddCard/components/AddCardForm.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import type { AddCardFormProps, CardType } from '../../../type'; -import { newCardList } from '../../../utils/applicationStorage'; +import { newCardList } from '../../../utils/card'; import ExpireDateInput from './ExpireDateInput'; import OwnerInput from './OwnerInput'; import SecurityCodeInput from './SecurityCodeInput'; diff --git a/src/pages/CardAlias/index.tsx b/src/pages/CardAlias/index.tsx index 60eaaa90ac..f20f6ba6d3 100644 --- a/src/pages/CardAlias/index.tsx +++ b/src/pages/CardAlias/index.tsx @@ -3,20 +3,21 @@ import { useNavigate } from 'react-router-dom'; import Card from '../../components/Card'; import useInput from '../../hooks/useInput'; import { isValidCardAlias } from '../AddCard/domain/validation'; -import { fetchLocalStorage, registerCardAlias } from '../../utils/applicationStorage'; +import { registerCardAlias } from '../../utils/card'; import './index.css'; import useRedirection from '../../hooks/useRedirection'; +import useLocalStorage from '../../hooks/useLocalStorage'; const CardAliasPage = () => { const navigate = useNavigate(); const { value, onChange } = useInput(isValidCardAlias); - const cardList = fetchLocalStorage('cardList', '[]'); + const { value: cardList, postLocalStorage } = useLocalStorage('cardList', '[]'); const currentCard = cardList[cardList.length - 1]; useRedirection(cardList.length === 0 || currentCard.alias); const onConfirmButtonClick = () => { - registerCardAlias(value, currentCard.cardNumber); + postLocalStorage(registerCardAlias(cardList, value, currentCard.cardNumber)); navigate('/'); }; diff --git a/src/pages/CardList/index.tsx b/src/pages/CardList/index.tsx index 9f6f280be3..8268c5187d 100644 --- a/src/pages/CardList/index.tsx +++ b/src/pages/CardList/index.tsx @@ -4,10 +4,10 @@ import type { CardType } from '../../type'; import Card from '../../components/Card'; import Header from '../../components/Header'; import './index.css'; -import { fetchLocalStorage } from '../../utils/applicationStorage'; +import useLocalStorage from '../../hooks/useLocalStorage'; const CardListPage = () => { - const cardList = fetchLocalStorage('cardList', '[]'); + const { value: cardList } = useLocalStorage('cardList', '[]'); const navigate = useNavigate(); const onAddButton = () => { diff --git a/src/utils/applicationStorage.ts b/src/utils/card.ts similarity index 80% rename from src/utils/applicationStorage.ts rename to src/utils/card.ts index 0f46b35841..d5ed7e8b9b 100644 --- a/src/utils/applicationStorage.ts +++ b/src/utils/card.ts @@ -29,13 +29,8 @@ export const newCardList = (recentList: CardType[], data: Omit) return JSON.stringify(recentList); }; -export const fetchLocalStorage = (key: string, initial = '') => { - return JSON.parse(localStorage.getItem('cardList') ?? initial); -}; - -export const registerCardAlias = (alias: string, cardNumber: CardNumber) => { +export const registerCardAlias = (cardList: CardType[], alias: string, cardNumber: CardNumber) => { const registerdCardNumber = getSerialNumber(cardNumber); - const cardList = fetchLocalStorage('cardList', '[]'); const currentCard = cardList.find( (card: CardType) => getSerialNumber(card.cardNumber) === registerdCardNumber ); @@ -48,5 +43,5 @@ export const registerCardAlias = (alias: string, cardNumber: CardNumber) => { const newCardList = [...restCardList, addedAliasCard]; - localStorage.setItem('cardList', JSON.stringify(newCardList)); + return JSON.stringify(newCardList); }; From 5ce6b6896778bcba92cd9e8d4047623a6193fbbe Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 17:11:01 +0900 Subject: [PATCH 101/102] =?UTF-8?q?refactor:=20useEffect=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=B0=B0=EC=97=B4=20=EA=B0=92=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useRedirection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useRedirection.ts b/src/hooks/useRedirection.ts index 270ce8189b..a2c08407f3 100644 --- a/src/hooks/useRedirection.ts +++ b/src/hooks/useRedirection.ts @@ -7,7 +7,7 @@ const useRedirection = (condition: boolean) => { if (condition) { navigate('/'); } - }, []); + }, [navigate, condition]); }; export default useRedirection; From 4b5beeeeca8c88952375b2d14a0f9074ead4db3e Mon Sep 17 00:00:00 2001 From: 2yunseong Date: Thu, 4 May 2023 17:17:35 +0900 Subject: [PATCH 102/102] =?UTF-8?q?fix:=20=EC=86=8C=EC=9C=A0=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=B4=20=EC=84=A0=ED=83=9D=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=9C=BC=EB=A1=9C=20=EC=A0=81=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInput.ts | 4 ++-- src/pages/AddCard/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index ebbd285b52..2e13ccc028 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -2,8 +2,8 @@ import { useState } from 'react'; import { InputStatus } from '../type'; -const useInput = (formatDispatcher: (str: string) => InputStatus, init = '') => { - const [status, setStatus] = useState('INIT'); +const useInput = (formatDispatcher: (str: string) => InputStatus, init: InputStatus = 'INIT') => { + const [status, setStatus] = useState(init); const [value, setValue] = useState(''); const onChange = (e: React.ChangeEvent) => { diff --git a/src/pages/AddCard/index.tsx b/src/pages/AddCard/index.tsx index e75a48e7aa..b70674074b 100644 --- a/src/pages/AddCard/index.tsx +++ b/src/pages/AddCard/index.tsx @@ -33,7 +33,7 @@ const AddCardPage = () => { const expireMonth = useInput(isValidExpiredMonthFormat); const expireYear = useInput(isValidExpiredYearFormat); const securityCode = useInput(isValidSecurityCode); - const cardOwner = useInput(isValidOwnerName); + const cardOwner = useInput(isValidOwnerName, 'VALID'); const { isOpen, toggleOpen } = useBottomSheet(true); const onBackButtonClick = useCallback(() => {