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 dec1828a..19615eab 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 @@ -4,7 +4,7 @@ public record TestcaseResultPayload( - int seqId, + Long testcaseId, boolean isPassed, @@ -17,9 +17,9 @@ public record TestcaseResultPayload( String message ) { - public static TestcaseResultPayload fromEvaluation(int seqId, boolean isPassed, JudgeResult result) { + public static TestcaseResultPayload fromEvaluation(Long testcaseId, boolean isPassed, JudgeResult result) { return new TestcaseResultPayload( - seqId, + testcaseId, isPassed, result.actualOutput(), result.executionTime(), 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 ebfc0a0f..04047ca5 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,8 +1,5 @@ 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( @@ -14,14 +11,11 @@ public record CodeCompileRequest( String stdin ) { - public static CodeCompileRequest of(int seqId, SubmissionContext ctx) { + public static CodeCompileRequest of(int index, SubmissionContext ctx) { return new CodeCompileRequest( ctx.getSourceCode(), ctx.getJudge0Id(), - ctx.getInput(seqId)); + ctx.getInput(index)); } - 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/submission/SubmitResponse.java b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmitResponse.java new file mode 100644 index 00000000..596f7a18 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/submission/dto/response/submission/SubmitResponse.java @@ -0,0 +1,17 @@ +package org.ezcode.codetest.application.submission.dto.response.submission; + +import java.util.List; + +import org.ezcode.codetest.domain.problem.model.entity.Testcase; + +public record SubmitResponse( + + String sessionKey, + + List testcaseIds + +) { + public static SubmitResponse of(String sessionKey, List testcases) { + return new SubmitResponse(sessionKey, testcases.stream().map(Testcase::getId).toList()); + } +} 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 f88ccf83..759aa748 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 @@ -102,12 +102,16 @@ public long getJudge0Id() { return language.getJudge0Id(); } - public String getInput(int seqId) { - return getTestcases().get(seqId - 1).getInput(); + public String getInput(int index) { + return getTestcases().get(index).getInput(); } - public String getExpectedOutput(int seqId) { - return getTestcases().get(seqId - 1).getOutput(); + public String getExpectedOutput(int index) { + return getTestcases().get(index).getOutput(); + } + + public Long getTestcaseId(int index) { + return getTestcases().get(index).getId(); } public long getTimeLimit() { 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 index befb6877..c5ec821e 100644 --- a/src/main/java/org/ezcode/codetest/application/submission/service/JudgementService.java +++ b/src/main/java/org/ezcode/codetest/application/submission/service/JudgementService.java @@ -47,8 +47,9 @@ public void publishInitTestcases(SubmissionContext ctx) { } public void runTestcases(SubmissionContext ctx) throws InterruptedException { - IntStream.rangeClosed(1, ctx.getTestcaseCount()) - .forEach(seqId -> runTestcaseAsync(seqId, ctx)); + + IntStream.range(0, ctx.getTestcaseCount()) + .forEach(i -> runTestcaseAsync(i, ctx)); if (!ctx.latch().await(100, TimeUnit.SECONDS)) { throw new SubmissionException(SubmissionExceptionCode.TESTCASE_TIMEOUT); @@ -66,18 +67,18 @@ public void publishSubmissionError(String sessionKey, Exception e) { submissionEventService.publishSubmissionError(new SubmissionErrorEvent(sessionKey, e)); } - private void runTestcaseAsync(int seqId, SubmissionContext ctx) { + private void runTestcaseAsync(int index, SubmissionContext ctx) { CompletableFuture.runAsync(() -> { try { log.info("[Judge RUN] Thread = {}", Thread.currentThread().getName()); - String token = judgeClient.submitAndGetToken(CodeCompileRequest.of(seqId, ctx)); + String token = judgeClient.submitAndGetToken(CodeCompileRequest.of(index, ctx)); JudgeResult result = judgeClient.pollUntilDone(token); - TestcaseEvaluationInput input = TestcaseEvaluationInput.from(result, ctx, seqId); + TestcaseEvaluationInput input = TestcaseEvaluationInput.from(result, ctx, index); boolean isPassed = submissionDomainService.handleEvaluationAndUpdateStats(input, ctx); - publishTestcaseUpdate(seqId, ctx, isPassed, result); + publishTestcaseUpdate(index, ctx, isPassed, result); } catch (Exception e) { if (ctx.notified().compareAndSet(false, true)) { publishSubmissionError(ctx.getSessionKey(), e); @@ -89,9 +90,9 @@ private void runTestcaseAsync(int seqId, SubmissionContext ctx) { }, judgeTestcaseExecutor); } - private void publishTestcaseUpdate(int seqId, SubmissionContext ctx, boolean isPassed, JudgeResult result) { + private void publishTestcaseUpdate(int index, SubmissionContext ctx, boolean isPassed, JudgeResult result) { submissionEventService.publishTestcaseUpdate(TestcaseEvaluatedEvent.of( - ctx, TestcaseResultPayload.fromEvaluation(seqId, isPassed, result)) + ctx, TestcaseResultPayload.fromEvaluation(ctx.getTestcaseId(index), isPassed, result)) ); } 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 b59d3972..0286e47a 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 @@ -8,11 +8,14 @@ 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.response.submission.SubmitResponse; 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.QueueProducer; +import org.ezcode.codetest.domain.problem.model.entity.Testcase; +import org.ezcode.codetest.domain.problem.service.TestcaseDomainService; import org.ezcode.codetest.domain.submission.exception.SubmissionException; import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode; import org.ezcode.codetest.infrastructure.event.dto.submission.SubmissionMessage; @@ -43,6 +46,7 @@ public class SubmissionService { private final ReviewClient reviewClient; private final UserDomainService userDomainService; private final ProblemDomainService problemDomainService; + private final TestcaseDomainService testcaseDomainService; private final LanguageDomainService languageDomainService; private final SubmissionDomainService submissionDomainService; private final QueueProducer queueProducer; @@ -51,7 +55,7 @@ public class SubmissionService { private final JudgementService judgementService; private final GitHubPushService gitHubPushService; - public String enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { + public SubmitResponse enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) { boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId); if (!acquired) { @@ -59,11 +63,13 @@ public String enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, A } String sessionKey = authUser.getId() + "_" + UUID.randomUUID(); + List testcaseList = testcaseDomainService.getTestcaseListByProblemId(problemId); + queueProducer.enqueue( new SubmissionMessage(sessionKey, problemId, request.languageId(), authUser.getId(), request.sourceCode()) ); - return sessionKey; + return SubmitResponse.of(sessionKey, testcaseList); } @Async("judgeSubmissionExecutor") @@ -73,7 +79,6 @@ public void processSubmissionAsync(SubmissionMessage msg) { log.info("[Submission RUN] Thread = {}", Thread.currentThread().getName()); log.info("[큐 수신] SubmissionMessage.sessionKey: {}", msg.sessionKey()); SubmissionContext ctx = createSubmissionContext(msg); - judgementService.publishInitTestcases(ctx); judgementService.runTestcases(ctx); judgementService.finalizeAndPublish(ctx); gitHubPushService.pushSolutionToRepo(ctx); diff --git a/src/main/java/org/ezcode/codetest/domain/problem/repository/TestcaseRepository.java b/src/main/java/org/ezcode/codetest/domain/problem/repository/TestcaseRepository.java index 66d75ca8..1a9fc514 100644 --- a/src/main/java/org/ezcode/codetest/domain/problem/repository/TestcaseRepository.java +++ b/src/main/java/org/ezcode/codetest/domain/problem/repository/TestcaseRepository.java @@ -11,6 +11,8 @@ public interface TestcaseRepository { List findAllByProblem(Problem problem); + List findAllByProblemId(Long problemId); + Testcase findByTestcase(Long testcaseId); void delete(Testcase testcase); diff --git a/src/main/java/org/ezcode/codetest/domain/problem/service/TestcaseDomainService.java b/src/main/java/org/ezcode/codetest/domain/problem/service/TestcaseDomainService.java index 4c39ea81..6589356f 100644 --- a/src/main/java/org/ezcode/codetest/domain/problem/service/TestcaseDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/problem/service/TestcaseDomainService.java @@ -34,6 +34,10 @@ public List getTestcaseList(Problem problem) { return testcaseRepository.findAllByProblem(problem); } + public List getTestcaseListByProblemId(Long problemId) { + return testcaseRepository.findAllByProblemId(problemId); + } + public Testcase getTestcase(Long testcaseId) { return testcaseRepository.findByTestcase(testcaseId); } 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 18f41414..09bbed85 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 @@ -22,9 +22,9 @@ public record TestcaseEvaluationInput( long memoryLimit ) { - public static TestcaseEvaluationInput from(JudgeResult result, SubmissionContext ctx, int seqId) { + public static TestcaseEvaluationInput from(JudgeResult result, SubmissionContext ctx, int index) { return new TestcaseEvaluationInput( - ctx.getExpectedOutput(seqId), + ctx.getExpectedOutput(index), result.actualOutput(), result.message(), result.success(), diff --git a/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/JudgeResultResponse.java b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/JudgeResultResponse.java index dd08d207..2db85029 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/JudgeResultResponse.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/response/JudgeResultResponse.java @@ -9,8 +9,8 @@ @Schema(description = "각 테스트케이스에 대한 채점 결과") public record JudgeResultResponse( - @Schema(description = "테스트케이스 번호", example = "1") - int seqId, + @Schema(description = "테스트케이스 아이디", example = "1") + Long testcaseId, @Schema(description = "테스트케이스 통과 여부", example = "true") boolean isPassed, @@ -30,7 +30,7 @@ public record JudgeResultResponse( ) { public static JudgeResultResponse from(TestcaseResultPayload payload) { return new JudgeResultResponse( - payload.seqId(), + payload.testcaseId(), payload.isPassed(), payload.actualOutput(), payload.executionTime(), diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseJpaRepository.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseJpaRepository.java index 5f5f74f0..40a03e4d 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseJpaRepository.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseJpaRepository.java @@ -12,4 +12,6 @@ public interface TestcaseJpaRepository extends JpaRepository { @EntityGraph(attributePaths = "problem") List findAllByProblem(Problem problem); + @EntityGraph(attributePaths = "problem") + List findAllByProblem_Id(Long problemId); } diff --git a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseRepositoryImpl.java b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseRepositoryImpl.java index a0d5ec00..7b5fe427 100644 --- a/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseRepositoryImpl.java +++ b/src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/problem/TestcaseRepositoryImpl.java @@ -27,6 +27,11 @@ public List findAllByProblem(Problem problem) { return testcaseJpaRepository.findAllByProblem(problem); } + @Override + public List findAllByProblemId(Long problemId) { + return testcaseJpaRepository.findAllByProblem_Id(problemId); + } + @Override public Testcase findByTestcase(Long testcaseId) { 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 e865baf6..f14531d0 100644 --- a/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java +++ b/src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java @@ -1,27 +1,21 @@ package org.ezcode.codetest.presentation.submission; -import java.util.HashSet; import java.util.List; -import java.util.Map; - -import io.swagger.v3.oas.annotations.media.ArraySchema; -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; import org.ezcode.codetest.application.submission.dto.response.submission.GroupedSubmissionResponse; +import org.ezcode.codetest.application.submission.dto.response.submission.SubmitResponse; import org.ezcode.codetest.application.submission.service.SubmissionService; import org.ezcode.codetest.domain.user.model.entity.AuthUser; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -46,9 +40,7 @@ public class SubmissionController { 서버는 WebSocket(STOMP)을 통해 채점 결과를 실시간으로 전송합니다. 반환된 sessionKey를 사용해 다음 경로로 구독하세요. - - • /user/queue/submission/{sessionKey}/init - + • /user/queue/submission/{sessionKey}/case • /user/queue/submission/{sessionKey}/final @@ -63,16 +55,14 @@ public class SubmissionController { @ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터"), @ApiResponse(responseCode = "409", description = "이미 해당 문제를 채점 중인 경우"), }) - public ResponseEntity> submitCodeStream( + public ResponseEntity submitCodeStream( @Parameter(description = "제출할 문제 ID", required = true) @PathVariable Long problemId, @RequestBody @Valid CodeSubmitRequest request, @AuthenticationPrincipal AuthUser authUser ) { - HashSet set = new HashSet<>(); - set.add(submissionService.enqueueCodeSubmission(problemId, request, authUser)); return ResponseEntity .status(HttpStatus.OK) - .body(set); + .body(submissionService.enqueueCodeSubmission(problemId, request, authUser)); } @Operation( diff --git a/src/main/resources/templates/test-submit.html b/src/main/resources/templates/test-submit.html index 0531f86d..7ee85322 100644 --- a/src/main/resources/templates/test-submit.html +++ b/src/main/resources/templates/test-submit.html @@ -326,7 +326,7 @@

채점 결과

let stompClient; let connected = false; let finalIsCorrect = null; - + const testcaseSlotMap = {}; // { pk값: index } const gitDot = document.getElementById('gitDot'); const reviewBtn = document.getElementById('reviewBtn'); const reviewBox = document.getElementById('reviewBox'); @@ -392,18 +392,36 @@

채점 결과

body: JSON.stringify(payload) }); const resp = await res.json(); + console.log('[DEBUG] 전체 응답:', resp); + console.log('[DEBUG] result:', resp.result); if (!resp.success) { judgeEl.textContent = `Error: ${resp.message}`; return; } - const sessionKey = resp.result; + const { sessionKey, testcaseIds } = resp.result; initWebSocket(sessionKey); + renderInitSlots(testcaseIds); } catch (err) { console.error(err); judgeEl.textContent = '채점 요청 실패'; } } + function renderInitSlots(testcaseIds) { + console.log('[DEBUG] init 호출됨:', testcaseIds); // 추가 + const judgeEl = document.getElementById('judgeResult'); + testcaseIds.forEach((pk, index) => { + const visibleIdx = index + 1; + testcaseSlotMap[pk] = visibleIdx; + + const d = document.createElement('div'); + d.id = `slot-${visibleIdx}`; + d.className = 'slot init'; + d.textContent = `[${visibleIdx}] 상태: 채점 중...`; + judgeEl.appendChild(d); + }); + } + const languageTemplates = { 1: `public class Main { public static void main(String[] args) { @@ -471,6 +489,8 @@

채점 결과

stompClient.connect({}, () => { connected = true; const base = `/user/queue/submission/${sessionKey}`; + stompClient.subscribe(`${base}/case`, msg => handleCase(JSON.parse(msg.body))); + stompClient.subscribe(`${base}/final`, msg => handleFinal(JSON.parse(msg.body))); stompClient.subscribe(`${base}/git-status`, msg => { let status; try { @@ -484,29 +504,19 @@

채점 결과

else if (status === 'FAILED') gitDot.style.backgroundColor = '#ff4d4d'; else gitDot.style.backgroundColor = 'gray'; }); - stompClient.subscribe(`${base}/init`, msg => handleInit(JSON.parse(msg.body))); - stompClient.subscribe(`${base}/case`, msg => handleCase(JSON.parse(msg.body))); - stompClient.subscribe(`${base}/final`, msg => handleFinal(JSON.parse(msg.body))); }, err => console.error('[STOMP] 연결 오류:', err)); } - function handleInit(slots) { - const judgeEl = document.getElementById('judgeResult'); - judgeEl.innerHTML = ''; - slots.forEach(s => { - const d = document.createElement('div'); - d.id = `slot-${s.seqId}`; - d.className = 'slot init'; - d.textContent = `[${s.seqId}] 상태: ${s.status}`; - judgeEl.appendChild(d); - }); - } - function handleCase(data) { - const d = document.getElementById(`slot-${data.seqId}`); + const pk = data.testcaseId || data.id; // 서버에서 오는 실제 PK + const slotIndex = testcaseSlotMap[pk]; + if (!slotIndex) return; + + const d = document.getElementById(`slot-${slotIndex}`); if (!d) return; + d.className = data.isPassed ? 'slot passed' : 'slot failed'; - d.textContent = `[${data.seqId}] ${data.message} (${data.executionTime}ms, ${data.memoryUsage}KB)`; + d.textContent = `[${slotIndex}] ${data.message} (${data.executionTime}ms, ${data.memoryUsage}KB)`; } function handleFinal(res) { @@ -546,7 +556,6 @@

채점 결과

} } -