From b721151d0fe2e2872186b8cc256169e51501927e Mon Sep 17 00:00:00 2001 From: Hyun An Date: Sat, 18 Jul 2026 18:49:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[REFACTOR]=20=ED=80=B4=EC=A6=88=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=AA=A8=EB=8B=AC=20=EB=A9=94=ED=83=80=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20useEffect=20=ED=8E=98=EC=B9=AD=E2=86=92use?= =?UTF-8?q?Transition=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20#872?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 강의 선택 시 메타(섹션 주차·takenWeeks) 조회를 반응형 useEffect([courseId])에서 '연결 강의' onChange 핸들러의 useTransition 호출로 이동 (렌더 워터폴 제거, §4) - 취소 안전성: transition엔 cleanup 취소가 없어 metaReqRef로 stale 응답 무시로 대체 (빠른 강의 전환 시 옛 응답이 새 선택을 덮지 않게) - presetCourseId(개별 강의 페이지·강의 고정)만 마운트 1회 로드로 축소 - metaLoading에 isPending 통합, 에러 토스트(§0.1)·1주1퀴즈 중복차단 동작 보존 - 회귀 테스트 18/18 통과 (presetCourseId·adminMeta 라우팅·에러토스트·주차소진) --- .../quizzes/components/QuizFormModal.tsx | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx index 067e0c01..e6402522 100644 --- a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx +++ b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx @@ -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'; @@ -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 없음). + } + }); + }; + // presetCourseId(개별 강의 페이지, 강의 고정)만 마운트 시 1회 로드 — 반응형 useEffect 페칭 제거. + // 사용자가 강의를 고르는 경우는 '연결 강의' onChange에서 loadMeta가 처리한다. + useEffect(() => { + if (presetCourseId) loadMeta(presetCourseId); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // 로딩 = 선택 강의 기준 meta 페칭 중이거나 아직 안 옴 (isPending 통합). const metaLoading = - mode === 'create' && courseId > 0 && meta.courseId !== courseId; + mode === 'create' && + courseId > 0 && + (isMetaPending || meta.courseId !== courseId); // 등록: 강의 실제 섹션 주차 중 아직 퀴즈 없는 것 / 수정: 자기 주차 고정(변경 불가) const weekOptions = @@ -384,6 +394,7 @@ export default function QuizFormModal({ setInstructor(v); setCourseId(0); setWeek(0); + metaReqRef.current++; // 강의 리셋 — 진행 중 메타 로드 무효화 }} disabled={mode === 'edit'} fullWidth @@ -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 From 5d90b8a6f015fd3f7707caf65d559aa2369776f2 Mon Sep 17 00:00:00 2001 From: Hyun An Date: Sat, 18 Jul 2026 20:18:33 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[REFACTOR]=20metaLoading=EC=97=90=EC=84=9C?= =?UTF-8?q?=20isPending=20=EC=A0=9C=EA=B1=B0=20=E2=80=94=20stale=20transit?= =?UTF-8?q?ion=20=EC=BB=A4=ED=94=8C=EB=A7=81=20=ED=95=B4=EC=86=8C=20#872?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - metaLoading이 isMetaPending을 OR로 참조해, 빠른 강의 전환으로 무효화된 stale transition이 아직 pending이면 현재 선택 로드 후에도 주차 드롭다운이 계속 잠기던 회귀 수정 - 파생 조건(meta.courseId !== courseId)만으로 로딩 판별 — 더 정확하고 stale 커플링 없음 - 코드리뷰(적대적 검증) 반영, 회귀 테스트 18/18 유지 --- .../src/features/quizzes/components/QuizFormModal.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx index e6402522..537399c5 100644 --- a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx +++ b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx @@ -135,7 +135,7 @@ export default function QuizFormModal({ [visibleCourses] ); const metaReqRef = useRef(0); - const [isMetaPending, startMetaTransition] = useTransition(); + const [, startMetaTransition] = useTransition(); // 강의 선택 시 실제 섹션(주차)·이미 쓴 주차를 서버에서 로드. useEffect 반응형 페칭(렌더 워터폴) 대신 // 이벤트 핸들러('연결 강의' onChange)에서 useTransition으로 호출(§4). transition엔 cleanup 취소가 // 없어, 취소 안전성은 metaReqRef로 stale 응답 무시로 대체(빠른 강의 전환 시 옛 응답이 새 선택을 덮지 않게). @@ -165,11 +165,10 @@ export default function QuizFormModal({ if (presetCourseId) loadMeta(presetCourseId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // 로딩 = 선택 강의 기준 meta 페칭 중이거나 아직 안 옴 (isPending 통합). + // 로딩 = 선택 강의 기준 meta가 아직 안 옴 (파생). isPending은 안 씀 — 빠른 강의 전환으로 무효화된 + // stale transition이 pending이면 현재 선택 로드 후에도 주차 드롭다운을 계속 잠글 수 있어서. const metaLoading = - mode === 'create' && - courseId > 0 && - (isMetaPending || meta.courseId !== courseId); + mode === 'create' && courseId > 0 && meta.courseId !== courseId; // 등록: 강의 실제 섹션 주차 중 아직 퀴즈 없는 것 / 수정: 자기 주차 고정(변경 불가) const weekOptions = From 06aaf77b4d107f3f7b07d928d78351695de89bd4 Mon Sep 17 00:00:00 2001 From: Hyun An Date: Sat, 18 Jul 2026 20:22:00 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[REFACTOR]=20presetCourseId=20=EB=A7=88?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EB=A1=9C=EB=93=9C=EC=97=90=20=EC=96=B8?= =?UTF-8?q?=EB=A7=88=EC=9A=B4=ED=8A=B8=20=EB=AC=B4=ED=9A=A8=ED=99=94=20cle?= =?UTF-8?q?anup=20=EC=B6=94=EA=B0=80=20#872?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모달이 닫힌 뒤 진행 중이던 preset 메타 로드의 stale 응답이 에러 토스트를 띄우거나 setMeta를 호출하는 것 방지 — cleanup에서 metaReqRef 증가로 무효화 (기존 useEffect cleanup의 cancelled 가드를 언마운트 경로에서 복원) - CodeRabbit 리뷰 반영, 회귀 테스트 18/18 유지 --- .../src/features/quizzes/components/QuizFormModal.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx index 537399c5..b81254b0 100644 --- a/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx +++ b/hard-click-frontend/src/features/quizzes/components/QuizFormModal.tsx @@ -161,10 +161,15 @@ export default function QuizFormModal({ }; // presetCourseId(개별 강의 페이지, 강의 고정)만 마운트 시 1회 로드 — 반응형 useEffect 페칭 제거. // 사용자가 강의를 고르는 경우는 '연결 강의' onChange에서 loadMeta가 처리한다. + // 언마운트 cleanup은 reqId를 올려 진행 중 로드를 무효화(모달 닫힌 뒤 stale 토스트·setMeta 방지). + /* eslint-disable react-hooks/exhaustive-deps -- 마운트 1회 로드 의도 + cleanup은 카운터 ref의 최신값을 써야 무효화가 맞음(DOM node ref 아님) */ useEffect(() => { if (presetCourseId) loadMeta(presetCourseId); - // eslint-disable-next-line react-hooks/exhaustive-deps + return () => { + metaReqRef.current += 1; + }; }, []); + /* eslint-enable react-hooks/exhaustive-deps */ // 로딩 = 선택 강의 기준 meta가 아직 안 옴 (파생). isPending은 안 씀 — 빠른 강의 전환으로 무효화된 // stale transition이 pending이면 현재 선택 로드 후에도 주차 드롭다운을 계속 잠글 수 있어서. const metaLoading =