diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/InitTestcaseListPayload.java b/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/InitTestcaseListPayload.java index 26513876..eb42b9e5 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/InitTestcaseListPayload.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/InitTestcaseListPayload.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.stream.IntStream; -import org.ezcode.codetest.domain.problem.model.ProblemInfo; +import org.ezcode.codetest.application.submission.model.SubmissionContext; import org.ezcode.codetest.domain.problem.model.entity.Testcase; public record InitTestcaseListPayload( @@ -17,10 +17,10 @@ public record InitTestcaseListPayload( String status ) { - public static List from(ProblemInfo info) { - return IntStream.rangeClosed(1, info.getTestcaseCount()) + public static List from(SubmissionContext ctx) { + return IntStream.rangeClosed(1, ctx.getTestcaseCount()) .mapToObj(seq -> { - Testcase tc = info.testcaseList().get(seq - 1); + Testcase tc = ctx.getTestcases().get(seq - 1); return new InitTestcaseListPayload( seq, tc.getInput(), diff --git a/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/TestcaseResultPayload.java b/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/TestcaseResultPayload.java index e0579a60..dec1828a 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/TestcaseResultPayload.java +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/event/payload/TestcaseResultPayload.java @@ -1,7 +1,6 @@ package org.ezcode.codetest.application.submission.dto.event.payload; import org.ezcode.codetest.application.submission.model.JudgeResult; -import org.ezcode.codetest.domain.submission.dto.AnswerEvaluation; public record TestcaseResultPayload( @@ -18,10 +17,10 @@ public record TestcaseResultPayload( String message ) { - public static TestcaseResultPayload fromEvaluation(int seqId, JudgeResult result, AnswerEvaluation evaluation) { + public static TestcaseResultPayload fromEvaluation(int seqId, boolean isPassed, JudgeResult result) { return new TestcaseResultPayload( seqId, - evaluation.isPassed(), + isPassed, result.actualOutput(), result.executionTime(), result.memoryUsage(), 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 889c046a..ebfc0a0f 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 @@ -1,5 +1,10 @@ package org.ezcode.codetest.application.submission.dto.request.compile; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.ezcode.codetest.application.submission.model.SubmissionContext; + public record CodeCompileRequest( String source_code, @@ -9,4 +14,14 @@ public record CodeCompileRequest( String stdin ) { + public static CodeCompileRequest of(int seqId, SubmissionContext ctx) { + return new CodeCompileRequest( + ctx.getSourceCode(), + ctx.getJudge0Id(), + ctx.getInput(seqId)); + } + + private static String encodeBase64(String str) { + return Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8)); + } } 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 31022288..9621f24b 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 @@ -19,6 +19,18 @@ public record ExecutionResultResponse( ExecutionStatus status ) { + public static ExecutionResultResponse ofCompileError() { + return new ExecutionResultResponse( + null, + 0.0, + 0L, + null, + null, + "Compilation Error", + 1, + new ExecutionResultResponse.ExecutionStatus(6, "Compilation Error") + ); + } public long getMemory() { return this.memory == null ? 0L : memory; diff --git a/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java b/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java index 2100c193..2a468f13 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java +++ b/src/main/java/org/ezcode/codetest/application/submission/model/SubmissionContext.java @@ -1,8 +1,15 @@ package org.ezcode.codetest.application.submission.model; import org.ezcode.codetest.application.submission.dto.event.payload.SubmissionFinalResultPayload; +import org.ezcode.codetest.domain.language.model.entity.Language; +import org.ezcode.codetest.domain.problem.model.ProblemInfo; +import org.ezcode.codetest.domain.problem.model.entity.Problem; +import org.ezcode.codetest.domain.problem.model.entity.Testcase; import org.ezcode.codetest.domain.submission.model.SubmissionAggregator; +import org.ezcode.codetest.domain.user.model.entity.User; +import org.ezcode.codetest.infrastructure.event.dto.submission.SubmissionMessage; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -20,41 +27,55 @@ public record SubmissionContext( CountDownLatch latch, - AtomicBoolean notified + AtomicBoolean notified, + + User user, + + Language language, + + ProblemInfo problemInfo, + + SubmissionMessage msg ) { - public static SubmissionContext initialize(int totalTestcaseCount) { + public static SubmissionContext initialize( + User user, Language language, ProblemInfo problemInfo, SubmissionMessage msg + ) { return new SubmissionContext( new SubmissionAggregator(), new AtomicInteger(0), new AtomicInteger(0), new AtomicReference<>("Accepted"), - new CountDownLatch(totalTestcaseCount), - new AtomicBoolean(false) + new CountDownLatch(problemInfo.getTestcaseCount()), + new AtomicBoolean(false), + user, + language, + problemInfo, + msg ); } - public SubmissionFinalResultPayload toFinalResult(int totalTestcaseCount) { + public SubmissionFinalResultPayload toFinalResult() { return new SubmissionFinalResultPayload( - totalTestcaseCount, + this.getTestcaseCount(), this.getPassedCount(), this.getCurrentMessage() ); } public void incrementPassedCount() { - this.passedCount.incrementAndGet(); + passedCount.incrementAndGet(); } public void incrementProcessedCount() { - this.processedCount.incrementAndGet(); + processedCount.incrementAndGet(); } public int getPassedCount() { - return this.passedCount.get(); + return passedCount.get(); } public String getCurrentMessage() { - return this.message.get(); + return message.get(); } public void updateMessage(String message) { @@ -62,6 +83,46 @@ public void updateMessage(String message) { } public void countDown() { - this.latch.countDown(); + latch.countDown(); + } + + public List getTestcases() { + return problemInfo.testcaseList(); + } + + public int getTestcaseCount() { + return problemInfo.getTestcaseCount(); + } + + public String getSourceCode() { + return msg.sourceCode(); + } + + public long getJudge0Id() { + return language.getJudge0Id(); + } + + public String getInput(int seqId) { + return getTestcases().get(seqId - 1).getInput(); + } + + public String getExpectedOutput(int seqId) { + return getTestcases().get(seqId - 1).getOutput(); + } + + public long getTimeLimit() { + return problemInfo.getTimeLimit(); + } + + public long getMemoryLimit() { + return problemInfo.getMemoryLimit(); + } + + public String getSessionKey() { + return msg.sessionKey(); + } + + public Problem getProblem() { + return problemInfo.problem(); } } 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 c5397b42..ee0d8580 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 notifyException(String methodName, Throwable t); } diff --git a/src/main/java/org/ezcode/codetest/application/submission/service/JudgementService.java b/src/main/java/org/ezcode/codetest/application/submission/service/JudgementService.java new file mode 100644 index 00000000..fa89bb85 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/submission/service/JudgementService.java @@ -0,0 +1,109 @@ +package org.ezcode.codetest.application.submission.service; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent; +import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent; +import org.ezcode.codetest.application.submission.dto.event.TestcaseEvaluatedEvent; +import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent; +import org.ezcode.codetest.application.submission.dto.event.payload.InitTestcaseListPayload; +import org.ezcode.codetest.application.submission.dto.event.payload.TestcaseResultPayload; +import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest; +import org.ezcode.codetest.application.submission.model.JudgeResult; +import org.ezcode.codetest.application.submission.model.SubmissionContext; +import org.ezcode.codetest.application.submission.port.ExceptionNotifier; +import org.ezcode.codetest.application.submission.port.JudgeClient; +import org.ezcode.codetest.application.submission.port.ProblemEventService; +import org.ezcode.codetest.application.submission.port.SubmissionEventService; +import org.ezcode.codetest.domain.submission.exception.SubmissionException; +import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode; +import org.ezcode.codetest.domain.submission.model.SubmissionResult; +import org.ezcode.codetest.domain.submission.model.TestcaseEvaluationInput; +import org.ezcode.codetest.domain.submission.service.SubmissionDomainService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JudgementService { + + private final SubmissionDomainService submissionDomainService; + private final SubmissionEventService submissionEventService; + private final ProblemEventService problemEventService; + private final JudgeClient judgeClient; + private final Executor judgeTestcaseExecutor; + private final ExceptionNotifier exceptionNotifier; + + public void publishInitTestcases(SubmissionContext ctx) { + submissionEventService.publishInitTestcases( + new TestcaseListInitializedEvent(ctx.getSessionKey(), InitTestcaseListPayload.from(ctx)) + ); + } + + public void runTestcases(SubmissionContext ctx) throws InterruptedException { + IntStream.rangeClosed(1, ctx.getTestcaseCount()) + .forEach(seqId -> runTestcaseAsync(seqId, ctx)); + + if (!ctx.latch().await(100, TimeUnit.SECONDS)) { + throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT); + } + } + + @Transactional + public void finalizeAndPublish(SubmissionContext ctx) { + SubmissionResult submissionResult = submissionDomainService.finalizeSubmission(ctx); + + publishFinalResult(ctx); + publishProblemSolve(submissionResult); + } + + public void publishSubmissionError(String sessionKey, Exception e) { + submissionEventService.publishSubmissionError(new SubmissionErrorEvent(sessionKey, e)); + } + + private void runTestcaseAsync(int seqId, SubmissionContext ctx) { + CompletableFuture.runAsync(() -> { + try { + log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName()); + String token = judgeClient.submitAndGetToken(CodeCompileRequest.of(seqId, ctx)); + JudgeResult result = judgeClient.pollUntilDone(token); + + TestcaseEvaluationInput input = TestcaseEvaluationInput.from(result, ctx, seqId); + + boolean isPassed = submissionDomainService.handleEvaluationAndUpdateStats(input, ctx); + + publishTestcaseUpdate(seqId, ctx, isPassed, result); + } catch (Exception e) { + if (ctx.notified().compareAndSet(false, true)) { + publishSubmissionError(ctx.getSessionKey(), e); + exceptionNotifier.notifyException("runTestcaseAsync", e); + } + } finally { + ctx.countDown(); + } + }, judgeTestcaseExecutor); + } + + private void publishTestcaseUpdate(int seqId, SubmissionContext ctx, boolean isPassed, JudgeResult result) { + submissionEventService.publishTestcaseUpdate(new TestcaseEvaluatedEvent( + ctx.getSessionKey(), TestcaseResultPayload.fromEvaluation(seqId, isPassed, result)) + ); + } + + private void publishFinalResult(SubmissionContext ctx){ + submissionEventService.publishFinalResult( + new SubmissionJudgingFinishedEvent(ctx.getSessionKey(), ctx.toFinalResult()) + ); + } + + private void publishProblemSolve(SubmissionResult submissionResult) { + problemEventService.publishProblemSolveEvent(submissionResult); + } +} 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 9047ed43..28407db1 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 @@ -1,46 +1,26 @@ package org.ezcode.codetest.application.submission.service; import java.util.List; -import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; import org.ezcode.codetest.application.submission.aop.CodeReviewLock; -import org.ezcode.codetest.application.submission.dto.event.SubmissionErrorEvent; -import org.ezcode.codetest.application.submission.dto.event.SubmissionJudgingFinishedEvent; -import org.ezcode.codetest.application.submission.dto.event.TestcaseEvaluatedEvent; -import org.ezcode.codetest.application.submission.dto.event.payload.InitTestcaseListPayload; -import org.ezcode.codetest.application.submission.dto.event.payload.TestcaseResultPayload; import org.ezcode.codetest.application.submission.dto.request.review.CodeReviewRequest; import org.ezcode.codetest.application.submission.dto.request.review.ReviewPayload; import org.ezcode.codetest.application.submission.dto.response.review.CodeReviewResponse; import org.ezcode.codetest.application.submission.dto.response.submission.GroupedSubmissionResponse; -import org.ezcode.codetest.application.submission.dto.event.TestcaseListInitializedEvent; -import org.ezcode.codetest.application.submission.model.JudgeResult; import org.ezcode.codetest.application.submission.model.ReviewResult; import org.ezcode.codetest.application.submission.model.SubmissionContext; import org.ezcode.codetest.application.submission.port.ExceptionNotifier; import org.ezcode.codetest.application.submission.port.LockManager; -import org.ezcode.codetest.application.submission.port.ProblemEventService; import org.ezcode.codetest.application.submission.port.QueueProducer; -import org.ezcode.codetest.application.submission.port.SubmissionEventService; import org.ezcode.codetest.domain.submission.exception.SubmissionException; import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode; -import org.ezcode.codetest.domain.submission.model.SubmissionResult; -import org.ezcode.codetest.domain.submission.model.TestcaseEvaluationInput; import org.ezcode.codetest.infrastructure.event.dto.submission.SubmissionMessage; import org.ezcode.codetest.application.submission.port.ReviewClient; import org.ezcode.codetest.domain.problem.model.ProblemInfo; -import org.ezcode.codetest.domain.submission.dto.AnswerEvaluation; -import org.ezcode.codetest.domain.submission.dto.SubmissionData; -import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest; import org.ezcode.codetest.application.submission.dto.request.submission.CodeSubmitRequest; -import org.ezcode.codetest.application.submission.port.JudgeClient; import org.ezcode.codetest.domain.language.model.entity.Language; import org.ezcode.codetest.domain.problem.model.entity.Problem; -import org.ezcode.codetest.domain.problem.model.entity.Testcase; import org.ezcode.codetest.domain.submission.model.entity.Submission; import org.ezcode.codetest.domain.language.service.LanguageDomainService; import org.ezcode.codetest.domain.problem.service.ProblemDomainService; @@ -60,29 +40,24 @@ @RequiredArgsConstructor public class SubmissionService { - private final JudgeClient judgeClient; private final ReviewClient reviewClient; private final UserDomainService userDomainService; private final ProblemDomainService problemDomainService; private final LanguageDomainService languageDomainService; private final SubmissionDomainService submissionDomainService; private final QueueProducer queueProducer; - private final Executor judgeTestcaseExecutor; private final ExceptionNotifier exceptionNotifier; private final LockManager lockManager; - private final SubmissionEventService submissionEventService; - private final ProblemEventService problemEventService; + private final JudgementService judgementService; public String enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId); - if (!acquired) { throw new SubmissionException(SubmissionExceptionCode.ALREADY_JUDGING); } String sessionKey = authUser.getId() + "_" + UUID.randomUUID(); - queueProducer.enqueue( new SubmissionMessage(sessionKey, problemId, request.languageId(), authUser.getId(), request.sourceCode()) ); @@ -95,49 +70,27 @@ public void submitCodeStream(SubmissionMessage msg) { try { log.info("[Submission RUN] Thread = {}", Thread.currentThread().getName()); log.info("[큐 수신] SubmissionMessage.sessionKey: {}", msg.sessionKey()); - User user = userDomainService.getUserById(msg.userId()); - Language language = languageDomainService.getLanguage(msg.languageId()); - ProblemInfo problemInfo = problemDomainService.getProblemInfo(msg.problemId()); - - List testcaseList = problemInfo.testcaseList(); - int totalTestcaseCount = problemInfo.getTestcaseCount(); - - SubmissionContext context = SubmissionContext.initialize(totalTestcaseCount); - - submissionEventService.publishInitTestcases( - new TestcaseListInitializedEvent(msg.sessionKey(), InitTestcaseListPayload.from(problemInfo)) - ); - - for (int i = 0; i < totalTestcaseCount; i++) { - int seqId = i + 1; - runTestcaseAsync(seqId, testcaseList.get(i), msg, language.getJudge0Id(), problemInfo, context); - } - - if (!context.latch().await(100, TimeUnit.SECONDS)) { - throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT); - } - - submissionEventService.publishFinalResult( - new SubmissionJudgingFinishedEvent(msg.sessionKey(), context.toFinalResult(totalTestcaseCount)) - ); - - SubmissionData submissionData = SubmissionData.base( - user, problemInfo, language, msg.sourceCode(), context.getCurrentMessage() - ); - - SubmissionResult submissionResult = submissionDomainService.finalizeSubmission( - submissionData, context.aggregator(), context.getPassedCount() - ); - problemEventService.publishProblemSolveEvent(submissionResult); + SubmissionContext ctx = createSubmissionContext(msg); + judgementService.publishInitTestcases(ctx); + judgementService.runTestcases(ctx); + judgementService.finalizeAndPublish(ctx); } catch (Exception e) { - submissionEventService.publishSubmissionError(new SubmissionErrorEvent(msg.sessionKey(), e)); - exceptionNotificationHelper(e); + judgementService.publishSubmissionError(msg.sessionKey(), e); + exceptionNotifier.notifyException("submitCodeStream", e); } finally { lockManager.releaseLock("submission", msg.userId(), msg.problemId()); } } + private SubmissionContext createSubmissionContext(SubmissionMessage msg) { + User user = userDomainService.getUserById(msg.userId()); + Language language = languageDomainService.getLanguage(msg.languageId()); + ProblemInfo problemInfo = problemDomainService.getProblemInfo(msg.problemId()); + + return SubmissionContext.initialize(user, language, problemInfo, msg); + } + @Transactional(readOnly = true) public List getSubmissions(AuthUser authUser) { @@ -149,6 +102,7 @@ public List getSubmissions(AuthUser authUser) { @Transactional @CodeReviewLock(prefix = "review") public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest request, AuthUser authUser) { + User user = userDomainService.getUserById(authUser.getId()); userDomainService.decreaseReviewToken(user); @@ -159,61 +113,4 @@ public CodeReviewResponse getCodeReview(Long problemId, CodeReviewRequest reques return new CodeReviewResponse(reviewResult.reviewContent()); } - - private void runTestcaseAsync( - int seqId, Testcase tc, SubmissionMessage msg, - Long judge0Id, ProblemInfo problemInfo, SubmissionContext context - ) { - 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 - ); - - submissionEventService.publishTestcaseUpdate( - new TestcaseEvaluatedEvent(msg.sessionKey(), TestcaseResultPayload.fromEvaluation(seqId, result, evaluation)) - ); - } catch (Exception e) { - if (context.notified().compareAndSet(false, true)) { - submissionEventService.publishSubmissionError(new SubmissionErrorEvent(msg.sessionKey(), e)); - 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/domain/problem/model/ProblemInfo.java b/src/main/java/org/ezcode/codetest/domain/problem/model/ProblemInfo.java index 414a5c4a..0ff4ad6c 100644 --- a/src/main/java/org/ezcode/codetest/domain/problem/model/ProblemInfo.java +++ b/src/main/java/org/ezcode/codetest/domain/problem/model/ProblemInfo.java @@ -12,7 +12,7 @@ public record ProblemInfo( List testcaseList ) { - public double getTimeLimit() { + public long getTimeLimit() { return this.problem.getTimeLimit(); } 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 fee2d754..697f0d65 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 @@ -13,6 +13,6 @@ public record AnswerEvaluation( ) { public boolean isPassed() { - return this.isCorrect() && this.timeEfficient() && this.memoryEfficient(); + return isCorrect && timeEfficient && 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 4cb8ae5f..ba5a1bae 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 @@ -2,8 +2,7 @@ import java.util.List; -import org.ezcode.codetest.domain.problem.model.ProblemInfo; -import org.ezcode.codetest.domain.submission.model.SubmissionAggregator; +import org.ezcode.codetest.application.submission.model.SubmissionContext; import org.ezcode.codetest.domain.language.model.entity.Language; import org.ezcode.codetest.domain.problem.model.entity.Problem; import org.ezcode.codetest.domain.submission.model.entity.Submission; @@ -32,28 +31,14 @@ public record SubmissionData( long memoryUsage ) { - public static SubmissionData base( - User user, ProblemInfo problemInfo, Language language, String code, String message) { + public static SubmissionData from(SubmissionContext ctx) { 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()) + .user(ctx.user()) + .problem(ctx.getProblem()) + .language(ctx.language()) + .testCaseList(ctx.getTestcases()) + .code(ctx.getSourceCode()) + .message(ctx.getCurrentMessage()) .build(); } @@ -71,6 +56,20 @@ public static Submission toEntity(SubmissionData submissionData, int testCasePas .build(); } + public static Submission toEntity(SubmissionContext ctx) { + return Submission.builder() + .user(ctx.user()) + .problem(ctx.getProblem()) + .language(ctx.language()) + .code(ctx.getSourceCode()) + .message(ctx.getCurrentMessage()) + .testCasePassedCount(ctx.getPassedCount()) + .testCaseTotalCount(ctx.getTestcaseCount()) + .executionTime(ctx.aggregator().averageExecutionTime()) + .memoryUsage(ctx.aggregator().averageMemoryUsage()) + .build(); + } + public Long getUserId() { return this.user.getId(); } 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 71375578..9b3ef070 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 @@ -8,9 +8,9 @@ public class SubmissionAggregator { private int count; - public void accumulate(long executionTime, long memoryUsage) { - totalExecutionTime += executionTime; - totalMemoryUsage += memoryUsage; + public void accumulate(TestcaseEvaluationInput input) { + totalExecutionTime += input.executionTime(); + totalMemoryUsage += input.memoryUsage(); 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 f661f171..18f41414 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 @@ -1,7 +1,7 @@ package org.ezcode.codetest.domain.submission.model; import org.ezcode.codetest.application.submission.model.JudgeResult; -import org.ezcode.codetest.domain.problem.model.entity.Testcase; +import org.ezcode.codetest.application.submission.model.SubmissionContext; public record TestcaseEvaluationInput( @@ -15,17 +15,35 @@ public record TestcaseEvaluationInput( long executionTime, - long memoryUsage + long memoryUsage, + + long timeLimit, + + long memoryLimit ) { - public static TestcaseEvaluationInput from(Testcase testcase, JudgeResult result) { + public static TestcaseEvaluationInput from(JudgeResult result, SubmissionContext ctx, int seqId) { return new TestcaseEvaluationInput( - testcase.getOutput(), + ctx.getExpectedOutput(seqId), result.actualOutput(), result.message(), result.success(), result.executionTime(), - result.memoryUsage() + result.memoryUsage(), + ctx.getTimeLimit(), + ctx.getMemoryLimit() ); } + + public boolean isCorrect() { + return success && expectedOutput.strip().equals(actualOutput.strip()); + } + + public boolean timeEfficient() { + return executionTime <= timeLimit; + } + + public boolean memoryEfficient() { + return memoryUsage <= memoryLimit; + } } 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 8eb06c5f..55433d77 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 @@ -5,7 +5,6 @@ import java.util.Optional; import org.ezcode.codetest.domain.submission.model.entity.UserProblemResult; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -14,17 +13,8 @@ public interface UserProblemResultRepository { UserProblemResult saveUserProblemResult(UserProblemResult userProblemResult); - UserProblemResult 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 - """) + void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect); + List findScoresBetween(LocalDateTime start, LocalDateTime end); Optional sumPointByUserId(@Param("userId") Long userId); 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 55b0f8d4..70e35a59 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 @@ -6,7 +6,6 @@ import org.ezcode.codetest.application.submission.model.SubmissionContext; import org.ezcode.codetest.domain.submission.dto.WeeklySolveCount; -import org.ezcode.codetest.domain.problem.model.ProblemInfo; import org.ezcode.codetest.domain.submission.model.SubmissionResult; import org.ezcode.codetest.domain.submission.model.TestcaseEvaluationInput; import org.ezcode.codetest.domain.submission.model.SubmissionAggregator; @@ -17,7 +16,6 @@ import org.ezcode.codetest.domain.submission.repository.SubmissionRepository; import org.ezcode.codetest.domain.submission.repository.UserProblemResultRepository; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @@ -28,21 +26,13 @@ public class SubmissionDomainService { private final SubmissionRepository submissionRepository; private final UserProblemResultRepository userProblemResultRepository; - @Transactional - public SubmissionResult finalizeSubmission( - SubmissionData submissionData, - SubmissionAggregator aggregator, - int passedCount) { + public SubmissionResult finalizeSubmission(SubmissionContext ctx) { - createSubmission(SubmissionData.toEntity( - submissionData.withAggregatedStats(aggregator), - passedCount - ) - ); + createSubmission(SubmissionData.toEntity(ctx)); - boolean allPassed = (passedCount == submissionData.getTestCaseSize()); + boolean allPassed = (ctx.getPassedCount() == ctx.getTestcaseCount()); - return getUserProblemResult(submissionData.getUserId(), submissionData.getProblemId()).map( + return getUserProblemResult(ctx.user().getId(), ctx.getProblem().getId()).map( result -> { if (!result.isCorrect() && allPassed) { modifyUserProblemResult(result, true); @@ -51,32 +41,30 @@ public SubmissionResult finalizeSubmission( return SubmissionResult.from(result, true); }) .orElseGet(() -> SubmissionResult.from(createUserProblemResult( - UserProblemResult.builder() - .user(submissionData.user()) - .problem(submissionData.problem()) - .isCorrect(allPassed) - .build() + UserProblemResult.builder() + .user(ctx.user()) + .problem(ctx.getProblem()) + .isCorrect(allPassed) + .build() ),false) ); } - public AnswerEvaluation handleEvaluationAndUpdateStats( - TestcaseEvaluationInput input, ProblemInfo problemInfo, SubmissionContext context + public boolean handleEvaluationAndUpdateStats( + TestcaseEvaluationInput input, SubmissionContext ctx ) { - AnswerEvaluation evaluation = - evaluate(input.expectedOutput(), input.actualOutput(), input.success(), - input.executionTime(), input.memoryUsage(), problemInfo); + boolean isPassed = evaluate(input); - if (evaluation.isPassed()) { - context.incrementPassedCount(); + if (isPassed) { + ctx.incrementPassedCount(); } else { - context.updateMessage(input.resultMessage()); + ctx.updateMessage(input.resultMessage()); } - context.incrementProcessedCount(); + ctx.incrementProcessedCount(); - collectStatistics(context.aggregator(), input.executionTime(), input.memoryUsage()); + collectStatistics(ctx.aggregator(), input); - return evaluation; + return isPassed; } public List getSubmissions(Long userId) { @@ -89,18 +77,16 @@ public List getWeeklySolveCounts( return submissionRepository.fetchWeeklySolveCounts(startDateTime, endDateTime); } - private AnswerEvaluation evaluate( - String expectedOutput, String actualOutput, boolean success, long 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); + private boolean evaluate(TestcaseEvaluationInput input) { + boolean isCorrect = input.isCorrect(); + boolean timeEfficient = input.timeEfficient(); + boolean memoryEfficient = input.memoryEfficient(); + AnswerEvaluation answerEvaluation = new AnswerEvaluation(isCorrect, timeEfficient, memoryEfficient); + return answerEvaluation.isPassed(); } - private void collectStatistics(SubmissionAggregator aggregator, long executionTime, long memoryUsage) { - aggregator.accumulate(executionTime, memoryUsage); + private void collectStatistics(SubmissionAggregator aggregator, TestcaseEvaluationInput input) { + aggregator.accumulate(input); } private void createSubmission(Submission submission) { @@ -115,7 +101,7 @@ private UserProblemResult createUserProblemResult(UserProblemResult userProblemR return userProblemResultRepository.saveUserProblemResult(userProblemResult); } - private UserProblemResult modifyUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { - return userProblemResultRepository.updateUserProblemResult(userProblemResult, isCorrect); + private void modifyUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { + userProblemResultRepository.updateUserProblemResult(userProblemResult, isCorrect); } } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/GameLevelUpListener.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/GameLevelUpListener.java index 69af5789..4c6bca45 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/GameLevelUpListener.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/GameLevelUpListener.java @@ -3,10 +3,11 @@ import org.ezcode.codetest.domain.game.exception.GameException; import org.ezcode.codetest.domain.game.service.CharacterStatusDomainService; import org.ezcode.codetest.infrastructure.event.dto.GameLevelUpEvent; -import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.interceptor.TransactionAspectSupport; import lombok.RequiredArgsConstructor; @@ -19,7 +20,7 @@ public class GameLevelUpListener { private final CharacterStatusDomainService characterDomainService; - @EventListener + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) @Transactional(propagation = Propagation.REQUIRES_NEW) public void handleGameCharacterLevelUp(GameLevelUpEvent event) { diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java index 649042d2..5290ccff 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/listener/SubmissionEventListener.java @@ -13,6 +13,8 @@ import org.ezcode.codetest.infrastructure.event.publisher.StompMessageService; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; import lombok.RequiredArgsConstructor; @@ -34,7 +36,7 @@ public void onTestcaseUpdate(TestcaseEvaluatedEvent event) { messageService.sendTestcaseResultUpdate(event.sessionKey(), wsDto); } - @EventListener + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void onSubmissionFinished(SubmissionJudgingFinishedEvent event) { SubmissionFinalResultResponse wsDto = SubmissionFinalResultResponse.from(event.payload()); messageService.sendFinalResult(event.sessionKey(), wsDto); diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/DiscordNotifier.java b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/DiscordNotifier.java index 08a0786f..e1274505 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/DiscordNotifier.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/DiscordNotifier.java @@ -3,8 +3,11 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.Optional; import org.ezcode.codetest.application.submission.port.ExceptionNotifier; +import org.ezcode.codetest.domain.submission.exception.CodeReviewException; +import org.ezcode.codetest.domain.submission.exception.SubmissionException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -26,7 +29,43 @@ public class DiscordNotifier implements ExceptionNotifier { private final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void sendEmbed(String title, String description, String exception, String methodName) { + public void notifyException(String methodName, Throwable t) { + String title = "채점 시스템 예외"; + String description; + String exceptionDetail; + + if (t instanceof SubmissionException se) { + var code = se.getResponseCode(); + description = "채점 중 SubmissionException 발생"; + exceptionDetail = + """ + • 성공 여부: %s + • 상태코드: %s + • 메시지: %s + """.formatted(code.isSuccess(), code.getStatus(), code.getMessage()); + } else if (t instanceof CodeReviewException ce) { + var code = ce.getResponseCode(); + description = "코드 리뷰 중 CodeReviewException 발생"; + exceptionDetail = + """ + • 성공 여부: %s + • 상태코드: %s + • 메시지: %s + """.formatted(code.isSuccess(), code.getStatus(), code.getMessage()); + } else { + description = "채점 중 알 수 없는 예외 발생"; + exceptionDetail = + """ + • 성공 여부: false + • 상태코드: 500 + • 메시지: %s + """.formatted(Optional.ofNullable(t.getMessage()).orElse("No message")); + } + + sendEmbed(title, description, exceptionDetail, methodName); + } + + private void sendEmbed(String title, String description, String exception, String methodName) { try { Map embed = Map.of( "title", title, diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/RedisJudgeQueueProducer.java b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/RedisJudgeQueueProducer.java index 1d4e557a..a4a12c7b 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/RedisJudgeQueueProducer.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/publisher/RedisJudgeQueueProducer.java @@ -18,7 +18,7 @@ public class RedisJudgeQueueProducer implements QueueProducer { private final RedisTemplate redisTemplate; public void enqueue(SubmissionMessage submissionMessage) { - log.info("[SSE enqueue] emitterKey: {}", submissionMessage.sessionKey()); + log.info("[WS enqueue] sessionKey: {}", submissionMessage.sessionKey()); Map map = Map.of( "sessionKey", submissionMessage.sessionKey(), "problemId", submissionMessage.problemId().toString(), 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 6410ba0d..9a603142 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0Client.java @@ -1,5 +1,7 @@ package org.ezcode.codetest.infrastructure.judge0; +import java.time.Duration; + import org.ezcode.codetest.application.submission.dto.request.compile.CodeCompileRequest; import org.ezcode.codetest.application.submission.dto.response.compile.ExecutionResultResponse; import org.ezcode.codetest.application.submission.model.JudgeResult; @@ -7,13 +9,19 @@ import org.ezcode.codetest.domain.submission.exception.SubmissionException; import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import io.netty.handler.timeout.TimeoutException; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; @Slf4j @Component @@ -27,65 +35,63 @@ public class Judge0Client implements JudgeClient { @PostConstruct private void init() { - this.webClient = WebClient.create(judge0ApiUrl); + this.webClient = WebClient.builder() + .baseUrl(judge0ApiUrl) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .build(); } @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); - } + ExecutionResultResponse resp = webClient.post() + .uri("/submissions?base64_encoded=false&wait=false") + .bodyValue(request) + .retrieve() + .bodyToMono(ExecutionResultResponse.class) + .timeout(Duration.ofSeconds(5)) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(4)) + .filter(ex -> ex instanceof WebClientResponseException + || ex instanceof TimeoutException) + ) + .onErrorMap(WebClientResponseException.class, + ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR)) + .onErrorMap(TimeoutException.class, + ex -> new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT)) + .block(); - return executionResultResponse.token(); - } catch (Exception e) { + if (resp == null || resp.token() == null) { throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); } + + return resp.token(); } @Override public JudgeResult pollUntilDone(String token) { - try { - int maxAttempts = 30; - int attempt = 0; - - while (attempt < maxAttempts) { - ExecutionResultResponse executionResultResponse = webClient.get() + ExecutionResultResponse finalResp = Flux + .interval(Duration.ZERO, Duration.ofSeconds(1)) + .flatMap(tick -> + webClient.get() .uri("/submissions/{token}", token) .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(ExecutionResultResponse.class) - .block(); + .onErrorResume(WebClientResponseException.BadRequest.class, + ex -> Mono.just(ExecutionResultResponse.ofCompileError()) + ) + ) + .filter(resp -> resp.status().id() >= 3) + .next() + .timeout(Duration.ofSeconds(60), + Mono.error(new SubmissionException(SubmissionExceptionCode.COMPILE_TIMEOUT))) + .block(); - if (executionResultResponse == null) { - throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); - } - - 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) { + if (finalResp == null) { throw new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR); } + + return interpreter.toJudgeResult(finalResp); } } 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 7e067c7b..0da0a632 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/judge0/Judge0ResponseMapper.java @@ -1,5 +1,8 @@ package org.ezcode.codetest.infrastructure.judge0; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + import org.ezcode.codetest.application.submission.dto.response.compile.ExecutionResultResponse; import org.ezcode.codetest.application.submission.model.JudgeResult; import org.springframework.stereotype.Component; @@ -48,4 +51,9 @@ private String normalizeOutput(String output) { .replaceAll("[\\r\\n]+$", "") .strip(); } + + private String decodeBase64(String base64) { + if (base64 == null) return ""; + return new String(Base64.getDecoder().decode(base64), StandardCharsets.US_ASCII); + } } 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 984c4bcc..0745d20d 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/openai/OpenAIReviewClient.java @@ -8,6 +8,7 @@ import org.ezcode.codetest.application.submission.dto.request.review.ReviewPayload; import org.ezcode.codetest.application.submission.model.OpenAIResponse; import org.ezcode.codetest.application.submission.model.ReviewResult; +import org.ezcode.codetest.application.submission.port.ExceptionNotifier; import org.ezcode.codetest.application.submission.port.ReviewClient; import org.ezcode.codetest.domain.submission.exception.CodeReviewException; import org.ezcode.codetest.domain.submission.exception.code.CodeReviewExceptionCode; @@ -37,6 +38,7 @@ public class OpenAIReviewClient implements ReviewClient { private WebClient webClient; private final OpenAIMessageBuilder openAiMessageBuilder; private final OpenAIResponseValidator openAiResponseValidator; + private final ExceptionNotifier exceptionNotifier; @PostConstruct private void init() { @@ -59,6 +61,7 @@ public ReviewResult requestReview(ReviewPayload reviewPayload) { content = callChatApi(requestBody); } catch (CodeReviewException e) { log.error("OpenAI API 호출 실패: {}, {}", e.getHttpStatus(), e.getMessage()); + exceptionNotifier.notifyException("requestReview", e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); } @@ -68,6 +71,7 @@ public ReviewResult requestReview(ReviewPayload reviewPayload) { } log.warn("[{}/{}][isCorrect={}] 포맷 검증 실패:\n{}", attempt, maxAttempts, reviewPayload.isCorrect(), content); } + exceptionNotifier.notifyException("requestReview", new CodeReviewException(CodeReviewExceptionCode.REVIEW_INVALID_FORMAT)); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return new ReviewResult(openAiMessageBuilder.buildErrorMessage()); } 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 4afd820a..bcfe468a 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 @@ -28,9 +28,8 @@ public UserProblemResult saveUserProblemResult(UserProblemResult userProblemResu } @Override - public UserProblemResult updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { + public void updateUserProblemResult(UserProblemResult userProblemResult, boolean isCorrect) { userProblemResult.updateResult(isCorrect); - return userProblemResult; } @Override diff --git a/src/main/resources/templates/submit-test.html b/src/main/resources/templates/submit-test.html index f22e71b4..0a3b0804 100644 --- a/src/main/resources/templates/submit-test.html +++ b/src/main/resources/templates/submit-test.html @@ -107,11 +107,15 @@

채점 결과

}); } + let receivedCases = new Set(); + function handleCase(data) { const d = document.getElementById(`slot-${data.seqId}`); if (!d) return; d.className = data.isPassed ? 'slot passed' : 'slot failed'; d.textContent = `[${data.seqId}] ${data.message} (${data.executionTime}s, ${data.memoryUsage}KB)`; + + receivedCases.add(data.seqId); } function handleFinal(res) { @@ -119,7 +123,6 @@

채점 결과

sum.style.marginTop = '12px'; sum.innerHTML = `최종: ${res.passedCount}/${res.totalCount} 통과 — ${res.message}`; judgeEl.appendChild(sum); - stompClient.disconnect(); }