Conversation
Walkthrough온보딩 퍼널 및 단계별 컴포넌트(헤더, MainCard, Story/Alarm/Mac/Final/AlarmBox/TimePicker)와 관련 상수(stories, alarms), framer-motion 의존성, 디자인시스템의 Progress 재수출 변화, 아이콘 추가, 경로 별칭 및 Vite 설정이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as 사용자
participant OB as OnBoarding 페이지
participant MC as MainCard
participant S as StoryStep (0–2)
participant A as AlarmStep / AlarmBox (3)
participant TP as TimePicker
participant M as MacStep (4)
participant F as FinalStep (4/5)
U->>OB: 온보딩 진입
OB->>MC: 초기 렌더 (step=0)
MC->>S: step 0–2 내용 렌더(스토리, progress)
U-->>MC: 다음/이전 클릭 (direction 변경)
MC->>A: step=3 렌더(선택 상태 전달)
A->>A: AlarmBox들 렌더 및 클릭 수신
alt select === 3 && showPicker
A->>TP: TimePicker 오픈
U-->>TP: 시간 저장/취소
TP-->>A: AlarmsType[2].time 직접 업데이트
end
MC->>M: step=4에서 isMac ? MacStep : FinalStep
MC->>F: step=5 또는 비-Mac의 step=4 -> FinalStep
U-->>MC: 완료 -> 라우트 '/'로 리다이렉트
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Pre-merge checks (5 passed)✅ Passed checks (5 passed)
Poem
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Storybook chromatic 배포 확인: |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (13)
apps/client/package.json (1)
13-14: framer-motion v12 React 18/19 호환성 확인 완료 – 번들 크기 영향만 점검 필요
framer-motion v12.23.12는 React ^18.0.0 || ^19.0.0을 지원해 프로젝트의 React v19.1.x와 호환되며, class-variance-authority 0.7.1은 별도 peerDependencies가 없습니다. 번들 크기 증가는 webpack-bundle-analyzer나 Vite bundle analyzer 등을 활용해 확인해 주세요.apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (3)
12-16: 데이터 테이블 타입 안정성 보강(선택사항)
AlarmsType에 명시적 타입을 부여하면 실수(속성 누락/오타)를 컴파일 타임에 잡을 수 있습니다.-const AlarmsType = [ +type AlarmItem = { img: string; title: string; time?: string }; +const AlarmsType = [ { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, { img: avatar3, title: '사용자 설정' }, -]; +] satisfies ReadonlyArray<AlarmItem>;
35-39: 클래스 결합 가독성 개선(선택사항)template literal 대신
clsx/cn유틸을 쓰면 조건부 클래스가 더 읽기 쉽습니다.- <p - className={`sub3-sb ${ - isDisabled ? 'text-main500' : 'text-font-black-1' - }`} - > + <p className={isDisabled ? 'sub3-sb text-main500' : 'sub3-sb text-font-black-1'}>
31-49: 라디오 패턴 고려(선택사항)세 가지 중 하나를 선택하는 패턴이라면 상위 컨테이너를
role="radiogroup", 각 항목을role="radio"+aria-checked로 모델링하면 접근성이 더 명확합니다. 필요 시AlarmStep쪽 리팩터 지원 가능합니다.apps/client/src/pages/onBoarding/components/header/Header.tsx (1)
6-6: SPA에서 window.location.reload()는 지양온보딩 상태 초기화가 목적이라면 라우터 네비게이션(예: 홈으로 이동) 또는 상위 상태 리셋 콜백을 받는 방식이 더 낫습니다. 새로고침은 번들 재로딩과 상태 손실을 유발합니다.
apps/client/src/pages/onBoarding/OnBoarding.tsx (1)
7-7: 불필요한 템플릿 리터럴 제거문자열 보간이 없어 단순 문자열로 간소화 가능합니다.
- className={`relative flex h-screen w-screen items-center justify-center bg-cover bg-center bg-no-repeat`} + className="relative flex h-screen w-screen items-center justify-center bg-cover bg-center bg-no-repeat"apps/client/src/pages/onBoarding/components/funnel/AlarmStep.tsx (3)
6-6: 리터럴 타입 배열을 도입해 불필요한 타입 캐스팅 제거
as 1 | 2 | 3캐스팅을 없애고as const배열을 사용하세요.const [selected, setSelected] = useState<1 | 2 | 3>(1); // 기본값은 1번 선택 + const OPTIONS = [1, 2, 3] as const; @@ - {[1, 2, 3].map((n) => ( + {OPTIONS.map((n) => ( <AlarmBox key={n} - select={n as 1 | 2 | 3} - isDisabled={selected === n} - onClick={() => setSelected(n as 1 | 2 | 3)} + select={n} + isDisabled={selected === n} + onClick={() => setSelected(n)} /> ))}Also applies to: 19-26
10-10: 이미지 대체 텍스트를 더 구체적으로스크린리더 친화적으로 alt를 구체화하세요.
- <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> + <img src={dotori} className="mb-[1.2rem]" alt="도토리 아이콘" />
18-27: 단일 선택 UI의 접근성 롤 확인(라디오 그룹/토글 버튼)이 영역은 단일 선택입니다. 컨테이너에
role="radiogroup"을 부여하고, 각AlarmBox가<button aria-pressed>또는<input type="radio">로 구현됐는지 확인 바랍니다. 현재는 선택 항목을 비활성화로 표시하고 있는데, 명확한 선택 상태 표기가 더 적절합니다.apps/client/src/pages/onBoarding/components/funnel/StoryStep.tsx (2)
7-40: progress 값 중복 정의 제거여기
steps[*].progress는 이 파일 내에서 사용되지 않고,MainCard의stepProgress와 값이 중복입니다. 한 곳에서만 관리하도록 정리해 드리프트를 방지하세요(예: MainCard에만 두고 여기선 제거).
45-49: 이미지 alt를 단계에 맞게 동적으로보다 설명적인 대체 텍스트로 변경 권장.
- <img - src={steps[step].img} - className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" - alt="onboarding" - /> + <img + src={steps[step].img} + className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" + alt={`온보딩 스텝 ${step + 1} 이미지`} + />apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
22-24: state 타입을 리터럴 유니온으로 좁혀 런타임 가드를 단순화정적 타이핑을 명확히 하면 범위 체크 로직이 더 안전해집니다.
- const [step, setStep] = useState(0); - const [direction, setDirection] = useState(0); + const [step, setStep] = useState<0 | 1 | 2 | 3>(0); + const [direction, setDirection] = useState<-1 | 0 | 1>(0);
7-7: progress 소스 단일화 제안
stepProgress와StoryStep의steps[*].progress가 중복입니다. 한 곳으로 통합해 상수 한 군데만 업데이트하면 되도록 하세요.
📜 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.
⛔ Files ignored due to path filters (12)
apps/client/src/assets/onBoarding/background/onBoardingBg.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/icons/chippi_bell.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/icons/chippi_morning.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/icons/chippi_night.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/icons/dotori.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/icons/header_logo.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/story/macNotice.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/story/story1.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/story/story2.svgis excluded by!**/*.svgapps/client/src/assets/onBoarding/story/story3.svgis excluded by!**/*.svgpackages/design-system/src/icons/source/main_header_logo.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
apps/client/package.json(1 hunks)apps/client/src/pages/onBoarding/OnBoarding.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/AlarmStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/StoryStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/header/Header.tsx(1 hunks)packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/components/progress/Progress.tsx(2 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
🔇 Additional comments (6)
packages/design-system/src/components/progress/Progress.tsx (3)
41-55: A11y: 접근 가능한 이름 제공 가이드 명시 필요Radix가
role="progressbar"및aria-valuenow등을 처리하지만, 접근 가능한 이름(aria-label/aria-labelledby)은 소비자가 반드시 제공해야 합니다. 스토리북 문서/예제에aria-label사용을 명시해 주세요.
39-39: 값 클램핑 로직 LGTM0–100 범위 강제는 요구사항과 일치하며 방어적입니다.
38-56: 경로 직접progress/Progress임포트 사용처 없음 — 빌드 영향 없음packages/design-system/src/icons/iconNames.ts (2)
14-15: iconNames.ts 자동 생성 파일 확인 후 스크립트 재생성
src/icons에 main_header_logo.svg 파일이 추가됐는지 확인하고,npm run generate:icons(또는yarn generate:icons)를 실행해iconNames.ts가 자동으로 업데이트되는지 검증하세요. 수동 편집을 제거하고 SVG 추가 후 스크립트로 관리해야 합니다.
14-15: 스프라이트 번들 등록 확인
main_header_logo.svg가 packages/design-system/src/icons/source에 존재하며 generate-icon-list 스크립트로 iconNames.ts에 정상 반영됨을 확인했습니다. 해당 SVG가 빌드된 스프라이트 또는 아이콘 컴포넌트에 제대로 등록되어 실제로 렌더링되는지 검증해주세요.apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
1-1: { Progress } 네임드 임포트가 유효하며 변경 필요 없음
| const boxStyle = cva( | ||
| 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition', | ||
| { | ||
| variants: { | ||
| disabled: { | ||
| true: 'border-main400 bg-main100 border', | ||
| false: 'bg-white border border-transparent hover:border-main300', | ||
| }, | ||
| }, | ||
| defaultVariants: { disabled: false }, | ||
| } | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
disabled 변형에서도 커서/클릭이 유지됨
베이스 클래스에 cursor-pointer가 있어 disabled 상태에서도 포인터 커서가 유지됩니다. 비활성화 시 커서/포인터 이벤트를 비활성화하세요.
-const boxStyle = cva(
- 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition',
+const boxStyle = cva(
+ 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] transition',
{
variants: {
disabled: {
- true: 'border-main400 bg-main100 border',
- false: 'bg-white border border-transparent hover:border-main300',
+ true: 'border-main400 bg-main100 border cursor-not-allowed pointer-events-none',
+ false: 'bg-white border border-transparent hover:border-main300 cursor-pointer',
},
},
defaultVariants: { disabled: false },
}
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const boxStyle = cva( | |
| 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition', | |
| { | |
| variants: { | |
| disabled: { | |
| true: 'border-main400 bg-main100 border', | |
| false: 'bg-white border border-transparent hover:border-main300', | |
| }, | |
| }, | |
| defaultVariants: { disabled: false }, | |
| } | |
| ); | |
| const boxStyle = cva( | |
| 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] transition', | |
| { | |
| variants: { | |
| disabled: { | |
| true: 'border-main400 bg-main100 border cursor-not-allowed pointer-events-none', | |
| false: 'bg-white border border-transparent hover:border-main300 cursor-pointer', | |
| }, | |
| }, | |
| defaultVariants: { disabled: false }, | |
| } | |
| ); |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx around lines
18 to 29, the base class includes cursor-pointer so the pointer cursor and
clickability remain when the component is disabled; update the cva variants so
cursor-pointer is applied only for the non-disabled variant and add disabled
styles like cursor-default and pointer-events-none to the disabled:true variant
(and remove hover styles for disabled) so the element looks and behaves
non-interactive when disabled.
| const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => { | ||
| return ( | ||
| <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}> | ||
| <img src={AlarmsType[select - 1].img} alt="chippi" /> | ||
| <p | ||
| className={`sub3-sb ${ | ||
| isDisabled ? 'text-main500' : 'text-font-black-1' | ||
| }`} | ||
| > | ||
| {AlarmsType[select - 1].title} | ||
| </p> | ||
| {select <= 2 && ( | ||
| <p className="caption2-m text-font-gray-3"> | ||
| {AlarmsType[select - 1].time} | ||
| </p> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
클릭 가능한 div → button으로 전환하여 키보드/스크린리더 접근성 보장
현재 div+onClick은 키보드 포커스/Space/Enter 동작, aria-disabled 처리 등이 부재합니다. 네이티브 button으로 전환하고 disabled를 활용하세요.
아래 diff 적용 제안:
-const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
+const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
return (
- <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}>
- <img src={AlarmsType[select - 1].img} alt="chippi" />
+ <button
+ type="button"
+ className={boxStyle({ disabled: isDisabled })}
+ onClick={onClick}
+ disabled={isDisabled}
+ >
+ <img src={AlarmsType[select - 1].img} alt={AlarmsType[select - 1].title} loading="lazy" />
<p
className={`sub3-sb ${
isDisabled ? 'text-main500' : 'text-font-black-1'
}`}
>
{AlarmsType[select - 1].title}
</p>
{select <= 2 && (
<p className="caption2-m text-font-gray-3">
{AlarmsType[select - 1].time}
</p>
)}
- </div>
+ </button>
);
};| <div | ||
| className="z-5 absolute top-0 flex w-full justify-items-start px-[8rem] py-[2.5rem]" | ||
| onClick={() => window.location.reload()} | ||
| > | ||
| <img src={HeaderLogo} alt="header logo" /> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
클릭 가능한 div → button으로 교체하고 무효한 Tailwind 클래스를 수정하세요
- 접근성: div+onClick은 키보드 포커스/엔터가 안 됩니다. button으로 바꾸고 aria-label을 부여하세요.
- 스타일:
justify-items-*는 flex 컨테이너에서 효과가 없고,z-5는 기본 Tailwind 스케일에 없습니다.justify-start,z-[5]로 교체 권장.
적용 예시:
- <div
- className="z-5 absolute top-0 flex w-full justify-items-start px-[8rem] py-[2.5rem]"
- onClick={() => window.location.reload()}
- >
+ <button
+ type="button"
+ aria-label="홈으로 이동"
+ className="z-[5] absolute top-0 flex w-full justify-start px-[8rem] py-[2.5rem]"
+ onClick={() => window.location.reload()}
+ >
<img src={HeaderLogo} alt="header logo" />
- </div>
+ </button>📝 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.
| <div | |
| className="z-5 absolute top-0 flex w-full justify-items-start px-[8rem] py-[2.5rem]" | |
| onClick={() => window.location.reload()} | |
| > | |
| <img src={HeaderLogo} alt="header logo" /> | |
| </div> | |
| <button | |
| type="button" | |
| aria-label="홈으로 이동" | |
| className="z-[5] absolute top-0 flex w-full justify-start px-[8rem] py-[2.5rem]" | |
| onClick={() => window.location.reload()} | |
| > | |
| <img src={HeaderLogo} alt="header logo" /> | |
| </button> |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/header/Header.tsx around lines
4-9, replace the clickable div with a semantic <button type="button"> to restore
keyboard activation and add an aria-label (e.g., "Reload page"); preserve the
onClick handler (window.location.reload()). Update the Tailwind classes: change
justify-items-start to justify-start and z-5 to z-[5], and ensure button styles
don't introduce unwanted defaults (use inline-flex/px/py and
transparent/background-none/border-none as needed) so the layout and image
remain unchanged while improving accessibility.
| export { default as Input } from './input/Input'; | ||
| export { default as Level } from './level/Level'; | ||
| export { Progress } from './progress/Progress'; | ||
| export { default as Progress } from './progress/Progress'; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
ProgressProps 타입 재노출 누락으로 인한 잠재적 BP
이전 소비자가 import type { ProgressProps } from '@pinback/design-system'를 사용했다면 깨질 수 있습니다. 타입도 함께 재노출해 주세요.
export { default as Progress } from './progress/Progress';
+export type { ProgressProps } from './progress/Progress';🏁 Script executed:
#!/bin/bash
# 레포 전역에서 ProgressProps 직접 임포트 사용 여부 확인
rg -nP '\bProgressProps\b' -SLength of output: 264
🏁 Script executed:
#!/bin/bash
rg -n "export type" -C2 packages/design-system/src/components/index.ts || trueLength of output: 276
ProgressProps 타입 재노출 추가
Progress 소비자가 import type { ProgressProps } from '@pinback/design-system'를 그대로 유지할 수 있도록, 컴포넌트와 함께 타입도 재노출하세요.
export { default as Progress } from './progress/Progress';
+export type { ProgressProps } from './progress/Progress';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export { default as Progress } from './progress/Progress'; | |
| export { default as Progress } from './progress/Progress'; | |
| export type { ProgressProps } from './progress/Progress'; |
🤖 Prompt for AI Agents
In packages/design-system/src/components/index.ts around line 7, the Progress
component is exported but its Props type is not re-exported; add a re-export of
the ProgressProps type from './progress/Progress' so consumers can continue to
do `import type { ProgressProps } from '@pinback/design-system'`. Update the
barrel export to export the type alongside the component export (i.e., re-export
the ProgressProps type from the same module).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
packages/design-system/src/components/progress/Progress.stories.tsx (2)
130-137: 테스트 쿼리를 role 기반으로 변경해 내부 구현 결합도 축소
data-slot에 의존하지 말고 WAI-ARIA role을 사용하면 컴포넌트 내부 마크업 변경에도 테스트가 안정적입니다.- const roots = canvasElement.querySelectorAll('[data-slot="progress"]'); - roots.forEach((el) => expect(el.getAttribute('aria-valuenow')).toBe('40')); + const bars = await canvas.findAllByRole('progressbar'); + bars.forEach((el) => expect(el).toHaveAttribute('aria-valuenow', '40')); + bars.forEach((el) => { + expect(el).toHaveAttribute('aria-valuemin', '0'); + expect(el).toHaveAttribute('aria-valuemax', '100'); + });
6-39: Storybook 메타 타이핑을satisfies패턴으로 개선(선택)TS 4.9+ 환경이라면
satisfies를 쓰면 과잉 속성 체크가 강화되어 스토리 구성 오류를 줄일 수 있습니다.-const meta: Meta<typeof Progress> = { +const meta = { title: 'Components/Progress', component: Progress, tags: ['autodocs'], parameters: { layout: 'centered', docs: { description: { component: '단일 로직을 공유하고 **variant**로 스타일만 분기하는 progress입니다.\n' + '- `variant="profile"`: 얇은 트랙 + 단색 인디케이터 (기본)\n' + '- `variant="tree"`: 두꺼운 트랙 + 그라데이션 인디케이터\n', }, }, }, argTypes: { className: { table: { disable: true } }, asChild: { table: { disable: true } }, ref: { table: { disable: true } }, value: { control: { type: 'range', min: 0, max: 100, step: 1 }, description: '진행 퍼센트(0–100)', }, variant: { control: { type: 'radio' }, options: ['profile', 'tree'], description: '스타일 분기', }, }, args: { value: 40, variant: 'profile', }, -}; +} satisfies Meta<typeof Progress>;
📜 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.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/components/progress/Progress.stories.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/design-system/src/components/index.ts
🔇 Additional comments (3)
packages/design-system/src/components/progress/Progress.stories.tsx (3)
4-4: default import 전환 적절Progress가 default export로 바뀐 변경과 정확히 일치합니다. Storybook 제네릭(
Meta<typeof Progress>,StoryObj<typeof Progress>)과의 정합도 유지됩니다.
4-4: 로컬 경로에서Progressnamed import 없음 확인됨
검증 스크립트 실행 결과import { Progress } from './progress/Progress'형태가 전역에 남아 있지 않아 추가 조치 불필요합니다.
1-1: 스토리북 타입 임포트 경로는@storybook/react-vite가 맞습니다
Vite 빌더를 사용하는 SB9 프로젝트에서는@storybook/react-vite에서Meta·StoryObj를 가져와야 합니다. (storybook.js.org)Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (9)
apps/client/src/pages/onBoarding/components/funnel/MacStep.tsx (3)
5-14: absolute 배치의 기준 컨테이너가 외부에 의존 — 컴포넌트 캡슐화 위해 relative 추가 권장현재 absolute 요소가 상위(MainCard) 컨테이너의
relative에 의존합니다. 재사용성/격리를 위해 이 컴포넌트의 루트에relative를 부여하는 편이 안전합니다.- <div className="flex h-full flex-col items-center"> + <div className="relative flex h-full flex-col items-center">
6-6: 대체 텍스트(alt) 구체화스크린 리더 친화적으로 더 구체적인 한국어 alt 텍스트를 권장합니다.
- <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> + <img src={dotori} className="mb-[1.2rem]" alt="도토리 아이콘" /> @@ - <img src={MacNotice} className="absolute -bottom-[104px]" alt="mac" /> + <img src={MacNotice} className="absolute -bottom-[104px]" alt="Mac 알림 설정 안내 이미지" />Also applies to: 13-13
7-9: 시맨틱 마크업 제안: 제목은 h2 사용디자인 클래스는 유지하되, 의미론적으로 제목은
<h2>가 적합합니다.- <p className="head2 text-font-black-1"> + <h2 className="head2 text-font-black-1"> 도토리 찾으러 갈 시간을 정해볼까요? - </p> + </h2>apps/client/src/pages/onBoarding/components/funnel/FinalStep.tsx (2)
5-5: 대체 텍스트(alt) 구체화접근성 향상을 위해 alt를 한국어로 구체화하세요.
- <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> + <img src={dotori} className="mb-[1.2rem]" alt="도토리 아이콘" />
6-9: 시맨틱 마크업 제안: 제목은 h2 사용- <p className="head2 text-font-black-1"> + <h2 className="head2 text-font-black-1"> 도토리 찾으러 갈 시간을 정해볼까요? - </p> + </h2>apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (4)
2-2: Mac 전용 스텝(4단계) 비Mac 사용자에게는 스킵 권장현재 MacStep이 모든 사용자에게 노출됩니다. 비Mac 환경에서는 UX 혼란이 있어 스킵하는 흐름이 깔끔합니다. 최소 변경으로 조건부 렌더 + 이동 로직만 조정하는 패치를 제안합니다.
- import { useState } from 'react'; + import { useEffect, useState } from 'react'; @@ const MainCard = () => { const [step, setStep] = useState(0); const [direction, setDirection] = useState(0); + const [isMac, setIsMac] = useState(false); + + useEffect(() => { + if (typeof window !== 'undefined') { + const ua = window.navigator.userAgent || ''; + const plat = (window.navigator.platform || ''); + const macLike = /Mac|Macintosh|MacIntel|MacPPC|Mac68K/i.test(plat) || /Mac OS X/i.test(ua); + setIsMac(macLike); + } + }, []); @@ - case 4: - return <MacStep />; + case 4: + return isMac ? <MacStep /> : <FinalStep />; @@ - const nextStep = () => { - if (step < 5) { - setDirection(1); - setStep((prev) => prev + 1); - } - if (step === 5) { - window.location.href = '/'; - } - }; + const nextStep = () => { + if (step < 5) { + setDirection(1); + setStep((prev) => { + const next = prev + 1; + // 비Mac이면 4단계를 건너뛰고 파이널로 이동 + if (next === 4 && !isMac) return 5; + return next; + }); + } else { + window.location.href = '/'; + } + }; @@ - const prevStep = () => { - if (step > 0) { - setDirection(-1); - setStep((prev) => prev - 1); - } - }; + const prevStep = () => { + if (step > 0) { + setDirection(-1); + setStep((prev) => { + const p = prev - 1; + // 비Mac이면 4단계로 되돌아가지 않음 + if (p === 4 && !isMac) return 3; + return p; + }); + } + };보완 포인트:
- 현재 파이널(5단계)에서 이전 버튼이 숨겨져 있어(아래 코멘트 참조) 비Mac 사용자는 사실상 3→5로 직행 후 되돌리기 어려움. 정책에 따라 파이널에서도 이전 노출을 고려해 주세요.
Also applies to: 22-25, 33-36, 41-49, 51-56
100-103: Next 버튼 비활성화 조건이 항상 false — 제거 권장
step이 6이 되는 경우가 없어 의미 없는 prop입니다.- isDisabled={step === 6}
46-48: SPA 라우팅은 풀 리로드 대신 라우터 네비게이션 사용 권장
window.location.href = '/'대신react-router-dom의useNavigate를 쓰면 상태 유지/성능 면에서 유리합니다.예시:
import { useNavigate } from 'react-router-dom'; const navigate = useNavigate(); // ... if (step === 5) navigate('/');
74-76: 최초 마운트 시 슬라이드 인 방지 옵션처음 진입 시에도
initial="enter"로 애니메이션이 실행됩니다. 초기 진입은 고정 화면을 원하면initial={false}고려하세요.- initial="enter" + initial={false}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/client/src/layout/Layout.tsx(0 hunks)apps/client/src/pages/onBoarding/components/funnel/FinalStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MacStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)
💤 Files with no reviewable changes (1)
- apps/client/src/layout/Layout.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 (3)
apps/client/src/pages/onBoarding/components/funnel/FinalStep.tsx (1)
6-8: 파이널 스텝 카피가 MacStep과 동일 — 의도 확인 필요최종 단계 메시지가 MacStep의 헤더 문구와 동일합니다. 완료/다음 단계 유도(예: “시작하기”, “온보딩 완료”)가 의도였다면 카피를 구분하는 편이 좋습니다.
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
1-1: Design System의 Progress export 방식 확인 필요PR 요약에 따르면 Progress가 default export로 변경되었습니다. 현재는
{ Progress, Button }형태로 임포트하고 있어 빌드가 깨질 수 있습니다. 만약 default export가 맞다면 아래 형태로 조정해 주세요.- import { Progress, Button } from '@pinback/design-system/ui'; + import Progress, { Button } from '@pinback/design-system/ui';
86-96: 파이널(5단계)에서 이전 버튼 미노출 — 의도 확인 필요
step < 4조건으로 4,5단계에서 이전 버튼이 숨겨집니다. 파이널에서도 뒤로가기를 허용할지 정책 확인 요청드립니다(특히 비Mac에서 3→5로 스킵되는 경우).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (5)
47-51: 콘솔 로그 제거 또는 로거로 대체 (lint 경고)ESLint가 Unexpected console statement로 경고합니다. 배포 시 잔류하지 않도록 제거하거나 로거/분석 이벤트로 대체하세요.
const nextStep = () => { - if (step === 3) { - console.log('선택된 알람:', AlarmsType[alarmSelected - 1].time); - } + // TODO: 알람 선택값은 추후 상태/분석 이벤트로 처리
55-56: SPA 네비게이션은 window.location.href 대신 라우터 사용 권장전체 리로드 없이 이동하도록 라우터의 navigate 사용을 권장합니다.
- window.location.href = '/'; + navigate('/');컴포넌트 외곽 변경(예시):
import { useNavigate } from 'react-router-dom'; // ... const navigate = useNavigate();
105-114: 다음 버튼 isDisabled 조건(step === 6)은 도달 불가step 최대값이 5이므로 항상 false입니다. 혼동 방지를 위해 제거를 권장합니다.
- isDisabled={step === 6}
25-28: step을 리터럴 유니온으로 제한해 타입 안전성 강화컴파일 타임에 단계 범위를 보장하면 분기 누락을 줄일 수 있습니다.
- const [step, setStep] = useState(0); + type Step = 0 | 1 | 2 | 3 | 4 | 5; + const [step, setStep] = useState<Step>(0);renderStep에서 총괄 체크를 추가하면 더 안전합니다(참고):
default: { const _: never = step; return null; }
12-22: 접근성: 사용자 ‘감소된 모션’ 선호 존중prefers-reduced-motion을 고려해 전환을 끄거나 duration을 0으로 줄이는 것을 권장합니다.
import { useReducedMotion } from 'framer-motion'; // ... const reduce = useReducedMotion(); // ... transition={{ duration: reduce ? 0 : 0.4 }}Also applies to: 76-85
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/AlarmStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/client/src/pages/onBoarding/components/funnel/AlarmStep.tsx
- apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (1)
AlarmsType(12-16)
🪛 GitHub Check: lint
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
[warning] 49-49:
Unexpected console statement
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: storybook
🔇 Additional comments (2)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
93-104: 이전 버튼 노출 조건 검토 (step 4에서도 숨김)현재 step 4(MacStep)부터 이전 버튼이 사라집니다. PR 목표(이전/다음으로 단계 이동) 기준으론 step > 0에서 항상 노출이 자연스럽습니다. 의도된 UX인지 확인 부탁드립니다. 의도와 다르면 아래처럼 수정하세요.
- {step < 4 && ( + {step > 0 && (
68-74: Progress 값/variant 사용 적합0–100 범위 값과 variant="profile" 사용이 요구사항에 부합합니다.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (2)
10-21: disabled 변형에서도 커서/클릭 유지 — 변형별로 분리 필요 (이전 코멘트 중복)베이스 클래스의
cursor-pointer로 인해 비활성화 시에도 포인터/클릭이 유지됩니다. 변형으로 분리하세요. 또한 TimePicker 포지셔닝을 위해 컨테이너에relative추가 권장.-const boxStyle = cva( - 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition', +const boxStyle = cva( + 'relative flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] transition', { variants: { disabled: { - true: 'border-main400 bg-main100 border', - false: 'bg-white border border-transparent hover:border-main300', + true: 'border-main400 bg-main100 border cursor-not-allowed pointer-events-none', + false: 'bg-white border border-transparent hover:border-main300 cursor-pointer', }, }, defaultVariants: { disabled: false }, } );
25-25: 클릭 가능한 div → button으로 전환 (접근성) — (이전 코멘트 중복)키보드 포커스/엔터/스페이스/aria-disabled 처리가 자동 지원됩니다.
- <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}> + <button + type="button" + className={boxStyle({ disabled: isDisabled })} + onClick={onClick} + disabled={isDisabled} + > ... - </div> + </button>또한 이미지 대체 텍스트를 동적으로 지정하세요.
- <img src={AlarmsType[select - 1].img} alt="chippi" /> + <img src={AlarmsType[select - 1].img} alt={AlarmsType[select - 1].title} loading="lazy" />
🧹 Nitpick comments (11)
apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx (3)
41-41: 절대 위치/음수 오프셋 사용 — 포지셔닝 컨텍스트 확인 필요
absolute -bottom-[180px] -right-[100px] z-10는 부모에relative가 없으면 예기치 않은 위치/오버플로우가 발생할 수 있습니다. AlarmBox 컨테이너(박스 카드)에relative를 추가하고, 필요 시 Popover/Portal 계열로 치환하는 것을 권장합니다.
51-53: 클래스명 오타 의심:bod y2-m→body2-m타이포로 보입니다. 스타일 미적용 가능성이 있습니다. 아래처럼 수정 제안드립니다.
- <p className="bod y2-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">
20-23: AM/PM 라벨 현지화 일관성초기 알람 텍스트는 '오전/오후'를 사용하지만 TimePicker는 'AM/PM'입니다. 표시 문자열을 통일하세요. 간단히 라벨과 값 모두를 '오전'/'오후'로 바꾸는 방법을 권장합니다.
-const meridiemOptions: WheelPickerOption[] = [ - { label: 'AM', value: 'AM' }, - { label: 'PM', value: 'PM' }, -]; +const meridiemOptions: WheelPickerOption[] = [ + { label: '오전', value: '오전' }, + { label: '오후', value: '오후' }, +];apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (1)
51-51: 불필요한 콘솔 로그 제거빌드/린트 경고가 발생합니다. 삭제 또는 환경 가드 처리하세요.
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (7)
10-11: 하드코딩된 진행률을 계산식으로 전환해주세요.3단계 스토리 진행률(30/60/100)을 상수로 두면 유지보수 비용이 커집니다. 표시식으로 계산하면 단계 증감/추가에 강해집니다.
적용 예시:
-const stepProgress = [{ progress: 30 }, { progress: 60 }, { progress: 100 }];- value={stepProgress[step].progress} + value={Math.round(((step + 1) / 3) * 100)}Also applies to: 79-85
36-39: 매직 넘버(5) 제거 및 경계 명시로 가독성/안정성 향상.
5와 같은 경계값을 상수로 올려두면 오타/경계 변경에 안전합니다.Diff(조건문 치환):
- if (step < 5) { + if (step < MAX_STEP) { setDirection(1); setStep((prev) => prev + 1); - } else if (step === 5) { + } else if (step === MAX_STEP) { window.location.href = '/'; }상단에 아래 상수를 추가해 주세요:
// 컴포넌트 상단 근처 const MAX_STEP = 5 as const;Also applies to: 62-67
58-61: 콘솔 로그 제거 및 알람 시간 데이터 흐름 재검토.
- lint 경고대로 console.log는 제거/가드 필요합니다.
AlarmsType[alarmSelected - 1].time는 정적 배열 참조라 사용자 설정 시간이 반영되지 않을 수 있습니다(3번 "사용자 설정"의 경우 특히).제안:
- AlarmStep에서 선택/커스텀된 시간을 상위(MainCard)로 전달받아 상태로 보관하고 사용.
- 또는 전역 상태(예: Zustand/Redux/Context)에 기록 후 여기서 조회.
간단 예시:
// MainCard const [alarmTime, setAlarmTime] = useState<string | null>(null); // <AlarmStep onTimeChange={setAlarmTime} ... /> // if (step === 3) { analytics('alarm_selected', { time: alarmTime }); }불필요한 로그 제거:
- if (step === 3) { - console.log('선택된 알람:', AlarmsType[alarmSelected - 1].time); - }
65-67: SPA 네비게이션 API로 전환을 권장합니다.
window.location.href는 전체 리로드를 유발합니다. 라우터의push/navigate사용이 UX와 성능에 유리합니다.두 가지 예시 중 사용하는 라우터에 맞춰 선택해 주세요:
- Next.js(pages):
추가:import { useRouter } from 'next/router';
컴포넌트 내부:const router = useRouter();
치환:window.location.href = '/';
router.push('/');
- React Router v6:
추가:import { useNavigate } from 'react-router-dom';
컴포넌트 내부:const navigate = useNavigate();
치환:window.location.href = '/';
navigate('/');
104-115: 이전 버튼 표시 조건 확인 필요(4단계 이상에서 숨김).현재 step ≥ 4에서 이전 버튼이 사라져 사용자가 Alarm/Mac/Final 단계에서 뒤로 못 갑니다. 의도라면 OK, 아니라면 step ≤ 5에서도 노출을 고려해 주세요.
116-121: 다음 버튼 비활성 조건이 무의미합니다.
isDisabled={step === 6}은 도달 불가한 조건입니다(최대 5). 제거하거나 실제 조건으로 교체해 주세요.- isDisabled={step === 6}
40-57: renderStep 스위치를 단계 배열로 단순화 가능.가독성/추가 단계 확장성을 위해 컴포넌트 배열/맵 기반 렌더링을 고려할 수 있습니다. 현재 구현도 동작상 문제는 없습니다.
예시:
const steps = [ <StoryStep step={0} />, <StoryStep step={1} />, <StoryStep step={2} />, <AlarmStep selected={alarmSelected} setSelected={setAlarmSelected} />, <MacStep />, <FinalStep /> ]; return steps[step] ?? <FinalStep />;
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/alarms.ts(1 hunks)apps/client/src/pages/onBoarding/components/timePicker/TimePicker.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (1)
apps/client/src/pages/onBoarding/components/funnel/alarms.ts (1)
AlarmsType(11-15)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
apps/client/src/pages/onBoarding/components/funnel/alarms.ts (1)
AlarmsType(11-15)
🪛 GitHub Check: lint
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx
[warning] 51-51:
Unexpected console statement
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
[warning] 60-60:
Unexpected console statement
🔇 Additional comments (2)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
12-22: Framer Motion variants 구성 깔끔합니다.direction 기반의 enter/exit 좌우 전환이 명확하고
mode="wait"와도 잘 어울립니다.
23-34: cva 변형 처리 적절합니다.boolean variant(
overflow: true/false)로 TimePicker 오버플로우 케이스를 제어하는 접근이 명확합니다.Also applies to: 78-78
| onSave={({ hour, minute, meridiem }) => { | ||
| const formatted = `${meridiem} ${hour}:${minute}`; | ||
| AlarmsType[2].time = formatted; | ||
| console.log('저장된 사용자 알람:', AlarmsType[2].time); | ||
| }} | ||
| onCancel={() => { | ||
| AlarmsType[2].time = ''; | ||
| }} |
There was a problem hiding this comment.
모듈 전역 상수(AlarmsType) 직접 변경 + 리렌더 미보장
AlarmsType[2].time을 직접 대입해도 React 리렌더가 보장되지 않습니다. 전역 가변 상태는 사이드이펙트 리스크도 큽니다. 로컬 상태(또는 상위 상태)로 관리하세요. 콘솔 로그도 린트 경고가 있습니다.
아래처럼 최소 수정안을 제안드립니다.
+import { useState } from 'react';
@@
-const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
- return (
+const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => {
+ const [customTime, setCustomTime] = useState('');
+ return (
@@
- {AlarmsType[2].time && (
- <p className="caption2-m text-font-gray-3">{AlarmsType[2].time}</p>
- )}
+ {customTime && (
+ <p className="caption2-m text-font-gray-3">{customTime}</p>
+ )}
@@
- onSave={({ hour, minute, meridiem }) => {
- const formatted = `${meridiem} ${hour}:${minute}`;
- AlarmsType[2].time = formatted;
- console.log('저장된 사용자 알람:', AlarmsType[2].time);
- }}
+ onSave={({ hour, minute, meridiem }) => {
+ const formatted = `${meridiem} ${hour}:${minute}`;
+ setCustomTime(formatted);
+ // 필요 시 상위로 콜백 전달: onChangeCustomTime?.(formatted)
+ }}
onCancel={() => {
- AlarmsType[2].time = '';
+ setCustomTime('');
}}추가로, TimePicker에서 '오전/오후'를 사용하도록 바꾸면 표시 문자열도 자연스럽게 맞춰집니다.
📝 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.
| onSave={({ hour, minute, meridiem }) => { | |
| const formatted = `${meridiem} ${hour}:${minute}`; | |
| AlarmsType[2].time = formatted; | |
| console.log('저장된 사용자 알람:', AlarmsType[2].time); | |
| }} | |
| onCancel={() => { | |
| AlarmsType[2].time = ''; | |
| }} | |
| import { useState } from 'react'; | |
| import React from 'react'; | |
| import TimePicker from '...'; // 기존 TimePicker 경로 | |
| import AlarmsType from '...'; // 기존 AlarmsType 경로 | |
| import { boxStyle } from '...'; // 기존 boxStyle 경로 | |
| const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => { | |
| const [customTime, setCustomTime] = useState(''); | |
| return ( | |
| <button | |
| type="button" | |
| className={boxStyle({ disabled: isDisabled })} | |
| onClick={onClick} | |
| disabled={isDisabled} | |
| > | |
| <img | |
| src={AlarmsType[select - 1].img} | |
| alt={AlarmsType[select - 1].title} | |
| loading="lazy" | |
| /> | |
| <p className={`sub3-sb ${isDisabled ? 'text-main500' : 'text-font-black-1'}`}> | |
| {AlarmsType[select - 1].title} | |
| </p> | |
| {customTime && ( | |
| <p className="caption2-m text-font-gray-3">{customTime}</p> | |
| )} | |
| <TimePicker | |
| onSave={({ hour, minute, meridiem }) => { | |
| const formatted = `${meridiem} ${hour}:${minute}`; | |
| setCustomTime(formatted); | |
| // 필요 시 상위로 콜백 전달: onChangeCustomTime?.(formatted) | |
| }} | |
| onCancel={() => { | |
| setCustomTime(''); | |
| }} | |
| /> | |
| </button> | |
| ); | |
| }; | |
| export default AlarmBox; |
🧰 Tools
🪛 GitHub Check: lint
[warning] 51-51:
Unexpected console statement
| export const AlarmsType: AlarmType[] = [ | ||
| { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, | ||
| { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, | ||
| { img: avatar3, title: '사용자 설정', time: '' }, | ||
| ]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
표시용 데이터와 상태 변경 분리
AlarmsType은 모듈 상수처럼 보이는데, 다른 컴포넌트(AlarmBox)에서 직접 값을 변경하고 있습니다. 렌더 트리거가 되지 않아 UI가 갱신되지 않을 수 있고, 전역 가변 상태로 인한 사이드이펙트 위험이 큽니다. 기본값 소스로만 두고, 선택/변경된 시간은 상위 컴포넌트 상태로 관리하세요. 또한 초기 포맷도 오전 09:00처럼 분까지 맞추면 일관성이 좋아집니다.
-export const AlarmsType: AlarmType[] = [
- { img: avatar1, title: '아침형 치삐', time: '오전 9시' },
- { img: avatar2, title: '저녁형 치삐', time: '오후 8시' },
- { img: avatar3, title: '사용자 설정', time: '' },
-];
+export const AlarmsType: AlarmType[] = [
+ { img: avatar1, title: '아침형 치삐', time: '오전 09:00' },
+ { img: avatar2, title: '저녁형 치삐', time: '오후 08:00' },
+ { img: avatar3, title: '사용자 설정', time: '' }, // 표시값은 상태에서 관리 권장
+];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const AlarmsType: AlarmType[] = [ | |
| { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, | |
| { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, | |
| { img: avatar3, title: '사용자 설정', time: '' }, | |
| ]; | |
| export const AlarmsType: AlarmType[] = [ | |
| { img: avatar1, title: '아침형 치삐', time: '오전 09:00' }, | |
| { img: avatar2, title: '저녁형 치삐', time: '오후 08:00' }, | |
| { img: avatar3, title: '사용자 설정', time: '' }, // 표시값은 상태에서 관리 권장 | |
| ]; |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/alarms.ts around lines
11-15, AlarmsType is defined as a mutable module-level array but is being
mutated by other components; change it to be a pure display-only constant (do
not mutate it), lift selected/changed time and any updates into the parent
component state (pass value + onChange down to AlarmBox), and when creating
initial/default time values use a consistent minute format like "오전 09:00" (pad
hours and include ":00"). Ensure components always work with copies of
AlarmsType items (or ids) and never write back to the imported constant to avoid
render issues and side effects.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (6)
11-21: 초기 진입 애니메이션 방향값(0) 처리 제안초기 렌더링 때도 좌우 슬라이드가 발생합니다. direction이 0일 땐 이동 없이 페이드인만 하도록 보정하면 UX가 안정적입니다.
-const variants = { - enter: (direction: number) => ({ - x: direction > 0 ? 200 : -200, - opacity: 0, - }), - center: { x: 0, opacity: 1 }, - exit: (direction: number) => ({ - x: direction > 0 ? -200 : 200, - opacity: 0, - }), -}; +const variants = { + enter: (direction: number) => ({ + x: direction === 0 ? 0 : direction > 0 ? 200 : -200, + opacity: 0, + }), + center: { x: 0, opacity: 1 }, + exit: (direction: number) => ({ + x: direction === 0 ? 0 : direction > 0 ? -200 : 200, + opacity: 0, + }), +};
100-104: Framer Motion transition 명시로 일관성 확보브라우저별 기본값 차이를 피하려면 transition에 type/ease를 명시하는 편이 안전합니다.
- transition={{ duration: 0.4 }} + transition={{ type: 'tween', duration: 0.4, ease: 'easeInOut' }}추가로 접근성 관점에서 prefers-reduced-motion 대응을 고려해도 좋습니다(예: duration을 0으로).
34-39: step 타입 한정 및 isMac 명명 정합성
- step을 0|1|2|3|4|5 유니온으로 한정하면 실수 방지가 됩니다.
- 주석대로 iPhone/iPad까지 포함한다면 isMac보다 isApple(또는 isApplePlatform)이 의미상 명확합니다. 반대로 macOS만 타깃이면 iOS 감지는 제외해야 합니다(아래 감지 로직 코멘트 참고).
가능하면 다음처럼 타입을 좁혀주세요:
- const [step, setStep] = useState(0); + type Step = 0 | 1 | 2 | 3 | 4 | 5; + const [step, setStep] = useState<Step>(0);변수명 변경을 원하시면 다음도 고려:
- const [isMac, setIsMac] = useState(false); + const [isApple, setIsApple] = useState(false);(라인 58의 사용처도 함께 변경 필요)
40-46: UA-CH(userAgentData) 우선의 플랫폼 감지로 견고성 강화userAgent는 축소되거나 브라우저별 편차가 큽니다. UA-CH(platform) 우선 + UA 폴백으로 보강을 권장합니다.
- useEffect(() => { - const ua = navigator.userAgent.toLowerCase(); - // Mac OS (Intel/ARM), iPhone, iPad 까지 포함하고 싶으면 아래처럼 체크 가능 - if (ua.includes('mac os') || ua.includes('iphone') || ua.includes('ipad')) { - setIsMac(true); - } - }, []); + useEffect(() => { + const uaData = (navigator as any).userAgentData; + const platform = uaData?.platform?.toLowerCase?.(); + const ua = navigator.userAgent?.toLowerCase?.() ?? ''; + const isApplePlatform = platform + ? /mac|ios/.test(platform) + : /mac os|iphone|ipad/.test(ua); + setIsMac(isApplePlatform); + }, []);
65-75: 라우터 API로 네비게이션 교체 권장window.location.href는 전체 리로드를 유발합니다. 앱 라우터(React Router/Next 등)를 사용해 클라이언트 내 전환으로 바꾸는 것을 권장합니다.
React Router 예시:
// import { useNavigate } from 'react-router-dom'; const navigate = useNavigate(); // ... else if (step === 5) { navigate('/'); }Next.js(App Router) 예시:
// import { useRouter } from 'next/navigation'; const router = useRouter(); // ... else if (step === 5) { router.replace('/'); }
94-109: 빠른 연타로 인한 다중 전환 방지(애니메이션 중 버튼 비활성화) 제안mode="wait"로 exit 대기하더라도 빠른 클릭 시 setStep 큐잉될 수 있습니다. 전환 중 버튼을 잠깐 비활성화하면 안전합니다.
변경 요지:
- 전역 상태: isAnimating 추가
- nextStep 시작 시 가드 및 setIsAnimating(true)
- AnimatePresence에 onExitComplete로 해제
해당 블록 변경:
- <AnimatePresence custom={direction} mode="wait"> + <AnimatePresence custom={direction} mode="wait" onExitComplete={() => setIsAnimating(false)}>다른 위치에 필요한 보조 변경(참고):
// state - const [direction, setDirection] = useState(0); + const [direction, setDirection] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); // nextStep - const nextStep = () => { + const nextStep = () => { + if (isAnimating) return; if (step === 3) { // ... } if (step < 5) { setDirection(1); + setIsAnimating(true); setStep((prev) => prev + 1); } else if (step === 5) { window.location.href = '/'; } };그리고 아래 “다음” 버튼의 isDisabled에 isAnimating을 반영해주세요(별도 코멘트 참고).
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)
⏰ 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 (5)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (5)
1-10: LGTM — import 구성과 진행률 상수 정의 적절디자인 시스템·Framer Motion 의존성 사용 및 3단계 진행률(30/60/100) 매핑이 요구사항과 잘 맞습니다.
22-33: overflow-visible 토글 의도 확인TimePicker/팝업이 Portal(예: Radix)로 렌더링된다면 카드의 overflow를 토글할 필요가 없습니다. 현재는 step 3에서 alarmSelected===3일 때만 visible인데, 다른 팝업/툴팁이 생기면 예외가 늘어날 수 있습니다. Portal 사용 여부에 따라 전략을 통일하는지 확인 부탁드립니다.
47-64: iOS에서도 MacStep 노출되는 현재 로직 의도 확인현재 감지 로직상 iPhone/iPad도 isMac=true가 되어 step 4에서 MacStep이 렌더됩니다. iOS 사용자는 FinalStep으로 가야 한다면 감지 조건에서 iOS를 제외해야 합니다.
85-93: 진행률 표시 조건/값 매핑 적절스토리 3단계에만 Progress를 노출하는 현재 조건(step < 3)과 값 매핑(stepProgress[step])이 기획 의도와 일치해 보입니다.
111-122: 이전 버튼(step ≥ 4에서 비노출) UX 의도 확인step 4(맥 안내/최종 직전)에서도 뒤로가기를 숨기는 게 기획 의도인지 확인이 필요합니다. 데이터 유실 이슈가 없다면 step ≤ 5까지 노출하는 편이 사용자에게 더 관대합니다.
| <Button | ||
| variant="primary" | ||
| size="medium" | ||
| isDisabled={step === 6} | ||
| className="ml-auto w-[4.8rem]" | ||
| onClick={nextStep} | ||
| > | ||
| 다음 | ||
| </Button> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
다음 버튼 비활성화 조건 버그(step === 6) 수정
step은 0–5 범위로 운용되므로 현재 조건은 영원히 false입니다. 의미 없는 prop 전달이므로 제거하거나 전환 중(isAnimating) 비활성화로 교체하세요.
전환 중 비활성화로 교체 예:
- isDisabled={step === 6}
+ isDisabled={isAnimating}단순 제거 예:
- isDisabled={step === 6}
+ // isDisabled 제거📝 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.
| <Button | |
| variant="primary" | |
| size="medium" | |
| isDisabled={step === 6} | |
| className="ml-auto w-[4.8rem]" | |
| onClick={nextStep} | |
| > | |
| 다음 | |
| </Button> | |
| </div> | |
| <Button | |
| variant="primary" | |
| size="medium" | |
| isDisabled={isAnimating} | |
| className="ml-auto w-[4.8rem]" | |
| onClick={nextStep} | |
| > | |
| 다음 | |
| </Button> | |
| </div> |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
123 to 132 the Button has isDisabled={step === 6} which is always false because
step ranges 0–5; remove this meaningless prop or replace it with a real
animation/transition flag (e.g., isDisabled={isAnimating}) so the Next button is
disabled during transitions; update the component to either drop isDisabled
entirely or wire it to the existing isAnimating state (ensure isAnimating is
defined and passed down or managed in this component).
There was a problem hiding this comment.
@jjangminii 네, 확인했습니다! CodeRabbit이 지적한 버그가 맞습니다.
문제점:
isDisabled={step === 6}조건이 의미없습니다step상태는 0-5 범위에서만 동작하므로step === 6은 항상false입니다- 따라서 "다음" 버튼이 실제로는 비활성화되지 않습니다
수정 방안:
- 단순 제거 (가장 간단):
<Button
variant="primary"
size="medium"
className="ml-auto w-[4.8rem]"
onClick={nextStep}
>
다음
</Button>- 애니메이션 중 비활성화 (더 나은 UX):
// 상태 추가
const [isAnimating, setIsAnimating] = useState(false);
// 애니메이션 처리
const nextStep = () => {
setIsAnimating(true);
// ... 기존 로직
setTimeout(() => setIsAnimating(false), 400); // 애니메이션 duration과 맞춤
};
// 버튼에서 사용
<Button
variant="primary"
size="medium"
isDisabled={isAnimating}
className="ml-auto w-[4.8rem]"
onClick={nextStep}
>
다음
</Button>어떤 방향으로 수정하실 건가요?
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (12)
apps/client/src/pages/onBoarding/components/funnel/step/FinalStep.tsx (3)
4-4: 세로 중앙 정렬이 목적이라면justify-center가 필요합니다.현재는
items-center만 있어 가로 정렬만 중앙이고, 세로는 상단 정렬입니다.- <div className="flex h-full flex-col items-center"> + <div className="flex h-full flex-col items-center justify-center">
5-5: 이미지 접근성/성능: 장식용이면 빈 alt + 보조 속성 추가를 권장합니다.의미 전달이 텍스트로 충분하다면 스크린 리더 노이즈를 줄이기 위해
alt=""와aria-hidden을 권장합니다. 또한decoding="async"로 페인트 지연을 줄일 수 있습니다. 원본 크기를 알 수 있다면width/height지정으로 CLS도 예방하세요.- <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> + <img + src={dotori} + className="mb-[1.2rem]" + alt="" + aria-hidden="true" + decoding="async" + />의미 있는 아이콘이라면 대안으로
alt="도토리 아이콘"등 구체적인 한글 대체텍스트를 사용하세요.
6-8: 시맨틱 마크업: 스타일 헤딩(head2)은 실제 헤딩 태그를 사용하세요.시각적 헤딩을
<p>로 마크업하면 내비게이션/접근성에 불리합니다.<h2>로 교체를 권장합니다. 텍스트는 i18n 키로 외부화하면 향후 다국어 대응이 수월합니다.- <p className="head2 text-font-black-1"> + <h2 className="head2 text-font-black-1"> 도토리 찾으러 갈 시간을 정해볼까요? - </p> + </h2>(선택) i18n 사용 예시:
// import { useTranslation } from 'react-i18next'; // const { t } = useTranslation(); // <h2 className="head2 text-font-black-1"> // {t('onboarding.finalStep.title', '도토리 찾으러 갈 시간을 정해볼까요?')} // </h2>apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx (3)
6-6: 장식용 이미지 접근성/성능 개선: alt 비우고 지연 로드 추가텍스트가 동일 정보를 제공하므로 장식 이미지라면 alt를 비우고 lazy 로딩을 적용하세요.
- <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> + <img src={dotori} className="mb-[1.2rem]" alt="" aria-hidden="true" loading="lazy" decoding="async" />
7-12: 의미 요소 사용: 제목은 h2로시맨틱 마크업과 스크린리더 내비게이션을 위해 제목을 h2로 바꾸는 것이 좋습니다.
- <p className="head2 text-font-black-1"> + <h2 className="head2 text-font-black-1"> 도토리 찾으러 갈 시간을 정해볼까요? - </p> + </h2>
13-13: 설명 이미지 대체 텍스트 구체화 + 지연 로딩Mac 알림 설정 안내임이 드러나도록 alt를 구체화하고 lazy 로딩을 추가하세요.
- <img src={MacNotice} className="absolute -bottom-[104px]" alt="mac" /> + <img src={MacNotice} className="absolute -bottom-[104px]" alt="Mac 알림 설정 안내" loading="lazy" decoding="async" />apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx (2)
7-40: 사용되지 않는progress필드 정리 또는 사용처 확인 필요
steps의progress가 현재 컴포넌트 내에서 미사용입니다. 상위(예: Progress 바)에 연결될 계획이 없다면 제거해 주세요.원한다면 제가 사용처 스캔 스크립트를 드릴게요.
45-50: 이미지 접근성/성능: alt 구체화 및 lazy 로딩단일 "onboarding" 대신 단계 정보가 드러나도록 alt를 구체화하고 lazy 로딩을 적용하세요.
- <img - src={steps[step].img} - className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" - alt="onboarding" - /> + <img + src={steps[step].img} + className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" + alt={`온보딩 스토리 이미지 ${step + 1}단계`} + loading="lazy" + decoding="async" + />apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx (1)
3-6: 반복되는 리터럴 타입(1|2|3) 공통화
AlarmOption타입을 정의해 재사용하면 가독성과 변경 용이성이 좋아집니다.-interface AlarmStepProps { - selected: 1 | 2 | 3; - setSelected: (n: 1 | 2 | 3) => void; -} +type AlarmOption = 1 | 2 | 3; +interface AlarmStepProps { + selected: AlarmOption; + setSelected: (n: AlarmOption) => void; +}apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx (3)
29-36: alt 텍스트를 데이터 기반으로 노출타이틀을 alt로 사용하면 더 의미 있는 설명이 됩니다.
- <img src={AlarmsType[select - 1].img} alt="chippi" /> + <img src={AlarmsType[select - 1].img} alt={AlarmsType[select - 1].title} loading="lazy" decoding="async" />
11-21: 의미 불일치:isDisabled→isSelected로 명명 수정 제안스타일 분기에 실질적으로 선택 상태를 사용하고 있습니다. 명확한 의미 전달을 위해 prop/variant 이름을
isSelected또는selected로 바꾸는 것이 좋습니다.예:
-variants: { disabled: { true: '...', false: '...' } } +variants: { selected: { true: 'border-main400 bg-main100 border', false: 'bg-white border border-transparent hover:border-main300' } }그리고 호출부/props도 동일하게 변경.
56-56: 개발 메모/콘솔 주석 제거배포 코드에서 불필요합니다.
- // 이거 나중에 api 연결때 쓸려고 표시한거.. 그떄 지우겠듬여 console.log('저장된 사용자 알람:', AlarmsType[2].time); + // TODO: 상위 상태/API 연동 시 제거
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/FinalStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/type/alarms.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx (1)
apps/client/src/pages/onBoarding/components/funnel/type/alarms.ts (1)
AlarmsType(11-15)
⏰ 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)
apps/client/src/pages/onBoarding/components/funnel/step/FinalStep.tsx (2)
2-12: 전반적으로 깔끔한 프레젠테이셔널 컴포넌트입니다.부수효과 없고 재사용 가능성이 높아 보입니다. 위 제안만 반영되면 더욱 견고해질 것 같습니다.
1-1: 잘못된 경로 별칭 제안: apps/client의 tsconfig.json과 vite.config.ts에@/assetsalias가 설정되어 있지 않으므로import dotori from '@/assets/onBoarding/icons/dotori.svg';는 동작하지 않습니다. assets 폴더에 별칭을 사용하려면 tsconfig.json의
compilerOptions.paths와 Vite의resolve.alias에 해당 alias를 먼저 정의해야 합니다.Likely an incorrect or invalid review comment.
| const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => { | ||
| const [showPicker, setShowPicker] = useState(false); | ||
|
|
||
| return ( | ||
| <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}> |
There was a problem hiding this comment.
TimePicker가 열리지 않는 기능 결함 — 클릭 시 showPicker 토글 필요
현재 showPicker를 true로 만드는 경로가 없어 사용자 설정(3) 선택 시 피커가 표시되지 않습니다.
다음과 같이 컨테이너 클릭 시 선택 로직과 함께 피커를 열어주세요(3번 선택일 때):
- <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}>
+ <div
+ className={boxStyle({ disabled: isDisabled })}
+ onClick={(e) => {
+ onClick?.();
+ if (select === 3) setShowPicker(true);
+ }}
+ >📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => { | |
| const [showPicker, setShowPicker] = useState(false); | |
| return ( | |
| <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}> | |
| const AlarmBox = ({ select, isDisabled, onClick }: AlarmBoxProps) => { | |
| const [showPicker, setShowPicker] = useState(false); | |
| return ( | |
| <div | |
| className={boxStyle({ disabled: isDisabled })} | |
| onClick={(e) => { | |
| onClick?.(); | |
| if (select === 3) setShowPicker(true); | |
| }} | |
| > |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx around
lines 24-28, the click handler on the container does not toggle showPicker so
choosing option 3 never opens the TimePicker; update the onClick flow to first
guard isDisabled, then call the existing onClick/select logic and when select
=== 3 setShowPicker(true) (or toggle it appropriately) so the picker opens for
choice 3, ensuring you don't open the picker when disabled and preserve any
existing onClick behavior.
| const [showPicker, setShowPicker] = useState(false); | ||
|
|
||
| return ( | ||
| <div className={boxStyle({ disabled: isDisabled })} onClick={onClick}> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
클릭 가능한 div 접근성 개선: 버튼 역할/키보드 핸들링 추가
div 대신 버튼 사용 또는 ARIA 역할을 부여해야 합니다.
- <div
+ <div
className={boxStyle({ disabled: isDisabled })}
+ role="radio"
+ aria-checked={isDisabled}
+ tabIndex={0}
onClick={(e) => {
onClick?.();
if (select === 3) setShowPicker(true);
}}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onClick?.();
+ if (select === 3) setShowPicker(true);
+ }
+ }}
>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx around
line 28, the clickable div lacks proper accessibility; replace the div with a
semantic <button> (or if you must keep a div, add role="button", tabIndex={0},
aria-disabled when appropriate, and implement an onKeyDown handler that triggers
onClick for Enter and Space while preventing action when disabled). Ensure
disabled state is reflected in attributes (disabled on <button> or aria-disabled
and remove focusable tabIndex when disabled), preserve existing styling and
onClick behavior, and keep keyboard focus/visual styles consistent with other
controls.
| onSave={({ hour, minute, meridiem }) => { | ||
| const formatted = `${meridiem} ${hour}:${minute}`; | ||
| AlarmsType[2].time = formatted; | ||
| setShowPicker(false); | ||
| // 이거 나중에 api 연결때 쓸려고 표시한거.. 그떄 지우겠듬여 console.log('저장된 사용자 알람:', AlarmsType[2].time); | ||
| }} | ||
| onCancel={() => { | ||
| AlarmsType[2].time = ''; | ||
| }} | ||
| onClick={(e) => e.stopPropagation()} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
전역 상수 변경과 취소 동작 누락
AlarmsType[2].time을 직접 변경하면 전역 가변 상태가 됩니다. 상위에서customTime을 state로 내려받아 갱신하세요.- 취소 시 피커가 닫히지 않습니다.
- onSave={({ hour, minute, meridiem }) => {
- const formatted = `${meridiem} ${hour}:${minute}`;
- AlarmsType[2].time = formatted;
- setShowPicker(false);
- // 이거 나중에 api 연결때 쓸려고 표시한거.. 그떄 지우겠듬여 console.log('저장된 사용자 알람:', AlarmsType[2].time);
- }}
- onCancel={() => {
- AlarmsType[2].time = '';
- }}
+ onSave={({ hour, minute, meridiem }) => {
+ const mm = `${minute}`.padStart(2, '0');
+ const formatted = `${meridiem} ${hour}:${mm}`;
+ // TODO: 상위에서 내려준 setCustomTime(formatted) 호출
+ setShowPicker(false);
+ }}
+ onCancel={() => {
+ // TODO: 상위에서 내려준 setCustomTime('') 호출
+ setShowPicker(false);
+ }}또한 파일 상단 props에 customTime?: string; setCustomTime?: (v: string) => void;를 추가하고, 표시부에서도 AlarmsType[2].time 대신 customTime을 사용하도록 변경해 주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/step/AlarmBox.tsx around
lines 52-61, the code mutates the global AlarmsType[2].time and doesn't close
the picker on cancel; change to accept props customTime?: string and
setCustomTime?: (v: string) => void (add to component props at top), use
customTime for display instead of AlarmsType[2].time, call
setCustomTime(formatted) inside onSave and setShowPicker(false), and in onCancel
call setCustomTime('') and setShowPicker(false) (guard calls with optional
chaining if setCustomTime may be undefined).
| <div className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]"> | ||
| {[1, 2, 3].map((n) => ( | ||
| <AlarmBox | ||
| key={n} | ||
| select={n as 1 | 2 | 3} | ||
| isDisabled={selected === n} | ||
| onClick={() => setSelected(n as 1 | 2 | 3)} | ||
| /> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
라디오 그룹 역할 명시 및 키보드 접근성 개선
세 가지 중 하나를 선택하는 UI는 라디오 그룹 패턴에 적합합니다. role="radiogroup"를 선언하고 각 항목에 role="radio"/aria-checked가 노출되도록 하세요(AlarmBox 수정 포함).
- <div className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]">
+ <div
+ className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]"
+ role="radiogroup"
+ aria-label="알람 옵션 선택"
+ >
{[1, 2, 3].map((n) => (
<AlarmBox
key={n}
select={n as 1 | 2 | 3}
isDisabled={selected === n}
onClick={() => setSelected(n as 1 | 2 | 3)}
/>
))}
</div>추가로 AlarmBox에서 role="radio"와 aria-checked={isDisabled}를 부여해 주세요.
📝 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.
| <div className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]"> | |
| {[1, 2, 3].map((n) => ( | |
| <AlarmBox | |
| key={n} | |
| select={n as 1 | 2 | 3} | |
| isDisabled={selected === n} | |
| onClick={() => setSelected(n as 1 | 2 | 3)} | |
| /> | |
| ))} | |
| </div> | |
| <div | |
| className="mb-[2rem] flex w-full items-center justify-center gap-[1.4rem]" | |
| role="radiogroup" | |
| aria-label="알람 옵션 선택" | |
| > | |
| {[1, 2, 3].map((n) => ( | |
| <AlarmBox | |
| key={n} | |
| select={n as 1 | 2 | 3} | |
| isDisabled={selected === n} | |
| onClick={() => setSelected(n as 1 | 2 | 3)} | |
| /> | |
| ))} | |
| </div> |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx around
lines 18-27, the selection UI must expose a radiogroup for screen readers and
keyboard users: add role="radiogroup" to the wrapper div and pass role="radio"
plus aria-checked={selected === n} (or aria-checked={isDisabled} if you keep
that prop naming) into each AlarmBox via props; update AlarmBox to accept and
apply role="radio" and aria-checked accordingly and ensure it remains
keyboard-focusable (e.g., tabIndex/Enter/Space handling) so assistive tech and
keyboard users can operate the radio items.
| import dotori from '../../../../assets/onBoarding/icons/dotori.svg'; | ||
| const MacStep = () => { | ||
| return ( | ||
| <div className="flex h-full flex-col items-center"> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
절대 위치 요소가 기준 요소 없이 배치됩니다 — 컨테이너에 relative 추가 권장
하단 이미지가 absolute인데 상위에 포지셔닝 컨텍스트가 없어 레이아웃이 흔들릴 수 있습니다.
다음으로 수정해 주세요:
- <div className="flex h-full flex-col items-center">
+ <div className="relative flex h-full flex-col items-center">📝 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.
| <div className="flex h-full flex-col items-center"> | |
| <div className="relative flex h-full flex-col items-center"> |
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx around
line 5, the container <div className="flex h-full flex-col items-center"> is
missing a positioning context while it contains absolutely positioned children;
add the positioning context by adding the "relative" class to this container
(e.g., className="flex h-full flex-col items-center relative") so the
absolute-positioned image is anchored to this element.
| export const AlarmsType: AlarmType[] = [ | ||
| { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, | ||
| { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, | ||
| { img: avatar3, title: '사용자 설정', time: '' }, | ||
| ]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
전역 데이터 변경 방지: 읽기 전용으로 고정
현재 AlarmBox가 AlarmsType[2].time을 직접 변경하고 있어 전역 가변 상태가 됩니다. 데이터 정의는 읽기 전용으로 두고, 선택/시간은 상위 상태로 관리하세요.
-export const AlarmsType: AlarmType[] = [
+export const AlarmsType: Readonly<AlarmType[]> = [
{ img: avatar1, title: '아침형 치삐', time: '오전 9시' },
{ img: avatar2, title: '저녁형 치삐', time: '오후 8시' },
{ img: avatar3, title: '사용자 설정', time: '' },
];이 변경에 맞춰 AlarmBox에서의 직접 할당을 제거하고 props로 시간을 주입하도록 리팩터링해 주세요.
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/type/alarms.ts around
lines 11-15, the AlarmsType array is currently mutable and AlarmBox mutates
AlarmsType[2].time; make AlarmsType read-only (use a const/readonly type or TS
const assertion) so its entries cannot be mutated, then refactor AlarmBox to
stop assigning into AlarmsType and instead accept a time prop (and any id/index)
for display; move selection/time state up into the parent component that renders
AlarmBox (manage selectedTime in parent, pass setter to update parent state),
and update all call sites to pass the current time and onChange handler rather
than relying on mutating the exported array.
jjangminii
left a comment
There was a problem hiding this comment.
AnimatePresence + custom={direction} 패턴으로 좌/우 슬라이드 방향 제어한 부분이 저번에 구현할 때 생각하지 못한 부분이었는데 ux적으로 더 좋아보여요 -!!
고생많으셨어요 코멘트 몇개만 확인해주세요
There was a problem hiding this comment.
로고는 사이드바에서도 사용하고 익스텐션에서등 다양한 위치에서 사용된다고 생각하는데 디자인 시스템에 추가하는 것은 어떤가요?
괜찮으시다면 추후 제 사이드바 pr에서 로고 디자인 시스템 위치에 아이콘 적용해뒀는데 가져다 사용해도 좋을것같아요-!
| @@ -0,0 +1,12 @@ | |||
| import dotori from '../../../../../assets/onBoarding/icons/dotori.svg'; | |||
There was a problem hiding this comment.
이 부분에 상대경로에 대해서 저도 코드를 작성하며 가독성이 좋지 못하다고 느꼈는데 절대경로를 더 추가하는 것에 대해 어떻게 생각하시나여
There was a problem hiding this comment.
절대경로 선언하여 적용해두었습니당!
| <p className="body2-m text-font-gray-3 mb-[4.3rem] mt-[0.8rem] text-center"> | ||
| Mac 사용자는 추가 알림 설정을 진행해 주세요. | ||
| </p> | ||
| <img src={MacNotice} className="absolute -bottom-[104px]" alt="mac" /> |
There was a problem hiding this comment.
이부분 특별히 px로 하신 이유가 있을까요??
| <Button | ||
| variant="primary" | ||
| size="medium" | ||
| isDisabled={step === 6} | ||
| className="ml-auto w-[4.8rem]" | ||
| onClick={nextStep} | ||
| > | ||
| 다음 | ||
| </Button> | ||
| </div> |
constantly-dev
left a comment
There was a problem hiding this comment.
고생하셨습니다!! funnel에 알람 select, 캐러셀과 mac 분기 처리까지 생각할 부분이 정말 많은 작업이었을텐데 너무 잘 구현해주셨네요!
PR에 작성해주신 것처럼 사실 지금은 funnel 구조를 사용하는 부분이 한 곳이라 괜찮지만, 이후에 더 확장이 될 가능성이 생기면 useFunnel 등으로 훅으로 만드는 방법도 좋겠네요!
현재 step 잘 구현해주셨는데 step 관련된 코드는 각 step 컴포넌트 내에 위치시켜서 MainCard 역할은 step을 보여주기만 하는 card 역할(책임)이 되도록 리팩토링 하면 좋을 것 같아요!
추가로 여러가지 코멘트 남겼으니 확인 부탁드려요 👍
| @@ -1,5 +1,16 @@ | |||
| import onBoardingBg from '../../assets/onBoarding/background/onBoardingBg.svg'; | |||
There was a problem hiding this comment.
필요하면 assets도 절대 경로에 추가해도 될 것 같은데 어떻게 생각하시나요!
| export const AlarmsType: AlarmType[] = [ | ||
| { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, | ||
| { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, | ||
| { img: avatar3, title: '사용자 설정', time: '' }, | ||
| ]; |
There was a problem hiding this comment.
요건 type이 아니라 constants에 들어가야 할 것 같아요!
| <div | ||
| className="z-5 absolute top-0 flex w-full justify-items-start px-[8rem] py-[2.5rem]" | ||
| onClick={() => window.location.reload()} | ||
| > | ||
| <img src={HeaderLogo} alt="header logo" /> | ||
| </div> |
There was a problem hiding this comment.
헤더라면 의미(semantic)를 생각하면 header 태그를 쓰는 것이 더 좋을 것 같아요!
그리고 header 전체는 w-full 영역을 잡으니 onClick은 logo image에 달아 주는 것이 맞을 것 같아요!
There was a problem hiding this comment.
이 파일은 step에 들어가는 것이 아닌 것 같아요!
| case 0: | ||
| case 1: | ||
| case 2: | ||
| return <StoryStep step={step as 0 | 1 | 2} />; |
There was a problem hiding this comment.
혹시 스텝꼬일까봐
step이 0,1,2 중 하나인 걸 보장할때만 하도록 타입 단언해두긴 했습니다.! 너무 불필요한 단언이라면 뺴두겠습니다 :)
| <Progress | ||
| value={stepProgress[step].progress} | ||
| variant="profile" | ||
| className="w-[30%]" |
There was a problem hiding this comment.
30%가 figma상으로 정확한 width 값인가요?!?!?
There was a problem hiding this comment.
px/rem로 박으면 너무 쪼그라들거나 작아보이는 거 같아서 %로 하긴했는데, 이부분은 QA때 디바이스 별로 확인하는 걸로 하고 실제 width로 다시 지정해두겠습니당 :)
| onSave={({ hour, minute, meridiem }) => { | ||
| const formatted = `${meridiem} ${hour}:${minute}`; | ||
| AlarmsType[2].time = formatted; | ||
| setShowPicker(false); | ||
| // 이거 나중에 api 연결때 쓸려고 표시한거.. 그떄 지우겠듬여 console.log('저장된 사용자 알람:', AlarmsType[2].time); |
There was a problem hiding this comment.
잘 구현해주셨네요! 다만 안에서 모든 분기처리를 하는 방법도 있지만, 단순 텍스트와 이미지를 외부에서 주입하게 하는 방법도 있을 것 같아요.
인터페이스를 생각해보면
title
subTitle (optional) // 마지막 3번째 box는 없으니!
imageUrl
onClick
isActive
이런 느낌!
There was a problem hiding this comment.
아하! 그러네요 추후 리팩토링 때, 이 방법이 더 관리가 쉽다면 수정해보겠습니다! 아이디어 감사합니다
| {[1, 2, 3].map((n) => ( | ||
| <AlarmBox | ||
| key={n} | ||
| select={n as 1 | 2 | 3} | ||
| isDisabled={selected === n} | ||
| onClick={() => setSelected(n as 1 | 2 | 3)} | ||
| /> | ||
| ))} |
There was a problem hiding this comment.
as를 써도 되고 해당 배열을 따로 분리해서 as const 키워드를 붙여서 값 (리터럴) 자체로 type이 되도록 설정하면 as 안 써도 에러가 안 뜰 거에요!
There was a problem hiding this comment.
아하! 단언문을 불필요하게 반복하고 있었군요..! as const로 리터럴 타입 그대로 고정하게 할 수 있군녀 꿀정보 감삼다
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (4)
12-21: disabled에서도 포인터/호버가 활성화됨베이스 클래스에
cursor-pointer가 있어 비활성 상태에도 포인터/호버가 적용됩니다. 변형별로 분기하세요.-const boxStyle = cva( - 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] cursor-pointer transition', +const boxStyle = cva( + 'flex h-[22.4rem] w-[18rem] flex-col items-center rounded-[1.2rem] px-[3.9rem] pb-[2.6rem] pt-[3.6rem] transition', { variants: { disabled: { - true: 'border-main400 bg-main100 border', - false: 'bg-white border border-transparent hover:border-main300', + true: 'border-main400 bg-main100 border cursor-not-allowed pointer-events-none', + false: 'bg-white border border-transparent hover:border-main300 cursor-pointer', }, }, defaultVariants: { disabled: false }, } );
24-36: 클릭 가능한 div → button으로 전환해 접근성 확보키보드 포커스/Space/Enter 동작,
disabled/aria-*지원을 위해 네이티브 버튼 사용 권장.- return ( - <div - className={boxStyle({ disabled: isDisabled })} - onClick={() => { + return ( + <button + type="button" + className={boxStyle({ disabled: isDisabled })} + disabled={isDisabled} + onClick={() => { if (select === 3 && isDisabled) { setShowPicker(true); } onClick?.(); }} - > + > ... - </div> + </button>추가로,
onClick타입을React.MouseEventHandler<HTMLButtonElement>로 바꾸면 타입 안정성이 좋아집니다.
31-33: TimePicker 오픈 조건 역전됨사용자 설정은 활성 상태에서 열려야 합니다. 현재
select === 3 && isDisabled로 되어 있어 UX가 반대로 동작합니다.- if (select === 3 && isDisabled) { + if (select === 3 && !isDisabled) { setShowPicker(true); }
52-73: 전역 상수(AlarmsType) 직접 변이 + 취소 시 닫힘 불일치
AlarmsType[2].time = ...은 전역 상수 변이로 리렌더 보장이 약하고 사이드이펙트 리스크가 큽니다.onCancel에서setShowPicker(false)가 없어 피커가 닫히지 않을 수 있습니다.다음처럼 로컬 상태를 도입하고 저장/취소 시 상태만 갱신하세요.
- const [showPicker, setShowPicker] = useState(false); + const [showPicker, setShowPicker] = useState(false); + const [customTime, setCustomTime] = useState(''); @@ - {select === 3 && isDisabled && ( + {select === 3 && !isDisabled && ( <> - {AlarmsType[2].time && ( - <p className="caption2-m text-font-gray-3">{AlarmsType[2].time}</p> - )} + {customTime && ( + <p className="caption2-m text-font-gray-3">{customTime}</p> + )} {showPicker && ( <TimePicker onSave={({ hour, minute, meridiem }) => { const formatted = `${meridiem} ${hour}:${minute}`; - AlarmsType[2].time = formatted; - setShowPicker(false); + setCustomTime(formatted); + setShowPicker(false); // 이거 나중에 api 연결때 쓸려고 표시한거.. 그떄 지우겠듬여 console.log('저장된 사용자 알람:', AlarmsType[2].time); }} onCancel={() => { - AlarmsType[2].time = ''; + setShowPicker(false); }} onClick={(e) => e.stopPropagation()} /> )} </> )}상태를 상위 스텝으로 올려서 최종 온보딩 데이터와 함께 제출하는 구조도 고려해주세요.
apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx (1)
12-16: img 대체텍스트 개선 및 로딩 최적화.기존 alt="onboarding"은 정보성이 낮습니다. 스텝 정보를 포함하고, 지연 로딩을 적용하세요.
<img src={story.img} - className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" - alt="onboarding" + className="mb-[1.6rem] mt-[2.4rem] w-[31.2rem]" + alt={`onboarding-step-${step + 1}`} + loading="lazy" + decoding="async" />추가로, 상수에
alt를 넣으셨다면 소비처도 다음처럼 사용하는 것이 이상적입니다(별도 변경안):<img ... alt={story.alt} loading="lazy" decoding="async" />
🧹 Nitpick comments (10)
apps/client/src/constants/alarms.ts (2)
11-15: 네이밍 정리 제안: 데이터 상수에 Type 접미사 지양
AlarmsType는 타입으로 오인될 수 있습니다.ALARM_OPTIONS또는ALARM_PRESETS처럼 의도가 드러나는 이름 권장. 사용처 전반 변경 필요.원하시면 대체 이름으로 리팩터링 패치 생성해드리겠습니다.
1-3: 경로 별칭 일관화 제안(@assets 사용)동일 PR에서 경로 별칭을 쓰고 있으므로, 에셋도 상대경로 대신 별칭을 사용해 유지보수성을 높이세요. 실제 별칭 명은 레포 설정에 맞춰 조정 필요.
-import avatar1 from '../../../../../assets/onBoarding/icons/chippi_morning.svg'; -import avatar2 from '../../../../../assets/onBoarding/icons/chippi_night.svg'; -import avatar3 from '../../../../../assets/onBoarding/icons/chippi_bell.svg'; +import avatar1 from '@assets/onBoarding/icons/chippi_morning.svg'; +import avatar2 from '@assets/onBoarding/icons/chippi_night.svg'; +import avatar3 from '@assets/onBoarding/icons/chippi_bell.svg';별칭 확인 스크립트:
#!/bin/bash # tsconfig* 내 paths 설정 확인 fd -HI -a 'tsconfig*.json' | while read f; do echo "---- $f" jq '.compilerOptions.paths' "$f" 2>/dev/null || rg -n '"paths"' "$f" -n -C2 doneapps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (2)
37-37: img 대체 텍스트와 지연 로딩시맨틱/성능 개선: alt에 타이틀을 사용하고 lazy 로딩을 추가하세요.
- <img src={AlarmsType[select - 1].img} alt="chippi" /> + <img + src={AlarmsType[select - 1].img} + alt={AlarmsType[select - 1].title} + loading="lazy" + />
46-50: 매직 넘버(<= 2) 의존 최소화프리셋/커스텀 구분을 인덱스/범위가 아닌 속성(
kind: 'preset' | 'custom')으로 분리하면 확장성/가독성이 좋아집니다.원하시면
AlarmType에kind추가 및 사용처 리팩터링 패치를 제안드릴게요.apps/client/tsconfig.json (2)
8-11: Vite alias와 TS paths 정합성 점검 (특히 @assets).
vite-tsconfig-paths가 있어 런타임 해상도는 대체로 문제없지만,vite.config.ts의resolve.alias에는@assets가 누락되어 있어 일부 에디터/도구에서 불일치가 생길 수 있습니다. 두 가지 중 하나로 정리 권장:
- 전부
vite-tsconfig-paths에 위임(수동 alias 제거), 또는@assets를 포함해 양쪽을 일치.아래는 두 번째 방법 예시입니다(다른 파일 변경):
// apps/client/vite.config.ts resolve: { alias: { '@': resolve(__dirname, './src'), '@api': resolve(__dirname, './src/api'), '@utils': resolve(__dirname, './src/utils'), '@constants': resolve(__dirname, './src/constants'), '@shared-types': resolve(__dirname, './src/types'), '@components': resolve(__dirname, './src/components'), + '@assets': resolve(__dirname, './src/assets'), }, },필요하시면
rg -n "@assets/" apps/client/src로 실제 사용처를 빠르게 스캔해 드릴게요.
3-13: SVG/환경 타입 오류 예방:vite/client타입 명시 권장.
.svg등 자산 임포트와import.meta.env타입 보장을 위해compilerOptions.types에vite/client추가를 권장합니다(상위 설정에 없다면)."compilerOptions": { "baseUrl": ".", "plugins": [{ "name": "react" }], + "types": ["vite/client"], "paths": {상위
@pinback/typescript-config/react.json에 이미 선언되어 있다면 이 변경은 생략해도 됩니다.apps/client/vite.config.ts (1)
21-30: 경로 alias 일관성:@assets추가 또는 중복 제거로 단순화.현재 TS paths와 Vite alias가 일부 중복·누락 혼재입니다. 유지보수성을 위해:
@assets를 추가해 TS와 동등화, 또는- 수동 alias를 제거하고
vite-tsconfig-paths만 사용.위 방법 중 택1을 권장합니다(예시는
@assets추가).resolve: { alias: { '@': resolve(__dirname, './src'), '@api': resolve(__dirname, './src/api'), '@utils': resolve(__dirname, './src/utils'), '@constants': resolve(__dirname, './src/constants'), '@shared-types': resolve(__dirname, './src/types'), '@components': resolve(__dirname, './src/components'), + '@assets': resolve(__dirname, './src/assets'), }, },apps/client/src/constants/stories.tsx (2)
1-3: 자산 임포트 경로는 alias 사용으로 단순화.깊은 상대경로 대신
@assets를 사용하면 이동/리팩토링 내성이 좋아집니다.-import story1 from '../../../../../assets/onBoarding/story/story1.svg'; -import story2 from '../../../../../assets/onBoarding/story/story2.svg'; -import story3 from '../../../../../assets/onBoarding/story/story3.svg'; +import story1 from '@assets/onBoarding/story/story1.svg'; +import story2 from '@assets/onBoarding/story/story2.svg'; +import story3 from '@assets/onBoarding/story/story3.svg';빌드 시
@assetsalias가 정상 해상되는지 한번만 확인 부탁드립니다.
11-44: 스마트한 대체 텍스트 제공:alt필드 추가.A11y 및 소비처(StoryStep)에서의 일관된 사용을 위해 각 스토리에
alt를 포함하시죠.export const stories: Story[] = [ { img: story1, + alt: '온보딩 스텝 1: 지식 나무 일러스트', text: ( <> 깊고 신비한 숲에는 지식 나무가 있어요. <br /> 지식 나무는 사람들의 잊힌 기록을 도토리 씨앗으로 바꾼답니다. </> ), progress: 30, }, { img: story2, + alt: '온보딩 스텝 2: 도토리 열매가 열린 나무 일러스트', text: ( <> 당신이 정보를 읽고 활용하는 것을 양분삼아, <br /> 지식 나무에는 맛있는 도토리 열매가 열려요. </> ), progress: 60, }, { img: story3, + alt: '온보딩 스텝 3: 다람쥐 치삐와 도토리 만개 숲 일러스트', text: ( <> 다람쥐 치삐는 정보를 활용하지 못해 아직 도토리 만개 숲에 도착하지 못하고 있어요. <br /> 도토리를 모아 치삐가 숲에 닿을 수 있도록 도와주세요! </> ), progress: 100, } ];apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx (1)
7-9: 인덱스 접근 가드(선택).타입으로 0|1|2를 강제해두셨지만, 상위에서 잘못된 값이 들어오는 방어가 필요하다면 간단히 early-return을 둘 수 있습니다.
const story = stories[step]; if (!story) return null;
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/client/src/constants/alarms.ts(1 hunks)apps/client/src/constants/stories.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/FinalStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/header/Header.tsx(1 hunks)apps/client/tsconfig.json(1 hunks)apps/client/vite.config.ts(2 hunks)packages/design-system/src/components/index.ts(1 hunks)packages/design-system/src/icons/iconNames.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
- packages/design-system/src/components/index.ts
- packages/design-system/src/icons/iconNames.ts
- apps/client/src/pages/onBoarding/components/header/Header.tsx
- apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx
- apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx
- apps/client/src/pages/onBoarding/components/funnel/step/FinalStep.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This is used with the tailwindcss/vite plugin.
Applied to files:
apps/client/vite.config.ts
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This single import pulls in all of Tailwind's preflight, components, and utilities in one step and is used with the tailwindcss/vite plugin.
Applied to files:
apps/client/vite.config.ts
🔇 Additional comments (3)
apps/client/vite.config.ts (1)
15-19: vite.config.ts import 및__dirname정의 확인 요청
apps/client/vite.config.ts파일 상단에서resolve를 import한 부분과__dirname이 정의된 부분을 공유해주세요.apps/client/src/constants/stories.tsx (1)
11-44: 데이터 구조화는 좋습니다.단계별 문구/이미지/진행률 분리가 명확해 유지보수에 유리합니다.
apps/client/src/pages/onBoarding/components/funnel/step/StoryStep.tsx (1)
3-5: Props 스키마가 명확해 좋습니다.
step: 0 | 1 | 2제한으로 런타임 오류 가능성을 낮췄습니다.
| export interface AlarmType { | ||
| img: string; | ||
| title: string; | ||
| time: string; | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
상수(프리셋) 변이 방지: 필드를 readonly로 고정하세요
컴포넌트에서 AlarmsType[2].time = ...와 같이 변경 시도가 보입니다. 타입 레벨에서 변이를 차단하면 의도치 않은 전역 상태 오염을 예방할 수 있습니다.
-export interface AlarmType {
- img: string;
- title: string;
- time: string;
-}
+export interface AlarmType {
+ readonly img: string;
+ readonly title: string;
+ readonly time: string;
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface AlarmType { | |
| img: string; | |
| title: string; | |
| time: string; | |
| } | |
| export interface AlarmType { | |
| readonly img: string; | |
| readonly title: string; | |
| readonly time: string; | |
| } |
🤖 Prompt for AI Agents
In apps/client/src/constants/alarms.ts around lines 5 to 10, the AlarmType
interface allows mutation of its properties which enables code like
AlarmsType[2].time = ..., so change the interface fields to be readonly (e.g.,
mark img, title, time as readonly) to enforce immutability at the type level;
also ensure any exported presets use a readonly array type
(ReadonlyArray<AlarmType> or declare the constant with an immutable assertion)
so consumers cannot mutate elements.
| export interface Story { | ||
| img: string; | ||
| text: React.ReactNode; | ||
| progress: number; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
React.ReactNode 직접 참조는 타입 오류 소지 → 타입 임포트로 전환.
모듈 스코프에서 React 네임스페이스를 직접 참조하면 TS 설정에 따라 “Cannot find namespace 'React'”가 날 수 있습니다. 안전하게 타입만 임포트해 사용하세요.
+import type { ReactNode } from 'react';
export interface Story {
img: string;
- text: React.ReactNode;
+ text: ReactNode;
progress: number;
+ alt: string;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export interface Story { | |
| img: string; | |
| text: React.ReactNode; | |
| progress: number; | |
| } | |
| import type { ReactNode } from 'react'; | |
| export interface Story { | |
| img: string; | |
| text: ReactNode; | |
| progress: number; | |
| alt: string; | |
| } |
🤖 Prompt for AI Agents
In apps/client/src/constants/stories.tsx around lines 5 to 9, the interface
references React.ReactNode directly which can cause "Cannot find namespace
'React'" errors; change it to import the type and use it: add "import type {
ReactNode } from 'react';" at the top of the file and replace React.ReactNode
with ReactNode in the Story interface.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
apps/client/src/constants/alarms.ts (2)
5-9: 상수(프리셋) 변이 방지: 필드를 readonly로 고정하고, 결측 시간은 null로 명시하세요컴포넌트에서 프리셋을 직접 수정하는 시도가 있어 보입니다. 타입 레벨에서 변이를 차단하고, 빈 문자열('') 대신 null을 써서 “미설정” 상태를 명확히 표현하는 편이 안전합니다.
export interface AlarmType { - img: string; - title: string; - time: string; + readonly img: string; + readonly title: string; + readonly time: string | null; }
11-15: 배열/요소 불변성 보장: ReadonlyArray(모든 TS) 또는 satisfies+as const(TS≥4.9)프리셋 배열 자체와 각 요소를 모두 readonly로 만들어 외부에서 인덱스 재할당/속성 수정을 막아주세요.
옵션 A — TypeScript 버전 무관(보수적):
-export const AlarmsType: AlarmType[] = [ +export const AlarmsType: ReadonlyArray<Readonly<AlarmType>> = [ { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, - { img: avatar3, title: '사용자 설정', time: '' }, + { img: avatar3, title: '사용자 설정', time: null }, -]; +];옵션 B — TS 4.9+ 권장(문자열 리터럴 보존 + 불변성):
-export const AlarmsType: AlarmType[] = [ +export const AlarmsType = [ { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, - { img: avatar3, title: '사용자 설정', time: '' }, -]; + { img: avatar3, title: '사용자 설정', time: null }, +] as const satisfies ReadonlyArray<Readonly<AlarmType>>;TS 버전 확인 스크립트:
#!/bin/bash # 리포지토리 내 package.json들에서 TypeScript 버전 조사 set -euo pipefail fd -a package.json -E node_modules | while read -r p; do ver=$(jq -r '.devDependencies.typescript // .dependencies.typescript // empty' "$p") if [ -n "$ver" ]; then echo "$p => typescript@$ver"; fi done
🧹 Nitpick comments (2)
apps/client/src/constants/alarms.ts (1)
11-15: 시간 표현 분리 제안: 표시 문자열과 머신-리더블 값을 분리국제화(i18n)·비교·알람 스케줄링을 위해 표시용 time('오전 9시')과 값용 timeValue('09:00' | null 등)를 구분하는 것을 권장합니다. 렌더링 시 Intl.DateTimeFormat/dayjs로 포맷하면 로케일 전환도 용이합니다.
apps/client/vite.config.ts (1)
25-25: 소소한 포맷팅 불일치(Prettier 기준 공백 누락)가독성 및 일관성 차원에서 공백을 추가하세요.
- '@assets':resolve(__dirname,'./src/assets'), + '@assets': resolve(__dirname, './src/assets'),
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/client/src/constants/alarms.ts(1 hunks)apps/client/src/constants/stories.tsx(1 hunks)apps/client/src/pages/onBoarding/OnBoarding.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx(1 hunks)apps/client/src/pages/onBoarding/components/header/Header.tsx(1 hunks)apps/client/vite.config.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/client/src/constants/stories.tsx
- apps/client/src/pages/onBoarding/OnBoarding.tsx
- apps/client/src/pages/onBoarding/components/header/Header.tsx
- apps/client/src/pages/onBoarding/components/funnel/step/MacStep.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This is used with the tailwindcss/vite plugin.
Applied to files:
apps/client/vite.config.ts
📚 Learning: 2025-07-04T10:12:01.690Z
Learnt from: constantly-dev
PR: Pinback-Team/pinback-client#10
File: apps/landing/src/index.css:1-1
Timestamp: 2025-07-04T10:12:01.690Z
Learning: In TailwindCSS v4, the import "tailwindcss"; syntax is the new recommended approach that replaces the three separate tailwind directives (tailwind base;, tailwind components;, tailwind utilities;). This single import pulls in all of Tailwind's preflight, components, and utilities in one step and is used with the tailwindcss/vite plugin.
Applied to files:
apps/client/vite.config.ts
⏰ 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)
apps/client/src/constants/alarms.ts (1)
1-3: SVG 모듈 타입 선언 확인 필요샌드박스 검증(rg)이 "No files were searched"를 반환해 자동 확인에 실패했습니다. 레포에 아래와 같은 *.svg 모듈 선언(d.ts)이 있는지 직접 확인하거나 없으면 추가하세요.
declare module "*.svg" { const src: string; export default src }
권장 위치: src/types/svg.d.ts 또는 types/global.d.ts
apps/client/vite.config.ts (1)
21-31: Vite alias ↔ tsconfig paths 불일치 위험 — tsconfig를 단일 소스로 정렬하고 Vite 중복 alias 제거
- 요약: apps/client/vite.config.ts에 '@', '@api', '@utils', '@shared-types', '@components' 등 별칭이 resolve.alias로 중복 선언되어 있습니다. 타입 검사기와 에디터는 tsconfig의 paths를 기준으로 하므로 모든 별칭을 apps/client/tsconfig*.json의 compilerOptions.paths로 통합하고 Vite의 resolve.alias 중복 선언을 제거하세요.
- 조치(간단 예시): apps/client/tsconfig.json에 다음 paths를 추가한 뒤 apps/client/vite.config.ts의 resolve.alias 블록을 제거/주석 처리하세요.
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@api/*": ["src/api/*"], "@assets/*": ["src/assets/*"], "@utils/*": ["src/utils/*"], "@constants/*": ["src/constants/*"], "@shared-types/*": ["src/types/*"], "@components/*":["src/components/*"] } } }
- 검증: 자동 검사 스크립트가 sandbox에서 실패했습니다(오류: "/bin/bash: -c: line 7: conditional binary operator expected"). 로컬에서 DECLARED(=tsconfig paths) vs USED(코드 내 사용) 비교 스크립트를 실행하거나, 직접 apps/client/tsconfig*.json과 apps/client/vite.config.ts를 확인해 일치 여부를 확인해 주세요.
| export const AlarmsType: AlarmType[] = [ | ||
| { img: avatar1, title: '아침형 치삐', time: '오전 9시' }, | ||
| { img: avatar2, title: '저녁형 치삐', time: '오후 8시' }, | ||
| { img: avatar3, title: '사용자 설정', time: '' }, | ||
| ]; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
프리셋은 ‘상태’가 아닙니다: 사용자 선택 값(시간)은 컴포넌트/스토어 상태로 관리하세요
모듈 상수(AlarmsType)를 직접 변경하면 전역 공유로 인해 리렌더/다중 인스턴스/스토리북 병렬 실행 시 예기치 않은 사이드이펙트가 납니다. 프리셋은 불변 상수로 두고, 선택/수정은 로컬 상태(useState)나 전역 스토어(Zustand/Redux 등)로 분리하세요.
다음 명령으로 상수 변이(write) 사용처를 확인해 보세요:
🏁 Script executed:
#!/bin/bash
# AlarmsType 요소의 time을 대입(=)하는 코드 탐지
rg -nP 'AlarmsType\s*\[\s*\d+\s*\]\s*\.time\s*=' -C2
rg -nP 'AlarmsType\s*\[\s*.*?\s*\]\s*\.time\s*=' -C2Length of output: 2277
AlarmsType은 불변으로 유지 — 전역 상수 직접 변이 제거하고 사용자 선택은 상태로 관리하세요.
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx에서 AlarmsType[2].time에 직접 할당이 발견됩니다 (라인 62: AlarmsType[2].time = formatted, 라인 67: AlarmsType[2].time = ''). 프리셋(AlarmsType)은 불변 상수로 두고, 사용자 선택/수정은 컴포넌트 useState 또는 전역 스토어(Zustand/Redux 등)로 이동해 저장·갱신하세요.
🤖 Prompt for AI Agents
In apps/client/src/constants/alarms.ts around lines 11-15, the AlarmsType array
is intended as a constant preset but is currently being mutated elsewhere; stop
direct mutation and treat this file as read-only. In
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx (where you
currently do AlarmsType[2].time = formatted at line 62 and AlarmsType[2].time =
'' at line 67) introduce a local state (useState) or use your global store
(Zustand/Redux) to hold the user-selected time, initialize it from
AlarmsType[2].time, update that state when the user picks or clears a time, and
read from that state for rendering instead of writing back into AlarmsType;
ensure AlarmsType remains unchanged.
| import tailwindcss from '@tailwindcss/vite'; | ||
| import react from '@vitejs/plugin-react-swc'; | ||
| import { defineConfig } from 'vite'; | ||
| import { resolve } from 'path'; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
ESM 환경에서 __dirname 사용 시 런타임 오류 가능 — node:url/node:path 패턴으로 교체 권장
프로젝트가 ESM("type": "module")이면 __dirname이 정의되지 않아 Vite 설정 로드 시 즉시 크래시합니다. ESM-세이프한 방식으로 변경하세요.
적용 diff(해당 라인 교체):
-import { resolve } from 'path';
+import { fileURLToPath } from 'node:url';
+import { resolve } from 'node:path';그리고 import 아래에 다음을 추가:
const __dirname = fileURLToPath(new URL('.', import.meta.url));ESM 여부 빠른 점검 스크립트:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package type (apps/client) =="
if [[ -f apps/client/package.json ]]; then
jq -r '.type // "(not set)"' apps/client/package.json
else
echo "apps/client/package.json not found"
fi
echo "== tsconfig module (apps/client) =="
if [[ -f apps/client/tsconfig.json ]]; then
jq -r '.compilerOptions.module // "(not set)"' apps/client/tsconfig.json
else
echo "apps/client/tsconfig.json not found"
fiLength of output: 85
ESM 환경 — apps/client/vite.config.ts에서 __dirname 대체 필요
검증: apps/client/package.json의 "type"이 "module"로 확인되어 ESM 환경에서 __dirname 사용 시 런타임 오류가 발생합니다. 아래로 교체하세요.
파일: apps/client/vite.config.ts
-import { resolve } from 'path';
+import { fileURLToPath } from 'node:url';
+import { resolve } from 'node:path';import들 아래에 추가:
const __dirname = fileURLToPath(new URL('.', import.meta.url));🤖 Prompt for AI Agents
In apps/client/vite.config.ts around lines 4 to 4, the repo is ESM (package.json
"type":"module") so using __dirname will fail at runtime; to fix, add an import
for fileURLToPath from 'url' and then define __dirname using fileURLToPath(new
URL('.', import.meta.url)) directly below the existing imports, and update any
__dirname references to use that value.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (2)
35-36: ‘다음’ 버튼의 무의미한 비활성화 조건(step === 6) 제거 + 전환 중 다중 클릭 방지 가드 추가step은 0–5 범위라
step === 6은 영원히 false입니다(이전 코멘트와 동일 이슈). 동시에 애니메이션 중 중복 클릭을 막기 위해isAnimating가드를 두는 편이 안전합니다.@@ - const [step, setStep] = useState(0); - const [direction, setDirection] = useState(0); + const [step, setStep] = useState(0); + const [direction, setDirection] = useState(0); + const [isAnimating, setIsAnimating] = useState(false); @@ <motion.div key={step} custom={direction} variants={variants} initial="slideIn" animate="slideCenter" exit="slideOut" transition={{ duration: 0.4 }} + onAnimationStart={() => setIsAnimating(true)} + onAnimationComplete={() => setIsAnimating(false)} className="flex h-full flex-col items-center" > @@ <Button variant="primary" size="medium" - isDisabled={step === 0} + isDisabled={step === 0 || isAnimating} className="w-[4.8rem]" onClick={prevStep} > @@ <Button variant="primary" size="medium" - isDisabled={step === 6} + isDisabled={isAnimating} className="ml-auto w-[4.8rem]" onClick={nextStep} >Also applies to: 95-107, 111-121, 123-131
35-36: step을 의미 있는 string union으로 전환해 가독성/안정성 개선숫자 step은 맥락 없이 읽기 어렵습니다. ‘story-0|1|2’, ‘alarm’, ‘mac’, ‘final’ 같은 union으로 전환하고, story 인덱스는 별도로 관리하는 구조를 고려해주세요. 이전 논의와도 일치합니다.
예시(개념):
type Step = 'story-0' | 'story-1' | 'story-2' | 'alarm' | 'mac' | 'final'; // const [step, setStep] = useState<Step>('story-0');Also applies to: 47-63
🧹 Nitpick comments (1)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx (1)
41-45: UA 판별 로직 보강: ‘Macintosh’ 토큰 포함데스크톱 Safari의 UA는 보통 ‘Macintosh’를 포함합니다. 정규식으로 단순화해 누락을 줄이세요.
- const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('mac os') || ua.includes('iphone') || ua.includes('ipad')) { - setIsMac(true); - } + const ua = navigator.userAgent; + const isAppleUA = /Macintosh|Mac OS|iPhone|iPad/i.test(ua); + if (isAppleUA) setIsMac(true);
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/client/src/pages/onBoarding/components/funnel/AlarmBox.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx(1 hunks)apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/client/src/pages/onBoarding/components/funnel/step/AlarmStep.tsx
- apps/client/src/pages/onBoarding/components/funnel/AlarmBox.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:
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
| return <FinalStep />; | ||
| case 5: | ||
| if (isMac) return <FinalStep />; | ||
| return null; | ||
| default: |
There was a problem hiding this comment.
비-Apple(Windows 등) 환경에서 step 5가 공백으로 렌더되고, ‘다음’을 두 번 눌러야 홈으로 이동하는 버그
step 5가 !isMac일 때 null을 반환하고, 리다이렉트는 step 5에서만 발생합니다. step 4에서 step 5로 이동(첫 클릭) 후 빈 화면이 표시되고, 한 번 더 눌러야 이동됩니다. 아래처럼 비Mac은 step 4에서 바로 리다이렉트하고, 안전망으로 step 5는 항상 FinalStep을 렌더하도록 수정하세요.
@@
- case 5:
- if (isMac) return <FinalStep />;
- return null;
+ case 5:
+ return <FinalStep />;
@@
- const nextStep = () => {
+ const nextStep = () => {
if (step === 3) {
// 이거 이후에 api 붙일 자리 표시임! console.log('선택된 알람:', AlarmsType[alarmSelected - 1].time);
}
- if (step < 5) {
- setDirection(1);
- setStep((prev) => prev + 1);
- } else if (step === 5) {
- window.location.href = '/';
- }
+ const shouldRedirect = (!isMac && step === 4) || (isMac && step === 5);
+ if (shouldRedirect) {
+ window.location.href = '/';
+ return;
+ }
+ setDirection(1);
+ setStep((prev) => prev + 1);
};Also applies to: 65-75
🤖 Prompt for AI Agents
In apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx around lines
56-60 (and similarly 65-75), the switch currently returns null for case 5 on
non-Mac causing a blank render and requiring a second click to trigger the
redirect; change the flow so non-Mac users are redirected earlier and case 5
never returns null. Specifically: update case 4 to perform the non-Mac redirect
immediately (same redirect logic used for Mac) so clicking Next from step 4
moves straight to home, and change case 5 to always render FinalStep as a safety
net (do not return null). Apply the same fixes in the repeated block at lines
65-75.

📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
1. 이번 온보딩은 퍼널 구조가 아닌, 멀티 스텝 ui구조!
퍼널처럼 “조건 분기마다 다른 path”를 타는 게 아니라, 공통된 플로우 안에서 step 상태 별 컨텐츠만 교체하는 구조입니다!
[이유]
2. 애니메이션 적용 (Framer Motion 활용)
[overflow-hidden 문제]
기본적으로 MainCard는 깔끔하게 보여주기 위해 overflow-hidden 유지했는데,,
단, TimePicker는 카드 밖으로 자연스럽게 보여야 해서 hidden되면 안됨..
→ "사용자 설정" 선택 시에만 overflow-visible로 잠시 분기 처리
3. MacOS 분기 로직 추가
navigator.userAgent 기반으로 macOS, iPhone, iPad 판별 후 state(isMac)에 저장하여 분기하였습니당
📷 Screenshot
Summary by CodeRabbit
신기능
아이콘
Summary by CodeRabbit