export type ChatSenderType = 'STUDENT' | 'AI' | 'INSTRUCTOR' | 'SYSTEM' | string; export type InstructorChatMessage = { id: string; senderType: ChatSenderType; senderName?: string; senderAvatarUrl?: string | null; content: string; createdAt?: string; }; export type InstructorChatThread = { id: string; title: string; studentName: string; studentAvatarUrl?: string | null; groupName?: string; week?: number | string; status: string; lastMessage: string; lastMessageAt?: string; messages: InstructorChatMessage[]; }; const CHAT_API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? 'https://workspace.autonomystudio.net/lms-api/api').replace(/\/+$/, ''); const AUTH_BASE_URL = (import.meta.env.VITE_ATNMY_AUTH_URL ?? import.meta.env.VITE_ADS_IDE_AUTH_URL ?? 'https://auth.autonomystudio.net').replace(/\/+$/, ''); export const CHAT_SOCKET_BASE_URL = CHAT_API_BASE_URL.replace(/\/api$/, ''); const CHAT_SOCKET_URL = new URL(CHAT_SOCKET_BASE_URL || '/', window.location.origin); const CHAT_SOCKET_PREFIX = CHAT_SOCKET_URL.pathname.replace(/\/+$/, ''); export const CHAT_SOCKET_ORIGIN = CHAT_SOCKET_URL.origin; export const CHAT_SOCKET_PATH = `${CHAT_SOCKET_PREFIX}/socket.io`; function normalizeAvatarUrl(value: unknown): string | null { if (typeof value !== 'string' || !value.trim()) return null; try { const url = new URL(value); if (url.hostname === 'studio-auth-be.studio-system.svc.cluster.local') { return `${AUTH_BASE_URL}${url.pathname}${url.search}`; } return value; } catch { return value; } } type ChatThreadsPayload = { threads?: unknown[]; data?: unknown[]; }; export async function listInstructorChatThreads(accessToken: string, courseId: string, week?: number): Promise { const params = new URLSearchParams(); if (courseId) params.set('courseId', courseId); if (week) params.set('week', String(week)); const queryString = params.toString(); const response = await fetch(`${CHAT_API_BASE_URL}/chat/instructor/threads${queryString ? `?${queryString}` : ''}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new Error(`강사 채팅 목록을 불러오지 못했습니다. (${response.status})`); } const payload = (await response.json()) as ChatThreadsPayload | unknown[]; const items = Array.isArray(payload) ? payload : Array.isArray(payload?.threads) ? payload.threads : Array.isArray(payload?.data) ? payload.data : []; return items.map(normalizeThread).filter((thread): thread is InstructorChatThread => Boolean(thread)); } export async function getInstructorChatThread(accessToken: string, threadId: string): Promise { const response = await fetch(`${CHAT_API_BASE_URL}/chat/instructor/threads/${threadId}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new Error(`강사 채팅방을 불러오지 못했습니다. (${response.status})`); } const payload = await response.json(); const thread = normalizeThread(payload?.thread ?? payload); if (!thread) throw new Error('강사 채팅방 응답이 올바르지 않습니다.'); return thread; } export async function createInstructorChatMessage(accessToken: string, threadId: string, content: string): Promise { const response = await fetch(`${CHAT_API_BASE_URL}/chat/instructor/threads/${threadId}/messages`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ content }), }); if (!response.ok) { throw new Error(`강사 답변을 저장하지 못했습니다. (${response.status})`); } const payload = await response.json(); const thread = normalizeThread(payload?.thread ?? payload); if (!thread) throw new Error('강사 답변 응답이 올바르지 않습니다.'); return thread; } function normalizeThread(raw: unknown): InstructorChatThread | null { if (!raw || typeof raw !== 'object') return null; const item = raw as Record; const messages = Array.isArray(item.messages) ? item.messages.map(normalizeMessage).filter((message): message is InstructorChatMessage => Boolean(message)) : []; const lastMessageFromList = messages.length > 0 ? messages[messages.length - 1]?.content : undefined; const rawLastMessage = item.lastMessage?.content ?? item.lastMessageContent ?? item.lastMessage; const studentName = item.student?.name ?? item.studentName ?? item.student?.email ?? item.studentEmail ?? item.studentId ?? '수강생'; const title = item.title ?? item.threadTitle ?? studentName; const weekValue = typeof item.week === 'object' && item.week ? item.week.week : item.week; return { id: String(item.id ?? item.threadId ?? title), title: String(title), studentName: String(studentName), studentAvatarUrl: normalizeAvatarUrl(item.studentAvatarUrl), groupName: item.group?.name ?? item.groupName ?? item.metadata?.groupName, week: weekValue ?? item.weekId ?? item.metadata?.week, status: String(item.status ?? 'WAITING_INSTRUCTOR'), lastMessage: String(rawLastMessage ?? lastMessageFromList ?? '저장된 문의 내용을 확인해 주세요.'), lastMessageAt: item.lastMessageAt ?? item.updatedAt ?? item.createdAt, messages, }; } function normalizeMessage(raw: unknown): InstructorChatMessage | null { if (!raw || typeof raw !== 'object') return null; const item = raw as Record; return { id: String(item.id ?? `${item.senderType ?? 'MESSAGE'}-${item.createdAt ?? item.content ?? 'unknown'}`), senderType: String(item.senderType ?? 'STUDENT'), senderName: item.senderName ?? item.sender?.name, senderAvatarUrl: normalizeAvatarUrl(item.senderAvatarUrl), content: String(item.content ?? ''), createdAt: item.createdAt, }; }