-
Notifications
You must be signed in to change notification settings - Fork 0
[Refactor/82] 코드 리팩토링 #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Walkthrough카테고리·역할·테마 설정을 중앙화하고 인증을 분리(AuthCore, UserData, Scrap)했으며 여러 훅(useChatBot, useQuizState, useShare, useDropdown 등)과 유틸(date/sorting)을 도입하고 아이콘/레거시 카테고리 모듈을 정리했습니다. UI 컴포넌트들은 새 설정·훅에 맞춰 리팩터링되었습니다. Changes
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 }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this 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:@deprecatedJSDoc 주석 추가 권장주석으로 deprecated를 표시했지만, IDE 지원을 위해
@deprecatedJSDoc 어노테이션을 사용하면 더 좋습니다. 또한Conversation.role과Role타입의 값이 다릅니다 (pmvsPM,developervsDev).-// 대화 상황 타입 (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: 카테고리 설정 중앙화 연동이 일관성 있게 잘 바뀌었습니다
getCategoryType→CATEGORIES[category]로 이어지는 흐름이 명확하고, 기존getCategoryConfig의 사용 방식도 그대로 유지돼서 리팩터링 방향 좋습니다.선택 사항이지만, 반환 값을 명시적으로 타입 선언해 두면(예:
{ category: CategoryType; config: CategoryConfig }) 이후 사용처에서 타입 추론과 리팩터링 시 좀 더 안전하게 동작할 것 같습니다.src/app/chatbot/components/ChatBot.tsx (1)
4-19: useChatBot 훅으로 상태/이펙트 분리한 구조가 깔끔합니다채팅 상태, 로딩, 스크롤 처리까지 훅으로 위임해서
ChatBot컴포넌트가 뷰 레이어에만 집중하게 된 점이 좋습니다.QuickActionButton과textarea의 엔터 처리도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내부에서isScraped를useCallback등으로 메모이즈하지 않았다면, 렌더마다 참조가 바뀌면서 용어/연관 용어를 매번 다시 로드할 수 있습니다. 이미 메모이즈돼 있다면 그대로 두셔도 괜찮고, 아니라면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 재사용성은 좋지만, 타입/버튼 기본 동작 두 가지는 보완을 권장합니다
React.MouseEvent타입 사용 시 React 네임스페이스
- 현재 파일에서
React.MouseEvent를 사용하지만React를 import 하지 않고 있어, TS 설정에 따라Cannot find namespace 'React'에러가 날 수 있습니다.- 보다 안전하게는 타입 전용 import 후
MouseEventHandler를 쓰는 편을 권장합니다.기본
buttontype 명시
- 재사용 가능한 버튼 컴포넌트인 만큼, 폼 안에서 사용될 가능성을 고려하면 기본 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] );또는
setInput과setMessages는 안정적이므로:- [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도 동일한 문서 존재 가정 문제가 있습니다.사용자 문서가 없는 상태에서 호출되면 실패합니다. 서비스 레이어에서 에러 처리를 추가하거나, 문서 생성과 업데이트를 동시에 처리하는
setDoc의merge옵션 사용을 고려해보세요.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
📒 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: 새로운 컨텍스트 구조로 잘 마이그레이션되었습니다.
useAuthCore와useScrap으로의 분리가 관심사의 분리(separation of concerns) 원칙을 잘 따르고 있습니다. 인증과 스크랩 로직이 명확히 구분되어 유지보수가 용이해졌습니다.src/app/dashboard/components/ProfileCard.tsx (2)
6-12: 중앙화된 설정과 컨텍스트로 잘 마이그레이션되었습니다.
CATEGORIES설정을 사용하여 카테고리 정보를 중앙화하고, 인증 로직을useAuthCore와useUserData로 분리한 것이 코드 유지보수성을 크게 향상시켰습니다.
15-17: 적절한 폴백 로직입니다.
userData와user간의 폴백 체인이 잘 구현되어 있어, 데이터 로딩 중이거나 일부 정보가 누락된 경우에도 안정적으로 동작합니다.src/utils/sorting.ts (3)
15-23: 문자 타입 우선순위가 잘 구현되었습니다.숫자 → 특수문자 → 한글 → 영문 순서의 우선순위가 명확하게 구현되었습니다. 빈 문자열이 영문과 동일한 우선순위(4)를 받는 것은 의도된 동작으로 보입니다.
28-44: 한글 정렬이 올바르게 구현되었습니다.
localeCompare에ko로케일과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 optionalslugfield appropriately. InScrapCard.tsx(line 19), there is a conditional checkif (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, thetermToScrapCardfunction 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-400은src/styles/globals.css에--color-primary-400: #ba9be5로 정의되어 있으므로 Tailwind에서 올바르게 적용됩니다.src/components/providers/Providers.tsx (1)
4-4: 認証コンテキスト分離が完全に実装され、すべてのコンシューマーが正しく移行済みです。
CombinedAuthProviderはAuthProvider>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: 검색 결과 카드의 공유/스크랩/카테고리 리팩토링 구조가 잘 정리되어 있습니다
useShare의shareTerm을 사용해 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를 한 곳에서 모아 쓰기 좋게 잘 정리되었습니다
CombinedAuthProvider가AuthProvider → 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_KEYS와CATEGORIES를 사용하여 이전의 하드코딩된 설정을 대체했습니다. 타입 안정성과 코드 일관성이 유지되고 있습니다.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: 정렬 로직을 유틸리티 함수로 위임하여 코드 중복을 제거했습니다.
sortByKorean과sortByDateDesc유틸리티를 활용하여 정렬 로직을 중앙화했습니다. 한글 정렬과 날짜 정렬이 올바르게 구현되어 있으며, 타입 안정성도 유지되고 있습니다.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 lifecycleThe concern about
questionschanging at runtime and causinguserAnswerssync 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. Thequestionsprop 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타입 적용으로 불변성 보장.
navItemsprops에readonly수식자를 사용하여 배열 변경을 방지하는 것이 좋습니다.src/config/categories.ts (1)
1-9: LGTM! 중복 제거를 위한 중앙화된 설정 파일.여러 파일에 분산되어 있던 카테고리 설정을 하나의 파일로 통합하여 유지보수성을 크게 향상시켰습니다.
src/app/dashboard/components/ScrapSection.tsx (2)
6-7: LGTM! 중앙화된 설정 사용.
CATEGORIES와CATEGORY_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.
mergeScrapListis only called in theelsebranch ofloadUserData(UserDataContext.tsx:71), which executes only whenfetchUserDatareturns non-null data. SincefetchUserDatareturnsnullwhen the document doesn't exist, any non-null return value guarantees the document exists in Firestore.
There was a problem hiding this 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;같은 플래그를 두고useEffectcleanup에서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
📒 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단계 폴백 전략이 잘 구현되어 있습니다.
There was a problem hiding this 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
📒 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: 이전 리뷰 피드백이 잘 반영되었습니다.
loadUserData가useCallback으로 감싸져 있고 의존성 배열에 포함되어 있습니다. 에러 처리도 적절하게 구현되었습니다.
99-128: 이전 리뷰 피드백이 잘 반영되었습니다.
completeOnboarding과updateCategory에 에러 처리가 추가되어 서버 호출 실패 시 로컬 상태 불일치 문제가 해결되었습니다.
134-157: LGTM!Provider와 hook 패턴이 올바르게 구현되었습니다. Context 외부에서 hook 사용 시 명확한 에러 메시지를 제공합니다.
130-132: This concern is invalid. TheupdateScrapListfunction is intentionally a local-state-only updater. Firestore synchronization happens inScrapContext.tsx(line 63) beforeupdateScrapListis 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" 지시어가 올바르게 추가되었습니다.이전 리뷰에서 지적된 사항이 정상적으로 반영되었으며,
navigator와window같은 브라우저 전용 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 훅의 관례를 잘 따르고 있습니다.
✨ 작업 개요
코드 리팩토링
📌 관련 이슈
✅ 작업 내용
코드 리팩토링
📷 UI 스크린샷 (해당 시)
💬 기타 사항
Summary by CodeRabbit
New Features
Improvements
Removed
✏️ Tip: You can customize this high-level summary in your review settings.