Skip to content

Comments

Feat(client): Time picker 구현#49

Merged
constantly-dev merged 15 commits intodevelopfrom
feat/#43/time-picker
Sep 3, 2025
Merged

Feat(client): Time picker 구현#49
constantly-dev merged 15 commits intodevelopfrom
feat/#43/time-picker

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Sep 1, 2025

📌 Related Issues

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

📄 Tasks

  • 온보딩에서 사용하는 time picker 구현

⭐ PR Point (To Reviewer)

📍 React Wheel Picker 도입

일단 해당 picker 구현이 리소스가 많이 들 것 같고, 기존 ios 스타일 기반의 shadcn 기반 쾨드를 발견하여 그걸 사용해서 구현하게 되었어요.

[사용 이유]
-> 사용 라이브러리

  • 마감 기한에 비해 생각보다 리소스가 많이 들어감
  • shadcn 기반이라 코드 기반이니 원하는 커스텀이 가능할 것으로 판단

📍 구조

물론 현재 디자인 기준으로는 time picker가 onboarding 한 개의 도메인에서만 사용되지만, 이후 재사용이 될 것을 생각해서 pds에 추가를 했어요. 하지만 정확하게 똑같은 디자인이 다 구현된 상태로 pds에 넣기에는 공통의 의미가 많이 없어질 수 있다고 생각했어요. 디자인이 같은 방식으로 재사용되도록 디자인 파트분들께 말씀드려도 되지만, 혹시나 약간의 디자인 변경을 허용할 수 있도록 확장성을 조금 추가해봤어요.

그래서 최소한의 디자인을 pds내 WheelPicker.tsx에 두고, 사용하는 곳에서 이 컴포넌트를 가져와 추가적인 디자인을 주입할 수 있도록 설계했어요 (ex. margin, layout, color 등)

📦 packages/design-system
└─ src/components
   └─ wheelPicker
      └─ WheelPicker.tsx (WheelPickerWrapper, WheelPicker)

client/src/pages/onBoarding/components/timePicker/TimePicker.tsx
아래와 같이 필요한 디자인에 따라 커스텀 사용

return (
    <div className="common-shadow flex w-[26rem] flex-col items-center px-[1.6rem] m-20">
      <WheelPickerWrapper className="flex h-[16.8rem] !items-center py-[0.8rem]">
        <WheelPicker options={hourOptions} infinite optionItemHeight={56} />
        <p className="body2-m z-2 mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
          :
        </p>
        <WheelPicker options={minuteOptions} infinite optionItemHeight={56} />
        <div className="mx-[0.4rem]" />
        <WheelPicker options={meridiemOptions} optionItemHeight={56} />
      </WheelPickerWrapper>
      <div className="flex w-full gap-[1.2rem] pb-[2.4rem] pt-[0.8rem]">
        <Button variant="secondary">취소</Button>
        <Button variant="primary">확인</Button>
      </div>
    </div>
  );

📍 해당 코드 사용의 커스텀 한계

shadcn기반이라고 해서 모든 add 했는데 사실 완벽하게 코드 기반이 아닌 것이 사실 가장 큰 문제였어요. 사실상 의존성이 생기는 것을 보아 shadcn 기반이라고 하지만 라이브러리라고 봐야 할 것 같아요. 아래와 같은 import문이 생겼기 때문이에요!

import '@ncdai/react-wheel-picker/style.css';
import * as WheelPickerPrimitive from '@ncdai/react-wheel-picker';

그러다 보니 완벽하게 해당 컴포넌트를 커스텀하기가 불가능한 부분이 있었어요.
WheelPickerWrapper에는 props로 className를 줘서 string 형식으로 스타일을 자유롭게 추가가 가능하기는 하지만, WheelPicker에는 classNames라는 이름으로 기존 tailwind의 클래스를 추가하는 것이 아닌 해당 라이브러리에서 정의한 인터페이스만 추가가 가능한 형태였기 때문이에요.

type WheelPickerProps = {
    /** 생략 */
 
    /** Custom class names for styling different parts of the wheel */
    classNames?: WheelPickerClassNames; // 해당 부분
};

따라서 picker 내부의 태그들에 완벽하게 커스텀이 쉽지는 않았고, 특히 cn util 등으로 스타일 우선순위에 따른 병합이 되지 않았기 때문에 ! important 사용을 하게 되었어요.

! important 사용이 css의 우선순위 체계를 파괴한다는 점에서 지양하는 것이 좋지만 해당 라이브러리에 숨겨진 코드 내 스타일이 우선순위가 높아서 커스텀이 불가능해서 사용을 피할 수 없다고 생각했고, 이러한 이유라면 크게 문제가 없을 것이라고 생각했어요.


📍 tailwind @utility의 스타일 적용 문제

WheelPicker는 위에서 언급한 것처럼 완벽하게 모든 스타일을 커스텀 할 수 없지만, 제공하는 optionItem등의 인터페이스에는 스타일 추가가 가능했어요. 저희 pds의 token으로 color 등이 잘 적용이 되었지만 이상하게 typo 관련 token만 적용이 안되는 것을 확인했어요.

color token은 잘 적용되니 shared-styles.css는 잘 불러오는 것 같았고, color와 typo의 차이를 살펴보니 결국 이전에 자동완성에도 문제가 되었던 @utility 키워드가 문제인 것 같았어요.

color에는 @theme을 사용해서 tailwind 기존 클래스로 정의하는 의미를 갖지만 @utility는 기존 클래스에 추가하는 것이 아닌 완전 그 자체로 새로운 유틸리티 클래스를 추가하기 때문에 이를 WheelPicker 컴포넌트로 넣을 때 인식을 제대로 못한다고... 추측했어요.

그래서 저는 해결책으로 body2-m 유틸리티 클래스 대신 이 내부 속성 값들을 다 하나하나 적어줬어요. 그리고 이 또한 우선순위 문제 때문에 !를 사용해서 해결했습니다.

  optionItem: cn(
          'text-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]', // 이렇게!
          classNames?.optionItem
        ),

사실 뭔가 완벽하게 해결한 느낌이 아니라.. 찝찝한데 이외에 더 좋은 방법이 생각나지 않고 찾지 못해서..!!
혹시나 더 좋은 방법을 찾게 되면 편하게 의견 공유해주세요!!!!!!!



📷 Screenshot

image
bandicam.2025-09-01.23-14-14-187.mp4

Summary by CodeRabbit

  • 신규 기능
    • 온보딩 페이지 추가 및 라우팅으로 접근 가능.
    • 온보딩용 시간 선택 UI(휠 피커) 추가: 시/분/오전·오후 선택, 확인/취소 버튼 제공.
  • 스타일
    • 휠 피커 기본 스타일과 하이라이트 적용으로 일관된 UI 제공.
    • 공통 색상 토큰(white, black, transparent) 추가로 색상 일관성 개선.
  • 작업
    • 디자인 시스템에 휠 피커 컴포넌트·타입과 외부 휠피커 의존성 및 레지스트리 추가.
    • 빌드 설정에 tsconfig-paths 도입 및 경로 해석 정비.
  • 테스트
    • Storybook 관련 테스트 통합 추가.

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Walkthrough

온보딩 페이지와 라우트가 추가되고, 시/분/AM·PM 휠 기반 TimePicker UI가 도입되며 디자인시스템에 WheelPicker 래퍼·타입·의존성·레지스트리 추가와 tsconfig/vite 설정 및 공통 색상 토큰·import 경로 조정이 적용되었습니다.

Changes

Cohort / File(s) Change Summary
OnBoarding 페이지 및 라우트 추가
apps/client/src/pages/onBoarding/OnBoarding.tsx, apps/client/src/routes/router.tsx, apps/client/src/routes/routesConfig.ts
OnBoarding 컴포넌트 추가 및 라우터·ROUTES_CONFIG에 onBoarding 경로 등록
TimePicker 컴포넌트 (온보딩 하위)
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx
WheelPicker 기반 시/분/AM·PM 선택 UI와 취소/확인 버튼을 갖는 TimePicker 컴포넌트 추가 (onSave/onCancel/onClick props)
Design System: WheelPicker 통합 및 재노출
packages/design-system/src/components/wheelPicker/WheelPicker.tsx, packages/design-system/src/components/index.ts
@ncdai/react-wheel-picker 래핑 컴포넌트(WheelPicker, WheelPickerWrapper)와 타입(WheelPickerOption 등) 추가 및 인덱스 재노출
Design System: 의존성·레지스트리 변경
packages/design-system/package.json, packages/design-system/components.json
@ncdai/react-wheel-picker 의존성 추가, vite-tsconfig-paths devDependency 추가, components.json에 registries 항목 추가
Design System: 경로/빌드 설정 변경
packages/design-system/vite.config.ts, packages/design-system/tsconfig.json
tsconfig-paths 플러그인 도입 및 alias 제거, Storybook Vitest 통합 플러그인 추가, __dirname 호환 처리 추가
공통 스타일 토큰 추가
packages/tailwind-config/shared-styles.css
@theme--color-white, --color-black, --color-transparent 기본 토큰 추가
Import 경로 정리(여러 컴포넌트)
packages/design-system/src/components/*
여러 컴포넌트에서 @/lib 등 경로를 상대경로로 변경 및 일부 import 순서 조정

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Router
  participant OnBoarding
  participant TimePicker
  participant DS as DesignSystem/WheelPicker

  User->>Router: /onBoarding 요청
  Router->>OnBoarding: <OnBoarding/> 렌더
  OnBoarding->>TimePicker: <TimePicker/> 렌더
  TimePicker->>DS: WheelPickerWrapper & WheelPicker 렌더 (hour, minute, meridiem)
  User->>TimePicker: 휠 조작 / 확인 → onSave(selected) 또는 취소 → onCancel()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Timepicker 컴포넌트 구현 (#43)
Progress Bar 구현 및 분기/역할 정의 (#25) Progress 관련 구현(컴포넌트 로직·스토리·테스트)은 포함되지 않음; only reordering/exports/path tweaks present

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
components.json에 registries 추가 (packages/design-system/components.json) 레지스트리 메타데이터 추가는 TimePicker(#43)·Progress(#25) 기능 요구와 직접적 관련 없음
Vite/tsconfig 경로·플러그인 변경 (packages/design-system/vite.config.ts, packages/design-system/tsconfig.json) 빌드/경로 인프라 변경으로, 명시된 이슈의 컴포넌트 기능 요구를 벗어남
Tailwind 색상 토큰 추가 (packages/tailwind-config/shared-styles.css) 전역 스타일 토큰 추가는 두 이슈의 범위(타임피커·프로그레스)와 직접 연관되지 않음

Possibly related PRs

Suggested labels

🛠️ Feature, frontend, needs-review

Suggested reviewers

  • jllee000
  • jjangminii
  • karnelll

Poem

"당근 들고 휠을 굴려요 🐇
시와 분이 빙글 돌아 길을 맞추고
온보딩 문턱에 살며시 멈춰서
확인 한 번이면 시간이 정해지네
배포 앞에 당근 한 입, 축하해요"


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a379f82 and 3c9d62c.

📒 Files selected for processing (1)
  • apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#43/time-picker

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Sep 1, 2025
@github-actions
Copy link

github-actions bot commented Sep 1, 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: 2

🧹 Nitpick comments (10)
packages/design-system/package.json (1)

54-54: @ncdai/react-wheel-picker peerDependencies 호환성 확인 완료

  • "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"로 React ^19.1.0과 충돌 없음
  • license: MIT, dependencies 없음
  • 선택: 디자인 시스템 번들러에 CSS sideEffects 설정 추가 ("sideEffects": ["**/*.css"] 등)
  • 선택: 번들 크기 점검
apps/client/src/routes/routesConfig.ts (1)

18-21: 경로 네이밍을 kebab-case로 통일

다른 경로들이 모두 소문자·kebab-case(/my-bookmarks)를 사용하는 만큼, 온보딩 경로도 /onboarding으로 변경을 권장합니다.

   onBoarding: {
     title: '온보딩',
-    path: '/onBoarding',
+    path: '/onboarding',
   },
apps/client/src/pages/onBoarding/OnBoarding.tsx (1)

1-5: 폴더/경로 케이스 통일(onBoarding → onboarding) 및 문구 현지화

페이지·폴더·라우트 키를 모두 onboarding으로 통일하면 추후 혼선을 줄일 수 있습니다. 화면 문구는 임시라면 OK이나, 병합 전 i18n 적용 또는 최종 문구로 교체를 권장합니다.

apps/client/src/routes/router.tsx (2)

4-4: 온디맨드 로딩으로 초기 번들 최적화

OnBoarding 페이지를 lazy import로 전환해 초기 로드를 줄이는 것을 권장합니다.

아래와 같이 변경 제안:

-import OnBoarding from '@pages/onBoarding/OnBoarding';
+import { lazy, Suspense } from 'react';
+const OnBoarding = lazy(() => import('@pages/onBoarding/OnBoarding'));

31-34: Lazy 컴포넌트는 Suspense로 감싸기

lazy 도입 시 Suspense로 감싸주세요. Fallback은 스켈레톤 등으로 교체 가능.

       {
-        path: ROUTES_CONFIG.onBoarding.path,
-        element: <OnBoarding />,
+        path: ROUTES_CONFIG.onBoarding.path,
+        element: (
+          <Suspense fallback={null}>
+            <OnBoarding />
+          </Suspense>
+        ),
       },
packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1)

27-38: selected 가시성 개선 여지: selectedOptionItem 스타일도 오버라이드 가능하도록 기본값을 제안

현재 optionItemhighlightWrapper만 지정되어 있어 선택 항목의 대비가 약할 수 있습니다. 기본 selected 스타일을 살짝 부여하고, 외부에서 덮어쓸 수 있게 병합해두면 UX가 선명해집니다.

예시 diff:

   <WheelPickerPrimitive.WheelPicker
     classNames={{
       ...classNames,
       optionItem: cn(
         'text-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]',
         classNames?.optionItem
       ),
+      selectedOptionItem: cn(
+        'text-font-black-1',
+        classNames?.selectedOptionItem
+      ),
       highlightWrapper: cn(
         'bg-white border-y-[0.5px] border-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]',
         classNames?.highlightWrapper
       ),
     }}
packages/design-system/src/components/index.ts (1)

12-13: WheelPickerClassNames 타입도 re-export 해주세요.

외부에서 커스텀 스타일 키 입력 시 타입 보조가 됩니다. 현재 WheelPickerOption만 노출되어 있어 아쉽습니다.

적용 diff:

 export { WheelPicker, WheelPickerWrapper } from './wheelPicker/WheelPicker';
-export type { WheelPickerOption } from './wheelPicker/WheelPicker';
+export type { WheelPickerOption, WheelPickerClassNames } from './wheelPicker/WheelPicker';
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (3)

29-31: z-index 유틸 오타 및 장식 문자 접근성 처리

z-2는 스케일에 없을 가능성이 큽니다. 임의 값으로 z-[2]를 사용하고, 콜론 문자는 스크린리더에 불필요하므로 aria-hidden을 추가해 주세요.

적용 diff:

-        <p className="body2-m z-2 mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
+        <p aria-hidden="true" className="body2-m z-[2] mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
           :
         </p>

27-35: 휠 별 ARIA 레이블 추가로 접근성 보완

각 휠에 의미 있는 레이블을 부여해 스크린리더 탐색성을 개선하세요.

적용 diff:

-        <WheelPicker options={hourOptions} infinite optionItemHeight={56} />
+        <WheelPicker aria-label="시간(시)" options={hourOptions} infinite optionItemHeight={56} />
...
-        <WheelPicker options={minuteOptions} infinite optionItemHeight={56} />
+        <WheelPicker aria-label="시간(분)" options={minuteOptions} infinite optionItemHeight={56} />
...
-        <WheelPicker options={meridiemOptions} optionItemHeight={56} />
+        <WheelPicker aria-label="오전/오후" options={meridiemOptions} optionItemHeight={56} />

8-23: 렌더링마다 옵션 배열 재생성 회피(미세 최적화)

빈번한 재렌더링 시 불필요할 수 있습니다. 컴포넌트 밖 상수로 승격하거나 useMemo 사용을 고려해 주세요.

예시:

// 컴포넌트 밖
const createArray = (length: number, add = 0): WheelPickerOption[] =>
  Array.from({ length }, (_, i) => {
    const value = i + add;
    return { label: value.toString().padStart(2, '0'), value: value.toString() };
  });

const HOUR_OPTIONS = createArray(12, 1);
const MINUTE_OPTIONS = createArray(60);
const MERIDIEM_OPTIONS: WheelPickerOption[] = [{ label: 'AM', value: 'AM' }, { label: 'PM', value: 'PM' }];

// 컴포넌트 내부에서는 위 상수 사용
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb59c9d and 6c73342.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • apps/client/src/pages/onBoarding/OnBoarding.tsx (1 hunks)
  • apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (1 hunks)
  • apps/client/src/routes/router.tsx (2 hunks)
  • apps/client/src/routes/routesConfig.ts (1 hunks)
  • packages/design-system/components.json (1 hunks)
  • packages/design-system/package.json (1 hunks)
  • packages/design-system/src/components/index.ts (1 hunks)
  • packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1 hunks)
  • packages/tailwind-config/shared-styles.css (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🧬 Code graph analysis (2)
packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1)
packages/design-system/src/lib/utils.ts (1)
  • cn (108-110)
apps/client/src/routes/router.tsx (1)
apps/client/src/routes/routesConfig.ts (1)
  • ROUTES_CONFIG (1-22)
🪛 Biome (2.1.2)
packages/tailwind-config/shared-styles.css

[error] 165-165: expected , but instead found --color-white

Remove --color-white

(parse)


[error] 165-165: Unexpected value or character.

Expected one of:

(parse)


[error] 165-165: expected , but instead found ;

Remove ;

(parse)


[error] 166-166: expected , but instead found --color-black

Remove --color-black

(parse)


[error] 166-166: Unexpected value or character.

Expected one of:

(parse)


[error] 166-166: Expected an identifier but instead found '000000'.

Expected an identifier here.

(parse)


[error] 167-167: expected , but instead found --color-transparent

Remove --color-transparent

(parse)


[error] 167-167: expected , but instead found ;

Remove ;

(parse)


[error] 167-167: Unexpected unknown pseudo-class transparent

See MDN web docs for more details.

(lint/correctness/noUnknownPseudoClass)

🔇 Additional comments (3)
packages/tailwind-config/shared-styles.css (1)

169-177: 색 토큰 추가 방향은 LGTM.

bg-white/text-black/투명 레이어 등의 DS 일관성 확보에 유용합니다. 위 parse 회피만 적용하면 문제없습니다.

packages/design-system/src/components/wheelPicker/WheelPicker.tsx (2)

10-20: Wrapper의 기본 flex 부여와 cn 병합 방식은 적절합니다.

props 포워딩과 className 병합이 깔끔해 재사용성 좋습니다.


1-1: 라이브러리 글로벌 CSS import의 번들러 호환성 확인 권장

라이브러리 컴포넌트 파일에서 글로벌 CSS를 side-effect import 하면 Next.js 설정(특히 pages 라우터)에서는 제약이 있을 수 있습니다. 현 레포의 앱 번들러(Vite/Next app 라우터 등)에서 문제없는지 한번 확인 부탁드립니다. 이슈 시 DS 루트에서 한 번만 import 하거나 consumer 앱 엔트리에서 가져오는 방식을 고려해볼 수 있습니다.

Comment on lines +20 to +23
"iconLibrary": "lucide",
"registries": {
"@ncdai": "https://chanhdai.com/r/{name}.json"
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

shadcn components.json의 registries 커스텀 URL 사용 — CLI 호환성 확인

해당 필드는 개발 시 스캐폴딩 용도로만 쓰이는 게 이상적입니다. 사용 중인 shadcn CLI 버전이 "registries" 맵 형태와 커스텀 레지스트리 URL을 지원하는지 확인해주세요. 외부 도메인(https://chanhdai.com) 신뢰·가용성도 한 번 검토하면 좋겠습니다.

최신 스키마 확인용 웹 질의:


🌐 Web query:

shadcn components.json "registries" field schema and support for custom registries in current CLI

💡 Result:

Short answer

  • components.json "registries" is an array of registry objects (minimum fields: name and baseUrl) that tell the CLI where to fetch registries from. [1][2]
  • You can host custom registries as JSON (registry.json / registry-item.json) that conform to shadcn's registry schemas; the CLI expects those JSON endpoints and will resolve registry dependencies automatically. [3][5]
  • The CLI supports adding/using custom registries (e.g., shadxn/shadcn CLI: registry add / registry init / registry build; and add components with -r ). Auth must be handled by your registry server (token query param is suggested). [4][5]

Relevant docs

  • components.json / registries usage. [1]
  • Registry overview (requirements, hosting JSON, run-your-own). [2]
  • registry-item.json schema (item/files/registryDependencies and automatic resolution). [3]
  • CLI registry commands (registry add/init/build and using -r to install from a registry). [4]
  • Getting started / auth and publishing guidance. [5]

If you want, I can fetch the exact JSON schema URLs (registry.json and registry-item.json $schema links) and show an example components.json + sample registry.json you can deploy.


components.json의 registries 필드 스키마 및 CLI 지원 확인 및 형식 수정

  • 최신 shadcn CLI 스키마에 따르면 registries는 맵이 아니라 레지스트리 객체 배열이어야 하며, 각 객체에 필수로 namebaseUrl을 지정해야 합니다.
  • 예시 (packages/design-system/components.json):
    "registries": [
      { "name": "@ncdai", "baseUrl": "https://chanhdai.com/r" }
    ]
  • 사용 중인 shadcn CLI 버전이 커스텀 레지스트리 기능(registry add/init/build-r <registry-name> 플래그)을 지원하는지 검증
  • 호스트 도메인의 신뢰성·가용성 및 인증 방식(token 쿼리 파라미터 권장) 점검 필요
🤖 Prompt for AI Agents
In packages/design-system/components.json around lines 20–23, the registries
field currently uses a map but should be an array of registry objects per the
latest shadcn CLI schema; replace the map with an array of objects each
containing required "name" and "baseUrl" (e.g. { "name": "@ncdai", "baseUrl":
"https://chanhdai.com/r" }), update any code/CI that reads this file to the new
shape, verify the installed shadcn CLI version supports custom registries and
the registry-related flags/commands, and validate the registry host for
availability/trust and recommend using a token passed as a query parameter for
authentication.

Comment on lines +164 to +168
/* default */
--color-white: #ffffff;
--color-black: #000000;
--color-transparent: transparent;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Biome 파서 오류로 CI 깨짐: @theme 블록 안의 커스텀 프로퍼티 선언을 :root로 감싸주세요.

Biome(2.1.2)가 Tailwind v4 @theme의 루트 레벨 커스텀 프로퍼티 선언을 제대로 파싱하지 못해 parse 에러가 납니다. 동일 파일의 기존 토큰들은 변경 라인이 아니라 리포팅되지 않았을 뿐, 새로 추가된 3개 라인에서 빌드가 막힙니다. :root로 한 번 감싸면 Biome이 “스타일 규칙”으로 인식해 통과합니다(Tailwind v4도 스코프드 토큰을 지원하므로 동작 동일).

적용 diff:

   /* default */
-  --color-white: #ffffff;
-  --color-black: #000000;
-  --color-transparent: transparent;
+  :root {
+    --color-white: #ffffff;
+    --color-black: #000000;
+    --color-transparent: transparent;
+  }
📝 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
/* default */
--color-white: #ffffff;
--color-black: #000000;
--color-transparent: transparent;
/* default */
:root {
--color-white: #ffffff;
--color-black: #000000;
--color-transparent: transparent;
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 165-165: expected , but instead found --color-white

Remove --color-white

(parse)


[error] 165-165: Unexpected value or character.

Expected one of:

(parse)


[error] 165-165: expected , but instead found ;

Remove ;

(parse)


[error] 166-166: expected , but instead found --color-black

Remove --color-black

(parse)


[error] 166-166: Unexpected value or character.

Expected one of:

(parse)


[error] 166-166: Expected an identifier but instead found '000000'.

Expected an identifier here.

(parse)


[error] 167-167: expected , but instead found --color-transparent

Remove --color-transparent

(parse)


[error] 167-167: expected , but instead found ;

Remove ;

(parse)


[error] 167-167: Unexpected unknown pseudo-class transparent

See MDN web docs for more details.

(lint/correctness/noUnknownPseudoClass)

🤖 Prompt for AI Agents
In packages/tailwind-config/shared-styles.css around lines 164 to 168, the three
custom property declarations currently at root level inside the @theme block
must be wrapped in a :root selector; update that section so the declarations
(--color-white, --color-black, --color-transparent) are placed inside a :root {
... } block within the existing @theme block so the parser accepts them.

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 (3)
packages/design-system/src/components/card/RemindCard.tsx (1)

31-38: 이미지 대체 텍스트 및 지연 로딩 추가로 접근성/성능 개선

동적 이미지에 alt가 없어 스크린리더 노출이 누락됩니다. lazy 로딩도 함께 권장합니다.

-        {imageUrl ? (
-          <img src={imageUrl} className="h-full w-full object-cover" />
+        {imageUrl ? (
+          <img
+            src={imageUrl}
+            alt={`${title} 이미지`}
+            loading="lazy"
+            className="h-full w-full object-cover"
+          />
         ) : (
           <img
             src={chippiNoImage}
             alt="이미지 없을 경우 logo"
             className="h-[12rem]"
           />
         )}
packages/design-system/src/components/card/MyBookmarkCard.tsx (1)

5-11: img 대체 텍스트 및 지연 로딩 추가

실제 이미지 분기에서 alt가 누락되어 접근성 검증에 실패합니다. 또한 lazy 로딩을 추가하면 성능에 유리합니다.

 interface MyBookmarkCardProps {
   title: string;
   content?: string;
   category?: string;
   imageUrl?: string;
   date: string;
+  imageAlt?: string;
 }
@@
-        {imageUrl ? (
-          <img src={imageUrl} className="h-full w-full object-cover" />
+        {imageUrl ? (
+          <img
+            src={imageUrl}
+            alt={imageAlt ?? '북마크 이미지'}
+            loading="lazy"
+            decoding="async"
+            className="h-full w-full object-cover"
+          />
         ) : (
           <img
             src={chippiNoImage}
             alt="이미지 없을 경우 logo"
+            loading="lazy"
+            decoding="async"
             className="h-[12rem]"
           />
         )}

Also applies to: 23-31

packages/design-system/vite.config.ts (1)

44-69: Playwright 의존성 추가 필요

  • packages/design-system/package.json에 @playwright/test를 devDependencies로 추가하세요.

alias import 미검출, tsconfig 경로 매핑 및 .storybook 디렉토리는 정상 확인되었습니다.

🧹 Nitpick comments (10)
packages/design-system/src/components/badge/Badge.stories.tsx (2)

5-5: CSF 타입 안전성 향상: satisfies 패턴 적용 제안

과도한 타입 확장을 막고 오타를 조기에 잡기 위해 const meta = { ... } satisfies Meta<typeof Badge> 패턴을 권장합니다.

다음 diff 제안:

-const meta: Meta<typeof Badge> = {
+const meta = {
   title: 'Components/Badge',
   component: Badge,
   tags: ['autodocs'],
   args: {
@@
-};
+} satisfies Meta<typeof Badge>;

Also applies to: 28-28


45-48: 상호작용 테스트의 선택자 안정성 개선

텍스트 매칭은 사소한 라벨 변경에 취약합니다. 버튼 역할 기반 쿼리로 견고하게 바꾸는 것을 추천합니다(컴포넌트가 실제 button을 렌더링한다는 전제).

-    await userEvent.click(await canvas.findByText(String(args.text)));
+    await userEvent.click(
+      await canvas.findByRole('button', { name: String(args.text) })
+    );

버튼이 아니라면 해당 역할에 맞는 접근성 선택자로 조정해 주세요.

packages/design-system/src/components/button/Button.tsx (1)

33-47: disabled vs. isDisabled 이중화 해소 제안

현재 {...props}가 disabled를 덮어쓸 수 있어 혼동 여지가 있습니다. 단일 계산 값으로 일관되게 처리해 주세요.

-const Button = ({
-  variant = 'primary',
-  size,
-  children,
-  className,
-  isDisabled = false,
-  ...props
-}: ButtonProps) => {
+const Button = ({
+  variant = 'primary',
+  size,
+  children,
+  className,
+  isDisabled = false,
+  disabled,
+  ...props
+}: ButtonProps) => {
+  const computedDisabled = isDisabled || disabled;
   return (
     <button
       type="button"
-      className={cn(buttonVariants({ variant, size }), className)}
-      disabled={isDisabled}
+      className={cn(buttonVariants({ variant, size }), className)}
+      disabled={computedDisabled}
       {...props}
     >
       {children}
     </button>
   );
 }
packages/design-system/src/components/card/RemindCard.tsx (1)

23-24: 장식용 아이콘은 스크린리더에서 숨기기

버튼에 aria-label이 있으므로 내부 아이콘은 aria-hidden 처리해 중복 낭독을 방지하세요.

-        <Icon name="ic_clock_active" />
+        <Icon name="ic_clock_active" aria-hidden="true" />
...
-            <Icon name="ic_details_category" />
+            <Icon name="ic_details_category" aria-hidden="true" />

Also applies to: 50-51

packages/design-system/src/components/toast/Toast.tsx (1)

10-12: role/aria-live 의미 충돌 정리

role="alert"는 기본적으로 assertive입니다. polite를 원하면 role="status"가 더 적절합니다.

-      role="alert"
-      aria-live="polite"
+      role="status"
+      aria-live="polite"
packages/design-system/src/components/toast/hooks/uesFadeOut.tsx (1)

1-50: 파일명 오타 및 네이밍 정리

  • packages/design-system/src/components/toast/hooks/uesFadeOut.tsx 파일명을 컴포넌트명에 맞춰 AutoDismissToast.tsx로 변경하거나, 훅으로 분리할 경우 useFadeOut.tsx로 수정
  • 변경된 파일명에 맞게 아래 경로의 import 문 업데이트
    • packages/design-system/src/components/toast/Toast.stories.tsx (4행)
    • packages/design-system/src/components/index.ts (10행)
packages/design-system/src/components/textarea/Textarea.tsx (1)

9-31: ref 전달을 위해 forwardRef로 전환 제안

폼 포커스 관리/스크롤 복원 등을 위해 ref 노출이 필요할 수 있습니다. 아래처럼 forwardRef로 감싸두면 재사용성이 올라갑니다.

-export function Textarea({
-  className,
-  maxLength,
-  style,
-  ...props
-}: TextareaProps) {
-  return (
-    <textarea
+export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+  ({ className, maxLength, style, ...props }, ref) => {
+    return (
+      <textarea
+        ref={ref}
         data-slot="textarea"
         maxLength={maxLength}
         className={cn(
           'h-[12rem] w-full',
           'resize-none overflow-y-auto',
           'body3-r border-gray200 bg-white-bg text-font-gray-3 rounded-[0.4rem] border px-[0.8rem] py-[1.2rem] pr-[1.4rem]',
           'focus:border-input outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
           'ds-scrollbar',
           className
         )}
         style={{ scrollbarGutter: 'stable', ...style }}
         {...props}
       />
-  );
-}
+    );
+  }
+);
packages/design-system/src/components/switch/Switch.tsx (1)

10-10: 불필요한 wrapper 제거 제안

텍스트 색상을 지정하는 wrapper div가 실제로는 의미가 없어 보이고, 상위 컨텍스트에 의도치 않은 색상 상속을 야기할 수 있습니다. Root를 최상위로 두는 편이 안전합니다.

-  return (
-    <div className="text-main600">
-      <SwitchPrimitive.Root
+  return (
+    <SwitchPrimitive.Root
         data-slot="switch"
         className={cn(
           'data-[state=checked]:bg-main400 data-[state=unchecked]:bg-gray200 focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[2rem] w-[4rem] shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
           className
         )}
         {...props}
       >
         <SwitchPrimitive.Thumb
           data-slot="switch-thumb"
           className={cn(
             'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-[1.6rem] rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[2.1rem] data-[state=unchecked]:translate-x-[0.1rem]'
           )}
         />
-      </SwitchPrimitive.Root>
-    </div>
+      </SwitchPrimitive.Root>
   );
packages/design-system/src/components/card/MyBookmarkCard.tsx (1)

50-55: 카테고리 배지 컴포넌트화 TODO 상기

재사용성을 위해 배지 컴포넌트 추출 시 디자인 토큰(색상/사이즈)을 prop으로 받는 형태로 정리 권장.

원하시면 디자인 토큰 기반 Badge 컴포넌트 초안 드리겠습니다.

packages/design-system/vite.config.ts (1)

21-26: svg 스프라이트 경로의 절대화 제안

테스트/툴 실행 디렉터리에 따라 상대 경로 해석이 달라질 수 있어 절대 경로로 고정하는 편이 안전합니다.

-    svgSpritePlugin({
-      iconDirs: ['src/icons/source'],
+    svgSpritePlugin({
+      iconDirs: [path.resolve(dirname, 'src/icons/source')],
       symbolId: 'icon-[name]',
       inject: 'body-last',
     }),
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6c73342 and c086a33.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • packages/design-system/package.json (1 hunks)
  • packages/design-system/src/components/badge/Badge.stories.tsx (1 hunks)
  • packages/design-system/src/components/button/Button.tsx (1 hunks)
  • packages/design-system/src/components/card/MyBookmarkCard.tsx (1 hunks)
  • packages/design-system/src/components/card/RemindCard.tsx (1 hunks)
  • packages/design-system/src/components/progress/Progress.tsx (1 hunks)
  • packages/design-system/src/components/switch/Switch.tsx (1 hunks)
  • packages/design-system/src/components/textarea/Textarea.tsx (1 hunks)
  • packages/design-system/src/components/toast/Toast.tsx (1 hunks)
  • packages/design-system/src/components/toast/hooks/uesFadeOut.tsx (1 hunks)
  • packages/design-system/src/components/wheelPicker/WheelPicker.tsx (1 hunks)
  • packages/design-system/tsconfig.json (1 hunks)
  • packages/design-system/vite.config.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/design-system/package.json
  • packages/design-system/src/components/wheelPicker/WheelPicker.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
📚 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/components/card/MyBookmarkCard.tsx
🔇 Additional comments (14)
packages/design-system/src/components/badge/Badge.stories.tsx (2)

3-3: 기본 import 전환 OK

스토리 파일 입장에서 Badge의 default export 사용은 일관성 있고 문제 없어 보입니다.


1-1: import 경로 유지 필요 디자인 시스템 패키지가 Storybook 9의 @storybook/react-vite@9.1.3을 사용 중이므로 수정 불필요합니다.

Likely an incorrect or invalid review comment.

packages/design-system/src/components/button/Button.tsx (1)

2-2: 상대 경로로의 전환, 이상 없습니다.

패키지 경계(DS → 앱)에서 tsconfig 경로별칭 의존도를 낮추는 목적에 부합합니다.

packages/design-system/src/components/card/RemindCard.tsx (1)

2-2: 아이콘 import 경로 수정, 일관성 OK

동일한 패턴으로 다른 컴포넌트도 정리된 점 확인했습니다.

packages/design-system/src/components/toast/Toast.tsx (1)

1-1: cn 경로 변경 문제 없습니다.

packages/design-system/src/components/toast/hooks/uesFadeOut.tsx (1)

2-2: 경로 변경 OK

packages/design-system/src/components/textarea/Textarea.tsx (1)

2-2: 상대 경로 변경 일관성 OK

다른 DS 컴포넌트들과 동일한 경로 규칙으로 맞춰졌습니다. 빌드 설정(tsconfigPaths)과도 일치합니다.

packages/design-system/src/components/switch/Switch.tsx (2)

2-3: import 순서/경로 정리 좋습니다

경로 alias 정리와 함께 React import 이동이 문제없습니다.


11-26: 접근성 라벨 확인 요청

컴포넌트 자체에 시각적 라벨이 없으므로 소비 측에서 aria-label/aria-labelledby를 반드시 제공하는지 확인 부탁드립니다.

packages/design-system/src/components/card/MyBookmarkCard.tsx (1)

2-2: Icon 경로 정리 OK

alias 제거 정책과 일관됩니다.

packages/design-system/src/components/progress/Progress.tsx (2)

3-4: 경로 정리 및 import 순서 OK

동일 규칙 적용 확인했습니다.


47-58: 접근성 라벨 제공 여부 확인

Radix Progress는 value/max는 처리하지만 라벨은 소비 측 책임입니다. 사용처에서 aria-label 또는 aria-labelledby를 제공하는지 점검 부탁드립니다. 예)

packages/design-system/vite.config.ts (2)

20-21: tsconfigPaths 도입 적절

수동 alias 제거와 함께 경로 해석 일관성이 좋아집니다.


10-14: ESM 환경에서 dirname 계산 방식 적절

fileURLToPath fallback으로 CI/Node 버전 차이에도 안전합니다.

Comment on lines +9 to 10
"@/*": ["./src/*"]
},
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

경로별칭 축소(@lib 제거) 일관성 확인 필요

@/는 유지되지만, 본 PR에서는 상대경로로 전환 중입니다. 잔존 @lib/, '@/lib/*' 참조가 없는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# design-system 내 잔존 별칭 검색
rg -nP "@lib/|@/lib/" packages/design-system -g '!**/dist/**' -S
# 앱 레벨에서도 혹시 참조 중인지 확인
rg -nP "@lib/|@/lib/" apps -g '!**/dist/**' -S

Length of output: 185


@lib 별칭 제거 후 경로 일관성 확보

packages/design-system/components.json (15행)에 "@/lib/utils" 참조가 남아 있으니 상대경로 또는 @/* 별칭으로 교체해주세요.

🤖 Prompt for AI Agents
packages/design-system/tsconfig.json around lines 9-10: components.json still
references "@/lib/utils" after removing any @lib alias; open
packages/design-system/components.json (line ~15) and replace the "@/lib/utils"
import with a consistent relative path (e.g., "./src/lib/utils" or
"../src/lib/utils" as appropriate to that file's location) so paths are
consistent with the current "@/*": ["./src/*"] mapping, or alternatively add a
specific "@/lib/*": ["./src/lib/*"] entry to tsconfig.json if you prefer to keep
the "@/lib/..." alias.

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.

타임피커 어려우셨을텐데 고생하셨습니다-! 비슷한 라이브러리가 있다해도 스타일을 완전히 공개하지 않아 복잡했을텐데 잘 구현하셨네요 !!
추가적으로 저번에 있던 글자 넘김시 그림자처럼 두개로 보이던 효과 어떻게 제거하셨는지 궁금합니다

Comment on lines +20 to +23
"iconLibrary": "lucide",
"registries": {
"@ncdai": "https://chanhdai.com/r/{name}.json"
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 어떤 역할을 하나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

"lucide" 아이콘 라이브러리를 사용하고, "@ncdai"라는 스코프를 가진 컴포넌트를 특정 URL에서 가져오도록 설정하는 내용이라고 하네요!

해당 React Wheel Picker 라이브러리에서 아래와 같이 설정이 필요하다고 말해줘서 추가했어요.
image

classNames={{
...classNames,
optionItem: cn(
'text-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]',
Copy link
Collaborator

Choose a reason for hiding this comment

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

! 붙여서 스타일 지정하는게 아쉽지만 스타일이 숨김처리된 부분이 있어서 이렇게밖에 하지 못하는게 어쩔 수 없네요 다른 방법이 있나 저도 더 찾아보겠습니다-!

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

🧹 Nitpick comments (5)
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (5)

35-40: WheelPicker를 제어 컴포넌트로 사용(가능 시) — 초기값/동기화 보장

라이브러리가 value(또는 유사 prop)를 지원한다면 바인딩해 UI와 상태의 불일치를 예방하세요. 미지원이면 이 코멘트는 스킵 가능합니다.

         <WheelPicker
           options={hourOptions}
           infinite
           optionItemHeight={56}
+          value={selectedHour}
           onValueChange={(value: string) => setSelectedHour(value)}
         />
...
         <WheelPicker
           options={minuteOptions}
           infinite
           optionItemHeight={56}
+          value={selectedMinute}
           onValueChange={(value: string) => setSelectedMinute(value)}
         />
...
         <WheelPicker
           options={meridiemOptions}
           optionItemHeight={56}
+          value={selectedMeridiem}
           onValueChange={(value: string) => setSelectedMeridiem(value)}
         />

Also applies to: 44-49, 51-55


41-41: Tailwind z-index 클래스 비표준 사용 — z-[2]로 교체

기본 Tailwind 스케일에는 z-2가 없습니다. z-[2] 또는 사전에 정의된 토큰(z-10 등)으로 변경하세요.

-        <p className="body2-m z-2 mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
+        <p className="body2-m z-[2] mx-[0.8rem] flex h-[5.6rem] items-center justify-center">

35-40: 접근성 향상: 휠에는 레이블, 콜론은 장식 처리

각 휠에 목적을 설명하는 접근성 레이블을 부여하고, 콜론은 스크린리더에서 무시되도록 설정을 권장합니다.

         <WheelPicker
           options={hourOptions}
           infinite
           optionItemHeight={56}
+          aria-label="시간 선택"
           onValueChange={(value: string) => setSelectedHour(value)}
         />
-        <p className="body2-m z-2 mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
+        <p aria-hidden="true" role="presentation" className="body2-m z-[2] mx-[0.8rem] flex h-[5.6rem] items-center justify-center">
           :
         </p>
         <WheelPicker
           options={minuteOptions}
           infinite
           optionItemHeight={56}
+          aria-label="분 선택"
           onValueChange={(value: string) => setSelectedMinute(value)}
         />
...
         <WheelPicker
           options={meridiemOptions}
           optionItemHeight={56}
+          aria-label="오전/오후 선택"
           onValueChange={(value: string) => setSelectedMeridiem(value)}
         />

Also applies to: 41-43, 44-49, 51-55


20-23: 로케일 정합성: 라벨만 한국어로 노출(값은 AM/PM 유지)

한국어 UI 맥락에서 라벨은 ‘오전/오후’가 자연스럽습니다. 내부 값은 기존 AM/PM 유지로 로직 영향 최소화.

-  { label: 'AM', value: 'AM' },
-  { label: 'PM', value: 'PM' },
+  { label: '오전', value: 'AM' },
+  { label: '오후', value: 'PM' },

9-16: 네이밍 가독성: createArray → createTimeOptions

의도를 드러내는 이름이 유지보수에 유리합니다.

-const createArray = (length: number, add = 0): WheelPickerOption[] =>
+const createTimeOptions = (length: number, add = 0): WheelPickerOption[] =>
   Array.from({ length }, (_, i) => {
     const value = i + add;
     return {
       label: value.toString().padStart(2, '0'),
       value: value.toString(),
     };
   });
 
-const hourOptions = createArray(12, 1);
-const minuteOptions = createArray(60);
+const hourOptions = createTimeOptions(12, 1);
+const minuteOptions = createTimeOptions(60);

Also applies to: 18-19

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c086a33 and 2b17f59.

📒 Files selected for processing (1)
  • apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.
🪛 GitHub Check: lint
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx

[warning] 28-28:
'selectedMeridiem' is assigned a value but never used


[warning] 27-27:
'selectedMinute' is assigned a value but never used


[warning] 26-26:
'selectedHour' is assigned a value but never used

🪛 GitHub Actions: ci
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx

[error] 26-26: TypeScript error TS6133: 'selectedHour' is declared but its value is never read.

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

♻️ Duplicate comments (1)
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (1)

67-82: 이전 리뷰 피드백 반영 확인: 상태 사용 및 콜백 연결 완료

selectedHour/minute/meridiem이 실제로 소비되고, 취소/확인 버튼에 핸들러가 연결되었습니다. CI 경고(TS6133) 원인이 해소된 것으로 보입니다.

Also applies to: 31-36

🧹 Nitpick comments (5)
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (5)

39-41: 루트 컨테이너 onClick 전파로 인한 오동작 가능성

휠/버튼 클릭이 모두 상위로 버블되어 부모가 바깥 클릭으로 오인할 수 있습니다(모달 닫힘 등). 컨테이너 내부 상호작용을 보호하려면 전파 차단 또는 명시적 백드롭 핸들링으로 분리하는 것을 권장합니다.

예시:

-  <div
-    onClick={onClick}
+  <div
+    onClick={(e) => { e.stopPropagation(); onClick?.(e); }}
     className="common-shadow flex w-[26rem] flex-col items-center px-[1.6rem]"
   >

혹은 상위에서 백드롭 영역에만 onClick을 부여하고, 이 컴포넌트는 onClick을 제거하세요.


45-49: WheelPicker를 “controlled”로 유지하여 상태와 UI 동기화 보장

현재 선택값 state는 저장(확인) 시에만 쓰이고, 휠의 초기/외부 변경과 동기화를 보장하지 않습니다. 라이브러리/래퍼가 value(또는 동등 prop)를 지원한다면 전달하여 단일 소스 유지가 좋습니다.

예시(래퍼가 value를 지원한다고 가정):

 <WheelPicker
   options={hourOptions}
   infinite
   optionItemHeight={56}
+  value={selectedHour}
   onValueChange={(value: string) => setSelectedHour(value)}
 />
 ...
 <WheelPicker
   options={minuteOptions}
   infinite
   optionItemHeight={56}
+  value={selectedMinute}
   onValueChange={(value: string) => setSelectedMinute(value)}
 />
 ...
 <WheelPicker
   options={meridiemOptions}
   optionItemHeight={56}
+  value={selectedMeridiem}
   onValueChange={(value: string) => setSelectedMeridiem(value)}
 />

지원 prop 명이 상이하면 래퍼 정의에 맞춰 조정 바랍니다.

Also applies to: 54-58, 61-64


50-52: 타이포그래피/레이어 유틸 검증 필요

body2-m, z-2가 Tailwind 설정에 존재하는지 확인이 필요합니다. 기본 스케일에는 z-2가 없습니다. 미정의일 경우 적용되지 않아 시각적 이슈가 생길 수 있습니다.

대안: 디자인 토큰 기반 유틸 또는 z-10 등 정의된 값 사용.


9-16: 값 포맷 일관화(라벨=값)로 후처리 혼란 최소화

라벨은 2자리('01', '00')인데 값은 비패딩('1', '0')이라 포맷팅 시 혼선이 생길 수 있습니다. 저장/표시 일관성을 위해 값도 패딩하거나, 반대로 값은 number로 보관하는 방식을 권장합니다.

패딩 일치 예:

-  Array.from({ length }, (_, i) => {
-    const value = i + add;
-    return {
-      label: value.toString().padStart(2, '0'),
-      value: value.toString(),
-    };
-  });
+  Array.from({ length }, (_, i) => {
+    const n = i + add;
+    const s = n.toString().padStart(2, '0');
+    return { label: s, value: s };
+  });

18-23: 타입 명시 강화(AM/PM 리터럴, 콜백 시그니처 구체화)

meridiem을 문자열 리터럴 합집합으로 제한하면 오타/외부 입력 오류를 줄일 수 있습니다.

예시:

+type Meridiem = 'AM' | 'PM';
 const meridiemOptions: WheelPickerOption[] = [
   { label: 'AM', value: 'AM' },
   { label: 'PM', value: 'PM' },
 ];
 
 interface TimePickerProps {
-  onSave: (time: { hour: string; minute: string; meridiem: string }) => void;
+  onSave: (time: { hour: string; minute: string; meridiem: Meridiem }) => void;

Also applies to: 25-29

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2b17f59 and a379f82.

📒 Files selected for processing (1)
  • apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#2
File: pnpm-workspace.yaml:3-3
Timestamp: 2025-08-18T13:48:59.065Z
Learning: constantly-dev는 docs 디렉터리를 컨벤션 문서 추가용으로 사용할 예정이라고 명시했습니다.

Comment on lines 25 to 29
interface TimePickerProps {
onSave: (time: { hour: string; minute: string; meridiem: string }) => void;
onCancel: () => void;
onClick: React.MouseEventHandler<HTMLDivElement>;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

TS2503 가능성: React 네임스페이스 타입 사용으로 컴파일 실패 위험

onClick: React.MouseEventHandlerReact 네임스페이스를 import하지 않아 TS가 실패할 수 있습니다. 타입 전용 import로 네임스페이스 의존을 제거하세요. 또한 외부에서 반드시 전달되지 않아도 되는 prop이면 optional로 두는 편이 안전합니다.

적용 diff:

-import { useState } from 'react';
+import { useState, type MouseEventHandler } from 'react';

 interface TimePickerProps {
   onSave: (time: { hour: string; minute: string; meridiem: string }) => void;
   onCancel: () => void;
-  onClick: React.MouseEventHandler<HTMLDivElement>;
+  onClick?: MouseEventHandler<HTMLDivElement>;
 }

Also applies to: 7-7

🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx around
lines 7 and 25-29, the prop type uses the React namespace
(React.MouseEventHandler) which can cause TS2503 if React isn't imported as a
namespace; change it to a type-only import and make the prop optional if it may
not be provided: add an import type for MouseEventHandler from 'react' at the
top, replace React.MouseEventHandler<HTMLDivElement> with
MouseEventHandler<HTMLDivElement> and mark onClick as optional (onClick?: ...).

@constantly-dev
Copy link
Member Author

타임피커 어려우셨을텐데 고생하셨습니다-! 비슷한 라이브러리가 있다해도 스타일을 완전히 공개하지 않아 복잡했을텐데 잘 구현하셨네요 !! 추가적으로 저번에 있던 글자 넘김시 그림자처럼 두개로 보이던 효과 어떻게 제거하셨는지 궁금합니다

저도 왜 두 개의 Text가 겹쳐 보일까 내부 구현을 까보니까,

  1. 처음에 준 option 리스트를 전부 보여주는 세로 축
  2. 중간에 highlight를 보여주는 축

이 두 개가 있더라고요.

처음에는 highlight 부분은 저 첫 번째 리스트에서 highlight 영역에 들어오는 숫자만 보여지는 공간인 줄 알았는데 그게 아니라 둘 다 options로 넘겨준 리스트가 세로 축으로 돌아가는 구조더라고요. 대신 highlightWrapper라는 부분에는 2번 축 options들 중 그 영역에 들어온 부분을 보여주는 곳이고 나머지 부분은 hide해서 2번 축의 나머지 option 리스트는 안 보여주는 구조에요!

그래서 요약하자면 다른 곳은 괜찮은데 highlightWrapper라고 하는 중간 부분에 2개의 리스트들이 겹쳐지면서 생기는 문제였어요!
따라서 이 부분이 style이 transparent(투명)한 상태여서 이 부분 bg를 white 처리하는 방식으로 해결했습니다 👍

highlightWrapper: cn(
    'bg-white border-y-[0.5px] border-gray300 !font-medium !text-[1.6rem] !leading-[150%] !tracking-[-0.025em]',
    classNames?.highlightWrapper
),
image

@constantly-dev constantly-dev merged commit deaa027 into develop Sep 3, 2025
6 checks passed
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.

[Feat] Timepicker 컴포넌트 구현

2 participants