Feat(client): 대시보드 직무 핀 선택 팝업 추가#266
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (6)
Walkthrough직무 선택 펀널을 온보딩 흐름에 통합합니다. Google OAuth 응답에서 hasJob 플래그를 추출하여 localStorage에 저장하고, Remind 페이지에서 미선택 사용자를 위한 3단계 펀널(직무 선택 → 핀 → 공유)을 모달로 표시합니다. Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 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:useEffectdeps 누락은 의도인지 확인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 calculationinfo.level * 20is 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
⛔ Files ignored due to path filters (2)
apps/client/public/assets/jobSelectionFunnel/pinStep_description.svgis excluded by!**/*.svgapps/client/public/assets/jobSelectionFunnel/shareStep_description.svgis excluded by!**/*.svg
📒 Files selected for processing (7)
apps/client/src/pages/onBoarding/GoogleCallback.tsxapps/client/src/pages/remind/Remind.tsxapps/client/src/shared/components/jobSelectionFunnel/FunnelProgress.tsxapps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsxapps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsxapps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsxapps/client/src/shared/components/jobSelectionFunnel/step/ShareStep.tsx
| const [showJobSelectionFunnel, setShowJobSelectionFunnel] = useState( | ||
| () => localStorage.getItem('hasJob') === 'false' | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| {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> | ||
| )} |
There was a problem hiding this comment.
🛠️ 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.
| const FunnelProgress = ({ currentIndex, totalSteps }: FunnelProgressProps) => { | ||
| const maxIndex = Math.max(1, totalSteps - 1); | ||
| const percent = Math.max(0, Math.min(100, (currentIndex / maxIndex) * 100)); | ||
|
|
There was a problem hiding this comment.
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.
apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx
Show resolved
Hide resolved
| <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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -path "*/packages/*" | head -20Repository: 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 5Repository: 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.tsxRepository: Pinback-Team/pinback-client
Length of output: 3233
🏁 Script executed:
cat packages/design-system/src/components/checkbox/Checkbox.stories.tsxRepository: 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.
apps/client/src/shared/components/jobSelectionFunnel/step/PinStep.tsx
Outdated
Show resolved
Hide resolved
jjangminii
left a comment
There was a problem hiding this comment.
프로그래스바 재사용 안되는게 아쉽네요.. 도토리 이미지 한번만 확인해주세요
수고하셨습니당~
|
|
||
| return ( | ||
| <div className="flex w-full flex-col items-center"> | ||
| <img src={dotori} className="mb-[1.2rem]" alt="dotori" /> |
There was a problem hiding this comment.
피그마상에서 도토리 못본거 같은데 어디 있었을까요?

📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
기존에 추가한
useFunnel훅 써서 구현했습니다.처음 구글 로그인하고 response로 내려오는
hasJob(boolean)을 로컬스토리지에 저장하는데, 이를로그인 후 첫페이지인 remind 페이지에서 초기 값 지정을 통해, funnel을 보여주는 분기처리를 추가했어요.
사실 해당 뷰는 현재 기존 유저가 모두 hasJob을 true로 가지게 되면 없애도 되는 뷰라고 생각해주시면 될 것 같아요. 따라서 임시로 일단 로컬스토리지에 간단하게 구현해봤습니다. 더 좋은 방법이 있다면 편하게 말해주세요!!
📷 Screenshot
2026-02-24.12.47.16.mov
Summary by CodeRabbit
릴리스 노트