Skip to content

Comments

Feat(client, design-system): 온보딩 직무 step UI 추가, checkbox 구현#257

Merged
constantly-dev merged 11 commits intodevelopfrom
feat/#256/onboarding-job-step
Feb 20, 2026
Merged

Feat(client, design-system): 온보딩 직무 step UI 추가, checkbox 구현#257
constantly-dev merged 11 commits intodevelopfrom
feat/#256/onboarding-job-step

Conversation

@constantly-dev
Copy link
Member

@constantly-dev constantly-dev commented Feb 19, 2026

📌 Related Issues

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

📄 Tasks

  • 온보딩 직무 step 추가
  • checkbox components 구현

⭐ PR Point (To Reviewer)

📷 Screenshot

Summary by CodeRabbit

  • 새로운 기능

    • 온보딩에 직업 선택 단계 추가(기획자, 디자이너, 프론트엔드, 백엔드).
    • 직업 데이터 공유 동의 체크박스 추가 — 미동의 시 다음 버튼 비활성화.
    • 온보딩 카드 레이아웃에 폭/크기 변형 추가 및 JOB 단계에서 와이드 뷰 적용.
  • 설명서

    • 체크박스 컴포넌트용 Storybook 예시 추가.

@vercel
Copy link

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
pinback-client-client Ready Ready Preview, Comment Feb 20, 2026 2:39am
pinback-client-landing Ready Ready Preview, Comment Feb 20, 2026 2:39am

@github-actions github-actions bot added the feat 기능 개발하라 개발 달려라 달려 label Feb 19, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Warning

Rate limit exceeded

@constantly-dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 3 seconds before requesting another review.

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

Walkthrough

온보딩에 직무 선택(Job) 단계를 추가하고(JobStep, MainCard·onboardingSteps 변경), 재사용 가능한 Checkbox 컴포넌트를 디자인 시스템에 도입했으며, Google OAuth 콜백 로직을 v3 엔드포인트와 redirectUri 포함 페이로드로 업데이트했습니다. (50단어 이내)

Changes

Cohort / File(s) Summary
온보딩 — 메인 통합
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx, apps/client/src/pages/onBoarding/constants/onboardingSteps.ts
Step.JOB 추가 및 stepOrder에 삽입. JobStep을 lazy 로드해 JOB 단계에서 렌더링. CardStyle에 wide size 추가 및 JOB일 때 wide 적용. NEXT 버튼은 JOB 단계에서 동의(false)면 비활성화. 레이아웃 너비 조정.
온보딩 — JobStep 구현
apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx
신규 JobStep 컴포넌트 추가: 4개 직무 카드, 라디오그룹 역할/aria, 동의 체크박스. JobKey 타입 및 JobStepProps(selectedJob, onSelectJob, agreeChecked, onAgreeChange) 제공. 제어/비제어 사용 지원 및 CVA 기반 스타일 적용.
디자인 시스템 — Checkbox 추가 및 스토리
packages/design-system/src/components/checkbox/Checkbox.tsx, packages/design-system/src/components/checkbox/Checkbox.stories.tsx, packages/design-system/src/components/index.ts
Checkbox 컴포넌트 추가(제어/비제어, size/selected/disabled variants, 접근성 처리). Storybook 스토리 추가. components 인덱스에 re-export 추가.
OAuth 콜백 업데이트
apps/client/src/pages/onBoarding/GoogleCallback.tsx
redirectUri 상수 도입(PROD/DEV env 선택). Google 로그인 핸들러가 /api/v3/auth/google 엔드포인트로 코드와 uri를 전송하도록 변경.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

🛠️ Feature

Suggested reviewers

  • jllee000
  • jjangminii

Poem

🐰 깡충 깡충 새 단계로 왔어요,
체크 하나로 길이 보이고, 카드들 춤추네,
기획·디자인·프론트·백 중 택해봐요,
다음 버튼엔 동의가 필요하답니다,
당근 한 입 축하해요 🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR은 #256(온보딩 직무 선택 Step 추가)을 충족하지만, 링크된 #25(Progress bar 구현)의 요구사항은 포함되지 않았습니다. PR 설명에서 #25를 제거하거나, 해당 이슈의 요구사항(Progress 컴포넌트 구현)을 완료해야 합니다.
Out of Scope Changes check ❓ Inconclusive GoogleCallback.tsx의 Google 로그인 API 업데이트는 #256 범위와 직접적인 관계가 불명확합니다. Google 로그인 API 변경사항이 온보딩 job step과의 관련성 또는 필요성에 대해 설명해주세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 온보딩 직무 step UI 추가와 checkbox 구현이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 필수 섹션(Related Issues, Tasks)을 포함하고 있으며, 작업 내용이 명확하게 요약되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#256/onboarding-job-step

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

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Feb 19, 2026

✅ Storybook chromatic 배포 확인:
🐿️ storybook

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx (2)

64-72: 의존성 없는 useMemo → 모듈 수준 상수로 추출 권장

jobs 배열은 컴포넌트 외부 상수로 분리하면 useMemo 오버헤드 없이 동일한 효과를 얻을 수 있습니다. 아울러 key 필드를 JobKey로 명시하면 하위 코드에서 as JobKey 단언(type assertion)도 제거할 수 있습니다.

♻️ 제안된 리팩터
+const JOBS: { key: JobKey; label: string }[] = [
+  { key: 'planner',  label: '기획자' },
+  { key: 'designer', label: '디자이너' },
+  { key: 'frontend', label: '프론트엔드' },
+  { key: 'backend',  label: '백엔드' },
+];

 const JobStep = ({ ... }: JobStepProps) => {
-  const jobs = useMemo(
-    () => [
-      { key: 'planner',  label: '기획자' },
-      { key: 'designer', label: '디자이너' },
-      { key: 'frontend', label: '프론트엔드' },
-      { key: 'backend',  label: '백엔드' },
-    ],
-    []
-  );

그리고 map 내부에서:

-            onClick={() => handleSelect(job.key as JobKey)}
-            className={jobCardStyle({ selected: isSelected })}
+            onClick={() => handleSelect(job.key)}
-                <JobIcon type={job.key as JobKey} />
+                <JobIcon type={job.key} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` around
lines 64 - 72, The jobs array defined with useMemo in JobStep.tsx should be
moved to a module-level constant to avoid unnecessary memoization overhead:
replace the useMemo block with a top-level exported/const JOBS (or similar) and
type it explicitly as Array<{ key: JobKey; label: string }>; update all
references in the JobStep component to use this JOBS constant and remove any as
JobKey assertions inside map/rendering since the keys are now strongly typed.

74-79: handleSelect 조건식에서 !onSelectJob 분기 불필요

controlled/uncontrolled 패턴의 관용적 기준은 selectedJob === undefined 여부입니다. !onSelectJob 조건을 추가하면 "콜백만 주고 값은 주지 않은" 경우에도 내부 상태를 업데이트해 예외적인 split-brain 상태를 만들 수 있습니다.

♻️ 제안된 수정
 const handleSelect = (job: JobKey) => {
   onSelectJob?.(job);
-  if (!onSelectJob || selectedJob === undefined) {
+  if (selectedJob === undefined) {
     setInternalJob(job);
   }
 };

 const handleAgreeChange = (checked: boolean) => {
   onAgreeChange?.(checked);
-  if (!onAgreeChange || agreeChecked === undefined) {
+  if (agreeChecked === undefined) {
     setInternalAgree(checked);
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` around
lines 74 - 79, The handleSelect function currently updates internal state when
either onSelectJob is missing or selectedJob is undefined, which can produce
split-brain when a callback is provided without a value; change the conditional
to only check selectedJob === undefined so internal state (setInternalJob) is
updated only in uncontrolled mode; keep the existing optional call to
onSelectJob?.(job) but remove the !onSelectJob check so handleSelect uses
selectedJob as the single source for controlled vs uncontrolled behavior.
packages/design-system/src/components/index.ts (1)

4-5: Checkbox export가 알파벳 순서에서 Chip 뒤에 위치

"Checkbox" < "Chip" (알파벳 순) 이므로 Chip 앞에 오도록 순서를 조정하는 것이 barrel 파일의 일관성을 유지합니다.

♻️ 제안된 수정
 export { default as Card } from './card/Card';
+export { default as Checkbox } from './checkbox/Checkbox';
 export { default as Chip } from './chip/Chip';
-export { default as Checkbox } from './checkbox/Checkbox';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/components/index.ts` around lines 4 - 5, The
barrel export order is not alphabetized: move the export for Checkbox so it
appears before Chip in the components index (update the exports for "Checkbox"
and "Chip" in components/index.ts), ensuring the exported symbols are listed in
alphabetical order (e.g., export { default as Checkbox } ... appears above
export { default as Chip } ...).
packages/design-system/src/components/checkbox/Checkbox.stories.tsx (1)

43-45: Medium 스토리가 Selected와 실질적으로 중복 — Small 스토리 누락

meta의 default args가 이미 size: 'medium'이므로 Medium 스토리(size: 'medium', isSelected: true)는 Selected 스토리(isSelected: true)와 시각적으로 동일합니다. 대신 아직 문서화되지 않은 Small 사이즈 스토리를 추가하는 것이 API 커버리지 측면에서 더 유용합니다.

♻️ 제안된 수정
-export const Medium: Story = {
-  args: { size: 'medium', isSelected: true },
-};
+export const Small: Story = {
+  args: { size: 'small', isSelected: false },
+};
+
+export const SmallSelected: Story = {
+  args: { size: 'small', isSelected: true },
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/components/checkbox/Checkbox.stories.tsx` around
lines 43 - 45, The Medium story duplicates Selected because meta.default args
already set size: 'medium'; replace or remove the redundant export Medium and
add a new Small story export (e.g., export const Small: Story) that sets args: {
size: 'small', isSelected: true } so the small-size checkbox is covered; keep
Selected as-is and ensure meta/default args remain unchanged so stories use the
correct defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx`:
- Line 68: The state jobShareAgree is initialized to true which pre-checks
consent and effectively forces agreement via the isDisabled={step === Step.JOB
&& !jobShareAgree} check; change the default to false so users must opt-in
(update useState usage for jobShareAgree and setJobShareAgree), and verify
downstream logic that references jobShareAgree (the isDisabled condition and any
submit handlers in the MainCard component) still behaves correctly or instead
allow advancing without consent while gating only the consent-dependent
features; ensure no other code assumes a default true value.
- Around line 140-146: MainCard currently doesn't pass selected job state into
JobStep nor include it in the signup payload; add a state in MainCard (e.g.,
const [selectedJob, setSelectedJob] = useState<JobKey>('planner')) and pass
selectedJob and onSelectJob={setSelectedJob} into the <JobStep ... /> call
(alongside existing jobShareAgree and setJobShareAgree), then include
selectedJob in the postSignData payload (e.g., postSignData({ email,
remindDefault, fcmToken, job: selectedJob }, ...)) so the chosen job is
preserved across unmounts and sent to the backend.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx`:
- Around line 57-58: The checkbox state internalAgree is defaulting to true
(useState(true)) which creates a pre-checked opt-out pattern inappropriate for
personal data consent; change the initializer in the JobStep component to
useState(false) (update internalAgree and any use of setInternalAgree), and then
update the flow in MainCard.tsx to allow progressing without consent or to
explicitly disable the feature when internalAgree is false so that consent is
opt-in and not required by implicit pre-check.

In `@packages/design-system/src/components/checkbox/Checkbox.tsx`:
- Around line 1-3: The file uses the React namespace types
(React.InputHTMLAttributes and React.ChangeEvent) but doesn't import React into
the type scope; add a type-only import for the React namespace (e.g., import
type React from 'react') at the top of the file so
React.InputHTMLAttributes<HTMLInputElement> and
React.ChangeEvent<HTMLInputElement> in the CheckboxProps/Checkbox component
resolve without TS errors.

---

Nitpick comments:
In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx`:
- Around line 64-72: The jobs array defined with useMemo in JobStep.tsx should
be moved to a module-level constant to avoid unnecessary memoization overhead:
replace the useMemo block with a top-level exported/const JOBS (or similar) and
type it explicitly as Array<{ key: JobKey; label: string }>; update all
references in the JobStep component to use this JOBS constant and remove any as
JobKey assertions inside map/rendering since the keys are now strongly typed.
- Around line 74-79: The handleSelect function currently updates internal state
when either onSelectJob is missing or selectedJob is undefined, which can
produce split-brain when a callback is provided without a value; change the
conditional to only check selectedJob === undefined so internal state
(setInternalJob) is updated only in uncontrolled mode; keep the existing
optional call to onSelectJob?.(job) but remove the !onSelectJob check so
handleSelect uses selectedJob as the single source for controlled vs
uncontrolled behavior.

In `@packages/design-system/src/components/checkbox/Checkbox.stories.tsx`:
- Around line 43-45: The Medium story duplicates Selected because meta.default
args already set size: 'medium'; replace or remove the redundant export Medium
and add a new Small story export (e.g., export const Small: Story) that sets
args: { size: 'small', isSelected: true } so the small-size checkbox is covered;
keep Selected as-is and ensure meta/default args remain unchanged so stories use
the correct defaults.

In `@packages/design-system/src/components/index.ts`:
- Around line 4-5: The barrel export order is not alphabetized: move the export
for Checkbox so it appears before Chip in the components index (update the
exports for "Checkbox" and "Chip" in components/index.ts), ensuring the exported
symbols are listed in alphabetical order (e.g., export { default as Checkbox }
... appears above export { default as Chip } ...).

const [userEmail, setUserEmail] = useState('');
const [remindTime, setRemindTime] = useState('09:00');
const [fcmToken, setFcmToken] = useState<string | null>(null);
const [jobShareAgree, setJobShareAgree] = useState(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

jobShareAgree 기본값 true + 미동의 시 진행 불가 = 강제 동의 패턴

jobShareAgreetrue로 초기화되어 동의 체크박스가 사전 선택된 상태로 표시되고, 사용자가 이를 해제하면 isDisabled={step === Step.JOB && !jobShareAgree}에 의해 다음 버튼이 비활성화됩니다. 이는 사실상 "동의하지 않으면 사용 불가"인 강제 동의 구조로, GDPR 제7조 4항의 동의 번들링 금지 원칙(개인정보 공유 동의를 서비스 이용의 조건으로 강제)에 위반될 수 있습니다.

권장 수정:

  • 기본값을 false로 변경하여 사용자가 명시적으로 동의를 선택(opt-in)하도록 설계
  • 또는, 동의 없이도 다음 단계로 진행 가능하게 하고 단순히 기능을 비활성화하는 방식 고려
🛡️ 제안된 수정
- const [jobShareAgree, setJobShareAgree] = useState(true);
+ const [jobShareAgree, setJobShareAgree] = useState(false);
- isDisabled={step === Step.JOB && !jobShareAgree}
+ isDisabled={false}  // 동의는 선택 사항으로 처리

Also applies to: 263-263

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx` at line 68,
The state jobShareAgree is initialized to true which pre-checks consent and
effectively forces agreement via the isDisabled={step === Step.JOB &&
!jobShareAgree} check; change the default to false so users must opt-in (update
useState usage for jobShareAgree and setJobShareAgree), and verify downstream
logic that references jobShareAgree (the isDisabled condition and any submit
handlers in the MainCard component) still behaves correctly or instead allow
advancing without consent while gating only the consent-dependent features;
ensure no other code assumes a default true value.

Comment on lines +140 to +146
case Step.JOB:
return (
<JobStep
agreeChecked={jobShareAgree}
onAgreeChange={setJobShareAgree}
/>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

선택된 직무(job)가 API 호출에 포함되지 않아 기능 목적이 불완전

JobStepselectedJob/onSelectJob이 전달되지 않아 직무 선택 결과가 JobStep 내부 상태에만 저장되고, 다음 단계로 이동(컴포넌트 언마운트) 시 소실됩니다. 회원가입 시 호출되는 postSignData{ email, remindDefault, fcmToken }만 전달하므로 선택된 직무 정보는 백엔드로 전송되지 않습니다.

직무 데이터가 온보딩 완료 시 실제로 필요하다면 MainCard 수준에서 상태로 관리하고 API 페이로드에 포함해야 합니다.

// MainCard 내 상태 추가
const [selectedJob, setSelectedJob] = useState<JobKey>('planner');

// JobStep에 전달
<JobStep
  selectedJob={selectedJob}
  onSelectJob={setSelectedJob}
  agreeChecked={jobShareAgree}
  onAgreeChange={setJobShareAgree}
/>

// postSignData 페이로드에 포함
postSignData({ email: userEmail, remindDefault: remindTime, fcmToken, job: selectedJob }, ...);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx` around lines
140 - 146, MainCard currently doesn't pass selected job state into JobStep nor
include it in the signup payload; add a state in MainCard (e.g., const
[selectedJob, setSelectedJob] = useState<JobKey>('planner')) and pass
selectedJob and onSelectJob={setSelectedJob} into the <JobStep ... /> call
(alongside existing jobShareAgree and setJobShareAgree), then include
selectedJob in the postSignData payload (e.g., postSignData({ email,
remindDefault, fcmToken, job: selectedJob }, ...)) so the chosen job is
preserved across unmounts and sent to the backend.

Comment on lines +57 to +58
const [internalJob, setInternalJob] = useState<JobKey>(defaultJob);
const [internalAgree, setInternalAgree] = useState(true);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

internalAgree 기본값 true는 사전 동의(pre-checked) 패턴 — 개인정보 처리 동의로는 부적절

동의 체크박스가 기본적으로 선택된 상태(true)로 초기화됩니다. 해당 동의의 내용("내 Google 이름과 함께 다른 사용자에게 추천")은 개인정보(식별자) 공유에 해당하므로, GDPR 제7조 및 국내 개인정보보호법에 따라 명시적이고 능동적인 동의(opt-in)를 요구합니다. 사전 체크(opt-out) 방식은 유효한 동의로 인정되지 않으며, MainCard.tsx에서 미동의 시 다음 단계 진행이 불가능한 구조와 결합되면 사실상 강제 동의가 됩니다.

기본값을 false로 변경하고, 동의 없이도 진행 가능하도록(또는 해당 기능을 비활성화) 설계를 재검토하는 것을 권장합니다.

🛡️ 제안된 수정
- const [internalAgree, setInternalAgree] = useState(true);
+ const [internalAgree, setInternalAgree] = useState(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.

Suggested change
const [internalJob, setInternalJob] = useState<JobKey>(defaultJob);
const [internalAgree, setInternalAgree] = useState(true);
const [internalJob, setInternalJob] = useState<JobKey>(defaultJob);
const [internalAgree, setInternalAgree] = useState(false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` around
lines 57 - 58, The checkbox state internalAgree is defaulting to true
(useState(true)) which creates a pre-checked opt-out pattern inappropriate for
personal data consent; change the initializer in the JobStep component to
useState(false) (update internalAgree and any use of setInternalAgree), and then
update the flow in MainCard.tsx to allow progressing without consent or to
explicitly disable the feature when internalAgree is false so that consent is
opt-in and not required by implicit pre-check.

Comment on lines 1 to 3
import { cva } from 'class-variance-authority';
import { useId, useState } from 'react';
import { cn } from '../../lib/utils';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Checkbox.tsx 파일 전체 내용 확인 (라인 번호와 함께)
cat -n packages/design-system/src/components/checkbox/Checkbox.tsx

Repository: Pinback-Team/pinback-client

Length of output: 4053


🏁 Script executed:

# tsconfig 설정 확인
fd 'tsconfig' --type f -x cat -n {}

Repository: Pinback-Team/pinback-client

Length of output: 2923


🏁 Script executed:

# 프로젝트 React 버전 확인
cd packages/design-system && cat -n package.json | grep -A 5 -B 5 '"react"'

Repository: Pinback-Team/pinback-client

Length of output: 462


🏁 Script executed:

# 다른 컴포넌트들의 import 패턴 확인
rg "^import.*React" packages/design-system/src/components/ --type tsx -A 3 | head -40

Repository: Pinback-Team/pinback-client

Length of output: 98


🏁 Script executed:

# 확장된 tsconfig의 base configuration 확인
find . -path "*/typescript-config/react.json" -o -path "*/typescript-config/base.json" | head -5

Repository: Pinback-Team/pinback-client

Length of output: 149


🏁 Script executed:

# 다른 컴포넌트에서 React 타입 사용 패턴 확인
rg "React\.(InputHTMLAttributes|ChangeEvent|CSSProperties)" packages/design-system/src/components/ --type tsx | head -20

Repository: Pinback-Team/pinback-client

Length of output: 98


🏁 Script executed:

# 프로젝트에서 React 타입을 어떻게 임포트하는지 패턴 확인
rg "import.*(?:InputHTMLAttributes|ChangeEvent)" packages/design-system/src/components/ --type tsx

Repository: Pinback-Team/pinback-client

Length of output: 98


🏁 Script executed:

# typescript-config 파일 확인
cat packages/typescript-config/react.json

Repository: Pinback-Team/pinback-client

Length of output: 617


🏁 Script executed:

# React 네임스페이스 타입 사용 패턴 확인
rg "React\.(InputHTMLAttributes|ChangeEvent|CSSProperties)" packages/design-system/src/components/

Repository: Pinback-Team/pinback-client

Length of output: 425


🏁 Script executed:

# 다른 컴포넌트에서 type import 패턴 확인
rg "import type.*from 'react'" packages/design-system/src/components/ | head -20

Repository: Pinback-Team/pinback-client

Length of output: 53


🏁 Script executed:

# 다른 컴포넌트에서 React 직접 임포트 패턴 확인
rg "^import.*React" packages/design-system/src/components/ | head -20

Repository: Pinback-Team/pinback-client

Length of output: 662


React 네임스페이스 미임포트 — TypeScript 타입 오류

React.InputHTMLAttributes<HTMLInputElement>(라인 7)과 React.ChangeEvent<HTMLInputElement>(라인 73)에서 React 네임스페이스를 사용하지만, 파일의 import 문(라인 1-3)에서 React 네임스페이스를 임포트하지 않았습니다.

새 JSX 변환(jsx: react-jsx)을 사용하면 JSX는 React를 필요로 하지 않지만, 타입 참조는 여전히 React를 scope에 두어야 합니다. 프로젝트의 기본 tsconfig에서 strict: true로 설정되어 있으므로, 이 코드는 TypeScript 컴파일 오류를 발생시킵니다.

명시적으로 named type import를 사용하는 것을 권장합니다:

제안된 수정
+import type { InputHTMLAttributes, ChangeEvent } from 'react';
 import { cva } from 'class-variance-authority';
 import { useId, useState } from 'react';
 import { cn } from '../../lib/utils';

 interface CheckboxProps
   extends Omit<
-    React.InputHTMLAttributes<HTMLInputElement>,
+    InputHTMLAttributes<HTMLInputElement>,
     'type' | 'size' | 'checked' | 'defaultChecked' | 'onChange'
   > {
-  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/components/checkbox/Checkbox.tsx` around lines 1 -
3, The file uses the React namespace types (React.InputHTMLAttributes and
React.ChangeEvent) but doesn't import React into the type scope; add a type-only
import for the React namespace (e.g., import type React from 'react') at the top
of the file so React.InputHTMLAttributes<HTMLInputElement> and
React.ChangeEvent<HTMLInputElement> in the CheckboxProps/Checkbox component
resolve without TS errors.

<div
className={CardStyle({
overflow: step === Step.ALARM && alarmSelected === 3,
size: step === Step.JOB ? 'wide' : 'default',
Copy link
Member Author

Choose a reason for hiding this comment

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

Image

직무 step이 다른 알람 step과 다르게 더 넓은 width값을 가지고 있어서 이를 분기처리하긴했어요.

처음에는 container에 width값을 안주고 내부 요소에 width를 설정해서 조절하도록 할까 했는데, 맥북 알림/리마인드 시간 선택 step등이 내부는 다른데 container는 63.2rem으로 동일해서 일단 저렇게 wide값을 가질때를 분기했어요.

사실 저렇게 하면 step이 바뀔 때 container의 width값이 변경되니 CLS(레이아웃 변경 횟수) 평가 지표에 좋지 않은 영향이 갈 것이라고 생각해요. 그래서 일단 디자이너에게 통일할 수 있는 방법을 물어본 상태이고, 만약 안된다면 layout 변경을 최소화하고 최적화 할 수 있는지 체크해볼게요!

Comment on lines +74 to +75
const isUncontrolled = isSelected === undefined;
const selected = isUncontrolled ? internalSelected : isSelected;
Copy link
Member Author

Choose a reason for hiding this comment

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

checkbox 컴포넌트를 제어/비제어 컴포넌트 모두 사용할 수 있게 선택권을 주도록 설계했어요.
만약 isSelected를 부모에서 관리하는 경우는 제어 컴포넌트고, 아니라면 비제어 컴포넌트로 내부 internalSelected 상태 값을 가지고/변경 되도록 했어요.

물론 대부분의 checkbox의 경우 사용하는 부모에서 그 체크 값을 form 등에서 사용하기 때문에 제어 컴포넌트로 많이 사용하겠지만, 단순 UI 용으로 checkbox를 사용하는 등의 경우는 비제어로도 사용이 가능해요.

Copy link
Collaborator

Choose a reason for hiding this comment

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

우와.. 제 생각 이 짧았네요 이런 방식도 있군요..배워갑니당

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/design-system/src/components/checkbox/Checkbox.tsx (1)

58-67: ref 미노출 — 폼 라이브러리 통합에 제약

디자인 시스템 컴포넌트에서 네이티브 <input>에 대한 ref 접근은 react-hook-form 등 폼 라이브러리 연동에 필수적입니다. React 19에서는 forwardRef 없이 ref를 일반 prop으로 받을 수 있으므로 Ref<HTMLInputElement>CheckboxProps에 추가하는 것을 권장합니다.

♻️ 제안된 개선
-import { ChangeEvent, InputHTMLAttributes, useId, useState } from 'react';
+import { ChangeEvent, InputHTMLAttributes, Ref, useId, useState } from 'react';

 interface CheckboxProps
   extends Omit<
     InputHTMLAttributes<HTMLInputElement>,
     'type' | 'size' | 'checked' | 'defaultChecked' | 'onChange'
   > {
   isSelected?: boolean;
   defaultSelected?: boolean;
   onSelectedChange?: (checked: boolean) => void;
   size?: 'small' | 'medium';
+  ref?: Ref<HTMLInputElement>;
 }

 const Checkbox = ({
   isSelected,
   defaultSelected = false,
   onSelectedChange,
   size = 'medium',
   className,
   id,
   disabled,
+  ref,
   ...props
 }: CheckboxProps) => {
       <input
         id={inputId}
         type="checkbox"
         className="peer sr-only"
         checked={selected}
         disabled={disabled}
         onChange={handleChange}
+        ref={ref}
         {...props}
       />

Based on learnings: "React 19부터 함수 컴포넌트에서 ref를 일반 prop으로 받을 수 있어 forwardRef가 더 이상 필요하지 않다. InputProps interface에서 ref: Ref<HTMLInputElement>로 정의하고 함수 컴포넌트에서 ({ ref, ...props }) => 형태로 받을 수 있다."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/components/checkbox/Checkbox.tsx` around lines 58
- 67, The Checkbox component currently doesn't expose a ref which prevents form
libs from accessing the native input; update the CheckboxProps type to include
ref: React.Ref<HTMLInputElement> (import Ref/React as needed), accept ref in the
Checkbox function signature (e.g., include ref in the destructured props for
Checkbox), and forward that ref down to the native <input> element rendered by
Checkbox (ensure the input's ref prop uses the incoming ref). Keep existing
isSelected/defaultSelected/onSelectedChange behavior and types intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/design-system/src/components/checkbox/Checkbox.tsx`:
- Around line 85-131: The visible checkbox container currently uses a <span>
with aria-hidden="true" and isn't associated with the hidden <input> so
mouse/touch clicks don't toggle the checkbox; change the span to a <label
htmlFor={inputId}> (using the existing inputId from useId) so the visible
element is associated with the input, remove aria-hidden (or ensure it is not
set to true) on that element, preserve the className logic that uses
checkboxBoxVariants and checkIconVariants, and keep the <input id={inputId} ...
/> and onChange={handleChange} intact so clicks on the label toggle the input's
selected state.

---

Duplicate comments:
In `@packages/design-system/src/components/checkbox/Checkbox.tsx`:
- Line 3: Import the missing ChangeEvent named export from React and use it
consistently to fix the TypeScript compile error: add ChangeEvent to the
existing named imports (alongside InputHTMLAttributes, useId, useState) and
replace occurrences of React.ChangeEvent<HTMLInputElement> with
ChangeEvent<HTMLInputElement> in Checkbox.tsx (e.g., where event types are
declared for onChange handlers), ensuring the type reference matches the new
import.

---

Nitpick comments:
In `@packages/design-system/src/components/checkbox/Checkbox.tsx`:
- Around line 58-67: The Checkbox component currently doesn't expose a ref which
prevents form libs from accessing the native input; update the CheckboxProps
type to include ref: React.Ref<HTMLInputElement> (import Ref/React as needed),
accept ref in the Checkbox function signature (e.g., include ref in the
destructured props for Checkbox), and forward that ref down to the native
<input> element rendered by Checkbox (ensure the input's ref prop uses the
incoming ref). Keep existing isSelected/defaultSelected/onSelectedChange
behavior and types intact.

Comment on lines +85 to +131
return (
<div
className={cn(
'inline-flex items-center',
disabled && 'cursor-not-allowed',
className
)}
>
<input
id={inputId}
type="checkbox"
className="peer sr-only"
checked={selected}
disabled={disabled}
onChange={handleChange}
{...props}
/>

<span
aria-hidden="true"
data-state={selected ? 'checked' : 'unchecked'}
className={cn(
checkboxBoxVariants({
size,
selected,
disabled: Boolean(disabled),
})
)}
>
<svg
viewBox="0 0 12 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn(checkIconVariants({ size, selected }))}
>
<path
d="M1 4.5L4.5 8L11 1"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</div>
);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

클릭이 동작하지 않는 구조 — <label> 연결 누락

<input>sr-only로 시각적으로 숨겨져 있고, 보이는 <span>에는 aria-hidden="true"만 있고 onClick도 없습니다. 컴포넌트 내부에 <label htmlFor={inputId}>가 없어 마우스/터치 클릭으로 체크박스를 토글할 수 없습니다. 키보드(Tab+Space)로만 동작하는 상태입니다.

useId로 생성한 inputId가 컴포넌트 안에서 활용되지 않고 있습니다. <span><label htmlFor={inputId}>로 교체하는 것이 가장 간단한 수정입니다.

🐛 제안된 수정
-      <span
+      <label
+        htmlFor={inputId}
         aria-hidden="true"
         data-state={selected ? 'checked' : 'unchecked'}
         className={cn(
           checkboxBoxVariants({
             size,
             selected,
             disabled: Boolean(disabled),
           })
         )}
       >
         <svg
           viewBox="0 0 12 9"
           fill="none"
           xmlns="http://www.w3.org/2000/svg"
           className={cn(checkIconVariants({ size, selected }))}
         >
           <path
             d="M1 4.5L4.5 8L11 1"
             stroke="currentColor"
             strokeWidth="1.5"
             strokeLinecap="round"
             strokeLinejoin="round"
           />
         </svg>
-      </span>
+      </label>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/design-system/src/components/checkbox/Checkbox.tsx` around lines 85
- 131, The visible checkbox container currently uses a <span> with
aria-hidden="true" and isn't associated with the hidden <input> so mouse/touch
clicks don't toggle the checkbox; change the span to a <label htmlFor={inputId}>
(using the existing inputId from useId) so the visible element is associated
with the input, remove aria-hidden (or ensure it is not set to true) on that
element, preserve the className logic that uses checkboxBoxVariants and
checkIconVariants, and keep the <input id={inputId} ... /> and
onChange={handleChange} intact so clicks on the label toggle the input's
selected state.

Copy link
Collaborator

@jjangminii jjangminii left a comment

Choose a reason for hiding this comment

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

확실히 코드 설계에 있어 고민한 흔적이 느껴지네요,, 저도 많이 배워갑니당 질문 몇개만 답변해주세요-!

@@ -0,0 +1,133 @@
import { cva } from 'class-variance-authority';
import { cn } from '../../lib/utils';
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 원래 상대경로로 하는건가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

절대경로를 하는 목적이 상대 경로가 길어지고 파악이 어려울 때 사용한다고 생각하는데, pds 내에서 적게 이동하는 cn 함수의 경우에는 크게 파악하는데 문제가 없다고 생각이 들었어요!

const defaultJob: JobKey = 'planner';
const [internalJob, setInternalJob] = useState<JobKey>(defaultJob);
const [internalAgree, setInternalAgree] = useState(true);
const agreeId = useId();
Copy link
Collaborator

Choose a reason for hiding this comment

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

useId는 어떻게 동작하나요? 이게 왜 필요한지 궁금합니다

Copy link
Member Author

Choose a reason for hiding this comment

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

useId는 고유 ID를 생성하기 위한 React Hook이에요.
어트리뷰트에 unique한 값을 넣을 때 보통 사용하는데, 해당 jobStep에서도 label태그의 htmlFor와 input의 id에 같은 값을 연결하기 위해 직접 값을 입력하는 것이 아닌 useId를 사용했어요!

다만 지금 생각해보니 label 태그 안에 input을 넣을 경우는 굳이 htmlFor로 연결을 안해도 되는 것 같아서 해당 useId는 제거할게요!

88232ae

const activeJob = selectedJob ?? internalJob;
const activeAgree = agreeChecked ?? internalAgree;

const jobs = useMemo(
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서 왜 useMemo가 필요하나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

불필요한 useMemo 같네요! 제거 완완입니다~

Comment on lines +32 to +38
const JobIcon = ({ type }: { type: JobKey }) => {
const iconMap: Record<JobKey, string> = {
planner: jobPlan,
designer: jobDesign,
frontend: jobFrontend,
backend: jobBackend,
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

이건 따로 타입 분리하는건 어떤가요

Copy link
Member Author

Choose a reason for hiding this comment

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

해당 JobIcon은 컴포넌트입니다! JobStep과 많이 연관되어있다고 판단해서 안에 같이 두는 것도 좋을 것 같은데 어떻게 생각하시나요?

InputHTMLAttributes<HTMLInputElement>,
'type' | 'size' | 'checked' | 'defaultChecked' | 'onChange'
> {
isSelected?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

isSelected가 옵셔널인 이유가 있을까요? 그리고 isSelected와 defaultSelected를 구분한 이유도 궁금합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

체크박스가 비제어인 경우도 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

isSelected는 제어 컴포넌트의 경우지만, 해당 checkbox를 비제어 컴포넌트로도 사용할 수 있도록 설계를 했어요. 그래서 isSelected가 없는 비제어인 경우에도 에러가 안나도록 optional로 두었습니다!

물론 말씀해주신 것처럼 체크박스가 비제어로 사용하는 경우가 많지는 않지만 material UI과 같이 유명한 컴포넌트 라이브러리도 선택권을 넓게 주는 것 같아요. 그만큼 책임도 개발자에게 있는 것이지만 열어두는 것도 괜찮을 것 같습니다!

https://github.com/mui/material-ui/blob/master/packages/mui-material/src/internal/SwitchBase.js#L84

image

Comment on lines +74 to +75
const isUncontrolled = isSelected === undefined;
const selected = isUncontrolled ? internalSelected : isSelected;
Copy link
Collaborator

Choose a reason for hiding this comment

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

우와.. 제 생각 이 짧았네요 이런 방식도 있군요..배워갑니당

Comment on lines +77 to +83
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextChecked = e.target.checked;

if (isUncontrolled) setInternalSelected(nextChecked);

onSelectedChange?.(nextChecked);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

너무 고능해요

@jjangminii jjangminii self-requested a review February 19, 2026 15:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx (2)

89-89: 장식용 이미지 dotoriaria-hidden="true" 누락

JobIcon에는 aria-hidden="true"가 적용되어 있는데, 같은 파일의 dotori 이미지는 누락되어 있습니다. 순수 장식용이라면 스크린 리더가 "dotori"를 불필요하게 읽게 됩니다.

♻️ 제안된 수정
- <img src={dotori} className="mb-[1.2rem]" alt="dotori" />
+ <img src={dotori} className="mb-[1.2rem]" alt="" aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` at line
89, The decorative dotori image in JobStep.tsx is missing accessibility markup;
update the <img src={dotori} ... /> element (the dotori image in the JobStep
component) to be treated as purely decorative by screen readers—add
aria-hidden="true" (and/or set an empty alt="" if you prefer) to match how
JobIcon is handled so assistive tech ignores it.

63-71: useMemo의 빈 의존성 배열([]) — 모듈 상수로 대체 가능

jobs 배열은 컴포넌트의 어떤 상태나 props에도 의존하지 않는 순수 정적 데이터입니다. useMemo(fn, []) 는 "렌더 전까지 계산을 미루는" 효과만 있을 뿐, 모듈 스코프 const로 정의하는 것과 의미상 동일합니다.

♻️ 제안된 리팩터
+const JOBS: { key: JobKey; label: string }[] = [
+  { key: 'planner', label: '기획자' },
+  { key: 'designer', label: '디자이너' },
+  { key: 'frontend', label: '프론트엔드' },
+  { key: 'backend', label: '백엔드' },
+];
+
 const JobStep = ({ ... }: JobStepProps) => {
   ...
-  const jobs = useMemo(
-    () => [
-      { key: 'planner', label: '기획자' },
-      { key: 'designer', label: '디자이너' },
-      { key: 'frontend', label: '프론트엔드' },
-      { key: 'backend', label: '백엔드' },
-    ],
-    []
-  );
+  // 모듈 상수 JOBS 사용
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` around
lines 63 - 71, The jobs array is static and wrapped unnecessarily in useMemo
inside the JobStep component; replace the useMemo(...) call by defining a
module‑level constant (e.g., const jobs = [{ key: 'planner', ... }, ...])
outside the JobStep component, remove the useMemo usage (and its import if now
unused), and update any references to the jobs identifier inside JobStep to use
the module constant.
apps/client/src/pages/onBoarding/GoogleCallback.tsx (1)

47-49: redirectUri를 컴포넌트 외부 모듈 스코프로 이동하는 것을 권장합니다

import.meta.env는 빌드 타임 상수이므로 렌더마다 재계산될 이유가 없습니다. 컴포넌트 외부로 이동하면 의도가 명확해지고 불필요한 재평가도 제거됩니다.

♻️ 제안된 리팩터
+const redirectUri = import.meta.env.PROD
+  ? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
+  : import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
+
 const GoogleCallback = () => {
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
   ...
-  const redirectUri = import.meta.env.PROD
-    ? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
-    : import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
-
   const loginWithCode = async (code: string) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/GoogleCallback.tsx` around lines 47 - 49,
The redirectUri constant inside GoogleCallback.tsx should be moved out of the
React component into module scope to avoid re-evaluating import.meta.env on
every render; locate the current const redirectUri = import.meta.env.PROD ?
import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD :
import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV and hoist it to the top-level of
the file (above the component function) so the value is computed once at build
time and the component uses that module-scoped redirectUri.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx`:
- Around line 97-126: The radio-group pattern is missing ARIA arrow-key
navigation; update the JobStep card buttons so arrow keys move selection and Tab
focuses the selected item: add an onKeyDown handler (e.g., handleKeyDown(event,
job.key)) and implement handleKeyDown to call handleSelect with the
next/previous JobKey on ArrowLeft/ArrowUp/ArrowRight/ArrowDown, and set tabIndex
on each button to (isSelected ? 0 : -1) so only the selected item is tabbable;
keep role="radiogroup" and role="radio" (or remove roles if you prefer simple
buttons) and reuse existing identifiers like jobs.map, activeJob, handleSelect,
and jobCardStyle to locate and wire up these changes.

In `@apps/client/src/pages/onBoarding/GoogleCallback.tsx`:
- Around line 47-56: The redirectUri computed at top (variable redirectUri) can
be undefined and is passed from loginWithCode into
apiRequest.post('/api/v3/auth/google', { code, uri }), causing the v3 endpoint
to reject the request; add a safe fallback (e.g., a known default redirect URL)
when computing redirectUri and validate it at the start of loginWithCode before
calling apiRequest.post — if redirectUri is missing, either populate it with the
fallback or throw/handle an explicit error and avoid sending {uri: undefined} so
the API always receives a defined uri field; update the redirectUri
initialization and add a pre-send check in loginWithCode to enforce this.

---

Duplicate comments:
In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx`:
- Line 58: The state internalAgree in JobStep.tsx is initialized to true
(opt-out); change the default to false by updating useState(true) →
useState(false) and ensure the toggle handler (setInternalAgree) and any
submission/next-step guard that reads internalAgree (referencing JobStep
component and the step advancement logic in MainCard.tsx) treat false as the
default and require an explicit user action to set it true before allowing
progression; review and adjust the MainCard.tsx next-step validation to check
internalAgree === true before advancing and update any tests or UI copy if
needed.

---

Nitpick comments:
In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx`:
- Line 89: The decorative dotori image in JobStep.tsx is missing accessibility
markup; update the <img src={dotori} ... /> element (the dotori image in the
JobStep component) to be treated as purely decorative by screen readers—add
aria-hidden="true" (and/or set an empty alt="" if you prefer) to match how
JobIcon is handled so assistive tech ignores it.
- Around line 63-71: The jobs array is static and wrapped unnecessarily in
useMemo inside the JobStep component; replace the useMemo(...) call by defining
a module‑level constant (e.g., const jobs = [{ key: 'planner', ... }, ...])
outside the JobStep component, remove the useMemo usage (and its import if now
unused), and update any references to the jobs identifier inside JobStep to use
the module constant.

In `@apps/client/src/pages/onBoarding/GoogleCallback.tsx`:
- Around line 47-49: The redirectUri constant inside GoogleCallback.tsx should
be moved out of the React component into module scope to avoid re-evaluating
import.meta.env on every render; locate the current const redirectUri =
import.meta.env.PROD ? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD :
import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV and hoist it to the top-level of
the file (above the component function) so the value is computed once at build
time and the component uses that module-scoped redirectUri.

Comment on lines 97 to 126
<div
role="radiogroup"
aria-label="직무 선택"
className="grid w-full grid-cols-2 justify-items-center gap-[1.4rem] sm:grid-cols-4"
>
{jobs.map((job) => {
const isSelected = activeJob === job.key;
return (
<button
key={job.key}
type="button"
role="radio"
aria-checked={isSelected}
onClick={() => handleSelect(job.key as JobKey)}
className={jobCardStyle({ selected: isSelected })}
>
<div className="flex flex-col items-center gap-[1.6rem]">
<JobIcon type={job.key as JobKey} />
<span
className={`sub3-sb ${
isSelected ? 'text-main500' : 'text-font-black-1'
}`}
>
{job.label}
</span>
</div>
</button>
);
})}
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

role="radiogroup" + role="radio" 패턴에서 키보드 화살표 키 내비게이션이 구현되지 않았습니다

WAI-ARIA Authoring Practices의 Radio Group 패턴에 따르면, role="radio" 요소 간 이동은 화살표 키(←→↑↓) 로 해야 합니다. Tab 키는 그룹 전체를 단일 포커스 단위로 취급하고, 화살표 키로 내부 선택을 변경합니다. 현재 구현은 <button> 요소를 그대로 사용하기 때문에 Tab으로 각 카드를 개별 탐색하게 되어 ARIA 스펙과 어긋납니다.

화살표 키 핸들러를 추가하거나, ARIA radiogroup 패턴 대신 단순 <button> 그룹으로 역할을 바꾸는(role 제거) 방향을 검토해주세요.

♿ 화살표 키 내비게이션 추가 예시
+  const jobKeys = jobs.map((j) => j.key as JobKey);
+
+  const handleKeyDown = (e: React.KeyboardEvent, currentKey: JobKey) => {
+    const idx = jobKeys.indexOf(currentKey);
+    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
+      e.preventDefault();
+      handleSelect(jobKeys[(idx + 1) % jobKeys.length]);
+    } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
+      e.preventDefault();
+      handleSelect(jobKeys[(idx - 1 + jobKeys.length) % jobKeys.length]);
+    }
+  };

<button>onKeyDown={(e) => handleKeyDown(e, job.key as JobKey)} 를 추가하고, Tab 포커스는 선택된 항목(tabIndex={isSelected ? 0 : -1})에만 두도록 조정하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx` around
lines 97 - 126, The radio-group pattern is missing ARIA arrow-key navigation;
update the JobStep card buttons so arrow keys move selection and Tab focuses the
selected item: add an onKeyDown handler (e.g., handleKeyDown(event, job.key))
and implement handleKeyDown to call handleSelect with the next/previous JobKey
on ArrowLeft/ArrowUp/ArrowRight/ArrowDown, and set tabIndex on each button to
(isSelected ? 0 : -1) so only the selected item is tabbable; keep
role="radiogroup" and role="radio" (or remove roles if you prefer simple
buttons) and reuse existing identifiers like jobs.map, activeJob, handleSelect,
and jobCardStyle to locate and wire up these changes.

Comment on lines +47 to +56
const redirectUri = import.meta.env.PROD
? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
: import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;

const loginWithCode = async (code: string) => {
try {
const res = await apiRequest.post('/api/v2/auth/google', { code });
const res = await apiRequest.post('/api/v3/auth/google', {
code,
uri: redirectUri,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

환경 변수 미설정 시 uriundefined로 전송되어 API 요청이 실패할 수 있습니다

VITE_GOOGLE_REDIRECT_URI_PROD 또는 VITE_GOOGLE_REDIRECT_URI_DEV가 설정되지 않으면 redirectUriundefined가 됩니다. { code, uri: undefined }JSON.stringifyuri 키가 생략되어 ({"code":"..."}), v3 엔드포인트가 필수 필드 부재로 4xx를 반환하고 catch 블록에서 /onboarding?step=SOCIAL_LOGIN으로 조용히 리다이렉트됩니다.

🛡️ 제안된 수정 — 폴백 & 사전 검증 추가
- const redirectUri = import.meta.env.PROD
-   ? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
-   : import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;

+ const redirectUri: string | undefined = import.meta.env.PROD
+   ? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
+   : import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
  const loginWithCode = async (code: string) => {
    try {
+     if (!redirectUri) {
+       console.error('Redirect URI가 설정되지 않았습니다.');
+       navigate('/onboarding?step=SOCIAL_LOGIN');
+       return;
+     }
      const res = await apiRequest.post('/api/v3/auth/google', {
        code,
        uri: redirectUri,
      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const redirectUri = import.meta.env.PROD
? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
: import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
const loginWithCode = async (code: string) => {
try {
const res = await apiRequest.post('/api/v2/auth/google', { code });
const res = await apiRequest.post('/api/v3/auth/google', {
code,
uri: redirectUri,
});
const redirectUri: string | undefined = import.meta.env.PROD
? import.meta.env.VITE_GOOGLE_REDIRECT_URI_PROD
: import.meta.env.VITE_GOOGLE_REDIRECT_URI_DEV;
const loginWithCode = async (code: string) => {
try {
if (!redirectUri) {
console.error('Redirect URI가 설정되지 않았습니다.');
navigate('/onboarding?step=SOCIAL_LOGIN');
return;
}
const res = await apiRequest.post('/api/v3/auth/google', {
code,
uri: redirectUri,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/onBoarding/GoogleCallback.tsx` around lines 47 - 56,
The redirectUri computed at top (variable redirectUri) can be undefined and is
passed from loginWithCode into apiRequest.post('/api/v3/auth/google', { code,
uri }), causing the v3 endpoint to reject the request; add a safe fallback
(e.g., a known default redirect URL) when computing redirectUri and validate it
at the start of loginWithCode before calling apiRequest.post — if redirectUri is
missing, either populate it with the fallback or throw/handle an explicit error
and avoid sending {uri: undefined} so the API always receives a defined uri
field; update the redirectUri initialization and add a pre-send check in
loginWithCode to enforce this.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 온보딩 직무 선택 Step 추가

2 participants