Skip to content

Conversation

@hyesngy
Copy link
Member

@hyesngy hyesngy commented Nov 30, 2025

✨ 작업 개요

코드 리팩토링

📌 관련 이슈

✅ 작업 내용

코드 리팩토링

📷 UI 스크린샷 (해당 시)

💬 기타 사항

Summary by CodeRabbit

  • New Features

    • 챗봇 대화 훅 도입으로 채팅 응답·추천이 더 매끄러워짐
    • 공유 기능(페이지/용어 공유) 및 공유 버튼 추가
    • 스크랩 버튼 컴포넌트 도입으로 스크랩 UX 개선
    • 퀴즈 점수 카드와 오답 카드 UI 추가
  • Improvements

    • 인증과 사용자 데이터 분리로 로그인·온보딩 흐름 안정화
    • 카테고리 설정 중앙화로 라벨·색상 일관성 제공
    • 헤더 구성요소 재구성 및 네비게이션 정비
    • 브랜드 그라디언트 적용 및 정렬/날짜 유틸 통일
  • Removed

    • 사용되지 않는 일부 아이콘 컴포넌트 제거

✏️ Tip: You can customize this high-level summary in your review settings.

@hyesngy hyesngy self-assigned this Nov 30, 2025
@hyesngy hyesngy added the ✨ style 코드 포맷팅, 코드 오타, 함수명 수정 등 스타일 label Nov 30, 2025
@hyesngy hyesngy requested a review from a team as a code owner November 30, 2025 23:17
@hyesngy hyesngy added the 🧹 Refactor 코드 리팩토링 label Nov 30, 2025
@hyesngy hyesngy linked an issue Nov 30, 2025 that may be closed by this pull request
1 task
@vercel
Copy link

vercel bot commented Nov 30, 2025

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

Project Deployment Preview Comments Updated (UTC)
gotit Ready Ready Preview Comment Nov 30, 2025 11:55pm

@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Walkthrough

카테고리·역할·테마 설정을 중앙화하고 인증을 분리(AuthCore, UserData, Scrap)했으며 여러 훅(useChatBot, useQuizState, useShare, useDropdown 등)과 유틸(date/sorting)을 도입하고 아이콘/레거시 카테고리 모듈을 정리했습니다. UI 컴포넌트들은 새 설정·훅에 맞춰 리팩터링되었습니다.

Changes

Cohort / File(s) 변경 요약
설정 중앙화
src/config/categories.ts, src/config/roles.ts, src/constants/theme.ts
카테고리·역할·브랜드 그라디언트 중앙 설정(CATEGORIES, ROLE_CONFIG, BRAND_GRADIENT, CATEGORY_ROWS 등) 추가
인증 분리 및 재구성
src/contexts/AuthContext.tsx, src/contexts/auth/*, src/contexts/auth/index.tsx
기존 단일 AuthContext 제거, Firebase 기반 AuthProvider(useAuthCore), UserDataProvider, ScrapProvider 도입 및 CombinedAuthProvider로 집계
프로바이더/컨텍스트 사용처 업데이트
src/components/providers/Providers.tsx, 여러 컴포넌트 (src/app/*, src/components/*)
Providers에서 AuthProvider → CombinedAuthProvider로 교체, useAuth → useAuthCore / useUserData / useScrap로 사용처 전환
아이콘 및 레거시 카테고리 제거/대체
src/components/icons/*, src/components/ui/category/*, src/types/category.ts, src/components/ui/category/config.ts
개별 아이콘 파일 및 레거시 category config/exports 삭제, CategoryChip/ScrapButton 등으로 대체, 관련 index 재정리
헤더 파편화 및 재구성
src/components/layout/Header.tsx, src/components/layout/header-parts/*, src/components/layout/header-parts/index.ts
Header 내부 서브컴포넌트(Logo, NavItem, LoginButton, ProfileDropdown) 추출 및 재사용 모듈화, 타입/기본값 변경, useAuthCore 사용
대시보드·프로필·스크랩 UI 리팩터링
src/app/dashboard/components/*, src/components/term-detail/*, src/components/search/*, src/components/ui/buttons/ScrapButton.tsx
CATEGORIES 기반 렌더링으로 전환, ScrapButton/CategoryChip 도입, BRAND_GRADIENT 적용, 훅(useUserData/useScrap) 사용 위치 변경
퀴즈·온보딩·채팅 훅 추가 및 적용
src/hooks/useQuizState.ts, src/hooks/useChatBot.ts, src/hooks/useShare.ts, src/hooks/useDropdown.ts, 관련 컴포넌트
퀴즈/채팅/공유/드롭다운 훅 추가; QuizSession, QuizResult, ChatBot 등에서 훅 사용으로 상태 로직 이동
스크랩·북마크 로직 정비
src/contexts/auth/ScrapContext.tsx, src/lib/bookmarks.ts, src/hooks/useScrapToggle.ts
ScrapContext에 in-flight 보호 추가, 일부 로컬 북마크 헬퍼 제거, 훅에서 useScrap 사용으로 분리
타입·도메인 분리
src/types/terms.ts, src/types/quiz.ts, src/types/scrapCard.ts, src/lib/quiz.ts, src/lib/terms.ts
용어·퀴즈·스크랩 카드 타입을 전용 파일로 이동/추가, lib 로직이 중앙 타입을 사용하도록 업데이트
유틸·라이브러리 변경
src/utils/sorting.ts, src/utils/date.ts, src/lib/scrap.ts, src/lib/sortTerms.ts
정렬·날짜 유틸 추가(sortByKorean, sortByDateDesc, formatKoreanDate), scrap/terms 관련 내부 의존성 업데이트 및 리팩터링
채팅 컴포넌트 훅화
src/app/chatbot/components/ChatBot.tsx, src/hooks/useChatBot.ts
ChatBot의 상태/로직을 useChatBot 훅으로 이관, 컴포넌트는 훅 반환값만 소비

Sequence Diagram(s)

sequenceDiagram
  participant Client as UI (Login / Navigation)
  participant AuthCore as AuthProvider (useAuthCore)
  participant UserData as UserDataProvider
  participant Scrap as ScrapProvider
  participant Firestore as Firestore (users collection)

  Note over Client,AuthCore: 사용자 로그인 시 플로우
  Client->>AuthCore: loginWithGoogle / loginWithDemo
  AuthCore->>Firestore: Firebase auth state change -> uid
  AuthCore->>UserData: notify user change (uid)
  UserData->>Firestore: fetchUserData(uid)
  alt no userData
    UserData->>Firestore: createUserData(user, migratedScraps)
  else existing user
    UserData->>Firestore: mergeScrapList(if local items)
  end
  UserData-->>Client: userData + isNewUser/onboarding state
  Client->>Scrap: toggleScrap(termId) (via useScrap)
  Scrap->>Firestore: update user's scrapList (with in-flight protection)
  Scrap-->>Client: { success, isScraped }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

  • 집중 검토 권장 파일/영역:
    • src/contexts/auth/** (AuthCore, UserDataProvider, ScrapProvider — 인증·동기화·병합·in-flight 로직)
    • src/config/categories.ts / usages (모든 category 참조가 새 config로 올바르게 전환되었는지)
    • src/lib/terms.ts 및 validateIndex (타입 재배치와 중복 검사 로직)
    • src/lib/userService.ts (Firestore 쓰기/병합 로직의 정확성 및 에러/경계조건)
    • 아이콘 삭제 영향: 삭제된 아이콘을 참조하는 남은 코드 존재 여부
    • hooks with side effects: useChatBot, useQuizState, useShare (비동기/에러 처리 경로)

Possibly related PRs

  • #56 — ChatBot 훅 및 채팅 동작 변경들과 관련 (ChatBot/useChatBot 연관)
  • #58 — 스크랩/북마크 리팩터링 및 ScrapContext 변경과 직접 연관
  • #61 — 용어 인덱스 생성 및 중복 검증(validateIndex) 관련 변경과 연관

Poem

🐰 깡총, 설정은 한곳에 모였네
인증은 나눠 맡아 차례로 보았네
훅들은 춤추고 타입들은 집으로
아이콘은 손 흔들며 안녕, 당근 축하해요! 🥕🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive 제목 '[Refactor/82] 코드 리팩토링'은 포괄적이고 구체성이 부족하여 변경 사항을 명확하게 설명하지 못합니다. 제목을 더 구체적으로 수정하여 주요 리팩토링 작업을 명시하세요. 예: '[Refactor/82] 인증 시스템 및 카테고리 설정 중앙화' 또는 유사한 구체적 설명.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed PR의 변경 사항들이 이슈 #82의 '프로젝트 전체 코드 리팩토링' 목표를 달성합니다. 인증 시스템 분리, 설정 중앙화, 컴포넌트 추출 등이 포함됩니다.
Out of Scope Changes check ✅ Passed 모든 변경 사항이 리팩토링 범위 내이며, 기능 추가나 버그 수정 없이 코드 구조 개선과 설정 중앙화에 집중하고 있습니다.
✨ 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 refactor/82

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

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

🧹 Nitpick comments (28)
src/hooks/useScrapToggle.ts (1)

21-25: 사용자 변경 시 로컬 상태 초기화 로직이 좋습니다.

로그인/로그아웃 시 localOverride를 초기화하여 이전 사용자의 로컬 상태가 남지 않도록 처리한 점이 훌륭합니다. 다만 이 비교는 렌더링 중에 실행되므로, 이상적으로는 useEffect에서 처리하는 것이 React 권장사항에 더 부합합니다.

다음과 같이 변경할 수 있습니다:

-	// user가 변경되면 로컬 오버라이드 초기화
-	if (user !== lastUser) {
-		setLastUser(user);
-		setLocalOverride(null);
-	}
+	useEffect(() => {
+		if (user !== lastUser) {
+			setLastUser(user);
+			setLocalOverride(null);
+		}
+	}, [user, lastUser]);
src/types/terms.ts (2)

14-14: level 타입 중복 - 타입 별칭 추출 권장

"beginner" | "intermediate" | "advanced" 유니온이 TermIndexItem(라인 14)과 TermDetail(라인 52)에 중복됩니다. 유지보수성을 위해 별도 타입으로 추출하는 것을 권장합니다.

+// 난이도 레벨 타입
+export type Level = "beginner" | "intermediate" | "advanced";
+
 // Index 아이템 타입
 export interface TermIndexItem {
 	id: number;
 	slug: string;
 	termKo: string;
 	termEn?: string;
 	summary: string;
 	tags: string[];
 	primaryTag: string;
-	level: "beginner" | "intermediate" | "advanced";
+	level: Level;
 	file: string;
 }

27-31: @deprecated JSDoc 주석 추가 권장

주석으로 deprecated를 표시했지만, IDE 지원을 위해 @deprecated JSDoc 어노테이션을 사용하면 더 좋습니다. 또한 Conversation.roleRole 타입의 값이 다릅니다 (pm vs PM, developer vs Dev).

-// 대화 상황 타입 (deprecated - useCases로 대체)
+/**
+ * 대화 상황 타입
+ * @deprecated useCases로 대체되었습니다
+ */
 export interface Conversation {
 	role: "pm" | "developer" | "designer";
 	message: string;
 }
src/components/term-detail/tabs/DescriptionTab.tsx (1)

44-62: 리스트 렌더링 key 사용 검토

idx를 key로 사용하고 있습니다. 태그와 키워드가 정적 데이터라면 큰 문제는 없지만, 안정적인 고유 식별자(예: tag 값 자체)를 key로 사용하면 React 재조정 성능이 향상됩니다. 기존 코드이므로 이번 PR 범위 외일 수 있습니다.

 {term.tags
 	.filter((tag) => tag !== term.primaryTag)
 	.map((tag, idx) => (
 		<span
-			key={idx}
+			key={tag}
 			className="rounded-full bg-gray-900 px-2 py-0.5 text-[10px] leading-3.5 font-light text-gray-300"
 		>
 			# {tag}
 		</span>
 	))}
src/components/search/SearchBar.tsx (2)

23-25: cn() 유틸리티 사용 권장

이 파일에서 cn() 유틸리티가 import되어 있지 않지만, 다른 컴포넌트에서 cn()을 사용한다면 일관성을 위해 여기서도 사용하는 것을 고려해 보세요.

+import { cn } from "@/utils/cn";
...
 <div
-	className={`absolute top-0 left-0 h-20 w-[864px] rounded-[20px] border border-violet-700 ${BRAND_GRADIENT.bg} opacity-30 blur-sm`}
+	className={cn(
+		"absolute top-0 left-0 h-20 w-[864px] rounded-[20px] border border-violet-700 opacity-30 blur-sm",
+		BRAND_GRADIENT.bg
+	)}
 ></div>

18-28: 고정 너비 값 상수화 검토

864px, 856px 같은 매직 넘버가 여러 곳에 반복됩니다. 상수로 추출하면 유지보수가 용이해집니다. 이번 PR 범위 외일 수 있지만, 향후 리팩토링 시 고려해 주세요.

src/components/term-detail/types.ts (1)

1-8: 카테고리 설정 중앙화 연동이 일관성 있게 잘 바뀌었습니다

getCategoryTypeCATEGORIES[category]로 이어지는 흐름이 명확하고, 기존 getCategoryConfig의 사용 방식도 그대로 유지돼서 리팩터링 방향 좋습니다.

선택 사항이지만, 반환 값을 명시적으로 타입 선언해 두면(예: { category: CategoryType; config: CategoryConfig }) 이후 사용처에서 타입 추론과 리팩터링 시 좀 더 안전하게 동작할 것 같습니다.

src/app/chatbot/components/ChatBot.tsx (1)

4-19: useChatBot 훅으로 상태/이펙트 분리한 구조가 깔끔합니다

채팅 상태, 로딩, 스크롤 처리까지 훅으로 위임해서 ChatBot 컴포넌트가 뷰 레이어에만 집중하게 된 점이 좋습니다. QuickActionButtontextarea의 엔터 처리도 handleSubmit 시그니처와 잘 맞습니다.

추가로, 추후 메시지에 고유 ID가 생긴다면 key={index} 대신 ID 기반 key를 쓰는 정도만 고려해 보시면 좋겠습니다.

src/app/onboarding/page.tsx (1)

8-57: Auth/UserData 컨텍스트 분리가 의도에 잘 맞습니다

useAuthCore로 인증 상태만, useUserData로 온보딩 완료 액션만 가져오는 구조가 역할 분리가 명확해서 유지보수에 유리해 보입니다. handleSkip/handleStart에서 공통으로 비로그인 사용자를 홈으로 돌려보내는 처리도 자연스럽습니다.

선택 사항이지만, 온보딩 완료 후 브라우저 히스토리에 남지 않게 하려면 router.push("/") 대신 router.replace("/")를 사용하는 것도 한 번 검토해 볼 수 있겠습니다.

src/components/layout/header-parts/LoginButton.tsx (1)

1-15: 폼 내 사용 가능성을 고려해 type="button" 지정 권장

지금 구현은 <button> 기본값 때문에 폼 내부에서 사용될 경우 의도치 않게 submit 이벤트를 발생시킬 수 있습니다. 헤더 로그인 버튼은 제출 용도가 아니므로 type="button"을 명시하는 편이 안전합니다.

아래처럼 수정하는 것을 제안드립니다.

-export function LoginButton({ onClick }: LoginButtonProps) {
-	return (
-		<GlassButton className="px-6 py-2" onClick={onClick}>
+export function LoginButton({ onClick }: LoginButtonProps) {
+	return (
+		<GlassButton type="button" className="px-6 py-2" onClick={onClick}>
 			<span className="text-base leading-7 font-bold text-neutral-300 transition-colors hover:text-white">
 				로그인
 			</span>
 		</GlassButton>
 	);
}

텍스트 컬러 호버를 버튼 전체 호버와 연동하고 싶다면, GlassButton에 group 클래스를 주고 group-hover:text-white로 바꾸는 것도 선택지입니다.

src/components/term-detail/tabs/UseCaseTab.tsx (1)

2-4: ROLE_CONFIG 기반으로 역할 설정을 중앙화한 점이 좋습니다

로컬 roleConfig를 제거하고 ROLE_CONFIG를 사용해 라벨/색상/아이콘을 일관되게 가져오는 구조라 다른 화면과 스타일을 맞추기 용이해졌습니다.

백엔드에서 useCase.role에 예상치 못한 값이 들어올 가능성을 조금이라도 방어하고 싶다면, 아래처럼 기본값을 두는 것도 고려해 볼 수 있습니다.

-const config = ROLE_CONFIG[useCase.role];
+const config = ROLE_CONFIG[useCase.role] ?? ROLE_CONFIG.Other;

데이터가 항상 검증된다는 전제가 명확하다면 지금 구현도 충분히 괜찮습니다.

Also applies to: 43-45

src/components/term-detail/HeroSection.tsx (1)

11-12: ScrapButton으로 북마크 액션을 공통화한 점이 좋습니다

히어로 섹션에서도 ScrapButton을 사용하도록 통일해서 아이콘/배경 스타일이 다른 영역과 자동으로 맞춰지는 구조가 잘 잡힌 것 같습니다. bookmarked/onBookmark만 넘기면 되도록 만든 API도 깔끔합니다.

추가로, ScrapButton 쪽(별도 파일)에서 토글 버튼 패턴에 맞춰 aria-pressed={bookmarked}까지 설정해 주면 스크린 리더 사용성도 더 좋아질 것 같습니다.

Also applies to: 61-61

src/components/quiz/CategorySelection.tsx (1)

4-6: CATEGORY_ROWS 기반 렌더링으로 중복이 잘 제거되었습니다

카테고리 행을 하드코딩 배열 대신 CATEGORY_ROWS 설정으로 렌더링하게 바꿔서, 온보딩 등 다른 화면과 카테고리 구성이 자연스럽게 동기화되는 점이 좋습니다.

한 가지 개선 포인트는 handleStartQuiz의 로딩 상태 처리입니다. 현재는 성공 시에는 setIsLoading(false)가 호출되지 않아, 같은 컴포넌트를 다시 보여 주는 시나리오에서는 버튼이 계속 비활성화로 남을 수 있습니다. 아래처럼 finally에서 공통으로 해제하는 패턴을 추천드립니다.

-		setIsLoading(true);
-
-		try {
+		setIsLoading(true);
+
+		try {
 			const questions = await generateQuizQuestions(
 				selectedCategory,
 				questionCount
 			);
 			onCategorySelect(selectedCategory, questions);
 		} catch (err) {
 			showToast(
 				err instanceof Error ? err.message : "퀴즈 생성에 실패했습니다.",
 				"error"
 			);
-			setIsLoading(false);
-		}
+		} finally {
+			setIsLoading(false);
+		}

부모에서 바로 다른 화면으로 전환해 주는 구조라면 지금도 큰 문제는 아니지만, 재사용 가능성을 고려하면 위와 같이 정리해 두는 게 조금 더 안전합니다.

Also applies to: 66-77

src/hooks/useDropdown.ts (1)

1-36: 드롭다운 열림 상태를 캡슐화한 훅 구현이 간결하고 재사용성이 높습니다

isOpen, toggle, close, dropdownRef만 노출해서 ProfileDropdown 등에서 쉽게 쓸 수 있는 형태라 좋고, 바깥 클릭을 useEffect로 한 번만 바인딩하는 방식도 직관적입니다.

선택 사항으로, 열려 있을 때만 이벤트 리스너를 등록/해제하는 패턴(의존성 배열에 isOpen 추가 후 if (!isOpen) return;)을 쓰면 비활성 상태에서의 불필요한 이벤트 처리를 줄일 수는 있겠지만, 현재 규모에서는 지금 구현도 충분히 가볍다고 보입니다.

src/lib/scrap.ts (1)

2-4: 날짜/카테고리 유틸 중앙화는 좋고, 날짜 인자만 나중에 확장 여지가 있어 보입니다

ScrapCardData, 카테고리 헬퍼, formatKoreanDate로 의존성을 정리한 방향은 👍 입니다.
지금은 항상 호출 시점의 날짜(오늘)가 들어가는데, 나중에 스크랩한 날짜나 원본 데이터의 날짜를 표시해야 하는 요구가 생기면 formatKoreanDate(term.updatedAt ?? new Date()) 처럼 명시적으로 Date를 넘기는 형태로 바꾸기만 하면 될 것 같아요. 지금 상태에서도 동작상 문제는 없어 보입니다.

Also applies to: 20-20

src/app/terms/[slug]/page.tsx (1)

9-11: Auth/Scrap/Share 훅 분리 방향은 좋고, isScraped 의존성만 한 번 점검해 주세요

  • useAuthCore / useScrap / useShare로 관심사가 잘 분리돼서 가독성과 재사용성이 확실히 좋아진 것 같습니다.
  • useEffect의 의존성 배열에 isScraped가 들어가 있는데, useScrap 내부에서 isScrapeduseCallback 등으로 메모이즈하지 않았다면, 렌더마다 참조가 바뀌면서 용어/연관 용어를 매번 다시 로드할 수 있습니다. 이미 메모이즈돼 있다면 그대로 두셔도 괜찮고, 아니라면 isScraped를 의존성에서 빼고(또는 별도 useEffect로 분리하고) 초기 북마크 판별만 여기서 하도록 정리하는 것도 고려할 만합니다.
  • 공유 핸들러에서 shareCurrentPage(term.term.en || term.term.ko, term.summary)만 호출하도록 정리한 건 UI 쪽에서 공유 전략을 통일하는 데 도움이 될 것 같습니다.

Also applies to: 19-22, 49-49, 74-77

src/components/layout/header-parts/NavItem.tsx (1)

1-25: NavItem 컴포넌트 구조는 깔끔하고, 고정 폭만 추후 디자인에 맞춰 확인해 보시면 좋겠습니다

  • label, href, isActive만 받는 단순한 API에 data-isactive로 상태를 노출하는 패턴이 깔끔합니다. active/hover 컬러 처리도 직관적이에요.
  • 다만 className="... w-24 ..."라서 라벨이 길어질 경우 줄바꿈/잘림 이슈가 생길 수 있으니, 실제 메뉴 텍스트 길이에 따라 필요하면 w-24를 유동 폭(flex 기반)으로 조정하는 것도 한 번 고려해 보셔도 좋겠습니다.
src/config/roles.ts (1)

5-46: 역할(Role) 설정 구조 적절하며, 아이콘 타입 추상화는 선택적으로 고려 가능

  • Role 유니온 타입을 key 로 쓰는 ROLE_CONFIG: Record<Role, RoleConfig> 정의가 명확하고, PM/Dev/Design/Marketer/Other 각각에 레이블·색·배경·아이콘이 잘 매핑돼 있습니다.
  • icon: typeof PmIcon 으로 타입을 잡은 것은 구조적으로 문제는 없지만, 향후 다른 아이콘 컴포넌트와의 결합도를 낮추고 싶다면 type RoleIcon = typeof PmIcon; 또는 공용 IconComponent 타입을 정의해 두고 그 별칭을 쓰는 방식도 고려해 볼 수 있습니다.
src/components/ui/buttons/ScrapButton.tsx (1)

3-50: ScrapButton 재사용성은 좋지만, 타입/버튼 기본 동작 두 가지는 보완을 권장합니다

  1. React.MouseEvent 타입 사용 시 React 네임스페이스

    • 현재 파일에서 React.MouseEvent를 사용하지만 React를 import 하지 않고 있어, TS 설정에 따라 Cannot find namespace 'React' 에러가 날 수 있습니다.
    • 보다 안전하게는 타입 전용 import 후 MouseEventHandler를 쓰는 편을 권장합니다.
  2. 기본 button type 명시

    • 재사용 가능한 버튼 컴포넌트인 만큼, 폼 안에서 사용될 가능성을 고려하면 기본 type 이 submit 이 아니라 button 이 되도록 명시하는 것이 안전합니다.

예시 diff:

+"use client";
+
+import type React from "react";
 import { cn } from "@/utils/cn";
 import { ScrapIcon } from "@/components/icons";
@@
 interface ScrapButtonProps {
 	bookmarked: boolean;
-	onClick: (e: React.MouseEvent) => void;
+	onClick: React.MouseEventHandler<HTMLButtonElement>;
 	size?: ScrapButtonSize;
 	className?: string;
 }
@@
 	return (
-		<button
-			onClick={onClick}
+		<button
+			type="button"
+			onClick={onClick}
 			className={cn(
src/app/dashboard/components/DashboardClient.tsx (1)

7-8: auth/userData/scrap 로딩 상태 분리가 명확하고, scrap 재로딩 시 UX 개선 여지는 조금 있습니다

  • useAuthCore / useUserData 로 auth 와 userData 로딩을 분리하고, 스크랩 섹션만 scrapLoading 으로 따로 관리하는 구조가 읽기 쉽고 책임도 잘 나뉘어 있습니다.
  • loading = authLoading || userDataLoading 을 기준으로 전체 페이지 스피너를, scrapLoading 으로 스크랩 섹션 로딩만 제어하는 패턴도 합리적입니다.
  • 다만 useEffect 안에서 userData 가 변경되어 스크랩 목록을 다시 불러오는 상황(예: 스크랩 리스트를 서버에서 재동기화)이 있다면, loadScrapTerms 시작 시 setScrapLoading(true) 를 먼저 호출해 재요청 때도 로딩 상태를 보여주는 쪽이 UX 측면에서 조금 더 친절할 수 있습니다. 현재 구현도 초회 로딩만 고려한다면 문제는 없습니다.

Also applies to: 16-27, 30-45, 48-59, 82-89

src/app/dashboard/components/CategoryTag.tsx (1)

3-4: 카테고리 태그를 CATEGORIES 설정 기반으로 단일화한 점이 좋고, 상호작용 엘리먼트 타입만 추후 보완해도 좋겠습니다

  • getCategoryType + CATEGORIES[categoryType] 로 아이콘/색/hover/selected 상태를 모두 설정에서 가져오는 구조가 깔끔하고, 다른 컴포넌트와의 일관성 유지에도 유리합니다.
  • 현재 onClick 이 달린 루트가 div 이라서 키보드 포커스/엔터 동작 측면에서 접근성이 부족할 수 있습니다. 구조를 크게 바꾸지 않는 선에서, 추후 다음과 같이 button 으로 전환하는 것도 고려해 볼 만합니다.
-		<div onClick={onClick} className={`${baseClasses} ${stateClasses}`}>
+		<button
+			type="button"
+			onClick={onClick}
+			className={`${baseClasses} ${stateClasses}`}
+		>
@@
-		</div>
+		</button>

Also applies to: 16-37

src/hooks/useChatBot.ts (1)

38-62: 의존성 배열 최적화를 고려해보세요.

handleSubmit의 의존성 배열에 input이 포함되어 있지만, input은 함수 호출 시점에 캡처되고 즉시 초기화됩니다(line 46). customInput 매개변수를 통한 호출도 지원하므로, input을 의존성에서 제거해도 동작에 문제가 없을 수 있습니다.

다음과 같이 수정을 고려해보세요:

  const handleSubmit = useCallback(
    async (e?: React.FormEvent, customInput?: string) => {
      e?.preventDefault();
      const userMessage = customInput || input;

      if (!userMessage.trim() || isLoading) return;

      setMessages((prev) => [...prev, { role: "user", content: userMessage }]);
      setInput("");
      setIsLoading(true);

      const result = await getChatResponse(userMessage);

      setMessages((prev) => [
        ...prev,
        {
          role: "bot",
          content: result.answer,
          recommendations: result.recommendations,
        },
      ]);
      setIsLoading(false);
    },
-   [input, isLoading]
+   [isLoading, setInput, setMessages]
  );

또는 setInputsetMessages는 안정적이므로:

- [input, isLoading]
+ [isLoading]
src/contexts/auth/AuthContext.tsx (1)

89-101: loginWithGoogle, loginWithDemo, logout 함수가 매 렌더링마다 재생성됩니다.

현재 구현에서는 이 함수들이 컴포넌트가 렌더링될 때마다 새로운 참조를 생성합니다. 이로 인해 하위 컴포넌트에서 불필요한 리렌더링이 발생할 수 있습니다.

useCallback으로 함수들을 메모이제이션하는 것을 고려해보세요:

+import { useCallback } from "react";
+
-	const loginWithGoogle = async (): Promise<User> => {
+	const loginWithGoogle = useCallback(async (): Promise<User> => {
 		const provider = new GoogleAuthProvider();
 		try {
 			const result = await signInWithPopup(auth, provider);
 			return result.user;
 		} catch (error) {
 			console.error("Google 로그인 실패:", error);
 			throw error;
 		}
-	};
+	}, []);
src/components/quiz/QuizResult.tsx (1)

44-51: 순차적 스크랩 요청으로 인한 성능 저하 가능성

for...of 루프 내에서 await toggleScrap()을 순차적으로 호출하면 오답이 많을 경우 응답 시간이 길어질 수 있습니다.

병렬 처리를 고려해보세요:

-			for (const question of wrongQuestions) {
-				if (!isScraped(question.term.id)) {
-					await toggleScrap(question.term.id);
-					scrapCount++;
-				}
-			}
+			const unscrapedQuestions = wrongQuestions.filter(
+				(q) => !isScraped(q.term.id)
+			);
+			await Promise.all(
+				unscrapedQuestions.map((q) => toggleScrap(q.term.id))
+			);
+			scrapCount = unscrapedQuestions.length;
src/config/categories.ts (2)

167-182: LABEL_TO_CATEGORY 매핑이 CATEGORIES와 중복됩니다.

CATEGORIES에 이미 라벨 정보가 있으므로, 역매핑을 자동 생성하는 것을 고려해보세요. 단, UX/UI 변형(UI/UX, UX/UI디자인 등)의 별칭 처리가 필요하여 현재 방식도 유효합니다.

동적 생성 예시 (별칭 제외):

const LABEL_TO_CATEGORY: Record<string, CategoryType> = {
	...Object.fromEntries(
		CATEGORY_KEYS.map((key) => [CATEGORIES[key].label, key])
	),
	// 별칭은 수동 추가
	"UI/UX": "uxui",
	"UX/UI디자인": "uxui",
	"UI/UX디자인": "uxui",
};

160-162: getCategoryLabel 폴백이 버그를 숨길 수 있습니다.

잘못된 카테고리 키가 전달되면 해당 키를 그대로 반환합니다. 개발 모드에서 경고를 추가하거나 명시적인 처리를 고려해보세요.

 export function getCategoryLabel(category: string): string {
+	const label = CATEGORIES[category as CategoryType]?.label;
+	if (!label && process.env.NODE_ENV === "development") {
+		console.warn(`Unknown category: ${category}`);
+	}
-	return CATEGORIES[category as CategoryType]?.label || category;
+	return label || category;
 }
src/lib/userService.ts (1)

76-85: completeUserOnboarding도 동일한 문서 존재 가정 문제가 있습니다.

사용자 문서가 없는 상태에서 호출되면 실패합니다. 서비스 레이어에서 에러 처리를 추가하거나, 문서 생성과 업데이트를 동시에 처리하는 setDocmerge 옵션 사용을 고려해보세요.

 export async function completeUserOnboarding(
 	uid: string,
 	category: CategoryType
 ): Promise<void> {
 	const userRef = doc(db, "users", uid);
-	await updateDoc(userRef, {
+	await setDoc(userRef, {
 		onboardingCompleted: true,
 		selectedCategory: category,
-	});
+	}, { merge: true });
 }
src/app/dashboard/components/ScrapSection.tsx (1)

29-30: 카테고리 라벨 배열이 매 렌더링마다 재생성됩니다.

성능에 큰 영향은 없지만, 정적 데이터이므로 컴포넌트 외부로 이동하거나 useMemo를 사용할 수 있습니다.

+// 컴포넌트 외부에서 한 번만 생성
+const CATEGORY_LABELS = CATEGORY_KEYS.map((key) => CATEGORIES[key].label);
+
 export default function ScrapSection({
 	totalCount,
 	selectedCategory,
 	onCategorySelect,
 	cards,
 	isLoading = false,
 }: ScrapSectionProps) {
-	// 한글 라벨 목록 생성
-	const categories = CATEGORY_KEYS.map((key) => CATEGORIES[key].label);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0e16ee and 79224a4.

📒 Files selected for processing (79)
  • src/app/chatbot/components/ChatBot.tsx (1 hunks)
  • src/app/dashboard/components/CategoryEditModal.tsx (3 hunks)
  • src/app/dashboard/components/CategoryTag.tsx (2 hunks)
  • src/app/dashboard/components/DashboardClient.tsx (4 hunks)
  • src/app/dashboard/components/ProfileCard.tsx (3 hunks)
  • src/app/dashboard/components/ScrapCard.tsx (2 hunks)
  • src/app/dashboard/components/ScrapSection.tsx (4 hunks)
  • src/app/dashboard/components/TodayTermCard.tsx (4 hunks)
  • src/app/dashboard/utils/order.ts (1 hunks)
  • src/app/login/auth/LoginBody.tsx (1 hunks)
  • src/app/onboarding/components/CategoryButton.tsx (2 hunks)
  • src/app/onboarding/components/categoryList.tsx (3 hunks)
  • src/app/onboarding/page.tsx (1 hunks)
  • src/app/quiz/page.tsx (1 hunks)
  • src/app/terms/[slug]/page.tsx (2 hunks)
  • src/components/TagList.tsx (1 hunks)
  • src/components/icons/ic_bang.tsx (0 hunks)
  • src/components/icons/ic_chevron_up.tsx (0 hunks)
  • src/components/icons/ic_chevrons_up.tsx (0 hunks)
  • src/components/icons/ic_copy.tsx (0 hunks)
  • src/components/icons/ic_external_link.tsx (0 hunks)
  • src/components/icons/ic_info2.tsx (0 hunks)
  • src/components/icons/ic_time.tsx (0 hunks)
  • src/components/icons/index.ts (0 hunks)
  • src/components/layout/Header.tsx (2 hunks)
  • src/components/layout/header-parts/LoginButton.tsx (1 hunks)
  • src/components/layout/header-parts/Logo.tsx (1 hunks)
  • src/components/layout/header-parts/NavItem.tsx (1 hunks)
  • src/components/layout/header-parts/ProfileDropdown.tsx (1 hunks)
  • src/components/layout/header-parts/index.ts (1 hunks)
  • src/components/providers/Providers.tsx (1 hunks)
  • src/components/quiz/CategorySelection.tsx (2 hunks)
  • src/components/quiz/QuizResult.tsx (5 hunks)
  • src/components/quiz/QuizScoreCard.tsx (1 hunks)
  • src/components/quiz/QuizSession.tsx (7 hunks)
  • src/components/quiz/WrongAnswerCard.tsx (1 hunks)
  • src/components/search/RecommendedTermsSection.tsx (1 hunks)
  • src/components/search/SearchBar.tsx (2 hunks)
  • src/components/search/SearchResultCard.tsx (5 hunks)
  • src/components/term-detail/HeroSection.tsx (2 hunks)
  • src/components/term-detail/tabs/DescriptionTab.tsx (1 hunks)
  • src/components/term-detail/tabs/RelatedTab.tsx (3 hunks)
  • src/components/term-detail/tabs/UseCaseTab.tsx (2 hunks)
  • src/components/term-detail/types.ts (1 hunks)
  • src/components/ui/buttons/ScrapButton.tsx (1 hunks)
  • src/components/ui/category/CategoryChip.tsx (2 hunks)
  • src/components/ui/category/CategorySquareBadge.tsx (2 hunks)
  • src/components/ui/category/CategoryTag.tsx (0 hunks)
  • src/components/ui/category/config.ts (0 hunks)
  • src/components/ui/category/index.ts (0 hunks)
  • src/config/categories.ts (1 hunks)
  • src/config/roles.ts (1 hunks)
  • src/constants/theme.ts (1 hunks)
  • src/contexts/AuthContext.tsx (0 hunks)
  • src/contexts/ToastContext.tsx (2 hunks)
  • src/contexts/auth/AuthContext.tsx (1 hunks)
  • src/contexts/auth/ScrapContext.tsx (1 hunks)
  • src/contexts/auth/UserDataContext.tsx (1 hunks)
  • src/contexts/auth/index.tsx (1 hunks)
  • src/hooks/useChatBot.ts (1 hunks)
  • src/hooks/useDropdown.ts (1 hunks)
  • src/hooks/useQuizState.ts (1 hunks)
  • src/hooks/useScrapToggle.ts (1 hunks)
  • src/hooks/useShare.ts (1 hunks)
  • src/lib/bookmarks.ts (0 hunks)
  • src/lib/category.ts (0 hunks)
  • src/lib/quiz.ts (2 hunks)
  • src/lib/recommendations.ts (3 hunks)
  • src/lib/scrap.ts (2 hunks)
  • src/lib/sortTerms.ts (1 hunks)
  • src/lib/terms.server.ts (0 hunks)
  • src/lib/terms.ts (1 hunks)
  • src/lib/userService.ts (1 hunks)
  • src/types/category.ts (0 hunks)
  • src/types/quiz.ts (1 hunks)
  • src/types/scrapCard.ts (1 hunks)
  • src/types/terms.ts (1 hunks)
  • src/utils/date.ts (1 hunks)
  • src/utils/sorting.ts (1 hunks)
💤 Files with no reviewable changes (16)
  • src/components/icons/ic_chevron_up.tsx
  • src/components/icons/ic_external_link.tsx
  • src/lib/terms.server.ts
  • src/components/icons/ic_copy.tsx
  • src/lib/bookmarks.ts
  • src/components/icons/ic_info2.tsx
  • src/components/ui/category/index.ts
  • src/components/icons/ic_chevrons_up.tsx
  • src/lib/category.ts
  • src/components/icons/ic_time.tsx
  • src/components/icons/ic_bang.tsx
  • src/contexts/AuthContext.tsx
  • src/types/category.ts
  • src/components/ui/category/CategoryTag.tsx
  • src/components/ui/category/config.ts
  • src/components/icons/index.ts
🧰 Additional context used
🧬 Code graph analysis (48)
src/components/quiz/CategorySelection.tsx (2)
src/config/categories.ts (1)
  • CATEGORY_ROWS (140-143)
src/app/onboarding/components/CategoryButton.tsx (1)
  • CategoryButton (13-34)
src/components/layout/header-parts/LoginButton.tsx (2)
src/components/layout/header-parts/index.ts (1)
  • LoginButton (3-3)
src/components/ui/GlassButton.tsx (1)
  • GlassButton (9-29)
src/components/ui/category/CategorySquareBadge.tsx (1)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/contexts/ToastContext.tsx (1)
src/constants/theme.ts (1)
  • BRAND_GRADIENT (5-8)
src/components/quiz/QuizScoreCard.tsx (1)
src/components/quiz/QuizResult.tsx (1)
  • QuizResult (23-129)
src/components/term-detail/HeroSection.tsx (1)
src/components/ui/buttons/ScrapButton.tsx (1)
  • ScrapButton (26-52)
src/components/layout/header-parts/NavItem.tsx (1)
src/components/layout/header-parts/index.ts (1)
  • NavItem (2-2)
src/utils/sorting.ts (2)
src/app/dashboard/utils/order.ts (1)
  • SortType (4-4)
src/lib/sortTerms.ts (1)
  • SortType (4-4)
src/components/providers/Providers.tsx (1)
src/contexts/auth/index.tsx (1)
  • CombinedAuthProvider (24-32)
src/types/quiz.ts (1)
src/lib/quiz.ts (2)
  • QuizQuestion (8-8)
  • QuizResult (8-8)
src/app/chatbot/components/ChatBot.tsx (1)
src/hooks/useChatBot.ts (1)
  • useChatBot (28-80)
src/components/layout/header-parts/ProfileDropdown.tsx (4)
src/components/layout/header-parts/index.ts (1)
  • ProfileDropdown (4-4)
src/hooks/useDropdown.ts (1)
  • useDropdown (12-36)
src/components/ui/GlassButton.tsx (1)
  • GlassButton (9-29)
src/components/icons/ic_user.tsx (1)
  • UserIcon (3-25)
src/components/ui/buttons/ScrapButton.tsx (2)
src/utils/cn.ts (1)
  • cn (4-6)
src/components/icons/ic_scrap.tsx (1)
  • ScrapIcon (3-33)
src/config/roles.ts (3)
src/components/icons/ic_pm.tsx (1)
  • PmIcon (3-25)
src/components/icons/ic_edit.tsx (1)
  • EditIcon (3-28)
src/components/icons/ic_comment.tsx (1)
  • CommentIcon (3-25)
src/lib/userService.ts (3)
src/contexts/auth/UserDataContext.tsx (1)
  • UserData (26-26)
src/config/categories.ts (1)
  • CategoryType (26-36)
src/utils/firebase.ts (1)
  • db (23-23)
src/contexts/auth/ScrapContext.tsx (3)
src/contexts/auth/AuthContext.tsx (1)
  • useAuthCore (104-110)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (140-146)
src/utils/firebase.ts (1)
  • db (23-23)
src/components/search/RecommendedTermsSection.tsx (1)
src/contexts/auth/index.tsx (1)
  • useUserData (35-35)
src/app/dashboard/utils/order.ts (2)
src/types/scrapCard.ts (1)
  • ScrapCardData (4-12)
src/utils/sorting.ts (3)
  • SortType (63-63)
  • sortByDateDesc (49-58)
  • sortByKorean (28-44)
src/app/login/auth/LoginBody.tsx (1)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/config/categories.ts (10)
src/components/icons/ic_category_all.tsx (1)
  • CategoryAllIcon (3-25)
src/components/icons/ic_category_frontend.tsx (1)
  • CategoryFrontendIcon (3-25)
src/components/icons/ic_category_backend.tsx (1)
  • CategoryBackendIcon (3-25)
src/components/icons/ic_category_uiux.tsx (1)
  • CategoryUiuxIcon (3-25)
src/components/icons/ic_category_ai.tsx (1)
  • CategoryAiIcon (3-25)
src/components/icons/ic_category_cloud.tsx (1)
  • CategoryCloudIcon (3-25)
src/components/icons/ic_category_data.tsx (1)
  • CategoryDataIcon (3-25)
src/components/icons/ic_category_security.tsx (1)
  • CategorySecurityIcon (3-25)
src/components/icons/ic_category_devops.tsx (1)
  • CategoryDevopsIcon (3-25)
src/components/icons/ic_category_business.tsx (1)
  • CategoryBusinessIcon (3-25)
src/app/dashboard/components/ScrapCard.tsx (4)
src/types/scrapCard.ts (1)
  • ScrapCardData (4-12)
src/config/categories.ts (1)
  • getCategoryType (189-191)
src/components/ui/category/CategoryChip.tsx (1)
  • CategoryChip (10-29)
src/components/ui/category/index.ts (1)
  • CategoryChip (2-2)
src/components/term-detail/tabs/UseCaseTab.tsx (1)
src/config/roles.ts (1)
  • ROLE_CONFIG (15-46)
src/components/quiz/QuizSession.tsx (2)
src/hooks/useQuizState.ts (1)
  • useQuizState (29-94)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/app/dashboard/components/CategoryTag.tsx (1)
src/config/categories.ts (2)
  • getCategoryType (189-191)
  • CATEGORIES (59-130)
src/app/onboarding/page.tsx (2)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (140-146)
src/app/dashboard/components/TodayTermCard.tsx (1)
src/constants/theme.ts (1)
  • BRAND_GRADIENT (5-8)
src/app/terms/[slug]/page.tsx (2)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useScrap (35-35)
src/hooks/useShare.ts (1)
  • useShare (11-62)
src/contexts/auth/UserDataContext.tsx (4)
src/lib/userService.ts (6)
  • UserData (10-18)
  • fetchUserData (23-32)
  • createUserData (37-55)
  • mergeScrapList (60-71)
  • completeUserOnboarding (76-85)
  • updateUserCategory (90-96)
src/config/categories.ts (1)
  • CategoryType (26-36)
src/contexts/auth/AuthContext.tsx (1)
  • useAuthCore (104-110)
src/lib/bookmarks.ts (2)
  • getBookmarks (10-19)
  • clearBookmarks (49-51)
src/components/term-detail/tabs/RelatedTab.tsx (3)
src/components/ui/category/CategoryChip.tsx (1)
  • CategoryChip (10-29)
src/components/ui/category/index.ts (1)
  • CategoryChip (2-2)
src/components/ui/buttons/ScrapButton.tsx (1)
  • ScrapButton (26-52)
src/contexts/auth/AuthContext.tsx (2)
src/contexts/auth/index.tsx (2)
  • AuthProvider (38-38)
  • useAuthCore (35-35)
src/utils/firebase.ts (1)
  • auth (22-22)
src/components/ui/category/CategoryChip.tsx (1)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/app/dashboard/components/DashboardClient.tsx (3)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (140-146)
src/types/scrapCard.ts (1)
  • ScrapCardData (4-12)
src/hooks/useChatBot.ts (1)
src/app/chatbot/utils/actions.ts (1)
  • getChatResponse (7-73)
src/lib/recommendations.ts (1)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/components/layout/header-parts/Logo.tsx (2)
src/components/layout/header-parts/index.ts (1)
  • Logo (1-1)
src/components/icons/logo_text.tsx (1)
  • LogoText (3-34)
src/app/dashboard/components/ProfileCard.tsx (3)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (140-146)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/lib/sortTerms.ts (2)
src/lib/terms.ts (1)
  • TermIndexItem (6-6)
src/utils/sorting.ts (2)
  • SortType (63-63)
  • sortByKorean (28-44)
src/components/term-detail/types.ts (1)
src/config/categories.ts (2)
  • getCategoryType (189-191)
  • CATEGORIES (59-130)
src/components/search/SearchBar.tsx (1)
src/constants/theme.ts (1)
  • BRAND_GRADIENT (5-8)
src/components/search/SearchResultCard.tsx (3)
src/hooks/useShare.ts (1)
  • useShare (11-62)
src/components/ui/buttons/ScrapButton.tsx (1)
  • ScrapButton (26-52)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/hooks/useQuizState.ts (1)
src/lib/quiz.ts (1)
  • calculateQuizResult (96-119)
src/types/terms.ts (1)
src/lib/terms.ts (5)
  • TermIndexItem (6-6)
  • Role (8-8)
  • UseCase (9-9)
  • Conversation (10-10)
  • TermDetail (7-7)
src/app/dashboard/components/CategoryEditModal.tsx (1)
src/config/categories.ts (2)
  • CATEGORY_KEYS (135-135)
  • CATEGORIES (59-130)
src/app/onboarding/components/CategoryButton.tsx (4)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/utils/cn.ts (1)
  • cn (4-6)
src/components/ui/category/CategorySquareBadge.tsx (1)
  • CategorySquareBadge (9-27)
src/components/ui/category/index.ts (1)
  • CategorySquareBadge (3-3)
src/app/dashboard/components/ScrapSection.tsx (2)
src/config/categories.ts (2)
  • CATEGORY_KEYS (135-135)
  • CATEGORIES (59-130)
src/constants/theme.ts (1)
  • BRAND_GRADIENT (5-8)
src/components/quiz/QuizResult.tsx (3)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useScrap (35-35)
src/components/quiz/QuizScoreCard.tsx (1)
  • QuizScoreCard (9-42)
src/components/quiz/WrongAnswerCard.tsx (1)
  • WrongAnswerCard (11-54)
src/lib/quiz.ts (1)
src/config/categories.ts (1)
  • CATEGORIES (59-130)
src/lib/scrap.ts (1)
src/utils/date.ts (1)
  • formatKoreanDate (5-11)
🪛 GitHub Actions: CI
src/hooks/useShare.ts

[warning] 25-25: 'error' is defined but never used. (no-unused-vars)

src/contexts/auth/UserDataContext.tsx

[warning] 55-55: React Hook useEffect has a missing dependency: 'loadUserData'. Either include it or remove the dependency array.

🪛 GitHub Check: next (format/lint/typecheck/build)
src/hooks/useShare.ts

[warning] 25-25:
'error' is defined but never used

src/contexts/auth/UserDataContext.tsx

[warning] 55-55:
React Hook useEffect has a missing dependency: 'loadUserData'. Either include it or remove the dependency array

🔇 Additional comments (65)
src/components/layout/header-parts/Logo.tsx (1)

1-10: 구현이 깔끔합니다!

로고 컴포넌트가 적절하게 구현되었습니다. Next.js Link로 루트 경로를 감싸고 LogoText 아이콘을 고정 크기로 렌더링하는 단순하고 명확한 구조입니다.

src/utils/date.ts (1)

5-11: 구현이 올바릅니다.

한국 날짜 포맷팅이 정확하게 구현되었습니다. 로컬 타임존을 사용하므로 한국 사용자 대상 서비스에서는 문제없이 동작할 것입니다.

참고: 서버/클라이언트 간 날짜 일관성이 중요한 경우 UTC 변환을 고려할 수 있지만, 현재 구현으로 충분해 보입니다.

src/hooks/useScrapToggle.ts (1)

2-11: 새로운 컨텍스트 구조로 잘 마이그레이션되었습니다.

useAuthCoreuseScrap으로의 분리가 관심사의 분리(separation of concerns) 원칙을 잘 따르고 있습니다. 인증과 스크랩 로직이 명확히 구분되어 유지보수가 용이해졌습니다.

src/app/dashboard/components/ProfileCard.tsx (2)

6-12: 중앙화된 설정과 컨텍스트로 잘 마이그레이션되었습니다.

CATEGORIES 설정을 사용하여 카테고리 정보를 중앙화하고, 인증 로직을 useAuthCoreuseUserData로 분리한 것이 코드 유지보수성을 크게 향상시켰습니다.


15-17: 적절한 폴백 로직입니다.

userDatauser 간의 폴백 체인이 잘 구현되어 있어, 데이터 로딩 중이거나 일부 정보가 누락된 경우에도 안정적으로 동작합니다.

src/utils/sorting.ts (3)

15-23: 문자 타입 우선순위가 잘 구현되었습니다.

숫자 → 특수문자 → 한글 → 영문 순서의 우선순위가 명확하게 구현되었습니다. 빈 문자열이 영문과 동일한 우선순위(4)를 받는 것은 의도된 동작으로 보입니다.


28-44: 한글 정렬이 올바르게 구현되었습니다.

localeCompareko 로케일과 sensitivity: "base" 옵션을 사용하여 한글 자모 순서를 정확히 반영한 정렬이 구현되었습니다. 문자 타입 우선순위를 먼저 비교한 후 같은 타입 내에서 정렬하는 2단계 접근 방식이 적절합니다.


49-58: 날짜 정렬이 정확합니다.

Date.getTime()을 사용한 밀리초 단위 비교로 정확한 내림차순 정렬이 구현되었습니다. 원본 배열을 변경하지 않도록 스프레드 연산자로 복사한 점도 좋습니다.

src/lib/sortTerms.ts (1)

2-17: 중앙화된 정렬 유틸리티로 잘 리팩토링되었습니다.

정렬 로직을 @/utils/sorting으로 추출하여 코드 중복을 제거하고 재사용성을 높였습니다. sortByKorean을 사용하여 한글 자모 순서를 올바르게 처리하고 있으며, SortType을 재내보내어 공개 API를 유지한 점이 좋습니다.

src/contexts/auth/ScrapContext.tsx (2)

50-55: Firestore 업데이트가 적절합니다.

updateDoc를 사용하여 사용자 문서의 scrapList 필드만 업데이트하는 것이 효율적입니다. 전체 문서를 다시 쓰지 않아 성능과 데이터 일관성 측면에서 좋습니다.


61-64: 에러 처리가 적절합니다.

에러 로깅과 함께 이전 상태를 반환하여 사용자에게 실패를 명확히 알리고 있습니다. currentlyScraped를 반환하여 UI가 실패 전 상태를 유지할 수 있도록 한 점이 좋습니다.

src/types/scrapCard.ts (1)

4-12: The code already handles the optional slug field appropriately. In ScrapCard.tsx (line 19), there is a conditional check if (card.slug) before navigation that prevents errors when slug is undefined. When slug is absent, the card click simply does not navigate rather than throwing an error. Additionally, the termToScrapCard function always assigns the slug field from the source data, so it's only optional in the type definition for flexibility, not due to missing implementation.

Likely an incorrect or invalid review comment.

src/lib/terms.ts (3)

5-13: 타입 재내보내기 패턴 LGTM!

중앙화된 @/types/terms에서 타입을 재내보내기하여 API 표면을 일관되게 유지하는 좋은 리팩토링입니다. 기존 코드에서 @/lib/terms를 통해 타입을 가져오던 부분도 그대로 동작합니다.


21-43: 개발 환경 검증 로직 LGTM!

중복 ID/slug 검증을 통해 데이터 무결성을 개발 시점에 확인할 수 있어 유용합니다. Map을 사용한 O(1) 조회와 명확한 에러 메시지가 좋습니다.


48-63: 캐시 무효화 전략 검토 필요

현재 indexCache가 한 번 설정되면 세션 동안 갱신되지 않습니다. SPA에서는 대부분 문제 없지만, 용어 데이터가 동적으로 업데이트되는 경우 캐시 무효화 메커니즘이 필요할 수 있습니다.

src/types/terms.ts (2)

1-16: 타입 중앙화 LGTM!

타입 정의를 별도 파일로 분리하여 관리하기 좋습니다. TermIndexItem 인터페이스가 명확하게 정의되어 있습니다.


33-55: TermDetail 인터페이스 LGTM!

상세 용어 타입이 잘 정의되어 있습니다. 선택적 필드들이 적절하게 표시되어 있고, 구조가 명확합니다.

src/components/term-detail/tabs/DescriptionTab.tsx (1)

4-4: import 경로 변경 LGTM!

getCategoryType의 import 경로를 @/config/categories로 변경하여 중앙화된 설정 패턴에 맞게 정리했습니다.

src/components/search/SearchBar.tsx (1)

6-6: 브랜드 그라디언트 중앙화 LGTM!

BRAND_GRADIENT를 사용하여 테마를 중앙에서 관리하는 좋은 리팩토링입니다.

src/contexts/ToastContext.tsx (2)

11-11: 브랜드 그라디언트 import LGTM!

중앙화된 테마 상수를 사용하여 일관된 스타일링을 유지합니다.


77-82: 로그인 버튼 스타일 적용 LGTM!

BRAND_GRADIENT.bg를 사용하여 브랜드 일관성을 유지하는 좋은 리팩토링입니다.

src/app/onboarding/components/categoryList.tsx (1)

11-15: LGTM! 온보딩 카테고리 구조가 명확합니다.

튜플 타입 선언으로 2줄 구조를 명시적으로 표현했고, 중앙화된 CategoryType을 사용하여 타입 안정성을 확보했습니다.

src/components/ui/category/CategorySquareBadge.tsx (1)

2-2: LGTM! 중앙화된 카테고리 설정으로 성공적으로 마이그레이션되었습니다.

CATEGORIES 상수를 사용하여 카테고리 설정을 중앙에서 관리하도록 변경되었습니다. 타입 안정성과 로직은 그대로 유지됩니다.

Also applies to: 13-13

src/components/ui/category/CategoryChip.tsx (1)

2-2: LGTM! 카테고리 설정 중앙화가 일관되게 적용되었습니다.

CategorySquareBadge와 동일한 패턴으로 CATEGORIES를 사용하도록 마이그레이션되어 일관성이 유지됩니다.

Also applies to: 15-15

src/constants/theme.ts (1)

1-8: LGTM! 테마 상수 중앙화가 잘 구현되었습니다.

브랜드 그라데이션을 중앙에서 관리하여 일관성을 확보했고, as const 단언으로 타입 안정성도 강화했습니다. 여러 컴포넌트에서 하드코딩된 그라데이션을 제거하는 데 기여합니다.

src/app/quiz/page.tsx (1)

8-8: LGTM! 임포트 경로가 중앙화된 설정으로 업데이트되었습니다.

CategoryType이 새로운 중앙 설정 모듈에서 임포트되며, 사용 방식은 변경되지 않았습니다.

src/components/search/RecommendedTermsSection.tsx (1)

7-7: LGTM! 분리된 인증 컨텍스트 훅으로 마이그레이션되었습니다.

useAuth()에서 useUserData()로 변경하여 관심사가 명확히 분리되었습니다. userData 객체의 구조와 사용 방식은 동일하게 유지됩니다.

Also applies to: 14-14

src/components/quiz/QuizScoreCard.tsx (1)

9-42: 새로운 QuizScoreCard 컴포넌트가 깔끔하게 구현되었습니다.

퀴즈 결과를 시각적으로 표현하는 프레젠테이션 컴포넌트로, 타입 안정성과 색상 구분이 잘 적용되었습니다. text-primary-400src/styles/globals.css--color-primary-400: #ba9be5로 정의되어 있으므로 Tailwind에서 올바르게 적용됩니다.

src/components/providers/Providers.tsx (1)

4-4: 認証コンテキスト分離が完全に実装され、すべてのコンシューマーが正しく移行済みです。

CombinedAuthProviderAuthProvider > UserDataProvider > ScrapProvider の順序で正しくネストされており、Providers.tsx での使用法も適切です。codebase全体で useAuthCore(), useUserData(), useScrap() フックが正しく使用されており、レガシーな useAuth() への参照は存在しません。

src/app/dashboard/components/TodayTermCard.tsx (1)

7-7: BRAND_GRADIENT 재사용으로 스타일 일관성이 좋아졌습니다

  • 캘린더 아이콘, 버튼, 요약 박스에서 공통으로 BRAND_GRADIENT.bg를 쓰도록 바꾼 덕에 브랜딩 컬러를 한 곳에서 관리할 수 있게 된 점이 좋습니다.
  • bg-black/60와 gradient를 함께 쓰는 구성도 시각적으로 자연스러울 것 같고, 나머지 타이포/톤 조정도 카드 전체 톤앤매너에 잘 맞아 보입니다.

Also applies to: 29-31, 45-45, 67-76

src/lib/recommendations.ts (1)

6-7: 카테고리 라벨을 CATEGORIES 기반으로 통일한 변경이 적절합니다

  • CATEGORIES[targetCategory].label을 한 번 계산해서, 용어 개수가 부족한 경우/셔플 후 선택하는 경우 모두에서 재사용하는 구조가 중복을 잘 줄여줍니다.
  • 추천 영역에서도 이제 카테고리 라벨이 다른 곳과 동일한 소스(config)에서 나오므로, 향후 라벨 변경 시 싱크가 어긋날 여지도 줄어든 것 같습니다.

Also applies to: 60-60, 66-66, 81-81

src/components/term-detail/tabs/RelatedTab.tsx (1)

11-13: CategoryChip/ScrapButton으로 책임 분리된 구조가 좋습니다

  • getCategoryType(term.primaryTag)로 얻은 카테고리를 그대로 CategoryChip에 넘겨서, 관련 탭에서도 카테고리 아이콘/색상이 전역 설정과 일관되게 맞춰진 점이 좋습니다.
  • 스크랩 버튼을 ScrapButton으로 교체하고, handleBookmark에서 e.stopPropagation()으로 카드 클릭 내비게이션과 이벤트를 분리한 것도 UX 측면에서 안전한 구현입니다.
  • bg-category-${category}/50 같은 태그 스타일도 기존 패턴을 유지하고 있어, 테마/카테고리 시스템 전체와 잘 맞는 것 같습니다.

Also applies to: 62-62, 71-71

src/components/layout/header-parts/index.ts (1)

1-4: 헤더 파츠 re-export로 헤더 관련 import 경로가 명확해졌습니다

Logo, NavItem, LoginButton, ProfileDropdown을 한 파일에서 re-export 하는 구조라, 상위 레이아웃/헤더에서 header-parts만 바라보면 되는 점이 깔끔합니다. 네이밍과 구성도 직관적이라 현재 형태로 충분해 보입니다.

src/components/search/SearchResultCard.tsx (1)

5-11: 검색 결과 카드의 공유/스크랩/카테고리 리팩토링 구조가 잘 정리되어 있습니다

  • useShareshareTerm을 사용해 slug 기반 공유를 한 곳에서 처리하는 구조가 명확하고, 클릭 이벤트에서 stopPropagation으로 카드 네비게이션과 충돌도 막고 있어 좋습니다.
  • 스크랩 버튼을 ScrapButton 컴포넌트로 공통화해 상단 히어로/검색 카드 모두 동일한 UI/동작을 쓰게 된 점도 재사용성과 일관성 측면에서 적절합니다.
  • 태그 첫 번째 요소에만 CATEGORIES[category].selectedColor를 적용하고 나머지는 공통 색으로 처리하는 패턴도 중앙 설정(getCategoryType + CATEGORIES)과 잘 맞아 떨어집니다.

Also applies to: 19-20, 27-30, 51-52, 79-83

src/contexts/auth/index.tsx (1)

1-40: Auth 관련 Provider/Hooks를 한 곳에서 모아 쓰기 좋게 잘 정리되었습니다

  • CombinedAuthProviderAuthProvider → UserDataProvider → ScrapProvider 순서로 감싸는 구조라 각 컨텍스트 의존 관계를 이해하기 쉽습니다.
  • 훅(useAuthCore, useUserData, useScrap)과 개별 Provider를 같이 재-export 해서, 사용하는 쪽에서 import 경로를 단일화할 수 있는 점도 좋습니다.
  • 한 가지 확인만: 기존에 루트에서 개별 Provider를 직접 중첩해서 사용하던 곳이 있다면, 이제는 CombinedAuthProvider 하나로 교체되었는지 한 번 더 점검해 보시면 좋겠습니다.
src/types/quiz.ts (1)

1-21: 퀴즈 관련 타입 정의가 명확하고 사용처와도 잘 맞습니다

  • QuizQuestion 이 용어(TermIndexItem), 정답, 보기, 문제 타입을 모두 포함해 퀴즈 생성/렌더링에 필요한 정보를 잘 캡슐화하고 있습니다.
  • QuizResult 에 총 문항/정답/오답/점수/질문 목록/사용자 답안을 모두 담아 후처리(리포트, 리뷰 화면 등) 에 활용하기 좋게 설계된 것 같습니다.
  • 현재 요구사항 기준으로는 추가 수정 없이 그대로 사용해도 무방해 보입니다.
src/app/onboarding/components/CategoryButton.tsx (1)

4-4: 중앙화된 카테고리 설정으로의 마이그레이션이 깔끔합니다.

CATEGORIES 설정을 사용하여 색상과 레이블을 관리하는 방식이 일관성 있고 유지보수하기 좋습니다. 타입 안정성도 CategoryType을 통해 잘 유지되고 있습니다.

Also applies to: 18-18, 26-27, 31-31

src/components/TagList.tsx (2)

14-45: 재사용 가능한 TagItem 컴포넌트 추출이 우수합니다.

설정 기반 접근 방식으로 리팩토링하여 코드 중복을 제거하고 유지보수성을 향상시켰습니다. 아이콘, 레이블, 색상이 모두 중앙화된 설정에서 관리되고 있습니다.


50-64: 동적 다중 행 렌더링이 잘 구현되었습니다.

CATEGORY_ROWS를 활용한 렌더링 로직이 명확하고, 활성 상태 비교 및 클릭 핸들러가 올바르게 작동합니다.

src/app/dashboard/components/CategoryEditModal.tsx (1)

6-8: 중앙화된 카테고리 설정으로의 마이그레이션이 완료되었습니다.

CATEGORY_KEYSCATEGORIES를 사용하여 이전의 하드코딩된 설정을 대체했습니다. 타입 안정성과 코드 일관성이 유지되고 있습니다.

Also applies to: 63-86

src/components/quiz/WrongAnswerCard.tsx (1)

11-54: 오답 카드 컴포넌트가 잘 구현되었습니다.

컴포넌트가 명확하고 집중적인 책임을 가지고 있으며, 용어 상세 페이지로의 네비게이션과 조건부 설명 렌더링이 적절합니다. 사용자 미응답 처리도 좋은 UX를 제공합니다.

src/components/layout/header-parts/ProfileDropdown.tsx (1)

15-77: 프로필 드롭다운 컴포넌트가 잘 구조화되어 있습니다.

useDropdown 훅을 활용한 상태 관리가 깔끔하고, 액션 전에 드롭다운을 닫는 패턴이 올바르게 구현되었습니다. 접근성 레이블도 제공되고 있습니다.

src/app/dashboard/components/ScrapCard.tsx (1)

6-8: 카테고리 표시 로직이 CategoryChip 컴포넌트로 잘 캡슐화되었습니다.

getCategoryType을 사용한 매핑과 CategoryChip 컴포넌트 사용으로 카테고리 표시 로직이 일관성 있게 개선되었습니다. 타입 전용 import 사용도 적절합니다.

Also applies to: 16-16, 31-31

src/app/dashboard/utils/order.ts (1)

1-18: 정렬 로직을 유틸리티 함수로 위임하여 코드 중복을 제거했습니다.

sortByKoreansortByDateDesc 유틸리티를 활용하여 정렬 로직을 중앙화했습니다. 한글 정렬과 날짜 정렬이 올바르게 구현되어 있으며, 타입 안정성도 유지되고 있습니다.

src/hooks/useChatBot.ts (1)

28-80: 챗봇 훅이 깔끔하게 구현되었습니다.

메시지 관리, 자동 스크롤, 로딩 상태 처리가 모두 적절하게 구현되어 있습니다. 추천 질문 클릭 처리와 중복 제출 방지 로직도 잘 작동합니다.

src/contexts/auth/AuthContext.tsx (2)

34-46: LGTM! 인증 상태 구독이 잘 구현되었습니다.

Firebase onAuthStateChanged 리스너 설정 및 cleanup 함수 반환이 올바르게 구현되어 있습니다. 컴포넌트 언마운트 시 메모리 누수를 방지합니다.


59-78: 데모 계정 정보가 클라이언트에 노출됩니다.

NEXT_PUBLIC_ 접두사가 붙은 환경 변수는 클라이언트 번들에 포함됩니다. 데모 계정의 이메일과 비밀번호가 브라우저에서 접근 가능하므로, 데모 계정에 민감한 데이터나 권한이 없는지 확인이 필요합니다.

src/components/quiz/QuizResult.tsx (2)

4-14: LGTM! 임포트 구조가 잘 정리되었습니다.

새로운 인증 훅(useAuthCore, useScrap)과 분리된 UI 컴포넌트(QuizScoreCard, WrongAnswerCard)로 깔끔하게 리팩토링되었습니다.


98-106: indexOf를 사용한 원본 인덱스 조회

동일한 질문 객체가 배열에 중복될 가능성이 낮다면 현재 구현도 문제없습니다. 다만, 필터링 시 인덱스를 함께 보존하면 더 명확합니다.

src/components/quiz/QuizSession.tsx (3)

20-31: LGTM! useQuizState 훅을 활용한 깔끔한 상태 관리

로컬 상태 관리 로직이 커스텀 훅으로 잘 분리되었습니다. 컴포넌트는 이제 순수하게 UI 렌더링에 집중하며, 상태 관리는 훅에 위임되어 관심사 분리가 잘 이루어졌습니다.


39-40: LGTM! 중앙화된 카테고리 설정 사용

CATEGORIES[category].label을 통해 중앙화된 설정에서 라벨을 가져오는 것이 유지보수성을 높입니다.


142-158: LGTM! 네비게이션 로직이 훅의 상태에 잘 연결됨

isFirstQuestion, isLastQuestion, goNext, goPrevious를 훅에서 제공받아 사용하여 코드가 간결해졌습니다.

src/contexts/auth/UserDataContext.tsx (1)

62-82: LGTM! 로컬 북마크 마이그레이션 로직

신규 사용자와 기존 사용자 모두에 대해 로컬 북마크를 서버로 병합하고 로컬 스토리지를 정리하는 로직이 잘 구현되었습니다.

src/hooks/useQuizState.ts (4)

29-43: LGTM! 퀴즈 상태 훅이 잘 구현되었습니다.

상태 초기화와 파생 값 계산이 명확하게 분리되어 있습니다. currentQuestion, progress, answeredCount 등의 파생 값이 매 렌더링마다 계산되지만, 이는 간단한 연산이므로 성능에 큰 영향이 없습니다.


49-68: LGTM! goNext 로직이 정확합니다.

현재 답변 저장, 다음 문제로 이동, 마지막 문제에서 결과 계산 및 onComplete 호출까지 모든 흐름이 올바르게 구현되어 있습니다. useCallback 의존성 배열도 완전합니다.


70-79: LGTM! goPrevious 로직이 정확합니다.

이전 문제로 이동 시 현재 답변을 저장하고 이전 답변을 복원하는 로직이 올바릅니다.


33-36: No actionable issue - questions prop is stable during QuizSession lifecycle

The concern about questions changing at runtime and causing userAnswers sync issues is not applicable to the current implementation. The quiz flow uses stage-based conditional rendering (category → quiz → result) where QuizSession unmounts before questions change. On retry, new questions are generated while QuizSession is unmounted, then the component remounts with the new questions array. The questions prop remains immutable during each active QuizSession lifecycle, so no synchronization mechanism is needed.

src/lib/quiz.ts (2)

6-9: LGTM! 타입 중앙화 및 재내보내기 패턴이 적절합니다.

CATEGORIES를 중앙화된 설정에서 가져오고, QuizQuestion/QuizResult 타입을 @/types/quiz에서 재내보내기하여 공개 API를 유지하는 구조가 깔끔합니다.


38-38: 카테고리 라벨 조회 방식 개선 확인.

CATEGORIES[category].label을 통해 중앙화된 설정에서 라벨을 가져오는 방식이 일관성 있게 적용되었습니다.

src/components/layout/Header.tsx (2)

5-6: LGTM! 컴포넌트 분리 및 인증 컨텍스트 변경이 적절합니다.

useAuthCore로 변경하여 관심사를 분리하고, UI 컴포넌트들을 header-parts로 추출한 구조가 모듈화 원칙에 부합합니다.


10-10: readonly 타입 적용으로 불변성 보장.

navItems props에 readonly 수식자를 사용하여 배열 변경을 방지하는 것이 좋습니다.

src/config/categories.ts (1)

1-9: LGTM! 중복 제거를 위한 중앙화된 설정 파일.

여러 파일에 분산되어 있던 카테고리 설정을 하나의 파일로 통합하여 유지보수성을 크게 향상시켰습니다.

src/app/dashboard/components/ScrapSection.tsx (2)

6-7: LGTM! 중앙화된 설정 사용.

CATEGORIESCATEGORY_KEYS를 사용하여 카테고리 목록을 생성하는 방식이 일관성 있게 적용되었습니다.


39-41: BRAND_GRADIENT.bg 적용으로 일관된 테마 스타일링.

상수를 통해 그라디언트 스타일을 관리하여 유지보수성이 향상되었습니다.

Also applies to: 94-94

src/lib/userService.ts (1)

60-71: No error handling required for missing documents—document existence is guaranteed at the call site.

mergeScrapList is only called in the else branch of loadUserData (UserDataContext.tsx:71), which executes only when fetchUserData returns non-null data. Since fetchUserData returns null when the document doesn't exist, any non-null return value guarantees the document exists in Firestore.

Copy link

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

♻️ Duplicate comments (1)
src/contexts/auth/UserDataContext.tsx (1)

99-118: 온보딩/카테고리 업데이트 시 에러 처리 보완이 필요합니다

completeOnboarding/updateCategory 모두 서버에 쓰기(completeUserOnboarding, updateUserCategory)를 수행한 뒤 로컬 상태를 갱신하는 구조인데, try/catch가 없어 실패 시 에러가 상위로 전파될 뿐 컨텍스트 내부에서는 아무 처리도 하지 않습니다. 호출부에서 항상 await + try/catch를 해주지 않으면 미처리된 Promise rejection이 날 수 있고, 사용자 입장에서도 실패 여부를 알 수 없는 상태가 됩니다.

서버와 로컬 상태 불일치를 줄이려면, 각 함수에서 최소한 다음 정도는 처리하는 것을 권장합니다.

 const completeOnboarding = async (category: CategoryType) => {
 	if (!user) return;

-	await completeUserOnboarding(user.uid, category);
-	setUserData((prev) =>
-		prev
-			? { ...prev, onboardingCompleted: true, selectedCategory: category }
-			: null
-	);
-	setIsNewUser(false);
+	try {
+		await completeUserOnboarding(user.uid, category);
+		setUserData((prev) =>
+			prev
+				? { ...prev, onboardingCompleted: true, selectedCategory: category }
+				: null
+		);
+		setIsNewUser(false);
+	} catch (error) {
+		console.error("온보딩 완료 실패:", error);
+		throw error; // 필요 시 호출부에서 토스트 등으로 처리
+	}
 };

 const updateCategory = async (category: CategoryType) => {
 	if (!user) return;

-	await updateUserCategory(user.uid, category);
-	setUserData((prev) =>
-		prev ? { ...prev, selectedCategory: category } : null
-	);
+	try {
+		await updateUserCategory(user.uid, category);
+		setUserData((prev) =>
+			prev ? { ...prev, selectedCategory: category } : null
+		);
+	} catch (error) {
+		console.error("카테고리 업데이트 실패:", error);
+		throw error;
+	}
 };

이렇게 하면 서버 쓰기가 성공했을 때만 로컬 상태가 바뀌고, 실패 시에는 명시적으로 에러를 상위로 전달할 수 있어 UI 레벨에서 일관된 에러 처리가 가능합니다.

🧹 Nitpick comments (3)
src/contexts/auth/UserDataContext.tsx (2)

41-83: loadUserData 구조 및 의존성 설정이 적절합니다

useCallback으로 loadUserData를 감싸고 useEffect 의존성 배열에 loadUserData를 포함한 구조가 린트 경고도 해소하면서, 사용자 변경 시 데이터 로딩 흐름을 깔끔하게 정리해 줍니다. 신규/기존 사용자 분기와 로컬 북마크 병합 로직도 한 함수 안에서 일관되게 처리되고 있어 유지보수 측면에서 좋습니다.

필요하다면, 아주 드물지만 사용자 전환 타이밍에 오래 걸린 이전 호출이 나중에 완료되면서 상태를 덮어쓰는 레이스를 막기 위해, loadUserData 내부에 let cancelled = false/if (cancelled) return; 같은 플래그를 두고 useEffect cleanup에서 cancelled = true로 설정하는 패턴을 고려해 볼 수는 있겠습니다만, 현재 요구사항 기준으로는 과도할 수 있어 선택사항 수준으로 보여집니다.


124-138: 컨텍스트 value를 useMemo로 감싸면 불필요한 리렌더를 줄일 수 있습니다

현재 UserDataContext.Provider에 전달하는 value 객체가 매 렌더마다 새로 생성되기 때문에, 실제 값 변화가 없더라도 컨텍스트 소비자 컴포넌트들이 리렌더될 수 있습니다. 규모가 커지면 이 부분이 미세한 성능 비용이 될 수 있어, 선택적으로 useMemo를 고려해 볼 수 있습니다.

예시:

-import {
-	createContext,
-	useCallback,
-	useContext,
-	useEffect,
-	useState,
-	type ReactNode,
-} from "react";
+import {
+	createContext,
+	useCallback,
+	useContext,
+	useEffect,
+	useMemo,
+	useState,
+	type ReactNode,
+} from "react";
@@
-	return (
-		<UserDataContext.Provider
-			value={{
-				userData,
-				userDataLoading,
-				isNewUser,
-				completeOnboarding,
-				updateCategory,
-				refreshUserData,
-				updateScrapList,
-			}}
-		>
+	const contextValue = useMemo(
+		() => ({
+			userData,
+			userDataLoading,
+			isNewUser,
+			completeOnboarding,
+			updateCategory,
+			refreshUserData,
+			updateScrapList,
+		}),
+		[
+			userData,
+			userDataLoading,
+			isNewUser,
+			completeOnboarding,
+			updateCategory,
+			refreshUserData,
+			updateScrapList,
+		]
+	);
+
+	return (
+		<UserDataContext.Provider value={contextValue}>
 			{children}
 		</UserDataContext.Provider>
 	);

필수 변경은 아니지만, 컨텍스트 소비자가 많을수록 점진적으로 도움이 될 수 있는 미세 최적화 포인트입니다.

src/hooks/useShare.ts (1)

29-31: 사용자 피드백 추가를 고려해보세요

모든 공유 시도가 실패했을 때 조용히 무시하고 있어서 사용자는 공유가 성공했는지 실패했는지 알 수 없습니다. 토스트 메시지나 alert 등으로 사용자에게 피드백을 제공하는 것이 좋겠습니다.

예시:

 		} catch {
 			// 사용자가 공유를 취소했거나 에러 발생 시 클립보드에 복사
 			try {
 				await navigator.clipboard.writeText(params.url);
+				// 성공 시 사용자에게 알림 (예: toast 메시지)
 			} catch {
-				// 클립보드 복사도 실패한 경우 조용히 무시
+				// 클립보드 복사도 실패한 경우 사용자에게 알림
+				console.error('공유 및 클립보드 복사에 실패했습니다.');
+				// TODO: 토스트 메시지 또는 alert로 사용자에게 알림
 			}
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79224a4 and c5e2ed7.

📒 Files selected for processing (2)
  • src/contexts/auth/UserDataContext.tsx (1 hunks)
  • src/hooks/useShare.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/contexts/auth/UserDataContext.tsx (4)
src/lib/userService.ts (6)
  • UserData (10-18)
  • fetchUserData (23-32)
  • createUserData (37-55)
  • mergeScrapList (60-71)
  • completeUserOnboarding (76-85)
  • updateUserCategory (90-96)
src/config/categories.ts (1)
  • CategoryType (26-36)
src/contexts/auth/AuthContext.tsx (1)
  • useAuthCore (104-110)
src/lib/bookmarks.ts (2)
  • getBookmarks (10-19)
  • clearBookmarks (49-51)
🔇 Additional comments (4)
src/contexts/auth/UserDataContext.tsx (3)

95-98: refreshUserData 추상화가 간결합니다

loadUserData만 래핑해서 노출하는 형태라 컨텍스트 소비자 입장에서는 await refreshUserData()로 의도를 명확히 표현할 수 있어 좋습니다. 이 함수는 user가 없으면 내부적으로 바로 return 되기 때문에, 실제 사용 시에는 로그인 이후 시점에서만 호출되도록 컨테이너/페이지 단에서 한 번 더 보장해 두면 좋겠습니다.


120-122: updateScrapList는 로컬 상태 역할이 명확합니다

스크랩 목록을 로컬 상태에만 반영하고, 서버 동기화는 별도 서비스/로직에서 담당하도록 분리되어 있는 구조로 보입니다. prev 가드까지 포함해서 NPE 방지 측면에서도 안전하게 잘 작성되어 있습니다.


141-147: useUserData 훅 방어 코드가 적절합니다

컨텍스트 미설정 시 명시적으로 에러를 던져 잘못된 사용 패턴을 초기에 발견할 수 있어, 디버깅과 유지보수에 모두 도움이 되는 구현입니다. useAuthCore와 동일한 패턴으로 일관성도 좋아 보입니다.

src/hooks/useShare.ts (1)

11-33: 이전 리뷰 이슈가 해결되었으며, 폴백 로직이 견고합니다

이전 리뷰에서 지적된 catch (error)의 미사용 파라미터가 catch {}로 수정되어 lint 경고가 해결되었습니다. Web Share API → 클립보드 → 에러 시 재시도 클립보드 복사로 이어지는 3단계 폴백 전략이 잘 구현되어 있습니다.

Copy link

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

♻️ Duplicate comments (1)
src/contexts/auth/ScrapContext.tsx (1)

39-78: pendingTerms로 중복 토글을 막은 점은 좋지만, Firestore 쪽은 원자 연산도 검토해 보세요.

  • pendingTerms를 통해 같은 termId에 대한 동시 토글을 클라이언트 단에서 잘 방지하고 있어서, 이전 리뷰에서 지적된 “빠른 연속 클릭” 문제는 대부분 해소된 것으로 보입니다.
  • 다만 여전히 전체 scrapList를 읽어서 새 배열을 만들어 통째로 updateDoc에 쓰는 패턴이라, 여러 디바이스/탭에서 동시에 다른 용어를 스크랩/언스클랩하는 경우 마지막 요청이 다른 변경을 덮어쓸 수 있는 여지는 남아 있습니다.

가능하다면 Firestore의 배열 원자 연산을 활용해 다음처럼 개선할 수 있습니다:

-import { doc, updateDoc } from "firebase/firestore";
+import { doc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore";

-const newScrapList = currentlyScraped
-  ? userData.scrapList.filter((id) => id !== termId)
-  : [...userData.scrapList, termId];

-const userRef = doc(db, "users", user.uid);
-await updateDoc(userRef, {
-  scrapList: newScrapList,
-});

-// 로컬 상태 업데이트
-updateScrapList(newScrapList);
+const userRef = doc(db, "users", user.uid);
+const updateData = currentlyScraped
+  ? { scrapList: arrayRemove(termId) }
+  : { scrapList: arrayUnion(termId) };
+
+await updateDoc(userRef, updateData);
+
+// Firestore를 기준으로 로컬 상태 동기화
+const nextScrapList = currentlyScraped
+  ? userData.scrapList.filter((id) => id !== termId)
+  : [...userData.scrapList, termId];
+updateScrapList(nextScrapList);

이렇게 하면 단일 필드에 대해 해당 항목만 추가/제거하는 원자 연산이 되어, 다중 클라이언트 환경에서도 충돌 가능성이 더 줄어듭니다. (이전 리뷰에서도 비슷한 방향이 언급되었으므로, 요구사항 상 다중 클라이언트 동시성을 어디까지 고려할지 한 번 더 정리해 보셔도 좋겠습니다.)

Cloud Firestore arrayUnion arrayRemove updateDoc atomic updates concurrent writes
🧹 Nitpick comments (2)
src/app/login/auth/LoginBody.tsx (1)

14-24: 이미 로그인된 사용자가 로그인 페이지 방문 시 처리 고려.

현재 구현은 isLoggingIn.current가 true일 때만 라우팅하므로, 이미 로그인된 사용자가 /login에 직접 접근하면 페이지에 머무르게 됩니다.

의도된 동작이라면 문제없지만, 로그인된 사용자를 자동으로 홈으로 리다이렉트하려면 별도 처리가 필요합니다.

src/hooks/useShare.ts (1)

40-47: slug를 URL 인코딩하는 것을 권장합니다.

Line 45에서 slug를 URL에 직접 삽입하고 있습니다. Next.js 라우트 파라미터에서 온 slug는 일반적으로 안전하지만, 특수 문자가 포함된 경우를 대비해 encodeURIComponent로 인코딩하는 것이 더 안전합니다.

다음과 같이 수정할 수 있습니다:

 	const shareTerm = async (
 		title: string,
 		summary: string,
 		slug: string
 	): Promise<void> => {
-		const url = `${window.location.origin}/terms/${slug}`;
+		const url = `${window.location.origin}/terms/${encodeURIComponent(slug)}`;
 		await share({ title, text: summary, url });
 	};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5e2ed7 and 7093b9e.

📒 Files selected for processing (5)
  • src/app/login/auth/LoginBody.tsx (1 hunks)
  • src/contexts/auth/ScrapContext.tsx (1 hunks)
  • src/contexts/auth/UserDataContext.tsx (1 hunks)
  • src/hooks/useShare.ts (1 hunks)
  • src/lib/userService.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/login/auth/LoginBody.tsx (2)
src/contexts/auth/index.tsx (2)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (151-157)
src/contexts/auth/ScrapContext.tsx (4)
src/contexts/auth/index.tsx (3)
  • ScrapProvider (40-40)
  • useAuthCore (35-35)
  • useUserData (35-35)
src/contexts/auth/AuthContext.tsx (1)
  • useAuthCore (104-110)
src/contexts/auth/UserDataContext.tsx (1)
  • useUserData (151-157)
src/utils/firebase.ts (1)
  • db (23-23)
src/lib/userService.ts (3)
src/contexts/auth/UserDataContext.tsx (1)
  • UserData (27-27)
src/config/categories.ts (1)
  • CategoryType (26-36)
src/utils/firebase.ts (1)
  • db (23-23)
src/contexts/auth/UserDataContext.tsx (5)
src/lib/userService.ts (6)
  • UserData (10-18)
  • fetchUserData (23-41)
  • createUserData (46-64)
  • mergeScrapList (69-80)
  • completeUserOnboarding (85-94)
  • updateUserCategory (99-105)
src/contexts/auth/index.tsx (3)
  • UserData (19-19)
  • UserDataProvider (39-39)
  • useAuthCore (35-35)
src/config/categories.ts (1)
  • CategoryType (26-36)
src/contexts/auth/AuthContext.tsx (1)
  • useAuthCore (104-110)
src/lib/bookmarks.ts (2)
  • getBookmarks (10-19)
  • clearBookmarks (49-51)
🔇 Additional comments (17)
src/contexts/auth/ScrapContext.tsx (2)

28-34: isScraped의 null 가드와 단순 로직이 적절합니다.

userData가 없는 경우 바로 false를 반환하고, 있을 때만 includes로 확인하는 구조가 명확하고 안전합니다. 추가적인 예외 처리가 필요해 보이지 않습니다.


93-99: useScrap 훅의 컨텍스트 가드 패턴이 깔끔합니다.

useContext로 값을 받아오고, 없을 때 명시적으로 에러를 던지는 패턴이 다른 컨텍스트들과도 일관되고, 오용을 초기에 잡을 수 있어 좋습니다.

src/lib/userService.ts (4)

23-41: 이전 리뷰 피드백이 잘 반영되었습니다.

Firestore 데이터에 기본값 병합 로직이 추가되어 스키마 변경이나 마이그레이션 시 안전성이 향상되었습니다.


46-64: LGTM!

신규 사용자 데이터 생성 로직이 명확하고 기본값 설정이 적절합니다.


69-80: LGTM!

Set을 사용한 중복 제거 로직이 적절합니다.


85-105: LGTM!

온보딩 완료 및 카테고리 업데이트 함수가 간결하게 구현되었습니다. 에러 처리는 호출측(UserDataContext)에서 담당하므로 적절한 구조입니다.

src/contexts/auth/UserDataContext.tsx (4)

47-82: 이전 리뷰 피드백이 잘 반영되었습니다.

loadUserDatauseCallback으로 감싸져 있고 의존성 배열에 포함되어 있습니다. 에러 처리도 적절하게 구현되었습니다.


99-128: 이전 리뷰 피드백이 잘 반영되었습니다.

completeOnboardingupdateCategory에 에러 처리가 추가되어 서버 호출 실패 시 로컬 상태 불일치 문제가 해결되었습니다.


134-157: LGTM!

Provider와 hook 패턴이 올바르게 구현되었습니다. Context 외부에서 hook 사용 시 명확한 에러 메시지를 제공합니다.


130-132: This concern is invalid. The updateScrapList function is intentionally a local-state-only updater. Firestore synchronization happens in ScrapContext.tsx (line 63) before updateScrapList is called (line 66), so there is no risk of data inconsistency. This design correctly separates concerns between the scrap toggle logic (in ScrapContext) and state management (in UserDataContext).

Likely an incorrect or invalid review comment.

src/app/login/auth/LoginBody.tsx (2)

26-34: 이전 리뷰 피드백이 잘 반영되었습니다.

isLoggingIn.current가 로그인 요청 전에 설정되고, 에러 발생 시 false로 리셋되어 경쟁 조건 문제가 해결되었습니다.


36-45: LGTM!

데모 로그인 핸들러가 Google 로그인과 일관된 패턴으로 구현되었고, 실패 시 사용자 피드백(alert)도 제공합니다.

src/hooks/useShare.ts (5)

1-1: "use client" 지시어가 올바르게 추가되었습니다.

이전 리뷰에서 지적된 사항이 정상적으로 반영되었으며, navigatorwindow 같은 브라우저 전용 API를 사용하는 훅이므로 적절한 처리입니다.


7-11: 인터페이스 설계가 깔끔합니다.

ShareParams 인터페이스가 Web Share API의 요구사항과 잘 맞으며, 타입 정의가 명확합니다.


14-35: 공유 및 폴백 로직이 견고하게 구현되었습니다.

이전 리뷰의 미사용 error 파라미터 문제가 해결되었으며(line 27), 에러 처리 흐름이 적절합니다:

  • Web Share API를 우선 사용
  • 미지원 시 클립보드 복사로 폴백
  • 공유 취소/실패 시에도 클립보드 복사 시도
  • 모든 방법 실패 시 조용히 무시

52-57: 현재 페이지 공유 기능이 올바르게 구현되었습니다.

window.location.href를 사용하여 현재 페이지 URL을 가져오고, share 함수에 위임하는 구조가 간결하고 적절합니다.


59-63: 훅의 반환 구조가 명확합니다.

세 가지 공유 함수를 일관되게 노출하고 있으며, React 훅의 관례를 잘 따르고 있습니다.

@hyesngy hyesngy merged commit 0cb357b into develop Dec 1, 2025
4 checks passed
@hyesngy hyesngy deleted the refactor/82 branch December 1, 2025 01:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧹 Refactor 코드 리팩토링 ✨ style 코드 포맷팅, 코드 오타, 함수명 수정 등 스타일

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 코드 리팩토링

2 participants