add commits..
This commit is contained in:
+59
-1
@@ -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<InstructorCourse[]>([]);
|
||||
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<string | null>(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() {
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user