Conversation
폴더 하나에서 내가 틀린 문항을 모아 유형마다 새 문제집을 만든다. 새 문제를 생성하지 않고 틀렸던 문항을 그대로 복제하므로 AI 를 거치지 않는다. 수집 범위는 quiz_history 한 쿼리(내 기록 ∩ 이 폴더 ∩ 끝까지 푼 것)로 닫는다. 정오답은 저장돼 있지 않아 저장된 답안으로 되짚는데, 기록 상세가 쓰던 판정을 AnswerJudge 로 꺼내 수집과 같은 함수를 거치게 했다 — 새 채점 규칙을 만들지 않는다. 서술형은 정오답이 규칙으로 갈리지 않아 제외하고 제외된 문항 수만 알린다. 정렬 → 중복 제거 → 상한 순서가 규칙의 일부다. 중복 제거가 앞서면 같은 문항의 오래된 쪽이 남아 상한에서 최신 오답이 밀려난다. 유형 하나가 트랜잭션 하나라, 한 유형이 실패해도 나머지는 남고 실패한 유형은 반쯤 만들어진 채 남지 않는다. session_id 를 요청 단위로 결정론 생성해 연타·재시도가 목록을 어지럽히지 않는다. - V21: problem_set.origin·source_folder_id, problem.origin_*(재출제 혈통), quiz_history.completed_at(기존 완료 행은 created_at 으로 백필) - 문제집과 함께 풀이 기록을 출처 폴더에 만든다. 이 레포에서 폴더는 문제집이 아니라 기록에 붙고 목록도 기록을 훑으므로, 기록이 없으면 만들어진 문제집이 목록에도 폴더에도 안 뜬다. - 원본 자료를 가리키는 페이지 번호는 물려주지 않는다. 자료가 없는 문제집에서 해설의 참조 자료 안내가 사실과 다른 말을 하게 된다. - 빈 결과·상한 초과·부분 실패는 200 으로 내린다. 알려야 할 상태이지 요청의 실패가 아니다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzgVQVdWdJQTp1cUvu44X
오답 모아풀기가 미완료 기록을 폴더에 만들면서, 아직 풀지도 않은 문제집이 목록 '완료일' 칸에 날짜를 표시하게 됐다. takenAt 이 기록 생성 시각을 싣고 있었기 때문이다. 완료 시각 컬럼으로 옮긴다. 기존 완료 행은 백필 값이 생성 시각이라 표시가 그대로고, 완료하지 않은 기록은 날짜 대신 비어서 내려간다. 푼 시각이 모두 같은 구간에서 무엇이 상한에 잘려나가는지도 함께 고정했다 — 컬럼이 생기기 전 기록은 백필로 전부 동률이 되고, 그때는 문항 번호만이 순서를 가른다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzgVQVdWdJQTp1cUvu44X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ICC-419 오답 모아풀기 — 폴더의 틀린 문항을 유형별 문제집으로 재출제
요약
폴더 하나를 기준으로 내가 틀린 문항만 모아 유형마다 새 문제집을 만든다. 문제를 새로 생성하지 않고
틀렸던 문항을 그대로 복제하므로 AI 호출이 없고, 요청 하나로 끝난다(SSE 불필요).
만들어진 문제집은 그 시점부터 일반 문제집과 같다 — 기존 풀이·채점·해설·기록·목록·관리 경로가 코드 변경
없이 그대로 동작한다. 목록에서는 자료로 만든 문제집과 구별된다.
요구 원장은
specs/008-wrong-answer-set/spec.md, 팀 실행 계약은 같은 폴더contract.md §확정 인터페이스다.이 레포를 조사해서 알게 된 것 (설계의 전제)
설계 판단이 여기서 갈렸으므로 먼저 적는다.
quiz_folder+quiz_history.folder_id(V16).problem_set에는 폴더 개념이 없다. 그래서 수집 범위가quiz_history한 쿼리로 닫힌다.ProblemSet.quizType). 유형별 분할은 원본 세트의quizType으로 묶으면 끝이고, "한 문제집에 한 유형만"이 코드가 아니라 스키마로 보장된다.다시 판정한다. 그래서 수집도 기존 판정기를 그대로 재사용한다 — 새 채점 규칙을 만들지 않는다.
GET /history하나뿐이다. 문제집 목록 API 가 없어서, 풀이 기록이 없는 문제집은목록에 나타나지 않는다. 유형별로 3개를 만들고 하나만 풀면 나머지 2개가 영영 접근 불가가 된다는 뜻이다.
file_url은 이미NOT NULL DEFAULT '',session_id는 UNIQUE 다. 자료 없는 세트를 만들기 위해스키마를 고칠 필요가 없고,
session_idUNIQUE 는 오히려 멱등성 열쇠로 재활용할 수 있다.변경 상세
1. 신규 엔드포인트
POST /problem-set/wrong-answers— 인증 필요,@RateLimit(WRITE).요청
{ "folderId": "8Owg7XmO" }Idempotency-Key는 한 번의 사용자 조작을 가리킨다. 이 값으로 세트의session_id를 결정론적으로 만들어UNIQUE 제약에 태우므로, 연타·새로고침 재시도·다중 탭이 목록을 어지럽히지 않는다.
응답 (200)
{ "createdSets": [ { "problemSetId": "1OJ265nO", "historyId": "eVZLpGXM", "quizType": "MULTIPLE", "title": "오답 모음 · 객관식 · 09/09 10:24", "questionCount": 3, "truncated": false }, { "problemSetId": "vO5oK15a", "historyId": "yMkZdGoa", "quizType": "REAL_BLANK", "title": "오답 모음 · 빈칸 직접입력 · 09/09 10:24", "questionCount": 2, "truncated": false }, { "problemSetId": "zO01ZmwV", "historyId": "0M6XwAeV", "quizType": "OX", "title": "오답 모음 · OX 퀴즈 · 09/09 10:24", "questionCount": 2, "truncated": false } ], "excludedEssayCount": 3, "deletedSourceCount": 0, "failedTypes": [], "emptyReason": null }모을 오답이 없거나, 상한에 걸려 일부만 담기거나, 일부 유형이 실패해도 전부 200 이다. 사용자에게 알려야 할
상태이지 요청의 실패가 아니고, 프론트 공통 오류 처리가 401 외 모든 에러에 자동 토스트를 띄우기 때문에
오류로 내리면 상황에 맞는 안내를 할 수 없다. 진짜 오류만 4xx·5xx 로 간다.
emptyReason은 만들어진 문제집이 하나도 없을 때만 채워지고 우선순위는NO_HISTORY>ESSAY_ONLY>SOURCE_DELETED>ALL_CORRECT다.excludedEssayCount는 이와 독립으로 항상 채워, 세트가 만들어진 경우에도 서술형 제외를 안내할 수 있게 한다.2. 생성된 실제 DB ROW
아래는 위 요청 하나로 실제로 저장된 행이다(로컬 격리 DB 실측, 값 가공 없음).
problem_set— 유형별로 3행이 만들어진다wa-02381E17-…-MULTIPLEwa-02381E17-…-REAL_BLANKwa-02381E17-…-OXgeneration_status가 처음부터COMPLETED라 응답 직후 바로 풀 수 있고,file_url은 컬럼 기본값인 빈문자열이라 스키마를 건드리지 않았다.
source_folder_id는 모아온 폴더다.problem— 틀렸던 문항만, 번호만 다시 매겨 복제된다원본 세트 900001(객관식 5문항) 중 사용자가 틀린 것은 1·3·5번이었다. 그 셋만 담겼다.
[][][]다른 두 유형도 같다 — OX 는 원본 900002 의 틀린 2·4번이, 빈칸 직접입력은 원본 900003 의 틀린 1·2번이 담겼다.
[{"content":"정답1","explanation":"빈칸 해설","correct":true,"acceptedAnswers"…[][{"content":"정답2",…[][{"content":"O","explanation":"틀린 설명","correct":false,…[][{"content":"O",…[]selections는 정답 표시·선지별 해설·빈칸 인정 표현까지 원본 그대로 복제된다. 그래서 복제된 빈칸 직접입력세트도 기존 서버 채점(
POST /grade)이 그대로 동작한다.referenced_pages만 비워 저장한다. 해설 화면의 "참조 자료" 패널을 여는 조건이 파일 URL 이 아니라문항별 참조 페이지라, 그대로 복제하면 자료가 없는 문제집에서 패널이 열리며 "파일 링크가 만료되었습니다"라는
사실과 다른 문구가 뜬다(만료된 게 아니라 애초에 자료가 없다). 응답에서 거르지 않고 저장 시점에 비운
이유는, 응답 분기는 새 소비자가 생기면 놓치지만 저장 차단은 모든 경로에 자동으로 적용되기 때문이다.
origin_problem_set_id/origin_number는 최초 조상을 가리킨다. 복제할 때 조상 값을 물려받으므로 세대가거듭돼도 같은 문항이 두 번 담기지 않는다.
quiz_history— 문제집과 함께 미완료 기록이 출처 폴더에 만들어진다이 행이 없으면 만들어진 문제집이 목록에도 폴더에도 나타나지 않는다(위 조사 4). 폴더에 들어가야 다음
모아풀기의 수집 대상이 될 수 있다는 점도 같이 걸린다.
3. 기존 응답 변경 (전부 비파괴)
GET /problem-set/{id}·GET /history목록 항목에origin: "DOCUMENT" | "WRONG_ANSWER"추가.목록에서 오답 문제집을 구별하는 근거이자, 원본 자료를 전제로 하는 진입점을 감추는 근거다.
GET /problem-set/{id}/regeneration-condition은 오답 세트에서documentAvailable: false.404 로 막지 않는다 — 404 면 프론트가 오류 토스트 + 홈 이동이라는 막다른 실패로 흐른다.
GET /history목록의takenAt매핑을 완료 시각으로 옮겼다.-)으로 바뀐다.지금까지 이 칸은 기록이 만들어진 시각을 싣고 있었다. 이번 기능이 미완료 기록을 폴더에 만들면서 아직 풀지도
않은 문제집에 완료일이 찍히는 문제가 생기므로, 우리 변경이 만든 거짓 표시를 함께 치웠다.
완료된 기존 행은 백필 덕에 표시가 그대로다. 프론트는 원래 null 방어가 있어 코드 변경이 없다.
GET /explanation/{id}은 변경 없다. 해설 화면이/problem-set을 이미 병렬로 호출해origin을가지고 있고, 참조 페이지는 저장 시점에 비워지므로 응답을 건드릴 이유가 없다.
4. 마이그레이션 V21
착수 시점에
origin/*전 원격 브랜치의 마이그레이션 파일을 전수 스캔해 최대가 V20 임을 확인하고 골랐다.quiz_history.completed_at이 필요한 이유: 이 테이블은(user_id, problem_set_id)UNIQUE 라 세트당행이 하나이고, 재풀이는 같은 행을 덮어쓰면서도
created_at은 최초 시작 시각으로 남는다. 즉 기존 컬럼으로는"가장 최근에 틀린 순"을 표현할 수 없다. 이 정렬이 상한에서 어떤 문항이 남는지를 결정하므로 부정확한 정렬은
결과 자체를 바꾼다. 세팅 지점은
QuizHistory.completeQuiz()한 곳이다.created_at으로 백필해 화면 표시 회귀를 0으로 만들었다. 정렬은 방어적으로COALESCE(completed_at, created_at)를 쓴다.file_url·session_id는 손대지 않았다.5. 처리 파이프라인
순서가 규칙의 일부다. 중복 제거가 정렬보다 앞서면 같은 문항의 오래된 쪽이 남아 상한에서 최신 오답이
밀려난다. 수렴 과정에서 실제로 잡은 오류라 테스트로 고정했다.
유형 하나 = 트랜잭션 하나(
REQUIRES_NEW)라, 한 유형이 실패해도 이미 만들어진 유형은 살아남고 실패한유형은 반쯤 만들어진 채 남지 않는다. 문제집·문항·풀이 기록이 한 덩어리로 커밋된다.
서술형은 수집하지 않는다. 이 유형은 정오답이 규칙으로 갈리지 않고 AI 채점 점수만 있어서, "몇 개 틀렸다"를
세려면 없던 임계값을 정해야 한다. 그건 이번 범위가 금지한 새 채점 규칙이므로 "제외된 문항 수"만 알린다.
6. 판정기 추출
문항별 정오답 판정이 기록 상세 매퍼 안의 private 메서드로만 있었다. 수집이 같은 규칙을 써야 하므로
AnswerJudge로 꺼내 두 경로가 한 함수를 거치게 했다. 동작은 그대로이고, 두 경로가 서로 다른 규칙으로갈라지는 것을 막는 것이 목적이다.
테스트
./gradlew test전체 통과(기존 스위트 회귀 없음). 새 테스트 27개 추가.WrongAnswerCollectorTestWrongAnswerSetServiceImplTestWrongAnswerSetCreationServiceImplTest(JPA)문제집당 상한·"가장 최근에 틀린 순"·중복 제거의 증명 책임은 계약상 전적으로 백엔드 테스트에 있다
(기능 E2E 는 100문항 시드를 만들지 않는다).
기능 E2E
통과했다 — 프론트 러너 기준 9/9, 마커
RESULT=PASS/EXIT=0/2026-09-09 10:52:58.시드(
scripts/e2e/seed-wrong-answer-set.sql)는 한 폴더에 유형이 다른 문제집 4개와 "일부만 틀린" 기록을 넣고,폴더 밖 문제집과 타인 소유 문제집을 함정으로 함께 심는다. 둘 다 오답이 4개씩이라, 수집 범위가 새면
객관식 문항 수가 3이 아니라 7로 나와 범위 오염이 숫자 하나로 드러난다.
로컬 실행으로 확인한 것:
Idempotency-Key로 두 번 호출해도 응답이 동일하고 문제집이 새로 만들어지지 않음그대로다. 2세대 문항의 혈통이 전부 최초 조상을 가리켜 중복이 생기지 않는다. 아직 풀지 않은 오답 문제집은
미완료라 수집 대상이 아니므로 자기 복제도 일어나지 않는다.
반복 실행에서도 결과가 흔들리지 않는다. 프론트 E2E 스위트를 여러 번 돌려 같은 폴더에 오답 문제집이 30개
넘게 쌓이고 그중 일부가 실제로 풀려 완료된 상태에서도, 수집 결과가 계속
객관식 3 / 빈칸 직접입력 2 / OX 2,서술형 3문항 제외로 유지됐다. 만들어진 오답 문제집이 폴더에 그대로 남는데도 결과가 변하지 않는 이유는 두
가지가 맞물려서다 — 아직 풀지 않은 것은 미완료라 수집 대상이 아니고, 풀어서 또 틀린 문항은 혈통이 최초
조상을 가리켜 원본과 중복 제거된다. 세대가 거듭돼도 목록이 오염되거나 문항이 중복되지 않는다는 것을 단위
테스트 밖에서 확인한 셈이다.
또한 프론트 시나리오가 오답 문제집을 풀어 제출하는 경로에서, 해당 기록에
status=COMPLETED와 함께completed_at이 실제로 찍히는 것을 확인했다 — 새 컬럼이 실 흐름에서 채워진다는 증거다.관련 제품 결정
contract.md §7.0의 사용자 결정을 따랐다.빈칸 넣기/빈칸 직접입력).알려진 동작(결함이 아니라 합의된 것): 오답 문제집에서 다시 맞힌 문항이라도 원본 기록은 그 문항을 오답으로
유지하므로 다음 모아풀기에 계속 수집된다. 제외하려면 새 규칙이 필요해 이번 범위에서 다루지 않기로 결정됐다.
작업 브랜치
ICC-419(base:develop)🤖 Generated with Claude Code