Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
43e1686
feat: 클립보드 복사 공용 훅 분리
TTOCHIwas Mar 25, 2026
7102da9
feat: 단계 Indicator, 작업 결과 입력 Input 컴포넌트화
TTOCHIwas Mar 25, 2026
47eddb7
feat: useUploadStepFlow 훅 분리
TTOCHIwas Mar 25, 2026
c347620
feat: UploadStepHeaderSection 섹션 분리
TTOCHIwas Mar 25, 2026
91ea403
feat: UploadStepContentSection 섹션 분리
TTOCHIwas Mar 25, 2026
155773d
refector: UploadStepContent 제거 및 UploadStepPage 재구성
TTOCHIwas Mar 25, 2026
801f4f1
refector: useClipboardCopy 섹션으로 위치 변경
TTOCHIwas Mar 25, 2026
7a636fb
refector: useUploadStepFlow 관심사 분리
TTOCHIwas Mar 25, 2026
373248c
refector: UploadStepPage 사용 훅을 각 섹션에서 사용하도록 위치 이동 및 Props 정리
TTOCHIwas Mar 25, 2026
7a92475
feat: 뒤로가기 버튼 공용 컴포넌트화
TTOCHIwas Mar 25, 2026
d5df9db
refector: useUploadStepActions 역할 분리
TTOCHIwas Mar 25, 2026
d66f02b
refactor: 업로드 step 페이지 잘못된 경로 처리 방식 개선
TTOCHIwas Mar 25, 2026
6910cd7
refector: ROADMAP_TYPE_LABEL 사용
TTOCHIwas Mar 25, 2026
b724add
chore: 불필요한 속성 제거
TTOCHIwas Mar 25, 2026
dcee919
refector: 진행 중인 프로젝트 선택 시 UploadStepPage로 이동
TTOCHIwas Mar 25, 2026
39f598e
refactor: 업로드 step 경로 생성 및 진행 중 프로젝트 진입 흐름 정리
TTOCHIwas Mar 25, 2026
7d885b7
chore: 컴포넌트 경로 변경
TTOCHIwas Mar 25, 2026
b5c47e4
chore: 한글 깨짐 수정
TTOCHIwas Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/features/mypage/ui/section/ProjectListSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,18 @@ export const ProjectListSection = () => {
<ProjectListItem
key={project.projectId}
name={project.title}
onClick={() =>
navigate(DYNAMIC_ROUTE_PATHS.PROJECT_DETAIL(project.projectId))
}
onClick={() => {
if (project.status === "IN_PROGRESS") {
navigate(
DYNAMIC_ROUTE_PATHS.UPLOAD_STEP_RESUME_ENTRY(
project.projectId,
),
);
return;
}

navigate(DYNAMIC_ROUTE_PATHS.PROJECT_DETAIL(project.projectId));
}}
updatedAt={project.createdAt}
status={project.status}
roadmapType={project.roadmapType}
Expand Down
1 change: 1 addition & 0 deletions src/features/upload/components/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./upload-step";
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Box, Flex, Text } from "@chakra-ui/react";

import { ROADMAP_STEP_NAMES } from "@/entities";

type Props = {
current: number;
stepCodes: string[];
};

export const UploadStepIndicator = ({ current, stepCodes }: Props) => {
return (
<Box position="relative" px="88px" w="full">
<Flex
align="center"
justify="space-between"
left="88px"
maxW="672px"
position="absolute"
px={12}
right="88px"
top="22px"
>
{Array.from({ length: stepCodes.length - 1 }, (_, i) => (
<Box key={i} bg="neutral.100" flex={1} h="2px" />
))}
</Flex>

<Flex align="flex-start" justify="space-between" maxW="672px" w="full">
{stepCodes.map((code, i) => {
const stepId = i + 1;
const isActive = stepId === current;
const isDone = stepId < current;

return (
<Flex align="center" direction="column" gap={3} key={code} w={24}>
<Flex
align="center"
boxSize={12}
justify="center"
position="relative"
>
<Box
bg={isActive ? "seed" : "neutral.100"}
borderRadius="full"
bottom={0}
left={0}
opacity={isActive ? 0.3 : 0.6}
position="absolute"
right={0}
top={0}
/>
<Flex
align="center"
bg={isActive || isDone ? "seed" : "neutral.300"}
borderRadius="full"
boxSize={7}
justify="center"
position="relative"
zIndex={1}
>
<Text
color="white"
fontSize="xs"
fontWeight="bold"
lineHeight="16px"
textAlign="center"
>
{stepId}
</Text>
</Flex>
</Flex>

<Text
color={isActive ? "neutral.900" : "neutral.600"}
fontSize="sm"
fontWeight={isActive ? "bold" : "medium"}
lineHeight="20px"
textAlign="center"
wordBreak="keep-all"
>
{ROADMAP_STEP_NAMES[code] ?? code}
</Text>
</Flex>
);
})}
</Flex>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Box, Text, Textarea, VStack } from "@chakra-ui/react";

type Props = {
value: string;
onChange: (value: string) => void;
};

export const UploadStepResultInput = ({ value, onChange }: Props) => {
return (
<VStack align="flex-start" gap={6} w="full">
<Text
color="neutral.900"
fontSize="2xl"
fontWeight="bold"
lineHeight="1.4"
>
작업 결과 입력
</Text>

<Box position="relative" w="full">
<Textarea
_focusVisible={{
outline: "none",
boxShadow: "none",
}}
_placeholder={{ color: "neutral.300" }}
bg="neutral.50"
border="none"
borderRadius="xl"
color="neutral.900"
fontSize="sm"
fontWeight="medium"
minH={60}
onChange={(e) => onChange(e.target.value)}
p={6}
placeholder="이전 단계 프롬프트로 얻은 AI의 답변을 여기에 붙여넣어 주세요. 정보를 입력하면 다음 단계 로드맵이 더욱 정교해집니다."
resize="vertical"
value={value}
/>
<Box
backdropFilter="blur(2px)"
bg="rgba(255,255,255,0.6)"
borderRadius="4px"
bottom="20px"
position="absolute"
px={2}
py={1}
right="20px"
>
<Text color="neutral.600" fontSize="xs" fontWeight="medium">
Ctrl + V
</Text>
</Box>
</Box>
</VStack>
);
};
2 changes: 2 additions & 0 deletions src/features/upload/components/features/upload-step/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./UploadStepIndicator";
export * from "./UploadStepResultInput";
1 change: 1 addition & 0 deletions src/features/upload/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./features";
5 changes: 5 additions & 0 deletions src/features/upload/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./useUploadStepData";
export * from "./useUploadStepNavigation";
export * from "./useUploadStepProject";
export * from "./useUploadStepResumeRedirect";
export * from "./useUploadStepSubmission";
63 changes: 63 additions & 0 deletions src/features/upload/hooks/useUploadStepData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useEffect, useState } from "react";

import { type ProjectStepResponse, startStepAPI } from "@/entities";
import { getApiErrorMessage, toaster } from "@/shared";

type Params = {
projectId: string;
stepCode?: string;
};

type Result = {
stepData: ProjectStepResponse | null;
isStepLoading: boolean;
};

export const useUploadStepData = ({ projectId, stepCode }: Params): Result => {
const [stepData, setStepData] = useState<ProjectStepResponse | null>(null);
const [isStepLoading, setIsStepLoading] = useState(false);

useEffect(() => {
if (!projectId || !stepCode) {
setStepData(null);
return;
}

let cancelled = false;

const fetchStep = async () => {
setIsStepLoading(true);
setStepData(null);

try {
const data = await startStepAPI({ projectId, stepCode });

if (!cancelled) {
setStepData(data);
}
} catch (error) {
if (!cancelled) {
toaster.create({
type: "error",
description: getApiErrorMessage(error),
});
}
} finally {
if (!cancelled) {
setIsStepLoading(false);
}
}
};

void fetchStep();

return () => {
cancelled = true;
};
}, [projectId, stepCode]);

return {
stepData,
isStepLoading,
};
};
31 changes: 31 additions & 0 deletions src/features/upload/hooks/useUploadStepNavigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback } from "react";
import { useNavigate } from "react-router";

import { DYNAMIC_ROUTE_PATHS, ROUTE_PATHS } from "@/shared";

type Params = {
projectId: string;
stepNum: number;
};

type Result = {
goToPrevStep: () => void;
};

export const useUploadStepNavigation = ({
projectId,
stepNum,
}: Params): Result => {
const navigate = useNavigate();

const goToPrevStep = useCallback(() => {
if (stepNum <= 1) {
navigate(ROUTE_PATHS.FILE_UPLOAD);
return;
}

navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, stepNum - 1));
}, [navigate, projectId, stepNum]);

return { goToPrevStep };
};
35 changes: 35 additions & 0 deletions src/features/upload/hooks/useUploadStepProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
type ProjectDetailResponse,
ROADMAP_STEP_CODES,
useGetProjectDetail,
} from "@/entities";

type Params = {
projectId: string;
stepNum: number;
};

type Result = {
project: ProjectDetailResponse | undefined;
steps: string[];
stepCode: string | undefined;
isLastStep: boolean;
};

export const useUploadStepProject = ({
projectId,
stepNum,
}: Params): Result => {
const { data: project } = useGetProjectDetail(projectId);
const roadmapType = project?.roadmapType;
const steps = roadmapType ? ROADMAP_STEP_CODES[roadmapType] : [];
const stepCode = steps[stepNum - 1];
const isLastStep = steps.length > 0 && stepNum >= steps.length;

return {
project,
steps,
stepCode,
isLastStep,
};
};
67 changes: 67 additions & 0 deletions src/features/upload/hooks/useUploadStepResumeRedirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useMemo } from "react";
import { useNavigate } from "react-router";

import { useGetProjectDetail } from "@/entities";
import { DYNAMIC_ROUTE_PATHS } from "@/shared";

type Params = {
projectId: string;
stepNum: number;
enabled: boolean;
};

type Result = {
isResolved: boolean;
};

export const useUploadStepResumeRedirect = ({
projectId,
stepNum,
enabled,
}: Params): Result => {
const navigate = useNavigate();
const shouldResolveResume = enabled && stepNum === 1;
const { data: project, isLoading } = useGetProjectDetail(
enabled ? projectId : "",
);

const targetStep = useMemo(() => {
if (!shouldResolveResume || !project) {
return null;
}

const stepResponses = project.stepResponses ?? [];

if (stepResponses.length === 0) {
return stepNum;
}

const nextStepIndex = stepResponses.findIndex(
(step) => !step.userSubmittedResult,
);

return nextStepIndex === -1 ? stepResponses.length : nextStepIndex + 1;
}, [project, shouldResolveResume, stepNum]);

useEffect(() => {
if (!projectId || targetStep === null || targetStep === stepNum) {
return;
}

navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, targetStep), {
replace: true,
});
}, [navigate, projectId, stepNum, targetStep]);

if (!shouldResolveResume) {
return { isResolved: true };
}

if (isLoading) {
return { isResolved: false };
}

return {
isResolved: !project || targetStep === null || targetStep === stepNum,
};
};
Loading
Loading