From eac1107d28cd9b06ea48fc5aff137ed416e6a4f6 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:22:13 +0900 Subject: [PATCH 01/40] =?UTF-8?q?style:=20prettier=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Placeholder Author --- .prettierrc | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..acb462469b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "semi": true, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "auto" +} From a2eaf720cab13aa1b97e8620a06009ed627f1940 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:55:24 +0900 Subject: [PATCH 02/40] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- README.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d917806e0..0460dfa928 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# react-payments +# ๐Ÿ’ณ ํŽ˜์ด๋จผ์ธ  + +ํŽ˜์ด๋จผ์ธ  ์‚ฌ์šฉ์ž์˜ ์นด๋“œ ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ํ•ด๋‹น ์นด๋“œ๋ฅผ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋“ฑ๋กํ•œ๋‹ค. + +### ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ ์‹๋ณ„ + +- ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ์•…ํ•˜์—ฌ, Visa๋‚˜ MasterCard์— ํ•ด๋‹นํ•˜๋ฉด ํ•ด๋‹น ๋ธŒ๋žœ๋“œ์˜ ๋กœ๊ณ ๋ฅผ UI์— ํ‘œ์‹œํ•œ๋‹ค. +- ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. + +### ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ + +- ์›”๊ณผ ๋…„๋„๋ฅผ ๋ฒ”์œ„ ๋‚ด์—์„œ๋งŒ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•˜๋ฉฐ, ์ž…๋ ฅ ์ œํ•œ์„ ๋‘์–ด ์‚ฌ์šฉ์ž๊ฐ€ ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. + +### ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ + +- ์˜๋ฌธ์ž ๋Œ€๋ฌธ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ ํผ์„ ๊ตฌํ˜„ํ•œ๋‹ค. + +### ์‹ค์‹œ๊ฐ„ ํ”„๋ฆฌ๋ทฐ ์—…๋ฐ์ดํŠธ + +- ์‚ฌ์šฉ์ž์˜ ์นด๋“œ ์ •๋ณด ์ž…๋ ฅ์— ๋”ฐ๋ผ ์นด๋“œ ํ”„๋ฆฌ๋ทฐ๋ฅผ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค. + +### โš ๏ธ ์ฃผ์˜ ์‚ฌํ•ญ + +- ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ input์— ํฌ์ปค์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ด๋™ํ•˜๋Š” ๋ถ€๋ถ„์„ 1๋‹จ๊ณ„์—์„œ ๊ณ ๋ คํ•˜์ง€ ์•Š๋Š”๋‹ค. **'๊ธฐ๋Šฅ' ์ž์ฒด์— ์ง‘์ค‘ํ•ด์„œ ๊ตฌํ˜„**ํ•œ๋‹ค. ๋งŒ์•ฝ ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ธก๋ฉด์—์„œ ํ”ผ๋“œ๋ฐฑ์„ ์ค€๋‹ค๋ฉด, ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜๋Š” ์‹œ์ ์—์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๋„ ํ•จ๊ป˜ ๊ณ ๋ คํ•˜์—ฌ ๊ฐœ์„ ํ•œ๋‹ค. +- ๊ธฐ๋ณธ์ ์ธ ๊ธฐ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•œ ์ฝ”๋“œ๋Š” ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค. From bcd9c7d583e569e5fe2dd9c7041d367f5fe4f31c Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:17:27 +0900 Subject: [PATCH 03/40] =?UTF-8?q?chore:=20styled-components,=20styled-rese?= =?UTF-8?q?t=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 1613fb4bec..46ea7b3b5f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "styled-components": "^6.1.8", + "styled-reset": "^4.5.2" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.3", From cded571783ad3c473c8038a6fc92407757cbb2dd Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:11:58 +0900 Subject: [PATCH 04/40] =?UTF-8?q?feat:=20=EA=B3=B5=EC=9A=A9=20=EC=9D=B8?= =?UTF-8?q?=ED=92=8B=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 Co-authored-by: brgndyy --- src/components/composables/Input.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/components/composables/Input.tsx diff --git a/src/components/composables/Input.tsx b/src/components/composables/Input.tsx new file mode 100644 index 0000000000..7b71ca3541 --- /dev/null +++ b/src/components/composables/Input.tsx @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +type InputProps = { + value: string; + onChange: (e: React.ChangeEvent) => void; + type: 'text' | 'number' | 'email' | 'password'; + placeholder: string; + id: string; +}; + +const StyledInput = styled.input` + border: 1px solid rgba(172, 172, 172, 1); + padding: 8px; + font-size: 0.6875rem; + border-radius: 2px; + height: 32px; +`; + +export default function Input({ value, onChange, type, placeholder, id }: InputProps) { + return ( + + ); +} From 6dcf8e2068dad07b63f8e205b7ab04f286732704 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:12:30 +0900 Subject: [PATCH 05/40] =?UTF-8?q?feat:=20sr-only=20=EB=9D=BC=EB=B2=A8=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 Co-authored-by: brgndyy --- src/components/composables/Label.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/components/composables/Label.tsx diff --git a/src/components/composables/Label.tsx b/src/components/composables/Label.tsx new file mode 100644 index 0000000000..3a289f29c8 --- /dev/null +++ b/src/components/composables/Label.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +type LabelProps = { + htmlFor: string; +}; + +const SrOnly = styled.label` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + border: 0; + clip: rect(0 0 0 0); +`; + +export default function Label({ htmlFor, children }: PropsWithChildren) { + return {children}; +} From b5f81956c030c3255c190d253d9914b02dae17e6 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:09:53 +0900 Subject: [PATCH 06/40] =?UTF-8?q?feat:=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/GlobalStyles.tsx | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/GlobalStyles.tsx diff --git a/src/GlobalStyles.tsx b/src/GlobalStyles.tsx new file mode 100644 index 0000000000..c980c15b30 --- /dev/null +++ b/src/GlobalStyles.tsx @@ -0,0 +1,35 @@ +import { createGlobalStyle } from 'styled-components'; +import reset from 'styled-reset'; + +const GlobalStyles = createGlobalStyle` + ${reset} + + a{ + text-decoration: none; + color: inherit; + } + + *{ + box-sizing: border-box; + } + + input, textarea { + -moz-user-select: auto; + -webkit-user-select: auto; + -ms-user-select: auto; + user-select: auto; + } + + input:focus { + outline: none; + } + + button { + border: none; + background: none; + padding: 0; + cursor: pointer; + } +`; + +export default GlobalStyles; From 7173e0735a588ea1032a9c470b9ac8def34e92fa Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:11:41 +0900 Subject: [PATCH 07/40] =?UTF-8?q?feat(storybook):=20=EC=9D=B8=ED=92=8B=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/stories/Input.stories.tsx | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/stories/Input.stories.tsx diff --git a/src/stories/Input.stories.tsx b/src/stories/Input.stories.tsx new file mode 100644 index 0000000000..f1f271ec25 --- /dev/null +++ b/src/stories/Input.stories.tsx @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Input from '../components/composables/Input'; +import { fn } from '@storybook/test'; +import GlobalStyles from '../GlobalStyles'; + +const meta = { + title: 'Composable/Input', + component: Input, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + <> + + + + ), + ], + argTypes: { + value: { + control: 'text', + description: '์ธํ’‹ ํƒœ๊ทธ์˜ value ์†์„ฑ', + }, + id: { + control: 'text', + description: '์ธํ’‹ ํƒœ๊ทธ์˜ id ์†์„ฑ', + }, + type: { + options: ['text', 'number', 'email', 'password'], + description: '์ธํ’‹ ํƒœ๊ทธ์˜ type ์†์„ฑ', + }, + placeholder: { + control: 'string', + description: '์ธํ’‹ ํƒœ๊ทธ์˜ placeholder ์†์„ฑ', + }, + }, + args: { + onChange: fn(), + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + id: '', + value: '', + type: 'text', + placeholder: '๊ฐ’์„ ์ž…๋ ฅํ•˜์„ธ์š”.', + }, +}; From 4552764e3cb1291aee7e45a28cad634b43d9c18e Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:04:30 +0900 Subject: [PATCH 08/40] =?UTF-8?q?feat:=20=EC=9D=B8=ED=92=8B=20=EC=84=B9?= =?UTF-8?q?=EC=85=98=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 Co-authored-by: brgndyy --- src/components/InputSection.tsx | 77 ++++++++++++++++++++++++++++ src/components/composables/Input.tsx | 1 + 2 files changed, 78 insertions(+) create mode 100644 src/components/InputSection.tsx diff --git a/src/components/InputSection.tsx b/src/components/InputSection.tsx new file mode 100644 index 0000000000..9fa77c6402 --- /dev/null +++ b/src/components/InputSection.tsx @@ -0,0 +1,77 @@ +import { PropsWithChildren } from 'react'; +import styled from 'styled-components'; + +type InfoProps = { + title: string; + description: string; + inputTitle: string; +}; + +const Section = styled.section` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const TitleContainer = styled.div` + display: flex; + align-items: center; + + height: 26px; +`; + +const Title = styled.h2` + font-size: 1.125rem; + font-weight: 700; +`; + +const DescriptionContainer = styled.div` + height: 14px; +`; + +const Description = styled.span` + line-height: 0.875rem; + font-size: 0.5938rem; + font-weight: 400; + color: rgba(139, 149, 161, 1); +`; + +const Span = styled.span` + font-weight: 500; + font-size: 0.75rem; + line-height: 0.9375rem; +`; + +const SpanWrapper = styled.div``; + +const InputWrapper = styled.div` + display: flex; + gap: 10px; +`; + +export default function InputSection({ + title, + description, + inputTitle, + children, +}: PropsWithChildren) { + return ( +
+
+ + {title} + + + {description} + +
+
+ + {inputTitle} + + {children} + {/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */} +
+
+ ); +} diff --git a/src/components/composables/Input.tsx b/src/components/composables/Input.tsx index 7b71ca3541..9d26ddb6be 100644 --- a/src/components/composables/Input.tsx +++ b/src/components/composables/Input.tsx @@ -14,6 +14,7 @@ const StyledInput = styled.input` font-size: 0.6875rem; border-radius: 2px; height: 32px; + flex: 1; `; export default function Input({ value, onChange, type, placeholder, id }: InputProps) { From 2ea7df5228f866ad56e66d20f66b0a23ee7a7d25 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:49:44 +0900 Subject: [PATCH 09/40] =?UTF-8?q?feat:=20useCardNumber=20=EC=BB=A4?= =?UTF-8?q?=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 Co-authored-by: brgndyy --- src/hooks/useCardNumber.ts | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/hooks/useCardNumber.ts diff --git a/src/hooks/useCardNumber.ts b/src/hooks/useCardNumber.ts new file mode 100644 index 0000000000..e736e39633 --- /dev/null +++ b/src/hooks/useCardNumber.ts @@ -0,0 +1,39 @@ +import { useState } from 'react'; +import { InitialState } from '../App'; + +const useCardNumber = (initialStates: InitialState[]) => { + const [cardNumbers, setCardNumbers] = useState(initialStates); + + const cardNumbersChangeHandler = (e: React.ChangeEvent, index: number) => { + if (e.target.value === 'a') { + setCardNumbers( + cardNumbers.map((cardNumber, i) => { + if (i === index) { + return { + value: e.target.value, + isError: true, + }; + } + return cardNumber; + }), + ); + return; + } + + return setCardNumbers( + cardNumbers.map((cardNumber, i) => { + if (i === index) { + return { + value: e.target.value, + isError: false, + }; + } + return cardNumber; + }), + ); + }; + + return { cardNumbers, cardNumbersChangeHandler }; +}; + +export default useCardNumber; From 3b17e49a061a8c4f244e9ea5dfac2f199760b40b Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:37:04 +0900 Subject: [PATCH 10/40] =?UTF-8?q?feat:=20useInput=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=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 Co-authored-by: brgndyy --- src/hooks/useInput.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/hooks/useInput.ts diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts new file mode 100644 index 0000000000..e3cdd7cdc3 --- /dev/null +++ b/src/hooks/useInput.ts @@ -0,0 +1,28 @@ +import { useState } from 'react'; +import { InitialState } from '../App'; + +const useInput = ( + initialState: InitialState, + isValid: (e: React.ChangeEvent) => boolean, +) => { + const [inputState, setInputState] = useState(initialState); + + const inputChangeHandler = (e: React.ChangeEvent) => { + if (isValid(e)) { + setInputState({ + value: e.target.value, + isError: true, + }); + return; + } + + setInputState({ + value: e.target.value, + isError: false, + }); + }; + + return { inputState, inputChangeHandler }; +}; + +export default useInput; From 7f0e7d11171ec357f1e344dd56d3bba1680f03b4 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:50:29 +0900 Subject: [PATCH 11/40] =?UTF-8?q?feat(validate):=20=EC=88=AB=EC=9E=90=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/utils/validate.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/utils/validate.ts diff --git a/src/utils/validate.ts b/src/utils/validate.ts new file mode 100644 index 0000000000..7492b4d48a --- /dev/null +++ b/src/utils/validate.ts @@ -0,0 +1,15 @@ +const validate = { + isNumberInRange: ({ + min, + max, + compareNumber, + }: { + min: number; + max: number; + compareNumber: number; + }) => { + return compareNumber >= min && compareNumber <= max; + }, +}; + +export default validate; From 152e9befc55a2e8784928f162520058db8036e2d Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:11:41 +0900 Subject: [PATCH 12/40] =?UTF-8?q?feat:=20CreditCard=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/images/mastercard.png | Bin 0 -> 3470 bytes src/assets/images/visa.png | Bin 0 -> 3926 bytes src/components/CreditCard.tsx | 141 +++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/assets/images/mastercard.png create mode 100644 src/assets/images/visa.png create mode 100644 src/components/CreditCard.tsx diff --git a/src/assets/images/mastercard.png b/src/assets/images/mastercard.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe529136b3abba20425538147fc648e60bb823c GIT binary patch literal 3470 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/visa.png b/src/assets/images/visa.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fe0edcb1ba7094c6b3cc2b55cd22788676ef83 GIT binary patch literal 3926 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 literal 0 HcmV?d00001 diff --git a/src/components/CreditCard.tsx b/src/components/CreditCard.tsx new file mode 100644 index 0000000000..3cfc15df2c --- /dev/null +++ b/src/components/CreditCard.tsx @@ -0,0 +1,141 @@ +import styled from 'styled-components'; +import { InitialState } from '../App'; + +type CreditCardProps = { + cardNumbers: InitialState[]; + month: string; + year: string; + name: string; + cardImageSrc: string; +}; + +const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + + margin-top: 50px; + margin-bottom: 45px; + + width: 100%; +`; + +const CardContainer = styled.div` + background-color: #333333; + + width: 212px; + height: 132px; + + padding: 8px 12px; + + border-radius: 4px; +`; + +const NumbersContainer = styled.div` + display: flex; + gap: 10px; + min-height: 20px; +`; + +const NumbersWrapper = styled.div` + display: flex; + align-items: center; + gap: 5px; + + min-width: 32px; +`; + +const Text = styled.span` + color: #fff; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.25rem; +`; + +const Dot = styled.span` + width: 4px; + height: 4px; + + background-color: #fff; + border-radius: 50%; +`; + +const CardHeader = styled.div` + display: flex; + justify-content: space-between; +`; + +const CardHeaderContentWrapper = styled.div` + width: 36px; + height: 22px; +`; + +const IcChip = styled.div` + width: 100%; + height: 100%; + + border-radius: 4px; + + background-color: #ddcd78; +`; + +const CardBrand = styled.img` + width: 100%; + height: 100%; + + border-radius: 4px; +`; + +const CardInfoWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + + margin-top: 14px; + margin-left: 5px; +`; + +export default function CreditCard({ + cardNumbers, + month, + year, + name, + cardImageSrc, +}: CreditCardProps) { + return ( + + + + + + + + {cardImageSrc ? : null} + + + + + + {cardNumbers[0].value} + + + {cardNumbers[1].value} + + + {Array.from({ length: cardNumbers[2].value.length }).map((_, index) => ( + + ))} + + + {Array.from({ length: cardNumbers[3].value.length }).map((_, index) => ( + + ))} + + + {month + `${month || year ? '/' : ''}` + year} + {name} + + + + ); +} From f3b9ee0ae63cf1ec40381ff6cae8794699c0b1f6 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:11:48 +0900 Subject: [PATCH 13/40] =?UTF-8?q?refactor(InputSection):=20description=20?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/InputSection.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/InputSection.tsx b/src/components/InputSection.tsx index 9fa77c6402..6314ce6618 100644 --- a/src/components/InputSection.tsx +++ b/src/components/InputSection.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; type InfoProps = { title: string; - description: string; + description?: string; inputTitle: string; }; @@ -33,7 +33,7 @@ const Description = styled.span` line-height: 0.875rem; font-size: 0.5938rem; font-weight: 400; - color: rgba(139, 149, 161, 1); + color: #8b95a1; `; const Span = styled.span` @@ -61,16 +61,17 @@ export default function InputSection({ {title} - - {description} - + {description ? ( + + {description} + + ) : null}
{inputTitle} {children} - {/* ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */}
); From ac06ece899e52df827e7f5ab713c736228a2f204 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:12:08 +0900 Subject: [PATCH 14/40] =?UTF-8?q?docs:=20=EC=B9=B4=EB=93=9C=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EB=93=9C=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0460dfa928..983d03929f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ ### ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๋ฐ ์‹๋ณ„ - ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•˜๋Š” ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํŒŒ์•…ํ•˜์—ฌ, Visa๋‚˜ MasterCard์— ํ•ด๋‹นํ•˜๋ฉด ํ•ด๋‹น ๋ธŒ๋žœ๋“œ์˜ ๋กœ๊ณ ๋ฅผ UI์— ํ‘œ์‹œํ•œ๋‹ค. + - Visa : 4๋กœ ์‹œ์ž‘ํ•˜๋Š” 16์ž๋ฆฌ + - MasterCard: 51~55๋กœ ์‹œ์ž‘ํ•˜๋Š” 16์ž๋ฆฌ ์ˆซ์ž - ์ž…๋ ฅ์€ ์ˆซ์ž๋งŒ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•œ๋‹ค. ### ์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„ ์ž…๋ ฅ From 17fabd60151b21175f99f1b1a8e1316fcb0a7ccb Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:17:35 +0900 Subject: [PATCH 15/40] =?UTF-8?q?feat(validate):=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์ˆซ์ž ์œ ํšจ์„ฑ ๊ฒ€์ฆ - ๋น„์ž ์นด๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ - ๋งˆ์Šคํ„ฐ ์นด๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ - ์˜์–ด ์œ ํšจ์„ฑ ๊ฒ€์ฆ Co-authored-by: brgndyy --- src/utils/validate.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/validate.ts b/src/utils/validate.ts index 7492b4d48a..1303a697dc 100644 --- a/src/utils/validate.ts +++ b/src/utils/validate.ts @@ -10,6 +10,22 @@ const validate = { }) => { return compareNumber >= min && compareNumber <= max; }, + + isValidDigit: (value: string) => { + return /^\d+$/.test(value); + }, + + isVisa: (value: string) => { + return /\b4\d{15}\b/.test(value); + }, + + isMasterCard: (value: string) => { + return /\b5[1-5]\d{14}\b/.test(value); + }, + + isEnglish: (value: string) => { + return /^[a-zA-Z ]*$/.test(value); + }, }; export default validate; From 3f86c2a7fdfdcb6c16f7cf6c424463d98cb35606 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:18:19 +0900 Subject: [PATCH 16/40] =?UTF-8?q?refactor:=20useInput=20=EB=A1=9C=EC=A7=81?= =?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 Co-authored-by: brgndyy --- src/hooks/useInput.ts | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index e3cdd7cdc3..e82895570d 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -1,28 +1,14 @@ import { useState } from 'react'; -import { InitialState } from '../App'; -const useInput = ( - initialState: InitialState, - isValid: (e: React.ChangeEvent) => boolean, -) => { - const [inputState, setInputState] = useState(initialState); +const useInput = () => { + const [inputState, setInputState] = useState(''); + const [error, setError] = useState(false); const inputChangeHandler = (e: React.ChangeEvent) => { - if (isValid(e)) { - setInputState({ - value: e.target.value, - isError: true, - }); - return; - } - - setInputState({ - value: e.target.value, - isError: false, - }); + setInputState(e.target.value.toUpperCase()); }; - return { inputState, inputChangeHandler }; + return { inputState, inputChangeHandler, error, setError }; }; export default useInput; From 4c06f2f265df58dc38bfe4bc48742b91896646b7 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:19:27 +0900 Subject: [PATCH 17/40] =?UTF-8?q?feat:=20useCardNumber=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/hooks/useCardNumber.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/useCardNumber.ts b/src/hooks/useCardNumber.ts index e736e39633..90df75a635 100644 --- a/src/hooks/useCardNumber.ts +++ b/src/hooks/useCardNumber.ts @@ -1,16 +1,17 @@ import { useState } from 'react'; -import { InitialState } from '../App'; +import { InitialCardNumberState } from '../App'; +import validate from '../utils/validate'; -const useCardNumber = (initialStates: InitialState[]) => { - const [cardNumbers, setCardNumbers] = useState(initialStates); +const useCardNumber = (initialStates: InitialCardNumberState[]) => { + const [cardNumbers, setCardNumbers] = useState(initialStates); const cardNumbersChangeHandler = (e: React.ChangeEvent, index: number) => { - if (e.target.value === 'a') { + if (e.target.value !== '' && !validate.isValidDigit(e.target.value)) { setCardNumbers( cardNumbers.map((cardNumber, i) => { if (i === index) { return { - value: e.target.value, + ...cardNumber, isError: true, }; } From b323507d2e6c1ff8366abd1038525af25983a026 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:20:20 +0900 Subject: [PATCH 18/40] =?UTF-8?q?refactor:=20Input=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/components/composables/Input.tsx | 33 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/composables/Input.tsx b/src/components/composables/Input.tsx index 9d26ddb6be..c67a3727b3 100644 --- a/src/components/composables/Input.tsx +++ b/src/components/composables/Input.tsx @@ -3,22 +3,45 @@ import styled from 'styled-components'; type InputProps = { value: string; onChange: (e: React.ChangeEvent) => void; - type: 'text' | 'number' | 'email' | 'password'; + type: 'text' | 'number' | 'email' | 'password' | 'tel'; placeholder: string; id: string; + isError: boolean; + maxLength?: number; }; -const StyledInput = styled.input` - border: 1px solid rgba(172, 172, 172, 1); +const StyledInput = styled.input<{ isError: boolean }>` + border: 1px solid #acacac; padding: 8px; font-size: 0.6875rem; border-radius: 2px; + min-width: 71.25px; height: 32px; flex: 1; + + &:focus { + border: 1px solid ${(props) => (props.isError ? '#ff3d3d' : '#000')}; + } `; -export default function Input({ value, onChange, type, placeholder, id }: InputProps) { +export default function Input({ + value, + onChange, + type, + placeholder, + id, + isError, + maxLength, +}: InputProps) { return ( - + ); } From 11aeaf6958c10067d8ad7cd8dc690e812ff348ab Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:20:53 +0900 Subject: [PATCH 19/40] =?UTF-8?q?chore:=20cardNumbers=20=ED=83=80=EC=9E=85?= =?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 Co-authored-by: brgndyy --- src/components/CreditCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CreditCard.tsx b/src/components/CreditCard.tsx index 3cfc15df2c..1c911a2c6e 100644 --- a/src/components/CreditCard.tsx +++ b/src/components/CreditCard.tsx @@ -1,8 +1,8 @@ import styled from 'styled-components'; -import { InitialState } from '../App'; +import { InitialCardNumberState } from '../App'; type CreditCardProps = { - cardNumbers: InitialState[]; + cardNumbers: InitialCardNumberState[]; month: string; year: string; name: string; From 2185961a60316181bc83f765e80c08d37609c847 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:22:21 +0900 Subject: [PATCH 20/40] =?UTF-8?q?feat(App):=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EB=B0=94=EC=9D=B8?= =?UTF-8?q?=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/App.tsx | 235 ++++++++++++++++++++++++++++++++++++++++++- src/GlobalStyles.tsx | 10 ++ 2 files changed, 243 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ef7e3632d2..7fa06792e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,240 @@ -import "./App.css"; +import styled from 'styled-components'; +import GlobalStyles from './GlobalStyles'; +import InputInfo from './components/InputSection'; +import Input from './components/composables/Input'; +import CreditCard from './components/CreditCard'; +import useCardNumber from './hooks/useCardNumber'; +import useInput from './hooks/useInput'; +import { useEffect, useState } from 'react'; +import Label from './components/composables/Label'; +import MasterCardImage from './assets/images/mastercard.png'; +import VisaCardImage from './assets/images/visa.png'; +import validate from './utils/validate'; + +export const StyledInput = styled.input` + border: 1px solid #acacac; + padding: 8px; + font-size: 0.6875rem; + border-radius: 2px; + height: 32px; +`; + +export type InitialCardNumberState = { + value: string; + isError: boolean; +}; + +const InitialCardNumberState: InitialCardNumberState = { + value: '', + isError: false, +}; + +const Container = styled.div` + padding: 20px 30px; + + width: 376px; + height: 680px; + background-color: beige; +`; + +const CardInfoContainer = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const ErrorContainer = styled.div` + height: 14px; +`; + +const ErrorMessageSpan = styled.span` + color: #ff3d3d; + + font-size: 0.5938rem; + font-weight: 400; + line-height: 0.875rem; +`; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 8px; +`; function App() { + const { cardNumbers, cardNumbersChangeHandler } = useCardNumber( + Array.from({ length: 4 }, () => InitialCardNumberState), + ); + const [cardImageSrc, setCardImageSrc] = useState(''); + + const { + inputState: month, + inputChangeHandler: monthChangeHandler, + error: monthError, + setError: setMonthError, + } = useInput(); + + const { + inputState: year, + inputChangeHandler: yearChangeHandler, + error: yearError, + setError: setYearError, + } = useInput(); + + const { + inputState: name, + inputChangeHandler: nameChangeHandler, + error: nameError, + setError: setNameError, + } = useInput(); + + useEffect(() => { + if ( + month !== '' && + (!validate.isNumberInRange({ min: 1, max: 12, compareNumber: Number(month) }) || + !validate.isValidDigit(month)) + ) { + setMonthError(true); + + return; + } + + setMonthError(false); + }, [month, monthError]); + + useEffect(() => { + if (year !== '' && !validate.isValidDigit(year)) { + setYearError(true); + + return; + } + + setYearError(false); + }, [year, yearError]); + + useEffect(() => { + if (name !== '' && !validate.isEnglish(name)) { + setNameError(true); + + return; + } + + setNameError(false); + }, [name, nameError]); + + useEffect(() => { + setCardImageSrc(''); + + const cardNumberString = cardNumbers.map((cardNumber) => cardNumber.value).join(''); + + if (validate.isVisa(cardNumberString)) { + setCardImageSrc(VisaCardImage); + } + + if (validate.isMasterCard(cardNumberString)) { + setCardImageSrc(MasterCardImage); + } + }, [cardNumbers]); + return ( <> -

React Payments

+ + + + + + + {cardNumbers.map((cardNumber, index) => { + const uniqueId = 'cardNumbers' + index; + return ( + <> + + + + {cardNumbers.some((cardNumber) => cardNumber.isError) && 'cardNumberError'} + + + + + + + + + + {monthError && yearError ? 'MonthError' : ''} + {!monthError && yearError ? 'yearError' : ''} + {monthError && !yearError ? 'monthError' : ''} + + + + + + + + + {nameError && 'nameError'} + + + + ); } diff --git a/src/GlobalStyles.tsx b/src/GlobalStyles.tsx index c980c15b30..aaeb0f44a8 100644 --- a/src/GlobalStyles.tsx +++ b/src/GlobalStyles.tsx @@ -13,6 +13,16 @@ const GlobalStyles = createGlobalStyle` box-sizing: border-box; } + #root { + display: flex; + justify-content: center; + align-items: center; + + width: 100vw; + height: auto; + min-height: 100vh; + } + input, textarea { -moz-user-select: auto; -webkit-user-select: auto; From 8bd49827cc391fa29988b18a37d9c2520c074b9a Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:36:27 +0900 Subject: [PATCH 21/40] =?UTF-8?q?feat:=20=EC=83=81=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/App.tsx | 25 +++++++++++++------------ src/constants/cardSection.ts | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 src/constants/cardSection.ts diff --git a/src/App.tsx b/src/App.tsx index 7fa06792e4..4a20bbaa23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Label from './components/composables/Label'; import MasterCardImage from './assets/images/mastercard.png'; import VisaCardImage from './assets/images/visa.png'; import validate from './utils/validate'; +import { CARD_NUMBER, EXPIRATION_PERIOD, OWNER_NAME } from './constants/cardSection'; export const StyledInput = styled.input` border: 1px solid #acacac; @@ -150,9 +151,9 @@ function App() { {cardNumbers.map((cardNumber, index) => { const uniqueId = 'cardNumbers' + index; @@ -175,16 +176,16 @@ function App() { - {cardNumbers.some((cardNumber) => cardNumber.isError) && 'cardNumberError'} + {cardNumbers.some((cardNumber) => cardNumber.isError) && CARD_NUMBER.errorMessage} - + - {nameError && 'nameError'} + {nameError && OWNER_NAME.errorMessage} diff --git a/src/constants/cardSection.ts b/src/constants/cardSection.ts new file mode 100644 index 0000000000..4d3d5f335b --- /dev/null +++ b/src/constants/cardSection.ts @@ -0,0 +1,22 @@ +const NUMBER_ERROR_MESSAGE = '์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'; + +export const CARD_NUMBER = Object.freeze({ + title: '๊ฒฐ์ œํ•  ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”', + description: '๋ณธ์ธ ๋ช…์˜์˜ ์นด๋“œ๋งŒ ๊ฒฐ์ œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.', + inputTitle: '์นด๋“œ ๋ฒˆํ˜ธ', + errorMessage: NUMBER_ERROR_MESSAGE, +}); + +export const EXPIRATION_PERIOD = Object.freeze({ + title: '์นด๋“œ ์œ ํšจ๊ธฐ๊ฐ„์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”', + description: '์›”/๋…„๋„(MMYY)๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”.', + inputTitle: '์œ ํšจ๊ธฐ๊ฐ„', + monthErrorMessage: '1๋ถ€ํ„ฐ 12์‚ฌ์ด์˜ ์ˆซ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.', + yearErrorMessage: NUMBER_ERROR_MESSAGE, +}); + +export const OWNER_NAME = Object.freeze({ + title: '์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”', + inputTitle: '์†Œ์œ ์ž ์ด๋ฆ„', + errorMessage: '์˜๋ฌธ ๋Œ€๋ฌธ์ž๋งŒ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.', +}); From c843b8a62a1b23fbabe791ae001ba6af9a38ab17 Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:43:25 +0900 Subject: [PATCH 22/40] =?UTF-8?q?feat:=20=EC=9D=B8=ED=92=8B=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B4=80=EB=A0=A8=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/stories/Input.stories.tsx | 46 +++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/stories/Input.stories.tsx b/src/stories/Input.stories.tsx index f1f271ec25..6bf8393e6c 100644 --- a/src/stories/Input.stories.tsx +++ b/src/stories/Input.stories.tsx @@ -21,19 +21,27 @@ const meta = { argTypes: { value: { control: 'text', - description: '์ธํ’‹ ํƒœ๊ทธ์˜ value ์†์„ฑ', + description: 'value ์†์„ฑ', }, id: { control: 'text', - description: '์ธํ’‹ ํƒœ๊ทธ์˜ id ์†์„ฑ', + description: 'id ์†์„ฑ', }, type: { - options: ['text', 'number', 'email', 'password'], - description: '์ธํ’‹ ํƒœ๊ทธ์˜ type ์†์„ฑ', + options: ['text', 'number', 'email', 'password', 'tel'], + description: 'type ์†์„ฑ', }, placeholder: { control: 'string', - description: '์ธํ’‹ ํƒœ๊ทธ์˜ placeholder ์†์„ฑ', + description: 'placeholder ์†์„ฑ', + }, + isError: { + control: 'boolean', + description: '์—๋Ÿฌ ๊ฐ์ง€ ์†์„ฑ ๊ฐ’', + }, + maxLength: { + control: 'number', + description: '์ž…๋ ฅ๊ฐ’ ์ตœ๋Œ€ ๊ธธ์ด', }, }, args: { @@ -43,11 +51,35 @@ const meta = { export default meta; type Story = StoryObj; -export const Default: Story = { +export const CardNumber: Story = { + args: { + id: '', + value: '', + type: 'text', + placeholder: '1234', + isError: false, + maxLength: 4, + }, +}; + +export const Month: Story = { + args: { + id: '', + value: '', + type: 'text', + placeholder: 'MM', + isError: false, + maxLength: 2, + }, +}; + +export const Year: Story = { args: { id: '', value: '', type: 'text', - placeholder: '๊ฐ’์„ ์ž…๋ ฅํ•˜์„ธ์š”.', + placeholder: 'YY', + isError: false, + maxLength: 2, }, }; From 76a8058315fc2789bc535d03c1795997ae23356b Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:54:23 +0900 Subject: [PATCH 23/40] =?UTF-8?q?feat:=20CreditCard=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/stories/CreditCard.stories.tsx | 92 ++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/stories/CreditCard.stories.tsx diff --git a/src/stories/CreditCard.stories.tsx b/src/stories/CreditCard.stories.tsx new file mode 100644 index 0000000000..548e4f77d4 --- /dev/null +++ b/src/stories/CreditCard.stories.tsx @@ -0,0 +1,92 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CreditCard from '../components/CreditCard'; +import GlobalStyles from '../GlobalStyles'; +import MasterCard from '../assets/images/mastercard.png'; +import VisaCard from '../assets/images/visa.png'; + +const meta = { + title: 'CreditCard', + component: CreditCard, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + <> + + + + ), + ], + argTypes: { + cardNumbers: { + control: 'text', + description: '์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ๊ฐ’', + }, + month: { + control: 'text', + description: '์นด๋“œ ์›” ์ž…๋ ฅ ๊ฐ’', + }, + year: { + control: 'text', + description: '์นด๋“œ ์—ฐ๋„ ์ž…๋ ฅ ๊ฐ’', + }, + name: { + control: 'text', + description: '์นด๋“œ ์†Œ์œ ์ž ์ž…๋ ฅ ๊ฐ’', + }, + cardImageSrc: { + control: [MasterCard, VisaCard], + description: '์นด๋“œ ๋ธŒ๋žœ๋“œ ์ด๋ฏธ์ง€', + }, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + cardNumbers: [ + { value: '0000', isError: false }, + { value: '0000', isError: false }, + { value: '0000', isError: false }, + { value: '0000', isError: false }, + ], + month: '00', + year: '00', + name: 'JOHN DOE', + cardImageSrc: '', + }, +}; + +export const Visa: Story = { + args: { + cardNumbers: [ + { value: '4321', isError: false }, + { value: '1234', isError: false }, + { value: '1234', isError: false }, + { value: '9876', isError: false }, + ], + month: '12', + year: '29', + name: 'LIM DONGJUN', + cardImageSrc: VisaCard, + }, +}; + +export const Master: Story = { + args: { + cardNumbers: [ + { value: '5323', isError: false }, + { value: '1234', isError: false }, + { value: '4321', isError: false }, + { value: '9872', isError: false }, + ], + month: '12', + year: '29', + name: 'LIM DONGJUN', + cardImageSrc: MasterCard, + }, +}; From fecb622fa321c5982e054279b5c6094cd276971f Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:01:42 +0900 Subject: [PATCH 24/40] =?UTF-8?q?feat:=20InputSection=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/stories/InputSection.stories.tsx | 64 ++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/stories/InputSection.stories.tsx diff --git a/src/stories/InputSection.stories.tsx b/src/stories/InputSection.stories.tsx new file mode 100644 index 0000000000..ebeed6c9fc --- /dev/null +++ b/src/stories/InputSection.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import InputSection from '../components/InputSection'; +import GlobalStyles from '../GlobalStyles'; +import { CARD_NUMBER, EXPIRATION_PERIOD, OWNER_NAME } from '../constants/cardSection'; + +const meta = { + title: 'InputSection', + component: InputSection, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + <> + + + + ), + ], + argTypes: { + title: { + control: 'text', + description: '์ œ๋ชฉ', + }, + description: { + control: 'text', + description: '์„ค๋ช…', + }, + inputTitle: { + control: 'text', + description: '๋ผ๋ฒจ', + }, + }, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Title: Story = { + args: { + title: CARD_NUMBER.title, + description: CARD_NUMBER.description, + inputTitle: CARD_NUMBER.inputTitle, + children: <>, + }, +}; + +export const ExpirationPeriod: Story = { + args: { + title: EXPIRATION_PERIOD.title, + description: EXPIRATION_PERIOD.description, + inputTitle: EXPIRATION_PERIOD.inputTitle, + children: <>, + }, +}; + +export const OwnerName: Story = { + args: { + title: OWNER_NAME.title, + inputTitle: OWNER_NAME.inputTitle, + children: <>, + }, +}; From a9f5148164e115ae340e64e75292a77d4ee7971f Mon Sep 17 00:00:00 2001 From: Largopie <106071687+Largopie@users.noreply.github.com> Date: Thu, 18 Apr 2024 15:02:08 +0900 Subject: [PATCH 25/40] =?UTF-8?q?chore:=20App=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20?= =?UTF-8?q?=EC=A4=91=EC=95=99=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: brgndyy --- src/stories/App.stories.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/stories/App.stories.tsx b/src/stories/App.stories.tsx index 7c5f8946bc..cb53061acf 100644 --- a/src/stories/App.stories.tsx +++ b/src/stories/App.stories.tsx @@ -1,9 +1,12 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import App from "../App"; +import type { Meta, StoryObj } from '@storybook/react'; +import App from '../App'; const meta = { - title: "App", + title: 'App', component: App, + parameters: { + layout: 'centered', + }, } satisfies Meta; export default meta; From f60738d6963b21775438e6b1ebe9fd1758b1fc16 Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 19 Apr 2024 14:30:45 +0900 Subject: [PATCH 26/40] =?UTF-8?q?feat:=20useCardImageBrand=20hook=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCardBrandImage.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/hooks/useCardBrandImage.ts diff --git a/src/hooks/useCardBrandImage.ts b/src/hooks/useCardBrandImage.ts new file mode 100644 index 0000000000..036fa74c4f --- /dev/null +++ b/src/hooks/useCardBrandImage.ts @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; +import validate from '../utils/validate'; +import MasterCardImage from '../assets/images/mastercard.png'; +import VisaCardImage from '../assets/images/visa.png'; +import { InitialCardNumberState } from '../App'; + +const useCardBrandImage = (cardNumberStates: InitialCardNumberState[]) => { + const [cardImageSrc, setCardImageSrc] = useState(''); + + useEffect(() => { + setCardImageSrc(''); + + const cardNumberString = cardNumberStates.map((cardNumber) => cardNumber.value).join(''); + + if (validate.isVisa(cardNumberString)) { + setCardImageSrc(VisaCardImage); + } + + if (validate.isMasterCard(cardNumberString)) { + setCardImageSrc(MasterCardImage); + } + }, [cardNumberStates]); + + return { cardImageSrc }; +}; + +export default useCardBrandImage; From 10a93082933507b5ecbe2e6adfaa06692e32b14c Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 19 Apr 2024 20:27:36 +0900 Subject: [PATCH 27/40] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20props=20=ED=83=80?= =?UTF-8?q?=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/Input.tsx | 13 ++++--------- src/components/composables/Label.tsx | 12 +++++------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/composables/Input.tsx b/src/components/composables/Input.tsx index c67a3727b3..7f6649cfb1 100644 --- a/src/components/composables/Input.tsx +++ b/src/components/composables/Input.tsx @@ -1,14 +1,9 @@ +import { InputHTMLAttributes } from 'react'; import styled from 'styled-components'; -type InputProps = { - value: string; - onChange: (e: React.ChangeEvent) => void; - type: 'text' | 'number' | 'email' | 'password' | 'tel'; - placeholder: string; - id: string; +interface InputProps extends InputHTMLAttributes { isError: boolean; - maxLength?: number; -}; +} const StyledInput = styled.input<{ isError: boolean }>` border: 1px solid #acacac; @@ -41,7 +36,7 @@ export default function Input({ maxLength={maxLength} placeholder={placeholder} id={id} - isError={isError} + $isError={isError} /> ); } diff --git a/src/components/composables/Label.tsx b/src/components/composables/Label.tsx index 3a289f29c8..87ee201d42 100644 --- a/src/components/composables/Label.tsx +++ b/src/components/composables/Label.tsx @@ -1,11 +1,9 @@ -import { PropsWithChildren } from 'react'; +import { PropsWithChildren, LabelHTMLAttributes } from 'react'; import styled from 'styled-components'; -type LabelProps = { - htmlFor: string; -}; +type LabelProps = LabelHTMLAttributes; -const SrOnly = styled.label` +const StyledLabel = styled.label` position: absolute; width: 1px; height: 1px; @@ -17,6 +15,6 @@ const SrOnly = styled.label` clip: rect(0 0 0 0); `; -export default function Label({ htmlFor, children }: PropsWithChildren) { - return {children}; +export default function Label({ children, ...props }: PropsWithChildren) { + return {children}; } From 420baadcd11a92e002eba106dc6651af78242fb3 Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 19 Apr 2024 20:28:54 +0900 Subject: [PATCH 28/40] =?UTF-8?q?refactor:=20useInput=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=ED=9B=85=20=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 | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/hooks/useInput.ts b/src/hooks/useInput.ts index e82895570d..ddd08d8636 100644 --- a/src/hooks/useInput.ts +++ b/src/hooks/useInput.ts @@ -1,14 +1,24 @@ import { useState } from 'react'; -const useInput = () => { - const [inputState, setInputState] = useState(''); +const useInput = (validators: { fn: (value: string) => boolean }[]) => { + const [inputValue, setInputValue] = useState(''); const [error, setError] = useState(false); - const inputChangeHandler = (e: React.ChangeEvent) => { - setInputState(e.target.value.toUpperCase()); + const onChange = (e: React.ChangeEvent) => { + setError(false); + const inputValue = e.target.value.toUpperCase(); + + for (let validator of validators) { + if (!validator.fn(inputValue)) { + setError(true); + break; + } + } + + setInputValue(e.target.value.toUpperCase()); }; - return { inputState, inputChangeHandler, error, setError }; + return { inputValue, onChange, error }; }; export default useInput; From c1d5ea547b3d825f7393263eeec3aee81942234b Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 19 Apr 2024 20:29:41 +0900 Subject: [PATCH 29/40] =?UTF-8?q?refactor:=20InitialCardNumberState=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=ED=95=98=EC=9C=84=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreditCard.tsx | 4 ++-- src/hooks/useCardBrandImage.ts | 2 +- src/hooks/useCardNumber.ts | 6 +++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/CreditCard.tsx b/src/components/CreditCard.tsx index 1c911a2c6e..69f10e48de 100644 --- a/src/components/CreditCard.tsx +++ b/src/components/CreditCard.tsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { InitialCardNumberState } from '../App'; +import { InitialCardNumberState } from '../hooks/useCardNumber'; type CreditCardProps = { cardNumbers: InitialCardNumberState[]; @@ -110,7 +110,7 @@ export default function CreditCard({ - {cardImageSrc ? : null} + {cardImageSrc ? : null} diff --git a/src/hooks/useCardBrandImage.ts b/src/hooks/useCardBrandImage.ts index 036fa74c4f..6430120eca 100644 --- a/src/hooks/useCardBrandImage.ts +++ b/src/hooks/useCardBrandImage.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import validate from '../utils/validate'; import MasterCardImage from '../assets/images/mastercard.png'; import VisaCardImage from '../assets/images/visa.png'; -import { InitialCardNumberState } from '../App'; +import { InitialCardNumberState } from './useCardNumber'; const useCardBrandImage = (cardNumberStates: InitialCardNumberState[]) => { const [cardImageSrc, setCardImageSrc] = useState(''); diff --git a/src/hooks/useCardNumber.ts b/src/hooks/useCardNumber.ts index 90df75a635..d8df77beed 100644 --- a/src/hooks/useCardNumber.ts +++ b/src/hooks/useCardNumber.ts @@ -1,7 +1,11 @@ import { useState } from 'react'; -import { InitialCardNumberState } from '../App'; import validate from '../utils/validate'; +export type InitialCardNumberState = { + value: string; + isError: boolean; +}; + const useCardNumber = (initialStates: InitialCardNumberState[]) => { const [cardNumbers, setCardNumbers] = useState(initialStates); From 208b14d6d953f76e052042a92706f0d515b6a8ef Mon Sep 17 00:00:00 2001 From: brgndy Date: Fri, 19 Apr 2024 20:29:57 +0900 Subject: [PATCH 30/40] =?UTF-8?q?refactor:=20=EB=B3=80=EA=B2=BD=EB=90=9C?= =?UTF-8?q?=20useInput=EC=97=90=20=EB=A7=9E=EA=B2=8C=20useEffect=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=EB=93=A4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 119 ++++++++++++++++++---------------------------------- 1 file changed, 41 insertions(+), 78 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4a20bbaa23..7aa71f8ec8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,12 +5,12 @@ import Input from './components/composables/Input'; import CreditCard from './components/CreditCard'; import useCardNumber from './hooks/useCardNumber'; import useInput from './hooks/useInput'; -import { useEffect, useState } from 'react'; import Label from './components/composables/Label'; -import MasterCardImage from './assets/images/mastercard.png'; -import VisaCardImage from './assets/images/visa.png'; import validate from './utils/validate'; import { CARD_NUMBER, EXPIRATION_PERIOD, OWNER_NAME } from './constants/cardSection'; +import useCardBrandImage from './hooks/useCardBrandImage'; +import * as React from 'react'; +import { InitialCardNumberState } from './hooks/useCardNumber'; export const StyledInput = styled.input` border: 1px solid #acacac; @@ -20,12 +20,7 @@ export const StyledInput = styled.input` height: 32px; `; -export type InitialCardNumberState = { - value: string; - isError: boolean; -}; - -const InitialCardNumberState: InitialCardNumberState = { +const initialCardNumberState: InitialCardNumberState = { value: '', isError: false, }; @@ -62,80 +57,49 @@ const Wrapper = styled.div` gap: 8px; `; +const CARD_NUMBER_LENGTH = 4; + +const MONTH = Object.freeze({ + MIN: 1, + MAX: 12, +}); + +const MAX_LENGTH = Object.freeze({ + CARD_NUMBERS: 4, + MONTH: 2, + YEAR: 2, + NAME: 30, +}); + function App() { const { cardNumbers, cardNumbersChangeHandler } = useCardNumber( - Array.from({ length: 4 }, () => InitialCardNumberState), + Array.from({ length: CARD_NUMBER_LENGTH }, () => initialCardNumberState), ); - const [cardImageSrc, setCardImageSrc] = useState(''); + const { cardImageSrc } = useCardBrandImage(cardNumbers); const { - inputState: month, - inputChangeHandler: monthChangeHandler, + inputValue: month, + onChange: monthChangeHandler, error: monthError, - setError: setMonthError, - } = useInput(); + } = useInput([ + { + fn: (value) => + validate.isNumberInRange({ min: MONTH.MIN, max: MONTH.MAX, compareNumber: Number(value) }), + }, + { fn: (value) => validate.isValidDigit(value) }, + ]); const { - inputState: year, - inputChangeHandler: yearChangeHandler, + inputValue: year, + onChange: yearChangeHandler, error: yearError, - setError: setYearError, - } = useInput(); + } = useInput([{ fn: (value) => validate.isValidDigit(value) }]); const { - inputState: name, - inputChangeHandler: nameChangeHandler, + inputValue: name, + onChange: nameChangeHandler, error: nameError, - setError: setNameError, - } = useInput(); - - useEffect(() => { - if ( - month !== '' && - (!validate.isNumberInRange({ min: 1, max: 12, compareNumber: Number(month) }) || - !validate.isValidDigit(month)) - ) { - setMonthError(true); - - return; - } - - setMonthError(false); - }, [month, monthError]); - - useEffect(() => { - if (year !== '' && !validate.isValidDigit(year)) { - setYearError(true); - - return; - } - - setYearError(false); - }, [year, yearError]); - - useEffect(() => { - if (name !== '' && !validate.isEnglish(name)) { - setNameError(true); - - return; - } - - setNameError(false); - }, [name, nameError]); - - useEffect(() => { - setCardImageSrc(''); - - const cardNumberString = cardNumbers.map((cardNumber) => cardNumber.value).join(''); - - if (validate.isVisa(cardNumberString)) { - setCardImageSrc(VisaCardImage); - } - - if (validate.isMasterCard(cardNumberString)) { - setCardImageSrc(MasterCardImage); - } - }, [cardNumbers]); + } = useInput([{ fn: (value) => validate.isEnglish(value) }]); return ( <> @@ -158,19 +122,18 @@ function App() { {cardNumbers.map((cardNumber, index) => { const uniqueId = 'cardNumbers' + index; return ( - <> + ); })} @@ -193,7 +156,7 @@ function App() { placeholder={'MM'} type="text" value={month} - maxLength={2} + maxLength={MAX_LENGTH.MONTH} onChange={monthChangeHandler} isError={monthError} /> @@ -202,7 +165,7 @@ function App() { id={'year'} placeholder={'YY'} type="text" - maxLength={2} + maxLength={MAX_LENGTH.YEAR} value={year} onChange={yearChangeHandler} isError={yearError} @@ -222,7 +185,7 @@ function App() {