Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 178 additions & 62 deletions src/main/resources/templates/submit-test.html
Original file line number Diff line number Diff line change
@@ -1,29 +1,125 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta charset="UTF-8"/>
<title>코드 제출 테스트 (WebSocket)</title>
<link rel="icon" href="data:;base64,=">
<style>
body { font-family:'Segoe UI',sans-serif; background:#f8f9fa; padding:30px; }
.section { background:#fff; padding:20px; border-radius:8px; margin-bottom:30px; box-shadow:0 2px 4px rgba(0,0,0,0.05); }
.result-box { background:#fff; border:1px solid #dee2e6; border-radius:5px; padding:15px; white-space:pre; }
.slot { padding:8px; margin:4px 0; border-radius:4px; }
.init { background:#eef; } .passed { background:#efe; } .failed { background:#fee; }
button { padding:10px 20px; background:#007bff; border:none; border-radius:4px; color:#fff; cursor:pointer; }
button:hover { background:#0056b3; }
textarea,input { width:100%; padding:10px; margin:5px 0 15px; box-sizing:border-box; border:1px solid #ced4da; border-radius:4px; font-family:monospace; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f8f9fa;
padding: 30px;
}

.section {
background: #fff;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.result-box {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
white-space: pre;
}

.slot {
padding: 8px;
margin: 4px 0;
border-radius: 4px;
}

.init {
background: #eef;
}

.passed {
background: #efe;
}

.failed {
background: #fee;
}

button {
padding: 10px 20px;
background: #007bff;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
}

button:hover {
background: #0056b3;
}

textarea, input {
width: 100%;
padding: 10px;
margin: 5px 0 15px;
box-sizing: border-box;
border: 1px solid #ced4da;
border-radius: 4px;
font-family: monospace;
}

/* Markdown styling */
#prob-desc { padding:10px; border:1px solid #ced4da; border-radius:4px; background:#f1f1f1; }
#prob-desc h1 { font-size:1.5em; margin-bottom:0.5em; }
#prob-desc h2 { font-size:1.3em; margin-bottom:0.4em; }
#prob-desc p { margin-bottom:0.8em; }
#prob-desc code { background:#eaeaea; padding:2px 4px; border-radius:3px; font-family:monospace; }
#prob-desc pre { background:#eaeaea; padding:10px; border-radius:4px; overflow:auto; }
#prob-desc ul { margin-left:1em; list-style:disc; }
#prob-desc ol { margin-left:1em; list-style:decimal; }
#prob-desc blockquote { border-left:4px solid #ccc; padding-left:10px; color:#666; margin:1em 0; }
#prob-desc {
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
background: #f1f1f1;
}

#prob-desc h1 {
font-size: 1.5em;
margin-bottom: 0.5em;
}

#prob-desc h2 {
font-size: 1.3em;
margin-bottom: 0.4em;
}

#prob-desc p {
margin-bottom: 0.8em;
}

#prob-desc code {
background: #eaeaea;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}

#prob-desc pre {
background: #eaeaea;
padding: 10px;
border-radius: 4px;
overflow: auto;
}

#prob-desc ul {
margin-left: 1em;
list-style: disc;
}

#prob-desc ol {
margin-left: 1em;
list-style: decimal;
}

#prob-desc blockquote {
border-left: 4px solid #ccc;
padding-left: 10px;
color: #666;
margin: 1em 0;
}
</style>

<!-- SockJS, STOMP.js, marked.js -->
Expand All @@ -33,34 +129,34 @@

<script defer>
document.addEventListener('DOMContentLoaded', () => {
// JWT 토큰 ↔ localStorage
// JWT 토큰 ↔ localStorage
const jwtInput = document.getElementById('jwtToken');
const stored = localStorage.getItem('jwtToken');
if (stored) jwtInput.value = stored;
if (localStorage.getItem('jwtToken')) jwtInput.value = localStorage.getItem('jwtToken');
jwtInput.addEventListener('input', () => localStorage.setItem('jwtToken', jwtInput.value.trim()));

function getTokenValue() {
const raw = jwtInput.value.trim();
return raw.startsWith('Bearer ') ? raw : 'Bearer ' + raw;
}

function tokenHeader() {
const raw = jwtInput.value.trim();
return raw ? { 'Authorization': getTokenValue() } : {};
return raw ? {'Authorization': getTokenValue()} : {};
}

// 요소 참조
// 요소 참조
const probTitleEl = document.getElementById('prob-title');
const probDescEl = document.getElementById('prob-desc');
const probTimeEl = document.getElementById('prob-time');
const probMemEl = document.getElementById('prob-mem');
const probCatsEl = document.getElementById('prob-cats');
const pidInput = document.getElementById('problemId');
const judgeEl = document.getElementById('judgeResult');

// 초기 problemId 설정 (쿼리스트링 또는 기본 1)
const probDescEl = document.getElementById('prob-desc');
const probTimeEl = document.getElementById('prob-time');
const probMemEl = document.getElementById('prob-mem');
const probCatsEl = document.getElementById('prob-cats');
const pidInput = document.getElementById('problemId');
const judgeEl = document.getElementById('judgeResult');

// 초기 problemId 설정 (쿼리스트링 or 기본 1)
pidInput.value = new URLSearchParams(location.search).get('problemId') || '1';

// 문제 상세 조회 Markdown 렌더링
// 문제 상세 조회 & Markdown 렌더링
async function loadProblem() {
const pid = pidInput.value.trim();
if (!pid) {
Expand All @@ -71,7 +167,7 @@
}
try {
const res = await fetch(`/api/problems/${pid}`, {
headers: { 'Accept':'application/json', ...tokenHeader() }
headers: {'Accept': 'application/json', ...tokenHeader()}
});
if (res.status === 401) {
probTitleEl.textContent = '인증 필요: JWT 토큰을 입력하세요';
Expand All @@ -83,13 +179,12 @@
}
const wrapper = await res.json();
const data = wrapper.result ?? wrapper;

probTitleEl.textContent = `${data.id}. ${data.title}`;
probDescEl.innerHTML = data.description
probDescEl.innerHTML = data.description
? marked.parse(data.description)
: '';
probTimeEl.textContent = data.timeLimit ?? '--';
probMemEl.textContent = data.memoryLimit ?? '--';
probTimeEl.textContent = data.timeLimit ?? '--';
probMemEl.textContent = data.memoryLimit ?? '--';
probCatsEl.textContent = Array.isArray(data.categories)
? data.categories.join(', ')
: '';
Expand All @@ -98,19 +193,40 @@
probTitleEl.textContent = '네트워크 오류';
}
}

pidInput.addEventListener('change', loadProblem);
loadProblem();

// WebSocket 제출 & 구독
let stompClient, sessionKey;
// — 1) WebSocket 연결 (한번만) —
let stompClient;
let connected = false;

function initWebSocket() {
const tokenOnly = (jwtInput.value.trim().startsWith('Bearer ')
? jwtInput.value.trim().slice(7)
: jwtInput.value.trim());
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
stompClient = Stomp.over(socket);
stompClient.debug = msg => console.log('[STOMP]', msg);
Comment on lines +204 to +210
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

JWT 토큰을 URL 파라미터로 노출하는 방식은 보안 리스크가 큽니다
URL(쿼리스트링)에 토큰이 포함되면 브라우저 히스토리·서버 액세스 로그·프락시 등 여러 경로로 유출될 수 있습니다.
STOMP connect(headers, …) 인자로 Authorization 헤더를 전달하면 동일 기능을 쿠키·URL 로그에 노출하지 않고 구현할 수 있습니다.

-const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
-stompClient = Stomp.over(socket);
-stompClient.debug = msg => console.log('[STOMP]', msg);
-stompClient.connect({}, () => {
+const socket = new SockJS('/ws');
+stompClient = Stomp.over(socket);
+stompClient.debug = msg => console.log('[STOMP]', msg);  // 배포 시 끄는 것도 고려
+stompClient.connect({ Authorization: getTokenValue() }, () => {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/resources/templates/submit-test.html around lines 204 to 210, the
JWT token is currently passed as a URL query parameter, which poses a security
risk due to potential exposure in browser history and logs. To fix this, remove
the token from the URL and instead pass it as an Authorization header in the
STOMP client's connect method by adding a headers object with the Authorization
field set to 'Bearer ' plus the token. This change keeps the token out of URLs
and logs while maintaining the same authentication functionality.

stompClient.connect({}, () => {
console.log('[STOMP] Connected');
connected = true;
}, err => console.error('[STOMP] 연결 오류:', err));
}

initWebSocket();

// — 2) 코드 제출 & sessionKey 구한 뒤, 즉시 구독 추가 —
window.submitCode = async () => {
judgeEl.innerHTML = '';
const pid = pidInput.value.trim();
const payload = {
languageId: +document.getElementById('languageId').value,
sourceCode: document.getElementById('sourceCode').value
};

try {
// REST 호출로 sessionKey 받기
const res = await fetch(`/api/problems/${pid}/submit-ws`, {
method: 'POST',
headers: {
Expand All @@ -124,33 +240,33 @@
judgeEl.textContent = `Error: ${resp.message}`;
return;
}
sessionKey = resp.result;
connectAndSubscribe();
} catch (err) {
console.error(err);
judgeEl.textContent = '채점 요청 실패';
}
};

function connectAndSubscribe() {
const tokenOnly = getTokenValue().replace(/^Bearer\s+/, '');
const socket = new SockJS(`/ws?token=${encodeURIComponent(tokenOnly)}`);
stompClient = Stomp.over(socket);
const sessionKey = resp.result;

// 디버그 로그
stompClient.debug = msg => console.log('[STOMP]', msg);
// (A) stompClient 가 연결될 때까지 대기
if (!connected) {
await new Promise(r => {
const iv = setInterval(() => {
if (connected) {
clearInterval(iv);
r()
}
}, 50);
});
}

stompClient.connect({}, () => {
// (B) sessionKey별 채널 구독
const base = `/topic/submission/${sessionKey}`;
// INIT 이벤트 채널
stompClient.subscribe(`${base}/init`, msg => handleInit(JSON.parse(msg.body)));
// CASE 이벤트 채널
stompClient.subscribe(`${base}/case`, msg => handleCase(JSON.parse(msg.body)));
// FINAL 이벤트 채널
stompClient.subscribe(`${base}/final`, msg => handleFinal(JSON.parse(msg.body)));
}, err => console.error('STOMP 연결 오류:', err));
}

} catch (err) {
console.error(err);
judgeEl.textContent = '채점 요청 실패';
}
};

// — 메시지 핸들러 —
function handleInit(slots) {
judgeEl.innerHTML = '';
slots.forEach(s => {
Expand Down Expand Up @@ -193,11 +309,11 @@ <h2 id="prob-title">로딩 중…</h2>
<div class="section">
<h2>코드 제출 (WebSocket)</h2>
<label>JWT 토큰:</label>
<input type="text" id="jwtToken" placeholder="Bearer …" />
<input type="text" id="jwtToken" placeholder="Bearer …"/>
<label>문제 ID:</label>
<input type="number" id="problemId" placeholder="문제 ID" />
<input type="number" id="problemId" placeholder="문제 ID"/>
<label>언어 ID:</label>
<input type="number" id="languageId" placeholder="언어 ID" />
<input type="number" id="languageId" placeholder="언어 ID"/>
<label>소스코드:</label>
<textarea id="sourceCode" rows="8">// 코드를 입력하세요</textarea>
<button onclick="submitCode()">코드 제출 (WS)</button>
Expand Down