[FEAT] 구글 캘린더 연동 구현#227
Conversation
- 캘린더 연동/해제/인증 관련 엔드포인트 및 모델을 추가했습니다 - 기존 엔드포인트 파일들을 최신 스펙으로 재생성했습니다
- 캘린더 OAuth 콜백 페이지를 추가했습니다 - 콜백에서 code/state로 캘린더 연동 API를 호출하고 프로필 쿼리를 갱신했습니다 - 온보딩 캘린더 연동 버튼에 authorize API 호출 및 리다이렉트 로직을 연결했습니다 - 설정 페이지 캘린더 연동 버튼에 authorize API 호출 및 리다이렉트 로직을 연결했습니다 - localStorage의 calendarConnectOrigin 값으로 OAuth 완료 후 이동 경로를 구분했습니다
…to feat/web/187-google-calendar-api-integration
…e-calendar-api-integration
…e-calendar-api-integration
- pnpm gen:api 재생성 반영 - CalendarEventsResponse 로컬 zod 스키마 및 타입 추가 (schemas/calendar) - useCalendarEventsQuery 훅 추가 (DAY/WEEK/TWO_WEEK 필터 지원) - HomeTodoContainer: 홈 뷰 필터에 따라 WEEK/TWO_WEEK로 캘린더 이벤트 조회 후 일별 렌더링 - TodayTodoListContainer: DAY 필터로 오늘 캘린더 이벤트 조회 후 렌더링 - CalendarEventItem 컴포넌트 추가 - 구글 캘린더 연동 해제 확인 모달(SettingsCalendarDisconnectModalContainer) 적용 - 캘린더 연동/해제 실패 토스트 처리 추가 - focus 엔드포인트 응답 구조 변경 반영 (BaseResponse 래퍼 제거) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HomeCalendarEventCard, TodayCalendarEventCard 컴포넌트를 추가했습니다 - CalendarEventItem 컴포넌트를 삭제했습니다 - 캘린더 이벤트 체크박스 상태를 컨테이너로 리프팅했습니다 - completedCount와 totalCount 계산에 캘린더 이벤트를 포함했습니다 - 캘린더 연결 해제 시 이벤트 쿼리 캐시를 제거했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
WalkthroughGoogle Calendar OAuth 연결·해제와 콜백 처리를 추가하고, 연결된 캘린더 이벤트를 홈·오늘 화면에 표시합니다. 이벤트 조회 스키마와 React Query 훅, 설정 오류 토스트, 다국어 문구, 온보딩 완료 흐름도 변경했습니다. ChangesGoogle Calendar 통합
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant SettingsProfileContainer
participant useSettingsProfile
participant authorize
participant CalendarCallbackContainer
participant connectCalendar
User->>SettingsProfileContainer: 캘린더 연결 클릭
SettingsProfileContainer->>useSettingsProfile: 연결 처리 요청
useSettingsProfile->>authorize: authorizationUrl 요청
authorize-->>useSettingsProfile: authorizationUrl 반환
useSettingsProfile-->>User: OAuth 페이지로 리다이렉트
User->>CalendarCallbackContainer: code/state 콜백
CalendarCallbackContainer->>connectCalendar: 캘린더 연결 요청
connectCalendar-->>CalendarCallbackContainer: 연결 결과
CalendarCallbackContainer-->>User: 설정 또는 홈으로 리다이렉트
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ 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 |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Around line 168-175: Replace index-based calendar event keys and checked-state
tracking with each event’s unique id. In
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx
lines 168-175, update the rendered key and checked lookup to use event.id,
change the related state from Set<number> to Set<string>, and update
toggleCalendarEvent parameters and callers accordingly. Apply the same
event.id-based rendering and state handling in
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
lines 112-119.
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/account/SettingsCalendarDisconnectModalContainer.tsx:
- Around line 19-25: Move SettingsCalendarDisconnectModalContainer to
_components/account/SettingsCalendarDisconnectModal.tsx and rename the exported
component to SettingsCalendarDisconnectModal, removing the Container suffix.
Update all imports and usages to reference the new path and symbol while
preserving its props-only rendering behavior.
In
`@apps/timo-web/app/`[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsx:
- Around line 25-26: Rename the state variable and setter from
isCalendarErrorToastOpen/setIsCalendarErrorToastOpen to
isCalendarDisconnectErrorToastOpen/setIsCalendarDisconnectErrorToastOpen, and
update every corresponding JSX prop and usage in SettingsProfileContainer.
In
`@apps/timo-web/app/`[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx:
- Around line 22-23: Update isCalendarConnected in CalendarConnectStepContainer
to use a plain constant initialized to false instead of useState, and remove the
now-unused useState import while preserving all existing references.
In `@apps/timo-web/components/calendar/TodayCalendarEventCard.tsx`:
- Around line 19-48: Replace the isHovered useState and derived isDimmed logic
in TodayCalendarEventCard with Tailwind parent-hover styling: add the group
class to the card container and use group-hover variants for the checked-state
background and text colors. Preserve the existing checked behavior when the card
is not hovered and remove the mouse event handlers and unused state.
In `@apps/timo-web/schemas/calendar/calendar-events-schema.ts`:
- Around line 5-23: calendar-events-schema.ts의 타입 네이밍과 선언 방식을 정리하세요. 유니언 타입인
CalendarFilter를 CalendarFilterTypes로 변경하고,
CalendarEvent·CalendarDay·CalendarEventsData는 interface로 전환하세요. 해당 타입을 import하거나
참조하는 모든 위치도 새 이름과 interface 선언에 맞게 수정하세요.
🪄 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: 20f6cc6e-16f6-4e34-8dea-fa2b6a735909
⛔ Files ignored due to path filters (25)
apps/timo-web/api/generated/endpoints/auth/auth.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/auth/auth.zod.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/calendar/calendar.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/calendar/calendar.zod.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/focus/focus.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/focus/focus.zod.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/todo/todo.tsis excluded by!**/generated/**apps/timo-web/api/generated/endpoints/todo/todo.zod.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/baseResponseCalendarAuthorizeResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/baseResponseCalendarConnectResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/baseResponseCalendarDisconnectResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/baseResponseCalendarEventsResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarAuthorizeResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarConnectRequest.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarConnectResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarDayResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarDisconnectResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarEventResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/calendarEventsResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/errorDto.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/focusTodoDetailResponse.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/getCalendarEventsParams.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/index.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/todoCreateRequestIcon.tsis excluded by!**/generated/**apps/timo-web/api/generated/models/todoUpdateRequestIcon.tsis excluded by!**/generated/**
📒 Files selected for processing (17)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/app/[locale]/(main)/focus/_queries/use-focus-todo-query.tsapps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsCalendarDisconnectModalContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile-labels.tsapps/timo-web/app/[locale]/(main)/settings/_hooks/account/use-settings-profile.tsapps/timo-web/app/[locale]/(main)/settings/_types/account/profile-type.tsapps/timo-web/app/[locale]/oauth/calendar/callback/_containers/CalendarCallbackContainer.tsxapps/timo-web/app/[locale]/oauth/calendar/callback/page.tsxapps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsxapps/timo-web/components/calendar/HomeCalendarEventCard.tsxapps/timo-web/components/calendar/TodayCalendarEventCard.tsxapps/timo-web/messages/en.jsonapps/timo-web/messages/ko.jsonapps/timo-web/queries/calendar/use-calendar-events-query.tsapps/timo-web/schemas/calendar/calendar-events-schema.ts
| const [isHovered, setIsHovered] = useState(false); | ||
| const isDimmed = checked && !isHovered; | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| "border-timo-gray-500 flex min-w-80 rounded-[4px] border px-5 py-4", | ||
| isDimmed ? "bg-timo-gray-200" : "bg-white", | ||
| )} | ||
| onMouseEnter={() => setIsHovered(true)} | ||
| onMouseLeave={() => setIsHovered(false)} | ||
| > | ||
| <div className="flex min-w-0 flex-1 items-center gap-2"> | ||
| <div className="inline-flex items-center"> | ||
| <Checkbox checked={checked} onChange={onToggle} /> | ||
| </div> | ||
| <div className="shrink-0 p-1"> | ||
| <Image | ||
| src="/images/google-calendar.png" | ||
| alt="Google Calendar" | ||
| width={14} | ||
| height={14} | ||
| /> | ||
| </div> | ||
| <span | ||
| className={cn( | ||
| "typo-headline-b-14 min-w-0 truncate", | ||
| isDimmed ? "text-timo-gray-700" : "text-timo-gray-900", | ||
| )} | ||
| > |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
React 상태 대신 CSS(Tailwind) Hover를 활용해 성능을 개선해 보세요.
단순한 UI Hover 상태를 위해 useState를 사용하면 불필요한 렌더링이 발생합니다. Tailwind의 group과 group-hover 속성을 사용하면 코드를 더 간결하게 만들고 성능도 높일 수 있습니다. 멋지게 최적화해 볼까요? 🚀 (Tailwind 문서 참고: https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state)
✨ 제안하는 수정안 (CSS Hover 적용)
- const [isHovered, setIsHovered] = useState(false);
- const isDimmed = checked && !isHovered;
-
return (
<div
className={cn(
- "border-timo-gray-500 flex min-w-80 rounded-[4px] border px-5 py-4",
- isDimmed ? "bg-timo-gray-200" : "bg-white",
+ "border-timo-gray-500 group flex min-w-80 rounded-[4px] border px-5 py-4 transition-colors",
+ checked ? "bg-timo-gray-200 hover:bg-white" : "bg-white",
)}
- onMouseEnter={() => setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
>
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="inline-flex items-center">
<Checkbox checked={checked} onChange={onToggle} />
</div>
<div className="shrink-0 p-1">
<Image
src="/images/google-calendar.png"
alt="Google Calendar"
width={14}
height={14}
/>
</div>
<span
className={cn(
"typo-headline-b-14 min-w-0 truncate",
- isDimmed ? "text-timo-gray-700" : "text-timo-gray-900",
+ checked ? "text-timo-gray-700 group-hover:text-timo-gray-900" : "text-timo-gray-900",
)}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [isHovered, setIsHovered] = useState(false); | |
| const isDimmed = checked && !isHovered; | |
| return ( | |
| <div | |
| className={cn( | |
| "border-timo-gray-500 flex min-w-80 rounded-[4px] border px-5 py-4", | |
| isDimmed ? "bg-timo-gray-200" : "bg-white", | |
| )} | |
| onMouseEnter={() => setIsHovered(true)} | |
| onMouseLeave={() => setIsHovered(false)} | |
| > | |
| <div className="flex min-w-0 flex-1 items-center gap-2"> | |
| <div className="inline-flex items-center"> | |
| <Checkbox checked={checked} onChange={onToggle} /> | |
| </div> | |
| <div className="shrink-0 p-1"> | |
| <Image | |
| src="/images/google-calendar.png" | |
| alt="Google Calendar" | |
| width={14} | |
| height={14} | |
| /> | |
| </div> | |
| <span | |
| className={cn( | |
| "typo-headline-b-14 min-w-0 truncate", | |
| isDimmed ? "text-timo-gray-700" : "text-timo-gray-900", | |
| )} | |
| > | |
| return ( | |
| <div | |
| className={cn( | |
| "border-timo-gray-500 group flex min-w-80 rounded-[4px] border px-5 py-4 transition-colors", | |
| checked ? "bg-timo-gray-200 hover:bg-white" : "bg-white", | |
| )} | |
| > | |
| <div className="flex min-w-0 flex-1 items-center gap-2"> | |
| <div className="inline-flex items-center"> | |
| <Checkbox checked={checked} onChange={onToggle} /> | |
| </div> | |
| <div className="shrink-0 p-1"> | |
| <Image | |
| src="/images/google-calendar.png" | |
| alt="Google Calendar" | |
| width={14} | |
| height={14} | |
| /> | |
| </div> | |
| <span | |
| className={cn( | |
| "typo-headline-b-14 min-w-0 truncate", | |
| checked ? "text-timo-gray-700 group-hover:text-timo-gray-900" : "text-timo-gray-900", | |
| )} | |
| > |
🤖 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 `@apps/timo-web/components/calendar/TodayCalendarEventCard.tsx` around lines 19
- 48, Replace the isHovered useState and derived isDimmed logic in
TodayCalendarEventCard with Tailwind parent-hover styling: add the group class
to the card container and use group-hover variants for the checked-state
background and text colors. Preserve the existing checked behavior when the card
is not hovered and remove the mouse event handlers and unused state.
- 캘린더 이벤트 key와 체크 상태를 index 대신 title 기반으로 변경했습니다 - SettingsCalendarDisconnectModal을 _components로 이동하고 Container suffix를 제거했습니다 - isCalendarErrorToastOpen을 isCalendarDisconnectErrorToastOpen으로 이름을 변경했습니다 - CalendarConnectStepContainer의 불필요한 useState를 상수로 변경했습니다 - TodayCalendarEventCard의 hover 상태를 Tailwind CSS group-hover로 변경했습니다
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx (1)
23-33: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
localStorage예외 발생 시나리오에 대비하세요.일부 브라우저의 시크릿 모드나 보안 설정에 의해
localStorage접근이 차단된 경우,setItem호출 시 예외(QuotaExceededError또는SecurityError)가 발생할 수 있습니다. 현재 로직에서는 이 예외가 발생하면 외부catch블록으로 넘어가게 되어, 가장 중요한 OAuth 인증 URL로의 리다이렉션(window.location.assign)이 아예 실행되지 않는 불상사가 발생할 수 있습니다.
localStorage할당 실패가 전체 인증 흐름을 차단하지 않도록, 해당 부분만 별도의try-catch로 감싸주세요.
이전 리뷰의 불필요한useState피드백을 변수로 깔끔하게 반영해주신 점, 아주 센스 넘칩니다! ✨관련 공식 문서: MDN - Web Storage API 사용하기
💡 제안하는 수정안
const handleGoogleConnect = async () => { try { const response = await authorize(); const url = response.data?.authorizationUrl; if (!url) return; - localStorage.setItem("calendarConnectOrigin", "onboarding"); + try { + localStorage.setItem("calendarConnectOrigin", "onboarding"); + } catch { + // 브라우저 설정으로 인한 localStorage 접근 실패 무시 + } window.location.assign(url); } catch { // authorize 실패 시 아무 동작 없음 — 사용자가 재시도 가능 } };🤖 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 `@apps/timo-web/app/`[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx around lines 23 - 33, Update handleGoogleConnect so the localStorage.setItem call is wrapped in its own try-catch, allowing window.location.assign(url) to execute even when storage access fails. Keep the existing authorization error handling and onboarding origin value unchanged.apps/timo-web/app/[locale]/(main)/settings/_components/account/SettingsCalendarDisconnectModal.tsx (1)
47-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value비동기 동작 시 모달의 로딩 UX를 고려해 보세요.
현재
onDisconnect호출 후 즉시onClose가 실행되어 모달이 바로 닫힙니다. 기능상 문제는 없고 에러 토스트도 상위 컴포넌트에 잘 연결되어 있지만, 만약 연결 해제 API 응답이 지연된다면 사용자 입장에서는 처리가 완료된 것인지 헷갈릴 수 있습니다.추후 상위 컨테이너 컴포넌트에서 React 19의
useTransition이나isPending상태를 모달로 전달하여,onDisconnect가 진행되는 동안 버튼에 로딩 스피너를 보여주고 완료 후에 모달을 닫도록 개선한다면 훨씬 부드러운 사용자 경험을 제공할 수 있을 거예요.
순수 UI 컴포넌트로 역할 분리를 가이드라인에 맞게 아주 깔끔하게 해주셨네요! 👍관련 공식 문서: React - useTransition
🤖 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 `@apps/timo-web/app/`[locale]/(main)/settings/_components/account/SettingsCalendarDisconnectModal.tsx around lines 47 - 56, Update the SettingsCalendarDisconnectModal disconnect flow so onClose runs only after the asynchronous onDisconnect operation completes. Accept a pending/loading state from the parent, disable the confirmation action and show its loading indicator while the operation is in progress, then close the modal on successful completion while preserving existing error-toast handling.
🤖 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
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Line 63: Rename toggleCalendarEvent to handleToggleCalendarEvent in both
HomeTodoContainer.tsx (lines 63-63) and TodayTodoListContainer.tsx (lines
48-48), updating all references at each site to preserve event-handler behavior.
- Around line 136-138: Duplicate calendar titles cause incorrect completion
counts and React key warnings. In HomeTodoContainer.tsx lines 136-138 and
TodayTodoListContainer.tsx lines 93-94, calculate completed calendar items by
filtering the calendar-event arrays rather than using
checkedCalendarTitles.size. In HomeTodoContainer.tsx lines 168-175 and
TodayTodoListContainer.tsx lines 112-119, combine each event.title with its map
index for the React key.
---
Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/settings/_components/account/SettingsCalendarDisconnectModal.tsx:
- Around line 47-56: Update the SettingsCalendarDisconnectModal disconnect flow
so onClose runs only after the asynchronous onDisconnect operation completes.
Accept a pending/loading state from the parent, disable the confirmation action
and show its loading indicator while the operation is in progress, then close
the modal on successful completion while preserving existing error-toast
handling.
In
`@apps/timo-web/app/`[locale]/onboarding/_containers/CalendarConnectStepContainer.tsx:
- Around line 23-33: Update handleGoogleConnect so the localStorage.setItem call
is wrapped in its own try-catch, allowing window.location.assign(url) to execute
even when storage access fails. Keep the existing authorization error handling
and onboarding origin value unchanged.
🪄 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: 4698aae2-4a7b-415c-86ff-827c71839833
📒 Files selected for processing (6)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/app/[locale]/(main)/settings/_components/account/SettingsCalendarDisconnectModal.tsxapps/timo-web/app/[locale]/(main)/settings/_containers/account/SettingsProfileContainer.tsxapps/timo-web/app/[locale]/onboarding/_containers/CalendarConnectStepContainer.tsxapps/timo-web/components/calendar/TodayCalendarEventCard.tsx
- PillButton에 gray-dark variant를 추가했습니다 - Integrate 상태에서는 blue, Disconnect 상태에서는 gray-dark로 변경했습니다
- 이벤트 식별자를 title 단독에서 title-index composite key로 변경했습니다 - 동일 제목 일정이 여러 개일 때 각각 독립적으로 체크 상태를 관리합니다 - React 중복 key 경고를 해소했습니다
…to feat/web/187-google-calendar-api-integration
…to feat/web/187-google-calendar-api-integration
…to feat/web/187-google-calendar-api-integration # Conflicts: # apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx
…//github.com/Team-Timo/Timo-client into feat/web/187-google-calendar-api-integration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/timo-web/app/[locale]/onboarding/_containers/LifePatternStepContainer.tsx (1)
89-95: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAPI 호출 중 중복 클릭을 방지하기 위한 로딩 상태 처리
기존 화면 이동 대신 온보딩 완료 API가 바로 호출되도록 변경되었네요! 🎉
하지만 통신 중 버튼이 여러 번 클릭되는 것을 막기 위한 비활성화 처리가 누락되었습니다. 중복 제출을 방지하기 위해 상위 컴포넌트로부터 로딩 상태(isPending)를 전달받아 적용해 주시면 더 견고한 UX가 될 거예요. Props 인터페이스에isPending?: boolean을 추가한 후 아래와 같이 수정해 보세요!(참고: TanStack Query Mutations 문서)
💡 제안하는 수정안
- <OnboardingButtonContainer - variant="start" - isActive={canProceed} - disabled={!canProceed} - onClick={onNext} - /> + <OnboardingButtonContainer + variant="start" + isActive={canProceed} + disabled={!canProceed || isPending} + onClick={onNext} + />🤖 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 `@apps/timo-web/app/`[locale]/onboarding/_containers/LifePatternStepContainer.tsx around lines 89 - 95, Update LifePatternStepContainer to accept an optional isPending prop from its parent and use it to disable OnboardingButtonContainer while the completion API request is in progress, alongside the existing canProceed condition, preventing duplicate submissions.apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx (3)
54-56: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value🚀
Map객체 생성을 메모이제이션하여 성능을 높여보세요!컴포넌트가 렌더링될 때마다 새로운
Map인스턴스가 생성되고 있습니다.useMemo를 활용해calendarEventsData가 변경될 때만Map을 다시 계산하도록 최적화하면 불필요한 연산을 줄일 수 있습니다. ✨
React 공식 문서:useMemo를 참고해 보세요!💡 제안하는 수정안
- const calendarEventsByDate = new Map( - (calendarEventsData?.days ?? []).map((day) => [day.date, day.events]), - ); + const calendarEventsByDate = useMemo(() => new Map( + (calendarEventsData?.days ?? []).map((day) => [day.date, day.events]), + ), [calendarEventsData?.days]);(이 수정을 적용하려면 상단에
useMemo를 import 해주세요.)🤖 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 `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx around lines 54 - 56, Memoize the calendarEventsByDate Map in HomeTodoContainer using React’s useMemo, with calendarEventsData as the dependency so it is recalculated only when the source data changes. Add the required useMemo import and preserve the existing fallback and day-to-events mapping behavior.
51-51: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win🛡️
profile데이터 로딩 상태를 안전하게 처리해 볼까요?
useMyProfileQuery의 결과인profile이 아직 로드되지 않은 상태(undefined)일 경우.calendarConnected에 접근하면 런타임 에러가 발생할 수 있습니다. 두 컨테이너 모두 옵셔널 체이닝(?.)을 사용해 더욱 견고한 코드를 만들어 주세요! 💪
(참고로 Suspense를 사용해 데이터 로딩을 100% 보장하는 구조라면 이 제안은 무시하셔도 좋습니다.)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx#L51-L51:enabled: profile?.calendarConnected,로 수정해 주세요.apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx#L41-L41:enabled: profile?.calendarConnected,로 수정해 주세요.🤖 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 `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx at line 51, Update the enabled option using the profile result from useMyProfileQuery in both HomeTodoContainer.tsx (line 51) and TodayTodoListContainer.tsx (line 41), accessing calendarConnected through optional chaining so undefined profile data does not throw.
137-137: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win🎯 캘린더 이벤트 카운트 로직을 수정해 주세요!
현재 캘린더 이벤트 카드(
HomeCalendarEventCard,TodayCalendarEventCard)에서 체크박스 UI가 제거되어 캘린더 이벤트를 완료할 수 없는 상태입니다. 하지만 상단 헤더의totalCount에는 캘린더 이벤트 개수가 포함되어 있어, 사용자가 하루의 달성률 100%를 영원히 채울 수 없는 버그가 발생하게 됩니다! 🐛캘린더 이벤트를 단순 조회 및 참고용으로만 제공하신다면, 전체 개수에서
calendarEvents.length를 제외해 주세요.
(만약 PR 설명에 기재된 대로 체크 기능이 의도된 것이라면, 카드 UI에 체크박스를 다시 추가하고completedCount로직도 함께 복구해야 합니다.)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx#L137-L137:totalCount={todos.length}로 수정해 주세요.apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx#L96-L96:totalCount={todos.length}로 수정해 주세요.🤖 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 `@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx at line 137, Exclude non-completable calendar events from the total count by changing totalCount to use only todos.length in HomeTodoContainer at apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:137-137 and TodayTodoListContainer at apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx:96-96.
🤖 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
`@apps/timo-web/app/`[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx:
- Around line 117-139: Extract the duplicated onboarding submission flow into a
shared handleCompleteOnboarding function used by both the LifePattern and
CalendarConnect paths, preserving the existing validation, success navigation,
and error-toast behavior. Update the LifePatternStepContainer invocation to pass
the completeOnboarding pending state through its isPending prop so repeated
submissions are prevented.
---
Outside diff comments:
In
`@apps/timo-web/app/`[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:
- Around line 54-56: Memoize the calendarEventsByDate Map in HomeTodoContainer
using React’s useMemo, with calendarEventsData as the dependency so it is
recalculated only when the source data changes. Add the required useMemo import
and preserve the existing fallback and day-to-events mapping behavior.
- Line 51: Update the enabled option using the profile result from
useMyProfileQuery in both HomeTodoContainer.tsx (line 51) and
TodayTodoListContainer.tsx (line 41), accessing calendarConnected through
optional chaining so undefined profile data does not throw.
- Line 137: Exclude non-completable calendar events from the total count by
changing totalCount to use only todos.length in HomeTodoContainer at
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsx:137-137
and TodayTodoListContainer at
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsx:96-96.
In
`@apps/timo-web/app/`[locale]/onboarding/_containers/LifePatternStepContainer.tsx:
- Around line 89-95: Update LifePatternStepContainer to accept an optional
isPending prop from its parent and use it to disable OnboardingButtonContainer
while the completion API request is in progress, alongside the existing
canProceed condition, preventing duplicate submissions.
🪄 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: 3e5b0b43-73c0-48e6-b95f-ccb9d67ea086
📒 Files selected for processing (8)
apps/timo-web/app/[locale]/(main)/(with-time-sidebar)/home/_containers/HomeTodoContainer.tsxapps/timo-web/app/[locale]/(main)/(with-time-sidebar)/today/_containers/TodayTodoListContainer.tsxapps/timo-web/app/[locale]/onboarding/_components/OnboardingStepButton.tsxapps/timo-web/app/[locale]/onboarding/_containers/LifePatternStepContainer.tsxapps/timo-web/app/[locale]/onboarding/_containers/OnboardingFunnelContainer.tsxapps/timo-web/components/calendar/HomeCalendarEventCard.tsxapps/timo-web/components/calendar/TodayCalendarEventCard.tsxapps/timo-web/messages/en.json
| // TODO: 구글 캘린더 연동 단계 재활성화 시 아래 completeOnboarding 블록을 제거하고 | ||
| // history.push("CalendarConnect", { language: answers.language, predictionAccuracy: answers.predictionAccuracy, wakeUpTime: answers.wakeUpTime, bedTime: answers.bedTime }) 로 복구 | ||
| completeOnboarding( | ||
| { | ||
| data: { | ||
| language: ONBOARDING_LANGUAGE_MAP[answers.language], | ||
| predictionAccuracy: answers.predictionAccuracy, | ||
| wakeUpTime: answers.wakeUpTime, | ||
| bedTime: answers.bedTime, | ||
| }, | ||
| }, | ||
| { | ||
| onSuccess: () => { | ||
| setOnboardingCompleted(true); | ||
| router.replace(ROUTES.HOME, { | ||
| locale: answers.language, | ||
| }); | ||
| }, | ||
| onError: () => { | ||
| setIsErrorToastOpen(true); | ||
| }, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
온보딩 완료 로직 함수 분리 및 isPending 전달
완벽하게 작동하는 코드네요! 👍
약간의 개선을 제안하자면, LifePattern 단계와 CalendarConnect 단계의 API 제출 로직이 동일하게 중복되어 있습니다. 이를 공통 함수로 추출하면 코드 유지보수가 훨씬 수월해집니다. 더불어 앞서 제안해 드린 중복 클릭 방지를 위해 LifePatternStepContainer에 isPending을 함께 넘겨주세요.
(참고: React 컴포넌트 합성 및 재사용 문서)
💡 제안하는 수정안 (함수 분리 및 prop 전달)
컴포넌트 상단에 공통 제출 함수를 선언해 주세요:
const handleCompleteOnboarding = () => {
if (
!answers.language ||
!answers.predictionAccuracy ||
!answers.wakeUpTime ||
!answers.bedTime
)
return;
completeOnboarding(
{
data: {
language: ONBOARDING_LANGUAGE_MAP[answers.language],
predictionAccuracy: answers.predictionAccuracy,
wakeUpTime: answers.wakeUpTime,
bedTime: answers.bedTime,
},
},
{
onSuccess: () => {
setOnboardingCompleted(true);
router.replace(ROUTES.HOME, {
locale: answers.language,
});
},
onError: () => {
setIsErrorToastOpen(true);
},
},
);
};그리고 해당 부분을 아래처럼 깔끔하게 교체할 수 있습니다!
- // TODO: 구글 캘린더 연동 단계 재활성화 시 아래 completeOnboarding 블록을 제거하고
- // history.push("CalendarConnect", { language: answers.language, predictionAccuracy: answers.predictionAccuracy, wakeUpTime: answers.wakeUpTime, bedTime: answers.bedTime }) 로 복구
- completeOnboarding(
- {
- data: {
- language: ONBOARDING_LANGUAGE_MAP[answers.language],
- predictionAccuracy: answers.predictionAccuracy,
- wakeUpTime: answers.wakeUpTime,
- bedTime: answers.bedTime,
- },
- },
- {
- onSuccess: () => {
- setOnboardingCompleted(true);
- router.replace(ROUTES.HOME, {
- locale: answers.language,
- });
- },
- onError: () => {
- setIsErrorToastOpen(true);
- },
- },
- );
+ // TODO: 구글 캘린더 연동 단계 재활성화 시 handleCompleteOnboarding 호출을 제거하고
+ // history.push("CalendarConnect", ...) 로 복구
+ handleCompleteOnboarding();마지막으로 상단의 LifePatternStepContainer 호출부에 isPending을 함께 넘겨주시면 완벽합니다!
LifePattern={({ history }) => (
<LifePatternStepContainer
// ... 기존 속성들 ...
isPending={isPending}
onNext={() => {
handleCompleteOnboarding();
}}
/>
)}🤖 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
`@apps/timo-web/app/`[locale]/onboarding/_containers/OnboardingFunnelContainer.tsx
around lines 117 - 139, Extract the duplicated onboarding submission flow into a
shared handleCompleteOnboarding function used by both the LifePattern and
CalendarConnect paths, preserving the existing validation, success navigation,
and error-toast behavior. Update the LifePatternStepContainer invocation to pass
the completeOnboarding pending state through its isPending prop so repeated
submissions are prevented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

ISSUE 🔗
close #187
What is this PR? 🔍
구글 캘린더 OAuth 연동, 일정 조회 API 연동, 캘린더 이벤트 카드 UI를 구현했습니다.
배경
/api/v1/users/calendar/eventsAPI 연동을 통해 캘린더 일정을 홈·오늘 탭에 표시했습니다.OAuth 연동
authorize()API로 OAuth URL을 받아window.location.assign으로 리다이렉트하고, 콜백 페이지(/oauth/calendar/callback)에서 인가 코드를 서버로 전달합니다. 해제 시disconnectCalendar()mutate 후 프로필 쿼리를 invalidate하고 캘린더 이벤트 캐시를removeQueries로 제거합니다.캘린더 이벤트 조회
useCalendarEventsQuery를useQuery로 구현해enabled: calendarConnected조건을 적용했습니다.useSuspenseQuery는enabled옵션을 지원하지 않아useQuery를 선택했습니다. 홈은 뷰 모드에 따라TWO_WEEK/WEEK, 오늘은DAY필터를 사용합니다.staleTime: 0으로 매 마운트마다 최신 데이터를 가져옵니다.캘린더 이벤트 카드
HomeCalendarEventCard, 오늘용TodayCalendarEventCard컴포넌트를 추가했습니다.Map<string, Set<number>>/Set<number>)로 리프팅해completedCount와totalCount계산에 반영했습니다. 카드 내부에는 체크박스와 타이틀 사이에 구글 캘린더 아이콘(14×14, padding 4px)을 삽입했습니다. 체크 시 dimmed 처리(bg-timo-gray-200,text-timo-gray-700), hover 시 undim되는 동작은 기존 투두 카드와 동일합니다.To Reviewers
캘린더 이벤트 카드를 눌렀을 때 상세 모달이 뜨는 기능은 추후에 작업할 예정입니다.
캘린더 연결이 된 상태에서 투두 체크 완료/미완료 계산이 캘린더 이벤트를 포함하는지 확인 부탁드립니다.
Screenshot 📷
Test Checklist ✔