diff --git a/package-lock.json b/package-lock.json index d898fb6d..abb4363d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@storybook/addon-a11y": "^9.1.7", "@storybook/addon-docs": "^9.1.7", "@storybook/addon-vitest": "^9.1.7", - "@storybook/nextjs-vite": "^9.1.7", + "@storybook/nextjs-vite": "^9.1.8", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -2068,53 +2068,6 @@ } } }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@joshwooding/vite-plugin-react-docgen-typescript/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2867,13 +2820,13 @@ } }, "node_modules/@storybook/builder-vite": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.7.tgz", - "integrity": "sha512-9nflIekC220TSKprN/dDW+tAZSxwkRaq0C6mc5UCgXKjgq4oXditpdwrAcoH0v91RC/bN7LW9Xu5IbvnLNiqLQ==", + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.8.tgz", + "integrity": "sha512-JjvBag0nM1N51O3VF5++op9Ly5OC8Q+y4PrWLgi2dKhMxJFs8fD9D4PeI/v41PUiQcI0suQxN9BoYoKn2QxUZw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "9.1.7", + "@storybook/csf-plugin": "9.1.8", "ts-dedent": "^2.0.0" }, "funding": { @@ -2881,10 +2834,27 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^9.1.7", + "storybook": "^9.1.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@storybook/builder-vite/node_modules/@storybook/csf-plugin": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.8.tgz", + "integrity": "sha512-KnrXPz87bn+8ZGkzFEBc7TT5HkWpR1Xz7ojxPclSvkKxTfzazuaw0JlOQMzJoI1+wHXDAIw/4MIsO8HEiaWyfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.1.8" + } + }, "node_modules/@storybook/csf-plugin": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.7.tgz", @@ -2924,15 +2894,15 @@ } }, "node_modules/@storybook/nextjs-vite": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/@storybook/nextjs-vite/-/nextjs-vite-9.1.7.tgz", - "integrity": "sha512-Z9u1hyWaqOCHyYiiw5y9qTl+XaEYc++VNaD9IvsXE/gi5dK2RpDnNdTdotZ7cDBDEqc1WGzndmovlRF3MlClTA==", + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/nextjs-vite/-/nextjs-vite-9.1.8.tgz", + "integrity": "sha512-f4UrKGTYVgz1suTwUN0b9C/1u4sqDRsfyG0S/eryEyxdTb9VDa8WxUDAita2lyWENXy9XwmkBv8v3iula21VRA==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-vite": "9.1.7", - "@storybook/react": "9.1.7", - "@storybook/react-vite": "9.1.7", + "@storybook/builder-vite": "9.1.8", + "@storybook/react": "9.1.8", + "@storybook/react-vite": "9.1.8", "styled-jsx": "5.1.6", "vite-plugin-storybook-nextjs": "^2.0.7" }, @@ -2947,7 +2917,7 @@ "next": "^14.1.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.7", + "storybook": "^9.1.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "peerDependenciesMeta": { @@ -2957,14 +2927,14 @@ } }, "node_modules/@storybook/react": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.7.tgz", - "integrity": "sha512-GxuA2Eh3LlkEF4HHDKFGP+bqQ1+7VtABVacSXukMu82WV4VAOXhhHEDII8R9AVl2Fbs/iPJnNVj06wnkDeUZhA==", + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.1.8.tgz", + "integrity": "sha512-EULkwHroJ4IDYcjIBj9VpGhaZ9E5b8LI84hlfBkJ9rnK44a/GrK1yFRIusukO58qTJSh2Y7zfAFKNuiaWh3Sfw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "9.1.7" + "@storybook/react-dom-shim": "9.1.8" }, "engines": { "node": ">=20.0.0" @@ -2976,7 +2946,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.7", + "storybook": "^9.1.8", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -3002,16 +2972,16 @@ } }, "node_modules/@storybook/react-vite": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.7.tgz", - "integrity": "sha512-552jMY5eKnP/rWKpcEjyE4ppyGmO+r9IoYNIJQBWA4DpXAQ8NjhsygCFhdDPFGfCxx7+KmfRgOBPcXeywWNgtQ==", + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.1.8.tgz", + "integrity": "sha512-DIxp76vcelyFOUJupeQEIHXDrSPP6KDXj6Z+Z9thS1HH7JY+OdGtcMLy4fbiD77Zyc8TV9RRZ1D33z2Ot/v9Vw==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "9.1.7", - "@storybook/react": "9.1.7", + "@storybook/builder-vite": "9.1.8", + "@storybook/react": "9.1.8", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", @@ -3028,23 +2998,10 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^9.1.7", + "storybook": "^9.1.8", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/@storybook/react-vite/node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@storybook/react-vite/node_modules/find-up": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", @@ -3134,28 +3091,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/@storybook/react-vite/node_modules/react-docgen": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.1.tgz", - "integrity": "sha512-kQKsqPLplY3Hx4jGnM3jpQcG3FQDt7ySz32uTHt3C9HAe45kNXG+3o16Eqn3Fw1GtMfHoN3b4J/z2e6cZJCmqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.2", - "@types/babel__core": "^7.20.5", - "@types/babel__traverse": "^7.20.7", - "@types/doctrine": "^0.0.9", - "@types/resolve": "^1.20.2", - "doctrine": "^3.0.0", - "resolve": "^1.22.1", - "strip-indent": "^4.0.0" - }, - "engines": { - "node": "^20.9.0 || >=22" - } - }, "node_modules/@storybook/react-vite/node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -3184,6 +3119,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.8.tgz", + "integrity": "sha512-OepccjVZh/KQugTH8/RL2CIyf1g5Lwc5ESC8x8BH3iuYc82WMQBwMJzRI5EofQdirau63NGrqkWCgQASoVreEA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.1.8" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -4797,9 +4748,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", + "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5505,9 +5456,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", + "version": "1.5.227", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", + "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", "dev": true, "license": "ISC" }, @@ -6701,6 +6652,27 @@ "node": ">=16" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -6714,6 +6686,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -9015,6 +9013,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-docgen": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.1.tgz", + "integrity": "sha512-kQKsqPLplY3Hx4jGnM3jpQcG3FQDt7ySz32uTHt3C9HAe45kNXG+3o16Eqn3Fw1GtMfHoN3b4J/z2e6cZJCmqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, "node_modules/react-docgen-typescript": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", @@ -9025,6 +9045,19 @@ "typescript": ">= 4.3.x" } }, + "node_modules/react-docgen/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -9652,9 +9685,9 @@ } }, "node_modules/storybook": { - "version": "9.1.7", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.7.tgz", - "integrity": "sha512-X8YSQMNuqV9DklQLZH6mLKpDn15Z5tuUUTAIYsiGqx5BwsjtXnv5K04fXgl3jqTZyUauzV/ii8KdT04NVLtMwQ==", + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.8.tgz", + "integrity": "sha512-/iP+DvieJ6Mnixy4PFY/KXnhsg/IHIDlTbZqly3EDbveuhsCuIUELfGnj+QSRGf9C6v/f4sZf9sZ3r80ZnKuEA==", "dev": true, "license": "MIT", "dependencies": { @@ -10067,27 +10100,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/test-exclude/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", diff --git a/package.json b/package.json index b626eaac..4f1f9730 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@storybook/addon-a11y": "^9.1.7", "@storybook/addon-docs": "^9.1.7", "@storybook/addon-vitest": "^9.1.7", - "@storybook/nextjs-vite": "^9.1.7", + "@storybook/nextjs-vite": "^9.1.8", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 00000000..0eeb2068 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,36 @@ +"use client"; + +import React, { useState } from "react"; +import { SignUpForm } from "@/features/auth/ui/SignUpForm/SignUpForm"; +import { Modal } from "@/shared/ui/Modal/Modal"; +import { useRouter } from "next/navigation"; + +export default function SignUpPage() { + const [showModal, setShowModal] = useState(false); + const router = useRouter(); + + const handleSuccess = () => { + setShowModal(true); // 회원가입 성공 시 모달 띄우기 + }; + + const handleCloseModal = () => { + setShowModal(false); + router.push("/login"); // 모달 닫고 로그인 페이지로 이동 + }; + + return ( +
+

회원가입

+ + + {showModal && ( + + )} +
+ ); +} diff --git a/src/features/auth/ui/LoginForm/LoginForm.stories.tsx b/src/features/auth/ui/LoginForm/LoginForm.stories.tsx new file mode 100644 index 00000000..c0eb2ed9 --- /dev/null +++ b/src/features/auth/ui/LoginForm/LoginForm.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from "@storybook/nextjs-vite"; +import { LoginForm } from "./LoginForm"; + +const meta: Meta = { + title: "Auth/LoginForm", + component: LoginForm, + tags: ["autodocs"], + argTypes: { + size: { + control: { type: "radio" }, + options: ["sm", "md", "lg"], + description: "폼 전체 Input과 Button 크기를 조절합니다", + }, + }, +}; +export default meta; + +type Story = StoryObj; + +// 기본 (Large size) +export const Default: Story = { + args: { + size: "lg", + }, +}; + +// Small size +export const Small: Story = { + args: { + size: "sm", + }, +}; + +// Medium size +export const Medium: Story = { + args: { + size: "md", + }, +}; diff --git a/src/features/auth/ui/LoginForm/LoginForm.tsx b/src/features/auth/ui/LoginForm/LoginForm.tsx new file mode 100644 index 00000000..af741655 --- /dev/null +++ b/src/features/auth/ui/LoginForm/LoginForm.tsx @@ -0,0 +1,99 @@ +"use client"; + +import React, { useState } from "react"; +import { Input } from "@/entities/user/ui/Input/Input"; +import Button from "@/shared/ui/Button/Button"; + +type FormSize = "sm" | "md" | "lg"; + +interface LoginFormProps { + size?: FormSize; + onSuccess?: () => void; + onError?: (msg: string) => void; +} + +export const LoginForm = ({ + size = "lg", + onSuccess, + onError, + ...props +}: LoginFormProps) => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + //에러 메시지 관리 + const [emailError, setEmailError] = useState(null); + const [passwordError, setPasswordError] = useState(null); + + //로그인 요청 제출 핸들러, 추후 API 호출 예정 + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // TODO : api 호출 및 검증 로직 추가, Zustand를 통한 토큰 저장 및 부모 핸들러 전달 로직 추가 + console.log("로그인 요청 폼 제출"); + const res = {}; // + }; + + // 이메일 유효성 검사 함수 + const validateEmail = (value: string) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; //이메일 regex + return regex.test(value); + }; + + //이메일 변경 처리 핸들러 + const handleEmailChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setEmail(value); + + if (value != null && value !== "" && !validateEmail(value)) { + setEmailError("올바른 이메일 형식이 아닙니다."); + } else { + setEmailError(null); + } + }; + + //비밀번호 변경 처리 핸들러 + const handlePasswordChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setPassword(value); + + if (value != null && value !== "" && value.length < 8) { + setPasswordError("비밀번호는 최소 8자 이상이어야 합니다."); + } else { + setPasswordError(null); + } + }; + + return ( +
+ + + + + +
+ ); +}; diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx new file mode 100644 index 00000000..01228eb0 --- /dev/null +++ b/src/features/auth/ui/SignUpForm/SignUpForm.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/nextjs-vite"; +import { SignUpForm } from "./SignUpForm"; + +const meta: Meta = { + title: "AUTH/SignUpForm", + component: SignUpForm, + parameters: { + layout: "centered", + }, + argTypes: { + size: { + control: { type: "radio" }, + options: ["sm", "md", "lg"], + }, + onSuccess: { action: "success" }, + onError: { action: "error" }, + }, +}; + +export default meta; +type Story = StoryObj; + +// lg +export const Large: Story = { + args: { + size: "lg", + }, +}; + +// md +export const Medium: Story = { + args: { + size: "md", + }, +}; + +// sm +export const Small: Story = { + args: { + size: "sm", + }, +}; diff --git a/src/features/auth/ui/SignUpForm/SignUpForm.tsx b/src/features/auth/ui/SignUpForm/SignUpForm.tsx new file mode 100644 index 00000000..2c117c16 --- /dev/null +++ b/src/features/auth/ui/SignUpForm/SignUpForm.tsx @@ -0,0 +1,283 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Input } from "@/entities/user/ui/Input/Input"; +import { DropDown } from "@/shared/ui/DropDown/DropDown"; +import Button from "@/shared/ui/Button/Button"; +import { apiFetch } from "@/shared/api/fetcher"; + +type FormSize = "sm" | "md" | "lg"; + +interface SignUpFormProps { + size?: FormSize; + onSuccess?: () => void; + onError?: (msg: string) => void; +} + +export const SignUpForm = ({ + size = "lg", + onSuccess, + onError, +}: SignUpFormProps) => { + //input text 상태 + const [email, setEmail] = useState(""); + const [nickname, setNickname] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + // 생년월일 상태 + const [year, setYear] = useState(""); + const [month, setMonth] = useState(""); + const [day, setDay] = useState(""); + + // input error 상태 + const [emailError, setEmailError] = useState(null); + const [nicknameError, setNicknameError] = useState(null); + const [passwordError, setPasswordError] = useState(null); + const [confirmPasswordError, setConfirmPasswordError] = useState< + string | null + >(null); + + //이메일 유효성 검사 + const validateEmail = (value: string) => { + const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return regex.test(value); + }; + + //input value change 핸들러 + const handleEmailChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setEmail(value); + + if (value && !validateEmail(value)) { + setEmailError("올바른 이메일 형식이 아닙니다."); + } else { + setEmailError(null); + } + }; + + const handleNicknameChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setNickname(value); + + if (value.length > 10) { + setNicknameError("닉네임은 최대 10자 이하이어야 합니다."); + } else { + setNicknameError(null); + } + }; + + const handlePasswordChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setPassword(value); + + if (value && value.length < 8) { + setPasswordError("비밀번호는 최소 8자 이상이어야 합니다."); + } else { + setPasswordError(null); + } + }; + + const handleConfirmPasswordChange = ( + e: React.ChangeEvent, + ) => { + const value = e.target.value; + setConfirmPassword(value); + + if (password && value && password !== value) { + setConfirmPasswordError("비밀번호가 일치하지 않습니다."); + } else { + setConfirmPasswordError(null); + } + }; + + // password, confirmPassword 동기화 체크 + useEffect(() => { + if (password && confirmPassword && password !== confirmPassword) { + setConfirmPasswordError("비밀번호가 일치하지 않습니다."); + } else { + setConfirmPasswordError(null); + } + }, [password, confirmPassword]); + + //연, 월, 일 드롭다운 옵션 생성 + const yearOptions = Array.from({ length: 2025 - 1900 + 1 }, (_, i) => { + const y = 1900 + i; + return { label: `${y}년`, value: y }; + }); + + const monthOptions = () => { + if (!year) return []; + return Array.from({ length: 12 }, (_, i) => { + const m = i + 1; + return { label: `${m}월`, value: m }; + }); + }; + + const dayOptions = () => { + if (!year || !month) return []; + const daysInMonth = new Date(year, month, 0).getDate(); + return Array.from({ length: daysInMonth }, (_, i) => { + const d = i + 1; + return { label: `${d}일`, value: d }; + }); + }; + + // 회원가입 폼 제출 핸들러 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + console.log("회원가입 요청 폼 제출"); + const res = await apiFetch<{ + status: string; + }>("/health", { + method: "GET", + noAuth: true, + }); + console.log(res); + try { + const body = { + email, + nickname, + birthDate: `${year}-${String(month).padStart(2, "0")}-${String( + day, + ).padStart(2, "0")}`, + password, + }; + + console.log(body); + const res = await apiFetch<{ + userId: number; + email: string; + nickname: string; + birthDate: string; + createdAt: string; + updatedAt: string; + }>("/auth/signup", { + method: "POST", + body: JSON.stringify(body), + noAuth: true, + }); + + console.log("회원가입 성공:", res); + onSuccess?.(); + } catch (error: unknown) { + console.error("회원가입 실패:", error + "\n"); + if (error instanceof Error) { + // 서버 에러 메시지 처리 + if (error.message.includes("isEmailUsed")) { + setEmailError("이미 사용 중인 이메일입니다."); + } else if (error.message.includes("isNicknameUsed")) { + setNicknameError("이미 사용 중인 닉네임입니다."); + } else { + onError?.("회원가입에 실패했습니다. 잠시 후 다시 시도해주세요."); + } + } + } + }; + + return ( +
+ + + + + + + + + {/* 생년월일 DropDown */} +
+ +
+ { + setYear(val as number); + setMonth(""); + setDay(""); + }} + placeholder="연도" + /> + { + setMonth(val as number); + setDay(""); + }} + placeholder="월" + /> + setDay(val as number)} + placeholder="일" + /> +
+
+ + +
+ ); +};