From dcdc2078382151706547d1c3291fefaea1aa2725 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Wed, 25 Feb 2026 16:52:57 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EC=A7=81=EB=AC=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20get=20api=20function=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/shared/apis/axios.ts | 7 ++++++- apps/client/src/shared/apis/queries.ts | 23 +++++++++++++++++++++-- apps/client/src/shared/types/api.ts | 9 +++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/client/src/shared/apis/axios.ts b/apps/client/src/shared/apis/axios.ts index 898f4868..9762b213 100644 --- a/apps/client/src/shared/apis/axios.ts +++ b/apps/client/src/shared/apis/axios.ts @@ -1,5 +1,5 @@ import apiRequest from '@shared/apis/setting/axiosInstance'; -import { EditArticleRequest } from '@shared/types/api'; +import { EditArticleRequest, JobsResponse } from '@shared/types/api'; import { formatLocalDateTime } from '@shared/utils/formatDateTime'; export const getDashboardCategories = async () => { @@ -81,3 +81,8 @@ export const getMyProfile = async () => { const { data } = await apiRequest.get('/api/v2/users/me'); return data.data; }; + +export const getJobs = async (): Promise => { + const { data } = await apiRequest.get('/api/v3/enums/jobs'); + return data.data; +}; diff --git a/apps/client/src/shared/apis/queries.ts b/apps/client/src/shared/apis/queries.ts index 33431aaf..4d2f9488 100644 --- a/apps/client/src/shared/apis/queries.ts +++ b/apps/client/src/shared/apis/queries.ts @@ -3,6 +3,7 @@ import { UseMutationResult, useQuery, UseQueryResult, + useSuspenseQuery, } from '@tanstack/react-query'; import { deleteCategory, @@ -18,6 +19,7 @@ import { deleteRemindArticle, getGoogleProfile, getMyProfile, + getJobs, } from '@shared/apis/axios'; import { AxiosError } from 'axios'; import { @@ -26,6 +28,7 @@ import { EditArticleRequest, ArticleReadStatusResponse, ArticleDetailResponse, + JobsResponse, } from '@shared/types/api'; import { fetchOGData } from '@shared/utils/fetchOgData'; @@ -146,9 +149,25 @@ export const useGetGoogleProfile = () => { }); }; -export function useGetMyProfile() { +export const useGetMyProfile = () => { return useQuery({ queryKey: ['myProfile'], queryFn: getMyProfile, }); -} +}; + +export const useGetJobs = (): UseQueryResult => { + return useQuery({ + queryKey: ['jobs'], + queryFn: getJobs, + staleTime: Infinity, + }); +}; + +export const useSuspenseGetJobs = () => { + return useSuspenseQuery({ + queryKey: ['jobs'], + queryFn: getJobs, + staleTime: Infinity, + }); +}; diff --git a/apps/client/src/shared/types/api.ts b/apps/client/src/shared/types/api.ts index aa3ec6db..e95fd1a8 100644 --- a/apps/client/src/shared/types/api.ts +++ b/apps/client/src/shared/types/api.ts @@ -39,3 +39,12 @@ export interface ArticleDetailResponse { createdAt: string; categoryResponse: CategoryResponse; } + +export interface JobOption { + imageUrl: string; + job: string; +} + +export interface JobsResponse { + jobs: JobOption[]; +} From 9fe3a244fc226a0864b3760e55c74687963cd17e Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Wed, 25 Feb 2026 16:53:31 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20onboarding=20funnel=20job=20state?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts b/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts index 571d8333..bc9a1426 100644 --- a/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts +++ b/apps/client/src/pages/onBoarding/hooks/useOnboardingFunnel.ts @@ -27,6 +27,7 @@ export function useOnboardingFunnel() { const [remindTime, setRemindTime] = useState('09:00'); const [fcmToken, setFcmToken] = useState(null); const [jobShareAgree, setJobShareAgree] = useState(true); + const [selectedJob, setSelectedJob] = useState(null); useEffect(() => { const storedEmail = localStorage.getItem('email'); @@ -149,8 +150,10 @@ export function useOnboardingFunnel() { direction, alarmSelected, jobShareAgree, + selectedJob, setAlarmSelected, setJobShareAgree, + setSelectedJob, nextStep, prevStep, }; From 173789a1f2af9226a648faf641aaaf1bc54d7106 Mon Sep 17 00:00:00 2001 From: constantly-dev Date: Wed, 25 Feb 2026 17:01:41 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20jobs=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onBoarding/components/funnel/MainCard.tsx | 6 +- .../components/funnel/step/JobStep.tsx | 165 ++++++++++-------- 2 files changed, 94 insertions(+), 77 deletions(-) diff --git a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx index 71763bcd..c922669d 100644 --- a/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx +++ b/apps/client/src/pages/onBoarding/components/funnel/MainCard.tsx @@ -47,8 +47,10 @@ const MainCard = () => { direction, alarmSelected, jobShareAgree, + selectedJob, setAlarmSelected, setJobShareAgree, + setSelectedJob, nextStep, prevStep, } = useOnboardingFunnel(); @@ -66,6 +68,8 @@ const MainCard = () => { case Step.JOB: return ( @@ -132,7 +136,7 @@ const MainCard = () => { size="medium" className="ml-auto w-[4.8rem]" onClick={nextStep} - isDisabled={step === Step.JOB && !jobShareAgree} + isDisabled={step === Step.JOB && (!jobShareAgree || !selectedJob)} > 다음 diff --git a/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx b/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx index 440adb31..40d62363 100644 --- a/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx +++ b/apps/client/src/pages/onBoarding/components/funnel/step/JobStep.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { Suspense, useEffect } from 'react'; import { cva } from 'class-variance-authority'; import dotori from '/assets/onBoarding/icons/dotori.svg'; import jobPlan from '/assets/onBoarding/jobs/jobPlan.svg'; @@ -6,14 +6,18 @@ 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 { Checkbox } from '@pinback/design-system/ui'; - -type JobKey = 'planner' | 'designer' | 'frontend' | 'backend'; +import { useSuspenseGetJobs } from '@shared/apis/queries'; interface JobStepProps { - selectedJob?: JobKey; - onSelectJob?: (job: JobKey) => void; - agreeChecked?: boolean; - onAgreeChange?: (checked: boolean) => void; + selectedJob: string | null; + onSelectJob: (jobName: string) => void; + agreeChecked: boolean; + onAgreeChange: (checked: boolean) => void; +} + +interface JobCardsProps { + activeJob: string | null; + onSelectJob: (jobName: string) => void; } const jobCardStyle = cva( @@ -29,58 +33,94 @@ const jobCardStyle = cva( } ); -const JobIcon = ({ type }: { type: JobKey }) => { - const iconMap: Record = { - planner: jobPlan, - designer: jobDesign, - frontend: jobFrontend, - backend: jobBackend, - }; +const jobImageByName: Record = { + 기획자: jobPlan, + 디자이너: jobDesign, + 프론트엔드: jobFrontend, + 백엔드: jobBackend, +}; +const JobIcon = ({ + jobName, + imageUrl, +}: { + jobName: string; + imageUrl: string; +}) => { + const resolvedImageUrl = jobImageByName[jobName] ?? imageUrl; return ( ); }; +const JobCardsSkeleton = () => { + return ( +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+ ))} +
+ ); +}; + +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 ( +
+ {jobOptions.map(({ imageUrl: jobImageUrl, job: jobName }) => { + const isSelected = activeJob === jobName; + return ( + + ); + })} +
+ ); +}; + const JobStep = ({ selectedJob, onSelectJob, agreeChecked, onAgreeChange, }: JobStepProps) => { - const defaultJob: JobKey = 'planner'; - const [internalJob, setInternalJob] = useState(defaultJob); - const [internalAgree, setInternalAgree] = useState(true); - - const activeJob = selectedJob ?? internalJob; - const activeAgree = agreeChecked ?? internalAgree; - - const jobs: { key: JobKey; label: string }[] = [ - { key: 'planner', label: '기획자' }, - { key: 'designer', label: '디자이너' }, - { key: 'frontend', label: '프론트엔드' }, - { key: 'backend', label: '백엔드' }, - ]; - - const handleSelect = (job: JobKey) => { - onSelectJob?.(job); - if (!onSelectJob || selectedJob === undefined) { - setInternalJob(job); - } - }; - - const handleAgreeChange = (checked: boolean) => { - onAgreeChange?.(checked); - if (!onAgreeChange || agreeChecked === undefined) { - setInternalAgree(checked); - } - }; - return (
dotori @@ -91,42 +131,15 @@ const JobStep = ({

-
- {jobs.map((job) => { - const isSelected = activeJob === job.key; - return ( - - ); - })} -
+ }> + +