[feature] 관리자 캘린더 연동 탭에 Notion DB 선택/캘린더 표시 기능 추가#1352
[feature] 관리자 캘린더 연동 탭에 Notion DB 선택/캘린더 표시 기능 추가#1352seongwon030 wants to merge 28 commits intodevelop-fefrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
Warning
|
| Cohort / File(s) | Summary |
|---|---|
OAuth 및 클라이언트 API frontend/src/apis/calendarOAuth.ts |
Google Calendar REST 호출(캘린더 목록/기본 이벤트) 및 Notion 연동용 클라이언트 함수(인증 URL 생성, 코드 교환, 페이지/데이터베이스 조회) 추가. 응답 정규화 및 에러 처리 포함. |
관리자 라우트 · 사이드바 frontend/src/pages/AdminPage/AdminRoutes.tsx, frontend/src/pages/AdminPage/components/SideBar/SideBar.tsx |
관리자 라우트 /admin/calendar-sync 추가 및 사이드바에 "동아리 일정 관리" 항목 추가. |
CalendarSync UI · 스타일 frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx, .../CalendarSyncTab.styles.ts |
캘린더 동기화 UI 컴포넌트와 styled-components 추가(설정 패널, 데이터 카드, 캘린더 그리드, 토글 패널 등). |
훅: 통합 · Google · Notion OAuth / 데이터 / UI frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/... |
useCalendarSync, useGoogleCalendarData, useNotionOAuth, useNotionCalendarData, useNotionCalendarUiState 등 OAuth 흐름, 데이터 로드/적용, UI 상태(월 네비게이션/토글) 관리 훅 추가. |
유틸리티 및 단위테스트 frontend/src/utils/calendarSyncUtils.ts, frontend/src/utils/calendarSyncUtils.test.ts |
OAuth 상태 생성, 토큰 마스킹, 날짜 키 변환·월 캘린더 생성, Notion→캘린더 이벤트 파싱 유틸 및 해당 Jest 테스트 추가. |
Club 캘린더 통합 frontend/src/apis/club.ts, frontend/src/hooks/Queries/useClub.ts, frontend/src/types/club.ts, frontend/src/constants/queryKeys.ts |
클럽 캘린더 엔드포인트 클라이언트(getClubCalendarEvents), 타입 ClubCalendarEvent, React Query 훅 useGetClubCalendarEvents, query key 추가. |
ClubDetail UI: 스케줄 탭 및 컴포넌트 frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx, frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/... |
ClubDetail에 SCHEDULE 탭 추가 및 ClubScheduleCalendar 컴포넌트·스타일 추가(월 뷰 캘린더·이벤트 리스트). |
이벤트 상수 frontend/src/constants/eventName.ts |
사용자 이벤트 상수에 USER_EVENT.CLUB_SCHEDULE_TAB_CLICKED 추가. |
Sequence Diagram(s)
sequenceDiagram
participant User as 사용자
participant UI as CalendarSyncTab UI
participant GoogleHook as useGoogleCalendarData
participant Session as sessionStorage
participant GoogleOAuth as Google OAuth
participant GoogleAPI as Google Calendar API
User->>UI: "Google 인증" 버튼 클릭
UI->>GoogleHook: startGoogleOAuth()
GoogleHook->>GoogleHook: createState()
GoogleHook->>Session: state 저장
GoogleHook->>GoogleOAuth: 인증 URL로 리다이렉트
GoogleOAuth->>User: 로그인/권한 요청
User->>GoogleOAuth: 승인
GoogleOAuth->>UI: callback (access_token, state)
UI->>GoogleHook: 콜백 처리
GoogleHook->>Session: state 검증 및 token 저장
GoogleHook->>GoogleAPI: fetchGoogleCalendarList(token)
GoogleAPI->>GoogleHook: 캘린더 목록 반환
GoogleHook->>GoogleAPI: fetchGooglePrimaryEvents(token)
GoogleAPI->>GoogleHook: 이벤트 반환
GoogleHook->>UI: 상태 업데이트 (캘린더/이벤트)
sequenceDiagram
participant User as 사용자
participant UI as CalendarSyncTab UI
participant NotionHook as useNotionOAuth
participant Session as sessionStorage
participant Backend as Backend 엔드포인트
participant NotionOAuth as Notion OAuth
participant NotionAPI as Notion API
User->>UI: "Notion 인증" 버튼 클릭
UI->>NotionHook: startNotionOAuth()
NotionHook->>NotionHook: createState()
NotionHook->>Session: state 저장
NotionHook->>Backend: fetchNotionAuthorizeUrl(state)
Backend->>NotionHook: 인증 URL 반환
NotionHook->>NotionOAuth: 리다이렉트
NotionOAuth->>User: 승인 요청
User->>NotionOAuth: 승인
NotionOAuth->>UI: callback (code, state)
UI->>NotionHook: 콜백 처리
NotionHook->>Session: state 검증
NotionHook->>Backend: exchangeNotionCode({code})
Backend->>NotionAPI: code 교환
NotionAPI->>Backend: 토큰/정보 반환
Backend->>NotionHook: 성공 응답
NotionHook->>Backend: fetchNotionPages()
Backend->>NotionHook: 정규화된 페이지 응답
NotionHook->>UI: notionItems 업데이트
sequenceDiagram
participant UI as CalendarSyncTab UI
participant UiState as useNotionCalendarUiState
participant Utils as calendarSyncUtils
UI->>UiState: notionItems 전달
UiState->>Utils: parseNotionCalendarEvent(item)
Utils->>UiState: NotionCalendarEvent 반환
UiState->>UiState: 이벤트 정렬·활성화 맵 생성
UiState->>Utils: buildMonthCalendarDays(visibleMonth)
Utils->>UiState: 날짜 배열 반환
UiState->>UI: 렌더링용 상태 반환
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
- [release] FE v1.1.19 #1106: ClubScheduleCalendar 컴포넌트·스타일 및
ClubCalendarEvent타입 추가와 직접적인 중복 가능성. - [release] FE v1.1.6 릴리즈 #842: 관리자 라우트/사이드바 및 관련 UI 변경과 연관 가능성(관리자 네비게이션 변경).
- [feature] 동아리 상세페이지 > 공통 레이아웃(UI 틀) 구현 #964: 이벤트 상수 변경(
USER_EVENT)과 중복 또는 충돌 가능성.
Suggested labels
📬 API, ✅ Test
Suggested reviewers
- oesnuj
- lepitaaar
- suhyun113
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | 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 (2 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | 제목이 PR의 주요 변경 사항을 명확하게 요약하고 있습니다. 'Notion DB 선택/캘린더 표시 기능 추가'는 관리자 캘린더 연동 탭의 핵심 기능을 정확하게 설명합니다. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
feature/#1350-calendar-fe-MOA-759
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 @coderabbitai help to get the list of available commands and usage tips.
🎨 UI 변경사항을 확인해주세요
2개 스토리 변경 · 전체 56개 스토리 · 22개 컴포넌트 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
frontend/src/apis/calendarOAuth.ts (2)
38-54: Google API 호출에 타임아웃 처리가 없습니다.외부 API 호출 시 네트워크 지연이나 장애 상황에 대비한 타임아웃 설정을 고려해보세요.
AbortController를 사용하여 요청 타임아웃을 구현할 수 있습니다.♻️ 타임아웃 적용 예시
export const fetchGoogleCalendarList = async (accessToken: string) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + const response = await fetch( 'https://www.googleapis.com/calendar/v3/users/me/calendarList', { headers: { Authorization: `Bearer ${accessToken}`, }, + signal: controller.signal, }, ); + clearTimeout(timeoutId); if (!response.ok) { throw new Error('Google 캘린더 목록 조회에 실패했습니다.'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/apis/calendarOAuth.ts` around lines 38 - 54, fetchGoogleCalendarList lacks a request timeout; wrap the fetch call in an AbortController with a configurable timeout (e.g., 5–10s), call controller.abort() when the timer elapses, and pass signal: controller.signal to fetch. Update error handling in fetchGoogleCalendarList to treat an aborted request distinctly (throw a timeout-specific Error) and ensure the timeout is cleared on success or failure; reference the fetchGoogleCalendarList function and the created AbortController/signal when making these changes.
173-193: Notion 응답 정규화 로직이 중복되어 있습니다.
fetchNotionPages와fetchNotionDatabasePages에서 응답을NotionPagesResponse로 변환하는 로직이 거의 동일합니다. 헬퍼 함수로 추출하면 유지보수성이 향상됩니다.♻️ 중복 제거를 위한 헬퍼 함수 추출
const normalizeNotionPagesResponse = ( data: NotionPagesPayload | NotionSearchItem[] | undefined, fallbackDatabaseId?: string, ): NotionPagesResponse => { if (Array.isArray(data)) { return { items: data, totalResults: data.length, databaseId: fallbackDatabaseId, }; } const items = (data?.items ?? data?.results ?? []) as NotionSearchItem[]; const totalResults = data?.total_results ?? data?.totalResults ?? items.length; const databaseId = data?.database_id ?? data?.databaseId ?? fallbackDatabaseId; return { items, totalResults, databaseId }; };Also applies to: 218-241
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/apis/calendarOAuth.ts` around lines 173 - 193, The response-normalization logic duplicated in fetchNotionPages and fetchNotionDatabasePages should be extracted into a single helper (e.g., normalizeNotionPagesResponse) that accepts data: NotionPagesPayload | NotionSearchItem[] | undefined and an optional fallbackDatabaseId, and returns NotionPagesResponse; move the Array.isArray branch and the items/totalResults/databaseId extraction into that helper, then replace the inlined logic in fetchNotionPages and fetchNotionDatabasePages with calls to normalizeNotionPagesResponse(data, databaseId) to eliminate duplication and preserve existing semantics.frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx (1)
206-216: 체크박스 접근성 개선을 고려해보세요.
<input type="checkbox">에 연결된<label>이 있지만, 스크린 리더 사용자를 위해 체크박스에 명시적인aria-label이나id-htmlFor연결을 추가하면 접근성이 향상됩니다.♿ 접근성 개선 예시
{notionCalendarEvents.map((event) => ( - <Styled.ToggleItem key={event.id}> + <Styled.ToggleItem key={event.id} htmlFor={`toggle-${event.id}`}> <Styled.ToggleCheckbox type='checkbox' + id={`toggle-${event.id}`} + aria-label={`${event.title} 이벤트 표시 여부`} checked={notionEventEnabledMap[event.id] !== false} onChange={() => toggleNotionEvent(event.id)} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx` around lines 206 - 216, The checkbox lacks an explicit accessible association: assign a unique id to the Styled.ToggleCheckbox (e.g. `notion-event-${event.id}`) and connect the visible text element to it by using htmlFor on the label element (or convert Styled.ToggleText to a <label> with htmlFor), or if you cannot change the text element to a label, add a descriptive aria-label to Styled.ToggleCheckbox; update the JSX around Styled.ToggleCheckbox/Styled.ToggleText and keep the existing checked={notionEventEnabledMap[event.id] !== false} and onChange={() => toggleNotionEvent(event.id)} behavior.frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts (1)
97-108: 데이터베이스 목록 조회 실패 시 사용자에게 알림 없음
fetchNotionDatabases()호출이 실패해도 빈catch블록으로 인해 사용자는 문제를 인지할 수 없습니다. OAuth 전 단계에서 401/403 오류는 무시하되, 네트워크 오류나 서버 오류(500 등)는 사용자에게 알려주는 것이 좋습니다.♻️ 제안된 수정
useEffect(() => { fetchNotionDatabases() .then((options) => { setNotionDatabaseOptions(options); setSelectedNotionDatabaseId( (previous) => previous || options[0]?.id || '', ); }) - .catch(() => { - // OAuth 전 단계에서는 목록 실패가 자연스러울 수 있다. + .catch((error: unknown) => { + const status = + typeof error === 'object' && + error !== null && + 'status' in error && + typeof (error as { status?: unknown }).status === 'number' + ? (error as { status: number }).status + : undefined; + + // OAuth 전 단계(401/403)에서는 목록 실패가 자연스러울 수 있다. + if (status !== 401 && status !== 403) { + onError((error as Error)?.message ?? 'Notion 데이터베이스 목록 조회에 실패했습니다.'); + } }); - }, []); + }, [onError]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts` around lines 97 - 108, The empty catch on fetchNotionDatabases hides real failures; update the catch to distinguish auth errors from other failures: in the catch for fetchNotionDatabases() check the error/status (e.g., err?.status or err?.response?.status) and if it's 401/403 simply ignore, otherwise call the existing UI error path (show a toast/snackbar or set an error state) so the user is notified; keep existing behavior that still setsNotionDatabaseOptions and setSelectedNotionDatabaseId on success and only notify for non-auth/network/server errors.frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts (1)
83-110: 이벤트 핸들러에useCallback누락
goToPreviousMonth,goToNextMonth,toggleNotionEvent,setAllNotionEventsEnabled함수들이 매 렌더링마다 새로 생성됩니다. 이 훅을 사용하는 컴포넌트에서 이 함수들을 자식 컴포넌트에 전달하면 불필요한 리렌더링이 발생할 수 있습니다.♻️ useCallback 적용 예시
- const goToPreviousMonth = () => { + const goToPreviousMonth = useCallback(() => { setVisibleMonth( - new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1), + (prev) => new Date(prev.getFullYear(), prev.getMonth() - 1, 1), ); - }; + }, []); - const goToNextMonth = () => { + const goToNextMonth = useCallback(() => { setVisibleMonth( - new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1), + (prev) => new Date(prev.getFullYear(), prev.getMonth() + 1, 1), ); - }; + }, []); - const toggleNotionEvent = (id: string) => { + const toggleNotionEvent = useCallback((id: string) => { setNotionEventEnabledMap((previous) => ({ ...previous, [id]: !(previous[id] ?? true), })); - }; + }, []); - const setAllNotionEventsEnabled = (enabled: boolean) => { + const setAllNotionEventsEnabled = useCallback((enabled: boolean) => { setNotionEventEnabledMap((previous) => { const next = { ...previous }; notionCalendarEvents.forEach((event) => { next[event.id] = enabled; }); return next; }); - }; + }, [notionCalendarEvents]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts` around lines 83 - 110, The handlers goToPreviousMonth, goToNextMonth, toggleNotionEvent, and setAllNotionEventsEnabled are recreated every render causing extra child re-renders; wrap each in React.useCallback with appropriate dependency arrays: use callbacks for goToPreviousMonth/goToNextMonth depending on visibleMonth (or use functional state updater to avoid visibleMonth in deps) and for toggleNotionEvent/setAllNotionEventsEnabled depend on setNotionEventEnabledMap and notionCalendarEvents (or memoize notionCalendarEvents if needed); ensure you reference the exact function names (goToPreviousMonth, goToNextMonth, toggleNotionEvent, setAllNotionEventsEnabled) and preserve existing behavior and state updates when adding useCallback.frontend/src/utils/calendarSyncUtils.ts (1)
74-78:dateFromKey입력 유효성 검증 부재잘못된 형식의
dateKey가 전달되면split('-').map(Number)가NaN값을 반환하여 잘못된Date객체가 생성될 수 있습니다. 이 함수는parseDateKey의 결과를 사용하므로 실제로는 문제가 발생하지 않을 가능성이 높지만, 방어적 코딩을 고려해 볼 수 있습니다.🛡️ 방어적 검증 추가 예시
/** `YYYY-MM-DD` 키 문자열을 Date 객체(로컬 시간대)로 변환한다. */ export const dateFromKey = (dateKey: string) => { const [year, month, day] = dateKey.split('-').map(Number); + if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) { + throw new Error(`Invalid dateKey format: ${dateKey}`); + } return new Date(year, month - 1, day); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/utils/calendarSyncUtils.ts` around lines 74 - 78, dateFromKey lacks input validation and will produce an invalid Date when given malformed dateKey; update dateFromKey to validate the incoming dateKey (e.g., assert it matches /^\d{4}-\d{2}-\d{2}$/), parse year/month/day, verify year/month/day are finite numbers and month in 1..12 and day in 1..31 (and optionally validate day vs month/year), and if validation fails either throw a descriptive Error or return null/undefined per project convention so callers of dateFromKey (and any code relying on it) don’t receive an invalid Date object.frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts (1)
30-32: 토큰 만료 처리 고려 필요Google 액세스 토큰은 약 1시간 후 만료됩니다. 현재 구현은 세션 스토리지에서 토큰을 복원하지만, 만료된 토큰으로 API 호출 시 실패할 수 있습니다. 장시간 세션에서는 재인증이 필요할 수 있습니다.
향후 개선 사항으로, API 호출 실패 시(401 응답) 토큰을 삭제하고 재인증을 유도하는 로직을 추가하는 것을 고려해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts` around lines 30 - 32, The hook useGoogleCalendarData currently restores googleToken from sessionStorage (GOOGLE_TOKEN_KEY) but doesn't handle expired tokens; update API call handlers inside useGoogleCalendarData to detect 401/unauthorized responses, remove the stale token from sessionStorage (sessionStorage.removeItem(GOOGLE_TOKEN_KEY)), call setGoogleToken('') to update state, and surface an auth-required signal (e.g., invoke an onAuthRequired callback or set a flag in the hook) so the UI can prompt reauthentication; ensure all fetch/axios calls in the hook check for 401 and run this cleanup path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts`:
- Around line 66-87: The OAuth callback handling in the useEffect inside
useGoogleCalendarData ignores an error param in the URL hash; update the logic
in that useEffect to parse params.get('error') and, if present, call the
existing error reporting path (e.g., invoke clearError? or better: set an error
via the same mechanism you use elsewhere — call onStatus with a clear failure
message and/or set an explicit error state), ensure you do not store a token or
overwrite sessionStorage keys (do not call setGoogleToken or
sessionStorage.setItem(GOOGLE_TOKEN_KEY, ...) when error exists), and still
perform the cleanup with window.history.replaceState; reference the useEffect,
params.get('error'), setGoogleToken, GOOGLE_STATE_KEY, GOOGLE_TOKEN_KEY,
onStatus and clearError when making the change.
In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts`:
- Around line 64-79: After the Notion OAuth flow completes (in the promise chain
that calls exchangeNotionCode, onWorkspaceName, loadNotionPages, onStatus,
onError, and setIsNotionOAuthLoading), remove the stale OAuth state from
sessionStorage in the .finally() block so returning to the page won't reuse it;
call sessionStorage.removeItem using the same state key (or state variable) that
was set earlier in this hook (e.g., the notion OAuth state key name) immediately
before or after window.history.replaceState.
- Around line 45-80: The effect in useNotionOAuth uses clearError which is
recreated each render, causing unnecessary re-runs; memoize clearError with
useCallback where it’s defined (so it has stable identity) or, if safe, remove
clearError from the useEffect dependency array and ensure you still call it
before starting exchangeNotionCode; update the useEffect dependency list to
reference the stable function (the memoized clearError) along with
loadNotionPages, onError, onStatus, onWorkspaceName, and keep NOTION_STATE_KEY,
exchangeNotionCode and related logic unchanged.
---
Nitpick comments:
In `@frontend/src/apis/calendarOAuth.ts`:
- Around line 38-54: fetchGoogleCalendarList lacks a request timeout; wrap the
fetch call in an AbortController with a configurable timeout (e.g., 5–10s), call
controller.abort() when the timer elapses, and pass signal: controller.signal to
fetch. Update error handling in fetchGoogleCalendarList to treat an aborted
request distinctly (throw a timeout-specific Error) and ensure the timeout is
cleared on success or failure; reference the fetchGoogleCalendarList function
and the created AbortController/signal when making these changes.
- Around line 173-193: The response-normalization logic duplicated in
fetchNotionPages and fetchNotionDatabasePages should be extracted into a single
helper (e.g., normalizeNotionPagesResponse) that accepts data:
NotionPagesPayload | NotionSearchItem[] | undefined and an optional
fallbackDatabaseId, and returns NotionPagesResponse; move the Array.isArray
branch and the items/totalResults/databaseId extraction into that helper, then
replace the inlined logic in fetchNotionPages and fetchNotionDatabasePages with
calls to normalizeNotionPagesResponse(data, databaseId) to eliminate duplication
and preserve existing semantics.
In `@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsx`:
- Around line 206-216: The checkbox lacks an explicit accessible association:
assign a unique id to the Styled.ToggleCheckbox (e.g.
`notion-event-${event.id}`) and connect the visible text element to it by using
htmlFor on the label element (or convert Styled.ToggleText to a <label> with
htmlFor), or if you cannot change the text element to a label, add a descriptive
aria-label to Styled.ToggleCheckbox; update the JSX around
Styled.ToggleCheckbox/Styled.ToggleText and keep the existing
checked={notionEventEnabledMap[event.id] !== false} and onChange={() =>
toggleNotionEvent(event.id)} behavior.
In
`@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts`:
- Around line 30-32: The hook useGoogleCalendarData currently restores
googleToken from sessionStorage (GOOGLE_TOKEN_KEY) but doesn't handle expired
tokens; update API call handlers inside useGoogleCalendarData to detect
401/unauthorized responses, remove the stale token from sessionStorage
(sessionStorage.removeItem(GOOGLE_TOKEN_KEY)), call setGoogleToken('') to update
state, and surface an auth-required signal (e.g., invoke an onAuthRequired
callback or set a flag in the hook) so the UI can prompt reauthentication;
ensure all fetch/axios calls in the hook check for 401 and run this cleanup
path.
In
`@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.ts`:
- Around line 97-108: The empty catch on fetchNotionDatabases hides real
failures; update the catch to distinguish auth errors from other failures: in
the catch for fetchNotionDatabases() check the error/status (e.g., err?.status
or err?.response?.status) and if it's 401/403 simply ignore, otherwise call the
existing UI error path (show a toast/snackbar or set an error state) so the user
is notified; keep existing behavior that still setsNotionDatabaseOptions and
setSelectedNotionDatabaseId on success and only notify for
non-auth/network/server errors.
In
`@frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.ts`:
- Around line 83-110: The handlers goToPreviousMonth, goToNextMonth,
toggleNotionEvent, and setAllNotionEventsEnabled are recreated every render
causing extra child re-renders; wrap each in React.useCallback with appropriate
dependency arrays: use callbacks for goToPreviousMonth/goToNextMonth depending
on visibleMonth (or use functional state updater to avoid visibleMonth in deps)
and for toggleNotionEvent/setAllNotionEventsEnabled depend on
setNotionEventEnabledMap and notionCalendarEvents (or memoize
notionCalendarEvents if needed); ensure you reference the exact function names
(goToPreviousMonth, goToNextMonth, toggleNotionEvent, setAllNotionEventsEnabled)
and preserve existing behavior and state updates when adding useCallback.
In `@frontend/src/utils/calendarSyncUtils.ts`:
- Around line 74-78: dateFromKey lacks input validation and will produce an
invalid Date when given malformed dateKey; update dateFromKey to validate the
incoming dateKey (e.g., assert it matches /^\d{4}-\d{2}-\d{2}$/), parse
year/month/day, verify year/month/day are finite numbers and month in 1..12 and
day in 1..31 (and optionally validate day vs month/year), and if validation
fails either throw a descriptive Error or return null/undefined per project
convention so callers of dateFromKey (and any code relying on it) don’t receive
an invalid Date object.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 562107fc-842f-4e62-a587-802b6ab42a35
📒 Files selected for processing (12)
frontend/src/apis/calendarOAuth.tsfrontend/src/pages/AdminPage/AdminRoutes.tsxfrontend/src/pages/AdminPage/components/SideBar/SideBar.tsxfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.styles.tsfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/CalendarSyncTab.tsxfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useCalendarSync.tsfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.tsfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarData.tsfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionCalendarUiState.tsfrontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.tsfrontend/src/utils/calendarSyncUtils.test.tsfrontend/src/utils/calendarSyncUtils.ts
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts
Show resolved
Hide resolved
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts
Show resolved
Hide resolved
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts
Show resolved
Hide resolved
lepitaaar
left a comment
There was a problem hiding this comment.
<역할>당신은 코드 리뷰 전문가입니다.</역할>
<컨텍스트>제공된 [코드 조각]에 대해 종합적인 리뷰를 수행하세요.</컨텍스트>
<단계>1. 코드의 효율성, 가독성, 유지보수성을 평가하세요. 2. 버그, 보안 문제, 성능 병목현상을 식별하세요. 3. 개선을 위한 실질적인 제안을 제공하세요.</단계>
아르키 프론트엔드 시니어 코드리뷰(자동, 테스트 제외)
- 조건: open + base=
develop-fe+ label=💻 FE
- 효율성/가독성/유지보수성 평가
- 컴포넌트 책임 분리, 상태/렌더링 일관성, 재사용성, 의존성 관리 관점으로 평가했습니다.
- 버그/보안/성능 리스크 식별
-
- [Medium] useEffect 의존성 배열 점검 필요: stale closure/미반영 상태 가능성
-
- [Medium] 브라우저 전역 객체 접근(localStorage/window): SSR 환경 가드 여부 확인 필요
- 개선 제안
- XSS/신뢰경계 처리 및 SSR 가드를 명확히 하고, effect cleanup/의존성 배열을 엄격히 관리하세요.
- 리스트 key 안정성 보장 및 불필요 렌더링 방지(메모이제이션/분리)를 적용하세요.
- UI 상태 전이(로딩/에러/빈값)를 표준화해 예외 흐름을 예측 가능하게 만드세요.
lepitaaar
left a comment
There was a problem hiding this comment.
핵심 리뷰
- [Medium] frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts | @@ -0,0 +1,119 @@ | useEffect 의존성 배열 점검 필요. stale closure/누락 의존성 확인 권장
- [Medium] frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts | @@ -0,0 +1,119 @@ | SSR 환경 가드 필요(typeof window !== 'undefined' 등)
수정 위치는 각 항목의 '파일 | @@ hunk @@' 기준으로 반영하면 됩니다.
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useGoogleCalendarData.ts
Show resolved
Hide resolved
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts
Show resolved
Hide resolved
lepitaaar
left a comment
There was a problem hiding this comment.
좋은 기능 추가입니다. 다만 현재 상태로는 머지 리스크가 있어 변경 요청드립니다.
핵심:
- 의 로컬 타임존 정규화로 인해 ISO datetime 입력 시 날짜가 하루 이동할 수 있습니다(인라인 코멘트 참고).
- Google/Notion OAuth state를 검증 후 제거하지 않아 stale/replay 가능 상태가 남습니다(인라인 코멘트 2건 참고).
- 정적분석이 정책에서 실패 중입니다. 특히 의 경고는 머지 블로커로 보입니다.
위 3개 정리되면 재검토 후 approve 하겠습니다.
lepitaaar
left a comment
There was a problem hiding this comment.
좋은 기능 추가입니다. 다만 현재 상태로는 머지 리스크가 있어 변경 요청드립니다.
핵심:
- parseDateKey의 로컬 타임존 정규화로 인해 ISO datetime 입력 시 날짜가 하루 이동할 수 있습니다(인라인 코멘트 참고).
- Google/Notion OAuth state를 검증 후 제거하지 않아 stale/replay 가능 상태가 남습니다(인라인 코멘트 2건 참고).
- 정적분석이 max-warnings=0 정책에서 실패 중입니다. 특히 webviewBridge.ts의 no-console 경고는 머지 블로커로 보입니다.
위 3개 정리되면 재검토 후 approve 하겠습니다.
lepitaaar
left a comment
There was a problem hiding this comment.
재리뷰 완료했습니다.
이번 변경에서 관리자 캘린더 동기화 플로우(구글/노션 OAuth, DB 선택, 캘린더 렌더링) 전반은 기능적으로 일관되고, 구조 분리(apis/hooks/ui)도 유지보수 관점에서 괜찮습니다. 현재 기준으로 머지 가능하다고 판단하여 Approve합니다.
비차단(후속 개선 권장):
useNotionOAuth: OAuth 완료 후NOTION_STATE_KEY정리(sessionStorage remove)useGoogleCalendarData: OAuth hash의error파라미터 처리 및 401 시 토큰 정리/재인증 유도useNotionCalendarData: DB 목록 조회 실패 catch에서 401/403 외 에러 사용자 노출
위 3개는 안정성 개선용 follow-up으로 별도 PR에서 다루면 충분합니다.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/utils/calendarSyncUtils.test.ts`:
- Around line 1-14: The file frontend/src/utils/calendarSyncUtils.test.ts fails
Prettier formatting; run the formatter to fix spacing and import ordering issues
(or apply the project's Prettier rules) so CI passes—e.g., run npx prettier
--write "frontend/src/utils/calendarSyncUtils.test.ts" or reformat the import
block that lists NotionSearchItem and the functions buildDateKeyFromDate,
buildDefaultRedirectUri, buildMonthCalendarDays, createState, dateFromKey,
formatDateText, formatMonthLabel, maskToken, parseDateKey,
parseNotionCalendarEvent, and WEEKDAY_LABELS to match project Prettier settings.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3fa951ff-0a5b-44d1-85b0-159af9f3f08b
📒 Files selected for processing (3)
frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.tsfrontend/src/utils/calendarSyncUtils.test.tsfrontend/src/utils/calendarSyncUtils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/pages/AdminPage/tabs/CalendarSyncTab/hooks/useNotionOAuth.ts
- frontend/src/utils/calendarSyncUtils.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/src/utils/calendarSyncUtils.test.ts (1)
82-154:parseNotionCalendarEvent의 “키 이름 무관” 동작을 명시적으로 검증하면 더 안전합니다.현재 테스트는
날짜,이름같은 의미 있는 키를 사용해, 구현의 핵심인type기반 탐색(키 비의존)을 직접 보장하지 못합니다. 랜덤 키 케이스 1개만 추가해도 회귀 방지 효과가 큽니다.✅ 제안 테스트(diff)
describe('Notion 이벤트 변환', () => { @@ it('날짜/제목 속성이 있으면 캘린더 이벤트로 변환한다', () => { @@ }); + + it('속성 키 이름과 무관하게 type 기준으로 파싱한다', () => { + const item = createNotionItem({ + properties: { + any_date_key: { + type: 'date', + date: { + start: '2026-03-19', + end: null, + }, + }, + any_title_key: { + type: 'title', + title: [{ plain_text: '키 무관 제목' }], + }, + } as NotionSearchItem['properties'], + }); + + const event = parseNotionCalendarEvent(item); + expect(event).not.toBeNull(); + expect(event?.title).toBe('키 무관 제목'); + expect(event?.dateKey).toBe('2026-03-19'); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/utils/calendarSyncUtils.test.ts` around lines 82 - 154, The tests assume the Notion property keys are named '날짜' and '이름' which doesn't verify parseNotionCalendarEvent actually finds properties by type rather than by key name; add a test case that builds a NotionSearchItem whose date and title properties use non-semantic/random keys (e.g., 'fooDateKey' / 'barTitleKey') but keep the same property shapes (type: 'date' and type: 'title') and assert parseNotionCalendarEvent returns the expected event (id, title fallback, and dateKey), referencing parseNotionCalendarEvent and the helper createNotionItem to create that item so the test guarantees key-name independence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@frontend/src/utils/calendarSyncUtils.test.ts`:
- Around line 82-154: The tests assume the Notion property keys are named '날짜'
and '이름' which doesn't verify parseNotionCalendarEvent actually finds properties
by type rather than by key name; add a test case that builds a NotionSearchItem
whose date and title properties use non-semantic/random keys (e.g., 'fooDateKey'
/ 'barTitleKey') but keep the same property shapes (type: 'date' and type:
'title') and assert parseNotionCalendarEvent returns the expected event (id,
title fallback, and dateKey), referencing parseNotionCalendarEvent and the
helper createNotionItem to create that item so the test guarantees key-name
independence.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1310ee6c-41b0-4244-b13c-b38e2dbd808a
📒 Files selected for processing (1)
frontend/src/utils/calendarSyncUtils.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (2)
frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx (1)
94-99:visibleMonth초기값이events변경 시 동기화되지 않습니다.
useState의 초기값 함수는 최초 렌더링에서만 실행됩니다. 부모에서eventsprop이 나중에 로드되거나 변경되어도visibleMonth는 초기값을 유지합니다.현재 사용 패턴(탭 전환 시 컴포넌트가 마운트/언마운트)에서는 문제없지만, 향후
events가 동적으로 변경될 수 있다면useEffect로 동기화를 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx` around lines 94 - 99, visibleMonth is initialized from parsedEvents once via useState and won't update when parsedEvents changes; add a useEffect that watches parsedEvents (or events) and calls setVisibleMonth with new Date(firstEventMonth.getFullYear(), firstEventMonth.getMonth(), 1) where firstEventMonth is computed with dateFromKey(parsedEvents[0].dateKey) or fallback new Date(), so visibleMonth stays in sync when parsedEvents updates; reference visibleMonth, setVisibleMonth, parsedEvents, and dateFromKey to locate where to add the effect.frontend/src/hooks/Queries/useClub.ts (1)
53-60: 선택적 필드(description,url)는 검증되지 않습니다.
select필터가id,title,start만 검증하고 있어, API 응답에서description이나url이 잘못된 타입(예: 숫자)으로 오는 경우 필터를 통과합니다. 다만ClubScheduleCalendar에서 조건부 렌더링(event.description && ...)으로 처리하고 있어 실질적 영향은 낮습니다.현재 구현으로도 무방하지만, 방어적으로 타입 검증을 추가할 수도 있습니다:
♻️ 선택적 필드 검증 추가 (옵션)
select: (data) => data.filter( (event): event is ClubCalendarEvent => !!event && typeof event.id === 'string' && typeof event.title === 'string' && - typeof event.start === 'string', + typeof event.start === 'string' && + (event.description === undefined || typeof event.description === 'string') && + (event.url === undefined || typeof event.url === 'string'), ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/hooks/Queries/useClub.ts` around lines 53 - 60, The select predicate in useClub.ts currently only validates id, title, and start; extend the filter in the select function to defensively validate optional fields description and url by allowing them only if they're undefined/null or strings (e.g., check (event.description === undefined || event.description === null || typeof event.description === 'string') and similarly for event.url) so the predicate still narrows to ClubCalendarEvent; locate and update the select: (data) => data.filter(...) block to include these additional checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@frontend/src/hooks/Queries/useClub.ts`:
- Around line 53-60: The select predicate in useClub.ts currently only validates
id, title, and start; extend the filter in the select function to defensively
validate optional fields description and url by allowing them only if they're
undefined/null or strings (e.g., check (event.description === undefined ||
event.description === null || typeof event.description === 'string') and
similarly for event.url) so the predicate still narrows to ClubCalendarEvent;
locate and update the select: (data) => data.filter(...) block to include these
additional checks.
In
`@frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx`:
- Around line 94-99: visibleMonth is initialized from parsedEvents once via
useState and won't update when parsedEvents changes; add a useEffect that
watches parsedEvents (or events) and calls setVisibleMonth with new
Date(firstEventMonth.getFullYear(), firstEventMonth.getMonth(), 1) where
firstEventMonth is computed with dateFromKey(parsedEvents[0].dateKey) or
fallback new Date(), so visibleMonth stays in sync when parsedEvents updates;
reference visibleMonth, setVisibleMonth, parsedEvents, and dateFromKey to locate
where to add the effect.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2aa41630-11d1-481c-8cb8-63dc5048604b
📒 Files selected for processing (8)
frontend/src/apis/club.tsfrontend/src/constants/eventName.tsfrontend/src/constants/queryKeys.tsfrontend/src/hooks/Queries/useClub.tsfrontend/src/pages/ClubDetailPage/ClubDetailPage.tsxfrontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.tsfrontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsxfrontend/src/types/club.ts
✅ Files skipped from review due to trivial changes (1)
- frontend/src/types/club.ts
- src/types/google.ts: GoogleCalendarItem, GoogleEventItem - src/types/notion.ts: NotionSearchItem, NotionDatabaseOption, NotionPagesResponse - calendarOAuth.ts에서 re-export하여 기존 import 호환성 유지
- OAuth 에러 시 sessionStorage에서 state 제거 - OAuth 완료 시 finally에서 state 제거 - stale state로 인한 예기치 않은 동작 방지
- 순수 날짜(YYYY-MM-DD)만 그대로 반환하도록 정규식 수정 - datetime 문자열은 UTC 기준으로 파싱하여 날짜 추출 - 타임존에 따른 날짜 밀림 문제 방지
- OAuth 성공 시 GOOGLE_STATE_KEY 즉시 제거 - replay 공격 방지 및 fresh state 보장
- hasCalendarEvents 플래그로 캘린더 이벤트 유무 확인 - tabs, topBarTabs를 useMemo로 조건부 생성 - 일정 탭 컨텐츠도 조건부 렌더링
- hasCalendarEvents 플래그로 탭 표시 여부 결정 - 일정 탭 클릭 시에만 캘린더 API 호출 (lazy loading) - staleTime 1분 → 5분으로 증가하여 불필요한 재요청 방지
#️⃣연관된 이슈
변경 내용
관리자
CalendarSyncTab에 Google/Notion 캘린더 연동 UI를 정리했습니다.Notion 연동 흐름을 확장했습니다.
GET /api/integration/notion/databasesGET /api/integration/notion/databases/{databaseId}/pagesGET /api/integration/notion/pages로직 분리:
useGoogleCalendarDatauseNotionOAuthuseNotionCalendarDatauseNotionCalendarUiStateuseCalendarSync유틸 분리 및 문서화:
src/utils/calendarSyncUtils.tssrc/utils/calendarSyncUtils.test.ts테스트 추가중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항
Summary by CodeRabbit
새로운 기능
테스트