From 787a19b6e7644b985d404fda9b5bb77848e2d3f6 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 24 May 2026 00:18:53 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[Feat]=20=EC=88=98=EA=B0=95=EC=83=9D=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20UI=20=EB=BC=88=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.js | 4 + .../pages/pirocheck/students/StudentDetail.js | 254 ++++++++++++++++ .../students/StudentDetail.module.css | 275 ++++++++++++++++++ .../pages/pirocheck/students/StudentList.js | 78 +++++ .../pirocheck/students/StudentList.module.css | 100 +++++++ 5 files changed, 711 insertions(+) create mode 100644 frontend/src/pages/pirocheck/students/StudentDetail.js create mode 100644 frontend/src/pages/pirocheck/students/StudentDetail.module.css create mode 100644 frontend/src/pages/pirocheck/students/StudentList.js create mode 100644 frontend/src/pages/pirocheck/students/StudentList.module.css 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/pages/pirocheck/students/StudentDetail.js b/frontend/src/pages/pirocheck/students/StudentDetail.js new file mode 100644 index 0000000..65c666d --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentDetail.js @@ -0,0 +1,254 @@ +import { useState, useEffect } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import styles from './StudentDetail.module.css'; + +const IS_MOCK = true; + +const MOCK_DETAIL = { + deposit: { amount: 100000, ascentDefence: 10000 }, + weeks: [ + { + week: 1, + days: [ + { + day: 'TUESDAY', + sessionDate: '2026-06-24', + 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', + 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', + 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 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)}> +
+ {/* 주차 로고는 SVG로 교체 예정 */} + + WEEK {weekData.week} +
+ {isOpen ? '▲' : '▼'} +
+ + {isOpen && ( +
+ {weekData.days.length === 0 && ( +
데이터가 없습니다.
+ )} + {weekData.days.map((day, i) => ( +
+
toggleDay(day.day)}> +
+ {dayLabel[day.day]} + {day.sessionDate} +
+ {openDays[day.day] ? '∧' : '∨'} +
+ + {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; + } + // TODO: GET /api/admin/{userId}/deposit/view + // TODO: GET /api/admin/admin/student/{userId}/status/{week} (1~5주차) + }, [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 && ( +
+ ⚠️ 현재 임시 데이터로 표시 중입니다. +
+ )} + +
+ {/* 프로필 */} +
+ {/* 프로필 이미지는 SVG로 교체 예정 */} +
👤
+
{studentName}
+
+ + {/* 보증금 */} +
+
+
잔여 보증금
+
{data.deposit.amount.toLocaleString()}원
+
+
+
보증금 방어권
+
+ setDefence(e.target.value)} + /> + + +
+
+
+ + {/* 주차별 현황 */} + {data.weeks.map((w, i) => ( + + ))} + + +
+
+ ); +} + +export default StudentDetail; 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..42c608b --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentDetail.module.css @@ -0,0 +1,275 @@ +.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: #3a3a3a; + border-radius: 20px; + padding: 40px 32px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* 프로필 */ +.profileArea { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.profileImgPlaceholder { + font-size: 4rem; + width: 90px; + height: 90px; + background: #555; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +.profileName { + font-family: var(--font-main); + font-size: 1.4rem; + font-weight: 700; + color: var(--white); +} + +/* 보증금 */ +.depositRow { + display: flex; + gap: 12px; +} + +.depositBox { + flex: 1; + background: #e8f5e9; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.depositLabel { + font-family: var(--font-main); + font-size: 0.85rem; + color: #555; +} + +.depositValue { + font-family: var(--font-main); + font-size: 1.1rem; + font-weight: 700; + color: #333; +} + +.depositEditRow { + display: flex; + align-items: center; + gap: 4px; +} + +.defenceInput { + width: 80px; + border: none; + background: transparent; + font-family: var(--font-main); + font-size: 1.1rem; + font-weight: 700; + color: #333; + outline: none; +} + +.won { + font-family: var(--font-main); + font-size: 1rem; + color: #333; +} + +.saveBtn { + padding: 4px 10px; + background: #555; + border: none; + border-radius: 6px; + color: var(--white); + font-family: var(--font-main); + font-size: 0.75rem; + font-weight: 700; + cursor: pointer; +} + +.saveBtn:hover { background: #666; } + +/* 주차 블록 */ +.weekBlock { + background: #2a2a2a; + 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; +} + +.weekLogoPlaceholder { + color: var(--main); + font-size: 1.2rem; +} + +.weekLabel { + font-family: var(--font-main); + font-size: 1rem; + font-weight: 600; + color: var(--white); +} + +.weekBody { + padding: 0 20px 16px; +} + +.empty { + color: #aaa; + font-family: var(--font-main); + font-size: 0.9rem; + text-align: center; + padding: 12px 0; +} + +/* 요일 블록 */ +.dayBlock { margin-top: 10px; } + +.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(--main); + font-family: var(--font-title); + font-size: 0.95rem; + font-weight: 700; +} + +.sessionDate { + color: #aaa; + font-family: var(--font-main); + font-size: 0.85rem; +} + +.dayBody { + padding: 8px 0 0 8px; +} + +.sectionLabel { + color: #aaa; + font-family: var(--font-main); + font-size: 0.8rem; + margin: 8px 0 4px; +} + +.statusRow { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; +} + +.itemLabel { + color: var(--white); + font-family: var(--font-main); + font-size: 0.9rem; +} + +.select { + padding: 4px 10px; + background: #444; + border: none; + border-radius: 6px; + color: var(--white); + font-family: var(--font-main); + font-size: 0.85rem; + cursor: pointer; +} + +.saveWeekBtn { + margin-top: 12px; + padding: 8px 24px; + background: transparent; + border: 1.5px solid var(--main); + border-radius: 50px; + color: var(--main); + font-family: var(--font-main); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + display: block; + margin-left: auto; + margin-right: auto; +} + +.saveWeekBtn:hover { background: var(--main); color: var(--black); } + +.divider { + border: none; + border-top: 1px solid #444; + margin: 10px 0; +} + +/* 전체 저장 */ +.saveAllBtn { + padding: 14px 0; + background: var(--main); + border: none; + border-radius: 50px; + color: var(--black); + font-family: var(--font-main); + font-size: 1rem; + font-weight: 700; + cursor: pointer; + width: 100%; + transition: opacity 0.2s; +} + +.saveAllBtn:hover { opacity: 0.85; } \ 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..2ce0ae1 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentList.js @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './StudentList.module.css'; + +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..0289648 --- /dev/null +++ b/frontend/src/pages/pirocheck/students/StudentList.module.css @@ -0,0 +1,100 @@ +.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: #f5f5f5; + border-radius: 50px; + overflow: hidden; + width: 480px; + margin-bottom: 30px; + padding: 4px 4px 4px 20px; +} + +.searchInput { + flex: 1; + background: transparent; + border: none; + outline: none; + font-family: var(--font-main); + font-size: 1rem; + color: #333; +} + +.searchInput::placeholder { color: #aaa; } + +.searchBtn { + padding: 10px 24px; + background: var(--main); + border: none; + border-radius: 50px; + color: var(--black); + font-family: var(--font-main); + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; +} + +.list { + width: 480px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.studentItem { + display: flex; + align-items: center; + justify-content: space-between; + background: #3a3a3a; + border: none; + border-radius: 12px; + padding: 20px 24px; + 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 { + color: var(--white); + font-size: 1.4rem; + opacity: 0.6; +} \ No newline at end of file From 46d55573f3c2b9d0d5ebd0331dbfff753fe6b928 Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 24 May 2026 10:29:52 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[Feat]=20Studentlist=20css=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/images/icon_arrow_right.svg | 3 ++ frontend/src/assets/images/icon_togle1.svg | 3 ++ frontend/src/assets/images/icon_togle2.svg | 3 ++ frontend/src/assets/images/logo2.svg | 6 ++++ frontend/src/assets/images/profile.svg | 9 +++++ .../pirocheck/students/StudentList.module.css | 35 ++++++++++--------- 6 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 frontend/src/assets/images/icon_arrow_right.svg create mode 100644 frontend/src/assets/images/icon_togle1.svg create mode 100644 frontend/src/assets/images/icon_togle2.svg create mode 100644 frontend/src/assets/images/logo2.svg create mode 100644 frontend/src/assets/images/profile.svg 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/StudentList.module.css b/frontend/src/pages/pirocheck/students/StudentList.module.css index 0289648..abba372 100644 --- a/frontend/src/pages/pirocheck/students/StudentList.module.css +++ b/frontend/src/pages/pirocheck/students/StudentList.module.css @@ -32,12 +32,12 @@ .searchRow { display: flex; align-items: center; - background: #f5f5f5; - border-radius: 50px; + background: var(--gray600); + border-radius: 10px; overflow: hidden; width: 480px; margin-bottom: 30px; - padding: 4px 4px 4px 20px; + padding: 6px 6px 6px 20px; } .searchInput { @@ -47,25 +47,25 @@ outline: none; font-family: var(--font-main); font-size: 1rem; - color: #333; + color: var(--white); } -.searchInput::placeholder { color: #aaa; } +.searchInput::placeholder { color: var(--gray200); } .searchBtn { - padding: 10px 24px; - background: var(--main); + padding: 7px 15px; + background: var(--dark); border: none; - border-radius: 50px; - color: var(--black); + border-radius: 10px; + color: var(--white); font-family: var(--font-main); font-size: 0.95rem; - font-weight: 700; + font-weight: 600; cursor: pointer; } .list { - width: 480px; + width: 450px; display: flex; flex-direction: column; gap: 12px; @@ -75,10 +75,10 @@ display: flex; align-items: center; justify-content: space-between; - background: #3a3a3a; - border: none; - border-radius: 12px; - padding: 20px 24px; + background: var(--gray600); + border: 1px solid var(--gray50); + border-radius: 10px; + padding: 7px 15px 7px 20px; cursor: pointer; transition: background 0.2s; width: 100%; @@ -95,6 +95,7 @@ .arrow { color: var(--white); - font-size: 1.4rem; - opacity: 0.6; + font-size: 1.6rem; + opacity: 0.7; + margin-bottom: 1px; } \ No newline at end of file From eb927018a67c99377900c89aee5400ce70fb4c9f Mon Sep 17 00:00:00 2001 From: plumbestie Date: Sun, 24 May 2026 11:35:03 +0900 Subject: [PATCH 3/3] [Feat] StudentDetail UI --- .../pages/pirocheck/students/StudentDetail.js | 128 +++++++---- .../students/StudentDetail.module.css | 200 ++++++++++++------ .../pages/pirocheck/students/StudentList.js | 3 +- .../pirocheck/students/StudentList.module.css | 10 +- 4 files changed, 226 insertions(+), 115 deletions(-) diff --git a/frontend/src/pages/pirocheck/students/StudentDetail.js b/frontend/src/pages/pirocheck/students/StudentDetail.js index 65c666d..6c31628 100644 --- a/frontend/src/pages/pirocheck/students/StudentDetail.js +++ b/frontend/src/pages/pirocheck/students/StudentDetail.js @@ -1,6 +1,10 @@ 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; @@ -13,6 +17,7 @@ const MOCK_DETAIL = { { day: 'TUESDAY', sessionDate: '2026-06-24', + sessionTitles: 'HTML/CSS 기초, Git 기초', attendances: [ { attendanceId: 1, attendanceOrder: '1차', attended: true }, { attendanceId: 2, attendanceOrder: '2차', attended: true }, @@ -26,6 +31,7 @@ const MOCK_DETAIL = { { day: 'THURSDAY', sessionDate: '2026-06-26', + sessionTitles: 'JS 기초, JS 심화', attendances: [ { attendanceId: 4, attendanceOrder: '1차', attended: false }, { attendanceId: 5, attendanceOrder: '2차', attended: false }, @@ -38,6 +44,7 @@ const MOCK_DETAIL = { { day: 'SATURDAY', sessionDate: '2026-06-28', + sessionTitles: 'DB 개론', attendances: [ { attendanceId: 7, attendanceOrder: '1차', attended: false }, { attendanceId: 8, attendanceOrder: '2차', attended: false }, @@ -56,7 +63,14 @@ const MOCK_DETAIL = { const dayLabel = { TUESDAY: 'TUE', THURSDAY: 'THU', SATURDAY: 'SAT' }; const statusOptions = ['SUBMITTED', 'LATE', 'NOT_SUBMITTED']; -const statusLabel = { 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); @@ -70,11 +84,14 @@ function WeekBlock({ weekData, onChange }) {
setIsOpen(p => !p)}>
- {/* 주차 로고는 SVG로 교체 예정 */} - + logo WEEK {weekData.week}
- {isOpen ? '▲' : '▼'} + toggle
{isOpen && ( @@ -87,48 +104,58 @@ function WeekBlock({ weekData, onChange }) {
toggleDay(day.day)}>
{dayLabel[day.day]} - {day.sessionDate} + {day.sessionTitles || day.sessionDate}
- {openDays[day.day] ? '∧' : '∨'} + toggle
{openDays[day.day] && (
{/* 출석 */} -
출석
- {day.attendances.map((att, j) => ( -
- {att.attendanceOrder} - -
- ))} - - {/* 과제 */} - {day.assignments.length > 0 && ( - <> -
과제
- {day.assignments.map((asg, j) => ( -
- {asg.title} +
+ 출석 +
+ {day.attendances.map((att, j) => ( +
+ {att.attendanceOrder}
))} - +
+
+ + {/* 과제 */} + {day.assignments.length > 0 && ( +
+ 과제 +
+ {day.assignments.map((asg, j) => ( +
+ {asg.title} + +
+ ))} +
+
)} @@ -158,8 +185,25 @@ function StudentDetail() { setDefence(MOCK_DETAIL.deposit.ascentDefence.toString()); return; } - // TODO: GET /api/admin/{userId}/deposit/view - // TODO: GET /api/admin/admin/student/{userId}/status/{week} (1~5주차) + + 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 () => { @@ -213,20 +257,17 @@ function StudentDetail() { )}
- {/* 프로필 */}
- {/* 프로필 이미지는 SVG로 교체 예정 */} -
👤
+ profile
{studentName}
- {/* 보증금 */}
-
+
잔여 보증금
{data.deposit.amount.toLocaleString()}원
-
+
보증금 방어권
- {/* 주차별 현황 */} {data.weeks.map((w, i) => ( ))} @@ -251,4 +291,4 @@ function StudentDetail() { ); } -export default StudentDetail; +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 index 42c608b..e073df1 100644 --- a/frontend/src/pages/pirocheck/students/StudentDetail.module.css +++ b/frontend/src/pages/pirocheck/students/StudentDetail.module.css @@ -22,9 +22,9 @@ .card { width: 480px; - background: #3a3a3a; + background: var(--gray600); border-radius: 20px; - padding: 40px 32px; + padding: 40px 40px; display: flex; flex-direction: column; gap: 20px; @@ -38,22 +38,18 @@ gap: 12px; } -.profileImgPlaceholder { - font-size: 4rem; - width: 90px; - height: 90px; - background: #555; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; +.profileImg { + width: 100px; + height: 100px; + margin-left: 2px; + object-fit: cover; } .profileName { font-family: var(--font-main); - font-size: 1.4rem; + font-size: 1.8rem; font-weight: 700; - color: var(--white); + color: var(--dark); } /* 보증금 */ @@ -62,11 +58,21 @@ gap: 12px; } -.depositBox { +.depositBoxGreen { flex: 1; - background: #e8f5e9; - border-radius: 12px; - padding: 16px; + 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; @@ -74,15 +80,16 @@ .depositLabel { font-family: var(--font-main); - font-size: 0.85rem; - color: #555; + font-size: 1.1rem; + font-weight: 600; + color: var(--gray600); } .depositValue { font-family: var(--font-main); - font-size: 1.1rem; + font-size: 1.2rem; font-weight: 700; - color: #333; + color: var(--black); } .depositEditRow { @@ -96,35 +103,37 @@ border: none; background: transparent; font-family: var(--font-main); - font-size: 1.1rem; + font-size: 1.2rem; font-weight: 700; - color: #333; + color: var(--black); outline: none; } .won { font-family: var(--font-main); - font-size: 1rem; - color: #333; + font-size: 1.2rem; + font-weight: 700; + color: var(--black); } .saveBtn { padding: 4px 10px; - background: #555; + background: var(--gray200); border: none; border-radius: 6px; - color: var(--white); + color: var(--gray600); font-family: var(--font-main); font-size: 0.75rem; - font-weight: 700; + font-weight: 550; cursor: pointer; + margin-left: 30px; } -.saveBtn:hover { background: #666; } +.saveBtn:hover { background: var(--gray600); color: var(--white); transition: all ease-in-out 0.2s; } /* 주차 블록 */ .weekBlock { - background: #2a2a2a; + background: var(--gray600); border-radius: 12px; overflow: hidden; } @@ -144,32 +153,53 @@ gap: 10px; } -.weekLogoPlaceholder { - color: var(--main); - font-size: 1.2rem; +.weekLogo { + width: 30px; + height: 30px; + object-fit: contain; } .weekLabel { font-family: var(--font-main); - font-size: 1rem; + 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 20px 16px; + padding: 0 0 16px; + margin-left: 45px; + margin-right: 20px; } .empty { color: #aaa; font-family: var(--font-main); - font-size: 0.9rem; + font-size: 1rem; text-align: center; padding: 12px 0; } /* 요일 블록 */ -.dayBlock { margin-top: 10px; } .dayHeader { display: flex; @@ -186,62 +216,99 @@ } .dayLabel { - color: var(--main); - font-family: var(--font-title); - font-size: 0.95rem; - font-weight: 700; + color: var(--dark); + font-family: var(--font-main); + font-size: 1.2rem; + font-weight: 650; } .sessionDate { - color: #aaa; + color: var(--light); font-family: var(--font-main); - font-size: 0.85rem; + font-size: 1rem; + font-weight: 500; } .dayBody { - padding: 8px 0 0 8px; + padding: 5px 16px 0 5px; + display: flex; + flex-direction: column; + gap: 8px; +} + +/* 출석/과제 그룹 - 레이블 옆에 아이템들 */ +.statusGroup { + display: flex; + align-items: flex-start; + gap: 16px; } .sectionLabel { - color: #aaa; + color: var(--white); font-family: var(--font-main); - font-size: 0.8rem; - margin: 8px 0 4px; + font-size: 1.1rem; + font-weight: 500; + min-width: 30px; + padding-top: 4px; +} + +.statusItems { + flex: 1; + display: flex; + flex-direction: column; + gap: 10px; } -.statusRow { +.statusItem { display: flex; align-items: center; justify-content: space-between; - padding: 4px 0; } .itemLabel { color: var(--white); font-family: var(--font-main); - font-size: 0.9rem; + font-size: 1rem; + font-weight: 450; + padding-top: 3px; } .select { - padding: 4px 10px; - background: #444; - border: none; + 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; + 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-top: 12px; - padding: 8px 24px; + margin: 20px 0; + padding: 5px 30px; background: transparent; border: 1.5px solid var(--main); - border-radius: 50px; + border-radius: 10px; color: var(--main); font-family: var(--font-main); - font-size: 0.9rem; + font-size: 1rem; font-weight: 600; cursor: pointer; display: block; @@ -249,7 +316,7 @@ margin-right: auto; } -.saveWeekBtn:hover { background: var(--main); color: var(--black); } +.saveWeekBtn:hover { background: var(--main); color: var(--white); transition: all ease-in-out 0.2s; } .divider { border: none; @@ -259,17 +326,18 @@ /* 전체 저장 */ .saveAllBtn { - padding: 14px 0; - background: var(--main); + width: 60%; + margin: 0 auto; + padding: 10px 0; + background: var(--dark); border: none; - border-radius: 50px; - color: var(--black); + border-radius: 10px; + color: var(--white); font-family: var(--font-main); font-size: 1rem; - font-weight: 700; + font-weight: 600; cursor: pointer; - width: 100%; transition: opacity 0.2s; } -.saveAllBtn:hover { opacity: 0.85; } \ No newline at end of file +.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 index 2ce0ae1..98790c8 100644 --- a/frontend/src/pages/pirocheck/students/StudentList.js +++ b/frontend/src/pages/pirocheck/students/StudentList.js @@ -1,6 +1,7 @@ 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: '김피로' }, @@ -67,7 +68,7 @@ function StudentList() { onClick={() => navigate(`/pirocheck/students/${s.userId}`, { state: { name: s.name } })} > {s.name} - + arrow ))}
diff --git a/frontend/src/pages/pirocheck/students/StudentList.module.css b/frontend/src/pages/pirocheck/students/StudentList.module.css index abba372..4eea6e6 100644 --- a/frontend/src/pages/pirocheck/students/StudentList.module.css +++ b/frontend/src/pages/pirocheck/students/StudentList.module.css @@ -64,6 +64,11 @@ cursor: pointer; } +.searchBtn:hover { + background: var(--main); + transition: all ease-in-out 0.2s; +} + .list { width: 450px; display: flex; @@ -94,8 +99,5 @@ } .arrow { - color: var(--white); - font-size: 1.6rem; - opacity: 0.7; - margin-bottom: 1px; + width: 25px; height: 25px; } \ No newline at end of file