From 9f17543ffc5b3fd36434641a76d7dbabe927d1a5 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Tue, 29 Jul 2025 01:05:00 +0900
Subject: [PATCH 01/46] =?UTF-8?q?feat:=20eslint=20=EB=B0=8F=20prettier=20?=
=?UTF-8?q?=EC=84=A4=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.prettierrc | 11 +
eslint.config.js | 32 +
package.json | 6 +
pnpm-lock.yaml | 1772 +++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 1819 insertions(+), 2 deletions(-)
create mode 100644 .prettierrc
create mode 100644 eslint.config.js
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..d4c0dd53d
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,11 @@
+{
+ "semi": true,
+ "trailingComma": "all",
+ "singleQuote": true,
+ "tabWidth": 2,
+ "printWidth": 100,
+ "bracketSpacing": true,
+ "arrowParens": "always",
+ "useTabs": false,
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 000000000..59db8f77f
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,32 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import pluginReact from 'eslint-plugin-react';
+import { defineConfig } from 'eslint/config';
+
+export default defineConfig([
+ {
+ files: ['**/*.{js,mjs,cjs,jsx}'],
+ plugins: { js },
+ extends: ['js/recommended'],
+ languageOptions: { globals: globals.browser },
+ rules: {
+ // 기본 규칙들
+ 'no-console': 'warn',
+ 'no-unused-vars': 'error',
+ 'no-var': 'error',
+ 'no-debugger': 'error',
+ 'no-unused-expressions': 'error',
+ 'no-duplicate-imports': 'error',
+ 'no-multiple-empty-lines': 'error',
+ 'no-else-return': 'error',
+ 'no-param-reassign': 'error',
+
+ // 최신 문법 권장 규칙들
+ 'prefer-const': 'error', // 재할당하지 않는 변수 const 사용
+ 'object-shorthand': 'error', // 객체 리터럴 속성 단축 구문 사용
+ 'prefer-template': 'error', // 템플릿 리터럴 사용
+ 'prefer-destructuring': 'error', // 구조 분해 할당 사용
+ },
+ },
+ pluginReact.configs.flat.recommended,
+]);
\ No newline at end of file
diff --git a/package.json b/package.json
index 121aab60d..32d86ebb2 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
+ "lint:basic": "pnpm eslint src/basic/**",
+ "lint:advanced": "pnpm eslint src/advanced/**",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
@@ -15,9 +17,13 @@
"test:ui": "vitest --ui"
},
"devDependencies": {
+ "@eslint/js": "^9.32.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/user-event": "^14.6.1",
"@vitest/ui": "^3.2.4",
+ "eslint": "^9.32.0",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^16.3.0",
"jsdom": "^26.1.0",
"vite": "^7.0.5",
"vitest": "^3.2.4"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 49a2140ea..c24f43d65 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
devDependencies:
+ '@eslint/js':
+ specifier: ^9.32.0
+ version: 9.32.0
'@testing-library/jest-dom':
specifier: ^6.6.3
version: 6.6.3
@@ -17,6 +20,15 @@ importers:
'@vitest/ui':
specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4)
+ eslint:
+ specifier: ^9.32.0
+ version: 9.32.0
+ eslint-plugin-react:
+ specifier: ^7.37.5
+ version: 7.37.5(eslint@9.32.0)
+ globals:
+ specifier: ^16.3.0
+ version: 16.3.0
jsdom:
specifier: ^26.1.0
version: 26.1.0
@@ -231,6 +243,64 @@ packages:
cpu: [x64]
os: [win32]
+ '@eslint-community/eslint-utils@4.7.0':
+ resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+ 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}
+
+ '@eslint/config-array@0.21.0':
+ resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/config-helpers@0.3.0':
+ resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/core@0.15.1':
+ resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/eslintrc@3.3.1':
+ resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/js@9.32.0':
+ resolution: {integrity: sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/object-schema@2.1.6':
+ resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@eslint/plugin-kit@0.3.4':
+ resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@humanfs/core@0.19.1':
+ resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanfs/node@0.16.6':
+ resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+ engines: {node: '>=18.18.0'}
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/retry@0.3.1':
+ resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+ engines: {node: '>=18.18'}
+
+ '@humanwhocodes/retry@0.4.3':
+ resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+ engines: {node: '>=18.18'}
+
'@jridgewell/sourcemap-codec@1.5.4':
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
@@ -363,6 +433,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
@@ -397,10 +470,23 @@ packages:
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -413,6 +499,9 @@ packages:
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
engines: {node: '>=10'}
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -420,14 +509,72 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
+ array-buffer-byte-length@1.0.2:
+ resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+ engines: {node: '>= 0.4'}
+
+ array-includes@3.1.9:
+ resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.findlast@1.2.5:
+ resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flat@1.3.3:
+ resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.flatmap@1.3.3:
+ resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+ engines: {node: '>= 0.4'}
+
+ array.prototype.tosorted@1.1.4:
+ resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
+ engines: {node: '>= 0.4'}
+
+ arraybuffer.prototype.slice@1.0.4:
+ resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+ engines: {node: '>= 0.4'}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ async-function@1.0.0:
+ resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+ engines: {node: '>= 0.4'}
+
+ available-typed-arrays@1.0.7:
+ resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+ engines: {node: '>= 0.4'}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bind@1.0.8:
+ resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
chai@5.2.1:
resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==}
engines: {node: '>=18'}
@@ -451,6 +598,13 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
@@ -462,6 +616,18 @@ packages:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
+ data-view-buffer@1.0.2:
+ resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-length@1.0.2:
+ resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+ engines: {node: '>= 0.4'}
+
+ data-view-byte-offset@1.0.1:
+ resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+ engines: {node: '>= 0.4'}
+
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'}
@@ -478,35 +644,147 @@ packages:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ define-properties@1.2.1:
+ resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+ engines: {node: '>= 0.4'}
+
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
+ doctrine@2.1.0:
+ resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+ engines: {node: '>=0.10.0'}
+
dom-accessibility-api@0.5.16:
resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
+ es-abstract@1.24.0:
+ resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
+ engines: {node: '>= 0.4'}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-iterator-helpers@1.2.1:
+ resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
+ engines: {node: '>= 0.4'}
+
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+ es-object-atoms@1.1.1:
+ 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'}
+
+ es-shim-unscopables@1.1.0:
+ resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+ engines: {node: '>= 0.4'}
+
+ es-to-primitive@1.3.0:
+ resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.25.8:
resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
engines: {node: '>=18'}
hasBin: true
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ eslint-plugin-react@7.37.5:
+ resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
+
+ eslint-scope@8.4.0:
+ resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.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.32.0:
+ resolution: {integrity: sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ hasBin: true
+ peerDependencies:
+ jiti: '*'
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+
+ espree@10.4.0:
+ resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
expect-type@1.2.2:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'}
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
@@ -518,18 +796,99 @@ packages:
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+ file-entry-cache@8.0.0:
+ resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+ engines: {node: '>=16.0.0'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ flat-cache@4.0.1:
+ resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+ engines: {node: '>=16'}
+
flatted@3.3.3:
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+ for-each@0.3.5:
+ resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+ engines: {node: '>= 0.4'}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ function.prototype.name@1.1.8:
+ resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+ engines: {node: '>= 0.4'}
+
+ functions-have-names@1.2.3:
+ resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-symbol-description@1.1.0:
+ resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+ engines: {node: '>= 0.4'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ globals@14.0.0:
+ resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+ engines: {node: '>=18'}
+
+ globals@16.3.0:
+ resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==}
+ engines: {node: '>=18'}
+
+ globalthis@1.0.4:
+ resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+ engines: {node: '>= 0.4'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ has-bigints@1.1.0:
+ resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+ engines: {node: '>= 0.4'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.2.0:
+ resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.1.0:
+ 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'}
+
html-encoding-sniffer@4.0.0:
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
engines: {node: '>=18'}
@@ -546,19 +905,145 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-fresh@3.3.1:
+ resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+ engines: {node: '>=6'}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
+ internal-slot@1.1.0:
+ resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+ engines: {node: '>= 0.4'}
+
+ is-array-buffer@3.0.5:
+ resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+ engines: {node: '>= 0.4'}
+
+ is-async-function@2.1.1:
+ resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+ engines: {node: '>= 0.4'}
+
+ is-bigint@1.1.0:
+ resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+ engines: {node: '>= 0.4'}
+
+ is-boolean-object@1.2.2:
+ resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+ engines: {node: '>= 0.4'}
+
+ is-callable@1.2.7:
+ resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-data-view@1.0.2:
+ resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+ engines: {node: '>= 0.4'}
+
+ is-date-object@1.1.0:
+ resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-finalizationregistry@1.1.1:
+ resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+ engines: {node: '>= 0.4'}
+
+ is-generator-function@1.1.0:
+ resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
+ engines: {node: '>= 0.4'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-map@2.0.3:
+ resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+ engines: {node: '>= 0.4'}
+
+ is-negative-zero@2.0.3:
+ resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+ engines: {node: '>= 0.4'}
+
+ is-number-object@1.1.1:
+ resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+ engines: {node: '>= 0.4'}
+
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+ is-regex@1.2.1:
+ resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+ engines: {node: '>= 0.4'}
+
+ is-set@2.0.3:
+ resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+ engines: {node: '>= 0.4'}
+
+ is-shared-array-buffer@1.0.4:
+ resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+ engines: {node: '>= 0.4'}
+
+ is-string@1.1.1:
+ resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+ engines: {node: '>= 0.4'}
+
+ is-symbol@1.1.1:
+ resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+ engines: {node: '>= 0.4'}
+
+ is-typed-array@1.1.15:
+ resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+ engines: {node: '>= 0.4'}
+
+ is-weakmap@2.0.2:
+ resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+ engines: {node: '>= 0.4'}
+
+ is-weakref@1.1.1:
+ resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+ engines: {node: '>= 0.4'}
+
+ is-weakset@2.0.4:
+ resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+ engines: {node: '>= 0.4'}
+
+ isarray@2.0.5:
+ resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ iterator.prototype@1.1.5:
+ resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
+ engines: {node: '>= 0.4'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'}
@@ -568,9 +1053,40 @@ packages:
canvas:
optional: true
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ jsx-ast-utils@3.3.5:
+ resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
+ engines: {node: '>=4.0'}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+
loupe@3.2.0:
resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==}
@@ -584,10 +1100,17 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -600,12 +1123,74 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
nwsapi@2.2.20:
resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==}
+ object-assign@4.1.1:
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ object-keys@1.1.1:
+ resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+ engines: {node: '>= 0.4'}
+
+ object.assign@4.1.7:
+ resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+ engines: {node: '>= 0.4'}
+
+ object.entries@1.1.9:
+ resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
+ engines: {node: '>= 0.4'}
+
+ object.fromentries@2.0.8:
+ resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+ engines: {node: '>= 0.4'}
+
+ object.values@1.2.1:
+ resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+ engines: {node: '>= 0.4'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ own-keys@1.0.1:
+ resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+ engines: {node: '>= 0.4'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
parse5@7.3.0:
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -620,18 +1205,32 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
+ possible-typed-array-names@1.1.0:
+ resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+ engines: {node: '>= 0.4'}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
+ prop-types@15.8.1:
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
+ react-is@16.13.1:
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
@@ -639,6 +1238,22 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'}
+ reflect.getprototypeof@1.0.10:
+ resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+ engines: {node: '>= 0.4'}
+
+ regexp.prototype.flags@1.5.4:
+ resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+ engines: {node: '>= 0.4'}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve@2.0.0-next.5:
+ resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
+ hasBin: true
+
rollup@4.45.1:
resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -647,6 +1262,18 @@ packages:
rrweb-cssom@0.8.0:
resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+ safe-array-concat@1.1.3:
+ resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
+ engines: {node: '>=0.4'}
+
+ safe-push-apply@1.0.0:
+ resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+ engines: {node: '>= 0.4'}
+
+ safe-regex-test@1.1.0:
+ resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+ engines: {node: '>= 0.4'}
+
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
@@ -654,6 +1281,46 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ set-function-name@2.0.2:
+ resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+ engines: {node: '>= 0.4'}
+
+ set-proto@1.0.0:
+ resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+ engines: {node: '>= 0.4'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -671,10 +1338,37 @@ packages:
std-env@3.9.0:
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
+ stop-iteration-iterator@1.1.0:
+ resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.matchall@4.0.12:
+ resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.repeat@1.0.0:
+ resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
+
+ string.prototype.trim@1.2.10:
+ resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimend@1.0.9:
+ resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+ engines: {node: '>= 0.4'}
+
+ string.prototype.trimstart@1.0.8:
+ resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+ engines: {node: '>= 0.4'}
+
strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'}
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
@@ -682,6 +1376,10 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
@@ -726,6 +1424,33 @@ packages:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ typed-array-buffer@1.0.3:
+ resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-length@1.0.3:
+ resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-byte-offset@1.0.4:
+ resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+ engines: {node: '>= 0.4'}
+
+ typed-array-length@1.0.7:
+ resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+ engines: {node: '>= 0.4'}
+
+ unbox-primitive@1.1.0:
+ resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+ engines: {node: '>= 0.4'}
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -819,11 +1544,36 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
+ which-boxed-primitive@1.1.1:
+ resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+ engines: {node: '>= 0.4'}
+
+ which-builtin-type@1.2.1:
+ resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+ engines: {node: '>= 0.4'}
+
+ which-collection@1.0.2:
+ resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+ engines: {node: '>= 0.4'}
+
+ which-typed-array@1.1.19:
+ resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+ engines: {node: '>= 0.4'}
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
@@ -843,6 +1593,10 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
snapshots:
'@adobe/css-tools@4.4.3': {}
@@ -963,6 +1717,63 @@ snapshots:
'@esbuild/win32-x64@0.25.8':
optional: true
+ '@eslint-community/eslint-utils@4.7.0(eslint@9.32.0)':
+ dependencies:
+ eslint: 9.32.0
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/config-array@0.21.0':
+ dependencies:
+ '@eslint/object-schema': 2.1.6
+ debug: 4.4.1
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/config-helpers@0.3.0': {}
+
+ '@eslint/core@0.15.1':
+ dependencies:
+ '@types/json-schema': 7.0.15
+
+ '@eslint/eslintrc@3.3.1':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.4.1
+ espree: 10.4.0
+ globals: 14.0.0
+ ignore: 5.3.2
+ import-fresh: 3.3.1
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@9.32.0': {}
+
+ '@eslint/object-schema@2.1.6': {}
+
+ '@eslint/plugin-kit@0.3.4':
+ dependencies:
+ '@eslint/core': 0.15.1
+ levn: 0.4.1
+
+ '@humanfs/core@0.19.1': {}
+
+ '@humanfs/node@0.16.6':
+ dependencies:
+ '@humanfs/core': 0.19.1
+ '@humanwhocodes/retry': 0.3.1
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/retry@0.3.1': {}
+
+ '@humanwhocodes/retry@0.4.3': {}
+
'@jridgewell/sourcemap-codec@1.5.4': {}
'@polka/url@1.0.0-next.29': {}
@@ -1062,6 +1873,8 @@ snapshots:
'@types/estree@1.0.8': {}
+ '@types/json-schema@7.0.15': {}
+
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.2
@@ -1115,8 +1928,21 @@ snapshots:
loupe: 3.2.0
tinyrainbow: 2.0.0
+ acorn-jsx@5.3.2(acorn@8.15.0):
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
agent-base@7.1.4: {}
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
@@ -1125,15 +1951,106 @@ snapshots:
ansi-styles@5.2.0: {}
+ argparse@2.0.1: {}
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
aria-query@5.3.2: {}
- assertion-error@2.0.1: {}
+ array-buffer-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ is-array-buffer: 3.0.5
+
+ array-includes@3.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ is-string: 1.1.1
+ math-intrinsics: 1.1.0
+
+ array.prototype.findlast@1.2.5:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flat@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.flatmap@1.3.3:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-shim-unscopables: 1.1.0
+
+ array.prototype.tosorted@1.1.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-shim-unscopables: 1.1.0
+
+ arraybuffer.prototype.slice@1.0.4:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ is-array-buffer: 3.0.5
+
+ assertion-error@2.0.1: {}
+
+ async-function@1.0.0: {}
+
+ available-typed-arrays@1.0.7:
+ dependencies:
+ possible-typed-array-names: 1.1.0
+
+ balanced-match@1.0.2: {}
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ cac@6.7.14: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bind@1.0.8:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ get-intrinsic: 1.3.0
+ set-function-length: 1.2.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
- cac@6.7.14: {}
+ callsites@3.1.0: {}
chai@5.2.1:
dependencies:
@@ -1161,6 +2078,14 @@ snapshots:
color-name@1.1.4: {}
+ concat-map@0.0.1: {}
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
css.escape@1.5.1: {}
cssstyle@4.6.0:
@@ -1173,6 +2098,24 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
+ data-view-buffer@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-length@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
+ data-view-byte-offset@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-data-view: 1.0.2
+
debug@4.4.1:
dependencies:
ms: 2.1.3
@@ -1181,16 +2124,141 @@ snapshots:
deep-eql@5.0.2: {}
+ deep-is@0.1.4: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ define-properties@1.2.1:
+ dependencies:
+ define-data-property: 1.1.4
+ has-property-descriptors: 1.0.2
+ object-keys: 1.1.1
+
dequal@2.0.3: {}
+ doctrine@2.1.0:
+ dependencies:
+ esutils: 2.0.3
+
dom-accessibility-api@0.5.16: {}
dom-accessibility-api@0.6.3: {}
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
entities@6.0.1: {}
+ es-abstract@1.24.0:
+ dependencies:
+ array-buffer-byte-length: 1.0.2
+ arraybuffer.prototype.slice: 1.0.4
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ data-view-buffer: 1.0.2
+ data-view-byte-length: 1.0.2
+ data-view-byte-offset: 1.0.1
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ es-set-tostringtag: 2.1.0
+ es-to-primitive: 1.3.0
+ function.prototype.name: 1.1.8
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ get-symbol-description: 1.1.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ internal-slot: 1.1.0
+ is-array-buffer: 3.0.5
+ is-callable: 1.2.7
+ is-data-view: 1.0.2
+ is-negative-zero: 2.0.3
+ is-regex: 1.2.1
+ is-set: 2.0.3
+ is-shared-array-buffer: 1.0.4
+ is-string: 1.1.1
+ is-typed-array: 1.1.15
+ is-weakref: 1.1.1
+ math-intrinsics: 1.1.0
+ object-inspect: 1.13.4
+ object-keys: 1.1.1
+ object.assign: 4.1.7
+ own-keys: 1.0.1
+ regexp.prototype.flags: 1.5.4
+ safe-array-concat: 1.1.3
+ safe-push-apply: 1.0.0
+ safe-regex-test: 1.1.0
+ set-proto: 1.0.0
+ stop-iteration-iterator: 1.1.0
+ string.prototype.trim: 1.2.10
+ string.prototype.trimend: 1.0.9
+ string.prototype.trimstart: 1.0.8
+ typed-array-buffer: 1.0.3
+ typed-array-byte-length: 1.0.3
+ typed-array-byte-offset: 1.0.4
+ typed-array-length: 1.0.7
+ unbox-primitive: 1.1.0
+ which-typed-array: 1.1.19
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-iterator-helpers@1.2.1:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-set-tostringtag: 2.1.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ globalthis: 1.0.4
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+ has-proto: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ iterator.prototype: 1.1.5
+ safe-array-concat: 1.1.3
+
es-module-lexer@1.7.0: {}
+ es-object-atoms@1.1.1:
+ 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
+
+ es-shim-unscopables@1.1.0:
+ dependencies:
+ hasown: 2.0.2
+
+ es-to-primitive@1.3.0:
+ dependencies:
+ is-callable: 1.2.7
+ is-date-object: 1.1.0
+ is-symbol: 1.1.1
+
esbuild@0.25.8:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.8
@@ -1220,25 +2288,212 @@ snapshots:
'@esbuild/win32-ia32': 0.25.8
'@esbuild/win32-x64': 0.25.8
+ escape-string-regexp@4.0.0: {}
+
+ eslint-plugin-react@7.37.5(eslint@9.32.0):
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.findlast: 1.2.5
+ array.prototype.flatmap: 1.3.3
+ array.prototype.tosorted: 1.1.4
+ doctrine: 2.1.0
+ es-iterator-helpers: 1.2.1
+ eslint: 9.32.0
+ estraverse: 5.3.0
+ hasown: 2.0.2
+ jsx-ast-utils: 3.3.5
+ minimatch: 3.1.2
+ object.entries: 1.1.9
+ object.fromentries: 2.0.8
+ object.values: 1.2.1
+ prop-types: 15.8.1
+ resolve: 2.0.0-next.5
+ semver: 6.3.1
+ string.prototype.matchall: 4.0.12
+ string.prototype.repeat: 1.0.0
+
+ eslint-scope@8.4.0:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.1: {}
+
+ eslint@9.32.0:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0)
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/config-array': 0.21.0
+ '@eslint/config-helpers': 0.3.0
+ '@eslint/core': 0.15.1
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.32.0
+ '@eslint/plugin-kit': 0.3.4
+ '@humanfs/node': 0.16.6
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ '@types/json-schema': 7.0.15
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.1
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ transitivePeerDependencies:
+ - supports-color
+
+ espree@10.4.0:
+ dependencies:
+ acorn: 8.15.0
+ acorn-jsx: 5.3.2(acorn@8.15.0)
+ eslint-visitor-keys: 4.2.1
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
+ esutils@2.0.3: {}
+
expect-type@1.2.2: {}
+ fast-deep-equal@3.1.3: {}
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
fdir@6.4.6(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fflate@0.8.2: {}
+ file-entry-cache@8.0.0:
+ dependencies:
+ flat-cache: 4.0.1
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ flat-cache@4.0.1:
+ dependencies:
+ flatted: 3.3.3
+ keyv: 4.5.4
+
flatted@3.3.3: {}
+ for-each@0.3.5:
+ dependencies:
+ is-callable: 1.2.7
+
fsevents@2.3.3:
optional: true
+ function-bind@1.1.2: {}
+
+ function.prototype.name@1.1.8:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ functions-have-names: 1.2.3
+ hasown: 2.0.2
+ is-callable: 1.2.7
+
+ functions-have-names@1.2.3: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-symbol-description@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globals@14.0.0: {}
+
+ globals@16.3.0: {}
+
+ globalthis@1.0.4:
+ dependencies:
+ define-properties: 1.2.1
+ gopd: 1.2.0
+
+ gopd@1.2.0: {}
+
+ has-bigints@1.1.0: {}
+
has-flag@4.0.0: {}
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.1
+
+ has-proto@1.2.0:
+ dependencies:
+ dunder-proto: 1.0.1
+
+ 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
+
html-encoding-sniffer@4.0.0:
dependencies:
whatwg-encoding: 3.1.1
@@ -1261,14 +2516,151 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ ignore@5.3.2: {}
+
+ import-fresh@3.3.1:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ imurmurhash@0.1.4: {}
+
indent-string@4.0.0: {}
+ internal-slot@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ hasown: 2.0.2
+ side-channel: 1.1.0
+
+ is-array-buffer@3.0.5:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ is-async-function@2.1.1:
+ dependencies:
+ async-function: 1.0.0
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-bigint@1.1.0:
+ dependencies:
+ has-bigints: 1.1.0
+
+ is-boolean-object@1.2.2:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-callable@1.2.7: {}
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-data-view@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ is-typed-array: 1.1.15
+
+ is-date-object@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-extglob@2.1.1: {}
+
+ is-finalizationregistry@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-generator-function@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ get-proto: 1.0.1
+ has-tostringtag: 1.0.2
+ safe-regex-test: 1.1.0
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-map@2.0.3: {}
+
+ is-negative-zero@2.0.3: {}
+
+ is-number-object@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
is-potential-custom-element-name@1.0.1: {}
+ is-regex@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ is-set@2.0.3: {}
+
+ is-shared-array-buffer@1.0.4:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-string@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-tostringtag: 1.0.2
+
+ is-symbol@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+ has-symbols: 1.1.0
+ safe-regex-test: 1.1.0
+
+ is-typed-array@1.1.15:
+ dependencies:
+ which-typed-array: 1.1.19
+
+ is-weakmap@2.0.2: {}
+
+ is-weakref@1.1.1:
+ dependencies:
+ call-bound: 1.0.4
+
+ is-weakset@2.0.4:
+ dependencies:
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+
+ isarray@2.0.5: {}
+
+ isexe@2.0.0: {}
+
+ iterator.prototype@1.1.5:
+ dependencies:
+ define-data-property: 1.1.4
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ has-symbols: 1.1.0
+ set-function-name: 2.0.2
+
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
jsdom@26.1.0:
dependencies:
cssstyle: 4.6.0
@@ -1296,8 +2688,40 @@ snapshots:
- supports-color
- utf-8-validate
+ json-buffer@3.0.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ jsx-ast-utils@3.3.5:
+ dependencies:
+ array-includes: 3.1.9
+ array.prototype.flat: 1.3.3
+ object.assign: 4.1.7
+ object.values: 1.2.1
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ lodash.merge@4.6.2: {}
+
lodash@4.17.21: {}
+ loose-envify@1.4.0:
+ dependencies:
+ js-tokens: 4.0.0
+
loupe@3.2.0: {}
lru-cache@10.4.3: {}
@@ -1308,20 +2732,97 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.4
+ math-intrinsics@1.1.0: {}
+
min-indent@1.0.1: {}
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
mrmime@2.0.1: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
+ natural-compare@1.4.0: {}
+
nwsapi@2.2.20: {}
+ object-assign@4.1.1: {}
+
+ object-inspect@1.13.4: {}
+
+ object-keys@1.1.1: {}
+
+ object.assign@4.1.7:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+ has-symbols: 1.1.0
+ object-keys: 1.1.1
+
+ object.entries@1.1.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ object.fromentries@2.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+
+ object.values@1.2.1:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ own-keys@1.0.1:
+ dependencies:
+ get-intrinsic: 1.3.0
+ object-keys: 1.1.1
+ safe-push-apply: 1.0.0
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
parse5@7.3.0:
dependencies:
entities: 6.0.1
+ path-exists@4.0.0: {}
+
+ path-key@3.1.1: {}
+
+ path-parse@1.0.7: {}
+
pathe@2.0.3: {}
pathval@2.0.1: {}
@@ -1330,20 +2831,32 @@ snapshots:
picomatch@4.0.3: {}
+ possible-typed-array-names@1.1.0: {}
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
+ prelude-ls@1.2.1: {}
+
pretty-format@27.5.1:
dependencies:
ansi-regex: 5.0.1
ansi-styles: 5.2.0
react-is: 17.0.2
+ prop-types@15.8.1:
+ dependencies:
+ loose-envify: 1.4.0
+ object-assign: 4.1.1
+ react-is: 16.13.1
+
punycode@2.3.1: {}
+ react-is@16.13.1: {}
+
react-is@17.0.2: {}
redent@3.0.0:
@@ -1351,6 +2864,34 @@ snapshots:
indent-string: 4.0.0
strip-indent: 3.0.0
+ reflect.getprototypeof@1.0.10:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ get-proto: 1.0.1
+ which-builtin-type: 1.2.1
+
+ regexp.prototype.flags@1.5.4:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-errors: 1.3.0
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ set-function-name: 2.0.2
+
+ resolve-from@4.0.0: {}
+
+ resolve@2.0.0-next.5:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
rollup@4.45.1:
dependencies:
'@types/estree': 1.0.8
@@ -1379,12 +2920,89 @@ snapshots:
rrweb-cssom@0.8.0: {}
+ safe-array-concat@1.1.3:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ get-intrinsic: 1.3.0
+ has-symbols: 1.1.0
+ isarray: 2.0.5
+
+ safe-push-apply@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ isarray: 2.0.5
+
+ safe-regex-test@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-regex: 1.2.1
+
safer-buffer@2.1.2: {}
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
+ semver@6.3.1: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-property-descriptors: 1.0.2
+
+ set-function-name@2.0.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ functions-have-names: 1.2.3
+ has-property-descriptors: 1.0.2
+
+ set-proto@1.0.0:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
siginfo@2.0.0: {}
sirv@3.0.1:
@@ -1399,10 +3017,61 @@ snapshots:
std-env@3.9.0: {}
+ stop-iteration-iterator@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ internal-slot: 1.1.0
+
+ string.prototype.matchall@4.0.12:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ get-intrinsic: 1.3.0
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ internal-slot: 1.1.0
+ regexp.prototype.flags: 1.5.4
+ set-function-name: 2.0.2
+ side-channel: 1.1.0
+
+ string.prototype.repeat@1.0.0:
+ dependencies:
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+
+ string.prototype.trim@1.2.10:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-data-property: 1.1.4
+ define-properties: 1.2.1
+ es-abstract: 1.24.0
+ es-object-atoms: 1.1.1
+ has-property-descriptors: 1.0.2
+
+ string.prototype.trimend@1.0.9:
+ dependencies:
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
+ string.prototype.trimstart@1.0.8:
+ dependencies:
+ call-bind: 1.0.8
+ define-properties: 1.2.1
+ es-object-atoms: 1.1.1
+
strip-indent@3.0.0:
dependencies:
min-indent: 1.0.1
+ strip-json-comments@3.1.1: {}
+
strip-literal@3.0.0:
dependencies:
js-tokens: 9.0.1
@@ -1411,6 +3080,8 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ supports-preserve-symlinks-flag@1.0.0: {}
+
symbol-tree@3.2.4: {}
tinybench@2.9.0: {}
@@ -1444,6 +3115,54 @@ snapshots:
dependencies:
punycode: 2.3.1
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ typed-array-buffer@1.0.3:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-length@1.0.3:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+
+ typed-array-byte-offset@1.0.4:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ has-proto: 1.2.0
+ is-typed-array: 1.1.15
+ reflect.getprototypeof: 1.0.10
+
+ typed-array-length@1.0.7:
+ dependencies:
+ call-bind: 1.0.8
+ for-each: 0.3.5
+ gopd: 1.2.0
+ is-typed-array: 1.1.15
+ possible-typed-array-names: 1.1.0
+ reflect.getprototypeof: 1.0.10
+
+ unbox-primitive@1.1.0:
+ dependencies:
+ call-bound: 1.0.4
+ has-bigints: 1.1.0
+ has-symbols: 1.1.0
+ which-boxed-primitive: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
vite-node@3.2.4:
dependencies:
cac: 6.7.14
@@ -1535,13 +3254,62 @@ snapshots:
tr46: 5.1.1
webidl-conversions: 7.0.0
+ which-boxed-primitive@1.1.1:
+ dependencies:
+ is-bigint: 1.1.0
+ is-boolean-object: 1.2.2
+ is-number-object: 1.1.1
+ is-string: 1.1.1
+ is-symbol: 1.1.1
+
+ which-builtin-type@1.2.1:
+ dependencies:
+ call-bound: 1.0.4
+ function.prototype.name: 1.1.8
+ has-tostringtag: 1.0.2
+ is-async-function: 2.1.1
+ is-date-object: 1.1.0
+ is-finalizationregistry: 1.1.1
+ is-generator-function: 1.1.0
+ is-regex: 1.2.1
+ is-weakref: 1.1.1
+ isarray: 2.0.5
+ which-boxed-primitive: 1.1.1
+ which-collection: 1.0.2
+ which-typed-array: 1.1.19
+
+ which-collection@1.0.2:
+ dependencies:
+ is-map: 2.0.3
+ is-set: 2.0.3
+ is-weakmap: 2.0.2
+ is-weakset: 2.0.4
+
+ which-typed-array@1.1.19:
+ dependencies:
+ available-typed-arrays: 1.0.7
+ call-bind: 1.0.8
+ call-bound: 1.0.4
+ for-each: 0.3.5
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-tostringtag: 1.0.2
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
+ word-wrap@1.2.5: {}
+
ws@8.18.3: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
+
+ yocto-queue@0.1.0: {}
From 403fca5bc38556915fac1d017b5346060a2f6951 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Tue, 29 Jul 2025 14:41:42 +0900
Subject: [PATCH 02/46] =?UTF-8?q?=EC=BD=94=EB=93=9C=20prettier=20=EC=A0=81?=
=?UTF-8?q?=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 260 ++++++++++++++++++++++++++++------------
1 file changed, 186 insertions(+), 74 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 825eae3a5..8af340cfd 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,17 +1,18 @@
-var prodList
-var bonusPts = 0
-var stockInfo
-var itemCnt
-var lastSel
-var sel
-var addBtn
-var totalAmt = 0
-var PRODUCT_ONE = 'p1'
-var p2 = 'p2'
-var product_3 = 'p3'
-var p4 = "p4"
-var PRODUCT_5 = `p5`
-var cartDisp
+var prodList;
+var bonusPts = 0;
+var stockInfo;
+var itemCnt;
+var lastSel;
+var sel;
+var addBtnss;
+var totalAmt = 0;
+var PRODUCT_ONE = 'p1';
+var p2 = 'p2';
+var product_3 = 'p3';
+var p4 = 'p4';
+var PRODUCT_5 = `p5`;
+var cartDisp;
+
function main() {
var root;
var header;
@@ -27,10 +28,42 @@ function main() {
itemCnt = 0;
lastSel = null;
prodList = [
- {id: PRODUCT_ONE, name: '버그 없애는 키보드', val: 10000, originalVal: 10000, q: 50, onSale: false, suggestSale: false},
- {id: p2, name: '생산성 폭발 마우스', val: 20000, originalVal: 20000, q: 30, onSale: false, suggestSale: false},
- {id: product_3, name: "거북목 탈출 모니터암", val: 30000, originalVal: 30000, q: 20, onSale: false, suggestSale: false},
- {id: p4, name: "에러 방지 노트북 파우치", val: 15000, originalVal: 15000, q: 0, onSale: false, suggestSale: false},
+ {
+ id: PRODUCT_ONE,
+ name: '버그 없애는 키보드',
+ val: 10000,
+ originalVal: 10000,
+ q: 50,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: p2,
+ name: '생산성 폭발 마우스',
+ val: 20000,
+ originalVal: 20000,
+ q: 30,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: product_3,
+ name: '거북목 탈출 모니터암',
+ val: 30000,
+ originalVal: 30000,
+ q: 20,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: p4,
+ name: '에러 방지 노트북 파우치',
+ val: 15000,
+ originalVal: 15000,
+ q: 0,
+ onSale: false,
+ suggestSale: false,
+ },
{
id: PRODUCT_5,
name: `코딩할 때 듣는 Lo-Fi 스피커`,
@@ -38,12 +71,12 @@ function main() {
originalVal: 25000,
q: 10,
onSale: false,
- suggestSale: false
- }
- ]
- var root = document.getElementById('app')
+ suggestSale: false,
+ },
+ ];
+ var root = document.getElementById('app');
header = document.createElement('div');
- header.className = 'mb-8'
+ header.className = 'mb-8';
header.innerHTML = `
Shopping Cart
@@ -52,19 +85,21 @@ function main() {
sel = document.createElement('select');
sel.id = 'product-select';
gridContainer = document.createElement('div');
- leftColumn = document.createElement("div");
- leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto'
+ leftColumn = document.createElement('div');
+ leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
selectorContainer = document.createElement('div');
selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
- gridContainer.className = 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
+ gridContainer.className =
+ 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
addBtn = document.createElement('button');
stockInfo = document.createElement('div');
addBtn.id = 'add-to-cart';
stockInfo.id = 'stock-status';
stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
addBtn.innerHTML = 'Add to Cart';
- addBtn.className = 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+ addBtn.className =
+ 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
selectorContainer.appendChild(sel);
selectorContainer.appendChild(addBtn);
selectorContainer.appendChild(stockInfo);
@@ -109,7 +144,8 @@ function main() {
manualOverlay.classList.toggle('hidden');
manualColumn.classList.toggle('translate-x-full');
};
- manualToggle.className = 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50';
+ manualToggle.className =
+ 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50';
manualToggle.innerHTML = `
`;
sum = rightColumn.querySelector('#cart-total');
+
+ manualOverlay = document.createElement('div');
+ manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300';
+ manualOverlay.onclick = function (e) {
+ if (e.target === manualOverlay) {
+ manualOverlay.classList.add('hidden');
+ manualColumn.classList.add('translate-x-full');
+ }
+ };
+
manualToggle = document.createElement('button');
manualToggle.onclick = function () {
manualOverlay.classList.toggle('hidden');
@@ -151,14 +174,7 @@ function main() {
`;
- manualOverlay = document.createElement('div');
- manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300';
- manualOverlay.onclick = function (e) {
- if (e.target === manualOverlay) {
- manualOverlay.classList.add('hidden');
- manualColumn.classList.add('translate-x-full');
- }
- };
+
manualColumn = document.createElement('div');
manualColumn.className =
'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300';
@@ -222,6 +238,7 @@ function main() {
`;
+
gridContainer.appendChild(leftColumn);
gridContainer.appendChild(rightColumn);
manualOverlay.appendChild(manualColumn);
@@ -229,10 +246,12 @@ function main() {
root.appendChild(gridContainer);
root.appendChild(manualToggle);
root.appendChild(manualOverlay);
+
var initStock = 0;
for (var i = 0; i < prodList.length; i++) {
initStock += prodList[i].q;
}
+
onUpdateSelectOptions();
handleCalculateCartStuff();
lightningDelay = Math.random() * 10000;
@@ -240,6 +259,7 @@ function main() {
setInterval(function () {
var luckyIdx = Math.floor(Math.random() * prodList.length);
var luckyItem = prodList[luckyIdx];
+
if (luckyItem.q > 0 && !luckyItem.onSale) {
luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100);
luckyItem.onSale = true;
@@ -249,10 +269,12 @@ function main() {
}
}, 30000);
}, lightningDelay);
+
setTimeout(function () {
setInterval(function () {
if (cartDisp.children.length === 0) {
}
+
if (lastSel) {
var suggest = null;
for (var k = 0; k < prodList.length; k++) {
@@ -265,6 +287,7 @@ function main() {
}
}
}
+
if (suggest) {
alert('💝 ' + suggest.name + '은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!');
suggest.val = Math.round((suggest.val * (100 - 5)) / 100);
@@ -276,25 +299,32 @@ function main() {
}, 60000);
}, Math.random() * 20000);
}
+
var sum;
+
function onUpdateSelectOptions() {
var totalStock;
var opt;
var discountText;
+
sel.innerHTML = '';
totalStock = 0;
+
for (var idx = 0; idx < prodList.length; idx++) {
var _p = prodList[idx];
totalStock = totalStock + _p.q;
}
+
for (var i = 0; i < prodList.length; i++) {
(function () {
var item = prodList[i];
opt = document.createElement('option');
opt.value = item.id;
discountText = '';
+
if (item.onSale) discountText += ' ⚡SALE';
if (item.suggestSale) discountText += ' 💝추천';
+
if (item.q === 0) {
opt.textContent = item.name + ' - ' + item.val + '원 (품절)' + discountText;
opt.disabled = true;
@@ -325,12 +355,14 @@ function onUpdateSelectOptions() {
sel.appendChild(opt);
})();
}
+
if (totalStock < 50) {
sel.style.borderColor = 'orange';
} else {
sel.style.borderColor = '';
}
}
+
function handleCalculateCartStuff() {
var cartItems;
var subTot;
@@ -353,6 +385,7 @@ function handleCalculateCartStuff() {
var hasP1;
var hasP2;
var loyaltyDiv;
+
totalAmt = 0;
itemCnt = 0;
originalTotal = totalAmt;
@@ -361,11 +394,13 @@ function handleCalculateCartStuff() {
bulkDisc = subTot;
itemDiscounts = [];
lowStockItems = [];
+
for (idx = 0; idx < prodList.length; idx++) {
if (prodList[idx].q < 5 && prodList[idx].q > 0) {
lowStockItems.push(prodList[idx].name);
}
}
+
for (let i = 0; i < cartItems.length; i++) {
(function () {
var curItem;
@@ -375,15 +410,18 @@ function handleCalculateCartStuff() {
break;
}
}
+
var qtyElem = cartItems[i].querySelector('.quantity-number');
var q;
var itemTot;
var disc;
+
q = parseInt(qtyElem.textContent);
itemTot = curItem.val * q;
disc = 0;
itemCnt += q;
subTot += itemTot;
+
var itemDiv = cartItems[i];
var priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
priceElems.forEach(function (elem) {
@@ -391,6 +429,7 @@ function handleCalculateCartStuff() {
elem.style.fontWeight = q >= 10 ? 'bold' : 'normal';
}
});
+
if (q >= 10) {
if (curItem.id === PRODUCT_ONE) {
disc = 10 / 100;
@@ -415,20 +454,25 @@ function handleCalculateCartStuff() {
itemDiscounts.push({ name: curItem.name, discount: disc * 100 });
}
}
+
totalAmt += itemTot * (1 - disc);
})();
}
+
let discRate = 0;
var originalTotal = subTot;
+
if (itemCnt >= 30) {
totalAmt = (subTot * 75) / 100;
discRate = 25 / 100;
} else {
discRate = (subTot - totalAmt) / subTot;
}
+
const today = new Date();
var isTuesday = today.getDay() === 2;
var tuesdaySpecial = document.getElementById('tuesday-special');
+
if (isTuesday) {
if (totalAmt > 0) {
totalAmt = (totalAmt * 90) / 100;
@@ -440,9 +484,12 @@ function handleCalculateCartStuff() {
} else {
tuesdaySpecial.classList.add('hidden');
}
+
document.getElementById('item-count').textContent = '🛍️ ' + itemCnt + ' items in cart';
+
summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
+
if (subTot > 0) {
for (let i = 0; i < cartItems.length; i++) {
var curItem;
@@ -452,9 +499,11 @@ function handleCalculateCartStuff() {
break;
}
}
+
var qtyElem = cartItems[i].querySelector('.quantity-number');
var q = parseInt(qtyElem.textContent);
var itemTotal = curItem.val * q;
+
summaryDetails.innerHTML += `
${curItem.name} x ${q}
@@ -462,6 +511,7 @@ function handleCalculateCartStuff() {
`;
}
+
summaryDetails.innerHTML += `
@@ -469,6 +519,7 @@ function handleCalculateCartStuff() {
₩${subTot.toLocaleString()}
`;
+
if (itemCnt >= 30) {
summaryDetails.innerHTML += `
@@ -486,6 +537,7 @@ function handleCalculateCartStuff() {
`;
});
}
+
if (isTuesday) {
if (totalAmt > 0) {
summaryDetails.innerHTML += `
@@ -496,6 +548,7 @@ function handleCalculateCartStuff() {
`;
}
}
+
summaryDetails.innerHTML += `
Shipping
@@ -503,10 +556,12 @@ function handleCalculateCartStuff() {
`;
}
+
totalDiv = sum.querySelector('.text-2xl');
if (totalDiv) {
totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString();
}
+
loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
points = Math.floor(totalAmt / 1000);
@@ -518,8 +573,10 @@ function handleCalculateCartStuff() {
loyaltyPointsDiv.style.display = 'block';
}
}
+
discountInfoDiv = document.getElementById('discount-info');
discountInfoDiv.innerHTML = '';
+
if (discRate > 0 && totalAmt > 0) {
savedAmount = originalTotal - totalAmt;
discountInfoDiv.innerHTML = `
@@ -534,6 +591,7 @@ function handleCalculateCartStuff() {
`;
}
+
itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
@@ -542,6 +600,7 @@ function handleCalculateCartStuff() {
itemCountElement.setAttribute('data-changed', 'true');
}
}
+
stockMsg = '';
for (var stockIdx = 0; stockIdx < prodList.length; stockIdx++) {
var item = prodList[stockIdx];
@@ -554,9 +613,11 @@ function handleCalculateCartStuff() {
}
}
stockInfo.textContent = stockMsg;
+
handleStockInfoUpdate();
doRenderBonusPoints();
}
+
var doRenderBonusPoints = function () {
var basePoints;
var finalPoints;
@@ -565,27 +626,33 @@ var doRenderBonusPoints = function () {
var hasMouse;
var hasMonitorArm;
var nodes;
+
if (cartDisp.children.length === 0) {
document.getElementById('loyalty-points').style.display = 'none';
return;
}
+
basePoints = Math.floor(totalAmt / 1000);
finalPoints = 0;
pointsDetail = [];
+
if (basePoints > 0) {
finalPoints = basePoints;
pointsDetail.push('기본: ' + basePoints + 'p');
}
+
if (new Date().getDay() === 2) {
if (basePoints > 0) {
finalPoints = basePoints * 2;
pointsDetail.push('화요일 2배');
}
}
+
hasKeyboard = false;
hasMouse = false;
hasMonitorArm = false;
nodes = cartDisp.children;
+
for (const node of nodes) {
var product = null;
for (var pIdx = 0; pIdx < prodList.length; pIdx++) {
@@ -594,7 +661,9 @@ var doRenderBonusPoints = function () {
break;
}
}
+
if (!product) continue;
+
if (product.id === PRODUCT_ONE) {
hasKeyboard = true;
} else if (product.id === p2) {
@@ -603,14 +672,17 @@ var doRenderBonusPoints = function () {
hasMonitorArm = true;
}
}
+
if (hasKeyboard && hasMouse) {
finalPoints = finalPoints + 50;
pointsDetail.push('키보드+마우스 세트 +50p');
}
+
if (hasKeyboard && hasMouse && hasMonitorArm) {
finalPoints = finalPoints + 100;
pointsDetail.push('풀세트 구매 +100p');
}
+
if (itemCnt >= 30) {
finalPoints = finalPoints + 100;
pointsDetail.push('대량구매(30개+) +100p');
@@ -627,6 +699,7 @@ var doRenderBonusPoints = function () {
}
bonusPts = finalPoints;
var ptsTag = document.getElementById('loyalty-points');
+
if (ptsTag) {
if (bonusPts > 0) {
ptsTag.innerHTML =
@@ -643,10 +716,12 @@ var doRenderBonusPoints = function () {
}
}
};
+
function onGetStockTotal() {
var sum;
var i;
var currentProduct;
+
sum = 0;
for (i = 0; i < prodList.length; i++) {
currentProduct = prodList[i];
@@ -654,14 +729,18 @@ function onGetStockTotal() {
}
return sum;
}
+
var handleStockInfoUpdate = function () {
var infoMsg;
var totalStock;
var messageOptimizer;
+
infoMsg = '';
totalStock = onGetStockTotal();
+
if (totalStock < 30) {
}
+
prodList.forEach(function (item) {
if (item.q < 5) {
if (item.q > 0) {
@@ -671,34 +750,43 @@ var handleStockInfoUpdate = function () {
}
}
});
+
stockInfo.textContent = infoMsg;
};
+
function doUpdatePricesInCart() {
var totalCount = 0,
j = 0;
var cartItems;
+
while (cartDisp.children[j]) {
var qty = cartDisp.children[j].querySelector('.quantity-number');
totalCount += qty ? parseInt(qty.textContent) : 0;
j++;
}
+
totalCount = 0;
for (j = 0; j < cartDisp.children.length; j++) {
totalCount += parseInt(cartDisp.children[j].querySelector('.quantity-number').textContent);
}
+
cartItems = cartDisp.children;
+
for (var i = 0; i < cartItems.length; i++) {
var itemId = cartItems[i].id;
var product = null;
+
for (var productIdx = 0; productIdx < prodList.length; productIdx++) {
if (prodList[productIdx].id === itemId) {
product = prodList[productIdx];
break;
}
}
+
if (product) {
var priceDiv = cartItems[i].querySelector('.text-lg');
var nameDiv = cartItems[i].querySelector('h3');
+
if (product.onSale && product.suggestSale) {
priceDiv.innerHTML =
'₩' +
@@ -729,21 +817,27 @@ function doUpdatePricesInCart() {
}
}
}
+
handleCalculateCartStuff();
}
+
main();
+
addBtn.addEventListener('click', function () {
var selItem = sel.value;
var hasItem = false;
+
for (var idx = 0; idx < prodList.length; idx++) {
if (prodList[idx].id === selItem) {
hasItem = true;
break;
}
}
+
if (!selItem || !hasItem) {
return;
}
+
var itemToAdd = null;
for (var j = 0; j < prodList.length; j++) {
if (prodList[j].id === selItem) {
@@ -751,11 +845,14 @@ addBtn.addEventListener('click', function () {
break;
}
}
+
if (itemToAdd && itemToAdd.q > 0) {
var item = document.getElementById(itemToAdd['id']);
+
if (item) {
var qtyElem = item.querySelector('.quantity-number');
var newQty = parseInt(qtyElem['textContent']) + 1;
+
if (newQty <= itemToAdd.q + parseInt(qtyElem.textContent)) {
qtyElem.textContent = newQty;
itemToAdd['q']--;
@@ -831,27 +928,33 @@ addBtn.addEventListener('click', function () {
cartDisp.appendChild(newItem);
itemToAdd.q--;
}
+
handleCalculateCartStuff();
lastSel = selItem;
}
});
+
cartDisp.addEventListener('click', function (event) {
var tgt = event.target;
+
if (tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) {
var prodId = tgt.dataset.productId;
var itemElem = document.getElementById(prodId);
var prod = null;
+
for (var prdIdx = 0; prdIdx < prodList.length; prdIdx++) {
if (prodList[prdIdx].id === prodId) {
prod = prodList[prdIdx];
break;
}
}
+
if (tgt.classList.contains('quantity-change')) {
var qtyChange = parseInt(tgt.dataset.change);
var qtyElem = itemElem.querySelector('.quantity-number');
var currentQty = parseInt(qtyElem.textContent);
var newQty = currentQty + qtyChange;
+
if (newQty > 0 && newQty <= prod.q + currentQty) {
qtyElem.textContent = newQty;
prod.q -= qtyChange;
@@ -867,8 +970,10 @@ cartDisp.addEventListener('click', function (event) {
prod.q += remQty;
itemElem.remove();
}
+
if (prod && prod.q < 5) {
}
+
handleCalculateCartStuff();
onUpdateSelectOptions();
}
From 1eec76fcd9eee1c11eac43d199f39c910996a5b8 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Tue, 29 Jul 2025 15:51:56 +0900
Subject: [PATCH 06/46] =?UTF-8?q?chore:=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?=
=?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 46 ++++++++++++++++++++++++++++++-----------
1 file changed, 34 insertions(+), 12 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 0fcd10d96..09558a9da 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -29,6 +29,7 @@ function main() {
itemCnt = 0;
lastSel = null;
+ // 상품 정보 초기화
prodList = [
{
id: PRODUCT_ONE,
@@ -108,6 +109,7 @@ function main() {
addBtn.className =
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+ // 상품 선택/추가/재고 표시 컨테이너
selectorContainer = document.createElement('div');
selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
selectorContainer.appendChild(sel);
@@ -120,6 +122,7 @@ function main() {
leftColumn.appendChild(selectorContainer);
leftColumn.appendChild(cartDisp);
+ // 오른쪽 컬럼(주문 요약) 생성
rightColumn = document.createElement('div');
rightColumn.className = 'bg-black text-white p-8 flex flex-col';
rightColumn.innerHTML = `
@@ -153,6 +156,7 @@ function main() {
`;
sum = rightColumn.querySelector('#cart-total');
+ // 이용 안내(오버레이) 관련 요소 생성
manualOverlay = document.createElement('div');
manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300';
manualOverlay.onclick = function (e) {
@@ -252,14 +256,16 @@ function main() {
initStock += prodList[i].q;
}
+ // 상품 옵션, 장바구니, 가격 등 초기 렌더링
onUpdateSelectOptions();
handleCalculateCartStuff();
+
+ // 번개 세일(랜덤 상품 20% 할인) 타이머 설정
lightningDelay = Math.random() * 10000;
setTimeout(() => {
setInterval(function () {
var luckyIdx = Math.floor(Math.random() * prodList.length);
var luckyItem = prodList[luckyIdx];
-
if (luckyItem.q > 0 && !luckyItem.onSale) {
luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100);
luckyItem.onSale = true;
@@ -270,6 +276,7 @@ function main() {
}, 30000);
}, lightningDelay);
+ // 추천 할인(다른 상품 5% 할인) 타이머 설정
setTimeout(function () {
setInterval(function () {
if (cartDisp.children.length === 0) {
@@ -302,6 +309,7 @@ function main() {
var sum;
+// 상품 선택 옵션 렌더링 및 재고 상태 표시
function onUpdateSelectOptions() {
var totalStock;
var opt;
@@ -314,7 +322,7 @@ function onUpdateSelectOptions() {
var _p = prodList[idx];
totalStock = totalStock + _p.q;
}
-
+ // 각 상품별 옵션 생성
for (var i = 0; i < prodList.length; i++) {
(function () {
var item = prodList[i];
@@ -363,6 +371,7 @@ function onUpdateSelectOptions() {
}
}
+// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
function handleCalculateCartStuff() {
var cartItems;
var subTot;
@@ -395,12 +404,14 @@ function handleCalculateCartStuff() {
itemDiscounts = [];
lowStockItems = [];
+ // 재고 부족 상품 체크
for (idx = 0; idx < prodList.length; idx++) {
if (prodList[idx].q < 5 && prodList[idx].q > 0) {
lowStockItems.push(prodList[idx].name);
}
}
+ // 장바구니 내 각 상품별 합계/할인 계산
for (let i = 0; i < cartItems.length; i++) {
(function () {
var curItem;
@@ -430,6 +441,7 @@ function handleCalculateCartStuff() {
}
});
+ // 10개 이상 구매시 개별 할인 적용
if (q >= 10) {
if (curItem.id === PRODUCT_ONE) {
disc = 10 / 100;
@@ -484,9 +496,9 @@ function handleCalculateCartStuff() {
} else {
tuesdaySpecial.classList.add('hidden');
}
-
+ // 장바구니 수량 표시 갱신
document.getElementById('item-count').textContent = '🛍️ ' + itemCnt + ' items in cart';
-
+ // 주문 요약(상품별, 할인, 배송 등) 갱신
summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
@@ -556,12 +568,12 @@ function handleCalculateCartStuff() {
`;
}
-
+ // 총 결제 금액 표시 갱신
totalDiv = sum.querySelector('.text-2xl');
if (totalDiv) {
totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString();
}
-
+ // 적립 포인트 표시 갱신
loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
points = Math.floor(totalAmt / 1000);
@@ -573,7 +585,7 @@ function handleCalculateCartStuff() {
loyaltyPointsDiv.style.display = 'block';
}
}
-
+ // 할인 정보 표시 갱신
discountInfoDiv = document.getElementById('discount-info');
discountInfoDiv.innerHTML = '';
@@ -591,7 +603,7 @@ function handleCalculateCartStuff() {
`;
}
-
+ // 장바구니 수량 변화 애니메이션 표시
itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
@@ -600,7 +612,7 @@ function handleCalculateCartStuff() {
itemCountElement.setAttribute('data-changed', 'true');
}
}
-
+ // 재고 부족/품절 안내 메시지 갱신
stockMsg = '';
for (var stockIdx = 0; stockIdx < prodList.length; stockIdx++) {
var item = prodList[stockIdx];
@@ -618,6 +630,7 @@ function handleCalculateCartStuff() {
doRenderBonusPoints();
}
+// 적립 포인트 계산 및 상세 내역 표시
var doRenderBonusPoints = function () {
var basePoints;
var finalPoints;
@@ -640,14 +653,14 @@ var doRenderBonusPoints = function () {
finalPoints = basePoints;
pointsDetail.push('기본: ' + basePoints + 'p');
}
-
+ // 화요일 2배 포인트
if (new Date().getDay() === 2) {
if (basePoints > 0) {
finalPoints = basePoints * 2;
pointsDetail.push('화요일 2배');
}
}
-
+ // 키보드/마우스/모니터암 포함 여부 체크
hasKeyboard = false;
hasMouse = false;
hasMonitorArm = false;
@@ -672,7 +685,7 @@ var doRenderBonusPoints = function () {
hasMonitorArm = true;
}
}
-
+ // 키보드+마우스 세트, 풀세트, 대량구매 추가 포인트
if (hasKeyboard && hasMouse) {
finalPoints = finalPoints + 50;
pointsDetail.push('키보드+마우스 세트 +50p');
@@ -717,6 +730,7 @@ var doRenderBonusPoints = function () {
}
};
+// 전체 재고 합계 반환
function onGetStockTotal() {
var sum;
var i;
@@ -730,6 +744,7 @@ function onGetStockTotal() {
return sum;
}
+// 재고 부족/품절 안내 메시지 갱신
var handleStockInfoUpdate = function () {
var infoMsg;
var totalStock;
@@ -754,11 +769,13 @@ var handleStockInfoUpdate = function () {
stockInfo.textContent = infoMsg;
};
+// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
function doUpdatePricesInCart() {
var totalCount = 0,
j = 0;
var cartItems;
+ // 장바구니 내 전체 수량 계산
while (cartDisp.children[j]) {
var qty = cartDisp.children[j].querySelector('.quantity-number');
totalCount += qty ? parseInt(qty.textContent) : 0;
@@ -772,6 +789,7 @@ function doUpdatePricesInCart() {
cartItems = cartDisp.children;
+ // 각 상품별 할인/이름/가격 갱신
for (var i = 0; i < cartItems.length; i++) {
var itemId = cartItems[i].id;
var product = null;
@@ -823,6 +841,7 @@ function doUpdatePricesInCart() {
main();
+// 장바구니 추가 버튼 클릭 이벤트
addBtn.addEventListener('click', function () {
var selItem = sel.value;
var hasItem = false;
@@ -850,6 +869,7 @@ addBtn.addEventListener('click', function () {
var item = document.getElementById(itemToAdd['id']);
if (item) {
+ // 이미 장바구니에 있으면 수량 증가
var qtyElem = item.querySelector('.quantity-number');
var newQty = parseInt(qtyElem['textContent']) + 1;
@@ -860,6 +880,7 @@ addBtn.addEventListener('click', function () {
alert('재고가 부족합니다.');
}
} else {
+ // 장바구니에 새로 추가
var newItem = document.createElement('div');
newItem.id = itemToAdd.id;
newItem.className =
@@ -934,6 +955,7 @@ addBtn.addEventListener('click', function () {
}
});
+// 장바구니 내 수량 변경/삭제 이벤트 처리
cartDisp.addEventListener('click', function (event) {
var tgt = event.target;
From c080c881259d4b2b88639eb7c58b499b8498c18f Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 01:10:03 +0900
Subject: [PATCH 07/46] =?UTF-8?q?refactor:=20=EB=B3=80=EC=88=98=20?=
=?UTF-8?q?=EC=84=A0=EC=96=B8=20=EC=9D=BC=EA=B4=80=EC=84=B1=20=EC=A0=81?=
=?UTF-8?q?=EC=9A=A9=20(=20const,=20let)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 277 +++++++++++++++++-----------------------
1 file changed, 120 insertions(+), 157 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 09558a9da..ebfc190c3 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,30 +1,19 @@
-var prodList;
-var bonusPts = 0;
-var stockInfo;
-var itemCnt;
-var lastSel;
-var sel;
-var addBtn;
-var totalAmt = 0;
-var PRODUCT_ONE = 'p1';
-var p2 = 'p2';
-var product_3 = 'p3';
-var p4 = 'p4';
-var PRODUCT_5 = `p5`;
-var cartDisp;
+let prodList;
+let bonusPts = 0;
+let stockInfo;
+let itemCnt;
+let lastSel;
+let sel;
+let addBtn;
+let totalAmt = 0;
+const PRODUCT_ONE = 'p1';
+const p2 = 'p2';
+const product_3 = 'p3';
+const p4 = 'p4';
+const PRODUCT_5 = `p5`;
+let cartDisp;
function main() {
- var root;
- var header;
- var gridContainer;
- var leftColumn;
- var selectorContainer;
- var rightColumn;
- var manualToggle;
- var manualOverlay;
- var manualColumn;
- var lightningDelay;
-
totalAmt = 0;
itemCnt = 0;
lastSel = null;
@@ -78,9 +67,9 @@ function main() {
},
];
- var root = document.getElementById('app');
+ const root = document.getElementById('app');
- header = document.createElement('div');
+ const header = document.createElement('div');
header.className = 'mb-8';
header.innerHTML = `
@@ -92,10 +81,10 @@ function main() {
sel.id = 'product-select';
sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
- leftColumn = document.createElement('div');
+ const leftColumn = document.createElement('div');
leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
- gridContainer = document.createElement('div');
+ const gridContainer = document.createElement('div');
gridContainer.className =
'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
@@ -110,7 +99,7 @@ function main() {
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
// 상품 선택/추가/재고 표시 컨테이너
- selectorContainer = document.createElement('div');
+ const selectorContainer = document.createElement('div');
selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
selectorContainer.appendChild(sel);
selectorContainer.appendChild(addBtn);
@@ -123,7 +112,7 @@ function main() {
leftColumn.appendChild(cartDisp);
// 오른쪽 컬럼(주문 요약) 생성
- rightColumn = document.createElement('div');
+ const rightColumn = document.createElement('div');
rightColumn.className = 'bg-black text-white p-8 flex flex-col';
rightColumn.innerHTML = `
@@ -157,7 +146,7 @@ function main() {
sum = rightColumn.querySelector('#cart-total');
// 이용 안내(오버레이) 관련 요소 생성
- manualOverlay = document.createElement('div');
+ const manualOverlay = document.createElement('div');
manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300';
manualOverlay.onclick = function (e) {
if (e.target === manualOverlay) {
@@ -166,7 +155,7 @@ function main() {
}
};
- manualToggle = document.createElement('button');
+ const manualToggle = document.createElement('button');
manualToggle.onclick = function () {
manualOverlay.classList.toggle('hidden');
manualColumn.classList.toggle('translate-x-full');
@@ -179,7 +168,7 @@ function main() {
`;
- manualColumn = document.createElement('div');
+ const manualColumn = document.createElement('div');
manualColumn.className =
'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300';
manualColumn.innerHTML = `
@@ -251,8 +240,8 @@ function main() {
root.appendChild(manualToggle);
root.appendChild(manualOverlay);
- var initStock = 0;
- for (var i = 0; i < prodList.length; i++) {
+ let initStock = 0;
+ for (let i = 0; i < prodList.length; i++) {
initStock += prodList[i].q;
}
@@ -261,15 +250,15 @@ function main() {
handleCalculateCartStuff();
// 번개 세일(랜덤 상품 20% 할인) 타이머 설정
- lightningDelay = Math.random() * 10000;
+ const lightningDelay = Math.random() * 10000;
setTimeout(() => {
setInterval(function () {
- var luckyIdx = Math.floor(Math.random() * prodList.length);
- var luckyItem = prodList[luckyIdx];
+ const luckyIdx = Math.floor(Math.random() * prodList.length);
+ const luckyItem = prodList[luckyIdx];
if (luckyItem.q > 0 && !luckyItem.onSale) {
luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100);
luckyItem.onSale = true;
- alert('⚡번개세일! ' + luckyItem.name + '이(가) 20% 할인 중입니다!');
+ alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
onUpdateSelectOptions();
doUpdatePricesInCart();
}
@@ -283,8 +272,8 @@ function main() {
}
if (lastSel) {
- var suggest = null;
- for (var k = 0; k < prodList.length; k++) {
+ let suggest = null;
+ for (let k = 0; k < prodList.length; k++) {
if (prodList[k].id !== lastSel) {
if (prodList[k].q > 0) {
if (!prodList[k].suggestSale) {
@@ -296,7 +285,7 @@ function main() {
}
if (suggest) {
- alert('💝 ' + suggest.name + '은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!');
+ alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
suggest.val = Math.round((suggest.val * (100 - 5)) / 100);
suggest.suggestSale = true;
onUpdateSelectOptions();
@@ -307,25 +296,25 @@ function main() {
}, Math.random() * 20000);
}
-var sum;
+let sum;
// 상품 선택 옵션 렌더링 및 재고 상태 표시
function onUpdateSelectOptions() {
- var totalStock;
- var opt;
- var discountText;
+ let totalStock;
+ let opt;
+ let discountText;
sel.innerHTML = '';
totalStock = 0;
- for (var idx = 0; idx < prodList.length; idx++) {
- var _p = prodList[idx];
+ for (let idx = 0; idx < prodList.length; idx++) {
+ const _p = prodList[idx];
totalStock = totalStock + _p.q;
}
// 각 상품별 옵션 생성
- for (var i = 0; i < prodList.length; i++) {
+ for (let i = 0; i < prodList.length; i++) {
(function () {
- var item = prodList[i];
+ const item = prodList[i];
opt = document.createElement('option');
opt.value = item.id;
discountText = '';
@@ -373,36 +362,21 @@ function onUpdateSelectOptions() {
// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
function handleCalculateCartStuff() {
- var cartItems;
- var subTot;
- var itemDiscounts;
- var lowStockItems;
- var idx;
- var originalTotal;
- var bulkDisc;
- var itemDisc;
- var savedAmount;
- var summaryDetails;
- var totalDiv;
- var loyaltyPointsDiv;
- var points;
- var discountInfoDiv;
- var itemCountElement;
- var previousCount;
- var stockMsg;
- var pts;
- var hasP1;
- var hasP2;
- var loyaltyDiv;
+ let subTot;
+ let idx;
+ let originalTotal;
+ let savedAmount;
+ let points;
+ let previousCount;
+ let stockMsg;
totalAmt = 0;
itemCnt = 0;
originalTotal = totalAmt;
- cartItems = cartDisp.children;
+ const cartItems = cartDisp.children;
subTot = 0;
- bulkDisc = subTot;
- itemDiscounts = [];
- lowStockItems = [];
+ const itemDiscounts = [];
+ const lowStockItems = [];
// 재고 부족 상품 체크
for (idx = 0; idx < prodList.length; idx++) {
@@ -414,27 +388,23 @@ function handleCalculateCartStuff() {
// 장바구니 내 각 상품별 합계/할인 계산
for (let i = 0; i < cartItems.length; i++) {
(function () {
- var curItem;
- for (var j = 0; j < prodList.length; j++) {
+ let curItem;
+ for (let j = 0; j < prodList.length; j++) {
if (prodList[j].id === cartItems[i].id) {
curItem = prodList[j];
break;
}
}
- var qtyElem = cartItems[i].querySelector('.quantity-number');
- var q;
- var itemTot;
- var disc;
-
- q = parseInt(qtyElem.textContent);
- itemTot = curItem.val * q;
- disc = 0;
+ const qtyElem = cartItems[i].querySelector('.quantity-number');
+ const q = parseInt(qtyElem.textContent);
+ const itemTot = curItem.val * q;
+ let disc = 0;
itemCnt += q;
subTot += itemTot;
- var itemDiv = cartItems[i];
- var priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
+ const itemDiv = cartItems[i];
+ const priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
priceElems.forEach(function (elem) {
if (elem.classList.contains('text-lg')) {
elem.style.fontWeight = q >= 10 ? 'bold' : 'normal';
@@ -472,7 +442,7 @@ function handleCalculateCartStuff() {
}
let discRate = 0;
- var originalTotal = subTot;
+ originalTotal = subTot;
if (itemCnt >= 30) {
totalAmt = (subTot * 75) / 100;
@@ -482,8 +452,8 @@ function handleCalculateCartStuff() {
}
const today = new Date();
- var isTuesday = today.getDay() === 2;
- var tuesdaySpecial = document.getElementById('tuesday-special');
+ const isTuesday = today.getDay() === 2;
+ const tuesdaySpecial = document.getElementById('tuesday-special');
if (isTuesday) {
if (totalAmt > 0) {
@@ -499,22 +469,22 @@ function handleCalculateCartStuff() {
// 장바구니 수량 표시 갱신
document.getElementById('item-count').textContent = '🛍️ ' + itemCnt + ' items in cart';
// 주문 요약(상품별, 할인, 배송 등) 갱신
- summaryDetails = document.getElementById('summary-details');
+ const summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
if (subTot > 0) {
for (let i = 0; i < cartItems.length; i++) {
- var curItem;
- for (var j = 0; j < prodList.length; j++) {
+ let curItem;
+ for (let j = 0; j < prodList.length; j++) {
if (prodList[j].id === cartItems[i].id) {
curItem = prodList[j];
break;
}
}
- var qtyElem = cartItems[i].querySelector('.quantity-number');
- var q = parseInt(qtyElem.textContent);
- var itemTotal = curItem.val * q;
+ const qtyElem = cartItems[i].querySelector('.quantity-number');
+ const q = parseInt(qtyElem.textContent);
+ const itemTotal = curItem.val * q;
summaryDetails.innerHTML += `
@@ -569,12 +539,12 @@ function handleCalculateCartStuff() {
`;
}
// 총 결제 금액 표시 갱신
- totalDiv = sum.querySelector('.text-2xl');
+ const totalDiv = sum.querySelector('.text-2xl');
if (totalDiv) {
totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString();
}
// 적립 포인트 표시 갱신
- loyaltyPointsDiv = document.getElementById('loyalty-points');
+ const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
points = Math.floor(totalAmt / 1000);
if (points > 0) {
@@ -586,7 +556,7 @@ function handleCalculateCartStuff() {
}
}
// 할인 정보 표시 갱신
- discountInfoDiv = document.getElementById('discount-info');
+ const discountInfoDiv = document.getElementById('discount-info');
discountInfoDiv.innerHTML = '';
if (discRate > 0 && totalAmt > 0) {
@@ -604,7 +574,7 @@ function handleCalculateCartStuff() {
`;
}
// 장바구니 수량 변화 애니메이션 표시
- itemCountElement = document.getElementById('item-count');
+ const itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
itemCountElement.textContent = '🛍️ ' + itemCnt + ' items in cart';
@@ -614,8 +584,8 @@ function handleCalculateCartStuff() {
}
// 재고 부족/품절 안내 메시지 갱신
stockMsg = '';
- for (var stockIdx = 0; stockIdx < prodList.length; stockIdx++) {
- var item = prodList[stockIdx];
+ for (let stockIdx = 0; stockIdx < prodList.length; stockIdx++) {
+ const item = prodList[stockIdx];
if (item.q < 5) {
if (item.q > 0) {
stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
@@ -631,23 +601,20 @@ function handleCalculateCartStuff() {
}
// 적립 포인트 계산 및 상세 내역 표시
-var doRenderBonusPoints = function () {
- var basePoints;
- var finalPoints;
- var pointsDetail;
- var hasKeyboard;
- var hasMouse;
- var hasMonitorArm;
- var nodes;
+const doRenderBonusPoints = function () {
+ let finalPoints;
+ let hasKeyboard;
+ let hasMouse;
+ let hasMonitorArm;
if (cartDisp.children.length === 0) {
document.getElementById('loyalty-points').style.display = 'none';
return;
}
- basePoints = Math.floor(totalAmt / 1000);
+ const basePoints = Math.floor(totalAmt / 1000);
finalPoints = 0;
- pointsDetail = [];
+ const pointsDetail = [];
if (basePoints > 0) {
finalPoints = basePoints;
@@ -664,11 +631,11 @@ var doRenderBonusPoints = function () {
hasKeyboard = false;
hasMouse = false;
hasMonitorArm = false;
- nodes = cartDisp.children;
+ const nodes = cartDisp.children;
for (const node of nodes) {
- var product = null;
- for (var pIdx = 0; pIdx < prodList.length; pIdx++) {
+ let product = null;
+ for (let pIdx = 0; pIdx < prodList.length; pIdx++) {
if (prodList[pIdx].id === node.id) {
product = prodList[pIdx];
break;
@@ -711,7 +678,7 @@ var doRenderBonusPoints = function () {
}
}
bonusPts = finalPoints;
- var ptsTag = document.getElementById('loyalty-points');
+ const ptsTag = document.getElementById('loyalty-points');
if (ptsTag) {
if (bonusPts > 0) {
@@ -732,9 +699,9 @@ var doRenderBonusPoints = function () {
// 전체 재고 합계 반환
function onGetStockTotal() {
- var sum;
- var i;
- var currentProduct;
+ let sum;
+ let i;
+ let currentProduct;
sum = 0;
for (i = 0; i < prodList.length; i++) {
@@ -745,13 +712,11 @@ function onGetStockTotal() {
}
// 재고 부족/품절 안내 메시지 갱신
-var handleStockInfoUpdate = function () {
- var infoMsg;
- var totalStock;
- var messageOptimizer;
+const handleStockInfoUpdate = function () {
+ let infoMsg;
infoMsg = '';
- totalStock = onGetStockTotal();
+ const totalStock = onGetStockTotal();
if (totalStock < 30) {
}
@@ -771,30 +736,28 @@ var handleStockInfoUpdate = function () {
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
function doUpdatePricesInCart() {
- var totalCount = 0,
- j = 0;
- var cartItems;
+ let totalCount = 0;
+ let j = 0;
// 장바구니 내 전체 수량 계산
while (cartDisp.children[j]) {
- var qty = cartDisp.children[j].querySelector('.quantity-number');
+ const qty = cartDisp.children[j].querySelector('.quantity-number');
totalCount += qty ? parseInt(qty.textContent) : 0;
j++;
}
- totalCount = 0;
for (j = 0; j < cartDisp.children.length; j++) {
totalCount += parseInt(cartDisp.children[j].querySelector('.quantity-number').textContent);
}
- cartItems = cartDisp.children;
+ const cartItems = cartDisp.children;
// 각 상품별 할인/이름/가격 갱신
- for (var i = 0; i < cartItems.length; i++) {
- var itemId = cartItems[i].id;
- var product = null;
+ for (let i = 0; i < cartItems.length; i++) {
+ const itemId = cartItems[i].id;
+ let product = null;
- for (var productIdx = 0; productIdx < prodList.length; productIdx++) {
+ for (let productIdx = 0; productIdx < prodList.length; productIdx++) {
if (prodList[productIdx].id === itemId) {
product = prodList[productIdx];
break;
@@ -802,8 +765,8 @@ function doUpdatePricesInCart() {
}
if (product) {
- var priceDiv = cartItems[i].querySelector('.text-lg');
- var nameDiv = cartItems[i].querySelector('h3');
+ const priceDiv = cartItems[i].querySelector('.text-lg');
+ const nameDiv = cartItems[i].querySelector('h3');
if (product.onSale && product.suggestSale) {
priceDiv.innerHTML =
@@ -843,10 +806,10 @@ main();
// 장바구니 추가 버튼 클릭 이벤트
addBtn.addEventListener('click', function () {
- var selItem = sel.value;
- var hasItem = false;
+ const selItem = sel.value;
+ let hasItem = false;
- for (var idx = 0; idx < prodList.length; idx++) {
+ for (let idx = 0; idx < prodList.length; idx++) {
if (prodList[idx].id === selItem) {
hasItem = true;
break;
@@ -857,8 +820,8 @@ addBtn.addEventListener('click', function () {
return;
}
- var itemToAdd = null;
- for (var j = 0; j < prodList.length; j++) {
+ let itemToAdd = null;
+ for (let j = 0; j < prodList.length; j++) {
if (prodList[j].id === selItem) {
itemToAdd = prodList[j];
break;
@@ -866,12 +829,12 @@ addBtn.addEventListener('click', function () {
}
if (itemToAdd && itemToAdd.q > 0) {
- var item = document.getElementById(itemToAdd['id']);
+ const item = document.getElementById(itemToAdd['id']);
if (item) {
// 이미 장바구니에 있으면 수량 증가
- var qtyElem = item.querySelector('.quantity-number');
- var newQty = parseInt(qtyElem['textContent']) + 1;
+ const qtyElem = item.querySelector('.quantity-number');
+ const newQty = parseInt(qtyElem['textContent']) + 1;
if (newQty <= itemToAdd.q + parseInt(qtyElem.textContent)) {
qtyElem.textContent = newQty;
@@ -881,7 +844,7 @@ addBtn.addEventListener('click', function () {
}
} else {
// 장바구니에 새로 추가
- var newItem = document.createElement('div');
+ const newItem = document.createElement('div');
newItem.id = itemToAdd.id;
newItem.className =
'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
@@ -957,14 +920,14 @@ addBtn.addEventListener('click', function () {
// 장바구니 내 수량 변경/삭제 이벤트 처리
cartDisp.addEventListener('click', function (event) {
- var tgt = event.target;
+ const tgt = event.target;
if (tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) {
- var prodId = tgt.dataset.productId;
- var itemElem = document.getElementById(prodId);
- var prod = null;
+ const prodId = tgt.dataset.productId;
+ const itemElem = document.getElementById(prodId);
+ let prod = null;
- for (var prdIdx = 0; prdIdx < prodList.length; prdIdx++) {
+ for (let prdIdx = 0; prdIdx < prodList.length; prdIdx++) {
if (prodList[prdIdx].id === prodId) {
prod = prodList[prdIdx];
break;
@@ -972,10 +935,10 @@ cartDisp.addEventListener('click', function (event) {
}
if (tgt.classList.contains('quantity-change')) {
- var qtyChange = parseInt(tgt.dataset.change);
- var qtyElem = itemElem.querySelector('.quantity-number');
- var currentQty = parseInt(qtyElem.textContent);
- var newQty = currentQty + qtyChange;
+ const qtyChange = parseInt(tgt.dataset.change);
+ const qtyElem = itemElem.querySelector('.quantity-number');
+ const currentQty = parseInt(qtyElem.textContent);
+ const newQty = currentQty + qtyChange;
if (newQty > 0 && newQty <= prod.q + currentQty) {
qtyElem.textContent = newQty;
@@ -987,8 +950,8 @@ cartDisp.addEventListener('click', function (event) {
alert('재고가 부족합니다.');
}
} else if (tgt.classList.contains('remove-item')) {
- var qtyElem = itemElem.querySelector('.quantity-number');
- var remQty = parseInt(qtyElem.textContent);
+ const qtyElem = itemElem.querySelector('.quantity-number');
+ const remQty = parseInt(qtyElem.textContent);
prod.q += remQty;
itemElem.remove();
}
From 84d9e86b6b454af3b9cc0d2ff48c23800ff63f0a Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 01:31:57 +0900
Subject: [PATCH 08/46] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?=
=?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index ebfc190c3..e507ff837 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -240,11 +240,6 @@ function main() {
root.appendChild(manualToggle);
root.appendChild(manualOverlay);
- let initStock = 0;
- for (let i = 0; i < prodList.length; i++) {
- initStock += prodList[i].q;
- }
-
// 상품 옵션, 장바구니, 가격 등 초기 렌더링
onUpdateSelectOptions();
handleCalculateCartStuff();
From f2229b7321118e44228b9d9b4f7df1e08ee6debd Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 02:24:09 +0900
Subject: [PATCH 09/46] =?UTF-8?q?refactor:=20=EC=98=88=EC=B8=A1=20?=
=?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20?=
=?UTF-8?q?=EC=A0=81=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 314 +++++++++++++++++++---------------------
1 file changed, 152 insertions(+), 162 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index e507ff837..917843c55 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,27 +1,24 @@
-let prodList;
-let bonusPts = 0;
-let stockInfo;
-let itemCnt;
-let lastSel;
-let sel;
-let addBtn;
-let totalAmt = 0;
-const PRODUCT_ONE = 'p1';
-const p2 = 'p2';
-const product_3 = 'p3';
-const p4 = 'p4';
-const PRODUCT_5 = `p5`;
-let cartDisp;
+// 전역 변수들 (명명 규칙 적용)
+let productList;
+let bonusPoints = 0;
+let stockInfoElement;
+let itemCount;
+let lastSelectedProductId;
+let productSelector;
+let addToCartButton;
+let totalAmount = 0;
+let cartDisplayElement;
+let orderSummaryElement;
function main() {
- totalAmt = 0;
- itemCnt = 0;
- lastSel = null;
+ totalAmount = 0;
+ itemCount = 0;
+ lastSelectedProductId = null;
// 상품 정보 초기화
- prodList = [
+ productList = [
{
- id: PRODUCT_ONE,
+ id: 'p1',
name: '버그 없애는 키보드',
val: 10000,
originalVal: 10000,
@@ -30,7 +27,7 @@ function main() {
suggestSale: false,
},
{
- id: p2,
+ id: 'p2',
name: '생산성 폭발 마우스',
val: 20000,
originalVal: 20000,
@@ -39,7 +36,7 @@ function main() {
suggestSale: false,
},
{
- id: product_3,
+ id: 'p3',
name: '거북목 탈출 모니터암',
val: 30000,
originalVal: 30000,
@@ -48,7 +45,7 @@ function main() {
suggestSale: false,
},
{
- id: p4,
+ id: 'p4',
name: '에러 방지 노트북 파우치',
val: 15000,
originalVal: 15000,
@@ -57,7 +54,7 @@ function main() {
suggestSale: false,
},
{
- id: PRODUCT_5,
+ id: 'p5',
name: `코딩할 때 듣는 Lo-Fi 스피커`,
val: 25000,
originalVal: 25000,
@@ -77,9 +74,9 @@ function main() {
🛍️ 0 items in cart
`;
- sel = document.createElement('select');
- sel.id = 'product-select';
- sel.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+ productSelector = document.createElement('select');
+ productSelector.id = 'product-select';
+ productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
const leftColumn = document.createElement('div');
leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
@@ -88,28 +85,28 @@ function main() {
gridContainer.className =
'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
- stockInfo = document.createElement('div');
- stockInfo.id = 'stock-status';
- stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+ stockInfoElement = document.createElement('div');
+ stockInfoElement.id = 'stock-status';
+ stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
- addBtn = document.createElement('button');
- addBtn.id = 'add-to-cart';
- addBtn.innerHTML = 'Add to Cart';
- addBtn.className =
+ addToCartButton = document.createElement('button');
+ addToCartButton.id = 'add-to-cart';
+ addToCartButton.innerHTML = 'Add to Cart';
+ addToCartButton.className =
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
// 상품 선택/추가/재고 표시 컨테이너
const selectorContainer = document.createElement('div');
selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
- selectorContainer.appendChild(sel);
- selectorContainer.appendChild(addBtn);
- selectorContainer.appendChild(stockInfo);
+ selectorContainer.appendChild(productSelector);
+ selectorContainer.appendChild(addToCartButton);
+ selectorContainer.appendChild(stockInfoElement);
- cartDisp = document.createElement('div');
- cartDisp.id = 'cart-items';
+ cartDisplayElement = document.createElement('div');
+ cartDisplayElement.id = 'cart-items';
leftColumn.appendChild(selectorContainer);
- leftColumn.appendChild(cartDisp);
+ leftColumn.appendChild(cartDisplayElement);
// 오른쪽 컬럼(주문 요약) 생성
const rightColumn = document.createElement('div');
@@ -143,7 +140,7 @@ function main() {
Earn loyalty points with purchase.
`;
- sum = rightColumn.querySelector('#cart-total');
+ orderSummaryElement = rightColumn.querySelector('#cart-total');
// 이용 안내(오버레이) 관련 요소 생성
const manualOverlay = document.createElement('div');
@@ -241,21 +238,21 @@ function main() {
root.appendChild(manualOverlay);
// 상품 옵션, 장바구니, 가격 등 초기 렌더링
- onUpdateSelectOptions();
- handleCalculateCartStuff();
+ updateProductOptions();
+ calculateCartSummary();
// 번개 세일(랜덤 상품 20% 할인) 타이머 설정
const lightningDelay = Math.random() * 10000;
setTimeout(() => {
setInterval(function () {
- const luckyIdx = Math.floor(Math.random() * prodList.length);
- const luckyItem = prodList[luckyIdx];
+ const luckyIdx = Math.floor(Math.random() * productList.length);
+ const luckyItem = productList[luckyIdx];
if (luckyItem.q > 0 && !luckyItem.onSale) {
luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100);
luckyItem.onSale = true;
alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
- onUpdateSelectOptions();
- doUpdatePricesInCart();
+ updateProductOptions();
+ updateCartPrices();
}
}, 30000);
}, lightningDelay);
@@ -263,16 +260,13 @@ function main() {
// 추천 할인(다른 상품 5% 할인) 타이머 설정
setTimeout(function () {
setInterval(function () {
- if (cartDisp.children.length === 0) {
- }
-
- if (lastSel) {
+ if (lastSelectedProductId && cartDisplayElement.children.length > 0) {
let suggest = null;
- for (let k = 0; k < prodList.length; k++) {
- if (prodList[k].id !== lastSel) {
- if (prodList[k].q > 0) {
- if (!prodList[k].suggestSale) {
- suggest = prodList[k];
+ for (let k = 0; k < productList.length; k++) {
+ if (productList[k].id !== lastSelectedProductId) {
+ if (productList[k].q > 0) {
+ if (!productList[k].suggestSale) {
+ suggest = productList[k];
break;
}
}
@@ -283,33 +277,31 @@ function main() {
alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
suggest.val = Math.round((suggest.val * (100 - 5)) / 100);
suggest.suggestSale = true;
- onUpdateSelectOptions();
- doUpdatePricesInCart();
+ updateProductOptions();
+ updateCartPrices();
}
}
}, 60000);
}, Math.random() * 20000);
}
-let sum;
-
// 상품 선택 옵션 렌더링 및 재고 상태 표시
-function onUpdateSelectOptions() {
+function updateProductOptions() {
let totalStock;
let opt;
let discountText;
- sel.innerHTML = '';
+ productSelector.innerHTML = '';
totalStock = 0;
- for (let idx = 0; idx < prodList.length; idx++) {
- const _p = prodList[idx];
+ for (let idx = 0; idx < productList.length; idx++) {
+ const _p = productList[idx];
totalStock = totalStock + _p.q;
}
// 각 상품별 옵션 생성
- for (let i = 0; i < prodList.length; i++) {
+ for (let i = 0; i < productList.length; i++) {
(function () {
- const item = prodList[i];
+ const item = productList[i];
opt = document.createElement('option');
opt.value = item.id;
discountText = '';
@@ -344,19 +336,19 @@ function onUpdateSelectOptions() {
opt.textContent = item.name + ' - ' + item.val + '원' + discountText;
}
}
- sel.appendChild(opt);
+ productSelector.appendChild(opt);
})();
}
if (totalStock < 50) {
- sel.style.borderColor = 'orange';
+ productSelector.style.borderColor = 'orange';
} else {
- sel.style.borderColor = '';
+ productSelector.style.borderColor = '';
}
}
// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
-function handleCalculateCartStuff() {
+function calculateCartSummary() {
let subTot;
let idx;
let originalTotal;
@@ -365,18 +357,18 @@ function handleCalculateCartStuff() {
let previousCount;
let stockMsg;
- totalAmt = 0;
- itemCnt = 0;
- originalTotal = totalAmt;
- const cartItems = cartDisp.children;
+ totalAmount = 0;
+ itemCount = 0;
+ originalTotal = totalAmount;
+ const cartItems = cartDisplayElement.children;
subTot = 0;
const itemDiscounts = [];
const lowStockItems = [];
// 재고 부족 상품 체크
- for (idx = 0; idx < prodList.length; idx++) {
- if (prodList[idx].q < 5 && prodList[idx].q > 0) {
- lowStockItems.push(prodList[idx].name);
+ for (idx = 0; idx < productList.length; idx++) {
+ if (productList[idx].q < 5 && productList[idx].q > 0) {
+ lowStockItems.push(productList[idx].name);
}
}
@@ -384,9 +376,9 @@ function handleCalculateCartStuff() {
for (let i = 0; i < cartItems.length; i++) {
(function () {
let curItem;
- for (let j = 0; j < prodList.length; j++) {
- if (prodList[j].id === cartItems[i].id) {
- curItem = prodList[j];
+ for (let j = 0; j < productList.length; j++) {
+ if (productList[j].id === cartItems[i].id) {
+ curItem = productList[j];
break;
}
}
@@ -395,7 +387,7 @@ function handleCalculateCartStuff() {
const q = parseInt(qtyElem.textContent);
const itemTot = curItem.val * q;
let disc = 0;
- itemCnt += q;
+ itemCount += q;
subTot += itemTot;
const itemDiv = cartItems[i];
@@ -408,19 +400,19 @@ function handleCalculateCartStuff() {
// 10개 이상 구매시 개별 할인 적용
if (q >= 10) {
- if (curItem.id === PRODUCT_ONE) {
+ if (curItem.id === 'p1') {
disc = 10 / 100;
} else {
- if (curItem.id === p2) {
+ if (curItem.id === 'p2') {
disc = 15 / 100;
} else {
- if (curItem.id === product_3) {
+ if (curItem.id === 'p3') {
disc = 20 / 100;
} else {
- if (curItem.id === p4) {
+ if (curItem.id === 'p4') {
disc = 5 / 100;
} else {
- if (curItem.id === PRODUCT_5) {
+ if (curItem.id === 'p5') {
disc = 25 / 100;
}
}
@@ -432,18 +424,18 @@ function handleCalculateCartStuff() {
}
}
- totalAmt += itemTot * (1 - disc);
+ totalAmount += itemTot * (1 - disc);
})();
}
let discRate = 0;
originalTotal = subTot;
- if (itemCnt >= 30) {
- totalAmt = (subTot * 75) / 100;
+ if (itemCount >= 30) {
+ totalAmount = (subTot * 75) / 100;
discRate = 25 / 100;
} else {
- discRate = (subTot - totalAmt) / subTot;
+ discRate = (subTot - totalAmount) / subTot;
}
const today = new Date();
@@ -451,9 +443,9 @@ function handleCalculateCartStuff() {
const tuesdaySpecial = document.getElementById('tuesday-special');
if (isTuesday) {
- if (totalAmt > 0) {
- totalAmt = (totalAmt * 90) / 100;
- discRate = 1 - totalAmt / originalTotal;
+ if (totalAmount > 0) {
+ totalAmount = (totalAmount * 90) / 100;
+ discRate = 1 - totalAmount / originalTotal;
tuesdaySpecial.classList.remove('hidden');
} else {
tuesdaySpecial.classList.add('hidden');
@@ -462,7 +454,7 @@ function handleCalculateCartStuff() {
tuesdaySpecial.classList.add('hidden');
}
// 장바구니 수량 표시 갱신
- document.getElementById('item-count').textContent = '🛍️ ' + itemCnt + ' items in cart';
+ document.getElementById('item-count').textContent = '🛍️ ' + itemCount + ' items in cart';
// 주문 요약(상품별, 할인, 배송 등) 갱신
const summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
@@ -470,9 +462,9 @@ function handleCalculateCartStuff() {
if (subTot > 0) {
for (let i = 0; i < cartItems.length; i++) {
let curItem;
- for (let j = 0; j < prodList.length; j++) {
- if (prodList[j].id === cartItems[i].id) {
- curItem = prodList[j];
+ for (let j = 0; j < productList.length; j++) {
+ if (productList[j].id === cartItems[i].id) {
+ curItem = productList[j];
break;
}
}
@@ -497,7 +489,7 @@ function handleCalculateCartStuff() {
`;
- if (itemCnt >= 30) {
+ if (itemCount >= 30) {
summaryDetails.innerHTML += `
🎉 대량구매 할인 (30개 이상)
@@ -516,7 +508,7 @@ function handleCalculateCartStuff() {
}
if (isTuesday) {
- if (totalAmt > 0) {
+ if (totalAmount > 0) {
summaryDetails.innerHTML += `
🌟 화요일 추가 할인
@@ -534,14 +526,14 @@ function handleCalculateCartStuff() {
`;
}
// 총 결제 금액 표시 갱신
- const totalDiv = sum.querySelector('.text-2xl');
+ const totalDiv = orderSummaryElement.querySelector('.text-2xl');
if (totalDiv) {
- totalDiv.textContent = '₩' + Math.round(totalAmt).toLocaleString();
+ totalDiv.textContent = '₩' + Math.round(totalAmount).toLocaleString();
}
// 적립 포인트 표시 갱신
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
- points = Math.floor(totalAmt / 1000);
+ points = Math.floor(totalAmount / 1000);
if (points > 0) {
loyaltyPointsDiv.textContent = '적립 포인트: ' + points + 'p';
loyaltyPointsDiv.style.display = 'block';
@@ -554,8 +546,8 @@ function handleCalculateCartStuff() {
const discountInfoDiv = document.getElementById('discount-info');
discountInfoDiv.innerHTML = '';
- if (discRate > 0 && totalAmt > 0) {
- savedAmount = originalTotal - totalAmt;
+ if (discRate > 0 && totalAmount > 0) {
+ savedAmount = originalTotal - totalAmount;
discountInfoDiv.innerHTML = `
@@ -572,15 +564,15 @@ function handleCalculateCartStuff() {
const itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
- itemCountElement.textContent = '🛍️ ' + itemCnt + ' items in cart';
- if (previousCount !== itemCnt) {
+ itemCountElement.textContent = '🛍️ ' + itemCount + ' items in cart';
+ if (previousCount !== itemCount) {
itemCountElement.setAttribute('data-changed', 'true');
}
}
// 재고 부족/품절 안내 메시지 갱신
stockMsg = '';
- for (let stockIdx = 0; stockIdx < prodList.length; stockIdx++) {
- const item = prodList[stockIdx];
+ for (let stockIdx = 0; stockIdx < productList.length; stockIdx++) {
+ const item = productList[stockIdx];
if (item.q < 5) {
if (item.q > 0) {
stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
@@ -589,25 +581,24 @@ function handleCalculateCartStuff() {
}
}
}
- stockInfo.textContent = stockMsg;
+ stockInfoElement.textContent = stockMsg;
- handleStockInfoUpdate();
- doRenderBonusPoints();
+ renderBonusPoints();
}
// 적립 포인트 계산 및 상세 내역 표시
-const doRenderBonusPoints = function () {
+const renderBonusPoints = function () {
let finalPoints;
let hasKeyboard;
let hasMouse;
let hasMonitorArm;
- if (cartDisp.children.length === 0) {
+ if (cartDisplayElement.children.length === 0) {
document.getElementById('loyalty-points').style.display = 'none';
return;
}
- const basePoints = Math.floor(totalAmt / 1000);
+ const basePoints = Math.floor(totalAmount / 1000);
finalPoints = 0;
const pointsDetail = [];
@@ -626,24 +617,24 @@ const doRenderBonusPoints = function () {
hasKeyboard = false;
hasMouse = false;
hasMonitorArm = false;
- const nodes = cartDisp.children;
+ const nodes = cartDisplayElement.children;
for (const node of nodes) {
let product = null;
- for (let pIdx = 0; pIdx < prodList.length; pIdx++) {
- if (prodList[pIdx].id === node.id) {
- product = prodList[pIdx];
+ for (let pIdx = 0; pIdx < productList.length; pIdx++) {
+ if (productList[pIdx].id === node.id) {
+ product = productList[pIdx];
break;
}
}
if (!product) continue;
- if (product.id === PRODUCT_ONE) {
+ if (product.id === 'p1') {
hasKeyboard = true;
- } else if (product.id === p2) {
+ } else if (product.id === 'p2') {
hasMouse = true;
- } else if (product.id === product_3) {
+ } else if (product.id === 'p3') {
hasMonitorArm = true;
}
}
@@ -658,28 +649,28 @@ const doRenderBonusPoints = function () {
pointsDetail.push('풀세트 구매 +100p');
}
- if (itemCnt >= 30) {
+ if (itemCount >= 30) {
finalPoints = finalPoints + 100;
pointsDetail.push('대량구매(30개+) +100p');
} else {
- if (itemCnt >= 20) {
+ if (itemCount >= 20) {
finalPoints = finalPoints + 50;
pointsDetail.push('대량구매(20개+) +50p');
} else {
- if (itemCnt >= 10) {
+ if (itemCount >= 10) {
finalPoints = finalPoints + 20;
pointsDetail.push('대량구매(10개+) +20p');
}
}
}
- bonusPts = finalPoints;
+ bonusPoints = finalPoints;
const ptsTag = document.getElementById('loyalty-points');
if (ptsTag) {
- if (bonusPts > 0) {
+ if (bonusPoints > 0) {
ptsTag.innerHTML =
'
적립 포인트: ' +
- bonusPts +
+ bonusPoints +
'p
' +
'
' +
pointsDetail.join(', ') +
@@ -693,30 +684,27 @@ const doRenderBonusPoints = function () {
};
// 전체 재고 합계 반환
-function onGetStockTotal() {
+function getTotalStock() {
let sum;
let i;
let currentProduct;
sum = 0;
- for (i = 0; i < prodList.length; i++) {
- currentProduct = prodList[i];
+ for (i = 0; i < productList.length; i++) {
+ currentProduct = productList[i];
sum += currentProduct.q;
}
return sum;
}
// 재고 부족/품절 안내 메시지 갱신
-const handleStockInfoUpdate = function () {
+const updateStockInfo = function () {
let infoMsg;
infoMsg = '';
- const totalStock = onGetStockTotal();
-
- if (totalStock < 30) {
- }
+ const totalStock = getTotalStock();
- prodList.forEach(function (item) {
+ productList.forEach(function (item) {
if (item.q < 5) {
if (item.q > 0) {
infoMsg = infoMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
@@ -726,35 +714,37 @@ const handleStockInfoUpdate = function () {
}
});
- stockInfo.textContent = infoMsg;
+ stockInfoElement.textContent = infoMsg;
};
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
-function doUpdatePricesInCart() {
+function updateCartPrices() {
let totalCount = 0;
let j = 0;
// 장바구니 내 전체 수량 계산
- while (cartDisp.children[j]) {
- const qty = cartDisp.children[j].querySelector('.quantity-number');
+ while (cartDisplayElement.children[j]) {
+ const qty = cartDisplayElement.children[j].querySelector('.quantity-number');
totalCount += qty ? parseInt(qty.textContent) : 0;
j++;
}
- for (j = 0; j < cartDisp.children.length; j++) {
- totalCount += parseInt(cartDisp.children[j].querySelector('.quantity-number').textContent);
+ for (j = 0; j < cartDisplayElement.children.length; j++) {
+ totalCount += parseInt(
+ cartDisplayElement.children[j].querySelector('.quantity-number').textContent,
+ );
}
- const cartItems = cartDisp.children;
+ const cartItems = cartDisplayElement.children;
// 각 상품별 할인/이름/가격 갱신
for (let i = 0; i < cartItems.length; i++) {
const itemId = cartItems[i].id;
let product = null;
- for (let productIdx = 0; productIdx < prodList.length; productIdx++) {
- if (prodList[productIdx].id === itemId) {
- product = prodList[productIdx];
+ for (let productIdx = 0; productIdx < productList.length; productIdx++) {
+ if (productList[productIdx].id === itemId) {
+ product = productList[productIdx];
break;
}
}
@@ -794,18 +784,18 @@ function doUpdatePricesInCart() {
}
}
- handleCalculateCartStuff();
+ calculateCartSummary();
}
main();
// 장바구니 추가 버튼 클릭 이벤트
-addBtn.addEventListener('click', function () {
- const selItem = sel.value;
+addToCartButton.addEventListener('click', function () {
+ const selItem = productSelector.value;
let hasItem = false;
- for (let idx = 0; idx < prodList.length; idx++) {
- if (prodList[idx].id === selItem) {
+ for (let idx = 0; idx < productList.length; idx++) {
+ if (productList[idx].id === selItem) {
hasItem = true;
break;
}
@@ -816,9 +806,9 @@ addBtn.addEventListener('click', function () {
}
let itemToAdd = null;
- for (let j = 0; j < prodList.length; j++) {
- if (prodList[j].id === selItem) {
- itemToAdd = prodList[j];
+ for (let j = 0; j < productList.length; j++) {
+ if (productList[j].id === selItem) {
+ itemToAdd = productList[j];
break;
}
}
@@ -904,17 +894,17 @@ addBtn.addEventListener('click', function () {
}">Remove
`;
- cartDisp.appendChild(newItem);
+ cartDisplayElement.appendChild(newItem);
itemToAdd.q--;
}
- handleCalculateCartStuff();
- lastSel = selItem;
+ calculateCartSummary();
+ lastSelectedProductId = selItem;
}
});
// 장바구니 내 수량 변경/삭제 이벤트 처리
-cartDisp.addEventListener('click', function (event) {
+cartDisplayElement.addEventListener('click', function (event) {
const tgt = event.target;
if (tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) {
@@ -922,9 +912,9 @@ cartDisp.addEventListener('click', function (event) {
const itemElem = document.getElementById(prodId);
let prod = null;
- for (let prdIdx = 0; prdIdx < prodList.length; prdIdx++) {
- if (prodList[prdIdx].id === prodId) {
- prod = prodList[prdIdx];
+ for (let prdIdx = 0; prdIdx < productList.length; prdIdx++) {
+ if (productList[prdIdx].id === prodId) {
+ prod = productList[prdIdx];
break;
}
}
@@ -954,7 +944,7 @@ cartDisp.addEventListener('click', function (event) {
if (prod && prod.q < 5) {
}
- handleCalculateCartStuff();
- onUpdateSelectOptions();
+ calculateCartSummary();
+ updateProductOptions();
}
});
From 0157c4e1f2259766a1de1e654ede989ffa030b1e Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 03:11:49 +0900
Subject: [PATCH 10/46] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?=
=?UTF-8?q?=EC=A0=95=EC=9D=98=20=EB=B0=8F=20=EB=A7=A4=EC=A7=81=20=EB=84=98?=
=?UTF-8?q?=EB=B2=84=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 134 ++++++++++++++++++++++++++--------------
1 file changed, 89 insertions(+), 45 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 917843c55..3d0c7ec64 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,3 +1,47 @@
+// 상수 정의
+const PRODUCT_IDS = {
+ KEYBOARD: 'p1',
+ MOUSE: 'p2',
+ MONITOR_ARM: 'p3',
+ LAPTOP_CASE: 'p4',
+ SPEAKER: 'p5',
+};
+
+const DISCOUNT_THRESHOLDS = {
+ INDIVIDUAL_ITEM: 10,
+ BULK_PURCHASE: 30,
+};
+
+const DISCOUNT_RATES = {
+ KEYBOARD: 0.1,
+ MOUSE: 0.15,
+ MONITOR_ARM: 0.2,
+ LAPTOP_CASE: 0.05,
+ SPEAKER: 0.25,
+ BULK_PURCHASE: 0.25,
+ TUESDAY: 0.1,
+};
+
+const POINT_RATES = {
+ BASE_RATE: 0.001, // 0.1% (1000원당 1포인트)
+ TUESDAY_MULTIPLIER: 2,
+ SET_BONUS: 50,
+ FULL_SET_BONUS: 100,
+ QUANTITY_BONUS_10: 20,
+ QUANTITY_BONUS_20: 50,
+ QUANTITY_BONUS_30: 100,
+};
+
+const UI_CONSTANTS = {
+ LOW_STOCK_THRESHOLD: 5,
+ TOTAL_STOCK_THRESHOLD: 50,
+ TUESDAY: 2,
+ LIGHTNING_SALE_INTERVAL: 30000,
+ LIGHTNING_SALE_DELAY: 10000,
+ SUGGEST_SALE_INTERVAL: 60000,
+ SUGGEST_SALE_DELAY: 20000,
+};
+
// 전역 변수들 (명명 규칙 적용)
let productList;
let bonusPoints = 0;
@@ -18,7 +62,7 @@ function main() {
// 상품 정보 초기화
productList = [
{
- id: 'p1',
+ id: PRODUCT_IDS.KEYBOARD,
name: '버그 없애는 키보드',
val: 10000,
originalVal: 10000,
@@ -27,7 +71,7 @@ function main() {
suggestSale: false,
},
{
- id: 'p2',
+ id: PRODUCT_IDS.MOUSE,
name: '생산성 폭발 마우스',
val: 20000,
originalVal: 20000,
@@ -36,7 +80,7 @@ function main() {
suggestSale: false,
},
{
- id: 'p3',
+ id: PRODUCT_IDS.MONITOR_ARM,
name: '거북목 탈출 모니터암',
val: 30000,
originalVal: 30000,
@@ -45,7 +89,7 @@ function main() {
suggestSale: false,
},
{
- id: 'p4',
+ id: PRODUCT_IDS.LAPTOP_CASE,
name: '에러 방지 노트북 파우치',
val: 15000,
originalVal: 15000,
@@ -54,7 +98,7 @@ function main() {
suggestSale: false,
},
{
- id: 'p5',
+ id: PRODUCT_IDS.SPEAKER,
name: `코딩할 때 듣는 Lo-Fi 스피커`,
val: 25000,
originalVal: 25000,
@@ -242,7 +286,7 @@ function main() {
calculateCartSummary();
// 번개 세일(랜덤 상품 20% 할인) 타이머 설정
- const lightningDelay = Math.random() * 10000;
+ const lightningDelay = Math.random() * UI_CONSTANTS.LIGHTNING_SALE_DELAY;
setTimeout(() => {
setInterval(function () {
const luckyIdx = Math.floor(Math.random() * productList.length);
@@ -254,7 +298,7 @@ function main() {
updateProductOptions();
updateCartPrices();
}
- }, 30000);
+ }, UI_CONSTANTS.LIGHTNING_SALE_INTERVAL);
}, lightningDelay);
// 추천 할인(다른 상품 5% 할인) 타이머 설정
@@ -281,8 +325,8 @@ function main() {
updateCartPrices();
}
}
- }, 60000);
- }, Math.random() * 20000);
+ }, UI_CONSTANTS.SUGGEST_SALE_INTERVAL);
+ }, Math.random() * UI_CONSTANTS.SUGGEST_SALE_DELAY);
}
// 상품 선택 옵션 렌더링 및 재고 상태 표시
@@ -340,7 +384,7 @@ function updateProductOptions() {
})();
}
- if (totalStock < 50) {
+ if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
productSelector.style.borderColor = 'orange';
} else {
productSelector.style.borderColor = '';
@@ -367,7 +411,7 @@ function calculateCartSummary() {
// 재고 부족 상품 체크
for (idx = 0; idx < productList.length; idx++) {
- if (productList[idx].q < 5 && productList[idx].q > 0) {
+ if (productList[idx].q < UI_CONSTANTS.LOW_STOCK_THRESHOLD && productList[idx].q > 0) {
lowStockItems.push(productList[idx].name);
}
}
@@ -399,21 +443,21 @@ function calculateCartSummary() {
});
// 10개 이상 구매시 개별 할인 적용
- if (q >= 10) {
- if (curItem.id === 'p1') {
- disc = 10 / 100;
+ if (q >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ if (curItem.id === PRODUCT_IDS.KEYBOARD) {
+ disc = DISCOUNT_RATES.KEYBOARD;
} else {
- if (curItem.id === 'p2') {
- disc = 15 / 100;
+ if (curItem.id === PRODUCT_IDS.MOUSE) {
+ disc = DISCOUNT_RATES.MOUSE;
} else {
- if (curItem.id === 'p3') {
- disc = 20 / 100;
+ if (curItem.id === PRODUCT_IDS.MONITOR_ARM) {
+ disc = DISCOUNT_RATES.MONITOR_ARM;
} else {
- if (curItem.id === 'p4') {
- disc = 5 / 100;
+ if (curItem.id === PRODUCT_IDS.LAPTOP_CASE) {
+ disc = DISCOUNT_RATES.LAPTOP_CASE;
} else {
- if (curItem.id === 'p5') {
- disc = 25 / 100;
+ if (curItem.id === PRODUCT_IDS.SPEAKER) {
+ disc = DISCOUNT_RATES.SPEAKER;
}
}
}
@@ -431,20 +475,20 @@ function calculateCartSummary() {
let discRate = 0;
originalTotal = subTot;
- if (itemCount >= 30) {
- totalAmount = (subTot * 75) / 100;
- discRate = 25 / 100;
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ totalAmount = subTot * (1 - DISCOUNT_RATES.BULK_PURCHASE);
+ discRate = DISCOUNT_RATES.BULK_PURCHASE;
} else {
discRate = (subTot - totalAmount) / subTot;
}
const today = new Date();
- const isTuesday = today.getDay() === 2;
+ const isTuesday = today.getDay() === UI_CONSTANTS.TUESDAY;
const tuesdaySpecial = document.getElementById('tuesday-special');
if (isTuesday) {
if (totalAmount > 0) {
- totalAmount = (totalAmount * 90) / 100;
+ totalAmount = totalAmount * (1 - DISCOUNT_RATES.TUESDAY);
discRate = 1 - totalAmount / originalTotal;
tuesdaySpecial.classList.remove('hidden');
} else {
@@ -489,7 +533,7 @@ function calculateCartSummary() {
`;
- if (itemCount >= 30) {
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
summaryDetails.innerHTML += `
🎉 대량구매 할인 (30개 이상)
@@ -533,7 +577,7 @@ function calculateCartSummary() {
// 적립 포인트 표시 갱신
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
- points = Math.floor(totalAmount / 1000);
+ points = Math.floor(totalAmount * POINT_RATES.BASE_RATE);
if (points > 0) {
loyaltyPointsDiv.textContent = '적립 포인트: ' + points + 'p';
loyaltyPointsDiv.style.display = 'block';
@@ -573,7 +617,7 @@ function calculateCartSummary() {
stockMsg = '';
for (let stockIdx = 0; stockIdx < productList.length; stockIdx++) {
const item = productList[stockIdx];
- if (item.q < 5) {
+ if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
if (item.q > 0) {
stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
} else {
@@ -598,7 +642,7 @@ const renderBonusPoints = function () {
return;
}
- const basePoints = Math.floor(totalAmount / 1000);
+ const basePoints = Math.floor(totalAmount * POINT_RATES.BASE_RATE);
finalPoints = 0;
const pointsDetail = [];
@@ -607,9 +651,9 @@ const renderBonusPoints = function () {
pointsDetail.push('기본: ' + basePoints + 'p');
}
// 화요일 2배 포인트
- if (new Date().getDay() === 2) {
+ if (new Date().getDay() === UI_CONSTANTS.TUESDAY) {
if (basePoints > 0) {
- finalPoints = basePoints * 2;
+ finalPoints = basePoints * POINT_RATES.TUESDAY_MULTIPLIER;
pointsDetail.push('화요일 2배');
}
}
@@ -630,35 +674,35 @@ const renderBonusPoints = function () {
if (!product) continue;
- if (product.id === 'p1') {
+ if (product.id === PRODUCT_IDS.KEYBOARD) {
hasKeyboard = true;
- } else if (product.id === 'p2') {
+ } else if (product.id === PRODUCT_IDS.MOUSE) {
hasMouse = true;
- } else if (product.id === 'p3') {
+ } else if (product.id === PRODUCT_IDS.MONITOR_ARM) {
hasMonitorArm = true;
}
}
// 키보드+마우스 세트, 풀세트, 대량구매 추가 포인트
if (hasKeyboard && hasMouse) {
- finalPoints = finalPoints + 50;
+ finalPoints = finalPoints + POINT_RATES.SET_BONUS;
pointsDetail.push('키보드+마우스 세트 +50p');
}
if (hasKeyboard && hasMouse && hasMonitorArm) {
- finalPoints = finalPoints + 100;
+ finalPoints = finalPoints + POINT_RATES.FULL_SET_BONUS;
pointsDetail.push('풀세트 구매 +100p');
}
- if (itemCount >= 30) {
- finalPoints = finalPoints + 100;
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_30;
pointsDetail.push('대량구매(30개+) +100p');
} else {
if (itemCount >= 20) {
- finalPoints = finalPoints + 50;
+ finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_20;
pointsDetail.push('대량구매(20개+) +50p');
} else {
- if (itemCount >= 10) {
- finalPoints = finalPoints + 20;
+ if (itemCount >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_10;
pointsDetail.push('대량구매(10개+) +20p');
}
}
@@ -705,7 +749,7 @@ const updateStockInfo = function () {
const totalStock = getTotalStock();
productList.forEach(function (item) {
- if (item.q < 5) {
+ if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
if (item.q > 0) {
infoMsg = infoMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
} else {
@@ -941,7 +985,7 @@ cartDisplayElement.addEventListener('click', function (event) {
itemElem.remove();
}
- if (prod && prod.q < 5) {
+ if (prod && prod.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
}
calculateCartSummary();
From 8bb5bf17d9ee9a3006258839364f90eab9fd1237 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 03:27:32 +0900
Subject: [PATCH 11/46] =?UTF-8?q?chore:=20=EB=AC=B8=EC=9E=90=EC=97=B4=20?=
=?UTF-8?q?=EC=97=B0=EA=B2=B0=EC=9D=84=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?=
=?UTF-8?q?=EB=A6=AC=ED=84=B0=EB=9F=B4=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 114 ++++++++++++++--------------------------
1 file changed, 39 insertions(+), 75 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 3d0c7ec64..739df8862 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -354,30 +354,21 @@ function updateProductOptions() {
if (item.suggestSale) discountText += ' 💝추천';
if (item.q === 0) {
- opt.textContent = item.name + ' - ' + item.val + '원 (품절)' + discountText;
+ opt.textContent = `${item.name} - ${item.val}원 (품절)${discountText}`;
opt.disabled = true;
opt.className = 'text-gray-400';
} else {
if (item.onSale && item.suggestSale) {
- opt.textContent =
- '⚡💝' +
- item.name +
- ' - ' +
- item.originalVal +
- '원 → ' +
- item.val +
- '원 (25% SUPER SALE!)';
+ opt.textContent = `⚡💝${item.name} - ${item.originalVal}원 → ${item.val}원 (25% SUPER SALE!)`;
opt.className = 'text-purple-600 font-bold';
} else if (item.onSale) {
- opt.textContent =
- '⚡' + item.name + ' - ' + item.originalVal + '원 → ' + item.val + '원 (20% SALE!)';
+ opt.textContent = `⚡${item.name} - ${item.originalVal}원 → ${item.val}원 (20% SALE!)`;
opt.className = 'text-red-500 font-bold';
} else if (item.suggestSale) {
- opt.textContent =
- '💝' + item.name + ' - ' + item.originalVal + '원 → ' + item.val + '원 (5% 추천할인!)';
+ opt.textContent = `💝${item.name} - ${item.originalVal}원 → ${item.val}원 (5% 추천할인!)`;
opt.className = 'text-blue-500 font-bold';
} else {
- opt.textContent = item.name + ' - ' + item.val + '원' + discountText;
+ opt.textContent = `${item.name} - ${item.val}원${discountText}`;
}
}
productSelector.appendChild(opt);
@@ -498,7 +489,7 @@ function calculateCartSummary() {
tuesdaySpecial.classList.add('hidden');
}
// 장바구니 수량 표시 갱신
- document.getElementById('item-count').textContent = '🛍️ ' + itemCount + ' items in cart';
+ document.getElementById('item-count').textContent = `🛍️ ${itemCount} items in cart`;
// 주문 요약(상품별, 할인, 배송 등) 갱신
const summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
@@ -572,14 +563,14 @@ function calculateCartSummary() {
// 총 결제 금액 표시 갱신
const totalDiv = orderSummaryElement.querySelector('.text-2xl');
if (totalDiv) {
- totalDiv.textContent = '₩' + Math.round(totalAmount).toLocaleString();
+ totalDiv.textContent = `₩${Math.round(totalAmount).toLocaleString()}`;
}
// 적립 포인트 표시 갱신
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
points = Math.floor(totalAmount * POINT_RATES.BASE_RATE);
if (points > 0) {
- loyaltyPointsDiv.textContent = '적립 포인트: ' + points + 'p';
+ loyaltyPointsDiv.textContent = `적립 포인트: ${points}p`;
loyaltyPointsDiv.style.display = 'block';
} else {
loyaltyPointsDiv.textContent = '적립 포인트: 0p';
@@ -608,7 +599,7 @@ function calculateCartSummary() {
const itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
- itemCountElement.textContent = '🛍️ ' + itemCount + ' items in cart';
+ itemCountElement.textContent = `🛍️ ${itemCount} items in cart`;
if (previousCount !== itemCount) {
itemCountElement.setAttribute('data-changed', 'true');
}
@@ -619,9 +610,9 @@ function calculateCartSummary() {
const item = productList[stockIdx];
if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
if (item.q > 0) {
- stockMsg = stockMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
+ stockMsg += `${item.name}: 재고 부족 (${item.q}개 남음)\n`;
} else {
- stockMsg = stockMsg + item.name + ': 품절\n';
+ stockMsg += `${item.name}: 품절\n`;
}
}
}
@@ -648,7 +639,7 @@ const renderBonusPoints = function () {
if (basePoints > 0) {
finalPoints = basePoints;
- pointsDetail.push('기본: ' + basePoints + 'p');
+ pointsDetail.push(`기본: ${basePoints}p`);
}
// 화요일 2배 포인트
if (new Date().getDay() === UI_CONSTANTS.TUESDAY) {
@@ -713,12 +704,8 @@ const renderBonusPoints = function () {
if (ptsTag) {
if (bonusPoints > 0) {
ptsTag.innerHTML =
- '적립 포인트: ' +
- bonusPoints +
- 'p
' +
- '' +
- pointsDetail.join(', ') +
- '
';
+ `적립 포인트: ${bonusPoints}p
` +
+ `${pointsDetail.join(', ')}
`;
ptsTag.style.display = 'block';
} else {
ptsTag.textContent = '적립 포인트: 0p';
@@ -751,9 +738,9 @@ const updateStockInfo = function () {
productList.forEach(function (item) {
if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
if (item.q > 0) {
- infoMsg = infoMsg + item.name + ': 재고 부족 (' + item.q + '개 남음)\n';
+ infoMsg = `${infoMsg + item.name}: 재고 부족 (${item.q}개 남음)\n`;
} else {
- infoMsg = infoMsg + item.name + ': 품절\n';
+ infoMsg = `${infoMsg + item.name}: 품절\n`;
}
}
});
@@ -798,31 +785,16 @@ function updateCartPrices() {
const nameDiv = cartItems[i].querySelector('h3');
if (product.onSale && product.suggestSale) {
- priceDiv.innerHTML =
- '₩' +
- product.originalVal.toLocaleString() +
- ' ₩' +
- product.val.toLocaleString() +
- '';
- nameDiv.textContent = '⚡💝' + product.name;
+ priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ nameDiv.textContent = `⚡💝${product.name}`;
} else if (product.onSale) {
- priceDiv.innerHTML =
- '₩' +
- product.originalVal.toLocaleString() +
- ' ₩' +
- product.val.toLocaleString() +
- '';
- nameDiv.textContent = '⚡' + product.name;
+ priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ nameDiv.textContent = `⚡${product.name}`;
} else if (product.suggestSale) {
- priceDiv.innerHTML =
- '₩' +
- product.originalVal.toLocaleString() +
- ' ₩' +
- product.val.toLocaleString() +
- '';
- nameDiv.textContent = '💝' + product.name;
+ priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ nameDiv.textContent = `💝${product.name}`;
} else {
- priceDiv.textContent = '₩' + product.val.toLocaleString();
+ priceDiv.textContent = `₩${product.val.toLocaleString()}`;
nameDiv.textContent = product.name;
}
}
@@ -894,18 +866,14 @@ addToCartButton.addEventListener('click', function () {
PRODUCT
${
itemToAdd.onSale || itemToAdd.suggestSale
- ? '₩' +
- itemToAdd.originalVal.toLocaleString() +
- ' ₩' +
- itemToAdd.val.toLocaleString() +
- ''
- : '₩' + itemToAdd.val.toLocaleString()
+ ? `₩${itemToAdd.originalVal.toLocaleString()} ₩${itemToAdd.val.toLocaleString()}`
+ : `₩${itemToAdd.val.toLocaleString()}`
}
+ `;
+ }
+
+ // 소계 표시
+ summaryDetails.innerHTML += `
+
+
+ Subtotal
+ ₩${subTot.toLocaleString()}
+
+ `;
+
+ // 할인 정보 표시
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ summaryDetails.innerHTML += `
+
+ 🎉 대량구매 할인 (30개 이상)
+ -25%
+
+ `;
+ } else if (itemDiscounts.length > 0) {
+ itemDiscounts.forEach(function (item) {
+ summaryDetails.innerHTML += `
+
+ ${item.name} (10개↑)
+ -${item.discount}%
+
+ `;
+ });
+ }
+
+ // 화요일 할인 표시
+ if (isTuesday && totalAmount > 0) {
+ summaryDetails.innerHTML += `
+
+ 🌟 화요일 추가 할인
+ -10%
+
+ `;
+ }
+
+ // 배송비 표시
+ summaryDetails.innerHTML += `
+
+ Shipping
+ Free
+
+ `;
+ }
+}
+
// 상품 선택 옵션 렌더링 및 재고 상태 표시
function updateProductOptions() {
let totalStock;
@@ -491,76 +567,9 @@ function calculateCartSummary() {
}
// 장바구니 수량 표시 갱신
document.getElementById('item-count').textContent = `🛍️ ${itemCount} items in cart`;
- // 주문 요약(상품별, 할인, 배송 등) 갱신
- const summaryDetails = document.getElementById('summary-details');
- summaryDetails.innerHTML = '';
-
- if (subTot > 0) {
- for (let i = 0; i < cartItems.length; i++) {
- let curItem;
- for (let j = 0; j < productList.length; j++) {
- if (productList[j].id === cartItems[i].id) {
- curItem = productList[j];
- break;
- }
- }
-
- const qtyElem = cartItems[i].querySelector('.quantity-number');
- const q = parseInt(qtyElem.textContent);
- const itemTotal = curItem.val * q;
-
- summaryDetails.innerHTML += `
-
- ${curItem.name} x ${q}
- ₩${itemTotal.toLocaleString()}
-
- `;
- }
-
- summaryDetails.innerHTML += `
-
-
- Subtotal
- ₩${subTot.toLocaleString()}
-
- `;
- if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
- summaryDetails.innerHTML += `
-
- 🎉 대량구매 할인 (30개 이상)
- -25%
-
- `;
- } else if (itemDiscounts.length > 0) {
- itemDiscounts.forEach(function (item) {
- summaryDetails.innerHTML += `
-
- ${item.name} (10개↑)
- -${item.discount}%
-
- `;
- });
- }
-
- if (isTuesday) {
- if (totalAmount > 0) {
- summaryDetails.innerHTML += `
-
- 🌟 화요일 추가 할인
- -10%
-
- `;
- }
- }
-
- summaryDetails.innerHTML += `
-
- Shipping
- Free
-
- `;
- }
+ // 주문 요약(상품별, 할인, 배송 등) 갱신
+ updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesday, totalAmount);
// 총 결제 금액 표시 갱신
const totalDiv = orderSummaryElement.querySelector('.text-2xl');
if (totalDiv) {
From 665a9ff66313ae7ca359ea589e69cf737caebbfe Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 03:50:10 +0900
Subject: [PATCH 14/46] =?UTF-8?q?refactor:=20=EC=83=81=ED=92=88=20?=
=?UTF-8?q?=ED=95=A0=EC=9D=B8=20&=20=ED=95=A0=EC=9D=B8=20=EC=B4=9D?=
=?UTF-8?q?=ED=95=A9=20=EA=B3=84=EC=82=B0=20=ED=95=A8=EC=88=98=EB=A1=9C=20?=
=?UTF-8?q?=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 59 ++++++++++++++++++++++++++++-------------
1 file changed, 41 insertions(+), 18 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index caeeea6e7..70f63fe51 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -350,6 +350,35 @@ function calculateItemDiscount(productId, quantity) {
return 0;
}
+// 할인 총합 계산 (대량구매 할인 + 화요일 할인)
+function calculateTotalDiscount(subTot, itemCount, currentAmount) {
+ let finalAmount = currentAmount;
+ let discountRate = 0;
+
+ // 대량구매 할인 적용
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ finalAmount = subTot * (1 - DISCOUNT_RATES.BULK_PURCHASE);
+ discountRate = DISCOUNT_RATES.BULK_PURCHASE;
+ } else {
+ discountRate = (subTot - finalAmount) / subTot;
+ }
+
+ // 화요일 할인 적용
+ const today = new Date();
+ const isTuesday = today.getDay() === UI_CONSTANTS.TUESDAY;
+
+ if (isTuesday && finalAmount > 0) {
+ finalAmount = finalAmount * (1 - DISCOUNT_RATES.TUESDAY);
+ discountRate = 1 - finalAmount / subTot;
+ }
+
+ return {
+ finalAmount,
+ discountRate,
+ isTuesday,
+ };
+}
+
// 주문 요약 상세 내역 갱신
function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesday, totalAmount) {
const summaryDetails = document.getElementById('summary-details');
@@ -540,28 +569,21 @@ function calculateCartSummary() {
})();
}
- let discRate = 0;
originalTotal = subTot;
- if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
- totalAmount = subTot * (1 - DISCOUNT_RATES.BULK_PURCHASE);
- discRate = DISCOUNT_RATES.BULK_PURCHASE;
- } else {
- discRate = (subTot - totalAmount) / subTot;
- }
+ // 할인 총합 계산 적용
+ const { finalAmount, discountRate, isTuesday } = calculateTotalDiscount(
+ subTot,
+ itemCount,
+ totalAmount,
+ );
+ totalAmount = finalAmount;
+ const discRate = discountRate;
- const today = new Date();
- const isTuesday = today.getDay() === UI_CONSTANTS.TUESDAY;
+ // 화요일 특별 할인 UI 표시
const tuesdaySpecial = document.getElementById('tuesday-special');
-
- if (isTuesday) {
- if (totalAmount > 0) {
- totalAmount = totalAmount * (1 - DISCOUNT_RATES.TUESDAY);
- discRate = 1 - totalAmount / originalTotal;
- tuesdaySpecial.classList.remove('hidden');
- } else {
- tuesdaySpecial.classList.add('hidden');
- }
+ if (isTuesday && totalAmount > 0) {
+ tuesdaySpecial.classList.remove('hidden');
} else {
tuesdaySpecial.classList.add('hidden');
}
@@ -960,6 +982,7 @@ cartDisplayElement.addEventListener('click', function (event) {
}
if (prod && prod.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
+ // 재고 부족 알림 (필요시 추가 구현)
}
calculateCartSummary();
From c555bd386ca248f0282c8c1f5e72b581fed55a6c Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 04:02:48 +0900
Subject: [PATCH 15/46] =?UTF-8?q?refactor:=20=EC=9E=AC=EA=B3=A0=20?=
=?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EB=B0=8F=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?=
=?UTF-8?q?=EA=B0=B1=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EB=B6=84=EB=A6=AC=20?=
=?UTF-8?q?&=20-=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A?=
=?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=A0=95=EB=A6=AC:=20updateSt?=
=?UTF-8?q?ockInfo,=20getTotalStock,=20getLowStockItems=20=EC=A0=9C?=
=?UTF-8?q?=EA=B1=B0=20-=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20?=
=?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=B3=80=EC=88=98=20=EC=A0=95=EB=A6=AC:?=
=?UTF-8?q?=20idx,=20stockMsg,=20lowStockItems,=20totalCount=20=EC=A0=9C?=
=?UTF-8?q?=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 77 ++++++-----------------------------------
1 file changed, 11 insertions(+), 66 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 70f63fe51..60818aece 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -511,12 +511,10 @@ function updateProductOptions() {
// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
function calculateCartSummary() {
let subTot;
- let idx;
let originalTotal;
let savedAmount;
let points;
let previousCount;
- let stockMsg;
totalAmount = 0;
itemCount = 0;
@@ -524,14 +522,6 @@ function calculateCartSummary() {
const cartItems = cartDisplayElement.children;
subTot = 0;
const itemDiscounts = [];
- const lowStockItems = [];
-
- // 재고 부족 상품 체크
- for (idx = 0; idx < productList.length; idx++) {
- if (productList[idx].q < UI_CONSTANTS.LOW_STOCK_THRESHOLD && productList[idx].q > 0) {
- lowStockItems.push(productList[idx].name);
- }
- }
// 장바구니 내 각 상품별 합계/할인 계산
for (let i = 0; i < cartItems.length; i++) {
@@ -637,18 +627,7 @@ function calculateCartSummary() {
}
}
// 재고 부족/품절 안내 메시지 갱신
- stockMsg = '';
- for (let stockIdx = 0; stockIdx < productList.length; stockIdx++) {
- const item = productList[stockIdx];
- if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
- if (item.q > 0) {
- stockMsg += `${item.name}: 재고 부족 (${item.q}개 남음)\n`;
- } else {
- stockMsg += `${item.name}: 품절\n`;
- }
- }
- }
- stockInfoElement.textContent = stockMsg;
+ updateStockMessages();
renderBonusPoints();
}
@@ -746,58 +725,24 @@ const renderBonusPoints = function () {
}
};
-// 전체 재고 합계 반환
-function getTotalStock() {
- let sum;
- let i;
- let currentProduct;
-
- sum = 0;
- for (i = 0; i < productList.length; i++) {
- currentProduct = productList[i];
- sum += currentProduct.q;
- }
- return sum;
-}
-
-// 재고 부족/품절 안내 메시지 갱신
-const updateStockInfo = function () {
- let infoMsg;
-
- infoMsg = '';
- const totalStock = getTotalStock();
-
- productList.forEach(function (item) {
+// 재고 부족/품절 안내 메시지 생성 및 표시
+function updateStockMessages() {
+ let stockMsg = '';
+ for (let stockIdx = 0; stockIdx < productList.length; stockIdx++) {
+ const item = productList[stockIdx];
if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
if (item.q > 0) {
- infoMsg = `${infoMsg + item.name}: 재고 부족 (${item.q}개 남음)\n`;
+ stockMsg += `${item.name}: 재고 부족 (${item.q}개 남음)\n`;
} else {
- infoMsg = `${infoMsg + item.name}: 품절\n`;
+ stockMsg += `${item.name}: 품절\n`;
}
}
- });
-
- stockInfoElement.textContent = infoMsg;
-};
+ }
+ stockInfoElement.textContent = stockMsg;
+}
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
function updateCartPrices() {
- let totalCount = 0;
- let j = 0;
-
- // 장바구니 내 전체 수량 계산
- while (cartDisplayElement.children[j]) {
- const qty = cartDisplayElement.children[j].querySelector('.quantity-number');
- totalCount += qty ? parseInt(qty.textContent) : 0;
- j++;
- }
-
- for (j = 0; j < cartDisplayElement.children.length; j++) {
- totalCount += parseInt(
- cartDisplayElement.children[j].querySelector('.quantity-number').textContent,
- );
- }
-
const cartItems = cartDisplayElement.children;
// 각 상품별 할인/이름/가격 갱신
From 46b751d894a8cf31c7a2beac612c1e0945ff1b03 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 04:10:23 +0900
Subject: [PATCH 16/46] =?UTF-8?q?refactor:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?=
=?UTF-8?q?=EB=8B=88=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=B2=98=EB=A6=AC=20?=
=?UTF-8?q?=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 100 ++++++++++++++++++++++++----------------
1 file changed, 59 insertions(+), 41 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 60818aece..ba94d5ec9 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -350,6 +350,56 @@ function calculateItemDiscount(productId, quantity) {
return 0;
}
+// 장바구니 내 각 상품별 합계/할인 계산
+function processCartItems(cartItems) {
+ let totalAmount = 0;
+ let itemCount = 0;
+ let subTot = 0;
+ const itemDiscounts = [];
+
+ for (let i = 0; i < cartItems.length; i++) {
+ // 상품 찾기
+ let curItem;
+ for (let j = 0; j < productList.length; j++) {
+ if (productList[j].id === cartItems[i].id) {
+ curItem = productList[j];
+ break;
+ }
+ }
+
+ const qtyElem = cartItems[i].querySelector('.quantity-number');
+ const q = parseInt(qtyElem.textContent);
+ const itemTot = curItem.val * q;
+
+ itemCount += q;
+ subTot += itemTot;
+
+ // UI 스타일 조정 (10개 이상시 볼드 처리)
+ const itemDiv = cartItems[i];
+ const priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
+ priceElems.forEach(function (elem) {
+ if (elem.classList.contains('text-lg')) {
+ elem.style.fontWeight = q >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM ? 'bold' : 'normal';
+ }
+ });
+
+ // 개별 할인 계산
+ const disc = calculateItemDiscount(curItem.id, q);
+ if (disc > 0) {
+ itemDiscounts.push({ name: curItem.name, discount: disc * 100 });
+ }
+
+ totalAmount += itemTot * (1 - disc);
+ }
+
+ return {
+ totalAmount,
+ itemCount,
+ subTot,
+ itemDiscounts,
+ };
+}
+
// 할인 총합 계산 (대량구매 할인 + 화요일 할인)
function calculateTotalDiscount(subTot, itemCount, currentAmount) {
let finalAmount = currentAmount;
@@ -510,56 +560,24 @@ function updateProductOptions() {
// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
function calculateCartSummary() {
- let subTot;
- let originalTotal;
let savedAmount;
let points;
let previousCount;
- totalAmount = 0;
- itemCount = 0;
- originalTotal = totalAmount;
const cartItems = cartDisplayElement.children;
- subTot = 0;
- const itemDiscounts = [];
// 장바구니 내 각 상품별 합계/할인 계산
- for (let i = 0; i < cartItems.length; i++) {
- (function () {
- let curItem;
- for (let j = 0; j < productList.length; j++) {
- if (productList[j].id === cartItems[i].id) {
- curItem = productList[j];
- break;
- }
- }
-
- const qtyElem = cartItems[i].querySelector('.quantity-number');
- const q = parseInt(qtyElem.textContent);
- const itemTot = curItem.val * q;
- let disc = 0;
- itemCount += q;
- subTot += itemTot;
-
- const itemDiv = cartItems[i];
- const priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
- priceElems.forEach(function (elem) {
- if (elem.classList.contains('text-lg')) {
- elem.style.fontWeight = q >= 10 ? 'bold' : 'normal';
- }
- });
-
- // 10개 이상 구매시 개별 할인 적용
- disc = calculateItemDiscount(curItem.id, q);
- if (disc > 0) {
- itemDiscounts.push({ name: curItem.name, discount: disc * 100 });
- }
+ const {
+ totalAmount: calcTotalAmount,
+ itemCount: calcItemCount,
+ subTot,
+ itemDiscounts,
+ } = processCartItems(cartItems);
- totalAmount += itemTot * (1 - disc);
- })();
- }
+ totalAmount = calcTotalAmount;
+ itemCount = calcItemCount;
- originalTotal = subTot;
+ const originalTotal = subTot;
// 할인 총합 계산 적용
const { finalAmount, discountRate, isTuesday } = calculateTotalDiscount(
From eed3b8c1fa5477a5078ac2ee2fd7d246710d2374 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 04:41:11 +0900
Subject: [PATCH 17/46] =?UTF-8?q?refactor:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?=
=?UTF-8?q?=EB=8B=88=20=EB=B0=8F=20=EC=83=81=ED=92=88=20=EA=B4=80=EB=A6=AC?=
=?UTF-8?q?=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=ED=99=94=20=EB=B0=8F?=
=?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20ShoppingCartApp?=
=?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A5=BC=20=EB=8F=84=EC=9E=85?=
=?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=A0=84=EC=97=AD=20=EB=B3=80=EC=88=98?=
=?UTF-8?q?=EB=A5=BC=20=EC=BA=A1=EC=8A=90=ED=99=94=ED=95=98=EA=B3=A0=20?=
=?UTF-8?q?=EC=83=81=ED=92=88=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=A1=9C?=
=?UTF-8?q?=EC=A7=81=EC=9D=84=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20?=
=?UTF-8?q?=EB=B6=84=EB=A6=AC=20-=20=EC=83=81=ED=92=88=20=EA=B4=80?=
=?UTF-8?q?=EB=A0=A8=20=EC=83=81=EC=88=98=20=EB=B0=8F=20=ED=95=A0=EC=9D=B8?=
=?UTF-8?q?,=20=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B3=84=EC=82=B0=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=B3=84=EB=8F=84=EC=9D=98=20?=
=?UTF-8?q?=EB=AA=A8=EB=93=88=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EA=B4=80=EB=A6=AC=20-=20=EC=9E=A5=EB=B0=94?=
=?UTF-8?q?=EA=B5=AC=EB=8B=88=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B0=8F=20?=
=?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4?=
=?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EC=9E=A5?=
=?UTF-8?q?=EB=B0=94=EA=B5=AC=EB=8B=88=20=EC=95=84=EC=9D=B4=ED=85=9C=20?=
=?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EA=B0=9C?=
=?UTF-8?q?=EC=84=A0=20-=20UI=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?=
=?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EC=82=AC?=
=?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4?=
=?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EA=B5=AC=EC=84=B1=ED=95=98=EA=B3=A0=20?=
=?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=94=EC=9D=B8=EB=94=A9=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/constants/DiscountConstants.js | 33 +
src/basic/constants/PointConstants.js | 20 +
src/basic/constants/ProductConstants.js | 35 +
src/basic/constants/UIConstants.js | 31 +
src/basic/constants/index.js | 4 +
src/basic/domains/cart/CartData.js | 193 ++++
src/basic/domains/cart/CartService.js | 186 ++++
src/basic/domains/cart/index.js | 2 +
.../domains/discount/DiscountCalculator.js | 119 +++
src/basic/domains/discount/index.js | 1 +
src/basic/domains/point/PointCalculator.js | 132 +++
src/basic/domains/point/index.js | 1 +
src/basic/domains/product/ProductData.js | 124 +++
src/basic/domains/product/ProductService.js | 144 +++
src/basic/domains/product/index.js | 2 +
src/basic/main.basic.js | 161 ++--
src/basic/refactored/main.refactored.js | 869 ++++++++++++++++++
src/basic/ui/components/HeaderComponent.js | 54 ++
.../ui/components/ProductSelectorComponent.js | 152 +++
src/basic/ui/index.js | 2 +
src/basic/utils/DateUtils.js | 21 +
src/basic/utils/PriceUtils.js | 38 +
src/basic/utils/index.js | 2 +
23 files changed, 2265 insertions(+), 61 deletions(-)
create mode 100644 src/basic/constants/DiscountConstants.js
create mode 100644 src/basic/constants/PointConstants.js
create mode 100644 src/basic/constants/ProductConstants.js
create mode 100644 src/basic/constants/UIConstants.js
create mode 100644 src/basic/constants/index.js
create mode 100644 src/basic/domains/cart/CartData.js
create mode 100644 src/basic/domains/cart/CartService.js
create mode 100644 src/basic/domains/cart/index.js
create mode 100644 src/basic/domains/discount/DiscountCalculator.js
create mode 100644 src/basic/domains/discount/index.js
create mode 100644 src/basic/domains/point/PointCalculator.js
create mode 100644 src/basic/domains/point/index.js
create mode 100644 src/basic/domains/product/ProductData.js
create mode 100644 src/basic/domains/product/ProductService.js
create mode 100644 src/basic/domains/product/index.js
create mode 100644 src/basic/refactored/main.refactored.js
create mode 100644 src/basic/ui/components/HeaderComponent.js
create mode 100644 src/basic/ui/components/ProductSelectorComponent.js
create mode 100644 src/basic/ui/index.js
create mode 100644 src/basic/utils/DateUtils.js
create mode 100644 src/basic/utils/PriceUtils.js
create mode 100644 src/basic/utils/index.js
diff --git a/src/basic/constants/DiscountConstants.js b/src/basic/constants/DiscountConstants.js
new file mode 100644
index 000000000..0cf3e8a36
--- /dev/null
+++ b/src/basic/constants/DiscountConstants.js
@@ -0,0 +1,33 @@
+import { PRODUCT_IDS } from './ProductConstants.js';
+
+// 할인 기준 수량
+export const DISCOUNT_THRESHOLDS = {
+ INDIVIDUAL_ITEM: 10,
+ BULK_PURCHASE: 30,
+};
+
+// 할인율 상수
+export const DISCOUNT_RATES = {
+ // 개별 상품 할인율
+ [PRODUCT_IDS.KEYBOARD]: 0.1,
+ [PRODUCT_IDS.MOUSE]: 0.15,
+ [PRODUCT_IDS.MONITOR_ARM]: 0.2,
+ [PRODUCT_IDS.LAPTOP_CASE]: 0.05,
+ [PRODUCT_IDS.SPEAKER]: 0.25,
+
+ // 특별 할인율
+ BULK_PURCHASE: 0.25,
+ TUESDAY: 0.1,
+ LIGHTNING_SALE: 0.2,
+ RECOMMENDATION: 0.05,
+};
+
+// 할인 타입
+export const DISCOUNT_TYPES = {
+ INDIVIDUAL: 'individual',
+ BULK: 'bulk',
+ TUESDAY: 'tuesday',
+ LIGHTNING: 'lightning',
+ RECOMMENDATION: 'recommendation',
+ SUPER: 'super', // 번개 + 추천 중복
+};
diff --git a/src/basic/constants/PointConstants.js b/src/basic/constants/PointConstants.js
new file mode 100644
index 000000000..150454763
--- /dev/null
+++ b/src/basic/constants/PointConstants.js
@@ -0,0 +1,20 @@
+// 포인트 적립 기준
+export const POINT_RATES = {
+ BASE_RATE: 0.001, // 0.1% (1000원당 1포인트)
+ TUESDAY_MULTIPLIER: 2,
+};
+
+// 포인트 보너스
+export const POINT_BONUS = {
+ SET_BONUS: 50, // 키보드+마우스 세트
+ FULL_SET_BONUS: 100, // 풀세트 (키보드+마우스+모니터암)
+ QUANTITY_BONUS_10: 20, // 10개 이상
+ QUANTITY_BONUS_20: 50, // 20개 이상
+ QUANTITY_BONUS_30: 100, // 30개 이상
+};
+
+// 포인트 적립 조건
+export const POINT_CONDITIONS = {
+ MIN_QUANTITY_FOR_BONUS: 10,
+ MIN_QUANTITY_FOR_SET: 1, // 세트 보너스는 각 상품 1개씩만 있으면 됨
+};
diff --git a/src/basic/constants/ProductConstants.js b/src/basic/constants/ProductConstants.js
new file mode 100644
index 000000000..f71af599e
--- /dev/null
+++ b/src/basic/constants/ProductConstants.js
@@ -0,0 +1,35 @@
+// 상품 ID 상수
+export const PRODUCT_IDS = {
+ KEYBOARD: 'p1',
+ MOUSE: 'p2',
+ MONITOR_ARM: 'p3',
+ LAPTOP_CASE: 'p4',
+ SPEAKER: 'p5',
+};
+
+// 상품 가격 상수
+export const PRODUCT_PRICES = {
+ [PRODUCT_IDS.KEYBOARD]: 10000,
+ [PRODUCT_IDS.MOUSE]: 20000,
+ [PRODUCT_IDS.MONITOR_ARM]: 30000,
+ [PRODUCT_IDS.LAPTOP_CASE]: 15000,
+ [PRODUCT_IDS.SPEAKER]: 25000,
+};
+
+// 상품명 상수
+export const PRODUCT_NAMES = {
+ [PRODUCT_IDS.KEYBOARD]: '버그 없애는 키보드',
+ [PRODUCT_IDS.MOUSE]: '생산성 폭발 마우스',
+ [PRODUCT_IDS.MONITOR_ARM]: '거북목 탈출 모니터암',
+ [PRODUCT_IDS.LAPTOP_CASE]: '에러 방지 노트북 파우치',
+ [PRODUCT_IDS.SPEAKER]: '코딩할 때 듣는 Lo-Fi 스피커',
+};
+
+// 초기 재고 상수
+export const INITIAL_STOCK = {
+ [PRODUCT_IDS.KEYBOARD]: 50,
+ [PRODUCT_IDS.MOUSE]: 30,
+ [PRODUCT_IDS.MONITOR_ARM]: 20,
+ [PRODUCT_IDS.LAPTOP_CASE]: 0,
+ [PRODUCT_IDS.SPEAKER]: 10,
+};
diff --git a/src/basic/constants/UIConstants.js b/src/basic/constants/UIConstants.js
new file mode 100644
index 000000000..9b6e7bcc5
--- /dev/null
+++ b/src/basic/constants/UIConstants.js
@@ -0,0 +1,31 @@
+// UI 관련 상수
+export const UI_CONSTANTS = {
+ // 재고 관련
+ LOW_STOCK_THRESHOLD: 5,
+ TOTAL_STOCK_THRESHOLD: 50,
+
+ // 요일 상수
+ TUESDAY: 2,
+
+ // 타이머 관련
+ LIGHTNING_SALE_INTERVAL: 30000,
+ LIGHTNING_SALE_DELAY: 10000,
+ SUGGEST_SALE_INTERVAL: 60000,
+ SUGGEST_SALE_DELAY: 20000,
+
+ // CSS 클래스명
+ CLASSES: {
+ LOW_STOCK_WARNING: 'text-red-500',
+ SALE_ITEM: 'text-red-500 font-bold',
+ RECOMMENDATION_ITEM: 'text-blue-500 font-bold',
+ SUPER_SALE_ITEM: 'text-purple-600 font-bold',
+ SOLD_OUT_ITEM: 'text-gray-400',
+ },
+
+ // 메시지
+ MESSAGES: {
+ INSUFFICIENT_STOCK: '재고가 부족합니다.',
+ LIGHTNING_SALE: '⚡번개세일! {productName}이(가) 20% 할인 중입니다!',
+ RECOMMENDATION_SALE: '💝 {productName}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!',
+ },
+};
diff --git a/src/basic/constants/index.js b/src/basic/constants/index.js
new file mode 100644
index 000000000..75a518547
--- /dev/null
+++ b/src/basic/constants/index.js
@@ -0,0 +1,4 @@
+export * from './ProductConstants.js';
+export * from './DiscountConstants.js';
+export * from './PointConstants.js';
+export * from './UIConstants.js';
diff --git a/src/basic/domains/cart/CartData.js b/src/basic/domains/cart/CartData.js
new file mode 100644
index 000000000..3aa0dff31
--- /dev/null
+++ b/src/basic/domains/cart/CartData.js
@@ -0,0 +1,193 @@
+/**
+ * 장바구니 아이템 데이터 모델
+ */
+export class CartItem {
+ constructor(product, quantity = 1) {
+ this.id = product.id;
+ this.name = product.name;
+ this.price = product.val;
+ this.originalPrice = product.originalVal;
+ this.quantity = quantity;
+ this.onSale = product.onSale;
+ this.suggestSale = product.suggestSale;
+ }
+
+ /**
+ * 아이템 총 가격 계산
+ * @returns {number} 총 가격
+ */
+ getTotalPrice() {
+ return this.price * this.quantity;
+ }
+
+ /**
+ * 아이템 원래 총 가격 계산
+ * @returns {number} 원래 총 가격
+ */
+ getOriginalTotalPrice() {
+ return this.originalPrice * this.quantity;
+ }
+
+ /**
+ * 할인 금액 계산
+ * @returns {number} 할인 금액
+ */
+ getDiscountAmount() {
+ return this.getOriginalTotalPrice() - this.getTotalPrice();
+ }
+
+ /**
+ * 할인율 계산
+ * @returns {number} 할인율
+ */
+ getDiscountRate() {
+ const originalTotal = this.getOriginalTotalPrice();
+ return originalTotal > 0 ? this.getDiscountAmount() / originalTotal : 0;
+ }
+
+ /**
+ * 수량 증가
+ * @param {number} quantity - 증가할 수량
+ */
+ increaseQuantity(quantity) {
+ this.quantity += quantity;
+ }
+
+ /**
+ * 수량 감소
+ * @param {number} quantity - 감소할 수량
+ */
+ decreaseQuantity(quantity) {
+ this.quantity = Math.max(0, this.quantity - quantity);
+ }
+
+ /**
+ * 할인 상태 업데이트
+ * @param {Object} product - 상품 정보
+ */
+ updateDiscountStatus(product) {
+ this.price = product.val;
+ this.originalPrice = product.originalVal;
+ this.onSale = product.onSale;
+ this.suggestSale = product.suggestSale;
+ }
+}
+
+/**
+ * 장바구니 데이터 모델
+ */
+export class Cart {
+ constructor() {
+ this.items = new Map(); // productId -> CartItem
+ }
+
+ /**
+ * 아이템 추가
+ * @param {Product} product - 상품 정보
+ * @param {number} quantity - 수량
+ * @returns {boolean} 성공 여부
+ */
+ addItem(product, quantity = 1) {
+ if (product.isSoldOut() || product.q < quantity) {
+ return false;
+ }
+
+ const existingItem = this.items.get(product.id);
+ if (existingItem) {
+ existingItem.increaseQuantity(quantity);
+ } else {
+ this.items.set(product.id, new CartItem(product, quantity));
+ }
+
+ return true;
+ }
+
+ /**
+ * 아이템 제거
+ * @param {string} productId - 상품 ID
+ * @returns {boolean} 성공 여부
+ */
+ removeItem(productId) {
+ return this.items.delete(productId);
+ }
+
+ /**
+ * 아이템 수량 변경
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 새로운 수량
+ * @returns {boolean} 성공 여부
+ */
+ updateItemQuantity(productId, quantity) {
+ const item = this.items.get(productId);
+ if (!item) {
+ return false;
+ }
+
+ if (quantity <= 0) {
+ this.removeItem(productId);
+ } else {
+ item.quantity = quantity;
+ }
+
+ return true;
+ }
+
+ /**
+ * 아이템 조회
+ * @param {string} productId - 상품 ID
+ * @returns {CartItem|null} 장바구니 아이템 또는 null
+ */
+ getItem(productId) {
+ return this.items.get(productId) || null;
+ }
+
+ /**
+ * 모든 아이템 조회
+ * @returns {CartItem[]} 장바구니 아이템 목록
+ */
+ getAllItems() {
+ return Array.from(this.items.values());
+ }
+
+ /**
+ * 장바구니 비우기
+ */
+ clear() {
+ this.items.clear();
+ }
+
+ /**
+ * 장바구니가 비어있는지 확인
+ * @returns {boolean} 비어있음 여부
+ */
+ isEmpty() {
+ return this.items.size === 0;
+ }
+
+ /**
+ * 총 아이템 수량 계산
+ * @returns {number} 총 수량
+ */
+ getTotalQuantity() {
+ return Array.from(this.items.values()).reduce((total, item) => total + item.quantity, 0);
+ }
+
+ /**
+ * 총 가격 계산 (할인 적용 전)
+ * @returns {number} 총 가격
+ */
+ getSubtotal() {
+ return Array.from(this.items.values()).reduce((total, item) => total + item.getTotalPrice(), 0);
+ }
+
+ /**
+ * 원래 총 가격 계산 (할인 적용 전)
+ * @returns {number} 원래 총 가격
+ */
+ getOriginalSubtotal() {
+ return Array.from(this.items.values()).reduce(
+ (total, item) => total + item.getOriginalTotalPrice(),
+ 0,
+ );
+ }
+}
diff --git a/src/basic/domains/cart/CartService.js b/src/basic/domains/cart/CartService.js
new file mode 100644
index 000000000..fd333dfa4
--- /dev/null
+++ b/src/basic/domains/cart/CartService.js
@@ -0,0 +1,186 @@
+import { Cart, CartItem } from './CartData.js';
+import { ProductService } from '../product/ProductService.js';
+
+/**
+ * 장바구니 관리 서비스
+ */
+export class CartService {
+ constructor(productService) {
+ this.cart = new Cart();
+ this.productService = productService;
+ }
+
+ /**
+ * 상품을 장바구니에 추가
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 수량
+ * @returns {boolean} 성공 여부
+ */
+ addItemToCart(productId, quantity = 1) {
+ const product = this.productService.getProductById(productId);
+ if (!product) {
+ return false;
+ }
+
+ const success = this.cart.addItem(product, quantity);
+ if (success) {
+ this.productService.decreaseProductStock(productId, quantity);
+ }
+
+ return success;
+ }
+
+ /**
+ * 장바구니에서 상품 제거
+ * @param {string} productId - 상품 ID
+ * @returns {boolean} 성공 여부
+ */
+ removeItemFromCart(productId) {
+ const item = this.cart.getItem(productId);
+ if (!item) {
+ return false;
+ }
+
+ const success = this.cart.removeItem(productId);
+ if (success) {
+ this.productService.increaseProductStock(productId, item.quantity);
+ }
+
+ return success;
+ }
+
+ /**
+ * 장바구니 상품 수량 변경
+ * @param {string} productId - 상품 ID
+ * @param {number} newQuantity - 새로운 수량
+ * @returns {boolean} 성공 여부
+ */
+ updateItemQuantity(productId, newQuantity) {
+ const item = this.cart.getItem(productId);
+ if (!item) {
+ return false;
+ }
+
+ const quantityDifference = newQuantity - item.quantity;
+
+ if (quantityDifference > 0) {
+ // 수량 증가
+ const product = this.productService.getProductById(productId);
+ if (!product || product.q < quantityDifference) {
+ return false;
+ }
+
+ this.productService.decreaseProductStock(productId, quantityDifference);
+ } else if (quantityDifference < 0) {
+ // 수량 감소
+ this.productService.increaseProductStock(productId, Math.abs(quantityDifference));
+ }
+
+ return this.cart.updateItemQuantity(productId, newQuantity);
+ }
+
+ /**
+ * 장바구니 상품 수량 증가
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 증가할 수량
+ * @returns {boolean} 성공 여부
+ */
+ increaseItemQuantity(productId, quantity = 1) {
+ const item = this.cart.getItem(productId);
+ if (!item) {
+ return false;
+ }
+
+ const newQuantity = item.quantity + quantity;
+ return this.updateItemQuantity(productId, newQuantity);
+ }
+
+ /**
+ * 장바구니 상품 수량 감소
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 감소할 수량
+ * @returns {boolean} 성공 여부
+ */
+ decreaseItemQuantity(productId, quantity = 1) {
+ const item = this.cart.getItem(productId);
+ if (!item) {
+ return false;
+ }
+
+ const newQuantity = item.quantity - quantity;
+ return this.updateItemQuantity(productId, newQuantity);
+ }
+
+ /**
+ * 장바구니 아이템 조회
+ * @param {string} productId - 상품 ID
+ * @returns {CartItem|null} 장바구니 아이템 또는 null
+ */
+ getCartItem(productId) {
+ return this.cart.getItem(productId);
+ }
+
+ /**
+ * 모든 장바구니 아이템 조회
+ * @returns {CartItem[]} 장바구니 아이템 목록
+ */
+ getAllCartItems() {
+ return this.cart.getAllItems();
+ }
+
+ /**
+ * 장바구니 총 수량 조회
+ * @returns {number} 총 수량
+ */
+ getTotalQuantity() {
+ return this.cart.getTotalQuantity();
+ }
+
+ /**
+ * 장바구니 소계 조회
+ * @returns {number} 소계
+ */
+ getSubtotal() {
+ return this.cart.getSubtotal();
+ }
+
+ /**
+ * 장바구니 원래 소계 조회
+ * @returns {number} 원래 소계
+ */
+ getOriginalSubtotal() {
+ return this.cart.getOriginalSubtotal();
+ }
+
+ /**
+ * 장바구니가 비어있는지 확인
+ * @returns {boolean} 비어있음 여부
+ */
+ isEmpty() {
+ return this.cart.isEmpty();
+ }
+
+ /**
+ * 장바구니 비우기
+ */
+ clearCart() {
+ // 모든 아이템의 재고를 복원
+ this.cart.getAllItems().forEach((item) => {
+ this.productService.increaseProductStock(item.id, item.quantity);
+ });
+
+ this.cart.clear();
+ }
+
+ /**
+ * 장바구니 아이템 할인 상태 업데이트
+ */
+ updateCartItemDiscounts() {
+ this.cart.getAllItems().forEach((item) => {
+ const product = this.productService.getProductById(item.id);
+ if (product) {
+ item.updateDiscountStatus(product);
+ }
+ });
+ }
+}
diff --git a/src/basic/domains/cart/index.js b/src/basic/domains/cart/index.js
new file mode 100644
index 000000000..8918b52fa
--- /dev/null
+++ b/src/basic/domains/cart/index.js
@@ -0,0 +1,2 @@
+export * from './CartData.js';
+export * from './CartService.js';
diff --git a/src/basic/domains/discount/DiscountCalculator.js b/src/basic/domains/discount/DiscountCalculator.js
new file mode 100644
index 000000000..0eb1f0b57
--- /dev/null
+++ b/src/basic/domains/discount/DiscountCalculator.js
@@ -0,0 +1,119 @@
+import { DISCOUNT_THRESHOLDS, DISCOUNT_RATES } from '../../constants/DiscountConstants.js';
+import { isTuesday } from '../../utils/DateUtils.js';
+
+/**
+ * 할인 계산기
+ */
+export class DiscountCalculator {
+ /**
+ * 개별 상품 할인율 계산
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 수량
+ * @returns {number} 할인율 (0~1)
+ */
+ calculateItemDiscount(productId, quantity) {
+ if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ return 0;
+ }
+
+ return DISCOUNT_RATES[productId] || 0;
+ }
+
+ /**
+ * 대량구매 할인율 계산
+ * @param {number} totalQuantity - 총 수량
+ * @returns {number} 할인율 (0~1)
+ */
+ calculateBulkDiscount(totalQuantity) {
+ return totalQuantity >= DISCOUNT_THRESHOLDS.BULK_PURCHASE ? DISCOUNT_RATES.BULK_PURCHASE : 0;
+ }
+
+ /**
+ * 화요일 할인율 계산
+ * @returns {number} 할인율 (0~1)
+ */
+ calculateTuesdayDiscount() {
+ return isTuesday() ? DISCOUNT_RATES.TUESDAY : 0;
+ }
+
+ /**
+ * 총 할인율 계산
+ * @param {number} subtotal - 소계
+ * @param {number} totalQuantity - 총 수량
+ * @param {number} itemDiscounts - 개별 할인 적용된 금액
+ * @returns {Object} 할인 정보
+ */
+ calculateTotalDiscount(subtotal, totalQuantity, itemDiscounts) {
+ let finalAmount = itemDiscounts;
+ let discountRate = 0;
+
+ // 대량구매 할인 적용
+ const bulkDiscountRate = this.calculateBulkDiscount(totalQuantity);
+ if (bulkDiscountRate > 0) {
+ finalAmount = subtotal * (1 - bulkDiscountRate);
+ discountRate = bulkDiscountRate;
+ } else {
+ discountRate = (subtotal - finalAmount) / subtotal;
+ }
+
+ // 화요일 할인 적용
+ const tuesdayDiscountRate = this.calculateTuesdayDiscount();
+ if (tuesdayDiscountRate > 0 && finalAmount > 0) {
+ finalAmount = finalAmount * (1 - tuesdayDiscountRate);
+ discountRate = 1 - finalAmount / subtotal;
+ }
+
+ return {
+ finalAmount,
+ discountRate,
+ isTuesday: tuesdayDiscountRate > 0,
+ isBulkPurchase: bulkDiscountRate > 0,
+ };
+ }
+
+ /**
+ * 할인 정보 생성
+ * @param {Array} cartItems - 장바구니 아이템 목록
+ * @returns {Object} 할인 정보
+ */
+ generateDiscountInfo(cartItems) {
+ const itemDiscounts = [];
+ let totalAmount = 0;
+ let itemCount = 0;
+ let subtotal = 0;
+
+ cartItems.forEach((item) => {
+ const itemDiscount = this.calculateItemDiscount(item.id, item.quantity);
+ const itemTotal = item.getTotalPrice();
+ const itemOriginalTotal = item.getOriginalTotalPrice();
+
+ itemCount += item.quantity;
+ subtotal += itemOriginalTotal;
+ totalAmount += itemTotal;
+
+ if (itemDiscount > 0) {
+ itemDiscounts.push({
+ name: item.name,
+ discount: itemDiscount * 100,
+ });
+ }
+ });
+
+ const { finalAmount, discountRate, isTuesday, isBulkPurchase } = this.calculateTotalDiscount(
+ subtotal,
+ itemCount,
+ totalAmount,
+ );
+
+ return {
+ finalAmount,
+ discountRate,
+ subtotal,
+ itemCount,
+ itemDiscounts,
+ isTuesday,
+ isBulkPurchase,
+ savedAmount: subtotal - finalAmount,
+ };
+ }
+}
diff --git a/src/basic/domains/discount/index.js b/src/basic/domains/discount/index.js
new file mode 100644
index 000000000..615b974e3
--- /dev/null
+++ b/src/basic/domains/discount/index.js
@@ -0,0 +1 @@
+export * from './DiscountCalculator.js';
diff --git a/src/basic/domains/point/PointCalculator.js b/src/basic/domains/point/PointCalculator.js
new file mode 100644
index 000000000..88051232d
--- /dev/null
+++ b/src/basic/domains/point/PointCalculator.js
@@ -0,0 +1,132 @@
+import {
+ POINT_RATES,
+ POINT_BONUS,
+ POINT_CONDITIONS,
+ PRODUCT_IDS,
+} from '../../constants/PointConstants.js';
+import { isTuesday } from '../../utils/DateUtils.js';
+
+/**
+ * 포인트 계산기
+ */
+export class PointCalculator {
+ /**
+ * 기본 포인트 계산
+ * @param {number} totalAmount - 총 구매액
+ * @returns {number} 기본 포인트
+ */
+ calculateBasePoints(totalAmount) {
+ return Math.floor(totalAmount * POINT_RATES.BASE_RATE);
+ }
+
+ /**
+ * 화요일 2배 포인트 적용
+ * @param {number} basePoints - 기본 포인트
+ * @returns {number} 화요일 적용 포인트
+ */
+ calculateTuesdayBonus(basePoints) {
+ return isTuesday() ? basePoints * POINT_RATES.TUESDAY_MULTIPLIER : basePoints;
+ }
+
+ /**
+ * 세트 보너스 포인트 계산
+ * @param {Array} cartItems - 장바구니 아이템 목록
+ * @returns {number} 세트 보너스 포인트
+ */
+ calculateSetBonus(cartItems) {
+ const hasKeyboard = cartItems.some((item) => item.id === PRODUCT_IDS.KEYBOARD);
+ const hasMouse = cartItems.some((item) => item.id === PRODUCT_IDS.MOUSE);
+ const hasMonitorArm = cartItems.some((item) => item.id === PRODUCT_IDS.MONITOR_ARM);
+
+ let bonus = 0;
+
+ // 키보드+마우스 세트 보너스
+ if (hasKeyboard && hasMouse) {
+ bonus += POINT_BONUS.SET_BONUS;
+ }
+
+ // 풀세트 보너스 (키보드+마우스+모니터암)
+ if (hasKeyboard && hasMouse && hasMonitorArm) {
+ bonus += POINT_BONUS.FULL_SET_BONUS;
+ }
+
+ return bonus;
+ }
+
+ /**
+ * 수량 보너스 포인트 계산
+ * @param {number} totalQuantity - 총 수량
+ * @returns {number} 수량 보너스 포인트
+ */
+ calculateQuantityBonus(totalQuantity) {
+ if (totalQuantity >= 30) {
+ return POINT_BONUS.QUANTITY_BONUS_30;
+ } else if (totalQuantity >= 20) {
+ return POINT_BONUS.QUANTITY_BONUS_20;
+ } else if (totalQuantity >= POINT_CONDITIONS.MIN_QUANTITY_FOR_BONUS) {
+ return POINT_BONUS.QUANTITY_BONUS_10;
+ }
+ return 0;
+ }
+
+ /**
+ * 총 포인트 계산
+ * @param {number} totalAmount - 총 구매액
+ * @param {Array} cartItems - 장바구니 아이템 목록
+ * @returns {Object} 포인트 정보
+ */
+ calculateTotalPoints(totalAmount, cartItems) {
+ const basePoints = this.calculateBasePoints(totalAmount);
+ const tuesdayPoints = this.calculateTuesdayBonus(basePoints);
+ const setBonus = this.calculateSetBonus(cartItems);
+ const quantityBonus = this.calculateQuantityBonus(cartItems.length);
+
+ const totalPoints = tuesdayPoints + setBonus + quantityBonus;
+
+ return {
+ basePoints,
+ tuesdayPoints,
+ setBonus,
+ quantityBonus,
+ totalPoints,
+ isTuesday: isTuesday(),
+ };
+ }
+
+ /**
+ * 포인트 상세 내역 생성
+ * @param {Object} pointInfo - 포인트 정보
+ * @returns {Array} 포인트 상세 내역
+ */
+ generatePointDetails(pointInfo) {
+ const details = [];
+
+ if (pointInfo.basePoints > 0) {
+ details.push(`기본: ${pointInfo.basePoints}p`);
+ }
+
+ if (pointInfo.isTuesday && pointInfo.basePoints > 0) {
+ details.push('화요일 2배');
+ }
+
+ if (pointInfo.setBonus > 0) {
+ if (pointInfo.setBonus >= POINT_BONUS.FULL_SET_BONUS) {
+ details.push('풀세트 구매 +100p');
+ } else {
+ details.push('키보드+마우스 세트 +50p');
+ }
+ }
+
+ if (pointInfo.quantityBonus > 0) {
+ if (pointInfo.quantityBonus >= POINT_BONUS.QUANTITY_BONUS_30) {
+ details.push('대량구매(30개+) +100p');
+ } else if (pointInfo.quantityBonus >= POINT_BONUS.QUANTITY_BONUS_20) {
+ details.push('대량구매(20개+) +50p');
+ } else {
+ details.push('대량구매(10개+) +20p');
+ }
+ }
+
+ return details;
+ }
+}
diff --git a/src/basic/domains/point/index.js b/src/basic/domains/point/index.js
new file mode 100644
index 000000000..94478a9b4
--- /dev/null
+++ b/src/basic/domains/point/index.js
@@ -0,0 +1 @@
+export * from './PointCalculator.js';
diff --git a/src/basic/domains/product/ProductData.js b/src/basic/domains/product/ProductData.js
new file mode 100644
index 000000000..eee654f89
--- /dev/null
+++ b/src/basic/domains/product/ProductData.js
@@ -0,0 +1,124 @@
+import {
+ PRODUCT_IDS,
+ PRODUCT_PRICES,
+ PRODUCT_NAMES,
+ INITIAL_STOCK,
+} from '../../constants/ProductConstants.js';
+
+/**
+ * 상품 데이터 모델
+ */
+export class Product {
+ constructor(id, name, price, stock) {
+ this.id = id;
+ this.name = name;
+ this.val = price; // 현재 가격
+ this.originalVal = price; // 원래 가격
+ this.q = stock; // 재고
+ this.onSale = false; // 번개세일 여부
+ this.suggestSale = false; // 추천할인 여부
+ }
+
+ /**
+ * 할인 적용
+ * @param {number} discountRate - 할인율
+ */
+ applyDiscount(discountRate) {
+ this.val = Math.round(this.originalVal * (1 - discountRate));
+ }
+
+ /**
+ * 할인 해제
+ */
+ removeDiscount() {
+ this.val = this.originalVal;
+ }
+
+ /**
+ * 번개세일 적용
+ */
+ applyLightningSale() {
+ this.onSale = true;
+ this.applyDiscount(0.2);
+ }
+
+ /**
+ * 추천할인 적용
+ */
+ applyRecommendationSale() {
+ this.suggestSale = true;
+ this.applyDiscount(0.05);
+ }
+
+ /**
+ * 재고 감소
+ * @param {number} quantity - 감소할 수량
+ */
+ decreaseStock(quantity) {
+ this.q = Math.max(0, this.q - quantity);
+ }
+
+ /**
+ * 재고 증가
+ * @param {number} quantity - 증가할 수량
+ */
+ increaseStock(quantity) {
+ this.q += quantity;
+ }
+
+ /**
+ * 품절 여부 확인
+ * @returns {boolean} 품절 여부
+ */
+ isSoldOut() {
+ return this.q === 0;
+ }
+
+ /**
+ * 재고 부족 여부 확인
+ * @param {number} threshold - 기준 수량
+ * @returns {boolean} 재고 부족 여부
+ */
+ isLowStock(threshold = 5) {
+ return this.q > 0 && this.q < threshold;
+ }
+}
+
+/**
+ * 상품 데이터 초기화
+ * @returns {Product[]} 상품 목록
+ */
+export function initializeProducts() {
+ return [
+ new Product(
+ PRODUCT_IDS.KEYBOARD,
+ PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD],
+ PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
+ INITIAL_STOCK[PRODUCT_IDS.KEYBOARD],
+ ),
+ new Product(
+ PRODUCT_IDS.MOUSE,
+ PRODUCT_NAMES[PRODUCT_IDS.MOUSE],
+ PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
+ INITIAL_STOCK[PRODUCT_IDS.MOUSE],
+ ),
+ new Product(
+ PRODUCT_IDS.MONITOR_ARM,
+ PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM],
+ PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
+ INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM],
+ ),
+ new Product(
+ PRODUCT_IDS.LAPTOP_CASE,
+ PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE],
+ PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
+ INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE],
+ ),
+ new Product(
+ PRODUCT_IDS.SPEAKER,
+ PRODUCT_NAMES[PRODUCT_IDS.SPEAKER],
+ PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
+ INITIAL_STOCK[PRODUCT_IDS.SPEAKER],
+ ),
+ ];
+}
diff --git a/src/basic/domains/product/ProductService.js b/src/basic/domains/product/ProductService.js
new file mode 100644
index 000000000..777724862
--- /dev/null
+++ b/src/basic/domains/product/ProductService.js
@@ -0,0 +1,144 @@
+import { initializeProducts } from './ProductData.js';
+import { UI_CONSTANTS } from '../../constants/UIConstants.js';
+
+/**
+ * 상품 관리 서비스
+ */
+export class ProductService {
+ constructor() {
+ this.products = initializeProducts();
+ }
+
+ /**
+ * 모든 상품 조회
+ * @returns {Product[]} 상품 목록
+ */
+ getAllProducts() {
+ return this.products;
+ }
+
+ /**
+ * ID로 상품 조회
+ * @param {string} productId - 상품 ID
+ * @returns {Product|null} 상품 객체 또는 null
+ */
+ getProductById(productId) {
+ return this.products.find((product) => product.id === productId) || null;
+ }
+
+ /**
+ * 재고 있는 상품만 조회
+ * @returns {Product[]} 재고 있는 상품 목록
+ */
+ getAvailableProducts() {
+ return this.products.filter((product) => !product.isSoldOut());
+ }
+
+ /**
+ * 재고 부족 상품 조회
+ * @param {number} threshold - 기준 수량
+ * @returns {Product[]} 재고 부족 상품 목록
+ */
+ getLowStockProducts(threshold = UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
+ return this.products.filter((product) => product.isLowStock(threshold));
+ }
+
+ /**
+ * 품절 상품 조회
+ * @returns {Product[]} 품절 상품 목록
+ */
+ getSoldOutProducts() {
+ return this.products.filter((product) => product.isSoldOut());
+ }
+
+ /**
+ * 상품 재고 감소
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 감소할 수량
+ * @returns {boolean} 성공 여부
+ */
+ decreaseProductStock(productId, quantity) {
+ const product = this.getProductById(productId);
+ if (!product || product.q < quantity) {
+ return false;
+ }
+
+ product.decreaseStock(quantity);
+ return true;
+ }
+
+ /**
+ * 상품 재고 증가
+ * @param {string} productId - 상품 ID
+ * @param {number} quantity - 증가할 수량
+ * @returns {boolean} 성공 여부
+ */
+ increaseProductStock(productId, quantity) {
+ const product = this.getProductById(productId);
+ if (!product) {
+ return false;
+ }
+
+ product.increaseStock(quantity);
+ return true;
+ }
+
+ /**
+ * 번개세일 적용
+ * @param {string} productId - 상품 ID
+ * @returns {boolean} 성공 여부
+ */
+ applyLightningSale(productId) {
+ const product = this.getProductById(productId);
+ if (!product || product.isSoldOut() || product.onSale) {
+ return false;
+ }
+
+ product.applyLightningSale();
+ return true;
+ }
+
+ /**
+ * 추천할인 적용
+ * @param {string} productId - 상품 ID
+ * @returns {boolean} 성공 여부
+ */
+ applyRecommendationSale(productId) {
+ const product = this.getProductById(productId);
+ if (!product || product.isSoldOut() || product.suggestSale) {
+ return false;
+ }
+
+ product.applyRecommendationSale();
+ return true;
+ }
+
+ /**
+ * 전체 재고 수량 계산
+ * @returns {number} 전체 재고 수량
+ */
+ getTotalStock() {
+ return this.products.reduce((total, product) => total + product.q, 0);
+ }
+
+ /**
+ * 재고 부족 메시지 생성
+ * @returns {string} 재고 부족 메시지
+ */
+ generateLowStockMessage() {
+ const lowStockProducts = this.getLowStockProducts();
+ const soldOutProducts = this.getSoldOutProducts();
+
+ let message = '';
+
+ lowStockProducts.forEach((product) => {
+ message += `${product.name}: 재고 부족 (${product.q}개 남음)\n`;
+ });
+
+ soldOutProducts.forEach((product) => {
+ message += `${product.name}: 품절\n`;
+ });
+
+ return message;
+ }
+}
diff --git a/src/basic/domains/product/index.js b/src/basic/domains/product/index.js
new file mode 100644
index 000000000..abd9857e8
--- /dev/null
+++ b/src/basic/domains/product/index.js
@@ -0,0 +1,2 @@
+export * from './ProductData.js';
+export * from './ProductService.js';
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index ba94d5ec9..1f5575d58 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -42,7 +42,79 @@ const UI_CONSTANTS = {
SUGGEST_SALE_DELAY: 20000,
};
-// 전역 변수들 (명명 규칙 적용)
+// ShoppingCartApp 클래스 - 전역 변수 캡슐화
+class ShoppingCartApp {
+ constructor() {
+ this.productList = [];
+ this.bonusPoints = 0;
+ this.itemCount = 0;
+ this.lastSelectedProductId = null;
+ this.totalAmount = 0;
+
+ // DOM 요소들
+ this.stockInfoElement = null;
+ this.productSelector = null;
+ this.addToCartButton = null;
+ this.cartDisplayElement = null;
+ this.orderSummaryElement = null;
+ }
+
+ // 상품 정보 초기화
+ initializeProducts() {
+ this.productList = [
+ {
+ id: PRODUCT_IDS.KEYBOARD,
+ name: '버그 없애는 키보드',
+ val: 10000,
+ originalVal: 10000,
+ q: 50,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MOUSE,
+ name: '생산성 폭발 마우스',
+ val: 20000,
+ originalVal: 20000,
+ q: 30,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MONITOR_ARM,
+ name: '거북목 탈출 모니터암',
+ val: 30000,
+ originalVal: 30000,
+ q: 20,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.LAPTOP_CASE,
+ name: '에러 방지 노트북 파우치',
+ val: 15000,
+ originalVal: 15000,
+ q: 0,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.SPEAKER,
+ name: `코딩할 때 듣는 Lo-Fi 스피커`,
+ val: 25000,
+ originalVal: 25000,
+ q: 10,
+ onSale: false,
+ suggestSale: false,
+ },
+ ];
+ }
+}
+
+// 전역 앱 인스턴스
+let app;
+
+// 점진적 리팩토링을 위한 임시 전역 변수들 (추후 제거 예정)
let productList;
let bonusPoints = 0;
let stockInfoElement;
@@ -55,58 +127,22 @@ let cartDisplayElement;
let orderSummaryElement;
function main() {
- totalAmount = 0;
- itemCount = 0;
- lastSelectedProductId = null;
+ // 앱 인스턴스 생성 및 초기화
+ app = new ShoppingCartApp();
+ app.totalAmount = 0;
+ app.itemCount = 0;
+ app.lastSelectedProductId = null;
// 상품 정보 초기화
- productList = [
- {
- id: PRODUCT_IDS.KEYBOARD,
- name: '버그 없애는 키보드',
- val: 10000,
- originalVal: 10000,
- q: 50,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.MOUSE,
- name: '생산성 폭발 마우스',
- val: 20000,
- originalVal: 20000,
- q: 30,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.MONITOR_ARM,
- name: '거북목 탈출 모니터암',
- val: 30000,
- originalVal: 30000,
- q: 20,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.LAPTOP_CASE,
- name: '에러 방지 노트북 파우치',
- val: 15000,
- originalVal: 15000,
- q: 0,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.SPEAKER,
- name: `코딩할 때 듣는 Lo-Fi 스피커`,
- val: 25000,
- originalVal: 25000,
- q: 10,
- onSale: false,
- suggestSale: false,
- },
- ];
+ app.initializeProducts();
+
+ // 기존 전역 변수들을 앱 속성으로 임시 매핑 (점진적 리팩토링)
+ ({ totalAmount, itemCount, lastSelectedProductId, productList } = {
+ totalAmount: app.totalAmount,
+ itemCount: app.itemCount,
+ lastSelectedProductId: app.lastSelectedProductId,
+ productList: app.productList,
+ });
const root = document.getElementById('app');
@@ -118,9 +154,10 @@ function main() {
🛍️ 0 items in cart
`;
- productSelector = document.createElement('select');
- productSelector.id = 'product-select';
- productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+ app.productSelector = document.createElement('select');
+ app.productSelector.id = 'product-select';
+ app.productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+ productSelector = app.productSelector;
const leftColumn = document.createElement('div');
leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
@@ -129,15 +166,17 @@ function main() {
gridContainer.className =
'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
- stockInfoElement = document.createElement('div');
- stockInfoElement.id = 'stock-status';
- stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+ app.stockInfoElement = document.createElement('div');
+ app.stockInfoElement.id = 'stock-status';
+ app.stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+ stockInfoElement = app.stockInfoElement;
- addToCartButton = document.createElement('button');
- addToCartButton.id = 'add-to-cart';
- addToCartButton.innerHTML = 'Add to Cart';
- addToCartButton.className =
+ app.addToCartButton = document.createElement('button');
+ app.addToCartButton.id = 'add-to-cart';
+ app.addToCartButton.innerHTML = 'Add to Cart';
+ app.addToCartButton.className =
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+ addToCartButton = app.addToCartButton;
// 상품 선택/추가/재고 표시 컨테이너
const selectorContainer = document.createElement('div');
diff --git a/src/basic/refactored/main.refactored.js b/src/basic/refactored/main.refactored.js
new file mode 100644
index 000000000..ad7347a85
--- /dev/null
+++ b/src/basic/refactored/main.refactored.js
@@ -0,0 +1,869 @@
+// ============================================================================
+// 상수 정의 (Phase 1)
+// ============================================================================
+
+// 상품 관련 상수
+const PRODUCT_IDS = {
+ KEYBOARD: 'p1',
+ MOUSE: 'p2',
+ MONITOR_ARM: 'p3',
+ LAPTOP_CASE: 'p4',
+ SPEAKER: 'p5',
+};
+
+// 가격 관련 상수
+const PRICES = {
+ KEYBOARD: 10000,
+ MOUSE: 20000,
+ MONITOR_ARM: 30000,
+ LAPTOP_CASE: 15000,
+ SPEAKER: 25000,
+};
+
+// 할인 정책 상수
+const DISCOUNT_RATES = {
+ BULK_PURCHASE_THRESHOLD: 30,
+ BULK_PURCHASE_RATE: 0.25,
+ TUESDAY_RATE: 0.1,
+ LIGHTNING_SALE_RATE: 0.2,
+ RECOMMENDATION_RATE: 0.05,
+ INDIVIDUAL_THRESHOLD: 10,
+};
+
+// 개별 상품 할인율
+const INDIVIDUAL_DISCOUNT_RATES = {
+ [PRODUCT_IDS.KEYBOARD]: 0.1,
+ [PRODUCT_IDS.MOUSE]: 0.15,
+ [PRODUCT_IDS.MONITOR_ARM]: 0.2,
+ [PRODUCT_IDS.LAPTOP_CASE]: 0.05,
+ [PRODUCT_IDS.SPEAKER]: 0.25,
+};
+
+// 시간 관련 상수
+const TIMING = {
+ LIGHTNING_SALE_INTERVAL: 30000,
+ RECOMMENDATION_INTERVAL: 60000,
+ LIGHTNING_SALE_DELAY_MAX: 10000,
+ RECOMMENDATION_DELAY_MAX: 20000,
+};
+
+// UI 관련 상수
+const UI = {
+ LOW_STOCK_THRESHOLD: 5,
+ TOTAL_STOCK_WARNING_THRESHOLD: 50,
+ BORDER_COLOR_WARNING: 'orange',
+};
+
+// 포인트 적립 기준
+const POINT_RATES = {
+ BASE_RATE: 0.001, // 0.1%
+ TUESDAY_MULTIPLIER: 2,
+ SET_BONUS: 50,
+ FULL_SET_BONUS: 100,
+ QUANTITY_BONUS_10: 20,
+ QUANTITY_BONUS_20: 50,
+ QUANTITY_BONUS_30: 100,
+};
+
+// ============================================================================
+// 상품 관리 모듈 (Phase 2)
+// ============================================================================
+
+class ProductService {
+ constructor() {
+ this.products = this.initializeProducts();
+ }
+
+ initializeProducts() {
+ return [
+ this.createProduct(PRODUCT_IDS.KEYBOARD, '버그 없애는 키보드', PRICES.KEYBOARD, 50),
+ this.createProduct(PRODUCT_IDS.MOUSE, '생산성 폭발 마우스', PRICES.MOUSE, 30),
+ this.createProduct(PRODUCT_IDS.MONITOR_ARM, '거북목 탈출 모니터암', PRICES.MONITOR_ARM, 20),
+ this.createProduct(PRODUCT_IDS.LAPTOP_CASE, '에러 방지 노트북 파우치', PRICES.LAPTOP_CASE, 0),
+ this.createProduct(PRODUCT_IDS.SPEAKER, '코딩할 때 듣는 Lo-Fi 스피커', PRICES.SPEAKER, 10),
+ ];
+ }
+
+ createProduct(id, name, originalPrice, initialStock) {
+ return {
+ id,
+ name,
+ price: originalPrice,
+ originalPrice,
+ stock: initialStock,
+ isOnSale: false,
+ isRecommended: false,
+ };
+ }
+
+ getProductById(productId) {
+ return this.products.find((product) => product.id === productId);
+ }
+
+ updateProductStock(productId, quantity) {
+ const product = this.getProductById(productId);
+ if (product) {
+ product.stock -= quantity;
+ }
+ }
+
+ getTotalStock() {
+ return this.products.reduce((total, product) => total + product.stock, 0);
+ }
+
+ getLowStockProducts() {
+ return this.products.filter(
+ (product) => product.stock < UI.LOW_STOCK_THRESHOLD && product.stock > 0,
+ );
+ }
+
+ getOutOfStockProducts() {
+ return this.products.filter((product) => product.stock === 0);
+ }
+
+ applyLightningSale(productId) {
+ const product = this.getProductById(productId);
+ if (product && product.stock > 0 && !product.isOnSale) {
+ product.price = Math.round(product.originalPrice * (1 - DISCOUNT_RATES.LIGHTNING_SALE_RATE));
+ product.isOnSale = true;
+ return true;
+ }
+ return false;
+ }
+
+ applyRecommendationSale(productId) {
+ const product = this.getProductById(productId);
+ if (product && product.stock > 0 && !product.isRecommended) {
+ product.price = Math.round(product.originalPrice * (1 - DISCOUNT_RATES.RECOMMENDATION_RATE));
+ product.isRecommended = true;
+ return true;
+ }
+ return false;
+ }
+}
+
+// ============================================================================
+// 할인 계산 모듈 (Phase 2)
+// ============================================================================
+
+class DiscountCalculator {
+ calculateItemDiscount(product, quantity) {
+ if (quantity < DISCOUNT_RATES.INDIVIDUAL_THRESHOLD) return 0;
+ return INDIVIDUAL_DISCOUNT_RATES[product.id] || 0;
+ }
+
+ calculateBulkDiscount(totalQuantity) {
+ return totalQuantity >= DISCOUNT_RATES.BULK_PURCHASE_THRESHOLD
+ ? DISCOUNT_RATES.BULK_PURCHASE_RATE
+ : 0;
+ }
+
+ calculateTuesdayDiscount() {
+ const today = new Date();
+ return today.getDay() === 2 ? DISCOUNT_RATES.TUESDAY_RATE : 0;
+ }
+
+ calculateTotalDiscount(cartItems, totalQuantity) {
+ const itemDiscounts = cartItems.map((item) =>
+ this.calculateItemDiscount(item.product, item.quantity),
+ );
+
+ const maxItemDiscount = Math.max(...itemDiscounts, 0);
+ const bulkDiscount = this.calculateBulkDiscount(totalQuantity);
+ const tuesdayDiscount = this.calculateTuesdayDiscount();
+
+ // 더 큰 할인율 적용 (개별 vs 전체)
+ const baseDiscount = Math.max(maxItemDiscount, bulkDiscount);
+
+ // 화요일 할인은 중복 적용
+ return baseDiscount + tuesdayDiscount;
+ }
+}
+
+// ============================================================================
+// 포인트 계산 모듈 (Phase 2)
+// ============================================================================
+
+class PointCalculator {
+ calculateBasePoints(totalAmount) {
+ return Math.floor(totalAmount * POINT_RATES.BASE_RATE);
+ }
+
+ isTuesday() {
+ return new Date().getDay() === 2;
+ }
+
+ hasKeyboardAndMouse(cartItems) {
+ const hasKeyboard = cartItems.some((item) => item.product.id === PRODUCT_IDS.KEYBOARD);
+ const hasMouse = cartItems.some((item) => item.product.id === PRODUCT_IDS.MOUSE);
+ return hasKeyboard && hasMouse;
+ }
+
+ hasFullSet(cartItems) {
+ const hasKeyboard = cartItems.some((item) => item.product.id === PRODUCT_IDS.KEYBOARD);
+ const hasMouse = cartItems.some((item) => item.product.id === PRODUCT_IDS.MOUSE);
+ const hasMonitorArm = cartItems.some((item) => item.product.id === PRODUCT_IDS.MONITOR_ARM);
+ return hasKeyboard && hasMouse && hasMonitorArm;
+ }
+
+ calculateQuantityBonus(totalQuantity) {
+ if (totalQuantity >= 30) return POINT_RATES.QUANTITY_BONUS_30;
+ if (totalQuantity >= 20) return POINT_RATES.QUANTITY_BONUS_20;
+ if (totalQuantity >= 10) return POINT_RATES.QUANTITY_BONUS_10;
+ return 0;
+ }
+
+ calculateBonusPoints(cartItems, totalQuantity) {
+ let bonusPoints = 0;
+
+ // 세트 보너스
+ if (this.hasKeyboardAndMouse(cartItems)) {
+ bonusPoints += POINT_RATES.SET_BONUS;
+ }
+
+ if (this.hasFullSet(cartItems)) {
+ bonusPoints += POINT_RATES.FULL_SET_BONUS;
+ }
+
+ // 수량 보너스
+ bonusPoints += this.calculateQuantityBonus(totalQuantity);
+
+ return bonusPoints;
+ }
+
+ calculateTotalPoints(totalAmount, cartItems, totalQuantity) {
+ let basePoints = this.calculateBasePoints(totalAmount);
+
+ // 화요일 2배
+ if (this.isTuesday()) {
+ basePoints *= POINT_RATES.TUESDAY_MULTIPLIER;
+ }
+
+ const bonusPoints = this.calculateBonusPoints(cartItems, totalQuantity);
+
+ return basePoints + bonusPoints;
+ }
+}
+
+// ============================================================================
+// 장바구니 관리 모듈 (Phase 2)
+// ============================================================================
+
+class CartService {
+ constructor() {
+ this.items = [];
+ }
+
+ addItem(product) {
+ const existingItem = this.items.find((item) => item.product.id === product.id);
+
+ if (existingItem) {
+ existingItem.quantity += 1;
+ } else {
+ this.items.push({
+ product,
+ quantity: 1,
+ });
+ }
+ }
+
+ removeItem(productId) {
+ this.items = this.items.filter((item) => item.product.id !== productId);
+ }
+
+ updateItemQuantity(productId, newQuantity) {
+ const item = this.items.find((item) => item.product.id === productId);
+ if (item) {
+ if (newQuantity <= 0) {
+ this.removeItem(productId);
+ } else {
+ item.quantity = newQuantity;
+ }
+ }
+ }
+
+ getItemById(productId) {
+ return this.items.find((item) => item.product.id === productId);
+ }
+
+ getTotalQuantity() {
+ return this.items.reduce((total, item) => total + item.quantity, 0);
+ }
+
+ getTotalAmount() {
+ return this.items.reduce((total, item) => {
+ return total + item.product.price * item.quantity;
+ }, 0);
+ }
+
+ clear() {
+ this.items = [];
+ }
+}
+
+// ============================================================================
+// UI 컴포넌트 (Phase 3)
+// ============================================================================
+
+class HeaderComponent {
+ constructor() {
+ this.element = this.createElement();
+ }
+
+ createElement() {
+ const header = document.createElement('div');
+ header.className = 'mb-8';
+ header.innerHTML = this.getHeaderTemplate();
+ return header;
+ }
+
+ getHeaderTemplate() {
+ return `
+
+ Shopping Cart
+ 🛍️ 0 items in cart
+ `;
+ }
+
+ updateItemCount(count) {
+ const itemCountElement = this.element.querySelector('#item-count');
+ itemCountElement.textContent = `🛍️ ${count} items in cart`;
+ }
+}
+
+class ProductSelectorComponent {
+ constructor(productService, onAddToCart) {
+ this.productService = productService;
+ this.onAddToCart = onAddToCart;
+ this.element = this.createElement();
+ this.bindEvents();
+ }
+
+ createElement() {
+ const container = document.createElement('div');
+ container.className = 'mb-6 pb-6 border-b border-gray-200';
+
+ const select = document.createElement('select');
+ select.id = 'product-select';
+ select.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+
+ const addButton = document.createElement('button');
+ addButton.id = 'add-to-cart';
+ addButton.innerHTML = 'Add to Cart';
+ addButton.className =
+ 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+
+ const stockInfo = document.createElement('div');
+ stockInfo.id = 'stock-status';
+ stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+
+ container.appendChild(select);
+ container.appendChild(addButton);
+ container.appendChild(stockInfo);
+
+ return container;
+ }
+
+ bindEvents() {
+ const addButton = this.element.querySelector('#add-to-cart');
+ addButton.addEventListener('click', () => {
+ const select = this.element.querySelector('#product-select');
+ const selectedProductId = select.value;
+ this.onAddToCart(selectedProductId);
+ });
+ }
+
+ updateOptions() {
+ const select = this.element.querySelector('#product-select');
+ select.innerHTML = '';
+
+ this.productService.products.forEach((product) => {
+ const option = this.createProductOption(product);
+ select.appendChild(option);
+ });
+
+ this.updateBorderColor();
+ }
+
+ createProductOption(product) {
+ const option = document.createElement('option');
+ option.value = product.id;
+
+ const discountText = this.getDiscountText(product);
+ const stockText = product.stock === 0 ? ' (품절)' : '';
+
+ option.textContent = `${product.name} - ${product.price}원${stockText}${discountText}`;
+ option.disabled = product.stock === 0;
+
+ if (product.isOnSale && product.isRecommended) {
+ option.className = 'text-purple-600 font-bold';
+ } else if (product.isOnSale) {
+ option.className = 'text-red-500 font-bold';
+ } else if (product.isRecommended) {
+ option.className = 'text-blue-500 font-bold';
+ }
+
+ return option;
+ }
+
+ getDiscountText(product) {
+ let text = '';
+ if (product.isOnSale) text += ' ⚡SALE';
+ if (product.isRecommended) text += ' 💝추천';
+ return text;
+ }
+
+ updateBorderColor() {
+ const select = this.element.querySelector('#product-select');
+ const totalStock = this.productService.getTotalStock();
+
+ if (totalStock < UI.TOTAL_STOCK_WARNING_THRESHOLD) {
+ select.style.borderColor = UI.BORDER_COLOR_WARNING;
+ } else {
+ select.style.borderColor = '';
+ }
+ }
+
+ updateStockInfo() {
+ const stockInfo = this.element.querySelector('#stock-status');
+ const lowStockProducts = this.productService.getLowStockProducts();
+ const outOfStockProducts = this.productService.getOutOfStockProducts();
+
+ let message = '';
+
+ lowStockProducts.forEach((product) => {
+ message += `${product.name}: 재고 부족 (${product.stock}개 남음)\n`;
+ });
+
+ outOfStockProducts.forEach((product) => {
+ message += `${product.name}: 품절\n`;
+ });
+
+ stockInfo.textContent = message;
+ }
+}
+
+// ============================================================================
+// 메인 애플리케이션 클래스
+// ============================================================================
+
+class ShoppingCartApp {
+ constructor() {
+ this.productService = new ProductService();
+ this.cartService = new CartService();
+ this.discountCalculator = new DiscountCalculator();
+ this.pointCalculator = new PointCalculator();
+
+ this.headerComponent = new HeaderComponent();
+ this.productSelectorComponent = new ProductSelectorComponent(
+ this.productService,
+ this.handleAddToCart.bind(this),
+ );
+
+ this.initializeUI();
+ this.startSpecialSales();
+ }
+
+ initializeUI() {
+ const root = document.getElementById('app');
+
+ // 헤더 추가
+ root.appendChild(this.headerComponent.element);
+
+ // 그리드 컨테이너 생성
+ const gridContainer = this.createGridContainer();
+ root.appendChild(gridContainer);
+
+ // 상품 선택기 추가
+ const leftColumn = gridContainer.querySelector('.left-column');
+ leftColumn.appendChild(this.productSelectorComponent.element);
+
+ // 장바구니 표시 영역 추가
+ const cartDisplay = this.createCartDisplay();
+ leftColumn.appendChild(cartDisplay);
+
+ // 옵션 업데이트
+ this.productSelectorComponent.updateOptions();
+ this.productSelectorComponent.updateStockInfo();
+ }
+
+ createGridContainer() {
+ const gridContainer = document.createElement('div');
+ gridContainer.className =
+ 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
+
+ const leftColumn = document.createElement('div');
+ leftColumn.className = 'bg-white border border-gray-200 p-8 overflow-y-auto';
+
+ const rightColumn = this.createOrderSummary();
+
+ gridContainer.appendChild(leftColumn);
+ gridContainer.appendChild(rightColumn);
+
+ return gridContainer;
+ }
+
+ createOrderSummary() {
+ const rightColumn = document.createElement('div');
+ rightColumn.className = 'bg-black text-white p-8 flex flex-col';
+ rightColumn.innerHTML = this.getOrderSummaryTemplate();
+
+ return rightColumn;
+ }
+
+ getOrderSummaryTemplate() {
+ return `
+
+
+
+
+
+
+
+
+ 🎉
+ Tuesday Special 10% Applied
+
+
+
+
+
+
+ Free shipping on all orders.
+ Earn loyalty points with purchase.
+
+ `;
+ }
+
+ createCartDisplay() {
+ const cartDisplay = document.createElement('div');
+ cartDisplay.id = 'cart-items';
+ return cartDisplay;
+ }
+
+ handleAddToCart(productId) {
+ const product = this.productService.getProductById(productId);
+ if (!product || product.stock <= 0) {
+ return;
+ }
+
+ this.cartService.addItem(product);
+ this.productService.updateProductStock(productId, 1);
+ this.updateUI();
+ }
+
+ updateUI() {
+ this.updateHeader();
+ this.updateCartDisplay();
+ this.updateOrderSummary();
+ this.updateProductSelector();
+ }
+
+ updateHeader() {
+ const totalQuantity = this.cartService.getTotalQuantity();
+ this.headerComponent.updateItemCount(totalQuantity);
+ }
+
+ updateCartDisplay() {
+ const cartDisplay = document.getElementById('cart-items');
+ cartDisplay.innerHTML = '';
+
+ this.cartService.items.forEach((item) => {
+ const cartItemElement = this.createCartItemElement(item);
+ cartDisplay.appendChild(cartItemElement);
+ });
+ }
+
+ createCartItemElement(item) {
+ const itemElement = document.createElement('div');
+ itemElement.id = item.product.id;
+ itemElement.className =
+ 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
+
+ const discountText = this.getProductDiscountText(item.product);
+
+ itemElement.innerHTML = `
+
+
+
${discountText}${
+ item.product.name
+ }
+
PRODUCT
+
${this.getPriceDisplayText(item.product)}
+
+
+ ${
+ item.quantity
+ }
+
+
+
+
+
${this.getPriceDisplayText(
+ item.product,
+ )}
+
Remove
+
+ `;
+
+ this.bindCartItemEvents(itemElement);
+
+ return itemElement;
+ }
+
+ getProductDiscountText(product) {
+ if (product.isOnSale && product.isRecommended) return '⚡💝';
+ if (product.isOnSale) return '⚡';
+ if (product.isRecommended) return '💝';
+ return '';
+ }
+
+ getPriceDisplayText(product) {
+ if (product.isOnSale || product.isRecommended) {
+ const discountClass =
+ product.isOnSale && product.isRecommended
+ ? 'text-purple-600'
+ : product.isOnSale
+ ? 'text-red-500'
+ : 'text-blue-500';
+ return `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ }
+ return `₩${product.price.toLocaleString()}`;
+ }
+
+ bindCartItemEvents(itemElement) {
+ const quantityButtons = itemElement.querySelectorAll('.quantity-change');
+ const removeButton = itemElement.querySelector('.remove-item');
+
+ quantityButtons.forEach((button) => {
+ button.addEventListener('click', (e) => {
+ const {productId} = e.target.dataset;
+ const change = parseInt(e.target.dataset.change);
+ this.handleQuantityChange(productId, change);
+ });
+ });
+
+ removeButton.addEventListener('click', (e) => {
+ const {productId} = e.target.dataset;
+ this.handleRemoveItem(productId);
+ });
+ }
+
+ handleQuantityChange(productId, change) {
+ const cartItem = this.cartService.getItemById(productId);
+ if (!cartItem) return;
+
+ const newQuantity = cartItem.quantity + change;
+ const product = this.productService.getProductById(productId);
+
+ if (newQuantity <= 0) {
+ this.cartService.removeItem(productId);
+ this.productService.updateProductStock(productId, -cartItem.quantity);
+ } else if (newQuantity <= product.stock + cartItem.quantity) {
+ cartItem.quantity = newQuantity;
+ this.productService.updateProductStock(productId, change);
+ } else {
+ alert('재고가 부족합니다.');
+ return;
+ }
+
+ this.updateUI();
+ }
+
+ handleRemoveItem(productId) {
+ const cartItem = this.cartService.getItemById(productId);
+ if (cartItem) {
+ this.productService.updateProductStock(productId, -cartItem.quantity);
+ this.cartService.removeItem(productId);
+ this.updateUI();
+ }
+ }
+
+ updateOrderSummary() {
+ this.updateSummaryDetails();
+ this.updateTotalAmount();
+ this.updateLoyaltyPoints();
+ this.updateDiscountInfo();
+ this.updateTuesdaySpecial();
+ }
+
+ updateSummaryDetails() {
+ const summaryDetails = document.getElementById('summary-details');
+ summaryDetails.innerHTML = '';
+
+ if (this.cartService.items.length === 0) return;
+
+ this.cartService.items.forEach((item) => {
+ const itemTotal = item.product.price * item.quantity;
+ summaryDetails.innerHTML += `
+
+ ${item.product.name} x ${item.quantity}
+ ₩${itemTotal.toLocaleString()}
+
+ `;
+ });
+
+ const subtotal = this.cartService.getTotalAmount();
+ summaryDetails.innerHTML += `
+
+
+ Subtotal
+ ₩${subtotal.toLocaleString()}
+
+ `;
+ }
+
+ updateTotalAmount() {
+ const totalAmount = this.cartService.getTotalAmount();
+ const totalQuantity = this.cartService.getTotalQuantity();
+
+ const discountRate = this.discountCalculator.calculateTotalDiscount(
+ this.cartService.items,
+ totalQuantity,
+ );
+
+ const finalAmount = Math.round(totalAmount * (1 - discountRate));
+
+ const totalElement = document.querySelector('#cart-total .text-2xl');
+ if (totalElement) {
+ totalElement.textContent = `₩${finalAmount.toLocaleString()}`;
+ }
+ }
+
+ updateLoyaltyPoints() {
+ const totalAmount = this.cartService.getTotalAmount();
+ const totalQuantity = this.cartService.getTotalQuantity();
+
+ const discountRate = this.discountCalculator.calculateTotalDiscount(
+ this.cartService.items,
+ totalQuantity,
+ );
+ const finalAmount = Math.round(totalAmount * (1 - discountRate));
+
+ const totalPoints = this.pointCalculator.calculateTotalPoints(
+ finalAmount,
+ this.cartService.items,
+ totalQuantity,
+ );
+
+ const loyaltyPointsElement = document.getElementById('loyalty-points');
+ if (loyaltyPointsElement) {
+ loyaltyPointsElement.textContent = `적립 포인트: ${totalPoints}p`;
+ }
+ }
+
+ updateDiscountInfo() {
+ const discountInfo = document.getElementById('discount-info');
+ const totalAmount = this.cartService.getTotalAmount();
+ const totalQuantity = this.cartService.getTotalQuantity();
+
+ const discountRate = this.discountCalculator.calculateTotalDiscount(
+ this.cartService.items,
+ totalQuantity,
+ );
+
+ if (discountRate > 0 && totalAmount > 0) {
+ const savedAmount = Math.round(totalAmount * discountRate);
+ discountInfo.innerHTML = `
+
+
+ 총 할인율
+ ${(discountRate * 100).toFixed(
+ 1,
+ )}%
+
+
₩${savedAmount.toLocaleString()} 할인되었습니다
+
+ `;
+ } else {
+ discountInfo.innerHTML = '';
+ }
+ }
+
+ updateTuesdaySpecial() {
+ const tuesdaySpecial = document.getElementById('tuesday-special');
+ const isTuesday = new Date().getDay() === 2;
+ const hasItems = this.cartService.items.length > 0;
+
+ if (isTuesday && hasItems) {
+ tuesdaySpecial.classList.remove('hidden');
+ } else {
+ tuesdaySpecial.classList.add('hidden');
+ }
+ }
+
+ updateProductSelector() {
+ this.productSelectorComponent.updateOptions();
+ this.productSelectorComponent.updateStockInfo();
+ }
+
+ startSpecialSales() {
+ // 번개세일 시작
+ const lightningDelay = Math.random() * TIMING.LIGHTNING_SALE_DELAY_MAX;
+ setTimeout(() => {
+ setInterval(() => {
+ const luckyIndex = Math.floor(Math.random() * this.productService.products.length);
+ const luckyProduct = this.productService.products[luckyIndex];
+
+ if (this.productService.applyLightningSale(luckyProduct.id)) {
+ alert(`⚡번개세일! ${luckyProduct.name}이(가) 20% 할인 중입니다!`);
+ this.updateUI();
+ }
+ }, TIMING.LIGHTNING_SALE_INTERVAL);
+ }, lightningDelay);
+
+ // 추천할인 시작
+ const recommendationDelay = Math.random() * TIMING.RECOMMENDATION_DELAY_MAX;
+ setTimeout(() => {
+ setInterval(() => {
+ if (this.cartService.items.length === 0) return;
+
+ const lastSelectedProduct =
+ this.cartService.items[this.cartService.items.length - 1].product;
+ const availableProducts = this.productService.products.filter(
+ (product) =>
+ product.id !== lastSelectedProduct.id && product.stock > 0 && !product.isRecommended,
+ );
+
+ if (availableProducts.length > 0) {
+ const recommendProduct = availableProducts[0];
+ if (this.productService.applyRecommendationSale(recommendProduct.id)) {
+ alert(`💝 ${recommendProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
+ this.updateUI();
+ }
+ }
+ }, TIMING.RECOMMENDATION_INTERVAL);
+ }, recommendationDelay);
+ }
+}
+
+// ============================================================================
+// 애플리케이션 초기화
+// ============================================================================
+
+function initializeApp() {
+ new ShoppingCartApp();
+}
+
+// DOM이 로드된 후 애플리케이션 시작
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeApp);
+} else {
+ initializeApp();
+}
diff --git a/src/basic/ui/components/HeaderComponent.js b/src/basic/ui/components/HeaderComponent.js
new file mode 100644
index 000000000..5abc1321e
--- /dev/null
+++ b/src/basic/ui/components/HeaderComponent.js
@@ -0,0 +1,54 @@
+/**
+ * 헤더 컴포넌트
+ */
+export class HeaderComponent {
+ constructor() {
+ this.element = this.createElement();
+ }
+
+ /**
+ * 헤더 요소 생성
+ * @returns {HTMLElement} 헤더 요소
+ */
+ createElement() {
+ const header = document.createElement('div');
+ header.className = 'mb-8';
+ header.innerHTML = this.getHeaderTemplate();
+ return header;
+ }
+
+ /**
+ * 헤더 템플릿 생성
+ * @returns {string} 헤더 HTML 템플릿
+ */
+ getHeaderTemplate() {
+ return `
+
+ Shopping Cart
+
+ 🛍️ 0 items in cart
+
+ `;
+ }
+
+ /**
+ * 아이템 수량 업데이트
+ * @param {number} count - 아이템 수량
+ */
+ updateItemCount(count) {
+ const itemCountElement = this.element.querySelector('#item-count');
+ if (itemCountElement) {
+ itemCountElement.textContent = `🛍️ ${count} items in cart`;
+ }
+ }
+
+ /**
+ * 헤더 요소 반환
+ * @returns {HTMLElement} 헤더 요소
+ */
+ getElement() {
+ return this.element;
+ }
+}
diff --git a/src/basic/ui/components/ProductSelectorComponent.js b/src/basic/ui/components/ProductSelectorComponent.js
new file mode 100644
index 000000000..0e84f1bda
--- /dev/null
+++ b/src/basic/ui/components/ProductSelectorComponent.js
@@ -0,0 +1,152 @@
+import { formatPrice } from '../../utils/PriceUtils.js';
+import { UI_CONSTANTS } from '../../constants/UIConstants.js';
+
+/**
+ * 상품 선택 컴포넌트
+ */
+export class ProductSelectorComponent {
+ constructor(productService, onProductSelect) {
+ this.productService = productService;
+ this.onProductSelect = onProductSelect;
+ this.element = this.createElement();
+ this.bindEvents();
+ }
+
+ /**
+ * 상품 선택 요소 생성
+ * @returns {HTMLElement} 상품 선택 컨테이너
+ */
+ createElement() {
+ const container = document.createElement('div');
+ container.className = 'mb-6 pb-6 border-b border-gray-200';
+
+ const select = document.createElement('select');
+ select.id = 'product-select';
+ select.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+
+ const addButton = document.createElement('button');
+ addButton.id = 'add-to-cart';
+ addButton.innerHTML = 'Add to Cart';
+ addButton.className =
+ 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+
+ const stockInfo = document.createElement('div');
+ stockInfo.id = 'stock-status';
+ stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+
+ container.appendChild(select);
+ container.appendChild(addButton);
+ container.appendChild(stockInfo);
+
+ return container;
+ }
+
+ /**
+ * 이벤트 바인딩
+ */
+ bindEvents() {
+ const addButton = this.element.querySelector('#add-to-cart');
+ if (addButton) {
+ addButton.addEventListener('click', () => {
+ const select = this.element.querySelector('#product-select');
+ const selectedProductId = select.value;
+
+ if (selectedProductId && this.onProductSelect) {
+ this.onProductSelect(selectedProductId);
+ }
+ });
+ }
+ }
+
+ /**
+ * 상품 옵션 업데이트
+ */
+ updateOptions() {
+ const select = this.element.querySelector('#product-select');
+ if (!select) return;
+
+ select.innerHTML = '';
+ const products = this.productService.getAllProducts();
+
+ products.forEach((product) => {
+ const option = this.createProductOption(product);
+ select.appendChild(option);
+ });
+
+ this.updateStockWarning();
+ }
+
+ /**
+ * 상품 옵션 생성
+ * @param {Object} product - 상품 정보
+ * @returns {HTMLElement} 옵션 요소
+ */
+ createProductOption(product) {
+ const option = document.createElement('option');
+ option.value = product.id;
+
+ const discountText = this.getDiscountText(product);
+ const stockText = product.isSoldOut() ? ' (품절)' : '';
+
+ option.textContent = `${product.name} - ${formatPrice(product.val)}${stockText}${discountText}`;
+ option.disabled = product.isSoldOut();
+
+ if (product.isSoldOut()) {
+ option.className = UI_CONSTANTS.CLASSES.SOLD_OUT_ITEM;
+ } else if (product.onSale && product.suggestSale) {
+ option.className = UI_CONSTANTS.CLASSES.SUPER_SALE_ITEM;
+ } else if (product.onSale) {
+ option.className = UI_CONSTANTS.CLASSES.SALE_ITEM;
+ } else if (product.suggestSale) {
+ option.className = UI_CONSTANTS.CLASSES.RECOMMENDATION_ITEM;
+ }
+
+ return option;
+ }
+
+ /**
+ * 할인 텍스트 생성
+ * @param {Object} product - 상품 정보
+ * @returns {string} 할인 텍스트
+ */
+ getDiscountText(product) {
+ let discountText = '';
+
+ if (product.onSale) discountText += ' ⚡SALE';
+ if (product.suggestSale) discountText += ' 💝추천';
+
+ return discountText;
+ }
+
+ /**
+ * 재고 경고 업데이트
+ */
+ updateStockWarning() {
+ const select = this.element.querySelector('#product-select');
+ const totalStock = this.productService.getTotalStock();
+
+ if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
+ select.style.borderColor = 'orange';
+ } else {
+ select.style.borderColor = '';
+ }
+ }
+
+ /**
+ * 재고 상태 메시지 업데이트
+ */
+ updateStockMessage() {
+ const stockInfo = this.element.querySelector('#stock-status');
+ if (stockInfo) {
+ stockInfo.textContent = this.productService.generateLowStockMessage();
+ }
+ }
+
+ /**
+ * 컴포넌트 요소 반환
+ * @returns {HTMLElement} 컴포넌트 요소
+ */
+ getElement() {
+ return this.element;
+ }
+}
diff --git a/src/basic/ui/index.js b/src/basic/ui/index.js
new file mode 100644
index 000000000..7dcabd873
--- /dev/null
+++ b/src/basic/ui/index.js
@@ -0,0 +1,2 @@
+export * from './components/HeaderComponent.js';
+export * from './components/ProductSelectorComponent.js';
diff --git a/src/basic/utils/DateUtils.js b/src/basic/utils/DateUtils.js
new file mode 100644
index 000000000..bbd1b17a9
--- /dev/null
+++ b/src/basic/utils/DateUtils.js
@@ -0,0 +1,21 @@
+/**
+ * 현재 요일이 화요일인지 확인
+ * @returns {boolean} 화요일 여부
+ */
+export function isTuesday() {
+ const today = new Date();
+ return today.getDay() === 2; // 0: 일요일, 1: 월요일, 2: 화요일
+}
+
+/**
+ * 현재 날짜 정보를 가져옴
+ * @returns {Object} 날짜 정보
+ */
+export function getCurrentDateInfo() {
+ const today = new Date();
+ return {
+ day: today.getDay(),
+ isTuesday: today.getDay() === 2,
+ isWeekend: today.getDay() === 0 || today.getDay() === 6,
+ };
+}
diff --git a/src/basic/utils/PriceUtils.js b/src/basic/utils/PriceUtils.js
new file mode 100644
index 000000000..5a5ba5d63
--- /dev/null
+++ b/src/basic/utils/PriceUtils.js
@@ -0,0 +1,38 @@
+/**
+ * 가격을 한국어 형식으로 포맷팅
+ * @param {number} price - 포맷팅할 가격
+ * @returns {string} 포맷팅된 가격 문자열
+ */
+export function formatPrice(price) {
+ return `₩${price.toLocaleString()}`;
+}
+
+/**
+ * 할인된 가격 계산
+ * @param {number} originalPrice - 원래 가격
+ * @param {number} discountRate - 할인율 (0~1)
+ * @returns {number} 할인된 가격
+ */
+export function calculateDiscountedPrice(originalPrice, discountRate) {
+ return Math.round(originalPrice * (1 - discountRate));
+}
+
+/**
+ * 할인 금액 계산
+ * @param {number} originalPrice - 원래 가격
+ * @param {number} discountedPrice - 할인된 가격
+ * @returns {number} 할인 금액
+ */
+export function calculateDiscountAmount(originalPrice, discountedPrice) {
+ return originalPrice - discountedPrice;
+}
+
+/**
+ * 할인율 계산
+ * @param {number} originalPrice - 원래 가격
+ * @param {number} discountedPrice - 할인된 가격
+ * @returns {number} 할인율 (0~1)
+ */
+export function calculateDiscountRate(originalPrice, discountedPrice) {
+ return (originalPrice - discountedPrice) / originalPrice;
+}
diff --git a/src/basic/utils/index.js b/src/basic/utils/index.js
new file mode 100644
index 000000000..1c3b4e5f4
--- /dev/null
+++ b/src/basic/utils/index.js
@@ -0,0 +1,2 @@
+export * from './DateUtils.js';
+export * from './PriceUtils.js';
From de1f0636ba54db66929f86aa585db0f211e95339 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 05:34:16 +0900
Subject: [PATCH 18/46] =?UTF-8?q?Revert=20"refactor:=20=EC=9E=A5=EB=B0=94?=
=?UTF-8?q?=EA=B5=AC=EB=8B=88=20=EB=B0=8F=20=EC=83=81=ED=92=88=20=EA=B4=80?=
=?UTF-8?q?=EB=A6=AC=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=ED=99=94=20?=
=?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit eed3b8c1fa5477a5078ac2ee2fd7d246710d2374.
---
src/basic/constants/DiscountConstants.js | 33 -
src/basic/constants/PointConstants.js | 20 -
src/basic/constants/ProductConstants.js | 35 -
src/basic/constants/UIConstants.js | 31 -
src/basic/constants/index.js | 4 -
src/basic/domains/cart/CartData.js | 193 ----
src/basic/domains/cart/CartService.js | 186 ----
src/basic/domains/cart/index.js | 2 -
.../domains/discount/DiscountCalculator.js | 119 ---
src/basic/domains/discount/index.js | 1 -
src/basic/domains/point/PointCalculator.js | 132 ---
src/basic/domains/point/index.js | 1 -
src/basic/domains/product/ProductData.js | 124 ---
src/basic/domains/product/ProductService.js | 144 ---
src/basic/domains/product/index.js | 2 -
src/basic/main.basic.js | 161 ++--
src/basic/refactored/main.refactored.js | 869 ------------------
src/basic/ui/components/HeaderComponent.js | 54 --
.../ui/components/ProductSelectorComponent.js | 152 ---
src/basic/ui/index.js | 2 -
src/basic/utils/DateUtils.js | 21 -
src/basic/utils/PriceUtils.js | 38 -
src/basic/utils/index.js | 2 -
23 files changed, 61 insertions(+), 2265 deletions(-)
delete mode 100644 src/basic/constants/DiscountConstants.js
delete mode 100644 src/basic/constants/PointConstants.js
delete mode 100644 src/basic/constants/ProductConstants.js
delete mode 100644 src/basic/constants/UIConstants.js
delete mode 100644 src/basic/constants/index.js
delete mode 100644 src/basic/domains/cart/CartData.js
delete mode 100644 src/basic/domains/cart/CartService.js
delete mode 100644 src/basic/domains/cart/index.js
delete mode 100644 src/basic/domains/discount/DiscountCalculator.js
delete mode 100644 src/basic/domains/discount/index.js
delete mode 100644 src/basic/domains/point/PointCalculator.js
delete mode 100644 src/basic/domains/point/index.js
delete mode 100644 src/basic/domains/product/ProductData.js
delete mode 100644 src/basic/domains/product/ProductService.js
delete mode 100644 src/basic/domains/product/index.js
delete mode 100644 src/basic/refactored/main.refactored.js
delete mode 100644 src/basic/ui/components/HeaderComponent.js
delete mode 100644 src/basic/ui/components/ProductSelectorComponent.js
delete mode 100644 src/basic/ui/index.js
delete mode 100644 src/basic/utils/DateUtils.js
delete mode 100644 src/basic/utils/PriceUtils.js
delete mode 100644 src/basic/utils/index.js
diff --git a/src/basic/constants/DiscountConstants.js b/src/basic/constants/DiscountConstants.js
deleted file mode 100644
index 0cf3e8a36..000000000
--- a/src/basic/constants/DiscountConstants.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { PRODUCT_IDS } from './ProductConstants.js';
-
-// 할인 기준 수량
-export const DISCOUNT_THRESHOLDS = {
- INDIVIDUAL_ITEM: 10,
- BULK_PURCHASE: 30,
-};
-
-// 할인율 상수
-export const DISCOUNT_RATES = {
- // 개별 상품 할인율
- [PRODUCT_IDS.KEYBOARD]: 0.1,
- [PRODUCT_IDS.MOUSE]: 0.15,
- [PRODUCT_IDS.MONITOR_ARM]: 0.2,
- [PRODUCT_IDS.LAPTOP_CASE]: 0.05,
- [PRODUCT_IDS.SPEAKER]: 0.25,
-
- // 특별 할인율
- BULK_PURCHASE: 0.25,
- TUESDAY: 0.1,
- LIGHTNING_SALE: 0.2,
- RECOMMENDATION: 0.05,
-};
-
-// 할인 타입
-export const DISCOUNT_TYPES = {
- INDIVIDUAL: 'individual',
- BULK: 'bulk',
- TUESDAY: 'tuesday',
- LIGHTNING: 'lightning',
- RECOMMENDATION: 'recommendation',
- SUPER: 'super', // 번개 + 추천 중복
-};
diff --git a/src/basic/constants/PointConstants.js b/src/basic/constants/PointConstants.js
deleted file mode 100644
index 150454763..000000000
--- a/src/basic/constants/PointConstants.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// 포인트 적립 기준
-export const POINT_RATES = {
- BASE_RATE: 0.001, // 0.1% (1000원당 1포인트)
- TUESDAY_MULTIPLIER: 2,
-};
-
-// 포인트 보너스
-export const POINT_BONUS = {
- SET_BONUS: 50, // 키보드+마우스 세트
- FULL_SET_BONUS: 100, // 풀세트 (키보드+마우스+모니터암)
- QUANTITY_BONUS_10: 20, // 10개 이상
- QUANTITY_BONUS_20: 50, // 20개 이상
- QUANTITY_BONUS_30: 100, // 30개 이상
-};
-
-// 포인트 적립 조건
-export const POINT_CONDITIONS = {
- MIN_QUANTITY_FOR_BONUS: 10,
- MIN_QUANTITY_FOR_SET: 1, // 세트 보너스는 각 상품 1개씩만 있으면 됨
-};
diff --git a/src/basic/constants/ProductConstants.js b/src/basic/constants/ProductConstants.js
deleted file mode 100644
index f71af599e..000000000
--- a/src/basic/constants/ProductConstants.js
+++ /dev/null
@@ -1,35 +0,0 @@
-// 상품 ID 상수
-export const PRODUCT_IDS = {
- KEYBOARD: 'p1',
- MOUSE: 'p2',
- MONITOR_ARM: 'p3',
- LAPTOP_CASE: 'p4',
- SPEAKER: 'p5',
-};
-
-// 상품 가격 상수
-export const PRODUCT_PRICES = {
- [PRODUCT_IDS.KEYBOARD]: 10000,
- [PRODUCT_IDS.MOUSE]: 20000,
- [PRODUCT_IDS.MONITOR_ARM]: 30000,
- [PRODUCT_IDS.LAPTOP_CASE]: 15000,
- [PRODUCT_IDS.SPEAKER]: 25000,
-};
-
-// 상품명 상수
-export const PRODUCT_NAMES = {
- [PRODUCT_IDS.KEYBOARD]: '버그 없애는 키보드',
- [PRODUCT_IDS.MOUSE]: '생산성 폭발 마우스',
- [PRODUCT_IDS.MONITOR_ARM]: '거북목 탈출 모니터암',
- [PRODUCT_IDS.LAPTOP_CASE]: '에러 방지 노트북 파우치',
- [PRODUCT_IDS.SPEAKER]: '코딩할 때 듣는 Lo-Fi 스피커',
-};
-
-// 초기 재고 상수
-export const INITIAL_STOCK = {
- [PRODUCT_IDS.KEYBOARD]: 50,
- [PRODUCT_IDS.MOUSE]: 30,
- [PRODUCT_IDS.MONITOR_ARM]: 20,
- [PRODUCT_IDS.LAPTOP_CASE]: 0,
- [PRODUCT_IDS.SPEAKER]: 10,
-};
diff --git a/src/basic/constants/UIConstants.js b/src/basic/constants/UIConstants.js
deleted file mode 100644
index 9b6e7bcc5..000000000
--- a/src/basic/constants/UIConstants.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// UI 관련 상수
-export const UI_CONSTANTS = {
- // 재고 관련
- LOW_STOCK_THRESHOLD: 5,
- TOTAL_STOCK_THRESHOLD: 50,
-
- // 요일 상수
- TUESDAY: 2,
-
- // 타이머 관련
- LIGHTNING_SALE_INTERVAL: 30000,
- LIGHTNING_SALE_DELAY: 10000,
- SUGGEST_SALE_INTERVAL: 60000,
- SUGGEST_SALE_DELAY: 20000,
-
- // CSS 클래스명
- CLASSES: {
- LOW_STOCK_WARNING: 'text-red-500',
- SALE_ITEM: 'text-red-500 font-bold',
- RECOMMENDATION_ITEM: 'text-blue-500 font-bold',
- SUPER_SALE_ITEM: 'text-purple-600 font-bold',
- SOLD_OUT_ITEM: 'text-gray-400',
- },
-
- // 메시지
- MESSAGES: {
- INSUFFICIENT_STOCK: '재고가 부족합니다.',
- LIGHTNING_SALE: '⚡번개세일! {productName}이(가) 20% 할인 중입니다!',
- RECOMMENDATION_SALE: '💝 {productName}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!',
- },
-};
diff --git a/src/basic/constants/index.js b/src/basic/constants/index.js
deleted file mode 100644
index 75a518547..000000000
--- a/src/basic/constants/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './ProductConstants.js';
-export * from './DiscountConstants.js';
-export * from './PointConstants.js';
-export * from './UIConstants.js';
diff --git a/src/basic/domains/cart/CartData.js b/src/basic/domains/cart/CartData.js
deleted file mode 100644
index 3aa0dff31..000000000
--- a/src/basic/domains/cart/CartData.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/**
- * 장바구니 아이템 데이터 모델
- */
-export class CartItem {
- constructor(product, quantity = 1) {
- this.id = product.id;
- this.name = product.name;
- this.price = product.val;
- this.originalPrice = product.originalVal;
- this.quantity = quantity;
- this.onSale = product.onSale;
- this.suggestSale = product.suggestSale;
- }
-
- /**
- * 아이템 총 가격 계산
- * @returns {number} 총 가격
- */
- getTotalPrice() {
- return this.price * this.quantity;
- }
-
- /**
- * 아이템 원래 총 가격 계산
- * @returns {number} 원래 총 가격
- */
- getOriginalTotalPrice() {
- return this.originalPrice * this.quantity;
- }
-
- /**
- * 할인 금액 계산
- * @returns {number} 할인 금액
- */
- getDiscountAmount() {
- return this.getOriginalTotalPrice() - this.getTotalPrice();
- }
-
- /**
- * 할인율 계산
- * @returns {number} 할인율
- */
- getDiscountRate() {
- const originalTotal = this.getOriginalTotalPrice();
- return originalTotal > 0 ? this.getDiscountAmount() / originalTotal : 0;
- }
-
- /**
- * 수량 증가
- * @param {number} quantity - 증가할 수량
- */
- increaseQuantity(quantity) {
- this.quantity += quantity;
- }
-
- /**
- * 수량 감소
- * @param {number} quantity - 감소할 수량
- */
- decreaseQuantity(quantity) {
- this.quantity = Math.max(0, this.quantity - quantity);
- }
-
- /**
- * 할인 상태 업데이트
- * @param {Object} product - 상품 정보
- */
- updateDiscountStatus(product) {
- this.price = product.val;
- this.originalPrice = product.originalVal;
- this.onSale = product.onSale;
- this.suggestSale = product.suggestSale;
- }
-}
-
-/**
- * 장바구니 데이터 모델
- */
-export class Cart {
- constructor() {
- this.items = new Map(); // productId -> CartItem
- }
-
- /**
- * 아이템 추가
- * @param {Product} product - 상품 정보
- * @param {number} quantity - 수량
- * @returns {boolean} 성공 여부
- */
- addItem(product, quantity = 1) {
- if (product.isSoldOut() || product.q < quantity) {
- return false;
- }
-
- const existingItem = this.items.get(product.id);
- if (existingItem) {
- existingItem.increaseQuantity(quantity);
- } else {
- this.items.set(product.id, new CartItem(product, quantity));
- }
-
- return true;
- }
-
- /**
- * 아이템 제거
- * @param {string} productId - 상품 ID
- * @returns {boolean} 성공 여부
- */
- removeItem(productId) {
- return this.items.delete(productId);
- }
-
- /**
- * 아이템 수량 변경
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 새로운 수량
- * @returns {boolean} 성공 여부
- */
- updateItemQuantity(productId, quantity) {
- const item = this.items.get(productId);
- if (!item) {
- return false;
- }
-
- if (quantity <= 0) {
- this.removeItem(productId);
- } else {
- item.quantity = quantity;
- }
-
- return true;
- }
-
- /**
- * 아이템 조회
- * @param {string} productId - 상품 ID
- * @returns {CartItem|null} 장바구니 아이템 또는 null
- */
- getItem(productId) {
- return this.items.get(productId) || null;
- }
-
- /**
- * 모든 아이템 조회
- * @returns {CartItem[]} 장바구니 아이템 목록
- */
- getAllItems() {
- return Array.from(this.items.values());
- }
-
- /**
- * 장바구니 비우기
- */
- clear() {
- this.items.clear();
- }
-
- /**
- * 장바구니가 비어있는지 확인
- * @returns {boolean} 비어있음 여부
- */
- isEmpty() {
- return this.items.size === 0;
- }
-
- /**
- * 총 아이템 수량 계산
- * @returns {number} 총 수량
- */
- getTotalQuantity() {
- return Array.from(this.items.values()).reduce((total, item) => total + item.quantity, 0);
- }
-
- /**
- * 총 가격 계산 (할인 적용 전)
- * @returns {number} 총 가격
- */
- getSubtotal() {
- return Array.from(this.items.values()).reduce((total, item) => total + item.getTotalPrice(), 0);
- }
-
- /**
- * 원래 총 가격 계산 (할인 적용 전)
- * @returns {number} 원래 총 가격
- */
- getOriginalSubtotal() {
- return Array.from(this.items.values()).reduce(
- (total, item) => total + item.getOriginalTotalPrice(),
- 0,
- );
- }
-}
diff --git a/src/basic/domains/cart/CartService.js b/src/basic/domains/cart/CartService.js
deleted file mode 100644
index fd333dfa4..000000000
--- a/src/basic/domains/cart/CartService.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import { Cart, CartItem } from './CartData.js';
-import { ProductService } from '../product/ProductService.js';
-
-/**
- * 장바구니 관리 서비스
- */
-export class CartService {
- constructor(productService) {
- this.cart = new Cart();
- this.productService = productService;
- }
-
- /**
- * 상품을 장바구니에 추가
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 수량
- * @returns {boolean} 성공 여부
- */
- addItemToCart(productId, quantity = 1) {
- const product = this.productService.getProductById(productId);
- if (!product) {
- return false;
- }
-
- const success = this.cart.addItem(product, quantity);
- if (success) {
- this.productService.decreaseProductStock(productId, quantity);
- }
-
- return success;
- }
-
- /**
- * 장바구니에서 상품 제거
- * @param {string} productId - 상품 ID
- * @returns {boolean} 성공 여부
- */
- removeItemFromCart(productId) {
- const item = this.cart.getItem(productId);
- if (!item) {
- return false;
- }
-
- const success = this.cart.removeItem(productId);
- if (success) {
- this.productService.increaseProductStock(productId, item.quantity);
- }
-
- return success;
- }
-
- /**
- * 장바구니 상품 수량 변경
- * @param {string} productId - 상품 ID
- * @param {number} newQuantity - 새로운 수량
- * @returns {boolean} 성공 여부
- */
- updateItemQuantity(productId, newQuantity) {
- const item = this.cart.getItem(productId);
- if (!item) {
- return false;
- }
-
- const quantityDifference = newQuantity - item.quantity;
-
- if (quantityDifference > 0) {
- // 수량 증가
- const product = this.productService.getProductById(productId);
- if (!product || product.q < quantityDifference) {
- return false;
- }
-
- this.productService.decreaseProductStock(productId, quantityDifference);
- } else if (quantityDifference < 0) {
- // 수량 감소
- this.productService.increaseProductStock(productId, Math.abs(quantityDifference));
- }
-
- return this.cart.updateItemQuantity(productId, newQuantity);
- }
-
- /**
- * 장바구니 상품 수량 증가
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 증가할 수량
- * @returns {boolean} 성공 여부
- */
- increaseItemQuantity(productId, quantity = 1) {
- const item = this.cart.getItem(productId);
- if (!item) {
- return false;
- }
-
- const newQuantity = item.quantity + quantity;
- return this.updateItemQuantity(productId, newQuantity);
- }
-
- /**
- * 장바구니 상품 수량 감소
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 감소할 수량
- * @returns {boolean} 성공 여부
- */
- decreaseItemQuantity(productId, quantity = 1) {
- const item = this.cart.getItem(productId);
- if (!item) {
- return false;
- }
-
- const newQuantity = item.quantity - quantity;
- return this.updateItemQuantity(productId, newQuantity);
- }
-
- /**
- * 장바구니 아이템 조회
- * @param {string} productId - 상품 ID
- * @returns {CartItem|null} 장바구니 아이템 또는 null
- */
- getCartItem(productId) {
- return this.cart.getItem(productId);
- }
-
- /**
- * 모든 장바구니 아이템 조회
- * @returns {CartItem[]} 장바구니 아이템 목록
- */
- getAllCartItems() {
- return this.cart.getAllItems();
- }
-
- /**
- * 장바구니 총 수량 조회
- * @returns {number} 총 수량
- */
- getTotalQuantity() {
- return this.cart.getTotalQuantity();
- }
-
- /**
- * 장바구니 소계 조회
- * @returns {number} 소계
- */
- getSubtotal() {
- return this.cart.getSubtotal();
- }
-
- /**
- * 장바구니 원래 소계 조회
- * @returns {number} 원래 소계
- */
- getOriginalSubtotal() {
- return this.cart.getOriginalSubtotal();
- }
-
- /**
- * 장바구니가 비어있는지 확인
- * @returns {boolean} 비어있음 여부
- */
- isEmpty() {
- return this.cart.isEmpty();
- }
-
- /**
- * 장바구니 비우기
- */
- clearCart() {
- // 모든 아이템의 재고를 복원
- this.cart.getAllItems().forEach((item) => {
- this.productService.increaseProductStock(item.id, item.quantity);
- });
-
- this.cart.clear();
- }
-
- /**
- * 장바구니 아이템 할인 상태 업데이트
- */
- updateCartItemDiscounts() {
- this.cart.getAllItems().forEach((item) => {
- const product = this.productService.getProductById(item.id);
- if (product) {
- item.updateDiscountStatus(product);
- }
- });
- }
-}
diff --git a/src/basic/domains/cart/index.js b/src/basic/domains/cart/index.js
deleted file mode 100644
index 8918b52fa..000000000
--- a/src/basic/domains/cart/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './CartData.js';
-export * from './CartService.js';
diff --git a/src/basic/domains/discount/DiscountCalculator.js b/src/basic/domains/discount/DiscountCalculator.js
deleted file mode 100644
index 0eb1f0b57..000000000
--- a/src/basic/domains/discount/DiscountCalculator.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { DISCOUNT_THRESHOLDS, DISCOUNT_RATES } from '../../constants/DiscountConstants.js';
-import { isTuesday } from '../../utils/DateUtils.js';
-
-/**
- * 할인 계산기
- */
-export class DiscountCalculator {
- /**
- * 개별 상품 할인율 계산
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 수량
- * @returns {number} 할인율 (0~1)
- */
- calculateItemDiscount(productId, quantity) {
- if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
- return 0;
- }
-
- return DISCOUNT_RATES[productId] || 0;
- }
-
- /**
- * 대량구매 할인율 계산
- * @param {number} totalQuantity - 총 수량
- * @returns {number} 할인율 (0~1)
- */
- calculateBulkDiscount(totalQuantity) {
- return totalQuantity >= DISCOUNT_THRESHOLDS.BULK_PURCHASE ? DISCOUNT_RATES.BULK_PURCHASE : 0;
- }
-
- /**
- * 화요일 할인율 계산
- * @returns {number} 할인율 (0~1)
- */
- calculateTuesdayDiscount() {
- return isTuesday() ? DISCOUNT_RATES.TUESDAY : 0;
- }
-
- /**
- * 총 할인율 계산
- * @param {number} subtotal - 소계
- * @param {number} totalQuantity - 총 수량
- * @param {number} itemDiscounts - 개별 할인 적용된 금액
- * @returns {Object} 할인 정보
- */
- calculateTotalDiscount(subtotal, totalQuantity, itemDiscounts) {
- let finalAmount = itemDiscounts;
- let discountRate = 0;
-
- // 대량구매 할인 적용
- const bulkDiscountRate = this.calculateBulkDiscount(totalQuantity);
- if (bulkDiscountRate > 0) {
- finalAmount = subtotal * (1 - bulkDiscountRate);
- discountRate = bulkDiscountRate;
- } else {
- discountRate = (subtotal - finalAmount) / subtotal;
- }
-
- // 화요일 할인 적용
- const tuesdayDiscountRate = this.calculateTuesdayDiscount();
- if (tuesdayDiscountRate > 0 && finalAmount > 0) {
- finalAmount = finalAmount * (1 - tuesdayDiscountRate);
- discountRate = 1 - finalAmount / subtotal;
- }
-
- return {
- finalAmount,
- discountRate,
- isTuesday: tuesdayDiscountRate > 0,
- isBulkPurchase: bulkDiscountRate > 0,
- };
- }
-
- /**
- * 할인 정보 생성
- * @param {Array} cartItems - 장바구니 아이템 목록
- * @returns {Object} 할인 정보
- */
- generateDiscountInfo(cartItems) {
- const itemDiscounts = [];
- let totalAmount = 0;
- let itemCount = 0;
- let subtotal = 0;
-
- cartItems.forEach((item) => {
- const itemDiscount = this.calculateItemDiscount(item.id, item.quantity);
- const itemTotal = item.getTotalPrice();
- const itemOriginalTotal = item.getOriginalTotalPrice();
-
- itemCount += item.quantity;
- subtotal += itemOriginalTotal;
- totalAmount += itemTotal;
-
- if (itemDiscount > 0) {
- itemDiscounts.push({
- name: item.name,
- discount: itemDiscount * 100,
- });
- }
- });
-
- const { finalAmount, discountRate, isTuesday, isBulkPurchase } = this.calculateTotalDiscount(
- subtotal,
- itemCount,
- totalAmount,
- );
-
- return {
- finalAmount,
- discountRate,
- subtotal,
- itemCount,
- itemDiscounts,
- isTuesday,
- isBulkPurchase,
- savedAmount: subtotal - finalAmount,
- };
- }
-}
diff --git a/src/basic/domains/discount/index.js b/src/basic/domains/discount/index.js
deleted file mode 100644
index 615b974e3..000000000
--- a/src/basic/domains/discount/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './DiscountCalculator.js';
diff --git a/src/basic/domains/point/PointCalculator.js b/src/basic/domains/point/PointCalculator.js
deleted file mode 100644
index 88051232d..000000000
--- a/src/basic/domains/point/PointCalculator.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import {
- POINT_RATES,
- POINT_BONUS,
- POINT_CONDITIONS,
- PRODUCT_IDS,
-} from '../../constants/PointConstants.js';
-import { isTuesday } from '../../utils/DateUtils.js';
-
-/**
- * 포인트 계산기
- */
-export class PointCalculator {
- /**
- * 기본 포인트 계산
- * @param {number} totalAmount - 총 구매액
- * @returns {number} 기본 포인트
- */
- calculateBasePoints(totalAmount) {
- return Math.floor(totalAmount * POINT_RATES.BASE_RATE);
- }
-
- /**
- * 화요일 2배 포인트 적용
- * @param {number} basePoints - 기본 포인트
- * @returns {number} 화요일 적용 포인트
- */
- calculateTuesdayBonus(basePoints) {
- return isTuesday() ? basePoints * POINT_RATES.TUESDAY_MULTIPLIER : basePoints;
- }
-
- /**
- * 세트 보너스 포인트 계산
- * @param {Array} cartItems - 장바구니 아이템 목록
- * @returns {number} 세트 보너스 포인트
- */
- calculateSetBonus(cartItems) {
- const hasKeyboard = cartItems.some((item) => item.id === PRODUCT_IDS.KEYBOARD);
- const hasMouse = cartItems.some((item) => item.id === PRODUCT_IDS.MOUSE);
- const hasMonitorArm = cartItems.some((item) => item.id === PRODUCT_IDS.MONITOR_ARM);
-
- let bonus = 0;
-
- // 키보드+마우스 세트 보너스
- if (hasKeyboard && hasMouse) {
- bonus += POINT_BONUS.SET_BONUS;
- }
-
- // 풀세트 보너스 (키보드+마우스+모니터암)
- if (hasKeyboard && hasMouse && hasMonitorArm) {
- bonus += POINT_BONUS.FULL_SET_BONUS;
- }
-
- return bonus;
- }
-
- /**
- * 수량 보너스 포인트 계산
- * @param {number} totalQuantity - 총 수량
- * @returns {number} 수량 보너스 포인트
- */
- calculateQuantityBonus(totalQuantity) {
- if (totalQuantity >= 30) {
- return POINT_BONUS.QUANTITY_BONUS_30;
- } else if (totalQuantity >= 20) {
- return POINT_BONUS.QUANTITY_BONUS_20;
- } else if (totalQuantity >= POINT_CONDITIONS.MIN_QUANTITY_FOR_BONUS) {
- return POINT_BONUS.QUANTITY_BONUS_10;
- }
- return 0;
- }
-
- /**
- * 총 포인트 계산
- * @param {number} totalAmount - 총 구매액
- * @param {Array} cartItems - 장바구니 아이템 목록
- * @returns {Object} 포인트 정보
- */
- calculateTotalPoints(totalAmount, cartItems) {
- const basePoints = this.calculateBasePoints(totalAmount);
- const tuesdayPoints = this.calculateTuesdayBonus(basePoints);
- const setBonus = this.calculateSetBonus(cartItems);
- const quantityBonus = this.calculateQuantityBonus(cartItems.length);
-
- const totalPoints = tuesdayPoints + setBonus + quantityBonus;
-
- return {
- basePoints,
- tuesdayPoints,
- setBonus,
- quantityBonus,
- totalPoints,
- isTuesday: isTuesday(),
- };
- }
-
- /**
- * 포인트 상세 내역 생성
- * @param {Object} pointInfo - 포인트 정보
- * @returns {Array} 포인트 상세 내역
- */
- generatePointDetails(pointInfo) {
- const details = [];
-
- if (pointInfo.basePoints > 0) {
- details.push(`기본: ${pointInfo.basePoints}p`);
- }
-
- if (pointInfo.isTuesday && pointInfo.basePoints > 0) {
- details.push('화요일 2배');
- }
-
- if (pointInfo.setBonus > 0) {
- if (pointInfo.setBonus >= POINT_BONUS.FULL_SET_BONUS) {
- details.push('풀세트 구매 +100p');
- } else {
- details.push('키보드+마우스 세트 +50p');
- }
- }
-
- if (pointInfo.quantityBonus > 0) {
- if (pointInfo.quantityBonus >= POINT_BONUS.QUANTITY_BONUS_30) {
- details.push('대량구매(30개+) +100p');
- } else if (pointInfo.quantityBonus >= POINT_BONUS.QUANTITY_BONUS_20) {
- details.push('대량구매(20개+) +50p');
- } else {
- details.push('대량구매(10개+) +20p');
- }
- }
-
- return details;
- }
-}
diff --git a/src/basic/domains/point/index.js b/src/basic/domains/point/index.js
deleted file mode 100644
index 94478a9b4..000000000
--- a/src/basic/domains/point/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './PointCalculator.js';
diff --git a/src/basic/domains/product/ProductData.js b/src/basic/domains/product/ProductData.js
deleted file mode 100644
index eee654f89..000000000
--- a/src/basic/domains/product/ProductData.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import {
- PRODUCT_IDS,
- PRODUCT_PRICES,
- PRODUCT_NAMES,
- INITIAL_STOCK,
-} from '../../constants/ProductConstants.js';
-
-/**
- * 상품 데이터 모델
- */
-export class Product {
- constructor(id, name, price, stock) {
- this.id = id;
- this.name = name;
- this.val = price; // 현재 가격
- this.originalVal = price; // 원래 가격
- this.q = stock; // 재고
- this.onSale = false; // 번개세일 여부
- this.suggestSale = false; // 추천할인 여부
- }
-
- /**
- * 할인 적용
- * @param {number} discountRate - 할인율
- */
- applyDiscount(discountRate) {
- this.val = Math.round(this.originalVal * (1 - discountRate));
- }
-
- /**
- * 할인 해제
- */
- removeDiscount() {
- this.val = this.originalVal;
- }
-
- /**
- * 번개세일 적용
- */
- applyLightningSale() {
- this.onSale = true;
- this.applyDiscount(0.2);
- }
-
- /**
- * 추천할인 적용
- */
- applyRecommendationSale() {
- this.suggestSale = true;
- this.applyDiscount(0.05);
- }
-
- /**
- * 재고 감소
- * @param {number} quantity - 감소할 수량
- */
- decreaseStock(quantity) {
- this.q = Math.max(0, this.q - quantity);
- }
-
- /**
- * 재고 증가
- * @param {number} quantity - 증가할 수량
- */
- increaseStock(quantity) {
- this.q += quantity;
- }
-
- /**
- * 품절 여부 확인
- * @returns {boolean} 품절 여부
- */
- isSoldOut() {
- return this.q === 0;
- }
-
- /**
- * 재고 부족 여부 확인
- * @param {number} threshold - 기준 수량
- * @returns {boolean} 재고 부족 여부
- */
- isLowStock(threshold = 5) {
- return this.q > 0 && this.q < threshold;
- }
-}
-
-/**
- * 상품 데이터 초기화
- * @returns {Product[]} 상품 목록
- */
-export function initializeProducts() {
- return [
- new Product(
- PRODUCT_IDS.KEYBOARD,
- PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD],
- PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
- INITIAL_STOCK[PRODUCT_IDS.KEYBOARD],
- ),
- new Product(
- PRODUCT_IDS.MOUSE,
- PRODUCT_NAMES[PRODUCT_IDS.MOUSE],
- PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
- INITIAL_STOCK[PRODUCT_IDS.MOUSE],
- ),
- new Product(
- PRODUCT_IDS.MONITOR_ARM,
- PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM],
- PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
- INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM],
- ),
- new Product(
- PRODUCT_IDS.LAPTOP_CASE,
- PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE],
- PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
- INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE],
- ),
- new Product(
- PRODUCT_IDS.SPEAKER,
- PRODUCT_NAMES[PRODUCT_IDS.SPEAKER],
- PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
- INITIAL_STOCK[PRODUCT_IDS.SPEAKER],
- ),
- ];
-}
diff --git a/src/basic/domains/product/ProductService.js b/src/basic/domains/product/ProductService.js
deleted file mode 100644
index 777724862..000000000
--- a/src/basic/domains/product/ProductService.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import { initializeProducts } from './ProductData.js';
-import { UI_CONSTANTS } from '../../constants/UIConstants.js';
-
-/**
- * 상품 관리 서비스
- */
-export class ProductService {
- constructor() {
- this.products = initializeProducts();
- }
-
- /**
- * 모든 상품 조회
- * @returns {Product[]} 상품 목록
- */
- getAllProducts() {
- return this.products;
- }
-
- /**
- * ID로 상품 조회
- * @param {string} productId - 상품 ID
- * @returns {Product|null} 상품 객체 또는 null
- */
- getProductById(productId) {
- return this.products.find((product) => product.id === productId) || null;
- }
-
- /**
- * 재고 있는 상품만 조회
- * @returns {Product[]} 재고 있는 상품 목록
- */
- getAvailableProducts() {
- return this.products.filter((product) => !product.isSoldOut());
- }
-
- /**
- * 재고 부족 상품 조회
- * @param {number} threshold - 기준 수량
- * @returns {Product[]} 재고 부족 상품 목록
- */
- getLowStockProducts(threshold = UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
- return this.products.filter((product) => product.isLowStock(threshold));
- }
-
- /**
- * 품절 상품 조회
- * @returns {Product[]} 품절 상품 목록
- */
- getSoldOutProducts() {
- return this.products.filter((product) => product.isSoldOut());
- }
-
- /**
- * 상품 재고 감소
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 감소할 수량
- * @returns {boolean} 성공 여부
- */
- decreaseProductStock(productId, quantity) {
- const product = this.getProductById(productId);
- if (!product || product.q < quantity) {
- return false;
- }
-
- product.decreaseStock(quantity);
- return true;
- }
-
- /**
- * 상품 재고 증가
- * @param {string} productId - 상품 ID
- * @param {number} quantity - 증가할 수량
- * @returns {boolean} 성공 여부
- */
- increaseProductStock(productId, quantity) {
- const product = this.getProductById(productId);
- if (!product) {
- return false;
- }
-
- product.increaseStock(quantity);
- return true;
- }
-
- /**
- * 번개세일 적용
- * @param {string} productId - 상품 ID
- * @returns {boolean} 성공 여부
- */
- applyLightningSale(productId) {
- const product = this.getProductById(productId);
- if (!product || product.isSoldOut() || product.onSale) {
- return false;
- }
-
- product.applyLightningSale();
- return true;
- }
-
- /**
- * 추천할인 적용
- * @param {string} productId - 상품 ID
- * @returns {boolean} 성공 여부
- */
- applyRecommendationSale(productId) {
- const product = this.getProductById(productId);
- if (!product || product.isSoldOut() || product.suggestSale) {
- return false;
- }
-
- product.applyRecommendationSale();
- return true;
- }
-
- /**
- * 전체 재고 수량 계산
- * @returns {number} 전체 재고 수량
- */
- getTotalStock() {
- return this.products.reduce((total, product) => total + product.q, 0);
- }
-
- /**
- * 재고 부족 메시지 생성
- * @returns {string} 재고 부족 메시지
- */
- generateLowStockMessage() {
- const lowStockProducts = this.getLowStockProducts();
- const soldOutProducts = this.getSoldOutProducts();
-
- let message = '';
-
- lowStockProducts.forEach((product) => {
- message += `${product.name}: 재고 부족 (${product.q}개 남음)\n`;
- });
-
- soldOutProducts.forEach((product) => {
- message += `${product.name}: 품절\n`;
- });
-
- return message;
- }
-}
diff --git a/src/basic/domains/product/index.js b/src/basic/domains/product/index.js
deleted file mode 100644
index abd9857e8..000000000
--- a/src/basic/domains/product/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './ProductData.js';
-export * from './ProductService.js';
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 1f5575d58..ba94d5ec9 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -42,79 +42,7 @@ const UI_CONSTANTS = {
SUGGEST_SALE_DELAY: 20000,
};
-// ShoppingCartApp 클래스 - 전역 변수 캡슐화
-class ShoppingCartApp {
- constructor() {
- this.productList = [];
- this.bonusPoints = 0;
- this.itemCount = 0;
- this.lastSelectedProductId = null;
- this.totalAmount = 0;
-
- // DOM 요소들
- this.stockInfoElement = null;
- this.productSelector = null;
- this.addToCartButton = null;
- this.cartDisplayElement = null;
- this.orderSummaryElement = null;
- }
-
- // 상품 정보 초기화
- initializeProducts() {
- this.productList = [
- {
- id: PRODUCT_IDS.KEYBOARD,
- name: '버그 없애는 키보드',
- val: 10000,
- originalVal: 10000,
- q: 50,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.MOUSE,
- name: '생산성 폭발 마우스',
- val: 20000,
- originalVal: 20000,
- q: 30,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.MONITOR_ARM,
- name: '거북목 탈출 모니터암',
- val: 30000,
- originalVal: 30000,
- q: 20,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.LAPTOP_CASE,
- name: '에러 방지 노트북 파우치',
- val: 15000,
- originalVal: 15000,
- q: 0,
- onSale: false,
- suggestSale: false,
- },
- {
- id: PRODUCT_IDS.SPEAKER,
- name: `코딩할 때 듣는 Lo-Fi 스피커`,
- val: 25000,
- originalVal: 25000,
- q: 10,
- onSale: false,
- suggestSale: false,
- },
- ];
- }
-}
-
-// 전역 앱 인스턴스
-let app;
-
-// 점진적 리팩토링을 위한 임시 전역 변수들 (추후 제거 예정)
+// 전역 변수들 (명명 규칙 적용)
let productList;
let bonusPoints = 0;
let stockInfoElement;
@@ -127,22 +55,58 @@ let cartDisplayElement;
let orderSummaryElement;
function main() {
- // 앱 인스턴스 생성 및 초기화
- app = new ShoppingCartApp();
- app.totalAmount = 0;
- app.itemCount = 0;
- app.lastSelectedProductId = null;
+ totalAmount = 0;
+ itemCount = 0;
+ lastSelectedProductId = null;
// 상품 정보 초기화
- app.initializeProducts();
-
- // 기존 전역 변수들을 앱 속성으로 임시 매핑 (점진적 리팩토링)
- ({ totalAmount, itemCount, lastSelectedProductId, productList } = {
- totalAmount: app.totalAmount,
- itemCount: app.itemCount,
- lastSelectedProductId: app.lastSelectedProductId,
- productList: app.productList,
- });
+ productList = [
+ {
+ id: PRODUCT_IDS.KEYBOARD,
+ name: '버그 없애는 키보드',
+ val: 10000,
+ originalVal: 10000,
+ q: 50,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MOUSE,
+ name: '생산성 폭발 마우스',
+ val: 20000,
+ originalVal: 20000,
+ q: 30,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MONITOR_ARM,
+ name: '거북목 탈출 모니터암',
+ val: 30000,
+ originalVal: 30000,
+ q: 20,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.LAPTOP_CASE,
+ name: '에러 방지 노트북 파우치',
+ val: 15000,
+ originalVal: 15000,
+ q: 0,
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.SPEAKER,
+ name: `코딩할 때 듣는 Lo-Fi 스피커`,
+ val: 25000,
+ originalVal: 25000,
+ q: 10,
+ onSale: false,
+ suggestSale: false,
+ },
+ ];
const root = document.getElementById('app');
@@ -154,10 +118,9 @@ function main() {
🛍️ 0 items in cart
`;
- app.productSelector = document.createElement('select');
- app.productSelector.id = 'product-select';
- app.productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
- productSelector = app.productSelector;
+ productSelector = document.createElement('select');
+ productSelector.id = 'product-select';
+ productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
const leftColumn = document.createElement('div');
leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
@@ -166,17 +129,15 @@ function main() {
gridContainer.className =
'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
- app.stockInfoElement = document.createElement('div');
- app.stockInfoElement.id = 'stock-status';
- app.stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
- stockInfoElement = app.stockInfoElement;
+ stockInfoElement = document.createElement('div');
+ stockInfoElement.id = 'stock-status';
+ stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
- app.addToCartButton = document.createElement('button');
- app.addToCartButton.id = 'add-to-cart';
- app.addToCartButton.innerHTML = 'Add to Cart';
- app.addToCartButton.className =
+ addToCartButton = document.createElement('button');
+ addToCartButton.id = 'add-to-cart';
+ addToCartButton.innerHTML = 'Add to Cart';
+ addToCartButton.className =
'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
- addToCartButton = app.addToCartButton;
// 상품 선택/추가/재고 표시 컨테이너
const selectorContainer = document.createElement('div');
diff --git a/src/basic/refactored/main.refactored.js b/src/basic/refactored/main.refactored.js
deleted file mode 100644
index ad7347a85..000000000
--- a/src/basic/refactored/main.refactored.js
+++ /dev/null
@@ -1,869 +0,0 @@
-// ============================================================================
-// 상수 정의 (Phase 1)
-// ============================================================================
-
-// 상품 관련 상수
-const PRODUCT_IDS = {
- KEYBOARD: 'p1',
- MOUSE: 'p2',
- MONITOR_ARM: 'p3',
- LAPTOP_CASE: 'p4',
- SPEAKER: 'p5',
-};
-
-// 가격 관련 상수
-const PRICES = {
- KEYBOARD: 10000,
- MOUSE: 20000,
- MONITOR_ARM: 30000,
- LAPTOP_CASE: 15000,
- SPEAKER: 25000,
-};
-
-// 할인 정책 상수
-const DISCOUNT_RATES = {
- BULK_PURCHASE_THRESHOLD: 30,
- BULK_PURCHASE_RATE: 0.25,
- TUESDAY_RATE: 0.1,
- LIGHTNING_SALE_RATE: 0.2,
- RECOMMENDATION_RATE: 0.05,
- INDIVIDUAL_THRESHOLD: 10,
-};
-
-// 개별 상품 할인율
-const INDIVIDUAL_DISCOUNT_RATES = {
- [PRODUCT_IDS.KEYBOARD]: 0.1,
- [PRODUCT_IDS.MOUSE]: 0.15,
- [PRODUCT_IDS.MONITOR_ARM]: 0.2,
- [PRODUCT_IDS.LAPTOP_CASE]: 0.05,
- [PRODUCT_IDS.SPEAKER]: 0.25,
-};
-
-// 시간 관련 상수
-const TIMING = {
- LIGHTNING_SALE_INTERVAL: 30000,
- RECOMMENDATION_INTERVAL: 60000,
- LIGHTNING_SALE_DELAY_MAX: 10000,
- RECOMMENDATION_DELAY_MAX: 20000,
-};
-
-// UI 관련 상수
-const UI = {
- LOW_STOCK_THRESHOLD: 5,
- TOTAL_STOCK_WARNING_THRESHOLD: 50,
- BORDER_COLOR_WARNING: 'orange',
-};
-
-// 포인트 적립 기준
-const POINT_RATES = {
- BASE_RATE: 0.001, // 0.1%
- TUESDAY_MULTIPLIER: 2,
- SET_BONUS: 50,
- FULL_SET_BONUS: 100,
- QUANTITY_BONUS_10: 20,
- QUANTITY_BONUS_20: 50,
- QUANTITY_BONUS_30: 100,
-};
-
-// ============================================================================
-// 상품 관리 모듈 (Phase 2)
-// ============================================================================
-
-class ProductService {
- constructor() {
- this.products = this.initializeProducts();
- }
-
- initializeProducts() {
- return [
- this.createProduct(PRODUCT_IDS.KEYBOARD, '버그 없애는 키보드', PRICES.KEYBOARD, 50),
- this.createProduct(PRODUCT_IDS.MOUSE, '생산성 폭발 마우스', PRICES.MOUSE, 30),
- this.createProduct(PRODUCT_IDS.MONITOR_ARM, '거북목 탈출 모니터암', PRICES.MONITOR_ARM, 20),
- this.createProduct(PRODUCT_IDS.LAPTOP_CASE, '에러 방지 노트북 파우치', PRICES.LAPTOP_CASE, 0),
- this.createProduct(PRODUCT_IDS.SPEAKER, '코딩할 때 듣는 Lo-Fi 스피커', PRICES.SPEAKER, 10),
- ];
- }
-
- createProduct(id, name, originalPrice, initialStock) {
- return {
- id,
- name,
- price: originalPrice,
- originalPrice,
- stock: initialStock,
- isOnSale: false,
- isRecommended: false,
- };
- }
-
- getProductById(productId) {
- return this.products.find((product) => product.id === productId);
- }
-
- updateProductStock(productId, quantity) {
- const product = this.getProductById(productId);
- if (product) {
- product.stock -= quantity;
- }
- }
-
- getTotalStock() {
- return this.products.reduce((total, product) => total + product.stock, 0);
- }
-
- getLowStockProducts() {
- return this.products.filter(
- (product) => product.stock < UI.LOW_STOCK_THRESHOLD && product.stock > 0,
- );
- }
-
- getOutOfStockProducts() {
- return this.products.filter((product) => product.stock === 0);
- }
-
- applyLightningSale(productId) {
- const product = this.getProductById(productId);
- if (product && product.stock > 0 && !product.isOnSale) {
- product.price = Math.round(product.originalPrice * (1 - DISCOUNT_RATES.LIGHTNING_SALE_RATE));
- product.isOnSale = true;
- return true;
- }
- return false;
- }
-
- applyRecommendationSale(productId) {
- const product = this.getProductById(productId);
- if (product && product.stock > 0 && !product.isRecommended) {
- product.price = Math.round(product.originalPrice * (1 - DISCOUNT_RATES.RECOMMENDATION_RATE));
- product.isRecommended = true;
- return true;
- }
- return false;
- }
-}
-
-// ============================================================================
-// 할인 계산 모듈 (Phase 2)
-// ============================================================================
-
-class DiscountCalculator {
- calculateItemDiscount(product, quantity) {
- if (quantity < DISCOUNT_RATES.INDIVIDUAL_THRESHOLD) return 0;
- return INDIVIDUAL_DISCOUNT_RATES[product.id] || 0;
- }
-
- calculateBulkDiscount(totalQuantity) {
- return totalQuantity >= DISCOUNT_RATES.BULK_PURCHASE_THRESHOLD
- ? DISCOUNT_RATES.BULK_PURCHASE_RATE
- : 0;
- }
-
- calculateTuesdayDiscount() {
- const today = new Date();
- return today.getDay() === 2 ? DISCOUNT_RATES.TUESDAY_RATE : 0;
- }
-
- calculateTotalDiscount(cartItems, totalQuantity) {
- const itemDiscounts = cartItems.map((item) =>
- this.calculateItemDiscount(item.product, item.quantity),
- );
-
- const maxItemDiscount = Math.max(...itemDiscounts, 0);
- const bulkDiscount = this.calculateBulkDiscount(totalQuantity);
- const tuesdayDiscount = this.calculateTuesdayDiscount();
-
- // 더 큰 할인율 적용 (개별 vs 전체)
- const baseDiscount = Math.max(maxItemDiscount, bulkDiscount);
-
- // 화요일 할인은 중복 적용
- return baseDiscount + tuesdayDiscount;
- }
-}
-
-// ============================================================================
-// 포인트 계산 모듈 (Phase 2)
-// ============================================================================
-
-class PointCalculator {
- calculateBasePoints(totalAmount) {
- return Math.floor(totalAmount * POINT_RATES.BASE_RATE);
- }
-
- isTuesday() {
- return new Date().getDay() === 2;
- }
-
- hasKeyboardAndMouse(cartItems) {
- const hasKeyboard = cartItems.some((item) => item.product.id === PRODUCT_IDS.KEYBOARD);
- const hasMouse = cartItems.some((item) => item.product.id === PRODUCT_IDS.MOUSE);
- return hasKeyboard && hasMouse;
- }
-
- hasFullSet(cartItems) {
- const hasKeyboard = cartItems.some((item) => item.product.id === PRODUCT_IDS.KEYBOARD);
- const hasMouse = cartItems.some((item) => item.product.id === PRODUCT_IDS.MOUSE);
- const hasMonitorArm = cartItems.some((item) => item.product.id === PRODUCT_IDS.MONITOR_ARM);
- return hasKeyboard && hasMouse && hasMonitorArm;
- }
-
- calculateQuantityBonus(totalQuantity) {
- if (totalQuantity >= 30) return POINT_RATES.QUANTITY_BONUS_30;
- if (totalQuantity >= 20) return POINT_RATES.QUANTITY_BONUS_20;
- if (totalQuantity >= 10) return POINT_RATES.QUANTITY_BONUS_10;
- return 0;
- }
-
- calculateBonusPoints(cartItems, totalQuantity) {
- let bonusPoints = 0;
-
- // 세트 보너스
- if (this.hasKeyboardAndMouse(cartItems)) {
- bonusPoints += POINT_RATES.SET_BONUS;
- }
-
- if (this.hasFullSet(cartItems)) {
- bonusPoints += POINT_RATES.FULL_SET_BONUS;
- }
-
- // 수량 보너스
- bonusPoints += this.calculateQuantityBonus(totalQuantity);
-
- return bonusPoints;
- }
-
- calculateTotalPoints(totalAmount, cartItems, totalQuantity) {
- let basePoints = this.calculateBasePoints(totalAmount);
-
- // 화요일 2배
- if (this.isTuesday()) {
- basePoints *= POINT_RATES.TUESDAY_MULTIPLIER;
- }
-
- const bonusPoints = this.calculateBonusPoints(cartItems, totalQuantity);
-
- return basePoints + bonusPoints;
- }
-}
-
-// ============================================================================
-// 장바구니 관리 모듈 (Phase 2)
-// ============================================================================
-
-class CartService {
- constructor() {
- this.items = [];
- }
-
- addItem(product) {
- const existingItem = this.items.find((item) => item.product.id === product.id);
-
- if (existingItem) {
- existingItem.quantity += 1;
- } else {
- this.items.push({
- product,
- quantity: 1,
- });
- }
- }
-
- removeItem(productId) {
- this.items = this.items.filter((item) => item.product.id !== productId);
- }
-
- updateItemQuantity(productId, newQuantity) {
- const item = this.items.find((item) => item.product.id === productId);
- if (item) {
- if (newQuantity <= 0) {
- this.removeItem(productId);
- } else {
- item.quantity = newQuantity;
- }
- }
- }
-
- getItemById(productId) {
- return this.items.find((item) => item.product.id === productId);
- }
-
- getTotalQuantity() {
- return this.items.reduce((total, item) => total + item.quantity, 0);
- }
-
- getTotalAmount() {
- return this.items.reduce((total, item) => {
- return total + item.product.price * item.quantity;
- }, 0);
- }
-
- clear() {
- this.items = [];
- }
-}
-
-// ============================================================================
-// UI 컴포넌트 (Phase 3)
-// ============================================================================
-
-class HeaderComponent {
- constructor() {
- this.element = this.createElement();
- }
-
- createElement() {
- const header = document.createElement('div');
- header.className = 'mb-8';
- header.innerHTML = this.getHeaderTemplate();
- return header;
- }
-
- getHeaderTemplate() {
- return `
-
- Shopping Cart
- 🛍️ 0 items in cart
- `;
- }
-
- updateItemCount(count) {
- const itemCountElement = this.element.querySelector('#item-count');
- itemCountElement.textContent = `🛍️ ${count} items in cart`;
- }
-}
-
-class ProductSelectorComponent {
- constructor(productService, onAddToCart) {
- this.productService = productService;
- this.onAddToCart = onAddToCart;
- this.element = this.createElement();
- this.bindEvents();
- }
-
- createElement() {
- const container = document.createElement('div');
- container.className = 'mb-6 pb-6 border-b border-gray-200';
-
- const select = document.createElement('select');
- select.id = 'product-select';
- select.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
-
- const addButton = document.createElement('button');
- addButton.id = 'add-to-cart';
- addButton.innerHTML = 'Add to Cart';
- addButton.className =
- 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
-
- const stockInfo = document.createElement('div');
- stockInfo.id = 'stock-status';
- stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
-
- container.appendChild(select);
- container.appendChild(addButton);
- container.appendChild(stockInfo);
-
- return container;
- }
-
- bindEvents() {
- const addButton = this.element.querySelector('#add-to-cart');
- addButton.addEventListener('click', () => {
- const select = this.element.querySelector('#product-select');
- const selectedProductId = select.value;
- this.onAddToCart(selectedProductId);
- });
- }
-
- updateOptions() {
- const select = this.element.querySelector('#product-select');
- select.innerHTML = '';
-
- this.productService.products.forEach((product) => {
- const option = this.createProductOption(product);
- select.appendChild(option);
- });
-
- this.updateBorderColor();
- }
-
- createProductOption(product) {
- const option = document.createElement('option');
- option.value = product.id;
-
- const discountText = this.getDiscountText(product);
- const stockText = product.stock === 0 ? ' (품절)' : '';
-
- option.textContent = `${product.name} - ${product.price}원${stockText}${discountText}`;
- option.disabled = product.stock === 0;
-
- if (product.isOnSale && product.isRecommended) {
- option.className = 'text-purple-600 font-bold';
- } else if (product.isOnSale) {
- option.className = 'text-red-500 font-bold';
- } else if (product.isRecommended) {
- option.className = 'text-blue-500 font-bold';
- }
-
- return option;
- }
-
- getDiscountText(product) {
- let text = '';
- if (product.isOnSale) text += ' ⚡SALE';
- if (product.isRecommended) text += ' 💝추천';
- return text;
- }
-
- updateBorderColor() {
- const select = this.element.querySelector('#product-select');
- const totalStock = this.productService.getTotalStock();
-
- if (totalStock < UI.TOTAL_STOCK_WARNING_THRESHOLD) {
- select.style.borderColor = UI.BORDER_COLOR_WARNING;
- } else {
- select.style.borderColor = '';
- }
- }
-
- updateStockInfo() {
- const stockInfo = this.element.querySelector('#stock-status');
- const lowStockProducts = this.productService.getLowStockProducts();
- const outOfStockProducts = this.productService.getOutOfStockProducts();
-
- let message = '';
-
- lowStockProducts.forEach((product) => {
- message += `${product.name}: 재고 부족 (${product.stock}개 남음)\n`;
- });
-
- outOfStockProducts.forEach((product) => {
- message += `${product.name}: 품절\n`;
- });
-
- stockInfo.textContent = message;
- }
-}
-
-// ============================================================================
-// 메인 애플리케이션 클래스
-// ============================================================================
-
-class ShoppingCartApp {
- constructor() {
- this.productService = new ProductService();
- this.cartService = new CartService();
- this.discountCalculator = new DiscountCalculator();
- this.pointCalculator = new PointCalculator();
-
- this.headerComponent = new HeaderComponent();
- this.productSelectorComponent = new ProductSelectorComponent(
- this.productService,
- this.handleAddToCart.bind(this),
- );
-
- this.initializeUI();
- this.startSpecialSales();
- }
-
- initializeUI() {
- const root = document.getElementById('app');
-
- // 헤더 추가
- root.appendChild(this.headerComponent.element);
-
- // 그리드 컨테이너 생성
- const gridContainer = this.createGridContainer();
- root.appendChild(gridContainer);
-
- // 상품 선택기 추가
- const leftColumn = gridContainer.querySelector('.left-column');
- leftColumn.appendChild(this.productSelectorComponent.element);
-
- // 장바구니 표시 영역 추가
- const cartDisplay = this.createCartDisplay();
- leftColumn.appendChild(cartDisplay);
-
- // 옵션 업데이트
- this.productSelectorComponent.updateOptions();
- this.productSelectorComponent.updateStockInfo();
- }
-
- createGridContainer() {
- const gridContainer = document.createElement('div');
- gridContainer.className =
- 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
-
- const leftColumn = document.createElement('div');
- leftColumn.className = 'bg-white border border-gray-200 p-8 overflow-y-auto';
-
- const rightColumn = this.createOrderSummary();
-
- gridContainer.appendChild(leftColumn);
- gridContainer.appendChild(rightColumn);
-
- return gridContainer;
- }
-
- createOrderSummary() {
- const rightColumn = document.createElement('div');
- rightColumn.className = 'bg-black text-white p-8 flex flex-col';
- rightColumn.innerHTML = this.getOrderSummaryTemplate();
-
- return rightColumn;
- }
-
- getOrderSummaryTemplate() {
- return `
-
-
-
-
-
-
-
-
- 🎉
- Tuesday Special 10% Applied
-
-
-
-
-
-
- Free shipping on all orders.
- Earn loyalty points with purchase.
-
- `;
- }
-
- createCartDisplay() {
- const cartDisplay = document.createElement('div');
- cartDisplay.id = 'cart-items';
- return cartDisplay;
- }
-
- handleAddToCart(productId) {
- const product = this.productService.getProductById(productId);
- if (!product || product.stock <= 0) {
- return;
- }
-
- this.cartService.addItem(product);
- this.productService.updateProductStock(productId, 1);
- this.updateUI();
- }
-
- updateUI() {
- this.updateHeader();
- this.updateCartDisplay();
- this.updateOrderSummary();
- this.updateProductSelector();
- }
-
- updateHeader() {
- const totalQuantity = this.cartService.getTotalQuantity();
- this.headerComponent.updateItemCount(totalQuantity);
- }
-
- updateCartDisplay() {
- const cartDisplay = document.getElementById('cart-items');
- cartDisplay.innerHTML = '';
-
- this.cartService.items.forEach((item) => {
- const cartItemElement = this.createCartItemElement(item);
- cartDisplay.appendChild(cartItemElement);
- });
- }
-
- createCartItemElement(item) {
- const itemElement = document.createElement('div');
- itemElement.id = item.product.id;
- itemElement.className =
- 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
-
- const discountText = this.getProductDiscountText(item.product);
-
- itemElement.innerHTML = `
-
-
-
${discountText}${
- item.product.name
- }
-
PRODUCT
-
${this.getPriceDisplayText(item.product)}
-
-
- ${
- item.quantity
- }
-
-
-
-
-
${this.getPriceDisplayText(
- item.product,
- )}
-
Remove
-
- `;
-
- this.bindCartItemEvents(itemElement);
-
- return itemElement;
- }
-
- getProductDiscountText(product) {
- if (product.isOnSale && product.isRecommended) return '⚡💝';
- if (product.isOnSale) return '⚡';
- if (product.isRecommended) return '💝';
- return '';
- }
-
- getPriceDisplayText(product) {
- if (product.isOnSale || product.isRecommended) {
- const discountClass =
- product.isOnSale && product.isRecommended
- ? 'text-purple-600'
- : product.isOnSale
- ? 'text-red-500'
- : 'text-blue-500';
- return `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
- }
- return `₩${product.price.toLocaleString()}`;
- }
-
- bindCartItemEvents(itemElement) {
- const quantityButtons = itemElement.querySelectorAll('.quantity-change');
- const removeButton = itemElement.querySelector('.remove-item');
-
- quantityButtons.forEach((button) => {
- button.addEventListener('click', (e) => {
- const {productId} = e.target.dataset;
- const change = parseInt(e.target.dataset.change);
- this.handleQuantityChange(productId, change);
- });
- });
-
- removeButton.addEventListener('click', (e) => {
- const {productId} = e.target.dataset;
- this.handleRemoveItem(productId);
- });
- }
-
- handleQuantityChange(productId, change) {
- const cartItem = this.cartService.getItemById(productId);
- if (!cartItem) return;
-
- const newQuantity = cartItem.quantity + change;
- const product = this.productService.getProductById(productId);
-
- if (newQuantity <= 0) {
- this.cartService.removeItem(productId);
- this.productService.updateProductStock(productId, -cartItem.quantity);
- } else if (newQuantity <= product.stock + cartItem.quantity) {
- cartItem.quantity = newQuantity;
- this.productService.updateProductStock(productId, change);
- } else {
- alert('재고가 부족합니다.');
- return;
- }
-
- this.updateUI();
- }
-
- handleRemoveItem(productId) {
- const cartItem = this.cartService.getItemById(productId);
- if (cartItem) {
- this.productService.updateProductStock(productId, -cartItem.quantity);
- this.cartService.removeItem(productId);
- this.updateUI();
- }
- }
-
- updateOrderSummary() {
- this.updateSummaryDetails();
- this.updateTotalAmount();
- this.updateLoyaltyPoints();
- this.updateDiscountInfo();
- this.updateTuesdaySpecial();
- }
-
- updateSummaryDetails() {
- const summaryDetails = document.getElementById('summary-details');
- summaryDetails.innerHTML = '';
-
- if (this.cartService.items.length === 0) return;
-
- this.cartService.items.forEach((item) => {
- const itemTotal = item.product.price * item.quantity;
- summaryDetails.innerHTML += `
-
- ${item.product.name} x ${item.quantity}
- ₩${itemTotal.toLocaleString()}
-
- `;
- });
-
- const subtotal = this.cartService.getTotalAmount();
- summaryDetails.innerHTML += `
-
-
- Subtotal
- ₩${subtotal.toLocaleString()}
-
- `;
- }
-
- updateTotalAmount() {
- const totalAmount = this.cartService.getTotalAmount();
- const totalQuantity = this.cartService.getTotalQuantity();
-
- const discountRate = this.discountCalculator.calculateTotalDiscount(
- this.cartService.items,
- totalQuantity,
- );
-
- const finalAmount = Math.round(totalAmount * (1 - discountRate));
-
- const totalElement = document.querySelector('#cart-total .text-2xl');
- if (totalElement) {
- totalElement.textContent = `₩${finalAmount.toLocaleString()}`;
- }
- }
-
- updateLoyaltyPoints() {
- const totalAmount = this.cartService.getTotalAmount();
- const totalQuantity = this.cartService.getTotalQuantity();
-
- const discountRate = this.discountCalculator.calculateTotalDiscount(
- this.cartService.items,
- totalQuantity,
- );
- const finalAmount = Math.round(totalAmount * (1 - discountRate));
-
- const totalPoints = this.pointCalculator.calculateTotalPoints(
- finalAmount,
- this.cartService.items,
- totalQuantity,
- );
-
- const loyaltyPointsElement = document.getElementById('loyalty-points');
- if (loyaltyPointsElement) {
- loyaltyPointsElement.textContent = `적립 포인트: ${totalPoints}p`;
- }
- }
-
- updateDiscountInfo() {
- const discountInfo = document.getElementById('discount-info');
- const totalAmount = this.cartService.getTotalAmount();
- const totalQuantity = this.cartService.getTotalQuantity();
-
- const discountRate = this.discountCalculator.calculateTotalDiscount(
- this.cartService.items,
- totalQuantity,
- );
-
- if (discountRate > 0 && totalAmount > 0) {
- const savedAmount = Math.round(totalAmount * discountRate);
- discountInfo.innerHTML = `
-
-
- 총 할인율
- ${(discountRate * 100).toFixed(
- 1,
- )}%
-
-
₩${savedAmount.toLocaleString()} 할인되었습니다
-
- `;
- } else {
- discountInfo.innerHTML = '';
- }
- }
-
- updateTuesdaySpecial() {
- const tuesdaySpecial = document.getElementById('tuesday-special');
- const isTuesday = new Date().getDay() === 2;
- const hasItems = this.cartService.items.length > 0;
-
- if (isTuesday && hasItems) {
- tuesdaySpecial.classList.remove('hidden');
- } else {
- tuesdaySpecial.classList.add('hidden');
- }
- }
-
- updateProductSelector() {
- this.productSelectorComponent.updateOptions();
- this.productSelectorComponent.updateStockInfo();
- }
-
- startSpecialSales() {
- // 번개세일 시작
- const lightningDelay = Math.random() * TIMING.LIGHTNING_SALE_DELAY_MAX;
- setTimeout(() => {
- setInterval(() => {
- const luckyIndex = Math.floor(Math.random() * this.productService.products.length);
- const luckyProduct = this.productService.products[luckyIndex];
-
- if (this.productService.applyLightningSale(luckyProduct.id)) {
- alert(`⚡번개세일! ${luckyProduct.name}이(가) 20% 할인 중입니다!`);
- this.updateUI();
- }
- }, TIMING.LIGHTNING_SALE_INTERVAL);
- }, lightningDelay);
-
- // 추천할인 시작
- const recommendationDelay = Math.random() * TIMING.RECOMMENDATION_DELAY_MAX;
- setTimeout(() => {
- setInterval(() => {
- if (this.cartService.items.length === 0) return;
-
- const lastSelectedProduct =
- this.cartService.items[this.cartService.items.length - 1].product;
- const availableProducts = this.productService.products.filter(
- (product) =>
- product.id !== lastSelectedProduct.id && product.stock > 0 && !product.isRecommended,
- );
-
- if (availableProducts.length > 0) {
- const recommendProduct = availableProducts[0];
- if (this.productService.applyRecommendationSale(recommendProduct.id)) {
- alert(`💝 ${recommendProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
- this.updateUI();
- }
- }
- }, TIMING.RECOMMENDATION_INTERVAL);
- }, recommendationDelay);
- }
-}
-
-// ============================================================================
-// 애플리케이션 초기화
-// ============================================================================
-
-function initializeApp() {
- new ShoppingCartApp();
-}
-
-// DOM이 로드된 후 애플리케이션 시작
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initializeApp);
-} else {
- initializeApp();
-}
diff --git a/src/basic/ui/components/HeaderComponent.js b/src/basic/ui/components/HeaderComponent.js
deleted file mode 100644
index 5abc1321e..000000000
--- a/src/basic/ui/components/HeaderComponent.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 헤더 컴포넌트
- */
-export class HeaderComponent {
- constructor() {
- this.element = this.createElement();
- }
-
- /**
- * 헤더 요소 생성
- * @returns {HTMLElement} 헤더 요소
- */
- createElement() {
- const header = document.createElement('div');
- header.className = 'mb-8';
- header.innerHTML = this.getHeaderTemplate();
- return header;
- }
-
- /**
- * 헤더 템플릿 생성
- * @returns {string} 헤더 HTML 템플릿
- */
- getHeaderTemplate() {
- return `
-
- Shopping Cart
-
- 🛍️ 0 items in cart
-
- `;
- }
-
- /**
- * 아이템 수량 업데이트
- * @param {number} count - 아이템 수량
- */
- updateItemCount(count) {
- const itemCountElement = this.element.querySelector('#item-count');
- if (itemCountElement) {
- itemCountElement.textContent = `🛍️ ${count} items in cart`;
- }
- }
-
- /**
- * 헤더 요소 반환
- * @returns {HTMLElement} 헤더 요소
- */
- getElement() {
- return this.element;
- }
-}
diff --git a/src/basic/ui/components/ProductSelectorComponent.js b/src/basic/ui/components/ProductSelectorComponent.js
deleted file mode 100644
index 0e84f1bda..000000000
--- a/src/basic/ui/components/ProductSelectorComponent.js
+++ /dev/null
@@ -1,152 +0,0 @@
-import { formatPrice } from '../../utils/PriceUtils.js';
-import { UI_CONSTANTS } from '../../constants/UIConstants.js';
-
-/**
- * 상품 선택 컴포넌트
- */
-export class ProductSelectorComponent {
- constructor(productService, onProductSelect) {
- this.productService = productService;
- this.onProductSelect = onProductSelect;
- this.element = this.createElement();
- this.bindEvents();
- }
-
- /**
- * 상품 선택 요소 생성
- * @returns {HTMLElement} 상품 선택 컨테이너
- */
- createElement() {
- const container = document.createElement('div');
- container.className = 'mb-6 pb-6 border-b border-gray-200';
-
- const select = document.createElement('select');
- select.id = 'product-select';
- select.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
-
- const addButton = document.createElement('button');
- addButton.id = 'add-to-cart';
- addButton.innerHTML = 'Add to Cart';
- addButton.className =
- 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
-
- const stockInfo = document.createElement('div');
- stockInfo.id = 'stock-status';
- stockInfo.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
-
- container.appendChild(select);
- container.appendChild(addButton);
- container.appendChild(stockInfo);
-
- return container;
- }
-
- /**
- * 이벤트 바인딩
- */
- bindEvents() {
- const addButton = this.element.querySelector('#add-to-cart');
- if (addButton) {
- addButton.addEventListener('click', () => {
- const select = this.element.querySelector('#product-select');
- const selectedProductId = select.value;
-
- if (selectedProductId && this.onProductSelect) {
- this.onProductSelect(selectedProductId);
- }
- });
- }
- }
-
- /**
- * 상품 옵션 업데이트
- */
- updateOptions() {
- const select = this.element.querySelector('#product-select');
- if (!select) return;
-
- select.innerHTML = '';
- const products = this.productService.getAllProducts();
-
- products.forEach((product) => {
- const option = this.createProductOption(product);
- select.appendChild(option);
- });
-
- this.updateStockWarning();
- }
-
- /**
- * 상품 옵션 생성
- * @param {Object} product - 상품 정보
- * @returns {HTMLElement} 옵션 요소
- */
- createProductOption(product) {
- const option = document.createElement('option');
- option.value = product.id;
-
- const discountText = this.getDiscountText(product);
- const stockText = product.isSoldOut() ? ' (품절)' : '';
-
- option.textContent = `${product.name} - ${formatPrice(product.val)}${stockText}${discountText}`;
- option.disabled = product.isSoldOut();
-
- if (product.isSoldOut()) {
- option.className = UI_CONSTANTS.CLASSES.SOLD_OUT_ITEM;
- } else if (product.onSale && product.suggestSale) {
- option.className = UI_CONSTANTS.CLASSES.SUPER_SALE_ITEM;
- } else if (product.onSale) {
- option.className = UI_CONSTANTS.CLASSES.SALE_ITEM;
- } else if (product.suggestSale) {
- option.className = UI_CONSTANTS.CLASSES.RECOMMENDATION_ITEM;
- }
-
- return option;
- }
-
- /**
- * 할인 텍스트 생성
- * @param {Object} product - 상품 정보
- * @returns {string} 할인 텍스트
- */
- getDiscountText(product) {
- let discountText = '';
-
- if (product.onSale) discountText += ' ⚡SALE';
- if (product.suggestSale) discountText += ' 💝추천';
-
- return discountText;
- }
-
- /**
- * 재고 경고 업데이트
- */
- updateStockWarning() {
- const select = this.element.querySelector('#product-select');
- const totalStock = this.productService.getTotalStock();
-
- if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
- select.style.borderColor = 'orange';
- } else {
- select.style.borderColor = '';
- }
- }
-
- /**
- * 재고 상태 메시지 업데이트
- */
- updateStockMessage() {
- const stockInfo = this.element.querySelector('#stock-status');
- if (stockInfo) {
- stockInfo.textContent = this.productService.generateLowStockMessage();
- }
- }
-
- /**
- * 컴포넌트 요소 반환
- * @returns {HTMLElement} 컴포넌트 요소
- */
- getElement() {
- return this.element;
- }
-}
diff --git a/src/basic/ui/index.js b/src/basic/ui/index.js
deleted file mode 100644
index 7dcabd873..000000000
--- a/src/basic/ui/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './components/HeaderComponent.js';
-export * from './components/ProductSelectorComponent.js';
diff --git a/src/basic/utils/DateUtils.js b/src/basic/utils/DateUtils.js
deleted file mode 100644
index bbd1b17a9..000000000
--- a/src/basic/utils/DateUtils.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * 현재 요일이 화요일인지 확인
- * @returns {boolean} 화요일 여부
- */
-export function isTuesday() {
- const today = new Date();
- return today.getDay() === 2; // 0: 일요일, 1: 월요일, 2: 화요일
-}
-
-/**
- * 현재 날짜 정보를 가져옴
- * @returns {Object} 날짜 정보
- */
-export function getCurrentDateInfo() {
- const today = new Date();
- return {
- day: today.getDay(),
- isTuesday: today.getDay() === 2,
- isWeekend: today.getDay() === 0 || today.getDay() === 6,
- };
-}
diff --git a/src/basic/utils/PriceUtils.js b/src/basic/utils/PriceUtils.js
deleted file mode 100644
index 5a5ba5d63..000000000
--- a/src/basic/utils/PriceUtils.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * 가격을 한국어 형식으로 포맷팅
- * @param {number} price - 포맷팅할 가격
- * @returns {string} 포맷팅된 가격 문자열
- */
-export function formatPrice(price) {
- return `₩${price.toLocaleString()}`;
-}
-
-/**
- * 할인된 가격 계산
- * @param {number} originalPrice - 원래 가격
- * @param {number} discountRate - 할인율 (0~1)
- * @returns {number} 할인된 가격
- */
-export function calculateDiscountedPrice(originalPrice, discountRate) {
- return Math.round(originalPrice * (1 - discountRate));
-}
-
-/**
- * 할인 금액 계산
- * @param {number} originalPrice - 원래 가격
- * @param {number} discountedPrice - 할인된 가격
- * @returns {number} 할인 금액
- */
-export function calculateDiscountAmount(originalPrice, discountedPrice) {
- return originalPrice - discountedPrice;
-}
-
-/**
- * 할인율 계산
- * @param {number} originalPrice - 원래 가격
- * @param {number} discountedPrice - 할인된 가격
- * @returns {number} 할인율 (0~1)
- */
-export function calculateDiscountRate(originalPrice, discountedPrice) {
- return (originalPrice - discountedPrice) / originalPrice;
-}
diff --git a/src/basic/utils/index.js b/src/basic/utils/index.js
deleted file mode 100644
index 1c3b4e5f4..000000000
--- a/src/basic/utils/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './DateUtils.js';
-export * from './PriceUtils.js';
From 37f10acadafbca1829bb2079c29752c9af3f8a5b Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Wed, 30 Jul 2025 16:24:18 +0900
Subject: [PATCH 19/46] =?UTF-8?q?refactor:=20=EC=83=81=EC=88=98=20?=
=?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EB=B3=80?=
=?UTF-8?q?=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/05-clean-code-theory-lesson.md | 73 ++++++++---
src/basic/constants/UIConstants.js | 12 ++
src/basic/constants/discountInfo.js | 17 +++
src/basic/constants/index.js | 4 +
src/basic/constants/pointRate.js | 12 ++
src/basic/constants/productInfo.js | 34 +++++
src/basic/main.basic.js | 186 ++++++++++++----------------
7 files changed, 210 insertions(+), 128 deletions(-)
create mode 100644 src/basic/constants/UIConstants.js
create mode 100644 src/basic/constants/discountInfo.js
create mode 100644 src/basic/constants/index.js
create mode 100644 src/basic/constants/pointRate.js
create mode 100644 src/basic/constants/productInfo.js
diff --git a/docs/05-clean-code-theory-lesson.md b/docs/05-clean-code-theory-lesson.md
index a0aacfdf2..1e8902526 100644
--- a/docs/05-clean-code-theory-lesson.md
+++ b/docs/05-clean-code-theory-lesson.md
@@ -3,11 +3,13 @@
## 서론: AI가 코딩하는 시대, 왜 클린코드를 알아야 하는가?
### 1. AI 시대의 개발자 역할 변화
+
- **코드 작성자 → 코드 큐레이터**: AI가 생성한 코드의 품질을 판단하고 개선
- **문제 해결자 → 아키텍트**: 전체적인 구조와 설계를 이해하고 지시
- **디버거 → 코드 리뷰어**: 더 많은 코드를 빠르게 검토하고 평가
### 2. 클린코드 분별 능력의 중요성
+
- AI는 동작하는 코드를 만들 수 있지만, 유지보수하기 좋은 코드인지는 별개
- 기술 부채를 조기에 발견하고 예방하는 능력 필요
- 팀 협업과 장기적인 프로젝트 성공을 위한 필수 역량
@@ -17,24 +19,30 @@
### 1.1 전역 상태 관리의 문제점
#### 안티패턴 예시
+
```javascript
// ❌ 나쁜 예: cart-tailwind.html에서
-var prodList,sel,addBtn,cartDisp,sum,stockInfo
-var lastSel,bonusPts=0,totalAmt=0,itemCnt=0
+var prodList, sel, addBtn, cartDisp, sum, stockInfo;
+var lastSel,
+ bonusPts = 0,
+ totalAmt = 0,
+ itemCnt = 0;
```
#### 문제점
+
- **예측 불가능성**: 어디서든 값이 변경될 수 있음
- **테스트 어려움**: 전역 상태 때문에 격리된 테스트 불가능
- **동시성 문제**: 여러 함수가 동시에 접근하면 예상치 못한 결과
#### 클린코드 원칙
+
```javascript
// ✅ 좋은 예: 상태를 캡슐화
class ShoppingCart {
#items = [];
#totalAmount = 0;
-
+
addItem(product, quantity) {
// 명확한 인터페이스를 통한 상태 변경
}
@@ -44,14 +52,19 @@ class ShoppingCart {
### 1.2 네이밍의 중요성
#### 안티패턴 예시
+
```javascript
// ❌ 나쁜 예
-var p, q, amt, sel, tgt
-const PRODUCT_ONE = 'p1', PRODUCT_TWO = 'p2', p3_id = 'p3'
-let p4 = "p4", productFive = `p5` // 일관성 없는 선언과 네이밍
+var p, q, amt, sel, tgt;
+const PRODUCT_ONE = 'p1',
+ PRODUCT_TWO = 'p2',
+ p3_id = 'p3';
+let p4 = 'p4',
+ productFive = `p5`; // 일관성 없는 선언과 네이밍
```
#### 클린코드 원칙
+
- **의미 있는 이름 사용**: `p` → `product`, `q` → `quantity`
- **일관된 네이밍 컨벤션**: camelCase 또는 snake_case 중 하나만
- **검색 가능한 이름**: 약어보다는 전체 단어 사용
@@ -59,6 +72,7 @@ let p4 = "p4", productFive = `p5` // 일관성 없는 선언과 네이밍
### 1.3 함수의 단일 책임 원칙
#### 안티패턴 예시
+
```javascript
// ❌ calcCart 함수가 너무 많은 일을 함
function calcCart() {
@@ -73,12 +87,13 @@ function calcCart() {
```
#### 클린코드 원칙
+
```javascript
// ✅ 각 함수는 하나의 일만
-function calculateSubtotal(items) { }
-function applyDiscounts(subtotal, discountRules) { }
-function calculatePoints(amount, bonusRules) { }
-function updateUI(cartState) { }
+function calculateSubtotal(items) {}
+function applyDiscounts(subtotal, discountRules) {}
+function calculatePoints(amount, bonusRules) {}
+function updateUI(cartState) {}
```
## Part 2: 코드 중복과 DRY 원칙
@@ -86,24 +101,26 @@ function updateUI(cartState) { }
### 2.1 중복 코드의 문제점
#### 안티패턴 예시
+
```javascript
// ❌ 포인트 계산이 여러 곳에 중복
// calcCart() 함수 내부
-var pts = Math.floor(totalAmt/1000);
-if(new Date().getDay() === 2) pts *= 2;
+var pts = Math.floor(totalAmt / 1000);
+if (new Date().getDay() === 2) pts *= 2;
// renderBonusPts() 함수 내부
-var basePoints = Math.floor(totalAmt/1000)
-if(new Date().getDay() === 2) finalPoints *= 2;
+var basePoints = Math.floor(totalAmt / 1000);
+if (new Date().getDay() === 2) finalPoints *= 2;
// addBtn 이벤트 핸들러 내부
var tempTotal = 0;
-for(var i=0; i= 30) { // 30은 왜?
```
#### 클린코드 원칙
+
```javascript
// ✅ 상수로 의미 부여
const DISCOUNT_THRESHOLD = 10;
@@ -128,7 +147,7 @@ const TUESDAY_DISCOUNT_RATE = 0.1;
const PRODUCT_DISCOUNTS = {
KEYBOARD: 0.1,
MOUSE: 0.15,
- MONITOR_ARM: 0.2
+ MONITOR_ARM: 0.2,
};
```
@@ -137,21 +156,23 @@ const PRODUCT_DISCOUNTS = {
### 3.1 관심사의 분리 (Separation of Concerns)
#### 안티패턴 예시
+
```javascript
// ❌ 비즈니스 로직과 UI 로직이 혼재
function calcCart() {
// 계산 로직
totalAmt += itemTot * (1 - disc);
-
+
// DOM 직접 조작
elem.style.fontWeight = q >= 10 ? 'bold' : 'normal';
-
+
// 콘솔 로깅
console.log('할인 적용: ' + curItem.name);
}
```
#### 클린코드 원칙
+
- **Model**: 데이터와 비즈니스 로직
- **View**: UI 렌더링
- **Controller**: 사용자 입력 처리와 조정
@@ -159,10 +180,12 @@ function calcCart() {
### 3.2 의존성 역전 원칙
#### 문제점
+
- 고수준 모듈이 저수준 모듈에 직접 의존
- DOM 요소에 직접 접근하여 테스트 어려움
#### 해결 방법
+
```javascript
// ✅ 인터페이스를 통한 의존성 주입
class CartService {
@@ -185,11 +208,13 @@ class CartService {
### 4.2 리팩토링 우선순위
1. **가독성 개선**
+
- 변수명 개선
- 함수 분리
- 주석 대신 자명한 코드
2. **구조 개선**
+
- 중복 제거
- 모듈화
- 의존성 정리
@@ -202,6 +227,7 @@ class CartService {
## Part 5: AI 시대의 코드 리뷰 체크리스트
### 5.1 즉시 거부해야 할 코드
+
- [ ] 전역 변수 남용
- [ ] 하드코딩된 값
- [ ] 300줄 이상의 함수
@@ -209,6 +235,7 @@ class CartService {
- [ ] 테스트 불가능한 구조
### 5.2 개선 요청할 코드
+
- [ ] 불명확한 변수명
- [ ] 주석 없는 복잡한 로직
- [ ] 일관성 없는 코딩 스타일
@@ -216,6 +243,7 @@ class CartService {
- [ ] 에러 처리 부재
### 5.3 AI 생성 코드 평가 기준
+
1. **정확성**: 요구사항을 충족하는가?
2. **가독성**: 다른 개발자가 이해할 수 있는가?
3. **유지보수성**: 변경이 쉬운가?
@@ -225,20 +253,24 @@ class CartService {
## 실습 과제 가이드
### Phase 1: 문제 인식
+
- cart-tailwind.html의 안티패턴 10개 찾기
- 각 문제가 야기할 수 있는 실제 버그 시나리오 작성
### Phase 2: 설계
+
- 클린 아키텍처로 재설계
- 모듈 다이어그램 작성
- 인터페이스 정의
### Phase 3: 구현
+
- 테스트 먼저 작성 (TDD)
- 점진적 리팩토링
- 코드 리뷰와 개선
### Phase 4: 검증
+
- 성능 비교
- 가독성 평가
- 확장성 테스트
@@ -246,17 +278,20 @@ class CartService {
## 결론: 클린코드는 팀워크다
### AI와의 협업에서 클린코드의 역할
+
- AI는 빠르게 코드를 생성하지만, 품질 판단은 인간의 몫
- 클린코드 원칙을 알면 AI에게 더 나은 지시 가능
- 코드 리뷰 능력이 곧 AI 활용 능력
### 지속적인 학습
+
- 클린코드는 한 번에 완성되지 않음
- 팀의 합의와 지속적인 개선이 필요
- 실무 경험을 통한 감각 습득이 중요
## 참고 자료
+
- Clean Code by Robert C. Martin
- Refactoring by Martin Fowler
- The Pragmatic Programmer by David Thomas & Andrew Hunt
-- Working Effectively with Legacy Code by Michael Feathers
\ No newline at end of file
+- Working Effectively with Legacy Code by Michael Feathers
diff --git a/src/basic/constants/UIConstants.js b/src/basic/constants/UIConstants.js
new file mode 100644
index 000000000..3e9c8f209
--- /dev/null
+++ b/src/basic/constants/UIConstants.js
@@ -0,0 +1,12 @@
+/**
+ * UI 관련 상수 정의
+ */
+export const UI_CONSTANTS = {
+ LOW_STOCK_THRESHOLD: 5, //재고 부족 기준 수량
+ TOTAL_STOCK_THRESHOLD: 50, //총 재고 기준 수량
+ TUESDAY: 2, //화요일 판매 할인 적용 여부
+ LIGHTNING_SALE_INTERVAL: 30000, //번개 판매 주기
+ LIGHTNING_SALE_DELAY: 10000, //번개 판매 지연 시간
+ SUGGEST_SALE_INTERVAL: 60000, //추천 판매 주기
+ SUGGEST_SALE_DELAY: 20000, //추천 판매 지연 시간
+};
diff --git a/src/basic/constants/discountInfo.js b/src/basic/constants/discountInfo.js
new file mode 100644
index 000000000..f5938529d
--- /dev/null
+++ b/src/basic/constants/discountInfo.js
@@ -0,0 +1,17 @@
+/**
+ * 할인 관련 상수 정의
+ */
+export const DISCOUNT_THRESHOLDS = {
+ INDIVIDUAL_ITEM: 10,
+ BULK_PURCHASE: 30,
+};
+
+export const DISCOUNT_RATES = {
+ KEYBOARD: 0.1,
+ MOUSE: 0.15,
+ MONITOR_ARM: 0.2,
+ LAPTOP_CASE: 0.05,
+ SPEAKER: 0.25,
+ BULK_PURCHASE: 0.25,
+ TUESDAY: 0.1,
+};
diff --git a/src/basic/constants/index.js b/src/basic/constants/index.js
new file mode 100644
index 000000000..1d285eef7
--- /dev/null
+++ b/src/basic/constants/index.js
@@ -0,0 +1,4 @@
+export * from './productInfo.js';
+export * from './discountInfo.js';
+export * from './pointRate.js';
+export * from './UIConstants.js';
diff --git a/src/basic/constants/pointRate.js b/src/basic/constants/pointRate.js
new file mode 100644
index 000000000..e07a815e4
--- /dev/null
+++ b/src/basic/constants/pointRate.js
@@ -0,0 +1,12 @@
+/**
+ * 포인트 관련 상수 정의
+ */
+export const POINT_RATES = {
+ BASE_RATE: 0.001, // 0.1% (1000원당 1포인트)
+ TUESDAY_MULTIPLIER: 2,
+ SET_BONUS: 50,
+ FULL_SET_BONUS: 100,
+ QUANTITY_BONUS_10: 20,
+ QUANTITY_BONUS_20: 50,
+ QUANTITY_BONUS_30: 100,
+};
diff --git a/src/basic/constants/productInfo.js b/src/basic/constants/productInfo.js
new file mode 100644
index 000000000..bbf755fd3
--- /dev/null
+++ b/src/basic/constants/productInfo.js
@@ -0,0 +1,34 @@
+/**
+ * 상품 관련 상수 정의
+ */
+export const PRODUCT_IDS = {
+ KEYBOARD: 'p1',
+ MOUSE: 'p2',
+ MONITOR_ARM: 'p3',
+ LAPTOP_CASE: 'p4',
+ SPEAKER: 'p5',
+};
+
+export const PRODUCT_NAMES = {
+ [PRODUCT_IDS.KEYBOARD]: '버그 없애는 키보드',
+ [PRODUCT_IDS.MOUSE]: '생산성 폭발 마우스',
+ [PRODUCT_IDS.MONITOR_ARM]: '거북목 탈출 모니터암',
+ [PRODUCT_IDS.LAPTOP_CASE]: '에러 방지 노트북 파우치',
+ [PRODUCT_IDS.SPEAKER]: '코딩할 때 듣는 Lo-Fi 스피커',
+};
+
+export const PRODUCT_PRICES = {
+ [PRODUCT_IDS.KEYBOARD]: 10000,
+ [PRODUCT_IDS.MOUSE]: 20000,
+ [PRODUCT_IDS.MONITOR_ARM]: 30000,
+ [PRODUCT_IDS.LAPTOP_CASE]: 15000,
+ [PRODUCT_IDS.SPEAKER]: 25000,
+};
+
+export const INITIAL_STOCK = {
+ [PRODUCT_IDS.KEYBOARD]: 50,
+ [PRODUCT_IDS.MOUSE]: 30,
+ [PRODUCT_IDS.MONITOR_ARM]: 20,
+ [PRODUCT_IDS.LAPTOP_CASE]: 0,
+ [PRODUCT_IDS.SPEAKER]: 10,
+};
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index ba94d5ec9..1f3323ebe 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,46 +1,14 @@
-// 상수 정의
-const PRODUCT_IDS = {
- KEYBOARD: 'p1',
- MOUSE: 'p2',
- MONITOR_ARM: 'p3',
- LAPTOP_CASE: 'p4',
- SPEAKER: 'p5',
-};
-
-const DISCOUNT_THRESHOLDS = {
- INDIVIDUAL_ITEM: 10,
- BULK_PURCHASE: 30,
-};
-
-const DISCOUNT_RATES = {
- KEYBOARD: 0.1,
- MOUSE: 0.15,
- MONITOR_ARM: 0.2,
- LAPTOP_CASE: 0.05,
- SPEAKER: 0.25,
- BULK_PURCHASE: 0.25,
- TUESDAY: 0.1,
-};
-
-const POINT_RATES = {
- BASE_RATE: 0.001, // 0.1% (1000원당 1포인트)
- TUESDAY_MULTIPLIER: 2,
- SET_BONUS: 50,
- FULL_SET_BONUS: 100,
- QUANTITY_BONUS_10: 20,
- QUANTITY_BONUS_20: 50,
- QUANTITY_BONUS_30: 100,
-};
-
-const UI_CONSTANTS = {
- LOW_STOCK_THRESHOLD: 5,
- TOTAL_STOCK_THRESHOLD: 50,
- TUESDAY: 2,
- LIGHTNING_SALE_INTERVAL: 30000,
- LIGHTNING_SALE_DELAY: 10000,
- SUGGEST_SALE_INTERVAL: 60000,
- SUGGEST_SALE_DELAY: 20000,
-};
+// 상수 import
+import {
+ PRODUCT_IDS,
+ PRODUCT_NAMES,
+ PRODUCT_PRICES,
+ INITIAL_STOCK,
+ DISCOUNT_THRESHOLDS,
+ DISCOUNT_RATES,
+ POINT_RATES,
+ UI_CONSTANTS,
+} from './constants/index.js';
// 전역 변수들 (명명 규칙 적용)
let productList;
@@ -63,46 +31,46 @@ function main() {
productList = [
{
id: PRODUCT_IDS.KEYBOARD,
- name: '버그 없애는 키보드',
- val: 10000,
- originalVal: 10000,
- q: 50,
+ name: PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD],
+ price: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.KEYBOARD],
onSale: false,
suggestSale: false,
},
{
id: PRODUCT_IDS.MOUSE,
- name: '생산성 폭발 마우스',
- val: 20000,
- originalVal: 20000,
- q: 30,
+ name: PRODUCT_NAMES[PRODUCT_IDS.MOUSE],
+ price: PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.MOUSE],
onSale: false,
suggestSale: false,
},
{
id: PRODUCT_IDS.MONITOR_ARM,
- name: '거북목 탈출 모니터암',
- val: 30000,
- originalVal: 30000,
- q: 20,
+ name: PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM],
+ price: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM],
onSale: false,
suggestSale: false,
},
{
id: PRODUCT_IDS.LAPTOP_CASE,
- name: '에러 방지 노트북 파우치',
- val: 15000,
- originalVal: 15000,
- q: 0,
+ name: PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE],
+ price: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE],
onSale: false,
suggestSale: false,
},
{
id: PRODUCT_IDS.SPEAKER,
- name: `코딩할 때 듣는 Lo-Fi 스피커`,
- val: 25000,
- originalVal: 25000,
- q: 10,
+ name: PRODUCT_NAMES[PRODUCT_IDS.SPEAKER],
+ price: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.SPEAKER],
onSale: false,
suggestSale: false,
},
@@ -291,8 +259,8 @@ function main() {
setInterval(function () {
const luckyIdx = Math.floor(Math.random() * productList.length);
const luckyItem = productList[luckyIdx];
- if (luckyItem.q > 0 && !luckyItem.onSale) {
- luckyItem.val = Math.round((luckyItem.originalVal * 80) / 100);
+ if (luckyItem.quantity > 0 && !luckyItem.onSale) {
+ luckyItem.price = Math.round((luckyItem.originalPrice * 80) / 100);
luckyItem.onSale = true;
alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
updateProductOptions();
@@ -308,7 +276,7 @@ function main() {
let suggest = null;
for (let k = 0; k < productList.length; k++) {
if (productList[k].id !== lastSelectedProductId) {
- if (productList[k].q > 0) {
+ if (productList[k].quantity > 0) {
if (!productList[k].suggestSale) {
suggest = productList[k];
break;
@@ -319,7 +287,7 @@ function main() {
if (suggest) {
alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
- suggest.val = Math.round((suggest.val * (100 - 5)) / 100);
+ suggest.price = Math.round((suggest.price * (100 - 5)) / 100);
suggest.suggestSale = true;
updateProductOptions();
updateCartPrices();
@@ -367,11 +335,11 @@ function processCartItems(cartItems) {
}
}
- const qtyElem = cartItems[i].querySelector('.quantity-number');
- const q = parseInt(qtyElem.textContent);
- const itemTot = curItem.val * q;
+ const quantityElem = cartItems[i].querySelector('.quantity-number');
+ const quantity = parseInt(quantityElem.textContent);
+ const itemTot = curItem.price * quantity;
- itemCount += q;
+ itemCount += quantity;
subTot += itemTot;
// UI 스타일 조정 (10개 이상시 볼드 처리)
@@ -379,12 +347,12 @@ function processCartItems(cartItems) {
const priceElems = itemDiv.querySelectorAll('.text-lg, .text-xs');
priceElems.forEach(function (elem) {
if (elem.classList.contains('text-lg')) {
- elem.style.fontWeight = q >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM ? 'bold' : 'normal';
+ elem.style.fontWeight = quantity >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM ? 'bold' : 'normal';
}
});
// 개별 할인 계산
- const disc = calculateItemDiscount(curItem.id, q);
+ const disc = calculateItemDiscount(curItem.id, quantity);
if (disc > 0) {
itemDiscounts.push({ name: curItem.name, discount: disc * 100 });
}
@@ -445,13 +413,13 @@ function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesd
}
}
- const qtyElem = cartItems[i].querySelector('.quantity-number');
- const q = parseInt(qtyElem.textContent);
- const itemTotal = curItem.val * q;
+ const quantityElem = cartItems[i].querySelector('.quantity-number');
+ const quantity = parseInt(quantityElem.textContent);
+ const itemTotal = curItem.price * quantity;
summaryDetails.innerHTML += `
- ${curItem.name} x ${q}
+ ${curItem.name} x ${quantity}
₩${itemTotal.toLocaleString()}
`;
@@ -516,7 +484,7 @@ function updateProductOptions() {
for (let idx = 0; idx < productList.length; idx++) {
const _p = productList[idx];
- totalStock = totalStock + _p.q;
+ totalStock = totalStock + _p.quantity;
}
// 각 상품별 옵션 생성
for (let i = 0; i < productList.length; i++) {
@@ -529,22 +497,22 @@ function updateProductOptions() {
if (item.onSale) discountText += ' ⚡SALE';
if (item.suggestSale) discountText += ' 💝추천';
- if (item.q === 0) {
- opt.textContent = `${item.name} - ${item.val}원 (품절)${discountText}`;
+ if (item.quantity === 0) {
+ opt.textContent = `${item.name} - ${item.price}원 (품절)${discountText}`;
opt.disabled = true;
opt.className = 'text-gray-400';
} else {
if (item.onSale && item.suggestSale) {
- opt.textContent = `⚡💝${item.name} - ${item.originalVal}원 → ${item.val}원 (25% SUPER SALE!)`;
+ opt.textContent = `⚡💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (25% SUPER SALE!)`;
opt.className = 'text-purple-600 font-bold';
} else if (item.onSale) {
- opt.textContent = `⚡${item.name} - ${item.originalVal}원 → ${item.val}원 (20% SALE!)`;
+ opt.textContent = `⚡${item.name} - ${item.originalPrice}원 → ${item.price}원 (20% SALE!)`;
opt.className = 'text-red-500 font-bold';
} else if (item.suggestSale) {
- opt.textContent = `💝${item.name} - ${item.originalVal}원 → ${item.val}원 (5% 추천할인!)`;
+ opt.textContent = `💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (5% 추천할인!)`;
opt.className = 'text-blue-500 font-bold';
} else {
- opt.textContent = `${item.name} - ${item.val}원${discountText}`;
+ opt.textContent = `${item.name} - ${item.price}원${discountText}`;
}
}
productSelector.appendChild(opt);
@@ -748,9 +716,9 @@ function updateStockMessages() {
let stockMsg = '';
for (let stockIdx = 0; stockIdx < productList.length; stockIdx++) {
const item = productList[stockIdx];
- if (item.q < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
- if (item.q > 0) {
- stockMsg += `${item.name}: 재고 부족 (${item.q}개 남음)\n`;
+ if (item.quantity < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
+ if (item.quantity > 0) {
+ stockMsg += `${item.name}: 재고 부족 (${item.quantity}개 남음)\n`;
} else {
stockMsg += `${item.name}: 품절\n`;
}
@@ -780,16 +748,16 @@ function updateCartPrices() {
const nameDiv = cartItems[i].querySelector('h3');
if (product.onSale && product.suggestSale) {
- priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
nameDiv.textContent = `⚡💝${product.name}`;
} else if (product.onSale) {
- priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
nameDiv.textContent = `⚡${product.name}`;
} else if (product.suggestSale) {
- priceDiv.innerHTML = `₩${product.originalVal.toLocaleString()} ₩${product.val.toLocaleString()}`;
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
nameDiv.textContent = `💝${product.name}`;
} else {
- priceDiv.textContent = `₩${product.val.toLocaleString()}`;
+ priceDiv.textContent = `₩${product.price.toLocaleString()}`;
nameDiv.textContent = product.name;
}
}
@@ -824,17 +792,17 @@ addToCartButton.addEventListener('click', function () {
}
}
- if (itemToAdd && itemToAdd.q > 0) {
+ if (itemToAdd && itemToAdd.quantity > 0) {
const item = document.getElementById(itemToAdd['id']);
if (item) {
// 이미 장바구니에 있으면 수량 증가
- const qtyElem = item.querySelector('.quantity-number');
- const newQty = parseInt(qtyElem['textContent']) + 1;
+ const quantityElem = item.querySelector('.quantity-number');
+ const newQty = parseInt(quantityElem['textContent']) + 1;
- if (newQty <= itemToAdd.q + parseInt(qtyElem.textContent)) {
- qtyElem.textContent = newQty;
- itemToAdd['q']--;
+ if (newQty <= itemToAdd.quantity + parseInt(quantityElem.textContent)) {
+ quantityElem.textContent = newQty;
+ itemToAdd['quantity']--;
} else {
alert('재고가 부족합니다.');
}
@@ -861,14 +829,14 @@ addToCartButton.addEventListener('click', function () {
PRODUCT
${
itemToAdd.onSale || itemToAdd.suggestSale
- ? `₩${itemToAdd.originalVal.toLocaleString()} ₩${itemToAdd.originalPrice.toLocaleString()} ₩${itemToAdd.val.toLocaleString()}`
- : `₩${itemToAdd.val.toLocaleString()}`
+ }">₩${itemToAdd.price.toLocaleString()}`
+ : `₩${itemToAdd.price.toLocaleString()}`
}
`;
cartDisplayElement.appendChild(newItem);
- itemToAdd.quantity--;
+ // ProductService의 decreaseStock 함수 사용
+ const result = decreaseStock(productList, itemToAdd.id, 1);
+ if (result.success) {
+ productList = result.products;
+ }
}
calculateCartSummary();
@@ -882,14 +832,9 @@ cartDisplayElement.addEventListener('click', function (event) {
if (tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) {
const prodId = tgt.dataset.productId;
const itemElem = document.getElementById(prodId);
- let prod = null;
- for (let prdIdx = 0; prdIdx < productList.length; prdIdx++) {
- if (productList[prdIdx].id === prodId) {
- prod = productList[prdIdx];
- break;
- }
- }
+ // ProductService의 getProductById 함수 사용
+ const prod = getProductById(productList, prodId);
if (tgt.classList.contains('quantity-change')) {
const qtyChange = parseInt(tgt.dataset.change);
@@ -899,9 +844,17 @@ cartDisplayElement.addEventListener('click', function (event) {
if (newQty > 0 && newQty <= prod.quantity + currentQty) {
qtyElem.textContent = newQty;
- prod.quantity -= qtyChange;
+ // ProductService의 decreaseStock 함수 사용
+ const result = decreaseStock(productList, prodId, qtyChange);
+ if (result.success) {
+ productList = result.products;
+ }
} else if (newQty <= 0) {
- prod.quantity += currentQty;
+ // ProductService의 increaseStock 함수 사용
+ const result = increaseStock(productList, prodId, currentQty);
+ if (result.success) {
+ productList = result.products;
+ }
itemElem.remove();
} else {
alert('재고가 부족합니다.');
@@ -909,7 +862,11 @@ cartDisplayElement.addEventListener('click', function (event) {
} else if (tgt.classList.contains('remove-item')) {
const qtyElem = itemElem.querySelector('.quantity-number');
const remQty = parseInt(qtyElem.textContent);
- prod.quantity += remQty;
+ // ProductService의 increaseStock 함수 사용
+ const result = increaseStock(productList, prodId, remQty);
+ if (result.success) {
+ productList = result.products;
+ }
itemElem.remove();
}
From 11a4f51c656257062b51703d813f664ef37cd367 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 02:10:27 +0900
Subject: [PATCH 22/46] =?UTF-8?q?chore:=20=ED=8F=B4=EB=8D=94=EB=AA=85=20?=
=?UTF-8?q?=EC=88=98=EC=A0=95=20(=EB=9D=84=EC=96=B4=EC=93=B0=EA=B8=B0)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../basic/services/product/ProductService.js | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename "src/basic/\bservices/product/ProductService.js" => src/basic/services/product/ProductService.js (100%)
diff --git "a/src/basic/\bservices/product/ProductService.js" b/src/basic/services/product/ProductService.js
similarity index 100%
rename from "src/basic/\bservices/product/ProductService.js"
rename to src/basic/services/product/ProductService.js
From 56de4bfdd91d7c93f16584871e23b5bcd9884d90 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 02:19:41 +0900
Subject: [PATCH 23/46] =?UTF-8?q?refactor:=20=ED=95=A0=EC=9D=B8=20?=
=?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 79 +++-----
.../services/discount/DiscountService.js | 180 ++++++++++++++++++
2 files changed, 203 insertions(+), 56 deletions(-)
create mode 100644 src/basic/services/discount/DiscountService.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 9ed1a66f0..9f7016532 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -5,7 +5,6 @@ import {
PRODUCT_PRICES,
INITIAL_STOCK,
DISCOUNT_THRESHOLDS,
- DISCOUNT_RATES,
POINT_RATES,
UI_CONSTANTS,
} from './constants/index.js';
@@ -22,7 +21,15 @@ import {
getOutOfStockProducts,
getTotalStock,
calculateItemDiscount,
-} from './services/product/ProductService.js';
+} from './services/product/ProductService.js';
+
+// DiscountService import
+import {
+ calculateTotalDiscountRate,
+ createDiscountInfo,
+ calculateSavedAmount,
+ checkIsTuesday,
+} from './services/discount/DiscountService.js';
// 전역 변수들 (명명 규칙 적용)
let productList;
@@ -322,35 +329,11 @@ function processCartItems(cartItems) {
// 할인 총합 계산 (대량구매 할인 + 화요일 할인)
function calculateTotalDiscount(subTot, itemCount, currentAmount) {
- let finalAmount = currentAmount;
- let discountRate = 0;
-
- // 대량구매 할인 적용
- if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
- finalAmount = subTot * (1 - DISCOUNT_RATES.BULK_PURCHASE);
- discountRate = DISCOUNT_RATES.BULK_PURCHASE;
- } else {
- discountRate = (subTot - finalAmount) / subTot;
- }
-
- // 화요일 할인 적용
- const today = new Date();
- const isTuesday = today.getDay() === UI_CONSTANTS.TUESDAY;
-
- if (isTuesday && finalAmount > 0) {
- finalAmount = finalAmount * (1 - DISCOUNT_RATES.TUESDAY);
- discountRate = 1 - finalAmount / subTot;
- }
-
- return {
- finalAmount,
- discountRate,
- isTuesday,
- };
+ return calculateTotalDiscountRate(itemCount, subTot, currentAmount);
}
// 주문 요약 상세 내역 갱신
-function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesday, totalAmount) {
+function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) {
const summaryDetails = document.getElementById('summary-details');
summaryDetails.innerHTML = '';
@@ -386,34 +369,18 @@ function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesd
`;
- // 할인 정보 표시
- if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
- summaryDetails.innerHTML += `
-
- 🎉 대량구매 할인 (30개 이상)
- -25%
-
- `;
- } else if (itemDiscounts.length > 0) {
- itemDiscounts.forEach(function (item) {
- summaryDetails.innerHTML += `
-
- ${item.name} (10개↑)
- -${item.discount}%
-
- `;
- });
- }
-
- // 화요일 할인 표시
- if (isTuesday && totalAmount > 0) {
+ // 할인 정보 표시 - DiscountService 사용
+ const discountInfo = createDiscountInfo(itemDiscounts, itemCount);
+ discountInfo.forEach(function (discount) {
+ const colorClass = discount.type === 'tuesday' ? 'text-purple-400' : 'text-green-400';
+ const icon = discount.type === 'tuesday' ? '🌟' : discount.type === 'bulk' ? '🎉' : '';
summaryDetails.innerHTML += `
-
-
🌟 화요일 추가 할인
-
-10%
+
+ ${icon} ${discount.name}
+ -${discount.rate}%
`;
- }
+ });
// 배송비 표시
summaryDetails.innerHTML += `
@@ -516,7 +483,7 @@ function calculateCartSummary() {
document.getElementById('item-count').textContent = `🛍️ ${itemCount} items in cart`;
// 주문 요약(상품별, 할인, 배송 등) 갱신
- updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts, isTuesday, totalAmount);
+ updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts);
// 총 결제 금액 표시 갱신
const totalDiv = orderSummaryElement.querySelector('.text-2xl');
if (totalDiv) {
@@ -539,7 +506,7 @@ function calculateCartSummary() {
discountInfoDiv.innerHTML = '';
if (discRate > 0 && totalAmount > 0) {
- savedAmount = originalTotal - totalAmount;
+ savedAmount = calculateSavedAmount(originalTotal, totalAmount);
discountInfoDiv.innerHTML = `
@@ -588,7 +555,7 @@ const renderBonusPoints = function () {
pointsDetail.push(`기본: ${basePoints}p`);
}
// 화요일 2배 포인트
- if (new Date().getDay() === UI_CONSTANTS.TUESDAY) {
+ if (checkIsTuesday()) {
if (basePoints > 0) {
finalPoints = basePoints * POINT_RATES.TUESDAY_MULTIPLIER;
pointsDetail.push('화요일 2배');
diff --git a/src/basic/services/discount/DiscountService.js b/src/basic/services/discount/DiscountService.js
new file mode 100644
index 000000000..6faec6632
--- /dev/null
+++ b/src/basic/services/discount/DiscountService.js
@@ -0,0 +1,180 @@
+import { DISCOUNT_THRESHOLDS, DISCOUNT_RATES, UI_CONSTANTS } from '../../constants/index.js';
+
+/**
+ * 할인 관련 비즈니스 로직을 담당하는 함수들
+ */
+
+/**
+ * 화요일 여부 확인
+ */
+export function checkIsTuesday() {
+ return new Date().getDay() === UI_CONSTANTS.TUESDAY;
+}
+
+/**
+ * 개별 상품 할인 계산
+ */
+export function calculateItemDiscount(productId, quantity, calculateItemDiscountFn) {
+ return calculateItemDiscountFn(productId, quantity);
+}
+
+/**
+ * 대량구매 할인 계산
+ */
+export function calculateBulkDiscount(itemCount) {
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ return DISCOUNT_RATES.BULK_PURCHASE;
+ }
+ return 0;
+}
+
+/**
+ * 화요일 할인 계산
+ */
+export function calculateTuesdayDiscount() {
+ if (checkIsTuesday()) {
+ return DISCOUNT_RATES.TUESDAY;
+ }
+ return 0;
+}
+
+/**
+ * 총 할인율 계산
+ */
+export function calculateTotalDiscountRate(itemCount, subtotal, currentAmount) {
+ let finalAmount = currentAmount;
+ let discountRate = 0;
+
+ // 대량구매 할인 적용
+ const bulkDiscount = calculateBulkDiscount(itemCount);
+ if (bulkDiscount > 0) {
+ finalAmount = subtotal * (1 - bulkDiscount);
+ discountRate = bulkDiscount;
+ } else {
+ discountRate = (subtotal - finalAmount) / subtotal;
+ }
+
+ // 화요일 할인 적용
+ const tuesdayDiscount = calculateTuesdayDiscount();
+ if (tuesdayDiscount > 0 && finalAmount > 0) {
+ finalAmount = finalAmount * (1 - tuesdayDiscount);
+ discountRate = 1 - finalAmount / subtotal;
+ }
+
+ return {
+ finalAmount,
+ discountRate,
+ isTuesday: checkIsTuesday(),
+ };
+}
+
+/**
+ * 할인 정보 생성
+ */
+export function createDiscountInfo(itemDiscounts, itemCount) {
+ const discountInfo = [];
+
+ // 대량구매 할인 정보
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ discountInfo.push({
+ type: 'bulk',
+ name: '대량구매 할인 (30개 이상)',
+ rate: DISCOUNT_RATES.BULK_PURCHASE * 100,
+ });
+ } else if (itemDiscounts.length > 0) {
+ // 개별 상품 할인 정보
+ itemDiscounts.forEach((item) => {
+ discountInfo.push({
+ type: 'individual',
+ name: `${item.name} (10개↑)`,
+ rate: item.discount,
+ });
+ });
+ }
+
+ // 화요일 할인 정보
+ if (checkIsTuesday()) {
+ discountInfo.push({
+ type: 'tuesday',
+ name: '화요일 추가 할인',
+ rate: DISCOUNT_RATES.TUESDAY * 100,
+ });
+ }
+
+ return discountInfo;
+}
+
+/**
+ * 할인된 금액 계산
+ */
+export function calculateDiscountedAmount(originalAmount, discountRate) {
+ return originalAmount * (1 - discountRate);
+}
+
+/**
+ * 절약된 금액 계산
+ */
+export function calculateSavedAmount(originalAmount, finalAmount) {
+ return originalAmount - finalAmount;
+}
+
+/**
+ * 할인율 적용
+ */
+export function applyDiscountRate(amount, discountRate) {
+ return amount * (1 - discountRate);
+}
+
+/**
+ * 복합 할인율 계산
+ */
+export function calculateCompoundDiscountRate(discountRates) {
+ if (discountRates.length === 0) return 0;
+
+ // 복합 할인율 = 1 - (1 - 할인율1) * (1 - 할인율2) * ...
+ const compoundRate = discountRates.reduce((rate, discount) => {
+ return rate * (1 - discount);
+ }, 1);
+
+ return 1 - compoundRate;
+}
+
+/**
+ * 할인 가능 여부 확인
+ */
+export function canApplyDiscount(productId, quantity) {
+ if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ return false;
+ }
+
+ // 상품별 할인 가능 여부 확인
+ const discountableProducts = [
+ 'p1', // 키보드
+ 'p2', // 마우스
+ 'p3', // 모니터암
+ 'p4', // 노트북 파우치
+ 'p5', // 스피커
+ ];
+
+ return discountableProducts.includes(productId);
+}
+
+/**
+ * 최대 할인율 계산
+ */
+export function calculateMaxDiscountRate(productId, quantity, itemCount) {
+ let maxDiscount = 0;
+
+ // 개별 상품 할인
+ if (quantity >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ const itemDiscount = calculateItemDiscount(productId, quantity, () => 0);
+ maxDiscount = Math.max(maxDiscount, itemDiscount);
+ }
+
+ // 대량구매 할인
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ maxDiscount = Math.max(maxDiscount, DISCOUNT_RATES.BULK_PURCHASE);
+ }
+
+ return maxDiscount;
+}
From 668f85a37d3f594bf9bc18e01803ab22134abb4f Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 15:44:54 +0900
Subject: [PATCH 24/46] =?UTF-8?q?refactor:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?=
=?UTF-8?q?=EB=8B=88=20point=20=EC=A0=81=EB=A6=BD=20=EC=84=9C=EB=B9=84?=
=?UTF-8?q?=EC=8A=A4=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 325 +++++++++++------------
src/basic/services/point/PointService.js | 230 ++++++++++++++++
2 files changed, 380 insertions(+), 175 deletions(-)
create mode 100644 src/basic/services/point/PointService.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 9f7016532..19ab65b16 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,13 +1,5 @@
// 상수 import
-import {
- PRODUCT_IDS,
- PRODUCT_NAMES,
- PRODUCT_PRICES,
- INITIAL_STOCK,
- DISCOUNT_THRESHOLDS,
- POINT_RATES,
- UI_CONSTANTS,
-} from './constants/index.js';
+import { DISCOUNT_THRESHOLDS, UI_CONSTANTS } from './constants/index.js';
// ProductService import
import {
@@ -28,9 +20,14 @@ import {
calculateTotalDiscountRate,
createDiscountInfo,
calculateSavedAmount,
- checkIsTuesday,
} from './services/discount/DiscountService.js';
+// PointService import
+import { createPointInfo } from './services/point/PointService.js';
+
+// CartService import
+import { createInitialCartState, addItemToCart } from './services/cart/CartService.js';
+
// 전역 변수들 (명명 규칙 적용)
let productList;
let bonusPoints = 0;
@@ -42,6 +39,26 @@ let addToCartButton;
let totalAmount = 0;
let cartDisplayElement;
let orderSummaryElement;
+let cartState; // CartService를 위한 상태 추가
+
+// ProductService 래퍼 (CartService에서 사용하기 위한 인터페이스)
+const productService = {
+ getProductById: (productId) => getProductById(productList, productId),
+ decreaseStock: (productId, quantity) => {
+ const result = decreaseStock(productList, productId, quantity);
+ if (result.success) {
+ productList = result.products;
+ }
+ return result;
+ },
+ increaseStock: (productId, quantity) => {
+ const result = increaseStock(productList, productId, quantity);
+ if (result.success) {
+ productList = result.products;
+ }
+ return result;
+ },
+};
function main() {
totalAmount = 0;
@@ -51,6 +68,9 @@ function main() {
// 상품 정보 초기화 - ProductService 사용
productList = initializeProducts();
+ // CartService 상태 초기화
+ cartState = createInitialCartState();
+
const root = document.getElementById('app');
const header = document.createElement('div');
@@ -489,10 +509,24 @@ function calculateCartSummary() {
if (totalDiv) {
totalDiv.textContent = `₩${Math.round(totalAmount).toLocaleString()}`;
}
- // 적립 포인트 표시 갱신
+ // 적립 포인트 표시 갱신 - PointService 사용
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
- points = Math.floor(totalAmount * POINT_RATES.BASE_RATE);
+ // CartService 상태에서 장바구니 아이템 정보 추출
+ const cartItems = cartState.items.map((item) => {
+ const product = productService.getProductById(item.id);
+ return {
+ id: item.id,
+ quantity: item.quantity,
+ name: product ? product.name : '',
+ price: product ? product.price : 0,
+ };
+ });
+
+ // PointService를 사용하여 포인트 계산
+ const pointInfo = createPointInfo(totalAmount, cartItems);
+ points = pointInfo.totalPoints;
+
if (points > 0) {
loyaltyPointsDiv.textContent = `적립 포인트: ${points}p`;
loyaltyPointsDiv.style.display = 'block';
@@ -536,89 +570,33 @@ function calculateCartSummary() {
// 적립 포인트 계산 및 상세 내역 표시
const renderBonusPoints = function () {
- let finalPoints;
- let hasKeyboard;
- let hasMouse;
- let hasMonitorArm;
-
if (cartDisplayElement.children.length === 0) {
document.getElementById('loyalty-points').style.display = 'none';
return;
}
- const basePoints = Math.floor(totalAmount * POINT_RATES.BASE_RATE);
- finalPoints = 0;
- const pointsDetail = [];
-
- if (basePoints > 0) {
- finalPoints = basePoints;
- pointsDetail.push(`기본: ${basePoints}p`);
- }
- // 화요일 2배 포인트
- if (checkIsTuesday()) {
- if (basePoints > 0) {
- finalPoints = basePoints * POINT_RATES.TUESDAY_MULTIPLIER;
- pointsDetail.push('화요일 2배');
- }
- }
- // 키보드/마우스/모니터암 포함 여부 체크
- hasKeyboard = false;
- hasMouse = false;
- hasMonitorArm = false;
- const nodes = cartDisplayElement.children;
-
- for (const node of nodes) {
- let product = null;
- for (let pIdx = 0; pIdx < productList.length; pIdx++) {
- if (productList[pIdx].id === node.id) {
- product = productList[pIdx];
- break;
- }
- }
-
- if (!product) continue;
-
- if (product.id === PRODUCT_IDS.KEYBOARD) {
- hasKeyboard = true;
- } else if (product.id === PRODUCT_IDS.MOUSE) {
- hasMouse = true;
- } else if (product.id === PRODUCT_IDS.MONITOR_ARM) {
- hasMonitorArm = true;
- }
- }
- // 키보드+마우스 세트, 풀세트, 대량구매 추가 포인트
- if (hasKeyboard && hasMouse) {
- finalPoints = finalPoints + POINT_RATES.SET_BONUS;
- pointsDetail.push('키보드+마우스 세트 +50p');
- }
+ // CartService 상태에서 장바구니 아이템 정보 추출
+ const cartItems = cartState.items.map((item) => {
+ const product = productService.getProductById(item.id);
+ return {
+ id: item.id,
+ quantity: item.quantity,
+ name: product ? product.name : '',
+ price: product ? product.price : 0,
+ };
+ });
- if (hasKeyboard && hasMouse && hasMonitorArm) {
- finalPoints = finalPoints + POINT_RATES.FULL_SET_BONUS;
- pointsDetail.push('풀세트 구매 +100p');
- }
+ // PointService를 사용하여 포인트 정보 생성
+ const pointInfo = createPointInfo(totalAmount, cartItems);
+ bonusPoints = pointInfo.totalPoints;
- if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
- finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_30;
- pointsDetail.push('대량구매(30개+) +100p');
- } else {
- if (itemCount >= 20) {
- finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_20;
- pointsDetail.push('대량구매(20개+) +50p');
- } else {
- if (itemCount >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
- finalPoints = finalPoints + POINT_RATES.QUANTITY_BONUS_10;
- pointsDetail.push('대량구매(10개+) +20p');
- }
- }
- }
- bonusPoints = finalPoints;
const ptsTag = document.getElementById('loyalty-points');
if (ptsTag) {
if (bonusPoints > 0) {
ptsTag.innerHTML =
`적립 포인트: ${bonusPoints}p
` +
- `${pointsDetail.join(', ')}
`;
+ `${pointInfo.detailText}
`;
ptsTag.style.display = 'block';
} else {
ptsTag.textContent = '적립 포인트: 0p';
@@ -689,107 +667,104 @@ function updateCartPrices() {
calculateCartSummary();
}
+// CartService를 사용한 장바구니 아이템 추가 함수
+function addItemToCartUI(productId, quantity = 1) {
+ const result = addItemToCart(cartState, productId, quantity, productService);
+
+ if (result.success) {
+ const { cartState: newCartState } = result;
+ cartState = newCartState;
+ renderCartItems();
+ calculateCartSummary();
+ lastSelectedProductId = productId;
+ } else {
+ alert(result.message);
+ }
+}
+
+// CartService 상태를 기반으로 UI 렌더링
+function renderCartItems() {
+ cartDisplayElement.innerHTML = '';
+
+ cartState.items.forEach((item) => {
+ const product = productService.getProductById(item.id);
+ if (!product) return;
+
+ const newItem = document.createElement('div');
+ newItem.id = item.id;
+ newItem.className =
+ 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
+ newItem.innerHTML = `
+
+
+
${
+ product.onSale && product.suggestSale
+ ? '⚡💝'
+ : product.onSale
+ ? '⚡'
+ : product.suggestSale
+ ? '💝'
+ : ''
+ }${product.name}
+
PRODUCT
+
${
+ product.onSale || product.suggestSale
+ ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`
+ : `₩${product.price.toLocaleString()}`
+ }
+
+
+ ${
+ item.quantity
+ }
+
+
+
+
+
${
+ product.onSale || product.suggestSale
+ ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`
+ : `₩${product.price.toLocaleString()}`
+ }
+
Remove
+
+ `;
+ cartDisplayElement.appendChild(newItem);
+ });
+}
+
main();
-// 장바구니 추가 버튼 클릭 이벤트
+// 장바구니 추가 버튼 클릭 이벤트 - CartService 사용
addToCartButton.addEventListener('click', function () {
const selItem = productSelector.value;
- // ProductService의 getProductById 함수 사용
- const itemToAdd = getProductById(productList, selItem);
-
- if (!selItem || !itemToAdd) {
+ if (!selItem) {
return;
}
- if (itemToAdd.quantity > 0) {
- const item = document.getElementById(itemToAdd.id);
-
- if (item) {
- // 이미 장바구니에 있으면 수량 증가
- const quantityElem = item.querySelector('.quantity-number');
- const newQty = parseInt(quantityElem.textContent) + 1;
-
- if (newQty <= itemToAdd.quantity + parseInt(quantityElem.textContent)) {
- quantityElem.textContent = newQty;
- // ProductService의 decreaseStock 함수 사용
- const result = decreaseStock(productList, itemToAdd.id, 1);
- if (result.success) {
- productList = result.products;
- }
- } else {
- alert('재고가 부족합니다.');
- }
- } else {
- // 장바구니에 새로 추가
- const newItem = document.createElement('div');
- newItem.id = itemToAdd.id;
- newItem.className =
- 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
- newItem.innerHTML = `
-
-
-
${
- itemToAdd.onSale && itemToAdd.suggestSale
- ? '⚡💝'
- : itemToAdd.onSale
- ? '⚡'
- : itemToAdd.suggestSale
- ? '💝'
- : ''
- }${itemToAdd.name}
-
PRODUCT
-
${
- itemToAdd.onSale || itemToAdd.suggestSale
- ? `₩${itemToAdd.originalPrice.toLocaleString()} ₩${itemToAdd.price.toLocaleString()}`
- : `₩${itemToAdd.price.toLocaleString()}`
- }
-
-
- 1
-
-
-
-
-
${
- itemToAdd.onSale || itemToAdd.suggestSale
- ? `₩${itemToAdd.originalPrice.toLocaleString()} ₩${itemToAdd.price.toLocaleString()}`
- : `₩${itemToAdd.price.toLocaleString()}`
- }
-
Remove
-
- `;
- cartDisplayElement.appendChild(newItem);
- // ProductService의 decreaseStock 함수 사용
- const result = decreaseStock(productList, itemToAdd.id, 1);
- if (result.success) {
- productList = result.products;
- }
- }
-
- calculateCartSummary();
- lastSelectedProductId = selItem;
- }
+ // CartService를 사용하여 장바구니에 추가
+ addItemToCartUI(selItem, 1);
});
// 장바구니 내 수량 변경/삭제 이벤트 처리
diff --git a/src/basic/services/point/PointService.js b/src/basic/services/point/PointService.js
new file mode 100644
index 000000000..0f8cea0b9
--- /dev/null
+++ b/src/basic/services/point/PointService.js
@@ -0,0 +1,230 @@
+import {
+ POINT_RATES,
+ PRODUCT_IDS,
+ DISCOUNT_THRESHOLDS,
+ UI_CONSTANTS,
+} from '../../constants/index.js';
+
+/**
+ * 포인트 관련 비즈니스 로직을 담당하는 함수들
+ */
+
+/**
+ * 화요일 여부 확인
+ */
+export function checkIsTuesday() {
+ return new Date().getDay() === UI_CONSTANTS.TUESDAY;
+}
+
+/**
+ * 기본 포인트 계산
+ */
+export function calculateBasePoints(totalAmount) {
+ return Math.floor(totalAmount * POINT_RATES.BASE_RATE);
+}
+
+/**
+ * 화요일 2배 포인트 적용
+ */
+export function applyTuesdayMultiplier(basePoints) {
+ if (checkIsTuesday() && basePoints > 0) {
+ return basePoints * POINT_RATES.TUESDAY_MULTIPLIER;
+ }
+ return basePoints;
+}
+
+/**
+ * 상품 조합 보너스 포인트 계산
+ */
+export function calculateProductCombinationBonus(cartItems) {
+ let bonusPoints = 0;
+ const productIds = cartItems.map((item) => item.id);
+
+ // 키보드+마우스 세트 보너스
+ const hasKeyboard = productIds.includes(PRODUCT_IDS.KEYBOARD);
+ const hasMouse = productIds.includes(PRODUCT_IDS.MOUSE);
+ const hasMonitorArm = productIds.includes(PRODUCT_IDS.MONITOR_ARM);
+
+ if (hasKeyboard && hasMouse) {
+ bonusPoints += POINT_RATES.SET_BONUS;
+ }
+
+ // 풀세트 보너스
+ if (hasKeyboard && hasMouse && hasMonitorArm) {
+ bonusPoints += POINT_RATES.FULL_SET_BONUS;
+ }
+
+ return bonusPoints;
+}
+
+/**
+ * 수량별 보너스 포인트 계산
+ */
+export function calculateQuantityBonus(itemCount) {
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ return POINT_RATES.QUANTITY_BONUS_30;
+ } else if (itemCount >= 20) {
+ return POINT_RATES.QUANTITY_BONUS_20;
+ } else if (itemCount >= DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ return POINT_RATES.QUANTITY_BONUS_10;
+ }
+ return 0;
+}
+
+/**
+ * 총 포인트 계산
+ */
+export function calculateTotalPoints(totalAmount, cartItems) {
+ let finalPoints = 0;
+ const pointsDetail = [];
+
+ // 기본 포인트 계산
+ const basePoints = calculateBasePoints(totalAmount);
+ if (basePoints > 0) {
+ finalPoints = basePoints;
+ pointsDetail.push(`기본: ${basePoints}p`);
+ }
+
+ // 화요일 2배 포인트 적용
+ const tuesdayPoints = applyTuesdayMultiplier(basePoints);
+ if (tuesdayPoints !== basePoints) {
+ finalPoints = tuesdayPoints;
+ pointsDetail.push('화요일 2배');
+ }
+
+ // 상품 조합 보너스
+ const combinationBonus = calculateProductCombinationBonus(cartItems);
+ if (combinationBonus > 0) {
+ finalPoints += combinationBonus;
+
+ // 키보드+마우스 세트 보너스 (50p)
+ const hasKeyboard = cartItems.some((item) => item.id === PRODUCT_IDS.KEYBOARD);
+ const hasMouse = cartItems.some((item) => item.id === PRODUCT_IDS.MOUSE);
+ if (hasKeyboard && hasMouse) {
+ pointsDetail.push('키보드+마우스 세트 +50p');
+ }
+
+ // 풀세트 보너스 (100p) - 키보드+마우스+모니터암
+ const hasMonitorArm = cartItems.some((item) => item.id === PRODUCT_IDS.MONITOR_ARM);
+ if (hasKeyboard && hasMouse && hasMonitorArm) {
+ pointsDetail.push('풀세트 구매 +100p');
+ }
+ }
+
+ // 수량별 보너스
+ const itemCount = cartItems.reduce((total, item) => total + item.quantity, 0);
+ const quantityBonus = calculateQuantityBonus(itemCount);
+ if (quantityBonus > 0) {
+ finalPoints += quantityBonus;
+ if (itemCount >= DISCOUNT_THRESHOLDS.BULK_PURCHASE) {
+ pointsDetail.push('대량구매(30개+) +100p');
+ } else if (itemCount >= 20) {
+ pointsDetail.push('대량구매(20개+) +50p');
+ } else {
+ pointsDetail.push('대량구매(10개+) +20p');
+ }
+ }
+
+ return {
+ totalPoints: finalPoints,
+ details: pointsDetail,
+ };
+}
+
+/**
+ * 포인트 정보 생성
+ */
+export function createPointInfo(totalAmount, cartItems) {
+ if (cartItems.length === 0) {
+ return {
+ totalPoints: 0,
+ details: [],
+ displayText: '적립 포인트: 0p',
+ };
+ }
+
+ const { totalPoints, details } = calculateTotalPoints(totalAmount, cartItems);
+
+ return {
+ totalPoints,
+ details,
+ displayText: `적립 포인트: ${totalPoints}p`,
+ detailText: details.join(', '),
+ };
+}
+
+/**
+ * 포인트 적립 가능 여부 확인
+ */
+// export function canEarnPoints(totalAmount) {
+// return totalAmount > 0;
+// }
+
+/**
+ * 포인트 적립율 계산
+ */
+// export function calculatePointRate(totalAmount, cartItems) {
+// if (!canEarnPoints(totalAmount)) {
+// return 0;
+// }
+
+// const { totalPoints } = calculateTotalPoints(totalAmount, cartItems);
+// return totalPoints / totalAmount;
+// }
+
+/**
+ * 포인트 적립 예상 금액 계산
+ */
+// export function calculateExpectedPoints(totalAmount, cartItems) {
+// const { totalPoints } = calculateTotalPoints(totalAmount, cartItems);
+// return totalPoints;
+// }
+
+/**
+ * 포인트 적립 내역 생성
+ */
+// export function createPointHistory(totalAmount, cartItems) {
+// const history = [];
+
+// // 기본 포인트
+// const basePoints = calculateBasePoints(totalAmount);
+// if (basePoints > 0) {
+// history.push({
+// type: 'base',
+// description: '기본 적립',
+// points: basePoints,
+// });
+// }
+
+// // 화요일 보너스
+// if (checkIsTuesday() && basePoints > 0) {
+// history.push({
+// type: 'tuesday',
+// description: '화요일 2배 보너스',
+// points: basePoints,
+// });
+// }
+
+// // 상품 조합 보너스
+// const combinationBonus = calculateProductCombinationBonus(cartItems);
+// if (combinationBonus > 0) {
+// history.push({
+// type: 'combination',
+// description: '상품 조합 보너스',
+// points: combinationBonus,
+// });
+// }
+
+// // 수량 보너스
+// const itemCount = cartItems.reduce((total, item) => total + item.quantity, 0);
+// const quantityBonus = calculateQuantityBonus(itemCount);
+// if (quantityBonus > 0) {
+// history.push({
+// type: 'quantity',
+// description: '수량 보너스',
+// points: quantityBonus,
+// });
+// }
+
+// return history;
+// }
From c9a5ba3a966400c52a0d66ac13ee6d64b9cb0ddd Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 20:51:24 +0900
Subject: [PATCH 25/46] =?UTF-8?q?refactor:=20UI=20component=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/components/cart/CartDisplay.js | 9 +
src/basic/components/cart/CartItem.js | 52 ++
src/basic/components/cart/index.js | 5 +
src/basic/components/index.js | 18 +
src/basic/components/layout/GridContainer.js | 12 +
src/basic/components/layout/Header.js | 14 +
src/basic/components/layout/index.js | 5 +
src/basic/components/modal/ManualColumn.js | 70 +++
src/basic/components/modal/ManualOverlay.js | 19 +
src/basic/components/modal/ManualToggle.js | 23 +
src/basic/components/modal/index.js | 6 +
src/basic/components/order/DiscountInfo.js | 25 +
src/basic/components/order/LoyaltyPoints.js | 18 +
src/basic/components/order/OrderSummary.js | 73 +++
src/basic/components/order/index.js | 6 +
.../components/product/ProductSelector.js | 10 +
src/basic/components/product/StockInfo.js | 10 +
src/basic/components/product/index.js | 5 +
src/basic/main.basic.js | 484 +++---------------
19 files changed, 456 insertions(+), 408 deletions(-)
create mode 100644 src/basic/components/cart/CartDisplay.js
create mode 100644 src/basic/components/cart/CartItem.js
create mode 100644 src/basic/components/cart/index.js
create mode 100644 src/basic/components/index.js
create mode 100644 src/basic/components/layout/GridContainer.js
create mode 100644 src/basic/components/layout/Header.js
create mode 100644 src/basic/components/layout/index.js
create mode 100644 src/basic/components/modal/ManualColumn.js
create mode 100644 src/basic/components/modal/ManualOverlay.js
create mode 100644 src/basic/components/modal/ManualToggle.js
create mode 100644 src/basic/components/modal/index.js
create mode 100644 src/basic/components/order/DiscountInfo.js
create mode 100644 src/basic/components/order/LoyaltyPoints.js
create mode 100644 src/basic/components/order/OrderSummary.js
create mode 100644 src/basic/components/order/index.js
create mode 100644 src/basic/components/product/ProductSelector.js
create mode 100644 src/basic/components/product/StockInfo.js
create mode 100644 src/basic/components/product/index.js
diff --git a/src/basic/components/cart/CartDisplay.js b/src/basic/components/cart/CartDisplay.js
new file mode 100644
index 000000000..e27f4c60b
--- /dev/null
+++ b/src/basic/components/cart/CartDisplay.js
@@ -0,0 +1,9 @@
+/**
+ * 장바구니 표시 영역 컴포넌트
+ * 장바구니 아이템들을 표시하는 컨테이너를 생성합니다.
+ */
+export function createCartDisplay() {
+ const cartDisplayElement = document.createElement('div');
+ cartDisplayElement.id = 'cart-items';
+ return cartDisplayElement;
+}
diff --git a/src/basic/components/cart/CartItem.js b/src/basic/components/cart/CartItem.js
new file mode 100644
index 000000000..f3275508e
--- /dev/null
+++ b/src/basic/components/cart/CartItem.js
@@ -0,0 +1,52 @@
+/**
+ * 장바구니 아이템 컴포넌트
+ * 개별 장바구니 아이템을 렌더링합니다.
+ */
+export function renderCartItem(item, product) {
+ const newItem = document.createElement('div');
+ newItem.id = item.id;
+ newItem.className =
+ 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
+
+ const saleIcon =
+ product.onSale && product.suggestSale
+ ? '⚡💝'
+ : product.onSale
+ ? '⚡'
+ : product.suggestSale
+ ? '💝'
+ : '';
+
+ const priceDisplay =
+ product.onSale || product.suggestSale
+ ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`
+ : `₩${product.price.toLocaleString()}`;
+
+ newItem.innerHTML = `
+
+
+
${saleIcon}${product.name}
+
PRODUCT
+
${priceDisplay}
+
+
+ ${item.quantity}
+
+
+
+
+
${priceDisplay}
+
Remove
+
+ `;
+
+ return newItem;
+}
diff --git a/src/basic/components/cart/index.js b/src/basic/components/cart/index.js
new file mode 100644
index 000000000..830419dda
--- /dev/null
+++ b/src/basic/components/cart/index.js
@@ -0,0 +1,5 @@
+/**
+ * Cart 컴포넌트들의 barrel export
+ */
+export { createCartDisplay } from './CartDisplay.js';
+export { renderCartItem } from './CartItem.js';
diff --git a/src/basic/components/index.js b/src/basic/components/index.js
new file mode 100644
index 000000000..c9ec2d033
--- /dev/null
+++ b/src/basic/components/index.js
@@ -0,0 +1,18 @@
+/**
+ * 모든 컴포넌트들의 barrel export
+ */
+
+// Layout 컴포넌트들
+export { createHeader, createGridContainer } from './layout/index.js';
+
+// Product 컴포넌트들
+export { createProductSelector, createStockInfo } from './product/index.js';
+
+// Cart 컴포넌트들
+export { createCartDisplay, renderCartItem } from './cart/index.js';
+
+// Order 컴포넌트들
+export { createRightColumn, renderDiscountInfo, renderLoyaltyPoints } from './order/index.js';
+
+// Modal 컴포넌트들
+export { createManualOverlay, createManualToggle, createManualColumn } from './modal/index.js';
diff --git a/src/basic/components/layout/GridContainer.js b/src/basic/components/layout/GridContainer.js
new file mode 100644
index 000000000..bb5e093fe
--- /dev/null
+++ b/src/basic/components/layout/GridContainer.js
@@ -0,0 +1,12 @@
+/**
+ * 그리드 컨테이너 컴포넌트
+ * 왼쪽 컬럼과 오른쪽 컬럼을 감싸는 그리드 레이아웃을 생성합니다.
+ */
+export function createGridContainer(leftColumn, rightColumn) {
+ const gridContainer = document.createElement('div');
+ gridContainer.className =
+ 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
+ gridContainer.appendChild(leftColumn);
+ gridContainer.appendChild(rightColumn);
+ return gridContainer;
+}
diff --git a/src/basic/components/layout/Header.js b/src/basic/components/layout/Header.js
new file mode 100644
index 000000000..1d2e032f9
--- /dev/null
+++ b/src/basic/components/layout/Header.js
@@ -0,0 +1,14 @@
+/**
+ * 헤더 컴포넌트
+ * 쇼핑 카트 페이지의 상단 헤더를 생성합니다.
+ */
+export function createHeader() {
+ const header = document.createElement('div');
+ header.className = 'mb-8';
+ header.innerHTML = `
+
+ Shopping Cart
+ 🛍️ 0 items in cart
+ `;
+ return header;
+}
diff --git a/src/basic/components/layout/index.js b/src/basic/components/layout/index.js
new file mode 100644
index 000000000..18f02431d
--- /dev/null
+++ b/src/basic/components/layout/index.js
@@ -0,0 +1,5 @@
+/**
+ * Layout 컴포넌트들의 barrel export
+ */
+export { createHeader } from './Header.js';
+export { createGridContainer } from './GridContainer.js';
diff --git a/src/basic/components/modal/ManualColumn.js b/src/basic/components/modal/ManualColumn.js
new file mode 100644
index 000000000..4961cc86d
--- /dev/null
+++ b/src/basic/components/modal/ManualColumn.js
@@ -0,0 +1,70 @@
+/**
+ * 이용 안내 컬럼 컴포넌트
+ * 모달의 실제 내용을 표시하는 컬럼을 생성합니다.
+ */
+export function createManualColumn() {
+ const manualColumn = document.createElement('div');
+ manualColumn.className =
+ 'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300 manual-column';
+ manualColumn.innerHTML = `
+
+ 📖 이용 안내
+
+
💰 할인 정책
+
+
+
개별 상품
+
+ • 키보드 10개↑: 10%
+ • 마우스 10개↑: 15%
+ • 모니터암 10개↑: 20%
+ • 스피커 10개↑: 25%
+
+
+
+
전체 수량
+
• 30개 이상: 25%
+
+
+
특별 할인
+
+ • 화요일: +10%
+ • ⚡번개세일: 20%
+ • 💝추천할인: 5%
+
+
+
+
+
+
🎁 포인트 적립
+
+
+
+
추가
+
+ • 화요일: 2배
+ • 키보드+마우스: +50p
+ • 풀세트: +100p
+ • 10개↑: +20p / 20개↑: +50p / 30개↑: +100p
+
+
+
+
+
+
💡 TIP
+
+ • 화요일 대량구매 = MAX 혜택
+ • ⚡+💝 중복 가능
+ • 상품4 = 품절
+
+
+ `;
+ return manualColumn;
+}
diff --git a/src/basic/components/modal/ManualOverlay.js b/src/basic/components/modal/ManualOverlay.js
new file mode 100644
index 000000000..b7933b37a
--- /dev/null
+++ b/src/basic/components/modal/ManualOverlay.js
@@ -0,0 +1,19 @@
+/**
+ * 이용 안내 오버레이 컴포넌트
+ * 모달의 배경 오버레이를 생성합니다.
+ */
+export function createManualOverlay() {
+ const manualOverlay = document.createElement('div');
+ manualOverlay.className =
+ 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300 manual-overlay';
+ manualOverlay.onclick = function (e) {
+ if (e.target === manualOverlay) {
+ manualOverlay.classList.add('hidden');
+ const manualColumn = document.querySelector('.manual-column');
+ if (manualColumn) {
+ manualColumn.classList.add('translate-x-full');
+ }
+ }
+ };
+ return manualOverlay;
+}
diff --git a/src/basic/components/modal/ManualToggle.js b/src/basic/components/modal/ManualToggle.js
new file mode 100644
index 000000000..19c58899d
--- /dev/null
+++ b/src/basic/components/modal/ManualToggle.js
@@ -0,0 +1,23 @@
+/**
+ * 이용 안내 토글 버튼 컴포넌트
+ * 모달을 열고 닫는 토글 버튼을 생성합니다.
+ */
+export function createManualToggle() {
+ const manualToggle = document.createElement('button');
+ manualToggle.onclick = function () {
+ const manualOverlay = document.querySelector('.manual-overlay');
+ const manualColumn = document.querySelector('.manual-column');
+ if (manualOverlay && manualColumn) {
+ manualOverlay.classList.toggle('hidden');
+ manualColumn.classList.toggle('translate-x-full');
+ }
+ };
+ manualToggle.className =
+ 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50';
+ manualToggle.innerHTML = `
+
+
+
+ `;
+ return manualToggle;
+}
diff --git a/src/basic/components/modal/index.js b/src/basic/components/modal/index.js
new file mode 100644
index 000000000..cdd65036c
--- /dev/null
+++ b/src/basic/components/modal/index.js
@@ -0,0 +1,6 @@
+/**
+ * Modal 컴포넌트들의 barrel export
+ */
+export { createManualOverlay } from './ManualOverlay.js';
+export { createManualToggle } from './ManualToggle.js';
+export { createManualColumn } from './ManualColumn.js';
diff --git a/src/basic/components/order/DiscountInfo.js b/src/basic/components/order/DiscountInfo.js
new file mode 100644
index 000000000..3c16428c0
--- /dev/null
+++ b/src/basic/components/order/DiscountInfo.js
@@ -0,0 +1,25 @@
+/**
+ * 할인 정보 렌더링 컴포넌트
+ * 할인 정보를 표시하는 컴포넌트입니다.
+ */
+export function renderDiscountInfo(discRate, originalTotal, totalAmount) {
+ const discountInfoDiv = document.getElementById('discount-info');
+ if (!discountInfoDiv) return;
+
+ discountInfoDiv.innerHTML = '';
+
+ if (discRate > 0 && totalAmount > 0) {
+ const savedAmount = originalTotal - totalAmount;
+ discountInfoDiv.innerHTML = `
+
+
+ 총 할인율
+ ${(discRate * 100).toFixed(1)}%
+
+
₩${Math.round(
+ savedAmount,
+ ).toLocaleString()} 할인되었습니다
+
+ `;
+ }
+}
diff --git a/src/basic/components/order/LoyaltyPoints.js b/src/basic/components/order/LoyaltyPoints.js
new file mode 100644
index 000000000..209c0ddc7
--- /dev/null
+++ b/src/basic/components/order/LoyaltyPoints.js
@@ -0,0 +1,18 @@
+/**
+ * 포인트 정보 렌더링 컴포넌트
+ * 적립 포인트 정보를 표시하는 컴포넌트입니다.
+ */
+export function renderLoyaltyPoints(points, pointInfo) {
+ const loyaltyPointsDiv = document.getElementById('loyalty-points');
+ if (!loyaltyPointsDiv) return;
+
+ if (points > 0) {
+ loyaltyPointsDiv.innerHTML =
+ `적립 포인트: ${points}p
` +
+ `${pointInfo.detailText}
`;
+ loyaltyPointsDiv.style.display = 'block';
+ } else {
+ loyaltyPointsDiv.textContent = '적립 포인트: 0p';
+ loyaltyPointsDiv.style.display = 'block';
+ }
+}
diff --git a/src/basic/components/order/OrderSummary.js b/src/basic/components/order/OrderSummary.js
new file mode 100644
index 000000000..953114eaa
--- /dev/null
+++ b/src/basic/components/order/OrderSummary.js
@@ -0,0 +1,73 @@
+/**
+ * 주문 요약 컴포넌트
+ * 오른쪽 컬럼의 주문 요약 섹션을 생성합니다.
+ */
+export function createRightColumn() {
+ const rightColumn = document.createElement('div');
+ rightColumn.className = 'bg-black text-white p-8 flex flex-col';
+ rightColumn.innerHTML =
+ createOrderSummaryHeader() +
+ createOrderSummaryDetails() +
+ createCheckoutButton() +
+ createPointsNotice();
+ return rightColumn;
+}
+
+/**
+ * 주문 요약 헤더 컴포넌트
+ */
+function createOrderSummaryHeader() {
+ return `
+
+ `;
+}
+
+/**
+ * 주문 요약 상세 영역 컴포넌트
+ */
+function createOrderSummaryDetails() {
+ return `
+
+
+
+
+
+
+
+ 🎉
+ Tuesday Special 10% Applied
+
+
+
+
+ `;
+}
+
+/**
+ * 체크아웃 버튼 컴포넌트
+ */
+function createCheckoutButton() {
+ return `
+
+ `;
+}
+
+/**
+ * 포인트 안내 컴포넌트
+ */
+function createPointsNotice() {
+ return `
+
+ Free shipping on all orders.
+ Earn loyalty points with purchase.
+
+ `;
+}
diff --git a/src/basic/components/order/index.js b/src/basic/components/order/index.js
new file mode 100644
index 000000000..392927139
--- /dev/null
+++ b/src/basic/components/order/index.js
@@ -0,0 +1,6 @@
+/**
+ * Order 컴포넌트들의 barrel export
+ */
+export { createRightColumn } from './OrderSummary.js';
+export { renderDiscountInfo } from './DiscountInfo.js';
+export { renderLoyaltyPoints } from './LoyaltyPoints.js';
diff --git a/src/basic/components/product/ProductSelector.js b/src/basic/components/product/ProductSelector.js
new file mode 100644
index 000000000..f2db31599
--- /dev/null
+++ b/src/basic/components/product/ProductSelector.js
@@ -0,0 +1,10 @@
+/**
+ * 상품 선택기 컴포넌트
+ * 사용자가 상품을 선택할 수 있는 드롭다운을 생성합니다.
+ */
+export function createProductSelector() {
+ const productSelector = document.createElement('select');
+ productSelector.id = 'product-select';
+ productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
+ return productSelector;
+}
diff --git a/src/basic/components/product/StockInfo.js b/src/basic/components/product/StockInfo.js
new file mode 100644
index 000000000..7725d1b48
--- /dev/null
+++ b/src/basic/components/product/StockInfo.js
@@ -0,0 +1,10 @@
+/**
+ * 재고 정보 컴포넌트
+ * 상품의 재고 상태를 표시하는 컴포넌트를 생성합니다.
+ */
+export function createStockInfo() {
+ const stockInfoElement = document.createElement('div');
+ stockInfoElement.id = 'stock-status';
+ stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
+ return stockInfoElement;
+}
diff --git a/src/basic/components/product/index.js b/src/basic/components/product/index.js
new file mode 100644
index 000000000..96f16eda7
--- /dev/null
+++ b/src/basic/components/product/index.js
@@ -0,0 +1,5 @@
+/**
+ * Product 컴포넌트들의 barrel export
+ */
+export { createProductSelector } from './ProductSelector.js';
+export { createStockInfo } from './StockInfo.js';
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 19ab65b16..0e5a62ea1 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -16,11 +16,7 @@ import {
} from './services/product/ProductService.js';
// DiscountService import
-import {
- calculateTotalDiscountRate,
- createDiscountInfo,
- calculateSavedAmount,
-} from './services/discount/DiscountService.js';
+import { calculateTotalDiscountRate } from './services/discount/DiscountService.js';
// PointService import
import { createPointInfo } from './services/point/PointService.js';
@@ -28,6 +24,40 @@ import { createPointInfo } from './services/point/PointService.js';
// CartService import
import { createInitialCartState, addItemToCart } from './services/cart/CartService.js';
+// Components import (새로운 폴더 구조)
+import {
+ createHeader,
+ createGridContainer,
+ createProductSelector,
+ createStockInfo,
+ createCartDisplay,
+ createRightColumn,
+ createManualOverlay,
+ createManualToggle,
+ createManualColumn,
+ renderCartItem,
+ renderDiscountInfo,
+ renderLoyaltyPoints,
+} from './components/index.js';
+
+// Utils import
+import {
+ createAddToCartButton,
+ createSelectorContainer,
+ createLeftColumn,
+} from './utils/UIRenderer.js';
+
+// Renderers import
+import {
+ renderProductOptions,
+ renderOrderSummaryDetails,
+ renderTuesdaySpecial,
+ renderTotalAmount,
+ renderItemCount,
+ renderStockMessages,
+ updateCartPrices,
+} from './utils/renderers/index.js';
+
// 전역 변수들 (명명 규칙 적용)
let productList;
let bonusPoints = 0;
@@ -73,177 +103,39 @@ function main() {
const root = document.getElementById('app');
- const header = document.createElement('div');
- header.className = 'mb-8';
- header.innerHTML = `
-
- Shopping Cart
- 🛍️ 0 items in cart
- `;
-
- productSelector = document.createElement('select');
- productSelector.id = 'product-select';
- productSelector.className = 'w-full p-3 border border-gray-300 rounded-lg text-base mb-3';
-
- const leftColumn = document.createElement('div');
- leftColumn['className'] = 'bg-white border border-gray-200 p-8 overflow-y-auto';
-
- const gridContainer = document.createElement('div');
- gridContainer.className =
- 'grid grid-cols-1 lg:grid-cols-[1fr_360px] gap-6 flex-1 overflow-hidden';
-
- stockInfoElement = document.createElement('div');
- stockInfoElement.id = 'stock-status';
- stockInfoElement.className = 'text-xs text-red-500 mt-3 whitespace-pre-line';
-
- addToCartButton = document.createElement('button');
- addToCartButton.id = 'add-to-cart';
- addToCartButton.innerHTML = 'Add to Cart';
- addToCartButton.className =
- 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
-
- // 상품 선택/추가/재고 표시 컨테이너
- const selectorContainer = document.createElement('div');
- selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
- selectorContainer.appendChild(productSelector);
- selectorContainer.appendChild(addToCartButton);
- selectorContainer.appendChild(stockInfoElement);
-
- cartDisplayElement = document.createElement('div');
- cartDisplayElement.id = 'cart-items';
-
- leftColumn.appendChild(selectorContainer);
- leftColumn.appendChild(cartDisplayElement);
-
- // 오른쪽 컬럼(주문 요약) 생성
- const rightColumn = document.createElement('div');
- rightColumn.className = 'bg-black text-white p-8 flex flex-col';
- rightColumn.innerHTML = `
-
-
-
-
-
-
-
-
- 🎉
- Tuesday Special 10% Applied
-
-
-
-
-
-
- Free shipping on all orders.
- Earn loyalty points with purchase.
-
- `;
- orderSummaryElement = rightColumn.querySelector('#cart-total');
+ // UI 컴포넌트들 생성
+ const header = createHeader();
- // 이용 안내(오버레이) 관련 요소 생성
- const manualOverlay = document.createElement('div');
- manualOverlay.className = 'fixed inset-0 bg-black/50 z-40 hidden transition-opacity duration-300';
- manualOverlay.onclick = function (e) {
- if (e.target === manualOverlay) {
- manualOverlay.classList.add('hidden');
- manualColumn.classList.add('translate-x-full');
- }
- };
+ productSelector = createProductSelector();
+ addToCartButton = createAddToCartButton();
+ stockInfoElement = createStockInfo();
- const manualToggle = document.createElement('button');
- manualToggle.onclick = function () {
- manualOverlay.classList.toggle('hidden');
- manualColumn.classList.toggle('translate-x-full');
- };
- manualToggle.className =
- 'fixed top-4 right-4 bg-black text-white p-3 rounded-full hover:bg-gray-900 transition-colors z-50';
- manualToggle.innerHTML = `
-
-
-
- `;
-
- const manualColumn = document.createElement('div');
- manualColumn.className =
- 'fixed right-0 top-0 h-full w-80 bg-white shadow-2xl p-6 overflow-y-auto z-50 transform translate-x-full transition-transform duration-300';
- manualColumn.innerHTML = `
-
- 📖 이용 안내
-
-
💰 할인 정책
-
-
-
개별 상품
-
- • 키보드 10개↑: 10%
- • 마우스 10개↑: 15%
- • 모니터암 10개↑: 20%
- • 스피커 10개↑: 25%
-
-
-
-
전체 수량
-
• 30개 이상: 25%
-
-
-
특별 할인
-
- • 화요일: +10%
- • ⚡번개세일: 20%
- • 💝추천할인: 5%
-
-
-
-
-
-
🎁 포인트 적립
-
-
-
-
추가
-
- • 화요일: 2배
- • 키보드+마우스: +50p
- • 풀세트: +100p
- • 10개↑: +20p / 20개↑: +50p / 30개↑: +100p
-
-
-
-
-
-
💡 TIP
-
- • 화요일 대량구매 = MAX 혜택
- • ⚡+💝 중복 가능
- • 상품4 = 품절
-
-
- `;
-
- gridContainer.appendChild(leftColumn);
- gridContainer.appendChild(rightColumn);
+ const selectorContainer = createSelectorContainer(
+ productSelector,
+ addToCartButton,
+ stockInfoElement,
+ );
+ cartDisplayElement = createCartDisplay();
+
+ const leftColumn = createLeftColumn(selectorContainer, cartDisplayElement);
+ const rightColumn = createRightColumn();
+
+ const manualOverlay = createManualOverlay();
+ const manualToggle = createManualToggle();
+ const manualColumn = createManualColumn();
+
+ const gridContainer = createGridContainer(leftColumn, rightColumn);
+
+ // DOM에 요소들 추가
manualOverlay.appendChild(manualColumn);
root.appendChild(header);
root.appendChild(gridContainer);
root.appendChild(manualToggle);
root.appendChild(manualOverlay);
+ // orderSummaryElement 참조 설정
+ orderSummaryElement = rightColumn.querySelector('#cart-total');
+
// 상품 옵션, 장바구니, 가격 등 초기 렌더링
updateProductOptions();
calculateCartSummary();
@@ -354,107 +246,16 @@ function calculateTotalDiscount(subTot, itemCount, currentAmount) {
// 주문 요약 상세 내역 갱신
function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) {
- const summaryDetails = document.getElementById('summary-details');
- summaryDetails.innerHTML = '';
-
- if (subTot > 0) {
- // 각 상품별 정보 표시
- for (let i = 0; i < cartItems.length; i++) {
- let curItem;
- for (let j = 0; j < productList.length; j++) {
- if (productList[j].id === cartItems[i].id) {
- curItem = productList[j];
- break;
- }
- }
-
- const quantityElem = cartItems[i].querySelector('.quantity-number');
- const quantity = parseInt(quantityElem.textContent);
- const itemTotal = curItem.price * quantity;
-
- summaryDetails.innerHTML += `
-
- ${curItem.name} x ${quantity}
- ₩${itemTotal.toLocaleString()}
-
- `;
- }
-
- // 소계 표시
- summaryDetails.innerHTML += `
-
-
- Subtotal
- ₩${subTot.toLocaleString()}
-
- `;
-
- // 할인 정보 표시 - DiscountService 사용
- const discountInfo = createDiscountInfo(itemDiscounts, itemCount);
- discountInfo.forEach(function (discount) {
- const colorClass = discount.type === 'tuesday' ? 'text-purple-400' : 'text-green-400';
- const icon = discount.type === 'tuesday' ? '🌟' : discount.type === 'bulk' ? '🎉' : '';
- summaryDetails.innerHTML += `
-
- ${icon} ${discount.name}
- -${discount.rate}%
-
- `;
- });
-
- // 배송비 표시
- summaryDetails.innerHTML += `
-
- Shipping
- Free
-
- `;
- }
+ renderOrderSummaryDetails(cartItems, productList, subTot, itemDiscounts);
}
// 상품 선택 옵션 렌더링 및 재고 상태 표시
function updateProductOptions() {
- let opt;
- let discountText;
-
- productSelector.innerHTML = '';
+ renderProductOptions(productSelector, productList);
// ProductService의 getTotalStock 함수 사용
const totalStock = getTotalStock(productList);
- // 각 상품별 옵션 생성
- for (let i = 0; i < productList.length; i++) {
- (function () {
- const item = productList[i];
- opt = document.createElement('option');
- opt.value = item.id;
- discountText = '';
-
- if (item.onSale) discountText += ' ⚡SALE';
- if (item.suggestSale) discountText += ' 💝추천';
-
- if (item.quantity === 0) {
- opt.textContent = `${item.name} - ${item.price}원 (품절)${discountText}`;
- opt.disabled = true;
- opt.className = 'text-gray-400';
- } else {
- if (item.onSale && item.suggestSale) {
- opt.textContent = `⚡💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (25% SUPER SALE!)`;
- opt.className = 'text-purple-600 font-bold';
- } else if (item.onSale) {
- opt.textContent = `⚡${item.name} - ${item.originalPrice}원 → ${item.price}원 (20% SALE!)`;
- opt.className = 'text-red-500 font-bold';
- } else if (item.suggestSale) {
- opt.textContent = `💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (5% 추천할인!)`;
- opt.className = 'text-blue-500 font-bold';
- } else {
- opt.textContent = `${item.name} - ${item.price}원${discountText}`;
- }
- }
- productSelector.appendChild(opt);
- })();
- }
-
if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
productSelector.style.borderColor = 'orange';
} else {
@@ -464,7 +265,6 @@ function updateProductOptions() {
// 장바구니, 할인, 포인트 등 계산 및 화면 갱신
function calculateCartSummary() {
- let savedAmount;
let points;
let previousCount;
@@ -493,22 +293,17 @@ function calculateCartSummary() {
const discRate = discountRate;
// 화요일 특별 할인 UI 표시
- const tuesdaySpecial = document.getElementById('tuesday-special');
- if (isTuesday && totalAmount > 0) {
- tuesdaySpecial.classList.remove('hidden');
- } else {
- tuesdaySpecial.classList.add('hidden');
- }
+ renderTuesdaySpecial(isTuesday, totalAmount);
+
// 장바구니 수량 표시 갱신
- document.getElementById('item-count').textContent = `🛍️ ${itemCount} items in cart`;
+ renderItemCount(itemCount);
// 주문 요약(상품별, 할인, 배송 등) 갱신
updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts);
+
// 총 결제 금액 표시 갱신
- const totalDiv = orderSummaryElement.querySelector('.text-2xl');
- if (totalDiv) {
- totalDiv.textContent = `₩${Math.round(totalAmount).toLocaleString()}`;
- }
+ renderTotalAmount(totalAmount, orderSummaryElement);
+
// 적립 포인트 표시 갱신 - PointService 사용
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
@@ -527,41 +322,21 @@ function calculateCartSummary() {
const pointInfo = createPointInfo(totalAmount, cartItems);
points = pointInfo.totalPoints;
- if (points > 0) {
- loyaltyPointsDiv.textContent = `적립 포인트: ${points}p`;
- loyaltyPointsDiv.style.display = 'block';
- } else {
- loyaltyPointsDiv.textContent = '적립 포인트: 0p';
- loyaltyPointsDiv.style.display = 'block';
- }
+ renderLoyaltyPoints(points, pointInfo);
}
+
// 할인 정보 표시 갱신
- const discountInfoDiv = document.getElementById('discount-info');
- discountInfoDiv.innerHTML = '';
-
- if (discRate > 0 && totalAmount > 0) {
- savedAmount = calculateSavedAmount(originalTotal, totalAmount);
- discountInfoDiv.innerHTML = `
-
-
- 총 할인율
- ${(discRate * 100).toFixed(1)}%
-
-
₩${Math.round(
- savedAmount,
- ).toLocaleString()} 할인되었습니다
-
- `;
- }
+ renderDiscountInfo(discRate, originalTotal, totalAmount);
+
// 장바구니 수량 변화 애니메이션 표시
const itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
- itemCountElement.textContent = `🛍️ ${itemCount} items in cart`;
if (previousCount !== itemCount) {
itemCountElement.setAttribute('data-changed', 'true');
}
}
+
// 재고 부족/품절 안내 메시지 갱신
updateStockMessages();
@@ -612,60 +387,11 @@ function updateStockMessages() {
// 품절 상품 조회
const outOfStockProducts = getOutOfStockProducts(productList);
- let stockMsg = '';
-
- // 재고 부족 상품 메시지
- lowStockProducts.forEach((item) => {
- stockMsg += `${item.name}: 재고 부족 (${item.quantity}개 남음)\n`;
- });
-
- // 품절 상품 메시지
- outOfStockProducts.forEach((item) => {
- stockMsg += `${item.name}: 품절\n`;
- });
-
- stockInfoElement.textContent = stockMsg;
-}
-
-// 상품 가격 렌더링 로직 분리
-function renderProductPrice(product, priceDiv, nameDiv) {
- if (product.onSale && product.suggestSale) {
- priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
- nameDiv.textContent = `⚡💝${product.name}`;
- } else if (product.onSale) {
- priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
- nameDiv.textContent = `⚡${product.name}`;
- } else if (product.suggestSale) {
- priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
- nameDiv.textContent = `💝${product.name}`;
- } else {
- priceDiv.textContent = `₩${product.price.toLocaleString()}`;
- nameDiv.textContent = product.name;
- }
+ renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement);
}
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
-function updateCartPrices() {
- const cartItems = cartDisplayElement.children;
-
- const productMap = productList.reduce((map, product) => {
- map[product.id] = product;
- return map;
- }, {});
-
- // 각 카트 아이템을 순회하며 가격/이름 업데이트 (productMap 사용)
- for (const cartItem of cartItems) {
- const product = productMap[cartItem.id];
-
- if (product) {
- const priceDiv = cartItem.querySelector('.text-lg');
- const nameDiv = cartItem.querySelector('h3');
- renderProductPrice(product, priceDiv, nameDiv);
- }
- }
-
- calculateCartSummary();
-}
+// updateCartPrices 함수는 utils/renderers/CartRenderer.js에서 import됨
// CartService를 사용한 장바구니 아이템 추가 함수
function addItemToCartUI(productId, quantity = 1) {
@@ -690,65 +416,7 @@ function renderCartItems() {
const product = productService.getProductById(item.id);
if (!product) return;
- const newItem = document.createElement('div');
- newItem.id = item.id;
- newItem.className =
- 'grid grid-cols-[80px_1fr_auto] gap-5 py-5 border-b border-gray-100 first:pt-0 last:border-b-0 last:pb-0';
- newItem.innerHTML = `
-
-
-
${
- product.onSale && product.suggestSale
- ? '⚡💝'
- : product.onSale
- ? '⚡'
- : product.suggestSale
- ? '💝'
- : ''
- }${product.name}
-
PRODUCT
-
${
- product.onSale || product.suggestSale
- ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`
- : `₩${product.price.toLocaleString()}`
- }
-
-
- ${
- item.quantity
- }
-
-
-
-
-
${
- product.onSale || product.suggestSale
- ? `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`
- : `₩${product.price.toLocaleString()}`
- }
-
Remove
-
- `;
+ const newItem = renderCartItem(item, product);
cartDisplayElement.appendChild(newItem);
});
}
From 5a80ddedcfa809db72f469ebfcbf4165a3e5cf91 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 23:17:12 +0900
Subject: [PATCH 26/46] =?UTF-8?q?feat:=20Cart=20store=EC=99=80=20actiion?=
=?UTF-8?q?=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 244 ++++++++++++++++++++---------------
src/basic/store/cartStore.js | 63 +++++++++
2 files changed, 206 insertions(+), 101 deletions(-)
create mode 100644 src/basic/store/cartStore.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 0e5a62ea1..6317a4842 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -21,8 +21,8 @@ import { calculateTotalDiscountRate } from './services/discount/DiscountService.
// PointService import
import { createPointInfo } from './services/point/PointService.js';
-// CartService import
-import { createInitialCartState, addItemToCart } from './services/cart/CartService.js';
+// cartStore import
+import { cartStore, cartStoreActions } from './store/cartStore.js';
// Components import (새로운 폴더 구조)
import {
@@ -35,7 +35,6 @@ import {
createManualOverlay,
createManualToggle,
createManualColumn,
- renderCartItem,
renderDiscountInfo,
renderLoyaltyPoints,
} from './components/index.js';
@@ -58,48 +57,39 @@ import {
updateCartPrices,
} from './utils/renderers/index.js';
-// 전역 변수들 (명명 규칙 적용)
-let productList;
-let bonusPoints = 0;
+// UI 요소들 (cartStore와 분리)
let stockInfoElement;
-let itemCount;
-let lastSelectedProductId;
let productSelector;
let addToCartButton;
-let totalAmount = 0;
let cartDisplayElement;
let orderSummaryElement;
-let cartState; // CartService를 위한 상태 추가
// ProductService 래퍼 (CartService에서 사용하기 위한 인터페이스)
const productService = {
- getProductById: (productId) => getProductById(productList, productId),
+ getProductById: (productId) => getProductById(cartStore.products, productId),
decreaseStock: (productId, quantity) => {
- const result = decreaseStock(productList, productId, quantity);
+ const result = decreaseStock(cartStore.products, productId, quantity);
if (result.success) {
- productList = result.products;
+ cartStore.products = result.products;
}
return result;
},
increaseStock: (productId, quantity) => {
- const result = increaseStock(productList, productId, quantity);
+ const result = increaseStock(cartStore.products, productId, quantity);
if (result.success) {
- productList = result.products;
+ cartStore.products = result.products;
}
return result;
},
};
function main() {
- totalAmount = 0;
- itemCount = 0;
- lastSelectedProductId = null;
+ // cartStore 초기화
+ cartStoreActions.reset();
// 상품 정보 초기화 - ProductService 사용
- productList = initializeProducts();
-
- // CartService 상태 초기화
- cartState = createInitialCartState();
+ const initialProducts = initializeProducts();
+ cartStoreActions.setProducts(initialProducts);
const root = document.getElementById('app');
@@ -144,13 +134,13 @@ function main() {
const lightningDelay = Math.random() * UI_CONSTANTS.LIGHTNING_SALE_DELAY;
setTimeout(() => {
setInterval(function () {
- const luckyIdx = Math.floor(Math.random() * productList.length);
- const luckyItem = productList[luckyIdx];
+ const luckyIdx = Math.floor(Math.random() * cartStore.products.length);
+ const luckyItem = cartStore.products[luckyIdx];
if (luckyItem.quantity > 0 && !luckyItem.onSale) {
// ProductService의 applySale 함수 사용
- const result = applySale(productList, luckyItem.id, 0.2);
+ const result = applySale(cartStore.products, luckyItem.id, 0.2);
if (result.success) {
- productList = result.products;
+ cartStore.products = result.products;
alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
updateProductOptions();
updateCartPrices();
@@ -162,13 +152,13 @@ function main() {
// 추천 할인(다른 상품 5% 할인) 타이머 설정
setTimeout(function () {
setInterval(function () {
- if (lastSelectedProductId && cartDisplayElement.children.length > 0) {
+ if (cartStore.lastSelectedId && cartDisplayElement.children.length > 0) {
let suggest = null;
- for (let k = 0; k < productList.length; k++) {
- if (productList[k].id !== lastSelectedProductId) {
- if (productList[k].quantity > 0) {
- if (!productList[k].suggestSale) {
- suggest = productList[k];
+ for (let k = 0; k < cartStore.products.length; k++) {
+ if (cartStore.products[k].id !== cartStore.lastSelectedId) {
+ if (cartStore.products[k].quantity > 0) {
+ if (!cartStore.products[k].suggestSale) {
+ suggest = cartStore.products[k];
break;
}
}
@@ -177,9 +167,9 @@ function main() {
if (suggest) {
alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
- const result = applySuggestSale(productList, suggest.id, 0.05);
+ const result = applySuggestSale(cartStore.products, suggest.id, 0.05);
if (result.success) {
- productList = result.products;
+ cartStore.products = result.products;
updateProductOptions();
updateCartPrices();
}
@@ -199,9 +189,9 @@ function processCartItems(cartItems) {
for (let i = 0; i < cartItems.length; i++) {
// 상품 찾기
let curItem;
- for (let j = 0; j < productList.length; j++) {
- if (productList[j].id === cartItems[i].id) {
- curItem = productList[j];
+ for (let j = 0; j < cartStore.products.length; j++) {
+ if (cartStore.products[j].id === cartItems[i].id) {
+ curItem = cartStore.products[j];
break;
}
}
@@ -246,15 +236,15 @@ function calculateTotalDiscount(subTot, itemCount, currentAmount) {
// 주문 요약 상세 내역 갱신
function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) {
- renderOrderSummaryDetails(cartItems, productList, subTot, itemDiscounts);
+ renderOrderSummaryDetails(cartItems, cartStore.products, subTot, itemDiscounts);
}
// 상품 선택 옵션 렌더링 및 재고 상태 표시
function updateProductOptions() {
- renderProductOptions(productSelector, productList);
+ renderProductOptions(productSelector, cartStore.products);
// ProductService의 getTotalStock 함수 사용
- const totalStock = getTotalStock(productList);
+ const totalStock = getTotalStock(cartStore.products);
if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
productSelector.style.borderColor = 'orange';
@@ -278,61 +268,67 @@ function calculateCartSummary() {
itemDiscounts,
} = processCartItems(cartItems);
- totalAmount = calcTotalAmount;
- itemCount = calcItemCount;
+ // cartStore 상태 업데이트
+ cartStore.totalAmount = calcTotalAmount;
+ cartStore.itemCount = calcItemCount;
const originalTotal = subTot;
// 할인 총합 계산 적용
const { finalAmount, discountRate, isTuesday } = calculateTotalDiscount(
subTot,
- itemCount,
- totalAmount,
+ cartStore.itemCount,
+ cartStore.totalAmount,
);
- totalAmount = finalAmount;
+ cartStore.totalAmount = finalAmount;
const discRate = discountRate;
// 화요일 특별 할인 UI 표시
- renderTuesdaySpecial(isTuesday, totalAmount);
+ renderTuesdaySpecial(isTuesday, cartStore.totalAmount);
// 장바구니 수량 표시 갱신
- renderItemCount(itemCount);
+ renderItemCount(cartStore.itemCount);
// 주문 요약(상품별, 할인, 배송 등) 갱신
- updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts);
+ updateOrderSummary(cartItems, subTot, cartStore.itemCount, itemDiscounts);
// 총 결제 금액 표시 갱신
- renderTotalAmount(totalAmount, orderSummaryElement);
+ renderTotalAmount(cartStore.totalAmount, orderSummaryElement);
// 적립 포인트 표시 갱신 - PointService 사용
const loyaltyPointsDiv = document.getElementById('loyalty-points');
if (loyaltyPointsDiv) {
- // CartService 상태에서 장바구니 아이템 정보 추출
- const cartItems = cartState.items.map((item) => {
- const product = productService.getProductById(item.id);
+ // cartStore에서 장바구니 아이템 정보 추출
+ const cartItemsData = Array.from(cartItems).map((item) => {
+ const productId = item.id;
+ const product = productService.getProductById(productId);
+ const quantity = parseInt(item.querySelector('.quantity-number').textContent);
return {
- id: item.id,
- quantity: item.quantity,
+ id: productId,
+ quantity,
name: product ? product.name : '',
price: product ? product.price : 0,
};
});
// PointService를 사용하여 포인트 계산
- const pointInfo = createPointInfo(totalAmount, cartItems);
+ const pointInfo = createPointInfo(cartStore.totalAmount, cartItemsData);
points = pointInfo.totalPoints;
+ // cartStore에 포인트 업데이트
+ cartStoreActions.updateBonusPoints(points);
+
renderLoyaltyPoints(points, pointInfo);
}
// 할인 정보 표시 갱신
- renderDiscountInfo(discRate, originalTotal, totalAmount);
+ renderDiscountInfo(discRate, originalTotal, cartStore.totalAmount);
// 장바구니 수량 변화 애니메이션 표시
const itemCountElement = document.getElementById('item-count');
if (itemCountElement) {
previousCount = parseInt(itemCountElement.textContent.match(/\d+/) || 0);
- if (previousCount !== itemCount) {
+ if (previousCount !== cartStore.itemCount) {
itemCountElement.setAttribute('data-changed', 'true');
}
}
@@ -350,27 +346,30 @@ const renderBonusPoints = function () {
return;
}
- // CartService 상태에서 장바구니 아이템 정보 추출
- const cartItems = cartState.items.map((item) => {
- const product = productService.getProductById(item.id);
+ // cartStore에서 장바구니 아이템 정보 추출
+ const cartItems = cartDisplayElement.children;
+ const cartItemsData = Array.from(cartItems).map((item) => {
+ const productId = item.id;
+ const product = productService.getProductById(productId);
+ const quantity = parseInt(item.querySelector('.quantity-number').textContent);
return {
- id: item.id,
- quantity: item.quantity,
+ id: productId,
+ quantity,
name: product ? product.name : '',
price: product ? product.price : 0,
};
});
// PointService를 사용하여 포인트 정보 생성
- const pointInfo = createPointInfo(totalAmount, cartItems);
- bonusPoints = pointInfo.totalPoints;
+ const pointInfo = createPointInfo(cartStore.totalAmount, cartItemsData);
+ cartStore.bonusPoints = pointInfo.totalPoints;
const ptsTag = document.getElementById('loyalty-points');
if (ptsTag) {
- if (bonusPoints > 0) {
+ if (cartStore.bonusPoints > 0) {
ptsTag.innerHTML =
- `적립 포인트: ${bonusPoints}p
` +
+ `적립 포인트: ${cartStore.bonusPoints}p
` +
`${pointInfo.detailText}
`;
ptsTag.style.display = 'block';
} else {
@@ -383,9 +382,9 @@ const renderBonusPoints = function () {
// 재고 부족/품절 안내 메시지 생성 및 표시
function updateStockMessages() {
// 재고 부족 상품 조회
- const lowStockProducts = getLowStockProducts(productList);
+ const lowStockProducts = getLowStockProducts(cartStore.products);
// 품절 상품 조회
- const outOfStockProducts = getOutOfStockProducts(productList);
+ const outOfStockProducts = getOutOfStockProducts(cartStore.products);
renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement);
}
@@ -393,37 +392,85 @@ function updateStockMessages() {
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
// updateCartPrices 함수는 utils/renderers/CartRenderer.js에서 import됨
-// CartService를 사용한 장바구니 아이템 추가 함수
+// cartStore를 사용한 장바구니 아이템 추가 함수
function addItemToCartUI(productId, quantity = 1) {
- const result = addItemToCart(cartState, productId, quantity, productService);
+ const success = cartStoreActions.addToCart(productId, quantity);
- if (result.success) {
- const { cartState: newCartState } = result;
- cartState = newCartState;
- renderCartItems();
+ if (success) {
+ // DOM에 아이템 추가
+ addItemToCartDOM(productId, quantity);
calculateCartSummary();
- lastSelectedProductId = productId;
+ cartStoreActions.setLastSelectedId(productId);
} else {
- alert(result.message);
+ alert('재고가 부족하거나 상품을 찾을 수 없습니다.');
}
}
-// CartService 상태를 기반으로 UI 렌더링
-function renderCartItems() {
- cartDisplayElement.innerHTML = '';
+// DOM에 장바구니 아이템 추가
+function addItemToCartDOM(productId, quantity = 1) {
+ const product = productService.getProductById(productId);
+ if (!product) return;
- cartState.items.forEach((item) => {
- const product = productService.getProductById(item.id);
- if (!product) return;
+ // 기존 아이템이 있는지 확인
+ const existingItem = cartDisplayElement.querySelector(`#${productId}`);
- const newItem = renderCartItem(item, product);
+ if (existingItem) {
+ // 기존 아이템 수량 증가
+ const qtyElement = existingItem.querySelector('.quantity-number');
+ const currentQty = parseInt(qtyElement.textContent);
+ qtyElement.textContent = currentQty + quantity;
+ } else {
+ // 새 아이템 생성
+ const newItem = createCartItemElement(product, quantity);
cartDisplayElement.appendChild(newItem);
- });
+ }
+}
+
+// 장바구니 아이템 DOM 요소 생성
+function createCartItemElement(product, quantity) {
+ const itemDiv = document.createElement('div');
+ itemDiv.id = product.id;
+ itemDiv.className =
+ 'cart-item bg-white rounded-lg shadow-md p-4 mb-4 border-b border-gray-200 first:pt-0 last:border-b-0';
+
+ itemDiv.innerHTML = `
+
+
+
+ ${product.name.charAt(0)}
+
+
+
${product.name}
+
₩${product.price.toLocaleString()}
+
+
+
+
+ ${quantity}
+
+
+
+
+ `;
+
+ return itemDiv;
}
main();
-// 장바구니 추가 버튼 클릭 이벤트 - CartService 사용
+// 장바구니 추가 버튼 클릭 이벤트 - cartStore 사용
addToCartButton.addEventListener('click', function () {
const selItem = productSelector.value;
@@ -431,7 +478,7 @@ addToCartButton.addEventListener('click', function () {
return;
}
- // CartService를 사용하여 장바구니에 추가
+ // cartStore를 사용하여 장바구니에 추가
addItemToCartUI(selItem, 1);
});
@@ -444,7 +491,7 @@ cartDisplayElement.addEventListener('click', function (event) {
const itemElem = document.getElementById(prodId);
// ProductService의 getProductById 함수 사용
- const prod = getProductById(productList, prodId);
+ const prod = getProductById(cartStore.products, prodId);
if (tgt.classList.contains('quantity-change')) {
const qtyChange = parseInt(tgt.dataset.change);
@@ -454,17 +501,15 @@ cartDisplayElement.addEventListener('click', function (event) {
if (newQty > 0 && newQty <= prod.quantity + currentQty) {
qtyElem.textContent = newQty;
- // ProductService의 decreaseStock 함수 사용
- const result = decreaseStock(productList, prodId, qtyChange);
- if (result.success) {
- productList = result.products;
+ // cartStore를 사용하여 재고 조정
+ if (qtyChange > 0) {
+ cartStoreActions.removeFromCart(prodId, qtyChange);
+ } else {
+ cartStoreActions.addToCart(prodId, -qtyChange);
}
} else if (newQty <= 0) {
- // ProductService의 increaseStock 함수 사용
- const result = increaseStock(productList, prodId, currentQty);
- if (result.success) {
- productList = result.products;
- }
+ // cartStore를 사용하여 재고 복원
+ cartStoreActions.removeFromCart(prodId, currentQty);
itemElem.remove();
} else {
alert('재고가 부족합니다.');
@@ -472,11 +517,8 @@ cartDisplayElement.addEventListener('click', function (event) {
} else if (tgt.classList.contains('remove-item')) {
const qtyElem = itemElem.querySelector('.quantity-number');
const remQty = parseInt(qtyElem.textContent);
- // ProductService의 increaseStock 함수 사용
- const result = increaseStock(productList, prodId, remQty);
- if (result.success) {
- productList = result.products;
- }
+ // cartStore를 사용하여 재고 복원
+ cartStoreActions.removeFromCart(prodId, remQty);
itemElem.remove();
}
diff --git a/src/basic/store/cartStore.js b/src/basic/store/cartStore.js
new file mode 100644
index 000000000..f27b66be5
--- /dev/null
+++ b/src/basic/store/cartStore.js
@@ -0,0 +1,63 @@
+export const cartStore = {
+ products: [],
+ totalAmount: 0,
+ itemCount: 0,
+ bonusPoints: 0,
+ lastSelectedId: null,
+};
+
+export const cartStoreActions = {
+ setProducts(products) {
+ cartStore.products = products.map((p) => ({
+ ...p,
+ originalQuantity: p.quantity, // 최초 재고 보존
+ quantity: p.quantity,
+ }));
+ },
+
+ addToCart(productId, quantity = 1) {
+ if (quantity <= 0) return false;
+
+ const product = cartStore.products.find((p) => p.id === productId);
+ if (!product || product.quantity < quantity) return false;
+
+ product.quantity -= quantity;
+ cartStore.itemCount += quantity;
+ cartStore.totalAmount += product.price * quantity;
+ cartStore.lastSelectedId = productId;
+
+ return true;
+ },
+
+ removeFromCart(productId, quantity = 1) {
+ if (quantity <= 0) return false;
+
+ const product = cartStore.products.find((p) => p.id === productId);
+ if (!product) return false;
+
+ product.quantity += quantity;
+ cartStore.itemCount -= quantity;
+ cartStore.totalAmount -= product.price * quantity;
+
+ return true;
+ },
+
+ updateBonusPoints(points) {
+ cartStore.bonusPoints = points;
+ },
+
+ setLastSelectedId(productId) {
+ cartStore.lastSelectedId = productId;
+ },
+
+ reset() {
+ cartStore.totalAmount = 0;
+ cartStore.itemCount = 0;
+ cartStore.bonusPoints = 0;
+ cartStore.lastSelectedId = null;
+ cartStore.products = cartStore.products.map((p) => ({
+ ...p,
+ quantity: p.originalQuantity,
+ }));
+ },
+};
From b6aea425f4f38bc74173a34a73958162b9d8b3a2 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 23:23:41 +0900
Subject: [PATCH 27/46] =?UTF-8?q?refactor:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?=
=?UTF-8?q?=EB=8B=88=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 74 ++++--
src/basic/services/cart/CartService.js | 330 +++++++++++++++++++++++++
2 files changed, 382 insertions(+), 22 deletions(-)
create mode 100644 src/basic/services/cart/CartService.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 6317a4842..940af307a 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -21,6 +21,14 @@ import { calculateTotalDiscountRate } from './services/discount/DiscountService.
// PointService import
import { createPointInfo } from './services/point/PointService.js';
+// CartService import
+import {
+ createInitialCartState,
+ addItemToCart,
+ updateCartItemQuantity,
+ removeItemFromCart,
+} from './services/cart/CartService.js';
+
// cartStore import
import { cartStore, cartStoreActions } from './store/cartStore.js';
@@ -64,6 +72,9 @@ let addToCartButton;
let cartDisplayElement;
let orderSummaryElement;
+// CartService를 위한 상태 관리
+let cartState = createInitialCartState();
+
// ProductService 래퍼 (CartService에서 사용하기 위한 인터페이스)
const productService = {
getProductById: (productId) => getProductById(cartStore.products, productId),
@@ -91,6 +102,9 @@ function main() {
const initialProducts = initializeProducts();
cartStoreActions.setProducts(initialProducts);
+ // CartService 상태 초기화
+ cartState = createInitialCartState();
+
const root = document.getElementById('app');
// UI 컴포넌트들 생성
@@ -152,10 +166,10 @@ function main() {
// 추천 할인(다른 상품 5% 할인) 타이머 설정
setTimeout(function () {
setInterval(function () {
- if (cartStore.lastSelectedId && cartDisplayElement.children.length > 0) {
+ if (cartState.lastSelectedProductId && cartDisplayElement.children.length > 0) {
let suggest = null;
for (let k = 0; k < cartStore.products.length; k++) {
- if (cartStore.products[k].id !== cartStore.lastSelectedId) {
+ if (cartStore.products[k].id !== cartState.lastSelectedProductId) {
if (cartStore.products[k].quantity > 0) {
if (!cartStore.products[k].suggestSale) {
suggest = cartStore.products[k];
@@ -392,17 +406,23 @@ function updateStockMessages() {
// 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
// updateCartPrices 함수는 utils/renderers/CartRenderer.js에서 import됨
-// cartStore를 사용한 장바구니 아이템 추가 함수
+// CartService를 사용한 장바구니 아이템 추가 함수
function addItemToCartUI(productId, quantity = 1) {
- const success = cartStoreActions.addToCart(productId, quantity);
+ const {
+ success,
+ cartState: newCartState,
+ message,
+ } = addItemToCart(cartState, productId, quantity, productService);
if (success) {
+ // CartService 상태 업데이트
+ cartState = newCartState;
+
// DOM에 아이템 추가
addItemToCartDOM(productId, quantity);
calculateCartSummary();
- cartStoreActions.setLastSelectedId(productId);
} else {
- alert('재고가 부족하거나 상품을 찾을 수 없습니다.');
+ alert(message || '재고가 부족하거나 상품을 찾을 수 없습니다.');
}
}
@@ -470,7 +490,7 @@ function createCartItemElement(product, quantity) {
main();
-// 장바구니 추가 버튼 클릭 이벤트 - cartStore 사용
+// 장바구니 추가 버튼 클릭 이벤트 - CartService 사용
addToCartButton.addEventListener('click', function () {
const selItem = productSelector.value;
@@ -478,7 +498,7 @@ addToCartButton.addEventListener('click', function () {
return;
}
- // cartStore를 사용하여 장바구니에 추가
+ // CartService를 사용하여 장바구니에 추가
addItemToCartUI(selItem, 1);
});
@@ -499,27 +519,37 @@ cartDisplayElement.addEventListener('click', function (event) {
const currentQty = parseInt(qtyElem.textContent);
const newQty = currentQty + qtyChange;
- if (newQty > 0 && newQty <= prod.quantity + currentQty) {
- qtyElem.textContent = newQty;
- // cartStore를 사용하여 재고 조정
- if (qtyChange > 0) {
- cartStoreActions.removeFromCart(prodId, qtyChange);
+ if (newQty > 0) {
+ // CartService를 사용하여 수량 변경
+ const result = updateCartItemQuantity(cartState, prodId, newQty, productService);
+ if (result.success) {
+ cartState = result.cartState;
+ qtyElem.textContent = newQty;
} else {
- cartStoreActions.addToCart(prodId, -qtyChange);
+ alert(result.message || '재고가 부족합니다.');
}
- } else if (newQty <= 0) {
- // cartStore를 사용하여 재고 복원
- cartStoreActions.removeFromCart(prodId, currentQty);
- itemElem.remove();
} else {
- alert('재고가 부족합니다.');
+ // CartService를 사용하여 상품 제거
+ const result = removeItemFromCart(cartState, prodId, productService);
+ if (result.success) {
+ cartState = result.cartState;
+ itemElem.remove();
+ } else {
+ alert(result.message || '상품 제거에 실패했습니다.');
+ }
}
} else if (tgt.classList.contains('remove-item')) {
const qtyElem = itemElem.querySelector('.quantity-number');
const remQty = parseInt(qtyElem.textContent);
- // cartStore를 사용하여 재고 복원
- cartStoreActions.removeFromCart(prodId, remQty);
- itemElem.remove();
+
+ // CartService를 사용하여 상품 제거
+ const result = removeItemFromCart(cartState, prodId, productService);
+ if (result.success) {
+ cartState = result.cartState;
+ itemElem.remove();
+ } else {
+ alert(result.message || '상품 제거에 실패했습니다.');
+ }
}
if (prod && prod.quantity < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
diff --git a/src/basic/services/cart/CartService.js b/src/basic/services/cart/CartService.js
new file mode 100644
index 000000000..c531535a4
--- /dev/null
+++ b/src/basic/services/cart/CartService.js
@@ -0,0 +1,330 @@
+/**
+ * 장바구니 관련 비즈니스 로직을 담당하는 함수들
+ */
+
+/**
+ * 장바구니 상태 초기화
+ */
+export function createInitialCartState() {
+ return {
+ items: [],
+ totalAmount: 0,
+ itemCount: 0,
+ lastSelectedProductId: null,
+ };
+}
+
+/**
+ * 장바구니에 상품 추가
+ */
+export function addItemToCart(cartState, productId, quantity, productService) {
+ const product = productService.getProductById(productId);
+
+ if (!product) {
+ return {
+ success: false,
+ message: '상품을 찾을 수 없습니다.',
+ cartState,
+ };
+ }
+
+ if (product.quantity < quantity) {
+ return {
+ success: false,
+ message: '재고가 부족합니다.',
+ cartState,
+ };
+ }
+
+ const existingItem = cartState.items.find((item) => item.id === productId);
+
+ if (existingItem) {
+ // 기존 상품의 추가 수량이 재고를 초과하는지 확인
+ if (product.quantity < quantity) {
+ return {
+ success: false,
+ message: '재고가 부족합니다.',
+ cartState,
+ };
+ }
+
+ // 기존 상품 수량 증가
+ const updatedItems = cartState.items.map((item) =>
+ item.id === productId ? { ...item, quantity: item.quantity + quantity } : item,
+ );
+
+ const newCartState = {
+ ...cartState,
+ items: updatedItems,
+ lastSelectedProductId: productId,
+ };
+
+ // 재고 감소
+ const stockResult = productService.decreaseStock(productId, quantity);
+ if (!stockResult.success) {
+ return {
+ success: false,
+ message: stockResult.message || '재고 업데이트에 실패했습니다.',
+ cartState,
+ };
+ }
+
+ return {
+ success: true,
+ cartState: updateCartSummary(newCartState, productService),
+ };
+ } else {
+ // 새 상품 추가
+ const newItem = {
+ id: productId,
+ name: product.name,
+ price: product.price,
+ originalPrice: product.originalPrice || product.price,
+ quantity,
+ onSale: product.onSale || false,
+ suggestSale: product.suggestSale || false,
+ };
+
+ const newCartState = {
+ ...cartState,
+ items: [...cartState.items, newItem],
+ lastSelectedProductId: productId,
+ };
+
+ // 재고 감소
+ const stockResult = productService.decreaseStock(productId, quantity);
+ if (!stockResult.success) {
+ return {
+ success: false,
+ message: stockResult.message || '재고 업데이트에 실패했습니다.',
+ cartState,
+ };
+ }
+
+ return {
+ success: true,
+ cartState: updateCartSummary(newCartState, productService),
+ };
+ }
+}
+
+/**
+ * 장바구니에서 상품 수량 변경
+ */
+export function updateCartItemQuantity(cartState, productId, newQuantity, productService) {
+ const item = cartState.items.find((item) => item.id === productId);
+ if (!item) {
+ return {
+ success: false,
+ message: '상품을 찾을 수 없습니다.',
+ cartState,
+ };
+ }
+
+ if (newQuantity <= 0) {
+ // 상품 제거
+ return removeItemFromCart(cartState, productId, productService);
+ }
+
+ const quantityDifference = newQuantity - item.quantity;
+ const product = productService.getProductById(productId);
+
+ if (!product) {
+ return {
+ success: false,
+ message: '상품 정보를 찾을 수 없습니다.',
+ cartState,
+ };
+ }
+
+ // 수량 증가 시 재고 확인
+ if (quantityDifference > 0 && product.quantity < quantityDifference) {
+ return {
+ success: false,
+ message: '재고가 부족합니다.',
+ cartState,
+ };
+ }
+
+ // 수량 변경
+ const updatedItems = cartState.items.map((item) =>
+ item.id === productId ? { ...item, quantity: newQuantity } : item,
+ );
+
+ const newCartState = {
+ ...cartState,
+ items: updatedItems,
+ };
+
+ // 재고 업데이트 (양수면 감소, 음수면 증가)
+ const stockResult = productService.decreaseStock(productId, quantityDifference);
+ if (!stockResult.success) {
+ return {
+ success: false,
+ message: stockResult.message || '재고 업데이트에 실패했습니다.',
+ cartState,
+ };
+ }
+
+ return {
+ success: true,
+ cartState: updateCartSummary(newCartState, productService),
+ };
+}
+
+/**
+ * 장바구니에서 상품 제거
+ */
+export function removeItemFromCart(cartState, productId, productService) {
+ const itemIndex = cartState.items.findIndex((item) => item.id === productId);
+ if (itemIndex === -1) {
+ return {
+ success: false,
+ message: '상품을 찾을 수 없습니다.',
+ cartState,
+ };
+ }
+
+ const item = cartState.items[itemIndex];
+
+ // 재고 복구
+ const stockResult = productService.increaseStock(productId, item.quantity);
+ if (!stockResult.success) {
+ return {
+ success: false,
+ message: stockResult.message || '재고 복구에 실패했습니다.',
+ cartState,
+ };
+ }
+
+ // 장바구니에서 제거
+ const newItems = cartState.items.filter((item) => item.id !== productId);
+ const newCartState = {
+ ...cartState,
+ items: newItems,
+ };
+
+ return {
+ success: true,
+ cartState: updateCartSummary(newCartState, productService),
+ };
+}
+
+/**
+ * 장바구니 비우기
+ */
+export function clearCart(cartState, productService) {
+ // 모든 상품의 재고 복구
+ cartState.items.forEach((item) => {
+ productService.increaseStock(item.id, item.quantity);
+ });
+
+ const newCartState = {
+ ...cartState,
+ items: [],
+ };
+
+ return updateCartSummary(newCartState, productService);
+}
+
+/**
+ * 장바구니 요약 정보 업데이트
+ */
+export function updateCartSummary(cartState, productService) {
+ const itemCount = cartState.items.reduce((total, item) => total + item.quantity, 0);
+
+ // 각 상품별 합계 계산
+ let totalAmount = 0;
+
+ cartState.items.forEach((item) => {
+ const product = productService.getProductById(item.id);
+ if (product) {
+ // 현재 상품의 실제 가격 사용 (할인이 적용된 가격)
+ const itemTotal = product.price * item.quantity;
+ totalAmount += itemTotal;
+ }
+ });
+
+ return {
+ ...cartState,
+ itemCount,
+ totalAmount,
+ };
+}
+
+/**
+ * 장바구니 상태 조회
+ */
+export function getCartState(cartState, productService, discountService, pointService) {
+ const updatedCartState = updateCartSummary(cartState, productService);
+
+ // 포인트 정보 계산 (pointService 주입 필요)
+ const pointInfo = {
+ totalPoints: 0,
+ details: [],
+ displayText: '적립 포인트: 0p',
+ };
+
+ // 할인 정보 계산 (discountService 주입 필요)
+ const discountInfo = [];
+
+ return {
+ items: [...updatedCartState.items],
+ summary: {
+ subtotal: updatedCartState.items.reduce((total, item) => {
+ const product = productService.getProductById(item.id);
+ return total + (product ? product.originalPrice || product.price : 0) * item.quantity;
+ }, 0),
+ totalAmount: updatedCartState.totalAmount,
+ itemCount: updatedCartState.itemCount,
+ itemDiscounts: [],
+ discountRate: 0,
+ isTuesday: false,
+ },
+ pointInfo,
+ discountInfo,
+ lastSelectedProductId: updatedCartState.lastSelectedProductId,
+ };
+}
+
+/**
+ * 장바구니가 비어있는지 확인
+ */
+export function isCartEmpty(cartState) {
+ return cartState.items.length === 0;
+}
+
+/**
+ * 특정 상품이 장바구니에 있는지 확인
+ */
+export function hasCartItem(cartState, productId) {
+ return cartState.items.some((item) => item.id === productId);
+}
+
+/**
+ * 장바구니 아이템 수 조회
+ */
+export function getCartItemCount(cartState) {
+ return cartState.items.reduce((total, item) => total + item.quantity, 0);
+}
+
+/**
+ * 총 금액 조회
+ */
+export function getCartTotalAmount(cartState) {
+ return cartState.totalAmount;
+}
+
+/**
+ * 장바구니 아이템 조회
+ */
+export function getCartItems(cartState) {
+ return [...cartState.items];
+}
+
+/**
+ * 특정 상품의 장바구니 아이템 조회
+ */
+export function getCartItem(cartState, productId) {
+ return cartState.items.find((item) => item.id === productId);
+}
From 3b147efeefe54b480fafd01e84d99c6e4ef69c10 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 23:28:34 +0900
Subject: [PATCH 28/46] =?UTF-8?q?feat:=20render=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/utils/UIRenderer.js | 33 +++++
src/basic/utils/renderers/CartRenderer.js | 47 ++++++++
src/basic/utils/renderers/OrderRenderer.js | 119 +++++++++++++++++++
src/basic/utils/renderers/ProductRenderer.js | 62 ++++++++++
src/basic/utils/renderers/index.js | 12 ++
5 files changed, 273 insertions(+)
create mode 100644 src/basic/utils/UIRenderer.js
create mode 100644 src/basic/utils/renderers/CartRenderer.js
create mode 100644 src/basic/utils/renderers/OrderRenderer.js
create mode 100644 src/basic/utils/renderers/ProductRenderer.js
create mode 100644 src/basic/utils/renderers/index.js
diff --git a/src/basic/utils/UIRenderer.js b/src/basic/utils/UIRenderer.js
new file mode 100644
index 000000000..16d0ea661
--- /dev/null
+++ b/src/basic/utils/UIRenderer.js
@@ -0,0 +1,33 @@
+/**
+ * UI 렌더링 컴포넌트들 (남은 컴포넌트들)
+ * 대부분의 컴포넌트는 src/basic/components/ 폴더로 이동되었습니다.
+ */
+
+// 장바구니 추가 버튼 컴포넌트
+export function createAddToCartButton() {
+ const addToCartButton = document.createElement('button');
+ addToCartButton.id = 'add-to-cart';
+ addToCartButton.innerHTML = 'Add to Cart';
+ addToCartButton.className =
+ 'w-full py-3 bg-black text-white text-sm font-medium uppercase tracking-wider hover:bg-gray-800 transition-all';
+ return addToCartButton;
+}
+
+// 상품 선택 컨테이너 컴포넌트
+export function createSelectorContainer(productSelector, addToCartButton, stockInfoElement) {
+ const selectorContainer = document.createElement('div');
+ selectorContainer.className = 'mb-6 pb-6 border-b border-gray-200';
+ selectorContainer.appendChild(productSelector);
+ selectorContainer.appendChild(addToCartButton);
+ selectorContainer.appendChild(stockInfoElement);
+ return selectorContainer;
+}
+
+// 왼쪽 컬럼 컴포넌트
+export function createLeftColumn(selectorContainer, cartDisplayElement) {
+ const leftColumn = document.createElement('div');
+ leftColumn.className = 'bg-white border border-gray-200 p-8 overflow-y-auto';
+ leftColumn.appendChild(selectorContainer);
+ leftColumn.appendChild(cartDisplayElement);
+ return leftColumn;
+}
diff --git a/src/basic/utils/renderers/CartRenderer.js b/src/basic/utils/renderers/CartRenderer.js
new file mode 100644
index 000000000..e7f9625fd
--- /dev/null
+++ b/src/basic/utils/renderers/CartRenderer.js
@@ -0,0 +1,47 @@
+/**
+ * 장바구니 렌더링 관련 함수들
+ */
+
+/**
+ * 장바구니 내 상품 가격/이름 갱신 및 전체 금액 재계산
+ */
+export function updateCartPrices(cartDisplayElement, productList, calculateCartSummary) {
+ const cartItems = cartDisplayElement.children;
+
+ const productMap = productList.reduce((map, product) => {
+ map[product.id] = product;
+ return map;
+ }, {});
+
+ // 각 카트 아이템을 순회하며 가격/이름 업데이트 (productMap 사용)
+ for (const cartItem of cartItems) {
+ const product = productMap[cartItem.id];
+
+ if (product) {
+ const priceDiv = cartItem.querySelector('.text-lg');
+ const nameDiv = cartItem.querySelector('h3');
+ renderProductPrice(product, priceDiv, nameDiv);
+ }
+ }
+
+ calculateCartSummary();
+}
+
+/**
+ * 상품 가격 렌더링 로직 분리
+ */
+function renderProductPrice(product, priceDiv, nameDiv) {
+ if (product.onSale && product.suggestSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `⚡💝${product.name}`;
+ } else if (product.onSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `⚡${product.name}`;
+ } else if (product.suggestSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `💝${product.name}`;
+ } else {
+ priceDiv.textContent = `₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = product.name;
+ }
+}
diff --git a/src/basic/utils/renderers/OrderRenderer.js b/src/basic/utils/renderers/OrderRenderer.js
new file mode 100644
index 000000000..55cb1bc64
--- /dev/null
+++ b/src/basic/utils/renderers/OrderRenderer.js
@@ -0,0 +1,119 @@
+/**
+ * 주문 렌더링 관련 함수들
+ */
+
+/**
+ * 주문 요약 상세 렌더링 컴포넌트
+ */
+export function renderOrderSummaryDetails(cartItems, productList, subTot, itemDiscounts) {
+ const summaryDetails = document.getElementById('summary-details');
+ if (!summaryDetails) return;
+
+ summaryDetails.innerHTML = '';
+
+ if (subTot > 0) {
+ // 각 상품별 정보 표시
+ for (let i = 0; i < cartItems.length; i++) {
+ let curItem;
+ for (let j = 0; j < productList.length; j++) {
+ if (productList[j].id === cartItems[i].id) {
+ curItem = productList[j];
+ break;
+ }
+ }
+
+ const quantityElem = cartItems[i].querySelector('.quantity-number');
+ const quantity = parseInt(quantityElem.textContent);
+ const itemTotal = curItem.price * quantity;
+
+ summaryDetails.innerHTML += `
+
+ ${curItem.name} x ${quantity}
+ ₩${itemTotal.toLocaleString()}
+
+ `;
+ }
+
+ // 소계 표시
+ summaryDetails.innerHTML += `
+
+
+ Subtotal
+ ₩${subTot.toLocaleString()}
+
+ `;
+
+ // 할인 정보 표시
+ itemDiscounts.forEach(function (discount) {
+ const colorClass = discount.type === 'tuesday' ? 'text-purple-400' : 'text-green-400';
+ const icon = discount.type === 'tuesday' ? '🌟' : discount.type === 'bulk' ? '🎉' : '';
+ summaryDetails.innerHTML += `
+
+ ${icon} ${discount.name}
+ -${discount.rate}%
+
+ `;
+ });
+
+ // 배송비 표시
+ summaryDetails.innerHTML += `
+
+ Shipping
+ Free
+
+ `;
+ }
+}
+
+/**
+ * 화요일 특별 할인 UI 렌더링 컴포넌트
+ */
+export function renderTuesdaySpecial(isTuesday, totalAmount) {
+ const tuesdaySpecial = document.getElementById('tuesday-special');
+ if (!tuesdaySpecial) return;
+
+ if (isTuesday && totalAmount > 0) {
+ tuesdaySpecial.classList.remove('hidden');
+ } else {
+ tuesdaySpecial.classList.add('hidden');
+ }
+}
+
+/**
+ * 총 결제 금액 렌더링 컴포넌트
+ */
+export function renderTotalAmount(totalAmount, orderSummaryElement) {
+ const totalDiv = orderSummaryElement.querySelector('.text-2xl');
+ if (totalDiv) {
+ totalDiv.textContent = `₩${Math.round(totalAmount).toLocaleString()}`;
+ }
+}
+
+/**
+ * 장바구니 수량 렌더링 컴포넌트
+ */
+export function renderItemCount(itemCount) {
+ const itemCountElement = document.getElementById('item-count');
+ if (itemCountElement) {
+ itemCountElement.textContent = `🛍️ ${itemCount} items in cart`;
+ }
+}
+
+/**
+ * 재고 메시지 렌더링 컴포넌트
+ */
+export function renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement) {
+ let stockMsg = '';
+
+ // 재고 부족 상품 메시지
+ lowStockProducts.forEach((item) => {
+ stockMsg += `${item.name}: 재고 부족 (${item.quantity}개 남음)\n`;
+ });
+
+ // 품절 상품 메시지
+ outOfStockProducts.forEach((item) => {
+ stockMsg += `${item.name}: 품절\n`;
+ });
+
+ stockInfoElement.textContent = stockMsg;
+}
diff --git a/src/basic/utils/renderers/ProductRenderer.js b/src/basic/utils/renderers/ProductRenderer.js
new file mode 100644
index 000000000..1e9014ec4
--- /dev/null
+++ b/src/basic/utils/renderers/ProductRenderer.js
@@ -0,0 +1,62 @@
+/**
+ * 상품 렌더링 관련 함수들
+ */
+import { UI_CONSTANTS } from '../../constants/UIConstants.js';
+
+/**
+ * 상품 옵션 렌더링 컴포넌트
+ * 상품 선택 드롭다운의 옵션들을 렌더링합니다.
+ */
+export function renderProductOptions(productSelector, productList) {
+ productSelector.innerHTML = '';
+
+ for (let i = 0; i < productList.length; i++) {
+ const item = productList[i];
+ const opt = document.createElement('option');
+ opt.value = item.id;
+ let discountText = '';
+
+ if (item.onSale) discountText += ' ⚡SALE';
+ if (item.suggestSale) discountText += ' 💝추천';
+
+ if (item.quantity === 0) {
+ opt.textContent = `${item.name} - ${item.price}원 (품절)${discountText}`;
+ opt.disabled = true;
+ opt.className = 'text-gray-400';
+ } else {
+ if (item.onSale && item.suggestSale) {
+ opt.textContent = `⚡💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (25% SUPER SALE!)`;
+ opt.className = 'text-purple-600 font-bold';
+ } else if (item.onSale) {
+ opt.textContent = `⚡${item.name} - ${item.originalPrice}원 → ${item.price}원 (20% SALE!)`;
+ opt.className = 'text-red-500 font-bold';
+ } else if (item.suggestSale) {
+ opt.textContent = `💝${item.name} - ${item.originalPrice}원 → ${item.price}원 (5% 추천할인!)`;
+ opt.className = 'text-blue-500 font-bold';
+ } else {
+ opt.textContent = `${item.name} - ${item.price}원${discountText}`;
+ }
+ }
+ productSelector.appendChild(opt);
+ }
+}
+
+/**
+ * 상품 가격 렌더링 로직 분리
+ * 상품의 가격과 이름을 렌더링합니다.
+ */
+export function renderProductPrice(product, priceDiv, nameDiv) {
+ if (product.onSale && product.suggestSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `⚡💝${product.name}`;
+ } else if (product.onSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `⚡${product.name}`;
+ } else if (product.suggestSale) {
+ priceDiv.innerHTML = `₩${product.originalPrice.toLocaleString()} ₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = `💝${product.name}`;
+ } else {
+ priceDiv.textContent = `₩${product.price.toLocaleString()}`;
+ nameDiv.textContent = product.name;
+ }
+}
diff --git a/src/basic/utils/renderers/index.js b/src/basic/utils/renderers/index.js
new file mode 100644
index 000000000..49f4e60f2
--- /dev/null
+++ b/src/basic/utils/renderers/index.js
@@ -0,0 +1,12 @@
+/**
+ * Renderers 컴포넌트들의 barrel export
+ */
+export { renderProductOptions, renderProductPrice } from './ProductRenderer.js';
+export { updateCartPrices } from './CartRenderer.js';
+export {
+ renderOrderSummaryDetails,
+ renderTuesdaySpecial,
+ renderTotalAmount,
+ renderItemCount,
+ renderStockMessages,
+} from './OrderRenderer.js';
From 9bae4a0b3530b1b07366fa9f2c4be9ec755f91bb Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 23:37:23 +0900
Subject: [PATCH 29/46] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?=
=?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 168 ++++++++++--------------------
src/basic/store/cartStore.js | 4 +
src/basic/utils/EventHandler.js | 175 ++++++++++++++++++++++++++++++++
src/basic/utils/TimerHandler.js | 105 +++++++++++++++++++
4 files changed, 336 insertions(+), 116 deletions(-)
create mode 100644 src/basic/utils/EventHandler.js
create mode 100644 src/basic/utils/TimerHandler.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 940af307a..191969f41 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -7,8 +7,6 @@ import {
getProductById,
decreaseStock,
increaseStock,
- applySale,
- applySuggestSale,
getLowStockProducts,
getOutOfStockProducts,
getTotalStock,
@@ -54,6 +52,12 @@ import {
createLeftColumn,
} from './utils/UIRenderer.js';
+// EventHandler import
+import { setupEventListeners } from './utils/EventHandler.js';
+
+// TimerHandler import
+import { setupAllTimers } from './utils/TimerHandler.js';
+
// Renderers import
import {
renderProductOptions,
@@ -144,53 +148,14 @@ function main() {
updateProductOptions();
calculateCartSummary();
- // 번개 세일(랜덤 상품 20% 할인) 타이머 설정
- const lightningDelay = Math.random() * UI_CONSTANTS.LIGHTNING_SALE_DELAY;
- setTimeout(() => {
- setInterval(function () {
- const luckyIdx = Math.floor(Math.random() * cartStore.products.length);
- const luckyItem = cartStore.products[luckyIdx];
- if (luckyItem.quantity > 0 && !luckyItem.onSale) {
- // ProductService의 applySale 함수 사용
- const result = applySale(cartStore.products, luckyItem.id, 0.2);
- if (result.success) {
- cartStore.products = result.products;
- alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
- updateProductOptions();
- updateCartPrices();
- }
- }
- }, UI_CONSTANTS.LIGHTNING_SALE_INTERVAL);
- }, lightningDelay);
-
- // 추천 할인(다른 상품 5% 할인) 타이머 설정
- setTimeout(function () {
- setInterval(function () {
- if (cartState.lastSelectedProductId && cartDisplayElement.children.length > 0) {
- let suggest = null;
- for (let k = 0; k < cartStore.products.length; k++) {
- if (cartStore.products[k].id !== cartState.lastSelectedProductId) {
- if (cartStore.products[k].quantity > 0) {
- if (!cartStore.products[k].suggestSale) {
- suggest = cartStore.products[k];
- break;
- }
- }
- }
- }
-
- if (suggest) {
- alert(`💝 ${suggest.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
- const result = applySuggestSale(cartStore.products, suggest.id, 0.05);
- if (result.success) {
- cartStore.products = result.products;
- updateProductOptions();
- updateCartPrices();
- }
- }
- }
- }, UI_CONSTANTS.SUGGEST_SALE_INTERVAL);
- }, Math.random() * UI_CONSTANTS.SUGGEST_SALE_DELAY);
+ // 타이머 설정
+ setupAllTimers({
+ products: cartStore.products,
+ cartDisplayElement,
+ lastSelectedProductId: cartState.lastSelectedProductId,
+ updateProductOptions,
+ updateCartPrices,
+ });
}
// 장바구니 내 각 상품별 합계/할인 계산
@@ -490,73 +455,44 @@ function createCartItemElement(product, quantity) {
main();
-// 장바구니 추가 버튼 클릭 이벤트 - CartService 사용
-addToCartButton.addEventListener('click', function () {
- const selItem = productSelector.value;
-
- if (!selItem) {
- return;
+// 이벤트 핸들러 래퍼 함수들
+function updateCartItemQuantityHandler(productId, newQuantity) {
+ const { success, cartState: newCartState } = updateCartItemQuantity(
+ cartState,
+ productId,
+ newQuantity,
+ productService,
+ );
+ if (success) {
+ cartState = newCartState;
}
+ return { success, cartState: newCartState };
+}
- // CartService를 사용하여 장바구니에 추가
- addItemToCartUI(selItem, 1);
-});
-
-// 장바구니 내 수량 변경/삭제 이벤트 처리
-cartDisplayElement.addEventListener('click', function (event) {
- const tgt = event.target;
-
- if (tgt.classList.contains('quantity-change') || tgt.classList.contains('remove-item')) {
- const prodId = tgt.dataset.productId;
- const itemElem = document.getElementById(prodId);
-
- // ProductService의 getProductById 함수 사용
- const prod = getProductById(cartStore.products, prodId);
-
- if (tgt.classList.contains('quantity-change')) {
- const qtyChange = parseInt(tgt.dataset.change);
- const qtyElem = itemElem.querySelector('.quantity-number');
- const currentQty = parseInt(qtyElem.textContent);
- const newQty = currentQty + qtyChange;
-
- if (newQty > 0) {
- // CartService를 사용하여 수량 변경
- const result = updateCartItemQuantity(cartState, prodId, newQty, productService);
- if (result.success) {
- cartState = result.cartState;
- qtyElem.textContent = newQty;
- } else {
- alert(result.message || '재고가 부족합니다.');
- }
- } else {
- // CartService를 사용하여 상품 제거
- const result = removeItemFromCart(cartState, prodId, productService);
- if (result.success) {
- cartState = result.cartState;
- itemElem.remove();
- } else {
- alert(result.message || '상품 제거에 실패했습니다.');
- }
- }
- } else if (tgt.classList.contains('remove-item')) {
- const qtyElem = itemElem.querySelector('.quantity-number');
- const remQty = parseInt(qtyElem.textContent);
-
- // CartService를 사용하여 상품 제거
- const result = removeItemFromCart(cartState, prodId, productService);
- if (result.success) {
- cartState = result.cartState;
- itemElem.remove();
- } else {
- alert(result.message || '상품 제거에 실패했습니다.');
- }
- }
-
- if (prod && prod.quantity < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
- // 재고 부족 알림 (필요시 추가 구현)
- }
-
- calculateCartSummary();
- updateProductOptions();
+function removeItemFromCartHandler(productId) {
+ const { success, cartState: newCartState } = removeItemFromCart(
+ cartState,
+ productId,
+ productService,
+ );
+ if (success) {
+ cartState = newCartState;
}
-});
+ return { success, cartState: newCartState };
+}
+
+// 이벤트 리스너 설정
+setupEventListeners(
+ {
+ addToCartButton,
+ productSelector,
+ cartDisplayElement,
+ },
+ {
+ addItemToCartUI,
+ updateCartItemQuantity: updateCartItemQuantityHandler,
+ removeItemFromCart: removeItemFromCartHandler,
+ calculateCartSummary,
+ updateProductOptions,
+ },
+);
diff --git a/src/basic/store/cartStore.js b/src/basic/store/cartStore.js
index f27b66be5..9686fa768 100644
--- a/src/basic/store/cartStore.js
+++ b/src/basic/store/cartStore.js
@@ -7,6 +7,10 @@ export const cartStore = {
};
export const cartStoreActions = {
+ getProducts() {
+ return cartStore.products;
+ },
+
setProducts(products) {
cartStore.products = products.map((p) => ({
...p,
diff --git a/src/basic/utils/EventHandler.js b/src/basic/utils/EventHandler.js
new file mode 100644
index 000000000..13a89f377
--- /dev/null
+++ b/src/basic/utils/EventHandler.js
@@ -0,0 +1,175 @@
+// EventHandler.js - 이벤트 핸들러 관리 유틸리티
+
+import { cartStoreActions } from '../store/cartStore.js';
+import { getProductById } from '../services/product/ProductService.js';
+import { UI_CONSTANTS } from '../constants/index.js';
+
+/**
+ * 장바구니 추가 버튼 클릭 이벤트 핸들러
+ * @param {HTMLSelectElement} productSelector - 상품 선택 요소
+ * @param {Function} addItemToCartUI - 장바구니 추가 UI 함수
+ */
+export function handleAddToCartClick(productSelector, addItemToCartUI) {
+ const selectedProductId = productSelector.value;
+
+ if (!selectedProductId) {
+ return;
+ }
+
+ // 장바구니에 상품 추가
+ addItemToCartUI(selectedProductId, 1);
+}
+
+/**
+ * 장바구니 내 수량 변경 이벤트 핸들러
+ * @param {Event} event - 클릭 이벤트
+ * @param {Object} handlers - 핸들러 함수들
+ */
+export function handleQuantityChange(event, handlers) {
+ const { updateCartItemQuantity, removeItemFromCart, calculateCartSummary, updateProductOptions } =
+ handlers;
+
+ const { target } = event;
+ const { productId } = target.dataset;
+ const quantityChange = parseInt(target.dataset.change);
+
+ const itemElement = document.getElementById(productId);
+ const quantityElement = itemElement.querySelector('.quantity-number');
+ const currentQuantity = parseInt(quantityElement.textContent);
+ const newQuantity = currentQuantity + quantityChange;
+
+ if (newQuantity > 0) {
+ // 수량 변경
+ const result = updateCartItemQuantity(productId, newQuantity);
+ if (result.success) {
+ quantityElement.textContent = newQuantity;
+ } else {
+ alert(result.message || '재고가 부족합니다.');
+ }
+ } else {
+ // 상품 제거
+ const result = removeItemFromCart(productId);
+ if (result.success) {
+ itemElement.remove();
+ } else {
+ alert(result.message || '상품 제거에 실패했습니다.');
+ }
+ }
+
+ // 재고 상태 확인 및 알림
+ checkStockStatus(productId);
+
+ // UI 업데이트
+ calculateCartSummary();
+ updateProductOptions();
+}
+
+/**
+ * 장바구니 내 상품 제거 이벤트 핸들러
+ * @param {Event} event - 클릭 이벤트
+ * @param {Object} handlers - 핸들러 함수들
+ */
+export function handleRemoveItem(event, handlers) {
+ const { removeItemFromCart, calculateCartSummary, updateProductOptions } = handlers;
+
+ const { target } = event;
+ const { productId } = target.dataset;
+
+ const itemElement = document.getElementById(productId);
+
+ // 상품 제거
+ const result = removeItemFromCart(productId);
+ if (result.success) {
+ itemElement.remove();
+ } else {
+ alert(result.message || '상품 제거에 실패했습니다.');
+ }
+
+ // 재고 상태 확인 및 알림
+ checkStockStatus(productId);
+
+ // UI 업데이트
+ calculateCartSummary();
+ updateProductOptions();
+}
+
+/**
+ * 재고 상태 확인 및 알림
+ * @param {string} productId - 상품 ID
+ */
+function checkStockStatus(productId) {
+ const product = getProductById(cartStoreActions.getProducts(), productId);
+
+ if (product && product.quantity < UI_CONSTANTS.LOW_STOCK_THRESHOLD) {
+ // 재고 부족 알림 (필요시 추가 구현)
+ console.log(`${product.name}의 재고가 부족합니다.`);
+ }
+}
+
+/**
+ * 장바구니 이벤트 위임 핸들러
+ * @param {Event} event - 클릭 이벤트
+ * @param {Object} handlers - 핸들러 함수들
+ */
+export function handleCartItemClick(event, handlers) {
+ const { target } = event;
+
+ if (target.classList.contains('quantity-change')) {
+ handleQuantityChange(event, handlers);
+ } else if (target.classList.contains('remove-item')) {
+ handleRemoveItem(event, handlers);
+ }
+}
+
+/**
+ * 상품 선택 변경 이벤트 핸들러
+ * @param {Event} event - change 이벤트
+ * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수
+ */
+export function handleProductSelectionChange(event, updateProductOptions) {
+ const selectedProductId = event.target.value;
+
+ if (selectedProductId) {
+ // 마지막 선택된 상품 ID 저장
+ cartStoreActions.setLastSelectedProductId(selectedProductId);
+ }
+
+ // 상품 옵션 업데이트
+ updateProductOptions();
+}
+
+/**
+ * 이벤트 리스너 등록 함수
+ * @param {Object} elements - DOM 요소들
+ * @param {Object} handlers - 핸들러 함수들
+ */
+export function setupEventListeners(elements, handlers) {
+ const { addToCartButton, productSelector, cartDisplayElement } = elements;
+ const {
+ addItemToCartUI,
+ updateCartItemQuantity,
+ removeItemFromCart,
+ calculateCartSummary,
+ updateProductOptions,
+ } = handlers;
+
+ // 장바구니 추가 버튼 클릭 이벤트
+ addToCartButton.addEventListener('click', () => {
+ handleAddToCartClick(productSelector, addItemToCartUI);
+ });
+
+ // 상품 선택 변경 이벤트
+ productSelector.addEventListener('change', (event) => {
+ handleProductSelectionChange(event, updateProductOptions);
+ });
+
+ // 장바구니 아이템 클릭 이벤트 (위임)
+ cartDisplayElement.addEventListener('click', (event) => {
+ handleCartItemClick(event, {
+ updateCartItemQuantity,
+ removeItemFromCart,
+ calculateCartSummary,
+ updateProductOptions,
+ });
+ });
+}
diff --git a/src/basic/utils/TimerHandler.js b/src/basic/utils/TimerHandler.js
new file mode 100644
index 000000000..c2fe0dc70
--- /dev/null
+++ b/src/basic/utils/TimerHandler.js
@@ -0,0 +1,105 @@
+// TimerHandler.js - 타이머 이벤트 핸들러 관리 유틸리티
+
+import { UI_CONSTANTS } from '../constants/index.js';
+import { applySale, applySuggestSale } from '../services/product/ProductService.js';
+
+/**
+ * 번개 세일 타이머 설정
+ * @param {Array} products - 상품 목록
+ * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수
+ * @param {Function} updateCartPrices - 장바구니 가격 업데이트 함수
+ */
+export function setupLightningSaleTimer(products, updateProductOptions, updateCartPrices) {
+ const lightningDelay = Math.random() * UI_CONSTANTS.LIGHTNING_SALE_DELAY;
+
+ setTimeout(() => {
+ setInterval(() => {
+ const luckyIndex = Math.floor(Math.random() * products.length);
+ const luckyItem = products[luckyIndex];
+
+ if (luckyItem.quantity > 0 && !luckyItem.onSale) {
+ const result = applySale(products, luckyItem.id, 0.2);
+ if (result.success) {
+ // products 배열 업데이트
+ Object.assign(products, result.products);
+ alert(`⚡번개세일! ${luckyItem.name}이(가) 20% 할인 중입니다!`);
+ updateProductOptions();
+ updateCartPrices();
+ }
+ }
+ }, UI_CONSTANTS.LIGHTNING_SALE_INTERVAL);
+ }, lightningDelay);
+}
+
+/**
+ * 추천 할인 타이머 설정
+ * @param {Array} products - 상품 목록
+ * @param {HTMLElement} cartDisplayElement - 장바구니 표시 요소
+ * @param {string} lastSelectedProductId - 마지막 선택된 상품 ID
+ * @param {Function} updateProductOptions - 상품 옵션 업데이트 함수
+ * @param {Function} updateCartPrices - 장바구니 가격 업데이트 함수
+ */
+export function setupSuggestSaleTimer(
+ products,
+ cartDisplayElement,
+ lastSelectedProductId,
+ updateProductOptions,
+ updateCartPrices,
+) {
+ setTimeout(() => {
+ setInterval(() => {
+ if (lastSelectedProductId && cartDisplayElement.children.length > 0) {
+ let suggestProduct = null;
+
+ for (let i = 0; i < products.length; i++) {
+ const product = products[i];
+ if (
+ product.id !== lastSelectedProductId &&
+ product.quantity > 0 &&
+ !product.suggestSale
+ ) {
+ suggestProduct = product;
+ break;
+ }
+ }
+
+ if (suggestProduct) {
+ alert(`💝 ${suggestProduct.name}은(는) 어떠세요? 지금 구매하시면 5% 추가 할인!`);
+ const result = applySuggestSale(products, suggestProduct.id, 0.05);
+ if (result.success) {
+ // products 배열 업데이트
+ Object.assign(products, result.products);
+ updateProductOptions();
+ updateCartPrices();
+ }
+ }
+ }
+ }, UI_CONSTANTS.SUGGEST_SALE_INTERVAL);
+ }, Math.random() * UI_CONSTANTS.SUGGEST_SALE_DELAY);
+}
+
+/**
+ * 모든 타이머 설정
+ * @param {Object} config - 타이머 설정 객체
+ */
+export function setupAllTimers(config) {
+ const {
+ products,
+ cartDisplayElement,
+ lastSelectedProductId,
+ updateProductOptions,
+ updateCartPrices,
+ } = config;
+
+ // 번개 세일 타이머 설정
+ setupLightningSaleTimer(products, updateProductOptions, updateCartPrices);
+
+ // 추천 할인 타이머 설정
+ setupSuggestSaleTimer(
+ products,
+ cartDisplayElement,
+ lastSelectedProductId,
+ updateProductOptions,
+ updateCartPrices,
+ );
+}
From d31e5b92f6c476c04a18a159ff5e01cfd98fceeb Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Thu, 31 Jul 2025 23:47:15 +0900
Subject: [PATCH 30/46] =?UTF-8?q?feat:=20Product=20store=EC=99=80=20action?=
=?UTF-8?q?=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 60 +++-----
src/basic/store/productStore.js | 252 ++++++++++++++++++++++++++++++++
2 files changed, 274 insertions(+), 38 deletions(-)
create mode 100644 src/basic/store/productStore.js
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 191969f41..204beb46a 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -1,17 +1,8 @@
// 상수 import
import { DISCOUNT_THRESHOLDS, UI_CONSTANTS } from './constants/index.js';
-// ProductService import
-import {
- initializeProducts,
- getProductById,
- decreaseStock,
- increaseStock,
- getLowStockProducts,
- getOutOfStockProducts,
- getTotalStock,
- calculateItemDiscount,
-} from './services/product/ProductService.js';
+// productStore import
+import { productStore, productStoreActions } from './store/productStore.js';
// DiscountService import
import { calculateTotalDiscountRate } from './services/discount/DiscountService.js';
@@ -79,22 +70,16 @@ let orderSummaryElement;
// CartService를 위한 상태 관리
let cartState = createInitialCartState();
-// ProductService 래퍼 (CartService에서 사용하기 위한 인터페이스)
+// productStore를 CartService에서 사용하기 위한 인터페이스
const productService = {
- getProductById: (productId) => getProductById(cartStore.products, productId),
+ getProductById: (productId) => productStoreActions.getProductById(productId),
decreaseStock: (productId, quantity) => {
- const result = decreaseStock(cartStore.products, productId, quantity);
- if (result.success) {
- cartStore.products = result.products;
- }
- return result;
+ const success = productStoreActions.decreaseStock(productId, quantity);
+ return { success, products: productStore.products };
},
increaseStock: (productId, quantity) => {
- const result = increaseStock(cartStore.products, productId, quantity);
- if (result.success) {
- cartStore.products = result.products;
- }
- return result;
+ const success = productStoreActions.increaseStock(productId, quantity);
+ return { success, products: productStore.products };
},
};
@@ -102,9 +87,8 @@ function main() {
// cartStore 초기화
cartStoreActions.reset();
- // 상품 정보 초기화 - ProductService 사용
- const initialProducts = initializeProducts();
- cartStoreActions.setProducts(initialProducts);
+ // productStore 초기화
+ productStoreActions.initializeProducts();
// CartService 상태 초기화
cartState = createInitialCartState();
@@ -150,7 +134,7 @@ function main() {
// 타이머 설정
setupAllTimers({
- products: cartStore.products,
+ products: productStore.products,
cartDisplayElement,
lastSelectedProductId: cartState.lastSelectedProductId,
updateProductOptions,
@@ -168,9 +152,9 @@ function processCartItems(cartItems) {
for (let i = 0; i < cartItems.length; i++) {
// 상품 찾기
let curItem;
- for (let j = 0; j < cartStore.products.length; j++) {
- if (cartStore.products[j].id === cartItems[i].id) {
- curItem = cartStore.products[j];
+ for (let j = 0; j < productStore.products.length; j++) {
+ if (productStore.products[j].id === cartItems[i].id) {
+ curItem = productStore.products[j];
break;
}
}
@@ -191,8 +175,8 @@ function processCartItems(cartItems) {
}
});
- // 개별 할인 계산 - ProductService 사용
- const disc = calculateItemDiscount(curItem.id, quantity);
+ // 개별 할인 계산 - productStore 사용
+ const disc = productStoreActions.calculateItemDiscount(curItem.id, quantity);
if (disc > 0) {
itemDiscounts.push({ name: curItem.name, discount: disc * 100 });
}
@@ -215,15 +199,15 @@ function calculateTotalDiscount(subTot, itemCount, currentAmount) {
// 주문 요약 상세 내역 갱신
function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) {
- renderOrderSummaryDetails(cartItems, cartStore.products, subTot, itemDiscounts);
+ renderOrderSummaryDetails(cartItems, productStore.products, subTot, itemDiscounts);
}
// 상품 선택 옵션 렌더링 및 재고 상태 표시
function updateProductOptions() {
- renderProductOptions(productSelector, cartStore.products);
+ renderProductOptions(productSelector, productStore.products);
- // ProductService의 getTotalStock 함수 사용
- const totalStock = getTotalStock(cartStore.products);
+ // productStore의 getTotalStock 함수 사용
+ const totalStock = productStoreActions.getTotalStock();
if (totalStock < UI_CONSTANTS.TOTAL_STOCK_THRESHOLD) {
productSelector.style.borderColor = 'orange';
@@ -361,9 +345,9 @@ const renderBonusPoints = function () {
// 재고 부족/품절 안내 메시지 생성 및 표시
function updateStockMessages() {
// 재고 부족 상품 조회
- const lowStockProducts = getLowStockProducts(cartStore.products);
+ const lowStockProducts = productStoreActions.getLowStockProducts();
// 품절 상품 조회
- const outOfStockProducts = getOutOfStockProducts(cartStore.products);
+ const outOfStockProducts = productStoreActions.getOutOfStockProducts();
renderStockMessages(lowStockProducts, outOfStockProducts, stockInfoElement);
}
diff --git a/src/basic/store/productStore.js b/src/basic/store/productStore.js
new file mode 100644
index 000000000..c350a276d
--- /dev/null
+++ b/src/basic/store/productStore.js
@@ -0,0 +1,252 @@
+import {
+ PRODUCT_IDS,
+ PRODUCT_NAMES,
+ PRODUCT_PRICES,
+ INITIAL_STOCK,
+ DISCOUNT_THRESHOLDS,
+ DISCOUNT_RATES,
+} from '../constants/index.js';
+
+export const productStore = {
+ products: [],
+ selectedProductId: null,
+ lowStockProducts: [],
+ outOfStockProducts: [],
+ totalStock: 0,
+};
+
+export const productStoreActions = {
+ getProducts() {
+ return productStore.products;
+ },
+
+ getProductById(productId) {
+ return productStore.products.find((product) => product.id === productId);
+ },
+
+ setProducts(products) {
+ productStore.products = products.map((product) => ({
+ ...product,
+ originalQuantity: product.quantity,
+ originalPrice: product.price,
+ }));
+ this.updateDerivedState();
+ },
+
+ initializeProducts() {
+ const products = [
+ {
+ id: PRODUCT_IDS.KEYBOARD,
+ name: PRODUCT_NAMES[PRODUCT_IDS.KEYBOARD],
+ price: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.KEYBOARD],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.KEYBOARD],
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MOUSE,
+ name: PRODUCT_NAMES[PRODUCT_IDS.MOUSE],
+ price: PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MOUSE],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.MOUSE],
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.MONITOR_ARM,
+ name: PRODUCT_NAMES[PRODUCT_IDS.MONITOR_ARM],
+ price: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.MONITOR_ARM],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.MONITOR_ARM],
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.LAPTOP_CASE,
+ name: PRODUCT_NAMES[PRODUCT_IDS.LAPTOP_CASE],
+ price: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.LAPTOP_CASE],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.LAPTOP_CASE],
+ onSale: false,
+ suggestSale: false,
+ },
+ {
+ id: PRODUCT_IDS.SPEAKER,
+ name: PRODUCT_NAMES[PRODUCT_IDS.SPEAKER],
+ price: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
+ originalPrice: PRODUCT_PRICES[PRODUCT_IDS.SPEAKER],
+ quantity: INITIAL_STOCK[PRODUCT_IDS.SPEAKER],
+ onSale: false,
+ suggestSale: false,
+ },
+ ];
+
+ this.setProducts(products);
+ },
+
+ decreaseStock(productId, quantity = 1) {
+ const product = this.getProductById(productId);
+ if (!product || product.quantity < quantity) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId ? { ...p, quantity: p.quantity - quantity } : p,
+ );
+
+ this.updateDerivedState();
+ return true;
+ },
+
+ increaseStock(productId, quantity = 1) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId ? { ...p, quantity: p.quantity + quantity } : p,
+ );
+
+ this.updateDerivedState();
+ return true;
+ },
+
+ applySale(productId, discountRate) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId
+ ? {
+ ...p,
+ price: Math.round(p.originalPrice * (1 - discountRate)),
+ onSale: true,
+ }
+ : p,
+ );
+
+ return true;
+ },
+
+ applySuggestSale(productId, discountRate) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId
+ ? {
+ ...p,
+ price: Math.round(p.price * (1 - discountRate)),
+ suggestSale: true,
+ }
+ : p,
+ );
+
+ return true;
+ },
+
+ resetSale(productId) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId
+ ? {
+ ...p,
+ price: p.originalPrice,
+ onSale: false,
+ suggestSale: false,
+ }
+ : p,
+ );
+
+ return true;
+ },
+
+ updateProductPrice(productId, newPrice) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId ? { ...p, price: newPrice } : p,
+ );
+
+ return true;
+ },
+
+ updateProductState(productId, updates) {
+ const product = this.getProductById(productId);
+ if (!product) return false;
+
+ productStore.products = productStore.products.map((p) =>
+ p.id === productId ? { ...p, ...updates } : p,
+ );
+
+ this.updateDerivedState();
+ return true;
+ },
+
+ setSelectedProductId(productId) {
+ productStore.selectedProductId = productId;
+ },
+
+ getSelectedProduct() {
+ return this.getProductById(productStore.selectedProductId);
+ },
+
+ getLowStockProducts() {
+ return productStore.lowStockProducts;
+ },
+
+ getOutOfStockProducts() {
+ return productStore.outOfStockProducts;
+ },
+
+ getTotalStock() {
+ return productStore.totalStock;
+ },
+
+ calculateItemDiscount(productId, quantity) {
+ if (quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM) {
+ return 0;
+ }
+
+ const discountMap = {
+ [PRODUCT_IDS.KEYBOARD]: DISCOUNT_RATES.KEYBOARD,
+ [PRODUCT_IDS.MOUSE]: DISCOUNT_RATES.MOUSE,
+ [PRODUCT_IDS.MONITOR_ARM]: DISCOUNT_RATES.MONITOR_ARM,
+ [PRODUCT_IDS.LAPTOP_CASE]: DISCOUNT_RATES.LAPTOP_CASE,
+ [PRODUCT_IDS.SPEAKER]: DISCOUNT_RATES.SPEAKER,
+ };
+
+ return discountMap[productId] || 0;
+ },
+
+ updateDerivedState() {
+ // Update low stock products
+ productStore.lowStockProducts = productStore.products.filter(
+ (product) => product.quantity < DISCOUNT_THRESHOLDS.INDIVIDUAL_ITEM && product.quantity > 0,
+ );
+
+ // Update out of stock products
+ productStore.outOfStockProducts = productStore.products.filter(
+ (product) => product.quantity === 0,
+ );
+
+ // Update total stock
+ productStore.totalStock = productStore.products.reduce(
+ (total, product) => total + product.quantity,
+ 0,
+ );
+ },
+
+ reset() {
+ productStore.products = productStore.products.map((p) => ({
+ ...p,
+ quantity: p.originalQuantity,
+ price: p.originalPrice,
+ onSale: false,
+ suggestSale: false,
+ }));
+ productStore.selectedProductId = null;
+ this.updateDerivedState();
+ },
+};
From d4870ad037f1f71b331049ab108af435ade23885 Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Fri, 1 Aug 2025 00:42:05 +0900
Subject: [PATCH 31/46] =?UTF-8?q?fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?=
=?UTF-8?q?=EB=8B=88=20=ED=95=A0=EC=9D=B8=20=EC=A0=95=EB=B3=B4=20undefined?=
=?UTF-8?q?=20=EB=9C=A8=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/basic/main.basic.js | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/basic/main.basic.js b/src/basic/main.basic.js
index 204beb46a..7a341a357 100644
--- a/src/basic/main.basic.js
+++ b/src/basic/main.basic.js
@@ -5,7 +5,10 @@ import { DISCOUNT_THRESHOLDS, UI_CONSTANTS } from './constants/index.js';
import { productStore, productStoreActions } from './store/productStore.js';
// DiscountService import
-import { calculateTotalDiscountRate } from './services/discount/DiscountService.js';
+import {
+ calculateTotalDiscountRate,
+ createDiscountInfo,
+} from './services/discount/DiscountService.js';
// PointService import
import { createPointInfo } from './services/point/PointService.js';
@@ -199,7 +202,9 @@ function calculateTotalDiscount(subTot, itemCount, currentAmount) {
// 주문 요약 상세 내역 갱신
function updateOrderSummary(cartItems, subTot, itemCount, itemDiscounts) {
- renderOrderSummaryDetails(cartItems, productStore.products, subTot, itemDiscounts);
+ // createDiscountInfo를 사용하여 올바른 할인 정보 생성
+ const discountInfo = createDiscountInfo(itemDiscounts, itemCount);
+ renderOrderSummaryDetails(cartItems, productStore.products, subTot, discountInfo);
}
// 상품 선택 옵션 렌더링 및 재고 상태 표시
From 923977248dda18642fcd75dd8b64fa498b0333de Mon Sep 17 00:00:00 2001
From: Amelia-Shin
Date: Fri, 1 Aug 2025 02:04:00 +0900
Subject: [PATCH 32/46] =?UTF-8?q?chore:=20advanced=20=ED=95=98=EA=B8=B0=20?=
=?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=A0=81?=
=?UTF-8?q?=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
eslint.config.js | 81 +-
index.advanced.html | 66 +-
package.json | 22 +-
pnpm-lock.yaml | 871 ++++++++++++++++++
src/advanced/App.tsx | 20 +
.../components/cart/ProductPicker.tsx | 26 +
src/advanced/components/cart/ShoppingCart.tsx | 52 ++
src/advanced/components/guide/GuideToggle.tsx | 32 +
.../components/guide/ShoppingGuide.tsx | 32 +
src/advanced/components/layout/Header.tsx | 15 +
src/advanced/components/layout/Layout.tsx | 9 +
.../components/order/OrderSummary.tsx | 74 ++
src/advanced/lib/product.ts | 52 ++
src/advanced/main.tsx | 12 +
tsconfig.json | 44 +
vite.config.js | 15 +-
16 files changed, 1363 insertions(+), 60 deletions(-)
create mode 100644 src/advanced/App.tsx
create mode 100644 src/advanced/components/cart/ProductPicker.tsx
create mode 100644 src/advanced/components/cart/ShoppingCart.tsx
create mode 100644 src/advanced/components/guide/GuideToggle.tsx
create mode 100644 src/advanced/components/guide/ShoppingGuide.tsx
create mode 100644 src/advanced/components/layout/Header.tsx
create mode 100644 src/advanced/components/layout/Layout.tsx
create mode 100644 src/advanced/components/order/OrderSummary.tsx
create mode 100644 src/advanced/lib/product.ts
create mode 100644 src/advanced/main.tsx
create mode 100644 tsconfig.json
diff --git a/eslint.config.js b/eslint.config.js
index 59db8f77f..ed9b29308 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,32 +1,63 @@
-import js from '@eslint/js';
-import globals from 'globals';
-import pluginReact from 'eslint-plugin-react';
-import { defineConfig } from 'eslint/config';
+import js from '@eslint/js'; // JavaScript 기본 규칙들 (no-unused-vars 등)
+import { defineConfig } from 'eslint/config'; // ESLint 설정 타입 안전성 제공
+import prettierConfig from 'eslint-config-prettier'; // ESLint와 Prettier 규칙 충돌 방지
+import prettier from 'eslint-plugin-prettier'; // Prettier 포맷팅을 ESLint 규칙으로 적용
+import pluginReact from 'eslint-plugin-react'; // React/JSX 전용 규칙들 (Hook 규칙, JSX 문법 등)
+import simpleImportSort from 'eslint-plugin-simple-import-sort'; // import/export 문 자동 정렬
+import globals from 'globals'; // 브라우저/Node.js 전역변수 정의 (window, document 등)
+import tseslint from 'typescript-eslint'; // TypeScript 코드 검사 및 타입 관련 규칙
export default defineConfig([
+ // 기본 파일 타입 및 언어 설정
{
- files: ['**/*.{js,mjs,cjs,jsx}'],
- plugins: { js },
- extends: ['js/recommended'],
- languageOptions: { globals: globals.browser },
+ files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], // 검사할 파일 확장자 지정
+ languageOptions: {
+ globals: globals.browser, // 브라우저 환경 전역변수 사용 가능 (window, document 등)
+ ecmaVersion: 12, // ES2021 문법 지원
+ sourceType: 'module', // ES6 모듈 시스템 사용
+ parserOptions: {
+ ecmaFeatures: {
+ jsx: true, // JSX 문법 파싱 활성화
+ },
+ },
+ },
+ },
+
+ // 기본 권장 설정들
+ js.configs.recommended, // JavaScript 기본 권장 규칙 (문법 오류, 일반적인 실수 방지)
+ ...tseslint.configs.recommended, // TypeScript 권장 규칙 (타입 안전성, TS 모범 사례)
+ pluginReact.configs.flat.recommended, // React 권장 규칙 (Hook 규칙, JSX 모범 사례)
+ prettierConfig, // Prettier와 충돌하는 ESLint 규칙들 비활성화
+
+ // 커스텀 플러그인 및 규칙 설정
+ {
+ plugins: {
+ 'simple-import-sort': simpleImportSort, // import 문 정렬 기능 활성화
+ prettier, // Prettier 포맷팅 검사 기능 활성화
+ },
rules: {
- // 기본 규칙들
- 'no-console': 'warn',
- 'no-unused-vars': 'error',
- 'no-var': 'error',
- 'no-debugger': 'error',
- 'no-unused-expressions': 'error',
- 'no-duplicate-imports': 'error',
- 'no-multiple-empty-lines': 'error',
- 'no-else-return': 'error',
- 'no-param-reassign': 'error',
+ // === 코드 포맷팅 관련 ===
+ 'prettier/prettier': 'error', // Prettier 규칙 위반시 에러 (일관된 코드 포맷팅)
+
+ // === Import/Export 정리 ===
+ 'simple-import-sort/imports': 'error', // import 문을 알파벳순으로 정렬 (가독성 향상)
+ 'simple-import-sort/exports': 'error', // export 문을 알파벳순으로 정렬
- // 최신 문법 권장 규칙들
- 'prefer-const': 'error', // 재할당하지 않는 변수 const 사용
- 'object-shorthand': 'error', // 객체 리터럴 속성 단축 구문 사용
- 'prefer-template': 'error', // 템플릿 리터럴 사용
- 'prefer-destructuring': 'error', // 구조 분해 할당 사용
+ // === 변수 및 코드 품질 ===
+ 'no-unused-vars': 'error', // 사용하지 않는 변수 금지 (불필요한 코드 제거)
+ 'no-console': 'warn', // console.log 사용 경고 (운영환경 배포시 제거 필요)
+ 'no-var': 'error', // var 사용 금지 (let/const 사용 강제)
+ 'prefer-const': 'error', // 재할당 없는 변수는 const 사용 강제
+ 'react/react-in-jsx-scope': 'off', // React 17 이상에서는 필요 없음
+ 'react/jsx-uses-react': 'off', // React 17 이상에서는 필요 없음
+
+ // === 코드 스타일 통일 ===
+ eqeqeq: ['error', 'always'], // === 연산자 사용 강제 (타입 안전성)
+ },
+ settings: {
+ react: {
+ version: 'detect', // React 버전 자동 감지 (버전별 규칙 적용)
+ },
},
},
- pluginReact.configs.flat.recommended,
-]);
\ No newline at end of file
+]);
diff --git a/index.advanced.html b/index.advanced.html
index a070c3355..9ce2fe931 100644
--- a/index.advanced.html
+++ b/index.advanced.html
@@ -1,32 +1,40 @@
-
-
-
- Hanghae Shopping Cart
-
-
-
+
-
-
-
-
-
-
-
-
\ No newline at end of file
+ };
+
+
+
+
+
+
+
+
+