From e9bce48a3eb021e9c89edf772c9f68ae6c372f64 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Thu, 4 Sep 2025 01:45:26 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/assets/TrackLoadMap.svg | 82 ----------------------------------- src/assets/ai_engineer.svg | 4 -- src/assets/ml_dl_model.svg | 4 -- src/assets/nlp_researcher.svg | 4 -- src/assets/smart_system.svg | 4 -- 5 files changed, 98 deletions(-) delete mode 100644 src/assets/TrackLoadMap.svg delete mode 100644 src/assets/ai_engineer.svg delete mode 100644 src/assets/ml_dl_model.svg delete mode 100644 src/assets/nlp_researcher.svg delete mode 100644 src/assets/smart_system.svg 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 @@ - - - - From 725322d340cd0e4b0784e6230691ca2d0a2cb45f Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 6 Sep 2025 00:22:54 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EA=B3=B5=EC=9A=A9=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EA=B0=9C=EC=84=A0,=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/components/AppBrand.jsx | 22 ++++++++ src/shared/components/AppButton.jsx | 40 ++++++++++++++ src/shared/components/AppPrimarySection.jsx | 12 ++++ src/shared/components/AppTextField.jsx | 61 +++++++++++++++++++++ src/shared/components/ChipButton.jsx | 26 +++++++++ src/shared/components/InfoLabel.jsx | 19 +++++++ 6 files changed, 180 insertions(+) create mode 100644 src/shared/components/AppBrand.jsx create mode 100644 src/shared/components/AppButton.jsx create mode 100644 src/shared/components/AppPrimarySection.jsx create mode 100644 src/shared/components/AppTextField.jsx create mode 100644 src/shared/components/ChipButton.jsx create mode 100644 src/shared/components/InfoLabel.jsx diff --git a/src/shared/components/AppBrand.jsx b/src/shared/components/AppBrand.jsx new file mode 100644 index 0000000..88c9151 --- /dev/null +++ b/src/shared/components/AppBrand.jsx @@ -0,0 +1,22 @@ +import Logo from '../../assets/logo-character.svg?react'; // 마스코트 로고 +import MyTrack from '../../assets/mytrack.svg?react'; // 워드마크 "마이트랙" + +/* + * AppBrand: 앱의 브랜드 락업(마스코트 + 워드마크)을 세로 스택으로 보여주는 컴포넌트 + * 고정 크기의 마스코트와 로고를 중앙 정렬해 온보딩/로그인 등에서 브랜드 아이덴티티를 강조 + * 외부 프롭 없이 정적 브랜딩 표시 목적에 특화 + */ + +function AppBrand() { + // 컴포넌트 선언 시작. 외부 프롭 없이 고정된 UI 반환 + return ( +
+ {/* 세로 스택 레이아웃(flex 컨테이너, 중앙 정렬, 아이템 간격)으로 상단에 마스코트, 하단에 마이트랙 로고 배치*/} + {/*브랜드 락업(마스코트+로고) 형태*/} + + +
+ ); +} + +export default AppBrand; diff --git a/src/shared/components/AppButton.jsx b/src/shared/components/AppButton.jsx new file mode 100644 index 0000000..82f3307 --- /dev/null +++ b/src/shared/components/AppButton.jsx @@ -0,0 +1,40 @@ +import React from 'react'; + +/* + * AppButton: 공통 버튼 컴포넌트 + * variant/disabled/isSelected 프롭으로 상태·스타일을 제어하고, 공통 Tailwind 유틸을 기반으로 일관된 UI를 제공 + * 기본 클릭 동작(onClick)과 네이티브 disabled 속성을 그대로 전달 + */ + +function AppButton({ + label, // 버튼에 표시할 텍스트 + onClick, // 클릭 핸들러 + disabled, // 비활성화 여부 HTML disalabled와 클래스 선택에 사용 + variant = 'default', // 버튼 스타일 종류: 'default', 'file', 'track' + isSelected = false, // 'track' variant에서 토글/선택 상태 표현용. 기본값 false +}) { + const baseClasses = 'w-full rounded-lg py-3 font-bold transition'; + // 공통 tailwind 유틸리티(풀 너비, 둥근 모서리, 패딩, 폰트 굵기, 트랜지션) 이후 각 variant별 클래스 추가 + + const variantClasses = { + // 객체 선언 시작: variant 값에 따른 클래스 문자열 매핑 + default: disabled ? 'bg-gray-300 text-white' : 'bg-blue-600 text-white', // 기본 버튼: 비활성화 시 회색, 활성화 시 파란색/흰색 + file: 'border border-blue-400 text-blue-400 bg-white', // 파일 업로드 버튼: 파란색 테두리/텍스트, 흰색 배경 + track: isSelected // 트랙 토글 버튼: 선택 상태에 따라 스타일 변경 + ? 'bg-blue-600 text-white border border-blue-600 rounded-full px-3 py-2' // 선택됨: 파란색 배경/테두리, 흰색 텍스트, 둥근 모서리, 패딩 + : 'bg-white text-blue-500 border border-blue-300 rounded-full px-3 py-2', // 선택 안됨: 흰색 배경, 파란색 텍스트/테두리, 둥근 모서리, 패딩 + }; + + return ( + // JSX 반환 시작 + + ); +} + +export default AppButton; // 컴포넌트 내보내기 diff --git a/src/shared/components/AppPrimarySection.jsx b/src/shared/components/AppPrimarySection.jsx new file mode 100644 index 0000000..106bbda --- /dev/null +++ b/src/shared/components/AppPrimarySection.jsx @@ -0,0 +1,12 @@ +const AppPrimarySection = ({ children }) => { + // children 프롭을 받아 내부 콘텐츠로 사용 + return ( +
+ {/* 전체 너비, 파란색 배경, 둥근 아래 모서리, 내부 여백, 세로 스택 레이아웃, 중앙 정렬, 약간의 음수 마진과 z-인덱스, 상대 위치 지정 */} + {children} + {/* 내부 콘텐츠 렌더링(슬롯 영역, 외부에서 전달한 임의의 콘텐츠를 그대로 감쌈) */} +
+ ); +}; + +export default AppPrimarySection; diff --git a/src/shared/components/AppTextField.jsx b/src/shared/components/AppTextField.jsx new file mode 100644 index 0000000..d74fbdb --- /dev/null +++ b/src/shared/components/AppTextField.jsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; // 함수형 컴포넌트에서 로컬 상태를 쓰기 위해 useState 훅을 임포트 +import { Eye, EyeOff, Search } from 'lucide-react'; // lucide-react에서 아이콘을 임포트 + +/** + * AppTextField: 프로젝트 공용 스타일의 컨트롤드 입력 필드 + * type='text' | 'password' | 'search'를 지원하며, 비밀번호 보기 토글과 에러 메시지 표시를 제공 + * value/onChange로 외부 상태를 제어하고, 접근성 강화를 위해 aria-* 속성 추가를 권장 + */ + +// 프롭으로 타입, 플레이스홀더, 값, 변경 핸들러, 에러 메시지를 받음 +// 기본 타입은 'text'로 설정 +function AppTextField({ type = 'text', placeholder, value, onChange, error }) { + // 비밀번호 표시 토글 상태 (초기값 false 숨김) + const [showPassword, setShowPassword] = useState(false); + // 입력 타입이 'password'인지 'search'인지 확인 + const isPassword = type === 'password'; + const isSearch = type === 'search'; + + return ( +
+
+ onChange(e.target.value)} // 상위에서 관리하는 값만 바구는 컨트롤드 패턴 + // 클래스: 전체 너비, 패딩, 테두리, 둥근 모서리, 포커스 시 테두리/링 효과 + className={` + w-full px-4 py-3 border rounded-md outline-none transition + border-blue-500 focus:border-blue-600 focus:ring-2 focus:ring-blue-200 + `} + /> + {/* 비밀번호 토글 아이콘 */} + {isPassword && ( +
setShowPassword(!showPassword)} + > + {/* showPassword 상태에 따라 Eye/EyeOff 토글 */} + {showPassword ? : } +
+ )} + {/* 검색 아이콘 (비활성화된 상태로 표시) */} + {isSearch && ( +
+ {' '} + {/* 우측 상단에 위치, 클릭 불가 */} + +
+ )} +
+ {/* 에러 메시지 (error 프롭이 존재할 때만 표시) */} + {/* 입력 하단에 빨간색 라벨로 에러 메시지 표시 */} + {error &&

{error}

} +
+ ); +} + +export default AppTextField; diff --git a/src/shared/components/ChipButton.jsx b/src/shared/components/ChipButton.jsx new file mode 100644 index 0000000..ad8d8b9 --- /dev/null +++ b/src/shared/components/ChipButton.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +/* + * ChipButton: 작은 pill 형태의 클릭 가능한 칩 컴포넌트 + * variant 프롭('blue' | 'yellow')으로 색상 테마를 바꾸고, onClick으로 상호작용을 처리 + * 목록 필터/태그 선택 등 보조 액션에 적합 + */ + +function ChipButton({ label, variant = 'blue', onClick }) { + // variant: 'blue' | 'yellow', label: 칩에 표시할 텍스트, onClick: 클릭 핸들러 + // 기본 스타일과 variant별 스타일을 조건에 따라 결합 + const baseStyle = 'px-1 py-1 rounded-full text-xs transition'; // 공통 스타일: 패딩, 둥근 모서리, 작은 텍스트, 트랜지션 + const variantStyle = // variant 값에 따른 배경색과 텍스트 색상 결정 + variant === 'yellow' + ? 'bg-yellow-300 text-blue-800' // 노란색 변형: 노란색 배경, 진한 파란색 텍스트 + : 'bg-blue-600 text-white'; // (기본) 파란색 변형: 파란색 배경, 흰색 텍스트 + + return ( + + ); +} + +export default ChipButton; diff --git a/src/shared/components/InfoLabel.jsx b/src/shared/components/InfoLabel.jsx new file mode 100644 index 0000000..4afb24c --- /dev/null +++ b/src/shared/components/InfoLabel.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +/** + * InfoLabel: 정보성 텍스트를 고정 폭(pill 형태)으로 표시하는 배지 컴포넌트. + * 파란 배경/흰 텍스트의 대비로 짧은 라벨을 강조하며, 리스트 메타/요약 정보에 적합하다. + * 내용은 text 프롭으로 전달된다. + */ + +// text 프롭 하나만 받아 고정 스타일의 라벨/뱃지 블록을 렌더링하는 단순 표시용 컴포넌트 +const InfoLabel = ({ text }) => { + return ( +
+ {/* 스타일: 파란 배경, 흰 텍스트, 중앙 정렬, 기본 크기, 패딩, 둥근 모서리, 고정 너비 */} + {text} {/* 전달된 텍스트를 그대로 표시 */} +
+ ); +}; + +export default InfoLabel; From c9a05cb470a1400f7ec2e40500637203379c62b5 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 6 Sep 2025 00:26:58 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=8A=A5=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EC=84=9D=20=EA=B0=9C=EC=84=A0,=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../track-management/components/CourseRow.jsx | 36 +++++++ .../components/TrackCourseList.jsx | 92 ++++++++++++++++ .../components/TrackDataUploadModal.jsx | 101 ++++++++++++++++++ .../components/TrackInitButton.jsx | 23 ++++ .../components/TrackIntroHeader.jsx | 51 +++++++++ .../components/TrackProgressHeader.jsx | 48 +++++++++ .../components/TrackStatus.jsx | 67 ++++++++++++ .../track-management/components/TrackTabs.jsx | 44 ++++++++ 8 files changed, 462 insertions(+) create mode 100644 src/features/track-management/components/CourseRow.jsx create mode 100644 src/features/track-management/components/TrackCourseList.jsx create mode 100644 src/features/track-management/components/TrackDataUploadModal.jsx create mode 100644 src/features/track-management/components/TrackInitButton.jsx create mode 100644 src/features/track-management/components/TrackIntroHeader.jsx create mode 100644 src/features/track-management/components/TrackProgressHeader.jsx create mode 100644 src/features/track-management/components/TrackStatus.jsx create mode 100644 src/features/track-management/components/TrackTabs.jsx 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 ( +
+

업로드된 이력이 없습니다.

+
+ ); + } + + // 구조분해로 완료/미완료 과목 배열을 추출, 기본값은 빈 배열 + const { completedCourses = [], remainingCourses = [] } = current; + + // 완료/미완료 배열을 합쳐서 상태 필드를 추가한 새 배열 생성 + const allCourses = [ + ...completedCourses.map((c) => ({ ...c, status: '이수' })), + ...remainingCourses.map((c) => ({ ...c, status: '미이수' })), + ]; + + 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 버튼 */} + + + {/* 상단 캐릭터 + 제목 */} + + + {/* 안내 텍스트 */} +
+

학사정보시스템 사이트에 로그인합니다.

+

+ 왼쪽 메뉴 바에서 {' '} + 로 + 이동합니다. +

+

+ 성적 엑셀다운로드 버튼을 클릭해 다운로드한 후, 해당 엑셀 파일을 + 업로드합니다. +

+
+ + {/* 파일 선택 및 업로드 */} +
+ {' '} + {/* 세로 방향, 중앙 정렬, 요소 간격 */} + +
+
+ ); +}; + +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; From dfded17fd96069cc0e2c582b17d83882d7bd0cc6 Mon Sep 17 00:00:00 2001 From: Yeryeong Kang Date: Sat, 6 Sep 2025 00:34:45 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20pages=20=EA=B2=BD=EB=A1=9C/?= =?UTF-8?q?=EC=9E=84=ED=8F=AC=ED=8A=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/index.jsx | 12 ++- .../track-management/api/userDataService.js | 23 +++++ .../track-management/api/userTrackService.js | 20 ++++ src/pages/home/index.jsx | 93 ++++++++++-------- src/pages/info/index.jsx | 24 +++-- src/pages/login/index.jsx | 97 ++++++++++++------- src/pages/my/index.jsx | 26 ++--- src/pages/splash/index.jsx | 36 ++++--- 8 files changed, 210 insertions(+), 121 deletions(-) create mode 100644 src/features/track-management/api/userDataService.js create mode 100644 src/features/track-management/api/userTrackService.js 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 &&