From 44f430532d64e91a0dd01c7c6291708a8ccde5a2 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 13:21:43 +0900 Subject: [PATCH 01/57] =?UTF-8?q?chore=20:=20=EB=A6=AC=EC=95=A1=ED=8A=B8?= =?UTF-8?q?=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EB=8F=94=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 048968a748..6a32d4b5bb 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.23.0", "styled-components": "^6.1.8", "styled-reset": "^4.5.2" }, From 12e57058ff9924beed3fe115af196ac3c3cafbce Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 13:22:25 +0900 Subject: [PATCH 02/57] =?UTF-8?q?docs:=20README.md=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-STEP2.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 README-STEP2.md diff --git a/README-STEP2.md b/README-STEP2.md new file mode 100644 index 0000000000..921f6878a1 --- /dev/null +++ b/README-STEP2.md @@ -0,0 +1,25 @@ +# 동적 입력 UI 구현 + +사용자는 카드 번호를 입력할 때 동적으로 제공되는 입력 UI를 통해 집중적으로 하나의 입력 필드에만 집중할 수 있다. + +# 카드사 선택 + +사용자는 카드사를 선택할 수 있고, 카드사에 따라 미리보기 카드의 색상을 변경한다. + +# CVC 번호 + +CVC 번호를 입력할 때는 미리보기 카드의 뒷면을 시각적으로 보여준다. + +입력은 숫자만 가능하며, 유효하지 않은 값을 입력 시 피드백을 제공한다. + +# 폼 제출 및 상태 관리 + +모든 카드 정보가 정확하게 입력되고 검증되었을 때 확인 버튼이 활성화된다. + +사용자가 입력한 정보 중 하나라도 지우거나 수정하여 유효하지 않게 되면, 확인 버튼은 보이지 않는다. + +# 카드 등록 완료 및 네비게이션 + +확인 버튼을 클릭하면 사용자는 카드 등록 완료 페이지로 이동한다. + +카드 등록 완료 페이지에서는 카드 등록이 성공적으로 완료되었다는 메시지와 함께, 다시 카드 등록 페이지로 돌아갈 수 있는 확인 버튼을 제공한다. From 14bfb388647ff19d2e72588f16f41106a70b4419 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 13:24:57 +0900 Subject: [PATCH 03/57] =?UTF-8?q?chore:=20=ED=83=80=EC=9E=85=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=A0=95=EC=9D=84=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?tsconfig.json=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.json | 11 +++++++---- tsconfig.node.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index a7fc6fbf23..fa313c5e7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,7 @@ "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "Node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, @@ -18,8 +17,12 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"], + "@types/*": ["./@types/*"] + } }, - "include": ["src"], + "include": ["src", "./@types/global.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 97ede7ee6f..5ff5f08afd 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -3,7 +3,7 @@ "composite": true, "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "bundler", + "moduleResolution": "Node", "allowSyntheticDefaultImports": true, "strict": true }, From 1007a9461006e515de37e35884092e6c8cb713dd Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 14:31:06 +0900 Subject: [PATCH 04/57] =?UTF-8?q?refactor:=20step1=EB=95=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=ED=96=88=EB=8D=98=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=93=A4=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=ED=9B=84=20=EC=83=88=EB=A1=AD=EA=B2=8C=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/components/InputSection.tsx | 36 -------- src/components/{ => cards}/CreditCard.tsx | 18 ++-- .../{ => cards}/creditCard.style.ts | 31 ++++--- src/components/inputSection.style.ts | 55 ------------ .../registerSection/RegisterCardNumber.tsx | 54 ++++++++++++ .../RegisterExpirationDate.tsx | 87 +++++++++++++++++++ .../registerSection/RegisterName.tsx | 36 ++++++++ 7 files changed, 211 insertions(+), 106 deletions(-) delete mode 100644 src/components/InputSection.tsx rename src/components/{ => cards}/CreditCard.tsx (80%) rename src/components/{ => cards}/creditCard.style.ts (63%) delete mode 100644 src/components/inputSection.style.ts create mode 100644 src/components/registerSection/RegisterCardNumber.tsx create mode 100644 src/components/registerSection/RegisterExpirationDate.tsx create mode 100644 src/components/registerSection/RegisterName.tsx diff --git a/src/components/InputSection.tsx b/src/components/InputSection.tsx deleted file mode 100644 index df827c0aa7..0000000000 --- a/src/components/InputSection.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { PropsWithChildren } from 'react'; -import * as S from './inputSection.style'; - -type InfoProps = { - title: string; - description?: string; - inputTitle: string; -}; - -export default function InputSection({ - title, - description, - inputTitle, - children, -}: PropsWithChildren) { - return ( - - - - {title} - - {description ? ( - - {description} - - ) : null} - - - - {inputTitle} - - {children} - - - ); -} diff --git a/src/components/CreditCard.tsx b/src/components/cards/CreditCard.tsx similarity index 80% rename from src/components/CreditCard.tsx rename to src/components/cards/CreditCard.tsx index 493da8a349..73cc74c92a 100644 --- a/src/components/CreditCard.tsx +++ b/src/components/cards/CreditCard.tsx @@ -1,6 +1,6 @@ -import { InitialCardNumberState } from '../hooks/useCardNumber'; -import MasterCardImage from '../assets/images/mastercard.png'; -import VisaCardImage from '../assets/images/visa.png'; +import { InitialCardNumberState } from '../../hooks/useCardNumber'; +import MasterCardImage from '../../assets/images/mastercard.png'; +import VisaCardImage from '../../assets/images/visa.png'; import * as S from './creditCard.style'; type CreditCardProps = { @@ -9,17 +9,25 @@ type CreditCardProps = { year: string; name: string; cardBrand: 'none' | 'Visa' | 'MasterCard'; + backgroundColor: string; }; const DATE_SEPARATOR = '/'; -export default function CreditCard({ cardNumbers, month, year, name, cardBrand }: CreditCardProps) { +export default function CreditCard({ + cardNumbers, + month, + year, + name, + cardBrand, + backgroundColor, +}: CreditCardProps) { const cardBrandImageSrc = cardBrand === 'MasterCard' ? MasterCardImage : cardBrand === 'Visa' ? VisaCardImage : ''; return ( - + diff --git a/src/components/creditCard.style.ts b/src/components/cards/creditCard.style.ts similarity index 63% rename from src/components/creditCard.style.ts rename to src/components/cards/creditCard.style.ts index 998091820c..864663a83f 100644 --- a/src/components/creditCard.style.ts +++ b/src/components/cards/creditCard.style.ts @@ -11,15 +11,15 @@ export const Container = styled.div` width: 100%; `; -export const CardContainer = styled.div` - background-color: #333333; - +export const CardContainer = styled.div<{ $backgroundColor: string; $padding: string }>` + background-color: ${(props) => + props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; width: 212px; height: 132px; - - padding: 8px 12px; - + padding: ${(props) => (props.$padding ? props.$padding : '0')}; border-radius: 4px; + position: relative; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); `; export const NumbersContainer = styled.div` @@ -64,16 +64,13 @@ export const CardHeaderContentWrapper = styled.div` export const IcChip = styled.div` width: 100%; height: 100%; - border-radius: 4px; - background-color: #ddcd78; `; export const CardBrand = styled.img` width: 100%; height: 100%; - border-radius: 4px; `; @@ -81,7 +78,21 @@ export const CardInfoWrapper = styled.div` display: flex; flex-direction: column; gap: 8px; - margin-top: 14px; margin-left: 5px; `; + +export const CvcNumberWrapper = styled.div<{ $backgroundColor: string; $padding: string }>` + background-color: ${(props) => + props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; + position: absolute; + bottom: 0; + width: 100%; + display: flex; + justify-content: end; + margin-bottom: 24px; +`; + +export const CvcNumberText = styled(Text)` + margin-right: 16px; +`; diff --git a/src/components/inputSection.style.ts b/src/components/inputSection.style.ts deleted file mode 100644 index d7148999a7..0000000000 --- a/src/components/inputSection.style.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { styled } from 'styled-components'; - -export const Section = styled.section` - display: flex; - flex-direction: column; - gap: 16px; -`; - -export const TitleContainer = styled.div` - display: flex; - align-items: center; - - height: 26px; -`; - -export const Title = styled.h2` - font-size: 1.125rem; - font-weight: 700; -`; - -export const DescriptionContainer = styled.div` - height: 14px; -`; - -export const Description = styled.span` - line-height: 0.875rem; - font-size: 0.5938rem; - font-weight: 400; - color: #8b95a1; -`; - -export const Span = styled.span` - font-weight: 500; - font-size: 0.75rem; - line-height: 0.9375rem; -`; - -export const SpanWrapper = styled.div``; - -export const InputWrapper = styled.div` - display: flex; - gap: 10px; -`; - -export const TitleDescriptionWrapper = styled.div` - display: 'flex'; - flex-direction: 'column'; - gap: '4px'; -`; - -export const TitleChildrenWrapper = styled.div` - display: 'flex'; - flex-direction: 'column'; - gap: '8px'; -`; diff --git a/src/components/registerSection/RegisterCardNumber.tsx b/src/components/registerSection/RegisterCardNumber.tsx new file mode 100644 index 0000000000..405167d1bb --- /dev/null +++ b/src/components/registerSection/RegisterCardNumber.tsx @@ -0,0 +1,54 @@ +import * as S from '../../app.style'; +import InputSection from './InputSection'; +import { CARD_NUMBER } from '../../constants/cardSection'; +import { InitialCardNumberState } from 'types'; +import { Fragment, forwardRef, RefObject } from 'react'; +import Label from '../composables/Label'; +import Input from '../composables/Input'; +import { MAX_LENGTH } from '../../App'; + +type RegisterCardNumberProps = { + cardNumbers: InitialCardNumberState[]; + cardNumbersChangeHandler: (e: React.ChangeEvent, index: number) => void; + refs: RefObject[]; +}; + +const RegisterCardNumber = forwardRef((props, ref) => { + const { cardNumbers, cardNumbersChangeHandler, refs } = props; + + return ( + + + {cardNumbers.map((cardNumber, index) => { + const boundHtmlForId = 'cardNumbers' + index; + return ( + + + ); + })} + + + + {cardNumbers.some((cardNumber) => cardNumber.isError) && CARD_NUMBER.errorMessage} + + + + ); +}); + +export default RegisterCardNumber; diff --git a/src/components/registerSection/RegisterExpirationDate.tsx b/src/components/registerSection/RegisterExpirationDate.tsx new file mode 100644 index 0000000000..70a89d3d07 --- /dev/null +++ b/src/components/registerSection/RegisterExpirationDate.tsx @@ -0,0 +1,87 @@ +import { MAX_LENGTH } from '../../App'; +import * as S from '../../app.style'; +import { EXPIRATION_PERIOD } from '../../constants/cardSection'; +import { Input } from '../composables/input.style'; +import Label from '../composables/Label'; +import InputSection from './InputSection'; +import { forwardRef, RefObject } from 'react'; + +type RegisterExpirationDateProps = { + month: string; + monthChangeHandler: React.ChangeEventHandler; + monthError: boolean; + monthRef: RefObject; + handleMonthKeyDown: (e: React.KeyboardEvent) => void; + handleMonthBlur: React.FocusEventHandler; + year: string; + yearChangeHandler: React.ChangeEventHandler; + yearError: boolean; + yearRef: RefObject; + handleYearKeyDown: (e: React.KeyboardEvent) => void; + handleYearBlur: React.FocusEventHandler; +}; + +const RegisterExpirationDate = forwardRef( + (props, ref) => { + const { + month, + monthError, + monthChangeHandler, + handleMonthKeyDown, + monthRef, + handleMonthBlur, + year, + yearChangeHandler, + yearError, + yearRef, + handleYearKeyDown, + handleYearBlur, + } = props; + + return ( + + + + + + {monthError && yearError ? EXPIRATION_PERIOD.monthErrorMessage : ''} + {!monthError && yearError ? EXPIRATION_PERIOD.yearErrorMessage : ''} + {monthError && !yearError ? EXPIRATION_PERIOD.monthErrorMessage : ''} + + + + ); + }, +); + +export default RegisterExpirationDate; diff --git a/src/components/registerSection/RegisterName.tsx b/src/components/registerSection/RegisterName.tsx new file mode 100644 index 0000000000..816ebf4e02 --- /dev/null +++ b/src/components/registerSection/RegisterName.tsx @@ -0,0 +1,36 @@ +import * as S from '../../app.style'; +import Input from '../composables/Input'; +import Label from '../composables/Label'; +import InputSection from './InputSection'; +import { forwardRef } from 'react'; +import { MAX_LENGTH } from '../../App'; +import { OWNER_NAME } from '../../constants/cardSection'; +import { RegisterStepProps } from 'types'; + +const RegisterName = forwardRef((props, ref) => { + const { onChange, value, onEnter, isError, onBlur } = props; + return ( + + + + + {isError && OWNER_NAME.errorMessage} + + + ); +}); + +export default RegisterName; From 854bc689e783d8522c3ad8fc4a23f3734fbc5988 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 14:31:43 +0900 Subject: [PATCH 05/57] =?UTF-8?q?refactor:=20Input=20forwardRef=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/composables/Input.tsx | 45 +++++++++++------------ src/components/composables/input.style.ts | 4 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/components/composables/Input.tsx b/src/components/composables/Input.tsx index a731f0cc4f..e44d7fd096 100644 --- a/src/components/composables/Input.tsx +++ b/src/components/composables/Input.tsx @@ -1,28 +1,27 @@ -import { InputHTMLAttributes } from 'react'; +import { forwardRef, InputHTMLAttributes } from 'react'; import * as S from './input.style'; interface InputProps extends InputHTMLAttributes { - isError: boolean; + isError?: boolean; } -export default function Input({ - value, - onChange, - type, - placeholder, - id, - isError, - maxLength, -}: InputProps) { - return ( - - ); -} +const Input = forwardRef( + ({ value, onChange, type, placeholder, id, isError, maxLength, onKeyDown, onBlur }, ref) => { + return ( + + ); + }, +); + +export default Input; diff --git a/src/components/composables/input.style.ts b/src/components/composables/input.style.ts index 22fb460e2b..5ad1d62a20 100644 --- a/src/components/composables/input.style.ts +++ b/src/components/composables/input.style.ts @@ -1,6 +1,6 @@ import { styled } from 'styled-components'; -export const Input = styled.input<{ isError: boolean }>` +export const Input = styled.input<{ $isError: boolean }>` border: 1px solid #acacac; padding: 8px; font-size: 0.6875rem; @@ -10,6 +10,6 @@ export const Input = styled.input<{ isError: boolean }>` flex: 1; &:focus { - border: 1px solid ${(props) => (props.isError ? '#ff3d3d' : '#000')}; + border: 1px solid ${(props) => (props.$isError ? '#ff3d3d' : '#000')}; } `; From 9bbcf05b252571e1e17de48487a59afa9e94a8f8 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 14:32:31 +0900 Subject: [PATCH 06/57] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=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/components/composables/Button.tsx | 10 ++++++++++ src/components/composables/button.style.ts | 9 +++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/components/composables/Button.tsx create mode 100644 src/components/composables/button.style.ts diff --git a/src/components/composables/Button.tsx b/src/components/composables/Button.tsx new file mode 100644 index 0000000000..b217643542 --- /dev/null +++ b/src/components/composables/Button.tsx @@ -0,0 +1,10 @@ +import { ButtonHTMLAttributes } from 'react'; +import * as S from './button.style'; + +interface CustomButtonProps extends ButtonHTMLAttributes { + text: string; +} + +export default function Button({ text, ...props }: CustomButtonProps) { + return {text}; +} diff --git a/src/components/composables/button.style.ts b/src/components/composables/button.style.ts new file mode 100644 index 0000000000..cc14c4edf8 --- /dev/null +++ b/src/components/composables/button.style.ts @@ -0,0 +1,9 @@ +import { styled } from 'styled-components'; + +export const Button = styled.button` + color: rgba(243, 243, 243, 1); + background: rgba(51, 51, 51, 1); + cursor: pointer; + width: 100%; + height: 100%; +`; From 86aecc8f7b1ec6957909360b6043cb8d3b15ad49 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 14:46:58 +0900 Subject: [PATCH 07/57] =?UTF-8?q?feat:=20=EB=B6=84=EA=B8=B0=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81,=20=EC=B9=B4=EB=93=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EC=BB=A4=EC=8A=A4=ED=85=80=20?= =?UTF-8?q?=ED=9B=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCardIssuer.ts | 47 ++++++++++++++++++++++++++++++++++++++ src/hooks/useRegister.tsx | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/hooks/useCardIssuer.ts create mode 100644 src/hooks/useRegister.tsx diff --git a/src/hooks/useCardIssuer.ts b/src/hooks/useCardIssuer.ts new file mode 100644 index 0000000000..d5a9a4d3a9 --- /dev/null +++ b/src/hooks/useCardIssuer.ts @@ -0,0 +1,47 @@ +import INITIAL_CARD_ISSUER_INFO from '../constants/initialCardIssuerInfo'; +import { useState, RefObject, useEffect, useRef } from 'react'; + +type UseCardIssuerProps = { + nextStepHandler: () => void; + nextRef: RefObject; + isValidCurrentStep: boolean; +}; + +const useCardIssuer = ({ nextStepHandler, nextRef, isValidCurrentStep }: UseCardIssuerProps) => { + const [cardIssuer, setCardIssuer] = useState(''); + const [backgroundColor, setBackgroundColor] = useState(''); + const [isCompleted, setIsCompleted] = useState(false); + const ref = useRef(null); + + const handleCardIssuer = (e: React.ChangeEvent) => { + setIsCompleted(false); + const targetValue = e.target.value; + const foundCardIssuer = INITIAL_CARD_ISSUER_INFO.find( + (cardIssuer) => cardIssuer.value === targetValue, + ); + + if (foundCardIssuer) { + setCardIssuer(foundCardIssuer.issuer); + setBackgroundColor(foundCardIssuer.backgroundColor); + + if (!isCompleted && isValidCurrentStep) { + nextStepHandler(); + } + + setIsCompleted(true); + } + }; + + useEffect(() => { + if (isCompleted && ref) { + ref.current?.blur(); + } + if (isCompleted && nextRef) { + nextRef.current?.focus(); + } + }, [isCompleted]); + + return { backgroundColor, handleCardIssuer, ref, cardIssuer }; +}; + +export default useCardIssuer; diff --git a/src/hooks/useRegister.tsx b/src/hooks/useRegister.tsx new file mode 100644 index 0000000000..85defa1dae --- /dev/null +++ b/src/hooks/useRegister.tsx @@ -0,0 +1,40 @@ +import { Children, useState, isValidElement, PropsWithChildren } from 'react'; +import { RegisterComponentProps, RegisterStep } from 'types'; + +interface StepProps { + name: string; +} + +function Register({ children, step }: PropsWithChildren) { + const steps = Children.toArray(children).reverse(); + + const currentIndex = steps.findIndex( + (child) => isValidElement(child) && child.props.name === step, + ); + + const validElements = steps.slice(0, currentIndex + 1).filter(isValidElement); + + return <>{validElements.reverse()}; +} + +function Step({ name, children }: PropsWithChildren) { + return <>{children}; +} + +Register.Step = Step; + +const useRegister = (steps: readonly [...T]) => { + const [step, setStep] = useState(steps[0]); + const [stepIndex, setStepIndex] = useState(0); + + const nextStepHandler = () => { + if (stepIndex < steps.length - 1) { + setStepIndex((prevStepIndex) => prevStepIndex + 1); + setStep(steps[stepIndex + 1]); + } + }; + + return { step, Register, nextStepHandler }; +}; + +export default useRegister; From 127b5a1fe29617a28c4085e1bf3fbcba385dc2b7 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:01:23 +0900 Subject: [PATCH 08/57] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EB=93=B1=EB=A1=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registerSection/InputSection.tsx | 36 ++++++++++++ .../registerSection/RegisterCVC.tsx | 37 +++++++++++++ .../registerSection/RegisterCardIssuer.tsx | 31 +++++++++++ .../registerSection/RegisterPassword.tsx | 39 +++++++++++++ .../registerSection/inputSection.style.ts | 55 +++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 src/components/registerSection/InputSection.tsx create mode 100644 src/components/registerSection/RegisterCVC.tsx create mode 100644 src/components/registerSection/RegisterCardIssuer.tsx create mode 100644 src/components/registerSection/RegisterPassword.tsx create mode 100644 src/components/registerSection/inputSection.style.ts diff --git a/src/components/registerSection/InputSection.tsx b/src/components/registerSection/InputSection.tsx new file mode 100644 index 0000000000..df827c0aa7 --- /dev/null +++ b/src/components/registerSection/InputSection.tsx @@ -0,0 +1,36 @@ +import { PropsWithChildren } from 'react'; +import * as S from './inputSection.style'; + +type InfoProps = { + title: string; + description?: string; + inputTitle: string; +}; + +export default function InputSection({ + title, + description, + inputTitle, + children, +}: PropsWithChildren) { + return ( + + + + {title} + + {description ? ( + + {description} + + ) : null} + + + + {inputTitle} + + {children} + + + ); +} diff --git a/src/components/registerSection/RegisterCVC.tsx b/src/components/registerSection/RegisterCVC.tsx new file mode 100644 index 0000000000..1af22d317e --- /dev/null +++ b/src/components/registerSection/RegisterCVC.tsx @@ -0,0 +1,37 @@ +import InputSection from './InputSection'; +import * as S from '../../app.style'; +import Input from '../composables/Input'; +import Label from '../composables/Label'; +import { MAX_LENGTH } from '../../App'; +import { forwardRef } from 'react'; +import { CVC } from '../../constants/cardSection'; +import { RegisterStepProps } from 'types'; + +const RegisterCVC = forwardRef((props, ref) => { + const { value, onChange, isError, onEnter, onBlur } = props; + + return ( + + + + + {isError && CVC.errorMessage} + + + ); +}); + +export default RegisterCVC; diff --git a/src/components/registerSection/RegisterCardIssuer.tsx b/src/components/registerSection/RegisterCardIssuer.tsx new file mode 100644 index 0000000000..67a760abaf --- /dev/null +++ b/src/components/registerSection/RegisterCardIssuer.tsx @@ -0,0 +1,31 @@ +import * as S from '../../app.style'; +import Option from '../composables/Option'; +import Label from '../composables/Label'; +import InputSection from './InputSection'; +import { CARD_ISSUER } from '../../constants/cardSection'; +import { forwardRef } from 'react'; +import INITIAL_CARD_ISSUER_INFO from '../../constants/initialCardIssuerInfo'; +import Select from '../composables/Select'; + +type RegisterCardIssuerProps = { + onChange: React.ChangeEventHandler; +}; + +const RegisterCardIssuer = forwardRef((props, ref) => { + const { onChange } = props; + + return ( + + + + + ); +}); + +export default RegisterCardIssuer; diff --git a/src/components/registerSection/RegisterPassword.tsx b/src/components/registerSection/RegisterPassword.tsx new file mode 100644 index 0000000000..75d802b81c --- /dev/null +++ b/src/components/registerSection/RegisterPassword.tsx @@ -0,0 +1,39 @@ +import { forwardRef } from 'react'; +import * as S from '../../app.style'; +import InputSection from './InputSection'; +import { PASSWORD } from '../../constants/cardSection'; +import Label from '../composables/Label'; +import Input from '../composables/Input'; +import { MAX_LENGTH } from '../../App'; +import { RegisterStepProps } from 'types'; + +const RegisterPassword = forwardRef>((props, ref) => { + const { value, onChange, isError, onBlur } = props; + + return ( + + + + + {isError && PASSWORD.errorMessage} + + + ); +}); + +export default RegisterPassword; diff --git a/src/components/registerSection/inputSection.style.ts b/src/components/registerSection/inputSection.style.ts new file mode 100644 index 0000000000..d7148999a7 --- /dev/null +++ b/src/components/registerSection/inputSection.style.ts @@ -0,0 +1,55 @@ +import { styled } from 'styled-components'; + +export const Section = styled.section` + display: flex; + flex-direction: column; + gap: 16px; +`; + +export const TitleContainer = styled.div` + display: flex; + align-items: center; + + height: 26px; +`; + +export const Title = styled.h2` + font-size: 1.125rem; + font-weight: 700; +`; + +export const DescriptionContainer = styled.div` + height: 14px; +`; + +export const Description = styled.span` + line-height: 0.875rem; + font-size: 0.5938rem; + font-weight: 400; + color: #8b95a1; +`; + +export const Span = styled.span` + font-weight: 500; + font-size: 0.75rem; + line-height: 0.9375rem; +`; + +export const SpanWrapper = styled.div``; + +export const InputWrapper = styled.div` + display: flex; + gap: 10px; +`; + +export const TitleDescriptionWrapper = styled.div` + display: 'flex'; + flex-direction: 'column'; + gap: '4px'; +`; + +export const TitleChildrenWrapper = styled.div` + display: 'flex'; + flex-direction: 'column'; + gap: '8px'; +`; From c5c75397f95ab9a071182f76470b0c5abbbe2a60 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:01:36 +0900 Subject: [PATCH 09/57] =?UTF-8?q?chore:=20tsconfig.json=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index fa313c5e7f..deb8eb72d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,9 +18,10 @@ "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + "baseUrl": ".", "paths": { - "@/*": ["./src/*"], - "@types/*": ["./@types/*"] + "@/*": ["src/*"], + "@types/*": ["@types/*"] } }, "include": ["src", "./@types/global.d.ts"], From 5be52a4d5abdbe9dce31498c3970be66eb7b464b Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:04:20 +0900 Subject: [PATCH 10/57] =?UTF-8?q?feat:=20cvc=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/cards/CvcCard.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/components/cards/CvcCard.tsx diff --git a/src/components/cards/CvcCard.tsx b/src/components/cards/CvcCard.tsx new file mode 100644 index 0000000000..6b3c1596cd --- /dev/null +++ b/src/components/cards/CvcCard.tsx @@ -0,0 +1,17 @@ +import * as S from './creditCard.style'; + +type CvcCardProps = { + cvc: string; +}; + +export default function CvcCard({ cvc }: CvcCardProps) { + return ( + + + + {cvc} + + + + ); +} From b43c38c99720de7984152a73357f7808136e34f1 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:05:17 +0900 Subject: [PATCH 11/57] =?UTF-8?q?refactor:=20useCardNumber=20=ED=9B=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCardNumber.ts | 65 ++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/hooks/useCardNumber.ts b/src/hooks/useCardNumber.ts index ae3c2989cb..c3a98d75ee 100644 --- a/src/hooks/useCardNumber.ts +++ b/src/hooks/useCardNumber.ts @@ -1,24 +1,36 @@ -import { useState } from 'react'; +import { useRef, useState, RefObject, useEffect } from 'react'; import validate from '../utils/validate'; +import { CARD_NUMBER } from '../constants/cardSection'; +import { InitialCardNumberState } from 'types'; -export type InitialCardNumberState = { - value: string; - isError: boolean; +type UseCardNumberHookProps = { + initialCardNumberStates: InitialCardNumberState[]; + nextStepHandler: () => void; + ref: RefObject; + isValidCurrentStep: boolean; }; -const MAX_CARD_NUMBER_LENGTH = 16; - -const useCardNumber = (initialStates: InitialCardNumberState[]) => { - const [cardNumbers, setCardNumbers] = useState(initialStates); +const useCardNumber = ({ + initialCardNumberStates, + nextStepHandler, + ref, + isValidCurrentStep, +}: UseCardNumberHookProps) => { + const [cardNumbers, setCardNumbers] = useState(initialCardNumberStates); const [cardBrand, setCardBrand] = useState<'none' | 'Visa' | 'MasterCard'>('none'); + const [isCompleted, setIsCompleted] = useState(false); + const refs = [ + useRef(null), + useRef(null), + useRef(null), + useRef(null), + ]; const handleCardBrandImage = (totalCardNumbers: string) => { if (validate.isVisa(totalCardNumbers)) { setCardBrand('Visa'); return; - } - - if (validate.isMasterCard(totalCardNumbers)) { + } else if (validate.isMasterCard(totalCardNumbers)) { setCardBrand('MasterCard'); return; } @@ -28,7 +40,9 @@ const useCardNumber = (initialStates: InitialCardNumberState[]) => { const cardNumbersChangeHandler = (e: React.ChangeEvent, index: number) => { const newValue = e.target.value; - const isValid = newValue === '' || validate.isValidDigit(newValue); + + const isValid = + newValue === '' || validate.isValidDigit(newValue) || validate.isEmptyValue(newValue); const newCardNumbers = cardNumbers.map((cardNumber, i) => { if (i === index) { @@ -37,18 +51,41 @@ const useCardNumber = (initialStates: InitialCardNumberState[]) => { isError: !isValid, }; } + return cardNumber; }); const totalCardNumbers = newCardNumbers.map((card) => card.value).join(''); - if (totalCardNumbers.length === MAX_CARD_NUMBER_LENGTH) { + + if (totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH) { handleCardBrandImage(totalCardNumbers); } setCardNumbers(newCardNumbers); + + if (isCompleted && totalCardNumbers.length < CARD_NUMBER.TOTAL_MAX_LENGTH) { + setIsCompleted(false); + } + + if (isValid && newValue.length === CARD_NUMBER.INDIVIDUAL_MAX_LENGTH && index < 3) { + refs[index + 1].current?.focus(); + } + + if (totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH) { + if (isValidCurrentStep) { + nextStepHandler(); + } + setIsCompleted(true); + } }; - return { cardNumbers, cardNumbersChangeHandler, cardBrand }; + useEffect(() => { + if (isCompleted && ref.current) { + ref.current.focus(); + } + }, [isCompleted]); + + return { cardNumbers, cardNumbersChangeHandler, cardBrand, refs }; }; export default useCardNumber; From dd7952477fe9a82fe6d05871c618c0cf98bb5ba3 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:05:27 +0900 Subject: [PATCH 12/57] =?UTF-8?q?refactor:=20useInput=20=ED=9B=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInput.ts | 94 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index a297d6aab6..0a08854336 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -1,25 +1,105 @@ -import { useState } from 'react'; +import { useState, useRef, RefObject, useEffect } from 'react'; +import validate from '../utils/validate'; -const useInput = (validators: { fn: (value: string) => boolean }[]) => { +type ValidateFn = { + fn: (value: string) => boolean; +}; + +type UseInputProps = { + validators: ValidateFn[]; + nextRef?: RefObject; + nextStepHandler?: () => void; + isValidCurrentStep: boolean; + maxLength: number; +}; + +const useInput = ({ + validators, + nextRef, + nextStepHandler, + maxLength, + isValidCurrentStep, +}: UseInputProps) => { const [inputValue, setInputValue] = useState(''); - const [error, setError] = useState(false); + const [isError, setIsError] = useState(false); + const [isCompleted, setIsCompleted] = useState(false); + const ref = useRef(null); const onChange = (e: React.ChangeEvent) => { - setError(false); const inputValue = e.target.value.toUpperCase(); + setIsError(false); + + if (!inputValue.length) { + setInputValue(''); + setIsCompleted(false); + return; + } for (let validator of validators) { if (!validator.fn(inputValue)) { - setError(true); + setIsError(true); setInputValue(''); return; } } - setInputValue(e.target.value.toUpperCase()); + setInputValue(inputValue); + + if (inputValue.length === maxLength) { + setIsCompleted(true); + if (nextStepHandler && isValidCurrentStep) { + nextStepHandler(); + } + } else { + setIsCompleted(false); + } + }; + + const onEnter = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + if ( + !isCompleted && + nextStepHandler && + !validate.isEmptyValue(inputValue) && + isValidCurrentStep + ) { + nextStepHandler(); + } + + setIsCompleted(true); + } + }; + + const onBlur = (e: React.FocusEvent) => { + const inputValue = e.target.value; + + setIsError(false); + if (!inputValue) { + setIsError(true); + return; + } + if ( + !isCompleted && + nextStepHandler && + isValidCurrentStep && + !validate.isEmptyValue(inputValue) + ) { + nextStepHandler(); + } + setIsCompleted(true); }; - return { inputValue, onChange, error }; + useEffect(() => { + if (isCompleted && ref && nextStepHandler && isValidCurrentStep) { + nextStepHandler(); + ref.current?.blur(); + } + if (isCompleted && nextRef) { + nextRef.current?.focus(); + } + }, [isCompleted]); + + return { inputValue, onChange, isError, onEnter, ref, onBlur }; }; export default useInput; From 934a07bd0bc25ad6bc7bf6e061f4c259388d9926 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:06:23 +0900 Subject: [PATCH 13/57] =?UTF-8?q?feat:=20=ED=99=95=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- @types/global.d.ts | 35 ++++++++++++++ src/main.tsx | 31 ++++++++---- src/pages/confirm/ConfirmPage.tsx | 38 +++++++++++++++ src/pages/confirm/components/CompleteText.tsx | 15 ++++++ .../confirm/components/ConfirmButton.tsx | 15 ++++++ .../confirm/components/ConfirmImageIcon.tsx | 12 +++++ .../confirm/components/confirmButton.style.ts | 13 +++++ src/pages/confirm/confirmPage.style.ts | 47 +++++++++++++++++++ src/pages/notFound/NotFoundPage.tsx | 3 ++ 9 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 @types/global.d.ts create mode 100644 src/pages/confirm/ConfirmPage.tsx create mode 100644 src/pages/confirm/components/CompleteText.tsx create mode 100644 src/pages/confirm/components/ConfirmButton.tsx create mode 100644 src/pages/confirm/components/ConfirmImageIcon.tsx create mode 100644 src/pages/confirm/components/confirmButton.style.ts create mode 100644 src/pages/confirm/confirmPage.style.ts create mode 100644 src/pages/notFound/NotFoundPage.tsx diff --git a/@types/global.d.ts b/@types/global.d.ts new file mode 100644 index 0000000000..56b2323845 --- /dev/null +++ b/@types/global.d.ts @@ -0,0 +1,35 @@ +declare module 'types' { + export type RegisterStepProps = { + value: string; + onChange: React.ChangeEventHandler; + isError: boolean; + onEnter: (e: React.KeyboardEvent) => void; + onBlur: React.FocusEventHandler; + }; + + export type InitialCardNumberState = { + value: string; + isError: boolean; + }; + + export type RegisterStep = + | 'cardNumbers' + | 'cardIssuer' + | 'cardExpirationDate' + | 'cardOwnerName' + | 'cardCvc' + | 'cardPassword'; + + export type RegisterComponentProps = { + step: RegisterStepType; + }; + + export type UseDetectCompleteHookProps = { + cardNumbers: InitialCardNumberState[]; + month: string; + year: string; + cvc: string; + password: string; + name: string; + }; +} diff --git a/src/main.tsx b/src/main.tsx index 3d7150da80..c4ae1263ef 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,25 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.tsx' -import './index.css' +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import ConfirmPage from './pages/confirm/ConfirmPage'; +import NotFoundPage from './pages/notFound/NotFoundPage'; + +const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/confirm', + element: , + }, + + { path: '*', element: }, +]); ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -) + // + , + // , +); diff --git a/src/pages/confirm/ConfirmPage.tsx b/src/pages/confirm/ConfirmPage.tsx new file mode 100644 index 0000000000..7a2fb363b8 --- /dev/null +++ b/src/pages/confirm/ConfirmPage.tsx @@ -0,0 +1,38 @@ +import { useLocation, useNavigate } from 'react-router-dom'; +import GlobalStyles from '../../GlobalStyles'; +import * as S from './confirmPage.style'; +import ConfirmButton from './components/ConfirmButton'; +import ConfirmImageIcon from './components/ConfirmImageIcon'; +import CompleteText from './components/CompleteText'; +import NotFoundPage from '../notFound/NotFoundPage'; + +export default function ConfirmPage() { + const location = useLocation(); + const navigate = useNavigate(); + const { state } = location; + + const isSucceed = state?.isSucceed ?? false; + const cardNumbers = state?.cardNumbers ?? '기본값'; + const cardIssuer = state?.cardIssuer ?? '기본 카드 발급자'; + + const goToHomePage = () => { + navigate('/'); + }; + + return ( + <> + + {isSucceed ? ( + + + + + + + + ) : ( + + )} + + ); +} diff --git a/src/pages/confirm/components/CompleteText.tsx b/src/pages/confirm/components/CompleteText.tsx new file mode 100644 index 0000000000..990cd541b2 --- /dev/null +++ b/src/pages/confirm/components/CompleteText.tsx @@ -0,0 +1,15 @@ +import * as S from '../confirmPage.style'; + +type CompleteTextProps = { + cardNumbers: string; + cardIssuer: string; +}; + +export default function CompleteText({ cardNumbers, cardIssuer }: CompleteTextProps) { + return ( + + {cardNumbers}로 시작하는 + {cardIssuer}가 등록 되었어요. + + ); +} diff --git a/src/pages/confirm/components/ConfirmButton.tsx b/src/pages/confirm/components/ConfirmButton.tsx new file mode 100644 index 0000000000..ab9a7d5374 --- /dev/null +++ b/src/pages/confirm/components/ConfirmButton.tsx @@ -0,0 +1,15 @@ +import { ButtonHTMLAttributes } from 'react'; +import * as B from './confirmButton.style'; +import * as S from '../confirmPage.style'; + +interface ConfirmButtonProps extends ButtonHTMLAttributes { + text: string; +} + +export default function ConfirmButton({ text, ...props }: ConfirmButtonProps) { + return ( + + {text} + + ); +} diff --git a/src/pages/confirm/components/ConfirmImageIcon.tsx b/src/pages/confirm/components/ConfirmImageIcon.tsx new file mode 100644 index 0000000000..5250bf6ce6 --- /dev/null +++ b/src/pages/confirm/components/ConfirmImageIcon.tsx @@ -0,0 +1,12 @@ +import * as S from '../confirmPage.style'; +import CheckImageIcon from '../../../assets/images/complete.png'; + +export default function ConfirmImageIcon() { + return ( + + + 체크 이모티콘 이미지 + + + ); +} diff --git a/src/pages/confirm/components/confirmButton.style.ts b/src/pages/confirm/components/confirmButton.style.ts new file mode 100644 index 0000000000..5b0d8df84d --- /dev/null +++ b/src/pages/confirm/components/confirmButton.style.ts @@ -0,0 +1,13 @@ +import { styled } from 'styled-components'; + +export const ConfirmButton = styled.button` + border-radius: 5px; + color: rgba(243, 243, 243, 1); + background: rgba(51, 51, 51, 1); + cursor: pointer; + width: 100%; + height: 100%; + padding: 8px 0; + height: 44px; + width: 320px; +`; diff --git a/src/pages/confirm/confirmPage.style.ts b/src/pages/confirm/confirmPage.style.ts new file mode 100644 index 0000000000..e2b57c27e8 --- /dev/null +++ b/src/pages/confirm/confirmPage.style.ts @@ -0,0 +1,47 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; +`; + +export const ContentCard = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + min-height: 100vh; +`; + +export const IconContainer = styled.div` + display: flex; + justify-content: center; +`; + +export const TextContainer = styled.div` + margin: 25px 0; + display: flex; + flex-direction: column; +`; + +export const Text = styled.span` + font-size: 25px; + font-weight: 700; + text-align: center; + margin: 5px 0; +`; + +export const ButtonContainer = styled.div` + width: 320px; +`; + +export const IconImageContainer = styled.div` + width: 76px; + height: 76px; + display: flex; + justify-content: center; + align-items: center; + background: rgba(51, 51, 51, 1); + border-radius: 50%; +`; diff --git a/src/pages/notFound/NotFoundPage.tsx b/src/pages/notFound/NotFoundPage.tsx new file mode 100644 index 0000000000..45cf4ae234 --- /dev/null +++ b/src/pages/notFound/NotFoundPage.tsx @@ -0,0 +1,3 @@ +export default function NotFoundPage() { + return
NotFound 페이지입니다!
; +} From 19c0c96adce3c633e8d76b8bb75d0f667f8c08d7 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:07:10 +0900 Subject: [PATCH 14/57] =?UTF-8?q?feat:=20=EC=83=81=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/complete.png | Bin 0 -> 941 bytes src/components/cards/CreditCard.tsx | 4 +- src/constants/cardSection.ts | 24 ++++++++++++ src/constants/initialCardIssuerInfo.ts | 52 +++++++++++++++++++++++++ src/constants/registerStep.ts | 11 ++++++ 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 src/assets/images/complete.png create mode 100644 src/constants/initialCardIssuerInfo.ts create mode 100644 src/constants/registerStep.ts diff --git a/src/assets/images/complete.png b/src/assets/images/complete.png new file mode 100644 index 0000000000000000000000000000000000000000..1a611652dca8972376c30a424db647314d0dd4a3 GIT binary patch literal 941 zcmV;e15*5nP)wh)Brsuum{yE{paN4?0uyzaPXdUl@C@>5^z@E^XAsXmj+LU|k^35ztt&r2)Lq%< zbx!bvAR8S(M|is$8Wliqco2M^?=X(w;{@| z8=eDy#P7W_ydb}MqE*Vg*ukF1 zb_co&kGOUc_}o7W?K1nfyJABXqo)L%73a(k_ceBUW=-cVXC;XS)Kdb^4#Lxhb){1e z)Fja?Jk>m#Ue%=+*WJob+tLa=CC$dmBLQmpO`8DOzxm3G341-tsl8dW0a5VSx0yb# zAVC4#6pYsydLS7!-;K$M;JuN+ev^DCD}qq->?Hw5*`d8W$F&ct{XFFrXVr^ zPTt-SYj@zODq=vQ?1lsMdJOZ=6-{^U4ODpT}mZ08fZ({1yI6`Nah07ne&qWwl{1t z9C!)DYcG1y&(eISc98HaL6TcKL%cTMvV(?aiIU`;mnPz>{6?C<-V`7jnh3Ds9*x3NY?Uj}5~32- zkUpZNn`2vhBHK7T^cvUuQr+LGHCx}4N+nXmRIPbX`7QCbiGcxylds75*nDBBCmED_S4UjRncx{bvj8mHBTn!P zTHZ#8E38_aw9T>ON6pEv>+*5!_kqVG1th)IUZ8rXDA}Oj {cardBrandImageSrc ? ( - + ) : null}
diff --git a/src/constants/cardSection.ts b/src/constants/cardSection.ts index 4d3d5f335b..28723fb36e 100644 --- a/src/constants/cardSection.ts +++ b/src/constants/cardSection.ts @@ -5,6 +5,8 @@ export const CARD_NUMBER = Object.freeze({ description: '본인 명의의 카드만 결제 가능합니다.', inputTitle: '카드 번호', errorMessage: NUMBER_ERROR_MESSAGE, + INDIVIDUAL_MAX_LENGTH: 4, + TOTAL_MAX_LENGTH: 16, }); export const EXPIRATION_PERIOD = Object.freeze({ @@ -13,6 +15,8 @@ export const EXPIRATION_PERIOD = Object.freeze({ inputTitle: '유효기간', monthErrorMessage: '1부터 12사이의 숫자만 입력 가능합니다.', yearErrorMessage: NUMBER_ERROR_MESSAGE, + MONTH_MAX_LENGTH: 2, + YEAR_MAX_LENGTH: 2, }); export const OWNER_NAME = Object.freeze({ @@ -20,3 +24,23 @@ export const OWNER_NAME = Object.freeze({ inputTitle: '소유자 이름', errorMessage: '영문 대문자만 입력 가능합니다.', }); + +export const CARD_ISSUER = Object.freeze({ + title: '카드사를 선택해 주세요', + inputTitle: '현재 국내 카드사만 가능합니다.', +}); + +export const CVC = Object.freeze({ + title: 'CVC 번호를 입력해 주세요.', + inputTitle: 'CVC', + errorMessage: '숫자 3개를 입력해 주세요', + MAX_LENGTH: 3, +}); + +export const PASSWORD = Object.freeze({ + title: '비밀번호를 입력해 주세요', + description: '앞의 2자리를 입력해 주세요', + inputTitle: '비밀번호 앞 2자리', + errorMessage: '숫자만 입력해 주세요', + MAX_LENGTH: 2, +}); diff --git a/src/constants/initialCardIssuerInfo.ts b/src/constants/initialCardIssuerInfo.ts new file mode 100644 index 0000000000..b60ec2410e --- /dev/null +++ b/src/constants/initialCardIssuerInfo.ts @@ -0,0 +1,52 @@ +const INITIAL_CARD_ISSUER_INFO = Object.freeze([ + { + id: 1, + issuer: 'BC카드', + value: 'bc', + backgroundColor: 'rgba(240, 70, 81, 1)', + }, + { + id: 2, + issuer: '신한카드', + value: 'shinhan', + backgroundColor: 'rgba(0, 70, 255, 1)', + }, + { + id: 3, + issuer: '카카오뱅크', + value: 'kakao', + backgroundColor: 'rgba(255, 230, 0, 1)', + }, + { + id: 4, + issuer: '현대카드', + value: 'hyundai', + backgroundColor: 'rgba(0, 0, 0, 1)', + }, + { + id: 5, + issuer: '우리카드', + value: 'woori', + backgroundColor: 'rgba(0, 123, 200, 1)', + }, + { + id: 6, + issuer: '롯데카드', + value: 'lotte', + backgroundColor: 'rgba(237, 28, 36, 1)', + }, + { + id: 7, + issuer: '하나카드', + value: 'hana', + backgroundColor: 'rgba(0, 148, 144, 1)', + }, + { + id: 8, + issuer: '국민카드', + value: 'kb', + backgroundColor: 'rgba(106, 96, 86, 1)', + }, +]); + +export default INITIAL_CARD_ISSUER_INFO; diff --git a/src/constants/registerStep.ts b/src/constants/registerStep.ts new file mode 100644 index 0000000000..59c9d72fe0 --- /dev/null +++ b/src/constants/registerStep.ts @@ -0,0 +1,11 @@ +const REGISTER_STEP = Object.freeze({ + CARD_NUMBER: 'cardNumbers', + CARD_ISSUER: 'cardIssuer', + CARD_EXPIRATION_DATE: 'cardExpirationDate', + CARD_OWNER_NAME: 'cardOwnerName', + CARD_CVC: 'cardCvc', + CARD_PASSWORD: 'cardPassword', + FINAL: 'final', +}); + +export default REGISTER_STEP; From 499c5edc99e2af01acbf13bdaeec81ebeb5a6352 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:07:22 +0900 Subject: [PATCH 15/57] =?UTF-8?q?feat:=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/validate.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 1303a697dc..b95de5f8d6 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -26,6 +26,10 @@ const validate = { isEnglish: (value: string) => { return /^[a-zA-Z ]*$/.test(value); }, + + isEmptyValue: (value: string) => { + return value.length === 0; + }, }; export default validate; From d803c5a3ffe3496a8869999a4ef71cae5fea4a8c Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:07:42 +0900 Subject: [PATCH 16/57] =?UTF-8?q?feat:=20=EC=99=84=EB=A3=8C=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDetectComplete.ts | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/hooks/useDetectComplete.ts diff --git a/src/hooks/useDetectComplete.ts b/src/hooks/useDetectComplete.ts new file mode 100644 index 0000000000..4e1a6560e0 --- /dev/null +++ b/src/hooks/useDetectComplete.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from 'react'; +import { MAX_LENGTH } from '../App'; +import { CARD_NUMBER } from '../constants/cardSection'; +import { UseDetectCompleteHookProps, InitialCardNumberState } from 'types'; + +const useDetectComplete = ({ + cardNumbers, + month, + year, + cvc, + password, + name, +}: UseDetectCompleteHookProps) => { + const [isValidAllFormStates, setIsValidAllFormStates] = useState(false); + + useEffect(() => { + const totalCardNumbers = cardNumbers + .map((cardNumber: InitialCardNumberState) => cardNumber.value) + .join(''); + + if ( + month.length === MAX_LENGTH.MONTH && + year.length === MAX_LENGTH.YEAR && + totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH && + cvc.length === MAX_LENGTH.CVC && + password.length === MAX_LENGTH.PASSWORD && + name.length + ) { + setIsValidAllFormStates(true); + + return; + } + + setIsValidAllFormStates(false); + }, [month, year, cardNumbers, cvc, name, password]); + + return { isValidAllFormStates }; +}; + +export default useDetectComplete; From 5bcb4d42e318b571ac6c2260fe1eae5282254444 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 15:08:10 +0900 Subject: [PATCH 17/57] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=B0=94?= =?UTF-8?q?=EC=9D=B8=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 352 ++++++++++++++++++++++++++++--------------- src/GlobalStyles.tsx | 4 - src/app.style.ts | 22 ++- 3 files changed, 251 insertions(+), 127 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c1aa7d86c9..f1f3fa82af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,23 @@ import GlobalStyles from './GlobalStyles'; -import InputInfo from './components/InputSection'; -import Input from './components/composables/Input'; -import CreditCard from './components/CreditCard'; +import CreditCard from './components/cards/CreditCard'; import useInput from './hooks/useInput'; -import Label from './components/composables/Label'; import validate from './utils/validate'; -import { CARD_NUMBER, EXPIRATION_PERIOD, OWNER_NAME } from './constants/cardSection'; -import * as React from 'react'; -import useCardNumber, { InitialCardNumberState } from './hooks/useCardNumber'; +import useCardNumber from './hooks/useCardNumber'; import * as S from './app.style'; +import useRegister from './hooks/useRegister'; +import RegisterCardNumber from './components/registerSection/RegisterCardNumber'; +import RegisterExpirationDate from './components/registerSection/RegisterExpirationDate'; +import RegisterName from './components/registerSection/RegisterName'; +import RegisterCardIssuer from './components/registerSection/RegisterCardIssuer'; +import useCardIssuer from './hooks/useCardIssuer'; +import RegisterCVC from './components/registerSection/RegisterCVC'; +import RegisterPassword from './components/registerSection/RegisterPassword'; +import Button from './components/composables/Button'; +import { useNavigate } from 'react-router-dom'; +import CvcCard from './components/cards/CvcCard'; +import REGISTER_STEP from './constants/registerStep'; +import { InitialCardNumberState } from 'types'; +import useDetectComplete from './hooks/useDetectComplete'; const initialCardNumberState: InitialCardNumberState = { value: '', @@ -22,140 +31,239 @@ const MONTH = Object.freeze({ MAX: 12, }); -const MAX_LENGTH = Object.freeze({ +export const MAX_LENGTH = Object.freeze({ CARD_NUMBERS: 4, MONTH: 2, YEAR: 2, NAME: 30, + CVC: 3, + PASSWORD: 2, }); function App() { - const { cardNumbers, cardNumbersChangeHandler, cardBrand } = useCardNumber( - Array.from({ length: CARD_NUMBER_LENGTH }, () => initialCardNumberState), - ); + const navigate = useNavigate(); + + const { step, Register, nextStepHandler } = useRegister([ + REGISTER_STEP.CARD_NUMBER, + REGISTER_STEP.CARD_ISSUER, + REGISTER_STEP.CARD_EXPIRATION_DATE, + REGISTER_STEP.CARD_OWNER_NAME, + REGISTER_STEP.CARD_CVC, + REGISTER_STEP.CARD_PASSWORD, + ] as const); const { - inputValue: month, - onChange: monthChangeHandler, - error: monthError, - } = useInput([ - { - fn: (value) => - validate.isNumberInRange({ min: MONTH.MIN, max: MONTH.MAX, compareNumber: Number(value) }), - }, - { fn: (value) => validate.isValidDigit(value) }, - ]); + inputValue: password, + onChange: handlePassword, + isError: passwordError, + ref: passwordRef, + onBlur: handlePasswordBlur, + } = useInput({ + validators: [{ fn: (value) => validate.isValidDigit(value) }], + maxLength: MAX_LENGTH.PASSWORD, + nextStepHandler, + isValidCurrentStep: step === REGISTER_STEP.CARD_PASSWORD, + }); const { - inputValue: year, - onChange: yearChangeHandler, - error: yearError, - } = useInput([{ fn: (value) => validate.isValidDigit(value) }]); + inputValue: cvc, + onChange: handleCvc, + isError: cvcError, + ref: cardCvcRef, + onEnter: handleEnterCvc, + onBlur: handleCvcBlur, + } = useInput({ + validators: [{ fn: (value) => validate.isValidDigit(value) }], + maxLength: MAX_LENGTH.CVC, + nextStepHandler, + nextRef: passwordRef, + isValidCurrentStep: step === REGISTER_STEP.CARD_CVC, + }); const { inputValue: name, onChange: nameChangeHandler, - error: nameError, - } = useInput([{ fn: (value) => validate.isEnglish(value) }]); + isError: nameError, + onEnter: handleEnterName, + ref: nameRef, + onBlur: handleNameBlur, + } = useInput({ + validators: [{ fn: (value) => validate.isEnglish(value) }], + maxLength: MAX_LENGTH.NAME, + nextStepHandler, + nextRef: cardCvcRef, + isValidCurrentStep: step === REGISTER_STEP.CARD_OWNER_NAME, + }); + + const { + inputValue: year, + onChange: yearChangeHandler, + isError: yearError, + ref: yearRef, + onEnter: handleYearKeyDown, + onBlur: handleYearBlur, + } = useInput({ + validators: [{ fn: (value) => validate.isValidDigit(value) }], + nextStepHandler, + nextRef: nameRef, + maxLength: MAX_LENGTH.YEAR, + isValidCurrentStep: step === REGISTER_STEP.CARD_EXPIRATION_DATE, + }); + + const { + inputValue: month, + onChange: monthChangeHandler, + isError: monthError, + ref: monthRef, + onEnter: handleMonthKeyDown, + onBlur: handleMonthBlur, + } = useInput({ + validators: [ + { + fn: (value) => + validate.isNumberInRange({ + min: MONTH.MIN, + max: MONTH.MAX, + compareNumber: Number(value), + }), + }, + { fn: (value) => validate.isValidDigit(value) }, + ], + nextRef: yearRef, + maxLength: MAX_LENGTH.MONTH, + isValidCurrentStep: step === REGISTER_STEP.CARD_EXPIRATION_DATE, + }); + + const { + backgroundColor, + handleCardIssuer, + ref: cardIssuerRef, + cardIssuer, + } = useCardIssuer({ + nextStepHandler, + nextRef: monthRef, + isValidCurrentStep: step === REGISTER_STEP.CARD_ISSUER, + }); + + const { cardNumbers, cardNumbersChangeHandler, cardBrand, refs } = useCardNumber({ + initialCardNumberStates: Array.from( + { length: CARD_NUMBER_LENGTH }, + () => initialCardNumberState, + ), + nextStepHandler, + ref: cardIssuerRef, + isValidCurrentStep: step === REGISTER_STEP.CARD_NUMBER, + }); + + const { isValidAllFormStates } = useDetectComplete({ + cardNumbers, + month, + year, + password, + cvc, + name, + }); + + const handleNavigateToConfirmPage = () => { + navigate('/confirm', { + state: { + isSucceed: true, + cardNumbers: cardNumbers[0].value, + cardIssuer: cardIssuer, + }, + }); + }; return ( <> - - - - - - {cardNumbers.map((cardNumber, index) => { - const uniqueId = 'cardNumbers' + index; - return ( - - - ); - })} - - - - {cardNumbers.some((cardNumber) => cardNumber.isError) && CARD_NUMBER.errorMessage} - - - - - - - - - - {monthError && yearError ? EXPIRATION_PERIOD.monthErrorMessage : ''} - {!monthError && yearError ? EXPIRATION_PERIOD.yearErrorMessage : ''} - {monthError && !yearError ? EXPIRATION_PERIOD.monthErrorMessage : ''} - - - - - - - - - {nameError && OWNER_NAME.errorMessage} - - - - + + + {step === REGISTER_STEP.CARD_CVC ? ( + + ) : ( + + )} + + + {isValidAllFormStates && ( + + + + ); +} diff --git a/src/pages/error/NotFoundPage.tsx b/src/pages/error/NotFoundPage.tsx new file mode 100644 index 0000000000..00933c06aa --- /dev/null +++ b/src/pages/error/NotFoundPage.tsx @@ -0,0 +1,15 @@ +import { useNavigate } from 'react-router-dom'; +import PAGE_ROUTES from '../../constants/routes'; + +export default function NotFoundPage() { + const navigate = useNavigate(); + const handleClickNavigateHomeButton = () => { + navigate(PAGE_ROUTES.MAIN, { replace: true }); + }; + return ( +
+ 페이지를 찾을수 없어요! + +
+ ); +} diff --git a/src/pages/notFound/NotFoundPage.tsx b/src/pages/notFound/NotFoundPage.tsx deleted file mode 100644 index 45cf4ae234..0000000000 --- a/src/pages/notFound/NotFoundPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function NotFoundPage() { - return
NotFound 페이지입니다!
; -} From 284be3d1e28de10866a7518557d944eff27a99f9 Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 19:52:34 +0900 Subject: [PATCH 21/57] =?UTF-8?q?refactor:=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=83=81=EC=88=98=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B7=B8=EC=99=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 ++- src/app.style.ts | 2 +- src/components/registerSection/RegisterCVC.tsx | 6 +++--- src/components/registerSection/RegisterCardIssuer.tsx | 7 +++++-- src/constants/cardSection.ts | 2 +- src/constants/routes.ts | 6 ++++++ src/hooks/useInput.ts | 3 ++- src/main.tsx | 5 ++++- src/pages/confirm/ConfirmPage.tsx | 5 +++-- 9 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 src/constants/routes.ts diff --git a/src/App.tsx b/src/App.tsx index f1f3fa82af..242b09d3eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import CvcCard from './components/cards/CvcCard'; import REGISTER_STEP from './constants/registerStep'; import { InitialCardNumberState } from 'types'; import useDetectComplete from './hooks/useDetectComplete'; +import PAGE_ROUTES from './constants/routes'; const initialCardNumberState: InitialCardNumberState = { value: '', @@ -165,7 +166,7 @@ function App() { }); const handleNavigateToConfirmPage = () => { - navigate('/confirm', { + navigate(PAGE_ROUTES.CONFIRM, { state: { isSucceed: true, cardNumbers: cardNumbers[0].value, diff --git a/src/app.style.ts b/src/app.style.ts index f347b850cd..738b533b6d 100644 --- a/src/app.style.ts +++ b/src/app.style.ts @@ -47,6 +47,6 @@ export const ButtonContainer = styled.div` right: 0; left: 0; width: 376px; - margin: 17px auto; + margin: 0px auto; height: 52px; `; diff --git a/src/components/registerSection/RegisterCVC.tsx b/src/components/registerSection/RegisterCVC.tsx index 1af22d317e..52349f9a69 100644 --- a/src/components/registerSection/RegisterCVC.tsx +++ b/src/components/registerSection/RegisterCVC.tsx @@ -4,7 +4,7 @@ import Input from '../composables/Input'; import Label from '../composables/Label'; import { MAX_LENGTH } from '../../App'; import { forwardRef } from 'react'; -import { CVC } from '../../constants/cardSection'; +import { CARD_CVC } from '../../constants/cardSection'; import { RegisterStepProps } from 'types'; const RegisterCVC = forwardRef((props, ref) => { @@ -12,7 +12,7 @@ const RegisterCVC = forwardRef((props, ref) return ( - + - {isError && CVC.errorMessage} + {isError && CARD_CVC.errorMessage} ); diff --git a/src/components/registerSection/RegisterCardIssuer.tsx b/src/components/registerSection/RegisterCardIssuer.tsx index 67a760abaf..729ed6ff43 100644 --- a/src/components/registerSection/RegisterCardIssuer.tsx +++ b/src/components/registerSection/RegisterCardIssuer.tsx @@ -1,5 +1,4 @@ import * as S from '../../app.style'; -import Option from '../composables/Option'; import Label from '../composables/Label'; import InputSection from './InputSection'; import { CARD_ISSUER } from '../../constants/cardSection'; @@ -20,7 +19,11 @@ const RegisterCardIssuer = forwardRef diff --git a/src/constants/cardSection.ts b/src/constants/cardSection.ts index 28723fb36e..557e43f692 100644 --- a/src/constants/cardSection.ts +++ b/src/constants/cardSection.ts @@ -30,7 +30,7 @@ export const CARD_ISSUER = Object.freeze({ inputTitle: '현재 국내 카드사만 가능합니다.', }); -export const CVC = Object.freeze({ +export const CARD_CVC = Object.freeze({ title: 'CVC 번호를 입력해 주세요.', inputTitle: 'CVC', errorMessage: '숫자 3개를 입력해 주세요', diff --git a/src/constants/routes.ts b/src/constants/routes.ts new file mode 100644 index 0000000000..54735d9f8f --- /dev/null +++ b/src/constants/routes.ts @@ -0,0 +1,6 @@ +const PAGE_ROUTES = Object.freeze({ + MAIN: '/', + CONFIRM: '/confirm', +}); + +export default PAGE_ROUTES; diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index 0a08854336..656345ef5c 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -61,7 +61,8 @@ const useInput = ({ !isCompleted && nextStepHandler && !validate.isEmptyValue(inputValue) && - isValidCurrentStep + isValidCurrentStep && + !inputValue ) { nextStepHandler(); } diff --git a/src/main.tsx b/src/main.tsx index c4ae1263ef..2de617abbf 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,16 +3,19 @@ import App from './App'; import './index.css'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import ConfirmPage from './pages/confirm/ConfirmPage'; -import NotFoundPage from './pages/notFound/NotFoundPage'; +import NotFoundPage from './pages/error/NotFoundPage'; +import ErrorPage from './pages/error/ErrorPage'; const router = createBrowserRouter([ { path: '/', element: , + errorElement: , }, { path: '/confirm', element: , + errorElement: , }, { path: '*', element: }, diff --git a/src/pages/confirm/ConfirmPage.tsx b/src/pages/confirm/ConfirmPage.tsx index 7a2fb363b8..80f62d5017 100644 --- a/src/pages/confirm/ConfirmPage.tsx +++ b/src/pages/confirm/ConfirmPage.tsx @@ -4,7 +4,8 @@ import * as S from './confirmPage.style'; import ConfirmButton from './components/ConfirmButton'; import ConfirmImageIcon from './components/ConfirmImageIcon'; import CompleteText from './components/CompleteText'; -import NotFoundPage from '../notFound/NotFoundPage'; +import NotFoundPage from '../error/NotFoundPage'; +import PAGE_ROUTES from '../../constants/routes'; export default function ConfirmPage() { const location = useLocation(); @@ -16,7 +17,7 @@ export default function ConfirmPage() { const cardIssuer = state?.cardIssuer ?? '기본 카드 발급자'; const goToHomePage = () => { - navigate('/'); + navigate(PAGE_ROUTES.MAIN); }; return ( From c4cec9681effb5fdb6a4aabd9a61c59d68134ddc Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 19:54:10 +0900 Subject: [PATCH 22/57] =?UTF-8?q?chore:=20=EC=8A=A4=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=B6=81=20addon=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 19 ++++++++++--------- package.json | 1 + 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 69ac0074d7..fe998862ea 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,20 +1,21 @@ -import type { StorybookConfig } from "@storybook/react-vite"; +import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { - stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], addons: [ - "@storybook/addon-onboarding", - "@storybook/addon-links", - "@storybook/addon-essentials", - "@chromatic-com/storybook", - "@storybook/addon-interactions", + '@storybook/addon-onboarding', + '@storybook/addon-links', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + 'storybook-addon-remix-react-router', ], framework: { - name: "@storybook/react-vite", + name: '@storybook/react-vite', options: {}, }, docs: { - autodocs: "tag", + autodocs: 'tag', }, }; export default config; diff --git a/package.json b/package.json index 6a32d4b5bb..df249f8108 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-storybook": "^0.8.0", "prettier": "^3.2.5", "storybook": "^8.0.8", + "storybook-addon-remix-react-router": "^3.0.0", "typescript": "^5.2.2", "vite": "^5.2.0" } From 741bb32a0156e4b9d5ca721aeaf8ccc9707f183f Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 20:29:43 +0900 Subject: [PATCH 23/57] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 10 +--------- src/components/registerSection/RegisterCVC.tsx | 2 +- .../registerSection/RegisterCardIssuer.tsx | 5 +++-- .../registerSection/RegisterCardNumber.tsx | 16 +++++++++------- .../registerSection/RegisterExpirationDate.tsx | 2 +- src/components/registerSection/RegisterName.tsx | 2 +- .../registerSection/RegisterPassword.tsx | 2 +- .../registerSection/registerCardIssuer.style.ts | 6 ++++++ src/constants/cardSection.ts | 16 ++++++++++------ src/hooks/useCardNumber.ts | 10 +++++----- src/hooks/useDetectComplete.ts | 5 ++--- src/hooks/useInput.ts | 6 ++---- 12 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 src/components/registerSection/registerCardIssuer.style.ts diff --git a/src/App.tsx b/src/App.tsx index 242b09d3eb..10d1a54488 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import REGISTER_STEP from './constants/registerStep'; import { InitialCardNumberState } from 'types'; import useDetectComplete from './hooks/useDetectComplete'; import PAGE_ROUTES from './constants/routes'; +import { MAX_LENGTH } from './constants/cardSection'; const initialCardNumberState: InitialCardNumberState = { value: '', @@ -32,15 +33,6 @@ const MONTH = Object.freeze({ MAX: 12, }); -export const MAX_LENGTH = Object.freeze({ - CARD_NUMBERS: 4, - MONTH: 2, - YEAR: 2, - NAME: 30, - CVC: 3, - PASSWORD: 2, -}); - function App() { const navigate = useNavigate(); diff --git a/src/components/registerSection/RegisterCVC.tsx b/src/components/registerSection/RegisterCVC.tsx index 52349f9a69..27297df78a 100644 --- a/src/components/registerSection/RegisterCVC.tsx +++ b/src/components/registerSection/RegisterCVC.tsx @@ -2,7 +2,7 @@ import InputSection from './InputSection'; import * as S from '../../app.style'; import Input from '../composables/Input'; import Label from '../composables/Label'; -import { MAX_LENGTH } from '../../App'; +import { MAX_LENGTH } from '../../constants/cardSection'; import { forwardRef } from 'react'; import { CARD_CVC } from '../../constants/cardSection'; import { RegisterStepProps } from 'types'; diff --git a/src/components/registerSection/RegisterCardIssuer.tsx b/src/components/registerSection/RegisterCardIssuer.tsx index 729ed6ff43..ff3574c052 100644 --- a/src/components/registerSection/RegisterCardIssuer.tsx +++ b/src/components/registerSection/RegisterCardIssuer.tsx @@ -1,4 +1,5 @@ import * as S from '../../app.style'; +import * as Styled from './registerCardIssuer.style'; import Label from '../composables/Label'; import InputSection from './InputSection'; import { CARD_ISSUER } from '../../constants/cardSection'; @@ -17,7 +18,7 @@ const RegisterCardIssuer = forwardRef ); diff --git a/src/components/registerSection/RegisterCardNumber.tsx b/src/components/registerSection/RegisterCardNumber.tsx index 405167d1bb..6490480b87 100644 --- a/src/components/registerSection/RegisterCardNumber.tsx +++ b/src/components/registerSection/RegisterCardNumber.tsx @@ -2,10 +2,10 @@ import * as S from '../../app.style'; import InputSection from './InputSection'; import { CARD_NUMBER } from '../../constants/cardSection'; import { InitialCardNumberState } from 'types'; -import { Fragment, forwardRef, RefObject } from 'react'; +import { Fragment, RefObject } from 'react'; import Label from '../composables/Label'; import Input from '../composables/Input'; -import { MAX_LENGTH } from '../../App'; +import { MAX_LENGTH } from '../../constants/cardSection'; type RegisterCardNumberProps = { cardNumbers: InitialCardNumberState[]; @@ -13,9 +13,11 @@ type RegisterCardNumberProps = { refs: RefObject[]; }; -const RegisterCardNumber = forwardRef((props, ref) => { - const { cardNumbers, cardNumbersChangeHandler, refs } = props; - +const RegisterCardNumber = ({ + cardNumbers, + cardNumbersChangeHandler, + refs, +}: RegisterCardNumberProps) => { return ( ref={refs[index]} placeholder="1234" type="text" - maxLength={MAX_LENGTH.CARD_NUMBERS} + maxLength={MAX_LENGTH.INDIVIDUAL_CARD_NUMBER} value={cardNumber.value} onChange={(e) => cardNumbersChangeHandler(e, index)} isError={cardNumber.isError} @@ -49,6 +51,6 @@ const RegisterCardNumber = forwardRef ); -}); +}; export default RegisterCardNumber; diff --git a/src/components/registerSection/RegisterExpirationDate.tsx b/src/components/registerSection/RegisterExpirationDate.tsx index 70a89d3d07..abd9bb6f32 100644 --- a/src/components/registerSection/RegisterExpirationDate.tsx +++ b/src/components/registerSection/RegisterExpirationDate.tsx @@ -1,4 +1,4 @@ -import { MAX_LENGTH } from '../../App'; +import { MAX_LENGTH } from '../../constants/cardSection'; import * as S from '../../app.style'; import { EXPIRATION_PERIOD } from '../../constants/cardSection'; import { Input } from '../composables/input.style'; diff --git a/src/components/registerSection/RegisterName.tsx b/src/components/registerSection/RegisterName.tsx index 816ebf4e02..37cb9f6212 100644 --- a/src/components/registerSection/RegisterName.tsx +++ b/src/components/registerSection/RegisterName.tsx @@ -3,7 +3,7 @@ import Input from '../composables/Input'; import Label from '../composables/Label'; import InputSection from './InputSection'; import { forwardRef } from 'react'; -import { MAX_LENGTH } from '../../App'; +import { MAX_LENGTH } from '../../constants/cardSection'; import { OWNER_NAME } from '../../constants/cardSection'; import { RegisterStepProps } from 'types'; diff --git a/src/components/registerSection/RegisterPassword.tsx b/src/components/registerSection/RegisterPassword.tsx index 75d802b81c..415c8cb1ea 100644 --- a/src/components/registerSection/RegisterPassword.tsx +++ b/src/components/registerSection/RegisterPassword.tsx @@ -4,7 +4,7 @@ import InputSection from './InputSection'; import { PASSWORD } from '../../constants/cardSection'; import Label from '../composables/Label'; import Input from '../composables/Input'; -import { MAX_LENGTH } from '../../App'; +import { MAX_LENGTH } from '../../constants/cardSection'; import { RegisterStepProps } from 'types'; const RegisterPassword = forwardRef>((props, ref) => { diff --git a/src/components/registerSection/registerCardIssuer.style.ts b/src/components/registerSection/registerCardIssuer.style.ts new file mode 100644 index 0000000000..37a1ec835c --- /dev/null +++ b/src/components/registerSection/registerCardIssuer.style.ts @@ -0,0 +1,6 @@ +import { styled } from 'styled-components'; + +export const Select = styled.select` + width: 100%; + padding: 8px; +`; diff --git a/src/constants/cardSection.ts b/src/constants/cardSection.ts index 557e43f692..7965043ddc 100644 --- a/src/constants/cardSection.ts +++ b/src/constants/cardSection.ts @@ -5,8 +5,16 @@ export const CARD_NUMBER = Object.freeze({ description: '본인 명의의 카드만 결제 가능합니다.', inputTitle: '카드 번호', errorMessage: NUMBER_ERROR_MESSAGE, - INDIVIDUAL_MAX_LENGTH: 4, - TOTAL_MAX_LENGTH: 16, +}); + +export const MAX_LENGTH = Object.freeze({ + TOTAL_CARD_NUMBER: 16, + INDIVIDUAL_CARD_NUMBER: 4, + MONTH: 2, + YEAR: 2, + NAME: 30, + CVC: 3, + PASSWORD: 2, }); export const EXPIRATION_PERIOD = Object.freeze({ @@ -15,8 +23,6 @@ export const EXPIRATION_PERIOD = Object.freeze({ inputTitle: '유효기간', monthErrorMessage: '1부터 12사이의 숫자만 입력 가능합니다.', yearErrorMessage: NUMBER_ERROR_MESSAGE, - MONTH_MAX_LENGTH: 2, - YEAR_MAX_LENGTH: 2, }); export const OWNER_NAME = Object.freeze({ @@ -34,7 +40,6 @@ export const CARD_CVC = Object.freeze({ title: 'CVC 번호를 입력해 주세요.', inputTitle: 'CVC', errorMessage: '숫자 3개를 입력해 주세요', - MAX_LENGTH: 3, }); export const PASSWORD = Object.freeze({ @@ -42,5 +47,4 @@ export const PASSWORD = Object.freeze({ description: '앞의 2자리를 입력해 주세요', inputTitle: '비밀번호 앞 2자리', errorMessage: '숫자만 입력해 주세요', - MAX_LENGTH: 2, }); diff --git a/src/hooks/useCardNumber.ts b/src/hooks/useCardNumber.ts index c3a98d75ee..54a2169d05 100644 --- a/src/hooks/useCardNumber.ts +++ b/src/hooks/useCardNumber.ts @@ -1,6 +1,6 @@ import { useRef, useState, RefObject, useEffect } from 'react'; import validate from '../utils/validate'; -import { CARD_NUMBER } from '../constants/cardSection'; +import { MAX_LENGTH } from '../constants/cardSection'; import { InitialCardNumberState } from 'types'; type UseCardNumberHookProps = { @@ -57,21 +57,21 @@ const useCardNumber = ({ const totalCardNumbers = newCardNumbers.map((card) => card.value).join(''); - if (totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH) { + if (totalCardNumbers.length === MAX_LENGTH.TOTAL_CARD_NUMBER) { handleCardBrandImage(totalCardNumbers); } setCardNumbers(newCardNumbers); - if (isCompleted && totalCardNumbers.length < CARD_NUMBER.TOTAL_MAX_LENGTH) { + if (isCompleted && totalCardNumbers.length < MAX_LENGTH.TOTAL_CARD_NUMBER) { setIsCompleted(false); } - if (isValid && newValue.length === CARD_NUMBER.INDIVIDUAL_MAX_LENGTH && index < 3) { + if (isValid && newValue.length === MAX_LENGTH.INDIVIDUAL_CARD_NUMBER && index < 3) { refs[index + 1].current?.focus(); } - if (totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH) { + if (totalCardNumbers.length === MAX_LENGTH.TOTAL_CARD_NUMBER) { if (isValidCurrentStep) { nextStepHandler(); } diff --git a/src/hooks/useDetectComplete.ts b/src/hooks/useDetectComplete.ts index 4e1a6560e0..1be433c1f0 100644 --- a/src/hooks/useDetectComplete.ts +++ b/src/hooks/useDetectComplete.ts @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; -import { MAX_LENGTH } from '../App'; -import { CARD_NUMBER } from '../constants/cardSection'; +import { MAX_LENGTH } from '../constants/cardSection'; import { UseDetectCompleteHookProps, InitialCardNumberState } from 'types'; const useDetectComplete = ({ @@ -21,7 +20,7 @@ const useDetectComplete = ({ if ( month.length === MAX_LENGTH.MONTH && year.length === MAX_LENGTH.YEAR && - totalCardNumbers.length === CARD_NUMBER.TOTAL_MAX_LENGTH && + totalCardNumbers.length === MAX_LENGTH.TOTAL_CARD_NUMBER && cvc.length === MAX_LENGTH.CVC && password.length === MAX_LENGTH.PASSWORD && name.length diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index 656345ef5c..4fe6ba0861 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -61,13 +61,11 @@ const useInput = ({ !isCompleted && nextStepHandler && !validate.isEmptyValue(inputValue) && - isValidCurrentStep && - !inputValue + isValidCurrentStep ) { nextStepHandler(); + setIsCompleted(true); } - - setIsCompleted(true); } }; From e2e01e743d063dea4ca84d565ab7a7fdb78e10ab Mon Sep 17 00:00:00 2001 From: brgndy Date: Thu, 25 Apr 2024 20:55:05 +0900 Subject: [PATCH 24/57] =?UTF-8?q?refactor:=20=EB=A6=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/composables/Option.tsx | 9 --------- src/components/registerSection/RegisterCardIssuer.tsx | 1 - .../registerSection/RegisterExpirationDate.tsx | 2 +- src/hooks/useRegister.tsx | 2 +- src/main.tsx | 4 ++-- 5 files changed, 4 insertions(+), 14 deletions(-) delete mode 100644 src/components/composables/Option.tsx diff --git a/src/components/composables/Option.tsx b/src/components/composables/Option.tsx deleted file mode 100644 index d47bb182a8..0000000000 --- a/src/components/composables/Option.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { OptionHTMLAttributes } from 'react'; - -interface OptionProps extends OptionHTMLAttributes { - text: string; -} - -export default function Option({ value, text }: OptionProps) { - return ; -} diff --git a/src/components/registerSection/RegisterCardIssuer.tsx b/src/components/registerSection/RegisterCardIssuer.tsx index ff3574c052..ac9fbcae73 100644 --- a/src/components/registerSection/RegisterCardIssuer.tsx +++ b/src/components/registerSection/RegisterCardIssuer.tsx @@ -5,7 +5,6 @@ import InputSection from './InputSection'; import { CARD_ISSUER } from '../../constants/cardSection'; import { forwardRef } from 'react'; import INITIAL_CARD_ISSUER_INFO from '../../constants/initialCardIssuerInfo'; -import Select from '../composables/Select'; type RegisterCardIssuerProps = { onChange: React.ChangeEventHandler; diff --git a/src/components/registerSection/RegisterExpirationDate.tsx b/src/components/registerSection/RegisterExpirationDate.tsx index abd9bb6f32..b80d09d110 100644 --- a/src/components/registerSection/RegisterExpirationDate.tsx +++ b/src/components/registerSection/RegisterExpirationDate.tsx @@ -22,7 +22,7 @@ type RegisterExpirationDateProps = { }; const RegisterExpirationDate = forwardRef( - (props, ref) => { + (props) => { const { month, monthError, diff --git a/src/hooks/useRegister.tsx b/src/hooks/useRegister.tsx index 85defa1dae..9a3f7970dc 100644 --- a/src/hooks/useRegister.tsx +++ b/src/hooks/useRegister.tsx @@ -17,7 +17,7 @@ function Register({ children, step }: PropsWithChildren) return <>{validElements.reverse()}; } -function Step({ name, children }: PropsWithChildren) { +function Step({ children }: PropsWithChildren) { return <>{children}; } diff --git a/src/main.tsx b/src/main.tsx index 2de617abbf..a9608fb34d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,4 +1,4 @@ -import ReactDOM from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; import App from './App'; import './index.css'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; @@ -21,7 +21,7 @@ const router = createBrowserRouter([ { path: '*', element: }, ]); -ReactDOM.createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById('root')!).render( // , // , From 4ce799e83c2ea2a2f3f6b3fa0cbd75aa8594fce1 Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 22:59:28 +0900 Subject: [PATCH 25/57] =?UTF-8?q?refactor:=20useDetectComplete=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=ED=95=A8=EC=88=98=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=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/hooks/useDetectComplete.ts | 19 ++++--------------- src/utils/validate.ts | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/hooks/useDetectComplete.ts b/src/hooks/useDetectComplete.ts index 1be433c1f0..114139fa09 100644 --- a/src/hooks/useDetectComplete.ts +++ b/src/hooks/useDetectComplete.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import { MAX_LENGTH } from '../constants/cardSection'; -import { UseDetectCompleteHookProps, InitialCardNumberState } from 'types'; +import { RegisterFieldInfos } from '@/types'; +import validate from '../utils/validate'; const useDetectComplete = ({ cardNumbers, @@ -9,22 +9,11 @@ const useDetectComplete = ({ cvc, password, name, -}: UseDetectCompleteHookProps) => { +}: RegisterFieldInfos) => { const [isValidAllFormStates, setIsValidAllFormStates] = useState(false); useEffect(() => { - const totalCardNumbers = cardNumbers - .map((cardNumber: InitialCardNumberState) => cardNumber.value) - .join(''); - - if ( - month.length === MAX_LENGTH.MONTH && - year.length === MAX_LENGTH.YEAR && - totalCardNumbers.length === MAX_LENGTH.TOTAL_CARD_NUMBER && - cvc.length === MAX_LENGTH.CVC && - password.length === MAX_LENGTH.PASSWORD && - name.length - ) { + if (validate.isValidAllFormStates({ cardNumbers, month, year, cvc, password, name })) { setIsValidAllFormStates(true); return; diff --git a/src/utils/validate.ts b/src/utils/validate.ts index b95de5f8d6..4cbd564291 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -1,3 +1,6 @@ +import { MAX_LENGTH } from '@/constants/cardSection'; +import { RegisterFieldInfos, InitialCardNumberState } from '@/types'; + const validate = { isNumberInRange: ({ min, @@ -30,6 +33,24 @@ const validate = { isEmptyValue: (value: string) => { return value.length === 0; }, + + isValidAllFormStates: ({ cardNumbers, month, year, cvc, password, name }: RegisterFieldInfos) => { + const totalCardNumbers = cardNumbers + .map((cardNumber: InitialCardNumberState) => cardNumber.value) + .join(''); + + if ( + month.length === MAX_LENGTH.MONTH && + year.length === MAX_LENGTH.YEAR && + totalCardNumbers.length === MAX_LENGTH.TOTAL_CARD_NUMBER && + cvc.length === MAX_LENGTH.CVC && + password.length === MAX_LENGTH.PASSWORD && + name.length + ) { + return true; + } + return false; + }, }; export default validate; From ccda0d0222799c5d40642108ecbd40852048bc17 Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:00:05 +0900 Subject: [PATCH 26/57] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8F=B4=EB=8D=94=20=EB=B0=8F=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- @types/global.d.ts | 35 ------------------------- src/types/index.d.ts | 61 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 35 deletions(-) delete mode 100644 @types/global.d.ts create mode 100644 src/types/index.d.ts diff --git a/@types/global.d.ts b/@types/global.d.ts deleted file mode 100644 index 56b2323845..0000000000 --- a/@types/global.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -declare module 'types' { - export type RegisterStepProps = { - value: string; - onChange: React.ChangeEventHandler; - isError: boolean; - onEnter: (e: React.KeyboardEvent) => void; - onBlur: React.FocusEventHandler; - }; - - export type InitialCardNumberState = { - value: string; - isError: boolean; - }; - - export type RegisterStep = - | 'cardNumbers' - | 'cardIssuer' - | 'cardExpirationDate' - | 'cardOwnerName' - | 'cardCvc' - | 'cardPassword'; - - export type RegisterComponentProps = { - step: RegisterStepType; - }; - - export type UseDetectCompleteHookProps = { - cardNumbers: InitialCardNumberState[]; - month: string; - year: string; - cvc: string; - password: string; - name: string; - }; -} diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000000..aadbdb3c40 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,61 @@ +import { InputHTMLAttributes } from 'react'; + +export interface RegisterFieldProps extends InputHTMLAttributes { + isError: boolean; +} + +export type InitialCardNumberState = { + value: string; + isError: boolean; +}; + +export type RegisterStep = + | 'cardNumbers' + | 'cardIssuer' + | 'cardExpirationDate' + | 'cardOwnerName' + | 'cardCvc' + | 'cardPassword'; + +export type RegisterComponentProps = { + step: RegisterStepType; +}; + +export type RegisterFieldInfos = { + cardNumbers: InitialCardNumberState[]; + month: string; + year: string; + cvc: string; + password: string; + name: string; +}; + +type CardIssuer = { + cardIssuer: AllCardIssuer; +}; + +export type AllCardIssuer = + | '' + | 'BC카드' + | '신한카드' + | '카카오뱅크' + | '현대카드' + | '우리카드' + | '롯데카드' + | '하나카드' + | '국민카드'; + +export type CardIssuerBackgroundColor = + | 'rgba(240, 70, 81, 1)' + | 'rgba(0, 70, 255, 1)' + | 'rgba(255, 230, 0, 1)' + | 'rgba(0, 0, 0, 1)' + | 'rgba(0, 123, 200, 1)' + | 'rgba(237, 28, 36, 1)' + | 'rgba(0, 148, 144, 1)' + | 'rgba(106, 96, 86, 1)'; + +export type ConfirmPageRouteProps = { + cardNumbers: string; + cardIssuer: AllCardIssuer; +}; From 6443a527454270aa06c9c5e6f6f456328cfd9769 Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:00:21 +0900 Subject: [PATCH 27/57] =?UTF-8?q?chore:=20=EA=B2=BD=EB=A1=9C=20alias=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite.config.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 861b04b356..3c86cd06df 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,14 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react-swc' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], -}) + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + // base: '/react-payments/dist', +}); From 919f76cec9e73c2372bfc3de3bf2eef9652be34f Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:02:03 +0900 Subject: [PATCH 28/57] =?UTF-8?q?refactor:=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/composables/Button.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/composables/Button.tsx b/src/components/composables/Button.tsx index b217643542..0dd4e97bb9 100644 --- a/src/components/composables/Button.tsx +++ b/src/components/composables/Button.tsx @@ -1,10 +1,8 @@ import { ButtonHTMLAttributes } from 'react'; import * as S from './button.style'; -interface CustomButtonProps extends ButtonHTMLAttributes { - text: string; -} +interface CustomButtonProps extends ButtonHTMLAttributes {} -export default function Button({ text, ...props }: CustomButtonProps) { - return {text}; +export default function Button({ children, ...props }: CustomButtonProps) { + return {children}; } From 0b048d52cda09ada5339982f75be665255cee16c Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:02:56 +0900 Subject: [PATCH 29/57] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20alias=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/registerSection/RegisterCVC.tsx | 11 +++++------ .../registerSection/RegisterCardIssuer.tsx | 10 ++++------ .../registerSection/RegisterCardNumber.tsx | 5 ++--- .../registerSection/RegisterExpirationDate.tsx | 3 +-- src/components/registerSection/RegisterName.tsx | 12 ++++++------ src/components/registerSection/RegisterPassword.tsx | 9 ++++----- 6 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/components/registerSection/RegisterCVC.tsx b/src/components/registerSection/RegisterCVC.tsx index 27297df78a..84e9380e16 100644 --- a/src/components/registerSection/RegisterCVC.tsx +++ b/src/components/registerSection/RegisterCVC.tsx @@ -2,13 +2,12 @@ import InputSection from './InputSection'; import * as S from '../../app.style'; import Input from '../composables/Input'; import Label from '../composables/Label'; -import { MAX_LENGTH } from '../../constants/cardSection'; +import { MAX_LENGTH, CARD_CVC } from '../../constants/cardSection'; +import { RegisterFieldProps } from '@/types'; import { forwardRef } from 'react'; -import { CARD_CVC } from '../../constants/cardSection'; -import { RegisterStepProps } from 'types'; -const RegisterCVC = forwardRef((props, ref) => { - const { value, onChange, isError, onEnter, onBlur } = props; +const RegisterCVC = forwardRef((props, ref) => { + const { value, onChange, isError, onKeyDown, onBlur } = props; return ( @@ -22,7 +21,7 @@ const RegisterCVC = forwardRef((props, ref) maxLength={MAX_LENGTH.CVC} onChange={onChange} isError={isError} - onKeyDown={onEnter} + onKeyDown={onKeyDown} ref={ref} onBlur={onBlur} /> diff --git a/src/components/registerSection/RegisterCardIssuer.tsx b/src/components/registerSection/RegisterCardIssuer.tsx index ac9fbcae73..fe5054be5b 100644 --- a/src/components/registerSection/RegisterCardIssuer.tsx +++ b/src/components/registerSection/RegisterCardIssuer.tsx @@ -2,13 +2,11 @@ import * as S from '../../app.style'; import * as Styled from './registerCardIssuer.style'; import Label from '../composables/Label'; import InputSection from './InputSection'; -import { CARD_ISSUER } from '../../constants/cardSection'; -import { forwardRef } from 'react'; -import INITIAL_CARD_ISSUER_INFO from '../../constants/initialCardIssuerInfo'; +import { CARD_ISSUER } from '@/constants/cardSection'; +import { forwardRef, SelectHTMLAttributes } from 'react'; +import INITIAL_CARD_ISSUER_INFO from '@/constants/allCardIssuerInfo'; -type RegisterCardIssuerProps = { - onChange: React.ChangeEventHandler; -}; +interface RegisterCardIssuerProps extends SelectHTMLAttributes {} const RegisterCardIssuer = forwardRef((props, ref) => { const { onChange } = props; diff --git a/src/components/registerSection/RegisterCardNumber.tsx b/src/components/registerSection/RegisterCardNumber.tsx index 6490480b87..468c1caaf0 100644 --- a/src/components/registerSection/RegisterCardNumber.tsx +++ b/src/components/registerSection/RegisterCardNumber.tsx @@ -1,11 +1,10 @@ import * as S from '../../app.style'; import InputSection from './InputSection'; -import { CARD_NUMBER } from '../../constants/cardSection'; -import { InitialCardNumberState } from 'types'; +import { InitialCardNumberState } from '@/types'; import { Fragment, RefObject } from 'react'; import Label from '../composables/Label'; import Input from '../composables/Input'; -import { MAX_LENGTH } from '../../constants/cardSection'; +import { MAX_LENGTH, CARD_NUMBER } from '@/constants/cardSection'; type RegisterCardNumberProps = { cardNumbers: InitialCardNumberState[]; diff --git a/src/components/registerSection/RegisterExpirationDate.tsx b/src/components/registerSection/RegisterExpirationDate.tsx index b80d09d110..98c9dd3d70 100644 --- a/src/components/registerSection/RegisterExpirationDate.tsx +++ b/src/components/registerSection/RegisterExpirationDate.tsx @@ -1,6 +1,5 @@ -import { MAX_LENGTH } from '../../constants/cardSection'; import * as S from '../../app.style'; -import { EXPIRATION_PERIOD } from '../../constants/cardSection'; +import { MAX_LENGTH, EXPIRATION_PERIOD } from '@/constants/cardSection'; import { Input } from '../composables/input.style'; import Label from '../composables/Label'; import InputSection from './InputSection'; diff --git a/src/components/registerSection/RegisterName.tsx b/src/components/registerSection/RegisterName.tsx index 37cb9f6212..e29f615334 100644 --- a/src/components/registerSection/RegisterName.tsx +++ b/src/components/registerSection/RegisterName.tsx @@ -2,13 +2,13 @@ import * as S from '../../app.style'; import Input from '../composables/Input'; import Label from '../composables/Label'; import InputSection from './InputSection'; +import { MAX_LENGTH, OWNER_NAME } from '@/constants/cardSection'; +import { RegisterFieldProps } from '@/types'; import { forwardRef } from 'react'; -import { MAX_LENGTH } from '../../constants/cardSection'; -import { OWNER_NAME } from '../../constants/cardSection'; -import { RegisterStepProps } from 'types'; -const RegisterName = forwardRef((props, ref) => { - const { onChange, value, onEnter, isError, onBlur } = props; +const RegisterName = forwardRef((props, ref) => { + const { onChange, value, onKeyDown, isError, onBlur } = props; + return ( @@ -22,7 +22,7 @@ const RegisterName = forwardRef((props, ref type="text" ref={ref} value={value} - onKeyDown={onEnter} + onKeyDown={onKeyDown} onBlur={onBlur} /> diff --git a/src/components/registerSection/RegisterPassword.tsx b/src/components/registerSection/RegisterPassword.tsx index 415c8cb1ea..77b240f03a 100644 --- a/src/components/registerSection/RegisterPassword.tsx +++ b/src/components/registerSection/RegisterPassword.tsx @@ -1,13 +1,12 @@ -import { forwardRef } from 'react'; import * as S from '../../app.style'; import InputSection from './InputSection'; -import { PASSWORD } from '../../constants/cardSection'; import Label from '../composables/Label'; import Input from '../composables/Input'; -import { MAX_LENGTH } from '../../constants/cardSection'; -import { RegisterStepProps } from 'types'; +import { MAX_LENGTH, PASSWORD } from '@/constants/cardSection'; +import { RegisterFieldProps } from '@/types'; +import { forwardRef } from 'react'; -const RegisterPassword = forwardRef>((props, ref) => { +const RegisterPassword = forwardRef>((props, ref) => { const { value, onChange, isError, onBlur } = props; return ( From 976bcf3bbe85f417345989dc607f72303f704e72 Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:03:22 +0900 Subject: [PATCH 30/57] =?UTF-8?q?refactor:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/cards/CreditCard.tsx | 11 +++---- src/components/cards/CvcCard.tsx | 11 +++---- src/components/cards/creditCard.style.ts | 37 ------------------------ src/components/cards/cvcCard.style.ts | 16 ++++++++++ src/components/cards/index.style.ts | 23 +++++++++++++++ 5 files changed, 51 insertions(+), 47 deletions(-) create mode 100644 src/components/cards/cvcCard.style.ts create mode 100644 src/components/cards/index.style.ts diff --git a/src/components/cards/CreditCard.tsx b/src/components/cards/CreditCard.tsx index 43cd4da6ec..734491615f 100644 --- a/src/components/cards/CreditCard.tsx +++ b/src/components/cards/CreditCard.tsx @@ -1,6 +1,7 @@ -import { InitialCardNumberState } from 'types'; +import { InitialCardNumberState } from '@/types'; import MasterCardImage from '../../assets/images/mastercard.png'; import VisaCardImage from '../../assets/images/visa.png'; +import * as C from './index.style'; import * as S from './creditCard.style'; type CreditCardProps = { @@ -26,8 +27,8 @@ export default function CreditCard({ cardBrand === 'MasterCard' ? MasterCardImage : cardBrand === 'Visa' ? VisaCardImage : ''; return ( - - + + @@ -60,7 +61,7 @@ export default function CreditCard({ {month + `${month || year ? DATE_SEPARATOR : ''}` + year} {name} - - + + ); } diff --git a/src/components/cards/CvcCard.tsx b/src/components/cards/CvcCard.tsx index 6b3c1596cd..969cd5b572 100644 --- a/src/components/cards/CvcCard.tsx +++ b/src/components/cards/CvcCard.tsx @@ -1,4 +1,5 @@ -import * as S from './creditCard.style'; +import * as S from './cvcCard.style'; +import * as C from './index.style'; type CvcCardProps = { cvc: string; @@ -6,12 +7,12 @@ type CvcCardProps = { export default function CvcCard({ cvc }: CvcCardProps) { return ( - - + + {cvc} - - + + ); } diff --git a/src/components/cards/creditCard.style.ts b/src/components/cards/creditCard.style.ts index 864663a83f..97e954fa05 100644 --- a/src/components/cards/creditCard.style.ts +++ b/src/components/cards/creditCard.style.ts @@ -1,27 +1,5 @@ import { styled } from 'styled-components'; -export const Container = styled.div` - display: flex; - justify-content: center; - align-items: center; - - margin-top: 50px; - margin-bottom: 45px; - - width: 100%; -`; - -export const CardContainer = styled.div<{ $backgroundColor: string; $padding: string }>` - background-color: ${(props) => - props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; - width: 212px; - height: 132px; - padding: ${(props) => (props.$padding ? props.$padding : '0')}; - border-radius: 4px; - position: relative; - box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); -`; - export const NumbersContainer = styled.div` display: flex; gap: 10px; @@ -81,18 +59,3 @@ export const CardInfoWrapper = styled.div` margin-top: 14px; margin-left: 5px; `; - -export const CvcNumberWrapper = styled.div<{ $backgroundColor: string; $padding: string }>` - background-color: ${(props) => - props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; - position: absolute; - bottom: 0; - width: 100%; - display: flex; - justify-content: end; - margin-bottom: 24px; -`; - -export const CvcNumberText = styled(Text)` - margin-right: 16px; -`; diff --git a/src/components/cards/cvcCard.style.ts b/src/components/cards/cvcCard.style.ts new file mode 100644 index 0000000000..872ee76ad5 --- /dev/null +++ b/src/components/cards/cvcCard.style.ts @@ -0,0 +1,16 @@ +import { styled } from 'styled-components'; + +export const CvcNumberWrapper = styled.div<{ $backgroundColor: string; $padding: string }>` + background-color: ${(props) => + props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; + position: absolute; + bottom: 0; + width: 100%; + display: flex; + justify-content: end; + margin-bottom: 24px; +`; + +export const CvcNumberText = styled.span` + margin-right: 16px; +`; diff --git a/src/components/cards/index.style.ts b/src/components/cards/index.style.ts new file mode 100644 index 0000000000..60865116c6 --- /dev/null +++ b/src/components/cards/index.style.ts @@ -0,0 +1,23 @@ +import { styled } from 'styled-components'; + +export const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + + margin-top: 50px; + margin-bottom: 45px; + + width: 100%; +`; + +export const CardContainer = styled.div<{ $backgroundColor: string; $padding: string }>` + background-color: ${(props) => + props.$backgroundColor !== '' ? props.$backgroundColor : '#333333'}; + width: 212px; + height: 132px; + padding: ${(props) => (props.$padding ? props.$padding : '0')}; + border-radius: 4px; + position: relative; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); +`; From a03d0b1c6406968896978f4700b8f439b9ee3c41 Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:03:39 +0900 Subject: [PATCH 31/57] =?UTF-8?q?refactor:=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tialCardIssuerInfo.ts => allCardIssuerInfo.ts} | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename src/constants/{initialCardIssuerInfo.ts => allCardIssuerInfo.ts} (74%) diff --git a/src/constants/initialCardIssuerInfo.ts b/src/constants/allCardIssuerInfo.ts similarity index 74% rename from src/constants/initialCardIssuerInfo.ts rename to src/constants/allCardIssuerInfo.ts index b60ec2410e..bd6a15e77e 100644 --- a/src/constants/initialCardIssuerInfo.ts +++ b/src/constants/allCardIssuerInfo.ts @@ -1,4 +1,13 @@ -const INITIAL_CARD_ISSUER_INFO = Object.freeze([ +import { AllCardIssuer, CardIssuerBackgroundColor } from '@/types'; + +type InitialCardIssuerInfo = { + id: number; + issuer: AllCardIssuer; + value: string; + backgroundColor: CardIssuerBackgroundColor; +}; + +const All_CARD_ISSUER_INFO: InitialCardIssuerInfo[] = [ { id: 1, issuer: 'BC카드', @@ -47,6 +56,6 @@ const INITIAL_CARD_ISSUER_INFO = Object.freeze([ value: 'kb', backgroundColor: 'rgba(106, 96, 86, 1)', }, -]); +]; -export default INITIAL_CARD_ISSUER_INFO; +export default All_CARD_ISSUER_INFO; From 0b86c017405c049b2d81e121023c327f3735008f Mon Sep 17 00:00:00 2001 From: brgndy Date: Sat, 27 Apr 2024 23:04:09 +0900 Subject: [PATCH 32/57] =?UTF-8?q?feat:=20=EC=B2=B4=ED=81=AC=20=EC=95=84?= =?UTF-8?q?=EC=9D=B4=EC=BD=98=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20svg=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../confirm/components/CheckIconImage.tsx | 22 +++++++++++++++++++ .../confirm/components/ConfirmImageIcon.tsx | 18 +++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/pages/confirm/components/CheckIconImage.tsx diff --git a/src/pages/confirm/components/CheckIconImage.tsx b/src/pages/confirm/components/CheckIconImage.tsx new file mode 100644 index 0000000000..6ef183ba01 --- /dev/null +++ b/src/pages/confirm/components/CheckIconImage.tsx @@ -0,0 +1,22 @@ +import { SVGProps } from 'react'; +import { JSX } from 'react/jsx-runtime'; + +const CheckIconImage = (props: JSX.IntrinsicAttributes & SVGProps) => ( + + + +); +export default CheckIconImage; diff --git a/src/pages/confirm/components/ConfirmImageIcon.tsx b/src/pages/confirm/components/ConfirmImageIcon.tsx index 5250bf6ce6..a7fc320cd5 100644 --- a/src/pages/confirm/components/ConfirmImageIcon.tsx +++ b/src/pages/confirm/components/ConfirmImageIcon.tsx @@ -1,11 +1,25 @@ import * as S from '../confirmPage.style'; -import CheckImageIcon from '../../../assets/images/complete.png'; +import CheckIconImage from './CheckIconImage'; export default function ConfirmImageIcon() { return ( - 체크 이모티콘 이미지 + + + ); From c9b777d4303e014dfd80cd6e1eaa7297ba6502fa Mon Sep 17 00:00:00 2001 From: brgndy Date: Sun, 28 Apr 2024 11:01:09 +0900 Subject: [PATCH 33/57] =?UTF-8?q?feat:=20useExpirationDate=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=ED=9B=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RegisterExpirationDate.tsx | 24 ++--- src/hooks/useExpirationDate.ts | 94 +++++++++++++++++++ src/utils/formatSingleDigitDate.ts | 5 + 3 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 src/hooks/useExpirationDate.ts create mode 100644 src/utils/formatSingleDigitDate.ts diff --git a/src/components/registerSection/RegisterExpirationDate.tsx b/src/components/registerSection/RegisterExpirationDate.tsx index 98c9dd3d70..f7225c2ad6 100644 --- a/src/components/registerSection/RegisterExpirationDate.tsx +++ b/src/components/registerSection/RegisterExpirationDate.tsx @@ -2,20 +2,20 @@ import * as S from '../../app.style'; import { MAX_LENGTH, EXPIRATION_PERIOD } from '@/constants/cardSection'; import { Input } from '../composables/input.style'; import Label from '../composables/Label'; -import InputSection from './InputSection'; -import { forwardRef, RefObject } from 'react'; +import InputSection from '../composables/InputSection'; +import { forwardRef, RefObject, useCallback, useRef } from 'react'; type RegisterExpirationDateProps = { month: string; - monthChangeHandler: React.ChangeEventHandler; + monthChangeHandler: ( + e: React.ChangeEvent, + nextRef: RefObject, + ) => void; monthError: boolean; - monthRef: RefObject; - handleMonthKeyDown: (e: React.KeyboardEvent) => void; handleMonthBlur: React.FocusEventHandler; year: string; yearChangeHandler: React.ChangeEventHandler; yearError: boolean; - yearRef: RefObject; handleYearKeyDown: (e: React.KeyboardEvent) => void; handleYearBlur: React.FocusEventHandler; }; @@ -26,17 +26,20 @@ const RegisterExpirationDate = forwardRef(null); + + const monthRef = useCallback((node: HTMLInputElement | null) => { + node?.focus(); + }, []); + return ( ) => monthChangeHandler(e, yearRef)} isError={monthError} ref={monthRef} - onKeyDown={handleMonthKeyDown} onBlur={handleMonthBlur} />