diff --git a/frontend/src/App.js b/frontend/src/App.js index 13c568a..0e5c892 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -10,6 +10,8 @@ import PiroCheckMain from './pages/pirocheck/PIroCheckMain'; import Attendance from './pages/pirocheck/attendance/Attendance' import Assignment from './pages/pirocheck/assignment/Assignment'; import Deposit from './pages/pirocheck/deposit/Deposit'; +import StudentList from './pages/pirocheck/students/StudentList'; +import StudentDetail from './pages/pirocheck/students/StudentDetail'; function App() { return ( @@ -35,6 +37,8 @@ function App() { }/> }/> }/> + }/> + }/> diff --git a/frontend/src/assets/images/icon_arrow_right.svg b/frontend/src/assets/images/icon_arrow_right.svg new file mode 100644 index 0000000..c5aa6f6 --- /dev/null +++ b/frontend/src/assets/images/icon_arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icon_togle1.svg b/frontend/src/assets/images/icon_togle1.svg new file mode 100644 index 0000000..3d85458 --- /dev/null +++ b/frontend/src/assets/images/icon_togle1.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icon_togle2.svg b/frontend/src/assets/images/icon_togle2.svg new file mode 100644 index 0000000..9aa266f --- /dev/null +++ b/frontend/src/assets/images/icon_togle2.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/logo2.svg b/frontend/src/assets/images/logo2.svg new file mode 100644 index 0000000..d667deb --- /dev/null +++ b/frontend/src/assets/images/logo2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/images/profile.svg b/frontend/src/assets/images/profile.svg new file mode 100644 index 0000000..84191cb --- /dev/null +++ b/frontend/src/assets/images/profile.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/src/pages/pirocheck/students/StudentDetail.js b/frontend/src/pages/pirocheck/students/StudentDetail.js new file mode 100644 index 0000000..6c31628 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentDetail.js @@ -0,0 +1,294 @@ +import { useState, useEffect } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import styles from './StudentDetail.module.css'; +import ProfileImg from '../../../assets/images/profile.svg'; +import Logo2 from '../../../assets/images/logo2.svg'; +import Toggle1 from '../../../assets/images/icon_togle1.svg'; +import Toggle2 from '../../../assets/images/icon_togle2.svg'; + +const IS_MOCK = true; + +const MOCK_DETAIL = { + deposit: { amount: 100000, ascentDefence: 10000 }, + weeks: [ + { + week: 1, + days: [ + { + day: 'TUESDAY', + sessionDate: '2026-06-24', + sessionTitles: 'HTML/CSS 기초, Git 기초', + attendances: [ + { attendanceId: 1, attendanceOrder: '1차', attended: true }, + { attendanceId: 2, attendanceOrder: '2차', attended: true }, + { attendanceId: 3, attendanceOrder: '3차', attended: true }, + ], + assignments: [ + { assignmentItemId: 1, title: '코딩앵무 클론 코딩', submitted: 'SUBMITTED' }, + { assignmentItemId: 2, title: '피로그래밍 페이지 클론 코딩', submitted: 'SUBMITTED' }, + ], + }, + { + day: 'THURSDAY', + sessionDate: '2026-06-26', + sessionTitles: 'JS 기초, JS 심화', + attendances: [ + { attendanceId: 4, attendanceOrder: '1차', attended: false }, + { attendanceId: 5, attendanceOrder: '2차', attended: false }, + { attendanceId: 6, attendanceOrder: '3차', attended: false }, + ], + assignments: [ + { assignmentItemId: 3, title: '코딩앵무 클론 코딩', submitted: 'NOT_SUBMITTED' }, + ], + }, + { + day: 'SATURDAY', + sessionDate: '2026-06-28', + sessionTitles: 'DB 개론', + attendances: [ + { attendanceId: 7, attendanceOrder: '1차', attended: false }, + { attendanceId: 8, attendanceOrder: '2차', attended: false }, + { attendanceId: 9, attendanceOrder: '3차', attended: false }, + ], + assignments: [], + }, + ], + }, + { week: 2, days: [] }, + { week: 3, days: [] }, + { week: 4, days: [] }, + { week: 5, days: [] }, + ], +}; + +const dayLabel = { TUESDAY: 'TUE', THURSDAY: 'THU', SATURDAY: 'SAT' }; +const statusOptions = ['SUBMITTED', 'LATE', 'NOT_SUBMITTED']; +const statusLabel = { SUBMITTED: '성공', LATE: '미달', NOT_SUBMITTED: '실패' }; + +// 커리큘럼 데이터에서 날짜별 세션 제목 추출 +function extractSessionTitles(curriculums, sessionDate) { + const day = curriculums.find(c => c.sessionDate === sessionDate); + if (!day || !day.sessions) return ''; + return day.sessions.map(s => s.title).join(', '); +} + +function WeekBlock({ weekData, onChange }) { + const [isOpen, setIsOpen] = useState(false); + const [openDays, setOpenDays] = useState({}); + + const toggleDay = (day) => { + setOpenDays(prev => ({ ...prev, [day]: !prev[day] })); + }; + + return ( +
+
setIsOpen(p => !p)}> +
+ logo + WEEK {weekData.week} +
+ toggle +
+ + {isOpen && ( +
+ {weekData.days.length === 0 && ( +
데이터가 없습니다.
+ )} + {weekData.days.map((day, i) => ( +
+
toggleDay(day.day)}> +
+ {dayLabel[day.day]} + {day.sessionTitles || day.sessionDate} +
+ toggle +
+ + {openDays[day.day] && ( +
+ {/* 출석 */} +
+ 출석 +
+ {day.attendances.map((att, j) => ( +
+ {att.attendanceOrder} + +
+ ))} +
+
+ + {/* 과제 */} + {day.assignments.length > 0 && ( +
+ 과제 +
+ {day.assignments.map((asg, j) => ( +
+ {asg.title} + +
+ ))} +
+
+ )} + + +
+ )} + + {i < weekData.days.length - 1 &&
} +
+ ))} +
+ )} +
+ ); +} + +function StudentDetail() { + const { userId } = useParams(); + const location = useLocation(); + const studentName = location.state?.name || '수강생'; + + const [data, setData] = useState(null); + const [defence, setDefence] = useState(''); + + useEffect(() => { + if (IS_MOCK) { + setData(MOCK_DETAIL); + setDefence(MOCK_DETAIL.deposit.ascentDefence.toString()); + return; + } + + const fetchData = async () => { + // TODO: GET /api/admin/{userId}/deposit/view + // TODO: GET /api/admin/admin/student/{userId}/status/{week} (1~5주차) + + // 커리큘럼에서 세션 제목 가져오기 + const curriculumRes = await fetch('/api/curriculums'); + const curriculums = await curriculumRes.json(); + + // weeks 데이터에 sessionTitles 추가 + // const mergedWeeks = weeks.map(w => ({ + // ...w, + // days: w.days.map(d => ({ + // ...d, + // sessionTitles: extractSessionTitles(curriculums, d.sessionDate), + // })) + // })); + }; + fetchData(); + }, [userId]); + + const handleSaveDefence = async () => { + if (IS_MOCK) { alert('저장됨 (임시)'); return; } + // TODO: PUT /api/admin/{userId}/deposit-defend + }; + + const handleStatusChange = (type, week, day, id, value) => { + setData(prev => { + const newWeeks = prev.weeks.map(w => { + if (w.week !== week) return w; + return { + ...w, + days: w.days.map(d => { + if (d.day !== day) return d; + if (type === 'attendance') { + return { + ...d, + attendances: d.attendances.map(a => + a.attendanceId === id ? { ...a, attended: value } : a + ), + }; + } else { + return { + ...d, + assignments: d.assignments.map(a => + a.assignmentItemId === id ? { ...a, submitted: value } : a + ), + }; + } + }), + }; + }); + return { ...prev, weeks: newWeeks }; + }); + }; + + const handleSaveAll = async () => { + if (IS_MOCK) { alert('전체 저장됨 (임시)'); return; } + // TODO: PATCH /api/admin/users/{userId}/weeks/{week} 주차별로 호출 + }; + + if (!data) return null; + + return ( +
+ {IS_MOCK && ( +
+ ⚠️ 현재 임시 데이터로 표시 중입니다. +
+ )} + +
+
+ profile +
{studentName}
+
+ +
+
+
잔여 보증금
+
{data.deposit.amount.toLocaleString()}원
+
+
+
보증금 방어권
+
+ setDefence(e.target.value)} + /> + + +
+
+
+ + {data.weeks.map((w, i) => ( + + ))} + + +
+
+ ); +} + +export default StudentDetail; \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/students/StudentDetail.module.css b/frontend/src/pages/pirocheck/students/StudentDetail.module.css new file mode 100644 index 0000000..e073df1 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentDetail.module.css @@ -0,0 +1,343 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 20px; + min-height: calc(100vh - 100px); + background: var(--black); +} + +.mockBanner { + background: #5a3e00; + color: #ffd166; + font-family: var(--font-main); + font-size: 0.85rem; + padding: 10px 20px; + border-radius: 8px; + margin-bottom: 20px; + width: 480px; + box-sizing: border-box; + text-align: center; +} + +.card { + width: 480px; + background: var(--gray600); + border-radius: 20px; + padding: 40px 40px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* 프로필 */ +.profileArea { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.profileImg { + width: 100px; + height: 100px; + margin-left: 2px; + object-fit: cover; +} + +.profileName { + font-family: var(--font-main); + font-size: 1.8rem; + font-weight: 700; + color: var(--dark); +} + +/* 보증금 */ +.depositRow { + display: flex; + gap: 12px; +} + +.depositBoxGreen { + flex: 1; + background: var(--light); + border-radius: 10px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.depositBoxGray { + flex: 1; + background: var(--pale); + border-radius: 10px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.depositLabel { + font-family: var(--font-main); + font-size: 1.1rem; + font-weight: 600; + color: var(--gray600); +} + +.depositValue { + font-family: var(--font-main); + font-size: 1.2rem; + font-weight: 700; + color: var(--black); +} + +.depositEditRow { + display: flex; + align-items: center; + gap: 4px; +} + +.defenceInput { + width: 80px; + border: none; + background: transparent; + font-family: var(--font-main); + font-size: 1.2rem; + font-weight: 700; + color: var(--black); + outline: none; +} + +.won { + font-family: var(--font-main); + font-size: 1.2rem; + font-weight: 700; + color: var(--black); +} + +.saveBtn { + padding: 4px 10px; + background: var(--gray200); + border: none; + border-radius: 6px; + color: var(--gray600); + font-family: var(--font-main); + font-size: 0.75rem; + font-weight: 550; + cursor: pointer; + margin-left: 30px; +} + +.saveBtn:hover { background: var(--gray600); color: var(--white); transition: all ease-in-out 0.2s; } + +/* 주차 블록 */ +.weekBlock { + background: var(--gray600); + border-radius: 12px; + overflow: hidden; +} + +.weekHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + cursor: pointer; + color: var(--white); +} + +.weekLeft { + display: flex; + align-items: center; + gap: 10px; +} + +.weekLogo { + width: 30px; + height: 30px; + object-fit: contain; +} + +.weekLabel { + font-family: var(--font-main); + font-size: 1.3rem; + font-weight: 600; + color: var(--white); + margin-left: 5px; +} + +/* 토글 애니메이션 */ +.toggleIcon { + width: 15px; + height: 15px; + transition: transform 0.3s ease; +} + +.toggleIcon2 { + width: 20px; + height: 20px; + transition: transform 0.3s ease; + vertical-align: middle; +} + +.toggleOpen { + transform: rotate(180deg); +} + +.weekBody { + padding: 0 0 16px; + margin-left: 45px; + margin-right: 20px; +} + +.empty { + color: #aaa; + font-family: var(--font-main); + font-size: 1rem; + text-align: center; + padding: 12px 0; +} + +/* 요일 블록 */ + +.dayHeader { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + padding: 6px 0; +} + +.dayLeft { + display: flex; + align-items: center; + gap: 10px; +} + +.dayLabel { + color: var(--dark); + font-family: var(--font-main); + font-size: 1.2rem; + font-weight: 650; +} + +.sessionDate { + color: var(--light); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 500; +} + +.dayBody { + padding: 5px 16px 0 5px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* 출석/과제 그룹 - 레이블 옆에 아이템들 */ +.statusGroup { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.sectionLabel { + color: var(--white); + font-family: var(--font-main); + font-size: 1.1rem; + font-weight: 500; + min-width: 30px; + padding-top: 4px; +} + +.statusItems { + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; +} + +.statusItem { + display: flex; + align-items: center; + justify-content: space-between; +} + +.itemLabel { + color: var(--white); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 450; + padding-top: 3px; +} + +.select { + padding: 3px 28px 3px 12px; + width: 80px; + box-sizing: border-box; + + background: var(--gray600); + border: 1px solid var(--white); + border-radius: 6px; + color: var(--white); + font-family: var(--font-main); + font-size: 0.85rem; + cursor: pointer; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + + background-position: right 10px center; +} + +.select::-ms-expand { + display: none; +} + +.saveWeekBtn { + margin: 20px 0; + padding: 5px 30px; + background: transparent; + border: 1.5px solid var(--main); + border-radius: 10px; + color: var(--main); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + display: block; + margin-left: auto; + margin-right: auto; +} + +.saveWeekBtn:hover { background: var(--main); color: var(--white); transition: all ease-in-out 0.2s; } + +.divider { + border: none; + border-top: 1px solid #444; + margin: 10px 0; +} + +/* 전체 저장 */ +.saveAllBtn { + width: 60%; + margin: 0 auto; + padding: 10px 0; + background: var(--dark); + border: none; + border-radius: 10px; + color: var(--white); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; +} + +.saveAllBtn:hover { background: var(--main); transition: all ease-in-out 0.2s; } \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/students/StudentList.js b/frontend/src/pages/pirocheck/students/StudentList.js new file mode 100644 index 0000000..98790c8 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentList.js @@ -0,0 +1,79 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './StudentList.module.css'; +import ArrowRight from '../../../assets/images/icon_arrow_right.svg'; + +const MOCK_STUDENTS = [ + { userId: 1, name: '김피로' }, + { userId: 2, name: '이피로' }, + { userId: 3, name: '박피로' }, + { userId: 4, name: '최피로' }, + { userId: 5, name: '정피로' }, +]; + +const IS_MOCK = true; + +function StudentList() { + const navigate = useNavigate(); + const [students, setStudents] = useState([]); + const [search, setSearch] = useState(''); + + const fetchStudents = async (keyword = '') => { + if (IS_MOCK) { + if (keyword) { + setStudents(MOCK_STUDENTS.filter(s => s.name.includes(keyword))); + } else { + setStudents(MOCK_STUDENTS); + } + return; + } + const url = keyword + ? `/api/admin/studentlist/search?name=${keyword}` + : '/api/admin/studentlist'; + const res = await fetch(url); + const data = await res.json(); + setStudents(data); + }; + + useEffect(() => { fetchStudents(); }, []); + + const handleSearch = () => fetchStudents(search); + + return ( +
+ {IS_MOCK && ( +
+ ⚠️ 현재 임시 데이터로 표시 중입니다. +
+ )} + +
PIROGRAMMER
+ +
+ setSearch(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleSearch()} + /> + +
+ +
+ {students.map((s, i) => ( + + ))} +
+
+ ); +} + +export default StudentList; \ No newline at end of file diff --git a/frontend/src/pages/pirocheck/students/StudentList.module.css b/frontend/src/pages/pirocheck/students/StudentList.module.css new file mode 100644 index 0000000..4eea6e6 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentList.module.css @@ -0,0 +1,103 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + padding: 60px 20px; + min-height: calc(100vh - 100px); + background: var(--black); +} + +.mockBanner { + background: #5a3e00; + color: #ffd166; + font-family: var(--font-main); + font-size: 0.85rem; + padding: 10px 20px; + border-radius: 8px; + margin-bottom: 20px; + width: 480px; + box-sizing: border-box; + text-align: center; +} + +.title { + font-family: var(--font-title); + font-size: 3rem; + font-weight: 800; + color: var(--main); + margin-bottom: 36px; + letter-spacing: 0; +} + +.searchRow { + display: flex; + align-items: center; + background: var(--gray600); + border-radius: 10px; + overflow: hidden; + width: 480px; + margin-bottom: 30px; + padding: 6px 6px 6px 20px; +} + +.searchInput { + flex: 1; + background: transparent; + border: none; + outline: none; + font-family: var(--font-main); + font-size: 1rem; + color: var(--white); +} + +.searchInput::placeholder { color: var(--gray200); } + +.searchBtn { + padding: 7px 15px; + background: var(--dark); + border: none; + border-radius: 10px; + color: var(--white); + font-family: var(--font-main); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; +} + +.searchBtn:hover { + background: var(--main); + transition: all ease-in-out 0.2s; +} + +.list { + width: 450px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.studentItem { + display: flex; + align-items: center; + justify-content: space-between; + background: var(--gray600); + border: 1px solid var(--gray50); + border-radius: 10px; + padding: 7px 15px 7px 20px; + cursor: pointer; + transition: background 0.2s; + width: 100%; +} + +.studentItem:hover { background: #4a4a4a; } + +.studentName { + font-family: var(--font-main); + font-size: 1rem; + font-weight: 500; + color: var(--white); +} + +.arrow { + width: 25px; height: 25px; +} \ No newline at end of file