Skip to content

Commit 46cf80e

Browse files
authored
refactor: add submit prepare, ready (#159)
1 parent 17f3609 commit 46cf80e

File tree

6 files changed

+106
-46
lines changed

6 files changed

+106
-46
lines changed

src/main/java/org/ezcode/codetest/application/submission/dto/request/submission/CodeSubmitRequest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ public record CodeSubmitRequest(
1616
example = "public class Main { public static void main(String[] args) { System.out.println(\"Hello World\"); } }"
1717
)
1818
@NotBlank(message = "소스 코드는 필수 입력 값입니다.")
19-
String sourceCode
19+
String sourceCode,
20+
21+
@NotBlank(message = "세션 키는 필수 입력 값입니다.")
22+
String sessionKey
2023

2124
) {
2225
}

src/main/java/org/ezcode/codetest/application/submission/service/SubmissionService.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ public class SubmissionService {
5555
private final JudgementService judgementService;
5656
private final GitHubPushService gitHubPushService;
5757

58-
public SubmitResponse enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) {
59-
58+
public SubmitResponse prepareSubmission(Long problemId, AuthUser authUser) {
6059
boolean acquired = lockManager.tryLock("submission", authUser.getId(), problemId);
6160
if (!acquired) {
6261
throw new SubmissionException(SubmissionExceptionCode.ALREADY_JUDGING);
@@ -65,11 +64,13 @@ public SubmitResponse enqueueCodeSubmission(Long problemId, CodeSubmitRequest re
6564
String sessionKey = authUser.getId() + "_" + UUID.randomUUID();
6665
List<Testcase> testcaseList = testcaseDomainService.getTestcaseListByProblemId(problemId);
6766

67+
return SubmitResponse.of(sessionKey, testcaseList);
68+
}
69+
70+
public void enqueueCodeSubmission(Long problemId, CodeSubmitRequest request, AuthUser authUser) {
6871
queueProducer.enqueue(
69-
new SubmissionMessage(sessionKey, problemId, request.languageId(), authUser.getId(), request.sourceCode())
72+
SubmissionMessage.of(request, problemId, authUser.getId())
7073
);
71-
72-
return SubmitResponse.of(sessionKey, testcaseList);
7374
}
7475

7576
@Async("judgeSubmissionExecutor")

src/main/java/org/ezcode/codetest/infrastructure/event/dto/submission/SubmissionMessage.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.ezcode.codetest.infrastructure.event.dto.submission;
22

3+
import org.ezcode.codetest.application.submission.dto.request.submission.CodeSubmitRequest;
4+
35
public record SubmissionMessage(
46

57
String sessionKey,
@@ -13,4 +15,13 @@ public record SubmissionMessage(
1315
String sourceCode
1416

1517
) {
18+
public static SubmissionMessage of(CodeSubmitRequest request, Long problemId, Long userId) {
19+
return new SubmissionMessage(
20+
request.sessionKey(),
21+
problemId,
22+
request.languageId(),
23+
userId,
24+
request.sourceCode()
25+
);
26+
}
1627
}

src/main/java/org/ezcode/codetest/presentation/submission/SubmissionController.java

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,37 +32,46 @@ public class SubmissionController {
3232

3333
private final SubmissionService submissionService;
3434

35-
@PostMapping("/problems/{problemId}/submit-ws")
35+
@PostMapping("/problems/{problemId}/submit-prepare")
36+
public ResponseEntity<SubmitResponse> prepareSubmission(
37+
@PathVariable Long problemId,
38+
@AuthenticationPrincipal AuthUser authUser
39+
) {
40+
return ResponseEntity
41+
.status(HttpStatus.OK)
42+
.body(submissionService.prepareSubmission(problemId, authUser));
43+
}
44+
45+
@PostMapping("/problems/{problemId}/submit-ready")
3646
@Operation(
3747
summary = "코드 제출 (WebSocket)",
3848
description = """
39-
문제에 대한 코드를 제출하면 채점 큐에 등록되고,
40-
서버는 WebSocket(STOMP)을 통해 채점 결과를 실시간으로 전송합니다.
41-
42-
반환된 sessionKey를 사용해 다음 경로로 구독하세요.
43-
44-
• /user/queue/submission/{sessionKey}/case
45-
46-
• /user/queue/submission/{sessionKey}/final
47-
48-
• /topic/submission/{sessionKey}/error
49-
50-
• /user/queue/submission/{sessionKey}/git-status
51-
"""
49+
문제에 대한 코드를 제출하면 채점 큐에 등록되고,
50+
서버는 WebSocket(STOMP)을 통해 채점 결과를 실시간으로 전송합니다.
51+
52+
반환된 sessionKey를 사용해 다음 경로로 구독하세요.
53+
54+
• /user/queue/submission/{sessionKey}/case
55+
56+
• /user/queue/submission/{sessionKey}/final
57+
58+
• /topic/submission/{sessionKey}/error
59+
60+
• /user/queue/submission/{sessionKey}/git-status
61+
"""
5262
)
5363
@ApiResponses({
5464
@ApiResponse(responseCode = "200", description = "코드 제출 성공 및 sessionKey 반환"),
5565
@ApiResponse(responseCode = "400", description = "유효하지 않은 요청 데이터"),
5666
@ApiResponse(responseCode = "409", description = "이미 해당 문제를 채점 중인 경우"),
5767
})
58-
public ResponseEntity<SubmitResponse> submitCodeStream(
68+
public ResponseEntity<Void> submitCodeStream(
5969
@Parameter(description = "제출할 문제 ID", required = true) @PathVariable Long problemId,
6070
@RequestBody @Valid CodeSubmitRequest request,
6171
@AuthenticationPrincipal AuthUser authUser
6272
) {
63-
return ResponseEntity
64-
.status(HttpStatus.OK)
65-
.body(submissionService.enqueueCodeSubmission(problemId, request, authUser));
73+
submissionService.enqueueCodeSubmission(problemId, request, authUser);
74+
return ResponseEntity.status(HttpStatus.OK).build();
6675
}
6776

6877
@Operation(

src/main/resources/templates/test-submit.html

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -953,32 +953,45 @@ <h3>채점 결과</h3>
953953
const pid = window.problemId;
954954
const languageId = +document.getElementById('language').value;
955955
const sourceCode = window.getEditorCode();
956-
const payload = { languageId, sourceCode };
957956
const judgeEl = document.getElementById('judgeResult');
958957
judgeEl.innerHTML = '';
959958
reviewBox.style.display = 'none';
960959
reviewBtn.disabled = true;
961960
finalIsCorrect = null;
962961

963962
try {
964-
const res = await fetch(`/api/problems/${pid}/submit-ws`, {
963+
const res = await fetch(`/api/problems/${pid}/submit-prepare`, {
965964
method: 'POST',
966965
headers: {
967966
'Content-Type': 'application/json',
968967
...tokenHeader()
969968
},
970-
body: JSON.stringify(payload)
971969
});
972970
const resp = await res.json();
973971
console.log('[DEBUG] 전체 응답:', resp);
974972
console.log('[DEBUG] result:', resp.result);
975973
if (!resp.success) {
976-
judgeEl.textContent = `Error: ${resp.message}`;
974+
judgeEl.textContent = `${resp.message}`;
977975
return;
978976
}
979977
const { sessionKey, testcaseIds } = resp.result;
980-
initWebSocket(sessionKey);
981-
renderInitSlots(testcaseIds);
978+
979+
initWebSocket(sessionKey, async () => {
980+
renderInitSlots(testcaseIds);
981+
982+
await fetch(`/api/problems/${pid}/submit-ready`, {
983+
method: 'POST',
984+
headers: {
985+
'Content-Type': 'application/json',
986+
...tokenHeader()
987+
},
988+
body: JSON.stringify({
989+
sessionKey,
990+
languageId,
991+
sourceCode
992+
})
993+
});
994+
});
982995
} catch (err) {
983996
console.error(err);
984997
judgeEl.textContent = '채점 요청 실패';
@@ -1058,7 +1071,7 @@ <h3>채점 결과</h3>
10581071
// submitCode() 에서 호출할 함수
10591072
window.getEditorCode = () => editor.getValue();
10601073

1061-
function initWebSocket(sessionKey) {
1074+
function initWebSocket(sessionKey, onReady) {
10621075
const tokenOnly = (sessionStorage.getItem('accessToken') || '').replace(/^Bearer /, '');
10631076
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
10641077
stompClient = Stomp.over(socket);
@@ -1082,7 +1095,13 @@ <h3>채점 결과</h3>
10821095
else if (status === 'FAILED') gitDot.style.backgroundColor = '#ff4d4d';
10831096
else gitDot.style.backgroundColor = 'gray';
10841097
});
1085-
}, err => console.error('[STOMP] 연결 오류:', err));
1098+
1099+
if (typeof onReady === 'function') {
1100+
onReady();
1101+
}
1102+
}, err => {
1103+
console.error('[STOMP] 연결 오류:', err);
1104+
});
10861105
}
10871106

10881107
function handleCase(data) {
@@ -1431,7 +1450,7 @@ <h3>채점 결과</h3>
14311450
if (res.ok) {
14321451
// 성공: 목록 새로고침
14331452
if (item.classList.contains('discussion-item')) {
1434-
loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
1453+
await loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
14351454
} else {
14361455
const discussionId = item.closest('.discussion-item').dataset.discussionId;
14371456
const container = document.getElementById(`replies-for-${discussionId}`);
@@ -1457,7 +1476,7 @@ <h3>채점 결과</h3>
14571476
const discussionId = item.closest('.discussion-item').dataset.discussionId;
14581477
url = `/api/problems/${window.problemId}/discussions/${discussionId}/replies/${e.target.dataset.id}`;
14591478
}
1460-
(async () => {
1479+
await (async () => {
14611480
const res = await fetch(url, {
14621481
method,
14631482
headers: {
@@ -1466,7 +1485,7 @@ <h3>채점 결과</h3>
14661485
});
14671486
if (res.ok) {
14681487
if (item.classList.contains('discussion-item')) {
1469-
loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
1488+
await loadDiscussions(window.problemId, 0, 10, document.getElementById('discussion-sort')?.value || 'best');
14701489
} else {
14711490
const discussionId = item.closest('.discussion-item').dataset.discussionId;
14721491
const container = document.getElementById(`replies-for-${discussionId}`);
@@ -1493,7 +1512,7 @@ <h3>채점 결과</h3>
14931512
body: JSON.stringify({ voteType })
14941513
});
14951514
if (res.ok) {
1496-
updateDiscussionVoteUI(discussionId);
1515+
await updateDiscussionVoteUI(discussionId);
14971516
} else {
14981517
alert('추천/비추천에 실패했습니다.');
14991518
}
@@ -1534,7 +1553,7 @@ <h3>채점 결과</h3>
15341553
body: JSON.stringify({ voteType })
15351554
});
15361555
if (res.ok) {
1537-
updateReplyVoteUI(discussionId, replyId);
1556+
await updateReplyVoteUI(discussionId, replyId);
15381557
} else {
15391558
alert('추천/비추천에 실패했습니다.');
15401559
}

src/test/java/org/ezcode/codetest/application/submission/SubmissionServiceTest.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.ezcode.codetest.domain.language.service.LanguageDomainService;
2727
import org.ezcode.codetest.domain.problem.model.ProblemInfo;
2828
import org.ezcode.codetest.domain.problem.model.entity.Problem;
29+
import org.ezcode.codetest.domain.problem.model.entity.Testcase;
2930
import org.ezcode.codetest.domain.problem.service.ProblemDomainService;
3031
import org.ezcode.codetest.domain.problem.service.TestcaseDomainService;
3132
import org.ezcode.codetest.domain.submission.exception.SubmissionException;
@@ -125,28 +126,44 @@ void setup() {
125126
class SuccessCases {
126127

127128
@Test
128-
@DisplayName("락 획득 성공 -> 메세지 큐에 전송하고 sessionKey 반환")
129+
@DisplayName("락 획득 성공 -> sessionKey, testcaseList 반환")
130+
void prepareSubmissionSuccess() {
131+
132+
// given
133+
given(authUser.getId()).willReturn(userId);
134+
given(lockManager.tryLock("submission", userId, problemId)).willReturn(true);
135+
136+
List<Testcase> testcaseList = List.of(mock(Testcase.class), mock(Testcase.class));
137+
given(testcaseDomainService.getTestcaseListByProblemId(problemId)).willReturn(testcaseList);
138+
139+
// when
140+
SubmitResponse result = submissionService.prepareSubmission(problemId, authUser);
141+
142+
// then
143+
assertThat(result.sessionKey()).startsWith(userId + "_");
144+
assertThat(result.testcaseIds()).hasSize(2);
145+
then(lockManager).should().tryLock("submission", userId, problemId);
146+
then(testcaseDomainService).should().getTestcaseListByProblemId(problemId);
147+
}
148+
149+
@Test
150+
@DisplayName("큐에 메세지 전송 확인")
129151
void enqueueCodeSubmissionSucceeds() {
130152

131153
// given
132154
given(authUser.getId()).willReturn(userId);
133155
given(request.languageId()).willReturn(languageId);
134156
given(request.sourceCode()).willReturn(sourceCode);
135-
given(lockManager.tryLock("submission", authUser.getId(), problemId))
136-
.willReturn(true);
137-
given(testcaseDomainService.getTestcaseListByProblemId(problemId)).willReturn(List.of());
138157

139158
// when
140-
SubmitResponse result = submissionService.enqueueCodeSubmission(problemId, request, authUser);
159+
submissionService.enqueueCodeSubmission(problemId, request, authUser);
141160

142161
then(queueProducer).should()
143162
.enqueue(argThat(msg ->
144-
msg.sessionKey().startsWith(sessionKey) &&
145163
msg.problemId().equals(problemId) &&
146164
msg.languageId().equals(languageId) &&
147165
msg.sourceCode().equals(sourceCode)
148166
));
149-
assertThat(result.sessionKey()).startsWith(sessionKey);
150167
}
151168

152169
@Test
@@ -251,7 +268,7 @@ class FailureCases {
251268

252269
@Test
253270
@DisplayName("락 획득 실패 -> ALREADY_JUDGING 예외")
254-
void enqueueCodeSubmissionFailed() {
271+
void prepareSubmissionFailed() {
255272

256273
// given
257274
given(authUser.getId()).willReturn(userId);
@@ -260,7 +277,7 @@ void enqueueCodeSubmissionFailed() {
260277

261278
// when & then
262279
assertThatThrownBy(() ->
263-
submissionService.enqueueCodeSubmission(problemId, request, authUser)
280+
submissionService.prepareSubmission(problemId, authUser)
264281
)
265282
.isInstanceOf(SubmissionException.class)
266283
.hasMessage(SubmissionExceptionCode.ALREADY_JUDGING.getMessage());

0 commit comments

Comments
 (0)