feat: 로그인 버튼 생성
This commit is contained in:
131
package-lock.json
generated
131
package-lock.json
generated
@@ -8,15 +8,23 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-oauth/google": "^0.13.4",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-apple-signin-auth": "^1.1.2",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"react-facebook-login": "^4.1.1",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-facebook-login": "^4.1.11",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
@@ -67,7 +75,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -310,6 +317,13 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
@@ -1227,6 +1241,16 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-oauth/google": {
|
||||||
|
"version": "0.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz",
|
||||||
|
"integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -1532,6 +1556,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
@@ -1562,7 +1593,6 @@
|
|||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1577,6 +1607,16 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-facebook-login": {
|
||||||
|
"version": "4.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-facebook-login/-/react-facebook-login-4.1.11.tgz",
|
||||||
|
"integrity": "sha512-7S/qzQMrS/zyJupX1RTiP9YV8qA0RyjS+up19G28XM6C9IN3NYfkrbKJEHOAfc79HdFYgpt9qQd1IHNZxuH+eA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.1",
|
"version": "8.56.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
@@ -1622,7 +1662,6 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -2148,7 +2187,6 @@
|
|||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2492,7 +2530,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2644,6 +2681,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3060,7 +3115,6 @@
|
|||||||
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3246,7 +3300,6 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -4441,6 +4494,15 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -5444,17 +5506,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-apple-signin-auth": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-apple-signin-auth/-/react-apple-signin-auth-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E5bPu4LtNR3IDsd08A/f1Y0HyuHfjqQpRNRCtQQ3JSVby2JK50FoixyK8EwUh6cbu8N4qrJStL77dEb51Ny5uA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0",
|
||||||
|
"react-dom": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -5462,6 +5532,15 @@
|
|||||||
"react": "^19.2.3"
|
"react": "^19.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-facebook-login": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-facebook-login/-/react-facebook-login-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-COnHEHlYGTKipz4963safFAK9PaNTcCiXfPXMS/yxo8El+/AJL5ye8kMJf23lKSSGGPgqFQuInskIHVqGqTvSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@@ -6143,7 +6222,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -6306,7 +6384,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -6582,7 +6659,6 @@
|
|||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -6599,6 +6675,35 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -6,18 +6,27 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"local": "cross-env NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 next dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-oauth/google": "^0.13.4",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-apple-signin-auth": "^1.1.2",
|
||||||
|
"react-dom": "19.2.3",
|
||||||
|
"react-facebook-login": "^4.1.1",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/react-facebook-login": "^4.1.11",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
'use client'; // 클라이언트 사이드 이벤트(onClick) 및 훅(useRouter)을 사용하므로 필수
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSocialLogin } from '../hooks/useSocialLogin';
|
import { useSocialLogin } from '../hooks/useSocialLogin';
|
||||||
|
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||||
|
import FacebookLogin from 'react-facebook-login/dist/facebook-login-render-props';
|
||||||
|
|
||||||
|
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '';
|
||||||
|
const FACEBOOK_APP_ID = process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 화면 UI (View)
|
* 실제 버튼들을 렌더링하고 커스텀 훅(useSocialLogin)을 호출하는 내부 컴포넌트입니다.
|
||||||
* 비즈니스 로직(토큰 교환 등)을 전혀 모른 채, 오직 버튼만 그리고 이벤트를 훅(Hook)으로 위임합니다.
|
* 이 컴포넌트는 반드시 GoogleOAuthProvider의 자식으로 존재해야 합니다.
|
||||||
*/
|
*/
|
||||||
export const SocialLoginGroup = () => {
|
const SocialLoginButtons = () => {
|
||||||
// 로직과 뷰의 완벽한 분리: useSocialLogin 커스텀 훅에서 필요한 함수만 꺼내옵니다.
|
const { loginWithGoogle, loginWithApple, handleFacebookCallback, isLoading, error } = useSocialLogin();
|
||||||
const { loginWithGoogle, loginWithApple, loginWithFacebook, isLoading, error } = useSocialLogin();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* 1. Google 로그인 (useGoogleLogin 훅 연동) */}
|
||||||
{/* 1. Google 로그인 */}
|
|
||||||
<button
|
<button
|
||||||
onClick={loginWithGoogle}
|
onClick={() => loginWithGoogle()}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-full flex items-center justify-center gap-3 bg-white border border-brand-dark/20 text-brand-dark px-6 py-3.5 rounded-xl font-bold hover:bg-slate-50 transition-colors shadow-sm disabled:opacity-50"
|
className="w-full flex items-center justify-center gap-3 bg-white border border-brand-dark/20 text-brand-dark px-6 py-3.5 rounded-xl font-bold hover:bg-slate-50 transition-colors shadow-sm disabled:opacity-50"
|
||||||
>
|
>
|
||||||
@@ -29,7 +32,7 @@ export const SocialLoginGroup = () => {
|
|||||||
{isLoading ? '연결 중...' : 'Google로 계속하기'}
|
{isLoading ? '연결 중...' : 'Google로 계속하기'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 2. Apple 로그인 */}
|
{/* 2. Apple 로그인 (react-apple-signin-auth 연동) */}
|
||||||
<button
|
<button
|
||||||
onClick={loginWithApple}
|
onClick={loginWithApple}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@@ -41,17 +44,23 @@ export const SocialLoginGroup = () => {
|
|||||||
{isLoading ? '연결 중...' : 'Apple로 계속하기'}
|
{isLoading ? '연결 중...' : 'Apple로 계속하기'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 3. Facebook 로그인 */}
|
{/* 3. Facebook 로그인 (react-facebook-login Render Props 연동) */}
|
||||||
<button
|
<FacebookLogin
|
||||||
onClick={loginWithFacebook}
|
appId={FACEBOOK_APP_ID}
|
||||||
disabled={isLoading}
|
callback={handleFacebookCallback}
|
||||||
className="w-full flex items-center justify-center gap-3 bg-[#1877F2] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-[#1864D9] transition-colors shadow-sm disabled:opacity-50"
|
render={(renderProps: any) => (
|
||||||
>
|
<button
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
onClick={renderProps.onClick}
|
||||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
disabled={isLoading || renderProps.isDisabled}
|
||||||
</svg>
|
className="w-full flex items-center justify-center gap-3 bg-[#1877F2] text-white px-6 py-3.5 rounded-xl font-bold hover:bg-[#1864D9] transition-colors shadow-sm disabled:opacity-50"
|
||||||
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
>
|
||||||
</button>
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||||
|
</svg>
|
||||||
|
{isLoading ? '연결 중...' : 'Facebook으로 계속하기'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 백엔드 연동 에러 메시지 표시 */}
|
{/* 백엔드 연동 에러 메시지 표시 */}
|
||||||
{error && (
|
{error && (
|
||||||
@@ -62,3 +71,15 @@ export const SocialLoginGroup = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인 화면 UI (View)
|
||||||
|
* GoogleOAuthProvider로 감싸서 내부에서 useGoogleLogin 훅을 사용할 수 있게 합니다.
|
||||||
|
*/
|
||||||
|
export const SocialLoginGroup = () => {
|
||||||
|
return (
|
||||||
|
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||||
|
<SocialLoginButtons />
|
||||||
|
</GoogleOAuthProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { authApi } from '../api/authApi';
|
import { authApi } from '../api/authApi';
|
||||||
|
import { useGoogleLogin } from '@react-oauth/google';
|
||||||
|
import appleAuthHelpers from 'react-apple-signin-auth';
|
||||||
|
import { useAuthStore } from '@/store/useAuthStore';
|
||||||
|
|
||||||
export const useSocialLogin = () => {
|
export const useSocialLogin = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -8,7 +11,8 @@ export const useSocialLogin = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [비즈니스 로직 1] 플랫폼별 소셜 SDK에서 받은 토큰을 백엔드로 보내어 VibeRoom JWT를 얻어오는 공통 함수
|
* [비즈니스 로직 1] 공통 토큰 교환 로직
|
||||||
|
* 플랫폼별 SDK에서 획득한 토큰을 백엔드로 보내어 VibeRoom 전용 JWT로 교환합니다.
|
||||||
*/
|
*/
|
||||||
const handleSocialLogin = async (provider: 'google' | 'apple' | 'facebook', socialToken: string) => {
|
const handleSocialLogin = async (provider: 'google' | 'apple' | 'facebook', socialToken: string) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -16,16 +20,14 @@ export const useSocialLogin = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
|
// 1. 백엔드로 소셜 토큰 전송 (토큰 교환)
|
||||||
const response = await authApi.loginWithSocial({
|
const response = await authApi.loginWithSocial({ provider, token: socialToken });
|
||||||
provider,
|
|
||||||
token: socialToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 응답받은 VibeRoom 전용 토큰을 로컬에 저장 (TODO: 실제로는 Zustand/Cookie에 저장)
|
|
||||||
console.log(`[${provider}] 백엔드 연동 성공! JWT:`, response.accessToken);
|
console.log(`[${provider}] 백엔드 연동 성공! JWT:`, response.accessToken);
|
||||||
// ex) useAuthStore.getState().setToken(response.accessToken);
|
|
||||||
|
// 2. 응답받은 VibeRoom 전용 토큰과 유저 정보를 전역 상태 및 쿠키에 저장
|
||||||
// 3. 성공 후 메인 대시보드 화면으로 이동
|
useAuthStore.getState().setAuth(response);
|
||||||
|
|
||||||
|
// 3. 메인 대시보드 화면으로 이동
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[${provider}] 로그인 실패:`, err);
|
console.error(`[${provider}] 로그인 실패:`, err);
|
||||||
@@ -36,32 +38,63 @@ export const useSocialLogin = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [비즈니스 로직 2] UI 컴포넌트(View)에서 호출할 개별 플랫폼 로그인 함수들
|
* [비즈니스 로직 2] Google Login
|
||||||
* 향후 여기에 구글/애플/페이스북의 실제 프론트엔드 SDK 코드가 들어갑니다.
|
* @react-oauth/google 라이브러리의 훅을 사용하여 구글 로그인 팝업을 호출합니다.
|
||||||
*/
|
*/
|
||||||
const loginWithGoogle = async () => {
|
const loginWithGoogle = useGoogleLogin({
|
||||||
// TODO: Google Identity Services(GSI) SDK 호출 코드 작성
|
onSuccess: (tokenResponse) => {
|
||||||
const mockGoogleToken = 'mock_google_token_123';
|
handleSocialLogin('google', tokenResponse.access_token);
|
||||||
await handleSocialLogin('google', mockGoogleToken);
|
},
|
||||||
|
onError: () => {
|
||||||
|
setError('구글 로그인에 실패했습니다. 팝업 차단 여부를 확인해 주세요.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [비즈니스 로직 3] Apple Login
|
||||||
|
* react-apple-signin-auth 라이브러리를 사용하여 애플 로그인을 팝업으로 호출합니다.
|
||||||
|
*/
|
||||||
|
const loginWithApple = () => {
|
||||||
|
try {
|
||||||
|
appleAuthHelpers.signIn({
|
||||||
|
authOptions: {
|
||||||
|
clientId: process.env.NEXT_PUBLIC_APPLE_CLIENT_ID || '',
|
||||||
|
scope: 'email name',
|
||||||
|
redirectURI: window.location.origin, // Apple 요구사항: 현재 도메인 입력 필요
|
||||||
|
usePopup: true,
|
||||||
|
},
|
||||||
|
onSuccess: (response: any) => {
|
||||||
|
if (response.authorization?.id_token) {
|
||||||
|
handleSocialLogin('apple', response.authorization.id_token);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err: any) => {
|
||||||
|
console.error('Apple SignIn error:', err);
|
||||||
|
setError('애플 로그인 중 오류가 발생했습니다.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('애플 로그인 초기화 실패');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginWithApple = async () => {
|
/**
|
||||||
// TODO: Sign in with Apple JS 호출 코드 작성
|
* [비즈니스 로직 4] Facebook Callback
|
||||||
const mockAppleToken = 'mock_apple_token_456';
|
* react-facebook-login 컴포넌트에서 콜백으로 받은 토큰을 처리합니다.
|
||||||
await handleSocialLogin('apple', mockAppleToken);
|
*/
|
||||||
|
const handleFacebookCallback = (response: any) => {
|
||||||
|
if (response?.accessToken) {
|
||||||
|
handleSocialLogin('facebook', response.accessToken);
|
||||||
|
} else {
|
||||||
|
setError('페이스북 로그인에 실패했습니다.');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginWithFacebook = async () => {
|
|
||||||
// TODO: Facebook Login SDK 호출 코드 작성
|
|
||||||
const mockFacebookToken = 'mock_facebook_token_789';
|
|
||||||
await handleSocialLogin('facebook', mockFacebookToken);
|
|
||||||
};
|
|
||||||
|
|
||||||
// UI 컴포넌트에 노출할 상태와 함수들만 캡슐화하여 반환
|
|
||||||
return {
|
return {
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
loginWithApple,
|
loginWithApple,
|
||||||
loginWithFacebook,
|
handleFacebookCallback,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
|||||||
62
src/store/useAuthStore.ts
Normal file
62
src/store/useAuthStore.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { AuthResponse } from '@/features/auth/types';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
accessToken: string | null;
|
||||||
|
user: AuthResponse['user'] | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
|
||||||
|
// 액션
|
||||||
|
setAuth: (data: AuthResponse) => void;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 쿠키 키 상수 정의 (Access Token 보관용)
|
||||||
|
const TOKEN_COOKIE_KEY = 'vr_access_token';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VibeRoom 전역 인증(Auth) 상태 저장소
|
||||||
|
*
|
||||||
|
* Zustand를 활용하여 메모리에 상태를 쥐고 있으면서,
|
||||||
|
* 쿠키(js-cookie)를 통해 브라우저 종료 후에도 세션이 유지되도록 동기화합니다.
|
||||||
|
*/
|
||||||
|
export const useAuthStore = create<AuthState>((set) => {
|
||||||
|
// 브라우저 환경에서만 쿠키 접근
|
||||||
|
const isClient = typeof window !== 'undefined';
|
||||||
|
const savedToken = isClient ? Cookies.get(TOKEN_COOKIE_KEY) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: savedToken || null,
|
||||||
|
user: null, // 새로고침 시 이 토큰을 기반으로 유저 정보(me API)를 다시 가져와야 합니다(Hydrate).
|
||||||
|
isAuthenticated: !!savedToken,
|
||||||
|
|
||||||
|
setAuth: (data: AuthResponse) => {
|
||||||
|
// 1. 브라우저 쿠키에 저장 (보안 옵션 설정)
|
||||||
|
Cookies.set(TOKEN_COOKIE_KEY, data.accessToken, {
|
||||||
|
expires: 7, // 7일 후 만료
|
||||||
|
secure: process.env.NODE_ENV === 'production', // HTTPS 환경에서만 전송
|
||||||
|
sameSite: 'strict' // CSRF 공격 방어
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Zustand 메모리 상태 업데이트 (연결된 UI 전체 리렌더링)
|
||||||
|
set({
|
||||||
|
accessToken: data.accessToken,
|
||||||
|
user: data.user,
|
||||||
|
isAuthenticated: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
// 1. 쿠키에서 토큰 삭제
|
||||||
|
Cookies.remove(TOKEN_COOKIE_KEY);
|
||||||
|
|
||||||
|
// 2. Zustand 상태 초기화
|
||||||
|
set({
|
||||||
|
accessToken: null,
|
||||||
|
user: null,
|
||||||
|
isAuthenticated: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user