add commits..

This commit is contained in:
2026-08-12 14:46:19 +09:00
parent 4869dbe4c1
commit 2a3e2ead65
2 changed files with 76 additions and 1 deletions
+59 -1
View File
@@ -3,7 +3,7 @@ import { Navigate, useLocation, useNavigate } from 'react-router-dom';
import { ToastProvider } from './components/ToastProvider'; import { ToastProvider } from './components/ToastProvider';
import { InstructorLayout } from './layouts/InstructorLayout'; import { InstructorLayout } from './layouts/InstructorLayout';
import { clearStoredSession, loadStoredSession, logoutInstructor, refreshCurrentUser, refreshInstructorSession } from './lib/auth'; 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 { AnalyticsPage } from './pages/analytics/AnalyticsPage';
import { CurriculumPage } from './pages/curriculum/CurriculumPage'; import { CurriculumPage } from './pages/curriculum/CurriculumPage';
import { DashboardPage } from './pages/dashboard/DashboardPage'; import { DashboardPage } from './pages/dashboard/DashboardPage';
@@ -22,6 +22,10 @@ function App() {
const [courses, setCourses] = useState<InstructorCourse[]>([]); const [courses, setCourses] = useState<InstructorCourse[]>([]);
const [selectedCourseId, setSelectedCourseId] = useState(() => window.localStorage.getItem('lms-admin-selected-course-id') ?? ''); const [selectedCourseId, setSelectedCourseId] = useState(() => window.localStorage.getItem('lms-admin-selected-course-id') ?? '');
const [sessionExpired, setSessionExpired] = useState(false); const [sessionExpired, setSessionExpired] = useState(false);
const [initialCourseOpen, setInitialCourseOpen] = useState(false);
const [initialCourseTitle, setInitialCourseTitle] = useState('');
const [initialCourseError, setInitialCourseError] = useState<string | null>(null);
const [creatingInitialCourse, setCreatingInitialCourse] = useState(false);
const [chatThreadOpenRequest, setChatThreadOpenRequest] = useState<{ threadId: string; requestId: number } | null>(null); const [chatThreadOpenRequest, setChatThreadOpenRequest] = useState<{ threadId: string; requestId: number } | null>(null);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -96,6 +100,7 @@ function App() {
.then((payload) => { .then((payload) => {
if (cancelled) return; if (cancelled) return;
setCourses(payload.courses); setCourses(payload.courses);
setInitialCourseOpen(payload.courses.length === 0);
setSelectedCourseId((current) => { setSelectedCourseId((current) => {
const next = payload.courses.some((course) => course.id === current) const next = payload.courses.some((course) => course.id === current)
? current ? current
@@ -117,6 +122,28 @@ function App() {
if (courseId) window.localStorage.setItem('lms-admin-selected-course-id', courseId); 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(() => { useEffect(() => {
if (!session?.accessToken || !session.refreshToken) return; if (!session?.accessToken || !session.refreshToken) return;
@@ -203,6 +230,37 @@ function App() {
/> />
</div> </div>
</InstructorLayout> </InstructorLayout>
{initialCourseOpen && (
<div className="fixed inset-0 z-[100] grid place-items-center bg-[#111827]/35 px-5" role="dialog" aria-modal="true" aria-labelledby="initial-course-title">
<form
className="w-full max-w-[400px] rounded-lg border border-[#d9e0ea] bg-white p-6 shadow-[0_22px_70px_rgba(15,23,42,0.22)]"
onSubmit={(event) => {
event.preventDefault();
void handleCreateInitialCourse();
}}
>
<h1 id="initial-course-title" className="text-[18px] font-black text-[#111827]">강좌명을 입력하세요</h1>
<p className="mt-2 text-[13px] leading-relaxed text-[#64748b]">NAV2 기반 자율주행 교육 과정 구성을 새 강좌로 복제합니다.</p>
<input
autoFocus
value={initialCourseTitle}
maxLength={120}
onChange={(event) => 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 && <p className="mt-2 text-[12px] font-semibold text-[#dc2626]">{initialCourseError}</p>}
<button
type="submit"
disabled={creatingInitialCourse}
className="mt-5 h-10 w-full rounded-md bg-[#2563eb] text-[14px] font-black text-white transition hover:bg-[#1d4ed8] disabled:cursor-not-allowed disabled:opacity-60"
>
{creatingInitialCourse ? '생성 중...' : '확인'}
</button>
</form>
</div>
)}
</ToastProvider> </ToastProvider>
); );
} }
+17
View File
@@ -24,3 +24,20 @@ export async function listInstructorCourses(accessToken: string): Promise<{ cour
if (!response.ok) throw new Error(`강좌 목록을 불러오지 못했습니다. (${response.status})`); if (!response.ok) throw new Error(`강좌 목록을 불러오지 못했습니다. (${response.status})`);
return response.json() as Promise<{ courses: InstructorCourse[] }>; 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 }>;
}