diff --git a/README.md b/README.md
index af19b7ef1..a9c7d1769 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,52 @@
# react-modules
+
+### Modal component
+
+- [x] 모달 props
+ - [x] 모달 위치
+ - [x] 모달 타이틀
+ - [x] 모달 내용
+ - [x] 모달 닫는 방식
+ - [x] prop 이름 : closeButtonPosition : 'top' | 'bottom'
+- [x] 모달 event
+ - [x] 열기
+ - [x] 닫기 - deem 눌러도 닫혀야된다.
+ - [ ] 확인 - optional
+- [x] npm으로 배포하기
+- [x] 설치 후 import해서 사용하기
+
+### Payment custom hook
+
+- 유효성 검사 결과와 에러 정보를 반환한다.
+
+- [x] useCardNumber
+ - [x] 숫자여야한다.
+ - [x] 16자리여야한다.
+- [x] useCardHolder
+ - [x] 영어 대문자+공백만 입력가능하다.
+ - [x] 공백 포함 15자까지만 가능하다.
+- [x] useExpiryDate
+ - [x] 월은 1~12만 입력 가능하다. (월도 두자리로 입력해달라는 description 추가 )
+ - [x] 년도 2자리 숫자만 입력 가능하다.
+ - [x] 년,월 조합을 봤을 때 오늘보다 과거이면 에러를 낸다.
+- [x] useCVC
+ - [x] 3자리 숫자만 입력 가능하다.
+- [x] useCardType
+ - [x] 선택한 값이 있는지 검증한다.
+- [x] usePassword
+ - [x] 2자리 숫자만 입력 가능하다.
+- [x] npm으로 배포하기
+- [x] 설치 후 import해서 사용하기
+
+ ### Storybook
+
+ - [x] 모달 위치에 대한 테스트 시나리오
+ - [x] 모달 내용에 대한 테스트 시나리오
+ - [x] 모달 이벤트 핸들러에 대한 테스트 시나리오
+
+ ### RTL
+
+ - [ ] 페이먼트 유효성 검사 커스텀 훅의 동작을 검증
+ - [ ] 다양한 입력 값에 대한 커스텀 훅의 결과
+ - [ ] 유효성 통과하는 경우
+ - [ ] 유효성 통과하지 않는 경우
diff --git a/components/.prettierrc b/components/.prettierrc
new file mode 100644
index 000000000..efd721c1c
--- /dev/null
+++ b/components/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "printWidth": 100,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "semi": true,
+ "singleQuote": false,
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "endOfLine": "auto"
+}
diff --git a/components/README.md b/components/README.md
index 47f87aca6..b63255df8 100644
--- a/components/README.md
+++ b/components/README.md
@@ -1 +1,63 @@
-# Button Module
+# choco-modal-component
+
+## Install
+
+이 라이브러리를 사용하기 위해서는 다음 패키지를 설치해야 합니다:
+
+```bash
+npm install choco-modal-component
+```
+
+## Usage
+
+React 컴포넌트에서 모달 라이브러리를 사용하려면 다음 단계를 따르세요
+
+```jsx
+import { useModal, Modal } from "choco-modal-component";
+
+function App() {
+ const { isOpen, openModal, closeModal } = useModal();
+
+ return (
+ <>
+ Open Modal
+
+
+ {/* 모달 내용 */}
+
+ >
+ );
+}
+
+export default App;
+```
+
+## API
+
+### ModalComponent
+
+`ModalComponent`는 다음 props를 받습니다:
+
+| props | description |
+| ---------------------------- | --------------------------------------------------------------- |
+| `modalPosition` (필수) | 모달의 위치입니다. 가능한 값은 `"center"`와 `"bottom"`입니다. |
+| `title` (필수) | 모달의 제목입니다. |
+| `children` (필수) | 모달의 내용입니다. |
+| `closeButtonPosition` (필수) | 닫기 버튼의 위치입니다. 가능한 값은 `"top"`과 `"bottom"`입니다. |
+
+### useModal
+
+`useModal` 훅은 다음 속성을 가진 객체를 반환합니다:
+
+| props | description |
+| ---------------- | --------------------------------------------------------- |
+| `isOpen` | 모달이 현재 열려 있는지 여부를 나타내는 boolean 값입니다. |
+| `openModal` | 모달을 열기 위한 함수입니다. |
+| `closeModal` | 모달을 닫기 위한 함수입니다. |
+| `ModalComponent` | 지정된 props로 모달을 렌더링하는 컴포넌트입니다. |
diff --git a/components/custom.d.ts b/components/custom.d.ts
new file mode 100644
index 000000000..66b245666
--- /dev/null
+++ b/components/custom.d.ts
@@ -0,0 +1,4 @@
+declare module "*.png" {
+ const value: string;
+ export default value;
+}
diff --git a/components/package-lock.json b/components/package-lock.json
index 9fbf5f751..f2e82ab25 100644
--- a/components/package-lock.json
+++ b/components/package-lock.json
@@ -1,13 +1,17 @@
{
- "name": "compoents",
- "version": "0.0.1",
+ "name": "choco-modal-component",
+ "version": "0.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "compoents",
- "version": "0.0.1",
+ "name": "choco-modal-component",
+ "version": "0.0.3",
"dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "choco-modal-component": "^0.0.2",
+ "choriver-modal-component": "^0.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -28,6 +32,7 @@
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
+ "chromatic": "^11.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
@@ -37,6 +42,10 @@
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-dts": "^3.9.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5"
}
},
"node_modules/@adobe/css-tools": {
@@ -74,7 +83,6 @@
"version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
"integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
- "dev": true,
"dependencies": {
"@babel/highlight": "^7.24.2",
"picocolors": "^1.0.0"
@@ -283,7 +291,6 @@
"version": "7.24.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
"integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
- "dev": true,
"dependencies": {
"@babel/types": "^7.24.0"
},
@@ -405,7 +412,6 @@
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -414,7 +420,6 @@
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
- "dev": true,
"engines": {
"node": ">=6.9.0"
}
@@ -460,7 +465,6 @@
"version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
"integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
- "dev": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.22.20",
"chalk": "^2.4.2",
@@ -1927,7 +1931,6 @@
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
"integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
- "dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -1974,7 +1977,6 @@
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
- "dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.23.4",
"@babel/helper-validator-identifier": "^7.22.20",
@@ -2026,15 +2028,163 @@
"node": ">=10.0.0"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+ "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/serialize": "^1.1.2",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
+ "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/sheet": "^1.2.2",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
+ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz",
+ "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/cache": "^11.11.0",
+ "@emotion/serialize": "^1.1.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz",
+ "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==",
+ "dependencies": {
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/unitless": "^0.8.1",
+ "@emotion/utils": "^1.2.1",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
+ "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.11.5",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz",
+ "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/is-prop-valid": "^1.2.2",
+ "@emotion/serialize": "^1.1.4",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
- "dev": true,
"peerDependencies": {
"react": ">=16.8.0"
}
},
+ "node_modules/@emotion/utils": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
+ "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
+ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -5469,6 +5619,11 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true
},
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="
+ },
"node_modules/@types/pretty-hrtime": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
@@ -6270,7 +6425,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
@@ -6282,7 +6436,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
"dependencies": {
"color-name": "1.1.3"
}
@@ -6290,8 +6443,7 @@
"node_modules/ansi-styles/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
- "dev": true
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/anymatch": {
"version": "3.1.3",
@@ -6428,6 +6580,20 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.11",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz",
@@ -6733,7 +6899,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -6780,7 +6945,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -6802,6 +6966,22 @@
"node": "*"
}
},
+ "node_modules/choco-modal-component": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/choco-modal-component/-/choco-modal-component-0.0.2.tgz",
+ "integrity": "sha512-DFV82sc9hnMAKT+Z5JvdyLGysVXBFSxBHZlDZgfpwZjQfToOMRhKsayGQZmSVEGktdnsKMV31+bDG1dYlFcuPw==",
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "choriver-modal-component": "^0.0.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5"
+ }
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -6838,6 +7018,37 @@
"node": ">= 6"
}
},
+ "node_modules/choriver-components": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/choriver-components/-/choriver-components-0.0.1.tgz",
+ "integrity": "sha512-ztcDZr5lDQE7Uuaxrfizgu3i+kvyIteCnVjneH0L2WstzB90Cj4U141YjoPK3Lq5GDtlwZ6/ksF15XIdRAC7eA==",
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5"
+ }
+ },
+ "node_modules/choriver-modal-component": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/choriver-modal-component/-/choriver-modal-component-0.0.2.tgz",
+ "integrity": "sha512-tVMdN7UWg1NV1l4RiQtCCdmKD/B99M7N8SCDZXFNz0ORiiXIEpf/H1Chyf7tp5bNqt4hWwOJnRvql51Ya1YoNA==",
+ "dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "choriver-components": "^0.0.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5"
+ }
+ },
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@@ -7184,6 +7395,21 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true
},
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -7216,8 +7442,7 @@
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/de-indent": {
"version": "1.0.2",
@@ -7647,7 +7872,6 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
"integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
- "dev": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
@@ -7774,7 +7998,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
- "dev": true,
"engines": {
"node": ">=0.8.0"
}
@@ -8674,6 +8897,11 @@
"node": ">=8"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8848,7 +9076,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -9089,7 +9316,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -9149,7 +9375,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -9205,6 +9430,19 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
@@ -9293,7 +9531,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
- "dev": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -9309,7 +9546,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -9433,8 +9669,7 @@
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
- "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "dev": true
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
},
"node_modules/is-bigint": {
"version": "1.0.4",
@@ -9492,7 +9727,6 @@
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
- "dev": true,
"dependencies": {
"hasown": "^2.0.0"
},
@@ -10476,8 +10710,7 @@
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "dev": true
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -10587,8 +10820,7 @@
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
- "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "dev": true
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -11586,7 +11818,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
"dependencies": {
"callsites": "^3.0.0"
},
@@ -11598,7 +11829,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
- "dev": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@@ -11667,8 +11897,7 @@
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
},
"node_modules/path-scurry": {
"version": "1.10.2",
@@ -11705,7 +11934,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -11754,8 +11982,7 @@
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -12381,8 +12608,7 @@
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
- "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
- "dev": true
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
@@ -12497,7 +12723,6 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
- "dev": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@@ -13176,11 +13401,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
"dependencies": {
"has-flag": "^3.0.0"
},
@@ -13192,7 +13421,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -13452,7 +13680,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -14317,6 +14544,14 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
},
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/components/package.json b/components/package.json
index ec502ac0f..1932ddbfe 100644
--- a/components/package.json
+++ b/components/package.json
@@ -1,7 +1,7 @@
{
- "name": "compoents",
- "version": "0.0.1",
- "description": "npm modules",
+ "name": "choco-modal-component",
+ "version": "0.0.3",
+ "description": "초코가 만든 Modal component 라이브러리",
"type": "module",
"types": "dist/index.d.ts",
"main": "dist/index.js",
@@ -18,6 +18,10 @@
"build-storybook": "storybook build"
},
"dependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5",
+ "choco-modal-component": "^0.0.2",
+ "choriver-modal-component": "^0.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -38,6 +42,7 @@
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
+ "chromatic": "^11.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
@@ -47,5 +52,9 @@
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-dts": "^3.9.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.11.4",
+ "@emotion/styled": "^11.11.5"
}
}
diff --git a/components/public/image/closeButton.png b/components/public/image/closeButton.png
new file mode 100644
index 000000000..99e06ba66
Binary files /dev/null and b/components/public/image/closeButton.png differ
diff --git a/components/src/App.tsx b/components/src/App.tsx
index 829d564cf..94d28a730 100644
--- a/components/src/App.tsx
+++ b/components/src/App.tsx
@@ -1,10 +1,34 @@
+import { useModal, Modal } from "choco-modal-component";
import React from "react";
import "./App.css";
function App() {
+ const { isOpen, openModal, closeModal } = useModal();
+
return (
<>
+
+
+ 리액트에서의 컴포넌트도 캡슐화 되어 있고, 재사용 및 재구성할 수 있습니다. 리액트를 이용해
+ 애플리케이션을 개발한다는 것은 레고를 이용해 조립하는 것과 비슷합니다. 차이점은 부품이
+ 부족할 일이 없고, 반드시 한 번은 재사용할 컴포넌트를 만들어야 한다는 것입니다. 그리고 이
+ 컴포넌트는 상호작용하며 함께 동작합니다. 그리고 상호작용하는 여러 컴포넌트들을 '조합' 해서
+ 새로운 형태의 '합성' 컴포넌트를 만들 수 있습니다. 컴포넌트의 조합은 리액트의 가장 큰 장점
+ 중 하나인데, 컴포넌트를 만든 후에는 재사용을 통해 애플리케이션의 다른 부분에서도 사용할 수
+ 있습니다. 재사용 가능한 특징은 애플리케이션의 중요한 요소입니다. 복제된 여러 컴포넌트들이
+ 독립적으로 실행할 수 있을 때 우리는 사이드 이펙트를 줄이고, 개발의 효율성을 더욱 극대화할
+ 수 있습니다. 페이스북의 경우 서로 여러개의 채팅창을 보여주고 있지만 독립적으로 작동할 수
+ 있습니다. 이는 재사용 가능한 컴포넌트를 만들었기 때문에 가능한 것이죠.
+
+
Component Modules
+ Open Modal
>
);
}
diff --git a/components/src/lib/CloseButton/CloseButton.style.ts b/components/src/lib/CloseButton/CloseButton.style.ts
new file mode 100644
index 000000000..c383c591a
--- /dev/null
+++ b/components/src/lib/CloseButton/CloseButton.style.ts
@@ -0,0 +1,17 @@
+import styled from "@emotion/styled";
+
+export const ButtonWrapper = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: none;
+ background: none;
+ cursor: pointer;
+
+ img {
+ width: 24px;
+ height: 24px;
+ }
+`;
diff --git a/components/src/lib/CloseButton/CloseButton.tsx b/components/src/lib/CloseButton/CloseButton.tsx
new file mode 100644
index 000000000..d03295393
--- /dev/null
+++ b/components/src/lib/CloseButton/CloseButton.tsx
@@ -0,0 +1,13 @@
+import { MouseEventHandler } from "react";
+import { ButtonWrapper } from "./CloseButton.style";
+
+interface CloseButtonProps {
+ children: React.ReactElement | string;
+ onClick: MouseEventHandler;
+}
+
+const CloseButton = ({ children, onClick }: CloseButtonProps) => {
+ return {children} ;
+};
+
+export default CloseButton;
diff --git a/components/src/lib/Modal/Modal.style.ts b/components/src/lib/Modal/Modal.style.ts
new file mode 100644
index 000000000..3f62ee164
--- /dev/null
+++ b/components/src/lib/Modal/Modal.style.ts
@@ -0,0 +1,76 @@
+import styled from "@emotion/styled";
+import { ModalProps } from "./Modal";
+
+interface ModalDimProps {
+ isOpen: boolean;
+}
+
+export const ModalDim = styled.div`
+ display: ${({ isOpen }) => (isOpen ? "block" : "none")};
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.35);
+ z-index: 1000;
+`;
+
+export const ModalContainer = styled.div>`
+ display: flex;
+ z-index: 1001;
+ flex-direction: column;
+ ${({ closeButtonPosition }) =>
+ closeButtonPosition === "bottom" && "justify-content: space-between;"}
+ gap: 16px;
+ position: absolute;
+ min-height: 216px;
+ max-height: 70%;
+ padding: 24px 32px;
+ box-sizing: border-box;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 1);
+ color: rgba(0, 0, 0, 1);
+
+ ${({ modalPosition }) => {
+ switch (modalPosition) {
+ case "center":
+ return `
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 304px;
+ `;
+ case "bottom":
+ return `
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ `;
+ default:
+ return "";
+ }
+ }}
+`;
+
+export const ModalHeader = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+
+ h1 {
+ width: 100%;
+ font-family: Noto Sans KR;
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 26.06px;
+ text-align: left;
+ color: rgba(0, 0, 0, 1);
+
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+`;
diff --git a/components/src/lib/Modal/Modal.tsx b/components/src/lib/Modal/Modal.tsx
new file mode 100644
index 000000000..44e0bcc94
--- /dev/null
+++ b/components/src/lib/Modal/Modal.tsx
@@ -0,0 +1,48 @@
+import React from "react";
+import CloseButton from "../CloseButton/CloseButton";
+import Button from "../common/Button";
+import { ModalContainer, ModalDim, ModalHeader } from "./Modal.style";
+
+export interface ModalProps {
+ modalPosition: "center" | "bottom";
+ title: string;
+ children: React.ReactNode;
+ closeButtonPosition: "top" | "bottom";
+ isOpen: boolean;
+ onClose: (e: React.MouseEvent) => void;
+}
+
+export const Modal = ({
+ modalPosition,
+ title,
+ children,
+ closeButtonPosition,
+ isOpen,
+ onClose,
+}: ModalProps) => {
+ if (!isOpen) return null;
+
+ return (
+
+
+
+ {title}
+ {closeButtonPosition === "top" && (
+ onClose(e)}>
+
+
+ )}
+
+ {children}
+ {closeButtonPosition === "bottom" && (
+ onClose(e)}
+ />
+ )}
+
+
+ );
+};
diff --git a/components/src/lib/Modal/index.tsx b/components/src/lib/Modal/index.tsx
new file mode 100644
index 000000000..1cb2e09da
--- /dev/null
+++ b/components/src/lib/Modal/index.tsx
@@ -0,0 +1,20 @@
+import { useState } from "react";
+
+export const useModal = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const openModal = () => {
+ setIsOpen(true);
+ };
+
+ const closeModal = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsOpen(false);
+ };
+
+ return {
+ isOpen,
+ openModal,
+ closeModal,
+ };
+};
diff --git a/components/src/lib/common/Button.style.ts b/components/src/lib/common/Button.style.ts
new file mode 100644
index 000000000..b89263182
--- /dev/null
+++ b/components/src/lib/common/Button.style.ts
@@ -0,0 +1,22 @@
+import styled from "@emotion/styled";
+import { ButtonProps } from "./Button";
+
+export const ButtonWrapper = styled.button>`
+ width: 100%;
+ height: 44px;
+ border-radius: 5px;
+ border: none;
+ background: ${({ backgroundColor }) => backgroundColor};
+ cursor: pointer;
+
+ &:hover {
+ border: 2px solid ${({ fontColor }) => fontColor};
+ }
+
+ font-family: Noto Sans KR;
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 21.72px;
+ text-align: center;
+ color: ${({ fontColor }) => fontColor};
+`;
diff --git a/components/src/lib/common/Button.tsx b/components/src/lib/common/Button.tsx
new file mode 100644
index 000000000..f39940a04
--- /dev/null
+++ b/components/src/lib/common/Button.tsx
@@ -0,0 +1,18 @@
+import { ButtonWrapper } from "./Button.style";
+
+export interface ButtonProps {
+ content: string;
+ backgroundColor: string;
+ fontColor: string;
+ onClick: React.MouseEventHandler;
+}
+
+const Button = ({ content, backgroundColor, fontColor, onClick }: ButtonProps) => {
+ return (
+
+ {content}
+
+ );
+};
+
+export default Button;
diff --git a/components/src/lib/index.ts b/components/src/lib/index.ts
new file mode 100644
index 000000000..884c4981c
--- /dev/null
+++ b/components/src/lib/index.ts
@@ -0,0 +1,2 @@
+export { useModal } from "./Modal";
+export { Modal } from "./Modal/Modal";
diff --git a/components/src/main.tsx b/components/src/main.tsx
index 3d7150da8..9b67590a0 100644
--- a/components/src/main.tsx
+++ b/components/src/main.tsx
@@ -1,10 +1,10 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
-ReactDOM.createRoot(document.getElementById('root')!).render(
+ReactDOM.createRoot(document.getElementById("root")!).render(
- ,
-)
+
+);
diff --git a/components/src/stories/Modal.stories.tsx b/components/src/stories/Modal.stories.tsx
new file mode 100644
index 000000000..875d3610e
--- /dev/null
+++ b/components/src/stories/Modal.stories.tsx
@@ -0,0 +1,121 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import React from "react";
+import { useModal } from "../lib/Modal";
+import { Modal, ModalProps } from "../lib/Modal/Modal";
+
+const meta: Meta = {
+ title: "Components/Modal",
+ tags: ["autodocs"],
+ component: Modal,
+ parameters: {
+ docs: {
+ source: {
+ state: "open",
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ argTypes: {
+ modalPosition: {
+ description: "모달의 위치",
+ control: {
+ type: "radio",
+ options: ["center", "bottom"],
+ },
+ },
+ title: {
+ description: "모달의 제목",
+ },
+ closeButtonPosition: {
+ description: "닫기 버튼의 위치",
+ control: {
+ type: "radio",
+ options: ["top", "bottom"],
+ },
+ },
+ children: {
+ description: "모달의 내용",
+ control: {
+ type: "text",
+ },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const Template: Story = {
+ render: (args: ModalProps) => {
+ const { closeModal } = useModal();
+
+ return (
+
+ {args.children}
+
+ );
+ },
+};
+
+export const 기본_모달: Story = {
+ ...Template,
+ args: {
+ modalPosition: "center",
+ title: "모달 제목",
+ closeButtonPosition: "top",
+ children: "모달 내용",
+ isOpen: false,
+ },
+};
+
+export const 중앙에_위치한_모달에_상단_X닫기_버튼: Story = {
+ ...Template,
+ args: {
+ modalPosition: "center",
+ title: "중앙에 위치한 모달",
+ closeButtonPosition: "top",
+ children: "이것은 중앙에 위치한 모달의 내용입니다.",
+ isOpen: true,
+ },
+};
+
+export const 중앙에_위치한_모달에_하단_사각형닫기_버튼: Story = {
+ ...Template,
+ args: {
+ modalPosition: "center",
+ title: "중앙에 위치한 모달",
+ closeButtonPosition: "bottom",
+ children:
+ "이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다. 이것은 중앙에 위치한 모달의 내용입니다.",
+ isOpen: true,
+ },
+};
+
+export const 하단에_위치한_모달메_상단_X닫기_버튼: Story = {
+ ...Template,
+ args: {
+ modalPosition: "bottom",
+ title: "하단에 위치한 모달",
+ closeButtonPosition: "top",
+ children:
+ "이것은 하단에 위치한 모달의 내용입니다. 이것은 하단에 위치한 모달의 내용입니다. 이것은 하단에 위치한 모달의 내용입니다. 이것은 하단에 위치한 모달의 내용입니다.",
+ isOpen: true,
+ },
+};
+
+export const 하단에_위치한_모달메_하단_사각형닫기_버튼: Story = {
+ ...Template,
+ args: {
+ modalPosition: "bottom",
+ title: "하단에 위치한 모달",
+ closeButtonPosition: "bottom",
+ children: "이것은 하단에 위치한 모달의 내용입니다.",
+ isOpen: true,
+ },
+};
diff --git a/hooks/.prettierrc b/hooks/.prettierrc
new file mode 100644
index 000000000..efd721c1c
--- /dev/null
+++ b/hooks/.prettierrc
@@ -0,0 +1,10 @@
+{
+ "printWidth": 100,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "semi": true,
+ "singleQuote": false,
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "endOfLine": "auto"
+}
diff --git a/hooks/README.md b/hooks/README.md
index 6c9e0ac45..4fc25a298 100644
--- a/hooks/README.md
+++ b/hooks/README.md
@@ -1 +1,98 @@
-# Hooks Module
+# choco-payments-validation-hooks
+
+## Install
+
+이 라이브러리를 사용하기 위해서는 다음 패키지를 설치해야 합니다:
+
+```bash
+npm install choco-payments-validation-hooks
+```
+
+## Usage
+
+React 컴포넌트에서 모달 라이브러리를 사용하려면 다음 단계를 따르세요
+
+```jsx
+import { useCVC } from "choco-payments-validation-hooks";
+
+function App() {
+ const [cardCVC, handleCardCVCChange, cardCVCValidation] = useCVC();
+
+ return (
+ <>
+ Card CVC:
+ handleCardCVCChange(e.target.value)}
+ />
+ {!cardCVCValidation.isValid && (
+ {cardCVCValidation.errorMessage}
+ )}
+
+ );
+}
+
+export default App;
+```
+
+## API
+
+각 훅은 다음과 같은 값을 반환합니다:
+
+- 첫 번째 값: 해당 필드의 현재 값을 나타내는 문자열 또는 객체입니다.
+- 두 번째 값: 해당 필드의 값을 업데이트하는 함수입니다.
+- 세 번째 값: 해당 필드의 유효성 검사 결과를 나타내는 객체입니다.
+ - `isValid`: 필드의 값이 유효한지 여부를 나타내는 boolean 값입니다.
+ - `errorMessage`: 유효성 검사에 실패한 경우 표시할 에러 메시지입니다.
+
+### useCVC
+
+| 반환값 | 설명 |
+| --------------------- | ----------------------------------------------------- |
+| `cardCVC` | 카드 CVC 값을 나타내는 문자열입니다. |
+| `handleCardCVCChange` | 카드 CVC 값을 업데이트하는 함수입니다. |
+| `cardCVCValidation` | 카드 CVC 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+### useCardHolder
+
+| 반환값 | 설명 |
+| ------------------------ | ------------------------------------------------------------- |
+| `cardHolder` | 카드 소유자 이름 값을 나타내는 문자열입니다. |
+| `handleCardHolderChange` | 카드 소유자 이름 값을 업데이트하는 함수입니다. |
+| `cardHolderValidation` | 카드 소유자 이름 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+### useCardIssuer
+
+| 반환값 | 설명 |
+| ------------------------ | -------------------------------------------------------- |
+| `cardIssuer` | 카드 발급사 값을 나타내는 문자열입니다. |
+| `handleCardIssuerChange` | 카드 발급사 값을 업데이트하는 함수입니다. |
+| `cardIssuerValidation` | 카드 발급사 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+### useCardNumber
+
+| 반환값 | 설명 |
+| ------------------------- | ------------------------------------------------------ |
+| `cardNumbers` | 카드 번호 값을 나타내는 객체입니다. |
+| `handleCardNumbersChange` | 카드 번호 값을 업데이트하는 함수입니다. |
+| `cardNumbersValidation` | 카드 번호 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+### useExpiryDate
+
+| 반환값 | 설명 |
+| ------------------------ | ------------------------------------------------------ |
+| `expiryDate` | 만료 일자 값을 나타내는 객체입니다. |
+| `handleExpiryDateChange` | 만료 일자 값을 업데이트하는 함수입니다. |
+| `expiryDateValidation` | 만료 일자 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+### usePassword
+
+| 반환값 | 설명 |
+| ---------------------- | ----------------------------------------------------- |
+| `password` | 비밀번호 값을 나타내는 문자열입니다. |
+| `handlePasswordChange` | 비밀번호 값을 업데이트하는 함수입니다. |
+| `passwordValidation` | 비밀번호 값의 유효성 검사 결과를 나타내는 객체입니다. |
+
+이 훅들을 사용하여 React 컴포넌트에서 payments validation을 쉽게 구현할 수 있습니다. 각 훅은 해당 필드의 값과 유효성 검사 결과를 반환하므로, 이를 활용하여 사용자 입력을 처리하고 에러 메시지를 표시할 수 있습니다.
diff --git a/hooks/__tests__/useCVC.test.ts b/hooks/__tests__/useCVC.test.ts
new file mode 100644
index 000000000..c0b67711c
--- /dev/null
+++ b/hooks/__tests__/useCVC.test.ts
@@ -0,0 +1,40 @@
+import { renderHook, act } from "@testing-library/react";
+import { useCVC } from "../src/lib/hooks/useCVC";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("useCVC 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCVC());
+ act(() => {
+ result.current[1]("");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값이 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCVC());
+ act(() => {
+ result.current[1]("jo22222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_CVC.INVALID_CHARACTERS);
+ });
+
+ it("입력값이 3자리 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCVC());
+ act(() => {
+ result.current[1]("222222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_CVC.MAX_LENGTH_EXCEEDED);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useCVC());
+ act(() => {
+ result.current[1]("123");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
diff --git a/hooks/__tests__/useCardHolder.test.ts b/hooks/__tests__/useCardHolder.test.ts
new file mode 100644
index 000000000..b77e270c7
--- /dev/null
+++ b/hooks/__tests__/useCardHolder.test.ts
@@ -0,0 +1,49 @@
+import { renderHook, act } from "@testing-library/react";
+import { useCardHolder } from "../src/lib/hooks/useCardHolder";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("useCardHolder 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardHolder());
+ act(() => {
+ result.current[1]("");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값에 유효하지 않은 문자(영어 소문자)가 포함되어 있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardHolder());
+ act(() => {
+ result.current[1]("jo h n");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_HOLDER.INVALID_CHARACTERS);
+ });
+
+ it("입력값에 유효하지 않은 문자(숫자)가 포함되어 있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardHolder());
+ act(() => {
+ result.current[1]("222222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_HOLDER.INVALID_CHARACTERS);
+ });
+
+ it("입력값의 길이가 15자를 초과한다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardHolder());
+ act(() => {
+ result.current[1]("JOHNDOELONGLONGLONG");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_HOLDER.MAX_LENGTH_EXCEEDED);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useCardHolder());
+ act(() => {
+ result.current[1]("JOHN DOE");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
diff --git a/hooks/__tests__/useCardIssuer.test.ts b/hooks/__tests__/useCardIssuer.test.ts
new file mode 100644
index 000000000..95f82fdf0
--- /dev/null
+++ b/hooks/__tests__/useCardIssuer.test.ts
@@ -0,0 +1,14 @@
+import { renderHook, act } from "@testing-library/react";
+import { useCardIssuer } from "../src/lib/hooks/useCardIssuer";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("useCardIssuer 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardIssuer());
+ act(() => {
+ result.current[1]("");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+});
diff --git a/hooks/__tests__/useCardNumber.test.ts b/hooks/__tests__/useCardNumber.test.ts
new file mode 100644
index 000000000..a6af8391f
--- /dev/null
+++ b/hooks/__tests__/useCardNumber.test.ts
@@ -0,0 +1,40 @@
+import { renderHook, act } from "@testing-library/react";
+import { useCardNumber } from "../src/lib/hooks/useCardNumber";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("useCardNumber 첫번째 input에 대한 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardNumber());
+ act(() => {
+ result.current[1]("first", "");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값이 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardNumber());
+ act(() => {
+ result.current[1]("first", "johnnn2");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD__NUMBER.INVALID_NUMBERS);
+ });
+
+ it("입력값이 4자리가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useCardNumber());
+ act(() => {
+ result.current[1]("first", "222222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD__NUMBER.MAX_LENGTH_EXCEEDED);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useCardNumber());
+ act(() => {
+ result.current[1]("first", "1234");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
diff --git a/hooks/__tests__/useExpiryDate.test.ts b/hooks/__tests__/useExpiryDate.test.ts
new file mode 100644
index 000000000..acfd0ed85
--- /dev/null
+++ b/hooks/__tests__/useExpiryDate.test.ts
@@ -0,0 +1,105 @@
+import { renderHook, act } from "@testing-library/react";
+import { useExpiryDate } from "../src/lib/hooks/useExpiryDate";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("useExpiryDate 첫 번째 input(month)에 대한 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("month", "");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값이 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("month", "johnnn2");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(
+ ERROR_MESSAGE.CARD_EXPIRY_DATE.INVALID_MONTH_FORMAT
+ );
+ });
+
+ it("입력값이 1~12가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("month", "222222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_EXPIRY_DATE.MONTH_OUT_OF_RANGE);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("month", "12");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
+
+describe("useExpiryDate 두 번째 input(year)에 대한 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("year", "");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값이 2자리 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("year", "123");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_EXPIRY_DATE.INVALID_YEAR_FORMAT);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+ act(() => {
+ result.current[1]("year", "30");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
+
+describe("useExpiryDate 만료 날짜 유효성 검사", () => {
+ it("입력된 만료 날짜가 현재 날짜보다 과거라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+
+ // 현재 날짜보다 과거 날짜 설정
+ const currentDate = new Date();
+ const pastYear = currentDate.getFullYear() - 1;
+ const pastMonth = currentDate.getMonth() + 1;
+
+ act(() => {
+ result.current[1]("year", pastYear.toString().slice(-2));
+ result.current[1]("month", pastMonth.toString());
+ });
+
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_EXPIRY_DATE.EXPIRED_CARD);
+ });
+
+ it("입력된 만료 날짜가 현재 날짜와 같으면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => useExpiryDate());
+
+ // 현재 날짜 설정
+ const currentDate = new Date();
+ const currentYear = currentDate.getFullYear().toString().slice(-2);
+ const currentMonth = (currentDate.getMonth() + 1).toString();
+
+ act(() => {
+ result.current[1]("year", currentYear);
+ result.current[1]("month", currentMonth);
+ });
+
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
diff --git a/hooks/__tests__/usePassword.test.ts b/hooks/__tests__/usePassword.test.ts
new file mode 100644
index 000000000..66236a80f
--- /dev/null
+++ b/hooks/__tests__/usePassword.test.ts
@@ -0,0 +1,40 @@
+import { renderHook, act } from "@testing-library/react";
+import { usePassword } from "../src/lib/hooks/usePassword";
+import { ERROR_MESSAGE } from "../src/lib/constants/errorMessage";
+
+describe("usePassword 테스트", () => {
+ it("touched 상태인데 입력값이 비어있다면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => usePassword());
+ act(() => {
+ result.current[1]("");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.NO_INPUT);
+ });
+
+ it("입력값이 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => usePassword());
+ act(() => {
+ result.current[1]("jo22222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_PASSWORD.INVALID_CHARACTERS);
+ });
+
+ it("입력값이 2자리 숫자가 아니라면 에러 메시지를 반환해야 한다", () => {
+ const { result } = renderHook(() => usePassword());
+ act(() => {
+ result.current[1]("222");
+ });
+ expect(result.current[2].isValid).toBe(false);
+ expect(result.current[2].errorMessage).toBe(ERROR_MESSAGE.CARD_PASSWORD.MAX_LENGTH_EXCEEDED);
+ });
+
+ it("입력값이 유효하다면 isValid가 true여야 한다", () => {
+ const { result } = renderHook(() => usePassword());
+ act(() => {
+ result.current[1]("12");
+ });
+ expect(result.current[2].isValid).toBe(true);
+ });
+});
diff --git a/hooks/package-lock.json b/hooks/package-lock.json
index 29f20e91c..1c1e8315d 100644
--- a/hooks/package-lock.json
+++ b/hooks/package-lock.json
@@ -1,13 +1,15 @@
{
- "name": "juncomponents",
- "version": "0.0.1",
+ "name": "choco-payments-validation-hooks",
+ "version": "0.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "juncomponents",
- "version": "0.0.1",
+ "name": "choco-payments-validation-hooks",
+ "version": "0.0.3",
"dependencies": {
+ "choco-payments-validation-hooks": "^0.0.1",
+ "choriver-payments-validation-hooks": "^0.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -3513,6 +3515,25 @@
"node": ">=10"
}
},
+ "node_modules/choco-payments-validation-hooks": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/choco-payments-validation-hooks/-/choco-payments-validation-hooks-0.0.1.tgz",
+ "integrity": "sha512-j8sz6PNjkBfRibXi49bChwsomOrg+WQSAWtGhRaeWslOkT+NikP/VKK2Qw6W8Zze7vMPIpHzFWvTOnzskK4K2Q==",
+ "dependencies": {
+ "choriver-payments-validation-hooks": "^0.0.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+ },
+ "node_modules/choriver-payments-validation-hooks": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/choriver-payments-validation-hooks/-/choriver-payments-validation-hooks-0.0.1.tgz",
+ "integrity": "sha512-4DjjoAcOHr8SIfEY62MLX7OIk1bdeX7mqACdlUdomtQAfs1tNo6FVny6WB0CgehnI6Wjbd6/jeZYUitrGijXpw==",
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+ },
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
diff --git a/hooks/package.json b/hooks/package.json
index 8d588e084..714935dd4 100644
--- a/hooks/package.json
+++ b/hooks/package.json
@@ -1,7 +1,7 @@
{
- "name": "hooks",
- "version": "0.0.1",
- "description": "npm modules",
+ "name": "choco-payments-validation-hooks",
+ "version": "0.0.3",
+ "description": "초코가 만든 payments validation hooks 라이브러리",
"type": "module",
"types": "dist/index.d.ts",
"main": "dist/index.js",
@@ -17,6 +17,8 @@
"test": "jest"
},
"dependencies": {
+ "choco-payments-validation-hooks": "^0.0.1",
+ "choriver-payments-validation-hooks": "^0.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
diff --git a/hooks/src/App.tsx b/hooks/src/App.tsx
index 909a1e8ff..58eb5ad3f 100644
--- a/hooks/src/App.tsx
+++ b/hooks/src/App.tsx
@@ -1,10 +1,127 @@
+import {
+ useCVC,
+ useCardHolder,
+ useCardIssuer,
+ useCardNumber,
+ useExpiryDate,
+ usePassword,
+} from "choco-payments-validation-hooks";
import React from "react";
import "./App.css";
+const cardNames = ["현대카드", "국민카드", "신한카드", "우리카드"];
+
function App() {
+ const [cardHolder, handleCardHolderChange, cardHolderValidation] = useCardHolder();
+ const [cardPassword, handleCardPasswordChange, cardPasswordValidation] = usePassword();
+ const [cardCVC, handleCardCVCChange, cardCVCValidation] = useCVC();
+ const [cardExpiryDate, handleCardExpiryDateChange, cardExpiryDateValidation] = useExpiryDate();
+ const [cardNumbers, handleCardNumbersChange, cardNumbersValidation] = useCardNumber();
+ const [cardIssuer, handleCardIssuerChange, cardIssuerValidation] = useCardIssuer();
+
+ const cardNumberKeys = Object.keys(cardNumbers) as Array;
+
return (
<>
Hooks Modules
+
+ Card Holder:
+ handleCardHolderChange(e.target.value)}
+ />
+ {!cardHolderValidation.isValid && (
+ {cardHolderValidation.errorMessage}
+ )}
+
+
+
+ Card Password:
+ handleCardPasswordChange(e.target.value)}
+ />
+ {!cardPasswordValidation.isValid && (
+ {cardPasswordValidation.errorMessage}
+ )}
+
+
+
+ Card CVC:
+ handleCardCVCChange(e.target.value)}
+ />
+ {!cardCVCValidation.isValid && (
+ {cardCVCValidation.errorMessage}
+ )}
+
+
+
+ Card Expiry Date:
+ handleCardExpiryDateChange("month", e.target.value)}
+ />
+ handleCardExpiryDateChange("year", e.target.value)}
+ />
+ {!cardExpiryDateValidation.isValid && (
+ {cardExpiryDateValidation.errorMessage}
+ )}
+
+
+
+ Card Number:
+ {cardNumberKeys.map((key, index) => (
+ handleCardNumbersChange(key, e.target.value)}
+ />
+ ))}
+ {!cardNumbersValidation.isValid && (
+ {cardNumbersValidation.errorMessage}
+ )}
+
+
+
+
Card Issuer:
+
+ {cardNames.map((name) => (
+
handleCardIssuerChange(cardIssuer === name ? "" : name)}
+ style={{
+ display: "inline-block",
+ padding: "8px",
+ border: `2px solid ${cardIssuer === name ? "blue" : "gray"}`,
+ borderRadius: "4px",
+ margin: "5px",
+ cursor: "pointer",
+ }}
+ >
+ {name}
+
+ ))}
+
+ {!cardIssuerValidation.isValid && (
+
{cardIssuerValidation.errorMessage}
+ )}
+
>
);
}
diff --git a/hooks/src/lib/constants/errorMessage.ts b/hooks/src/lib/constants/errorMessage.ts
new file mode 100644
index 000000000..2fdaa65fd
--- /dev/null
+++ b/hooks/src/lib/constants/errorMessage.ts
@@ -0,0 +1,25 @@
+export const ERROR_MESSAGE = {
+ NO_INPUT: "입력필드는 비어있을 수 없습니다.",
+ CARD_HOLDER: {
+ INVALID_CHARACTERS: "카드 소유자 이름은 영어 대문자와 공백만 입력 가능합니다.",
+ MAX_LENGTH_EXCEEDED: "카드 소유자 이름은 공백 포함 15자까지만 입력 가능합니다.",
+ },
+ CARD__NUMBER: {
+ INVALID_NUMBERS: "카드 번호는 숫자만 입력 가능합니다.",
+ MAX_LENGTH_EXCEEDED: "카드 번호는 4자리까지만 입력 가능합니다.",
+ },
+ CARD_CVC: {
+ INVALID_CHARACTERS: "카드 CVC는 숫자만 입력 가능합니다.",
+ MAX_LENGTH_EXCEEDED: "카드 CVC는 3자리까지만 입력 가능합니다.",
+ },
+ CARD_EXPIRY_DATE: {
+ INVALID_MONTH_FORMAT: "카드 유효기간의 월은 숫자만 입력 가능합니다.",
+ MONTH_OUT_OF_RANGE: "카드 유효기간의 월은 1부터 12까지만 입력 가능합니다.",
+ INVALID_YEAR_FORMAT: "카드 유효기간의 년은 숫자만 입력 가능합니다.",
+ EXPIRED_CARD: "카드 유효기간이 만료되었습니다.",
+ },
+ CARD_PASSWORD: {
+ INVALID_CHARACTERS: "카드 비밀번호는 숫자만 입력 가능합니다.",
+ MAX_LENGTH_EXCEEDED: "카드 비밀번호는 2자리까지만 입력 가능합니다.",
+ },
+};
diff --git a/hooks/src/lib/hooks/useCVC.ts b/hooks/src/lib/hooks/useCVC.ts
new file mode 100644
index 000000000..36bb67ec5
--- /dev/null
+++ b/hooks/src/lib/hooks/useCVC.ts
@@ -0,0 +1,34 @@
+import { useState } from "react";
+import { ValidationResult } from "../../type";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+
+export function useCVC(): [string, (value: string) => void, ValidationResult] {
+ const [password, setPassword] = useState("");
+ const [isTouched, setIsTouched] = useState(false);
+
+ function validatePassword(value: string): ValidationResult {
+ // 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (isTouched && value === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 입력된 문자열이 숫자가 아니라면 에러 발생
+ if (!/^\d+$/.test(value) && isTouched) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_CVC.INVALID_CHARACTERS };
+ }
+
+ // 입력된 문자열이 1부터 999 사이의 3자리가 아니라면 에러 발생
+ if (!/^\d{3}$/.test(value) && isTouched) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_CVC.MAX_LENGTH_EXCEEDED };
+ }
+
+ return { isValid: true };
+ }
+
+ function handlePasswordChange(value: string) {
+ if (!isTouched) setIsTouched(true);
+ setPassword(value);
+ }
+
+ return [password, handlePasswordChange, validatePassword(password)];
+}
diff --git a/hooks/src/lib/hooks/useCardHolder.ts b/hooks/src/lib/hooks/useCardHolder.ts
new file mode 100644
index 000000000..ef722c5f6
--- /dev/null
+++ b/hooks/src/lib/hooks/useCardHolder.ts
@@ -0,0 +1,40 @@
+import { useState } from "react";
+import { ValidationResult } from "../../type";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+
+export function useCardHolder(): [string, (value: string) => void, ValidationResult] {
+ const [cardHolder, setCardHolder] = useState("");
+ const [isTouched, setIsTouched] = useState(false);
+
+ function validateCardHolder(value: string): ValidationResult {
+ // 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (isTouched && value === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 입력된 문자열이 영어 대문자와 공백이 아니라면 에러 발생
+ if (!/^[A-Z\s]*$/.test(value) && isTouched) {
+ return {
+ isValid: false,
+ errorMessage: ERROR_MESSAGE.CARD_HOLDER.INVALID_CHARACTERS,
+ };
+ }
+
+ // 입력된 문자열의 길이가 공백 포함 15글자를 넘어가면 에러 발생
+ if (value.length > 15 && isTouched) {
+ return {
+ isValid: false,
+ errorMessage: ERROR_MESSAGE.CARD_HOLDER.MAX_LENGTH_EXCEEDED,
+ };
+ }
+
+ return { isValid: true };
+ }
+
+ function handleCardHolderChange(value: string) {
+ if (!isTouched) setIsTouched(true);
+ setCardHolder(value);
+ }
+
+ return [cardHolder, handleCardHolderChange, validateCardHolder(cardHolder)];
+}
diff --git a/hooks/src/lib/hooks/useCardIssuer.ts b/hooks/src/lib/hooks/useCardIssuer.ts
new file mode 100644
index 000000000..a4bbb0d21
--- /dev/null
+++ b/hooks/src/lib/hooks/useCardIssuer.ts
@@ -0,0 +1,24 @@
+import { useState } from "react";
+import { ValidationResult } from "../../type";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+
+export function useCardIssuer(): [string, (value: string) => void, ValidationResult] {
+ const [cardIssuer, setCardIssuer] = useState("");
+ const [isTouched, setIsTouched] = useState(false);
+
+ function validateCardIssuer(value: string): ValidationResult {
+ // 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (isTouched && value === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ return { isValid: true };
+ }
+
+ function handleCardIssuerChange(value: string) {
+ if (!isTouched) setIsTouched(true);
+ setCardIssuer(value);
+ }
+
+ return [cardIssuer, handleCardIssuerChange, validateCardIssuer(cardIssuer)];
+}
diff --git a/hooks/src/lib/hooks/useCardNumber.ts b/hooks/src/lib/hooks/useCardNumber.ts
new file mode 100644
index 000000000..b75331b58
--- /dev/null
+++ b/hooks/src/lib/hooks/useCardNumber.ts
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import { ValidationResult } from "../../type";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+
+type CardNumbers = {
+ first: string;
+ second: string;
+ third: string;
+ fourth: string;
+};
+
+type TouchedState = {
+ [key in keyof CardNumbers]: boolean;
+};
+
+export function useCardNumber(): [
+ CardNumbers,
+ (option: keyof CardNumbers, value: string) => void,
+ ValidationResult
+] {
+ const [cardNumbers, setCardNumbers] = useState({
+ first: "",
+ second: "",
+ third: "",
+ fourth: "",
+ });
+ const [isTouched, setIsTouched] = useState({
+ first: false,
+ second: false,
+ third: false,
+ fourth: false,
+ });
+
+ function validateCardNumbers(cardNumbers: CardNumbers): ValidationResult {
+ for (const key in cardNumbers) {
+ if (isTouched[key as keyof TouchedState]) {
+ const inputValue = cardNumbers[key as keyof CardNumbers];
+ // 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (inputValue === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 입력된 문자열이 숫자가 아니라면 에러 발생
+ if (!/^\d+$/.test(inputValue)) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD__NUMBER.INVALID_NUMBERS };
+ }
+
+ // 입력된 문자열이 1부터 9999 사이의 4자리가 아니라면 에러 발생
+ if (!/^\d{4}$/.test(inputValue)) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD__NUMBER.MAX_LENGTH_EXCEEDED };
+ }
+ }
+ }
+
+ return { isValid: true };
+ }
+
+ function handleCardNumbersChange(option: keyof CardNumbers, value: string) {
+ if (!isTouched[option])
+ setIsTouched((prev) => ({
+ ...prev,
+ [option]: true,
+ }));
+ setCardNumbers((prev) => ({ ...prev, [option]: value }));
+ }
+
+ return [cardNumbers, handleCardNumbersChange, validateCardNumbers(cardNumbers)];
+}
diff --git a/hooks/src/lib/hooks/useExpiryDate.ts b/hooks/src/lib/hooks/useExpiryDate.ts
new file mode 100644
index 000000000..d580e5f5e
--- /dev/null
+++ b/hooks/src/lib/hooks/useExpiryDate.ts
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+import { ValidationResult } from "./../../type.d";
+
+type ExpiryDate = {
+ month: string;
+ year: string;
+};
+
+export function useExpiryDate(): [
+ ExpiryDate,
+ (option: "year" | "month", value: string) => void,
+ ValidationResult
+] {
+ const [expiryDate, setExpiryDate] = useState({ month: "", year: "" });
+ const [isTouched, setIsTouched] = useState({ month: false, year: false });
+
+ function validateExpiryDate(expiryDate: ExpiryDate): ValidationResult {
+ const currentYear = new Date().getFullYear();
+ const currentMonth = new Date().getMonth() + 1;
+
+ const montNumber = Number(expiryDate.month);
+ const yearNumber = 2000 + Number(expiryDate.year);
+
+ if (isTouched.month) {
+ // 월 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (expiryDate.month === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 월에 입력된 문자열이 숫자가 아니라면 에러 발생
+ if (!/^\d+$/.test(expiryDate.month)) {
+ return {
+ isValid: false,
+ errorMessage: ERROR_MESSAGE.CARD_EXPIRY_DATE.INVALID_MONTH_FORMAT,
+ };
+ }
+
+ // 월에 입력된 문자열이 1부터 12 사이의 값이 아니라면 에러 발생
+ if (montNumber < 1 || montNumber > 12) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_EXPIRY_DATE.MONTH_OUT_OF_RANGE };
+ }
+ }
+
+ if (isTouched.year) {
+ // 년도 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (expiryDate.year === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 년도에 입력된 문자열이 1부터 99 사이의 2자리가 아니라면 에러 발생
+ if (!/^\d{2}$/.test(expiryDate.year)) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_EXPIRY_DATE.INVALID_YEAR_FORMAT };
+ }
+ }
+
+ if (isTouched.month && isTouched.year) {
+ // 월과 년도에 입력된 문자열이 현재날짜보다 과거라면 에러 발생
+ if (yearNumber < currentYear || (yearNumber === currentYear && montNumber < currentMonth)) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_EXPIRY_DATE.EXPIRED_CARD };
+ }
+ }
+
+ return { isValid: true };
+ }
+
+ function handleExpiryDateChange(option: "year" | "month", value: string) {
+ if (!isTouched[option]) setIsTouched((prev) => ({ ...prev, [option]: true }));
+ setExpiryDate((prev) => ({ ...prev, [option]: value }));
+ }
+
+ return [expiryDate, handleExpiryDateChange, validateExpiryDate(expiryDate)];
+}
diff --git a/hooks/src/lib/hooks/usePassword.ts b/hooks/src/lib/hooks/usePassword.ts
new file mode 100644
index 000000000..4d7930ac8
--- /dev/null
+++ b/hooks/src/lib/hooks/usePassword.ts
@@ -0,0 +1,34 @@
+import { useState } from "react";
+import { ValidationResult } from "../../type";
+import { ERROR_MESSAGE } from "../constants/errorMessage";
+
+export function usePassword(): [string, (value: string) => void, ValidationResult] {
+ const [password, setPassword] = useState("");
+ const [isTouched, setIsTouched] = useState(false);
+
+ function validatePassword(value: string): ValidationResult {
+ // 인풋을 클릭했지만 아무런 입력이 없다면 에러 발생
+ if (isTouched && value === "") {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.NO_INPUT };
+ }
+
+ // 입력된 문자열이 숫자가 아니라면 에러 발생
+ if (!/^\d+$/.test(value) && isTouched) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_PASSWORD.INVALID_CHARACTERS };
+ }
+
+ // 입력된 문자열이 1부터 99 사이의 2자리가 아니라면 에러 발생
+ if (!/^\d{2}$/.test(value) && isTouched) {
+ return { isValid: false, errorMessage: ERROR_MESSAGE.CARD_PASSWORD.MAX_LENGTH_EXCEEDED };
+ }
+
+ return { isValid: true };
+ }
+
+ function handlePasswordChange(value: string) {
+ if (!isTouched) setIsTouched(true);
+ setPassword(value);
+ }
+
+ return [password, handlePasswordChange, validatePassword(password)];
+}
diff --git a/hooks/src/lib/index.ts b/hooks/src/lib/index.ts
new file mode 100644
index 000000000..252c04e90
--- /dev/null
+++ b/hooks/src/lib/index.ts
@@ -0,0 +1,6 @@
+export { useCVC } from "./hooks/useCVC";
+export { useCardHolder } from "./hooks/useCardHolder";
+export { useCardIssuer } from "./hooks/useCardIssuer";
+export { useCardNumber } from "./hooks/useCardNumber";
+export { useExpiryDate } from "./hooks/useExpiryDate";
+export { usePassword } from "./hooks/usePassword";
diff --git a/hooks/src/type.d.ts b/hooks/src/type.d.ts
new file mode 100644
index 000000000..14c0f6042
--- /dev/null
+++ b/hooks/src/type.d.ts
@@ -0,0 +1,4 @@
+export type ValidationResult = {
+ isValid: boolean;
+ errorMessage?: string;
+};