Skip to content

Feat(재림): QA 반영 | 카테고리 선택 로직 및 대시보드 랜딩 수정#136

Merged
jllee000 merged 21 commits intodevelopfrom
feat/#109/jl-QA-2
Sep 20, 2025
Merged

Feat(재림): QA 반영 | 카테고리 선택 로직 및 대시보드 랜딩 수정#136
jllee000 merged 21 commits intodevelopfrom
feat/#109/jl-QA-2

Conversation

@jllee000
Copy link
Collaborator

@jllee000 jllee000 commented Sep 20, 2025

📌 Related Issues

관련된 Issue를 태그해주세요. (e.g. - close #25)

📄 Tasks

  1. 카테고리 새로 생성후, 저장하면 제대로된 카테고리id 전달 에러 수정 (토글시점에 api)
  2. 카테고리 10개 이상시, 추가 버튼 제거
  3. 대시보드 이동 로직 : 정책 상 크롬 방식으로 랜딩
  4. 리마인드 타임 null일 경우 분기
  5. 전송 리마인드 타임 포맷 수정

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • New Features

    • 온보딩에서 링크 이메일 자동 인식·저장 및 초반 푸시 토큰 준비, 확장프로그램 팝업의 새 탭 열기 지원 및 기본 썸네일 적용.
    • 카테고리 드롭다운 열 때 자동 새로고침, 알림 선택에 따른 기본 리마인드 시간 자동 설정.
  • Bug Fixes

    • 시간 선택기 취소 시 기존 선택 시간 유지.
    • 날짜/시간 입력과 유효성 검사 메시지·정규화가 보다 일관되고 강력해짐.
  • Style

    • 팝업 아이콘·이미지 교체 및 UI 미세 개선.

@jllee000 jllee000 self-assigned this Sep 20, 2025
@jllee000 jllee000 added feat 기능 개발하라 개발 달려라 달려 fix 버그 수정하라 러브버그 labels Sep 20, 2025
@vercel
Copy link

vercel bot commented Sep 20, 2025

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

Project Deployment Preview Comments Updated (UTC)
pinback-client-client Ready Ready Preview Comment Sep 20, 2025 2:50pm
pinback-client-landing Ready Ready Preview Comment Sep 20, 2025 2:50pm

@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (3)
  • packages/design-system/src/icons/source/tooltip_2.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_4.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_5.svg is excluded by !**/*.svg

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

온보딩 알람 취소 동작 보정, 온보딩 단계·리다이렉트·FCM 토큰 처리 변경, 확장 프로그램 저장 키 및 탭 오픈 방식 수정, 팝업(add/edit)에서 날짜/시간·카테고리 로직 강화, 디자인시스템의 DateTime/검증/Dropdown에 입력·API·UI 변경 및 아이콘 추가.

Changes

Cohort / File(s) Summary
Client Onboarding Flow
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx, apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
AlarmBox: TimePicker 취소 시 기존 시간 초기화 제거. MainCard: 쿼리에서 이메일 추출·저장, 마운트 시 FCM 토큰 선요청, remindTime 산출 로직 추가(고정/선택/정규화), 플랫폼별 단계 분기·리다이렉트 로직 및 뒤로가기 표시 조건 수정.
Client Validation Utils
apps/client/src/shared/utils/ValidateData.ts
validateDate/validateTime 시그니처를 고정 자리수(digits)로 변경, 슬라이스 기반 파싱으로 전환, 길이·범위·날짜 유효성 검사 및 오류 메시지 정리.
Extension Core & Storage
apps/extension/src/App.tsx, apps/extension/src/background.ts, apps/extension/src/apis/axiosInstance.ts
문자열 리터럴 표기 통일, 대시보드 이동을 chrome.tabs.create로 변경. 설치 시/배경에서 로컬 스토리지 키 userEmailemail로 변경, axiosInstance에서 동일 키 조회.
Extension Queries & Hooks
apps/extension/src/apis/query/queries.ts, apps/extension/src/hooks/useCategoryManager.ts, apps/extension/src/hooks/usePageMeta.ts
useGetCategoriesExtension에 react-query 옵션 인자 추가(외부 enabled 등 제어), useCategoryManager가 옵션 상태 관리·카테고리 동기화 추가, 사소한 포맷/주석 정리.
Extension Pages
apps/extension/src/pages/MainPop.tsx, apps/extension/src/pages/DuplicatePop.tsx
MainPop: add/edit 별 날짜·시간·검증 상태 추가, remindTime/date/time 처리·페이로드 정리, 카테고리 재조회(onToggle)·기본 썸네일 적용, chrome.tabs.create로 네비게이션 변경. DuplicatePop: 아이콘을 SVG 이미지로 교체.
Design System: Date/Time
packages/design-system/src/components/dateTime/DateTime.tsx, packages/design-system/src/components/dateTime/utils/FormatData.ts, packages/design-system/src/components/dateTime/utils/ValidateData.ts
DateTime: 제어 흐름을 onBeforeInput/onKeyDown 기반 숫자 전용 입력으로 전환, 내부 rawDigits 상태 도입. formatTime12/validate 유틸을 자리수 중심 파싱으로 강화 및 메시지·출력 보강.
Design System: Dropdown
packages/design-system/src/components/dropdown/Dropdown.tsx
onToggle?: (isOpen: boolean) => void prop 추가, 열림/닫힘 토글에서 콜백 호출 및 선택/추가 시 닫힘 알림 호출 추가.
Design System: Icons
packages/design-system/src/icons/iconNames.ts
extension_pop, extension_thumb 아이콘 이름 추가로 IconName 유니온 확장.

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) 분기 및 리다이렉트
Loading
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로 새 탭 오픈
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

재림, frontend

Suggested reviewers

  • constantly-dev
  • jjangminii

Poem

토끼가 톡톡, 숫자만 골라 모았네 🐇
알람은 남고 취소는 부드럽게, 탭은 펑—열려요! ✨
드롭다운이 속삭여요: "열림"을 알려줄게
아이콘 두 알 반짝, 시간은 네 자리로 딱
폴짝—배포 전 테스트도 잊지 말자!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 연결된 이슈들 중 #135(카테고리 저장 로직 및 대시보드 이동)은 useCategoryManager, Dropdown onToggle/limit, MainPop, 앱 내 크롬 탭 생성 변경 등으로 주요 요구사항(신규 카테고리 아이디 전달 문제 수정, 10개 이상 시 추가 버튼 제거, 크롬 방식 랜딩) 구현 흔적이 확인되어 충족됩니다. 반면 #25(Progress 컴포넌트 구현)는 본 PR의 변경 목록(raw_summary)에 관련 컴포넌트·스토리·테스트 추가 내용이 없으므로 해당 이슈는 이 PR에서 구현되지 않았습니다. 해결 방안으로는 #25를 이 PR의 linked issue에서 제거하거나 Progress 컴포넌트 구현(컴포넌트, 스토리, 인터랙션 테스트)을 이 PR에 포함시키고 설명에 반영해 주세요.
Out of Scope Changes Check ⚠️ Warning 여러 파일 변경 중 일부는 linked issue #135 범위를 벗어나는 것으로 보입니다. 예로 apps/client/src/pages/onBoarding/{MainCard,AlarmBox}.tsx의 온보딩·FCM 토큰/쿼리스트링 처리 변경, packages/design-system의 DateTime 컴포넌트 및 ValidateData 시그니처 변경 등은 카테고리 저장·대시보드 랜딩 요구사항과 직접 관련이 없어 별도 PR로 분리하는 것이 안전합니다. 또한 extension 쪽에서 저장키(userEmail → email) 변경은 외부 연동 영향이 크므로 별도로 문서화 및 롤아웃 계획이 필요합니다. 권장 조치: 본 PR은 카테고리/랜딩 관련 변경만 유지하고 기타 광범위한 API/컴포넌트 시그니처 변경(온보딩, DateTime, 저장키 변경 등)은 별도 PR로 분리해 영향 범위를 좁히고 관련 테스트와 마이그레이션 문서를 추가해 주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "QA(재림): 카테고리 선택 로직 및 대시보드 랜딩 수정"은 이 PR의 핵심 변경 사항인 카테고리 선택 로직 수정과 대시보드(크롬 방식) 랜딩 변경을 명확히 요약하고 있어 변경 의도와 일치합니다. 문구는 간결하며 팀원이 히스토리를 빠르게 파악할 수 있도록 충분히 구체적입니다.
Description Check ✅ Passed PR 설명은 템플릿의 필수 항목인 Related Issues( close #135 )와 Tasks 섹션을 충실히 작성해 주요 변경 목적과 작업 목록을 전달하고 있습니다. PR Point와 Screenshot 섹션은 비어 있으나 필수 항목이 아니므로 전반적으로 설명은 충분합니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4031b94 and c5c7d4f.

⛔ Files ignored due to path filters (12)
  • apps/extension/src/assets/extension_pop.svg is excluded by !**/*.svg
  • apps/extension/src/assets/extension_thumb.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/chippi_profile.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/extension_pop.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/extension_thumb.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/ic_extension.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/main_header_logo.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_1.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_2.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_3.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_4.svg is excluded by !**/*.svg
  • packages/design-system/src/icons/source/tooltip_5.svg is 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.ts
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
  • apps/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.tsx
  • packages/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: 익스텐션 → 온보딩 이메일 전달 처리 적합합니다.

쿼리스트링에서 email을 읽어 상태/로컬스토리지(email 키) 모두에 저장하는 방향이 조직 컨벤션과 일치합니다.

참고: 과거 러닝(키 통일) 반영 감사합니다.


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({ ... }) }
+ );

Comment on lines 1 to 4
export const validateDate = (digits: string): string => {
if (!digits || digits.length !== 8) {
return '날짜 8자리를 입력하세요 (예: 20250112)';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

날짜도 숫자만 허용하도록 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.

Comment on lines 30 to 36
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

숫자 검증 누락으로 비숫자 시간이 통과합니다

"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.

Comment on lines 6 to 7
console.log(info.email);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

민감정보(이메일/토큰) 콘솔 로그 제거.

이메일과 액세스 토큰을 콘솔에 남기면 심각한 보안/개인정보 이슈가 됩니다. 제거하세요.

-        console.log(info.email);
+        // do not log PII
...
-      console.log('Token saved!', message.token);
+      // do not log tokens

Also 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.

Comment on lines 209 to 219
putArticle({
articleId: isArticleId,
data: {
categoryId: saveData.selectedCategory
? parseInt(saveData.selectedCategory)
: 0,
memo: saveData.memo,
now: new Date().toISOString(),
remindTime: combineDateTime(saveData.date ?? '', saveData.time ?? ''),
},
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 86 to 95
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace') {
e.preventDefault();
setRawDigits((prev) => {
const next = prev.slice(0, -1);
onChange?.(next);
return next;
});
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Comment on lines 43 to 44
if (mmDigits.length > 0) out += `:${mmDigits.padEnd(2, '')}`;
return out;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

분 자리 패딩이 동작하지 않습니다 (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.

Comment on lines 1 to 4
export const validateDate = (digits: string): string => {
if (!digits || digits.length !== 8) {
return '날짜 8자리를 입력하세요 (예: 20250112)';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

길이 체크만으로는 부족 — 숫자 여부도 검증 필요

현재는 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.

Suggested change
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.

Comment on lines 30 to 37
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);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

시간 검증에서 NaN이 통과할 수 있습니다

parseIntNaN일 때 현재 조건식이 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.

Suggested change
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.

Comment on lines 66 to 71
{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'}`}
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
{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.

@jllee000 jllee000 changed the title QA(재림): 카테고리 선택 로직 및 대시보드 랜딩 수정 Feat(재림): QA 반영 | 카테고리 선택 로직 및 대시보드 랜딩 수정 Sep 20, 2025
Copy link
Collaborator

@jjangminii jjangminii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿굿 👍

{showPicker && ( <TimePicker
onSave={getTimePicker}
onCancel={() => {
AlarmsType[2].time = '';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 왜 없앴나요??


const handleDuplicateRightClick = () => {
window.location.href = "https://pinback.today/";
chrome.tabs.create({ url: 'https://pinback.today' });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.location.href = "https://pinback.today/";

저도 이 방법만 알고있었는데 이런 방법도 있었네요 하나 배워갑니다-!!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지..바뀌었네요 재저장하겠습니다-!

@github-actions
Copy link

✅ Storybook chromatic 배포 확인:
🐿️ storybook

@jllee000 jllee000 merged commit e0a54e8 into develop Sep 20, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려 fix 버그 수정하라 러브버그

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 카테고리 저장 로직 및 대시보드 이동 수정

2 participants