-
Notifications
You must be signed in to change notification settings - Fork 0
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
[YS-224] zod 활용하여 회원가입 validation 적용 #34
Conversation
Walkthrough이 풀 리퀘스트는 회원가입 프로세스의 유효성 검사를 Zod 스키마를 활용하여 개선하는 작업을 포함합니다. 기존의 인터페이스 기반 유효성 검사 방식에서 Zod 스키마 기반의 검증 방식으로 전환하여, 참여자와 연구자의 회원가입 과정에 대한 더욱 엄격하고 체계적인 데이터 검증을 구현했습니다. 이 변경은 폼 제출 시 데이터의 일관성과 정확성을 보장하기 위한 포괄적인 접근 방식을 제공합니다. Changes
Assessment against linked issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
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 (
|
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
🔭 Outside diff range comments (1)
src/app/join/components/JoinInput/JoinInput.tsx (1)
Line range hint
13-13
: control prop의 타입이 너무 광범위합니다.
any
타입 사용은 타입 안전성을 저해합니다.다음과 같이 수정하는 것을 제안합니다:
- control?: any; + control?: Control<any>;추가로 import 문도 필요합니다:
-import { Controller } from 'react-hook-form'; +import { Controller, Control } from 'react-hook-form';🧰 Tools
🪛 Biome (1.9.4)
[error] 63-63: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
[error] 70-70: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
🧹 Nitpick comments (19)
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (1)
22-22
: 접근성 향상을 위한 추가 제안
aria-invalid
속성이 잘 추가되었습니다. 접근성을 더욱 향상시키기 위해 다음과 같은 추가 속성들을 고려해보시는 것은 어떨까요?<button key={option.value} type="button" aria-invalid={isError} + role="radio" + aria-checked={selectedValue === option.value} + aria-describedby={isError ? `${option.value}-error` : undefined} css={(theme) => [src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (3)
14-17
: 입력값 유효성 검사를 개선하면 좋을 것 같습니다.현재 구현은 빈 문자열과 undefined만 체크하고 있습니다. 다음과 같은 개선사항을 제안드립니다:
- const isAllFilled = values.every((value) => value !== '' && value !== undefined); + const isAllFilled = values.every((value) => + value !== undefined && + value !== '' && + value?.trim().length > 0 + );
22-49
: 접근성 개선이 필요해 보입니다.폼 필드의 접근성을 높이기 위한 개선사항을 제안드립니다:
각
JoinInput
컴포넌트에 다음 속성들을 추가하면 좋을 것 같습니다:<JoinInput name="name" control={control} label="이름" required placeholder="이름(실명) 입력" + aria-required="true" + aria-invalid={!!errors.name} + aria-describedby="name-error" />
51-53
: 제출 버튼의 상태 처리를 개선하면 좋을 것 같습니다.폼 제출 시 중복 제출을 방지하고 로딩 상태를 표시하는 기능을 추가하면 좋을 것 같습니다:
+ const [isSubmitting, setIsSubmitting] = useState(false); + + const onSubmit = async () => { + if (isSubmitting) return; + setIsSubmitting(true); + try { + await handleSubmit(); + } finally { + setIsSubmitting(false); + } + }; - <button css={joinButton} onClick={handleSubmit} disabled={!isAllFilled}> + <button + css={joinButton} + onClick={onSubmit} + disabled={!isAllFilled || isSubmitting} + > - 회원가입 + {isSubmitting ? '처리중...' : '회원가입'} </button>src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (1)
11-13
: onChange 핸들러의 타입을 더 구체적으로 지정해주세요.현재
onChange
핸들러가 모든 문자열을 허용하고 있습니다. 가능한 값들을 제한하면 타입 안정성이 향상됩니다.다음과 같이 수정하는 것을 추천드립니다:
interface JoinSelectProps { placeholder: string; - onChange: (value: string) => void; + onChange: (value: FilterOption['value']) => void; isError?: boolean; options?: FilterOption[]; value?: string; }src/schema/join/ResearcherJoinSchema.ts (2)
8-8
: oauthEmail 필드에 사용자 정의 오류 메시지가 누락되었습니다.이메일 유효성 검사에 실패할 경우 사용자에게 친숙한 오류 메시지를 제공해야 합니다.
다음과 같이 수정하는 것을 제안합니다:
- oauthEmail: z.string().email(), + oauthEmail: z.string().email({ message: '이메일 형식이 올바르지 않아요' }),
45-45
: labInfo 필드에 문자 제한이 누락되었습니다.다른 필드들과 마찬가지로 한글과 영문만 허용하도록 제한하는 것이 일관성 있습니다.
다음과 같이 수정하는 것을 제안합니다:
- labInfo: z.string().max(100, { message: '최대 100자 이하로 입력해 주세요' }).optional(), + labInfo: z + .string() + .regex(/^[a-zA-Z가-힣\s-]*$/, { message: '한글, 영문, 공백, 하이픈만 입력 가능합니다' }) + .max(100, { message: '최대 100자 이하로 입력해 주세요' }) + .optional(),src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (1)
1-9
: import 문 순서를 수정해야 합니다.ESLint 규칙에 따라 import 문을 그룹화하고 정렬해야 합니다.
다음과 같이 수정하는 것을 제안합니다:
import { useFormContext, useWatch } from 'react-hook-form'; + +import JoinCheckboxContainer from '../../JoinCheckboxContainer/JoinCheckboxContainer'; +import JoinInput from '../../JoinInput/JoinInput'; import { nextButton } from './JoinEmailStep.styles'; -import JoinInput from '../../JoinInput/JoinInput'; import useServiceAgreeCheck from '@/app/join/hooks/useServiceAgreeCheck'; import { joinContentContainer, joinForm } from '@/app/join/JoinPage.styles'; import { ParticipantJoinSchemaType } from '@/schema/join/ParticipantJoinSchema'; -import JoinCheckboxContainer from '../../JoinCheckboxContainer/JoinCheckboxContainer';🧰 Tools
🪛 ESLint
[error] 8-8: There should be at least one empty line between import groups
(import/order)
[error] 9-9:
../../JoinCheckboxContainer/JoinCheckboxContainer
import should occur before import of../../JoinInput/JoinInput
(import/order)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (1)
5-11
: import 문 순서를 수정해야 합니다.ESLint 규칙에 따라 import 문을 그룹화하고 정렬해야 합니다.
다음과 같이 수정하는 것을 제안합니다:
+import JoinCheckboxContainer from '../../JoinCheckboxContainer/JoinCheckboxContainer'; +import JoinInput from '../../JoinInput/JoinInput'; import { joinContentContainer, nextButton } from './JoinEmailStep.styles'; -import JoinCheckboxContainer from '../../JoinCheckboxContainer/JoinCheckboxContainer'; import UnivAuthInput from './UnivAuthInput/UnivAuthInput'; -import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema'; import useServiceAgreeCheck from '@/app/join/hooks/useServiceAgreeCheck'; import { joinForm } from '@/app/join/JoinPage.styles'; -import JoinInput from '../../JoinInput/JoinInput'; +import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema';🧰 Tools
🪛 ESLint
[error] 6-6:
./UnivAuthInput/UnivAuthInput
import should occur before import of../../JoinCheckboxContainer/JoinCheckboxContainer
(import/order)
[error] 9-9:
@/app/join/hooks/useServiceAgreeCheck
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
[error] 10-10: There should be at least one empty line between import groups
(import/order)
[error] 10-10:
@/app/join/JoinPage.styles
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
[error] 11-11:
../../JoinInput/JoinInput
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
src/schema/join/ParticipantJoinSchema.ts (3)
13-16
: Provider 유효성 검사 메시지의 일관성 개선이 필요합니다.현재 'GOOGLE'에만 에러 메시지가 설정되어 있고 'NAVER'에는 없습니다. 일관성을 위해 모든 케이스에 메시지를 추가하는 것이 좋습니다.
provider: z.union([ z.literal('GOOGLE', { message: '유효한 도메인이 아닙니다' }), - z.literal('NAVER'), + z.literal('NAVER', { message: '유효한 도메인이 아닙니다' }), ]),
33-37
: Gender 유효성 검사 메시지의 일관성 개선이 필요합니다.현재 'MALE'에만 에러 메시지가 설정되어 있고 다른 옵션에는 없습니다. 모든 케이스에 동일한 메시지를 적용하는 것이 좋습니다.
gender: z.union([ z.literal('MALE', { message: '성별을 선택해주세요' }), - z.literal('FEMALE'), - z.literal('ALL'), + z.literal('FEMALE', { message: '성별을 선택해주세요' }), + z.literal('ALL', { message: '성별을 선택해주세요' }), ]),
76-76
: matchType에 유효성 검사 메시지 추가를 고려해보세요.선택적 필드이지만, 값이 제공될 경우 유효한 값인지 확인하는 메시지가 있으면 좋을 것 같습니다.
- matchType: z.union([z.literal('OFFLINE'), z.literal('ONLINE'), z.literal('ALL')]).optional(), + matchType: z.union([ + z.literal('OFFLINE', { message: '유효한 실험 진행 방식이 아닙니다' }), + z.literal('ONLINE', { message: '유효한 실험 진행 방식이 아닙니다' }), + z.literal('ALL', { message: '유효한 실험 진행 방식이 아닙니다' }) + ]).optional(),src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx (2)
14-15
: import 순서를 수정해 주세요.ESLint 규칙에 따라 import 문의 순서를 수정해야 합니다.
+import EmailToast from '@/app/join/components/EmailToast/EmailToast'; import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema'; -import EmailToast from '@/app/join/components/EmailToast/EmailToast';🧰 Tools
🪛 ESLint
[error] 15-15:
@/app/join/components/EmailToast/EmailToast
import should occur before import of@/app/join/hooks/useVerifyUnivAuthCodeMutation
(import/order)
17-17
: 인증 코드 길이 상수를 상수 파일로 분리하는 것을 고려해보세요.
AUTH_CODE_VALID_LENGTH
는 다른 컴포넌트에서도 사용될 수 있으므로, 상수 파일로 분리하는 것이 좋을 것 같습니다.상수를
@/constants/auth.ts
와 같은 파일로 분리하는 것을 추천드립니다.src/app/home/components/PostContainer/PostContainer.tsx (2)
Line range hint
23-29
: 구현된 TODO 주석을 제거해주세요.이미 구현된 것으로 보이는 TODO 주석들을 제거하는 것이 좋겠습니다.
31-35
: 역할 기반 쿼리 처리가 개선되었습니다.
useParticipantInfoQuery
와useResearcherInfoQuery
를 분리하여 처리한 것이 좋습니다. 다만, role 체크 로직을 좀 더 견고하게 만들 수 있을 것 같습니다.- const role = sessionStorage.getItem('role') || ''; + const role = sessionStorage.getItem('role'); + const isParticipant = role === ROLE.participant; + const isResearcher = role === ROLE.researcher; - const participantQuery = useParticipantInfoQuery({ enabled: role === ROLE.participant }); - const researcherQuery = useResearcherInfoQuery({ enabled: role === ROLE.researcher }); + const participantQuery = useParticipantInfoQuery({ enabled: isParticipant }); + const researcherQuery = useResearcherInfoQuery({ enabled: isResearcher });src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (2)
14-17
: 임포트 순서 개선 필요임포트 문이 ESLint 규칙을 따르지 않고 있습니다. 다음과 같이 수정이 필요합니다:
- 외부 라이브러리 임포트
- 절대 경로 임포트 (
@/
)- 상대 경로 임포트
import { useState } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; + import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema'; + import useSendUnivAuthCodeMutation from '@/app/join/hooks/useSendUnivAuthCodeMutation'; + import useAuthCodeTimer from '@/app/join/hooks/useAuthCodeTimer'; import AuthCodeInput from './AuthCodeInput/AuthCodeInput'; import { editButton, errorMessage, inputContainer, required, univAuthButton, univInputWrapper, } from './UnivAuthInput.styles'; + import EmailToast from '../../../EmailToast/EmailToast';🧰 Tools
🪛 ESLint
[error] 15-15:
@/app/join/hooks/useSendUnivAuthCodeMutation
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
[error] 16-16: There should be at least one empty line between import groups
(import/order)
[error] 16-16:
@/app/join/hooks/useAuthCodeTimer
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
[error] 17-17:
../../../EmailToast/EmailToast
import should occur before import of@/schema/join/ResearcherJoinSchema
(import/order)
70-70
: 불필요한 삼항 연산자 단순화
aria-invalid
속성의 불필요한 삼항 연산자를 단순화할 수 있습니다.- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (1)
42-55
: 이름 입력 필드의 유효성 검사 구현Controller를 사용하여 이름 입력 필드의 유효성 검사가 잘 구현되었습니다. 다만 다음과 같은 개선이 필요합니다:
- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}🧰 Tools
🪛 Biome (1.9.4)
[error] 50-51: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/apis/login.ts
(2 hunks)src/app/home/components/PostContainer/PostContainer.tsx
(2 hunks)src/app/home/hooks/useParticipantInfoQuery.ts
(1 hunks)src/app/home/hooks/useResearcherInfoQuery.ts
(1 hunks)src/app/home/hooks/useUserInfoQuery.ts
(0 hunks)src/app/join/JoinPage.types.ts
(0 hunks)src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx
(1 hunks)src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.tsx
(1 hunks)src/app/join/components/JoinInfoStep/JoinInfoStep.tsx
(0 hunks)src/app/join/components/JoinInput/JoinInput.tsx
(1 hunks)src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx
(3 hunks)src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
(3 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts
(2 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
(3 hunks)src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
(1 hunks)src/app/join/components/Researcher/index.tsx
(1 hunks)src/app/join/page.tsx
(6 hunks)src/schema/join/ParticipantJoinSchema.ts
(1 hunks)src/schema/join/ResearcherJoinSchema.ts
(1 hunks)
💤 Files with no reviewable changes (3)
- src/app/join/JoinPage.types.ts
- src/app/join/components/JoinInfoStep/JoinInfoStep.tsx
- src/app/home/hooks/useUserInfoQuery.ts
✅ Files skipped from review due to trivial changes (2)
- src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.tsx
- src/app/join/components/Researcher/index.tsx
🧰 Additional context used
🪛 ESLint
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx
[error] 15-15: @/app/join/components/EmailToast/EmailToast
import should occur before import of @/app/join/hooks/useVerifyUnivAuthCodeMutation
(import/order)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
[error] 6-6: ./UnivAuthInput/UnivAuthInput
import should occur before import of ../../JoinCheckboxContainer/JoinCheckboxContainer
(import/order)
[error] 9-9: @/app/join/hooks/useServiceAgreeCheck
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 10-10: There should be at least one empty line between import groups
(import/order)
[error] 10-10: @/app/join/JoinPage.styles
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 11-11: ../../JoinInput/JoinInput
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx
[error] 8-8: There should be at least one empty line between import groups
(import/order)
[error] 9-9: ../../JoinCheckboxContainer/JoinCheckboxContainer
import should occur before import of ../../JoinInput/JoinInput
(import/order)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
[error] 15-15: @/app/join/hooks/useSendUnivAuthCodeMutation
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 16-16: There should be at least one empty line between import groups
(import/order)
[error] 16-16: @/app/join/hooks/useAuthCodeTimer
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 17-17: ../../../EmailToast/EmailToast
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
[error] 13-13: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx
[error] 12-12: ../../JoinInput/JoinInput.styles
import should occur before import of @/components/Icon
(import/order)
🪛 Biome (1.9.4)
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
[error] 50-51: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
[error] 71-72: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
🔇 Additional comments (33)
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (2)
7-7
: 인터페이스에 isError 속성이 잘 추가되었습니다!폼 유효성 검사를 위한 에러 상태 관리가 잘 반영되었습니다.
14-14
: props 구조분해가 올바르게 되었습니다!새로 추가된 isError prop이 컴포넌트에 잘 전달되도록 구성되었습니다.
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx (1)
Line range hint
1-58
: 구현이 깔끔하고 잘 되어있습니다! 👍
- TypeScript 타입이 명확하게 정의되어 있습니다.
- 필수 필드에 대한 오류 처리가 적절히 구현되어 있습니다.
- 테마 시스템이 일관되게 적용되어 있습니다.
🧰 Tools
🪛 ESLint
[error] 11-11: There should be at least one empty line between import groups
(import/order)
[error] 12-12:
../../JoinInput/JoinInput.styles
import should occur before import of@/components/Icon
(import/order)
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (1)
1-11
: 코드가 깔끔하고 잘 구성되어 있습니다!필요한 모든 import문이 잘 정리되어 있고, Props 인터페이스가 명확하게 정의되어 있습니다.
src/app/join/page.tsx (7)
3-3
: 새로운 zodResolver 임포트
zod 스키마 검증을 간단하게 적용하기 위해 적절한 라이브러리를 사용하고 있습니다.
25-27
: ParticipantJoinSchema 및 타입 선언 추가
Zod 스키마를 통해 참여자 가입 정보를 구조적으로 검증할 수 있도록 잘 설정되었습니다.
43-46
: 연구자 폼 useForm 설정
zodResolver(ResearcherJoinSchema())
적용으로 유효성 검증을 명확히 처리하고 있습니다.mode: 'onBlur'
와reValidateMode: 'onChange'
설정 또한 의도에 맞게 잘 활용되고 있습니다.
58-61
: 참여자 폼 useForm 설정
참여자 폼 역시 Zod 스키마를 적용하여 유효성 검증 로직을 간소화했습니다. 이로써 검증 시점이 명확해지고, 폼 처리 흐름도 자연스러워졌습니다.
65-66
: 기본값 name, birthDate 추가
기본값 설정으로 폼 초기 상태를 명확하게 했습니다. 생년월일 등 날짜 처리 시 포맷 검사 로직이 올바르게 동작하는지 확인해 보시는 게 좋겠습니다.
107-109
: Researcher.InfoStep에 handleSubmit 전달
폼 제출 시점에 자동으로 Zod 검증이 수행되도록 구성되어 있어, 코드 유지보수가 편리해 보입니다.
137-139
: Participant.InfoStep에 handleSubmit 전달
위와 동일한 방식으로 참여자 폼도 스텝 전환 시 검증이 처리되어, 일관성 있는 UX를 구현했습니다.src/app/home/hooks/useResearcherInfoQuery.ts (4)
1-4
: React Query 및 API 함수 임포트
useQuery
와getResearcherInfo
를 적절히 가져와, 연구자 정보를 손쉽게 가져올 수 있도록 구성했습니다.
5-8
: useResearcherInfoQueryProps 인터페이스
enabled
필드를 이용해 쿼리 실행을 제어하는 구조가 명확합니다. 불필요한 필드가 없어 유지보수 면에서도 깔끔합니다.
9-16
: useResearcherInfoQuery 훅 구현
useQuery
로 데이터를 불러오며retry
옵션으로 재시도 횟수를 제한한 점이 안정적입니다. 필요한 설정이 간결하게 잘 드러나 있습니다.
18-18
: 기본 내보내기
해당 훅을 기본 내보내기로 선언하여 재사용성과 임포트 편의성이 높아졌습니다.src/app/home/hooks/useParticipantInfoQuery.ts (4)
1-4
: React Query 및 API 함수 임포트
참여자 정보를 가져오기 위한useQuery
와getParticipantInfo
가 잘 연결되어 있습니다.
5-8
: useParticipantInfoQueryProps 인터페이스
enabled
필드로 쿼리 실행 여부를 제어함으로써, 컴포넌트에서 조건부로 데이터 요청을 수행하기 쉽게 구성했습니다.
9-16
: useParticipantInfoQuery 훅 구현
쿼리 키와 함수가 명확하게 설정되어 있으며,retry: 1
로 무한 재시도를 방지하는 점도 적절합니다.
18-18
: 기본 내보내기
훅을 모듈의 기본 값으로 내보내서, 다른 파일에서 유연하게 불러올 수 있도록 했습니다.src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts (1)
30-32
: 오류 상태 스타일링이 적절하게 구현되었습니다!
aria-invalid
속성을 활용한 오류 상태 표시가 접근성과 사용자 경험을 향상시킵니다.src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts (2)
21-23
: 오류 상태 스타일링이 적절하게 구현되었습니다!
aria-invalid
속성을 활용한 오류 상태 표시가 접근성과 사용자 경험을 향상시킵니다.
4-5
: 주석 처리된 스타일의 필요성을 확인해주세요.font와 color 스타일이 주석 처리되어 있습니다. 이 스타일들이 다른 곳에서 적용되고 있는지 확인이 필요합니다.
✅ Verification successful
불필요한 주석 처리된 스타일 코드를 제거해주세요.
해당 스타일들이 같은 파일 내 다른 부분에서 이미 적용되어 있어 주석 처리된 코드는 불필요합니다. 코드 가독성을 위해 제거를 권장드립니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Check if these styles are defined elsewhere rg "theme\.fonts\.body\.normal\.M16" --type ts rg "theme\.colors\.text06" --type tsLength of output: 6598
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx (1)
38-52
: react-hook-form Controller 구현이 잘 되었습니다!폼 상태 관리와 에러 처리가 깔끔하게 구현되었습니다.
fieldState.error
를 활용한 에러 메시지 표시도 적절합니다.src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (1)
22-22
: 접근성이 잘 고려된 구현입니다!
aria-invalid
속성을 활용하여 오류 상태를 스크린 리더에 전달하는 것이 좋습니다.src/app/join/components/JoinInput/JoinInput.tsx (1)
54-54
: defaultValue 설정이 불필요할 수 있습니다.Controller는 이미 field value를 관리하고 있으므로, defaultValue 설정이 중복될 수 있습니다.
다음 스크립트로 Controller의 사용 패턴을 확인해보세요:
src/schema/join/ParticipantJoinSchema.ts (1)
39-54
: 생년월일 유효성 검사 로직이 잘 구현되어 있습니다.정규식과 refine을 사용하여 날짜 형식과 유효성을 모두 검증하는 방식이 훌륭합니다.
src/apis/login.ts (1)
89-98
: 타입 변경이 잘 이루어졌습니다.
ResearcherJoinSchemaType
와ParticipantJoinSchemaType
로의 타입 변경이 적절하게 이루어졌습니다. 이는 zod 스키마를 통한 런타임 유효성 검사와 타입 안정성을 모두 확보할 수 있게 해줍니다.src/app/home/components/PostContainer/PostContainer.tsx (1)
29-29
: FIXME 주석에서 언급된 이중 쿼리 호출 문제를 해결해주세요.참여자의 경우 쿼리가 두 번 호출되는 문제가 있다고 FIXME 주석에 명시되어 있습니다. 이 문제를 해결하기 위한 작업이 필요합니다.
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (2)
32-32
: useWatch 사용으로 반응성 개선
getValues
대신useWatch
를 사용하여 폼 값의 변경을 실시간으로 감지할 수 있도록 개선되었습니다.
82-83
: 에러 처리 개선필드 상태와 서버 에러를 명확하게 구분하여 표시하는 방식으로 개선되었습니다.
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (3)
27-31
: 폼 컨텍스트 타입 개선
ParticipantJoinSchemaType
을 사용하여 타입 안정성이 향상되었습니다.useWatch
를 사용하여 지역 선택의 반응성도 개선되었습니다.
92-117
: 지역 선택 컴포넌트의 에러 처리기본 지역과 추가 활동 지역 선택에 대한 에러 처리가 일관성 있게 구현되었습니다. 특히 다음 사항이 잘 처리되었습니다:
- 필수 입력 검증
- 하위 지역 선택 시 상위 지역 의존성
- 에러 상태 표시
Also applies to: 129-156
175-177
: 회원가입 버튼 핸들러 개선
onNext
대신handleSubmit
을 사용하여 폼 제출 로직이 더 명확해졌습니다.
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx
Outdated
Show resolved
Hide resolved
.../components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
Show resolved
Hide resolved
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
Outdated
Show resolved
Hide resolved
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: 10
🔭 Outside diff range comments (2)
src/app/home/components/PostContainer/PostContainer.tsx (1)
Line range hint
38-67
: 필터 상태 관리 로직 개선 필요필터 상태 관리가 복잡하고 여러 곳에 분산되어 있습니다. 다음과 같은 개선을 제안합니다:
- 필터 로직을 별도의 커스텀 훅으로 분리
- 필터 상태 업데이트 로직 단순화
// usePostFilters.ts const usePostFilters = (participantInfo: ParticipantInfo | undefined) => { const [filters, setFilters] = useState<PostListParams>({ recruitStatus: 'ALL', }); const updateFilter = (key: keyof PostListParams, value: any) => { setFilters(prev => ({ ...prev, [key]: value })); }; const toggleRecruitingFilter = () => { updateFilter('recruitStatus', filters.recruitStatus === 'OPEN' ? 'ALL' : 'OPEN'); }; useEffect(() => { if (participantInfo) { setFilters(prev => ({ ...prev, gender: participantInfo.gender, region: participantInfo.basicAddressInfo.region, matchType: participantInfo.matchType, areas: participantInfo.basicAddressInfo.area, })); } }, [participantInfo]); return { filters, updateFilter, toggleRecruitingFilter, isRecruiting: filters.recruitStatus === 'OPEN', }; };src/app/join/components/JoinInput/JoinInput.tsx (1)
Line range hint
13-14
: control prop 타입 개선 필요
control
prop의 타입이any
로 설정되어 있어 타입 안전성이 보장되지 않습니다.다음과 같이 수정하는 것을 제안합니다:
- // eslint-disable-next-line @typescript-eslint/no-explicit-any - control?: any; + import { Control, FieldValues } from 'react-hook-form'; + control?: Control<FieldValues>;🧰 Tools
🪛 Biome (1.9.4)
[error] 63-63: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
[error] 70-70: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
🧹 Nitpick comments (16)
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx (2)
12-12
: import 구문의 순서를 수정해 주세요.import 구문의 순서가 프로젝트의 컨벤션과 맞지 않습니다. 외부 라이브러리 import 이후에 내부 경로 import가 위치해야 합니다.
다음과 같이 수정해 주세요:
-import { tipAlert, tipWrapper } from '../../JoinInput/JoinInput.styles'; +import Icon from '@/components/Icon'; +import theme from '@/styles/theme'; +import { tipAlert, tipWrapper } from '../../JoinInput/JoinInput.styles';🧰 Tools
🪛 ESLint
[error] 12-12:
../../JoinInput/JoinInput.styles
import should occur before import of@/components/Icon
(import/order)
Line range hint
14-21
: onChange 이벤트 타입을 명시적으로 지정해 주세요.컴포넌트 구현이 전반적으로 잘 되어있습니다. 다만,
any
타입 사용을 피하고 더 명확한 타입 정의를 위해 onChange 이벤트 타입을 구체적으로 지정하는 것이 좋겠습니다.다음과 같이 수정해 주세요:
interface JoinCheckboxProps { label: string; isChecked: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onChange: (e: any) => void; + onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; isRequired?: boolean; isAllCheck?: boolean; isArrow?: boolean; isAlert?: boolean; }🧰 Tools
🪛 ESLint
[error] 11-11: There should be at least one empty line between import groups
(import/order)
[error] 12-12:
../../JoinInput/JoinInput.styles
import should occur before import of@/components/Icon
(import/order)
src/app/home/hooks/useResearcherInfoQuery.ts (2)
5-7
: 인터페이스 네이밍 컨벤션 개선 필요인터페이스 이름이 소문자로 시작하고 있습니다. TypeScript 네이밍 컨벤션에 따라 인터페이스는 대문자로 시작해야 합니다.
-interface useResearcherInfoQueryProps { +interface UseResearcherInfoQueryProps { enabled: boolean; }
9-16
: 쿼리 설정 검토 필요쿼리 구성이 잘 되어 있으나, 몇 가지 개선사항이 있습니다:
- staleTime 설정 추가 고려
- 에러 발생 시 retry 횟수가 적절한지 검토
- cacheTime 설정 추가 고려
const useResearcherInfoQuery = ({ enabled }: useResearcherInfoQueryProps) => { return useQuery({ queryKey: ['researcherInfo'], queryFn: getResearcherInfo, enabled, retry: 1, + staleTime: 5 * 60 * 1000, // 5분 + cacheTime: 30 * 60 * 1000, // 30분 }); };src/app/home/hooks/useParticipantInfoQuery.ts (1)
1-18
: 공통 로직 추출 고려useResearcherInfoQuery와 useParticipantInfoQuery 간에 중복된 로직이 많습니다. 공통 로직을 추출하여 재사용성을 높이는 것을 고려해보세요.
// useUserInfoBase.ts interface UseUserInfoQueryProps { enabled: boolean; queryKey: string; queryFn: () => Promise<any>; } const useUserInfoBase = ({ enabled, queryKey, queryFn }: UseUserInfoQueryProps) => { return useQuery({ queryKey: [queryKey], queryFn, enabled, retry: 1, staleTime: 5 * 60 * 1000, cacheTime: 30 * 60 * 1000, }); }; // useParticipantInfoQuery.ts const useParticipantInfoQuery = ({ enabled }: UseParticipantInfoQueryProps) => { return useUserInfoBase({ enabled, queryKey: 'participantInfo', queryFn: getParticipantInfo, }); };src/app/home/components/PostContainer/PostContainer.tsx (1)
Line range hint
23-31
: TODO 주석 처리 필요현재 TODO 주석들이 코드에 남아있습니다. 이슈 트래커로 이동하고 주석을 제거하는 것이 좋습니다.
이슈를 생성하고 TODO 항목들을 이동하는 것을 도와드릴까요?
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (1)
36-56
:이름
필드를Controller
로 관리하여 에러 메시지를 직접 표시하는 접근이 좋습니다.
- 사용자 피드백을 즉시 제공해 UX가 향상되었습니다.
- 다만, 다음 라인의 삼항 연산자는 불필요해 보입니다.
aria-invalid
에 직접 boolean을 전달하는 편이 좋겠습니다.아래와 같이 수정이 가능합니다:
- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}🧰 Tools
🪛 Biome (1.9.4)
[error] 50-51: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (1)
43-49
: 연구실 정보 필드에 대한 최대 길이 제한 추가 필요textarea 타입의 입력 필드에 최대 길이 제한이 설정되어 있지 않습니다. 데이터베이스 필드 길이 제한과의 일관성을 위해 제한을 추가하는 것이 좋습니다.
다음과 같이 수정하는 것을 제안합니다:
<JoinInput name="labInfo" control={control} label="소속 연구실 정보" placeholder="연구실 정보 입력" type="textarea" + maxLength={200} />
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (1)
1-1
: import 구문 정리 필요import 구문의 순서가 일관되지 않아 코드의 가독성이 저하됩니다.
다음과 같이 수정하는 것을 제안합니다:
+ import { useFormContext, useWatch } from 'react-hook-form'; + + import { ParticipantJoinSchemaType } from '@/schema/join/ParticipantJoinSchema'; + import useServiceAgreeCheck from '@/app/join/hooks/useServiceAgreeCheck'; + import { joinContentContainer, joinForm } from '@/app/join/JoinPage.styles'; + + import JoinCheckboxContainer from '../../JoinCheckboxContainer/JoinCheckboxContainer'; + import JoinInput from '../../JoinInput/JoinInput';Also applies to: 8-9
src/app/join/components/JoinInput/JoinInput.tsx (1)
Line range hint
58-75
: 접근성 개선 필요입력 필드의 접근성을 개선하기 위해 추가적인 ARIA 속성이 필요합니다.
다음과 같이 수정하는 것을 제안합니다:
<input {...field} placeholder={placeholder} disabled={disabled} maxLength={maxLength} aria-invalid={fieldState.invalid ? true : false} + aria-required={required} + aria-describedby={fieldState.error ? `${name}-error` : undefined} /> ... - {fieldState.error && <span css={errorMessage}>{fieldState.error.message}</span>} + {fieldState.error && ( + <span css={errorMessage} id={`${name}-error`} role="alert"> + {fieldState.error.message} + </span> + )}🧰 Tools
🪛 Biome (1.9.4)
[error] 63-63: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
[error] 70-70: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (1)
33-34
: 이메일 인증 상태 관리 개선 필요현재 이메일 인증 상태가 단순 boolean 값으로 관리되고 있어, 인증 과정에서 발생할 수 있는 다양한 상태를 처리하기 어렵습니다.
상태 관리를 다음과 같이 개선하는 것을 제안합니다:
- const [isEmailVerified, setIsEmailVerified] = useState(false); + const [emailVerificationStatus, setEmailVerificationStatus] = useState<{ + status: 'idle' | 'pending' | 'verified' | 'failed'; + message?: string; + }>({ status: 'idle' }); + + const isEmailVerified = emailVerificationStatus.status === 'verified';src/schema/join/ParticipantJoinSchema.ts (1)
13-16
: Provider 유효성 검사 개선이 필요해 보입니다.'NAVER' 리터럴에도 'GOOGLE'과 동일하게 에러 메시지를 추가하는 것이 일관성 있어 보입니다.
provider: z.union([ z.literal('GOOGLE', { message: '유효한 도메인이 아닙니다' }), - z.literal('NAVER'), + z.literal('NAVER', { message: '유효한 도메인이 아닙니다' }), ]),src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx (1)
14-15
: import 구문 순서를 수정해 주세요.import 구문의 순서가 ESLint 규칙과 맞지 않습니다. 절대 경로 import를 상대 경로 import 전에 배치해 주세요.
-import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema'; -import EmailToast from '@/app/join/components/EmailToast/EmailToast'; +import EmailToast from '@/app/join/components/EmailToast/EmailToast'; +import { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema';🧰 Tools
🪛 ESLint
[error] 15-15:
@/app/join/components/EmailToast/EmailToast
import should occur before import of@/app/join/hooks/useVerifyUnivAuthCodeMutation
(import/order)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (2)
70-70
: aria-invalid 속성 개선이 필요합니다.불필요한 삼항 연산자를 제거하고 직접 boolean 값을 할당하는 것이 더 명확합니다.
- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}
82-83
: 에러 메시지 처리 방식 개선이 필요합니다.현재 구현은 필드 에러와 API 에러를 별도로 표시하고 있습니다. 사용자 경험 향상을 위해 에러 메시지를 통합하여 표시하는 것이 좋을 것 같습니다.
- {fieldState.error && <span css={errorMessage}>{fieldState.error.message}</span>} - {sendError && <span css={errorMessage}>{sendError.message}</span>} + {(fieldState.error || sendError) && ( + <span css={errorMessage}> + {fieldState.error?.message || sendError?.message} + </span> + )}src/app/join/page.tsx (1)
43-46
: 폼 검증 모드 최적화가 필요합니다.현재 구성된 검증 모드는 다음과 같은 잠재적 문제가 있습니다:
mode: 'onBlur'
와reValidateMode: 'onChange'
의 조합은 불필요한 재검증을 발생시킬 수 있습니다.- 사용자 경험 측면에서 실시간 피드백이 과도할 수 있습니다.
다음과 같은 설정을 고려해보세요:
const researcherMethods = useForm<ResearcherJoinSchemaType>({ resolver: zodResolver(ResearcherJoinSchema()), - mode: 'onBlur', - reValidateMode: 'onChange', + mode: 'onChange', + reValidateMode: 'onBlur',
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (25)
src/apis/login.ts
(2 hunks)src/app/home/components/PostContainer/PostContainer.tsx
(2 hunks)src/app/home/hooks/useParticipantInfoQuery.ts
(1 hunks)src/app/home/hooks/useResearcherInfoQuery.ts
(1 hunks)src/app/home/hooks/useUserInfoQuery.ts
(0 hunks)src/app/join/JoinPage.types.ts
(0 hunks)src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx
(1 hunks)src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.tsx
(1 hunks)src/app/join/components/JoinInfoStep/JoinInfoStep.tsx
(0 hunks)src/app/join/components/JoinInput/JoinInput.tsx
(1 hunks)src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx
(3 hunks)src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
(3 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts
(2 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx
(1 hunks)src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx
(2 hunks)src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
(3 hunks)src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
(1 hunks)src/app/join/components/Researcher/index.tsx
(1 hunks)src/app/join/page.tsx
(6 hunks)src/schema/join/ParticipantJoinSchema.ts
(1 hunks)src/schema/join/ResearcherJoinSchema.ts
(1 hunks)
💤 Files with no reviewable changes (3)
- src/app/join/components/JoinInfoStep/JoinInfoStep.tsx
- src/app/join/JoinPage.types.ts
- src/app/home/hooks/useUserInfoQuery.ts
✅ Files skipped from review due to trivial changes (2)
- src/app/join/components/Researcher/index.tsx
- src/app/join/components/JoinCheckboxContainer/JoinCheckboxContainer.tsx
🧰 Additional context used
🪛 ESLint
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx
[error] 15-15: @/app/join/components/EmailToast/EmailToast
import should occur before import of @/app/join/hooks/useVerifyUnivAuthCodeMutation
(import/order)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
[error] 15-15: @/app/join/hooks/useSendUnivAuthCodeMutation
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 16-16: There should be at least one empty line between import groups
(import/order)
[error] 16-16: @/app/join/hooks/useAuthCodeTimer
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 17-17: ../../../EmailToast/EmailToast
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
[error] 13-13: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx
[error] 8-8: There should be at least one empty line between import groups
(import/order)
[error] 9-9: ../../JoinCheckboxContainer/JoinCheckboxContainer
import should occur before import of ../../JoinInput/JoinInput
(import/order)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
[error] 6-6: ./UnivAuthInput/UnivAuthInput
import should occur before import of ../../JoinCheckboxContainer/JoinCheckboxContainer
(import/order)
[error] 9-9: @/app/join/hooks/useServiceAgreeCheck
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 10-10: There should be at least one empty line between import groups
(import/order)
[error] 10-10: @/app/join/JoinPage.styles
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
[error] 11-11: ../../JoinInput/JoinInput
import should occur before import of @/schema/join/ResearcherJoinSchema
(import/order)
src/app/join/components/JoinCheckboxContainer/JoinCheckbox/JoinCheckbox.tsx
[error] 12-12: ../../JoinInput/JoinInput.styles
import should occur before import of @/components/Icon
(import/order)
🪛 Biome (1.9.4)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
[error] 71-72: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
[error] 50-51: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
🔇 Additional comments (20)
src/app/home/hooks/useParticipantInfoQuery.ts (1)
5-7
: 인터페이스 네이밍 컨벤션 개선 필요useResearcherInfoQuery와 동일한 네이밍 이슈가 있습니다.
-interface useParticipantInfoQueryProps { +interface UseParticipantInfoQueryProps { enabled: boolean; }src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (9)
1-1
: 새로운 import 구성이 적절합니다.
리팩터링 의도와 맞게 필요한 모듈들이 명확히 임포트되었습니다.Also applies to: 16-16, 20-20
24-24
:handleSubmit
프로퍼티 정의가 직관적입니다.
폼 제출 방식이 명확해져 가독성과 유지보수성이 향상되었습니다.
27-31
:useFormContext
와useWatch
활용이 좋습니다.
control
과setValue
를 구조분해 할당하여 재사용을 높이고 있습니다.useWatch
를 통해 지역별 선택 상태를 실시간으로 반영하는 로직이 직관적입니다.
58-62
: 성별을RadioButtonGroupContainer
로 관리하여 가독성이 올라갑니다.
onChange
를 통해gender
상태를 별도 관리하는 점이 분명합니다.- 나중에 수정 불가능하다는 안내 팁도 명확하여 사용자 혼동을 줄일 수 있습니다.
73-73
:생년월일
필드 구현이 간결하고 명확합니다.
JoinInput
컴포넌트로 필드 정의와 유효성 검증을 분리한 점이 좋습니다.- 날짜 포맷(
YYYY.MM.DD
) 안내가 명시되어 있어 사용자 이해가 용이합니다.Also applies to: 84-84
85-117
: 거주 지역(basicAddressInfo
)을 두 단계를 선택할 수 있도록 구분한 구조가 합리적입니다.
Controller
와JoinSelect
를 조합해 시·도, 시·군·구를 분리함으로써 사용자가 보다 직관적으로 입력 가능합니다.selectedArea
값에 따라JOIN_SUB_REGION
을 동적으로 바인딩하는 점도 깔끔합니다.
121-156
: 추가 활동 지역(additionalAddressInfo
) 필드가 기본 거주 지역과 동일한 로직을 재사용하여 통일감이 있습니다.
useWatch
로 추가 지역 상태를 별도로 관리하여 반복 코드가 최소화되었습니다.- ‘추가 활동 지역’이라는 안내가 사용자에게 확실히 구분되어 표시됩니다.
160-164
: 선호 실험 진행 방식(matchType
) 설정이 명료합니다.
RadioButtonGroupContainer
활용으로 UI의 일관성이 유지됩니다.- 후속 검증(예: 온라인/오프라인 선택)에 대해서도 추가 확장이 용이해 보입니다.
175-175
: '회원가입' 버튼의 콜백이handleSubmit
로 통일되어 있습니다.
- 폼 제출 로직이 중앙화되어, 다른 스텝과의 체계적 통합에 용이합니다.
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.tsx (1)
7-7
:isError
속성 추가로 에러 상태를 명확히 처리하고 있습니다.
aria-invalid
속성이 버튼에 적용되어 접근성을 높입니다.- 선택 상태(
selectedValue
)와 에러 상태(isError
)가 시각적으로 분리 가능하여 사용자 피드백에 도움이 됩니다.Also applies to: 14-14, 22-22
src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts (1)
29-32
: 에러 상태 시 outline을 적용하여 즉각적인 시인성을 확보했습니다.
aria-invalid="true"
일 때만 테두리가 생겨 시각적 구분이 분명합니다.- 접근성 향상에 유용한 스타일 변경으로 보입니다.
src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts (2)
4-5
: 주석 처리된 스타일 코드를 제거하거나 복원해주세요.주석 처리된 스타일 코드가 실제로 불필요한 것인지 확인이 필요합니다. 불필요하다면 제거하고, 필요하다면 복원해주세요.
21-23
: 에러 상태 스타일링이 잘 구현되었습니다.
aria-invalid
속성을 활용한 에러 상태 스타일링이 적절하게 구현되었습니다.src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx (1)
38-52
: Controller 컴포넌트 통합이 잘 구현되었습니다.react-hook-form의 Controller를 사용한 폼 상태 관리와 에러 처리가 적절하게 구현되었습니다.
src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx (1)
12-12
: 에러 상태 처리가 잘 구현되었습니다.
- isError prop 추가와 aria-invalid 속성 연동이 적절하게 구현되었습니다.
- 접근성 표준을 잘 준수하고 있습니다.
Also applies to: 17-17, 22-22
src/schema/join/ResearcherJoinSchema.ts (1)
7-45
: Zod 스키마가 잘 구현되었습니다.
- 각 필드별 유효성 검사 규칙이 명확하게 정의되어 있습니다.
- 에러 메시지가 사용자 친화적으로 작성되었습니다.
- 필수/선택 필드 구분이 잘 되어있습니다.
src/schema/join/ParticipantJoinSchema.ts (1)
56-73
: 주소 유효성 검사 개선이 필요합니다.현재는 문자열만 검사하고 있습니다. 실제 존재하는 주소인지 확인하는 로직이나 허용된 지역 목록과 대조하는 검증이 추가되면 좋을 것 같습니다.
src/apis/login.ts (1)
89-98
: LGTM! 스키마 타입 적용이 잘 되었습니다.
ResearcherJoinSchemaType
와ParticipantJoinSchemaType
으로의 타입 변경이 적절하게 이루어졌습니다.src/app/join/page.tsx (1)
3-3
: Zod 스키마 통합이 잘 구현되었습니다!zodResolver와 스키마 타입의 통합이 깔끔하게 이루어졌습니다.
Also applies to: 25-28
.../components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroupContainer.tsx
Show resolved
Hide resolved
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
Outdated
Show resolved
Hide resolved
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
Outdated
Show resolved
Hide resolved
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/AuthCodeInput/AuthCodeInput.tsx
Show resolved
Hide resolved
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.
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.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.
Actionable comments posted: 3
🔭 Outside diff range comments (2)
src/app/join/JoinPage.utils.ts (1)
Zod 스키마를 활용한 구현으로 전환이 필요합니다.
이미
src/schema/join
디렉토리에 Zod를 활용한 이메일 검증 스키마가 구현되어 있습니다. 현재 구현을 다음과 같이 개선할 것을 제안드립니다:
ResearcherJoinSchema
나ParticipantJoinSchema
의 이메일 검증 로직을 재사용- provider 검증도 기존 스키마의
z.union([z.literal('GOOGLE'), z.literal('NAVER')])
활용- 에러 메시지도 기존 스키마와 일관성 있게 통일
이를 통해 코드 중복을 제거하고 검증 로직의 일관성을 유지할 수 있습니다.
🔗 Analysis chain
Line range hint
7-24
: PR 목표와의 일치성 확인이 필요합니다.PR의 주요 목표는 Zod를 활용한 유효성 검사 구현인데, 현재 구현은 기본적인 조건문을 사용하고 있습니다. Zod 스키마를 활용한 구현으로 전환하면 다음과 같은 이점이 있습니다:
- 타입 안정성 향상
- 재사용 가능한 스키마 정의
- 더 강력한 유효성 검사 기능
다음 스크립트를 실행하여 현재 프로젝트에서 Zod 스키마의 사용 현황을 확인해보세요:
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Zod 스키마 사용 현황 확인 echo "Checking Zod schema usage..." rg -l "z\.string\(\)" src/ rg -l "z\.object\(\)" src/Length of output: 253
Script:
#!/bin/bash # Check email validation patterns in existing Zod schemas echo "Examining join schema implementations..." rg "email" src/schema/join/ -A 5 -B 5Length of output: 3438
src/app/join/page.tsx (1)
Line range hint
71-83
: 폼 제출 핸들러의 개선이 필요합니다.현재 구현에서 다음과 같은 개선사항이 있습니다:
- getValues 대신 폼 데이터 직접 사용
- 생년월일 포맷팅 로직 분리
다음과 같이 수정하는 것을 추천드립니다:
-const handleResearcherSubmit = () => { - const formData = researcherMethods.getValues(); +const handleResearcherSubmit = (formData: ResearcherJoinSchemaType) => { joinResearcher(formData, { onSuccess: () => setStep(STEP.success) }); }; -const handleParticipantSubmit = () => { - const formData = participantMethods.getValues(); +const handleParticipantSubmit = (formData: ParticipantJoinSchemaType) => { + const formatBirthDate = (date: string) => date.replaceAll('.', '-'); const formattedData = { ...formData, - birthDate: formData.birthDate.replaceAll('.', '-'), + birthDate: formatBirthDate(formData.birthDate), }; joinParticipant(formattedData, { onSuccess: () => setStep(STEP.success), }); };
♻️ Duplicate comments (3)
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (1)
23-29
: 🛠️ Refactor suggestion에러 처리 로직 개선이 필요합니다.
handleNextStep
함수에서 유효성 검사 실패 시 사용자에게 적절한 피드백이 제공되지 않습니다.다음과 같이 수정하는 것을 제안합니다:
const handleNextStep = async () => { - const isValid = await trigger(['oauthEmail', 'contactEmail']); - - if (isValid) { - onNext(); - } + try { + const isValid = await trigger(['oauthEmail', 'contactEmail']); + if (isValid) { + onNext(); + } else { + alert('이메일 정보를 올바르게 입력해주세요.'); + } + } catch (error) { + console.error('이메일 검증 중 오류 발생:', error); + alert('이메일 검증 중 오류가 발생했습니다. 다시 시도해주세요.'); + } };src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
22-22
: 🛠️ Refactor suggestion이메일 인증 상태 관리 개선이 필요합니다.
현재 이메일 인증 상태가 컴포넌트의 로컬 상태로만 관리되어 페이지 새로고침 시 초기화됩니다.
폼 컨텍스트를 통해 상태를 관리하도록 수정하는 것을 제안합니다:
- const [isEmailVerified, setIsEmailVerified] = useState(false); + const { setValue, watch } = useFormContext<ResearcherJoinSchemaType>(); + const isEmailVerified = watch('isEmailVerified'); const handleVerifyEmail = () => { - setIsEmailVerified(true); + setValue('isEmailVerified', true); };
ResearcherJoinSchema
도 다음과 같이 수정이 필요합니다:const ResearcherJoinSchema = z.object({ // ... 기존 필드들 isEmailVerified: z.boolean().default(false), });Also applies to: 33-35
24-30
: 🛠️ Refactor suggestion에러 처리 로직 개선이 필요합니다.
handleNextStep
함수에서 유효성 검사 실패 시 사용자에게 적절한 피드백이 제공되지 않습니다.다음과 같이 수정하는 것을 제안합니다:
const handleNextStep = async () => { - const isValid = await trigger(['oauthEmail', 'contactEmail']); - - if (isValid) { - onNext(); - } + try { + const isValid = await trigger(['oauthEmail', 'contactEmail']); + if (isValid) { + onNext(); + } else { + alert('이메일 정보를 올바르게 입력해주세요.'); + } + } catch (error) { + console.error('이메일 검증 중 오류 발생:', error); + alert('이메일 검증 중 오류가 발생했습니다. 다시 시도해주세요.'); + } };
🧹 Nitpick comments (15)
src/app/upload/components/RegionPopover/RegionPopover.styles.ts (1)
Line range hint
15-21
: 스타일 정의 단순화가 잘 되었습니다!테마 속성을 사용하지 않는 스타일을 정적 CSS 블록으로 변경한 것이 적절합니다. 이는 코드를 더 간단하고 명확하게 만듭니다.
다만, 스타일의 일관성을 위해 다음과 같은 개선사항을 고려해보세요:
- 다른 스타일 정의들과 마찬가지로 rem 단위 사용 시 소수점 앞의 0을 추가 (예: 0.1rem)
- border-radius 값을 다른 컴포넌트들과 일치시키는 것이 좋을 수 있습니다
export const regionField = css` display: flex; flex-flow: row nowrap; justify-content: space-between; align-items: center; - border-radius: 1.2rem; + border-radius: 0.12rem; `;src/app/join/components/Participant/JoinInfoStep/RadioButtonGroupContainer/RadioButtonGroup/RadioButtonGroup.styles.ts (1)
30-32
: LGTM! 접근성을 고려한 좋은 구현입니다.
aria-invalid
속성을 사용하여 접근성을 개선한 점이 훌륭합니다.다음과 같이 outline-offset을 추가하면 시각적으로 더 명확할 수 있습니다:
&[aria-invalid='true'] { outline: 0.1rem solid ${theme.colors.textAlert}; + outline-offset: 0.1rem; }
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (1)
71-71
: aria-invalid 속성을 단순화할 수 있습니다.불필요한 삼항 연산자를 제거하여 코드를 개선할 수 있습니다.
-aria-invalid={fieldState.invalid ? true : false} +aria-invalid={fieldState.invalid}src/app/login/google/layout.tsx (1)
3-5
: Suspense 컴포넌트에 fallback prop 추가 필요로딩 상태를 사용자에게 표시하기 위해 Suspense 컴포넌트에 fallback prop을 추가하는 것이 좋습니다. 또한 에러 처리를 위해 ErrorBoundary 구현을 고려해보세요.
function GoogleLoginLayout({ children }: { children: React.ReactNode }) { - return <Suspense>{children}</Suspense>; + return ( + <Suspense fallback={<div>로딩 중...</div>}> + {children} + </Suspense> + ); }src/app/join/layout.tsx (2)
9-11
: Suspense 사용 패턴 일관성 유지 필요다른 레이아웃 컴포넌트들과 마찬가지로 로딩 상태 처리가 필요합니다.
<div css={joinLayout}> - <Suspense>{children}</Suspense> + <Suspense fallback={<div>회원가입 페이지 로딩 중...</div>}> + {children} + </Suspense> </div>
Line range hint
15-21
: CSS 매직 넘버 개선 필요
calc(100vh - 12.2rem)
에서 사용된 12.2rem은 매직 넘버로 보입니다. 이 값의 의미를 명확히 하고 상수로 분리하는 것이 좋습니다.+const HEADER_HEIGHT = '12.2rem'; + const joinLayout = (theme: Theme) => css` display: flex; background-color: ${theme.colors.field01}; width: 56rem; margin: 0 auto; - min-height: calc(100vh - 12.2rem); + min-height: calc(100vh - ${HEADER_HEIGHT}); `;src/components/Header/Header.tsx (1)
23-25
: 쿼리 최적화가 잘 되어있습니다!역할에 따라 쿼리를 조건부로 활성화하는 방식이 효율적입니다. 다만, 쿼리 실패 시의 에러 처리를 추가하면 좋을 것 같습니다.
const role = useSessionStorage('role'); - const participantQuery = useParticipantInfoQuery({ enabled: role === ROLE.participant }); - const researcherQuery = useResearcherInfoQuery({ enabled: role === ROLE.researcher }); + const participantQuery = useParticipantInfoQuery({ + enabled: role === ROLE.participant, + onError: (error) => { + console.error('참가자 정보 조회 실패:', error); + }, + }); + const researcherQuery = useResearcherInfoQuery({ + enabled: role === ROLE.researcher, + onError: (error) => { + console.error('연구자 정보 조회 실패:', error); + }, + });src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (3)
26-37
: 성능 최적화를 위한 제안사항이 있습니다.
useWatch
를 통한 폼 상태 관리가 잘 구현되어 있습니다. 다만, 성능 최적화를 위해 다음과 같은 개선사항을 고려해보세요:+const memoizedValues = useMemo(() => values.every((value) => value !== '' && value !== undefined) +, [values]); -const isAllFilled = values.every((value) => value !== '' && value !== undefined);
Line range hint
42-152
: 검증 메시지 관리 개선을 제안합니다.폼 필드의 구현이 전반적으로 잘 되어있습니다. 하지만 유지보수성 향상을 위해 검증 메시지를 상수로 분리하는 것을 추천드립니다:
// constants/validationMessages.ts export const VALIDATION_MESSAGES = { required: { name: '이름을 입력해주세요', gender: '성별을 선택해주세요', birthDate: '생년월일을 입력해주세요', region: '지역을 선택해주세요', } };
168-170
: 제출 버튼에 로딩 상태 처리를 추가하면 좋겠습니다.폼 제출 시 사용자 경험 개선을 위해 로딩 상태를 추가하는 것을 제안드립니다:
-<button css={joinButton} onClick={handleSubmit} disabled={!isAllFilled}> +<button + css={[joinButton, isLoading && loadingStyles]} + onClick={handleSubmit} + disabled={!isAllFilled || isLoading} +> - 회원가입 + {isLoading ? '처리중...' : '회원가입'} </button>src/app/join/JoinPage.utils.ts (2)
11-13
: 이메일 유효성 검사 로직이 추가되었습니다.기본적인 이메일 형식 검증이 추가된 것은 좋은 개선사항입니다. 하지만 Zod를 활용하여 더 강력한 이메일 검증을 구현할 수 있습니다.
다음과 같이 Zod 스키마를 활용하여 이메일 검증을 개선해보세요:
import { z } from 'zod'; const emailSchema = z.string().email('잘못된 이메일 형식입니다.'); export const getProvider = (email: string): 'GOOGLE' | 'NAVER' => { const GOOGLE_DOMAINS = 'gmail.com'; const NAVER_DOMAINS = 'naver.com'; // Zod를 사용한 이메일 유효성 검사 emailSchema.parse(email); const targetDomain = email.split('@')[1].toLowerCase(); // ... 나머지 로직 };
11-14
: 공백 라인 사용이 일관적이지 않습니다.유효성 검사 블록 이후의 공백 라인은 불필요해 보입니다. 코드의 일관성을 위해 제거하는 것이 좋겠습니다.
if (!email || !email.includes('@')) { throw new Error('잘못된 이메일 형식입니다.'); } - const targetDomain = email.split('@')[1].toLowerCase();
src/app/join/hooks/useServiceAgreeCheck.ts (1)
36-40
: 역할 체크 로직을 개선할 수 있습니다.useEffect 내의 조건문을 더 명확하게 표현할 수 있습니다.
다음과 같이 수정하는 것을 추천드립니다:
useEffect(() => { - if (role && role === ROLE.participant) { + if (!role) return; + if (role === ROLE.participant) { setServiceAgreeCheck((prev) => ({ ...prev, isRecommend: false })); } }, [role]);src/app/home/components/PostContainer/PostContainer.tsx (1)
Line range hint
24-31
: TODO 및 FIXME 주석에 대한 해결 방안이 필요합니다.현재 구현에서 다음과 같은 이슈들이 남아있습니다:
- 참여자일 경우 쿼리가 2번 호출되는 문제
- 필터 상태 관리가 분산되어 있는 문제
다음과 같은 해결 방안을 제안드립니다:
- 쿼리 중복 호출 문제:
const useUserQueries = () => { const role = useSessionStorage('role'); const isParticipant = role === ROLE.participant; return { participantQuery: useParticipantInfoQuery({ enabled: isParticipant, staleTime: 5 * 60 * 1000 // 5분 동안 캐시 유지 }), researcherQuery: useResearcherInfoQuery({ enabled: !isParticipant && role === ROLE.researcher }) }; };
- 필터 상태 관리:
새로운 이슈를 생성하여 필터 상태 관리 로직을 개선하는 작업을 진행하시겠습니까?src/app/join/page.tsx (1)
87-95
: useEffect 의존성 배열 최적화가 필요합니다.useEffect 내에서 사용되는 함수를 의존성 배열에서 제외할 수 있습니다.
다음과 같이 수정하는 것을 추천드립니다:
useEffect(() => { if (oauthEmail) { - researcherMethods.setValue('oauthEmail', oauthEmail); - researcherMethods.setValue('provider', getProvider(oauthEmail)); + const provider = getProvider(oauthEmail); + researcherMethods.setValue('oauthEmail', oauthEmail); + researcherMethods.setValue('provider', provider); - participantMethods.setValue('oauthEmail', oauthEmail); - participantMethods.setValue('provider', getProvider(oauthEmail)); + participantMethods.setValue('oauthEmail', oauthEmail); + participantMethods.setValue('provider', provider); } - }, [oauthEmail, researcherMethods, participantMethods]); + }, [oauthEmail]);
🛑 Comments failed to post (3)
src/app/login/naver/layout.tsx (1)
1-7: 🛠️ Refactor suggestion
로그인 레이아웃 컴포넌트 중복 제거 필요
Google과 Naver 로그인 레이아웃이 동일한 구조를 가지고 있습니다. 코드 재사용성을 높이기 위해 공통 컴포넌트로 리팩토링하는 것을 추천드립니다.
다음과 같이 공통 컴포넌트를 만들어 사용할 수 있습니다:
// src/components/common/AuthLayout.tsx import { Suspense } from 'react'; interface AuthLayoutProps { children: React.ReactNode; provider: 'google' | 'naver'; } export function AuthLayout({ children, provider }: AuthLayoutProps) { return ( <Suspense fallback={<div>{provider} 로그인 진행 중...</div>}> {children} </Suspense> ); }src/components/Header/Header.tsx (1)
27-27: 🛠️ Refactor suggestion
데이터 할당 로직 개선이 필요합니다.
현재 구현은 쿼리 결과가 undefined일 경우 처리가 누락되어 있습니다. 안전한 데이터 처리를 위해 수정이 필요합니다.
- const myData = role === ROLE.participant ? participantQuery.data : researcherQuery.data; + const myData = role === ROLE.participant + ? participantQuery.data ?? null + : role === ROLE.researcher + ? researcherQuery.data ?? null + : null;📝 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 myData = role === ROLE.participant ? participantQuery.data ?? null : role === ROLE.researcher ? researcherQuery.data ?? null : null;
src/hooks/useSessionStorage.ts (1)
3-14: 🛠️ Refactor suggestion
세션 스토리지 훅의 기능 개선이 필요합니다.
다음과 같은 개선사항을 제안드립니다:
- 값을 설정하는 기능 추가
- 타입 안전성 강화
- 에러 처리 추가
다음과 같이 수정하는 것을 추천드립니다:
-const useSessionStorage = (key: string, defaultValue: string = '') => { +const useSessionStorage = <T>(key: string, defaultValue: T) => { const [storedValue, setStoredValue] = useState(defaultValue); useEffect(() => { if (typeof window !== 'undefined') { - const value = sessionStorage.getItem(key) || defaultValue; - setStoredValue(value); + try { + const item = sessionStorage.getItem(key); + const value = item ? JSON.parse(item) : defaultValue; + setStoredValue(value); + } catch (error) { + console.error(`Error reading from session storage: ${error}`); + setStoredValue(defaultValue); + } } }, [key, defaultValue]); - return storedValue; + const setValue = (value: T) => { + try { + setStoredValue(value); + if (typeof window !== 'undefined') { + sessionStorage.setItem(key, JSON.stringify(value)); + } + } catch (error) { + console.error(`Error writing to session storage: ${error}`); + } + }; + + return [storedValue, setValue] as const; };📝 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 useSessionStorage = <T>(key: string, defaultValue: T) => { const [storedValue, setStoredValue] = useState(defaultValue); useEffect(() => { if (typeof window !== 'undefined') { try { const item = sessionStorage.getItem(key); const value = item ? JSON.parse(item) : defaultValue; setStoredValue(value); } catch (error) { console.error(`Error reading from session storage: ${error}`); setStoredValue(defaultValue); } } }, [key, defaultValue]); const setValue = (value: T) => { try { setStoredValue(value); if (typeof window !== 'undefined') { sessionStorage.setItem(key, JSON.stringify(value)); } } catch (error) { console.error(`Error writing to session storage: ${error}`); } }; return [storedValue, setValue] as const; };
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: 0
🧹 Nitpick comments (2)
src/app/join/components/JoinInput/JoinInput.tsx (2)
68-68
: 불필요한 삼항 연산자 단순화 필요
aria-invalid
속성의 불필요한 삼항 연산자를 제거할 수 있습니다.다음과 같이 수정하는 것을 제안합니다:
- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}Also applies to: 76-76
🧰 Tools
🪛 Biome (1.9.4)
[error] 68-68: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
78-78
: maxLength 기본값 처리 개선 필요현재
maxLength ?? 0
는maxLength
가undefined
일 때 0을 사용하게 되는데, 이는 텍스트 입력을 완전히 막을 수 있습니다.적절한 기본값을 설정하는 것을 추천합니다:
- maxLength={maxLength ?? 0} + maxLength={maxLength ?? 100}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/app/join/components/JoinInput/JoinInput.styles.ts
(1 hunks)src/app/join/components/JoinInput/JoinInput.tsx
(2 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts
(3 hunks)src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx
(1 hunks)src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.styles.ts
- src/app/join/components/Participant/JoinInfoStep/JoinSelect/JoinSelect.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/join/components/JoinInput/JoinInput.tsx
[error] 68-68: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
[error] 76-76: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
🔇 Additional comments (4)
src/app/join/components/JoinInput/JoinInput.styles.ts (1)
46-55
: 스타일 구현이 적절합니다!입력 필드의 리셋 버튼 위치 지정이 잘 되어있습니다. 상대적/절대적 위치 지정을 통해 버튼이 입력 필드 내에서 올바르게 정렬됩니다.
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (2)
17-17
: 입력값 유효성 검사 로직 개선 필요현재
isAllFilled
로직은 빈 문자열과 undefined만 체크하고 있어, 공백 문자만 입력된 경우에도 유효한 것으로 처리될 수 있습니다.
19-56
: 폼 구조와 유효성 검사 구현이 잘 되어있습니다.
useFormContext
와useWatch
를 활용한 폼 상태 관리가 적절합니다.- 필수 필드에 대한 required 속성이 올바르게 지정되어 있습니다.
- 버튼의 활성화 상태가 입력값 검증과 연동되어 있습니다.
src/app/join/components/JoinInput/JoinInput.tsx (1)
82-92
: 리셋 버튼 구현이 잘 되어있습니다.
- 조건부 렌더링이 적절하게 구현되어 있습니다.
- 접근성을 위한 아이콘 크기가 적절합니다.
- 비활성화된 입력 필드에서는 리셋 버튼이 표시되지 않도록 처리되어 있습니다.
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: 7
♻️ Duplicate comments (1)
src/app/join/page.tsx (1)
117-119
: 🛠️ Refactor suggestion폼 제출 로직의 일관성 개선이 필요합니다.
연구자와 참여자의 폼 제출 구현 방식이 서로 다릅니다:
- 연구자:
handleSubmit={() => researcherMethods.handleSubmit(handleResearcherSubmit)}
- 참여자:
handleSubmit={() => participantMethods.handleSubmit(handleParticipantSubmit)}
일관성을 위해 다음과 같이 통일하는 것을 추천드립니다:
<Researcher.InfoStep - handleSubmit={() => researcherMethods.handleSubmit(handleResearcherSubmit)} + handleSubmit={researcherMethods.handleSubmit(handleResearcherSubmit)} />Also applies to: 147-149
🧹 Nitpick comments (8)
src/app/home/hooks/useUserInfo.ts (3)
1-5
: 타입 안전성을 위한 타입 정의 추가 제안타입스크립트의 장점을 최대한 활용하기 위해 다음 사항들을 고려해주세요:
useParticipantInfoQuery
와useResearcherInfoQuery
의 반환 타입 정의ROLE
상수의 타입 정의+import type { ParticipantInfo } from '@/types/participant'; +import type { ResearcherInfo } from '@/types/researcher'; import useParticipantInfoQuery from './useParticipantInfoQuery'; import useResearcherInfoQuery from './useResearcherInfoQuery'; -import { ROLE } from '@/constants/config'; +import type { Role } from '@/types/role'; +import { ROLE } from '@/constants/config'; import useSessionStorage from '@/hooks/useSessionStorage';
12-13
: 쿼리 최적화 제안현재 구현에서는 사용하지 않는 쿼리도 인스턴스화됩니다. 조건부 렌더링을 통해 필요한 쿼리만 생성하는 것이 좋습니다:
- const participantQuery = useParticipantInfoQuery({ enabled: isParticipant }); - const researcherQuery = useResearcherInfoQuery({ enabled: isResearcher }); + const participantQuery = isParticipant + ? useParticipantInfoQuery({ enabled: true }) + : { data: null, isLoading: false, isError: false }; + const researcherQuery = isResearcher + ? useResearcherInfoQuery({ enabled: true }) + : { data: null, isLoading: false, isError: false };
15-19
: 반환 타입 정의 추가 제안반환 값의 타입 안전성을 보장하기 위해 명시적인 타입 정의가 필요합니다:
+type UseUserInfoReturn = { + userInfo: ParticipantInfo | ResearcherInfo | null; + isLoading: boolean; + isError: boolean; +}; + -const useUserInfo = () => { +const useUserInfo = (): UseUserInfoReturn => { // ... existing code ...src/app/join/page.tsx (1)
44-64
: 폼 검증 모드 최적화가 필요합니다.현재 구성:
mode: 'onBlur'
reValidateMode: 'onChange'
사용자 경험 향상을 위해 다음과 같은 변경을 제안드립니다:
const researcherMethods = useForm<ResearcherJoinSchemaType>({ resolver: zodResolver(ResearcherJoinSchema()), - mode: 'onBlur', - reValidateMode: 'onChange', + mode: 'onChange', + reValidateMode: 'onBlur', ... });이유:
onChange
모드는 사용자가 입력하는 즉시 피드백을 제공합니다.onBlur
reValidateMode는 포커스가 벗어날 때 재검증하여 불필요한 검증을 줄입니다.src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (2)
18-50
: 접근성 개선을 위한 제안사항이 있습니다.폼 구조는 잘 구성되어 있지만, 다음과 같은 접근성 개선사항을 고려해보시면 좋을 것 같습니다:
- 각 입력 필드에 대한 aria-label 추가
- 에러 메시지를 위한 aria-describedby 속성 추가
- 필수 필드에 대한 aria-required 속성 추가
JoinInput 컴포넌트에 다음과 같은 속성들을 추가하는 것을 제안드립니다:
<JoinInput name="name" control={control} label="이름" required placeholder="이름(실명) 입력" + aria-label="이름 입력 필드" + aria-required="true" + aria-describedby="name-error" />
51-54
: 사용자 피드백 개선이 필요합니다.현재 버튼은 단순히 비활성화되는 것 외에는 사용자에게 충분한 피드백을 제공하지 않습니다.
다음과 같은 개선사항을 제안드립니다:
- 버튼에 툴팁 추가하여 비활성화 이유 표시
- 로딩 상태 추가
- 에러 발생 시 피드백 제공
- <button css={joinButton} onClick={handleSubmit} disabled={!isAllFilled}> - 회원가입 - </button> + <button + css={joinButton} + onClick={handleSubmit} + disabled={!isAllFilled} + title={!isAllFilled ? "모든 필수 항목을 입력해주세요" : ""} + aria-label="회원가입 버튼" + > + {isLoading ? "처리중..." : "회원가입"} + </button>src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (2)
Line range hint
1-25
: import문 구성 개선이 필요합니다.현재 import문들이 잘 구성되어 있지만, 더 나은 가독성을 위해 그룹화하면 좋을 것 같습니다.
다음과 같은 구조로 import문을 정리하는 것을 제안드립니다:
// External dependencies import { Controller, useFormContext, useWatch } from 'react-hook-form'; // Components import AreaTooltip from './AreaTooltip'; import JoinSelect from './JoinSelect/JoinSelect'; import RadioButtonGroupContainer from './RadioButtonGroupContainer/RadioButtonGroupContainer'; import JoinInput from '../../JoinInput/JoinInput'; // Styles import { filterTitle, filterTitleWrapper, joinAreaFilterContainer, joinAreaFilterWrapper, joinButton, joinContentContainer, requiredStar, } from './JoinInfoStep.styles'; import { joinForm } from '@/app/join/JoinPage.styles'; // Constants & Types import { JOIN_REGION, JOIN_SUB_REGION } from '@/app/join/JoinPage.constants'; import { Gender, MatchType } from '@/app/join/JoinPage.types'; import { ParticipantJoinSchemaType } from '@/schema/join/ParticipantJoinSchema';
154-167
: 실험 진행 방식 선택 구현 개선이 필요합니다.현재 구현의 개선 포인트들입니다:
- 타입 안전성이 부족합니다.
- 기본값 처리가 명시적이지 않습니다.
- 선택 변경 시 사용자 피드백이 부족합니다.
다음과 같은 개선사항을 제안드립니다:
const MATCH_TYPE_OPTIONS = [ { value: MatchType.ALL, label: '전체' }, { value: MatchType.OFFLINE, label: '대면' }, { value: MatchType.ONLINE, label: '비대면' }, ] as const; const DEFAULT_MATCH_TYPE = MatchType.ALL; <RadioButtonGroupContainer<MatchType> control={control} name="matchType" title="선호 실험 진행 방식" options={MATCH_TYPE_OPTIONS} defaultValue={DEFAULT_MATCH_TYPE} onChange={(value) => { setValue('matchType', value); // 선택 변경 시 피드백 제공 toast.success(`실험 진행 방식이 ${MATCH_TYPE_OPTIONS.find(opt => opt.value === value)?.label}로 설정되었습니다.`); }} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/app/home/components/PostContainer/PostContainer.tsx
(3 hunks)src/app/home/hooks/useUserInfo.ts
(1 hunks)src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
(3 hunks)src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
(1 hunks)src/app/join/page.tsx
(6 hunks)src/components/Header/Header.tsx
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/components/Header/Header.tsx
- src/app/home/components/PostContainer/PostContainer.tsx
🔇 Additional comments (2)
src/app/join/page.tsx (1)
3-3
: Zod 스키마 통합이 잘 구현되었습니다.필요한 의존성들이 잘 구성되어 있으며, 타입 안전성을 보장하기 위한 스키마 통합이 적절히 이루어졌습니다.
Also applies to: 26-30
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (1)
1-11
: 코드 구조가 깔끔하고 명확합니다!필요한 의존성들이 잘 정의되어 있으며, Props 인터페이스가 간단명료합니다.
const useUserInfo = () => { | ||
const role = useSessionStorage('role'); | ||
const isParticipant = role === ROLE.participant; | ||
const isResearcher = role === ROLE.researcher; |
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
역할 검증 로직 강화 제안
현재 구현은 잘못된 역할 값이 저장된 경우를 처리하지 않습니다. 다음과 같은 개선을 제안드립니다:
const useUserInfo = () => {
- const role = useSessionStorage('role');
+ const role = useSessionStorage<Role>('role');
+
+ if (!role || !Object.values(ROLE).includes(role)) {
+ throw new Error('Invalid or missing role');
+ }
+
const isParticipant = role === ROLE.participant;
const isResearcher = role === ROLE.researcher;
📝 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 useUserInfo = () => { | |
const role = useSessionStorage('role'); | |
const isParticipant = role === ROLE.participant; | |
const isResearcher = role === ROLE.researcher; | |
const useUserInfo = () => { | |
const role = useSessionStorage<Role>('role'); | |
if (!role || !Object.values(ROLE).includes(role)) { | |
throw new Error('Invalid or missing role'); | |
} | |
const isParticipant = role === ROLE.participant; | |
const isResearcher = role === ROLE.researcher; |
// TODO: 추후 스켈레톤 처리 | ||
if (!role) return null; |
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
로딩 상태 처리가 필요합니다.
현재 role이 없을 때 null을 반환하는 것은 사용자 경험을 저하시킬 수 있습니다. 스켈레톤 UI 구현 전까지 임시 로딩 상태를 추가하는 것을 제안드립니다.
- // TODO: 추후 스켈레톤 처리
- if (!role) return null;
+ if (!role) {
+ return (
+ <section css={joinLayout}>
+ <div>로딩 중...</div>
+ </section>
+ );
+ }
📝 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.
// TODO: 추후 스켈레톤 처리 | |
if (!role) return null; | |
if (!role) { | |
return ( | |
<section css={joinLayout}> | |
<div>로딩 중...</div> | |
</section> | |
); | |
} |
src/app/join/page.tsx
Outdated
useEffect(() => { | ||
if (oauthEmail) { | ||
researcherMethods.setValue('oauthEmail', oauthEmail); | ||
researcherMethods.setValue('provider', getProvider(oauthEmail)); | ||
|
||
participantMethods.setValue('oauthEmail', oauthEmail); | ||
participantMethods.setValue('provider', getProvider(oauthEmail)); | ||
} | ||
}, [oauthEmail, researcherMethods, participantMethods]); |
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
세션 스토리지 값 설정에 대한 에러 처리가 필요합니다.
현재 구현에서는 oauthEmail
이 존재하지 않는 경우에 대한 처리가 누락되어 있습니다.
useEffect(() => {
if (oauthEmail) {
+ try {
researcherMethods.setValue('oauthEmail', oauthEmail);
researcherMethods.setValue('provider', getProvider(oauthEmail));
participantMethods.setValue('oauthEmail', oauthEmail);
participantMethods.setValue('provider', getProvider(oauthEmail));
+ } catch (error) {
+ console.error('이메일 설정 중 오류 발생:', error);
+ // TODO: 에러 처리 로직 추가
+ }
}
}, [oauthEmail, researcherMethods, participantMethods]);
📝 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.
useEffect(() => { | |
if (oauthEmail) { | |
researcherMethods.setValue('oauthEmail', oauthEmail); | |
researcherMethods.setValue('provider', getProvider(oauthEmail)); | |
participantMethods.setValue('oauthEmail', oauthEmail); | |
participantMethods.setValue('provider', getProvider(oauthEmail)); | |
} | |
}, [oauthEmail, researcherMethods, participantMethods]); | |
useEffect(() => { | |
if (oauthEmail) { | |
try { | |
researcherMethods.setValue('oauthEmail', oauthEmail); | |
researcherMethods.setValue('provider', getProvider(oauthEmail)); | |
participantMethods.setValue('oauthEmail', oauthEmail); | |
participantMethods.setValue('provider', getProvider(oauthEmail)); | |
} catch (error) { | |
console.error('이메일 설정 중 오류 발생:', error); | |
// TODO: 에러 처리 로직 추가 | |
} | |
} | |
}, [oauthEmail, researcherMethods, participantMethods]); |
const JoinInfoStep = ({ handleSubmit }: JoinInfoStepProps) => { | ||
const { control } = useFormContext<ResearcherJoinSchemaType>(); | ||
const values = useWatch({ name: ['name', 'univName', 'major'], control }); | ||
|
||
const isAllFilled = values.every((value) => value.trim() !== '' && value !== undefined); |
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.
유효성 검사 로직 개선이 필요합니다.
현재 구현에는 다음과 같은 잠재적인 문제가 있습니다:
value
가undefined
일 때trim()
메서드 호출 시 오류가 발생할 수 있습니다.- 수동 검사 대신 zod 스키마를 활용한 유효성 검사가 더 적절할 것 같습니다.
다음과 같이 수정하는 것을 제안드립니다:
- const isAllFilled = values.every((value) => value.trim() !== '' && value !== undefined);
+ const isAllFilled = values.every((value) => value !== undefined && value !== null && value.trim() !== '');
또한, form의 전체적인 유효성 상태를 확인하기 위해 formState
를 활용하는 것을 고려해보세요:
const { formState: { isValid } } = useFormContext<ResearcherJoinSchemaType>();
const JoinInfoStep = ({ handleSubmit }: JoinInfoStepProps) => { | ||
const { control, setValue } = useFormContext<ParticipantJoinSchemaType>(); | ||
|
||
const gender = useWatch({ name: 'gender', control }); | ||
const matchType = useWatch({ name: 'matchType', control }); | ||
const selectedArea = useWatch({ name: 'basicAddressInfo.region', control }); | ||
const selectedAdditionalArea = useWatch({ name: 'additionalAddressInfo.region', control }); | ||
|
||
const selectedArea = watch('basicAddressInfo.region'); | ||
const selectedSubArea = watch('basicAddressInfo.area'); | ||
const selectedAdditionalArea = watch('additionalAddressInfo.region'); | ||
const selectedAdditionalSubArea = watch('additionalAddressInfo.area'); | ||
const values = useWatch({ | ||
name: ['name', 'gender', 'birthDate', 'basicAddressInfo.area', 'basicAddressInfo.region'], | ||
control, | ||
}); | ||
|
||
const isAllFilled = values.every((value) => value.trim() !== '' && value !== undefined); |
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
폼 상태 관리 개선이 필요합니다.
현재 구현에서 발견된 개선 포인트들입니다:
- 연구자 컴포넌트와 동일한 유효성 검사 이슈가 있습니다.
- 폼 상태 변경 시 불필요한 리렌더링이 발생할 수 있습니다.
다음과 같은 개선사항을 제안드립니다:
const JoinInfoStep = ({ handleSubmit }: JoinInfoStepProps) => {
const { control, setValue, formState: { errors, isValid } } = useFormContext<ParticipantJoinSchemaType>();
// 필수 필드들의 값 변경 감지
const values = useWatch({
name: ['name', 'gender', 'birthDate', 'basicAddressInfo.area', 'basicAddressInfo.region'],
control,
});
// 지역 선택 값 변경 감지 (별도로 관리하여 불필요한 리렌더링 방지)
const [selectedArea, selectedAdditionalArea] = useWatch({
name: ['basicAddressInfo.region', 'additionalAddressInfo.region'],
control,
});
const isAllFilled = values.every((value) =>
value !== undefined &&
value !== null &&
(typeof value === 'string' ? value.trim() !== '' : true)
);
// formState의 isValid와 isAllFilled를 함께 확인
const canSubmit = isValid && isAllFilled;
{/* 거주 지역 */} | ||
<div css={joinAreaFilterContainer}> | ||
<div css={filterTitleWrapper}> | ||
<span css={filterTitle}>거주 지역</span> | ||
<span css={requiredStar}>*</span> | ||
</div> | ||
<div css={joinAreaFilterWrapper}> | ||
<JoinSelect | ||
value={selectedArea} | ||
onChange={(value) => setValue('basicAddressInfo.region', value)} | ||
placeholder="시·도" | ||
options={JOIN_REGION} | ||
<Controller | ||
name="basicAddressInfo.region" | ||
control={control} | ||
render={({ field, fieldState }) => ( | ||
<JoinSelect | ||
value={field.value} | ||
onChange={(value) => setValue('basicAddressInfo.region', value)} | ||
placeholder="시·도" | ||
options={JOIN_REGION} | ||
isError={Boolean(fieldState.error) && Boolean(!field.value)} | ||
/> | ||
)} | ||
/> | ||
<JoinSelect | ||
value={selectedSubArea} | ||
onChange={(value) => setValue('basicAddressInfo.area', value)} | ||
placeholder="시·군·구" | ||
options={JOIN_SUB_REGION[selectedArea] || []} | ||
|
||
<Controller | ||
name="basicAddressInfo.area" | ||
control={control} | ||
render={({ field, fieldState }) => ( | ||
<JoinSelect | ||
value={field.value} | ||
onChange={(value) => setValue('basicAddressInfo.area', value)} | ||
placeholder="시·군·구" | ||
options={JOIN_SUB_REGION[selectedArea] || []} | ||
isError={Boolean(fieldState.error) && Boolean(!field.value)} | ||
/> | ||
)} | ||
/> | ||
</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
주소 선택 구현 개선이 필요합니다.
현재 구현의 개선 포인트들입니다:
- 에러 메시지가 사용자 친화적이지 않습니다.
- 지역 선택 시 연관된 구/군 선택이 초기화되지 않습니다.
- 선택된 옵션에 대한 시각적 피드백이 부족합니다.
다음과 같은 개선사항을 제안드립니다:
const handleRegionChange = (value: string) => {
setValue('basicAddressInfo.region', value);
// 시/도가 변경되면 구/군 초기화
setValue('basicAddressInfo.area', '');
};
return (
<Controller
name="basicAddressInfo.region"
control={control}
render={({ field, fieldState }) => (
<JoinSelect
value={field.value}
onChange={handleRegionChange}
placeholder="시·도"
options={JOIN_REGION}
isError={Boolean(fieldState.error)}
errorMessage={fieldState.error?.message || '시/도를 선택해주세요'}
aria-label="시도 선택"
/>
)}
/>
);
{/* 추가 활동 지역 */} | ||
<div css={joinAreaFilterContainer}> | ||
<div css={filterTitleWrapper}> | ||
<span css={filterTitle}>추가 활동 지역</span> | ||
<AreaTooltip /> | ||
</div> | ||
<div css={joinAreaFilterWrapper}> | ||
<JoinSelect | ||
value={selectedAdditionalArea} | ||
onChange={(value) => setValue('additionalAddressInfo.region', value)} | ||
placeholder="시·도" | ||
options={JOIN_REGION} | ||
<Controller | ||
name="additionalAddressInfo.region" | ||
control={control} | ||
render={({ field, fieldState }) => { | ||
return ( | ||
<JoinSelect | ||
value={field.value} | ||
onChange={(value) => setValue('additionalAddressInfo.region', value)} | ||
placeholder="시·도" | ||
options={JOIN_REGION} | ||
isError={Boolean(fieldState.error)} | ||
/> | ||
); | ||
}} | ||
/> | ||
<JoinSelect | ||
value={selectedAdditionalSubArea} | ||
onChange={(value) => setValue('additionalAddressInfo.area', value)} | ||
placeholder="시·군·구" | ||
options={JOIN_SUB_REGION[selectedAdditionalArea] || []} | ||
|
||
<Controller | ||
name="additionalAddressInfo.area" | ||
control={control} | ||
render={({ field, fieldState }) => ( | ||
<JoinSelect | ||
value={field.value} | ||
onChange={(value) => setValue('additionalAddressInfo.area', value)} | ||
placeholder="시·군·구" | ||
options={JOIN_SUB_REGION[selectedAdditionalArea || ''] || []} | ||
isError={Boolean(fieldState.error)} | ||
/> | ||
)} | ||
/> | ||
</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
코드 중복 제거가 필요합니다.
기본 주소 선택과 추가 주소 선택 구현이 매우 유사하여 중복 코드가 발생하고 있습니다.
다음과 같이 공통 컴포넌트로 분리하는 것을 제안드립니다:
interface AddressSelectProps {
namePrefix: 'basicAddressInfo' | 'additionalAddressInfo';
required?: boolean;
title: string;
tooltip?: React.ReactNode;
}
const AddressSelect = ({ namePrefix, required, title, tooltip }: AddressSelectProps) => {
const { control, setValue } = useFormContext<ParticipantJoinSchemaType>();
const selectedRegion = useWatch({ name: `${namePrefix}.region`, control });
return (
<div css={joinAreaFilterContainer}>
<div css={filterTitleWrapper}>
<span css={filterTitle}>{title}</span>
{required && <span css={requiredStar}>*</span>}
{tooltip}
</div>
{/* 기존 Controller 컴포넌트들... */}
</div>
);
};
// 사용 예시:
<AddressSelect
namePrefix="basicAddressInfo"
required
title="거주 지역"
/>
<AddressSelect
namePrefix="additionalAddressInfo"
title="추가 활동 지역"
tooltip={<AreaTooltip />}
/>
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.
React-Hook-Form, Zod 모두 처음 사용해보시느라 더 어려우셨을텐데 고생 많으셨습니다ㅠㅠ
더군다나 회원가입이 유형이 두가지이고 퍼널 형태라 까다로우셨을 것 같아요
그래도 빠르게 따로 드린 리뷰 반영해주시고 유효성 검증도 잘 동작하고 있어서 대단합니다 👍 👍
우선 테스트해보면서 눈에 보이는 부분들 위주로 리뷰 남겼고,
모든 구조를 함께 살펴본 건 아니라 제가 확인 못한 에러나 문제가 발생할 수 있으니 참고만 해주세요!
혹시 제 예상과 다르게 동작하는 부분이 있을 때 공유 해주시면 같이 확인해보겠습니다.
해당 리뷰들만 반영이 된다면 머지하셔도 괜찮을 것 같아요!
다시 한번 고생 많으셨습니다 🔥
icon="CloseRound" | ||
width={22} | ||
height={22} | ||
onClick={() => field.onChange('')} |
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.
input에서 직접 글자를 지우면 에러가 발생하는데 resetButton을 클릭해서 input value를 제거하면 에러가 발생하지 않더라고요!
field.onChange('')
와 함께 field.onBlur()
로 블러 이벤트를 다시 트리거 시키거나
setValue(name, '', { shouldValidate: true })
를 사용하면 입력값이 변경됨과 동시에 유효성 검증을 실행할 것 같습니다 👀
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.
꼼꼼한 리뷰 감사합니다!
해당 부분을 반영하기 위해 좀더 살펴보니, 리셋버튼을 눌렀을 때 인풋창에 포커스가 유지되는 게 자연스럽다고 느껴서 ref를 연결하여 focus를 유지
시켰습니다. 포커스를 유지했다가 blur 처리되었을 때 동일하게 동작하기 때문에, 버튼 이벤트 핸들러에 field.onBlur
나 setValue(name, '', { shouldValidate: true })
를 추가하지 않고 기존 로직을 유지하였습니다!
추가로 인풋창에서 포커싱을 없애도 리셋버튼이 남아있는 게 어색해서 isFocused 상태로 분기처리하였습니다.
onChange={(value) => setValue('matchType', value)} | ||
/> | ||
</div> | ||
|
||
<button css={joinButton} onClick={onNext}> | ||
<button css={joinButton} onClick={handleSubmit} disabled={!isAllFilled}> |
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.
isAllFilled뿐만 아니라 errors를 확인하여 에러 객체가 비어있는지도 버튼 활성화 기준에 추가하는 건 어떨까요?
예를 들면 disabled={!(isAllFilled && Object.keys(errors).length === 0)}
이런 식으로요!
- 모든 필수 필드가 입력되었는 지 확인(
isAllFilled
) - 모든 필수 필드가 유효성 검사를 통과했는지 확인 (
errors
가 비어있는지)
isAllFilled로만 체크했을 땐 유효성 검증을 통과하지 않았음에도 값이 존재하면 회원가입 버튼이 활성화가 되더라고요. (생년월일 필드를 yyyy.mm.dd 형식으로 넣지 않아 에러가 발생했음에도 회원가입 버튼 활성화)
모든 필드의 유효성 검증 통과 여부(errors)도 함께 체크하는 게 필요할 거 같아요.
우선 이렇게만 해봤을 땐 말씀 주신 에러 상태가 변경될 때 포커스가 풀리는 현상
은 안보였는데,,,
한번 적용해보시고 같은 문제가 발생한다면 공유 주세요..!
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.
처음에는 onBlur로 동작하기 때문에 제가 말씀 드린 현상은 발생하지 않네요! 에러 상태가 onBlur 시점에 변경이 되며 그때는 이미 포커스가 풀린 상태여서 그런 것 같아요!
submit 한 이후에 유효성 검사가 onChange로 변하면 에러 상태가 변할 때마다 포커스가 풀리게 되는 건 똑같아요ㅜ(저만 그런가요) 그런데 에러 상태인 폼 데이터가 없으면 disabled 처리한다면 큰 문제는 없어보이긴하네용 (악의적인 사용자가 아니라면!)
포커싱이 풀리는 시연 영상입니다.
default.mov
실행 코드입니다!
// Participant/JoinInfoStep/JoinInfoStep.tsx
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import AreaTooltip from './AreaTooltip';
import {
filterTitle,
filterTitleWrapper,
joinAreaFilterContainer,
joinAreaFilterWrapper,
joinButton,
joinContentContainer,
requiredStar,
} from './JoinInfoStep.styles';
import JoinSelect from './JoinSelect/JoinSelect';
import RadioButtonGroupContainer from './RadioButtonGroupContainer/RadioButtonGroupContainer';
import JoinInput from '../../JoinInput/JoinInput';
import { JOIN_REGION, JOIN_SUB_REGION } from '@/app/join/JoinPage.constants';
import { joinForm } from '@/app/join/JoinPage.styles';
import { Gender, MatchType } from '@/app/join/JoinPage.types';
import { ParticipantJoinSchemaType } from '@/schema/join/ParticipantJoinSchema';
interface JoinInfoStepProps {
handleSubmit: () => void;
}
const JoinInfoStep = ({ handleSubmit }: JoinInfoStepProps) => {
const {
control,
setValue,
formState: { errors },
} = useFormContext<ParticipantJoinSchemaType>();
const selectedArea = useWatch({ name: 'basicAddressInfo.region', control });
const selectedAdditionalArea = useWatch({ name: 'additionalAddressInfo.region', control });
const values = useWatch({
name: ['name', 'birthDate', 'basicAddressInfo.area', 'basicAddressInfo.region'],
control,
});
const isAllFilled = values.every((value) => value !== '' && value !== undefined);
return (
<section css={joinForm}>
<div css={joinContentContainer}>
{/* 이름 */}
<JoinInput<ParticipantJoinSchemaType>
name="name"
control={control}
label="이름"
required
placeholder="이름(실명) 입력"
/>
{/* 성별 */}
<RadioButtonGroupContainer<Gender>
control={control}
title="성별"
name="gender"
options={[
{ label: '남성', value: 'MALE' },
{ label: '여성', value: 'FEMALE' },
{ label: '선택 안 함', value: 'ALL' },
]}
onChange={(value) => setValue('gender', value)}
required
tip="나중에 수정할 수 없어요"
/>
{/* 생년월일 */}
<JoinInput<ParticipantJoinSchemaType>
name="birthDate"
control={control}
label="생년월일"
required
placeholder="YYYY.MM.DD"
maxLength={10}
tip="나중에 수정할 수 없어요"
isTip={false}
/>
{/* 거주 지역 */}
<div css={joinAreaFilterContainer}>
<div css={filterTitleWrapper}>
<span css={filterTitle}>거주 지역</span>
<span css={requiredStar}>*</span>
</div>
<div css={joinAreaFilterWrapper}>
<Controller
name="basicAddressInfo.region"
control={control}
render={({ field, fieldState }) => (
<JoinSelect
value={field.value}
onChange={(value) => setValue('basicAddressInfo.region', value)}
placeholder="시·도"
options={JOIN_REGION}
isError={Boolean(fieldState.error) && Boolean(!field.value)}
/>
)}
/>
<Controller
name="basicAddressInfo.area"
control={control}
render={({ field, fieldState }) => (
<JoinSelect
value={field.value}
onChange={(value) => setValue('basicAddressInfo.area', value)}
placeholder="시·군·구"
options={JOIN_SUB_REGION[selectedArea] || []}
isError={Boolean(fieldState.error) && Boolean(!field.value)}
/>
)}
/>
</div>
</div>
{/* 추가 활동 지역 */}
<div css={joinAreaFilterContainer}>
<div css={filterTitleWrapper}>
<span css={filterTitle}>추가 활동 지역</span>
<AreaTooltip />
</div>
<div css={joinAreaFilterWrapper}>
<Controller
name="additionalAddressInfo.region"
control={control}
render={({ field, fieldState }) => {
return (
<JoinSelect
value={field.value}
onChange={(value) => setValue('additionalAddressInfo.region', value)}
placeholder="시·도"
options={JOIN_REGION}
isError={Boolean(fieldState.error)}
/>
);
}}
/>
<Controller
name="additionalAddressInfo.area"
control={control}
render={({ field, fieldState }) => (
<JoinSelect
value={field.value}
onChange={(value) => setValue('additionalAddressInfo.area', value)}
placeholder="시·군·구"
options={JOIN_SUB_REGION[selectedAdditionalArea || ''] || []}
isError={Boolean(fieldState.error)}
/>
)}
/>
</div>
</div>
{/* 선호 실험 진행 방식 */}
<RadioButtonGroupContainer<MatchType>
control={control}
name="matchType"
title="선호 실험 진행 방식"
options={[
{ value: 'ALL', label: '전체' },
{ value: 'OFFLINE', label: '대면' },
{ value: 'ONLINE', label: '비대면' },
]}
onChange={(value) => setValue('matchType', value)}
/>
</div>
<button
css={joinButton}
onClick={handleSubmit}
disabled={!(isAllFilled && Object.keys(errors).length === 0)}
>
회원가입
</button>
</section>
);
};
export default JoinInfoStep;
|
||
return ( | ||
<section css={joinForm}> | ||
<div css={joinContentContainer}> | ||
{/* 이름 */} |
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.
참여자 회원가입 - 이름 입력창에 이름을 입력하자마자 해당 에러가 발생하였습니다.
useWatch
에서 values를 가져올 때 기본적으로 undefined
를 반환하는데 관련 문제이지 않을까 싶습니다.
그래서 trim()
전 (value ?? '')
으로 방어를 해주거나 defaultValues
로 빈문자열을 지정해주면 될 것 같습니다!
연구자의 코멘트와 이어서 참여자 회원가입의 경우도 유효성 검증이 실패해도 값만 존재하면 회원가입 버튼이 활성화되고 있습니다.
예를 들면 disabled={!(isAllFilled && Object.keys(errors).length === 0)}
와 같은 방식으로 버튼 활성화 여부를 처리해주면 좋을 것 같습니다!!
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.
엇 마지막에 토끼 코멘트 추가하다가 오류가 발생했네요 ㅠ 수정하겠습니다!
requiredStar, | ||
textCount, | ||
tipAlert, | ||
tipWrapper, | ||
} from './JoinInput.styles'; | ||
|
||
import Icon from '@/components/Icon'; | ||
|
||
interface JoinInputProps { | ||
type?: 'input' | 'textarea'; | ||
name: 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.
지금 커밋은 아니지만 control?: any
관련해서
JoinInput 컴포넌트 타입을 제네릭으로 변경하면 any를 쓰지 않아도 될 것 같아서요!
RHF의 Control로 타입을 지정하려고 하니 두개의 스키마 타입(Researcher, Participant)을 가질 수 있기 때문에 타입 충돌이 발생하는 거 같아서 다양한 폼에서 사용할 수 있도록 제네릭을 추가해봤습니다.
RHF에서 제공하는 타입들을 가지고 control
, name
타입을 유연하게 변경해보았는데
임시로 해본 거라 해당 컴포넌트를 사용하는 곳에서 다른 문제가 발생할 수 있으니 참고만 해주세요!
// JoinInput.tsx
import { Control, Controller, FieldValues, Path, PathValue } from 'react-hook-form';
import {
errorMessage,
inputContainer,
inputResetButton,
inputWrapper,
requiredStar,
textCount,
tipAlert,
tipWrapper,
} from './JoinInput.styles';
import Icon from '@/components/Icon';
interface JoinInputProps<T extends FieldValues> {
type?: 'input' | 'textarea';
name: Path<T>;
control: Control<T>;
rules?: object;
placeholder?: string;
label?: string;
required?: boolean;
disabled?: boolean;
tip?: string;
value?: PathValue<T, Path<T>>;
maxLength?: number;
isTip?: boolean;
}
const JoinInput = <T extends FieldValues>({
type = 'input',
name,
control,
rules = {},
placeholder = '',
label,
required = false,
disabled = false,
tip,
value,
maxLength,
isTip = true,
}: JoinInputProps<T>) => {
return (
<div css={inputContainer}>
{label && (
<label>
<span>{label}</span>
{required && <span css={requiredStar}>*</span>}
</label>
)}
<Controller
name={name}
control={control}
rules={rules}
defaultValue={value}
render={({ field, fieldState }) => (
<>
<div css={inputWrapper}>
{type === 'input' ? (
<input
{...field}
placeholder={placeholder}
disabled={disabled}
maxLength={maxLength}
aria-invalid={fieldState.invalid ? true : false}
style={{ width: '100%' }}
/>
) : (
<textarea
{...field}
placeholder={placeholder}
disabled={disabled}
aria-invalid={fieldState.invalid ? true : false}
rows={3}
maxLength={maxLength ?? 0}
style={{ width: '100%' }}
/>
)}
{field.value && !disabled && (
<button css={inputResetButton}>
<Icon
icon="CloseRound"
width={22}
height={22}
onClick={() => {
field.onChange('');
field.onBlur();
}}
cursor="pointer"
/>
</button>
)}
</div>
{fieldState.error && <span css={errorMessage}>{fieldState.error.message}</span>}
{type === 'textarea' && (
<span css={textCount}>
{field.value?.length || 0}/{maxLength}
</span>
)}
{tip && Boolean(!fieldState.error) && (
<div css={tipWrapper}>
{isTip && <span css={tipAlert}>Tip</span>}
<span>{tip}</span>
</div>
)}
</>
)}
/>
</div>
);
};
export default JoinInput;
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.
오 안그래도 any가 신경쓰였는데 제안 감사합니다!! Control을 제네릭을 바꿔도 오류가 나서 일단 유지했었는데 다른 값들도 타입이 정해져있군요 배워갑니다 🔥
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: 3
♻️ Duplicate comments (3)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
26-26
: 🛠️ Refactor suggestion이메일 인증 상태를 폼 컨텍스트로 이동해야 합니다.
이메일 인증 상태가 여전히 로컬 상태로 관리되고 있어, 페이지 새로고침 시 상태가 초기화되는 문제가 있습니다.
다음과 같이 수정하는 것을 제안합니다:
- const [isEmailVerified, setIsEmailVerified] = useState(false); + const { setValue, watch } = useFormContext<ResearcherJoinSchemaType>(); + const isEmailVerified = watch('isEmailVerified'); const handleVerifyEmail = () => { - setIsEmailVerified(true); + setValue('isEmailVerified', true); };
28-34
: 🛠️ Refactor suggestion유효성 검사 실패 시 사용자 피드백이 필요합니다.
현재 구현에서는 유효성 검사 실패 시 사용자에게 어떤 문제가 있는지 알려주지 않습니다.
다음과 같이 수정하는 것을 제안합니다:
const handleNextStep = async () => { - const isValid = await trigger(['oauthEmail', 'contactEmail', 'univEmail']); - - if (isValid) { - onNext(); - } + try { + const isValid = await trigger(['oauthEmail', 'contactEmail', 'univEmail']); + if (isValid) { + onNext(); + } else { + alert('모든 필수 항목을 올바르게 입력해주세요.'); + } + } catch (error) { + console.error('유효성 검사 중 오류 발생:', error); + alert('오류가 발생했습니다. 다시 시도해주세요.'); + } };src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (1)
89-114
: 🛠️ Refactor suggestion주소 선택 구현의 개선이 필요합니다.
이전 리뷰 코멘트에서 제기된 문제점들이 아직 해결되지 않았습니다:
- 시/도 선택 시 구/군 선택값이 초기화되지 않습니다.
- 에러 상태 처리가 복잡합니다.
<Controller name="basicAddressInfo.region" control={control} render={({ field, fieldState }) => ( <JoinSelect value={field.value} - onChange={(value) => setValue('basicAddressInfo.region', value)} + onChange={(value) => { + setValue('basicAddressInfo.region', value); + setValue('basicAddressInfo.area', ''); + }} placeholder="시·도" options={JOIN_REGION} - isError={Boolean(fieldState.error) && Boolean(!field.value)} + isError={Boolean(fieldState.error)} /> )} />
🧹 Nitpick comments (11)
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx (2)
1-4
: import 문 순서를 수정해 주세요.import 문을 다음과 같이 그룹화하고 순서를 조정하는 것이 좋습니다:
- 외부 패키지 (@radix-ui)
- 내부 컴포넌트 (@/components)
- 로컬 스타일
다음과 같이 수정해 주세요:
import * as Tooltip from '@radix-ui/react-tooltip'; import Icon from '@/components/Icon'; + import { tooltipArrow, tooltipContent } from './AreaTooltip.styles';
🧰 Tools
🪛 ESLint
[error] 3-3: There should be at least one empty line between import groups
(import/order)
[error] 4-4:
./AreaTooltip.styles
import should occur before import of@/components/Icon
(import/order)
11-13
: button 요소에 type 속성을 추가해 주세요.의도하지 않은 폼 제출을 방지하기 위해 button 요소에 type="button" 속성을 추가하는 것이 좋습니다.
- <button> + <button type="button"> <Icon icon="Information" width={18} height={18} /> </button>src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (1)
66-85
: 에러 처리가 개선되었으나, 불필요한 불리언 표현식이 있습니다.에러 상태와 메시지 처리가 잘 구현되어 있습니다. 다만, 다음과 같은 최적화가 가능합니다:
- aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid}또한, 에러 메시지 표시 로직이 중복되어 있습니다. 다음과 같이 통합하는 것을 고려해보세요:
- {fieldState.error && <span css={errorMessage}>{fieldState.error.message}</span>} - {sendError && <span css={errorMessage}>{sendError.message}</span>} + {(fieldState.error || sendError) && ( + <span css={errorMessage}> + {fieldState.error?.message || sendError?.message} + </span> + )}🧰 Tools
🪛 Biome (1.9.4)
[error] 72-72: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
68-68
: 이메일 인증 과정의 UX 개선이 필요합니다.이메일 인증 과정에서 사용자에게 진행 상태를 표시하지 않고 있습니다.
다음과 같이 로딩 상태를 추가하는 것을 제안합니다:
+ const [isVerifying, setIsVerifying] = useState(false); + + const handleVerifyEmail = async () => { + setIsVerifying(true); + try { + // 이메일 인증 로직 + setIsEmailVerified(true); + } finally { + setIsVerifying(false); + } + }; - <UnivAuthInput isEmailVerified={isEmailVerified} handleVerifyEmail={handleVerifyEmail} /> + <UnivAuthInput + isEmailVerified={isEmailVerified} + isVerifying={isVerifying} + handleVerifyEmail={handleVerifyEmail} + />
75-77
: 버튼의 접근성 개선이 필요합니다.현재 버튼은 비활성화 상태에 대한 설명이 없어 스크린 리더 사용자가 왜 버튼을 사용할 수 없는지 알 수 없습니다.
다음과 같이 수정하는 것을 제안합니다:
- <button css={nextButton} onClick={handleNextStep} disabled={!allValid}> + <button + css={nextButton} + onClick={handleNextStep} + disabled={!allValid} + aria-label="다음 단계로 이동" + aria-disabled={!allValid} + title={!allValid ? "모든 필수 항목을 입력하고 이메일 인증을 완료해주세요" : undefined} + > 다음 </button>src/app/join/components/Researcher/ResearcherForm.tsx (1)
1-14
: import 구문 순서를 개선해 주세요.import 구문을 다음과 같이 그룹화하고 정렬해 주세요:
- 외부 라이브러리
- 상대 경로 import
- 절대 경로 import (@/)
각 그룹 사이에는 빈 줄을 추가해 주세요.
import { zodResolver } from '@hookform/resolvers/zod'; import React, { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import useFunnel from '../../hooks/useFunnel'; import useResearcherJoinMutation from '../../hooks/useResearcherJoinMutation'; import { getProvider } from '../../JoinPage.utils'; +import { STEP } from '../../JoinPage.constants'; import JoinSuccessStep from '../JoinSuccessStep/JoinSuccessStep'; import { Researcher } from '.'; import useSessionStorage from '@/hooks/useSessionStorage'; import ResearcherJoinSchema, { ResearcherJoinSchemaType } from '@/schema/join/ResearcherJoinSchema'; -import { STEP } from '../../JoinPage.constants';🧰 Tools
🪛 ESLint
[error] 13-13: There should be at least one empty line between import groups
(import/order)
[error] 14-14:
../../JoinPage.constants
import should occur before import of../../JoinPage.utils
(import/order)
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.styles.ts (1)
3-21
: 툴팁의 접근성 개선이 필요합니다.현재 구현에서 다음과 같은 접근성 개선이 필요합니다:
- 툴팁 내용이 스크린 리더에서 적절히 읽힐 수 있도록
role="tooltip"
속성 추가- 키보드 사용자를 위한 포커스 관리
다음과 같이 수정하는 것을 제안합니다:
export const tooltipContent = (theme: Theme) => css` ${theme.fonts.label.medium.M13}; width: 20rem; left: 2.4rem; background-color: ${theme.colors.field01}; border-radius: 0.6rem; padding: 0.8rem 1.6rem; color: ${theme.colors.text05}; box-shadow: 0 4px 8px rgba(16, 17, 18, 0.1); border: 0.15rem solid ${theme.colors.line01}; + role: tooltip; + outline: none; user-select: none; animation-duration: 100ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); &[data-state='delayed-open'][data-side='bottom'] { animation-name: ${slideDownAndFade}; } `;src/app/join/components/JoinInput/JoinInput.tsx (2)
85-96
: input 요소의 접근성 개선이 필요합니다.다음과 같은 접근성 개선이 필요합니다:
- aria-label 또는 aria-labelledby 속성 추가
- 오류 상태를 위한 aria-errormessage 속성 추가
다음과 같이 수정하는 것을 제안합니다:
<input {...field} ref={inputRef} placeholder={placeholder} disabled={disabled} maxLength={maxLength} - aria-invalid={fieldState.invalid ? true : false} + aria-invalid={fieldState.invalid} + aria-label={label} + aria-errormessage={fieldState.error?.message} style={{ width: '100%' }} onFocus={() => setIsFocused(true)} onBlur={(e) => handleBlur(e, field.onBlur)} />🧰 Tools
🪛 Biome (1.9.4)
[error] 92-92: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with(lint/complexity/noUselessTernary)
111-121
: 리셋 버튼의 접근성과 사용성 개선이 필요합니다.리셋 버튼에 다음과 같은 개선이 필요합니다:
- 스크린 리더를 위한 aria-label 추가
- 키보드 접근성 개선
다음과 같이 수정하는 것을 제안합니다:
- <button css={inputResetButton} ref={resetButtonRef}> + <button + css={inputResetButton} + ref={resetButtonRef} + aria-label="입력 내용 지우기" + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleReset(field.onChange); + } + }} + >src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (2)
Line range hint
1-20
: import 문 순서를 정리해주세요.import 문을 다음과 같은 순서로 정리하면 코드의 가독성이 향상될 것 같습니다:
- 외부 라이브러리
- 상대 경로 컴포넌트
- 상수 및 타입
import { Controller, useFormContext, useWatch } from 'react-hook-form'; + +import AreaTooltip from './AreaTooltip/AreaTooltip'; import { filterTitle, filterTitleWrapper, joinAreaFilterContainer, joinAreaFilterWrapper, joinButton, joinContentContainer, requiredStar, } from './JoinInfoStep.styles'; import JoinSelect from './JoinSelect/JoinSelect'; import RadioButtonGroupContainer from './RadioButtonGroupContainer/RadioButtonGroupContainer'; import JoinInput from '../../JoinInput/JoinInput'; + import { JOIN_REGION, JOIN_SUB_REGION } from '@/app/join/JoinPage.constants'; import { joinForm } from '@/app/join/JoinPage.styles'; import { Gender, MatchType } from '@/app/join/JoinPage.types'; import { ParticipantJoinSchemaType } from '@/schema/join/ParticipantJoinSchema'; -import AreaTooltip from './AreaTooltip/AreaTooltip';🧰 Tools
🪛 ESLint
[error] 19-19: There should be at least one empty line between import groups
(import/order)
[error] 20-20:
./AreaTooltip/AreaTooltip
import should occur before import of./JoinInfoStep.styles
(import/order)
36-41
: 폼 상태 관리 최적화가 필요합니다.현재 구현에서 다음과 같은 개선사항을 제안드립니다:
useWatch
로 모든 필드를 개별적으로 추적하면 불필요한 리렌더링이 발생할 수 있습니다.isAllFilled
로직이 복잡해 보입니다.- const values = useWatch({ - name: ['name', 'gender', 'birthDate', 'basicAddressInfo.area', 'basicAddressInfo.region'], - control, - }); - - const isAllFilled = values.every((value) => (value ?? '').trim() !== '' && value !== undefined); + const formValues = useWatch({ control }); + + const isAllFilled = ['name', 'gender', 'birthDate', 'basicAddressInfo.area', 'basicAddressInfo.region'].every( + (field) => { + const value = formValues[field]; + return value != null && value.trim() !== ''; + } + );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
src/app/join/JoinPage.constants.ts
(1 hunks)src/app/join/components/JoinInput/JoinInput.tsx
(3 hunks)src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx
(2 hunks)src/app/join/components/Participant/JoinInfoStep/AreaTooltip.tsx
(0 hunks)src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.styles.ts
(1 hunks)src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx
(1 hunks)src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
(3 hunks)src/app/join/components/Participant/ParticipantForm.tsx
(1 hunks)src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
(1 hunks)src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
(3 hunks)src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx
(1 hunks)src/app/join/components/Researcher/ResearcherForm.tsx
(1 hunks)src/app/join/page.tsx
(3 hunks)
💤 Files with no reviewable changes (1)
- src/app/join/components/Participant/JoinInfoStep/AreaTooltip.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/join/page.tsx
🧰 Additional context used
🪛 ESLint
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx
[error] 3-3: There should be at least one empty line between import groups
(import/order)
[error] 4-4: ./AreaTooltip.styles
import should occur before import of @/components/Icon
(import/order)
src/app/join/components/Participant/ParticipantForm.tsx
[error] 13-15: There should be at least one empty line between import groups
(import/order)
[error] 16-16: ../../JoinPage.constants
import should occur before import of ../../JoinPage.utils
(import/order)
src/app/join/components/Researcher/ResearcherForm.tsx
[error] 13-13: There should be at least one empty line between import groups
(import/order)
[error] 14-14: ../../JoinPage.constants
import should occur before import of ../../JoinPage.utils
(import/order)
src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx
[error] 19-19: There should be at least one empty line between import groups
(import/order)
[error] 20-20: ./AreaTooltip/AreaTooltip
import should occur before import of ./JoinInfoStep.styles
(import/order)
🪛 Biome (1.9.4)
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx
[error] 72-72: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
src/app/join/components/JoinInput/JoinInput.tsx
[error] 55-55: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 92-92: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
[error] 103-103: Unnecessary use of boolean literals in conditional expression.
Simplify your code by directly assigning the result without using a ternary operator.
If your goal is negation, you may use the logical NOT (!) or double NOT (!!) operator for clearer and concise code.
Check for more details about NOT operator.
Unsafe fix: Remove the conditional expression with
(lint/complexity/noUselessTernary)
🔇 Additional comments (12)
src/app/join/components/Participant/JoinInfoStep/AreaTooltip/AreaTooltip.tsx (2)
6-24
: 구현이 잘 되었습니다!Radix UI Tooltip을 활용한 구현이 깔끔하고, 접근성도 잘 고려되었습니다. 툴팁의 위치와 화살표 스타일링도 적절합니다.
26-26
: export 문이 적절합니다.컴포넌트의 default export가 React 컴포넌트 export 관행을 잘 따르고 있습니다.
src/app/join/components/Researcher/JoinEmailStep/UnivAuthInput/UnivAuthInput.tsx (3)
15-22
: 타입 정의와 의존성 관리가 잘 되어있습니다!Zod 스키마를 활용한 타입 정의와 필요한 훅들의 import가 깔끔하게 구성되어 있습니다.
24-33
: useWatch 훅 사용으로 반응성이 개선되었습니다!
getValues
대신useWatch
를 사용함으로써 이메일 입력값의 변경을 더 효율적으로 감지할 수 있게 되었습니다. 이는 사용자 경험 향상에 도움이 됩니다.
Line range hint
35-48
: TODO 주석에 대한 후속 조치가 필요합니다.이미 인증된 사용자 처리에 대한 TODO 주석이 있습니다. 이 부분에 대한 구체적인 구현이 필요해 보입니다.
이 부분에 대한 구현을 도와드릴까요? 다음과 같은 방향으로 구현할 수 있습니다:
- 사용자 중복 체크 API 연동
- 이미 인증된 경우의 에러 메시지 처리
- 적절한 사용자 피드백 제공
src/app/join/JoinPage.constants.ts (1)
41-45
: LGTM! 단계 상수가 잘 정의되어 있습니다.
as const
타입 어서션을 사용하여 타입 안전성을 보장하고, 각 단계를 명확하게 정의했습니다.src/app/join/components/Researcher/ResearcherForm.tsx (1)
23-35
: 폼 설정이 잘 구현되어 있습니다.
- Zod 스키마를 사용한 validation
- 적절한 validation 모드 설정
- 기본값 초기화
src/app/join/components/Participant/ParticipantForm.tsx (2)
1-16
: import 구문 순서를 개선해 주세요.ResearcherForm과 동일한 import 구문 정렬 이슈가 있습니다.
🧰 Tools
🪛 ESLint
[error] 13-15: There should be at least one empty line between import groups
(import/order)
[error] 16-16:
../../JoinPage.constants
import should occur before import of../../JoinPage.utils
(import/order)
25-39
: 폼 설정이 잘 구현되어 있습니다.
- Zod 스키마를 활용한 validation
- 중첩된 객체(basicAddressInfo)에 대한 기본값 설정
- 적절한 validation 모드 설정
src/app/join/components/Researcher/JoinInfoStep/JoinInfoStep.tsx (1)
20-20
: 🛠️ Refactor suggestion유효성 검사 로직 개선이 필요합니다.
isAllFilled
로직이 개선되었지만, 여전히 다음과 같은 개선이 필요합니다:
- 공백 문자만 입력된 경우에 대한 처리
- 사용자에게 더 명확한 피드백 제공
다음과 같이 수정하는 것을 제안합니다:
- const isAllFilled = values.every((value) => (value ?? '').trim() !== '' && value !== undefined); + const isAllFilled = values.every((value) => { + const trimmedValue = value?.trim() ?? ''; + return trimmedValue !== '' && value !== undefined; + });Likely invalid or redundant comment.
src/app/join/components/Participant/JoinEmailStep/JoinEmailStep.tsx (1)
31-37
: handleNextStep 함수의 오류 처리가 필요합니다.이전 리뷰에서 지적된 오류 처리가 아직 구현되지 않았습니다. 사용자에게 적절한 피드백을 제공하기 위해 다음과 같이 수정하는 것을 제안합니다:
const handleNextStep = async () => { - const isValid = await trigger(['oauthEmail', 'contactEmail']); + try { + const isValid = await trigger(['oauthEmail', 'contactEmail']); + if (isValid) { + onNext(); + } + } catch (error) { + console.error('이메일 유효성 검사 중 오류 발생:', error); + // TODO: 사용자에게 에러 메시지 표시 - Toast 또는 Alert 컴포넌트 사용 + } - if (isValid) { - onNext(); - } };src/app/join/components/Participant/JoinInfoStep/JoinInfoStep.tsx (1)
172-178
: 회원가입 버튼 활성화 로직이 개선되었습니다.이전 리뷰 코멘트의 제안사항이 잘 반영되었습니다:
- 모든 필수 필드가 입력되었는지 확인
- 모든 필드가 유효성 검사를 통과했는지 확인
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
Outdated
Show resolved
Hide resolved
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: 0
♻️ Duplicate comments (2)
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
27-35
:⚠️ Potential issue이메일 검증 실패 시 에러 처리가 필요합니다.
handleNextStep
함수에서 이메일 검증 실패 시 사용자에게 적절한 피드백이 제공되지 않습니다.다음과 같이 수정하는 것을 제안합니다:
const handleNextStep = async () => { - const isValid = await trigger(['oauthEmail', 'contactEmail', 'univEmail']); - - if (isValid) { - onNext(); - } + try { + const isValid = await trigger(['oauthEmail', 'contactEmail', 'univEmail']); + if (isValid) { + onNext(); + } else { + alert('이메일 정보를 올바르게 입력해주세요.'); + } + } catch (error) { + console.error('이메일 검증 중 오류 발생:', error); + alert('이메일 검증 중 오류가 발생했습니다. 다시 시도해주세요.'); + } };
46-49
:⚠️ Potential issue이메일 인증 상태 관리를 개선해야 합니다.
현재 이메일 인증 상태가 컴포넌트 내부 상태로만 관리되고 있어, 페이지 새로고침 시 상태가 초기화됩니다.
폼 컨텍스트를 통해 이메일 인증 상태를 관리하는 것을 추천합니다:
- const [isEmailVerified, setIsEmailVerified] = useState(false); + const { setValue, watch } = useFormContext<ResearcherJoinSchemaType>(); + const isEmailVerified = watch('isEmailVerified'); const handleVerifyEmail = () => { - setIsEmailVerified(true); + setValue('isEmailVerified', true); };
🧹 Nitpick comments (6)
src/app/join/hooks/useFunnel.tsx (2)
34-43
: 컴포넌트 디버깅을 위한 displayName 추가 필요Funnel 컴포넌트의 memoization이 잘 구현되었습니다. 하지만 디버깅과 개발 도구에서의 식별을 위해 displayName을 추가하는 것이 좋습니다.
다음과 같이 수정해 주세요:
const Funnel = useMemo( () => { const FunnelComponent = ({ children }: FunnelProps) => { const targetStep = children.find((childStep) => childStep.props.name === currentStep); return <>{targetStep}</>; }; + FunnelComponent.displayName = 'Funnel'; + return FunnelComponent; }, [currentStep], );🧰 Tools
🪛 ESLint
[error] 36-40: Component definition is missing display name
(react/display-name)
45-50
: Step 컴포넌트에 displayName 추가 필요Step 컴포넌트의 memoization이 적절하게 구현되었습니다. 컴포넌트가 정적이므로 빈 의존성 배열은 적절합니다. 하지만 Funnel 컴포넌트와 마찬가지로 displayName을 추가하는 것이 좋습니다.
다음과 같이 수정해 주세요:
const Step = useMemo( () => { const StepComponent = (props: StepProps) => { return <>{props.children}</>; }; + StepComponent.displayName = 'Step'; + return StepComponent; }, [], );🧰 Tools
🪛 ESLint
[error] 46-48: Component definition is missing display name
(react/display-name)
src/app/join/components/Researcher/ResearcherForm.tsx (1)
27-35
: 폼의 초기값 설정을 개선해 주세요.빈 문자열로 초기화된 필드들이 사용자 경험에 영향을 줄 수 있습니다.
defaultValues: { oauthEmail: '', - contactEmail: '', + contactEmail: oauthEmail || '', univEmail: '', name: '', univName: '', major: '', },src/app/join/components/Participant/ParticipantForm.tsx (1)
25-39
: 폼 검증 모드 최적화가 필요합니다.현재 설정된 검증 모드가 사용자 경험을 최적화하지 못할 수 있습니다.
const participantMethods = useForm<ParticipantJoinSchemaType>({ resolver: zodResolver(ParticipantJoinSchema()), - mode: 'onBlur', - reValidateMode: 'onChange', + mode: 'onChange', + reValidateMode: 'onBlur', defaultValues: { ... }, });src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
17-26
: 폼 컨텍스트 에러 처리를 개선해야 합니다.
formState
에서isSubmitting
과 같은 추가적인 상태를 활용하여 사용자에게 더 나은 피드백을 제공할 수 있습니다.const { control, trigger, - formState: { errors }, + formState: { errors, isSubmitting, isValid }, } = useFormContext<ResearcherJoinSchemaType>();
37-45
: 폼 유효성 검사 로직을 개선할 수 있습니다.현재 유효성 검사 로직이 다소 복잡하며, 더 간단하고 견고하게 만들 수 있습니다.
const allValid = - oauthEmail && - univEmail && - Boolean(!errors.contactEmail) && - Boolean(!errors.univEmail) && - isEmailVerified && - serviceAgreeCheck.isTermOfService && - serviceAgreeCheck.isPrivacy; + Boolean(oauthEmail) && + Boolean(univEmail) && + !errors.contactEmail && + !errors.univEmail && + isEmailVerified && + Object.values(serviceAgreeCheck).every(Boolean);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/app/join/components/Participant/ParticipantForm.tsx
(1 hunks)src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx
(1 hunks)src/app/join/components/Researcher/ResearcherForm.tsx
(1 hunks)src/app/join/hooks/useFunnel.tsx
(2 hunks)src/app/join/page.tsx
(2 hunks)
🧰 Additional context used
🪛 ESLint
src/app/join/page.tsx
[error] 19-19: There should be at least one empty line between import groups
(import/order)
[error] 20-20: ./JoinPage.constants
import should occur before import of ./JoinPage.styles
(import/order)
src/app/join/hooks/useFunnel.tsx
[error] 36-40: Component definition is missing display name
(react/display-name)
[error] 46-48: Component definition is missing display name
(react/display-name)
src/app/join/components/Participant/ParticipantForm.tsx
[error] 13-15: There should be at least one empty line between import groups
(import/order)
[error] 16-16: ../../JoinPage.constants
import should occur before import of ../../JoinPage.utils
(import/order)
src/app/join/components/Researcher/ResearcherForm.tsx
[error] 13-13: There should be at least one empty line between import groups
(import/order)
[error] 14-14: ../../JoinPage.constants
import should occur before import of ../../JoinPage.utils
(import/order)
🔇 Additional comments (8)
src/app/join/hooks/useFunnel.tsx (1)
52-52
: 반환값 업데이트가 적절히 구현되었습니다.Step 컴포넌트를 반환값에 포함시킨 것은 좋은 변경이며, const assertion을 사용하여 타입 안정성을 보장한 것도 적절합니다.
src/app/join/page.tsx (2)
26-27
: 스켈레톤 UI 구현이 필요합니다.현재 role이 없을 때 null을 반환하는 것은 사용자 경험을 저하시킬 수 있습니다. 스켈레톤 UI 구현 전까지 임시 로딩 상태를 추가하는 것을 제안드립니다.
22-44
: 컴포넌트 구조가 잘 개선되었습니다!
- ResearcherForm과 ParticipantForm으로 책임을 분리하여 코드의 가독성과 유지보수성이 향상되었습니다.
- role에 따른 조건부 렌더링이 명확하게 구현되었습니다.
src/app/join/components/Researcher/ResearcherForm.tsx (1)
37-40
: 제출 핸들러의 에러 처리를 개선해 주세요.현재
handleResearcherSubmit
함수에는 에러 처리가 없습니다. mutation이 실패할 경우를 대비한 에러 처리를 추가하는 것이 좋습니다.src/app/join/components/Participant/ParticipantForm.tsx (2)
41-52
: 생년월일 형식 변환 및 에러 처리를 개선해 주세요.
- 생년월일 형식 변환 로직을 유틸리티 함수로 분리하는 것이 좋습니다.
- mutation 실패 시 에러 처리가 필요합니다.
61-77
: 폼 구조가 잘 구현되었습니다!
- FormProvider를 통한 폼 컨텍스트 관리가 잘 되어있습니다.
- Funnel 컴포넌트를 활용한 단계별 폼 구현이 명확합니다.
src/app/join/components/Researcher/JoinEmailStep/JoinEmailStep.tsx (2)
1-16
: 코드 구조가 잘 정리되어 있습니다!타입스크립트 인터페이스와 임포트 구문이 명확하게 정의되어 있습니다.
50-82
: 컴포넌트 구조가 깔끔합니다!컴포넌트 렌더링 로직이 잘 구성되어 있으며, 커스텀 컴포넌트들이 적절하게 활용되고 있습니다.
추가 반영사항입니다!
|
Issue Number
close #30
As-Is
To-Be
Check List
Test Screenshot
(Optional) Additional Description
에러 상태 처리
validaiton을 onChange 시점에 하는 경우 에러 상태가 변할 때마다 또는 에러 메세지가 변할 때마다 인풋창의 포커스를 잃는 문제가 발생했습니다. 지윤님의 코드랑 비교하면서 다른 부분을 하나씩 바꿔가면서 비교하다보니 문제를 찾을 수 있었습니다. validation 하는 부분에서 useFormContext에서 가져온 errors를 사용하는 경우 errors 객체 상태가 변경될 때마다 입력하던 인풋창의 포커스를 잃는 문제였습니다! 따라서 해당 문제를 Controller에서 제공하는 fieldState의 error.message로 원하는 동작을 처리할 수 있었습니다.
학교 인증 메일 처리
학교 인증 메일 처리 시 getValues로 univEmail을 가져와 이메일 전송 API를 호출하는 방식을 사용하고 있었습니다. 이때 에러가 발생하면 기존 상태값이 초기화되고, 체크되어있던 체크박스가 풀리면서 다시 처리해야되는 문제가 있었습니다. 해당 문제도 위와 비슷할 거라고 생각해서 getValues를 제거하고 useWatch 함수를 사용하여 univEmail을 가져왔더니 상태가 초기화되는 문제가 해결되었습니다.
Summary by CodeRabbit
릴리즈 노트
새로운 기능
useUserInfo
추가JoinEmailStep
,JoinInfoStep
,ParticipantForm
,ResearcherForm
와 같은 새로운 폼 컴포넌트 추가AreaTooltip
컴포넌트 추가useSessionStorage
훅 추가GoogleLoginLayout
및NaverLoginLayout
컴포넌트 추가버그 수정
사용자 경험 개선