Feat(재림): QA 반영 | 카테고리 선택 로직 및 대시보드 랜딩 수정#136
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (3)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including You can disable this status message by setting the Walkthrough온보딩 알람 취소 동작 보정, 온보딩 단계·리다이렉트·FCM 토큰 처리 변경, 확장 프로그램 저장 키 및 탭 오픈 방식 수정, 팝업(add/edit)에서 날짜/시간·카테고리 로직 강화, 디자인시스템의 DateTime/검증/Dropdown에 입력·API·UI 변경 및 아이콘 추가. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as 사용자
participant MainCard as Onboarding MainCard
participant FCM as FCM 서비스
participant Local as localStorage
participant Router as 라우터
User->>MainCard: 페이지 진입
MainCard->>Local: location.search에서 email 저장
MainCard->>FCM: 마운트 시 FCM 토큰 요청
FCM-->>MainCard: 토큰/에러
User->>MainCard: 알람 선택/단계 진행
MainCard->>MainCard: remindTime 계산(고정/선택/정규화)
MainCard->>Router: 플랫폼별(step) 분기 및 리다이렉트
sequenceDiagram
autonumber
actor User as 사용자
participant MainPop as Extension MainPop
participant Query as useGetCategoriesExtension
participant API as post/putArticle
participant Tabs as chrome.tabs
User->>MainPop: 팝업 열기 (add/edit)
MainPop->>Query: 카테고리 조회(드롭다운 open -> refetch)
User->>MainPop: 날짜/시간/카테고리/메모 입력
User->>MainPop: 저장 클릭
alt add
MainPop->>API: postArticle(payload with remindTime/date/time)
else edit
MainPop->>API: putArticle(articleId, payload)
end
User->>Tabs: 로고 클릭 → chrome.tabs.create로 새 탭 오픈
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
apps/extension/src/hooks/useCategoryManager.ts (1)
24-33: 최대 10개 제한 로직을 saveCategory에도 적용.UI에서 버튼을 숨기더라도 직접 호출 시 10개 초과 저장이 가능합니다. 저장 함수에 하드가드를 추가하세요.
const saveCategory = (onSuccess?: (category: Category) => void) => { + if (options.length >= 10) { + setIsPopError(true); + setErrorTxt("카테고리는 최대 10개까지 생성할 수 있어요"); + return; + } if (categoryTitle.length > 20) { setIsPopError(true); setErrorTxt("20자 이내로 작성해주세요"); return; }apps/extension/src/apis/axiosInstance.ts (1)
64-66: 토큰 재발급 시 하드코딩된 이메일 사용(보안/기능 치명적).401/403 처리에서
'test@gmail.com'으로 토큰을 갱신하고 있습니다. 실제 사용자 이메일을 사용해야 하며, 그렇지 않으면 오인증/권한 오류가 발생합니다.- const newToken = await fetchToken('test@gmail.com'); + const email = await new Promise<string | undefined>((resolve) => { + chrome.storage.local.get('email', (result) => resolve(result.email)); + }); + const newToken = await fetchToken(email);apps/extension/src/background.ts (1)
18-24: 메시지 출처 검증 추가.아무 곳에서나
SET_TOKEN을 보낼 수 없도록 sender 검증을 추가하세요(확장 내부 메시지만 허용).-chrome.runtime.onMessage.addListener((message) => { +chrome.runtime.onMessage.addListener((message, sender) => { + if (sender.id !== chrome.runtime.id) { + return; // 외부/알 수 없는 발신자 차단 + } if (message.type === 'SET_TOKEN') { chrome.storage.local.set({ 'token': message.token }, () => { - console.log('Token saved!', message.token); + // token stored }); } });apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
65-68: Firebase 초기화는 컴포넌트 외부 싱글턴으로 이동 권장렌더마다
initializeApp/getMessaging호출은 중복 초기화 리스크가 있습니다. 모듈 레벨 싱글턴으로 안전화하세요.- const app = initializeApp(firebaseConfig); - const messaging = getMessaging(app); + // 파일 상단(컴포넌트 밖)으로 이동 + // import { getApps, getApp } from 'firebase/app'; + // const app = getApps().length ? getApp() : initializeApp(firebaseConfig); + // const messaging = getMessaging(app);apps/extension/src/pages/MainPop.tsx (3)
105-124: edit 프리필 로직이 카테고리 로딩에 종속되어 실행되지 않습니다카테고리 쿼리가 enabled:false라 초기 로딩이 없고, 드롭다운을 열기 전까지 조건이 거짓이라서 메모/리마인드/선택값이 세팅되지 않습니다.
적용 제안:
- useEffect(() => { - if ( - type === 'edit' && - savedData && - categoryData?.data?.categories?.length - ) { + useEffect(() => { + if (type === 'edit' && savedData) { setMemo(savedData.memo ?? ''); setIsArticleId(savedData.id ?? 0); if (savedData.remindAt) { const [rawDate, rawTime] = savedData.remindAt.split('T'); setDate(updateDate(rawDate)); setTime(updateTime(rawTime)); setIsRemindOn(true); } if (savedData.categoryResponse) { setSelected(savedData.categoryResponse?.categoryId.toString()); setSelectedCategoryName(savedData.categoryResponse?.categoryName); } } - }, [type, savedData, categoryData?.data?.categories?.length]); + }, [type, savedData]);
169-186: 유효성 에러 상태에서도 저장 진행됨isRemindOn=true이고 dateError/timeError가 있거나 값이 비어도 저장을 막지 않습니다.
적용 제안:
const handleSave = async () => { const currentDate = date; const currentTime = time; if (!selected || parseInt(selected) === 0) { alert('카테고리를 선택해주세요!'); return; } + if (isRemindOn) { + if (dateError || timeError || !date || !time) { + alert('리마인드 날짜/시간을 올바르게 입력하세요'); + return; + } + }
153-161: validateDate/Time가 기대하는 입력형식과 onChange 값 불일치 가능성디자인시스템의 validate*는 숫자만(YYYYMMDD/HHMM)을 기대합니다. onChange 값에 구분자(., :)가 포함될 수 있으므로 숫자만 추출해 검증하세요.
적용 제안:
const handleDateChange = (value: string) => { setDate(value); - setDateError(validateDate(value)); + setDateError(validateDate(value.replace(/\D/g, ''))); }; const handleTimeChange = (value: string) => { setTime(value); - setTimeError(validateTime(value)); + setTimeError(validateTime(value.replace(/\D/g, ''))); };
🧹 Nitpick comments (15)
apps/extension/src/hooks/usePageMeta.ts (2)
40-55: 내부 브라우저 페이지 가드 복구 제안(개발 중 임시 주석 해제 조건부).chrome://, edge://, about: 페이지에서 OG 요청을 건너뛰도록 가드를 복구하면 불필요한 네트워크 호출과 팝업 닫힘 오동작을 막을 수 있습니다. PROD에서만 적용하도록 조건을 두는 것도 방법입니다.
아래처럼 getOgMeta 호출 전에 가드를 배치해 주세요.
const newMeta = await getOgMeta(currentUrl); - // 개발중에는 잠시 주석처리 + // 내부 브라우저 페이지는 OG 요청 건너뛰기 (개발 중에는 해제 가능) + if (import.meta.env.PROD) { + const isInternalChromePage = + /^chrome:\/\//.test(currentUrl) || + /^edge:\/\//.test(currentUrl) || + /^about:/.test(currentUrl); + // chrome-extension:// 은 내부 페이지로 취급하지 않음 + if (isInternalChromePage) { + setLoading(false); + return; + } + }
30-58: 언마운트 후 setState 방지.비동기 콜백에서 컴포넌트 언마운트 후 setState가 발생할 수 있습니다. 취소 플래그를 추가해 주세요.
useEffect(() => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + let cancelled = false; + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const activeTab = tabs[0]; if (!activeTab?.url) { - setLoading(false); + if (!cancelled) setLoading(false); return; } const currentUrl = activeTab.url; chrome.storage.local.set({ bookmarkedUrl: currentUrl }); const newMeta = await getOgMeta(currentUrl); setMeta(newMeta); - setLoading(false); + if (!cancelled) setLoading(false); chrome.storage.local.set({ titleSave: newMeta.title }); }); - }, []); + return () => { + cancelled = true; + }; + }, []);apps/extension/src/pages/DuplicatePop.tsx (2)
2-2: 변수명 오타 및 이미지 접근성(alt) 보완.import 변수명이
extesionPop(오타)입니다. 또한 img에 alt를 추가해 접근성을 보완해 주세요.-import extesionPop from '@assets/extension_pop.svg' +import extensionPop from '@assets/extension_pop.svg' ... - <img src={extesionPop} className="w-[7.2rem] h-[7.2rem] m-auto text-center"/> + <img src={extensionPop} alt="핀백 확장 프로그램 안내" className="w-[7.2rem] h-[7.2rem] m-auto text-center" />Also applies to: 11-11
14-23: button 기본 type 지정.폼 내부로 이동될 가능성에 대비해 버튼에 type="button"을 명시해 부작용을 방지하세요.
- <button + <button + type="button" className="border-gray200 sub5-sb bg-white-bg text-font-black-1 w-[10.8rem] rounded-[0.4rem] border py-[0.85rem]" onClick={onLeftClick} > ... - <button + <button + type="button" className="sub5-sb bg-gray900 text-white-bg w-[10.8rem] rounded-[0.4rem] py-[0.85rem]" onClick={onRightClick} >apps/extension/src/hooks/useCategoryManager.ts (1)
40-41: 중복 카테고리명 처리(선택).동일한 이름이 이미 존재할 때 중복 추가를 막는 방어 로직이 있으면 UX 개선됩니다.
- setOptions((prev) => [...prev, newCategory.categoryName]); + setOptions((prev) => + prev.includes(newCategory.categoryName) ? prev : [...prev, newCategory.categoryName] + );apps/extension/src/apis/axiosInstance.ts (1)
49-70: 동시 401 폭주 시 단일 비행(single-flight) 보호 권장.동시에 여러 요청이 401을 맞으면 다중 갱신이 발생할 수 있습니다. 진행 중인 refresh Promise를 공유하는 형태로 보호하는 게 안전합니다.
원하시면 단일 비행 토큰 갱신 유틸을 제안해 드립니다.
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (3)
100-108: 앱 초기 마운트 시 FCM 토큰 요청 시도는 OK, 실패시 흐름 재검토권한 거부/토큰 실패 시 alert만으로 끝나 UX가 끊길 수 있습니다. 재시도 버튼/지연 요청 등도 고려하세요.
134-138: 동등 비교는 엄격 비교(===)로 통일하세요.불필요한 타입 강제 변환을 피하고 일관성을 높입니다.
- if (alarmSelected==1){ + if (alarmSelected === 1){ - } else if (alarmSelected==2){ + } else if (alarmSelected === 2){ @@ - } else if ( (isMac && step === 5) || (!isMac && step==4)) { + } else if ((isMac && step === 5) || (!isMac && step === 4)) {Also applies to: 148-152
152-169: 에러시에도 홈으로 리다이렉트하면 문제 인지 어려움
onError에서 저장된 이메일이 있으면 바로 리다이렉트하면 실패 원인을 숨깁니다. 최소한 토스트/재시도 또는 오류 화면 전환을 고려하세요.API가
fcmToken: null을 허용하는지도 확인 바랍니다.packages/design-system/src/components/dateTime/DateTime.tsx (1)
106-111: 접근성/호환성 소소 개선 제안
aria-label부여와autoComplete="off"추가를 고려하세요.- <input + <input type="text" className={dateTimeTxtStyles({ state })} value={formatted} onBeforeInput={handleBeforeInput} onKeyDown={handleKeyDown} placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'} inputMode="numeric" + aria-label={type === 'date' ? '날짜 입력' : '시간 입력'} + autoComplete="off" disabled={isDisabled} />apps/extension/src/App.tsx (1)
27-28: 확인: manifest에 'tabs' 권한 선언됨 — 폴백 추가 권장apps/extension/manifest.json의 permissions 배열에 "tabs"가 포함되어 있어 chrome.tabs.create 호출은 권한 측면에서 허용됩니다(스크립트 출력 확인). 다만 확장 외 환경(테스트 페이지, 웹 빌드 등)이나 런타임에서 chrome.tabs가 없을 경우를 대비해 안전한 폴백을 추가하세요.
- const handleDuplicateRightClick = () => { - chrome.tabs.create({ url: 'https://pinback.today' }); - }; + const handleDuplicateRightClick = () => { + if (typeof chrome !== 'undefined' && chrome.tabs?.create) { + chrome.tabs.create({ url: 'https://pinback.today' }); + } else { + window.open('https://pinback.today', '_blank'); + } + };apps/client/src/shared/utils/ValidateData.ts (2)
32-32: 마이크로카피 정합성"HH:MM 형식"이라고 쓰면서 예시는 "2312"입니다. "시간 4자리를 입력하세요 (예: 2312)"로 바꾸거나 예시를 "23:12"로 통일하세요. 디자인시스템 메시지와도 함께 맞춰주세요.
12-19: 연도 경계값 가드(선택)JS Date(year<100)은 1900+year로 보정됩니다. 원치 않는 과거 연도(예: 00010101→1900)가 들어오지 않도록 최소 연도(예: 1970 또는 2000)를 두는 것을 권장합니다.
apps/extension/src/pages/MainPop.tsx (2)
163-165: 스위치 OFF 시 에러 표시 잔존OFF로 바꾸면 에러 강조를 함께 해제하는 편이 UX상 자연스럽습니다.
적용 제안:
const handleSwitchChange = (checked: boolean) => { setIsRemindOn(checked); + if (!checked) { + setDateError(''); + setTimeError(''); + } };
146-151: Dropdown 인덱스 의존성 감소 제안idx로 categoryId를 역추적하면 정렬/필터 변경 시 오동작 위험이 있습니다. 옵션 value에 categoryId를 넣고 그대로 설정하도록 API를 확장하는 것을 권장합니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (12)
apps/extension/src/assets/extension_pop.svgis excluded by!**/*.svgapps/extension/src/assets/extension_thumb.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/chippi_profile.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/extension_pop.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/extension_thumb.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/ic_extension.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/main_header_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 (16)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(0 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(6 hunks)apps/client/src/shared/utils/ValidateData.ts(2 hunks)apps/extension/src/App.tsx(3 hunks)apps/extension/src/apis/axiosInstance.ts(1 hunks)apps/extension/src/apis/query/queries.ts(2 hunks)apps/extension/src/background.ts(1 hunks)apps/extension/src/hooks/useCategoryManager.ts(3 hunks)apps/extension/src/hooks/usePageMeta.ts(1 hunks)apps/extension/src/pages/DuplicatePop.tsx(2 hunks)apps/extension/src/pages/MainPop.tsx(7 hunks)packages/design-system/src/components/dateTime/DateTime.tsx(3 hunks)packages/design-system/src/components/dateTime/utils/FormatData.ts(1 hunks)packages/design-system/src/components/dateTime/utils/ValidateData.ts(2 hunks)packages/design-system/src/components/dropdown/Dropdown.tsx(5 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
💤 Files with no reviewable changes (1)
- apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-11T11:48:10.615Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#75
File: apps/extension/src/apis/axiosInstance.ts:30-34
Timestamp: 2025-09-11T11:48:10.615Z
Learning: Pinback 프로젝트에서는 사용자 이메일 저장 시 'email' 키를 사용하도록 통일했습니다 (localStorage 및 chrome.storage.local 모두).
Applied to files:
apps/extension/src/apis/axiosInstance.tsapps/client/src/pages/onBoarding/components/funnel/MainCard.tsxapps/extension/src/background.ts
📚 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/pages/DuplicatePop.tsxpackages/design-system/src/icons/iconNames.ts
🧬 Code graph analysis (7)
apps/client/src/shared/utils/ValidateData.ts (1)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
validateDate(1-28)validateTime(30-43)
packages/design-system/src/components/dateTime/DateTime.tsx (1)
packages/design-system/src/components/dateTime/utils/FormatData.ts (3)
digitsOnly(2-4)formatDate(7-16)formatTime12(18-45)
apps/extension/src/apis/query/queries.ts (1)
apps/extension/src/apis/axios.ts (4)
getCategoriesExtension(26-29)getArticleSaved(51-56)PutArticleRequest(58-63)putArticle(65-68)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
apps/client/src/shared/utils/ValidateData.ts (2)
validateDate(1-28)validateTime(30-43)packages/design-system/src/components/index.ts (2)
validateDate(19-19)validateTime(19-19)
apps/extension/src/hooks/useCategoryManager.ts (1)
apps/extension/src/types/types.ts (1)
Category(16-20)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
apps/client/src/constants/alarms.ts (1)
AlarmsType(11-15)apps/client/src/pages/onBoarding/utils/formatRemindTime.ts (1)
normalizeTime(1-26)
apps/extension/src/pages/MainPop.tsx (5)
apps/extension/src/types/types.ts (1)
ArticleResponse(7-14)apps/extension/src/apis/query/queries.ts (4)
usePostArticle(16-20)usePutArticle(61-71)useGetRemindTime(46-51)useGetCategoriesExtension(36-44)apps/extension/src/utils/remindTimeFormat.ts (3)
updateDate(2-5)updateTime(8-11)combineDateTime(29-37)apps/extension/src/hooks/usePageMeta.ts (1)
usePageMeta(21-61)apps/extension/src/hooks/useSaveBookmarks.ts (1)
useSaveBookmark(13-49)
🔇 Additional comments (13)
apps/extension/src/hooks/useCategoryManager.ts (1)
35-41: API 응답 스키마 확인 필요(res.data. 접근).*
onSuccess에서res.data.categoryId/categoryColor를 직접 참조합니다. axios 응답이{ data: {...} }인지{ data: { data: {...} } }인지 백엔드 일관성 확인이 필요합니다. 불일치 시 undefined 접근이 납니다.원한다면 try-catch와 스키마 가드를 추가하는 패치를 제안할 수 있습니다.
packages/design-system/src/components/dropdown/Dropdown.tsx (1)
13-26: onToggle 추가 및 토글 핸들링 중앙화, 잘 반영됨.토글 상태를 단일 핸들러로 모으고 onToggle 훅을 노출한 점 좋습니다. 사용처에서 열림 상태 추적이 쉬워집니다.
Also applies to: 29-39
apps/extension/src/apis/query/queries.ts (1)
34-44: useGetCategoriesExtension에 options 주입 허용, 유연성 향상.enabled/placeholderData 등 외부 제어가 가능해져 확장성이 좋아졌습니다. 기존 호출부 호환성도 유지됩니다.
호출부에서
useGetCategoriesExtension({ enabled: true })등 실제로 활용되는지 한 번 점검해 주세요.packages/design-system/src/icons/iconNames.ts (1)
5-6: 자동 생성 파일 직접 수정 금지 — 자산은 존재하나 생성 스크립트·런타임 매핑 확인 필요검증: packages/design-system/src/icons/source/extension_pop.svg, packages/design-system/src/icons/source/extension_thumb.svg가 존재하며 packages/design-system/src/icons/iconNames.ts에 'extension_pop' / 'extension_thumb'가 등록되어 있습니다 (lines 5–6).
조치: 이 파일은 자동 생성 파일이므로 생성 스크립트(아이콘 목록/생성기)를 반드시 함께 갱신해야 합니다. 또한 런타임 매핑(아이콘 → 컴포넌트/임포트 매핑)이 실제로 존재하고 빌드 시 올바르게 연결되는지 확인하고, 필요하면 생성 스크립트 또는 매핑 파일을 PR에 포함하세요.
apps/extension/src/App.tsx (3)
13-13: 리터럴 타입 일관성 적용 좋습니다.
'add' | 'edit'로 통일해 가독성과 TS 협업성이 올라갑니다.
23-24: 중복 기사 → 편집 모드 전환 처리 적절합니다.토글 타이밍 이슈를 피하면서 명확한 상태 전환입니다.
38-39: MainPop 전달 props 변경 사항과 일치 확인
type={mainPopType},savedData={isSaved?.data}전달은 현재 타입 정의와 합치합니다. 상위/하위 컴포넌트 간 계약이 바뀌지 않았는지 한 번만 확인 부탁드립니다.packages/design-system/src/components/dateTime/utils/FormatData.ts (1)
32-41: 24→12시간 변환 및 AM/PM 한글 표기 로직 견고해졌습니다.시간 클램프(0–23)와 12시 처리(0→12) 모두 적절합니다.
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (3)
51-60: 익스텐션 → 온보딩 이메일 전달 처리 적합합니다.쿼리스트링에서
참고: 과거 러닝(키 통일) 반영 감사합니다.
208-209: 뒤로가기 노출 조건 변경 OK
step > 0 && step < 4로 불필요한 버튼 노출을 줄였습니다.
134-141: 시간 유효성 검사 필요 — NaN:NaN 발생 가능File: apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (L134-141)
- AlarmsType[alarmSelected - 1].time이 빈 문자열이면 normalizeTime('')가 'NaN:NaN'을 반환할 수 있으므로 setRemindTime 호출 전에 유효성 검사 필요.
- normalizeTime이 한국어 '오전/오후' 표기를 처리하지 못할 수 있으니 AlarmStep에서 전달하는 시간 포맷과의 호환성 확인 필요.
- } else{ - const raw = AlarmsType[alarmSelected - 1].time; - setRemindTime(normalizeTime(raw)) - } + } else { + const raw = AlarmsType[alarmSelected - 1].time; + const norm = normalizeTime(raw); + if (!norm || /NaN/.test(norm)) { + alert('시간을 선택해주세요.'); + return; // 유효한 시간 선택 전에는 진행 금지 + } + setRemindTime(norm); + }packages/design-system/src/components/dateTime/DateTime.tsx (1)
60-65: 초기 rawDigits 동기화 로직 적절
digitsOnly(value ?? '')로 외부값과 일치 유지합니다.apps/extension/src/pages/MainPop.tsx (1)
188-199: window.close() 타이밍과 네트워크 호출 경합 가능성add 플로우에서 save()가 창을 닫아 postArticle 완료 전에 팝업이 종료될 수 있습니다. postArticle onSuccess에서 save()를 호출하도록 순서를 바꾸는 것을 검토하세요.
변경 예시(아이디어):
- save({...}); - postArticle({ ... }); + postArticle( + { ... }, + { onSuccess: () => save({ ... }) } + );
| export const validateDate = (digits: string): string => { | ||
| if (!digits || digits.length !== 8) { | ||
| return '날짜 8자리를 입력하세요 (예: 20250112)'; | ||
| } |
There was a problem hiding this comment.
날짜도 숫자만 허용하도록 1차 검증 추가 필요
현재는 길이만 8을 확인합니다. 숫자만 허용하는 체크를 추가하면 빠르게 실패시킬 수 있습니다.
적용 제안:
export const validateDate = (digits: string): string => {
- if (!digits || digits.length !== 8) {
+ const s = digits.trim();
+ if (!s || s.length !== 8 || !/^\d{8}$/.test(s)) {
return '날짜 8자리를 입력하세요 (예: 20250112)';
}
- const year = parseInt(digits.slice(0, 4), 10);
- const month = parseInt(digits.slice(4, 6), 10);
- const day = parseInt(digits.slice(6, 8), 10);
+ const year = parseInt(s.slice(0, 4), 10);
+ const month = parseInt(s.slice(4, 6), 10);
+ const day = parseInt(s.slice(6, 8), 10);Also applies to: 6-8
🤖 Prompt for AI Agents
In apps/client/src/shared/utils/ValidateData.ts around lines 1-4 (and also apply
same fix to lines 6-8), the validateDate function only checks length==8; update
it to also enforce numeric-only input (e.g. test with /^\d{8}$/) and return the
same '날짜 8자리를 입력하세요 (예: 20250112)' error message when the check fails; keep the
existing length check behavior but combine into a single validation so non-digit
characters are rejected early.
| export const validateTime = (digits: string): string => { | ||
| if (!digits || digits.length !== 4) { | ||
| return 'HH:MM 형식으로 입력하세요 (예: 2312)'; | ||
| } | ||
|
|
||
| if (!match) return '유효한 시간을 작성하세요'; | ||
| const hour = parseInt(match[1], 10); | ||
| const minute = parseInt(match[2], 10); | ||
| const hour = parseInt(digits.slice(0, 2), 10); | ||
| const minute = parseInt(digits.slice(2, 4), 10); |
There was a problem hiding this comment.
숫자 검증 누락으로 비숫자 시간이 통과합니다
"12a3"처럼 비숫자 포함 입력이 길이만 4면 통과합니다. 숫자만 허용하고 NaN을 방지하세요.
적용 제안:
export const validateTime = (digits: string): string => {
- if (!digits || digits.length !== 4) {
+ const s = digits.trim();
+ if (!s || s.length !== 4 || !/^\d{4}$/.test(s)) {
return 'HH:MM 형식으로 입력하세요 (예: 2312)';
}
- const hour = parseInt(digits.slice(0, 2), 10);
- const minute = parseInt(digits.slice(2, 4), 10);
+ const hour = parseInt(s.slice(0, 2), 10);
+ const minute = parseInt(s.slice(2, 4), 10);
- if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
+ if (
+ Number.isNaN(hour) ||
+ Number.isNaN(minute) ||
+ hour < 0 || hour > 23 ||
+ minute < 0 || minute > 59
+ ) {
return '유효한 시간을 작성하세요';
}
return '';
};Also applies to: 38-42
🤖 Prompt for AI Agents
In apps/client/src/shared/utils/ValidateData.ts around lines 30-36 (and also
apply same fix to lines 38-42), the validateTime function only checks length and
thus allows non-numeric chars like "12a3"; update the validation to require
exactly 4 digits (e.g. /^\d{4}$/) before parsing, or explicitly verify each
character is a digit and that parseInt results are not NaN, then proceed to the
existing hour/minute range checks (0-23 and 0-59); return the same error message
when the numeric check fails.
| console.log(info.email); | ||
| }); |
There was a problem hiding this comment.
민감정보(이메일/토큰) 콘솔 로그 제거.
이메일과 액세스 토큰을 콘솔에 남기면 심각한 보안/개인정보 이슈가 됩니다. 제거하세요.
- console.log(info.email);
+ // do not log PII
...
- console.log('Token saved!', message.token);
+ // do not log tokensAlso applies to: 21-22
🤖 Prompt for AI Agents
In apps/extension/src/background.ts around lines 6-7 (and also lines 21-22),
remove the console.log that prints sensitive information (email and access
token); instead stop logging these values entirely or log only non-sensitive,
masked identifiers (e.g., hash or partial redaction) and if you need
auditability send secure telemetry to a controlled backend with proper
encryption/PII handling; ensure no plain tokens or full emails remain in any
console/log output.
| putArticle({ | ||
| articleId: isArticleId, | ||
| data: { | ||
| categoryId: saveData.selectedCategory | ||
| ? parseInt(saveData.selectedCategory) | ||
| : 0, | ||
| memo: saveData.memo, | ||
| now: new Date().toISOString(), | ||
| remindTime: combineDateTime(saveData.date ?? '', saveData.time ?? ''), | ||
| }, | ||
| }); |
There was a problem hiding this comment.
edit API도 동일 이슈
putArticle에도 동일하게 반영해야 합니다.
적용 제안:
putArticle({
articleId: isArticleId,
data: {
categoryId: saveData.selectedCategory
? parseInt(saveData.selectedCategory)
: 0,
memo: saveData.memo,
now: new Date().toISOString(),
- remindTime: combineDateTime(saveData.date ?? '', saveData.time ?? ''),
+ remindTime: isRemindOn
+ ? combineDateTime(saveData.date ?? '', saveData.time ?? '')
+ : null,
},
});📝 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.
| putArticle({ | |
| articleId: isArticleId, | |
| data: { | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| now: new Date().toISOString(), | |
| remindTime: combineDateTime(saveData.date ?? '', saveData.time ?? ''), | |
| }, | |
| }); | |
| putArticle({ | |
| articleId: isArticleId, | |
| data: { | |
| categoryId: saveData.selectedCategory | |
| ? parseInt(saveData.selectedCategory) | |
| : 0, | |
| memo: saveData.memo, | |
| now: new Date().toISOString(), | |
| remindTime: isRemindOn | |
| ? combineDateTime(saveData.date ?? '', saveData.time ?? '') | |
| : null, | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/extension/src/pages/MainPop.tsx around lines 209 to 219, the putArticle
call must receive the same data shape fixes applied to the edit API: convert
selectedCategory to a safe number using parseInt with radix 10 (or Number) and
return undefined/null instead of 0 when no category is chosen, ensure articleId
is passed as a number (Number(isArticleId)) if the API expects a numeric id, and
normalize remindTime/now (e.g., pass null or omit remindTime when empty) so the
payload matches the edit API changes; update the putArticle payload accordingly.
| const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === 'Backspace') { | ||
| e.preventDefault(); | ||
| setRawDigits((prev) => { | ||
| const next = prev.slice(0, -1); | ||
| onChange?.(next); | ||
| return next; | ||
| }); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Backspace를 onKeyDown에만 의존하면 모바일 입력기에서 동작 불안정
모바일은 onKeyDown이 보장되지 않습니다. 위 onBeforeInput에서 삭제를 처리했으므로, 이 핸들러는 폴백 정도로 두거나 제거 가능합니다.
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines 86
to 95, the Backspace deletion logic currently handled in handleKeyDown relies on
onKeyDown which is unreliable on mobile; since deletion is already handled in
onBeforeInput, remove the Backspace-specific logic from handleKeyDown (either
make it a no-op for Backspace or delete the handler entirely if unused) so
mobile input uses onBeforeInput as the source of truth and keep handleKeyDown
only for genuinely needed non-deletion behaviors.
| if (mmDigits.length > 0) out += `:${mmDigits.padEnd(2, '')}`; | ||
| return out; |
There was a problem hiding this comment.
분 자리 패딩이 동작하지 않습니다 (padEnd 사용 오류).
padEnd(2, '')는 패딩을 수행하지 않습니다. 사용자 입력이 한 자리일 때 :5처럼 보일 수 있습니다. 의도대로 두 자리 표시가 필요하면 '0'으로 패딩하세요.
- if (mmDigits.length > 0) out += `:${mmDigits.padEnd(2, '')}`;
+ if (mmDigits.length > 0) out += `:${mmDigits.padEnd(2, '0')}`;Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
packages/design-system/src/components/dateTime/utils/FormatData.ts around lines
43-44: the code uses mmDigits.padEnd(2, '') which does not add any padding,
causing single-digit minutes to render like ":5"; change the padding to
mmDigits.padStart(2, '0') so that single-digit minutes become two digits (e.g.,
":05"), keeping the existing ':' prefix logic intact.
| export const validateDate = (digits: string): string => { | ||
| if (!digits || digits.length !== 8) { | ||
| return '날짜 8자리를 입력하세요 (예: 20250112)'; | ||
| } |
There was a problem hiding this comment.
길이 체크만으로는 부족 — 숫자 여부도 검증 필요
현재는 8자 길이만 맞으면 통과합니다. 비숫자 입력을 차단하세요.
-export const validateDate = (digits: string): string => {
- if (!digits || digits.length !== 8) {
+export const validateDate = (digits: string): string => {
+ if (!digits || !/^\d{8}$/.test(digits)) {
return '날짜 8자리를 입력하세요 (예: 20250112)';
}📝 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 validateDate = (digits: string): string => { | |
| if (!digits || digits.length !== 8) { | |
| return '날짜 8자리를 입력하세요 (예: 20250112)'; | |
| } | |
| export const validateDate = (digits: string): string => { | |
| if (!digits || !/^\d{8}$/.test(digits)) { | |
| return '날짜 8자리를 입력하세요 (예: 20250112)'; | |
| } |
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/utils/ValidateData.ts around
lines 1 to 4, the validateDate function only checks length but accepts
non-numeric characters; update the validation to require 8 numeric digits (e.g.
with a regex like /^\d{8}$/) and return the existing error string when the input
is missing, wrong length, or contains any non-digit characters; keep the same
return behavior for valid input.
| export const validateTime = (digits: string): string => { | ||
| if (!digits || digits.length !== 4) { | ||
| return 'HH:MM 형식으로 입력하세요 (예: 2312)'; | ||
| } | ||
|
|
||
| const hour = parseInt(match[1], 10); | ||
| const minute = parseInt(match[2], 10); | ||
| const hour = parseInt(digits.slice(0, 2), 10); | ||
| const minute = parseInt(digits.slice(2, 4), 10); | ||
|
|
There was a problem hiding this comment.
시간 검증에서 NaN이 통과할 수 있습니다
parseInt가 NaN일 때 현재 조건식이 false로 평가되어 빈 문자열(성공) 반환이 됩니다. 숫자 4자리와 NaN 체크를 추가하세요.
-export const validateTime = (digits: string): string => {
- if (!digits || digits.length !== 4) {
+export const validateTime = (digits: string): string => {
+ if (!digits || !/^\d{4}$/.test(digits)) {
return 'HH:MM 형식으로 입력하세요 (예: 2312)';
}
- const hour = parseInt(digits.slice(0, 2), 10);
- const minute = parseInt(digits.slice(2, 4), 10);
+ const hour = parseInt(digits.slice(0, 2), 10);
+ const minute = parseInt(digits.slice(2, 4), 10);
+ if (Number.isNaN(hour) || Number.isNaN(minute)) {
+ return '유효한 시간을 작성하세요';
+ }📝 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 validateTime = (digits: string): string => { | |
| if (!digits || digits.length !== 4) { | |
| return 'HH:MM 형식으로 입력하세요 (예: 2312)'; | |
| } | |
| const hour = parseInt(match[1], 10); | |
| const minute = parseInt(match[2], 10); | |
| const hour = parseInt(digits.slice(0, 2), 10); | |
| const minute = parseInt(digits.slice(2, 4), 10); | |
| export const validateTime = (digits: string): string => { | |
| if (!digits || !/^\d{4}$/.test(digits)) { | |
| return 'HH:MM 형식으로 입력하세요 (예: 2312)'; | |
| } | |
| const hour = parseInt(digits.slice(0, 2), 10); | |
| const minute = parseInt(digits.slice(2, 4), 10); | |
| if (Number.isNaN(hour) || Number.isNaN(minute)) { | |
| return '유효한 시간을 작성하세요'; | |
| } |
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/utils/ValidateData.ts around
lines 30 to 37, the time validation uses parseInt but doesn't guard against NaN
so invalid numeric input can pass; update the function to check that digits is
exactly 4 numeric characters (or that parseInt on hour and minute yields finite
numbers), return the existing error string when either parse produces NaN, and
then keep the existing range checks (hour 0-23, minute 0-59) before returning
success.
| {options.map((option) => ( | ||
| <li | ||
| key={option} | ||
| onClick={() => handleSelect(option,options.indexOf(option))} | ||
| onClick={() => handleSelect(option, options.indexOf(option))} | ||
| className={`body4-r h-[3.6rem] cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`} | ||
| > |
There was a problem hiding this comment.
index 계산에 indexOf 사용 → 중복 옵션에서 오동작.
options.indexOf(option)은 중복 항목이 있을 때 첫 번째 인덱스만 반환합니다. map 콜백의 idx를 사용해 정확도를 확보하세요. key도 고유성을 위해 option+idx 조합을 권장합니다.
- {options.map((option) => (
+ {options.map((option, idx) => (
<li
- key={option}
- onClick={() => handleSelect(option, options.indexOf(option))}
+ key={`${option}-${idx}`}
+ onClick={() => handleSelect(option, idx)}
className={`body4-r h-[3.6rem] cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`}
>
{option}
</li>
))}📝 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.
| {options.map((option) => ( | |
| <li | |
| key={option} | |
| onClick={() => handleSelect(option,options.indexOf(option))} | |
| onClick={() => handleSelect(option, options.indexOf(option))} | |
| className={`body4-r h-[3.6rem] cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`} | |
| > | |
| {options.map((option, idx) => ( | |
| <li | |
| key={`${option}-${idx}`} | |
| onClick={() => handleSelect(option, idx)} | |
| className={`body4-r h-[3.6rem] cursor-pointer p-[0.8rem] ${selectedValue === option ? 'text-main600' : 'text-font-gray-3'}`} | |
| > | |
| {option} | |
| </li> | |
| ))} |
🤖 Prompt for AI Agents
In packages/design-system/src/components/dropdown/Dropdown.tsx around lines 66
to 71, using options.indexOf(option) inside the map causes incorrect indexes for
duplicate options and non-unique keys; change the map to use the callback index
parameter (e.g., (option, idx) => ...) and pass idx to handleSelect, and make
the key unique by combining option and idx (e.g., `${option}-${idx}`) so
selection and rendering behave correctly with duplicates.
| {showPicker && ( <TimePicker | ||
| onSave={getTimePicker} | ||
| onCancel={() => { | ||
| AlarmsType[2].time = ''; |
|
|
||
| const handleDuplicateRightClick = () => { | ||
| window.location.href = "https://pinback.today/"; | ||
| chrome.tabs.create({ url: 'https://pinback.today' }); |
There was a problem hiding this comment.
window.location.href = "https://pinback.today/";
저도 이 방법만 알고있었는데 이런 방법도 있었네요 하나 배워갑니다-!!
|
✅ Storybook chromatic 배포 확인: |
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
📷 Screenshot
Summary by CodeRabbit
New Features
Bug Fixes
Style