Skip to content

[feat] 약속 설정 UI 및 기능 구현#32

Merged
mgYang53 merged 13 commits intomainfrom
feat/meetings-setting-24
Jan 31, 2026
Merged

[feat] 약속 설정 UI 및 기능 구현#32
mgYang53 merged 13 commits intomainfrom
feat/meetings-setting-24

Conversation

@haruyam15
Copy link
Contributor

@haruyam15 haruyam15 commented Jan 28, 2026

🚀 풀 리퀘스트 제안

📋 작업 내용

약속 확정 대기 리스트

  • 거절, 수락 기능 구현

확정 완료 리스트

  • 삭제 기능 구현

리스트 페이지네이션 적용

  • 리스트 10개 이상일 때부터 페이지네이션이 노출

Alert, Confirm, Error 글로벌 모달 추가

타입

openAlert: (title: string, description: string) => void
openError: (title: string, description: string, onClose?: () => void) => void
openConfirm: (
    title: string,
    description: string,
    options?: {
      confirmText?: string //삭제, 확인 등 컨펌 버튼 명 커스텀
      variant?: 'primary' | 'danger'
    }
  ) => Promise<boolean>

사용예시

const { openConfirm, openError, openAlert } = useGlobalModalStore()
// 컨펌 모달
const confirmed = await openConfirm(
  '약속 삭제',
  '삭제된 약속은 리스트에서 사라지며 복구할 수 없어요.\n정말 약속을 삭제하시겠어요?',
  { confirmText: '삭제', variant: 'danger' }
)
if (!confirmed) return

// 에러 모달
openError('에러', error.userMessage, () => {
  navigate('/')
})

// 알림 모달
openAlert('알림', '알림입니다.')

📸 스크린샷

image image

📄 기타

  • Alert, Error는 디자인 변경 해야 함

Summary by CodeRabbit

  • 새로운 기능

    • 약속 관리 페이지 추가 — PENDING/CONFIRMED 탭과 페이징 지원.
    • 약속 승인/거절/삭제 UI 및 전역 확인 모달 흐름 추가.
    • 전역 모달 호스트 도입으로 일관된 확인/오류 처리 제공.
    • 전반적 회의 관련 API 및 모의 데이터, 카운트/목록 조회 및 포맷 유틸 추가.
    • 새로운 라우트(약속 설정) 등록.
  • 기타

    • 공유 디렉터리로 경로 정리 및 스타일 경로 업데이트.
    • 상태관리 런타임 의존성(zustand) 추가.

✏️ Tip: You can customize this high-level summary in your review settings.

- 약속 승인 대기/완료 리스트 조회 및 페이지네이션 기능
- 약속 승인, 거부, 삭제 기능 구현
- 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)
@haruyam15 haruyam15 self-assigned this Jan 28, 2026
@haruyam15 haruyam15 added the feat 새로운 기능 추가 label Jan 28, 2026
@haruyam15 haruyam15 linked an issue Jan 28, 2026 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

미팅 승인 관리 기능을 추가합니다. 전역 모달 스토어와 호스트, 미팅 승인 API/목데이터, React Query 훅(조회/뮤테이션), 승인 항목·목록 컴포넌트, 미팅 설정 페이지 및 라우트, 관련 타입/유틸, Zustand 의존성 추가가 포함됩니다.

Changes

Cohort / File(s) Summary
구성 및 의존성
components.json, package.json
Tailwind 경로와 모듈 alias를 shared로 변경. zustand@^5.0.10 의존성 추가.
전역 모달
src/store/globalModalStore.ts, src/store/index.ts, src/shared/ui/GlobalModalHost.tsx, src/App.tsx
Zustand 기반 글로벌 모달 스토어와 Host 컴포넌트 추가, App에 GlobalModalHost 마운트. 스토어 타입과 훅을 재내보내기 함.
미팅 타입·유틸·배럴
src/features/meetings/meetings.types.ts, src/features/meetings/lib/formatDateTime.ts, src/features/meetings/lib/index.ts, src/features/meetings/index.ts
미팅 관련 타입(상태, 항목, 요청/응답) 추가 및 날짜 포맷 유틸과 배럴 내보내기 추가.
API 및 목데이터
src/features/meetings/meetings.api.ts, src/features/meetings/meetings.mock.ts
getMeetingApprovals / confirm/reject/delete API 구현. 목데이터 제공과 USE_MOCK_DATA 분기(지연 포함).
React Query 훅
src/features/meetings/hooks/...
meetingQueryKeys.ts, useMeetingApprovals.ts, useMeetingApprovalsCount.ts, useConfirmMeeting.ts, useRejectMeeting.ts, useDeleteMeeting.ts, index.ts
쿼리 키 팩토리, 조회 훅(목록·카운트), 뮤테이션 훅(수락/거절/삭제) 추가. 적절한 queryKey와 무효화 전략 적용.
컴포넌트
src/features/meetings/components/MeetingApprovalItem.tsx, src/features/meetings/components/MeetingApprovalList.tsx, src/features/meetings/components/index.ts
승인 항목 및 목록 컴포넌트 추가. 전역 모달을 통한 확인 흐름과 에러 처리, 페이징 표시 포함.
페이지·라우팅
src/pages/Meetings/MeetingSettingPage.tsx, src/pages/Meetings/index.ts, src/routes/index.tsx, src/shared/constants/routes.ts
미팅 설정 페이지(탭별 카운트, 리스트) 추가 및 라우트(/gatherings/:id/meetings/setting) 등록. 라우트 상수 3개 추가.
사소한 정리
src/pages/ComponentGuide/ComponentGuidePage.tsx
import 순서 정리 (기능 변경 없음).

Sequence Diagram

sequenceDiagram
    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 업데이트
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • choiyoungae
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 및 기능 구현'은 전역 모달, 약속 승인 리스트, 페이지네이션 등 핵심 기능 추가를 포괄합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/meetings-setting-24

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: 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 콜백 파라미터 추가
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: 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

This comment was marked as duplicate.

Copy link
Contributor

@mgYang53 mgYang53 left a comment

Choose a reason for hiding this comment

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

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)
Copy link
Contributor

@mgYang53 mgYang53 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다!👍

@mgYang53 mgYang53 merged commit 806a718 into main Jan 31, 2026
1 check passed
@mgYang53 mgYang53 deleted the feat/meetings-setting-24 branch January 31, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 약속 설정 UI 및 기능 구현

2 participants