Skip to content

fix: 결과 화면 전환 및 캘린더 일정 로드 복구 - #113

Closed
kokkumong wants to merge 1 commit into
devfrom
fix/calendar-result-and-imports
Closed

kokkumong wants to merge 1 commit into
devfrom
fix/calendar-result-and-imports

Conversation

@kokkumong

@kokkumong kokkumong commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

요약

  • 제출 완료 화면의 결과 화면 열기 버튼이 같은 URL로만 이동하던 문제를 수정했습니다.
  • 모임 생성 화면에서 Google/iCloud 연동 상태만 확인하던 로직을 실제 캘린더 목록/이벤트 조회로 연결했습니다.
  • 연동 일정 조회 결과를 후보 날짜 범위의 요일/시간 바쁜 칸으로 반영하고, 실패/부분 성공 상태 메시지를 표시합니다.
  • 결과 화면 열기 버튼 동작 회귀 테스트를 추가했습니다.

관련 이슈

Closes #114
Closes #115

테스트

  • npm run lint
  • DATABASE_URL=postgresql://user:pass@localhost:5432/moim_test npm test -- --run 'src/app/schedule/[id]/ScheduleRoomClient.test.tsx' src/app/schedule/create/CreateScheduleClient.test.tsx src/lib/google/tests/events.test.ts src/lib/caldav/tests/query.test.ts
  • npm run build

체크리스트

  • 브랜치 컨벤션(feature/00-name)을 지켰나요?
  • 커밋 컨벤션(feat:, fix: 등)을 지켰나요?
  • 작업 전에 관련 이슈를 생성하고 연결했나요?
  • 내 코드가 팀의 기존 코드를 망가뜨리지 않았는지 확인했나요?

@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
moim-app Ready Ready Preview, Comment Jun 14, 2026 11:47pm

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

요약

ScheduleRoomClient의 "결과 화면 열기" 동작을 Link 페이지 이동에서 API 재조회 후 인라인 상태 갱신 방식으로 전환하고, CreateScheduleClient의 캘린더 바쁜 슬롯 로딩을 하드코딩된 mock에서 Google/iCloud API 실 조회 유틸 스택으로 대체했다.

변경 사항

ScheduleRoomClient 결과 화면 열기 리팩터

Layer / File(s) 요약
SubmissionDonePanel props 계약 및 isOpeningResult 상태 선언
src/app/schedule/[id]/ScheduleRoomClient.tsx
isOpeningResult useState 추가, SubmissionDonePanel props에서 scheduleId 제거 및 isOpeningResult/onOpenResult 추가.
openSubmittedResultView 핸들러 + 호출부 연결
src/app/schedule/[id]/ScheduleRoomClient.tsx
에러 초기화 → isOpeningResult 활성화 → /api/schedules/:id fetch → 상태 일괄 갱신 → isOpeningResult 해제 순서로 동작. SubmissionDonePanel 호출부에 props 전달.
SubmissionDonePanel 버튼 UI 교체
src/app/schedule/[id]/ScheduleRoomClient.tsx
Linkbutton으로 교체, isOpeningResult 동안 비활성화 + 스피너/"여는 중" 표시.
결과 화면 열기 통합 테스트
src/app/schedule/[id]/ScheduleRoomClient.test.tsx
next/link·Supabase·global.fetch mock, publicSchedule → 버튼 클릭 → resultSchedule 흐름에서 텍스트 출현 및 cache: "no-store" 호출 검증.

CreateScheduleClient 캘린더 바쁜 슬롯 실제 조회

Layer / File(s) 요약
바쁜 슬롯 조회 유틸 타입·함수 전체 스택 추가
src/app/schedule/create/CreateScheduleClient.tsx
calendarLoadStatus/calendarLoadMessage 상태 선언, fetchConnectedBusySlotKeys를 포함한 Google/iCloud 파싱 유틸(buildQueryWindow, parseProviderDate, addEventBusyKeys, toDayCode 등) 추가.
useEffect 교체 및 상태 메시지 UI 렌더링
src/app/schedule/create/CreateScheduleClient.tsx
mock useEffect를 fetchConnectedBusySlotKeys 호출로 교체, cancelled 플래그로 경쟁 상태 방어, 성공/부분 실패/실패별 calendarLoadMessage를 체크박스 하단에 조건부 렌더링.

시퀀스 다이어그램

sequenceDiagram
  participant User
  participant SubmissionDonePanel
  participant ScheduleRoomClient
  participant ScheduleAPI as /api/schedules/:id

  User->>SubmissionDonePanel: "결과 화면 열기" 클릭
  SubmissionDonePanel->>ScheduleRoomClient: onOpenResult()
  ScheduleRoomClient->>ScheduleRoomClient: setError(null), setIsOpeningResult(true)
  ScheduleRoomClient->>ScheduleAPI: fetch({ cache: "no-store" })
  ScheduleAPI-->>ScheduleRoomClient: resultSchedule JSON
  ScheduleRoomClient->>ScheduleRoomClient: schedule/serverSaysHost/hasSubmittedAvailability/submittedName 갱신
  ScheduleRoomClient->>ScheduleRoomClient: setIsOpeningResult(false)
  ScheduleRoomClient->>SubmissionDonePanel: 결과 화면 렌더링
Loading

코드 리뷰 난이도

🎯 3 (Moderate) | ⏱️ ~25 minutes

연관 PR

  • Siul49/moim#95: ScheduleRoomClient가 재조회하는 /api/schedules/[id] 응답에 isHost, hasSubmittedAvailability, participantName을 추가한 변경으로, 이번 PR의 openSubmittedResultView 핸들러가 직접 의존한다.
  • Siul49/moim#71: candidateStartDate/candidateEndDate 입력·검증 흐름을 추가한 PR로, 이번 PR의 fetchConnectedBusySlotKeys가 해당 날짜 범위를 쿼리 윈도우로 직접 사용한다.

제안 레이블

bug, feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 'fix:' 프리픽스로 시작하고 변경 사항의 핵심(결과 화면 전환, 캘린더 로드 복구)을 명확히 요약함.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR 설명이 변경 내용, 이유, 관련 이슈를 명확히 포함하고 체크리스트를 완료했으나, 필수 섹션 중 스크린샷이 누락되었다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/calendar-result-and-imports

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/schedule/`[id]/ScheduleRoomClient.test.tsx:
- Around line 116-117: The test is failing because line 117 uses getByRole to
query the "결과 화면 열기" button immediately after waiting for the submission text,
but the SubmissionDonePanel component's useEffect hook asynchronously calls
Supabase getUser() to set the isLoggedIn state. At the time of the query,
isLoggedIn may still be false, rendering the non-logged-in UI and the button
doesn't exist. Replace the getByRole call with findByRole to wait for the button
to appear in the DOM after the asynchronous login state update completes and the
logged-in UI is rendered.

In `@src/app/schedule/`[id]/ScheduleRoomClient.tsx:
- Around line 322-350: In the openSubmittedResultView function, the
setHasSubmittedAvailability state update uses an OR operation that allows a
client-side true value to override the server's false value, violating server
authority. Remove the OR operator and the existing hasSubmittedAvailability
reference, replacing it with just Boolean(result.hasSubmittedAvailability) to
make the server response the authoritative source of truth. If this OR pattern
is intentionally needed for a specific use case, add an explanatory comment
documenting why the client-side value should persist even when the server
indicates otherwise.

In `@src/app/schedule/create/CreateScheduleClient.tsx`:
- Around line 1043-1072: The pure utility functions fetchConnectedBusySlotKeys,
buildQueryWindow, addEventBusyKeys, and toDayCode are currently defined in the
CreateScheduleClient.tsx component file, which makes them difficult to unit test
and violates the MOIM architecture guidelines. Extract these pure functions and
their related type definitions (CalendarBusyLoadResult, GoogleCalendarSummary,
ICloudCalendarSummary, GoogleEventPayload, ICloudEventPayload) from
CreateScheduleClient.tsx and move them to a new utility module in
lib/scheduling/. Create corresponding unit test files in __tests__/ using
colocated testing patterns, then import the functions back into the component
file to maintain its current behavior.
- Around line 1221-1225: The conditional check on hour in
CreateScheduleClient.tsx is unnecessary dead code. Since
Date.prototype.getHours() always returns a value between 0 and 23, the condition
hour >= 0 && hour < 24 will always be true. Remove the if statement and directly
call keys.add() with the day and hour values without the conditional check.
- Around line 1106-1114: The sourceCount is being incremented by
calendars.length before the Promise.allSettled check, which means failed
calendar retrievals are still counted in the total, creating inaccurate
user-facing messages. Move the sourceCount increment logic to only count
calendars where the promise was fulfilled (result.status === "fulfilled"),
incrementing by 1 for each successful result instead of adding the total
calendars.length upfront. Apply this same fix to the iCloud calendar handling
code that contains the same pattern, ensuring both Google and iCloud calendar
sources only count successfully retrieved calendars in their respective
sourceCount calculations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: afc84605-ffe1-40eb-9605-52b91f51718e

📥 Commits

Reviewing files that changed from the base of the PR and between 5171f8d and 27f39a6.

📒 Files selected for processing (3)
  • src/app/schedule/[id]/ScheduleRoomClient.test.tsx
  • src/app/schedule/[id]/ScheduleRoomClient.tsx
  • src/app/schedule/create/CreateScheduleClient.tsx

Comment on lines +116 to +117
await screen.findByText(/시간 제출 완료/);
await user.click(screen.getByRole("button", { name: "결과 화면 열기" }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

테스트 실패: SubmissionDonePanel의 비동기 로그인 상태 반영 전에 버튼 조회.

Line 117에서 getByRole("button", { name: "결과 화면 열기" })가 실패하는 이유는 SubmissionDonePanel 내부 useEffect(원본 파일 Lines 1009-1016)에서 Supabase getUser()를 호출해 isLoggedIn을 비동기로 설정하기 때문. Line 116에서 "시간 제출 완료"만 기다린 시점엔 아직 isLoggedIn=false일 수 있어 비로그인 UI가 렌더링되고, "결과 화면 열기" 버튼이 존재하지 않음.

Why: 비동기 상태 반영이 완료되기 전에 DOM 쿼리를 수행하면 요소를 찾을 수 없음.

How: 로그인 전용 요소가 렌더링될 때까지 findBy*로 대기.

🔧 수정안
     await screen.findByText(/시간 제출 완료/);
+    await screen.findByRole("button", { name: "결과 화면 열기" });
-    await user.click(screen.getByRole("button", { name: "결과 화면 열기" }));
+    await user.click(screen.getByRole("button", { name: "결과 화면 열기" }));

또는 더 명확하게:

     await screen.findByText(/시간 제출 완료/);
-    await user.click(screen.getByRole("button", { name: "결과 화면 열기" }));
+    const resultButton = await screen.findByRole("button", { name: "결과 화면 열기" });
+    await user.click(resultButton);
🧰 Tools
🪛 GitHub Actions: CI Pipeline / 0_code_quality.txt

[error] 117-117: TestingLibraryElementError in test '제출 완료 화면의 결과 버튼이 같은 URL 이동 대신 결과 데이터를 불러와 현황 화면을 연다': Unable to find an accessible element with the role "button" and name "결과 화면 열기". Command/step: user.click(screen.getByRole("button", { name: "결과 화면 열기" }))

🪛 GitHub Actions: CI Pipeline / code_quality

[error] 117-117: TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "결과 화면 열기" (failed test: "제출 완료 화면의 결과 버튼이 같은 URL 이동 대신 결과 데이터를 불러와 현황 화면을 연다").

🪛 GitHub Check: code_quality

[failure] 117-117: src/app/schedule/[id]/ScheduleRoomClient.test.tsx > ScheduleRoomClient 결과 화면 열기 > 제출 완료 화면의 결과 버튼이 같은 URL 이동 대신 결과 데이터를 불러와 현황 화면을 연다
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "결과 화면 열기"

Here are the accessible roles:

main:

Name "":


banner:

Name "":


link:

Name "MOIM":

Name "홈으로 가기":

Name "캘린더 등록하기":

Name "모임 만들기":

Name "설정":

Name "로그인":

Name "3초 간편 소셜 회원가입 🚀":


navigation:

Name "":


button:

Name "메뉴 토글":

Name "적용":

Name "캘린더 연동 화면 보기":


complementary:

Name "":


paragraph:

Name "":

Name "":

Comment on lines +322 to +350
async function openSubmittedResultView() {
setError("");
setIsOpeningResult(true);

try {
const response = await fetch(`/api/schedules/${scheduleId}`, {
cache: "no-store",
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error ?? "결과 화면을 열 수 없습니다.");
}

setSchedule(result.schedule);
setServerSaysHost(Boolean(result.isHost));
setHasSubmittedAvailability(
Boolean(result.hasSubmittedAvailability) || hasSubmittedAvailability,
);
if (result.participantName) {
setSubmittedName(result.participantName);
}
} catch (caught) {
setError(
caught instanceof Error ? caught.message : "요청에 실패했습니다.",
);
} finally {
setIsOpeningResult(false);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚖️ Poor tradeoff

Line 338 상태 병합 로직이 서버 권위를 침해할 수 있음.

setHasSubmittedAvailability(
  Boolean(result.hasSubmittedAvailability) || hasSubmittedAvailability,
);

서버가 hasSubmittedAvailability: false를 반환해도 클라이언트가 이미 true면 계속 true로 유지된다. 이는 서버 상태를 source of truth로 보지 않는 패턴.

Why: 서버가 "제출 기록 삭제됨" 같은 상황을 명시적으로 알려도 클라이언트가 무시하면 UI와 실제 상태가 불일치.

How: 서버 응답을 신뢰하도록 OR 제거.

🔧 제안 수정
-      setHasSubmittedAvailability(
-        Boolean(result.hasSubmittedAvailability) || hasSubmittedAvailability,
-      );
+      setHasSubmittedAvailability(Boolean(result.hasSubmittedAvailability));

의도적으로 "한 번 true면 항상 유지"가 필요하다면 주석으로 Why를 명시해야 함.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/`[id]/ScheduleRoomClient.tsx around lines 322 - 350, In the
openSubmittedResultView function, the setHasSubmittedAvailability state update
uses an OR operation that allows a client-side true value to override the
server's false value, violating server authority. Remove the OR operator and the
existing hasSubmittedAvailability reference, replacing it with just
Boolean(result.hasSubmittedAvailability) to make the server response the
authoritative source of truth. If this OR pattern is intentionally needed for a
specific use case, add an explanatory comment documenting why the client-side
value should persist even when the server indicates otherwise.

Comment on lines +1043 to +1072
type CalendarBusyLoadResult = {
busySlotKeys: string[];
sourceCount: number;
errors: string[];
};

type GoogleCalendarSummary = {
id: string;
};

type ICloudCalendarSummary = {
calendarUrl: string;
};

type GoogleEventPayload = {
start?: {
dateTime?: string;
date?: string;
};
end?: {
dateTime?: string;
date?: string;
};
};

type ICloudEventPayload = {
startAt?: string;
endAt?: string;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

캘린더 파싱 유틸 함수를 lib/scheduling/으로 분리 권장.

fetchConnectedBusySlotKeys, buildQueryWindow, addEventBusyKeys, toDayCode 등은 React와 무관한 순수 함수. 현재 컴포넌트 파일 하단에 있어 단위 테스트 작성이 어려움.

MOIM 아키텍처 가이드에 따르면 순수 함수는 lib/scheduling/에 배치하고 __tests__/에 코로케이션 테스트를 두어야 함.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/create/CreateScheduleClient.tsx` around lines 1043 - 1072,
The pure utility functions fetchConnectedBusySlotKeys, buildQueryWindow,
addEventBusyKeys, and toDayCode are currently defined in the
CreateScheduleClient.tsx component file, which makes them difficult to unit test
and violates the MOIM architecture guidelines. Extract these pure functions and
their related type definitions (CalendarBusyLoadResult, GoogleCalendarSummary,
ICloudCalendarSummary, GoogleEventPayload, ICloudEventPayload) from
CreateScheduleClient.tsx and move them to a new utility module in
lib/scheduling/. Create corresponding unit test files in __tests__/ using
colocated testing patterns, then import the functions back into the component
file to maintain its current behavior.

Source: Coding guidelines

Comment on lines +1106 to +1114
sourceCount += calendars.length;
for (const result of results) {
if (result.status === "fulfilled") {
for (const event of result.value.events ?? []) {
addEventBusyKeys(busyKeys, getGoogleEventRange(event));
}
} else {
errors.push("google");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

sourceCount가 실패한 캘린더까지 포함하여 메시지가 부정확해질 수 있음.

Promise.allSettled 전에 calendars.length를 더하므로, 개별 캘린더 이벤트 조회가 실패해도 sourceCount에 포함됨. "3개 캘린더에서 0개 바쁜 시간대를 반영했습니다" 같은 오해의 소지가 있는 메시지가 출력될 수 있음.

🐛 성공한 캘린더만 카운트하도록 수정
-      sourceCount += calendars.length;
       for (const result of results) {
         if (result.status === "fulfilled") {
+          sourceCount += 1;
           for (const event of result.value.events ?? []) {
             addEventBusyKeys(busyKeys, getGoogleEventRange(event));
           }
         } else {
           errors.push("google");
         }
       }

iCloud 쪽(Line 1143)도 동일하게 수정 필요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/create/CreateScheduleClient.tsx` around lines 1106 - 1114,
The sourceCount is being incremented by calendars.length before the
Promise.allSettled check, which means failed calendar retrievals are still
counted in the total, creating inaccurate user-facing messages. Move the
sourceCount increment logic to only count calendars where the promise was
fulfilled (result.status === "fulfilled"), incrementing by 1 for each successful
result instead of adding the total calendars.length upfront. Apply this same fix
to the iCloud calendar handling code that contains the same pattern, ensuring
both Google and iCloud calendar sources only count successfully retrieved
calendars in their respective sourceCount calculations.

Comment on lines +1221 to +1225
const day = toDayCode(cursor);
const hour = cursor.getHours();
if (hour >= 0 && hour < 24) {
keys.add(`${day}-${hour}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

hour >= 0 && hour < 24 조건은 항상 참이므로 dead code.

Date.prototype.getHours()는 항상 0–23 범위를 반환함. 불필요한 조건 분기.

🧹 불필요한 조건 제거
     if (next > range.start && cursor < range.end) {
       const day = toDayCode(cursor);
       const hour = cursor.getHours();
-      if (hour >= 0 && hour < 24) {
-        keys.add(`${day}-${hour}`);
-      }
+      keys.add(`${day}-${hour}`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/schedule/create/CreateScheduleClient.tsx` around lines 1221 - 1225,
The conditional check on hour in CreateScheduleClient.tsx is unnecessary dead
code. Since Date.prototype.getHours() always returns a value between 0 and 23,
the condition hour >= 0 && hour < 24 will always be true. Remove the if
statement and directly call keys.add() with the day and hour values without the
conditional check.

@kokkumong

Copy link
Copy Markdown
Collaborator Author

Superseded by the updated branch fix/calendar-result-and-imports-v2, which includes the CI stability fix for the result button test.

@kokkumong kokkumong closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant