diff --git a/app/src/main/resources/config/ai-setting.yml b/app/src/main/resources/config/ai-setting.yml index 561bdcde..5ac68a48 100644 --- a/app/src/main/resources/config/ai-setting.yml +++ b/app/src/main/resources/config/ai-setting.yml @@ -17,3 +17,5 @@ q-asker: chat-timeout-ms: 360000 chunk: max-count-variants: 5 + multiple: + chunk-mode: 1 # 1: 단일 호출, 2: 멀티턴 순차 (A→B→C 중복방지), 3: 패턴별 병렬 (A/B/C) diff --git a/app/src/main/resources/config/resilience.yml b/app/src/main/resources/config/resilience.yml index a1f93e1a..bb102bfd 100644 --- a/app/src/main/resources/config/resilience.yml +++ b/app/src/main/resources/config/resilience.yml @@ -14,6 +14,9 @@ resilience4j: permitted-number-of-calls-in-half-open-state: 2 automatic-transition-from-open-to-half-open-enabled: true event-consumer-buffer-size: 10 + slow-call-duration-threshold: + seconds: 60 + slow-call-rate-threshold: 100 record-exceptions: - com.icc.qasker.ai.exception.GeminiInfraException metrics: diff --git a/modules/document/impl/src/main/java/com/icc/qasker/document/service/NoOpConvertService.java b/modules/document/impl/src/main/java/com/icc/qasker/document/service/NoOpConvertService.java index 5e96a1f2..68ec8f6f 100644 --- a/modules/document/impl/src/main/java/com/icc/qasker/document/service/NoOpConvertService.java +++ b/modules/document/impl/src/main/java/com/icc/qasker/document/service/NoOpConvertService.java @@ -21,7 +21,7 @@ public class NoOpConvertService implements ConvertService { @Override public Path convertToPdf(Path inputFile) { - log.warn("문서 변환 서비스가 비활성화 상태입니다. LibreOffice가 설치되지 않은 환경입니다."); + log.warn("[문서 변환 비활성화] LibreOffice 미설치 환경에서 변환 요청 수신"); throw new UnsupportedOperationException("문서 변환이 비활성화된 환경입니다."); } } diff --git a/modules/global/src/main/java/com/icc/qasker/global/component/HashUtil.java b/modules/global/src/main/java/com/icc/qasker/global/component/HashUtil.java index 872970fd..288bc2fd 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/component/HashUtil.java +++ b/modules/global/src/main/java/com/icc/qasker/global/component/HashUtil.java @@ -24,7 +24,7 @@ public long decode(String hashId) { throw new CustomException(ExceptionMessage.PROBLEM_SET_NOT_FOUND); } if (decoded.length > 1) { - log.error("중복된 ID가 발견되었습니다: {}", hashId); + log.error("[해시 디코딩 이상] 중복된 ID 발견 hashId={}", hashId); } return decoded[decoded.length - 1]; } diff --git a/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java b/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java index e428de6c..0abc1b47 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java +++ b/modules/global/src/main/java/com/icc/qasker/global/component/SlackNotifier.java @@ -50,7 +50,7 @@ private void sendSlack(String webhookUrl, String username, String icon, String t .retrieve() .toBodilessEntity(); } catch (Exception e) { - log.warn("Slack 알림 실패: {}", e.toString()); + log.warn("[Slack 알림 실패] 웹훅 전송 실패", e); } } } diff --git a/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java b/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java index 8a58b747..73130cf4 100644 --- a/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java +++ b/modules/global/src/main/java/com/icc/qasker/global/error/GlobalExceptionHandler.java @@ -29,14 +29,14 @@ public ResponseEntity handleCustomException( log.warn( "[{}] {}", customException.getContext(), customException.getMessage(), customException); } else { - log.warn(customException.getMessage(), customException); + log.warn("[클라이언트 오류] 요청 처리 중 예외 발생", customException); } } else { if (customException.getContext() != null) { log.error( "[{}] {}", customException.getContext(), customException.getMessage(), customException); } else { - log.error(customException.getMessage(), customException); + log.error("[서버 오류] 처리 중 예외 발생", customException); } } @@ -47,7 +47,7 @@ public ResponseEntity handleCustomException( @ExceptionHandler(CallNotPermittedException.class) public ResponseEntity handleCustomException( CallNotPermittedException exception) { - log.error("Circuit Breaker Activated", exception); + log.error("[서킷 브레이커] AI 서버 차단 상태", exception); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new CustomErrorResponse(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR.getMessage())); @@ -56,7 +56,7 @@ public ResponseEntity handleCustomException( @ExceptionHandler(MaxUploadSizeExceededException.class) public ResponseEntity handleMaxUploadSizeExceededException( MaxUploadSizeExceededException e) { - log.warn("파일 업로드 크기 초과: {}", e.getMessage()); + log.warn("[파일 업로드 제한] 업로드 크기 초과", e); return ResponseEntity.status(ExceptionMessage.FILE_SIZE_EXCEEDED.getHttpStatus()) .body(new CustomErrorResponse(ExceptionMessage.FILE_SIZE_EXCEEDED.getMessage())); } @@ -66,14 +66,14 @@ public void handleSseException() {} @ExceptionHandler(AsyncRequestTimeoutException.class) public ResponseEntity handleAsyncRequestTimeoutException() { - log.warn("SSE 비동기 요청 타임아웃 발생"); + log.warn("[SSE 타임아웃] 비동기 요청 타임아웃 발생"); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(new CustomErrorResponse(ExceptionMessage.AI_SERVER_COMMUNICATION_ERROR.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity handleCustomException(Exception exception) { - log.error("Unexpected Error Occurred", exception); + log.error("[예상치 못한 오류] 처리되지 않은 예외 발생", exception); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new CustomErrorResponse(ExceptionMessage.DEFAULT_ERROR.getMessage())); diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/BlankQuizOrchestrator.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/BlankQuizOrchestrator.java index 5d038f05..96c1dd28 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/BlankQuizOrchestrator.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/BlankQuizOrchestrator.java @@ -175,7 +175,7 @@ public int generateQuiz(GenerationRequestToAI request) { if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) { throw new GeminiInfraException("Gemini 블로킹 컨텍스트 오류", e); } - log.warn("BLANK 스트리밍 타임아웃 (6분 초과): 생성된 문항 {}개 유지", delivered.get()); + log.warn("[BLANK 스트리밍 타임아웃] 6분 초과, 생성된 문항 유지 deliveredCount={}", delivered.get()); metricsRecorder.recordStreamingTimeout("BLANK"); metricsRecorder.recordRequestDuration( 1, @@ -188,7 +188,7 @@ public int generateQuiz(GenerationRequestToAI request) { throw e; } catch (Exception e) { if (delivered.get() > 0) { - log.warn("BLANK 스트리밍 중 오류 발생이나 {}개 문항은 전달됨. 부분 성공 처리.", delivered.get(), e); + log.warn("[BLANK 부분 성공] 스트리밍 중 오류 발생이나 문항 전달됨 deliveredCount={}", delivered.get(), e); metricsRecorder.recordStreamingTimeout("BLANK"); metricsRecorder.recordRequestDuration( 1, diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankGuideLine.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankGuideLine.java index 3ace7000..d6a9789e 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankGuideLine.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankGuideLine.java @@ -8,157 +8,163 @@ public class BlankGuideLine { public static final String content = """ - > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용하세요. - - # 역할 - 당신은 교육측정학 기반의 빈칸채우기 문항 설계 전문가입니다. - **Remember 수준**과 **Understand 수준** 문항을 혼합하여 출제하세요. - - # Step 1 — 강의노트에서 출제 소재를 추출한다 - - **Remember용**: 강의노트에 실제 등장하는 명사(개념명, 용어, 명칭, 고유명사) - - **Understand용**: 비교(Comparing)·분류(Classifying)·추론(Inferring)이 가능한 개념 쌍 - - # Step 2 — 생성할 문제에 따라 적절한 전략을 선택한다 - - **다양성 지시**: 정의·명칭·비교·원인·분류 유형을 골고루 포함하세요. 한 유형에 50% 이상 편중 금지. - - **주제 중복 금지**: 같은 정답 용어가 2회 이상 등장하면 안 됩니다. - - **공통 제약** - - **핵심 개념**은 굵게, `기술 용어`는 코드로 강조하세요. - - 빈칸 정답은 반드시 **명사**여야 합니다. - - ## 1. Remember 수준: 강의노트에 등장한 명사 개념·용어를 재인하는 수준 - ### 패턴 A: 정의형 (개념의 정의·특성을 통해 명칭 재인) - **서술문 전략** - - 정의, 기능, 주요 특성을 서술하고 해당 명칭을 빈칸으로 처리한다 - - 정답 용어 자체가 서술문에 등장하지 않도록 한다 - **서술문 템플릿** - - "~하는 방식을 _______라고 한다." - - "~한 특성을 가지는 [프로토콜/알고리즘/구조]은 _______이다." - **서술문 예시** - - content: **전송 계층** 프로토콜 중, 비연결형으로 동작하며 `handshake` 없이 즉시 데이터를 전송하는 프로토콜은 _______이다. - - 정답: UDP - - 오답 예시: TCP (유사 개념형), ICMP (혼동 유발형), SCTP (유사 개념형) - - ### 패턴 B: 명칭형 (고유명사·사례명을 간접 단서로 재인) - **서술문 전략** - - 이름·약어·역사적 맥락·발명 배경 등을 단서로 제공한다 - - 정의를 직접 노출하지 않고 간접 단서로 유도한다 - **서술문 템플릿** - - "~으로 알려진 [알고리즘/프로토콜/구조]의 정식 명칭은 _______이다." - - "~목적으로 등장한 방식의 명칭은 _______이다." - **서술문 예시** - - content: **운영체제**에서 현재 실행 중인 프로세스의 레지스터·PC·스택 정보를 저장하고 복원하는 자료구조는 _______이다. - - 정답: PCB - - 오답 예시: TCB (유사 개념형), 스택 프레임 (혼동 유발형), 페이지 테이블 (혼동 유발형) - - ## 2. Understand 수준: 비교·원인·분류 문맥에서 정답만 유일하게 추론하는 수준 - - 간접적 단서 2개를 포함하여 정답만 유일하게 적합하도록 설계하세요. - - 빈칸 주변에 정답의 정의를 그대로 노출하지 마세요. 상위 범주·특징·대비 개념을 통해 추론하도록 설계하세요. - - 반드시 '~와 달리', '~에 비해', '~때문에' 등 비교/인과/분류 문맥을 포함해야 합니다. - - **서술문 앞에 "Understand:" 같은 접두사를 붙이지 마세요.** 학습자에게 보이는 문장입니다. - - ### 패턴 C: 비교형 (대비 관계를 통해 개념 추론) - **서술문 전략** - - 두 개념 중 하나를 명시하고, 대비를 통해 나머지를 빈칸으로 유도한다 - - '~와 달리', '~에 비해' 같은 비교 문맥 필수 - **서술문 템플릿** - - "**A**와 달리 _______는 ~한 특징을 가진다." - - "**A**가 ~하는 반면, _______는 ~하지 않는다." - **서술문 예시** - - content: **TCP**와 달리 _______는 `handshake` 없이 전송하여 지연이 낮지만 전달 보장이 없다. - - 정답: UDP - - 오답 예시: TCP (혼동 유발형 — 비교 대상 자체), SCTP (유사 개념형), QUIC (혼동 유발형) - - ### 패턴 D: 원인형 (인과 관계를 통해 개념 추론) - **서술문 전략** - - 원인·조건을 서술하고, 그로 인해 발생하는 결과의 원리·메커니즘 명칭을 빈칸으로 처리한다 - - '~때문에', '~로 인해' 같은 인과 문맥 필수 - **서술문 템플릿** - - "~한 조건 때문에 발생하는 [현상/메커니즘]을 _______라고 한다." - - "~를 유발하는 원리를 _______라고 한다." - **서술문 예시** - - content: **TCP**에서 수신 측 버퍼가 가득 찼을 때 송신 측이 전송량을 줄이는 메커니즘을 _______라고 한다. - - 정답: 흐름 제어 - - 오답 예시: 혼잡 제어 (유사 개념형), 오류 제어 (유사 개념형), 재전송 (혼동 유발형) - - ### 패턴 E: 분류형 (상위·하위 범주 관계를 통해 개념 추론) - **서술문 전략** - - 상위 범주와 하위 특성을 함께 제시하여 특정 하위 개념을 빈칸으로 유도한다 - - 같은 범주 내 다른 개념들과의 차이점을 단서로 활용한다 - **서술문 템플릿** - - "~범주에 속하는 개념 중 ~한 특성을 가지는 것을 _______라고 한다." - - "**A** 유형 중 ~조건을 만족하는 방식은 _______이다." - **서술문 예시** - - content: **스케줄링 알고리즘** 유형 중 실행 중인 프로세스를 강제로 중단하고 다른 프로세스에 CPU를 넘길 수 있는 방식은 _______이다. - - 정답: 선점형 스케줄링 - - 오답 예시: 비선점형 스케줄링 (유사 개념형), FCFS (혼동 유발형), 라운드 로빈 (유사 개념형) - - # Step 3 — 선택지 4개를 작성한다 (정답 1개 + 오답 3개) - **오답 제작 메커니즘 (핵심 규칙)**: 오답은 **강의노트에 실제로 등장하는 명사 용어**이어야 합니다. 강의노트에 없는 합성어·조어 금지. - 정답을 먼저 확정한 뒤, 오답 생성 시에는 아래 유형 중 적절한 것을 적용하세요. - - **유사 개념형**: 정답과 **같은 단원·범주**에 속하지만 기능·특성이 다른 용어. - - 허용 범위: 정답이 속한 분류 계층 내에서만 선택. 다른 챕터 용어 금지. - - 타겟 오개념: "같은 범주의 개념이니 이 문맥에도 들어갈 것이다" - - 예시 — 정답: "UDP", 오답: "TCP" ← 동일 전송 계층 프로토콜 범주, 기능이 다름 - - **혼동 유발형**: 정답과 강의에서 쌍으로 자주 언급되거나 맥락상 혼동하기 쉬운 용어. - - Remember 문항: 발음·약어·번역이 유사한 용어 - - 예시 — 정답: "캐시 히트", 오답: "캐시 미스" ← 같은 문단에서 쌍으로 등장 - - Understand 문항: 서술문에서 **비교 대상으로 등장한 용어 자체** - - 예시 — "TCP와 달리 _______는 handshake 없이 전송한다." → 오답: "TCP" - - 타겟 오개념: "비교 대상으로 언급된 개념이 빈칸에 들어갈 것이다" - **글자 수 균등**: 4개 선택지의 글자 수 편차를 가장 짧은 것 기준 **±2자 이내**로 맞추세요. 정답이 가장 길면 안 됩니다. - - # Step 4 — 해설을 작성한다 - **bloomsLevel**: `"수준 — 유형: [설명]"` 형식으로 기입하세요. - - Bloom's 수준(`"Remember"` 또는 `"Understand"`)과 Step 2에서 선택한 패턴, 그리고 **이 문항이 무엇을 측정하는지 한 문장**을 함께 작성합니다. - - Remember: 정의형 / 명칭형 - - Understand: 비교형 / 원인형 / 분류형 - - 예시: `"Remember — 정의형: 전송 계층에서 비연결형으로 동작하는 프로토콜 명칭을 재인하는 문항"` - - **정답 선택지 해설 (correct: true)**: - - `**정답 추론**`: 정답이 되는 이유를 단계적으로 서술 - - `**근거**`: 근거들을 페이지 번호와 함께 인용함 - - 구조: [Np] > "강의노트 원문 인용"\\n[Np] > "강의노트 원문 인용", ... - - `**스스로 점검**` (핵심 판단 주제): 이 문제의 판단 과정에서 놓치기 쉬운 사고 패턴을 질문 형태로 1개 제시 - **오답 선택지 해설 (correct: false)**: - - `오답 유형`: [유사 개념형 / 혼동 유발형] 중 하나 - - `진단`: 이 선택지를 고르는 학습자가 가진 오개념 한 줄 명시 - - `교정`: 정답과의 핵심 차이로 바로잡기 - - `스스로 점검`: 해당 오개념을 유발하는 사고 패턴을 질문 형태로 제시 - - --- - - # 완성본 예시 - ```json - { - "questions": [{ - "content": "**전송 계층** 프로토콜 중, 비연결형으로 동작하며 `handshake` 없이 즉시 데이터를 전송하는 프로토콜은 _______이다.", - "referencedPages": [12], - "bloomsLevel": "Remember — 정의형: 전송 계층에서 비연결형으로 동작하는 프로토콜 명칭을 재인하는 문항", - "selections": [ - { - "content": "UDP", - "correct": true, - "explanation": "- **정답 추론**: UDP는 비연결형 프로토콜로, 연결 설정(handshake) 없이 즉시 데이터를 전송합니다. 신뢰성보다 속도를 우선하는 경우에 사용됩니다.\\n- **근거**: [전송 계층 12p] > \\"UDP는 비연결형 프로토콜로 handshake 없이 즉시 전송한다\\"\\n- **스스로 점검**: 비연결형이라는 조건을 만족하는지 확인하지 않고 전송 계층 프로토콜이라는 범주만 보고 고르지 않았나요?" - }, - { - "content": "TCP", - "correct": false, - "explanation": "- **오답 유형**: [유사 개념형]\\n- **진단**: 전송 계층 프로토콜이므로 범주는 같지만, TCP는 3-way handshake로 연결을 설정하므로 '비연결형' 문맥에 부적합\\n- **교정**: TCP는 연결형 프로토콜로, 데이터 전송 전 반드시 handshake 과정을 거침\\n- **스스로 점검**: '전송 계층 프로토콜'이라는 범주만 보고 서술의 세부 조건(비연결형)을 확인하지 않은 것은 아닌가요?" - }, - { - "content": "ICMP", - "correct": false, - "explanation": "- **오답 유형**: [혼동 유발형]\\n- **진단**: 강의노트에서 UDP와 같은 문단에 자주 등장하여 혼동하기 쉬우나, 네트워크 계층의 오류 보고 프로토콜\\n- **교정**: ICMP는 데이터 전송이 아닌 오류 알림 목적이며, 전송 계층 프로토콜이 아님\\n- **스스로 점검**: 같은 문단에 함께 등장했다는 이유만으로 같은 역할을 한다고 판단한 것은 아닌가요?" - }, - { - "content": "SCTP", - "correct": false, - "explanation": "- **오답 유형**: [유사 개념형]\\n- **진단**: 전송 계층 프로토콜이지만, SCTP는 4-way handshake로 연결을 설정하므로 '비연결형' 문맥에 부적합\\n- **교정**: SCTP는 연결 지향형으로, handshake 없이 전송한다는 서술의 조건을 충족하지 못함\\n- **스스로 점검**: 낯선 용어라서 정답 대신 소거하지 않았나요? SCTP가 연결형인지 비연결형인지 근거를 확인했나요?" - } - ] - }] - } - ``` - """; + > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용한다. + > **CRITICAL RULE**: 빈칸 정답은 반드시 **명사**여야 한다. 같은 정답 용어가 2회 이상 등장하면 안 된다. + + # 역할 + 당신은 Bloom's Revised Taxonomy 기반 빈칸채우기 문항 설계 전문가다. + **Remember 수준**과 **Understand 수준** 문항을 혼합하여 출제한다. + + # Step 1 — 강의노트에서 출제 소재를 추출하고, 각 소재의 지식 유형을 판별한다 + | 지식 유형 | 판별 기준 | 소재 예시 | + |----------|----------|----------| + | 사실적 | 수치·날짜·명칭 등 검증 가능한 정보 | 프로토콜 이름, 역사적 인물, 영양소 명칭 | + | 개념적 | 원리·이론·분류 체계 간의 관계 | 경제 원리 비교, 세포 구조 대조, 프로토콜 계층 분류 | + + # Step 2 — 지식 유형과 인지 수준에 맞는 패턴을 선택한다 + | 인지 수준 | 패턴 | 적합한 지식 유형 | + |----------|------|----------------| + | Remember | A (정의형) | 사실적, 개념적 | + | Remember | B (명칭형) | 사실적 | + | Remember | C (특성형) | 개념적, 사실적 | + | Understand | D (비교형) | 개념적 | + | Understand | E (원인형) | 개념적 | + | Understand | F (분류형) | 개념적, 사실적 | + + ## 1. Remember 수준 + ### 패턴 A: 정의형 — 빈칸 위치: 문장 끝 허용 + + **사실적 지식** + 서술문은 단서를 전반·후반에 분산시키세요. 전반 단서만으로는 유사 개념도 후보가 되고, 후반 단서까지 읽어야 정답이 확정되도록 하세요. + 정답은 정의의 핵심 명사로 하세요. + 오답은 전반 단서에만 해당하는 유사 개념(유사개념형)과 같은 문단에서 함께 등장하여 혼동하는 용어(혼동유발형)로 구성하세요. + - **예시** + - content: **원자 구조**에서 화학적 성질은 동일하지만 질량이 서로 다른 _______은(는) 원자핵 내 중성자 수의 차이에서 비롯되며, 일부는 방사성 붕괴를 일으킨다. + - 정답: 동위원소 + - 오답: 동소체 (유사개념형), 이성질체 (유사개념형), 동족원소 (혼동유발형) + + **개념적 지식** + 서술문은 2중 이상의 단서로 정답을 특정하세요. 각 오답이 단서 중 하나에는 해당하되 다른 단서에서 구별되도록 메커니즘 차이를 활용하세요. + 정답은 개념의 명칭으로 하세요. + 오답은 메커니즘이 유사하지만 핵심 조건이 다른 개념(유사개념형)과 같은 범주에 속하지만 대상이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: 자신이 받아들이기 어려운 감정이나 욕구를 무의식적으로 타인에게 돌려서 해석하는 **방어기제**를 _______이라 한다. + - 정답: 투사 + - 오답: 합리화 (유사개념형), 억압 (유사개념형), 전위 (혼동유발형) + + ### 패턴 B: 명칭형 — 빈칸 위치: 반드시 문장 중간 + + **사실적 지식** + 서술문은 유사 대상과 겹치는 단서(이중막, 자체 DNA 등)를 먼저 제시하고, 정답만의 고유 변별 단서(크리스타, 산화적 인산화 등)를 후반에 배치하세요. 2중 변별 단서로 유사 대상을 배제하세요. + 정답은 고유명사·사례명으로 하세요. + 오답은 겹치는 단서에 해당하는 유사 대상(유사개념형)과 같은 세포 소기관이지만 기능이 다른 대상(혼동유발형)으로 구성하세요. + - **예시** + - content: **진핵세포**에서 _______은(는) 이중막으로 둘러싸여 있고 내막이 크리스타로 접혀 있으며, 자체 DNA를 보유한 채 산화적 인산화를 통해 ATP를 합성한다. + - 정답: 미토콘드리아 + - 오답: 엽록체 (유사개념형), 퍼옥시좀 (혼동유발형), 소포체 (혼동유발형) + + ### 패턴 C: 특성형 — 빈칸 위치: 반드시 문장 중간 + + **개념적 지식** + 서술문은 맥락 단서(직선 충돌→선형 운동량)로 유사 개념(각운동량)을 배제하고, 조건 단서(탄성·비탄성 모두)로 또 다른 유사 개념(운동에너지)을 배제하는 2중 변별 구조로 하세요. + 정답은 고유 속성의 명칭으로 하세요. + 오답은 단서 중 하나에만 해당하는 유사 개념(유사개념형)과 같은 맥락에서 등장하지만 조건이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: 닫힌 계에서 외부 알짜힘이 작용하지 않으면 직선 충돌 전후로 각 물체의 속도가 바뀌더라도 계의 총 _______이(가) 보존되며, 이는 탄성·비탄성 충돌 모두에서 성립한다. + - 정답: 운동량 + - 오답: 운동에너지 (유사개념형), 역학적 에너지 (혼동유발형), 각운동량 (유사개념형) + + **사실적 지식** + 서술문은 3중 이상의 속성 단서를 나열하세요. 정답어가 문맥에 직접 노출되지 않도록 우회 표현을 사용하세요. + 정답은 결합/현상의 명칭으로 하세요. + 오답은 속성 중 일부만 해당하는 유사 개념(유사개념형)과 같은 범주이지만 형성 메커니즘이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: **화학 결합**에서 전기음성도 차이가 큰 두 원자 사이에서 전자가 완전히 이동하여 서로 반대 전하를 띤 입자 간의 정전기적 인력으로 형성되는 결합을 _______결합이라 한다. + - 정답: 이온 + - 오답: 공유 (유사개념형), 금속 (유사개념형), 수소 (혼동유발형) + + ## 2. Understand 수준 + + ### 패턴 D: 비교형 — 빈칸 위치: 반드시 문장 중간 + + **개념적 지식** + 서술문은 비교 대상을 문장에만 등장시키고 오답에는 포함하지 마세요. 3가지 이상의 메커니즘 차이(교차, 독립적 분리, 산물 수 등)로 비교하세요. 비교 대상을 직접 명시하지 않고 메커니즘만으로 정답을 특정하여 쌍 개념 숏컷을 차단하세요. + 정답은 비교 대상과 대비되는 개념의 명칭으로 하세요. + 오답은 같은 상위 범주에 속하는 유사 개념(유사개념형)과 비교 맥락에서 연상되지만 메커니즘이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: **체세포분열**이 유전적으로 동일한 딸세포 2개를 만드는 것과 달리, _______은(는) 교차와 독립적 분리를 거쳐 유전적으로 다양한 생식세포 4개를 생산한다. + - 정답: 감수분열 + - 오답: 접합 (혼동유발형), 다분열 (유사개념형), 무성생식 (혼동유발형) + - **예시** + - content: **학습심리학**에서 유기체의 자발적 행동이 그 결과로 주어지는 강화나 벌에 의해 빈도가 증가하거나 감소하는 과정을 _______이라 한다. + - 정답: 조작적 조건형성 + - 오답: 관찰학습 (유사개념형), 통찰학습 (혼동유발형), 습관화 (혼동유발형) + + ### 패턴 E: 원인형 — 빈칸 위치: 반드시 문장 중간 + + **개념적 지식** + 서술문은 원인→결과→증상의 인과 체인을 구성하세요. 2중 변별 단서(조직 괴사 없이+가역적 등)로 유사 개념(심근경색 등)과 명확히 구분하세요. + 정답은 인과 체인의 결과에 해당하는 명칭으로 하세요. + 오답은 인과 체인의 다른 단계에 해당하거나 변별 단서에서 구별되는 유사 개념(유사개념형)과 같은 기관에서 발생하지만 메커니즘이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: **심장**에서 관상동맥이 죽상경화로 좁아지면 심근에 충분한 산소가 공급되지 못하여 조직 괴사 없이 가역적인 _______이(가) 발생하며, 이때 환자는 흉통과 호흡곤란을 호소한다. + - 정답: 심근허혈 + - 오답: 심근경색 (유사개념형), 부정맥 (혼동유발형), 심부전 (혼동유발형) + + ### 패턴 F: 분류형 — 빈칸 위치: 반드시 문장 중간 + + **개념적 지식** + 서술문은 단서를 전·중·후반에 분산시키세요. 전반 단서만으로는 복수 후보가 남고, 중반 단서로 일부를 배제하고, 후반 단서로 최종 확정하는 구조로 하세요. + 정답은 분류 체계의 명칭으로 하세요. + 오답은 전반 단서에 해당하지만 중반 단서에서 배제되는 유사 개념(유사개념형)과 같은 상위 범주에 속하지만 핵심 속성이 다른 개념(혼동유발형)으로 구성하세요. + - **예시** + - content: **동물 분류**에서 현존하는 종 다양성이 가장 높은 _______문은 체절 구조에 키틴질 외골격을 가지며, 성장 과정에서 주기적으로 탈피한다. + - 정답: 절지동물 + - 오답: 환형동물 (유사개념형), 선형동물 (유사개념형), 극피동물 (혼동유발형) + + **사실적 지식** + 서술문은 3중 이상의 속성으로 분류 체계 내 하나만 특정하세요. 각 속성이 하나의 후보를 배제하는 구조로 하세요. + 정답은 분류 체계의 명칭으로 하세요. + 오답은 속성 중 일부만 충족하는 유사 범주(유사개념형)와 같은 분류 체계에 속하지만 핵심 속성이 다른 범주(혼동유발형)로 구성하세요. + - **예시** + - content: **시장 구조** 중 다수의 판매자가 차별화된 상품을 공급하면서 진입장벽이 낮은 _______시장에서는 단기적으로 초과이윤이 가능하나 장기적으로는 정상이윤만 남는다. + - 정답: 독점적 경쟁 + - 오답: 완전경쟁 (유사개념형), 과점 (유사개념형), 독점 (혼동유발형) + + # Step 3 — 해설을 작성한다 + **bloomsLevel**: `"수준 — 유형: [설명]"` 형식으로 기입한다. + + **정답 선택지 해설 (correct: true)**: + - `**정답 추론**`: 정답이 되는 이유를 단계적으로 서술 + - `**근거**`: 페이지 번호와 함께 인용. 구조: [Np] > "강의노트 원문 인용" + - `**스스로 점검**`: 놓치기 쉬운 사고 패턴을 질문 형태로 1개 제시 + + **오답 선택지 해설 (correct: false)**: + - `오답 유형`: [유사 개념형 / 혼동 유발형] 중 하나 + - `진단`: 이 선택지를 고르는 학습자의 오개념 한 줄 + - `교정`: 정답과의 핵심 차이로 바로잡기 + - `스스로 점검`: 해당 오개념을 유발하는 사고 패턴을 질문 형태로 제시 + + --- + + # 완성본 예시 + ```json + { + "questions": [{ + "content": "**전송 계층** 프로토콜 중, 비연결형으로 동작하며 `handshake` 없이 즉시 데이터를 전송하는 프로토콜은 _______이다.", + "referencedPages": [12], + "bloomsLevel": "Remember — 정의형: 전송 계층에서 비연결형으로 동작하는 프로토콜 명칭을 재인하는 문항", + "selections": [ + {"content": "UDP", "correct": true, "explanation": "- **정답 추론**: UDP는 비연결형 프로토콜로, 연결 설정 없이 즉시 데이터를 전송합니다.\\n- **근거**: [12p] > \\"UDP는 비연결형 프로토콜로 handshake 없이 즉시 전송한다\\"\\n- **스스로 점검**: 비연결형이라는 조건을 만족하는지 확인했나요?"}, + {"content": "TCP", "correct": false, "explanation": "- **오답 유형**: [유사 개념형]\\n- **진단**: 전송 계층 프로토콜이라는 범주만 보고 선택한 오개념\\n- **교정**: TCP는 연결형 프로토콜로 handshake를 거침\\n- **스스로 점검**: 범주만 보고 세부 조건을 확인하지 않은 것은 아닌가요?"}, + {"content": "ICMP", "correct": false, "explanation": "- **오답 유형**: [혼동 유발형]\\n- **진단**: 같은 문단에서 함께 등장하여 혼동한 오개념\\n- **교정**: ICMP는 네트워크 계층의 오류 보고 프로토콜\\n- **스스로 점검**: 같은 문단에 등장했다는 이유로 같은 역할이라고 판단하지 않았나요?"}, + {"content": "SCTP", "correct": false, "explanation": "- **오답 유형**: [유사 개념형]\\n- **진단**: 전송 계층 프로토콜이지만 연결 지향형인 점을 간과한 오개념\\n- **교정**: SCTP는 4-way handshake로 연결을 설정함\\n- **스스로 점검**: SCTP가 연결형인지 비연결형인지 근거를 확인했나요?"} + ] + }] + } + ``` + + > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용한다. + > **CRITICAL RULE**: 빈칸 정답은 반드시 **명사**여야 한다. 같은 정답 용어가 2회 이상 등장하면 안 된다. + """; } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankRequestPrompt.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankRequestPrompt.java index bd519f14..88323670 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankRequestPrompt.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/blank/prompt/BlankRequestPrompt.java @@ -11,8 +11,7 @@ public class BlankRequestPrompt { private static final String APPLIED_INSTRUCTION_SPEC = """ # 사용자 지시 반영 - - 지시가 특정 형식을 요청하면, 그 형식에 대응하는 전략이 존재하면 해당 전략을 우선 선택한다. - - 대응 전략이 없는 형식은 요청 형식에 맞게 질문문을 자유롭게 구성다. + - 사용자 지시에 맞는 패턴과 지식 유형을 Step 1-2의 테이블에서 찾아 해당 few-shot을 따른다. 대응 패턴이 없으면 자유롭게 구성한다. # 사용자 지시 반영 결과 기록 - 사용자 지시를 반영한 내용을 `appliedInstruction` 필드에 1~2문장으로 기록한다. diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayGradingServiceImpl.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayGradingServiceImpl.java index 86c4025a..9e3bdfc5 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayGradingServiceImpl.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayGradingServiceImpl.java @@ -77,7 +77,7 @@ public EssayGradingResult grade( try { pass1 = extractEvidence(question, rubric, studentAnswer); } catch (Exception e) { - log.warn("Pass 1 (증거 추출) 실패, 1-pass fallback 시도", e); + log.warn("[채점 Pass1 실패] 증거 추출 실패, 1-pass fallback 시도", e); return fallbackSinglePass( question, modelAnswer, rubric, studentAnswer, attemptCount, startMs); } @@ -279,7 +279,7 @@ private String serializeEvidence(GeminiEvidenceExtractionResponse evidence) { try { return objectMapper.writeValueAsString(evidence); } catch (JsonProcessingException e) { - log.warn("증거 JSON 직렬화 실패", e); + log.warn("[채점 직렬화 실패] 증거 JSON 직렬화 실패", e); return null; } } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayQuizOrchestrator.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayQuizOrchestrator.java index 7cb75ffd..d77c1673 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayQuizOrchestrator.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/EssayQuizOrchestrator.java @@ -167,7 +167,7 @@ public int generateQuiz(GenerationRequestToAI request) { if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) { throw new GeminiInfraException("Gemini 블로킹 컨텍스트 오류", e); } - log.warn("ESSAY 스트리밍 타임아웃 (6분 초과): 생성된 문항 {}개 유지", delivered.get()); + log.warn("[ESSAY 스트리밍 타임아웃] 6분 초과, 생성된 문항 유지 deliveredCount={}", delivered.get()); metricsRecorder.recordStreamingTimeout("ESSAY"); metricsRecorder.recordRequestDuration( 1, @@ -180,7 +180,7 @@ public int generateQuiz(GenerationRequestToAI request) { throw e; } catch (Exception e) { if (delivered.get() > 0) { - log.warn("ESSAY 스트리밍 중 오류 발생이나 {}개 문항은 전달됨. 부분 성공 처리.", delivered.get(), e); + log.warn("[ESSAY 부분 성공] 스트리밍 중 오류 발생이나 문항 전달됨 deliveredCount={}", delivered.get(), e); metricsRecorder.recordStreamingTimeout("ESSAY"); metricsRecorder.recordRequestDuration( 1, diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGradingGuideLine.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGradingGuideLine.java index 88d28ced..e82d41d9 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGradingGuideLine.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGradingGuideLine.java @@ -27,12 +27,14 @@ public class EssayGradingGuideLine { - 학생의 원문이 핵심 개념을 명시적으로 표현하고 있는지만 판단하세요. ## 2. 점수 부여 - - 각 요소의 만점은 루브릭에 명시된 배점을 따릅니다. + - 각 요소의 만점(maxPoints)은 루브릭 표의 **"[카테고리, N점]"에 명시된 N값을 그대로 사용**합니다. + - 예: "자원 이질성 [사실 확인, 2점]" → maxPoints = 2 + - **maxPoints를 임의로 변경하거나 재해석하지 마세요.** 루브릭의 숫자를 그대로 복사하세요. - **충족**: 해당 요소의 만점을 부여합니다. - **부분 충족**: 해당 요소 만점의 50% (소수점 이하 반올림)를 부여합니다. - **미충족**: 0점을 부여합니다. - totalScore는 각 요소의 earnedPoints 합계입니다. - - maxScore는 각 요소의 maxPoints 합계입니다. + - maxScore는 각 요소의 maxPoints 합계입니다. 루브릭의 배점 합계와 반드시 일치해야 합니다. """; // 1차 시도: 평가기준명 + 점수 + 수행수준만 제공 diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGuideLine.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGuideLine.java index 1e6d5398..9319a806 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGuideLine.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayGuideLine.java @@ -8,141 +8,113 @@ public class EssayGuideLine { public static final String content = """ - > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용하세요. - - # 역할 - 당신은 교육측정학 기반의 서술형 문항 설계 전문가입니다. - **Analyze 수준**과 **Evaluate 수준** 문항을 혼합하여 출제하세요. - - # Step 1 — 강의노트에서 출제 소재를 추출한다 - - **Analyze용**: 구성 요소로 분해 가능한 개념·원리, 비교·대조가 가능한 개념 쌍, 인과 관계가 있는 현상 - - **Evaluate용**: 서로 양립하기 어려운 대안·접근법, 특정 조건에서 판단이 필요한 시나리오, 찬반 논증이 가능한 주장 - - # Step 2 — 생성할 문제에 따라 적절한 전략을 선택한다 - - **다양성 지시**: 설명형·비교분석형·인과추론형·적용판단형·논증형 유형을 골고루 포함하세요. 한 유형에 50% 이상 편중 금지. - - **주제 중복 금지**: 같은 핵심 개념이 2회 이상 출제되면 안 됩니다. - - **공통 제약** - - **핵심 개념**은 굵게, `기술 용어`는 코드로 강조하세요. - - 질문문 끝에 "~를 서술하시오.", "~를 비교하시오.", "~에 대해 논하시오." 등 명확한 지시어를 사용하세요. - - 질문문에 **답안에 포함해야 할 핵심 요소의 개수**를 명시하세요. (예: "3가지 측면에서 서술하시오") - - ## 1. Analyze 수준: 개념·원리를 구성 요소로 분해하고, 요소 간 관계를 파악하는 수준 - - ### 패턴 A: 설명형 (개념·원리의 구성 요소를 분해하여 서술) - **질문문 전략** - - 하나의 개념·원리를 제시하고, 그 구성 요소·단계·메커니즘을 분해하여 서술하도록 요구한다 - - 단순 정의 나열이 아니라, 각 요소가 전체에 기여하는 방식까지 설명하도록 유도한다 - **질문문 템플릿** - - "**A**의 주요 구성 요소를 N가지로 나누고, 각 요소의 역할을 서술하시오." - - "**A** 과정의 각 단계가 수행하는 기능을 설명하시오." - **질문문 예시** - - content: **TCP 3-way handshake**의 각 단계(SYN, SYN-ACK, ACK)가 **신뢰성 있는 연결 설정**에 기여하는 방식을 3가지 측면에서 서술하시오. - - ### 패턴 B: 비교분석형 (두 개념의 공통점·차이점을 기준별 분석) - **질문문 전략** - - 두 개념을 모두 **볼드**로 명시하고, 비교 기준을 제시하여 체계적으로 분석하도록 요구한다 - - 단순 나열이 아니라, 각 기준에서 왜 차이가 발생하는지 원리까지 서술하도록 유도한다 - **질문문 템플릿** - - "**A**와 **B**를 N가지 기준에서 비교하고, 각각 적합한 상황을 서술하시오." - - "**A**와 **B**의 공통점과 차이점을 분석하고, 차이가 발생하는 원인을 서술하시오." - **질문문 예시** - - content: **선점형 스케줄링**과 **비선점형 스케줄링**을 응답 시간, 처리량, 구현 복잡도의 3가지 기준에서 비교하고, 각각 적합한 운영 환경을 서술하시오. - - ### 패턴 C: 인과추론형 (원인-결과 관계를 추적하여 논리 전개) - **질문문 전략** - - 현상·결과를 제시하고, 그 원인을 추적하거나 원인이 결과에 영향을 미치는 경로를 분석하도록 요구한다 - - 인과 관계의 방향성과 매개 요인까지 서술하도록 유도한다 - **질문문 템플릿** - - "**A** 현상이 발생하는 원인을 분석하고, **B**에 미치는 영향을 서술하시오." - - "**A**가 **B**를 유발하는 메커니즘을 단계별로 서술하시오." - **질문문 예시** - - content: **캐시 히트율**이 낮아지는 원인을 3가지 분석하고, 각 원인이 **시스템 전체 성능**에 미치는 영향을 서술하시오. - - ## 2. Evaluate 수준: 기준에 따라 판단하고, 근거를 들어 자신의 입장을 정당화하는 수준 - - ### 패턴 D: 적용판단형 (시나리오에서 최적 방안 선택 + 근거 제시) - **질문문 전략** - - 구체적 시나리오를 제시하고, 복수의 대안 중 최적 방안을 선택하여 근거를 서술하도록 요구한다 - - 판단 기준을 **볼드**로 명시하고, 기준 간 우선순위나 트레이드오프를 고려하도록 유도한다 - **질문문 템플릿** - - "{시나리오}에서 **기준 A**와 **기준 B**를 고려할 때 가장 적합한 방안을 선택하고, 그 근거를 서술하시오." - - "{시나리오}에 적합한 [기술/방법/전략]을 제안하고, 선택의 근거를 서술하시오." - **질문문 예시** - - content: 실시간 채팅 서비스를 설계할 때, **전송 신뢰성**과 **응답 지연 최소화**를 고려하여 가장 적합한 전송 프로토콜을 선택하고, 그 근거를 서술하시오. - - ### 패턴 E: 논증형 (주장에 대한 찬반 입장 + 논거 전개) - **질문문 전략** - - 논쟁 가능한 주장을 제시하고, 찬성 또는 반대 입장을 정하여 논거를 전개하도록 요구한다 - - 강의노트의 내용을 근거로 활용하되, 단순 인용이 아니라 논리적 추론을 통해 입장을 정당화하도록 유도한다 - **질문문 템플릿** - - "**A**가 **B**보다 우월하다는 주장에 대해 찬성 또는 반대 입장을 정하고, 근거를 들어 논하시오." - - "{주장}에 대해 자신의 입장을 밝히고, 강의노트의 내용을 근거로 논거를 전개하시오." - **질문문 예시** - - content: "**마이크로서비스 아키텍처**가 **모놀리식 아키텍처**보다 대규모 시스템에 항상 적합하다"는 주장에 대해 찬성 또는 반대 입장을 정하고, 강의노트의 내용을 근거로 2가지 이상의 논거를 들어 논하시오. - - # Step 3 — 모범답안(modelAnswer)을 작성한다 - **모범답안 작성 규칙** - - 모범답안은 학습자가 도달해야 할 **이상적인 답안**을 완전한 문장으로 서술한다. - - **핵심 요소**(채점 포인트)를 3~5개 포함한다. - - 각 핵심 요소는 강의노트에 근거가 있어야 한다. - - 핵심 요소 간 논리적 흐름(인과, 비교, 분류)을 유지한다. - - 모범답안의 분량은 200~500자 내외로 작성한다. - - **모범 답안 예시** - - modelAnswer: "TCP 3-way handshake의 첫 단계에서 클라이언트가 SYN 패킷을 전송하여 연결 요청을 시작하고, 초기 시퀀스 번호를 설정한다. 서버는 SYN-ACK 패킷으로 응답하여 클라이언트의 요청을 수락하고, 서버 측 시퀀스 번호를 전달한다. 이 단계에서 양방향 통신의 기반이 마련된다. 마지막으로 클라이언트가 ACK 패킷을 전송하여 연결이 완전히 설정되며, 이후 양측이 합의된 시퀀스 번호를 기반으로 신뢰성 있는 데이터 전송을 시작할 수 있다." - - # Step 4 — 해설을 작성한다 - **bloomsLevel**: `"수준 — 유형: [설명]"` 형식으로 기입하세요. - - Bloom's 수준(`"Analyze"` 또는 `"Evaluate"`)과 Step 2에서 선택한 유형, 그리고 **이 문항이 무엇을 측정하는지 한 문장**을 함께 작성합니다. - - Analyze: 설명형 / 비교분석형 / 인과추론형 - - Evaluate: 적용판단형 / 논증형 - - 예시: `"Analyze — 설명형: TCP 3-way handshake의 각 단계별 기능을 분해하여 설명할 수 있는지 측정함"` - - **해설 (explanation) — 채점 기준 표 형식으로 작성**: - 요소별로 개별 점수를 부여하여 학습자에게 상세한 진단 피드백을 제공합니다. - - - `**채점 기준 표**`: 핵심 요소별로 배점·충족 기준·부분 점수 기준을 **표**로 명시 - - 구조 (마크다운 표): - | 요소 | 충족 (N점) | 부분 충족 (N점) | 미충족 (0점) | - |---|---|---|---| - | 요소명 [카테고리, N점] | 충족 조건 기술 | 부분 충족 조건 기술 | 미충족 조건 기술 | - | 요소명 [카테고리, N점] | 충족 조건 기술 | 부분 충족 조건 기술 | 미충족 조건 기술 | - | ... | ... | ... | ... | - - **배점 설계 원칙**: - · **배점 결정 기준 2가지** — 각 요소의 배점은 아래 두 축을 모두 고려하여 결정한다: - (1) **내용 핵심성**: 질문의 핵심 논점에 직접 답하는 요소일수록 높은 배점 - (2) **인지적 난이도**: 단순 사실 확인 < 개념 설명 < 비교·분석 < 인과 추론·판단 순으로 높은 배점 - · **배점 범위**: 요소당 1~5점. 사실 확인 1~2점, 개념 설명 2~3점, 분석·추론·판단 3~5점 - · **균형 제약**: 가장 높은 배점이 가장 낮은 배점의 3배를 초과하지 않는다 - · **부분 충족**: 해당 요소 만점의 50% (소수점 이하 반올림) - · **수준 기술 규칙**: "우수함", "미흡함" 같은 평가적 표현을 사용하지 말고, 학생 답안에서 관찰 가능한 구체적 특성을 기술한다 - - (X) "핵심 개념을 우수하게 서술함" - - (O) "클라이언트의 SYN 전송과 초기 시퀀스 번호 설정을 모두 언급" - · **충족 조건 구체화 규칙**: "논리적으로 서술", "체계적으로 분석" 같은 추상적 동사를 사용하지 말고, 학생 답안에서 확인 가능한 구체적 내용을 기술한다 - - (X) "인과 관계를 논리적으로 연결하여 서술" - - (O) "A가 B를 유발하는 경로(A → C → B)를 명시" - · **부분 충족 긍정 기술**: 부분 충족은 "충족 못 한 것"의 부정형이 아니라, 학생이 실제로 보여줄 수 있는 불완전한 답변의 특성을 기술한다 - - (X) "인과적 연결을 서술하지 않음" - - (O) "결과만 언급하고 원인이나 매개 과정을 생략" - · **내용 우선 원칙**: 표현의 정교함이나 문법이 아닌, 핵심 개념의 정확성과 논리적 연결에 배점을 집중한다 - - `**근거**`: 근거들을 페이지 번호와 함께 인용함 - - 구조: [Np] > "강의노트 원문 인용"\\n[Np] > "강의노트 원문 인용", ... - - `**스스로 점검**`: 이 문제의 서술 과정에서 놓치기 쉬운 사고 패턴을 질문 형태로 1개 제시 - - --- - - # 완성본 예시 - ```json - { - "questions": [{ - "content": "**TCP 3-way handshake**의 각 단계(SYN, SYN-ACK, ACK)가 **신뢰성 있는 연결 설정**에 기여하는 방식을 3가지 측면에서 서술하시오.", - "referencedPages": [8, 12], - "bloomsLevel": "Analyze — 설명형: TCP 3-way handshake의 각 단계별 기능을 분해하여 설명할 수 있는지 측정함", - "modelAnswer": "TCP 3-way handshake의 첫 단계에서 클라이언트가 SYN 패킷을 전송하여 연결 요청을 시작하고, 초기 시퀀스 번호를 설정한다. 서버는 SYN-ACK 패킷으로 응답하여 클라이언트의 요청을 수락하고, 서버 측 시퀀스 번호를 전달한다. 이 단계에서 양방향 통신의 기반이 마련된다. 마지막으로 클라이언트가 ACK 패킷을 전송하여 연결이 완전히 설정되며, 이후 양측이 합의된 시퀀스 번호를 기반으로 신뢰성 있는 데이터 전송을 시작할 수 있다.", - "explanation": "| 요소 | 충족 | 부분 충족 | 미충족 (0점) |\\n|---|---|---|---|\\n| SYN 단계 — 연결 요청과 시퀀스 번호 설정 [사실 확인, 2점] | (2점) 클라이언트의 SYN 전송과 초기 시퀀스 번호 설정을 모두 언급 | (1점) SYN 전송만 언급하고 시퀀스 번호 설정을 누락 | SYN 단계를 언급하지 않음 |\\n| SYN-ACK 단계 — 수락 응답과 양방향 통신 기반 [개념 설명, 3점] | (3점) 서버의 SYN-ACK 응답과 서버 측 시퀀스 번호 전달을 언급하고, 양측 시퀀스 번호 교환이 양방향 통신을 가능하게 하는 원리를 명시 | (2점) 서버 응답은 언급하나 시퀀스 번호 교환과 양방향 통신의 관계를 생략 | SYN-ACK 단계를 언급하지 않음 |\\n| ACK 단계와 신뢰성 확보의 인과적 연결 [분석·추론, 핵심 논점, 4점] | (4점) ACK 전송으로 연결이 완성됨을 언급하고, 시퀀스 번호 합의 → 순서 보장 → 신뢰성 확보의 인과 경로를 명시 | (2점) ACK 전송으로 연결 완성은 언급하나, 시퀀스 번호와 신뢰성 사이의 인과 경로를 생략 | ACK 단계를 언급하지 않음 |\\n\\n- **근거**: [8p] > \\"TCP는 3-way handshake를 통해 연결을 설정한다\\"\\n[12p] > \\"SYN → SYN-ACK → ACK 순서로 양측의 시퀀스 번호를 교환한다\\"\\n- **스스로 점검**: 각 단계의 '동작'만 나열하지 않고, 각 단계가 '신뢰성 확보'에 어떻게 기여하는지까지 서술했나요?" - }] - } - ``` - """; + > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용한다. + > **CRITICAL RULE**: 질문문 끝에 "~를 서술하시오." 등 명확한 지시어와 답안에 포함해야 할 핵심 요소 개수를 명시한다. + + # 역할 + 당신은 Bloom's Revised Taxonomy 기반 서술형 문항 설계 전문가다. + **Analyze 수준**과 **Evaluate 수준** 문항을 혼합하여 출제한다. + + # Step 1 — 강의노트에서 출제 소재를 추출하고, 각 소재의 지식 유형을 판별한다 + | 지식 유형 | 판별 기준 | 소재 예시 | + |----------|----------|----------| + | 사실적 | 수치·날짜·명칭 등 검증 가능한 정보 | 프로토콜 단계, 역사적 사건의 경과, 영양소의 수치 | + | 개념적 | 원리·이론·분류 체계 간의 관계 | 경제 원리 비교, 세포 구조 대조, 스케줄링 방식 차이 | + | 절차적 | 순서가 있는 방법·알고리즘·프로세스 | 실험 절차, 연결 설정 과정, 수식 전개 순서 | + | 메타인지적 | 방법론·모형·분석 도구 자체에 대한 판단 | 모형의 유용성, 방법론의 한계, 접근법 간 트레이드오프 | + + # Step 2 — 지식 유형과 인지 수준에 맞는 패턴을 선택한다 + | 인지 수준 | 패턴 | 적합한 지식 유형 | + |----------|------|----------------| + | Analyze | A (설명형) | 사실적, 절차적 | + | Analyze | B (비교분석형) | 개념적 | + | Analyze | C (인과추론형) | 개념적, 절차적 | + | Evaluate | D (적용판단형) | 개념적, 메타인지적 | + | Evaluate | E (논증형) | 개념적, 메타인지적 | + + ## 1. Analyze 수준 + + ### 패턴 A: 설명형 (구성 요소를 분해하여 서술) + **질문문 예시 — 절차적 지식** (경제학): + - content: **생산가능곡선**이 원점에 대해 오목한 형태를 가지는 이유를 **기회비용 체증의 법칙**과 연결하여 3가지 측면에서 서술하시오. + **질문문 예시 — 사실적 지식** (생물학): + - content: **체세포 분열**의 각 단계(전기, 중기, 후기, 말기)에서 일어나는 핵심 변화를 단계별로 서술하시오. + + ### 패턴 B: 비교분석형 (두 개념의 차이가 만드는 결과·원인까지 분석) + **질문문 예시 — 개념적 지식** (경제학): + - content: **실증경제학**과 **규범경제학**의 차이가 실제 정책 논쟁에서 어떤 결과 차이를 만드는지, 분석 목적과 가치 판단 여부를 기준으로 비교하고, 경제학자들의 의견이 일치하는 영역과 갈리는 영역이 나뉘는 구조적 원인을 서술하시오. + **질문문 예시 — 개념적 지식** (역사): + - content: **프랑스 혁명**과 **미국 독립혁명**이 서로 다른 결과적 체제(공화정 vs 연방제)를 낳은 구조적 원인을 발생 배경과 주도 세력의 차이에서 찾아 3가지 측면에서 분석하시오. + + ### 패턴 C: 인과추론형 (원인-결과 관계를 추적하여 논리 전개) + **질문문 예시 — 개념적 지식** (경제학): + - content: 한 국가가 **자본재 생산 비중**을 높이는 결정이 **미래의 생산가능곡선 이동**에 미치는 영향을 인과적으로 서술하고, 그 과정에서 발생하는 현세대의 비용을 설명하시오. + **질문문 예시 — 절차적 지식** (과학): + - content: **온실가스 농도 증가**가 **지구 평균 기온 상승**을 유발하는 메커니즘을 단계별로 서술하시오. + + ## 2. Evaluate 수준 + + ### 패턴 D: 적용판단형 (시나리오에서 최적 방안 선택 + 근거 제시) + **질문문 예시 — 개념적 지식** (경제학): + - content: 정부가 실업 해소를 위해 공공 근로 사업을 확대하려 한다. **단기 고용 지표 개선**과 **장기 성장 잠재력 유지**를 모두 고려할 때, 생산가능곡선 모델에 근거하여 가장 적합한 정책 방향을 제안하고, 그 근거를 서술하시오. + **질문문 예시 — 메타인지적 지식** (과학 방법론): + - content: 사회과학 연구에서 **실험법**과 **관찰법** 중 적합한 연구 방법을 선택해야 하는 상황을 가정하고, **내적 타당도**와 **외적 타당도**를 기준으로 각 방법의 장단점을 비교하여 연구 설계를 제안하시오. + + ### 패턴 E: 논증형 (주장에 대한 찬반 입장 + 논거 전개) + **질문문 예시 — 개념적 지식** (경제학): + - content: "국제 무역에서 **비교우위에 따른 전문화**는 모든 참여국에게 이득이다"라는 주장에 대해 찬성 또는 반대 입장을 정하고, 강의노트의 내용을 근거로 2가지 이상의 논거를 들어 논하시오. + **질문문 예시 — 메타인지적 지식** (경제학 방법론): + - content: "경제학 모형은 현실을 단순화하므로 정책 결정에 직접 활용하기 어렵다"는 주장에 대해 자신의 입장을 밝히고, **ceteris paribus** 가정의 유용성과 한계를 근거로 논하시오. + + # Step 3 — 질문문을 작성한다 + Step 2의 질문문 예시에서 구조·어조를 그대로 따른다. + + # Step 4 — 모범답안(modelAnswer)을 작성한다 + - 학습자가 도달해야 할 이상적인 답안을 완전한 문장으로 서술한다. + - 핵심 요소(채점 포인트)를 3~5개 포함한다. 각 핵심 요소는 강의노트에 근거가 있어야 한다. + - 분량은 200~500자 내외로 작성한다. + + # Step 5 — 해설을 작성한다 + **bloomsLevel**: `"수준 — 유형: [설명]"` 형식으로 기입한다. + + **해설 (explanation) — 채점 기준 표 형식으로 작성**: + - `**채점 기준 표**`: 핵심 요소별로 배점·충족 기준·부분 점수 기준을 **표**로 명시 + - 구조: + | 요소 | 충족 (N점) | 부분 충족 (N점) | 미충족 (0점) | + |---|---|---|---| + | 요소명 [카테고리, N점] | 충족 조건 | 부분 충족 조건 | 미충족 조건 | + - **배점 원칙**: 요소당 1~5점. 사실 확인 1~2점, 개념 설명 2~3점, 분석·추론·판단 3~5점 + - **부분 충족**: 해당 요소 만점의 50% (반올림) + - **수준 기술**: "우수함/미흡함" 대신 학생 답안에서 관찰 가능한 구체적 특성을 기술한다 + - `**근거**`: 페이지 번호와 함께 인용. 구조: [Np] > "강의노트 원문 인용" + - `**스스로 점검**`: 놓치기 쉬운 사고 패턴을 질문 형태로 1개 제시 + + --- + + # 완성본 예시 1 — Analyze (설명형) + ```json + { + "questions": [{ + "content": "**생산가능곡선**이 원점에 대해 오목한 형태를 가지는 이유를 **기회비용 체증의 법칙**과 연결하여 3가지 측면에서 서술하시오.", + "referencedPages": [8, 12], + "bloomsLevel": "Analyze — 설명형: 생산가능곡선의 형태와 기회비용 체증의 인과관계를 분해하여 설명할 수 있는지 측정함", + "modelAnswer": "생산가능곡선이 원점에 대해 오목한 형태를 보이는 첫 번째 이유는 자원의 이질성이다. 모든 자원이 두 재화 생산에 동일하게 적합하지 않으므로, 한 재화의 생산을 늘릴 때 점점 부적합한 자원이 투입된다. 두 번째로, 이로 인해 추가 1단위 생산에 필요한 기회비용이 체증한다. 세 번째로, 기회비용 체증은 곡선의 기울기가 점점 가팔라지는 것으로 나타나며, 이것이 수학적으로 원점에 대한 오목한 형태를 만들어낸다.", + "explanation": "| 요소 | 충족 | 부분 충족 | 미충족 (0점) |\\n|---|---|---|---|\\n| 자원 이질성 [사실 확인, 2점] | (2점) 자원이 두 재화 생산에 동일하게 적합하지 않다는 점을 언급 | (1점) 자원의 차이를 언급하나 이질성과 생산 적합도를 연결하지 않음 | 자원 이질성을 언급하지 않음 |\\n| 기회비용 체증의 원리 [개념 설명, 3점] | (3점) 부적합한 자원 투입 → 추가 1단위당 포기량 증가의 인과 경로를 명시 | (2점) 기회비용이 증가한다고만 언급하고 원인(자원 부적합성)을 생략 | 기회비용 체증을 언급하지 않음 |\\n| 곡선 형태와 기회비용의 연결 [분석·추론, 4점] | (4점) 기울기 변화 → 오목한 형태라는 수학적 연결을 명시 | (2점) 오목하다는 결과만 언급하고 기울기 변화와의 관계를 생략 | 곡선 형태를 언급하지 않음 |\\n\\n- **근거**: [8p] > \\"자원의 이질성으로 인해 기회비용이 체증한다\\"\\n[12p] > \\"기회비용 체증은 생산가능곡선이 오목한 형태를 갖게 하는 원인이다\\"\\n- **스스로 점검**: 각 측면을 독립적으로 나열하지 않고, 자원 이질성 → 기회비용 체증 → 곡선 형태라는 인과 흐름으로 연결했나요?" + }] + } + ``` + + # 완성본 예시 2 — Evaluate (적용판단형) + ```json + { + "questions": [{ + "content": "어느 국가가 현재 **생산가능곡선의 내부** 점에서 생산하고 있다. 이 국가의 경제 상태를 진단하고, **자원과 기술 수준의 변화 없이** 곡선 경계면으로 이동할 수 있는 방법을 2가지 제안하시오.", + "referencedPages": [8, 14], + "bloomsLevel": "Evaluate — 적용판단형: 생산가능곡선 모델을 적용하여 비효율 상태를 진단하고 해결 방안을 판단할 수 있는지 측정함", + "modelAnswer": "현재 이 국가는 가용 자원을 완전히 활용하지 못한 비효율 상태(실업 또는 자원 배분 비효율)에 있다. 첫째, 유휴 노동력과 설비를 가동하여 완전고용을 달성하면 곡선 경계면으로 이동할 수 있다. 둘째, 기존 자원의 배분을 최적화하여 각 재화 생산에 적합한 자원을 재배치함으로써 총생산을 늘릴 수 있다.", + "explanation": "| 요소 | 충족 | 부분 충족 | 미충족 (0점) |\\n|---|---|---|---|\\n| 경제 상태 진단 [분석·추론, 3점] | (3점) 내부 점이 비효율/실업 상태임을 명시하고 곡선 경계와의 차이를 설명 | (2점) 비효율이라고만 언급하고 구체적 원인(실업/배분 비효율) 생략 | 내부 점의 의미를 잘못 해석 |\\n| 해결 방안 1: 완전고용 달성 [개념 설명, 3점] | (3점) 유휴 자원 가동으로 곡선 경계 이동을 구체적으로 서술 | (2점) 자원 활용을 언급하나 경계면 이동과 연결하지 않음 | 해결 방안을 제시하지 않음 |\\n| 해결 방안 2: 자원 배분 최적화 [개념 설명, 2점] | (2점) 자원 재배치로 효율성 개선 가능함을 명시 | (1점) 효율성 개선을 언급하나 재배치 방법을 생략 | 두 번째 방안 미제시 |\\n\\n- **근거**: [8p] > \\"곡선 위의 점은 효율적, 안쪽 점은 비효율적 자원 배분 상태\\"\\n[14p] > \\"완전고용과 효율적 배분이 달성되면 곡선 위에서 생산한다\\"\\n- **스스로 점검**: 두 해결 방안이 서로 독립적인 경로인지, 하나가 다른 하나의 부분집합이 아닌지 확인했나요?" + }] + } + ``` + """; } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayRequestPrompt.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayRequestPrompt.java index fb3ab64a..f16916f3 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayRequestPrompt.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/essay/prompt/EssayRequestPrompt.java @@ -11,8 +11,7 @@ public class EssayRequestPrompt { private static final String APPLIED_INSTRUCTION_SPEC = """ # 사용자 지시 반영 - - 지시가 특정 형식을 요청하면, 그 형식에 대응하는 전략이 존재하면 해당 전략을 우선 선택한다. - - 대응 전략이 없는 형식은 요청 형식에 맞게 질문문을 자유롭게 구성한다. + - 사용자 지시에 맞는 패턴과 지식 유형을 Step 1-2의 테이블에서 찾아 해당 few-shot을 따른다. 대응 패턴이 없으면 자유롭게 구성한다. # 사용자 지시 반영 결과 기록 - 사용자 지시를 반영한 내용을 `appliedInstruction` 필드에 1~2문장으로 기록한다. diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleGuideLine.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleGuideLine.java index 27141465..cf705052 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleGuideLine.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleGuideLine.java @@ -8,168 +8,193 @@ public class MultipleGuideLine { public static final String content = """ - > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용 - > **CRITICAL RULE**: 4개 선택지의 글자수를 가장 짧은 것 기준 ±20% 이내로 맞추세요. 정답이 가장 길면 안 됩니다. + > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용한다. + > **CRITICAL RULE**: 오답 선택지의 길이를 제일 길게한다. # 역할 - 당신은 Bloom's Revised Taxonomy 기반 문항 설계 전문가입니다. - **Evaluate 수준**과 **Apply 수준** 문항을 혼합하여 다양한 인지 수준의 문제를 출제하세요. - - # Step 1 — 강의노트에서 출제 소재를 추출한다 - - **Evaluate용**: 서로 양립하기 어려운 개념 쌍(A vs B), 오류/모순이 숨겨진 비교 대상, 숨겨진 전제가 있는 주장 쌍 - - **Apply용**: 다른 분야/규모/조건에 전이 가능한 개념·원리·절차 - - # Step 2 — 생성할 문제에 따라 적절한 전략을 선택한다 - ## 1. Evaluate 수준: 단일 판단을 넘어, "오류 탐지 → 바로잡기 → 기준 적용 판단"처럼 2~3단계 인지 작업을 결합하는 수준 - ### 패턴 A: Critiquing (기준 충돌 판단) - **질문문 전략** - - **실질적 기준 간 긴장 관계 필수**: 두 기준은 반드시 역방향 관계여야 합니다. 기준1을 극대화하면 기준2가 악화되는 관계여야한다 - - 판단 기준을 **반드시** 2개 이상 **볼드로** 명시 - - 질문문에 **서로 양립하기 어려운 기준 2개**를 **볼드로** 명시하고, 그 충돌을 해결하는 판단을 요구한다 (단, "상황이 충돌한다"와 같은 평가 의도가 노출되는 교육학적 표현은 금지) - **질문문 템플릿** - - 양보형: "**성능 최적화**에서 일부 손실을 감수하더라도 **유지보수성**을 확보하는 방안은?" - - 우선순위형: "**보안 강도**를 우선하되 **사용 편의성**을 일정 수준 이상 유지해야 한다면?" - - 인과 추론형: "다음 현상의 원인을 진단하고, **처리 속도**와 **자원 효율성**을 고려한 대응은?" - - 단일 기준 심층형: "**확장성**을 극대화할 때 발생하는 부작용을 최소화하는 방법은?" - - 분류/배제형: "**안정성** 측면에서 부적절한 선택지를 배제할 때, 가장 적합한 것은?" - **질문문 및 선택지 예시** - - content: 한 기업이 새 데이터센터를 구축하려 한다. **초기 구축 비용 절감**을 우선하되 **장기 운영 안정성**도 일정 수준 이상 유지해야 할 때, 다음 중 두 기준을 가장 균형 있게 충족하는 인프라 전략은? - - selection.content 1 (정답): 온프레미스와 퍼블릭 클라우드를 혼합하여 초기 투자를 분산하고 핵심 서비스의 가용성을 확보한다 - - selection.content 2 (오답): 퍼블릭 클라우드를 전면 도입하여 초기 하드웨어 투자를 제거하고 온디맨드 확장성으로 초기 구축 비용 절감을 달성한다 - - selection.content 3 (오답): 온프레미스 인프라를 전면 구축하고 이중화 구성으로 핵심 서비스의 가용성을 높여 장기 운영 안정성을 확보한다 - - selection.content 4 (오답): 서버리스 아키텍처를 전면 도입하여 유휴 자원 비용을 제거하고 사용량 기반 과금으로 초기 구축 비용과 장기 운영 비용을 동시에 절감한다 - - ### B-1: 표 기반 Checking: 표를 제시하고, 그 중 하나의 항목(셀)이 잘못 기재된 것을 찾게 한다. - **질문문 템플릿** - - "다음 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?" - **선택지 전략 (2-2 대칭)** - - 타겟 분할: 표의 2개의 행(특성 A, 특성 B)을 타겟으로 삼는다. - - 구성: [특성 A 행의 오류를 지적하는 선지 2개] + [특성 B 행의 오류를 지적하는 선지 2개] - **질문문 및 선택지 예시** - - content: 다음 표는 두 전송 프로토콜의 특성을 비교한 것이다.\\n\\n| 특성 | 프로토콜 A | 프로토콜 B |\\n| :--- | :--- | :--- |\\n| 연결 설정 | 3-way handshake | 없음 |\\n| 패킷 - 순서 보장 | 보장 | 미보장 |\\n| 혼잡 제어 | 있음 | 있음 |\\n\\n위 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은? - - selection.content 1 (정답): [혼잡 제어] 행의 프로토콜 B 항목이 잘못되었다. 비연결형이므로 '없음'으로 수정해야 한다 - - selection.content 2 (오답): [혼잡 제어] 행의 프로토콜 B 항목이 잘못되었다. 옵션 사항이므로 '선택적 적용'으로 수정해야 한다 - - selection.content 3 (오답): [패킷 순서 보장] 행의 프로토콜 B 항목이 잘못되었다. 상위 계층에서 보장하므로 '보장'으로 수정해야 한다 - - selection.content 4 (오답): [패킷 순서 보장] 행의 프로토콜 B 항목이 잘못되었다. 연결 없이도 순서를 추적하므로 '보장'으로 수정해야 한다 - - ### B-2: 다이어그램 기반 Checking: Mermaid 다이어그램을 활용하여 시각적 흐름이나 구조에 논리적 오류를 삽입하고 그것을 찾게한다 - **질문문 전략** - - 각 노드 레이블에 정점 번호(A, B, C …)를 표시한다. 선택지 설명에서도 정점 번호로 노드를 지칭한다. - **질문문 템플릿** - - "다음 다이어그램의 흐름(또는 구조) 중 **논리적으로 잘못 연결되거나 모순되는 부분**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?" - **선택지 전략 (2-2 대칭)** - - 타겟 분할: 다이어그램 내 핵심 노드 또는 분기점 2개(예: 노드 B, 노드 C)를 타겟으로 삼는다. - - 구성: [노드 B의 연결 오류를 지적하는 선지 2개] + [노드 C의 연결 오류를 지적하는 선지 2개] - **질문문 및 선택지 예시** - - content: 다음은 웹 서버의 캐시 처리 흐름을 나타낸 다이어그램이다.\\n\\n```mermaid\\nflowchart TD\\n A[A: 클라이언트 요청 수신] --> B{B: 캐시 히트?}\\n B -- Yes --> C[C: DB 조회 후 데이터 로드]\\n B -- No --> D[D: 캐시에서 데이터 즉시 반환]\\n C --> E[E: 캐시에 결과 저장]\\n E --> F[F: 응답 전송]\\n D --> F\\n```\\n\\n위 다이어그램의 흐름 중 **논리적으로 잘못 연결된 부분**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은? - - selection.content 1 (정답): B(캐시 히트?)의 Yes/No 분기가 뒤바뀌어 있다. Yes → C(DB 조회), No → D(캐시 즉시 반환)로 각각 교체해야 한다 - - selection.content 2 (오답): B(캐시 히트?)의 Yes 분기가 잘못되었다. 캐시 히트 시 결과를 갱신해야 하므로 Yes → E(캐시에 결과 저장)로 직접 연결해야 한다 - - selection.content 3 (오답): C(DB 조회) 이후 순서가 잘못되었다. DB 조회 직후 응답을 먼저 전송해야 하므로 C → F(응답 전송) → E(캐시 저장) 순서로 수정해야 한다 - - selection.content 4 (오답): C(DB 조회) 이후 흐름이 잘못되었다. DB 조회 결과는 캐시에 저장하지 않아야 하므로 E(캐시에 결과 저장)를 제거하고 C → F(응답 전송)로 바로 연결해야 한다 - - ### B-3: 서술문/주장 기반 Checking**: 2개의 주장/설명을 나열하고, 그 중 논리적으로 잘못된 부분을 찾게한다. - **질문문 템플릿** - - "다음 설명 중 **잘못된 부분**을 찾아 바르게 수정한 것으로 가장 적절한 것은?" - - 두개의 인용문을 나열한다. 각 인용문은 개행한다: "\\n\\n> **[인물의 이름]**: \\"내용\\"\\n\\n> **[인물의 이름]**: \\"내용\\"\\n\\n" - **선택지 전략 (2-2 대칭)** - - 타겟 분할: 제시된 2개의 주장(ⓐ, ⓑ)을 각각 타겟으로 삼는다. - - 구성: [주장 ⓐ의 오류를 지적하는 선지 2개] + [주장 ⓑ의 오류를 지적하는 선지 2개] - **문제 본문 및 선택지 예시** - - content: 다음은 OS 프로세스 스케줄링에 관한 두 가지 설명이다.\\n\\n> **ⓐ**: \\"선점형 스케줄링은 현재 실행 중인 프로세스가 자발적으로 CPU를 반납할 때까지 다른 프로세스가 실행될 수 없다.\\"\\n\\n> **ⓑ**: \\"SJF 스케줄링은 평균 대기 시간을 최소화하지만, CPU 버스트가 긴 프로세스가 계속 뒤로 밀리는 기아(Starvation) 문제가 있다.\\"\\n\\n위 설명 중 **논리적으로 잘못된 항목**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은? - - selection.content 1 (정답): ⓐ가 잘못되었다. 선점형은 '스케줄러가 실행 중인 프로세스에서 강제로 CPU를 회수할 수 있다'로 수정해야 한다 - - selection.content 2 (오답): ⓐ가 잘못되었다. 선점형은 '우선순위가 높은 프로세스가 도착했을 때만 자발적으로 반납한다'로 수정해야 한다 - - selection.content 3 (오답): ⓑ가 잘못되었다. SJF는 '대부분의 프로세스에서 대기 시간을 줄여 기아 발생 가능성을 낮출 수 있다'로 수정해야 한다 - - selection.content 4 (오답): ⓑ가 잘못되었다. SJF는 'CPU 버스트가 상대적으로 짧은 프로세스가 우선순위에서 밀릴 수 있다'로 수정해야 한다 - - ### B-4: 코드 기반 Checking**: 코드 블록을 제시하고, 그 안의 논리적 오류나 비효율을 찾게한다. - **질문문 전략** - - 자바, 파이썬 등 코딩 관련 내용이 등장할 경우에만 사용한다. - **질문문 템플릿** - - "다음 코드에서 **논리적 오류(또는 비효율)**를 찾아 수정한 것으로 가장 적절한 것은?" - **선택지 전략 (2-2 대칭)** - - 타겟 분할: 논리적 오류를 발생시키는 2개의 상호 배타적/대칭적 변수나 연산 방향(예: left 조작 vs right 조작, 증가 vs 감소)을 타겟으로 삼는다. - - 구성: [방향 A(예: left)를 수정하는 선지 2개] + [방향 B(예: right)를 수정하는 선지 2개] - **문제 본문 및 선택지 예시** - - content: 다음은 정렬된 배열에서 목표 값을 찾는 이진 탐색 코드이다.\\n\\n```python\\ndef binary_search(arr, target):\\n left, right = 0, len(arr) - 1\\n while left <= right:\\n mid = (left + right) // 2\\n if arr[mid] == target:\\n return mid\\n elif arr[mid] < target:\\n left = mid\\n else:\\n right = mid - 1\\n return -1\\n```\\n\\n위 코드에서 **논리적 오류**를 찾아 수정한 것으로 가장 적절한 것은? - - selection.content 1 (정답): `left = mid`를 `left = mid + 1`로 수정한다 — mid는 정답이 될 수 없으므로 탐색 범위에서 제외해야 한다 - - selection.content 2 (오답): `left = mid`를 `left = mid - 1`로 수정한다 — 탐색 방향을 왼쪽으로 역전시켜 누락된 값을 재탐색해야 한다 - - selection.content 3 (오답): `right = mid - 1`을 `right = mid`로 수정한다 — mid 값이 정답일 가능성을 대비해 탐색 범위에 남겨두어야 한다 - - selection.content 4 (오답): `right = mid - 1`을 `right = mid + 1`로 수정한다 — 타겟이 작을 경우 오른쪽 절반을 탐색하도록 범위를 확장해야 한다 - - ### B-5: 수식 기반 Checking**: 강의노트에 수식이 포함된 경우, 수식이나 수학적 전개 과정을 제시하고, 그 안의 논리적 비약이나 오류 찾게한다. - **질문문 전략** - - "다음 수식 전개 과정에서 **오류**를 찾아 올바르게 수정한 것으로 가장 적절한 것은?" - **선택지 전략 (2-2 대칭)** - - 타겟 분할: 수식 전개 과정 중 논리적 비약이 발생하기 쉬운 2개의 핵심 단계(예: Step 2, Step 3)를 타겟으로 삼는다. - - 구성: [Step X의 오류를 지적하는 선지 2개] + [Step Y의 오류를 지적하는 선지 2개] - **문제 본문 및 선택지 예시**: - - content: 다음은 슬라이딩 윈도우 프로토콜의 **최대 채널 이용률(Utilization)**을 도출하는 과정이다.\\n\\n> **전제**: 송신 윈도우 크기 $W = 4$, 패킷 크기 $L = 1{,}000\\\\text{ bit}$, 전송 속도 $R = 1\\\\text{ Mbps}$, 편도 전파 지연 $t_p = 10\\\\text{ ms}$\\n\\n> **Step 1**: $t_t = \\\\dfrac{L}{R} = \\\\dfrac{1{,}000}{1{,}000{,}000} = 1\\\\text{ ms}$\\n\\n> **Step 2**: $\\\\text{RTT} = t_p = 10\\\\text{ ms}$     \\n\\n> **Step 3**: $U = \\\\dfrac{W \\\\cdot t_t}{t_t + \\\\text{RTT}} = \\\\dfrac{4 \\\\times 1}{1 + 10} \\\\approx 36.4\\\\%$\\n\\n위 수식 전개 중 **오류**를 찾아 올바르게 수정한 것으로 가장 적절한 것은? - - selection.content 1 (정답): Step 2가 잘못되었다. RTT는 왕복 시간이므로 편도 지연의 2배인 $\\text{RTT} = 20\\text{ ms}$로 수정해야 한다 - - selection.content 2 (오답): Step 2가 잘못되었다. RTT는 왕복 시간이므로 수정한 뒤 Step 3의 분모도 $t_t + 2 \\times \\text{RTT}$로 함께 수정해야 한다 - - selection.content 3 (오답): Step 3이 잘못되었다. 효율성 공식의 분모에서 $t_t$를 제거하고 $U = \\frac{W \\cdot t_t}{\\text{RTT}}$로 수정해야 한다 - - selection.content 4 (오답): Step 3이 잘못되었다. 효율성 공식의 분자에서 $t_t$를 제거하고 $U = \\frac{W}{t_t + \\text{RTT}}$로 수정해야 한다 - - ## 2. Apply 수준**: 개념·원리·절차를 **같은 도메인 내에서 이전에 접하지 않은 새로운 상황**에 적용하여 결과를 예측하거나 문제를 해결하게한다. - ### 패턴 C - **질문문 전략** - - 새로운 상황을 서술한 뒤, 두 제약 조건을 명시하고 해당 원리를 적용하면 어떤 결과가 나오는지 묻는다. - - 강의노트에 없는 시나리오를 제시한 뒤, 해당 원리에 따라 이 문제를 해결할 때 가장 적절한 접근법을 묻는다. - - 기존과 다른 조건을 서술한 뒤, 학습한 절차를 이 상황에 적용할 때 가장 먼저 확인할 것을 묻는다. - **질문문 템플릿**: - - 같은 대상에 다른 조건 (예: 학습한 팀 편성 원리를 인원수/제약이 다른 팀에 적용) - - 같은 분야의 유사 대상 (예: 학습한 보스 AI를 NPC 동료 AI에 적용) - - 기존 원리의 예외 상황 (예: 원리가 성립하지 않는 극한 조건에서의 판단) - **문제 본문 및 선택지 예시**: - - content: 강의노트에서 학습한 **로드밸런싱**의 원리(요청을 여러 서버에 분산하여 단일 서버 과부하를 방지)는 동일 사양 서버 환경을 전제로 설명되었다. 만약 **서버 사양이 각각 다른 이기종 클러스터**에 이 원리를 적용할 때, **응답 시간 균등화**와 **고사양 서버 활용 극대화**라는 두 제약을 동시에 고려한다면 가장 적절한 접근법은? - - selection.content 1 (정답): 처리 능력 비례 가중치를 서버별로 부여하고, 응답 시간 피드백으로 동적 조정하여 두 기준을 달성한다 - - selection.content 2 (오답): 서버 사양에 관계없이 균등 배분 방식을 적용하여 요청 처리 편차를 최소화하고, 일관된 부하 분산으로 응답 시간의 균등화를 달성한다 - - selection.content 3 (오답): 고사양 서버에 높은 가중치를 고정 부여하여 클러스터의 처리 용량을 최대한 활용하고, 전체 처리량 증대를 통해 고사양 서버 활용 극대화를 달성한다 - - selection.content 4 (오답): 현재 응답 시간이 가장 짧은 서버에 요청을 우선 배분하고, 실시간 응답 속도 기반 동적 라우팅으로 클러스터 전체의 응답 시간을 최적화한다 - - ### Step 4 — 질문문을 작성한다. - - 질문문은 학습자가 읽는 텍스트입니다. **학생에게 친숙한 일상 표현만 사용하세요.** (평가/교육학적 전문 용어 노출 금지) - - **질문문 금지 형식**: "적절하지 않은 것은?", "틀린 것은?", "옳지 않은 것은?", "잘못된 것은?", "부적절한 것은?" 형식의 질문은 구조상 올바른 진술 3개 + 틀린 진술 1개가 되어 선택지 구조(정답 1개 + 오답 3개)와 충돌하며 2-2 대칭을 깹니다. 반드시 **긍정 형식**("가장 적절한 것은?", "가장 올바른 것은?")으로 작성하세요. - - ### Step 5 — 선택지 4개 작성한다. (정답 1개 + 오답 3개) - **정답**: 복수 기준을 종합한 판단. 강의노트에 근거. - **오답 제작 메커니즘 (핵심 규칙)**: 오답은 "정답의 요소 하나만 바꾼 것"이어야 합니다. 정답을 먼저 작성한 뒤, 오답 생성 시에는 아래 유형 중 적절한 것을 적용하세요. - - **조건 누락형**: 정답이 성립하는 전제 조건을 한 개 제거하여, 그 원리를 조건 밖 상황에 그대로 적용한 것처럼 서술. - - 허용 범위: 나머지 논리 구조(방향·인과·결론)는 정답과 동일하게 유지. 조건 하나만 빠진 것. - - 타겟 오개념: "원리는 맞으니 이 조건에서도 쓸 수 있다" - - 예시 (경제학) — 정답: "완전경쟁시장에서 외부효과가 없을 때, 가격 규제 없이 수요·공급 균형이 자원을 효율적으로 배분한다" - - 예시 (경제학) — 오답: "수요·공급이 균형을 이룰 때, 가격 규제 없이 시장이 자원을 효율적으로 배분한다" ← '완전경쟁·외부효과 없음' 조건만 제거, 나머지 논리는 동일 - - **범위 과잉형**: 정답이 특정 조건에서만 성립하는데, 그 조건 없이도 성립한다고 일반화. - - 허용 범위: "대부분", "일반적으로", "주로"처럼 과잉이지만 절대어가 아닌 표현만 사용. "모든/항상" 금지. - - 타겟 오개념: "이 원리는 이 상황보다 더 넓은 상황에 적용된다" - - 예시 (의학) — 정답: "내성균이 검출된 감염 사례에서는 동일 계열 항생제를 중단하고 감수성 검사 결과에 따라 대체 약제를 선택해야 한다" - - 예시 (의학) — 오답: "세균 감염이 확인된 대부분의 사례에서는 기존 항생제를 중단하고 감수성 검사 결과에 따라 대체 약제를 선택하는 것이 적절하다" ← '내성균 검출'이라는 조건 없이 '대부분의 사례'로 과잉. "항상/모든" 미사용. - - **오개념 혼동형**: 정답 개념과 혼동하기 쉬운 유사 개념·용어 하나를 잘못 대입. 나머지는 정답 논리와 동일하게 유지. - - 허용 범위: 교체하는 개념은 1개. 문장 전체를 다른 주장으로 바꾸지 말 것. - - 타겟 오개념: "비슷해 보이는 A와 B는 사실상 같다" - - 예시 (지리) — 정답: "편서풍 영향권 대륙 서안은 연중 강수가 고르게 분포하는 서안해양성 기후를 나타내며, 기온의 연교차가 작다" - - 예시 (지리) — 오답: "편서풍 영향권 대륙 서안은 여름 건조·겨울 강수가 특징인 지중해성 기후를 나타내며, 기온의 연교차가 작다" ← '서안해양성' → '지중해성' 1개만 교체, 나머지 논리 동일 - - **핵심 역전형**: 정답의 인과·순서·방향에서 **한 단계만** 틀리게 교체. 전면 부정·완전 역전 금지. - - 허용 범위: "A → B → C" 흐름에서 B만 교체하거나, 인접한 두 단계의 순서만 바꾸는 수준. "A도 B도 C도 없다"처럼 전체를 부정하는 것은 금지. - - 타겟 오개념: "이 인과 관계에서 원인과 결과의 방향이 반대다" - - 예시 (행정·법학) — 정답: "의회가 법률을 제정한 뒤, 행정부가 시행령을 마련하여 현장에 적용한다" (법률 제정 → 시행령 마련 → 현장 적용) - - 예시 (행정·법학) — 오답: "행정부가 시행령을 먼저 마련한 뒤, 의회가 법률을 제정하여 적용한다" ← '법률 제정'과 '시행령 마련' 순서만 교체, 나머지 흐름 동일 - - **길이 균등**: 4개 선택지의 글자수를 가장 짧은 것 기준 ±20% 이내로 맞추세요. 정답이 가장 길면 안 됩니다. - - ### Step 6 — 해설을 작성한다. - **bloomsLevel**: - - `"수준 — 전략"` 형식으로 기입하세요. - - Bloom's 수준(`Evaluate` 또는 `Apply`)과 Step 2에서 선택한 전략을 함께 작성합니다. - - **정답 선택지 해설 (correct: true)**: - - `**정답 추론**`: 정답이 되는 추론 경로를 단계적으로 서술 - - `**근거**`: 근거들을 페이지 번호와 함께 인용함 - - 구조: [Np] > "강의노트 원문 인용"\\n[Np] > "강의노트 원문 인용", ... - - `**스스로 점검**`: 이 문제의 판단 과정에서 놓치기 쉬운 사고 패턴을 질문 형태로 1개 제시 - **오답 선택지 해설 (correct: false)**: + 당신은 Bloom's Revised Taxonomy 기반 문항 설계 전문가다. + **Evaluate 수준**과 **Apply 수준** 문항을 혼합하여 출제한다. + + # Step 1 — 강의노트에서 출제 소재를 추출하고, 각 소재의 지식 유형을 판별한다 + | 지식 유형 | 판별 기준 | 소재 예시 | + |----------|----------|----------| + | 사실적 | 수치·날짜·명칭 등 검증 가능한 정보 | 공식의 계수, 역사적 사건의 연도, 영양소의 열량 | + | 개념적 | 원리·이론·분류 체계 간의 관계 | 경제 원리 간 충돌, 세포 구조 비교, 학습 이론 구분 | + | 절차적 | 순서가 있는 방법·알고리즘·프로세스 | 코드 로직, 실험 절차, 수식 전개 순서 | + | 메타인지적 | 방법론·모형·분석 도구 자체에 대한 판단 | 경제 모형의 유용성, 과학적 방법론의 한계, 가정의 타당성 | + + # Step 2 — 파악한 지식 유형과 인지 수준에 맞는 패턴을 선택한다 + ## 1. Evaluate 수준 + ### 패턴 A: Critiquing — 선지 설계 절차 (Misconception-Based) + > **핵심 원칙**: "맞는 풀이에서 한 발짝만 이탈하라" + > **구조적 제약**: **4개 선지 모두** 두 기준을 언급해야 한다. 오답도 두 기준을 모두 다루되, 균형의 방식에서 미묘하게 이탈한다. + > **금지 표현**: 오답에 "~만", "모든 ~을", "완전한 ~", "오직 ~" 같은 한쪽 극단 수식어를 사용하지 않는다. + + **1단계: 질문문 설계** + 충돌하는 두 기준을 **볼드**로 명시한다. + 종결: "이 두 기준 사이에서 균형을 잡으려는 아래 전략들 중, 가장 적절한 것은?" + - 개념적 지식: 원리·이론 간 충돌 (예: **성장 잠재력** vs **현세대 생활 수준**) + - 메타인지적 지식: 방법론·모형 설계 기준 충돌 (예: **인과 구조의 명확성** vs **현실 반영도**) + + **2단계: 정답 풀이를 단계별로 작성한다** + - R1: 기준 A의 의미를 강의노트에서 확인한다 + - R2: 기준 B의 의미를 강의노트에서 확인한다 + - R3: 두 기준을 동시에 충족하는 행동을 도출한다 + - R4: 그 행동의 실무적 약점을 명시한다 → 정답 완성 ("~대신, ~감수한다") + -> 예시 (PPF 자원 배분): + R1: 성장 잠재력 = 자본재 투자 확대 + R2: 현세대 생활 수준 = 소비재 일정량 유지 + R3: 소비재를 줄이되 완전히 없애지 않고 자본재 투자 확대 + R4: 약점 = 현세대 실질 소비 감소 + → 정답: "소비재 생산을 줄여서 자본재 투자를 늘리는 대신, 저소득층의 생활 수준이 떨어지는 걸 감수한다" + + **3단계: 정답 풀이에서 이탈하여 오답 3개를 생성한다** + 각 오답은 R1~R3 중 **한 단계만** 이탈하고 나머지는 정답과 공유한다. + 이탈은 반드시 **강의노트 같은 단원의 인접 개념**을 사용한다. + **각 오답도 두 기준(A와 B)을 모두 언급해야 한다** — 한쪽만 언급하면 즉시 소거되므로 실패. + + **오답1 — R2 이탈**: 기준 B를 인접 개념으로 오해하여 균형점이 어긋남 + -> 이탈: "현세대 생활 수준"을 "단기 후생 수준"으로 오해 + -> 결과: "자본재 비중을 늘려서 장기 성장률을 끌어올리는 대신, 현세대의 소비 여력이 줄어들어 단기 후생이 악화되는 걸 감수한다" + + **오답2 — R1 이탈**: 기준 A의 프레임을 인접 메커니즘으로 확대하여 범위 이탈 + -> 이탈: "곡선 위에서 배분"을 "곡선 자체를 이동"으로 확대 + -> 결과: "기술 투자로 생산가능곡선 자체를 바깥으로 밀어내는 대신, 소비와 투자 양쪽의 즉각적 성과를 포기하는 걸 감수한다" + + **오답3 — R3 이탈**: 전제 조건을 착각하여 출발점이 다름 + -> 이탈: "곡선 위(완전고용)"를 "유휴 자원 존재"로 착각 + -> 결과: "유휴 자원을 자본재에 우선 투입하는 대신, 유휴 자원이 바닥나면 결국 소비재가 줄어드는 걸 감수한다" + + **4단계: 자가점검** + > 1. **4개 선지 모두 두 기준을 언급하는가?** → 한쪽만 언급하는 선지가 있으면 두 기준을 모두 다루도록 수정 + > 2. 오답에 "~만", "모든", "완전히" 같은 극단 수식어가 있는가? → 있으면 삭제하고 인접 개념으로 교체 + > 3. 정답만 유독 온건/균형적인가? → 오답에도 동등한 수준의 균형적 어조를 부여 + + ### B-1: 표 기반 Checking + + **개념적 지식** + 질문문은 3행 이상의 비교표에서 오류 1개만 삽입하세요. 학습자가 자주 혼동하는 경계 사례를 오류로 사용하세요. + -> 예시: 다음 표는 세포 소기관의 특성을 비교한 것이다.\\n\\n| 소기관 | 존재 위치 | 주요 기능 | 에너지 관련 역할 |\\n| :--- | :--- | :--- | :--- |\\n| 미토콘드리아 | 동물·식물 | 세포 호흡 | ATP 합성 |\\n| 엽록체 | 식물 | 광합성 | 포도당 합성 |\\n| 리보솜 | 동물·식물 | 단백질 합성 | ATP 소비 |\\n\\n위 표에서 **틀린 항목**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은? + -> 설명: 3행 비교표에서 리보솜의 에너지 관련 역할에 오류 1개만 삽입. ATP/GTP 혼동이라는 경계 사례를 사용함 + 정답은 올바른 행/열을 지목하고 구체적 수정안을 제시하세요. + -> 예시: 리보솜의 에너지 관련 역할이 틀렸다. 단백질 합성은 ATP가 아니라 GTP를 쓰는 과정이니까 'GTP 소비'로 고쳐야 한다 + -> 설명: 올바른 행(리보솜)/열(에너지 역할)을 지목하고 'GTP 소비'라는 구체적 수정안을 제시함 + 첫번째 오답(오개념혼동형)은 **같은 행의 다른 열**을 지목하세요. 상위·하위 개념이 실제로 겹치는 부분(세포 호흡≈포도당 분해 등)을 활용하여, 혼동 자체가 합리적 추론처럼 보이게 하세요. + -> 예시: 리보솜의 주요 기능이 틀렸다. 리보솜은 단백질 합성이 아니라 mRNA 번역이 핵심이니까 '번역(translation)'으로 고쳐야 한다 + -> 설명: 같은 행(리보솜)의 다른 열(주요 기능)을 지목. 단백질 합성≈mRNA 번역이라는 실제로 겹치는 개념을 활용함 + 두번째 오답(오개념혼동형)은 **다른 행**을 지목하세요. 세부 과정 혼동(명반응/암반응, 분해/합성 등)에서 학습자가 실제로 헷갈리는 경계 사례를 선택하세요. + -> 예시: 엽록체의 에너지 관련 역할이 틀렸다. 엽록체는 포도당이 아니라 ATP를 직접 합성하고, 포도당 합성은 세포질에서 일어나니까 'ATP 합성'으로 고쳐야 한다 + -> 설명: 다른 행(엽록체)을 지목. 명반응의 ATP 합성과 암반응의 포도당 합성이라는 세부 과정 혼동을 활용함 + 세번째 오답(조건누락형)은 **두번째 오답과 같은 행의 다른 열**을 지목하세요. 추가하는 세부 사항은 사실적으로 정확해야 하며, "더 정밀한 수정"처럼 보이게 하세요. + -> 예시: 엽록체의 존재 위치가 틀렸다. 엽록체는 식물뿐 아니라 일부 조류에도 존재하니까 '식물·조류'로 고쳐야 한다 + -> 설명: 두번째 오답과 같은 행(엽록체)의 다른 열(존재 위치)을 지목. '일부 조류에도 존재'라는 정확한 사실을 불필요하게 추가함 + + ### B-2: 서술문/주장 기반 Checking + + **개념적 지식** + 질문문은 ⓐ/ⓑ 라벨을 부여하고 인용부호(`\\n\\n>`)로 분리하세요. ⓐ는 양면(효과+부작용) 균형 구조로, ⓑ에 논거 불충분 포인트를 삽입하세요. + -> 예시: 다음은 인플레이션 대응에 관한 두 경제학자의 주장이다.\\n\\n> **ⓐ**: \\"중앙은행이 기준금리를 인상하면 시중 유동성이 줄어 물가 상승 압력이 완화되지만, 기업의 차입 비용이 늘어 투자가 위축될 수 있다.\\"\\n\\n> **ⓑ**: \\"정부가 재정지출을 확대하여 공급 병목을 해소하면 물가를 안정시킬 수 있으며, 이 과정에서 재정 건전성에 미치는 영향은 미미하다.\\"\\n\\n위 주장 중 **논거가 불충분한 쪽**을 찾아 바르게 보완한 것으로 가장 적절한 것은? + -> 설명: ⓐ/ⓑ 라벨을 부여하고 인용부호로 분리. ⓐ는 양면(물가 완화+투자 위축) 균형 구조, ⓑ에 '재정 건전성 영향 미미'라는 불충분 포인트를 삽입함 + 정답은 불충분한 쪽을 정확히 지목하고 누락된 메커니즘을 보완하세요. + -> 예시: ⓑ가 부족하다. 재정지출을 늘리면 단기적으로 총수요가 자극돼서 오히려 물가를 끌어올릴 수 있다는 점이 빠져 있다 + -> 설명: 불충분한 쪽(ⓑ)을 정확히 지목하고, 총수요 자극→물가 상승이라는 누락된 메커니즘을 보완함 + 첫번째 오답(조건누락형)은 반대쪽을 지목하세요. 보완 근거로 제시하는 경제 메커니즘은 그 자체로 정확해야 하며, "이 관점도 보완이 필요하다"는 인상을 주게 하세요. + -> 예시: ⓐ가 부족하다. 금리 인상이 환율 상승을 유발해서 수입 물가를 높이니까 물가 안정 효과가 상쇄될 수 있다는 점이 빠져 있다 + -> 설명: 반대쪽(ⓐ)을 지목. '환율→수입 물가' 경로는 정확한 메커니즘이지만 ⓐ의 논거가 아닌 추가 관점임 + 두번째 오답(핵심역전형)은 같은 쪽을 지목하되 보완 방향을 틀리게 하세요. 고급 경제 논제를 활용하여 인과 역전이 심화 분석처럼 보이게 하세요. + -> 예시: ⓑ가 부족하다. 재정지출 확대가 통화정책과 상반되어 금리가 자동으로 하락하니까 물가 안정 효과가 소멸된다는 점이 빠져 있다 + -> 설명: 같은 쪽(ⓑ)을 지목하되 '금리 자동 하락'이라는 인과 역전으로 보완 방향을 틀림. 통화-재정 정책 상호작용이라는 고급 논제를 활용함 + 세번째 오답(오개념혼동형)은 반대쪽을 지목하세요. 제시하는 현상은 실제로 존재하는 것이어야 하며, 정확한 현상 서술이 과잉 확대를 정당화하게 하세요. + -> 예시: ⓐ가 부족하다. 금리 인상이 소비자 심리를 급격히 위축시켜 디플레이션을 초래하니까 물가 '안정'이 아니라 물가 '하락'으로 표현해야 한다 + -> 설명: 반대쪽(ⓐ)을 지목. '소비자 심리 위축→디플레이션'은 실제 존재하는 현상이지만 과잉 확대함 + + ### B-3: 다이어그램 정합성 Checking + + **절차적 지식** + 질문문은 Mermaid 정점마다 식별 문자를 부여하세요(예: `A[A: 설명]`). 다이어그램과 설명은 줄바꿈으로 분리하세요. + -> 예시: 다음은 웹 서버의 캐시 처리 흐름을 나타낸 다이어그램이다.\\n\\n```mermaid\\nflowchart TD\\n A[A: 클라이언트 요청 수신] --> B{B: 캐시 히트?}\\n B -- Yes --> C[C: DB 조회 후 데이터 로드]\\n B -- No --> D[D: 캐시에서 데이터 즉시 반환]\\n C --> E[E: 캐시에 결과 저장]\\n E --> F[F: 응답 전송]\\n D --> F\\n```\\n\\n위 다이어그램에서 **잘못 연결된 곳**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은? + -> 설명: Mermaid 정점마다 A~F 식별 문자를 부여하고, 다이어그램과 설명을 줄바꿈으로 분리함 + 정답은 잘못된 연결/방향을 지목하고 수정안을 제시하세요. + -> 예시: B의 Yes/No 분기가 뒤바뀌어 있다. 캐시 히트면 바로 반환하고 미스면 DB를 조회해야 하니까 Yes → D, No → C로 바꿔야 한다 + -> 설명: B의 Yes/No 분기가 뒤바뀌어 있다는 잘못된 연결을 지목하고 구체적 수정안을 제시함 + 첫번째 오답(오개념혼동형)은 **같은 구간**을 지목하되 수정 방향을 틀리게 하세요. 실제로 사용되는 대안 전략(write-through 등)을 근거로 제시하여, 틀린 수정이 설계 개선처럼 보이게 하세요. + -> 예시: B의 Yes 분기가 틀렸다. 캐시 히트 시에도 결과를 갱신해야 하니까 Yes → E(캐시에 결과 저장)로 연결해야 한다 + -> 설명: 같은 구간(B 분기)을 지목하되 write-through 전략(캐시 히트 시 갱신)으로 수정 방향을 틀림 + 두번째 오답(핵심역전형)은 **다른 구간**을 지목하세요. 성능 최적화("응답 먼저, 저장 나중")처럼 현실에서 실제로 쓰이는 논리를 근거로 제시하여, 순서 역전이 최적화처럼 보이게 하세요. + -> 예시: C 이후 순서가 틀렸다. DB 조회 직후 응답을 먼저 보내야 하니까 C → F → E 순서로 고쳐야 한다 + -> 설명: 다른 구간(C 이후)을 지목. '응답 먼저, 저장 나중'이라는 성능 최적화 논리가 순서 역전을 정당화함 + 세번째 오답(조건누락형)은 **두번째 오답과 같은 구간**을 지목하세요. 일부 아키텍처에서 실제로 캐시를 생략하는 사례를 근거로 제시하여, 노드 제거가 합리적 간소화처럼 보이게 하세요. + -> 예시: C 이후 흐름이 틀렸다. DB 조회 결과는 캐시에 저장하지 않으니까 E를 빼고 C → F로 바로 연결해야 한다 + -> 설명: 두번째 오답과 같은 구간(C 이후)을 지목. 일부 아키텍처에서 캐시를 생략하는 사례가 노드 제거를 합리적으로 보이게 함 + + + ### B-4: 코드 오류 Checking (코딩 관련 내용이 등장할 경우에만 사용) + + **절차적 지식** + 질문문은 코드 블록과 설명을 줄바꿈으로 분리하세요. 하나의 라인에만 버그를 삽입하고 나머지는 정확하게 유지하세요. + -> 예시: 다음은 정렬된 배열에서 목표 값을 찾는 이진 탐색 코드이다.\\n\\n```python\\ndef binary_search(arr, target):\\n left, right = 0, len(arr) - 1\\n while left <= right:\\n mid = (left + right) // 2\\n if arr[mid] == target:\\n return mid\\n elif arr[mid] < target:\\n left = mid\\n else:\\n right = mid - 1\\n return -1\\n```\\n\\n위 코드에서 **버그**를 찾아 수정한 것으로 가장 적절한 것은? + -> 설명: 코드 블록과 설명을 줄바꿈으로 분리. `left = mid` 한 라인에만 버그를 삽입하고 나머지는 정확하게 유지함 + 정답은 버그 라인을 정확히 지목하고 올바른 방향으로 수정하세요. + -> 예시: `left = mid`가 문제다. mid는 이미 확인했으니까 `left = mid + 1`로 탐색 범위에서 빼야 한다 + -> 설명: 버그 라인(`left = mid`)을 정확히 지목하고 `mid + 1`로 올바른 방향으로 수정함 + 첫번째 오답(핵심역전형)은 **같은 라인**을 지목하되 반대 방향으로 수정하세요. 수정 근거는 알고리즘적으로 그럴듯해야 하며("누락된 값을 재탐색" 등), 방향 역전이 논리적 추론처럼 보이게 하세요. + -> 예시: `left = mid`가 문제다. 탐색 방향을 왼쪽으로 역전시켜 누락된 값을 재탐색해야 하니까 `left = mid - 1`로 고쳐야 한다 + -> 설명: 같은 라인(`left = mid`)을 지목하되 `mid - 1`로 반대 방향으로 수정. '누락된 값을 재탐색'이라는 알고리즘적 근거를 제시함 + 두번째 오답(오개념혼동형)은 **다른 라인**을 지목하세요. 실제 코딩에서 흔히 논의되는 최적화 포인트를 근거로 제시하여, 정확한 코드 수정이 개선처럼 보이게 하세요. + -> 예시: `while left <= right`가 문제다. 등호 조건이 같은 인덱스를 중복 비교하니까 `while left < right`로 고쳐야 한다 + -> 설명: 다른 라인(`while left <= right`)을 지목. '등호 중복 비교 제거'라는 흔한 최적화 논의를 근거로 제시함 + 세번째 오답(조건누락형)은 **두번째 오답과 같은 라인**을 지목하세요. 엣지 케이스 방지처럼 방어적 프로그래밍 논리를 근거로 제시하여, 올바른 코드를 망가뜨리는 수정이 안전한 수정처럼 보이게 하세요. + -> 예시: `while left <= right`가 문제다. right가 -1이 되는 경우를 방지해야 하니까 `while left < right`로 바꾸고 종료 후 별도 검사를 추가해야 한다 + -> 설명: 두번째 오답과 같은 라인(`while left <= right`)을 지목. 'right가 -1이 되는 경우 방지'라는 방어적 프로그래밍 논리를 근거로 제시함 + + ### B-5: 수식 오류 Checking (수식이 포함된 경우에만 사용) + + **절차적 지식** + 질문문은 반응식/풀이 과정에서 절차 오류(단위 전환 누락, 계수 미반영)를 삽입하세요. + -> 예시: 다음은 과산화수소(H₂O₂) 분해 반응에서 생성되는 **O₂의 질량**을 구하는 과정이다.\\n\\n> **반응식**: $2H_2O_2 \\\\rightarrow 2H_2O + O_2$\\n\\n> **전제**: H₂O₂ 68g이 완전 분해된다. (H₂O₂ 분자량 = 34, O₂ 분자량 = 32)\\n\\n> **Step 1**: 분자량 확인 — H₂O₂ = 2(1) + 2(16) = 34\\n\\n> **Step 2**: 질량비로 생성물 계산 — O₂ = 68 × (32/34) = 64g\\n\\n위 수식 전개에서 **틀린 단계**를 찾아 올바르게 수정한 것으로 가장 적절한 것은? + -> 설명: 반응식 풀이 과정에서 Step 2의 절차 오류(몰비 미반영, 질량비로 직접 계산)를 삽입함 + 정답은 절차 오류를 정확히 지목하고 올바른 풀이 절차를 제시하세요. + -> 예시: Step 2가 틀렸다. 질량비로 바로 계산하면 안 되고 몰수를 먼저 구해야 하니까, H₂O₂ = 68/34 = 2mol이고 몰비(2:1)에 따라 O₂ = 1mol = 32g이다 + -> 설명: 절차 오류(Step 2)를 정확히 지목하고 '몰수 먼저 → 몰비 적용'이라는 올바른 풀이 절차를 제시함 + 첫번째 오답(오개념혼동형)은 **같은 단계**를 지목하고 정확한 교정 용어('몰비')를 사용하되, 계수를 오적용하세요. 용어 사용이 정확해서 "풀이 방법은 맞는데 계산만 다르다"는 인상을 주게 하세요. + -> 예시: Step 2가 틀렸다. 질량비가 아니라 몰비로 계산해야 하니까, H₂O₂ 2mol이 O₂ 2mol을 생성하므로 O₂ = 2 × 32 = 64g이다 + -> 설명: 같은 단계(Step 2)를 지목하고 '몰비'라는 정확한 용어를 사용하되, 계수를 2:2로 오적용(정답은 2:1)함 + 두번째 오답(핵심역전형)은 **다른 단계**를 지목하세요. 학습자가 실제로 자주 틀리는 오류 유형(원자량 혼동, 분자량 계산 착오 등)을 근거로 제시하여, 경험적으로 납득되게 하세요. + -> 예시: Step 1이 틀렸다. 수소의 원자량을 2로 적용해야 하니까 H₂O₂ = 2(2) + 2(16) = 36으로 고쳐야 한다 + -> 설명: 다른 단계(Step 1)를 지목. '수소 원자량=2'는 학습자가 실제로 자주 틀리는 원자량 혼동 유형임 + 세번째 오답(조건누락형)은 **두번째 오답과 같은 단계**를 지목하세요. 추가하는 정밀도(유효숫자, 동위원소 보정 등)는 과학적으로 실재해야 하며, "더 정확한 풀이"라는 인상을 주게 하세요. + -> 예시: Step 1이 틀렸다. 산소의 원자량은 16이 아니라 15.999이고 수소도 1.008이니까 H₂O₂ = 2(1.008) + 2(15.999) = 34.014로 보정해야 한다 + -> 설명: 두번째 오답과 같은 단계(Step 1)를 지목. 15.999/1.008이라는 정밀 원자량은 과학적으로 실재하지만 불필요한 보정임 + + ## 2. Apply 수준: 개념·원리·절차를 같은 도메인 내 새로운 상황에 적용하여 결과를 예측하거나 문제를 해결하게 한다 + ### 패턴 C: Apply (원리 적용) + + **개념적 지식** + 질문문은 단일 제약("가계 소득"+"단기적" 등)으로 적용 범위를 한정하세요. + -> 예시: 정부가 특정 산업에 대한 생산 보조금을 도입하려 한다. 이 정책이 **가계 소득**에 미치는 단기적 영향으로 가장 적절한 것은? + -> 설명: '가계 소득'+'단기적'이라는 단일 제약으로 적용 범위를 한정함 + 정답은 직접 인과 경로를 서술하되 약점을 부가하세요. 어미는 예측형(`~하지만, ~수 있다` / `~되지만, ~로 이어진다`)으로 작성하세요. + -> 예시: 생산이 늘면서 고용이 생겨 가계 소득이 단기적으로 상승하지만, 보조금이 끝나면 고용이 급감해서 소득이 다시 떨어질 수 있다 + -> 설명: 생산 확대→고용 증가→소득 상승이라는 직접 인과 경로를 서술하되 '보조금 종료 시 역전'이라는 약점을 부가함 + 첫번째 오답(핵심역전형)은 서술하는 메커니즘 자체는 경제학적으로 정확해야 합니다. 정확한 메커니즘이 인과 경로 치환을 "다른 관점에서 본 분석"처럼 보이게 하세요. + -> 예시: 임금 상승으로 가계 소득이 높아지지만, 기업의 인건비 부담으로 일부 업종에서 고용이 줄어 전체 소득 효과가 감소할 수 있다 + -> 설명: '임금 상승→인건비 부담→고용 축소'는 경제학적으로 정확한 메커니즘이지만, 주된 인과 경로(고용 증가)를 다른 경로(임금 상승)로 치환함 + 두번째 오답(조건누락형)은 사용하는 개념은 정확해야 합니다. 정확한 개념 서술이 범위 과잉이나 인과 비약을 "포괄적 분석"처럼 보이게 하세요. + -> 예시: 소비재 생산이 줄어 물가가 오르지만, 명목 소득 증가분이 물가 상승폭을 일부 상쇄해서 실질 소득은 유지된다 + -> 설명: '소비재 생산 감소→물가 상승→실질 소득'은 정확한 개념이지만, 단기 가계 소득이라는 범위를 넘어 물가까지 인과 비약함 + 세번째 오답(오개념혼동형)은 사용하는 용어와 메커니즘은 정확해야 합니다. 장기적 메커니즘의 정확한 서술이 시간 범위 오적용을 "구조적 분석"처럼 보이게 하세요. + -> 예시: 자본 투입 확대로 노동 생산성이 올라가지만, 자동화 전환 과정에서 구조적 실업이 생겨 소득 분배가 악화될 수 있다 + -> 설명: '자본 투입→생산성→자동화→구조적 실업'은 정확한 메커니즘이지만, 장기적 메커니즘을 '단기적 영향'이라는 시간 범위에 오적용함 + + # Step 3 — 해설을 작성한다 + **bloomsLevel**: `"Bloom's 수준 — Step 2에서 선택한 전략: [평가하고자 하는 것]"` 형식으로 기입한다. + + **정답 해설 (correct: true)** + - `**정답 설명**`: STEP2에 제시된 정답 설명 부분을 그대로 작성하세요 + - `**근거**`: 정답인 이유를 페이지 번호와 함께 인용. 구조: ~하므로 정답이다. [Np] > "강의노트 원문 인용", [Np] > "강의노트 원문 인용" + **오답 해설 (correct: false)** - `오답 유형`: [조건 누락형 / 범위 과잉형 / 오개념 혼동형 / 핵심 역전형] 중 하나 - - `진단`: 이 선택지를 고르는 학습자가 가진 오개념 한 줄 명시 - - `교정`: 올바른 개념·범위·조건으로 바로잡기 + - `오답 설명`: STEP2에 제시된 오답 설명 부분을 그대로 작성하세요 - `스스로 점검`: 해당 오개념을 유발하는 사고 패턴을 질문 형태로 제시 --- @@ -177,17 +202,20 @@ public class MultipleGuideLine { ```json { "questions": [{ - "content": "한 기업이 새 데이터센터를 구축하려 한다. **초기 구축 비용 절감**을 우선하되 **장기 운영 안정성**도 일정 수준 이상 유지해야 할 때, 다음 중 두 기준을 가장 균형 있게 충족하는 인프라 전략은?", + "content": "한 기업이 새 데이터센터를 구축하려 한다. **초기 구축 비용 절감**을 우선하되 **장기 운영 안정성**도 일정 수준 이상 유지해야 할 때, 가장 적절한 인프라 전략은?", "referencedPages": [25, 26], - "bloomsLevel": "Evaluate — 패턴 A: 초기 구축 비용 절감과 장기 운영 안정성이라는 두 기준이 충돌하는 상황에서 균형점을 판단하는지 평가함", + "bloomsLevel": "Evaluate — 패턴 A: 초기 구축 비용 절감과 장기 운영 안정성이라는 두 기준이 충돌하는 상황에서 적절한 전략을 판단하는지 평가함", "selections": [ - {"content": "온프레미스와 퍼블릭 클라우드를 혼합하여 초기 투자를 분산하고 핵심 서비스의 가용성을 확보한다", "correct": true, "explanation": "- **정답 추론**: 하이브리드 접근은 초기 비용(퍼블릭 클라우드로 분산)과 장기 안정성(핵심은 온프레미스)을 동시에 충족합니다.\\n- **근거**: [25p] > \\"하이브리드 모델은 비용과 안정성 간 균형점을 제공한다\\"\\n[30p] > \\"퍼블릭 클라우드의 장점\\"\\n- **스스로 점검**: 비용만 또는 안정성만 보고 판단하지 않았나요?"}, - {"content": "퍼블릭 클라우드를 전면 도입하여 초기 하드웨어 투자를 제거하고 온디맨드 확장성으로 초기 구축 비용 절감을 달성한다", "correct": false, "explanation": "- **오답 유형**: [조건 누락형]\\n- **진단**: 초기 비용 절감 기준은 충족하나, 벤더 종속으로 인한 SLA 변동 리스크로 장기 운영 안정성 기준을 충족하지 못함\\n- **교정**: 장기 운영 안정성에는 비용 예측 가능성과 벤더 독립성도 포함됨\\n- **스스로 점검**: 초기 비용 절감만으로 두 기준이 모두 충족된다고 판단하지 않았나요?"}, - {"content": "온프레미스 인프라를 전면 구축하고 이중화 구성으로 핵심 서비스의 가용성을 높여 장기 운영 안정성을 확보한다", "correct": false, "explanation": "- **오답 유형**: [조건 누락형]\\n- **진단**: 장기 운영 안정성 기준은 충족하나, 초기 하드웨어 구축 비용이 높아 초기 구축 비용 절감 기준을 충족하지 못함\\n- **교정**: 초기 구축 비용 절감이라는 명시된 기준을 직접적으로 충족하지 못함\\n- **스스로 점검**: '장기적으로 상쇄'라는 논리가 단기 기준을 대체할 수 있을까요?"}, - {"content": "서버리스 아키텍처를 전면 도입하여 유휴 자원 비용을 제거하고 사용량 기반 과금으로 초기 구축 비용과 장기 운영 비용을 동시에 절감한다", "correct": false, "explanation": "- **오답 유형**: [범위 과잉형]\\n- **진단**: 비용 절감 주장은 참이나, 복잡한 상태 관리 워크로드에서 cold start 지연 등으로 장기 운영 안정성 기준을 충족하지 못함\\n- **교정**: 서버리스는 특정 워크로드에 적합하며, 전면 도입은 복잡한 워크로드의 안정성을 저해할 수 있음\\n- **스스로 점검**: 특정 기술의 비용 장점을 전체 인프라 전략의 안정성 보장으로 확대 적용하지 않았나요?"} + {"content": "핵심 서비스는 온프레미스에 유지하고 비핵심 워크로드만 퍼블릭 클라우드에 배치해서 초기 투자를 분산하는 대신, 이중 관리 체계로 운영 인력이 늘고 장애 원인 추적이 지연되는 걸 감수한다", "correct": true, "explanation": "- **정답 설명**: 온프레미스↔클라우드 분리 배치로 두 기준(비용 절감+운영 안정성)을 동시에 다루되, '이중 관리 체계로 운영 인력 증가와 장애 추적 지연'이라는 실무적 약점을 부가함\\n- **근거**: 핵심 서비스를 온프레미스에 유지하면 장기 안정성을 확보하고, 비핵심을 클라우드에 배치하면 초기 투자가 분산되므로 정답이다. [25p] > \\"핵심-비핵심 분리 배치는 비용 절감과 안정성 확보를 동시에 도모할 수 있다\\", [26p] > \\"클라우드 전환 시 비핵심 워크로드부터 단계적으로 이전하는 것이 리스크를 줄인다\\""}, + {"content": "퍼블릭 클라우드 중심으로 전환해서 초기 투자를 절감하고 탄력적 확장을 확보하는 대신, 장기 벤더 종속으로 SLA가 바뀌면 운영 안정성이 흔들리는 걸 감수한다", "correct": false, "explanation": "- **오답 유형**: [조건 누락형]\\n- **오답 설명**: 비용 절감(기준 A)만 과잉 충족하고 운영 안정성(기준 B)을 누락함. '탄력적 확장'이라는 매력적 이점이 안정성 누락보다 먼저 눈에 들어옴\\n- **스스로 점검**: 초기 비용 절감이라는 한쪽 기준 충족만으로 두 기준이 모두 충족된다고 판단하지 않았나요?"}, + {"content": "온프레미스 인프라를 중심으로 이중화 구성해서 핵심 서비스의 가용성을 높이는 대신, 초기 장비 구매와 구축 비용이 크게 올라가는 걸 감수한다", "correct": false, "explanation": "- **오답 유형**: [조건 누락형]\\n- **오답 설명**: 운영 안정성(기준 B)만 과잉 충족하고 비용 절감(기준 A)을 누락함. '가용성 향상'이라는 현실적 장점이 비용 누락보다 먼저 눈에 들어옴\\n- **스스로 점검**: '장기적으로 상쇄'라는 논리가 단기 기준을 대체할 수 있을까요?"}, + {"content": "서버리스 아키텍처 비중을 높여서 유휴 자원 비용을 줄이고 사용량 기반 과금으로 운영 비용을 절감하는 대신, 상태 관리 워크로드에서 cold start 지연이 생기는 걸 감수한다", "correct": false, "explanation": "- **오답 유형**: [범위 과잉형]\\n- **오답 설명**: '서버리스'라는 정확한 기술 개념을 서술하지만, 특정 워크로드의 장점을 전체 인프라 전략으로 확대 적용함. 기술의 정확성이 범위 과잉을 정당화함\\n- **스스로 점검**: 특정 기술의 비용 장점에 설득되어 전체 인프라 전략의 안정성까지 보장된다고 확대 적용하지 않았나요?"} ] }] } ``` + + > **CRITICAL RULE**: 강의노트에 명시된 내용만 출제 근거로 사용한다. + > **CRITICAL RULE**: 오답 선택지의 길이를 제일 길게한다. """; } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleRequestPrompt.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleRequestPrompt.java index bbad7577..b3d19544 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleRequestPrompt.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/multiple/prompt/MultipleRequestPrompt.java @@ -25,8 +25,7 @@ private static String buildBase(List referencePages, int quizCount) { private static final String APPLIED_INSTRUCTION_SPEC = """ # 사용자 지시 반영 - - 지시가 특정 형식을 요청하면, 그 형식에 대응하는 전략이 존재하면 해당 전략을 우선 선택한다. - - 대응 전략이 없는 형식은 요청 형식에 맞게 질문문을 자유롭게 구성다. + - 사용자 지시에 맞는 패턴과 지식 유형을 Step 1-2의 테이블에서 찾아 해당 few-shot을 따른다. 대응 패턴이 없으면 자유롭게 구성한다. # 사용자 지시 반영 결과 기록 - 사용자 지시를 반영한 내용을 `appliedInstruction` 필드에 1~2문장으로 기록한다. diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/OXQuizOrchestrator.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/OXQuizOrchestrator.java index ba8d9877..5e501e97 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/OXQuizOrchestrator.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/OXQuizOrchestrator.java @@ -173,7 +173,7 @@ public int generateQuiz(GenerationRequestToAI request) { if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) { throw new GeminiInfraException("Gemini 블로킹 컨텍스트 오류", e); } - log.warn("OX 스트리밍 타임아웃 (6분 초과): 생성된 문항 {}개 유지", delivered.get()); + log.warn("[OX 스트리밍 타임아웃] 6분 초과, 생성된 문항 유지 deliveredCount={}", delivered.get()); metricsRecorder.recordStreamingTimeout("OX"); metricsRecorder.recordRequestDuration( 1, @@ -186,7 +186,7 @@ public int generateQuiz(GenerationRequestToAI request) { throw e; } catch (Exception e) { if (delivered.get() > 0) { - log.warn("OX 스트리밍 중 오류 발생이나 {}개 문항은 전달됨. 부분 성공 처리.", delivered.get(), e); + log.warn("[OX 부분 성공] 스트리밍 중 오류 발생이나 문항 전달됨 deliveredCount={}", delivered.get(), e); metricsRecorder.recordStreamingTimeout("OX"); metricsRecorder.recordRequestDuration( 1, diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/prompt/OXRequestPrompt.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/prompt/OXRequestPrompt.java index a051bbaa..95f7a05f 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/prompt/OXRequestPrompt.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/ox/prompt/OXRequestPrompt.java @@ -14,8 +14,7 @@ public class OXRequestPrompt { private static final String APPLIED_INSTRUCTION_SPEC = """ # 사용자 지시 반영 - - 지시가 특정 형식을 요청하면, 그 형식에 대응하는 전략이 존재하면 해당 전략을 우선 선택한다. - - 대응 전략이 없는 형식은 요청 형식에 맞게 질문문을 자유롭게 구성다. + - 사용자 지시에 맞는 패턴과 지식 유형을 Step 1-2의 테이블에서 찾아 해당 few-shot을 따른다. 대응 패턴이 없으면 자유롭게 구성한다. # 사용자 지시 반영 결과 기록 - 사용자 지시를 반영한 내용을 `appliedInstruction` 필드에 1~2문장으로 기록한다. @@ -80,7 +79,7 @@ public static String generate(List referencePages, int quizCount) { [문항별 상세 계획] %s - - **[계획 엄수]** 위 계획에 명시된 문항별 정답(O/X)과 수준(Understand/Apply), 그리고 X 문항의 변조 유형을 반드시 준수하여 생성하세요.""" + - **[계획 엄수]** 위 계획에 명시된 문항별 정답(O/X)을 반드시 준수하여 생성하세요. 정답이 O인 문항은 참 진술문을, X인 문항은 거짓 진술문을 작성하세요.""" .formatted(quizCount, plan.toString().strip()); } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiCacheService.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiCacheService.java index 45eed1c8..360d558d 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiCacheService.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiCacheService.java @@ -79,6 +79,43 @@ public CacheInfo createCache(String fileUri, String strategyValue, String langua } } + /** + * PDF 파일만 캐시에 등록한다 (시스템 프롬프트 제외). 3-chunk 병렬 생성에서 각 청크가 서로 다른 시스템 프롬프트를 사용하되 동일 PDF를 공유할 때 활용한다. + */ + public CacheInfo createPdfOnlyCache(String fileUri) { + try { + Content pdfContent = + Content.builder() + .role("user") + .parts( + Part.builder() + .fileData( + FileData.builder().fileUri(fileUri).mimeType("application/pdf").build()) + .build()) + .build(); + + CachedContentRequest request = + CachedContentRequest.builder() + .model(model) + .contents(List.of(pdfContent)) + .ttl(Duration.ofMinutes(10)) + .build(); + + GoogleGenAiCachedContent cache = cachedContentService.create(request); + String cacheName = cache.getName(); + long tokenCount = + cache.getUsageMetadata() != null && cache.getUsageMetadata().totalTokenCount().isPresent() + ? cache.getUsageMetadata().totalTokenCount().get() + : 0; + + log.info("PDF 전용 캐시 생성 완료: name={}, tokenCount={}", cacheName, tokenCount); + return new CacheInfo(cacheName, tokenCount); + } catch (Exception e) { + throw new CustomException( + ExceptionMessage.AI_SERVER_RESPONSE_ERROR, "PDF 캐시 생성 실패: fileUri=" + fileUri, e); + } + } + public void deleteCache(String cacheName) { if (cacheName == null) { return; @@ -88,10 +125,10 @@ public void deleteCache(String cacheName) { if (deleted) { log.info("캐시 삭제 완료: name={}", cacheName); } else { - log.warn("캐시 삭제 실패: name={}", cacheName); + log.warn("[Gemini 캐시 삭제 실패] 캐시 삭제 실패 name={}", cacheName); } } catch (Exception e) { - log.warn("캐시 삭제 중 오류: name={}, error={}", cacheName, e.getMessage()); + log.warn("[Gemini 캐시 삭제 오류] 캐시 삭제 중 예외 발생 name={}", cacheName, e); } } } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiChatService.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiChatService.java index eb242195..ed5151e1 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiChatService.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiChatService.java @@ -117,7 +117,7 @@ public ParsedResult callAndParse( String json = chatResponse.getResult().getOutput().getText(); if (json == null || json.isBlank()) { - log.error("응답이 비어있습니다: pages={}", pages); + log.error("[AI 응답 오류] Gemini 응답이 비어있음 pages={}", pages); return new ParsedResult(null, chunkCost); } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiFileServiceImpl.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiFileServiceImpl.java index c06db15a..4635af53 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiFileServiceImpl.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiFileServiceImpl.java @@ -148,7 +148,7 @@ public Optional awaitCachedFileMetadata(String cacheKey) { return Optional.of(metadata); } catch (CompletionException e) { uploadFutureCache.invalidate(cacheKey); - log.warn("캐시된 GCS 업로드 실패, 캐시 제거: key={}, error={}", cacheKey, e.getMessage()); + log.warn("[GCS 캐시 실패] 캐시된 업로드 실패, 캐시 제거 key={}", cacheKey, e); return Optional.empty(); } } @@ -189,10 +189,10 @@ public void deleteFile(String fileName) { if (deleted) { log.info("GCS 파일 삭제 완료: name={}", fileName); } else { - log.warn("GCS 파일 삭제 대상 없음: name={}", fileName); + log.warn("[GCS 파일 삭제] 삭제 대상 없음 name={}", fileName); } } catch (Exception e) { - log.warn("GCS 파일 삭제 실패 (무시): name={}, error={}", fileName, e.getMessage()); + log.warn("[GCS 파일 삭제 실패] 파일 삭제 실패 name={}", fileName, e); } } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingEssayQuestionExtractor.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingEssayQuestionExtractor.java index 5ee3d6fb..085f578e 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingEssayQuestionExtractor.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingEssayQuestionExtractor.java @@ -84,17 +84,17 @@ private void emitQuestion(String json) { question = objectMapper.readValue(json, GeminiEssayQuestion.class); } catch (Exception e) { log.warn( - "ESSAY JSON 파싱 실패: {} | JSON 길이: {} | JSON 앞 300자: {}", - e.getMessage(), + "[ESSAY JSON 파싱 실패] 문항 JSON 역직렬화 실패 jsonLength={} jsonPreview={}", json.length(), - json.length() > 300 ? json.substring(0, 300) : json); + json.length() > 300 ? json.substring(0, 300) : json, + e); return; } try { questionCount++; questionConsumer.accept(question); } catch (Exception e) { - log.warn("ESSAY 문항 처리 실패: {}", e.getMessage(), e); + log.warn("[ESSAY 문항 처리 실패] 문항 소비자 호출 중 오류 발생", e); } } } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingQuestionExtractor.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingQuestionExtractor.java index 97dbc943..38744aa2 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingQuestionExtractor.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/StreamingQuestionExtractor.java @@ -90,17 +90,17 @@ private void emitQuestion(String json) { question = objectMapper.readValue(json, GeminiQuestion.class); } catch (Exception e) { log.warn( - "JSON 파싱 실패: {} | JSON 길이: {} | JSON 앞 300자: {}", - e.getMessage(), + "[JSON 파싱 실패] 문항 JSON 역직렬화 실패 jsonLength={} jsonPreview={}", json.length(), - json.length() > 300 ? json.substring(0, 300) : json); + json.length() > 300 ? json.substring(0, 300) : json, + e); return; } try { questionCount++; questionConsumer.accept(question); } catch (Exception e) { - log.warn("문항 처리 실패: {}", e.getMessage(), e); + log.warn("[문항 처리 실패] 문항 소비자 호출 중 오류 발생", e); } } } diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/util/PdfUtils.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/util/PdfUtils.java index 50bcd0a0..e43b0187 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/util/PdfUtils.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/util/PdfUtils.java @@ -61,12 +61,13 @@ public SlicedPdf extractPages(Path sourcePdf, List pages) throws IOExce targetDoc.addPage(sourceDoc.getPage(pageNum - 1)); successPages.add(pageNum); } else { - log.warn("유효하지 않은 페이지 번호 무시: {} (총 페이지: {})", pageNum, totalPages); + log.warn( + "[PDF 페이지 범위 초과] 유효하지 않은 페이지 번호 무시 pageNum={} totalPages={}", pageNum, totalPages); } } if (targetDoc.getNumberOfPages() == 0) { - log.warn("추출된 페이지가 없어 원본을 그대로 반환합니다."); + log.warn("[PDF 추출 실패] 추출된 페이지가 없어 원본 반환"); return new SlicedPdf(sourcePdf, List.of()); } @@ -90,7 +91,7 @@ public void deleteTempFile(Path tempFile) { log.debug("임시 파일 삭제 완료: {}", tempFile); } } catch (IOException e) { - log.warn("임시파일 삭제 실패: {} - {}", tempFile, e.getMessage()); + log.warn("[파일 정리 실패] 임시파일 삭제 실패 path={}", tempFile, e); } } } diff --git a/modules/quiz-history/impl/src/main/java/com/icc/qasker/quizhistory/service/EssayGradeService.java b/modules/quiz-history/impl/src/main/java/com/icc/qasker/quizhistory/service/EssayGradeService.java index 328f409e..ec7526ee 100644 --- a/modules/quiz-history/impl/src/main/java/com/icc/qasker/quizhistory/service/EssayGradeService.java +++ b/modules/quiz-history/impl/src/main/java/com/icc/qasker/quizhistory/service/EssayGradeService.java @@ -125,7 +125,7 @@ private void saveLogAsync( essayGradeLogRepository.save(log); } catch (Exception e) { log.warn( - "서술형 채점 로그 저장 실패: problemSetId={}, problemNumber={}", + "[채점 로그 저장 실패] 서술형 채점 로그 저장 실패 problemSetId={} problemNumber={}", problemSetId, problemNumber, e); diff --git a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationCommandServiceImpl.java b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationCommandServiceImpl.java index bf897269..a34ca4e0 100644 --- a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationCommandServiceImpl.java +++ b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationCommandServiceImpl.java @@ -89,6 +89,7 @@ private void processGenerationAsync( AtomicInteger atomicGeneratedCount = new AtomicInteger(0); AtomicInteger numberCounter = new AtomicInteger(1); AtomicLong firstConsumerNanos = new AtomicLong(0); + AtomicLong lastConsumerNanos = new AtomicLong(0); ReentrantLock consumerLock = new ReentrantLock(); GenerationRequestToAI requestToAI = @@ -106,7 +107,7 @@ private void processGenerationAsync( // 1. 전송용 DTO로 변환 ProblemSetGeneratedEvent problemSet = AIProblemSetMapper.toEvent(aiProblemSet); if (CollectionUtils.isEmpty(problemSet.getQuiz())) { - log.warn("빈 배치 수신, 건너뜀: sessionId={}", sessionId); + log.warn("[AI 생성 스킵] 빈 배치 수신 sessionId={}", sessionId); return; } @@ -176,6 +177,7 @@ private void processGenerationAsync( atomicGeneratedCount.addAndGet(quizViews.size()); firstConsumerNanos.compareAndSet(0, System.nanoTime()); + lastConsumerNanos.set(System.nanoTime()); } finally { consumerLock.unlock(); } @@ -186,7 +188,7 @@ private void processGenerationAsync( try { maxChunkCount = aiServerAdapter.streamRequest(requestToAI); } catch (Exception e) { - log.error("생성 중 오류 발생: sessionId={}", sessionId, e); + log.error("[AI 생성 실패] 퀴즈 생성 중 오류 발생 sessionId={}", sessionId, e); finalizeError( sessionId, problemSetId, @@ -199,6 +201,8 @@ private void processGenerationAsync( int quizCount = request.quizCount(); long ttfqMs = firstConsumerNanos.get() > 0 ? (firstConsumerNanos.get() - startNanos) / 1_000_000 : -1; + long ttlqMs = + lastConsumerNanos.get() > 0 ? (lastConsumerNanos.get() - startNanos) / 1_000_000 : -1; // 요청/생성/실패 문제 수 메트릭 기록 (finalize 결과와 무관하게 항상 실행) resultRecorder.recordQuizCounts(request.quizType(), quizCount, generatedCount, maxChunkCount); @@ -210,18 +214,23 @@ private void processGenerationAsync( request.quizType(), ExceptionMessage.AI_GENERATION_FAILED.getMessage()); } else if (generatedCount == quizCount) { - finalizeSuccess(sessionId, problemSetId, request.quizType(), generatedCount, ttfqMs); + finalizeSuccess(sessionId, problemSetId, request.quizType(), generatedCount, ttfqMs, ttlqMs); } else { finalizePartialSuccess( - sessionId, problemSetId, request.quizType(), generatedCount, quizCount, ttfqMs); + sessionId, problemSetId, request.quizType(), generatedCount, quizCount, ttfqMs, ttlqMs); } } private void finalizeSuccess( - String sessionId, Long problemSetId, QuizType quizType, long generatedCount, long ttfqMs) { + String sessionId, + Long problemSetId, + QuizType quizType, + long generatedCount, + long ttfqMs, + long ttlqMs) { quizCommandService.updateStatus(problemSetId, COMPLETED); notificationService.sendComplete(sessionId); - resultRecorder.recordSuccess(problemSetId, quizType, generatedCount, ttfqMs); + resultRecorder.recordSuccess(problemSetId, quizType, generatedCount, ttfqMs, ttlqMs); } private void finalizePartialSuccess( @@ -230,10 +239,12 @@ private void finalizePartialSuccess( QuizType quizType, long generatedCount, long quizCount, - long ttfqMs) { + long ttfqMs, + long ttlqMs) { quizCommandService.updateStatus(problemSetId, COMPLETED); notificationService.sendComplete(sessionId); - resultRecorder.recordPartialSuccess(problemSetId, quizType, generatedCount, quizCount, ttfqMs); + resultRecorder.recordPartialSuccess( + problemSetId, quizType, generatedCount, quizCount, ttfqMs, ttlqMs); } private void finalizeError( diff --git a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationResultRecorder.java b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationResultRecorder.java index 75ad71fe..14e204a7 100644 --- a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationResultRecorder.java +++ b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationResultRecorder.java @@ -54,14 +54,20 @@ public GenerationResultRecorder( } public void recordSuccess( - Long problemSetId, QuizType quizType, long generatedCount, long ttfqMs) { - slackNotifier.notifySuccess(problemSetId, quizType, generatedCount, ttfqMs); + Long problemSetId, QuizType quizType, long generatedCount, long ttfqMs, long ttlqMs) { + slackNotifier.notifySuccess(problemSetId, quizType, generatedCount, ttfqMs, ttlqMs); incrementOutcome("success", quizType); } public void recordPartialSuccess( - Long problemSetId, QuizType quizType, long generatedCount, long quizCount, long ttfqMs) { - slackNotifier.notifyPartialSuccess(problemSetId, quizType, generatedCount, quizCount, ttfqMs); + Long problemSetId, + QuizType quizType, + long generatedCount, + long quizCount, + long ttfqMs, + long ttlqMs) { + slackNotifier.notifyPartialSuccess( + problemSetId, quizType, generatedCount, quizCount, ttfqMs, ttlqMs); incrementOutcome("partial", quizType); } diff --git a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationSlackNotifier.java b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationSlackNotifier.java index 51b6a3cd..2f98a1e5 100644 --- a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationSlackNotifier.java +++ b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/GenerationSlackNotifier.java @@ -16,7 +16,7 @@ public class GenerationSlackNotifier { private final QAskerProperties qAskerProperties; public void notifySuccess( - Long problemSetId, QuizType quizType, long generatedCount, long ttfqMs) { + Long problemSetId, QuizType quizType, long generatedCount, long ttfqMs, long ttlqMs) { String encodedId = hashUtil.encode(problemSetId); String quizUrl = buildQuizUrl(encodedId); slackNotifier.asyncNotifyText( @@ -26,12 +26,24 @@ public void notifySuccess( 퀴즈 타입: %s 문제 수: %d TTFQ: %s + TTLQ: %s """ - .formatted(quizUrl, encodedId, quizType, generatedCount, formatTtfq(ttfqMs))); + .formatted( + quizUrl, + encodedId, + quizType, + generatedCount, + formatTime(ttfqMs), + formatTime(ttlqMs))); } public void notifyPartialSuccess( - Long problemSetId, QuizType quizType, long generatedCount, long quizCount, long ttfqMs) { + Long problemSetId, + QuizType quizType, + long generatedCount, + long quizCount, + long ttfqMs, + long ttlqMs) { String encodedId = hashUtil.encode(problemSetId); String quizUrl = buildQuizUrl(encodedId); slackNotifier.asyncNotifyText( @@ -41,9 +53,16 @@ public void notifyPartialSuccess( 퀴즈 타입: %s 생성된 문제 수: %d개 / 총 문제 수: %d개 TTFQ: %s + TTLQ: %s """ .formatted( - quizUrl, encodedId, quizType, generatedCount, quizCount, formatTtfq(ttfqMs))); + quizUrl, + encodedId, + quizType, + generatedCount, + quizCount, + formatTime(ttfqMs), + formatTime(ttlqMs))); } public void notifyError(Long problemSetId, String errorMessage) { @@ -62,7 +81,7 @@ private String buildQuizUrl(String encodedId) { return qAskerProperties.getFrontendDeployUrl() + "/quiz/" + encodedId; } - private String formatTtfq(long ttfqMs) { + private String formatTime(long ttfqMs) { if (ttfqMs < 0) { return "N/A"; } diff --git a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/SseNotificationServiceImpl.java b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/SseNotificationServiceImpl.java index 2a596274..fd5a7087 100644 --- a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/SseNotificationServiceImpl.java +++ b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/generation/SseNotificationServiceImpl.java @@ -74,7 +74,7 @@ public void sendConnected(String sessionId) { try { emitter.send(SseEmitter.event().name("connected").data("hello")); } catch (IOException e) { - log.warn("클라이언트에게 연결 중 에러 사유: {}", e.getMessage()); + log.warn("[SSE 연결 실패] 클라이언트 연결 중 에러 발생", e); emitter.completeWithError(e); } } @@ -90,13 +90,13 @@ public void sendConnected(String sessionId) { }); emitter.onTimeout( () -> { - log.warn("SSE 연결 타임아웃 (Timeout): {}", sessionId); + log.warn("[SSE 타임아웃] SSE 연결 타임아웃 sessionId={}", sessionId); emitterMap.remove(sessionId, emitter); sseTimeoutCounter.increment(); }); emitter.onError( (e) -> { - log.warn("SSE 연결 에러 발생 (Session: {}): {}", sessionId, e.getMessage()); + log.warn("[SSE 연결 에러] SSE 연결 중 에러 발생 sessionId={}", sessionId); emitterMap.remove(sessionId, emitter); }); return emitter; @@ -109,7 +109,7 @@ public void sendCreatedMessageWithId(String sessionId, String eventId, Object da try { emitter.send(SseEmitter.event().id(eventId).name("created").data(data)); } catch (IOException e) { - log.warn("클라이언트에게 전송 중 에러 발생: {} 사유: {}", data, e.getMessage()); + log.warn("[SSE 전송 실패] 클라이언트에게 데이터 전송 중 에러 발생", e); emitter.completeWithError(e); } } diff --git a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/upload/FileUploadService.java b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/upload/FileUploadService.java index f0d4bd7c..39f9b843 100644 --- a/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/upload/FileUploadService.java +++ b/modules/quiz-make/impl/src/main/java/com/icc/qasker/quizmake/service/upload/FileUploadService.java @@ -94,7 +94,7 @@ public FileUploadResponse upload(MultipartFile file) { (metadata, ex) -> { deleteQuietly(geminiCopy); if (ex != null) { - log.warn("Gemini 백그라운드 업로드 실패 (퀴즈 생성 시 재시도): {}", ex.getMessage()); + log.warn("[Gemini 업로드 실패] 백그라운드 업로드 실패, 퀴즈 생성 시 재시도", ex); } else { log.info("Gemini 백그라운드 업로드 완료: name={}", metadata.name()); } @@ -136,7 +136,7 @@ private void deleteQuietly(Path path) { try { Files.deleteIfExists(path); } catch (Exception e) { - log.warn("임시 파일 삭제 실패: {}", path, e); + log.warn("[파일 정리 실패] 임시 파일 삭제 실패 path={}", path, e); } } } diff --git a/modules/quiz-set/impl/src/main/java/com/icc/qasker/quizset/scheduler/StaleGenerationRecoveryScheduler.java b/modules/quiz-set/impl/src/main/java/com/icc/qasker/quizset/scheduler/StaleGenerationRecoveryScheduler.java index dbd72ecc..787f6613 100644 --- a/modules/quiz-set/impl/src/main/java/com/icc/qasker/quizset/scheduler/StaleGenerationRecoveryScheduler.java +++ b/modules/quiz-set/impl/src/main/java/com/icc/qasker/quizset/scheduler/StaleGenerationRecoveryScheduler.java @@ -34,7 +34,7 @@ public void deleteStaleProblemSets() { if (!staleList.isEmpty()) { problemSetRepository.deleteAll(staleList); - log.warn("방치된 ProblemSet {}건 삭제 (FAILED + GENERATING 10분 초과)", staleList.size()); + log.warn("[방치 ProblemSet 정리] FAILED/GENERATING 10분 초과 삭제 count={}", staleList.size()); } } }