From 861a5e449c830f763b3e23af158918f165d3b0a4 Mon Sep 17 00:00:00 2001 From: NamKyeongMin Date: Wed, 21 May 2025 11:28:55 +0900 Subject: [PATCH 01/27] =?UTF-8?q?[fix]:=20ManageStudent.jsx=20css=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/admin/ManageStudent.module.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/pages/admin/ManageStudent.module.css b/frontend/src/pages/admin/ManageStudent.module.css index 931680a..3a900c7 100644 --- a/frontend/src/pages/admin/ManageStudent.module.css +++ b/frontend/src/pages/admin/ManageStudent.module.css @@ -25,6 +25,9 @@ text-align: left; font-size: 16px; width: 100%; + display: flex; + align-items: center; + justify-content: space-between; } .student_button:hover { border: 1px solid #39ff14; From 296161b61417980f9c8eb838523f1e211f8a2b9d Mon Sep 17 00:00:00 2001 From: NamKyeongMin Date: Wed, 21 May 2025 12:03:19 +0900 Subject: [PATCH 02/27] =?UTF-8?q?[add]:=20DetailManageStudent.jsx=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C,=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 9 ++++ frontend/src/api/students.js | 10 ++++ .../src/pages/admin/DetailManageStudent.jsx | 47 +++++++++++++++++++ .../admin/DetailManageStudent.module.css | 11 +++++ frontend/src/pages/admin/ManageStudent.jsx | 8 +++- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/admin/DetailManageStudent.jsx create mode 100644 frontend/src/pages/admin/DetailManageStudent.module.css diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5d442d1..5bc8878 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,6 +6,7 @@ import Assignment from "./pages/generation/Assignment"; import Deposit from "./pages/generation/Deposit"; import Intro from "./Intro"; import Admin from "./pages/admin/Admin"; +import DetailManageStudent from "./pages/admin/DetailManageStudent.jsx"; import ManageStudent from "./pages/admin/ManageStudent.jsx"; import ManageTask from "./pages/admin/ManageTask.jsx"; import AttendanceCode from "./pages/admin/AttendanceCode"; @@ -68,6 +69,14 @@ function App() { } /> + + + + } + /> { }); return res.data; // [{ id: ..., name: ... }] }; + +export const getStudentDetail = async (studentId) => { + try { + const res = await api.get(`/admin/managestudent/${studentId}`); + return res.data; + } catch (error) { + console.error("학생 상세 정보 불러오기 실패:", error); + throw error; + } +}; diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx new file mode 100644 index 0000000..f932ea9 --- /dev/null +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -0,0 +1,47 @@ +import { useParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import Header from "../../components/Header"; +import style from "./DetailManageStudent.module.css"; +import { getStudentDetail } from "../../api/students"; + +const DetailManageStudent = () => { + const { studentId } = useParams(); + const [student, setStudent] = useState(null); + + useEffect(() => { + const fetchStudent = async () => { + try { + const data = await getStudentDetail(studentId); + setStudent(data); + } catch (err) { + console.error("학생 상세 정보 불러오기 실패:", err); + } + }; + + fetchStudent(); + }, [studentId]); + + if (!student) return
loading...
; + + return ( +
+
+
+
+

{student.name}

+

잔여 보증금: {student.deposit}원

+

보증금 방어권: {student.defence}

+
+ +
+ {student.assignmentTitles.map((title, idx) => ( + + ))} +
+
+
+ ); +}; +export default DetailManageStudent; diff --git a/frontend/src/pages/admin/DetailManageStudent.module.css b/frontend/src/pages/admin/DetailManageStudent.module.css new file mode 100644 index 0000000..86fe69d --- /dev/null +++ b/frontend/src/pages/admin/DetailManageStudent.module.css @@ -0,0 +1,11 @@ +.managestudent_wrapper { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; +} +.under_header { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/frontend/src/pages/admin/ManageStudent.jsx b/frontend/src/pages/admin/ManageStudent.jsx index 252175f..80a02e3 100644 --- a/frontend/src/pages/admin/ManageStudent.jsx +++ b/frontend/src/pages/admin/ManageStudent.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { getStudentsByName } from "../../api/students"; import Header from "../../components/Header"; import InputBlock from "../../components/InputBlock"; @@ -8,6 +9,7 @@ const ManageStudent = () => { const [studentName, setStudentName] = useState([""]); const [page, setPage] = useState(1); const [students, setStudents] = useState([]); // 서버 데이터 저장 + const navigate = useNavigate(); const studentsPerPage = 6; @@ -54,7 +56,11 @@ const ManageStudent = () => { />
{paginatedStudents.map((student, index) => ( - ))} From c7f6dd8c886f67558f3c893a4c6c968dec25986b Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Wed, 21 May 2025 19:48:34 +0900 Subject: [PATCH 03/27] =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=A0=84=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 3 +- .../pages/admin/AdminStudentAssignment.jsx | 125 ++++++++++++++++++ .../admin/AdminStudentAssignment.module.css | 112 ++++++++++++++++ 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/admin/AdminStudentAssignment.jsx create mode 100644 frontend/src/pages/admin/AdminStudentAssignment.module.css diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ed53cf3..e483d0a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,7 +11,7 @@ import ManageTask from "./pages/admin/ManageTask.jsx"; import AttendanceCode from "./pages/admin/AttendanceCode"; import Attendance from "./pages/generation/Attendance"; import AdminStudentAttendance from "./pages/admin/AdminStudentAttendance"; - +import AdminStudentAssignment from "./pages/admin/AdminStudentAssignment.jsx"; function App() { return ( @@ -27,6 +27,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/frontend/src/pages/admin/AdminStudentAssignment.jsx b/frontend/src/pages/admin/AdminStudentAssignment.jsx new file mode 100644 index 0000000..2114cb4 --- /dev/null +++ b/frontend/src/pages/admin/AdminStudentAssignment.jsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import AdminStudentHeader from "../../components/AdminStudentHeader"; +import WeeklyOpenBlock from "../../components/WeeklyOpenBlock"; +import AssignmentInfoBlock from "../../components/AssignmentInfoBlock"; +import api from "../../api/api"; +import styles from "./AdminStudentAssignment.module.css"; + +const AdminStudentAssignment = () => { + const { studentId, week } = useParams(); + const [studentInfo, setStudentInfo] = useState(null); + const [weeks, setWeeks] = useState([]); + const [highlightCard, setHighlightCard] = useState(null); + const [selectedWeekLabel, setSelectedWeekLabel] = useState(null); + + useEffect(() => { + api.get(`/admin/users/${studentId}`).then((res) => { + setStudentInfo(res.data.data); + }); + + api + .get(`/admin/managestudent/{studentId}`, { + params: { userId: studentId }, + withCredentials: true, + }) + .then((res) => { + const formatted = res.data.data.map((weekItem) => ({ + week: weekItem.week, + label: `${weekItem.week}주차 ${weekItem.title}`, + days: weekItem.days.map((dayItem) => ({ + day: dayItem.day, + subject: weekItem.title, + tasks: dayItem.details.map((task) => ({ + id: task.id, + label: task.assignmentName, + status: task.status, + modified: false, + })), + })), + })); + + setWeeks(formatted); + + const matched = formatted.find((w) => String(w.week) === String(week)); + if (matched) { + setSelectedWeekLabel(matched.label); + if (matched.days.length > 0) { + setHighlightCard({ + weekLabel: matched.label, + day: matched.days[0].day, + tasks: matched.days[0].tasks, + }); + } + } + }); + }, [studentId, week]); + + const handleStatusChange = (weekIdx, dayIdx, taskIdx, newStatus) => { + const updated = [...weeks]; + const task = updated[weekIdx].days[dayIdx].tasks[taskIdx]; + task.status = newStatus; + task.modified = true; + setWeeks(updated); + }; + + const handleSave = async (taskId, status) => { + await api.put("/admin/assignment/status", { + assignmentId: taskId, + status, + }); + }; + + return ( +
+ window.history.back()} + /> + + {highlightCard && ( +
+ +
+ )} + +
+ {weeks.map((weekItem, weekIdx) => ( +
+

{weekItem.label}

+ {weekItem.days.map((dayItem, dayIdx) => ( +
+

{dayItem.day}   {dayItem.subject}

+
+ {dayItem.tasks.map((task, taskIdx) => ( +
+ {task.label} + + +
+ ))} +
+ +
+ ))} +
+ ))} +
+
+ ); +}; + +export default AdminStudentAssignment; \ No newline at end of file diff --git a/frontend/src/pages/admin/AdminStudentAssignment.module.css b/frontend/src/pages/admin/AdminStudentAssignment.module.css new file mode 100644 index 0000000..04e3e68 --- /dev/null +++ b/frontend/src/pages/admin/AdminStudentAssignment.module.css @@ -0,0 +1,112 @@ +.container { + display: flex; + flex-direction: column; + padding: 20px; + font-family: "Inter", sans-serif; + color: white; + background-color: #1e1e1e; +} + +/* 과제 개요 카드 (상단 형광 카드) */ +.info { + background-color: #045e07; + border-radius: 12px; + padding: 16px; + margin-bottom: 24px; +} + +/* 주차별 목록 */ +.weekList { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* 주차 구간 */ +.weekBlock { + border-left: 4px solid #00c851; + background-color: #2d2d2d; + border-radius: 10px; + padding: 16px; +} + +.weekTitle { + font-size: 18px; + font-weight: bold; + color: #00ff99; + margin-bottom: 12px; +} + +/* 요일별 카드 */ +.dayCard { + background-color: #3a3a3a; + padding: 12px 16px; + border-radius: 10px; + margin-bottom: 16px; +} + +.dayLabel { + font-size: 16px; + font-weight: 600; + margin-bottom: 8px; +} + +/* 개별 과제 */ +.taskList { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 12px; +} + +.taskRow { + display: flex; + align-items: center; + gap: 10px; + background-color: #505050; + border-radius: 6px; + padding: 8px 12px; +} + +.taskLabel { + flex: 1; + font-size: 14px; + color: white; +} + +/* 드롭다운 */ +.taskRow select { + padding: 6px 8px; + border-radius: 6px; + background-color: #2a2a2a; + color: white; + border: 1px solid #777; +} + +/* save 버튼 */ +.saveButton { + padding: 4px 10px; + border-radius: 6px; + background-color: #00c851; + color: white; + font-weight: bold; + border: none; + cursor: pointer; +} + +.saveButton:disabled { + background-color: gray; + cursor: not-allowed; +} + +/* submit 버튼 */ +.submitBtn { + margin-top: 8px; + padding: 6px 12px; + border-radius: 8px; + background-color: #1fa067; + color: white; + font-weight: bold; + border: none; + cursor: pointer; +} From 4b35e53ac425968e1e001d7bd284fd3d6396c255 Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Wed, 21 May 2025 20:37:07 +0900 Subject: [PATCH 04/27] =?UTF-8?q?[Feat]=20admin=20attendance=20student=20a?= =?UTF-8?q?pi=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/adminattendance.js | 26 ++++++++++++++++++ .../pages/admin/AdminStudentAttendance.jsx | 27 +++++++++---------- 2 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 frontend/src/api/adminattendance.js diff --git a/frontend/src/api/adminattendance.js b/frontend/src/api/adminattendance.js new file mode 100644 index 0000000..4506980 --- /dev/null +++ b/frontend/src/api/adminattendance.js @@ -0,0 +1,26 @@ +import api from "./api"; + +// api/attendanceApi.js + +export const getStudentBasicInfo = async (studentId) => { + try { + const res = await api.get(`/admin/managestudent/${studentId}`); + return res.data; + } catch (error) { + console.error("학생 기본 정보 불러오기 실패:", error); + throw error; + } +}; + +export const getStudentAttendance = async (studentId) => { + try { + const res = await api.get("/admin/attendance/user", { + params: { userId: studentId }, + withCredentials: true, + }); + return res.data; + } catch (error) { + console.error("학생 출석 정보 불러오기 실패:", error); + throw error; + } +}; diff --git a/frontend/src/pages/admin/AdminStudentAttendance.jsx b/frontend/src/pages/admin/AdminStudentAttendance.jsx index 827248b..6eed5d1 100644 --- a/frontend/src/pages/admin/AdminStudentAttendance.jsx +++ b/frontend/src/pages/admin/AdminStudentAttendance.jsx @@ -5,6 +5,7 @@ import DailyAttendanceCard from "../../components/AdminDailyAttendanceCard"; import api from "../../api/api"; import styles from "./AdminStudentAttendance.module.css"; import AdminWeeklyAttendanceList from "../../components/AdminWeeklyAttendanceList"; +import { getStudentBasicInfo, getStudentAttendance } from "../../api/adminattendance"; const AdminStudentAttendance = () => { const { studentId } = useParams(); @@ -13,22 +14,20 @@ const AdminStudentAttendance = () => { const [selectedDate, setSelectedDate] = useState(null); useEffect(() => { - // 1. 학생 정보 가져오기 - api.get(`/admin/users/${studentId}`).then((res) => { - setStudentInfo(res.data.data); - }); + const fetchData = async () => { + try { + const studentRes = await getStudentBasicInfo(studentId); + setStudentInfo(studentRes.data); - // 2. 주차별 출석 데이터 가공 - api - .get("/admin/attendance/user", { - params: { userId: studentId }, - withCredentials: true, - }) - .then((res) => { - const raw = res.data.data; - const processed = processWeeklyAttendance(raw); + const attendanceRes = await getStudentAttendance(studentId); + const processed = processWeeklyAttendance(attendanceRes.data); setAttendanceData(processed); - }); + } catch (err) { + console.error("데이터 불러오기 실패:", err); + } + }; + + fetchData(); }, [studentId]); /* From cfef8c897d98bd30a2d54ea22430afe7a295e4c6 Mon Sep 17 00:00:00 2001 From: NamKyeongMin Date: Wed, 21 May 2025 21:11:24 +0900 Subject: [PATCH 05/27] =?UTF-8?q?[fix]:=20DetailManageStudent.jsx=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/api.js | 2 -- .../src/pages/admin/DetailManageStudent.jsx | 8 +++++-- frontend/src/pages/admin/ManageStudent.jsx | 21 +++++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 60d0af3..1950026 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -2,10 +2,8 @@ import axios from "axios"; const api = axios.create({ baseURL: "http://api.pirocheck.org:8080/api", - // 수정 필요한지 재검 필요함 // "http://api.pirocheck.org:8080/api" - withCredentials: true, }); diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx index f932ea9..70266a2 100644 --- a/frontend/src/pages/admin/DetailManageStudent.jsx +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -6,12 +6,13 @@ import { getStudentDetail } from "../../api/students"; const DetailManageStudent = () => { const { studentId } = useParams(); + const numericId = Number(studentId); const [student, setStudent] = useState(null); useEffect(() => { const fetchStudent = async () => { try { - const data = await getStudentDetail(studentId); + const data = await getStudentDetail(numericId); setStudent(data); } catch (err) { console.error("학생 상세 정보 불러오기 실패:", err); @@ -19,10 +20,13 @@ const DetailManageStudent = () => { }; fetchStudent(); - }, [studentId]); + }, [numericId]); if (!student) return
loading...
; + console.log("studentId from URL:", studentId); + console.log("numericId:", numericId); + return (
diff --git a/frontend/src/pages/admin/ManageStudent.jsx b/frontend/src/pages/admin/ManageStudent.jsx index 80a02e3..525a021 100644 --- a/frontend/src/pages/admin/ManageStudent.jsx +++ b/frontend/src/pages/admin/ManageStudent.jsx @@ -55,15 +55,18 @@ const ManageStudent = () => { onChange={handleChange} />
- {paginatedStudents.map((student, index) => ( - - ))} + {paginatedStudents.map((student, index) => { + console.log("student to show:", student); // 🔍 여기 추가 + return ( + + ); + })}
{students.length > studentsPerPage && ( From b399b35bbe34d7cead94dd7b45db5248a7ea1338 Mon Sep 17 00:00:00 2001 From: Imggaggu Date: Wed, 21 May 2025 21:20:24 +0900 Subject: [PATCH 06/27] =?UTF-8?q?[Fix]=20assignment.js=20api=20=EC=9E=AC?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/assignment.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/assignment.js b/frontend/src/api/assignment.js index cf1a11a..323730a 100644 --- a/frontend/src/api/assignment.js +++ b/frontend/src/api/assignment.js @@ -1,6 +1,16 @@ import api from "./api"; - +/* export const fetchAssignmentsByUser = async (userId) => { const res = await api.get(`/assignment/grouped/${userId}`); return res.data; }; +*/ +export const fetchAssignmentsByUser = async (userId) => { + try { + const res = await api.get(`/api/assignment/${userId}`); + return res.data; // 백엔드가 반환하는 JSON 그대로 + } catch (err) { + console.error("과제 데이터 불러오기 실패:", err); + throw err; + } +}; \ No newline at end of file From 94a8a5220c1e48b3b6b267642aee88eb02737bd2 Mon Sep 17 00:00:00 2001 From: NamKyeongMin Date: Wed, 21 May 2025 21:27:17 +0900 Subject: [PATCH 07/27] =?UTF-8?q?[add]:=20Detail=5Fgreenbox=20UI=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/DetailManageStudent.jsx | 15 +++++++++---- .../admin/DetailManageStudent.module.css | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx index 70266a2..24ca910 100644 --- a/frontend/src/pages/admin/DetailManageStudent.jsx +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -32,11 +32,18 @@ const DetailManageStudent = () => {
-

{student.name}

-

잔여 보증금: {student.deposit}원

-

보증금 방어권: {student.defence}

+

{student.name}

+
+

+ 잔여 보증금:

{student.deposit}원

+

+
+
+

+ 보증금 방어권:

{student.defence}

+

+
-
{student.assignmentTitles.map((title, idx) => ( - ) : ( -
- )} + ) : null} {showRightMagageStudent ? (
+
{student.assignmentTitles.map((title, idx) => (
- {student.assignmentTitles.map((title, idx) => ( - ))}
From a5c9d1183c54a5cb8394743b49bd9186855f121f Mon Sep 17 00:00:00 2001 From: NamKyeongMin Date: Thu, 22 May 2025 02:11:44 +0900 Subject: [PATCH 26/27] =?UTF-8?q?[add]:=20=EB=B2=84=ED=8A=BC=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EB=B6=80=20=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/DetailManageStudent.jsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/src/pages/admin/DetailManageStudent.jsx b/frontend/src/pages/admin/DetailManageStudent.jsx index 69a9074..e6b20bb 100644 --- a/frontend/src/pages/admin/DetailManageStudent.jsx +++ b/frontend/src/pages/admin/DetailManageStudent.jsx @@ -50,23 +50,27 @@ const DetailManageStudent = () => { 보증금 방어권: {student.defence}
- -
- {weekData.map((week, index) => ( - - ))} -
+ {student && ( + + )} + {student && ( +
+ {weekData.map((week, index) => ( + + ))} +
+ )} ); From 6d80ef436f7505db696d446ccf50a3c10c09df9e Mon Sep 17 00:00:00 2001 From: dietken1 Date: Thu, 22 May 2025 22:03:32 +0900 Subject: [PATCH 27/27] =?UTF-8?q?fix:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=B6=9C=EC=84=9D=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminAttendanceController.java | 179 +++++++++++------- .../request/UpdateAttendanceStatusReq.java | 3 - .../Attendance/service/AttendanceService.java | 78 ++++++++ 3 files changed, 190 insertions(+), 70 deletions(-) diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java index 5b75a14..591b212 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/controller/AdminAttendanceController.java @@ -23,7 +23,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/admin/attendance") +@RequestMapping("/api") @Tag(name = "관리자 출석관리", description = "관리자용 출석 관리 API") public class AdminAttendanceController { @@ -31,127 +31,172 @@ public class AdminAttendanceController { // 출석체크 시작 @Operation(summary = "출석 체크 시작", description = "새로운 출석 코드를 생성하고 출석 체크를 시작합니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "출석 코드 생성 성공", - content = @Content(schema = @Schema(implementation = AttendanceCodeResponse.class)) - ), + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 코드 생성 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") }) - @PostMapping("/start") - public ApiResponse startAttendance() { + @PostMapping("/admin/attendance/start") + public AttendanceCodeResponse startAttendance() { try { AttendanceCode code = attendanceService.generateCodeAndCreateAttendances(); - return ApiResponse.success(AttendanceCodeResponse.from(code)); + return AttendanceCodeResponse.from(code); } catch (IllegalStateException e) { // 하루 최대 출석 체크 횟수를 초과한 경우 - return ApiResponse.error(e.getMessage()); + throw new IllegalStateException(e.getMessage()); } catch (Exception e) { - return ApiResponse.error("출석 코드 생성 중 오류가 발생했습니다: " + e.getMessage()); + throw new RuntimeException("출석 코드 생성 중 오류가 발생했습니다: " + e.getMessage()); } } // 현재 활성화된 출석코드 조회 @Operation(summary = "현재 활성화된 출석 코드 조회", description = "현재 활성화된 출석 코드 정보를 조회합니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "조회 성공", - content = @Content(schema = @Schema(implementation = AttendanceCodeResponse.class)) - ), + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드 없음") }) - @GetMapping("/active-code") - public ApiResponse getActiveCode() { + @GetMapping("/admin/attendance/active-code") + public AttendanceCodeResponse getActiveCode() { Optional codeOpt = attendanceService.getActiveAttendanceCode(); if (codeOpt.isEmpty()) { - return ApiResponse.error("현재 활성화된 출석코드가 없습니다"); + throw new RuntimeException("현재 활성화된 출석코드가 없습니다"); } - return ApiResponse.success(AttendanceCodeResponse.from(codeOpt.get())); + return AttendanceCodeResponse.from(codeOpt.get()); } // 출석체크 종료 (코드 직접 전달) @Operation(summary = "특정 출석 코드 만료", description = "특정 출석 코드를 만료 처리합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "만료 처리 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 코드를 찾을 수 없음") }) - @PutMapping("/expire") - public ApiResponse expireAttendance( - @Parameter(description = "만료할 출석 코드", required = true) + @PutMapping("/admin/attendance/expire") + public String expireAttendance( + @Parameter(description = "만료할 출석 코드", example = "1234") @RequestParam String code) { - String result = attendanceService.expireAttendanceCode(code); - - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } + return attendanceService.expireAttendanceCode(code); } // 출석체크 종료 (가장 최근 활성화된 코드 자동 만료) @Operation(summary = "최근 활성화된 출석 코드 만료", description = "가장 최근 활성화된 출석 코드를 자동으로 만료 처리합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "만료 처리 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "활성화된 출석 코드가 없음") }) - @PutMapping("/expire-latest") - public ApiResponse expireLatestAttendance() { - String result = attendanceService.expireLatestAttendanceCode(); - - if (result.equals("출석 코드가 성공적으로 만료되었습니다")) { - return ApiResponse.success(result, null); - } else { - return ApiResponse.error(result); - } + @PutMapping("/admin/attendance/expire-latest") + public String expireLatestAttendance() { + return attendanceService.expireLatestAttendanceCode(); } // 출석 상태 변경 (관리자 전용) @Operation(summary = "출석 상태 변경", description = "관리자가 특정 사용자의 출석 상태를 변경합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 상태 변경 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") }) - @PutMapping("/status") - public ApiResponse updateAttendanceStatus( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "출석 상태 변경 요청", - required = true, - content = @Content(schema = @Schema(implementation = UpdateAttendanceStatusReq.class)) - ) + @PutMapping("/admin/users/{userId}/attendance/{attendanceId}/status") + public boolean updateAttendanceStatus( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId, @RequestBody UpdateAttendanceStatusReq req) { - boolean result = attendanceService.updateAttendanceStatus( - req.getAttendanceId(), - req.isStatus() - ); + // userId 파라미터 검증은 여기서 할 수 있음 (필요 시) + return attendanceService.updateAttendanceStatus(attendanceId, req.isStatus()); + } + + // 출석 기록 삭제 (관리자 전용) + @Operation(summary = "출석 기록 삭제", description = "관리자가 특정 사용자의 출석 기록을 삭제합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "출석 기록 삭제 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") + }) + @DeleteMapping("/admin/users/{userId}/attendance/{attendanceId}") + public boolean deleteAttendance( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId) { - if (result) { - return ApiResponse.success("출석 상태가 성공적으로 변경되었습니다", null); - } else { - return ApiResponse.error("출석 상태 변경에 실패했습니다. 출석 기록을 찾을 수 없습니다."); - } + // userId 파라미터 검증은 여기서 할 수 있음 (필요 시) + return attendanceService.deleteAttendance(attendanceId); } // 특정 날짜와 차수에 대한 모든 학생의 출석 현황 조회 @Operation(summary = "특정 날짜와 차수의 출석 현황 조회", description = "특정 날짜와 차수에 대한 모든 학생의 출석 현황을 조회합니다.") - @ApiResponses({ + @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") }) - @GetMapping("/list") - public ApiResponse> getAllAttendanceByDateAndOrder( - @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", required = true) + @GetMapping("/admin/attendance/list") + public List getAllAttendanceByDateAndOrder( + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", example = "2023-08-01") + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + @Parameter(description = "조회할 차수", example = "1") + @RequestParam int order) { + return attendanceService.findAllByDateAndOrder(date, order); + } + + // 특정 사용자의 특정 날짜와 차수 출석 기록 조회 + @Operation(summary = "특정 사용자의 특정 날짜와 차수 출석 조회", description = "특정 사용자의 특정 날짜와 차수 출석 기록을 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") + }) + @GetMapping("/admin/users/{userId}/attendance") + public UserAttendanceStatusRes getUserAttendanceByDateAndOrder( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "조회할 날짜 (YYYY-MM-DD)", example = "2023-08-01") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, - @Parameter(description = "조회할 차수", required = true) + @Parameter(description = "조회할 차수", example = "1") @RequestParam int order) { + return attendanceService.findByUserIdAndDateAndOrder(userId, date, order); + } + + // 특정 출석 ID로 출석 기록 조회 + @Operation(summary = "특정 출석 기록 조회", description = "특정 학생의 특정 출석 기록을 ID로 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "출석 기록을 찾을 수 없음") + }) + @GetMapping("/admin/users/{userId}/attendance/{attendanceId}") + public UserAttendanceStatusRes getAttendanceById( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId, + @Parameter(description = "출석 ID", example = "1") + @PathVariable Long attendanceId) { - List attendances = attendanceService.findAllByDateAndOrder(date, order); - return ApiResponse.success(attendances); + UserAttendanceStatusRes attendance = attendanceService.findById(attendanceId); + + if (attendance == null) { + throw new RuntimeException("출석 기록을 찾을 수 없습니다"); + } + + // 요청된 userId와 조회된 출석 기록의 userId가 일치하는지 확인 + if (!attendance.getUserId().equals(userId)) { + throw new RuntimeException("요청된 사용자 ID와 출석 기록의 사용자 ID가 일치하지 않습니다"); + } + + return attendance; + } + + // 학생용 출석 현황 조회 + @Operation(summary = "학생별 출석 현황 조회", description = "특정 학생의 출석 현황을 조회합니다.") + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "조회 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청") + }) + @GetMapping("/attendance/{userId}") + public List getUserAttendances( + @Parameter(description = "사용자 ID", example = "1") + @PathVariable Long userId) { + return attendanceService.findAllByUserId(userId); } } \ No newline at end of file diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java index 2c6973c..0bea7c5 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/dto/request/UpdateAttendanceStatusReq.java @@ -12,9 +12,6 @@ @AllArgsConstructor @Schema(description = "출석 상태 수정 요청") public class UpdateAttendanceStatusReq { - @Schema(description = "출석 기록 ID", example = "1") - private Long attendanceId; - @Schema(description = "변경할 출석 상태", example = "true") private boolean status; } \ No newline at end of file diff --git a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java index 4a1adb8..ae0dec4 100644 --- a/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java +++ b/backend/pirocheck/src/main/java/backend/pirocheck/Attendance/service/AttendanceService.java @@ -247,4 +247,82 @@ public List findAllByDateAndOrder(LocalDate date, int o .sorted(Comparator.comparing(UserAttendanceStatusRes::getUsername)) .toList(); } + + // 특정 학생의 모든 출석 현황 조회 + public List findAllByUserId(Long userId) { + // 해당 사용자의 모든 출석 기록 조회 + List attendances = attendanceRepository.findByUserId(userId); + + // DTO 변환 + return attendances.stream() + .map(attendance -> { + User user = attendance.getUser(); + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + }) + .sorted(Comparator.comparing(UserAttendanceStatusRes::getDate).reversed() + .thenComparing(UserAttendanceStatusRes::getOrder)) + .toList(); + } + + // 특정 사용자의 특정 출석 기록 삭제 + @Transactional + public boolean deleteAttendance(Long attendanceId) { + Optional attendanceOpt = attendanceRepository.findById(attendanceId); + + if (attendanceOpt.isEmpty()) { + return false; + } + + attendanceRepository.delete(attendanceOpt.get()); + return true; + } + + // 특정 사용자의 특정 날짜와 차수 출석 기록 조회 + public UserAttendanceStatusRes findByUserIdAndDateAndOrder(Long userId, LocalDate date, int order) { + Optional attendanceOpt = attendanceRepository.findByUserIdAndDateAndOrder(userId, date, order); + + if (attendanceOpt.isEmpty()) { + return null; + } + + Attendance attendance = attendanceOpt.get(); + User user = attendance.getUser(); + + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + } + + // 특정 출석 ID로 출석 기록 조회 + public UserAttendanceStatusRes findById(Long attendanceId) { + Optional attendanceOpt = attendanceRepository.findById(attendanceId); + + if (attendanceOpt.isEmpty()) { + return null; + } + + Attendance attendance = attendanceOpt.get(); + User user = attendance.getUser(); + + return UserAttendanceStatusRes.builder() + .userId(user.getId()) + .username(user.getName()) + .date(attendance.getDate()) + .order(attendance.getOrder()) + .status(attendance.isStatus()) + .attendanceId(attendance.getId()) + .build(); + } }