From fc12da2e359cf54398a30d4e48547996e26a03d7 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:17:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[ICC-325]=20=EA=B1=B4=EC=9D=98=ED=95=A8=20A?= =?UTF-8?q?PI=EC=99=80=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: 피드백 게시판 기능 구현 * ✨ feat: 객관식 퀴즈 생성 프롬프트 가이드라인 업데이트 * ♻️ refactor: OCI 메트릭 세분화 및 Gemini 설정 업데이트 * 🐛 fix: OX 퀴즈 선택지 개수 수정 * 🔧 chore: .gitignore에 Gemini CLI 경로 추가 * [ICC-325] 안쓰는 속성 제거 * [ICC-325] 최대 출력 토큰 수 조정 --- .gitignore | 3 + app/src/main/resources/application-test.yml | 4 + app/src/main/resources/config/ai-setting.yml | 4 +- .../migration/V7__create_feedback_board.sql | 7 + .../controller/FeedbackBoardController.java | 34 ++++ .../controller/dto/PostFeedbackRequest.java | 5 + .../qasker/board/entity/FeedbackBoard.java | 35 ++++ .../repository/FeedbackBoardRepository.java | 8 + .../qasker/board/service/FeedbackService.java | 20 +++ .../service/OciObjectStorageServiceImpl.java | 17 +- .../ai/properties/QAskerAiProperties.java | 7 - .../multiple/prompt/MultipleGuideLine.java | 152 ++++++++++++++---- .../ai/service/ox/OXQuizOrchestrator.java | 2 +- .../support/GeminiMetricsRecorder.java | 8 +- 14 files changed, 255 insertions(+), 51 deletions(-) create mode 100644 app/src/main/resources/db/migration/V7__create_feedback_board.sql create mode 100644 modules/board/impl/src/main/java/com/icc/qasker/board/controller/FeedbackBoardController.java create mode 100644 modules/board/impl/src/main/java/com/icc/qasker/board/controller/dto/PostFeedbackRequest.java create mode 100644 modules/board/impl/src/main/java/com/icc/qasker/board/entity/FeedbackBoard.java create mode 100644 modules/board/impl/src/main/java/com/icc/qasker/board/repository/FeedbackBoardRepository.java create mode 100644 modules/board/impl/src/main/java/com/icc/qasker/board/service/FeedbackService.java diff --git a/.gitignore b/.gitignore index ebef0c34..5430ea38 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ shrimp-rules.md shrimp_data docs .gstack/ + +# Gemini CLI +.gemini diff --git a/app/src/main/resources/application-test.yml b/app/src/main/resources/application-test.yml index 3965d7d0..0c9e2491 100644 --- a/app/src/main/resources/application-test.yml +++ b/app/src/main/resources/application-test.yml @@ -67,6 +67,10 @@ spring: thinking-budget: 0 q-asker: + ai: + gcs: + bucket-name: ci-dummy + hashid: salt: ci-dummy-salt min-length: 8 diff --git a/app/src/main/resources/config/ai-setting.yml b/app/src/main/resources/config/ai-setting.yml index 718dda11..fbbcdf03 100644 --- a/app/src/main/resources/config/ai-setting.yml +++ b/app/src/main/resources/config/ai-setting.yml @@ -12,11 +12,11 @@ spring: top-p: 0.6 model: gemini-3-flash-preview thinking-budget: 0 + max-output-tokens: 25000 + q-asker: ai: chat-timeout-ms: 180000 - equalization-model: gemini-2.5-flash-lite - ox-thinking-level: LOW chunk: max-count-variants: 5 diff --git a/app/src/main/resources/db/migration/V7__create_feedback_board.sql b/app/src/main/resources/db/migration/V7__create_feedback_board.sql new file mode 100644 index 00000000..c59872ab --- /dev/null +++ b/app/src/main/resources/db/migration/V7__create_feedback_board.sql @@ -0,0 +1,7 @@ +CREATE TABLE feedback_board +( + feedback_board_id BIGINT AUTO_INCREMENT PRIMARY KEY KEY, + user_id VARCHAR(255), + content LONGTEXT, + created_at DATETIME(6) NOT NULL +); diff --git a/modules/board/impl/src/main/java/com/icc/qasker/board/controller/FeedbackBoardController.java b/modules/board/impl/src/main/java/com/icc/qasker/board/controller/FeedbackBoardController.java new file mode 100644 index 00000000..3704c7da --- /dev/null +++ b/modules/board/impl/src/main/java/com/icc/qasker/board/controller/FeedbackBoardController.java @@ -0,0 +1,34 @@ +package com.icc.qasker.board.controller; + +import com.icc.qasker.board.controller.dto.PostFeedbackRequest; +import com.icc.qasker.board.service.FeedbackService; +import com.icc.qasker.global.annotation.RateLimit; +import com.icc.qasker.global.annotation.UserId; +import com.icc.qasker.global.ratelimit.RateLimitTier; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Board", description = "피드백 관련 API") +@RestController +@RequestMapping("/feedback") +@RequiredArgsConstructor +public class FeedbackBoardController { + + private final FeedbackService feedbackService; + + @Operation(summary = "피드백을 작성한다") + @RateLimit(RateLimitTier.WRITE) + @PostMapping + public ResponseEntity postFeedback( + @UserId String userId, @Valid @RequestBody PostFeedbackRequest request) { + feedbackService.postFeedback(userId, request); + return ResponseEntity.accepted().build(); + } +} diff --git a/modules/board/impl/src/main/java/com/icc/qasker/board/controller/dto/PostFeedbackRequest.java b/modules/board/impl/src/main/java/com/icc/qasker/board/controller/dto/PostFeedbackRequest.java new file mode 100644 index 00000000..b2eaef12 --- /dev/null +++ b/modules/board/impl/src/main/java/com/icc/qasker/board/controller/dto/PostFeedbackRequest.java @@ -0,0 +1,5 @@ +package com.icc.qasker.board.controller.dto; + +import jakarta.validation.constraints.NotBlank; + +public record PostFeedbackRequest(@NotBlank(message = "피드백이 존재하지 않습니다.") String content) {} diff --git a/modules/board/impl/src/main/java/com/icc/qasker/board/entity/FeedbackBoard.java b/modules/board/impl/src/main/java/com/icc/qasker/board/entity/FeedbackBoard.java new file mode 100644 index 00000000..c8a65d01 --- /dev/null +++ b/modules/board/impl/src/main/java/com/icc/qasker/board/entity/FeedbackBoard.java @@ -0,0 +1,35 @@ +package com.icc.qasker.board.entity; + +import com.icc.qasker.global.entity.CreatedAt; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "feedback_board") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class FeedbackBoard extends CreatedAt { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long feedbackBoardId; + + @Column(name = "user_id") + private String userId; + + @Column(name = "content", columnDefinition = "LONGTEXT") + private String content; + + @Builder + public FeedbackBoard(String userId, String content) { + this.userId = userId; + this.content = content; + } +} diff --git a/modules/board/impl/src/main/java/com/icc/qasker/board/repository/FeedbackBoardRepository.java b/modules/board/impl/src/main/java/com/icc/qasker/board/repository/FeedbackBoardRepository.java new file mode 100644 index 00000000..e096e0ad --- /dev/null +++ b/modules/board/impl/src/main/java/com/icc/qasker/board/repository/FeedbackBoardRepository.java @@ -0,0 +1,8 @@ +package com.icc.qasker.board.repository; + +import com.icc.qasker.board.entity.FeedbackBoard; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FeedbackBoardRepository extends JpaRepository {} diff --git a/modules/board/impl/src/main/java/com/icc/qasker/board/service/FeedbackService.java b/modules/board/impl/src/main/java/com/icc/qasker/board/service/FeedbackService.java new file mode 100644 index 00000000..10dd4a62 --- /dev/null +++ b/modules/board/impl/src/main/java/com/icc/qasker/board/service/FeedbackService.java @@ -0,0 +1,20 @@ +package com.icc.qasker.board.service; + +import com.icc.qasker.board.controller.dto.PostFeedbackRequest; +import com.icc.qasker.board.entity.FeedbackBoard; +import com.icc.qasker.board.repository.FeedbackBoardRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FeedbackService { + + private final FeedbackBoardRepository feedbackBoardRepository; + + public void postFeedback(String userId, PostFeedbackRequest request) { + FeedbackBoard feedbackBoard = + FeedbackBoard.builder().userId(userId).content(request.content()).build(); + feedbackBoardRepository.save(feedbackBoard); + } +} diff --git a/modules/oci/impl/src/main/java/com/icc/qasker/oci/service/OciObjectStorageServiceImpl.java b/modules/oci/impl/src/main/java/com/icc/qasker/oci/service/OciObjectStorageServiceImpl.java index 28e23854..ab86c68e 100644 --- a/modules/oci/impl/src/main/java/com/icc/qasker/oci/service/OciObjectStorageServiceImpl.java +++ b/modules/oci/impl/src/main/java/com/icc/qasker/oci/service/OciObjectStorageServiceImpl.java @@ -27,7 +27,8 @@ public class OciObjectStorageServiceImpl implements ObjectStorageService { private final CdnProperties cdnProperties; private final OciObjectStorageProperties ociProperties; private final UploadManager uploadManager; - private final Timer uploadTimer; + private final Timer pdfUploadTimer; + private final Timer imageUploadTimer; public OciObjectStorageServiceImpl( CdnProperties cdnProperties, @@ -37,16 +38,22 @@ public OciObjectStorageServiceImpl( this.cdnProperties = cdnProperties; this.ociProperties = ociProperties; this.uploadManager = uploadManager; - this.uploadTimer = + this.pdfUploadTimer = Timer.builder("file.upload.oci.duration") - .description("OCI Object Storage 파일 업로드 소요 시간") + .description("OCI Object Storage PDF 업로드 소요 시간") + .tag("type", "pdf") + .register(registry); + this.imageUploadTimer = + Timer.builder("file.upload.oci.duration") + .description("OCI Object Storage 이미지 업로드 소요 시간") + .tag("type", "image") .register(registry); } @Override public String uploadImage( InputStream inputStream, long contentLength, String contentType, String originalFileName) { - return uploadTimer.record( + return imageUploadTimer.record( () -> { String extension = originalFileName.substring(originalFileName.lastIndexOf(".")); String objectName = "images/" + UUID.randomUUID() + extension; @@ -77,7 +84,7 @@ public String uploadImage( @Override public String uploadPdf(Path pdfFile, String originalFileName) { - return uploadTimer.record( + return pdfUploadTimer.record( () -> { String uuid = UUID.randomUUID().toString(); String objectName = uuid + ".pdf"; diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/properties/QAskerAiProperties.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/properties/QAskerAiProperties.java index 21d04ba8..1d43bd97 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/properties/QAskerAiProperties.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/properties/QAskerAiProperties.java @@ -5,7 +5,6 @@ import java.util.concurrent.atomic.AtomicInteger; import lombok.Getter; import lombok.Setter; -import org.springframework.ai.google.genai.common.GoogleGenAiThinkingLevel; import org.springframework.boot.context.properties.ConfigurationProperties; @Getter @@ -16,12 +15,6 @@ public class QAskerAiProperties { /** Gemini Chat API 타임아웃 (ms) */ private int chatTimeoutMs = 90_000; - /** 선택지 균등화에 사용할 모델 (미설정 시 기본 모델 사용) */ - private String equalizationModel; - - /** OX 퀴즈 생성 시 thinking level 오버라이드 (미설정 시 글로벌 기본값 사용) */ - private GoogleGenAiThinkingLevel oxThinkingLevel; - /** 청크 분할 설정 */ private Chunk chunk = new Chunk(); 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 02140849..35f72557 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 @@ -29,7 +29,7 @@ public class MultipleGuideLine { #### ▶ 상위 수준: Evaluate (평가) — 전체의 65~75% - **패턴 A: Critiquing (기준 충돌 판단) — 주력 패턴** + **패턴 A: Critiquing (기준 충돌 판단)** - **인지 체인 (4점 문항 설계)**: 단일 판단(3점)을 넘어, "오류 탐지 → 바로잡기 → 기준 적용 판단"처럼 2~3단계 인지 작업을 결합하세요. - 질문문에 **서로 양립하기 어려운 기준 2개**를 **볼드로** 명시하고, 그 충돌을 해결하는 판단을 요구합니다. - **실질적 기준 간 긴장 관계 필수**: 두 기준은 반드시 역방향 관계여야 합니다. 기준1을 극대화하면 기준2가 악화되는 관계여야 합니다. @@ -45,38 +45,50 @@ public class MultipleGuideLine { - ⑤ 분류/배제형: "**안정성** 측면에서 부적절한 선택지를 배제할 때, 가장 적합한 것은?" - (위 기준명은 예시입니다. 강의노트에 맞는 구체적 기준명으로 교체하세요.) - **패턴 B: Checking (오류 탐지) — 보조 패턴** - 학습자가 2개↑ 요소를 교차 대조하여 모순/오류/비일관성을 탐지하도록 설계합니다. - **다양한 자료 형식을 사용하세요**: 비교표, mermaid 다이어그램, 서술문 나열 등을 골고루 혼합하세요. + **패턴 B: Checking (오류 탐지)** + 자료에 삽입된 단일 오류를 교차 대조로 발견하고, 어떤 항목이 잘못되었으며 어떻게 수정해야 하는가를 묻습니다. 오류를 바로잡는 것 자체가 정답입니다. 선택지 4개는 오류 식별 및 수정 방법을 기준으로 설계합니다. - - 핵심 설계 원칙: 학습자가 자료를 "읽기만" 해서는 답을 구할 수 없어야 합니다. 여러 요소를 **교차 대조**하여 모순이나 비일관성을 추론해야 합니다. + **다양한 자료 형식을 사용하세요**: 비교표, mermaid 다이어그램, 서술문 나열 등을 골고루 혼합하세요. - **B-1: 표/다이어그램 기반 Checking** + **B-1: 표 기반 Checking** - 오류 삽입 방법 (아래 4가지 중 하나 선택): - **(1) 속성 뒤바꿈**: A의 특성을 B의 셀에, B의 특성을 A의 셀에 교차 배치. - **(2) 논리적 모순**: 한 셀이 다른 셀과 논리적으로 양립 불가능한 값 (예: "연결 없음"인데 "순서 보장"). - **(3) 수치 불일치**: 합계/비율이 다른 수치와 맞지 않음. - - **(4) 분류 오류**: 분류표에서 한 항목이 잘못된 범주에 배치됨. 바로잡은 뒤 판단을 요구. + - **(4) 분류 오류**: 분류표에서 한 항목이 잘못된 범주에 배치됨. - 표는 최소 3행×3열 이상. 오류 셀은 교차 대조해야만 발견 가능해야 합니다. - - 질문문 템플릿: "다음 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바로잡은 뒤, **첫 번째 기준**과 **두 번째 기준**을 고려할 때 ~은?" (기준명은 강의노트에 맞게 구체적으로 작성) + - 질문문 템플릿: "다음 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?" (선택지: 어떤 셀을 어떻게 수정하는가) + + **B-2: 다이어그램 기반 Checking** + - Mermaid 다이어그램(순서도, 상태도, 시퀀스 다이어그램 등)을 활용하여 시각적 흐름이나 구조에 논리적 오류를 삽입합니다. + - 오류 삽입 방법 (아래 4가지 중 하나 선택): + - **(1) 흐름/순서 역전**: 프로세스의 선후 관계 화살표가 뒤바뀌어 있음. + - **(2) 분기(조건) 오류**: 분기점(Yes/No 등)에서 이동해야 할 노드의 연결이 서로 바뀌어 있거나 논리에 맞지 않음. + - **(3) 무한 루프/데드락 모순**: 종료 조건 없이 빠져나올 수 없는 잘못된 순환 화살표가 있거나, 다음 단계로 넘어갈 수 없는 상태. + - **(4) 컴포넌트 간 상호작용 오류**: 시퀀스 다이어그램 등에서 요청-응답의 주체와 객체가 잘못 지정됨. + - 질문문 템플릿: "다음 다이어그램의 흐름(또는 구조) 중 **논리적으로 잘못 연결되거나 모순되는 부분**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?" - **B-2: 서술문/주장 기반 Checking** + **B-3: 서술문/주장 기반 Checking** - 3~4개의 주장/설명을 나열하고, 그 중 논리적으로 양립 불가능한 쌍을 찾도록 요구합니다. - 오류 유형: 전제-결론 불일치, 인과 역전, 범위 모순, **숨겨진 전제의 오류** - 질문문 템플릿: - - "다음 설명 중 **서로 모순되는 부분**을 찾고, 이를 바로잡은 뒤 두 기준을 고려할 때 ~은?" - - "다음 두 주장이 공유하는 **암묵적 전제**에 오류가 있다. 이를 찾아 바로잡은 뒤, 두 기준을 고려할 때 더 타당한 결론은?" + - "다음 설명 중 **서로 모순되는 부분**을 찾아 바르게 수정한 것으로 가장 적절한 것은?" + - "다음 두 주장이 공유하는 **암묵적 전제**에 오류가 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?" + + **B-4: 코드 기반 Checking** (강의노트에 코드가 포함된 경우) + - 코드 블록을 제시하고, 그 안의 논리적 오류나 비효율을 탐지하도록 요구합니다. + - 오류 유형: 잘못된 조건문/반복문 범위(Off-by-one 등), 변수명 혼동 및 오타, 알고리즘 실행 순서 오류, 예외 처리 누락 + - 질문문 템플릿: "다음 코드에서 **논리적 오류(또는 비효율)**를 찾아 수정한 것으로 가장 적절한 것은?" - **B-3: 코드/수식 기반 Checking** (강의노트에 코드/수식이 포함된 경우) - - 코드 블록이나 수식을 제시하고, 그 안의 논리적 오류/비효율을 탐지하도록 요구합니다. - - 오류 유형: 변수명 불일치, 조건문 논리 오류, 알고리즘 순서 오류, 수식 전개 오류 - - 질문문 템플릿: "다음 코드/수식에서 **오류**를 찾고, 이를 수정한 뒤 두 기준을 고려할 때 ~은?" + **B-5: 수식 기반 Checking** (강의노트에 수식이나 도출 과정이 포함된 경우) + - 수식이나 수학적 전개 과정을 제시하고, 그 안의 논리적 비약이나 오류를 탐지하도록 요구합니다. + - 오류 유형: 연산자/부호 오류, 잘못된 변수 대입, 성립 조건 무시(예: 분모가 0이 되는 상황 누락), 수식 전개 순서 오류 + - 질문문 템플릿: "다음 수식 전개 과정에서 **오류**를 찾아 올바르게 수정한 것으로 가장 적절한 것은?" **Checking 원칙**: - - 학습자는 모순/오류를 찾은 뒤, 바로잡은 정보를 기반으로 **기준 2개에 근거하여** 판단해야 합니다. - - 단순히 "사실 오류를 찾으세요"만 묻는 것은 금지합니다 (= 사실 매칭, Checking 1점). - - **질문문에서 오류의 원인이나 답을 직접 설명하지 마세요.** 학습자가 직접 오류를 탐지해야 합니다. "프로토콜 B는 비연결형인데 혼잡 제어가 있으므로 모순이다"처럼 답을 알려주는 것은 금지합니다. - - **오류 난이도**: 교과서 정의와 직접 모순되는 쉬운 오류(예: "건조 기후에서 강수량이 증발량보다 많다") 대신, 교차 대조해야만 발견할 수 있는 미묘한 불일치(예: 수치 간 비일관성, 속성 뒤바꿈)를 삽입하세요. + - 학습자가 자료를 "읽기만" 해서는 오류를 발견할 수 없어야 합니다. 여러 요소를 **교차 대조**해야만 발견 가능한 미묘한 불일치(수치 비일관성, 속성 뒤바꿈 등)를 삽입하세요. + - 질문문에서 오류의 원인이나 위치를 설명하지 마세요. 학습자가 직접 탐지해야 합니다. + - 선택지 4개는 "어떤 항목이 오류이고 어떻게 수정하는가"를 기준으로 구성하며, 각 오답은 오류 위치나 수정 방향을 다르게 설정합니다. **금지 패턴** (위장이므로 절대 사용 금지): - 기준 없이 "가장 적절한 것은?" (= Remember 수준) @@ -105,8 +117,8 @@ public class MultipleGuideLine { ### Step 3 — 질문문 작성 - 질문문은 학습자가 읽는 텍스트입니다. **학생에게 친숙한 일상 표현만 사용하세요.** - - 기준 간 관계: "**한쪽을 우선하면 다른 쪽이 약해질 때**", "**A에서 일부 손실을 감수하면서**", "**A를 우선하되 B도 일정 수준 유지해야 할 때**" - - 오류 탐지: "**하나의 항목이 잘못 기재되어 있다**", "**서로 모순되는 부분을 찾고**" + - 기준 간 관계: "**한쪽을 우선하면 다른 쪽이 약해질 때**", "**A에서 일부 손실을 감수하면서**", "**A를 우선하되 B도 일정 수준 유지해야 할 때**" + - 오류 탐지: "**하나의 항목이 잘못 기재되어 있다**", "**서로 모순되는 부분을 찾고**" - **질문문 금지 용어** (사용 시 0점 처리): "트레이드오프", "trade-off", "Trade-off", "상충 관계", "타협점", "절충안", "최적의 타협". 이 단어들을 질문문·선택지·해설 어디에도 쓰지 마세요. - 핵심 용어·조건은 **굵게** 강조하세요. 자료(표·인용문)는 읽기만으로 정답을 고를 수 없도록 추론이 필요하게 설계하세요. - 복수 인용은 각각 별도 인용블록: "\\n\\n> **A**: \\"내용\\"\\n\\n> **B**: \\"내용\\"\\n\\n" @@ -120,9 +132,9 @@ public class MultipleGuideLine { - **오답 매력도 (핵심 규칙)**: 각 오답은 부분적으로 참이어야 합니다. "이것도 맞는 것 같은데?"라고 고민하게 만드세요. - **좋은 오답**: "카나트의 지하수로 원리를 적용하되, 수로의 경사를 급하게 설정하여 유속을 높인다" (원리는 맞지만 경사 설계가 틀림) - **나쁜 오답**: "건조 기후에서 벼 농사를 짓는다" (상식만으로 즉시 탈락 — 금지) - - **자가점검**: 각 오답을 작성한 뒤 확인하세요 — "이 오답이 왜 틀린지 설명하려면 강의노트 지식이 필요한가?" → Yes여야 합니다. 상식만으로 탈락 가능하면 재작성하세요. + - **자가점검**: 각 오답을 작성한 뒤 확인하세요 — "이 오답이 왜 틀린지 설명하려면 강의노트 지식이 필요한가?" → Yes여야 합니다. 상식만으로 탈락 가능하면 재작성하세요. - **오답 표현 규칙**: "무제한", "완벽히", "전적으로 의존", "항상", "절대", "완전히 제거" 같은 극단적/절대적 표현 금지. 대신 조건부 서술("~하면 ~할 수 있으나", "~를 시도하되 ~의 위험이 있다")을 사용하세요. - - **길이 균등 (필수)**: 4개 선택지의 글자수를 가장 짧은 것 기준 ±20%% 이내로 맞추세요. 정답이 가장 길면 안 됩니다. 오답에도 구체적 근거·조건문·수치를 포함하여 길이를 맞추세요. + - **길이 균등 (필수)**: 4개 선택지의 글자수를 가장 짧은 것 기준 ±20% 이내로 맞추세요. 정답이 가장 길면 안 됩니다. 오답에도 구체적 근거·조건문·수치를 포함하여 길이를 맞추세요. ### Step 5 — 해설 작성 - 정답: 근거와 추론 경로 + 강의노트 인용 @@ -149,19 +161,19 @@ public class MultipleGuideLine { ``` ## 예시 2 — 패턴 B-1 (Checking: 표 기반 오류 탐지형) - 표의 2개 이상 요소 간 교차 대조가 필요하며, 오류 발견 후 기준 2개로 판단합니다. - **핵심**: 질문문에서 오류가 무엇인지 설명하지 않습니다. 학습자가 직접 찾아야 합니다. + 표의 2개 이상 요소를 교차 대조해야만 발견 가능한 오류를 찾고 바르게 수정합니다. + **핵심**: 질문문에서 오류가 무엇인지 설명하지 않습니다. 학습자가 교차 대조로 직접 찾아야 합니다. ```json { "questions": [{ - "content": "다음 표는 두 전송 프로토콜의 특성을 비교한 것이다.\\n\\n| 특성 | 프로토콜 A | 프로토콜 B |\\n| :--- | :--- | :--- |\\n| 연결 설정 | 3-way handshake | 없음 |\\n| 패킷 순서 보장 | 보장 | 미보장 |\\n| 혼잡 제어 | 있음 | 있음 |\\n\\n위 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바로잡은 뒤, **전송 지연 최소화**와 **데이터 무결성** 두 기준을 동시에 고려할 때, 실시간 영상 스트리밍에 더 적합한 프로토콜과 그 근거로 가장 타당한 것은?", + "content": "다음 표는 두 전송 프로토콜의 특성을 비교한 것이다.\\n\\n| 특성 | 프로토콜 A | 프로토콜 B |\\n| :--- | :--- | :--- |\\n| 연결 설정 | 3-way handshake | 없음 |\\n| 패킷 순서 보장 | 보장 | 미보장 |\\n| 혼잡 제어 | 있음 | 있음 |\\n\\n위 표에서 **하나의 항목이 잘못 기재**되어 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?", "referencedPages": [12, 13], - "quizExplanation": "**[자기 점검]** (프로토콜 선택 기준)\\n'연결 없음'인데 '혼잡 제어 있음'이 맞는지 확인했나요?\\n\\n**[심화 학습]**\\nHTTP/3가 UDP 기반 QUIC을 채택한 이유를 분석해 보세요. [네트워크 계층 구조 13p] 참조.", + "quizExplanation": "**[자기 점검]** (특성 간 교차 대조)\\n프로토콜 B의 '연결 설정: 없음'과 '혼잡 제어: 있음'을 함께 읽었을 때 논리적으로 양립 가능한지 확인했나요?\\n\\n**[심화 학습]**\\n비연결형 프로토콜이 혼잡 제어를 제공하지 않는 이유를 연결 상태 관리 관점에서 분석해 보세요. [네트워크 계층 구조 13p] 참조.", "selections": [ - {"content": "프로토콜 B — 혼잡 제어가 없어 전송 지연이 최소화되며, 실시간 서비스에서는 일부 패킷 손실이 재전송 대기보다 영향이 적기 때문이다", "correct": true, "explanation": "**[정답 추론]**\\n표의 논리적 모순: 프로토콜 B는 비연결형인데 혼잡 제어가 있다고 표기됨.\\n- 근거: [네트워크 계층 구조 13p]"}, - {"content": "프로토콜 A — 3-way handshake로 연결을 보장하므로 지연이 크지만, 패킷 순서 보장이 영상 프레임 연속성에 유리하기 때문이다", "correct": false, "explanation": "[조건 누락형]\\n- **진단**: 순서 보장 이점은 참이나 지연 최소화 기준 미충족"}, - {"content": "프로토콜 B — 표에 따르면 혼잡 제어가 있어 네트워크 안정성이 확보되므로, 지연 최소화와 무결성을 동시에 달성할 수 있기 때문이다", "correct": false, "explanation": "[오개념형]\\n- **진단**: 표의 오류를 사실로 받아들인 경우"}, - {"content": "프로토콜 A — 혼잡 제어로 네트워크 과부하를 방지하므로, 대역폭이 제한된 환경에서 안정적 전송이 영상 품질 유지에 유리하기 때문이다", "correct": false, "explanation": "[범위 과잉형]\\n- **진단**: 특정 조건의 이점을 전체 상황에 일반화"} + {"content": "프로토콜 B의 '혼잡 제어' 항목이 '있음'으로 잘못 기재되어 있다. 연결을 설정하지 않는 비연결형 프로토콜은 전송 경로와 상태를 추적하지 않으므로 혼잡 제어 메커니즘을 가질 수 없으며, '없음'으로 수정해야 한다", "correct": true, "explanation": "**[정답 추론]**\\n교차 대조: '연결 설정: 없음'(비연결형) ↔ '혼잡 제어: 있음'은 논리적으로 양립 불가. 비연결형은 전송 상태를 유지하지 않으므로 혼잡 제어를 수행할 수 없음.\\n- 근거: [네트워크 계층 구조 13p] > \\"비연결형 프로토콜은 각 패킷을 독립적으로 처리하며 혼잡 제어를 수행하지 않는다\\""}, + {"content": "프로토콜 B의 '패킷 순서 보장' 항목이 '미보장'으로 잘못 기재되어 있다. 비연결형이더라도 수신 측 버퍼를 통해 패킷을 재조립할 수 있으므로 '보장'으로 수정해야 한다", "correct": false, "explanation": "[오류 항목 오인형]\\n- **진단**: 비연결형 프로토콜의 순서 미보장은 올바른 기재. 수신 측 재조립은 상위 계층의 역할이며, 프로토콜 자체가 순서를 보장하는 것과는 다름\\n- **교정**: 오류는 '패킷 순서 보장'이 아닌 '혼잡 제어' 항목에 있음\\n- 복습: [네트워크 계층 구조 12p]"}, + {"content": "프로토콜 A의 '혼잡 제어' 항목이 '있음'으로 잘못 기재되어 있다. 3-way handshake는 연결 수립 절차일 뿐이며, 혼잡 제어 기능과는 별개이므로 '없음'으로 수정해야 한다", "correct": false, "explanation": "[수정 방향 오류형]\\n- **진단**: 연결 지향형 프로토콜(A)의 혼잡 제어 '있음'은 올바른 기재. 혼잡 제어는 연결 지향형의 특성이며 3-way handshake와 독립적\\n- **교정**: 오류는 프로토콜 A가 아닌 프로토콜 B의 '혼잡 제어' 항목에 있음\\n- 복습: [네트워크 계층 구조 13p]"}, + {"content": "프로토콜 A의 '패킷 순서 보장'과 프로토콜 B의 '혼잡 제어' 두 항목이 모두 잘못 기재되어 있다. A는 3-way handshake로 연결만 수립하므로 순서 보장과 무관하며, B는 비연결형이므로 혼잡 제어도 없다", "correct": false, "explanation": "[범위 과잉형]\\n- **진단**: 잘못된 항목은 하나뿐임. 연결 지향형(A)의 '패킷 순서 보장: 보장'은 핵심 특성으로 올바른 기재\\n- **교정**: 오류는 B의 '혼잡 제어: 있음' 단 하나\\n- 복습: [네트워크 계층 구조 12p]"} ] }] } @@ -185,6 +197,82 @@ public class MultipleGuideLine { } ``` - (mermaid 흐름도, 코드 블록 등 다양한 서식도 강의노트에 맞게 활용하세요. 위 예시 외에도 B-1은 mermaid 흐름 검증, B-3는 코드 오류 탐지에 사용할 수 있습니다.) + ## 예시 4 — 패턴 B-2 (Checking: 다이어그램 기반 오류 탐지형) + 다이어그램 흐름을 직접 추적하여 논리적으로 잘못 연결된 부분을 찾고 수정합니다. + **핵심**: 질문문에서 어떤 노드가 잘못됐는지 설명하지 않습니다. 학습자가 흐름을 직접 추적해야 합니다. + ```json + { + "questions": [{ + "content": "다음은 웹 서버의 캐시 처리 흐름을 나타낸 다이어그램이다.\\n\\n```mermaid\\nflowchart TD\\n A[클라이언트 요청 수신] --> B{캐시 히트?}\\n B -- Yes --> C[DB 조회 후 데이터 로드]\\n B -- No --> D[캐시에서 데이터 즉시 반환]\\n C --> E[캐시에 결과 저장]\\n E --> F[응답 전송]\\n D --> F\\n```\\n\\n위 다이어그램의 흐름 중 **논리적으로 잘못 연결된 부분**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?", + "referencedPages": [18, 19], + "quizExplanation": "**[자기 점검]** (캐시 히트/미스 흐름 추적)\\n'캐시 히트(Yes)'일 때 DB를 조회하는 것이 캐시의 목적과 부합하는지 직접 추적해 보세요.\\n\\n**[심화 학습]**\\nCache-Aside 패턴에서 캐시 히트와 미스 각각의 정확한 처리 흐름을 직접 도식화해 보세요. [캐시 전략 19p] 참조.", + "selections": [ + {"content": "캐시 히트(Yes) 분기가 'DB 조회 후 데이터 로드'로 잘못 연결되어 있다. 캐시 히트는 이미 캐시에 데이터가 존재하는 상태이므로 DB 조회 없이 캐시에서 즉시 반환해야 하며, Yes → D(캐시에서 즉시 반환), No → C(DB 조회)로 분기를 교체해야 한다", "correct": true, "explanation": "**[정답 추론]**\\n흐름 추적: '캐시 히트(Yes)' → DB 조회는 캐시의 목적(DB 조회 생략)을 정면으로 위반.\\n- 수정: Yes(히트) → D[캐시 즉시 반환], No(미스) → C[DB 조회] → E[캐시 저장] → F[응답]\\n- 근거: [캐시 전략 18p] > \\"캐시 히트 시 저장소 접근을 생략하는 것이 캐시의 핵심 이점이다\\""}, + {"content": "C 노드 이후 'E[캐시에 결과 저장]' 단계가 잘못 배치되어 있다. DB 조회 결과는 응답 전송 이후에 비동기로 저장해야 하므로, E 노드를 F 노드 이후로 이동해야 한다", "correct": false, "explanation": "[오류 위치 오인형]\\n- **진단**: E 노드의 위치는 문제가 아님. DB 조회 후 캐시에 저장하고 응답하는 흐름(C → E → F)은 올바른 순서\\n- **교정**: 오류는 E의 위치가 아닌 Yes/No 분기 연결 자체가 뒤바뀐 것\\n- 복습: [캐시 전략 18p]"}, + {"content": "캐시 미스(No) 분기가 '캐시에서 데이터 즉시 반환'으로 잘못 연결되어 있다. No 경로만 DB 조회로 수정하면 되며, Yes 경로의 DB 조회는 유지해야 캐시 갱신이 이루어진다", "correct": false, "explanation": "[부분 수정형]\\n- **진단**: No 분기 오류는 맞게 지목했으나, Yes 분기의 DB 조회를 유지하는 것이 틀림. Yes/No 두 분기가 동시에 뒤바뀐 상태이므로 양쪽 모두 교체해야 함\\n- **교정**: Yes → D, No → C로 분기를 함께 교체해야 함\\n- 복습: [캐시 전략 19p]"}, + {"content": "A 노드와 B 노드 사이에 '캐시 유효성 검사' 단계가 누락되어 있다. 만료된 캐시 데이터가 히트로 판정될 수 있으므로, A → 유효성 검사 → B 순서로 추가해야 한다", "correct": false, "explanation": "[오류 유형 혼동형]\\n- **진단**: 유효성 검사 누락은 기능 추가 사항이지 논리적 연결 오류가 아님. 문제에서 묻는 것은 노드 누락이 아닌 잘못된 분기 연결\\n- **교정**: 현재 다이어그램에서 잘못된 것은 Yes/No 분기 방향\\n- 복습: [캐시 전략 19p]"} + ] + }] + } + ``` + + ## 예시 5 — 패턴 B-3 (Checking: 서술문/주장 기반 오류 탐지형) + 여러 설명 중 논리적으로 모순되는 항목을 찾아 바르게 수정합니다. + **핵심**: 질문문에서 모순의 내용이나 위치를 설명하지 않습니다. + ```json + { + "questions": [{ + "content": "다음은 OS 프로세스 스케줄링에 관한 네 가지 설명이다.\\n\\n> **ⓐ**: \\"라운드 로빈(RR) 스케줄링은 CPU를 선점하여 각 프로세스에 동일한 시간 할당량(Time Quantum)을 순서대로 부여한다.\\"\\n\\n> **ⓑ**: \\"선점형 스케줄링은 현재 실행 중인 프로세스가 자발적으로 CPU를 반납할 때까지 다른 프로세스가 실행될 수 없다.\\"\\n\\n> **ⓒ**: \\"FCFS 스케줄링에서는 CPU 버스트가 긴 프로세스 뒤에 짧은 프로세스가 오래 대기하는 Convoy Effect가 발생할 수 있다.\\"\\n\\n> **ⓓ**: \\"SJF 스케줄링은 평균 대기 시간을 최소화하지만, CPU 버스트가 긴 프로세스가 계속 뒤로 밀리는 기아(Starvation) 문제가 있다.\\"\\n\\n위 설명 중 **논리적으로 모순되는 항목**이 하나 있다. 이를 찾아 바르게 수정한 것으로 가장 적절한 것은?", + "referencedPages": [30, 31], + "quizExplanation": "**[자기 점검]** (설명 간 교차 대조)\\nⓐ와 ⓑ를 함께 읽었을 때 논리적 충돌이 발생하는지 확인했나요?\\n\\n**[심화 학습]**\\n선점형과 비선점형 스케줄링에서 CPU 회수 주체의 차이를 정확히 구분해 보세요. [프로세스 스케줄링 30p] 참조.", + "selections": [ + {"content": "ⓑ가 잘못되었다. '자발적으로 CPU를 반납할 때까지 다른 프로세스가 실행될 수 없다'는 비선점형 스케줄링의 정의이므로, '스케줄러가 실행 중인 프로세스에서 강제로 CPU를 회수하여 다른 프로세스에 할당할 수 있다'로 수정해야 한다", "correct": true, "explanation": "**[정답 추론]**\\n교차 대조: ⓐ(RR은 선점형) ↔ ⓑ(선점형은 자발적 반납까지 대기). ⓑ는 비선점형의 정의를 선점형에 적용한 직접 모순.\\n- 수정: 선점형은 스케줄러가 CPU를 강제 회수 가능\\n- 근거: [프로세스 스케줄링 30p] > \\"선점형 스케줄링은 스케줄러가 실행 중인 프로세스를 강제로 교체한다\\""}, + {"content": "ⓐ가 잘못되었다. 라운드 로빈은 Time Quantum이 만료될 때까지 현재 프로세스가 CPU를 점유하므로 비선점형으로 분류해야 하며, '비선점형으로 각 프로세스에 동일한 시간 할당량을 순서대로 부여한다'로 수정해야 한다", "correct": false, "explanation": "[오류 항목 오인형]\\n- **진단**: RR은 Time Quantum 만료 시 스케줄러가 강제로 CPU를 전환하므로 선점형이 맞음. ⓐ는 올바른 설명\\n- **교정**: 오류는 ⓐ가 아닌 ⓑ에 있음\\n- 복습: [프로세스 스케줄링 30p]"}, + {"content": "ⓓ가 잘못되었다. SJF에서는 짧은 작업이 우선 처리된 후 긴 작업도 순서대로 실행되므로 기아 문제가 발생하지 않으며, '기아 문제 없이 평균 대기 시간을 최소화한다'로 수정해야 한다", "correct": false, "explanation": "[오류 항목 오인형]\\n- **진단**: SJF의 기아 문제는 실제로 발생함. 짧은 프로세스가 계속 도착하면 긴 프로세스는 무기한 대기 가능. ⓓ는 올바른 설명\\n- **교정**: 오류는 ⓓ가 아닌 ⓑ에 있음\\n- 복습: [프로세스 스케줄링 31p]"}, + {"content": "ⓑ와 ⓒ가 서로 모순된다. 선점형이 가능하다면 FCFS에서도 Convoy Effect를 방지할 수 있으므로, ⓒ를 '선점형 스케줄링 적용 시 Convoy Effect는 발생하지 않는다'로 수정해야 한다", "correct": false, "explanation": "[모순 위치 오인형]\\n- **진단**: ⓑ의 오류는 ⓒ와의 모순이 아니라 ⓐ와의 직접 모순. Convoy Effect는 FCFS 고유 특성으로 ⓒ는 올바른 설명\\n- **교정**: 수정이 필요한 것은 ⓒ가 아닌 ⓑ\\n- 복습: [프로세스 스케줄링 30p]"} + ] + }] + } + ``` + + ## 예시 6 — 패턴 B-4 (Checking: 코드 기반 오류 탐지형) + 코드의 논리적 오류를 찾아 바르게 수정합니다. + **핵심**: 오류 위치를 주석으로 힌트를 줄 수 있으나, 오류의 의미나 수정 방법은 설명하지 않습니다. + ```json + { + "questions": [{ + "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위 코드에서 **논리적 오류**를 찾아 수정한 것으로 가장 적절한 것은?", + "referencedPages": [44, 45], + "quizExplanation": "**[자기 점검]** (이진 탐색 불변 조건 이해)\\n`left = mid`일 때 `arr[mid] < target`인 상황에서 탐색 범위가 실제로 줄어드는지 직접 시뮬레이션해 보세요.\\n\\n**[심화 학습]**\\n이진 탐색의 루프 불변 조건(Loop Invariant)을 정의하고, `left`·`right` 갱신 규칙이 이를 어떻게 보장하는지 분석해 보세요. [이진 탐색 구현 45p] 참조.", + "selections": [ + {"content": "`left = mid`를 `left = mid + 1`로 수정한다 — `arr[mid] < target`이면 mid는 정답이 될 수 없으므로 탐색 범위에서 제외해야 하며, 그렇지 않으면 left가 변하지 않아 탐색 범위가 줄어들지 않는다", "correct": true, "explanation": "**[정답 추론]**\\n오류: `left = mid`는 `arr[mid] < target`일 때 탐색 범위가 줄어들지 않아 무한루프를 유발.\\n- 수정: `left = mid + 1` → mid를 탐색 대상에서 제외하여 범위를 반드시 축소\\n- 근거: [이진 탐색 구현 44p] > \\"각 이터레이션에서 탐색 범위는 반드시 감소해야 한다\\""}, + {"content": "`left = mid`를 `left = mid - 1`로 수정한다 — mid가 target보다 작으면 탐색 방향을 역전시켜 누락된 값을 재탐색하고, 이로써 탐색 범위를 양방향으로 좁혀 무한루프 없이 정확한 탐색이 가능하다", "correct": false, "explanation": "[오개념형]\\n- **진단**: `left = mid - 1`은 탐색 범위를 오히려 왼쪽으로 이동시켜 단조 증가 조건을 위반함\\n- **교정**: `arr[mid] < target`이면 목표값은 mid 오른쪽에 있으므로 left는 반드시 증가해야 함\\n- **스스로 점검**: 탐색 방향(오른쪽/왼쪽)과 left/right 갱신 방향이 일치하는지 확인했는가?\\n- 복습: [이진 탐색 구현 44p]"}, + {"content": "`while left <= right`를 `while left < right`로 수정한다 — 동등 조건을 제거하면 left와 right가 같을 때 루프를 강제 종료하여 무한루프를 방지하나, target이 배열 마지막 원소일 경우 탐색에 실패하여 효율 기준을 충족하기 어렵다", "correct": false, "explanation": "[조건 누락형]\\n- **진단**: 루프 조건 변경으로 일부 무한루프를 막을 수 있으나, `left = mid` 오류를 수정하지 않아 탐색 범위 축소 보장에 실패\\n- **교정**: 근본 원인은 범위 축소 실패이므로 루프 조건이 아닌 left 갱신 로직을 수정해야 함\\n- **스스로 점검**: 증상(무한루프)을 해결하는 것이 원인(범위 축소 실패)을 해결하는 것과 동일한가?\\n- 복습: [이진 탐색 구현 45p]"}, + {"content": "`mid = (left + right) // 2`를 `mid = left + (right - left) // 2`로 수정한다 — 정수 오버플로우를 방지하여 대용량 배열에서 탐색 정확도를 높이고, 이로써 무한루프 없이 O(log n)을 유지한다", "correct": false, "explanation": "[범위 과잉형]\\n- **진단**: 오버플로우 방지 관행은 올바르나, 이것이 현재 코드의 무한루프 원인은 아님\\n- **교정**: 현재 오류는 `left = mid`로 인한 탐색 범위 미축소이며, mid 계산식 변경으로는 해결 불가\\n- **스스로 점검**: 좋은 코딩 관행을 '이 코드의 논리적 오류'와 혼동하지 않았는가?\\n- 복습: [이진 탐색 구현 45p]"} + ] + }] + } + ``` + + ## 예시 7 — 패턴 B-5 (Checking: 수식 기반 오류 탐지형) + 수식 전개 과정에서 논리적으로 잘못된 단계를 찾아 바르게 수정합니다. + **핵심**: 오류가 있는 Step을 화살표로 표시할 수 있으나, 오류의 의미나 수정 결과는 설명하지 않습니다. + ```json + { + "questions": [{ + "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위 수식 전개 중 **오류**를 찾아 올바르게 수정한 것으로 가장 적절한 것은?", + "referencedPages": [55, 56], + "quizExplanation": "**[자기 점검]** (RTT 정의 확인)\\nRTT는 편도 지연인가요, 왕복 지연인가요? Step 2의 값이 전제의 $t_p$와 어떤 관계여야 하는지 확인했나요?\\n\\n**[심화 학습]**\\n수정된 RTT를 적용했을 때 이용률 100% 달성을 위한 최소 윈도우 크기 W를 일반식으로 유도해 보세요. [슬라이딩 윈도우 56p] 참조.", + "selections": [ + {"content": "Step 2가 잘못되었다. RTT는 왕복(Round-Trip) 시간이므로 편도 지연 $t_p$의 2배인 $\\\\text{RTT} = 2 \\\\times t_p = 20\\\\text{ ms}$로 수정해야 하며, 수정 시 $U = \\\\frac{4 \\\\times 1}{1 + 20} \\\\approx 19.0\\\\%$이다", "correct": true, "explanation": "**[정답 추론]**\\n오류 위치: Step 2 — RTT는 왕복 시간이므로 편도 $t_p$의 2배여야 함.\\n- 수정: $\\\\text{RTT} = 2 \\\\times t_p = 20\\\\text{ ms}$\\n- 수정된 이용률: $U = \\\\frac{4 \\\\times 1}{1 + 20} \\\\approx 19.0\\\\%$\\n- 근거: [슬라이딩 윈도우 55p] > \\"RTT = 2 × 편도 전파 지연 + 처리 지연\\""}, + {"content": "Step 3이 잘못되었다. 공식 분모에서 $t_t$를 제거하고 $U = \\\\frac{W \\\\cdot t_t}{\\\\text{RTT}}$로 수정해야 하며, 수정 시 $U = \\\\frac{4}{10} = 40\\\\%$이다", "correct": false, "explanation": "[오류 위치 오인형]\\n- **진단**: 분모에서 $t_t$를 제거하면 패킷 전송 시간 동안 채널이 점유된다는 사실을 무시하는 것으로, 공식 구조 자체가 잘못됨\\n- **교정**: Step 3의 공식 구조는 올바르며, 오류는 Step 3이 아닌 Step 2의 RTT 값 계산에 있음\\n- 복습: [슬라이딩 윈도우 56p]"}, + {"content": "Step 1이 잘못되었다. $t_t$를 ms 단위로 계산했으나 RTT도 ms 단위이므로 단위 불일치가 발생하며, $t_t = 0.001\\\\text{ s}$와 $\\\\text{RTT} = 0.01\\\\text{ s}$로 단위를 통일하여 재계산해야 한다", "correct": false, "explanation": "[오류 유형 혼동형]\\n- **진단**: Step 1과 Step 3 모두 ms 단위로 일관되게 사용하고 있어 단위 혼용 오류가 없음\\n- **교정**: 오류는 단위 환산이 아닌 Step 2의 RTT 정의(편도 vs 왕복) 문제\\n- 복습: [슬라이딩 윈도우 55p]"}, + {"content": "Step 2가 잘못되었다. RTT는 왕복 시간이므로 $\\\\text{RTT} = 2 \\\\times t_p = 20\\\\text{ ms}$로 수정해야 하지만, Step 3의 분모도 $t_t + 2 \\\\times \\\\text{RTT}$로 함께 수정해야 올바른 이용률이 계산된다", "correct": false, "explanation": "[범위 과잉형]\\n- **진단**: Step 2 수정은 맞으나, Step 3 공식은 $t_t + \\\\text{RTT}$가 올바른 구조이므로 추가 수정이 필요하지 않음\\n- **교정**: Step 2를 수정하면 Step 3은 수정된 RTT 값을 그대로 대입하면 됨\\n- 복습: [슬라이딩 윈도우 56p]"} + ] + }] + } + ``` + + (위 예시들을 참고하여 강의노트에 등장하는 실제 개념·수식·코드·다이어그램으로 대체하세요. 패턴 선택 시 강의노트 자료 형식에 맞는 B-1~B-5 변형을 선택합니다.) """; } 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 bdb630f5..51cb265c 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 @@ -38,7 +38,7 @@ @Component public class OXQuizOrchestrator implements QuizTypeOrchestrator { - private static final int MAX_SELECTION_COUNT = 4; + private static final int MAX_SELECTION_COUNT = 2; private static final String RESPONSE_JSON_SCHEMA = new BeanOutputConverter<>(com.icc.qasker.ai.structure.GeminiResponse.class).getJsonSchema(); diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiMetricsRecorder.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiMetricsRecorder.java index 5c740e3d..8c916b51 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiMetricsRecorder.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/service/support/GeminiMetricsRecorder.java @@ -16,10 +16,10 @@ @Component public class GeminiMetricsRecorder { - // Gemini 3.1 Flash Lite 단가 - private static final double PRICE_INPUT_PER_1M = 0.25; - private static final double PRICE_CACHE_READ_PER_1M = 0.025; - private static final double PRICE_OUTPUT_PER_1M = 1.50; + // Gemini 3 Flash Preview 단가 + private static final double PRICE_INPUT_PER_1M = 0.50; + private static final double PRICE_CACHE_READ_PER_1M = 0.050; + private static final double PRICE_OUTPUT_PER_1M = 3.00; private final MeterRegistry registry; private final Timer chunkDuration; From de6d562c5037b9288fef8608ae07da92181c71ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Apr 2026 11:17:47 +0000 Subject: [PATCH 2/3] chore: bump version to 2.4.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 71c6132c..d198b375 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { group = "com.icc.qasker" // 프로젝트 버전 (Docker 이미지 태그, 배포 아티팩트 버전에 사용) // 예: jib으로 빌드하면 Docker 이미지에 "1.7.0" 태그가 붙음 -version = "2.4.2" +version = "2.4.3" // Git hooks 경로를 .githooks/로 자동 설정 // 예: ./gradlew build 실행 시 자동으로 git config core.hooksPath .githooks 적용 From f926b463081ca2fb4710df61563d6f28091c0233 Mon Sep 17 00:00:00 2001 From: Oh YoungJe <139232765+GulSauce@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:58:07 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EC=9A=B0=EC=95=84=ED=95=9C=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EC=8B=9C=EA=B0=84=20=EC=A6=9D=EA=B0=80,=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B9=8C=EB=93=9C=20=EC=8B=A4=ED=8C=A8=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0=20=EC=8B=9C=EB=8F=84=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/resources/config/app-common.yml | 2 +- infra/blue-green/deploy.sh | 2 +- .../com/icc/qasker/ai/config/GeminiClientConfig.java | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/resources/config/app-common.yml b/app/src/main/resources/config/app-common.yml index 89f817a9..8a9deb17 100644 --- a/app/src/main/resources/config/app-common.yml +++ b/app/src/main/resources/config/app-common.yml @@ -18,7 +18,7 @@ server: spring: lifecycle: - timeout-per-shutdown-phase: 60s + timeout-per-shutdown-phase: 300s servlet: multipart: diff --git a/infra/blue-green/deploy.sh b/infra/blue-green/deploy.sh index afb86202..c674a2cd 100644 --- a/infra/blue-green/deploy.sh +++ b/infra/blue-green/deploy.sh @@ -30,7 +30,7 @@ EOF # ============================================================================== MAX_RETRIES=36 SLEEP_TIME=5 -SHUTDOWN_TIMEOUT=60 +SHUTDOWN_TIMEOUT=250 PULL_TIMEOUT=120 BLUE_PORT="${1:?BLUE_PORT is required}" GREEN_PORT="${2:?GREEN_PORT is required}" diff --git a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/config/GeminiClientConfig.java b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/config/GeminiClientConfig.java index 8dd66fc2..ebde870a 100644 --- a/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/config/GeminiClientConfig.java +++ b/modules/quiz-ai/impl/src/main/java/com/icc/qasker/ai/config/GeminiClientConfig.java @@ -18,6 +18,7 @@ public class GeminiClientConfig { private final QAskerAiProperties aiProperties; @Bean + @org.springframework.context.annotation.Profile("!test") public Client googleGenAiClient(GoogleGenAiConnectionProperties properties) { return Client.builder() .project(properties.getProjectId()) @@ -27,6 +28,15 @@ public Client googleGenAiClient(GoogleGenAiConnectionProperties properties) { .build(); } + @Bean + @org.springframework.context.annotation.Profile("test") + public Client googleGenAiClientTest() { + return Client.builder() + .apiKey("ci-dummy-key") + .httpOptions(HttpOptions.builder().timeout(aiProperties.getChatTimeoutMs()).build()) + .build(); + } + @Bean public ChunkProperties chunkProperties() { return aiProperties.getChunk();