Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<UserContext.Provider value={{ accessToken, refreshToken, user, setAuthInfo, removeAuthInfo }}>
{!hideNavbar && (
Expand Down
5 changes: 5 additions & 0 deletions src/features/Common/Class/Lesson/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const LessonComponent: React.FC<LessonProps> = ({ 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(),
Expand Down
211 changes: 172 additions & 39 deletions src/features/Common/Class/Lesson/markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<s.Container>
<s.ViewerSection>
<s.SectionTitle value={title}></s.SectionTitle>
<s.ViewerWrapper data-color-mode="light">
<MDEditor.Markdown
source={mdContent}
style={{ padding: '20px' }}
/>
</s.ViewerWrapper>
</s.ViewerSection>
</s.Container>
);
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<Directory[]>([]);
const [openDirs, setOpenDirs] = useState<Set<number>>(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 (
<s.Sidebar>
<s.TopTabs>
<s.TabButton
active={activeTab === 'curriculum'}
onClick={() => setActiveTab('curriculum')}
>
<IoListOutline />
<span>커리큘럼</span>
</s.TabButton>
<s.TabButton
active={activeTab === 'chat'}
onClick={() => setActiveTab('chat')}
>
<IoChatbubbleOutline />
<span>채팅</span>
</s.TabButton>
<s.TabButton
active={activeTab === 'question'}
onClick={() => setActiveTab('question')}
>
<AiOutlineQuestionCircle />
<span>질문</span>
</s.TabButton>
</s.TopTabs>

{activeTab === 'curriculum' && (
<s.NavigationSection>
{directories.map((dir) => (
<div key={dir.directoryId}>
<s.DirectoryItem onClick={() => toggleDir(dir.directoryId)}>
<span>{dir.directoryName}</span>
<s.ArrowIcon isOpen={openDirs.has(dir.directoryId)}>
<IoIosArrowDown />
</s.ArrowIcon>
</s.DirectoryItem>
{openDirs.has(dir.directoryId) && dir.documentList && (
<s.DocumentList>
{dir.documentList.map((doc: Document) => (
<s.DocumentItem
key={doc.documentId}
active={String(doc.documentId) === documentId}
onClick={() =>
navigate(`/class/${classRoomId}/${doc.documentId}`, {
state: { title: doc.title },
})
}
>
{doc.title}
</s.DocumentItem>
))}
</s.DocumentList>
)}
</div>
))}
</s.NavigationSection>
)}
</s.Sidebar>
);
};

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 (
<s.PageWrapper>
<Sidebar />
<s.Container>
<s.ViewerContainer>
<s.ViewerHeader>
<h1>{title}</h1>
</s.ViewerHeader>
<s.ViewerWrapper data-color-mode="light">
<MDEditor.Markdown source={mdContent} />
</s.ViewerWrapper>
</s.ViewerContainer>
</s.Container>
</s.PageWrapper>
);
}
26 changes: 26 additions & 0 deletions src/features/Common/Class/Lesson/markdown/api.ts
Original file line number Diff line number Diff line change
@@ -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<ClassAllResponse> {
try {
const response = await CustomApi.get<ClassAllResponse>(`/api/class/${classRoomId}/all`);
return response.data;
} catch (error) {
console.error("Failed to fetch class directories and documents:", error);
return { directoryList: [] };
}
}
Loading
Loading