diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..28ee3331 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + rules: {}, + settings: { + react: { + version: 'detect', + }, + }, + env: { + es6: true, + browser: true, + node: true, + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..19632195 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ + + # See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# production +build + +# misc +.DS_Store +npm-debug.log +yarn-error.log diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..5085f417 --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,36 @@ +const { createConfigItem } = require('@babel/core'); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const path = require('path'); + +module.exports = { + stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/preset-scss'], + framework: '@storybook/react', + typescript: { + check: false, + checkOptions: {}, + reactDocgen: 'react-docgen-typescript', + reactDoggenTypescriptOptions: { + shouldExtractLiteralValuesFromEnum: true, + propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true), + }, + }, + webpackFinal: async (config) => { + config.resolve.plugins.push(new TsconfigPathsPlugin({})); + config.module.rules.push({ + test: /\.scss$/, + use: [ + 'style-loader', + 'css-loader', + { + loader: 'sass-loader', + options: { + // additionalData: `@import "src/assets/styles/card.scss"`, + }, + }, + ], + include: path.resolve(__dirname, '../'), + }); + return config; + }, +}; diff --git a/.storybook/preview.js b/.storybook/preview.js new file mode 100644 index 00000000..d448457a --- /dev/null +++ b/.storybook/preview.js @@ -0,0 +1,13 @@ +import { configure } from '@storybook/react'; + +export const parameters = { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; + +configure(require.context('../src', true, /\.stories\.js$/), module); diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..20950f53 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true, + "source.organizeImports": false + }, + "editor.formatOnSave": true, + "search.useIgnoreFiles": true, + "[javascript]": { + "editor.formatOnSave": true + }, + "[javascriptreact]": { + "editor.formatOnSave": true + }, + "[typescript]": { + "editor.formatOnSave": true + }, + "[typescriptreact]": { + "editor.formatOnSave": true + } +} \ No newline at end of file diff --git a/README.md b/README.md index 4c730c72..6d3dc1bb 100644 --- a/README.md +++ b/README.md @@ -26,54 +26,54 @@ ## ๐Ÿ“ Requirements ### ํ•„์ˆ˜ ์š”๊ตฌ์‚ฌํ•ญ -- [ ] `Storybook` ์ƒํ˜ธ ์ž‘์šฉ ํ…Œ์ŠคํŠธ -- [ ] `์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Component` ์ž‘์„ฑ +- [x] `Storybook` ์ƒํ˜ธ ์ž‘์šฉ ํ…Œ์ŠคํŠธ +- [x] `์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ Component` ์ž‘์„ฑ #### ์นด๋“œ ์ถ”๊ฐ€ -- [ ] `<`(๋’ค๋กœ๊ฐ€๊ธฐ) ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ, ์นด๋“œ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค. -- [ ] ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ 4์ž๋ฆฌ๋งˆ๋‹ค `-`๊ฐ€ ์‚ฝ์ž…๋œ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ UI์— ๋ฐ˜์˜๋œ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์•ž 8์ž๋ฆฌ๋งŒ ์ˆซ์ž๋กœ ๋ณด์—ฌ์ง€๊ณ , ๋‚˜๋จธ์ง€ ์ˆซ์ž๋Š” `*`๋กœ ๋ณด์—ฌ์ง„๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ ์•ž 8์ž๋ฆฌ๋กœ ์นด๋“œ์‚ฌ๋ฅผ ์ถ”์ •ํ•˜์—ฌ ๊ทธ ํ…Œ๋งˆ๋ฅผ ์นด๋“œ UI์— ๋ฐ˜์˜ํ•œ๋‹ค. - - [ ] ์นด๋“œ์‚ฌ๊ฐ€ ์„ ํƒ๋˜๊ณ  ์œ ํšจํ•œ ์นด๋“œ ๋ฒˆํ˜ธ 16์ž๋ฆฌ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•˜๋ฉด, ์ž๋™์œผ๋กœ ๋งŒ๋ฃŒ์ผ๋กœ focus๋œ๋‹ค. - - [ ] ์นด๋“œ ์•ž 8์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์นด๋“œ์‚ฌ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๋‚˜๋จธ์ง€ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ๋„ ์‹œ, ์นด๋“œ์‚ฌ ์„ ํƒ ๋ชจ๋‹ฌ์ด ๋ณด์—ฌ์ง„๋‹ค. - - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. -- [ ] ๋งŒ๋ฃŒ์ผ์„ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. - - [ ] ๋งŒ๋ฃŒ์ผ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] `MM / YY` ๋กœ placeholder๋ฅผ ์ ์šฉํ•œ๋‹ค. - - [ ] ์›”, ๋…„ ์‚ฌ์ด์— ์ž๋™์œผ๋กœ `/`๊ฐ€ ์‚ฝ์ž…๋œ๋‹ค. - - [ ] ๋งŒ๋ฃŒ์ผ์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ UI์— ๋ฐ˜์˜๋œ๋‹ค. - - [ ] ์›”์€ 1์ด์ƒ 12์ดํ•˜ ์ˆซ์ž์—ฌ์•ผ ํ•œ๋‹ค. - - [ ] ์›”, ๋…„ ์ž…๋ ฅ์ด ์œ ํšจํ•˜๋ฉด ๋ณด์•ˆ์ฝ”๋“œ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค - - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ๋งŒ๋ฃŒ์ผ์„ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. -- [ ] ๋ณด์•ˆ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. - - [ ] ์นด๋“œ ๋ณด์•ˆ์ฝ”๋“œ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] ๋ณด์•ˆ์ฝ”๋“œ๋Š” `*`์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค. - - [ ] ๋ณด์•ˆ์ฝ”๋“œ 3์ž๋ฆฌ๊ฐ€ ์ž…๋ ฅ๋˜๋ฉด ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค. - - [ ] ๋ณด์•ˆ์ฝ”๋“œ๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. - - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณด์•ˆ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. -- [ ] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ์•ž 2์ž๋ฆฌ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. - - [ ] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๊ฐ ํผ๋งˆ๋‹ค ํ•œ์ž๋ฆฌ ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. - - [ ] ์ฒซ์ž๋ฆฌ ์ž…๋ ฅ์ด ์™„๋ฃŒ๋˜๋ฉด ๋‘˜์งธ์ž๋ฆฌ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค. - - [ ] ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ, `*`์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค. - - [ ] ์œ ํšจํ•˜์ง€ ์•Š์€ ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. -- [ ] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„์„ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. - - [ ] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ ํผ ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] ์ด๋ฆ„์€ 30์ž๋ฆฌ๊นŒ์ง€ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค. - - [ ] ์ด๋ฆ„ ์ž…๋ ฅ ํผ ์œ„์—, ํ˜„์žฌ ์ž…๋ ฅ ์ž๋ฆฟ์ˆ˜์™€ ์ตœ๋Œ€ ์ž…๋ ฅ ์ž๋ฆฟ์ˆ˜๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค. -- [ ] ํด๋ฆญ ์‹œ, ์นด๋“œ ๋“ฑ๋ก ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค. +- [x] `<`(๋’ค๋กœ๊ฐ€๊ธฐ) ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ, ์นด๋“œ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค. +- [x] ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ 4์ž๋ฆฌ๋งˆ๋‹ค `-`๊ฐ€ ์‚ฝ์ž…๋œ๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ UI์— ๋ฐ˜์˜๋œ๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ๋Š” ์•ž 8์ž๋ฆฌ๋งŒ ์ˆซ์ž๋กœ ๋ณด์—ฌ์ง€๊ณ , ๋‚˜๋จธ์ง€ ์ˆซ์ž๋Š” `*`๋กœ ๋ณด์—ฌ์ง„๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ ์•ž 8์ž๋ฆฌ๋กœ ์นด๋“œ์‚ฌ๋ฅผ ์ถ”์ •ํ•˜์—ฌ ๊ทธ ํ…Œ๋งˆ๋ฅผ ์นด๋“œ UI์— ๋ฐ˜์˜ํ•œ๋‹ค. + - [x] ์นด๋“œ์‚ฌ๊ฐ€ ์„ ํƒ๋˜๊ณ  ์œ ํšจํ•œ ์นด๋“œ ๋ฒˆํ˜ธ 16์ž๋ฆฌ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•˜๋ฉด, ์ž๋™์œผ๋กœ ๋งŒ๋ฃŒ์ผ๋กœ focus๋œ๋‹ค. + - [x] ์นด๋“œ ์•ž 8์ž๋ฆฌ ์ˆซ์ž๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์นด๋“œ์‚ฌ๊ฐ€ ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ๋‚˜๋จธ์ง€ ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ๋„ ์‹œ, ์นด๋“œ์‚ฌ ์„ ํƒ ๋ชจ๋‹ฌ์ด ๋ณด์—ฌ์ง„๋‹ค. + - [x] ์œ ํšจํ•˜์ง€ ์•Š์€ ์นด๋“œ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. +- [x] ๋งŒ๋ฃŒ์ผ์„ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. + - [x] ๋งŒ๋ฃŒ์ผ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. + - [x] `MM / YY` ๋กœ placeholder๋ฅผ ์ ์šฉํ•œ๋‹ค. + - [x] ์›”, ๋…„ ์‚ฌ์ด์— ์ž๋™์œผ๋กœ `/`๊ฐ€ ์‚ฝ์ž…๋œ๋‹ค. + - [x] ๋งŒ๋ฃŒ์ผ์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์นด๋“œ UI์— ๋ฐ˜์˜๋œ๋‹ค. + - [x] ์›”์€ 1์ด์ƒ 12์ดํ•˜ ์ˆซ์ž์—ฌ์•ผ ํ•œ๋‹ค. + - [x] ์›”, ๋…„ ์ž…๋ ฅ์ด ์œ ํšจํ•˜๋ฉด ๋ณด์•ˆ์ฝ”๋“œ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค + - [x] ์œ ํšจํ•˜์ง€ ์•Š์€ ๋งŒ๋ฃŒ์ผ์„ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. +- [x] ๋ณด์•ˆ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. + - [x] ์นด๋“œ ๋ณด์•ˆ์ฝ”๋“œ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. + - [x] ๋ณด์•ˆ์ฝ”๋“œ๋Š” `*`์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค. + - [x] ๋ณด์•ˆ์ฝ”๋“œ 3์ž๋ฆฌ๊ฐ€ ์ž…๋ ฅ๋˜๋ฉด ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค. + - [x] ๋ณด์•ˆ์ฝ”๋“œ๋Š” ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. + - [x] ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณด์•ˆ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. +- [x] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ์•ž 2์ž๋ฆฌ๋ฅผ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. + - [x] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํผ์— ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. + - [x] ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๊ฐ ํผ๋งˆ๋‹ค ํ•œ์ž๋ฆฌ ์ˆซ์ž๋งŒ ์ž…๋ ฅ๊ฐ€๋Šฅํ•˜๋‹ค. + - [x] ์ฒซ์ž๋ฆฌ ์ž…๋ ฅ์ด ์™„๋ฃŒ๋˜๋ฉด ๋‘˜์งธ์ž๋ฆฌ ์ž…๋ ฅ์œผ๋กœ focus๋œ๋‹ค. + - [x] ์นด๋“œ ๋ฒˆํ˜ธ ์ž…๋ ฅ ์‹œ, `*`์œผ๋กœ ๋ณด์—ฌ์ง„๋‹ค. + - [x] ์œ ํšจํ•˜์ง€ ์•Š์€ ์นด๋“œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, ์ž…๋ ฅ ํผ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. +- [x] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„์„ ์ž…๋ ฅ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. + - [x] ์นด๋“œ ์†Œ์œ ์ž ์ด๋ฆ„ ์ž…๋ ฅ ํผ ๋ผ๋ฒจ์„ ๋ณด์—ฌ์ค€๋‹ค. + - [x] ์ด๋ฆ„์€ 30์ž๋ฆฌ๊นŒ์ง€ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค. + - [x] ์ด๋ฆ„ ์ž…๋ ฅ ํผ ์œ„์—, ํ˜„์žฌ ์ž…๋ ฅ ์ž๋ฆฟ์ˆ˜์™€ ์ตœ๋Œ€ ์ž…๋ ฅ ์ž๋ฆฟ์ˆ˜๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ค€๋‹ค. +- [x] ํด๋ฆญ ์‹œ, ์นด๋“œ ๋“ฑ๋ก ์™„๋ฃŒ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค. ### ์‹ฌํ™” ์š”๊ตฌ์‚ฌํ•ญ (์„ ํƒ์‚ฌํ•ญ) - [ ] `Storybook` ๋‹จ์œ„ ํ…Œ์ŠคํŠธ -- [ ] ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ์— ๋Œ€ํ•œ UI/UX ์ถ”๊ฐ€ -- [ ] ์นด๋“œ์‚ฌ ์„ ํƒ - - [ ] ์นด๋“œ์‚ฌ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๊ณ , ๊ทธ ํ…Œ๋งˆ๋ฅผ ์นด๋“œ UI์— ๋ฐ˜์˜ํ•œ๋‹ค. - - [ ] ์นด๋“œ์‚ฌ๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์•„๋„ ๋ชจ๋‹ฌ์„ ๋‹ซ์„ ์ˆ˜ ์žˆ๋‹ค. -- [ ] ๋ณด์•ˆ์ฝ”๋“œ ํˆดํŒ - - [ ] ํด๋ฆญ ์‹œ, ๋ณด์•ˆ์ฝ”๋“œ ๊ด€๋ จ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. - - [ ] focusout ์‹œ, ํˆดํŒ์ด ๋‹ซํžŒ๋‹ค. +- [x] ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ์— ๋Œ€ํ•œ UI/UX ์ถ”๊ฐ€ +- [x] ์นด๋“œ์‚ฌ ์„ ํƒ + - [x] ์นด๋“œ์‚ฌ๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๊ณ , ๊ทธ ํ…Œ๋งˆ๋ฅผ ์นด๋“œ UI์— ๋ฐ˜์˜ํ•œ๋‹ค. + - [x] ์นด๋“œ์‚ฌ๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์•„๋„ ๋ชจ๋‹ฌ์„ ๋‹ซ์„ ์ˆ˜ ์žˆ๋‹ค. +- [x] ๋ณด์•ˆ์ฝ”๋“œ ํˆดํŒ + - [x] ํด๋ฆญ ์‹œ, ๋ณด์•ˆ์ฝ”๋“œ ๊ด€๋ จ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด์—ฌ์ค€๋‹ค. + - [x] focusout ์‹œ, ํˆดํŒ์ด ๋‹ซํžŒ๋‹ค. - [ ] ๊ฐ€์ƒ ํ‚ค๋ณด๋“œ - [ ] ๋งˆ์Šคํ‚น ์ฒ˜๋ฆฌ๋œ ๊ฐ’ ์ž…๋ ฅ์‹œ ์‚ฌ์šฉ - [ ] ์ˆซ์ž๋ฅผ ๋žœ๋ค์œผ๋กœ ๋ฐฐ์—ด @@ -113,3 +113,15 @@ - [ ] ๋“ฑ๋ก๋œ ์นด๋“œ ์ •๋ณด๋ฅผ CRUD ํ•ฉ๋‹ˆ๋‹ค. - [ ] ๋‚˜์—ด๋œ ์นด๋“œ ํด๋ฆญ์‹œ `์นด๋“œ ์ถ”๊ฐ€ ํ™•์ธ` ํ™”๋ฉด ์žฌํ™œ์šฉ - [ ] ๋ณ„์นญ ์ˆ˜์ • ๊ฐ€๋Šฅ + + + +#### ๊ณ ๋ฏผํ–ˆ๋˜ ๋‚ด์šฉ๋“ค +- form elements ์ด๋™๋“ค vs ref๋กœ ์ผ์ผํžˆ ์ง€์ • +- ํ•œ ํŽ˜์ด์ง€์—์„œ ๋ณด์—ฌ์ฃผ๋Š” ์ถ”์ƒํ™” ๋ ˆ๋ฒจ์„ ๋™์ผํ•˜๊ฒŒ ํ•˜์ž(์ฝ๋Š”์ด์—๊ฒŒ ํŽธํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด) +- ํด๋” ๊ตฌ์กฐ(page, component, share component) +- ํ”„๋กœ์ ํŠธ๋‚ด์—์„œ css-in-js๋งŒ ์“ฐ๋“ ์ง€ css๋งŒ ์“ฐ๋“ ์ง€ ํ†ต์ผํ•˜๋Š”๊ฒŒ ์ข‹์€๊ฑด์ง€?? +- setter๋ฅผ ์ผ์ผํžˆ ๋„˜๊ฒจ์ฃผ๋Š”๊ฒƒ์ด ๋ฐ˜๋ณต๋˜๊ณ  ์žˆ๋Š”๋ฐ ์ด๊ฑธ context provider๋ฅผ ์ด์šฉํ•˜๋ฉด ์ข‹์€๊ฑด๊ฐ€?? ์ผ์ผํžˆ ์ž…๋ ฅํ•ด์ฃผ์ง€ ์•Š์•„๋„ ๋˜์„œ?? ๊ทผ๋ฐ ๋‚˜์ค‘์— ๋ณด๋ฉด ๋ชจ๋ฅผ์ˆ˜๋„ ์žˆ์ง€ ์•Š์„๊นŒ? ==> ์ด๋ž˜์„œ store, recoil์ด ๋‚˜์˜จ๊ฑด๊ฐ€?? +- type๊ณผ interface์ค‘ ์ผ๋‹จ์€ interface๋กœ ์ „๋ถ€ ํ†ต์ผํ–ˆ๋Š”๋ฐ ๋ญ๊ฐ€ ์–ด๋–ค ์ƒํ™ฉ์— ์“ฐ์ด๋Š”๊ฒŒ ์ ์ ˆํ•œ์ง€ ์•Œ์•„์•ผ๊ฒ ๋‹ค. +- cardNewPage์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋Š”๊ฒƒ์„ ๊ต‰์žฅํžˆ ๊ณ ๋ฏผํ–ˆ๋Š”๋ฐ ์ปดํฌ๋„ŒํŠธ ์ž์ฒด์˜ ์ƒ์„ฑ ๊ธฐ์ค€์ด ๋ญ˜์ง€ ์ฐพ์•„๋ด์•ผ๊ฒ ๋‹ค. +- store ๊ฐœ๋…์„ ๊ตฌํ˜„ํ•ด๋ณด์ž \ No newline at end of file diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..c08e96e5 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + '@babel/preset-react', + '@babel/preset-typescript', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: ['@babel/plugin-transform-runtime'], +}; diff --git a/index.html b/info.html similarity index 100% rename from index.html rename to info.html diff --git a/package.json b/package.json new file mode 100644 index 00000000..e1dda969 --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "name": "react-payments", + "version": "1.0.0", + "description": "

ํŽ˜์ด๋จผ์ธ 

React ๋ชจ๋ฐ”์ผ ํŽ˜์ด๋จผ์ธ  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "npm run dev", + "dev": "cross-env NODE_ENV=development webpack serve --config ./webpack/webpack.config.js --host 0.0.0.0", + "build": "cross-env NODE_ENV=production node ./webpack/build.js", + "storybook": "start-storybook -p 6006", + "build-storybook": "build-storybook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/woobottle/react-payments.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/woobottle/react-payments/issues" + }, + "homepage": "https://github.com/woobottle/react-payments#readme", + "dependencies": { + "cross-env": "^7.0.3", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-icons": "^4.3.1", + "react-router-dom": "^6.2.1", + "styled-components": "^5.3.3", + "tsconfig-paths-webpack-plugin": "^3.5.2", + "typescript": "^4.5.5" + }, + "devDependencies": { + "@babel/core": "^7.16.12", + "@babel/plugin-transform-runtime": "^7.16.10", + "@babel/preset-env": "^7.16.11", + "@babel/preset-react": "^7.16.7", + "@babel/preset-typescript": "^7.16.7", + "@babel/runtime": "^7.16.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", + "@storybook/addon-actions": "^6.4.14", + "@storybook/addon-essentials": "^6.4.14", + "@storybook/addon-links": "^6.4.14", + "@storybook/preset-scss": "^1.0.3", + "@storybook/react": "^6.4.14", + "@types/react": "^17.0.38", + "@types/react-dom": "^17.0.11", + "@types/styled-components": "^5.1.21", + "@typescript-eslint/eslint-plugin": "^5.10.0", + "@typescript-eslint/parser": "^5.10.0", + "babel-core": "^7.0.0-beta.3", + "babel-loader": "^8.2.3", + "chalk": "^5.0.0", + "copy-webpack-plugin": "^10.2.1", + "css-loader": "^5", + "css-minimizer-webpack-plugin": "^3.4.1", + "dotenv-webpack": "^7.0.3", + "eslint": "^8.7.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-airbnb": "^0.0.1-security", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.28.0", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.5.0", + "mini-css-extract-plugin": "^2.5.2", + "ora": "^6.0.1", + "postcss-loader": "^6.2.1", + "prettier": "^2.5.1", + "react-refresh": "^0.11.0", + "rimraf": "^3.0.2", + "sass": "^1.49.0", + "sass-loader": "^10", + "source-map-loader": "^3.0.1", + "style-loader": "^2", + "terser-webpack-plugin": "^5.3.0", + "ts-loader": "^9.2.6", + "url-loader": "^4.1.1", + "webpack": "^5.67.0", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.7.3" + } +} diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..0cd0580f --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,10 @@ +module.exports = { + printWidth: 240, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: true, + trailingComma: 'all', + bracketSpacing: true, + arrowParens: 'always', +}; diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 00000000..d2245d11 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,22 @@ +import React, { useState } from 'react'; +import { Routes, Route } from 'react-router-dom'; +import { CardIndexPage, CardNewPage, CardCompletePage } from './pages/Card'; +import { CardProps } from '@interface'; +import { initialNewCard } from '@constants'; + +const App = () => { + const [cards, setCards] = useState([]); + const [newCard, setNewCard] = useState(initialNewCard); + + return ( +
+ + } /> + } /> + } /> + +
+ ); +}; + +export default App; diff --git a/src/assets/styles/button.css b/src/assets/styles/button.css new file mode 100644 index 00000000..972ce4c3 --- /dev/null +++ b/src/assets/styles/button.css @@ -0,0 +1,8 @@ +.button-box { + width: 100%; + text-align: right; +} + +.button-text { + margin-right: 10px; +} diff --git a/src/assets/styles/card.css b/src/assets/styles/card.css new file mode 100644 index 00000000..8f5e8b8e --- /dev/null +++ b/src/assets/styles/card.css @@ -0,0 +1,136 @@ +.card-box { + display: flex; + align-items: center; + justify-content: center; + + margin: 10px 0; +} + +.empty-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 208px; + height: 130px; + + font-size: 30px; + color: #575757; + + background: #e5e5e5; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); + border-radius: 5px; + + user-select: none; +} + +.small-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 208px; + height: 130px; + + background: #94dacd; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); + border-radius: 5px; +} + +.small-card__chip { + width: 40px; + height: 26px; + left: 95px; + top: 122px; + + background: #cbba64; + border-radius: 4px; +} + +.big-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 290px; + height: 180px; + + background: #94dacd; + box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.25); + border-radius: 5px; +} + +.big-card__chip { + width: 55.04px; + height: 35.77px; + + background: #cbba64; + border-radius: 4px; + + font-size: 24px; +} + +.card-top { + width: 100%; + height: 100%; + + display: flex; + align-items: center; +} + +.card-middle { + width: 100%; + height: 100%; + margin-left: 30px; + + display: flex; + align-items: center; +} + +.card-bottom { + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + align-items: center; +} + +.card-bottom__number { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} + +.card-bottom__info { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + justify-content: space-between; +} + +.card-text { + margin: 0 16px; + + font-size: 14px; + line-height: 16px; + vertical-align: middle; + font-weight: 400; +} + +.card-text__big { + margin: 0 16px; + + font-size: 18px; + line-height: 20px; + vertical-align: middle; + font-weight: 400; +} \ No newline at end of file diff --git a/src/assets/styles/index.css b/src/assets/styles/index.css new file mode 100644 index 00000000..543392f5 --- /dev/null +++ b/src/assets/styles/index.css @@ -0,0 +1,73 @@ +@import "./card.css"; +@import "./input.css"; +@import "./button.css"; +@import "./modal.css"; +@import "./utils.css"; + +body { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; + justify-content: center; + background-color: #e5e5e5; +} + +input { + font-size: 16px; +} + +.root { + background-color: #fff; + width: 375px; + min-width: 375px; + height: 730px; + position: relative; + border-radius: 15px; +} + +.app { + height: 100%; + padding: 16px 24px; +} + +.page-title { + font-weight: 500; + font-size: 20px; + line-height: 22px; + display: flex; + align-items: center; + + color: #383838; +} + +.title-with-tooltip { + display: flex; + align-items: center; + width: 40%; + justify-content: space-between; +} + +.tooltip { + position: absolute; + font-size: 0.5rem; + left: 35%; + width: 40%; + background-color: cornflowerblue; + padding: 0.5rem; + color: white; + margin-top: 0.5rem; + line-height: 1rem; + text-align: left; +} + +.tooltip::before { + content: ''; + width: 0; + height: 0; + position: absolute; + top: -1rem; + border-width: 8px; + border-color: transparent transparent cornflowerblue transparent; + border-style: solid; +} \ No newline at end of file diff --git a/src/assets/styles/input.css b/src/assets/styles/input.css new file mode 100644 index 00000000..505b0584 --- /dev/null +++ b/src/assets/styles/input.css @@ -0,0 +1,64 @@ +.input-container { + margin: 16px 0; +} + +.input-box { + display: flex; + align-items: center; + margin-top: 0.375rem; + color: #d3d3d3; + border-radius: 0.25rem; +} + +.input-title { + display: flex; + align-items: center; + + font-size: 12px; + line-height: 14px; + + margin-bottom: 4px; + + color: #525252; +} + +.input-basic { + background-color: #ecebf1; + height: 45px; + width: 100%; + text-align: center; + outline: 2px solid transparent; + outline-offset: 2px; + border-color: #9ca3af; + border: none; + border-radius: 0.25rem; + margin-right: 0.5rem; +} + +.input-underline { + text-align: center; + border: none; + background: none; + outline: none; + + margin: 16px 0; + padding: 4px 0; + + border-bottom: 1px solid #383838; +} + +.space-between { + justify-content: space-between; +} + +.password-placeholder { + text-align: center; + margin: auto 0; + color: black; +} + +.input-error-msg { + font-size: 0.5rem; + margin-top: 0.25rem; + color: red; +} \ No newline at end of file diff --git a/src/assets/styles/modal.css b/src/assets/styles/modal.css new file mode 100644 index 00000000..d86fa3b0 --- /dev/null +++ b/src/assets/styles/modal.css @@ -0,0 +1,53 @@ +.modal { + width: 375px; + height: 220px; + + border-radius: 5px 5px 15px 15px; + + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; + + background: #fff; + z-index: 10; +} + +.modal-dimmed { + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: flex-end; + + position: absolute; + top: 0; + left: 0; + + background: rgba(0, 0, 0, 0.5); + + border-radius: 15px; + + z-index: 5; +} + +.modal-item-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.modal-item-dot { + margin: 0.5rem 1rem; + border-radius: 50%; + width: 2.8rem; + height: 2.8rem; + background-color: #94dacd; +} + +.modal-item-name { + font-size: 12px; + letter-spacing: -0.085rem; +} diff --git a/src/assets/styles/utils.css b/src/assets/styles/utils.css new file mode 100644 index 00000000..e86f525f --- /dev/null +++ b/src/assets/styles/utils.css @@ -0,0 +1,56 @@ +.flex-center { + display: flex; + justify-content: center; + align-items: center; +} + +.flex-column-center { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-20 { + margin-top: 5rem; +} + +.mt-30 { + margin-top: 7.5rem; +} + +.mt-40 { + margin-top: 9rem; +} + +.mt-50 { + margin-top: 11.5rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.w-100 { + width: 100%; +} + +.w-75 { + width: 75%; +} + +.w-50 { + width: 50%; +} + +.w-25 { + width: 25%; +} + +.w-15 { + width: 15%; +} diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx new file mode 100644 index 00000000..c295f169 --- /dev/null +++ b/src/components/card/Card.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { CardProps } from '@interface'; +import { PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX } from '@constants'; + +interface CardInfo { + cardInfo: CardProps; + type?: string; +} + +const Card = ({ cardInfo, type }: CardInfo) => { + const { company, cardNumber, ownerName, expireMonth, expireYear, nickname, bgColor } = cardInfo; + + return ( + <> +
+
+
+
{company}
+
+
+
+
+
+
+ + {cardNumber && + cardNumber + .filter((el) => el !== '') + .map((el, index) => (index <= PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX ? el : '*'.repeat(el.length))) + .join(' - ')} + +
+
+ {ownerName || 'NAME'} + + {expireMonth || 'MM'} / {expireYear || 'YY'} + +
+
+
+
+ {nickname} + + ); +}; + +export default Card; diff --git a/src/components/card/CardForm.tsx b/src/components/card/CardForm.tsx new file mode 100644 index 00000000..c5cc726e --- /dev/null +++ b/src/components/card/CardForm.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +interface CardFormProps { + children: React.ReactNode; + onSubmit?: React.FormEventHandler | undefined; +} + +interface CardFormTitleProps { + title: string; + onClick?: React.MouseEventHandler | undefined; +} + +interface CardFormButtonProps { + children?: React.ReactNode; +} + +const CardForm = ({ children, onSubmit }: CardFormProps) => { + return
{children}
; +}; + +CardForm.title = ({ onClick, title }: CardFormTitleProps) => { + return ( +

+ {title} +

+ ); +}; + +CardForm.button = ({ children }: CardFormButtonProps) => { + return ( +
+ +
+ ); +}; + +export default CardForm; diff --git a/src/components/card/CompanyModal.tsx b/src/components/card/CompanyModal.tsx new file mode 100644 index 00000000..571542a9 --- /dev/null +++ b/src/components/card/CompanyModal.tsx @@ -0,0 +1,50 @@ +import { CardProps } from '@interface'; +import React from 'react'; +import { CARD_COMPANIES } from '@constants'; + +interface CompanyModalProps { + setNewCard: React.Dispatch>; + setCompanyModalOpened: React.Dispatch>; +} + +const CompanyModal = ({ setNewCard, setCompanyModalOpened }: CompanyModalProps) => { + const modalClose = () => { + setCompanyModalOpened(false); + }; + + const companyClick = (event: React.MouseEvent) => { + const { name, color } = (event.target as HTMLElement).dataset; + setCompanyModalOpened(false); + setNewCard((prevVal) => { + return { + ...prevVal, + company: `${name}`, + bgColor: color, + }; + }); + }; + + return ( +
+
+
+ x +
+
+ {CARD_COMPANIES && + CARD_COMPANIES.map((company) => { + const { name, color } = company; + return ( +
+
+ {name} +
+ ); + })} +
+
+
+ ); +}; + +export default CompanyModal; diff --git a/src/components/card/index.ts b/src/components/card/index.ts new file mode 100644 index 00000000..93ebd8bc --- /dev/null +++ b/src/components/card/index.ts @@ -0,0 +1,10 @@ +import Card from './Card'; +import CardForm from './CardForm'; +import CardNumberInput from './inputs/CardNumberInput'; +import ExpireDateInput from './inputs/ExpireDateInput'; +import CvcInput from './inputs/CvcInput'; +import OwnerNameInput from './inputs/OwnerNameInput'; +import PasswordInput from './inputs/PasswordInput'; +import CompanyModal from './CompanyModal'; + +export { Card, CardForm, CardNumberInput, ExpireDateInput, CvcInput, OwnerNameInput, PasswordInput, CompanyModal }; diff --git a/src/components/card/inputs/CardNumberInput.tsx b/src/components/card/inputs/CardNumberInput.tsx new file mode 100644 index 00000000..140cea7a --- /dev/null +++ b/src/components/card/inputs/CardNumberInput.tsx @@ -0,0 +1,108 @@ +import React, { useCallback, useState } from 'react'; +import { Input, InputContainer } from '@share'; +import { CardProps } from '@interface'; +import { focusOnNextRequiredInput, getCardBackGroundColor, getErrorMsgByType, isIndividualCardNumberInputValid, isPublicCardNumberInCardCompany, isStringInputed, isTotalCardNumberValid } from '@utils'; +import { PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX, CARD_NUMBER_TOTAL_LAST_INDEX, PUBLIC_CARD_NUMBER_LENGTH } from '@constants'; + +interface CardNumberInputProps { + newCard: CardProps; + setNewCard: React.Dispatch>; + setCompanyModalOpened: React.Dispatch>; +} + +const CardNumberInput = ({ newCard, setNewCard, setCompanyModalOpened }: CardNumberInputProps) => { + const [isCardNumberError, setIsCardNumberError] = useState(false); + const [isPublicCardNumberCompleted, setIsPublicCardNumberCompleted] = useState(false); + const { cardNumber, company } = newCard; + + const getCurrentCardNumberOnInputIndex = useCallback( + (index, value) => { + const sliceIndex = index <= PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX ? PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX + 1 : CARD_NUMBER_TOTAL_LAST_INDEX + 1; + const tempArray = cardNumber.slice(0, sliceIndex); + tempArray[index] = value; + return tempArray.join(''); + }, + [cardNumber], + ); + + const cardNumberHandler = (changeEvent: React.ChangeEvent) => { + const { target } = changeEvent; + const { + dataset: { index: targetIndex }, + } = target; + + if (isStringInputed(target.value)) { + setIsCardNumberError(true); + return; + } + + target.value = target.value.replace(/[^0-9]/g, ''); + const index = Number(targetIndex); + + setNewCard((prevVal) => { + prevVal.cardNumber[index] = target.value; + return { ...prevVal }; + }); + + if (isIndividualCardNumberInputValid(target.value)) { + setIsCardNumberError(false); + } + + // ๋‘๋ฒˆ์งธ ๋ถ€ํ„ฐ ์ž…๋ ฅํ•˜๋Š” ๊ฒฝ์šฐ๋„ ๊ณ ๋ ค + if (index <= PUBLIC_CARD_NUMBER_INPUT_LAST_INDEX) { + const publicCardNumber = getCurrentCardNumberOnInputIndex(index, target.value); + + if (publicCardNumber.length !== PUBLIC_CARD_NUMBER_LENGTH) { + return; + } + + setIsPublicCardNumberCompleted(true); + + if (isPublicCardNumberInCardCompany(publicCardNumber)) { + const bgColor = getCardBackGroundColor(publicCardNumber); + setNewCard((prevVal) => { + return { + ...prevVal, + bgColor, + }; + }); + return; + } + + setCompanyModalOpened(true); + } + + if (index === CARD_NUMBER_TOTAL_LAST_INDEX) { + const totalCardNumber = getCurrentCardNumberOnInputIndex(index, target.value); + if (isTotalCardNumberValid(totalCardNumber)) { + focusOnNextRequiredInput(changeEvent); + } + } + }; + + const isNeedCompanySelect = useCallback( + (event: React.KeyboardEvent) => { + event.preventDefault(); + if (isPublicCardNumberCompleted && company === '') { + setCompanyModalOpened(true); + return true; + } + return false; + }, + [isPublicCardNumberCompleted, company], + ); + + return ( + + + + + + + + + {isCardNumberError && } + + ); +}; +export default CardNumberInput; diff --git a/src/components/card/inputs/CvcInput.tsx b/src/components/card/inputs/CvcInput.tsx new file mode 100644 index 00000000..9c410c2f --- /dev/null +++ b/src/components/card/inputs/CvcInput.tsx @@ -0,0 +1,51 @@ +import { CardProps } from '@interface'; +import { Input, InputContainer } from '@share'; +import { focusOnNextRequiredInput, getErrorMsgByType, isCvcValid, isStringInputed } from '@utils'; +import React, { useState } from 'react'; +import { AiOutlineQuestionCircle } from 'react-icons/ai'; + +interface CvcInputProps { + setNewCard: React.Dispatch>; +} + +const CvcInput = ({ setNewCard }: CvcInputProps) => { + const [isCvcError, setIsCvcError] = useState(false); + const [tooltipOpened, setTooltipOpened] = useState(false); + + const cardCvcHandler = (changeEvent: React.ChangeEvent) => { + const { target } = changeEvent; + if (isStringInputed(target.value)) { + setIsCvcError(true); + return; + } + + target.value = target.value.replace(/[^0-9]/g, ''); + setNewCard((prevVal) => { + prevVal['cvcNumber'] = target.value; + return prevVal; + }); + + if (isCvcValid(target.value)) { + setIsCvcError(false); + focusOnNextRequiredInput(changeEvent); + } + }; + + return ( + + +
+ ๋ณด์•ˆ์ฝ”๋“œ(CVC/CVV) + setTooltipOpened(!tooltipOpened)} size="1rem" /> +
+
+ {tooltipOpened &&
CVV/CVC ๋ฒˆํ˜ธ๋Š” ์นด๋“œ ๋’ท ๋ฉด์— ์žˆ๋Š” 3์ž๋ฆฌ ์ˆซ์ž์ด๋ฉฐ ์นด๋“œ ๋ณด์•ˆ์„ ์œ„ํ•œ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.
} + + + + {isCvcError && } +
+ ); +}; + +export default CvcInput; diff --git a/src/components/card/inputs/ExpireDateInput.tsx b/src/components/card/inputs/ExpireDateInput.tsx new file mode 100644 index 00000000..53a9c09b --- /dev/null +++ b/src/components/card/inputs/ExpireDateInput.tsx @@ -0,0 +1,65 @@ +import React, { useState } from 'react'; +import { Input, InputContainer } from '@share'; +import { focusOnNextRequiredInput, getErrorMsgByType, isMonthValid, isStringInputed, isYearValid } from '@utils'; +import { CardProps } from '@interface'; + +interface ExpireDateInputProps { + setNewCard: React.Dispatch>; +} + +const ExpireDateInput = ({ setNewCard }: ExpireDateInputProps) => { + const [isExpireDateError, setIsExpireDateError] = useState(false); + + const cardMonthHandler = (changeEvent: React.ChangeEvent) => { + const { target } = changeEvent; + if (isStringInputed(target.value)) { + setIsExpireDateError(true); + return; + } + + setNewCard((prevVal) => { + prevVal['expireMonth'] = target.value; + return { ...prevVal }; + }); + + if (isMonthValid(target.value)) { + setIsExpireDateError(false); + focusOnNextRequiredInput(changeEvent); + } else { + setIsExpireDateError(true); + } + }; + + const cardYearHandler = (changeEvent: React.ChangeEvent) => { + const { target } = changeEvent; + if (isStringInputed(target.value)) { + setIsExpireDateError(true); + return; + } + + setNewCard((prevVal) => { + prevVal['expireYear'] = target.value; + return { ...prevVal }; + }); + + if (isYearValid(target.value)) { + setIsExpireDateError(false); + focusOnNextRequiredInput(changeEvent); + } else { + setIsExpireDateError(true); + } + }; + + return ( + + + + + + + {isExpireDateError && } + + ); +}; + +export default ExpireDateInput; diff --git a/src/components/card/inputs/OwnerNameInput.tsx b/src/components/card/inputs/OwnerNameInput.tsx new file mode 100644 index 00000000..b1446380 --- /dev/null +++ b/src/components/card/inputs/OwnerNameInput.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Input, InputContainer } from '@share'; +import { CardProps } from '@interface'; + +interface OwnerNameInput { + setNewCard: React.Dispatch>; + ownerName?: string; +} + +const OwnerNameInput = ({ setNewCard, ownerName }: OwnerNameInput) => { + const cardOwnerNameHandler = (changeEvent: React.ChangeEvent) => { + setNewCard((prevVal) => { + prevVal['ownerName'] = changeEvent.target.value; + return { ...prevVal }; + }); + }; + + return ( + + + {ownerName && `${ownerName.length}/30`} + + + + + + ); +}; + +export default OwnerNameInput; diff --git a/src/components/card/inputs/PasswordInput.tsx b/src/components/card/inputs/PasswordInput.tsx new file mode 100644 index 00000000..67ce3a33 --- /dev/null +++ b/src/components/card/inputs/PasswordInput.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { InputContainer, Input } from '@share'; +import { focusOnNextRequiredInput, getErrorMsgByType, isPasswordValid, isStringInputed } from '@utils'; +import { CardProps } from '@interface'; + +interface PasswordInputProps { + setNewCard: React.Dispatch>; +} + +const PasswordInput = ({ setNewCard }: PasswordInputProps) => { + const [isPasswordError, setIsPasswordError] = useState(false); + + const cardPasswordHandler = (changeEvent: React.ChangeEvent) => { + const { target } = changeEvent; + + if (isStringInputed(target.value)) { + setIsPasswordError(true); + return; + } + + const { + dataset: { index: targetIndex }, + } = target; + + const index = Number(targetIndex); + const value = target.value; + + setNewCard((prevVal) => { + const { password } = prevVal; + if (password) password[index] = value; + return { ...prevVal }; + }); + + if (isPasswordValid(value)) { + setIsPasswordError(false); + focusOnNextRequiredInput(changeEvent); + } + }; + + return ( + + + + + +
*
+
*
+
+ {isPasswordError && } +
+ ); +}; + +export default PasswordInput; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..75e2a91c --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export * from './card/index'; +export * from './share/index'; diff --git a/src/components/share/Input.tsx b/src/components/share/Input.tsx new file mode 100644 index 00000000..5368a27d --- /dev/null +++ b/src/components/share/Input.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +interface InputProps { + id: string; + type: string; + required?: boolean; + className?: string; + maxLength?: number; + placeHolder?: string; + onChange?: (changeEvent: React.ChangeEvent) => void; + dataIndex?: string; + onKeyPress?: (pressEvent: React.KeyboardEvent) => void; +} + +const Input = ({ type, className, onChange, maxLength, id, required, placeHolder, dataIndex, onKeyPress }: InputProps) => { + return ( + <> +