Skip to content

Comments

스터디 문의(기능 추가) 및 스터디 목록/상세 UI 개선#401

Merged
HA-SEUNG-JEONG merged 37 commits intodevelopfrom
fix/study
Feb 25, 2026
Merged

스터디 문의(기능 추가) 및 스터디 목록/상세 UI 개선#401
HA-SEUNG-JEONG merged 37 commits intodevelopfrom
fix/study

Conversation

@HA-SEUNG-JEONG
Copy link
Contributor

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented Feb 25, 2026

🌱 연관된 이슈

☘️ 작업 내용

문의(Question) 게시판 시스템 구축

  • 문의 목록(/inquiry) 및 상세(/inquiry/[questionId]) 페이지 신규 추가
  • 그룹/프리미엄 스터디 상세 내 문의 탭(InquirySection) 통합
  • 문의 작성 모달 — 이미지 첨부 지원 (inquiry-modal → QuestionModal 리네이밍)
  • InquiryStatusBadge — 답변 대기/완료 상태 뱃지
  • Question API 레이어 및 TanStack Query 훅 (useGetQuestions, useGetQuestionDetail, useCreateQuestion, useCreateAnswer)
  • 미신청자에게 문의하기 버튼 숨김 처리 (403 에러 방지)

스터디 카드 및 목록 UX 개선

  • StudyCardCountdownBadge — 모집 마감까지 남은 시간 카운트다운 뱃지
  • StudyActiveTicker — 스터디 상세 실시간 전광판
  • 경험 레벨 필터 추가

스터디 상세 페이지 보강

  • CurriculumSummarySection — 커리큘럼 요약 섹션 추가
  • 참가자 목록 UI 개선 (왕관 아이콘, 팝오버 브라우저 잘림 방지)
  • 종료된 스터디 UI 처리 변경
  • 신청하기 버튼 첫 렌더 오표시 수정

코드 품질 개선

  • axiosaxiosV2 부분 마이그레이션
  • alert() → toast 알림으로 대체, 토스트 표시 타이밍 수정
  • GlobalToast 중복 제거 — 개별 레이아웃에서 서비스 루트 레이아웃으로 통합
  • ImageUploadInput 공통 컴포넌트 추출 (문의 모달, 스터디 폼 공용)

🍀 참고사항

스크린샷 (선택)

Summary by CodeRabbit

  • New Features

    • 문의 게시판 도입 — 목록/상세 보기, 작성(이미지 첨부) 및 답변 기능.
    • 플로팅 문의 버튼과 문의 섹션/리스트/상세 UI 추가.
    • 교육과정 요약 카드, 모집 카운트 배지, 전광판(토글 메시지) 도입.
    • 경험 레벨 필터로 스터디 대상 필터링 가능.
    • 이미지 업로드 위젯 및 날짜/시간 형식 유틸, 실시간 시계 훅 추가.
  • Refactor

    • 전역 토스트 기반 알림 통합 및 UI 전반의 토스트 처리 정비.

HA-SEUNG-JEONG and others added 30 commits February 17, 2026 18:50
- useUserStore(Zustand persist) → useAuth로 교체하여 Zustand rehydration 타이밍 이슈 해결
- 첫 렌더부터 서버 토큰 기반으로 isLoggedIn/isLeader 판단하여 리더가 모집 마감 상태를 즉시 표시
- prefetchQuery queryKey 불일치 수정: 'groupStudyMyStatus' → 'groupStudyMemberStatus'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- inquiry -> question
- 스터디 신청자 승인 후 뒤로 가기를 눌러야 토스트가 뜨고 있던 문제 발견
- 승인 버튼 클릭 후 바로 토스트가 뜨도록 수정
- 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>
@HA-SEUNG-JEONG HA-SEUNG-JEONG self-assigned this Feb 25, 2026
@vercel
Copy link

vercel bot commented Feb 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment Feb 25, 2026 6:49am

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7de619 and 91b1ca9.

📒 Files selected for processing (1)
  • src/utils/time.ts

📝 Walkthrough

Walkthrough

문의(Question) 기능을 새로 도입하고 관련 API/스키마/훅/컴포넌트를 추가했으며, GlobalToast 렌더링을 메인 레이아웃으로 통합, 이미지 업로드·플로팅 버튼·틱커·카운트다운·경험 수준 필터 등을 추가하고 기존 Inquiry 파일들을 삭제 또는 리브랜딩했습니다.

Changes

Cohort / File(s) Summary
레이아웃 / 전역 토스트
src/app/(service)/layout.tsx, src/app/(service)/(my)/layout.tsx, src/app/(service)/group-study/layout.tsx, src/app/(service)/home/page.tsx, src/app/(service)/premium-study/layout.tsx
GlobalToast를 메인 layout에 통합하고 하위 레이아웃들에서 GlobalToast 렌더링을 제거/정리. FloatingInquiryButton을 메인 레이아웃에 추가.
문의 페이지(목록/상세)
src/app/(service)/inquiry/page.tsx, src/app/(service)/inquiry/[questionId]/page.tsx
문의 목록 및 상세 클라이언트 페이지 추가(쿼리파라미터 처리, 페이지네이션, 상세 이동, 로딩/에러/권한 처리 포함).
문의 섹션·리스트·모달
src/components/section/inquiry-section.tsx, src/components/lists/inquiry-list-table.tsx, src/components/modals/question-modal.tsx
InquirySection(목록/상세 전환) 추가, InquiryListTable 추가, InquiryModal → QuestionModal로 리브랜딩 및 이미지 업로드·폼·전송 로직 변경(이미지 업로드 처리 포함).
API, 훅, 스키마
src/features/study/group/api/question-api.ts, src/features/study/group/model/question.schema.ts, src/hooks/queries/question-api.ts, removed: src/features/study/group/api/create-inquiry.ts, src/features/study/group/model/inquiry.schema.ts, src/hooks/queries/inquiry-api.ts
그룹 스터디용 Question API/타입/스키마 도입 및 React-Query 훅 추가; 기존 Inquiry API/스키마/훅 삭제.
스터디 카드 / 배지 / 카운트다운
src/components/card/study-card.tsx, src/components/ui/study-card-countdown-badge.tsx, src/components/ui/badge/inquiry-status-badge.tsx
스터디 카드에 모집 카운트다운 배지와 경험 수준/타입 배지 추가; 신규 badge 컴포넌트 추가 및 배너 메시지 로직 추가.
UI 컴포넌트 추가
src/components/ui/image-upload-input.tsx, src/components/ui/floating-inquiry-button.tsx, src/components/ui/study-active-ticker.tsx
이미지 업로드 입력, 플로팅 문의 버튼(경로 매칭), 스터디 활성 전광판(틱커) 컴포넌트 추가.
필터·검색·리스트 변화
src/components/filtering/study-filter.tsx, src/hooks/common/use-study-list-filter.ts
experienceLevels 필드 및 옵션 추가, 클라이언트 측 필터링(경험 수준 포함) 도입 및 페이징/페이지네이션 처리 변경(대용량 fetch 경로 포함).
스터디 상세 통합
src/components/pages/group-study-detail-page.tsx, src/components/pages/premium-study-detail-page.tsx, src/components/section/group-study-info-section.tsx, src/config/constants.ts
스터디 상세에 '문의' 탭 추가 및 InquirySection 렌더링, 커리큘럼 요약 및 StudyActiveTicker 통합, 탭 상수 확장.
토스트·알림 전환 및 API 클라이언트 사용
여러 파일(예: src/features/study/group/ui/group-study-form-modal.tsx, src/features/auth/model/use-nickname-check.ts, src/features/phone-verification/model/use-phone-auth-mutation.ts, src/features/my-page/model/use-update-user-profile-mutation.ts)
alert() → toast 전환, 기존 API 래퍼 파일 제거 후 OpenAPI client 또는 axiosV2 직접 호출로 변경(관련 훅/모듈 업데이트).
유틸 및 카운트다운/시간
src/utils/time.ts, src/lib/countdown.ts, src/hooks/use-now.ts
날짜 포맷 유틸 추가, 카운트다운 상태 계산 로직 추가, 전역 단일 인터벌 기반 useNow 훅 추가.
삭제된 API 파일들
removed: src/features/auth/api/nickname-check.ts, src/features/my-page/api/update-user-profile.ts, src/features/phone-verification/api/phone-auth.ts, src/features/study/group/api/create-inquiry.ts
여러 기존 API 래퍼 파일 삭제(대체는 OpenAPI client 또는 새 API 모듈).

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: 성공 토스트 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 안녕! 문의창에 폴짝 뛰어들어,
배지와 카운트다운이 반짝이네,
그림 하나 첨부해 마음 전하고,
토스트로 소식 전하면 모두 웃음,
토끼도 껑충, 축하해요! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항들을 정확하게 반영하고 있습니다. '스터디 문의(기능 추가) 및 스터디 목록/상세 UI 개선'은 질의응답 시스템 추가와 UI 개선이라는 두 가지 핵심 변경 사항을 명확하게 요약합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/study

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)이 onChangeImage prop 타입 (File | undefined)과 불일치

ImageUploadInputonChangeImage prop은 (file: File | undefined) => void를 기대하지만, handleImageChangeFile | null을 파라미터로 받습니다. TypeScript strict 모드에서는 nullundefined에 할당되지 않아 타입 오류가 발생합니다.

🐛 제안 수정
-  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에 제네릭 타입 파라미터 추가를 고려해 주세요.

mutationFnres.content를 반환하지만, useMutation에 타입 파라미터가 없어서 onSuccessdataany로 추론됩니다. 또한 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.ts wrapper가 삭제되고, 모든 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: useUpdateUserProfileInfoMutationmutationKey 누락

useUpdateUserProfileMutationmutationKey: ['updateUserProfile', memberId]를 명시하지만, useUpdateUserProfileInfoMutationmutationKey가 없어 일관성이 깨집니다. 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 추가 권장

useStudyDashboardQuerystaleTime: 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의 부모 divcursor-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_CONFIGminDays/maxDays가 동일한 값이라 불필요한 복잡성이 있습니다.

각 stage의 minDaysmaxDays가 항상 같은 값입니다 (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을 명확히 하려면 setTimeout ID도 함께 정리하는 것이 더 견고합니다.

🤖 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 값이 StudyCardCountdownBadge prop(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/postcss plugin, 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 리터럴 유니온 타입이 두 인터페이스에 중복 선언

QuestionListItemResponseQuestionDetailResponse 모두 동일한 '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/postcss plugin, 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.ACCEPTED fallback은 도달 불가능한 코드

status의 타입이 InquiryStatus이고 STATUS_CONFIGRecord<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(수정/삭제)가 모든 사용자에게 표시됩니다.

isLeaderisAdmin이 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.tsxDetailView와 UI/로직이 거의 동일합니다.

이 페이지 컴포넌트와 src/components/section/inquiry-section.tsxDetailView 함수가 거의 같은 구조(헤더, 메타데이터 그리드, 본문+이미지, 답변 섹션)를 반복하고 있습니다. 차이점은 답변 헤더의 역할 표시(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

📥 Commits

Reviewing files that changed from the base of the PR and between f4002d2 and ccf330f.

📒 Files selected for processing (46)
  • src/app/(service)/(my)/layout.tsx
  • src/app/(service)/group-study/layout.tsx
  • src/app/(service)/home/page.tsx
  • src/app/(service)/inquiry/[questionId]/page.tsx
  • src/app/(service)/inquiry/page.tsx
  • src/app/(service)/layout.tsx
  • src/app/(service)/premium-study/[id]/page.tsx
  • src/app/(service)/premium-study/layout.tsx
  • src/components/card/study-card.tsx
  • src/components/filtering/study-filter.tsx
  • src/components/lists/inquiry-list-table.tsx
  • src/components/modals/question-modal.tsx
  • src/components/pages/group-study-detail-page.tsx
  • src/components/pages/premium-study-detail-page.tsx
  • src/components/section/curriculum-summary-section.tsx
  • src/components/section/group-study-info-section.tsx
  • src/components/section/inquiry-section.tsx
  • src/components/summary/study-info-summary.tsx
  • src/components/ui/avatar-stack.tsx
  • src/components/ui/badge/inquiry-status-badge.tsx
  • src/components/ui/floating-inquiry-button.tsx
  • src/components/ui/image-upload-input.tsx
  • src/components/ui/study-active-ticker.tsx
  • src/components/ui/study-card-countdown-badge.tsx
  • src/config/constants.ts
  • src/features/auth/api/nickname-check.ts
  • src/features/auth/model/use-nickname-check.ts
  • src/features/my-page/api/update-user-profile.ts
  • src/features/my-page/model/use-update-user-profile-mutation.ts
  • src/features/my-page/ui/my-study-info-card.tsx
  • src/features/phone-verification/api/phone-auth.ts
  • src/features/phone-verification/model/use-phone-auth-mutation.ts
  • src/features/study/group/api/create-inquiry.ts
  • src/features/study/group/api/group-study-types.ts
  • src/features/study/group/api/question-api.ts
  • src/features/study/group/model/inquiry.schema.ts
  • src/features/study/group/model/question.schema.ts
  • src/features/study/group/ui/group-study-form-modal.tsx
  • src/features/study/group/ui/group-study-thumbnail-input.tsx
  • src/features/study/group/ui/step/step2-group.tsx
  • src/hooks/common/use-study-list-filter.ts
  • src/hooks/queries/inquiry-api.ts
  • src/hooks/queries/question-api.ts
  • src/hooks/use-now.ts
  • src/lib/countdown.ts
  • src/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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'이 반환됩니다. ImageUploadInputaccept="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

📥 Commits

Reviewing files that changed from the base of the PR and between ccf330f and 664ca1f.

📒 Files selected for processing (8)
  • src/app/(service)/inquiry/[questionId]/page.tsx
  • src/components/card/study-card.tsx
  • src/components/modals/question-modal.tsx
  • src/components/pages/group-study-detail-page.tsx
  • src/components/ui/image-upload-input.tsx
  • src/components/ui/study-active-ticker.tsx
  • src/hooks/queries/question-api.ts
  • src/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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

info variant에서 에러 아이콘(XCircle)이 렌더링됩니다.

isError 변수가 보더 색상에만 사용되고 아이콘 렌더링 분기에는 반영되지 않아, info variant에서도 빨간색 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 메모리 누수 (미해결)

handleChangeImageresetImageState에서는 명시적 교체·초기화 시 URL.revokeObjectURL을 호출하지만, 모달이 열린 상태에서 부모 컴포넌트가 언마운트될 경우 handleOpenChange가 호출되지 않아 imagePreview URL이 해제되지 않습니다.

🛡️ 제안 수정
+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

📥 Commits

Reviewing files that changed from the base of the PR and between 664ca1f and d7de619.

📒 Files selected for processing (6)
  • src/app/(service)/inquiry/[questionId]/page.tsx
  • src/components/modals/question-modal.tsx
  • src/components/section/inquiry-section.tsx
  • src/components/ui/dropdown/more-menu.tsx
  • src/components/ui/toast.tsx
  • src/stores/use-toast-store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/section/inquiry-section.tsx

@HA-SEUNG-JEONG HA-SEUNG-JEONG merged commit 915ed57 into develop Feb 25, 2026
7 of 9 checks passed
@HA-SEUNG-JEONG HA-SEUNG-JEONG deleted the fix/study branch February 25, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant