Skip to content
Merged
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo, useEffect, useRef, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { toast } from '@/lib/toast';
Expand Down Expand Up @@ -134,32 +134,42 @@ export default function QuizFormModal({
})),
[visibleCourses]
);
// 강의 선택 시 실제 섹션(주차)·이미 쓴 주차 로드 (등록 모드). setState는 async 콜백에서만.
useEffect(() => {
if (mode !== 'create' || courseId <= 0) return;
let cancelled = false;
const metaReqRef = useRef(0);
const [isMetaPending, startMetaTransition] = useTransition();
// 강의 선택 시 실제 섹션(주차)·이미 쓴 주차를 서버에서 로드. useEffect 반응형 페칭(렌더 워터폴) 대신
// 이벤트 핸들러('연결 강의' onChange)에서 useTransition으로 호출(§4). transition엔 cleanup 취소가
// 없어, 취소 안전성은 metaReqRef로 stale 응답 무시로 대체(빠른 강의 전환 시 옛 응답이 새 선택을 덮지 않게).
const loadMeta = (cid: number) => {
if (mode !== 'create' || cid <= 0) return;
const reqId = ++metaReqRef.current;
// 관리자(adminMeta)는 소유자무관 관리자 목록으로 takenWeeks 집계 —
// 강사 엔드포인트(/api/instructor/quizzes)는 로그인 관리자 소유 퀴즈만 반환(0개)이라 1주1퀴즈 중복차단이 안 됨.
const metaAction = adminMeta
? getAdminQuizFormMetaAction
: getQuizFormMetaAction;
metaAction(courseId)
.then((m) => {
if (!cancelled) setMeta({ courseId, ...m });
})
.catch(() => {
startMetaTransition(async () => {
try {
const m = await metaAction(cid);
if (metaReqRef.current === reqId) setMeta({ courseId: cid, ...m });
} catch {
// 실패를 빈 주차로 폴백하면 '등록 가능한 주차가 없습니다'로 오표시돼(§0.1),
// 조용히 삼키지 않고 사용자에게 알린다. 주차 드롭다운은 로딩 상태로 남아 오등록을 막는다.
if (!cancelled)
if (metaReqRef.current === reqId)
toast.error('주차 정보를 불러오지 못했습니다. 잠시 후 다시 시도해주세요.');
});
return () => {
cancelled = true;
};
}, [courseId, mode, adminMeta]);
// 로딩 = 선택 강의 기준 meta가 아직 안 옴 (파생, 동기 setState 없음).
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx"

echo "== outline =="
ast-grep outline "$file" --view expanded || true

echo
echo "== relevant lines =="
nl -ba "$file" | sed -n '110,210p'

Repository: Hard-Click/Hard-Click-FrontEnd

Length of output: 528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx"

echo "== lines 130-185 =="
sed -n '130,185p' "$file"

echo
echo "== search for metaLoading / metaReqRef / useTransition =="
grep -nE 'useTransition|metaReqRef|metaLoading|loadMeta|startMetaTransition|isMetaPending' "$file" || true

Repository: Hard-Click/Hard-Click-FrontEnd

Length of output: 3398


🌐 Web query:

React useTransition async callback pending promise ignored startTransition docs

💡 Result:

In React 19 and later, the useTransition hook has been improved to automatically handle state updates that occur after an await within an async transition [1]. If you are using the startTransition function returned by useTransition, you no longer need to manually wrap state updates that follow an asynchronous request in an additional startTransition call [1]. These updates are automatically included in the transition [1]. However, this improvement applies specifically to the startTransition provided by the useTransition hook [1]. If you are using the standalone React.startTransition function (often used outside of components or where useTransition is unavailable), you must still manually wrap any state updates that occur after an await in a new startTransition call [2][1]. The requirement to wrap updates in earlier versions of React or when using standalone startTransition arises because React loses the asynchronous context when an await expression is encountered [3]. Without this context, React cannot automatically associate subsequent state updates with the original transition, causing them to be treated as standard, urgent updates instead of non-blocking transitions [4][3]. Summary of Behavior: 1. useTransition.startTransition: Supports async functions natively. State updates after await are automatically part of the transition [1]. 2. React.startTransition (standalone): Does not automatically track context after await. State updates after await must be wrapped in another startTransition [2][1]. If you find that your state updates after an await are not being treated as transitions, ensure you are utilizing the startTransition returned by useTransition and check that you are not accidentally relying on the standalone version [2][1].

Citations:


로딩 상태를 현재 요청 기준으로 분리하세요. isMetaPending만 쓰면 이전 강의의 느린 요청이 끝날 때까지 주차 선택이 계속 비활성화됩니다. reqId로 현재 요청의 pending을 따로 추적하고, 해당 요청이 끝날 때만 로딩을 해제하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx` around
lines 138 - 160, Update loadMeta to track pending state for the current reqId
rather than relying solely on the shared isMetaPending transition state. Mark
the active request as pending when it starts, and clear loading only when that
same request resolves or rejects; stale responses must not change the current
request’s loading state. Use the existing metaReqRef and metadata loading state
so an earlier course request cannot keep the current course’s week selector
disabled.

};
// presetCourseId(개별 강의 페이지, 강의 고정)만 마운트 시 1회 로드 — 반응형 useEffect 페칭 제거.
// 사용자가 강의를 고르는 경우는 '연결 강의' onChange에서 loadMeta가 처리한다.
useEffect(() => {
if (presetCourseId) loadMeta(presetCourseId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// 로딩 = 선택 강의 기준 meta 페칭 중이거나 아직 안 옴 (isPending 통합).
const metaLoading =
mode === 'create' && courseId > 0 && meta.courseId !== courseId;
mode === 'create' &&
courseId > 0 &&
(isMetaPending || meta.courseId !== courseId);

// 등록: 강의 실제 섹션 주차 중 아직 퀴즈 없는 것 / 수정: 자기 주차 고정(변경 불가)
const weekOptions =
Expand Down Expand Up @@ -384,6 +394,7 @@ export default function QuizFormModal({
setInstructor(v);
setCourseId(0);
setWeek(0);
metaReqRef.current++; // 강의 리셋 — 진행 중 메타 로드 무효화
}}
disabled={mode === 'edit'}
fullWidth
Expand All @@ -399,8 +410,10 @@ export default function QuizFormModal({
value={courseId > 0 ? String(courseId) : ''}
options={courseOptions}
onChange={(v) => {
setCourseId(Number(v));
const cid = Number(v);
setCourseId(cid);
setWeek(0); // 강의 바뀌면 주차 다시 선택
loadMeta(cid); // 반응형 effect 대신 선택 시점에 메타 로드
}}
disabled={mode === 'edit' || presetCourseId !== undefined}
fullWidth
Expand Down