add guide..
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { BookOpenText, X } from 'lucide-react';
|
||||||
|
|
||||||
|
type GuideLesson = {
|
||||||
|
code: string;
|
||||||
|
title: string;
|
||||||
|
guidance: string;
|
||||||
|
tools: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GuideDay = {
|
||||||
|
title: string;
|
||||||
|
lessons: GuideLesson[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const GUIDE_DAYS: GuideDay[] = [
|
||||||
|
{
|
||||||
|
title: 'DAY 1. ROS2 & Simulation',
|
||||||
|
lessons: [
|
||||||
|
{
|
||||||
|
code: '01',
|
||||||
|
title: 'ROS2 기초',
|
||||||
|
guidance: 'Gazebo와 TurtleBot3 또는 MK-mini를 실행하고 /odom, /tf, /scan, /cmd_vel, /map의 역할을 설명합니다. Goal 지정은 Nav2 Action의 실제 사례로 사용하고, TF tree는 터미널에서 map-odom-base_link 관계를 확인합니다.',
|
||||||
|
tools: '모듈 리스트, ROS 연결상태, 터미널, 시각화, RViz2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '02',
|
||||||
|
title: 'Sensor Processing',
|
||||||
|
guidance: 'NAV2-Perception을 실행해 LiDAR /scan을 확인하고, 퍼셉션 입력 토픽과 TF, LiDAR 장착 위치 설정을 읽고 변경 전후를 비교합니다.',
|
||||||
|
tools: 'NAV2-Perception, 코드 편집, 파라미터 설정, 시각화 Sensor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '03',
|
||||||
|
title: 'Simulation 환경 구축 Part 1',
|
||||||
|
guidance: '기본 월드와 실내 환경을 비교한 뒤 커스텀 맵에서 벽과 장애물을 배치합니다. Gazebo 레이저가 환경을 어떻게 인식하는지 함께 확인합니다.',
|
||||||
|
tools: '맵 설정, 커스텀 맵 편집, NAV2-Gazebo, 시각화',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '04',
|
||||||
|
title: 'Simulation 환경 구축 Part 2',
|
||||||
|
guidance: '동일한 맵에서 TurtleBot3와 MK-mini를 번갈아 실행해 차동구동과 애커만 조향 차량의 차이를 비교합니다.',
|
||||||
|
tools: 'NAV2-TurtleBot3Burger, NAV2-MKMini, Gazebo, 시각화',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DAY 2-3. SLAM & Localization',
|
||||||
|
lessons: [
|
||||||
|
{
|
||||||
|
code: '05',
|
||||||
|
title: 'LiDAR SLAM',
|
||||||
|
guidance: '차량 또는 Gazebo, Perception, SLAM, 시각화 순서로 실행합니다. /scan, /odom, /map, /tf 관계를 확인하고 결과 맵을 저장합니다.',
|
||||||
|
tools: 'NAV2-SLAM, NAV2-Perception, 맵 저장, 시각화, ROS 연결상태',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '06',
|
||||||
|
title: 'LiDAR Localization',
|
||||||
|
guidance: '저장한 맵을 선택하고 Map, Localization, Planning, Control 조합으로 전환합니다. Pose 지정과 Goal 주행으로 map 기준 위치 보정을 확인합니다.',
|
||||||
|
tools: 'NAV2-Map, NAV2-Localization, NAV2-Planning, NAV2-Control, 시각화 Pose/Goal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '07',
|
||||||
|
title: 'Parameter 튜닝',
|
||||||
|
guidance: '모듈 파라미터와 설정 파일을 바꾸고 로그, 토픽, 주행 경로의 변화를 비교합니다. 수정 근거와 결과를 Git 변경사항으로 남기게 합니다.',
|
||||||
|
tools: '파라미터 설정, 코드 편집, 로그, 소스 제어',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DAY 4-5. Planning & Control',
|
||||||
|
lessons: [
|
||||||
|
{
|
||||||
|
code: '09',
|
||||||
|
title: 'Occupancy Map',
|
||||||
|
guidance: 'SLAM으로 생성된 2D 점유맵을 Nav2 장애물 인식 기준으로 설명합니다. 커스텀 월드, LiDAR 스캔, 저장된 맵을 비교해 맵 품질이 주행에 미치는 영향을 확인합니다.',
|
||||||
|
tools: '커스텀 맵, NAV2-SLAM, NAV2-Map, 시각화',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '10',
|
||||||
|
title: 'Planning',
|
||||||
|
guidance: 'Goal을 지정하고 NAV2-Planning 경로를 확인합니다. TurtleBot3와 MK-mini의 경로 차이, 장애물 회피, 전진 중심과 후진 허용 설정을 비교합니다.',
|
||||||
|
tools: 'NAV2-Planning, 시각화 Path/Goal, ROS 연결상태',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '11',
|
||||||
|
title: 'Control',
|
||||||
|
guidance: 'NAV2-Control의 /cmd_vel 흐름과 차량 조향을 연결해 설명합니다. 시뮬레이션으로 검증한 뒤 원격 플랫폼 제어권을 배정해 저속 실차 검증을 진행합니다.',
|
||||||
|
tools: 'NAV2-Control, NAV2-MKMini, 원격 플랫폼 워크스페이스, 시각화',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type InstructorLearningGuideModalProps = {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InstructorLearningGuideModal({ open, onClose }: InstructorLearningGuideModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [onClose, open]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[1000] grid place-items-center bg-[#0f172a]/50 px-4 py-6 backdrop-blur-[2px]"
|
||||||
|
role="presentation"
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (event.currentTarget === event.target) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section className="flex max-h-full w-full max-w-[940px] flex-col overflow-hidden rounded-xl border border-[#cbd5e1] bg-white shadow-[0_28px_80px_rgba(15,23,42,0.30)]" role="dialog" aria-modal="true" aria-labelledby="instructor-learning-guide-title">
|
||||||
|
<header className="flex items-start justify-between gap-4 border-b border-[#cbd5e1] px-6 py-4">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<span className="grid size-9 shrink-0 place-items-center rounded-md bg-[#eef2ff] text-[#4f46e5]">
|
||||||
|
<BookOpenText size={18} aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2 id="instructor-learning-guide-title" className="text-[18px] font-black text-[#111827]">LMS 학습 운영 가이드</h2>
|
||||||
|
<p className="mt-1 text-[12px] font-semibold text-[#64748b]">ROS2, 시뮬레이션, SLAM, Planning 및 Control 실습 운영 기준</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="가이드 닫기" title="닫기" className="grid size-8 shrink-0 cursor-pointer place-items-center rounded-md text-[#64748b] transition hover:bg-[#f1f5f9] hover:text-[#172033]">
|
||||||
|
<X size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="min-h-0 overflow-y-auto px-6 py-5">
|
||||||
|
<section className="border-b border-[#e2e8f0] pb-5">
|
||||||
|
<h3 className="text-[14px] font-black text-[#111827]">강사 운영 흐름</h3>
|
||||||
|
<ol className="mt-3 grid gap-2.5 text-[13px] font-semibold leading-6 text-[#475569] md:grid-cols-3">
|
||||||
|
<li className="rounded-md border border-[#dbe4f0] bg-[#f8fafc] px-3 py-2.5"><strong className="block text-[#1e293b]">1. 수업 전 구성</strong>커리큘럼, 학습단계, 사용 모듈, 기본 파라미터를 구성하고 그룹에 플랫폼을 배정합니다.</li>
|
||||||
|
<li className="rounded-md border border-[#dbe4f0] bg-[#f8fafc] px-3 py-2.5"><strong className="block text-[#1e293b]">2. 수업 중 검증</strong>모듈 실행, ROS 연결상태, 코드 수정, 시각화 순서로 원인을 확인합니다.</li>
|
||||||
|
<li className="rounded-md border border-[#dbe4f0] bg-[#f8fafc] px-3 py-2.5"><strong className="block text-[#1e293b]">3. 수업 후 기록</strong>저장 맵, 파라미터, Git 변경사항, 주행 결과를 제출 기준으로 남깁니다.</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-5 space-y-5">
|
||||||
|
{GUIDE_DAYS.map((day) => (
|
||||||
|
<section key={day.title}>
|
||||||
|
<h3 className="text-[14px] font-black text-[#111827]">{day.title}</h3>
|
||||||
|
<div className="mt-2 overflow-hidden rounded-lg border border-[#dbe4f0]">
|
||||||
|
{day.lessons.map((lesson, index) => (
|
||||||
|
<article key={lesson.code} className={['grid gap-3 px-4 py-3.5 md:grid-cols-[124px_minmax(0,1fr)_220px]', index ? 'border-t border-[#e2e8f0]' : ''].join(' ')}>
|
||||||
|
<div>
|
||||||
|
<span className="text-[11px] font-black text-[#4f46e5]">{lesson.code}</span>
|
||||||
|
<h4 className="mt-0.5 text-[13px] font-black text-[#1e293b]">{lesson.title}</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-[12px] font-semibold leading-5 text-[#475569]">{lesson.guidance}</p>
|
||||||
|
<p className="border-l border-[#e2e8f0] pl-3 text-[11px] font-bold leading-5 text-[#64748b]"><span className="block text-[10px] font-black text-[#94a3b8]">LMS 사용 기능</span>{lesson.tools}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="mt-5 rounded-lg border border-[#dbe4f0] bg-[#f8fafc] p-4">
|
||||||
|
<h3 className="text-[14px] font-black text-[#111827]">팀 프로젝트 운영</h3>
|
||||||
|
<p className="mt-2 text-[12px] font-semibold leading-6 text-[#475569]">3~4인 그룹마다 문제 상황과 맵을 정하고, 강사는 그룹별 원격 플랫폼 사용 시간을 배정합니다. 학생은 커스텀 맵에서 먼저 SLAM, Localization, Planning, Control을 검증한 뒤 원격 MK-mini에서 저속 검증합니다. 질의응답 강사 탭에는 주행 실패 원인, 파라미터 변경 근거, 토픽 상태 캡처를 남기게 합니다.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mt-4 rounded-lg border border-[#fde68a] bg-[#fffbeb] p-4">
|
||||||
|
<h3 className="text-[14px] font-black text-[#92400e]">현재 과정에서 제외할 항목</h3>
|
||||||
|
<ul className="mt-2 list-disc space-y-1 pl-5 text-[12px] font-semibold leading-5 text-[#78350f]">
|
||||||
|
<li>카메라 기반 SLAM과 딥러닝 객체 인식은 현재 정식 LMS 모듈 흐름으로 구성되어 있지 않습니다.</li>
|
||||||
|
<li>저장된 3D SLAM 결과를 로컬 Gazebo의 물리 충돌 월드로 재현하는 실습은 지원하지 않습니다.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="flex justify-end border-t border-[#cbd5e1] bg-[#f8fafc] px-6 py-3">
|
||||||
|
<button type="button" onClick={onClose} className="h-9 cursor-pointer rounded-md bg-[#1e293b] px-4 text-[12px] font-black text-white transition hover:bg-[#0f172a]">닫기</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { io } from 'socket.io-client';
|
|||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
import { ConfirmDialog } from '../components/ConfirmDialog';
|
import { ConfirmDialog } from '../components/ConfirmDialog';
|
||||||
import { InstructorChatPanel } from '../components/InstructorChatPanel';
|
import { InstructorChatPanel } from '../components/InstructorChatPanel';
|
||||||
|
import { InstructorLearningGuideModal } from '../components/InstructorLearningGuideModal';
|
||||||
import { useToast } from '../components/ToastProvider';
|
import { useToast } from '../components/ToastProvider';
|
||||||
import autonomyLogo from '../assets/icon.png';
|
import autonomyLogo from '../assets/icon.png';
|
||||||
import { useProfileAvatar } from '../hooks/useProfileAvatar';
|
import { useProfileAvatar } from '../hooks/useProfileAvatar';
|
||||||
@@ -78,6 +79,7 @@ export function InstructorLayout({ session, route, onNavigate, onLogout, courses
|
|||||||
const [notificationTab, setNotificationTab] = useState<'unread' | 'read'>('unread');
|
const [notificationTab, setNotificationTab] = useState<'unread' | 'read'>('unread');
|
||||||
const [notificationPage, setNotificationPage] = useState(1);
|
const [notificationPage, setNotificationPage] = useState(1);
|
||||||
const [chatOpen, setChatOpen] = useState(false);
|
const [chatOpen, setChatOpen] = useState(false);
|
||||||
|
const [guideOpen, setGuideOpen] = useState(false);
|
||||||
const [hasUnreadChat, setHasUnreadChat] = useState(false);
|
const [hasUnreadChat, setHasUnreadChat] = useState(false);
|
||||||
const [notifications, setNotifications] = useState<NotificationMessage[]>([]);
|
const [notifications, setNotifications] = useState<NotificationMessage[]>([]);
|
||||||
const profileRef = useRef<HTMLDivElement | null>(null);
|
const profileRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -503,6 +505,8 @@ export function InstructorLayout({ session, route, onNavigate, onLogout, courses
|
|||||||
onConfirm={() => void handleAvatarDelete()}
|
onConfirm={() => void handleAvatarDelete()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<InstructorLearningGuideModal open={guideOpen} onClose={() => setGuideOpen(false)} />
|
||||||
|
|
||||||
<InstructorChatPanel
|
<InstructorChatPanel
|
||||||
session={session}
|
session={session}
|
||||||
courseId={selectedCourseId}
|
courseId={selectedCourseId}
|
||||||
@@ -575,7 +579,7 @@ export function InstructorLayout({ session, route, onNavigate, onLogout, courses
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-white/10 text-[12px] font-bold text-[#a78bfa] hover:bg-white/14" type="button">
|
<button onClick={() => setGuideOpen(true)} className="flex h-8 w-full items-center justify-center gap-2 rounded-md bg-white/10 text-[12px] font-bold text-[#a78bfa] hover:bg-white/14" type="button">
|
||||||
가이드 보기
|
가이드 보기
|
||||||
<ExternalLink size={13} aria-hidden="true" />
|
<ExternalLink size={13} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user