Feat(client): 직무 조회 API 연결 (온보딩/대시보드)#274
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
✅ Storybook chromatic 배포 확인: |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review infoConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
워크스루온보딩 단계에서 직무 선택 기능을 새로운 컴포넌트 구조로 재구현. 직무 조회 API를 연결하고, 상태 관리를 통해 선택된 직무를 추적하며, 버튼 활성화 로직에 선택 필수 검증을 추가했습니다. 기존 JobStep 컴포넌트를 제거하고 더 모듈화된 구조로 재작성. 변경 사항
시퀀스 다이어그램sequenceDiagram
actor User
participant MainCard
participant useOnboardingFunnel
participant JobStep
participant useQueryClient
participant API as getJobs API
participant QueryCache as React Query Cache
User->>MainCard: 온보딩 시작
MainCard->>useOnboardingFunnel: 초기화 (selectedJob = null)
Note over MainCard: JOB 단계 도달
MainCard->>JobStep: props 전달 (selectedJob, onSelectJob)
JobStep->>useQueryClient: 캐시된 jobs 조회
alt 캐시 없음
useQueryClient-->>JobStep: undefined
else 캐시 있음
QueryCache-->>useQueryClient: JobsResponse
useQueryClient-->>JobStep: jobs 개수
end
JobStep->>JobStep: Suspense 경계 내 JobCards 렌더링
alt 로딩 중
JobStep->>JobStep: JobCardsSkeleton 표시
else 로드 완료
API-->>JobStep: 직무 목록 수신
JobStep->>JobStep: JobCards 렌더링
end
User->>JobStep: 직무 선택
JobStep->>useOnboardingFunnel: onSelectJob 호출
useOnboardingFunnel->>useOnboardingFunnel: selectedJob 상태 업데이트
User->>JobStep: 동의 체크박스 클릭
JobStep->>useOnboardingFunnel: onAgreeChange 호출
MainCard->>MainCard: 다음 버튼 활성화 조건 재평가
Note over MainCard: isDisabled = !jobShareAgree || !selectedJob
User->>MainCard: 다음 단계 진행
MainCard->>useOnboardingFunnel: nextStep 실행
예상 코드 리뷰 난이도🎯 4 (복잡함) | ⏱️ ~45분 관련 PR
제안 라벨
제안 리뷰어
시
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✅ Passed checks (2 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx (1)
28-34:⚠️ Potential issue | 🟠 Major
onComplete콜백이 선택된 직무 정보를 전달하지 않습니다.
handleNext완료 시selectedJob값을 부모 컴포넌트에서 접근할 수 없습니다. Remind.tsx의 TODO 주석("관심 직무 핀 API 연동 필요")에서 API 연동 시selectedJob이 필요하지만, 현재는 컴포넌트 내부에만 캡슐화되어 있습니다.onComplete콜백에selectedJob을 인자로 전달하거나, 별도의 콜백(예:onCompleteWithJob)을 추가하여 부모에서 값을 활용할 수 있도록 수정이 필요합니다.🤖 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 28 - 34, handleNext currently calls onComplete() without passing the selected job, so the parent cannot access selectedJob for the Remind.tsx API work; update handleNext to call onComplete(selectedJob) (or add a new prop like onCompleteWithJob(selectedJob) and call that) instead of onComplete(), keep the existing isLastStep/return flow and goNext() behavior, and update the component props/type signatures and any parent usages to accept the job argument so the selectedJob is available to the caller.apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts (1)
106-118:⚠️ Potential issue | 🟡 Minor
selectedJob이 회원가입 요청에 포함되지 않습니다.
postSignData호출 시selectedJob값이 payload에 포함되어 있지 않습니다.postSignUpRequest인터페이스는remindDefault,fcmToken만 정의하며, 선택된 직무 정보는 전달되지 않습니다. 직무 선택이 UI에서 필수 항목으로 검증되고 있으므로, 해당 데이터를 서버로 전송하거나 별도 API로 저장해야 할 것으로 보입니다. 의도적으로 별도 방식으로 처리하는 것이라면 무시해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts` around lines 106 - 118, The final signup branch omits selectedJob from the payload: when isFinalStep is true you call postSignData with { email: userEmail, remindDefault: remindTime, fcmToken } but selectedJob (the UI-required job selection) is not sent; update the postSignData call to include selectedJob (or call the appropriate saveJob API) and ensure the payload type (postSignUpRequest) is extended or mapped to include selectedJob so the server receives the chosen job; touch the postSignData invocation and the postSignUpRequest type/mapper to pass selectedJob alongside userEmail, remindTime, and fcmToken.
♻️ Duplicate comments (2)
apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx (1)
1-55: sharedJobStep.tsx와 거의 동일한 중복 — 통합 필요.앞서
apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx리뷰에서 언급한 대로, dotori 아이콘 차이만 존재합니다. 하나의 컴포넌트로 통합해 주세요.추가로, 이 파일의
JobStepProps인터페이스는export되지 않았지만 shared 버전에서는export됩니다. 통합 시 일관성 있게 처리해 주세요.🤖 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/job/JobStep.tsx` around lines 1 - 55, This file duplicates the shared JobStep; consolidate by removing this app-specific JobStep and using the shared component (or move the shared one up) and add a prop to handle the only visual difference (dotori icon) so the icon can be injected (e.g., add an iconSrc or Icon component prop used where this file imports dotori), ensure JobStepProps is exported the same as the shared version (export interface JobStepProps) and keep usages of JobCards, JobCardsSkeleton and Suspense the same; update imports/usages to pass the icon prop and export the unified JobStep and JobStepProps for consistent reuse.apps/client/src/pages/onBoarding/components/funnel/step/job/JobCards.tsx (1)
1-96: 이 파일은 shared 경로의JobCards.tsx와 완전히 동일한 중복 파일입니다.
apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsx에 이미 동일한 컴포넌트가 존재합니다. 이 파일을 제거하고 shared 컴포넌트를 re-export하거나 직접 import하도록 변경해 주세요.🤖 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/job/JobCards.tsx` around lines 1 - 96, This file is a duplicate of the shared JobCards component; remove this duplicate and switch consumers to the single shared implementation: delete this component file, ensure the shared JobCards component (default export name JobCards) is re-exported from a central barrel or imported directly by places that used this duplicate, and update any imports that referenced this duplicate to import the shared JobCards instead so the same JobCards symbol is used across the codebase.
🧹 Nitpick comments (4)
apps/client/src/pages/onBoarding/components/funnel/step/job/JobCardsSkeleton.tsx (1)
1-18: 동일한JobCardsSkeleton컴포넌트의 중복을 제거하세요.
apps/client/src/pages/onBoarding/components/funnel/step/job/JobCardsSkeleton.tsx는apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsx와 완전히 동일합니다. onBoarding의 JobStep에서 shared 컴포넌트를 import하여 재사용하면 중복 코드를 제거할 수 있습니다.🤖 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/job/JobCardsSkeleton.tsx` around lines 1 - 18, Remove the duplicate JobCardsSkeleton component in apps/client/src/pages/onBoarding/... by replacing its usage with the shared component export; update the onBoarding JobStep (where JobCardsSkeleton is imported/used) to import JobCardsSkeleton from the existing shared module (shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsx) and delete the duplicate file (JobCardsSkeleton.tsx) in the onBoarding folder; ensure the imported symbol name JobCardsSkeleton and its props (count?: number) remain unchanged so consumers compile without modifications.apps/client/src/shared/apis/queries.ts (1)
159-173:useGetJobs와useSuspenseGetJobs의 query 설정이 중복됩니다.두 hook 모두 동일한
queryKey,queryFn,staleTime을 사용합니다. 공통 옵션을 상수로 추출하면 향후 키나 설정 변경 시 한 곳만 수정하면 됩니다.♻️ 수정 제안
+const jobsQueryOptions = { + queryKey: ['jobs'] as const, + queryFn: getJobs, + staleTime: Infinity, +}; + export const useGetJobs = (): UseQueryResult<JobsResponse, AxiosError> => { - return useQuery({ - queryKey: ['jobs'], - queryFn: getJobs, - staleTime: Infinity, - }); + return useQuery(jobsQueryOptions); }; export const useSuspenseGetJobs = () => { - return useSuspenseQuery({ - queryKey: ['jobs'], - queryFn: getJobs, - staleTime: Infinity, - }); + return useSuspenseQuery(jobsQueryOptions); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/client/src/shared/apis/queries.ts` around lines 159 - 173, Extract the duplicated query configuration into a shared constant (e.g., const jobsQueryOptions) and reuse it in both useGetJobs and useSuspenseGetJobs; specifically capture the common bits (queryKey: ['jobs'], queryFn: getJobs, staleTime: Infinity) into that constant and spread or pass it into useQuery and useSuspenseQuery so future changes to the key or options only require updating the single jobsQueryOptions definition.apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsx (2)
56-60:useEffect기반 자동 선택 — 상태 업데이트 타이밍 주의.
activeJob이null일 때 첫 번째 직무를 자동 선택하는 로직은 렌더 후 effect로 동작하므로, 초기 렌더 시 선택되지 않은 상태가 한 프레임 노출된 뒤 re-render됩니다. 또한onSelectJob이 부모에서 매 렌더마다 새로 생성되면 effect가 불필요하게 재실행될 수 있습니다. 부모 쪽에서onSelectJob을useCallback으로 안정화하거나, 초기 선택 로직을 부모 컴포넌트로 이동하는 것을 권장합니다.🤖 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/job/JobCards.tsx` around lines 56 - 60, The effect in JobCards.tsx that auto-selects the first job (useEffect watching activeJob, jobOptions, onSelectJob) causes a visible unselected frame and can re-run if onSelectJob is recreated each render; fix by moving the initial-selection logic out of an effect into the parent or stabilizing the callback: either have the parent set the initial active job when it initializes (so JobCards receives a non-null activeJob) or wrap the parent's onSelectJob in useCallback to prevent unnecessary effect triggers; update references to activeJob, jobOptions, and onSelectJob accordingly and remove the auto-selecting useEffect if parent handles initialization.
43-48:aria-hidden="true"와alt텍스트가 모순됩니다.아이콘 아래에 이미
jobName이 텍스트로 렌더링되므로 이미지는 장식용(decorative)입니다.aria-hidden="true"는 올바르지만, 이 경우alt는 빈 문자열("")이어야 합니다. 의미 있는alt텍스트와aria-hidden="true"를 동시에 사용하면 스크린 리더 동작이 일관되지 않을 수 있습니다.♻️ 수정 제안
<img src={resolvedImageUrl} - alt={`${jobName} 직무 아이콘`} + alt="" aria-hidden="true" className="h-[10.2rem] w-[10.2rem]" />🤖 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/job/JobCards.tsx` around lines 43 - 48, The img in JobCards.tsx uses aria-hidden="true" but also provides a non-empty alt (alt={`${jobName} 직무 아이콘`}), which is contradictory; since the jobName is rendered as visible text and the image is decorative, change the alt to an empty string ("") and keep aria-hidden="true" (or remove aria-hidden if you prefer relying solely on alt=""). Update the img element that references resolvedImageUrl and jobName accordingly so screen readers ignore the decorative image consistently.
🤖 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/shared/components/jobSelectionFunnel/step/job/JobCards.tsx`:
- Around line 1-96: There are duplicate JobCards implementations; remove the
copy in pages/onBoarding and have that package import the shared one to avoid
divergence: delete the file that duplicates the shared JobCards component, keep
the shared file (containing JobCards, JobIcon, jobCardStyle, jobImageByName, and
useSuspenseGetJobs usage) as the single source of truth, update all imports in
onBoarding code to import JobCards from the shared path, and run
typechecks/build/tests to fix any broken import paths or relative asset
references after switching to the shared component.
In `@apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx`:
- Around line 1-53: The two JobStep components are duplicated; consolidate them
by making the shared JobStep accept an optional prop (e.g., headerIcon or
headerChildren) to render the dotori icon when needed, then remove the
near-duplicate component and update the onBoarding usage to import the shared
JobStep and pass the icon prop; update the JobStep signature (JobStepProps) and
JSX to conditionally render the optional header prop while keeping existing
props (selectedJob, onSelectJob, agreeChecked, onAgreeChange) and behavior
unchanged.
---
Outside diff comments:
In `@apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts`:
- Around line 106-118: The final signup branch omits selectedJob from the
payload: when isFinalStep is true you call postSignData with { email: userEmail,
remindDefault: remindTime, fcmToken } but selectedJob (the UI-required job
selection) is not sent; update the postSignData call to include selectedJob (or
call the appropriate saveJob API) and ensure the payload type
(postSignUpRequest) is extended or mapped to include selectedJob so the server
receives the chosen job; touch the postSignData invocation and the
postSignUpRequest type/mapper to pass selectedJob alongside userEmail,
remindTime, and fcmToken.
In `@apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx`:
- Around line 28-34: handleNext currently calls onComplete() without passing the
selected job, so the parent cannot access selectedJob for the Remind.tsx API
work; update handleNext to call onComplete(selectedJob) (or add a new prop like
onCompleteWithJob(selectedJob) and call that) instead of onComplete(), keep the
existing isLastStep/return flow and goNext() behavior, and update the component
props/type signatures and any parent usages to accept the job argument so the
selectedJob is available to the caller.
---
Duplicate comments:
In `@apps/client/src/pages/onBoarding/components/funnel/step/job/JobCards.tsx`:
- Around line 1-96: This file is a duplicate of the shared JobCards component;
remove this duplicate and switch consumers to the single shared implementation:
delete this component file, ensure the shared JobCards component (default export
name JobCards) is re-exported from a central barrel or imported directly by
places that used this duplicate, and update any imports that referenced this
duplicate to import the shared JobCards instead so the same JobCards symbol is
used across the codebase.
In `@apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx`:
- Around line 1-55: This file duplicates the shared JobStep; consolidate by
removing this app-specific JobStep and using the shared component (or move the
shared one up) and add a prop to handle the only visual difference (dotori icon)
so the icon can be injected (e.g., add an iconSrc or Icon component prop used
where this file imports dotori), ensure JobStepProps is exported the same as the
shared version (export interface JobStepProps) and keep usages of JobCards,
JobCardsSkeleton and Suspense the same; update imports/usages to pass the icon
prop and export the unified JobStep and JobStepProps for consistent reuse.
---
Nitpick comments:
In
`@apps/client/src/pages/onBoarding/components/funnel/step/job/JobCardsSkeleton.tsx`:
- Around line 1-18: Remove the duplicate JobCardsSkeleton component in
apps/client/src/pages/onBoarding/... by replacing its usage with the shared
component export; update the onBoarding JobStep (where JobCardsSkeleton is
imported/used) to import JobCardsSkeleton from the existing shared module
(shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsx) and delete
the duplicate file (JobCardsSkeleton.tsx) in the onBoarding folder; ensure the
imported symbol name JobCardsSkeleton and its props (count?: number) remain
unchanged so consumers compile without modifications.
In `@apps/client/src/shared/apis/queries.ts`:
- Around line 159-173: Extract the duplicated query configuration into a shared
constant (e.g., const jobsQueryOptions) and reuse it in both useGetJobs and
useSuspenseGetJobs; specifically capture the common bits (queryKey: ['jobs'],
queryFn: getJobs, staleTime: Infinity) into that constant and spread or pass it
into useQuery and useSuspenseQuery so future changes to the key or options only
require updating the single jobsQueryOptions definition.
In `@apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsx`:
- Around line 56-60: The effect in JobCards.tsx that auto-selects the first job
(useEffect watching activeJob, jobOptions, onSelectJob) causes a visible
unselected frame and can re-run if onSelectJob is recreated each render; fix by
moving the initial-selection logic out of an effect into the parent or
stabilizing the callback: either have the parent set the initial active job when
it initializes (so JobCards receives a non-null activeJob) or wrap the parent's
onSelectJob in useCallback to prevent unnecessary effect triggers; update
references to activeJob, jobOptions, and onSelectJob accordingly and remove the
auto-selecting useEffect if parent handles initialization.
- Around line 43-48: The img in JobCards.tsx uses aria-hidden="true" but also
provides a non-empty alt (alt={`${jobName} 직무 아이콘`}), which is contradictory;
since the jobName is rendered as visible text and the image is decorative,
change the alt to an empty string ("") and keep aria-hidden="true" (or remove
aria-hidden if you prefer relying solely on alt=""). Update the img element that
references resolvedImageUrl and jobName accordingly so screen readers ignore the
decorative image consistently.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsxapps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/alarm/AlarmStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/final/FinalStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/job/JobCards.tsxapps/client/src/pages/onBoarding/components/funnel/step/job/JobCardsSkeleton.tsxapps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/mac/MacStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/socialLogin/SocialLoginStep.tsxapps/client/src/pages/onBoarding/components/funnel/step/story/StoryStep.tsxapps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.tsapps/client/src/shared/apis/axios.tsapps/client/src/shared/apis/queries.tsapps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsxapps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsxapps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsxapps/client/src/shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsxapps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsxapps/client/src/shared/components/jobSelectionFunnel/step/pin/PinStep.tsxapps/client/src/shared/components/jobSelectionFunnel/step/share/ShareStep.tsxapps/client/src/shared/types/api.ts
💤 Files with no reviewable changes (2)
- apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx
- apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx
| import { useEffect } from 'react'; | ||
| import { cva } from 'class-variance-authority'; | ||
| import jobPlan from '/assets/onBoarding/jobs/jobPlan.svg'; | ||
| import jobDesign from '/assets/onBoarding/jobs/jobDesign.svg'; | ||
| import jobFrontend from '/assets/onBoarding/jobs/jobFrontend.svg'; | ||
| import jobBackend from '/assets/onBoarding/jobs/jobBackend.svg'; | ||
| import { useSuspenseGetJobs } from '@shared/apis/queries'; | ||
|
|
||
| interface JobCardsProps { | ||
| activeJob: string | null; | ||
| onSelectJob: (jobName: string) => void; | ||
| } | ||
|
|
||
| const jobCardStyle = cva( | ||
| 'flex h-[22.4rem] w-[18rem] flex-col items-center justify-center rounded-[1.2rem] border transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-main400 focus-visible:ring-offset-2 focus-visible:ring-offset-white-bg', | ||
| { | ||
| variants: { | ||
| selected: { | ||
| true: 'border-main400 bg-main0', | ||
| false: 'border-transparent bg-white-bg hover:border-main300', | ||
| }, | ||
| }, | ||
| defaultVariants: { selected: false }, | ||
| } | ||
| ); | ||
|
|
||
| const jobImageByName: Record<string, string> = { | ||
| 기획자: jobPlan, | ||
| 디자이너: jobDesign, | ||
| 프론트엔드: jobFrontend, | ||
| 백엔드: jobBackend, | ||
| }; | ||
|
|
||
| const JobIcon = ({ | ||
| jobName, | ||
| imageUrl, | ||
| }: { | ||
| jobName: string; | ||
| imageUrl: string; | ||
| }) => { | ||
| const resolvedImageUrl = jobImageByName[jobName] ?? imageUrl; | ||
| return ( | ||
| <img | ||
| src={resolvedImageUrl} | ||
| alt={`${jobName} 직무 아이콘`} | ||
| aria-hidden="true" | ||
| className="h-[10.2rem] w-[10.2rem]" | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| const JobCards = ({ activeJob, onSelectJob }: JobCardsProps) => { | ||
| const { data: jobData } = useSuspenseGetJobs(); | ||
| const jobOptions = jobData.jobs; | ||
|
|
||
| useEffect(() => { | ||
| if (!activeJob && jobOptions.length > 0) { | ||
| onSelectJob(jobOptions[0].job); | ||
| } | ||
| }, [activeJob, jobOptions, onSelectJob]); | ||
|
|
||
| return ( | ||
| <div | ||
| role="radiogroup" | ||
| aria-label="직무 선택" | ||
| className="grid w-full grid-cols-2 justify-items-center gap-[1.4rem] sm:grid-cols-4" | ||
| > | ||
| {jobOptions.map(({ imageUrl: jobImageUrl, job: jobName }) => { | ||
| const isSelected = activeJob === jobName; | ||
| return ( | ||
| <button | ||
| key={jobName} | ||
| type="button" | ||
| role="radio" | ||
| aria-checked={isSelected} | ||
| onClick={() => onSelectJob(jobName)} | ||
| className={jobCardStyle({ selected: isSelected })} | ||
| > | ||
| <div className="flex flex-col items-center gap-[1.6rem]"> | ||
| <JobIcon jobName={jobName} imageUrl={jobImageUrl} /> | ||
| <span | ||
| className={`sub3-sb ${ | ||
| isSelected ? 'text-main500' : 'text-font-black-1' | ||
| }`} | ||
| > | ||
| {jobName} | ||
| </span> | ||
| </div> | ||
| </button> | ||
| ); | ||
| })} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default JobCards; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
이 파일은 apps/client/src/pages/onBoarding/components/funnel/step/job/JobCards.tsx와 완전히 동일한 코드입니다.
두 경로에 동일한 JobCards 컴포넌트가 중복 존재합니다. shared 경로의 이 파일 하나만 유지하고, onBoarding 쪽에서는 이 shared 컴포넌트를 import하여 사용해야 합니다. 동일한 버그 수정이나 기능 변경을 두 곳에 반복 적용해야 하는 유지보수 부담이 생깁니다.
#!/bin/bash
# Description: Verify that the two JobCards.tsx files are identical
diff <(fd -t f 'JobCards.tsx' --exec cat {}) 2>/dev/null || true
# More targeted approach:
echo "=== shared JobCards.tsx ==="
fd -p 'shared/components/jobSelectionFunnel/step/job/JobCards.tsx' --exec md5sum {}
echo "=== onBoarding JobCards.tsx ==="
fd -p 'pages/onBoarding/components/funnel/step/job/JobCards.tsx' --exec md5sum {}🤖 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/job/JobCards.tsx`
around lines 1 - 96, There are duplicate JobCards implementations; remove the
copy in pages/onBoarding and have that package import the shared one to avoid
divergence: delete the file that duplicates the shared JobCards component, keep
the shared file (containing JobCards, JobIcon, jobCardStyle, jobImageByName, and
useSuspenseGetJobs usage) as the single source of truth, update all imports in
onBoarding code to import JobCards from the shared path, and run
typechecks/build/tests to fix any broken import paths or relative asset
references after switching to the shared component.
| import { Suspense } from 'react'; | ||
| import { useQueryClient } from '@tanstack/react-query'; | ||
| import { Checkbox } from '@pinback/design-system/ui'; | ||
| import { JobsResponse } from '@shared/types/api'; | ||
| import JobCards from './JobCards'; | ||
| import JobCardsSkeleton from './JobCardsSkeleton'; | ||
|
|
||
| export interface JobStepProps { | ||
| selectedJob: string | null; | ||
| onSelectJob: (job: string) => void; | ||
| agreeChecked: boolean; | ||
| onAgreeChange: (checked: boolean) => void; | ||
| } | ||
|
|
||
| const JobStep = ({ | ||
| selectedJob, | ||
| onSelectJob, | ||
| agreeChecked, | ||
| onAgreeChange, | ||
| }: JobStepProps) => { | ||
| const queryClient = useQueryClient(); | ||
| const cachedJobs = queryClient.getQueryData<JobsResponse>(['jobs']); | ||
| const skeletonCount = cachedJobs?.jobs.length ?? 4; | ||
|
|
||
| return ( | ||
| <div className="flex w-full flex-col items-center"> | ||
| <div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]"> | ||
| <p className="head3 text-font-black-1">직무를 선택해주세요</p> | ||
| <p className="body2-m text-font-gray-3 text-center"> | ||
| 직무에 따라 아티클을 추천해드려요 | ||
| </p> | ||
| </div> | ||
|
|
||
| <Suspense fallback={<JobCardsSkeleton count={skeletonCount} />}> | ||
| <JobCards activeJob={selectedJob} onSelectJob={onSelectJob} /> | ||
| </Suspense> | ||
|
|
||
| <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> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default JobStep; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
onBoarding/JobStep.tsx와 거의 동일한 코드 — 중복을 통합해 주세요.
이 shared JobStep과 apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx의 유일한 차이점은 dotori 아이콘 유무입니다. 아이콘을 선택적 prop이나 children slot으로 처리하면 하나의 컴포넌트로 통합할 수 있습니다.
♻️ 예시: 선택적 header prop 추가
export interface JobStepProps {
selectedJob: string | null;
onSelectJob: (job: string) => void;
agreeChecked: boolean;
onAgreeChange: (checked: boolean) => void;
+ headerIcon?: React.ReactNode;
}
const JobStep = ({
selectedJob,
onSelectJob,
agreeChecked,
onAgreeChange,
+ headerIcon,
}: JobStepProps) => {
// ...
return (
<div className="flex w-full flex-col items-center">
+ {headerIcon}
<div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]">📝 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.
| import { Suspense } from 'react'; | |
| import { useQueryClient } from '@tanstack/react-query'; | |
| import { Checkbox } from '@pinback/design-system/ui'; | |
| import { JobsResponse } from '@shared/types/api'; | |
| import JobCards from './JobCards'; | |
| import JobCardsSkeleton from './JobCardsSkeleton'; | |
| export interface JobStepProps { | |
| selectedJob: string | null; | |
| onSelectJob: (job: string) => void; | |
| agreeChecked: boolean; | |
| onAgreeChange: (checked: boolean) => void; | |
| } | |
| const JobStep = ({ | |
| selectedJob, | |
| onSelectJob, | |
| agreeChecked, | |
| onAgreeChange, | |
| }: JobStepProps) => { | |
| const queryClient = useQueryClient(); | |
| const cachedJobs = queryClient.getQueryData<JobsResponse>(['jobs']); | |
| const skeletonCount = cachedJobs?.jobs.length ?? 4; | |
| return ( | |
| <div className="flex w-full flex-col items-center"> | |
| <div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]"> | |
| <p className="head3 text-font-black-1">직무를 선택해주세요</p> | |
| <p className="body2-m text-font-gray-3 text-center"> | |
| 직무에 따라 아티클을 추천해드려요 | |
| </p> | |
| </div> | |
| <Suspense fallback={<JobCardsSkeleton count={skeletonCount} />}> | |
| <JobCards activeJob={selectedJob} onSelectJob={onSelectJob} /> | |
| </Suspense> | |
| <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> | |
| </div> | |
| ); | |
| }; | |
| export default JobStep; | |
| import { Suspense } from 'react'; | |
| import { useQueryClient } from '@tanstack/react-query'; | |
| import { Checkbox } from '@pinback/design-system/ui'; | |
| import { JobsResponse } from '@shared/types/api'; | |
| import JobCards from './JobCards'; | |
| import JobCardsSkeleton from './JobCardsSkeleton'; | |
| export interface JobStepProps { | |
| selectedJob: string | null; | |
| onSelectJob: (job: string) => void; | |
| agreeChecked: boolean; | |
| onAgreeChange: (checked: boolean) => void; | |
| headerIcon?: React.ReactNode; | |
| } | |
| const JobStep = ({ | |
| selectedJob, | |
| onSelectJob, | |
| agreeChecked, | |
| onAgreeChange, | |
| headerIcon, | |
| }: JobStepProps) => { | |
| const queryClient = useQueryClient(); | |
| const cachedJobs = queryClient.getQueryData<JobsResponse>(['jobs']); | |
| const skeletonCount = cachedJobs?.jobs.length ?? 4; | |
| return ( | |
| <div className="flex w-full flex-col items-center"> | |
| {headerIcon} | |
| <div className="mb-[2.4rem] flex flex-col items-center gap-[0.8rem]"> | |
| <p className="head3 text-font-black-1">직무를 선택해주세요</p> | |
| <p className="body2-m text-font-gray-3 text-center"> | |
| 직무에 따라 아티클을 추천해드려요 | |
| </p> | |
| </div> | |
| <Suspense fallback={<JobCardsSkeleton count={skeletonCount} />}> | |
| <JobCards activeJob={selectedJob} onSelectJob={onSelectJob} /> | |
| </Suspense> | |
| <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> | |
| </div> | |
| ); | |
| }; | |
| export default JobStep; |
🤖 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/job/JobStep.tsx`
around lines 1 - 53, The two JobStep components are duplicated; consolidate them
by making the shared JobStep accept an optional prop (e.g., headerIcon or
headerChildren) to render the dotori icon when needed, then remove the
near-duplicate component and update the onBoarding usage to import the shared
JobStep and pass the icon prop; update the JobStep signature (JobStepProps) and
JSX to conditionally render the optional header prop while keeping existing
props (selectedJob, onSelectJob, agreeChecked, onAgreeChange) and behavior
unchanged.
jjangminii
left a comment
There was a problem hiding this comment.
"source.organizeImports": "always" 이건 처음보는데 하나 알아갑니당~
📌 Related Issues
📄 Tasks
⭐ PR Point (To Reviewer)
API 연결
이전에 목업 데이터로 되어있던 직무들(디자이너, 기획자, 프론트엔드/백엔드 개발자 등)을 데이터로 받아오도록 API 연결했습니다.
file change가 많긴한데 onboarding/funnel 부분이랑 dashboard/job funnel 부분 폴더구조를 바꿔서 그래요!
axios/query 부분이랑 온보딩과 대시보드 각 JobStep.tsx를 중심으로 봐주시면 될 것 같아요. 추가로 스켈레톤 피그마에는 없지만 임시로 넣어뒀어요.
unused import문 저장 시 자동 제거 설정 추가
사용하지 않는 import문 등이 저장할 때 자동으로 안사라져서 실수로 남겨둘 때 build error가 많이 뜨더라구요. 그래서
setting.json에 추가하긴 했는데 불필요하다고 생각이 드시거나, 불편하시면 편하게 의견 말씀해주세요!
📷 Screenshot
2026-02-25.6.40.40.mov
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정
개선 사항