|
129 | 129 | box-shadow: 0 2px 6px rgba(76, 175, 80, 0.3); |
130 | 130 | } |
131 | 131 |
|
| 132 | + #login-btn:hover { |
| 133 | + background: #1976d2; |
| 134 | + transform: translateY(-1px); |
| 135 | + box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3); |
| 136 | + } |
| 137 | + |
| 138 | + #login-btn:active { |
| 139 | + transform: translateY(0); |
| 140 | + box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3); |
| 141 | + } |
| 142 | + |
| 143 | + #login-btn:disabled { |
| 144 | + background: #cccccc; |
| 145 | + cursor: not-allowed; |
| 146 | + transform: none; |
| 147 | + box-shadow: none; |
| 148 | + } |
| 149 | + |
132 | 150 | #audio-player { |
133 | 151 | display: none; |
134 | 152 | } |
|
140 | 158 | <div id="status"> |
141 | 159 | <div style="display: flex; justify-content: space-between; align-items: center;"> |
142 | 160 | <div style="display: flex; align-items: center; gap: 1em;"> |
143 | | - <span>WebSocket 상태: <span id="ws-status">초기화 대기 중</span></span> |
| 161 | + <span>로그인: <span id="login-status">대기 중</span></span> |
| 162 | + <span style="color: #6c757d; font-size: 0.85em;">| WebSocket: <span id="ws-status">대기 중</span></span> |
144 | 163 | <span style="color: #6c757d; font-size: 0.85em;">| 세션: <span id="session-id">없음</span></span> |
145 | 164 | </div> |
146 | 165 | <span style="color: #6c757d; font-size: 0.85em;">서버: <span id="server-info"></span></span> |
147 | 166 | </div> |
148 | 167 | </div> |
149 | 168 |
|
| 169 | + <div id="login-section"> |
| 170 | + <div style="display: flex; align-items: center; gap: 1em; margin-bottom: 1em;"> |
| 171 | + <input id="guest-id" type="text" placeholder="게스트 ID를 입력하세요" value="test-guest-123" |
| 172 | + style="flex: 1; padding: 0.75em; border-radius: 8px; border: 1px solid #c8e6c9; font-size: 1em;" /> |
| 173 | + <button id="login-btn" style="background: #2196f3; color: white; border: none; padding: 0.75em 1.5em; border-radius: 8px; font-size: 1em; cursor: pointer; font-weight: 500;"> |
| 174 | + 게스트 로그인 |
| 175 | + </button> |
| 176 | + </div> |
| 177 | + </div> |
| 178 | + |
150 | 179 | <div class="top-row"> |
151 | 180 | <select id="character-select"> |
152 | 181 | <option value="44444444-4444-4444-4444-444444444444">제로</option> |
|
182 | 211 | const ENDPOINT = `${currentHost}:${serverPort}`; |
183 | 212 | const WS_URL = `ws://${ENDPOINT}/ws`; |
184 | 213 | const HTTP_URL = `http://${ENDPOINT}/api/v1/chat`; |
| 214 | + const LOGIN_URL = `http://${ENDPOINT}/api/auth/guest-login`; |
185 | 215 | const SERVER_MESSAGE_TYPE = "json"; |
186 | 216 | let sessionId = null; |
187 | 217 | let ws = null; |
188 | 218 | let reconnectAttempts = 0; |
189 | 219 | const MAX_RECONNECT = 3; |
| 220 | + let authToken = null; |
| 221 | + let isLoggedIn = false; |
190 | 222 |
|
191 | 223 | const statusBox = document.getElementById('status'); |
| 224 | + const loginStatus = document.getElementById('login-status'); |
192 | 225 | const wsStatus = document.getElementById('ws-status'); |
193 | 226 | const sessionIdDisplay = document.getElementById('session-id'); |
194 | 227 | const serverInfo = document.getElementById('server-info'); |
|
197 | 230 | const sendBtn = document.getElementById('send-btn'); |
198 | 231 | const audioPlayer = document.getElementById('audio-player'); |
199 | 232 | const characterSelect = document.getElementById('character-select'); |
| 233 | + const guestIdInput = document.getElementById('guest-id'); |
| 234 | + const loginBtn = document.getElementById('login-btn'); |
| 235 | + const loginSection = document.getElementById('login-section'); |
200 | 236 |
|
201 | 237 | const audioQueue = []; |
202 | 238 | let isPlayingAudio = false; |
|
212 | 248 | if (response.ok) { |
213 | 249 | serverConfig = await response.json(); |
214 | 250 | console.log("서버 설정:", serverConfig); |
215 | | - setStatus(`연결됨`); |
216 | 251 | } |
217 | 252 | } catch (e) { |
218 | 253 | console.warn("서버 설정 확인 실패:", e); |
219 | | - setStatus("연결됨 (설정 확인 실패)"); |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + // Guest 로그인 함수 |
| 258 | + async function guestLogin() { |
| 259 | + const guestId = guestIdInput.value.trim(); |
| 260 | + if (!guestId) { |
| 261 | + appendLog('<span style="color:red">[로그인 오류] 게스트 ID를 입력하세요</span>'); |
| 262 | + return; |
| 263 | + } |
| 264 | + |
| 265 | + setLoginStatus('로그인 중...', false); |
| 266 | + loginBtn.disabled = true; |
| 267 | + loginBtn.textContent = '로그인 중...'; |
| 268 | + |
| 269 | + try { |
| 270 | + const response = await fetch(LOGIN_URL, { |
| 271 | + method: 'POST', |
| 272 | + headers: { |
| 273 | + 'Content-Type': 'application/json' |
| 274 | + }, |
| 275 | + body: JSON.stringify({ |
| 276 | + guestId: guestId |
| 277 | + }) |
| 278 | + }); |
| 279 | + |
| 280 | + if (response.ok) { |
| 281 | + const data = await response.json(); |
| 282 | + if (data.success && data.tokens) { |
| 283 | + authToken = data.tokens.accessToken; |
| 284 | + isLoggedIn = true; |
| 285 | + setLoginStatus('로그인 성공', false); |
| 286 | + updateOverallStatus(); |
| 287 | + |
| 288 | + appendLog(`<b>[로그인 성공]</b> 게스트 ID: ${guestId}`); |
| 289 | + appendLog(`<small>사용자: ${data.user.username} (${data.user.email})</small>`); |
| 290 | + |
| 291 | + loginSection.style.display = 'none'; |
| 292 | + |
| 293 | + // 로그인 성공 후 WebSocket 연결 시작 |
| 294 | + connectWebSocket(); |
| 295 | + } else { |
| 296 | + throw new Error(data.message || '로그인 실패'); |
| 297 | + } |
| 298 | + } else { |
| 299 | + const errorData = await response.json(); |
| 300 | + throw new Error(errorData.message || `HTTP ${response.status}`); |
| 301 | + } |
| 302 | + } catch (error) { |
| 303 | + console.error('로그인 오류:', error); |
| 304 | + setLoginStatus('로그인 실패', true); |
| 305 | + updateOverallStatus(); |
| 306 | + appendLog(`<span style="color:red">[로그인 오류] ${error.message}</span>`); |
| 307 | + } finally { |
| 308 | + loginBtn.disabled = false; |
| 309 | + loginBtn.textContent = '게스트 로그인'; |
220 | 310 | } |
221 | 311 | } |
222 | 312 |
|
|
275 | 365 | } |
276 | 366 | } |
277 | 367 |
|
278 | | - function setStatus(message, isError = false) { |
| 368 | + function setLoginStatus(message, isError = false) { |
| 369 | + loginStatus.textContent = message; |
| 370 | + loginStatus.style.color = isError ? '#dc3545' : '#2e7d32'; |
| 371 | + } |
| 372 | + |
| 373 | + function setWSStatus(message, isError = false) { |
279 | 374 | wsStatus.textContent = message; |
280 | 375 | wsStatus.style.color = isError ? '#dc3545' : '#2e7d32'; |
281 | | - statusBox.style.background = isError ? '#ffebee' : '#e8f5e8'; |
282 | | - statusBox.style.borderColor = isError ? '#ffcdd2' : '#c8e6c9'; |
| 376 | + } |
| 377 | + |
| 378 | + function updateOverallStatus() { |
| 379 | + const hasError = loginStatus.style.color === 'rgb(220, 53, 69)' || wsStatus.style.color === 'rgb(220, 53, 69)'; |
| 380 | + statusBox.style.background = hasError ? '#ffebee' : '#e8f5e8'; |
| 381 | + statusBox.style.borderColor = hasError ? '#ffcdd2' : '#c8e6c9'; |
283 | 382 | } |
284 | 383 |
|
285 | 384 | function updateSessionId(sessionId) { |
|
336 | 435 | } |
337 | 436 |
|
338 | 437 | function connectWebSocket() { |
| 438 | + if (!isLoggedIn) { |
| 439 | + setWSStatus("로그인 필요", true); |
| 440 | + updateOverallStatus(); |
| 441 | + return; |
| 442 | + } |
| 443 | + |
| 444 | + setWSStatus("연결 중...", false); |
| 445 | + updateOverallStatus(); |
| 446 | + |
339 | 447 | ws = new WebSocket(sessionId ? `${WS_URL}?sessionId=${sessionId}` : WS_URL); |
340 | 448 | ws.binaryType = "arraybuffer"; |
341 | 449 |
|
342 | 450 | ws.onopen = () => { |
343 | | - setStatus("연결됨"); |
| 451 | + setWSStatus("연결됨", false); |
| 452 | + updateOverallStatus(); |
344 | 453 | reconnectAttempts = 0; |
345 | 454 | checkServerConfig(); |
346 | 455 | }; |
|
528 | 637 | }; |
529 | 638 |
|
530 | 639 | ws.onclose = () => { |
531 | | - setStatus("연결 종료됨", true); |
| 640 | + setWSStatus("연결 종료됨", true); |
| 641 | + updateOverallStatus(); |
532 | 642 | updateSessionId(null); |
533 | 643 | appendLog("<b>[WebSocket 연결 종료]</b>"); |
534 | | - tryReconnect(); |
| 644 | + if (isLoggedIn) tryReconnect(); |
535 | 645 | }; |
536 | 646 |
|
537 | 647 | ws.onerror = () => { |
538 | | - setStatus("오류 발생", true); |
| 648 | + setWSStatus("오류 발생", true); |
| 649 | + updateOverallStatus(); |
539 | 650 | updateSessionId(null); |
540 | 651 | appendLog("<b>[WebSocket 오류]</b>"); |
541 | 652 | }; |
|
544 | 655 | function tryReconnect() { |
545 | 656 | if (reconnectAttempts < MAX_RECONNECT) { |
546 | 657 | reconnectAttempts++; |
547 | | - setStatus(`재연결 시도 중... (${reconnectAttempts}/${MAX_RECONNECT})`, true); |
| 658 | + setWSStatus(`재연결 시도 중... (${reconnectAttempts}/${MAX_RECONNECT})`, true); |
| 659 | + updateOverallStatus(); |
548 | 660 | setTimeout(connectWebSocket, 1000 * reconnectAttempts); |
549 | 661 | } else { |
550 | | - setStatus("재연결 실패. 새로고침 해주세요.", true); |
| 662 | + setWSStatus("재연결 실패. 새로고침 해주세요.", true); |
| 663 | + updateOverallStatus(); |
551 | 664 | } |
552 | 665 | } |
553 | 666 |
|
|
566 | 679 | }; |
567 | 680 | if (sessionId) payload.session_id = sessionId; |
568 | 681 |
|
| 682 | + const headers = { "Content-Type": "application/json" }; |
| 683 | + if (authToken) { |
| 684 | + headers["Authorization"] = `Bearer ${authToken}`; |
| 685 | + } |
| 686 | + |
569 | 687 | fetch(HTTP_URL, { |
570 | 688 | method: "POST", |
571 | | - headers: { "Content-Type": "application/json" }, |
| 689 | + headers: headers, |
572 | 690 | body: JSON.stringify(payload) |
573 | 691 | }) |
574 | 692 | .then(res => { |
|
589 | 707 |
|
590 | 708 | sendBtn.onclick = sendChat; |
591 | 709 | userInput.onkeydown = (e) => { if (e.key === "Enter") sendChat(); }; |
| 710 | + |
| 711 | + loginBtn.onclick = guestLogin; |
| 712 | + guestIdInput.onkeydown = (e) => { if (e.key === "Enter") guestLogin(); }; |
592 | 713 |
|
593 | 714 | characterSelect.onchange = () => { |
594 | 715 | userInput.value = ""; |
595 | 716 | chatLog.innerHTML = ""; |
596 | 717 | appendLog(`<i>[캐릭터 변경됨: ${characterSelect.options[characterSelect.selectedIndex].text}]</i>`); |
597 | 718 | }; |
598 | 719 |
|
599 | | - connectWebSocket(); |
| 720 | + // 초기화 - 로그인을 기다림 |
| 721 | + appendLog('<b>[시작]</b> 게스트 ID를 입력하고 로그인하세요.'); |
600 | 722 | </script> |
601 | 723 | </body> |
602 | 724 |
|
|
0 commit comments