Skip to content

feat: 같은 조건으로 문제 이어서 더 풀기 — 세트 생성조건 되돌리기 (feedback #7) - #399

Closed
GulSauce wants to merge 9 commits into
developfrom
feature/feedback7-repeat-gen
Closed

GulSauce wants to merge 9 commits into
developfrom
feature/feedback7-repeat-gen

Conversation

@GulSauce

Copy link
Copy Markdown
Member

feat: 같은 조건으로 문제 이어서 더 풀기 — 세트 생성조건 되돌리기 (feedback #7 / api)

  • 브랜치: feature/feedback7-repeat-gen (base: develop)
  • 대상 레포: api
  • 관련 명세: specs/002-repeat-quiz-generation/spec.md, 팀 계약 contract.md

요약

방금 푼 문제 세트와 같은 조건으로 문제를 이어서 더 생성(동일 재현)할 수 있도록, 세트의 생성 조건을
서버가 되돌려 주는 통로를 신설한다. 재현에 필요한 조건 중 pageNumbers·language는 지금까지 생성 시
소비 후 폐기되어 DB에도 조회 응답에도 남지 않았다(backend-context.md). 이 둘을 세트 단위로 저장하고,
전용 조회 엔드포인트로 반환한다.

즉시 재생성(조건 온전) vs 옵션 화면 폴백(조건 소실)의 분기 판정은 프론트가 반환 필드로 수행한다
(documentAvailable && pageNumbers?.length && language). 서버는 집계 플래그를 두지 않아 계약이 얇다.
재생성 실행은 기존 POST /generation 재요청을 그대로 재사용하므로 생성 파이프라인은 무수정이다.

변경 상세

1. 마이그레이션 V19 — problem_set 컬럼 2개 추가

app/src/main/resources/db/migration/V19__add_page_numbers_and_language_to_problem_set.sql

ALTER TABLE problem_set
    ADD COLUMN page_numbers TEXT NULL,
    ADD COLUMN language VARCHAR(8) NULL;
  • 둘 다 nullable — V19 이전 legacy 세트는 NULL로 남아 프론트 폴백으로 자연 유입된다(별도 백필 불필요).
  • customInstruction은 이미 저장 컬럼이 있어(V8) 마이그레이션 불필요 — 응답 노출만 신규.

⚠️ 마이그레이션 번호 주의 (머지 시 재확인) — 계약 §7.3. 작업 브랜치 base(develop HEAD)의
커밋된 마이그레이션 최댓값은 V16이다(V17·V18은 develop에 미커밋 상태였음). 이 브랜치는 V19로 넣어
V16→V19 간극이 있다. Flyway 적용 자체는 막히지 않으나, 머지 시점에 V17·V18이 develop에 먼저
랜딩하지 않으면 간극이 남으므로 규약상 실제 최신 번호로 리넘버링을 검토한다.

2. ProblemSet 엔티티 — 필드 2개 추가

modules/quiz-set/impl/.../entity/ProblemSet.java

// 동일 재현용 생성 조건 — 생성 시점에만 알 수 있어 세트에 함께 저장한다.
// V19 이전 legacy 세트는 null. IntegerListConverter는 NULL을 빈 리스트로 읽는다.
@Convert(converter = IntegerListConverter.class)
@Column(columnDefinition = "TEXT")
private List<Integer> pageNumbers;

// 요청 Language enum(quiz-make 모듈)을 이 모듈이 의존하지 않아 문자열("KO"/"EN")로 저장한다.
@Column(length = 8)
private String language;
  • pageNumbers는 기존 IntegerListConverter(JSON 배열 문자열 [1,2,3]) 재사용.
  • languageString으로 두는 이유: quiz-set 모듈이 Language enum이 있는 quiz-make 모듈을
    의존하지 않는다(모듈 경계). 저장·반환·재요청 전 구간 대문자(KO/EN) 일관.

3. initProblemSet 시그니처 확장 — 생성 시 두 조건 저장

modules/quiz-set/api/.../QuizCommandService.java (인터페이스)

Long initProblemSet(
    ... String customInstruction,
    List<Integer> pageNumbers,   // 신규
    String language);            // 신규

modules/quiz-make/impl/.../GenerationCommandServiceImpl.java (호출부, triggerGeneration)

quizCommandService.initProblemSet(
    ... request.customInstruction(),
    request.pageNumbers(),
    request.language().name());  // enum → 대문자 문자열

4. 신규 조회 엔드포인트 — GET /problem-set/{id}/regeneration-condition

modules/quiz-set/impl/.../controller/ProblemSetQueryController.java
ProblemSetServiceImpl.getRegenerationConditionProblemSetResponseMapper.toRegenerationCondition

  • id: Hashids 인코딩. decode 실패 시 INVALID_URL_REQUEST(400).
  • 소유권 검증 없음 — 현행 공개 조회(GET /problem-set/{id}) 정책과 일관(사용자 결정 ①).
    익명 생성 세트도 재생성 가능. 노출은 on-demand·전용 엔드포인트로 최소화.
  • 매퍼는 pageNumbers가 null/빈 리스트면 응답에 null, language가 공백/없으면 null로 내려
    legacy 세트를 자연히 폴백으로 유도한다. documentAvailable은 현 단계 항상 true(결정 ②).

응답 스키마

200 GET /problem-set/{id}/regeneration-condition
{
  quizType: MULTIPLE|BLANK|REAL_BLANK|OX|ESSAY,
  quizCount: number,                 // = ProblemSet.totalQuizCount
  pageNumbers: number[] | null,      // 신규 저장 컬럼, legacy 세트는 null
  language: 'KO' | 'EN' | null,      // 신규 저장 컬럼(String), legacy 세트는 null
  customInstruction: string | null,  // 기존 저장 컬럼(응답 노출만 신규)
  uploadedUrl: string,               // = fileUrl. non-null (문서 CDN URL)
  title: string,                     // POST /generation 재요청 payload 자체완결성
  documentAvailable: boolean         // 서버 소유 판정. 현 단계 항상 true, 향후 문서 HEAD/TTL 훅 자리
}

테스트 (편집 모듈 그린)

./gradlew :quiz-set-impl:test :quiz-make-impl:test --rerun-tasksBUILD SUCCESSFUL (failures=0, errors=0)

테스트 tests 비고
ProblemSetResponseMapperTest 5 toRegenerationCondition 매핑(값 있음 / legacy null 폴백)
ProblemSetServiceImplTest 5 getRegenerationCondition(인코딩 decode·조회)
GenerationCommandServiceImplTest 12 initProblemSet 확장 인자 전달(pageNumbers·language().name())

실백엔드 통합 검증 (PHASE 3, SPRING_PROFILES_ACTIVE=local, 포트 8080)

로컬 백엔드 기동(DB=MySQL, Flyway V19 적용) 후 실 엔드포인트를 curl로 검증. seed 세트
nYayjMZE(problem_set id=5, page_numbers=[1,2,3], language=KO).

  1. 인증 O → 200 (정상 반환)
    GET /problem-set/nYayjMZE/regeneration-condition  (Authorization: Bearer <jwt>)
    → 200
    {"quizType":"MULTIPLE","quizCount":10,"pageNumbers":[1,2,3],"language":"KO",
     "customInstruction":null,"uploadedUrl":"https://files.q-asker.com/...pdf",
     "title":"08. 프록시와 연관관계 관리.pdf","documentAvailable":true}
    
  2. 무인증 → 200 — 소유권/인증 검증이 없음을 확인(결정 ①).
  3. 잘못된 인코딩 id → 400GET /problem-set/ZZZZZZ99/...INVALID_URL_REQUEST.
  4. legacy 세트 → null 폴백odOAlMbm(id=3, 두 컬럼 미저장)
    {"quizType":"MULTIPLE","quizCount":25,"pageNumbers":null,"language":null,...,"documentAvailable":true}
    → 프론트 판정 false → 옵션 화면 폴백 경로.
  5. language 대문자 반환 — seed 세트 하나가 소문자 ko로 오염돼 있어 KO로 교정(코드 결함 아님,
    시드 오염 1건). POST /generation의 Language enum 역직렬화는 case-sensitive이므로 저장·반환·재요청
    전 구간 대문자 일관이 필요하다(계약 §7.4-note).

프론트(client) 측 실통합 E2E도 통과: 실 regeneration-condition(documentAvailable:true·pageNumbers[1,2,3]·language KO)
→ 즉시생성 분기 → 홈 직행 → 실 POST /generation 트리거 관측(frontend 커밋 18b6f02).

사용자 결정 2건 (2026-07-22)

  • ① 소유권 검증: 새 엔드포인트에 하드 소유권 검증을 걸지 않는다 — 현행 공개 조회 정책과 일관,
    익명 생성 세트도 재생성 가능.
  • ② 문서 만료검사: 능동 만료검사 미도입 — 후속 과제. api에 TTL/만료 코드가 전무해
    documentAvailable은 현 단계 항상 true이고, 실제 문서 만료는 재생성 후 SSE 에러로만 드러난다.

스코프 밖 (후속 과제)

  • 유사 변주(앞 문항과 겹치지 않는 새 문제) — 이번 스코프는 동일 재현만.
  • 문서 만료 능동검사(documentAvailable 실제 판정 훅).
  • 재생성 세트 lineage 컬럼 — 새 sessionId로 자연히 별도 세트가 되어 미도입.

🤖 Generated with Claude Code

GulSauce and others added 9 commits July 22, 2026 01:50
- build.gradle: quiz-set-impl 인핸스먼트 게이트를 enableHibernateEnhancement →
  !disableHibernateEnhancement 로 반전(기본 ON). 프로덕션 빌드도 계측 적용됨.
- hibernate-enhancement/run.sh: on=무플래그, off=-PdisableHibernateEnhancement
- query-tuning: run-all(스윕 전 1회)·run-level(단독 호출 시) bootJar 자동 재빌드.
  build/libs 에 남은 이전 모드 jar 를 주워 오염 측정하던 문제 차단(LT_SKIP_BUILD 로 중복 방지)
- LocalTokenController: @Profile("loadtest") → @Profile("local"). 로컬 개발·E2E에서
  OAuth 없이 토큰 발급 가능. prod 미노출은 유지.
- CLAUDE.md: 인증/JWT 작업 가이드 섹션 추가(JwtProvider 단일 진입점, 필터·리졸버·회전),
  JASYPT 키는 app/gradle.properties 자동 주입 안내, 문서 동기화(V16, quiz-history 폴더 분류)
- 미사용 항목 제거(HELP.md, newrelic.yml, monitor_downtime.sh, shrimp_data, .gemini)
- docs 무시, CLAUDE.local.md 추가
- 컨테이너명 local-mysql-x1/x10/x100, 포트 3307/3308/3309 로 정합.
- run-all.sh LEVELS, hibernate-enhancement/run.sh 포트 case+기본(3307), loadtest.yml 분석 DB 3309.
problem 테이블 SQL(lazy explanation 로딩 등)이 컬럼명 problem_set_id 때문에
%problem_set% 폴백에 걸려 ProblemSetRepository 로 오귀속되던 것을,
테이블명 뒤 경계(공백/괄호) 앵커 REGEXP 로 교정. update problem set 도 이스케이프로 차단.
- 시드 단순화: 필드 합성 → 원본 행 복제(제약키 PK/UK/FK만 새로, 내용은 원본 그대로).
- seed-scale 에 초기화 통합: DROP DATABASE + x1 덤프 복원 + 배수 복제 → 재실행해도 항상 fresh x<scale>(누적·중복 없음).
- check-x1-scale: x1×100<10K 도메인 테이블 경고(경고 전용·information_schema 자동 발견). seed-scale 자동 호출.
- x1 세팅 api 이관·분리: download-masked(OCI 마스킹본 다운로드) / restore-x1(파일 복원+FK 정합 검증) 독립. plg 백업 코드 비의존.
- 런북.md 추가(콜드스타트→대시보드 절차) + README 갱신.
- loadtest.yml 분석 DB 3307→3309(컨벤션 통일 반영).
- loadtest 드라이버에 RequestResourceMetricsFilter/Config(uri별 CPU·힙할당 계측) 설명 추가.
- LocalTokenController @Profile("local") 문서 동기화.
findExplanationsBySetId(@EntityGraph)가 @basic(LAZY) @LazyGroup explanation 을
단일 쿼리로 못 당겨 N+1이 잔존함을 trace_snapshot 으로 확인 → findByIdProblemSetId 로 원복.
N+1 자체는 별도 해결(단일 문항 조회 등) 예정.
결과/해설 화면에서 방금 세트와 같은 조건으로 재생성할 수 있도록,
세트의 생성 조건을 되돌려주는 조회 통로와 조건 영속화를 추가한다.

- ProblemSet에 page_numbers·language 저장(마이그레이션 V19, nullable).
  기존 저장되던 quizType·totalQuizCount·customInstruction·fileUrl과 함께
  세트가 자기 생성 조건을 온전히 소유하게 된다. legacy 세트는 두 값 null.
- GET /problem-set/{id}/regeneration-condition 신설. 저장된 조건을 반환하되
  즉시생성/폴백 판정은 프론트가 documentAvailable && pageNumbers && language로
  수행하도록 서버는 조건 값만 되돌려 준다(reproducible 집계 플래그 없음).
  현행 공개 조회 정책과 일관되게 하드 소유권 검증은 걸지 않는다.
- documentAvailable은 문서 유효성 판정 자리로 두되 능동 만료검사는
  이번 스코프 미도입(후속 과제) — 현 단계 항상 true.
- initProblemSet 시그니처를 pageNumbers·language까지 확장, 생성 흐름이 전달.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHtDVmEQtk2i5RJXYgWJnH
@GulSauce GulSauce closed this Jul 29, 2026
@GulSauce
GulSauce deleted the feature/feedback7-repeat-gen branch July 31, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant