-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/101/login signup api #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Walkthrough์ด๋ฒ ๋ณ๊ฒฝ ์ฌํญ์ Next.js ์ ํ๋ฆฌ์ผ์ด์
์ ๋ผ์ฐํ
๋ฐ ์ธ์ฆ ํ๋ฆ์ ๊ฐ์ ํ๋ ์ฌ๋ฌ ๊ฐ์ง ๊ธฐ๋ฅ์ ํฌํจํฉ๋๋ค. Changes
Suggested labels
Suggested reviewers
Warning There were issues while running some tools. Please review the errors and either fix the toolโs configuration or disable the tool if itโs a critical failure. ๐ง eslint
src/_queries/auth/auth-queries.tsxOops! Something went wrong! :( ESLint: 8.57.1 ESLint couldn't determine the plugin "react-hooks" uniquely.
Please remove the "plugins" setting from either config or remove either plugin installation. If you still can't figure out the problem, please stop by https://eslint.org/chat/help to chat with the team. Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? ๐ชง TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
๐storybook: https://67206cc6ff9d7a05a3528ff8-hkxashooej.chromatic.com/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
๐งน Outside diff range and nitpick comments (6)
src/types/auth.ts (1)
Line range hint
1-24: ์ธํฐํ์ด์ค์ JSDoc ์ฃผ์ ์ถ๊ฐ ๊ถ์ฅ๊ฐ ์ธํฐํ์ด์ค์ ๋ชฉ์ ๊ณผ ์์ฑ์ ๋ํ ์ค๋ช ์ JSDoc ํ์์ผ๋ก ์ถ๊ฐํ๋ฉด ์ฝ๋์ ๊ฐ๋ ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ด ํฅ์๋ ๊ฒ ๊ฐ์ต๋๋ค.
์์:
+/** + * ํ์๊ฐ์ ์๋ต ์ธํฐํ์ด์ค + * @property {string | null} token - ์ธ์ฆ ์ฑ๊ณต ์ JWT ํ ํฐ, ์คํจ ์ null + */ export interface SignupResponse { token: string | null; } +/** + * ํ์๊ฐ์ ์์ฒญ ์ธํฐํ์ด์ค + * @property {string} nickname - ์ฌ์ฉ์ ๋๋ค์ + * @property {string} email - ์ฌ์ฉ์ ์ด๋ฉ์ผ + * @property {string} password - ์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ + */ export interface SignupRequest { nickname: string; email: string; password: string; }src/store/use-auth-store.tsx (1)
20-24: ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ ์ถ๊ฐ๋ฅผ ๊ณ ๋ คํด์ฃผ์ธ์ํ์ฌ ๊ตฌํ์ ๊ธฐ๋ฅ์ ์ผ๋ก๋ ๋ฌธ์ ๊ฐ ์์ผ๋, ๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ์ฌํญ์ ์ ์๋๋ฆฝ๋๋ค:
- login: (token) => + login: (token: string) => + if (!token) { + throw new Error('ํ ํฐ์ด ํ์ํฉ๋๋ค'); + } set({ isAuth: true, token, }),๋ํ
setUser๋ฉ์๋์๋ ์ ์ฌํ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค:- setUser: (user: User) => set((state) => ({ ...state, user })), + setUser: (user: User) => { + if (!user) { + throw new Error('์ฌ์ฉ์ ์ ๋ณด๊ฐ ํ์ํฉ๋๋ค'); + } + set((state) => ({ ...state, user })); + },Also applies to: 31-31
src/app/(auth)/signup/page.tsx (2)
Line range hint
11-11: ์ปดํฌ๋ํธ ์ด๋ฆ์ด ๊ธฐ๋ฅ๊ณผ ์ผ์นํ์ง ์์ต๋๋คํ์๊ฐ์ ํ์ด์ง๋ฅผ ๋ด๋นํ๋ ์ปดํฌ๋ํธ์ ์ด๋ฆ์ด
LoginPage๋ก ๋์ด์์ด ํผ๋์ ์ผ๊ธฐํ ์ ์์ต๋๋ค. ์ปดํฌ๋ํธ์ ์ค์ ๊ธฐ๋ฅ๊ณผ ์ผ์นํ๋๋ก ์ด๋ฆ์ ๋ณ๊ฒฝํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ ๊ฒ์ ์ ์ํฉ๋๋ค:
-export default function LoginPage() { +export default function SignupPage() {
19-23: ์ฑ๊ณต ์ฒ๋ฆฌ ๋ก์ง ๊ฒํ ๊ฐ ํ์ํฉ๋๋คํ์๊ฐ์ ์ฑ๊ณต ์ ๋ฐ๋ก ๋ฉ์ธ ํ์ด์ง('/')๋ก ์ด๋ํ๊ธฐ ์ ์ ์ฌ์ฉ์์๊ฒ ์ฑ๊ณต ๋ฉ์์ง๋ฅผ ํ์ํ๊ฑฐ๋, ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ์ ํ๋ ๊ฒ์ด ๋ ๋์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ ์ ์์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ์ ์ ์ํฉ๋๋ค:
postSignup(requestData, { onSuccess: () => { - router.push('/'); + // ํ ์คํธ ๋ฉ์์ง ํ์ + toast.success('ํ์๊ฐ์ ์ด ์๋ฃ๋์์ต๋๋ค. ๋ก๊ทธ์ธํด ์ฃผ์ธ์.'); + // ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ์ + router.push('/login'); }, onError: (error) => {src/utils/api.ts (2)
4-14: ApiError ํด๋์ค ๊ตฌํ์ด ์ ๋์์ต๋๋ค!validation ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๊ตฌ์กฐ๊ฐ ์ ์ค๊ณ๋์์ต๋๋ค. ๋ค๋ง, ํด๋์ค์ ๋ฉ์๋์ ๋ํ JSDoc ๋ฌธ์ํ๋ฅผ ์ถ๊ฐํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ๋ฌธ์ํ๋ฅผ ์ถ๊ฐํด๋ณด์ธ์:
+/** + * API ์์ฒญ ์ค ๋ฐ์ํ๋ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ํด๋์ค + * @property {number} status - HTTP ์ํ ์ฝ๋ + * @property {Object} detail - ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์์ธ ์ ๋ณด + */ export class ApiError extends Error { detail: { validationErrors: Record<string, string> } = { validationErrors: {} }; + /** + * @param {number} status - HTTP ์ํ ์ฝ๋ + * @param {string} message - ์๋ฌ ๋ฉ์์ง + * @param {Object} detail - ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์์ธ ์ ๋ณด + */ constructor( public status: number, message: string, detail?: { validationErrors: Record<string, string> }, ) {
38-54: ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง์ด ๊ฐ์ ๋์์ต๋๋ค๋ง, ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ ๋ช ํํ๊ฒ ๋ง๋ค์ด๋ณด์ธ์.์ ๋ฐ์ ์ธ ๊ตฌํ์ด ์ ๋์์ต๋๋ค. ๋ค๋ง, ์๋ฌ ๋ฉ์์ง ์ฒ๋ฆฌ๋ฅผ ๋ ๊ตฌ์ฒด์ ์ผ๋ก ๊ฐ์ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํด๋ณด์ธ์:
try { const { status, message, ...detail } = await response.json(); - errorMessage = message || `HTTP error! status: ${response.status}`; + errorMessage = message || `์๋ฒ ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค. (์ํ ์ฝ๋: ${response.status})`; errorDetail = detail; } catch { - errorMessage = `HTTP error! status: ${response.status}`; + errorMessage = `์๋ฒ ์๋ต์ ์ฒ๋ฆฌํ๋ ์ค ์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค. (์ํ ์ฝ๋: ${response.status})`; }
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
๐ Files selected for processing (8)
next.config.mjs(1 hunks)src/_apis/auth/auth-apis.tsx(1 hunks)src/_queries/auth/auth-queries.tsx(1 hunks)src/app/(auth)/login/page.tsx(1 hunks)src/app/(auth)/signup/page.tsx(1 hunks)src/store/use-auth-store.tsx(3 hunks)src/types/auth.ts(2 hunks)src/utils/api.ts(2 hunks)
๐ Additional comments (12)
src/types/auth.ts (1)
2-2: ํ ํฐ ํ์
๋ณ๊ฒฝ์ด ์ ์ ํฉ๋๋ค
string | null ํ์
์ผ๋ก ๋ณ๊ฒฝํ ๊ฒ์ ์ธ์ฆ ์คํจ ์๋๋ฆฌ์ค๋ฅผ ๋ ๋ช
ํํ๊ฒ ์ฒ๋ฆฌํ ์ ์๊ฒ ํด์ฃผ๋ฉฐ, API ์๋ต ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํ๋๋ฐ ๋์์ด ๋ฉ๋๋ค.
Also applies to: 12-12
src/store/use-auth-store.tsx (2)
9-9: ์ธํฐํ์ด์ค ๋ณ๊ฒฝ์ด ์ ์ด๋ฃจ์ด์ก์ต๋๋ค!
ํ ํฐ๊ณผ ์ฌ์ฉ์ ์ ๋ณด ๊ด๋ฆฌ๋ฅผ ๋ถ๋ฆฌํ ๊ฒ์ ์ข์ ์ค๊ณ ๊ฒฐ์ ์ ๋๋ค. ์ด๋ฅผ ํตํด:
- ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ๊ณผ ์ฌ์ฉ์ ์ ๋ณด ๊ด๋ฆฌ๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ์ํํ ์ ์์ต๋๋ค
- API ์๋ต ์ฒ๋ฆฌ ์ ๋ ์ ์ฐํ ์ํ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํด์ง๋๋ค
Also applies to: 11-11
Line range hint 34-41: ์๊ตฌ ์ ์ฅ์ ์ค์ ์ด ์ ์ ํฉ๋๋ค
์ธ์ ์คํ ๋ฆฌ์ง ์ฌ์ฉ๊ณผ ์ ์ฅํ ์ํ ํ๋ ์ ํ์ด ๋ณด์์ ์ผ๋ก ์ ์ ํฉ๋๋ค. ํนํ:
- ์ธ์ ์คํ ๋ฆฌ์ง๋ ์ธ์ฆ ๋ฐ์ดํฐ์ ์ ํฉํ ์ ํ์ ๋๋ค
- ํ์ํ ์ํ๋ง ์ ๋ณ์ ์ผ๋ก ์ ์ฅ๋๊ณ ์์ต๋๋ค
next.config.mjs (2)
33-40: ๋ณด์ ๋ฐ ๊ตฌ์ฑ ๊ด๋ จ ๊ฒํ ํ์
๋ชจ๋ ๊ฒฝ๋ก๋ฅผ ์ธ๋ถ API๋ก ๋ฆฌ๋ค์ด๋ ์ ํ๋ ํ์ฌ ๊ตฌํ ๋ฐฉ์์ ๋ํด ๋ช ๊ฐ์ง ๊ณ ๋ ค์ฌํญ์ด ์์ต๋๋ค:
/:path*ํจํด์ ๋ชจ๋ ์์ฒญ์ ์ธ๋ถ API๋ก ์ ๋ฌํ๋ฏ๋ก ๋ณด์์ ์ํํ ์ ์์ต๋๋ค.- ํ๊ฒฝ ๋ณ์๊ฐ ์์ ๊ฒฝ์ฐ์ ๋์ฒด ๋ก์ง(fallback)์ด ํ์ํฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ์ ์ ์๋๋ฆฝ๋๋ค:
async rewrites() {
+ const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
+ if (!apiBaseUrl) {
+ throw new Error('NEXT_PUBLIC_API_BASE_URL is not defined');
+ }
return [
{
- source: '/:path*',
+ source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`,
},
];
},33-40: CORS ์ค์ ๊ณผ์ ์ฐ๊ด์ฑ ๊ฒํ ํ์
ํ์ฌ headers()์ rewrites() ๋ฉ์๋๊ฐ ๋์ผํ ํ๊ฒฝ ๋ณ์๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. CORS ์ค์ ์ด ๋ฆฌ๋ค์ด๋ ์
๋ ์์ฒญ์๋ ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉ๋๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค.
src/_apis/auth/auth-apis.tsx (1)
Line range hint 31-36: ์ธ์ฆ ํ ํฐ์ด ์์ฒญ ํค๋์ ํฌํจ๋์ด์ผ ํฉ๋๋ค
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ ๋ณดํธ๋ ์๋ํฌ์ธํธ์ ์ ๊ทผํ๊ธฐ ์ํด์๋ ์ธ์ฆ ํ ํฐ์ด ํ์ํฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ์์ ์ ์ ์ํฉ๋๋ค:
return fetchApi<{ data: User }>('/auths/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`, // ์ ์ฅ๋ ํ ํฐ์ ์ฌ์ฉ
},
});src/app/(auth)/login/page.tsx (2)
22-28: ์๋ฒ ์๋ต ๊ตฌ์กฐ ๊ฒ์ฆ ํ์
ํ์ฌ ๊ตฌํ์ ์๋ฒ์์ ํน์ ํํ์ ์๋ฌ ์๋ต์ ๊ฐ์ ํ๊ณ ์์ต๋๋ค. ์๋ฒ ์๋ต ๊ตฌ์กฐ๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ํด๋ผ์ด์ธํธ ์ฝ๋๊ฐ ๊นจ์ง ์ ์์ต๋๋ค.
์๋ฒ์ ์๋ฌ ์๋ต ๊ตฌ์กฐ๋ฅผ ํ์ธํ๊ธฐ ์ํด ๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํด์ฃผ์ธ์:
#!/bin/bash
# Description: API ์๋ฌ ์๋ต ๊ตฌ์กฐ ํ์ธ
# API ๊ด๋ จ ํ์
์ ์ ๊ฒ์
rg -t typescript "interface.*ApiError|type.*ApiError"
# ์๋ฌ ์ฒ๋ฆฌ ๊ด๋ จ ์ ํธ๋ฆฌํฐ ํจ์ ๊ฒ์
rg -t typescript "function.*handleError|function.*createError"22-28: ์ผ๊ด๋ ์๋ฌ ์ฒ๋ฆฌ ํจํด ์ ์ฉ ํ์
๋ก๊ทธ์ธ ํ์ด์ง์ ์๋ฌ ์ฒ๋ฆฌ ํจํด์ด ๋ค๋ฅธ ์ธ์ฆ ๊ด๋ จ ํ์ด์ง๋ค๊ณผ ์ผ๊ด์ฑ์ด ์๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค.
๋ค๋ฅธ ์ธ์ฆ ํ์ด์ง๋ค์ ์๋ฌ ์ฒ๋ฆฌ ํจํด์ ํ์ธํ๊ธฐ ์ํด ๋ค์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํด์ฃผ์ธ์:
src/app/(auth)/signup/page.tsx (1)
17-18: ๋ฐ์ดํฐ ๊ตฌ์กฐ ๊ฐ์ ์ด ์ ์ด๋ฃจ์ด์ก์ต๋๋ค
confirmPassword๋ฅผ API ์์ฒญ ๋ฐ์ดํฐ์์ ์ ์ธํ๋ ์ฒ๋ฆฌ๊ฐ ๊น๋ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค. ์คํ๋ ๋ ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ๊ตฌ์กฐ ๋ถํด๊ฐ ๊ฐ๋
์ฑ์ด ์ข์ต๋๋ค.
src/_queries/auth/auth-queries.tsx (2)
4-4: ์ปค์คํ
ApiError ํ์
์ผ๋ก์ ๋ง์ด๊ทธ๋ ์ด์
์ด ์ ์ ํฉ๋๋ค.
๋ก์ปฌ ์ ํธ๋ฆฌํฐ์์ ApiError๋ฅผ ์ํฌํธํ๋๋ก ๋ณ๊ฒฝํ ๊ฒ์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ๋ ์ฒด๊ณ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์๊ฒ ํด์ค๋๋ค.
10-10: ํ ํฐ ์ฒ๋ฆฌ ๋ฐ ์ํ ๊ด๋ฆฌ ๋ก์ง ๊ฒํ ๊ฐ ํ์ํฉ๋๋ค.
ํ ํฐ ์ฒ๋ฆฌ์ ์ฌ์ฉ์ ์ํ ๊ด๋ฆฌ๊ฐ ๋ถ๋ฆฌ๋์ด ๊ฐ์ ๋์์ผ๋, ๋ค์ ์ฌํญ๋ค์ ๊ณ ๋ คํด์ฃผ์ธ์:
- ํ ํฐ์ด ์๋ ๊ฒฝ์ฐ์ ์ฒ๋ฆฌ๊ฐ ํ์ํ ์ ์์ต๋๋ค
- ์ํ ์ ๋ฐ์ดํธ ์คํจ ์์ ์๋ฌ ์ฒ๋ฆฌ๊ฐ ํ์ํฉ๋๋ค
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค:
onSuccess: async (response) => {
const token = response.data.token?.replace(/^Bearer\s/, '');
- if (token) await login(token);
+ if (!token) {
+ throw new Error('ํ ํฐ์ด ์์ต๋๋ค');
+ }
+
+ try {
+ await login(token);
+ const user: User = await queryClient.fetchQuery(getUserQuery());
+ setUser(user);
+ } catch (error) {
+ console.error('์ฌ์ฉ์ ์ํ ์
๋ฐ์ดํธ ์คํจ:', error);
+ throw error;
+ }
- const user: User = await queryClient.fetchQuery(getUserQuery());
- setUser(user);
},Also applies to: 15-19
src/utils/api.ts (1)
38-38: API ๊ธฐ๋ณธ URL ์ค์ ์ด ์ ๊ฑฐ๋ ๊ฒ์ด ์๋๋ ๋ณ๊ฒฝ์ธ์ง ํ์ธํด์ฃผ์ธ์.
๊ธฐ์กด base URL ์ฐ๊ฒฐ์ด ์ ๊ฑฐ๋์์ต๋๋ค. ๋ชจ๋ API ํธ์ถ ๋ถ๋ถ์์ ์์ ํ URL์ ์ฌ์ฉํ๊ณ ์๋์ง ํ์ธ์ด ํ์ํฉ๋๋ค.
๋ค์ ์คํฌ๋ฆฝํธ๋ก API URL ์ฌ์ฉ์ ํ์ธํด๋ณด์ธ์:
โ Verification successful
Let me search for fetch calls and URL construction patterns to verify the API URL usage.
API URL ์ค์ ์ด ์๋์ ์ผ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค.
์ฝ๋ ๋ถ์ ๊ฒฐ๊ณผ, fetchApi ํจ์๋ ์๋ ๊ฒฝ๋ก๋ฅผ ์ฌ์ฉํ์ฌ API๋ฅผ ํธ์ถํ๊ณ ์์ผ๋ฉฐ, ๋ชจ๋ API ํธ์ถ์ด ์ผ๊ด๋ ๋ฐฉ์์ผ๋ก ๊ตฌํ๋์ด ์์ต๋๋ค. ๊ฐ API ์๋ํฌ์ธํธ(/auths/signup, /crews, /crewDetail ๋ฑ)๊ฐ ์๋ ๊ฒฝ๋ก๋ก ์ฌ๋ฐ๋ฅด๊ฒ ์ง์ ๋์ด ์์ด base URL ์ค์ ์ ๊ฑฐ๋ ์๋๋ ๋ณ๊ฒฝ์ผ๋ก ๋ณด์
๋๋ค.
๐ Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# API ํธ์ถ ํจํด ๊ฒ์
rg "fetchApi\(" -A 2
Length of output: 22
Script:
#!/bin/bash
# API URL ํจํด ๊ฒ์
rg "fetch\(" -A 2
# URL ๋ฌธ์์ด ํจํด ๊ฒ์
rg "const url = " -A 1
Length of output: 736
Script:
#!/bin/bash
# API URL ๊ด๋ จ ์ฝ๋ ๊ฒ์
rg -B 3 -A 3 "const response = await fetch"
# API ๊ด๋ จ ์ค์ ํ์ผ ๊ฒ์
fd -e ts -e js -e json "config|env|constant" --exec cat {}
Length of output: 8314
Script:
#!/bin/bash
# API ๊ด๋ จ ์ค์ ๋ฐ ๊ตฌํ ํ์ผ ๊ฒ์
cat src/utils/api.ts
# fetchApi ํจ์ ์ฌ์ฉ ํจํด ๊ฒ์
rg "fetchApi<" -B 2 -A 2
Length of output: 4847
| return fetchApi<{ data: LoginResponse; headers: Headers }>('/auths/login', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(data), | ||
| }).then((response) => { | ||
| const token = response.headers.get('Authorization'); | ||
| return { data: { token } }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
์ฝ๋ ์ค๋ณต์ ์ ๊ฑฐํ๊ณ ์ค๋ฅ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํด์ผ ํฉ๋๋ค
loginUser์ signupUser ํจ์๊ฐ ๋งค์ฐ ์ ์ฌํ ๋ก์ง์ ๊ฐ์ง๊ณ ์์ต๋๋ค. ๋ํ ํ ํฐ ์ถ์ถ ์ ์ค๋ฅ ์ฒ๋ฆฌ๊ฐ ๋๋ฝ๋์ด ์์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ ๊ฐ์ ์ ์ ์ํฉ๋๋ค:
- ํ ํฐ ์ถ์ถ ๋ก์ง์ ๊ณตํต ํจ์๋ก ๋ถ๋ฆฌ:
const extractAuthToken = (headers: Headers) => {
const token = headers.get('Authorization');
if (!token) {
throw new Error('์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค');
}
return token;
};- ๊ฐ ํจ์์์ ๊ณตํต ํจ์ ์ฌ์ฉ:
}).then((response) => {
- const token = response.headers.get('Authorization');
+ const token = extractAuthToken(response.headers);
return { data: { token } };
});| return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(data), | ||
| }).then((response) => { | ||
| const token = response.headers.get('Authorization'); | ||
| return { data: { token } }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํ ํฐ ์ถ์ถ ์ ์ค๋ฅ ์ฒ๋ฆฌ ๋ก์ง์ด ํ์ํฉ๋๋ค
ํ์ฌ ๊ตฌํ์์๋ ๋ค์๊ณผ ๊ฐ์ ์ ์ฌ์ ์ธ ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
- Authorization ํค๋๊ฐ ์๋ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค
- ์๋ต ๋ฐ์ดํฐ์ ์ ํจ์ฑ ๊ฒ์ฆ์ด ์์ต๋๋ค
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํ๋ ๊ฒ์ ์ ์ํฉ๋๋ค:
}).then((response) => {
const token = response.headers.get('Authorization');
+ if (!token) {
+ throw new Error('์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค');
+ }
return { data: { token } };
});๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(data), | |
| }).then((response) => { | |
| const token = response.headers.get('Authorization'); | |
| return { data: { token } }; | |
| return fetchApi<{ data: SignupResponse; headers: Headers }>('/auths/signup', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(data), | |
| }).then((response) => { | |
| const token = response.headers.get('Authorization'); | |
| if (!token) { | |
| throw new Error('์ธ์ฆ ํ ํฐ์ด ์์ต๋๋ค'); | |
| } | |
| return { data: { token } }; |
src/app/(auth)/login/page.tsx
Outdated
| if (error.status === 401) { | ||
| const { validationErrors } = error.detail; | ||
| Object.keys(validationErrors).forEach((key) => { | ||
| setError(key as 'email' | 'password', { | ||
| type: 'manual', | ||
| message: validationErrors[key], | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
์๋ฌ ์ฒ๋ฆฌ ๊ตฌํ์ ๋ํ ๊ฐ์ ์ ์
ํ์ฌ ๊ตฌํ์ ๋ค์๊ณผ ๊ฐ์ ์ ์ฌ์ ์ธ ๋ฌธ์ ๊ฐ ์์ต๋๋ค:
error.detail์ด ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋คvalidationErrors๊ฐ ์๋ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ๋๋ฝ๋์์ต๋๋ค- ์์์น ๋ชปํ ํ๋๋ช ์ด ๋ค์ด์ฌ ๊ฒฝ์ฐ ํ์ ์์ ์ฑ์ด ๋ณด์ฅ๋์ง ์์ต๋๋ค
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค:
if (error.status === 401) {
- const { validationErrors } = error.detail;
- Object.keys(validationErrors).forEach((key) => {
- setError(key as 'email' | 'password', {
- type: 'manual',
- message: validationErrors[key],
- });
- });
+ const validationErrors = error.detail?.validationErrors;
+ if (validationErrors) {
+ (Object.entries(validationErrors) as [keyof LoginFormValues, string][]).forEach(
+ ([field, message]) => {
+ setError(field, {
+ type: 'manual',
+ message,
+ });
+ }
+ );
+ } else {
+ setError('password', {
+ type: 'manual',
+ message: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.',
+ });
+ }
}๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (error.status === 401) { | |
| const { validationErrors } = error.detail; | |
| Object.keys(validationErrors).forEach((key) => { | |
| setError(key as 'email' | 'password', { | |
| type: 'manual', | |
| message: validationErrors[key], | |
| }); | |
| if (error.status === 401) { | |
| const validationErrors = error.detail?.validationErrors; | |
| if (validationErrors) { | |
| (Object.entries(validationErrors) as [keyof LoginFormValues, string][]).forEach( | |
| ([field, message]) => { | |
| setError(field, { | |
| type: 'manual', | |
| message, | |
| }); | |
| } | |
| ); | |
| } else { | |
| setError('password', { | |
| type: 'manual', | |
| message: '๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.', | |
| }); | |
| } | |
| } |
| if (error.status === 400) { | ||
| const { validationErrors } = error.detail; | ||
| Object.keys(validationErrors).forEach((key) => { | ||
| setError(key as 'email', { | ||
| type: 'manual', | ||
| message: validationErrors[key], | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
์๋ฌ ์ฒ๋ฆฌ ๋ก์ง ๊ฐ์ ์ด ํ์ํฉ๋๋ค
ํ์ฌ ๊ตฌํ๋ ์๋ฌ ์ฒ๋ฆฌ์ ๋ช ๊ฐ์ง ๊ฐ์ ์ด ํ์ํฉ๋๋ค:
- ํ์
์์ ์ฑ:
key as 'email'ํ์ ๋จ์ธ์ ์ํํ ์ ์์ต๋๋ค - ์๋ฌ ๋ฉ์์ง ์ฒ๋ฆฌ: ์๋ฒ ์๋ต์ด ์์๊ณผ ๋ค๋ฅผ ๊ฒฝ์ฐ์ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค
- ์ฌ์ฉ์ ํผ๋๋ฐฑ: ์ผ๋ฐ์ ์ธ ์๋ฒ ์ค๋ฅ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ๋๋ฝ๋์์ต๋๋ค
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํ๋ ๊ฒ์ ์ ์ํฉ๋๋ค:
if (error.status === 400) {
const { validationErrors } = error.detail;
- Object.keys(validationErrors).forEach((key) => {
- setError(key as 'email', {
- type: 'manual',
- message: validationErrors[key],
- });
- });
+ try {
+ Object.entries(validationErrors).forEach(([key, message]) => {
+ if (key in formMethods.getValues()) {
+ setError(key as keyof SignupFormValues, {
+ type: 'manual',
+ message: String(message),
+ });
+ }
+ });
+ } catch (e) {
+ console.error('์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค:', e);
+ setError('root', {
+ type: 'manual',
+ message: 'ํ์๊ฐ์
์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.',
+ });
+ }
+} else {
+ setError('root', {
+ type: 'manual',
+ message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด ์ฃผ์ธ์.',
+ });
}๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (error.status === 400) { | |
| const { validationErrors } = error.detail; | |
| Object.keys(validationErrors).forEach((key) => { | |
| setError(key as 'email', { | |
| type: 'manual', | |
| message: validationErrors[key], | |
| }); | |
| }); | |
| if (error.status === 400) { | |
| const { validationErrors } = error.detail; | |
| try { | |
| Object.entries(validationErrors).forEach(([key, message]) => { | |
| if (key in formMethods.getValues()) { | |
| setError(key as keyof SignupFormValues, { | |
| type: 'manual', | |
| message: String(message), | |
| }); | |
| } | |
| }); | |
| } catch (e) { | |
| console.error('์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค:', e); | |
| setError('root', { | |
| type: 'manual', | |
| message: 'ํ์๊ฐ์ ์ฒ๋ฆฌ ์ค ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.', | |
| }); | |
| } | |
| } else { | |
| setError('root', { | |
| type: 'manual', | |
| message: '์๋ฒ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด ์ฃผ์ธ์.', | |
| }); | |
| } |
src/_queries/auth/auth-queries.tsx
Outdated
| export function usePostLoginQuery() { | ||
| const queryClient = useQueryClient(); | ||
| const { login } = useAuthStore(); | ||
| const { login, setUser } = useAuthStore(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
์ธ์ฆ ๋ก์ง ์ค๋ณต ๊ฐ์ ์ด ํ์ํฉ๋๋ค.
๋ก๊ทธ์ธ๊ณผ ํ์๊ฐ์ ์์ ๋์ผํ ํ ํฐ ์ฒ๋ฆฌ ๋ฐ ์ฌ์ฉ์ ์ํ ๊ด๋ฆฌ ๋ก์ง์ด ๋ฐ๋ณต๋ฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ณตํต ๋ก์ง์ ๋ถ๋ฆฌํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค:
// ๊ณตํต ์ ํธ๋ฆฌํฐ ํจ์
async function handleAuthSuccess(
response: { data: { token?: string } },
queryClient: QueryClient,
login: (token: string) => Promise<void>,
setUser: (user: User) => void
) {
const token = response.data.token?.replace(/^Bearer\s/, '');
if (!token) {
throw new Error('ํ ํฐ์ด ์์ต๋๋ค');
}
try {
await login(token);
const user: User = await queryClient.fetchQuery(getUserQuery());
setUser(user);
} catch (error) {
console.error('์ฌ์ฉ์ ์ํ ์
๋ฐ์ดํธ ์คํจ:', error);
throw error;
}
}
// ์ฌ์ฉ ์์
onSuccess: (response) => handleAuthSuccess(response, queryClient, login, setUser)Also applies to: 31-35
|
๐storybook: https://67206cc6ff9d7a05a3528ff8-vybrpdfwrb.chromatic.com/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
๐งน Outside diff range and nitpick comments (1)
src/app/(auth)/layout.tsx (1)
33-33: ์ปจํ ์ธ ์์ญ์ ๋ฐ์ํ ๋์์ธ์ด ๊ฐ์ ๋์์ต๋๋ค.๋๋น์ ์ฌ๋ฐฑ ์กฐ์ ์ด ์ ์ ํ๊ฒ ์ด๋ฃจ์ด์ก์ต๋๋ค. ๋ค๋ง, ์ฌ์ฉ์ ๊ฒฝํ์ ๋์ฑ ํฅ์์ํค๊ธฐ ์ํด ์์ ํ๋ฉด์์์ ์ฌ๋ฐฑ๋ ๊ณ ๋ คํด๋ณด์๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
-<div className="mt-6 w-full md:mt-12 md:w-2/3 lg:w-1/2">{children}</div> +<div className="mx-auto mt-6 w-full max-w-md md:mt-12 md:w-2/3 lg:w-1/2">{children}</div>
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
๐ Files selected for processing (1)
src/app/(auth)/layout.tsx(1 hunks)
๐ Additional comments (1)
src/app/(auth)/layout.tsx (1)
22-24: ์ด๋ฏธ์ง ์ปจํ
์ด๋์ ๋ฐ์ํ ๋ ์ด์์์ด ๊ฐ์ ๋์์ต๋๋ค.
์ค๊ฐ ํฌ๊ธฐ(md) ์ด์์ ํ๋ฉด์์ ์ผ๊ด๋ 50% ๋๋น๋ฅผ ์ ์งํ๋๋ก ๋ณ๊ฒฝ๋ ๊ฒ์ด ์ ์ ํด ๋ณด์ ๋๋ค.
src/app/(auth)/layout.tsx
Outdated
| <Image src={Auth} alt="auth" /> | ||
| </div> | ||
| <div className="flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center md:p-8 lg:w-1/3 lg:p-20"> | ||
| <div className="md:p-8lg:p-20 flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ํจ๋ฉ ํด๋์ค ๊ตฌ๋ฌธ ์ค๋ฅ๋ฅผ ์์ ํด์ผ ํฉ๋๋ค.
Tailwind CSS ํด๋์ค ์ฌ์ด์ ๊ณต๋ฐฑ์ด ๋๋ฝ๋์ด ์์ด ํฐ ํ๋ฉด์์ ํจ๋ฉ์ด ์ ์ฉ๋์ง ์์ต๋๋ค.
-<div className="md:p-8lg:p-20 flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center">
+<div className="md:p-8 lg:p-20 flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center">๐ Committable suggestion
โผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div className="md:p-8lg:p-20 flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center"> | |
| <div className="md:p-8 lg:p-20 flex h-full w-full flex-col items-center bg-white p-6 md:w-1/2 md:justify-center"> |
|
๐storybook: https://67206cc6ff9d7a05a3528ff8-bdrdxnootl.chromatic.com/ |
|
๐storybook: https://67206cc6ff9d7a05a3528ff8-nranmpgqjr.chromatic.com/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
๐งน Outside diff range and nitpick comments (2)
.github/workflows/ci.yml (1)
80-81: ํ๊ฒฝ ๋ณ์ ์ค์ ์ด ์ ์ ํ ๊ตฌ์ฑ๋์์ต๋๋ค.๋น๋ ๋จ๊ณ์์ API ๊ธฐ๋ณธ URL์ ํ๊ฒฝ ๋ณ์๋ก ์ค์ ํ ๊ฒ์ ์ ์ ํฉ๋๋ค. GitHub Secrets๋ฅผ ํตํด ์์ ํ๊ฒ ๊ด๋ฆฌ๋๊ณ ์์ต๋๋ค.
์ํฌํ๋ก์ฐ ํ์ผ์ ํ๊ฒฝ ๋ณ์์ ์ฉ๋์ ํ์์ ๋ํ ๊ฐ๋จํ ์ฃผ์์ ์ถ๊ฐํ๋ฉด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค:
env: + # API ์๋ํฌ์ธํธ์ ๊ธฐ๋ณธ URL (์: https://api.example.com) NEXT_PUBLIC_API_BASE_URL: ${{secrets.NEXT_PUBLIC_API_BASE_URL}}src/components/common/header/header.stories.tsx (1)
Line range hint
25-30: ํ ์คํธ ๋ฐ์ดํฐ ๊ด๋ฆฌ ๊ฐ์ ์ด ํ์ํฉ๋๋ค.ํ ์คํธ ๋ฐ์ดํฐ๊ฐ ์ปดํฌ๋ํธ ๋ด๋ถ์ ํ๋์ฝ๋ฉ๋์ด ์์ต๋๋ค. ์ด๋ ์ ์ง๋ณด์์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ ์ธก๋ฉด์์ ๊ฐ์ ์ด ํ์ํฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ๋ณ๋์ ํ ์คํธ ๋ฐ์ดํฐ ํ์ผ์ ์์ฑํ์ฌ ๊ด๋ฆฌํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค:
// src/__mocks__/auth-test-data.ts export const TEST_AUTH_DATA = { token: 'test token', user: { id: 1, nickname: 'ํฌ๋ฃจํฌ๋ฃจ', email: 'john@example.com', profileImageUrl: 'https://image.file', }, };
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
๐ Files selected for processing (5)
.github/workflows/ci.yml(1 hunks)src/_queries/auth/auth-queries.tsx(2 hunks)src/app/(auth)/layout.tsx(1 hunks)src/app/(auth)/login/page.tsx(1 hunks)src/components/common/header/header.stories.tsx(2 hunks)
๐ง Files skipped from review as they are similar to previous changes (3)
- src/_queries/auth/auth-queries.tsx
- src/app/(auth)/layout.tsx
- src/app/(auth)/login/page.tsx
| export default meta; | ||
| function Template() { | ||
| const { isAuth, login, logout } = useAuthStore(); | ||
| const { isAuth, login, logout, setUser } = useAuthStore(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์ธ์ฆ ๋ก์ง ๊ฐ์ ์ด ํ์ํฉ๋๋ค.
๋ก๊ทธ์ธ ํ๋ก์ธ์ค๊ฐ ๋ ๋จ๊ณ๋ก ๋ถ๋ฆฌ๋์ด ์์ด ์ ์ฌ์ ์ธ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. login๊ณผ setUser ํธ์ถ ์ฌ์ด์ ๊ฒฝ์ ์ํ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฉฐ, ์ด๋ ์ฌ์ฉ์ ์ํ๊ฐ ์ผ์์ ์ผ๋ก ๋ถ์ผ์นํ๋ ์ํฉ์ ์ด๋ํ ์ ์์ต๋๋ค.
๋ค์๊ณผ ๊ฐ์ด ๊ฐ์ ํ๋ ๊ฒ์ ์ ์๋๋ฆฝ๋๋ค:
- login(testToken);
- setUser(testUser);
+ await login(testToken);
+ await setUser(testUser);๋๋ ๋ ๋์ ๋ฐฉ๋ฒ์ผ๋ก, ์ธ์ฆ ๋ก์ง์ ํ๋์ ํจ์๋ก ํตํฉํ๋ ๊ฒ์ ๊ณ ๋ คํด๋ณด์ธ์:
- login(testToken);
- setUser(testUser);
+ await loginWithUser(testToken, testUser);Also applies to: 37-38
ChoYenii
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
์๊ณ ๋ง์ผ์ จ์ต๋๋ค!!
yulrang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๋์ ํ์ธํ์ต๋๋ค~ ์๊ณ ํ์
จ์ด์!!
env api ์๋ถ์ด๋๊ฑธ๋ก ํต์ผํฉ์๋น~~
๐ Issue Ticket
Ticket
โ๏ธ Description
๋ก๊ทธ์ธ/ํ์๊ฐ์ API ์ฐ๊ฒฐ
ํผ ์๋ฌ์ฒ๋ฆฌ
โ Checklist
PR
Test
Summary by CodeRabbit
New Features
setUser๋์ .Bug Fixes
Documentation
Chores