diff --git a/src/features/upload/hooks/index.ts b/src/features/upload/hooks/index.ts
index c8ab502..99b1e5d 100644
--- a/src/features/upload/hooks/index.ts
+++ b/src/features/upload/hooks/index.ts
@@ -1,3 +1,4 @@
+export * from "./useUploadLoadingProgress";
export * from "./useUploadStepData";
export * from "./useUploadStepNavigation";
export * from "./useUploadStepProject";
diff --git a/src/features/upload/hooks/useUploadLoadingProgress.ts b/src/features/upload/hooks/useUploadLoadingProgress.ts
new file mode 100644
index 0000000..e0b87bf
--- /dev/null
+++ b/src/features/upload/hooks/useUploadLoadingProgress.ts
@@ -0,0 +1,86 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router";
+
+import { useUploadFlowStore } from "@/entities";
+import { DYNAMIC_ROUTE_PATHS, ROUTE_PATHS } from "@/shared";
+
+const MAX_TIMER_PROGRESS = 90;
+const PROGRESS_SLOWDOWN_THRESHOLD = 70;
+const FAST_PROGRESS_INCREMENT = 1.2;
+const SLOW_PROGRESS_INCREMENT = 0.6;
+const PROGRESS_INTERVAL_MS = 80;
+const STEP_REDIRECT_DELAY_MS = 600;
+
+const LOADING_STEPS = [
+ { threshold: 95, message: "마무리 중.." },
+ { threshold: 75, message: "로드맵 생성 중.." },
+ { threshold: 50, message: "과제 내용 분석 중.." },
+ { threshold: 20, message: "파일 텍스트 추출 중.." },
+ { threshold: 0, message: "파일 업로드 중.." },
+];
+
+type Result = {
+ progress: number;
+ currentStepMessage: string;
+};
+
+export const useUploadLoadingProgress = (): Result => {
+ const navigate = useNavigate();
+ const [timerProgress, setTimerProgress] = useState(0);
+
+ const projectId = useUploadFlowStore((state) => state.projectId);
+ const error = useUploadFlowStore((state) => state.error);
+
+ const progress = projectId ? 100 : timerProgress;
+
+ useEffect(() => {
+ if (projectId) {
+ return;
+ }
+
+ const interval = setInterval(() => {
+ setTimerProgress((prev) => {
+ if (prev >= MAX_TIMER_PROGRESS) {
+ clearInterval(interval);
+ return MAX_TIMER_PROGRESS;
+ }
+
+ const increment =
+ prev < PROGRESS_SLOWDOWN_THRESHOLD
+ ? FAST_PROGRESS_INCREMENT
+ : SLOW_PROGRESS_INCREMENT;
+
+ return Math.min(prev + increment, MAX_TIMER_PROGRESS);
+ });
+ }, PROGRESS_INTERVAL_MS);
+
+ return () => clearInterval(interval);
+ }, [projectId]);
+
+ useEffect(() => {
+ if (error) {
+ navigate(ROUTE_PATHS.FILE_UPLOAD);
+ }
+ }, [error, navigate]);
+
+ useEffect(() => {
+ if (!projectId) {
+ return;
+ }
+
+ const timer = setTimeout(() => {
+ navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, 1));
+ }, STEP_REDIRECT_DELAY_MS);
+
+ return () => clearTimeout(timer);
+ }, [projectId, navigate]);
+
+ const currentStepMessage =
+ LOADING_STEPS.find((step) => progress >= step.threshold)?.message ??
+ LOADING_STEPS[LOADING_STEPS.length - 1].message;
+
+ return {
+ progress,
+ currentStepMessage,
+ };
+};
diff --git a/src/features/upload/ui/section/UploadLoadingSection.tsx b/src/features/upload/ui/section/UploadLoadingSection.tsx
new file mode 100644
index 0000000..0e4023c
--- /dev/null
+++ b/src/features/upload/ui/section/UploadLoadingSection.tsx
@@ -0,0 +1,108 @@
+import { Box, Flex, Image, Text, VStack } from "@chakra-ui/react";
+
+import { SproutAnimation } from "@/features";
+import AbstractBackgroundCircle from "@/shared/_assets/images/abstract-background-circle.svg";
+
+type Props = {
+ progress: number;
+ currentStepMessage: string;
+};
+
+export const UploadLoadingSection = ({
+ progress,
+ currentStepMessage,
+}: Props) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ AI가 과제의 핵심을
+
+ 분석하고 있어요
+
+
+ 분석이 완료되면 나만의 로드맵이 펼쳐집니다.
+
+
+
+
+
+
+ {currentStepMessage}
+
+
+ {Math.round(progress)}%
+
+
+
+
+
+
+
+
+ 잠시만 기다려주세요, 거의 다 되었습니다.
+
+
+
+
+ );
+};
diff --git a/src/features/upload/ui/section/index.ts b/src/features/upload/ui/section/index.ts
index d17f2c8..07e0795 100644
--- a/src/features/upload/ui/section/index.ts
+++ b/src/features/upload/ui/section/index.ts
@@ -1,2 +1,3 @@
+export * from "./UploadLoadingSection";
export * from "./UploadStepContentSection";
export * from "./UploadStepHeaderSection";
diff --git a/src/pages/upload/_loading/UploadLoadingPage.tsx b/src/pages/upload/_loading/UploadLoadingPage.tsx
index c023f50..6cd7c1a 100644
--- a/src/pages/upload/_loading/UploadLoadingPage.tsx
+++ b/src/pages/upload/_loading/UploadLoadingPage.tsx
@@ -1,154 +1,12 @@
-import { useEffect, useState } from "react";
-import { useNavigate } from "react-router";
-
-import { Box, Flex, Image, Text, VStack } from "@chakra-ui/react";
-
-import { useUploadFlowStore } from "@/entities";
-import { SproutAnimation } from "@/features";
-import { DYNAMIC_ROUTE_PATHS, ROUTE_PATHS } from "@/shared";
-import AbstractBackgroundCircle from "@/shared/_assets/images/abstract-background-circle.svg";
-
-const LOADING_STEPS = [
- { threshold: 0, message: "파일 업로드 중..." },
- { threshold: 20, message: "PDF 텍스트 추출 중..." },
- { threshold: 50, message: "과제 내용 분석 중..." },
- { threshold: 75, message: "로드맵 생성 중..." },
- { threshold: 95, message: "마무리 중..." },
-];
+import { UploadLoadingSection, useUploadLoadingProgress } from "@/features";
export default function UploadLoadingPage() {
- const navigate = useNavigate();
- const [timerProgress, setTimerProgress] = useState(0);
-
- const projectId = useUploadFlowStore((state) => state.projectId);
- const error = useUploadFlowStore((state) => state.error);
-
- const progress = projectId ? 100 : timerProgress;
-
- useEffect(() => {
- const interval = setInterval(() => {
- setTimerProgress((prev) => {
- if (prev >= 90) {
- clearInterval(interval);
- return 90;
- }
- const increment = prev < 70 ? 1.2 : 0.6;
- return Math.min(prev + increment, 90);
- });
- }, 80);
- return () => clearInterval(interval);
- }, []);
-
- useEffect(() => {
- if (error) {
- navigate(ROUTE_PATHS.FILE_UPLOAD);
- }
- }, [error, navigate]);
-
- useEffect(() => {
- if (projectId) {
- const timer = setTimeout(() => {
- navigate(DYNAMIC_ROUTE_PATHS.UPLOAD_STEP(projectId, 1));
- }, 600);
- return () => clearTimeout(timer);
- }
- }, [projectId, navigate]);
-
- const currentStep =
- [...LOADING_STEPS].reverse().find((s) => progress >= s.threshold) ??
- LOADING_STEPS[0];
+ const { progress, currentStepMessage } = useUploadLoadingProgress();
return (
-
-
-
-
-
-
-
-
-
-
- AI가 과제의 핵심을
-
- 분석하고 있어요
-
-
- 분석이 완료되면 나만의 로드맵이 펼쳐집니다.
-
-
-
-
-
-
- {currentStep.message}
-
-
- {Math.round(progress)}%
-
-
-
-
-
-
-
-
- 잠시만 기다려주세요, 거의 다 되었습니다.
-
-
-
-
+
);
}