From a83633351258601ed49a4e127e8b2c16c227fee3 Mon Sep 17 00:00:00 2001 From: "JoohyunKim(Lucy)" Date: Fri, 10 Apr 2026 12:23:07 +0900 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20lint=20=EC=88=98=EC=A0=95,=20packa?= =?UTF-8?q?ge=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web-app/eslint.config.js | 21 +++++++- web-app/package.json | 4 ++ web-app/pnpm-lock.yaml | 105 +++++++++++++++++++++++++++++++++++++++ web-app/tsconfig.json | 2 +- 4 files changed, 130 insertions(+), 2 deletions(-) diff --git a/web-app/eslint.config.js b/web-app/eslint.config.js index dd9ac71..5dedf91 100644 --- a/web-app/eslint.config.js +++ b/web-app/eslint.config.js @@ -1,6 +1,7 @@ import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; import reactHooks from 'eslint-plugin-react-hooks'; +import unusedImports from 'eslint-plugin-unused-imports'; +import tseslint from 'typescript-eslint'; import stylistic from '@stylistic/eslint-plugin'; @@ -19,10 +20,28 @@ export default tseslint.config( }), reactHooks.configs.flat.recommended, { + plugins: { + 'unused-imports': unusedImports, + }, rules: { '@stylistic/jsx-one-expression-per-line': 'off', '@stylistic/multiline-ternary': 'off', 'react-hooks/set-state-in-effect': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@stylistic/no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], + '@stylistic/comma-dangle': 'off', + '@stylistic/semi': 'off', + '@stylistic/no-trailing-spaces': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'unused-imports/no-unused-imports': 'error', + 'unused-imports/no-unused-vars': ['warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }], + '@stylistic/arrow-parens': 'off', + '@stylistic/member-delimiter-style': 'off', + '@typescript-eslint/consistent-type-imports': 'off', + '@stylistic/max-statements-per-line': 'off', + '@stylistic/operator-linebreak': 'off', }, }, ); diff --git a/web-app/package.json b/web-app/package.json index 5e23bba..aecfb2f 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -13,6 +13,8 @@ "dependencies": { "@react-router/node": "7.14.0", "@react-router/serve": "7.14.0", + "axios": "^1.15.0", + "clsx": "^2.1.1", "dayjs": "^1.11.20", "isbot": "^5.1.37", "ol": "^10.8.0", @@ -22,6 +24,7 @@ "react-dom": "^19.2.4", "react-hook-form": "^7.72.1", "react-router": "7.14.0", + "react-stately": "^3.45.0", "tailwind-merge": "^3.5.0", "tailwind-variants": "^3.2.2", "zod": "^4.3.6" @@ -36,6 +39,7 @@ "@types/react-dom": "^19.2.3", "eslint": "^10.2.0", "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-unused-imports": "^4.4.1", "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "typescript-eslint": "^8.58.0", diff --git a/web-app/pnpm-lock.yaml b/web-app/pnpm-lock.yaml index 3bffe80..c71ab87 100644 --- a/web-app/pnpm-lock.yaml +++ b/web-app/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@react-router/serve': specifier: 7.14.0 version: 7.14.0(react-router@7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@5.9.3) + axios: + specifier: ^1.15.0 + version: 1.15.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 dayjs: specifier: ^1.11.20 version: 1.11.20 @@ -41,6 +47,9 @@ importers: react-router: specifier: 7.14.0 version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-stately: + specifier: ^3.45.0 + version: 3.45.0(react@19.2.4) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -78,6 +87,9 @@ importers: eslint-plugin-react-hooks: specifier: ^7.0.1 version: 7.0.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-unused-imports: + specifier: ^4.4.1 + version: 4.4.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1)) tailwindcss: specifier: ^4.2.2 version: 4.2.2 @@ -1568,6 +1580,12 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -1630,6 +1648,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -1704,6 +1726,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1752,6 +1778,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -1774,6 +1804,15 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + eslint-plugin-unused-imports@4.4.1: + resolution: {integrity: sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 + eslint: ^10.0.0 || ^9.0.0 || ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + eslint-scope@9.1.2: resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -1879,6 +1918,19 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1933,6 +1985,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2274,6 +2330,10 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4555,6 +4615,16 @@ snapshots: array-flatten@1.1.1: {} + asynckit@0.4.0: {} + + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.0 @@ -4627,6 +4697,10 @@ snapshots: clsx@2.1.1: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -4683,6 +4757,8 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} destroy@1.2.0: {} @@ -4718,6 +4794,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -4764,6 +4847,12 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1)): + dependencies: + eslint: 10.2.0(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3) + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 @@ -4920,6 +5009,16 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -4977,6 +5076,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5253,6 +5356,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@2.1.0: {} + punycode@2.3.1: {} qs@6.14.2: diff --git a/web-app/tsconfig.json b/web-app/tsconfig.json index cbe49c7..96317a8 100644 --- a/web-app/tsconfig.json +++ b/web-app/tsconfig.json @@ -17,7 +17,7 @@ "~/*": ["./app/*"] }, "esModuleInterop": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "noEmit": true, "resolveJsonModule": true, "skipLibCheck": true, -- 2.49.1 From 35f6023f5a96630705d8b9383ecbb9d37a2cfc58 Mon Sep 17 00:00:00 2001 From: "JoohyunKim(Lucy)" Date: Fri, 10 Apr 2026 12:23:48 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20=EC=8A=A4=ED=83=80=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/shared/components/button/Button.tsx | 32 +- .../shared/components/calendar/Calendar.tsx | 14 +- .../components/calendar/RangeCalendar.tsx | 4 +- .../components/datePicker/DatePicker.tsx | 16 +- .../components/datePicker/DateRangePicker.tsx | 20 +- .../components/descriptions/Descriptions.tsx | 70 +++ .../descriptions/items/DescriptionsItem.tsx | 39 ++ .../components/inputGroup/InputGroup.tsx | 22 +- .../shared/components/modal/AlertModal.tsx | 57 +++ .../shared/components/modal/ConfirmModal.tsx | 79 +++ web-app/app/shared/components/modal/Modal.tsx | 229 +++++++++ .../shared/components/modal/ModalRenderer.tsx | 29 ++ web-app/app/shared/components/modal/index.ts | 25 + web-app/app/shared/components/modal/store.ts | 43 ++ .../app/shared/components/modal/useModal.ts | 50 ++ .../components/pagination/Pagination.tsx | 2 +- .../app/shared/components/section/Section.tsx | 2 +- .../app/shared/components/select/Select.tsx | 105 ++++ web-app/app/shared/components/tree/Tree.tsx | 481 ++++++++++++++++++ 19 files changed, 1263 insertions(+), 56 deletions(-) create mode 100644 web-app/app/shared/components/descriptions/Descriptions.tsx create mode 100644 web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx create mode 100644 web-app/app/shared/components/modal/AlertModal.tsx create mode 100644 web-app/app/shared/components/modal/ConfirmModal.tsx create mode 100644 web-app/app/shared/components/modal/Modal.tsx create mode 100644 web-app/app/shared/components/modal/ModalRenderer.tsx create mode 100644 web-app/app/shared/components/modal/index.ts create mode 100644 web-app/app/shared/components/modal/store.ts create mode 100644 web-app/app/shared/components/modal/useModal.ts create mode 100644 web-app/app/shared/components/select/Select.tsx create mode 100644 web-app/app/shared/components/tree/Tree.tsx diff --git a/web-app/app/shared/components/button/Button.tsx b/web-app/app/shared/components/button/Button.tsx index d9eda45..f25ff51 100644 --- a/web-app/app/shared/components/button/Button.tsx +++ b/web-app/app/shared/components/button/Button.tsx @@ -19,21 +19,21 @@ const button = tv({ large: 'min-w-30 h-13 px-5 text-sm font-semibold', }, color: { - primary: 'text-white bg-primary border-primary disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-primary', + primary: 'text-white bg-primary border-primary disabled:bg-dabeeo-gray-be disabled:border-dabeeo-gray-be ring-primary', light: - 'text-primary border-primary bg-primary-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-primary', + 'text-primary border-primary bg-primary-tertiary02 disabled:text-dabeeo-gray-99 disabled:bg-dabeeo-gray-eb disabled:border-dabeeo-gray-be ring-primary', green: - 'text-white bg-kc-green-main border-kc-green-main disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-green-main', + 'text-white bg-dabeeo-green-main border-dabeeo-green-main disabled:bg-dabeeo-gray-be disabled:border-dabeeo-gray-be ring-dabeeo-green-main', lightGreen: - 'text-kc-green-main border-kc-green-main bg-kc-green-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-green-main', + 'text-dabeeo-green-main border-dabeeo-green-main bg-dabeeo-green-tertiary02 disabled:text-dabeeo-gray-99 disabled:bg-dabeeo-gray-eb disabled:border-dabeeo-gray-be ring-dabeeo-green-main', black: - 'text-white bg-kc-black-34 border-kc-black-34 disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-black-34', - gray: 'text-kc-black-34 bg-kc-gray-eb border-kc-black-34 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-black-34', + 'text-white bg-dabeeo-black-34 border-dabeeo-black-34 disabled:bg-dabeeo-gray-be disabled:border-dabeeo-gray-be ring-dabeeo-black-34', + gray: 'text-dabeeo-black-34 bg-dabeeo-gray-eb border-dabeeo-black-34 disabled:text-dabeeo-gray-99 disabled:bg-dabeeo-gray-eb disabled:border-dabeeo-gray-be ring-dabeeo-black-34', orange: - 'text-kc-orange-main bg-kc-orange-tertiary01 border-kc-orange-main disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-orange-main', - navy: 'text-white bg-kc-navy-main border-kc-navy-main disabled:bg-kc-gray-be disabled:border-kc-gray-be ring-kc-navy-main', + 'text-dabeeo-orange-main bg-dabeeo-orange-tertiary01 border-dabeeo-orange-main disabled:text-dabeeo-gray-99 disabled:bg-dabeeo-gray-eb disabled:border-dabeeo-gray-be ring-dabeeo-orange-main', + navy: 'text-white bg-dabeeo-navy-main border-dabeeo-navy-main disabled:bg-dabeeo-gray-be disabled:border-dabeeo-gray-be ring-dabeeo-navy-main', lightNavy: - 'text-kc-navy-main border-kc-navy-main bg-kc-navy-tertiary02 disabled:text-kc-gray-99 disabled:bg-kc-gray-eb disabled:border-kc-gray-be ring-kc-navy-main', + 'text-dabeeo-navy-main border-dabeeo-navy-main bg-dabeeo-navy-tertiary02 disabled:text-dabeeo-gray-99 disabled:bg-dabeeo-gray-eb disabled:border-dabeeo-gray-be ring-dabeeo-navy-main', }, isDisabled: { true: 'cursor-not-allowed', @@ -62,48 +62,48 @@ const button = tv({ isDisabled: false, isPending: false, className: - 'hover:bg-kc-green-tertiary hover:border-kc-green-tertiary active:bg-kc-green-secondary active:border-kc-green-secondary', + 'hover:bg-dabeeo-green-tertiary hover:border-dabeeo-green-tertiary active:bg-dabeeo-green-secondary active:border-dabeeo-green-secondary', }, { color: 'lightGreen', isDisabled: false, isPending: false, className: - 'hover:bg-kc-green-tertiary01 active:text-kc-green-secondary active:bg-kc-green-tertiary01 active:border-kc-green-secondary', + 'hover:bg-dabeeo-green-tertiary01 active:text-dabeeo-green-secondary active:bg-dabeeo-green-tertiary01 active:border-dabeeo-green-secondary', }, { color: 'black', isDisabled: false, isPending: false, - className: 'hover:bg-kc-black-47 hover:border-kc-black-47 active:bg-kc-black-22 active:border-kc-black-22', + className: 'hover:bg-dabeeo-black-47 hover:border-dabeeo-black-47 active:bg-dabeeo-black-22 active:border-dabeeo-black-22', }, { color: 'gray', isDisabled: false, isPending: false, className: - 'hover:bg-kc-gray-da hover:border-kc-black-47 active:text-kc-black-22 active:bg-kc-gray-da active:border-kc-black-22', + 'hover:bg-dabeeo-gray-da hover:border-dabeeo-black-47 active:text-dabeeo-black-22 active:bg-dabeeo-gray-da active:border-dabeeo-black-22', }, { color: 'orange', isDisabled: false, isPending: false, className: - 'hover:bg-kc-orange-tertiary02 active:text-kc-orange-secondary active:bg-kc-orange-tertiary02 active:border-kc-orange-secondary', + 'hover:bg-dabeeo-orange-tertiary02 active:text-dabeeo-orange-secondary active:bg-dabeeo-orange-tertiary02 active:border-dabeeo-orange-secondary', }, { color: 'navy', isDisabled: false, isPending: false, className: - 'hover:border-kc-navy-tertiary hover:bg-kc-navy-tertiary active:bg-kc-navy-secondary active:border-kc-navy-secondary', + 'hover:border-dabeeo-navy-tertiary hover:bg-dabeeo-navy-tertiary active:bg-dabeeo-navy-secondary active:border-dabeeo-navy-secondary', }, { color: 'lightNavy', isDisabled: false, isPending: false, className: - 'hover:bg-kc-navy-tertiary01 active:text-kc-navy-secondary active:bg-kc-navy-tertiary01 active:border-kc-navy-secondary', + 'hover:bg-dabeeo-navy-tertiary01 active:text-dabeeo-navy-secondary active:bg-dabeeo-navy-tertiary01 active:border-dabeeo-navy-secondary', }, ], }); diff --git a/web-app/app/shared/components/calendar/Calendar.tsx b/web-app/app/shared/components/calendar/Calendar.tsx index d63e673..a15b1e6 100644 --- a/web-app/app/shared/components/calendar/Calendar.tsx +++ b/web-app/app/shared/components/calendar/Calendar.tsx @@ -24,13 +24,13 @@ const cellStyles = tv({ false: '', }, isDisabled: { - true: 'cursor-default text-kc-gray-be', + true: 'cursor-default text-dabeeo-gray-be', }, isUnavailable: { - true: 'cursor-default text-kc-gray-be line-through', + true: 'cursor-default text-dabeeo-gray-be line-through', }, isOutsideMonth: { - true: 'text-kc-gray-be', + true: 'text-dabeeo-gray-be', }, isSunday: { true: 'text-[#e48686]', @@ -143,8 +143,8 @@ const navButton = tv({ base: 'flex h-7 w-7 items-center justify-center rounded-sm border-none bg-transparent outline-none', variants: { isDisabled: { - true: 'cursor-default text-kc-gray-be', - false: 'cursor-pointer text-kc-black-34 hover:bg-kc-gray-eb', + true: 'cursor-default text-dabeeo-gray-be', + false: 'cursor-pointer text-dabeeo-black-34 hover:bg-dabeeo-gray-eb', }, isFocusVisible: { true: 'ring-2 ring-offset-2 ring-primary', @@ -161,7 +161,7 @@ export function CalendarHeader() { navButton(renderProps)}> - + navButton(renderProps)}> @@ -173,7 +173,7 @@ export function CalendarGridHeader() { return ( {(day) => ( - + {day} )} diff --git a/web-app/app/shared/components/calendar/RangeCalendar.tsx b/web-app/app/shared/components/calendar/RangeCalendar.tsx index 987cab1..fb52045 100644 --- a/web-app/app/shared/components/calendar/RangeCalendar.tsx +++ b/web-app/app/shared/components/calendar/RangeCalendar.tsx @@ -21,10 +21,10 @@ const rangeCell = tv({ cap: 'bg-primary font-bold text-white', }, isDisabled: { - true: 'text-kc-gray-be', + true: 'text-dabeeo-gray-be', }, isOutsideMonth: { - true: 'text-kc-gray-be', + true: 'text-dabeeo-gray-be', }, isSunday: { true: 'text-[#e48686]', diff --git a/web-app/app/shared/components/datePicker/DatePicker.tsx b/web-app/app/shared/components/datePicker/DatePicker.tsx index f3b1987..7003aaf 100644 --- a/web-app/app/shared/components/datePicker/DatePicker.tsx +++ b/web-app/app/shared/components/datePicker/DatePicker.tsx @@ -8,16 +8,16 @@ import { calendarDateToDate, dateToCalendarDate } from '../calendar/utils'; import { CalendarIcon } from '../icons'; const trigger = tv({ - base: 'flex h-9 w-[156px] cursor-pointer items-center border border-kc-gray-be bg-white text-left outline-none transition', + base: 'flex h-9 w-[156px] cursor-pointer items-center border border-dabeeo-gray-be bg-white text-left outline-none transition', variants: { isHovered: { - true: 'border-kc-black-34', + true: 'border-dabeeo-black-34', }, isFocused: { - true: 'border-kc-black-34', + true: 'border-dabeeo-black-34', }, isDisabled: { - true: 'cursor-default border-kc-gray-99 bg-kc-gray-eb', + true: 'cursor-default border-dabeeo-gray-99 bg-dabeeo-gray-eb', }, isFocusVisible: { true: 'ring-2 ring-offset-2 ring-primary', @@ -51,21 +51,21 @@ export function DatePicker({ value, onChange, maxValue, minValue, isDisabled, cl > - + diff --git a/web-app/app/shared/components/datePicker/DateRangePicker.tsx b/web-app/app/shared/components/datePicker/DateRangePicker.tsx index c88f176..41b99e1 100644 --- a/web-app/app/shared/components/datePicker/DateRangePicker.tsx +++ b/web-app/app/shared/components/datePicker/DateRangePicker.tsx @@ -16,16 +16,16 @@ import { RangeCalendar } from '../calendar/RangeCalendar'; import { calendarDateToDate, dateToCalendarDate } from '../calendar/utils'; const trigger = tv({ - base: 'flex h-9 w-[260px] cursor-pointer items-center border border-kc-gray-be bg-white text-left outline-none transition', + base: 'flex h-9 w-[260px] cursor-pointer items-center border border-dabeeo-gray-be bg-white text-left outline-none transition', variants: { isHovered: { - true: 'border-kc-black-34', + true: 'border-dabeeo-black-34', }, isFocused: { - true: 'border-kc-black-34', + true: 'border-dabeeo-black-34', }, isDisabled: { - true: 'cursor-default border-kc-gray-99 bg-kc-gray-eb', + true: 'cursor-default border-dabeeo-gray-99 bg-dabeeo-gray-eb', }, isFocusVisible: { true: 'ring-2 ring-offset-2 ring-primary', @@ -81,25 +81,25 @@ export function DateRangePicker({ > - + diff --git a/web-app/app/shared/components/descriptions/Descriptions.tsx b/web-app/app/shared/components/descriptions/Descriptions.tsx new file mode 100644 index 0000000..13f682e --- /dev/null +++ b/web-app/app/shared/components/descriptions/Descriptions.tsx @@ -0,0 +1,70 @@ +import { DescriptionsItem, DescriptionsItemProps } from './items/DescriptionsItem'; + +export type DescriotionItemType = { + items: (DescriptionsItemProps & { key?: string })[]; + column?: 1 | 2 | 3 | 4; + titleWidth?: number; + className?: string; + titleClassName?: string; + contentClassName?: string; +}; + +export const Descriptions = (props: DescriotionItemType) => { + const { items, column = 1, titleWidth, className, titleClassName, contentClassName } = props; + + const columnClassName = + { + 1: 'grid-cols-1', + 2: 'grid-cols-2', + 3: 'grid-cols-3', + 4: 'grid-cols-4', + }[column] || 'grid-cols-1'; + + const getFirstRowItemIndices = () => { + const firstRowItemIndices: number[] = []; + let availableColumns = column; + + for (let index = 0; index < items.length; index += 1) { + if (availableColumns <= 0) { + break; + } + + const item = items[index]; + const itemSpan = item.span === 'filled' ? column : (item.span ?? 1); + const effectiveSpan = Math.min(typeof itemSpan === 'number' ? itemSpan : column, column); + + if (effectiveSpan > availableColumns) { + break; + } + + firstRowItemIndices.push(index); + availableColumns -= effectiveSpan; + + if (availableColumns === 0) { + break; + } + } + + return firstRowItemIndices; + }; + + const firstRowItemIndices = getFirstRowItemIndices(); + + return ( +
+ {items.map((item, index) => { + const { key, ...itemProps } = item; + return ( + + ); + })} +
+ ); +}; diff --git a/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx b/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx new file mode 100644 index 0000000..eb53f00 --- /dev/null +++ b/web-app/app/shared/components/descriptions/items/DescriptionsItem.tsx @@ -0,0 +1,39 @@ +import clsx from 'clsx'; + +export type DescriptionsItemProps = { + title: React.ReactNode; + content: React.ReactNode; + span?: number | 'filled'; + isFirstRow?: boolean; + titleWidth?: number; + titleClassName?: string; + contentClassName?: string; +}; + +export const DescriptionsItem = (props: DescriptionsItemProps) => { + const { title, content, span, isFirstRow = false, titleWidth, titleClassName, contentClassName } = props; + + const gridColumnStyle = + span === 'filled' ? { gridColumn: '1 / -1' } : span && span > 0 ? { gridColumn: `span ${span}` } : {}; + + return ( +
+
+ {title} +
+
+ {content} +
+
+ ); +}; diff --git a/web-app/app/shared/components/inputGroup/InputGroup.tsx b/web-app/app/shared/components/inputGroup/InputGroup.tsx index 04f7ac5..b087edc 100644 --- a/web-app/app/shared/components/inputGroup/InputGroup.tsx +++ b/web-app/app/shared/components/inputGroup/InputGroup.tsx @@ -16,18 +16,18 @@ const style = tv({ 'flex', 'items-center', 'text-sm', - 'text-kc-black-34', + 'text-dabeeo-black-34', 'leading-[18px]', 'border', - 'border-kc-gray-be', - 'has-data-focused:border-kc-black-34', - 'has-data-hovered:border-kc-black-34', - 'has-data-disabled:bg-kc-gray-eb', - 'has-data-disabled:text-kc-gray-99', - 'has-data-disabled:border-kc-gray-be', - 'has-data-invalid:border-kc-red', - 'has-data-invalid:has-data-focused:border-kc-red', - 'has-data-invalid:has-data-hovered:border-kc-red', + 'border-dabeeo-gray-be', + 'has-data-focused:border-dabeeo-black-34', + 'has-data-hovered:border-dabeeo-black-34', + 'has-data-disabled:bg-dabeeo-gray-eb', + 'has-data-disabled:text-dabeeo-gray-99', + 'has-data-disabled:border-dabeeo-gray-be', + 'has-data-invalid:border-dabeeo-red', + 'has-data-invalid:has-data-focused:border-dabeeo-red', + 'has-data-invalid:has-data-hovered:border-dabeeo-red', 'w-full', 'has-[[type=number]]:leading-6', ], @@ -36,7 +36,7 @@ const style = tv({ variants: { isReadOnly: { true: { - base: 'bg-kc-yellow-secondary has-data-hovered:border-kc-gray-be has-data-focused:border-kc-gray-be', + base: 'bg-dabeeo-yellow-secondary has-data-hovered:border-dabeeo-gray-be has-data-focused:border-dabeeo-gray-be', }, }, }, diff --git a/web-app/app/shared/components/modal/AlertModal.tsx b/web-app/app/shared/components/modal/AlertModal.tsx new file mode 100644 index 0000000..5b30229 --- /dev/null +++ b/web-app/app/shared/components/modal/AlertModal.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from 'react'; + +import { Button } from '../button/Button'; +import type { ModalRootProps } from './Modal'; +import { ModalBody, ModalFooter, ModalHeader, ModalRoot } from './Modal'; + +interface AlertModalProps { + isOpen?: ModalRootProps['isOpen']; + onOpenChange?: ModalRootProps['onOpenChange']; + title?: string; + content?: ReactNode; + confirmText?: string; + onConfirm?: () => void; +} + +export const AlertModal = (props: AlertModalProps) => { + const { isOpen, onOpenChange, title, ...contentProps } = props; + + return ( + + {({ close }) => } + + ); +}; + +function AlertModalContent({ + close, + title, + content, + confirmText = '확인', + onConfirm, +}: { + close: () => void; + title?: string; + content?: ReactNode; + confirmText?: string; + onConfirm?: () => void; +}) { + const handleConfirm = () => { + onConfirm?.(); + close(); + }; + + return ( + <> + {title && {title}} + +

{content}

+
+ + + + + ); +} diff --git a/web-app/app/shared/components/modal/ConfirmModal.tsx b/web-app/app/shared/components/modal/ConfirmModal.tsx new file mode 100644 index 0000000..f1dcd04 --- /dev/null +++ b/web-app/app/shared/components/modal/ConfirmModal.tsx @@ -0,0 +1,79 @@ +import type { ReactNode } from 'react'; +import { useTransition } from 'react'; + +import { Button } from '../button/Button'; +import { ModalBody, ModalFooter, ModalHeader, ModalRoot } from './Modal'; + +interface ConfirmModalProps { + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; + title?: string; + content?: ReactNode; + confirmText?: string; + cancelText?: string; + onConfirm?: () => Promise | void; + onCancel?: () => void; +} + +export const ConfirmModal = (props: ConfirmModalProps) => { + const { isOpen, onOpenChange, title, ...contentProps } = props; + + return ( + + {({ close }) => } + + ); +}; + +function ConfirmModalContent({ + close, + title, + content, + confirmText = '확인', + cancelText = '취소', + onConfirm, + onCancel, +}: { + close: () => void; + title?: string; + content?: ReactNode; + confirmText?: string; + cancelText?: string; + onConfirm?: () => Promise | void; + onCancel?: () => void; +}) { + const [isPending, startTransition] = useTransition(); + + const handleConfirm = () => { + if (onConfirm) { + startTransition(async () => { + await onConfirm(); + close(); + }); + } else { + close(); + } + }; + + const handleCancel = () => { + onCancel?.(); + close(); + }; + + return ( + <> + {title && {title}} + +

{content}

+
+ + + + + + ); +} diff --git a/web-app/app/shared/components/modal/Modal.tsx b/web-app/app/shared/components/modal/Modal.tsx new file mode 100644 index 0000000..c44bc8c --- /dev/null +++ b/web-app/app/shared/components/modal/Modal.tsx @@ -0,0 +1,229 @@ +'use client'; +import { use, useSyncExternalStore, useTransition } from 'react'; +import type { DialogProps, ModalOverlayProps } from 'react-aria-components'; +import { + Button as AriaButton, + Dialog, + Heading, + Modal, + ModalOverlay, + OverlayTriggerStateContext, +} from 'react-aria-components'; + +import clsx from 'clsx'; +import { twMerge } from 'tailwind-merge'; +import { Button } from '../button/Button'; + +interface ModalRootProps + extends DialogProps, + Pick { + className?: string; +} +const ModalRoot = (props: ModalRootProps) => { + const { isOpen, onOpenChange, isKeyboardDismissDisabled, isDismissable, className, ...dialogProps } = props; + return ( + + + + {props.children} + + + + ); +}; + +interface ModalHeaderProps { + hasCloseButton?: boolean; + children: React.ReactNode; +} +const ModalHeader = (props: ModalHeaderProps) => { + const ctx = use(OverlayTriggerStateContext); + + const { hasCloseButton = true } = props; + return ( +
+ + {props.children} + + {hasCloseButton && ( + + + + + + )} +
+ ); +}; + +interface ModalBodyProps extends React.PropsWithChildren { + className?: string; +} +const ModalBody = (props: ModalBodyProps) => { + return
{props.children}
; +}; + +interface ModalFooterProps extends React.PropsWithChildren {} +const ModalFooter = (props: ModalFooterProps) => { + return
{props.children}
; +}; + +let _modals: ( + | { + typeof: 'confirm'; + title?: string; + content: string; + confirm?: boolean; + cancelText?: string; + onCancel?: () => void; + confirmText?: string; + onConfirm: (close: () => void) => void; + } + | { + typeof: 'alert'; + title?: string; + content: string; + cancelText?: string; + onCancel?: () => void; + } +)[] = []; +let listeners: (() => void)[] = []; +function subscribe(listener: () => void) { + listeners = [...listeners, listener]; + return () => { + listeners = listeners.filter((l) => l !== listener); + }; +} +function getSnapshot() { + return _modals; +} + +function emitChange() { + for (const listener of listeners) { + listener(); + } +} + +type AlertProps = { + title?: string; + content?: string; + cancelText?: string; + onCancel?: () => void; +}; +const alert = (props: AlertProps) => { + _modals = [ + ..._modals, + { + typeof: 'alert', + content: props.content || '', + cancelText: props.cancelText || '확인', + onCancel: props.onCancel, + }, + ]; + emitChange(); +}; + +const confirm = (props: { + title?: string; + content: string; + cancelText?: string; + onCancel?: () => void; + confirmText?: string; + onConfirm: (close: () => void) => void; +}) => { + _modals = [ + ..._modals, + { + typeof: 'confirm', + title: props.title, + content: props.content, + cancelText: props.cancelText, + onCancel: props.onCancel, + confirmText: props.confirmText, + onConfirm: props.onConfirm, + }, + ]; + emitChange(); +}; + +function getServerSnapshot() { + return _modals; +} + +const ModalRegion = () => { + const modals = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + + const [isPending, startTransition] = useTransition(); + return ( + <> + {modals.map((modal, idx) => ( + { + if (!isOpen) { + _modals = _modals.filter((f) => f !== modal); + emitChange(); + } + }} + > + {({ close }) => ( + <> + {modal.title && {modal.title}} + +

{modal.content}

+
+ + + {modal.typeof === 'confirm' && ( + + )} + + + )} +
+ ))} + + ); +}; +export { alert, confirm, ModalBody, ModalFooter, ModalHeader, ModalRegion, ModalRoot }; +export type { ModalBodyProps, ModalFooterProps, ModalHeaderProps, ModalRootProps }; + diff --git a/web-app/app/shared/components/modal/ModalRenderer.tsx b/web-app/app/shared/components/modal/ModalRenderer.tsx new file mode 100644 index 0000000..ec4f41b --- /dev/null +++ b/web-app/app/shared/components/modal/ModalRenderer.tsx @@ -0,0 +1,29 @@ +'use client'; +import { useSyncExternalStore } from 'react'; + +import { modalStore } from './store'; + +export const ModalRenderer = () => { + const modals = useSyncExternalStore(modalStore.subscribe, modalStore.getSnapshot, modalStore.getServerSnapshot); + + return ( + <> + {modals.map((modal) => { + const { Component, props, id, resolve } = modal; + return ( + { + if (!open) { + resolve(); + modalStore.removeModal(id); + } + }} + /> + ); + })} + + ); +}; diff --git a/web-app/app/shared/components/modal/index.ts b/web-app/app/shared/components/modal/index.ts new file mode 100644 index 0000000..d5d6383 --- /dev/null +++ b/web-app/app/shared/components/modal/index.ts @@ -0,0 +1,25 @@ +import { + ModalBody, + ModalFooter, + ModalHeader, + ModalRegion as ModalRegionPrimitive, + ModalRoot, + alert, + confirm, +} from './Modal'; + +export const Modal = Object.assign(ModalRoot, { + Header: ModalHeader, + Body: ModalBody, + Footer: ModalFooter, +}); + +export const ModalRegion = Object.assign(ModalRegionPrimitive, { + alert, + confirm, +}); + +export { useModal } from './useModal'; +export { ModalRenderer } from './ModalRenderer'; +export { AlertModal } from './AlertModal'; +export { ConfirmModal } from './ConfirmModal'; diff --git a/web-app/app/shared/components/modal/store.ts b/web-app/app/shared/components/modal/store.ts new file mode 100644 index 0000000..ac2d9dc --- /dev/null +++ b/web-app/app/shared/components/modal/store.ts @@ -0,0 +1,43 @@ +import type { ComponentType } from 'react'; + +interface ModalData { + id: string; + scopeId: string; + Component: ComponentType; + props: Record; + resolve: () => void; +} + +let modals: ModalData[] = []; +let listeners: (() => void)[] = []; + +const emitChange = () => { + listeners.forEach((listener) => listener()); +}; + +const SERVER_SNAPSHOT: ModalData[] = []; + +export const modalStore = { + subscribe: (listener: () => void) => { + listeners = [...listeners, listener]; + return () => { + listeners = listeners.filter((l) => l !== listener); + }; + }, + getSnapshot: () => modals, + getServerSnapshot: () => SERVER_SNAPSHOT, + addModal: (modal: ModalData) => { + modals = [...modals, modal]; + emitChange(); + }, + removeModal: (id: string) => { + modals = modals.filter((m) => m.id !== id); + emitChange(); + }, + removeByScope: (scopeId: string) => { + modals = modals.filter((m) => m.scopeId !== scopeId); + emitChange(); + }, +}; + +export type { ModalData }; diff --git a/web-app/app/shared/components/modal/useModal.ts b/web-app/app/shared/components/modal/useModal.ts new file mode 100644 index 0000000..d637246 --- /dev/null +++ b/web-app/app/shared/components/modal/useModal.ts @@ -0,0 +1,50 @@ +import type { ComponentType } from 'react'; +import { useCallback, useEffect, useId } from 'react'; + +import { modalStore } from './store'; + +interface ModalControlProps { + isOpen?: boolean; + onOpenChange?: (isOpen: boolean) => void; +} + +export const useModal = () => { + const scopeId = useId(); + + useEffect(() => { + return () => { + modalStore + .getSnapshot() + .filter((m) => m.scopeId === scopeId) + .forEach((m) => m.resolve()); + modalStore.removeByScope(scopeId); + }; + }, [scopeId]); + + const show = useCallback( +

( + Component: ComponentType

, + props?: Omit + ): Promise => + new Promise((resolve) => { + modalStore.addModal({ + id: crypto.randomUUID(), + scopeId, + Component, + props: props ?? {}, + resolve, + }); + }), + [scopeId] + ); + + const hide = useCallback(() => { + modalStore + .getSnapshot() + .filter((m) => m.scopeId === scopeId) + .forEach((m) => m.resolve()); + modalStore.removeByScope(scopeId); + }, [scopeId]); + + return { show, hide }; +}; diff --git a/web-app/app/shared/components/pagination/Pagination.tsx b/web-app/app/shared/components/pagination/Pagination.tsx index 294555f..1037bd7 100644 --- a/web-app/app/shared/components/pagination/Pagination.tsx +++ b/web-app/app/shared/components/pagination/Pagination.tsx @@ -49,7 +49,7 @@ export const Pagination = ({ totalPages, currentPage, pageCount = 10, onPageChan const noNext = start + pageCount >= totalPages; return ( -