diff --git a/.eslintrc.js b/.eslintrc.js index 032b56dac..e97857e1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,7 +1,11 @@ module.exports = { root: true, env: { browser: true, es2020: true }, - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:storybook/recommended', + ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh'], diff --git a/package.json b/package.json index 8d4f7222e..d1759a38e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "test": "echo \"Error: no test specified\" && exit 1", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "lint": "eslint --fix --ext .js,.jsx,.ts,.tsx ." }, "keywords": [], "author": "", @@ -27,9 +28,8 @@ "@typescript-eslint/parser": "^7.0.2", "@vitejs/plugin-react-swc": "^3.6.0", "eslint": "^8.57.0", - "eslint-config-xo": "^0.44.0", - "eslint-config-xo-typescript": "^3.0.0", "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-storybook": "^0.8.0", "prettier": "^3.2.5", "typescript": "^5.3.3", diff --git a/src/App.tsx b/src/App.tsx index 867db8fce..cc8d639e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,25 +1,16 @@ -import CardList from './pages/card-list/CardList'; -import AddCard from './pages/card-add/AddCard'; -import CardRegisterComplete from './pages/card-register-complete/CardRegisterComplete'; +import Stepper from './pages/Stepper'; import CardInfoProvider from './provider/card-info-provider/CardInfoProvider'; -import MyCardsProvider from './provider/my-cards-provider/MyCardsProvider'; + import StepProvider from './provider/step-provider/StepProvider'; const App = () => (
- + - {(route) => ( - <> - {'LIST' === route && } - - {'CARD' === route && } - {'COMPLETE' === route && } - - - )} + - + +
); diff --git a/src/assets/question.svg b/src/assets/question.svg new file mode 100644 index 000000000..1cb52439f --- /dev/null +++ b/src/assets/question.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/card/Card.stories.tsx b/src/components/card/Card.stories.tsx index 934665bf6..77ecd0eca 100644 --- a/src/components/card/Card.stories.tsx +++ b/src/components/card/Card.stories.tsx @@ -1,6 +1,9 @@ -import { Meta, StoryObj } from '@storybook/react'; -import Card from './Card'; +import { type Meta, type StoryObj } from '@storybook/react'; + +import { Card } from '.'; + import '../../../styles/card.css'; + const meta = { title: 'Card', component: Card, diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 43226aaec..ad63c1df9 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -1,40 +1,48 @@ -import CardNumbers from './parts/CardNumbers'; +import { type CardBrand, type CardStateType } from '@/domain/type'; import CardBox from './parts/CardBox'; import CardForm from './parts/CardForm'; +import CardNumbers from './parts/CardNumbers'; import CardTitle from './parts/CardTitle'; -import { CardStateType } from '@/domain/type'; +import CardBottom from './parts/CardBottom'; import CardText from './parts/CardText'; import Chip from './parts/Chip'; -import CardBottom from './parts/CardBottom'; -interface CardProps extends CardStateType { +type CardProps = { status?: 'small' | 'big' | 'empty'; onClick?: () => void; -} + cardBrandName: string; +} & CardStateType & + CardBrand; -const Card = ({ +const REGEX = /[1-9]/gi; + +export const Card = ({ ownerName = 'NAME', month, year = '', cardNumbers, - status = 'small', + status = 'empty', + color, + cardBrandName, onClick, }: CardProps) => { + const cardNumber = `${cardNumbers?.first ?? ''} ${cardNumbers?.second ?? ''} ${cardNumbers?.third?.replace(REGEX, '*') ?? ''} ${cardNumbers?.fourth?.replace(REGEX, '*') ?? ''}`; + const displayMonth = month ? `${month} / ` : ''; const expirationDate = `${displayMonth}${year}`; return ( - + - 타이틀 + {cardBrandName}
- +
{ownerName} {expirationDate || 'MM/YY'} @@ -44,5 +52,3 @@ const Card = ({ ); }; - -export default Card; diff --git a/src/components/card/CardBox.tsx b/src/components/card/CardBox.tsx deleted file mode 100644 index c47402ef2..000000000 --- a/src/components/card/CardBox.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { PropsWithChildren } from 'react'; - -const CardBox = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -export default CardBox; diff --git a/src/components/card/CardForm.tsx b/src/components/card/CardForm.tsx deleted file mode 100644 index 582f9bddd..000000000 --- a/src/components/card/CardForm.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import classNames from 'classnames'; -import React, { ReactNode } from 'react'; - -const CardForm = ({ status, children }: { status: string; children: ReactNode }) => { - return
{children}
; -}; - -export default CardForm; diff --git a/src/components/card/CardNumbers.tsx b/src/components/card/CardNumbers.tsx deleted file mode 100644 index 7586814d9..000000000 --- a/src/components/card/CardNumbers.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; - -interface CardNumbers { - first: string; - second: string; - third: string; - fourth: string; -} -const CardNumbers = ({ first, second, third, fourth }: CardNumbers) => { - return ( -
-
- {first} -
-
- {second} -
-
- {third?.replace(/[1-9]/gi, '*')} -
-
- {fourth?.replace(/[1-9]/gi, '*')} -
-
- ); -}; - -export default CardNumbers; diff --git a/src/components/card/EmptyCard.tsx b/src/components/card/EmptyCard.tsx index 3b431970a..39bc14e1f 100644 --- a/src/components/card/EmptyCard.tsx +++ b/src/components/card/EmptyCard.tsx @@ -1,17 +1,14 @@ import CardBox from './parts/CardBox'; import CardForm from './parts/CardForm'; -interface EmptyCardProps { +type EmptyCardProps = { onClick: () => void; -} -const EmptyCard = ({ onClick }: EmptyCardProps) => { - return ( - - -
+
-
-
- ); }; -export default EmptyCard; +export const EmptyCard = ({ onClick }: EmptyCardProps) => ( + + +
+
+
+
+); diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 000000000..a405533d1 --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1,2 @@ +export { Card } from './Card'; +export { EmptyCard } from './EmptyCard'; diff --git a/src/components/card/parts/CardBottom.tsx b/src/components/card/parts/CardBottom.tsx index 0c1dc7135..387c6d491 100644 --- a/src/components/card/parts/CardBottom.tsx +++ b/src/components/card/parts/CardBottom.tsx @@ -1,7 +1,7 @@ -import { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -const CardBottom = ({ children }: PropsWithChildren) => { - return
{children}
; -}; +const CardBottom = ({ children }: PropsWithChildren) => ( +
{children}
+); export default CardBottom; diff --git a/src/components/card/parts/CardBox.tsx b/src/components/card/parts/CardBox.tsx index e38d72c63..f596fb006 100644 --- a/src/components/card/parts/CardBox.tsx +++ b/src/components/card/parts/CardBox.tsx @@ -1,14 +1,13 @@ -import { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -interface CardBoxProps extends PropsWithChildren { +type CardBoxProps = { onClick?: () => void; -} -const CardBox = ({ onClick, children }: CardBoxProps) => { - return ( -
- {children} -
- ); -}; +} & PropsWithChildren; + +const CardBox = ({ onClick, children }: CardBoxProps) => ( +
+ {children} +
+); export default CardBox; diff --git a/src/components/card/parts/CardForm.tsx b/src/components/card/parts/CardForm.tsx index 8ef0c6eb0..e8bc3ef8b 100644 --- a/src/components/card/parts/CardForm.tsx +++ b/src/components/card/parts/CardForm.tsx @@ -1,11 +1,14 @@ import classNames from 'classnames'; -import { PropsWithChildren } from 'react'; - -interface CardFormProps extends PropsWithChildren { +import { type HTMLAttributes, type PropsWithChildren } from 'react'; +type BaseCardFormProps = HTMLAttributes; +type CardFormProps = { status: string; -} -const CardForm = ({ status, children }: CardFormProps) => { - return
{children}
; -}; +} & PropsWithChildren; + +const CardForm = ({ status, children, style }: CardFormProps) => ( +
+ {children} +
+); export default CardForm; diff --git a/src/components/card/parts/CardNumbers.tsx b/src/components/card/parts/CardNumbers.tsx index 41c31ea99..686bfbf6e 100644 --- a/src/components/card/parts/CardNumbers.tsx +++ b/src/components/card/parts/CardNumbers.tsx @@ -1,24 +1,14 @@ -import { CardNumbersType } from '@/domain/type'; import CardText from './CardText'; -const REGEX = /[1-9]/gi; -interface CardNumbersProps extends CardNumbersType { +type CardNumbersProps = { status: 'big' | 'small' | 'empty'; -} -const CardNumbers = ({ - status, - first = '', - second = '', - third = '', - fourth = '', -}: CardNumbersProps) => { - return ( -
- {`${first} ${second} ${third.replace(REGEX, '*')} ${fourth.replace(REGEX, '*')}`} -
- ); + cardNumber: string; }; +const CardNumbers = ({ status, cardNumber }: CardNumbersProps) => ( +
+ {cardNumber} +
+); + export default CardNumbers; diff --git a/src/components/card/parts/CardText.tsx b/src/components/card/parts/CardText.tsx index 19c11ce59..a3389c42f 100644 --- a/src/components/card/parts/CardText.tsx +++ b/src/components/card/parts/CardText.tsx @@ -1,17 +1,14 @@ import classNames from 'classnames'; -import { DetailedHTMLProps, PropsWithChildren } from 'react'; +import { type HTMLAttributes, type PropsWithChildren } from 'react'; -type BaseCardTextProps = DetailedHTMLProps, HTMLSpanElement>; -interface CardTextProps extends BaseCardTextProps, PropsWithChildren { +type BaseCardTextProps = HTMLAttributes; + +type CardTextProps = { status: string; -} +} & PropsWithChildren; -const CardText = ({ status, children }: CardTextProps) => { - return ( - - {children} - - ); -}; +const CardText = ({ status, children }: CardTextProps) => ( + {children} +); export default CardText; diff --git a/src/components/card/parts/CardTitle.tsx b/src/components/card/parts/CardTitle.tsx index 154599326..29e29629e 100644 --- a/src/components/card/parts/CardTitle.tsx +++ b/src/components/card/parts/CardTitle.tsx @@ -1,7 +1,5 @@ -import { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -const CardTitle = ({ children }: PropsWithChildren) => { - return
{children}
; -}; +const CardTitle = ({ children }: PropsWithChildren) =>
{children}
; export default CardTitle; diff --git a/src/components/card/parts/Chip.tsx b/src/components/card/parts/Chip.tsx index 4a9b774ed..414f6fe22 100644 --- a/src/components/card/parts/Chip.tsx +++ b/src/components/card/parts/Chip.tsx @@ -1,10 +1,11 @@ import classNames from 'classnames'; -interface ChipProps { +type ChipProps = { status: 'small' | 'big' | 'empty'; -} -const Chip = ({ status }: ChipProps) => { - return
; }; +const Chip = ({ status }: ChipProps) => ( +
+); + export default Chip; diff --git a/src/components/common/button-box/ButtonBox.tsx b/src/components/common/button-box/ButtonBox.tsx index ff5cf5843..ad31d6ee6 100644 --- a/src/components/common/button-box/ButtonBox.tsx +++ b/src/components/common/button-box/ButtonBox.tsx @@ -1,11 +1,10 @@ import classNames from 'classnames'; -import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from 'react'; +import { type DetailedHTMLProps, type HTMLAttributes, type PropsWithChildren } from 'react'; type BaseButtonBox = DetailedHTMLProps, HTMLDivElement>; -interface ButtonBoxProps extends BaseButtonBox, PropsWithChildren {} -const ButtonBox = ({ className, children }: ButtonBoxProps) => { - return
{children}
; -}; +type ButtonBoxProps = Record & PropsWithChildren; -export default ButtonBox; +export const ButtonBox = ({ className, children }: ButtonBoxProps) => ( +
{children}
+); diff --git a/src/components/common/button/Button.stories.tsx b/src/components/common/button/Button.stories.tsx deleted file mode 100644 index ad2de3fe4..000000000 --- a/src/components/common/button/Button.stories.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import Button from './Button'; -import '../../../../styles/button.css'; - -const meta: Meta = { - title: 'Button', - component: Button, - // decorators: [ - // (Story) => ( - // - // - // - // ), - // ], -}; - -export default meta; - -type Story = StoryObj; - -export const DefaultButton: Story = { - args: { - className: 'button-text button-border-none', - children: '다음', - }, -}; diff --git a/src/components/common/button/Button.tsx b/src/components/common/button/Button.tsx deleted file mode 100644 index eb1f08907..000000000 --- a/src/components/common/button/Button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ButtonHTMLAttributes, DetailedHTMLProps, PropsWithChildren } from 'react'; - -type BaseButtonProps = DetailedHTMLProps< - ButtonHTMLAttributes, - HTMLButtonElement ->; - -interface ButtonProps extends BaseButtonProps, PropsWithChildren { - type: 'button' | 'submit' | 'reset'; -} -const Button = ({ type = 'button', className, children, ...props }: ButtonProps) => { - return ( - - ); -}; - -export default Button; diff --git a/src/components/common/flex-center/FlexCenter.tsx b/src/components/common/flex-center/FlexCenter.tsx index a5cfaf009..611e297b0 100644 --- a/src/components/common/flex-center/FlexCenter.tsx +++ b/src/components/common/flex-center/FlexCenter.tsx @@ -1,7 +1,5 @@ -import { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -const FlexCenter = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -export default FlexCenter; +export const FlexCenter = ({ children }: PropsWithChildren) => ( +
{children}
+); diff --git a/src/components/common/form/Form.tsx b/src/components/common/form/Form.tsx deleted file mode 100644 index b55542f38..000000000 --- a/src/components/common/form/Form.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { PropsWithChildren } from 'react'; - -const Form = ({ children }: PropsWithChildren) => { - return
{children}
; -}; - -export default Form; diff --git a/src/components/common/index.ts b/src/components/common/index.ts new file mode 100644 index 000000000..03d03243e --- /dev/null +++ b/src/components/common/index.ts @@ -0,0 +1,8 @@ +export { ButtonBox } from './button-box/ButtonBox'; +export { FlexCenter } from './flex-center/FlexCenter'; +export { Input } from './input/Input'; +export { InputBox } from './input-box/InputBox'; +export { Container } from './input-container/Container'; +export { Modal } from './modal/Modal'; +export { PageTitle } from './page-title/PageTitle'; +export { Tooltip } from './tooltip/Tooltip'; diff --git a/src/components/common/input-box/InputBox.tsx b/src/components/common/input-box/InputBox.tsx index a961b7578..deac7e43f 100644 --- a/src/components/common/input-box/InputBox.tsx +++ b/src/components/common/input-box/InputBox.tsx @@ -1,16 +1,12 @@ import classNames from 'classnames'; -import { DetailedHTMLProps, HTMLAttributes } from 'react'; +import { type HTMLAttributes } from 'react'; -type BaseInputBoxProps = DetailedHTMLProps, HTMLDivElement>; +type BaseInputBoxProps = HTMLAttributes; -interface InputBoxProps extends BaseInputBoxProps {} +type InputBoxProps = Record & BaseInputBoxProps; -const InputBox = ({ className, children, ...props }: InputBoxProps) => { - return ( -
- {children} -
- ); -}; - -export default InputBox; +export const InputBox = ({ className, children, ...props }: InputBoxProps) => ( +
+ {children} +
+); diff --git a/src/components/common/input-container/Container.tsx b/src/components/common/input-container/Container.tsx index bcd45ceb2..c6c0abb8b 100644 --- a/src/components/common/input-container/Container.tsx +++ b/src/components/common/input-container/Container.tsx @@ -1,24 +1,20 @@ import classNames from 'classnames'; -import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from 'react'; +import type { HTMLAttributes, PropsWithChildren } from 'react'; -type BaseContainerProps = DetailedHTMLProps, HTMLDivElement>; +type BaseContainerProps = HTMLAttributes; -interface ContainerProps extends BaseContainerProps, PropsWithChildren { +type ContainerProps = { title?: string; className?: string; -} +} & PropsWithChildren; -const Container = ({ title, className, children }: ContainerProps) => { - return ( -
- {title && ( - - {title} - - )} - {children} -
- ); -}; - -export default Container; +export const Container = ({ title, className, children }: ContainerProps) => ( +
+ {title ? ( + + {title} + + ) : null} + {children} +
+); diff --git a/src/components/common/input/Input.stories.tsx b/src/components/common/input/Input.stories.tsx index 926984211..24493c898 100644 --- a/src/components/common/input/Input.stories.tsx +++ b/src/components/common/input/Input.stories.tsx @@ -1,6 +1,7 @@ -import { Meta, StoryObj } from '@storybook/react'; -import Input from './Input'; +import { type Meta, type StoryObj } from '@storybook/react'; + import '../../../../styles/input.css'; +import { Input } from '..'; const meta: Meta = { title: 'Input', diff --git a/src/components/common/input/Input.tsx b/src/components/common/input/Input.tsx index a317902fd..b016f219b 100644 --- a/src/components/common/input/Input.tsx +++ b/src/components/common/input/Input.tsx @@ -1,20 +1,15 @@ import classNames from 'classnames'; -import { ForwardedRef, InputHTMLAttributes, forwardRef } from 'react'; +import { type InputHTMLAttributes, forwardRef } from 'react'; export type BaseInputProps = InputHTMLAttributes; -interface InputProps extends BaseInputProps { +type InputProps = { type: 'text' | 'password'; boxType?: 'input-basic' | 'input-underline'; -} +} & BaseInputProps; -const Input = forwardRef( - ( - { type = 'text', boxType = 'input-basic', className, ...props }: InputProps, - ref: ForwardedRef, - ) => { - return ; - }, +export const Input = forwardRef( + ({ type = 'text', boxType = 'input-basic', className, ...props }, ref) => ( + + ), ); - -export default Input; diff --git a/src/components/common/modal-portal/ModalPortal.tsx b/src/components/common/modal-portal/ModalPortal.tsx new file mode 100644 index 000000000..3a5156ede --- /dev/null +++ b/src/components/common/modal-portal/ModalPortal.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react'; +import ReactDom from 'react-dom'; + +const ModalPortal = ({ children }: PropsWithChildren) => { + const el = document.getElementById('modal') as HTMLElement; + return ReactDom.createPortal(children, el); +}; + +export default ModalPortal; diff --git a/src/components/common/modal/Modal.tsx b/src/components/common/modal/Modal.tsx index 263c78aad..7aa5c1133 100644 --- a/src/components/common/modal/Modal.tsx +++ b/src/components/common/modal/Modal.tsx @@ -1,30 +1,23 @@ -import FlexCenter from '../flex-center/FlexCenter'; -import ModalItemContainer from './parts/ModalItemContainer'; -import ModalItemDot from './parts/ModalItemDot'; -import ModalItemName from './parts/ModalItemName'; +import { type PropsWithChildren } from 'react'; +import ModalPortal from '../modal-portal/ModalPortal'; -const dummyData = [ - [{ name: '클린 카드' }, { name: '클린 카드' }, { name: '클린 카드' }, { name: '클린 카드' }], - [{ name: '클린 카드' }, { name: '클린 카드' }, { name: '클린 카드' }, { name: '클린 카드' }], -]; +type ModalProps = { + onToggle: () => void; + isOpen: boolean; +} & PropsWithChildren; -const Modal = () => { - return ( -
-
- {dummyData.map((item, i) => ( - - {item.map(({ name }, i) => ( - - - - - ))} - - ))} +export const Modal = ({ onToggle, isOpen, children }: ModalProps) => + isOpen ? ( + +
+
{ + e.stopPropagation(); + }} + > + {children} +
-
- ); -}; - -export default Modal; + + ) : null; diff --git a/src/components/common/modal/parts/ModalItemContainer.tsx b/src/components/common/modal/parts/ModalItemContainer.tsx index ee4504326..958a7cdac 100644 --- a/src/components/common/modal/parts/ModalItemContainer.tsx +++ b/src/components/common/modal/parts/ModalItemContainer.tsx @@ -1,7 +1,7 @@ -import React, { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -const ModalItemContainer = ({ children }: PropsWithChildren) => { - return
{children}
; -}; +const ModalItemContainer = ({ children }: PropsWithChildren) => ( +
{children}
+); export default ModalItemContainer; diff --git a/src/components/common/modal/parts/ModalItemDot.tsx b/src/components/common/modal/parts/ModalItemDot.tsx index b662e64a0..b0e88b100 100644 --- a/src/components/common/modal/parts/ModalItemDot.tsx +++ b/src/components/common/modal/parts/ModalItemDot.tsx @@ -1,7 +1,13 @@ -import React from 'react'; +import { type HTMLAttributes } from 'react'; -const ModalItemDot = () => { - return
; -}; +type BaseModalItemDot = HTMLAttributes; + +type ModalItemDotProps = { + onClick: () => void; +} & BaseModalItemDot; + +const ModalItemDot = ({ onClick, style }: ModalItemDotProps) => ( +
+); export default ModalItemDot; diff --git a/src/components/common/modal/parts/ModalItemName.tsx b/src/components/common/modal/parts/ModalItemName.tsx index e5dea2fb1..133378aad 100644 --- a/src/components/common/modal/parts/ModalItemName.tsx +++ b/src/components/common/modal/parts/ModalItemName.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -interface ModalItemNameProps { +type ModalItemNameProps = { name: string; -} -const ModalItemName = ({ name }: ModalItemNameProps) => { - return {name}; }; +const ModalItemName = ({ name }: ModalItemNameProps) => ( + {name} +); + export default ModalItemName; diff --git a/src/components/common/page-title/PageTitle.tsx b/src/components/common/page-title/PageTitle.tsx index 2aa403122..4f0d13db1 100644 --- a/src/components/common/page-title/PageTitle.tsx +++ b/src/components/common/page-title/PageTitle.tsx @@ -1,11 +1,12 @@ -import { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from 'react'; +import { type HTMLAttributes, type PropsWithChildren } from 'react'; import classNames from 'classnames'; -type BasePageTitleProps = DetailedHTMLProps, HTMLDivElement>; -interface PageTitleProps extends BasePageTitleProps, PropsWithChildren { + +type BasePageTitleProps = HTMLAttributes; + +type PageTitleProps = { className?: string; -} -const PageTitle = ({ className, children }: PageTitleProps) => { - return

{children}

; -}; +} & PropsWithChildren; -export default PageTitle; +export const PageTitle = ({ className, children }: PageTitleProps) => ( +

{children}

+); diff --git a/src/components/common/tooltip/Tooltip.tsx b/src/components/common/tooltip/Tooltip.tsx new file mode 100644 index 000000000..dc41aa030 --- /dev/null +++ b/src/components/common/tooltip/Tooltip.tsx @@ -0,0 +1,23 @@ +import { PropsWithChildren, useState } from 'react'; + +type TooltipType = { + content: string; +} & PropsWithChildren; + +export const Tooltip = ({ content, children }: TooltipType) => { + const [isShow, setIsShow] = useState(false); + const mouseEnter = () => { + setIsShow(true); + }; + + const mouseLeave = () => { + setIsShow(false); + }; + + return ( +
+ {children} + {isShow ?
{content}
: null} +
+ ); +}; diff --git a/src/domain/cardItem.ts b/src/domain/cardItem.ts new file mode 100644 index 000000000..cf101506a --- /dev/null +++ b/src/domain/cardItem.ts @@ -0,0 +1,18 @@ +export const CARD_COMPANY_LIST = [ + [ + { cardBrandName: '포코카드', color: 'red', pattern: ['1111', '2222'] }, + { cardBrandName: '준카드', color: 'blue', pattern: ['3333', '4444'] }, + { cardBrandName: '현석카드', color: 'green', pattern: ['5555', '6666'] }, + { cardBrandName: '윤호카드', color: 'hotpink', pattern: ['7777', '8888'] }, + ], + [ + { cardBrandName: '환오카드', color: '#94dacd', pattern: ['1234', '5678'] }, + { cardBrandName: '태은카드', color: 'pink', pattern: ['1212', '3434'] }, + { cardBrandName: '준일카드', color: 'orange', pattern: ['1313', '2323'] }, + { cardBrandName: '은규카드', color: 'yellow', pattern: ['1414', '2424'] }, + ], +]; + +// Export const findCardBrand = (label: string) => { +// return; +// }; diff --git a/src/domain/constant.ts b/src/domain/constant.ts index b19cf3a18..17de980bd 100644 --- a/src/domain/constant.ts +++ b/src/domain/constant.ts @@ -2,3 +2,9 @@ export const MIN_MONTH = 1; export const MAX_MONTH = 12; export const REGEX = /\D/g; + +export const CARD_EXPIRATION_DATE_LIMIT = 2; +export const CARD_NUMBER_LIMIT = 4; +export const CARD_OWNER_NAME_LIMIT = 30; +export const CARD_PASSWORD_LIMIT = 1; +export const CARD_SECURITY_CODE_LIMIT = 3; diff --git a/src/domain/type.ts b/src/domain/type.ts index e78f25291..ceff79e00 100644 --- a/src/domain/type.ts +++ b/src/domain/type.ts @@ -1,17 +1,26 @@ -export type CardNumbersType = { - first?: string; - second?: string; - third?: string; - fourth?: string; -}; +export type CardNumbersType = Partial<{ + first: string; + second: string; + third: string; + fourth: string; +}>; + +export type CardStateType = Partial<{ + id: number; + cardNumbers: CardNumbersType; + securityCode: string; + firstCardPassword: string; + secondCardPassword: string; + ownerName: string; + month: string; + year: string; + nickname: string; +}>; + +export type Route = 'CARD' | 'LIST' | 'COMPLETE'; -export type CardStateType = { - cardNumbers?: CardNumbersType; - securityCode?: string; - firstCardPassword?: string; - secondCardPassword?: string; - ownerName?: string; - month?: string; - year?: string; - nickname?: string; +export type CardBrand = { + cardBrandName: string; + color: string; + pattern: string[]; }; diff --git a/src/domain/validate.ts b/src/domain/validate.ts index 46b3a6ecb..109684c24 100644 --- a/src/domain/validate.ts +++ b/src/domain/validate.ts @@ -1,17 +1,23 @@ import { MAX_MONTH, MIN_MONTH } from './constant'; -export const validNumber = (value: string) => { - return /^(?:[1-9][0-9]{0,2}|)$/.test(value); -}; +export const isValidNumber = (value: string) => /^(?:[1-9][0-9]{0,2}|)$/.test(value); -export const validCardPassword = (value: string) => { - return /^(?:[1-9][0-9]{0,0}|)$/.test(value); -}; +export const isValidCardPassword = (value: string) => /^(?:[1-9][0-9]{0,0}|)$/.test(value); -export const validCardNumber = (value: string) => { - return /^(?:[1-9][0-9]{0,3}|)$/.test(value); -}; +export const isValidCardNumber = (value: string) => /^(?:[1-9][0-9]{0,3}|)$/.test(value); -export const validExpirationDate = (value: string) => { - return MIN_MONTH <= Number(value) && MAX_MONTH >= Number(value); -}; +export const isValidExpirationDate = (value: string) => + MIN_MONTH <= Number(value) && MAX_MONTH >= Number(value); + +export const isFailed = (state = '', limit: number) => state.length === limit; + +export const isLimitFailed = (state = '', limit: number) => state.length <= limit; + +export const isObjectFailed = (state: Record = {}, limit?: number) => + Object.values(state).every((item) => { + if (limit) { + return item && item.length === limit; + } + + return item; + }); diff --git a/src/pages/Stepper.tsx b/src/pages/Stepper.tsx new file mode 100644 index 000000000..ed817439f --- /dev/null +++ b/src/pages/Stepper.tsx @@ -0,0 +1,25 @@ +import ModalProvider from '@/provider/modal-provider/ModalProvider'; +import useStepContext from '@/provider/step-provider/hook/useStepContext'; +import { AddCard, CardList, CardRegisterComplete } from '.'; + +const StepperContent = { + LIST: , + CARD: ( + + + + ), + COMPLETE: ( + + + + ), +}; +const Stepper = () => { + const { route } = useStepContext(); + const Component = StepperContent[route] ?? null; + + return Component; +}; + +export default Stepper; diff --git a/src/pages/card-add/AddCard.tsx b/src/pages/card-add/AddCard.tsx index ec935c4aa..43312a4c7 100644 --- a/src/pages/card-add/AddCard.tsx +++ b/src/pages/card-add/AddCard.tsx @@ -1,25 +1,26 @@ -import Card from '@/components/card/Card'; -import PageTitle from '@/components/common/page-title/PageTitle'; -import { useContext } from 'react'; -import CardForm from './components/card-form/CardForm'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; +import useModalContext from '@/provider/modal-provider/hooks/useModalContext'; +import useStepContext from '@/provider/step-provider/hook/useStepContext'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import ModalProvider from '@/provider/modal-provider/ModalProvider'; -import { StepContext } from '@/provider/step-provider/StepProvider'; +import { Card } from '@/components/card'; +import { PageTitle } from '@/components/common'; +import CardForm from './components/card-form/CardForm'; const AddCard = () => { - const { cardState } = useContext(CardInfoContext); - const { navigate } = useContext(StepContext); + const { navigate } = useStepContext(); + const { cardState } = useCardContext(); + + const { cardBrand, toggle } = useModalContext(); + const goToPage = () => navigate('LIST'); + return (
- - -
{'< 카드 추가'}
-
- - -
+ +
{'< 카드 추가'}
+
+ +
); }; diff --git a/src/pages/card-add/components/card-form/CardForm.tsx b/src/pages/card-add/components/card-form/CardForm.tsx index e0c9bf4fc..fcef95308 100644 --- a/src/pages/card-add/components/card-form/CardForm.tsx +++ b/src/pages/card-add/components/card-form/CardForm.tsx @@ -1,47 +1,61 @@ -import Container from '@/components/common/input-container/Container'; import CardExpirationDate from './card-expiration-date/CardExpirationDate'; import CardNumbers from './card-numbers/CardNumbers'; import CardOwner from './card-owner/CardOwner'; import CardPassword from './card-password/CardPassword'; import CardSecurityCode from './card-security-code/CardSecurityCode'; -import InputBox from '@/components/common/input-box/InputBox'; -import ButtonBox from '@/components/common/button-box/ButtonBox'; +import { ButtonBox, Container, InputBox } from '@/components/common'; -import { StepContext } from '@/provider/step-provider/StepProvider'; -import { useContext } from 'react'; -import Button from '@/components/common/button/Button'; +import { isObjectFailed } from '@/domain/validate'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; +import useModalContext from '@/provider/modal-provider/hooks/useModalContext'; +import useStepContext from '@/provider/step-provider/hook/useStepContext'; +import useInputFocus from '../../hook/useInputFocus'; + +const REF_SIZE = 3; const CardForm = () => { - const { navigate } = useContext(StepContext); - // const { toggle } = useContext(ModalContext); + const { cardValidation } = useCardContext(); + const { navigate } = useStepContext(); + const { + cardBrand: { cardBrandName, color }, + } = useModalContext(); + + const { inputRef } = useInputFocus(REF_SIZE); + const [expirationDate, ownerName, password] = inputRef; + const goToPage = () => { - navigate('COMPLETE'); + const isValid = cardValidation(); + const isCardBrandVaild = isObjectFailed({ cardBrandName, color }); + if (isValid && isCardBrandVaild) { + navigate('COMPLETE'); + } }; + return ( <> - + - + - + - + - + - + ); diff --git a/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.stories.tsx b/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.stories.tsx new file mode 100644 index 000000000..7832338aa --- /dev/null +++ b/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import CardExpirationDate from './CardExpirationDate'; +import { Container, InputBox } from '@/components/common'; +import CardInfoProvider from '@/provider/card-info-provider/CardInfoProvider'; + +import '../../../../../../styles/input.css'; +const meta = { + title: 'CardExpirationDate', + component: CardExpirationDate, + decorators: [ + (Story) => ( +
+ + + + + + + +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const DefaultCardExpirationDate: Story = { + args: {}, +}; diff --git a/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.tsx b/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.tsx index 9a03d6a06..ef60e63e2 100644 --- a/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.tsx +++ b/src/pages/card-add/components/card-form/card-expiration-date/CardExpirationDate.tsx @@ -1,31 +1,40 @@ -import Input from '@/components/common/input/Input'; +import { Input } from '@/components/common'; +import { CARD_EXPIRATION_DATE_LIMIT } from '@/domain/constant'; +import { forwardRef, type RefObject } from 'react'; import useCardExpirationDate from './hook/useCardExpirationDate'; -const MAX_LENGTH = 2; +type CardExpirationDateProps = { + nextFocus: RefObject; +}; -const CardExpirationDate = () => { - const { month, year, handleChange } = useCardExpirationDate(); +const CardExpirationDate = forwardRef( + ({ nextFocus }, ref) => { + const { inputRef, month = '', year = '', handleChange } = useCardExpirationDate({ nextFocus }); + const [yearRef] = inputRef; - return ( - <> - - - - ); -}; + return ( + <> + + + + ); + }, +); export default CardExpirationDate; diff --git a/src/pages/card-add/components/card-form/card-expiration-date/hook/useCardExpirationDate.ts b/src/pages/card-add/components/card-form/card-expiration-date/hook/useCardExpirationDate.ts index 1a0b7c3ba..710540f71 100644 --- a/src/pages/card-add/components/card-form/card-expiration-date/hook/useCardExpirationDate.ts +++ b/src/pages/card-add/components/card-form/card-expiration-date/hook/useCardExpirationDate.ts @@ -1,13 +1,25 @@ -import { REGEX } from '@/domain/constant'; -import { validExpirationDate } from '@/domain/validate'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { ChangeEvent, useContext } from 'react'; +import { type RefObject, type ChangeEvent } from 'react'; -const useCardExpirationDate = () => { +import { isValidExpirationDate } from '@/domain/validate'; +import { CARD_EXPIRATION_DATE_LIMIT, REGEX } from '@/domain/constant'; + +import useInputFocus from '@/pages/card-add/hook/useInputFocus'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; + +const REF_SIZE = 1; + +type UseCardExpirationDate = { + nextFocus: RefObject; +}; + +const useCardExpirationDate = ({ nextFocus }: UseCardExpirationDate) => { const { cardState: { month, year }, handleCardState, - } = useContext(CardInfoContext); + } = useCardContext(); + + const { inputRef } = useInputFocus(REF_SIZE); + const [yearRef] = inputRef; const handleExpirationDate = (e: ChangeEvent) => { const { value, name } = e.target; @@ -17,16 +29,32 @@ const useCardExpirationDate = () => { return; } - if (name === 'month' && validExpirationDate(value)) { + if (name === 'month' && isValidExpirationDate(value)) { handleCardState({ [name]: value }); + if (value.length === CARD_EXPIRATION_DATE_LIMIT) { + yearRef.current?.focus(); + } + return; } if (name === 'year') { - if (!REGEX.test(value)) handleCardState({ [name]: value }); + if (!REGEX.test(value)) { + handleCardState({ [name]: value }); + } + + if (value.length === CARD_EXPIRATION_DATE_LIMIT) { + nextFocus?.current?.focus(); + } } }; - return { month, year, handleChange: handleExpirationDate }; + + return { + inputRef, + month, + year, + handleChange: handleExpirationDate, + }; }; export default useCardExpirationDate; diff --git a/src/pages/card-add/components/card-form/card-numbers/CardNumbers.stories.tsx b/src/pages/card-add/components/card-form/card-numbers/CardNumbers.stories.tsx new file mode 100644 index 000000000..aa39e9132 --- /dev/null +++ b/src/pages/card-add/components/card-form/card-numbers/CardNumbers.stories.tsx @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import CardNumbers from './CardNumbers'; +import { Container, InputBox } from '@/components/common'; +import CardInfoProvider from '@/provider/card-info-provider/CardInfoProvider'; + +import '../../../../../../styles/input.css'; + +const meta = { + title: 'CardNumbers', + component: CardNumbers, + decorators: [ + (Story) => ( +
+ + + + + + + +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const DefaultCardNumbers: Story = { + args: {}, +}; diff --git a/src/pages/card-add/components/card-form/card-numbers/CardNumbers.tsx b/src/pages/card-add/components/card-form/card-numbers/CardNumbers.tsx index 412e65367..b14b43d69 100644 --- a/src/pages/card-add/components/card-form/card-numbers/CardNumbers.tsx +++ b/src/pages/card-add/components/card-form/card-numbers/CardNumbers.tsx @@ -1,14 +1,21 @@ -import Input from '@/components/common/input/Input'; +import { Input } from '@/components/common'; +import { CARD_NUMBER_LIMIT } from '@/domain/constant'; +import { type RefObject } from 'react'; import useNumbers from './hook/useCardNumbers'; -const MAX_LENGTH = 4; +const createHyphen = (value: string = '') => (value.length === CARD_NUMBER_LIMIT ? '-' : ''); -export const createHyphen = (value: string) => { - return value && value.length === MAX_LENGTH && '-'; +type CardNumberProps = { + nextFocus: RefObject; }; -const CardNumbers = () => { - const { cardNumbers, handleChange } = useNumbers(); +const CardNumbers = ({ nextFocus }: CardNumberProps) => { + const { inputRef, cardNumbers, handleChange } = useNumbers({ nextFocus }); + const [second, third, fourth] = inputRef; + + const firstHypen = cardNumbers ? createHyphen(Object.values(cardNumbers)[0]) : ''; + const secondHypen = cardNumbers ? createHyphen(Object.values(cardNumbers)[1]) : ''; + const thridHypen = cardNumbers ? createHyphen(Object.values(cardNumbers)[2]) : ''; return ( <> @@ -16,32 +23,35 @@ const CardNumbers = () => { type="text" name="first" onChange={handleChange} - value={cardNumbers?.first} - maxLength={MAX_LENGTH} + value={cardNumbers?.first ?? ''} + maxLength={CARD_NUMBER_LIMIT} /> - {cardNumbers && createHyphen(Object.values(cardNumbers)[0])} + {firstHypen} - {cardNumbers && createHyphen(Object.values(cardNumbers)[1])} + {secondHypen} - {cardNumbers && createHyphen(Object.values(cardNumbers)[2])} + {thridHypen} ); diff --git a/src/pages/card-add/components/card-form/card-numbers/hook/useCardNumbers.ts b/src/pages/card-add/components/card-form/card-numbers/hook/useCardNumbers.ts index 626eb0d94..bb0009c93 100644 --- a/src/pages/card-add/components/card-form/card-numbers/hook/useCardNumbers.ts +++ b/src/pages/card-add/components/card-form/card-numbers/hook/useCardNumbers.ts @@ -1,21 +1,51 @@ -import { validCardNumber } from '@/domain/validate'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { ChangeEvent, useContext } from 'react'; +import { CARD_NUMBER_LIMIT } from '@/domain/constant'; +import { isValidCardNumber } from '@/domain/validate'; -const useNumbers = () => { +import useInputFocus from '@/pages/card-add/hook/useInputFocus'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; + +import { RefObject, type ChangeEvent } from 'react'; + +const REF_SIZE = 3; + +type UseNumbers = { + nextFocus: RefObject; +}; + +const useNumbers = ({ nextFocus }: UseNumbers) => { const { cardState: { cardNumbers }, handleCardState, - } = useContext(CardInfoContext); + } = useCardContext(); + + const { inputRef } = useInputFocus(REF_SIZE); + const [second, third, fourth] = inputRef; const handleCardNumbers = (e: ChangeEvent) => { const { name, value } = e.target; - if (validCardNumber(value)) { + + if (isValidCardNumber(value)) { handleCardState({ cardNumbers: { ...cardNumbers, [name]: value } }); + + if (name === 'first' && value.length === CARD_NUMBER_LIMIT) { + second.current?.focus(); + } + + if (name === 'second' && value.length === CARD_NUMBER_LIMIT) { + third.current?.focus(); + } + + if (name === 'third' && value.length === CARD_NUMBER_LIMIT) { + fourth.current?.focus(); + } + + if (name === 'fourth' && value.length === CARD_NUMBER_LIMIT) { + nextFocus?.current?.focus(); + } } }; - return { cardNumbers, handleChange: handleCardNumbers }; + return { inputRef, cardNumbers, handleChange: handleCardNumbers }; }; export default useNumbers; diff --git a/src/pages/card-add/components/card-form/card-owner/CardOwner.stories.tsx b/src/pages/card-add/components/card-form/card-owner/CardOwner.stories.tsx new file mode 100644 index 000000000..a4dd6390c --- /dev/null +++ b/src/pages/card-add/components/card-form/card-owner/CardOwner.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Container } from '@/components/common/index'; +import CardOwner from './CardOwner'; +import CardInfoProvider from '@/provider/card-info-provider/CardInfoProvider'; + +import '../../../../../../styles/input.css'; + +const meta = { + title: 'CardOnwer', + component: CardOwner, + decorators: [ + (Story) => ( +
+ + + + + +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const DefaultCardOnwer: Story = {}; diff --git a/src/pages/card-add/components/card-form/card-owner/CardOwner.tsx b/src/pages/card-add/components/card-form/card-owner/CardOwner.tsx index e3fd73196..da965018c 100644 --- a/src/pages/card-add/components/card-form/card-owner/CardOwner.tsx +++ b/src/pages/card-add/components/card-form/card-owner/CardOwner.tsx @@ -1,11 +1,10 @@ -import Input from '@/components/common/input/Input'; +import { Input } from '@/components/common'; +import { CARD_OWNER_NAME_LIMIT } from '@/domain/constant'; +import { forwardRef } from 'react'; import useCardOwner from './hook/useCardOwner'; -import { useRef } from 'react'; -const MAX_LENGTH = 30; -const CardOwner = () => { - const inputRef = useRef(null); - const { ownerName, handleChange } = useCardOwner(); +const CardOwner = forwardRef((_props, ref) => { + const { ownerName = '', handleChange } = useCardOwner(); return ( <> @@ -21,11 +20,11 @@ const CardOwner = () => { value={ownerName} onChange={handleChange} placeholder="카드에 표시된 이름과 동일하게 입력하세요." - maxLength={MAX_LENGTH} - ref={inputRef} + maxLength={CARD_OWNER_NAME_LIMIT} + ref={ref} /> ); -}; +}); export default CardOwner; diff --git a/src/pages/card-add/components/card-form/card-owner/hook/useCardOwner.ts b/src/pages/card-add/components/card-form/card-owner/hook/useCardOwner.ts index 7b401a312..ff3a4f70e 100644 --- a/src/pages/card-add/components/card-form/card-owner/hook/useCardOwner.ts +++ b/src/pages/card-add/components/card-form/card-owner/hook/useCardOwner.ts @@ -1,11 +1,13 @@ -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { ChangeEvent, useContext } from 'react'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; + +import { type ChangeEvent } from 'react'; const useCardOwner = () => { const { cardState: { ownerName }, handleCardState, - } = useContext(CardInfoContext); + } = useCardContext(); + const handleOwnerName = (e: ChangeEvent) => { const { name, value } = e.target; handleCardState({ [name]: value }); diff --git a/src/pages/card-add/components/card-form/card-password/CardInfoProvider.tsx b/src/pages/card-add/components/card-form/card-password/CardInfoProvider.tsx index efbe05af7..e8a4d9910 100644 --- a/src/pages/card-add/components/card-form/card-password/CardInfoProvider.tsx +++ b/src/pages/card-add/components/card-form/card-password/CardInfoProvider.tsx @@ -1,9 +1,9 @@ -import { CardStateType } from '@/provider/card-info-provider/CardInfoProvider'; +import { CardStateType } from '@/domain/type'; import { createContext, useState, type PropsWithChildren } from 'react'; interface CardInfoType { cardState: CardStateType; - handleCardState: (data: any) => void; + handleCardState: (data: CardStateType) => void; } const initialState: CardInfoType = { diff --git a/src/pages/card-add/components/card-form/card-password/CardPassword.stories.tsx b/src/pages/card-add/components/card-form/card-password/CardPassword.stories.tsx new file mode 100644 index 000000000..ef0058fbc --- /dev/null +++ b/src/pages/card-add/components/card-form/card-password/CardPassword.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Container } from '@/components/common'; +import CardPassword from './CardPassword'; +import CardInfoProvider from '@/provider/card-info-provider/CardInfoProvider'; + +import '../../../../../../styles/input.css'; +const meta = { + title: 'CardPassword', + component: CardPassword, + decorators: [ + (Story) => ( +
+ + +
+ +
+
+
+
+ ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const DefaultCardPassword: Story = {}; diff --git a/src/pages/card-add/components/card-form/card-password/CardPassword.tsx b/src/pages/card-add/components/card-form/card-password/CardPassword.tsx index 83133b3b1..d25df80a1 100644 --- a/src/pages/card-add/components/card-form/card-password/CardPassword.tsx +++ b/src/pages/card-add/components/card-form/card-password/CardPassword.tsx @@ -1,11 +1,16 @@ -import Input from '@/components/common/input/Input'; +import { Input } from '@/components/common'; import useCardPassword from './hook/useCardPassword'; +import { CARD_PASSWORD_LIMIT } from '@/domain/constant'; +import { forwardRef } from 'react'; -const MAX_LENGTH = 1; - -const CardPassword = () => { - const { firstCardPassword, secondCardPassword, handleChange } = useCardPassword(); - +const CardPassword = forwardRef((_props, ref) => { + const { + inputRef, + firstCardPassword = '', + secondCardPassword = '', + handleChange, + } = useCardPassword(); + const [second] = inputRef; return (
{ name="firstCardPassword" value={firstCardPassword} onChange={handleChange} - maxLength={MAX_LENGTH} + maxLength={CARD_PASSWORD_LIMIT} + ref={ref} /> { name="secondCardPassword" value={secondCardPassword} onChange={handleChange} - maxLength={MAX_LENGTH} + maxLength={CARD_PASSWORD_LIMIT} + ref={second} />
); -}; +}); export default CardPassword; diff --git a/src/pages/card-add/components/card-form/card-password/hook/useCardPassword.ts b/src/pages/card-add/components/card-form/card-password/hook/useCardPassword.ts index 7261a2f59..1f31db1cd 100644 --- a/src/pages/card-add/components/card-form/card-password/hook/useCardPassword.ts +++ b/src/pages/card-add/components/card-form/card-password/hook/useCardPassword.ts @@ -1,20 +1,38 @@ -import { validCardPassword } from '@/domain/validate'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { ChangeEvent, useContext } from 'react'; +import { CARD_PASSWORD_LIMIT } from '@/domain/constant'; +import { isValidCardPassword } from '@/domain/validate'; +import useInputFocus from '@/pages/card-add/hook/useInputFocus'; +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; + +import { type ChangeEvent } from 'react'; + +const REF_SIZE = 1; const useCardPassword = () => { const { cardState: { firstCardPassword, secondCardPassword }, handleCardState, - } = useContext(CardInfoContext); + } = useCardContext(); + + const { inputRef } = useInputFocus(REF_SIZE); + const [second] = inputRef; + const handleCardPassword = (e: ChangeEvent) => { const { name, value } = e.target; - if (validCardPassword(value)) { + if (isValidCardPassword(value)) { handleCardState({ [name]: value }); + if (name === 'firstCardPassword' && value.length === CARD_PASSWORD_LIMIT) { + second.current?.focus(); + } } }; - return { firstCardPassword, secondCardPassword, handleChange: handleCardPassword }; + + return { + inputRef, + firstCardPassword, + secondCardPassword, + handleChange: handleCardPassword, + }; }; export default useCardPassword; diff --git a/src/pages/card-add/components/card-form/card-security-code/CardSecurity.stories.tsx b/src/pages/card-add/components/card-form/card-security-code/CardSecurity.stories.tsx new file mode 100644 index 000000000..e7e123ee1 --- /dev/null +++ b/src/pages/card-add/components/card-form/card-security-code/CardSecurity.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { Container } from '@/components/common'; +import CardSecurityCode from './CardSecurityCode'; + +import CardInfoProvider from '@/provider/card-info-provider/CardInfoProvider'; + +import '../../../../../../styles/input.css'; +import '../../../../../../styles/utils.css'; +import '../../../../../../styles/tooltip.css'; + +const meta = { + title: 'CardSecurityCode', + component: CardSecurityCode, + decorators: [ + (Story) => ( +
+ + + + + +
+ ), + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const DefaultCardSecurityCode: Story = { + args: {}, +}; diff --git a/src/pages/card-add/components/card-form/card-security-code/CardSecurityCode.tsx b/src/pages/card-add/components/card-form/card-security-code/CardSecurityCode.tsx index f821e9541..04105ecc6 100644 --- a/src/pages/card-add/components/card-form/card-security-code/CardSecurityCode.tsx +++ b/src/pages/card-add/components/card-form/card-security-code/CardSecurityCode.tsx @@ -1,20 +1,43 @@ -import Input from '@/components/common/input/Input'; +import questionIcon from '@/assets/question.svg'; + +import { Input, Tooltip } from '@/components/common'; + +import { CARD_SECURITY_CODE_LIMIT } from '@/domain/constant'; + import useSecurityCode from './hook/useSecurityCode'; -const MAX_LENGTH = 3; +import { RefObject, forwardRef } from 'react'; -const CardSecurityCode = () => { - const { securityCode, handleScurityCode } = useSecurityCode(); - return ( - - ); +type CardSecurityCodeProps = { + nextFocus: RefObject; }; +const CardSecurityCode = forwardRef( + ({ nextFocus }, ref) => { + const { securityCode = '', handleScurityCode } = useSecurityCode({ nextFocus }); + + return ( +
+ + + Question Icon + +
+ ); + }, +); export default CardSecurityCode; diff --git a/src/pages/card-add/components/card-form/card-security-code/hook/useSecurityCode.ts b/src/pages/card-add/components/card-form/card-security-code/hook/useSecurityCode.ts index cd5a2f9f4..97d3fe80b 100644 --- a/src/pages/card-add/components/card-form/card-security-code/hook/useSecurityCode.ts +++ b/src/pages/card-add/components/card-form/card-security-code/hook/useSecurityCode.ts @@ -1,18 +1,26 @@ -import { validNumber } from '@/domain/validate'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { ChangeEvent, useContext } from 'react'; +import { isValidNumber } from '@/domain/validate'; -const useSecurityCode = () => { +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; + +import { RefObject, type ChangeEvent } from 'react'; + +type UseCardSecurityCode = { + nextFocus: RefObject; +}; + +const useSecurityCode = ({ nextFocus }: UseCardSecurityCode) => { const { cardState: { securityCode }, handleCardState, - } = useContext(CardInfoContext); + } = useCardContext(); const handleScurityCode = (e: ChangeEvent) => { const { name, value } = e.target; - - if (validNumber(value)) { + if (isValidNumber(value)) { handleCardState({ [name]: value }); + if (value.length === 3) { + nextFocus?.current?.focus(); + } } }; diff --git a/src/pages/card-add/hook/useInputFocus.ts b/src/pages/card-add/hook/useInputFocus.ts new file mode 100644 index 000000000..426d36131 --- /dev/null +++ b/src/pages/card-add/hook/useInputFocus.ts @@ -0,0 +1,11 @@ +import { createRef } from 'react'; + +const useInputFocus = (size: number) => { + const inputRef = Array(size) + .fill(null) + .map(() => createRef()); + + return { inputRef }; +}; + +export default useInputFocus; diff --git a/src/pages/card-list/CardList.tsx b/src/pages/card-list/CardList.tsx index 1995c47aa..67aa8e158 100644 --- a/src/pages/card-list/CardList.tsx +++ b/src/pages/card-list/CardList.tsx @@ -1,17 +1,18 @@ -import Card from '@/components/card/Card'; -import EmptyCard from '@/components/card/EmptyCard'; -import FlexCenter from '@/components/common/flex-center/FlexCenter'; -import PageTitle from '@/components/common/page-title/PageTitle'; -import { MyCardsContext } from '@/provider/my-cards-provider/MyCardsProvider'; -import { StepContext } from '@/provider/step-provider/StepProvider'; -import { useContext } from 'react'; +import { Card, EmptyCard } from '@/components/card'; +import { FlexCenter, PageTitle } from '@/components/common'; + +import { type Route } from '@/domain/type'; + +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; +import useStepContext from '@/provider/step-provider/hook/useStepContext'; const CardList = () => { - const { myCardList } = useContext(MyCardsContext); - const { navigate } = useContext(StepContext); - const goToPage = (path: string) => { + const { myCardList, handleCardState, removeCard } = useCardContext(); + const { navigate } = useStepContext(); + const goToPage = (path: Route) => { navigate(path); }; + return (
@@ -19,11 +20,23 @@ const CardList = () => { {myCardList.map((cardState, i) => ( <> - goToPage('COMPLETE')} {...cardState} /> + { + handleCardState(cardState); + removeCard(i); + goToPage('COMPLETE'); + }} + {...cardState} + /> {cardState.nickname} ))} - goToPage('CARD')} /> + { + goToPage('CARD'); + }} + />
); }; diff --git a/src/pages/card-register-complete/CardRegisterComplete.tsx b/src/pages/card-register-complete/CardRegisterComplete.tsx index fbc7eec1e..e923e8201 100644 --- a/src/pages/card-register-complete/CardRegisterComplete.tsx +++ b/src/pages/card-register-complete/CardRegisterComplete.tsx @@ -1,61 +1,44 @@ -import Card from '@/components/card/Card'; -import FlexCenter from '@/components/common/flex-center/FlexCenter'; -import Button from '@/components/common/button/Button'; -import Input from '@/components/common/input/Input'; -import PageTitle from '@/components/common/page-title/PageTitle'; -import { CardInfoContext } from '@/provider/card-info-provider/CardInfoProvider'; -import { MyCardsContext } from '@/provider/my-cards-provider/MyCardsProvider'; -import { StepContext } from '@/provider/step-provider/StepProvider'; -import { ChangeEvent, useContext } from 'react'; -import ButtonBox from '@/components/common/button-box/ButtonBox'; -import Container from '@/components/common/input-container/Container'; +import { Card } from '@/components/card'; +import { ButtonBox, FlexCenter, PageTitle } from '@/components/common'; +import CardNickname from './components/card-nickname/CardNickname'; + +import { isLimitFailed } from '@/domain/validate'; + +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; +import useModalContext from '@/provider/modal-provider/hooks/useModalContext'; +import useStepContext from '@/provider/step-provider/hook/useStepContext'; -const MAX_LENGTH = 10; const CardRegisterComplete = () => { - const { addCard } = useContext(MyCardsContext); - const { navigate } = useContext(StepContext); - const { cardState, handleCardState, reset } = useContext(CardInfoContext); + const { navigate } = useStepContext(); + const { cardState, reset, addCard } = useCardContext(); + const { cardBrand, resetCardBrand } = useModalContext(); + + const isNickNameValid = (nickname = '') => isLimitFailed(nickname, 10); const goToPage = () => { + const { nickname } = cardState; + const { cardBrandName } = cardBrand; + + const isVaild = isNickNameValid(nickname); + const cardDefaultNickname = isVaild ? nickname : cardBrandName; + + addCard({ ...cardState, ...cardBrand, nickname: cardDefaultNickname }); reset(); + resetCardBrand(); navigate('LIST'); }; - const handleChange = (e: ChangeEvent) => { - const { value, name } = e.target; - handleCardState({ [name]: value }); - }; return (
카드 등록이 완료되었습니다. - - - - + + - +
); diff --git a/src/pages/card-register-complete/components/card-nickname/CardNickname.tsx b/src/pages/card-register-complete/components/card-nickname/CardNickname.tsx new file mode 100644 index 000000000..2122dfadd --- /dev/null +++ b/src/pages/card-register-complete/components/card-nickname/CardNickname.tsx @@ -0,0 +1,24 @@ +import { Container, Input } from '@/components/common'; +import useCardNickName from '../../hook/useCardNickName'; + +const MAX_LENGTH = 10; +const CardNickname = () => { + const { nickname, handleChange } = useCardNickName(); + + return ( + + + + ); +}; + +export default CardNickname; diff --git a/src/pages/card-register-complete/hook/useCardNickName.ts b/src/pages/card-register-complete/hook/useCardNickName.ts new file mode 100644 index 000000000..8bbe25f02 --- /dev/null +++ b/src/pages/card-register-complete/hook/useCardNickName.ts @@ -0,0 +1,18 @@ +import useCardContext from '@/provider/card-info-provider/hooks/useCardContext'; +import { type ChangeEvent } from 'react'; + +const useCardNickName = () => { + const { + cardState: { nickname }, + handleCardState, + } = useCardContext(); + + const handleChange = (e: ChangeEvent) => { + const { value, name } = e.target; + handleCardState({ [name]: value }); + }; + + return { nickname, handleChange }; +}; + +export default useCardNickName; diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 000000000..1a9af06f6 --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,3 @@ +export {default as AddCard} from './card-add/AddCard'; +export {default as CardList} from './card-list/CardList'; +export {default as CardRegisterComplete} from './card-register-complete/CardRegisterComplete'; diff --git a/src/provider/CardInfoProvider.tsx b/src/provider/CardInfoProvider.tsx deleted file mode 100644 index c3d8cc0c7..000000000 --- a/src/provider/CardInfoProvider.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { createContext, useState, type PropsWithChildren } from 'react'; - -export type KeyIndex = { - [key: string]: string; -}; - -export type CardNumbersType = { - first?: string; - second?: string; - third?: string; - fourth?: string; -} & KeyIndex; - -export type CardStateType = { - cardNumbers?: CardNumbersType; - securityCode?: string; - firstCardPassword?: string; - secondCardPassword?: string; - ownerName?: string; - month?: string; - year?: string; -} & KeyIndex; - -interface CardInfoType { - cardState: CardStateType; - handleCardState: (data: any) => void; -} - -const initialState: CardInfoType = { - cardState: {}, - handleCardState: () => null, -}; - -export const CardInfoContext = createContext(initialState); - -const CardInfoProvider = ({ children }: PropsWithChildren) => { - const [cardState, setCardState] = useState({}); - - const handleCardState = (data: CardStateType) => { - setCardState((prev) => ({ ...prev, ...data })); - }; - - return ( - - {children} - - ); -}; - -export default CardInfoProvider; diff --git a/src/provider/card-info-provider/CardInfoProvider.tsx b/src/provider/card-info-provider/CardInfoProvider.tsx index 5356784d2..62b87134f 100644 --- a/src/provider/card-info-provider/CardInfoProvider.tsx +++ b/src/provider/card-info-provider/CardInfoProvider.tsx @@ -1,42 +1,81 @@ -import { CardStateType } from '@/domain/type'; -import { createContext, useState, type PropsWithChildren } from 'react'; +import { + CARD_EXPIRATION_DATE_LIMIT, + CARD_NUMBER_LIMIT, + CARD_OWNER_NAME_LIMIT, + CARD_PASSWORD_LIMIT, + CARD_SECURITY_CODE_LIMIT, +} from '@/domain/constant'; +import { type CardBrand, type CardStateType } from '@/domain/type'; +import { isFailed, isLimitFailed, isObjectFailed } from '@/domain/validate'; +import { createContext, type PropsWithChildren } from 'react'; +import useCardInfo from './hooks/useCardInfo'; +import useCardList from './hooks/useCardList'; -interface CardInfoType { +type CardListType = CardStateType & CardBrand; + +type MyCardListType = { + myCardList: CardListType[]; + addCard: (card: CardListType) => void; + removeCard: (i: number) => void; +}; + +type CardInfoType = { cardState: CardStateType; - handleCardState: (data: any) => void; + handleCardState: (data: CardStateType) => void; reset: () => void; -} + cardValidation: () => boolean; +} & MyCardListType; const initialContext: CardInfoType = { cardState: {}, + myCardList: [], handleCardState: () => undefined, + addCard: () => undefined, reset: () => undefined, -}; -const initialState = { - cardNumbers: { first: '', second: '', third: '', fourth: '' }, - securityCode: '', - firstCardPassword: '', - secondCardPassword: '', - ownerName: '', - month: '', - year: '', + cardValidation: () => false, + removeCard: () => undefined, }; export const CardInfoContext = createContext(initialContext); const CardInfoProvider = ({ children }: PropsWithChildren) => { - const [cardState, setCardState] = useState(initialState); + const { cardState, handleCardState, reset } = useCardInfo(); + const { myCardList, addCard, removeCard } = useCardList(); - const handleCardState = (data: CardStateType) => { - setCardState((prev) => ({ ...prev, ...data })); - }; + const cardValidation = () => { + const { + cardNumbers, + securityCode, + firstCardPassword, + secondCardPassword, + ownerName, + month, + year, + } = cardState; - const reset = () => { - setCardState(initialState); + return ( + isObjectFailed(cardNumbers, CARD_NUMBER_LIMIT) && + isFailed(securityCode, CARD_SECURITY_CODE_LIMIT) && + isFailed(firstCardPassword, CARD_PASSWORD_LIMIT) && + isFailed(secondCardPassword, CARD_PASSWORD_LIMIT) && + isLimitFailed(ownerName, CARD_OWNER_NAME_LIMIT) && + isFailed(month, CARD_EXPIRATION_DATE_LIMIT) && + isFailed(year, CARD_EXPIRATION_DATE_LIMIT) + ); }; return ( - + {children} ); diff --git a/src/provider/card-info-provider/hook/useCardContext.ts b/src/provider/card-info-provider/hooks/useCardContext.ts similarity index 73% rename from src/provider/card-info-provider/hook/useCardContext.ts rename to src/provider/card-info-provider/hooks/useCardContext.ts index b0cae86c5..b73d37e40 100644 --- a/src/provider/card-info-provider/hook/useCardContext.ts +++ b/src/provider/card-info-provider/hooks/useCardContext.ts @@ -4,8 +4,9 @@ import { CardInfoContext } from '../CardInfoProvider'; const useCardContext = () => { const cardInfoContext = useContext(CardInfoContext); if (!cardInfoContext) { - return null; + throw new Error('해당 Context는 CardInfoProvider 하위에서만 사용해주세요!'); } + return cardInfoContext; }; diff --git a/src/provider/card-info-provider/hooks/useCardInfo.ts b/src/provider/card-info-provider/hooks/useCardInfo.ts new file mode 100644 index 000000000..98a4938c4 --- /dev/null +++ b/src/provider/card-info-provider/hooks/useCardInfo.ts @@ -0,0 +1,15 @@ +import { CardStateType } from '@/domain/type'; +import { useCallback, useState } from 'react'; + +const useCardInfo = () => { + const [cardState, setCardState] = useState({}); + + const handleCardState = useCallback((data: CardStateType) => { + setCardState((prev) => ({ ...prev, ...data })); + }, []); + + const reset = () => setCardState({}); + + return { cardState, handleCardState, reset }; +}; +export default useCardInfo; diff --git a/src/provider/card-info-provider/hooks/useCardList.ts b/src/provider/card-info-provider/hooks/useCardList.ts new file mode 100644 index 000000000..df6786ffd --- /dev/null +++ b/src/provider/card-info-provider/hooks/useCardList.ts @@ -0,0 +1,15 @@ +import { CardBrand, CardStateType } from '@/domain/type'; +import { useState } from 'react'; + +type CardListType = CardStateType & CardBrand; + +const useCardList = () => { + const [myCardList, setMyCardList] = useState([]); + + const addCard = (card: CardListType) => setMyCardList((prev) => [...prev, card]); + + const removeCard = (i: number) => setMyCardList(myCardList.filter((_, idx) => idx !== i)); + + return { myCardList, addCard, removeCard }; +}; +export default useCardList; diff --git a/src/provider/modal-provider/ModalProvider.tsx b/src/provider/modal-provider/ModalProvider.tsx index 361b5f509..032cb95e7 100644 --- a/src/provider/modal-provider/ModalProvider.tsx +++ b/src/provider/modal-provider/ModalProvider.tsx @@ -1,26 +1,59 @@ -import Modal from '@/components/common/modal/Modal'; -import { PropsWithChildren, createContext, useState } from 'react'; +import { FlexCenter, Modal } from '@/components/common'; +import ModalItemContainer from '@/components/common/modal/parts/ModalItemContainer'; +import ModalItemDot from '@/components/common/modal/parts/ModalItemDot'; +import ModalItemName from '@/components/common/modal/parts/ModalItemName'; +import { CARD_COMPANY_LIST } from '@/domain/cardItem'; +import { type CardBrand } from '@/domain/type'; -interface ModalType { +import { createContext, type PropsWithChildren } from 'react'; +import useCardBrand from './hooks/useCardBrand'; +import useToggle from './hooks/useToggle'; + +type ModalType = { + cardBrand: CardBrand; toggle: () => void; -} + resetCardBrand: () => void; +}; const initialContext: ModalType = { - toggle: () => null, + cardBrand: { cardBrandName: '', color: '', pattern: [] }, + toggle: () => undefined, + resetCardBrand: () => undefined, }; export const ModalContext = createContext(initialContext); const ModalProvider = ({ children }: PropsWithChildren) => { - const [isOpen, setIsOpen] = useState(false); - const toggle = () => { - setIsOpen(true); + const { isOpen, toggle } = useToggle(); + const { cardBrand, handleCardBrand, resetCardBrand } = useCardBrand(); + + const clickCardDot = ({ cardBrandName, color, pattern }: CardBrand) => { + handleCardBrand({ cardBrandName, color, pattern }); + toggle(); }; return ( - + {children} - {isOpen ? : null} + {isOpen && ( + + {CARD_COMPANY_LIST.map((item, i) => ( + + {item.map(({ cardBrandName, color, pattern }, i) => ( + + { + clickCardDot({ cardBrandName, color, pattern }); + }} + /> + + + ))} + + ))} + + )} ); }; diff --git a/src/provider/modal-provider/hooks/useCardBrand.ts b/src/provider/modal-provider/hooks/useCardBrand.ts new file mode 100644 index 000000000..9b8ab80f3 --- /dev/null +++ b/src/provider/modal-provider/hooks/useCardBrand.ts @@ -0,0 +1,24 @@ +import { CardBrand } from '@/domain/type'; +import { useState } from 'react'; + +const initialState = { + cardBrandName: '', + color: '', + pattern: [], +}; + +const useCardBrand = () => { + const [cardBrand, setCardBrand] = useState({} as CardBrand); + + const handleCardBrand = ({ cardBrandName, color, pattern }: CardBrand) => { + setCardBrand({ cardBrandName, color, pattern }); + }; + + const resetCardBrand = () => { + setCardBrand(initialState); + }; + + return { cardBrand, handleCardBrand, resetCardBrand }; +}; + +export default useCardBrand; diff --git a/src/provider/modal-provider/hook/useModalContext.ts b/src/provider/modal-provider/hooks/useModalContext.ts similarity index 73% rename from src/provider/modal-provider/hook/useModalContext.ts rename to src/provider/modal-provider/hooks/useModalContext.ts index 2bdc6d715..b32489353 100644 --- a/src/provider/modal-provider/hook/useModalContext.ts +++ b/src/provider/modal-provider/hooks/useModalContext.ts @@ -4,8 +4,9 @@ import { ModalContext } from '../ModalProvider'; const useModalContext = () => { const modalContext = useContext(ModalContext); if (!modalContext) { - return null; + throw new Error('해당 Context는 ModalProvider 하위에서만 사용해주세요!'); } + return modalContext; }; diff --git a/src/provider/modal-provider/hooks/useToggle.ts b/src/provider/modal-provider/hooks/useToggle.ts new file mode 100644 index 000000000..a4a6350a5 --- /dev/null +++ b/src/provider/modal-provider/hooks/useToggle.ts @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +const useToggle = () => { + const [isOpen, setIsOpen] = useState(false); + + const toggle = () => setIsOpen(!isOpen); + + return { isOpen, toggle }; +}; +export default useToggle; diff --git a/src/provider/my-cards-provider/MyCardsProvider.tsx b/src/provider/my-cards-provider/MyCardsProvider.tsx deleted file mode 100644 index ad0eafda4..000000000 --- a/src/provider/my-cards-provider/MyCardsProvider.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { CardStateType } from '@/domain/type'; -import { PropsWithChildren, createContext, useState } from 'react'; - -interface MyCardListType { - myCardList: CardStateType[]; - addCard: (card: CardStateType) => void; -} - -const initialContext: MyCardListType = { - myCardList: [], - addCard: () => undefined, -}; - -export const MyCardsContext = createContext(initialContext); - -const MyCardsProvider = ({ children }: PropsWithChildren) => { - const [myCardList, setMyCardList] = useState([]); - const addCard = (card: CardStateType) => { - setMyCardList((prev) => [card, ...prev]); - }; - return ( - {children} - ); -}; - -export default MyCardsProvider; diff --git a/src/provider/my-cards-provider/hook/useMyCardsContext.ts b/src/provider/my-cards-provider/hook/useMyCardsContext.ts deleted file mode 100644 index c6737be83..000000000 --- a/src/provider/my-cards-provider/hook/useMyCardsContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from 'react'; -import { MyCardsContext } from '../MyCardsProvider'; - -const useMyCardsContext = () => { - const myCardsContext = useContext(MyCardsContext); - if (!myCardsContext) { - return null; - } - return myCardsContext; -}; - -export default useMyCardsContext; diff --git a/src/provider/step-provider/StepProvider.tsx b/src/provider/step-provider/StepProvider.tsx index 45051fdd8..330bb4991 100644 --- a/src/provider/step-provider/StepProvider.tsx +++ b/src/provider/step-provider/StepProvider.tsx @@ -1,22 +1,26 @@ -import { ReactNode, createContext, useState } from 'react'; +import { type Route } from '@/domain/type'; +import { type PropsWithChildren, createContext, useState, useCallback } from 'react'; + +type StepType = { + route: Route; + navigate: (path: Route) => void; +}; -interface StepType { - route: string; - navigate: (path: string) => void; -} const initialState: StepType = { route: 'CARD', navigate: () => undefined, }; -interface StepProviderProps { - children: (route: string) => ReactNode; -} + export const StepContext = createContext(initialState); -const StepProvider = ({ children }: StepProviderProps) => { - const [route, setRoute] = useState('CARD'); - const navigate = (path: string) => setRoute(path); - return {children(route)}; +const StepProvider = ({ children }: PropsWithChildren) => { + const [route, setRoute] = useState('CARD'); + + const navigate = useCallback((path: Route) => { + setRoute(path); + }, []); + + return {children}; }; export default StepProvider; diff --git a/src/provider/step-provider/hook/useStepContext.ts b/src/provider/step-provider/hook/useStepContext.ts index d78b4355c..1921726c5 100644 --- a/src/provider/step-provider/hook/useStepContext.ts +++ b/src/provider/step-provider/hook/useStepContext.ts @@ -4,8 +4,9 @@ import { StepContext } from '../StepProvider'; const useStepContext = () => { const stepContext = useContext(StepContext); if (!stepContext) { - return null; + throw new Error('해당 Context는 StepProvider 하위에서만 사용해주세요!'); } + return stepContext; }; diff --git a/src/provider/step-provider/useStepContext.ts b/src/provider/step-provider/useStepContext.ts deleted file mode 100644 index 97d939cba..000000000 --- a/src/provider/step-provider/useStepContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import React, { useContext } from 'react'; -import { StepContext } from './StepProvider'; - -const useStepContext = () => { - return useContext(StepContext); -}; - -export default useStepContext; diff --git a/src/provider/useCardContext.ts b/src/provider/useCardContext.ts deleted file mode 100644 index 0609f33de..000000000 --- a/src/provider/useCardContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { useContext } from 'react'; -import { CardInfoContext } from './CardInfoProvider'; - -const useCardContext = () => { - return useContext(CardInfoContext); -}; - -export default useCardContext; diff --git a/src/stories/Button.stories.ts b/src/stories/Button.stories.ts deleted file mode 100644 index 742c3aa7b..000000000 --- a/src/stories/Button.stories.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Button } from './Button'; - -// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export -const meta = { - title: 'Example/Button', - component: Button, - parameters: { - // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout - layout: 'centered', - }, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - // More on argTypes: https://storybook.js.org/docs/api/argtypes - argTypes: { - backgroundColor: { control: 'color' }, - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args -export const Primary: Story = { - args: { - primary: true, - label: 'Button', - }, -}; - -export const Secondary: Story = { - args: { - label: 'Button', - }, -}; - -export const Large: Story = { - args: { - size: 'large', - label: 'Button', - }, -}; - -export const Small: Story = { - args: { - size: 'small', - label: 'Button', - }, -}; diff --git a/src/stories/Button.tsx b/src/stories/Button.tsx deleted file mode 100644 index c33be6ec5..000000000 --- a/src/stories/Button.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import './button.css'; - -interface ButtonProps { - /** - * Is this the principal call to action on the page? - */ - primary?: boolean; - /** - * What background color to use - */ - backgroundColor?: string; - /** - * How large should the button be? - */ - size?: 'small' | 'medium' | 'large'; - /** - * Button contents - */ - label: string; - /** - * Optional click handler - */ - onClick?: () => void; -} - -/** - * Primary UI component for user interaction - */ -export const Button = ({ - primary = false, - size = 'medium', - backgroundColor, - label, - ...props -}: ButtonProps) => { - const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; - return ( - - ); -}; diff --git a/src/stories/Configure.mdx b/src/stories/Configure.mdx deleted file mode 100644 index 51570900c..000000000 --- a/src/stories/Configure.mdx +++ /dev/null @@ -1,364 +0,0 @@ -import { Meta } from "@storybook/blocks"; - -import Github from "./assets/github.svg"; -import Discord from "./assets/discord.svg"; -import Youtube from "./assets/youtube.svg"; -import Tutorials from "./assets/tutorials.svg"; -import Styling from "./assets/styling.png"; -import Context from "./assets/context.png"; -import Assets from "./assets/assets.png"; -import Docs from "./assets/docs.png"; -import Share from "./assets/share.png"; -import FigmaPlugin from "./assets/figma-plugin.png"; -import Testing from "./assets/testing.png"; -import Accessibility from "./assets/accessibility.png"; -import Theming from "./assets/theming.png"; -import AddonLibrary from "./assets/addon-library.png"; - -export const RightArrow = () => - - - - - -
-
- # Configure your project - - Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community. -
-
-
- A wall of logos representing different styling technologies -

Add styling and CSS

-

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

- Learn more -
-
- An abstraction representing the composition of data for a component -

Provide context and mocking

-

Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.

- Learn more -
-
- A representation of typography and image assets -
-

Load assets and resources

-

To link static files (like fonts) to your projects and stories, use the - `staticDirs` configuration option to specify folders to load when - starting Storybook.

- Learn more -
-
-
-
-
-
- # Do more with Storybook - - Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs. -
- -
-
-
- A screenshot showing the autodocs tag being set, pointing a docs page being generated -

Autodocs

-

Auto-generate living, - interactive reference documentation from your components and stories.

- Learn more -
-
- A browser window showing a Storybook being published to a chromatic.com URL -

Publish to Chromatic

-

Publish your Storybook to review and collaborate with your entire team.

- Learn more -
-
- Windows showing the Storybook plugin in Figma -

Figma Plugin

-

Embed your stories into Figma to cross-reference the design and live - implementation in one place.

- Learn more -
-
- Screenshot of tests passing and failing -

Testing

-

Use stories to test a component in all its variations, no matter how - complex.

- Learn more -
-
- Screenshot of accessibility tests passing and failing -

Accessibility

-

Automatically test your components for a11y issues as you develop.

- Learn more -
-
- Screenshot of Storybook in light and dark mode -

Theming

-

Theme Storybook's UI to personalize it to your project.

- Learn more -
-
-
-
-
-
-

Addons

-

Integrate your tools with Storybook to connect workflows.

- Discover all addons -
-
- Integrate your tools with Storybook to connect workflows. -
-
- -
-
- Github logo - Join our contributors building the future of UI development. - - Star on GitHub -
-
- Discord logo -
- Get support and chat with frontend developers. - - Join Discord server -
-
-
- Youtube logo -
- Watch tutorials, feature previews and interviews. - - Watch on YouTube -
-
-
- A book -

Follow guided walkthroughs on for key workflows.

- - Discover tutorials -
-
- - diff --git a/src/stories/Header.stories.ts b/src/stories/Header.stories.ts deleted file mode 100644 index 61cf98aa0..000000000 --- a/src/stories/Header.stories.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import { Header } from './Header'; - -const meta = { - title: 'Example/Header', - component: Header, - // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs - tags: ['autodocs'], - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedIn: Story = { - args: { - user: { - name: 'Jane Doe', - }, - }, -}; - -export const LoggedOut: Story = {}; diff --git a/src/stories/Header.tsx b/src/stories/Header.tsx deleted file mode 100644 index 015046013..000000000 --- a/src/stories/Header.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react'; - -import { Button } from './Button'; -import './header.css'; - -type User = { - name: string; -}; - -interface HeaderProps { - user?: User; - onLogin: () => void; - onLogout: () => void; - onCreateAccount: () => void; -} - -export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( -
-
-
- - - - - - - -

Acme

-
-
- {user ? ( - <> - - Welcome, {user.name}! - -
-
-
-); diff --git a/src/stories/Page.stories.ts b/src/stories/Page.stories.ts deleted file mode 100644 index f7a06817f..000000000 --- a/src/stories/Page.stories.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { within, userEvent, expect } from '@storybook/test'; - -import { Page } from './Page'; - -const meta = { - title: 'Example/Page', - component: Page, - parameters: { - // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout - layout: 'fullscreen', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const LoggedOut: Story = {}; - -// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing -export const LoggedIn: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const loginButton = canvas.getByRole('button', { name: /Log in/i }); - await expect(loginButton).toBeInTheDocument(); - await userEvent.click(loginButton); - await expect(loginButton).not.toBeInTheDocument(); - - const logoutButton = canvas.getByRole('button', { name: /Log out/i }); - await expect(logoutButton).toBeInTheDocument(); - }, -}; diff --git a/src/stories/Page.tsx b/src/stories/Page.tsx deleted file mode 100644 index e11748301..000000000 --- a/src/stories/Page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import { Header } from './Header'; -import './page.css'; - -type User = { - name: string; -}; - -export const Page: React.FC = () => { - const [user, setUser] = React.useState(); - - return ( -
-
setUser({ name: 'Jane Doe' })} - onLogout={() => setUser(undefined)} - onCreateAccount={() => setUser({ name: 'Jane Doe' })} - /> - -
-

Pages in Storybook

-

- We recommend building UIs with a{' '} - - component-driven - {' '} - process starting with atomic components and ending with pages. -

-

- Render pages with mock data. This makes it easy to build and review page states without - needing to navigate to them in your app. Here are some handy patterns for managing page - data in Storybook: -

-
    -
  • - Use a higher-level connected component. Storybook helps you compose such data from the - "args" of child component stories -
  • -
  • - Assemble data in the page component from your services. You can mock these services out - using Storybook. -
  • -
-

- Get a guided tutorial on component-driven development at{' '} - - Storybook tutorials - - . Read more in the{' '} - - docs - - . -

-
- Tip Adjust the width of the canvas with the{' '} - - - - - - Viewports addon in the toolbar -
-
-
- ); -}; diff --git a/src/stories/assets/accessibility.png b/src/stories/assets/accessibility.png deleted file mode 100644 index 6ffe6feab..000000000 Binary files a/src/stories/assets/accessibility.png and /dev/null differ diff --git a/src/stories/assets/accessibility.svg b/src/stories/assets/accessibility.svg deleted file mode 100644 index a3288830e..000000000 --- a/src/stories/assets/accessibility.svg +++ /dev/null @@ -1,5 +0,0 @@ - - Accessibility - - - \ No newline at end of file diff --git a/src/stories/assets/addon-library.png b/src/stories/assets/addon-library.png deleted file mode 100644 index 95deb38a8..000000000 Binary files a/src/stories/assets/addon-library.png and /dev/null differ diff --git a/src/stories/assets/assets.png b/src/stories/assets/assets.png deleted file mode 100644 index cfba6817a..000000000 Binary files a/src/stories/assets/assets.png and /dev/null differ diff --git a/src/stories/assets/avif-test-image.avif b/src/stories/assets/avif-test-image.avif deleted file mode 100644 index 530709bc1..000000000 Binary files a/src/stories/assets/avif-test-image.avif and /dev/null differ diff --git a/src/stories/assets/context.png b/src/stories/assets/context.png deleted file mode 100644 index e5cd249a2..000000000 Binary files a/src/stories/assets/context.png and /dev/null differ diff --git a/src/stories/assets/discord.svg b/src/stories/assets/discord.svg deleted file mode 100644 index 1204df998..000000000 --- a/src/stories/assets/discord.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/src/stories/assets/docs.png b/src/stories/assets/docs.png deleted file mode 100644 index a749629df..000000000 Binary files a/src/stories/assets/docs.png and /dev/null differ diff --git a/src/stories/assets/figma-plugin.png b/src/stories/assets/figma-plugin.png deleted file mode 100644 index 8f79b08cd..000000000 Binary files a/src/stories/assets/figma-plugin.png and /dev/null differ diff --git a/src/stories/assets/github.svg b/src/stories/assets/github.svg deleted file mode 100644 index 158e0268a..000000000 --- a/src/stories/assets/github.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/stories/assets/share.png b/src/stories/assets/share.png deleted file mode 100644 index 8097a3707..000000000 Binary files a/src/stories/assets/share.png and /dev/null differ diff --git a/src/stories/assets/styling.png b/src/stories/assets/styling.png deleted file mode 100644 index d341e8263..000000000 Binary files a/src/stories/assets/styling.png and /dev/null differ diff --git a/src/stories/assets/testing.png b/src/stories/assets/testing.png deleted file mode 100644 index d4ac39a0c..000000000 Binary files a/src/stories/assets/testing.png and /dev/null differ diff --git a/src/stories/assets/theming.png b/src/stories/assets/theming.png deleted file mode 100644 index 1535eb9b8..000000000 Binary files a/src/stories/assets/theming.png and /dev/null differ diff --git a/src/stories/assets/tutorials.svg b/src/stories/assets/tutorials.svg deleted file mode 100644 index 4b2fc7c44..000000000 --- a/src/stories/assets/tutorials.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/stories/assets/youtube.svg b/src/stories/assets/youtube.svg deleted file mode 100644 index 33a3a61f6..000000000 --- a/src/stories/assets/youtube.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/stories/button.css b/src/stories/button.css deleted file mode 100644 index dc91dc763..000000000 --- a/src/stories/button.css +++ /dev/null @@ -1,30 +0,0 @@ -.storybook-button { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-weight: 700; - border: 0; - border-radius: 3em; - cursor: pointer; - display: inline-block; - line-height: 1; -} -.storybook-button--primary { - color: white; - background-color: #1ea7fd; -} -.storybook-button--secondary { - color: #333; - background-color: transparent; - box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; -} -.storybook-button--small { - font-size: 12px; - padding: 10px 16px; -} -.storybook-button--medium { - font-size: 14px; - padding: 11px 20px; -} -.storybook-button--large { - font-size: 16px; - padding: 12px 24px; -} diff --git a/src/stories/header.css b/src/stories/header.css deleted file mode 100644 index d9a70528a..000000000 --- a/src/stories/header.css +++ /dev/null @@ -1,32 +0,0 @@ -.storybook-header { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - padding: 15px 20px; - display: flex; - align-items: center; - justify-content: space-between; -} - -.storybook-header svg { - display: inline-block; - vertical-align: top; -} - -.storybook-header h1 { - font-weight: 700; - font-size: 20px; - line-height: 1; - margin: 6px 0 6px 10px; - display: inline-block; - vertical-align: top; -} - -.storybook-header button + button { - margin-left: 10px; -} - -.storybook-header .welcome { - color: #333; - font-size: 14px; - margin-right: 10px; -} diff --git a/src/stories/page.css b/src/stories/page.css deleted file mode 100644 index 098dad118..000000000 --- a/src/stories/page.css +++ /dev/null @@ -1,69 +0,0 @@ -.storybook-page { - font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 24px; - padding: 48px 20px; - margin: 0 auto; - max-width: 600px; - color: #333; -} - -.storybook-page h2 { - font-weight: 700; - font-size: 32px; - line-height: 1; - margin: 0 0 4px; - display: inline-block; - vertical-align: top; -} - -.storybook-page p { - margin: 1em 0; -} - -.storybook-page a { - text-decoration: none; - color: #1ea7fd; -} - -.storybook-page ul { - padding-left: 30px; - margin: 1em 0; -} - -.storybook-page li { - margin-bottom: 8px; -} - -.storybook-page .tip { - display: inline-block; - border-radius: 1em; - font-size: 11px; - line-height: 12px; - font-weight: 700; - background: #e7fdd8; - color: #66bf3c; - padding: 4px 12px; - margin-right: 10px; - vertical-align: top; -} - -.storybook-page .tip-wrapper { - font-size: 13px; - line-height: 20px; - margin-top: 40px; - margin-bottom: 40px; -} - -.storybook-page .tip-wrapper svg { - display: inline-block; - height: 12px; - width: 12px; - margin-right: 4px; - vertical-align: top; - margin-top: 3px; -} - -.storybook-page .tip-wrapper svg path { - fill: #1ea7fd; -} diff --git a/styles/tooltip.css b/styles/tooltip.css new file mode 100644 index 000000000..5c2325c2c --- /dev/null +++ b/styles/tooltip.css @@ -0,0 +1,19 @@ +.custom-tooltip-container { + position: relative; + display: inline-block; +} + +.custom-tooltip { + visibility: visible; + width: 120px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + margin-left: -60px; +} diff --git a/styles/utils.css b/styles/utils.css index e86f525ff..76162e274 100644 --- a/styles/utils.css +++ b/styles/utils.css @@ -54,3 +54,7 @@ .w-15 { width: 15%; } + +.gap-5 { + gap: 5px; +} diff --git a/vite.config.ts b/vite.config.ts index 8a24b8835..72620c456 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,10 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], resolve: { - alias: [{ find: "@", replacement: "/src" }], + alias: [{ find: '@', replacement: '/src' }], }, }); diff --git a/yarn.lock b/yarn.lock index 32a0cf775..f556e98df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3872,11 +3872,6 @@ concat-stream@^1.6.2: readable-stream "^2.2.2" typedarray "^0.0.6" -confusing-browser-globals@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" - integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== - consola@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" @@ -4434,17 +4429,10 @@ escodegen@^2.1.0: optionalDependencies: source-map "~0.6.1" -eslint-config-xo-typescript@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-xo-typescript/-/eslint-config-xo-typescript-3.0.0.tgz#d6c36a9057eeebfb4f76d56f60d98a0bbd919832" - integrity sha512-8C1EFMHBHMxnFuY+0gSLrnOfSvGG766L85EEFYNqEfO7XmfAVvEKUDgLMwDQ34Dq/bEpOSvVx8gkN9Owx3iKEw== - -eslint-config-xo@^0.44.0: - version "0.44.0" - resolved "https://registry.yarnpkg.com/eslint-config-xo/-/eslint-config-xo-0.44.0.tgz#b4a68da791ecfd329bc7e1f88f9edea3d4dca70c" - integrity sha512-YG4gdaor0mJJi8UBeRJqDPO42MedTWYMaUyucF5bhm2pi/HS98JIxfFQmTLuyj6hGpQlAazNfyVnn7JuDn+Sew== - dependencies: - confusing-browser-globals "1.0.11" +eslint-plugin-react-refresh@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.6.tgz#e8e8accab681861baed00c5c12da70267db0936f" + integrity sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA== eslint-plugin-react@^7.33.2: version "7.33.2"