Skip to content

Feat(재림): 익스텐션, 온보딩 2차 QA #115

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

Feat(재림): 익스텐션, 온보딩 2차 QA #115
jllee000 merged 14 commits intodevelopfrom
feat/#109/jl-QA-2

Conversation

@jllee000
Copy link
Collaborator

@jllee000 jllee000 commented Sep 17, 2025

📌 Related Issues

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

📄 Tasks

  1. 데이트 타임 공컴 포맷팅 로직 전체 수정
  2. 온보딩 (ios,aos) 스텝 분기 수정
  3. duplicate Pop 아이콘 이미지 수정
  4. 익스텐션 팝업 카테고리 선택 노출 수정
  5. 익스텐션 팝업 랜딩 수정
  6. 온보딩 타임 피커 시간 노출 수정
  7. 로컬스토리지 저장 네이밍 수정(email로 통일)
  8. 온보딩 fcm 요청 시점 수정

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • 신기능

    • 온보딩 URL 기반 이메일 자동입력 및 로컬 저장/복원 지원.
    • 날짜(8자리)/시간(4자리) 숫자 전용 입력 방식 도입 — 부분 입력 지원 및 시각 포맷 유지.
    • 확장 팝업에 로컬 썸네일 적용 및 새 아이콘 추가.
  • 버그 수정 / 사용성 개선

    • 푸시 토큰 요청 추가 및 실패 시 알림.
    • 온보딩 초기 단계에서 뒤로가기 숨김; 회원가입 실패 시 저장된 이메일이 있으면 안전 복귀.
    • 시간 선택 취소 시 기존 설정 유지; 확장 프로그램의 자동 창 닫기 비활성화.

@vercel
Copy link

vercel bot commented Sep 17, 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 18, 2025 1:05pm

@coderabbitai
Copy link

coderabbitai bot commented Sep 17, 2025

Warning

Rate limit exceeded

@jllee000 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 12 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 0168c30 and 328f78e.

📒 Files selected for processing (3)
  • apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (0 hunks)
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (6 hunks)
  • apps/extension/src/pages/MainPop.tsx (5 hunks)

Walkthrough

온보딩 MainCard가 URL 쿼리의 이메일을 읽어 state 및 localStorage['email']에 저장하고, postSignUp 실패 시 저장된 이메일이 있으면 루트('/')로 리다이렉트; FCM 토큰 요청 흐름과 remindTime 계산 위치 변경; DateTime 입력/포맷/검증이 digits 기반으로 재구성되고 디자인 시스템 유틸·아이콘이 업데이트되며, 익스텐션 팝업·북마크/카테고리 로직 일부가 수정됨.

Changes

Cohort / File(s) Summary of Changes
Onboarding funnel: 이메일 프리필 및 에러 리다이렉트
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
URL 쿼리 emailuseLocation()으로 추출해 userEmail state 및 localStorage['email']에 저장; useEffectlocation.search 의존 추가; FCM 토큰 비동기 요청 추가(실패 시 alert); nextStep에서 step === 3일 때 remindTime 결정 로직 변경; postSignDataonError에서 저장된 이메일이 있으면 /로 navigate; Back 버튼 표시 조건을 step < 4 && step > 0로 변경.
AlarmBox: TimePicker 취소 동작 변경
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx
TimePicker onCancel에서 알람 시간 초기화 제거(이제 Picker만 숨김); 디버깅용 console.log 추가.
앱 유틸: 날짜/시간 검증 변경 (앱 전역)
apps/client/src/shared/utils/ValidateData.ts
validateDate/validateTime를 8자리/4자리 숫자(digits) 기준으로 검증하게 변경; 파라미터명·에러 메시지·유효성 검사 방식 업데이트.
Design system: DateTime 컴포넌트 리팩터
packages/design-system/src/components/dateTime/DateTime.tsx
입력 처리 재구성: rawDigits 상태 기반, onBeforeInput·onKeyDown으로 숫자 입력·Backspace 제어; ref·maxLength 제거; value 기본값 제거; onChange 호출 방식 변경.
Design system: 포맷/검증 유틸 업데이트
packages/design-system/src/components/dateTime/utils/FormatData.ts, packages/design-system/src/components/dateTime/utils/ValidateData.ts
formatTime12와 validate 유틸을 digits 기반으로 단순화·강화; validate 시그니처가 digits: string으로 변경 및 에러 메시지 조정.
Design system: 아이콘 추가
packages/design-system/src/icons/iconNames.ts
iconNames 배열에 'extension_pop''extension_thumb' 추가(IconName 타입에 신규 값 포함).
Extension: axios/background email key 변경
apps/extension/src/apis/axiosInstance.ts, apps/extension/src/background.ts
chrome.storage.local에 저장/조회하는 이메일 키를 기존 userEmail'email'로 변경(인터셉터 및 설치 핸들러).
Extension: 북마크 저장/팝업/메타 변경
apps/extension/src/hooks/useSaveBookmarks.ts, apps/extension/src/pages/DuplicatePop.tsx, apps/extension/src/pages/MainPop.tsx
북마크 저장 후 window.close() 호출 주석 처리(자동 닫기 비활성화); DuplicatePop에서 Icon → 로컬 extension_pop.svg 이미지로 교체; MainPop은 remind 데이터 항상 조회, date/time 로컬 state·검증 추가, 로컬 썸네일 사용 및 관련 초기화 정리.
Extension: 카테고리 관리 동기화
apps/extension/src/hooks/useCategoryManager.ts
options를 state로 전환하고 categoryData 변화 시 동기화 useEffect 추가; save 후 options에 새 카테고리 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as 사용자
  participant R as 라우터
  participant MC as MainCard
  participant LS as localStorage
  participant API as postSignUp

  U->>R: GET /onboarding?email=foo@bar.com
  R->>MC: mount (location.search)
  MC->>MC: 쿼리에서 email 파싱 → set userEmail
  MC->>LS: setItem('email', email)
  U->>MC: 회원가입 제출
  MC->>API: mutate(signUpData)
  API-->>MC: 오류 응답
  MC->>LS: getItem('email')
  alt email 있음
    MC->>R: navigate('/')
  else
    MC-->>U: 오류 표시
  end
Loading
sequenceDiagram
  autonumber
  participant U as 사용자
  participant DT as DateTime 컴포넌트
  participant UTIL as Format/Validate 유틸

  U->>DT: 숫자 입력 / Backspace
  DT->>DT: onBeforeInput / onKeyDown → rawDigits 업데이트
  DT->>UTIL: formatDate / formatTime12 (렌더용)
  DT->>UTIL: validateDate(digits) / validateTime(digits)
  UTIL-->>DT: validation 결과
  DT-->>U: 포맷된 텍스트 및 에러 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

🛠️ Feature, 재림, frontend

Suggested reviewers

  • constantly-dev
  • jjangminii

Poem

"토끼가 와서 쿼리 속 편지를 훔쳤네 🐇
로컬에 담아두고 혹시 에러면 집으로 돌려보내네.
숫자들만 쾅쾅, 시간은 반짝 정리했지,
아이콘 새 옷 입고 창 닫기는 잠시 쉬어,
깡총깡총 코드를 축하한다 — 냠!"

Pre-merge checks and finishing touches

❌ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning 링크된 이슈 [#25]는 디자인 시스템에 Progress 컴포넌트 추가(variant="profile" 및 "tree"), 값 0–100 제약과 접근성 속성 부여, Storybook 스토리·인터랙션 테스트 추가 및 런타임 의존성(@radix-ui/react-progress) 도입을 요구합니다. 제출된 변경사항(raw_summary)은 익스텐션/온보딩 수정, dateTime 관련 유틸 및 컴포넌트 변경, 아이콘명 추가 등으로 구성되어 있으며 Progress 컴포넌트 구현·스토리·테스트·의존성 추가 관련 코드는 확인되지 않습니다. 따라서 링크된 이슈의 코딩 요구사항을 충족하지 않습니다. 이 PR이 실제로 [#25]을 구현하려면 Progress 컴포넌트 소스와 스타일 분기, Storybook 스토리 및 인터랙션 테스트, 그리고 package.json과 빌드 설정에 @radix-ui/react-progress 추가를 포함하거나, PR 설명과 연결된 이슈를 실제 변경 내용에 맞게 수정해 주세요.
Out of Scope Changes Check ⚠️ Warning 이번 PR에는 링크된 이슈([#25])의 목표와 직접 관련이 없는 여러 변경이 포함되어 있어 범위를 벗어납니다; 예를 들어 apps/extension/src/(background, axiosInstance, hooks, pages), apps/client/src/pages/onBoarding/, 그리고 packages/design-system/src/components/dateTime/ 및 관련 유틸 변경 등은 Progress 컴포넌트 요구사항과 무관합니다. 이러한 비관련 변경은 리뷰 범위를 확장시키고 병합 리스크를 증가시키므로 out-of-scope로 판단합니다. PR의 범위 분리 또는 설명 보완이 필요합니다. 권장 해결책은 변경을 기능별로 분리해 Progress 관련 구현만 포함한 PR과 익스텐션/온보딩/DateTime 개선 등 별도 PR로 나누거나, PR 본문에서 각 변경의 목적·영향 범위·테스트 방법을 명확히 설명하고 각각 적절한 이슈로 연결하는 것입니다.
Description Check ⚠️ Warning PR 설명이 저장소 템플릿을 그대로 둔 채 실제 내용을 기입하지 않아 관련 이슈 번호, 수행한 작업 요약, 리뷰어에게 전달할 포인트 및 스크린샷 등 필수·권장 정보가 비어 있습니다. 이로 인해 변경 목적과 검증 방법을 검토자가 파악할 수 없으므로 템플릿 기준에서 불완전합니다. 따라서 PR 설명 검사는 실패로 판단합니다. 관련 이슈 번호를 정확히 기입(예: close #25), 수행한 작업 항목과 변경 요약, 리뷰 포인트(주요 영향·검증 방법) 및 필요 시 스크린샷이나 재현 절차를 추가해 PR 본문을 완성해 주세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "Feat(재림): 익스텐션, 온보딩 2차 QA"는 이번 변경이 익스텐션과 온보딩 관련 QA 수정임을 직접적으로 나타내며 raw_summary에 있는 변경 파일군(apps/extension 및 apps/client/onBoarding 관련)과 범위가 일치합니다. 다만 '2차 QA'와 작성자 식별자(재림)는 작업의 성격을 나타내지만 구체적 변경 포인트나 관련 이슈 번호가 없어 한눈에 세부 목적을 파악하기 어렵습니다. 전체적으로 PR의 주된 변경 범위를 반영하므로 제목 기준에서 허용 가능합니다.
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.

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Sep 17, 2025
@github-actions github-actions bot requested a review from jjangminii September 17, 2025 17:06
@jllee000 jllee000 changed the title Feat/#109/jl qa 2 Feat(재림): 익스텐션, 온보딩 2차 QA Sep 17, 2025
@github-actions
Copy link

github-actions bot commented Sep 17, 2025

✅ Storybook chromatic 배포 확인:
🐿️ storybook

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)

66-68: Firebase를 컴포넌트 본문에서 매 렌더마다 초기화 — 중복 초기화/런타임 오류 위험

initializeApp/getMessaging가 렌더 때마다 실행됩니다. StrictMode나 재렌더 상황에서 [DEFAULT] 앱 중복 초기화 오류가 날 수 있고, 불필요한 비용도 큽니다. 모듈 스코프로 올리거나 getApps() 가드로 단 한 번만 초기화하세요.

적용 diff(컴포넌트 내부 제거):

-  const app = initializeApp(firebaseConfig);
-  const messaging = getMessaging(app);

모듈 스코프 추가(참고용, 파일 상단 import 인접 위치에 배치):

import { initializeApp, getApps, getApp } from 'firebase/app';
const firebaseApp = getApps().length ? getApp() : initializeApp(firebaseConfig);
const messaging = getMessaging(firebaseApp);

140-147: setState 직후 상태 사용으로 인한 잘못된 remindDefault 전송

setRemindTime은 비동기입니다. 직후의 remindTime은 이전 값일 수 있어 서버에 구형 값이 전송됩니다. 로컬 변수로 계산해 사용하세요.

-      const raw = AlarmsType[alarmSelected - 1].time;
-      setRemindTime(normalizeTime(raw));
+      const raw = AlarmsType[alarmSelected - 1].time;
+      const normalizedTime = normalizeTime(raw);
+      setRemindTime(normalizedTime);
...
-          remindDefault: remindTime,
+          remindDefault: normalizedTime,
🧹 Nitpick comments (3)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (3)

72-72: registerServiceWorker 비동기 완료 대기 누락

SW 등록이 끝나기 전에 getToken을 호출하면 토큰 발급이 실패할 수 있습니다. 대기하세요.

-      registerServiceWorker();
+      await registerServiceWorker();

199-209: Back 버튼 조건 변경은 👍. 다만 isDisabled는 중복

{step < 4 && step > 0}로 이미 렌더가 차단되므로 isDisabled={step === 0}는 의미가 없습니다. 제거해도 됩니다.

           <Button
             variant="secondary"
             size="medium"
-            isDisabled={step === 0}
             className="w-[4.8rem]"
             onClick={prevStep}
           >

47-47: Next 버튼 비활성 조건 도달 불가 + 전송 중 중복 클릭 방지 필요

nextStep에서 step이 6이 되지 않아 isDisabled={step === 6}는 작동하지 않습니다. 변이 진행 중에는 버튼을 비활성화하세요.

React Query v4/v5 호환 접근(변이 객체 사용):

-  const { mutate: postSignData } = usePostSignUp();
+  const postSignUpMutation = usePostSignUp();
+  const { mutate: postSignData } = postSignUpMutation;
         <Button
           variant="primary"
           size="medium"
-          isDisabled={step === 6}
+          isDisabled={Boolean(postSignUpMutation.isPending || postSignUpMutation.isLoading)}
           className="ml-auto w-[4.8rem]"
           onClick={nextStep}
         >

프로젝트가 React Query v5만 사용한다면 isPending만, v4라면 isLoading만 쓰도록 조정해 주세요.

Also applies to: 211-219

📜 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 0543419 and 6a064db.

📒 Files selected for processing (1)
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/client/src/pages/onBoarding/components/funnel/MainCard.tsx
⏰ 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/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)

51-60: URL의 이메일 파라미터 처리: 구현 OK. 최소 검증·수명 관리 보완 제안

  • 표준화된 'email' 키 사용은 프로젝트 컨벤션과 일치합니다. 굿.
  • 저장 전 간단한 이메일 형식 검증을 추가해 잘못된 값 영구 저장을 방지하세요.
  • 회원가입 성공 시 localStorage의 'email'을 제거해 PII 잔존을 최소화하는 게 바람직합니다(아래 onSuccess 제안 참고).
  • 이후 URL에서 email 파라미터가 사라지는 경우의 정책(유지/삭제)도 결정이 필요합니다.

Comment on lines +153 to +158
onError: () => {
const savedEmail = localStorage.getItem('email');
if (savedEmail) {
window.location.href = '/';
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

모든 onError에서 루트로 리다이렉트는 실패 은폐 위험

네트워크/서버 오류까지 모두 홈으로 보내면 사용자는 실패를 인지/복구하지 못합니다. 이미 가입(409 Conflict) 등 명시 케이스에 한해 리다이렉트하고, 성공 시에는 저장된 이메일을 정리하세요.

           onSuccess: () => {
-            window.location.href = '/';
+            try { localStorage.removeItem('email'); } catch {}
+            window.location.href = '/';
           },
-          onError: () => {
-            const savedEmail = localStorage.getItem('email');
-            if (savedEmail) {
-              window.location.href = '/';
-            }
-          }
+          onError: (error: any) => {
+            const status = error?.response?.status;
+            const savedEmail = localStorage.getItem('email');
+            // 이미 가입(409) 또는 익스텐션/웹에서 이메일이 저장된 경우에만 홈으로 이동
+            if (status === 409 || savedEmail) {
+              try { localStorage.removeItem('email'); } catch {}
+              window.location.href = '/';
+              return;
+            }
+            alert('회원가입에 실패했습니다. 잠시 후 다시 시도해주세요.');
+          }
📝 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
onError: () => {
const savedEmail = localStorage.getItem('email');
if (savedEmail) {
window.location.href = '/';
}
}
onSuccess: () => {
try { localStorage.removeItem('email'); } catch {}
window.location.href = '/';
},
onError: (error: any) => {
const status = error?.response?.status;
const savedEmail = localStorage.getItem('email');
// 이미 가입(409) 또는 익스텐션/웹에서 이메일이 저장된 경우에만 홈으로 이동
if (status === 409 || savedEmail) {
try { localStorage.removeItem('email'); } catch {}
window.location.href = '/';
return;
}
alert('회원가입에 실패했습니다. 잠시 후 다시 시도해주세요.');
}
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
153-158, change the blanket onError redirect to only redirect for explicit cases
(e.g., 409 Conflict) by inspecting the error object/response status inside
onError and only call window.location.href = '/' when error.status === 409 (or
other explicitly handled statuses); for other errors do not redirect — surface
the error to the UI (or keep the user on the page) so they can recover;
additionally, ensure success handler clears localStorage.removeItem('email') so
savedEmail is removed on successful signup.

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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/extension/src/hooks/useSaveBookmarks.ts (1)

21-30: chrome.storage 콜백의 lastError 미확인 → 실패 시 침묵 상태

스토리지 get/set 콜백에서 chrome.runtime.lastError를 확인하지 않아 실패가 조용히 넘어갑니다. 실패 시 reject하도록 보완하세요.

다음처럼 수정 제안:

-      const result = await new Promise<{ bookmarks?: any[] }>((resolve) => {
-        chrome.storage.local.get(['bookmarks'], (items) => resolve(items));
-      });
+      const result = await new Promise<{ bookmarks?: any[] }>((resolve, reject) => {
+        chrome.storage.local.get(['bookmarks'], (items) => {
+          if (chrome.runtime.lastError) return reject(chrome.runtime.lastError);
+          resolve(items);
+        });
+      });
...
-      await new Promise<void>((resolve) => {
-        chrome.storage.local.set({ bookmarks }, resolve);
-      });
+      await new Promise<void>((resolve, reject) => {
+        chrome.storage.local.set({ bookmarks }, () => {
+          if (chrome.runtime.lastError) return reject(chrome.runtime.lastError);
+          resolve();
+        });
+      });
apps/client/src/shared/utils/ValidateData.ts (1)

1-43: Design-system의 validateDate/validateTime로 통합 — 앱 쪽 중복 제거

apps/client/src/shared/utils/ValidateData.ts와 packages/design-system/src/components/dateTime/utils/ValidateData.ts(및 packages/design-system/src/components/index.ts에서 export됨)에 동일한 구현이 존재합니다. 앱에서 design-system의 validateDate/validateTime을 직접 import하거나 design-system에서 재export하도록 변경한 뒤, 앱 쪽 중복 파일을 제거하세요.

🧹 Nitpick comments (18)
apps/extension/src/hooks/useSaveBookmarks.ts (3)

25-27: 중복 저장 방지 가드 추가 제안

동일 URL 중복 저장을 막으면 UX가 좋아집니다.

-      const bookmarks = result.bookmarks || [];
-      bookmarks.push(saveData);
+      const bookmarks = result.bookmarks || [];
+      if (!bookmarks.some(b => b.url === saveData.url)) {
+        bookmarks.push(saveData);
+      }

32-41: 북마크 생성 콜백 오류 미처리

chrome.bookmarks.create에서도 lastError를 체크하고 실패 시 로깅/사용자 알림이 필요합니다.

-      chrome.bookmarks.create(
+      chrome.bookmarks.create(
         {
           parentId: '1',
           title: params.title || params.url,
           url: params.url,
         },
-        (newBookmark) => {
-          console.log('크롬 북마크바에 저장 완료: ', newBookmark);
-        }
+        (newBookmark) => {
+          if (chrome.runtime.lastError) {
+            console.error('북마크 저장 실패:', chrome.runtime.lastError);
+            return;
+          }
+          console.log('크롬 북마크바에 저장 완료: ', newBookmark);
+        }
       );

21-41: 로컬 스토리지 무한 증가 가능성

chrome.storage.local에 북마크를 무제한 축적하면 성능/용량 이슈가 생길 수 있습니다. 용량 제한(예: LRU 상위 N개, 또는 날짜 기준 정리) 정책을 고려하세요.

apps/extension/src/pages/DuplicatePop.tsx (1)

10-12: 장식 아이콘 접근성 점검

해당 아이콘이 순수 장식이라면 aria-hidden 처리 혹은 role="img"와 적절한 aria-label 중 하나를 지정해 스크린리더 노이즈를 줄여주세요(Icon 컴포넌트 기본값 확인 필요).

Icon 컴포넌트가 기본으로 aria-hidden을 부여하는지 확인 부탁드립니다.

apps/client/src/shared/utils/ValidateData.ts (2)

1-4: 입력 요구 메시지 개선 제안

사용자에게 “숫자 8자리(YYYYMMDD)”를 명확히 안내하는 편이 좋습니다.

-    return '날짜 8자리를 입력하세요 (예: 20250112)';
+    return 'YYYYMMDD 숫자 8자리를 입력하세요 (예: 20250112)';

30-33: 시간 형식 안내 문구가 혼동 유발(HH:MM vs 2312)

콜론 없는 4자리 입력을 요구하므로 메시지를 일관되게 수정하세요.

-    return 'HH:MM 형식으로 입력하세요 (예: 2312)';
+    return 'HHMM 숫자 4자리를 입력하세요 (예: 2312)';
packages/design-system/src/components/dateTime/utils/ValidateData.ts (3)

1-4: 입력 요구 메시지 통일 필요

앱 단과 동일하게 명확한 문구로 통일하면 혼란이 줄어듭니다.

-    return '날짜 8자리를 입력하세요 (예: 20250112)';
+    return 'YYYYMMDD 숫자 8자리를 입력하세요 (예: 20250112)';

30-33: 시간 안내 문구 통일(HHMM)

콜론 없는 4자리 요구에 맞춰 문구 정리 권장.

-    return 'HH:MM 형식으로 입력하세요 (예: 2312)';
+    return 'HHMM 숫자 4자리를 입력하세요 (예: 2312)';

1-44: 숫자만 허용을 명시적으로 강제할지 검토

현재는 길이만 체크합니다. 명시적으로 /^\d{8}$/, /^\d{4}$/ 검사로 의도를 드러내면 유지보수성이 향상됩니다.

apps/extension/src/pages/MainPop.tsx (4)

124-133: 디버그 로그 제거 권장

프로덕션 빌드에서 console.log는 노이즈가 됩니다. 필요 시 환경 변수로 가드하세요.

-        console.log(remindData?.data);

152-171: 리마인드 설정 시 유효성 오류가 있어도 저장 진행됨

isRemindOn이 true인데 dateError/timeError가 존재하거나 값이 비어도 저장이 진행됩니다. 초기에 가드하세요.

  const handleSave = async () => {
+    if (isRemindOn) {
+      const dDigits = (date || '').replace(/\D/g, '');
+      const tDigits = (time || '').replace(/\D/g, '');
+      const dErr = validateDate(dDigits);
+      const tErr = validateTime(tDigits);
+      if (dErr || tErr) {
+        alert('리마인드 날짜/시간을 올바르게 입력해 주세요.');
+        setDateError(dErr);
+        setTimeError(tErr);
+        return;
+      }
+    }

298-303: 에러 메시지 표시 조건을 리마인드 ON에 한정

토글 OFF 상태에서 오류 문구가 남아 있을 수 있습니다. isRemindOn일 때만 노출하세요.

-            {dateError ? (
+            {isRemindOn && dateError ? (
               <p className="body3-r text-error">{dateError}</p>
-            ) : timeError ? (
+            ) : isRemindOn && timeError ? (
               <p className="body3-r text-error">{timeError}</p>
             ) : null}

241-247: InfoBox 이미지 소스 일관화

state로 관리하는 imgUrl이 있음에도 InfoBox에는 initialImgUrl || defaultImageUrl를 전달합니다. 편집 도중 이미지 교체가 필요하다면 imgUrl을 전달하는 편이 일관됩니다.

-            imgUrl={initialImgUrl || defaultImageUrl}
+            imgUrl={imgUrl || defaultImageUrl}
packages/design-system/src/components/dateTime/DateTime.tsx (2)

82-89: 삭제 키 처리 보완

Backspace만 처리되어 있습니다. Delete도 동일하게 마지막 자리 제거로 처리하면 UX가 일관됩니다. (캐럿 위치 편집을 지원하지 않으므로 단순화가 합리적)

적용 diff:

-  if (e.key === 'Backspace') {
+  if (e.key === 'Backspace' || e.key === 'Delete') {

67-79: 기본 동작 무조건 차단하여 캐럿 점프/이중 입력 방지

허용 입력에서도 preventDefault()를 호출하도록 위에서 제안했습니다. 이와 함께 iOS/안드로이드 가상 키보드, 한글 IME 환경에서의 beforeinput/onPaste 동작을 수기 테스트해 주세요.

테스트 제안:

  • 날짜/시간 각각에 대해: 단일 입력, 다중 붙여넣기(숫자/문자혼합), Backspace/Delete, 빠른 타이핑, 모바일 시뮬레이터(숫자 키패드)에서 입력.

Also applies to: 102-104

packages/design-system/src/components/dateTime/utils/FormatData.ts (3)

18-21: 입력 정규화 보강: 내부에서도 digitsOnly 적용 권장

외부에서 보장되지 않더라도 함수 내부에서 한 번 더 숫자만 남기면 방어적입니다.

-  const clean = digits.slice(0, 4);
+  const clean = digitsOnly(digits).slice(0, 4);

25-30: 초기 포맷이 caret 매핑과 불일치 가능성 있음 (PREFIX=3 가정과 상충)

mapCaretByDigitsPos('time')가 항상 '오전 '/'오후 '(3자) 프리픽스를 가정하는데, 여기서 '' 또는 단일 숫자만 반환하면 caret 계산이 어긋날 수 있습니다. 최소한 프리픽스는 항상 노출하는 쪽이 안전합니다. (프리픽스 동적 표시는 유지되며, 이후 시각이 12 이상이 되면 '오후'로 전환됨)

-  if (hhDigits.length === 0) return '';
-  if (hhDigits.length === 1 && !mmDigits) {
-    // 한 자리만 있을 때는 그대로 보여줌 (ex: "2")
-    return hhDigits;
-  }
+  if (hhDigits.length === 0) return '오전 ';
+  if (hhDigits.length === 1 && !mmDigits) {
+    // 한 자리 시 입력 중에도 프리픽스 유지
+    return `오전 ${hhDigits}`;
+  }

참고: 만약 현재 UI 의도대로 프리픽스를 초기에 숨기고 싶다면, mapCaretByDigitsPos의 time 분기에서 digitsPos===0일 때 PREFIX0으로 처리하는 예외를 추가해야 합니다. 필요하면 패치안 제안드리겠습니다.


22-24: 분(minute) 범위 미클램프: 60–99가 그대로 노출됨

mmDigits가 두 자리일 때 59로 클램프하지 않아 오전 12:89 같은 값이 표시될 수 있습니다. 두 자리 입력이 완료된 경우에만 클램프+두 자리 보장하면 UX가 자연스럽습니다. (한 자리 입력 중에는 그대로 노출)

-  const hhDigits = clean.slice(0, 2); // 시
-  const mmDigits = clean.slice(2, 4); // 분
+  const hhDigits = clean.slice(0, 2); // 시
+  let mmDigits = clean.slice(2, 4);   // 분
+  // 두 자리 분이 입력된 경우에만 0–59로 클램프하고 두 자리 보장
+  if (mmDigits.length === 2) {
+    const mmVal = parseInt(mmDigits, 10);
+    const safe = isNaN(mmVal) ? 0 : Math.min(59, mmVal);
+    mmDigits = String(safe).padStart(2, '0');
+  }
@@
-  if (mmDigits.length > 0) out += `:${mmDigits.padEnd(2, '')}`;
+  if (mmDigits.length > 0) out += `:${mmDigits}`;

Also applies to: 43-43

📜 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 6a064db and 4c6e350.

⛔ Files ignored due to path filters (11)
  • 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 (9)
  • apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (0 hunks)
  • apps/client/src/shared/utils/ValidateData.ts (2 hunks)
  • apps/extension/src/hooks/useSaveBookmarks.ts (1 hunks)
  • apps/extension/src/pages/DuplicatePop.tsx (1 hunks)
  • apps/extension/src/pages/MainPop.tsx (5 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/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-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/pages/DuplicatePop.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:

  • packages/design-system/src/icons/iconNames.ts
🧬 Code graph analysis (4)
apps/client/src/shared/utils/ValidateData.ts (1)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (2)
  • validateDate (1-28)
  • validateTime (30-43)
apps/extension/src/pages/MainPop.tsx (2)
apps/extension/src/utils/remindTimeFormat.ts (2)
  • updateDate (2-5)
  • updateTime (8-11)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (1)
  • validateDate (1-28)
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)
packages/design-system/src/components/dateTime/utils/ValidateData.ts (1)
apps/client/src/shared/utils/ValidateData.ts (2)
  • validateDate (1-28)
  • validateTime (30-43)
🔇 Additional comments (10)
apps/extension/src/hooks/useSaveBookmarks.ts (1)

42-42: 자동 창 닫기 비활성화 OK

저장 직후 창을 강제로 닫지 않는 변경은 UX/디버깅에 유리합니다.

packages/design-system/src/icons/iconNames.ts (2)

5-7: 아이콘 이름 추가 자체는 무해

소비처(DuplicatePop 등)에서 정상 참조됩니다.

필요 시 디자인 시스템 Icon 렌더러가 신규 이름을 문제없이 매핑하는지 스토리북에서 한 번 확인해주세요.


1-8: 자동 생성 파일 직접 편집 감지 — 제너레이터 입력/재생성 확인 필요

  • 요지: packages/design-system/src/icons/iconNames.ts는 자동 생성 파일입니다. 직접 수정하면 재생성 시 변경이 소실됩니다.
  • 발견: 에셋 — packages/design-system/src/icons/source/extension_thumb.svg, packages/design-system/src/icons/source/extension_pop.svg, apps/extension/src/assets/extension_thumb.svg
  • 문제: iconNames.ts를 생성하는 제너레이터(스크립트/플롭/도구)를 저장소에서 찾지 못했습니다.
  • 조치: 제너레이터 입력(아이콘 목록/스크립트)을 업데이트한 뒤 재생성하거나, 제너레이터 위치 및 재생성 절차를 공유하여 검증 가능하게 하십시오.
apps/extension/src/pages/DuplicatePop.tsx (1)

11-11: 신규 아이콘 사용 전환 OK

dotoriextension_pop 전환 문제 없습니다.

apps/extension/src/pages/MainPop.tsx (3)

44-47: 자동 창 닫기 주석 처리 OK

저장 불가 페이지에서 즉시 종료하지 않는 방향은 QA에 유리합니다.


51-59: 기본 썸네일 자산 사용 전환 OK

원격 기본 이미지 대신 번들 자산 사용으로 안정성이 향상됩니다.


136-146: validateDate/validateTime는 숫자(YYYYMMDD/HHMM)를 기대합니다 — 입력 포맷 정규화 필요

DateTime의 onChange가 YYYY.MM.DD / HH:MM 같은 구분자 포함 문자열을 넘기면 길이 검증이 항상 실패합니다. 핸들러에서 비숫자 문자를 제거한 뒤 validate 함수에 전달하세요.

파일: apps/extension/src/pages/MainPop.tsx (lines 136-146)

-  const handleDateChange = (value: string) => {
-    setDate(value);
-    console.log(date,'d',value);
-    setDateError(validateDate(value));
-  };
+  const handleDateChange = (value: string) => {
+    setDate(value);
+    const digits = value.replace(/\D/g, '');
+    setDateError(validateDate(digits));
+  };
...
-  const handleTimeChange = (value: string) => {
-    setTime(value);
-    console.log(time,'d',value);
-    setTimeError(validateTime(value));
-  };
+  const handleTimeChange = (value: string) => {
+    setTime(value);
+    const digits = value.replace(/\D/g, '');
+    setTimeError(validateTime(digits));
+  };

저장소 검색에서 DateTime 컴포넌트 및 validateDate/validateTime 정의를 찾을 수 없었습니다(스크립트 결과 없음). onChange가 포맷 문자열을 넘기는지 또는 숫자만 넘기는지 직접 확인하세요.

packages/design-system/src/components/dateTime/DateTime.tsx (1)

62-66: 표시값 파생 로직은 적절합니다

rawDigits를 길이 제한 후 포매팅하는 접근은 명확합니다.

packages/design-system/src/components/dateTime/utils/FormatData.ts (2)

32-35: 시(hour) 범위 클램프 LGTM

parseInt NaN 가드 및 0–23 클램프는 적절합니다.


38-40: 12시간 변환 간결화 LGTM

hour24 % 12 || 12 형태로 단순화된 로직은 가독성과 일관성 모두 좋습니다.

@@ -1,4 +1,4 @@
import { useRef, useState, type ChangeEvent } from 'react';
import { useState} from 'react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

React 네임스페이스 타입 미수입으로 컴파일 오류 발생 가능

React.FormEvent, React.KeyboardEvent 네임스페이스 타입을 사용하지만 React 타입을 임포트하지 않아 TS에서 Cannot find namespace 'React'가 납니다. 타입 전용 임포트로 수정하고, 핸들러 시그니처도 네임스페이스 없이 사용하는 편이 안전합니다.

적용 diff:

-import {  useState} from 'react';
+import { useEffect, useId, useState } from 'react';
+import type { FormEvent, KeyboardEvent, ClipboardEvent } from 'react';
-  const handleBeforeInput = (e: React.FormEvent<HTMLInputElement>) => {
+  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {
-  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {

Also applies to: 67-68, 82-83

🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around line 1
(and also affecting lines 67-68 and 82-83), TypeScript fails because the React
namespace types (React.FormEvent, React.KeyboardEvent) are referenced but React
is not imported; update the top of the file to add a type-only import for React
(import type React from 'react') or preferably replace usages of
React.FormEvent/React.KeyboardEvent with the direct imported DOM event types
(e.g., import type { FormEvent, KeyboardEvent } from 'react' and use those in
handler signatures) and update the handler signatures accordingly so the file
compiles without the missing namespace error.

Comment on lines +67 to +79
const input = e as unknown as InputEvent;

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const onlyDigits = digitsOnly(e.target.value);
setRawDigits(onlyDigits);
if (!/[0-9]/.test(input.data ?? '')) {
e.preventDefault();
return;
}

setRawDigits((prev) => {
const next = (prev + input.data!).slice(0, type === 'date' ? 8 : 4);
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

붙여넣기/다중 입력 시 비숫자 허용 및 DOM 기본 입력과의 충돌

  • onBeforeInput에서 /[0-9]/.test(input.data)로 한 글자라도 숫자가 포함되면 통과시켜 비숫자가 그대로 rawDigits에 섞일 수 있습니다(예: abc123).
  • 허용 케이스에서도 기본 동작을 막지 않아(append는 상태로, DOM은 캐럿 위치에 삽입) 캐럿 점프/깜빡임이 생길 수 있습니다.

digitsOnly로 정규화하고 항상 preventDefault 하세요.

적용 diff:

-  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {
-    const input = e as unknown as InputEvent;
-
-    if (!/[0-9]/.test(input.data ?? '')) {
-      e.preventDefault();
-      return;
-    }
-
-    setRawDigits((prev) => {
-      const next = (prev + input.data!).slice(0, type === 'date' ? 8 : 4);
-      onChange?.(next); 
-      return next;
-    });
-  };
+  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {
+    const ev = (e.nativeEvent as InputEvent);
+    const added = digitsOnly(ev.data ?? '');
+    if (!added) {
+      e.preventDefault();
+      return;
+    }
+    e.preventDefault();
+    setRawDigits((prev) => {
+      const limit = type === 'date' ? 8 : 4;
+      const next = (prev + added).slice(0, limit);
+      onChange?.(next);
+      return next;
+    });
+  };

추가(파일 내부에 새로 추가): 붙여넣기 전용 처리

// 핸들러들 아래에 추가
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
  e.preventDefault();
  const txt = digitsOnly(e.clipboardData.getData('text') || '');
  if (!txt) return;
  setRawDigits(prev => {
    const limit = type === 'date' ? 8 : 4;
    const next = (prev + txt).slice(0, limit);
    onChange?.(next);
    return next;
  });
};
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines
67-79, onBeforeInput currently lets input.data through if it contains any digit
and doesn't always prevent the DOM default, which allows non-digits to mix into
rawDigits (e.g. "abc123") and causes caret jump/flash; change the handler to
always preventDefault, sanitize input.data via digitsOnly (only keep 0-9),
append the sanitized string truncated to the type-specific limit, call onChange
with the new rawDigits, and return that new state; also add a new handlePaste
handler that prevents default, extracts clipboard text, normalizes via
digitsOnly, appends/truncates to the limit, calls onChange and updates state,
and wire both handlers to the input.

Comment on lines +102 to 107
onKeyDown={handleKeyDown}
placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'}
inputMode="numeric"
disabled={isDisabled}
maxLength={type === 'date' ? 10 : 8}
/>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

접근성 속성 부족: a11y 요구사항 충족 필요

PR 목표에 “접근성 속성 포함”이 있으므로 최소한의 ARIA를 추가하세요. 또한 붙여넣기 처리 핸들러를 연결하세요.

적용 diff:

         value={formatted}
-        onBeforeInput={handleBeforeInput}
-        onKeyDown={handleKeyDown}
+        onBeforeInput={handleBeforeInput}
+        onKeyDown={handleKeyDown}
+        onPaste={handlePaste}
         placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'}
         inputMode="numeric"
+        aria-invalid={state === 'error'}
+        aria-label={type === 'date' ? '날짜 입력' : '시간 입력'}
         disabled={isDisabled}

추가로 권장(선택): useId()로 레이블 요소와 htmlFor 연결(현 spanlabel) 및 aria-labelledby 사용을 검토해 주세요.

📝 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
onBeforeInput={handleBeforeInput}
onKeyDown={handleKeyDown}
placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'}
inputMode="numeric"
disabled={isDisabled}
maxLength={type === 'date' ? 10 : 8}
/>
value={formatted}
onBeforeInput={handleBeforeInput}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'}
inputMode="numeric"
aria-invalid={state === 'error'}
aria-label={type === 'date' ? '날짜 입력' : '시간 입력'}
disabled={isDisabled}
/>
🤖 Prompt for AI Agents
In packages/design-system/src/components/dateTime/DateTime.tsx around lines 102
to 107, the input is missing required accessibility attributes and a paste
handler; add minimal ARIA and event wiring by: attach an onPaste handler (e.g.,
handlePaste) to process pasted input, add aria-label or aria-labelledby
(preferably generate an id with useId() for the visible label and reference it
via htmlFor/aria-labelledby), set role if appropriate and include aria-disabled
when disabled, and ensure inputMode/placeholder remain; keep the ARIA text
concise and tie the label to the input for screen-readers.

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: 0

♻️ Duplicate comments (4)
packages/design-system/src/components/dateTime/DateTime.tsx (4)

62-64: 외부 value 동기화 useEffect 추가 — 좋습니다

제어/비제어 불일치 이슈가 해소되었습니다.


1-1: React 네임스페이스 타입 미수입으로 TS 컴파일 에러 가능

React.FormEvent/React.KeyboardEvent를 쓰지만 React 타입을 임포트하지 않았습니다. DOM 이벤트 타입을 직접 임포트해 시그니처를 단순화하세요.

적용 diff:

- import {  useState, useEffect } from 'react';
+ import { useEffect, useId, useState } from 'react';
+ import type { FormEvent, KeyboardEvent, ClipboardEvent } from 'react';

-  const handleBeforeInput = (e: React.FormEvent<HTMLInputElement>) => {
+  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {

-  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {

다음으로 잔여 네임스페이스 참조가 없는지 확인하세요.

#!/bin/bash
rg -nP "React\.(FormEvent|KeyboardEvent|ClipboardEvent)" -g '!**/dist/**' -g '!**/build/**'

Also applies to: 71-71, 86-86


71-83: 비숫자 유입 및 DOM 기본 입력과의 충돌

/[0-9]/ 포함 여부만 검사하고 원문을 그대로 append 하여 비숫자가 섞일 수 있고, 허용 케이스에서도 기본 입력을 막지 않아 깜빡임/캐럿 점프가 납니다. 항상 preventDefault하고 digitsOnly로 정규화하세요.

적용 diff:

-  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {
-    const input = e as unknown as InputEvent;
-
-    if (!/[0-9]/.test(input.data ?? '')) {
-      e.preventDefault();
-      return;
-    }
-
-    setRawDigits((prev) => {
-      const next = (prev + input.data!).slice(0, type === 'date' ? 8 : 4);
-      onChange?.(next); 
-      return next;
-    });
-  };
+  const handleBeforeInput = (e: FormEvent<HTMLInputElement>) => {
+    const ev = e.nativeEvent as InputEvent;
+    const added = digitsOnly(ev.data ?? '');
+    if (!added) {
+      e.preventDefault();
+      return;
+    }
+    e.preventDefault();
+    setRawDigits((prev) => {
+      const limit = type === 'date' ? 8 : 4;
+      const next = (prev + added).slice(0, limit);
+      onChange?.(next);
+      return next;
+    });
+  };

102-111: 붙여넣기 미처리 + a11y 속성 누락

PR 목표의 “접근성 속성 포함”을 충족하려면 onPaste 처리와 최소 ARIA가 필요합니다.

적용 diff(입력 요소):

       value={formatted}
       onBeforeInput={handleBeforeInput}
       onKeyDown={handleKeyDown}
+      onPaste={handlePaste}
       placeholder={type === 'date' ? 'YYYY.MM.DD' : 'HH:MM'}
       inputMode="numeric"
+      aria-invalid={state === 'error'}
+      aria-label={type === 'date' ? '날짜 입력' : '시간 입력'}
       disabled={isDisabled}

추가 함수(파일 내부 핸들러들 아래에 삽입):

const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
  e.preventDefault();
  const txt = digitsOnly(e.clipboardData.getData('text') || '');
  if (!txt) return;
  setRawDigits(prev => {
    const limit = type === 'date' ? 8 : 4;
    const next = (prev + txt).slice(0, limit);
    onChange?.(next);
    return next;
  });
};
🧹 Nitpick comments (3)
packages/design-system/src/components/dateTime/DateTime.tsx (3)

41-43: 포커스 가시성 보장 필요

outline-none만 적용되어 키보드 포커스가 보이지 않을 수 있습니다. 대체 포커스 스타일(:focus-visible 링/아웃라인) 추가를 권장합니다.


53-53: 세미콜론 뒤 export가 같은 줄에 위치(스타일 니트픽)

자동 포매터가 없다면 줄바꿈해 가독성을 유지하세요.

적용 diff:

-);export default function DateTime({
+);
+
+export default function DateTime({

86-95: Delete 키 처리도 보완 권장

Backspace만 처리합니다. 동일 로직으로 Delete도 처리하면 일관성이 좋아집니다.

예시 diff:

-  if (e.key === 'Backspace') {
+  if (e.key === 'Backspace' || e.key === 'Delete') {
     e.preventDefault();
     setRawDigits((prev) => {
       const next = prev.slice(0, -1);
       onChange?.(next);
       return next;
     });
   }
📜 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 8c5eca3 and 2275203.

📒 Files selected for processing (2)
  • apps/extension/src/pages/MainPop.tsx (5 hunks)
  • packages/design-system/src/components/dateTime/DateTime.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/extension/src/pages/MainPop.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 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/dateTime/DateTime.tsx
🧬 Code graph analysis (1)
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)
⏰ 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 (2)
packages/design-system/src/components/dateTime/DateTime.tsx (2)

66-69: 파생 표시값 계산 로직 — OK

rawDigits 슬라이스를 명시해 최대 길이를 보장하는 점이 좋습니다.


60-61: 초기 파싱 — OK

digitsOnly로 초기 정규화하는 접근 적절합니다.

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/extension/src/background.ts (2)

18-22: 액세스 토큰 평문 로그 제거

토큰은 비밀정보입니다. 콘솔 로그를 제거하거나 마스킹하세요.

-    chrome.storage.local.set({ 'token': message.token }, () => {
-      console.log('Token saved!', message.token);
-    });
+    chrome.storage.local.set({ token: message.token }, () => {
+      // Do not log tokens
+    });

4-12: PII 로그 제거 · 이메일 미존재 처리 · 쿼리 인코딩 적용 — 수정 필요

  • 문제: 콘솔에 이메일 직접 출력 → PII 노출 위험.
  • 문제: 설치 직후 info.email이 빈 문자열일 수 있음 → 빈값 처리 필요.
  • 문제: 쿼리값 미인코딩 → URL에 안전하게 전달하도록 encodeURIComponent 필요.

파일: apps/extension/src/background.ts (약 4-12줄)

-    chrome.identity.getProfileUserInfo(function (info) {
-       chrome.storage.local.set({ 'email': info.email }, () => {
-          console.log(info.email);
-        });
+    chrome.identity.getProfileUserInfo(function (info) {
+      const email = (info?.email || '').trim();
+      if (email) {
+        chrome.storage.local.set({ email }, () => {});
+      }
       setTimeout(() => {
         chrome.tabs.create({
-          url: `https://www.pinback.today/onboarding?email=${info.email}`,
+          url: email
+            ? `https://www.pinback.today/onboarding?email=${encodeURIComponent(email)}`
+            : `https://www.pinback.today/onboarding`,
         });
       }, 1000);
     });

manifest 권한 확인: apps/extension/manifest.json에 "identity" 및 "identity.email" 포함(확인됨).

apps/extension/src/apis/axiosInstance.ts (1)

64-66: 하드코딩된 이메일로 토큰 재발급됨

401/403 처리에서 'test@gmail.com'을 사용해 토큰을 재발급하고 있어 실제 사용자 세션이 깨집니다. 저장된 이메일을 읽어 사용해야 합니다.

-      const newToken = await fetchToken('test@gmail.com');
+      const emailForRetry = await new Promise<string | undefined>((resolve) => {
+        chrome.storage.local.get('email', (result) => resolve(result.email));
+      });
+      if (!emailForRetry) return Promise.reject(error);
+      const newToken = await fetchToken(emailForRetry);
♻️ Duplicate comments (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)

160-168: 모든 onError에서 홈 이동은 실패 은폐 위험 — 상태별 처리로 보완

이전 코멘트와 동일 이슈입니다. 성공 시에는 저장된 이메일 제거도 필요합니다.

-          onSuccess: () => {
-            window.location.href = '/';
-          },
-          onError: () => {
-            const savedEmail = localStorage.getItem('email');
-            if (savedEmail) {
-              window.location.href = '/';
-            }
-          }
+          onSuccess: () => {
+            try { localStorage.removeItem('email'); } catch {}
+            window.location.href = '/';
+          },
+          onError: (error: any) => {
+            const status = error?.response?.status;
+            const savedEmail = localStorage.getItem('email');
+            if (status === 409 || savedEmail) {
+              try { localStorage.removeItem('email'); } catch {}
+              window.location.href = '/';
+              return;
+            }
+            alert('회원가입에 실패했습니다. 잠시 후 다시 시도해주세요.');
+          }
🧹 Nitpick comments (6)
apps/extension/src/background.ts (1)

8-12: 설치 직후 1초 지연은 불필요할 수 있음

탭 오픈을 setTimeout으로 지연할 필요가 없다면 제거해 UX를 단순화하세요.

apps/extension/src/hooks/useCategoryManager.ts (3)

14-16: 초기 options 중복 제거

카테고리명이 중복일 수 있으니 Set으로 중복 제거를 권장합니다.

-const [options, setOptions] = useState<string[]>(
-  categoryData?.data?.categories?.map((c: Category) => c.categoryName) ?? []
-);
+const [options, setOptions] = useState<string[]>(
+  Array.from(
+    new Set(
+      categoryData?.data?.categories?.map((c: Category) => c.categoryName) ?? []
+    )
+  )
+);

18-22: 서버 갱신 반영 + 중복 방지 동기화

서버 갱신 시 기존 로컬 추가분을 유지하며 중복 없이 합치는 편이 안전합니다.

-useEffect(() => {
-  if (categoryData?.data?.categories) {
-    setOptions(categoryData.data.categories.map((c) => c.categoryName));
-  }
-}, [categoryData]);
+useEffect(() => {
+  if (categoryData?.data?.categories) {
+    setOptions((prev) =>
+      Array.from(
+        new Set([
+          ...prev,
+          ...categoryData.data.categories.map((c: Category) => c.categoryName),
+        ])
+      )
+    );
+  }
+}, [categoryData]);

40-43: 옵션 중복 추가 방지

같은 이름의 카테고리가 여러 번 들어갈 수 있습니다.

-setOptions((prev) => [...prev, newCategory.categoryName]);
+setOptions((prev) =>
+  prev.includes(newCategory.categoryName)
+    ? prev
+    : [...prev, newCategory.categoryName]
+);

신규 카테고리 추가 후 목록 쿼리 무효화(usePostCategories 내부 invalidate)가 되는지 확인 부탁드립니다.

apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)

71-75: 서비스 워커 등록 대기

getToken 전에 서비스 워커 등록을 await하지 않아 간헐 실패 가능성이 있습니다.

-      const permission = await Notification.requestPermission();
-      registerServiceWorker();
+      const permission = await Notification.requestPermission();
+      await registerServiceWorker();

102-109: 중복 알림 제거

requestFCMToken 내부에서 이미 실패 알림을 띄웁니다. 바깥에서 한 번 더 alert를 띄우면 UX가 나빠집니다.

-  if (token) {
-    setFcmToken(token);
-  } else {
-    alert('푸시 알람 설정 에러');
-  }
+  if (token) setFcmToken(token);
📜 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 2275203 and 1419a3f.

📒 Files selected for processing (5)
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (6 hunks)
  • apps/extension/src/apis/axiosInstance.ts (1 hunks)
  • apps/extension/src/background.ts (1 hunks)
  • apps/extension/src/hooks/useCategoryManager.ts (3 hunks)
  • apps/extension/src/pages/DuplicatePop.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/extension/src/pages/DuplicatePop.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 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/background.ts
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
🧬 Code graph analysis (2)
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)
🔇 Additional comments (6)
apps/extension/src/background.ts (1)

5-5: 'email' 키 통일 OK

프로젝트 컨벤션(스토리지 키 'email' 사용)에 맞춰져 있어요.

apps/extension/src/apis/axiosInstance.ts (1)

24-29: noAuth 경로 분기 적절

토큰/회원가입 엔드포인트 우회 처리는 합리적입니다.

apps/extension/src/hooks/useCategoryManager.ts (1)

1-1: useEffect 임포트 추가 OK

동기화 목적의 useEffect 도입이 합리적입니다.

apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (3)

51-60: URL 이메일 저장 로직 정합

쿼리에서 읽어 state/localStorage에 저장하는 흐름은 합리적입니다.


209-219: 뒤로 버튼 표시 조건 개선 LGTM

최초(step 0)에는 숨기고 이후 단계만 노출하는 변경이 합리적입니다.


155-158: fcmToken null 허용 여부 확인

백엔드가 fcmToken: null을 허용하는지 확인 필요합니다. 미허용이면 조건부 포함하세요.

Comment on lines 14 to 16
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken } from 'firebase/messaging';
import { registerServiceWorker } from '@pages/onBoarding/utils/registerServiceWorker';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Firebase 앱 중복 초기화 위험

initializeApp을 컴포넌트 렌더마다 호출하면 재랜더 시 "[DEFAULT] already exists" 예외가 날 수 있습니다. getApps/getApp으로 보호하세요.

-import { initializeApp } from 'firebase/app';
+import { initializeApp, getApps, getApp } from 'firebase/app';
@@
-const app = initializeApp(firebaseConfig);
+const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
 const messaging = getMessaging(app);

추가로, 모듈 스코프로 이동시키면 더 안전합니다.

Also applies to: 66-68

🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
14-16 (and similarly lines 66-68), the code calls initializeApp on every render
which can throw "[DEFAULT] already exists"; to fix, move Firebase initialization
to module scope and guard it using getApps/getApp (or try getApp and fall back
to initializeApp) so initialization only happens once, then export or reuse the
initialized app/messaging instances from module-scope variables; ensure
component imports these pre-initialized instances instead of calling
initializeApp inside the render body.

Comment on lines +135 to 142
if (alarmSelected==1){
setRemindTime('09:00');
} else if (alarmSelected==2){
setRemindTime('20:00');
} else{
const raw = AlarmsType[alarmSelected - 1].time;
setRemindTime(normalizeTime(raw))
}
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:NaN 가능)

AlarmsType[2].time이 빈 문자열이면 normalizeTime('') → 'NaN:NaN'이 됩니다. 시간 미입력 시 진행을 막고, 입력값을 받아오도록 해야 합니다.

-      if (alarmSelected==1){
+      if (alarmSelected === 1){
         setRemindTime('09:00');
-      } else if (alarmSelected==2){
+      } else if (alarmSelected === 2){
         setRemindTime('20:00');
       } else{
-        const raw = AlarmsType[alarmSelected - 1].time;
-        setRemindTime(normalizeTime(raw))
+        const raw = AlarmsType[alarmSelected - 1]?.time;
+        if (!raw) {
+          alert('시간을 입력해주세요.');
+          return; // 시간 미입력 시 다음 단계로 넘어가지 않음
+        }
+        setRemindTime(normalizeTime(raw));
       }

권장: AlarmStep에서 사용자 입력 시간을 state로 올려(MainCard가 보유) 직접 사용하세요.

📝 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
if (alarmSelected==1){
setRemindTime('09:00');
} else if (alarmSelected==2){
setRemindTime('20:00');
} else{
const raw = AlarmsType[alarmSelected - 1].time;
setRemindTime(normalizeTime(raw))
}
if (alarmSelected === 1){
setRemindTime('09:00');
} else if (alarmSelected === 2){
setRemindTime('20:00');
} else{
const raw = AlarmsType[alarmSelected - 1]?.time;
if (!raw) {
alert('시간을 입력해주세요.');
return; // 시간 미입력 시 다음 단계로 넘어가지 않음
}
setRemindTime(normalizeTime(raw));
}
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
135 to 142, the code calls normalizeTime on AlarmsType[alarmSelected - 1].time
which can be an empty string and produce "NaN:NaN"; update this to (1) lift the
user-entered alarm time state from AlarmStep up into MainCard so MainCard reads
and validates the actual input, (2) before calling normalizeTime check for
empty/null/invalid time and block progression if missing (show/return an error
or prompt the user to enter a time), and (3) only call normalizeTime when the
value is non-empty and valid; ensure alarmSelected cases still set defaults for
preset options (1 and 2).

Comment on lines 30 to 33
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

이메일/토큰 부재 시 처리 보완 및 헤더 세팅 안전화

이메일이 없을 때 토큰 엔드포인트를 호출하면 서버 에러가 날 수 있고, headers가 undefined일 수 있습니다.

   const email = await new Promise<string | undefined>((resolve) => {
     chrome.storage.local.get('email', (result) => resolve(result.email));
   });
+  if (!email) {
+    // 이메일이 없으면 인증 없이 진행
+    return config;
+  }

-  if (!token || token === 'undefined') {
+  if (!token) {
     token = await fetchToken(email);
   }

-  config.headers.Authorization = `Bearer ${token}`;
+  config.headers = config.headers ?? {};
+  if (token) {
+    config.headers.Authorization = `Bearer ${token}`;
+  }

동시 다발 요청 시 토큰 재발급이 중복 호출되지 않도록 간단한 in-flight Promise(lock) 적용을 권장합니다. 필요 시 패치 드리겠습니다.

Also applies to: 41-46

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 기능 개발하라 개발 달려라 달려

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] sp1 QA 온보딩&익스텐션 2차 반영

1 participant