Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // 백엔드 API 요청에만 CORS 허용
.allowedOrigins("http://www.pirocheck.org") // 프론트 배포 URL
.allowedOrigins("http://localhost:5173", "https://www.pirocheck.org") // 프론트 배포 URL
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
.allowedHeaders("*")
.allowCredentials(true); // 세션 쿠키 주고받기 허용
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MagageStudent from "./pages/admin/ManageStudent.jsx";
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";

function App() {
return (
Expand All @@ -25,6 +26,7 @@ function App() {
<Route path="/magagestudent" element={<MagageStudent />} />
<Route path="/magagetask" element={<ManageTask />} />
<Route path="/attendancecode" element={<AttendanceCode />} />
<Route path="/admin/attendance/:studentId" element={<AdminStudentAttendance />} />
</Routes>
</BrowserRouter>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/api/api.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import axios from "axios";

const api = axios.create({
baseURL: "http://api.:8080/api",
baseURL: "http://api.pirocheck.org:8080/api",
withCredentials: true,
});

Expand Down
112 changes: 112 additions & 0 deletions frontend/src/components/AdminDailyAttendanceCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useEffect, useState } from "react";
import "./componentsCss/AdminDailyAttendanceCard.css";
import api from "../api/api";

const AdminDailyAttendanceCard = ({ date, studentId, onClose }) => {
const [slots, setSlots] = useState([]);
const [modified, setModified] = useState([]);

useEffect(() => {
const fetchSlots = async () => {
/*
{
// 개발용 더미 데이터
const dummySlots = [
{ id: 1, status: true },
{ id: 2, status: false },
{ id: 3, status: true },
];
setSlots(dummySlots);
setModified(Array(dummySlots.length).fill(false));
return;
}
*/
try {
const res = await api.get("/attendance/user/date", {
params: { userId: studentId, date },
withCredentials: true,
});

const rawSlots = res.data.data?.[0]?.slots || [];
setSlots(rawSlots);
setModified(Array(rawSlots.length).fill(false));
} catch (err) {
console.error("슬롯 정보 불러오기 실패:", err);
}
};

fetchSlots();
}, [date, studentId]);

const handleToggle = (idx) => {
const newSlots = [...slots];
newSlots[idx].status = !newSlots[idx].status;
setSlots(newSlots);

const newModified = [...modified];
newModified[idx] = true;
setModified(newModified);
};

const handleSave = async (idx) => {
try {
const slotId = slots[idx].id;
await api.put(`/attendance/slot/${slotId}`, {
status: slots[idx].status,
}, { withCredentials: true });

const newModified = [...modified];
newModified[idx] = false;
setModified(newModified);
} catch (err) {
console.error("슬롯 저장 실패:", err);
alert("저장 실패");
}
};

const handleSubmit = async () => {
try {
for (let i = 0; i < slots.length; i++) {
if (modified[i]) {
await handleSave(i);
}
}
alert("전체 저장 완료");
} catch (err) {
console.error("전체 저장 실패:", err);
}
};

return (
<div className="daily-card">
<div className="card-header">
<p>{date} 출석 수정</p>
<button onClick={onClose}>❌</button>
</div>
<div className="card-body">
{slots.map((slot, idx) => (
<div key={slot.id} className="slot-row">
<span>{idx + 1}차 출석</span>
<select value={slot.status} onChange={(e) => handleChange(idx, e.target.value)}>
<option value="SUCCESS">성공</option>
<option value="FAILURE">실패</option>
</select>

<button
className="save-btn"
onClick={() => handleSave(idx)}
disabled={!modified[idx]}
>
save
</button>
</div>
))}
</div>
<button className="submit-btn" onClick={handleSubmit}>
submit
</button>
</div>
);
};

export default AdminDailyAttendanceCard;
32 changes: 32 additions & 0 deletions frontend/src/components/AdminStudentHeader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import { useNavigate, useParams } from "react-router-dom";
import "./componentsCss/Header.css";

const AdminStudentHeader = ({ studentName = "default", onBack }) => {
const navigate = useNavigate();
const { studentId } = useParams();

return (
<div className="header-container">
<button className="icon-button" onClick={onBack}>
<img
src="/assets/img/arrowicon.svg"
alt="Back"
width={34}
height={34}
/>
</button>

<h1 className="header-title">{studentName} 출석</h1>

<button
className="icon-button"
onClick={() => navigate(`/admin/managestudent`)}
>
👥
</button>
</div>
);
};

export default AdminStudentHeader;
32 changes: 32 additions & 0 deletions frontend/src/components/AdminWeeklyAttendanceList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";
import "./componentsCss/AdminWeeklyAttendanceList.css";
//import "./componentsCss/AttendanceWeekInfo.css";

const statusImageMap = {
SUCCESS: "/assets/img/full_coin_green.png",
INSUFFICIENT: "/assets/img/two_coin_yellow.png",
FAILURE: "/assets/img/one_coin_yellow.png",
EMPTY: "/assets/img/three_out_red.png",
};
const AdminWeeklyAttendanceList = ({ attendanceData, onSelectDate }) => {
return (
<div className="weekly-container">
{attendanceData.map(({ week, classes }) => (
<div key={week} className="eachWeekInfo" /*onClick={() => onSelectWeek(week)}*/>
<p className="weekInfo">{week}주차</p>
<div className="coin_img_container">
{classes.map((cls, idx) => (
<img key={idx}
src={statusImageMap[cls.status]}
style={{ cursor: "pointer" }}
onClick={() => cls.date && onSelectDate(cls.date)}
/>
))}
</div>
</div>
))}
</div>
);
};

export default AdminWeeklyAttendanceList;
55 changes: 55 additions & 0 deletions frontend/src/components/componentsCss/AdminDailyAttendanceCard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.daily-card {
background-color: #1d471d;
padding: 20px;
border-radius: 20px;
color: white;
width: 300px;
margin: 24px auto;
font-family: "Pretendard";
}

.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
margin-bottom: 10px;
}

.card-body {
display: flex;
flex-direction: column;
gap: 12px;
}

.slot-row {
display: flex;
justify-content: space-between;
align-items: center;
}

.save-btn {
background-color: #666;
color: white;
border: none;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
}

.save-btn:disabled {
background-color: #999;
cursor: not-allowed;
}

.submit-btn {
margin-top: 16px;
width: 100%;
background-color: white;
color: #1d471d;
border: none;
border-radius: 20px;
padding: 6px 18px;
font-weight: bold;
cursor: pointer;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.eachWeekInfo {
width: 300px;
padding: 6px;
border-radius: 50px;
background-color: #434343;
border: 1px solid #d9d9d9;
margin-block: 8px;
display: flex;
align-items: center;
justify-content: space-around;
}
.weekInfo {
font-weight: 600;
font-size: 15px;
color: #ffffff;
font-family: "Inter", sans-serif;
}
.coin_img_container {
width: 150px;
height: 30px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.coin_img_container > img {
width: 30px;
height: 100%;
}

.weekly-container{
color: var(--main-green);
font-family: "Cafe24Moyamoya-Regular-v1.0", sans-serif;
display: flex;
flex-direction: column;
align-items: center;
width: 390px;

justify-content: space-between;
padding: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
margin: 10px;

}
Loading