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 ( + + + + + background circle + + + + + + 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 ( - - - - - background circle - - - - - - AI가 과제의 핵심을 -
- 분석하고 있어요 -
- - 분석이 완료되면 나만의 로드맵이 펼쳐집니다. - -
- - - - - {currentStep.message} - - - {Math.round(progress)}% - - - - - - - - - 잠시만 기다려주세요, 거의 다 되었습니다. - - -
-
+ ); }