{item}
+후속 API와 상세 UI를 이 페이지 폴더 안에서 확장합니다.
+diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9fa4f28 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +VITE_API_BASE_URL=http://localhost:4400/api +VITE_LAUNCHER_API_BASE_URL=http://localhost:4200/api +VITE_ADS_IDE_AUTH_URL=http://localhost:4100 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..225489c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.env +.env.local +*.tsbuildinfo diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2acf836 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine AS builder +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 7892b07..3a22c2f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ -# lms-adm-fe +# LMS Instructor Admin -LMS 강사 웹 \ No newline at end of file +React 기반 강사용 LMS 관리 프론트엔드입니다. 학생용 Electron 앱(`/home/chyoo/tmp/lms`)과 같은 백엔드를 바라보는 웹 관리자 콘솔로 분리하는 구조를 기준으로 만들었습니다. + +## Scripts + +```bash +npm install +npm run dev +npm run build +``` + +## Environment + +`.env.example`을 참고해 API 서버 주소를 설정합니다. + +```bash +VITE_API_BASE_URL=http://localhost:4400/api +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..2c77dfe --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + +
+ + ++ 보안을 위해 현재 로그인 세션을 사용할 수 없습니다. 새로고침 후 다시 로그인해 주세요. +
+ +{message}
+ {detail ?{thread.studentName}
+ {thread.status === 'WAITING_INSTRUCTOR' ?+ {[thread.groupName, thread.week ? `${thread.week}주차` : null].filter(Boolean).join(' · ') || '강사 문의'} +
+{sendError}
: null} +{message.content}
+{title}
+{description}
+{meta.description}
+{totalLabel}
+ +{toast.title}
+ {toast.message ?{toast.message}
: null} +알림
+ 안읽음 {unreadCount}건 +표시할 알림이 없습니다.
+{displayName}
+메뉴별 라우팅과 페이지 경계가 분리되어 있습니다.
+{item}
+후속 API와 상세 UI를 이 페이지 폴더 안에서 확장합니다.
+강사가 선택한 주차가 학생 앱에서 자동 선택됩니다.
+등록된 커리큘럼이 없습니다.
+{selectedWeek ? `${selectedWeek.week}주차` : '-'}
++ 강의 진행을 적용하려면 먼저 그룹을 생성해 주세요. +
+학습단계가 없습니다.
+ )} +체크된 모듈만 학생 앱의 모듈 리스트에 표시됩니다.
+등록된 모듈이 없습니다.
+ )} +시각화 화면을 사용할 플랫폼을 선택하세요.
+등록된 시각화 플랫폼이 없습니다.
+ )} +학생 앱의 예제 코드 화면에 표시할 주차별 파일을 업로드합니다.
+{example.title}
++ {example.fileName} + {example.fileSizeBytes ? ` · ${formatFileSize(Number(example.fileSizeBytes))}` : ''} +
+등록된 예제 코드가 없습니다.
+ )} +학습단계가 없습니다.
+ )} +실습 내용이 없습니다.
+ )} +{reference.title}
++ {reference.fileName} + {reference.fileSizeBytes ? ` · ${formatFileSize(Number(reference.fileSizeBytes))}` : ''} +
+등록된 참고자료가 없습니다.
+ )} +{selectedWeek.title}
+추가 주차만 삭제할 수 있습니다.
+{selectedWeek.title}
+현재 단계: {activeLearningStep ?? '미선택'}
+ + ) : null} + confirmLabel="완료처리" + loading={saving} + icon={CheckCircle2} + onCancel={() => setCompleteConfirmOpen(false)} + onConfirm={() => { + setCompleteConfirmOpen(false); + void handleCompleteWeek(); + }} + /> + + ); +} + +function SummaryCard({ label, value, icon: Icon }: { label: string; value: string; icon: LucideIcon }) { + return ( +{label}
+{value}
+{error}
: null} + +하드웨어 모니터링
+그룹 구성, 플랫폼 배정, 진행률 조회
+강사 지원이 필요한 질문
++ {platform.user ? `사용자: ${platform.user} 배터리: ${platform.battery}% 속도: ${platform.speed}` : `배터리: ${platform.battery}%`} +
+{group.members.length}명 | {group.week}
+{formatRelativeTime(thread.lastMessageAt)}
+{thread.lastMessage}
+ + +{label}
+{loading ? '-' : value}
+{message}
+그룹 구성, 플랫폼 배정, 진행률 조회
+{error}
+번호
+ {filteredGroups.length - ((page - 1) * pageSize + index)} ++ {group.members.length}명 | {group.week} +
+그룹원
+플랫폼
+ {group.platform} ++ 완료 {group.completedMissions} · 활동 {group.activeUsers} +
+조회된 그룹이 없습니다.
+그룹 정보와 배정 인원을 설정해 주세요.
+{formError}
: null} +그룹원은 모두 미배정 상태가 됩니다.
+{deleteTarget.name}
+{deleteTarget.members.length}명 | {deleteTarget.platform}
+그룹만 삭제되고 수강생 계정과 수강 등록은 유지됩니다.
+{label}
+{value}
+Learning Management System
++ 실제 하드웨어와 시뮬레이션을 활용한 차세대 학습 관리 시스템 +
+ +© 2026. All rights reserved.
+이메일과 비밀번호를 입력해주세요.
+접근 권한이 없는 계정은 관리자에게 문의하세요.
+배터리, 가동 시간, 할당 그룹, 오류 상태 조회
+{error}
+번호
+ {filteredPlatforms.length - ((page - 1) * pageSize + index)} +{platform.type}
+연결
+ {platform.agent ? ( +상태
+ {platform.status} +할당
+등록된 플랫폼이 없습니다.
+추가할 플랫폼 유형을 선택해 주세요.
+{assignmentTarget.name}을 배정할 그룹을 선택해 주세요.
+{assignmentTarget.name}
++ 현재 배정: {assignmentTarget.group ?? '미배정'} · 상태: {assignmentTarget.status} +
+센서 드라이버 및 위치 설정
++ 제어 시간이 만료되면 자동으로 제어권이 반납되며, 대기 중인 다음 학생에게 제어권이 할당됩니다. +
+{deleteTarget.name}
+{deleteTarget.type}
+{forceDeleteTarget.platform.name}
++ 사용 중: {forceDeleteTarget.session.holderName ?? forceDeleteTarget.session.holderEmail ?? '알 수 없음'} +
++ 종료 예정: {new Date(forceDeleteTarget.session.endsAt).toLocaleString('ko-KR')} +
+ + ) : null} + confirmLabel="강제 종료 후 삭제" + danger + icon={ShieldAlert} + loading={Boolean(forceDeleteTarget && deletingPlatformId === forceDeleteTarget.platform.id)} + onCancel={() => { + if (!deletingPlatformId) setForceDeleteTarget(null); + }} + onConfirm={() => { + if (forceDeleteTarget) void handleDeletePlatform(forceDeleteTarget.platform, true); + }} + /> + +{forceAssignmentTarget.platform.name}
++ 배정 대상: {forceAssignmentTarget.group.name} +
++ 사용 중: {forceAssignmentTarget.session.holderName ?? forceAssignmentTarget.session.holderEmail ?? '알 수 없음'} +
++ 종료 예정: {new Date(forceAssignmentTarget.session.endsAt).toLocaleString('ko-KR')} +
+ + ) : null} + confirmLabel="강제 종료 후 배정" + danger + icon={ShieldAlert} + loading={Boolean(forceAssignmentTarget && assigningPlatformId === forceAssignmentTarget.platform.id)} + onCancel={() => { + if (!assigningPlatformId) setForceAssignmentTarget(null); + }} + onConfirm={() => { + if (forceAssignmentTarget) void handleAssignPlatform(forceAssignmentTarget.platform, forceAssignmentTarget.group, true); + }} + /> + + ); +} + +function PlatformMeta({ icon: Icon, label, value, danger = false }: { icon: LucideIcon; label: string; value: string; danger?: boolean }) { + return ( +{value}
+{label}
+{value}
+{title}
+{description}
+{roleLabel}
+공통 인증 서버에서 전달된 로그인 정보입니다.
+{label}
+{value}
+{label}
+{value}
+그룹, 주차, 진도, 커밋, 이슈, 최근 활동 조회
+이름
+현재 소속의 수강 목록에서 제외합니다.
+{removeTarget.name}
+{removeTarget.email}
+{message}
+{label}
+ {children} +{value}
+{student.name} · {student.email}
+{issue.title}
+{issue.detail}
+{label}
+{value}
+{item.course.title}
+{item.week.week}주차 {item.week.title} · {item.student.email}
+제출 내역
+과정 총점
++ {scoreMessage.text} +
+ ) : null} ++ 최종 {item.finalFiles.length}개 파일 · 제출 {item.commitCount}회 +
+원본 대비 최종 변경사항
+마지막 제출 기준 · {formatDate(item.submittedAt)}
+{commit.message}
+{formatDate(commit.date)}
+파일 목록 없음
+ )} +{modal.subtitle}
+
+ {line.content || ' '}
+
+