From 43e16861f3e60f9b62e76b1b47779be0ef575a77 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 01:15:44 +0900
Subject: [PATCH 01/18] =?UTF-8?q?feat:=20=ED=81=B4=EB=A6=BD=EB=B3=B4?=
=?UTF-8?q?=EB=93=9C=20=EB=B3=B5=EC=82=AC=20=EA=B3=B5=EC=9A=A9=20=ED=9B=85?=
=?UTF-8?q?=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/upload/_step/UploadStepPage.tsx | 246 ++++------------------
src/shared/hooks/index.ts | 1 +
src/shared/hooks/useClipboardCopy.ts | 64 ++++++
src/shared/index.ts | 1 +
4 files changed, 103 insertions(+), 209 deletions(-)
create mode 100644 src/shared/hooks/index.ts
create mode 100644 src/shared/hooks/useClipboardCopy.ts
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 50e3166..896f6b7 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -5,6 +5,7 @@ import { Box, Button, Flex, Text, Textarea, VStack } from "@chakra-ui/react";
import {
type ProjectStepResponse,
+ PromptCard,
ROADMAP_STEP_CODES,
ROADMAP_STEP_NAMES,
completeProjectAPI,
@@ -12,56 +13,13 @@ import {
startStepAPI,
useGetProjectDetail,
} from "@/entities";
-import { ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared";
import {
- ArrowLeftIcon,
- ArrowRightIcon,
- CopyIcon,
- DocumentTextIcon,
-} from "@/shared/_assets/icons";
-
-function PromptLine({ line }: { line: string }) {
- if (line.startsWith("# ") || line === "#") {
- return (
-
- {line}
-
- );
- }
- if (line.startsWith("//")) {
- return (
-
- {line}
-
- );
- }
- return (
-
- {line}
-
- );
-}
+ ROUTE_PATHS,
+ getApiErrorMessage,
+ toaster,
+ useClipboardCopy,
+} from "@/shared";
+import { ArrowLeftIcon, ArrowRightIcon } from "@/shared/_assets/icons";
function StepIndicator({
current,
@@ -160,8 +118,8 @@ function UploadStepContent({
const navigate = useNavigate();
const [resultText, setResultText] = useState("");
- const [copiedPrompt, setCopiedPrompt] = useState(false);
- const [copiedFormat, setCopiedFormat] = useState(false);
+ const { copied: copiedPrompt, copy: copyPrompt } = useClipboardCopy();
+ const { copied: copiedFormat, copy: copyFormat } = useClipboardCopy();
const [stepData, setStepData] = useState(null);
const [isStepLoading, setIsStepLoading] = useState(false);
@@ -195,31 +153,6 @@ function UploadStepContent({
fetchStep();
}, [projectId, stepCode]);
- const copyToClipboard = (text: string, setter: (v: boolean) => void) => {
- if (!navigator?.clipboard?.writeText) {
- toaster.create({
- type: "error",
- description:
- "클립보드 복사에 실패했습니다. 브라우저가 클립보드를 지원하지 않습니다.",
- });
- return;
- }
- navigator.clipboard
- .writeText(text)
- .then(() => {
- setter(true);
- setTimeout(() => setter(false), 2000);
- })
- .catch((error) => {
- console.error("Failed to copy to clipboard", error);
- toaster.create({
- type: "error",
- description:
- "클립보드 복사에 실패했습니다. 브라우저 권한 또는 HTTPS 환경을 확인해주세요.",
- });
- });
- };
-
const goToPrevStep = () => {
if (stepNum <= 1) {
navigate(ROUTE_PATHS.FILE_UPLOAD);
@@ -345,67 +278,16 @@ function UploadStepContent({
-
-
-
-
-
- 생성된 프롬프트
-
-
-
-
- stepData?.providedPromptSnapshot &&
- copyToClipboard(
- stepData.providedPromptSnapshot,
- setCopiedPrompt,
- )
- }
- px="13px"
- py="7px"
- _hover={{ boxShadow: "0px 2px 4px 0px rgba(0,0,0,0.08)" }}
- >
-
-
-
- {copiedPrompt ? "복사됨 ✓" : "복사하기"}
-
-
-
-
-
-
- {isStepLoading ? (
+
프롬프트를 불러오는 중...
- ) : (
- stepData?.providedPromptSnapshot
- ?.split("\n")
- .map((line, i) => )
- )}
+
-
+ ) : stepData?.providedPromptSnapshot ? (
+ {
+ void copyPrompt(stepData.providedPromptSnapshot);
+ }}
+ />
+ ) : null}
{stepData?.formatPrompt && (
@@ -435,73 +322,14 @@ function UploadStepContent({
이 프롬프트를 사용하여 ai와 함께 작업한 결과를 추출해주세요.
-
-
-
-
-
- 작업 결과 추출 프롬프트
-
-
-
-
- copyToClipboard(stepData.formatPrompt, setCopiedFormat)
- }
- px="13px"
- py="7px"
- _hover={{
- boxShadow: "0px 2px 4px 0px rgba(0,0,0,0.08)",
- }}
- >
-
-
-
- {copiedFormat ? "복사됨 ✓" : "복사하기"}
-
-
-
-
-
-
- {stepData.formatPrompt.split("\n").map((line, i) => (
-
- ))}
-
-
+ {
+ void copyFormat(stepData.formatPrompt);
+ }}
+ />
)}
diff --git a/src/shared/hooks/index.ts b/src/shared/hooks/index.ts
new file mode 100644
index 0000000..ea68209
--- /dev/null
+++ b/src/shared/hooks/index.ts
@@ -0,0 +1 @@
+export * from "./useClipboardCopy";
diff --git a/src/shared/hooks/useClipboardCopy.ts b/src/shared/hooks/useClipboardCopy.ts
new file mode 100644
index 0000000..42c6f84
--- /dev/null
+++ b/src/shared/hooks/useClipboardCopy.ts
@@ -0,0 +1,64 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+
+import { toaster } from "../utils";
+
+type UseClipboardCopyOptions = {
+ successDuration?: number;
+ unsupportedMessage?: string;
+ errorMessage?: string;
+};
+
+export const useClipboardCopy = ({
+ successDuration = 2000,
+ unsupportedMessage,
+ errorMessage,
+}: UseClipboardCopyOptions = {}) => {
+ const [copied, setCopied] = useState(false);
+ const resetTimerRef = useRef(null);
+
+ const clearResetTimer = useCallback(() => {
+ if (resetTimerRef.current !== null) {
+ window.clearTimeout(resetTimerRef.current);
+ resetTimerRef.current = null;
+ }
+ }, []);
+
+ useEffect(() => clearResetTimer, [clearResetTimer]);
+
+ const copy = useCallback(
+ async (text: string) => {
+ if (!navigator?.clipboard?.writeText) {
+ toaster.create({
+ type: "error",
+ description:
+ unsupportedMessage ??
+ "클립보드 복사에 실패했습니다. 브라우저가 클립보드를 지원하지 않습니다.",
+ });
+ return false;
+ }
+
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ clearResetTimer();
+ resetTimerRef.current = window.setTimeout(() => {
+ setCopied(false);
+ resetTimerRef.current = null;
+ }, successDuration);
+ return true;
+ } catch (error) {
+ console.error("Failed to copy to clipboard", error);
+ toaster.create({
+ type: "error",
+ description:
+ errorMessage ??
+ "클립보드 복사에 실패했습니다. 브라우저 권한 또는 HTTPS 환경을 확인해주세요.",
+ });
+ return false;
+ }
+ },
+ [clearResetTimer, errorMessage, successDuration, unsupportedMessage],
+ );
+
+ return { copied, copy };
+};
diff --git a/src/shared/index.ts b/src/shared/index.ts
index a6f2226..1762bff 100644
--- a/src/shared/index.ts
+++ b/src/shared/index.ts
@@ -1,6 +1,7 @@
export * from "./_assets";
export * from "./components";
export * from "./constants";
+export * from "./hooks";
export * from "./libs";
export * from "./supabase";
export * from "./theme";
From 7102da917ffe76706a5d47483384d8c4e52b035c Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 01:31:39 +0900
Subject: [PATCH 02/18] =?UTF-8?q?feat:=20=EB=8B=A8=EA=B3=84=20Indicator,?=
=?UTF-8?q?=20=EC=9E=91=EC=97=85=20=EA=B2=B0=EA=B3=BC=20=EC=9E=85=EB=A0=A5?=
=?UTF-8?q?=20Input=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/features/upload/index.ts | 1 +
.../upload/ui/UploadStepIndicator.tsx | 89 +++++++++++
.../upload/ui/UploadStepResultInput.tsx | 57 +++++++
src/features/upload/ui/index.ts | 2 +
src/pages/upload/_step/UploadStepPage.tsx | 143 +-----------------
5 files changed, 156 insertions(+), 136 deletions(-)
create mode 100644 src/features/upload/ui/UploadStepIndicator.tsx
create mode 100644 src/features/upload/ui/UploadStepResultInput.tsx
create mode 100644 src/features/upload/ui/index.ts
diff --git a/src/features/upload/index.ts b/src/features/upload/index.ts
index 178cd64..70c139d 100644
--- a/src/features/upload/index.ts
+++ b/src/features/upload/index.ts
@@ -1 +1,2 @@
+export * from "./ui";
export * from "./utils";
diff --git a/src/features/upload/ui/UploadStepIndicator.tsx b/src/features/upload/ui/UploadStepIndicator.tsx
new file mode 100644
index 0000000..308ec3c
--- /dev/null
+++ b/src/features/upload/ui/UploadStepIndicator.tsx
@@ -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 (
+
+
+ {Array.from({ length: stepCodes.length - 1 }, (_, i) => (
+
+ ))}
+
+
+
+ {stepCodes.map((code, i) => {
+ const stepId = i + 1;
+ const isActive = stepId === current;
+ const isDone = stepId < current;
+
+ return (
+
+
+
+
+
+ {stepId}
+
+
+
+
+
+ {ROADMAP_STEP_NAMES[code] ?? code}
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/src/features/upload/ui/UploadStepResultInput.tsx b/src/features/upload/ui/UploadStepResultInput.tsx
new file mode 100644
index 0000000..0fbbe55
--- /dev/null
+++ b/src/features/upload/ui/UploadStepResultInput.tsx
@@ -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 (
+
+
+ 작업 결과 입력
+
+
+
+
+
+ );
+};
diff --git a/src/features/upload/ui/index.ts b/src/features/upload/ui/index.ts
new file mode 100644
index 0000000..09b073a
--- /dev/null
+++ b/src/features/upload/ui/index.ts
@@ -0,0 +1,2 @@
+export * from "./UploadStepIndicator";
+export * from "./UploadStepResultInput";
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 896f6b7..469eb22 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -1,18 +1,18 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
-import { Box, Button, Flex, Text, Textarea, VStack } from "@chakra-ui/react";
+import { Box, Button, Flex, Text, VStack } from "@chakra-ui/react";
import {
type ProjectStepResponse,
PromptCard,
ROADMAP_STEP_CODES,
- ROADMAP_STEP_NAMES,
completeProjectAPI,
saveStepResultAPI,
startStepAPI,
useGetProjectDetail,
} from "@/entities";
+import { UploadStepIndicator, UploadStepResultInput } from "@/features";
import {
ROUTE_PATHS,
getApiErrorMessage,
@@ -21,93 +21,6 @@ import {
} from "@/shared";
import { ArrowLeftIcon, ArrowRightIcon } from "@/shared/_assets/icons";
-function StepIndicator({
- current,
- stepCodes,
-}: {
- current: number;
- stepCodes: string[];
-}) {
- return (
-
-
- {Array.from({ length: stepCodes.length - 1 }, (_, i) => (
-
- ))}
-
-
-
- {stepCodes.map((code, i) => {
- const stepId = i + 1;
- const isActive = stepId === current;
- const isDone = stepId < current;
-
- return (
-
-
-
-
-
- {stepId}
-
-
-
-
-
- {ROADMAP_STEP_NAMES[code] ?? code}
-
-
- );
- })}
-
-
- );
-}
-
function UploadStepContent({
projectId,
stepNum,
@@ -247,7 +160,7 @@ function UploadStepContent({
{steps.length > 0 && (
-
+
)}
)}
-
-
- 작업 결과 입력
-
-
-
-
-
+
From 373248ceece7dc2eb1c29f4d4d9ff255ada2b35a Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 02:54:03 +0900
Subject: [PATCH 09/18] =?UTF-8?q?refector:=20UploadStepPage=20=EC=82=AC?=
=?UTF-8?q?=EC=9A=A9=20=ED=9B=85=EC=9D=84=20=EA=B0=81=20=EC=84=B9=EC=85=98?=
=?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?=
=?UTF-8?q?=EB=A1=9D=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F?=
=?UTF-8?q?=20Props=20=EC=A0=95=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../upload/hooks/useUploadStepActions.ts | 7 ++-
.../ui/section/UploadStepContentSection.tsx | 48 ++++++++++++-------
.../ui/section/UploadStepHeaderSection.tsx | 15 +++---
src/pages/upload/_step/UploadStepPage.tsx | 41 ++--------------
4 files changed, 45 insertions(+), 66 deletions(-)
diff --git a/src/features/upload/hooks/useUploadStepActions.ts b/src/features/upload/hooks/useUploadStepActions.ts
index 1bd8b18..0b400dc 100644
--- a/src/features/upload/hooks/useUploadStepActions.ts
+++ b/src/features/upload/hooks/useUploadStepActions.ts
@@ -4,11 +4,11 @@ import { useNavigate } from "react-router";
import { completeProjectAPI, saveStepResultAPI } from "@/entities";
import { ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared";
+import { useUploadStepProject } from "./useUploadStepProject";
+
type Params = {
projectId: string;
stepNum: number;
- stepCode?: string;
- isLastStep: boolean;
};
type Result = {
@@ -20,12 +20,11 @@ type Result = {
export const useUploadStepActions = ({
projectId,
stepNum,
- stepCode,
- isLastStep,
}: Params): Result => {
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
const [isCompleting, setIsCompleting] = useState(false);
+ const { stepCode, isLastStep } = useUploadStepProject({ projectId, stepNum });
const goToPrevStep = useCallback(() => {
if (stepNum <= 1) {
diff --git a/src/features/upload/ui/section/UploadStepContentSection.tsx b/src/features/upload/ui/section/UploadStepContentSection.tsx
index 19b3807..37e46f1 100644
--- a/src/features/upload/ui/section/UploadStepContentSection.tsx
+++ b/src/features/upload/ui/section/UploadStepContentSection.tsx
@@ -1,38 +1,52 @@
+import { useState } from "react";
+
import { Box, Button, Flex, Text, VStack } from "@chakra-ui/react";
import { PromptCard } from "@/entities";
import { useClipboardCopy } from "@/shared";
import { ArrowRightIcon } from "@/shared/_assets/icons";
+import { useUploadStepData, useUploadStepProject } from "../../hooks";
import { UploadStepResultInput } from "../UploadStepResultInput";
type Props = {
+ projectId: string;
stepNum: number;
- stepName?: string;
- providedPromptSnapshot?: string;
- formatPrompt?: string;
- resultText: string;
- isStepLoading: boolean;
isSubmitting: boolean;
- isLastStep: boolean;
- onResultTextChange: (value: string) => void;
- onSubmit: () => void;
+ onSubmit: (resultText: string) => void;
};
export const UploadStepContentSection = ({
+ projectId,
stepNum,
- stepName,
- providedPromptSnapshot,
- formatPrompt,
- resultText,
- isStepLoading,
isSubmitting,
- isLastStep,
- onResultTextChange,
onSubmit,
}: Props) => {
+ const [resultTextByStep, setResultTextByStep] = useState<
+ Record
+ >({});
const { copied: copiedPrompt, copy: copyPrompt } = useClipboardCopy();
const { copied: copiedFormat, copy: copyFormat } = useClipboardCopy();
+ const { stepCode, isLastStep } = useUploadStepProject({ projectId, stepNum });
+ const { stepData, isStepLoading } = useUploadStepData({
+ projectId,
+ stepCode,
+ });
+ const resultTextKey = `${projectId}:${stepNum}`;
+ const savedResultText =
+ stepData?.stepCode === stepCode
+ ? (stepData?.userSubmittedResult ?? "")
+ : "";
+ const resultText = resultTextByStep[resultTextKey] ?? savedResultText;
+ const handleResultTextChange = (value: string) => {
+ setResultTextByStep((prev) => ({
+ ...prev,
+ [resultTextKey]: value,
+ }));
+ };
+ const stepName = stepData?.stepName;
+ const providedPromptSnapshot = stepData?.providedPromptSnapshot;
+ const formatPrompt = stepData?.formatPrompt;
return (
@@ -131,7 +145,7 @@ export const UploadStepContentSection = ({
disabled={!resultText.trim() || isSubmitting}
fontWeight="bold"
gap={1}
- onClick={onSubmit}
+ onClick={() => onSubmit(resultText)}
opacity={resultText.trim() && !isSubmitting ? 1 : 0.5}
px={10}
py={4}
diff --git a/src/features/upload/ui/section/UploadStepHeaderSection.tsx b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
index 3f24627..2cf158a 100644
--- a/src/features/upload/ui/section/UploadStepHeaderSection.tsx
+++ b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
@@ -2,23 +2,22 @@ import { Box, Button, Text, VStack } from "@chakra-ui/react";
import { ArrowLeftIcon } from "@/shared/_assets/icons";
+import { useUploadStepProject } from "../../hooks";
import { UploadStepIndicator } from "../UploadStepIndicator";
type Props = {
- projectTitle?: string;
- roadmapType?: string;
+ projectId: string;
stepNum: number;
- steps: string[];
onGoBack: () => void;
};
export const UploadStepHeaderSection = ({
- projectTitle,
- roadmapType,
+ projectId,
stepNum,
- steps,
onGoBack,
}: Props) => {
+ const { project, steps } = useUploadStepProject({ projectId, stepNum });
+
return (
<>
@@ -48,7 +47,7 @@ export const UploadStepHeaderSection = ({
px="9px"
py="5px"
>
- {roadmapType}
+ {project?.roadmapType}
- {projectTitle}
+ {project?.title}
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 66f1750..a547dab 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from "react";
+import { useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import { Flex } from "@chakra-ui/react";
@@ -7,8 +7,6 @@ import {
UploadStepContentSection,
UploadStepHeaderSection,
useUploadStepActions,
- useUploadStepData,
- useUploadStepProject,
} from "@/features";
import { ROUTE_PATHS } from "@/shared";
@@ -22,35 +20,12 @@ export default function UploadStepPage() {
const isInvalidRoute = !projectId || isNaN(stepNum) || stepNum < 1;
const resolvedProjectId = projectId ?? "";
const resolvedStepNum = isNaN(stepNum) ? 1 : stepNum;
- const [resultTextByStep, setResultTextByStep] = useState<
- Record
- >({});
- const { project, steps, stepCode, isLastStep } = useUploadStepProject({
- projectId: resolvedProjectId,
- stepNum: resolvedStepNum,
- });
- const { stepData, isStepLoading } = useUploadStepData({
- projectId: resolvedProjectId,
- stepCode,
- });
const { isSubmitting, goToPrevStep, submitStepResult } = useUploadStepActions(
{
projectId: resolvedProjectId,
stepNum: resolvedStepNum,
- stepCode,
- isLastStep,
},
);
- const resultTextKey = `${resolvedProjectId}:${resolvedStepNum}`;
- const savedResultText =
- stepData?.stepCode === stepCode ? (stepData.userSubmittedResult ?? "") : "";
- const resultText = resultTextByStep[resultTextKey] ?? savedResultText;
- const handleResultTextChange = (value: string) => {
- setResultTextByStep((prev) => ({
- ...prev,
- [resultTextKey]: value,
- }));
- };
useEffect(() => {
if (isInvalidRoute) {
@@ -67,24 +42,16 @@ export default function UploadStepPage() {
{
+ onSubmit={(resultText) => {
void submitStepResult(resultText);
}}
- providedPromptSnapshot={stepData?.providedPromptSnapshot}
- resultText={resultText}
- stepName={stepData?.stepName}
stepNum={resolvedStepNum}
/>
From 7a924754db2a05e2a9eb509c9f71a0e207fac477 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 03:06:39 +0900
Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=EB=92=A4=EB=A1=9C=EA=B0=80?=
=?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EA=B3=B5=EC=9A=A9=20=EC=BB=B4?=
=?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ui/section/UploadStepHeaderSection.tsx | 19 +++----------
src/shared/components/common/BackButton.tsx | 27 +++++++++++++++++++
src/shared/components/common/index.ts | 1 +
3 files changed, 31 insertions(+), 16 deletions(-)
create mode 100644 src/shared/components/common/BackButton.tsx
diff --git a/src/features/upload/ui/section/UploadStepHeaderSection.tsx b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
index 2cf158a..51229db 100644
--- a/src/features/upload/ui/section/UploadStepHeaderSection.tsx
+++ b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
@@ -1,6 +1,6 @@
-import { Box, Button, Text, VStack } from "@chakra-ui/react";
+import { Box, Text, VStack } from "@chakra-ui/react";
-import { ArrowLeftIcon } from "@/shared/_assets/icons";
+import { BackButton } from "@/shared";
import { useUploadStepProject } from "../../hooks";
import { UploadStepIndicator } from "../UploadStepIndicator";
@@ -21,20 +21,7 @@ export const UploadStepHeaderSection = ({
return (
<>
-
-
- 이전 단계로
-
+
void;
+};
+
+export const BackButton = ({ label, onClick }: Props) => {
+ return (
+
+
+ {label}
+
+ );
+};
diff --git a/src/shared/components/common/index.ts b/src/shared/components/common/index.ts
index 2f25d1d..e719178 100644
--- a/src/shared/components/common/index.ts
+++ b/src/shared/components/common/index.ts
@@ -1,2 +1,3 @@
+export * from "./BackButton";
export * from "./ConfirmDialog";
export * from "./Pagination";
From d5df9db8c0a88d61d044dafe9742155f354fee54 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 03:10:30 +0900
Subject: [PATCH 11/18] =?UTF-8?q?refector:=20useUploadStepActions=20?=
=?UTF-8?q?=EC=97=AD=ED=95=A0=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/features/upload/hooks/index.ts | 3 +-
.../upload/hooks/useUploadStepNavigation.ts | 31 +++++++++++++++++++
...pActions.ts => useUploadStepSubmission.ts} | 20 +++---------
.../ui/section/UploadStepContentSection.tsx | 25 +++++++++------
.../ui/section/UploadStepHeaderSection.tsx | 12 +++----
src/pages/upload/_step/UploadStepPage.tsx | 17 +---------
6 files changed, 58 insertions(+), 50 deletions(-)
create mode 100644 src/features/upload/hooks/useUploadStepNavigation.ts
rename src/features/upload/hooks/{useUploadStepActions.ts => useUploadStepSubmission.ts} (80%)
diff --git a/src/features/upload/hooks/index.ts b/src/features/upload/hooks/index.ts
index 9717bd7..6052f68 100644
--- a/src/features/upload/hooks/index.ts
+++ b/src/features/upload/hooks/index.ts
@@ -1,3 +1,4 @@
-export * from "./useUploadStepActions";
export * from "./useUploadStepData";
+export * from "./useUploadStepNavigation";
export * from "./useUploadStepProject";
+export * from "./useUploadStepSubmission";
diff --git a/src/features/upload/hooks/useUploadStepNavigation.ts b/src/features/upload/hooks/useUploadStepNavigation.ts
new file mode 100644
index 0000000..152b585
--- /dev/null
+++ b/src/features/upload/hooks/useUploadStepNavigation.ts
@@ -0,0 +1,31 @@
+import { useCallback } from "react";
+import { useNavigate } from "react-router";
+
+import { 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(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/${stepNum - 1}`);
+ }, [navigate, projectId, stepNum]);
+
+ return { goToPrevStep };
+};
diff --git a/src/features/upload/hooks/useUploadStepActions.ts b/src/features/upload/hooks/useUploadStepSubmission.ts
similarity index 80%
rename from src/features/upload/hooks/useUploadStepActions.ts
rename to src/features/upload/hooks/useUploadStepSubmission.ts
index 0b400dc..02c0ea0 100644
--- a/src/features/upload/hooks/useUploadStepActions.ts
+++ b/src/features/upload/hooks/useUploadStepSubmission.ts
@@ -4,36 +4,27 @@ import { useNavigate } from "react-router";
import { completeProjectAPI, saveStepResultAPI } from "@/entities";
import { ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared";
-import { useUploadStepProject } from "./useUploadStepProject";
-
type Params = {
projectId: string;
stepNum: number;
+ stepCode?: string;
+ isLastStep: boolean;
};
type Result = {
isSubmitting: boolean;
- goToPrevStep: () => void;
submitStepResult: (resultText: string) => Promise;
};
-export const useUploadStepActions = ({
+export const useUploadStepSubmission = ({
projectId,
stepNum,
+ stepCode,
+ isLastStep,
}: Params): Result => {
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
const [isCompleting, setIsCompleting] = useState(false);
- const { stepCode, isLastStep } = useUploadStepProject({ projectId, stepNum });
-
- const goToPrevStep = useCallback(() => {
- if (stepNum <= 1) {
- navigate(ROUTE_PATHS.FILE_UPLOAD);
- return;
- }
-
- navigate(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/${stepNum - 1}`);
- }, [navigate, projectId, stepNum]);
const submitStepResult = useCallback(
async (resultText: string) => {
@@ -95,7 +86,6 @@ export const useUploadStepActions = ({
return {
isSubmitting: isSaving || isCompleting,
- goToPrevStep,
submitStepResult,
};
};
diff --git a/src/features/upload/ui/section/UploadStepContentSection.tsx b/src/features/upload/ui/section/UploadStepContentSection.tsx
index 37e46f1..110129d 100644
--- a/src/features/upload/ui/section/UploadStepContentSection.tsx
+++ b/src/features/upload/ui/section/UploadStepContentSection.tsx
@@ -6,22 +6,19 @@ import { PromptCard } from "@/entities";
import { useClipboardCopy } from "@/shared";
import { ArrowRightIcon } from "@/shared/_assets/icons";
-import { useUploadStepData, useUploadStepProject } from "../../hooks";
+import {
+ useUploadStepData,
+ useUploadStepProject,
+ useUploadStepSubmission,
+} from "../../hooks";
import { UploadStepResultInput } from "../UploadStepResultInput";
type Props = {
projectId: string;
stepNum: number;
- isSubmitting: boolean;
- onSubmit: (resultText: string) => void;
};
-export const UploadStepContentSection = ({
- projectId,
- stepNum,
- isSubmitting,
- onSubmit,
-}: Props) => {
+export const UploadStepContentSection = ({ projectId, stepNum }: Props) => {
const [resultTextByStep, setResultTextByStep] = useState<
Record
>({});
@@ -32,6 +29,12 @@ export const UploadStepContentSection = ({
projectId,
stepCode,
});
+ const { isSubmitting, submitStepResult } = useUploadStepSubmission({
+ projectId,
+ stepNum,
+ stepCode,
+ isLastStep,
+ });
const resultTextKey = `${projectId}:${stepNum}`;
const savedResultText =
stepData?.stepCode === stepCode
@@ -145,7 +148,9 @@ export const UploadStepContentSection = ({
disabled={!resultText.trim() || isSubmitting}
fontWeight="bold"
gap={1}
- onClick={() => onSubmit(resultText)}
+ onClick={() => {
+ void submitStepResult(resultText);
+ }}
opacity={resultText.trim() && !isSubmitting ? 1 : 0.5}
px={10}
py={4}
diff --git a/src/features/upload/ui/section/UploadStepHeaderSection.tsx b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
index 51229db..5f79eef 100644
--- a/src/features/upload/ui/section/UploadStepHeaderSection.tsx
+++ b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
@@ -2,26 +2,22 @@ import { Box, Text, VStack } from "@chakra-ui/react";
import { BackButton } from "@/shared";
-import { useUploadStepProject } from "../../hooks";
+import { useUploadStepNavigation, useUploadStepProject } from "../../hooks";
import { UploadStepIndicator } from "../UploadStepIndicator";
type Props = {
projectId: string;
stepNum: number;
- onGoBack: () => void;
};
-export const UploadStepHeaderSection = ({
- projectId,
- stepNum,
- onGoBack,
-}: Props) => {
+export const UploadStepHeaderSection = ({ projectId, stepNum }: Props) => {
const { project, steps } = useUploadStepProject({ projectId, stepNum });
+ const { goToPrevStep } = useUploadStepNavigation({ projectId, stepNum });
return (
<>
-
+
{
if (isInvalidRoute) {
@@ -41,17 +31,12 @@ export default function UploadStepPage() {
{
- void submitStepResult(resultText);
- }}
stepNum={resolvedStepNum}
/>
From d66f02bfc3f0151b746c1805d71c651d0e3b3884 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 03:22:54 +0900
Subject: [PATCH 12/18] =?UTF-8?q?refactor:=20=EC=97=85=EB=A1=9C=EB=93=9C?=
=?UTF-8?q?=20step=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=98=EB=AA=BB?=
=?UTF-8?q?=EB=90=9C=20=EA=B2=BD=EB=A1=9C=20=EC=B2=98=EB=A6=AC=20=EB=B0=A9?=
=?UTF-8?q?=EC=8B=9D=20=EA=B0=9C=EC=84=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/upload/_step/UploadStepPage.tsx | 26 +++++------------------
1 file changed, 5 insertions(+), 21 deletions(-)
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 059c3b0..248c256 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -1,5 +1,4 @@
-import { useEffect } from "react";
-import { useNavigate, useParams } from "react-router";
+import { Navigate, useParams } from "react-router";
import { Flex } from "@chakra-ui/react";
@@ -7,38 +6,23 @@ import { UploadStepContentSection, UploadStepHeaderSection } from "@/features";
import { ROUTE_PATHS } from "@/shared";
export default function UploadStepPage() {
- const navigate = useNavigate();
const { projectId, step } = useParams<{
projectId: string;
step: string;
}>();
- const stepNum = parseInt(step ?? "1", 10);
+ const stepNum = Number(step);
const isInvalidRoute = !projectId || isNaN(stepNum) || stepNum < 1;
- const resolvedProjectId = projectId ?? "";
- const resolvedStepNum = isNaN(stepNum) ? 1 : stepNum;
-
- useEffect(() => {
- if (isInvalidRoute) {
- navigate(ROUTE_PATHS.FILE_UPLOAD, { replace: true });
- }
- }, [isInvalidRoute, navigate]);
if (isInvalidRoute) {
- return null;
+ return ;
}
return (
-
+
-
+
);
From 6910cd7dbf440dac69bf83b099e4ffb27a18ae14 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 03:29:11 +0900
Subject: [PATCH 13/18] =?UTF-8?q?refector:=20ROADMAP=5FTYPE=5FLABEL=20?=
=?UTF-8?q?=EC=82=AC=EC=9A=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/features/upload/ui/section/UploadStepHeaderSection.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/features/upload/ui/section/UploadStepHeaderSection.tsx b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
index 5f79eef..f83af40 100644
--- a/src/features/upload/ui/section/UploadStepHeaderSection.tsx
+++ b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
@@ -1,5 +1,6 @@
import { Box, Text, VStack } from "@chakra-ui/react";
+import { ROADMAP_TYPE_LABEL } from "@/entities";
import { BackButton } from "@/shared";
import { useUploadStepNavigation, useUploadStepProject } from "../../hooks";
@@ -30,7 +31,9 @@ export const UploadStepHeaderSection = ({ projectId, stepNum }: Props) => {
px="9px"
py="5px"
>
- {project?.roadmapType}
+ {project?.roadmapType
+ ? (ROADMAP_TYPE_LABEL[project.roadmapType] ?? project.roadmapType)
+ : ""}
Date: Thu, 26 Mar 2026 03:40:57 +0900
Subject: [PATCH 14/18] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?=
=?UTF-8?q?=ED=95=9C=20=EC=86=8D=EC=84=B1=20=EC=A0=9C=EA=B1=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/features/upload/ui/UploadStepResultInput.tsx | 2 +-
.../upload/ui/section/UploadStepContentSection.tsx | 8 +-------
2 files changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/features/upload/ui/UploadStepResultInput.tsx b/src/features/upload/ui/UploadStepResultInput.tsx
index 0fbbe55..fa20850 100644
--- a/src/features/upload/ui/UploadStepResultInput.tsx
+++ b/src/features/upload/ui/UploadStepResultInput.tsx
@@ -7,7 +7,7 @@ type Props = {
export const UploadStepResultInput = ({ value, onChange }: Props) => {
return (
-
+
{
borderRadius="4xl"
boxShadow="0px 20px 60px -10px rgba(0,0,0,0.08)"
overflow="hidden"
- pb="1px"
>
@@ -68,12 +67,7 @@ export const UploadStepContentSection = ({ projectId, stepNum }: Props) => {
{stepName}
-
+
AI가 과제 주제를 분석하여 최적의 자료 조사를 위한 프롬프트를
생성했습니다.
이 프롬프트를 사용하여 고품질의 레퍼런스를 확보하세요.
From dcee9193ae3430a2dcb6ae66ce17cbb6de4cb87f Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 04:17:21 +0900
Subject: [PATCH 15/18] =?UTF-8?q?refector:=20=EC=A7=84=ED=96=89=20?=
=?UTF-8?q?=EC=A4=91=EC=9D=B8=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?=
=?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20UploadStepPage=EB=A1=9C=20?=
=?UTF-8?q?=EC=9D=B4=EB=8F=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../mypage/ui/section/ProjectListSection.tsx | 13 +++-
src/features/upload/hooks/index.ts | 1 +
.../upload/hooks/useUploadStepResumeGuard.ts | 63 +++++++++++++++++++
src/pages/upload/_step/UploadStepPage.tsx | 18 +++++-
4 files changed, 90 insertions(+), 5 deletions(-)
create mode 100644 src/features/upload/hooks/useUploadStepResumeGuard.ts
diff --git a/src/features/mypage/ui/section/ProjectListSection.tsx b/src/features/mypage/ui/section/ProjectListSection.tsx
index 60d1e5a..d18ecb7 100644
--- a/src/features/mypage/ui/section/ProjectListSection.tsx
+++ b/src/features/mypage/ui/section/ProjectListSection.tsx
@@ -70,9 +70,16 @@ export const ProjectListSection = () => {
- navigate(DYNAMIC_ROUTE_PATHS.PROJECT_DETAIL(project.projectId))
- }
+ onClick={() => {
+ if (project.status === "IN_PROGRESS") {
+ navigate(
+ `${ROUTE_PATHS.UPLOAD_STEP_BASE}/${project.projectId}/1?resume=true`,
+ );
+ return;
+ }
+
+ navigate(DYNAMIC_ROUTE_PATHS.PROJECT_DETAIL(project.projectId));
+ }}
updatedAt={project.createdAt}
status={project.status}
roadmapType={project.roadmapType}
diff --git a/src/features/upload/hooks/index.ts b/src/features/upload/hooks/index.ts
index 6052f68..cd2d143 100644
--- a/src/features/upload/hooks/index.ts
+++ b/src/features/upload/hooks/index.ts
@@ -1,4 +1,5 @@
export * from "./useUploadStepData";
export * from "./useUploadStepNavigation";
export * from "./useUploadStepProject";
+export * from "./useUploadStepResumeGuard";
export * from "./useUploadStepSubmission";
diff --git a/src/features/upload/hooks/useUploadStepResumeGuard.ts b/src/features/upload/hooks/useUploadStepResumeGuard.ts
new file mode 100644
index 0000000..c60adb8
--- /dev/null
+++ b/src/features/upload/hooks/useUploadStepResumeGuard.ts
@@ -0,0 +1,63 @@
+import { useEffect, useMemo } from "react";
+import { useNavigate } from "react-router";
+
+import { useGetProjectDetail } from "@/entities";
+import { ROUTE_PATHS } from "@/shared";
+
+type Params = {
+ projectId?: string;
+ stepNum: number;
+};
+
+type Result = {
+ isResolved: boolean;
+};
+
+export const useUploadStepResumeGuard = ({
+ projectId,
+ stepNum,
+}: Params): Result => {
+ const navigate = useNavigate();
+ const shouldResolveResume = Boolean(projectId) && stepNum === 1;
+ const { data: project, isLoading } = useGetProjectDetail(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(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${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,
+ };
+};
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 248c256..626782c 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -1,22 +1,36 @@
-import { Navigate, useParams } from "react-router";
+import { Navigate, useParams, useSearchParams } from "react-router";
import { Flex } from "@chakra-ui/react";
-import { UploadStepContentSection, UploadStepHeaderSection } from "@/features";
+import {
+ UploadStepContentSection,
+ UploadStepHeaderSection,
+ useUploadStepResumeGuard,
+} from "@/features";
import { ROUTE_PATHS } from "@/shared";
export default function UploadStepPage() {
+ const [searchParams] = useSearchParams();
const { projectId, step } = useParams<{
projectId: string;
step: string;
}>();
const stepNum = Number(step);
const isInvalidRoute = !projectId || isNaN(stepNum) || stepNum < 1;
+ const shouldResume = searchParams.get("resume") === "true";
+ const { isResolved } = useUploadStepResumeGuard({
+ projectId: !isInvalidRoute && shouldResume ? projectId : undefined,
+ stepNum,
+ });
if (isInvalidRoute) {
return ;
}
+ if (shouldResume && !isResolved) {
+ return null;
+ }
+
return (
From 39f598e649593ee4d9997c164ea66784fb3bea25 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 04:50:20 +0900
Subject: [PATCH 16/18] =?UTF-8?q?refactor:=20=EC=97=85=EB=A1=9C=EB=93=9C?=
=?UTF-8?q?=20step=20=EA=B2=BD=EB=A1=9C=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?=
=?UTF-8?q?=20=EC=A7=84=ED=96=89=20=EC=A4=91=20=ED=94=84=EB=A1=9C=EC=A0=9D?=
=?UTF-8?q?=ED=8A=B8=20=EC=A7=84=EC=9E=85=20=ED=9D=90=EB=A6=84=20=EC=A0=95?=
=?UTF-8?q?=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../mypage/ui/section/ProjectListSection.tsx | 4 +++-
src/features/upload/hooks/index.ts | 2 +-
.../upload/hooks/useUploadStepNavigation.ts | 4 ++--
...meGuard.ts => useUploadStepResumeRedirect.ts} | 16 ++++++++++------
.../upload/hooks/useUploadStepSubmission.ts | 8 +++-----
src/pages/upload/_loading/UploadLoadingPage.tsx | 4 ++--
src/pages/upload/_step/UploadStepPage.tsx | 11 ++++++-----
src/shared/constants/route-path.ts | 9 +++++++++
8 files changed, 36 insertions(+), 22 deletions(-)
rename src/features/upload/hooks/{useUploadStepResumeGuard.ts => useUploadStepResumeRedirect.ts} (76%)
diff --git a/src/features/mypage/ui/section/ProjectListSection.tsx b/src/features/mypage/ui/section/ProjectListSection.tsx
index d18ecb7..1760d98 100644
--- a/src/features/mypage/ui/section/ProjectListSection.tsx
+++ b/src/features/mypage/ui/section/ProjectListSection.tsx
@@ -73,7 +73,9 @@ export const ProjectListSection = () => {
onClick={() => {
if (project.status === "IN_PROGRESS") {
navigate(
- `${ROUTE_PATHS.UPLOAD_STEP_BASE}/${project.projectId}/1?resume=true`,
+ DYNAMIC_ROUTE_PATHS.UPLOAD_STEP_RESUME_ENTRY(
+ project.projectId,
+ ),
);
return;
}
diff --git a/src/features/upload/hooks/index.ts b/src/features/upload/hooks/index.ts
index cd2d143..c8ab502 100644
--- a/src/features/upload/hooks/index.ts
+++ b/src/features/upload/hooks/index.ts
@@ -1,5 +1,5 @@
export * from "./useUploadStepData";
export * from "./useUploadStepNavigation";
export * from "./useUploadStepProject";
-export * from "./useUploadStepResumeGuard";
+export * from "./useUploadStepResumeRedirect";
export * from "./useUploadStepSubmission";
diff --git a/src/features/upload/hooks/useUploadStepNavigation.ts b/src/features/upload/hooks/useUploadStepNavigation.ts
index 152b585..ae692ce 100644
--- a/src/features/upload/hooks/useUploadStepNavigation.ts
+++ b/src/features/upload/hooks/useUploadStepNavigation.ts
@@ -1,7 +1,7 @@
import { useCallback } from "react";
import { useNavigate } from "react-router";
-import { ROUTE_PATHS } from "@/shared";
+import { DYNAMIC_ROUTE_PATHS, ROUTE_PATHS } from "@/shared";
type Params = {
projectId: string;
@@ -24,7 +24,7 @@ export const useUploadStepNavigation = ({
return;
}
- navigate(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/${stepNum - 1}`);
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, stepNum - 1));
}, [navigate, projectId, stepNum]);
return { goToPrevStep };
diff --git a/src/features/upload/hooks/useUploadStepResumeGuard.ts b/src/features/upload/hooks/useUploadStepResumeRedirect.ts
similarity index 76%
rename from src/features/upload/hooks/useUploadStepResumeGuard.ts
rename to src/features/upload/hooks/useUploadStepResumeRedirect.ts
index c60adb8..4accbbf 100644
--- a/src/features/upload/hooks/useUploadStepResumeGuard.ts
+++ b/src/features/upload/hooks/useUploadStepResumeRedirect.ts
@@ -2,24 +2,28 @@ import { useEffect, useMemo } from "react";
import { useNavigate } from "react-router";
import { useGetProjectDetail } from "@/entities";
-import { ROUTE_PATHS } from "@/shared";
+import { DYNAMIC_ROUTE_PATHS } from "@/shared";
type Params = {
- projectId?: string;
+ projectId: string;
stepNum: number;
+ enabled: boolean;
};
type Result = {
isResolved: boolean;
};
-export const useUploadStepResumeGuard = ({
+export const useUploadStepResumeRedirect = ({
projectId,
stepNum,
+ enabled,
}: Params): Result => {
const navigate = useNavigate();
- const shouldResolveResume = Boolean(projectId) && stepNum === 1;
- const { data: project, isLoading } = useGetProjectDetail(projectId ?? "");
+ const shouldResolveResume = enabled && stepNum === 1;
+ const { data: project, isLoading } = useGetProjectDetail(
+ enabled ? projectId : "",
+ );
const targetStep = useMemo(() => {
if (!shouldResolveResume || !project) {
@@ -44,7 +48,7 @@ export const useUploadStepResumeGuard = ({
return;
}
- navigate(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/${targetStep}`, {
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, targetStep), {
replace: true,
});
}, [navigate, projectId, stepNum, targetStep]);
diff --git a/src/features/upload/hooks/useUploadStepSubmission.ts b/src/features/upload/hooks/useUploadStepSubmission.ts
index 02c0ea0..4e6facc 100644
--- a/src/features/upload/hooks/useUploadStepSubmission.ts
+++ b/src/features/upload/hooks/useUploadStepSubmission.ts
@@ -2,7 +2,7 @@ import { useCallback, useState } from "react";
import { useNavigate } from "react-router";
import { completeProjectAPI, saveStepResultAPI } from "@/entities";
-import { ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared";
+import { DYNAMIC_ROUTE_PATHS, getApiErrorMessage, toaster } from "@/shared";
type Params = {
projectId: string;
@@ -48,9 +48,7 @@ export const useUploadStepSubmission = ({
try {
await completeProjectAPI(projectId);
- navigate(
- ROUTE_PATHS.UPLOAD_COMPLETE.replace(":projectId", projectId),
- );
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_COMPLETE(projectId));
} catch (error) {
toaster.create({
type: "error",
@@ -63,7 +61,7 @@ export const useUploadStepSubmission = ({
return;
}
- navigate(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/${stepNum + 1}`);
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, stepNum + 1));
} catch (error) {
toaster.create({
type: "error",
diff --git a/src/pages/upload/_loading/UploadLoadingPage.tsx b/src/pages/upload/_loading/UploadLoadingPage.tsx
index 1d2172f..c023f50 100644
--- a/src/pages/upload/_loading/UploadLoadingPage.tsx
+++ b/src/pages/upload/_loading/UploadLoadingPage.tsx
@@ -5,7 +5,7 @@ import { Box, Flex, Image, Text, VStack } from "@chakra-ui/react";
import { useUploadFlowStore } from "@/entities";
import { SproutAnimation } from "@/features";
-import { ROUTE_PATHS } from "@/shared";
+import { DYNAMIC_ROUTE_PATHS, ROUTE_PATHS } from "@/shared";
import AbstractBackgroundCircle from "@/shared/_assets/images/abstract-background-circle.svg";
const LOADING_STEPS = [
@@ -48,7 +48,7 @@ export default function UploadLoadingPage() {
useEffect(() => {
if (projectId) {
const timer = setTimeout(() => {
- navigate(`${ROUTE_PATHS.UPLOAD_STEP_BASE}/${projectId}/1`);
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, 1));
}, 600);
return () => clearTimeout(timer);
}
diff --git a/src/pages/upload/_step/UploadStepPage.tsx b/src/pages/upload/_step/UploadStepPage.tsx
index 626782c..8fb5756 100644
--- a/src/pages/upload/_step/UploadStepPage.tsx
+++ b/src/pages/upload/_step/UploadStepPage.tsx
@@ -5,7 +5,7 @@ import { Flex } from "@chakra-ui/react";
import {
UploadStepContentSection,
UploadStepHeaderSection,
- useUploadStepResumeGuard,
+ useUploadStepResumeRedirect,
} from "@/features";
import { ROUTE_PATHS } from "@/shared";
@@ -16,14 +16,15 @@ export default function UploadStepPage() {
step: string;
}>();
const stepNum = Number(step);
- const isInvalidRoute = !projectId || isNaN(stepNum) || stepNum < 1;
+ const isValidStepNum = Number.isInteger(stepNum) && stepNum > 0;
const shouldResume = searchParams.get("resume") === "true";
- const { isResolved } = useUploadStepResumeGuard({
- projectId: !isInvalidRoute && shouldResume ? projectId : undefined,
+ const { isResolved } = useUploadStepResumeRedirect({
+ projectId: projectId ?? "",
stepNum,
+ enabled: Boolean(projectId) && isValidStepNum && shouldResume,
});
- if (isInvalidRoute) {
+ if (!projectId || !isValidStepNum) {
return ;
}
diff --git a/src/shared/constants/route-path.ts b/src/shared/constants/route-path.ts
index 9d6a122..dcef152 100644
--- a/src/shared/constants/route-path.ts
+++ b/src/shared/constants/route-path.ts
@@ -20,4 +20,13 @@ export const ROUTE_PATHS = {
export const DYNAMIC_ROUTE_PATHS = {
PROJECT_DETAIL: (projectId: string) =>
ROUTE_PATHS.PROJECT_DETAIL.replace(":projectId", projectId),
+ UPLOAD_STEP: (projectId: string, step: number | string) =>
+ ROUTE_PATHS.UPLOAD_STEP.replace(":projectId", projectId).replace(
+ ":step",
+ String(step),
+ ),
+ UPLOAD_STEP_RESUME_ENTRY: (projectId: string) =>
+ `${DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, 1)}?resume=true`,
+ UPLOAD_COMPLETE: (projectId: string) =>
+ ROUTE_PATHS.UPLOAD_COMPLETE.replace(":projectId", projectId),
};
From 7d885b753b639b550aeae43c58ad4e9e15266053 Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 04:56:52 +0900
Subject: [PATCH 17/18] =?UTF-8?q?chore:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?=
=?UTF-8?q?=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/features/upload/components/features/index.ts | 1 +
.../features/upload-step}/UploadStepIndicator.tsx | 0
.../features/upload-step}/UploadStepResultInput.tsx | 4 ++--
src/features/upload/components/features/upload-step/index.ts | 2 ++
src/features/upload/components/index.ts | 1 +
src/features/upload/index.ts | 1 +
src/features/upload/ui/index.ts | 2 --
src/features/upload/ui/section/UploadStepContentSection.tsx | 2 +-
src/features/upload/ui/section/UploadStepHeaderSection.tsx | 2 +-
9 files changed, 9 insertions(+), 6 deletions(-)
create mode 100644 src/features/upload/components/features/index.ts
rename src/features/upload/{ui => components/features/upload-step}/UploadStepIndicator.tsx (100%)
rename src/features/upload/{ui => components/features/upload-step}/UploadStepResultInput.tsx (84%)
create mode 100644 src/features/upload/components/features/upload-step/index.ts
create mode 100644 src/features/upload/components/index.ts
diff --git a/src/features/upload/components/features/index.ts b/src/features/upload/components/features/index.ts
new file mode 100644
index 0000000..56f8211
--- /dev/null
+++ b/src/features/upload/components/features/index.ts
@@ -0,0 +1 @@
+export * from "./upload-step";
diff --git a/src/features/upload/ui/UploadStepIndicator.tsx b/src/features/upload/components/features/upload-step/UploadStepIndicator.tsx
similarity index 100%
rename from src/features/upload/ui/UploadStepIndicator.tsx
rename to src/features/upload/components/features/upload-step/UploadStepIndicator.tsx
diff --git a/src/features/upload/ui/UploadStepResultInput.tsx b/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
similarity index 84%
rename from src/features/upload/ui/UploadStepResultInput.tsx
rename to src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
index fa20850..a6fb7be 100644
--- a/src/features/upload/ui/UploadStepResultInput.tsx
+++ b/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
@@ -14,7 +14,7 @@ export const UploadStepResultInput = ({ value, onChange }: Props) => {
fontWeight="bold"
lineHeight="1.4"
>
- 작업 결과 입력
+ ?묒뾽 寃곌낵 ?낅젰
@@ -33,7 +33,7 @@ export const UploadStepResultInput = ({ value, onChange }: Props) => {
minH={60}
onChange={(e) => onChange(e.target.value)}
p={6}
- placeholder="이전 단계 프롬프트로 얻은 AI의 답변을 여기에 붙여넣어 주세요. 정보를 입력하면 다음 단계 로드맵이 더욱 정교해집니다."
+ placeholder="?댁쟾 ?④퀎 ?꾨\?꾪듃濡??살? AI???듬????ш린??遺숈뿬?l뼱 二쇱꽭?? ?뺣낫瑜??낅젰?섎㈃ ?ㅼ쓬 ?④퀎 濡쒕뱶留듭씠 ?붿슧 ?뺢탳?댁쭛?덈떎."
resize="vertical"
value={value}
/>
diff --git a/src/features/upload/components/features/upload-step/index.ts b/src/features/upload/components/features/upload-step/index.ts
new file mode 100644
index 0000000..09b073a
--- /dev/null
+++ b/src/features/upload/components/features/upload-step/index.ts
@@ -0,0 +1,2 @@
+export * from "./UploadStepIndicator";
+export * from "./UploadStepResultInput";
diff --git a/src/features/upload/components/index.ts b/src/features/upload/components/index.ts
new file mode 100644
index 0000000..0e84926
--- /dev/null
+++ b/src/features/upload/components/index.ts
@@ -0,0 +1 @@
+export * from "./features";
diff --git a/src/features/upload/index.ts b/src/features/upload/index.ts
index 406fe60..69d2709 100644
--- a/src/features/upload/index.ts
+++ b/src/features/upload/index.ts
@@ -1,3 +1,4 @@
+export * from "./components";
export * from "./hooks";
export * from "./ui";
export * from "./utils";
diff --git a/src/features/upload/ui/index.ts b/src/features/upload/ui/index.ts
index eabfd57..d489887 100644
--- a/src/features/upload/ui/index.ts
+++ b/src/features/upload/ui/index.ts
@@ -1,3 +1 @@
export * from "./section";
-export * from "./UploadStepIndicator";
-export * from "./UploadStepResultInput";
diff --git a/src/features/upload/ui/section/UploadStepContentSection.tsx b/src/features/upload/ui/section/UploadStepContentSection.tsx
index e39d583..a18ef33 100644
--- a/src/features/upload/ui/section/UploadStepContentSection.tsx
+++ b/src/features/upload/ui/section/UploadStepContentSection.tsx
@@ -6,12 +6,12 @@ import { PromptCard } from "@/entities";
import { useClipboardCopy } from "@/shared";
import { ArrowRightIcon } from "@/shared/_assets/icons";
+import { UploadStepResultInput } from "../../components";
import {
useUploadStepData,
useUploadStepProject,
useUploadStepSubmission,
} from "../../hooks";
-import { UploadStepResultInput } from "../UploadStepResultInput";
type Props = {
projectId: string;
diff --git a/src/features/upload/ui/section/UploadStepHeaderSection.tsx b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
index f83af40..d542118 100644
--- a/src/features/upload/ui/section/UploadStepHeaderSection.tsx
+++ b/src/features/upload/ui/section/UploadStepHeaderSection.tsx
@@ -3,8 +3,8 @@ import { Box, Text, VStack } from "@chakra-ui/react";
import { ROADMAP_TYPE_LABEL } from "@/entities";
import { BackButton } from "@/shared";
+import { UploadStepIndicator } from "../../components";
import { useUploadStepNavigation, useUploadStepProject } from "../../hooks";
-import { UploadStepIndicator } from "../UploadStepIndicator";
type Props = {
projectId: string;
From b5c47e4ad2b52ee3e8dd0af64da52999ca2a0e3f Mon Sep 17 00:00:00 2001
From: TTOCHIwas <95687307+TTOCHIwas@users.noreply.github.com>
Date: Thu, 26 Mar 2026 05:16:32 +0900
Subject: [PATCH 18/18] =?UTF-8?q?chore:=20=ED=95=9C=EA=B8=80=20=EA=B9=A8?=
=?UTF-8?q?=EC=A7=90=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../components/features/upload-step/UploadStepResultInput.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx b/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
index a6fb7be..fa20850 100644
--- a/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
+++ b/src/features/upload/components/features/upload-step/UploadStepResultInput.tsx
@@ -14,7 +14,7 @@ export const UploadStepResultInput = ({ value, onChange }: Props) => {
fontWeight="bold"
lineHeight="1.4"
>
- ?묒뾽 寃곌낵 ?낅젰
+ 작업 결과 입력
@@ -33,7 +33,7 @@ export const UploadStepResultInput = ({ value, onChange }: Props) => {
minH={60}
onChange={(e) => onChange(e.target.value)}
p={6}
- placeholder="?댁쟾 ?④퀎 ?꾨\?꾪듃濡??살? AI???듬????ш린??遺숈뿬?l뼱 二쇱꽭?? ?뺣낫瑜??낅젰?섎㈃ ?ㅼ쓬 ?④퀎 濡쒕뱶留듭씠 ?붿슧 ?뺢탳?댁쭛?덈떎."
+ placeholder="이전 단계 프롬프트로 얻은 AI의 답변을 여기에 붙여넣어 주세요. 정보를 입력하면 다음 단계 로드맵이 더욱 정교해집니다."
resize="vertical"
value={value}
/>