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 ?? 'http://localhost:4400/api').replace(/\/+$/, ''); export const CHAT_SOCKET_BASE_URL = CHAT_API_BASE_URL.replace(/\/api$/, ''); type ChatThreadsPayload = { threads?: unknown[]; data?: unknown[]; }; export async function listInstructorChatThreads(accessToken: string, week?: number): Promise { const params = new URLSearchParams(); 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: typeof item.studentAvatarUrl === 'string' ? item.studentAvatarUrl : null, 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: typeof item.senderAvatarUrl === 'string' ? item.senderAvatarUrl : null, content: String(item.content ?? ''), createdAt: item.createdAt, }; }