Skip to content

Feat(client): 대시보드 직무 핀 선택 팝업 추가#266

Merged
constantly-dev merged 11 commits intodevelopfrom
feat/#263/dashboard-jobs-pin-funnel
Feb 24, 2026
Merged

Feat(client): 대시보드 직무 핀 선택 팝업 추가#266
constantly-dev merged 11 commits intodevelopfrom
feat/#263/dashboard-jobs-pin-funnel

Conversation

@constantly-dev
Copy link
Member

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

📌 Related Issues

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

📄 Tasks

  • funnel progress bar 컴포넌트 구현
  • 직무 핀 선택 팝업 컴포넌트 구현

⭐ PR Point (To Reviewer)

기존에 추가한 useFunnel 훅 써서 구현했습니다.
처음 구글 로그인하고 response로 내려오는 hasJob(boolean)을 로컬스토리지에 저장하는데, 이를

const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(
    () => localStorage.getItem('hasJob') === 'false'
  );

로그인 후 첫페이지인 remind 페이지에서 초기 값 지정을 통해, funnel을 보여주는 분기처리를 추가했어요.
사실 해당 뷰는 현재 기존 유저가 모두 hasJob을 true로 가지게 되면 없애도 되는 뷰라고 생각해주시면 될 것 같아요. 따라서 임시로 일단 로컬스토리지에 간단하게 구현해봤습니다. 더 좋은 방법이 있다면 편하게 말해주세요!!

사실 로컬스토리지로 관리해서 사용자가 hasJob을 true로 바꾸면 저게 안뜨긴해요. 하지만 임시 view이기도 하고, 그런 작업을 해서 안뜨는 것은 사용자 책임도 어느정도 있다고 판단해서 이렇게 구현했어요. 만약 크리티컬 하다고 판단이 들면 소셜 로그인 후 리마인드로 리다이렉트 시킬 때 navigatestate로 해당 값을 담아서 보내는 방법도 있을 것 같아요!

📷 Screenshot

2026-02-24.12.47.16.mov

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • Google 로그인 후 직무 선택 플로우 추가
    • 3단계 온보딩 펀넬 도입 (직무 선택, 핀 설정, 공유)
    • 4가지 직무 옵션 제공 (플래너, 디자이너, 프론트엔드, 백엔드)
    • 진행 상황을 시각적으로 표시하는 진행 표시기 추가

@vercel
Copy link

vercel bot commented Feb 24, 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 24, 2026 7:15am
pinback-client-landing Ready Ready Preview, Comment Feb 24, 2026 7:15am

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

coderabbitai bot commented Feb 24, 2026

Warning

Rate limit exceeded

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4867758 and 8189571.

📒 Files selected for processing (6)
  • apps/client/src/pages/onBoarding/GoogleCallback.tsx
  • apps/client/src/pages/remind/Remind.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx

Walkthrough

직무 선택 펀널을 온보딩 흐름에 통합합니다. Google OAuth 응답에서 hasJob 플래그를 추출하여 localStorage에 저장하고, Remind 페이지에서 미선택 사용자를 위한 3단계 펀널(직무 선택 → 핀 → 공유)을 모달로 표시합니다.

Changes

Cohort / File(s) Summary
온보딩 로그인 플로우 통합
apps/client/src/pages/onBoarding/GoogleCallback.tsx
handleUserLogin 서명을 확장하여 hasJob 선택 매개변수를 받고, OAuth 응답에서 hasJob을 추출하여 전달합니다.
대시보드 펀널 표시
apps/client/src/pages/remind/Remind.tsx
JobSelectionFunnel을 임포트하고, localStorage.hasJob이 'false'일 때 전체 화면 모달로 펀널을 렌더링합니다. 완료 시 localStorage 업데이트 및 모달 숨김을 처리합니다.
펀널 진행 표시기
apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx
수평 진행 바 컴포넌트를 새로 추가합니다. currentIndex 기반으로 채움 너비를 계산하고 단계별 원형 표시기를 렌더링합니다.
펀널 핵심 조율
apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx
useFunnel 훅으로 3단계(직무/핀/공유) 펀널을 조율합니다. 선택한 직무와 동의 상태를 관리하고, 각 단계의 조건부 렌더링 및 완료 콜백을 처리합니다.
직무 선택 스텝
apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx
4개의 직무 옵션(기획자/디자이너/프론트엔드/백엔드)을 선택 가능한 카드로 렌더링합니다. 라디오 그룹 역할 기반의 접근성 속성과 동의 체크박스를 포함합니다.
핀/공유 스텝
apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx, apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx
각각 핀과 공유 단계를 위한 제목/부제목 및 이미지를 렌더링하는 단순한 UI 컴포넌트입니다.

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant GoogleAuth as Google OAuth
    participant GoogleCallback as GoogleCallback
    participant localStorage as localStorage
    participant Remind as Remind 페이지
    participant JobFunnel as JobSelectionFunnel
    participant JobStep as JobStep
    participant PinStep as PinStep
    participant ShareStep as ShareStep

    User->>GoogleAuth: Google 로그인
    GoogleAuth-->>GoogleCallback: hasJob 포함된 응답
    GoogleCallback->>GoogleCallback: OAuth 데이터에서 hasJob 추출
    GoogleCallback->>localStorage: hasJob 값 저장
    GoogleCallback->>localStorage: 기타 사용자 정보 저장
    
    User->>Remind: 대시보드 접근
    Remind->>localStorage: hasJob 값 확인
    alt hasJob === 'false'
        Remind->>JobFunnel: 펀널 모달 표시
        JobFunnel->>JobStep: 1단계: 직무 선택
        User->>JobStep: 직무 선택 & 동의 체크
        JobStep-->>JobFunnel: 선택 데이터 전달
        JobFunnel->>PinStep: 2단계: 핀 단계
        User->>PinStep: 핀 단계 확인
        JobFunnel->>ShareStep: 3단계: 공유 단계
        User->>ShareStep: 공유 단계 확인
        ShareStep-->>JobFunnel: 완료
        JobFunnel->>localStorage: hasJob을 'true'로 업데이트
        JobFunnel-->>Remind: 펀널 닫기
    else hasJob === 'true'
        Remind->>User: 대시보드 표시
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #54 — JobSelectionFunnel이 직접 임포트하는 useFunnel 공유 훅 추가
  • #204 — GoogleCallback 로그인 플로우의 handleUserLogin 서명 변경으로 직접적인 기반 형성
  • #60 — useFunnel 훅과 펀널 관련 컴포넌트 구조의 일관성 관련

Suggested labels

🛠️ Feature, feat

Suggested reviewers

  • jllee000
  • jjangminii

Poem

🐰 직무 선택 펀널이 생겼네요,
세 단계로 사용자를 유도하고,
localStorage에 hasJob 저장하고,
검사 박스는 동의를 확인하고,
모달이 우아하게 펼쳐지네요!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Pull request title clearly and concisely summarizes the main change: adding a job selection pin popup to the dashboard.
Linked Issues check ✅ Passed All coding requirements from #263 (implement dashboard job pin step using useFunnel hook) and #25 (progress bar component) are fully implemented across the new components (FunnelProgress, JobSelectionFunnel, JobStep, PinStep, ShareStep).
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the job selection funnel and progress bar for issue #263, with supporting modifications to GoogleCallback.tsx and Remind.tsx for integration. No extraneous changes detected.
Description check ✅ Passed PR 설명이 요구 템플릿의 모든 필수 섹션(Related Issues, Tasks, PR Point)을 포함하고 있으며, 구현 세부사항과 설계 결정사항을 명확하게 제시하고 있습니다.

✏️ 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/#263/dashboard-jobs-pin-funnel

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 24, 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: 7

🧹 Nitpick comments (10)
apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx (2)

57-66: 버튼 w-[4.8rem] 고정폭은 라벨 잘림 가능성

Line 60의 고정 폭이 ‘완료’/‘다음’에 딱 맞지 않거나 폰트/로케일 변경 시 잘릴 수 있어요. 특별한 이유가 없다면 width 고정은 제거하고 padding으로 맞추는 편이 안전합니다.

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

In `@apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx`
around lines 57 - 66, The Button in JobSelectionFunnel.tsx currently uses a
fixed width class "w-[4.8rem]" which can cause label clipping; remove that fixed
width from the Button element (the one with onClick={handleNext} and
isDisabled={currentStep === 'job' && !jobShareAgree}) and replace it with
responsive spacing using padding classes (e.g., px and py utility classes) or a
min-width if you want a floor, so the label determined by isLastStep ('완료'/'다음')
can resize without clipping.

25-50: selectedJob이 현재 퍼널 밖으로 전달/저장되지 않음: 의도 확인 필요

Line 25의 selectedJob은 JobStep에서만 쓰이고 onComplete 때도 밖으로 전달되지 않아서, “선택 결과가 어디에도 반영되지 않는” 상태로 끝날 수 있어요. 이후 step에서 사용하거나(핀 추천 등) 상위로 콜백 전달이 필요했다면 onComplete(selectedJob, ...) 형태가 더 자연스럽습니다.

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

In `@apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx`
around lines 25 - 50, selectedJob is only stored locally and never passed out
when the funnel finishes, so the caller never receives the chosen job; update
the completion flow to forward the selection by changing onComplete usage:
accept selectedJob (and any other needed state like jobShareAgree) and invoke it
with those values instead of calling onComplete() with no args in
handleNext/when isLastStep, and ensure JobSelectionFunnel's props (and any
parent handlers) are updated to accept the selectedJob param; locate symbols
selectedJob, setSelectedJob, jobShareAgree, onComplete, handleNext and JobStep
to implement the change.
apps/client/src/pages/onBoarding/GoogleCallback.tsx (1)

10-20: useEffect deps 누락은 의도인지 확인

Line 10-20에서 navigate, searchParams, loginWithCode를 참조하면서 deps가 []라 lint 룰에 따라 경고가 날 수 있어요(기존 코드 스타일이면 유지해도 OK). 최소한 팀 컨벤션 상 suppress/의도 표기가 필요한지 확인해 주세요.

🤖 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 10 - 20,
The useEffect currently references navigate, searchParams, and loginWithCode but
has an empty deps array, which will trigger lint warnings; either add the
referenced variables to the dependency array (e.g., include navigate,
searchParams, loginWithCode) so the effect runs correctly when they change, or
if the empty deps is intentional, add an explicit explanatory
eslint-disable-next-line/react-hooks/exhaustive-deps comment with a short
rationale above the useEffect to satisfy team convention and silence the linter.
Ensure you update the useEffect that contains useEffect(...), navigate,
searchParams, and loginWithCode accordingly.
apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx (1)

7-12: 타이틀/설명은 heading 구조로 정리하면 접근성에 유리

Line 7-12도 <h2>/<p> 조합을 고려해 주세요.

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

In `@apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx` around
lines 7 - 12, The title and description in the PinStep component are currently
both rendered as <p> which hurts semantic heading structure; update the JSX in
PinStep.tsx to use a proper heading element (e.g., replace the element rendering
the title with <h2> while keeping the "head3 text-font-black-1" classes) and
keep the description as a <p> with "body2-m text-font-gray-3 text-center";
ensure the component (PinStep) preserves styling and layout when switching the
title from a <p> to an <h2>.
apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx (2)

9-11: (UX) 시작 단계 0% 노출이 의도인지 확인해도 좋겠습니다

현재 currentIndex=0이면 percent가 0%라 트랙이 비어 보이는데, repo 내 다른 진행률 UI는 “첫 단계도 0%는 피함” 의도가 있었다고 해서(학습 내용) 퍼널에서도 동일 UX를 원하는지 한 번만 합의하면 좋겠어요. Based on learnings: "In TreeStatusCard component, the progress bar calculation info.level * 20 is intentional design ... ensures no level shows 0% progress for UX purposes."

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

In `@apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx`
around lines 9 - 11, The progress calculation in FunnelProgress (variables
maxIndex and percent using currentIndex and totalSteps) yields 0% when
currentIndex is 0 which may conflict with the app-wide UX where the first step
should not show 0%; update the percent computation in FunnelProgress so the
first step renders a non-zero minimum (e.g., offset currentIndex by one or clamp
percent with a MIN_PERCENT) to match the TreeStatusCard pattern (avoid 0% for
the initial step) and keep percent bounded 0–100.

12-20: Progress 접근성(aria) 보강 추천

현재는 시각적 진행바라 SR에서 “현재 몇 단계인지”가 잘 전달되지 않을 수 있어요. role="progressbar" + aria-valuenow/aria-valuemin/aria-valuemax 또는 “3단계 중 2단계” 텍스트(스크린리더 전용) 추가를 고려해 주세요.

Also applies to: 24-35

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

In `@apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx`
around lines 12 - 20, The progress bar is currently only visual; update the
progress container in FunnelProgress (the div with class "bg-main400" that uses
style width `{percent}%`) to include accessibility attributes: add
role="progressbar" and set aria-valuenow to the numeric percent,
aria-valuemin="0" and aria-valuemax="100". Additionally, include a
visually-hidden text node (e.g., screen-reader-only span) near the progress bar
that reads the step status like "Step X of Y" (use the component's percent/step
props or computed step variables) so screen readers receive "3 of 5" style
context; ensure the hidden element is associated with the progressbar (either
placed inside it or referenced via aria-labelledby if needed).
apps/client/src/pages/remind/Remind.tsx (1)

253-256: 완료 시 hasJob만 true로 바꾸고 끝: 상태 소스 분산 최소화 고려

Line 253-256에서 localStorage와 React state를 동시에 갱신하는데, 이후 다른 페이지/탭에서의 동기화가 필요해질 수 있어요(예: storage event). 지금은 임시 뷰라 OK일 수 있지만, 최소한 hasJob를 읽는 유틸을 한 군데로 모아두면 추후 제거/대체가 쉬워집니다.

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

In `@apps/client/src/pages/remind/Remind.tsx` around lines 253 - 256, Replace the
direct localStorage write in the onComplete callback with a centralized helper
(e.g., setHasJob(true) in a utils/storage module) and call that helper alongside
setShowJobSelectionFunnel(false); specifically, remove
localStorage.setItem('hasJob','true') and call the new setHasJob(true) function
from onComplete, where setHasJob encapsulates reading/writing the 'hasJob' key
and any cross-tab sync or future state-source logic (and export a matching
getHasJob) so other components use the same single source of truth instead of
touching localStorage directly.
apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx (1)

5-14: 텍스트는 heading 태그로 의미 부여 고려

Line 7-13이 섹션 타이틀/설명 역할이라 <p> 대신 <h2>/<h3> + <p>로 두면 스크린리더 탐색성이 좋아집니다(시각 디자인은 클래스 그대로 유지 가능).

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

In `@apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx`
around lines 5 - 14, The section title in the ShareStep component uses a <p> for
a heading; change the title element (the first paragraph with class "head3
text-font-black-1") to a semantic heading (e.g., <h2> or <h3>) while keeping the
same CSS classes, leaving the secondary description as a <p> (the element with
"body2-m text-font-gray-3 text-center") so screen readers can navigate the
section properly; update the ShareStep component accordingly.
apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx (2)

60-61: 장식 이미지라면 alt는 비우는 쪽이 자연스러울 수 있음

Line 60의 dotori가 purely decorative면 alt="" aria-hidden="true"가 더 적절합니다(반대로 의미가 있으면 “도토리 캐릭터”처럼 의미 있는 alt 권장).

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

In `@apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx` around
lines 60 - 61, The decorative image using the dotori variable in JobStep (the
<img src={dotori} ... /> element) should be marked decorative for accessibility;
update the img element to use an empty alt attribute and add aria-hidden="true"
(i.e. alt="" aria-hidden="true") when the image is purely decorative, or replace
with a meaningful alt text like "도토리 캐릭터" only if it conveys content.

68-97: radiogroup 접근성: roving tabIndex/방향키 지원까지 하면 더 탄탄해짐

role="radiogroup" + role="radio"(Line 68-81) 구성은 좋습니다. 다만 현재는 모든 옵션이 tab으로 순회되고, 라디오 그룹에서 흔히 기대하는 ←/→/↑/↓ 이동이 없습니다. 여유 되면 roving tabIndex(선택된 항목만 tabIndex=0, 나머지 -1) + keydown으로 선택 이동을 넣는 걸 추천해요.

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

In `@apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx` around
lines 68 - 97, Update JobStep.tsx to implement roving tabindex and arrow-key
navigation: make each radio button set tabIndex={isSelected ? 0 : -1} (use the
existing selectedJob to determine isSelected), add a onKeyDown handler (attached
to each button or the radiogroup) to intercept
ArrowLeft/ArrowRight/ArrowUp/ArrowDown and compute the next job index from the
jobs array, call onSelectJob(nextKey) and move focus to that button (use refs or
a ref array keyed by job.key). Use the existing symbols jobs, selectedJob,
onSelectJob, jobCardStyle and JobIcon to locate elements; ensure aria-checked
remains correct and only the selected item is tabbable.
🤖 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/GoogleCallback.tsx`:
- Around line 42-44: The code currently writes non-boolean values (e.g., null ->
"null") to localStorage which breaks the Remind.tsx check; update the
GoogleCallback.tsx logic so you only persist boolean values: check typeof hasJob
=== 'boolean' before calling localStorage.setItem('hasJob', String(hasJob))
(skip writing for null/undefined or non-boolean), and ensure any other code that
reads hasJob (e.g., Remind.tsx uses localStorage.getItem('hasJob') === 'false')
continues to expect the literal strings "true"/"false".

In `@apps/client/src/pages/remind/Remind.tsx`:
- Around line 250-259: The JobSelectionFunnel overlay lacks modal semantics and
focus/scroll management; wrap the conditional JSX that renders
JobSelectionFunnel (controlled by showJobSelectionFunnel and closed via
setShowJobSelectionFunnel) in a proper modal primitive (e.g., reuse
PopupContainer if available) or add role="dialog" and aria-modal="true" to the
container, implement a focus trap that moves focus into the funnel on open and
returns focus on close, add an ESC key handler to call
setShowJobSelectionFunnel(false), and lock background scrolling while the modal
is open; also ensure the onComplete path (which sets localStorage.hasJob)
triggers the same close and focus/scroll cleanup.
- Around line 38-40: The state initializer for showJobSelectionFunnel reads
localStorage directly which will crash in SSR/prerender/storybook; update
Remind.tsx to avoid accessing localStorage during render by either guarding with
typeof window !== 'undefined' inside the useState initializer or initialize with
a safe default (e.g., false) and then read localStorage inside a useEffect that
calls setShowJobSelectionFunnel; touch the useState call and the
setShowJobSelectionFunnel updates to ensure no direct localStorage access
happens during server render.

In `@apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx`:
- Around line 8-11: FunnelProgress currently clamps percent but not the
step-activation logic, so if currentIndex is <0 or >= totalSteps the "active"
circles break; introduce a clamped display index (e.g., displayedIndex =
Math.max(0, Math.min(totalSteps - 1, currentIndex))) and use that displayedIndex
for both the percent calculation and the step active checks (replace usages of
currentIndex in percent and the `index <= currentIndex` checks) while retaining
maxIndex for division safety.

In `@apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx`:
- Around line 25-27: The checkbox state jobShareAgree is currently initialized
to true which implicitly opts users in; change its default to false by updating
useState for jobShareAgree (and ensure setJobShareAgree usage remains
compatible) so users must explicitly opt-in; also audit the related UI logic
(the checkbox render and any gating logic around advancing the funnel—e.g., Next
button enablement that checks jobShareAgree) and either make the consent
mandatory with clear copy or keep it optional and update the copy to reflect
opt-in behavior consistently.

In `@apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx`:
- Around line 100-110: The implicit label wrapping the Checkbox in JobStep.tsx
is fragile; update the Checkbox component to accept an id prop (or provide a
useCheckboxId hook) and forward that id to its internal <input>; then in
JobStep, generate a stable unique id, pass it as id to Checkbox (used with
agreeChecked and onAgreeChange), and change the markup so the text uses an
explicit htmlFor linking to that id (or apply aria-labelledby referencing that
id) to ensure durable, explicit label-input association.

In `@apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx`:
- Around line 1-20: PinStep currently defines the image path as a string
constant (pinStepImage = '/assets/jobSelectionFunnel/pinStep_description.svg')
which can break when a base path is added; change to an ES module import like
the other components (import pinStepImage from '.../pinStep_description.svg')
and remove the string constant, and update any similar usage in ShareStep to
match so assets resolve correctly under Vite base path changes; locate the
pinStepImage constant and the PinStep component to apply this replacement.

---

Nitpick comments:
In `@apps/client/src/pages/onBoarding/GoogleCallback.tsx`:
- Around line 10-20: The useEffect currently references navigate, searchParams,
and loginWithCode but has an empty deps array, which will trigger lint warnings;
either add the referenced variables to the dependency array (e.g., include
navigate, searchParams, loginWithCode) so the effect runs correctly when they
change, or if the empty deps is intentional, add an explicit explanatory
eslint-disable-next-line/react-hooks/exhaustive-deps comment with a short
rationale above the useEffect to satisfy team convention and silence the linter.
Ensure you update the useEffect that contains useEffect(...), navigate,
searchParams, and loginWithCode accordingly.

In `@apps/client/src/pages/remind/Remind.tsx`:
- Around line 253-256: Replace the direct localStorage write in the onComplete
callback with a centralized helper (e.g., setHasJob(true) in a utils/storage
module) and call that helper alongside setShowJobSelectionFunnel(false);
specifically, remove localStorage.setItem('hasJob','true') and call the new
setHasJob(true) function from onComplete, where setHasJob encapsulates
reading/writing the 'hasJob' key and any cross-tab sync or future state-source
logic (and export a matching getHasJob) so other components use the same single
source of truth instead of touching localStorage directly.

In `@apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx`:
- Around line 9-11: The progress calculation in FunnelProgress (variables
maxIndex and percent using currentIndex and totalSteps) yields 0% when
currentIndex is 0 which may conflict with the app-wide UX where the first step
should not show 0%; update the percent computation in FunnelProgress so the
first step renders a non-zero minimum (e.g., offset currentIndex by one or clamp
percent with a MIN_PERCENT) to match the TreeStatusCard pattern (avoid 0% for
the initial step) and keep percent bounded 0–100.
- Around line 12-20: The progress bar is currently only visual; update the
progress container in FunnelProgress (the div with class "bg-main400" that uses
style width `{percent}%`) to include accessibility attributes: add
role="progressbar" and set aria-valuenow to the numeric percent,
aria-valuemin="0" and aria-valuemax="100". Additionally, include a
visually-hidden text node (e.g., screen-reader-only span) near the progress bar
that reads the step status like "Step X of Y" (use the component's percent/step
props or computed step variables) so screen readers receive "3 of 5" style
context; ensure the hidden element is associated with the progressbar (either
placed inside it or referenced via aria-labelledby if needed).

In `@apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx`:
- Around line 57-66: The Button in JobSelectionFunnel.tsx currently uses a fixed
width class "w-[4.8rem]" which can cause label clipping; remove that fixed width
from the Button element (the one with onClick={handleNext} and
isDisabled={currentStep === 'job' && !jobShareAgree}) and replace it with
responsive spacing using padding classes (e.g., px and py utility classes) or a
min-width if you want a floor, so the label determined by isLastStep ('완료'/'다음')
can resize without clipping.
- Around line 25-50: selectedJob is only stored locally and never passed out
when the funnel finishes, so the caller never receives the chosen job; update
the completion flow to forward the selection by changing onComplete usage:
accept selectedJob (and any other needed state like jobShareAgree) and invoke it
with those values instead of calling onComplete() with no args in
handleNext/when isLastStep, and ensure JobSelectionFunnel's props (and any
parent handlers) are updated to accept the selectedJob param; locate symbols
selectedJob, setSelectedJob, jobShareAgree, onComplete, handleNext and JobStep
to implement the change.

In `@apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx`:
- Around line 60-61: The decorative image using the dotori variable in JobStep
(the <img src={dotori} ... /> element) should be marked decorative for
accessibility; update the img element to use an empty alt attribute and add
aria-hidden="true" (i.e. alt="" aria-hidden="true") when the image is purely
decorative, or replace with a meaningful alt text like "도토리 캐릭터" only if it
conveys content.
- Around line 68-97: Update JobStep.tsx to implement roving tabindex and
arrow-key navigation: make each radio button set tabIndex={isSelected ? 0 : -1}
(use the existing selectedJob to determine isSelected), add a onKeyDown handler
(attached to each button or the radiogroup) to intercept
ArrowLeft/ArrowRight/ArrowUp/ArrowDown and compute the next job index from the
jobs array, call onSelectJob(nextKey) and move focus to that button (use refs or
a ref array keyed by job.key). Use the existing symbols jobs, selectedJob,
onSelectJob, jobCardStyle and JobIcon to locate elements; ensure aria-checked
remains correct and only the selected item is tabbable.

In `@apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx`:
- Around line 7-12: The title and description in the PinStep component are
currently both rendered as <p> which hurts semantic heading structure; update
the JSX in PinStep.tsx to use a proper heading element (e.g., replace the
element rendering the title with <h2> while keeping the "head3
text-font-black-1" classes) and keep the description as a <p> with "body2-m
text-font-gray-3 text-center"; ensure the component (PinStep) preserves styling
and layout when switching the title from a <p> to an <h2>.

In `@apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx`:
- Around line 5-14: The section title in the ShareStep component uses a <p> for
a heading; change the title element (the first paragraph with class "head3
text-font-black-1") to a semantic heading (e.g., <h2> or <h3>) while keeping the
same CSS classes, leaving the secondary description as a <p> (the element with
"body2-m text-font-gray-3 text-center") so screen readers can navigate the
section properly; update the ShareStep component accordingly.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17daed1 and 4867758.

⛔ Files ignored due to path filters (2)
  • apps/client/public/assets/jobSelectionFunnel/pinStep_description.svg is excluded by !**/*.svg
  • apps/client/public/assets/jobSelectionFunnel/shareStep_description.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • apps/client/src/pages/onBoarding/GoogleCallback.tsx
  • apps/client/src/pages/remind/Remind.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx

Comment on lines 38 to 40
const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(
() => localStorage.getItem('hasJob') === 'false'
);
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

localStorage를 state 초기화에서 바로 읽는 패턴은 SSR/프리렌더에서 터질 수 있음

Line 38-40은 CSR-only면 문제 없지만, SSR/프리렌더/스토리북 환경에선 localStorage가 undefined라 런타임 에러가 날 수 있어요. 방어적으로 typeof window !== 'undefined' 가드(또는 effect에서 setState)를 두는 편이 안전합니다.

제안(예시): window 가드
-  const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(
-    () => localStorage.getItem('hasJob') === 'false'
-  );
+  const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(() => {
+    if (typeof window === 'undefined') return false;
+    return window.localStorage.getItem('hasJob') === '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 [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(
() => localStorage.getItem('hasJob') === 'false'
);
const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState(() => {
if (typeof window === 'undefined') return false;
return window.localStorage.getItem('hasJob') === 'false';
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/client/src/pages/remind/Remind.tsx` around lines 38 - 40, The state
initializer for showJobSelectionFunnel reads localStorage directly which will
crash in SSR/prerender/storybook; update Remind.tsx to avoid accessing
localStorage during render by either guarding with typeof window !== 'undefined'
inside the useState initializer or initialize with a safe default (e.g., false)
and then read localStorage inside a useEffect that calls
setShowJobSelectionFunnel; touch the useState call and the
setShowJobSelectionFunnel updates to ensure no direct localStorage access
happens during server render.

Comment on lines 250 to 259
{showJobSelectionFunnel && (
<div className="fixed inset-0 z-[2000] flex items-center justify-center bg-black/40 p-4">
<JobSelectionFunnel
onComplete={() => {
localStorage.setItem('hasJob', 'true');
setShowJobSelectionFunnel(false);
}}
/>
</div>
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

퍼널 오버레이: 접근성(aria-modal) + 포커스/스크롤 관리가 필요

Line 250-259는 “모달” 성격인데 role="dialog", aria-modal="true", 포커스 트랩, ESC 닫기, 배경 스크롤 락 등이 없어서 키보드/스크린리더 사용 시 이탈 가능성이 큽니다. 가능하면 기존 PopupContainer처럼 모달 primitive(있다면)로 감싸는 쪽이 안전해요.

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

In `@apps/client/src/pages/remind/Remind.tsx` around lines 250 - 259, The
JobSelectionFunnel overlay lacks modal semantics and focus/scroll management;
wrap the conditional JSX that renders JobSelectionFunnel (controlled by
showJobSelectionFunnel and closed via setShowJobSelectionFunnel) in a proper
modal primitive (e.g., reuse PopupContainer if available) or add role="dialog"
and aria-modal="true" to the container, implement a focus trap that moves focus
into the funnel on open and returns focus on close, add an ESC key handler to
call setShowJobSelectionFunnel(false), and lock background scrolling while the
modal is open; also ensure the onComplete path (which sets localStorage.hasJob)
triggers the same close and focus/scroll cleanup.

Comment on lines +8 to +11
const FunnelProgress = ({ currentIndex, totalSteps }: FunnelProgressProps) => {
const maxIndex = Math.max(1, totalSteps - 1);
const percent = Math.max(0, Math.min(100, (currentIndex / maxIndex) * 100));

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

currentIndex 범위 클램프(퍼센트뿐 아니라 step 활성화도) 권장

퍼센트는 clamp( Line 10 )되어 안전한데, step 활성화는 index <= currentIndex( Line 22 )라서 currentIndex-1이거나 totalSteps 이상이면 동그라미 상태가 깨질 수 있어요(예: 전부 active). 상위에서 항상 보장한다면 OK지만, 컴포넌트 단에서 방어하는 편이 안전합니다.

제안(예시): 표시용 index를 별도로 clamp
 const FunnelProgress = ({ currentIndex, totalSteps }: FunnelProgressProps) => {
   const maxIndex = Math.max(1, totalSteps - 1);
-  const percent = Math.max(0, Math.min(100, (currentIndex / maxIndex) * 100));
+  const safeIndex = Math.max(0, Math.min(currentIndex, Math.max(0, totalSteps - 1)));
+  const percent = Math.max(0, Math.min(100, (safeIndex / maxIndex) * 100));
...
-          const isActive = index <= currentIndex;
+          const isActive = index <= safeIndex;

Also applies to: 21-36

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

In `@apps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsx`
around lines 8 - 11, FunnelProgress currently clamps percent but not the
step-activation logic, so if currentIndex is <0 or >= totalSteps the "active"
circles break; introduce a clamped display index (e.g., displayedIndex =
Math.max(0, Math.min(totalSteps - 1, currentIndex))) and use that displayedIndex
for both the percent calculation and the step active checks (replace usages of
currentIndex in percent and the `index <= currentIndex` checks) while retaining
maxIndex for division safety.

Comment on lines +100 to +110
<label className="mt-[2.4rem] flex max-w-[62rem] items-start gap-[1.2rem]">
<Checkbox
size="small"
isSelected={agreeChecked}
onSelectedChange={onAgreeChange}
/>
<span className="body3-r text-font-gray-3">
내가 북마크한 아티클이 내 Google 이름과 함께 다른 사용자에게 추천될 수
있어요.
</span>
</label>
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

🧩 Analysis chain

🏁 Script executed:

find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/packages/*" | head -20

Repository: Pinback-Team/pinback-client

Length of output: 1275


🏁 Script executed:

rg -n --type=ts --type=tsx "export (function|const) Checkbox|function Checkbox|const Checkbox" packages -A 5

Repository: Pinback-Team/pinback-client

Length of output: 98


🏁 Script executed:

fd "Checkbox" packages -t f \( -name "*.ts" -o -name "*.tsx" \)

Repository: Pinback-Team/pinback-client

Length of output: 301


🏁 Script executed:

cat packages/design-system/src/components/checkbox/Checkbox.tsx

Repository: Pinback-Team/pinback-client

Length of output: 3233


🏁 Script executed:

cat packages/design-system/src/components/checkbox/Checkbox.stories.tsx

Repository: Pinback-Team/pinback-client

Length of output: 1223


라벨 결합 접근성 개선 필요

<label> 태그로 Checkbox를 감싸는 방식은 암묵적 라벨 연결(implicit association)이 작동하지만, Checkbox 컴포넌트가 ID를 외부로 export하지 않아 명시적 연결(htmlFor)을 사용할 수 없습니다.

Checkbox는 내부에 native <input type="checkbox" />를 가지고 있으나, 컴포넌트 ID를 접근할 방법이 없으므로 다음 중 하나를 권장합니다:

  • Checkbox 컴포넌트에 ID export 메서드 추가 (예: useCheckboxId() hook이나 id 반환)
  • 또는 라벨 텍스트를 aria-label / aria-labelledby로 처리

현재 암묵적 연결도 작동하지만, 향후 DOM 구조 변경에 취약할 수 있습니다.

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

In `@apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx` around
lines 100 - 110, The implicit label wrapping the Checkbox in JobStep.tsx is
fragile; update the Checkbox component to accept an id prop (or provide a
useCheckboxId hook) and forward that id to its internal <input>; then in
JobStep, generate a stable unique id, pass it as id to Checkbox (used with
agreeChecked and onAgreeChange), and change the markup so the text uses an
explicit htmlFor linking to that id (or apply aria-labelledby referencing that
id) to ensure durable, explicit label-input association.

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.

프로그래스바 재사용 안되는게 아쉽네요.. 도토리 이미지 한번만 확인해주세요
수고하셨습니당~


return (
<div className="flex w-full flex-col items-center">
<img src={dotori} className="mb-[1.2rem]" alt="dotori" />
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.

image

온보딩에 있는 funnel UI를 비슷하다고 그대로 가져와서 그런 것 같네요...ㅎㅎ 수정하겠습니다

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