Skip to content

[Deploy] develop → main 배포 반영#288

Merged
YermIm merged 83 commits into
mainfrom
develop
Jul 9, 2026
Merged

[Deploy] develop → main 배포 반영#288
YermIm merged 83 commits into
mainfrom
develop

Conversation

@YermIm

@YermIm YermIm commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

🚨 관련 이슈

N/A

✨ 변경사항

  • 🐞 BugFix Something isn't working
  • 💻 CrossBrowsing Browser compatibility
  • 🌏 Deploy Deploy
  • 🎨 Design Markup & styling
  • 📃 Docs Documentation writing and editing (README.md, etc.)
  • ✨ Feature Feature
  • 🔨 Refactor Code refactoring
  • ⚙️ Setting Development environment setup
  • ✅ Test Test related (storybook, jest, etc.)

✏️ 작업 내용

N/A

😅 미완성 작업

N/A

📢 논의 사항 및 참고 사항

N/A

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • New Features
    • 타임라인 관리 화면이 추가되어 생성, 수정, 삭제, 요약 요청, 목록/상세 조회를 지원합니다.
    • 네이버 연동에 재연동 및 해제 흐름이 추가되고, 메타 OAuth 결과 처리 화면이 제공됩니다.
    • 드롭다운, 에러 폴백, 타임라인 빈 상태/범례/스켈레톤 등 공통 UI가 확장되었습니다.
  • Bug Fixes
    • 차트와 대시보드 주요 영역에 오류 대체 화면을 적용해 더 안정적으로 표시됩니다.
    • 랜딩 및 타임라인 화면의 레이아웃과 상호작용이 개선되었습니다.

jjjsun and others added 30 commits June 25, 2026 01:15
AI 요약하기 버튼과 워크스페이스 동그란 버튼에 primary-300 → primary-500 그라데이션 적용
- 4개 섹션으로 재구성: 플랫폼 연동 → 통합 대시보드 → 캠페인 관리 → 팀 협업
- 피처 카드 설명 제거 및 체크리스트 스타일로 변경
- 뱃지 디자인 소프트 tint 스타일로 변경 (bg-primary-100 + text-primary-500)
- 목업 컬럼 비율 조정 (3/5 → 1/2)
- GuideOverviewChart: 플랫폼 드롭다운 토글 추가 및 색상 동적 변경
- GuidePlatform: 플랫폼 연동 카드 UI로 교체
- GuideWorkspace: 팀원 초대 모달 디자인 기반 신규 목업 추가
[Feature/#263] 타임라인 생성 모달 UI 구현
[Feature/#265] 타임라인 페이지 레이아웃 조립 및 디자인 수정 및 UI 구현
- GuideOverviewChart: 플랫폼별 series 맵 도입, 색상 토큰 적용, legend 및 sr-only 동적화
- GuidePlatform: 목업 연동하기 버튼 비활성 스타일로 교체
- GuideWorkspace: 목업 루트에 aria-hidden 추가
- LandingFeatures: 인라인 gradient style을 Tailwind bg-linear-to-br 유틸리티로 교체
[Refactor/#230] Landing Page 플랫폼 변경 및 요소 추가
rounded-[8px]→rounded-lg, rounded-[6px]→rounded-md, h-[28px]→h-7,
w-[55px]→w-13.75, rounded-[12px]→rounded-xl, h-[52px]→h-13,
w-[4px]→w-1, h-[30px]→h-7.5
토글 없이 항상 펼쳐진 형태로 바꾸고, 카드형 테두리 대신 얇은 구분선으로 스타일 단순화
- GuidePlatform/Timeline/Workspace/OverviewChart 높이를 h-85(340px)로 통일해 텍스트 대비 목업이 과도하게 커 보이던 문제 개선
- OverviewChart y축 그리드라인을 6개에서 3개로 줄여 축소된 높이에서도 답답하지 않게 조정
- GuidePlatform에 NAVER 연동 완료 예시 상태(Badge)를 추가해 미연동 카드들과 시각적으로 구분
세로 스크롤 구조를 유지하면서, 화면 우측에 01~04 진행 인디케이터를 sticky하게 표시해 현재 보고 있는 스텝을 안내
"Google·Meta 파트너 서비스로..." 대신 매체별로 따로 확인하던 불편함을 강조하는 문구로 교체
- LandingHeader: sticky → fixed, 스크롤 비례 진행도(progress 0-1)로 배경
  투명도·backdrop-blur·보더·텍스트·로고 색상을 실시간 연속 보간
  (ease-out quadratic 곡선, RAF 쓰로틀)
- LandingHeader: 로고를 컬러/흰색 두 이미지 교차 페이드로 전환
- LandingHeader: Bigin 스타일 [로고+네비] / [로그인·CTA] 2그룹 레이아웃
  및 max-w-7xl 컨테이너 좌우 여백 적용
- LandingHero: id="hero" 추가(IntersectionObserver 타깃),
  콘텐츠 높이 min-h-dvh로 확장해 투명 헤더 오버랩 공간 확보
- 네비게이션 링크에 rounded-full 호버 필 배경 추가
  (progress 기반 pillBg로 dark/light 모드 자동 대응)
- 텍스트 opacity-90 → group-hover:opacity-100으로 살짝 밝아지는 효과
- 회원가입 버튼을 진한 파란 배경에서 네비게이션 필 스타일로 변경
  (기본 pillBg 표시, 호버 시 동일 레이어 중첩으로 자연스럽게 진해짐)
- 회원가입 텍스트 font-label(14px/600)로 크기·굵기 조정
- CTA 그룹 gap gap-2 md:gap-3 → gap-1로 통일
jjjsun and others added 25 commits July 5, 2026 04:31
[Feature/#271] 타임라인 조회/생성 API 연동 및 mock 데이터 교체
[Feature/#282] 타임라인 수정/삭제/AI 요약 API 연동
[Feature/#285] 타임라인 상세조회 API 응답에 comparisonPeriodType 반영 및 수정 prefilling
[Feature/#267] 플랫폼 연동 플로우 (Naver·Meta·Google 복귀·연동 해제)
@YermIm YermIm added the 🌏 Deploy 배포 관련 label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

이번 PR은 타임라인 CRUD 및 UI(그리드, 바, 성과 패널, 생성/수정 모달), 네이버/메타 플랫폼 연동 및 해제 플로우(암호화 전송, 재연동, OAuth 콜백), 랜딩 페이지 리디자인(헤더 스크롤 효과, 가이드 섹션, FAQ/푸터 재구성), 공통 에러/404 처리 및 라우팅, 그리고 DropdownMenu·WorkspaceSwitcher·useCampaignGroup의 데이터 훅 전환을 포함합니다.

Changes

타임라인 기능

Layer / File(s) Summary
타임라인 API/타입/상수
src/api/timeline/timeline.ts, src/types/timeline/api.ts, src/constants/timeline/*
타임라인 상세/목록/생성/수정/삭제/AI요약 API 래퍼와 비교기간/지표/레이아웃/상태 스타일 상수가 추가됨.
타임라인 데이터 훅
src/hooks/timeline/*
목록/상세/생성/수정/삭제/요약요청을 위한 useCoreQuery/useCoreMutation 기반 훅이 추가됨.
그리드/기간/요약 유틸 및 스키마
src/utils/timeline/*
기간 계산, 그리드 컬럼/바 배치, 성과 요약 변환, 생성 폼 Zod 스키마가 구현됨.
타임라인 목 데이터
src/types/timeline/timeline.mock.ts
뷰 단위별 그리드 목과 provider 배열 기반 성과 요약 오버라이드가 추가됨.
그리드/바/축 UI
src/components/timeline/TimelineAxis.tsx, TimelineBar.tsx, TimelineGrid.tsx, TimelinePeriodSelector.tsx, TimelineStatusLegend.tsx, skeleton/TimelineSkeleton.tsx
z-index 처리, 드롭다운 기반 바 메뉴, 세그먼트형 기간 선택, 상태 범례가 추가/변경됨.
생성/수정 모달
src/components/timeline/TimelineCreateModal.tsx, TimelineCreateModal.stories.tsx, TimelineEmptyState.tsx
폼 기반 생성/수정 모달과 빈 상태 컴포넌트가 신규 구현됨.
성과 요약 패널
src/components/timeline/TimelinePerformancePanel.tsx
내부 타이머 대신 외부 pending/loading 플래그로 AI 요약 UI 상태가 파생됨.
타임라인 페이지 통합
src/pages/dashboard/timeline/Timeline.tsx
목록/생성/편집/삭제/요약 훅을 연결하고 폴링·에러·빈 상태 처리가 구현됨.

플랫폼 연동(Naver/Meta) 및 해제

Layer / File(s) Summary
암호화 유틸 및 API
src/utils/integration/encryptAesCbcBase64.ts, src/api/integration/naver.ts, platformAccounts.ts
AES-CBC 암호화 유틸, 네이버 연결/수정 API, 플랫폼 계정 해제 API가 추가됨.
네이버 연동/재연동 모달
src/components/integration/NaverConnectModal.tsx
모드별 폼 리셋, useCoreMutation 전환, 문구/버튼 라벨 분기가 적용됨.
연동 해제 확인 모달
src/components/integration/PlatformDisconnectModal.tsx
해제 확인 모달이 신규 구현되고 로딩 중 동작 제한이 추가됨.
연동 페이지 통합
src/pages/integration/PlatformIntegrationsPage.tsx
연결/재연결/해제 핸들러 및 캐시 무효화/토스트 처리가 연결됨.
OAuth 콜백 처리
src/hooks/integration/useIntegrationOAuthReturn.ts, useMetaOAuthReturn.ts, src/pages/integration/MetaOAuthResultPage.tsx, src/routes/AuthRoutes.tsx
구글/메타 OAuth 결과 토스트, 쿼리 무효화, 결과 페이지 라우팅이 구현됨.

랜딩 페이지 리디자인

Layer / File(s) Summary
가이드/차트 상수
src/constants/landing/guide.ts, overviewChart.ts
가이드 4단계 구조와 차트 옵션 export/틱 수가 변경됨.
헤더/히어로
src/components/landing/LandingHeader.tsx, LandingHero.tsx
스크롤 진행률 기반 헤더 스타일 및 히어로 레이아웃/문구가 변경됨.
가이드 섹션
src/components/landing/GuideOverviewChart.tsx, GuidePlatform.tsx, GuideTimeline.tsx, GuideWorkspace.tsx, LandingGuide.tsx
플랫폼 필터 차트, 정적 플랫폼 카드, 워크스페이스 초대 UI, 스텝 인디케이터가 추가/변경됨.
FAQ/피처/푸터/페이지 정리
LandingFAQ.tsx, LandingFeatures.tsx, LandingFooter.tsx, LandingMultiDevice.tsx(삭제), LandingPage.tsx
정적 FAQ 리스트, 그라디언트 스타일, 푸터 CTA/링크 재구성, 멀티디바이스 섹션 제거가 반영됨.
공통 애니메이션/스타일
src/lib/animation.ts, ComingSoonPlaceholder.tsx
reduce motion variants 및 플레이스홀더 클래스가 조정됨.

공통 에러 처리 및 라우팅

Layer / File(s) Summary
에러 폴백/레이아웃 컴포넌트
src/components/common/error/*
ChartErrorFallback, MetricErrorFallback, ErrorLayout이 신규 구현되고 스토리가 추가됨.
에러/404 페이지 및 라우터
src/pages/common/Error.tsx, NotFound.tsx, src/routes/Router.tsx
ErrorPage/NotFound 구현 및 errorElement/wildcard 라우트가 연결됨.
대시보드 ErrorBoundary 적용
OverviewDashboard.tsx, OverviewKpiSection.tsx, OverviewBudgetSection.tsx
Overview 섹션들이 ErrorBoundary와 폴백으로 감싸짐.

공유 컴포넌트/훅 리팩터링

Layer / File(s) Summary
DropdownMenu 배치
src/components/common/dropdownmenu/DropdownMenu.tsx
클리핑 계산 및 auto 배치 로직, placement/onOpenChange props가 추가됨.
WorkspaceSwitcher 전환
src/components/sidebar/WorkspaceSwitcher.tsx
useCoreMutation 기반 저장 로직과 최신 요청 가드가 도입됨.
useCampaignGroup 전환
src/hooks/ads/useCampaignGroup.ts
캠페인 조회/그룹 생성 로직이 useCoreQuery/useCoreMutation으로 교체됨.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TimelinePage as Timeline
  participant useRequestTimelineSummary
  participant TimelineAPI as requestTimelineSummary
  participant useTimelineDetail

  User->>TimelinePage: 막대 클릭 후 요약 요청
  TimelinePage->>useRequestTimelineSummary: mutate(timelineId)
  useRequestTimelineSummary->>TimelineAPI: requestTimelineSummary(orgId, timelineId)
  TimelineAPI-->>useRequestTimelineSummary: 202 Accepted
  TimelinePage->>useTimelineDetail: 폴링(재조회)
  useTimelineDetail-->>TimelinePage: aiSummary 반영된 상세 데이터
Loading
sequenceDiagram
  participant Browser
  participant MetaOAuthResultPage
  participant useMetaOAuthReturn
  participant ReactQueryCache as QueryClient

  Browser->>MetaOAuthResultPage: /oauth2/meta/result?status=...
  MetaOAuthResultPage->>useMetaOAuthReturn: 마운트 시 호출
  useMetaOAuthReturn->>useMetaOAuthReturn: status/error 검증 및 토스트 결정
  useMetaOAuthReturn->>ReactQueryCache: invalidateQueries(platform.connections)
  useMetaOAuthReturn->>Browser: /integrations로 replace 리다이렉트
Loading

Possibly related PRs

Suggested reviewers: Seojegyeong, jjjsun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.26% 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 배포 반영이라는 PR의 핵심 변경을 짧고 명확하게 전달하고 있습니다.
Description check ✅ Passed 필수 섹션(관련 이슈, 변경사항, 작업 내용, 미완성 작업, 참고 사항)이 모두 포함되어 템플릿 요구를 대부분 충족합니다.
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.
✨ 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 develop

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.

@YermIm YermIm merged commit 4b3df70 into main Jul 9, 2026
2 of 3 checks passed
@YermIm YermIm self-assigned this Jul 9, 2026

@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: 11

🧹 Nitpick comments (10)
src/routes/AuthRoutes.tsx (1)

10-10: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

MetaOAuthResultPage direct import vs lazy loading 일관성

RedirectPage를 비롯한 다른 auth 페이지들은 loadable(lazy(...)) 패턴을 사용하는데, MetaOAuthResultPage만 direct import로 되어 있습니다. 컴포넌트가 작아 번들 영향은 미미하지만, 패턴 일관성을 위해 loadable 적용을 고려해 보세요. 다만 OAuth 리다이렉트 안정성을 위해 의도적으로 즉시 로드한 것이라면 현상태로 유지해도 좋습니다.

♻️ 제안: lazy loading 적용
-import MetaOAuthResultPage from "`@/pages/integration/MetaOAuthResultPage`";
+const MetaOAuthResultPage = loadable(
+  lazy(() => import("`@/pages/integration/MetaOAuthResultPage`")),
+  <AuthFormSkeleton />,
+);

Also applies to: 68-71

🤖 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/routes/AuthRoutes.tsx` at line 10, `AuthRoutes`의 `MetaOAuthResultPage`만
direct import로 되어 있어 다른 auth 페이지들의 `loadable(lazy(...))` 패턴과 불일치합니다.
`RedirectPage` 등 기존 라우팅 방식과 맞추려면 `MetaOAuthResultPage`도 동일하게 lazy loading으로
전환하고, 라우트 정의에서 이를 사용하는 흐름을 함께 정리하세요. 단, OAuth 리다이렉트 시 즉시 로드가 의도된 경우라면
`AuthRoutes`의 해당 import와 라우트 연결을 그대로 유지해도 됩니다.
src/components/timeline/TimelineCreateModal.tsx (1)

215-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Controller render에서 field를 사용하지 않고 외부 watch 값을 직접 참조하고 있습니다.

Controllerrender prop은 field 객체를 통해 폼 상태와 동기화하는 것이 권장 패턴입니다. 현재는 watch("metrics")로 외부에서 값을 읽고 setValue로만 갱신하고 있어 Controller의 역할이 중복됩니다. watch + setValue만 사용하거나, field를 활용하는 쪽으로 통일하면 의도가 더 명확해집니다.

🤖 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/components/timeline/TimelineCreateModal.tsx` around lines 215 - 245, The
metrics picker inside TimelineCreateModal’s Controller is mixing Controller with
external watch/setValue state, and the render prop ignores the field object.
Refactor the metrics selection to use one pattern consistently: either remove
Controller and keep watch("metrics") plus setValue via toggleMetric, or switch
the render callback to use field.value and field.onChange for syncing selected
metrics. Keep the change localized to TimelineCreateModal and the metrics toggle
UI.
src/api/timeline/timeline.ts (1)

34-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

validateStatus 로직이 중복입니다.

status === 201 (또는 202)는 이미 status >= 200 && status < 300 범위에 포함되므로, 명시적인 equality check는 axios 기본 동작과 동일한 결과를 냅니다. 의도한 것이 아니라면 불필요한 코드이므로 제거를 권장합니다.

♻️ 제안하는 수정
 export const createTimeline = async (
   orgId: number,
   body: ITimelineUpsertRequest,
 ): Promise<ITimelineMutationResponse> => {
   const { data } = await axiosInstance.post<
     ICommonResponse<ITimelineMutationResponse>
-  >(`/api/org/${orgId}/timeline`, body, {
-    validateStatus: (status) =>
-      status === 201 || (status >= 200 && status < 300),
-  });
+  >(`/api/org/${orgId}/timeline`, body);
   return data.data;
 };
 export const requestTimelineSummary = async (
   orgId: number,
   timelineId: number,
 ): Promise<TTimelineEmptyResponse> => {
   const { data } = await axiosInstance.post<
     ICommonResponse<TTimelineEmptyResponse>
-  >(
-    `/api/org/${orgId}/timeline/${timelineId}/summary`,
-    {},
-    {
-      validateStatus: (status) =>
-        status === 202 || (status >= 200 && status < 300),
-    },
-  );
+  >(`/api/org/${orgId}/timeline/${timelineId}/summary`, {});
   return data.data;
 };

Also applies to: 70-86

🤖 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/api/timeline/timeline.ts` around lines 34 - 45, The `createTimeline`
request in `timeline.ts` has redundant `validateStatus` logic: the explicit
`status === 201` check is already covered by `status >= 200 && status < 300`.
Simplify the `axiosInstance.post` options by removing the equality branch and
keep only the standard 2xx range check unless there is a separate special-case
requirement; apply the same cleanup to the matching timeline request code
referenced by the comment.
src/hooks/timeline/useTimelineDetail.ts (1)

16-20: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

enabled 옵션이 호출자에 의해 override될 수 있습니다.

{ enabled: orgId != null && timelineId != null, ...options } 순서로 spread되어 있어, 호출자가 optionsenabled를 전달하면 내부 null 가드가 우회됩니다. 이 경우 orgId!, timelineId! non-null assertion이 런타임 에러를 유발할 수 있습니다. 현재 호출처에서는 문제가 없지만, 방어적으로 enabled를 병합하는 것이 안전합니다.

🛡️ 제안하는 수정
   return useCoreQuery(
     QUERY_KEYS.timeline.detail(orgId, timelineId),
     () => getTimelineDetail(orgId!, timelineId!),
-    { enabled: orgId != null && timelineId != null, ...options },
+    {
+      ...options,
+      enabled:
+        (orgId != null && timelineId != null) &&
+ (options?.enabled ?? true),
+    },
   );
🤖 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/hooks/timeline/useTimelineDetail.ts` around lines 16 - 20, The
`useTimelineDetail` query options currently let caller-provided
`options.enabled` override the internal null guard, which can bypass the safety
check before `getTimelineDetail(orgId!, timelineId!)` runs. Update the options
handling in `useTimelineDetail` so `enabled` is merged defensively, preserving
`orgId != null && timelineId != null` as a required condition even when
`options` includes `enabled`, while keeping the rest of the caller options
intact.
src/utils/timeline/period.ts (1)

132-133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

resolveVisiblePeriod MONTH 케이스의 불필요한 삼항 연산자

periodIndex === 0 ? formatMonthLabel(start) : formatMonthLabel(start)에서 두 분기가 동일한 값을 반환합니다. 삼항 연산자 없이 formatMonthLabel(start)만 호출하면 됩니다.

♻️ 제안 수정
   return {
     start,
     end,
-    periodLabel:
-      periodIndex === 0 ? formatMonthLabel(start) : formatMonthLabel(start),
+    periodLabel: formatMonthLabel(start),
   };
🤖 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/utils/timeline/period.ts` around lines 132 - 133, The
`resolveVisiblePeriod` MONTH branch contains a redundant ternary where both
outcomes call `formatMonthLabel(start)`. Simplify the `periodLabel` assignment
in `resolveVisiblePeriod` by removing the `periodIndex === 0 ? ... : ...`
condition and using `formatMonthLabel(start)` directly, keeping the rest of the
period calculation unchanged.
src/utils/timeline/timeline.ts (1)

8-18: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

날짜 필드에 날짜 형식 검증 추가를 권장

dataFieldSchemaz.string().trim().min(1)만 수행하여 빈 문자열만 막고, "YYYY-MM-DD" 형식인지 검증하지 않습니다. 현재 <Input type="date">에서만 입력되므로 실제 버그는 아니지만, 폼 값이 프로그래밍 방식으로 설정될 경우 .refine()의 문자열 비교(startDate <= endDate)가 잘못된 결과를 낼 수 있습니다. Zod 4의 z.iso.date() 또는 regex 검증 추가를 권장합니다.

🛡️ 제안 수정 (regex 검증 추가)
-const dataFieldSchema = z.string().trim().min(1, "날짜를 선택해 주세요");
+const dataFieldSchema = z
+  .string()
+  .trim()
+  .min(1, "날짜를 선택해 주세요")
+  .regex(/^\d{4}-\d{2}-\d{2}$/, "올바른 날짜 형식이 아닙니다");
🤖 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/utils/timeline/timeline.ts` around lines 8 - 18, `dataFieldSchema` only
checks for non-empty strings, so it should also validate that the value is a
real `YYYY-MM-DD` date before `timelineCreateSchema` uses it. Update the shared
date schema in `src/utils/timeline/timeline.ts` to include ISO date validation
(prefer `z.iso.date()` in Zod 4, or an equivalent regex/refine check) while
keeping the existing empty-value message. This ensures `startDate` and `endDate`
stay well-formed when values are set programmatically and that the `refine`
comparison in `timelineCreateSchema` works correctly.
src/components/landing/GuideOverviewChart.tsx (1)

44-47: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

SPLIT_INDEX를 공통 상수로 묶어주세요. GuideOverviewChart.tsx:44-47SPLIT_INDEXoverviewChart.ts의 기준값과 같은 값이지만 중복 선언돼 있어, 이후 분기점이 바뀌면 annotation과 series 기준이 따로 어긋날 수 있습니다. overviewChart.ts에서 export해서 두 곳이 같은 값을 쓰게 맞추면 됩니다.

🤖 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/components/landing/GuideOverviewChart.tsx` around lines 44 - 47,
`GuideOverviewChart`에서 `SPLIT_INDEX`를 로컬 상수로 중복 선언하지 말고, `overviewChart`에서
export한 공통 기준값을 import해서 사용하도록 바꾸세요. 이렇게 하면 annotation과 series가 같은 분기점을 참조하게 되어
값 변경 시 서로 어긋나지 않습니다. `SPLIT_INDEX`와 `SINGLE_SERIES_DATA`가 있는
`GuideOverviewChart.tsx`와 기준값을 정의한 `overviewChart.ts`를 함께 수정해 일관된 상수를 공유하게 하세요.
src/components/landing/LandingHeader.tsx (2)

12-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

scrollToSection 함수가 LandingHero.tsx와 중복 정의되어 있으며 구현도 다릅니다.

LandingHeaderscrollend 이벤트 기반, LandingHero는 이중 requestAnimationFrame 기반으로 포커스 시점을 처리하고 있어 일관성이 깨집니다. 공통 유틸로 추출하여 동일한 동작을 보장하도록 권장합니다.

♻️ 공통 유틸 추출 제안
// src/utils/landing/scrollToSection.ts
export function scrollToSection(id: string) {
  const el = document.getElementById(id);
  if (!(el instanceof HTMLElement)) return;

  const prefersReducedMotion =
    window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;

  if (prefersReducedMotion) {
    el.scrollIntoView({ behavior: "auto" });
    el.focus({ preventScroll: true });
    return;
  }

  el.scrollIntoView({ behavior: "smooth" });

  if ("onscrollend" in window) {
    window.addEventListener(
      "scrollend",
      () => el.focus({ preventScroll: true }),
      { once: true },
    );
  } else {
    setTimeout(() => el.focus({ preventScroll: true }), 600);
  }
}
🤖 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/components/landing/LandingHeader.tsx` around lines 12 - 39,
`scrollToSection` is duplicated in `LandingHeader` and `LandingHero` with
different focus timing, so extract the shared scrolling/focus behavior into a
single utility and use it from both components. Keep the existing reduced-motion
handling and `scrollend`/timeout fallback in one place so both `LandingHeader`
and `LandingHero` behave consistently and future changes only need to be made
once.

70-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

인라인 스타일의 임의 RGB 색상 값을 @theme 토큰 기반으로 전환을 권장합니다.

textColorBase, pillBg, headerStyle에서 rgb(255,255,255), rgb(55,65,81), rgba(221,227,240,...) 등 원시 RGB 값을 직접 계산하고 있습니다. 코딩 가이드라인에 따르면 src/styles/tokens.css@theme 토큰만 사용해야 하며, 임의 색상은 금지됩니다. Tailwind v4의 @theme 토큰은 CSS 변수(var(--color-text-body), var(--color-surface-100), var(--color-surface-400) 등)로 노출되므로, color-mix()를 활용해 동적 보간을 토큰 기반으로 수행할 수 있습니다.

As per coding guidelines: **/*.{ts,tsx}: "Use only @theme tokens from src/styles/tokens.css for colors. Forbidden: token renaming, arbitrary colors."

♻️ `color-mix()` 기반 제안
- const textColorBase = `rgb(${Math.round(255 - 200 * eased)},${Math.round(255 - 190 * eased)},${Math.round(255 - 174 * eased)})`;
+ const textColorBase = `color-mix(in srgb, var(--color-surface-100) ${(1 - eased) * 100}%, var(--color-text-body) ${eased * 100}%)`;

- const pillBg = `rgba(${Math.round(255 - 200 * eased)},${Math.round(255 - 190 * eased)},${Math.round(255 - 174 * eased)},${(0.12 - 0.05 * eased).toFixed(3)})`;
+ const pillBg = `color-mix(in srgb, var(--color-surface-100) ${(1 - eased) * 100}%, var(--color-text-body) ${eased * 100}%) / ${(0.12 - 0.05 * eased).toFixed(3)})`;

  const headerStyle = {
-   backgroundColor: `rgba(255,255,255,${(eased * 0.82).toFixed(3)})`,
+   backgroundColor: `color-mix(in srgb, var(--color-surface-100) ${eased * 82}%, transparent)`,
    backdropFilter: `blur(${(eased * 12).toFixed(1)}px)`,
    WebkitBackdropFilter: `blur(${(eased * 12).toFixed(1)}px)`,
-   borderBottomColor: `rgba(221,227,240,${eased.toFixed(3)})`,
+   borderBottomColor: `color-mix(in srgb, var(--color-surface-400) ${eased * 100}%, transparent)`,
  } as CSSProperties;
🤖 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/components/landing/LandingHeader.tsx` around lines 70 - 87, The inline
color math in LandingHeader should stop using hardcoded RGB/RGBA values and
switch to the existing `@theme` token palette. Update the style logic in the
LandingHeader component (textColorBase, pillBg, and headerStyle) to derive
colors from token-backed CSS variables such as var(--color-text-body) and
var(--color-surface-*) instead of raw numeric channels. If dynamic blending is
still needed, use token-based color-mix() so the header animation remains
consistent with the design system and coding guidelines.

Source: Coding guidelines

src/components/landing/LandingFooter.tsx (1)

118-118: 📐 Maintainability & Code Quality | 🔵 Trivial

TODO: 대표자명 및 사업자등록번호 추가 건에 대해 도움을 드릴 수 있습니다.

{/* TODO: 대표자명, 사업자등록번호 등 확정 시 아래 줄에 추가 */} 주석이 남아 있습니다. 사업자 정보가 확정되면 이 자리에 추가해야 합니다. 데이터 구조 설계나 하드코딩된 값의 외부화가 필요하시면 도움을 드릴 수 있습니다. 새 이슈를 열어 추적하시겠습니까?

🤖 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/components/landing/LandingFooter.tsx` at line 118, The LandingFooter
component still contains a placeholder TODO comment for the company
representative and business registration number; update this footer block to
render the finalized business information in place of the comment, using the
existing LandingFooter markup and any related constants or props if available.
If the values are not yet available in the component, move them into a clearly
defined source and wire them into the footer instead of leaving the TODO behind.
🤖 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/components/integration/NaverConnectModal.tsx`:
- Around line 55-68: In NaverConnectModal, the reconnect path only resets the
form when initialCustomerId exists, which leaves Customer ID empty but locked
when mode is "reconnect" and the ID is missing. Update the useEffect reset logic
so reconnect still initializes the form even without initialCustomerId, and
adjust the Customer ID input’s readOnly condition to depend on both mode ===
"reconnect" and the presence of initialCustomerId. This should be applied in
NaverConnectModal around the reset behavior and the Customer ID field props.

In `@src/components/landing/LandingHeader.tsx`:
- Line 91: The Tailwind height utility in LandingHeader is using the wrong
arbitrary value syntax, so update the className on LandingHeader to use the
bracket form for the CSS variable fallback, matching the pattern used elsewhere
like LandingFAQ. Replace the current h-(--landing-header-height,64px) usage with
the proper height utility that maps to var(--landing-header-height,64px) so the
header height is applied consistently.

In `@src/components/timeline/TimelineCreateModal.tsx`:
- Around line 265-289: The dropdown trigger in TimelineCreateModal is focusable
via tabIndex but lacks keyboard activation, so add a key handling path on the
trigger element rendered by the trigger prop to open it with Enter and Space.
Update the same div that currently renders comparisonLabel and ChevronIcon to
handle keyboard events consistently with mouse clicks, while preserving the
existing open state behavior and disabled styling tied to isPending and
errors.comparisonPeriodType.

In `@src/hooks/ads/useCampaignGroup.ts`:
- Around line 84-93: The group creation flow only invalidates
QUERY_KEYS.campaign.list(orgId), but useCampaignGroup also reads
platform-specific campaign lists via QUERY_KEYS.campaign.platformList(orgId,
"GOOGLE" | "NAVER" | "META"). Update the invalidateKeys in this hook so campaign
platform lists are invalidated together with the group list, ensuring connected
campaigns are removed from the selectable options on the next visit.

In `@src/hooks/timeline/useDeleteTimeline.ts`:
- Line 5: The import in useDeleteTimeline should use the project alias instead
of a relative path to match the coding guideline and keep consistency with
useTimelineList. Update the useCoreMutation import to reference the shared
customQuery module via the "`@/hooks/customQuery`" alias rather than
"../customQuery", and keep the change limited to the import statement in
useDeleteTimeline.

In `@src/hooks/timeline/useRequestTimelineSummary.ts`:
- Line 5: The import in useRequestTimelineSummary should use the project alias
instead of a relative path. Update the useCoreMutation import in this hook to
reference the canonical `@/hooks/customQuery` path so it matches the repo-wide
import convention and keeps the timeline hook aligned with the rest of the
codebase.

In `@src/hooks/timeline/useUpdateTimeline.ts`:
- Line 7: The import in useUpdateTimeline should use the required `@/` alias
instead of a relative path. Update the useCoreMutation import to reference the
shared customQuery module through the alias path (matching the project’s import
conventions) so the hook stays consistent with the rest of the codebase.

In `@src/pages/dashboard/overview/sections/OverviewKpiSection.tsx`:
- Around line 38-58: The KPI ErrorBoundary around the `OverviewKpiSection` grid
is missing `resetKeys`, so `MetricErrorFallback` can stay stuck even after
`kpis` are refetched. Update the `ErrorBoundary` in `OverviewKpiSection` to use
`resetKeys` based on the `kpis` prop, matching the auto-reset behavior used in
`OverviewBudgetSection` and `OverviewDashboard` platform sections. This will let
the boundary recover automatically when the KPI data changes instead of
requiring a manual retry.

In `@src/utils/integration/encryptAesCbcBase64.ts`:
- Around line 32-56: The encryptAesCbcBase64 helper is using build-time client
env values for AES secret/IV, which exposes sensitive material in the browser
bundle. Update encryptAesCbcBase64 to stop relying on VITE_NAVER_AES_SECRET and
the fixed VITE_NAVER_AES_IV in client-side code; move this encryption to a
server-side path or redesign it so a fresh random IV is generated per call and
transmitted alongside the ciphertext. Keep the changes localized around
encryptAesCbcBase64 and parseKeyOrIv usage so the browser no longer contains
reusable secret material or a static CBC IV.

In `@src/utils/timeline/buildTimelineSummaryPanel.ts`:
- Line 9: The import in buildTimelineSummaryPanel should use the project’s "`@/`”
alias instead of a relative path. Update the existing formatDot import to
reference "`@/utils/timeline/period`" so the module follows the codebase import
convention and remains easy to locate alongside other timeline utilities.

In `@src/utils/timeline/period.ts`:
- Around line 67-77: formatWeekLabel has redundant if/else branches that both
return the same string, so the month-aware branch is dead code. Update
formatWeekLabel to use different formatting in the same-month case versus
cross-month case, likely by showing the month only once when start.getMonth()
=== end.getMonth() and keeping the full start/end month format otherwise. Use
the existing formatWeekLabel, MONTH, and date extraction logic to make the
intended week label concise and distinct across the two cases.

---

Nitpick comments:
In `@src/api/timeline/timeline.ts`:
- Around line 34-45: The `createTimeline` request in `timeline.ts` has redundant
`validateStatus` logic: the explicit `status === 201` check is already covered
by `status >= 200 && status < 300`. Simplify the `axiosInstance.post` options by
removing the equality branch and keep only the standard 2xx range check unless
there is a separate special-case requirement; apply the same cleanup to the
matching timeline request code referenced by the comment.

In `@src/components/landing/GuideOverviewChart.tsx`:
- Around line 44-47: `GuideOverviewChart`에서 `SPLIT_INDEX`를 로컬 상수로 중복 선언하지 말고,
`overviewChart`에서 export한 공통 기준값을 import해서 사용하도록 바꾸세요. 이렇게 하면 annotation과
series가 같은 분기점을 참조하게 되어 값 변경 시 서로 어긋나지 않습니다. `SPLIT_INDEX`와
`SINGLE_SERIES_DATA`가 있는 `GuideOverviewChart.tsx`와 기준값을 정의한 `overviewChart.ts`를
함께 수정해 일관된 상수를 공유하게 하세요.

In `@src/components/landing/LandingFooter.tsx`:
- Line 118: The LandingFooter component still contains a placeholder TODO
comment for the company representative and business registration number; update
this footer block to render the finalized business information in place of the
comment, using the existing LandingFooter markup and any related constants or
props if available. If the values are not yet available in the component, move
them into a clearly defined source and wire them into the footer instead of
leaving the TODO behind.

In `@src/components/landing/LandingHeader.tsx`:
- Around line 12-39: `scrollToSection` is duplicated in `LandingHeader` and
`LandingHero` with different focus timing, so extract the shared scrolling/focus
behavior into a single utility and use it from both components. Keep the
existing reduced-motion handling and `scrollend`/timeout fallback in one place
so both `LandingHeader` and `LandingHero` behave consistently and future changes
only need to be made once.
- Around line 70-87: The inline color math in LandingHeader should stop using
hardcoded RGB/RGBA values and switch to the existing `@theme` token palette.
Update the style logic in the LandingHeader component (textColorBase, pillBg,
and headerStyle) to derive colors from token-backed CSS variables such as
var(--color-text-body) and var(--color-surface-*) instead of raw numeric
channels. If dynamic blending is still needed, use token-based color-mix() so
the header animation remains consistent with the design system and coding
guidelines.

In `@src/components/timeline/TimelineCreateModal.tsx`:
- Around line 215-245: The metrics picker inside TimelineCreateModal’s
Controller is mixing Controller with external watch/setValue state, and the
render prop ignores the field object. Refactor the metrics selection to use one
pattern consistently: either remove Controller and keep watch("metrics") plus
setValue via toggleMetric, or switch the render callback to use field.value and
field.onChange for syncing selected metrics. Keep the change localized to
TimelineCreateModal and the metrics toggle UI.

In `@src/hooks/timeline/useTimelineDetail.ts`:
- Around line 16-20: The `useTimelineDetail` query options currently let
caller-provided `options.enabled` override the internal null guard, which can
bypass the safety check before `getTimelineDetail(orgId!, timelineId!)` runs.
Update the options handling in `useTimelineDetail` so `enabled` is merged
defensively, preserving `orgId != null && timelineId != null` as a required
condition even when `options` includes `enabled`, while keeping the rest of the
caller options intact.

In `@src/routes/AuthRoutes.tsx`:
- Line 10: `AuthRoutes`의 `MetaOAuthResultPage`만 direct import로 되어 있어 다른 auth
페이지들의 `loadable(lazy(...))` 패턴과 불일치합니다. `RedirectPage` 등 기존 라우팅 방식과 맞추려면
`MetaOAuthResultPage`도 동일하게 lazy loading으로 전환하고, 라우트 정의에서 이를 사용하는 흐름을 함께 정리하세요.
단, OAuth 리다이렉트 시 즉시 로드가 의도된 경우라면 `AuthRoutes`의 해당 import와 라우트 연결을 그대로 유지해도 됩니다.

In `@src/utils/timeline/period.ts`:
- Around line 132-133: The `resolveVisiblePeriod` MONTH branch contains a
redundant ternary where both outcomes call `formatMonthLabel(start)`. Simplify
the `periodLabel` assignment in `resolveVisiblePeriod` by removing the
`periodIndex === 0 ? ... : ...` condition and using `formatMonthLabel(start)`
directly, keeping the rest of the period calculation unchanged.

In `@src/utils/timeline/timeline.ts`:
- Around line 8-18: `dataFieldSchema` only checks for non-empty strings, so it
should also validate that the value is a real `YYYY-MM-DD` date before
`timelineCreateSchema` uses it. Update the shared date schema in
`src/utils/timeline/timeline.ts` to include ISO date validation (prefer
`z.iso.date()` in Zod 4, or an equivalent regex/refine check) while keeping the
existing empty-value message. This ensures `startDate` and `endDate` stay
well-formed when values are set programmatically and that the `refine`
comparison in `timelineCreateSchema` works correctly.
🪄 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: CHILL

Plan: Pro

Run ID: 0c033ab5-29de-48f1-8ae0-9152eaa7a685

📥 Commits

Reviewing files that changed from the base of the PR and between 31520f7 and 97afd1b.

⛔ Files ignored due to path filters (17)
  • .storybook/main.ts is excluded by none and included by none
  • .storybook/preview.tsx is excluded by none and included by none
  • package-lock.json is excluded by !**/package-lock.json and included by none
  • package.json is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml and included by none
  • src/assets/icon/ai/ai-요약버튼.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/ai/sparkle-circle.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/lightbulb.svg is excluded by !**/*.svg and included by src/**
  • src/assets/icon/common/plus.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/social-logo/plain/google_ads.png is excluded by !**/*.png and included by src/**
  • src/assets/logo/social-logo/plain/meta.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/social-logo/wordmark/kakao-wordmark.svg is excluded by !**/*.svg and included by src/**
  • src/assets/logo/social-logo/wordmark/naver-wordmark.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/footer_bg.jpg is excluded by !**/*.jpg and included by src/**
  • src/assets/mockup/iOS app dock.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/iPad Air mockup.png is excluded by !**/*.png and included by src/**
  • src/assets/mockup/optimized/timeline_dashboard.jpg is excluded by !**/*.jpg and included by src/**
📒 Files selected for processing (70)
  • src/api/integration/naver.ts
  • src/api/integration/platformAccounts.ts
  • src/api/timeline/timeline.ts
  • src/components/common/ComingSoonPlaceholder.tsx
  • src/components/common/dropdownmenu/DropdownMenu.tsx
  • src/components/common/error/ChartErrorFallback.stories.tsx
  • src/components/common/error/ChartErrorFallback.tsx
  • src/components/common/error/ErrorBoundary.tsx
  • src/components/common/error/ErrorLayout.tsx
  • src/components/common/error/MetricErrorFallback.stories.tsx
  • src/components/common/error/MetricErrorFallback.tsx
  • src/components/integration/NaverConnectModal.tsx
  • src/components/integration/PlatformDisconnectModal.tsx
  • src/components/landing/GuideOverviewChart.tsx
  • src/components/landing/GuidePlatform.tsx
  • src/components/landing/GuideTimeline.tsx
  • src/components/landing/GuideWorkspace.tsx
  • src/components/landing/LandingFAQ.tsx
  • src/components/landing/LandingFeatures.tsx
  • src/components/landing/LandingFooter.tsx
  • src/components/landing/LandingGuide.tsx
  • src/components/landing/LandingHeader.tsx
  • src/components/landing/LandingHero.tsx
  • src/components/landing/LandingMultiDevice.tsx
  • src/components/sidebar/WorkspaceSwitcher.tsx
  • src/components/timeline/TimelineAxis.tsx
  • src/components/timeline/TimelineBar.tsx
  • src/components/timeline/TimelineCreateModal.stories.tsx
  • src/components/timeline/TimelineCreateModal.tsx
  • src/components/timeline/TimelineEmptyState.tsx
  • src/components/timeline/TimelineGrid.tsx
  • src/components/timeline/TimelinePerformancePanel.tsx
  • src/components/timeline/TimelinePeriodSelector.tsx
  • src/components/timeline/TimelineStatusLegend.tsx
  • src/components/timeline/skeleton/TimelineSkeleton.tsx
  • src/constants/landing/guide.ts
  • src/constants/landing/overviewChart.ts
  • src/constants/timeline/formOptions.ts
  • src/constants/timeline/layout.ts
  • src/constants/timeline/statusStyle.ts
  • src/hooks/ads/useCampaignGroup.ts
  • src/hooks/integration/useIntegrationOAuthReturn.ts
  • src/hooks/integration/useMetaOAuthReturn.ts
  • src/hooks/timeline/useCreateTimeline.ts
  • src/hooks/timeline/useDeleteTimeline.ts
  • src/hooks/timeline/useRequestTimelineSummary.ts
  • src/hooks/timeline/useTimelineDetail.ts
  • src/hooks/timeline/useTimelineList.ts
  • src/hooks/timeline/useUpdateTimeline.ts
  • src/lib/animation.ts
  • src/lib/queryKeys.ts
  • src/pages/common/Error.tsx
  • src/pages/common/NotFound.tsx
  • src/pages/dashboard/overview/OverviewDashboard.tsx
  • src/pages/dashboard/overview/sections/OverviewBudgetSection.tsx
  • src/pages/dashboard/overview/sections/OverviewKpiSection.tsx
  • src/pages/dashboard/timeline/Timeline.tsx
  • src/pages/integration/MetaOAuthResultPage.tsx
  • src/pages/integration/PlatformIntegrationsPage.tsx
  • src/pages/landing/LandingPage.tsx
  • src/routes/AuthRoutes.tsx
  • src/routes/Router.tsx
  • src/types/timeline/api.ts
  • src/types/timeline/timeline.mock.ts
  • src/types/timeline/ui.ts
  • src/utils/integration/encryptAesCbcBase64.ts
  • src/utils/timeline/buildTimelineGrid.ts
  • src/utils/timeline/buildTimelineSummaryPanel.ts
  • src/utils/timeline/period.ts
  • src/utils/timeline/timeline.ts
💤 Files with no reviewable changes (2)
  • src/components/landing/LandingMultiDevice.tsx
  • src/pages/landing/LandingPage.tsx

Comment on lines 55 to +68
useEffect(() => {
if (!isOpen) {
reset();
return;
}
}, [isOpen, reset]);

const connectMutation = useMutation<
void,
IApiErrorResponse,
TNaverConnectFormValues
>({
mutationFn: (body) => connectNaverAccount(orgId, body),
onSuccess: () => {
toast.success("네이버 광고 계정을 연동했습니다.");
reset();
onClose();
void queryClient.invalidateQueries({
queryKey: ["platform-connections", orgId],

if (mode === "reconnect" && initialCustomerId) {
reset({
customerId: initialCustomerId,
apiKey: "",
secretKey: "",
});
}
}, [isOpen, mode, initialCustomerId, reset]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

reconnect 모드에서 initialCustomerId가 없을 때 폼 리셋 및 readOnly 처리 누락

mode === "reconnect"이지만 initialCustomerId가 falsy인 경우, if 조건이 충족되지 않아 폼이 리셋되지 않습니다. 이때 readOnly={mode === "reconnect"}로 인해 고객 ID 입력이 잠기지만 값은 비어 있어, 사용자가 고객 ID를 입력할 수 없는 상태가 됩니다.

부모 컴포넌트에서 naverItem.externalAccountIdundefined일 수 있으므로, 이 케이스가 발생할 수 있습니다.

🛠️ Proposed fix
   if (mode === "reconnect" && initialCustomerId) {
     reset({
       customerId: initialCustomerId,
       apiKey: "",
       secretKey: "",
     });
+  } else {
+    reset();
   }

그리고 readOnly 조건에 initialCustomerId 존재 여부를 추가하여, 값이 없을 때는 직접 입력할 수 있도록 변경:

-          readOnly={mode === "reconnect"}
+          readOnly={mode === "reconnect" && !!initialCustomerId}
📝 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.

Suggested change
useEffect(() => {
if (!isOpen) {
reset();
return;
}
}, [isOpen, reset]);
const connectMutation = useMutation<
void,
IApiErrorResponse,
TNaverConnectFormValues
>({
mutationFn: (body) => connectNaverAccount(orgId, body),
onSuccess: () => {
toast.success("네이버 광고 계정을 연동했습니다.");
reset();
onClose();
void queryClient.invalidateQueries({
queryKey: ["platform-connections", orgId],
if (mode === "reconnect" && initialCustomerId) {
reset({
customerId: initialCustomerId,
apiKey: "",
secretKey: "",
});
}
}, [isOpen, mode, initialCustomerId, reset]);
useEffect(() => {
if (!isOpen) {
reset();
return;
}
if (mode === "reconnect" && initialCustomerId) {
reset({
customerId: initialCustomerId,
apiKey: "",
secretKey: "",
});
} else {
reset();
}
}, [isOpen, mode, initialCustomerId, reset]);
🤖 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/components/integration/NaverConnectModal.tsx` around lines 55 - 68, In
NaverConnectModal, the reconnect path only resets the form when
initialCustomerId exists, which leaves Customer ID empty but locked when mode is
"reconnect" and the ID is missing. Update the useEffect reset logic so reconnect
still initializes the form even without initialCustomerId, and adjust the
Customer ID input’s readOnly condition to depend on both mode === "reconnect"
and the presence of initialCustomerId. This should be applied in
NaverConnectModal around the reset behavior and the Customer ID field props.

isScrolled
? "bg-surface-100 shadow-Soft border-transparent"
: "bg-surface-100/80 backdrop-blur-xl border-surface-400"
className={`fixed top-0 left-0 right-0 z-50 border-b transition-shadow duration-300 h-(--landing-header-height,64px) ${

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Tailwind v4 빌드 결과에서 h-(--landing-header-height,64px)가 올바른 CSS로 생성되는지 확인
rg -n "landing-header-height" src/components/landing/ --type=tsx

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## LandingHeader file\n'
sed -n '1,180p' src/components/landing/LandingHeader.tsx

printf '\n## FAQ file\n'
sed -n '1,180p' src/components/landing/LandingFAQ.tsx

printf '\n## Tailwind 관련 설정/참조\n'
git ls-files | rg 'tailwind|postcss|vite|css$|LandingHeader|LandingFAQ'

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 7673


🌐 Web query:

Tailwind CSS v4 h-(--foo,64px) var fallback syntax parentheses comma arbitrary values

💡 Result:

In Tailwind CSS v4, you can use parentheses () to reference CSS variables within arbitrary values, which serves as a cleaner alternative to the traditional var() syntax [1][2]. Regarding your specific query about the syntax h-(--foo,64px): 1. Parentheses Syntax: Tailwind v4 introduced this shorthand specifically for CSS variables [1]. For example, h-(--foo) automatically resolves to height: var(--foo). 2. Fallback Handling: While the parentheses syntax is designed to simplify referencing variables, it does not natively support the standard CSS comma-separated fallback syntax (e.g., var(--foo, 64px)) directly inside the parentheses in the same way var() does [1][2]. If you need to include a fallback value, you should continue using the standard square bracket notation with the full var() function [3]:

This traditional approach ensures that Tailwind correctly parses the comma as part of the var() function arguments rather than as a separator or delimiter within the utility class [3]. For simple variable references without fallbacks, the parentheses shorthand remains the recommended, more concise choice [1][2].

Citations:


h-(--landing-header-height,64px)는 대괄호 구문으로 바꾸는 게 맞습니다.
Tailwind v4의 괄호 구문은 CSS 변수 참조용이라 fallback(,64px)을 섞으면 의도한 height: var(--landing-header-height,64px)로 처리되지 않을 수 있습니다. h-[var(--landing-header-height,64px)]처럼 적거나, LandingFAQ.tsx처럼 대괄호 구문으로 통일하세요.

🤖 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/components/landing/LandingHeader.tsx` at line 91, The Tailwind height
utility in LandingHeader is using the wrong arbitrary value syntax, so update
the className on LandingHeader to use the bracket form for the CSS variable
fallback, matching the pattern used elsewhere like LandingFAQ. Replace the
current h-(--landing-header-height,64px) usage with the proper height utility
that maps to var(--landing-header-height,64px) so the header height is applied
consistently.

Comment on lines +265 to +289
trigger={(open) => (
<div
tabIndex={0}
className={twMerge(
"flex h-14 w-full cursor-pointer items-center justify-between rounded-2xl bg-surface-100 px-5 text-left font-body1 ring-1 ring-surface-400 transition-colors duration-200 ease-out outline-none",
"hover:bg-surface-200 hover:ring-surface-400",
"focus-visible:ring-2 focus-visible:ring-surface-400",
isPending && "cursor-not-allowed opacity-60",
errors.comparisonPeriodType
? "ring-2 ring-info-red bg-info-red/5"
: "",
)}
>
<span className="min-w-0 flex-1 truncate text-text-title">
{comparisonLabel}
</span>
<ChevronIcon
className={twMerge(
"h-4 w-4 shrink-0 text-text-muted transition-transform duration-200",
open ? "rotate-0" : "rotate-180",
)}
aria-hidden
/>
</div>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

드롭다운 트리거에 키보드 활성화 핸들러가 없습니다.

tabIndex={0}으로 포커스는 가능하지만 onKeyDown 핸들러가 없어 Enter/Space 키로 드롭다운을 열 수 없습니다. 접근성을 위해 키보드 활성화를 추가해 주세요.

♿ 제안: 키보드 핸들러 추가
                       <div
                         tabIndex={0}
+                        onKeyDown={(e) => {
+                          if (e.key === "Enter" || e.key === " ") {
+                            e.preventDefault();
+                            e.currentTarget.click();
+                          }
+                        }}
                         className={twMerge(
📝 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.

Suggested change
trigger={(open) => (
<div
tabIndex={0}
className={twMerge(
"flex h-14 w-full cursor-pointer items-center justify-between rounded-2xl bg-surface-100 px-5 text-left font-body1 ring-1 ring-surface-400 transition-colors duration-200 ease-out outline-none",
"hover:bg-surface-200 hover:ring-surface-400",
"focus-visible:ring-2 focus-visible:ring-surface-400",
isPending && "cursor-not-allowed opacity-60",
errors.comparisonPeriodType
? "ring-2 ring-info-red bg-info-red/5"
: "",
)}
>
<span className="min-w-0 flex-1 truncate text-text-title">
{comparisonLabel}
</span>
<ChevronIcon
className={twMerge(
"h-4 w-4 shrink-0 text-text-muted transition-transform duration-200",
open ? "rotate-0" : "rotate-180",
)}
aria-hidden
/>
</div>
)}
trigger={(open) => (
<div
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.currentTarget.click();
}
}}
className={twMerge(
"flex h-14 w-full cursor-pointer items-center justify-between rounded-2xl bg-surface-100 px-5 text-left font-body1 ring-1 ring-surface-400 transition-colors duration-200 ease-out outline-none",
"hover:bg-surface-200 hover:ring-surface-400",
"focus-visible:ring-2 focus-visible:ring-surface-400",
isPending && "cursor-not-allowed opacity-60",
errors.comparisonPeriodType
? "ring-2 ring-info-red bg-info-red/5"
: "",
)}
>
<span className="min-w-0 flex-1 truncate text-text-title">
{comparisonLabel}
</span>
<ChevronIcon
className={twMerge(
"h-4 w-4 shrink-0 text-text-muted transition-transform duration-200",
open ? "rotate-0" : "rotate-180",
)}
aria-hidden
/>
</div>
)}
🤖 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/components/timeline/TimelineCreateModal.tsx` around lines 265 - 289, The
dropdown trigger in TimelineCreateModal is focusable via tabIndex but lacks
keyboard activation, so add a key handling path on the trigger element rendered
by the trigger prop to open it with Enter and Space. Update the same div that
currently renders comparisonLabel and ChevronIcon to handle keyboard events
consistently with mouse clicks, while preserving the existing open state
behavior and disabled styling tied to isPending and errors.comparisonPeriodType.

Source: Path instructions

Comment on lines +84 to +93
{
invalidateKeys: [QUERY_KEYS.campaign.list(orgId)],
userOnSuccess: () => {
setIsSuccessModalOpen(true);
},
userOnError: (error) => {
toast.error(error.message ?? "그룹 생성에 실패했습니다.");
},
},
});
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# createCampaignGroup API 정의를 확인하여 그룹 생성 후 캠페인 상태 변경 여부를 검증합니다.
rg -n "createCampaignGroup" --type=ts -C5 src/api/ads/ads.ts

# 기존 코드에서 platformList 무효화 여부를 확인하기 위해 git 히스토리를 검색합니다.
git log --oneline -5 -- src/hooks/ads/useCampaignGroup.ts

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 781


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 관련 훅과 쿼리 키, 그리고 platformList / list의 데이터 소스를 확인합니다.
rg -n "QUERY_KEYS\.campaign\.(list|platformList)|platformList\(|campaign\.list\(" src -C 3
echo '---'
sed -n '1,220p' src/hooks/ads/useCampaignGroup.ts
echo '---'
sed -n '1,220p' src/api/ads/ads.ts

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 9391


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# platformList 사용처와 관련 mutation에서의 invalidateKeys를 확인합니다.
rg -n "platformList\(|invalidateKeys: \[.*campaign\.(list|platformList)|createCampaignGroup|update.*Campaign|delete.*Campaign|campaignIds" src -C 3

echo '--- useCampaignGroup related files ---'
sed -n '1,220p' src/hooks/ads/useCampaignGroup.ts

echo '--- query keys ---'
sed -n '1,220p' src/lib/queryKeys.ts

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 12990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 캠페인 그룹 생성이 플랫폼별 목록의 의미와 실제로 충돌하는지 판단할 수 있도록
# 사용처, 타입 정의, 관련 화면을 더 확인합니다.
rg -n "useCampaignGroup|platformCampaigns|연결 가능한 캠페인|캠페인 그룹|createCampaignGroup|getPlatformCampaigns" src -C 3

echo '--- campaign types ---'
sed -n '1,220p' src/types/ads/campaign.ts

echo '--- useCampaignGroup UI ---'
sed -n '1,240p' src/pages/ads/group/CreateCampaignGroupPage.tsx 2>/dev/null || true
sed -n '1,240p' src/components/ads/group/* 2>/dev/null || true

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 7257


그룹 생성 후 플랫폼별 캠페인 캐시도 함께 무효화해야 합니다.
invalidateKeys에는 QUERY_KEYS.campaign.list(orgId)만 들어 있습니다. 이 훅에서 읽는 QUERY_KEYS.campaign.platformList(orgId, "GOOGLE" | "NAVER" | "META")는 연결 가능한 캠페인 목록이라, 그룹 생성 후에는 함께 무효화해야 이미 연결된 캠페인이 다음 진입 시 옵션에 남지 않습니다.

🤖 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/hooks/ads/useCampaignGroup.ts` around lines 84 - 93, The group creation
flow only invalidates QUERY_KEYS.campaign.list(orgId), but useCampaignGroup also
reads platform-specific campaign lists via
QUERY_KEYS.campaign.platformList(orgId, "GOOGLE" | "NAVER" | "META"). Update the
invalidateKeys in this hook so campaign platform lists are invalidated together
with the group list, ensuring connected campaigns are removed from the
selectable options on the next visit.


import type { IApiErrorResponse } from "@/types/common/common";

import { useCoreMutation } from "../customQuery";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

@/ alias 대신 상대 경로 import 사용

../customQuery 대신 @/hooks/customQuery를 사용해야 합니다. 코딩 가이드라인에서 모든 import에 @/ alias를 사용하도록 규정하고 있으며, 같은 계층의 useTimelineList.ts@/hooks/customQuery를 사용하고 있어 일관성이 깨집니다.

As per coding guidelines: "Use @/ alias for all imports."

🔧 제안 수정
-import { useCoreMutation } from "../customQuery";
+import { useCoreMutation } from "`@/hooks/customQuery`";
📝 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.

Suggested change
import { useCoreMutation } from "../customQuery";
import { useCoreMutation } from "`@/hooks/customQuery`";
🤖 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/hooks/timeline/useDeleteTimeline.ts` at line 5, The import in
useDeleteTimeline should use the project alias instead of a relative path to
match the coding guideline and keep consistency with useTimelineList. Update the
useCoreMutation import to reference the shared customQuery module via the
"`@/hooks/customQuery`" alias rather than "../customQuery", and keep the change
limited to the import statement in useDeleteTimeline.

Source: Coding guidelines

import type { IApiErrorResponse } from "@/types/common/common";
import type { IUpdateTimelineVariables } from "@/types/timeline/api";

import { useCoreMutation } from "../customQuery";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

@/ alias 대신 상대 경로 import가 사용되었습니다.

"../customQuery""@/hooks/customQuery"로 변경해야 합니다. As per coding guidelines, 모든 import에 @/ alias를 사용해야 합니다.

♻️ 제안하는 수정
-import { useCoreMutation } from "../customQuery";
+import { useCoreMutation } from "`@/hooks/customQuery`";
📝 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.

Suggested change
import { useCoreMutation } from "../customQuery";
import { useCoreMutation } from "`@/hooks/customQuery`";
🤖 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/hooks/timeline/useUpdateTimeline.ts` at line 7, The import in
useUpdateTimeline should use the required `@/` alias instead of a relative path.
Update the useCoreMutation import to reference the shared customQuery module
through the alias path (matching the project’s import conventions) so the hook
stays consistent with the rest of the codebase.

Source: Coding guidelines

Comment on lines +38 to +58
<ErrorBoundary FallbackComponent={MetricErrorFallback}>
{isKpisError ? (
<div className="flex items-center justify-center rounded-3xl border border-surface-100/40 bg-surface-100/80 py-8 font-body2 text-info-red">
{kpisError?.message ??
"지표 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
</div>
) : (
<div className="grid min-w-0 grid-cols-4 gap-4 tablet:grid-cols-2 tablet:gap-4">
{isKpisLoading
? [0, 1, 2, 3].map((i) => <OverviewKpiCardSkeleton key={i} />)
: kpiList.map((kpi) => (
<StatCard
key={`d-${kpi.title}`}
{...kpi}
className="gap-3 py-5"
compact
/>
))}
</div>
)}
</ErrorBoundary>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

KPI ErrorBoundary에 resetKeys 추가를 권장합니다.

OverviewBudgetSectionOverviewDashboard의 플랫폼 섹션에는 resetKeys가 설정되어 있어 데이터 갱신 시 에러 바운더리가 자동 리셋됩니다. 하지만 KPI 그리드를 감싼 ErrorBoundary에는 resetKeys가 없어, kpis 데이터가 리패치되어도 MetricErrorFallback가 유지됩니다. 사용자가 매번 "다시 시도" 버튼을 눌러야 합니다.

kpis가 컴포넌트 props로 사용 가능하므로 resetKeys={[kpis]} 추가만으로 자동 복구가 가능합니다.

💡 제안: resetKeys 추가
-      <ErrorBoundary FallbackComponent={MetricErrorFallback}>
+      <ErrorBoundary FallbackComponent={MetricErrorFallback} resetKeys={[kpis]}>
         {isKpisError ? (
📝 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.

Suggested change
<ErrorBoundary FallbackComponent={MetricErrorFallback}>
{isKpisError ? (
<div className="flex items-center justify-center rounded-3xl border border-surface-100/40 bg-surface-100/80 py-8 font-body2 text-info-red">
{kpisError?.message ??
"지표 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
</div>
) : (
<div className="grid min-w-0 grid-cols-4 gap-4 tablet:grid-cols-2 tablet:gap-4">
{isKpisLoading
? [0, 1, 2, 3].map((i) => <OverviewKpiCardSkeleton key={i} />)
: kpiList.map((kpi) => (
<StatCard
key={`d-${kpi.title}`}
{...kpi}
className="gap-3 py-5"
compact
/>
))}
</div>
)}
</ErrorBoundary>
<ErrorBoundary FallbackComponent={MetricErrorFallback} resetKeys={[kpis]}>
{isKpisError ? (
<div className="flex items-center justify-center rounded-3xl border border-surface-100/40 bg-surface-100/80 py-8 font-body2 text-info-red">
{kpisError?.message ??
"지표 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
</div>
) : (
<div className="grid min-w-0 grid-cols-4 gap-4 tablet:grid-cols-2 tablet:gap-4">
{isKpisLoading
? [0, 1, 2, 3].map((i) => <OverviewKpiCardSkeleton key={i} />)
: kpiList.map((kpi) => (
<StatCard
key={`d-${kpi.title}`}
{...kpi}
className="gap-3 py-5"
compact
/>
))}
</div>
)}
</ErrorBoundary>
🤖 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/pages/dashboard/overview/sections/OverviewKpiSection.tsx` around lines 38
- 58, The KPI ErrorBoundary around the `OverviewKpiSection` grid is missing
`resetKeys`, so `MetricErrorFallback` can stay stuck even after `kpis` are
refetched. Update the `ErrorBoundary` in `OverviewKpiSection` to use `resetKeys`
based on the `kpis` prop, matching the auto-reset behavior used in
`OverviewBudgetSection` and `OverviewDashboard` platform sections. This will let
the boundary recover automatically when the KPI data changes instead of
requiring a manual retry.

Comment on lines +32 to +56
export function encryptAesCbcBase64(plainText: string): string {
const secret = import.meta.env.VITE_NAVER_AES_SECRET as string | undefined;
const iv = import.meta.env.VITE_NAVER_AES_IV as string | undefined;

if (!secret || !iv) {
throw new Error("네이버 연동 암호화 설정이 없습니다.");
}

const keyWordArray = parseKeyOrIv(
secret,
"VITE_NAVER_AES_SECRET",
AES_KEY_BYTE_LENGTHS,
);
const ivWordArray = parseKeyOrIv(iv, "VITE_NAVER_AES_IV", [
AES_IV_BYTE_LENGTH,
]);

const encrypted = CryptoJS.AES.encrypt(plainText, keyWordArray, {
iv: ivWordArray,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});

return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file =="
sed -n '1,220p' src/utils/integration/encryptAesCbcBase64.ts

echo
echo "== package / vite indicators =="
grep -nE '"vite"|import\.meta\.env|VITE_' package.json vite.config.* 2>/dev/null || true

echo
echo "== usages of encryptAesCbcBase64 =="
rg -n "encryptAesCbcBase64|VITE_NAVER_AES_SECRET|VITE_NAVER_AES_IV" src . --glob '!**/node_modules/**'

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 3677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== naver integration api =="
sed -n '1,220p' src/api/integration/naver.ts

echo
echo "== vite config =="
sed -n '1,220p' vite.config.ts

echo
echo "== env references around naver =="
rg -n "NAVER_AES|encryptAesCbcBase64|apiKey:|secretKey:" src --glob '!**/node_modules/**'

Repository: WhereYouAd/WhereYouAd-Frontend

Length of output: 4701


클라이언트 번들에서 AES 비밀키를 제거해 주세요
src/utils/integration/encryptAesCbcBase64.ts:32-56에서 VITE_NAVER_AES_SECRET를 브라우저 번들에 포함된 값으로 쓰고 있어 비밀키가 노출됩니다. VITE_NAVER_AES_IV도 고정값이라 CBC 패턴이 반복 노출됩니다. 이 방식은 서버 측으로 옮기거나, 최소한 요청마다 무작위 IV를 생성해 함께 전달하도록 바꿔야 합니다.

🤖 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/utils/integration/encryptAesCbcBase64.ts` around lines 32 - 56, The
encryptAesCbcBase64 helper is using build-time client env values for AES
secret/IV, which exposes sensitive material in the browser bundle. Update
encryptAesCbcBase64 to stop relying on VITE_NAVER_AES_SECRET and the fixed
VITE_NAVER_AES_IV in client-side code; move this encryption to a server-side
path or redesign it so a fresh random IV is generated per call and transmitted
alongside the ciphertext. Keep the changes localized around encryptAesCbcBase64
and parseKeyOrIv usage so the browser no longer contains reusable secret
material or a static CBC IV.

import type { ITimelineSummaryPanelData } from "@/types/timeline/summary";
import { TIMELINE_METRIC_OPTIONS } from "@/constants/timeline/formOptions";

import { formatDot } from "./period";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

@/ alias 대신 상대 경로 import가 사용되었습니다.

"./period""@/utils/timeline/period"로 변경해야 합니다. As per coding guidelines, 모든 import에 @/ alias를 사용해야 합니다.

♻️ 제안하는 수정
-import { formatDot } from "./period";
+import { formatDot } from "`@/utils/timeline/period`";
📝 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.

Suggested change
import { formatDot } from "./period";
import { formatDot } from "`@/utils/timeline/period`";
🤖 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/utils/timeline/buildTimelineSummaryPanel.ts` at line 9, The import in
buildTimelineSummaryPanel should use the project’s "`@/`” alias instead of a
relative path. Update the existing formatDot import to reference
"`@/utils/timeline/period`" so the module follows the codebase import convention
and remains easy to locate alongside other timeline utilities.

Source: Coding guidelines

Comment on lines +67 to +77
function formatWeekLabel(start: Date, end: Date): string {
const startDay = start.getDate();
const endDay = end.getDate();
const startMonth = MONTH[start.getMonth()];
const endMonth = MONTH[end.getMonth()];

if (start.getMonth() === end.getMonth()) {
return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
}
return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

formatWeekLabel if/else 양 분기가 동일한 결과 반환

같은 달인지 여부에 따라 다른 포맷을 의도했던 것으로 보이지만, 두 분기가 완전히 동일한 템플릿 리터럴을 반환하고 있어 if/else가 dead code입니다. 같은 달일 때는 "29 - 31 January"처럼 월명을 한 번만 표시하는 것이 자연스러울 수 있습니다.

♻️ 제안 수정 (같은 달 축약 포맷)
 function formatWeekLabel(start: Date, end: Date): string {
   const startDay = start.getDate();
   const endDay = end.getDate();
   const startMonth = MONTH[start.getMonth()];
   const endMonth = MONTH[end.getMonth()];

   if (start.getMonth() === end.getMonth()) {
-    return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
+    return `${startDay} - ${endDay} ${startMonth}`;
   }
   return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
 }
📝 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.

Suggested change
function formatWeekLabel(start: Date, end: Date): string {
const startDay = start.getDate();
const endDay = end.getDate();
const startMonth = MONTH[start.getMonth()];
const endMonth = MONTH[end.getMonth()];
if (start.getMonth() === end.getMonth()) {
return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
}
return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
}
function formatWeekLabel(start: Date, end: Date): string {
const startDay = start.getDate();
const endDay = end.getDate();
const startMonth = MONTH[start.getMonth()];
const endMonth = MONTH[end.getMonth()];
if (start.getMonth() === end.getMonth()) {
return `${startDay} - ${endDay} ${startMonth}`;
}
return `${startDay} ${startMonth} - ${endDay} ${endMonth}`;
}
🤖 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/utils/timeline/period.ts` around lines 67 - 77, formatWeekLabel has
redundant if/else branches that both return the same string, so the month-aware
branch is dead code. Update formatWeekLabel to use different formatting in the
same-month case versus cross-month case, likely by showing the month only once
when start.getMonth() === end.getMonth() and keeping the full start/end month
format otherwise. Use the existing formatWeekLabel, MONTH, and date extraction
logic to make the intended week label concise and distinct across the two cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🌏 Deploy 배포 관련

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants