diff --git a/next.config.ts b/next.config.ts index 0ecd118..aeb8204 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,17 +2,6 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - // TurboPack 설정 - experimental: { - turbo: { - rules: { - '*.svg': { - loaders: ['@svgr/webpack'], - as: '*.js', - }, - }, - }, - }, // webpack 설정 webpack: (config) => { // @ts-expect-error 타입 에러 무시 @@ -42,6 +31,14 @@ const nextConfig: NextConfig = { fileLoaderRule.exclude = /\.svg$/i; return config; }, + turbopack: { + rules: { + '*.svg': { + loaders: ['@svgr/webpack'], + as: '*.js', + }, + } + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 0b1c61b..958ff80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "react-hot-toast": "^2.6.0" + "react-hot-toast": "^2.6.0", + "react-use": "^17.6.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1758,6 +1759,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -2499,7 +2509,6 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -3316,6 +3325,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3917,6 +3932,12 @@ "win32" ] }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==", + "license": "MIT" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4660,6 +4681,15 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/core-js-compat": { "version": "3.45.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", @@ -4716,6 +4746,15 @@ "node": ">= 8" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -5113,6 +5152,15 @@ "dev": true, "license": "MIT" }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -5750,7 +5798,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -5797,6 +5844,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -6230,6 +6288,12 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6267,6 +6331,15 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6748,6 +6821,12 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7452,6 +7531,54 @@ "dev": true, "license": "MIT" }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "license": "Unlicense", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nano-css/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/nano-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/nano-spawn": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.3.tgz", @@ -8095,6 +8222,41 @@ "dev": true, "license": "MIT" }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz", + "integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==", + "license": "Unlicense", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8210,6 +8372,12 @@ "node": ">=6" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8286,6 +8454,15 @@ "dev": true, "license": "MIT" }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8371,6 +8548,18 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -8418,6 +8607,15 @@ "node": ">= 0.4" } }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "license": "Unlicense", + "engines": { + "node": ">=6.9" + } + }, "node_modules/set-proto": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", @@ -8639,6 +8837,15 @@ "tslib": "^2.0.3" } }, + "node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8655,6 +8862,42 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8871,6 +9114,12 @@ } } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8980,6 +9229,15 @@ "node": ">=18" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -9041,6 +9299,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -9054,6 +9318,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==", + "license": "Unlicense" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 3458d02..d1469b9 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "react-hot-toast": "^2.6.0" + "react-hot-toast": "^2.6.0", + "react-use": "^17.6.0" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/api/index.ts b/src/api/index.ts deleted file mode 100644 index 344a3d3..0000000 --- a/src/api/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 서버에서 불러오는 것 - -// 공통불러오는 것 -// 페이지 불러오는 것 diff --git a/src/app/api/login/set-pre-login-path/route.ts b/src/app/api/login/set-pre-login-path/route.ts new file mode 100644 index 0000000..248d5eb --- /dev/null +++ b/src/app/api/login/set-pre-login-path/route.ts @@ -0,0 +1,18 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(req: NextRequest) { + const { path } = await req.json(); // 클라이언트가 보내는 페이지 경로 + const res = NextResponse.json({ ok: true }); + + res.cookies.set({ + name: 'preLoginPath', + value: path, + path: '/', + maxAge: 60 * 30, // 30분 + httpOnly: false, // JS에서 읽을 수 있게 + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + }); + + return res; +} diff --git a/src/app/community/[id]/page.tsx b/src/app/community/[id]/page.tsx new file mode 100644 index 0000000..0604213 --- /dev/null +++ b/src/app/community/[id]/page.tsx @@ -0,0 +1,29 @@ +import CommentList from '@/domains/community/detail/comment/CommentList'; +import DetailComment from '@/domains/community/detail/comment/DetailComment'; +import DetailContent from '@/domains/community/detail/DetailContent'; +import DetailHeader from '@/domains/community/detail/DetailHeader'; +import DetailTitle from '@/domains/community/detail/DetailTitle'; +import DetailTabDesktop from '@/domains/community/detail/tab/DetailTabDesktop'; +import StarBg from '@/domains/shared/starBg/StarBg'; + +function Page() { + return ( +
+ +
+ + + +
+ + +
+
+
+ +
+
+ ); +} + +export default Page; diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 84390d4..f22bf8d 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -1,37 +1,46 @@ -import CommunityFilter from '@/shared/components/community/CommunityFilter'; -import CommunityHeader from '@/shared/components/community/CommunityHeader'; -import CommunityTab from '@/shared/components/community/CommunityTab'; -import PostCard from '@/shared/components/community/PostCard'; -import WriteBtn from '@/shared/components/community/WriteBtn'; +import CommunityFilter from '@/domains/community/main/CommunityFilter'; +import CommunityTab from '@/domains/community/main/CommunityTab'; +import PostCard from '@/domains/community/main/PostCard'; +import WriteBtn from '@/domains/community/main/WriteBtn'; + +import PageHeader from '@/domains/shared/pageHeader/PageHeader'; +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '커뮤니티', + description: '칵테일에 관한 모든 이야기', +}; function Page() { return ( -
-
-
-

- 커뮤니티 페이지 -

- -
+
+ +
+
+
+

+ 커뮤니티 페이지 +

+
-
- - -
+
+ + +
-
- - - - - -
+
+ + + + + +
+
-
+ ); } export default Page; diff --git a/src/app/community/write/page.tsx b/src/app/community/write/page.tsx new file mode 100644 index 0000000..d52dcb0 --- /dev/null +++ b/src/app/community/write/page.tsx @@ -0,0 +1,5 @@ +function Page() { + return
; +} + +export default Page; diff --git a/src/app/design-system/page.tsx b/src/app/design-system/page.tsx index ad3e5cb..048b3f6 100644 --- a/src/app/design-system/page.tsx +++ b/src/app/design-system/page.tsx @@ -6,12 +6,13 @@ import Input from '@/shared/components/InputBox/Input'; import { useState } from 'react'; import { customToast } from '@/shared/components/toast/CustomToastUtils'; import ModalLayout from '@/shared/components/modalPop/ModalLayout'; -import ConfirmPop from '@/shared/components/modalPop/ConfirmPop'; -import SelectBox from '@/shared/components/InputBox/SelectBox'; -import LikeBtn from '@/shared/components/like/LikeBtn'; -import Share from '@/shared/components/share/Share'; -import Keep from '@/shared/components/keep/Keep'; +import SelectBox from '@/domains/shared/select-box/SelectBox'; + import Spinner from '@/shared/components/spinner/Spinner'; +import LikeBtn from '@/domains/community/components/like/LikeBtn'; +import Share from '@/domains/shared/share/Share'; +import Keep from '@/domains/shared/keep/Keep'; +import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; function Page() { const [isModalOpen, setModalOpen] = useState(false); @@ -128,7 +129,7 @@ function Page() {
모달팝업 내용
- setConfirmOpen(false)} title="Confirm제목" @@ -141,12 +142,12 @@ function Page() {

Icons

like

- +

Share

- - + +

keep

diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 16084e3..cef553c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,10 +18,7 @@ export default function RootLayout({
-
-
- {children} -
+
{children}
diff --git a/src/app/login/first-user/page.tsx b/src/app/login/first-user/page.tsx deleted file mode 100644 index 7f902a4..0000000 --- a/src/app/login/first-user/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler'; - -function Page() { - return ; -} - -export default Page; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index d1e4fb8..d2548bb 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import type { Metadata } from 'next'; import loginBg from '@/shared/assets/images/login_bg.webp'; -import SocialLogin from './SocialLogin'; +import SocialLogin from '@/domains/login/main/SocialLogin'; export const metadata: Metadata = { title: 'SSOUL | 로그인', diff --git a/src/app/login/success/page.tsx b/src/app/login/success/page.tsx index 15766eb..48cef7d 100644 --- a/src/app/login/success/page.tsx +++ b/src/app/login/success/page.tsx @@ -1,4 +1,4 @@ -import LoginRedirectHandler from '@/shared/components/auth/LoginRedirectHandler'; +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; function Page() { return ; diff --git a/src/app/login/user/first-user/page.tsx b/src/app/login/user/first-user/page.tsx new file mode 100644 index 0000000..131a5e1 --- /dev/null +++ b/src/app/login/user/first-user/page.tsx @@ -0,0 +1,7 @@ +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; + +function Page() { + return ; +} + +export default Page; diff --git a/src/app/login/user/success/page.tsx b/src/app/login/user/success/page.tsx new file mode 100644 index 0000000..48cef7d --- /dev/null +++ b/src/app/login/user/success/page.tsx @@ -0,0 +1,6 @@ +import LoginRedirectHandler from '@/domains/login/components/LoginRedirectHandler'; + +function Page() { + return ; +} +export default Page; diff --git a/src/app/recipe/[id]/page.tsx b/src/app/recipe/[id]/page.tsx index 9d89ac4..8eec717 100644 --- a/src/app/recipe/[id]/page.tsx +++ b/src/app/recipe/[id]/page.tsx @@ -1,9 +1,11 @@ -import StarBg from '@/shared/components/starBg/StarBg'; +import DetailMain from '@/domains/recipe/details/DetailMain'; +import StarBg from '@/domains/shared/starBg/StarBg'; function page() { return ( -
- +
+ +
); } diff --git a/src/app/recipe/page.tsx b/src/app/recipe/page.tsx index 9f5e417..e2383b7 100644 --- a/src/app/recipe/page.tsx +++ b/src/app/recipe/page.tsx @@ -1,10 +1,10 @@ -import PageHeader from '@/shared/components/pageHeader/PageHeader'; import { Metadata } from 'next'; -import Glass from '@/shared/assets/images/recipe_page_header.webp'; -import SelectBox from '@/shared/components/InputBox/SelectBox'; +import SelectBox from '@/domains/shared/select-box/SelectBox'; import Input from '@/shared/components/InputBox/Input'; -import CocktailList from '@/shared/components/recipePage/cocktailList/CocktailList'; -import Accordion from './components/Accordion'; + +import Accordion from '../../domains/recipe/components/main/Accordion'; +import CocktailList from '@/domains/recipe/CocktailList'; +import PageHeader from '@/domains/shared/pageHeader/PageHeader'; export const metadata: Metadata = { title: 'SSOUL | 칵테일레시피', @@ -15,11 +15,7 @@ function Page() { return (
- +
diff --git a/src/app/recommend/page.tsx b/src/app/recommend/page.tsx index e8575e9..ee68e29 100644 --- a/src/app/recommend/page.tsx +++ b/src/app/recommend/page.tsx @@ -1,7 +1,7 @@ +import ChatForm from '@/domains/recommend/components/ChatForm'; +import MyChat from '@/domains/recommend/components/MyChat'; +import SsuryChat from '@/domains/recommend/components/SsuryChat'; import Bg from '@/shared/assets/images/recommend_bg.webp'; -import ChatForm from './components/ChatForm'; -import SsuryChat from './components/SsuryChat'; -import MyChat from './components/MyChat'; function Page() { return ( diff --git a/src/shared/components/community/CommunityFilter.tsx b/src/domains/community/CommunityFilter.tsx similarity index 87% rename from src/shared/components/community/CommunityFilter.tsx rename to src/domains/community/CommunityFilter.tsx index c8deedd..fa7b129 100644 --- a/src/shared/components/community/CommunityFilter.tsx +++ b/src/domains/community/CommunityFilter.tsx @@ -1,6 +1,7 @@ 'use client'; -import SelectBox from '../InputBox/SelectBox'; +import SelectBox from '../shared/select-box/SelectBox'; + function CommunityFilter() { return (
+ +
+ ); +} + +export default CommunityHeader; diff --git a/src/shared/components/community/CommunityTab.tsx b/src/domains/community/CommunityTab.tsx similarity index 100% rename from src/shared/components/community/CommunityTab.tsx rename to src/domains/community/CommunityTab.tsx diff --git a/src/shared/components/community/PostCard.tsx b/src/domains/community/PostCard.tsx similarity index 67% rename from src/shared/components/community/PostCard.tsx rename to src/domains/community/PostCard.tsx index cc672e8..2514a24 100644 --- a/src/shared/components/community/PostCard.tsx +++ b/src/domains/community/PostCard.tsx @@ -1,11 +1,13 @@ import Image from 'next/image'; import prePost from '@/shared/assets/images/prepost_img.webp'; -import PostLabel from './PostLabel'; + +import PostInfo from './PostInfo'; +import Label from '../shared/label/Label'; function PostCard({ label }: { label: string }) { return (
- +
+ {hasUserName && ( + <> +
  • 실버븬
  • + + + )} +
  • 3분 전
  • + +
  • 조회 3
  • + +
  • 댓글 3
  • + + ); +} + +export default PostInfo; diff --git a/src/domains/community/WriteBtn.tsx b/src/domains/community/WriteBtn.tsx new file mode 100644 index 0000000..f6a8752 --- /dev/null +++ b/src/domains/community/WriteBtn.tsx @@ -0,0 +1,27 @@ +'use client'; + +import Write from '@/shared/assets/icons/edit_28.svg'; +import { useRouter } from 'next/navigation'; + +type RouterType = ReturnType; + +function WriteBtn() { + const router = useRouter(); + + const handleClick = (router: RouterType) => { + router.push('/community/write'); + }; + + return ( + + ); +} + +export default WriteBtn; diff --git a/src/domains/community/components/comment/CommentBtn.tsx b/src/domains/community/components/comment/CommentBtn.tsx new file mode 100644 index 0000000..b3f497f --- /dev/null +++ b/src/domains/community/components/comment/CommentBtn.tsx @@ -0,0 +1,25 @@ +import CommentIcon from '@/shared/assets/icons/comment_28.svg'; +import { useState } from 'react'; + +function CommentBtn({ size }: { size: 'sm' | 'md' }) { + const [isClick, setIsClick] = useState(false); + + const handleClick = () => { + setIsClick(!isClick); + }; + return ( + + ); +} +export default CommentBtn; diff --git a/src/shared/components/like/LikeBtn.tsx b/src/domains/community/components/like/LikeBtn.tsx similarity index 76% rename from src/shared/components/like/LikeBtn.tsx rename to src/domains/community/components/like/LikeBtn.tsx index 48cbf5c..0ba3be4 100644 --- a/src/shared/components/like/LikeBtn.tsx +++ b/src/domains/community/components/like/LikeBtn.tsx @@ -1,7 +1,7 @@ import LikeIcon from '@/shared/assets/icons/like_28.svg'; import { useState } from 'react'; -function LikeBtn() { +function LikeBtn({ size }: { size: 'sm' | 'md' }) { const [isClick, setIsClick] = useState(false); const handleClick = () => { @@ -10,7 +10,7 @@ function LikeBtn() { return (
    + ); +} + +export default CocktailTag; diff --git a/src/domains/community/detail/DetailContent.tsx b/src/domains/community/detail/DetailContent.tsx new file mode 100644 index 0000000..bcf5f65 --- /dev/null +++ b/src/domains/community/detail/DetailContent.tsx @@ -0,0 +1,46 @@ +import Image from 'next/image'; +import prePost from '@/shared/assets/images/prepost_img.webp'; + +import PostInfo from '../main/PostInfo'; + +import CocktailTag from './CocktailTag'; +import DetailTabMobile from './tab/DetailTabMobile'; + +function DetailContent() { + return ( +
    +
    + 더미 이미지 +
    +
    +

    내용은 이거입니다 하하하

    +

    그런가요 안녕하세요

    +
    +

    글입니다다다다다다다다다다다다

    +

    내용은 이거입니다 하하하

    +

    그런가요 안녕하세요

    +
    +

    글입니다다다다다다다다다다다다

    +

    내용은 이거입니다 하하하

    +

    그런가요 안녕하세요

    +
    +

    글입니다다다다다다다다다다다다

    +

    내용은 이거입니다 하하하

    +

    그런가요 안녕하세요

    +
    +

    글입니다다다다다다다다다다다다

    +

    내용은 이거입니다 하하하

    +

    그런가요 안녕하세요

    +
    +

    글입니다다다다다다다다다다다다

    +
    + + +
    + +
    +
    + ); +} + +export default DetailContent; diff --git a/src/domains/community/detail/DetailHeader.tsx b/src/domains/community/detail/DetailHeader.tsx new file mode 100644 index 0000000..9eb4300 --- /dev/null +++ b/src/domains/community/detail/DetailHeader.tsx @@ -0,0 +1,13 @@ +import Label from '@/domains/shared/label/Label'; +import EditDelete from './EditDelete'; + +function DetailHeader() { + return ( +
    +
    + ); +} + +export default DetailHeader; diff --git a/src/domains/community/detail/DetailTitle.tsx b/src/domains/community/detail/DetailTitle.tsx new file mode 100644 index 0000000..1850aff --- /dev/null +++ b/src/domains/community/detail/DetailTitle.tsx @@ -0,0 +1,12 @@ +import Profile from './Profile'; + +function DetailTitle() { + return ( +
    +

    칵테일 만들 때 준비물

    + +
    + ); +} + +export default DetailTitle; diff --git a/src/domains/community/detail/EditDelete.tsx b/src/domains/community/detail/EditDelete.tsx new file mode 100644 index 0000000..a07aec5 --- /dev/null +++ b/src/domains/community/detail/EditDelete.tsx @@ -0,0 +1,25 @@ +function EditDelete({ use }: { use: 'post' | 'comment' }) { + return ( +
    + + + +
    + ); +} + +export default EditDelete; diff --git a/src/domains/community/detail/Profile.tsx b/src/domains/community/detail/Profile.tsx new file mode 100644 index 0000000..6a07849 --- /dev/null +++ b/src/domains/community/detail/Profile.tsx @@ -0,0 +1,12 @@ +function Profile() { + return ( +
    +
    + + 실버븬 + +
    + ); +} + +export default Profile; diff --git a/src/domains/community/detail/comment/CommentHeader.tsx b/src/domains/community/detail/comment/CommentHeader.tsx new file mode 100644 index 0000000..822736a --- /dev/null +++ b/src/domains/community/detail/comment/CommentHeader.tsx @@ -0,0 +1,17 @@ +import EditDelete from '../EditDelete'; +import Profile from '../Profile'; + +function CommentHeader() { + return ( +
    +
    + + | +

    3분 전

    +
    + +
    + ); +} + +export default CommentHeader; diff --git a/src/domains/community/detail/comment/CommentList.tsx b/src/domains/community/detail/comment/CommentList.tsx new file mode 100644 index 0000000..137e2ec --- /dev/null +++ b/src/domains/community/detail/comment/CommentList.tsx @@ -0,0 +1,26 @@ +import CommentHeader from './CommentHeader'; + +function CommentList() { + return ( +
      +
    • +
      + +
      +

      정말 대단하시네요

      +
      +
      +
    • +
    • +
      + +
      +

      정말 대단하시네요

      +
      +
      +
    • +
    + ); +} + +export default CommentList; diff --git a/src/domains/community/detail/comment/DetailComment.tsx b/src/domains/community/detail/comment/DetailComment.tsx new file mode 100644 index 0000000..1f747df --- /dev/null +++ b/src/domains/community/detail/comment/DetailComment.tsx @@ -0,0 +1,21 @@ +import Button from '@/shared/components/button/Button'; +import Input from '@/shared/components/InputBox/Input'; + +function DetailComment() { + return ( +
    +
    + + +
    +
    + ); +} + +export default DetailComment; diff --git a/src/domains/community/detail/tab/DetailTabDesktop.tsx b/src/domains/community/detail/tab/DetailTabDesktop.tsx new file mode 100644 index 0000000..d0ccdfa --- /dev/null +++ b/src/domains/community/detail/tab/DetailTabDesktop.tsx @@ -0,0 +1,32 @@ +'use client'; + +import Share from '@/domains/shared/share/Share'; +import CommentBtn from '../../components/comment/CommentBtn'; +import LikeBtn from '../../components/like/LikeBtn'; + +function DetailTabDesktop() { + return ( +
    +
    +
    +
    + + 2 +
    +
    + + 2 +
    +
    + +
    +
    +
    +
    + ); +} + +export default DetailTabDesktop; diff --git a/src/domains/community/detail/tab/DetailTabMobile.tsx b/src/domains/community/detail/tab/DetailTabMobile.tsx new file mode 100644 index 0000000..042413d --- /dev/null +++ b/src/domains/community/detail/tab/DetailTabMobile.tsx @@ -0,0 +1,25 @@ +'use client'; + +import Share from '@/domains/shared/share/Share'; +import LikeBtn from '../../components/like/LikeBtn'; + +function DetailTabMobile() { + return ( +
    +
    +
    + + 2 +
    +
    + +
    +
    +
    + ); +} + +export default DetailTabMobile; diff --git a/src/domains/community/main/CommunityFilter.tsx b/src/domains/community/main/CommunityFilter.tsx new file mode 100644 index 0000000..8c33c81 --- /dev/null +++ b/src/domains/community/main/CommunityFilter.tsx @@ -0,0 +1,17 @@ +'use client'; + +import SelectBox from '../../shared/select-box/SelectBox'; + +function CommunityFilter() { + return ( +
    +

    100개

    + +
    + ); +} + +export default CommunityFilter; diff --git a/src/domains/community/main/CommunityHeader.tsx b/src/domains/community/main/CommunityHeader.tsx new file mode 100644 index 0000000..9ef4ab6 --- /dev/null +++ b/src/domains/community/main/CommunityHeader.tsx @@ -0,0 +1,11 @@ +import PageHeader from '../../shared/pageHeader/PageHeader'; + +function CommunityHeader() { + return ( +
    + +
    + ); +} + +export default CommunityHeader; diff --git a/src/domains/community/main/CommunityTab.tsx b/src/domains/community/main/CommunityTab.tsx new file mode 100644 index 0000000..06ea6ca --- /dev/null +++ b/src/domains/community/main/CommunityTab.tsx @@ -0,0 +1,42 @@ +'use client'; + +import tw from '@/shared/utills/tw'; +import { useState } from 'react'; + +const tabItem = [ + { title: '전체' }, + { title: '레시피' }, + { title: '팁' }, + { title: '질문' }, + { title: '자유' }, +]; + +function CommunityTab() { + const [selectedIdx, setSelectedIdx] = useState(0); + + return ( +
    +
    +
    + {tabItem.map(({ title }, idx) => ( + + ))} +
    +
    +
    + ); +} + +export default CommunityTab; diff --git a/src/domains/community/main/PostCard.tsx b/src/domains/community/main/PostCard.tsx new file mode 100644 index 0000000..c08cbc1 --- /dev/null +++ b/src/domains/community/main/PostCard.tsx @@ -0,0 +1,35 @@ +import Image from 'next/image'; +import prePost from '@/shared/assets/images/prepost_img.webp'; + +import PostInfo from './PostInfo'; +import Label from '../../shared/label/Label'; + +function PostCard({ label }: { label: string }) { + return ( +
    +
    + ); +} + +export default PostCard; diff --git a/src/domains/community/main/PostInfo.tsx b/src/domains/community/main/PostInfo.tsx new file mode 100644 index 0000000..80d7243 --- /dev/null +++ b/src/domains/community/main/PostInfo.tsx @@ -0,0 +1,22 @@ +function PostInfo({ hasUserName = false }: { hasUserName?: boolean }) { + return ( +
      + {hasUserName && ( + <> +
    • 실버븬
    • + + + )} +
    • 3분 전
    • + +
    • 조회 3
    • + +
    • 댓글 3
    • +
    + ); +} + +export default PostInfo; diff --git a/src/domains/community/main/WriteBtn.tsx b/src/domains/community/main/WriteBtn.tsx new file mode 100644 index 0000000..6b60af7 --- /dev/null +++ b/src/domains/community/main/WriteBtn.tsx @@ -0,0 +1,30 @@ +'use client'; + +import Write from '@/shared/assets/icons/edit_28.svg'; +import { useRouter } from 'next/navigation'; +import Button from '@/shared/components/button/Button'; + +type RouterType = ReturnType; + +function WriteBtn() { + const router = useRouter(); + + const handleClick = (router: RouterType) => { + router.push('/community/write'); + }; + + return ( + + ); +} + +export default WriteBtn; diff --git a/src/domains/login/components/LoginRedirectHandler.tsx b/src/domains/login/components/LoginRedirectHandler.tsx new file mode 100644 index 0000000..79d03b1 --- /dev/null +++ b/src/domains/login/components/LoginRedirectHandler.tsx @@ -0,0 +1,87 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; +import { customToast } from '@/shared/components/toast/CustomToastUtils'; +import { getCookie, removeCookie } from '@/domains/shared/auth/utils/cookie'; +import { useAuthStore } from '@/domains/shared/store/auth'; +import Spinner from '@/shared/components/spinner/Spinner'; +import WelcomeModal from '@/domains/login/components/WelcomeModal'; + +function LoginRedirectHandler() { + const pathname = usePathname(); + const router = useRouter(); + const { user, updateUser } = useAuthStore(); + const [loading, setLoading] = useState(true); + const [welcomeModalOpen, setWelcomeModalOpen] = useState(false); + + useEffect(() => { + if (!user && loading) { + updateUser() + .then((fetchedUser) => { + if (!fetchedUser) { + router.replace('/login'); + } + }) + .catch(() => { + router.replace('/login'); + }) + .finally(() => setLoading(false)); + } else { + setLoading(false); + } + }, [user, loading, updateUser, router]); + + useEffect(() => { + if (!user || loading) return; + + const preLoginPath = getCookie('preLoginPath') || '/'; + // 로그인 상태인데 이전 페이지가 /login이면 메인으로 이동 + if (user && preLoginPath === '/login') { + router.replace('/'); + removeCookie('preLoginPath'); + return; + } + + // 첫 유저일 경우 모달 오픈 + if (pathname.startsWith('/login/user/first-user')) { + setWelcomeModalOpen(true); + } + // 기존 유저일 경우 + else if (pathname.startsWith('/login/user/success')) { + customToast.success(`${user.nickname}님 \n 로그인 성공 🎉`); + router.replace(preLoginPath); + removeCookie('preLoginPath'); + } + }, [pathname, user, loading, router]); + + // 환영 모달 닫힐 때 이동 + const handleCloseWelcomeModal = () => { + setWelcomeModalOpen(false); + const preLoginPath = getCookie('preLoginPath') || '/'; + removeCookie('preLoginPath'); + router.replace(preLoginPath); + }; + + if (loading) { + return ( +
    + +
    + ); + } + + return ( + <> + {/* 첫 유저 모달 */} + {user && ( + + )} + + ); +} +export default LoginRedirectHandler; diff --git a/src/domains/login/components/LogoutConfirm.tsx b/src/domains/login/components/LogoutConfirm.tsx new file mode 100644 index 0000000..b7f5f26 --- /dev/null +++ b/src/domains/login/components/LogoutConfirm.tsx @@ -0,0 +1,20 @@ +import ConfirmModal from '@/shared/components/modalPop/ConfirmModal'; + +interface Props { + open: boolean; + onClose: () => void; + onLogout: () => void; +} + +function LogoutConfirm({ open, onClose, onLogout }: Props) { + return ( + + ); +} +export default LogoutConfirm; diff --git a/src/shared/components/auth/Welcome.tsx b/src/domains/login/components/WelcomeModal.tsx similarity index 71% rename from src/shared/components/auth/Welcome.tsx rename to src/domains/login/components/WelcomeModal.tsx index b479480..f4cd3b1 100644 --- a/src/shared/components/auth/Welcome.tsx +++ b/src/domains/login/components/WelcomeModal.tsx @@ -6,21 +6,21 @@ import Button from '@/shared/components/button/Button'; import ModalLayout from '@/shared/components/modalPop/ModalLayout'; import Ssury from '@/shared/assets/ssury/ssury_jump.webp'; import { useRouter } from 'next/navigation'; -import { useModalStore } from '@/shared/@store/modal'; -import { useAuthStore } from '@/shared/@store/auth'; -function Welcome() { - const router = useRouter(); - const { user } = useAuthStore(); - const { welcomeModal, closeWelcomeModal } = useModalStore(); +interface Props { + userNickname: string; + open: boolean; + onClose: () => void; +} - if (!welcomeModal.open || !user) return null; +function Welcome({ userNickname, open, onClose }: Props) { + const router = useRouter(); return ( @@ -28,7 +28,7 @@ function Welcome() { type="button" color="purple" onClick={() => { - closeWelcomeModal(); + onClose(); router.push('/recipe'); }} > @@ -37,7 +37,7 @@ function Welcome() { + ); +} +export default BackBtn; diff --git a/src/app/recipe/components/Accordion.tsx b/src/domains/recipe/components/main/Accordion.tsx similarity index 92% rename from src/app/recipe/components/Accordion.tsx rename to src/domains/recipe/components/main/Accordion.tsx index aff8c14..a1a71e2 100644 --- a/src/app/recipe/components/Accordion.tsx +++ b/src/domains/recipe/components/main/Accordion.tsx @@ -1,6 +1,6 @@ 'use client'; -import SelectBox from '@/shared/components/InputBox/SelectBox'; +import SelectBox from '@/domains/shared/select-box/SelectBox'; const selectOption = [ { diff --git a/src/domains/recipe/details/DetailItem.tsx b/src/domains/recipe/details/DetailItem.tsx new file mode 100644 index 0000000..ab1b5fc --- /dev/null +++ b/src/domains/recipe/details/DetailItem.tsx @@ -0,0 +1,73 @@ +import Image from 'next/image'; +import Short from '@/shared/assets/icons/short_36.svg'; +import Example from '@/shared/assets/images/dummy/exampleCocktail.png'; +import Label from '@/domains/shared/label/Label'; + +function DetailItem() { + return ( +
    +
    +
    + + +

    + Old Fashioned +

    +

    + 올드 패션드 +

    +
    + +

    + 쿠바 아바나의 전설적인 바 엘 플로리디타(El Floridita).이곳에서 노벨문학상 작가 어니스트 + 헤밍웨이가 즐겨 찾던 특별한 한 잔이 탄생했습니다. +

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

    도수

    + | +
    +
    +

    24.8%

    +
    +
    +
    +
    +
    +
    +
    +

    글래스 타입

    + | +
    +
    + +

    숏 드링크

    +
    +
    +
    +
    + ); +} +export default DetailItem; diff --git a/src/domains/recipe/details/DetailMain.tsx b/src/domains/recipe/details/DetailMain.tsx new file mode 100644 index 0000000..ee30a44 --- /dev/null +++ b/src/domains/recipe/details/DetailMain.tsx @@ -0,0 +1,43 @@ +import DetailItem from './DetailItem'; +import DetailRecipe from './DetailRecipe'; +import DetailsHeader from './DetailsHeader'; +import SsuryShake from '@/shared/assets/ssury/ssury_make.webp'; +import SsuryDrink from '@/shared/assets/ssury/ssury_drink.webp'; +import Image from 'next/image'; + +function DetailMain() { + return ( +
    + + +
    + + + +
    + +
    +
    +
    + +

    레시피

    +
    +
    + +
    + +
    +
    +
    + +

    추천리스트

    +
    +
    + {/* 여기에 컴포넌트 */} +
    + +
    {/* 여기에 댓글 컴포넌트 */}
    +
    + ); +} +export default DetailMain; diff --git a/src/domains/recipe/details/DetailRecipe.tsx b/src/domains/recipe/details/DetailRecipe.tsx new file mode 100644 index 0000000..ed6decd --- /dev/null +++ b/src/domains/recipe/details/DetailRecipe.tsx @@ -0,0 +1,36 @@ +function DetailRecipe() { + return ( +
    +
    +

    재료

    +
      +
    • + 럼 1 1/2oz | 90ml +
    • +
    • + 심플시럽 1/2oz | 30ml +
    • +
    • + 라임 1/2개 +
    • +
    • + 자몽 1/2개 +
    • +
    +
    + + +
    +

    만드는 법

    +
      +
    1. 셰이커에 라임즙을 착즙기로 짜 넣습니다
    2. +
    3. 셰이커에 자몽즙을 착즙기로 짜 넣습니다
    4. +
    5. 셰이커에 재료를 넣습니다
    6. +
    7. 셰이킹 후 잔에 따라줍니다
    8. +
    +
    +
    +
    + ); +} +export default DetailRecipe; diff --git a/src/domains/recipe/details/DetailsHeader.tsx b/src/domains/recipe/details/DetailsHeader.tsx new file mode 100644 index 0000000..b983174 --- /dev/null +++ b/src/domains/recipe/details/DetailsHeader.tsx @@ -0,0 +1,16 @@ +import Share from '@/domains/shared/share/Share'; +import BackBtn from '../components/details/BackBtn'; +import Keep from '@/domains/shared/keep/Keep'; + +function DetailsHeader() { + return ( +
    + +
    + + +
    +
    + ); +} +export default DetailsHeader; diff --git a/src/shared/@store/accordionStore.ts b/src/domains/recipe/store/accordionStore.ts similarity index 100% rename from src/shared/@store/accordionStore.ts rename to src/domains/recipe/store/accordionStore.ts diff --git a/src/app/recommend/components/ChatCocktailCard.tsx b/src/domains/recommend/components/ChatCocktailCard.tsx similarity index 94% rename from src/app/recommend/components/ChatCocktailCard.tsx rename to src/domains/recommend/components/ChatCocktailCard.tsx index 19b6c4a..0af0862 100644 --- a/src/app/recommend/components/ChatCocktailCard.tsx +++ b/src/domains/recommend/components/ChatCocktailCard.tsx @@ -1,7 +1,7 @@ import Image from 'next/image'; import Dummy from '@/shared/assets/images/dummy/exampleCocktail.png'; import Link from 'next/link'; -import Keep from '@/shared/components/keep/Keep'; +import Keep from '@/domains/shared/keep/Keep'; function ChatCocktailCard() { return ( diff --git a/src/app/recommend/components/ChatForm.tsx b/src/domains/recommend/components/ChatForm.tsx similarity index 100% rename from src/app/recommend/components/ChatForm.tsx rename to src/domains/recommend/components/ChatForm.tsx diff --git a/src/app/recommend/components/ChatRadio.tsx b/src/domains/recommend/components/ChatRadio.tsx similarity index 100% rename from src/app/recommend/components/ChatRadio.tsx rename to src/domains/recommend/components/ChatRadio.tsx diff --git a/src/app/recommend/components/MyChat.tsx b/src/domains/recommend/components/MyChat.tsx similarity index 100% rename from src/app/recommend/components/MyChat.tsx rename to src/domains/recommend/components/MyChat.tsx diff --git a/src/app/recommend/components/SsuryChat.tsx b/src/domains/recommend/components/SsuryChat.tsx similarity index 100% rename from src/app/recommend/components/SsuryChat.tsx rename to src/domains/recommend/components/SsuryChat.tsx diff --git a/src/domains/shared/auth/utils/cookie.ts b/src/domains/shared/auth/utils/cookie.ts new file mode 100644 index 0000000..51a33ac --- /dev/null +++ b/src/domains/shared/auth/utils/cookie.ts @@ -0,0 +1,10 @@ +export function getCookie(name: string) { + if (typeof document === 'undefined') return null; + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; +} + +export function removeCookie(name: string) { + if (typeof document === 'undefined') return; + document.cookie = `${name}=; path=/; max-age=0`; +} diff --git a/src/domains/shared/auth/utils/setPreLoginPath.ts b/src/domains/shared/auth/utils/setPreLoginPath.ts new file mode 100644 index 0000000..5ca0bdd --- /dev/null +++ b/src/domains/shared/auth/utils/setPreLoginPath.ts @@ -0,0 +1,8 @@ +export async function setPreLoginPath(path: string) { + await fetch('/api/login/set-pre-login-path', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), // 인자로 받은 path 사용 + credentials: 'include', + }); +} diff --git a/src/shared/components/keep/Keep.tsx b/src/domains/shared/keep/Keep.tsx similarity index 98% rename from src/shared/components/keep/Keep.tsx rename to src/domains/shared/keep/Keep.tsx index fb7fd18..21352a3 100644 --- a/src/shared/components/keep/Keep.tsx +++ b/src/domains/shared/keep/Keep.tsx @@ -1,3 +1,4 @@ +'use client'; import KeepIcon from '@/shared/assets/icons/keep_36.svg'; import KeepIconActive from '@/shared/assets/icons/keep_active_36.svg'; import { useState } from 'react'; diff --git a/src/shared/components/community/PostLabel.tsx b/src/domains/shared/label/Label.tsx similarity index 76% rename from src/shared/components/community/PostLabel.tsx rename to src/domains/shared/label/Label.tsx index c838e52..e89be9e 100644 --- a/src/shared/components/community/PostLabel.tsx +++ b/src/domains/shared/label/Label.tsx @@ -1,9 +1,9 @@ -function PostLabel({ title }: { title: string }) { +function Label({ title }: { title: string }) { return (
    -

    {title} diff --git a/src/shared/components/InputBox/SelectBox.tsx b/src/domains/shared/select-box/SelectBox.tsx similarity index 97% rename from src/shared/components/InputBox/SelectBox.tsx rename to src/domains/shared/select-box/SelectBox.tsx index dfeb341..455b43d 100644 --- a/src/shared/components/InputBox/SelectBox.tsx +++ b/src/domains/shared/select-box/SelectBox.tsx @@ -1,8 +1,9 @@ 'use client'; import { Ref, useMemo, useState } from 'react'; import Down from '@/shared/assets/icons/selectDown_24.svg'; -import { ID, useAccordionStore } from '@/shared/@store/accordionStore'; + import { useShallow } from 'zustand/shallow'; +import { ID, useAccordionStore } from '@/domains/recipe/store/accordionStore'; interface Props { id?: ID; diff --git a/src/shared/components/share/Share.tsx b/src/domains/shared/share/Share.tsx similarity index 78% rename from src/shared/components/share/Share.tsx rename to src/domains/shared/share/Share.tsx index f1f292f..4acc64a 100644 --- a/src/shared/components/share/Share.tsx +++ b/src/domains/shared/share/Share.tsx @@ -5,19 +5,20 @@ interface Props { variants?: 'default' | 'community'; title?: string; content?: string; + size: 'sm' | 'md'; } -function Share({ onClick, variants = 'default', title, content }: Props) { +function Share({ onClick, variants = 'default', title, content, size }: Props) { // title과 content는 추후 API가 들어오면 사용예정 API가 들어오면 타입 옵셔널 체크 해제헤 주세요 return ( - ); -} - -export default WriteBtn; diff --git a/src/shared/components/header/DropdownMenu.tsx b/src/shared/components/header/DropdownMenu.tsx index a64a21a..b333b04 100644 --- a/src/shared/components/header/DropdownMenu.tsx +++ b/src/shared/components/header/DropdownMenu.tsx @@ -1,145 +1,177 @@ +'use client'; + import { navItem } from '@/shared/utills/navigation'; import Image from 'next/image'; import Close from '@/shared/assets/icons/close_32.svg'; import User from '@/shared/assets/icons/user_24.svg'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import gsap from 'gsap'; -import { useAuthStore } from '@/shared/@store/auth'; + +import { createPortal } from 'react-dom'; +import { useAuthStore } from '@/domains/shared/store/auth'; +import { setPreLoginPath } from '@/domains/shared/auth/utils/setPreLoginPath'; +import LogoutConfirm from '@/domains/login/components/LogoutConfirm'; interface Props { isClicked: boolean; setIsClicked: (state: boolean) => void; + visible: boolean; + setVisible: (state: boolean) => void; } -function DropdownMenu({ isClicked, setIsClicked }: Props) { +function DropdownMenu({ isClicked, setIsClicked, visible, setVisible }: Props) { const pathname = usePathname(); const menuRef = useRef(null); const textRef = useRef<(HTMLSpanElement | null)[]>([]); + const tlRef = useRef(null); + const [mounted, setMounted] = useState(false); const { isLoggedIn, logout } = useAuthStore(); + const [logoutModalOpen, setLogoutModalOpen] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { if (!menuRef.current) return; + if (!tlRef.current) { + const tl = gsap.timeline({ + paused: true, + onReverseComplete: () => { + setVisible(false); + }, + }); - if (isClicked) { - gsap.fromTo( + tl.fromTo( menuRef.current, - { - x: -200, - opacity: 0, - }, - { - x: 0, - opacity: 1, - duration: 0.8, - ease: 'expo.inOut', - } + { x: -200, opacity: 0 }, + { x: 0, opacity: 1, duration: 0.5, ease: 'expo.inOut' } ); + + tlRef.current = tl; } - }, [isClicked]); - - const handleMouseEnter = (index: number) => { - const el = textRef.current[index]; - if (!el) return; - gsap.to(el, { - y: -5, - duration: 0.3, - ease: 'power1.out', - }); - }; - - const handleMouseLeave = (index: number) => { - const el = textRef.current[index]; - if (!el) return; - gsap.to(el, { - y: 0, - duration: 0.3, - ease: 'power1.out', - }); - }; - - return ( -

    -
    - {isLoggedIn ? ( +
    - ) : ( - { + aria-label="메인 네비게이션 메뉴 닫기" + onClick={() => { setIsClicked(false); - sessionStorage.setItem('preLoginPath', window.location.pathname); }} - className="flex items-center gap-2 text-black font-light text-xl hover:text-black/70" > - - 로그인/회원가입 - - )} -
    - -
    - +
    + + {logoutModalOpen && ( + setLogoutModalOpen(false)} + onLogout={async () => { + await logout(); + setLogoutModalOpen(false); setIsClicked(false); }} - > - - -
    - + /> + )} + , + document.body ); } diff --git a/src/shared/components/header/HamburgerMenu.tsx b/src/shared/components/header/HamburgerMenu.tsx index aa4d950..46d2351 100644 --- a/src/shared/components/header/HamburgerMenu.tsx +++ b/src/shared/components/header/HamburgerMenu.tsx @@ -4,9 +4,13 @@ import DropdownMenu from './DropdownMenu'; function HamburgerMenu() { const [isClicked, setIsClicked] = useState(false); + const [visible, setVisible] = useState(false); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); + if (!visible) { + setVisible(true); + } setIsClicked(true); }; @@ -22,7 +26,12 @@ function HamburgerMenu() { > - {isClicked && } + ); } diff --git a/src/shared/components/header/Header.tsx b/src/shared/components/header/Header.tsx index dbad4da..e91d2fc 100644 --- a/src/shared/components/header/Header.tsx +++ b/src/shared/components/header/Header.tsx @@ -5,54 +5,42 @@ import HeaderBtn from './HeaderBtn'; import HeaderLogo from './HeaderLogo'; import NavItem from './NavItem'; import HamburgerMenu from './HamburgerMenu'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import tw from '@/shared/utills/tw'; +import { useWindowScroll } from 'react-use'; function Header() { const pathname = usePathname(); - const [showShadow, setShowShadow] = useState(true); - const [lastScrollTop, setLastScrollTop] = useState(0); // 마지막 스크롤 위치 저장 - const headerRef = useRef(null); - - useEffect(() => { - const handleScroll = () => { - const currentScrollTop = window.scrollY; - // console.log(currentScrollTop, lastScrollTop); + const [visible, setVisible] = useState(true); - if (Math.abs(currentScrollTop - lastScrollTop) < -5) return; + const { y } = useWindowScroll(); - if (currentScrollTop > lastScrollTop) { - // 유저가 아래로 스크롤 -> 헤더 숨기기 - if (headerRef.current) { - headerRef.current.style.top = '-60px'; - setShowShadow(false); - } - } else { - // 유저가 위로 스크롤 -> 헤더 다시 보이기 - if (headerRef.current) { - headerRef.current.style.top = '0'; - setShowShadow(true); - } - } - - setLastScrollTop(currentScrollTop); // 마지막 위치 갱신 - }; - - window.addEventListener('scroll', handleScroll); - - // 클린업 함수 - return () => window.removeEventListener('scroll', handleScroll); - }, [lastScrollTop]); + useEffect(() => { + const diff = y - lastScrollTop; + if (Math.abs(diff) < 10) return; // 10px 이하는 무시하도록 너무 민감하게 하는것도 안좋음 + + if (diff > 0) { + // 유저가 아래로 스크롤 -> 헤더 숨기기 + setVisible(false); + setShowShadow(false); + } else if (diff < 0) { + // 유저가 위로 스크롤 -> 헤더 다시 보이기 + setVisible(true); + setShowShadow(true); + } + + setLastScrollTop(y); // 마지막 위치 갱신 + }, [lastScrollTop, y]); return (
    diff --git a/src/shared/components/header/HeaderBtn.tsx b/src/shared/components/header/HeaderBtn.tsx index ad2c91d..07808a4 100644 --- a/src/shared/components/header/HeaderBtn.tsx +++ b/src/shared/components/header/HeaderBtn.tsx @@ -1,68 +1,93 @@ +'use client'; + import Bell from '@/shared/assets/icons/bell_24.svg'; import User from '@/shared/assets/icons/user_24.svg'; -import SignOut from '@/shared/assets/icons/sign_out_24.svg'; -import SignIn from '@/shared/assets/icons/sign_in_24.svg'; import { useRouter } from 'next/navigation'; import tw from '@/shared/utills/tw'; -import { useAuthStore } from '@/shared/@store/auth'; - -type RouterType = ReturnType; +import { useAuthStore } from '@/domains/shared/store/auth'; +import { setPreLoginPath } from '@/domains/shared/auth/utils/setPreLoginPath'; +import { useState } from 'react'; +import LogoutConfirm from '@/domains/login/components/LogoutConfirm'; function HeaderBtn({ pathname }: { pathname: string }) { const { isLoggedIn, logout } = useAuthStore(); - const router = useRouter(); - const headerBtn = [ - ...(isLoggedIn - ? [ - { - icon: Bell, - label: '알림', - onClick: () => {}, - }, - { - icon: User, - label: '마이 페이지', - className: pathname === '/mypage' ? 'text-tertiary' : 'text-current', - onClick: (router: RouterType) => router.push('/mypage'), - }, - { - icon: SignOut, - label: '로그아웃', - onClick: async () => { - await logout(); - }, - }, - ] - : [ - { - icon: SignIn, - label: '로그인', - className: `${pathname === '/login' ? 'text-tertiary' : ''}`, - onClick: () => { - sessionStorage.setItem('preLoginPath', window.location.pathname); - router.push('/login'); - }, - }, - ]), + const [logoutModalOpen, setLogoutModalOpen] = useState(false); + + const navButtons = [ + { + icon: Bell, + label: '알림', + onClick: () => setLogoutModalOpen(true), + }, + { + icon: User, + label: '마이 페이지', + className: pathname === '/mypage' ? 'text-tertiary' : 'text-current', + hiddenMobile: true, + onClick: () => router.push('/mypage'), + }, ]; + const authButton = isLoggedIn + ? { + label: 'Logout', + onClick: () => setLogoutModalOpen(true), + } + : { + label: 'Login', + className: pathname === '/login' ? 'bg-white text-primary' : 'bg-transparent text-white', + onClick: async () => { + await setPreLoginPath(window.location.pathname); + router.push('/login'); + }, + }; return ( -
    - {headerBtn.map(({ icon: Icon, label, onClick, className }) => ( + <> +
    + {/* 아이콘 버튼들 */} +
    + {isLoggedIn && + navButtons.map(({ icon: Icon, label, onClick, className, hiddenMobile }) => ( + + ))} +
    + + {/* 로그인/로그아웃 버튼 */} - ))} -
    +
    + + {/* 로그아웃 확인 모달 */} + {logoutModalOpen && ( + setLogoutModalOpen(false)} + onLogout={async () => { + await logout(); + setLogoutModalOpen(false); + }} + /> + )} + ); } diff --git a/src/shared/components/header/NavItem.tsx b/src/shared/components/header/NavItem.tsx index 570871f..8442c29 100644 --- a/src/shared/components/header/NavItem.tsx +++ b/src/shared/components/header/NavItem.tsx @@ -11,8 +11,8 @@ function NavItem({ pathname, className }: Props) { return (