diff --git a/client/src/app/leader/newcomer/layout.tsx b/client/src/app/leader/newcomer/layout.tsx index e679b1dd..647cb1f0 100644 --- a/client/src/app/leader/newcomer/layout.tsx +++ b/client/src/app/leader/newcomer/layout.tsx @@ -7,6 +7,7 @@ const menuItems = [ { label: "새신자 등록/조회", path: "/leader/newcomer/management" }, { label: "교육 현황", path: "/leader/newcomer/education" }, { label: "섬김이 관리", path: "/leader/newcomer/managers" }, + { label: "주차별 예상 참석", path: "/leader/newcomer/weekly-attendance" }, ] export default function NewcomerLayout({ diff --git a/client/src/app/leader/newcomer/management/NewcomerFilter.tsx b/client/src/app/leader/newcomer/management/NewcomerFilter.tsx index 39560e60..995ae49a 100644 --- a/client/src/app/leader/newcomer/management/NewcomerFilter.tsx +++ b/client/src/app/leader/newcomer/management/NewcomerFilter.tsx @@ -9,6 +9,8 @@ interface NewcomerFilterProps { setFilterMinYear: (value: string) => void filterMaxYear: string setFilterMaxYear: (value: string) => void + filterStatus: string + setFilterStatus: (value: string) => void clearFilters: () => void } @@ -21,6 +23,8 @@ export default function NewcomerFilter({ setFilterMinYear, filterMaxYear, setFilterMaxYear, + filterStatus, + setFilterStatus, clearFilters, }: NewcomerFilterProps) { return ( @@ -90,6 +94,26 @@ export default function NewcomerFilter({ + + + 상태: + + setFilterStatus(e.target.value)} + variant="outlined" + sx={{ flex: 1 }} + > + 전체 + 활동중 + 등반 + 보류 + 삭제 + + + 생년: diff --git a/client/src/app/leader/newcomer/management/NewcomerForm.tsx b/client/src/app/leader/newcomer/management/NewcomerForm.tsx index 547fabf6..e70b8bfc 100644 --- a/client/src/app/leader/newcomer/management/NewcomerForm.tsx +++ b/client/src/app/leader/newcomer/management/NewcomerForm.tsx @@ -11,6 +11,7 @@ interface Newcomer { yearOfBirth: number | null phone: string | null gender: "man" | "woman" | "" | null + status?: "NORMAL" | "PROMOTED" | "PENDING" | "DELETED" newcomerManager?: { id: string user: { id: string; name: string } @@ -22,6 +23,8 @@ interface NewcomerFormProps { onDataChange: (key: string, value: any) => void onSave: () => void onDelete: () => void + onPending: () => void + onPromote: () => void onClear: () => void managerList: Manager[] } @@ -31,9 +34,26 @@ export default function NewcomerForm({ onDataChange, onSave, onDelete, + onPending, + onPromote, onClear, managerList, }: NewcomerFormProps) { + const getStatusLabel = (status?: string) => { + switch (status) { + case "NORMAL": + return "활동중" + case "PROMOTED": + return "등반" + case "PENDING": + return "보류" + case "DELETED": + return "삭제" + default: + return "활동중" + } + } + return ( - {selectedNewcomer.id && ( - + {selectedNewcomer.id && selectedNewcomer.status !== "DELETED" && ( + <> + + + + )} + + + ) } diff --git a/client/src/app/leader/newcomer/weekly-attendance/page.tsx b/client/src/app/leader/newcomer/weekly-attendance/page.tsx new file mode 100644 index 00000000..81174067 --- /dev/null +++ b/client/src/app/leader/newcomer/weekly-attendance/page.tsx @@ -0,0 +1,219 @@ +"use client" + +import { useEffect, useState } from "react" +import { + Box, + Paper, + Typography, + Stack, + Grid, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + CircularProgress, + Chip, +} from "@mui/material" +import axios from "@/config/axios" + +enum EducationLecture { + OT = "OT", + L1 = "L1", + L2 = "L2", + L3 = "L3", + L4 = "L4", + L5 = "L5", +} + +const LECTURE_ORDER = [ + EducationLecture.L1, + EducationLecture.L2, + EducationLecture.L3, + EducationLecture.L4, + EducationLecture.L5, +] + +const LECTURE_LABELS: Record = { + [EducationLecture.OT]: "OT", + [EducationLecture.L1]: "1강", + [EducationLecture.L2]: "2강", + [EducationLecture.L3]: "3강", + [EducationLecture.L4]: "4강", + [EducationLecture.L5]: "5강", + PROMOTION: "등반 예정", +} + +interface WorshipSchedule { + id: string + date: string + lectureType?: string +} + +interface NewcomerEducation { + id: string + lectureType: EducationLecture + worshipSchedule?: WorshipSchedule +} + +interface Newcomer { + id: string + name: string + status: string + educationRecords: NewcomerEducation[] + phone?: string +} + +export default function WeeklyAttendancePage() { + const [loading, setLoading] = useState(true) + const [newcomers, setNewcomers] = useState([]) + const [stats, setStats] = useState([]) + + useEffect(() => { + fetchData() + }, []) + + const fetchData = async () => { + try { + setLoading(true) + const res = await axios.get("/newcomer?status=NORMAL") + const data: Newcomer[] = res.data + setNewcomers(data) + calculateStats(data) + } catch (error) { + console.error("Failed to fetch newcomers", error) + } finally { + setLoading(false) + } + } + + const calculateStats = (data: Newcomer[]) => { + // Group newcomers by "Next Lecture" + const groups: Record = { + [EducationLecture.L1]: [], + [EducationLecture.L2]: [], + [EducationLecture.L3]: [], + [EducationLecture.L4]: [], + [EducationLecture.L5]: [], + PROMOTION: [], + } + + data.forEach((nc) => { + const completed = new Set(nc.educationRecords.map((r) => r.lectureType)) + let nextStep = "PROMOTION" + + for (const lecture of LECTURE_ORDER) { + if (!completed.has(lecture)) { + nextStep = lecture + break + } + } + if (groups[nextStep]) { + groups[nextStep].push(nc) + } + }) + + // Build Stats Array for Display + const displayOrder = [ + EducationLecture.L1, + EducationLecture.L2, + EducationLecture.L3, + EducationLecture.L4, + EducationLecture.L5, + "PROMOTION", + ] + + const calculatedStats = displayOrder.map((step) => { + const waiting = groups[step] || [] + const count = waiting.length + + return { + step, + label: LECTURE_LABELS[step], + count, + people: waiting, + } + }) + + setStats(calculatedStats) + } + + if (loading) { + return ( + + + + ) + } + + return ( + + + 주차별 예상 참석자 현황 + + + + {stats.map((stat) => ( + + + + + {stat.label} + + + + + + + + + 이름 + 연락처 + + + + {stat.people.length > 0 ? ( + stat.people.map((person: any) => ( + + {person.name} + + {person.phone + ? person.phone.replace( + /(\d{3})(\d{4})(\d{4})/, + "$1-$2-$3", + ) + : "-"} + + + )) + ) : ( + + + + 대상자가 없습니다. + + + + )} + +
+
+
+
+ ))} +
+
+ ) +} diff --git a/server/src/routes/newcomer/newcomerRouter.ts b/server/src/routes/newcomer/newcomerRouter.ts index 00ac19fb..8365cb96 100644 --- a/server/src/routes/newcomer/newcomerRouter.ts +++ b/server/src/routes/newcomer/newcomerRouter.ts @@ -113,6 +113,8 @@ router.put("/:id", async (req, res) => { assignmentId, newcomerManagerId, status, + pendingDate, + promotionDate, } = req.body try { @@ -129,6 +131,8 @@ router.put("/:id", async (req, res) => { if (phone !== undefined) newcomer.phone = phone?.replace(/[^\d]/g, "") || null if (status) newcomer.status = status + if (pendingDate !== undefined) newcomer.pendingDate = pendingDate + if (promotionDate !== undefined) newcomer.promotionDate = promotionDate // 인도자 업데이트 if (guiderId !== undefined) { @@ -172,7 +176,7 @@ router.put("/:id", async (req, res) => { } }) -// 1-2. 새신자 삭제 +// 1-2. 새신자 삭제 (소프트 딜리트) router.delete("/:id", async (req, res) => { const user = await checkJwt(req) if (!user) { @@ -183,7 +187,7 @@ router.delete("/:id", async (req, res) => { const { id } = req.params try { - const result = await newcomerDatabase.delete(id) + const result = await newcomerDatabase.softDelete(id) if (result.affected === 0) { res.status(404).send({ error: "새신자를 찾을 수 없습니다." }) return