From 4e784a1fbc12d1d879b49ca160d92e0774837d63 Mon Sep 17 00:00:00 2001 From: hyungi Date: Sun, 7 Jun 2026 14:34:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(study):=20=EB=AC=B8=EC=A0=9C=20=EC=9D=B4?= =?UTF-8?q?=EC=83=81=20=EC=8B=A0=EA=B3=A0(=ED=83=9C=EA=B9=85)=20UI=20?= =?UTF-8?q?=E2=80=94=20=ED=80=B4=EC=A6=88=C2=B7=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=ED=94=8C=EB=9E=98=EA=B7=B8=20+=20=EC=8B=A0=EA=B3=A0=ED=95=A8?= =?UTF-8?q?=20=ED=81=90=20+=20=ED=97=88=EB=B8=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드(needs_review/flagged_by 컬럼·PATCH·needs-review 큐 API)는 P1 때 깔렸으나 이를 쓰는 화면이 없어 사실상 미구현 상태였음. 프론트 UI 보강(백엔드 무변경). - 퀴즈 세션·문제 상세에 '이 문제 이상해요' 플래그 버튼(PATCH needs_review toggle, flagged_by='user'). 신고/해제 토스트. - 신규 /study/questions-review 신고함: 전 토픽 횡단 목록 + 사유칩(직접신고/문제수정됨/문제삭제됨) + 문제보기·수정 링크 + 검토완료(해제)·폐기(soft-delete). - 허브에 '문제 신고함' 카드 + count 배지(GET needs-review/count). - 퀴즈 세션 신고 상태는 세션 내 optimistic(결과 payload 에 needs_review 없음, 영속 source=신고함 큐). flagQuestion 은 PATCH 응답 needs_review 반영. 검증: 적대검토(runes·API계약·UX) 통과 — blocker(payload 미포함)는 프론트 init 제거로 해소(study_topics.py 미편집=타 세션 작업 보호). 기존 이모지(repeatBadge/근거)는 본 변경 무관. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/study/+page.svelte | 23 +++- .../study/questions-review/+page.svelte | 125 ++++++++++++++++++ .../topics/[id]/questions/[qid]/+page.svelte | 39 +++++- .../[id]/quiz-sessions/[sid]/+page.svelte | 43 +++++- 4 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 frontend/src/routes/study/questions-review/+page.svelte diff --git a/frontend/src/routes/study/+page.svelte b/frontend/src/routes/study/+page.svelte index 463137c..ce26300 100644 --- a/frontend/src/routes/study/+page.svelte +++ b/frontend/src/routes/study/+page.svelte @@ -3,14 +3,19 @@ // 주제로 보기(퀴즈·복습·통계) / 자료 학습 / 필사 세션 / 암기카드 검수. import { onMount } from 'svelte'; import { api } from '$lib/api'; - import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers, Repeat } from 'lucide-svelte'; + import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers, Repeat, Flag } from 'lucide-svelte'; let cardReviewCount = $state(0); + let questionFlagCount = $state(0); onMount(async () => { try { const r = await api('/study-cards/needs-review/count'); cardReviewCount = r?.count ?? 0; } catch {} + try { + const r2 = await api('/study-questions/needs-review/count'); + questionFlagCount = r2?.count ?? 0; + } catch {} }); @@ -78,7 +83,21 @@

암기카드 학습

-

검수한 암기카드를 모바일에서 학습. 복습(간격반복 1·3·7·14일)으로 자기평가하거나, 그냥 공부로 덜 본 카드를 휙휙 훑어봅니다.

+

검수한 암기카드를 모바일에서 학습. 복습(간격반복 1·3·7·14일)으로 자기평가하거나, 그냥 공부로 덜 본 카드를 가볍게 훑어봅니다.

+ + + +
+ +

문제 신고함

+ {#if questionFlagCount > 0} + {questionFlagCount} + {/if} +
+

퀴즈·문제 화면에서 이상하다고 신고한 문제를 모아 확인하고 수정·검토 완료·폐기합니다.

diff --git a/frontend/src/routes/study/questions-review/+page.svelte b/frontend/src/routes/study/questions-review/+page.svelte new file mode 100644 index 0000000..db8c4b4 --- /dev/null +++ b/frontend/src/routes/study/questions-review/+page.svelte @@ -0,0 +1,125 @@ + + +문제 신고함 + +
+
+ +

문제 신고함

+ {#if items.length > 0} + {items.length} + {/if} +
+ +

+ 퀴즈나 문제 화면에서 이상하다고 신고한 문제가 여기에 모입니다. + 내용을 확인해 수정하거나, 검토 완료(신고 해제)하거나, 폐기하세요. +

+ + {#if loading} +
{#each Array(4).fill(0) as _, i (i)}{/each}
+ {:else if items.length === 0} + + {:else} +
+ {#each items as it (it.id)} + {@const r = reasonOf(it.flagged_by)} +
+
+ {r.label} + {#if it.flagged_at}{fmtDate(it.flagged_at)}{/if} +
+
{it.question_text}
+
+ + + + +
+
+ {/each} +
+ {/if} +
diff --git a/frontend/src/routes/study/topics/[id]/questions/[qid]/+page.svelte b/frontend/src/routes/study/topics/[id]/questions/[qid]/+page.svelte index 996be4e..627aa3f 100644 --- a/frontend/src/routes/study/topics/[id]/questions/[qid]/+page.svelte +++ b/frontend/src/routes/study/topics/[id]/questions/[qid]/+page.svelte @@ -22,7 +22,7 @@ import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; import { - ArrowLeft, ArrowRight, Edit, Sparkles, AlertCircle, CheckCircle2, XCircle, ListChecks, + ArrowLeft, ArrowRight, Edit, Sparkles, AlertCircle, CheckCircle2, XCircle, ListChecks, Flag, } from 'lucide-svelte'; import { renderMathMarkdown, renderMathMarkdownInline } from '$lib/utils/mathMarkdown'; import Button from '$lib/components/ui/Button.svelte'; @@ -237,6 +237,26 @@ if (!s) return ''; return new Date(s).toLocaleString('ko-KR', { dateStyle: 'short', timeStyle: 'short' }); } + + // 이 문제 이상해요 신고 토글 (PATCH needs_review). 신고함(/study/questions-review)에 모임. + let flagBusy = $state(false); + async function toggleFlag() { + if (flagBusy || !q) return; + flagBusy = true; + const next = !q.needs_review; + try { + const res = await api(`/study-questions/${qid}`, { + method: 'PATCH', + body: JSON.stringify({ needs_review: next }), + }); + q = { ...q, needs_review: res.needs_review, flagged_by: res.flagged_by, flagged_at: res.flagged_at }; + addToast(next ? 'success' : 'info', next ? '이상 문제로 신고했어요 — 검수함에 모았습니다' : '신고를 해제했어요'); + } catch (err) { + addToast('error', err?.detail || '신고 처리 실패'); + } finally { + flagBusy = false; + } + } 문제 상세 — {topicName || '주제'} @@ -279,7 +299,22 @@ {#if !q.is_active}· 비활성{/if} - +
+ + +
diff --git a/frontend/src/routes/study/topics/[id]/quiz-sessions/[sid]/+page.svelte b/frontend/src/routes/study/topics/[id]/quiz-sessions/[sid]/+page.svelte index 843025d..b376dd3 100644 --- a/frontend/src/routes/study/topics/[id]/quiz-sessions/[sid]/+page.svelte +++ b/frontend/src/routes/study/topics/[id]/quiz-sessions/[sid]/+page.svelte @@ -14,7 +14,7 @@ import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; import { - ArrowLeft, CheckCircle2, XCircle, HelpCircle, Sparkles, BookOpen, AlertCircle, Square, CheckSquare, + ArrowLeft, CheckCircle2, XCircle, HelpCircle, Sparkles, BookOpen, AlertCircle, Square, CheckSquare, Flag, } from 'lucide-svelte'; import { renderMathMarkdown, renderMathMarkdownInline } from '$lib/utils/mathMarkdown'; import Button from '$lib/components/ui/Button.svelte'; @@ -36,6 +36,24 @@ // PR-12-A: 카드별 round_count 배지 (틀린/모르겠음 헤더에 표시). let relatedCounts = $state({}); // { [qid]: { repeat_round_count, similar_round_count, ... } } + // 문제 신고(이상 태깅): qid -> true. 검수함(/study/questions-review)에 모임. + let flagged = $state({}); + let flagBusy = $state({}); + async function flagQuestion(qid) { + if (flagBusy[qid]) return; + const next = !flagged[qid]; + flagBusy = { ...flagBusy, [qid]: true }; + try { + const res = await api(`/study-questions/${qid}`, { method: 'PATCH', body: JSON.stringify({ needs_review: next }) }); + flagged = { ...flagged, [qid]: res?.needs_review ?? next }; + addToast(next ? 'success' : 'info', next ? '이상 문제로 신고했어요 — 검수함에 모았습니다' : '신고를 해제했어요'); + } catch (err) { + addToast('error', err?.detail || '신고 처리 실패'); + } finally { + flagBusy = { ...flagBusy, [qid]: false }; + } + } + async function loadTopic() { try { const t = await api(`/study-topics/${topicId}`); @@ -51,6 +69,8 @@ if ((detail.summary.wrong_count ?? 0) > 0) activeTab = 'wrong'; else if ((detail.summary.unsure_count ?? 0) > 0) activeTab = 'unsure'; else activeTab = 'correct'; + // 신고 상태의 영속 source 는 신고함 큐(/study/questions-review) — 세션 결과 payload 엔 + // needs_review 가 없으므로 여기선 세션 내 optimistic 표시만. 새로고침 시 초기화됨. // PR-12-A: 카드별 반복 출제/유사 유형 배지 — 1회 bulk 호출. void loadRelatedCounts(); } catch (err) { @@ -562,8 +582,21 @@ {@render subjectNoteBlock(it, cardState)} {/if} - {#if kind !== 'correct'} -
+
+ + {#if kind !== 'correct'} -
- {/if} + {/if} +
{/if}