내용이 길어지는 관계로 프론트 코드 부분만 따로 분리했다.
REST API로 백엔드의 taortchat_getsession Lambda와 연결, 세션 목록을 불러오는 함수.
async function fetchSessions(userId) {
// 사용자 ID가 존재하지 않을 경우 동작하지 않음
if (!userId) {
console.error('User ID is required to fetch sessions');
return;
}
API 접속 전 토큰 존재를 확인하고 그 유효성을 체크. ensureValidToken 함수를 비롯한 인증 관련 내용은 뒤에서 따로 다루겠다.
try {
const token = localStorage.getItem('auth_token');
if (!token) {
console.error('No auth token available');
return;
}
await ensureValidToken();
백엔드에서 세션 목록에 대한 응답을 받아온다.
const response = await fetch(`${config.restEndpoint}/sessions?userId=${userId}`, {
method: 'GET',
headers: {
'Authorization': localStorage.getItem('auth_token'),
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
응답을 파싱하여 세션 목록을 반환하고 표시한다. displaysession 함수는 아래에서 설명하겠다.
const sessions = await response.json();
displaySessions(sessions);
return sessions;
} catch (error) {
console.error('Error fetching sessions:', error);
hideSessionLoadingIndicator();
if (error.message.includes('token')) {
handleLogout();
}
}
}
function displaySessions(sessions) {
const sessionList = document.getElementById('sessionList');
if (!sessionList) return; // sessionList 존재하지 않으면 함수 종료
세션 목록 전체에 클릭 이벤트 리스너를 추가하고 기존 세션 목록을 비운다. 이때 hasEventListener 플래그는 이벤트 리스너가 중복 등록되지 않도록 하기 위해 추가하였다.
if (!sessionList.hasEventListener) {
sessionList.addEventListener('click', handleSessionClick);
sessionList.hasEventListener = true;
}
sessionList.innerHTML = '';
각 세션을 DOM 요소 sessionElement로 생성하였다. 이때 해당 요소가 버튼으로 기능하도록 role="button" 속성을, tab버튼으로 액세스 가능하도록 tabindex="0" 속성을 명시하였다.
sessions.forEach(session => {
const sessionElement = document.createElement('div');
sessionElement.className = 'session-item';
sessionElement.setAttribute('data-session-id', session.SessionId);
sessionElement.setAttribute('role', 'button');
sessionElement.setAttribute('tabindex', '0');
세션명을 표시할 요소와 삭제 버튼을 생성하여 sessionElement에 하위 요소로 삽입한다.
const sessionName = document.createElement('span');
sessionName.textContent = session.SessionName;
sessionName.className = 'session-name';
const deleteButton = document.createElement('button');
deleteButton.className = 'delete-button';
deleteButton.setAttribute('data-action', 'delete');
deleteButton.setAttribute('aria-label', 'Delete session');
sessionElement.appendChild(sessionName);
sessionElement.appendChild(deleteButton);
세션 ID가 현재 세션 ID와 동일한 경우, 즉 활성화된 세션의 경우 active 클래스를 달아서 현재 활성화된 세션임을 알 수 있도록 한다.
if (session.SessionId === currentSessionId) {
sessionElement.classList.add('active');
}
sessionList.appendChild(sessionElement);
});
}
세션 버튼 클릭과 관련된 이벤트들을 따로 정리한 함수.
우선 각 세션 아이템 클릭과 관련하여, 클릭이벤트 발생 시 가장 가까운 session item을 탐색, child 요소를 클릭하더라도 세션 전체가 클릭되도록 하였다.
function handleSessionClick(event) {
const sessionElement = event.target.closest('.session-item');
if (!sessionElement) return;
다만 삭제 버튼의 경우 stopPropagation을 통해 클릭 시 세션이 선택되는 것을 막았다. 이후 해당 세션 ID를 불러온 뒤 삭제 모달을 표시하도록 하였다 (최종 삭제는 모달에서 삭제 버튼을 누른 후에 deletesession 함수에서 담당)
const deleteButton = event.target.closest('[data-action="delete"]');
if (deleteButton) {
event.stopPropagation();
const sessionId = sessionElement.getAttribute('data-session-id');
showDeleteModal(sessionId);
return;
}
세션 클릭 시 sessionId를 가져온다. 이때 세션이 로딩 중일 경우 중복 클릭을 막는다.
세션이 로드되고 나면 로딩 class를 제거한다.
const sessionId = sessionElement.getAttribute('data-session-id');
if (sessionId && sessionId !== currentSessionId) {
if (sessionElement.classList.contains('loading')) return;
sessionElement.classList.add('loading');
loadSession(sessionId).finally(() => {
sessionElement.classList.remove('loading');
});
}
}
특정 세션을 DynamoDB에서 불러와 로드한다.
만약 불러와야 하는 세션이 현재 세션과 동일하다면 함수를 종료한다.
async function loadSession(sessionId) {
if (currentSessionId === sessionId) {
return;
}
먼저 이전 세션의 active 클래스를 제거하고 연결을 끊는다.
const previousActive = document.querySelector('.session-item.active');
if (previousActive) {
previousActive.classList.remove('active');
}
if (currentSessionId) {
await disconnectCurrentSession();
}
그 후 현재 세션의 ID를 currentsessionId로 새롭게 지정하고, API 호출 전 토큰 유효성을 확인한다.
currentSessionId = sessionId;
try {
const idToken = localStorage.getItem('auth_token');
if (!idToken) {
console.error('No auth token available');
return;
}
await ensureValidToken();
REST API로 백엔드로부터 세션을 호출한다. 연결된 함수는 tarotchat_getsessionmessage.
const response = await fetch(`${config.restEndpoint}/sessions/${sessionId}?userId=${userId}`, {
headers: {
'Authorization': `Bearer ${idToken}`
}
});
const messages = await response.json();
응답 처리 부분. Websocket API를 연결한다. 연결에 성공하면 해당 session item에 active 클래스를 추가한다.
if (!Array.isArray(messages)) {
console.error('Unexpected response format');
return; // 메시지가 비어 있으면 함수를 종료
}
await connectWebSocket();
const newActive = document.querySelector(`.session-item[data-session-id="${sessionId}"]`);
if (newActive) {
newActive.classList.add('active');
}
} catch (error) {
console.error('Error loading session:', error);
// 에러 발생 시 롤백
currentSessionId = null;
}
}
새 대화를 시작하기 위한 함수를 만들었다.
먼저 현재 연결된 ws가 있다면 이를 닫는다. disconnectcurrentsession 함수는 아래에서 다로 설명하겠다.
async function startNewChat() {
if (socket) {
socket.close();
}
if (currentSessionId) {
await disconnectCurrentSession();
}
현재 세션 ID를 null값으로 바꾸고, 이번에도 토큰 유효성을 확인한다.
currentSessionId = null;
try {
const idToken = localStorage.getItem('auth_token'); // idToken 가져오기
if (!idToken) {
throw new Error('No auth token available');
}
await ensureValidToken();
rest API로 요청을 보내고 응답을 파싱한다. 연결된 함수는 tarotchat_newsession
const response = await fetch(`${config.restEndpoint}/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${idToken}`
},
credentials: 'include',
body: JSON.stringify({ userId: userId })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
현재 세션의 sessionID를 currentSessionId로 받고 세션 목록을 새로고침한다.
if (result.sessionId) {
currentSessionId = result.sessionId;
await fetchSessions(userId);
Websocket 연결을 수행한다.
console.log('Attempting WebSocket connection...');
await connectWebSocket();
console.log('WebSocket connection established');
} else {
throw new Error('Session ID not received from server');
}
} catch (error) {
console.error('Error in startNewChat:', error);
}
}
현재 세션과 소켓 연결을 끊는 단순한 코드이다.
async function disconnectCurrentSession() {
if (socket) {
socket.close();
반면 소켓 연결은 아래 함수가 담당한다.
먼저 localstorage에서 accessToken을 가져온다.
async function connectWebSocket() {
const accessToken = localStorage.getItem('access_token');
if (!accessToken || !userId || !currentSessionId) {
console.error('Missing required parameters for WebSocket connection');
throw new Error('WebSocket connection failed: Missing parameters');
}
이어서 토큰 정보와 사용자 ID, 세션 ID 등 호출에 필요한 정보를 모두 URL에 담아 socket 개체를 정의한다.
const wsUrl = `${config.wsEndpoint}?token=${accessToken}&userId=${userId}&sessionId=${currentSessionId}`;
if (socket) {
socket.close();
}
socket = new WebSocket(wsUrl);
리턴값에서는 웹소켓 상태에 따라 promise 개체를 리턴
return new Promise((resolve, reject) => {
socket.onopen = function() {
resolve();
};
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
handleIncomingMessage(data);
};
socket.onclose = function(event) {
console.log('WebSocket closed:', event.code, event.reason);
};
socket.onerror = function(error) {
console.error('WebSocket error:', error);
reject(error);
};
5초 타임아웃을 설정하여 에러 발생 시 과도한 대기를 일으키지 않고 에러 반환시킨다.
const timeout = setTimeout(() => {
socket.close();
reject(new Error('WebSocket connection timeout'));
}, 5000);
socket.onopen = function() {
clearTimeout(timeout);
console.log('WebSocket connected for session:', currentSessionId);
resolve();
};
});
}