Feat(extension): 익스텐션 페이지 레이아웃 및 북마크 저장#64
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough크롬 익스텐션 매니페스트/빌드 설정을 갱신하고 팝업 UI에 북마크 저장·카테고리·리마인더 기능을 추가했으며 OG 메타 수집(유틸 + background 메시지 핸들러)과 관련 훅/저장 로직을 도입했습니다. 디자인 시스템에는 입력·팝업·날짜/시간·휠피커 API 확장 및 검증 유틸이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant Popup as Extension Popup (App)
participant Hook as usePageMeta
participant Chrome as chrome.tabs
participant Util as OgImageFetcher
User->>Popup: 팝업 열기
Popup->>Hook: 마운트 시 메타 요청
Hook->>Chrome: query({ active: true, currentWindow: true })
Chrome-->>Hook: 현재 탭 URL
Hook->>Util: OgImageFetcher({ url })
Util-->>Hook: {title, description, siteName, image, url} 또는 null
Hook-->>Popup: meta 상태 업데이트
sequenceDiagram
autonumber
participant CS as Content/Popup Script
participant BG as Background Service Worker
participant Net as Network
CS->>BG: postMessage { type: FETCH_OG_META, url }
BG->>Net: fetch(url)
alt 성공
Net-->>BG: HTML
BG->>BG: DOMParser로 OG 추출
BG-->>CS: { title, description, siteName, image, url }
else 실패
BG-->>CS: null
end
note over BG,CS: 비동기 sendResponse 사용 (listener returns true)
sequenceDiagram
autonumber
actor User as 사용자
participant UI as Popup(App)
participant Save as useSaveBookmark
participant Store as chrome.storage.local
participant BM as chrome.bookmarks
User->>UI: 저장 클릭
UI->>Save: save(params)
Save->>Store: get(['bookmarks'])
Store-->>Save: 기존 목록(or 빈 배열)
Save->>Store: set({ bookmarks: [..., saveData(createdAt)] })
Save->>BM: bookmarks.create({ parentId: '1', title, url })
BM-->>Save: 생성 결과
Save-->>UI: 완료/오류 반환
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (26)
packages/design-system/src/components/wheelPicker/WheelPicker.tsx (2)
15-24: 컴포넌트 props 범위 점검 필요(패스스루 제약).명시 인터페이스로 안정성은 좋아졌지만,
id,data-*, 추가aria-*등 전달이 필요한 속성이 누락될 수 있습니다. 의도대로 “표면 API 최소화”라면 OK이지만, 프리미티브에 전달되어야 하는 접근성/추적 속성이 있다면 확장 방식(Omit + 확장)으로 전환을 고려해 주세요.
41-61:onValueChange가 DOM으로 새어 나갈 가능성 제거.라이브러리가
onValueChange를 인식하지 않으면 루트 DOM에 전달되어 경고가 날 수 있습니다. 하나의 핸들러로 정규화해onChange만 넘기세요.-function WheelPicker({ - classNames, - options, - ...props -}: WheelPickerComponentProps) { +function WheelPicker({ + classNames, + options, + onChange, + onValueChange, + ...rest +}: WheelPickerComponentProps) { return ( <WheelPickerPrimitive.WheelPicker options={options} + onChange={onChange ?? onValueChange} classNames={{ ...classNames, optionItem: cn( 'text-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]', classNames?.optionItem ), highlightWrapper: cn( 'bg-white border-y-[0.5px] border-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]', classNames?.highlightWrapper ), }} - {...props} + {...rest} /> ); }apps/extension/vite.config.ts (1)
31-36: 중복 alias 정의 제거
apps/extension/tsconfig.json의paths에 이미"@assets/*": ["src/assets/*"]매핑이 존재하므로, vite.config.ts에서 수동으로 정의한@assets별칭을 제거해 중복을 해소하십시오.apps/extension/src/content.ts (1)
1-1: lint 경고: console.log는 개발 모드에서만 노출프로덕션 번들에 불필요한 로그가 남지 않도록 모드 가드가 좋겠습니다.
-console.log('컨텐츠 스크립트 로드됨'); +if (import.meta.env.MODE === 'development') { + console.debug('컨텐츠 스크립트 로드됨'); +}packages/design-system/src/components/infobox/InfoBox.tsx (2)
12-14: 텍스트 박스 고정폭 180px → 유연한 레이아웃으로 트렁케이션 보장고정폭만 두면 컨테이너 폭 변경 시 줄바꿈/흐름이 깨질 수 있습니다. flex 컨테이너에서 min-w-0을 주고 최대폭만 제한하는 쪽이 안전합니다.
- <p className="caption2-m text-font-gray-3 w-[180px] truncate"> + <p className="caption2-m text-font-gray-3 min-w-0 max-w-[180px] truncate"> {source} </p>
9-9: img 접근성/빈 URL 처리imgUrl이 없을 때 빈 src 요청(…/undefined)이 발생합니다. 또한 alt 속성, lazy 로딩을 추가해 주세요.
예시:
{imgUrl ? ( <img className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem]" src={imgUrl} alt={title} loading="lazy" referrerPolicy="no-referrer" /> ) : ( <div className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem] bg-gray200" aria-hidden="true" /> )}packages/design-system/src/components/textarea/Textarea.tsx (1)
13-21: Controlled/Uncontrolled 경계 안전화: value undefined 전달value가 undefined일 때는 속성을 아예 생략해야 React 경고(제어/비제어 전환)를 피할 수 있습니다.
- value={value} + value={value ?? undefined}apps/extension/src/utils/OGFetch.ts (1)
16-27: 메타 추출 보강: name 속성/타이틀/트위터 카드로 폴백많은 사이트가 property 대신 name을 사용하거나 OG가 비어 있습니다. 폴백을 추가하면 수집 성공률이 크게 올라갑니다.
- const getMeta = (property: string) => - doc - .querySelector(`meta[property="${property}"]`) - ?.getAttribute('content') || ''; + const getAttr = (sel: string, attr = 'content') => + doc.querySelector(sel)?.getAttribute(attr) || ''; + const getMeta = (key: string) => + getAttr(`meta[property="${key}"]`) || + getAttr(`meta[name="${key.replace('og:', '')}"]`) || + getAttr(`meta[name="${key}"]`) || ''; @@ - title: getMeta('og:title'), - description: getMeta('og:description'), - siteName: getMeta('og:site_name'), - image: getMeta('og:image'), - url: getMeta('og:url'), + title: getMeta('og:title') || doc.querySelector('title')?.textContent?.trim() || '', + description: getMeta('og:description') || getAttr('meta[name="description"]'), + siteName: getMeta('og:site_name'), + image: getMeta('og:image') || getMeta('twitter:image'), + url: getMeta('og:url') || url,apps/extension/src/utils/ValidateData.ts (2)
40-45: 시간 범위 체크는 그대로 유지하되 분은 2자리로 고정하는 것이 안전합니다.
현재 정규식이 1자리 분도 허용합니다. UI가 "HH:MM" 포맷이면 2자리 분을 강제하세요.- const hour = parseInt(match[1], 10); - const minute = parseInt(match[2], 10); + const hour = parseInt(match[1], 10); + const minute = parseInt(match[2], 10);(정규식 수정으로 00–59만 매칭되므로 하단 조건은 동일하게 유지 가능)
2-6: 미세 개선: 트림 후 검사.앞뒤 공백("2025.09.09 ")으로 인한 실패를 방지하려면 트림 후 매칭하세요.
-export const validateDate = (value: string): string => { - const regex = /^(\d{4})\.(\d{2})\.(\d{2})$/; - const match = value.match(regex); +export const validateDate = (value: string): string => { + const s = value.trim(); + const regex = /^(\d{4})\.(\d{2})\.(\d{2})$/; + const match = s.match(regex);apps/extension/tsconfig.json (1)
19-19: 파일명 대소문자 강제는 켜두는 것이 안전합니다.다른 OS 간(Windows/macOS ↔ Linux)에서 케이스 미스매치로 런타임 오류가 납니다.
- "forceConsistentCasingInFileNames": false, + "forceConsistentCasingInFileNames": true,apps/extension/src/hooks/useSaveBookmarks.ts (4)
45-48: 닫기 타이밍을 비동기 완료 후로 이동하고 에러 메시지는 사용자에게 피드백 되게 처리하세요.현재는 생성 요청 직후 창을 닫습니다. 생성 실패 시 사용자 인지가 불가합니다.
- window.close(); - } catch (error) { - console.error('저장 중 오류:', error); + window.close(); + } catch (error) { + console.error('저장 중 오류:', error); + // TODO: UI 토스트/배너로 사용자에게 안내
21-23: 콘솔 사용과 any 제거.ESLint 경고가 있습니다. 개발 환경에서만 로깅하고, storage 타입을 명시하세요.
- console.log('저장 데이터:', saveData); + if (process.env.NODE_ENV !== 'production') { + console.log('저장 데이터:', saveData); + }- const result = await new Promise<{ bookmarks?: any[] }>((resolve) => { + type Stored = { bookmarks?: SaveBookmarkParams[] }; + const result = await new Promise<Stored>((resolve) => { chrome.storage.local.get(['bookmarks'], (items) => resolve(items)); });
14-19: URL 유효성 및 중복 방지 체크를 추가하세요.잘못된 URL 혹은 동일 URL 중복 저장을 예방하면 품질이 높아집니다.
const save = async (params: SaveBookmarkParams) => { try { + try { + // 유효한 http/https만 허용 + const u = new URL(params.url); + if (!/^https?:$/.test(u.protocol)) throw new Error('unsupported scheme'); + } catch { + throw new Error('유효한 URL이 아닙니다.'); + } + // 중복 여부 확인 + const dup = await chrome.bookmarks.search({ url: params.url }); + if (dup?.length) { + // TODO: UI에서 중복 안내/덮어쓰기 선택지 제공 + }
1-11: 타입 확장 제안: 저장 스키마에 id/createdAt 등 명시.스토리지 구조를 고정하면 후속 마이그레이션이 수월합니다.
interface SaveBookmarkParams { url: string; title: string; description: string; imgUrl: string; memo: string; isRemindOn: boolean; selectedCategory: string | null; date: string | null; time: string | null; } +type SavedBookmark = SaveBookmarkParams & { createdAt: string };apps/extension/src/background.ts (1)
1-1: 불필요한 콘솔 로그 제거.릴리즈 번들에 잔류하지 않도록 제거하거나 NODE_ENV로 가드하세요.
-console.log('백그라운드 기능'); +// console.debug('background ready'); // dev onlyapps/extension/popup.html (2)
2-2: lang 속성 'en' → 한국어 페이지에 맞게 'ko-KR'로UI 텍스트가 한국어이므로 접근성과 번역 도구를 위해 lang을 ko-KR로 지정하는 것이 맞습니다.
-<html lang="en"> +<html lang="ko-KR">
3-6: viewport 메타태그 추가 권장확장 팝업이라도 HiDPI·줌 환경에서 안정적 렌더링을 위해 viewport를 명시하세요.
<head> <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>PinBack Extension</title> </head>apps/extension/manifest.json (1)
9-9: "scripting" 권한 제거
apps/extension/manifest.json:9 에서 불필요한"scripting"권한을 제거하세요.apps/extension/src/App.tsx (4)
39-41: 리마인드 OFF 시 에러 상태 초기화스위치를 끄면 에러 메시지와 값도 함께 무효화/초기화하는 편이 UX에 좋습니다.
const handleSwitchChange = (checked: boolean) => { setIsRemindOn(checked); + if (!checked) { + setDateError(''); + setTimeError(''); + } };
23-26: 기본 날짜/시간 하드코딩 값 제거 권장기본값이 미래 날짜로 고정되어 있어 시점이 지나면 즉시 에러가 뜰 수 있습니다. 리마인드 OFF가 기본이므로 빈 문자열로 두고, ON에서만 요구/검증하는 편이 안전합니다.
- const [date, setDate] = useState('2025.10.10'); - const [time, setTime] = useState('19:00'); + const [date, setDate] = useState(''); + const [time, setTime] = useState('');
100-110: 카테고리 옵션 하드코딩 → TODO 명시 또는 실제 소스 연동임시 값이라면 TODO를 남기고, 아니면 storage/API에서 불러오도록 훗날 훅으로 분리 제안.
- <Dropdown + {/* TODO: 카테고리 옵션 실제 데이터 소스로 교체 */} + <Dropdown options={['옵션1', '옵션2']}
90-92: 로고 클릭으로 '카테고리 추가'는 발견성 낮음FAB/텍스트 버튼 등 명시적 트리거가 UX에 더 직관적입니다. 로고 클릭은 유지해도 보조 트리거 추가 권장.
apps/extension/src/hooks/usePageMeta.ts (3)
19-27: 언마운트 후 setState 호출 가능성 — 취소 플래그로 메모리 누수/경고 방지비동기 흐름에서 팝업이 빨리 닫히면 언마운트 후
setMeta/storage.set이 호출될 수 있습니다. 간단한 취소 플래그를 추가해 주세요.- useEffect(() => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + useEffect(() => { + let canceled = false; + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const activeTab = tabs[0]; if (!activeTab?.url) return; @@ - setMeta(newMeta); - - chrome.storage.local.set({ titleSave: newMeta.title }); + if (canceled) return; + setMeta(newMeta); + chrome.storage.local.set({ titleSave: newMeta.title }); }); - }, []); + return () => { + canceled = true; + }; + }, []);Also applies to: 48-52
24-27: UX: OG 수집 대기 없이 즉시 기본 메타 세팅OG 수집 전에도 탭의 URL/제목(권한 보유 시)을 먼저 표시하면 깜빡임이 줄어듭니다.
const currentUrl = activeTab.url; chrome.storage.local.set({ bookmarkedUrl: currentUrl }); + + // 즉시 기본 메타 반영 (OG 수집 전 프리세트) + setMeta((prev) => ({ + ...prev, + url: currentUrl, + title: activeTab.title ?? prev.title, + }));
50-51: 스토리지 키 네이밍/스키마
titleSave는 의미가 모호합니다. 차후 확장성을 위해 단일 객체로 캐싱하거나 명확한 키 네이밍을 권장합니다. 예:bookmark.meta.title또는pageMetaCache: PageMeta.- chrome.storage.local.set({ titleSave: newMeta.title }); + chrome.storage.local.set({ pageMetaCache: newMeta });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
apps/extension/public/icon.pngis excluded by!**/*.pngapps/extension/public/icon128.pngis excluded by!**/*.pngapps/extension/src/assets/logo.svgis excluded by!**/*.svg
📒 Files selected for processing (17)
apps/extension/manifest.json(1 hunks)apps/extension/popup.html(1 hunks)apps/extension/src/App.tsx(1 hunks)apps/extension/src/background.ts(1 hunks)apps/extension/src/content.ts(1 hunks)apps/extension/src/hooks/usePageMeta.ts(1 hunks)apps/extension/src/hooks/useSaveBookmarks.ts(1 hunks)apps/extension/src/popup.tsx(1 hunks)apps/extension/src/utils/OGFetch.ts(1 hunks)apps/extension/src/utils/ValidateData.ts(1 hunks)apps/extension/tsconfig.json(1 hunks)apps/extension/vite.config.ts(1 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(3 hunks)packages/design-system/src/components/infobox/InfoBox.tsx(1 hunks)packages/design-system/src/components/popup/PopupContainer.tsx(2 hunks)packages/design-system/src/components/textarea/Textarea.tsx(1 hunks)packages/design-system/src/components/wheelPicker/WheelPicker.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-08T11:47:10.642Z
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.
Applied to files:
apps/extension/src/App.tsx
📚 Learning: 2025-07-15T20:00:13.756Z
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.756Z
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.
Applied to files:
apps/extension/src/App.tsx
📚 Learning: 2025-07-08T11:47:27.279Z
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.
Applied to files:
packages/design-system/src/components/wheelPicker/WheelPicker.tsx
🧬 Code graph analysis (3)
apps/extension/src/App.tsx (5)
apps/extension/src/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(11-55)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-52)packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(9-33)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(56-176)
packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1)
packages/design-system/src/lib/utils.ts (1)
cn(108-110)
apps/extension/src/hooks/usePageMeta.ts (1)
apps/extension/src/utils/OGFetch.ts (1)
OgImageFetcher(1-8)
🪛 GitHub Check: lint
apps/extension/src/App.tsx
[warning] 70-70:
Unexpected console statement
apps/extension/src/content.ts
[warning] 1-1:
Unexpected console statement
apps/extension/src/hooks/useSaveBookmarks.ts
[warning] 47-47:
Unexpected console statement
[warning] 41-41:
Unexpected console statement
[warning] 23-23:
Unexpected any. Specify a different type
[warning] 21-21:
Unexpected console statement
apps/extension/src/background.ts
[warning] 34-34:
Unexpected console statement
[warning] 1-1:
Unexpected console statement
🪛 GitHub Actions: ci
apps/extension/src/App.tsx
[error] 15-15: Build step '@pinback/extension:build' failed: Cannot find module '@utils/validateData' or its corresponding type declarations.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (10)
packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1)
49-59: 클래스 병합 방식 LGTM.
...classNames후 키 단위로cn으로 오버라이드하는 패턴이 예측 가능하고 안전합니다.apps/extension/src/popup.tsx (1)
5-15: 간단 확인: 엔트리 부트스트랩 정상QueryClient Provider로 감싸는 구성과 루트 엘리먼트 체크 흐름 문제 없습니다. 이 파일은 그대로 LGTM입니다.
apps/extension/src/utils/OGFetch.ts (2)
1-32: manifest.json에 host_permissions 확인됨:host_permissions에["<all_urls>"]가 선언되어 있어 확장 프로그램에서 모든 도메인에 대한 fetch가 이미 허용됩니다.
9-15: fetchOgMeta는 백그라운드 SW가 아닌 팝업(UI)에서만 호출되므로 DOMParser 제약이 없습니다
rg 검색 결과 해당 함수는 오직apps/extension/src/utils/OGFetch.ts내 React 컴포넌트에서만 사용되며 서비스 워커에서는 호출되지 않습니다.Likely an incorrect or invalid review comment.
apps/extension/src/utils/ValidateData.ts (1)
23-27: "이후" 정책 확인 필요: 오늘 날짜 허용 여부 명확화.현재는 오늘 00:00과 동일한 날짜를 허용합니다(미만 비교). "현재 시점 이후"를 엄격히 요구한다면 오늘 날짜도 시간 검증과 결합해 제한해야 합니다.
원하시는 정책에 따라 아래 중 하나를 적용하세요:
- 날짜만으로 과거 차단(현행 유지) + 별도 시간 검증에서 오늘·과거 시각 차단
- 날짜 단계에서 오늘도 차단:
- if (testDate < today) return '현재 시점 이후 날짜로 작성하세요'; + if (testDate <= today) return '현재 시점 이후 날짜로 작성하세요';apps/extension/tsconfig.json (1)
4-7: 빌드 완화를 위한 낮은 엄격도를 임시로 유지하고 복구 계획을 반드시 이슈로 관리하세요.
skipLibCheck,noImplicitAny: false,strict: false설정은 잠재적 회귀를 숨깁니다. TODO나 이슈로 복구 계획 등록 후 단계적으로 엄격 모드를 복원하며 경고를 해소하세요.복구에 필요한 오류 목록을 수집하려면 로컬에서 다음을 실행해 주세요:
npm exec tsc --project apps/extension/tsconfig.json --noEmit --strictapps/extension/src/background.ts (1)
25-31: 상대 이미지 URL 절대화 로직은 적절합니다.
new URL(img, base).href로 안전하게 정규화하고, 실패 시 원본 반환하는 폴백도 좋습니다.apps/extension/manifest.json (1)
7-8: 아이콘 경로 확인 완료
apps/extension/public/icon.png가 존재하며 빌드 시 루트로 복사되어manifest.json에서"icon.png"로 참조하는 경로가 올바릅니다.apps/extension/src/hooks/usePageMeta.ts (2)
20-25: 권한 설정 확인 완료 –manifest.json에 이미activeTab및tabs권한이 포함되어 있어 추가 조치가 필요 없습니다.
1-3: chrome 전역 타입 선언 확인 완료:apps/extension/package.json에@types/chrome가 이미 포함되어 있어 전역chrome타입이 적용되어 있습니다. CI에서 TS 에러가 없는 것을 확인했습니다.
| "service_worker": "src/background.js", | ||
| "type": "module" | ||
| }, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
TS 엔트리 → .js로 변경은 빌드 파이프라인과 충돌 위험(소스에 .js 미존재)
레포는 TS 소스(src/background.ts, src/content.ts)를 사용 중인데 매니페스트가 .js를 직접 가리키면 번들러(예: vite-plugin-web-extension)가 엔트리 추적을 못 해 빌드/핫리로드가 깨질 수 있습니다. 실제로 소스 트리에 src/background.js/src/content.js는 없을 확률이 큽니다. TS 엔트리를 그대로 두고 번들러가 JS를 산출하도록 되돌리는 것을 권장합니다.
적용 제안:
"background": {
- "service_worker": "src/background.js",
+ "service_worker": "src/background.ts",
"type": "module"
},
@@
"matches": ["<all_urls>"],
- "js": ["src/content.js"]
+ "js": ["src/content.ts"]검증 스크립트(소스에 .js가 없는지, TS가 존재하는지 확인):
Also applies to: 17-18
🏁 Script executed:
#!/bin/bash
fd -a 'background.js' apps/extension/src || echo "no background.js in src (expected)"
fd -a 'content.js' apps/extension/src || echo "no content.js in src (expected)"
fd -a 'background.ts' apps/extension/src
fd -a 'content.ts' apps/extension/srcLength of output: 100
manifest.json에서 service_worker·content 경로를 .ts로 수정 필요
실제 apps/extension/src에 background.js/content.js가 없고 background.ts/content.ts만 존재하여(스크립트로 확인), 번들러가 엔트리를 찾지 못해 빌드·핫리로드가 깨집니다. 아래처럼 .js → .ts로 변경하세요.
"background": {
- "service_worker": "src/background.js",
+ "service_worker": "src/background.ts",
"type": "module"
},
@@
"matches": ["<all_urls>"],
- "js": ["src/content.js"]
+ "js": ["src/content.ts"]🤖 Prompt for AI Agents
In apps/extension/manifest.json around lines 11 to 13, the manifest points
service_worker and content script files to .js files that don't exist; update
the paths to reference the actual TypeScript sources (e.g., change
"src/background.js" → "src/background.ts" and any "content.js" →
"src/content.ts") so the bundler can find the entry points and restore
build/hot-reload.
| const handleSave = async () => { | ||
| save({ | ||
| url, | ||
| title, | ||
| description, | ||
| imgUrl, | ||
| memo, | ||
| isRemindOn, | ||
| selectedCategory: selected, | ||
| date: isRemindOn ? date : null, | ||
| time: isRemindOn ? time : null, | ||
| }); | ||
| const saveData = { | ||
| url, | ||
| title, | ||
| description, | ||
| imgUrl, | ||
| memo, | ||
| isRemindOn, | ||
| selectedCategory: selected, | ||
| date: isRemindOn ? date : null, | ||
| time: isRemindOn ? time : null, | ||
| createdAt: new Date().toISOString(), | ||
| }; | ||
| console.log('저장 데이터:', saveData); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
저장 전 유효성 게이트 + 불필요한 로그/중복 객체 제거 + await 누락
- 리마인드 ON인데 에러가 있는 경우 저장을 막아야 합니다.
- 동일 payload를 다시 만들어 로그만 찍는 코드는 제거하세요(ESLint: Unexpected console).
save는 Promise이므로await로 완료 보장(훅 내부에서 window.close 호출).
- const handleSave = async () => {
- save({
+ const handleSave = async () => {
+ if (isRemindOn) {
+ const dErr = validateDate(date);
+ const tErr = validateTime(time);
+ setDateError(dErr);
+ setTimeError(tErr);
+ if (dErr || tErr) return;
+ }
+ await save({
url,
title,
description,
imgUrl,
memo,
isRemindOn,
selectedCategory: selected,
date: isRemindOn ? date : null,
time: isRemindOn ? time : null,
});
- const saveData = {
- url,
- title,
- description,
- imgUrl,
- memo,
- isRemindOn,
- selectedCategory: selected,
- date: isRemindOn ? date : null,
- time: isRemindOn ? time : null,
- createdAt: new Date().toISOString(),
- };
- console.log('저장 데이터:', saveData);
};📝 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 handleSave = async () => { | |
| save({ | |
| url, | |
| title, | |
| description, | |
| imgUrl, | |
| memo, | |
| isRemindOn, | |
| selectedCategory: selected, | |
| date: isRemindOn ? date : null, | |
| time: isRemindOn ? time : null, | |
| }); | |
| const saveData = { | |
| url, | |
| title, | |
| description, | |
| imgUrl, | |
| memo, | |
| isRemindOn, | |
| selectedCategory: selected, | |
| date: isRemindOn ? date : null, | |
| time: isRemindOn ? time : null, | |
| createdAt: new Date().toISOString(), | |
| }; | |
| console.log('저장 데이터:', saveData); | |
| }; | |
| const handleSave = async () => { | |
| if (isRemindOn) { | |
| const dErr = validateDate(date); | |
| const tErr = validateTime(time); | |
| setDateError(dErr); | |
| setTimeError(tErr); | |
| if (dErr || tErr) return; | |
| } | |
| await save({ | |
| url, | |
| title, | |
| description, | |
| imgUrl, | |
| memo, | |
| isRemindOn, | |
| selectedCategory: selected, | |
| date: isRemindOn ? date : null, | |
| time: isRemindOn ? time : null, | |
| }); | |
| }; |
🧰 Tools
🪛 GitHub Check: lint
[warning] 70-70:
Unexpected console statement
🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around lines 46 to 71, add a pre-save validation
that prevents calling save when isRemindOn is true and there are validation
errors (e.g., missing/invalid date or time), remove the duplicate saveData
object and the console.log that only re-creates the payload, and call await
save(...) so the Promise completes before proceeding (so any subsequent
window.close or follow-up logic in the hook runs after save finishes); keep the
payload construction as originally passed to save and ensure date/time are set
to null when isRemindOn is false.
| <div className="mb-[0.4rem] flex items-center justify-between gap-[0.8rem]"> | ||
| <DateTime | ||
| type="date" | ||
| state={ | ||
| dateError ? 'error' : isRemindOn ? 'default' : 'disabled' | ||
| } | ||
| value={date} | ||
| onChange={handleDateChange} | ||
| /> | ||
| <DateTime | ||
| type="time" | ||
| state={ | ||
| timeError ? 'error' : isRemindOn ? 'default' : 'disabled' | ||
| } | ||
| value={time} | ||
| onChange={handleTimeChange} | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
DateTime(time) 자식 → onChange 미전파로 handleTimeChange가 호출되지 않을 가능성
디자인시스템 DateTime.tsx를 보면 time 타입에서 props.onChange를 호출하지 않습니다(현재는 date 타입만 onChange 전달). App에서 onChange={handleTimeChange}가 무의미해져 time 상태/에러가 갱신되지 않을 수 있습니다.
검증 스크립트(자식 컴포넌트의 onChange 호출 여부):
빠른 수정 제안(DateTime.tsx 내부, time 분기에서 부모 onChange 호출):
// beforeInput insert/삭제/붙여넣기 각 분기에서 setInput(...) 뒤에 추가
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);
...
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);
...
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);🏁 Script executed:
#!/bin/bash
rg -n 'DateTime\.tsx' packages/design-system/src/components/dateTime -n
rg -nP 'onChange\(.*\)' packages/design-system/src/components/dateTime/DateTime.tsx -n
rg -nP 'type === .time.*\)' -n -C3 packages/design-system/src/components/dateTime/DateTime.tsxLength of output: 0
🏁 Script executed:
#!/bin/bash
# DateTime 컴포넌트 파일 경로 찾기
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
echo "DateTime.tsx 파일을 찾을 수 없습니다."
exit 0
fi
echo "Inspecting: $FILE"
echo -e "\n--- props.onChange 호출 여부 ---"
rg -n 'props\.onChange' "$FILE" || echo "없음"
echo -e "\n--- time 타입 분기 코드 ---"
rg -nP "type\\s*[:=]\\s*['\"]time" -nC3 "$FILE" || echo "없음"Length of output: 587
🏁 Script executed:
#!/bin/bash
# DateTime.tsx 길이 및 time 분기 확인
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
echo "DateTime.tsx 파일을 찾을 수 없습니다."
exit 1
fi
echo "파일 경로: $FILE"
echo "총 라인 수:"
wc -l "$FILE"
echo -e "\n--- time 타입 분기 주변 코드 (컨텍스트 5줄) ---"
rg -n -C5 "type\\s*===\\s*['\"]time['\"]" "$FILE" || echo "time 분기 없음"
echo -e "\n--- props.onChange 호출 위치 (컨텍스트 5줄) ---"
rg -n -C5 "props\\.onChange" "$FILE" || echo "props.onChange 호출 없음"Length of output: 790
🏁 Script executed:
#!/bin/bash
# DateTime.tsx에서 formatTime 사용 및 onChange 호출 확인
FILE=$(find packages/design-system -type f -iname "DateTime.tsx" | head -n1)
if [ -z "$FILE" ]; then
echo "DateTime.tsx 파일을 찾을 수 없습니다."
exit 1
fi
echo "파일 경로: $FILE"
echo
echo "--- formatTime 사용 위치 (컨텍스트 3줄) ---"
rg -n -C3 "formatTime" "$FILE" || echo "formatTime 없음"
echo
echo "--- formatTime 분기에서 props.onChange 호출 여부 (컨텍스트 3줄) ---"
rg -n -C3 "formatTime.*props\.onChange" "$FILE" || echo "formatTime 이후 onChange 호출 없음"Length of output: 1646
DateTime(time) onChange 미호출로 handleTimeChange 미실행
packages/extension/src/App.tsx(131–148)에서 <DateTime type="time" onChange={handleTimeChange} /> 사용 시, 디자인시스템의 time 분기(90–144라인) 로직에 props.onChange 호출이 없어 상위 handleTimeChange가 실행되지 않습니다. 아래처럼 setInput 직후에 호출을 추가해야 합니다.
@@ packages/design-system/src/components/dateTime/DateTime.tsx
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/extension/src/App.tsx around lines 131 to 148, the DateTime component
for type="time" updates internal input state but does not call props.onChange,
so the parent handleTimeChange never runs; after the setInput call in the
time-handling branch of the design system component (around lines 90–144
referenced in the comment), invoke props.onChange with the normalized time value
(or the same value passed into setInput), guarding that props.onChange exists
before calling it, so the parent handler receives the update.
| if (message.type === 'FETCH_OG_META') { | ||
| fetch(message.url) | ||
| .then((res) => res.text()) | ||
| .then((html) => { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(html, 'text/html'); | ||
|
|
||
| const getMeta = (prop) => | ||
| doc | ||
| .querySelector(`meta[property="${prop}"]`) | ||
| ?.getAttribute('content') || ''; | ||
|
|
||
| const makeAbsoluteUrl = (base, img) => { | ||
| try { | ||
| return img ? new URL(img, base).href : ''; | ||
| } catch { | ||
| return img; | ||
| } | ||
| }; | ||
|
|
||
| const image = getMeta('og:image'); | ||
|
|
||
| sendResponse({ | ||
| title: getMeta('og:title'), | ||
| description: getMeta('og:description'), | ||
| siteName: getMeta('og:site_name'), | ||
| image: makeAbsoluteUrl(message.url, image), | ||
| url: getMeta('og:url') || message.url, |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
페치 안정성: 타임아웃·스킴 검증 추가, 호스트 권한 점검 필요.
- 무한 대기 방지를 위해 AbortController로 타임아웃을 넣으세요.
- http/https 외 스킴은 즉시 거부하세요.
- 크로스 오리진 HTML을 가져오려면 host_permissions가 필요합니다. 미설정 시 다수 사이트에서 CORS로 실패합니다. (developer.chrome.com)
if (message.type === 'FETCH_OG_META') {
- fetch(message.url)
+ try {
+ if (!/^https?:\/\//i.test(message.url)) throw new Error('unsupported scheme');
+ } catch {
+ sendResponse(null);
+ return true;
+ }
+ const ac = new AbortController();
+ const to = setTimeout(() => ac.abort(), 8000);
+ fetch(message.url, { signal: ac.signal })
.then((res) => res.text())
.then((html) => {
+ clearTimeout(to);
const parser = new DOMParser();권한 확인 스크립트:
🏁 Script executed:
#!/bin/bash
# manifest.json에서 권한 확인
rg -n --json '"(permissions|host_permissions)"' apps/extension/manifest.json | jq -r '.data.lines.text' || trueLength of output: 130
Fetch 안정성 강화 및 스킴 검증
- AbortController로 타임아웃(예: 8000ms) 처리해 무한 대기 방지
if (!/^https?:\/\//i)검사를 통해 http/https 외 스킴 즉시 거부- manifest.json에 이미
"host_permissions": ["<all_urls>"]가 설정돼 있어 크로스 오리진 fetch 권한 확보됨
| onChange?: (value: string) => void; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
onChange 콜백이 date 타입에만 호출됨 → time 타입에서도 일관되게 트리거 필요
현재 onChange는 날짜 입력에만 호출됩니다. 시간 입력(beforeInput/Backspace 경로)에서도 외부에 변경을 알려야 제어형 사용이 가능합니다.
@@
if (inputType.startsWith('insert') && data) {
@@
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);
@@
if (inputType === 'deleteContentBackward') {
@@
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);
@@
if (pasted) {
@@
- setInput(formatTime12(next));
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);
@@
} else {
@@
}
@@
if (['Backspace', 'Delete'].includes(e.key)) {
if (timeDigits.length > 0) {
const next = timeDigits.slice(0, -1);
- setTimeDigits(next);
- setInput(formatTime12(next));
+ setTimeDigits(next);
+ const formatted = formatTime12(next);
+ setInput(formatted);
+ props.onChange?.(formatted);Also applies to: 56-61, 130-131
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines
14-15 (also applies to logic at 56-61 and 130-131), the onChange prop is only
invoked for the date branch so time edits (including beforeInput and Backspace
paths) do not notify external controllers; update the time-handling code paths
to call props.onChange(formattedValue) whenever a time segment is changed or
cleared (input handler, beforeInput handler, and Backspace/key handlers) using
the same string format used for date updates, guard the call with if (onChange)
to respect optional prop, and ensure the formatted value reflects the current
composed time so controlled consumers receive consistent updates.
| export default function DateTime({ | ||
| type, | ||
| value = '', | ||
| state, | ||
| ...props | ||
| }: DateTimeProps) { |
There was a problem hiding this comment.
🛠️ Refactor suggestion
value prop 동기화 누락(제어형 지원 미비)
외부에서 value가 바뀌어도 내부 상태(input, timeDigits)가 동기화되지 않습니다. 제어형 사용을 지원하려면 value 변화에 반응하는 useEffect가 필요합니다.
예시:
useEffect(() => {
const only = digitsOnly(value ?? '');
if (type === 'date') {
setInput(formatDate(only.slice(0, 8)));
nextCaretRef.current = null;
} else {
const d = only.slice(0, 4);
setTimeDigits(d);
setInput(formatTime12(d));
nextCaretRef.current = mapCaretByDigitsPos(d.length, 'time');
}
}, [value, type]);🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines 56
to 61, the component does not sync its internal state when the incoming value
prop changes, so it cannot behave as a controlled component; add a useEffect
that watches [value, type], normalizes digitsOnly(value), and for type 'date'
setInput to formatted date (slice to 8) and reset nextCaretRef.current, and for
type 'time' setTimeDigits to the first 4 digits, setInput to formatted 12‑hour
time and update nextCaretRef.current using mapCaretByDigitsPos(d.length,
'time'); ensure state setters and refs are updated inside the effect to keep
internal input/timeDigits in sync with external value.
| }: PopupContainerProps): React.ReactNode => { | ||
| // ESC 키로 닫는 것 정도 (외부 클릭은 안되게! : 어차피 x박스나 취소버튼이 있음) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
createPortal 반환 타입 캐스팅 제거 및 올바른 반환 타입 명시
createPortal은 ReactPortal을 반환합니다. 현재 ReactNode로 선언 후 ReactElement로 강제 캐스팅하면 타입 불일치가 가려지고, 소비 측이 Element만 기대할 때 런타임/타입 미스매치가 생길 수 있습니다. 컴포넌트 반환 타입을 ReactPortal | null로 명시하고 캐스팅을 제거하세요.
-}: PopupContainerProps): React.ReactNode => {
+}: PopupContainerProps): React.ReactPortal | null => {
// ...
- return createPortal(
+ return createPortal(
<div className="fixed inset-0 z-10 flex items-center justify-center">
<div className="absolute inset-0 bg-[#00000099]" />
<div className="relative">
<Popup {...popupProps} onLeftClick={onClose} />
</div>
</div>,
- document.body // body 위에서 렌더링 되게 함!
- ) as unknown as React.ReactElement;
+ document.body // body 위에서 렌더링 되게 함!
+ );추가로, 접근성 향상을 위해 역할/속성 부여를 권장합니다(선택):
- 최상위 div에 role="dialog" aria-modal="true"
- 제목/설명과 aria-labelledby/aria-describedby 연결
Also applies to: 48-49
🤖 Prompt for AI Agents
In packages/design-system/src/components/popup/PopupContainer.tsx around lines
27-28 (also apply same change at 48-49): the component currently declares a
ReactNode return type and force-casts the createPortal result to ReactElement;
change the component return type to ReactPortal | null and remove the cast so
createPortal's native ReactPortal type is preserved, returning null when
appropriate; additionally (optional) add accessibility attributes on the
top-level container (e.g., role="dialog" aria-modal="true" and connect
title/description with aria-labelledby/aria-describedby) to improve a11y.
| // 타입을 명시적으로 정의 | ||
| interface WheelPickerWrapperProps { | ||
| className?: string; | ||
| children: React.ReactNode; | ||
| } |
There was a problem hiding this comment.
React 타입 네임스페이스 미임포트로 빌드 실패 위험 (React.ReactNode).
React.ReactNode를 참조하지만 react 타입 임포트가 없어 TS 설정에 따라 에러가 납니다. 타입 전용 임포트로 해결하세요.
적용 diff(선택된 범위 내 변경):
- children: React.ReactNode;
+ children: ReactNode;그리고 파일 상단에 타입 임포트 추가(선택된 범위 외):
import type { ReactNode } from 'react';🤖 Prompt for AI Agents
In packages/design-system/src/components/wheelPicker/WheelPicker.tsx around
lines 9 to 13, the interface uses React.ReactNode but the file lacks a type-only
import from 'react', which can break builds under strict TS settings; add a
top-of-file type import "import type { ReactNode } from 'react';" and update the
interface to use ReactNode (or alternatively add "import type React from
'react';" and keep React.ReactNode) so the type reference is explicitly
imported.
| function WheelPickerWrapper({ | ||
| className, | ||
| children, | ||
| ...props | ||
| }: React.ComponentProps<typeof WheelPickerPrimitive.WheelPickerWrapper>) { | ||
| }: WheelPickerWrapperProps) { | ||
| return ( | ||
| <WheelPickerPrimitive.WheelPickerWrapper | ||
| className={cn('flex', className)} | ||
| {...props} | ||
| /> | ||
| > | ||
| {children} | ||
| </WheelPickerPrimitive.WheelPickerWrapper> |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Wrapper의 패스스루 props가 타입상 소거되어 전달되지 않음.
현재 WheelPickerWrapperProps에 className, children만 있어 ...props는 {}가 되어 실제로 아무 속성도 전달되지 않습니다. 혼란을 줄이려면 (A) 전체 프리미티브 props를 확장해 진짜 패스스루를 허용하거나, (B) 의도적으로 패스스루를 끄고 ...props를 제거하세요.
옵션 B(안전, 즉시 적용) diff:
-function WheelPickerWrapper({
- className,
- children,
- ...props
-}: WheelPickerWrapperProps) {
+function WheelPickerWrapper({
+ className,
+ children,
+}: WheelPickerWrapperProps) {
return (
<WheelPickerPrimitive.WheelPickerWrapper
className={cn('flex', className)}
- {...props}
>
{children}
</WheelPickerPrimitive.WheelPickerWrapper>
);
}옵션 A(권장, 빌드 이슈 재발 가능성 확인 필요): 프리미티브 props 확장(선택된 범위 외 코드 예시)
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
type WheelPickerWrapperProps =
Omit<ComponentPropsWithoutRef<typeof WheelPickerPrimitive.WheelPickerWrapper>, 'className'> & {
className?: string;
children: ReactNode;
};🏁 Script executed:
#!/bin/bash
# WheelPickerWrapperProps 정의 확인
rg -nP "type\s+WheelPickerWrapperProps" -n packages/design-system/src/components/wheelPicker/WheelPicker.tsx -A5Length of output: 0
🏁 Script executed:
#!/bin/bash
# Print the first 200 lines of WheelPicker.tsx
sed -n '1,200p' packages/design-system/src/components/wheelPicker/WheelPicker.tsx || echo "sed failed"
# Search for WheelPickerWrapperProps type definition in the file
rg -nP "type\\s+WheelPickerWrapperProps" -n packages/design-system/src/components/wheelPicker/WheelPicker.tsx || echo "rg no match"Length of output: 1690
WheelPickerWrapperProps에 패스스루 props 미전달
WheelPickerWrapperProps에 className과 children만 정의되어 있어 ...props가 빈 객체로 되어 실제 <WheelPickerPrimitive.WheelPickerWrapper>에 전달되지 않습니다. 의도한 동작에 따라 아래 중 하나를 선택하세요:
- (A)
ComponentPropsWithoutRef<typeof WheelPickerPrimitive.WheelPickerWrapper>를 확장해 프리미티브 props를 허용 (추천) - (B)
...props를 제거해 패스스루를 비활성화 (안전, 즉시 적용)
Option B 적용 예시:
-function WheelPickerWrapper({
- className,
- children,
- ...props
-}: WheelPickerWrapperProps) {
+function WheelPickerWrapper({
+ className,
+ children,
+}: WheelPickerWrapperProps) {
return (
<WheelPickerPrimitive.WheelPickerWrapper
className={cn('flex', className)}
- {...props}
>
{children}
</WheelPickerPrimitive.WheelPickerWrapper>
);
}📝 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.
| function WheelPickerWrapper({ | |
| className, | |
| children, | |
| ...props | |
| }: React.ComponentProps<typeof WheelPickerPrimitive.WheelPickerWrapper>) { | |
| }: WheelPickerWrapperProps) { | |
| return ( | |
| <WheelPickerPrimitive.WheelPickerWrapper | |
| className={cn('flex', className)} | |
| {...props} | |
| /> | |
| > | |
| {children} | |
| </WheelPickerPrimitive.WheelPickerWrapper> | |
| function WheelPickerWrapper({ | |
| className, | |
| children, | |
| }: WheelPickerWrapperProps) { | |
| return ( | |
| <WheelPickerPrimitive.WheelPickerWrapper | |
| className={cn('flex', className)} | |
| > | |
| {children} | |
| </WheelPickerPrimitive.WheelPickerWrapper> | |
| ); | |
| } |
🤖 Prompt for AI Agents
packages/design-system/src/components/wheelPicker/WheelPicker.tsx lines 26-37:
WheelPickerWrapperProps currently only defines className and children so the
spread ...props is effectively empty and primitive props are not passed through;
update the props type to extend ComponentPropsWithoutRef<typeof
WheelPickerPrimitive.WheelPickerWrapper> (importing it from React) so the
wrapper accepts and forwards all native props, keep the {...props} spread in the
JSX, or alternatively remove the ...props spread and usages if you intentionally
want to disable pass-through.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
apps/extension/src/App.tsx (3)
15-15: 대소문자 경로 수정으로 CI 빌드 실패 원인 해결됨
@utils/ValidateData로 수정해 리눅스 CI의 대소문자 민감 이슈를 해소합니다. 이전 코멘트와 일치합니다.
46-71: 저장 로직: 유효성 게이트 추가 + await 누락 + 중복 payload/console 제거리마인드 ON에서 에러가 있어도 저장이 진행되고,
save가 비동기임에도await가 없어 후속 동작 순서 보장이 안 됩니다. 또한 동일 payload를 다시 만들어 콘솔에 출력하는 중복 코드가 있습니다(ESLint 경고와 일치). 아래처럼 정리하세요.const handleSave = async () => { - save({ + // 리마인드 유효성 체크 (ON일 때만) + if (isRemindOn) { + const dErr = validateDate(date); + const tErr = validateTime(time); + setDateError(dErr); + setTimeError(tErr); + if (dErr || tErr) return; + } + if (!url) return; // 메타 로딩 전 저장 방지 + await save({ url, title, description, imgUrl, memo, isRemindOn, selectedCategory: selected, date: isRemindOn ? date : null, time: isRemindOn ? time : null, }); - const saveData = { - url, - title, - description, - imgUrl, - memo, - isRemindOn, - selectedCategory: selected, - date: isRemindOn ? date : null, - time: isRemindOn ? time : null, - createdAt: new Date().toISOString(), - }; - console.log('저장 데이터:', saveData); };
131-148: DateTime(time) onChange 미호출로 시간/에러 상태 미갱신 가능성현재 디자인시스템 DateTime의 time 분기는
props.onChange를 호출하지 않아handleTimeChange가 실행되지 않습니다. 아래처럼 DS 컴포넌트에 콜백 전달을 추가해야 합니다.추가 변경(패키지 외부 파일):
--- a/packages/design-system/src/components/dateTime/DateTime.tsx +++ b/packages/design-system/src/components/dateTime/DateTime.tsx @@ - const next = (timeDigits + add).slice(0, 4); - setTimeDigits(next); - setInput(formatTime12(next)); + const next = (timeDigits + add).slice(0, 4); + setTimeDigits(next); + const formatted = formatTime12(next); + setInput(formatted); + props.onChange?.(formatted); @@ - const next = timeDigits.slice(0, -1); - setTimeDigits(next); - setInput(formatTime12(next)); + const next = timeDigits.slice(0, -1); + setTimeDigits(next); + const formatted = formatTime12(next); + setInput(formatted); + props.onChange?.(formatted); @@ - const next = (timeDigits + pasted).slice(0, 4); - setTimeDigits(next); - setInput(formatTime12(next)); + const next = (timeDigits + pasted).slice(0, 4); + setTimeDigits(next); + const formatted = formatTime12(next); + setInput(formatted); + props.onChange?.(formatted); @@ - const next = timeDigits.slice(0, -1); - setTimeDigits(next); - setInput(formatTime12(next)); + const next = timeDigits.slice(0, -1); + setTimeDigits(next); + const formatted = formatTime12(next); + setInput(formatted); + props.onChange?.(formatted);다음 스크립트로 해당 콜백 호출이 누락돼 있는지 확인할 수 있습니다.
#!/bin/bash # DateTime(time) 분기에서 props.onChange 호출 여부 점검 FILE=$(fd -a DateTime.tsx packages/design-system | head -n1) echo "Inspecting: $FILE" rg -n "type=.*['\"]time['\"]" -nC3 "$FILE" echo -e "\n-- props.onChange 호출 위치 --" rg -n "props\.onChange" "$FILE" || echo "props.onChange not found"
🧹 Nitpick comments (3)
apps/extension/src/App.tsx (3)
90-93: 클릭 가능한 로고는 button 사용으로 접근성 확보div+img 조합 대신 버튼 시맨틱과 ARIA 레이블을 적용하세요.
- <div className="mr-auto" onClick={() => setIsPopupOpen(true)}> - <img src={Logo} alt="로고" /> - </div> + <button + type="button" + className="mr-auto" + aria-label="카테고리 추가하기" + onClick={() => setIsPopupOpen(true)} + > + <img src={Logo} alt="" aria-hidden="true" /> + </button>
100-110: 카테고리: 더미 옵션 제거 및 스토리지 연동 제안
옵션1/옵션2는 임시 값으로 보입니다.chrome.storage.local또는 백엔드 소스와 동기화해 실제 카테고리 목록을 주입하고,onAddItem에서 생성 후 목록 갱신까지 연결하세요. 필요 시 구현 도와드릴게요.
39-41: 스위치 OFF 시 에러 상태 초기화(경험 개선)OFF로 전환 시 즉시 에러 문구를 걷어내면 UX가 매끄럽습니다.
const handleSwitchChange = (checked: boolean) => { setIsRemindOn(checked); + if (!checked) { + setDateError(''); + setTimeError(''); + } };
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/extension/src/App.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-08T11:47:10.642Z
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.
Applied to files:
apps/extension/src/App.tsx
📚 Learning: 2025-07-15T20:00:13.756Z
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.756Z
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.
Applied to files:
apps/extension/src/App.tsx
🧬 Code graph analysis (1)
apps/extension/src/App.tsx (5)
apps/extension/src/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(11-55)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-52)packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(9-33)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(56-176)
🪛 GitHub Check: lint
apps/extension/src/App.tsx
[warning] 70-70:
Unexpected console statement
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (1)
apps/extension/src/App.tsx (1)
94-99: OG 메타 기반 InfoBox 적용 좋습니다하드코딩 아이콘에서 동적 OG 이미지/메타 사용으로 전환되어 이전 러닝과 정합적입니다.
jjangminii
left a comment
There was a problem hiding this comment.
확장 프로그램은 볼때마다 신기한게 많네요.. 고생하셨습니다-!! 이 부분은 저도 겹치는 부분이 많았는데 막히는 부분 덕분에 해결하고갑니당-!!
apps/extension/src/App.tsx
Outdated
| <div className="App"> | ||
| <div className="flex h-[50rem] w-[26rem] items-center justify-center bg-blue-500 text-2xl text-white"> | ||
| 자 핀백 앱잼 시작~오늘은 7월 7일임~ | ||
| <div className="relative flex h-[56.8rem] w-[31.2rem] items-center justify-center bg-[#00000000] text-2xl text-white"> |
There was a problem hiding this comment.
bg-[#00000000] text-2xl text-white 이 부분 지정된 컬러토큰 없는건가요??
There was a problem hiding this comment.
헉 테스트 한다고 작업해둔 코드가 남아있었네요..! 감사합니다!
apps/extension/src/App.tsx
Outdated
| import { useState } from 'react'; | ||
| import { usePageMeta } from './hooks/usePageMeta'; | ||
| import { useSaveBookmark } from './hooks/useSaveBookmarks'; | ||
| import { validateDate, validateTime } from '@utils/ValidateData'; |
There was a problem hiding this comment.
혹시 유효성 검사 클라이언트에서도 동일하게 적용되는걸로 알고있는데 이 유틸 공통으로 빼는것에 대해 어떻게 생각하시나요?
There was a problem hiding this comment.
상의했던 부분처럼!
- 공컴(dateTime)을 위한 유틸이다
- 최소 2곳 (client, extension) 에서 공통적으로 쓰이는 유틸
이러한 2가지 이유로, 말씀해주신대로 디자인시스템에 빼도 될거 같다는 생각입니다!
There was a problem hiding this comment.
ds에 이동 완료했습니다! client 작업하실때 ValidateDate, ValidateTime 두개 빼서 쓰시면 될듯해용!
| 자 핀백 앱잼 시작~오늘은 7월 7일임~ | ||
| <div className="relative flex h-[56.8rem] w-[31.2rem] items-center justify-center bg-[#00000000] text-2xl text-white"> | ||
| {isPopupOpen && ( | ||
| <PopupContainer |
There was a problem hiding this comment.
키테고리 입력창에 대해 에러 메세지는 어떻게 나타내나요?
There was a problem hiding this comment.
input에서 입력값 받는 부분, 하단에 에러메시지 띄우는 부분이 공컴 작업에서 누락이 되었었나봐요!
해당 기능 추가해두었습니다~!
apps/extension/src/App.tsx
Outdated
| )} | ||
| <div className="flex flex-col justify-between gap-[1.6rem] rounded-[12px] bg-white px-[3.2rem] py-[2.4rem] text-black"> | ||
| <div className="mr-auto" onClick={() => setIsPopupOpen(true)}> | ||
| <img src={Logo} alt="로고" /> |
There was a problem hiding this comment.
왜 Icon이 아닌 img를 사용하셨나요? 익스텐션에서는 Icon 사용이 안되나요??
There was a problem hiding this comment.
엇 해당 아이콘을 '공통 디자인시스템에 넣는 것이 아니지 않나' 라는 잠깐의 생각으로 해당 폴더 내 assets에서 관리하고 있었는데요!
다시 생각해보니 저희 서비스 아이콘들은 다 최대한 Icon에 모아서 import하기로 했던 거 같아서! 빼놓겠습니다!
There was a problem hiding this comment.
로고가 모든 페이지에서 대부분 사용된다고 생각이드는데 디자인시스템에 저장하고 하나를 재사용하는 방싣에 대해 어떻게 생각하시나요?
| onClose, | ||
| ...popupProps | ||
| }: PopupContainerProps) => { | ||
| }: PopupContainerProps): React.ReactNode => { |
There was a problem hiding this comment.
단지 궁금해서 그러는데 이 코드는 왜 추가하셨나요??
There was a problem hiding this comment.
넵넵! 이부분 공유가 필요할 것 같네요!
PR 상단에 수정했다고 명시만 해두었는데, 추가적으로 내용 및 이유 상세히 작성해두겠습니다!
| <textarea | ||
| data-slot="textarea" | ||
| maxLength={maxLength} | ||
| value={value} |
There was a problem hiding this comment.
설명주신 부분 인지했습니다! 누락되어있는 사항이라 생각하고 추가했던 것 같습니다!
| <div className="items-left flex flex-col justify-center gap-[0.2rem] text-left"> | ||
| <p className="sub3-sb w-[180px] truncate">{title}</p> | ||
| <p className="caption2-m text-font-gray-3">{source}</p> | ||
| <p className="caption2-m text-font-gray-3 w-[180px] truncate"> |
There was a problem hiding this comment.
이 부분 따로 px로 하신 이유가 있으신가요?
| state: 'default' | 'disabled' | 'error'; | ||
| type: 'date' | 'time'; | ||
| value?: string; | ||
| onChange?: (value: string) => void; |
There was a problem hiding this comment.
이 파일 수정부분은 저도 필요했던 부분이라 어떻게 해야할지 고민이었는데 이렇게 수정하면 되는군요-!! 이 부분은 저도 머지 돼서 반영되면 코드에 반영하겠습니다-!! 최고
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
apps/extension/src/App.tsx (4)
154-156: 저장 버튼 비활성화 조건 추가메타 미준비/리마인드 오류 시 비활성화하여 무효 저장 방지.
- <Button size="medium" onClick={handleSave}> + <Button + size="medium" + onClick={handleSave} + disabled={!url || (isRemindOn && (!!dateError || !!timeError))} + > 저장 </Button>
90-92: 서비스 로고는 Icon으로 통일해서 사용해주세요디자인시스템에 main_logo 추가된 만큼 img 대신 Icon 사용 권장(테마/크기 일관).
-import Logo from '@assets/logo.svg'; +// 로고 에셋 import 제거 (Icon 사용) - <div className="mr-auto" onClick={() => setIsPopupOpen(true)}> - <img src={Logo} alt="로고" /> - </div> + <div className="mr-auto" onClick={() => setIsPopupOpen(true)}> + <Icon name="main_logo" /> + </div>추가: 상단 import에 Icon 포함
import { InfoBox, Button, Textarea, DateTime, Switch, PopupContainer, Dropdown, + Icon, } from '@pinback/design-system/ui';
130-147: DateTime(time) onChange 미전파 이슈 영향디자인시스템 DateTime(time) 분기에서 props.onChange 호출이 없어 handleTimeChange가 불리지 않습니다(별도 파일 수정 필요). 이전 제안 그대로 반영 부탁드립니다.
참고 패치(DateTime.tsx의 time 처리 분기):
- setInput(formatTime12(next)); + const formatted = formatTime12(next); + setInput(formatted); + props.onChange?.(formatted);
46-71: 저장 전 유효성 게이트 + await 누락 + 중복 payload/log 제거이전 코멘트와 동일합니다. 리마인드 켠 상태에서 에러가 있으면 저장 막고, save는 await, 중복 객체/console 제거해주세요.
const handleSave = async () => { - save({ + if (isRemindOn) { + const dErr = validateDate(date); + const tErr = validateTime(time); + setDateError(dErr); + setTimeError(tErr); + if (dErr || tErr) return; + } + await save({ url, title, description, imgUrl, memo, isRemindOn, selectedCategory: selected, date: isRemindOn ? date : null, time: isRemindOn ? time : null, }); - const saveData = { - url, - title, - description, - imgUrl, - memo, - isRemindOn, - selectedCategory: selected, - date: isRemindOn ? date : null, - time: isRemindOn ? time : null, - createdAt: new Date().toISOString(), - }; - console.log('저장 데이터:', saveData); };
🧹 Nitpick comments (5)
packages/design-system/src/components/infobox/InfoBox.tsx (2)
9-9: 이미지 접근성/성능 보완(alt, lazy 로딩 등)단순/안전 보강입니다.
- <img className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem]" src={imgUrl} /> + <img + className="h-[4.4rem] w-[4.4rem] rounded-[0.4rem]" + src={imgUrl} + alt={title} + loading="lazy" + decoding="async" + referrerPolicy="no-referrer" + />
10-10: Tailwind 클래스 오타 의심: items-left → items-startitems-left는 유효하지 않습니다. 수직 정렬 의도라면 items-start 사용이 맞습니다.
- <div className="items-left flex flex-col justify-center gap-[0.2rem] text-left"> + <div className="items-start flex flex-col justify-center gap-[0.2rem] text-left">apps/extension/src/App.tsx (3)
101-110: 카테고리 옵션 하드코딩 제거(TODO 권장)옵션1/옵션2는 임시로 보입니다. 저장소/백그라운드에서 가져와 동기화하도록 TODO로 남겨주세요.
- options={['옵션1', '옵션2']} + options={categories} // TODO: storage/백엔드에서 로드
23-26: 초기 날짜/시간 고정값 대신 빈값 권장리마인드 off 기본값에서 특정 미래일 고정은 혼란을 줄 수 있습니다. on 시 최초 진입에 한해 현재 시각 기준 제안값을 세팅하거나, 빈값으로 두고 입력 유도하세요.
- const [date, setDate] = useState('2025.10.10'); - const [time, setTime] = useState('19:00'); + const [date, setDate] = useState(''); + const [time, setTime] = useState('');
149-152: 에러 메시지는 리마인드 on일 때만 노출off 상태에서 에러 노출은 UX 혼란을 유발합니다.
- {dateError && <p className="body3-r text-error">{dateError}</p>} - {timeError && <p className="body3-r text-error">{timeError}</p>} + {isRemindOn && dateError && ( + <p className="body3-r text-error">{dateError}</p> + )} + {isRemindOn && timeError && ( + <p className="body3-r text-error">{timeError}</p> + )}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
packages/design-system/src/icons/source/main_logo.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_1.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_2.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_3.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_4.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/tooltip_5.svgis excluded by!**/*.svg
📒 Files selected for processing (4)
apps/extension/src/App.tsx(1 hunks)apps/extension/src/hooks/useSaveBookmarks.ts(1 hunks)packages/design-system/src/components/infobox/InfoBox.tsx(1 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/extension/src/hooks/useSaveBookmarks.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-15T20:00:13.756Z
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.756Z
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.
Applied to files:
packages/design-system/src/components/infobox/InfoBox.tsxapps/extension/src/App.tsx
📚 Learning: 2025-07-08T11:47:10.642Z
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.
Applied to files:
apps/extension/src/App.tsx
🧬 Code graph analysis (1)
apps/extension/src/App.tsx (5)
apps/extension/src/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(11-55)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-52)packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(9-31)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(56-176)
| 'ic_details_disable', | ||
| 'ic_info', | ||
| 'ic_plus', | ||
| 'main_logo', |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
자동 생성 파일 직접 수정 주의 + 아이콘 자산/매핑 존재 확인 필요
main_logo 추가 자체는 OK. 다만 이 파일은 자동생성 경고가 있어 스크립트에도 main_logo가 반영돼야 덮어쓰기 이슈가 없습니다. 또한 실제 SVG/컴포넌트 자산과 Icon 매핑 존재를 확인해주세요.
다음 스크립트로 자산/매핑 확인:
🏁 Script executed:
#!/bin/bash
# main_logo 자산과 사용처 확인
fd -H -t f -a 'main_logo*' | sed -n '1,200p'
rg -n --no-ignore -S "main_logo" packages/design-system | sed -n '1,200p'
# Icon 컴포넌트 매핑 안에 main_logo 포함 여부
fd -H -t f -a 'Icon.tsx' packages/design-system | xargs -I{} rg -n "main_logo|iconNames" {}Length of output: 136
생성 스크립트와 Icon 매핑에 main_logo 반영 필요
자동 생성되는 iconNames.ts를 직접 수정하지 말고, 아이콘 생성 스크립트에 main_logo를 추가해 덮어쓰기 이슈를 방지하세요. 또한 packages/design-system/src/icons/Icon.tsx의 매핑 객체에 main_logo를 등록해야 컴포넌트 참조가 정상 작동합니다. SVG 자산(src/icons/source/main_logo.svg)은 이미 존재합니다.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/design-system/src/components/input/Input.tsx (2)
5-11: value/onChange 재정의는 타입 충돌 소지 있으며 ref를 props로 노출하지 마세요
- InputHTMLAttributes가 이미
value/onChange를 제공합니다. 여기서value?: string으로 좁히면 TS가 충돌을 일으킬 수 있습니다(기존 타입은string | number | readonly string[] | undefined).ref는 특수 prop이므로 함수 컴포넌트 props에 두지 말고forwardRef로 전달해야 합니다.아래처럼 불필요한 재정의와
ref를 제거하고 forwardRef로 전달하도록 준비해 주세요.interface InputProps extends InputHTMLAttributes<HTMLInputElement> { - ref?: Ref<HTMLInputElement>; isError?: boolean; helperText?: string; - value?: string; - onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; }
28-46: forwardRef로 리팩터링하여 ref 전달 문제 해결현재
ref를 props에서 구조분해 후<input ref={ref}>로 넘기고 있어 소비 측ref가 동작하지 않습니다.forwardRef로 감싸고 props에서는ref를 제거하세요. 또한value/onChange는{...props}에 포함되므로 별도 전달 불필요합니다.-const Input = ({ - ref, - isError, - className, - helperText, - value, - onChange, - ...props -}: InputProps) => { +const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input( + { isError, className, helperText, ...props }, + ref +) { return ( <div className="flex flex-col gap-[0.5rem]"> <div className={cn(inputBorderVariants({ isError }), className)}> <input ref={ref} - value={value} - onChange={onChange} className="placeholder-font-gray-3 w-full focus:outline-none" aria-invalid={isError} {...props} /> </div> {isError && helperText && ( <div className="flex items-center"> <p className="text-error body4-r">{helperText}</p> </div> )} </div> - ); -}; + ); +});추가로 import 갱신이 필요합니다(파일 상단 적용):
import React, { forwardRef, InputHTMLAttributes } from 'react';
♻️ Duplicate comments (1)
packages/design-system/src/components/popup/PopupContainer.tsx (1)
30-31: createPortal 반환 타입 캐스팅 제거 및 올바른 반환 타입 명시
createPortal은ReactPortal을 반환합니다. 현재ReactNode반환 선언 후as unknown as ReactElement캐스팅은 타입 오류를 가립니다. 반환 타입을ReactPortal | null로 명시하고 강제 캐스팅을 제거하세요.-}: PopupContainerProps): React.ReactNode => { +}: PopupContainerProps): React.ReactPortal | null => { @@ - ) as unknown as React.ReactElement; + );Also applies to: 56-56
🧹 Nitpick comments (11)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (5)
23-26: "이후" 문구 vs 구현(오늘 포함) 불일치 가능현재 구현은 오늘을 허용(<), 메시지는 “이후”(보통 오늘 제외)로 읽힐 여지가 있습니다. 의도에 맞게 둘 중 하나를 맞춰주세요.
옵션 A: 오늘 제외(문구 유지)
- if (testDate < today) return '현재 시점 이후 날짜로 작성하세요'; + if (testDate <= today) return '현재 시점 이후 날짜로 작성하세요';옵션 B: 오늘 포함(로직 유지)
- if (testDate < today) return '현재 시점 이후 날짜로 작성하세요'; + if (testDate < today) return '오늘 또는 이후 날짜로 작성하세요';
34-37: 과도한 sanitize로 잘못된 입력이 정상화될 수 있음
"ab12:34cd"→"12:34"로 정규화되어 통과합니다. 사용자가 의도치 않게 잘못된 포맷을 넣어도 통과할 수 있어 UX/검증 일관성 측면에서 모호합니다. 공백만 trim하고 포맷은 엄격히 검사하는 쪽을 권장합니다. 분(minute)은 2자리를 강제하면 가독성도 좋아집니다.다음처럼 조정해보세요:
- const clean = value.replace(/[^0-9:]/g, ''); - const regex = /^(\d{1,2}):(\d{1,2})$/; + const clean = value.trim(); + const regex = /^(\d{1,2}):(\d{2})$/;
6-7: 디자인 시스템에서의 하드코딩 메시지 제거(국제화/I18N 분리)디자인 시스템 유틸에서 한글 문자열을 직접 반환하면 다국어/복수 앱에서 재사용이 어려워집니다. 코드(예: 'REQUIRED' | 'FORMAT' | 'PAST')를 반환하고, 메시지는 소비 측에서 매핑하도록 분리하는 것을 권장합니다. 호환성 이슈를 피하려면 기존 문자열 반환을 유지하되, 선택적으로 코드도 함께 반환하는 확장안도 가능합니다.
원하시면 non-breaking 형태의
validateDate/TimeWithCode보조 API 초안과 매핑 예시를 드리겠습니다.Also applies to: 12-12, 20-21, 25-25, 32-33, 38-38, 44-45
2-28: 검증 결과 계약 형태 개선 제안(선택)현재 “빈 문자열=성공” 패턴은 호출부에서 가독성이 떨어집니다.
string | null반환 또는{ ok: boolean; code?; message? }로 바꾸면 호출부 가독성과 타입 안정성이 좋아집니다. 새 퍼블릭 API이므로 변경 시 리포 전반 호출부 영향도 점검이 필요합니다.원하시면 호출부 영향도를 점검하는 스크립트를 제공하겠습니다.
Also applies to: 31-48
1-49: 엣지 케이스 테스트 추가 제안다음 케이스에 대한 단위 테스트를 권장합니다.
- 날짜: 유효/무효 윤년(2024.02.29 vs 2025.02.29), 30일/31일 말일, 오늘/어제/내일, 타임존 경계(자정 직후).
- 시간: 00:00, 23:59, 24:00(정책 결정 시), 단자리 입력, 공백/노이즈 포함 입력.
원하시면 Jest 테스트 스캐폴딩을 추가해 드릴게요.
packages/design-system/src/components/index.ts (1)
19-19: 코드 스타일 일관화: 중괄호 공백 추가다른 export 라인들과 스타일을 맞추면 가독성이 좋아집니다.
-export {validateDate, validateTime} from './dateTime/utils/ValidateData'; +export { validateDate, validateTime } from './dateTime/utils/ValidateData';packages/design-system/src/components/popup/PopupContainer.tsx (2)
47-52: 중복 prop 전달 제거
{...popupProps}에 이미inputValue/onInputChange가 포함되어 있어 중복 전달입니다. 유지해도 동작은 같지만 코드를 간결히 하세요.- <Popup - {...popupProps} - onLeftClick={onClose} - inputValue={popupProps.inputValue} - onInputChange={popupProps.onInputChange} - /> + <Popup + {...popupProps} + onLeftClick={onClose} + />
43-46: 대화상자 a11y 속성 추가(선택)오버레이 컨테이너에
role="dialog"와aria-modal="true"를 부여하면 보조기기 호환성이 향상됩니다. 제목/설명이 있다면aria-labelledby/aria-describedby연결도 고려하세요.- <div className="fixed inset-0 z-10 flex items-center justify-center"> + <div className="fixed inset-0 z-10 flex items-center justify-center" role="dialog" aria-modal="true">packages/design-system/src/components/popup/Popup.tsx (3)
11-11: errortext → errorText로 카멜케이스 통일 제안프로퍼티/변수 네이밍 일관성(helperText와의 대칭, IDE 검색 편의) 차원에서 권장합니다.
적용 diff:
- errortext?: string; + errorText?: string; ... - errortext, + errorText, ... - {isError && errortext && ( - <p className='mt-[0.5rem] text-error body3-r'>{errortext}</p> + {isError && errorText && ( + <p className="mt-[0.5rem] text-error body3-r" role="alert" aria-live="polite"> + {errorText} + </p> )}Also applies to: 28-28, 47-49
14-14: 에러 문구 이중 노출 가능성 제거Input 내부에서 helperText가 표시되고, 아래 p 태그로 errortext를 또 노출하면 중복될 수 있습니다. 에러가 있을 땐 helperText를 숨기고 하단 에러만 노출하도록 정리하는 편이 명확합니다.
제안 diff:
- helperText={helperText} - isError={isError} + helperText={isError ? undefined : helperText} + isError={isError} ... - {isError && errorText && ( + {isError && errorText && ( <p className="mt-[0.5rem] text-error body3-r" role="alert" aria-live="polite"> {errorText} </p> )}Also applies to: 43-44, 47-49
44-46: Optional: Controlled 입력 전환 경고 방지 및 onChange 타입 안전성 강화
- onInputChange가 있을 때
value={inputValue ?? ''}로 설정해 uncontrolled↔controlled 전환 경고 예방e.currentTarget.value사용해 onChange 이벤트 핸들러 타입 안전성 강화제안 diff:
- value={inputValue} - onChange={(e) => onInputChange?.(e.target.value)} + value={onInputChange ? (inputValue ?? '') : inputValue} + onChange={(e) => onInputChange?.(e.currentTarget.value)}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/extension/src/App.tsx(1 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(3 hunks)packages/design-system/src/components/dateTime/utils/ValidateData.ts(1 hunks)packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/components/infobox/InfoBox.tsx(1 hunks)packages/design-system/src/components/input/Input.tsx(2 hunks)packages/design-system/src/components/popup/Popup.tsx(3 hunks)packages/design-system/src/components/popup/PopupContainer.tsx(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/design-system/src/components/infobox/InfoBox.tsx
- apps/extension/src/App.tsx
- packages/design-system/src/components/dateTime/DateTime.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (1)
packages/design-system/src/components/index.ts (2)
validateDate(19-19)validateTime(19-19)
packages/design-system/src/components/input/Input.tsx (1)
packages/design-system/src/lib/utils.ts (1)
cn(108-110)
🔇 Additional comments (6)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (3)
14-21: 날짜 역검증 로직은 적절합니다Date 객체로 재조합 후 원본과 일치 여부를 확인하는 방식이 윤년/말일 등 가장 까다로운 케이스를 안정적으로 걸러줍니다. 굳이 외부 라이브러리 없이도 충분히 신뢰할 수 있습니다.
43-45: 24:00 허용 여부 결정 필요현재는 0–23시만 허용되어
24:00을 거부합니다. 제품 기준이 “자정 표기 허용(24:00)”인지 “엄격히 00:00”인지 확인이 필요합니다. 허용하려면 날짜와의 상호작용(다음 날 00:00 처리) 정책도 함께 정의되어야 합니다.
3-3: 날짜 포맷(두 자리 고정) 의도 확인
YYYY.MM.DD만 허용되어2025.9.7같은 단자리 입력은 거부됩니다. 입력 컴포넌트가 항상 0-padding을 보장한다면 괜찮지만, 자유 입력을 받는다면 다음과 같이 1–2자리 허용을 고려할 수 있습니다.- const regex = /^(\d{4})\.(\d{2})\.(\d{2})$/; + const regex = /^(\d{4})\.(\d{1,2})\.(\d{1,2})$/;(표시는 허용하되, 저장 시에는 UI에서
2025.09.07로 정규화하는 접근을 권장)packages/design-system/src/components/index.ts (1)
19-19: 퍼블릭 재노출 방향은 적절합니다앱에서 디자인 시스템 유틸을 직접 가져다 쓰기 좋습니다. 경로/케이스도 파일명과 일치합니다.
packages/design-system/src/components/popup/PopupContainer.tsx (1)
32-39: ESC 닫기 처리 LGTM열림 상태에서만 리스너 등록/해제하며 메모리 릭 우려도 없습니다.
packages/design-system/src/components/popup/Popup.tsx (1)
15-16: Controlled 입력 지원 방향 LGTMinputValue/onInputChange 도입으로 Popup → Input 제어 흐름이 명확해졌습니다. 상위 컨테이너에서 상태 관리하기 용이합니다.
Also applies to: 30-31
constantly-dev
left a comment
There was a problem hiding this comment.
수고하셨습니다! 확실히 extenstion.. 어렵네요....
| @@ -1 +1,39 @@ | |||
| console.log("백그라운드 기능") No newline at end of file | |||
| console.log('백그라운드 기능'); | |||
| createdAt: new Date().toISOString(), | ||
| }; | ||
|
|
||
| console.log('저장 데이터:', saveData); |
| "extends": "@pinback/typescript-config/react.json", | ||
| "compilerOptions": { | ||
| "skipLibCheck": true, | ||
| "noImplicitAny": false, // any 타입 허용 |
There was a problem hiding this comment.
요건 조금 익스텐션이 레거시한 부분이 있어서 그런 건가요??
apps/extension/vite.config.ts
Outdated
| resolve: { | ||
| alias: { | ||
| '@': resolve(__dirname, './src'), | ||
| '@assets': resolve(__dirname, './src/assets'), | ||
| '@api': resolve(__dirname, './src/api'), | ||
| '@utils': resolve(__dirname, './src/utils'), | ||
| '@constants': resolve(__dirname, './src/constants'), |
There was a problem hiding this comment.
tsconfig에도 절대 경로 설정해서 ts가 인식할 수 있게 해주고, vite.config.ts에도 절대 경로 이렇게 설정해서 vite가 이해하게 하는 게 맞긴합니다! 둘다 인식해야 되는데, 하나 추가될 때마다 이렇게 2개를 전부 바꾸는 것이 불편하니까 vite에 tsconfig path설정을 가져와서 연동해주는 플러그인이 존재해요!
apps/client에도 있을텐데 vite-tsconfig-paths이 라이브러리 의존 추가해주고 vite.config.ts에
import tsconfigPaths from 'vite-tsconfig-paths';이거 import한 다음에 plugin안에 tsconfigPaths() 해주면 됩니다!
그러면 vite가 알아서 tsconfig 절대 경로 설정 참고해서 인식해서 중앙화된 하나의 config만 두기 때문에 유지보수가 쉬워지는 거죠!!
There was a problem hiding this comment.
아하! 지금 현재 익스텐션 vite.config.ts에서 import tsconfigPaths from 'vite-tsconfig-paths'; 이게 불러는 오고 있었는데 중복된 기능이였군요, 해당 방향으로 수정해두었습니다!
| </div>, | ||
| document.body // body 위에서 렌더링 되게 함! | ||
| ); | ||
| ) as unknown as React.ReactElement; |
There was a problem hiding this comment.
오호... pr에서 말씀하신 부분이 이거일까요??
| useEffect(() => { | ||
| chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { | ||
| const activeTab = tabs[0]; | ||
| if (!activeTab?.url) return; | ||
|
|
||
| const currentUrl = activeTab.url; | ||
|
|
||
| chrome.storage.local.set({ bookmarkedUrl: currentUrl }); | ||
|
|
||
| const imageUrl = await OgImageFetcher({ url: currentUrl }); | ||
| // 개발중에는 잠시 주석처리 | ||
| // const isInternalChromePage = | ||
| // /^chrome:\/\//.test(currentUrl) || | ||
| // /^edge:\/\//.test(currentUrl) || | ||
| // /^about:/.test(currentUrl); | ||
| // // chrome-extension:// 은 내부 페이지로 취급하지 않음 | ||
|
|
||
| // if (isInternalChromePage || !imageUrl?.title) { | ||
| // window.close(); | ||
| // return; | ||
| // } | ||
|
|
||
| const newMeta = { | ||
| url: currentUrl, | ||
| title: imageUrl.title ?? '', | ||
| description: imageUrl.description ?? '', | ||
| imgUrl: imageUrl.image ?? '', | ||
| }; | ||
|
|
||
| setMeta(newMeta); | ||
|
|
||
| chrome.storage.local.set({ titleSave: newMeta.title }); | ||
| }); | ||
| }, []); | ||
|
|
||
| return meta; | ||
| }; |
There was a problem hiding this comment.
부분적으로 역할이 많은거 같아서 몇 개의 함수로 분리해도 좋을 것 같은데 어떻게 생각하시나요?
There was a problem hiding this comment.
오 피드백 감사합니다! 역할에 따라 분리해두었습니다
| chrome.bookmarks.create( | ||
| { | ||
| parentId: '1', | ||
| title: params.title || params.url, | ||
| url: params.url, | ||
| }, | ||
| (newBookmark) => { | ||
| console.log('크롬 북마크바에 저장 완료:', newBookmark); | ||
| } | ||
| ); |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (3)
apps/extension/src/App.tsx (3)
171-173: 무효 저장 방지를 위한 버튼 비활성화메타(url) 미준비 또는 리마인드 오류 시 비활성화해 UX/데이터 무결성 보완.
- <Button size="medium" onClick={handleSave}> + <Button + size="medium" + onClick={handleSave} + disabled={!url || (isRemindOn && (!!dateError || !!timeError))} + > 저장 </Button>
47-72: 저장 전 유효성 게이트 + await 누락 + 중복 payload/콘솔 제거리마인드 켠 상태에서 에러가 있어도 저장되고, 비동기 저장 완료를 보장하지 않으며, 동일 payload를 재생성·로그로만 남깁니다(라인 71 Lint 경고도 발생). 아래처럼 한 번에 정리하세요.
@@ - const handleSave = async () => { - save({ + const handleSave = async () => { + if (isRemindOn) { + const dErr = validateDate(date); + const tErr = validateTime(time); + setDateError(dErr); + setTimeError(tErr); + if (dErr || tErr) return; + } + await save({ url, title, description, imgUrl, memo, isRemindOn, selectedCategory: selected, date: isRemindOn ? date : null, time: isRemindOn ? time : null, }); - const saveData = { - url, - title, - description, - imgUrl, - memo, - isRemindOn, - selectedCategory: selected, - date: isRemindOn ? date : null, - time: isRemindOn ? time : null, - createdAt: new Date().toISOString(), - }; - console.log('저장된 데이터:', saveData); };
156-163: DateTime(time) onChange 미전파 + 12/24시간 포맷 불일치로 검증 실패 위험디자인시스템 DateTime(time)이 부모 onChange를 호출하지 않아 시간 상태/에러가 갱신되지 않습니다. 또한 UI는 ‘오전/오후 HH:MM’(12h)로 표시하지만 validateTime은 ‘HH:MM’(24h)만 허용해 포맷 불일치 시 검증이 실패합니다. DS에서 onChange를 호출하되 24h ‘HH:MM’으로 전달하도록 정합성 맞추는 것을 권장합니다. 빠른 패치 제안(DS 내부):
@@ packages/design-system/src/components/dateTime/DateTime.tsx - setInput(formatTime12(next)); + const formatted12 = formatTime12(next); + setInput(formatted12); + // onChange는 24h 'HH:MM'로 전달 + const h = next.slice(0, 2).padStart(2, '0'); + const m = next.slice(2, 4).padStart(2, '0'); + const normalized24 = `${h}:${m}`; + props.onChange?.(normalized24); @@ - setInput(formatTime12(next)); + const formatted12 = formatTime12(next); + setInput(formatted12); + const h = next.slice(0, 2).padStart(2, '0'); + const m = next.slice(2, 4).padStart(2, '0'); + props.onChange?.(`${h}:${m}`); @@ - setInput(formatTime12(next)); + const formatted12 = formatTime12(next); + setInput(formatted12); + const h = next.slice(0, 2).padStart(2, '0'); + const m = next.slice(2, 4).padStart(2, '0'); + props.onChange?.(`${h}:${m}`);임시 워크어라운드로 App 측에서도 normalize 가능(DS 반영 전):
@@ - const handleTimeChange = (value: string) => { - setTime(value); - setTimeError(validateTime(value)); - }; + const handleTimeChange = (value: string) => { + // '오전/오후 HH:MM' → 24h 'HH:MM' + const m = value.match(/(오전|오후)?\s*(\d{1,2}):(\d{2})/); + const normalized = + m + ? (() => { + let hh = parseInt(m[2], 10); + const mm = m[3]; + if (m[1] === '오후' && hh < 12) hh += 12; + if (m[1] === '오전' && hh === 12) hh = 0; + return `${String(hh).padStart(2, '0')}:${mm}`; + })() + : value; + setTime(normalized); + setTimeError(validateTime(normalized)); + };
🧹 Nitpick comments (6)
apps/extension/src/App.tsx (6)
166-169: 리마인드 OFF 상태에서는 에러 메시지 숨김비활성 상태에서 오류 노출은 혼란을 줍니다.
- {dateError && <p className="body3-r text-error">{dateError}</p>} - {timeError && <p className="body3-r text-error">{timeError}</p>} + {isRemindOn && dateError && ( + <p className="body3-r text-error">{dateError}</p> + )} + {isRemindOn && timeError && ( + <p className="body3-r text-error">{timeError}</p> + )}
40-42: 스위치 토글 시 즉시 검증/리셋ON에서 현재 값 검증, OFF에서 에러 리셋으로 상태 일관성 확보.
- const handleSwitchChange = (checked: boolean) => { - setIsRemindOn(checked); - }; + const handleSwitchChange = (checked: boolean) => { + setIsRemindOn(checked); + if (checked) { + setDateError(validateDate(date)); + setTimeError(validateTime(time)); + } else { + setDateError(''); + setTimeError(''); + } + };
131-135: Textarea를 제어 컴포넌트로 유지value 미지정 시 언컨트롤드로 전환됩니다.
- <Textarea + <Textarea maxLength={100} placeholder="나중에 내가 꺼내줄 수 있게 살짝 적어줘!" - onChange={(e) => setMemo(e.target.value)} + value={memo} + onChange={(e) => setMemo(e.target.value)} />
76-83: 카테고리 저장 검증 강화(트림/빈값/리셋) 및 후처리공백만 입력, 20자 초과 처리, 닫기 시 상태 리셋. 실제 저장 로직 TODO도 남겨두세요.
- const saveCategory = () => { - if (categoryTitle.length >20){ + const saveCategory = () => { + const name = categoryTitle.trim(); + if (name.length === 0) { + setIsPopError(true); + setErrorTxt('제목을 입력해주세요'); + return; + } + if (name.length > 20){ setIsPopError(true); setErrorTxt('20자 이내로 작성해주세요'); - } else{ - setIsPopupOpen(false); + } else{ + // TODO: 카테고리 영속화(storage/API) 후 Dropdown 갱신 + setIsPopupOpen(false); + setCategoryTitle(''); + setIsPopError(false); + setErrorTxt(''); } }
124-126: 팝업 오픈 시 입력/에러 상태 초기화재오픈 시 이전 에러가 남지 않도록 초기화.
- onAddItem={() => setIsPopupOpen(true)} + onAddItem={() => { + setCategoryTitle(''); + setIsPopError(false); + setErrorTxt(''); + setIsPopupOpen(true); + }}
22-28: 기본 날짜/시간 값 하드코딩 재검토초기값을 빈 문자열로 두고 스위치 ON 시에만 요구/검증하는 편이 안전합니다.
- const [date, setDate] = useState('2025.10.10'); - const [time, setTime] = useState('19:00'); + const [date, setDate] = useState(''); + const [time, setTime] = useState('');
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/extension/src/App.tsx(1 hunks)packages/design-system/src/components/popup/Popup.tsx(3 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/design-system/src/icons/iconNames.ts
- packages/design-system/src/components/popup/Popup.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-08T11:47:10.642Z
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.
Applied to files:
apps/extension/src/App.tsx
🧬 Code graph analysis (1)
apps/extension/src/App.tsx (5)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
validateDate(2-28)validateTime(31-48)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(11-55)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-52)packages/design-system/src/components/textarea/Textarea.tsx (1)
Textarea(9-31)packages/design-system/src/components/dateTime/DateTime.tsx (1)
DateTime(56-176)
🪛 GitHub Check: lint
apps/extension/src/App.tsx
[warning] 71-71:
Unexpected console statement
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
apps/extension/src/hooks/usePageMeta.ts (1)
4-9: imgUrl → imageUrl로 프로퍼티 네이밍 통일 필요디자인 시스템/OG 응답과 용어를 맞추세요. 관련 파일 전체 일괄 변경 요망.
export interface PageMeta { url: string; title: string; description: string; - imgUrl: string; + imageUrl: string; } @@ const [meta, setMeta] = useState<PageMeta>({ url: '', title: '', description: '', - imgUrl: '', + imageUrl: '', }); @@ - title: imageUrl?.title ?? '', - description: imageUrl?.description ?? '', - imgUrl: imageUrl?.image ?? '', + title: ogMeta?.title ?? '', + description: ogMeta?.description ?? '', + imageUrl: ogMeta?.image ?? '', @@ - chrome.storage.local.set({ titleSave: newMeta.title }); + chrome.storage.local.set({ lastPageMeta: newMeta });검증 스크립트(레포 전체 사용처 탐색):
#!/bin/bash rg -nP --type ts --type tsx -C1 '\bimgUrl\b|\bimageUrl\b'Also applies to: 20-25, 14-17, 52-53
apps/extension/src/hooks/useSaveBookmarks.ts (2)
1-11: imgUrl → imageUrl로 인터페이스 정합성 맞추기위 usePageMeta와 함께 일괄 리팩터 필요.
interface SaveBookmarkParams { url: string; title: string; description: string; - imgUrl: string; + imageUrl: string; memo: string; isRemindOn: boolean; selectedCategory: string | null; date: string | null; time: string | null; }
32-41: 북마크바 parentId 하드코딩(‘1’) 제거 + Promise 화환경에 따라 실패 가능. 동적 탐색 후 생성하고, 완료까지 await 권장.
- chrome.bookmarks.create( - { - parentId: '1', - title: params.title || params.url, - url: params.url, - }, - (newBookmark) => { - console.log('크롬 북마크바에 저장 완료:', newBookmark); - } - ); + const tree = await chrome.bookmarks.getTree(); + const findBar = (nodes: chrome.bookmarks.BookmarkTreeNode[]): string | undefined => { + for (const n of nodes) { + if ((n as any).folderType === 'bookmarks-bar') return n.id; + if (n.children?.length) { + const r = findBar(n.children); + if (r) return r; + } + } + }; + const parentId = findBar(tree); + const newBookmark = await new Promise<chrome.bookmarks.BookmarkTreeNode>((resolve, reject) => { + chrome.bookmarks.create( + { parentId, title: params.title || params.url, url: params.url }, + (node) => { + const err = chrome.runtime?.lastError; + if (err) return reject(err); + resolve(node); + } + ); + }); + if (import.meta.env.MODE !== 'production') { + console.log('북마크 저장 완료:', newBookmark); + }
🧹 Nitpick comments (7)
apps/extension/vite.config.ts (2)
20-23: svg 스프라이트 아이콘 경로를 절대 경로로 고정해 빌드 안정성 확보모노레포 루트/작업 디렉터리 변동 시 상대경로가 깨질 수 있습니다.
resolve(__dirname, …)로 고정해주세요.- svgSpritePlugin({ - iconDirs: ['../../packages/design-system/src/icons/source'], + svgSpritePlugin({ + iconDirs: [resolve(__dirname, '../../packages/design-system/src/icons/source')], symbolId: 'icon-[name]', inject: 'body-last', }),
12-16: (니트픽) tsconfigPaths를 react 플러그인보다 먼저 등록 권장모듈 해석 단계에서 경로가 먼저 풀리도록 순서를 바꾸면 트러블슈팅이 수월합니다. 큰 차이는 없으나 일관된 패턴 권장합니다.
- react(), - tsconfigPaths({ + tsconfigPaths({ projects: [resolve(__dirname, './tsconfig.json')], }), + react(),apps/extension/src/hooks/usePageMeta.ts (1)
32-36: (니트픽) 선언한 currentUrl을 활용해 일관성 유지가독성 차원에서 동일 변수를 사용하세요.
- const newMeta = await getOgMeta(activeTab.url); + const newMeta = await getOgMeta(currentUrl);apps/extension/src/hooks/useSaveBookmarks.ts (4)
21-27: 스토리지 타입 any 제거 및 북마크 레코드 타입 선언정적 분석 경고 해소 및 추후 유지보수 용이.
+interface Bookmark extends SaveBookmarkParams { + createdAt: string; +} - - const result = await new Promise<{ bookmarks?: any[] }>((resolve) => { + const result = await new Promise<{ bookmarks?: Bookmark[] }>((resolve) => { chrome.storage.local.get(['bookmarks'], (items) => resolve(items)); }); - - const bookmarks = result.bookmarks || []; + const bookmarks: Bookmark[] = result.bookmarks ?? []; bookmarks.push(saveData);
28-31: storage.set 콜백 에러 처리 보강(chrome.runtime.lastError 확인)콜백 기반 API는 오류를 lastError로 제공합니다.
- await new Promise<void>((resolve) => { - chrome.storage.local.set({ bookmarks }, resolve); - }); + await new Promise<void>((resolve, reject) => { + chrome.storage.local.set({ bookmarks }, () => { + const err = chrome.runtime?.lastError; + if (err) return reject(err); + resolve(); + }); + });
39-40: 테스트용 console 제거 또는 환경 분기lint 경고 해결을 위해 prod에서 로그 비활성화하세요.
- console.log('크롬 북마크바에 저장 완료:', newBookmark); + if (import.meta.env.MODE !== 'production') { + console.log('크롬 북마크바에 저장 완료:', newBookmark); + } @@ - console.error('저장 중 오류:', error); + console.error('저장 중 오류:', error);Also applies to: 45-45
43-46: 성공 시점에window.close()호출 고려
manifest.json에storage와bookmarks권한이 이미 등록되어 있어 정상 동작합니다. (선택 사항)
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/extension/src/hooks/usePageMeta.ts(1 hunks)apps/extension/src/hooks/useSaveBookmarks.ts(1 hunks)apps/extension/vite.config.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/extension/src/hooks/usePageMeta.ts (1)
apps/extension/src/utils/OGFetch.ts (1)
OgImageFetcher(1-8)
🪛 GitHub Check: lint
apps/extension/src/hooks/useSaveBookmarks.ts
[warning] 45-45:
Unexpected console statement
[warning] 39-39:
Unexpected console statement
[warning] 21-21:
Unexpected any. Specify a different type
🔇 Additional comments (2)
apps/extension/vite.config.ts (1)
15-16: tsconfigPaths 프로젝트 경로 로컬 tsconfig로 맞춘 것 👍apps/extension 기준으로 중앙화되어 유지보수 용이합니다.
apps/extension/src/hooks/useSaveBookmarks.ts (1)
16-19: createdAt 추가 로직 깔끔합니다ISO 타임스탬프 사용 적절합니다.
| const getOgMeta = async (url: string) => { | ||
| const imageUrl = await OgImageFetcher({ url }); | ||
| return { | ||
| url, | ||
| title: imageUrl?.title ?? '', | ||
| description: imageUrl?.description ?? '', | ||
| imgUrl: imageUrl?.image ?? '', | ||
| }; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
OG 메타 페처 안전 가드 및 변수명 명확화
OgImageFetcher가 undefined를 반환할 수 있어 try/catch + 옵셔널 체이닝이 필요합니다. 변수명도 ogMeta로 정리 권장.
-const getOgMeta = async (url: string) => {
- const imageUrl = await OgImageFetcher({ url });
- return {
- url,
- title: imageUrl?.title ?? '',
- description: imageUrl?.description ?? '',
- imgUrl: imageUrl?.image ?? '',
- };
-};
+const getOgMeta = async (url: string) => {
+ try {
+ const ogMeta = await OgImageFetcher({ url });
+ return {
+ url,
+ title: ogMeta?.title ?? '',
+ description: ogMeta?.description ?? '',
+ imageUrl: ogMeta?.image ?? '',
+ };
+ } catch (e) {
+ console.warn('OG 메타 수집 실패', e);
+ return { url, title: '', description: '', imageUrl: '' };
+ }
+};📝 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 getOgMeta = async (url: string) => { | |
| const imageUrl = await OgImageFetcher({ url }); | |
| return { | |
| url, | |
| title: imageUrl?.title ?? '', | |
| description: imageUrl?.description ?? '', | |
| imgUrl: imageUrl?.image ?? '', | |
| }; | |
| }; | |
| const getOgMeta = async (url: string) => { | |
| try { | |
| const ogMeta = await OgImageFetcher({ url }); | |
| return { | |
| url, | |
| title: ogMeta?.title ?? '', | |
| description: ogMeta?.description ?? '', | |
| imageUrl: ogMeta?.image ?? '', | |
| }; | |
| } catch (e) { | |
| console.warn('OG 메타 수집 실패', e); | |
| return { url, title: '', description: '', imageUrl: '' }; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In apps/extension/src/hooks/usePageMeta.ts around lines 10 to 18, the current
getOgMeta uses OgImageFetcher without error handling and names the result
imageUrl which is misleading; wrap the await OgImageFetcher({ url }) call in a
try/catch, assign its result to a clearer variable name ogMeta, and in the catch
(or if ogMeta is falsy) return the default object with empty
title/description/imgUrl and the original url; ensure optional chaining is used
when reading ogMeta fields so the function never throws if OgImageFetcher
returns undefined or throws.

📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
1. 익스텐션 내 공컴 빌드 에러 (특히 wheelPicker..)
(문제1) 사용 ui 라이브러리와의 wheelPicker코드 충돌
라이브러리에서 필수로 정의된 props(children, options)를 받지 않아 에러
(TS2741 에러코드!!)
React.ComponentProps<typeof …> → FunctionComponent와 JSXElementConstructor 간 제네릭 충돌.
(문제2) extension tsconfig 충돌
여기 익스텐션 설정이 꽤 빡빡하여 빌드 에러..
=> 익스텐션 tsconfig 느슨하게 설정
2. PopupContainer 타입 오류 수정
React 19 + TypeScript 환경에서 createPortal을 사용하는 컴포넌트가 React.FC나 기본 함수 시그니처로 선언되어 있을 때,
리턴 타입(ReactPortal)과 JSX가 기대하는 타입(ReactNode) 사이에 호환성 에러가 발생했어요,!
Type 'ReactPortal' is not assignable to type 'ReactNode'이 문제로 PopupContainer를 JSX에서 <PopupContainer ... />처럼 사용할 수 없었고, 빌드 에러 발생
[Why]
React 19 타입 정의 이슈로 ReactPortal이 ReactNode랑 호환이 안 됨..
PopupContainer가 createPortal을 리턴하면서 타입이 ReactPortal → JSX에서 못 씀 문제가 있었음
[How]
리턴 타입을 ReactNode로 바꾸고, createPortal 결과를 ReactElement로 캐스팅
📷 Screenshot
Summary by CodeRabbit
New Features
Style / UI
Chores