Skip to content

Commit 3ca212f

Browse files
authored
feat(mission): 목업 생성 로딩 시간 적용 (#254)
1 parent a052858 commit 3ca212f

2 files changed

Lines changed: 30 additions & 3 deletions

File tree

apps/web/app/mission/_components/mission-loading.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,40 @@ import { Button } from "@repo/ui";
44
import MissionLoadingCoin from "@repo/ui/svg/mission-loading-coin.svg";
55
import { useQuery } from "@tanstack/react-query";
66
import { useRouter } from "next/navigation";
7-
import { useEffect } from "react";
7+
import { useEffect, useState } from "react";
88
import { buildMissionCreationResultHref } from "@/app/mission/constants/mission-creation";
99
import { generationJobStatusOptions } from "@/lib/queries/mission-generation";
1010
import styles from "./mission-loading.module.css";
1111

12+
const MIN_LOADING_DURATION_MS = 8_000;
13+
const MAX_LOADING_DURATION_MS = 12_000;
14+
15+
function getLoadingDuration() {
16+
return (
17+
MIN_LOADING_DURATION_MS +
18+
Math.floor(Math.random() * (MAX_LOADING_DURATION_MS - MIN_LOADING_DURATION_MS + 1))
19+
);
20+
}
21+
1222
/**
1323
* AI 미션 초안 생성 job이 끝날 때까지 polling한다. jobId는 설문 제출 단계에서 만들어
1424
* URL로 넘겨받는다 — 생성 화면에서 mutation을 쏘지 않으므로 새로고침해도 새 job이 생기지 않고,
1525
* StrictMode에서 mutation 결과가 유실되던 문제도 없다. 반복 폴링은 refetchInterval에 맡긴다.
1626
*/
1727
export function MissionLoading({ jobId }: { jobId: string }) {
1828
const router = useRouter();
29+
const [loadingDuration] = useState(getLoadingDuration);
30+
const [hasMinimumLoadingTimeElapsed, setHasMinimumLoadingTimeElapsed] = useState(false);
1931
const { data: job, isError, refetch } = useQuery(generationJobStatusOptions(jobId));
2032

33+
useEffect(() => {
34+
const timeoutId = window.setTimeout(
35+
() => setHasMinimumLoadingTimeElapsed(true),
36+
loadingDuration,
37+
);
38+
return () => window.clearTimeout(timeoutId);
39+
}, [loadingDuration]);
40+
2141
useEffect(() => {
2242
const refetchOnAppActive = () => {
2343
void refetch();
@@ -27,10 +47,10 @@ export function MissionLoading({ jobId }: { jobId: string }) {
2747
}, [refetch]);
2848

2949
useEffect(() => {
30-
if (job?.status === "SUCCEEDED" && job.draftsAvailable) {
50+
if (hasMinimumLoadingTimeElapsed && job?.status === "SUCCEEDED" && job.draftsAvailable) {
3151
router.replace(buildMissionCreationResultHref(jobId));
3252
}
33-
}, [job, jobId, router]);
53+
}, [hasMinimumLoadingTimeElapsed, job, jobId, router]);
3454

3555
// 5초마다 폴링하므로 일시적인 조회 실패로 "생성 실패"를 띄우면 안 된다 — 다음 폴링이
3656
// 성공할 수 있다. 서버가 FAILED를 주거나, 첫 조회부터 실패해 상태를 아예 못 받은 경우만 실패다.

apps/web/app/mission/new/loading/page.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ const PENDING_JOB = {
4949
describe("MissionLoading", () => {
5050
beforeEach(() => {
5151
vi.useFakeTimers();
52+
vi.spyOn(Math, "random").mockReturnValue(0);
5253
});
5354

5455
afterEach(() => {
5556
vi.useRealTimers();
57+
vi.restoreAllMocks();
5658
vi.clearAllMocks();
5759
});
5860

@@ -114,6 +116,11 @@ describe("MissionLoading", () => {
114116

115117
renderWithClient();
116118

119+
await vi.waitFor(() => expect(fetchGenerationJobStatus).toHaveBeenCalledTimes(1));
120+
expect(replaceMock).not.toHaveBeenCalled();
121+
122+
await vi.advanceTimersByTimeAsync(8_000);
123+
117124
await vi.waitFor(() =>
118125
expect(replaceMock).toHaveBeenCalledWith("/mission/new/result?jobId=job-1"),
119126
);

0 commit comments

Comments
 (0)