diff --git a/frontend/public/graduate-curriculum1.png b/frontend/public/graduate-curriculum1.png
new file mode 100644
index 00000000..0214048b
Binary files /dev/null and b/frontend/public/graduate-curriculum1.png differ
diff --git a/frontend/public/graduate-curriculum2.png b/frontend/public/graduate-curriculum2.png
new file mode 100644
index 00000000..277ac9a7
Binary files /dev/null and b/frontend/public/graduate-curriculum2.png differ
diff --git a/frontend/src/AppContent.tsx b/frontend/src/AppContent.tsx
index e966cb19..ba0fe034 100644
--- a/frontend/src/AppContent.tsx
+++ b/frontend/src/AppContent.tsx
@@ -14,6 +14,7 @@ import Main from './pages/Main/Main';
import SignInPage from './pages/Auth/SignInPage';
import Hyperlink from './pages/Undergraduate/Hyperlink';
import GraduateOverview from './pages/Graduate/GraduateOverview';
+import GraduateCurriculum from './pages/Graduate/GraduateCurriculum';
import Overview from './pages/About/About';
import Professor from './pages/About/Faculty/Professor';
import NoticeBoard from './pages/News/NoticeBoard/NoticeBoard';
@@ -29,7 +30,10 @@ import ThesisCreate from './pages/News/Thesis/ThesisCreate';
import ThesisEdit from './pages/News/Thesis/ThesisEdit';
import ThesisDetail from './pages/News/Thesis/ThesisDetail';
import Organization from './pages/About/Organization/Organization';
+
+import mainImage from './assets/images/main_picture.svg';
import Curriculum from './pages/Undergraduate/Curriculum/Curriculum';
+
import NotFound from './components/Notfound/NotFound';
interface PageTransitionProps {
@@ -193,6 +197,7 @@ function AppContent() {
{/* graduate */}
} />
+ } />
{/* about */}
} />
diff --git a/frontend/src/pages/Graduate/GraduateCurriculum.tsx b/frontend/src/pages/Graduate/GraduateCurriculum.tsx
new file mode 100644
index 00000000..fc9bb5b1
--- /dev/null
+++ b/frontend/src/pages/Graduate/GraduateCurriculum.tsx
@@ -0,0 +1,359 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+import { ZoomIn, ZoomOut, Download } from 'lucide-react';
+import * as S from './GraduateCurriculumStyle';
+
+interface Position {
+ x: number;
+ y: number;
+}
+
+interface DragState {
+ isDragging: boolean;
+ startX: number;
+ startY: number;
+ scrollLeft: number;
+ scrollTop: number;
+ lastTouch?: { x: number; y: number };
+}
+
+function GraduateCurriculum() {
+ const [scaleCur, setScaleCur] = useState(0.8);
+ const [scaleRoad, setScaleRoad] = useState(0.8);
+ const [positionCur, setPositionCur] = useState({ x: 0, y: 0 });
+ const [positionRoad, setPositionRoad] = useState({ x: 0, y: 0 });
+
+ const dragStateCur = useRef({
+ isDragging: false,
+ startX: 0,
+ startY: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ });
+
+ const dragStateRoad = useRef({
+ isDragging: false,
+ startX: 0,
+ startY: 0,
+ scrollLeft: 0,
+ scrollTop: 0,
+ });
+
+ const curContainerRef = useRef(null);
+ const roadContainerRef = useRef(null);
+
+ const MIN_SCALE = 0.5;
+ const MAX_SCALE = 2;
+ const SCALE_STEP = 0.2;
+
+ const handleMouseDown = useCallback(
+ (
+ e: React.MouseEvent,
+ dragState: React.RefObject,
+ containerRef: React.RefObject,
+ ) => {
+ if (!containerRef.current || !dragState.current) return;
+
+ dragState.current.isDragging = true;
+ dragState.current.startX = e.pageX - containerRef.current.offsetLeft;
+ dragState.current.startY = e.pageY - containerRef.current.offsetTop;
+ containerRef.current.style.cursor = 'grabbing';
+ },
+ [],
+ );
+
+ const handleMouseMove = useCallback(
+ (
+ e: React.MouseEvent,
+ dragState: React.RefObject,
+ containerRef: React.RefObject,
+ setPosition: React.Dispatch>,
+ currentScale: number,
+ ) => {
+ if (!dragState.current?.isDragging || !containerRef.current) return;
+
+ e.preventDefault();
+
+ const x = e.pageX - containerRef.current.offsetLeft;
+ const y = e.pageY - containerRef.current.offsetTop;
+
+ const walkX = (x - dragState.current.startX) / currentScale;
+ const walkY = (y - dragState.current.startY) / currentScale;
+
+ setPosition((prev: Position) => ({
+ x: prev.x + walkX,
+ y: prev.y + walkY,
+ }));
+
+ dragState.current.startX = x;
+ dragState.current.startY = y;
+ },
+ [],
+ );
+
+ const handleMouseUp = useCallback(
+ (
+ dragState: React.RefObject,
+ containerRef: React.RefObject,
+ ) => {
+ if (!dragState.current || !containerRef.current) return;
+
+ dragState.current.isDragging = false;
+ containerRef.current.style.cursor = 'grab';
+ },
+ [],
+ );
+
+ const handleTouchStart = useCallback(
+ (e: React.TouchEvent, dragState: React.RefObject) => {
+ if (!dragState.current) return;
+
+ const touch = e.touches[0];
+ dragState.current.isDragging = true;
+ dragState.current.lastTouch = {
+ x: touch.clientX,
+ y: touch.clientY,
+ };
+ },
+ [],
+ );
+
+ const handleTouchMove = useCallback(
+ (
+ e: React.TouchEvent,
+ dragState: React.RefObject,
+ setPosition: React.Dispatch>,
+ currentScale: number,
+ ) => {
+ if (!dragState.current?.isDragging || !dragState.current.lastTouch)
+ return;
+
+ e.preventDefault();
+
+ const touch = e.touches[0];
+ const walkX =
+ (touch.clientX - dragState.current.lastTouch.x) / currentScale;
+ const walkY =
+ (touch.clientY - dragState.current.lastTouch.y) / currentScale;
+
+ setPosition((prev: Position) => ({
+ x: prev.x + walkX,
+ y: prev.y + walkY,
+ }));
+
+ dragState.current.lastTouch = {
+ x: touch.clientX,
+ y: touch.clientY,
+ };
+ },
+ [],
+ );
+
+ const handleTouchEnd = useCallback(
+ (dragState: React.RefObject) => {
+ if (!dragState.current) return;
+ dragState.current.isDragging = false;
+ dragState.current.lastTouch = undefined;
+ },
+ [],
+ );
+
+ const handleZoom = useCallback(
+ (
+ type: 'in' | 'out',
+ currentScale: number,
+ setScale: React.Dispatch>,
+ setPosition: React.Dispatch>,
+ ) => {
+ if (type === 'in') {
+ setScale((prev) => Math.min(prev + SCALE_STEP, MAX_SCALE));
+ } else {
+ setScale((prev) => {
+ const newScale = Math.max(prev - SCALE_STEP, MIN_SCALE);
+ if (newScale === MIN_SCALE) {
+ setPosition({ x: 0, y: 0 });
+ }
+ return newScale;
+ });
+ }
+ },
+ [MIN_SCALE, MAX_SCALE, SCALE_STEP],
+ );
+
+ useEffect(() => {
+ const handleGlobalMouseUp = () => {
+ if (curContainerRef.current) {
+ curContainerRef.current.style.cursor = 'grab';
+ dragStateCur.current.isDragging = false;
+ }
+ if (roadContainerRef.current) {
+ roadContainerRef.current.style.cursor = 'grab';
+ dragStateRoad.current.isDragging = false;
+ }
+ };
+
+ document.addEventListener('mouseup', handleGlobalMouseUp);
+ document.addEventListener('touchend', () => {
+ handleTouchEnd(dragStateCur);
+ handleTouchEnd(dragStateRoad);
+ });
+
+ return () => {
+ document.removeEventListener('mouseup', handleGlobalMouseUp);
+ document.removeEventListener('touchend', () => {
+ handleTouchEnd(dragStateCur);
+ handleTouchEnd(dragStateRoad);
+ });
+ };
+ }, []);
+
+ const renderImage = useCallback(
+ (
+ type: 'curriculum' | 'roadmap',
+ {
+ src,
+ alt,
+ scale,
+ position,
+ dragState,
+ containerRef,
+ setPosition,
+ }: {
+ src: string;
+ alt: string;
+ scale: number;
+ position: Position;
+ dragState: React.RefObject;
+ containerRef: React.RefObject;
+ setPosition: React.Dispatch>;
+ },
+ ) => (
+
+ handleMouseDown(e, dragState, containerRef)
+ }
+ onMouseMove={(e: React.MouseEvent) =>
+ handleMouseMove(e, dragState, containerRef, setPosition, scale)
+ }
+ onMouseLeave={() => handleMouseUp(dragState, containerRef)}
+ onTouchStart={(e) => handleTouchStart(e, dragState)}
+ onTouchMove={(e) => handleTouchMove(e, dragState, setPosition, scale)}
+ onTouchEnd={() => handleTouchEnd(dragState)}
+ style={{ cursor: scale > MIN_SCALE ? 'grab' : 'default' }}
+ >
+ MIN_SCALE ? 'none' : 'auto',
+ }}
+ onError={(e) => {
+ e.currentTarget.src = src;
+ e.currentTarget.onerror = null;
+ }}
+ draggable="false"
+ />
+
+
+
+ handleZoom(
+ 'out',
+ scale,
+ type === 'curriculum' ? setScaleCur : setScaleRoad,
+ setPosition,
+ )
+ }
+ disabled={scale <= MIN_SCALE}
+ aria-label={`${type} 축소`}
+ >
+
+
+
+ handleZoom(
+ 'in',
+ scale,
+ type === 'curriculum' ? setScaleCur : setScaleRoad,
+ setPosition,
+ )
+ }
+ disabled={scale >= MAX_SCALE}
+ aria-label={`${type} 확대`}
+ >
+
+
+
+
+ ),
+ [
+ handleMouseDown,
+ handleMouseMove,
+ handleMouseUp,
+ handleTouchStart,
+ handleTouchMove,
+ handleTouchEnd,
+ ],
+ );
+
+ return (
+
+
+ {renderImage('curriculum', {
+ src: '/graduate-curriculum1.png',
+ alt: '대학원 커리큘럼',
+ scale: scaleCur,
+ position: positionCur,
+ dragState: dragStateCur,
+ containerRef: curContainerRef,
+ setPosition: setPositionCur,
+ })}
+ 바이오융합공학 교과과정표1
+
+
+
+
+
+ 교과과정표1 다운로드
+
+
+
+
+ {renderImage('roadmap', {
+ src: '/graduate-curriculum2.png',
+ alt: '대학원 커리큘럼',
+ scale: scaleRoad,
+ position: positionRoad,
+ dragState: dragStateRoad,
+ containerRef: roadContainerRef,
+ setPosition: setPositionRoad,
+ })}
+ 바이오융합공학 교과과정표2
+
+
+
+
+
+ 교과과정표2 다운로드
+
+
+
+ );
+}
+
+export default GraduateCurriculum;
diff --git a/frontend/src/pages/Graduate/GraduateCurriculumStyle.tsx b/frontend/src/pages/Graduate/GraduateCurriculumStyle.tsx
new file mode 100644
index 00000000..1b04522a
--- /dev/null
+++ b/frontend/src/pages/Graduate/GraduateCurriculumStyle.tsx
@@ -0,0 +1,228 @@
+import styled from 'styled-components';
+
+const media = {
+ mobile: '@media(max-width: 768px)',
+ tablet: '@media(max-width: 1024px)',
+};
+
+export const Container = styled.div`
+ max-width: 1400px;
+ width: 95%;
+ margin: 0 auto;
+ padding: 40px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+
+ ${media.mobile} {
+ width: 100%;
+ padding: 20px 16px;
+ gap: 2rem;
+ }
+
+ ${media.tablet} {
+ width: 90%;
+ padding: 30px 20px;
+ }
+`;
+
+export const ImageWrapper = styled.div`
+ width: 100%;
+ background: white;
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+`;
+
+export const ImageContainer = styled.div`
+ position: relative;
+ width: 100%;
+ background: white;
+ overflow: hidden;
+ user-select: none;
+ touch-action: none;
+ min-height: 200px;
+`;
+
+export const CurriculumImage = styled.img`
+ width: 100%;
+ height: auto;
+ display: block;
+ object-fit: contain;
+ transform-origin: center;
+ will-change: transform;
+ user-select: none;
+ -webkit-user-drag: none;
+`;
+
+export const ImageCaption = styled.div`
+ padding: 1rem;
+ text-align: center;
+ color: #4a5568;
+ font-size: 0.9rem;
+ border-top: 1px solid #e2e8f0;
+ background: #f8fafc;
+
+ ${media.mobile} {
+ padding: 0.75rem;
+ font-size: 0.8rem;
+ }
+`;
+
+export const DownloadSection = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-top: 1rem;
+
+ ${media.mobile} {
+ margin-top: 0.75rem;
+ }
+`;
+
+export const DownloadLink = styled.a`
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1rem;
+ color: #2d3748;
+ text-decoration: none;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ transition: all 0.2s ease-in-out;
+ background: white;
+
+ &:hover {
+ background: #f8fafc;
+ border-color: #cbd5e0;
+ }
+
+ svg {
+ width: 18px;
+ height: 18px;
+ }
+
+ ${media.mobile} {
+ width: 100%;
+ justify-content: center;
+ font-size: 0.85rem;
+ }
+`;
+export const ZoomControls = styled.div`
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ display: flex;
+ gap: 0.5rem;
+ background: white;
+ padding: 0.25rem;
+ border-radius: 6px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+`;
+
+export const ZoomButton = styled.button`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border: 1px solid #e2e8f0;
+ border-radius: 4px;
+ background: white;
+ color: #4a5568;
+ cursor: pointer;
+ transition: all 0.2s;
+
+ &:hover:not(:disabled) {
+ background: #f8fafc;
+ border-color: #cbd5e0;
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ svg {
+ width: 18px;
+ height: 18px;
+ }
+`;
+
+export const LoadingContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ color: #4a5568;
+ font-size: 1.1rem;
+
+ ${media.mobile} {
+ min-height: 200px;
+ font-size: 0.9rem;
+ }
+`;
+
+export const ErrorContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 2rem auto;
+ padding: 1rem;
+ max-width: 600px;
+ background-color: #fff5f5;
+ color: #c53030;
+ border-radius: 8px;
+ font-size: 1rem;
+ border: 1px solid #feb2b2;
+ text-align: center;
+ justify-content: center;
+
+ svg {
+ flex-shrink: 0;
+ }
+
+ ${media.mobile} {
+ margin: 1rem;
+ padding: 0.75rem;
+ font-size: 0.875rem;
+ gap: 0.375rem;
+ }
+`;
+
+export const DragInstructions = styled.div`
+ position: absolute;
+ bottom: 1rem;
+ left: 50%;
+ transform: translateX(-50%);
+ background: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.2s;
+
+ ${ImageContainer}:hover & {
+ opacity: 1;
+ }
+
+ ${media.mobile} {
+ display: none;
+ }
+`;
+
+export const TouchInstructions = styled.div`
+ display: none;
+ text-align: center;
+ color: #4a5568;
+ font-size: 0.85rem;
+ margin-top: 0.5rem;
+
+ ${media.mobile} {
+ display: block;
+ }
+`;