-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/markup/signup/DEVING-25 회원가입 페이지 개발 #25
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
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Walkthrough이번 PR은 로그인과 회원가입 관련 컴포넌트, 훅, API 및 유틸리티 모듈을 대대적으로 리팩토링 및 신규 추가하는 변경 사항입니다. 로그인 폼은 Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant LF as LoginForm
participant LH as useLoginForm
participant NR as useRouter
U->>LF: 로그인 정보 입력 후 제출
LF->>LH: handleSubmit() 호출
LH-->>LF: 로그인 처리 결과 반환
U->>LF: 회원가입 버튼 클릭
LF->>NR: router.push('/signup') 호출
sequenceDiagram
participant U as 사용자
participant SF as SignupForm
participant SS as useSignUpForm
participant API as 서버 API
U->>SF: 회원가입 정보 입력 및 제출
SF->>SS: onSubmit() 호출하여 검증
SS->>API: postSignup() API 호출
API-->>SS: 회원가입 처리 결과 반환
SS-->>SF: 결과에 따른 UI 업데이트
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
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 (
|
Codecov ReportAll modified and coverable lines are covered by tests ✅ |
CI Status Report검사 결과
✅ 모든 검사가 통과되었습니다. |
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: 14
🔭 Outside diff range comments (3)
src/components/common/Header.tsx (2)
25-31:⚠️ Potential issue사용자 정보와 로그아웃 처리 개선이 필요합니다.
하드코딩된 사용자 정보를 상태 관리로 대체하고, 로그아웃 기능을 구현해야 합니다.
const AfterLogin = () => { const router = useRouter(); + const { user, logout } = useAuth(); const menu = [ { label: '내 모임', onSelect: () => router.push('/my-meeting') }, { label: '마이페이지', onSelect: () => router.push('/my-page') }, - { label: '로그아웃', onSelect: () => console.log('로그아웃') }, + { label: '로그아웃', onSelect: () => logout() }, ];🧰 Tools
🪛 GitHub Check: check
[warning] 30-30:
Unexpected console statement
158-166: 🛠️ Refactor suggestion모바일 메뉴의 접근성 개선이 필요합니다.
모바일 메뉴에 ARIA 속성을 추가하고 키보드 네비게이션을 지원해야 합니다.
<div className={`fixed right-0 h-screen w-screen transform overflow-x-hidden bg-BG px-[24px] transition-transform duration-300 ease-in-out ${ isOpen ? 'translate-x-0' : 'translate-x-full' } lg:hidden`} + role="dialog" + aria-modal="true" + aria-label="모바일 메뉴" >src/hooks/useDebounde.ts (1)
1-38:⚠️ Potential issue파일 이름 오타를 수정해 주세요.
파일 이름이
useDebounde.ts로 되어 있는데, 올바른 철자는useDebounce.ts입니다.
🧹 Nitpick comments (22)
src/app/signup/page.tsx (1)
3-9: 인터페이스 필드에 대한 JSDoc 문서화 추천
ISignupFormData인터페이스의 각 필드에 대한 설명을 추가하면 더 명확한 문서화가 될 것 같습니다.다음과 같이 JSDoc을 추가해주세요:
export interface ISignupFormData { + /** 사용자 이름 */ name: string; + /** 이메일 주소 */ email: string; + /** 직무 포지션 */ position: string; + /** 비밀번호 */ password: string; + /** 비밀번호 확인 */ passwordCheck: string; }src/lib/axios/defaultConfig.ts (1)
5-12: 타임아웃 설정값 상수화 추천타임아웃 값을 상수로 분리하면 설정 관리가 더 용이할 것 같습니다.
다음과 같이 수정해주세요:
+const REQUEST_TIMEOUT_MS = 10000; + export const defaultConfig: AxiosRequestConfig = { baseURL, withCredentials: true, - timeout: 10000, + timeout: REQUEST_TIMEOUT_MS, headers: { 'Content-Type': 'application/json', }, };src/hooks/useReactQuery.tsx (1)
9-18: 설정값 상수화 및 영문 주석 변환 추천쿼리 클라이언트 설정값을 상수로 분리하고, 한글 주석을 영문으로 변환하면 좋을 것 같습니다.
다음과 같이 수정해주세요:
+const STALE_TIME_MS = 60 * 1000; +const GC_TIME_MS = 5 * 60 * 1000; + const queryClient = new QueryClient({ defaultOptions: { queries: { - staleTime: 60 * 1000, - gcTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, // 창을 다시 활성화할 때 자동으로 refetch 안 함 (옵션) - refetchOnReconnect: true, // 네트워크 재연결 시 refetch + staleTime: STALE_TIME_MS, + gcTime: GC_TIME_MS, + refetchOnWindowFocus: false, // Disable automatic refetch on window focus (optional) + refetchOnReconnect: true, // Enable refetch on network reconnection }, }, });src/lib/serverActions.ts (3)
5-8: 반환 타입을 명시적으로 지정하는 것이 좋습니다.타입 안전성을 높이기 위해 함수의 반환 타입을 명시적으로 지정하는 것이 좋습니다.
-export async function getAccessToken() { +export async function getAccessToken(): Promise<string | null> { const cookieStore = cookies(); return cookieStore.get('accessToken')?.value || null; }
10-13: 쿠키 삭제 실패에 대한 예외 처리가 필요합니다.쿠키 삭제 작업이 실패할 경우를 대비한 예외 처리를 추가하는 것이 좋습니다.
export async function removeAccessToken() { const cookieStore = cookies(); - cookieStore.delete('accessToken'); + try { + cookieStore.delete('accessToken'); + } catch (error) { + console.error('액세스 토큰 삭제 중 오류 발생:', error); + throw new Error('액세스 토큰 삭제 실패'); + } }
15-23: 상수 분리 및 예외 처리가 필요합니다.
- 매직 넘버(24시간)를 상수로 분리
- 쿠키 설정 실패에 대한 예외 처리 추가
+const ONE_DAY_IN_SECONDS = 60 * 60 * 24; + export async function setAccessToken(token: string) { const cookieStore = cookies(); - cookieStore.set('accessToken', token, { - httpOnly: true, - sameSite: 'strict', - path: '/', - maxAge: 60 * 60 * 24, - }); + try { + cookieStore.set('accessToken', token, { + httpOnly: true, + sameSite: 'strict', + path: '/', + maxAge: ONE_DAY_IN_SECONDS, + }); + } catch (error) { + console.error('액세스 토큰 설정 중 오류 발생:', error); + throw new Error('액세스 토큰 설정 실패'); + } }src/app/signup/components/ChipContainer.tsx (2)
3-9: 타입 안전성 개선이 필요합니다.position 값의 타입을 문자열 리터럴 유니온 타입으로 정의하여 타입 안전성을 높이는 것이 좋습니다.
+type Position = 'Frontend' | 'Backend' | 'Designer'; + export const ChipContainer = ({ position, setPosition, }: { - position: string; - setPosition: (value: string) => void; + position: Position; + setPosition: (value: Position) => void; }) => {
11-33: 상수 분리 및 반복 코드 개선이 필요합니다.
- 포지션 데이터를 상수로 분리
- 반복되는 Chip 컴포넌트를 map으로 리팩토링
+const POSITIONS = [ + { value: 'Frontend', label: '프론트엔드' }, + { value: 'Backend', label: '백엔드' }, + { value: 'Designer', label: '디자이너' }, +] as const; + return ( <div className="flex w-full gap-[8px]"> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Frontend'} - onClick={() => setPosition('Frontend')} - > - 프론트엔드 - </Chip> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Backend'} - onClick={() => setPosition('Backend')} - > - 백엔드 - </Chip> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Designer'} - onClick={() => setPosition('Designer')} - > - 디자이너 - </Chip> + {POSITIONS.map(({ value, label }) => ( + <Chip + key={value} + className="flex-1 hover:cursor-pointer" + isActive={position === value} + onClick={() => setPosition(value)} + > + {label} + </Chip> + ))} </div> );src/app/preview/chip/page.tsx (1)
6-8: 타입 안전성 개선이 필요합니다.useState의 초기값 타입을 명시적으로 지정하는 것이 좋습니다.
+type Position = 'Frontend' | 'Backend' | 'Designer' | ''; + export default function ChipPreview() { - const [position, setPosition] = useState(''); + const [position, setPosition] = useState<Position>('');src/app/layout.tsx (1)
28-31: 에러 바운더리 추가가 필요합니다.React Query 관련 오류를 적절히 처리하기 위해 에러 바운더리를 추가하는 것이 좋습니다.
+import { ErrorBoundary } from 'react-error-boundary'; + +function ErrorFallback({ error }: { error: Error }) { + return ( + <div role="alert"> + <p>문제가 발생했습니다:</p> + <pre>{error.message}</pre> + </div> + ); +} + <ReactQueryProviders> - <Header /> - {children} + <ErrorBoundary FallbackComponent={ErrorFallback}> + <Header /> + {children} + </ErrorBoundary> </ReactQueryProviders>src/components/ui/Chip.tsx (2)
4-6: 인터페이스 타입 정의 개선이 필요합니다.
className타입을 명시적으로 정의하여 타입 안정성을 향상시킬 수 있습니다.interface IChipProps extends React.ComponentPropsWithRef<'div'> { isActive?: boolean; + className?: string; }
11-21: 접근성 개선이 필요합니다.상호작용 가능한 요소로 사용될 경우를 대비하여 ARIA 속성을 추가하는 것이 좋습니다.
<div className={cn( 'typo-button2 flex h-[40px] items-center justify-center rounded-[8px] px-[16px] py-[12px]', isActive ? 'bg-default text-main' : 'bg-disable text-Cgray500', className, )} ref={ref} + role="button" + aria-pressed={isActive} {...props} > {children} </div>src/app/login/components/LoginForm.tsx (1)
19-22: 로그인 상태 관리 기능이 필요합니다.사용자 편의를 위해 "로그인 상태 유지" 옵션을 추가해야 합니다.
<form onSubmit={handleSubmit(onSubmit)} className="flex w-[544px] flex-col gap-[48px] rounded-[16px] bg-BG_2 p-[40px]" > + <div className="flex items-center gap-2"> + <input + type="checkbox" + id="remember" + {...register('remember')} + /> + <label htmlFor="remember" className="text-Cgray700"> + 로그인 상태 유지 + </label> + </div>src/hooks/useLoginForm.ts (1)
20-20: 사용하지 않는 상태 변수 제거
isDirty와dirtyFields는 선언되었지만 사용되지 않습니다.다음과 같이 수정하세요:
- formState: { errors, isDirty, dirtyFields }, + formState: { errors },🧰 Tools
🪛 GitHub Check: check
[warning] 20-20:
'isDirty' is assigned a value but never used. Allowed unused vars must match /^_/u
[warning] 20-20:
'dirtyFields' is assigned a value but never used. Allowed unused vars must match /^_/usrc/service/api/user.ts (1)
4-14: 응답 타입 정의 필요API 응답의 타입이 정의되어 있지 않아 타입 안전성이 보장되지 않습니다.
다음과 같이 수정하세요:
+interface LoginResponse { + accessToken: string; + refreshToken: string; +} + const postLogin = async ({ email, password, }: { email: string; password: string; -}) => { +}): Promise<LoginResponse> => { const res = await basicAPI.post('/api/v1/auths/login', { email, password }); - return res; + return res.data; };src/hooks/useDebounde.ts (2)
21-23: null 체크를 개선해 주세요.현재는
null체크만 하고 있지만,undefined도 함께 체크하는 것이 더 안전합니다.- if (value === null) return; + if (value == null) return;
3-9: JSDoc 문서를 개선해 주세요.반환값에 대한 설명을 추가하면 더 완성도 있는 문서가 될 것 같습니다.
/** * 특정 값이 변경된 후 지정된 시간이 지나면 콜백 함수를 실행하는 Debounce 훅 * * @param {T} value - 감지할 값 * @param {number} delay - 딜레이(ms) (기본값: 1000ms) * @param {Function} callback - 딜레이 후 실행할 콜백 함수 + * @returns {T} debounceValue - 디바운스된 값 */src/hooks/useSignupForm.ts (2)
48-55: useEffect 의존성 배열을 개선해 주세요.
watch함수 호출을 의존성 배열 외부로 분리하면 정적 분석이 가능해집니다.+ const watchedName = watch('name'); useEffect(() => { setIsNameCheck(false); - }, [watch('name')]); + }, [watchedName]); + const watchedEmail = watch('email'); useEffect(() => { setIsEmailCheck(false); - }, [watch('email')]); + }, [watchedEmail]);🧰 Tools
🪛 GitHub Check: check
[warning] 51-51:
React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked
[warning] 55-55:
React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked
89-91: 오타를 수정해 주세요.
singupMutate를signupMutate로 수정해야 합니다.- const { mutate: singupMutate } = useSignupMutation({ + const { mutate: signupMutate } = useSignupMutation({ onSuccessCallback: () => router.push('/login'), });src/util/validation.ts (2)
18-24: 중복된 이메일 유효성 검사를 통합해 주세요.
emailValidation과loginEmailValidation이 동일한 규칙을 사용하고 있습니다. 하나의 상수로 통합하여 재사용하면 좋을 것 같습니다.export const emailValidation = { required: '이메일을 입력해주세요.', pattern: { value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, message: '올바른 이메일 형식이 아닙니다.', }, }; -export const loginEmailValidation = { - required: '이메일을 입력해주세요.', - pattern: { - value: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, - message: '올바른 이메일 형식이 아닙니다.', - }, -}; +export const loginEmailValidation = emailValidation;Also applies to: 48-55
26-36: 비밀번호 유효성 검사를 강화해 주세요.현재 비밀번호 규칙이 다소 약합니다. 특수문자 요구사항을 추가하여 보안을 강화하면 좋을 것 같습니다.
export const passwordValidation = { required: '비밀번호를 입력해주세요.', minLength: { value: 6, message: '비밀번호는 최소 6자 이상이어야 합니다.', }, pattern: { - value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,}$/, - message: '비밀번호는 영어와 숫자 포함 6자 이상이어야 합니다.', + value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{6,}$/, + message: '비밀번호는 영어, 숫자, 특수문자를 포함하여 6자 이상이어야 합니다.', }, };src/hooks/mutations/useUserMutation.ts (1)
47-51: TODO 주석을 해결해 주세요.여러 TODO 주석이 있습니다. 이를 해결하거나 이슈로 만들어 추적하면 좋을 것 같습니다.
이러한 TODO 항목들을 이슈로 만들어 추적하는 것을 도와드릴까요?
Also applies to: 72-76, 94-99
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (23)
package.json(1 hunks)src/app/layout.tsx(2 hunks)src/app/login/components/LoginForm.tsx(1 hunks)src/app/login/page.tsx(1 hunks)src/app/preview/chip/page.tsx(1 hunks)src/app/signup/components/ChipContainer.tsx(1 hunks)src/app/signup/components/SignupForm.tsx(1 hunks)src/app/signup/page.tsx(1 hunks)src/components/common/Header.tsx(2 hunks)src/components/ui/Chip.tsx(1 hunks)src/hooks/mutations/useUserMutation.ts(1 hunks)src/hooks/useDebounde.ts(1 hunks)src/hooks/useLoginForm.ts(1 hunks)src/hooks/useReactQuery.tsx(1 hunks)src/hooks/useSignupForm.ts(1 hunks)src/lib/axios/authApi.ts(1 hunks)src/lib/axios/basicApi.ts(1 hunks)src/lib/axios/defaultConfig.ts(1 hunks)src/lib/serverActions.ts(1 hunks)src/service/api/user.ts(1 hunks)src/styles/globals.css(2 hunks)src/util/validation.ts(1 hunks)tailwind.config.ts(1 hunks)
✅ Files skipped from review due to trivial changes (3)
- src/lib/axios/basicApi.ts
- tailwind.config.ts
- src/styles/globals.css
🧰 Additional context used
🧠 Learnings (1)
src/app/signup/page.tsx (1)
Learnt from: dbswl701
PR: MoimService/Moim-FE#20
File: src/app/signup/page.tsx:76-87
Timestamp: 2025-02-24T02:43:28.082Z
Learning: The signup functionality and related improvements in src/app/signup/page.tsx will be handled in a separate PR, distinct from the login page PR.
🪛 GitHub Check: check
src/hooks/useSignupForm.ts
[warning] 51-51:
React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked
[warning] 55-55:
React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked
src/hooks/useLoginForm.ts
[warning] 20-20:
'isDirty' is assigned a value but never used. Allowed unused vars must match /^_/u
[warning] 20-20:
'dirtyFields' is assigned a value but never used. Allowed unused vars must match /^_/u
[warning] 56-56:
React Hook useEffect has a missing dependency: 'router'. Either include it or remove the dependency array
src/lib/axios/authApi.ts
[warning] 27-27:
'error' is defined but never used
src/hooks/mutations/useUserMutation.ts
[warning] 30-30:
Unexpected console statement
🔇 Additional comments (4)
src/app/login/page.tsx (1)
16-22: LGTM! 깔끔한 컴포넌트 구조입니다.중앙 정렬을 위한 Flexbox 사용이 적절하며, 컴포넌트 구조가 명확합니다.
src/app/signup/page.tsx (1)
11-17: LGTM! 로그인 페이지와 일관된 구조를 유지하고 있습니다.컴포넌트 구조가 로그인 페이지와 동일한 패턴을 따르고 있어 일관성이 좋습니다.
src/lib/axios/defaultConfig.ts (1)
3-3:Details
❓ Verification inconclusive
환경 변수 존재 여부 확인 필요
NEXT_PUBLIC_API_URL환경 변수가 없을 경우에 대한 처리가 필요합니다.다음 스크립트로 환경 변수 설정을 확인해주세요:
🏁 Script executed:
#!/bin/bash # Description: Check if the required environment variable is set in .env files # Look for NEXT_PUBLIC_API_URL in .env* files fd -H "^\.env" -x grep -l "NEXT_PUBLIC_API_URL"Length of output: 47
환경 변수 정의의 존재 여부 재확인이 필요합니다.
리뷰어가 제안한 스크립트 실행 결과, 리포지토리 내의
.env*파일에서NEXT_PUBLIC_API_URL변수를 찾을 수 없었습니다. 이 변수는 실행 환경이나 별도의 설정 파일을 통해 정의될 가능성이 있으므로, 아래 사항들을 확인해 주시기 바랍니다:
- 실행 환경 변수 확인: 실제 배포 혹은 로컬 실행 환경에서
NEXT_PUBLIC_API_URL이 올바르게 설정되어 있는지 확인해 주세요.- 별도 설정 파일 검토:
.env파일 외에 다른 설정 파일(예:.env.local,.env.production등)에 해당 변수가 정의되어 있는지 점검해 주세요.필요에 따라 수동 검증을 진행해 주시기 바랍니다.
src/hooks/useReactQuery.tsx (1)
20-25: LGTM! React Query 설정이 적절합니다.QueryClientProvider와 ReactQueryDevtools가 잘 구성되어 있습니다.
src/app/login/page.tsx
Outdated
| import LoginForm from './components/LoginForm'; | ||
|
|
||
| export const metadata = { | ||
| metadataBase: new URL('http://localhost:3000/login'), |
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
하드코딩된 URL을 환경 변수로 대체 필요
localhost:3000 URL이 하드코딩되어 있습니다. 환경에 따라 유연하게 대응할 수 있도록 환경 변수를 사용하는 것이 좋습니다.
다음과 같이 환경 변수를 사용하도록 수정해주세요:
- metadataBase: new URL('http://localhost:3000/login'),
+ metadataBase: new URL(`${process.env.NEXT_PUBLIC_BASE_URL}/login`),- url: 'http://localhost:3000/login', // 추후 수정
+ url: `${process.env.NEXT_PUBLIC_BASE_URL}/login`,Also applies to: 10-10
| <div className="w-[544px] p-[40px]"> | ||
| <div className="flex w-full"> | ||
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Frontend'} | ||
| onClick={() => setPosition('Frontend')} | ||
| > | ||
| 프론트엔드 | ||
| </Chip> | ||
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Backend'} | ||
| onClick={() => setPosition('Backend')} | ||
| > | ||
| 백엔드 | ||
| </Chip> | ||
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Designer'} | ||
| onClick={() => setPosition('Designer')} | ||
| > | ||
| 디자이너 | ||
| </Chip> | ||
| </div> | ||
| </div> |
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
ChipContainer 컴포넌트 재사용이 필요합니다.
이미 구현된 ChipContainer 컴포넌트를 재사용하여 코드 중복을 제거하는 것이 좋습니다.
- <div className="w-[544px] p-[40px]">
- <div className="flex w-full">
- <Chip
- className={`flex-1 hover:cursor-pointer`}
- isActive={position === 'Frontend'}
- onClick={() => setPosition('Frontend')}
- >
- 프론트엔드
- </Chip>
- <Chip
- className={`flex-1 hover:cursor-pointer`}
- isActive={position === 'Backend'}
- onClick={() => setPosition('Backend')}
- >
- 백엔드
- </Chip>
- <Chip
- className={`flex-1 hover:cursor-pointer`}
- isActive={position === 'Designer'}
- onClick={() => setPosition('Designer')}
- >
- 디자이너
- </Chip>
- </div>
+ <div className="w-[544px] p-[40px]">
+ <ChipContainer position={position} setPosition={setPosition} />
</div>📝 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="w-[544px] p-[40px]"> | |
| <div className="flex w-full"> | |
| <Chip | |
| className={`flex-1 hover:cursor-pointer`} | |
| isActive={position === 'Frontend'} | |
| onClick={() => setPosition('Frontend')} | |
| > | |
| 프론트엔드 | |
| </Chip> | |
| <Chip | |
| className={`flex-1 hover:cursor-pointer`} | |
| isActive={position === 'Backend'} | |
| onClick={() => setPosition('Backend')} | |
| > | |
| 백엔드 | |
| </Chip> | |
| <Chip | |
| className={`flex-1 hover:cursor-pointer`} | |
| isActive={position === 'Designer'} | |
| onClick={() => setPosition('Designer')} | |
| > | |
| 디자이너 | |
| </Chip> | |
| </div> | |
| </div> | |
| <div className="w-[544px] p-[40px]"> | |
| <ChipContainer position={position} setPosition={setPosition} /> | |
| </div> |
| <Button | ||
| disabled={isNameCheck} | ||
| variant={'outline'} | ||
| size={'sm'} | ||
| className="h-[50px]" | ||
| type="button" | ||
| onClick={handleNameCheck} | ||
| > | ||
| 중복확인 | ||
| </Button> | ||
| </div> |
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
중복 확인 버튼의 로딩 상태 처리가 필요합니다.
사용자 경험 향상을 위해 중복 확인 중인 상태를 표시해야 합니다.
<Button
disabled={isNameCheck}
variant={'outline'}
size={'sm'}
className="h-[50px]"
type="button"
+ isLoading={isCheckingName}
onClick={handleNameCheck}
>
- 중복확인
+ {isCheckingName ? '확인중...' : '중복확인'}
</Button>📝 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.
| <Button | |
| disabled={isNameCheck} | |
| variant={'outline'} | |
| size={'sm'} | |
| className="h-[50px]" | |
| type="button" | |
| onClick={handleNameCheck} | |
| > | |
| 중복확인 | |
| </Button> | |
| </div> | |
| <Button | |
| disabled={isNameCheck} | |
| variant={'outline'} | |
| size={'sm'} | |
| className="h-[50px]" | |
| type="button" | |
| isLoading={isCheckingName} | |
| onClick={handleNameCheck} | |
| > | |
| {isCheckingName ? '확인중...' : '중복확인'} | |
| </Button> | |
| </div> |
| <form | ||
| onSubmit={handleSubmit(onSubmit)} | ||
| className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px]" | ||
| > |
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
폼 제출 시 피드백이 필요합니다.
사용자에게 폼 제출 상태와 결과를 알려주어야 합니다.
<form
onSubmit={handleSubmit(onSubmit)}
- className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px]"
+ className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px] relative"
>
+ {isSubmitting && (
+ <div className="absolute inset-0 bg-black/50 flex items-center justify-center rounded-[16px]">
+ <p className="text-white">가입 처리중...</p>
+ </div>
+ )}📝 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.
| <form | |
| onSubmit={handleSubmit(onSubmit)} | |
| className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px]" | |
| > | |
| <form | |
| onSubmit={handleSubmit(onSubmit)} | |
| className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px] relative" | |
| > | |
| {isSubmitting && ( | |
| <div className="absolute inset-0 bg-black/50 flex items-center justify-center rounded-[16px]"> | |
| <p className="text-white">가입 처리중...</p> | |
| </div> | |
| )} |
| <Button type="submit" className="w-full"> | ||
| 로그인 | ||
| </Button> |
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
로그인 버튼의 로딩 상태 처리가 필요합니다.
사용자에게 로그인 진행 상태를 표시해야 합니다.
- <Button type="submit" className="w-full">
- 로그인
+ <Button
+ type="submit"
+ className="w-full"
+ isLoading={isSubmitting}
+ disabled={isSubmitting}
+ >
+ {isSubmitting ? '로그인중...' : '로그인'}
</Button>📝 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.
| <Button type="submit" className="w-full"> | |
| 로그인 | |
| </Button> | |
| <Button | |
| type="submit" | |
| className="w-full" | |
| isLoading={isSubmitting} | |
| disabled={isSubmitting} | |
| > | |
| {isSubmitting ? '로그인중...' : '로그인'} | |
| </Button> |
| const getEmailCheck = async (email: string) => { | ||
| const res = await basicAPI.get(`/api/v1/auths/signup/email?email=${email}`); | ||
| return res; | ||
| }; |
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
이메일 검증 엔드포인트 보안 개선 필요
이메일 주소도 URL 파라미터로 직접 전달되고 있습니다.
다음과 같이 수정하세요:
const getEmailCheck = async (email: string) => {
- const res = await basicAPI.get(`/api/v1/auths/signup/email?email=${email}`);
+ const res = await basicAPI.get('/api/v1/auths/signup/email', {
+ params: { email }
+ });
return res;
};📝 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.
| const getEmailCheck = async (email: string) => { | |
| const res = await basicAPI.get(`/api/v1/auths/signup/email?email=${email}`); | |
| return res; | |
| }; | |
| const getEmailCheck = async (email: string) => { | |
| const res = await basicAPI.get('/api/v1/auths/signup/email', { | |
| params: { email } | |
| }); | |
| return res; | |
| }; |
| const getNameCheck = async (name: string) => { | ||
| const res = await basicAPI.get(`/api/v1/auths/signup/name?name=${name}`); | ||
| return res; | ||
| }; |
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
GET 요청의 보안 개선 필요
URL 파라미터로 직접 데이터를 전달하는 것은 XSS 공격에 취약할 수 있습니다.
다음과 같이 수정하세요:
const getNameCheck = async (name: string) => {
- const res = await basicAPI.get(`/api/v1/auths/signup/name?name=${name}`);
+ const res = await basicAPI.get('/api/v1/auths/signup/name', {
+ params: { name }
+ });
return res;
};📝 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.
| const getNameCheck = async (name: string) => { | |
| const res = await basicAPI.get(`/api/v1/auths/signup/name?name=${name}`); | |
| return res; | |
| }; | |
| const getNameCheck = async (name: string) => { | |
| const res = await basicAPI.get('/api/v1/auths/signup/name', { | |
| params: { name } | |
| }); | |
| return res; | |
| }; |
| const onSubmit = (data: ISignupFormData) => { | ||
| if (!isNameCheck) { | ||
| setError('name', { | ||
| type: 'nameCheck', | ||
| message: '닉네임 중복확인이 필요합니다.', | ||
| }); | ||
| } | ||
| if (!isEmailCheck) { | ||
| setError('email', { | ||
| type: 'emailCheck', | ||
| message: '이메일 중복확인이 필요합니다.', | ||
| }); | ||
| } | ||
|
|
||
| if (Object.keys(errors).length) { | ||
| return; | ||
| } | ||
| singupMutate(data); | ||
| }; |
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
에러 처리를 개선해 주세요.
현재 구현에서는 errors 객체의 길이를 확인하여 에러를 체크하고 있지만, 이는 신뢰할 수 없는 방법입니다. 대신 isNameCheck와 isEmailCheck를 직접 확인하는 것이 더 명확합니다.
const onSubmit = (data: ISignupFormData) => {
+ let hasError = false;
if (!isNameCheck) {
+ hasError = true;
setError('name', {
type: 'nameCheck',
message: '닉네임 중복확인이 필요합니다.',
});
}
if (!isEmailCheck) {
+ hasError = true;
setError('email', {
type: 'emailCheck',
message: '이메일 중복확인이 필요합니다.',
});
}
- if (Object.keys(errors).length) {
+ if (hasError) {
return;
}
- singupMutate(data);
+ signupMutate(data);
};📝 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.
| const onSubmit = (data: ISignupFormData) => { | |
| if (!isNameCheck) { | |
| setError('name', { | |
| type: 'nameCheck', | |
| message: '닉네임 중복확인이 필요합니다.', | |
| }); | |
| } | |
| if (!isEmailCheck) { | |
| setError('email', { | |
| type: 'emailCheck', | |
| message: '이메일 중복확인이 필요합니다.', | |
| }); | |
| } | |
| if (Object.keys(errors).length) { | |
| return; | |
| } | |
| singupMutate(data); | |
| }; | |
| const onSubmit = (data: ISignupFormData) => { | |
| let hasError = false; | |
| if (!isNameCheck) { | |
| hasError = true; | |
| setError('name', { | |
| type: 'nameCheck', | |
| message: '닉네임 중복확인이 필요합니다.', | |
| }); | |
| } | |
| if (!isEmailCheck) { | |
| hasError = true; | |
| setError('email', { | |
| type: 'emailCheck', | |
| message: '이메일 중복확인이 필요합니다.', | |
| }); | |
| } | |
| if (hasError) { | |
| return; | |
| } | |
| signupMutate(data); | |
| }; |
| onError: () => { | ||
| console.log('로그인 에러'); | ||
| }, |
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.
에러 처리를 개선해 주세요.
console.log를 사용한 에러 처리는 프로덕션 환경에서 적절하지 않습니다. 사용자에게 의미 있는 피드백을 제공하는 것이 좋습니다.
- onError: () => {
- console.log('로그인 에러');
- },
+ onError: (error) => {
+ // TODO: 에러 타입 정의 필요
+ const errorMessage = error.response?.data?.message ?? '로그인에 실패했습니다.';
+ // 토스트 메시지나 다른 UI 피드백으로 사용자에게 알림
+ },📝 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.
| onError: () => { | |
| console.log('로그인 에러'); | |
| }, | |
| onError: (error) => { | |
| // TODO: 에러 타입 정의 필요 | |
| const errorMessage = error.response?.data?.message ?? '로그인에 실패했습니다.'; | |
| // 토스트 메시지나 다른 UI 피드백으로 사용자에게 알림 | |
| }, |
🧰 Tools
🪛 GitHub Check: check
[warning] 30-30:
Unexpected console statement
| return useMutation({ | ||
| mutationFn: ({ email, password }: { email: string; password: string }) => | ||
| postLogin({ email, password }), | ||
| onSuccess: async (res) => { | ||
| // 쿠키 저장 | ||
| const accessToken = res.headers.token; | ||
| if (accessToken) { | ||
| await setAccessToken(accessToken); | ||
| } | ||
|
|
||
| // 메인페이지로 리다이렉트 | ||
| onSuccessCallback(); | ||
| }, | ||
| onError: () => { | ||
| console.log('로그인 에러'); | ||
| }, | ||
| }); | ||
| }; |
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
타입 안전성을 개선해 주세요.
postLogin API 응답의 타입이 명시되어 있지 않습니다. 타입을 정의하여 타입 안전성을 개선하면 좋을 것 같습니다.
+interface LoginResponse {
+ headers: {
+ token: string;
+ };
+ // 다른 응답 필드들...
+}
return useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
postLogin({ email, password }),
- onSuccess: async (res) => {
+ onSuccess: async (res: LoginResponse) => {
// 쿠키 저장
const accessToken = res.headers.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 useMutation({ | |
| mutationFn: ({ email, password }: { email: string; password: string }) => | |
| postLogin({ email, password }), | |
| onSuccess: async (res) => { | |
| // 쿠키 저장 | |
| const accessToken = res.headers.token; | |
| if (accessToken) { | |
| await setAccessToken(accessToken); | |
| } | |
| // 메인페이지로 리다이렉트 | |
| onSuccessCallback(); | |
| }, | |
| onError: () => { | |
| console.log('로그인 에러'); | |
| }, | |
| }); | |
| }; | |
| interface LoginResponse { | |
| headers: { | |
| token: string; | |
| }; | |
| // 다른 응답 필드들... | |
| } | |
| return useMutation({ | |
| mutationFn: ({ email, password }: { email: string; password: string }) => | |
| postLogin({ email, password }), | |
| onSuccess: async (res: LoginResponse) => { | |
| // 쿠키 저장 | |
| const accessToken = res.headers.token; | |
| if (accessToken) { | |
| await setAccessToken(accessToken); | |
| } | |
| // 메인페이지로 리다이렉트 | |
| onSuccessCallback(); | |
| }, | |
| onError: () => { | |
| console.log('로그인 에러'); | |
| }, | |
| }); | |
| }; |
🧰 Tools
🪛 GitHub Check: check
[warning] 30-30:
Unexpected console statement
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Frontend'} | ||
| onClick={() => setPosition('Frontend')} | ||
| > | ||
| 프론트엔드 | ||
| </Chip> | ||
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Backend'} | ||
| onClick={() => setPosition('Backend')} | ||
| > | ||
| 백엔드 | ||
| </Chip> | ||
| <Chip | ||
| className={`flex-1 hover:cursor-pointer`} | ||
| isActive={position === 'Designer'} | ||
| onClick={() => setPosition('Designer')} | ||
| > | ||
| 디자이너 | ||
| </Chip> |
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.
요 Chip은 마이페이지 수정에서도 사용될 컴포넌트여서 추후 분리 요청가능할까용
src/app/signup/page.tsx
Outdated
| export interface ISignupFormData { | ||
| name: string; | ||
| email: string; | ||
| position: string; | ||
| password: string; | ||
| passwordCheck: string; | ||
| } |
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.
markup하시면서 이번주부터 타입 같이 관리해보아요!
|
헤더컴포넌트에서 얘기 나누었어야했는데, 해당 작업에 댓글남겨서 죄송합니다. 헤더컴포넌트 반복되는 부분들 객체화로 관리하여 map으로 리펙토링 필요합니당! const navigation = [
{ href: '/Mogakco', label: '모각코' },
{ href: '/study', label: '스터디' },
{ href: '/side-project', label: '사이드 프로젝트' },
{ href: '/hobby', label: '취미' },
]; |
Feat/component/toast/DEVING-20 Toast 구현
Markup 브랜치 dev 동기화
…t/markup/signup/DEVING-25
…e/Moim-FE into feat/markup/signup/DEVING-25
CI Status Report검사 결과
❌ 일부 검사가 실패했습니다. |
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: 6
🧹 Nitpick comments (6)
src/components/common/Header.tsx (2)
51-55: 로그아웃 시 에러 처리 보완 제안
removeAccessToken()함수가 실패할 경우 사용자에게 알림이 되지 않을 가능성이 있습니다. 아래와 같이try...catch블록을 추가하여 에러 처리를 보완해 보세요.onSelect: async () => { + try { await removeAccessToken(); showToast('로그아웃 되었습니다.', 'success'); + } catch (error) { + showToast('로그아웃에 실패했습니다.', 'error'); + } },
135-144:map활용으로 반복 렌더링
navigation배열을map으로 렌더링하여 중복 코드를 줄인 점이 좋습니다. 단,label이 변경될 가능성이 있다면key로 다른 고유 ID를 사용해 안정성을 높이는 것도 고려해 보세요.src/app/login/components/EmailInput.tsx (1)
25-38: 컴포넌트 구조의 일관성 고려현재 EmailInput 컴포넌트는 Fragment(
<></>)를 반환하고 있습니다. 다른 입력 컴포넌트들과의 일관성을 위해 div로 감싸는 것을 고려해 보세요. 특히 signup 컴포넌트들은 div로 감싸고 있어 일관된 스타일링과 구조를 유지하는 데 도움이 됩니다.return ( - <> + <div className="flex flex-col gap-[8px]"> <label htmlFor="email" className="typo-head3 text-Cgray700"> 이메일 </label> <Input id="email" className="mb-[20px] mt-[8px]" placeholder="이메일을 입력해주세요." {...register('email', loginEmailValidation)} errorMessage={errors.email?.message} /> - </> + </div> );src/components/common/PositionSelect.tsx (1)
3-35: 코드 중복을 제거하여 가독성과 유지보수성을 높이세요현재
Chip컴포넌트가 세 번 반복되고 있습니다. 아래와 같이 배열과map함수를 사용하여 코드를 리팩토링하면 더 간결하고 유지보수하기 쉬운 코드가 될 것입니다.- return ( - <div className="flex w-full gap-[8px]"> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Frontend'} - onClick={() => setPosition('Frontend')} - > - 프론트엔드 - </Chip> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Backend'} - onClick={() => setPosition('Backend')} - > - 백엔드 - </Chip> - <Chip - className={`flex-1 hover:cursor-pointer`} - isActive={position === 'Designer'} - onClick={() => setPosition('Designer')} - > - 디자이너 - </Chip> - </div> - ); + const positions = [ + { value: 'Frontend', label: '프론트엔드' }, + { value: 'Backend', label: '백엔드' }, + { value: 'Designer', label: '디자이너' }, + ]; + + return ( + <div className="flex w-full gap-[8px]"> + {positions.map((pos) => ( + <Chip + key={pos.value} + className="flex-1 hover:cursor-pointer" + isActive={position === pos.value} + onClick={() => setPosition(pos.value)} + > + {pos.label} + </Chip> + ))} + </div> + );이렇게 수정하면 나중에 포지션을 추가하거나 수정할 때 배열만 변경하면 되어 유지보수가 용이해집니다.
src/app/signup/components/EmailInput.tsx (1)
1-68: 중복 코드 추상화 고려
EmailInput컴포넌트는NameInput컴포넌트와 구조와 로직이 매우 유사합니다. 두 컴포넌트 간에 공통된 로직을 추출하여 재사용 가능한 컴포넌트나 훅으로 만드는 것을 고려해보세요.예를 들어, 다음과 같은 공통 컴포넌트를 만들 수 있습니다:
const CheckableInput = ({ register, errors, isChecked, handleCheck, setIsChecked, control, trigger, fieldName, label, placeholder, validation, }: ICheckableInputProps) => { const value = useWatch({ control, name: fieldName }); useEffect(() => { setIsChecked(false); }, [value, setIsChecked]); useDebounce({ value, callBack: useCallback(() => { trigger?.(fieldName); }, [fieldName, trigger, value]), }); return ( <div className="flex flex-col gap-[8px]"> <label htmlFor={fieldName} className="typo-head3 text-Cgray700"> {label} </label> <div className="flex flex-row gap-[8px]"> <Input id={fieldName} className="h-full" placeholder={placeholder} {...register(fieldName, validation)} state={isChecked ? 'success' : 'default'} errorMessage={errors[fieldName]?.message} /> <Button disabled={isChecked} variant={'outline'} size={'sm'} className="h-[50px]" type="button" onClick={handleCheck} > 중복확인 </Button> </div> </div> ); };🧰 Tools
🪛 GitHub Check: check
[warning] 31-31:
React Hook useEffect has a missing dependency: 'setIsEmailCheck'. Either include it or remove the dependency array. If 'setIsEmailCheck' changes too often, find the parent component that defines it and wrap that definition in useCallback
[warning] 37-37:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallbacksrc/hooks/mutations/useUserMutation.ts (1)
32-33: 에러 객체를 활용해 추가적인 사용자 피드백을 제공해 보세요.
onError콜백에서 단순 토스트 메시지 외에 서버에서 반환되는 에러 정보를 활용하면, 사용자에게 더 정확한 문제 상황을 안내할 수 있습니다.-onError: () => { - showToast('이메일 또는 비밀번호가 틀렸습니다.', 'error'); +onError: (error) => { + // TODO: 에러 상태나 메시지를 파악하여 조건별로 다른 안내를 해 줄 수도 있습니다. + showToast('이메일 또는 비밀번호가 틀렸습니다.', 'error'); },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
src/app/login/components/EmailInput.tsx(1 hunks)src/app/login/components/LoginForm.tsx(3 hunks)src/app/login/components/PasswordInput.tsx(1 hunks)src/app/signup/components/EmailInput.tsx(1 hunks)src/app/signup/components/NameInput.tsx(1 hunks)src/app/signup/components/PasswordCheckInput.tsx(1 hunks)src/app/signup/components/PasswordInput.tsx(1 hunks)src/app/signup/components/PositionInput.tsx(1 hunks)src/app/signup/components/SignupForm.tsx(1 hunks)src/app/signup/page.tsx(1 hunks)src/components/common/Header.tsx(4 hunks)src/components/common/PositionSelect.tsx(1 hunks)src/hooks/mutations/useUserMutation.ts(2 hunks)src/hooks/useDebounde.ts(2 hunks)src/hooks/useLoginForm.ts(2 hunks)src/hooks/useSignupForm.ts(1 hunks)src/service/api/endpoints.ts(1 hunks)src/types/auth.ts(1 hunks)src/util/validation.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/hooks/useDebounde.ts
- src/hooks/useSignupForm.ts
- src/app/login/components/LoginForm.tsx
🧰 Additional context used
🧠 Learnings (3)
src/app/signup/page.tsx (1)
Learnt from: dbswl701
PR: MoimService/Moim-FE#20
File: src/app/signup/page.tsx:76-87
Timestamp: 2025-02-24T02:43:28.082Z
Learning: The signup functionality and related improvements in src/app/signup/page.tsx will be handled in a separate PR, distinct from the login page PR.
src/components/common/Header.tsx (1)
Learnt from: dbswl701
PR: MoimService/Moim-FE#15
File: src/components/common/Header.tsx:0-0
Timestamp: 2025-02-19T02:47:47.597Z
Learning: The console.log statements in Header.tsx for logout functionality are temporary placeholders that will be replaced with actual logout logic in a future update.
src/hooks/mutations/useUserMutation.ts (1)
Learnt from: dbswl701
PR: MoimService/Moim-FE#20
File: src/hooks/mutations/useUserMutation.ts:23-25
Timestamp: 2025-02-24T04:20:01.755Z
Learning: Error handling in the login mutation (src/hooks/mutations/useUserMutation.ts) will be implemented in a future update to provide proper user feedback instead of console.log.
🪛 GitHub Check: check
src/app/login/components/PasswordInput.tsx
[warning] 22-22:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
src/app/login/components/EmailInput.tsx
[warning] 22-22:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
src/app/signup/components/EmailInput.tsx
[warning] 31-31:
React Hook useEffect has a missing dependency: 'setIsEmailCheck'. Either include it or remove the dependency array. If 'setIsEmailCheck' changes too often, find the parent component that defines it and wrap that definition in useCallback
[warning] 37-37:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
src/hooks/mutations/useUserMutation.ts
[failure] 1-1:
Module '"@/app/signup/page"' has no exported member 'ISignupFormData'. Did you mean to use 'import ISignupFormData from "@/app/signup/page"' instead?
src/app/signup/components/NameInput.tsx
[warning] 31-31:
React Hook useEffect has a missing dependency: 'setIsNameCheck'. Either include it or remove the dependency array. If 'setIsNameCheck' changes too often, find the parent component that defines it and wrap that definition in useCallback
[warning] 37-37:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
🪛 GitHub Actions: PR Check
src/hooks/mutations/useUserMutation.ts
[error] 1-1: Type error: Module '@app/signup/page' has no exported member 'ISignupFormData'. Did you mean to use 'import ISignupFormData from "@/app/signup/page"' instead?
🔇 Additional comments (26)
src/components/common/Header.tsx (4)
5-5:removeAccessToken임포트 확인
서버 액션을 통한 인증 토큰 제거 로직으로 보이며, 필요한 위치에서 정상적으로 사용된다면 문제 없어 보입니다.
12-12: ToastContext 활용
useToast훅을 임포트하여 토스트 알림을 간단히 구현할 수 있으므로 사용자 경험 개선에 유용합니다.
14-19:navigation배열을 통한 라우트 관리
하드코딩 대신navigation배열에 라우트들을 모아두면 확장성과 유지보수성이 크게 향상됩니다.
36-36:showToast사용
useToast에서showToast를 구조분해 할당하여 사용한 방식은 일관성과 가독성이 높습니다.src/app/signup/page.tsx (3)
5-16: 메타데이터 설정 잘 되어있습니다.SEO와 소셜 미디어 공유를 위한 메타데이터가 잘 구성되어 있습니다. OpenGraph 속성도 적절하게 설정되어 있어 좋습니다.
12-12: URL 수정 사항을 TODO로 추적하세요."추후 수정" 주석이 있는데, 이러한 수정 사항은 코드 내 주석보다는 이슈 트래커나 TODO 리스트에서 관리하는 것이 좋습니다. 가능하다면 지금 수정하거나 별도의 이슈로 등록해주세요.
18-24: 컴포넌트 구조 개선이 잘 되었습니다.SignupForm 컴포넌트를 분리하여 코드 구조가 더 깔끔해졌습니다. 관심사 분리가 잘 되어 있어 유지보수가 용이해졌습니다.
src/app/signup/components/PasswordCheckInput.tsx (1)
28-41: 상태 처리가 잘 구현되어 있습니다.성공 상태와 에러 상태를 모두 적절히 처리하고 있어 사용자에게 명확한 피드백을 제공합니다. dirtyFields를 활용하여 입력 필드의 상태를 표시하는 것이 좋은 UX를 제공합니다.
src/app/login/components/PasswordInput.tsx (1)
25-38: 컴포넌트 구조의 일관성 검토EmailInput 컴포넌트와 마찬가지로 Fragment(
<></>)를 사용하고 있습니다. 반면, 회원가입 페이지의 입력 컴포넌트들은 모두 div로 감싸고 있어 일관성이 없습니다. 팀 내에서 입력 컴포넌트 구조에 대한 일관된 패턴을 정하는 것이 좋겠습니다.return ( - <> + <div className="flex flex-col gap-[8px]"> <label htmlFor="password" className="typo-head3 text-Cgray700"> 비밀번호 </label> <Input {...register('password', loginPasswordValidation)} id="password" type="password" className="mb-[20px] mt-[8px]" placeholder="비밀번호를 입력해주세요." errorMessage={errors.password?.message} /> - </> + </div> );src/types/auth.ts (1)
1-30: 잘 구조화된 타입 정의입니다!인터페이스 구조와 타입 정의가 명확하게 작성되어 있습니다. 폼 입력 관련 컴포넌트에 대한 재사용 가능한 타입을 제공하여 타입 안정성을 높였습니다.
src/app/signup/components/PositionInput.tsx (1)
1-34: 폼 통합이 잘 구현되어 있습니다
PositionSelect컴포넌트와 폼 제어 로직이 잘 통합되어 있습니다. 숨겨진 입력 필드를 사용하여 폼 등록과 검증을 처리하는 방식이 효과적이며, 오류 메시지 표시도 적절하게 구현되어 있습니다.src/app/signup/components/SignupForm.tsx (1)
32-35: 폼 제출 시 피드백과 로딩 상태 표시가 필요합니다현재 구현에서는 사용자가 폼을 제출할 때 진행 상태를 시각적으로 확인할 수 없습니다. 제출 중임을 나타내는 로딩 인디케이터를 추가하여 사용자 경험을 향상시켜야 합니다.
또한, 중복 확인 버튼에도 로딩 상태를 표시하여 사용자가 확인 중임을 알 수 있도록 해야 합니다.
<form onSubmit={handleSubmit(onSubmit)} - className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px]" + className="flex w-[544px] flex-col rounded-[16px] bg-BG_2 p-[40px] relative" > + {isSubmitting && ( + <div className="absolute inset-0 bg-black/50 flex items-center justify-center rounded-[16px]"> + <p className="text-white">가입 처리중...</p> + </div> + )}폼 제출 상태와 중복 확인 로딩 상태를 적절히 표시하여 사용자가 현재 진행 상황을 인지할 수 있도록 개선해주세요.
src/hooks/useLoginForm.ts (2)
33-34: 새로운 반환값 추가
control과trigger를 추가적으로 반환하도록 변경되었습니다. 이는 hook의 유연성을 높이지만, 이 hook을 사용하는 컴포넌트에서 이 속성들을 활용하고 있는지 확인해야 합니다.
7-7:Details
❓ Verification inconclusive
함수 선언 방식과 내보내기 방식의 변경
함수가 named export에서 default export로 변경되었습니다. 이는 import 방식에 영향을 줄 수 있으며, 이 hook을 사용하는 모든 컴포넌트에서 import 구문을 업데이트해야 합니다.
다음 스크립트를 실행하여 이 hook을 사용하는 컴포넌트를 찾아보세요:
Also applies to: 36-38
🏁 Script executed:
#!/bin/bash # useLoginForm의 사용 위치 확인 rg -l "useLoginForm" --type ts --type tsxLength of output: 69
함수 선언 및 export 방식 변경 영향 확인
현재
useLoginForm이 named export에서 default export로 변경됨에 따라 이 hook을 사용하는 모든 컴포넌트의 import 구문 또한 업데이트되어야 합니다.
먼저, hook 사용 위치를 검증하기 위한 기존 명령어가 tsx 파일 인식을 지원하지 않아 오류가 발생했습니다. 아래와 같이 수정된 명령어로 ts와 tsx 파일 모두에서useLoginForm을 검색해 주세요:#!/bin/bash # 수정된 useLoginForm 사용 위치 확인 (ts 및 tsx 파일 검색) rg -l "useLoginForm" -g "*.ts" -g "*.tsx"수동 검증을 통해 이 hook을 사용하는 모든 컴포넌트의 import 구문이 올바르게 변경되었는지 확인해주시기 바랍니다.
src/app/signup/components/PasswordInput.tsx (2)
32-35: 의존성 배열 누락
useCallback내에서trigger를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다. 이로 인해 오래된 클로저 문제가 발생할 수 있습니다.다음과 같이 수정하세요:
callBack: useCallback(() => { trigger?.('password'); - }, [password]), + }, [password, trigger]),
47-48: 상태 로직 검토 필요현재 입력 필드의 상태는
dirtyFields.password에 따라 결정됩니다. 이는 필드가 수정되었는지 여부만 확인하며, 실제 유효성 검사 결과를 반영하지 않습니다. 사용자가 잘못된 형식의 비밀번호를 입력해도 success 상태로 표시될 수 있습니다.유효성 검사 결과와 dirtyFields를 함께 고려하는 것이 좋습니다:
- state={dirtyFields.password ? 'success' : 'default'} + state={dirtyFields.password && !errors.password ? 'success' : errors.password ? 'error' : 'default'}src/app/signup/components/NameInput.tsx (2)
29-31: useEffect 의존성 배열 수정 필요useEffect 내에서
setIsNameCheck를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다.다음과 같이 수정하세요:
useEffect(() => { setIsNameCheck(false); - }, [name]); + }, [name, setIsNameCheck]);🧰 Tools
🪛 GitHub Check: check
[warning] 31-31:
React Hook useEffect has a missing dependency: 'setIsNameCheck'. Either include it or remove the dependency array. If 'setIsNameCheck' changes too often, find the parent component that defines it and wrap that definition in useCallback
35-38: useCallback 의존성 배열 수정 필요useCallback 내에서
trigger를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다.다음과 같이 수정하세요:
callBack: useCallback(() => { trigger?.('name'); - }, [name]), + }, [name, trigger]),🧰 Tools
🪛 GitHub Check: check
[warning] 37-37:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallbacksrc/app/signup/components/EmailInput.tsx (2)
29-31: useEffect 의존성 배열 수정 필요useEffect 내에서
setIsEmailCheck를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다.다음과 같이 수정하세요:
useEffect(() => { setIsEmailCheck(false); - }, [email]); + }, [email, setIsEmailCheck]);🧰 Tools
🪛 GitHub Check: check
[warning] 31-31:
React Hook useEffect has a missing dependency: 'setIsEmailCheck'. Either include it or remove the dependency array. If 'setIsEmailCheck' changes too often, find the parent component that defines it and wrap that definition in useCallback
35-38: useCallback 의존성 배열 수정 필요useCallback 내에서
trigger를 사용하고 있지만 의존성 배열에 포함되어 있지 않습니다.다음과 같이 수정하세요:
callBack: useCallback(() => { trigger?.('email'); - }, [email]), + }, [email, trigger]),🧰 Tools
🪛 GitHub Check: check
[warning] 37-37:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallbacksrc/hooks/mutations/useUserMutation.ts (4)
38-61: 닉네임 중복 검사 로직 정상 동작 확인됨.콜백 함수를 주입받아 성공/실패를 처리하는 구조가 명확합니다.
63-86: 이메일 중복 검사 로직 정상 동작 확인됨.닉네임 중복 검사와 동일한 구조를 유지함으로써 일관된 처리가 기대됩니다.
88-105: 회원가입 데이터 처리 로직 정상 동작 확인됨.성공 시 콜백 함수로 후속 동작을 수행하는 방식이 유연합니다.
107-112: 각 훅을 일괄적으로 export하는 구조가 깔끔합니다.src/util/validation.ts (1)
1-25: 다른 유효성 검사 항목들은 전체적으로 일관성 있고 적절합니다.Also applies to: 38-65
src/service/api/endpoints.ts (1)
1-88: API 버전을 사용해 엔드포인트를 모듈화한 방식이 명확하고 확장성이 좋아 보입니다.
| useDebounce({ | ||
| value: email, | ||
| callBack: useCallback(() => { | ||
| trigger?.('email'); | ||
| }, [email]), | ||
| }); |
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
useCallback 의존성 배열에 trigger를 추가해주세요.
useCallback 훅을 사용할 때 의존성 배열에 trigger 함수를 포함시켜야 합니다. 현재는 email만 포함되어 있어 trigger 함수가 변경될 경우 콜백이 업데이트되지 않을 수 있습니다.
useDebounce({
value: email,
callBack: useCallback(() => {
trigger?.('email');
- }, [email]),
+ }, [email, trigger]),
});📝 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.
| useDebounce({ | |
| value: email, | |
| callBack: useCallback(() => { | |
| trigger?.('email'); | |
| }, [email]), | |
| }); | |
| useDebounce({ | |
| value: email, | |
| callBack: useCallback(() => { | |
| trigger?.('email'); | |
| }, [email, trigger]), | |
| }); |
🧰 Tools
🪛 GitHub Check: check
[warning] 22-22:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
| useDebounce({ | ||
| value: passwordCheck, | ||
| callBack: useCallback(() => { | ||
| trigger?.('passwordCheck'); | ||
| }, [passwordCheck]), | ||
| }); |
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
useCallback 의존성 배열에 trigger를 추가해주세요.
useCallback 훅의 의존성 배열에 trigger 함수가 누락되어 있습니다. 이는 trigger 함수가 변경되었을 때 콜백이 업데이트되지 않는 문제를 일으킬 수 있습니다.
useDebounce({
value: passwordCheck,
callBack: useCallback(() => {
trigger?.('passwordCheck');
- }, [passwordCheck]),
+ }, [passwordCheck, trigger]),
});📝 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.
| useDebounce({ | |
| value: passwordCheck, | |
| callBack: useCallback(() => { | |
| trigger?.('passwordCheck'); | |
| }, [passwordCheck]), | |
| }); | |
| useDebounce({ | |
| value: passwordCheck, | |
| callBack: useCallback(() => { | |
| trigger?.('passwordCheck'); | |
| }, [passwordCheck, trigger]), | |
| }); |
| useDebounce({ | ||
| value: password, | ||
| callBack: useCallback(() => { | ||
| trigger?.('password'); | ||
| }, [password]), | ||
| }); |
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
useCallback 의존성 배열에 trigger를 추가해주세요.
useCallback 훅의 의존성 배열에 trigger 함수가 포함되어 있지 않습니다. 이로 인해 trigger 함수가 변경될 때 콜백이 업데이트되지 않을 수 있습니다.
useDebounce({
value: password,
callBack: useCallback(() => {
trigger?.('password');
- }, [password]),
+ }, [password, trigger]),
});📝 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.
| useDebounce({ | |
| value: password, | |
| callBack: useCallback(() => { | |
| trigger?.('password'); | |
| }, [password]), | |
| }); | |
| useDebounce({ | |
| value: password, | |
| callBack: useCallback(() => { | |
| trigger?.('password'); | |
| }, [password, trigger]), | |
| }); |
🧰 Tools
🪛 GitHub Check: check
[warning] 22-22:
React Hook useCallback has a missing dependency: 'trigger'. Either include it or remove the dependency array. If 'trigger' changes too often, find the parent component that defines it and wrap that definition in useCallback
| <div className="mb-[20px] mt-[48px] flex flex-col"> | ||
| <Button type="submit" className="w-full"> | ||
| 회원가입 | ||
| </Button> | ||
| </div> |
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
폼 유효성 검사 상태 표시 필요
현재 제출 버튼은 폼의 유효성 검사 상태와 관계없이 활성화되어 있습니다. 모든 필드가 유효하지 않을 때는 버튼을 비활성화하거나 시각적으로 표시하여 사용자에게 명확한 피드백을 제공해야 합니다.
<div className="mb-[20px] mt-[48px] flex flex-col">
- <Button type="submit" className="w-full">
+ <Button
+ type="submit"
+ className="w-full"
+ disabled={!isValid || isSubmitting || !isNameCheck || !isEmailCheck}
+ >
회원가입
</Button>
</div>이렇게 수정하면 폼이 유효하지 않거나 제출 중일 때, 또는 닉네임과 이메일 중복 확인이 완료되지 않았을 때 버튼이 비활성화되어 사용자에게 더 명확한 지침을 제공할 수 있습니다.
📝 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="mb-[20px] mt-[48px] flex flex-col"> | |
| <Button type="submit" className="w-full"> | |
| 회원가입 | |
| </Button> | |
| </div> | |
| <div className="mb-[20px] mt-[48px] flex flex-col"> | |
| <Button | |
| type="submit" | |
| className="w-full" | |
| disabled={!isValid || isSubmitting || !isNameCheck || !isEmailCheck} | |
| > | |
| 회원가입 | |
| </Button> | |
| </div> |
| @@ -1,12 +1,20 @@ | |||
| import { ISignupFormData } from '@/app/signup/page'; | |||
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.
중요: ISignupFormData import 에러를 확인해주세요.
파이프라인 로그에 따르면 ISignupFormData가 @/app/signup/page에서 내보내지 않고 있어 컴파일 에러가 발생합니다. 혹시 파일 내부에서 기본 내보내기(default export) 형태로 제공하는지 확인해 주세요. 아래와 같이 수정하는 방안도 고려해 볼 수 있습니다:
- import { ISignupFormData } from '@/app/signup/page';
+ import ISignupFormData from '@/app/signup/page';📝 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.
| import { ISignupFormData } from '@/app/signup/page'; | |
| import ISignupFormData from '@/app/signup/page'; |
🧰 Tools
🪛 GitHub Check: check
[failure] 1-1:
Module '"@/app/signup/page"' has no exported member 'ISignupFormData'. Did you mean to use 'import ISignupFormData from "@/app/signup/page"' instead?
🪛 GitHub Actions: PR Check
[error] 1-1: Type error: Module '@app/signup/page' has no exported member 'ISignupFormData'. Did you mean to use 'import ISignupFormData from "@/app/signup/page"' instead?
| export const passwordValidation = { | ||
| required: '비밀번호를 입력해주세요.', | ||
| minLength: { | ||
| value: 6, | ||
| message: '비밀번호는 최소 8자 이상이어야 합니다.', | ||
| }, | ||
| pattern: { | ||
| value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, | ||
| message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.', | ||
| }, | ||
| }; |
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.
비밀번호 길이 설정이 불일치합니다.
minLength.value가 6으로 설정되어 있지만, 에러 메시지와 정규식 패턴은 8자 이상을 요구하고 있어 충돌이 발생합니다. 다음 예시처럼 최소 길이 값을 8자로 수정해 주세요.
- minLength: {
- value: 6,
- message: '비밀번호는 최소 8자 이상이어야 합니다.',
- },
- pattern: {
- value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
- message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.',
- },
+ minLength: {
+ value: 8,
+ message: '비밀번호는 최소 8자 이상이어야 합니다.',
+ },
+ pattern: {
+ value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
+ message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.',
+ },📝 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.
| export const passwordValidation = { | |
| required: '비밀번호를 입력해주세요.', | |
| minLength: { | |
| value: 6, | |
| message: '비밀번호는 최소 8자 이상이어야 합니다.', | |
| }, | |
| pattern: { | |
| value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, | |
| message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.', | |
| }, | |
| }; | |
| export const passwordValidation = { | |
| required: '비밀번호를 입력해주세요.', | |
| minLength: { | |
| value: 8, | |
| message: '비밀번호는 최소 8자 이상이어야 합니다.', | |
| }, | |
| pattern: { | |
| value: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/, | |
| message: '비밀번호는 영어와 숫자 포함 8자 이상이어야 합니다.', | |
| }, | |
| }; |
Lee-Dong-Seok
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.
고생 많으셨습니다!! 코드 리뷰 천천히 진행 하겠습니다!👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍
InJaEE
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.
코드가 많아서 자세히 보지는 못했는데요,
전반적으로 컴포넌트를 어떻게하면 더 재사용 가능하고 가독성이 좋게 설계할지 고민해보셨으면 좋겠습니다.
📝 주요 작업 내용
📺 스크린샷
🔗 참고 사항
💬 리뷰 요구사항
📃 관련 이슈
DEVING-25
Summary by CodeRabbit