130 lines
5.0 KiB
TypeScript
130 lines
5.0 KiB
TypeScript
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<InstructorChatThread[]> {
|
|
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<InstructorChatThread> {
|
|
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<InstructorChatThread> {
|
|
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<string, any>;
|
|
|
|
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<string, any>;
|
|
|
|
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,
|
|
};
|
|
}
|