diff --git a/.github/workflows/deply.yml b/.github/workflows/deply.yml new file mode 100644 index 00000000..7ca142f2 --- /dev/null +++ b/.github/workflows/deply.yml @@ -0,0 +1,48 @@ +name: Deploy to GitHub Pages + +on: + push: # push trigger + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm run build + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./dist" + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/eslint.config.js b/eslint.config.js index 9e887bc1..717e39a4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,11 +2,17 @@ import globals from "globals"; import pluginJs from "@eslint/js"; import eslintConfigPrettier from "eslint-config-prettier"; import eslintPluginPrettier from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.Config[]} */ export default [ + { ignores: ["dist", "build", "node_modules", "src/template.ts"] }, { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, + ...tseslint.configs.recommended, + { + files: ["**/*.{ts,tsx}"], + }, eslintPluginPrettier, eslintConfigPrettier, ]; diff --git a/package.json b/package.json index 5ec7f3f3..dfe8a180 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@9.0.0", "scripts": { "dev": "vite", "dev:hash": "vite --open ./index.hash.html", @@ -30,6 +31,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", + "@types/node": "^24.10.1", "@vitest/coverage-v8": "latest", "@vitest/ui": "^2.1.8", "eslint": "^9.16.0", @@ -41,7 +43,10 @@ "lint-staged": "^15.2.11", "msw": "^2.10.2", "prettier": "^3.4.2", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.4", "vite": "npm:rolldown-vite@latest", + "vite-tsconfig-paths": "^5.1.4", "vitest": "latest" }, "msw": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8137d4c8..40b72f5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,12 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.0) + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 '@vitest/coverage-v8': specifier: latest - version: 3.2.4(vitest@3.2.4) + version: 3.2.4(vitest@3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3))) '@vitest/ui': specifier: ^2.1.8 version: 2.1.9(vitest@3.2.4) @@ -52,16 +55,25 @@ importers: version: 15.5.0 msw: specifier: ^2.10.2 - version: 2.10.2 + version: 2.10.2(@types/node@24.10.1)(typescript@5.9.3) prettier: specifier: ^3.4.2 version: 3.5.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.46.4 + version: 8.46.4(eslint@9.23.0)(typescript@5.9.3) vite: specifier: npm:rolldown-vite@latest - version: rolldown-vite@6.3.21(esbuild@0.25.1)(yaml@2.7.0) + version: rolldown-vite@6.3.21(@types/node@24.10.1)(esbuild@0.25.1)(yaml@2.7.0) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(rolldown-vite@6.3.21(@types/node@24.10.1)(esbuild@0.25.1)(yaml@2.7.0))(typescript@5.9.3) vitest: specifier: latest - version: 3.2.4(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2) + version: 3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3)) packages: @@ -448,6 +460,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -559,6 +577,18 @@ packages: '@napi-rs/wasm-runtime@0.2.11': resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -784,12 +814,74 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@typescript-eslint/eslint-plugin@8.46.4': + resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.46.4 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.46.4': + resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.46.4': + resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.46.4': + resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.46.4': + resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: @@ -1148,6 +1240,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.23.0: resolution: {integrity: sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1198,12 +1294,19 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + fdir@6.4.3: resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} peerDependencies: @@ -1283,6 +1386,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1299,10 +1406,16 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.11.0: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -1358,6 +1471,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1582,6 +1699,10 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1772,6 +1893,9 @@ packages: querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -1797,6 +1921,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -1855,6 +1983,9 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2022,6 +2153,22 @@ packages: resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} engines: {node: '>=18'} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2037,6 +2184,21 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typescript-eslint@8.46.4: + resolution: {integrity: sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -2052,6 +2214,14 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@5.4.14: resolution: {integrity: sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2447,6 +2617,11 @@ snapshots: eslint: 9.23.0 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.23.0)': + dependencies: + eslint: 9.23.0 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.19.2': @@ -2499,25 +2674,31 @@ snapshots: '@humanwhocodes/retry@0.4.2': {} - '@inquirer/confirm@5.1.12': + '@inquirer/confirm@5.1.12(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.13 - '@inquirer/type': 3.0.7 + '@inquirer/core': 10.1.13(@types/node@24.10.1) + '@inquirer/type': 3.0.7(@types/node@24.10.1) + optionalDependencies: + '@types/node': 24.10.1 - '@inquirer/core@10.1.13': + '@inquirer/core@10.1.13(@types/node@24.10.1)': dependencies: '@inquirer/figures': 1.0.12 - '@inquirer/type': 3.0.7 + '@inquirer/type': 3.0.7(@types/node@24.10.1) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 + optionalDependencies: + '@types/node': 24.10.1 '@inquirer/figures@1.0.12': {} - '@inquirer/type@3.0.7': {} + '@inquirer/type@3.0.7(@types/node@24.10.1)': + optionalDependencies: + '@types/node': 24.10.1 '@isaacs/cliui@8.0.2': dependencies: @@ -2560,6 +2741,18 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/logger@0.3.0': @@ -2725,11 +2918,108 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + '@types/statuses@2.0.6': {} '@types/tough-cookie@4.0.5': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': + '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.23.0)(typescript@5.9.3))(eslint@9.23.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/type-utils': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + eslint: 9.23.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.46.4(eslint@9.23.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.1 + eslint: 9.23.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + debug: 4.4.1 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + + '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.46.4(eslint@9.23.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + debug: 4.4.1 + eslint: 9.23.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.46.4': {} + + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.46.4(eslint@9.23.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.23.0) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + eslint: 9.23.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.46.4': + dependencies: + '@typescript-eslint/types': 8.46.4 + eslint-visitor-keys: 4.2.1 + + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3)))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -2744,7 +3034,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2) + vitest: 3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3)) transitivePeerDependencies: - supports-color @@ -2756,14 +3046,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.2)(vite@5.4.14(lightningcss@1.30.1))': + '@vitest/mocker@3.2.4(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3))(vite@5.4.14(@types/node@24.10.1)(lightningcss@1.30.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.2 - vite: 5.4.14(lightningcss@1.30.1) + msw: 2.10.2(@types/node@24.10.1)(typescript@5.9.3) + vite: 5.4.14(@types/node@24.10.1)(lightningcss@1.30.1) '@vitest/pretty-format@2.1.9': dependencies: @@ -2798,7 +3088,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.12 tinyrainbow: 1.2.0 - vitest: 3.2.4(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2) + vitest: 3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3)) '@vitest/utils@2.1.9': dependencies: @@ -3106,6 +3396,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.23.0: dependencies: '@eslint-community/eslint-utils': 4.5.1(eslint@9.23.0) @@ -3188,10 +3480,22 @@ snapshots: fast-diff@1.3.0: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + fdir@6.4.3(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -3266,6 +3570,10 @@ snapshots: get-stream@8.0.1: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -3283,8 +3591,12 @@ snapshots: globals@15.15.0: {} + globrex@0.1.2: {} + gopd@1.2.0: {} + graphemer@1.4.0: {} + graphql@16.11.0: {} has-flag@4.0.0: {} @@ -3331,6 +3643,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3555,6 +3869,8 @@ snapshots: merge-stream@2.0.0: {} + merge2@1.4.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3586,12 +3902,12 @@ snapshots: ms@2.1.3: {} - msw@2.10.2: + msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.12 + '@inquirer/confirm': 5.1.12(@types/node@24.10.1) '@mswjs/interceptors': 0.39.2 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 @@ -3606,6 +3922,8 @@ snapshots: strict-event-emitter: 0.5.1 type-fest: 4.41.0 yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - '@types/node' @@ -3721,6 +4039,8 @@ snapshots: querystringify@2.2.0: {} + queue-microtask@1.2.3: {} + react-is@17.0.2: {} redent@3.0.0: @@ -3741,9 +4061,11 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + reusify@1.1.0: {} + rfdc@1.4.1: {} - rolldown-vite@6.3.21(esbuild@0.25.1)(yaml@2.7.0): + rolldown-vite@6.3.21(@types/node@24.10.1)(esbuild@0.25.1)(yaml@2.7.0): dependencies: '@oxc-project/runtime': 0.73.0 fdir: 6.4.6(picomatch@4.0.2) @@ -3753,6 +4075,7 @@ snapshots: rolldown: 1.0.0-beta.16 tinyglobby: 0.2.14 optionalDependencies: + '@types/node': 24.10.1 esbuild: 0.25.1 fsevents: 2.3.3 yaml: 2.7.0 @@ -3806,6 +4129,10 @@ snapshots: rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -3956,6 +4283,14 @@ snapshots: dependencies: punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfck@3.1.6(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + tslib@2.8.1: {} type-check@0.4.0: @@ -3966,6 +4301,21 @@ snapshots: type-fest@4.41.0: {} + typescript-eslint@8.46.4(eslint@9.23.0)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.23.0)(typescript@5.9.3))(eslint@9.23.0)(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.4(eslint@9.23.0)(typescript@5.9.3) + eslint: 9.23.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + universalify@0.2.0: {} uri-js@4.4.1: @@ -3977,13 +4327,13 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - vite-node@3.2.4(lightningcss@1.30.1): + vite-node@3.2.4(@types/node@24.10.1)(lightningcss@1.30.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.14(lightningcss@1.30.1) + vite: 5.4.14(@types/node@24.10.1)(lightningcss@1.30.1) transitivePeerDependencies: - '@types/node' - less @@ -3995,20 +4345,32 @@ snapshots: - supports-color - terser - vite@5.4.14(lightningcss@1.30.1): + vite-tsconfig-paths@5.1.4(rolldown-vite@6.3.21(@types/node@24.10.1)(esbuild@0.25.1)(yaml@2.7.0))(typescript@5.9.3): + dependencies: + debug: 4.4.1 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: rolldown-vite@6.3.21(@types/node@24.10.1)(esbuild@0.25.1)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + - typescript + + vite@5.4.14(@types/node@24.10.1)(lightningcss@1.30.1): dependencies: esbuild: 0.21.5 postcss: 8.5.3 rollup: 4.36.0 optionalDependencies: + '@types/node': 24.10.1 fsevents: 2.3.3 lightningcss: 1.30.1 - vitest@3.2.4(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2): + vitest@3.2.4(@types/node@24.10.1)(@vitest/ui@2.1.9)(jsdom@25.0.1)(lightningcss@1.30.1)(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.2)(vite@5.4.14(lightningcss@1.30.1)) + '@vitest/mocker': 3.2.4(msw@2.10.2(@types/node@24.10.1)(typescript@5.9.3))(vite@5.4.14(@types/node@24.10.1)(lightningcss@1.30.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4026,10 +4388,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 5.4.14(lightningcss@1.30.1) - vite-node: 3.2.4(lightningcss@1.30.1) + vite: 5.4.14(@types/node@24.10.1)(lightningcss@1.30.1) + vite-node: 3.2.4(@types/node@24.10.1)(lightningcss@1.30.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 24.10.1 '@vitest/ui': 2.1.9(vitest@3.2.4) jsdom: 25.0.1 transitivePeerDependencies: diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js index d2b72964..b1f186b6 100644 --- a/public/mockServiceWorker.js +++ b/public/mockServiceWorker.js @@ -100,10 +100,7 @@ addEventListener("fetch", function (event) { // Opening the DevTools triggers the "only-if-cached" request // that cannot be handled by the worker. Bypass such requests. - if ( - event.request.cache === "only-if-cached" && - event.request.mode !== "same-origin" - ) { + if (event.request.cache === "only-if-cached" && event.request.mode !== "same-origin") { return; } @@ -219,9 +216,7 @@ async function getResponse(event, client, requestId) { const acceptHeader = headers.get("accept"); if (acceptHeader) { const values = acceptHeader.split(",").map((value) => value.trim()); - const filteredValues = values.filter( - (value) => value !== "msw/passthrough", - ); + const filteredValues = values.filter((value) => value !== "msw/passthrough"); if (filteredValues.length > 0) { headers.set("accept", filteredValues.join(", ")); @@ -291,10 +286,7 @@ function sendToClient(client, message, transferrables = []) { resolve(event.data); }; - client.postMessage(message, [ - channel.port2, - ...transferrables.filter(Boolean), - ]); + client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]); }); } diff --git a/src/App.ts b/src/App.ts new file mode 100644 index 00000000..00dd0244 --- /dev/null +++ b/src/App.ts @@ -0,0 +1,38 @@ +import { AppContents, ProductListPage } from "./pages"; +import createRouter from "./router"; +import { fetchProducts } from "./store/products-list/fetchProducts"; +import { ProductState, productStore } from "./store/products-list/productStore"; +import { setupFilterEventHandlers } from "./components/SearchForm/filterEventHandlers"; + +export default function App() { + const $root = document.querySelector("#root"); + + const renderHome = (state: ProductState) => { + $root.innerHTML = AppContents({ + children: ProductListPage(state), + }); + // DOM 렌더링 후 이벤트 리스너 등록 + setupFilterEventHandlers(); + }; + + const pages = { + home: () => { + const currentState = productStore.getState(); + renderHome(currentState); + }, + products: () => ($root.innerHTML = AppContents({ children: `products 입니다` })), + }; + const router = createRouter(); + + router.addRoute("#/", pages.home).addRoute("#/products", pages.products).start(); + + productStore.subscribe((state) => { + const hash = window.location.hash || "#/"; + + if (hash === "#/" || hash === "" || hash === "#") { + renderHome(state); + } + }); + + fetchProducts(productStore.getState().params); +} diff --git a/src/api/productApi.js b/src/api/productApi.ts similarity index 77% rename from src/api/productApi.js rename to src/api/productApi.ts index bbdea046..fa6e5aa4 100644 --- a/src/api/productApi.js +++ b/src/api/productApi.ts @@ -1,9 +1,11 @@ +import { GetProductsParams, GetProductsResponse } from "../types"; + // 상품 목록 조회 -export async function getProducts(params = {}) { +export async function getProducts(params: GetProductsParams): Promise { const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params; const page = params.current ?? params.page ?? 1; - const searchParams = new URLSearchParams({ + const searchParams: URLSearchParams = new URLSearchParams({ page: page.toString(), limit: limit.toString(), ...(search && { search }), diff --git a/src/components/Loading/Skeleton.ts b/src/components/Loading/Skeleton.ts new file mode 100644 index 00000000..e9561feb --- /dev/null +++ b/src/components/Loading/Skeleton.ts @@ -0,0 +1,11 @@ +export const Skeleton = /* html */ ` +
+
+
+
+
+
+
+
+
+`; diff --git a/src/components/Loading/Spinner.ts b/src/components/Loading/Spinner.ts new file mode 100644 index 00000000..d69dddf0 --- /dev/null +++ b/src/components/Loading/Spinner.ts @@ -0,0 +1,12 @@ +const Spinner = /* html */ ` +
+ + + + + 상품을 불러오는 중... +
+`; + +export { Spinner }; diff --git a/src/components/Loading/index.ts b/src/components/Loading/index.ts new file mode 100644 index 00000000..b45e4f84 --- /dev/null +++ b/src/components/Loading/index.ts @@ -0,0 +1,13 @@ +import { Skeleton } from "./Skeleton"; +import { Spinner } from "./Spinner"; + +export const Loading = /* html */ ` +
+
+ ${Skeleton.repeat(8)} +
+
+ ${Spinner} +
+
+`; diff --git a/src/components/ProductList/ProductCard.ts b/src/components/ProductList/ProductCard.ts new file mode 100644 index 00000000..1867f43e --- /dev/null +++ b/src/components/ProductList/ProductCard.ts @@ -0,0 +1,33 @@ +import { ProductItem } from "@/types"; + +export const ProductCard = (product: ProductItem) => { + return /* html */ ` +
+ +
+ ${product.title} +
+ +
+
+

+ ${product.title} +

+

${product.brand}

+

+ ${parseInt(product.lprice).toLocaleString()}원 +

+
+ + +
+
+ `; +}; diff --git a/src/components/ProductList/index.ts b/src/components/ProductList/index.ts new file mode 100644 index 00000000..0e8a29e2 --- /dev/null +++ b/src/components/ProductList/index.ts @@ -0,0 +1,24 @@ +import { ProductItem } from "@/types"; +import { ProductCard } from "./ProductCard"; + +interface ProductListProps { + products: ProductItem[]; + total: number; +} + +export const ProductList = ({ products, total }: ProductListProps) => { + console.log("ProductList: ", products); + + return /* html */ ` +
+ +
+ 총 ${total}개의 상품 +
+ +
+ ${products.map((product) => ProductCard(product)).join("")} +
+
+`; +}; diff --git a/src/components/SearchForm/FilterOptions.ts b/src/components/SearchForm/FilterOptions.ts new file mode 100644 index 00000000..1fa358ee --- /dev/null +++ b/src/components/SearchForm/FilterOptions.ts @@ -0,0 +1,72 @@ +import { LIMIT_OPTIONS, SORT_OPTIONS } from "@/constants"; +import { Loading } from "./Loading"; +import type { GetProductsParams } from "@/types"; + +interface FilterOptionsProps { + loading: boolean; + params: GetProductsParams; +} + +export const FilterOptions = ({ loading, params }: FilterOptionsProps) => { + const currentLimit = params.limit ?? 20; + const currentSort = params.sort ?? "price_asc"; + + return /* html */ ` + +
+ +
+
+ + +
+ ${ + loading + ? `${Loading}` + : /*html */ ` + +
+ + +
+ ` + } + +
+ +
+ +
+ + +
+ +
+ + +
+
+
`; +}; diff --git a/src/components/SearchForm/Loading.ts b/src/components/SearchForm/Loading.ts new file mode 100644 index 00000000..3af07891 --- /dev/null +++ b/src/components/SearchForm/Loading.ts @@ -0,0 +1,5 @@ +export const Loading = /* html */ ` +
+
카테고리 로딩 중...
+
+`; diff --git a/src/components/SearchForm/SearchInput.ts b/src/components/SearchForm/SearchInput.ts new file mode 100644 index 00000000..b30e0724 --- /dev/null +++ b/src/components/SearchForm/SearchInput.ts @@ -0,0 +1,14 @@ +export const SearchInput = /* html */ ` +
+
+ +
+ + + +
+
+
+`; diff --git a/src/components/SearchForm/filterEventHandlers.ts b/src/components/SearchForm/filterEventHandlers.ts new file mode 100644 index 00000000..af6f25c1 --- /dev/null +++ b/src/components/SearchForm/filterEventHandlers.ts @@ -0,0 +1,40 @@ +import { fetchProducts } from "@/store/products-list/fetchProducts"; + +/** + * FilterOptions의 select 요소에 이벤트 리스너를 등록하는 함수 + */ +export function setupFilterEventHandlers() { + // 기존 리스너 제거를 위해 클론하여 새로 등록 + const limitSelect = document.getElementById("limit-select"); + const sortSelect = document.getElementById("sort-select"); + + // limit-select 이벤트 리스너 + if (limitSelect) { + limitSelect.removeEventListener("change", handleLimitChange); + limitSelect.addEventListener("change", handleLimitChange); + } + + // sort-select 이벤트 리스너 + if (sortSelect) { + sortSelect.removeEventListener("change", handleSortChange); + sortSelect.addEventListener("change", handleSortChange); + } +} + +/** + * limit-select 변경 핸들러 + */ +function handleLimitChange(event: Event) { + const target = event.target as HTMLSelectElement; + const limit = parseInt(target.value, 10); + fetchProducts({ limit, page: 1 }); +} + +/** + * sort-select 변경 핸들러 + */ +function handleSortChange(event: Event) { + const target = event.target as HTMLSelectElement; + const sort = target.value as "price_asc" | "price_desc" | "name_asc" | "name_desc"; + fetchProducts({ sort, page: 1 }); +} diff --git a/src/components/SearchForm/index.ts b/src/components/SearchForm/index.ts new file mode 100644 index 00000000..eda6618d --- /dev/null +++ b/src/components/SearchForm/index.ts @@ -0,0 +1,17 @@ +import { SearchInput } from "./SearchInput"; +import { FilterOptions } from "./FilterOptions"; +import type { GetProductsParams } from "@/types"; + +interface SearchFormProps { + loading: boolean; + params: GetProductsParams; +} + +export const SearchForm = ({ loading, params }: SearchFormProps) => { + return /* html */ ` +
+ ${SearchInput} + ${FilterOptions({ loading, params })} +
+ `; +}; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..199e6bbc --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,13 @@ +export const LIMIT_OPTIONS = [ + { value: 10, label: "10개" }, + { value: 20, label: "20개" }, + { value: 50, label: "50개" }, + { value: 100, label: "100개" }, +]; + +export const SORT_OPTIONS = [ + { value: "price_asc", label: "가격 낮은순" }, + { value: "price_desc", label: "가격 높은순" }, + { value: "name_asc", label: "이름순" }, + { value: "name_desc", label: "이름 역순" }, +]; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 4b055b89..00000000 --- a/src/main.js +++ /dev/null @@ -1,1152 +0,0 @@ -const enableMocking = () => - import("./mocks/browser.js").then(({ worker }) => - worker.start({ - onUnhandledRequest: "bypass", - }), - ); - -function main() { - const 상품목록_레이아웃_로딩 = ` -
-
-
-
-

- 쇼핑몰 -

-
- - -
-
-
-
-
- -
- -
-
- -
- - - -
-
-
- -
- -
-
- - -
- -
-
카테고리 로딩 중...
-
- -
- -
- -
- - -
- -
- - -
-
-
-
- -
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- - - - - 상품을 불러오는 중... -
-
-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; - - const 상품목록_레이아웃_로딩완료 = ` -
-
-
-
-

- 쇼핑몰 -

-
- - -
-
-
-
-
- -
- -
-
- -
- - - -
-
-
- -
- -
-
- - -
- -
- - -
- -
- -
- -
- - -
- -
- - -
-
-
-
- -
-
- -
- 총 340개의 상품 -
- -
-
- -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

-

- 220원 -

-
- - -
-
-
- -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

이지웨이건축자재

-

- 230원 -

-
- - -
-
-
- -
- 모든 상품을 확인했습니다 -
-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; - - const 상품목록_레이아웃_카테고리_1Depth = ` -
- -
- -
-
- -
- - - -
-
-
- - -
- - -
-
- - > -
-
-
- - - -
-
-
- - -
- -
- - -
- -
- - -
-
-
-
-
- `; - - const 상품목록_레이아웃_카테고리_2Depth = ` -
- -
- -
-
- -
- - - -
-
-
- - -
- - -
-
- - >>주방용품 -
-
-
- - - -
-
-
- - -
- -
- - -
- -
- - -
-
-
-
-
- `; - - const 토스트 = ` -
-
-
- - - -
-

장바구니에 추가되었습니다

- -
- -
-
- - - -
-

선택된 상품들이 삭제되었습니다

- -
- -
-
- - - -
-

오류가 발생했습니다.

- -
-
- `; - - const 장바구니_비어있음 = ` -
-
- -
-

- - - - 장바구니 -

- - -
- - -
- -
-
-
- - - -
-

장바구니가 비어있습니다

-

원하는 상품을 담아보세요!

-
-
-
-
-
- `; - - const 장바구니_선택없음 = ` -
-
- -
-

- - - - 장바구니 - (2) -

- -
- -
- -
- -
- -
-
-
- - - -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

- -
-
-
- - - -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

- 230원 -

- -
- - - -
-
- -
-

- 230원 -

- -
-
-
-
-
- -
- - -
- 총 금액 - 670원 -
- -
-
- - -
-
-
-
-
- `; - - const 장바구니_선택있음 = ` -
-
- -
-

- - - - 장바구니 - (2) -

- -
- -
- -
- -
- -
-
-
- - - -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -

-

- 220원 -

- -
- - - -
-
- -
-

- 440원 -

- -
-
-
- - - -
- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -
- -
-

- 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 -

-

- 230원 -

- -
- - - -
-
- -
-

- 230원 -

- -
-
-
-
-
- -
- -
- 선택한 상품 (1개) - 440원 -
- -
- 총 금액 - 670원 -
- -
- -
- - -
-
-
-
-
- `; - - const 상세페이지_로딩 = ` -
-
-
-
-
- -

상품 상세

-
-
- - -
-
-
-
-
-
-
-
-

상품 정보를 불러오는 중...

-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; - - const 상세페이지_로딩완료 = ` -
-
-
-
-
- -

상품 상세

-
-
- - -
-
-
-
-
- - - -
- -
-
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 -
- -
-

-

PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장

- -
-
- - - - - - - - - - - - - - - -
- 4.0 (749개 리뷰) -
- -
- 220원 -
- -
- 재고 107개 -
- -
- PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다. -
-
-
- -
-
- 수량 -
- - - -
-
- - -
-
- -
- -
- -
-
-

관련 상품

-

같은 카테고리의 다른 상품들

-
-
-
- - -
-
-
-
-
-
-

© 2025 항해플러스 프론트엔드 쇼핑몰

-
-
-
- `; - - const _404_ = ` -
-
- - - - - - - - - - - - - 404 - - - - - - - - - 페이지를 찾을 수 없습니다 - - - - - - 홈으로 -
-
- `; - - document.body.innerHTML = ` - ${상품목록_레이아웃_로딩} -
- ${상품목록_레이아웃_로딩완료} -
- ${상품목록_레이아웃_카테고리_1Depth} -
- ${상품목록_레이아웃_카테고리_2Depth} -
- ${토스트} -
- ${장바구니_비어있음} -
- ${장바구니_선택없음} -
- ${장바구니_선택있음} -
- ${상세페이지_로딩} -
- ${상세페이지_로딩완료} -
- ${_404_} - `; -} - -// 애플리케이션 시작 -if (import.meta.env.MODE !== "test") { - enableMocking().then(main); -} else { - main(); -} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..aaee91b3 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,18 @@ +import App from "./App"; + +const enableMocking = () => + import("./mocks/browser.js").then(({ worker }) => + worker.start({ + serviceWorker: { + url: `${import.meta.env.BASE_URL}mockServiceWorker.js`, + }, + onUnhandledRequest: "bypass", + }), + ); + +// 애플리케이션 시작 +if (import.meta.env.MODE !== "test") { + enableMocking().then(App); +} else { + App(); +} diff --git a/src/pages/AppContents.ts b/src/pages/AppContents.ts new file mode 100644 index 00000000..3100a086 --- /dev/null +++ b/src/pages/AppContents.ts @@ -0,0 +1,15 @@ +import { AppHeader, AppFooter } from "@/pages"; + +const AppContents = ({ children }) => { + return /* html */ ` +
+ ${AppHeader} +
+ ${children} +
+ ${AppFooter} +
+ `; +}; + +export { AppContents }; diff --git a/src/pages/AppFooter.ts b/src/pages/AppFooter.ts new file mode 100644 index 00000000..223f4f40 --- /dev/null +++ b/src/pages/AppFooter.ts @@ -0,0 +1,9 @@ +const AppFooter = /* html */ ` + +`; + +export { AppFooter }; diff --git a/src/pages/AppHeader.ts b/src/pages/AppHeader.ts new file mode 100644 index 00000000..2a286857 --- /dev/null +++ b/src/pages/AppHeader.ts @@ -0,0 +1,22 @@ +const AppHeader = /* html */ ` +
+
+
+

+ 쇼핑몰 +

+
+ + +
+
+
+
+ `; + +export { AppHeader }; diff --git a/src/pages/ProductListPage.ts b/src/pages/ProductListPage.ts new file mode 100644 index 00000000..ded75986 --- /dev/null +++ b/src/pages/ProductListPage.ts @@ -0,0 +1,17 @@ +import { Loading } from "@/components/Loading"; +import { ProductList } from "@/components/ProductList"; +import { SearchForm } from "@/components/SearchForm"; +import { ProductState } from "@/store/products-list/productStore"; + +function ProductListPage(state: ProductState) { + const { loading, products, params, pagination } = state; + + return /* html */ ` + ${SearchForm({ loading, params })} +
+ ${loading ? `${Loading}` : `${ProductList({ products, total: pagination?.total ?? 0 })}`} +
+`; +} + +export { ProductListPage }; diff --git a/src/pages/index.ts b/src/pages/index.ts new file mode 100644 index 00000000..6d3fde8c --- /dev/null +++ b/src/pages/index.ts @@ -0,0 +1,5 @@ +export { AppHeader } from "./AppHeader"; +export { AppFooter } from "./AppFooter"; +export { AppContents } from "./AppContents"; + +export { ProductListPage } from "./ProductListPage"; diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 00000000..e22014e4 --- /dev/null +++ b/src/router.ts @@ -0,0 +1,49 @@ +/** + * 해시 기반 라우터를 생성하는 함수 + * + * @returns {object} router - 라우터 객체로, `addRoute()`와 `start()` 메서드를 제공한다. + * + * @see {@link https://tech.kakaoent.com/front-end/2022/221124-router-without-library/ 라이브러리 없이 라우터(Router) 만들기 } + * + */ +export default function createRouter() { + const routes = []; // 라우트 목록을 담을 배열 + + const router = { + /** + * 라우터 배열에 URL과 컴포넌트를 매핑하여 저장한다. + * + * @method addRoute + * @param {string} fragment URL 해시 (예: "/#/home") + * @param {Function} component 해당 경로에서 실행할 함수 + */ + addRoute(fragment, component) { + routes.push({ fragment, component }); + return this; + }, + /** + * URL 해시 변경을 감지하여 해당 경로의 컴포넌트를 실행한다. + * + * @method start + * @fires window#hashchange + * @description + * 1. `checkRoutes` 함수에서 현재 URL 해시(`window.location.hash`)를 읽고, `routes` 배열에서 일치하는 항목을 찾는다. + * 2. 해당 라우트의 `component` 함수를 실행하여 화면을 렌더링한다. + * 3. `window` 객체에 `hashchange` 이벤트 리스너를 등록하여 사용자가 해시를 변경할 때마다 자동으로 `checkRoutes()`가 호출된다. + * 4. 초기 로드 시에도 한 번 실행하여 첫 화면을 표시한다. + */ + start() { + const checkRoutes = () => { + const currentRoute = routes.find((route) => route.fragment === window.location.hash) ?? routes[0]; + + if (!currentRoute) return; + currentRoute.component(); + }; + + window.addEventListener("hashchange", checkRoutes); + checkRoutes(); + }, + }; + + return router; +} diff --git a/src/store/products-list/fetchProducts.ts b/src/store/products-list/fetchProducts.ts new file mode 100644 index 00000000..d28ec1c9 --- /dev/null +++ b/src/store/products-list/fetchProducts.ts @@ -0,0 +1,45 @@ +import { productStore } from "./productStore"; +import { getProducts } from "@/api/productApi"; +import type { GetProductsParams } from "@/types"; + +// ⚡ 상품 목록을 불러오고, 그 결과를 store에 반영하는 함수 +export async function fetchProducts(paramsOverride?: Partial) { + // 1) 이전 상태 가져오기 + const prev = productStore.getState(); + + // 2) 기존 params + 새로 들어온 params 덮어쓰기 + const params: GetProductsParams = { + ...prev.params, + ...paramsOverride, + }; + + // 3) 로딩 시작 상태로 변경 + productStore.setState({ + loading: true, + error: null, + params, + }); + + try { + // 4) 실제 API 호출 + const res = await getProducts(params); + + // 5) 성공 시 상품 목록 + 페이지네이션/필터 반영 + productStore.setState({ + loading: false, + products: res.products, + pagination: res.pagination, + params: { + ...res.filters, // search, category, sort + limit: res.pagination.limit, + page: res.pagination.page, + }, + }); + } catch { + // 6) 실패 시 에러 상태 + productStore.setState({ + loading: false, + error: "상품을 불러오지 못했어요. 잠시 후 다시 시도해 주세요.", + }); + } +} diff --git a/src/store/products-list/productStore.ts b/src/store/products-list/productStore.ts new file mode 100644 index 00000000..72e9a950 --- /dev/null +++ b/src/store/products-list/productStore.ts @@ -0,0 +1,60 @@ +import { GetProductsParams, ProductItem, Pagination } from "@/types"; + +export type ProductState = { + loading: boolean; + products: ProductItem[]; + error: string | null; + params: GetProductsParams; + pagination: Pagination | null; +}; + +function createProductStore(initialState: ProductState) { + let state = initialState; + const observers = []; + + /** + * 현재 상태(state)를 가져오는 함수 + */ + const getState = () => state; + + /** + * 상태(state)를 업데이트 하는 함수 + */ + const setState = (partial: Partial) => { + state = { ...state, ...partial }; + + // 모든 옵저버 함수 실행 => 화면 다시 렌더링 + observers.forEach((observer) => observer(state)); + }; + + /** + * 상태 변화를 구독(subscribe)하는 함수 + */ + const subscribe = (observer: (state: ProductState) => void) => { + // 1) 구독자 목록에 observer 추가 + observers.push(observer); + // 2) 구독하자마자 현재 상태를 한번 실행 + observer(state); + // 3) unsubscribe 기능 반환 => 구독 해제 + return () => { + const index = observers.indexOf(observer); + if (index > -1) observers.splice(index, 1); + }; + }; + + return { getState, setState, subscribe }; +} + +export const productStore = createProductStore({ + loading: false, + products: [], + error: null, + params: { + limit: 20, + search: "", + category1: "", + category2: "", + sort: "price_asc", + }, + pagination: null, +}); diff --git a/src/template.ts b/src/template.ts new file mode 100644 index 00000000..19404654 --- /dev/null +++ b/src/template.ts @@ -0,0 +1,1112 @@ +const 상품목록_레이아웃_로딩 = ` +
+
+
+
+

+ 쇼핑몰 +

+
+ + +
+
+
+
+
+ +
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+
+ + +
+ +
+
카테고리 로딩 중...
+
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + + 상품을 불러오는 중... +
+
+
+
+
+
+
+

© 2025 항해플러스 프론트엔드 쇼핑몰

+
+
+
+`; + +const 상품목록_레이아웃_로딩완료 = ` +
+
+
+
+

+ 쇼핑몰 +

+
+ + +
+
+
+
+
+ +
+ +
+
+ +
+ + + +
+
+
+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ +
+ 총 340개의 상품 +
+ +
+
+ +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+

+ 220원 +

+
+ + +
+
+
+ +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

이지웨이건축자재

+

+ 230원 +

+
+ + +
+
+
+ +
+ 모든 상품을 확인했습니다 +
+
+
+
+
+
+

© 2025 항해플러스 프론트엔드 쇼핑몰

+
+
+
+`; + +const 상품목록_레이아웃_카테고리_1Depth = ` +
+ +
+ +
+
+ +
+ + + +
+
+
+ + +
+ + +
+
+ + > +
+
+
+ + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+`; + +const 상품목록_레이아웃_카테고리_2Depth = ` +
+ +
+ +
+
+ +
+ + + +
+
+
+ + +
+ + +
+
+ + >>주방용품 +
+
+
+ + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+`; + +const 토스트 = ` +
+
+
+ + + +
+

장바구니에 추가되었습니다

+ +
+ +
+
+ + + +
+

선택된 상품들이 삭제되었습니다

+ +
+ +
+
+ + + +
+

오류가 발생했습니다.

+ +
+
+`; + +const 장바구니_비어있음 = ` +
+
+ +
+

+ + + + 장바구니 +

+ + +
+ + +
+ +
+
+
+ + + +
+

장바구니가 비어있습니다

+

원하는 상품을 담아보세요!

+
+
+
+
+
+`; + +const 장바구니_선택없음 = ` +
+
+ +
+

+ + + + 장바구니 + (2) +

+ +
+ +
+ +
+ +
+ +
+
+
+ + + +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+ 220원 +

+ +
+ + + +
+
+ +
+

+ 440원 +

+ +
+
+
+ + + +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

+ 230원 +

+ +
+ + + +
+
+ +
+

+ 230원 +

+ +
+
+
+
+
+ +
+ + +
+ 총 금액 + 670원 +
+ +
+
+ + +
+
+
+
+
+`; + +const 장바구니_선택있음 = ` +
+
+ +
+

+ + + + 장바구니 + (2) +

+ +
+ +
+ +
+ +
+ +
+
+
+ + + +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +

+

+ 220원 +

+ +
+ + + +
+
+ +
+

+ 440원 +

+ +
+
+
+ + + +
+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +
+ +
+

+ 샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이 +

+

+ 230원 +

+ +
+ + + +
+
+ +
+

+ 230원 +

+ +
+
+
+
+
+ +
+ +
+ 선택한 상품 (1개) + 440원 +
+ +
+ 총 금액 + 670원 +
+ +
+ +
+ + +
+
+
+
+
+`; + +const 상세페이지_로딩 = ` +
+
+
+
+
+ +

상품 상세

+
+
+ + +
+
+
+
+
+
+
+
+

상품 정보를 불러오는 중...

+
+
+
+
+
+

© 2025 항해플러스 프론트엔드 쇼핑몰

+
+
+
+`; + +const 상세페이지_로딩완료 = ` +
+
+
+
+
+ +

상품 상세

+
+
+ + +
+
+
+
+
+ + + +
+ +
+
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장 +
+ +
+

+

PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장

+ +
+
+ + + + + + + + + + + + + + + +
+ 4.0 (749개 리뷰) +
+ +
+ 220원 +
+ +
+ 재고 107개 +
+ +
+ PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장에 대한 상세 설명입니다. 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다. +
+
+
+ +
+
+ 수량 +
+ + + +
+
+ + +
+
+ +
+ +
+ +
+
+

관련 상품

+

같은 카테고리의 다른 상품들

+
+
+
+ + +
+
+
+
+
+
+

© 2025 항해플러스 프론트엔드 쇼핑몰

+
+
+
+`; + +const _404_ = ` +
+
+ + + + + + + + + + + + + 404 + + + + + + + + + 페이지를 찾을 수 없습니다 + + + + + + 홈으로 +
+
+`; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..3604a7bf --- /dev/null +++ b/src/types.ts @@ -0,0 +1,46 @@ +export type SortOptions = "price_asc" | "price_desc" | "name_asc" | "name_desc"; + +export type Pagination = { + page: number; + limit: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +}; + +export type Filters = { + search: string; + category1: string; + category2: string; + sort: SortOptions; +}; + +export interface GetProductsParams extends Partial { + limit?: number; + current?: number; + page?: number; +} + +export interface GetProductsResponse { + products: ProductItem[]; + pagination: Pagination; + filters: Filters; +} + +export interface ProductItem { + title: string; + link: string; + image: string; + lprice: string; + hprice: string; + mallName: string; + productId: string; + productType: string; + brand: string; + maker: string; + category1: string; + category2: string; + category3: string; + category4: string; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a92df5b0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "ESNext", + "types": ["vite/client"], + "moduleResolution": "bundler", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..3c057c99 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + base: "/front_7th_chapter2-1/", + build: { + outDir: "dist", + }, + plugins: [tsconfigPaths()], +});