-
Notifications
You must be signed in to change notification settings - Fork 2
Api(client): 대시보드 api 연결 #101
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
|
Warning Rate limit exceeded@karnelll has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 21 minutes and 3 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (7)
Walkthrough이 변경사항은 대시보드의 리마인드 및 북마크 세션 API 연동을 중심으로, 기존의 목업 데이터와 타이머 유틸리티/훅을 제거하고, API 기반의 데이터 흐름으로 전환하는 대규모 리팩토링을 수행합니다. 주요 컴포넌트, 훅, 유틸리티, 타입 정의가 모두 실제 API와 연동되도록 수정되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Dashboard
participant useDashboard
participant useAcornCount
participant useCategories
participant useArticles
participant API
Dashboard->>useDashboard: 초기화 및 상태 관리
useDashboard->>useAcornCount: 도토리 개수 요청
useDashboard->>useCategories: 카테고리 목록 요청
useDashboard->>useArticles: 기사 데이터 요청(카테고리/리마인드/미읽음)
useAcornCount->>API: getAcornCountWithRemind(now)
useCategories->>API: getDashboardCategories()
useCategories->>API: getArticlesByCategory()
useArticles->>API: getUnreadArticles(), getDailyReminderArticles(), getArticlesByCategory()
Dashboard->>useDashboard: 카테고리/기사/도토리 상태, 핸들러 사용
Dashboard->>API: 기사 읽음 상태 업데이트 등
Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
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: 20
♻️ Duplicate comments (1)
apps/client/src/pages/dashboard/components/ui/overlays/LevelInfoModal.tsx (1)
3-3: 동일한 경로 변경 및 설정 중복 여부앞선 코멘트와 동일한 내용입니다. 경로 매핑이 전역 설정에 포함되어 있는지만 검토해 주시면 됩니다.
🧹 Nitpick comments (13)
apps/client/src/shared/utils/errorUtils.ts (1)
6-6: 타입 안전성 개선 권장
any타입 사용으로 인한 타입 안전성 문제가 있습니다.Axios 에러 타입을 활용하여 타입 안전성을 개선할 수 있습니다:
+import { AxiosError } from 'axios'; + -export const logApiError = (apiName: string, error: any) => { +export const logApiError = (apiName: string, error: AxiosError) => {Also applies to: 23-23
apps/client/src/api/thumbnail.ts (1)
55-58: 불필요한 헤더 설정
Content-Type: text/html헤더는 GET 요청에서 불필요합니다.const response = await fetch(proxyUrl, { method: 'GET', - headers: { 'Content-Type': 'text/html' } });apps/client/src/pages/dashboard/apis/userApi.ts (2)
5-11: now 파라미터의 타입을 더 구체적으로 지정해주세요.
now파라미터가string타입으로 되어 있는데, 실제로는 ISO 날짜 문자열이나 타임스탬프일 것으로 예상됩니다. 더 구체적인 타입을 사용하면 타입 안전성이 향상됩니다.export const getAcornCountWithRemind = async ( - now?: string + now?: string // ISO date string (e.g., '2023-12-25T10:00:00Z') ): Promise<ApiResponse<AcornCountWithRemindResponse>> => {
16-18: 에러 타입을 더 구체적으로 지정해주세요.
any타입 대신unknown이나 더 구체적인 에러 타입을 사용하는 것이 타입 안전성 측면에서 좋습니다.- } catch (error: any) { + } catch (error: unknown) { logApiError('getAcornCountWithRemind', error); throw error; }apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
30-52: 정규식 기반 추출 로직 승인 및 개선 제안메타태그와 favicon 추출 로직이 기본적인 케이스를 잘 처리합니다. 상대 URL 처리도 적절합니다.
향후 더 복잡한 HTML 구조를 처리해야 한다면 DOMParser 사용을 고려해볼 수 있습니다:
-const extractMetaContent = (html: string, selector: string): string | null => { - const regex = new RegExp(`<meta[^>]*${selector}[^>]*content="([^"]*)"`, 'i'); - const match = html.match(regex); - return match ? match[1] : null; -}; +const extractMetaContent = (html: string, selector: string): string | null => { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const meta = doc.querySelector(`meta[${selector}]`); + return meta?.getAttribute('content') || null; + } catch { + // 폴백: 기존 정규식 방식 + const regex = new RegExp(`<meta[^>]*${selector}[^>]*content="([^"]*)"`, 'i'); + const match = html.match(regex); + return match ? match[1] : null; + } +};apps/client/src/pages/dashboard/dashboard.tsx (1)
35-37: 초기화 로직 개선 필요
setIsInitialized(true)를 호출하는 로직이 컴포넌트 마운트 시 매번 실행됩니다. 이미 초기화된 상태인지 확인하는 조건문을 추가하거나,useDashboard훅 내부에서 자동으로 처리하는 것이 좋겠습니다.useEffect(() => { - setIsInitialized(true); -}, [setIsInitialized]); + // useDashboard 훅 내부에서 자동으로 처리하도록 이동 권장 + setIsInitialized(true); +}, [setIsInitialized]);apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx (1)
5-5: 상대 경로 임포트 개선 필요깊은 상대 경로 대신 경로 별칭을 사용하는 것이 유지보수에 유리합니다.
-import { generateThumbnail } from '../../../../../api/thumbnail'; +import { generateThumbnail } from '@api/thumbnail';apps/client/src/pages/dashboard/types/api.ts (1)
10-14: Category 인터페이스에 totalCount 속성 추가를 고려하세요.현재
unreadCount만 포함되어 있지만,useDashboard훅에서는 각 카테고리의 총 아티클 수를 별도로 조회하고 있습니다. 데이터 일관성을 위해totalCount속성 추가를 검토해보세요.export interface Category { categoryId: number; categoryName: string; unreadCount: number; + totalCount?: number; }apps/client/src/pages/dashboard/hooks/useDashboard.ts (3)
74-82: 반복되는 재시도 로직을 공통 설정으로 추출하세요.여러 쿼리에서 유사한 재시도 로직이 반복됩니다. 공통 설정으로 추출하면 유지보수가 용이해집니다.
// 파일 상단에 추가 const defaultRetryConfig = { retry: (failureCount: number, error: any) => { const status = error?.response?.status; const noRetryStatuses = [401, 404, 405, 500]; if (noRetryStatuses.includes(status)) { return false; } return failureCount < 2; }, retryDelay: 1000, }; // 사용 예시 useQuery({ queryKey: ['acornCount'], queryFn: () => getAcornCountWithRemind(getKSTLocalDateTime()), staleTime: 10 * 60 * 1000, enabled: isInitialized, ...defaultRetryConfig, });Also applies to: 94-101, 114-121, 150-158, 171-179
192-227: 카테고리 총 개수 조회 최적화를 고려하세요.각 카테고리마다 별도 API 호출을 하고 있어 카테고리가 많을 경우 성능 문제가 발생할 수 있습니다. 백엔드에서 모든 카테고리의 총 개수를 한 번에 제공하는 엔드포인트를 고려해보세요.
현재 구현도 병렬 처리로 최적화되어 있지만, 카테고리 수가 증가하면 다음과 같은 문제가 발생할 수 있습니다:
- N개의 카테고리에 대해 N개의 HTTP 요청 발생
- 네트워크 오버헤드 증가
- API 서버 부하 증가
백엔드 팀과 협의하여
/api/v1/categories/counts같은 통합 엔드포인트 추가를 검토하세요.
230-258: 에러 로깅 useEffect를 통합하여 간소화하세요.각 쿼리마다 별도의 useEffect를 사용하는 대신 하나로 통합할 수 있습니다.
// 모든 에러를 하나의 useEffect에서 처리 useEffect(() => { const errors = [ { name: 'getAcornCountWithRemind', error: acornError }, { name: 'getDashboardCategories', error: categoriesError }, { name: 'getUnreadArticles', error: unreadArticlesError }, { name: 'getDailyReminderArticles', error: dailyReminderError }, { name: 'getArticlesByCategory', error: categoryArticlesError }, ]; errors.forEach(({ name, error }) => { if (error) { logApiError(`useQuery: ${name}`, error); } }); }, [acornError, categoriesError, unreadArticlesError, dailyReminderError, categoryArticlesError]);apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
94-108: 타입 가드를 사용하여 유니온 타입 처리를 개선하세요.현재 'articleId' in bookmark 검사를 반복적으로 사용하고 있습니다. 타입 가드 함수를 만들어 코드를 더 깔끔하게 만들 수 있습니다.
// 컴포넌트 외부에 타입 가드 추가 const isArticle = (bookmark: BookmarkCardProps | Article): bookmark is Article => { return 'articleId' in bookmark; }; // handleCardClick 개선 const handleCardClick = (bookmark: BookmarkCardProps | Article) => { if (isArticle(bookmark) && onArticleRead) { onArticleRead(bookmark.articleId); } const url = isArticle(bookmark) ? bookmark.url : bookmark.url; if (url) { window.open(url, '_blank', 'noopener,noreferrer'); } }; // 렌더링 부분 개선 <BookmarkCard {...(isArticle(card) ? { id: card.articleId.toString(), title: card.url, memo: card.memo, isRead: card.isRead, categoryId: '1', url: card.url, handlePopUpOpen, } : card)} handlePopUpOpen={handlePopUpOpen} />Also applies to: 218-227
68-85: 빈 상태 결정 로직을 단순화하세요.현재 로직이 복잡하고 중복된 조건 검사가 있습니다.
const getEmptyStatePreset = () => { // 안읽은 카테고리에서 안읽은 아티클이 없는 경우 if (isUnreadCategory() && totalUnreadArticle === 0) { return 'unread'; } // 그 외 모든 경우 (카테고리 비어있음) return 'url'; };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (47)
apps/client/src/api/thumbnail.ts(1 hunks)apps/client/src/main.tsx(1 hunks)apps/client/src/pages/dashboard/apis/articleApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/categoryApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/index.ts(1 hunks)apps/client/src/pages/dashboard/apis/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/apis/userApi.ts(1 hunks)apps/client/src/pages/dashboard/components/common/display/BookmarkCategory.tsx(1 hunks)apps/client/src/pages/dashboard/components/common/layout/SectionContent.tsx(1 hunks)apps/client/src/pages/dashboard/components/common/layout/SectionTitle.tsx(1 hunks)apps/client/src/pages/dashboard/components/sections/BannerSection.tsx(1 hunks)apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx(6 hunks)apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx(3 hunks)apps/client/src/pages/dashboard/components/sections/LevelInfoSection.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/banner/BannerTimer.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/buttons/AllViewButton.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx(5 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/indicators/LevelInfoItem.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/overlays/LevelInfoModal.tsx(1 hunks)apps/client/src/pages/dashboard/constants/levelData.ts(0 hunks)apps/client/src/pages/dashboard/dashboard.tsx(1 hunks)apps/client/src/pages/dashboard/hooks/useDashboard.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useTimer.ts(0 hunks)apps/client/src/pages/dashboard/mockData.ts(0 hunks)apps/client/src/pages/dashboard/types/api.ts(1 hunks)apps/client/src/pages/dashboard/types/components.ts(2 hunks)apps/client/src/pages/onBoarding/OnBoarding.tsx(0 hunks)apps/client/src/pages/onBoarding/components/StoryStep.tsx(0 hunks)apps/client/src/shared/apis/axiosInstance.ts(1 hunks)apps/client/src/shared/apis/index.ts(1 hunks)apps/client/src/shared/apis/timerApi.ts(0 hunks)apps/client/src/shared/components/ui/modalPop/ModalPop.tsx(0 hunks)apps/client/src/shared/hooks/index.ts(1 hunks)apps/client/src/shared/hooks/useServerTimer.ts(0 hunks)apps/client/src/shared/index.ts(1 hunks)apps/client/src/shared/types/api.ts(0 hunks)apps/client/src/shared/types/index.ts(0 hunks)apps/client/src/shared/utils/errorUtils.ts(1 hunks)apps/client/src/shared/utils/index.ts(1 hunks)apps/client/src/shared/utils/timerUtils.ts(0 hunks)apps/client/vite.config.ts(0 hunks)packages/design-system/package.json(0 hunks)
💤 Files with no reviewable changes (13)
- apps/client/vite.config.ts
- apps/client/src/pages/onBoarding/OnBoarding.tsx
- packages/design-system/package.json
- apps/client/src/shared/components/ui/modalPop/ModalPop.tsx
- apps/client/src/pages/onBoarding/components/StoryStep.tsx
- apps/client/src/pages/dashboard/constants/levelData.ts
- apps/client/src/shared/types/index.ts
- apps/client/src/shared/hooks/useServerTimer.ts
- apps/client/src/pages/dashboard/hooks/useTimer.ts
- apps/client/src/shared/utils/timerUtils.ts
- apps/client/src/pages/dashboard/mockData.ts
- apps/client/src/shared/apis/timerApi.ts
- apps/client/src/shared/types/api.ts
🧰 Additional context used
🧠 Learnings (21)
apps/client/src/pages/dashboard/components/common/layout/SectionContent.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/ui/overlays/LevelInfoModal.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/components/ui/indicators/LevelInfoItem.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/ui/buttons/AllViewButton.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/sections/LevelInfoSection.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/common/display/BookmarkCategory.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/common/layout/SectionTitle.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/types/components.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx (3)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
apps/client/src/pages/dashboard/components/ui/banner/BannerTimer.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/api/thumbnail.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
apps/client/src/pages/dashboard/components/sections/BannerSection.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/dashboard.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx (3)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🧬 Code Graph Analysis (7)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
apps/client/src/api/thumbnail.ts (1)
generateThumbnail(45-79)
apps/client/src/api/thumbnail.ts (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
generateThumbnail(3-28)
apps/client/src/pages/dashboard/dashboard.tsx (5)
apps/client/src/pages/dashboard/hooks/useDashboard.ts (1)
useDashboard(17-437)apps/client/src/pages/dashboard/types/api.ts (1)
Category(10-14)apps/client/src/pages/dashboard/types/components.ts (1)
Category(24-28)apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
createTestCategories(9-20)apps/client/src/pages/dashboard/apis/articleApi.ts (1)
createTestArticles(94-107)
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (1)
apps/client/src/pages/dashboard/types/api.ts (1)
Article(1-8)
apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx (1)
apps/client/src/shared/utils/cn.ts (1)
cn(4-6)
apps/client/src/pages/dashboard/apis/categoryApi.ts (2)
apps/client/src/pages/dashboard/types/api.ts (2)
ApiResponse(57-61)CategoryListResponse(48-50)apps/client/src/shared/utils/errorUtils.ts (2)
logApiSuccess(23-28)logApiError(6-21)
apps/client/src/pages/dashboard/hooks/useDashboard.ts (6)
apps/client/src/pages/dashboard/constants/bannerImages.ts (1)
BannerStage(21-21)apps/client/src/pages/dashboard/apis/userApi.ts (1)
getAcornCountWithRemind(5-20)apps/client/src/pages/dashboard/apis/categoryApi.ts (2)
getDashboardCategories(22-33)createTestCategories(9-20)apps/client/src/pages/dashboard/apis/articleApi.ts (5)
getUnreadArticles(46-60)getDailyReminderArticles(62-77)getArticlesByCategory(29-44)updateArticleReadStatus(79-92)createTestArticles(94-107)apps/client/src/pages/dashboard/types/api.ts (3)
Category(10-14)ApiResponse(57-61)ReadStatusResponse(43-46)apps/client/src/shared/utils/errorUtils.ts (2)
logApiError(6-21)logApiSuccess(23-28)
🔇 Additional comments (33)
apps/client/src/shared/hooks/index.ts (1)
1-1: useServerTimer 제거로 인한 영향 없음 확인 완료코드베이스 전체에서
useServerTimer를 검색한 결과, 활성화된 호출은 모두 주석 처리된 상태로 남아있어 실제 사용처가 존재하지 않습니다. 더 이상 대체 작업이나 추가 검토가 필요하지 않습니다.apps/client/src/pages/dashboard/components/common/layout/SectionContent.tsx (1)
2-2: 타입 구조 리팩터링 잘 적용됨타입 전용 import와 새로운 경로 구조가 올바르게 적용되었습니다. 대시보드 전용 타입들을 별도로 관리하는 것은 모듈성 향상에 도움이 됩니다.
apps/client/src/pages/dashboard/components/sections/LevelInfoSection.tsx (1)
8-8: 타입 import 경로 업데이트 확인됨대시보드 컴포넌트 타입을 전용 디렉토리로 이동하는 리팩터링이 일관되게 적용되었습니다.
apps/client/src/pages/dashboard/components/ui/indicators/LevelInfoItem.tsx (1)
4-4: 타입 경로 변경이 프로젝트 설정에 반영됐는지 확인 필요
@pages/dashboard/types/components경로가tsconfig.json(또는vite.config.ts)의paths/alias에 등록되어 있지 않으면 컴파일 오류가 발생합니다. 경로 매핑이 이미 반영-되었는지 한 번 더 확인해 주세요.apps/client/src/pages/dashboard/components/ui/buttons/AllViewButton.tsx (1)
2-2:type전용 임포트로 변경 👍
import type사용으로 트리쉐이킹 최적화에 도움 됩니다. 경로 설정만 앞서 언급한 대로 확인해 주세요.apps/client/src/pages/dashboard/components/common/display/BookmarkCategory.tsx (1)
4-4: 경로 변경 확인타입 폴더 분리로 모듈 경로가 변경되었습니다. 실제 배포 번들에서 중복 정의나 사이클이 생기지 않는지 의존성 그래프만 한번 검토해 주세요.
apps/client/src/pages/dashboard/components/common/layout/SectionTitle.tsx (1)
1-1: 타입 import 경로 일관성 유지 완료모든 섹션 컴포넌트가 동일한 타입 경로를 사용하도록 맞춰졌습니다. 추가 수정 사항 없습니다.
apps/client/src/shared/apis/index.ts (1)
1-1: API 클라이언트 중앙화가 잘 구현되었습니다.axios 인스턴스를 중앙에서 관리하는 좋은 패턴입니다. 새로운 대시보드 API 모듈들이 일관된 HTTP 클라이언트를 사용할 수 있게 해줍니다.
apps/client/src/pages/dashboard/apis/index.ts (1)
1-3: API 모듈 구조화가 잘 되어있습니다.배럴 익스포트 패턴으로 대시보드 API들을 깔끔하게 정리했습니다. 컴포넌트에서 API를 사용할 때 단일 진입점을 통해 편리하게 접근할 수 있습니다.
apps/client/src/main.tsx (1)
15-18: StrictMode 제거 의도 확인 필요
apps/client/src/main.tsx에서<StrictMode>래퍼가 제거되어 개발 모드에서 제공되는 잠재적 오류 검출 기능이 비활성화되었습니다.
또한apps/landing/src/main.tsx에는 여전히<StrictMode>가 적용되어 있어 두 앱 간 일관성이 깨졌습니다. 이 변경이 React Query 호환성이나 다른 특정 이유로 의도된 것인지 확인 부탁드립니다.
- 대상 파일:
apps/client/src/main.tsx- 제거 위치: 렌더링 시점에 감싸던
<StrictMode>(기존 라인 15–18)apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx (1)
2-2: 타입 임포트 경로 변경이 적절합니다.대시보드별 타입을 별도 디렉토리로 분리하는 리팩토링이 잘 적용되었습니다.
apps/client/src/shared/utils/index.ts (1)
4-4: 유틸리티 모듈 교체가 적절합니다.timerUtils에서 errorUtils로의 변경이 새로운 API 통합 작업과 잘 맞습니다. API 에러 및 성공 로깅 기능이 대시보드 API 모듈들에서 유용하게 사용될 것 같습니다.
apps/client/src/shared/index.ts (1)
1-5: 와일드카드 export로 단순화된 구조가 좋습니다.개별 export를 와일드카드 export로 변경하여 코드가 단순해졌습니다. 다만 번들 크기 최적화를 위해 사용하지 않는 모듈의 tree-shaking이 잘 동작하는지 확인해보시기 바랍니다.
apps/client/src/pages/dashboard/components/sections/BannerSection.tsx (2)
7-7: 리마인더 시간 프로퍼티 추가가 적절합니다.
remindDateTime프로퍼티를 옵셔널로 추가하여 기존 코드와의 호환성을 유지하면서 새로운 기능을 지원하는 좋은 접근입니다.
22-26: Banner 컴포넌트로의 props 전달이 올바릅니다.
remindDateTime을 포함한 모든 props가 Banner 컴포넌트로 올바르게 전달되고 있습니다.apps/client/src/pages/dashboard/apis/userApi.ts (1)
13-15: API 요청과 로깅 구현이 우수합니다.apiRequest 사용과 성공/실패 로깅이 적절하게 구현되어 있습니다. URL 구성 로직도 깔끔합니다.
apps/client/src/pages/dashboard/components/ui/banner/BannerTimer.tsx (2)
7-7: 리마인더 시간 프로퍼티 추가가 적절합니다.
remindDateTime프로퍼티를 옵셔널로 추가하여 타이머 기능을 확장하면서도 기존 코드와의 호환성을 유지하는 좋은 설계입니다.
30-30: Timer 컴포넌트로의 targetTime 전달이 올바릅니다.
remindDateTime을targetTime으로 전달하여 동적 카운트다운을 지원하는 구현이 적절합니다.apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx (1)
91-91: 리마인드 날짜시간 prop 추가 승인
remindDateTimeprop이 적절하게 추가되었습니다. 타입 정의가 명확하고 BannerTimer 컴포넌트에 올바르게 전달되고 있습니다.Also applies to: 99-99, 129-129
apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx (2)
8-17: 스타일링 상수 통합 및 리팩터링 승인
CARD_DIMENSIONS객체로 스타일링 상수를 통합하고 템플릿 리터럴을 사용한 것이 코드 가독성과 유지보수성을 개선했습니다.
50-50: URL 기반 썸네일 생성으로 전환 승인
imageprop을url로 변경하여 동적 썸네일 생성 구조로 개선되었습니다. 이는 API 연동과 잘 맞는 변경사항입니다.Also applies to: 57-57, 66-66
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
3-28: 견고한 에러 핸들링과 폴백 메커니즘 승인다단계 폴백 전략(og:image → twitter:image → favicon → default)과 에러 처리가 잘 구현되어 있습니다. 항상 string을 반환하여 UI 안정성을 보장합니다.
apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx (2)
10-47: 타이머 로직 구현 승인타이머 계산 로직이 잘 구현되어 있습니다.
targetTimeprop 사용과 기본값(다음날 8시) 설정이 적절하며, 1초 간격 업데이트와 시간 포맷팅도 정확합니다.
52-52: React.memo 사용 승인타이머 컴포넌트에
memo를 적용하여 불필요한 리렌더링을 방지한 것이 성능 최적화에 도움이 됩니다.apps/client/src/pages/dashboard/apis/categoryApi.ts (2)
9-33: 일관된 API 패턴 구현 승인
createTestCategories와getDashboardCategories함수가 다른 API 모듈과 일관된 패턴을 사용하고 있습니다. 에러 처리, 로깅, 타입 정의가 모두 적절합니다.
5-7: 타입 정의 승인
CreateTestCategoriesResponse인터페이스가 적절하게 정의되어 있고, 다른 공통 타입들을 올바르게 import하여 일관성을 유지하고 있습니다.apps/client/src/pages/dashboard/apis/articleApi.ts (1)
1-107: API 함수들이 잘 구성되어 있습니다!일관된 에러 처리 패턴과 타입 안정성이 잘 적용되어 있습니다. React Query와의 통합도 원활하게 이루어질 것으로 보입니다.
apps/client/src/pages/dashboard/types/api.ts (2)
1-8: 인터페이스 구조가 적절합니다.Article 인터페이스가 필요한 속성들을 잘 정의하고 있으며, remindAt을 optional로 설정한 것이 비즈니스 로직에 부합합니다.
26-61: API 응답 인터페이스 구조가 일관성 있고 적절합니다.모든 응답 타입이 명확히 정의되어 있고, 제네릭
ApiResponse<T>래퍼를 통해 표준화된 응답 형식을 제공합니다.apps/client/src/shared/apis/axiosInstance.ts (1)
10-19: 토큰 발급 중복 방지 로직이 잘 구현되었습니다.플래그와 Promise를 활용한 동시성 제어로 중복 토큰 발급을 효과적으로 방지하고 있습니다.
apps/client/src/pages/dashboard/hooks/useDashboard.ts (2)
260-309: 뮤테이션 구현이 잘 되어 있습니다.적절한 타입 정의, 에러 처리, 그리고 관련 쿼리 무효화로 데이터 일관성을 보장하고 있습니다.
380-437: 훅의 전체적인 구조와 반환 값이 잘 설계되었습니다.상태, 데이터, 로딩 상태, 핸들러 등이 명확하게 구분되어 있고, 필요한 모든 기능을 제공하고 있습니다.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (1)
204-216: 접근성 구현이 훌륭합니다.키보드 네비게이션, ARIA 레이블, 시맨틱 HTML 사용이 적절하게 구현되어 있습니다.
| export const logApiError = (apiName: string, error: any) => { | ||
| const errorCode = error.response?.data?.code; | ||
| const status = error.response?.status; | ||
| const url = error.config?.url; | ||
| const method = error.config?.method?.toUpperCase(); | ||
|
|
||
| console.group(`API Error: ${apiName}`); | ||
| console.error('URL:', url); | ||
| console.error('Method:', method); | ||
| console.error('Status:', status); | ||
| console.error('Error Code:', errorCode); | ||
| console.error('Response:', error.response?.data); | ||
| console.error('Timestamp:', new Date().toISOString()); | ||
| console.error('Full Error:', error); | ||
| console.groupEnd(); | ||
| }; |
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.
🛠️ Refactor suggestion
프로덕션 환경에서의 로깅 처리 검토 필요
현재 구현은 개발 환경에서 유용하지만, 프로덕션 환경에서의 로깅 정책을 고려해야 합니다.
다음과 같이 환경별 로깅을 구현하는 것을 권장합니다:
export const logApiError = (apiName: string, error: any) => {
+ if (process.env.NODE_ENV === 'production') {
+ // 프로덕션에서는 에러 모니터링 서비스로 전송
+ return;
+ }
+
const errorCode = error.response?.data?.code;
// ... 기존 로직
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const logApiError = (apiName: string, error: any) => { | |
| const errorCode = error.response?.data?.code; | |
| const status = error.response?.status; | |
| const url = error.config?.url; | |
| const method = error.config?.method?.toUpperCase(); | |
| console.group(`API Error: ${apiName}`); | |
| console.error('URL:', url); | |
| console.error('Method:', method); | |
| console.error('Status:', status); | |
| console.error('Error Code:', errorCode); | |
| console.error('Response:', error.response?.data); | |
| console.error('Timestamp:', new Date().toISOString()); | |
| console.error('Full Error:', error); | |
| console.groupEnd(); | |
| }; | |
| export const logApiError = (apiName: string, error: any) => { | |
| if (process.env.NODE_ENV === 'production') { | |
| // 프로덕션에서는 에러 모니터링 서비스로 전송 | |
| return; | |
| } | |
| const errorCode = error.response?.data?.code; | |
| const status = error.response?.status; | |
| const url = error.config?.url; | |
| const method = error.config?.method?.toUpperCase(); | |
| console.group(`API Error: ${apiName}`); | |
| console.error('URL:', url); | |
| console.error('Method:', method); | |
| console.error('Status:', status); | |
| console.error('Error Code:', errorCode); | |
| console.error('Response:', error.response?.data); | |
| console.error('Timestamp:', new Date().toISOString()); | |
| console.error('Full Error:', error); | |
| console.groupEnd(); | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/shared/utils/errorUtils.ts between lines 6 and 21, the
current logApiError function logs API errors directly to the console, which is
suitable for development but not for production. Modify the function to check
the environment (e.g., using NODE_ENV) and conditionally log errors: use console
logging in development and integrate with a proper logging service or suppress
detailed logs in production. This ensures appropriate logging behavior based on
the environment.
apps/client/src/api/thumbnail.ts
Outdated
| }; | ||
|
|
||
| // 메인 썸네일 생성 함수 | ||
| export const generateThumbnail = async (url: string): Promise<string | null> => { |
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.
🛠️ Refactor suggestion
반환 타입 일관성 문제
관련 코드 스니펫에서 확인된 다른 구현(apps/client/src/pages/dashboard/apis/thumbnail.ts)은 Promise<string>을 반환하여 항상 기본 이미지를 보장하는 반면, 현재 구현은 Promise<string | null>을 반환합니다.
사용처에서의 null 처리 복잡성을 줄이기 위해 기본 이미지를 항상 반환하는 것을 고려해보세요:
-export const generateThumbnail = async (url: string): Promise<string | null> => {
+export const generateThumbnail = async (url: string): Promise<string> => {
try {
// ... 기존 로직
- return null;
+ return '/default-thumbnail.png';
} catch (error) {
console.error('썸네일 생성 실패:', error);
- return null;
+ return '/default-thumbnail.png';
}
};🤖 Prompt for AI Agents
In apps/client/src/api/thumbnail.ts at line 45, the function generateThumbnail
currently returns Promise<string | null>, which introduces complexity in
handling null values. To fix this, modify the function to always return a
Promise<string> by ensuring it returns a default image string whenever a
thumbnail cannot be generated, thus maintaining consistency with other
implementations and simplifying usage.
apps/client/src/api/thumbnail.ts
Outdated
| const PROXY_SERVICE = 'https://api.allorigins.win/raw'; | ||
| const FAVICON_SERVICE = 'https://www.google.com/s2/favicons'; |
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.
💡 Verification agent
🧩 Analysis chain
외부 프록시 서비스 사용에 대한 보안 검토 필요
api.allorigins.win 프록시 서비스를 사용하는 것은 보안상 위험할 수 있습니다.
다음 사항들을 검토해주세요:
- 프록시 서비스의 신뢰성과 가용성
- 사용자 데이터가 제3자 서비스를 통과하는 것에 대한 보안 정책
- 자체 프록시 서버 구축 고려
🌐 Web query:
api.allorigins.win 서비스의 보안성과 신뢰성은 어떤가요?
💡 Result:
api.allorigins.win 서비스의 신뢰성
api.allorigins.win는 CORS(Cross-Origin Resource Sharing) 정책 때문에 직접 접속이 막힌 웹사이트의 데이터를 프록시(제3자 서버)를 통해 가져와 주는 오픈 소스 서비스입니다[1][3].
이 서비스는 AnyOrigin, Whatever Origin의 역할을 대체하며 자바스크립트 라이브러리로도 사용할 수 있고, gzip 압축 지원, XML을 JSON으로 변환하는 메서드 제공 등 개발자 편의성을 강조합니다[3][4].
특히, 프론트엔드 개발 환경에서 API 호출에 CORS 문제가 발생할 때 쉽게 도입할 수 있는 점이 장점으로 꼽힙니다[4].
공공데이터 등 외부 API를 활용할 때 jsonP 방식, fetch API 등 표준 자바스크립트 코드로 간편하게 사용할 수 있습니다[3][4].
보안성 이슈
보안 측면에서는 다음과 같은 리스크와 한계가 있습니다.
- 데이터 유출 및 중개자 위험: 모든 요청이 api.allorigins.win 서버를 거치기 때문에, 사용자가 요청하는 URL과 그 응답 데이터가 이 서비스의 개발자 또는 운영자에게 노출될 수 있습니다. 민감한 정보(인증 토큰, 개인정보 등)가 포함된 요청을 보낼 경우, 유출 위험이 존재합니다.
- 서비스 지속성 불확실: 이와 같은 CORS 프록시 서비스는 무료로 운영되는 경우가 많으며, 수익 모델이 없어 갑자기 서비스가 중단되거나 접속이 불가능해질 수 있습니다[1]. 실제로 비슷한 서비스들이 운영 압박으로 사라진 사례가 있습니다.
- 제3자 의존: 프록시를 통해 데이터를 가져오므로, api.allorigins.win 서버에 장애가 발생하거나 변조가 일어나면 신뢰할 수 없는 데이터가 전달될 위험이 있습니다.
- 보안 정책 우회: CORS 정책은 웹 보안을 위한 표준적인 조치입니다. 이 정책을 우회하는 행위 자체가 보안 취약점으로 간주될 수 있으며, 특히 금융, 공공, 의료 등 민감한 데이터를 다루는 서비스에서는 사용을 권장하지 않습니다.
결론
신뢰성 측면에서는 개발 편의성과 오픈 소스라는 점에서 긍정적이나, 보안성에서는 치명적인 단점이 있습니다.
공공데이터나 오픈 API 등 민감하지 않은 데이터를 조회하는 데에는 간편한 도구로 활용할 수 있지만, 인증 토큰이나 개인정보 등 민감 데이터가 오가는 환경에서는 사용을 피해야 하며, 서비스 중단 가능성도 항상 염두에 두어야 합니다[1].
프록시 서비스의 특성상, 신뢰성과 보안 모두 본 서비스보다는 직접 백엔드에서 데이터를 중계하는 방식이 훨씬 안전합니다.
Citations:
- 1: https://forum.snap.berkeley.edu/t/what-is-https-api-allorigins-win/7897
- 2: https://koreascience.kr/article/JAKO202415754277026.page
- 3: http://allorigins.win
- 4: https://webdoli.tistory.com/181
외부 프록시 서비스 보안·안정성 검토 및 대체 방안 마련 필요
apps/client/src/api/thumbnail.ts에서 사용 중인 api.allorigins.win 서비스는 다음과 같은 리스크가 확인되었습니다:
- 데이터 유출·중개자 위험
• 요청 URL 및 응답 데이터가 제3자 서버에 노출될 수 있어, 인증 토큰·개인정보 등 민감 데이터 취급 불가 - 서비스 지속성 불확실
• 무료 운영으로 갑작스런 중단·접속 불가 가능성 - 제3자 의존성
• 프록시 서버 장애 또는 데이터 변조 시 신뢰할 수 없는 응답 수신 위험 - 보안 정책 우회
• CORS 정책을 우회하는 행위가 보안 취약점으로 간주될 수 있어, 금융·의료 등 민감 도메인에서는 사용 자제 권고
대응 방안 제안:
- 민감 데이터 요청 시 절대 외부 프록시 사용 금지
- 자체 백엔드 프록시 서버(예: API 게이트웨이) 구축 및 운영
- 외부 서비스 장애에 대비한 모니터링·페일오버 전략 수립
위 리스크를 해소할 수 있는 대체 방안을 검토해주시기 바랍니다.
🤖 Prompt for AI Agents
In apps/client/src/api/thumbnail.ts around lines 2 to 3, the use of the external
proxy service 'https://api.allorigins.win/raw' poses security and reliability
risks including data exposure, service discontinuity, and third-party
dependency. To fix this, replace the external proxy with a self-hosted backend
proxy server or API gateway that you control, ensuring sensitive data is not
exposed to third parties. Additionally, implement monitoring and failover
mechanisms to handle potential service outages and maintain reliability.
| console.log('토큰 발급 응답 전체:', response); | ||
| console.log('response.data:', response.data); | ||
| console.log('response.data.token:', response.data.token); | ||
| console.log('response.data.data:', response.data.data); | ||
| console.log('response.data.data?.token:', response.data.data?.token); | ||
|
|
||
| // 올바른 경로에서 토큰 찾기 (API 문서 기반) | ||
| const newToken = response.data.data?.token || response.data.token || response.data.accessToken; | ||
|
|
||
| console.log('토큰 발급 성공:', { | ||
| hasToken: !!newToken, | ||
| tokenLength: newToken?.length, | ||
| tokenStart: newToken?.substring(0, 20) + '...', | ||
| fullResponse: response.data, | ||
| foundToken: newToken | ||
| }); |
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.
🛠️ Refactor suggestion
프로덕션 환경을 위한 로깅 레벨 조정이 필요합니다.
현재 로깅이 너무 상세하여 프로덕션에서는 성능과 보안에 영향을 줄 수 있습니다. 환경 변수를 통한 조건부 로깅을 고려하세요.
- console.log('토큰 발급 응답 전체:', response);
- console.log('response.data:', response.data);
- console.log('response.data.token:', response.data.token);
- console.log('response.data.data:', response.data.data);
- console.log('response.data.data?.token:', response.data.data?.token);
+ if (import.meta.env.DEV) {
+ console.log('토큰 발급 응답:', {
+ hasData: !!response.data,
+ hasToken: !!response.data.data?.token || !!response.data.token,
+ });
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('토큰 발급 응답 전체:', response); | |
| console.log('response.data:', response.data); | |
| console.log('response.data.token:', response.data.token); | |
| console.log('response.data.data:', response.data.data); | |
| console.log('response.data.data?.token:', response.data.data?.token); | |
| // 올바른 경로에서 토큰 찾기 (API 문서 기반) | |
| const newToken = response.data.data?.token || response.data.token || response.data.accessToken; | |
| console.log('토큰 발급 성공:', { | |
| hasToken: !!newToken, | |
| tokenLength: newToken?.length, | |
| tokenStart: newToken?.substring(0, 20) + '...', | |
| fullResponse: response.data, | |
| foundToken: newToken | |
| }); | |
| if (import.meta.env.DEV) { | |
| console.log('토큰 발급 응답:', { | |
| hasData: !!response.data, | |
| hasToken: !!response.data.data?.token || !!response.data.token, | |
| }); | |
| } | |
| // 올바른 경로에서 토큰 찾기 (API 문서 기반) | |
| const newToken = response.data.data?.token || response.data.token || response.data.accessToken; | |
| console.log('토큰 발급 성공:', { | |
| hasToken: !!newToken, | |
| tokenLength: newToken?.length, | |
| tokenStart: newToken?.substring(0, 20) + '...', | |
| fullResponse: response.data, | |
| foundToken: newToken | |
| }); |
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axiosInstance.ts between lines 29 and 44, the
code contains detailed console.log statements that expose sensitive token
information and may impact performance in production. Modify the logging to only
output these details when running in a non-production environment by checking an
environment variable like NODE_ENV. Wrap all console.log calls in a conditional
that ensures they execute only if the environment is not production, thereby
preventing sensitive data exposure and reducing unnecessary logging overhead in
production.
| try { | ||
| const signupResponse = await axios.post( | ||
| `${import.meta.env.VITE_BASE_URL}/api/v1/auth/signup`, | ||
| { | ||
| email: '[email protected]', | ||
| remindDefault: '08:00', // LocalTime 형식 (HH:mm) | ||
| fcmToken: 'test-token-123', | ||
| } | ||
| ); | ||
|
|
||
| const signupToken = signupResponse.data.data?.token; | ||
| if (signupToken) { | ||
| console.log('signup 토큰 발급 성공:', { | ||
| hasToken: !!signupToken, | ||
| tokenLength: signupToken?.length, | ||
| tokenStart: signupToken?.substring(0, 20) + '...', | ||
| fullResponse: signupResponse.data | ||
| }); | ||
| localStorage.setItem('jwtToken', signupToken); | ||
| return signupToken; | ||
| } | ||
| } catch (signupError: any) { | ||
| console.error('signup도 실패:', { | ||
| status: signupError.response?.status, | ||
| message: signupError.response?.data?.message, | ||
| fullError: signupError.response?.data | ||
| }); | ||
| } | ||
|
|
||
| throw error; // 원래 에러 다시 던지기 |
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.
하드코딩된 인증 정보는 보안 위험입니다.
고정된 이메일과 테스트 토큰을 사용하는 것은 프로덕션 환경에서 심각한 보안 문제가 될 수 있습니다. 환경 변수나 설정 파일을 통해 관리하거나, 이 로직이 개발 환경에서만 동작하도록 제한해야 합니다.
+ const isDevelopment = import.meta.env.DEV;
+ if (!isDevelopment) {
+ throw new Error('Signup fallback is only available in development');
+ }
+
const signupResponse = await axios.post(
`${import.meta.env.VITE_BASE_URL}/api/v1/auth/signup`,
{
- email: '[email protected]',
- remindDefault: '08:00',
- fcmToken: 'test-token-123',
+ email: import.meta.env.VITE_TEST_EMAIL || '[email protected]',
+ remindDefault: import.meta.env.VITE_TEST_REMIND_TIME || '08:00',
+ fcmToken: import.meta.env.VITE_TEST_FCM_TOKEN || 'test-token-123',
}
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const signupResponse = await axios.post( | |
| `${import.meta.env.VITE_BASE_URL}/api/v1/auth/signup`, | |
| { | |
| email: '[email protected]', | |
| remindDefault: '08:00', // LocalTime 형식 (HH:mm) | |
| fcmToken: 'test-token-123', | |
| } | |
| ); | |
| const signupToken = signupResponse.data.data?.token; | |
| if (signupToken) { | |
| console.log('signup 토큰 발급 성공:', { | |
| hasToken: !!signupToken, | |
| tokenLength: signupToken?.length, | |
| tokenStart: signupToken?.substring(0, 20) + '...', | |
| fullResponse: signupResponse.data | |
| }); | |
| localStorage.setItem('jwtToken', signupToken); | |
| return signupToken; | |
| } | |
| } catch (signupError: any) { | |
| console.error('signup도 실패:', { | |
| status: signupError.response?.status, | |
| message: signupError.response?.data?.message, | |
| fullError: signupError.response?.data | |
| }); | |
| } | |
| throw error; // 원래 에러 다시 던지기 | |
| try { | |
| const isDevelopment = import.meta.env.DEV; | |
| if (!isDevelopment) { | |
| throw new Error('Signup fallback is only available in development'); | |
| } | |
| const signupResponse = await axios.post( | |
| `${import.meta.env.VITE_BASE_URL}/api/v1/auth/signup`, | |
| { | |
| email: import.meta.env.VITE_TEST_EMAIL || '[email protected]', | |
| remindDefault: import.meta.env.VITE_TEST_REMIND_TIME || '08:00', | |
| fcmToken: import.meta.env.VITE_TEST_FCM_TOKEN || 'test-token-123', | |
| } | |
| ); | |
| const signupToken = signupResponse.data.data?.token; | |
| if (signupToken) { | |
| console.log('signup 토큰 발급 성공:', { | |
| hasToken: !!signupToken, | |
| tokenLength: signupToken?.length, | |
| tokenStart: signupToken?.substring(0, 20) + '...', | |
| fullResponse: signupResponse.data | |
| }); | |
| localStorage.setItem('jwtToken', signupToken); | |
| return signupToken; | |
| } | |
| } catch (signupError: any) { | |
| console.error('signup도 실패:', { | |
| status: signupError.response?.status, | |
| message: signupError.response?.data?.message, | |
| fullError: signupError.response?.data | |
| }); | |
| } | |
| throw error; // 원래 에러 다시 던지기 |
🤖 Prompt for AI Agents
In apps/client/src/shared/apis/axiosInstance.ts between lines 60 and 89, the
signup request uses hardcoded email and token values, which pose a security risk
in production. Refactor the code to retrieve these values from environment
variables or configuration files instead of hardcoding them. Additionally,
ensure this signup logic only runs in development environments by adding
appropriate environment checks to prevent exposure in production.
| // 한국 시간(KST)으로 현재 시간 생성 | ||
| const now = new Date(); | ||
| const kstTime = new Date(now.getTime() + (9 * 60 * 60 * 1000)); // UTC+9 | ||
|
|
||
| // LocalDateTime 형식으로 변환 (YYYY-MM-DDTHH:mm:ss) | ||
| const year = kstTime.getUTCFullYear(); | ||
| const month = String(kstTime.getUTCMonth() + 1).padStart(2, '0'); | ||
| const day = String(kstTime.getUTCDate()).padStart(2, '0'); | ||
| const hours = String(kstTime.getUTCHours()).padStart(2, '0'); | ||
| const minutes = String(kstTime.getUTCMinutes()).padStart(2, '0'); | ||
| const seconds = String(kstTime.getUTCSeconds()).padStart(2, '0'); | ||
|
|
||
| const localDateTime = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; | ||
|
|
||
| return getAcornCountWithRemind(localDateTime); |
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.
🛠️ Refactor suggestion
중복된 시간 계산 로직을 유틸리티 함수로 추출하세요.
KST 시간 계산 로직이 두 곳에서 반복되고 있습니다. 유지보수성과 재사용성을 위해 별도 함수로 추출하는 것이 좋습니다.
// 파일 상단에 추가
const getKSTLocalDateTime = (): string => {
const now = new Date();
const kstTime = new Date(now.getTime() + (9 * 60 * 60 * 1000)); // UTC+9
const year = kstTime.getUTCFullYear();
const month = String(kstTime.getUTCMonth() + 1).padStart(2, '0');
const day = String(kstTime.getUTCDate()).padStart(2, '0');
const hours = String(kstTime.getUTCHours()).padStart(2, '0');
const minutes = String(kstTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(kstTime.getUTCSeconds()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
};
// 사용 예시
queryFn: () => getAcornCountWithRemind(getKSTLocalDateTime()),Also applies to: 132-146
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/hooks/useDashboard.ts around lines 56 to 70,
the KST time calculation logic is duplicated. Extract this logic into a reusable
utility function named getKSTLocalDateTime at the top of the file that returns
the formatted KST datetime string. Replace the existing inline KST calculation
with a call to this new function. Apply the same refactoring to the similar code
block at lines 132 to 146 to improve maintainability and reuse.
| if (isLoading) { | ||
| return ( | ||
| <section | ||
| className={SPACING_CLASSES.sectionMargin} | ||
| aria-label={UI_TEXT.dashboard.bookmarkSection} | ||
| > | ||
| <SectionTitle text={UI_TEXT.dashboard.bookmarkSection} /> | ||
| <div className="mt-[3rem]"> | ||
| <nav | ||
| className="mb-6 flex flex-wrap gap-[2rem]" | ||
| aria-label="북마크 카테고리 필터" | ||
| > | ||
| <BookmarkCategory | ||
| key="unread" | ||
| id="unread" | ||
| text="안 읽은 정보" | ||
| count={totalUnreadArticle} | ||
| isActive={activeCategory === '안 읽은 정보'} | ||
| onClick={() => onCategoryClick('안 읽은 정보')} | ||
| hideZeroCount={true} | ||
| /> | ||
| {categories.map((category) => ( | ||
| <BookmarkCategory | ||
| key={category.categoryId} | ||
| id={category.categoryId.toString()} | ||
| text={category.categoryName} | ||
| count={categoryTotalCounts[category.categoryId] || category.unreadCount} | ||
| isActive={activeCategory === category.categoryName} | ||
| onClick={() => onCategoryClick(category.categoryName, category.categoryId)} | ||
| hideZeroCount={true} | ||
| /> | ||
| ))} | ||
| </nav> | ||
| </div> | ||
| <div className="mt-[6rem] grid grid-cols-4 justify-center gap-[2.3rem]"> | ||
| {Array.from({ length: 8 }).map((_, index) => ( | ||
| <div | ||
| key={index} | ||
| className="h-[20rem] w-[30rem] animate-pulse rounded-[1.2rem] bg-gray-200" | ||
| /> | ||
| ))} | ||
| </div> | ||
| </section> | ||
| ); | ||
| } |
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.
🛠️ Refactor suggestion
카테고리 렌더링 로직 중복을 제거하세요.
로딩 상태와 일반 상태에서 카테고리 렌더링 로직이 중복되어 있습니다. 공통 컴포넌트로 추출하면 유지보수가 쉬워집니다.
// 카테고리 목록 렌더링을 별도 컴포넌트로 추출
const CategoryNav = ({
categories,
activeCategory,
totalUnreadArticle,
categoryTotalCounts,
onCategoryClick
}: CategoryNavProps) => (
<nav className="mb-6 flex flex-wrap gap-[2rem]" aria-label="북마크 카테고리 필터">
<BookmarkCategory
key="unread"
id="unread"
text="안 읽은 정보"
count={totalUnreadArticle}
isActive={activeCategory === '안 읽은 정보'}
onClick={() => onCategoryClick('안 읽은 정보')}
hideZeroCount={true}
/>
{categories.map((category) => (
<BookmarkCategory
key={category.categoryId}
id={category.categoryId.toString()}
text={category.categoryName}
count={categoryTotalCounts[category.categoryId] || category.unreadCount}
isActive={activeCategory === category.categoryName}
onClick={() => onCategoryClick(category.categoryName, category.categoryId)}
hideZeroCount={true}
/>
))}
</nav>
);Also applies to: 162-187
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx
around lines 110 to 154 and 162 to 187, the category rendering logic is
duplicated in both loading and normal states. To fix this, extract the category
navigation JSX into a separate reusable component (e.g., CategoryNav) that
accepts props like categories, activeCategory, totalUnreadArticle,
categoryTotalCounts, and onCategoryClick. Replace the duplicated JSX in both
places with this new component to improve maintainability and reduce code
duplication.
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 (6)
apps/client/src/shared/apis/axiosInstance.ts (3)
21-61: 로깅 레벨 조정과 보안 개선이 필요합니다.과거 리뷰에서 지적된 상세한 로깅 문제가 여전히 존재합니다. 토큰 정보를 포함한 민감한 데이터가 프로덕션 환경에서도 로그에 노출될 수 있습니다.
- console.log('토큰 발급 시작:', email); - console.log('토큰 발급 응답 전체:', response); - console.log('response.data:', response.data); - console.log('response.data.token:', response.data.token); - console.log('response.data.data:', response.data.data); - console.log('response.data.data?.token:', response.data.data?.token); - console.log('토큰 발급 성공:', { - hasToken: !!newToken, - tokenLength: newToken?.length, - tokenStart: newToken?.substring(0, 20) + '...', - fullResponse: response.data, - foundToken: newToken, - }); + if (import.meta.env.DEV) { + console.log('토큰 발급 시작:', email); + console.log('토큰 발급 성공:', { + hasToken: !!newToken, + tokenLength: newToken?.length, + }); + }
63-90: 하드코딩된 값들과 프로덕션 환경 고려사항이 있습니다.환경변수 사용이 개선되었지만 여전히 일부 값들이 하드코딩되어 있으며, 프로덕션 환경에서의 signup 폴백 로직에 대한 검토가 필요합니다.
const signupResponse = await axios.post( `${import.meta.env.VITE_BASE_URL}/api/v1/auth/signup`, { email: import.meta.env.VITE_DEFAULT_EMAIL || '[email protected]', - remindDefault: '08:00', // LocalTime 형식 (HH:mm) - fcmToken: 'test-token-123', + remindDefault: import.meta.env.VITE_DEFAULT_REMIND_TIME || '08:00', + fcmToken: import.meta.env.VITE_DEFAULT_FCM_TOKEN || 'test-token-123', } );또한 프로덕션 환경에서 signup 폴백이 적절한지 검토해보세요:
+ const isDevelopment = import.meta.env.DEV; + if (!isDevelopment) { + console.warn('Production 환경에서 signup 폴백 실행됨'); + }
146-148: 하드코딩된 이메일을 환경변수로 교체해야 합니다.에러 처리 로직은 잘 구현되었지만, 과거 리뷰에서 지적된 하드코딩 문제가 여전히 존재합니다.
- const newToken = await fetchToken('[email protected]'); + const newToken = await fetchToken(import.meta.env.VITE_DEFAULT_EMAIL || '[email protected]');apps/client/src/api/thumbnail.ts (2)
2-3: 외부 프록시 서비스 보안·안정성 검토 및 대체 방안 마련 필요
api.allorigins.win프록시 서비스 사용으로 인한 보안 및 안정성 리스크가 확인되었습니다:
- 데이터 유출·중개자 위험: 요청 URL과 응답 데이터가 제3자 서버에 노출
- 서비스 지속성 불확실: 무료 운영으로 갑작스런 중단 가능성
- 제3자 의존성: 프록시 서버 장애 시 신뢰할 수 없는 응답 수신 위험
자체 백엔드 프록시 서버 구축 또는 API 게이트웨이를 통한 대체 방안을 검토해주세요.
47-49: 반환 타입 일관성 개선 필요관련 구현체(
apps/client/src/pages/dashboard/apis/thumbnail.ts)와 비교했을 때 반환 타입이 일치하지 않습니다. 다른 구현은Promise<string>을 반환하여 항상 기본 이미지를 보장하는 반면, 현재 구현은Promise<string | null>을 반환합니다.사용처에서의 null 처리 복잡성을 줄이기 위해 기본 이미지를 항상 반환하는 것을 고려해보세요:
-export const generateThumbnail = async (url: string): Promise<string | null> => { +export const generateThumbnail = async (url: string): Promise<string> => { try { // ... 기존 로직 - return null; + return '/default-thumbnail.png'; } catch (error) { console.error('썸네일 생성 실패:', error); - return null; + return '/default-thumbnail.png'; } };apps/client/src/pages/dashboard/dashboard.tsx (1)
91-96: 사용자 입력 검증 강화 필요
prompt로 받은 입력값에 대한 검증이 부족합니다. 음수나 0과 같은 잘못된 값에 대한 처리가 필요합니다.const categoryId = prompt('카테고리 ID를 입력하세요 (예: 1, 2, 3...)'); -if (categoryId && !isNaN(Number(categoryId))) { +if (categoryId && !isNaN(Number(categoryId)) && Number(categoryId) > 0) { createTestArticles(Number(categoryId)); } else if (categoryId) { - alert('올바른 카테고리 ID를 입력해주세요.'); + alert('올바른 카테고리 ID를 입력해주세요. (1 이상의 숫자)'); }
🧹 Nitpick comments (2)
apps/client/src/shared/apis/axiosInstance.ts (1)
102-126: 토큰 검증 로직이 개선되었으나 로깅 최적화가 필요합니다.토큰 유효성 검사와 환경변수 사용이 잘 개선되었습니다. 다만 상세한 로깅은 프로덕션에서 성능에 영향을 줄 수 있습니다.
- console.log( - 'API 요청 토큰 사용:', - config.url, - token ? '토큰 있음' : '토큰 없음', - token?.substring(0, 20) + '...' - ); + if (import.meta.env.DEV) { + console.log('API 요청:', config.url, token ? '토큰 있음' : '토큰 없음'); + }apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx (1)
10-47: 타이머 로직 구현 우수하나 추가 개선 고려내부적으로 구현된 countdown 로직이 잘 작성되었습니다:
- 메모리 누수 방지를 위한 적절한 cleanup
- 의존성 배열을 통한 적절한 useEffect 재실행
- 정확한 시간 계산 로직
다음 개선사항을 고려해보세요:
useEffect(() => { const updateTimeLeft = () => { const now = new Date(); let targetDateTime: Date; if (targetTime) { targetDateTime = new Date(targetTime); + // 유효하지 않은 날짜 검증 + if (isNaN(targetDateTime.getTime())) { + setTimeLeft('00:00:00'); + return; + } } else { // 기본값: 다음날 오전 8시 const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(8, 0, 0, 0); targetDateTime = tomorrow; }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
apps/client/public/firebase_sdk/firebase-app-compat.js(1 hunks)apps/client/src/api/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/components/sections/BannerSection.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx(1 hunks)apps/client/src/pages/dashboard/dashboard.tsx(1 hunks)apps/client/src/pages/index.ts(1 hunks)apps/client/src/shared/apis/axiosInstance.ts(1 hunks)apps/client/src/shared/apis/index.ts(1 hunks)apps/client/src/shared/utils/errorUtils.ts(1 hunks)apps/client/src/shared/utils/index.ts(1 hunks)apps/client/tsconfig.json(1 hunks)
✅ Files skipped from review due to trivial changes (3)
- apps/client/src/pages/index.ts
- apps/client/tsconfig.json
- apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/client/src/shared/utils/index.ts
- apps/client/src/shared/utils/errorUtils.ts
- apps/client/src/shared/apis/index.ts
- apps/client/src/pages/dashboard/components/sections/BannerSection.tsx
🧰 Additional context used
🧠 Learnings (4)
apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/api/thumbnail.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/dashboard.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
apps/client/src/shared/apis/axiosInstance.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🧬 Code Graph Analysis (1)
apps/client/src/api/thumbnail.ts (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
generateThumbnail(3-28)
🪛 Biome (1.9.4)
apps/client/public/firebase_sdk/firebase-app-compat.js
[error] 10-10: Redundant use strict directive.
The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.
(lint/suspicious/noRedundantUseStrict)
[error] 154-154: e is assigned to itself.
This is where is assigned.
(lint/correctness/noSelfAssign)
[error] 163-163: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 175-175: Do not access Object.prototype method 'hasOwnProperty' from target object.
It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.
(lint/suspicious/noPrototypeBuiltins)
[error] 258-259: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 716-716: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 720-720: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 724-724: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 728-728: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 732-732: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 979-979: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 1109-1109: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 271-271: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 271-271: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 273-273: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 278-278: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 278-278: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 562-562: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 793-793: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 794-794: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 795-795: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 907-907: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1277-1277: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1277-1277: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1510-1526: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
🔇 Additional comments (7)
apps/client/public/firebase_sdk/firebase-app-compat.js (1)
1-1596: 외부 라이브러리 파일이므로 정적 분석 경고를 무시하세요.이 파일은 Firebase App 호환성 SDK의 minified 버전으로, 외부 라이브러리입니다. 정적 분석 도구가 지적한 문제들(use strict 중복, 변수 재선언, 호이스팅 등)은 minified 코드의 특성상 정상적인 것입니다. AI 요약에 따르면 모든 핵심 기능이 보존되고 API 서명에 변경이 없다고 확인되었습니다.
apps/client/src/shared/apis/axiosInstance.ts (1)
10-12: 토큰 발급 중복 방지 로직이 잘 구현되었습니다.동시 API 요청 시 토큰 발급이 중복으로 일어나는 것을 방지하는 플래그와 Promise 캐싱 방식이 효율적입니다.
apps/client/src/pages/dashboard/dashboard.tsx (3)
8-9: API 타입 import 경로 개선 완료타입 정의를 별도 폴더로 분리한 리팩토링이 잘 이루어졌습니다. 모듈화와 유지보수성이 향상되었습니다.
79-104: 개발 환경 전용 기능 구현 우수개발 환경에서만 노출되는 테스트 데이터 생성 버튼들이 잘 구현되었습니다. 프로덕션 빌드에서 제외되도록 환경 변수를 활용한 점이 좋습니다.
35-37: isInitialized 상태 선언 및 초기화 목적 명확화 필요
dashboard.tsx에서setIsInitialized의 정의 위치와 초기화 로직의 의도를 확인할 수 없습니다. 아래 사항을 검증해 주세요:
isInitialized/setIsInitialized가 선언된 파일과 위치(useState, 컨텍스트, 커스텀 훅 등)- 해당 초기화 로직이 어떤 부작용을 방지하거나 어떤 작업을 제어하는지
apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx (2)
1-5: 컴포넌트 인터페이스 설계 우수
targetTimeprop을 optional로 설정하고 기본값을 제공하는 설계가 좋습니다. 유연성과 사용성을 모두 고려한 인터페이스입니다.
52-52: 메모화 최적화 적절
memo를 사용한 컴포넌트 최적화가 적절히 적용되었습니다. 1초마다 업데이트되는 컴포넌트의 불필요한 리렌더링을 방지합니다.
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 (3)
apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx (1)
20-21: URL 유효성 검증 로직 추가 필요이전 리뷰에서 제기된 URL 검증 로직이 아직 구현되지 않았습니다. 유효하지 않은 URL에 대해 불필요한 API 호출을 방지하기 위해 URL 검증을 추가해주세요.
const generateThumbnailFromUrl = useCallback(async (url: string) => { - if (!url) return; + if (!url || !url.trim()) return; + + // URL 유효성 검증 + try { + new URL(url); + } catch { + setState(prev => ({ ...prev, hasError: true, isLoading: false })); + return; + } setState(prev => ({ ...prev, isLoading: true, hasError: false }));apps/client/src/api/thumbnail.ts (2)
1-2: 외부 프록시 서비스 보안 및 안정성 검토 필요이전 리뷰에서 제기된
api.allorigins.win서비스 사용의 보안 문제가 아직 해결되지 않았습니다. 다음 리스크들이 여전히 존재합니다:
- 데이터 유출 및 중개자 위험
- 서비스 지속성 불확실성
- 제3자 의존성 문제
자체 백엔드 프록시 서버 구축 또는 보안 정책에 부합하는 대체 방안을 검토해주세요.
76-142: 중복 구현 제거 및 반환 타입 일관성 개선 필요이전 리뷰에서 제기된 두 가지 문제가 여전히 존재합니다:
apps/client/src/pages/dashboard/apis/thumbnail.ts와의 중복 구현Promise<string | null>반환 타입으로 인한 복잡성다음과 같이 개선해주세요:
-export const generateThumbnail = async (url: string): Promise<string | null> => { +export const generateThumbnail = async (url: string): Promise<string> => { try { // ... 기존 로직 - return null; + return '/default-thumbnail.png'; } catch (error) { console.error('썸네일 생성 실패:', error); - return null; + return '/default-thumbnail.png'; } };그리고 중복 구현을 제거하기 위해 다음 중 하나를 선택해주세요:
- 현재 파일을 공통 모듈로 사용하고 다른 구현 제거
- 대시보드 전용 구현을 유지하고 현재 파일 제거
🧹 Nitpick comments (2)
apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx (1)
92-99: 이미지 로드 에러 핸들링 개선이미지 로드 실패 시 state를 더 정확하게 관리하고, 로깅을 개선할 수 있습니다.
onError={() => { - console.warn('썸네일 이미지 로드 실패:', state.url); - setState(prev => ({ ...prev, hasError: true, url: null })); + console.error('썸네일 이미지 로드 실패:', { url: state.url, originalUrl: url }); + setState(prev => ({ ...prev, hasError: true })); }} onLoad={() => { - console.log('썸네일 로드 성공:', state.url); + console.log('썸네일 로드 성공:', { thumbnailUrl: state.url, originalUrl: url }); }}apps/client/src/api/thumbnail.ts (1)
59-74: safeFetch 함수 개선사항타임아웃 및 에러 처리가 잘 구현되어 있습니다. 다만 몇 가지 개선점이 있습니다:
const safeFetch = async (url: string, timeout = 5000): Promise<Response> => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal, - headers: { 'Content-Type': 'text/html' }, + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'User-Agent': 'Mozilla/5.0 (compatible; ThumbnailBot/1.0)' + }, }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); + if (error.name === 'AbortError') { + throw new Error(`요청 타임아웃: ${timeout}ms`); + } throw error; } };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/api/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx (3)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/api/thumbnail.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🧬 Code Graph Analysis (1)
apps/client/src/api/thumbnail.ts (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
generateThumbnail(3-28)
🔇 Additional comments (1)
apps/client/src/api/thumbnail.ts (1)
144-154: 캐시 관리 함수들 구현 우수캐시 정리 및 통계 함수들이 잘 구현되어 있습니다. 디버깅과 메모리 관리에 유용할 것 같습니다.
| } catch (error) { | ||
| console.error('썸네일 생성 실패:', { url, error }); | ||
|
|
||
| setState(prev => ({ | ||
| ...prev, | ||
| hasError: false, | ||
| isLoading: false | ||
| })); | ||
| } |
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.
에러 처리 로직 수정 필요
catch 블록에서 hasError: false로 설정하는 것은 논리적으로 맞지 않습니다. 에러가 발생했을 때는 hasError: true로 설정해야 합니다.
} catch (error) {
console.error('썸네일 생성 실패:', { url, error });
setState(prev => ({
...prev,
- hasError: false,
+ hasError: true,
isLoading: false
}));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (error) { | |
| console.error('썸네일 생성 실패:', { url, error }); | |
| setState(prev => ({ | |
| ...prev, | |
| hasError: false, | |
| isLoading: false | |
| })); | |
| } | |
| } catch (error) { | |
| console.error('썸네일 생성 실패:', { url, error }); | |
| setState(prev => ({ | |
| ...prev, | |
| hasError: true, | |
| isLoading: false | |
| })); | |
| } |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx between
lines 48 and 56, the catch block incorrectly sets hasError to false when an
error occurs. Update the setState call to set hasError to true to correctly
reflect that an error has happened, while keeping isLoading set to false.
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: 3
♻️ Duplicate comments (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
1-2: 환경 변수 설정 개선 필요프록시 서비스 URL의 기본값이 기존 리뷰 댓글과 다릅니다. 이전 댓글에서
api.microlink.io를 사용한다고 했으나, 여기서는api.allorigins.win/raw를 기본값으로 사용하고 있습니다.-const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL || 'https://api.allorigins.win/raw'; +const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL || 'https://api.microlink.io';또한 FAVICON_SERVICE의 기본값도 제공하는 것이 좋겠습니다:
-const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL; +const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL || 'https://www.google.com/s2/favicons';
🧹 Nitpick comments (4)
apps/client/src/pages/dashboard/apis/thumbnail.ts (4)
7-10: 메타 태그 패턴 확장 고려현재 Open Graph와 Twitter 이미지만 추출하고 있습니다. 더 많은 메타 태그를 지원하면 썸네일 추출 성공률을 높일 수 있습니다.
const META_PATTERNS = { ogImage: /<meta[^>]*property="og:image"[^>]*content="([^"]*)"[^>]*>/i, twitterImage: /<meta[^>]*name="twitter:image"[^>]*content="([^"]*)"[^>]*>/i, + appleTouchIcon: /<link[^>]*rel="apple-touch-icon"[^>]*href="([^"]*)"[^>]*>/i, + icon: /<link[^>]*rel="icon"[^>]*href="([^"]*)"[^>]*>/i, } as const;
26-38: 메타 이미지 추출 로직 개선 제안현재 로직은 좋지만, 빈 문자열이나 유효하지 않은 URL을 필터링하는 로직을 추가하면 더 안정적입니다.
const extractMetaImages = (html: string, baseUrl: string): string | null => { const ogMatch = html.match(META_PATTERNS.ogImage); - if (ogMatch?.[1]) { + if (ogMatch?.[1] && ogMatch[1].trim()) { return normalizeImageUrl(ogMatch[1], baseUrl); } const twitterMatch = html.match(META_PATTERNS.twitterImage); - if (twitterMatch?.[1]) { + if (twitterMatch?.[1] && twitterMatch[1].trim()) { return normalizeImageUrl(twitterMatch[1], baseUrl); } return null; };
95-122: 프록시 서비스 요청 로직 개선 필요코드 주석과 실제 구현 순서가 다릅니다. 주석에는 "2. 프록시 서비스를 통한 메타데이터 추출 시도"라고 되어 있지만, 실제로는 첫 번째 단계입니다.
- // 2. 프록시 서비스를 통한 메타데이터 추출 시도 + // 1. 프록시 서비스를 통한 메타데이터 추출 시도 try { const proxyUrl = `${PROXY_SERVICE}?url=${encodeURIComponent(url)}`; console.log('프록시 서비스 요청:', url);
123-133: 폴백 로직 주석 수정 필요주석 번호가 일관되지 않습니다.
- // 3. 폴백: Favicon 사용 + // 2. 폴백: Favicon 사용 if (faviconUrl) { console.log('Favicon 사용:', domain); setCachedThumbnail(url, faviconUrl); return faviconUrl; } - // 4. 최종 폴백: null 반환 + // 3. 최종 폴백: null 반환 console.log('썸네일 생성 불가:', domain); return null;
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/dashboard/apis/index.ts(1 hunks)apps/client/src/pages/dashboard/apis/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/client/src/pages/dashboard/apis/index.ts
- apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx
🧰 Additional context used
🧠 Learnings (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
🔇 Additional comments (6)
apps/client/src/pages/dashboard/apis/thumbnail.ts (6)
12-18: 도메인 추출 함수 승인URL에서 도메인을 안전하게 추출하는 로직이 잘 구현되어 있습니다. try-catch를 사용한 에러 처리도 적절합니다.
20-24: URL 정규화 함수 승인상대 URL을 절대 URL로 변환하는 로직이 올바르게 구현되어 있습니다.
40-45: 파비콘 URL 생성 함수 승인환경 변수를 체크하고 적절한 URL을 생성하는 로직이 잘 구현되어 있습니다.
62-77: 안전한 fetch 함수 승인타임아웃과 AbortController를 사용한 네트워크 요청 처리가 잘 구현되어 있습니다. 리소스 정리도 적절하게 처리되고 있습니다.
79-94: 썸네일 생성 함수 시작 부분 승인URL 검증과 캐시 확인 로직이 올바르게 구현되어 있습니다.
156-166: 캐시 관리 유틸리티 함수 승인캐시 정리와 통계 조회 함수가 잘 구현되어 있습니다. 디버깅과 모니터링에 유용할 것입니다.
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | ||
| const CACHE_DURATION = 24 * 60 * 60 * 1000; |
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.
🛠️ Refactor suggestion
메모리 누수 방지를 위한 캐시 크기 제한 추가
현재 캐시는 무제한으로 성장할 수 있어 메모리 누수 위험이 있습니다. 캐시 크기 제한을 추가하는 것이 좋겠습니다.
const thumbnailCache = new Map<string, { url: string; timestamp: number }>();
-const CACHE_DURATION = 24 * 60 * 60 * 1000;
+const CACHE_DURATION = 24 * 60 * 60 * 1000;
+const MAX_CACHE_SIZE = 1000;그리고 캐시 설정 시 크기 제한 로직을 추가해야 합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | |
| const CACHE_DURATION = 24 * 60 * 60 * 1000; | |
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | |
| const CACHE_DURATION = 24 * 60 * 60 * 1000; | |
| const MAX_CACHE_SIZE = 1000; |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/apis/thumbnail.ts around lines 4 to 5, the
thumbnailCache Map currently has no size limit, which risks unbounded memory
growth. To fix this, implement a maximum cache size and add logic to remove the
oldest or least recently used entries when the cache exceeds this limit,
ensuring the cache size stays within the defined bounds.
| const getCachedThumbnail = (url: string): string | null => { | ||
| const cached = thumbnailCache.get(url); | ||
| if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { | ||
| return cached.url; | ||
| } | ||
| return null; | ||
| }; | ||
|
|
||
| const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { | ||
| thumbnailCache.set(url, { | ||
| url: thumbnailUrl, | ||
| timestamp: Date.now(), | ||
| }); | ||
| }; |
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.
🛠️ Refactor suggestion
캐시 관리 함수에 크기 제한 추가 필요
캐시 설정 시 앞서 제안한 크기 제한 로직이 필요합니다.
const setCachedThumbnail = (url: string, thumbnailUrl: string): void => {
+ // 캐시 크기 제한 확인
+ if (thumbnailCache.size >= MAX_CACHE_SIZE) {
+ const oldestKey = thumbnailCache.keys().next().value;
+ thumbnailCache.delete(oldestKey);
+ }
+
thumbnailCache.set(url, {
url: thumbnailUrl,
timestamp: Date.now(),
});
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getCachedThumbnail = (url: string): string | null => { | |
| const cached = thumbnailCache.get(url); | |
| if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { | |
| return cached.url; | |
| } | |
| return null; | |
| }; | |
| const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { | |
| thumbnailCache.set(url, { | |
| url: thumbnailUrl, | |
| timestamp: Date.now(), | |
| }); | |
| }; | |
| const getCachedThumbnail = (url: string): string | null => { | |
| const cached = thumbnailCache.get(url); | |
| if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { | |
| return cached.url; | |
| } | |
| return null; | |
| }; | |
| const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { | |
| // 캐시 크기 제한 확인 | |
| if (thumbnailCache.size >= MAX_CACHE_SIZE) { | |
| const oldestKey = thumbnailCache.keys().next().value; | |
| thumbnailCache.delete(oldestKey); | |
| } | |
| thumbnailCache.set(url, { | |
| url: thumbnailUrl, | |
| timestamp: Date.now(), | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/apis/thumbnail.ts between lines 47 and 60,
the cache management functions lack a size limit for the thumbnailCache. To fix
this, implement logic in setCachedThumbnail to check the cache size before
adding a new entry and remove the oldest entries if the cache exceeds the
defined maximum size, ensuring the cache does not grow indefinitely.
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: 4
♻️ Duplicate comments (5)
apps/client/src/pages/dashboard/apis/thumbnail.ts (3)
1-2: 프록시 서비스 불일치 문제다른 파일(
apps/client/src/api/thumbnail.ts)과 다른 프록시 서비스를 사용하는 문제가 여전히 해결되지 않았습니다.
4-5: 캐시 크기 제한 필요메모리 누수 방지를 위한 캐시 크기 제한이 여전히 구현되지 않았습니다.
55-60: 캐시 설정 시 크기 제한 로직 추가 필요캐시에 항목을 추가할 때 최대 크기를 확인하는 로직이 없습니다.
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (1)
36-42: URL 안전성 검증이 필요합니다.이전 리뷰에서 지적된
window.open사용 시 URL 안전성 검증이 여전히 누락되어 있습니다.다음과 같이 URL 검증을 추가하세요:
const handleCardClick = (article: Article) => { onArticleRead?.(article.articleId); if (article.url) { - window.open(article.url, '_blank', 'noopener,noreferrer'); + try { + const url = new URL(article.url); + if (url.protocol === 'http:' || url.protocol === 'https:') { + window.open(article.url, '_blank', 'noopener,noreferrer'); + } + } catch (error) { + console.error('유효하지 않은 URL:', article.url); + } } };apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (1)
103-147: 카테고리 렌더링 로직 중복이 여전히 존재합니다.이전 리뷰에서 지적된 카테고리 렌더링 중복 문제가 해결되지 않았습니다.
로딩 상태와 일반 상태에서 카테고리 렌더링 로직이 중복되어 있습니다. 공통 컴포넌트로 추출하세요:
const CategoryNav = ({ categories, activeCategory, totalUnreadArticle, categoryTotalCounts, onCategoryClick }: CategoryNavProps) => ( <nav className="mb-6 flex flex-wrap gap-[2rem]" aria-label="북마크 카테고리 필터"> <BookmarkCategory key="unread" id="unread" text="안 읽은 정보" count={totalUnreadArticle} isActive={activeCategory === '안 읽은 정보'} onClick={() => onCategoryClick('안 읽은 정보')} hideZeroCount={true} /> {categories.map((category) => ( <BookmarkCategory key={category.categoryId} id={category.categoryId.toString()} text={category.categoryName} count={categoryTotalCounts[category.categoryId] || category.unreadCount} isActive={activeCategory === category.categoryName} onClick={() => onCategoryClick(category.categoryName, category.categoryId)} hideZeroCount={true} /> ))} </nav> );
🧹 Nitpick comments (6)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
108-111: 불필요한 continue 문 제거catch 블록의 마지막에 있는
continue문은 불필요합니다.} catch { // 다음 크기 시도 - continue; }apps/client/src/pages/dashboard/hooks/useArticles.ts (2)
48-50: 성능 최적화: getCurrentKSTDateTime 호출 위치 개선
getCurrentKSTDateTime()가 매 렌더링마다 호출되어 불필요한 재실행을 유발할 수 있습니다. 쿼리 키에 포함시키거나 useMemo로 메모이제이션하는 것을 고려해보세요.+ const currentDateTime = useMemo(() => getCurrentKSTDateTime(), []); const { data: dailyReminderData, isLoading: isLoadingDailyReminder, refetch: refetchDailyReminder, error: dailyReminderError, } = useQuery({ - queryKey: ['dailyReminderArticles', 0, dailyReminderPageSize], + queryKey: ['dailyReminderArticles', currentDateTime, 0, dailyReminderPageSize], ...createQueryConfig( - () => getDailyReminderArticles(getCurrentKSTDateTime(), 0, dailyReminderPageSize), + () => getDailyReminderArticles(currentDateTime, 0, dailyReminderPageSize),
85-85: 일관된 로깅 사용 권장성공 메시지도
logApiSuccess를 사용하여 일관성을 유지하는 것이 좋습니다.- console.log('아티클 읽음 상태 업데이트 성공, 쿼리 무효화 시작'); + logApiSuccess('아티클 읽음 상태 업데이트', '쿼리 무효화 시작');apps/client/src/pages/dashboard/hooks/useCategories.ts (1)
32-46: API 호출 최적화 제안각 카테고리별로 개별 API 호출을 하는 대신, 배치 API나 카테고리 정보에 총 개수를 포함하는 방식을 고려해보세요. 현재 방식은 카테고리가 많을 경우 성능 문제를 일으킬 수 있습니다.
백엔드에 카테고리별 총 개수를 한 번에 가져올 수 있는 엔드포인트를 추가하거나, 기존 카테고리 목록 API에 총 개수를 포함시키는 것을 고려해보세요.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
26-39: Props 인터페이스가 복잡해졌습니다.여러 개의 count 관련 props (
totalUnreadArticle,totalArticleCount,categoryTotalCounts)가 추가되어 인터페이스가 복잡해졌습니다.count 관련 props를 하나의 객체로 통합하는 것을 고려해보세요:
interface CountData { totalUnreadArticle: number; totalArticleCount?: number; categoryTotalCounts: Record<number, number>; } interface BookmarkSectionProps { // 기존 props... counts: CountData; }
210-222: 타입 가드와 조건부 props 매핑이 복잡합니다.
Article과BookmarkCardProps타입을 구분하는 로직이 복잡하고 읽기 어렵습니다.타입 가드 함수를 분리하여 가독성을 향상시키세요:
const isArticle = (item: BookmarkCardProps | Article): item is Article => { return 'articleId' in item; }; const getCardProps = (item: BookmarkCardProps | Article) => { if (isArticle(item)) { return { id: item.articleId.toString(), title: item.url, memo: item.memo, isRead: item.isRead, categoryId: '1', url: item.url, handlePopUpOpen, }; } return item; }; // 사용 <BookmarkCard {...getCardProps(card)} handlePopUpOpen={handlePopUpOpen} />
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
apps/client/src/pages/dashboard/apis/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx(6 hunks)apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx(2 hunks)apps/client/src/pages/dashboard/components/sections/TestSection.tsx(0 hunks)apps/client/src/pages/dashboard/components/sections/index.ts(0 hunks)apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx(5 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx(1 hunks)apps/client/src/pages/dashboard/dashboard.tsx(1 hunks)apps/client/src/pages/dashboard/hooks/useAcornCount.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useArticles.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useCategories.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useDashboard.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useTestData.ts(1 hunks)apps/client/src/pages/dashboard/types/components.ts(1 hunks)apps/client/src/pages/dashboard/utils/dateUtils.ts(1 hunks)apps/client/src/pages/dashboard/utils/queryConfig.ts(1 hunks)
💤 Files with no reviewable changes (2)
- apps/client/src/pages/dashboard/components/sections/index.ts
- apps/client/src/pages/dashboard/components/sections/TestSection.tsx
✅ Files skipped from review due to trivial changes (2)
- apps/client/src/pages/dashboard/utils/dateUtils.ts
- apps/client/src/pages/dashboard/types/components.ts
🚧 Files skipped from review as they are similar to previous changes (8)
- apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx
- apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx
- apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx
- apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx
- apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx
- apps/client/src/pages/dashboard/hooks/useDashboard.ts
- apps/client/src/pages/dashboard/dashboard.tsx
- apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx
🧰 Additional context used
🧠 Learnings (3)
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/apis/thumbnail.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🧬 Code Graph Analysis (4)
apps/client/src/pages/dashboard/hooks/useTestData.ts (3)
apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
createTestCategories(9-20)apps/client/src/shared/utils/errorUtils.ts (2)
logApiSuccess(23-28)logApiError(6-21)apps/client/src/pages/dashboard/apis/articleApi.ts (1)
createTestArticles(94-107)
apps/client/src/pages/dashboard/hooks/useCategories.ts (4)
apps/client/src/pages/dashboard/utils/queryConfig.ts (2)
createQueryConfig(18-31)ERROR_STATUS(36-42)apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
getDashboardCategories(22-33)apps/client/src/pages/dashboard/apis/articleApi.ts (1)
getArticlesByCategory(29-44)apps/client/src/shared/utils/errorUtils.ts (1)
logApiError(6-21)
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (1)
apps/client/src/pages/dashboard/types/api.ts (1)
Article(1-8)
apps/client/src/pages/dashboard/hooks/useAcornCount.ts (5)
apps/client/src/pages/dashboard/constants/bannerImages.ts (1)
BannerStage(21-21)apps/client/src/pages/dashboard/utils/queryConfig.ts (2)
createQueryConfig(18-31)ERROR_STATUS(36-42)apps/client/src/pages/dashboard/apis/userApi.ts (1)
getAcornCountWithRemind(5-20)apps/client/src/pages/dashboard/utils/dateUtils.ts (1)
getCurrentKSTDateTime(5-17)apps/client/src/shared/utils/errorUtils.ts (1)
logApiError(6-21)
🪛 Biome (1.9.4)
apps/client/src/pages/dashboard/apis/thumbnail.ts
[error] 115-115: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
🔇 Additional comments (11)
apps/client/src/pages/dashboard/hooks/useTestData.ts (1)
1-38: 적절한 테스트 데이터 훅 구현React Query를 사용한 테스트 데이터 생성 뮤테이션이 잘 구현되었습니다. 성공 시 관련 쿼리들을 적절히 무효화하고 있으며, 에러 처리도 잘 되어 있습니다.
apps/client/src/pages/dashboard/hooks/useCategories.ts (1)
68-75: 적절한 카테고리 선택 핸들러 구현카테고리 선택 로직이 잘 구현되었습니다. 카테고리 선택 시 페이지 크기를 초기화하는 것도 좋은 UX입니다.
apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx (5)
6-6: 타입 import 경로 변경이 올바르게 적용되었습니다.API 타입 정의가
types/api.ts로 이동된 것에 맞춰 import 경로가 올바르게 업데이트되었습니다.
8-13: Props 인터페이스가 API 연동에 맞게 잘 설계되었습니다.동적 데이터 처리를 위한
articles,onArticleRead,isLoadingprops가 적절히 추가되었습니다.
44-63: 로딩 상태 UI가 잘 구현되었습니다.스켈레톤 UI로 로딩 상태를 표시하는 방식이 사용자 경험에 좋습니다.
65-67: 빈 상태 처리가 적절합니다.articles가 없을 때 null을 반환하는 것이 올바른 처리입니다.
78-89: 동적 데이터 렌더링이 올바르게 구현되었습니다.Article 객체의 필드들을 적절히 매핑하여 카드 컴포넌트에 전달하고 있습니다.
apps/client/src/pages/dashboard/utils/queryConfig.ts (3)
6-13: 재시도 로직이 안전하게 구현되었습니다.특정 HTTP 상태 코드에 대해 재시도를 방지하는 로직이 올바릅니다. 에러 객체 접근 시 optional chaining을 사용한 것도 좋습니다.
18-31: 쿼리 설정 팩토리 함수가 잘 설계되었습니다.기본값 설정과 유연한 옵션 제공이 균형있게 구현되어 있습니다.
staleTime2분,retryDelay1초는 적절한 값입니다.
36-42: 에러 상태 상수가 체계적으로 정의되었습니다.일반적인 HTTP 에러 상태 코드들이 의미있게 그룹핑되어 있어 재사용성이 높습니다.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (1)
11-11: 타입 import가 올바르게 업데이트되었습니다.API 타입 정의 경로 변경에 맞춰 import가 적절히 수정되었습니다.
| try { | ||
| const response = await fetch(url, { | ||
| signal: controller.signal, | ||
| headers: { 'Content-Type': 'text/html' }, |
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.
잘못된 요청 헤더 사용
GET 요청에서 Content-Type 헤더를 사용하는 것은 적절하지 않습니다. HTML을 요청할 때는 Accept 헤더를 사용해야 합니다.
- headers: { 'Content-Type': 'text/html' },
+ headers: { 'Accept': 'text/html' },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| headers: { 'Content-Type': 'text/html' }, | |
| const response = await fetch(url, { | |
| ...options, | |
| - headers: { 'Content-Type': 'text/html' }, | |
| + headers: { 'Accept': 'text/html' }, | |
| signal: controller.signal, | |
| }); |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/apis/thumbnail.ts at line 69, replace the
'Content-Type' header with the 'Accept' header in the GET request. Change the
header key from 'Content-Type' to 'Accept' and keep the value as 'text/html' to
correctly indicate the expected response format.
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.
해당 headers가 필요한 것인가요??
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.
@constantly-dev 좋은 질문입니다!
해당 headers가 반드시 필요한 것은 아닙니다.
헤더의 역할:
Accept: text/html- 서버에게 "HTML 형식의 응답을 원합니다"라고 알려주는 역할- 대부분의 웹 서버는 이 헤더가 없어도 기본적으로 HTML을 반환합니다
제거해도 되는 이유:
- 프록시 서비스와 대부분의 웹사이트는 헤더 없이도 HTML을 잘 반환합니다
- 불필요한 헤더를 제거하면 요청이 더 가볍고 간단해집니다
- 일부 서버에서는 예상치 못한 헤더로 인해 문제가 발생할 수도 있습니다
권장사항:
const response = await fetch(url, {
signal: controller.signal,
// headers 완전히 제거
});헤더를 제거하고 테스트해보시는 것을 추천드립니다. 대부분의 경우 문제없이 동작할 것입니다.
🧠 Learnings used
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
| setIsLoadingAcornCount(true); | ||
| const result = await refetchAcorn(); | ||
| if (result.data?.data?.acornCount !== undefined) { | ||
| const newAcornCount = result.data.data.acornCount as BannerStage; |
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.
안전하지 않은 타입 캐스팅
acornCount를 BannerStage로 캐스팅하는 것은 타입 안전성을 해칩니다. API 응답의 acornCount는 숫자인 것으로 보입니다.
- const newAcornCount = result.data.data.acornCount as BannerStage;
+ const newAcornCount = result.data.data.acornCount;- const newAcornCount = acornData.data.acornCount as BannerStage;
+ const newAcornCount = acornData.data.acornCount;Also applies to: 61-61
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/hooks/useAcornCount.ts at lines 35 and 61,
the code unsafely casts acornCount from the API response to BannerStage, which
is incorrect since acornCount is a number. To fix this, remove the type
assertion to BannerStage and instead properly handle acornCount as a number,
converting or mapping it to BannerStage only through safe, explicit logic if
needed.
| import { logApiError } from '@shared/utils'; | ||
|
|
||
| export const useAcornCount = (isInitialized: boolean) => { | ||
| const [acornCount, setAcornCount] = useState<BannerStage>(0); |
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.
타입 불일치 문제
BannerStage는 문자열 타입인데 숫자 0으로 초기화하고 있습니다. 타입 안전성을 위해 수정이 필요합니다.
- const [acornCount, setAcornCount] = useState<BannerStage>(0);
+ const [acornCount, setAcornCount] = useState<number>(0);또는 BannerStage 타입에 맞는 초기값을 사용하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [acornCount, setAcornCount] = useState<BannerStage>(0); | |
| const [acornCount, setAcornCount] = useState<number>(0); |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/hooks/useAcornCount.ts at line 10, the state
variable acornCount is initialized with the number 0, but its type BannerStage
is a string type. To fix this type mismatch, initialize acornCount with a string
value that matches the BannerStage type or update the initial value to a valid
BannerStage string literal instead of 0.
| const handleCardClick = (bookmark: BookmarkCardProps | Article) => { | ||
| if ('articleId' in bookmark && onArticleRead) { | ||
| onArticleRead(bookmark.articleId); | ||
| } | ||
|
|
||
| if ('articleId' in bookmark && bookmark.url) { | ||
| window.open(bookmark.url, '_blank', 'noopener,noreferrer'); | ||
| } else if ('url' in bookmark && bookmark.url) { | ||
| // BookmarkCardProps 타입인 경우 | ||
| window.open(bookmark.url, '_blank', 'noopener,noreferrer'); | ||
| } | ||
| }; |
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.
URL 안전성 검증이 필요합니다.
DailyReminderSection과 동일한 보안 이슈가 있습니다. window.open 사용 시 URL 검증이 누락되어 있습니다.
URL 검증을 추가하세요:
const handleCardClick = (bookmark: BookmarkCardProps | Article) => {
if ('articleId' in bookmark && onArticleRead) {
onArticleRead(bookmark.articleId);
}
- if ('articleId' in bookmark && bookmark.url) {
- window.open(bookmark.url, '_blank', 'noopener,noreferrer');
- } else if ('url' in bookmark && bookmark.url) {
- window.open(bookmark.url, '_blank', 'noopener,noreferrer');
- }
+ const url = ('articleId' in bookmark) ? bookmark.url : bookmark.url;
+ if (url) {
+ try {
+ const validUrl = new URL(url);
+ if (validUrl.protocol === 'http:' || validUrl.protocol === 'https:') {
+ window.open(url, '_blank', 'noopener,noreferrer');
+ }
+ } catch (error) {
+ console.error('유효하지 않은 URL:', url);
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleCardClick = (bookmark: BookmarkCardProps | Article) => { | |
| if ('articleId' in bookmark && onArticleRead) { | |
| onArticleRead(bookmark.articleId); | |
| } | |
| if ('articleId' in bookmark && bookmark.url) { | |
| window.open(bookmark.url, '_blank', 'noopener,noreferrer'); | |
| } else if ('url' in bookmark && bookmark.url) { | |
| // BookmarkCardProps 타입인 경우 | |
| window.open(bookmark.url, '_blank', 'noopener,noreferrer'); | |
| } | |
| }; | |
| const handleCardClick = (bookmark: BookmarkCardProps | Article) => { | |
| if ('articleId' in bookmark && onArticleRead) { | |
| onArticleRead(bookmark.articleId); | |
| } | |
| const url = ('articleId' in bookmark) ? bookmark.url : bookmark.url; | |
| if (url) { | |
| try { | |
| const validUrl = new URL(url); | |
| if (validUrl.protocol === 'http:' || validUrl.protocol === 'https:') { | |
| window.open(url, '_blank', 'noopener,noreferrer'); | |
| } | |
| } catch (error) { | |
| console.error('유효하지 않은 URL:', url); | |
| } | |
| } | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx
around lines 90 to 101, the handleCardClick function uses window.open with
bookmark URLs without validating them, which poses a security risk. Add URL
validation before calling window.open by checking that the URL is a valid, safe
URL (e.g., starts with "http://" or "https://") to prevent potential security
issues. Only open the URL if it passes this validation.
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 (4)
apps/client/src/pages/dashboard/apis/thumbnail.ts (4)
4-5: 메모리 누수 방지를 위한 캐시 크기 제한 추가현재 캐시는 무제한으로 성장할 수 있어 메모리 누수 위험이 있습니다. 캐시 크기 제한을 추가하는 것이 좋겠습니다.
const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); -const CACHE_DURATION = 24 * 60 * 60 * 1000; +const CACHE_DURATION = 24 * 60 * 60 * 1000; +const MAX_CACHE_SIZE = 1000;
47-60: 캐시 관리 함수에 크기 제한 추가 필요캐시 설정 시 앞서 제안한 크기 제한 로직이 필요합니다.
const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { + // 캐시 크기 제한 확인 + if (thumbnailCache.size >= MAX_CACHE_SIZE) { + const oldestKey = thumbnailCache.keys().next().value; + thumbnailCache.delete(oldestKey); + } + thumbnailCache.set(url, { url: thumbnailUrl, timestamp: Date.now(), }); };
62-77: 요청 헤더 수정 필요GET 요청에서 Content-Type 헤더 대신 Accept 헤더를 사용해야 합니다.
- headers: { 'Content-Type': 'text/html' }, + headers: { 'Accept': 'text/html' },
154-172: 에러 처리 로직 개선최종 폴백 로직에서 캐시 저장이 누락되어 있습니다.
try { const domain = extractDomain(url); if (domain) { const fallbackFavicon = getFaviconUrl(domain, 128); - console.log('최종 폴백 favicon 사용:', domain); + if (fallbackFavicon) { + console.log('최종 폴백 favicon 사용:', domain); + setCachedThumbnail(url, fallbackFavicon); + } return fallbackFavicon; } } catch (fallbackError) { console.error('최종 폴백도 실패:', fallbackError); }
🧹 Nitpick comments (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
95-113: 불필요한 continue 문 제거정적 분석 도구에서 지적한 대로 불필요한 continue 문이 있습니다.
- } catch { - // 다음 크기 시도 - continue; - } + } catch { + // 다음 크기 시도 + }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
apps/client/src/pages/dashboard/apis/articleApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/categoryApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/index.ts(1 hunks)apps/client/src/pages/dashboard/apis/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/apis/userApi.ts(1 hunks)apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx(7 hunks)apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx(5 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)apps/client/src/pages/dashboard/dashboard.tsx(1 hunks)apps/client/src/pages/dashboard/hooks/useAcornCount.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useArticles.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useCategories.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useDashboard.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useTestData.ts(1 hunks)apps/client/src/pages/dashboard/types/api.ts(1 hunks)apps/client/src/pages/dashboard/utils/dateUtils.ts(1 hunks)apps/client/src/pages/dashboard/utils/queryConfig.ts(1 hunks)apps/client/src/shared/apis/axiosInstance.ts(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/client/src/pages/dashboard/types/api.ts
🚧 Files skipped from review as they are similar to previous changes (19)
- apps/client/src/pages/dashboard/apis/index.ts
- apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx
- apps/client/src/pages/dashboard/hooks/useTestData.ts
- apps/client/src/pages/dashboard/apis/userApi.ts
- apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx
- apps/client/src/pages/dashboard/dashboard.tsx
- apps/client/src/pages/dashboard/utils/dateUtils.ts
- apps/client/src/pages/dashboard/apis/categoryApi.ts
- apps/client/src/pages/dashboard/hooks/useCategories.ts
- apps/client/src/pages/dashboard/apis/articleApi.ts
- apps/client/src/pages/dashboard/hooks/useAcornCount.ts
- apps/client/src/pages/dashboard/hooks/useArticles.ts
- apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx
- apps/client/src/pages/dashboard/hooks/useDashboard.ts
- apps/client/src/pages/dashboard/utils/queryConfig.ts
- apps/client/src/shared/apis/axiosInstance.ts
- apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx
- apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx
- apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx
🧰 Additional context used
🧠 Learnings (1)
apps/client/src/pages/dashboard/apis/thumbnail.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🪛 Biome (1.9.4)
apps/client/src/pages/dashboard/apis/thumbnail.ts
[error] 115-115: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
🔇 Additional comments (7)
apps/client/src/pages/dashboard/apis/thumbnail.ts (7)
7-10: 메타데이터 패턴 구현이 잘 되어 있음정규표현식을 사용한 메타데이터 추출 패턴이 올바르게 구현되어 있고, const assertion을 사용하여 타입 안정성을 확보한 점이 좋습니다.
12-18: 도메인 추출 함수 구현이 적절함URL 객체를 사용한 도메인 추출과 try-catch를 통한 에러 처리가 적절하게 구현되어 있습니다.
20-24: 이미지 URL 정규화 로직 개선상대 URL을 절대 URL로 변환하는 로직이 올바르게 구현되어 있습니다.
26-38: 메타데이터 추출 로직 구현이 우수함Open Graph 이미지와 Twitter 이미지를 순차적으로 확인하는 폴백 메커니즘이 잘 구현되어 있습니다.
40-45: 파비콘 서비스 설정 검증 로직 개선환경 변수가 설정되지 않은 경우 null을 반환하는 방어 코드가 잘 구현되어 있습니다.
115-144: 프록시 서비스 로직 구현이 적절함프록시 서비스 설정 여부를 확인하고 적절한 로깅을 제공하는 로직이 잘 구현되어 있습니다.
175-188: 캐시 관리 함수들이 잘 구현됨캐시 정리 및 통계 함수들이 적절하게 구현되어 있으며, 디버깅에 유용한 기능을 제공합니다.
| const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL; | ||
| const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL; |
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.
💡 Verification agent
🧩 Analysis chain
환경 변수 설정 확인 필요
프록시 서비스와 파비콘 서비스 URL이 환경 변수로 설정되어 있는데, 이들이 undefined일 경우에 대한 적절한 처리가 필요합니다.
🏁 Script executed:
#!/bin/bash
# 환경 변수 설정 확인
echo "=== 환경 변수 설정 확인 ==="
fd -e env -e example | xargs grep -l "VITE_THUMBNAIL_PROXY_URL\|VITE_FAVICON_SERVICE_URL" || echo "환경 변수 설정 파일을 찾을 수 없음"Length of output: 191
환경 변수 정의 및 유효성 검증 추가하기
현재 apps/client/src/pages/dashboard/apis/thumbnail.ts 1–2행에서 사용 중인
VITE_THUMBNAIL_PROXY_URL, VITE_FAVICON_SERVICE_URL이 프로젝트의 .env* 파일에 정의되어 있지 않습니다.
다음 중 하나를 적용해 주세요:
- 설정 파일에 추가
프로젝트 루트의.env.example(또는 실제.env)에 해당 변수를 선언하고, 문서에도 사용법을 안내합니다. - 코드 레벨 검증/기본값 지정
import.meta.env로 값을 가져온 직후에undefined여부를 확인하여 예외를 던지거나, 안전한 기본값을 지정합니다.
대상 위치
- apps/client/src/pages/dashboard/apis/thumbnail.ts (1–2행)
예시 수정안
- const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL;
- const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL;
+ const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL;
+ if (!PROXY_SERVICE) {
+ throw new Error('환경 변수 VITE_THUMBNAIL_PROXY_URL이 정의되지 않았습니다.');
+ }
+ const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL;
+ if (!FAVICON_SERVICE) {
+ throw new Error('환경 변수 VITE_FAVICON_SERVICE_URL이 정의되지 않았습니다.');
+ }또는
+ const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL ?? 'https://your-default-proxy.example.com';
+ const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL ?? 'https://your-default-favicon.example.com';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL; | |
| const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL; | |
| // apps/client/src/pages/dashboard/apis/thumbnail.ts | |
| - const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL; | |
| - const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL; | |
| + const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL; | |
| + if (!PROXY_SERVICE) { | |
| + throw new Error('환경 변수 VITE_THUMBNAIL_PROXY_URL이 정의되지 않았습니다.'); | |
| + } | |
| + const FAVICON_SERVICE = import.meta.env.VITE_FAVICON_SERVICE_URL; | |
| + if (!FAVICON_SERVICE) { | |
| + throw new Error('환경 변수 VITE_FAVICON_SERVICE_URL이 정의되지 않았습니다.'); | |
| + } |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/apis/thumbnail.ts at lines 1-2, the
environment variables VITE_THUMBNAIL_PROXY_URL and VITE_FAVICON_SERVICE_URL are
used but not defined in the project's .env files. Fix this by either adding
these variables with example values to the project's .env.example file and
documenting their usage, or by adding code-level validation immediately after
importing them to check if they are undefined and either throw an error or
assign safe default values to prevent runtime issues.
# Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx # apps/client/src/pages/onBoarding/OnBoarding.tsx
# Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx
# Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx
- TestSection 컴포넌트 삭제 - 테스트 버튼 UI 제거 - useDashboard 훅에서 테스트 관련 함수 제거 - 도토리 개수 업데이트 로직 개선 - 배너 렌더링 디버깅 로그 추가 - 썸네일 화질 개선 (64px → 128px) - api.allorigins.win 프록시 서비스 제거 - TypeScript 오류 수정 # Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx
- 빈 함수 오류 수정 (empty arrow function) - non-null assertion 오류 수정 - 디버깅용 console.log 제거 - 코드 포맷팅 적용
- BookmarkCard, DailyReminderCard에서 더보기 버튼 클릭 시 e.stopPropagation() 추가 - DailyReminderCard 스탬프 오버레이에 pointer-events-none 추가 - Dashboard 모달 팝업 시 스크롤 고정 로직 추가 - axiosInstance에서 하드코딩된 토큰 발급 로직 제거 - 온보딩에서 저장된 토큰만 사용하도록 수정 - 대시보드에서 토큰이 없으면 온보딩으로 리다이렉트하는 로직 추가
fa9fff6 to
25d8f3d
Compare
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: 5
♻️ Duplicate comments (6)
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
92-103: URL 안전성 검증이 누락되었습니다.
window.open사용 시 URL 검증 없이 임의 URL을 열 수 있어 보안 위험이 있습니다.URL 검증을 추가하세요:
const handleCardClick = (bookmark: BookmarkCardProps | Article) => { if ('articleId' in bookmark && onArticleRead) { onArticleRead(bookmark.articleId); } const url = ('articleId' in bookmark) ? bookmark.url : bookmark.url; if (url) { try { const validUrl = new URL(url); if (validUrl.protocol === 'http:' || validUrl.protocol === 'https:') { window.open(url, '_blank', 'noopener,noreferrer'); } } catch (error) { console.error('유효하지 않은 URL:', url); } } };
117-142: 카테고리 렌더링 로직 중복이 여전히 존재합니다.로딩 상태와 일반 상태에서 카테고리 렌더링 로직이 동일하게 중복되어 있습니다.
공통 컴포넌트로 추출하여 중복을 제거하세요:
const CategoryNav = ({ categories, activeCategory, totalUnreadArticle, categoryTotalCounts, onCategoryClick }: CategoryNavProps) => ( <nav className="mb-6 flex flex-wrap gap-[2rem]" aria-label="북마크 카테고리 필터"> <BookmarkCategory key="unread" id="unread" text="안 읽은 정보" count={totalUnreadArticle} isActive={activeCategory === '안 읽은 정보'} onClick={() => onCategoryClick('안 읽은 정보')} hideZeroCount={true} /> {categories.map((category) => ( <BookmarkCategory key={category.categoryId} id={category.categoryId.toString()} text={category.categoryName} count={categoryTotalCounts[category.categoryId] || category.unreadCount} isActive={activeCategory === category.categoryName} onClick={() => onCategoryClick(category.categoryName, category.categoryId)} hideZeroCount={true} /> ))} </nav> );Also applies to: 167-191
apps/client/src/pages/dashboard/apis/thumbnail.ts (4)
1-2: 환경 변수 검증 로직 추가 필요현재 환경 변수들이
undefined일 가능성이 있어 런타임 오류를 발생시킬 수 있습니다. 환경 변수 설정 파일 정의 및 코드 레벨 검증을 추가해주세요.
4-5: 캐시 크기 제한 추가 필요현재 캐시는 무제한으로 성장할 수 있어 메모리 누수 위험이 있습니다. 최대 캐시 크기를 설정하고 관리 로직을 추가해주세요.
55-60: 캐시 관리 함수에 크기 제한 추가 필요캐시 설정 시 크기 제한 로직이 필요합니다.
69-69: 잘못된 요청 헤더 사용GET 요청에서
Content-Type헤더 대신Accept헤더를 사용해야 합니다.
🧹 Nitpick comments (4)
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (1)
207-242: 복잡한 조건부 렌더링 로직을 개선할 수 있습니다.타입 가드와 prop 매핑이 복잡하여 가독성이 떨어집니다.
타입별 렌더링을 분리하여 개선하세요:
const renderBookmarkCard = (card: BookmarkCardProps | Article, idx: number) => { const isArticle = 'articleId' in card; const cardProps = isArticle ? { id: card.articleId.toString(), title: card.url, memo: card.memo, isRead: card.isRead, categoryId: '1', url: card.url, handlePopUpOpen, } : card; return ( <div key={isArticle ? card.articleId : card.id || `bookmark-${idx}`} className="cursor-pointer" role="gridcell" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleCardClick(card); } }} onClick={() => handleCardClick(card)} > <BookmarkCard {...cardProps} handlePopUpOpen={handlePopUpOpen} /> </div> ); }; // 사용 {filteredBookmarks.map(renderBookmarkCard)}apps/client/src/pages/dashboard/apis/thumbnail.ts (1)
20-24: 이미지 URL 정규화 로직 개선현재 구현이 적절하지만, 프로토콜 상대 URL(
//example.com/image.jpg)에 대한 처리도 고려해보세요.const normalizeImageUrl = (imageUrl: string, baseUrl: string): string => { + if (imageUrl.startsWith('//')) { + return `https:${imageUrl}`; + } return imageUrl.startsWith('http') ? imageUrl : new URL(imageUrl, baseUrl).href; };apps/client/public/firebase_sdk/firebase-app-compat.js (2)
10-10: 불필요한 'use strict' 지시문모듈은 자동으로 strict 모드에서 실행되므로 명시적인 'use strict' 지시문이 불필요합니다.
- 'use strict';
175-175: Object.prototype.hasOwnProperty 직접 접근 방지
Object.prototype.hasOwnProperty를 직접 호출하는 것보다Object.hasOwn()사용을 권장합니다.- t.hasOwnProperty(n) && '__proto__' !== n && (e[n] = c(e[n], t[n])); + Object.hasOwn(t, n) && '__proto__' !== n && (e[n] = c(e[n], t[n]));
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (55)
apps/client/public/firebase_sdk/firebase-app-compat.js(1 hunks)apps/client/src/main.tsx(1 hunks)apps/client/src/pages/dashboard/apis/articleApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/categoryApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/index.ts(1 hunks)apps/client/src/pages/dashboard/apis/thumbnail.ts(1 hunks)apps/client/src/pages/dashboard/apis/userApi.ts(1 hunks)apps/client/src/pages/dashboard/components/common/display/BookmarkCategory.tsx(1 hunks)apps/client/src/pages/dashboard/components/common/layout/SectionContent.tsx(1 hunks)apps/client/src/pages/dashboard/components/common/layout/SectionTitle.tsx(1 hunks)apps/client/src/pages/dashboard/components/sections/BannerSection.tsx(1 hunks)apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx(7 hunks)apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx(2 hunks)apps/client/src/pages/dashboard/components/sections/LevelInfoSection.tsx(1 hunks)apps/client/src/pages/dashboard/components/sections/TestSection.tsx(0 hunks)apps/client/src/pages/dashboard/components/sections/index.ts(0 hunks)apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/banner/BannerTimer.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/buttons/AllViewButton.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx(3 hunks)apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx(6 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx(2 hunks)apps/client/src/pages/dashboard/components/ui/indicators/LevelInfoItem.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx(1 hunks)apps/client/src/pages/dashboard/components/ui/overlays/LevelInfoModal.tsx(1 hunks)apps/client/src/pages/dashboard/constants/levelData.ts(0 hunks)apps/client/src/pages/dashboard/dashboard.tsx(2 hunks)apps/client/src/pages/dashboard/hooks/useAcornCount.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useArticles.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useCategories.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useDashboard.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useTestData.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useTimer.ts(0 hunks)apps/client/src/pages/dashboard/mockData.ts(0 hunks)apps/client/src/pages/dashboard/types/api.ts(1 hunks)apps/client/src/pages/dashboard/utils/dateUtils.ts(1 hunks)apps/client/src/pages/dashboard/utils/queryConfig.ts(1 hunks)apps/client/src/pages/index.ts(1 hunks)apps/client/src/pages/onBoarding/components/StoryStep.tsx(0 hunks)apps/client/src/shared/apis/axiosInstance.ts(2 hunks)apps/client/src/shared/apis/index.ts(1 hunks)apps/client/src/shared/apis/timerApi.ts(0 hunks)apps/client/src/shared/components/ui/layout/Header.tsx(1 hunks)apps/client/src/shared/components/ui/modalPop/ModalPop.tsx(0 hunks)apps/client/src/shared/hooks/index.ts(1 hunks)apps/client/src/shared/hooks/useServerTimer.ts(0 hunks)apps/client/src/shared/index.ts(1 hunks)apps/client/src/shared/types/api.ts(0 hunks)apps/client/src/shared/types/index.ts(0 hunks)apps/client/src/shared/utils/errorUtils.ts(1 hunks)apps/client/src/shared/utils/index.ts(1 hunks)apps/client/src/shared/utils/timerUtils.ts(0 hunks)apps/client/tsconfig.json(1 hunks)apps/client/vite.config.ts(0 hunks)
💤 Files with no reviewable changes (13)
- apps/client/src/pages/onBoarding/components/StoryStep.tsx
- apps/client/vite.config.ts
- apps/client/src/shared/components/ui/modalPop/ModalPop.tsx
- apps/client/src/pages/dashboard/components/sections/index.ts
- apps/client/src/pages/dashboard/constants/levelData.ts
- apps/client/src/pages/dashboard/hooks/useTimer.ts
- apps/client/src/shared/hooks/useServerTimer.ts
- apps/client/src/shared/types/index.ts
- apps/client/src/shared/utils/timerUtils.ts
- apps/client/src/pages/dashboard/components/sections/TestSection.tsx
- apps/client/src/shared/types/api.ts
- apps/client/src/shared/apis/timerApi.ts
- apps/client/src/pages/dashboard/mockData.ts
✅ Files skipped from review due to trivial changes (6)
- apps/client/src/pages/dashboard/components/ui/buttons/AllViewButton.tsx
- apps/client/src/pages/dashboard/components/common/layout/SectionTitle.tsx
- apps/client/src/pages/dashboard/components/common/layout/SectionContent.tsx
- apps/client/src/shared/components/ui/layout/Header.tsx
- apps/client/src/shared/utils/errorUtils.ts
- apps/client/src/pages/dashboard/types/api.ts
🚧 Files skipped from review as they are similar to previous changes (32)
- apps/client/src/pages/dashboard/components/ui/overlays/LevelInfoModal.tsx
- apps/client/src/main.tsx
- apps/client/src/pages/dashboard/components/common/display/BookmarkCategory.tsx
- apps/client/src/pages/dashboard/apis/index.ts
- apps/client/src/shared/apis/index.ts
- apps/client/src/shared/utils/index.ts
- apps/client/tsconfig.json
- apps/client/src/pages/dashboard/components/ui/feedback/Modal.tsx
- apps/client/src/pages/dashboard/apis/userApi.ts
- apps/client/src/pages/dashboard/components/sections/BannerSection.tsx
- apps/client/src/pages/dashboard/components/ui/cards/BookmarkCard.tsx
- apps/client/src/pages/dashboard/components/sections/LevelInfoSection.tsx
- apps/client/src/shared/index.ts
- apps/client/src/pages/dashboard/components/ui/indicators/LevelInfoItem.tsx
- apps/client/src/pages/index.ts
- apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx
- apps/client/src/pages/dashboard/hooks/useArticles.ts
- apps/client/src/pages/dashboard/components/ui/banner/BannerTimer.tsx
- apps/client/src/pages/dashboard/dashboard.tsx
- apps/client/src/pages/dashboard/hooks/useTestData.ts
- apps/client/src/shared/hooks/index.ts
- apps/client/src/shared/apis/axiosInstance.ts
- apps/client/src/pages/dashboard/utils/dateUtils.ts
- apps/client/src/pages/dashboard/hooks/useCategories.ts
- apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx
- apps/client/src/pages/dashboard/apis/articleApi.ts
- apps/client/src/pages/dashboard/components/ui/cards/DailyReminderCard.tsx
- apps/client/src/pages/dashboard/components/sections/DailyReminderSection.tsx
- apps/client/src/pages/dashboard/hooks/useAcornCount.ts
- apps/client/src/pages/dashboard/apis/categoryApi.ts
- apps/client/src/pages/dashboard/utils/queryConfig.ts
- apps/client/src/pages/dashboard/components/ui/indicators/Timer.tsx
🧰 Additional context used
🧠 Learnings (2)
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (4)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.792Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/components/InfoBox/InfoBox.tsx:10-11
Timestamp: 2025-07-08T11:47:27.279Z
Learning: In React TypeScript components, prefer semantic prop names with union types over abstract version numbers. For example, use `size: 'small' | 'large'` instead of `version: 1 | 2` for better code readability, type safety, and extensibility.
apps/client/src/pages/dashboard/apis/thumbnail.ts (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🪛 Biome (1.9.4)
apps/client/public/firebase_sdk/firebase-app-compat.js
[error] 10-10: Redundant use strict directive.
The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.
(lint/suspicious/noRedundantUseStrict)
[error] 154-154: e is assigned to itself.
This is where is assigned.
(lint/correctness/noSelfAssign)
[error] 163-163: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 175-175: Do not access Object.prototype method 'hasOwnProperty' from target object.
It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.
(lint/suspicious/noPrototypeBuiltins)
[error] 258-259: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 716-716: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 720-720: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 724-724: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 728-728: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 732-732: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 979-979: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 1109-1109: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 271-271: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 271-271: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 273-273: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 278-278: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 278-278: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 562-562: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 793-793: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 794-794: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 795-795: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 907-907: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1277-1277: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1277-1277: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1510-1526: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
apps/client/src/pages/dashboard/apis/thumbnail.ts
[error] 115-115: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
🔇 Additional comments (11)
apps/client/src/pages/dashboard/hooks/useDashboard.ts (3)
1-4: Import 구조가 깔끔하게 정리되었습니다.특화된 hooks로 분리하여 관심사를 명확히 분리한 것이 좋습니다.
52-58: 카테고리 클릭 핸들러가 적절히 구현되었습니다.로컬 상태 업데이트와 베이스 핸들러 호출을 적절히 분리하여 처리하고 있습니다.
95-142: 반환 객체가 잘 구조화되어 있습니다.상태, 데이터, 로딩 상태, 에러 상태, 액션 핸들러, 리페치 함수별로 명확히 분류되어 있어 사용하기 편리합니다.
apps/client/src/pages/dashboard/components/sections/BookmarkSection.tsx (2)
30-31: 타입 union 처리가 적절합니다.
BookmarkCardProps | Articleunion 타입을 사용하여 서버 API 데이터와 기존 데이터를 모두 지원하는 것이 좋습니다.
105-154: 로딩 상태 UI가 적절히 구현되었습니다.스켈레톤 UI와 카테고리 필터를 포함한 로딩 상태 처리가 사용자 경험을 개선합니다.
apps/client/src/pages/dashboard/apis/thumbnail.ts (6)
7-10: 메타데이터 패턴 정의 개선메타데이터 추출 패턴이 잘 정의되어 있습니다. 추가적으로
twitter:image:src패턴도 고려해볼 수 있습니다.
12-18: 도메인 추출 함수 구현 양호URL 파싱 시 예외 처리가 적절하게 구현되어 있습니다.
26-38: 메타데이터 추출 로직 효율적 구현Open Graph와 Twitter Card 이미지 우선순위 처리가 적절하게 구현되어 있습니다.
115-144: 프록시 서비스 조건부 처리 로직 개선프록시 서비스가 설정되지 않은 경우에 대한 처리가 적절하게 구현되어 있습니다.
154-173: 에러 처리 로직이 개선됨이전 리뷰에서 지적된 중복 폴백 로직과 주석 번호 문제가 해결되었습니다.
175-188: 캐시 관리 유틸리티 함수들 양호캐시 정리 및 통계 함수들이 잘 구현되어 있어 디버깅과 모니터링에 유용합니다.
| const handleAllViewClick = useCallback(() => { | ||
| setIsAllViewExpanded((prev) => !prev); | ||
| }, []); | ||
| setIsAllViewExpanded((prev) => { | ||
| const newExpanded = !prev; | ||
|
|
||
| const updateAcornCount = useCallback((count: BannerStage) => { | ||
| setAcornCount(count); | ||
| }, []); | ||
| if (newExpanded) { | ||
| if (selectedCategoryId) { | ||
| const totalCategoryArticles = | ||
| categoryArticlesData?.data?.totalArticle || 0; | ||
| setCategoryPageSizeToTotal(totalCategoryArticles); | ||
| } else { | ||
| const totalUnreadArticles = | ||
| unreadArticlesData?.data?.totalUnreadArticle || 0; | ||
| const totalDailyReminderArticles = | ||
| dailyReminderData?.data?.totalArticle || 0; | ||
| setPageSizesToTotal(totalUnreadArticles, totalDailyReminderArticles); | ||
| } | ||
| } else { | ||
| resetPageSizes(); | ||
| resetCategoryPageSize(); | ||
| } | ||
|
|
||
| return newExpanded; | ||
| }); | ||
| }, [ | ||
| selectedCategoryId, | ||
| categoryArticlesData, | ||
| unreadArticlesData, | ||
| dailyReminderData, | ||
| setCategoryPageSizeToTotal, | ||
| setPageSizesToTotal, | ||
| resetPageSizes, | ||
| resetCategoryPageSize, | ||
| ]); |
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.
🛠️ Refactor suggestion
전체보기 로직이 복잡하고 중복된 패턴이 있습니다.
조건부 로직이 복잡하여 가독성이 떨어지고, 유사한 패턴이 반복됩니다.
다음과 같이 리팩토링을 제안합니다:
const handleAllViewClick = useCallback(() => {
setIsAllViewExpanded((prev) => {
const newExpanded = !prev;
if (newExpanded) {
const expandHandlers = {
category: () => {
const totalCategoryArticles = categoryArticlesData?.data?.totalArticle || 0;
setCategoryPageSizeToTotal(totalCategoryArticles);
},
unread: () => {
const totalUnreadArticles = unreadArticlesData?.data?.totalUnreadArticle || 0;
const totalDailyReminderArticles = dailyReminderData?.data?.totalArticle || 0;
setPageSizesToTotal(totalUnreadArticles, totalDailyReminderArticles);
}
};
const handler = selectedCategoryId ? expandHandlers.category : expandHandlers.unread;
handler();
} else {
resetPageSizes();
resetCategoryPageSize();
}
return newExpanded;
});
}, [
selectedCategoryId,
categoryArticlesData,
unreadArticlesData,
dailyReminderData,
setCategoryPageSizeToTotal,
setPageSizesToTotal,
resetPageSizes,
resetCategoryPageSize,
]);🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/hooks/useDashboard.ts around lines 61 to 93,
the handleAllViewClick function contains complex and repetitive conditional
logic for expanding views. Refactor by defining an object with handlers for each
case (category and unread), then select and invoke the appropriate handler based
on selectedCategoryId. This reduces duplication and improves readability while
preserving existing functionality.
| const faviconSizes = [256, 128, 64]; | ||
| let faviconUrl: string | null = null; | ||
|
|
||
| for (const size of faviconSizes) { | ||
| const testUrl = getFaviconUrl(domain, size); | ||
| if (testUrl) { | ||
| try { | ||
| const response = await fetch(testUrl, { method: 'HEAD' }); | ||
| if (response.ok) { | ||
| faviconUrl = testUrl; | ||
| console.log(`Favicon ${size}px 사용 가능:`, domain); | ||
| break; | ||
| } | ||
| } catch { | ||
| // 다음 크기 시도 | ||
| continue; | ||
| } | ||
| } | ||
| } |
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.
파비콘 크기 탐지 로직 개선
여러 크기의 파비콘을 시도하는 로직이 좋습니다. 하지만 정적 분석 도구에서 지적한 대로 불필요한 continue 문을 제거해야 합니다.
} catch {
- // 다음 크기 시도
- continue;
+ // 다음 크기 시도 (자동으로 루프 계속)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const faviconSizes = [256, 128, 64]; | |
| let faviconUrl: string | null = null; | |
| for (const size of faviconSizes) { | |
| const testUrl = getFaviconUrl(domain, size); | |
| if (testUrl) { | |
| try { | |
| const response = await fetch(testUrl, { method: 'HEAD' }); | |
| if (response.ok) { | |
| faviconUrl = testUrl; | |
| console.log(`Favicon ${size}px 사용 가능:`, domain); | |
| break; | |
| } | |
| } catch { | |
| // 다음 크기 시도 | |
| continue; | |
| } | |
| } | |
| } | |
| const faviconSizes = [256, 128, 64]; | |
| let faviconUrl: string | null = null; | |
| for (const size of faviconSizes) { | |
| const testUrl = getFaviconUrl(domain, size); | |
| if (testUrl) { | |
| try { | |
| const response = await fetch(testUrl, { method: 'HEAD' }); | |
| if (response.ok) { | |
| faviconUrl = testUrl; | |
| console.log(`Favicon ${size}px 사용 가능:`, domain); | |
| break; | |
| } | |
| } catch { | |
| // 다음 크기 시도 (자동으로 루프 계속) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/apis/thumbnail.ts around lines 95 to 113, the
loop that tries different favicon sizes uses an unnecessary continue statement
inside the catch block. Remove the continue statement because the loop will
naturally proceed to the next iteration after the catch block, making the
continue redundant.
| !(function (e, t) { | ||
| 'object' == typeof exports && 'undefined' != typeof module | ||
| ? (module.exports = t()) | ||
| : 'function' == typeof define && define.amd | ||
| ? define(t) | ||
| : ((e = | ||
| 'undefined' != typeof globalThis ? globalThis : e || self).firebase = | ||
| t()); | ||
| })(this, function () { | ||
| 'use strict'; | ||
| const r = function (t) { | ||
| const r = []; | ||
| let n = 0; | ||
| for (let a = 0; a < t.length; a++) { | ||
| let e = t.charCodeAt(a); | ||
| e < 128 | ||
| ? (r[n++] = e) | ||
| : (e < 2048 | ||
| ? (r[n++] = (e >> 6) | 192) | ||
| : (55296 == (64512 & e) && | ||
| a + 1 < t.length && | ||
| 56320 == (64512 & t.charCodeAt(a + 1)) | ||
| ? ((e = | ||
| 65536 + ((1023 & e) << 10) + (1023 & t.charCodeAt(++a))), | ||
| (r[n++] = (e >> 18) | 240), | ||
| (r[n++] = ((e >> 12) & 63) | 128)) | ||
| : (r[n++] = (e >> 12) | 224), | ||
| (r[n++] = ((e >> 6) & 63) | 128)), | ||
| (r[n++] = (63 & e) | 128)); | ||
| } | ||
| return r; | ||
| }, | ||
| n = { | ||
| byteToCharMap_: null, | ||
| charToByteMap_: null, | ||
| byteToCharMapWebSafe_: null, | ||
| charToByteMapWebSafe_: null, | ||
| ENCODED_VALS_BASE: | ||
| 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', | ||
| get ENCODED_VALS() { | ||
| return this.ENCODED_VALS_BASE + '+/='; | ||
| }, | ||
| get ENCODED_VALS_WEBSAFE() { | ||
| return this.ENCODED_VALS_BASE + '-_.'; | ||
| }, | ||
| HAS_NATIVE_SUPPORT: 'function' == typeof atob, | ||
| encodeByteArray(r, e) { | ||
| if (!Array.isArray(r)) { | ||
| throw Error('encodeByteArray takes an array as a parameter'); | ||
| } | ||
| this.init_(); | ||
| var n = e ? this.byteToCharMapWebSafe_ : this.byteToCharMap_; | ||
| const a = []; | ||
| for (let l = 0; l < r.length; l += 3) { | ||
| var i = r[l], | ||
| s = l + 1 < r.length, | ||
| o = s ? r[l + 1] : 0, | ||
| c = l + 2 < r.length, | ||
| h = c ? r[l + 2] : 0; | ||
| let e = ((15 & o) << 2) | (h >> 6), | ||
| t = 63 & h; | ||
| (c || ((t = 64), s || (e = 64)), | ||
| a.push(n[i >> 2], n[((3 & i) << 4) | (o >> 4)], n[e], n[t])); | ||
| } | ||
| return a.join(''); | ||
| }, | ||
| encodeString(e, t) { | ||
| return this.HAS_NATIVE_SUPPORT && !t | ||
| ? btoa(e) | ||
| : this.encodeByteArray(r(e), t); | ||
| }, | ||
| decodeString(e, t) { | ||
| return this.HAS_NATIVE_SUPPORT && !t | ||
| ? atob(e) | ||
| : (function (e) { | ||
| const t = []; | ||
| let r = 0, | ||
| n = 0; | ||
| for (; r < e.length; ) { | ||
| var a, | ||
| i, | ||
| s = e[r++]; | ||
| s < 128 | ||
| ? (t[n++] = String.fromCharCode(s)) | ||
| : 191 < s && s < 224 | ||
| ? ((a = e[r++]), | ||
| (t[n++] = String.fromCharCode( | ||
| ((31 & s) << 6) | (63 & a) | ||
| ))) | ||
| : 239 < s && s < 365 | ||
| ? ((i = | ||
| (((7 & s) << 18) | | ||
| ((63 & e[r++]) << 12) | | ||
| ((63 & e[r++]) << 6) | | ||
| (63 & e[r++])) - | ||
| 65536), | ||
| (t[n++] = String.fromCharCode(55296 + (i >> 10))), | ||
| (t[n++] = String.fromCharCode(56320 + (1023 & i)))) | ||
| : ((a = e[r++]), | ||
| (i = e[r++]), | ||
| (t[n++] = String.fromCharCode( | ||
| ((15 & s) << 12) | ((63 & a) << 6) | (63 & i) | ||
| ))); | ||
| } | ||
| return t.join(''); | ||
| })(this.decodeStringToByteArray(e, t)); | ||
| }, | ||
| decodeStringToByteArray(e, t) { | ||
| this.init_(); | ||
| var r = t ? this.charToByteMapWebSafe_ : this.charToByteMap_; | ||
| const n = []; | ||
| for (let c = 0; c < e.length; ) { | ||
| var a = r[e.charAt(c++)], | ||
| i = c < e.length ? r[e.charAt(c)] : 0; | ||
| ++c; | ||
| var s = c < e.length ? r[e.charAt(c)] : 64; | ||
| ++c; | ||
| var o = c < e.length ? r[e.charAt(c)] : 64; | ||
| if ((++c, null == a || null == i || null == s || null == o)) { | ||
| throw new h(); | ||
| } | ||
| (n.push((a << 2) | (i >> 4)), | ||
| 64 !== s && | ||
| (n.push(((i << 4) & 240) | (s >> 2)), | ||
| 64 !== o && n.push(((s << 6) & 192) | o))); | ||
| } | ||
| return n; | ||
| }, | ||
| init_() { | ||
| if (!this.byteToCharMap_) { | ||
| ((this.byteToCharMap_ = {}), | ||
| (this.charToByteMap_ = {}), | ||
| (this.byteToCharMapWebSafe_ = {}), | ||
| (this.charToByteMapWebSafe_ = {})); | ||
| for (let e = 0; e < this.ENCODED_VALS.length; e++) { | ||
| ((this.byteToCharMap_[e] = this.ENCODED_VALS.charAt(e)), | ||
| (this.charToByteMap_[this.byteToCharMap_[e]] = e), | ||
| (this.byteToCharMapWebSafe_[e] = | ||
| this.ENCODED_VALS_WEBSAFE.charAt(e)), | ||
| (this.charToByteMapWebSafe_[this.byteToCharMapWebSafe_[e]] = e), | ||
| e >= this.ENCODED_VALS_BASE.length && | ||
| ((this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(e)] = e), | ||
| (this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(e)] = e))); | ||
| } | ||
| } | ||
| }, | ||
| }; | ||
| class h extends Error { | ||
| constructor() { | ||
| (super(...arguments), (this.name = 'DecodeBase64StringError')); | ||
| } | ||
| } | ||
| const a = function (e) { | ||
| return ((e = e), (t = r(e)), n.encodeByteArray(t, !0).replace(/\./g, '')); | ||
| var t; | ||
| }; | ||
| function c(e, t) { | ||
| if (!(t instanceof Object)) { | ||
| return t; | ||
| } | ||
| switch (t.constructor) { | ||
| case Date: | ||
| const r = t; | ||
| return new Date(r.getTime()); | ||
| case Object: | ||
| void 0 === e && (e = {}); | ||
| break; | ||
| case Array: | ||
| e = []; | ||
| break; | ||
| default: | ||
| return t; | ||
| } | ||
| for (const n in t) { | ||
| t.hasOwnProperty(n) && '__proto__' !== n && (e[n] = c(e[n], t[n])); | ||
| } | ||
| return e; | ||
| } | ||
| const e = () => | ||
| (function () { | ||
| if ('undefined' != typeof self) { | ||
| return self; | ||
| } | ||
| if ('undefined' != typeof window) { | ||
| return window; | ||
| } | ||
| if ('undefined' != typeof global) { | ||
| return global; | ||
| } | ||
| throw new Error('Unable to locate global object.'); | ||
| })().__FIREBASE_DEFAULTS__, | ||
| t = () => { | ||
| if ('undefined' != typeof document) { | ||
| let e; | ||
| try { | ||
| e = document.cookie.match(/__FIREBASE_DEFAULTS__=([^;]+)/); | ||
| } catch (e) { | ||
| return; | ||
| } | ||
| var t = | ||
| e && | ||
| (function (e) { | ||
| try { | ||
| return n.decodeString(e, !0); | ||
| } catch (e) { | ||
| console.error('base64Decode failed: ', e); | ||
| } | ||
| return null; | ||
| })(e[1]); | ||
| return t && JSON.parse(t); | ||
| } | ||
| }, | ||
| i = () => { | ||
| try { | ||
| return ( | ||
| e() || | ||
| (() => { | ||
| if ('undefined' != typeof process && void 0 !== process.env) { | ||
| var e = process.env.__FIREBASE_DEFAULTS__; | ||
| return e ? JSON.parse(e) : void 0; | ||
| } | ||
| })() || | ||
| t() | ||
| ); | ||
| } catch (e) { | ||
| return void console.info( | ||
| `Unable to get __FIREBASE_DEFAULTS__ due to: ${e}` | ||
| ); | ||
| } | ||
| }, | ||
| l = () => { | ||
| var e; | ||
| return null === (e = i()) || void 0 === e ? void 0 : e.config; | ||
| }; | ||
| class s { | ||
| constructor() { | ||
| ((this.reject = () => {}), | ||
| (this.resolve = () => {}), | ||
| (this.promise = new Promise((e, t) => { | ||
| ((this.resolve = e), (this.reject = t)); | ||
| }))); | ||
| } | ||
| wrapCallback(r) { | ||
| return (e, t) => { | ||
| (e ? this.reject(e) : this.resolve(t), | ||
| 'function' == typeof r && | ||
| (this.promise.catch(() => {}), 1 === r.length ? r(e) : r(e, t))); | ||
| }; | ||
| } | ||
| } | ||
| class o extends Error { | ||
| constructor(e, t, r) { | ||
| (super(t), | ||
| (this.code = e), | ||
| (this.customData = r), | ||
| (this.name = 'FirebaseError'), | ||
| Object.setPrototypeOf(this, o.prototype), | ||
| Error.captureStackTrace && | ||
| Error.captureStackTrace(this, d.prototype.create)); | ||
| } | ||
| } | ||
| class d { | ||
| constructor(e, t, r) { | ||
| ((this.service = e), (this.serviceName = t), (this.errors = r)); | ||
| } | ||
| create(e, ...t) { | ||
| var n, | ||
| r = t[0] || {}, | ||
| a = `${this.service}/${e}`, | ||
| i = this.errors[e], | ||
| i = i | ||
| ? ((n = r), | ||
| i.replace(p, (e, t) => { | ||
| var r = n[t]; | ||
| return null != r ? String(r) : `<${t}?>`; | ||
| })) | ||
| : 'Error', | ||
| i = `${this.serviceName}: ${i} (${a}).`; | ||
| return new o(a, i, r); | ||
| } | ||
| } | ||
| const p = /\{\$([^}]+)}/g; | ||
| function u(e, t) { | ||
| return Object.prototype.hasOwnProperty.call(e, t); | ||
| } | ||
| function f(e, t) { | ||
| if (e === t) { | ||
| return 1; | ||
| } | ||
| const r = Object.keys(e), | ||
| n = Object.keys(t); | ||
| for (const s of r) { | ||
| if (!n.includes(s)) { | ||
| return; | ||
| } | ||
| var a = e[s], | ||
| i = t[s]; | ||
| if (g(a) && g(i)) { | ||
| if (!f(a, i)) { | ||
| return; | ||
| } | ||
| } else if (a !== i) { | ||
| return; | ||
| } | ||
| } | ||
| for (const o of n) { | ||
| if (!r.includes(o)) { | ||
| return; | ||
| } | ||
| } | ||
| return 1; | ||
| } | ||
| function g(e) { | ||
| return null !== e && 'object' == typeof e; | ||
| } | ||
| function m(e, t) { | ||
| const r = new b(e, t); | ||
| return r.subscribe.bind(r); | ||
| } | ||
| class b { | ||
| constructor(e, t) { | ||
| ((this.observers = []), | ||
| (this.unsubscribes = []), | ||
| (this.observerCount = 0), | ||
| (this.task = Promise.resolve()), | ||
| (this.finalized = !1), | ||
| (this.onNoObservers = t), | ||
| this.task | ||
| .then(() => { | ||
| e(this); | ||
| }) | ||
| .catch((e) => { | ||
| this.error(e); | ||
| })); | ||
| } | ||
| next(t) { | ||
| this.forEachObserver((e) => { | ||
| e.next(t); | ||
| }); | ||
| } | ||
| error(t) { | ||
| (this.forEachObserver((e) => { | ||
| e.error(t); | ||
| }), | ||
| this.close(t)); | ||
| } | ||
| complete() { | ||
| (this.forEachObserver((e) => { | ||
| e.complete(); | ||
| }), | ||
| this.close()); | ||
| } | ||
| subscribe(e, t, r) { | ||
| let n; | ||
| if (void 0 === e && void 0 === t && void 0 === r) { | ||
| throw new Error('Missing Observer.'); | ||
| } | ||
| ((n = (function (e, t) { | ||
| if ('object' != typeof e || null === e) { | ||
| return !1; | ||
| } | ||
| for (const r of t) { | ||
| if (r in e && 'function' == typeof e[r]) { | ||
| return !0; | ||
| } | ||
| } | ||
| return !1; | ||
| })(e, ['next', 'error', 'complete']) | ||
| ? e | ||
| : { next: e, error: t, complete: r }), | ||
| void 0 === n.next && (n.next = v), | ||
| void 0 === n.error && (n.error = v), | ||
| void 0 === n.complete && (n.complete = v)); | ||
| var a = this.unsubscribeOne.bind(this, this.observers.length); | ||
| return ( | ||
| this.finalized && | ||
| this.task.then(() => { | ||
| try { | ||
| this.finalError ? n.error(this.finalError) : n.complete(); | ||
| } catch (e) {} | ||
| }), | ||
| this.observers.push(n), | ||
| a | ||
| ); | ||
| } | ||
| unsubscribeOne(e) { | ||
| void 0 !== this.observers && | ||
| void 0 !== this.observers[e] && | ||
| (delete this.observers[e], | ||
| --this.observerCount, | ||
| 0 === this.observerCount && | ||
| void 0 !== this.onNoObservers && | ||
| this.onNoObservers(this)); | ||
| } | ||
| forEachObserver(t) { | ||
| if (!this.finalized) { | ||
| for (let e = 0; e < this.observers.length; e++) { | ||
| this.sendOne(e, t); | ||
| } | ||
| } | ||
| } | ||
| sendOne(e, t) { | ||
| this.task.then(() => { | ||
| if (void 0 !== this.observers && void 0 !== this.observers[e]) { | ||
| try { | ||
| t(this.observers[e]); | ||
| } catch (e) { | ||
| 'undefined' != typeof console && console.error && console.error(e); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| close(e) { | ||
| this.finalized || | ||
| ((this.finalized = !0), | ||
| void 0 !== e && (this.finalError = e), | ||
| this.task.then(() => { | ||
| ((this.observers = void 0), (this.onNoObservers = void 0)); | ||
| })); | ||
| } | ||
| } | ||
| function v() {} | ||
| class _ { | ||
| constructor(e, t, r) { | ||
| ((this.name = e), | ||
| (this.instanceFactory = t), | ||
| (this.type = r), | ||
| (this.multipleInstances = !1), | ||
| (this.serviceProps = {}), | ||
| (this.instantiationMode = 'LAZY'), | ||
| (this.onInstanceCreated = null)); | ||
| } | ||
| setInstantiationMode(e) { | ||
| return ((this.instantiationMode = e), this); | ||
| } | ||
| setMultipleInstances(e) { | ||
| return ((this.multipleInstances = e), this); | ||
| } | ||
| setServiceProps(e) { | ||
| return ((this.serviceProps = e), this); | ||
| } | ||
| setInstanceCreatedCallback(e) { | ||
| return ((this.onInstanceCreated = e), this); | ||
| } | ||
| } | ||
| const E = '[DEFAULT]'; | ||
| class y { | ||
| constructor(e, t) { | ||
| ((this.name = e), | ||
| (this.container = t), | ||
| (this.component = null), | ||
| (this.instances = new Map()), | ||
| (this.instancesDeferred = new Map()), | ||
| (this.instancesOptions = new Map()), | ||
| (this.onInitCallbacks = new Map())); | ||
| } | ||
| get(e) { | ||
| var t = this.normalizeInstanceIdentifier(e); | ||
| if (!this.instancesDeferred.has(t)) { | ||
| const n = new s(); | ||
| if ( | ||
| (this.instancesDeferred.set(t, n), | ||
| this.isInitialized(t) || this.shouldAutoInitialize()) | ||
| ) { | ||
| try { | ||
| var r = this.getOrInitializeService({ instanceIdentifier: t }); | ||
| r && n.resolve(r); | ||
| } catch (e) {} | ||
| } | ||
| } | ||
| return this.instancesDeferred.get(t).promise; | ||
| } | ||
| getImmediate(e) { | ||
| var t = this.normalizeInstanceIdentifier( | ||
| null == e ? void 0 : e.identifier | ||
| ), | ||
| r = null !== (r = null == e ? void 0 : e.optional) && void 0 !== r && r; | ||
| if (!this.isInitialized(t) && !this.shouldAutoInitialize()) { | ||
| if (r) { | ||
| return null; | ||
| } | ||
| throw Error(`Service ${this.name} is not available`); | ||
| } | ||
| try { | ||
| return this.getOrInitializeService({ instanceIdentifier: t }); | ||
| } catch (e) { | ||
| if (r) { | ||
| return null; | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| getComponent() { | ||
| return this.component; | ||
| } | ||
| setComponent(e) { | ||
| if (e.name !== this.name) { | ||
| throw Error( | ||
| `Mismatching Component ${e.name} for Provider ${this.name}.` | ||
| ); | ||
| } | ||
| if (this.component) { | ||
| throw Error(`Component for ${this.name} has already been provided`); | ||
| } | ||
| if (((this.component = e), this.shouldAutoInitialize())) { | ||
| if ('EAGER' === e.instantiationMode) { | ||
| try { | ||
| this.getOrInitializeService({ instanceIdentifier: E }); | ||
| } catch (e) {} | ||
| } | ||
| for (var [t, r] of this.instancesDeferred.entries()) { | ||
| t = this.normalizeInstanceIdentifier(t); | ||
| try { | ||
| var n = this.getOrInitializeService({ instanceIdentifier: t }); | ||
| r.resolve(n); | ||
| } catch (e) {} | ||
| } | ||
| } | ||
| } | ||
| clearInstance(e = E) { | ||
| (this.instancesDeferred.delete(e), | ||
| this.instancesOptions.delete(e), | ||
| this.instances.delete(e)); | ||
| } | ||
| async delete() { | ||
| const e = Array.from(this.instances.values()); | ||
| await Promise.all([ | ||
| ...e.filter((e) => 'INTERNAL' in e).map((e) => e.INTERNAL.delete()), | ||
| ...e.filter((e) => '_delete' in e).map((e) => e._delete()), | ||
| ]); | ||
| } | ||
| isComponentSet() { | ||
| return null != this.component; | ||
| } | ||
| isInitialized(e = E) { | ||
| return this.instances.has(e); | ||
| } | ||
| getOptions(e = E) { | ||
| return this.instancesOptions.get(e) || {}; | ||
| } | ||
| initialize(e = {}) { | ||
| var { options: t = {} } = e, | ||
| r = this.normalizeInstanceIdentifier(e.instanceIdentifier); | ||
| if (this.isInitialized(r)) { | ||
| throw Error(`${this.name}(${r}) has already been initialized`); | ||
| } | ||
| if (!this.isComponentSet()) { | ||
| throw Error(`Component ${this.name} has not been registered yet`); | ||
| } | ||
| var n, | ||
| a, | ||
| i = this.getOrInitializeService({ instanceIdentifier: r, options: t }); | ||
| for ([n, a] of this.instancesDeferred.entries()) { | ||
| r === this.normalizeInstanceIdentifier(n) && a.resolve(i); | ||
| } | ||
| return i; | ||
| } | ||
| onInit(e, t) { | ||
| var r = this.normalizeInstanceIdentifier(t); | ||
| const n = | ||
| null !== (a = this.onInitCallbacks.get(r)) && void 0 !== a | ||
| ? a | ||
| : new Set(); | ||
| (n.add(e), this.onInitCallbacks.set(r, n)); | ||
| var a = this.instances.get(r); | ||
| return ( | ||
| a && e(a, r), | ||
| () => { | ||
| n.delete(e); | ||
| } | ||
| ); | ||
| } | ||
| invokeOnInitCallbacks(e, t) { | ||
| var r = this.onInitCallbacks.get(t); | ||
| if (r) { | ||
| for (const n of r) { | ||
| try { | ||
| n(e, t); | ||
| } catch (e) {} | ||
| } | ||
| } | ||
| } | ||
| getOrInitializeService({ instanceIdentifier: e, options: t = {} }) { | ||
| let r = this.instances.get(e); | ||
| if ( | ||
| !r && | ||
| this.component && | ||
| ((r = this.component.instanceFactory(this.container, { | ||
| instanceIdentifier: (n = e) === E ? void 0 : n, | ||
| options: t, | ||
| })), | ||
| this.instances.set(e, r), | ||
| this.instancesOptions.set(e, t), | ||
| this.invokeOnInitCallbacks(r, e), | ||
| this.component.onInstanceCreated) | ||
| ) { | ||
| try { | ||
| this.component.onInstanceCreated(this.container, e, r); | ||
| } catch (e) {} | ||
| } | ||
| var n; | ||
| return r || null; | ||
| } | ||
| normalizeInstanceIdentifier(e = E) { | ||
| return !this.component || this.component.multipleInstances ? e : E; | ||
| } | ||
| shouldAutoInitialize() { | ||
| return ( | ||
| !!this.component && 'EXPLICIT' !== this.component.instantiationMode | ||
| ); | ||
| } | ||
| } | ||
| class w { | ||
| constructor(e) { | ||
| ((this.name = e), (this.providers = new Map())); | ||
| } | ||
| addComponent(e) { | ||
| const t = this.getProvider(e.name); | ||
| if (t.isComponentSet()) { | ||
| throw new Error( | ||
| `Component ${e.name} has already been registered with ${this.name}` | ||
| ); | ||
| } | ||
| t.setComponent(e); | ||
| } | ||
| addOrOverwriteComponent(e) { | ||
| const t = this.getProvider(e.name); | ||
| (t.isComponentSet() && this.providers.delete(e.name), | ||
| this.addComponent(e)); | ||
| } | ||
| getProvider(e) { | ||
| if (this.providers.has(e)) { | ||
| return this.providers.get(e); | ||
| } | ||
| var t = new y(e, this); | ||
| return (this.providers.set(e, t), t); | ||
| } | ||
| getProviders() { | ||
| return Array.from(this.providers.values()); | ||
| } | ||
| } | ||
| const I = []; | ||
| var C, D, S; | ||
| (((D = C = C || {})[(D.DEBUG = 0)] = 'DEBUG'), | ||
| (D[(D.VERBOSE = 1)] = 'VERBOSE'), | ||
| (D[(D.INFO = 2)] = 'INFO'), | ||
| (D[(D.WARN = 3)] = 'WARN'), | ||
| (D[(D.ERROR = 4)] = 'ERROR'), | ||
| (D[(D.SILENT = 5)] = 'SILENT')); | ||
| const O = { | ||
| debug: C.DEBUG, | ||
| verbose: C.VERBOSE, | ||
| info: C.INFO, | ||
| warn: C.WARN, | ||
| error: C.ERROR, | ||
| silent: C.SILENT, | ||
| }, | ||
| A = C.INFO, | ||
| L = { | ||
| [C.DEBUG]: 'log', | ||
| [C.VERBOSE]: 'log', | ||
| [C.INFO]: 'info', | ||
| [C.WARN]: 'warn', | ||
| [C.ERROR]: 'error', | ||
| }, | ||
| N = (e, t, ...r) => { | ||
| if (!(t < e.logLevel)) { | ||
| var n = new Date().toISOString(), | ||
| a = L[t]; | ||
| if (!a) { | ||
| throw new Error( | ||
| `Attempted to log a message with an invalid logType (value: ${t})` | ||
| ); | ||
| } | ||
| console[a](`[${n}] ${e.name}:`, ...r); | ||
| } | ||
| }; | ||
| class B { | ||
| constructor(e) { | ||
| ((this.name = e), | ||
| (this._logLevel = A), | ||
| (this._logHandler = N), | ||
| (this._userLogHandler = null), | ||
| I.push(this)); | ||
| } | ||
| get logLevel() { | ||
| return this._logLevel; | ||
| } | ||
| set logLevel(e) { | ||
| if (!(e in C)) { | ||
| throw new TypeError(`Invalid value "${e}" assigned to \`logLevel\``); | ||
| } | ||
| this._logLevel = e; | ||
| } | ||
| setLogLevel(e) { | ||
| this._logLevel = 'string' == typeof e ? O[e] : e; | ||
| } | ||
| get logHandler() { | ||
| return this._logHandler; | ||
| } | ||
| set logHandler(e) { | ||
| if ('function' != typeof e) { | ||
| throw new TypeError( | ||
| 'Value assigned to `logHandler` must be a function' | ||
| ); | ||
| } | ||
| this._logHandler = e; | ||
| } | ||
| get userLogHandler() { | ||
| return this._userLogHandler; | ||
| } | ||
| set userLogHandler(e) { | ||
| this._userLogHandler = e; | ||
| } | ||
| debug(...e) { | ||
| (this._userLogHandler && this._userLogHandler(this, C.DEBUG, ...e), | ||
| this._logHandler(this, C.DEBUG, ...e)); | ||
| } | ||
| log(...e) { | ||
| (this._userLogHandler && this._userLogHandler(this, C.VERBOSE, ...e), | ||
| this._logHandler(this, C.VERBOSE, ...e)); | ||
| } | ||
| info(...e) { | ||
| (this._userLogHandler && this._userLogHandler(this, C.INFO, ...e), | ||
| this._logHandler(this, C.INFO, ...e)); | ||
| } | ||
| warn(...e) { | ||
| (this._userLogHandler && this._userLogHandler(this, C.WARN, ...e), | ||
| this._logHandler(this, C.WARN, ...e)); | ||
| } | ||
| error(...e) { | ||
| (this._userLogHandler && this._userLogHandler(this, C.ERROR, ...e), | ||
| this._logHandler(this, C.ERROR, ...e)); | ||
| } | ||
| } | ||
| const T = (t, e) => e.some((e) => t instanceof e); | ||
| let P, M; | ||
| const R = new WeakMap(), | ||
| k = new WeakMap(), | ||
| F = new WeakMap(), | ||
| $ = new WeakMap(), | ||
| j = new WeakMap(); | ||
| let H = { | ||
| get(e, t, r) { | ||
| if (e instanceof IDBTransaction) { | ||
| if ('done' === t) { | ||
| return k.get(e); | ||
| } | ||
| if ('objectStoreNames' === t) { | ||
| return e.objectStoreNames || F.get(e); | ||
| } | ||
| if ('store' === t) { | ||
| return r.objectStoreNames[1] | ||
| ? void 0 | ||
| : r.objectStore(r.objectStoreNames[0]); | ||
| } | ||
| } | ||
| return z(e[t]); | ||
| }, | ||
| set(e, t, r) { | ||
| return ((e[t] = r), !0); | ||
| }, | ||
| has(e, t) { | ||
| return ( | ||
| (e instanceof IDBTransaction && ('done' === t || 'store' === t)) || | ||
| t in e | ||
| ); | ||
| }, | ||
| }; | ||
| function V(n) { | ||
| return n !== IDBDatabase.prototype.transaction || | ||
| 'objectStoreNames' in IDBTransaction.prototype | ||
| ? (M = M || [ | ||
| IDBCursor.prototype.advance, | ||
| IDBCursor.prototype.continue, | ||
| IDBCursor.prototype.continuePrimaryKey, | ||
| ]).includes(n) | ||
| ? function (...e) { | ||
| return (n.apply(U(this), e), z(R.get(this))); | ||
| } | ||
| : function (...e) { | ||
| return z(n.apply(U(this), e)); | ||
| } | ||
| : function (e, ...t) { | ||
| var r = n.call(U(this), e, ...t); | ||
| return (F.set(r, e.sort ? e.sort() : [e]), z(r)); | ||
| }; | ||
| } | ||
| function x(e) { | ||
| return 'function' == typeof e | ||
| ? V(e) | ||
| : (e instanceof IDBTransaction && | ||
| ((i = e), | ||
| k.has(i) || | ||
| ((t = new Promise((e, t) => { | ||
| const r = () => { | ||
| (i.removeEventListener('complete', n), | ||
| i.removeEventListener('error', a), | ||
| i.removeEventListener('abort', a)); | ||
| }, | ||
| n = () => { | ||
| (e(), r()); | ||
| }, | ||
| a = () => { | ||
| (t(i.error || new DOMException('AbortError', 'AbortError')), | ||
| r()); | ||
| }; | ||
| (i.addEventListener('complete', n), | ||
| i.addEventListener('error', a), | ||
| i.addEventListener('abort', a)); | ||
| })), | ||
| k.set(i, t))), | ||
| T( | ||
| e, | ||
| (P = P || [ | ||
| IDBDatabase, | ||
| IDBObjectStore, | ||
| IDBIndex, | ||
| IDBCursor, | ||
| IDBTransaction, | ||
| ]) | ||
| ) | ||
| ? new Proxy(e, H) | ||
| : e); | ||
| var i, t; | ||
| } | ||
| function z(e) { | ||
| if (e instanceof IDBRequest) { | ||
| return (function (i) { | ||
| const e = new Promise((e, t) => { | ||
| const r = () => { | ||
| (i.removeEventListener('success', n), | ||
| i.removeEventListener('error', a)); | ||
| }, | ||
| n = () => { | ||
| (e(z(i.result)), r()); | ||
| }, | ||
| a = () => { | ||
| (t(i.error), r()); | ||
| }; | ||
| (i.addEventListener('success', n), i.addEventListener('error', a)); | ||
| }); | ||
| return ( | ||
| e | ||
| .then((e) => { | ||
| e instanceof IDBCursor && R.set(e, i); | ||
| }) | ||
| .catch(() => {}), | ||
| j.set(e, i), | ||
| e | ||
| ); | ||
| })(e); | ||
| } | ||
| if ($.has(e)) { | ||
| return $.get(e); | ||
| } | ||
| var t = x(e); | ||
| return (t !== e && ($.set(e, t), j.set(t, e)), t); | ||
| } | ||
| const U = (e) => j.get(e); | ||
| const W = ['get', 'getKey', 'getAll', 'getAllKeys', 'count'], | ||
| G = ['put', 'add', 'delete', 'clear'], | ||
| J = new Map(); | ||
| function K(e, t) { | ||
| if (e instanceof IDBDatabase && !(t in e) && 'string' == typeof t) { | ||
| if (J.get(t)) { | ||
| return J.get(t); | ||
| } | ||
| const a = t.replace(/FromIndex$/, ''), | ||
| i = t !== a, | ||
| s = G.includes(a); | ||
| if ( | ||
| a in (i ? IDBIndex : IDBObjectStore).prototype && | ||
| (s || W.includes(a)) | ||
| ) { | ||
| var r = async function (e, ...t) { | ||
| var r = this.transaction(e, s ? 'readwrite' : 'readonly'); | ||
| let n = r.store; | ||
| return ( | ||
| i && (n = n.index(t.shift())), | ||
| (await Promise.all([n[a](...t), s && r.done]))[0] | ||
| ); | ||
| }; | ||
| return (J.set(t, r), r); | ||
| } | ||
| } | ||
| } | ||
| H = { | ||
| ...(S = H), | ||
| get: (e, t, r) => K(e, t) || S.get(e, t, r), | ||
| has: (e, t) => !!K(e, t) || S.has(e, t), | ||
| }; | ||
| class Y { | ||
| constructor(e) { | ||
| this.container = e; | ||
| } | ||
| getPlatformInfoString() { | ||
| const e = this.container.getProviders(); | ||
| return e | ||
| .map((e) => { | ||
| if ( | ||
| 'VERSION' !== (null == (t = e.getComponent()) ? void 0 : t.type) | ||
| ) { | ||
| return null; | ||
| } | ||
| var t, | ||
| t = e.getImmediate(); | ||
| return `${t.library}/${t.version}`; | ||
| }) | ||
| .filter((e) => e) | ||
| .join(' '); | ||
| } | ||
| } | ||
| const X = '@firebase/app', | ||
| q = new B('@firebase/app'); | ||
| var Z; | ||
| const Q = '[DEFAULT]', | ||
| ee = { | ||
| '@firebase/app': 'fire-core', | ||
| '@firebase/app-compat': 'fire-core-compat', | ||
| '@firebase/analytics': 'fire-analytics', | ||
| '@firebase/analytics-compat': 'fire-analytics-compat', | ||
| '@firebase/app-check': 'fire-app-check', | ||
| '@firebase/app-check-compat': 'fire-app-check-compat', | ||
| '@firebase/auth': 'fire-auth', | ||
| '@firebase/auth-compat': 'fire-auth-compat', | ||
| '@firebase/database': 'fire-rtdb', | ||
| '@firebase/database-compat': 'fire-rtdb-compat', | ||
| '@firebase/functions': 'fire-fn', | ||
| '@firebase/functions-compat': 'fire-fn-compat', | ||
| '@firebase/installations': 'fire-iid', | ||
| '@firebase/installations-compat': 'fire-iid-compat', | ||
| '@firebase/messaging': 'fire-fcm', | ||
| '@firebase/messaging-compat': 'fire-fcm-compat', | ||
| '@firebase/performance': 'fire-perf', | ||
| '@firebase/performance-compat': 'fire-perf-compat', | ||
| '@firebase/remote-config': 'fire-rc', | ||
| '@firebase/remote-config-compat': 'fire-rc-compat', | ||
| '@firebase/storage': 'fire-gcs', | ||
| '@firebase/storage-compat': 'fire-gcs-compat', | ||
| '@firebase/firestore': 'fire-fst', | ||
| '@firebase/firestore-compat': 'fire-fst-compat', | ||
| 'fire-js': 'fire-js', | ||
| firebase: 'fire-js-all', | ||
| }, | ||
| te = new Map(), | ||
| re = new Map(); | ||
| function ne(t, r) { | ||
| try { | ||
| t.container.addComponent(r); | ||
| } catch (e) { | ||
| q.debug( | ||
| `Component ${r.name} failed to register with FirebaseApp ${t.name}`, | ||
| e | ||
| ); | ||
| } | ||
| } | ||
| function ae(e, t) { | ||
| e.container.addOrOverwriteComponent(t); | ||
| } | ||
| function ie(e) { | ||
| var t = e.name; | ||
| if (re.has(t)) { | ||
| return ( | ||
| q.debug(`There were multiple attempts to register component ${t}.`), | ||
| !1 | ||
| ); | ||
| } | ||
| re.set(t, e); | ||
| for (const r of te.values()) { | ||
| ne(r, e); | ||
| } | ||
| return !0; | ||
| } | ||
| function se(e, t) { | ||
| const r = e.container | ||
| .getProvider('heartbeat') | ||
| .getImmediate({ optional: !0 }); | ||
| return (r && r.triggerHeartbeat(), e.container.getProvider(t)); | ||
| } | ||
| const oe = new d('app', 'Firebase', { | ||
| 'no-app': | ||
| "No Firebase App '{$appName}' has been created - call initializeApp() first", | ||
| 'bad-app-name': "Illegal App name: '{$appName}", | ||
| 'duplicate-app': | ||
| "Firebase App named '{$appName}' already exists with different options or config", | ||
| 'app-deleted': "Firebase App named '{$appName}' already deleted", | ||
| 'no-options': | ||
| 'Need to provide options, when not being deployed to hosting via source.', | ||
| 'invalid-app-argument': | ||
| 'firebase.{$appName}() takes either no argument or a Firebase App instance.', | ||
| 'invalid-log-argument': | ||
| 'First argument to `onLog` must be null or a function.', | ||
| 'idb-open': | ||
| 'Error thrown when opening IndexedDB. Original error: {$originalErrorMessage}.', | ||
| 'idb-get': | ||
| 'Error thrown when reading from IndexedDB. Original error: {$originalErrorMessage}.', | ||
| 'idb-set': | ||
| 'Error thrown when writing to IndexedDB. Original error: {$originalErrorMessage}.', | ||
| 'idb-delete': | ||
| 'Error thrown when deleting from IndexedDB. Original error: {$originalErrorMessage}.', | ||
| }); | ||
| class ce { | ||
| constructor(e, t, r) { | ||
| ((this._isDeleted = !1), | ||
| (this._options = Object.assign({}, e)), | ||
| (this._config = Object.assign({}, t)), | ||
| (this._name = t.name), | ||
| (this._automaticDataCollectionEnabled = | ||
| t.automaticDataCollectionEnabled), | ||
| (this._container = r), | ||
| this.container.addComponent(new _('app', () => this, 'PUBLIC'))); | ||
| } | ||
| get automaticDataCollectionEnabled() { | ||
| return (this.checkDestroyed(), this._automaticDataCollectionEnabled); | ||
| } | ||
| set automaticDataCollectionEnabled(e) { | ||
| (this.checkDestroyed(), (this._automaticDataCollectionEnabled = e)); | ||
| } | ||
| get name() { | ||
| return (this.checkDestroyed(), this._name); | ||
| } | ||
| get options() { | ||
| return (this.checkDestroyed(), this._options); | ||
| } | ||
| get config() { | ||
| return (this.checkDestroyed(), this._config); | ||
| } | ||
| get container() { | ||
| return this._container; | ||
| } | ||
| get isDeleted() { | ||
| return this._isDeleted; | ||
| } | ||
| set isDeleted(e) { | ||
| this._isDeleted = e; | ||
| } | ||
| checkDestroyed() { | ||
| if (this.isDeleted) { | ||
| throw oe.create('app-deleted', { appName: this._name }); | ||
| } | ||
| } | ||
| } | ||
| const he = '9.23.0'; | ||
| function le(e, t = {}) { | ||
| let r = e; | ||
| if ('object' != typeof t) { | ||
| const a = t; | ||
| t = { name: a }; | ||
| } | ||
| var n = Object.assign({ name: Q, automaticDataCollectionEnabled: !1 }, t); | ||
| const a = n.name; | ||
| if ('string' != typeof a || !a) { | ||
| throw oe.create('bad-app-name', { appName: String(a) }); | ||
| } | ||
| if (((r = r || l()), !r)) { | ||
| throw oe.create('no-options'); | ||
| } | ||
| var i = te.get(a); | ||
| if (i) { | ||
| if (f(r, i.options) && f(n, i.config)) { | ||
| return i; | ||
| } | ||
| throw oe.create('duplicate-app', { appName: a }); | ||
| } | ||
| const s = new w(a); | ||
| for (const o of re.values()) { | ||
| s.addComponent(o); | ||
| } | ||
| n = new ce(r, n, s); | ||
| return (te.set(a, n), n); | ||
| } | ||
| async function de(e) { | ||
| var t = e.name; | ||
| te.has(t) && | ||
| (te.delete(t), | ||
| await Promise.all(e.container.getProviders().map((e) => e.delete())), | ||
| (e.isDeleted = !0)); | ||
| } | ||
| function pe(e, t, r) { | ||
| let n = null !== (i = ee[e]) && void 0 !== i ? i : e; | ||
| r && (n += `-${r}`); | ||
| var a = n.match(/\s|\//), | ||
| i = t.match(/\s|\//); | ||
| if (a || i) { | ||
| const s = [`Unable to register library "${n}" with version "${t}":`]; | ||
| return ( | ||
| a && | ||
| s.push( | ||
| `library name "${n}" contains illegal characters (whitespace or "/")` | ||
| ), | ||
| a && i && s.push('and'), | ||
| i && | ||
| s.push( | ||
| `version name "${t}" contains illegal characters (whitespace or "/")` | ||
| ), | ||
| void q.warn(s.join(' ')) | ||
| ); | ||
| } | ||
| ie(new _(`${n}-version`, () => ({ library: n, version: t }), 'VERSION')); | ||
| } | ||
| function ue(e, t) { | ||
| if (null !== e && 'function' != typeof e) { | ||
| throw oe.create('invalid-log-argument'); | ||
| } | ||
| !(function (i, e) { | ||
| for (const t of I) { | ||
| let a = null; | ||
| (e && e.level && (a = O[e.level]), | ||
| (t.userLogHandler = | ||
| null === i | ||
| ? null | ||
| : (e, t, ...r) => { | ||
| var n = r | ||
| .map((e) => { | ||
| if (null == e) { | ||
| return null; | ||
| } | ||
| if ('string' == typeof e) { | ||
| return e; | ||
| } | ||
| if ('number' == typeof e || 'boolean' == typeof e) { | ||
| return e.toString(); | ||
| } | ||
| if (e instanceof Error) { | ||
| return e.message; | ||
| } | ||
| try { | ||
| return JSON.stringify(e); | ||
| } catch (e) { | ||
| return null; | ||
| } | ||
| }) | ||
| .filter((e) => e) | ||
| .join(' '); | ||
| t >= (null !== a && void 0 !== a ? a : e.logLevel) && | ||
| i({ | ||
| level: C[t].toLowerCase(), | ||
| message: n, | ||
| args: r, | ||
| type: e.name, | ||
| }); | ||
| })); | ||
| } | ||
| })(e, t); | ||
| } | ||
| function fe(e) { | ||
| var t; | ||
| ((t = e), | ||
| I.forEach((e) => { | ||
| e.setLogLevel(t); | ||
| })); | ||
| } | ||
| const ge = 'firebase-heartbeat-database', | ||
| me = 1, | ||
| be = 'firebase-heartbeat-store'; | ||
| let ve = null; | ||
| function _e() { | ||
| return ( | ||
| (ve = | ||
| ve || | ||
| (function ( | ||
| e, | ||
| t, | ||
| { blocked: r, upgrade: n, blocking: a, terminated: i } | ||
| ) { | ||
| const s = indexedDB.open(e, t), | ||
| o = z(s); | ||
| return ( | ||
| n && | ||
| s.addEventListener('upgradeneeded', (e) => { | ||
| n(z(s.result), e.oldVersion, e.newVersion, z(s.transaction), e); | ||
| }), | ||
| r && | ||
| s.addEventListener('blocked', (e) => | ||
| r(e.oldVersion, e.newVersion, e) | ||
| ), | ||
| o | ||
| .then((e) => { | ||
| (i && e.addEventListener('close', () => i()), | ||
| a && | ||
| e.addEventListener('versionchange', (e) => | ||
| a(e.oldVersion, e.newVersion, e) | ||
| )); | ||
| }) | ||
| .catch(() => {}), | ||
| o | ||
| ); | ||
| })(ge, me, { | ||
| upgrade: (e, t) => { | ||
| 0 === t && e.createObjectStore(be); | ||
| }, | ||
| }).catch((e) => { | ||
| throw oe.create('idb-open', { originalErrorMessage: e.message }); | ||
| })), | ||
| ve | ||
| ); | ||
| } | ||
| async function Ee(e, t) { | ||
| try { | ||
| const n = await _e(), | ||
| a = n.transaction(be, 'readwrite'), | ||
| i = a.objectStore(be); | ||
| (await i.put(t, ye(e)), await a.done); | ||
| } catch (e) { | ||
| var r; | ||
| e instanceof o | ||
| ? q.warn(e.message) | ||
| : ((r = oe.create('idb-set', { | ||
| originalErrorMessage: null == e ? void 0 : e.message, | ||
| })), | ||
| q.warn(r.message)); | ||
| } | ||
| } | ||
| function ye(e) { | ||
| return `${e.name}!${e.options.appId}`; | ||
| } | ||
| class we { | ||
| constructor(e) { | ||
| ((this.container = e), (this._heartbeatsCache = null)); | ||
| var t = this.container.getProvider('app').getImmediate(); | ||
| ((this._storage = new Ce(t)), | ||
| (this._heartbeatsCachePromise = this._storage | ||
| .read() | ||
| .then((e) => (this._heartbeatsCache = e)))); | ||
| } | ||
| async triggerHeartbeat() { | ||
| const e = this.container.getProvider('platform-logger').getImmediate(); | ||
| var t = e.getPlatformInfoString(); | ||
| const r = Ie(); | ||
| if ( | ||
| (null === this._heartbeatsCache && | ||
| (this._heartbeatsCache = await this._heartbeatsCachePromise), | ||
| this._heartbeatsCache.lastSentHeartbeatDate !== r && | ||
| !this._heartbeatsCache.heartbeats.some((e) => e.date === r)) | ||
| ) { | ||
| return ( | ||
| this._heartbeatsCache.heartbeats.push({ date: r, agent: t }), | ||
| (this._heartbeatsCache.heartbeats = | ||
| this._heartbeatsCache.heartbeats.filter((e) => { | ||
| var t = new Date(e.date).valueOf(); | ||
| return Date.now() - t <= 2592e6; | ||
| })), | ||
| this._storage.overwrite(this._heartbeatsCache) | ||
| ); | ||
| } | ||
| } | ||
| async getHeartbeatsHeader() { | ||
| if ( | ||
| (null === this._heartbeatsCache && (await this._heartbeatsCachePromise), | ||
| null === this._heartbeatsCache || | ||
| 0 === this._heartbeatsCache.heartbeats.length) | ||
| ) { | ||
| return ''; | ||
| } | ||
| var e = Ie(), | ||
| { heartbeatsToSend: t, unsentEntries: r } = (function (e, t = 1024) { | ||
| const r = []; | ||
| let n = e.slice(); | ||
| for (const a of e) { | ||
| const i = r.find((e) => e.agent === a.agent); | ||
| if (i) { | ||
| if ((i.dates.push(a.date), De(r) > t)) { | ||
| i.dates.pop(); | ||
| break; | ||
| } | ||
| } else if ( | ||
| (r.push({ agent: a.agent, dates: [a.date] }), De(r) > t) | ||
| ) { | ||
| r.pop(); | ||
| break; | ||
| } | ||
| n = n.slice(1); | ||
| } | ||
| return { heartbeatsToSend: r, unsentEntries: n }; | ||
| })(this._heartbeatsCache.heartbeats), | ||
| t = a(JSON.stringify({ version: 2, heartbeats: t })); | ||
| return ( | ||
| (this._heartbeatsCache.lastSentHeartbeatDate = e), | ||
| 0 < r.length | ||
| ? ((this._heartbeatsCache.heartbeats = r), | ||
| await this._storage.overwrite(this._heartbeatsCache)) | ||
| : ((this._heartbeatsCache.heartbeats = []), | ||
| this._storage.overwrite(this._heartbeatsCache)), | ||
| t | ||
| ); | ||
| } | ||
| } | ||
| function Ie() { | ||
| const e = new Date(); | ||
| return e.toISOString().substring(0, 10); | ||
| } | ||
| class Ce { | ||
| constructor(e) { | ||
| ((this.app = e), | ||
| (this._canUseIndexedDBPromise = this.runIndexedDBEnvironmentCheck())); | ||
| } | ||
| async runIndexedDBEnvironmentCheck() { | ||
| return ( | ||
| !!(function () { | ||
| try { | ||
| return 'object' == typeof indexedDB; | ||
| } catch (e) { | ||
| return; | ||
| } | ||
| })() && | ||
| new Promise((t, r) => { | ||
| try { | ||
| let e = !0; | ||
| const n = 'validate-browser-context-for-indexeddb-analytics-module', | ||
| a = self.indexedDB.open(n); | ||
| ((a.onsuccess = () => { | ||
| (a.result.close(), e || self.indexedDB.deleteDatabase(n), t(!0)); | ||
| }), | ||
| (a.onupgradeneeded = () => { | ||
| e = !1; | ||
| }), | ||
| (a.onerror = () => { | ||
| var e; | ||
| r( | ||
| (null === (e = a.error) || void 0 === e | ||
| ? void 0 | ||
| : e.message) || '' | ||
| ); | ||
| })); | ||
| } catch (e) { | ||
| r(e); | ||
| } | ||
| }) | ||
| .then(() => !0) | ||
| .catch(() => !1) | ||
| ); | ||
| } | ||
| async read() { | ||
| return ( | ||
| ((await this._canUseIndexedDBPromise) && | ||
| (await (async function (e) { | ||
| try { | ||
| const r = await _e(); | ||
| return await r.transaction(be).objectStore(be).get(ye(e)); | ||
| } catch (e) { | ||
| var t; | ||
| e instanceof o | ||
| ? q.warn(e.message) | ||
| : ((t = oe.create('idb-get', { | ||
| originalErrorMessage: null == e ? void 0 : e.message, | ||
| })), | ||
| q.warn(t.message)); | ||
| } | ||
| })(this.app))) || { heartbeats: [] } | ||
| ); | ||
| } | ||
| async overwrite(e) { | ||
| var t; | ||
| if (await this._canUseIndexedDBPromise) { | ||
| var r = await this.read(); | ||
| return Ee(this.app, { | ||
| lastSentHeartbeatDate: | ||
| null !== (t = e.lastSentHeartbeatDate) && void 0 !== t | ||
| ? t | ||
| : r.lastSentHeartbeatDate, | ||
| heartbeats: e.heartbeats, | ||
| }); | ||
| } | ||
| } | ||
| async add(e) { | ||
| var t; | ||
| if (await this._canUseIndexedDBPromise) { | ||
| var r = await this.read(); | ||
| return Ee(this.app, { | ||
| lastSentHeartbeatDate: | ||
| null !== (t = e.lastSentHeartbeatDate) && void 0 !== t | ||
| ? t | ||
| : r.lastSentHeartbeatDate, | ||
| heartbeats: [...r.heartbeats, ...e.heartbeats], | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| function De(e) { | ||
| return a(JSON.stringify({ version: 2, heartbeats: e })).length; | ||
| } | ||
| ((Z = ''), | ||
| ie(new _('platform-logger', (e) => new Y(e), 'PRIVATE')), | ||
| ie(new _('heartbeat', (e) => new we(e), 'PRIVATE')), | ||
| pe(X, '0.9.13', Z), | ||
| pe(X, '0.9.13', 'esm2017'), | ||
| pe('fire-js', '')); | ||
| var Se = Object.freeze({ | ||
| __proto__: null, | ||
| SDK_VERSION: he, | ||
| _DEFAULT_ENTRY_NAME: Q, | ||
| _addComponent: ne, | ||
| _addOrOverwriteComponent: ae, | ||
| _apps: te, | ||
| _clearComponents: function () { | ||
| re.clear(); | ||
| }, | ||
| _components: re, | ||
| _getProvider: se, | ||
| _registerComponent: ie, | ||
| _removeServiceInstance: function (e, t, r = Q) { | ||
| se(e, t).clearInstance(r); | ||
| }, | ||
| deleteApp: de, | ||
| getApp: function (e = Q) { | ||
| var t = te.get(e); | ||
| if (!t && e === Q && l()) { | ||
| return le(); | ||
| } | ||
| if (!t) { | ||
| throw oe.create('no-app', { appName: e }); | ||
| } | ||
| return t; | ||
| }, | ||
| getApps: function () { | ||
| return Array.from(te.values()); | ||
| }, | ||
| initializeApp: le, | ||
| onLog: ue, | ||
| registerVersion: pe, | ||
| setLogLevel: fe, | ||
| FirebaseError: o, | ||
| }); | ||
| class Oe { | ||
| constructor(e, t) { | ||
| ((this._delegate = e), | ||
| (this.firebase = t), | ||
| ne(e, new _('app-compat', () => this, 'PUBLIC')), | ||
| (this.container = e.container)); | ||
| } | ||
| get automaticDataCollectionEnabled() { | ||
| return this._delegate.automaticDataCollectionEnabled; | ||
| } | ||
| set automaticDataCollectionEnabled(e) { | ||
| this._delegate.automaticDataCollectionEnabled = e; | ||
| } | ||
| get name() { | ||
| return this._delegate.name; | ||
| } | ||
| get options() { | ||
| return this._delegate.options; | ||
| } | ||
| delete() { | ||
| return new Promise((e) => { | ||
| (this._delegate.checkDestroyed(), e()); | ||
| }).then( | ||
| () => (this.firebase.INTERNAL.removeApp(this.name), de(this._delegate)) | ||
| ); | ||
| } | ||
| _getService(e, t = Q) { | ||
| var r; | ||
| this._delegate.checkDestroyed(); | ||
| const n = this._delegate.container.getProvider(e); | ||
| return ( | ||
| n.isInitialized() || | ||
| 'EXPLICIT' !== | ||
| (null === (r = n.getComponent()) || void 0 === r | ||
| ? void 0 | ||
| : r.instantiationMode) || | ||
| n.initialize(), | ||
| n.getImmediate({ identifier: t }) | ||
| ); | ||
| } | ||
| _removeServiceInstance(e, t = Q) { | ||
| this._delegate.container.getProvider(e).clearInstance(t); | ||
| } | ||
| _addComponent(e) { | ||
| ne(this._delegate, e); | ||
| } | ||
| _addOrOverwriteComponent(e) { | ||
| ae(this._delegate, e); | ||
| } | ||
| toJSON() { | ||
| return { | ||
| name: this.name, | ||
| automaticDataCollectionEnabled: this.automaticDataCollectionEnabled, | ||
| options: this.options, | ||
| }; | ||
| } | ||
| } | ||
| const Ae = new d('app-compat', 'Firebase', { | ||
| 'no-app': | ||
| "No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()", | ||
| 'invalid-app-argument': | ||
| 'firebase.{$appName}() takes either no argument or a Firebase App instance.', | ||
| }); | ||
| function Le(a) { | ||
| const i = {}, | ||
| s = { | ||
| __esModule: !0, | ||
| initializeApp: function (e, t = {}) { | ||
| var r = le(e, t); | ||
| if (u(i, r.name)) { | ||
| return i[r.name]; | ||
| } | ||
| var n = new a(r, s); | ||
| return (i[r.name] = n); | ||
| }, | ||
| app: o, | ||
| registerVersion: pe, | ||
| setLogLevel: fe, | ||
| onLog: ue, | ||
| apps: null, | ||
| SDK_VERSION: he, | ||
| INTERNAL: { | ||
| registerComponent: function (r) { | ||
| const n = r.name, | ||
| t = n.replace('-compat', ''); | ||
| { | ||
| var e; | ||
| ie(r) && | ||
| 'PUBLIC' === r.type && | ||
| ((e = (e = o()) => { | ||
| if ('function' != typeof e[t]) { | ||
| throw Ae.create('invalid-app-argument', { appName: n }); | ||
| } | ||
| return e[t](); | ||
| }), | ||
| void 0 !== r.serviceProps && c(e, r.serviceProps), | ||
| (s[t] = e), | ||
| (a.prototype[t] = function (...e) { | ||
| const t = this._getService.bind(this, n); | ||
| return t.apply(this, r.multipleInstances ? e : []); | ||
| })); | ||
| } | ||
| return 'PUBLIC' === r.type ? s[t] : null; | ||
| }, | ||
| removeApp: function (e) { | ||
| delete i[e]; | ||
| }, | ||
| useAsService: function (e, t) { | ||
| if ('serverAuth' === t) { | ||
| return null; | ||
| } | ||
| var r = t; | ||
| return r; | ||
| }, | ||
| modularAPIs: Se, | ||
| }, | ||
| }; | ||
| function o(e) { | ||
| if (((e = e || Q), !u(i, e))) { | ||
| throw Ae.create('no-app', { appName: e }); | ||
| } | ||
| return i[e]; | ||
| } | ||
| return ( | ||
| (s.default = s), | ||
| Object.defineProperty(s, 'apps', { | ||
| get: function () { | ||
| return Object.keys(i).map((e) => i[e]); | ||
| }, | ||
| }), | ||
| (o.App = a), | ||
| s | ||
| ); | ||
| } | ||
| var Ne = (function e() { | ||
| const t = Le(Oe); | ||
| return ( | ||
| (t.INTERNAL = Object.assign(Object.assign({}, t.INTERNAL), { | ||
| createFirebaseNamespace: e, | ||
| extendNamespace: function (e) { | ||
| c(t, e); | ||
| }, | ||
| createSubscribe: m, | ||
| ErrorFactory: d, | ||
| deepExtend: c, | ||
| })), | ||
| t | ||
| ); | ||
| })(); | ||
| const Be = new B('@firebase/app-compat'); | ||
| if ( | ||
| 'object' == typeof self && | ||
| self.self === self && | ||
| void 0 !== self.firebase | ||
| ) { | ||
| Be.warn(` | ||
| Warning: Firebase is already defined in the global scope. Please make sure | ||
| Firebase library is only loaded once. | ||
| `);const Pe=self.firebase.SDK_VERSION;Pe&&0<=Pe.indexOf("LITE")&&Be.warn(` | ||
| `); | ||
| const Pe = self.firebase.SDK_VERSION; | ||
| Pe && | ||
| 0 <= Pe.indexOf('LITE') && | ||
| Be.warn(` | ||
| Warning: You are trying to load Firebase while using Firebase Performance standalone script. | ||
| You should load Firebase Performance with this instance of Firebase to avoid loading duplicate code. | ||
| `)}const Te=Ne;pe("@firebase/app-compat","0.2.13",void 0);return Te.registerVersion("firebase","9.23.0","app-compat-cdn"),Te}); | ||
| `); | ||
| } | ||
| const Te = Ne; | ||
| pe('@firebase/app-compat', '0.2.13', void 0); | ||
| return (Te.registerVersion('firebase', '9.23.0', 'app-compat-cdn'), Te); | ||
| }); | ||
| //# sourceMappingURL=firebase-app-compat.js.map |
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.
🛠️ Refactor suggestion
파일 크기 및 로딩 성능 최적화 필요
현재 파일 크기가 약 1596줄로 매우 크며, 이는 초기 로딩 성능에 영향을 줄 수 있습니다.
다음과 같은 최적화를 권장합니다:
- Code Splitting:
// 필요한 Firebase 서비스만 동적 로드
const loadFirebaseAuth = () => import('firebase/auth');
const loadFirebaseFirestore = () => import('firebase/firestore');- CDN 사용:
<!-- CDN을 통한 Firebase 로드 -->
<script src="https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js"></script>- 압축 및 캐싱 설정:
// next.config.js 또는 웹서버 설정에서
module.exports = {
compress: true,
headers: [
{
source: '/firebase_sdk/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
],
};🧰 Tools
🪛 Biome (1.9.4)
[error] 10-10: Redundant use strict directive.
The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.
(lint/suspicious/noRedundantUseStrict)
[error] 154-154: e is assigned to itself.
This is where is assigned.
(lint/correctness/noSelfAssign)
[error] 163-163: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 175-175: Do not access Object.prototype method 'hasOwnProperty' from target object.
It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.
(lint/suspicious/noPrototypeBuiltins)
[error] 258-259: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 716-716: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 720-720: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 724-724: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 728-728: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 732-732: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 979-979: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 1109-1109: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 271-271: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 271-271: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 273-273: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 278-278: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 278-278: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 562-562: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 793-793: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 794-794: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 795-795: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 907-907: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1277-1277: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1277-1277: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1510-1526: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
🤖 Prompt for AI Agents
In apps/client/public/firebase_sdk/firebase-app-compat.js lines 1 to 1596, the
file size is very large, which can negatively impact initial load performance.
To fix this, implement code splitting by dynamically importing only the Firebase
services you need instead of loading the entire SDK at once. Additionally,
switch to loading Firebase via CDN scripts to leverage browser caching and
reduce bundle size. Finally, configure your web server or next.config.js to
enable compression and set long-term caching headers for the Firebase SDK files
to improve load times on repeat visits.
🛠️ Refactor suggestion
Minified 코드 사용으로 인한 유지보수성 문제
이 파일은 완전히 minified/uglified된 Firebase SDK 코드로, 다음과 같은 문제점들이 있습니다:
- 가독성 저하: 변수명이 한 글자로 압축되어 코드 이해가 어려움
- 디버깅 어려움: 스택 트레이스 분석 시 원본 코드 위치 파악 불가
- 보안 검토 제약: 코드 검토 시 실제 로직 파악이 어려움
다음과 같은 개선 방안을 권장합니다:
- Source Map 추가:
+//# sourceMappingURL=firebase-app-compat.js.map- CDN 사용 검토:
// package.json에 Firebase SDK 의존성 추가
"dependencies": {
"firebase": "^9.23.0"
}- 개발 환경용 비압축 버전 사용:
// 개발 환경에서는 원본 소스 사용
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';🧰 Tools
🪛 Biome (1.9.4)
[error] 10-10: Redundant use strict directive.
The entire contents of JavaScript modules are automatically in strict mode, with no statement needed to initiate it.
Safe fix: Remove the redundant use strict directive.
(lint/suspicious/noRedundantUseStrict)
[error] 154-154: e is assigned to itself.
This is where is assigned.
(lint/correctness/noSelfAssign)
[error] 163-163: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 175-175: Do not access Object.prototype method 'hasOwnProperty' from target object.
It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.
(lint/suspicious/noPrototypeBuiltins)
[error] 258-259: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 716-716: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 720-720: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 724-724: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 728-728: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 732-732: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 979-979: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 1109-1109: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 154-154: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 271-271: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 271-271: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 273-273: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 278-278: Shouldn't redeclare 'i'. Consider to delete it or rename it.
'i' is defined here:
(lint/suspicious/noRedeclare)
[error] 278-278: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 477-477: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 561-561: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 562-562: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 589-589: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 793-793: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 794-794: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 795-795: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 812-812: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 902-902: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 907-907: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1081-1081: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1277-1277: Shouldn't redeclare 't'. Consider to delete it or rename it.
't' is defined here:
(lint/suspicious/noRedeclare)
[error] 1277-1277: This variable is used before its declaration.
The variable is declared here:
(lint/correctness/noInvalidUseBeforeDeclaration)
[error] 1510-1526: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
🤖 Prompt for AI Agents
In apps/client/public/firebase_sdk/firebase-app-compat.js lines 1 to 1596, the
entire file is minified, causing poor readability, difficult debugging, and
limited security review. To fix this, add source maps to enable mapping minified
code back to original sources, switch to using Firebase SDK as a package
dependency in package.json instead of including minified files directly, and in
development environments, import and use the unminified Firebase modules from
the SDK to improve maintainability and debugging.
| pe('@firebase/app-compat', '0.2.13', void 0); | ||
| return (Te.registerVersion('firebase', '9.23.0', 'app-compat-cdn'), Te); |
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.
💡 Verification agent
🧩 Analysis chain
Firebase 버전 검증 필요
현재 Firebase 버전이 9.23.0으로 설정되어 있습니다. 보안 패치 및 최신 기능을 위해 버전 확인이 필요합니다.
다음 스크립트를 통해 최신 Firebase 버전을 확인하세요:
🏁 Script executed:
#!/bin/bash
# Firebase 최신 버전 확인
curl -s https://api.github.com/repos/firebase/firebase-js-sdk/releases/latest | jq '.tag_name'
# npm에서 Firebase 버전 확인
npm view firebase versionLength of output: 149
Firebase JS SDK 버전(9.23.0) → 최신(11.10.0)으로 업데이트 필요
Firebase JS SDK의 최신 릴리즈(11.10.0)가 확인되었습니다. 보안 패치와 신규 기능 적용을 위해 버전을 업데이트해주세요.
- 파일: apps/client/public/firebase_sdk/firebase-app-compat.js
- 대상 라인: 1593–1594
- pe('@firebase/app-compat', '0.2.13', void 0);
- return (Te.registerVersion('firebase', '9.23.0', 'app-compat-cdn'), Te);
+ pe('@firebase/app-compat', '0.2.13', void 0);
+ return (Te.registerVersion('firebase', '11.10.0', 'app-compat-cdn'), Te);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| pe('@firebase/app-compat', '0.2.13', void 0); | |
| return (Te.registerVersion('firebase', '9.23.0', 'app-compat-cdn'), Te); | |
| pe('@firebase/app-compat', '0.2.13', void 0); | |
| return (Te.registerVersion('firebase', '11.10.0', 'app-compat-cdn'), Te); |
🤖 Prompt for AI Agents
In apps/client/public/firebase_sdk/firebase-app-compat.js at lines 1593 to 1594,
update the Firebase JS SDK version from '9.23.0' to the latest '11.10.0'.
Replace the version string in the Te.registerVersion call to ensure the SDK uses
the most recent release with security patches and new features.
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: 2
♻️ Duplicate comments (1)
apps/client/src/pages/dashboard/dashboard.tsx (1)
91-96: 사용자 입력 검증 강화 필요이전 리뷰에서 지적된 내용과 동일한 문제가 여전히 존재하는지 확인이 필요합니다. 테스트 아티클 생성 관련 코드가 현재 파일에는 보이지 않지만, 관련 기능이 다른 곳으로 이동되었는지 확인해야 합니다.
#!/bin/bash # Description: Check if test article creation with prompt validation still exists # Expected: Find createTestArticles usage and input validation echo "=== Test article creation functions ===" rg "createTestArticles" -A 5 -B 5 echo "=== Prompt validation in dashboard files ===" rg "prompt.*카테고리.*ID" -A 5 -B 5
🧹 Nitpick comments (1)
apps/client/src/pages/dashboard/dashboard.tsx (1)
91-96: 동적 마진 계산 로직 개선 제안하드코딩된 마진 값들이 사용되고 있습니다. 유지보수성을 위해 상수화하는 것을 권장합니다.
+ const MARGIN_WITH_DAILY_REMINDERS = 'mt-[14rem]'; + const MARGIN_WITHOUT_DAILY_REMINDERS = 'mt-[7.7rem]'; const getBookmarkSectionMargin = () => { if (hasDailyReminders) { - return 'mt-[14rem]'; + return MARGIN_WITH_DAILY_REMINDERS; } - return 'mt-[7.7rem]'; + return MARGIN_WITHOUT_DAILY_REMINDERS; };
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx(2 hunks)apps/client/src/pages/dashboard/dashboard.tsx(2 hunks)apps/client/src/pages/dashboard/hooks/useAcornCount.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useArticles.ts(1 hunks)apps/client/src/shared/apis/axiosInstance.ts(2 hunks)apps/client/src/shared/components/ui/layout/Header.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/client/src/shared/components/ui/layout/Header.tsx
- apps/client/src/shared/apis/axiosInstance.ts
- apps/client/src/pages/dashboard/components/ui/banner/Banner.tsx
- apps/client/src/pages/dashboard/hooks/useArticles.ts
- apps/client/src/pages/dashboard/hooks/useAcornCount.ts
🧰 Additional context used
🧠 Learnings (1)
apps/client/src/pages/dashboard/dashboard.tsx (2)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.792Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
🧬 Code Graph Analysis (1)
apps/client/src/pages/dashboard/dashboard.tsx (3)
apps/client/src/pages/dashboard/hooks/useDashboard.ts (1)
useDashboard(6-142)apps/client/src/pages/dashboard/types/components.ts (1)
Category(24-28)apps/client/src/pages/dashboard/types/api.ts (1)
Category(10-14)
🔇 Additional comments (3)
apps/client/src/pages/dashboard/dashboard.tsx (3)
50-62: 모달 스크롤 관리 로직 승인모달 팝업 시 스크롤 고정 및 언마운트 시 복원 로직이 적절하게 구현되어 있습니다. 사용자 경험을 위한 좋은 구현입니다.
64-89: 데이터 변환 함수들의 null 안전성 확인데이터 변환 함수들이 null/undefined 값을 적절히 처리하고 있습니다. 특히 옵셔널 체이닝과 기본값 설정이 잘 되어 있습니다.
116-149: BannerSection에서 remindDateTime prop 정상 처리 확인됨BannerSectionProps 인터페이스에
remindDateTime?: string이 정의되어 있고,
BannerSection컴포넌트에서 해당 값을 하위Banner및BannerTimer컴포넌트로 올바르게 전달하는 것을 확인했습니다.
- 파일: apps/client/src/pages/dashboard/components/sections/BannerSection.tsx
- 5행:
remindDateTime?: string정의- 13행 이후:
remindDateTime을 파라미터로 받고- 24행:
<Banner … remindDateTime={remindDateTime} />₩따라서 추가 검증 없이 이 부분은 승인해도 무방합니다.
| // 토큰 체크 및 온보딩 리다이렉트 | ||
| useEffect(() => { | ||
| const token = localStorage.getItem('jwtToken'); | ||
|
|
||
| if (!token || token === 'undefined' || token === 'null' || token === '') { | ||
| navigate('/onboarding'); | ||
| return; | ||
| } | ||
|
|
||
| setIsInitialized(true); | ||
| }, [navigate, setIsInitialized]); |
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.
🛠️ Refactor suggestion
인증 로직 개선 필요
토큰 검증 로직이 기본적인 수준에만 머물러 있습니다. 보안 강화를 위해 개선이 필요합니다.
다음과 같이 개선하는 것을 권장합니다:
// 토큰 체크 및 온보딩 리다이렉트
useEffect(() => {
const token = localStorage.getItem('jwtToken');
- if (!token || token === 'undefined' || token === 'null' || token === '') {
+ if (!token || token === 'undefined' || token === 'null' || token === '' || token.trim() === '') {
navigate('/onboarding');
return;
}
+ // JWT 토큰 형식 기본 검증
+ try {
+ const parts = token.split('.');
+ if (parts.length !== 3) {
+ throw new Error('Invalid JWT format');
+ }
+ } catch (error) {
+ console.error('Invalid token format:', error);
+ localStorage.removeItem('jwtToken');
+ navigate('/onboarding');
+ return;
+ }
setIsInitialized(true);
}, [navigate, setIsInitialized]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // 토큰 체크 및 온보딩 리다이렉트 | |
| useEffect(() => { | |
| const token = localStorage.getItem('jwtToken'); | |
| if (!token || token === 'undefined' || token === 'null' || token === '') { | |
| navigate('/onboarding'); | |
| return; | |
| } | |
| setIsInitialized(true); | |
| }, [navigate, setIsInitialized]); | |
| // 토큰 체크 및 온보딩 리다이렉트 | |
| useEffect(() => { | |
| const token = localStorage.getItem('jwtToken'); | |
| if (!token || token === 'undefined' || token === 'null' || token === '' || token.trim() === '') { | |
| navigate('/onboarding'); | |
| return; | |
| } | |
| // JWT 토큰 형식 기본 검증 | |
| try { | |
| const parts = token.split('.'); | |
| if (parts.length !== 3) { | |
| throw new Error('Invalid JWT format'); | |
| } | |
| } catch (error) { | |
| console.error('Invalid token format:', error); | |
| localStorage.removeItem('jwtToken'); | |
| navigate('/onboarding'); | |
| return; | |
| } | |
| setIsInitialized(true); | |
| }, [navigate, setIsInitialized]); |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/dashboard.tsx around lines 38 to 48, the
token validation logic is too basic and insecure. Improve it by verifying the
token's validity and expiration instead of just checking for null or empty
strings. Use a JWT decoding library to decode the token and confirm it is not
expired before allowing navigation; if invalid, redirect to onboarding. This
will enhance security by ensuring only valid tokens are accepted.
| import { Header } from '@shared/components'; | ||
| import { | ||
| BannerSection, | ||
| BookmarkSection, | ||
| DailyReminderSection, | ||
| } from '@pages/dashboard/components'; | ||
| import { useDashboard } from '@pages/dashboard/hooks/useDashboard'; | ||
| import type { BookmarkCardProps } from '@pages/dashboard/mockData'; | ||
| import { mockBookmarkCards } from '@pages/dashboard/mockData'; | ||
| import { Header } from '@shared/components'; | ||
| import { useMemo, useState } from 'react'; | ||
| const CATEGORY_LIST = [ | ||
| { id: 'unread', text: '안 읽은 정보' }, | ||
| { id: 'all', text: '전체' }, | ||
| { id: 'frontend', text: '프론트엔드' }, | ||
| { id: 'backend', text: '백엔드' }, | ||
| { id: 'design', text: '디자인' }, | ||
| { id: 'devops', text: 'DevOps' }, | ||
| ]; | ||
| function getCategoryCount(cards: BookmarkCardProps[], id: string): number { | ||
| if (id === 'unread') { | ||
| return cards.filter((card) => !card.isRead).length; | ||
| } | ||
| if (id === 'all') { | ||
| return cards.length; | ||
| } | ||
| return cards.filter((card) => card.categoryId === id).length; | ||
| } | ||
| import type { Category } from '@pages/dashboard/types/api'; | ||
| import { useEffect, useState } from 'react'; | ||
| import { useNavigate } from 'react-router-dom'; |
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.
💡 Verification agent
🧩 Analysis chain
import 구문 정리 및 타입 안전성 확인
새로운 import들이 추가되었지만 구조는 깔끔합니다. 다만 Category 타입이 api.ts에서 import되고 있는데, components.ts에도 동일한 이름의 타입이 있어 혼동 가능성이 있습니다.
다음 스크립트로 타입 충돌 가능성을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# Description: Check for Category type conflicts between api.ts and components.ts
# Expected: Find usage patterns and potential conflicts
echo "=== Category type definitions ==="
ast-grep --pattern 'export interface Category {
$$$
}'
echo "=== Category type imports ==="
rg "import.*Category.*from" -A 2 -B 2Length of output: 3792
Category 타입 명확히 분리 필요
현재 @pages/dashboard/types/api.ts와 @pages/dashboard/types/components.ts에 서로 다른 구조의 Category 인터페이스가 정의되어 있어, 동일한 이름 사용 시 혼동과 타입 불일치 위험이 있습니다.
다음과 같이 조치해주세요:
- API 응답용 타입은
ApiCategory, 컴포넌트 용 타입은UiCategory등으로 명시적 네이밍 - import 시에도 alias 사용
import type { Category as ApiCategory } from '@pages/dashboard/types/api'; import type { Category as UiCategory } from '@pages/dashboard/types/components';
- API 타입 → 컴포넌트 타입 변환 유틸(매핑) 함수 도입 검토
수정이 필요한 위치:
- apps/client/src/pages/dashboard/types/api.ts (
Category정의) - apps/client/src/pages/dashboard/types/components.ts (
Category정의) - apps/client/src/pages/dashboard/dashboard.tsx (import 구문)
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/dashboard.tsx lines 3 to 12, rename the
imported Category types to distinct aliases like ApiCategory and UiCategory to
avoid confusion between the API and component Category interfaces. Update the
import statements to use these aliases from their respective files.
Additionally, in apps/client/src/pages/dashboard/types/api.ts and components.ts,
rename the Category interfaces to ApiCategory and UiCategory respectively.
Consider adding a utility function to map ApiCategory objects to UiCategory
objects for clear type conversion.
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: 4
♻️ Duplicate comments (2)
apps/client/src/pages/dashboard/apis/articleApi.ts (2)
35-35: 에러 타입 명시 필요Also applies to: 65-65
54-58: 날짜 형식 검증 필요
🧹 Nitpick comments (4)
apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
8-9: 불필요한 빈 줄을 제거하세요.코드의 가독성을 위해 불필요한 연속된 빈 줄을 제거하는 것이 좋습니다.
apps/client/src/shared/apis/axiosInstance.ts (1)
38-50: 토큰 자동 갱신 제거에 대한 고려사항이전의 토큰 자동 갱신 메커니즘이 제거되어 토큰 만료 시 사용자가 온보딩 페이지로 리다이렉트됩니다. 이는 사용자 경험에 영향을 줄 수 있으므로, 토큰 만료 전 알림이나 백그라운드 갱신 등의 대안을 고려해보세요.
apps/client/src/pages/dashboard/utils/thumbnail.ts (2)
105-107: 불필요한 continue 문을 제거하세요.catch 블록의 마지막에 있는 continue는 불필요합니다.
- } catch { - continue; - } + } catch { + // 다음 크기로 시도 + }
7-10: 메타 태그 추출 패턴 개선 제안현재 정규식은 속성 순서가 다른 경우를 처리하지 못할 수 있습니다.
const META_PATTERNS = { - ogImage: /<meta[^>]*property="og:image"[^>]*content="([^"]*)"[^>]*>/i, - twitterImage: /<meta[^>]*name="twitter:image"[^>]*content="([^"]*)"[^>]*>/i, + ogImage: /<meta[^>]*(?:property="og:image"[^>]*content="([^"]*)"|content="([^"]*)"[^>]*property="og:image")[^>]*>/i, + twitterImage: /<meta[^>]*(?:name="twitter:image"[^>]*content="([^"]*)"|content="([^"]*)"[^>]*name="twitter:image")[^>]*>/i, } as const;또한 추출 로직도 수정이 필요합니다:
const ogMatch = html.match(META_PATTERNS.ogImage); -if (ogMatch?.[1]) { - return normalizeImageUrl(ogMatch[1], baseUrl); +if (ogMatch?.[1] || ogMatch?.[2]) { + return normalizeImageUrl(ogMatch[1] || ogMatch[2], baseUrl); }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
apps/client/src/pages/dashboard/apis/articleApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/categoryApi.ts(1 hunks)apps/client/src/pages/dashboard/apis/index.ts(1 hunks)apps/client/src/pages/dashboard/apis/userApi.ts(1 hunks)apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx(2 hunks)apps/client/src/pages/dashboard/hooks/useAcornCount.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useArticles.ts(1 hunks)apps/client/src/pages/dashboard/hooks/useCategories.ts(1 hunks)apps/client/src/pages/dashboard/utils/index.ts(1 hunks)apps/client/src/pages/dashboard/utils/thumbnail.ts(1 hunks)apps/client/src/shared/apis/axiosInstance.ts(2 hunks)apps/client/src/shared/utils/index.ts(0 hunks)
💤 Files with no reviewable changes (1)
- apps/client/src/shared/utils/index.ts
✅ Files skipped from review due to trivial changes (1)
- apps/client/src/pages/dashboard/utils/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- apps/client/src/pages/dashboard/apis/index.ts
- apps/client/src/pages/dashboard/hooks/useArticles.ts
- apps/client/src/pages/dashboard/hooks/useCategories.ts
- apps/client/src/pages/dashboard/hooks/useAcornCount.ts
- apps/client/src/pages/dashboard/components/ui/cards/Thumbnail.tsx
🧰 Additional context used
🧠 Learnings (4)
apps/client/src/shared/apis/axiosInstance.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#80
File: apps/client/src/shared/components/ui/modalPop/ModalPop.tsx:36-41
Timestamp: 2025-07-15T20:00:13.720Z
Learning: In apps/client/src/shared/components/ui/modalPop/ModalPop.tsx, the InfoBox component uses hardcoded values for title, location, and icon URL as temporary test data. These should be replaced with dynamic data from props when implementing actual functionality and should be marked with TODO comments for future changes.
apps/client/src/pages/dashboard/utils/thumbnail.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
apps/client/src/pages/dashboard/apis/articleApi.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.792Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#102
File: apps/extension/src/components/modalPop/ModalPop.tsx:166-172
Timestamp: 2025-07-17T09:18:13.792Z
Learning: In apps/extension/src/components/modalPop/ModalPop.tsx, the categories array should include "안 읽은 정보" (Unread Information) as the first default category that cannot be deleted. This default category is used consistently across the client-side dashboard and should be protected from deletion in the extension as well.
🧬 Code Graph Analysis (3)
apps/client/src/pages/dashboard/apis/articleApi.ts (1)
apps/client/src/pages/dashboard/types/api.ts (6)
ApiResponse(57-61)ArticleDetail(16-24)ArticleListResponse(26-30)UnreadArticleResponse(32-35)DailyReminderArticleResponse(37-41)ReadStatusResponse(43-46)
apps/client/src/pages/dashboard/apis/categoryApi.ts (1)
apps/client/src/pages/dashboard/types/api.ts (2)
ApiResponse(57-61)CategoryListResponse(48-50)
apps/client/src/pages/dashboard/apis/userApi.ts (1)
apps/client/src/pages/dashboard/types/api.ts (2)
ApiResponse(57-61)AcornCountWithRemindResponse(52-55)
🪛 Biome (1.9.4)
apps/client/src/pages/dashboard/utils/thumbnail.ts
[error] 106-106: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
apps/client/src/pages/dashboard/apis/articleApi.ts
[error] 21-21: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
[error] 36-36: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
[error] 50-50: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
[error] 66-66: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
[error] 79-79: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
apps/client/src/pages/dashboard/apis/categoryApi.ts
[error] 19-19: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
apps/client/src/pages/dashboard/apis/userApi.ts
[error] 20-20: The catch clause that only rethrows the original error is useless.
An unnecessary catch clause can be confusing.
Unsafe fix: Remove the try/catch clause.
(lint/complexity/noUselessCatch)
🔇 Additional comments (2)
apps/client/src/shared/apis/axiosInstance.ts (1)
15-21: 토큰 유효성 검사가 개선되었습니다.토큰이 없거나 유효하지 않은 값('undefined', 'null', 빈 문자열)을 체크하는 로직이 적절합니다.
apps/client/src/pages/dashboard/utils/thumbnail.ts (1)
112-130: 프록시 서비스 보안 검증 및 환경 변수 설정 확인 필요
- 파일:
apps/client/src/pages/dashboard/utils/thumbnail.ts(112–130)- PROXY_SERVICE 선언:
현재 코드베이스 내const PROXY_SERVICE = import.meta.env.VITE_THUMBNAIL_PROXY_URL;.env*파일에서VITE_THUMBNAIL_PROXY_URL정의를 찾을 수 없습니다.
배포 및 로컬 개발 환경에서 해당 환경 변수가 올바르게 설정되어 있는지 확인하세요.프록시 서비스를 사용할 때 아래 보안 요건을 반드시 검증해 주세요:
- HTTPS 프로토콜 사용 여부
- 악성 스크립트나 콘텐츠 필터링
- 요청 당 속도 제한(rate limiting)
- 허용된(whitelisted) 도메인만 처리하도록 검증
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | ||
| const CACHE_DURATION = 24 * 60 * 60 * 1000; |
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.
🛠️ Refactor suggestion
캐시 크기 제한 추가 권장
메모리 사용량을 제어하기 위해 캐시 크기 제한을 추가하는 것이 좋습니다.
const thumbnailCache = new Map<string, { url: string; timestamp: number }>();
const CACHE_DURATION = 24 * 60 * 60 * 1000;
+const MAX_CACHE_SIZE = 1000;그리고 setCachedThumbnail 함수를 수정하세요:
const setCachedThumbnail = (url: string, thumbnailUrl: string): void => {
+ // 캐시 크기 제한
+ if (thumbnailCache.size >= MAX_CACHE_SIZE) {
+ // 가장 오래된 항목 제거
+ const oldestKey = Array.from(thumbnailCache.entries())
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)[0]?.[0];
+ if (oldestKey) {
+ thumbnailCache.delete(oldestKey);
+ }
+ }
thumbnailCache.set(url, {
url: thumbnailUrl,
timestamp: Date.now(),
});
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | |
| const CACHE_DURATION = 24 * 60 * 60 * 1000; | |
| const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); | |
| const CACHE_DURATION = 24 * 60 * 60 * 1000; | |
| const MAX_CACHE_SIZE = 1000; | |
| const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { | |
| // 캐시 크기 제한 | |
| if (thumbnailCache.size >= MAX_CACHE_SIZE) { | |
| // 가장 오래된 항목 제거 | |
| const oldestKey = Array.from(thumbnailCache.entries()) | |
| .sort((a, b) => a[1].timestamp - b[1].timestamp)[0]?.[0]; | |
| if (oldestKey) { | |
| thumbnailCache.delete(oldestKey); | |
| } | |
| } | |
| thumbnailCache.set(url, { | |
| url: thumbnailUrl, | |
| timestamp: Date.now(), | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/utils/thumbnail.ts around lines 4 to 5, the
thumbnailCache Map currently has no size limit, which can lead to uncontrolled
memory usage. Implement a maximum cache size limit and modify the
setCachedThumbnail function to check the cache size before adding new entries,
removing the oldest entries if the limit is exceeded to keep memory usage under
control.
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)
apps/client/src/pages/dashboard/utils/thumbnail.ts (1)
4-5: 캐시 크기 제한 추가 권장 (기존 제안 재확인)메모리 사용량을 제어하기 위해 캐시 크기 제한을 추가하는 것이 좋습니다. 이는 이전 리뷰에서도 제안되었던 사항입니다.
const thumbnailCache = new Map<string, { url: string; timestamp: number }>(); const CACHE_DURATION = 24 * 60 * 60 * 1000; +const MAX_CACHE_SIZE = 1000;그리고
setCachedThumbnail함수에서 캐시 크기를 확인하도록 수정하세요:const setCachedThumbnail = (url: string, thumbnailUrl: string): void => { + // 캐시 크기 제한 + if (thumbnailCache.size >= MAX_CACHE_SIZE) { + // 가장 오래된 항목 제거 + const oldestKey = Array.from(thumbnailCache.entries()) + .sort((a, b) => a[1].timestamp - b[1].timestamp)[0]?.[0]; + if (oldestKey) { + thumbnailCache.delete(oldestKey); + } + } thumbnailCache.set(url, { url: thumbnailUrl, timestamp: Date.now(), }); };
🧹 Nitpick comments (1)
apps/client/src/pages/dashboard/utils/thumbnail.ts (1)
79-149: 메인 썸네일 생성 함수의 전체적인 구조 검토함수의 전체적인 플로우는 적절하지만, 몇 가지 개선 사항을 제안합니다:
캐시된 썸네일 확인 → 파비콘 존재 확인 → 프록시를 통한 메타 이미지 추출 → 파비콘 fallback → 에러 처리의 순서가 논리적입니다.
하지만 파비콘 확인을 프록시 서비스보다 먼저 하는 것이 효율적인지 검토가 필요합니다.
파비콘 확인을 프록시 서비스 후에 수행하는 것을 고려해보세요:
const cachedThumbnail = getCachedThumbnail(url); if (cachedThumbnail) { return cachedThumbnail; } - const faviconSizes = [256, 128, 64]; - let faviconUrl: string | null = null; - - for (const size of faviconSizes) { - // ... 파비콘 확인 로직 - } if (PROXY_SERVICE) { try { const proxyUrl = `${PROXY_SERVICE}?url=${encodeURIComponent(url)}`; const response = await safeFetch(proxyUrl, 8000); if (response.ok) { const html = await response.text(); const metaImage = extractMetaImages(html, url); if (metaImage) { setCachedThumbnail(url, metaImage); return metaImage; } } } catch (proxyError) { // 프록시 서비스 실패 } } + // 메타 이미지 추출 실패 시 파비콘 fallback + const faviconSizes = [256, 128, 64]; + for (const size of faviconSizes) { + const testUrl = getFaviconUrl(domain, size); + if (testUrl) { + try { + const response = await fetch(testUrl, { method: 'HEAD' }); + if (response.ok) { + setCachedThumbnail(url, testUrl); + return testUrl; + } + } catch { + // 파비콘 URL 테스트 실패, 다음 크기로 진행 + } + } + }이렇게 하면 더 나은 품질의 메타 이미지를 우선으로 하고, 파비콘은 진짜 fallback으로 사용할 수 있습니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/client/src/pages/dashboard/utils/thumbnail.ts(1 hunks)apps/client/src/shared/apis/axiosInstance.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/shared/apis/axiosInstance.ts
🧰 Additional context used
🧠 Learnings (1)
apps/client/src/pages/dashboard/utils/thumbnail.ts (1)
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#30
File: apps/extension/src/App.tsx:10-21
Timestamp: 2025-07-08T11:47:10.642Z
Learning: In apps/extension/src/App.tsx, the InfoBox component currently uses a hardcoded external URL for the icon prop as a temporary static placeholder. The plan is to replace this with dynamic favicon extraction from bookmarked websites in future iterations.
🪛 Biome (1.9.4)
apps/client/src/pages/dashboard/utils/thumbnail.ts
[error] 106-106: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
🔇 Additional comments (11)
apps/client/src/pages/dashboard/utils/thumbnail.ts (11)
1-2: 환경 변수 설정이 적절합니다.VITE_ 접두사를 사용하여 클라이언트 환경에서 사용 가능한 환경 변수를 올바르게 설정했습니다.
7-10: 메타 패턴 정의가 적절합니다.Open Graph와 Twitter card 이미지 추출을 위한 정규식 패턴이 올바르게 정의되어 있습니다.
as const사용으로 타입 안정성도 확보했습니다.
12-18: 도메인 추출 함수가 안전하게 구현되었습니다.try-catch 블록을 사용하여 잘못된 URL 형식에 대한 에러 처리를 적절히 구현했습니다.
20-24: 이미지 URL 정규화가 올바르게 구현되었습니다.상대 경로와 절대 경로를 모두 처리하는 로직이 적절합니다.
26-38: 메타 이미지 추출 로직이 우선순위를 잘 구현했습니다.Open Graph 이미지를 우선으로 하고 Twitter 이미지를 fallback으로 사용하는 로직이 적절합니다.
40-45: 파비콘 URL 생성 함수가 안전하게 구현되었습니다.환경 변수 존재 여부를 확인하여 null을 반환하는 방어적 프로그래밍 패턴이 좋습니다.
47-60: 캐시 관련 함수들이 올바르게 구현되었습니다.TTL(Time To Live) 기반의 캐시 만료 검사와 설정 로직이 적절합니다.
62-77: 안전한 fetch 함수가 잘 구현되었습니다.AbortController를 사용한 타임아웃 처리와 적절한 헤더 설정이 포함되어 있습니다.
111-129: 프록시 서비스 처리가 안전하게 구현되었습니다.try-catch 블록으로 프록시 서비스 실패에 대한 적절한 처리가 구현되어 있습니다.
137-149: 에러 처리의 fallback 로직이 잘 구현되었습니다.메인 로직 실패 시 파비콘으로 fallback하는 이중 에러 처리가 적절합니다.
151-163: 캐시 관리 유틸리티 함수들이 유용합니다.캐시 클리어와 통계 조회 기능이 디버깅과 모니터링에 도움이 될 것입니다.
| for (const size of faviconSizes) { | ||
| const testUrl = getFaviconUrl(domain, size); | ||
| if (testUrl) { | ||
| try { | ||
| const response = await fetch(testUrl, { method: 'HEAD' }); | ||
| if (response.ok) { | ||
| faviconUrl = testUrl; | ||
| break; | ||
| } | ||
| } catch { | ||
| continue; | ||
| } | ||
| } | ||
| } |
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.
불필요한 continue 문 제거 필요
정적 분석 도구가 지적한 바와 같이 line 106의 continue 문이 불필요합니다.
} catch {
- continue;
+ // 파비콘 URL 테스트 실패, 다음 크기로 진행
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (const size of faviconSizes) { | |
| const testUrl = getFaviconUrl(domain, size); | |
| if (testUrl) { | |
| try { | |
| const response = await fetch(testUrl, { method: 'HEAD' }); | |
| if (response.ok) { | |
| faviconUrl = testUrl; | |
| break; | |
| } | |
| } catch { | |
| continue; | |
| } | |
| } | |
| } | |
| for (const size of faviconSizes) { | |
| const testUrl = getFaviconUrl(domain, size); | |
| if (testUrl) { | |
| try { | |
| const response = await fetch(testUrl, { method: 'HEAD' }); | |
| if (response.ok) { | |
| faviconUrl = testUrl; | |
| break; | |
| } | |
| } catch { | |
| // 파비콘 URL 테스트 실패, 다음 크기로 진행 | |
| } | |
| } | |
| } |
🧰 Tools
🪛 Biome (1.9.4)
[error] 106-106: Unnecessary continue statement
Unsafe fix: Delete the unnecessary continue statement
(lint/correctness/noUnnecessaryContinue)
🤖 Prompt for AI Agents
In apps/client/src/pages/dashboard/utils/thumbnail.ts between lines 96 and 109,
remove the unnecessary continue statement on line 106 inside the catch block.
Since the continue is at the end of the loop iteration, it can be safely omitted
without changing the logic.
constantly-dev
left a 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.
수고하셨습니다~~ 👍
| logApiSuccess('getArticleDetail', data); | ||
| return data; | ||
| } catch (error) { | ||
| logApiError('getArticleDetail', error); | ||
| throw error; |
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.
테스트 용으로 사용하신 log는 지워주세요!
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.
네!!
|
|
||
| export const getArticleDetail = async ( | ||
| articleId: number | ||
| ): Promise<ApiResponse<ArticleDetail>> => { |
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.
ApiResponse 커스텀 잘 해주셨네요! 다만 Axios에서 AxiosResponse라는 타입을 제공해주기도 합니다! 참고하면 좋을 것 같아요!
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.
제공하는 타입 활용해서 수정하겠습니다!
| ); | ||
| logApiSuccess('getArticlesByCategory', data); | ||
| return data; | ||
| } catch (error: any) { |
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.
error도 axiosError와 같은 타입으로 구체화하면 좋을 것 같아요!
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.
넵!!
| try { | ||
| const response = await fetch(url, { | ||
| signal: controller.signal, | ||
| headers: { 'Content-Type': 'text/html' }, |
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.
해당 headers가 필요한 것인가요??
| { | ||
| onSuccess: (response) => { | ||
| onSigninSuccess(response.data.token); | ||
| onSigninSuccess(response.data.token, email || undefined); |
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.
제가 email 빼먹었네요..!!! 굿굿 👍
- API 호출 함수에서 불필요한 try/catch 및 콘솔 로그 제거 - 썸네일 관련 유틸리티를 apis에서 utils로 분리 - Thumbnail 컴포넌트 import 경로 수정 - useAcornCount 훅에서 불필요한 try/catch 제거
- API 함수들이 AxiosResponse 타입을 반환하도록 수정 - 훅들에서 response.data로 접근하도록 수정 - 사용하지 않는 AxiosError import 제거 - 썸네일 유틸리티에서 불필요한 Content-Type 헤더 제거
constantly-dev
left a 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.
👍
* setting: create issue template * setting: create PR template * setting: update PR template * setting: create CODEOWNERS for auto reviewer * chore: 불필요한 next apps 삭제 * setting: create landing vite + react project * setting: create client vite + react project * chore: extension 폴더 생성 * setting: turbo.json 파일 설정 * setting: pnpm-workspace.yaml config 폴더 추가 * setting: package manager pnpm으로 강제하도록 설정 * setting: client app tsconfig pinback 공통 패키지로 설정 * setting: eslint 공통 pinback package로 설정 * setting: eslint 공통 pinback package로 설정 * chore: 불필요한 app 관련 파일 정리 * chore: 필요 없는 파일 제거 * chore: 필요 없는 파일 제거 * setting: design-system package 구조 생성 * setting: design-system 예시 코드 추가 * setting: landing app tsconfig pinback 공통 패키지로 설정 * setting: landing package.json client에 sync * chore: 불필요한 파일 제거 * chore: design-system test용 button 컴포넌트 추가 * setting: 증분 빌드 옵션 incremental false로 설정 * chore: html lan ko로 변경 * setting: 코드래빗 설정 파일(.coderabbit.yaml) 추가 * chore: CodeRabbit 리뷰 트리거용 주석 추가 * chore: CodeRabbit 트리거용 주석 제거 * Setting : 절대경로 설정 (#6) * setting : client 절대경로 설정 * setting : landing 절대경로 설정 * Merge pull request #16 from Pinback-Team/docs/#15/update-primitive-readme Docs(project): README ver 1.0 작성 * Update README.md * Setting(project): router 설정 (#14) * Setting : landing 라우터 설정 * Setting : client 라우터 설정 * Setting : 라우터 호출 위치 변경 * Setting(project): tailwind css setup (#10) * Setting : client tailwindcss 설치 * Setting : landing tailwind 설정 * fix : 사소한 수정 * refactor : 안쓰는 코드 제거 * Setting(project): axios query setup (#12) * setting : landing queryClient 설정 * setting : client Tanstack query 설정 * Stting : client Axios 설정 (instance) * Setting : landing Axios 설정 (instance) * refactor : 안쓰는 코드 삭제 * refactor : 안쓰는 코드 정리 * chore: 중복코드 제거 * setting(project): 깃 액션 세팅 및 vercel 배포 (#9) * setting : 깃 액션 세팅 * fix : labeler 파일 위치 수정 * fix : 주석 제거 * setting : 버전 수정 * setting : vercel 세팅 * Setting : 자동 리뷰어 할당 수정 (#22) * setting : 깃 액션 세팅 * fix : labeler 파일 위치 수정 * fix : 주석 제거 * setting : 버전 수정 * setting : vercel 세팅 * fix : 리뷰어 자동할당 본인 제거 * setting : frontend 라벨링 제거 * Setting(design-system): 디자인 시스템 토큰 & icons 설정 (#20) * setting: cn helper function 구현 * setting: .gitignore에 .tsbuildinfo 추가 * setting: cSpell pinback 및 turbo 추가 * setting: css 확장자 파일 tailwindcss로 인식되도록 설정 * setting: setting.json prettier & eslint 설정 추가 * setting: design-system styles token 설정 * setting: prettier/eslint root link * setting: 기본 폴더구조 gitkeep 추가 * chore: font token 오타 수정 * setting: font-family base pretendard 설정 * setting: svg sprite default setting * chore: code rabbit review 반영 * Setting(design-system): shadcn-ui default setting (#25) * setting: shadcn/ui default setting * chore: add pnpm-lock file * refactor: code review 반영 * chore: update pnpm-lock file * setting : 보일러플레이트 연동 * setting : tailwindCss 스타일 적용 * setting : custom.d.ts 타입 구체화 * setting : contents 스크립트 범위 수정 * setting : 폰트 1rem 기준 변경 * setting : extension 확장자 프로그램 초기 세팅 (#28) * setting : 보일러플레이트 연동 * setting : tailwindCss 스타일 적용 * setting : custom.d.ts 타입 구체화 * setting : contents 스크립트 범위 수정 * setting : 폰트 1rem 기준 변경 --------- Co-authored-by: 이재림 <[email protected]> * !HOTFIX(design-system): tailwind ds @source 추가 (#40) * Feature(extension): 토글 스위치 제작 (#39) * feat: 토글 스위치 제작 * chore: 안쓰는 코드 제거 * feat: 사이즈 분기 처리 추가 * feat: 토글 thumb 디테일 수정 * chore: 안쓰는 코드 제거 * feat: 의존성, duration 수정 * chore: design-system 패키지 export utils추가 * chore:안쓰는 코드 제거 * Feature(extension): 텍스트 필드 팝업 구현 (#32) * chore: 파일구조 세팅 * feat: ui 구현 * feat: 업데이트된 폰트 적용 * feat: 오버레이 추가 * feat: 삭제, 수정 팝업 분기처리 추가 * feat: x버튼 아이콘 추가 * chore: 안쓰는 코드 제거 * feat: 오버레이-> 박스 섀도우로 변경 * chore: 안쓰는 코드 제거 * feat: onCancel 옵셔널처리 * Feature(extension): textarea 컴포넌트 구현 (#35) * feat: textarea 구현 * feat: 스크롤바 디자인 커스텀 * chore: 안쓰는 코드 정리 * feat: 상대경로 절대경로로 변경 * feat: 글자수 제한 매직넘버 상수 정의 * feat: 유틸 경로 변경 * chore: 안쓰는 코드 삭제 * chore: 안쓰는 코드 제거 * feat: 유틸 경로 변경 * Feature(extension): 인포 박스 컴포넌트 제작 (#30) * feat : 인포 박스 컴포넌트 제작 * feat : ver->union 타입으로 수정 * fix : package.json 불필요 삭제 * fix : app.tsx 수정 사항 제거 * feat : 분기 옵션 수정 * Feature(design-system): Input 컴포넌트 구현 (#52) * feat: input 공통 컴포넌트 구현 * chore: svg.d.ts 파일 추가 * fix: error icon 오타 수정 * feat: aria-invalid 추가 * fix: design-system 내 절대 경로 추가 & tsconfig react.json으로 수정 * feat: input ref props 설정 * fix: build error 해결 * feat: input helperText error400 color로 변경 * Feature(client) useFunnel custom hook 구현 (#54) * setting: onBoarding 기본 구조 세팅 * chore: 코드 prettier 정리 * feat: useFunnel hook 구현 * fix: build error 해결 * chore: 불필요한 gitkeep 파일 삭제 * chore: tsconfig 경로 설정 수정 * refactor: Funnel 컴포넌트 fallback null 처리 추가 * Feature(design-system): dot-indicator 공통 컴포넌트 구현 (#57) * feat: dot-indicator 구현 * feat: input font token 미적용 이슈로 인한 tailwind 자체 value 사용 * Feature(client): dashboard-components (#43) * refactor: 코드 리뷰 피드백 반영 및 툴팁 간격 조정 - 레벨 데이터 중복 제거 (공통 상수로 분리) - 빈 index.ts 파일들에 실제 export 추가 - SectionTitle variant prop 구현 - Tailwind CSS 동적 클래스명 문제 해결 - 정보 아이콘과 툴팁 간격 8px로 조정 - 빌드 테스트 성공 * refactor: 대시보드 컴포넌트 리팩토링 및 코드 중복 제거 * fix: CodeRabbit 리뷰 피드백 반영 - useDashboard 훅 useCallback 의존성 배열 최적화 - AllViewButtonProps에서 사용하지 않는 children prop 제거 - styleUtils.ts CARD_CLASSES 중복 선언 문제 해결 - SectionTitle 컴포넌트 성능 최적화 및 CSS 클래스 일관성 개선 - React.memo 적용으로 불필요한 재렌더링 방지 - BookmarkCategory 컴포넌트 스타일 개선 * fix: CodeRabbit 리뷰 피드백 반영 * fix: CodeRabbit 리뷰 피드백 최종 반영 - 텍스트 색상 일관성 개선 (text-gray700 → text-gray800) - React import 추가로 타입 오류 해결 - BookmarkCategory 완전 리팩토링 (styleUtils 함수 적용) - 카테고리 스타일 유틸리티 함수 확장 - 인라인 스타일 제거로 코드 일관성 향상 * style: 코드 주석 제거 및 정리 * chore: .cursorrules를 gitignore에 추가 * fix: Git 추적에서 .cursorrules 파일 제거 * refactor: React.FC 제거 및 성능 최적화, 주석 정리 * fix: tailwindcss 중복 import 제거 * refactor: BookmarkSection 절대경로 적용 및 타입 정리 * refactor: Category 타입 분리 및 타입 시스템 개선 * refactor: Tooltip children prop required로 변경 및 사용처 수정 * refactor: 절대경로(@routes) 추가 및 관련 import 정리 * refactor: 절대경로 import 일괄 적용 및 경로 오류 수정 * refactor: 상대경로를 절대경로로 변경하고 React import 최적화 * feat: cva 라이브러리를 사용하여 variant 처리 개선 * refactor: tooltip 메시지를 constants 폴더로 분리 * refactor: Tooltip 컴포넌트의 CSS 변수를 Tailwind 클래스로 변경 * refactor: ThumbnailProps의 alt를 optional로 변경 * refactor: Thumbnail 컴포넌트에 cn 유틸리티 적용 * refactor: Props 타입을 컴포넌트 바로 위에 정의하는 컨벤션 적용 * refactor: DailyReminderCard 헤더 레이아웃을 absolute에서 flexbox로 개선 * refactor: 대시보드 컴포넌트 구조 정리 - features/dashboard 폴더를 pages/dashboard/components로 이동 * refactor: features 폴더 구조 개선 - dashboard 하위 폴더 제거 * refactor: CVA 라이브러리 적용으로 컴포넌트 스타일링 개선 * refactor: 인라인 스타일을 CVA로 변경 - 고정된 maxWidth 값 처리 * chore: 사용하지 않는 LAYOUT import 제거 (ts 경고 해결) * style: 리마인드 카드 패딩 및 스타일링 개선 * style: 카드 테두리 색상을 border-gray-200으로 통일 * style: 리마인드 카드 메모 텍스트 스타일 통일 및 디자인 토큰 적용 * refactor: 컴포넌트 구조 개선 및 디자인 시스템 통합 * test: 북마크 카드 테스트 케이스 추가 및 타이틀 스타일 개선 * Feature(client): onBoarding carousel및 story step 컴포넌트 구현 (#60) * feat: 온보딩 캐러셀 구현 * feat: funnel story step 구현 * feat: onBoarding funnel 구조 세팅 및 story step 추가 * refactor: story content 상수 분리 * refactor: story constants key에서 title로 변경 * refactor: STEP 상수 constants 폴더로 분리 * refactor: useFunnel initial step props 받도록 변경 * refactor: onboarding image alt 및 aria-label 추가 * refactor: story image depth onBoarding 추가하도록 변경 * Feature(extension): 카테고리 드롭다운 컴포넌트 구현 (#36) * feat : 드롭다운 퍼블리싱 * feat : hover 적용 처리 * feat : 아이콘 적용 * feat: 아이콘 적용 및 드롭다운 수정 * fix : 폰트 적용 수정 * feat : 클릭 후 텍스트 반영 * feat : 예시 카테고리 추가 * feat : 배열 유효성 체크 및 인덱스 고유값 * feat : 토글 아이콘 교체 * feat : medium,large 분기 처리 * feat : 다운 아이콘 추가 * feat : 아이콘 애니메이션 제거 * fix : app.tsx 수정사항 머지 충돌 해결 * feat : 아이콘 파일 수정 및 정상 적용 * feat: 아이콘 애니메이션 적용 * fix : app.tsx 상태 리버트 * fix : 아이콘 중복 이미지 삭제 및 수정 * fix: package.json 중복 제거 * feat : medium width 값 수정 * fix : console 제거 * fix : 변수 명칭 구체화 수정 * Feature(extension): 데이트 피커 컴포넌트 (#59) * feat : 인풋 컴포넌트 퍼블리싱 * feat : 공통화 후 분기 로직 분리 * feat : 시간 및 날짜 포맷팅 로직 * feat : 유효성 검사 * feat : 백스페이스 시 포맷팅 제거 * feat : 시간 유효성 추가 * feat : 수정 * refactor : 타입 파일 분리 * refactor : utils 파일로 리팩토링 * fix : app.tsx 초기 상태로 돌리기 * fix: 디자인시스템 package.json 모듈 타입 지정 * fix: 인터페이스 선언 위치 수정 * feat : constants로 상수 분리 * feat : constants 경로 지정 * Refactor(client): 파일 구조 변경 및 디자인 통일 (#65) * refactor: 대시보드 전용 컴포넌트 pages/dashboard/components로 이동 및 구조 정리 - Thumbnail, LevelInfoModal, Tooltip 대시보드 전용 폴더로 이동 - 관련 import 경로 일괄 수정 - shared/ui에서 export 및 파일 제거 - 구조 명확화로 유지보수성 향상 * refactor: 파일 구조 및 스타일 수정 * refactor: 상대경로를 절대경로로 변경하여 코드 가독성 향상 * Feat(client): SVG 파일 경로 수정 및 EmptyState 텍스트 색상 변경 * chore: public 폴더 재생성 및 vite.svg 복구 * refactor: 경로 변경 * Feature(design-systems): 공통 버튼 컴포넌트 (#63) * feat: 버튼 틀 공컴 연결 * Revert "feat: 버튼 틀 공컴 연결" This reverts commit 386d30c. * feat : 공통 버튼 공컴화 제작 * fix: 디자인 시스템 아이콘 상대경로로 수정 * feat : 사이즈에 따른 버튼 분기 cva * feat : 색상 분기 * feat : props 옵셔널 정의 제거 * feat : app.tsx 기본 형태로 돌리기 * [Refactor] 팝업 관련 컴포넌트 packages 공컴으로 마이그레이션 작업 (#68) * feat: 파일들 마이그레이션 * feat : index.ts 재정의 * fix: app.tsx 초기 상태로 수정 * Feature(extension): fcm token 발급 (#71) * feat: 파이어베이스 설치, 파일세팅 * feat: 환경변수 설정 * feat: env jason 설정 * feat: settingFCM 임포트 * feat: 수정중 * Revert "feat: 수정중" This reverts commit d123699. * feat: 필요한 파일 추가, .js->.ts 변경과 설정 변경 * feat: FCM SDK로 토큰 생성 성공 * feat: 환경변수 설정 * fix: 충돌 해결 * fix: build error fix * !HOTFIX: secret working * Feature(client): dashboard card view 구현 (#72) * feat: 북마크 카드 세션 구현 - 카테고리 필터링 기능 (BookmarkCategory) - 총 카드 개수 표시 (BookmarkCount) - 카드 그리드 4열 고정 - EmptyState 표시 - 전체보기 버튼 조건부 렌더링 (12개 이상) * feat: 데일리 리마인드 카드 세션 구현 - 타이머 기능 포함 (Timer 컴포넌트) - 카드 그리드 4열 고정 - 전체보기 버튼 조건부 렌더링 (12개 이상) - 토글 기능으로 확장/축소 가능 - 도토리 스탬프 표시 기능 * feat: 대시보드 레이아웃 및 스타일링 구현 - 최대 너비 1440px, 양쪽 패딩 설정 - 헤더 fixed 포지션, 높이 6rem - 세션 간 간격 140px, 카드 그리드 4열 고정 - 하단 패딩 36px 추가 * feat: 대시보드 데이터 및 훅 구현 - 북마크 카드 목데이터 추가 (카테고리, 읽음 상태 포함) - useDashboard 훅에서 불필요한 import 제거 - 카테고리 필터링 및 전체보기 기능 지원 * feat: API 및 타입 정의 추가 - timerApi 함수 추가 - EmptyStatePresets 업데이트 - API 타입 정의 추가 * fix: 린트 에러 수정 및 포맷팅 적용 * fix: BookmarkCardProps의 categoryId를 필수 속성으로 변경 * fix: 코드래빗 AI 봇 피드백 반영 - 안 읽은 정보 카테고리 count 값 수정 (3 → 4) - API limit 값 일관성 수정 (9 → 12) - 인터페이스 일관성 개선 (BaseCard 공통 인터페이스 추가) - 디자인 시스템 단위 일관성 수정 (px → rem) * feat: 북마크 카드 mock 데이터 주석 해제 및 대시보드 카테고리/EmptyState 로직 개선 * refactor: 카테고리 자동 생성 로직 함수화 및 불필요한 주석 제거 * fix: lint/prettier 에러 수정 및 코드 스타일 통일 * refactor: px 단위를 rem으로 통일하여 접근성 및 일관성 개선 * refactor: 중첩 삼항 연산자를 early return 패턴으로 개선하여 가독성 향상 * refactor: 조건에 의미있는 네이밍 추가 및 some 메서드 활용으로 가독성 및 성능 개선 * style: prettier 포맷팅 및 린트 자동 수정 * refactor: setIsExpanded 토글 시 prev 패턴 적용으로 상태 변경 안전성 향상 * Feature(client): onboarding time select step 컴포넌트 구현 (#73) * feat: timeSelect Step 기본 퍼블리싱 구현 * refactor: 버튼 역할 div button 태그로 변경 * feat: onBoarding header 연결 * feat: 리마인드 시간 아침/저녁 선택 로직 구현 * feat: max notice step & welcome step Funnel에 추가 * feat: useFunnel funnel내에서 boolean 분기처리 되도록 변경 * feat: mac window 구분용 useOSDetector 구현 * feat: 사용자 설정 dropdown open 로직 구현 * feat: header fixed로 변경 * Feature(extension): 익스텐션 팝업 컴포넌트 및 뷰 (#75) * fix: build 에러 상대 경로 수정 * feat : 익스텐션 medium 팝업 퍼블리싱 * feat: TimePicker에 사이즈 분기 처리 * feat: 팝업 레이아웃 수정 * feat: 팝업 헤더 분리 컴포넌트 연결 * feat: 토글 시 absolutea 처리 * feat: textarea 폰트 적용 및 placeholder * feat: 얼럿 메시지 -> 빨간 텍스트로 노출 * feat: 크롬 확장프로그램 디폴트 화면 세팅 * fix: 불필요 props 선언 제거 * feat: constants 상수 분리 * fix: firebase관련 세팅 삭제 * feat: 크롬 익스텐션 창 닫기 * feat: arial-label 추가 * feat: 아이콘 빌드파일 * Feature(client): onboarding mac notice & welcome step 컴포넌트 구현 (#77) * feat: mac notice step 구현 * feat: onboarding welcome step 컴포넌트 구현 * feat: main logo 수정 * chore: mac 분기처리 주석 제거 * refactor: img aria-label 중복 제거 * Feature(client): 대시보드용 팝업 컴포넌트 작업 및 플로우 연결 (#80) * feat: 대시보드 팝업 컴포넌트 퍼블리싱 * feat: 대시보드 팝업 토글 제어 props 연결 * feat: 카드 더보기 버튼 클릭 시, 팝업 핸들링 * feat: 모달 속 버튼 클릭시 닫기 제어 * feat: 북마크 카드 쪽 팝업닫기 제어 * feat: 북마크 카드 props 타입 통일 * feat: 팝업 바깥영역 클릭시 닫기 제어 * fix: 하드코딩 구간 표시 * fix: props에 옵셔널 조건 통일 * feat: 불필요 에러 메시지 함수 제거 및 팝업 상수 정의 * Setting(client): api 세팅 및 초기 토큰 발급 작업 (#88) * feat: api 연결 세팅 및 토큰 재발급 플로우 * fix: 콘솔 제거 * feat: 익스텐션 쪽 api 연결 확인 * feat: client api 세팅 주석 * Feature(client): design FCM token management & messaging architecture (#90) * feat: service worker register function 구현 * feat: background firebase messaging logic 구현 * setting: firebase sdk default setting * feat: FCM token 발급 test function 구현 * feat: extension google email get method 연결 * feat: 자동화 스크립트 추가 및 코드 품질 개선 - package.json에 format, lint:fix, fix-all, check-all 스크립트 추가 - 린트 및 포맷팅 자동 수정 기능 구현 - ModalPop 컴포넌트에서 불필요한 setErrorMessage prop 제거 - 코드 품질 개선 및 설정 파일 업데이트 * Feature(client): 대시보드 배너 구현 및 개발 워크플로우 개선 (#84) * feat: 대시보드 배너 및 타이머 기능 임시 저장 * feat: 배너 컴포넌트 및 관련 리소스 추가 * refactor: 배너 컴포넌트 분리 및 모달 위치 개선 * refactor: 배너 컴포넌트 UI 개선 및 중앙 정렬 적용 * feat: 카드 컴포넌트 UI 개선 - 썸네일 크기 통일 및 제목 말줄임 처리 * refactor: 배너 컴포넌트 코드 품질 개선 및 기능 최적화 * refactor: 배너 컴포넌트 폴더 구조 개선 및 코드 품질 향상 * fix: 리마인드 카드 썸네일 배경색 추가로 중앙 정렬 개선 * refactor: 배너 관련 파일 폴더 구조 개선 * cleanup: 중복 파일 제거 및 폴더 구조 정리 * fix: 썸네일 없을 때 배경 박스 제거 및 메모 텍스트 3줄 제한 강화 * fix: 린트 오류 해결 * fix: BannerHeadline 컴포넌트 수정 * fix: 헤더 로고 직접 import 방식으로 변경 및 스타일 수정 * refactor: 코드 품질 개선 및 최적화 - MAX_ACORN_COUNT 상수 추가로 마지막 단계 기준값 재사용 가능 - Timer 로직을 useTimer custom hook으로 분리 - 타입 추론 활용으로 제네릭 타입 제거 - API 응답에서 구조 분해 할당 사용 - 충돌하는 max-height 스타일 수정 - 조건부 스타일링 개선 * fix: TimePicker 컴포넌트에서 불필요한 setErrorMessage prop 제거 * chore: prettier 및 린트 포맷팅 반영 * Feature(client): 온보딩 ui 퍼블리싱 (#89) * feat: 다이얼 ui 구현 * feat: border 커스텀 * feat: ui 커스텀 * feat: ui 완성 * feat: 사용자 설정 시간 저장 * chore: 안쓰는 코드 삭제 * feat: 온보딩 사용자 설정 추가 * fix : 버블링 해결 * feat: 시간 선택 포맷 개선 및 팝업 토글 함수 수정 * feat: 사용자 설정 시간 선택 기능 개선 및 팝업 상태 반영 * feat: 팝업 외부 클릭 감지 기능 추가 및 사용자 설정 시간 선택 UI 개선 * feat: 온보딩 레이아웃 정렬, 프리셋 시간 선택 기능 개선 * feat: 버튼 컴포넌트 제작 * feat: 온보딩 단계에서 네비게이션 버튼 컴포넌트 추가 및 시간 선택 버튼 개선 * feat: 사용자 설정 시간 선택 UX 수정 * feat: 시간 선택 기능 개선 및 포맷 유틸리티 추가+ 버튼 레이아웃 * feat: 팝업 z인덱스 * fix: ts env 추적 못하는 문제 해결 * chore: 필요 없는 함수 주석 처리 * refactor: color 하드 코딩된 값 수정 * chore: props type interface로 변경 * feat: mac user button 분기 처리 & 토큰 발급 로직 수정 * chore: prettier 정리.. * Setting(extension): 익스텐션 쪽 API 세팅 및 client랑 데이터 연결 작업 (#93) * feat: api 연결 세팅 및 토큰 재발급 플로우 * fix: 콘솔 제거 * feat: 익스텐션 쪽 api 연결 확인 * feat: client api 세팅 주석 * feat: client->extension 토큰 전달 로직 * refactor: utils함수로 분리 * feat: 이메일 fetch 및 랜딩 연결 * fix: 콘솔 및 주석 제거 * Feature(extension): 북마크 저장 및 API 연결 작업 (#87) * setting: 익스텐션 세팅 수정 * feat: 북마크 저장 로직 구현 연습 * feat: url에 따른 썸네일 추적 * feat: 북마크 저장 기능 구현 * feat: 북마크 저장 시 창 닫기 * feat: 이미지 연결 * feat: 북마크 정보 팝업에 연동 * feat: 텍스트 포맷 및 이미지 연동 * feat: 북마크 제대로 저장 수정 * fix: 주석 제거 * feat: 텍스트 없으면 익스텐션 실행 끄기 * fix: 콘솔 제거 * feat: 크롬 도메인일 경우에는 꺼지지 않게 제어 * feat: api 테스트 코드 제거 * fix: 대시보드 누락 사항 복구 * fix: 데이터 누락 추가 * Api(client): 회원 가입 api 연결 (#97) * chore: signup 오타 수정 * feat: signup axios fetch 함수 구현 * feat: signup query 함수 구현 * api: signup api 연결 * feat: signup query onSuccess token localStorage 추가 로직 구현 * feat: next button step 변하는 로직 onSuccess로 이동 * feat: signup success시 localStorage 저장 및 send extension 로직 추가 * Feature(extension): modalpop의 카테고리 드롭다운에 popup 연결 (#102) * feat: 버튼 공통 컴포넌트 연결 * feat: input 아이콘 컬러 변경 * feat: 버튼 비활성화 * feat: 추가하기 팝업 연결 * feat: 카테고리 수정하기 팝업 연결 * chore: 안쓰는 코드 삭제 * feat: 이벤트 전파방지 코드 * chore: 안쓰는 코드 제거 * Api(client): 대시보드 api 연결 (#101) * feat: 대시보드 API 연동 # Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx # apps/client/src/pages/onBoarding/OnBoarding.tsx * refactor: api 타입 분리 및 import 경로 일괄 수정 # Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx * fix: 이메일 하드코딩 환경변수 처리 및 fallback 적용 # Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx * fix: 썸네일 캐싱 및 에러 처리 개선, 환경변수 적용 * refactor: 썸네일 API를 대시보드 전용 폴더로 이동 및 구조 정리 * refactor: 대시보드 코드 정리 및 최적화 - TestSection 컴포넌트 삭제 - 테스트 버튼 UI 제거 - useDashboard 훅에서 테스트 관련 함수 제거 - 도토리 개수 업데이트 로직 개선 - 배너 렌더링 디버깅 로그 추가 - 썸네일 화질 개선 (64px → 128px) - api.allorigins.win 프록시 서비스 제거 - TypeScript 오류 수정 # Conflicts: # apps/client/src/pages/dashboard/dashboard.tsx * fix: ESLint 오류 수정 및 코드 정리 - 빈 함수 오류 수정 (empty arrow function) - non-null assertion 오류 수정 - 디버깅용 console.log 제거 - 코드 포맷팅 적용 * fix: 카드 더보기 버튼 이벤트 중첩 해결 및 토큰 관리 개선 - BookmarkCard, DailyReminderCard에서 더보기 버튼 클릭 시 e.stopPropagation() 추가 - DailyReminderCard 스탬프 오버레이에 pointer-events-none 추가 - Dashboard 모달 팝업 시 스크롤 고정 로직 추가 - axiosInstance에서 하드코딩된 토큰 발급 로직 제거 - 온보딩에서 저장된 토큰만 사용하도록 수정 - 대시보드에서 토큰이 없으면 온보딩으로 리다이렉트하는 로직 추가 * fix: 대시보드 토큰 통합 및 이벤트 처리 문제 수정 * fix: 콘솔 로그 및 errorUtils 파일 제거 및 개발자 테스트용 API 및 관련 코드 제거 * fix: api 폴더에서 썸네일 유틸리티를 utils 폴더로 분리 * fix: TypeScript 에러 수정 * fix: 주석 정리 및 간소화 * fix: axiosInstance에서 온보딩 토큰 사용하도록 수정 * fix: 주석 지우기 * fix: 주석 정리 * refactor: API 호출 함수 정리 및 썸네일 유틸리티 분리 - API 호출 함수에서 불필요한 try/catch 및 콘솔 로그 제거 - 썸네일 관련 유틸리티를 apis에서 utils로 분리 - Thumbnail 컴포넌트 import 경로 수정 - useAcornCount 훅에서 불필요한 try/catch 제거 * refactor: AxiosResponse 타입 활용 및 코드 개선 - API 함수들이 AxiosResponse 타입을 반환하도록 수정 - 훅들에서 response.data로 접근하도록 수정 - 사용하지 않는 AxiosError import 제거 - 썸네일 유틸리티에서 불필요한 Content-Type 헤더 제거 * Feature(client): dashboard popup에 추가 popup 연결 (#105) * feat: client dashboard modalpop에 추가 popup 연결 * feat: popup overlay z-index 임시 수정 * feat: popup overlay 적용 * feat: url title & location truncate 처리 * Feature(onboarding): story 변경사항 반영 (#113) * feat: 스토리 변경+헤더 프롭스 변경 * feat: 디테일 반영 * chore: 프리티어 적용 * Feature(landing): UI 구현과 로띠 적용 (#98) * Feature(landing): 홈 페이지 섹션 구성 요소 추가 * feat: landing 로띠 추가 * feat: 스크롤 효과 적용 * feat: 로띠 루프 설정 * feat: 로띠 추가 * feat: 버튼 컴포넌트 추가 * chore: 스타일 수정 * feat: 코드리뷰 반영 * feat: 텍스트 추가 * feat: 최종 수정 반영 * fix: 이름 자동 생성 오류 변경 * chore: 프리티어 수정 * Api(extension): 익스텐션 북마크 아티클 저장 API 연결 (#107) * feat: 크롬 스토리지에 저장 * feat: 모달 레이아웃 조정 및 색 적용 * feat: 카테고리 조회 및 가져오기 * fix: 드롭다운 초기값 설정 * feat: 포맷 함수+리마인드 get 연결 * feat: 북마크 아티클 api연결 * fix: 불필요 api 제거 * feat: 포맷 시간 추가 * fix: 콘솔 제거 --------- Co-authored-by: jjangminii <[email protected]> * Api (extension): 익스텐션 팝업 수정/삭제/생성 Edit 기능 API 연결 (#114) * feat: 크롬 스토리지에 저장 * feat: 모달 레이아웃 조정 및 색 적용 * feat: 카테고리 조회 및 가져오기 * fix: 드롭다운 초기값 설정 * feat: 포맷 함수+리마인드 get 연결 * feat: 북마크 아티클 api연결 * fix: 불필요 api 제거 * feat: 포맷 시간 추가 * fix: 콘솔 제거 * feat: 카테고리 생성 api * feat: 카테고리 수정 api * fix: 에러 던지기 * fix: 에러 얼럿 추가 * feat: 카테고리 삭제 api 연결 * feat: 토큰 연결 및 받아오기 * fix: 콘솔제거 * fix: lint 수정 --------- Co-authored-by: jjangminii <[email protected]> * Api(client) 대쉬보드 팝업 상세 조회/카테고리 조회/삭제 API 연결 (#115) * api: dashboard axios fetch 함수 구현 * api: dashboard query 함수 구현 * chore: query import 경로 수정 * chore: 불필요한 파일 삭제 * api: dashboard api 연결 * feat: og proxy 우회 로직 구현 * chore: 필요없는 코드 제거 * chore: pnpm lock sync * !HOTFIX: infoBox edit * Refactor(client): 1차 개발 QA (#118) * chore: level modal scroll bar 덜그럭 문제 해결 * feat: 아티클 삭제 query invalidate 적용 * Refactor: 개발 2차 QA 작업 (#119) * feat: 대시보드 카테고리 수정 및 삭제 api * feat: 토글 버튼 zindex 제어 * feat: 아티클 수정 patch api 연결 * feat: 카테고리 생성하기 * fix: lint 수정 * fix: lint dpfj * feat: 저장 시 모달 닫기 * fix: err lint 수정 * feat: 시간 포맷팅 수정 * feat: 모달 닫기 로직 --------- Co-authored-by: constantly-dev <[email protected]> Co-authored-by: karnelll <[email protected]> Co-authored-by: KIM JEONGMIN <[email protected]> Co-authored-by: 이재림 <[email protected]> Co-authored-by: SEOHEE CHOI <[email protected]> Co-authored-by: jjangminii <[email protected]>
📌 Related Issues
✅ 체크 리스트
📄 Tasks
🔧 대시보드 API 연동 및 타입 구조 리팩토링
API 연동 작업:
userApi.ts- 도토리 개수 및 리마인더 시간 조회articleApi.ts- 아티클 목록, 상세, 읽음 상태 업데이트categoryApi.ts- 카테고리별 아티클 조회thumbnail.ts- 아티클 썸네일 생성 및 조회주요 API 기능:
getAcornCountWithRemind(): 사용자 도토리 개수 및 다음 리마인더 시간 조회getArticlesByCategory(): 카테고리별 아티클 목록 조회 (페이지네이션 지원)getUnreadArticles(): 읽지 않은 아티클 목록 조회getDailyReminderArticles(): 데일리 리마인더 아티클 조회updateArticleReadStatus(): 아티클 읽음 상태 업데이트 (도토리 수집)createTestArticles(): 테스트용 아티클 생성썸네일 API 연동 상세
썸네일 생성 로직:
썸네일 추출 우선순위:
og:image메타 태그)twitter:image메타 태그)기술적 구현:
https://api.allorigins.win/raw사용하여 CORS 우회https://www.google.com/s2/favicons활용에러 처리:
성능 최적화:
타입 구조 개선:
apps/client/src/pages/dashboard/apis/api.ts→apps/client/src/pages/dashboard/types/api.ts로 타입 파일 이동types폴더로 통합하여 구조 개선수정된 파일:
articleApi.ts,userApi.ts,categoryApi.ts- API 타입 import 경로 수정BookmarkSection.tsx,DailyReminderSection.tsx- 컴포넌트 타입 import 경로 수정useDashboard.ts,dashboard.tsx- 훅 및 메인 컴포넌트 타입 import 경로 수정개선 효과:
⭐ PR Point (To Reviewer)
리뷰 포인트
types폴더로 이동한 것이 적절한지 확인추가 작업 사항
@shared/types에서 대시보드 전용 타입들을 분리하여 대시보드 독립성 확보썸네일 API 주요 특징
API 연동 주요 특징
📷 Screenshot
변경 전:
변경 후:
썸네일 생성 플로우:
API 연동 구조:
Summary by CodeRabbit
새로운 기능
버그 수정
리팩터링 및 구조 개선
스타일
문서화 및 타입