diff --git a/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLock.java b/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLock.java index 5310976b..67c37d80 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLock.java +++ b/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLock.java @@ -10,5 +10,5 @@ @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CodeReviewLock { - String prefix(); + String prefix(); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java b/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java index 6e248a4b..0a4c9c52 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java +++ b/src/main/java/org/ezcode/codetest/application/submission/aop/CodeReviewLockAspect.java @@ -20,38 +20,38 @@ @Order(Ordered.HIGHEST_PRECEDENCE) public class CodeReviewLockAspect { - private final LockManager lockManager; - - @Around("@annotation(org.ezcode.codetest.application.submission.aop.CodeReviewLock)") - public Object lock(ProceedingJoinPoint joinPoint) throws Throwable { - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - String prefix = signature.getMethod().getAnnotation(CodeReviewLock.class).prefix(); - Object[] args = joinPoint.getArgs(); - - Long problemId = null; - Long userId = null; - - for (Object arg : args) { - if (arg instanceof Long) { - problemId = (Long) arg; - } else if (arg instanceof AuthUser) { - userId = ((AuthUser)arg).getId(); - } - } - - if (problemId == null || userId == null) { - throw new CodeReviewException(CodeReviewExceptionCode.REQUIRED_ARGS_NOT_FOUND); - } - - boolean locked = lockManager.tryLock(prefix, userId, problemId); - if (!locked) { - throw new CodeReviewException(CodeReviewExceptionCode.ALREADY_REVIEWING); - } - - try { - return joinPoint.proceed(); - } finally { - lockManager.releaseLock(prefix, userId, problemId); - } - } + private final LockManager lockManager; + + @Around("@annotation(org.ezcode.codetest.application.submission.aop.CodeReviewLock)") + public Object lock(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); + String prefix = signature.getMethod().getAnnotation(CodeReviewLock.class).prefix(); + Object[] args = joinPoint.getArgs(); + + Long problemId = null; + Long userId = null; + + for (Object arg : args) { + if (arg instanceof Long) { + problemId = (Long)arg; + } else if (arg instanceof AuthUser) { + userId = ((AuthUser)arg).getId(); + } + } + + if (problemId == null || userId == null) { + throw new CodeReviewException(CodeReviewExceptionCode.REQUIRED_ARGS_NOT_FOUND); + } + + boolean locked = lockManager.tryLock(prefix, userId, problemId); + if (!locked) { + throw new CodeReviewException(CodeReviewExceptionCode.ALREADY_REVIEWING); + } + + try { + return joinPoint.proceed(); + } finally { + lockManager.releaseLock(prefix, userId, problemId); + } + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java index 5e758425..889c046a 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/compile/CodeCompileRequest.java @@ -2,11 +2,11 @@ public record CodeCompileRequest( - String source_code, + String source_code, - Long language_id, + Long language_id, - String stdin + String stdin ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/language/LanguageUpdateRequest.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/language/LanguageUpdateRequest.java index 4e2e9204..d1e4fb9c 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/language/LanguageUpdateRequest.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/language/LanguageUpdateRequest.java @@ -6,9 +6,9 @@ @Schema(description = "언어 업데이트 요청") public record LanguageUpdateRequest( - @Schema(description = "Judge0에서 사용하는 언어 ID", example = "62") - @NotNull(message = "Judge0 아이디는 필수 입력 값입니다.") - Long judge0Id + @Schema(description = "Judge0에서 사용하는 언어 ID", example = "62") + @NotNull(message = "Judge0 아이디는 필수 입력 값입니다.") + Long judge0Id ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/CodeReviewRequest.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/CodeReviewRequest.java index 757746b4..5de38662 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/CodeReviewRequest.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/CodeReviewRequest.java @@ -7,20 +7,20 @@ @Schema(description = "코드 리뷰 요청 DTO") public record CodeReviewRequest( - @Schema(description = "언어 ID", example = "62") - @NotNull(message = "언어 번호는 필수 입력 값입니다.") - Long languageId, + @Schema(description = "언어 ID", example = "62") + @NotNull(message = "언어 번호는 필수 입력 값입니다.") + Long languageId, - @Schema( - description = "소스 코드", - example = "public class Main { public static void main(String[] args) { System.out.println(\"Hello\"); } }" - ) - @NotBlank(message = "소스 코드는 필수 입력 값입니다.") - String sourceCode, + @Schema( + description = "소스 코드", + example = "public class Main { public static void main(String[] args) { System.out.println(\"Hello\"); } }" + ) + @NotBlank(message = "소스 코드는 필수 입력 값입니다.") + String sourceCode, - @Schema(description = "정답 여부", example = "true") - @NotNull(message = "정답 여부는 필수 입력 값입니다.") - Boolean isCorrect + @Schema(description = "정답 여부", example = "true") + @NotNull(message = "정답 여부는 필수 입력 값입니다.") + Boolean isCorrect ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/ReviewPayload.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/ReviewPayload.java index 8f414024..aeaaaf34 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/ReviewPayload.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/review/ReviewPayload.java @@ -5,21 +5,21 @@ public record ReviewPayload( - String problemDescription, + String problemDescription, - String languageName, + String languageName, - String sourceCode, + String sourceCode, - boolean isCorrect + boolean isCorrect ) { - public static ReviewPayload of(Problem problem, Language language, CodeReviewRequest request) { - return new ReviewPayload( - problem.getDescription(), - language.getName(), - request.sourceCode(), - request.isCorrect() - ); - } + public static ReviewPayload of(Problem problem, Language language, CodeReviewRequest request) { + return new ReviewPayload( + problem.getDescription(), + language.getName(), + request.sourceCode(), + request.isCorrect() + ); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/request/submission/CodeSubmitRequest.java b/src/main/java/org/ezcode/codetest/application/submission/dto/request/submission/CodeSubmitRequest.java index e0648f0e..2342c8b6 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/request/submission/CodeSubmitRequest.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/request/submission/CodeSubmitRequest.java @@ -7,16 +7,16 @@ @Schema(description = "코드 제출 요청 DTO") public record CodeSubmitRequest( - @Schema(description = "언어 ID", example = "1") - @NotNull(message = "언어 번호는 필수 입력 값입니다.") - Long languageId, + @Schema(description = "언어 ID", example = "1") + @NotNull(message = "언어 번호는 필수 입력 값입니다.") + Long languageId, - @Schema( - description = "소스 코드", - example = "public class Main { public static void main(String[] args) { System.out.println(\"Hello World\"); } }" - ) - @NotBlank(message = "소스 코드는 필수 입력 값입니다.") - String sourceCode + @Schema( + description = "소스 코드", + example = "public class Main { public static void main(String[] args) { System.out.println(\"Hello World\"); } }" + ) + @NotBlank(message = "소스 코드는 필수 입력 값입니다.") + String sourceCode ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/compile/ExecutionResultResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/compile/ExecutionResultResponse.java index 8e5488a2..31022288 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/compile/ExecutionResultResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/compile/ExecutionResultResponse.java @@ -2,37 +2,38 @@ public record ExecutionResultResponse( - String stdout, + String stdout, - Double time, + Double time, - Long memory, + Long memory, - String stderr, + String stderr, - String token, + String token, - String compile_output, + String compile_output, - int exit_code, + int exit_code, - ExecutionStatus status + ExecutionStatus status ) { - public long getMemory() { - return this.memory == null ? 0L : memory; - } + public long getMemory() { + return this.memory == null ? 0L : memory; + } - public double getTime() { - return this.time == null ? 0.0 : time; - } + public double getTime() { + return this.time == null ? 0.0 : time; + } - public record ExecutionStatus( + public record ExecutionStatus( - int id, + int id, - String description + String description - ) {} + ) { + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/language/LanguageResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/language/LanguageResponse.java index 04361f2a..fab0216e 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/language/LanguageResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/language/LanguageResponse.java @@ -5,24 +5,24 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "언어 응답 정보") -public record LanguageResponse ( +public record LanguageResponse( - @Schema(description = "언어 ID", example = "1") - Long id, + @Schema(description = "언어 ID", example = "1") + Long id, - @Schema(description = "언어 이름", example = "Java") - String name, + @Schema(description = "언어 이름", example = "Java") + String name, - @Schema(description = "언어 버전", example = "17") - String version, + @Schema(description = "언어 버전", example = "17") + String version, - @Schema(description = "Judge0에서 사용하는 언어 ID", example = "62") - Long judge0Id + @Schema(description = "Judge0에서 사용하는 언어 ID", example = "62") + Long judge0Id -){ - public static LanguageResponse from(Language language) { - return new LanguageResponse( - language.getId(), language.getName(), language.getVersion(), language.getJudge0Id() - ); - } +) { + public static LanguageResponse from(Language language) { + return new LanguageResponse( + language.getId(), language.getName(), language.getVersion(), language.getJudge0Id() + ); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/review/CodeReviewResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/review/CodeReviewResponse.java index 155938fc..3a0d4ce8 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/review/CodeReviewResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/review/CodeReviewResponse.java @@ -5,8 +5,8 @@ @Schema(description = "코드 리뷰 응답 DTO") public record CodeReviewResponse( - @Schema(description = "리뷰 내용", example = "변수명이 명확하지 않습니다. 의미 있는 이름을 사용해주세요.") - String reviewContent + @Schema(description = "리뷰 내용", example = "변수명이 명확하지 않습니다. 의미 있는 이름을 사용해주세요.") + String reviewContent ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/FinalResultResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/FinalResultResponse.java index 97f668fb..c8e2503e 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/FinalResultResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/FinalResultResponse.java @@ -7,22 +7,22 @@ @Schema(description = "최종 채점 결과 응답 DTO") public class FinalResultResponse { - @Schema(description = "전체 테스트케이스 수", example = "5") - private final int totalCount; + @Schema(description = "전체 테스트케이스 수", example = "5") + private final int totalCount; - @Schema(description = "통과한 테스트케이스 수", example = "5") - private final int passedCount; + @Schema(description = "통과한 테스트케이스 수", example = "5") + private final int passedCount; - @Schema(description = "전체 통과 여부", example = "true") - private final boolean isCorrect; + @Schema(description = "전체 통과 여부", example = "true") + private final boolean isCorrect; - @Schema(description = "메시지", example = "Accepted") - private final String message; + @Schema(description = "메시지", example = "Accepted") + private final String message; - public FinalResultResponse(int totalCount, int passedCount, String message) { - this.totalCount = totalCount; - this.passedCount = passedCount; - this.isCorrect = totalCount == passedCount; - this.message = message; - } + public FinalResultResponse(int totalCount, int passedCount, String message) { + this.totalCount = totalCount; + this.passedCount = passedCount; + this.isCorrect = totalCount == passedCount; + this.message = message; + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/GroupedSubmissionResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/GroupedSubmissionResponse.java index fad95963..f8acdb2a 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/GroupedSubmissionResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/GroupedSubmissionResponse.java @@ -13,41 +13,41 @@ import lombok.Getter; @Getter -@JsonPropertyOrder({ "problemId", "problemDescription", "submissions" }) +@JsonPropertyOrder({"problemId", "problemDescription", "submissions"}) @Schema(description = "문제 단위로 묶은 제출 목록 응답") public class GroupedSubmissionResponse { - @Schema(description = "문제 ID", example = "10") - private final Long problemId; - - @Schema(description = "문제 설명", example = "두 수의 합을 구하는 문제입니다.") - private final String problemDescription; - - @Schema(description = "해당 문제에 대한 제출 목록") - private final List submissions; - - public static List groupByProblem(List submissions) { - return submissions.stream() - .collect(Collectors.groupingBy(Submission::getProblem)) - .entrySet() - .stream() - .map(entry -> createSorted(entry.getKey(), entry.getValue())) - .toList(); - } - - private static GroupedSubmissionResponse createSorted(Problem problem, List submissions) { - List sorted = submissions.stream() - .sorted(Comparator.comparing(Submission::getCreatedAt).reversed()) - .toList(); - - return new GroupedSubmissionResponse(problem, sorted); - } - - private GroupedSubmissionResponse(Problem problem, List submissions) { - this.problemId = problem.getId(); - this.problemDescription = problem.getDescription(); - this.submissions = submissions.stream() - .map(SubmissionDetailResponse::from) - .toList(); - } + @Schema(description = "문제 ID", example = "10") + private final Long problemId; + + @Schema(description = "문제 설명", example = "두 수의 합을 구하는 문제입니다.") + private final String problemDescription; + + @Schema(description = "해당 문제에 대한 제출 목록") + private final List submissions; + + public static List groupByProblem(List submissions) { + return submissions.stream() + .collect(Collectors.groupingBy(Submission::getProblem)) + .entrySet() + .stream() + .map(entry -> createSorted(entry.getKey(), entry.getValue())) + .toList(); + } + + private static GroupedSubmissionResponse createSorted(Problem problem, List submissions) { + List sorted = submissions.stream() + .sorted(Comparator.comparing(Submission::getCreatedAt).reversed()) + .toList(); + + return new GroupedSubmissionResponse(problem, sorted); + } + + private GroupedSubmissionResponse(Problem problem, List submissions) { + this.problemId = problem.getId(); + this.problemDescription = problem.getDescription(); + this.submissions = submissions.stream() + .map(SubmissionDetailResponse::from) + .toList(); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/JudgeResultResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/JudgeResultResponse.java index bb8684f1..c83f5c91 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/JudgeResultResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/JudgeResultResponse.java @@ -10,32 +10,32 @@ @Schema(description = "각 테스트케이스에 대한 채점 결과") public record JudgeResultResponse( - @Schema(description = "테스트케이스 통과 여부", example = "true") - boolean isPassed, + @Schema(description = "테스트케이스 통과 여부", example = "true") + boolean isPassed, - @Schema(description = "기댓값", example = "12") - String expectedOutput, + @Schema(description = "기댓값", example = "12") + String expectedOutput, - @Schema(description = "실제 출력값", example = "12") - String actualOutput, + @Schema(description = "실제 출력값", example = "12") + String actualOutput, - @Schema(description = "실행 시간 (s)", example = "0.129") - Double executionTime, + @Schema(description = "실행 시간 (s)", example = "0.129") + Double executionTime, - @Schema(description = "메모리 사용량 (KB)", example = "12196") - Long memoryUsage, + @Schema(description = "메모리 사용량 (KB)", example = "12196") + Long memoryUsage, - @Schema(description = "결과 메시지", example = "Accepted") - String message + @Schema(description = "결과 메시지", example = "Accepted") + String message ) { - public static JudgeResultResponse fromEvaluation(JudgeResult result, AnswerEvaluation evaluation) { - return new JudgeResultResponse( - evaluation.isPassed(), - evaluation.expectedOutput(), - evaluation.actualOutput(), - result.executionTime(), - result.memoryUsage(), - result.message()); - } + public static JudgeResultResponse fromEvaluation(JudgeResult result, AnswerEvaluation evaluation) { + return new JudgeResultResponse( + evaluation.isPassed(), + evaluation.expectedOutput(), + evaluation.actualOutput(), + result.executionTime(), + result.memoryUsage(), + result.message()); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmissionDetailResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmissionDetailResponse.java index 2f15d979..d47b8023 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmissionDetailResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmissionDetailResponse.java @@ -9,37 +9,37 @@ @Schema(description = "개별 제출 결과 응답") public record SubmissionDetailResponse( - @Schema(description = "제출 ID", example = "101") - Long id, + @Schema(description = "제출 ID", example = "101") + Long id, - @Schema(description = "소스 코드", example = "System.out.println(\"Hello\");") - String sourceCode, + @Schema(description = "소스 코드", example = "System.out.println(\"Hello\");") + String sourceCode, - @Schema(description = "정답 여부", example = "true") - boolean isCorrect, + @Schema(description = "정답 여부", example = "true") + boolean isCorrect, - @Schema(description = "결과 메시지", example = "Accepted") - String message, + @Schema(description = "결과 메시지", example = "Accepted") + String message, - @Schema(description = "실행 시간 (s)", example = "0.129") - Double executionTime, + @Schema(description = "실행 시간 (s)", example = "0.129") + Double executionTime, - @Schema(description = "메모리 사용량 (KB)", example = "12196") - Long memoryUsage, + @Schema(description = "메모리 사용량 (KB)", example = "12196") + Long memoryUsage, - @Schema(description = "제출 시간", example = "2025-06-11T19:00:00") - LocalDateTime submittedAt + @Schema(description = "제출 시간", example = "2025-06-11T19:00:00") + LocalDateTime submittedAt ) { - public static SubmissionDetailResponse from(Submission submission) { - return new SubmissionDetailResponse( - submission.getId(), - submission.getCode(), - submission.isCorrect(), - submission.getMessage(), - submission.getExecutionTime(), - submission.getMemoryUsage(), - submission.getCreatedAt().withNano(0) - ); - } + public static SubmissionDetailResponse from(Submission submission) { + return new SubmissionDetailResponse( + submission.getId(), + submission.getCode(), + submission.isCorrect(), + submission.getMessage(), + submission.getExecutionTime(), + submission.getMemoryUsage(), + submission.getCreatedAt().withNano(0) + ); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/model/JudgeResult.java b/src/main/java/org/ezcode/codetest/application/submission/model/JudgeResult.java index 427ff248..fdb46018 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/model/JudgeResult.java +++ b/src/main/java/org/ezcode/codetest/application/submission/model/JudgeResult.java @@ -5,15 +5,15 @@ @Builder public record JudgeResult( - String actualOutput, + String actualOutput, - double executionTime, + double executionTime, - long memoryUsage, + long memoryUsage, - boolean success, + boolean success, - String message + String message ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/model/OpenAIResponse.java b/src/main/java/org/ezcode/codetest/application/submission/model/OpenAIResponse.java index bc867bec..e7859d6e 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/model/OpenAIResponse.java +++ b/src/main/java/org/ezcode/codetest/application/submission/model/OpenAIResponse.java @@ -7,15 +7,18 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record OpenAIResponse( - List choices + List choices ) { - public record Choice(Message message) {} - public record Message(String role, String content) {} + public record Choice(Message message) { + } - public String getReviewContent() { - return Optional.ofNullable(choices) - .flatMap(list -> list.stream().findFirst()) - .map(choice -> choice.message().content()) - .orElse(""); - } + public record Message(String role, String content) { + } + + public String getReviewContent() { + return Optional.ofNullable(choices) + .flatMap(list -> list.stream().findFirst()) + .map(choice -> choice.message().content()) + .orElse(""); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/model/ReviewResult.java b/src/main/java/org/ezcode/codetest/application/submission/model/ReviewResult.java index ef92fc84..1cf8a3c2 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/model/ReviewResult.java +++ b/src/main/java/org/ezcode/codetest/application/submission/model/ReviewResult.java @@ -2,7 +2,7 @@ public record ReviewResult( - String reviewContent + String reviewContent ) { } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/EmitterStore.java b/src/main/java/org/ezcode/codetest/application/submission/port/EmitterStore.java index d130ece8..e7376d5c 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/EmitterStore.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/EmitterStore.java @@ -6,12 +6,12 @@ public interface EmitterStore { - void saveWithCallbacks(String key, SseEmitter emitter); + void saveWithCallbacks(String key, SseEmitter emitter); - Optional get(String key); + Optional get(String key); - SseEmitter getOrElseThrow(String key); + SseEmitter getOrElseThrow(String key); - void remove(String key); + void remove(String key); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/ExceptionNotifier.java b/src/main/java/org/ezcode/codetest/application/submission/port/ExceptionNotifier.java index eec3b963..c5397b42 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/ExceptionNotifier.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/ExceptionNotifier.java @@ -2,6 +2,6 @@ public interface ExceptionNotifier { - void sendEmbed(String title, String description, String exception, String methodName); + void sendEmbed(String title, String description, String exception, String methodName); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/JudgeClient.java b/src/main/java/org/ezcode/codetest/application/submission/port/JudgeClient.java index cabc7fbd..9a4b0b78 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/JudgeClient.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/JudgeClient.java @@ -5,8 +5,8 @@ public interface JudgeClient { - String submitAndGetToken(CodeCompileRequest request); + String submitAndGetToken(CodeCompileRequest request); - JudgeResult pollUntilDone(String token); + JudgeResult pollUntilDone(String token); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/LockManager.java b/src/main/java/org/ezcode/codetest/application/submission/port/LockManager.java index 620edfe0..e888265a 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/LockManager.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/LockManager.java @@ -2,8 +2,8 @@ public interface LockManager { - boolean tryLock(String prefix, Long userId, Long problemId); + boolean tryLock(String prefix, Long userId, Long problemId); - void releaseLock(String prefix, Long userId, Long problemId); + void releaseLock(String prefix, Long userId, Long problemId); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/QueueProducer.java b/src/main/java/org/ezcode/codetest/application/submission/port/QueueProducer.java index ec5cb040..36a9a654 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/QueueProducer.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/QueueProducer.java @@ -4,6 +4,6 @@ public interface QueueProducer { - void enqueue(SubmissionMessage submissionMessage); + void enqueue(SubmissionMessage submissionMessage); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/port/ReviewClient.java b/src/main/java/org/ezcode/codetest/application/submission/port/ReviewClient.java index 07a9296b..85e73695 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/port/ReviewClient.java +++ b/src/main/java/org/ezcode/codetest/application/submission/port/ReviewClient.java @@ -5,6 +5,6 @@ public interface ReviewClient { - ReviewResult requestReview(ReviewPayload request); + ReviewResult requestReview(ReviewPayload request); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/LanguageService.java b/src/main/java/org/ezcode/codetest/application/submission/service/LanguageService.java index 48900d29..f7f186a1 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/service/LanguageService.java +++ b/src/main/java/org/ezcode/codetest/application/submission/service/LanguageService.java @@ -16,38 +16,38 @@ @RequiredArgsConstructor public class LanguageService { - private final LanguageDomainService languageDomainService; + private final LanguageDomainService languageDomainService; - @Transactional - public LanguageResponse createLanguage(LanguageCreateRequest request) { + @Transactional + public LanguageResponse createLanguage(LanguageCreateRequest request) { - languageDomainService.validateLanguageNotDuplicated(request.name(), request.version()); + languageDomainService.validateLanguageNotDuplicated(request.name(), request.version()); - Language language = languageDomainService.createLanguage(LanguageCreateRequest.toEntity(request)); + Language language = languageDomainService.createLanguage(LanguageCreateRequest.toEntity(request)); - return LanguageResponse.from(language); - } + return LanguageResponse.from(language); + } - @Transactional(readOnly = true) - public List getLanguages() { - return languageDomainService.getLanguages() - .stream() - .map(LanguageResponse::from) - .toList(); - } + @Transactional(readOnly = true) + public List getLanguages() { + return languageDomainService.getLanguages() + .stream() + .map(LanguageResponse::from) + .toList(); + } - @Transactional - public LanguageResponse modifyLanguage(Long languageId, LanguageUpdateRequest request) { - Language language = languageDomainService.getLanguage(languageId); - languageDomainService.modifyLanguage(language, request.judge0Id()); + @Transactional + public LanguageResponse modifyLanguage(Long languageId, LanguageUpdateRequest request) { + Language language = languageDomainService.getLanguage(languageId); + languageDomainService.modifyLanguage(language, request.judge0Id()); - return LanguageResponse.from(language); - } + return LanguageResponse.from(language); + } - @Transactional - public void removeLanguage(Long languageId) { + @Transactional + public void removeLanguage(Long languageId) { - languageDomainService.validateLanguageExists(languageId); - languageDomainService.removeLanguage(languageId); - } + languageDomainService.validateLanguageExists(languageId); + languageDomainService.removeLanguage(languageId); + } } diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java b/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java index 22dfa4d6..cd2547f0 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java +++ b/src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java @@ -62,73 +62,73 @@ public class SubmissionService { private final SubmissionDomainService submissionDomainService; private final EmitterStore emitterStore; private final QueueProducer queueProducer; - private final Executor judgeTestcaseExecutor; - private final ExceptionNotifier exceptionNotifier; - private final LockManager lockManager; + private final Executor judgeTestcaseExecutor; + private final ExceptionNotifier exceptionNotifier; + private final LockManager lockManager; - public SseEmitter enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { + public SseEmitter enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { - boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId); + boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId); - if (!acquired) { - throw new SubmissionException(SubmissionExceptionCode.ALREADY_JUDGING); - } + if (!acquired) { + throw new SubmissionException(SubmissionExceptionCode.ALREADY_JUDGING); + } SseEmitter emitter = new SseEmitter(10 * 60 * 1000L); String emitterKey = authUser.getId() + "_" + UUID.randomUUID(); - log.info("[SSE 저장] emitterKey: {}", emitterKey); - emitterStore.saveWithCallbacks(emitterKey, emitter); + log.info("[SSE 저장] emitterKey: {}", emitterKey); + emitterStore.saveWithCallbacks(emitterKey, emitter); queueProducer.enqueue( - new SubmissionMessage(emitterKey, problemId, request.languageId(), authUser.getId(), request.sourceCode()) - ); + new SubmissionMessage(emitterKey, problemId, request.languageId(), authUser.getId(), request.sourceCode()) + ); return emitter; } - @Async("judgeSubmissionExecutor") + @Async("judgeSubmissionExecutor") public void submitCodeStream(SubmissionMessage msg) { - try { + try { log.info("[Submission RUN] Thread = {}", Thread.currentThread().getName()); - log.info("[큐 수신] SubmissionMessage.emitterKey: {}", msg.emitterKey()); - User user = userDomainService.getUserById(msg.userId()); + log.info("[큐 수신] SubmissionMessage.emitterKey: {}", msg.emitterKey()); + User user = userDomainService.getUserById(msg.userId()); Language language = languageDomainService.getLanguage(msg.languageId()); ProblemInfo problemInfo = problemDomainService.getProblemInfo(msg.problemId()); SseEmitter emitter = emitterStore.getOrElseThrow(msg.emitterKey()); - int totalTestcaseCount = problemInfo.getTestcaseCount(); + int totalTestcaseCount = problemInfo.getTestcaseCount(); SubmissionContext context = SubmissionContext.initialize(totalTestcaseCount); for (Testcase tc : problemInfo.testcaseList()) { - runTestcaseAsync(tc, msg, language.getJudge0Id(), problemInfo, context, emitter); - } + runTestcaseAsync(tc, msg, language.getJudge0Id(), problemInfo, context, emitter); + } - if (!context.latch().await(60, TimeUnit.SECONDS)) { - emitter.completeWithError(new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT)); - return; - } + if (!context.latch().await(60, TimeUnit.SECONDS)) { + emitter.completeWithError(new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT)); + return; + } - emitter.send(SseEmitter.event() - .name("final") - .data(context.toFinalResult(totalTestcaseCount))); - emitter.complete(); + emitter.send(SseEmitter.event() + .name("final") + .data(context.toFinalResult(totalTestcaseCount))); + emitter.complete(); SubmissionData submissionData = SubmissionData.base( - user, problemInfo, language, msg.sourceCode(), context.getCurrentMessage() + user, problemInfo, language, msg.sourceCode(), context.getCurrentMessage() ); submissionDomainService.finalizeSubmission( - submissionData, context.aggregator(), context.getPassedCount() - ); - - } catch (Exception e) { - emitterStore.get(msg.emitterKey()).ifPresent(emitter -> emitter.completeWithError(e)); - exceptionNotificationHelper(e); - } finally { - emitterStore.remove(msg.emitterKey()); - lockManager.releaseLock("submission", msg.userId(), msg.problemId()); - } + submissionData, context.aggregator(), context.getPassedCount() + ); + + } catch (Exception e) { + emitterStore.get(msg.emitterKey()).ifPresent(emitter -> emitter.completeWithError(e)); + exceptionNotificationHelper(e); + } finally { + emitterStore.remove(msg.emitterKey()); + lockManager.releaseLock("submission", msg.userId(), msg.problemId()); + } } @Transactional(readOnly = true) @@ -139,13 +139,13 @@ public List getSubmissions(AuthUser authUser) { return GroupedSubmissionResponse.groupByProblem(submissions); } - @Transactional - @CodeReviewLock(prefix = "review") + @Transactional + @CodeReviewLock(prefix = "review") public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request, AuthUser authUser) { - User user = userDomainService.getUserById(authUser.getId()); - userDomainService.decreaseReviewToken(user); + User user = userDomainService.getUserById(authUser.getId()); + userDomainService.decreaseReviewToken(user); - Problem problem = problemDomainService.getProblem(problemId); + Problem problem = problemDomainService.getProblem(problemId); Language language = languageDomainService.getLanguage(request.languageId()); ReviewResult reviewResult = reviewClient.requestReview(ReviewPayload.of(problem, language, request)); @@ -153,58 +153,58 @@ public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest reques return new CodeReviewResponse(reviewResult.reviewContent()); } - private void runTestcaseAsync( - Testcase tc, SubmissionMessage msg, Long judge0Id, - ProblemInfo problemInfo, SubmissionContext context, SseEmitter emitter - ) { - CompletableFuture.runAsync(() -> { - try { - log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName()); - String token = judgeClient.submitAndGetToken( - new CodeCompileRequest(msg.sourceCode(), judge0Id, tc.getInput()) - ); - JudgeResult result = judgeClient.pollUntilDone(token); - - AnswerEvaluation evaluation = submissionDomainService.handleEvaluationAndUpdateStats( - TestcaseEvaluationInput.from(tc, result), problemInfo, context - ); - emitter.send(JudgeResultResponse.fromEvaluation(result, evaluation)); - } catch (Exception e) { - if (context.notified().compareAndSet(false, true)) { - emitter.completeWithError(e); - emitterStore.remove(msg.emitterKey()); - exceptionNotificationHelper(e); - } - } finally { - context.countDown(); - } - }, judgeTestcaseExecutor); - } - - private void exceptionNotificationHelper(Throwable t) { - if (t instanceof SubmissionException se) { - var code = se.getResponseCode(); - exceptionNotifier.sendEmbed( - "채점 예외", - "채점 중 SubmissionException 발생", - """ - • 성공 여부: %s - • 상태코드: %s - • 메시지: %s - """.formatted(code.isSuccess(), code.getStatus(), code.getMessage()), - "submitCodeStream" - ); - } else { - exceptionNotifier.sendEmbed( - "채점 예외", - "채점 중 알 수 없는 예외 발생", - """ - • 성공 여부: false - • 상태코드: 500 - • 메시지: %s - """.formatted(Optional.ofNullable(t.getMessage()).orElse("No message")), - "submitCodeStream" - ); - } - } + private void runTestcaseAsync( + Testcase tc, SubmissionMessage msg, Long judge0Id, + ProblemInfo problemInfo, SubmissionContext context, SseEmitter emitter + ) { + CompletableFuture.runAsync(() -> { + try { + log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName()); + String token = judgeClient.submitAndGetToken( + new CodeCompileRequest(msg.sourceCode(), judge0Id, tc.getInput()) + ); + JudgeResult result = judgeClient.pollUntilDone(token); + + AnswerEvaluation evaluation = submissionDomainService.handleEvaluationAndUpdateStats( + TestcaseEvaluationInput.from(tc, result), problemInfo, context + ); + emitter.send(JudgeResultResponse.fromEvaluation(result, evaluation)); + } catch (Exception e) { + if (context.notified().compareAndSet(false, true)) { + emitter.completeWithError(e); + emitterStore.remove(msg.emitterKey()); + exceptionNotificationHelper(e); + } + } finally { + context.countDown(); + } + }, judgeTestcaseExecutor); + } + + private void exceptionNotificationHelper(Throwable t) { + if (t instanceof SubmissionException se) { + var code = se.getResponseCode(); + exceptionNotifier.sendEmbed( + "채점 예외", + "채점 중 SubmissionException 발생", + """ + • 성공 여부: %s + • 상태코드: %s + • 메시지: %s + """.formatted(code.isSuccess(), code.getStatus(), code.getMessage()), + "submitCodeStream" + ); + } else { + exceptionNotifier.sendEmbed( + "채점 예외", + "채점 중 알 수 없는 예외 발생", + """ + • 성공 여부: false + • 상태코드: 500 + • 메시지: %s + """.formatted(Optional.ofNullable(t.getMessage()).orElse("No message")), + "submitCodeStream" + ); + } + } } diff --git a/src/main/java/org/ezcode/codetest/common/config/ExecutorConfig.java b/src/main/java/org/ezcode/codetest/common/config/ExecutorConfig.java index a43fd63c..27b78066 100644 --- a/src/main/java/org/ezcode/codetest/common/config/ExecutorConfig.java +++ b/src/main/java/org/ezcode/codetest/common/config/ExecutorConfig.java @@ -11,36 +11,36 @@ @Configuration public class ExecutorConfig { - @Bean(name = "consumerExecutor") - public Executor consumerExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("consumer-"); - executor.initialize(); - return executor; - } + @Bean(name = "consumerExecutor") + public Executor consumerExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("consumer-"); + executor.initialize(); + return executor; + } - @Bean(name = "judgeSubmissionExecutor") - public Executor judgeSubmissionExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); - executor.setQueueCapacity(100); - executor.setThreadNamePrefix("submission-"); - executor.initialize(); - return executor; - } + @Bean(name = "judgeSubmissionExecutor") + public Executor judgeSubmissionExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("submission-"); + executor.initialize(); + return executor; + } - @Bean(name = "judgeTestcaseExecutor") - public Executor judgeTestcaseExecutor() { - ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(25); - executor.setMaxPoolSize(50); - executor.setQueueCapacity(500); - executor.setThreadNamePrefix("testcase-"); - executor.initialize(); - return executor; - } + @Bean(name = "judgeTestcaseExecutor") + public Executor judgeTestcaseExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(25); + executor.setMaxPoolSize(50); + executor.setQueueCapacity(500); + executor.setThreadNamePrefix("testcase-"); + executor.initialize(); + return executor; + } } diff --git a/src/main/java/org/ezcode/codetest/common/config/TaskSchedulerConfig.java b/src/main/java/org/ezcode/codetest/common/config/TaskSchedulerConfig.java index 2614af66..2aaa3cbb 100644 --- a/src/main/java/org/ezcode/codetest/common/config/TaskSchedulerConfig.java +++ b/src/main/java/org/ezcode/codetest/common/config/TaskSchedulerConfig.java @@ -8,12 +8,12 @@ @Configuration public class TaskSchedulerConfig { - @Bean(name = "appTaskScheduler") - public TaskScheduler taskScheduler() { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setPoolSize(1); - scheduler.setThreadNamePrefix("token-reset-scheduler-"); - scheduler.initialize(); - return scheduler; - } + @Bean(name = "appTaskScheduler") + public TaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(1); + scheduler.setThreadNamePrefix("token-reset-scheduler-"); + scheduler.initialize(); + return scheduler; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/language/exception/LanguageException.java b/src/main/java/org/ezcode/codetest/domain/language/exception/LanguageException.java index 5acc73eb..3a76223c 100644 --- a/src/main/java/org/ezcode/codetest/domain/language/exception/LanguageException.java +++ b/src/main/java/org/ezcode/codetest/domain/language/exception/LanguageException.java @@ -10,13 +10,13 @@ @Getter public class LanguageException extends BaseException { - private final ResponseCode responseCode; - private final HttpStatus httpStatus; - private final String message; + private final ResponseCode responseCode; + private final HttpStatus httpStatus; + private final String message; - public LanguageException(LanguageExceptionCode responseCode) { - this.responseCode = responseCode; - this.httpStatus = responseCode.getStatus(); - this.message = responseCode.getMessage(); - } + public LanguageException(LanguageExceptionCode responseCode) { + this.responseCode = responseCode; + this.httpStatus = responseCode.getStatus(); + this.message = responseCode.getMessage(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/language/exception/code/LanguageExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/language/exception/code/LanguageExceptionCode.java index 6e012757..32a58758 100644 --- a/src/main/java/org/ezcode/codetest/domain/language/exception/code/LanguageExceptionCode.java +++ b/src/main/java/org/ezcode/codetest/domain/language/exception/code/LanguageExceptionCode.java @@ -10,10 +10,10 @@ @RequiredArgsConstructor public enum LanguageExceptionCode implements ResponseCode { - LANGUAGE_ALREADY_EXISTS(false, HttpStatus.CONFLICT, "이미 존재하는 언어입니다."), - LANGUAGE_NOT_FOUND(false, HttpStatus.NOT_FOUND, "존재하지 않는 언어입니다."); + LANGUAGE_ALREADY_EXISTS(false, HttpStatus.CONFLICT, "이미 존재하는 언어입니다."), + LANGUAGE_NOT_FOUND(false, HttpStatus.NOT_FOUND, "존재하지 않는 언어입니다."); - private final boolean success; - private final HttpStatus status; - private final String message; + private final boolean success; + private final HttpStatus status; + private final String message; } diff --git a/src/main/java/org/ezcode/codetest/domain/language/model/entity/Language.java b/src/main/java/org/ezcode/codetest/domain/language/model/entity/Language.java index 803a3798..30583001 100644 --- a/src/main/java/org/ezcode/codetest/domain/language/model/entity/Language.java +++ b/src/main/java/org/ezcode/codetest/domain/language/model/entity/Language.java @@ -15,27 +15,27 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Language { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - public Long id; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Long id; - @Column(nullable = false) - private String name; + @Column(nullable = false) + private String name; - @Column(nullable = false) - private String version; + @Column(nullable = false) + private String version; - @Column(name = "judge0_id", nullable = false) - private Long judge0Id; + @Column(name = "judge0_id", nullable = false) + private Long judge0Id; - @Builder - public Language(String name, String version, Long judge0Id) { - this.name = name; - this.version = version; - this.judge0Id = judge0Id; - } + @Builder + public Language(String name, String version, Long judge0Id) { + this.name = name; + this.version = version; + this.judge0Id = judge0Id; + } - public void updateJudge0Id(Long judge0Id) { - this.judge0Id = judge0Id; - } + public void updateJudge0Id(Long judge0Id) { + this.judge0Id = judge0Id; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/language/repository/LanguageRepository.java b/src/main/java/org/ezcode/codetest/domain/language/repository/LanguageRepository.java index 3adcae23..198dc8cd 100644 --- a/src/main/java/org/ezcode/codetest/domain/language/repository/LanguageRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/language/repository/LanguageRepository.java @@ -6,17 +6,17 @@ import org.ezcode.codetest.domain.language.model.entity.Language; public interface LanguageRepository { - boolean existsById(Long languageId); + boolean existsById(Long languageId); - boolean existsByNameAndVersion(String name, String version); + boolean existsByNameAndVersion(String name, String version); - Language saveLanguage(Language language); + Language saveLanguage(Language language); - Optional findLanguageById(Long languageId); + Optional findLanguageById(Long languageId); - List findLanguages(); + List findLanguages(); - void updateLanguage(Language language, Long judge0Id); + void updateLanguage(Language language, Long judge0Id); - void deleteLanguage(Long languageId); + void deleteLanguage(Long languageId); } diff --git a/src/main/java/org/ezcode/codetest/domain/language/service/LanguageDomainService.java b/src/main/java/org/ezcode/codetest/domain/language/service/LanguageDomainService.java index 7096532f..015e1b78 100644 --- a/src/main/java/org/ezcode/codetest/domain/language/service/LanguageDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/language/service/LanguageDomainService.java @@ -14,39 +14,39 @@ @RequiredArgsConstructor public class LanguageDomainService { - private final LanguageRepository languageRepository; - - public void validateLanguageExists(Long languageId) { - if (!languageRepository.existsById(languageId)) { - throw new LanguageException(LanguageExceptionCode.LANGUAGE_NOT_FOUND); - } - } - - public void validateLanguageNotDuplicated(String name, String version) { - if (languageRepository.existsByNameAndVersion(name, version)) { - throw new LanguageException(LanguageExceptionCode.LANGUAGE_ALREADY_EXISTS); - } - } - - public Language createLanguage(Language language) { - return languageRepository.saveLanguage(language); - } - - public Language getLanguage(Long languageId) { - return languageRepository.findLanguageById(languageId) - .orElseThrow(() -> new LanguageException(LanguageExceptionCode.LANGUAGE_NOT_FOUND)); - } - - public List getLanguages() { - return languageRepository.findLanguages(); - } - - public void modifyLanguage(Language language, Long judge0Id) { - languageRepository.updateLanguage(language, judge0Id); - } - - public void removeLanguage(Long languageId) { - languageRepository.deleteLanguage(languageId); - } + private final LanguageRepository languageRepository; + + public void validateLanguageExists(Long languageId) { + if (!languageRepository.existsById(languageId)) { + throw new LanguageException(LanguageExceptionCode.LANGUAGE_NOT_FOUND); + } + } + + public void validateLanguageNotDuplicated(String name, String version) { + if (languageRepository.existsByNameAndVersion(name, version)) { + throw new LanguageException(LanguageExceptionCode.LANGUAGE_ALREADY_EXISTS); + } + } + + public Language createLanguage(Language language) { + return languageRepository.saveLanguage(language); + } + + public Language getLanguage(Long languageId) { + return languageRepository.findLanguageById(languageId) + .orElseThrow(() -> new LanguageException(LanguageExceptionCode.LANGUAGE_NOT_FOUND)); + } + + public List getLanguages() { + return languageRepository.findLanguages(); + } + + public void modifyLanguage(Language language, Long judge0Id) { + languageRepository.updateLanguage(language, judge0Id); + } + + public void removeLanguage(Long languageId) { + languageRepository.deleteLanguage(languageId); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/dto/AnswerEvaluation.java b/src/main/java/org/ezcode/codetest/domain/submission/dto/AnswerEvaluation.java index e8bea7d3..169b14ee 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/dto/AnswerEvaluation.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/dto/AnswerEvaluation.java @@ -5,18 +5,18 @@ @Builder public record AnswerEvaluation( - boolean isCorrect, + boolean isCorrect, - boolean timeEfficient, + boolean timeEfficient, - boolean memoryEfficient, + boolean memoryEfficient, - String expectedOutput, + String expectedOutput, - String actualOutput + String actualOutput ) { - public boolean isPassed() { - return this.isCorrect() && this.timeEfficient() && this.memoryEfficient(); - } + public boolean isPassed() { + return this.isCorrect() && this.timeEfficient() && this.memoryEfficient(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmissionData.java b/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmissionData.java index a0e594d4..d51831db 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmissionData.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmissionData.java @@ -15,71 +15,71 @@ @Builder public record SubmissionData( - User user, + User user, - Problem problem, + Problem problem, - Language language, + Language language, - List testCaseList, + List testCaseList, - String code, + String code, - String message, + String message, - double executionTime, + double executionTime, - long memoryUsage + long memoryUsage ) { - public static SubmissionData base( - User user, ProblemInfo problemInfo, Language language, String code, String message) { - return SubmissionData.builder() - .user(user) - .problem(problemInfo.problem()) - .language(language) - .testCaseList(problemInfo.testcaseList()) - .code(code) - .message(message) - .build(); - } - - public SubmissionData withAggregatedStats(SubmissionAggregator aggregator) { - return SubmissionData.builder() - .user(this.user) - .problem(this.problem) - .language(this.language) - .testCaseList(this.testCaseList) - .code(this.code) - .message(this.message) - .executionTime(aggregator.averageExecutionTime()) - .memoryUsage(aggregator.averageMemoryUsage()) - .build(); - } - - public static Submission toEntity(SubmissionData submissionData, int testCasePassedCount) { - return Submission.builder() - .user(submissionData.user) - .problem(submissionData.problem) - .language(submissionData.language) - .code(submissionData.code) - .message(submissionData.message) - .testCasePassedCount(testCasePassedCount) - .testCaseTotalCount(submissionData.getTestCaseSize()) - .executionTime(submissionData.executionTime) - .memoryUsage(submissionData.memoryUsage) - .build(); - } - - public Long getUserId() { - return this.user.getId(); - } - - public Long getProblemId() { - return this.problem.getId(); - } - - public int getTestCaseSize() { - return this.testCaseList.size(); - } + public static SubmissionData base( + User user, ProblemInfo problemInfo, Language language, String code, String message) { + return SubmissionData.builder() + .user(user) + .problem(problemInfo.problem()) + .language(language) + .testCaseList(problemInfo.testcaseList()) + .code(code) + .message(message) + .build(); + } + + public SubmissionData withAggregatedStats(SubmissionAggregator aggregator) { + return SubmissionData.builder() + .user(this.user) + .problem(this.problem) + .language(this.language) + .testCaseList(this.testCaseList) + .code(this.code) + .message(this.message) + .executionTime(aggregator.averageExecutionTime()) + .memoryUsage(aggregator.averageMemoryUsage()) + .build(); + } + + public static Submission toEntity(SubmissionData submissionData, int testCasePassedCount) { + return Submission.builder() + .user(submissionData.user) + .problem(submissionData.problem) + .language(submissionData.language) + .code(submissionData.code) + .message(submissionData.message) + .testCasePassedCount(testCasePassedCount) + .testCaseTotalCount(submissionData.getTestCaseSize()) + .executionTime(submissionData.executionTime) + .memoryUsage(submissionData.memoryUsage) + .build(); + } + + public Long getUserId() { + return this.user.getId(); + } + + public Long getProblemId() { + return this.problem.getId(); + } + + public int getTestCaseSize() { + return this.testCaseList.size(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmitProcessResult.java b/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmitProcessResult.java index 06e09795..60636dc9 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmitProcessResult.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/dto/SubmitProcessResult.java @@ -5,27 +5,27 @@ @Builder public record SubmitProcessResult( - boolean isCorrect, + boolean isCorrect, - String expectedOutput, + String expectedOutput, - String actualOutput, + String actualOutput, - double executionTime, + double executionTime, - long memoryUsage, + long memoryUsage, - String message + String message ) { - public static SubmitProcessResult of(SubmissionData submissionData, AnswerEvaluation evaluation) { - return SubmitProcessResult.builder() - .isCorrect(evaluation.isCorrect()) - .expectedOutput(evaluation.expectedOutput()) - .actualOutput(evaluation.actualOutput()) - .executionTime(submissionData.executionTime()) - .memoryUsage(submissionData.memoryUsage()) - .message(submissionData.message()) - .build(); - } + public static SubmitProcessResult of(SubmissionData submissionData, AnswerEvaluation evaluation) { + return SubmitProcessResult.builder() + .isCorrect(evaluation.isCorrect()) + .expectedOutput(evaluation.expectedOutput()) + .actualOutput(evaluation.actualOutput()) + .executionTime(submissionData.executionTime()) + .memoryUsage(submissionData.memoryUsage()) + .message(submissionData.message()) + .build(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/dto/WeeklySolveCount.java b/src/main/java/org/ezcode/codetest/domain/submission/dto/WeeklySolveCount.java index 05f2d121..104551a5 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/dto/WeeklySolveCount.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/dto/WeeklySolveCount.java @@ -1,11 +1,10 @@ package org.ezcode.codetest.domain.submission.dto; - public record WeeklySolveCount( - Long userId, + Long userId, - long solveDayCount + long solveDayCount ) { diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/CodeReviewException.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/CodeReviewException.java index 9507bd38..123d72cb 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/exception/CodeReviewException.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/CodeReviewException.java @@ -10,13 +10,13 @@ @Getter public class CodeReviewException extends BaseException { - private final ResponseCode responseCode; - private final HttpStatus httpStatus; - private final String message; + private final ResponseCode responseCode; + private final HttpStatus httpStatus; + private final String message; - public CodeReviewException(CodeReviewExceptionCode responseCode) { - this.responseCode = responseCode; - this.httpStatus = responseCode.getStatus(); - this.message = responseCode.getMessage(); - } + public CodeReviewException(CodeReviewExceptionCode responseCode) { + this.responseCode = responseCode; + this.httpStatus = responseCode.getStatus(); + this.message = responseCode.getMessage(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/SubmissionException.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/SubmissionException.java index e235fba6..958911a4 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/exception/SubmissionException.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/SubmissionException.java @@ -10,13 +10,13 @@ @Getter public class SubmissionException extends BaseException { - private final ResponseCode responseCode; - private final HttpStatus httpStatus; - private final String message; + private final ResponseCode responseCode; + private final HttpStatus httpStatus; + private final String message; - public SubmissionException(SubmissionExceptionCode responseCode) { - this.responseCode = responseCode; - this.httpStatus = responseCode.getStatus(); - this.message = responseCode.getMessage(); - } + public SubmissionException(SubmissionExceptionCode responseCode) { + this.responseCode = responseCode; + this.httpStatus = responseCode.getStatus(); + this.message = responseCode.getMessage(); + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/code/CodeReviewExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/CodeReviewExceptionCode.java index cbf72b4a..2ac12a94 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/exception/code/CodeReviewExceptionCode.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/CodeReviewExceptionCode.java @@ -10,14 +10,14 @@ @RequiredArgsConstructor public enum CodeReviewExceptionCode implements ResponseCode { - REVIEW_SERVER_ERROR(false, HttpStatus.BAD_GATEWAY, "AI 서버 오류가 발생했습니다."), - REVIEW_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "AI 응답 시간이 초과되었습니다."), - REVIEW_INVALID_FORMAT(false, HttpStatus.INTERNAL_SERVER_ERROR, "AI 리뷰 형식 검증에 실패했습니다."), - ALREADY_REVIEWING(false, HttpStatus.CONFLICT, "이미 해당 코드에 대한 리뷰가 진행 중입니다."), - REQUIRED_ARGS_NOT_FOUND(false, HttpStatus.BAD_REQUEST, "필수 인수를 메서드 시그니처에서 찾을 수 없습니다."), - ; + REVIEW_SERVER_ERROR(false, HttpStatus.BAD_GATEWAY, "AI 서버 오류가 발생했습니다."), + REVIEW_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "AI 응답 시간이 초과되었습니다."), + REVIEW_INVALID_FORMAT(false, HttpStatus.INTERNAL_SERVER_ERROR, "AI 리뷰 형식 검증에 실패했습니다."), + ALREADY_REVIEWING(false, HttpStatus.CONFLICT, "이미 해당 코드에 대한 리뷰가 진행 중입니다."), + REQUIRED_ARGS_NOT_FOUND(false, HttpStatus.BAD_REQUEST, "필수 인수를 메서드 시그니처에서 찾을 수 없습니다."), + ; - private final boolean success; - private final HttpStatus status; - private final String message; + private final boolean success; + private final HttpStatus status; + private final String message; } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/exception/code/SubmissionExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/SubmissionExceptionCode.java index 4648a493..72923348 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/exception/code/SubmissionExceptionCode.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/exception/code/SubmissionExceptionCode.java @@ -10,15 +10,15 @@ @RequiredArgsConstructor public enum SubmissionExceptionCode implements ResponseCode { - EMITTER_NOT_FOUND(false, HttpStatus.NOT_FOUND, "emitter를 찾을 수 없습니다."), - EMITTER_SEND_ERROR(false, HttpStatus.INTERNAL_SERVER_ERROR, "SSE 전송 중 오류가 발생했습니다."), - COMPILE_SERVER_ERROR(false, HttpStatus.BAD_GATEWAY, "컴파일 서버 오류"), - COMPILE_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "컴파일 서버로부터 응답이 지연되고 있습니다."), - TESTCASE_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "테스트케이스 채점 시간이 초과되었습니다."), - REDIS_SERVER_ERROR(false, HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버 연결 실패"), - ALREADY_JUDGING(false, HttpStatus.CONFLICT, "이미 해당 문제에 대한 채점이 진행 중입니다."), - ; - private final boolean success; - private final HttpStatus status; - private final String message; + EMITTER_NOT_FOUND(false, HttpStatus.NOT_FOUND, "emitter를 찾을 수 없습니다."), + EMITTER_SEND_ERROR(false, HttpStatus.INTERNAL_SERVER_ERROR, "SSE 전송 중 오류가 발생했습니다."), + COMPILE_SERVER_ERROR(false, HttpStatus.BAD_GATEWAY, "컴파일 서버 오류"), + COMPILE_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "컴파일 서버로부터 응답이 지연되고 있습니다."), + TESTCASE_TIMEOUT(false, HttpStatus.GATEWAY_TIMEOUT, "테스트케이스 채점 시간이 초과되었습니다."), + REDIS_SERVER_ERROR(false, HttpStatus.INTERNAL_SERVER_ERROR, "Redis 서버 연결 실패"), + ALREADY_JUDGING(false, HttpStatus.CONFLICT, "이미 해당 문제에 대한 채점이 진행 중입니다."), + ; + private final boolean success; + private final HttpStatus status; + private final String message; } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/model/SubmissionAggregator.java b/src/main/java/org/ezcode/codetest/domain/submission/model/SubmissionAggregator.java index d1f19533..a7633d36 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/model/SubmissionAggregator.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/model/SubmissionAggregator.java @@ -2,23 +2,23 @@ public class SubmissionAggregator { - private double totalExecutionTime; + private double totalExecutionTime; - private long totalMemoryUsage; + private long totalMemoryUsage; - private int count; + private int count; - public void accumulate(double executionTime, long memoryUsage) { - totalExecutionTime += executionTime; - totalMemoryUsage += memoryUsage; - count++; - } + public void accumulate(double executionTime, long memoryUsage) { + totalExecutionTime += executionTime; + totalMemoryUsage += memoryUsage; + count++; + } - public double averageExecutionTime() { - return count == 0 ? 0.0 : Math.round(totalExecutionTime / count * 1000.0) / 1000.0; - } + public double averageExecutionTime() { + return count == 0 ? 0.0 : Math.round(totalExecutionTime / count * 1000.0) / 1000.0; + } - public long averageMemoryUsage() { - return count == 0 ? 0L : totalMemoryUsage / count; - } + public long averageMemoryUsage() { + return count == 0 ? 0L : totalMemoryUsage / count; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/model/TestcaseEvaluationInput.java b/src/main/java/org/ezcode/codetest/domain/submission/model/TestcaseEvaluationInput.java index 8fe4247e..8de981df 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/model/TestcaseEvaluationInput.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/model/TestcaseEvaluationInput.java @@ -5,27 +5,27 @@ public record TestcaseEvaluationInput( - String expectedOutput, + String expectedOutput, - String actualOutput, + String actualOutput, - String resultMessage, + String resultMessage, - boolean success, + boolean success, - double executionTime, + double executionTime, - long memoryUsage + long memoryUsage ) { - public static TestcaseEvaluationInput from (Testcase testcase, JudgeResult result) { + public static TestcaseEvaluationInput from(Testcase testcase, JudgeResult result) { return new TestcaseEvaluationInput( - testcase.getOutput(), - result.actualOutput(), - result.message(), - result.success(), - result.executionTime(), - result.memoryUsage() + testcase.getOutput(), + result.actualOutput(), + result.message(), + result.success(), + result.executionTime(), + result.memoryUsage() ); } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/model/entity/Submission.java b/src/main/java/org/ezcode/codetest/domain/submission/model/entity/Submission.java index 9cdbe6fa..509f0bab 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/model/entity/Submission.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/model/entity/Submission.java @@ -22,63 +22,67 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Submission extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne - @JoinColumn(name = "problem_id", nullable = false) - private Problem problem; - - @ManyToOne - @JoinColumn(name = "language_id", nullable = false) - private Language language; - - @Column(nullable = false, columnDefinition = "longtext") - private String code; - - @Column(nullable = false) - private String message; - - @Column(name = "testcase_passed_count", nullable = false) - private int testCasePassedCount; - - @Column(name = "testcase_total_count", nullable = false) - private int testCaseTotalCount; - - @Column(name = "execution_time", nullable = false) - private Double executionTime; - - @Column(name = "memory_usage", nullable = false) - private Long memoryUsage; - - @Builder - public Submission(User user, Problem problem, Language language, String code, String message, - int testCasePassedCount, int testCaseTotalCount, Double executionTime, Long memoryUsage) { - this.user = user; - this.problem = problem; - this.language = language; - this.code = code; - this.message = message; - this.testCasePassedCount = testCasePassedCount; - this.testCaseTotalCount = testCaseTotalCount; - this.executionTime = executionTime; - this.memoryUsage = memoryUsage; - } - - public Long getUserId() { - return this.user.getId(); - } - - public Long getProblemId() { return this.problem.getId(); } - - public String getProblemDescription() { return this.problem.getDescription(); } - - public boolean isCorrect() { - return this.testCasePassedCount == this.testCaseTotalCount; - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "problem_id", nullable = false) + private Problem problem; + + @ManyToOne + @JoinColumn(name = "language_id", nullable = false) + private Language language; + + @Column(nullable = false, columnDefinition = "longtext") + private String code; + + @Column(nullable = false) + private String message; + + @Column(name = "testcase_passed_count", nullable = false) + private int testCasePassedCount; + + @Column(name = "testcase_total_count", nullable = false) + private int testCaseTotalCount; + + @Column(name = "execution_time", nullable = false) + private Double executionTime; + + @Column(name = "memory_usage", nullable = false) + private Long memoryUsage; + + @Builder + public Submission(User user, Problem problem, Language language, String code, String message, + int testCasePassedCount, int testCaseTotalCount, Double executionTime, Long memoryUsage) { + this.user = user; + this.problem = problem; + this.language = language; + this.code = code; + this.message = message; + this.testCasePassedCount = testCasePassedCount; + this.testCaseTotalCount = testCaseTotalCount; + this.executionTime = executionTime; + this.memoryUsage = memoryUsage; + } + + public Long getUserId() { + return this.user.getId(); + } + + public Long getProblemId() { + return this.problem.getId(); + } + + public String getProblemDescription() { + return this.problem.getDescription(); + } + + public boolean isCorrect() { + return this.testCasePassedCount == this.testCaseTotalCount; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/model/entity/UserProblemResult.java b/src/main/java/org/ezcode/codetest/domain/submission/model/entity/UserProblemResult.java index dded370c..4c514321 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/model/entity/UserProblemResult.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/model/entity/UserProblemResult.java @@ -23,29 +23,29 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class UserProblemResult extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @ManyToOne - @JoinColumn(name = "problem_id", nullable = false) - private Problem problem; - - @Column(nullable = false) - private boolean isCorrect; - - @Builder - public UserProblemResult(User user, Problem problem, boolean isCorrect) { - this.user = user; - this.problem = problem; - this.isCorrect = isCorrect; - } - - public void updateResult(boolean isCorrect) { - this.isCorrect = isCorrect; - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne + @JoinColumn(name = "problem_id", nullable = false) + private Problem problem; + + @Column(nullable = false) + private boolean isCorrect; + + @Builder + public UserProblemResult(User user, Problem problem, boolean isCorrect) { + this.user = user; + this.problem = problem; + this.isCorrect = isCorrect; + } + + public void updateResult(boolean isCorrect) { + this.isCorrect = isCorrect; + } } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/repository/SubmissionRepository.java b/src/main/java/org/ezcode/codetest/domain/submission/repository/SubmissionRepository.java index c323f0db..2f79a060 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/repository/SubmissionRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/repository/SubmissionRepository.java @@ -7,9 +7,9 @@ import org.ezcode.codetest.domain.submission.model.entity.Submission; public interface SubmissionRepository { - void saveSubmission(Submission submission); + void saveSubmission(Submission submission); - List findSubmissionsByUserId(Long userId); + List findSubmissionsByUserId(Long userId); - List fetchWeeklySolveCounts(LocalDateTime startDateTime, LocalDateTime endDateTime); + List fetchWeeklySolveCounts(LocalDateTime startDateTime, LocalDateTime endDateTime); } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/repository/UserProblemResultRepository.java b/src/main/java/org/ezcode/codetest/domain/submission/repository/UserProblemResultRepository.java index f6b37caa..649a5381 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/repository/UserProblemResultRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/repository/UserProblemResultRepository.java @@ -7,22 +7,21 @@ import org.ezcode.codetest.domain.submission.model.entity.UserProblemResult; import org.springframework.data.jpa.repository.Query; - public interface UserProblemResultRepository { - Optional findUserProblemResultByUserIdAndProblemId(Long userId, Long problemId); + Optional findUserProblemResultByUserIdAndProblemId(Long userId, Long problemId); - void saveUserProblemResult(UserProblemResult userProblemResult); + void saveUserProblemResult(UserProblemResult userProblemResult); - void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect); + void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect); - @Query(""" - SELECT upr.user.id, SUM(p.score) - FROM UserProblemResult upr - JOIN upr.problem p - WHERE upr.isCorrect = true - AND (:start IS NULL OR upr.createdAt >= :start) - AND (:end IS NULL OR upr.createdAt < :end) - GROUP BY upr.user.id -""") - List findScoresBetween(LocalDateTime start, LocalDateTime end); + @Query(""" + SELECT upr.user.id, SUM(p.score) + FROM UserProblemResult upr + JOIN upr.problem p + WHERE upr.isCorrect = true + AND (:start IS NULL OR upr.createdAt >= :start) + AND (:end IS NULL OR upr.createdAt < :end) + GROUP BY upr.user.id + """) + List findScoresBetween(LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java b/src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java index faad5a62..66df757b 100644 --- a/src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/submission/service/SubmissionDomainService.java @@ -24,91 +24,92 @@ @RequiredArgsConstructor public class SubmissionDomainService { - private final SubmissionRepository submissionRepository; - private final UserProblemResultRepository userProblemResultRepository; - - @Transactional - public void finalizeSubmission(SubmissionData submissionData, SubmissionAggregator aggregator, int passedCount) { - - createSubmission(SubmissionData.toEntity( - submissionData.withAggregatedStats(aggregator), - passedCount - ) - ); - - boolean allPassed = passedCount == submissionData.getTestCaseSize(); - - getUserProblemResult(submissionData.getUserId(), submissionData.getProblemId()).ifPresentOrElse( - result -> { - if (!result.isCorrect()) { - modifyUserProblemResult(result, allPassed); - } - }, - () -> createUserProblemResult( - UserProblemResult.builder() - .user(submissionData.user()) - .problem(submissionData.problem()) - .isCorrect(allPassed) - .build() - ) - ); - } - - public AnswerEvaluation handleEvaluationAndUpdateStats( - TestcaseEvaluationInput input, ProblemInfo problemInfo, SubmissionContext context - ) { - AnswerEvaluation evaluation = - evaluate(input.expectedOutput(), input.actualOutput(), input.success(), - input.executionTime(), input.memoryUsage(), problemInfo); - - if (evaluation.isPassed()) { - context.incrementPassedCount(); - } else { - context.updateMessage(input.resultMessage()); - } - context.incrementProcessedCount(); - - collectStatistics(context.aggregator(), input.executionTime(), input.memoryUsage()); - - return evaluation; - } - - public List getSubmissions(Long userId) { - return submissionRepository.findSubmissionsByUserId(userId); - } - - public List getWeeklySolveCounts( - LocalDateTime startDateTime, LocalDateTime endDateTime - ) { - return submissionRepository.fetchWeeklySolveCounts(startDateTime, endDateTime); - } - - private AnswerEvaluation evaluate( - String expectedOutput, String actualOutput, boolean success, double executionTime, long memoryUsage, ProblemInfo problemInfo - ) { - boolean isCorrect = success && expectedOutput.strip().equals(actualOutput.strip()); - boolean timeEfficient = executionTime <= problemInfo.getTimeLimit(); - boolean memoryEfficient = memoryUsage <= problemInfo.getMemoryLimit(); - return new AnswerEvaluation(isCorrect, timeEfficient, memoryEfficient, expectedOutput, actualOutput); - } - - private void collectStatistics(SubmissionAggregator aggregator, double executionTime, long memoryUsage) { - aggregator.accumulate(executionTime, memoryUsage); - } - - private void createSubmission(Submission submission) { - submissionRepository.saveSubmission(submission); - } - - private Optional getUserProblemResult(Long userId, Long problemId) { - return userProblemResultRepository.findUserProblemResultByUserIdAndProblemId(userId, problemId); - } - - private void createUserProblemResult(UserProblemResult userProblemResult) { - userProblemResultRepository.saveUserProblemResult(userProblemResult); - } - - private void modifyUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { - userProblemResultRepository.updateUserProblemResult(userProblemResult, isCorrect); - } + private final SubmissionRepository submissionRepository; + private final UserProblemResultRepository userProblemResultRepository; + + @Transactional + public void finalizeSubmission(SubmissionData submissionData, SubmissionAggregator aggregator, int passedCount) { + + createSubmission(SubmissionData.toEntity( + submissionData.withAggregatedStats(aggregator), + passedCount + ) + ); + + boolean allPassed = passedCount == submissionData.getTestCaseSize(); + + getUserProblemResult(submissionData.getUserId(), submissionData.getProblemId()).ifPresentOrElse( + result -> { + if (!result.isCorrect()) { + modifyUserProblemResult(result, allPassed); + } + }, + () -> createUserProblemResult( + UserProblemResult.builder() + .user(submissionData.user()) + .problem(submissionData.problem()) + .isCorrect(allPassed) + .build() + ) + ); + } + + public AnswerEvaluation handleEvaluationAndUpdateStats( + TestcaseEvaluationInput input, ProblemInfo problemInfo, SubmissionContext context + ) { + AnswerEvaluation evaluation = + evaluate(input.expectedOutput(), input.actualOutput(), input.success(), + input.executionTime(), input.memoryUsage(), problemInfo); + + if (evaluation.isPassed()) { + context.incrementPassedCount(); + } else { + context.updateMessage(input.resultMessage()); + } + context.incrementProcessedCount(); + + collectStatistics(context.aggregator(), input.executionTime(), input.memoryUsage()); + + return evaluation; + } + + public List getSubmissions(Long userId) { + return submissionRepository.findSubmissionsByUserId(userId); + } + + public List getWeeklySolveCounts( + LocalDateTime startDateTime, LocalDateTime endDateTime + ) { + return submissionRepository.fetchWeeklySolveCounts(startDateTime, endDateTime); + } + + private AnswerEvaluation evaluate( + String expectedOutput, String actualOutput, boolean success, double executionTime, long memoryUsage, + ProblemInfo problemInfo + ) { + boolean isCorrect = success && expectedOutput.strip().equals(actualOutput.strip()); + boolean timeEfficient = executionTime <= problemInfo.getTimeLimit(); + boolean memoryEfficient = memoryUsage <= problemInfo.getMemoryLimit(); + return new AnswerEvaluation(isCorrect, timeEfficient, memoryEfficient, expectedOutput, actualOutput); + } + + private void collectStatistics(SubmissionAggregator aggregator, double executionTime, long memoryUsage) { + aggregator.accumulate(executionTime, memoryUsage); + } + + private void createSubmission(Submission submission) { + submissionRepository.saveSubmission(submission); + } + + private Optional getUserProblemResult(Long userId, Long problemId) { + return userProblemResultRepository.findUserProblemResultByUserIdAndProblemId(userId, problemId); + } + + private void createUserProblemResult(UserProblemResult userProblemResult) { + userProblemResultRepository.saveUserProblemResult(userProblemResult); + } + + private void modifyUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { + userProblemResultRepository.updateUserProblemResult(userProblemResult, isCorrect); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/config/RedisStreamConfig.java b/src/main/java/org/ezcode/codetest/infrastructure/event/config/RedisStreamConfig.java index 3c272a40..d8e97e39 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/config/RedisStreamConfig.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/config/RedisStreamConfig.java @@ -25,77 +25,77 @@ @RequiredArgsConstructor public class RedisStreamConfig { - private final RedisTemplate redisTemplate; - private final Executor consumerExecutor; + private final RedisTemplate redisTemplate; + private final Executor consumerExecutor; - @PostConstruct - public void initConsumerGroup() { - try { - Boolean exists = redisTemplate.hasKey("judge-queue"); + @PostConstruct + public void initConsumerGroup() { + try { + Boolean exists = redisTemplate.hasKey("judge-queue"); - if (Boolean.FALSE.equals(exists)) { - log.info("Redis Stream 'judge-queue'를 생성합니다."); - redisTemplate.opsForStream().add("judge-queue", Map.of( - "emitterKey", "dummy", - "problemId", "0", - "languageId", "0", - "userId", "0", - "sourceCode", "dummy" - )); - } + if (Boolean.FALSE.equals(exists)) { + log.info("Redis Stream 'judge-queue'를 생성합니다."); + redisTemplate.opsForStream().add("judge-queue", Map.of( + "emitterKey", "dummy", + "problemId", "0", + "languageId", "0", + "userId", "0", + "sourceCode", "dummy" + )); + } - try { - log.info("Redis Consumer Group의 기존 컨슈머 삭제 시도"); - redisTemplate.execute((RedisConnection connection) -> { - connection.xGroupDelConsumer( - "judge-queue".getBytes(), - "judge-group", - "consumer-1" - ); - return null; - }); - } catch (Exception e) { - log.warn("DELCONSUMER 중 오류 발생: {}", e.getMessage()); - } + try { + log.info("Redis Consumer Group의 기존 컨슈머 삭제 시도"); + redisTemplate.execute((RedisConnection connection) -> { + connection.xGroupDelConsumer( + "judge-queue".getBytes(), + "judge-group", + "consumer-1" + ); + return null; + }); + } catch (Exception e) { + log.warn("DELCONSUMER 중 오류 발생: {}", e.getMessage()); + } - redisTemplate.opsForStream().createGroup("judge-queue", ReadOffset.latest(), "judge-group"); + redisTemplate.opsForStream().createGroup("judge-queue", ReadOffset.latest(), "judge-group"); - log.info("Redis Stream 'judge-queue'에 대한 Consumer Group 'judge-group'을 생성했습니다."); - } catch (Exception e) { - log.error("예외 발생: {}, 메시지: {}", e.getClass(), e.getMessage()); - if (e.getCause() instanceof io.lettuce.core.RedisBusyException) { - log.info("Redis Consumer Group 'judge-group'이 이미 존재하여 생성을 건너뜁니다."); - } else { - log.error("Redis Consumer Group 초기화에 실패했습니다.", e); - throw e; - } - } - } + log.info("Redis Stream 'judge-queue'에 대한 Consumer Group 'judge-group'을 생성했습니다."); + } catch (Exception e) { + log.error("예외 발생: {}, 메시지: {}", e.getClass(), e.getMessage()); + if (e.getCause() instanceof io.lettuce.core.RedisBusyException) { + log.info("Redis Consumer Group 'judge-group'이 이미 존재하여 생성을 건너뜁니다."); + } else { + log.error("Redis Consumer Group 초기화에 실패했습니다.", e); + throw e; + } + } + } - @Bean(destroyMethod = "stop") - public StreamMessageListenerContainer> streamMessageListenerContainer( - RedisConnectionFactory factory, - RedisJudgeQueueConsumer consumer - ) { - StreamMessageListenerContainer - .StreamMessageListenerContainerOptions> options = - StreamMessageListenerContainer - .StreamMessageListenerContainerOptions - .builder() - .executor(consumerExecutor) - .pollTimeout(Duration.ofSeconds(2)) - .build(); + @Bean(destroyMethod = "stop") + public StreamMessageListenerContainer> streamMessageListenerContainer( + RedisConnectionFactory factory, + RedisJudgeQueueConsumer consumer + ) { + StreamMessageListenerContainer + .StreamMessageListenerContainerOptions> options = + StreamMessageListenerContainer + .StreamMessageListenerContainerOptions + .builder() + .executor(consumerExecutor) + .pollTimeout(Duration.ofSeconds(2)) + .build(); - StreamMessageListenerContainer> container = - StreamMessageListenerContainer.create(factory, options); + StreamMessageListenerContainer> container = + StreamMessageListenerContainer.create(factory, options); - container.receive( - Consumer.from("judge-group", "consumer-1"), - StreamOffset.create("judge-queue", ReadOffset.lastConsumed()), - consumer - ); + container.receive( + Consumer.from("judge-group", "consumer-1"), + StreamOffset.create("judge-queue", ReadOffset.lastConsumed()), + consumer + ); - container.start(); - return container; - } + container.start(); + return container; + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/dto/SubmissionMessage.java b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/SubmissionMessage.java index e89a98bc..b3224029 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/dto/SubmissionMessage.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/SubmissionMessage.java @@ -2,15 +2,15 @@ public record SubmissionMessage( - String emitterKey, + String emitterKey, - Long problemId, + Long problemId, - Long languageId, + Long languageId, - Long userId, + Long userId, - String sourceCode + String sourceCode ) { } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java index e5ff5681..1e3b45af 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/RedisJudgeQueueConsumer.java @@ -19,30 +19,30 @@ @RequiredArgsConstructor public class RedisJudgeQueueConsumer implements StreamListener> { - private final SubmissionService submissionService; - private final StringRedisTemplate redisTemplate; - - @Override - public void onMessage(MapRecord message) { - Map values = message.getValue(); - - SubmissionMessage msg = new SubmissionMessage( - values.get("emitterKey"), - Long.valueOf(values.get("problemId")), - Long.valueOf(values.get("languageId")), - Long.valueOf(values.get("userId")), - values.get("sourceCode") - ); - - try { - log.info("[컨슈머 수신] {}", msg.emitterKey()); - submissionService.submitCodeStream(msg); - - log.info("[컨슈머 ACK] messageId={}", message.getId()); - redisTemplate.opsForStream().acknowledge("judge-group", message); - } catch (Exception e) { - log.error("채점 메시지 처리 실패: {}", message.getId(), e); - throw new SubmissionException(SubmissionExceptionCode.REDIS_SERVER_ERROR); - } - } + private final SubmissionService submissionService; + private final StringRedisTemplate redisTemplate; + + @Override + public void onMessage(MapRecord message) { + Map values = message.getValue(); + + SubmissionMessage msg = new SubmissionMessage( + values.get("emitterKey"), + Long.valueOf(values.get("problemId")), + Long.valueOf(values.get("languageId")), + Long.valueOf(values.get("userId")), + values.get("sourceCode") + ); + + try { + log.info("[컨슈머 수신] {}", msg.emitterKey()); + submissionService.submitCodeStream(msg); + + log.info("[컨슈머 ACK] messageId={}", message.getId()); + redisTemplate.opsForStream().acknowledge("judge-group", message); + } catch (Exception e) { + log.error("채점 메시지 처리 실패: {}", message.getId(), e); + throw new SubmissionException(SubmissionExceptionCode.REDIS_SERVER_ERROR); + } + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/scheduler/RedisStreamCleanupScheduler.java b/src/main/java/org/ezcode/codetest/infrastructure/event/scheduler/RedisStreamCleanupScheduler.java index 54b741b2..fe2991af 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/scheduler/RedisStreamCleanupScheduler.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/scheduler/RedisStreamCleanupScheduler.java @@ -12,17 +12,17 @@ @RequiredArgsConstructor public class RedisStreamCleanupScheduler { - private final StringRedisTemplate redisTemplate; + private final StringRedisTemplate redisTemplate; - @Scheduled(fixedRate = 600000) - public void cleanUpOldMessages() { - try { - Long trimmed = redisTemplate.opsForStream() - .trim("judge-queue", 5); + @Scheduled(fixedRate = 600000) + public void cleanUpOldMessages() { + try { + Long trimmed = redisTemplate.opsForStream() + .trim("judge-queue", 5); - log.info("Redis Stream 트림(정리) 작업이 실행되었습니다. 삭제된 메시지 개수: {}", trimmed); - } catch (Exception e) { - log.error("Redis Stream 트림(정리) 작업에 실패했습니다.", e); - } - } + log.info("Redis Stream 트림(정리) 작업이 실행되었습니다. 삭제된 메시지 개수: {}", trimmed); + } catch (Exception e) { + log.error("Redis Stream 트림(정리) 작업에 실패했습니다.", e); + } + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/service/DiscordNotifier.java b/src/main/java/org/ezcode/codetest/infrastructure/event/service/DiscordNotifier.java index 15e5aa94..95fb5176 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/service/DiscordNotifier.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/service/DiscordNotifier.java @@ -20,52 +20,52 @@ @Component public class DiscordNotifier implements ExceptionNotifier { - @Value("${discord.webhook.url}") - private String webhookUrl; - private final RestTemplate restTemplate = new RestTemplate(); - private final ObjectMapper objectMapper = new ObjectMapper(); + @Value("${discord.webhook.url}") + private String webhookUrl; + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); - @Override - public void sendEmbed(String title, String description, String exception, String methodName) { - try { - Map embed = Map.of( - "title", title, - "description", description, - "color", 16711680, - "fields", List.of( - Map.of( - "name", "예외 메시지", - "value", exception, - "inline", false - ), - Map.of( - "name", "발생 메서드", - "value", methodName, - "inline", false - ), - Map.of( - "name", "발생 시각", - "value", Instant.now().toString(), - "inline", false - ) - ) - ); + @Override + public void sendEmbed(String title, String description, String exception, String methodName) { + try { + Map embed = Map.of( + "title", title, + "description", description, + "color", 16711680, + "fields", List.of( + Map.of( + "name", "예외 메시지", + "value", exception, + "inline", false + ), + Map.of( + "name", "발생 메서드", + "value", methodName, + "inline", false + ), + Map.of( + "name", "발생 시각", + "value", Instant.now().toString(), + "inline", false + ) + ) + ); - Map payload = Map.of( - "embeds", List.of(embed) - ); + Map payload = Map.of( + "embeds", List.of(embed) + ); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); - String body = objectMapper.writeValueAsString(payload); + String body = objectMapper.writeValueAsString(payload); - HttpEntity entity = new HttpEntity<>(body, headers); + HttpEntity entity = new HttpEntity<>(body, headers); - restTemplate.postForEntity(webhookUrl, entity, String.class); - } catch (Exception e) { - log.error("Discord 웹훅 전송 실패", e); - } - } + restTemplate.postForEntity(webhookUrl, entity, String.class); + } catch (Exception e) { + log.error("Discord 웹훅 전송 실패", e); + } + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/service/RedisJudgeQueueProducer.java b/src/main/java/org/ezcode/codetest/infrastructure/event/service/RedisJudgeQueueProducer.java index 836c9040..6dcd6286 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/service/RedisJudgeQueueProducer.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/service/RedisJudgeQueueProducer.java @@ -15,18 +15,18 @@ @RequiredArgsConstructor public class RedisJudgeQueueProducer implements QueueProducer { - private final RedisTemplate redisTemplate; + private final RedisTemplate redisTemplate; - public void enqueue(SubmissionMessage submissionMessage) { - log.info("[SSE enqueue] emitterKey: {}", submissionMessage.emitterKey()); - Map map = Map.of( - "emitterKey", submissionMessage.emitterKey(), - "problemId", submissionMessage.problemId().toString(), - "languageId", submissionMessage.languageId().toString(), - "userId", submissionMessage.userId().toString(), - "sourceCode", submissionMessage.sourceCode() - ); + public void enqueue(SubmissionMessage submissionMessage) { + log.info("[SSE enqueue] emitterKey: {}", submissionMessage.emitterKey()); + Map map = Map.of( + "emitterKey", submissionMessage.emitterKey(), + "problemId", submissionMessage.problemId().toString(), + "languageId", submissionMessage.languageId().toString(), + "userId", submissionMessage.userId().toString(), + "sourceCode", submissionMessage.sourceCode() + ); - redisTemplate.opsForStream().add("judge-queue", map); - } + redisTemplate.opsForStream().add("judge-queue", map); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java index d733da36..6410ba0d 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java @@ -20,72 +20,72 @@ @RequiredArgsConstructor public class Judge0Client implements JudgeClient { - @Value("${external.judge0.url}") - private String judge0ApiUrl; - private WebClient webClient; - private final Judge0ResponseMapper interpreter; + @Value("${external.judge0.url}") + private String judge0ApiUrl; + private WebClient webClient; + private final Judge0ResponseMapper interpreter; - @PostConstruct - private void init() { - this.webClient = WebClient.create(judge0ApiUrl); - } + @PostConstruct + private void init() { + this.webClient = WebClient.create(judge0ApiUrl); + } - @Override - public String submitAndGetToken(CodeCompileRequest request) { - try { - ExecutionResultResponse executionResultResponse = webClient.post() - .uri("/submissions?base64_encoded=false&wait=false") - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(request) - .retrieve() - .bodyToMono(ExecutionResultResponse.class) - .block(); + @Override + public String submitAndGetToken(CodeCompileRequest request) { + try { + ExecutionResultResponse executionResultResponse = webClient.post() + .uri("/submissions?base64_encoded=false&wait=false") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(request) + .retrieve() + .bodyToMono(ExecutionResultResponse.class) + .block(); - if (executionResultResponse == null) { - throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); - } + if (executionResultResponse == null) { + throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); + } - return executionResultResponse.token(); - } catch (Exception e) { - throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); - } - } + return executionResultResponse.token(); + } catch (Exception e) { + throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); + } + } - @Override - public JudgeResult pollUntilDone(String token) { - try { - int maxAttempts = 30; - int attempt = 0; + @Override + public JudgeResult pollUntilDone(String token) { + try { + int maxAttempts = 30; + int attempt = 0; - while (attempt < maxAttempts) { - ExecutionResultResponse executionResultResponse = webClient.get() - .uri("/submissions/{token}", token) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(ExecutionResultResponse.class) - .block(); + while (attempt < maxAttempts) { + ExecutionResultResponse executionResultResponse = webClient.get() + .uri("/submissions/{token}", token) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(ExecutionResultResponse.class) + .block(); - if (executionResultResponse == null) { - throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); - } + if (executionResultResponse == null) { + throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); + } - if (executionResultResponse.status().id() >= 3) { - return interpreter.toJudgeResult(executionResultResponse); - } + if (executionResultResponse.status().id() >= 3) { + return interpreter.toJudgeResult(executionResultResponse); + } - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT); - } - attempt++; - } - throw new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT); - } catch (SubmissionException se) { - throw se; - } catch (Exception e) { - throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); - } - } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT); + } + attempt++; + } + throw new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT); + } catch (SubmissionException se) { + throw se; + } catch (Exception e) { + throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); + } + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java index f0cbdc06..cb64f448 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java @@ -7,26 +7,29 @@ @Component public class Judge0ResponseMapper { - public JudgeResult toJudgeResult(ExecutionResultResponse executionResultResponse) { - String output = extractActualOutput(executionResultResponse); - boolean success = isSuccessful(executionResultResponse); - return JudgeResult.builder() - .actualOutput(output) - .executionTime(executionResultResponse.getTime()) - .memoryUsage(executionResultResponse.getMemory()) - .success(success) - .message(executionResultResponse.status().description()) - .build(); - } + public JudgeResult toJudgeResult(ExecutionResultResponse executionResultResponse) { + String output = extractActualOutput(executionResultResponse); + boolean success = isSuccessful(executionResultResponse); + return JudgeResult.builder() + .actualOutput(output) + .executionTime(executionResultResponse.getTime()) + .memoryUsage(executionResultResponse.getMemory()) + .success(success) + .message(executionResultResponse.status().description()) + .build(); + } - private String extractActualOutput(ExecutionResultResponse executionResultResponse) { - if (executionResultResponse.stdout() != null) return executionResultResponse.stdout(); - if (executionResultResponse.compile_output() != null) return executionResultResponse.compile_output(); - if (executionResultResponse.stderr() != null) return executionResultResponse.stderr(); - return "(No output)"; - } + private String extractActualOutput(ExecutionResultResponse executionResultResponse) { + if (executionResultResponse.stdout() != null) + return executionResultResponse.stdout(); + if (executionResultResponse.compile_output() != null) + return executionResultResponse.compile_output(); + if (executionResultResponse.stderr() != null) + return executionResultResponse.stderr(); + return "(No output)"; + } - private boolean isSuccessful(ExecutionResultResponse executionResultResponse) { - return executionResultResponse.stdout() != null && executionResultResponse.status().id() == 3; - } + private boolean isSuccessful(ExecutionResultResponse executionResultResponse) { + return executionResultResponse.stdout() != null && executionResultResponse.status().id() == 3; + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/lock/RedisLockManager.java b/src/main/java/org/ezcode/codetest/infrastructure/lock/RedisLockManager.java index 8ebf4a58..59043921 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/lock/RedisLockManager.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/lock/RedisLockManager.java @@ -12,24 +12,24 @@ @RequiredArgsConstructor public class RedisLockManager implements LockManager { - private final StringRedisTemplate redisTemplate; - - private static final String LOCK_KEY_FORMAT = "%s-lock:user:%d:problem:%d"; - private static final Duration LOCK_DURATION = Duration.ofMinutes(5); - - @Override - public boolean tryLock(String prefix, Long userId, Long problemId) { - String key = getKey(prefix, userId, problemId); - Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "LOCKED", LOCK_DURATION); - return Boolean.TRUE.equals(success); - } - - @Override - public void releaseLock(String prefix, Long userId, Long problemId) { - redisTemplate.delete(getKey(prefix, userId, problemId)); - } - - private String getKey(String prefix, Long userId, Long problemId) { - return LOCK_KEY_FORMAT.formatted(prefix, userId, problemId); - } + private final StringRedisTemplate redisTemplate; + + private static final String LOCK_KEY_FORMAT = "%s-lock:user:%d:problem:%d"; + private static final Duration LOCK_DURATION = Duration.ofMinutes(5); + + @Override + public boolean tryLock(String prefix, Long userId, Long problemId) { + String key = getKey(prefix, userId, problemId); + Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "LOCKED", LOCK_DURATION); + return Boolean.TRUE.equals(success); + } + + @Override + public void releaseLock(String prefix, Long userId, Long problemId) { + redisTemplate.delete(getKey(prefix, userId, problemId)); + } + + private String getKey(String prefix, Long userId, Long problemId) { + return LOCK_KEY_FORMAT.formatted(prefix, userId, problemId); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIMessageBuilder.java b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIMessageBuilder.java index 23938ab9..7f736c68 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIMessageBuilder.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIMessageBuilder.java @@ -9,90 +9,90 @@ @Component class OpenAIMessageBuilder { - private static final String MODEL_NAME = "o4-mini"; + private static final String MODEL_NAME = "o4-mini"; - private static final String PREFIX = """ - 당신은 코딩 테스트 사이트의 코드 리뷰어입니다. - 아래 **정확히** 이 형식을 지켜 응답하세요: - """.stripIndent(); + private static final String PREFIX = """ + 당신은 코딩 테스트 사이트의 코드 리뷰어입니다. + 아래 **정확히** 이 형식을 지켜 응답하세요: + """.stripIndent(); - private static final String SUFFIX = """ - **주의사항** - 1. 절대 코드 전문이나 정답을 알려주지 마세요. - 2. 절대 코드의 일부분을 작성하지 마세요. - 3. 칭찬할 건 칭찬하되 지적할 건 냉정하게 지적하세요. - 4. 늘 존댓말을 사용하세요. - 5. 이모지는 절대 사용하지 마세요. - 6. 다른 형식으로 답변하면 안 됩니다. - """.stripIndent(); + private static final String SUFFIX = """ + **주의사항** + 1. 절대 코드 전문이나 정답을 알려주지 마세요. + 2. 절대 코드의 일부분을 작성하지 마세요. + 3. 칭찬할 건 칭찬하되 지적할 건 냉정하게 지적하세요. + 4. 늘 존댓말을 사용하세요. + 5. 이모지는 절대 사용하지 마세요. + 6. 다른 형식으로 답변하면 안 됩니다. + """.stripIndent(); - public Map buildRequestBody(ReviewPayload reviewPayload) { - String systemPrompt = buildSystemPrompt(reviewPayload.isCorrect()); - String userPrompt = buildUserPrompt(reviewPayload); + public Map buildRequestBody(ReviewPayload reviewPayload) { + String systemPrompt = buildSystemPrompt(reviewPayload.isCorrect()); + String userPrompt = buildUserPrompt(reviewPayload); - List> messages = List.of( - Map.of("role", "system", "content", systemPrompt), - Map.of("role", "user", "content", userPrompt) - ); + List> messages = List.of( + Map.of("role", "system", "content", systemPrompt), + Map.of("role", "user", "content", userPrompt) + ); - return Map.of( - "model", MODEL_NAME, - "messages", messages - ); - } + return Map.of( + "model", MODEL_NAME, + "messages", messages + ); + } - private String buildUserPrompt(ReviewPayload request) { - return "문제: " - + request.problemDescription() - + "\n" - + "언어: " - + request.languageName() - + "\n" - + "정답 여부: " - + (request.isCorrect() ? "정답" : "오답") - + "\n" - + "```" - + request.languageName().toLowerCase() - + "\n" - + request.sourceCode() + "```"; - } + private String buildUserPrompt(ReviewPayload request) { + return "문제: " + + request.problemDescription() + + "\n" + + "언어: " + + request.languageName() + + "\n" + + "정답 여부: " + + (request.isCorrect() ? "정답" : "오답") + + "\n" + + "```" + + request.languageName().toLowerCase() + + "\n" + + request.sourceCode() + "```"; + } - private String buildSystemPrompt(boolean isCorrect) { + private String buildSystemPrompt(boolean isCorrect) { - String body; - if (isCorrect) { - body = """ - <정답일 경우> - - 시간 복잡도: Big-O 표기법으로만 답하세요. **단, N과 M을 같다고 가정하고 n으로 표기하세요.** - - 코드에 포함된 중첩 루프(depth)에 따라 O(N^k) 형태로 정확히 표기해주세요. - **for 루프뿐만 아니라 while 루프도 모두 중첩(depth)에 포함**하여, 코드에 실제로 있는 루프 개수만큼 exponent를 세십시오. - 예) for-for-for ⇒ O(n³), for-for-while ⇒ O(n³), for-for-for-for-while ⇒ O(n⁵) - - - 코드 총평: - 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. - 문장 끝에만 마침표를 붙이세요. - - 조금 더 개선할 수 있는 방안: - 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. - 문장 끝에만 마침표를 붙이세요. - """.stripIndent(); - } else { - body = """ - <오답일 경우> - 코드 총평: - 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. - 문장 끝에만 마침표를 붙이세요. - - 공부하면 좋은 키워드: - 1. 첫 번째 키워드 - 2. 두 번째 키워드 - 3. 세 번째 키워드 - … 필요한 만큼 번호를 늘려주세요. - """.stripIndent(); - } - - return PREFIX + "\n" + body + "\n" + SUFFIX; - } + String body; + if (isCorrect) { + body = """ + <정답일 경우> + - 시간 복잡도: Big-O 표기법으로만 답하세요. **단, N과 M을 같다고 가정하고 n으로 표기하세요.** + - 코드에 포함된 중첩 루프(depth)에 따라 O(N^k) 형태로 정확히 표기해주세요. + **for 루프뿐만 아니라 while 루프도 모두 중첩(depth)에 포함**하여, 코드에 실제로 있는 루프 개수만큼 exponent를 세십시오. + 예) for-for-for ⇒ O(n³), for-for-while ⇒ O(n³), for-for-for-for-while ⇒ O(n⁵) + + - 코드 총평: + 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. + 문장 끝에만 마침표를 붙이세요. + - 조금 더 개선할 수 있는 방안: + 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. + 문장 끝에만 마침표를 붙이세요. + """.stripIndent(); + } else { + body = """ + <오답일 경우> + 코드 총평: + 각 문장은 한 탭(\t) 들여쓰기 + '- ' 로 시작. + 문장 끝에만 마침표를 붙이세요. + - 공부하면 좋은 키워드: + 1. 첫 번째 키워드 + 2. 두 번째 키워드 + 3. 세 번째 키워드 + … 필요한 만큼 번호를 늘려주세요. + """.stripIndent(); + } - protected String buildErrorMessage() { - return "현재 리뷰 생성에 일시적인 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."; - } + return PREFIX + "\n" + body + "\n" + SUFFIX; + } + + protected String buildErrorMessage() { + return "현재 리뷰 생성에 일시적인 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."; + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIResponseValidator.java b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIResponseValidator.java index fe5bae60..f8fa919b 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIResponseValidator.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIResponseValidator.java @@ -4,16 +4,17 @@ @Component class OpenAIResponseValidator { - protected boolean isValidFormat(String content, boolean isCorrect) { - if (content == null) return false; + protected boolean isValidFormat(String content, boolean isCorrect) { + if (content == null) + return false; - if (isCorrect) { - return content.contains("시간 복잡도:") && - content.contains("코드 총평:") && - content.contains("조금 더 개선할 수 있는 방안:"); - } + if (isCorrect) { + return content.contains("시간 복잡도:") && + content.contains("코드 총평:") && + content.contains("조금 더 개선할 수 있는 방안:"); + } - return content.contains("코드 총평:") && - content.contains("공부하면 좋은 키워드:"); - } + return content.contains("코드 총평:") && + content.contains("공부하면 좋은 키워드:"); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java index 0c60915b..984c4bcc 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java @@ -29,71 +29,70 @@ @RequiredArgsConstructor public class OpenAIReviewClient implements ReviewClient { - @Value("${OPEN_API_URL}") - private String openApiUrl; - - @Value("${OPEN_API_KEY}") - private String openApiKey; - private WebClient webClient; - private final OpenAIMessageBuilder openAiMessageBuilder; - private final OpenAIResponseValidator openAiResponseValidator; - - - @PostConstruct - private void init() { - this.webClient = WebClient.builder() - .baseUrl(openApiUrl) - .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openApiKey) - .build(); - } - - @Override - public ReviewResult requestReview(ReviewPayload reviewPayload) { - Map requestBody = openAiMessageBuilder.buildRequestBody(reviewPayload); - - String content; - int maxAttempts = 3; - - for (int attempt = 1; attempt <= maxAttempts; attempt++) { - try { - content = callChatApi(requestBody); - } catch (CodeReviewException e) { - log.error("OpenAI API 호출 실패: {}, {}", e.getHttpStatus(), e.getMessage()); - TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); - return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); - } - - if (openAiResponseValidator.isValidFormat(content, reviewPayload.isCorrect())) { - return new ReviewResult(content); - } - log.warn("[{}/{}][isCorrect={}] 포맷 검증 실패:\n{}", attempt, maxAttempts, reviewPayload.isCorrect(), content); - } - - return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); - } - - private String callChatApi (Map requestBody){ - - OpenAIResponse response = webClient.post() - .uri("/v1/chat/completions") - .bodyValue(requestBody) - .retrieve() - .bodyToMono(OpenAIResponse.class) - .timeout(Duration.ofSeconds(10)) - .retryWhen( - Retry.backoff(3, Duration.ofSeconds(1)) - .maxBackoff(Duration.ofSeconds(5)) - .filter(ex -> ex instanceof WebClientResponseException - || ex instanceof TimeoutException) - .onRetryExhaustedThrow((spec, signal) -> signal.failure()) - ) - .onErrorMap(WebClientResponseException.class, - ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_SERVER_ERROR)) - .onErrorMap(TimeoutException.class, - ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_TIMEOUT)) - .block(); - - return Objects.requireNonNull(response).getReviewContent(); - } + @Value("${OPEN_API_URL}") + private String openApiUrl; + + @Value("${OPEN_API_KEY}") + private String openApiKey; + private WebClient webClient; + private final OpenAIMessageBuilder openAiMessageBuilder; + private final OpenAIResponseValidator openAiResponseValidator; + + @PostConstruct + private void init() { + this.webClient = WebClient.builder() + .baseUrl(openApiUrl) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openApiKey) + .build(); + } + + @Override + public ReviewResult requestReview(ReviewPayload reviewPayload) { + Map requestBody = openAiMessageBuilder.buildRequestBody(reviewPayload); + + String content; + int maxAttempts = 3; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) { + try { + content = callChatApi(requestBody); + } catch (CodeReviewException e) { + log.error("OpenAI API 호출 실패: {}, {}", e.getHttpStatus(), e.getMessage()); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); + } + + if (openAiResponseValidator.isValidFormat(content, reviewPayload.isCorrect())) { + return new ReviewResult(content); + } + log.warn("[{}/{}][isCorrect={}] 포맷 검증 실패:\n{}", attempt, maxAttempts, reviewPayload.isCorrect(), content); + } + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); + } + + private String callChatApi(Map requestBody) { + + OpenAIResponse response = webClient.post() + .uri("/v1/chat/completions") + .bodyValue(requestBody) + .retrieve() + .bodyToMono(OpenAIResponse.class) + .timeout(Duration.ofSeconds(10)) + .retryWhen( + Retry.backoff(3, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(5)) + .filter(ex -> ex instanceof WebClientResponseException + || ex instanceof TimeoutException) + .onRetryExhaustedThrow((spec, signal) -> signal.failure()) + ) + .onErrorMap(WebClientResponseException.class, + ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_SERVER_ERROR)) + .onErrorMap(TimeoutException.class, + ex -> new CodeReviewException(CodeReviewExceptionCode.REVIEW_TIMEOUT)) + .block(); + + return Objects.requireNonNull(response).getReviewContent(); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/LanguageRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/LanguageRepositoryImpl.java index 2a032aee..5220a1fa 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/LanguageRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/LanguageRepositoryImpl.java @@ -14,40 +14,40 @@ @RequiredArgsConstructor public class LanguageRepositoryImpl implements LanguageRepository { - private final LanguageJpaRepository languageJpaRepository; - - @Override - public boolean existsById(Long languageId) { - return languageJpaRepository.existsById(languageId); - } - - @Override - public boolean existsByNameAndVersion(String name, String version) { - return languageJpaRepository.existsByNameAndVersion(name, version); - } - - @Override - public Language saveLanguage(Language language) { - return languageJpaRepository.save(language); - } - - @Override - public Optional findLanguageById(Long languageId) { - return languageJpaRepository.findById(languageId); - } - - @Override - public List findLanguages() { - return languageJpaRepository.findAll(); - } - - @Override - public void updateLanguage(Language language, Long judge0Id) { - language.updateJudge0Id(judge0Id); - } - - @Override - public void deleteLanguage(Long languageId) { - languageJpaRepository.deleteById(languageId); - } + private final LanguageJpaRepository languageJpaRepository; + + @Override + public boolean existsById(Long languageId) { + return languageJpaRepository.existsById(languageId); + } + + @Override + public boolean existsByNameAndVersion(String name, String version) { + return languageJpaRepository.existsByNameAndVersion(name, version); + } + + @Override + public Language saveLanguage(Language language) { + return languageJpaRepository.save(language); + } + + @Override + public Optional findLanguageById(Long languageId) { + return languageJpaRepository.findById(languageId); + } + + @Override + public List findLanguages() { + return languageJpaRepository.findAll(); + } + + @Override + public void updateLanguage(Language language, Long judge0Id) { + language.updateJudge0Id(judge0Id); + } + + @Override + public void deleteLanguage(Long languageId) { + languageJpaRepository.deleteById(languageId); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/SubmissionRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/SubmissionRepositoryImpl.java index 450a7d91..e8281f89 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/SubmissionRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/SubmissionRepositoryImpl.java @@ -16,21 +16,21 @@ @RequiredArgsConstructor public class SubmissionRepositoryImpl implements SubmissionRepository { - private final SubmissionJpaRepository submissionJpaRepository; - private final SubmissionQueryRepository submissionQueryRepository; - - @Override - public void saveSubmission(Submission submission) { - submissionJpaRepository.save(submission); - } - - @Override - public List findSubmissionsByUserId(Long userId) { - return submissionJpaRepository.findAllByUser_Id(userId); - } - - @Override - public List fetchWeeklySolveCounts(LocalDateTime startDateTime, LocalDateTime endDateTime) { - return submissionQueryRepository.fetchWeeklySolveCounts(startDateTime, endDateTime); - } + private final SubmissionJpaRepository submissionJpaRepository; + private final SubmissionQueryRepository submissionQueryRepository; + + @Override + public void saveSubmission(Submission submission) { + submissionJpaRepository.save(submission); + } + + @Override + public List findSubmissionsByUserId(Long userId) { + return submissionJpaRepository.findAllByUser_Id(userId); + } + + @Override + public List fetchWeeklySolveCounts(LocalDateTime startDateTime, LocalDateTime endDateTime) { + return submissionQueryRepository.fetchWeeklySolveCounts(startDateTime, endDateTime); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/UserProblemResultRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/UserProblemResultRepositoryImpl.java index c611305d..512c5350 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/UserProblemResultRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/impl/UserProblemResultRepositoryImpl.java @@ -15,27 +15,26 @@ @RequiredArgsConstructor public class UserProblemResultRepositoryImpl implements UserProblemResultRepository { - private final UserProblemResultJpaRepository userProblemResultJpaRepository; - - @Override - public Optional findUserProblemResultByUserIdAndProblemId(Long userId, Long problemId) { - return userProblemResultJpaRepository.findByUserIdAndProblemId(userId, problemId); - } - - @Override - public void saveUserProblemResult(UserProblemResult userProblemResult) { - userProblemResultJpaRepository.save(userProblemResult); - } - - @Override - public void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { - userProblemResult.updateResult(isCorrect); - } - - @Override - public List findScoresBetween(LocalDateTime start, LocalDateTime end) { - return userProblemResultJpaRepository.findScoresBetween(start, end); - } - + private final UserProblemResultJpaRepository userProblemResultJpaRepository; + + @Override + public Optional findUserProblemResultByUserIdAndProblemId(Long userId, Long problemId) { + return userProblemResultJpaRepository.findByUserIdAndProblemId(userId, problemId); + } + + @Override + public void saveUserProblemResult(UserProblemResult userProblemResult) { + userProblemResultJpaRepository.save(userProblemResult); + } + + @Override + public void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { + userProblemResult.updateResult(isCorrect); + } + + @Override + public List findScoresBetween(LocalDateTime start, LocalDateTime end) { + return userProblemResultJpaRepository.findScoresBetween(start, end); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/LanguageJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/LanguageJpaRepository.java index cfea5237..c2044590 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/LanguageJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/LanguageJpaRepository.java @@ -4,5 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface LanguageJpaRepository extends JpaRepository { - boolean existsByNameAndVersion(String name, String version); + boolean existsByNameAndVersion(String name, String version); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/SubmissionJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/SubmissionJpaRepository.java index fa7b5d69..b3f1e042 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/SubmissionJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/SubmissionJpaRepository.java @@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface SubmissionJpaRepository extends JpaRepository { - List findAllByUser_Id(Long userId); + List findAllByUser_Id(Long userId); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/UserProblemResultJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/UserProblemResultJpaRepository.java index 113b7356..12b0dab2 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/UserProblemResultJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/jpa/UserProblemResultJpaRepository.java @@ -9,17 +9,17 @@ import org.springframework.data.jpa.repository.Query; public interface UserProblemResultJpaRepository extends JpaRepository { - Optional findByUserIdAndProblemId(Long userId, Long problemId); + Optional findByUserIdAndProblemId(Long userId, Long problemId); - @Query(""" - SELECT upr.user.id, SUM(p.score) - FROM UserProblemResult upr - JOIN upr.problem p - WHERE upr.isCorrect = true - AND (:start IS NULL OR upr.createdAt >= :start) - AND (:end IS NULL OR upr.createdAt < :end) - GROUP BY upr.user.id -""") - List findScoresBetween(LocalDateTime start, LocalDateTime end); + @Query(""" + SELECT upr.user.id, SUM(p.score) + FROM UserProblemResult upr + JOIN upr.problem p + WHERE upr.isCorrect = true + AND (:start IS NULL OR upr.createdAt >= :start) + AND (:end IS NULL OR upr.createdAt < :end) + GROUP BY upr.user.id + """) + List findScoresBetween(LocalDateTime start, LocalDateTime end); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepository.java index 33c46342..a4ef3ad0 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepository.java @@ -6,7 +6,7 @@ import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount; public interface SubmissionQueryRepository { - List fetchWeeklySolveCounts( - LocalDateTime startDateTime, LocalDateTime endDateTime - ); + List fetchWeeklySolveCounts( + LocalDateTime startDateTime, LocalDateTime endDateTime + ); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepositoryImpl.java index 12ece867..bbc5d831 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/submission/query/SubmissionQueryRepositoryImpl.java @@ -8,6 +8,7 @@ import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount; import org.ezcode.codetest.domain.submission.model.entity.QSubmission; import org.springframework.stereotype.Repository; + import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -17,32 +18,32 @@ @RequiredArgsConstructor public class SubmissionQueryRepositoryImpl implements SubmissionQueryRepository { - private final JPAQueryFactory jpaQueryFactory; - - @Override - public List fetchWeeklySolveCounts( - LocalDateTime startDateTime, - LocalDateTime endDateTime - ) { - - QSubmission s = QSubmission.submission; - - var dateOnly = Expressions.dateTemplate(java.sql.Date.class, "function('date',{0})", s.createdAt); - var cntExpr = dateOnly.countDistinct(); - - return jpaQueryFactory - .select(constructor( - WeeklySolveCount.class, - s.user.id, - cntExpr - )) - .from(s) - .where( - s.createdAt.goe(startDateTime), - s.createdAt.lt(endDateTime), - s.testCasePassedCount.eq(s.testCaseTotalCount) - ) - .groupBy(s.user.id) - .fetch(); - } + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List fetchWeeklySolveCounts( + LocalDateTime startDateTime, + LocalDateTime endDateTime + ) { + + QSubmission s = QSubmission.submission; + + var dateOnly = Expressions.dateTemplate(java.sql.Date.class, "function('date',{0})", s.createdAt); + var cntExpr = dateOnly.countDistinct(); + + return jpaQueryFactory + .select(constructor( + WeeklySolveCount.class, + s.user.id, + cntExpr + )) + .from(s) + .where( + s.createdAt.goe(startDateTime), + s.createdAt.lt(endDateTime), + s.testCasePassedCount.eq(s.testCaseTotalCount) + ) + .groupBy(s.user.id) + .fetch(); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/scheduler/WeeklyTokenResetScheduler.java b/src/main/java/org/ezcode/codetest/infrastructure/scheduler/WeeklyTokenResetScheduler.java index 4de8ee91..a3031a77 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/scheduler/WeeklyTokenResetScheduler.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/scheduler/WeeklyTokenResetScheduler.java @@ -20,37 +20,37 @@ @Configuration public class WeeklyTokenResetScheduler { - private final TaskScheduler scheduler; - private final UserService userService; - - public WeeklyTokenResetScheduler( - @Qualifier("appTaskScheduler") TaskScheduler scheduler, - UserService userService - ) { - this.scheduler = scheduler; - this.userService = userService; - } - - @PostConstruct - public void schedule() { - CronTrigger trigger = new CronTrigger( - "0 0 0 * * MON", - TimeZone.getTimeZone("Asia/Seoul") - ); - - scheduler.schedule(() -> { - try { - log.info("주간 토큰 리셋을 시작합니다."); - LocalDate lastMonday = LocalDate.now(ZoneId.of("Asia/Seoul")) - .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); - LocalDateTime startDateTime = lastMonday.atStartOfDay(); - LocalDateTime endDateTime = lastMonday.plusDays(7).atStartOfDay(); - - userService.resetAllUsersTokensWeekly(startDateTime, endDateTime); - log.info("주간 토큰 리셋을 성공적으로 완료했습니다."); - } catch (Exception e) { - log.error("주간 토큰 리셋에 실패했습니다.", e); - } - }, trigger); - } + private final TaskScheduler scheduler; + private final UserService userService; + + public WeeklyTokenResetScheduler( + @Qualifier("appTaskScheduler") TaskScheduler scheduler, + UserService userService + ) { + this.scheduler = scheduler; + this.userService = userService; + } + + @PostConstruct + public void schedule() { + CronTrigger trigger = new CronTrigger( + "0 0 0 * * MON", + TimeZone.getTimeZone("Asia/Seoul") + ); + + scheduler.schedule(() -> { + try { + log.info("주간 토큰 리셋을 시작합니다."); + LocalDate lastMonday = LocalDate.now(ZoneId.of("Asia/Seoul")) + .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)); + LocalDateTime startDateTime = lastMonday.atStartOfDay(); + LocalDateTime endDateTime = lastMonday.plusDays(7).atStartOfDay(); + + userService.resetAllUsersTokensWeekly(startDateTime, endDateTime); + log.info("주간 토큰 리셋을 성공적으로 완료했습니다."); + } catch (Exception e) { + log.error("주간 토큰 리셋에 실패했습니다.", e); + } + }, trigger); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/sse/InMemoryEmitterStore.java b/src/main/java/org/ezcode/codetest/infrastructure/sse/InMemoryEmitterStore.java index fed3a4ea..6713413b 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/sse/InMemoryEmitterStore.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/sse/InMemoryEmitterStore.java @@ -16,40 +16,40 @@ @Component public class InMemoryEmitterStore implements EmitterStore { - private final Map emitterMap = new ConcurrentHashMap<>(); - - @Override - public void saveWithCallbacks(String key, SseEmitter emitter) { - emitterMap.put(key, emitter); - - emitter.onCompletion(() -> log.info("[SSE 완료] 정상 종료됨")); - - emitter.onTimeout(() -> { - log.warn("[SSE 타임아웃] 연결 시간이 초과되었습니다"); - emitter.completeWithError(new SubmissionException(SubmissionExceptionCode.EMITTER_SEND_ERROR)); - remove(key); - }); - - emitter.onError(e -> { - log.error("[SSE 에러 발생] 예외: {}", e.toString(), e); - remove(key); - }); - - } - - @Override - public Optional get(String key) { - return Optional.ofNullable(emitterMap.get(key)); - } - - @Override - public SseEmitter getOrElseThrow(String key) { - return Optional.ofNullable(emitterMap.get(key)) - .orElseThrow(() -> new SubmissionException(SubmissionExceptionCode.EMITTER_NOT_FOUND)); - } - - @Override - public void remove(String key) { - emitterMap.remove(key); - } + private final Map emitterMap = new ConcurrentHashMap<>(); + + @Override + public void saveWithCallbacks(String key, SseEmitter emitter) { + emitterMap.put(key, emitter); + + emitter.onCompletion(() -> log.info("[SSE 완료] 정상 종료됨")); + + emitter.onTimeout(() -> { + log.warn("[SSE 타임아웃] 연결 시간이 초과되었습니다"); + emitter.completeWithError(new SubmissionException(SubmissionExceptionCode.EMITTER_SEND_ERROR)); + remove(key); + }); + + emitter.onError(e -> { + log.error("[SSE 에러 발생] 예외: {}", e.toString(), e); + remove(key); + }); + + } + + @Override + public Optional get(String key) { + return Optional.ofNullable(emitterMap.get(key)); + } + + @Override + public SseEmitter getOrElseThrow(String key) { + return Optional.ofNullable(emitterMap.get(key)) + .orElseThrow(() -> new SubmissionException(SubmissionExceptionCode.EMITTER_NOT_FOUND)); + } + + @Override + public void remove(String key) { + emitterMap.remove(key); + } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/swagger/config/SwaggerConfig.java b/src/main/java/org/ezcode/codetest/infrastructure/swagger/config/SwaggerConfig.java index 239bd68c..4d32355b 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/swagger/config/SwaggerConfig.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/swagger/config/SwaggerConfig.java @@ -11,19 +11,19 @@ @Configuration @OpenAPIDefinition( - info = @Info(title = "API 문서", version = "v1"), - security = @SecurityRequirement(name = "JWT") + info = @Info(title = "API 문서", version = "v1"), + security = @SecurityRequirement(name = "JWT") ) @SecurityScheme( - name = "JWT", - type = SecuritySchemeType.HTTP, - scheme = "bearer", - bearerFormat = "JWT" + name = "JWT", + type = SecuritySchemeType.HTTP, + scheme = "bearer", + bearerFormat = "JWT" ) @SecurityScheme( - name = "JWT_REFRESH", // refreshToken용 - type = SecuritySchemeType.APIKEY, - in = SecuritySchemeIn.HEADER + name = "JWT_REFRESH", // refreshToken용 + type = SecuritySchemeType.APIKEY, + in = SecuritySchemeIn.HEADER ) public class SwaggerConfig { } diff --git a/src/main/java/org/ezcode/codetest/presentation/language/LanguageController.java b/src/main/java/org/ezcode/codetest/presentation/language/LanguageController.java index 4b78e0c3..764148d3 100644 --- a/src/main/java/org/ezcode/codetest/presentation/language/LanguageController.java +++ b/src/main/java/org/ezcode/codetest/presentation/language/LanguageController.java @@ -30,51 +30,51 @@ @Tag(name = "Language", description = "프로그래밍 언어 관리 API") public class LanguageController { - private final LanguageService languageService; + private final LanguageService languageService; - @PostMapping - @Operation(summary = "언어 생성", description = "새로운 프로그래밍 언어를 등록합니다.") - @ApiResponse(responseCode = "201", description = "언어 생성 성공") - public ResponseEntity createLanguage( - @RequestBody @Valid LanguageCreateRequest request - ) { - return ResponseEntity - .status(HttpStatus.CREATED) - .body(languageService.createLanguage(request)); - } + @PostMapping + @Operation(summary = "언어 생성", description = "새로운 프로그래밍 언어를 등록합니다.") + @ApiResponse(responseCode = "201", description = "언어 생성 성공") + public ResponseEntity createLanguage( + @RequestBody @Valid LanguageCreateRequest request + ) { + return ResponseEntity + .status(HttpStatus.CREATED) + .body(languageService.createLanguage(request)); + } - @GetMapping - @Operation(summary = "언어 목록 조회", description = "등록된 모든 프로그래밍 언어 목록을 조회합니다.") - @ApiResponse(responseCode = "200", description = "언어 목록 조회 성공") - public ResponseEntity> getLanguages() { - return ResponseEntity - .status(HttpStatus.OK) - .body(languageService.getLanguages()); - } + @GetMapping + @Operation(summary = "언어 목록 조회", description = "등록된 모든 프로그래밍 언어 목록을 조회합니다.") + @ApiResponse(responseCode = "200", description = "언어 목록 조회 성공") + public ResponseEntity> getLanguages() { + return ResponseEntity + .status(HttpStatus.OK) + .body(languageService.getLanguages()); + } - @PutMapping("/{languageId}") - @Operation(summary = "언어 수정", description = "기존 프로그래밍 언어 정보를 수정합니다.") - @ApiResponse(responseCode = "200", description = "언어 수정 성공") - public ResponseEntity modifyLanguage( - @Parameter(description = "수정할 언어 ID", required = true) - @PathVariable Long languageId, - @RequestBody @Valid LanguageUpdateRequest request - ) { - return ResponseEntity - .status(HttpStatus.OK) - .body(languageService.modifyLanguage(languageId, request)); - } + @PutMapping("/{languageId}") + @Operation(summary = "언어 수정", description = "기존 프로그래밍 언어 정보를 수정합니다.") + @ApiResponse(responseCode = "200", description = "언어 수정 성공") + public ResponseEntity modifyLanguage( + @Parameter(description = "수정할 언어 ID", required = true) + @PathVariable Long languageId, + @RequestBody @Valid LanguageUpdateRequest request + ) { + return ResponseEntity + .status(HttpStatus.OK) + .body(languageService.modifyLanguage(languageId, request)); + } - @DeleteMapping("/{languageId}") - @Operation(summary = "언어 삭제", description = "등록된 프로그래밍 언어를 삭제합니다.") - @ApiResponse(responseCode = "204", description = "언어 삭제 성공") - public ResponseEntity removeLanguage( - @Parameter(description = "삭제할 언어 ID", required = true) - @PathVariable Long languageId - ) { - languageService.removeLanguage(languageId); - return ResponseEntity - .status(HttpStatus.NO_CONTENT) - .build(); - } + @DeleteMapping("/{languageId}") + @Operation(summary = "언어 삭제", description = "등록된 프로그래밍 언어를 삭제합니다.") + @ApiResponse(responseCode = "204", description = "언어 삭제 성공") + public ResponseEntity removeLanguage( + @Parameter(description = "삭제할 언어 ID", required = true) + @PathVariable Long languageId + ) { + languageService.removeLanguage(languageId); + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .build(); + } } diff --git a/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java b/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java index 3f956709..cbcc3e3d 100644 --- a/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java +++ b/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java @@ -3,6 +3,7 @@ import java.util.List; import io.swagger.v3.oas.annotations.media.Content; + import org.ezcode.codetest.application.submission.dto.request.review.CodeReviewRequest; import org.ezcode.codetest.application.submission.dto.request.submission.CodeSubmitRequest; import org.ezcode.codetest.application.submission.dto.response.review.CodeReviewResponse; @@ -30,68 +31,68 @@ @Tag(name = "Submission", description = "코드 제출 및 리뷰 관련 API") public class SubmissionController { - private final SubmissionService submissionService; - - @PostMapping("/problems/{problemId}/submit-stream") - @Operation( - summary = "코드 제출 (SSE 응답)", - description = """ - 이 API는 Server-Sent Events(SSE)를 통해 테스트케이스별 채점 결과를 스트리밍으로 전송합니다. + private final SubmissionService submissionService; - 응답 MIME 타입: `text/event-stream` - - 응답 예시: - ``` - data: {"isPassed":true,"expectedOutput":"7","actualOutput":"7","executionTime":0.129,"memoryUsage":12196,"message":"Accepted"} - ``` - - ``` - event: final - data: {"totalCount":5,"passedCount":5,"message":"Accepted", correct":true} - ``` - """ - ) - @ApiResponse( - responseCode = "200", - description = "SSE로 스트리밍 응답 전송", - content = @Content(mediaType = "text/event-stream") - ) - public SseEmitter submitCodeStream( - @Parameter(description = "제출할 문제 ID", required = true) @PathVariable Long problemId, - @RequestBody @Valid CodeSubmitRequest request, - @AuthenticationPrincipal AuthUser authUser - ) { - return submissionService.enqueueCodeSubmission(problemId, request, authUser); - } + @PostMapping("/problems/{problemId}/submit-stream") + @Operation( + summary = "코드 제출 (SSE 응답)", + description = """ + 이 API는 Server-Sent Events(SSE)를 통해 테스트케이스별 채점 결과를 스트리밍으로 전송합니다. + + 응답 MIME 타입: `text/event-stream` + + 응답 예시: + ``` + data: {"isPassed":true,"expectedOutput":"7","actualOutput":"7","executionTime":0.129,"memoryUsage":12196,"message":"Accepted"} + ``` + + ``` + event: final + data: {"totalCount":5,"passedCount":5,"message":"Accepted", correct":true} + ``` + """ + ) + @ApiResponse( + responseCode = "200", + description = "SSE로 스트리밍 응답 전송", + content = @Content(mediaType = "text/event-stream") + ) + public SseEmitter submitCodeStream( + @Parameter(description = "제출할 문제 ID", required = true) @PathVariable Long problemId, + @RequestBody @Valid CodeSubmitRequest request, + @AuthenticationPrincipal AuthUser authUser + ) { + return submissionService.enqueueCodeSubmission(problemId, request, authUser); + } - @Operation( - summary = "사용자 제출 목록 조회", - description = "현재 로그인한 사용자의 코드 제출 기록을 조회합니다.", - responses = { - @ApiResponse(responseCode = "200", description = "제출 목록 반환") - } - ) - @GetMapping("/submissions") - public ResponseEntity> getSubmissions(@AuthenticationPrincipal AuthUser authUser) { - return ResponseEntity - .status(HttpStatus.OK) - .body(submissionService.getSubmissions(authUser)); - } + @Operation( + summary = "사용자 제출 목록 조회", + description = "현재 로그인한 사용자의 코드 제출 기록을 조회합니다.", + responses = { + @ApiResponse(responseCode = "200", description = "제출 목록 반환") + } + ) + @GetMapping("/submissions") + public ResponseEntity> getSubmissions(@AuthenticationPrincipal AuthUser authUser) { + return ResponseEntity + .status(HttpStatus.OK) + .body(submissionService.getSubmissions(authUser)); + } - @Operation( - summary = "코드 리뷰 요청", - description = "특정 문제에 대해 사용자의 제출 코드를 리뷰 요청합니다.", - responses = { - @ApiResponse(responseCode = "200", description = "리뷰 결과 반환") - } - ) - @PostMapping("/problems/{problemId}/review") - public ResponseEntity getCodeReview( - @Parameter(description = "문제 ID", required = true) @PathVariable Long problemId, - @RequestBody @Valid CodeReviewRequest request, - @AuthenticationPrincipal AuthUser authUser) { - return ResponseEntity - .status(HttpStatus.OK) - .body(submissionService.getCodeReview(problemId, request, authUser)); - } + @Operation( + summary = "코드 리뷰 요청", + description = "특정 문제에 대해 사용자의 제출 코드를 리뷰 요청합니다.", + responses = { + @ApiResponse(responseCode = "200", description = "리뷰 결과 반환") + } + ) + @PostMapping("/problems/{problemId}/review") + public ResponseEntity getCodeReview( + @Parameter(description = "문제 ID", required = true) @PathVariable Long problemId, + @RequestBody @Valid CodeReviewRequest request, + @AuthenticationPrincipal AuthUser authUser) { + return ResponseEntity + .status(HttpStatus.OK) + .body(submissionService.getCodeReview(problemId, request, authUser)); + } } diff --git a/src/main/java/org/ezcode/codetest/presentation/submission/SubmitViewController.java b/src/main/java/org/ezcode/codetest/presentation/submission/SubmitViewController.java index 105029ff..bcf5e7c6 100644 --- a/src/main/java/org/ezcode/codetest/presentation/submission/SubmitViewController.java +++ b/src/main/java/org/ezcode/codetest/presentation/submission/SubmitViewController.java @@ -6,8 +6,8 @@ @Controller public class SubmitViewController { - @GetMapping("/submit-test") - public String getView() { - return "submit-test"; - } + @GetMapping("/submit-test") + public String getView() { + return "submit-test"; + } } diff --git a/src/main/resources/templates/submit-test.html b/src/main/resources/templates/submit-test.html index a8c19521..9deaea46 100644 --- a/src/main/resources/templates/submit-test.html +++ b/src/main/resources/templates/submit-test.html @@ -9,12 +9,15 @@ background-color: #f8f9fa; padding: 30px; } + h2, h3 { color: #343a40; } + label { font-weight: bold; } + textarea, input[type="number"], input[type="text"] { margin-top: 5px; margin-bottom: 15px; @@ -26,6 +29,7 @@ border: 1px solid #ced4da; border-radius: 4px; } + button { padding: 10px 20px; margin-right: 10px; @@ -36,9 +40,11 @@ font-weight: bold; cursor: pointer; } + button:hover { background-color: #0056b3; } + .result-box { background-color: #ffffff; border: 1px solid #dee2e6; @@ -48,11 +54,12 @@ white-space: pre-line; font-family: monospace; } + .section { background-color: #ffffff; padding: 20px; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); margin-bottom: 30px; } @@ -62,19 +69,19 @@

소스코드 제출

- + - + - + @@ -95,7 +102,7 @@

코드 리뷰 결과