diff --git a/src/app/App.tsx b/src/app/App.tsx index 4ef08ef1..b0ae2683 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -33,7 +33,10 @@ function AuthWrapper() { if (role === 'STUDENT') role = 'STU'; else if (role === 'TEACHER') role = 'TCH'; // Hide Navbar on markdown editor route - const hideNavbar = !!matchPath('/class/:classRoomId/:directoryId/make/lesson/markdown', location.pathname); + const hideNavbar = !!matchPath( + '/class/:classRoomId/:directoryId/make/lesson/markdown', + location.pathname + ); return ( {!hideNavbar && ( diff --git a/src/features/Common/Class/Lesson/index.tsx b/src/features/Common/Class/Lesson/index.tsx index 1d5bc8aa..45bed623 100644 --- a/src/features/Common/Class/Lesson/index.tsx +++ b/src/features/Common/Class/Lesson/index.tsx @@ -47,6 +47,11 @@ const LessonComponent: React.FC = ({ classRoomId }) => { const fetchData = async () => { try { const classInfo = await getLessonDirectories(classRoomId); + // 세션 스토리지에 디렉토리 정보 저장 + if (classInfo.directoryList) { + sessionStorage.setItem(`lessonDirectories-${classRoomId}`, JSON.stringify(classInfo.directoryList)); + } + setCode(classInfo.code) const dirs: Directory[] = classInfo.directoryList.map((dir) => ({ id: dir.directoryId.toString(), diff --git a/src/features/Common/Class/Lesson/markdown/Markdown.tsx b/src/features/Common/Class/Lesson/markdown/Markdown.tsx index cb647705..edb080a6 100644 --- a/src/features/Common/Class/Lesson/markdown/Markdown.tsx +++ b/src/features/Common/Class/Lesson/markdown/Markdown.tsx @@ -1,45 +1,178 @@ +// MarkDownViewerPage.tsx + import { useEffect, useState } from 'react'; import MDEditor from '@uiw/react-md-editor'; -import * as s from './styles' -import { useParams, useLocation } from 'react-router-dom'; +import * as s from './styles'; +import { useParams, useLocation, useNavigate } from 'react-router-dom'; import { getMarkDown } from '../../api/class/useMarkdown'; +import { getLessonDirectories as qre } from '@/features/Common/Class/api/useLesson'; +import { IoListOutline } from 'react-icons/io5'; +import { IoChatbubbleOutline } from 'react-icons/io5'; +import { AiOutlineQuestionCircle } from 'react-icons/ai'; +import { IoIosArrowDown } from 'react-icons/io'; -export default function MarkDownViewerPage() { - const { documentId } = useParams<{ documentId: string }>(); - const location = useLocation(); - const [mdContent, setMdContent] = useState('Loading...'); - const [title] = useState(location.state?.title || ''); - - useEffect(() => { - if (!documentId) return; - - const fetchMdData = async () => { - try { - const response = await getMarkDown(documentId); - setMdContent(response); - console.log(response); - } catch (error: unknown) { - console.error("Failed to fetch markdown:", error); - setMdContent("# Error\n\nFailed to load document."); - } - }; - - fetchMdData(); - }, [documentId]); - - - return ( - - - - - - - - - ); +interface Document { + documentId: number; + title: string; +} + +interface Directory { + directoryId: number; + directoryName: string; + documentList?: Document[]; +} + +interface DirectoryResponse { + directoryList: Directory[]; } +const Sidebar = () => { + const { classRoomId, documentId } = useParams<{ classRoomId: string; documentId: string }>(); + const [directories, setDirectories] = useState([]); + const [openDirs, setOpenDirs] = useState>(new Set()); + const [activeTab, setActiveTab] = useState<'curriculum' | 'chat' | 'question'>('curriculum'); + const navigate = useNavigate(); + + useEffect(() => { + if (classRoomId) { + const cachedDirectories = sessionStorage.getItem(`lessonDirectories-${classRoomId}`); + + const processDirectories = (directoryList: Directory[]) => { + setDirectories(directoryList || []); + const currentDir = directoryList?.find((dir) => + dir.documentList?.some((doc) => String(doc.documentId) === documentId)); + + if (currentDir?.directoryId) { + setOpenDirs((prev) => new Set(prev).add(currentDir.directoryId)); + } + }; + + if (cachedDirectories) { + try { + const parsedData = JSON.parse(cachedDirectories); + processDirectories(parsedData); + } catch (e) { + console.error("Failed to parse cached directories", e); + // 파싱 실패 시 API 호출 + qre(classRoomId).then((data: DirectoryResponse) => { + sessionStorage.setItem(`lessonDirectories-${classRoomId}`, JSON.stringify(data.directoryList || [])); + processDirectories(data.directoryList); + }); + } + } else { + qre(classRoomId).then((data: DirectoryResponse) => { + sessionStorage.setItem(`lessonDirectories-${classRoomId}`, JSON.stringify(data.directoryList || [])); + processDirectories(data.directoryList); + }); + } + } + }, [classRoomId, documentId]); + + const toggleDir = (dirId: number) => { + setOpenDirs((prev) => { + const newSet = new Set(prev); + if (newSet.has(dirId)) newSet.delete(dirId); + else newSet.add(dirId); + return newSet; + }); + }; + + return ( + + + setActiveTab('curriculum')} + > + + 커리큘럼 + + setActiveTab('chat')} + > + + 채팅 + + setActiveTab('question')} + > + + 질문 + + + + {activeTab === 'curriculum' && ( + + {directories.map((dir) => ( +
+ toggleDir(dir.directoryId)}> + {dir.directoryName} + + + + + {openDirs.has(dir.directoryId) && dir.documentList && ( + + {dir.documentList.map((doc: Document) => ( + + navigate(`/class/${classRoomId}/${doc.documentId}`, { + state: { title: doc.title }, + }) + } + > + {doc.title} + + ))} + + )} +
+ ))} +
+ )} +
+ ); +}; + +export default function MarkDownViewerPage() { + const { documentId } = useParams<{ documentId: string }>(); + const location = useLocation(); + const [mdContent, setMdContent] = useState('Loading...'); + const [title] = useState(location.state?.title || ''); + + useEffect(() => { + if (!documentId) return; + + const fetchMdData = async () => { + try { + const response = await getMarkDown(documentId); + setMdContent(response); + } catch (error: unknown) { + console.error('Failed to fetch markdown:', error); + setMdContent('# Error\n\nFailed to load document.'); + } + }; + + fetchMdData(); + }, [documentId]); + + return ( + + + + + +

{title}

+
+ + + +
+
+
+ ); +} \ No newline at end of file diff --git a/src/features/Common/Class/Lesson/markdown/api.ts b/src/features/Common/Class/Lesson/markdown/api.ts new file mode 100644 index 00000000..8870eb35 --- /dev/null +++ b/src/features/Common/Class/Lesson/markdown/api.ts @@ -0,0 +1,26 @@ +import CustomApi from "@/shared/config/api"; + +interface Directory { + directoryId: number; + directoryName: string; + documentList: Document[]; +} + +interface Document { + documentId: number; + title: string; +} + +interface ClassAllResponse { + directoryList: Directory[]; +} + +export async function qre(classRoomId: string): Promise { + try { + const response = await CustomApi.get(`/api/class/${classRoomId}/all`); + return response.data; + } catch (error) { + console.error("Failed to fetch class directories and documents:", error); + return { directoryList: [] }; + } +} \ No newline at end of file diff --git a/src/features/Common/Class/Lesson/markdown/styles.ts b/src/features/Common/Class/Lesson/markdown/styles.ts index 138763cc..add051ee 100644 --- a/src/features/Common/Class/Lesson/markdown/styles.ts +++ b/src/features/Common/Class/Lesson/markdown/styles.ts @@ -1,89 +1,75 @@ -import styled from '@emotion/styled'; -import { theme } from '@/shared/theme/theme.styles'; -import { fonts } from '@/shared/theme/font.styles'; +import styled from "@emotion/styled"; +import { theme } from "@/shared/theme/theme.styles"; +import { fonts } from "@/shared/theme/font.styles"; -export const Container = styled.div` +export const PageWrapper = styled.div` display: flex; - gap: 20px; - height: 100vh; - padding: 20px; - background-color: ${theme.colors.gray[200]}; - - @media (max-width: 1200px) { - gap: 16px; - padding: 16px; - } - - @media (max-width: 768px) { - flex-direction: column; - height: auto; - gap: 12px; - padding: 12px; - } + height: calc(100vh - var(--app-top-offset, 0px)); + background-color: #fff; `; -export const ViewerSection = styled.div` +export const Container = styled.div` flex: 1; display: flex; - flex-direction: column; + justify-content: center; + overflow-y: auto; + background-color: #fff; +`; + +export const ViewerContainer = styled.div` background: white; + width: 100%; border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - overflow: hidden; - height: 95%; - - @media (max-width: 768px) { - height: auto; - min-height: 300px; - } + padding: 40px 60px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); + overflow-y: auto; `; -export const SectionTitle = styled.input` - margin: 0; - padding: 16px 20px; - background: #f8f9fa; - border: none; - outline: none; - border-bottom: 1px solid #e9ecef; - ${fonts.P2}; - font-weight: 600; - color: #495057; +export const ViewerHeader = styled.div` + margin-bottom: 48px; - @media (max-width: 1200px) { - ${fonts.P2}; - padding: 12px 16px; - } - - @media (max-width: 768px) { - ${fonts.P1}; - padding: 10px 14px; + h1 { + font-size: 28px; + font-weight: 700; + color: #222; + margin: 0; + border-bottom: 2px solid ${theme.colors.gray[300]}; + padding-bottom: 12px; } `; export const ViewerWrapper = styled.div` - flex: 1; - overflow-y: auto; - padding: 20px; - - h1, h2, h3, h4, h5, h6 { - margin-top: 24px; - margin-bottom: 16px; - font-weight: 600; - line-height: 1.25; + font-family: "Noto Sans KR", sans-serif; + line-height: 1.8; + color: #2c2c2c; + + h1, + h2, + h3 { + margin-top: 32px; + font-weight: 700; + color: #111; } - h1 { font-size: 2em; } - h2 { font-size: 1.5em; } - h3 { font-size: 1.25em; } + h1 { + font-size: 24px; + } + h2 { + font-size: 20px; + color: #222; + } + h3 { + font-size: 18px; + color: #333; + } p { - margin-bottom: 16px; - line-height: 1.6; + margin-bottom: 18px; } - ul, ol { + ul { + margin-left: 24px; margin-bottom: 16px; - padding-left: 24px; } li { @@ -91,33 +77,126 @@ export const ViewerWrapper = styled.div` } code { - background-color: #f1f3f4; + background-color: #f2f3f5; padding: 2px 6px; border-radius: 4px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - ${fonts.P2}; } pre { - background-color: #f6f8fa; - padding: 16px; + background-color: #f8f9fa; + padding: 12px; border-radius: 6px; overflow-x: auto; - margin-bottom: 16px; } blockquote { - border-left: 4px solid #dfe2e5; - margin: 0 0 16px 0; - padding-left: 16px; - color: #6a737d; + border-left: 4px solid ${theme.colors.gray[300]}; + margin: 16px 0; + padding-left: 12px; + color: #555; + } +`; + +export const Sidebar = styled.div` + width: 280px; + background-color: #fafbfc; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; +`; + +export const TopTabs = styled.div` + display: flex; + gap: 8px; + padding: 16px; + border-bottom: 1px solid ${theme.colors.gray[400]}; +`; + +export const TabButton = styled.button<{ active: boolean }>` + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 10px 8px; + border: none; + background-color: ${({ active }) => (active ? theme.colors.gray[400] : 'transparent')}; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; + color: #333; + + svg { + font-size: 20px; } - @media (max-width: 1200px) { - padding: 16px; + &:hover { + background-color: ${({ active }) => (active ? theme.colors.blue[200] : theme.colors.gray[200])}; } - @media (max-width: 768px) { - padding: 12px; + span { + font-size: 11px; + font-weight: ${({ active }) => (active ? 600 : 400)}; + white-space: nowrap; } -`; \ No newline at end of file +`; + +export const NavigationSection = styled.div` + padding: 16px; + flex: 1; +`; + +export const SidebarTitle = styled.div` + ${fonts.P4} + color: #333; + margin-bottom: 20px; +`; + +export const DirectoryItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + font-weight: 600; + font-size: 15px; + padding: 10px 0; + cursor: pointer; + color: #222; + + &:hover { + color: ${theme.colors.blue[600]}; + } +`; + +export const ArrowIcon = styled.div<{ isOpen: boolean }>` + display: flex; + align-items: center; + transition: transform 0.2s; + transform: rotate(${({ isOpen }) => (isOpen ? '180deg' : '0deg')}); + + svg { + font-size: 16px; + color: #666; + } +`; + +export const DocumentList = styled.div` + padding-left: 0; + margin-top: 4px; +`; + +export const DocumentItem = styled.div<{ active: boolean }>` + padding: 8px 16px; + font-size: 14px; + cursor: pointer; + border-radius: 4px; + background-color: ${({ active }) => (active ? theme.colors.blue[200] : 'transparent')}; + color: ${({ active }) => (active ? '#111' : '#333')}; + margin-bottom: 2px; + transition: background-color 0.2s; + + &:hover { + background-color: ${({ active }) => (active ? theme.colors.blue[200] : theme.colors.gray[200])}; + color: ${({ active }) => (active ? '#111' : theme.colors.blue[700])}; + } +`;