Skip to content

Feat(client): 직무 조회 API 연결 (온보딩/대시보드)#274

Merged
constantly-dev merged 10 commits intodevelopfrom
feat/#273/job-get-list-and-selection-api
Feb 25, 2026
Merged

Feat(client): 직무 조회 API 연결 (온보딩/대시보드)#274
constantly-dev merged 10 commits intodevelopfrom
feat/#273/job-get-list-and-selection-api

Conversation

@constantly-dev
Copy link
Member

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

📌 Related Issues

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

📄 Tasks

  • 직무 조회 API 연결 (온보딩/대시보드)

⭐ PR Point (To Reviewer)

API 연결

이전에 목업 데이터로 되어있던 직무들(디자이너, 기획자, 프론트엔드/백엔드 개발자 등)을 데이터로 받아오도록 API 연결했습니다.

file change가 많긴한데 onboarding/funnel 부분이랑 dashboard/job funnel 부분 폴더구조를 바꿔서 그래요!
axios/query 부분이랑 온보딩과 대시보드 각 JobStep.tsx를 중심으로 봐주시면 될 것 같아요. 추가로 스켈레톤 피그마에는 없지만 임시로 넣어뒀어요.

unused import문 저장 시 자동 제거 설정 추가

사용하지 않는 import문 등이 저장할 때 자동으로 안사라져서 실수로 남겨둘 때 build error가 많이 뜨더라구요. 그래서

"source.organizeImports": "always"

setting.json에 추가하긴 했는데 불필요하다고 생각이 드시거나, 불편하시면 편하게 의견 말씀해주세요!


📷 Screenshot

2026-02-25.6.40.40.mov

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 직업 선택 단계 UI가 개선되었으며, 직업 카드 그리드가 더욱 효율적으로 로드됩니다.
  • 버그 수정

    • 직업 선택 단계에서 직업 선택 및 동의 확인을 모두 완료해야 다음 단계로 진행할 수 있도록 검증 로직이 강화되었습니다.
  • 개선 사항

    • 온보딩 단계 구조가 최적화되고 컴포넌트 로딩 성능이 개선되었습니다.

@vercel
Copy link

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

@github-actions
Copy link

github-actions bot commented Feb 25, 2026

✅ Storybook chromatic 배포 확인:
🐿️ storybook

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent 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 215aa93 and c99d6b1.

📒 Files selected for processing (2)
  • .vscode/settings.json
  • apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx

워크스루

온보딩 단계에서 직무 선택 기능을 새로운 컴포넌트 구조로 재구현. 직무 조회 API를 연결하고, 상태 관리를 통해 선택된 직무를 추적하며, 버튼 활성화 로직에 선택 필수 검증을 추가했습니다. 기존 JobStep 컴포넌트를 제거하고 더 모듈화된 구조로 재작성.

변경 사항

코호트 / 파일 요약
온보딩 메인 흐름
apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx, apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts
selectedJob 상태를 훅에 추가하고 선택된 직무를 JobStep으로 전달. 다음 버튼 활성화 조건에 직무 선택 검증 추가. jobShareAgree 초기값을 true에서 false로 변경.
직무 선택 단계 컴포넌트
apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx (신규), apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsx (신규), apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsx (신규), apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx (신규)`
새로운 디렉토리 구조로 JobStep 컴포넌트 재작성. JobCards에서 직무 목록을 렌더링하고 JobCardsSkeleton으로 로딩 상태 처리. Suspense 경계로 감싼 구조.
기존 JobStep 제거
apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx (삭제), apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx (삭제)
기존 위치의 JobStep 컴포넌트 및 관련 타입 정의 완전 제거.
API 및 쿼리 통합
apps/client/src/shared/apis/axios.ts, apps/client/src/shared/apis/queries.ts
직무 조회 API(getJobs) 추가. 표준 쿼리(useGetJobs)와 Suspense 쿼리(useSuspenseGetJobs) 훅 구현. 무한 staleTime으로 캐싱 최적화.
타입 및 임포트 경로
apps/client/src/shared/types/api.ts, apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx, apps/client/src/pages/onBoarding/components/funnel/step/alarm/AlarmStep.tsx
JobOption, JobsResponse 인터페이스 추가. 컴포넌트 임포트 경로 업데이트 및 AlarmBox 임포트 경로 심화.
IDE 설정
.vscode/settings.json
에디터 저장 시 import 자동 정렬 기능 추가.

시퀀스 다이어그램

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 실행
Loading

예상 코드 리뷰 난이도

🎯 4 (복잡함) | ⏱️ ~45분

관련 PR

  • PR #257: 동일한 온보딩 jobStep과 체크박스 통합을 구현하며, JobStepProps 및 agreeChecked/onAgreeChange 핸들러 연결이 동일합니다.
  • PR #261: 온보딩 훅(useOnboardingFunnel)과 MainCard를 수정하며, selectedJob 상태 추가 및 JobStep 통합이 직접 연관됩니다.
  • PR #266: 직무 선택 funnel UI(JobSelectionFunnel, JobStep 관련 컴포넌트/훅)를 수정하여 동일한 흐름을 다룹니다.

제안 라벨

api

제안 리뷰어

  • jllee000
  • jjangminii

🐰 직무 선택의 여정, API와 함께
상태 관리로 흐름을 잡고
스켈레톤이 기다리는 로딩,
Suspense의 경계 너머
직무 카드가 반짝인다,
온보딩이 완성되는 순간까지! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

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.
Linked Issues check ❓ Inconclusive PR이 #273(직무 조회 API 연결)의 요구사항을 충족하나, #25(프로그레스 바)와의 연관성은 불명확하여 혼재되어 있습니다. PR #274가 실제로 #25와 관련이 있는지 확인하고, 해당 사항이 없다면 Related Issues에서 #25를 제거하거나 명확한 연관성을 기술하세요.
Out of Scope Changes check ❓ Inconclusive PR의 대부분의 변경사항은 #273(직무 조회 API 연결) 범위 내에 있으나, .vscode/settings.json의 import 자동 정렬 설정은 추가적인 변경사항입니다. import 자동 정렬 설정(.vscode/settings.json)이 #273에 포함되어야 하는 변경인지, 아니면 별도의 변경사항인지 명확히 하세요.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 직무 조회 API 연결이라는 주요 변경사항을 명확하게 요약하고 있으며, 온보딩/대시보드 범위도 명시되어 있습니다.
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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#273/job-get-list-and-selection-api

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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 인터페이스는 email, 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: shared JobStep.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.tsxapps/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: useGetJobsuseSuspenseGetJobs의 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 기반 자동 선택 — 상태 업데이트 타이밍 주의.

activeJobnull일 때 첫 번째 직무를 자동 선택하는 로직은 렌더 후 effect로 동작하므로, 초기 렌더 시 선택되지 않은 상태가 한 프레임 노출된 뒤 re-render됩니다. 또한 onSelectJob이 부모에서 매 렌더마다 새로 생성되면 effect가 불필요하게 재실행될 수 있습니다. 부모 쪽에서 onSelectJobuseCallback으로 안정화하거나, 초기 선택 로직을 부모 컴포넌트로 이동하는 것을 권장합니다.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 5499fe0 and 3f2dbd0.

📒 Files selected for processing (21)
  • apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/alarm/AlarmStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/final/FinalStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/job/JobCards.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/job/JobCardsSkeleton.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/job/JobStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/mac/MacStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/socialLogin/SocialLoginStep.tsx
  • apps/client/src/pages/onBoarding/components/funnel/step/story/StoryStep.tsx
  • apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts
  • apps/client/src/shared/apis/axios.ts
  • apps/client/src/shared/apis/queries.ts
  • apps/client/src/shared/components/jobSelectionFunnel/JobSelectionFunnel.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/JobStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCards.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/job/JobCardsSkeleton.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/job/JobStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/pin/PinStep.tsx
  • apps/client/src/shared/components/jobSelectionFunnel/step/share/ShareStep.tsx
  • apps/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

Comment on lines 1 to 96
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;
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

이 파일은 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.

Comment on lines +1 to +53
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;
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

onBoarding/JobStep.tsx와 거의 동일한 코드 — 중복을 통합해 주세요.

이 shared JobStepapps/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.

Suggested change
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.

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.

"source.organizeImports": "always" 이건 처음보는데 하나 알아갑니당~

@constantly-dev constantly-dev merged commit a1ee533 into develop Feb 25, 2026
9 checks passed
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] 직무 조회 API 연결 (온보딩/대시보드)

2 participants