From a856c55625716ed37e168c792f91c8542529c9f7 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 17:57:25 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20msw=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EB=B0=8F=20=ED=80=B4=EC=A6=88=20=EC=85=8B=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20=EB=AA=A8=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/package-lock.json | 645 +++++++++++++++++++++++++++++++++ FE/package.json | 8 +- FE/public/mockServiceWorker.js | 295 +++++++++++++++ FE/src/main.tsx | 28 +- FE/src/mocks/browser.ts | 4 + FE/src/mocks/data.ts | 16 + FE/src/mocks/handlers.ts | 14 + 7 files changed, 1004 insertions(+), 6 deletions(-) create mode 100644 FE/public/mockServiceWorker.js create mode 100644 FE/src/mocks/browser.ts create mode 100644 FE/src/mocks/data.ts create mode 100644 FE/src/mocks/handlers.ts diff --git a/FE/package-lock.json b/FE/package-lock.json index 33f03a6..170377e 100644 --- a/FE/package-lock.json +++ b/FE/package-lock.json @@ -34,6 +34,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "msw": "^2.6.5", "postcss": "^8.4.47", "prettier": "^3.3.3", "tailwindcss": "^3.4.14", @@ -353,6 +354,37 @@ "node": ">=6.9.0" } }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", @@ -1099,6 +1131,127 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@inquirer/confirm": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", + "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.0.tgz", + "integrity": "sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", + "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1165,6 +1318,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mswjs/interceptors": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.0.tgz", + "integrity": "sha512-lDiHQMCBV9qz8c7+zxaNFQtWWaSogTYkqJ3Pg+FGYYC76nsfSxkMQ0df8fojyz16E+w4vp57NLjN2muNG7LugQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.6.tgz", @@ -1407,6 +1578,31 @@ "node": ">= 8" } }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1779,6 +1975,13 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1844,6 +2047,20 @@ "@types/react": "*" } }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.12.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.12.2.tgz", @@ -2146,6 +2363,35 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -2435,6 +2681,94 @@ "node": ">= 6" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2488,6 +2822,16 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3158,6 +3502,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3238,6 +3592,16 @@ "dev": true, "license": "MIT" }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3260,6 +3624,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3378,6 +3749,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3649,6 +4027,61 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.6.5.tgz", + "integrity": "sha512-PnlnTpUlOrj441kYQzzFhzMzMCGFT6a2jKUBG7zSpLkYS5oh8Arrbc0dL8/rNAtxaoBy0EVs2mFqj2qdmWK7lQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.37.0", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -3751,6 +4184,13 @@ "node": ">= 0.8.0" } }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3870,6 +4310,13 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4137,6 +4584,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.10.0.tgz", + "integrity": "sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4147,6 +4604,13 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4286,6 +4750,23 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -4487,6 +4968,23 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -4756,6 +5254,22 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/ts-api-utils": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", @@ -4795,6 +5309,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.27.0.tgz", + "integrity": "sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", @@ -4840,6 +5367,16 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -4881,6 +5418,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -5098,6 +5646,16 @@ "node": ">=0.4.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5118,6 +5676,80 @@ "node": ">= 14" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5131,6 +5763,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zustand": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", diff --git a/FE/package.json b/FE/package.json index 7d12965..3411c2b 100644 --- a/FE/package.json +++ b/FE/package.json @@ -36,11 +36,17 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "globals": "^15.11.0", + "msw": "^2.6.5", "postcss": "^8.4.47", "prettier": "^3.3.3", "tailwindcss": "^3.4.14", "typescript": "~5.6.2", "typescript-eslint": "^8.11.0", "vite": "^5.4.10" + }, + "msw": { + "workerDirectory": [ + "public" + ] } -} +} \ No newline at end of file diff --git a/FE/public/mockServiceWorker.js b/FE/public/mockServiceWorker.js new file mode 100644 index 0000000..89bce29 --- /dev/null +++ b/FE/public/mockServiceWorker.js @@ -0,0 +1,295 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.6.5' +const INTEGRITY_CHECKSUM = 'ca7800994cc8bfb5eb961e037c877074' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + headers.delete('accept', 'msw/passthrough') + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/FE/src/main.tsx b/FE/src/main.tsx index ea9e363..d6b402b 100644 --- a/FE/src/main.tsx +++ b/FE/src/main.tsx @@ -1,10 +1,28 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import App from './App.tsx'; import './index.css'; -createRoot(document.getElementById('root')!).render( - - - -); +async function enableMocking() { + if (process.env.NODE_ENV !== 'development') { + return; + } + + const { worker } = await import('./mocks/browser'); + + return worker.start({ + onUnhandledRequest: 'bypass' + }); +} + +enableMocking().finally(() => { + const queryClient = new QueryClient(); + createRoot(document.getElementById('root')!).render( + + + + + + ); +}); diff --git a/FE/src/mocks/browser.ts b/FE/src/mocks/browser.ts new file mode 100644 index 0000000..0a56427 --- /dev/null +++ b/FE/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw/browser'; +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/FE/src/mocks/data.ts b/FE/src/mocks/data.ts new file mode 100644 index 0000000..0cfad19 --- /dev/null +++ b/FE/src/mocks/data.ts @@ -0,0 +1,16 @@ +export const QuizSetList = Array(100) + .fill(null) + .map((_, i) => ({ + id: i, + title: 'title ' + i, + category: 'category ' + i, + quizList: Array(i).fill({ + id: '0', + quiz: '', + limitTime: 1000, + choiceList: { + content: 'content', + order: 1 + } + }) + })); diff --git a/FE/src/mocks/handlers.ts b/FE/src/mocks/handlers.ts new file mode 100644 index 0000000..c527f6d --- /dev/null +++ b/FE/src/mocks/handlers.ts @@ -0,0 +1,14 @@ +import { http, HttpResponse } from 'msw'; +import { QuizSetList } from './data'; + +export const handlers = [ + http.get('/api/quizset', ({ request }) => { + const url = new URL(request.url); + const offset = Number(url.searchParams.get('offset')); + const size = Number(url.searchParams.get('size')); + const search = url.searchParams.get('search'); + + console.log(offset, size, search); + return HttpResponse.json({ quizSetList: QuizSetList.slice(offset, offset + size) }); + }) +]; From 1c9bde7926ec0842b58e57ccb601c9c4d3583ab5 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 17:58:30 +0900 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20quizset=20>=20quizSet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/api/socketEventTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FE/src/api/socketEventTypes.ts b/FE/src/api/socketEventTypes.ts index 6c7127d..977bedc 100644 --- a/FE/src/api/socketEventTypes.ts +++ b/FE/src/api/socketEventTypes.ts @@ -52,12 +52,12 @@ type UpdateRoomOptionResponse = { // 게임방 퀴즈셋 수정 타입 type UpdateRoomQuizsetRequest = { - quizsetId: number; + quizSetId: number; quizCount: number; }; type UpdateRoomQuizsetResponse = { - quizsetId: number; + quizSetId: number; quizCount: number; }; From ed6131e1e48027d4ab77f6e236088782959e43d2 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 17:59:40 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=8F=99=EA=B8=B0=ED=99=94=ED=95=98=EB=8A=94=20use?= =?UTF-8?q?ServerDate=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/useServerDate.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 FE/src/hooks/useServerDate.ts diff --git a/FE/src/hooks/useServerDate.ts b/FE/src/hooks/useServerDate.ts new file mode 100644 index 0000000..d832a6d --- /dev/null +++ b/FE/src/hooks/useServerDate.ts @@ -0,0 +1,21 @@ +import { useCallback, useEffect, useState } from 'react'; + +const API_PATH = '/time'; + +const useServerDate = () => { + const [offset, setOffset] = useState(0); + useEffect(() => { + const startClientTime = Date.now(); + fetch(API_PATH) + .then((res) => res.json()) + .then((res) => { + const endClientTime = Date.now(); + const clientTime = (startClientTime + endClientTime) / 2; + setOffset(clientTime - res.serverTime); + }); + }, []); + const now = useCallback(() => Date.now() + offset, [offset]); + return now; +}; + +export default useServerDate; From 031e42adb9a05e04e2bfa5407b50f63340790003 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 18:00:22 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20Date.now=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20useServerDat?= =?UTF-8?q?e=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizHeader.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/FE/src/components/QuizHeader.tsx b/FE/src/components/QuizHeader.tsx index b0b3b00..0b89119 100644 --- a/FE/src/components/QuizHeader.tsx +++ b/FE/src/components/QuizHeader.tsx @@ -4,6 +4,7 @@ import AnswerModal from './AnswerModal'; import QuizState from '@/constants/quizState'; import Lottie from 'lottie-react'; import quizLoading from '../assets/lottie/quiz_loading.json'; +import useServerDate from '@/hooks/useServerDate'; export const QuizHeader = () => { const currentQuiz = useQuizeStore((state) => state.currentQuiz); @@ -12,12 +13,13 @@ export const QuizHeader = () => { const [isAnswerVisible, setIsAnswerVisible] = useState(false); const [limitTime, setLimitTime] = useState(0); const answer = useQuizeStore((state) => state.currentAnswer); + const serverNow = useServerDate(); useEffect(() => { if (currentQuiz) { - setSeconds((currentQuiz.endTime - Date.now()) / 1000); + setSeconds((currentQuiz.endTime - serverNow()) / 1000); setLimitTime((currentQuiz.endTime - currentQuiz.startTime) / 1000); } - }, [currentQuiz]); + }, [currentQuiz, serverNow]); useEffect(() => { setIsAnswerVisible(quizState === QuizState.END); @@ -26,9 +28,9 @@ export const QuizHeader = () => { useEffect(() => { requestAnimationFrame(() => { if (seconds <= 0 || !currentQuiz) return; - setSeconds((currentQuiz.endTime - Date.now()) / 1000); + setSeconds((currentQuiz.endTime - serverNow()) / 1000); }); - }, [currentQuiz, seconds]); + }, [currentQuiz, seconds, serverNow]); if (!currentQuiz) return ( @@ -38,10 +40,10 @@ export const QuizHeader = () => { ); - if (currentQuiz.startTime > Date.now()) + if (currentQuiz.startTime > serverNow()) return (
- {Math.ceil((currentQuiz.startTime - Date.now()) / 1000)} + {Math.ceil((currentQuiz.startTime - serverNow()) / 1000)}
); return ( From 5a385b3cbd73862e0064a4e842044aaf29eb358b Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 18:00:39 +0900 Subject: [PATCH 05/14] =?UTF-8?q?fix:=20QuizPreview=20=EB=AA=A9=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizPreview.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/FE/src/components/QuizPreview.tsx b/FE/src/components/QuizPreview.tsx index 24e0e63..12d366e 100644 --- a/FE/src/components/QuizPreview.tsx +++ b/FE/src/components/QuizPreview.tsx @@ -6,11 +6,6 @@ type Props = { description: string; }; -const mock = { - title: '내가만든퀴즈', - description: '아무거나 찍어주세요 행운을 테스트합니다.' -}; - export const QuizPreview = ({ title, description }: Props) => { return (
@@ -18,8 +13,8 @@ export const QuizPreview = ({ title, description }: Props) => {
-
{mock.title + title}
-
{mock.description + description}
+
{title}
+
{description}
); From d9f528be6da31ec1c01c2e45bf2f96d3e2b1fa6b Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 18:01:34 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20QuizSetSearchList=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 무한 스크롤 기능 개발 --- FE/src/components/QuizSetSearchList.tsx | 116 ++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 FE/src/components/QuizSetSearchList.tsx diff --git a/FE/src/components/QuizSetSearchList.tsx b/FE/src/components/QuizSetSearchList.tsx new file mode 100644 index 0000000..22c64d3 --- /dev/null +++ b/FE/src/components/QuizSetSearchList.tsx @@ -0,0 +1,116 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { QuizPreview } from './QuizPreview'; + +type Quiz = { + id: string; + quiz: string; + limitTime: number; + choiceList: { + content: string; + order: number; + }[]; +}; + +type QuizSet = { + id: string; + title: string; + category: string; + quizList: Quiz[]; +}; + +type Params = { + search: string; + onClick: (quizSet: QuizSet) => void; +}; + +const SEARCH_COUNT = 10; + +const QuizSetSearchList = ({ onClick, search }: Params) => { + const fetchPosts = async ({ pageParam = 1 }) => { + const res = await fetch( + '/api/quizset?' + + new URLSearchParams([ + ['search', search], + ['offset', String(pageParam * SEARCH_COUNT)], + ['size', String(SEARCH_COUNT)] + ]) + ); + const data: { quizSetList: QuizSet[] } = await res.json(); + return { + data: data.quizSetList, + nextPage: pageParam + 1, + hasMore: data.quizSetList.length > 0 + }; + }; + + const [selectedQuizSet, setSelectedQuizSet] = useState(null); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = + useInfiniteQuery({ + queryKey: ['posts'], + queryFn: fetchPosts, + initialPageParam: 0, + getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined) + }); + + console.log(data); + + const observerRef = useRef(null); + + const onIntersect = useCallback( + (entries) => { + const [entry] = entries; + + if (entry.isIntersecting && hasNextPage) { + fetchNextPage(); + } + }, + [fetchNextPage, hasNextPage] + ); + + useEffect(() => { + const observer = new IntersectionObserver(onIntersect, { + root: null, + rootMargin: '0px', + threshold: 0.5 + }); + + if (observerRef.current) observer.observe(observerRef.current); + + return () => observer.disconnect(); + }, [onIntersect]); + + if (isLoading) return

Loading...

; + if (isError) return

Error fetching data.

; + + return ( + <> + {data?.pages.map((page) => + page?.data.map((e) => ( +
{ + setSelectedQuizSet(e); + onClick(e); + }} + key={e.id} + style={{ + border: 'solid 2px ' + (selectedQuizSet?.id === e.id ? 'lightgreen' : 'white') + }} + > + +
+ )) + )} +
+ {(isFetchingNextPage || hasNextPage) && ( +
+
+
+ )} +
+ + ); +}; + +export default QuizSetSearchList; From e8a8abde583da59a59f3c40e070e932af300a5f8 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Mon, 18 Nov 2024 18:02:14 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20QuizSettingModal=EC=97=90=20?= =?UTF-8?q?=EB=AA=A9=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=B0=8F=20QuizSetSearchList=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizSettingModal.tsx | 73 +++++++++++++++----------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/FE/src/components/QuizSettingModal.tsx b/FE/src/components/QuizSettingModal.tsx index 0add157..3298f2e 100644 --- a/FE/src/components/QuizSettingModal.tsx +++ b/FE/src/components/QuizSettingModal.tsx @@ -1,9 +1,23 @@ import { useState } from 'react'; import { QuizPreview } from './QuizPreview'; +import { socketService } from '@/api/socket'; +import QuizSetSearchList from './QuizSetSearchList'; type Quiz = { + id: string; + quiz: string; + limitTime: number; + choiceList: { + content: string; + order: number; + }[]; +}; + +type QuizSet = { + id: string; title: string; category: string; + quizList: Quiz[]; }; type Props = { @@ -11,25 +25,24 @@ type Props = { onClose: () => void; }; -const sampleQuizList = [ - { title: 'quiz1', category: 'category1' }, - { title: 'quiz1', category: 'category1' }, - { title: 'quiz1', category: 'category1' }, - { title: 'quiz1', category: 'category1' }, - { title: 'quiz1', category: 'category1' }, - { title: 'quiz1', category: 'category1' } -]; - export const QuizSettingModal = ({ isOpen, onClose }: Props) => { - const [quizList, setQuizList] = useState([]); - const [selectedQuizId, setSelectedQuizId] = useState(-1); + const [selectedQuizSet, setSelectedQuizSet] = useState(null); const [inputValue, setInputValue] = useState(''); + const [searchParam, setSearchParam] = useState(''); const [quizCount, setQuizCount] = useState(0); - const handleSearch = (e: React.FormEvent) => { + const handleSearch: React.FormEventHandler = (e) => { e.preventDefault(); - //fetch - setQuizList(sampleQuizList); + const trimedInputValue = inputValue.trim(); + if (trimedInputValue && trimedInputValue !== searchParam) setSearchParam(trimedInputValue); + }; + + const handleChangeSetting = () => { + if (!selectedQuizSet) return; + socketService.emit('updateRoomQuizset', { + quizSetId: Number(selectedQuizSet.id), + quizCount: quizCount + }); }; return ( @@ -37,7 +50,7 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => { className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-10" style={{ display: isOpen ? 'flex' : 'none' }} > -
+
@@ -56,35 +69,33 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => {
- {quizList.map((_, i) => ( -
setSelectedQuizId(i)} - key={i} - style={{ border: 'solid 2px ' + (selectedQuizId === i ? 'lightgreen' : 'white') }} - > - -
- ))} +
- {selectedQuizId >= 0 ? ( + {selectedQuizSet ? (
선택된 퀴즈
- +
- {`퀴즈 개수(${quizCount})`} + {`퀴즈 개수(${Math.min(quizCount, selectedQuizSet.quizList.length)})`} setQuizCount(Number(e.target.value))} />
-
From 42cfc415f297237c6d6d949f7c618e1ff32cb6c5 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 00:25:31 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20QuizSetSearchList=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B2=80=EC=83=89=EC=96=B4=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=8B=9C=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizSetSearchList.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/FE/src/components/QuizSetSearchList.tsx b/FE/src/components/QuizSetSearchList.tsx index 22c64d3..d543250 100644 --- a/FE/src/components/QuizSetSearchList.tsx +++ b/FE/src/components/QuizSetSearchList.tsx @@ -47,14 +47,12 @@ const QuizSetSearchList = ({ onClick, search }: Params) => { const [selectedQuizSet, setSelectedQuizSet] = useState(null); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = useInfiniteQuery({ - queryKey: ['posts'], + queryKey: [search], queryFn: fetchPosts, initialPageParam: 0, getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined) }); - console.log(data); - const observerRef = useRef(null); const onIntersect = useCallback( From 1b30920124b802d65cd8ba1f9d980ba206bc3de7 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 00:26:10 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=EC=9E=AC=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EC=8B=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=ED=99=94=20=EB=B0=8F=20=ED=80=B4=EC=A6=88=20=EA=B0=9C=EC=88=98?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizSettingModal.tsx | 29 ++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/FE/src/components/QuizSettingModal.tsx b/FE/src/components/QuizSettingModal.tsx index 3298f2e..8d50077 100644 --- a/FE/src/components/QuizSettingModal.tsx +++ b/FE/src/components/QuizSettingModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useRef, useState } from 'react'; import { QuizPreview } from './QuizPreview'; import { socketService } from '@/api/socket'; import QuizSetSearchList from './QuizSetSearchList'; @@ -30,11 +30,15 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => { const [inputValue, setInputValue] = useState(''); const [searchParam, setSearchParam] = useState(''); const [quizCount, setQuizCount] = useState(0); + const scrollRef = useRef(null); const handleSearch: React.FormEventHandler = (e) => { e.preventDefault(); const trimedInputValue = inputValue.trim(); - if (trimedInputValue && trimedInputValue !== searchParam) setSearchParam(trimedInputValue); + if (trimedInputValue !== searchParam) { + setSearchParam(trimedInputValue); + if (scrollRef.current) scrollRef.current.scrollTop = 0; + } }; const handleChangeSetting = () => { @@ -45,6 +49,11 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => { }); }; + const handleSelectQuizSet = (quizSet: QuizSet) => { + setSelectedQuizSet(quizSet); + setQuizCount(quizSet.quizList.length); + }; + return (
{ ✕
-
- +
+ {searchParam && ( + + )}
@@ -82,12 +89,12 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => {
선택된 퀴즈
- {`퀴즈 개수(${Math.min(quizCount, selectedQuizSet.quizList.length)})`} + {`퀴즈 개수(${quizCount})`} setQuizCount(Number(e.target.value))} />
@@ -101,7 +108,7 @@ export const QuizSettingModal = ({ isOpen, onClose }: Props) => {
) : ( -
+
퀴즈를 선택해주세요
)} From 53a383f95472490b3b9c07d99976b05a83ac42e8 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 00:34:30 +0900 Subject: [PATCH 10/14] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=20=ED=81=AC?= =?UTF-8?q?=EA=B8=B0=EA=B0=80=20=EB=B0=94=EB=80=8C=EC=97=88=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9E=AC=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/QuizOptionBoard.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/FE/src/components/QuizOptionBoard.tsx b/FE/src/components/QuizOptionBoard.tsx index efd66e8..563d4e9 100644 --- a/FE/src/components/QuizOptionBoard.tsx +++ b/FE/src/components/QuizOptionBoard.tsx @@ -38,7 +38,6 @@ export const QuizOptionBoard = () => { setSelectedOption(option); }; - // const const boardRef = useRef(null); const [boardRect, setBoardRect] = useState(null); @@ -48,6 +47,16 @@ export const QuizOptionBoard = () => { } }, []); + useEffect(() => { + const handleResize = () => { + if (boardRef.current) setBoardRect(boardRef.current.getBoundingClientRect()); + }; + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [boardRef]); + return (
Date: Tue, 19 Nov 2024 01:41:18 +0900 Subject: [PATCH 11/14] =?UTF-8?q?feat:=20useServerDate=20path=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/useServerDate.ts | 2 +- FE/src/mocks/handlers.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/FE/src/hooks/useServerDate.ts b/FE/src/hooks/useServerDate.ts index d832a6d..f76f48d 100644 --- a/FE/src/hooks/useServerDate.ts +++ b/FE/src/hooks/useServerDate.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; -const API_PATH = '/time'; +const API_PATH = '/api/time'; const useServerDate = () => { const [offset, setOffset] = useState(0); diff --git a/FE/src/mocks/handlers.ts b/FE/src/mocks/handlers.ts index c527f6d..7a2635d 100644 --- a/FE/src/mocks/handlers.ts +++ b/FE/src/mocks/handlers.ts @@ -10,5 +10,8 @@ export const handlers = [ console.log(offset, size, search); return HttpResponse.json({ quizSetList: QuizSetList.slice(offset, offset + size) }); + }), + http.get('/api/time', () => { + return HttpResponse.json({ serverTime: Date.now() }); }) ]; From 7c8d18b06ca1712b4e842e4b3decea630daf39b3 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 01:42:04 +0900 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20=ED=80=B4=EC=A6=88=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=A7=80=20=EB=B3=B4=EB=93=9C=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 크기 변경 시 플레이어 위치 재조정 - 퀴즈 카운트 다운에는 선택지 보여주지 않음 --- FE/src/components/QuizOptionBoard.tsx | 62 ++++++++++++++++++--------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/FE/src/components/QuizOptionBoard.tsx b/FE/src/components/QuizOptionBoard.tsx index 563d4e9..47b60e5 100644 --- a/FE/src/components/QuizOptionBoard.tsx +++ b/FE/src/components/QuizOptionBoard.tsx @@ -4,6 +4,7 @@ import { socketService } from '@/api/socket'; import { useRoomStore } from '@/store/useRoomStore'; import { useEffect, useRef, useState } from 'react'; import { useQuizeStore } from '@/store/useQuizStore'; +import useServerDate from '@/hooks/useServerDate'; const optionColors = [ '#FF9AA2', // pastel red @@ -22,10 +23,13 @@ export const QuizOptionBoard = () => { const currentPlayerId = usePlayerStore((state) => state.currentPlayerId); const gameId = useRoomStore((state) => state.gameId); const players = usePlayerStore((state) => state.players); - const options = useQuizeStore((state) => state.currentQuiz?.choiceList) || []; + const currentQuiz = useQuizeStore((state) => state.currentQuiz); + const choiceList = currentQuiz?.choiceList || []; const quizState = useQuizeStore((state) => state.quizState); const quizAnswer = useQuizeStore((state) => state.currentAnswer); - const [selectedOption, setSelectedOption] = useState(options.length); + const [selectedOption, setSelectedOption] = useState(currentQuiz?.choiceList.length); + const [choiceListVisible, setChoiceListVisible] = useState(false); + const serverNow = useServerDate(); const handleClick: React.MouseEventHandler = (e) => { const { pageX, pageY } = e; @@ -34,19 +38,21 @@ export const QuizOptionBoard = () => { const y = (pageY - top) / height; if (x > 1 || y > 1) return; socketService.emit('updatePosition', { gameId, newPosition: [y, x] }); - const option = Math.round(x) + Math.floor(y * Math.ceil(options.length / 2)) * 2; + const option = Math.round(x) + Math.floor(y * Math.ceil(choiceList.length / 2)) * 2; setSelectedOption(option); }; const boardRef = useRef(null); const [boardRect, setBoardRect] = useState(null); + // 보드 크기 초기화 useEffect(() => { if (boardRef.current) { setBoardRect(boardRef.current.getBoundingClientRect()); } }, []); + // 화면 변화에 따라 보드 크기 재조정 useEffect(() => { const handleResize = () => { if (boardRef.current) setBoardRect(boardRef.current.getBoundingClientRect()); @@ -57,6 +63,17 @@ export const QuizOptionBoard = () => { }; }, [boardRef]); + // 퀴즈 시작 시간에 선택지 렌더링 + useEffect(() => { + const interval = setInterval(() => { + if (!choiceListVisible && currentQuiz && currentQuiz.startTime <= serverNow()) + setChoiceListVisible(true); + else if (choiceListVisible && currentQuiz && currentQuiz.startTime > serverNow()) + setChoiceListVisible(false); + }, 100); + return () => clearInterval(interval); + }, [choiceListVisible, currentQuiz, serverNow]); + return (
{ : null}
- {options.map((option, i) => ( -
( +
-
{i + 1 + '. ' + option.content}
-
- ))} + : 'none', + opacity: quizState === 'end' && option.order !== quizAnswer ? '0.3' : '1', + textShadow: '-1px 0 white, 0 1px white, 1px 0 white, 0 -1px white' + }} + > +
+ {i + 1 + '. ' + option.content} +
+
+ ))}
); From d18e2c4cf3951c593b10568e04e77c4078557f97 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 01:42:40 +0900 Subject: [PATCH 13/14] =?UTF-8?q?fix:=20=EC=86=8C=EC=BC=93=20=EB=AA=A9=20?= =?UTF-8?q?=EC=A0=95=EB=8B=B5=20=EA=B3=84=EC=82=B0=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/api/mocks/SocketMock.ts | 2 +- FE/src/api/mocks/socketMocks/SocketMockStartGame.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FE/src/api/mocks/SocketMock.ts b/FE/src/api/mocks/SocketMock.ts index 2282c5e..32c7f20 100644 --- a/FE/src/api/mocks/SocketMock.ts +++ b/FE/src/api/mocks/SocketMock.ts @@ -159,7 +159,7 @@ export class SocketMock { const players = this.players.map((p) => { const [y, x] = p.playerPosition; const option = - Math.round(x) + Math.floor(y * Math.ceil((this.quiz?.choiceList.length || 0) / 2)) * 2; + Math.round(x) + Math.floor(y * Math.ceil((this.quiz?.choiceList.length || 0) / 2)) * 2 + 1; return { playerId: p.playerId, isAnswer: option === answer, diff --git a/FE/src/api/mocks/socketMocks/SocketMockStartGame.ts b/FE/src/api/mocks/socketMocks/SocketMockStartGame.ts index c90da72..2c02836 100644 --- a/FE/src/api/mocks/socketMocks/SocketMockStartGame.ts +++ b/FE/src/api/mocks/socketMocks/SocketMockStartGame.ts @@ -25,7 +25,7 @@ export default class SocketMockStartGame extends SocketMock { // 퀴즈 종료 await this.delay(8); - this.calculateScore(0); + this.calculateScore(1); this.log('퀴즈 가 종료 되었습니다.'); } } From d9ab1ce0c6ddfa9154ad4fd16a0799afb82f45b2 Mon Sep 17 00:00:00 2001 From: ijun17 Date: Tue, 19 Nov 2024 10:21:18 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20offset=20=EB=B9=BC=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/hooks/useServerDate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/hooks/useServerDate.ts b/FE/src/hooks/useServerDate.ts index f76f48d..b2def38 100644 --- a/FE/src/hooks/useServerDate.ts +++ b/FE/src/hooks/useServerDate.ts @@ -14,7 +14,7 @@ const useServerDate = () => { setOffset(clientTime - res.serverTime); }); }, []); - const now = useCallback(() => Date.now() + offset, [offset]); + const now = useCallback(() => Date.now() - offset, [offset]); return now; };