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}