Conversation
- 약속 승인 대기/완료 리스트 조회 및 페이지네이션 기능 - 약속 승인, 거부, 삭제 기능 구현 - TanStack Query를 활용한 서버 상태 관리 및 캐시 최적화 - 로그인 기능 개발 전까지 목데이터로 테스트 가능하도록 구현
브라우저 네이티브 alert/confirm 대신 UI 디자인 일관성을 위한 전역 모달 시스템 구현. 주요 변경사항: - Zustand 기반 globalModalStore 추가 (openConfirm API 제공) - ConfirmModalHost 컴포넌트로 전역 모달 렌더링 처리 - 약속 승인/거절/삭제 액션에 모달 적용 (MeetingApprovalItem) - Promise 기반 비동기 confirm으로 사용성 개선 기술 스택: - Zustand v5.0.10 상태 관리 라이브러리 추가
기존 confirm 전용 모달을 3가지 타입(alert, error, confirm)을 지원하는 전역 모달 시스템으로 확장. - globalModalStore에 openAlert, openError 함수 추가 - 모든 모달 타입에서 제목과 내용 커스텀 가능 - ConfirmModalHost를 GlobalModalHost로 리네이밍 - 모달 타입에 따라 footer variant 자동 전환 (confirm: double, alert/error: full)
Walkthrough미팅 승인 관리 기능을 추가합니다. 전역 모달 스토어와 호스트, 미팅 승인 API/목데이터, React Query 훅(조회/뮤테이션), 승인 항목·목록 컴포넌트, 미팅 설정 페이지 및 라우트, 관련 타입/유틸, Zustand 의존성 추가가 포함됩니다. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Item as MeetingApprovalItem
participant Modal as GlobalModalStore
participant Mutation as MutationHook
participant API as API
participant QueryClient as QueryClient
User->>Item: 수락/거절/삭제 클릭
Item->>Modal: openConfirm(title, desc)
activate Modal
Modal-->>User: 모달 표시
deactivate Modal
User->>Modal: 확인/취소 클릭
activate Modal
Modal-->>Item: Promise<boolean> (true/false)
deactivate Modal
alt confirmed
Item->>Mutation: execute(meetingId)
activate Mutation
Mutation->>API: POST/DELETE 요청
activate API
API-->>Mutation: 응답
deactivate API
Mutation-->>QueryClient: invalidate meetingQueryKeys.approvals()
deactivate Mutation
end
QueryClient-->>Item: 캐시 갱신 -> UI 업데이트
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 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: 5
🤖 Fix all issues with AI agents
In `@src/features/meetings/components/MeetingApprovalList.tsx`:
- Around line 20-29: The MeetingApprovalList component keeps currentPage state
across prop changes, causing empty results when switching tabs; update the
component (MeetingApprovalList) to reset currentPage to 0 whenever status or
gatheringId changes by adding an effect that watches [status, gatheringId] and
calls setCurrentPage(0); ensure this effect runs only on those prop changes and
does not interfere with existing pagination logic or the useMeetingApprovals
call.
In `@src/features/meetings/hooks/useConfirmMeeting.ts`:
- Around line 27-38: The mutations useConfirmMeeting, useRejectMeeting, and
useDeleteMeeting lack onError handling so failures never surface to users; add
an onError handler to each hook that receives the error and shows a user-facing
toast (or notification) and optionally logs the error, and/or ensure callers
like MeetingApprovalItem pass an onError callback to mutate; specifically update
useConfirmMeeting/useRejectMeeting/useDeleteMeeting to accept or define onError
in their useMutation config (using queryClient.invalidateQueries on success
remains) and/or update MeetingApprovalItem where mutate is called to provide an
onError callback that displays the toast and handles rollback UI state.
In `@src/features/meetings/hooks/useMeetingApprovalsCount.ts`:
- Line 34: The enabled check for useMeetingApprovalsCount is using !!gatheringId
which treats 0 as falsy and makes intent ambiguous; update the enabled condition
in useMeetingApprovalsCount to explicitly validate the id (e.g., use gatheringId
> 0) or otherwise assert/annotate that 0 is invalid so the hook clearly requires
a positive gatheringId; locate the enabled option in the call that references
gatheringId inside useMeetingApprovalsCount and replace the boolean coercion
with the explicit numeric check.
In `@src/features/meetings/meetings.api.ts`:
- Around line 17-23: The constant USE_MOCK_DATA is hardcoded to true; change it
to read from an environment variable and default to false so mocks are opt-in.
Replace the fixed declaration for USE_MOCK_DATA with code that parses a process
environment variable (e.g. process.env.USE_MOCK_DATA) into a boolean and falls
back to false, and update any call sites that rely on USE_MOCK_DATA (the
constant in this module) so behavior is driven by the env var.
In `@src/pages/Meetings/MeetingSettingPage.tsx`:
- Around line 13-14: The code converts useParams id to a number via const
gatheringId = Number(id) but doesn't handle NaN; add validation immediately
after computing gatheringId (e.g., inside MeetingSettingPage or the component
that uses it) to check Number.isFinite(gatheringId) && gatheringId > 0 and
perform an early return/redirect/render an error UI when invalid so downstream
API calls or hooks that rely on gatheringId are not invoked with NaN; update any
usage sites (hooks, effects, API calls) that assume a valid gatheringId to
depend on this validated guard.
🧹 Nitpick comments (7)
src/features/meetings/hooks/useMeetingApprovalsCount.ts (1)
36-64:staleTime미설정으로 불필요한 refetch 발생 가능카운트 데이터는 자주 변경되지 않으므로
gcTime과 함께staleTime도 설정하면 탭 전환 시 불필요한 네트워크 요청을 줄일 수 있습니다.♻️ staleTime 추가 제안
{ queryKey: meetingQueryKeys.approvalCount(gatheringId, 'PENDING'), queryFn: () => getMeetingApprovals({ gatheringId, status: 'PENDING', page: 0, size: 1, }), enabled: !!gatheringId, select: (data: PaginatedResponse<MeetingApprovalItem>) => data.totalCount, + staleTime: 30 * 1000, // 30초 gcTime: 10 * 60 * 1000, }, { queryKey: meetingQueryKeys.approvalCount(gatheringId, 'CONFIRMED'), queryFn: () => getMeetingApprovals({ gatheringId, status: 'CONFIRMED', page: 0, size: 1, }), enabled: !!gatheringId, select: (data: PaginatedResponse<MeetingApprovalItem>) => data.totalCount, + staleTime: 30 * 1000, // 30초 gcTime: 10 * 60 * 1000, },src/features/meetings/lib/formatDateTime.ts (1)
4-7: 잘못된 날짜 문자열에 대한 방어 코드 부재
new Date()가 유효하지 않은 문자열을 받으면Invalid Date가 되고,format()은 예외 없이"Invalid Date"문자열을 반환합니다. API 응답이 항상 유효하다면 괜찮지만, 방어 코드를 추가하면 더 안전합니다.♻️ 유효성 검사 추가 제안
export default function formatDateTime(dateTimeString: string): string { const date = new Date(dateTimeString) + if (isNaN(date.getTime())) { + return '-' + } return format(date, 'yy.MM.dd(eee) HH:mm', { locale: ko }) }src/routes/index.tsx (1)
16-16: import 방식 불일치다른 페이지들은
@/pages배럴 export를 사용하는데,MeetingSettingPage만 직접 경로 import를 사용함.→ 배럴 export에 추가하거나, 의도적 분리라면 주석으로 사유를 명시하는 것이 좋음.
src/features/meetings/hooks/useDeleteMeeting.ts (1)
8-9: import 스타일 불일치
useRejectMeeting에서는import type { ApiError, ApiResponse } from '@/api'를 사용하는데, 여기서는 각각 다른 경로에서 import함.→ 동일 feature 내 hooks는 import 스타일을 통일하는 것이 유지보수에 유리함.
제안
-import { ApiError } from '@/api/errors' -import type { ApiResponse } from '@/api/types' +import type { ApiError, ApiResponse } from '@/api'src/shared/ui/GlobalModalHost.tsx (1)
63-81: inline 타입 대신ModalButton타입 사용 권장store에 이미
ModalButton타입이 정의되어 있음. inline 타입은 중복이고 동기화 문제 발생 가능.제안
+import { useGlobalModalStore, type ModalButton } from '@/shared/stores/globalModalStore' -import { useGlobalModalStore } from '@/shared/stores/globalModalStore'- {buttons.map( - ( - button: { - text: string - variant?: 'primary' | 'secondary' | 'danger' - onClick?: () => void - }, - index: number - ) => ( + {buttons.map((button: ModalButton, index: number) => ( <Button key={index} variant={button.variant === 'secondary' ? 'secondary' : button.variant || 'primary'} outline={button.variant === 'secondary'} onClick={button.onClick} > {button.text} </Button> - ) - )} + ))}src/shared/stores/globalModalStore.ts (1)
85-120: 모달 중복 호출 시 이전 Promise가 resolve되지 않는 문제
openConfirm이 이미 열린 상태에서 다시 호출되면, 이전 Promise가 resolve/reject 없이 남아있게 됩니다.→ 메모리 누수나 예기치 않은 동작 가능성
→ 모달 열기 전 기존 모달 상태 확인 또는 이전 Promise reject 처리 권장♻️ 제안
openConfirm: ( title: string, description: string, options: { confirmText?: string; variant?: 'primary' | 'danger' } = {} ) => { + // 이미 열린 모달이 있으면 먼저 닫기 + if (get().isOpen) { + get().close() + } + return new Promise<boolean>((resolve) => {src/features/meetings/components/MeetingApprovalItem.tsx (1)
34-57: 뮤테이션 실패 시 사용자 피드백 및 버튼 비활성화 누락뮤테이션 진행 중 버튼 중복 클릭 방지와 실패 시 사용자 알림이 없습니다.
→ 중복 요청 발생 가능, 실패 시 사용자가 결과를 알 수 없음
→isPending상태로 버튼 비활성화 및onError콜백으로 에러 모달 표시 권장♻️ 버튼 비활성화 예시
+ const { openConfirm, openError } = useGlobalModalStore() + const handleApprove = async () => { const confirmed = await openConfirm('약속 승인', '약속을 승인 하시겠습니까?') if (!confirmed) return - confirmMutation.mutate(item.meetingId) + confirmMutation.mutate(item.meetingId, { + onError: () => openError('오류', '약속 승인에 실패했습니다.') + }) }- <Button variant="primary" size="small" onClick={handleApprove}> + <Button + variant="primary" + size="small" + onClick={handleApprove} + disabled={confirmMutation.isPending || rejectMutation.isPending} + > 수락 </Button>
- 약속 승인/거절/삭제 시 에러 발생 시 에러 모달 표시 - 약속 리스트 조회 에러 시 에러 모달 표시 후 홈으로 리다이렉트 - gatheringId 유효성 검증 개선 (NaN 및 0 이하 값 체크) - 약속 수 조회 에러 처리 및 에러 상태 반환 추가 - globalModalStore의 openError에 onClose 콜백 파라미터 추가
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/features/meetings/components/MeetingApprovalItem.tsx`:
- Around line 29-90: The buttons allow duplicate calls while a mutation is in
progress; guard each handler (handleApprove, handleReject, handleDelete) by
returning early if the corresponding mutation .isLoading is true, and also
disable the rendered Button components when confirmMutation.isLoading,
rejectMutation.isLoading or deleteMutation.isLoading respectively (e.g., pass
disabled prop and/or aria-busy) so the UI and logic both prevent duplicate
approve/reject/delete requests.
In `@src/shared/stores/globalModalStore.ts`:
- Around line 88-126: The confirm Promise can remain unresolved when the modal
is closed via overlay/ESC/close button because close() just resets state; fix by
adding a module-scoped pendingResolve variable (e.g., pendingResolve: ((v:
boolean)=>void)|null) and: when openConfirm(...) is called, if a pendingResolve
exists first call it with false to resolve any previous confirm, then assign the
new resolve to pendingResolve; in openConfirm's handleConfirm/handleCancel call
the pendingResolve with true/false respectively and then clear pendingResolve;
in close() check if current type === 'confirm' and if pendingResolve exists then
call pendingResolve(false) and clear it before setting initialState; ensure
every resolution path clears pendingResolve so no leaked unresolved Promises
remain.
mgYang53
left a comment
There was a problem hiding this comment.
src/features/meetings/ 폴더에 index.ts 배럴 파일이 없는데
추후 편의와 auth 폴더와의 일관성을 위해 추가하는게 어떨까요?
meetings 피처의 모듈 구조를 개선하여 import 경로를 단순화하고, 전역 store를 재구성하여 타입 안정성을 향상시켰습니다. - index.ts 배럴 파일 추가로 export 중앙화 (components, hooks, lib, api, types) - 모든 파일에서 @/features/meetings 단일 경로로 import 통일 - MeetingApprovalItem 타입명을 MeetingApprovalItemType으로 명확화 - globalModalStore를 src/shared/stores에서 src/store로 이동 - 모달 관련 타입 정의 추가 (ModalType, ModalButton, ModalButtonVariant 등) - 에러 핸들링을 useEffect로 이동하여 render 블로킹 방지 - 컴포넌트 export 방식 변경 (named → default)
🚀 풀 리퀘스트 제안
📋 작업 내용
약속 확정 대기 리스트
확정 완료 리스트
리스트 페이지네이션 적용
Alert, Confirm, Error 글로벌 모달 추가
타입
사용예시
📸 스크린샷
📄 기타
Summary by CodeRabbit
새로운 기능
기타
✏️ Tip: You can customize this high-level summary in your review settings.