platform/lms-adm-fe release promotion snapshot

This commit is contained in:
2026-07-07 09:15:47 +00:00
parent 1bf81f86bb
commit d029bdeda7
59 changed files with 12096 additions and 2 deletions
+129
View File
@@ -0,0 +1,129 @@
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,
};
}