diff --git a/src/App.tsx b/src/App.tsx index c070612..f712111 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,7 @@ import { Navigate, useLocation, useNavigate } from 'react-router-dom'; import { ToastProvider } from './components/ToastProvider'; import { InstructorLayout } from './layouts/InstructorLayout'; import { clearStoredSession, loadStoredSession, logoutInstructor, refreshCurrentUser, refreshInstructorSession } from './lib/auth'; -import { listInstructorCourses, type InstructorCourse } from './lib/courses'; +import { createInitialInstructorCourse, listInstructorCourses, type InstructorCourse } from './lib/courses'; import { AnalyticsPage } from './pages/analytics/AnalyticsPage'; import { CurriculumPage } from './pages/curriculum/CurriculumPage'; import { DashboardPage } from './pages/dashboard/DashboardPage'; @@ -22,6 +22,10 @@ function App() { const [courses, setCourses] = useState([]); const [selectedCourseId, setSelectedCourseId] = useState(() => window.localStorage.getItem('lms-admin-selected-course-id') ?? ''); const [sessionExpired, setSessionExpired] = useState(false); + const [initialCourseOpen, setInitialCourseOpen] = useState(false); + const [initialCourseTitle, setInitialCourseTitle] = useState(''); + const [initialCourseError, setInitialCourseError] = useState(null); + const [creatingInitialCourse, setCreatingInitialCourse] = useState(false); const [chatThreadOpenRequest, setChatThreadOpenRequest] = useState<{ threadId: string; requestId: number } | null>(null); const location = useLocation(); const navigate = useNavigate(); @@ -96,6 +100,7 @@ function App() { .then((payload) => { if (cancelled) return; setCourses(payload.courses); + setInitialCourseOpen(payload.courses.length === 0); setSelectedCourseId((current) => { const next = payload.courses.some((course) => course.id === current) ? current @@ -117,6 +122,28 @@ function App() { if (courseId) window.localStorage.setItem('lms-admin-selected-course-id', courseId); }; + const handleCreateInitialCourse = async () => { + const title = initialCourseTitle.trim(); + if (!title) { + setInitialCourseError('강좌명을 입력해 주세요.'); + return; + } + + setCreatingInitialCourse(true); + setInitialCourseError(null); + try { + const { course } = await createInitialInstructorCourse(session!.accessToken, title); + setCourses([course]); + setSelectedCourseId(course.id); + window.localStorage.setItem('lms-admin-selected-course-id', course.id); + setInitialCourseOpen(false); + } catch (error) { + setInitialCourseError(error instanceof Error ? error.message : '강좌를 생성하지 못했습니다.'); + } finally { + setCreatingInitialCourse(false); + } + }; + useEffect(() => { if (!session?.accessToken || !session.refreshToken) return; @@ -203,6 +230,37 @@ function App() { /> + {initialCourseOpen && ( +
+
{ + event.preventDefault(); + void handleCreateInitialCourse(); + }} + > +

강좌명을 입력하세요

+

NAV2 기반 자율주행 교육 과정 구성을 새 강좌로 복제합니다.

+ setInitialCourseTitle(event.target.value)} + className="mt-5 h-10 w-full rounded-md border border-[#cbd5e1] px-3 text-[14px] font-semibold text-[#111827] outline-none focus:border-[#2563eb]" + placeholder="예: 2026 하반기 자율주행 교육" + disabled={creatingInitialCourse} + /> + {initialCourseError &&

{initialCourseError}

} + +
+
+ )} ); } diff --git a/src/components/InstructorChatPanel.tsx b/src/components/InstructorChatPanel.tsx index 664cd3d..fc55714 100644 --- a/src/components/InstructorChatPanel.tsx +++ b/src/components/InstructorChatPanel.tsx @@ -15,6 +15,7 @@ import type { AuthSession } from '../types/auth'; type InstructorChatPanelProps = { session: AuthSession; + courseId: string; open: boolean; initialThreadId?: string | null; initialThreadRequestId?: number; @@ -30,7 +31,7 @@ type ChatNotificationPayload = { createdAt: string; }; -export function InstructorChatPanel({ session, open, initialThreadId = null, initialThreadRequestId = 0, onClose }: InstructorChatPanelProps) { +export function InstructorChatPanel({ session, courseId, open, initialThreadId = null, initialThreadRequestId = 0, onClose }: InstructorChatPanelProps) { const [threads, setThreads] = useState([]); const [selectedThreadId, setSelectedThreadId] = useState(null); const [loading, setLoading] = useState(false); @@ -45,7 +46,7 @@ export function InstructorChatPanel({ session, open, initialThreadId = null, ini if (showLoading) setLoading(true); setError(null); - const request = listInstructorChatThreads(session.accessToken) + const request = listInstructorChatThreads(session.accessToken, courseId) .then((items) => { if (!cancelled) setThreads(items); }) @@ -91,7 +92,7 @@ export function InstructorChatPanel({ session, open, initialThreadId = null, ini return () => { refresh.cancel(); }; - }, [open, session.accessToken]); + }, [courseId, open, session.accessToken]); useEffect(() => { if (!open || !initialThreadId) return; @@ -124,7 +125,7 @@ export function InstructorChatPanel({ session, open, initialThreadId = null, ini socket.off('chat.message.created', handleMessageCreated); socket.disconnect(); }; - }, [open, selectedThreadId, session.accessToken]); + }, [courseId, open, selectedThreadId, session.accessToken]); useEffect(() => { if (!open) setSelectedThreadId(null); diff --git a/src/layouts/InstructorLayout.tsx b/src/layouts/InstructorLayout.tsx index 82c35d1..c4bea5c 100644 --- a/src/layouts/InstructorLayout.tsx +++ b/src/layouts/InstructorLayout.tsx @@ -505,6 +505,7 @@ export function InstructorLayout({ session, route, onNavigate, onLogout, courses { +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(); diff --git a/src/lib/courses.ts b/src/lib/courses.ts index 4642d42..2740847 100644 --- a/src/lib/courses.ts +++ b/src/lib/courses.ts @@ -24,3 +24,20 @@ export async function listInstructorCourses(accessToken: string): Promise<{ cour if (!response.ok) throw new Error(`강좌 목록을 불러오지 못했습니다. (${response.status})`); return response.json() as Promise<{ courses: InstructorCourse[] }>; } + +export async function createInitialInstructorCourse(accessToken: string, title: string): Promise<{ course: InstructorCourse }> { + const response = await fetch(`${LMS_API_BASE_URL}/instructor/courses/initial`, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ title }), + }); + + if (!response.ok) { + const payload = await response.json().catch(() => null) as { message?: string } | null; + throw new Error(payload?.message || `강좌를 생성하지 못했습니다. (${response.status})`); + } + return response.json() as Promise<{ course: InstructorCourse }>; +} diff --git a/src/pages/curriculum/CurriculumPage.tsx b/src/pages/curriculum/CurriculumPage.tsx index 5b9e290..4a45c1c 100644 --- a/src/pages/curriculum/CurriculumPage.tsx +++ b/src/pages/curriculum/CurriculumPage.tsx @@ -26,10 +26,6 @@ function isPlatformEnvironment(config: unknown): boolean { return target === 'remote-jetson' || target === 'platform'; } -function environmentStorageKey(profile: Pick): 'local' | 'platform' { - return profile.profileType.toUpperCase() === 'PLATFORM' || isPlatformEnvironment(profile.config) ? 'platform' : 'local'; -} - function parseModuleParameter(parameter: string): ModuleParameterDraft { const separatorIndex = parameter.indexOf(':='); if (separatorIndex < 0) return { key: parameter.trim(), value: '' }; @@ -1584,3 +1580,6 @@ function formatFileSize(bytes: number): string { if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } +function environmentStorageKey(profile: Pick): 'local' | 'platform' { + return profile.profileType.toUpperCase() === 'PLATFORM' || isPlatformEnvironment(profile.config) ? 'platform' : 'local'; +} diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index 1657677..1752691 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -40,7 +40,7 @@ export function DashboardPage({ session, selectedCourseId, onNavigate, onOpenCha Promise.allSettled([ listInstructorPlatforms(session.accessToken), listInstructorGroups(session.accessToken, selectedCourseId || null), - listInstructorChatThreads(session.accessToken), + listInstructorChatThreads(session.accessToken, selectedCourseId), ]) .then(([platformResult, groupResult, chatResult]) => { if (cancelled) return;