diff --git a/package-lock.json b/package-lock.json index d32fbf2..346fd42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,20 +8,28 @@ "name": "checkmo", "version": "0.1.0", "dependencies": { - "next": "16.0.1", + "@vercel/analytics": "^1.6.1", + "@vercel/speed-insights": "^1.3.1", + "js-cookie": "^3.0.5", + "next": "^16.1.6", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "react-hot-toast": "^2.6.0", + "zod": "^4.3.6", + "zustand": "^5.0.10" }, "devDependencies": { "@tailwindcss/cli": "^4.1.17", "@tailwindcss/postcss": "^4", + "@types/js-cookie": "^3.0.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.19", "eslint": "^9", - "eslint-config-next": "16.0.1", + "eslint-config-next": "^16.1.6", "postcss": "^8.5.6", "tailwindcss": "^4", "typescript": "^5" @@ -1039,15 +1047,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.1.tgz", - "integrity": "sha512-LFvlK0TG2L3fEOX77OC35KowL8D7DlFF45C0OvKMC4hy8c/md1RC4UMNDlUGJqfCoCS2VWrZ4dSE6OjaX5+8mw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.1.tgz", - "integrity": "sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1055,9 +1063,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.1.tgz", - "integrity": "sha512-R0YxRp6/4W7yG1nKbfu41bp3d96a0EalonQXiMe+1H9GTHfKxGNCGFNWUho18avRBPsO8T3RmdWuzmfurlQPbg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", "cpu": [ "arm64" ], @@ -1071,9 +1079,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.1.tgz", - "integrity": "sha512-kETZBocRux3xITiZtOtVoVvXyQLB7VBxN7L6EPqgI5paZiUlnsgYv4q8diTNYeHmF9EiehydOBo20lTttCbHAg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", "cpu": [ "x64" ], @@ -1087,9 +1095,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.1.tgz", - "integrity": "sha512-hWg3BtsxQuSKhfe0LunJoqxjO4NEpBmKkE+P2Sroos7yB//OOX3jD5ISP2wv8QdUwtRehMdwYz6VB50mY6hqAg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", "cpu": [ "arm64" ], @@ -1103,9 +1111,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.1.tgz", - "integrity": "sha512-UPnOvYg+fjAhP3b1iQStcYPWeBFRLrugEyK/lDKGk7kLNua8t5/DvDbAEFotfV1YfcOY6bru76qN9qnjLoyHCQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", "cpu": [ "arm64" ], @@ -1119,9 +1127,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.1.tgz", - "integrity": "sha512-Et81SdWkcRqAJziIgFtsFyJizHoWne4fzJkvjd6V4wEkWTB4MX6J0uByUb0peiJQ4WeAt6GGmMszE5KrXK6WKg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", "cpu": [ "x64" ], @@ -1135,9 +1143,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.1.tgz", - "integrity": "sha512-qBbgYEBRrC1egcG03FZaVfVxrJm8wBl7vr8UFKplnxNRprctdP26xEv9nJ07Ggq4y1adwa0nz2mz83CELY7N6Q==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", "cpu": [ "x64" ], @@ -1151,9 +1159,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.1.tgz", - "integrity": "sha512-cPuBjYP6I699/RdbHJonb3BiRNEDm5CKEBuJ6SD8k3oLam2fDRMKAvmrli4QMDgT2ixyRJ0+DTkiODbIQhRkeQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", "cpu": [ "arm64" ], @@ -1167,9 +1175,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.1.tgz", - "integrity": "sha512-XeEUJsE4JYtfrXe/LaJn3z1pD19fK0Q6Er8Qoufi+HqvdO4LEPyCxLUt4rxA+4RfYo6S9gMlmzCMU2F+AatFqQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", "cpu": [ "x64" ], @@ -1863,6 +1871,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "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", @@ -1891,7 +1906,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2445,6 +2460,78 @@ "win32" ] }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@vercel/speed-insights": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.3.1.tgz", + "integrity": "sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -2786,13 +2873,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", - "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", - "dev": true, + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { @@ -3009,7 +3098,6 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -3464,13 +3552,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.1.tgz", - "integrity": "sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.1", + "@next/eslint-plugin-next": "16.1.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -4156,6 +4244,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4796,6 +4893,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5366,14 +5472,14 @@ "license": "MIT" }, "node_modules/next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", - "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", "dependencies": { - "@next/env": "16.0.1", + "@next/env": "16.1.6", "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -5385,14 +5491,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.1", - "@next/swc-darwin-x64": "16.0.1", - "@next/swc-linux-arm64-gnu": "16.0.1", - "@next/swc-linux-arm64-musl": "16.0.1", - "@next/swc-linux-x64-gnu": "16.0.1", - "@next/swc-linux-x64-musl": "16.0.1", - "@next/swc-win32-arm64-msvc": "16.0.1", - "@next/swc-win32-x64-msvc": "16.0.1", + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { @@ -5830,6 +5936,23 @@ "react": "^19.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6953,10 +7076,9 @@ } }, "node_modules/zod": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.2.tgz", - "integrity": "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg==", - "dev": true, + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -6974,6 +7096,35 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 4a1ad1e..13073fa 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "react-hot-toast": "^2.6.0", + "zod": "^4.3.6", "zustand": "^5.0.10" }, "devDependencies": { @@ -27,6 +28,7 @@ "@types/react-dom": "^19", "autoprefixer": "^10.4.21", "babel-plugin-react-compiler": "1.0.0", + "baseline-browser-mapping": "^2.9.19", "eslint": "^9", "eslint-config-next": "^16.1.6", "postcss": "^8.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7cf01..9732f85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-hot-toast: specifier: ^2.6.0 version: 2.6.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + zod: + specifier: ^4.3.6 + version: 4.3.6 zustand: specifier: ^5.0.10 version: 5.0.10(@types/react@19.2.2)(react@19.2.0) @@ -57,6 +60,9 @@ importers: babel-plugin-react-compiler: specifier: 1.0.0 version: 1.0.0 + baseline-browser-mapping: + specifier: ^2.9.19 + version: 2.9.19 eslint: specifier: ^9 version: 9.39.1(jiti@2.6.1) @@ -970,8 +976,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.25: - resolution: {integrity: sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true brace-expansion@1.1.12: @@ -2166,8 +2172,8 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zustand@5.0.10: resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} @@ -2979,7 +2985,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.25: {} + baseline-browser-mapping@2.9.19: {} brace-expansion@1.1.12: dependencies: @@ -2996,7 +3002,7 @@ snapshots: browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.25 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001754 electron-to-chromium: 1.5.249 node-releases: 2.0.27 @@ -3326,8 +3332,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.39.1(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.2(zod@4.1.12) + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -3865,7 +3871,7 @@ snapshots: dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.8.25 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001754 postcss: 8.4.31 react: 19.2.0 @@ -4418,11 +4424,11 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.1.12): + zod-validation-error@4.0.2(zod@4.3.6): dependencies: - zod: 4.1.12 + zod: 4.3.6 - zod@4.1.12: {} + zod@4.3.6: {} zustand@5.0.10(@types/react@19.2.2)(react@19.2.0): optionalDependencies: diff --git a/public/icons_calling.svg b/public/icons_calling.svg new file mode 100644 index 0000000..c80042f --- /dev/null +++ b/public/icons_calling.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons_chat.svg b/public/icons_chat.svg new file mode 100644 index 0000000..9f8a19c --- /dev/null +++ b/public/icons_chat.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/icons_pencil.svg b/public/icons_pencil.svg new file mode 100644 index 0000000..4b04fca --- /dev/null +++ b/public/icons_pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app/(main)/books/[id]/page.tsx b/src/app/(main)/books/[id]/page.tsx index c40cf12..f2b6472 100644 --- a/src/app/(main)/books/[id]/page.tsx +++ b/src/app/(main)/books/[id]/page.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import SearchBookResult from "@/components/base-ui/Search/search_bookresult"; -import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card"; import { DUMMY_STORIES } from "@/data/dummyStories"; +import BookStoryCardLarge from "@/components/base-ui/BookStory/bookstory_card_large"; export default function BookDetailPage() { const params = useParams(); @@ -23,58 +23,61 @@ export default function BookDetailPage() { // 관련된 책 이야기들 (더미 데이터에서 필터링) const relatedStories = DUMMY_STORIES.filter( - (story) => story.bookTitle === bookData.title + (story) => story.bookTitle === bookData.title, ); return ( -
-

- 도서 선택 {bookData.title} 중 -

+
+
+

+ 도서 선택 {bookData.title} 중 +

- {/* 선택한 책 카드 */} -
- { - router.push(`/stories/new?bookId=${bookId}`); - }} - /> -
+ {/* 선택한 책 카드 */} +
+ { + router.push(`/stories/new?bookId=${bookId}`); + }} + /> +
- {/* 책이야기 */} -
-

- 책이야기 {relatedStories.length} -

-
+ {/* 책이야기 */} +
+

+ 책이야기{" "} + {relatedStories.length} +

+
- {/* 책 이야기 카드 */} -
- {relatedStories.map((story) => ( -
router.push(`/stories/${story.id}`)} - className="cursor-pointer" - > - -
- ))} + {/* 책 이야기 카드 */} +
+ {relatedStories.map((story) => ( +
router.push(`/stories/${story.id}`)} + className="cursor-pointer" + > + +
+ ))} +
); diff --git a/src/app/(main)/news/[id]/page.tsx b/src/app/(main)/news/[id]/page.tsx index 55f3cd6..23ec1ce 100644 --- a/src/app/(main)/news/[id]/page.tsx +++ b/src/app/(main)/news/[id]/page.tsx @@ -1,3 +1,4 @@ +import FloatingFab from "@/components/base-ui/Float"; import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books"; import Image from "next/image"; import { notFound } from "next/navigation"; @@ -159,21 +160,10 @@ export default async function NewsDetailPage({ params }: Props) {
- {/* 문의하기 */} - + ); } diff --git a/src/app/(main)/news/page.tsx b/src/app/(main)/news/page.tsx index 60ff7dc..3d2c943 100644 --- a/src/app/(main)/news/page.tsx +++ b/src/app/(main)/news/page.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import NewsList from "@/components/base-ui/News/news_list"; import TodayRecommendedBooks from "@/components/base-ui/News/today_recommended_books"; +import FloatingFab from "@/components/base-ui/Float"; const DUMMY_NEWS = [ { @@ -105,21 +106,11 @@ export default function NewsPage() {
- {/* 문의하기 */} - + +
); } diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 2799e9b..bdafd34 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Image from "next/image"; import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card"; import NewsBannerSlider from "@/components/base-ui/home/NewsBannerSlider"; import HomeBookclub from "@/components/base-ui/home/home_bookclub"; @@ -10,38 +11,30 @@ import LoginModal from "@/components/base-ui/Login/LoginModal"; import { DUMMY_STORIES } from "@/data/dummyStories"; import BookStoryCardLarge from "@/components/base-ui/BookStory/bookstory_card_large"; +import { useAuthStore } from "@/store/useAuthStore"; + export default function HomePage() { const groups: { id: string; name: string }[] = []; - const [showLoginModal, setShowLoginModal] = useState(false); - + const { isLoggedIn, isLoginModalOpen, openLoginModal, closeLoginModal } = useAuthStore(); + // 사용자 더미 데이터 const users = [ - { id: '1', name: 'hy_0716', subscribingCount: 17, subscribersCount: 32 }, - { id: '2', name: 'hy_0716', subscribingCount: 17, subscribersCount: 32 }, - { id: '3', name: 'hy_0716', subscribingCount: 17, subscribersCount: 32 }, - { id: '4', name: 'hy_0716', subscribingCount: 17, subscribersCount: 32 }, + { id: "1", name: "hy_0716", subscribingCount: 17, subscribersCount: 32 }, + { id: "2", name: "hy_0716", subscribingCount: 17, subscribersCount: 32 }, + { id: "3", name: "hy_0716", subscribingCount: 17, subscribersCount: 32 }, + { id: "4", name: "hy_0716", subscribingCount: 17, subscribersCount: 32 }, ]; return (
- {/* 임시 로그인 모달 테스트 버튼 */} - - - {showLoginModal && ( - setShowLoginModal(false)} /> + {isLoginModalOpen && ( + closeLoginModal()} /> )} {/* 모바일 */}
{/* 소식 */}
-

- 소식 -

+

소식

@@ -49,9 +42,7 @@ export default function HomePage() {
-

- 독서모임 -

+

독서모임

@@ -65,7 +56,7 @@ export default function HomePage() { name={u.name} subscribingCount={u.subscribingCount} subscribersCount={u.subscribersCount} - onSubscribeClick={() => console.log('subscribe', u.id)} + onSubscribeClick={() => console.log("subscribe", u.id)} /> ))}
@@ -151,17 +142,17 @@ export default function HomePage() {
{/* 소식 */}
-

- 소식 -

- +

+ 소식 +

+
{/* 책 이야기 카드 */}
{DUMMY_STORIES.slice(0, 3).map((story) => ( - + /> ))} -
-
+
+
+ + {/* 비로그인 시 플로팅 로그인 하기 버튼 */} + {!isLoggedIn && ( + + )}
); } diff --git a/src/app/(main)/profile/mypage/page.tsx b/src/app/(main)/profile/mypage/page.tsx index 2086238..dc54b66 100644 --- a/src/app/(main)/profile/mypage/page.tsx +++ b/src/app/(main)/profile/mypage/page.tsx @@ -11,8 +11,15 @@ import MyMeetingList from "@/components/base-ui/MyPage/MyMeetingList"; import MyNotificationList from "@/components/base-ui/MyPage/MyNotificationList"; import MyLibraryList from "@/components/base-ui/Profile/LibraryList"; +import { useAuthGuard } from "@/hooks/useAuthGuard"; + export default function MyPage() { const [activeTab, setActiveTab] = useState("stories"); + const { isInitialized, isLoggedIn } = useAuthGuard(); + + if (!isInitialized || !isLoggedIn) { + return null; // 초기화 중이거나 리다이렉트 중에는 빈 화면 + } return (
diff --git a/src/app/(main)/setting/layout.tsx b/src/app/(main)/setting/layout.tsx index 51b8273..63f23cb 100644 --- a/src/app/(main)/setting/layout.tsx +++ b/src/app/(main)/setting/layout.tsx @@ -1,3 +1,7 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useAuthGuard } from "@/hooks/useAuthGuard"; import Header from "@/components/layout/Header"; import SettingsSidebar from "@/components/base-ui/Settings/SettingsSidebar"; import BottomNav from "@/components/layout/BottomNav"; @@ -7,6 +11,11 @@ export default function SettingsLayout({ }: { children: React.ReactNode; }) { + const { isInitialized, isLoggedIn } = useAuthGuard(); + + if (!isInitialized || !isLoggedIn) { + return null; + } return (
{/* 메인 컨텐츠 래퍼 */} diff --git a/src/app/(main)/setting/logout/page.tsx b/src/app/(main)/setting/logout/page.tsx new file mode 100644 index 0000000..420599a --- /dev/null +++ b/src/app/(main)/setting/logout/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { authService } from "@/services/authService"; + +export default function LogoutPage() { + const router = useRouter(); + + useEffect(() => { + const performLogout = async () => { + await authService.logout(); + router.push("/"); + }; + + performLogout(); + }, [router]); + + return ( +
+
+
+ ); +} diff --git a/src/app/(main)/setting/profile/page.tsx b/src/app/(main)/setting/profile/page.tsx index 59cb42d..02ff2ad 100644 --- a/src/app/(main)/setting/profile/page.tsx +++ b/src/app/(main)/setting/profile/page.tsx @@ -1,8 +1,53 @@ +"use client"; + +import { useAuthStore } from "@/store/useAuthStore"; import CategorySelector from "@/components/base-ui/Settings/EditProfile/CategorySelector"; import ProfileImageSection from "@/components/base-ui/Settings/EditProfile/ProfileImageSection"; import SettingsDetailLayout from "@/components/base-ui/Settings/SettingsDetailLayout"; +import { useState, useEffect } from "react"; +import toast from "react-hot-toast"; export default function ProfileEditPage() { + const { user } = useAuthStore(); + const [nickname, setNickname] = useState(user?.nickname || ""); + const [intro, setIntro] = useState(user?.description || ""); + const [name, setName] = useState(user?.nickname || ""); // Assuming nickname is used for name if not separate + const [phone, setPhone] = useState(""); + const [selectedCategories, setSelectedCategories] = useState( + user?.categories || [] + ); + + useEffect(() => { + if (user) { + setNickname(user.nickname || ""); + setIntro(user.description || ""); + setName(user.nickname || ""); + setSelectedCategories(user.categories || []); + } + }, [user]); + + const handleToggleCategory = (category: string) => { + setSelectedCategories((prev) => + prev.includes(category) + ? prev.filter((c) => c !== category) + : prev.length < 6 + ? [...prev, category] + : prev + ); + }; + + const handleSave = () => { + console.log("Saving profile changes:", { + nickname, + intro, + name, + phone, + selectedCategories, + }); + // TODO: Connect to backend API for profile update + toast.success("프로필 정보가 저장되었습니다."); + }; + // 공통 스타일 상수 const inputContainerClass = "flex items-center gap-[10px] rounded-[8px] border border-Subbrown-4 bg-White px-[16px] py-[12px] h-[36px] md:h-[52px]"; @@ -16,10 +61,14 @@ export default function ProfileEditPage() { return (
- + {/* 닉네임 */}
@@ -28,43 +77,69 @@ export default function ProfileEditPage() {
setNickname(e.target.value)} placeholder="변경할 닉네임을 입력해주세요" />
-
- {/* 소개, 이름, 전화번호 (반복 패턴) */} - {["소개", "이름", "전화번호"].map((label) => ( -
- -
- -
+ {/* 소개 */} +
+ +
+ setIntro(e.target.value)} + placeholder="20자 이내로 작성해주세요" + />
- ))} +
+ + {/* 이름 */} +
+ +
+ setName(e.target.value)} + placeholder="이름을 입력해주세요" + /> +
+
- + {/* 전화번호 */} +
+ +
+ setPhone(e.target.value)} + placeholder="010-0000-0000" + /> +
+
+ + {/* 저장 버튼 */} -
); diff --git a/src/app/(public)/signup/[step]/page.tsx b/src/app/(public)/signup/[step]/page.tsx new file mode 100644 index 0000000..ccd72d3 --- /dev/null +++ b/src/app/(public)/signup/[step]/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import React from "react"; +import { useParams, useRouter } from "next/navigation"; +import TermsAgreement from "@/components/base-ui/Join/steps/TermsAgreement/TermsAgreement"; +import EmailVerification from "@/components/base-ui/Join/steps/EmailVerification/EmailVerification"; +import PasswordEntry from "@/components/base-ui/Join/steps/PasswordEntry/PasswordEntry"; +import ProfileSetup from "@/components/base-ui/Join/steps/ProfileSetup/ProfileSetup"; +import ProfileImage from "@/components/base-ui/Join/steps/ProfileImage/ProfileImage"; +import SignupComplete from "@/components/base-ui/Join/steps/SignupComplete/SignupComplete"; + +export default function SignupStepPage() { + const params = useParams(); + const router = useRouter(); + const step = params.step as string; + + const navigateTo = (nextStep: string) => { + router.push(`/signup/${nextStep}`); + }; + + const steps: Record = { + terms: navigateTo("email")} />, + email: navigateTo("password")} />, + password: navigateTo("profile")} />, + profile: navigateTo("profile-image")} />, + "profile-image": navigateTo("complete")} />, + complete: , + }; + + const currentStep = steps[step]; + + if (!currentStep) { + // Falls back to terms if step is invalid + return null; + } + + return <>{currentStep}; +} diff --git a/src/app/(public)/signup/layout.tsx b/src/app/(public)/signup/layout.tsx index 8b21600..835a483 100644 --- a/src/app/(public)/signup/layout.tsx +++ b/src/app/(public)/signup/layout.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { SignupProvider } from "@/contexts/SignupContext"; export default function SignupLayout({ children, @@ -6,11 +7,13 @@ export default function SignupLayout({ children: React.ReactNode; }) { return ( -
- {children} -
+ +
+ {children} +
+
); } \ No newline at end of file diff --git a/src/app/(public)/signup/page.tsx b/src/app/(public)/signup/page.tsx index a35a982..b916629 100644 --- a/src/app/(public)/signup/page.tsx +++ b/src/app/(public)/signup/page.tsx @@ -1,35 +1,14 @@ "use client"; -import React, { useState } from "react"; -import TermsAgreement from "@/components/base-ui/Join/steps/TermsAgreement/TermsAgreement"; -import EmailVerification from "@/components/base-ui/Join/steps/EmailVerification/EmailVerification"; -import PasswordEntry from "@/components/base-ui/Join/steps/PasswordEntry/PasswordEntry"; -import ProfileSetup from "@/components/base-ui/Join/steps/ProfileSetup/ProfileSetup"; -import ProfileImage from "@/components/base-ui/Join/steps/ProfileImage/ProfileImage"; -import SignupComplete from "@/components/base-ui/Join/steps/SignupComplete/SignupComplete"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; export default function SignupPage() { - const [step, setStep] = useState< - "terms" | "email" | "password" | "profile" | "profile-image" | "complete" - >("terms"); - const steps = { - terms: ( - { - setStep("email"); - }} - /> - ), - email: setStep("password")} />, - password: setStep("profile")} />, - profile: setStep("profile-image")} />, - "profile-image": setStep("complete")} />, - complete: , - }; + const router = useRouter(); - return ( - <> - {steps[step]} - - ); + useEffect(() => { + router.replace("/signup/terms"); + }, [router]); + + return null; } diff --git a/src/app/globals.css b/src/app/globals.css index 604a790..086e1a3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -146,3 +146,32 @@ body { --breakpoint-t: 768px; --breakpoint-d: 1440px; } + +/* 애니메이션 */ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slide-down { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@utility animate-fade-in { + animation: fade-in 0.2s ease-out; +} + +@utility animate-slide-down { + animation: slide-down 0.3s ease-out; +} diff --git a/src/app/groups/[id]/admin/notice/new/page.tsx b/src/app/groups/[id]/admin/notice/new/page.tsx index 553570a..12d33be 100644 --- a/src/app/groups/[id]/admin/notice/new/page.tsx +++ b/src/app/groups/[id]/admin/notice/new/page.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; -import { useState, useRef, useEffect, type ChangeEvent } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import Image from 'next/image'; -import BookshelfModal from '@/components/base-ui/Group/BookshelfModal'; -import BookDetailCard from '@/components/base-ui/Bookcase/BookDetailCard'; -import { useHeaderTitle } from '@/contexts/HeaderTitleContext'; +import { useState, useRef, useEffect, type ChangeEvent } from "react"; +import { useParams, useRouter } from "next/navigation"; +import Image from "next/image"; +import BookshelfModal from "@/components/base-ui/Group/BookshelfModal"; +import BookDetailCard from "@/components/base-ui/Bookcase/BookDetailCard"; +import { useHeaderTitle } from "@/contexts/HeaderTitleContext"; type Book = { id: number; @@ -21,16 +21,17 @@ export default function NewNoticePage() { const router = useRouter(); const groupId = params.id as string; const { setCustomTitle } = useHeaderTitle(); - - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); - const [selectedOption, setSelectedOption] = - useState<'vote' | 'bookshelf' | 'image' | null>(null); - const [voteItems, setVoteItems] = useState(['', '', '', '']); + + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [selectedOption, setSelectedOption] = useState< + "vote" | "bookshelf" | "image" | null + >(null); + const [voteItems, setVoteItems] = useState(["", "", "", ""]); const [isMultiple, setIsMultiple] = useState(false); const [isAnonymous, setIsAnonymous] = useState(false); const [isImportant, setIsImportant] = useState(false); - const [voteDate, setVoteDate] = useState(''); + const [voteDate, setVoteDate] = useState(""); const [isBookshelfModalOpen, setIsBookshelfModalOpen] = useState(false); const [imagePreviews, setImagePreviews] = useState([]); const [selectedBook, setSelectedBook] = useState(null); @@ -39,7 +40,7 @@ export default function NewNoticePage() { // 모바일 헤더 타이틀 설정 useEffect(() => { - setCustomTitle('공지사항 작성'); + setCustomTitle("공지사항 작성"); return () => setCustomTitle(null); }, [setCustomTitle]); @@ -49,20 +50,18 @@ export default function NewNoticePage() { const handleSubmit = () => { const trimmedVoteItems = - selectedOption === 'vote' - ? voteItems - .map((item) => item.trim()) - .filter((item) => item.length > 0) + selectedOption === "vote" + ? voteItems.map((item) => item.trim()).filter((item) => item.length > 0) : []; // TODO: 실제 저장 로직 구현 - console.log('공지사항 저장:', { + console.log("공지사항 저장:", { title, content, selectedOption, voteItems: trimmedVoteItems, voteSettings: - selectedOption === 'vote' + selectedOption === "vote" ? { isMultiple, isAnonymous, @@ -75,34 +74,31 @@ export default function NewNoticePage() { }; const handleCreateVote = () => { - setSelectedOption(selectedOption === 'vote' ? null : 'vote'); + setSelectedOption(selectedOption === "vote" ? null : "vote"); // TODO: 투표 생성 페이지로 이동 또는 모달 열기 - console.log('투표 생성'); + console.log("투표 생성"); }; const handleVoteItemChange = (index: number, value: string) => { - setVoteItems((prev) => - prev.map((item, i) => (i === index ? value : item)), - ); + setVoteItems((prev) => prev.map((item, i) => (i === index ? value : item))); }; const handleRegisterBookshelf = () => { - const nextSelected = - selectedOption === 'bookshelf' ? null : 'bookshelf'; + const nextSelected = selectedOption === "bookshelf" ? null : "bookshelf"; setSelectedOption(nextSelected); - setIsBookshelfModalOpen(nextSelected === 'bookshelf'); - console.log('책장 등록'); + setIsBookshelfModalOpen(nextSelected === "bookshelf"); + console.log("책장 등록"); }; const handleBookSelect = (book: Book) => { setSelectedBook(book); - setSelectedOption('bookshelf'); + setSelectedOption("bookshelf"); }; const handleImageFile = () => { - setSelectedOption('image'); + setSelectedOption("image"); imageInputRef.current?.click(); - console.log('이미지 파일'); + console.log("이미지 파일"); }; const handleImageChange = (e: ChangeEvent) => { @@ -122,27 +118,17 @@ export default function NewNoticePage() { }, []); return (
- {/* 뒤로가기 - 모바일에서만 표시 */} -
- -
- {/* 구분선 */}
- -
+ +
{/* 제목 및 내용 입력 영역 */}
-

공지사항 작성

- +

+ 공지사항 작성 +

+ {/* 선택된 책 표시 */} {selectedBook && (
@@ -157,215 +143,221 @@ export default function NewNoticePage() { )} {/* 입력 박스 */} -
- {/* 제목 */} -
- setTitle(e.target.value)} - placeholder="제목을 입력해주세요." - className="w-full bg-transparent outline-none text-Gray-7 subhead_4_1 placeholder:text-Gray-3" - /> -
- - {/* 내용 */} -
-