diff --git a/next.config.mjs b/next.config.mjs index 13f973a..d1c0f33 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -12,6 +12,26 @@ const nextConfig = { }, ], }, + experimental: { + instrumentationHook: true, + }, + webpack(config, { isServer }) { + if (isServer) { + if (Array.isArray(config.resolve.alias)) { + config.resolve.alias.push({ name: "msw/browser", alias: false }); + } else { + config.resolve.alias["msw/browser"] = false; + } + } else { + if (Array.isArray(config.resolve.alias)) { + config.resolve.alias.push({ name: "msw/node", alias: false }); + } else { + config.resolve.alias["msw/node"] = false; + } + } + + return config; + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index c47f2e4..cdda683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "lucide-react": "^0.344.0", "next": "14.1.0", "next-i18n-router": "^5.3.1", + "qs": "^6.11.2", "react": "^18", "react-dom": "^18", "react-i18next": "^14.1.0", @@ -58,6 +59,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "msw": "^2.2.13", "postcss": "^8", "prettier": "^2.7.1", "storybook": "^7.6.17", @@ -2241,6 +2243,33 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "dependencies": { + "statuses": "^2.0.1" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2873,6 +2902,116 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@inquirer/confirm": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.4.tgz", + "integrity": "sha512-2z2RC0JyQCmggQfRxFnQitGp8YZgdM/AqcOuLaUtL0dZHFByk5jgtzxECX4z5MsH8aq2WzdLPI2AHmHOkh8eRA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^8.0.0", + "@inquirer/type": "^1.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.0.0.tgz", + "integrity": "sha512-RAszmjXj+grbT9yQ9B+me40LskytwBYPhyl6yHI8h+J5BmL0gNI3pdvBBFD6S9LV0lzhzfCRMBMH5UvuUPYzZQ==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.0", + "@inquirer/type": "^1.3.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.12.7", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "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 + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "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/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, + "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.0", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.0.tgz", + "integrity": "sha512-3fw+7+77/duTnMJTeSS44wneszghI4tkr0m0xdIJabbYRe36ElzmsqyboMZ1nFRon6sT+ckVvYDVjwapKv+2sw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.0.tgz", + "integrity": "sha512-RW4Zf6RCTnInRaOZuRHTqAUl+v6VJuQGglir7nW2BkT3OXOphMhkIFhvFRjorBx2l0VwtC/M4No8vYR65TdN9Q==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3510,6 +3649,32 @@ "react": ">=16" } }, + "node_modules/@mswjs/cookies": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", + "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.26.15", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.26.15.tgz", + "integrity": "sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==", + "dev": true, + "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.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@ndelangen/get-tarball": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", @@ -3750,6 +3915,28 @@ "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 + }, + "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, + "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 + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6370,6 +6557,12 @@ "@types/node": "*" } }, + "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 + }, "node_modules/@types/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -6599,10 +6792,19 @@ "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", "dev": true }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "20.11.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "devOptional": true, "dependencies": { "undici-types": "~5.26.4" @@ -6719,6 +6921,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "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 + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -6737,6 +6945,12 @@ "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -8815,7 +9029,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -9250,6 +9463,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "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, + "engines": { + "node": ">= 12" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -10157,7 +10379,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -10809,7 +11030,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -10821,7 +11041,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -12719,7 +12938,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -12931,7 +13149,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -12950,6 +13167,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/graphql": { + "version": "16.8.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", + "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -13034,7 +13260,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -13046,7 +13271,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13058,7 +13282,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -13124,6 +13347,12 @@ "he": "bin/he" } }, + "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 + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -13955,6 +14184,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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 + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -16660,6 +16895,76 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/msw": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.2.13.tgz", + "integrity": "sha512-ljFf1xZsU0b4zv1l7xzEmC6OZA6yD06hcx0H+dc8V0VypaP3HGYJa1rMLjQbBWl32ptGhcfwcPCWDB1wjmsftw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@inquirer/confirm": "^3.0.0", + "@mswjs/cookies": "^1.1.0", + "@mswjs/interceptors": "^0.26.14", + "@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.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz", + "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -17236,7 +17541,6 @@ "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -17509,6 +17813,12 @@ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", "dev": true }, + "node_modules/outvariant": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", + "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", + "dev": true + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -18600,7 +18910,6 @@ "version": "6.12.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.0.tgz", "integrity": "sha512-trVZiI6RMOkO476zLGaBIzszOdFPnCCXHPG9kn0yuS1uz6xdVxPfZdB3vUig9pxPFDM9BRAgz/YUIVQ1/vuiUg==", - "dev": true, "dependencies": { "side-channel": "^1.0.6" }, @@ -19776,7 +20085,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -19920,7 +20228,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", @@ -20275,6 +20582,12 @@ "bare-events": "^2.2.0" } }, + "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 + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -21561,9 +21874,9 @@ "dev": true }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "devOptional": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index a74deee..2b5d5e4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "i18nexus pull && node server.js", + "dev": "i18nexus pull && next dev", "build": "i18nexus pull && next build", "start": "i18nexus pull && next start", "lint": "next lint", @@ -26,6 +26,7 @@ "lucide-react": "^0.344.0", "next": "14.1.0", "next-i18n-router": "^5.3.1", + "qs": "^6.11.2", "react": "^18", "react-dom": "^18", "react-i18next": "^14.1.0", @@ -65,11 +66,17 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "msw": "^2.2.13", "postcss": "^8", "prettier": "^2.7.1", "storybook": "^7.6.17", "tailwindcss": "^3.3.0", "ts-node": "^10.9.2", "typescript": "^5" + }, + "msw": { + "workerDirectory": [ + "public" + ] } } diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..d56bca4 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* 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.2.13"; +const INTEGRITY_CHECKSUM = "26357c79639bfa20d64c0efca2a87423"; +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: true, + }); + 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 (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() { + const headers = Object.fromEntries(requestClone.headers.entries()); + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers["x-msw-intention"]; + + 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/src/app/[locale]/home/components/header/MobileHomeHeader.tsx b/src/app/[locale]/home/components/header/MobileHomeHeader.tsx new file mode 100644 index 0000000..f553fd4 --- /dev/null +++ b/src/app/[locale]/home/components/header/MobileHomeHeader.tsx @@ -0,0 +1,27 @@ +import BellIcon from "@/components/icons/BellIcon"; +import Header from "@/components/common/Header"; +import Link from "next/link"; +import React from "react"; + +const MobileHomeHeader = () => { + return ( +
+ logo + + } + rightNode={ + // [TODO] : 기획에 따라 알림페이지 이동 or 알림리스트 모달 띄우기 + + } + /> + ); +}; + +export default MobileHomeHeader; diff --git a/src/app/[locale]/home/components/header/SearchPlace.tsx b/src/app/[locale]/home/components/header/SearchPlace.tsx new file mode 100644 index 0000000..077bbc1 --- /dev/null +++ b/src/app/[locale]/home/components/header/SearchPlace.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React, { useState } from "react"; + +import { InputWithIcon } from "@/components/ui/InputWithIcon"; +import { SearchIcon } from "lucide-react"; + +const SearchPlace = () => { + // query를 받아와 router push로 검색 결과 페이지로 이동 + const [query, setQuery] = useState(""); + + return ( + } + className="shadow-input py-2 rounded-full border border-[#e6e6e6] bg-white" + onChange={e => setQuery(e.target.value)} + placeholder="여행지를 입력해주세요." + value={query} + /> + ); +}; + +export default SearchPlace; diff --git a/src/app/[locale]/home/components/place/CityItem.tsx b/src/app/[locale]/home/components/place/CityItem.tsx new file mode 100644 index 0000000..b48b53a --- /dev/null +++ b/src/app/[locale]/home/components/place/CityItem.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; +import { Place } from "@/lib/types/place"; +import React from "react"; + +interface Props { + place: Place; + priority?: boolean; +} + +const CityItem = ({ place, priority }: Props) => { + const img = "https://www.ghibli.jp/gallery/ponyo016.jpg"; + return ( +
  • + travel +

    + {place.name} +

    +
  • + ); +}; + +export default CityItem; diff --git a/src/app/[locale]/home/components/place/PlaceList.tsx b/src/app/[locale]/home/components/place/PlaceList.tsx new file mode 100644 index 0000000..0628868 --- /dev/null +++ b/src/app/[locale]/home/components/place/PlaceList.tsx @@ -0,0 +1,52 @@ +import CityItem from "./CityItem"; +import { PlaceAPI } from "@/lib/api/place"; +import React from "react"; +import ThemeItem from "./ThemeItem"; +import TravelItem from "./TravelItem"; +import WithTitleWrapper from "@/components/common/WithTitleWrapper"; + +const PlaceMap = { + CITY: { + component: CityItem, + title: "인기 여행 도시", + }, + TRAVEL: { + component: TravelItem, + title: "지금 가장 인기있는 여행지", + }, + THEME: { + component: ThemeItem, + title: "지금가면 좋은 여행지", + }, +}; + +interface Props { + placeType: "CITY" | "TRAVEL" | "THEME"; + moreHref: string; +} + +const PlaceList = async ({ placeType, moreHref }: Props) => { + const { data } = await PlaceAPI.getPlace({ + page: 1, + placeType, + elementCnt: 10, + }); + + const placeItem = PlaceMap[placeType]; + + return ( + +
      + {data.map((place, index) => ( + + ))} +
    +
    + ); +}; + +export default PlaceList; diff --git a/src/app/[locale]/home/components/place/ThemeItem.tsx b/src/app/[locale]/home/components/place/ThemeItem.tsx new file mode 100644 index 0000000..8988e8e --- /dev/null +++ b/src/app/[locale]/home/components/place/ThemeItem.tsx @@ -0,0 +1,34 @@ +import Image from "next/image"; +import { Place } from "@/lib/types/place"; +import React from "react"; + +interface Props { + place: Place; + priority?: boolean; +} + +const ThemeItem = ({ place, priority }: Props) => { + const img = "https://www.ghibli.jp/gallery/ponyo016.jpg"; + return ( +
  • + travel +
    +

    + {place.name} +

    +

    + {place.subName} +

    +
    +
  • + ); +}; + +export default ThemeItem; diff --git a/src/app/[locale]/home/components/place/TravelItem.tsx b/src/app/[locale]/home/components/place/TravelItem.tsx new file mode 100644 index 0000000..ac863f1 --- /dev/null +++ b/src/app/[locale]/home/components/place/TravelItem.tsx @@ -0,0 +1,34 @@ +import Image from "next/image"; +import { Place } from "@/lib/types/place"; +import React from "react"; + +interface Props { + place: Place; + priority?: boolean; +} + +const TravelItem = ({ place, priority }: Props) => { + const img = "https://www.ghibli.jp/gallery/ponyo016.jpg"; + return ( +
  • + travel +
    +

    + {place.name} +

    +

    + {place.subName} +

    +
    +
  • + ); +}; + +export default TravelItem; diff --git a/src/app/[locale]/home/components/review/ReviewList.tsx b/src/app/[locale]/home/components/review/ReviewList.tsx new file mode 100644 index 0000000..46424b9 --- /dev/null +++ b/src/app/[locale]/home/components/review/ReviewList.tsx @@ -0,0 +1,28 @@ +import { IResponseReview } from "@/lib/types/travel"; +import React from "react"; +import ReviewCard from "@/components/ui/cards/ReviewCard"; +import WithTitleWrapper from "@/components/common/WithTitleWrapper"; + +async function getReviews() { + const response = await fetch(`${process.env.API_HOST}/review`); + + const reviews: IResponseReview = await response.json(); + + return reviews; +} + +const ReviewList = async () => { + const reviews = await getReviews(); + + return ( + +
    + {[...reviews.data].slice(0, 2).map(review => ( + + ))} +
    +
    + ); +}; + +export default ReviewList; diff --git a/src/app/[locale]/home/components/review/TravelStoryList.tsx b/src/app/[locale]/home/components/review/TravelStoryList.tsx new file mode 100644 index 0000000..b82cdfa --- /dev/null +++ b/src/app/[locale]/home/components/review/TravelStoryList.tsx @@ -0,0 +1,28 @@ +import { IResponseTravelStory } from "@/lib/types/travel"; +import React from "react"; +import TravelStoryCard from "@/components/ui/cards/TravelStoryCard"; +import WithTitleWrapper from "@/components/common/WithTitleWrapper"; + +async function getTravelStories() { + const response = await fetch(`${process.env.API_HOST}/story`); + + const stories: IResponseTravelStory = await response.json(); + + return stories; +} + +const TravelStoryList = async () => { + const stories = await getTravelStories(); + + return ( + +
    + {[...stories.data].slice(0, 2).map(story => ( + + ))} +
    +
    + ); +}; + +export default TravelStoryList; diff --git a/src/app/[locale]/home/page.tsx b/src/app/[locale]/home/page.tsx new file mode 100644 index 0000000..514e411 --- /dev/null +++ b/src/app/[locale]/home/page.tsx @@ -0,0 +1,30 @@ +import MobileHomeHeader from "./components/header/MobileHomeHeader"; +import MobileNavigation from "@/components/common/MobileNavigation"; +import PlaceList from "./components/place/PlaceList"; +import React from "react"; +import ReviewList from "./components/review/ReviewList"; +import SearchPlace from "./components/header/SearchPlace"; +import TravelStoryList from "./components/review/TravelStoryList"; + +const page = async () => { + return ( +
    +
    + + + + + + + + + + + +
    + +
    + ); +}; + +export default page; diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 8d44a60..7bfe020 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,15 +1,16 @@ import "../../lib/styles/globals.css"; import type { Metadata } from "next"; -import { Noto_Sans_KR } from "next/font/google"; +import { MockProvider } from "@/mocks/MockProvider"; import React from "react"; import { cn } from "../../lib/utils/cn"; import { dir } from "i18next"; import i18nConfig from "../../../i18nConfig"; +import localFont from "next/font/local"; -const notoSans = Noto_Sans_KR({ - subsets: ["latin"], - weight: ["100", "400", "700", "900"], +const suit = localFont({ + src: "../fonts/SUIT-Variable-woff2/SUIT-Variable.woff2", + display: "swap", }); export function generateStaticParams() { @@ -30,7 +31,10 @@ export default function RootLayout({ }>) { return ( - {children} + + + {children} + ); } diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx index dfa91f2..d1e0b80 100644 --- a/src/app/[locale]/page.tsx +++ b/src/app/[locale]/page.tsx @@ -1,29 +1,25 @@ -import { Button } from "@/components/ui/Button"; -import Card1 from "@/components/ui/cards/Card1"; -import Card2 from "@/components/ui/cards/Card2"; -import Card3 from "@/components/ui/cards/Card3"; -import ExampleClientComponent from "@/components/test/ExampleClientComponent"; -import LanguageChanger from "@/components/test/LanguageChanger"; -import TranslationProvider from "@/lib/shared/TranslationsProvider"; -import { TvIcon } from "lucide-react"; -import initTranslations from "@/lib/shared/i18n"; +// import initTranslations from "@/lib/shared/i18n"; -// import LanguageChanger from "../../components/test/LanguageChanger"; +import { redirect } from "next/navigation"; -type Params = { - params: { - locale: string; - }; -}; +// // import LanguageChanger from "../../components/test/LanguageChanger"; -const i18nNamespaces = ["home"]; +// type Params = { +// params: { +// locale: string; +// }; +// }; -export default async function Home({ params: { locale } }: Params) { - const { t, resources } = await initTranslations(locale, i18nNamespaces); +// const i18nNamespaces = ["home"]; + +export default function Home() { + // const { t, resources } = await initTranslations(locale, i18nNamespaces); + + redirect("/home"); return (
    -
    + {/*

    Hello World

    ); } diff --git a/src/app/fonts/SUIT-Variable-woff2/LICENSE b/src/app/fonts/SUIT-Variable-woff2/LICENSE new file mode 100644 index 0000000..7ee44cf --- /dev/null +++ b/src/app/fonts/SUIT-Variable-woff2/LICENSE @@ -0,0 +1,94 @@ +Copyright (c) 2022, SUNN (http://sun.fo/suit), +with Reserved Font Name SUIT. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.css b/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.css new file mode 100644 index 0000000..333665d --- /dev/null +++ b/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.css @@ -0,0 +1,5 @@ +@font-face { + font-family: 'SUIT Variable'; + font-weight: 100 900; + src: url('./SUIT-Variable.woff2') format('woff2-variations'); +} \ No newline at end of file diff --git a/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.woff2 b/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.woff2 new file mode 100644 index 0000000..31be573 Binary files /dev/null and b/src/app/fonts/SUIT-Variable-woff2/SUIT-Variable.woff2 differ diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx new file mode 100644 index 0000000..d477afc --- /dev/null +++ b/src/components/common/Header.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +interface Props { + leftNode?: React.ReactNode; + title?: string; + rightNode?: React.ReactNode; +} + +const Header = ({ leftNode, title, rightNode }: Props) => { + return ( +
    +
    {leftNode ?? leftNode}
    + {title && ( +

    + {title} +

    + )} +
    {rightNode ?? rightNode}
    +
    + ); +}; + +export default Header; diff --git a/src/components/common/MobileNavigation.tsx b/src/components/common/MobileNavigation.tsx new file mode 100644 index 0000000..c91ea6a --- /dev/null +++ b/src/components/common/MobileNavigation.tsx @@ -0,0 +1,56 @@ +"use client"; + +import CalendarIcon from "../icons/CalendarIcon"; +import HomeIcon from "../icons/HomeIcon"; +import Link from "next/link"; +import MessageIcon from "../icons/MessageIcon"; +import ProfileIcon from "../icons/ProfileIcon"; +import React from "react"; +import SearchIcon from "../icons/SearchIcon"; +import { usePathname } from "next/navigation"; + +// 임시 경로(논의 필요) +const Menus = [ + { name: "홈", icon: HomeIcon, href: "/home" }, + { name: "검색", icon: SearchIcon, href: "/search" }, + { name: "일정", icon: CalendarIcon, href: "/plan" }, + { name: "리뷰", icon: MessageIcon, href: "/review" }, + { name: "마이페이지", icon: ProfileIcon, href: "/mypage" }, +]; + +const MobileNavigation = () => { + const pathname = usePathname(); + + return ( +
    +
      + {Menus.map(menu => ( +
    • + + + + +

      + {menu.name} +

      + +
    • + ))} +
    +
    + ); +}; + +export default MobileNavigation; diff --git a/src/components/common/WithTitleWrapper.tsx b/src/components/common/WithTitleWrapper.tsx new file mode 100644 index 0000000..09813c2 --- /dev/null +++ b/src/components/common/WithTitleWrapper.tsx @@ -0,0 +1,26 @@ +import Link from "next/link"; +import React from "react"; + +interface Props { + title: string; + moreHref: string; + children: React.ReactNode; +} + +const WithTitleWrapper = ({ title, moreHref, children }: Props) => { + return ( +
    +
    +

    + {title} +

    + + 더보기 + +
    + {children} +
    + ); +}; + +export default WithTitleWrapper; diff --git a/src/components/icons/BellIcon.tsx b/src/components/icons/BellIcon.tsx new file mode 100644 index 0000000..ace6050 --- /dev/null +++ b/src/components/icons/BellIcon.tsx @@ -0,0 +1,30 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const BellIcon = ({ className, fill = "none", stroke = "#1A1A1A" }: Props) => { + return ( + + + + ); +}; + +export default BellIcon; diff --git a/src/components/icons/CalendarIcon.tsx b/src/components/icons/CalendarIcon.tsx new file mode 100644 index 0000000..f1ce37f --- /dev/null +++ b/src/components/icons/CalendarIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const CalendarIcon = ({ + className, + fill = "none", + stroke = "#4D4D4D", +}: Props) => { + return ( + + + + ); +}; + +export default CalendarIcon; diff --git a/src/components/icons/CommentIcon.tsx b/src/components/icons/CommentIcon.tsx new file mode 100644 index 0000000..5ddea33 --- /dev/null +++ b/src/components/icons/CommentIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const CommentIcon = ({ + className, + fill = "none", + stroke = "#B8B8B8", +}: Props) => { + return ( + + + + ); +}; + +export default CommentIcon; diff --git a/src/components/icons/HeartIcon.tsx b/src/components/icons/HeartIcon.tsx new file mode 100644 index 0000000..6b949a0 --- /dev/null +++ b/src/components/icons/HeartIcon.tsx @@ -0,0 +1,43 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const HeartIcon = ({ className, fill = "none", stroke = "#B8B8B8" }: Props) => { + return ( + + + + + + + + + + + ); +}; + +export default HeartIcon; diff --git a/src/components/icons/HomeIcon.tsx b/src/components/icons/HomeIcon.tsx new file mode 100644 index 0000000..db1d893 --- /dev/null +++ b/src/components/icons/HomeIcon.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const HomeIcon = ({ className, fill = "none", stroke = "#4D4D4D" }: Props) => { + return ( + + + + ); +}; + +export default HomeIcon; diff --git a/src/components/icons/MessageIcon.tsx b/src/components/icons/MessageIcon.tsx new file mode 100644 index 0000000..f58e5ce --- /dev/null +++ b/src/components/icons/MessageIcon.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const MessageIcon = ({ + className, + fill = "none", + stroke = "#4D4D4D", +}: Props) => { + return ( + + + + ); +}; + +export default MessageIcon; diff --git a/src/components/icons/ProfileIcon.tsx b/src/components/icons/ProfileIcon.tsx new file mode 100644 index 0000000..ca9c31b --- /dev/null +++ b/src/components/icons/ProfileIcon.tsx @@ -0,0 +1,37 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const ProfileIcon = ({ + className, + fill = "none", + stroke = "#4D4D4D", +}: Props) => { + return ( + + + + + ); +}; + +export default ProfileIcon; diff --git a/src/components/icons/SearchIcon.tsx b/src/components/icons/SearchIcon.tsx new file mode 100644 index 0000000..e713c2b --- /dev/null +++ b/src/components/icons/SearchIcon.tsx @@ -0,0 +1,35 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; + strokeWidth?: string; +} + +const SearchIcon = ({ + className, + fill = "none", + stroke = "#4D4D4D", + strokeWidth = "1.5", +}: Props) => { + return ( + + + + ); +}; + +export default SearchIcon; diff --git a/src/components/icons/StartIcon.tsx b/src/components/icons/StartIcon.tsx new file mode 100644 index 0000000..d90c4b8 --- /dev/null +++ b/src/components/icons/StartIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; + +interface Props { + className?: string; + fill?: string; + stroke?: string; +} + +const StarIcon = ({ + className, + fill = "#FFD990", + stroke = "#FFD990", +}: Props) => { + return ( + + + + ); +}; + +export default StarIcon; diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx index bd340d4..17bd5ff 100644 --- a/src/components/ui/Input.tsx +++ b/src/components/ui/Input.tsx @@ -13,7 +13,7 @@ const Input = React.forwardRef( ( return (
    ( { - const card = { - isLike: true, - likeCount: 10, - commentCount: 5, - imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", - tagList: ["#서울", "#대전"], - title: "첫번째 카드", - userName: "신윤철", - userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", - }; - return ( -
    - - -
    - - -
    - -
    - - -
    -
    -
    -
    -
    - ); -}; - -export default Card1; diff --git a/src/components/ui/cards/Card3.tsx b/src/components/ui/cards/Card3.tsx deleted file mode 100644 index 3b9148f..0000000 --- a/src/components/ui/cards/Card3.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import Card from "./context/CardProvider"; -import React from "react"; - -const Card3 = () => { - const card = { - isStar: true, - starCount: 10, - commentCount: 5, - imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", - title: "세번째 카드 종류", - description: "설명도 있음", - userName: "yunnnn", - userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", - }; - return ( -
    - - -
    - -
    - -
    - - -
    -
    -
    -
    -
    - ); -}; - -export default Card3; diff --git a/src/components/ui/cards/ReviewCard.tsx b/src/components/ui/cards/ReviewCard.tsx new file mode 100644 index 0000000..71a5197 --- /dev/null +++ b/src/components/ui/cards/ReviewCard.tsx @@ -0,0 +1,31 @@ +"use client"; + +import Card from "./context/CardProvider"; +import { IReview } from "@/lib/types/travel"; +import React from "react"; + +interface Props { + review: IReview; +} + +const ReviewCard = ({ review }: Props) => { + return ( +
    + + +
    + +
    + +
    + + +
    +
    +
    +
    +
    + ); +}; + +export default ReviewCard; diff --git a/src/components/ui/cards/TravelStoryCard.tsx b/src/components/ui/cards/TravelStoryCard.tsx new file mode 100644 index 0000000..1d594fc --- /dev/null +++ b/src/components/ui/cards/TravelStoryCard.tsx @@ -0,0 +1,32 @@ +"use client"; + +import Card from "./context/CardProvider"; +import { ITravelStory } from "@/lib/types/travel"; +import React from "react"; + +interface Props { + story: ITravelStory; +} + +const TravelStoryCard = ({ story }: Props) => { + return ( +
    + + +
    + + +
    + +
    + + +
    +
    +
    +
    +
    + ); +}; + +export default TravelStoryCard; diff --git a/src/components/ui/cards/context/CardImage.tsx b/src/components/ui/cards/context/CardImage.tsx index d483cde..c0b7e45 100644 --- a/src/components/ui/cards/context/CardImage.tsx +++ b/src/components/ui/cards/context/CardImage.tsx @@ -11,11 +11,11 @@ const CardImage = React.forwardRef< return (
    Ponyo
    diff --git a/src/components/ui/cards/context/CardProvider.tsx b/src/components/ui/cards/context/CardProvider.tsx index a20f2cb..ec5df3b 100644 --- a/src/components/ui/cards/context/CardProvider.tsx +++ b/src/components/ui/cards/context/CardProvider.tsx @@ -12,8 +12,7 @@ interface ICard { isLike?: boolean; likeCount?: number; commentCount: number; - isStar?: boolean; - starCount?: number; + score?: number; imageSrc?: string; tagList?: string[]; title: string; diff --git a/src/components/ui/cards/context/CardTags.tsx b/src/components/ui/cards/context/CardTags.tsx index 4799bc4..cf26837 100644 --- a/src/components/ui/cards/context/CardTags.tsx +++ b/src/components/ui/cards/context/CardTags.tsx @@ -16,8 +16,10 @@ const CardTags = React.forwardRef< {...props} > {tagList?.map(tag => ( - - {tag} + +

    + {tag} +

    ))}
    diff --git a/src/components/ui/cards/context/CardTitle.tsx b/src/components/ui/cards/context/CardTitle.tsx index bf77494..ade2e70 100644 --- a/src/components/ui/cards/context/CardTitle.tsx +++ b/src/components/ui/cards/context/CardTitle.tsx @@ -8,13 +8,15 @@ const CardTitle = React.forwardRef< >(({ className, ...props }, ref) => { const { title, description } = useCardContext(); return ( -
    -

    {title}

    - {description &&

    {description}

    } +
    +

    + {title} +

    + {description && ( +

    + {description} +

    + )}
    ); }); diff --git a/src/components/ui/cards/context/CommentButton.tsx b/src/components/ui/cards/context/CommentButton.tsx index 2079f07..4f41d28 100644 --- a/src/components/ui/cards/context/CommentButton.tsx +++ b/src/components/ui/cards/context/CommentButton.tsx @@ -1,5 +1,5 @@ import { Button } from "../../Button"; -import { CircleIcon } from "lucide-react"; +import CommentIcon from "@/components/icons/CommentIcon"; import React from "react"; import { cn } from "@/lib/utils/cn"; import { useCardContext } from "./CardProvider"; @@ -14,12 +14,15 @@ const CommentButton = React.forwardRef< ref={ref} size="icon" variant="ghost" - className={cn("flex items-center gap-2 w-6 h-6", className)} + className={cn( + "flex items-center align-baseline gap-1 text-[#b8b8b8] text-xs", + className + )} onClick={() => {}} {...props} > - - {commentCount} + + {commentCount} ); }); diff --git a/src/components/ui/cards/context/LikeButton.tsx b/src/components/ui/cards/context/LikeButton.tsx index 5af8c19..79e544a 100644 --- a/src/components/ui/cards/context/LikeButton.tsx +++ b/src/components/ui/cards/context/LikeButton.tsx @@ -1,7 +1,7 @@ -import { Heart, HeartOff } from "lucide-react"; import React, { useState } from "react"; import { Button } from "../../Button"; +import HeartIcon from "@/components/icons/HeartIcon"; import { cn } from "@/lib/utils/cn"; import { useCardContext } from "./CardProvider"; @@ -18,12 +18,18 @@ const LikeButton = React.forwardRef< ref={ref} size="icon" variant="ghost" - className={cn("flex items-center gap-2", className)} + className={cn("flex items-center gap-1", className)} onClick={() => setLike(!like)} {...props} > - {isLike ? : } - {like ? likeCount! + 1 : likeCount} + + + {like ? likeCount! + 1 : likeCount} + ); }); diff --git a/src/components/ui/cards/context/StarButton.tsx b/src/components/ui/cards/context/StarButton.tsx index 42af4b0..aa42948 100644 --- a/src/components/ui/cards/context/StarButton.tsx +++ b/src/components/ui/cards/context/StarButton.tsx @@ -1,7 +1,6 @@ -import React, { useState } from "react"; -import { Star, StarOff } from "lucide-react"; - import { Button } from "../../Button"; +import React from "react"; +import StarIcon from "@/components/icons/StartIcon"; import { cn } from "@/lib/utils/cn"; import { useCardContext } from "./CardProvider"; @@ -9,8 +8,7 @@ const StarButton = React.forwardRef< HTMLButtonElement, React.HTMLAttributes >(({ className, ...props }, ref) => { - const { isStar, starCount } = useCardContext(); - const [star, setStar] = useState(false); + const { score } = useCardContext(); return ( // [todo]: change click event to update like Api @@ -18,14 +16,11 @@ const StarButton = React.forwardRef< ref={ref} size="icon" variant="ghost" - className={cn("flex items-center gap-2 w-6 h-6", className)} - onClick={() => { - setStar(!star); - }} + className={cn("flex items-center gap-1", className)} {...props} > - {isStar ? : } - {star ? starCount! + 1 : starCount} + + {score} ); }); diff --git a/src/components/ui/cards/context/UserInfo.tsx b/src/components/ui/cards/context/UserInfo.tsx index bd2fc38..cd2adb3 100644 --- a/src/components/ui/cards/context/UserInfo.tsx +++ b/src/components/ui/cards/context/UserInfo.tsx @@ -16,10 +16,11 @@ const UserInfo = React.forwardRef< {...props} > - + + {/* [TODO]: change userName -> loading image */} {userName} -
    {userName}
    +
    {userName}
    ); }); diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 0000000..bf9ea07 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,6 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + const { server } = await import("./mocks/node"); + server.listen(); + } +} diff --git a/src/lib/api/place.ts b/src/lib/api/place.ts new file mode 100644 index 0000000..543e02a --- /dev/null +++ b/src/lib/api/place.ts @@ -0,0 +1,26 @@ +import { GetPlaceDTO, ResponsePlace } from "../types/place"; + +import QueryString from "qs"; + +export const PlaceAPI = { + getPlace: async (dto: GetPlaceDTO): Promise => { + const data = await ( + await fetch( + process.env.API_HOST + `/places?${QueryString.stringify(dto)}`, + { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + next: { revalidate: 600 }, + } + ) + ).json(); + + return { + data: data.data, + pageInfo: data.pageInfo, + }; + }, +}; diff --git a/src/lib/styles/globals.css b/src/lib/styles/globals.css index 8abdb15..1b2d2d7 100644 --- a/src/lib/styles/globals.css +++ b/src/lib/styles/globals.css @@ -74,3 +74,5 @@ @apply bg-background text-foreground; } } + +@layer; diff --git a/src/lib/types/place.ts b/src/lib/types/place.ts new file mode 100644 index 0000000..58eb7a5 --- /dev/null +++ b/src/lib/types/place.ts @@ -0,0 +1,24 @@ +export interface GetPlaceDTO { + page: number; + placeType?: "CITY" | "TRAVEL" | "THEME"; + searchString?: string; + elementCnt?: number; +} + +export interface ResponsePlace { + pageInfo: { + page: number; + hasNext: number; + hasPrevious: number; + totalElements: number; + totalPages: number; + }; + data: Place[]; +} + +export interface Place { + id: number; + placeId: string; + name: string; + subName: string; +} diff --git a/src/lib/types/travel.ts b/src/lib/types/travel.ts new file mode 100644 index 0000000..641cbd1 --- /dev/null +++ b/src/lib/types/travel.ts @@ -0,0 +1,44 @@ +export interface IResponseTravelStory { + pageInfo: { + page: number; + hasNext: number; + hasPrevious: number; + totalElements: number; + totalPages: number; + }; + data: ITravelStory[]; +} + +export interface ITravelStory { + id: number; + isLike: boolean; + likeCount: number; + commentCount: number; + imageSrc: string; + title: string; + tagList: string[]; + userName: string; + userImageSrc: string; +} + +export interface IResponseReview { + pageInfo: { + page: number; + hasNext: number; + hasPrevious: number; + totalElements: number; + totalPages: number; + }; + data: IReview[]; +} + +export interface IReview { + id: number; + score: number; + commentCount: number; + imageSrc: string; + title: string; + description: string; + userName: string; + userImageSrc: string; +} diff --git a/src/mocks/MockProvider.tsx b/src/mocks/MockProvider.tsx new file mode 100644 index 0000000..f0daef4 --- /dev/null +++ b/src/mocks/MockProvider.tsx @@ -0,0 +1,17 @@ +"use client"; + +let triggered = false; + +async function enableApiMocking() { + const { worker } = await import("../mocks/browser"); + await worker.start(); +} + +export function MockProvider() { + if (!triggered) { + triggered = true; + throw enableApiMocking(); + } + + return null; +} diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 0000000..24f87fb --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { handlers } from "./handlers"; +import { setupWorker } from "msw/browser"; + +export const worker = setupWorker(...handlers); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000..73569fc --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,5 @@ +import { getPlaces } from "./handlers/place"; +import { getReviews } from "./handlers/review"; +import { getTravelStories } from "./handlers/travelStory"; + +export const handlers = [getPlaces, getTravelStories, getReviews]; diff --git a/src/mocks/handlers/place.ts b/src/mocks/handlers/place.ts new file mode 100644 index 0000000..3000574 --- /dev/null +++ b/src/mocks/handlers/place.ts @@ -0,0 +1,11 @@ +import { HttpResponse, http } from "msw"; + +import { ResponsePlace } from "@/lib/types/place"; +import { placeResponse } from "../response/place"; + +export const getPlaces = http.get( + `${process.env.API_HOST}/place`, + () => { + return HttpResponse.json(placeResponse); + } +); diff --git a/src/mocks/handlers/review.ts b/src/mocks/handlers/review.ts new file mode 100644 index 0000000..7759ca1 --- /dev/null +++ b/src/mocks/handlers/review.ts @@ -0,0 +1,7 @@ +import { HttpResponse, http } from "msw"; + +import { reviewResponse } from "../response/review"; + +export const getReviews = http.get(`${process.env.API_HOST}/review`, () => { + return HttpResponse.json(reviewResponse); +}); diff --git a/src/mocks/handlers/travelStory.ts b/src/mocks/handlers/travelStory.ts new file mode 100644 index 0000000..c97cc90 --- /dev/null +++ b/src/mocks/handlers/travelStory.ts @@ -0,0 +1,10 @@ +import { HttpResponse, http } from "msw"; + +import { travelStoryResponse } from "../response/travelStory"; + +export const getTravelStories = http.get( + `${process.env.API_HOST}/story`, + () => { + return HttpResponse.json(travelStoryResponse); + } +); diff --git a/src/mocks/node.ts b/src/mocks/node.ts new file mode 100644 index 0000000..ec2a01c --- /dev/null +++ b/src/mocks/node.ts @@ -0,0 +1,4 @@ +import { handlers } from "./handlers"; +import { setupServer } from "msw/node"; + +export const server = setupServer(...handlers); diff --git a/src/mocks/response/place.ts b/src/mocks/response/place.ts new file mode 100644 index 0000000..6b11b99 --- /dev/null +++ b/src/mocks/response/place.ts @@ -0,0 +1,25 @@ +import { ResponsePlace } from "@/lib/types/place"; + +export const placeResponse: ResponsePlace = { + pageInfo: { + page: 1, + hasNext: 1, + hasPrevious: 0, + totalElements: 2, + totalPages: 1, + }, + data: [ + { + id: 1, + placeId: "hdaasdasdasdasas", + name: "서울", + subName: "서울", + }, + { + id: 2, + placeId: "asdasfasfasfas", + name: "대전", + subName: "대전", + }, + ], +}; diff --git a/src/mocks/response/review.ts b/src/mocks/response/review.ts new file mode 100644 index 0000000..c626e00 --- /dev/null +++ b/src/mocks/response/review.ts @@ -0,0 +1,53 @@ +import { IResponseReview } from "@/lib/types/travel"; + +export const reviewResponse: IResponseReview = { + pageInfo: { + page: 1, + hasNext: 1, + hasPrevious: 0, + totalElements: 4, + totalPages: 1, + }, + data: [ + { + id: 1, + score: 3.5, + commentCount: 10, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "리뷰 1", + description: "리뷰 1의 설명", + userName: "yun", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 2, + score: 4, + commentCount: 23, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "리뷰 2", + description: "리뷰 2의 설명", + userName: "yn", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 3, + score: 2.5, + commentCount: 5, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "리뷰 3", + description: "리뷰 3의 설명", + userName: "ㅎㅎㅋㅋ", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 4, + score: 1.5, + commentCount: 5, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "리뷰 4", + description: "리뷰 4의 설명", + userName: "nnnn", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + ], +}; diff --git a/src/mocks/response/travelStory.ts b/src/mocks/response/travelStory.ts new file mode 100644 index 0000000..e90e219 --- /dev/null +++ b/src/mocks/response/travelStory.ts @@ -0,0 +1,57 @@ +import { IResponseTravelStory } from "@/lib/types/travel"; + +export const travelStoryResponse: IResponseTravelStory = { + pageInfo: { + page: 1, + hasNext: 1, + hasPrevious: 0, + totalElements: 4, + totalPages: 1, + }, + data: [ + { + id: 1, + isLike: true, + likeCount: 120, + commentCount: 10, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "여행기 1", + tagList: ["#서울", "#대전"], + userName: "yun", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 2, + isLike: true, + likeCount: 15, + commentCount: 23, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "여행기 2", + tagList: ["#서울", "#대전"], + userName: "yn", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 3, + isLike: true, + likeCount: 1, + commentCount: 51, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "여행기 3", + tagList: ["#서울", "#대전"], + userName: "ㅎㅎㅋㅋ", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + { + id: 4, + isLike: true, + likeCount: 200, + commentCount: 52, + imageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + title: "여행기 4", + tagList: ["#서울", "#대전"], + userName: "nnnn", + userImageSrc: "https://www.ghibli.jp/gallery/ponyo016.jpg", + }, + ], +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 3ebba30..c1c7398 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -18,6 +18,9 @@ const config = { }, }, extend: { + spacing: { + 30: "7.5rem", + }, colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", @@ -58,6 +61,11 @@ const config = { md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, + boxShadow: { + input: "0px 4px 15px 0px rgba(26, 26, 26, 0.05)", + menuOpen: "4.1px -5px 0 0 rgb(17,24,39)", + menuClose: "-4.1px -5px 0 0 rgb(17,24,39)", + }, keyframes: { "accordion-down": { from: { height: "0" },