From a7b3164f78d216dc963e66632540165cdcbb4ab1 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 28 Apr 2026 13:25:08 +0900 Subject: [PATCH] =?UTF-8?q?fix(study):=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=B9=B4=EC=9A=B4=ED=84=B0=20=EC=8B=A0?= =?UTF-8?q?=EB=A2=B0=20X=20=E2=80=94=20=EC=84=9C=EB=B2=84=20max+1=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B1=84=EC=9B=80=20(user-edited=20dirty?= =?UTF-8?q?=20flag)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전 fix(effect→onchange)에도 race 재발 (id 306,307 qnum=1,2 로 또 들어감). 근본 해결 — 클라이언트의 f_qnum 표시값과 실제 저장값을 분리. 변경: - f_qnum_user_edited dirty flag 추가 - input 에 oninput → user_edited=true (사용자가 직접 박스 수정한 경우) - onMount fallback / onRoundChange / applyNewRound / 저장 후 → user_edited=false - POST body 의 exam_question_number: user_edited=true 면 명시 전송, false 면 null → 서버가 같은 회차 max+1 자동 채움 (PR-6 의 기존 서버 로직) - POST 응답의 실제 저장 qnum 으로 화면 동기화 (saved.exam_question_number) → 표시값이 어긋났어도 저장 후 정확하게 갱신 - applyNewRound 에서 이미 존재하는 회차명 입력 시 next_question_number 적용 (사용자가 dropdown 대신 새 회차 모드로 같은 이름 다시 입력해도 1번부터 다시 시작 X) 이제 클라이언트가 어떤 표시값을 보여주든 실제 저장은 항상 정확. 사용자가 직접 박스를 수정한 경우만 명시 전송. --- .../topics/[id]/questions/new/+page.svelte | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/frontend/src/routes/study/topics/[id]/questions/new/+page.svelte b/frontend/src/routes/study/topics/[id]/questions/new/+page.svelte index dec4c7b..7e7769e 100644 --- a/frontend/src/routes/study/topics/[id]/questions/new/+page.svelte +++ b/frontend/src/routes/study/topics/[id]/questions/new/+page.svelte @@ -48,7 +48,11 @@ let f_exam_round = $state(''); let f_exam_round_mode = $state('select'); // 'select' | 'new' let f_exam_round_new = $state(''); - let f_qnum = $state(1); // 문항 번호 + let f_qnum = $state(1); // 문항 번호 (표시용 — 저장 시 서버 max+1 우선) + // PR-7: 사용자가 input 박스를 직접 편집했는지 추적. true 면 저장 시 명시 전송, + // false 면 null 보내고 서버가 같은 회차 max+1 자동 채움. 회차 변경/저장 후/onMount + // 에서 false 로 reset. + let f_qnum_user_edited = $state(false); // 본문 자동 reset 안 되는 메타 (편의성 — 비워둠이 default) let explanation = $state(''); @@ -146,6 +150,8 @@ f_qnum = found.next_question_number; } } + // 진입 시점은 사용자가 input 박스를 만진 적 없음 — false 로 reset. + f_qnum_user_edited = false; // $effect 의 lastExamRound 를 현재 값으로 sync — 첫 실행이 또 reset 하지 않도록. lastExamRound = f_exam_round; }); @@ -176,6 +182,7 @@ } else if (!found) { f_qnum = 1; } + f_qnum_user_edited = false; lastExamRound = f_exam_round; refreshCompleteFlag(); } @@ -200,11 +207,20 @@ addToast('error', '회차명을 입력하세요'); return; } - f_exam_round = f_exam_round_new.trim(); + const newRound = f_exam_round_new.trim(); + f_exam_round = newRound; f_exam_round_new = ''; f_exam_round_mode = 'select'; - f_qnum = 1; - lastExamRound = f_exam_round; + // PR-7 fix: 사용자가 "새 회차" 모드에 이미 존재하는 회차명을 입력했으면 + // next_question_number 로 시작 (1번부터 다시 시작 X). dropdown 선택과 동일 동작. + const found = examRounds.find((r) => r.exam_round === newRound); + if (found && found.next_question_number) { + f_qnum = found.next_question_number; + } else { + f_qnum = 1; + } + f_qnum_user_edited = false; + lastExamRound = newRound; refreshCompleteFlag(); } @@ -234,6 +250,9 @@ persist(); try { const subj = effectiveSubject(); + // PR-7 fix: 클라이언트 카운터를 신뢰하지 않고 서버가 항상 max+1 결정. + // 사용자가 직접 input 박스를 다른 값으로 수정한 경우만 명시 전송. + const userEditedQnum = f_qnum_user_edited && f_exam_round; const body = { question_text: q_text.trim(), choice_1: c1.trim(), @@ -245,18 +264,25 @@ scope: f_scope || null, exam_name: f_exam_name || null, exam_round: f_exam_round || null, - exam_question_number: f_exam_round ? Number(f_qnum) : null, + // 사용자가 직접 수정 안 했으면 null → 서버가 같은 회차 max+1 자동 채움 + exam_question_number: userEditedQnum ? Number(f_qnum) : null, explanation: explanation || null, source_note: autoSourceNote() || null, }; - await api(`/study-topics/${topicId}/questions`, { + const saved = await api(`/study-topics/${topicId}/questions`, { method: 'POST', body: JSON.stringify(body), }); - addToast('success', `문제 저장됨${f_exam_round ? ` (${f_exam_round} ${f_qnum}번)` : ''}`); + // 응답의 실제 저장값으로 표시 동기화 (서버가 결정한 qnum) + const actualQnum = saved?.exam_question_number ?? null; + if (actualQnum) f_qnum = actualQnum; + addToast('success', `문제 저장됨${f_exam_round && actualQnum ? ` (${f_exam_round} ${actualQnum}번)` : ''}`); if (continueAfter) { clearForCont(); - f_qnum = Number(f_qnum) + 1; + // 다음 표시값 = 방금 저장된 qnum + 1. 사용자가 다시 수정하기 전까지는 + // 자동(서버 max+1) 모드 유지. + if (actualQnum) f_qnum = actualQnum + 1; + f_qnum_user_edited = false; // 회차 진행률 갱신 (도달 체크) await refreshExamRounds(); persist(); @@ -406,6 +432,7 @@ {#if currentProgress() && currentProgress().size}