스터디 문의(기능 추가) 및 스터디 목록/상세 UI 개선#401
Conversation
- useUserStore(Zustand persist) → useAuth로 교체하여 Zustand rehydration 타이밍 이슈 해결 - 첫 렌더부터 서버 토큰 기반으로 isLoggedIn/isLeader 판단하여 리더가 모집 마감 상태를 즉시 표시 - prefetchQuery queryKey 불일치 수정: 'groupStudyMyStatus' → 'groupStudyMemberStatus' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inquiry -> question
- 403 에러 방지용
- 스터디 신청자 승인 후 뒤로 가기를 눌러야 토스트가 뜨고 있던 문제 발견 - 승인 버튼 클릭 후 바로 토스트가 뜨도록 수정
- countdown.ts: D-3/D-2/D-1/HH:MM:SS 단계별 카운트다운 유틸리티 추가 - useNow: 싱글턴 setInterval로 1초마다 현재 시각 갱신하는 훅 추가 - StudyCardCountdownBadge: 스터디 카드 이미지 영역에 모집 마감 D-day 뱃지 표시 - StudyActiveTicker: 스터디 상세 페이지에 모집 현황 전광판 및 마감 카운트다운 추가 - study-card: 카운트다운 뱃지 연결 및 남은 자리 모집 현황 UI 추가 - group-study-info-section: StudyActiveTicker 연결 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- study-filter: 스터디 대상(입문자/취준생/주니어/미들/시니어) 필터 드롭다운 추가 - use-study-list-filter: 경험 레벨 클라이언트 사이드 필터링 로직 추가 - 경험 레벨 필터 적용 시 전체 데이터를 가져온 후 클라이언트에서 필터링 (isClientFiltered 플래그) - 검색과 경험 레벨 필터를 함께 지원하도록 filteredStudies 로직 개선 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ImageDto, ResizedImage 타입 추가 - QuestionListItemResponse: viewCount, authorProfileImage, authorId 필드 추가 - QuestionDetailResponse: viewCount, authorProfileImage, questionImage 필드 추가 - GetQuestionsResponse: page/hasNext/hasPrevious 페이지네이션 필드 추가 (number → page 변경) - category 필드 옵셔널 처리 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inquiry/page: Badge 컴포넌트 인라인 포맷팅 - group-study-form-modal: import 순서 정렬 - step2-group: ImageUploadInput props 인라인 포맷팅 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 남은 인원만 보이도록 수정 - 팝오버가 브라우저에 의해 잘리는 부분을 ref로 해결
- QuestionDetailResponse에 answer(string), answererId, answererNickname, answeredAt 플랫 필드 추가 - CreateQuestionRequest에 imageExtension 필드 추가 - createAnswer API 함수 추가 - question.schema에 imageExtension 옵셔널 필드 추가 - useCreateAnswer 뮤테이션 훅 추가 (onSuccess 캐시 무효화 포함) - useCreateQuestion에 onSuccess invalidateQueries 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- InquiryStatusBadge 컴포넌트 신규 추가 (ACCEPTED: gray, ANSWER_COMPLETED: green) - InquirySection 컴포넌트 신규 추가 (그룹 스터디 탭 내 문의 기능 통합) - QuestionModal에 onAfterSubmit 콜백 추가 (제출 후 동작 커스터마이즈 지원) - QuestionModal에서 imageExtension을 API 요청에 전달하도록 수정 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 문의 목록: 번호 역순 표시, 테이블 정렬 left, hover 상태 관리, date-fns 포맷 적용 - 문의 목록: InquiryStatusBadge 교체, 비접근 문의 opacity 처리, 조회수 아이콘 추가 - 문의 상세: MoreMenu(수정/삭제) 추가, 헤더 레이아웃 개선, ArrowLeft 목록 버튼 - 문의 상세: InquiryStatusBadge 교체, date-fns 포맷 적용, 카테고리 뱃지 스타일 개선 - 그룹 스터디 상세: 인라인 QuestionModal 제거 → InquirySection 탭으로 통합 - 그룹 스터디 상세: useAuthReady로 isAdmin 계산 추가 - constants.ts: STUDY_DETAIL_TABS에 문의 탭 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough문의(Question) 기능을 새로 도입하고 관련 API/스키마/훅/컴포넌트를 추가했으며, GlobalToast 렌더링을 메인 레이아웃으로 통합, 이미지 업로드·플로팅 버튼·틱커·카운트다운·경험 수준 필터 등을 추가하고 기존 Inquiry 파일들을 삭제 또는 리브랜딩했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant InquiryPage as 문의 페이지
participant InquirySection as InquirySection
participant QuestionModal as QuestionModal
participant QueryHooks as React-Query 훅
participant API as Question API
User->>InquiryPage: 문의 탭 접근
InquiryPage->>QueryHooks: useGetQuestions(groupStudyId, page)
QueryHooks->>API: GET /group-studies/{id}/questions
API-->>QueryHooks: 질문 목록 응답
QueryHooks-->>InquiryPage: 목록 데이터 제공
InquiryPage->>InquirySection: 목록 렌더링
User->>InquirySection: 항목 클릭
InquirySection->>QueryHooks: useGetQuestion(groupStudyId, questionId)
QueryHooks->>API: GET /group-studies/{id}/questions/{questionId}
API-->>QueryHooks: 상세 응답
QueryHooks-->>InquirySection: 상세 데이터 렌더링
User->>QuestionModal: 새 문의 제출(이미지 포함 가능)
QuestionModal->>QueryHooks: useCreateQuestion 호출
QueryHooks->>API: POST /group-studies/{id}/questions (body + imageExtension)
API-->>QueryHooks: 생성 응답
QueryHooks->>QueryHooks: invalidateQueries(questions 리스트)
QueryHooks-->>InquirySection: 목록 갱신 트리거
QuestionModal-->>User: 성공 토스트 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/components/ui/avatar-stack.tsx (1)
84-106:⚠️ Potential issue | 🟡 Minor오버플로우 목록에
max-height미지정 — 많은 참가자 시 팝오버가 뷰포트를 초과할 수 있음이전 구현의 스크롤 제약(
max-h,overflow-y-auto)이 제거되었습니다.overflow배열 크기가 크면 팝오버가 화면 밖으로 나가게 됩니다.🐛 제안 수정
- <ul className="flex flex-col gap-100"> + <ul className="flex flex-col gap-100 max-h-60 overflow-y-auto">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/avatar-stack.tsx` around lines 84 - 106, The overflow list can grow unbounded and cause the popover to exceed the viewport; update the <ul> that renders overflow (in avatar-stack.tsx, the element using overflow.map) to constrain its height and enable vertical scrolling by adding a max-height (e.g., a responsive value like max-h-[60vh] or a Tailwind token such as max-h-64) and overflow-y-auto (and keep gap-100) so extra members scroll inside the popover rather than expanding it; ensure the change targets the <ul className="..."> that renders the overflow array.src/features/study/group/ui/step/step2-group.tsx (2)
45-50:⚠️ Potential issue | 🟡 Minor
alert()사용 — toast로 교체 필요PR 목표 중 하나가
alert()→ toast 마이그레이션인데, 이 파일에 여전히alert()가 남아 있습니다.🐛 제안 수정
+ const showToast = useToastStore((state) => state.showToast); ... - alert('이미지 용량은 5MB 이하만 업로드할 수 있어요.'); + showToast('이미지 용량은 5MB 이하만 업로드할 수 있어요.', 'error');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/ui/step/step2-group.tsx` around lines 45 - 50, Replace the use of alert() in the file-size check with the project's toast API: where MAX_SIZE and the file.size check currently call alert('이미지 용량은 5MB 이하만 업로드할 수 있어요.'), call the toast error function instead (e.g., toast.error(...) or showToast.error(...), whichever is used across the codebase) and keep the early return; ensure the toast import is added/used consistently with other components and remove the alert reference (update any surrounding handler in the same function/component so the behavior and return remain unchanged).
36-61:⚠️ Potential issue | 🔴 Critical
handleImageChange의 파라미터 타입 (File | null)이onChangeImageprop 타입 (File | undefined)과 불일치
ImageUploadInput의onChangeImageprop은(file: File | undefined) => void를 기대하지만,handleImageChange는File | null을 파라미터로 받습니다. TypeScript strict 모드에서는null이undefined에 할당되지 않아 타입 오류가 발생합니다.🐛 제안 수정
- const handleImageChange = (file: File | null) => { - if (!file) { + const handleImageChange = (file: File | undefined) => { + if (!file) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/ui/step/step2-group.tsx` around lines 36 - 61, Change the handleImageChange parameter from File | null to File | undefined to match the ImageUploadInput onChangeImage signature, and update the branch that clears the image to use undefined consistently (setValue('thumbnailExtension','DEFAULT',...) and setValue('thumbnailFile', undefined) and setImage(undefined)). Keep the rest of the logic (size check, ext parsing, setValue('thumbnailExtension', validExt), setValue('thumbnailFile', file), setImage(URL.createObjectURL(file))) the same; reference handleImageChange, onChangeImage, thumbnailFile, thumbnailExtension, and setImage when making the edits.
♻️ Duplicate comments (1)
src/features/phone-verification/model/use-phone-auth-mutation.ts (1)
37-42: 위의useSendPhoneVerificationCodeMutation과 동일한 타입 안전성 관련 사항이 적용됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/phone-verification/model/use-phone-auth-mutation.ts` around lines 37 - 42, The mutation lacks explicit type parameters for useMutation causing weaker type safety like in useSendPhoneVerificationCodeMutation; update the useMutation call in usePhoneAuthMutation so the generic types are provided and the mutationFn return type is explicit — e.g., declare useMutation<ReturnContentType, ApiError, VerifyPhoneCodeRequest> and annotate mutationFn as async (data: VerifyPhoneCodeRequest): Promise<ReturnContentType>, using the same Response/Content type you used for phoneAuthApi.verifyCode and referencing phoneAuthApi.verifyCode, VerifyPhoneCodeRequest and res.content to ensure correct typing.
🧹 Nitpick comments (26)
src/features/study/group/ui/group-study-form-modal.tsx (1)
219-222: 수정 실패 경로에도 에러 로그를 남겨 주세요.
Line 219-222는 사용자 토스트만 있고 개발자용 진단 로그가 없어 장애 분석이 어렵습니다. 생성 경로(Line 190)와 동일하게 최소한의 콘솔 로깅을 맞추는 것을 권장합니다.권장 수정안
} catch (err) { + console.error('[handleEdit] 그룹 스터디 수정 실패:', err); showToast( '그룹 스터디 수정 중 오류가 발생했습니다. 다시 시도해 주세요.', 'error', ); } finally {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/ui/group-study-form-modal.tsx` around lines 219 - 222, The failure path currently only calls showToast('그룹 스터디 수정 중 오류가 발생했습니다. 다시 시도해 주세요.', 'error') without any developer-facing log; update the catch/failure block in group-study-form-modal (the update/submit handler where showToast is called) to also log the error details (e.g., console.error('Failed to update group study', error)) so diagnostics mirror the creation path's logging; ensure you log the caught error variable alongside a short contextual message.src/features/phone-verification/model/use-phone-auth-mutation.ts (2)
19-24:useMutation에 제네릭 타입 파라미터 추가를 고려해 주세요.
mutationFn이res.content를 반환하지만,useMutation에 타입 파라미터가 없어서onSuccess의data가any로 추론됩니다. 또한SendPhoneVerificationCodeRequest와 OpenAPI에서 생성된PhoneAuthSendRequestDto가 별도 타입으로 존재하므로, API 스펙 변경 시 동기화가 깨질 수 있습니다. OpenAPI 생성 타입을 직접 사용하면 타입 안전성을 확보하고 중복 타입 관리 부담을 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/phone-verification/model/use-phone-auth-mutation.ts` around lines 19 - 24, The mutation is missing a generic type on useMutation so onSuccess receives any; update the useMutation call to supply explicit generics: set the Variables type to the OpenAPI-generated request type (PhoneAuthSendRequestDto) instead of SendPhoneVerificationCodeRequest, set the Return type to whatever res.content's OpenAPI-generated response type is (use that DTO name), and keep/declare the Error type as appropriate; adjust the mutationFn signature to accept the OpenAPI request type and return the OpenAPI response DTO so onSuccess callbacks get proper typed `data`.
50-59: 중복된 쿼리 무효화 호출.Line 50-53의
invalidateQueries({ queryKey: ['userProfile', currentMemberId] })는 Line 55-59의predicate기반 무효화(queryKey[0] === 'userProfile')에 이미 포함되는 범위입니다. 첫 번째 호출은 제거해도 동작이 동일합니다.♻️ 중복 제거 제안
if (currentMemberId) { - await queryClient.invalidateQueries({ - queryKey: ['userProfile', currentMemberId], - }); - // 모든 userProfile 쿼리도 무효화 (다른 곳에서 사용 중일 수 있음) await queryClient.invalidateQueries({ predicate: (query) => Array.isArray(query.queryKey) && query.queryKey[0] === 'userProfile', }); // 페이지 새로고침하여 서버 데이터 반영 router.refresh(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/phone-verification/model/use-phone-auth-mutation.ts` around lines 50 - 59, Remove the redundant invalidateQueries call that passes queryKey: ['userProfile', currentMemberId] inside the currentMemberId branch; the subsequent await queryClient.invalidateQueries with predicate checking query.queryKey[0] === 'userProfile' already covers that case. In other words, in the function where you call queryClient.invalidateQueries twice (one with queryKey array and one with predicate), delete the first queryClient.invalidateQueries({ queryKey: ['userProfile', currentMemberId] }) invocation and keep the predicate-based invalidateQueries to invalidate all userProfile queries.src/features/my-page/model/use-update-user-profile-mutation.ts (3)
23-32: API 호출 로직이model/레이어에 직접 인라인됨 — 아키텍처 일관성 검토 필요기존의
src/features/my-page/api/update-user-profile.tswrapper가 삭제되고, 모든 axios 호출이model/파일 내부에 인라인되었습니다. 프로젝트 규칙에 따르면 "레거시 API 엔드포인트는src/features/<domain>/api/디렉터리에 axios 함수로 직접 작성"해야 합니다. 현재 구조는 API 호출과 쿼리 훅 로직의 관심사를 혼합하여 재사용성 및 테스트 용이성을 낮춥니다.인라인 방식보다는
src/features/my-page/api/하위에 순수 axios 함수를 유지하고,model/훅에서 이를 호출하는 패턴이 일관성 측면에서 권장됩니다.Based on learnings: "For legacy API endpoints, write axios functions directly in src/features//api/ directory using axiosInstance with baseURL /api/v1/."
Also applies to: 44-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/my-page/model/use-update-user-profile-mutation.ts` around lines 23 - 32, The inline axios call inside mutationFn should be moved into a dedicated API function to follow the legacy pattern: create/export a function (e.g. updateUserProfile(memberId: string, formData: UpdateUserProfileRequest)) under the my-page api module that uses axiosInstance (baseURL /api/v1/) to call PATCH `/members/${memberId}/profile` and returns the parsed content (UpdateUserProfileResponse); then change the model's mutationFn to call that API function instead of using axiosInstanceV2 directly, keeping the same types (UpdateUserProfileRequest/Response) and handling of the returned content.
43-53:useUpdateUserProfileInfoMutation에mutationKey누락
useUpdateUserProfileMutation은mutationKey: ['updateUserProfile', memberId]를 명시하지만,useUpdateUserProfileInfoMutation은mutationKey가 없어 일관성이 깨집니다.memberId를 URL에 사용하므로 캐시 키에도 포함해야 합니다.♻️ 수정 예시
return useMutation({ + mutationKey: ['updateUserProfileInfo', memberId], mutationFn: async ( formData: UpdateUserProfileInfoRequest, ): Promise<UpdateUserProfileInfoResponse> => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/my-page/model/use-update-user-profile-mutation.ts` around lines 43 - 53, The useUpdateUserProfileInfoMutation hook is missing a mutationKey causing inconsistency with useUpdateUserProfileMutation; update the useMutation call inside useUpdateUserProfileInfoMutation to include mutationKey: ['updateUserProfileInfo', memberId] (or similar unique key) so the cache key includes memberId and matches the pattern used by useUpdateUserProfileMutation; locate the useMutation invocation in use-update-user-profile-mutation.ts and add the mutationKey option alongside mutationFn.
61-70:staleTime미설정 — 조회 전용 쿼리에staleTime: 60 * 1000추가 권장
useStudyDashboardQuery만staleTime: 60 * 1000을 지정하고 있으나, 나머지 6개 쿼리(useAvailableStudyTimesQuery,useStudySubjectsQuery,useTechStacksQuery,useJobsQuery,useCareersQuery,useStudyFormatTypesQuery)는staleTime이 없어 컴포넌트 마운트 시마다 불필요한 네트워크 요청이 발생합니다. 특히jobs,careers,studyFormatTypes등 Enum 성격의 lookup 엔드포인트는 거의 변경되지 않으므로 더 긴 stale time이 적절합니다.♻️ 수정 예시 (각 쿼리에 동일하게 적용)
export const useAvailableStudyTimesQuery = () => { return useQuery({ queryKey: ['availableStudyTimes'], queryFn: async (): Promise<AvailableStudyTimeResponse[]> => { const res = await axiosInstanceV2.get('/api/v1/available-study-times'); return res.data.content; }, + staleTime: 60 * 1000, }); };Based on learnings: "Use TanStack Query for server state with a default staleTime of 60 seconds."
Also applies to: 72-81, 83-92, 109-118, 120-129, 131-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/my-page/model/use-update-user-profile-mutation.ts` around lines 61 - 70, Add a staleTime to the readonly query hooks so they don't refetch on every mount: update useAvailableStudyTimesQuery (and similarly useStudySubjectsQuery, useTechStacksQuery, useJobsQuery, useCareersQuery, useStudyFormatTypesQuery) to pass staleTime: 60 * 1000 in the useQuery options (and for true enum/lookup endpoints like useJobsQuery, useCareersQuery, useStudyFormatTypesQuery consider a longer staleTime), mirroring the pattern already used in useStudyDashboardQuery; ensure the staleTime value is applied to the returned useQuery call for each named hook.src/components/ui/avatar-stack.tsx (1)
142-147: Crown 아이콘의cursor-pointer가 중복
<Crown>은 이미 Line 135의 부모div에cursor-pointer가 적용되어 있으므로, Crown SVG에 별도로cursor-pointer를 지정할 필요가 없습니다.♻️ 제안 수정
- <Crown - className="h-4 w-4 cursor-pointer text-pink-400" - fill="currentColor" - /> + <Crown + className="h-4 w-4 text-pink-400" + fill="currentColor" + />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/avatar-stack.tsx` around lines 142 - 147, The Crown SVG has a duplicate cursor-pointer style because its parent div (the avatar wrapper) already applies cursor-pointer; remove the redundant "cursor-pointer" token from the Crown component's className (the <Crown ... className="h-4 w-4 cursor-pointer text-pink-400" /> instance) so the class becomes only the needed sizing and color classes, leaving the parent div's cursor behavior intact.src/lib/countdown.ts (1)
3-28:COUNTDOWN_STAGE_CONFIG의minDays/maxDays가 동일한 값이라 불필요한 복잡성이 있습니다.각 stage의
minDays와maxDays가 항상 같은 값입니다 (3-3, 2-2, 1-1).find를 사용한 범위 검색 대신diffDays를 키로 하는 맵(Map/Record)을 사용하면 조회가 더 명확하고 효율적입니다. 현재 구조도 동작에는 문제가 없으므, 향후 범위가 필요해질 경우를 대비한 것이라면 그대로 두셔도 됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/countdown.ts` around lines 3 - 28, COUNTDOWN_STAGE_CONFIG currently lists stages with identical minDays and maxDays (3-3, 2-2, 1-1); replace the array-of-ranges with a simple lookup keyed by diffDays to simplify and speed up lookups: create a Record<number, Stage> or Map<number, Stage> (referencing COUNTDOWN_STAGE_CONFIG, minDays, maxDays, and diffDays) where keys 3,2,1 map to the stage objects (label, bgClass, textColorClass, pulse), update any code that iterates or uses find on COUNTDOWN_STAGE_CONFIG to instead index into the new map (or use map.get(diffDays)); keep the existing stage shape and export name or provide a small compatibility wrapper if other modules expect an array.src/components/ui/study-active-ticker.tsx (1)
37-47:setInterval내부의setTimeout콜백이 언마운트 후 실행될 수 있습니다.컴포넌트 언마운트 시
clearInterval만 호출하고 이미 큐에 들어간setTimeout은 정리하지 않습니다. React 19에서는 언마운트된 컴포넌트의setState가 무시되므로 에러는 발생하지 않지만, cleanup을 명확히 하려면setTimeoutID도 함께 정리하는 것이 더 견고합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/study-active-ticker.tsx` around lines 37 - 47, The effect starts an interval that schedules a nested setTimeout but only clears the interval on cleanup, so the pending timeout callback (which calls setVisible/setCurrentIndex) can run after unmount; fix by capturing the timeout ID returned by setTimeout inside the interval callback (e.g., let timeoutId: ReturnType<typeof setTimeout>), store it in the effect scope, and call clearTimeout(timeoutId) in the cleanup returned by the useEffect alongside clearInterval; update the useEffect that references messages.length, setVisible, and setCurrentIndex to clear both interval and any pending timeout before exit.src/components/ui/study-card-countdown-badge.tsx (1)
14-31:useNow()가 무조건 호출되어 모든 카드가 매초 리렌더링됩니다.
useNow()는 훅이므로 조건부 호출이 불가능하지만, 현재 구조에서는status !== 'RECRUITING'이거나remaining <= 0인 카드도 매초 리렌더링됩니다. 스터디 목록에 카드가 많을 경우 성능 저하가 발생할 수 있습니다.카운트다운이 실제로 필요한 경우(RECRUITING 상태 + 잔여석 있음 + startDate 존재)에만
useNow를 구독하는 하위 컴포넌트로 분리하는 것을 권장합니다.♻️ 카운트다운 부분만 분리하는 구조 예시
+function CountdownBadgeInner({ startDate }: { startDate: string }) { + const now = useNow(); + const start = dayjs(startDate); + const diffMs = start.diff(now); + const state = getCountdownState(diffMs); + + if (!state || !state.urgent) { + return ( + <span className="rounded-50 bg-blue-500 px-200 py-50 text-[12px] font-semibold text-white"> + 모집 중 + </span> + ); + } + + return ( + <span + className={`rounded-50 px-200 py-50 text-[12px] font-semibold text-white ${state.bgClass} ${state.pulse ? 'animate-pulse' : ''}`} + > + 마감까지 {state.label} + </span> + ); +} + export default function StudyCardCountdownBadge({ startDate, status, remaining, }: Props) { - const now = useNow(); - if (status !== 'RECRUITING') return null; if (remaining !== undefined && remaining <= 0) { return ( <span className="rounded-50 bg-red-500 px-200 py-50 text-[12px] font-semibold text-white"> 모집 마감 </span> ); } if (!startDate) return null; - const start = dayjs(startDate); - const diffMs = start.diff(now); - const state = getCountdownState(diffMs); - - if (!state || !state.urgent) { - return ( - <span className="rounded-50 bg-blue-500 px-200 py-50 text-[12px] font-semibold text-white"> - 모집 중 - </span> - ); - } - - return ( - <span - className={`rounded-50 px-200 py-50 text-[12px] font-semibold text-white ${state.bgClass} ${state.pulse ? 'animate-pulse' : ''}`} - > - 마감까지 {state.label} - </span> - ); + return <CountdownBadgeInner startDate={startDate} />; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/study-card-countdown-badge.tsx` around lines 14 - 31, The component StudyCardCountdownBadge currently calls the useNow() hook unconditionally causing every card to re-render every second; change the implementation so StudyCardCountdownBadge only does quick checks for status, remaining, and startDate, and when countdown is actually needed (status === 'RECRUITING' && remaining !== undefined && remaining > 0 && startDate) render a small child component (e.g., StudyCardCountdownTimer) that calls useNow() and computes the live remaining time; move any JSX that depends on the ticking value into that child so useNow is only subscribed for cards that truly need live updates.src/components/card/study-card.tsx (1)
78-88:remaining계산이 중복되고 있습니다.
remaining값이StudyCardCountdownBadgeprop(Line 83-86)과 인라인 IIFE(Line 118-120)에서 동일한 로직으로 두 번 계산됩니다. 컴포넌트 상단에서 한 번만 계산하여 재사용하세요.♻️ 수정 제안
export default function StudyCard({ study, href, onClick }: StudyCardProps) { const studyType = study.basicInfo?.type as StudyType; const badgeColor = studyType ? STUDY_TYPE_BADGE_COLORS[studyType] : 'default'; const price = study.basicInfo?.price ?? 0; + const remaining = + (study.basicInfo?.maxMembersCount ?? 0) - + (study.basicInfo?.approvedCount ?? 0);그런 다음 Line 83-86과 Line 118-120에서 이 변수를 재사용합니다.
Also applies to: 115-145
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/card/study-card.tsx` around lines 78 - 88, The remaining seats count is being calculated twice (once for the StudyCardCountdownBadge prop and again in the inline IIFE); compute it once at the top of the StudyCard component (e.g., const remaining = (study.basicInfo?.maxMembersCount ?? 0) - (study.basicInfo?.approvedCount ?? 0)) and replace both uses — the remaining prop passed to StudyCardCountdownBadge and the inline IIFE that currently repeats the same arithmetic — to reuse this single variable; update any other duplicate occurrences in this component (including the block around StudyCardCountdownBadge and the later inline logic) to reference that variable.src/components/section/group-study-info-section.tsx (1)
157-165:curriculumSummary필드는GroupStudyFullResponseDto에 존재하지 않습니다. 타입 단언은 유지보수 부채입니다.OpenAPI 자동 생성 DTO에서
curriculumSummary필드가 누락되어 있어, 타입 단언으로 우회하고 있습니다.?? []폴백은 항상 빈 배열을 반환할 것입니다. OpenAPI 스펙을 업데이트하거나 별도의 API 호출로 데이터를 가져오도록 리팩토링해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/section/group-study-info-section.tsx` around lines 157 - 165, The code is asserting a non-existent curriculumSummary on studyDetail to satisfy CurriculumSummarySection; remove the type assertion and either (A) update the OpenAPI spec so GroupStudyFullResponseDto includes curriculumSummary and regenerate DTOs, then pass studyDetail.curriculumSummary directly to CurriculumSummarySection, or (B) implement a separate fetch (e.g., getGroupStudyCurriculum or similar) that retrieves the curriculum summary and pass that result (with proper loading/error handling) to CurriculumSummarySection instead of casting studyDetail; update references to CurriculumSummarySection and studyDetail accordingly.src/features/my-page/ui/my-study-info-card.tsx (1)
38-43: 조건부 클래스 조합은clsx/tailwind-merge유틸로 통일해 주세요.현재 문자열 템플릿 조합은 동작은 맞지만, 이 PR에서 추가한 상태 스타일은 팀 공통 패턴으로 맞추는 편이 유지보수에 유리합니다.
예시 수정
+import clsx from 'clsx'; ... - className={`rounded-100 h-[244px] w-[280px] object-cover ${status === 'COMPLETED' ? 'grayscale' : ''}`} + className={clsx( + 'rounded-100 h-[244px] w-[280px] object-cover', + status === 'COMPLETED' && 'grayscale', + )}As per coding guidelines, "Style components using Tailwind CSS 4 with
@tailwindcss/postcssplugin, clsx, tailwind-merge, and class-variance-authority (CVA) utilities".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/my-page/ui/my-study-info-card.tsx` around lines 38 - 43, Replace the inline template-string className combos with the team's utility (clsx or twMerge) in the MyStudyInfoCard component: import and use clsx/twMerge to compose the base classes ("rounded-100 h-[244px] w-[280px] object-cover") with the conditional grayscale class when status === 'COMPLETED', and similarly compute the overlay div's classes via the same utility rather than rendering a separate hardcoded string; update the JSX className props (the image element and the conditional overlay div) to use the composed value from clsx/twMerge so styling follows the shared pattern.src/components/ui/floating-inquiry-button.tsx (1)
23-32:src/components/ui레이어에서는 공통 UI 컴포넌트 사용으로 맞춰 주세요.여기서는 직접
<button>대신 프로젝트 공통Button컴포넌트를 사용하는 편이 일관성/토큰 적용 측면에서 좋습니다.As per coding guidelines, "src/components/ui/**/*.ts{,x}: Use shadcn/ui components from src/components/ui/ with 'new-york' style configuration from components.json".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/floating-inquiry-button.tsx` around lines 23 - 32, Replace the native <button> in floating-inquiry-button.tsx with the project shared Button component: import and use the Button component from the UI layer instead of the HTML button, preserve the aria-label and onClick={setOpen(true)} handler (reference setOpen and MessageCircle), and move or keep the positioning and styling via the Button's className prop so the fixed right/bottom/z-50/rounded/px/py/shadow/transition styles remain; ensure the MessageCircle icon and the span text "스터디 문의하기" remain as children of Button and that the Button variant/configuration follows the "new-york" style tokens used by other components in src/components/ui.src/features/study/group/model/question.schema.ts (1)
11-17:CATEGORY_LABEL의 타입을Record<QuestionCategory, string>으로 강화 권장현재
Record<string, string>으로 선언되어 있어QuestionCategory멤버가 누락되어도 컴파일 타임에 감지되지 않습니다.♻️ 제안 수정
-export const CATEGORY_LABEL: Record<string, string> = { +export const CATEGORY_LABEL: Record<QuestionCategory, string> = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/model/question.schema.ts` around lines 11 - 17, Change the CATEGORY_LABEL declaration to be strongly typed with the QuestionCategory enum/type so missing categories are caught at compile time: replace Record<string, string> with Record<QuestionCategory, string> on the CATEGORY_LABEL export and ensure QuestionCategory is imported or referenced in this file (verify the type is available where CATEGORY_LABEL is defined) so the compiler enforces that all QuestionCategory members have labels.src/features/study/group/api/question-api.ts (2)
33-33:status리터럴 유니온 타입이 두 인터페이스에 중복 선언
QuestionListItemResponse와QuestionDetailResponse모두 동일한'ACCEPTED' | 'ANSWER_COMPLETED'유니온을 인라인으로 갖고 있습니다. 별도 타입으로 추출하면 유지보수성이 향상됩니다.♻️ 제안 수정
+export type QuestionStatus = 'ACCEPTED' | 'ANSWER_COMPLETED'; + export interface QuestionListItemResponse { ... - status: 'ACCEPTED' | 'ANSWER_COMPLETED'; + status: QuestionStatus; ... } export interface QuestionDetailResponse { ... - status: 'ACCEPTED' | 'ANSWER_COMPLETED'; + status: QuestionStatus; ... }Also applies to: 66-66
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/api/question-api.ts` at line 33, Extract the duplicated literal union used for the status property into a single reusable type (e.g., declare type QuestionStatus = 'ACCEPTED' | 'ANSWER_COMPLETED' or an enum) and replace the inline declarations in both QuestionListItemResponse and QuestionDetailResponse so both use QuestionStatus for their status field; update any imports/exports as needed and run type checks to ensure no breaking changes.
122-133:createAnswer의 응답 타입이any
axiosInstance.post에 제네릭 타입 파라미터가 없어 반환 타입이 암묵적으로any가 됩니다. 응답 구조에 맞는 타입을 지정하거나 최소한unknown으로 선언하세요.♻️ 제안 수정
+export interface CreateAnswerResponse { + statusCode: number; + timestamp: string; + message: string; +} + export const createAnswer = async (...) => { - const { data } = await axiosInstance.post( + const { data } = await axiosInstance.post<CreateAnswerResponse>(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/study/group/api/question-api.ts` around lines 122 - 133, The createAnswer function currently returns an implicit any because axiosInstance.post lacks a generic response type; update createAnswer to specify the correct response type for axiosInstance.post (e.g., axiosInstance.post<CreateAnswerResponse>(...)) or at minimum use axiosInstance.post<unknown>(...) and update the function's return type accordingly so the function (createAnswer) no longer returns any and the response shape is typed.src/components/ui/image-upload-input.tsx (1)
74-74:className조합에cn()유틸리티 미사용As per coding guidelines, Tailwind 클래스 조합 시
clsx/tailwind-merge를 사용해야 합니다. 현재 템플릿 리터럴을 사용하면 충돌하는 클래스가 merge되지 않습니다.♻️ 제안 수정
+import { cn } from '@/lib/utils'; ... - className={`${inputStyles.base} ${isDragging ? inputStyles.dragging : inputStyles.notDragging}`} + className={cn(inputStyles.base, isDragging ? inputStyles.dragging : inputStyles.notDragging)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/image-upload-input.tsx` at line 74, The className currently uses a template literal which won't merge Tailwind classes; update the JSX in the ImageUploadInput component to use the project's cn() utility (or clsx/tailwind-merge wrapper) instead of the template string, passing inputStyles.base and the conditional isDragging ? inputStyles.dragging : inputStyles.notDragging so conflicting classes are properly merged; ensure cn is imported where used and replace the template literal expression that references inputStyles.base, isDragging, inputStyles.dragging, and inputStyles.notDragging.src/components/lists/inquiry-list-table.tsx (1)
85-88:className조합에cn()유틸리티 미사용테이블 행과 제목 셀에서 조건부 클래스를 템플릿 리터럴과 삼항 연산자로 조합하고 있습니다. 가이드라인에 따라
cn()(clsx + tailwind-merge)을 사용하세요.♻️ 제안 수정
+import { cn } from '@/lib/utils'; ... className={cn( 'border-border-default hover:bg-fill-neutral-subtle cursor-pointer border-b last:border-b-0', !item.accessible && 'opacity-60', )} ... className={cn(isHovered && 'text-text-brand underline')}As per coding guidelines: "Style components using Tailwind CSS 4 with
@tailwindcss/postcssplugin, clsx, tailwind-merge."Also applies to: 110-124
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/lists/inquiry-list-table.tsx` around lines 85 - 88, Replace the template-literal/ternary class composition used on the table row JSX (<tr key={item.questionId} ...>) and the title/header cell (around the other affected block) with the cn() utility (clsx + tailwind-merge) per guidelines: import the shared cn helper and build classes as cn('border-border-default hover:bg-fill-neutral-subtle cursor-pointer border-b last:border-b-0', { 'opacity-60': !item.accessible }) (and similarly convert the conditional classes in the title cell) so conditional and merged Tailwind classes use cn() instead of inline template literals.src/components/ui/badge/inquiry-status-badge.tsx (1)
21-21:?? STATUS_CONFIG.ACCEPTEDfallback은 도달 불가능한 코드
status의 타입이InquiryStatus이고STATUS_CONFIG가Record<InquiryStatus, ...>이므로 인덱스 접근 결과는 항상 정의됩니다. 해당 fallback은 죽은 코드(dead code)입니다.♻️ 제안 수정
- const config = STATUS_CONFIG[status] ?? STATUS_CONFIG.ACCEPTED; + const config = STATUS_CONFIG[status];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/badge/inquiry-status-badge.tsx` at line 21, The fallback "?? STATUS_CONFIG.ACCEPTED" is dead code because STATUS_CONFIG is a Record keyed by InquiryStatus and status is typed as InquiryStatus; remove the nullish coalescing fallback and directly use STATUS_CONFIG[status] (i.e. change the binding in the inquiry-status-badge component where const config is assigned) so the value is taken from STATUS_CONFIG without an unreachable default; if you were defensive about unexpected values instead, add an explicit type guard or switch on InquiryStatus to handle unknown cases rather than keeping the unreachable ?? fallback.src/app/(service)/inquiry/page.tsx (1)
10-10:PAGE_SIZE가 여러 파일에서 중복 정의되어 있습니다.
PAGE_SIZE = 15가 이 파일,inquiry-section.tsx,inquiry-list-table.tsx등에서 각각 정의되어 있습니다. 하나의 상수 파일(예:constants.ts)에서 export하여 공유하면 유지보수가 편해집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(service)/inquiry/page.tsx at line 10, PAGE_SIZE is duplicated across files (page.tsx, inquiry-section.tsx, inquiry-list-table.tsx); extract it into a single exported constant (e.g., export const PAGE_SIZE = 15 in a new constants.ts) and replace local definitions with imports from that module (update references in page.tsx, inquiry-section.tsx, inquiry-list-table.tsx to import { PAGE_SIZE } from './constants'). Ensure only the new constants export defines PAGE_SIZE and remove the other local declarations.src/components/section/inquiry-section.tsx (1)
246-301: 답변 섹션의MoreMenu(수정/삭제)가 모든 사용자에게 표시됩니다.
isLeader와isAdmin이 props로 전달되지만, 답변 카드의 MoreMenu 표시 여부를 제어하는 데 사용되지 않습니다. 현재 모든 사용자가 답변의 수정/삭제 메뉴를 볼 수 있습니다(현재 stub이지만). 질문 카드의 MoreMenu도 마찬가지입니다.기능 구현 시 권한 체크 없이 표시되면 혼란을 줄 수 있으므로, 최소한
isLeader || isAdmin조건으로 감싸는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/section/inquiry-section.tsx` around lines 246 - 301, The MoreMenu (the edit/delete menu) is currently rendered for all users; wrap its rendering with a permission check so only leaders or admins see it. Specifically, in the answer card render where MoreMenu is used (component MoreMenu, currently inside the block that shows data.answer) and likewise for the question card's MoreMenu, conditionally render MoreMenu only when (isLeader || isAdmin); keep the existing options/onMenuClick handlers unchanged so behavior is identical for permitted users.src/components/filtering/study-filter.tsx (1)
51-58:EXPERIENCE_LEVEL_OPTIONS의 값이group-study-const.ts의 목록과 동기화되어야 합니다.
src/features/study/group/const/group-study-const.ts에도EXPERIENCE_LEVEL_OPTIONS가 정의되어 있습니다. 현재 두 목록의 값은 일치하지만, 향후 한쪽만 수정될 위험이 있습니다. 공통 소스에서 값을 파생하면 불일치를 방지할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/filtering/study-filter.tsx` around lines 51 - 58, Replace the duplicated EXPERIENCE_LEVEL_OPTIONS array by deriving it from the single authoritative constant in group-study-const.ts: export the canonical experience-level enum/array from that module (e.g., the existing EXPERIENCE_LEVEL_OPTIONS or an EXPERIENCE_LEVELS/EXPERIENCE_LEVEL_ENUM) and import it into src/components/filtering/study-filter.tsx, then map/transform the canonical values to the local label shape if needed so the component uses the shared source of truth (reference symbols: EXPERIENCE_LEVEL_OPTIONS in this file and the canonical constant in group-study-const.ts).src/components/modals/question-modal.tsx (1)
97-98:imageExtension추출 방식이 일부 MIME 타입에서 예상과 다를 수 있습니다.
file.type.split('/')[1]로 확장자를 추출하면, 일반적인image/png,image/jpeg는 문제없지만,image/svg+xml같은 타입에서는svg+xml이 됩니다. 업로드 허용 타입이 제한되어 있다면 문제없지만, 서버 측에서 확장자 검증 시 불일치가 발생할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/modals/question-modal.tsx` around lines 97 - 98, In onSubmit, imageExtension extraction using imageFile.type.split('/')[1] can yield values like "svg+xml"; update the logic to derive the extension by taking the substring after the '/' then removing any suffix after a '+' (e.g., split on '+' and take the first segment), normalize to lowercase, and fallback to undefined if imageFile or type is missing; adjust the code around the imageFile and imageExtension variables to use this safer parsing so server-side extension checks receive "svg" not "svg+xml".src/hooks/common/use-study-list-filter.ts (1)
32-38:pageSize: 10000으로 전체 데이터를 가져오는 클라이언트 필터링 방식입니다.
experienceLevels필터가 서버 API에서 지원되지 않아 클라이언트에서 처리하는 것으로 이해됩니다. 현재 규모에서는 문제없지만, 스터디 수가 크게 증가하면 응답 시간과 메모리에 영향을 줄 수 있습니다. 서버 API에서experienceLevels파라미터를 지원하게 되면 다른 필터처럼 서버 사이드로 전환하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/common/use-study-list-filter.ts` around lines 32 - 38, The hook sets isClientFiltered (based on searchQuery or filterValues.experienceLevels) and then calls useGetStudies with pageSize: 10000 to fetch all items for client-side filtering, which will not scale; change this to avoid a huge one-time fetch by either (A) if the API can be extended, add experienceLevels to the useGetStudies call so filtering happens server-side (pass filterValues.experienceLevels into the request and remove the isClientFiltered branch), or (B) if server-side support is not yet available, implement incremental client-side pagination instead of pageSize: 10000 (fetch pages via useGetStudies using PAGE_SIZE and iterate/merge pages or load more on demand) and apply filterValues.experienceLevels in the client filter step that currently relies on isClientFiltered; update references to isClientFiltered, useGetStudies, pageSize, PAGE_SIZE, and experienceLevels accordingly.src/app/(service)/inquiry/[questionId]/page.tsx (1)
14-157:inquiry-section.tsx의DetailView와 UI/로직이 거의 동일합니다.이 페이지 컴포넌트와
src/components/section/inquiry-section.tsx의DetailView함수가 거의 같은 구조(헤더, 메타데이터 그리드, 본문+이미지, 답변 섹션)를 반복하고 있습니다. 차이점은 답변 헤더의 역할 표시(isAdmin/isPremium/isLeader)와 레이아웃 세부사항 정도입니다.공통
InquiryDetailCard같은 공유 컴포넌트를 추출하면 양쪽의 유지보수가 편해집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(service)/inquiry/[questionId]/page.tsx around lines 14 - 157, The page component duplicates the UI/logic found in DetailView (src/components/section/inquiry-section.tsx); extract a shared InquiryDetailCard component and use it from both places. Create InquiryDetailCard(props: { data, groupStudyId?, studyType?, moreMenuOptions?, roleFlags?: { isAdmin,isPremium,isLeader } }) and move the header, metadata grid (author/viewCount/createdAt/status), content+image rendering, and answer section into it; expose props to control the role/answerer display and the MoreMenu callbacks. Replace the duplicated markup in InquiryDetailPage (function InquiryDetailPage) and the DetailView function to render <InquiryDetailCard .../> with appropriate props (pass showToast-driven moreMenuOptions and any role flags) and remove the original duplicated JSX so both use the single component.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (46)
src/app/(service)/(my)/layout.tsxsrc/app/(service)/group-study/layout.tsxsrc/app/(service)/home/page.tsxsrc/app/(service)/inquiry/[questionId]/page.tsxsrc/app/(service)/inquiry/page.tsxsrc/app/(service)/layout.tsxsrc/app/(service)/premium-study/[id]/page.tsxsrc/app/(service)/premium-study/layout.tsxsrc/components/card/study-card.tsxsrc/components/filtering/study-filter.tsxsrc/components/lists/inquiry-list-table.tsxsrc/components/modals/question-modal.tsxsrc/components/pages/group-study-detail-page.tsxsrc/components/pages/premium-study-detail-page.tsxsrc/components/section/curriculum-summary-section.tsxsrc/components/section/group-study-info-section.tsxsrc/components/section/inquiry-section.tsxsrc/components/summary/study-info-summary.tsxsrc/components/ui/avatar-stack.tsxsrc/components/ui/badge/inquiry-status-badge.tsxsrc/components/ui/floating-inquiry-button.tsxsrc/components/ui/image-upload-input.tsxsrc/components/ui/study-active-ticker.tsxsrc/components/ui/study-card-countdown-badge.tsxsrc/config/constants.tssrc/features/auth/api/nickname-check.tssrc/features/auth/model/use-nickname-check.tssrc/features/my-page/api/update-user-profile.tssrc/features/my-page/model/use-update-user-profile-mutation.tssrc/features/my-page/ui/my-study-info-card.tsxsrc/features/phone-verification/api/phone-auth.tssrc/features/phone-verification/model/use-phone-auth-mutation.tssrc/features/study/group/api/create-inquiry.tssrc/features/study/group/api/group-study-types.tssrc/features/study/group/api/question-api.tssrc/features/study/group/model/inquiry.schema.tssrc/features/study/group/model/question.schema.tssrc/features/study/group/ui/group-study-form-modal.tsxsrc/features/study/group/ui/group-study-thumbnail-input.tsxsrc/features/study/group/ui/step/step2-group.tsxsrc/hooks/common/use-study-list-filter.tssrc/hooks/queries/inquiry-api.tssrc/hooks/queries/question-api.tssrc/hooks/use-now.tssrc/lib/countdown.tssrc/utils/time.ts
💤 Files with no reviewable changes (8)
- src/app/(service)/(my)/layout.tsx
- src/hooks/queries/inquiry-api.ts
- src/features/study/group/api/create-inquiry.ts
- src/features/auth/api/nickname-check.ts
- src/features/study/group/model/inquiry.schema.ts
- src/features/phone-verification/api/phone-auth.ts
- src/app/(service)/home/page.tsx
- src/features/my-page/api/update-user-profile.ts
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/components/modals/question-modal.tsx (1)
113-118:⚠️ Potential issue | 🟡 Minor이미지 업로드 실패 시 성공 토스트 동시 표시 문제 수정 확인 (이전 리뷰 반영)
에러 토스트 후
return이 추가되어 이전 리뷰에서 지적된 문제가 수정되었습니다. 다만 ESLint 정적 분석에서 Line 117의return앞에 빈 줄이 없다는 오류가 검출됩니다.🐛 제안 수정
} catch (error) { showToast('이미지 업로드 오류', 'error'); + return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/modals/question-modal.tsx` around lines 113 - 118, The catch block around the uploadImage call in question-modal.tsx (the try { await uploadImage(result.imageUploadUrl, imageFile); } catch (error) { showToast('이미지 업로드 오류', 'error'); return; }) needs a blank line before the return to satisfy ESLint; update the catch block that handles uploadImage/imageFile to insert an empty line above the return statement so the linter no longer flags the missing blank line.src/hooks/queries/question-api.ts (1)
42-51:staleTime추가 확인 (이전 리뷰 반영)
useGetQuestions,useGetQuestion모두staleTime: 60 * 1000이 올바르게 추가되었습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/queries/question-api.ts` around lines 42 - 51, The review confirms that staleTime: 60 * 1000 was correctly added to both hooks; verify that useGetQuestions (queryKey ['questions', groupStudyId, page, pageSize] / queryFn calling getQuestions and returning data.content) and useGetQuestion contain the same staleTime setting and, if present, approve/leave as-is — no code changes required.src/app/(service)/inquiry/[questionId]/page.tsx (1)
47-55: 잘못된 접근 처리 추가 확인 (이전 리뷰 반영)
groupStudyId가 없을 때 빈 화면 대신 명시적 에러 메시지를 렌더링하도록 개선한 점 확인했습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(service)/inquiry/[questionId]/page.tsx around lines 47 - 55, When groupStudyId is falsy render an explicit error UI instead of leaving a blank screen: ensure the early return using the groupStudyId check returns the JSX block that shows the error message (the existing return (...) JSX with the container div and the "잘못된 접근입니다. 스터디 문의 목록에서 다시 접근해주세요." text), so keep the groupStudyId guard and its returned markup in page.tsx (preserve the container classes and message) to handle invalid access cases.
🧹 Nitpick comments (4)
src/lib/countdown.ts (1)
1-28:URGENT_DAYS_THRESHOLD와 스테이지 기준의 중복 정의를 단일 소스로 정리하는 것을 권장합니다.현재 임계값이 상수와
COUNTDOWN_STAGE_CONFIG에 중복되어 있어, 한쪽만 수정되면 스테이지 매칭이 깨질 수 있습니다.♻️ 제안 diff
-export const URGENT_DAYS_THRESHOLD = 3; - export const COUNTDOWN_STAGE_CONFIG = [ { minDays: 3, @@ }, ] as const; + +export const URGENT_DAYS_THRESHOLD = Math.max( + ...COUNTDOWN_STAGE_CONFIG.map((stage) => stage.maxDays), +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/countdown.ts` around lines 1 - 28, The URGENT_DAYS_THRESHOLD constant is duplicated with COUNTDOWN_STAGE_CONFIG; make COUNTDOWN_STAGE_CONFIG the single source of truth and derive URGENT_DAYS_THRESHOLD from it (or vice versa). Locate URGENT_DAYS_THRESHOLD and COUNTDOWN_STAGE_CONFIG, remove the hardcoded numeric literal, and compute the threshold programmatically from the config (e.g., take the maximum minDays or appropriate stage boundary) so updates to COUNTDOWN_STAGE_CONFIG automatically update URGENT_DAYS_THRESHOLD and prevent drift.src/hooks/queries/question-api.ts (1)
88-94: 독립적인invalidateQueries호출을 병렬로 실행 가능두
invalidateQueries호출은 서로 의존성이 없으므로Promise.all로 병렬 실행하면 불필요한 직렬 대기를 제거할 수 있습니다.♻️ 제안 수정
- await queryClient.invalidateQueries({ - queryKey: ['question', variables.groupStudyId, variables.questionId], - }); - await queryClient.invalidateQueries({ - queryKey: ['questions', variables.groupStudyId], - }); + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: ['question', variables.groupStudyId, variables.questionId], + }), + queryClient.invalidateQueries({ + queryKey: ['questions', variables.groupStudyId], + }), + ]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/queries/question-api.ts` around lines 88 - 94, The onSuccess handler currently awaits two independent queryClient.invalidateQueries calls sequentially; change this to run them in parallel by calling Promise.all with both queryClient.invalidateQueries invocations (use the same query keys: ['question', variables.groupStudyId, variables.questionId] and ['questions', variables.groupStudyId]) so they execute concurrently and remove unnecessary serial waiting inside the onSuccess async function.src/app/(service)/inquiry/[questionId]/page.tsx (1)
19-19: 클라이언트 컴포넌트에서useParams()사용 권장클라이언트 컴포넌트에서 동적 라우트 파라미터에 접근할 때는
use(params)대신useParams()훅을 사용하는 것이 Next.js의 권장 방식입니다.'use client'페이지이므로use(params)패턴보다useParams()훅이 더 간결합니다.♻️ 제안 수정
-import { useRouter, useSearchParams } from 'next/navigation'; -import { use } from 'react'; +import { useParams, useRouter, useSearchParams } from 'next/navigation'; ... export default function InquiryDetailPage({ - params, -}: { - params: Promise<{ questionId: string }>; -}) { - const { questionId: questionIdStr } = use(params); +}: {}) { + const { questionId: questionIdStr } = useParams<{ questionId: string }>();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(service)/inquiry/[questionId]/page.tsx at line 19, Replace the use(params) pattern with Next.js's useParams() hook: import useParams from 'next/navigation', call useParams() inside the client component to retrieve params, and update the extraction of questionIdStr to use the returned object (replace the use(params) call and any reference to params with the value from useParams()). Ensure the file imports useParams and that the variable name questionIdStr remains assigned from the hook's result.src/components/modals/question-modal.tsx (1)
97-99: MIME 타입에서 확장자 추출 시 복합 서브타입 처리 개선 권장
file.type.split('/')[1]로 확장자를 추출하면image/svg+xml같은 복합 MIME 타입의 경우'svg+xml'이 반환됩니다.ImageUploadInput이accept="image/*"를 허용하므로 SVG도 선택 가능하며, 이 값을 직접 사용할 경우 일관성 문제가 발생할 수 있습니다.파일명에서 확장자를 추출하는 방식으로 정규화하면 더 안정적입니다. 이는 같은 프로젝트의
sign-up-modal.tsx에서도 사용 중인 패턴입니다.♻️ 제안 수정
- const imageExtension = imageFile ? imageFile.type.split('/')[1] : undefined; + const imageExtension = imageFile + ? imageFile.name.split('.').pop()?.toLowerCase() + : undefined;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/modals/question-modal.tsx` around lines 97 - 99, onSubmit currently derives the image extension from imageFile.type.split('/')[1], which yields composite values like "svg+xml"; instead, extract and normalize the extension from the file name first (e.g., take imageFile.name.split('.').pop()?.toLowerCase()), and only if that is missing fallback to the MIME subtype trimmed at '+' (e.g., imageFile.type.split('/')[1]?.split('+')[0]); update onSubmit (and any related handling used by ImageUploadInput) to use this normalized extension pattern consistent with the sign-up-modal.tsx approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`(service)/inquiry/[questionId]/page.tsx:
- Around line 34-45: The toast variant used for the placeholder menu actions in
moreMenuOptions is semantically wrong—replace the 'error' variant in the
onMenuClick handlers with a neutral/info variant (e.g., 'info' or 'neutral') so
that showToast calls for the '수정하기' and '삭제하기' menu items reflect an
informational placeholder rather than an error; update the two onMenuClick
lambdas inside the moreMenuOptions array to call showToast(..., 'info') (or your
project's neutral variant) instead of 'error'.
- Line 77: The component currently only checks data to render but misses the
case where isLoading is false and data is undefined, producing a blank screen;
update the useGetQuestion hook call to also destructure isError (e.g., const {
data, isLoading, isError } = useGetQuestion(...)) and change the render logic so
that when isLoading is false and (isError || !data) you render a clear
empty/error state (message + back button) instead of nothing; ensure the
existing back button (or navigation handler) is reused so the UI handles API
errors or disabled queries explicitly.
In `@src/components/modals/question-modal.tsx`:
- Around line 25-31: The labels are inconsistent for
QuestionCategory.STUDY_COMMON between QUESTION_CATEGORY_OPTIONS (label '스터디 공통')
and CATEGORY_LABEL ('스터디 일반'); pick CATEGORY_LABEL as the single source of truth
and update QUESTION_CATEGORY_OPTIONS to use that canonical label (or import/use
CATEGORY_LABEL mapping) so the option for QuestionCategory.STUDY_COMMON matches
what InquiryDetailPage renders, ensuring QUESTION_CATEGORY_OPTIONS and
CATEGORY_LABEL remain synchronized.
- Around line 51-54: The Object URL created via URL.createObjectURL is only
revoked in resetImageState and can leak if the component unmounts or
imagePreview is replaced; add a useEffect that watches imagePreview to revoke
the previous URL on change and also returns a cleanup to revoke the current
imagePreview on component unmount, and ensure any code that sets a new preview
(the place calling setImagePreview after URL.createObjectURL) revokes the prior
URL first; reference the imagePreview state, setImagePreview, and
resetImageState to implement this cleanup.
---
Duplicate comments:
In `@src/app/`(service)/inquiry/[questionId]/page.tsx:
- Around line 47-55: When groupStudyId is falsy render an explicit error UI
instead of leaving a blank screen: ensure the early return using the
groupStudyId check returns the JSX block that shows the error message (the
existing return (...) JSX with the container div and the "잘못된 접근입니다. 스터디 문의 목록에서
다시 접근해주세요." text), so keep the groupStudyId guard and its returned markup in
page.tsx (preserve the container classes and message) to handle invalid access
cases.
In `@src/components/modals/question-modal.tsx`:
- Around line 113-118: The catch block around the uploadImage call in
question-modal.tsx (the try { await uploadImage(result.imageUploadUrl,
imageFile); } catch (error) { showToast('이미지 업로드 오류', 'error'); return; }) needs
a blank line before the return to satisfy ESLint; update the catch block that
handles uploadImage/imageFile to insert an empty line above the return statement
so the linter no longer flags the missing blank line.
In `@src/hooks/queries/question-api.ts`:
- Around line 42-51: The review confirms that staleTime: 60 * 1000 was correctly
added to both hooks; verify that useGetQuestions (queryKey ['questions',
groupStudyId, page, pageSize] / queryFn calling getQuestions and returning
data.content) and useGetQuestion contain the same staleTime setting and, if
present, approve/leave as-is — no code changes required.
---
Nitpick comments:
In `@src/app/`(service)/inquiry/[questionId]/page.tsx:
- Line 19: Replace the use(params) pattern with Next.js's useParams() hook:
import useParams from 'next/navigation', call useParams() inside the client
component to retrieve params, and update the extraction of questionIdStr to use
the returned object (replace the use(params) call and any reference to params
with the value from useParams()). Ensure the file imports useParams and that the
variable name questionIdStr remains assigned from the hook's result.
In `@src/components/modals/question-modal.tsx`:
- Around line 97-99: onSubmit currently derives the image extension from
imageFile.type.split('/')[1], which yields composite values like "svg+xml";
instead, extract and normalize the extension from the file name first (e.g.,
take imageFile.name.split('.').pop()?.toLowerCase()), and only if that is
missing fallback to the MIME subtype trimmed at '+' (e.g.,
imageFile.type.split('/')[1]?.split('+')[0]); update onSubmit (and any related
handling used by ImageUploadInput) to use this normalized extension pattern
consistent with the sign-up-modal.tsx approach.
In `@src/hooks/queries/question-api.ts`:
- Around line 88-94: The onSuccess handler currently awaits two independent
queryClient.invalidateQueries calls sequentially; change this to run them in
parallel by calling Promise.all with both queryClient.invalidateQueries
invocations (use the same query keys: ['question', variables.groupStudyId,
variables.questionId] and ['questions', variables.groupStudyId]) so they execute
concurrently and remove unnecessary serial waiting inside the onSuccess async
function.
In `@src/lib/countdown.ts`:
- Around line 1-28: The URGENT_DAYS_THRESHOLD constant is duplicated with
COUNTDOWN_STAGE_CONFIG; make COUNTDOWN_STAGE_CONFIG the single source of truth
and derive URGENT_DAYS_THRESHOLD from it (or vice versa). Locate
URGENT_DAYS_THRESHOLD and COUNTDOWN_STAGE_CONFIG, remove the hardcoded numeric
literal, and compute the threshold programmatically from the config (e.g., take
the maximum minDays or appropriate stage boundary) so updates to
COUNTDOWN_STAGE_CONFIG automatically update URGENT_DAYS_THRESHOLD and prevent
drift.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/app/(service)/inquiry/[questionId]/page.tsxsrc/components/card/study-card.tsxsrc/components/modals/question-modal.tsxsrc/components/pages/group-study-detail-page.tsxsrc/components/ui/image-upload-input.tsxsrc/components/ui/study-active-ticker.tsxsrc/hooks/queries/question-api.tssrc/lib/countdown.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/components/ui/image-upload-input.tsx
- src/components/card/study-card.tsx
- src/components/pages/group-study-detail-page.tsx
- src/components/ui/study-active-ticker.tsx
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/ui/toast.tsx (1)
55-77:⚠️ Potential issue | 🟡 Minor
infovariant에서 에러 아이콘(XCircle)이 렌더링됩니다.
isError변수가 보더 색상에만 사용되고 아이콘 렌더링 분기에는 반영되지 않아,infovariant에서도 빨간색XCircle이 표시됩니다. 파란 보더와 빨간 에러 아이콘이 함께 표시되어 시각적으로 모순됩니다.
lucide-react에 포함된Info아이콘을 활용하여 분기를 수정하는 것을 권장합니다.🐛 제안 수정
-import { CheckCircle2, XCircle } from 'lucide-react'; +import { CheckCircle2, Info, XCircle } from 'lucide-react'; ... {isSuccess ? ( <CheckCircle2 className="h-5 w-5 shrink-0 text-green-600" /> + ) : isError ? ( + <XCircle className="h-5 w-5 shrink-0 text-red-600" /> ) : ( - <XCircle className="h-5 w-5 shrink-0 text-red-600" /> + <Info className="h-5 w-5 shrink-0 text-blue-500" /> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/ui/toast.tsx` around lines 55 - 77, The icon branch incorrectly only checks isSuccess so XCircle renders for both error and info variants; update the rendering logic in the toast component (the JSX that uses isSuccess, isError and renders CheckCircle2/XCircle) to select icons based on variant: render CheckCircle2 when isSuccess, XCircle when isError, and the lucide-react Info icon for the info/neutral case (ensure you import Info and use the same className as the other icons). Also verify the isError and isSuccess booleans are computed from variant so the border and icon branches stay consistent.
♻️ Duplicate comments (1)
src/components/modals/question-modal.tsx (1)
51-77:⚠️ Potential issue | 🟡 Minor컴포넌트 언마운트 시 Object URL 메모리 누수 (미해결)
handleChangeImage와resetImageState에서는 명시적 교체·초기화 시URL.revokeObjectURL을 호출하지만, 모달이 열린 상태에서 부모 컴포넌트가 언마운트될 경우handleOpenChange가 호출되지 않아imagePreviewURL이 해제되지 않습니다.🛡️ 제안 수정
+import { useState, useEffect } from 'react'; ... + useEffect(() => { + return () => { + if (imagePreview) URL.revokeObjectURL(imagePreview); + }; + }, [imagePreview]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/modals/question-modal.tsx` around lines 51 - 77, The image preview URL is not revoked on component unmount, causing a memory leak; update the component to revoke the object URL in a cleanup effect: add a useEffect that watches imagePreview (or imageFile) and on cleanup (and when imagePreview changes) calls URL.revokeObjectURL(imagePreview) if present, and ensure resetImageState/handleChangeImage still revoke before replacing; reference the existing handleChangeImage (and any resetImageState) to keep their revoke logic but add the useEffect cleanup to guarantee revocation when the modal/component unmounts.
🧹 Nitpick comments (1)
src/app/(service)/inquiry/[questionId]/page.tsx (1)
50-76: 에러/로딩 guard LGTM — 단, 에러 화면에 뒤로가기 버튼 추가를 고려하세요.
groupStudyId누락 시 에러 메시지를 표시하는 현재 방식은 기능적으로 문제없습니다. 다만 에러/오류 상태 화면(Lines 51–75)에는 뒤로가기 버튼이 없어, 사용자가 직접 브라우저 뒤로가기에 의존해야 합니다. 아래 핸들러(handleBack)가 이미 정의되어 있으므로 재사용할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(service)/inquiry/[questionId]/page.tsx around lines 50 - 76, Add a visible "Back" button to the error/loading guard UI by reusing the existing handleBack handler: update the three early-return blocks (the groupStudyId-missing message, the isLoading block if desired, and the isError/!data block) to include a button element that calls handleBack on click and uses the same styling pattern as other buttons in this component; ensure the button is keyboard-accessible and placed next to the existing message so users can navigate back without relying on the browser back control.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/components/ui/toast.tsx`:
- Around line 55-77: The icon branch incorrectly only checks isSuccess so
XCircle renders for both error and info variants; update the rendering logic in
the toast component (the JSX that uses isSuccess, isError and renders
CheckCircle2/XCircle) to select icons based on variant: render CheckCircle2 when
isSuccess, XCircle when isError, and the lucide-react Info icon for the
info/neutral case (ensure you import Info and use the same className as the
other icons). Also verify the isError and isSuccess booleans are computed from
variant so the border and icon branches stay consistent.
---
Duplicate comments:
In `@src/components/modals/question-modal.tsx`:
- Around line 51-77: The image preview URL is not revoked on component unmount,
causing a memory leak; update the component to revoke the object URL in a
cleanup effect: add a useEffect that watches imagePreview (or imageFile) and on
cleanup (and when imagePreview changes) calls URL.revokeObjectURL(imagePreview)
if present, and ensure resetImageState/handleChangeImage still revoke before
replacing; reference the existing handleChangeImage (and any resetImageState) to
keep their revoke logic but add the useEffect cleanup to guarantee revocation
when the modal/component unmounts.
---
Nitpick comments:
In `@src/app/`(service)/inquiry/[questionId]/page.tsx:
- Around line 50-76: Add a visible "Back" button to the error/loading guard UI
by reusing the existing handleBack handler: update the three early-return blocks
(the groupStudyId-missing message, the isLoading block if desired, and the
isError/!data block) to include a button element that calls handleBack on click
and uses the same styling pattern as other buttons in this component; ensure the
button is keyboard-accessible and placed next to the existing message so users
can navigate back without relying on the browser back control.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/app/(service)/inquiry/[questionId]/page.tsxsrc/components/modals/question-modal.tsxsrc/components/section/inquiry-section.tsxsrc/components/ui/dropdown/more-menu.tsxsrc/components/ui/toast.tsxsrc/stores/use-toast-store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/section/inquiry-section.tsx
🌱 연관된 이슈
☘️ 작업 내용
문의(Question) 게시판 시스템 구축
/inquiry) 및 상세(/inquiry/[questionId]) 페이지 신규 추가InquirySection) 통합InquiryStatusBadge— 답변 대기/완료 상태 뱃지useGetQuestions,useGetQuestionDetail,useCreateQuestion,useCreateAnswer)스터디 카드 및 목록 UX 개선
StudyCardCountdownBadge— 모집 마감까지 남은 시간 카운트다운 뱃지StudyActiveTicker— 스터디 상세 실시간 전광판스터디 상세 페이지 보강
CurriculumSummarySection— 커리큘럼 요약 섹션 추가코드 품질 개선
axios→axiosV2부분 마이그레이션alert()→ toast 알림으로 대체, 토스트 표시 타이밍 수정GlobalToast중복 제거 — 개별 레이아웃에서 서비스 루트 레이아웃으로 통합ImageUploadInput공통 컴포넌트 추출 (문의 모달, 스터디 폼 공용)🍀 참고사항
스크린샷 (선택)
Summary by CodeRabbit
New Features
Refactor