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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }>;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user