diff --git a/src/app/index.jsx b/src/app/index.jsx
index 0621930..88b7f9b 100644
--- a/src/app/index.jsx
+++ b/src/app/index.jsx
@@ -1,3 +1,5 @@
+// 전역 컨텍스트 및 상태 관리 설정 (라우팅, 인증 등)
+
import {
BrowserRouter,
Routes,
@@ -5,16 +7,18 @@ import {
useLocation,
Navigate,
} from 'react-router-dom';
+
+import Header from '../widgets/navigation/Header';
+import Footer from '../widgets/navigation/Footer';
+
import Home from '../pages/home';
import TrackInfo from '../pages/info';
import NotFound from '../pages/NotFound';
import Search from '../pages/Search';
import Splash from '../pages/splash';
-import Header from '../widgets/navigation/Header';
-import Footer from '../widgets/navigation/Footer';
import Login from '../pages/login';
import MyPage from '../pages/my';
-import UploadSection from '../components/UploadSection';
+import TrackDataUploadModal from '../features/track-management/components/TrackDataUploadModal';
function App() {
return (
@@ -40,7 +44,7 @@ function AppContent() {
} />
} />
} />
- } />
+ } />
} />
{!shouldHideHeaderFooter && }
diff --git a/src/assets/TrackLoadMap.svg b/src/assets/TrackLoadMap.svg
deleted file mode 100644
index 4d32e13..0000000
--- a/src/assets/TrackLoadMap.svg
+++ /dev/null
@@ -1,82 +0,0 @@
-
diff --git a/src/assets/ai_engineer.svg b/src/assets/ai_engineer.svg
deleted file mode 100644
index bfb80c2..0000000
--- a/src/assets/ai_engineer.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/assets/ml_dl_model.svg b/src/assets/ml_dl_model.svg
deleted file mode 100644
index 6773a0a..0000000
--- a/src/assets/ml_dl_model.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/assets/nlp_researcher.svg b/src/assets/nlp_researcher.svg
deleted file mode 100644
index b604cb8..0000000
--- a/src/assets/nlp_researcher.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/assets/smart_system.svg b/src/assets/smart_system.svg
deleted file mode 100644
index b7b04e1..0000000
--- a/src/assets/smart_system.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/src/features/track-management/api/userDataService.js b/src/features/track-management/api/userDataService.js
new file mode 100644
index 0000000..269b747
--- /dev/null
+++ b/src/features/track-management/api/userDataService.js
@@ -0,0 +1,23 @@
+// src/services/userDataService.js
+import { BASE_URL } from '../../../shared/api/api';
+import useUserStore from '../../../entities/user/model/useUserStore';
+
+export const uploadStudentExcel = async (file) => {
+ const studentId = useUserStore.getState().studentId;
+ if (!studentId) throw new Error('studentId가 존재하지 않습니다');
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch(`${BASE_URL}/student-data/upload/${studentId}`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || '엑셀 업로드 실패');
+ }
+
+ return await response.json();
+};
diff --git a/src/features/track-management/api/userTrackService.js b/src/features/track-management/api/userTrackService.js
new file mode 100644
index 0000000..59e9b75
--- /dev/null
+++ b/src/features/track-management/api/userTrackService.js
@@ -0,0 +1,20 @@
+import { BASE_URL } from '../../../shared/api/api';
+
+export const postUserTrack = async (studentId, trackName) => {
+ const response = await fetch(
+ `${BASE_URL}/api/user-track?studentId=${encodeURIComponent(studentId)}&trackName=${encodeURIComponent(trackName)}`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(errorData.message || '트랙 등록 실패');
+ }
+
+ return await response.json();
+};
diff --git a/src/features/track-management/components/CourseRow.jsx b/src/features/track-management/components/CourseRow.jsx
new file mode 100644
index 0000000..dadc7f1
--- /dev/null
+++ b/src/features/track-management/components/CourseRow.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+
+/**
+ * CourseRow: 과목 정보를 한 줄(행)로 보여주는 요약 컴포넌트
+ * 좌측에 과목명, 우측 그리드에 연도/학기/코드/이수 상태를 배치
+ * status가 '이수'일 때만 파란색 뱃지, 그 외엔 회색 뱃지로 표시
+ */
+
+const CourseRow = ({ name, year, semester, code, status }) => {
+ // 과목명, 연도, 학기, 코드, 이수상태를 프롭으로 받음
+ return (
+ // 루트 컨테이너: 흰색 배경, 둥근 모서리, 패딩, 플렉스 박스
+ // 좌측/우측을 justify-between으로 양끝 정렬해 좌: 과목명 / 우: 메타 정보 구조를 만듦
+
+
{name}
{' '}
+ {/* 과목명: 굵은 글씨, 기본 크기 */}
+ {/* 우측 메타 정보 컨테이너: 4열 그리드, 열 간격, 최소 너비, 작은 글씨, 파란색 텍스트, 중앙 정렬 */}
+
+ {/* 연도, 학기, 코드, 이수상태를 각각 스팬으로 표시 */}
+ {year}
+ {semester}
+ {code}
+ {/* 상태 뱃지: 공통 스타일에 배경색은 이수 상태에 따라 다르게 적용 */}
+
+ {status} {/* 텍스트는 status 그대로 표기 */}
+
+
+
+ );
+};
+
+export default CourseRow;
diff --git a/src/features/track-management/components/TrackCourseList.jsx b/src/features/track-management/components/TrackCourseList.jsx
new file mode 100644
index 0000000..b4c843d
--- /dev/null
+++ b/src/features/track-management/components/TrackCourseList.jsx
@@ -0,0 +1,92 @@
+import TrackProgressHeader from './TrackProgressHeader'; // 상단 헤더(진행률)
+import CourseRow from './CourseRow'; // 개별 과목 행
+import useTrackStore from '../../../entities/course/model/useTrackStore'; // 전역 데이터 스토어 훅
+
+/**
+ * TrackCourseList: 활성 트랙의 이수/미이수 과목을 합쳐 한 화면에 보여주는 섹션
+ * 상단에 트랙 진행률 헤더를, 하단에 과목 행(CourseRow)들을 렌더링
+ * 트랙 데이터는 전역 스토어(useTrackStore)의 trackData에서 조회
+ */
+
+// import { track1,track2,track3,track4,track5,track6,track7,track8 } from "../constants/mock";
+
+// const trackMap = {
+// "인공지능시스템": track1,
+// "메타버스 플랫폼": track2,
+// "클라우드 컴퓨팅": track3,
+// "공간비주얼 SW": track4,
+// "인터렉티브 플랫폼": track5,
+// "지능형에이전트": track6,
+// "AI 콘텐츠": track7,
+// "데이터인텔리전스": track8
+// };
+
+// 현재 활성 탭/트랙 이름을 프롭으로 받아 해당 트랙의 과목 목록을 보여줄 준비
+const TrackCourseList = ({ activeTrack }) => {
+ // const courses = trackMap[activeTrack] || [];
+ // 전역 스토어에서 업로드/계산된 트랙 데이터를 가져옴
+ const trackData = useTrackStore((state) => state.trackData);
+ // 현재 활성 트랙에 해당하는 데이터 객체를 찾음. 없으면 null 반환
+ const current = trackData.find((t) => t.trackName === activeTrack);
+
+ if (!current) {
+ // 데이터가 없으면 업로드 안내 메시지 표시
+ return (
+
+ {/* 트랙 데이터 업로드 모달 컴포넌트 */}
+ {/* 이수 개수/총 과목 수(고정: 6)를 헤더에 전달해 진행률과 요약 텍스트 표시 */}
+
+
+
+ {/*
+ 과목 명
+ 해당 학년
+ 해당 학기
+ 학수번호
+ 이수 여부
+
*/}
+
+ {/* 합쳐진 과목 배열을 맵핑해 CourseRow 컴포넌트로 렌더링: 키는 courseCode+인덱스 */}
+
+ {allCourses.map((course, i) => (
+
+ ))}
+
+
+ {/* */}
+
+ );
+};
+
+export default TrackCourseList;
diff --git a/src/features/track-management/components/TrackDataUploadModal.jsx b/src/features/track-management/components/TrackDataUploadModal.jsx
new file mode 100644
index 0000000..6761970
--- /dev/null
+++ b/src/features/track-management/components/TrackDataUploadModal.jsx
@@ -0,0 +1,101 @@
+import { useRef, useState } from 'react'; // 파일 입력 제어, 상태 관리
+import TrackStatus from './TrackStatus'; // 상단 타이틀, 캐릭터
+import Chip from '../../../shared/components/ChipButton'; // 안내 텍스트 내 강조용 칩
+import Button from '../../../shared/components/AppButton'; // 파일 선택 및 업로드 버튼
+import DeleteIcon from '../../../assets/delete.svg'; // 닫기 아이콘
+import { uploadStudentExcel } from '../api/userDataService'; // 엑셀 업로드 API
+import useTrackStore from '../../../entities/course/model/useTrackStore'; // 전역 상태 관리
+
+/**
+ * TrackDataUploadModal: 학사시스템에서 내려받은 수강이력 엑셀(.xlsx)을 업로드하는 모달.
+ * 파일 선택 → 업로드 API 호출 → 전역 스토어에 반영 후 모달을 닫는 흐름을 제공한다.
+ * onClose 콜백으로 외부에서 모달 닫기 동작을 제어한다.
+ */
+
+const TrackDataUploadModal = ({ onClose }) => {
+ // onClose: 모달 닫기 함수
+ const fileInputRef = useRef(null); // 숨겨진 를 클릭시키기 위한 ref)
+ const [selectedFile, setSelectedFile] = useState(null);
+
+ // 파일 선택 창 열기(파일 선택 버튼)
+ const handleFileClick = () => {
+ fileInputRef.current.click();
+ };
+
+ // 파일 선택 처리(파일 변경): 첫 번째 선택 파일을 selectedFile에 저장(존재 시에만)
+ const handleFileChange = (e) => {
+ const file = e.target.files[0];
+ if (file) setSelectedFile(file);
+ };
+
+ // 파일 업로드 처리
+ const handleUpload = async () => {
+ if (!selectedFile) {
+ // 파일이 선택되지 않은 경우
+ alert('엑셀 파일을 먼저 선택해주세요.');
+ return;
+ }
+
+ try {
+ const result = await uploadStudentExcel(selectedFile); // 업로드 API 호출(비동기)
+ console.log(result);
+ useTrackStore.getState().setTrackData(result); // 전역 상태에 업로드 결과 저장
+ alert('업로드 성공!');
+ onClose(); // 업로드 완료 후 모달 닫기
+ } catch (err) {
+ alert(`업로드 실패: ${err.message}`); // 업로드 실패 시 에러 메시지 표시
+ }
+ };
+
+ return (
+ // 카드형 모달 컨테이너 (가로 최대 크기 제한, 흰색 배경, 둥근 모서리, 그림자, 패딩, 중앙 정렬)
+
+ {/* X 버튼 */}
+
+
+ {/* 상단 캐릭터 + 제목 */}
+
+
+ {/* 안내 텍스트 */}
+
+
학사정보시스템 사이트에 로그인합니다.
+
+ 왼쪽 메뉴 바에서 {' '}
+ 로
+ 이동합니다.
+
+
+ 성적 엑셀다운로드 버튼을 클릭해 다운로드한 후, 해당 엑셀 파일을
+ 업로드합니다.
+
+
+
+ {/* 파일 선택 및 업로드 */}
+
+ {' '}
+ {/* 세로 방향, 중앙 정렬, 요소 간격 */}
+
+
+
+ {selectedFile && (
+
{selectedFile.name}
+ )}
+
+
+ );
+};
+
+export default TrackDataUploadModal;
diff --git a/src/features/track-management/components/TrackInitButton.jsx b/src/features/track-management/components/TrackInitButton.jsx
new file mode 100644
index 0000000..56e2bab
--- /dev/null
+++ b/src/features/track-management/components/TrackInitButton.jsx
@@ -0,0 +1,23 @@
+/**
+ * TrackInitButton: 트랙 추천(초기화)을 시작하는 CTA 버튼
+ * onClick으로 외부 동작을 실행하고, disabled일 때 시각적으로/기능적으로 비활성화됨
+ * 폼 내부에 둘 경우 type="button" 지정이 권장
+ */
+
+const TrackInitButton = ({ onClick, disabled }) => {
+ // 클릭 핸들러와 비활성화 상태를 props로 받음
+ return (
+
+ );
+};
+
+export default TrackInitButton;
diff --git a/src/features/track-management/components/TrackIntroHeader.jsx b/src/features/track-management/components/TrackIntroHeader.jsx
new file mode 100644
index 0000000..4bea7bf
--- /dev/null
+++ b/src/features/track-management/components/TrackIntroHeader.jsx
@@ -0,0 +1,51 @@
+import TrackIcon from '../../../assets/logo-character.svg?react'; // 일러스트 svg
+import useUserStore from '../../../entities/user/model/useUserStore'; // 사용자 상태 관리 훅: 학생 이름을 전역 상태에서 읽어옴
+
+/**
+ * TrackIntroHeader: 트랙 안내 페이지의 히어로 섹션(일러스트 + 환영 헤드라인 + 소개 문단)
+ * Zustand에서 studentName을 가져와 개인화된 인사말을 표시
+ * 온보딩/인트로 상단에서 트랙 제도의 개요와 핵심 포인트를 간결히 전달
+ */
+
+function TrackIntroHeader() {
+ // 프롭 없음. 정적 인트로 섹션 렌더링
+ // 스토어에서 학생 이름만 읽어옴. 스토리북에서는 기본 값 셋업해줘야 자연스러운 렌더 가능
+ const studentName = useUserStore((state) => state.studentName);
+
+ return (
+ // 전체 컨테이너: 왼쪽 정렬, 좌우 패딩
+
+ {/* 일러스트 */}
+
+
+
+
+ {/* 제목 */}
+
+ 안녕하세요! {studentName}님
+
+ ‘트랙제’에 대해 알아볼까요?
+
+
+ {/* 설명 */}
+
+ 전공 트랙 제도는 인공지능융합대학 소속 세 학과(컴퓨터공학과,
+ 콘텐츠소프트웨어학과, 인공지능데이터사이언스학과)의 학생들이 자신이
+ 원하는 전문 분야를 중심으로 학업을 설계하고 이수할 수 있도록 돕는{' '}
+
+ 맞춤형 진로 설계 시스템
+
+ 입니다. 학생들은 각자의 관심 분야에 따라 하나 이상의 트랙을 자유롭게
+ 선택할 수 있으며, 해당 트랙에 포함된 과목을 6과목 이상 이수하면 트랙
+ 인증을 받을 수 있습니다. 이 인증은 학생의 전문성과 학습 방향성을
+ 보여주는 지표로, 졸업 후 진로 탐색이나 대학원 진학, 기업 취업 등에
+ 실질적인 도움이 될 수 있습니다. 트랙을 선택한다고 해서 반드시 해당
+ 트랙을 이수해야 하는 것은 아니며, 중도 변경이나 이수 포기에도 졸업
+ 요건에 불이익은 없습니다. 학생의 학습 자율성과 유연성을 보장하는
+ 방향으로 운영됩니다.
+
+
+ );
+}
+
+export default TrackIntroHeader;
diff --git a/src/features/track-management/components/TrackProgressHeader.jsx b/src/features/track-management/components/TrackProgressHeader.jsx
new file mode 100644
index 0000000..f5dbb7e
--- /dev/null
+++ b/src/features/track-management/components/TrackProgressHeader.jsx
@@ -0,0 +1,48 @@
+/**
+ * TrackProgressHeader: 트랙 이수 현황을 요약하는 헤더 블록
+ * 업로드가 없으면 안내 문구를, 있으면 완료/전체와 진행률 바를 표시
+ * 진행률은 최대 100%로 캡핑되며, completedCount/total 형식으로 함께 보여줌
+ */
+
+const TrackProgressHeader = ({
+ completedCount = 0,
+ total = 6,
+ hasData = true,
+}) => {
+ // 기본값 설정: 이수 과목 수, 전체 과목 수, 업로드 여부
+ // 진행률 계산 후 100% 초과 방지 (total이 0일 경우 NaN 가능 -> 가드 권장)
+ const percent = Math.min((completedCount / total) * 100, 100);
+ // 우측에 표시될 완료/전체 형식 문자열
+ const displayProgress = `${completedCount}/${total}`;
+
+ // 업로드 데이터가 없을 경우 안내 메시지 표시
+ if (!hasData) {
+ return (
+
+
+ 수강이력을 업로드하여 달성률을 확인해보세요
+
+
+ );
+ }
+
+ return (
+
+ {' '}
+ {/* 정상 상태 UI 컨테이너: 상단 라인은 좌우 분할*/}
+
+
총 {completedCount}과목 이수 완료!
+ {displayProgress}
+
+ {/* 진행률 바 컨테이너: 흰색 배경에 노란색 진행률 표시 */}
+
+
+
+
+ );
+};
+
+export default TrackProgressHeader;
diff --git a/src/features/track-management/components/TrackStatus.jsx b/src/features/track-management/components/TrackStatus.jsx
new file mode 100644
index 0000000..75b7b09
--- /dev/null
+++ b/src/features/track-management/components/TrackStatus.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import LogoCharacter from '../../../assets/logo-character.svg?react';
+
+/**
+ * TrackStatus: 캐릭터 + 제목(+선택적 소제목) 헤더 블록.
+ * variant='card'|'large'로 크기/간격을 전환하고, 필요 시 onClickCharacter로 캐릭터 클릭을 허용
+ * title에 HTML을 써야 하면 allowHtmlTitle=true로 설정
+ */
+
+function TrackStatus({
+ variant = 'card',
+ title,
+ subtitle,
+ onClickCharacter,
+ allowHtmlTitle = false,
+}) {
+ // 프롭 정의: 두 컴포넌트의 차이(크기/HTML지원/소제목)를 속성으로 통합
+ const isLarge = variant === 'large'; // 크기 분기
+ const characterClasses = isLarge ? 'w-40 h-auto' : 'w-28 h-auto'; // 캐릭터 크기 매핑(large: w-40, card: w-28)
+ const titleClasses = isLarge // 제목 타이포, 마진 매핑
+ ? 'text-2xl font-semibold text-blue-600 mb-2 leading-relaxed'
+ : 'text-xl font-semibold text-blue-600 mt-16 mb-8';
+
+ return (
+ // 공통 레이아웃
+
+ {/* 캐릭터 */}
+
+ {onClickCharacter ? ( // 클릭 가능 여부에 따라 button으로 감싸기
+
+ ) : (
+ // 단순 표시(비인터랙션)
+ )}
+
+
+ {/* 제목 */}
+ {title && // 제목이 있을 때만 렌더
+ (allowHtmlTitle ? ( // HTML 허용 시 dangerouslySetInnerHTML 사용 (원래 Large가 HTML 허용하던 동작 이식)
+
+ ) : (
+
{title}
// 일반 텍스트 제목
+ ))}
+
+ {/* 소제목(옵션) */}
+ {/* subtitle이 있을 때만 렌더, large일 땐 색상과 마진 다르게 */}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+}
+
+export default TrackStatus;
diff --git a/src/features/track-management/components/TrackTabs.jsx b/src/features/track-management/components/TrackTabs.jsx
new file mode 100644
index 0000000..7f371be
--- /dev/null
+++ b/src/features/track-management/components/TrackTabs.jsx
@@ -0,0 +1,44 @@
+// import { useState } from 'react';
+
+// const TABS = [
+// '인공지능시스템', '메타버스 플랫폼', '클라우드 컴퓨팅', '공간비주얼 SW', '인터렉티브 플랫폼', '지능형에이전트', 'AI 콘텐츠', '데이터인텔리전스'
+// ];
+
+/**
+ * TrackTabs: 트랙 후보 배열에서 하나를 선택하는 컨트롤드 탭 컴포넌트
+ * activeTab과 onChange를 통해 외부 상태로 제어되며, 활성 탭은 채움/비활성은 아웃라인 스타일로 구분
+ * 탭이 많으면 flex-wrap으로 자동 줄바꿈되어 반응형으로 대응
+ */
+
+// 탭 목록, 현재 활성 탭, 탭 변경 콜백을 프롭으로 받음
+const TrackTabs = ({ tabs, activeTab, onChange }) => {
+ // const [activeTab, setActiveTab] = useState(TABS[0]);
+
+ return (
+ // 루트 컨테이너 (가로 정렬, 줄바꿈 허용, 중앙 정렬, 탭 간격: 탭 많아도 줄바꿈으로 대응)
+
+ {/* 각 탭을 버튼으로 렌더링, 활성 탭은 파란 배경/흰 글씨, 비활성 탭은 테두리/파란 글씨 */}
+ {tabs.map((tab) => {
+ const isActive = activeTab === tab;
+ return (
+ // 클릭 시 상위에서 관리하는 활성 탭 상태를 변경하는 콜백 호출: 스타일은 조건부로 적용
+
+ );
+ })}
+
+ );
+};
+
+export default TrackTabs;
diff --git a/src/pages/home/index.jsx b/src/pages/home/index.jsx
index 4afe967..f3b906b 100644
--- a/src/pages/home/index.jsx
+++ b/src/pages/home/index.jsx
@@ -1,56 +1,65 @@
-import TrackTabs from "../../components/TrackTabs";
-import CourseList from "../../components/CourseList";
-import TrackStatusCard from "../../components/TrackStatusCard";
-import UploadSection from "../../components/UploadSection"; // 이전에 만든 UploadSection 컴포넌트
-import { useState } from "react";
+import TrackTabs from '../../features/track-management/components/TrackTabs';
+import TrackCourseList from '../../features/track-management/components/TrackCourseList';
+import TrackStatus from '../../features/track-management/components/TrackStatus';
+import TrackDataUploadModal from '../../features/track-management/components/TrackDataUploadModal'; // 이전에 만든 TrackDataUploadModal 컴포넌트
+import { useState } from 'react';
const TABS = [
- "인공지능시스템", "메타버스 플랫폼", "클라우드 컴퓨팅", "공간비주얼 SW",
- "인터렉티브 플랫폼", "지능형에이전트", "AI 콘텐츠", "데이터인텔리전스"
+ '인공지능시스템',
+ '메타버스 플랫폼',
+ '클라우드 컴퓨팅',
+ '공간비주얼 SW',
+ '인터렉티브 플랫폼',
+ '지능형에이전트',
+ 'AI 콘텐츠',
+ '데이터인텔리전스',
];
const Home = () => {
-
- const [activeTab, setActiveTab] = useState(TABS[0]);
- const [showUpload, setShowUpload] = useState(false);
+ const [activeTab, setActiveTab] = useState(TABS[0]);
+ const [showUpload, setShowUpload] = useState(false);
- // 모달을 닫는 함수
- const handleCloseUpload = () => setShowUpload(false);
+ // 모달을 닫는 함수
+ const handleCloseUpload = () => setShowUpload(false);
- return (
-
);
diff --git a/src/pages/login/index.jsx b/src/pages/login/index.jsx
index a0a1974..0fa5482 100644
--- a/src/pages/login/index.jsx
+++ b/src/pages/login/index.jsx
@@ -1,11 +1,10 @@
import { useState } from 'react';
-import Input from '../../components/Input';
-import Button from '../../components/Button';
-import Character from '../../components/Character';
+import AppTextField from '../../shared/components/AppTextField';
+import AppButton from '../../shared/components/AppButton';
+import AppBrand from '../../shared/components/AppBrand';
import { loginWithSejongPortal } from '../../features/auth/api/userService'; // 포털 API 호출 함수
import { useNavigate } from 'react-router-dom'; // 리다이렉션을 위한 훅
import useUserStore from '../../entities/user/model/useUserStore';
-import BackGround from '../../assets/background.svg?react';
function Login() {
const [id, setId] = useState('');
@@ -41,37 +40,67 @@ function Login() {
};
return (
-