Skip to content

feat: 홈 API 연동 및 인증/캐러셀 개선#61

Merged
bk-git-hub merged 13 commits intodevelopfrom
feat/home-api-clean
Feb 15, 2026
Merged

feat: 홈 API 연동 및 인증/캐러셀 개선#61
bk-git-hub merged 13 commits intodevelopfrom
feat/home-api-clean

Conversation

@bk-git-hub
Copy link
Copy Markdown
Contributor

@bk-git-hub bk-git-hub commented Feb 14, 2026

📝 개요

홈 화면의 주요 섹션을 API 기반으로 리팩터링하고, OAuth/로그인 처리 및 캐러셀 렌더링 방식을 함께 개선했습니다.

🚀 주요 변경 사항

  • 홈 배너/추천 상품/추천 브랜드를 서버 컴포넌트 컨테이너 기반 API 연동 구조로 정리
  • OAuth 인증 요청/응답 로그에서 민감정보(code, token) 마스킹 처리 및 GET 요청 헤더 정리
  • 소셜 로그인 URL 생성 시 URLSearchParams 적용으로 쿼리 인코딩 안정화
  • 배너 캐러셀을 default export 패턴으로 통일하고 첫 슬라이드만 이미지 preload 적용
  • 추천 브랜드 빈 상태 문구를 로케일 기반으로 치환하고, 빈 배열 early return 처리
  • 홈 Suspense fallback을 섹션별 스켈레톤으로 변경해 레이아웃 이동(CLS) 완화

✅ 체크리스트

  • 빌드 테스트를 완료했나요?
  • 코드 컨벤션을 준수했나요?
  • 불필요한 console.log는 제거했나요?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 14, 2026

Walkthrough

OAuth 흐름과 JWT 토큰 갱신 경로에 방어 로직과 민감값 마스킹 유틸이 추가되고, 정적 UI를 서버-데이터 중심 컴포넌트로 전환해 배너/추천 섹션을 Suspense 경계에서 비동기 페칭하도록 리팩터링했습니다. 또한 S3 원격 이미지 패턴과 경량 i18n, 광고 타입·브랜드 타입이 추가되었습니다.

Changes

Cohort / File(s) Summary
인증 및 로깅 변경
auth.ts
로그 마스킹 유틸( REDACTED, SENSITIVE_TOKEN_KEYS, maskSensitiveUrl, maskTokenField, sanitizeLogData) 추가. OAuth 코드 URL-인코딩, Google은 GET/다른 제공자는 POST 분기, 요청/응답 및 에러 로그에 민감값 마스킹 적용. JWT 콜백에 refreshToken 부재 시 갱신 시도 금지 가드 추가.
Next.js 이미지 설정
next.config.ts
images.remotePatterns에 https://ongil-bucket.s3.ap-northeast-2.amazonaws.com 패턴 추가 (광고 이미지 허용).
로그인 UI (소셜) 변경
src/components/login/login-form.tsx
Kakao/Google 클라이언트 ID 런타임 검증 추가. 인증 URL을 URLSearchParams로 구성, 환경변수명 NEXT_PUBLIC_AUTH_GOOGLE_ID 사용, 오류 상태 표출 및 로깅 개선.
배너 캐러셀: 컨테이너 + 컴포넌트
src/components/banner-carousel/index.tsx, .../banner-carousel-container.tsx, .../carousel-with-dots.tsx
서버 컴포넌트 BannerCarouselContainer 추가: /advertisements/home에서 광고를 페칭해 CarouselWithDots에 전달. CarouselWithDots(자동재생 3s, 점 네비게이션, 이미지 오버레이) 추가. index 모듈이 컨테이너를 기본 재내보내기하도록 변경.
추천 상품 컨테이너
src/components/recommend-carousel/recommend-product-container.tsx
서버 컴포넌트로 endpoint+heading 받아 API에서 Product[] 페칭 후 RecommendCarousel로 렌더링하는 RecommendProductContainer 추가.
추천 브랜드: 서버/클라이언트 분리
src/components/recommend-brand/recommended-brand-container.tsx, .../recommended-brand-client.tsx
Container가 서버에서 BrandWithProducts를 페칭해 RecommendedBrandClient에 위임. Client는 selectedIndex 상태로 브랜드 전환 및 제품 그리드/플레이스홀더 렌더링.
페이지 레이아웃 업데이트
src/app/page.tsx
인라인 UI를 서버 컨테이너들(BannerCarouselContainer, RecommendProductContainer x2, RecommendedBrandContainer)로 교체, Suspense 폴백 추가.
도메인 타입 추가
src/types/domain/advertisement.ts, src/types/domain/brand.ts
새 인터페이스 Advertisement 추가(id,imageUrl,title,description). BrandWithProducts 추가(브랜드 + products: Product[]).
i18n 및 로케일
src/lib/i18n.ts, src/locales/en.ts, src/locales/ko.ts
경량 i18n 유틸(getLocaleFromDocument, t) 추가. 로케일 파일에 recommendedBrand.noProducts 메시지 추가(ko/en).

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser
  participant LoginUI as LoginForm (client)
  participant AppServer as NextAuth / auth.ts
  participant OAuth as OAuth Provider (Google/Kakao)
  participant Backend as Backend API

  Browser->>LoginUI: 소셜 로그인 클릭
  LoginUI->>OAuth: Redirect (code, URL-encoded)
  OAuth-->>Browser: Redirect back with code
  Browser->>AppServer: Callback request (code)
  AppServer->>Backend: 코드 교환 (POST/GET 분기, 마스킹 로깅)
  Backend-->>AppServer: tokens / error (마스킹)
  alt tokens
    AppServer-->>Browser: 세션 설정 / JWT 발급
  else error
    AppServer-->>Browser: 인증 실패 응답
  end
Loading
sequenceDiagram
  participant NextAuth as JWT Callback
  participant TokenStore as JWT/Session
  participant Backend as Auth Backend

  NextAuth->>TokenStore: 현재 토큰 조회 (refreshToken 여부 확인)
  alt no refreshToken
    NextAuth-->>TokenStore: 기존 토큰 반환 (갱신 미시도)
  else has refreshToken
    NextAuth->>Backend: refresh 요청 (마스킹 로깅)
    Backend-->>NextAuth: new tokens / error
    alt success
      NextAuth-->>TokenStore: 토큰 갱신 저장
    else failure
      NextAuth-->>TokenStore: 실패 경로 유지 (마스킹 로그)
    end
  end
Loading

Estimated code review effort

🎯 3 (보통) | ⏱️ ~25 minutes

Possibly related PRs

  • Feat/homepage api #58 — auth.ts의 토큰 갱신 및 JWT 콜백 처리 변경과 직접적인 중복/연속성 있음 (refresh 흐름 및 실패 처리 관련).
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항(홈 API 연동, 인증/캐러셀 개선)을 명확하게 요약하고 있으며, changeset의 핵심 내용과 정확히 일치합니다.
Description check ✅ Passed PR 설명이 홈 화면 리팩터링, OAuth 로깅 마스킹, 배너 캐러셀 최적화, 로케일 기반 텍스트 처리 등 changeset의 주요 변경사항과 밀접하게 관련되어 있습니다.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into develop

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

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@auth.ts`:
- Line 63: The console logs in auth.ts are exposing sensitive OAuth data
(backendUrl contains the authorization code and variables like
data.accessToken/data.refreshToken are printed); update logging in the OAuth
handling code (where console.log(`[${provider}] OAuth Request:`, method,
backendUrl) and other logs around the same block) to either remove these debug
logs in production or redact/mask sensitive values before logging: mask the
query param "code" inside backendUrl and replace token values (accessToken,
refreshToken, refresh_token, id_token, etc.) with a safe placeholder or
truncated form; implement and call a small helper (e.g.,
maskSensitiveUrl(backendUrl) and maskTokenField(value)) and use those helpers
when logging provider, method, and any response `data` to avoid printing raw
tokens or codes.

In `@src/components/banner-carousel/carousel-with-dots.tsx`:
- Line 20: The component is using a named export but must follow the "export
default function" pattern; change the declaration from "export function
CarouselWithDots({ advertisements }: CarouselWithDotsProps)" to a default
function declaration "export default function CarouselWithDots(props:
CarouselWithDotsProps)" (or keep destructuring inside the body) so it is a
default export and not an arrow function, and then update any imports that
referenced the named export to use the default import form; keep the
CarouselWithDots identifier and the CarouselWithDotsProps type unchanged.
- Around line 42-48: The Image components in carousel-with-dots.tsx currently
set priority on every slide; change the map callback that renders the slides to
accept the index (e.g., items.map((item, index) => ...)) and conditionally pass
priority only for the first slide (priority={index === 0}) when rendering <Image
src={item.imageUrl} alt={item.title} ... /> so only the initial slide is
preloaded.

In `@src/components/login/login-form.tsx`:
- Around line 24-26: The auth URL builder sets SCOPE = 'openid email profile'
but inserts it raw into authUrl, which can break the query string; update the
authUrl construction in the login-form component to URL-encode the scope value
(use encodeURIComponent on SCOPE) before concatenation (also consider encoding
callbackUrl and CLIENT_ID similarly) so the query param for scope is safely
encoded when creating authUrl.

In `@src/components/recommend-brand/recommended-brand-client.tsx`:
- Around line 38-40: The hardcoded English message in the RecommendedBrandClient
JSX should be replaced with an i18n lookup; locate the component in
recommended-brand-client.tsx and replace the string "No products available for
this brand." with a call to your translation method (e.g., using
useTranslation() -> t('recommendedBrand.noProducts') or
i18n.t('recommendedBrand.noProducts')), and add the corresponding translation
key/value to your locale files (Korean entry at minimum) so the UI shows the
localized text.
🧹 Nitpick comments (5)
auth.ts (2)

61-70: GET 요청에 Content-Type: application/json 헤더 불필요

Google OAuth는 GET으로 호출하는데, body가 없는 GET 요청에 Content-Type 헤더는 의미가 없습니다. provider별로 헤더를 분기하거나, POST일 때만 설정하는 것이 깔끔합니다.

제안
          const res = await fetch(backendUrl, {
            method,
-            headers: { 'Content-Type': 'application/json' },
+            ...(method === 'POST' && {
+              headers: { 'Content-Type': 'application/json' },
+            }),
          });

122-123: 기존 credentials-login에서도 토큰이 로그에 노출됨

이 라인은 이번 PR의 변경사항은 아니지만, dataaccessToken/refreshToken이 포함되어 있어 동일한 보안 우려가 있습니다. 위 OAuth 로그 정리 시 함께 제거하는 것을 권장합니다.

src/types/domain/brand.ts (1)

11-16: Brand를 확장하여 중복 필드를 줄이는 것을 고려해 보세요.

id, name, logoImageUrlBrand와 중복됩니다. logoImageUrl의 optionality 차이가 있으므로 다음과 같이 표현할 수 있습니다:

♻️ 제안
-export interface BrandWithProducts {
-  id: number;
-  name: string;
-  logoImageUrl: string;
-  products: Product[];
-}
+export interface BrandWithProducts extends Required<Pick<Brand, 'logoImageUrl'>> {
+  id: number;
+  name: string;
+  products: Product[];
+}

또는 API 응답 구조가 Brand와 다르다면 현재 형태를 유지하는 것도 괜찮습니다.

src/components/recommend-brand/recommended-brand-client.tsx (1)

12-18: brands가 빈 배열인 경우 빈 상태 처리가 없습니다.

brands[]이면 헤더에 빈 버튼 목록이 렌더링되고 "No products" 메시지만 표시됩니다. 상위 컨테이너에서 빈 배열을 걸러주거나, 여기서 early return을 고려해 주세요.

♻️ 제안
 export default function RecommendedBrandClient({
   brands,
 }: RecommendedBrandClientProps) {
+  if (brands.length === 0) return null;
+
   const [selectedIndex, setSelectedIndex] = useState<number>(0);
src/app/page.tsx (1)

15-38: Suspense fallback을 스켈레톤 UI로 교체 권장

모든 Suspense 경계에서 동일한 <div>Loading...</div> fallback을 사용하고 있습니다. 각 섹션의 레이아웃에 맞는 스켈레톤 컴포넌트를 제공하면 CLS(Cumulative Layout Shift)를 줄이고 사용자 체감 성능을 개선할 수 있습니다. 배너, 상품 캐러셀, 브랜드 섹션 각각의 높이/구조에 맞는 fallback이 이상적입니다.

Comment thread auth.ts Outdated
Comment thread src/components/banner-carousel/carousel-with-dots.tsx Outdated
Comment thread src/components/banner-carousel/carousel-with-dots.tsx
Comment thread src/components/login/login-form.tsx Outdated
Comment on lines +38 to +40
) : (
<p>No products available for this brand.</p>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

하드코딩된 영어 문구 — i18n 고려 필요.

한국어 서비스 기준으로 "No products available for this brand." 문구가 영어입니다. 한국어로 변경하거나 i18n 처리가 필요합니다.

🤖 Prompt for AI Agents
In `@src/components/recommend-brand/recommended-brand-client.tsx` around lines 38
- 40, The hardcoded English message in the RecommendedBrandClient JSX should be
replaced with an i18n lookup; locate the component in
recommended-brand-client.tsx and replace the string "No products available for
this brand." with a call to your translation method (e.g., using
useTranslation() -> t('recommendedBrand.noProducts') or
i18n.t('recommendedBrand.noProducts')), and add the corresponding translation
key/value to your locale files (Korean entry at minimum) so the UI shows the
localized text.

- OAuth 요청 URL 로그에서 code 파라미터를 마스킹 처리

- access/refresh/id token 계열 필드를 마스킹하는 sanitize 헬퍼 추가

- OAuth 성공/실패 응답 로그를 sanitize된 데이터로 출력

- Google(GET) 요청에는 Content-Type 헤더를 제거하고 POST일 때만 적용

- credentials-login 성공 로그도 sanitize 방식으로 변경
- CarouselWithDots를 named export에서 default function export로 변경

- BannerCarouselContainer import를 default import로 정리

- 배너 이미지 priority를 첫 슬라이드(index 0)에만 적용
- 문자열 결합 대신 URLSearchParams로 카카오/구글 인증 URL 구성

- scope, redirect_uri, client_id 파라미터가 안전하게 인코딩되도록 수정
- 추천 브랜드가 비어 있을 때 early return 처리

- 하드코딩된 빈 상태 문구를 번역 키 조회로 교체

- ko/en 로케일 사전과 경량 i18n 헬퍼 추가

- BrandWithProducts 타입을 Brand 기반으로 정리해 중복 필드 축소
- 공통 Loading 텍스트를 배너/카테고리/상품/브랜드 맞춤 스켈레톤으로 교체

- 섹션별 높이를 유지해 레이아웃 이동(CLS)을 줄이도록 조정
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@auth.ts`:
- Around line 10-17: SENSITIVE_TOKEN_KEYS currently misses common PII keys like
email and password; update the SENSITIVE_TOKEN_KEYS Set to include 'email' and
'password' (and optionally 'username'/'id' if you expect those in responses) so
sanitizeLogData will mask them when logging (used after credentials login);
modify the SENSITIVE_TOKEN_KEYS declaration to add these additional keys so
sensitive fields from the backend response are redacted.

In `@src/lib/i18n.ts`:
- Around line 19-25: Replace the hardcoded key branch in function t with a
runtime dot-path resolver: given the root object dictionaries[locale], split the
incoming key (TranslationKey) on '.' and traverse the nested objects (e.g.,
reduce over path segments) to resolve and return the string value; if any
segment is missing or resolution fails, return the original key as a fallback.
Ensure you reference the existing symbols dictionaries and t(locale: AppLocale,
key: TranslationKey) and that the resolver always returns a string (coerce
non-string values to string or fallback to key).
- Line 10: The TranslationKey type is hardcoded and causes duplication with the
t() branching; instead derive the key union from the translations dictionary and
make t() generic over those keys. Replace the manual export type TranslationKey
= 'recommendedBrand.noProducts' with a type alias like type TranslationMap =
typeof translations and export type TranslationKey = keyof TranslationMap (or
use directly in t), then change the t(key, ...) signature to accept K extends
keyof TranslationMap and return the appropriate string (and use typed lookups
like translations[key] rather than manual if/else branches) so adding keys to
the translations object auto-updates types and t() behavior; update any callers
to use the new typed key parameter (referencing TranslationKey, translations,
and t).
🧹 Nitpick comments (5)
src/components/banner-carousel/carousel-with-dots.tsx (3)

44-50: fill Image에 sizes prop 누락 — 불필요한 대용량 이미지 다운로드 발생 가능

fill 모드의 Imagesizes가 없으면 Next.js가 100vw를 기본값으로 사용하여, 실제 레이아웃보다 큰 srcset 후보를 preload합니다. max-w-200(800px) 컨테이너 기준으로 적절한 sizes 힌트를 제공하세요.

제안
               <Image
                 src={item.imageUrl}
                 alt={item.title}
                 fill
                 className="object-cover"
                 priority={index === 0}
+                sizes="(max-width: 800px) 100vw, 800px"
               />

41-62: advertisements가 빈 배열일 때 빈 캐러셀 렌더링 방지

빈 배열이면 Carousel + DotNavigation이 의미 없이 마운트됩니다. 상위 컨테이너에서 guard할 수도 있지만, 이 컴포넌트 자체에서도 early return을 두면 방어적입니다.

제안
 export default function CarouselWithDots({
   advertisements,
 }: CarouselWithDotsProps) {
+  if (advertisements.length === 0) return null;
+
   const [api, setApi] = useState<CarouselApi>();

26-66: 캐러셀 영역에 접근성 속성 부재

슬라이드 영역에 role="region" + aria-roledescription="carousel" + aria-label이 없으면 스크린 리더 사용자가 이 섹션의 목적을 파악하기 어렵습니다. 최소한 래퍼 div에 시맨틱 힌트를 추가하세요.

제안
-    <div className="w-screen max-w-200">
+    <div
+      className="w-screen max-w-200"
+      role="region"
+      aria-roledescription="carousel"
+      aria-label="배너 광고"
+    >
auth.ts (1)

37-55: sanitizeLogData에 순환 참조 보호 없음

JSON.parse에서 반환된 객체는 순환 참조가 없지만, 향후 다른 곳에서 이 유틸을 재사용할 경우 무한 재귀로 스택 오버플로우가 발생할 수 있습니다. seen WeakSet 가드 추가를 권장합니다.

♻️ 제안
-function sanitizeLogData(value: unknown): unknown {
+function sanitizeLogData(value: unknown, seen = new WeakSet()): unknown {
   if (Array.isArray(value)) {
-    return value.map(sanitizeLogData);
+    return value.map((v) => sanitizeLogData(v, seen));
   }
 
   if (value && typeof value === 'object') {
+    if (seen.has(value as object)) return '[Circular]';
+    seen.add(value as object);
     return Object.entries(value as Record<string, unknown>).reduce(
       (acc, [key, entryValue]) => {
         acc[key] = SENSITIVE_TOKEN_KEYS.has(key)
           ? maskTokenField(entryValue)
-          : sanitizeLogData(entryValue);
+          : sanitizeLogData(entryValue, seen);
         return acc;
       },
       {} as Record<string, unknown>,
     );
   }
 
   return value;
 }
src/components/login/login-form.tsx (1)

21-29: CLIENT_IDundefined일 때 빈 문자열로 fallback

process.env.NEXT_PUBLIC_AUTH_KAKAO_IDNEXT_PUBLIC_AUTH_GOOGLE_ID가 미설정 시 ?? ''로 빈 문자열이 전달되어, OAuth 프로바이더에서 불명확한 에러가 발생합니다. 조기에 에러를 던지거나 사용자에게 안내하는 것이 디버깅에 유리합니다.

♻️ 예시
   if (provider === 'kakao') {
     const CLIENT_ID = process.env.NEXT_PUBLIC_AUTH_KAKAO_ID;
+    if (!CLIENT_ID) {
+      console.error('Missing NEXT_PUBLIC_AUTH_KAKAO_ID');
+      setIsLoading(false);
+      return;
+    }
     const params = new URLSearchParams({
-      client_id: CLIENT_ID ?? '',
+      client_id: CLIENT_ID,

Comment thread auth.ts
Comment thread src/lib/i18n.ts Outdated
Comment thread src/lib/i18n.ts
@bk-git-hub bk-git-hub changed the title Feat/home api clean feat: 홈 API 연동 및 인증/캐러셀 개선 Feb 14, 2026
- SENSITIVE_TOKEN_KEYS에 email/password/username 추가

- sanitizeLogData에 WeakSet 기반 순환 참조 보호 추가
- 하드코딩 TranslationKey 제거 후 사전 구조 기반 키 유니온으로 전환

- t()에서 분기문 대신 dot-path reduce 해석 로직으로 문자열 조회
- 배너 캐러셀 빈 배열 early return, region/aria 속성 추가

- fill Image에 sizes 지정 및 첫 슬라이드만 preload 유지

- 소셜 로그인 CLIENT_ID 미설정 시 조기 에러 처리로 빈 문자열 전송 방지
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@auth.ts`:
- Around line 40-43: The Array branch in sanitizeLogData runs before checking
for circular references, so arrays that reference themselves can cause infinite
recursion; modify sanitizeLogData to first detect objects (typeof value ===
'object' && value !== null), then if seen.has(value) return a sentinel like
"[Circular]", otherwise seen.add(value) and only after that handle
Array.isArray(value) by mapping entries via sanitizeLogData(entry, seen); keep
the same seen WeakSet for nested object handling and do not add non-object
primitives to seen.

In `@src/components/banner-carousel/carousel-with-dots.tsx`:
- Around line 20-26: The early return in CarouselWithDots runs before hooks and
breaks the Rules of Hooks; move the guard that checks advertisements.length so
that the hooks are always called first: keep the useState<CarouselApi>() and the
useCarouselDots(api) calls (references: CarouselWithDots, useState,
useCarouselDots, api, setApi) at the top of the component, then after those
hooks return null if advertisements.length === 0 (or render an empty
placeholder) so hooks are invoked consistently across renders.
🧹 Nitpick comments (3)
src/components/banner-carousel/carousel-with-dots.tsx (2)

40-45: Autoplay 플러그인이 매 렌더마다 재생성됨

Autoplay({...})가 JSX 인라인에서 호출되어 매 렌더 시 새 인스턴스가 생성됩니다. plugins 배열 참조가 변경되면 Embla가 carousel을 재초기화할 수 있어 autoplay 타이머가 리셋됩니다.

useRef로 안정화하세요.

제안
+'use client';
+
+import { useState, useRef } from 'react';
 ...
+
 export default function CarouselWithDots({
   advertisements,
 }: CarouselWithDotsProps) {
+  const autoplayPlugin = useRef(
+    Autoplay({ delay: 3000, stopOnInteraction: false }),
+  );
   const [api, setApi] = useState<CarouselApi>();
   const { current, count, scrollTo } = useCarouselDots(api);
   ...
        plugins={[
-          Autoplay({
-            delay: 3000,
-            stopOnInteraction: false,
-          }),
+          autoplayPlugin.current,
        ]}

28-74: 접근성 및 렌더링 구조 — LGTM

role="region", aria-roledescription="carousel", priority={index === 0}, sizes 지정 모두 적절합니다. 이전 리뷰에서 제기된 사항들이 잘 반영되어 있습니다.

한 가지 참고: Line 33의 aria-label="배너 광고"가 하드코딩되어 있는데, PR에서 i18n 로케일 기반 문구 치환을 도입하고 있으므로 이 문자열도 t() 키로 전환하면 일관성이 높아집니다.

auth.ts (1)

116-122: URL 구성에 URLSearchParams 사용 권장.

현재 문자열 템플릿 + encodeURIComponent로 동작하지만, URLSearchParams를 사용하면 다중 파라미터 추가 시에도 안전하고 PR 설명과도 일치합니다.

♻️ 제안
-        const backendUrl = `${process.env.BACKEND_API_URL}/auth/oauth/${provider}?code=${encodeURIComponent(code)}`;
+        const url = new URL(`${process.env.BACKEND_API_URL}/auth/oauth/${provider}`);
+        url.searchParams.set('code', code);
+        const backendUrl = url.toString();

Comment thread auth.ts
Comment on lines +40 to +43
function sanitizeLogData(value: unknown, seen = new WeakSet<object>): unknown {
if (Array.isArray(value)) {
return value.map((entry) => sanitizeLogData(entry, seen));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

배열에 대한 순환 참조 검사 누락.

Array.isArray 분기가 seen WeakSet 검사보다 먼저 실행되어, 순환 참조를 가진 배열은 무한 재귀에 빠질 수 있습니다. 실제 로그 데이터에서 발생 확률은 낮지만 방어 코드의 일관성 측면에서 보완할 수 있습니다.

🛡️ 제안: 배열도 순환 참조 검사 추가
 function sanitizeLogData(value: unknown, seen = new WeakSet<object>): unknown {
+  if (value && typeof value === 'object') {
+    if (seen.has(value as object)) {
+      return '[Circular]';
+    }
+    seen.add(value as object);
+  }
+
   if (Array.isArray(value)) {
     return value.map((entry) => sanitizeLogData(entry, seen));
   }
 
-  if (value && typeof value === 'object') {
-    if (seen.has(value as object)) {
-      return '[Circular]';
-    }
-    seen.add(value as object);
-
+  if (value && typeof value === 'object') {
     return Object.entries(value as Record<string, unknown>).reduce(
🤖 Prompt for AI Agents
In `@auth.ts` around lines 40 - 43, The Array branch in sanitizeLogData runs
before checking for circular references, so arrays that reference themselves can
cause infinite recursion; modify sanitizeLogData to first detect objects (typeof
value === 'object' && value !== null), then if seen.has(value) return a sentinel
like "[Circular]", otherwise seen.add(value) and only after that handle
Array.isArray(value) by mapping entries via sanitizeLogData(entry, seen); keep
the same seen WeakSet for nested object handling and do not add non-object
primitives to seen.

Comment on lines +20 to +26
export default function CarouselWithDots({
advertisements,
}: CarouselWithDotsProps) {
if (advertisements.length === 0) return null;

const [api, setApi] = useState<CarouselApi>();
const { current, count, scrollTo } = useCarouselDots(api);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

React Rules of Hooks 위반 — early return이 useState보다 앞에 위치

Line 23의 early return이 Line 25의 useState 호출보다 먼저 실행됩니다. advertisements가 비어있을 때 hooks가 호출되지 않으므로, 배열이 비어있는 상태 ↔ 비어있지 않은 상태 간 전환 시 React가 "Rendered fewer hooks than expected" 에러를 발생시킵니다.

early return을 hooks 호출 이후로 이동시켜야 합니다.

제안
 export default function CarouselWithDots({
   advertisements,
 }: CarouselWithDotsProps) {
-  if (advertisements.length === 0) return null;
-
   const [api, setApi] = useState<CarouselApi>();
   const { current, count, scrollTo } = useCarouselDots(api);
 
+  if (advertisements.length === 0) return null;
+
   return (

Based on learnings: "ALWAYS use export default function for components (Named functions)." — 함수 선언 패턴은 준수하고 있으나, hooks 규칙이 우선입니다.

📝 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
export default function CarouselWithDots({
advertisements,
}: CarouselWithDotsProps) {
if (advertisements.length === 0) return null;
const [api, setApi] = useState<CarouselApi>();
const { current, count, scrollTo } = useCarouselDots(api);
export default function CarouselWithDots({
advertisements,
}: CarouselWithDotsProps) {
const [api, setApi] = useState<CarouselApi>();
const { current, count, scrollTo } = useCarouselDots(api);
if (advertisements.length === 0) return null;
return (
🤖 Prompt for AI Agents
In `@src/components/banner-carousel/carousel-with-dots.tsx` around lines 20 - 26,
The early return in CarouselWithDots runs before hooks and breaks the Rules of
Hooks; move the guard that checks advertisements.length so that the hooks are
always called first: keep the useState<CarouselApi>() and the
useCarouselDots(api) calls (references: CarouselWithDots, useState,
useCarouselDots, api, setApi) at the top of the component, then after those
hooks return null if advertisements.length === 0 (or render an empty
placeholder) so hooks are invoked consistently across renders.

@bk-git-hub bk-git-hub merged commit d88a596 into develop Feb 15, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant