diff --git a/client/src/app/admin/AttendanceChart.tsx b/client/src/app/admin/AttendanceChart.tsx
new file mode 100644
index 0000000..6dcb73e
--- /dev/null
+++ b/client/src/app/admin/AttendanceChart.tsx
@@ -0,0 +1,132 @@
+import { Box, Stack, Typography } from "@mui/material"
+
+interface WeekData {
+ date: string
+ male: number
+ female: number
+ total: number
+}
+
+interface AttendanceChartProps {
+ data: WeekData[]
+}
+
+export default function AttendanceChart({ data }: AttendanceChartProps) {
+ // 최대값 계산
+ const maxTotal = Math.max(...data.map((d) => d.total), 1)
+
+ return (
+
+ {data.map((week, index) => (
+
+
+ {week.date}
+
+
+ {/* 전체 */}
+
+
+ 전체
+
+ {week.total}
+
+
+
+
+
+
+
+ {/* 남성 */}
+
+
+ 남성
+
+ {week.male}
+
+
+
+
+
+
+
+ {/* 여성 */}
+
+
+ 여성
+
+ {week.female}
+
+
+
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/client/src/app/admin/darak/community/page.tsx b/client/src/app/admin/darak/community/page.tsx
index cc8149e..833fd39 100644
--- a/client/src/app/admin/darak/community/page.tsx
+++ b/client/src/app/admin/darak/community/page.tsx
@@ -7,7 +7,7 @@ import CommunityCard from "./CommunityCard"
import { dele, get, post, put } from "@/config/api"
import { MouseEvent, useEffect, useState } from "react"
import { type Community } from "@server/entity/community"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
import CommunityControlPanel from "./CommunityControlPanel"
enum EditMode {
@@ -32,7 +32,7 @@ export default function CommunityComponent() {
const [clickedCommunityName, setClickedCommunityName] = useState("")
const [isMoveMode, setIsMoveMode] = useState(false)
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
fetchData()
@@ -77,7 +77,7 @@ export default function CommunityComponent() {
const canAdd = checkLoopReference(selectedCommunity, parentCommunity.id)
if (!canAdd) {
if (selectedCommunity.id !== parentCommunity.id) {
- setNotificationMessage(`순환 참조 입니다.`)
+ error(`순환 참조 입니다.`)
}
setSelectedCommunity(undefined)
return
@@ -96,7 +96,7 @@ export default function CommunityComponent() {
function checkLoopReference(
checkCommunity: Community,
- parentId: number
+ parentId: number,
): boolean {
if (checkCommunity.id === parentId) return false
const childList = communityList.filter((community) => {
@@ -164,7 +164,7 @@ export default function CommunityComponent() {
selectedCommunity={selectedCommunity}
onEditModeChange={() =>
setEditMode(
- editMode === EditMode.All ? EditMode.Folder : EditMode.All
+ editMode === EditMode.All ? EditMode.Folder : EditMode.All,
)
}
onAddCommunity={addCommunity}
diff --git a/client/src/app/admin/page.tsx b/client/src/app/admin/page.tsx
index 15fe4a1..58d6557 100644
--- a/client/src/app/admin/page.tsx
+++ b/client/src/app/admin/page.tsx
@@ -12,16 +12,16 @@ import {
CardContent,
CircularProgress,
} from "@mui/material"
-import { useSetAtom } from "jotai"
import axios from "@/config/axios"
import useAuth from "@/hooks/useAuth"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import PeopleIcon from "@mui/icons-material/People"
import EventNoteIcon from "@mui/icons-material/EventNote"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
import TrendingUpIcon from "@mui/icons-material/TrendingUp"
import CalendarTodayIcon from "@mui/icons-material/CalendarToday"
+import AttendanceChart from "@/app/admin/AttendanceChart"
interface DashboardData {
totalUsers: number
@@ -70,10 +70,10 @@ interface DashboardData {
function index() {
const router = useRouter()
const { isLogin, authUserData } = useAuth()
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error: showError } = useNotification()
const [dashboardData, setDashboardData] = useState(null)
const [loading, setLoading] = useState(true)
- const [error, setError] = useState("")
+ const [errorMsg, setErrorMsg] = useState("")
useEffect(() => {
hasPermission()
@@ -89,7 +89,7 @@ function index() {
if (!isLogin || !authUserData) {
router.push("/common/login?returnUrl=/admin")
} else if (!authUserData.role.Admin) {
- setNotificationMessage("관리자 권한이 없습니다.")
+ showError("관리자 권한이 없습니다.")
router.push("/")
} else {
setLoading(false)
@@ -101,7 +101,7 @@ function index() {
const { data } = await axios.get("/admin/dashboard")
setDashboardData(data)
} catch (err) {
- setError("대시보드 데이터를 불러오는 중 오류가 발생했습니다.")
+ setErrorMsg("대시보드 데이터를 불러오는 중 오류가 발생했습니다.")
console.error("Dashboard fetch error:", err)
}
}
@@ -124,7 +124,7 @@ function index() {
)
}
- if (error) {
+ if (errorMsg) {
return (
- {error}
+ {errorMsg}
@@ -240,10 +240,37 @@ function index() {
출석 현황 (최근 4주)
-
+
+ {/* 모바일 버전: 막대 그래프 */}
+
+ ({
+ date: w.date,
+ male: w.genderCount.male,
+ female: w.genderCount.female,
+ total: w.genderCount.male + w.genderCount.female,
+ }))}
+ />
+
+
+ {/* 데스크톱 버전: 라인 그래프 */}
+
+
)
}
diff --git a/client/src/app/retreat/admin/carpooling-list/page.tsx b/client/src/app/retreat/admin/carpooling-list/page.tsx
index d8cd1c4..7975614 100644
--- a/client/src/app/retreat/admin/carpooling-list/page.tsx
+++ b/client/src/app/retreat/admin/carpooling-list/page.tsx
@@ -14,24 +14,24 @@ import {
} from "@mui/material"
import { get } from "@/config/api"
import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
import { useRouter } from "next/navigation"
import { Days, HowToMove } from "@server/entity/types"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
let allInoutInfoList: InOutInfo[] = []
export default function CarpoolingList() {
const router = useRouter()
const [moveTypeFilter, setMoveTypeFilter] = useState(
- HowToMove.none
+ HowToMove.none,
)
const [matchFilter, setMatchFilter] = useState<
"true" | "false" | "undefined"
>("undefined")
const [inOutInfoList, setAllUserList] = useState>([])
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
;(async () => {
@@ -43,7 +43,7 @@ export default function CarpoolingList() {
}
} catch {
router.push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
}
})()
@@ -54,18 +54,18 @@ export default function CarpoolingList() {
if (matchFilter === "true") {
result = result.filter(
(inOutInfo) =>
- inOutInfo.userInTheCar.length > 0 || !!inOutInfo.rideCarInfo
+ inOutInfo.userInTheCar.length > 0 || !!inOutInfo.rideCarInfo,
)
} else if (matchFilter === "false") {
result = result.filter(
(inOutInfo) =>
- inOutInfo.userInTheCar.length === 0 && !inOutInfo.rideCarInfo
+ inOutInfo.userInTheCar.length === 0 && !inOutInfo.rideCarInfo,
)
}
if (moveTypeFilter !== HowToMove.none) {
result = result.filter(
- (inOutInfo) => inOutInfo.howToMove === moveTypeFilter
+ (inOutInfo) => inOutInfo.howToMove === moveTypeFilter,
)
}
diff --git a/client/src/app/retreat/admin/carpooling/page.tsx b/client/src/app/retreat/admin/carpooling/page.tsx
index 8578b69..8d832a4 100644
--- a/client/src/app/retreat/admin/carpooling/page.tsx
+++ b/client/src/app/retreat/admin/carpooling/page.tsx
@@ -4,8 +4,7 @@ import { get, post } from "@/config/api"
import { useEffect, useState } from "react"
import { Box, MenuItem, Select, Stack } from "@mui/material"
import { useRouter } from "next/navigation"
-import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import { Days, HowToMove, InOutType } from "@server/entity/types"
import Header from "@/components/retreat/admin/Header"
@@ -13,7 +12,7 @@ import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
function Carpooling() {
const router = useRouter()
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
const [carList, setCarList] = useState([] as InOutInfo[])
const [rideUserList, setRideUserList] = useState([] as InOutInfo[])
const [selectedInfo, setSelectedInfo] = useState()
@@ -106,7 +105,7 @@ function Carpooling() {
})
.catch(() => {
router.push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}
diff --git a/client/src/app/retreat/admin/dash-board/StatCard.tsx b/client/src/app/retreat/admin/dash-board/StatCard.tsx
index 0cd454b..b7b0846 100644
--- a/client/src/app/retreat/admin/dash-board/StatCard.tsx
+++ b/client/src/app/retreat/admin/dash-board/StatCard.tsx
@@ -13,37 +13,79 @@ export default function StatCard({
title,
value,
subtitle,
- color = "#333",
+ color = "#0a0a0a",
backgroundColor = "#fff",
icon,
}: StatCardProps) {
return (
-
- {icon && {icon}}
-
-
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
{title}
-
+
{value}
{subtitle && (
-
+
{subtitle}
)}
diff --git a/client/src/app/retreat/admin/dash-board/page.tsx b/client/src/app/retreat/admin/dash-board/page.tsx
index dda6b92..735e8ab 100644
--- a/client/src/app/retreat/admin/dash-board/page.tsx
+++ b/client/src/app/retreat/admin/dash-board/page.tsx
@@ -1,187 +1,14 @@
"use client"
-import { Box, Grid, Typography } from "@mui/material"
-import { get } from "../../../../config/api"
-import { useEffect, useState } from "react"
-import { InOutInfo } from "@server/entity/retreat/inOutInfo"
-import Header from "@/components/retreat/admin/Header"
-import StatCard from "@/app/retreat/admin/dash-board/StatCard"
-import BarChart from "@/app/retreat/admin/dash-board/BarChart"
-import AttendanceTimeline from "@/app/retreat/admin/dash-board/AttendanceTimeline"
-import PeopleIcon from "@mui/icons-material/People"
-import TrendingUpIcon from "@mui/icons-material/TrendingUp"
-import MaleIcon from "@mui/icons-material/Male"
-import FemaleIcon from "@mui/icons-material/Female"
-import DirectionsBusIcon from "@mui/icons-material/DirectionsBus"
-import PaymentIcon from "@mui/icons-material/Payment"
+import { useRouter } from "next/navigation"
+import { useEffect } from "react"
-function DashBoard() {
- const [attendeeStatus, setAttendeeStatus] = useState(
- {} as Record
- )
- const [attendanceTimeList, setAttendanceTimeList] = useState([])
- const [ageInfoList, setAgeInfoList] = useState>({})
- const [infoList, setInfoList] = useState([])
+export default function DashBoard() {
+ const router = useRouter()
useEffect(() => {
- fetchData()
- const interval = setInterval(fetchData, 1000 * 60 * 30)
- return () => clearInterval(interval)
- }, [])
+ router.push("/retreat/admin")
+ }, [router])
- async function fetchData() {
- try {
- const [status, timeList, ageInfo, info] = await Promise.all([
- get("/retreat/admin/get-attendee-status"),
- get("/retreat/admin/get-attendance-time"),
- get("/retreat/admin/get-age-info"),
- get("/retreat/admin/in-out-info"),
- ])
-
- setAttendeeStatus(status)
- setAttendanceTimeList(timeList)
- setAgeInfoList(ageInfo)
- setInfoList(info)
- } catch (error) {
- console.error("데이터 조회 실패:", error)
- }
- }
-
- // 일별 등록자 데이터 처리 - 날짜순으로 정렬
- const getDailyRegistrationData = () => {
- const timeData: Record = {}
- attendanceTimeList.forEach((time) => {
- const date = new Date(time)
- const key = `${date.getMonth() + 1}.${date.getDate()}`
- timeData[key] = (timeData[key] || 0) + 1
- })
-
- // 날짜순으로 정렬
- const sortedEntries = Object.entries(timeData).sort(([a], [b]) => {
- const [monthA, dayA] = a.split(".").map(Number)
- const [monthB, dayB] = b.split(".").map(Number)
-
- if (monthA !== monthB) {
- return monthA - monthB
- }
- return dayA - dayB
- })
-
- return Object.fromEntries(sortedEntries)
- }
-
- const targetCount = 400
- const attendanceRate = ((attendeeStatus.all / targetCount) * 100).toFixed(1)
- const maleRate = ((attendeeStatus.man / attendeeStatus.all) * 100).toFixed(1)
- const femaleRate = (
- (attendeeStatus.woman / attendeeStatus.all) *
- 100
- ).toFixed(1)
- const depositRate = (
- (attendeeStatus.completeDeposit / attendeeStatus.all) *
- 100
- ).toFixed(1)
-
- return (
-
-
-
-
-
- 수련회 현황 대시보드
-
-
- {/* 주요 통계 카드들 */}
-
- }
- backgroundColor="#E3F2FD"
- />
-
- }
- backgroundColor="#E8F5E8"
- />
-
-
-
-
-
- }
- backgroundColor="#FFF3E0"
- />
-
- }
- backgroundColor="#F3E5F5"
- />
-
-
- {/* 버스 이용 현황 */}
-
- }
- backgroundColor="#E1F5FE"
- />
-
- }
- backgroundColor="#E8EAF6"
- />
-
- }
- backgroundColor="#FDE7E7"
- />
-
-
- {/* 차트들 */}
-
-
-
- [
- key.slice(2, 4) + "년생",
- value,
- ])
- )}
- color="#10B981"
- />
-
-
- {/* 시간별 참석자 현황
-
- */}
-
-
- )
+ return null
}
-
-export default DashBoard
diff --git a/client/src/app/retreat/admin/deposit-check/page.tsx b/client/src/app/retreat/admin/deposit-check/page.tsx
index 487f9a0..b15b049 100644
--- a/client/src/app/retreat/admin/deposit-check/page.tsx
+++ b/client/src/app/retreat/admin/deposit-check/page.tsx
@@ -12,11 +12,11 @@ import {
import { get, post } from "@/config/api"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
-import { NotificationMessage } from "@/state/notification"
import { useSetAtom } from "jotai"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
import { Deposit } from "@server/entity/types"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
function DepositCheck() {
const { push } = useRouter()
@@ -25,7 +25,7 @@ function DepositCheck() {
const [isShowUnpaid, setIsShowUnpaid] = useState(false)
const [depositSum, setDepositSum] = useState(0)
const [allUserList, setAllUserList] = useState([] as Array)
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
fetchData()
@@ -41,7 +41,7 @@ function DepositCheck() {
})
if (result.result === "error") {
- alert("오류 발생!")
+ error("오류 발생!")
return
}
@@ -89,7 +89,7 @@ function DepositCheck() {
})
.catch(() => {
push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}
diff --git a/client/src/app/retreat/admin/edit-user-data/page.tsx b/client/src/app/retreat/admin/edit-user-data/page.tsx
index 5b6ef2a..34c6e4d 100644
--- a/client/src/app/retreat/admin/edit-user-data/page.tsx
+++ b/client/src/app/retreat/admin/edit-user-data/page.tsx
@@ -12,29 +12,29 @@ import {
} from "@mui/material"
import UserInformationForm from "@/components/form/UserInformationForm"
import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
import { useRouter } from "next/navigation"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
export default function EditUserData() {
const { push } = useRouter()
const [userList, setUserList] = useState([] as Array)
const [selectedUserId, setSelectedUserId] = useState("")
const [retreatAttend, setRetreatAttend] = useState(
- undefined
+ undefined,
)
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error, success } = useNotification()
useEffect(() => {
get("/retreat/admin/get-all-user")
.then((response: Array) => {
setUserList(
- response.sort((a, b) => (a.user.name > b.user.name ? 1 : -1))
+ response.sort((a, b) => (a.user.name > b.user.name ? 1 : -1)),
)
if (global.location.search) {
const retreatAttendId = new URLSearchParams(
- global.location.search
+ global.location.search,
).get("retreatAttendId")
setSelectedUserId(retreatAttendId ? retreatAttendId : "")
} else if (response.length > 0) {
@@ -43,7 +43,7 @@ export default function EditUserData() {
})
.catch(() => {
push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}, [])
@@ -71,7 +71,7 @@ export default function EditUserData() {
`${
userList.find((retreatAttend) => retreatAttend.id === selectedUserId)
?.user.name
- }의 정보를 삭제하시겠습니까?`
+ }의 정보를 삭제하시겠습니까?`,
)
if (!c) {
return
@@ -81,7 +81,7 @@ export default function EditUserData() {
})
if (result === "success") {
- setNotificationMessage("삭제되었습니다.")
+ success("삭제되었습니다.")
}
}
diff --git a/client/src/app/retreat/admin/group-formation/page.tsx b/client/src/app/retreat/admin/group-formation/page.tsx
index 82a18eb..a4b4fef 100644
--- a/client/src/app/retreat/admin/group-formation/page.tsx
+++ b/client/src/app/retreat/admin/group-formation/page.tsx
@@ -4,16 +4,16 @@ import { Box, Stack } from "@mui/material"
import { useEffect, useState } from "react"
import { get, post } from "@/config/api"
import { useRouter } from "next/navigation"
-import { NotificationMessage } from "@/state/notification"
import { useSetAtom } from "jotai"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
function GroupFormation() {
const { push } = useRouter()
const [unassignedUserList, setUnassignedUserList] = useState(
- [] as Array
+ [] as Array,
)
const [groupList, setGroupList] = useState([] as Array>)
const [selectedUser, setSelectedUser] = useState()
@@ -24,7 +24,7 @@ function GroupFormation() {
const [showUserInfo, setShowUserInfo] = useState({} as RetreatAttend)
const [userAttendInfo, setUserAttendInfo] = useState([] as Array)
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
function onMouseMove(event: MouseEvent) {
setMousePoint([event.pageX, event.pageY])
@@ -49,7 +49,7 @@ function GroupFormation() {
const group = [] as Array>
const assignedUserList = response.filter(
- (retreatAttend) => retreatAttend.groupNumber !== 0
+ (retreatAttend) => retreatAttend.groupNumber !== 0,
)
assignedUserList.map((retreatAttend) => {
const groupNumber = retreatAttend.groupNumber - 1
@@ -61,13 +61,13 @@ function GroupFormation() {
setGroupList(group)
})
const maxNumber = Math.max(
- ...response.map((retreatAttend) => retreatAttend.groupNumber)
+ ...response.map((retreatAttend) => retreatAttend.groupNumber),
)
setMaxGroupNumber(maxNumber)
})
.catch(() => {
push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}
@@ -257,7 +257,7 @@ function GroupFormation() {
} else {
return Group(
group[0].groupNumber,
- group.sort((a, b) => a.user.yearOfBirth - b.user.yearOfBirth)
+ group.sort((a, b) => a.user.yearOfBirth - b.user.yearOfBirth),
)
}
})}
diff --git a/client/src/app/retreat/admin/inout-info/page.tsx b/client/src/app/retreat/admin/inout-info/page.tsx
index 724c0ed..b3603a3 100644
--- a/client/src/app/retreat/admin/inout-info/page.tsx
+++ b/client/src/app/retreat/admin/inout-info/page.tsx
@@ -4,17 +4,17 @@ import { useEffect, useState } from "react"
import { Box, Stack } from "@mui/material"
import { useRouter } from "next/navigation"
import { get } from "@/config/api"
-import { NotificationMessage } from "@/state/notification"
import { useSetAtom } from "jotai"
import { Days, HowToMove, InOutType } from "@server/entity/types"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
+import { useNotification } from "@/hooks/useNotification"
function InoutInfo() {
const router = useRouter()
const [allUserList, setAllUserList] = useState>([])
const [allInoutInfo, setAllInoutInfo] = useState([] as InOutInfo[])
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
;(async () => {
@@ -25,7 +25,7 @@ function InoutInfo() {
}
} catch {
router.push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
}
get("/retreat/admin/get-car-info")
@@ -34,7 +34,7 @@ function InoutInfo() {
})
.catch(() => {
router.push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
})()
@@ -70,7 +70,7 @@ function InoutInfo() {
.sort(
(a, b) =>
a.retreatAttend.user.yearOfBirth -
- b.retreatAttend.user.yearOfBirth
+ b.retreatAttend.user.yearOfBirth,
)
.map((info) => (
a.retreatAttend.user.yearOfBirth -
- b.retreatAttend.user.yearOfBirth
+ b.retreatAttend.user.yearOfBirth,
)
.map((info) => (
a.retreatAttend.user.yearOfBirth -
- b.retreatAttend.user.yearOfBirth
+ b.retreatAttend.user.yearOfBirth,
)
.map((info) => (
,
+ )
+ const [attendanceTimeList, setAttendanceTimeList] = useState([])
+ const [ageInfoList, setAgeInfoList] = useState>({})
+ const [infoList, setInfoList] = useState([])
useEffect(() => {
ifNotLoggedGoToLogin("/retreat/admin")
+ fetchData()
+ const interval = setInterval(fetchData, 1000 * 60 * 30)
+ return () => clearInterval(interval)
}, [])
+ async function fetchData() {
+ try {
+ const [status, timeList, ageInfo, info] = await Promise.all([
+ get("/retreat/admin/get-attendee-status"),
+ get("/retreat/admin/get-attendance-time"),
+ get("/retreat/admin/get-age-info"),
+ get("/retreat/admin/in-out-info"),
+ ])
+
+ setAttendeeStatus(status)
+ setAttendanceTimeList(timeList)
+ setAgeInfoList(ageInfo)
+ setInfoList(info)
+ } catch (error) {
+ console.error("데이터 조회 실패:", error)
+ }
+ }
+
+ // 일별 등록자 데이터 처리 - 날짜순으로 정렬
+ const getDailyRegistrationData = () => {
+ const timeData: Record = {}
+ attendanceTimeList.forEach((time) => {
+ const date = new Date(time)
+ const key = `${date.getMonth() + 1}.${date.getDate()}`
+ timeData[key] = (timeData[key] || 0) + 1
+ })
+
+ // 날짜순으로 정렬
+ const sortedEntries = Object.entries(timeData).sort(([a], [b]) => {
+ const [monthA, dayA] = a.split(".").map(Number)
+ const [monthB, dayB] = b.split(".").map(Number)
+
+ if (monthA !== monthB) {
+ return monthA - monthB
+ }
+ return dayA - dayB
+ })
+
+ return Object.fromEntries(sortedEntries)
+ }
+
+ const targetCount = 400
+ const attendanceRate = ((attendeeStatus.all / targetCount) * 100).toFixed(1)
+ const maleRate = ((attendeeStatus.man / attendeeStatus.all) * 100).toFixed(1)
+ const femaleRate = (
+ (attendeeStatus.woman / attendeeStatus.all) *
+ 100
+ ).toFixed(1)
+ const depositRate = (
+ (attendeeStatus.completeDeposit / attendeeStatus.all) *
+ 100
+ ).toFixed(1)
+
return (
-
+
-
+
+
+ {/* 주요 통계 섹션 */}
+
+
+
+ 주요 통계
+
+
+ }
+ backgroundColor="#E3F2FD"
+ />
+
+ }
+ backgroundColor="#E8F5E8"
+ />
+
+
+
+
+
+ }
+ backgroundColor="#FFF3E0"
+ />
+
+ }
+ backgroundColor="#F3E5F5"
+ />
+
+
+
+ {/* 버스 이용 현황 섹션 */}
+
+
+
+ 버스 이용 현황
+
+
+ }
+ backgroundColor="#E1F5FE"
+ />
+
+ }
+ backgroundColor="#E8EAF6"
+ />
+
+ }
+ backgroundColor="#FDE7E7"
+ />
+
+
+
+ {/* 통계 분석 섹션 */}
+
+
+
+ 통계 분석
+
+
+
+ [
+ key.slice(2, 4) + "년생",
+ value,
+ ]),
+ )}
+ color="#10B981"
+ />
+
+
+
+ {/* 시간별 참석자 현황
+
+ */}
+
)
}
diff --git a/client/src/app/retreat/admin/permission-manage/page.tsx b/client/src/app/retreat/admin/permission-manage/page.tsx
index 3682f01..53f6cff 100644
--- a/client/src/app/retreat/admin/permission-manage/page.tsx
+++ b/client/src/app/retreat/admin/permission-manage/page.tsx
@@ -15,9 +15,9 @@ import {
} from "@mui/material"
import { useRouter } from "next/navigation"
import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
import { PermissionType } from "@server/entity/types"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
function PermissionManage() {
const { push } = useRouter()
@@ -26,7 +26,7 @@ function PermissionManage() {
const [userPermission, setUserPermission] = useState<{
[key: number]: boolean
}>({})
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
get("/retreat/admin/get-all-user-name")
@@ -38,7 +38,7 @@ function PermissionManage() {
})
.catch(() => {
push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}, [])
@@ -73,7 +73,7 @@ function PermissionManage() {
const data: { [key: number]: boolean } = {}
response.map(
(permission: { permissionType: number; have: boolean }) =>
- (data[permission.permissionType] = permission.have)
+ (data[permission.permissionType] = permission.have),
)
setUserPermission(data)
})
@@ -81,7 +81,7 @@ function PermissionManage() {
function onChangePermission(
event: ChangeEvent,
- key: number
+ key: number,
) {
post("/retreat/admin/set-user-permission", {
userId: selectedUserId,
diff --git a/client/src/app/retreat/admin/room-assignment/page.tsx b/client/src/app/retreat/admin/room-assignment/page.tsx
index babcac5..2857518 100644
--- a/client/src/app/retreat/admin/room-assignment/page.tsx
+++ b/client/src/app/retreat/admin/room-assignment/page.tsx
@@ -5,15 +5,15 @@ import { useEffect, useState } from "react"
import { get, post } from "../../../../config/api"
import { useRouter } from "next/navigation"
import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
import Header from "@/components/retreat/admin/Header"
+import { useNotification } from "@/hooks/useNotification"
function RoomAssingment() {
const { push } = useRouter()
const [unassignedUserList, setUnassignedUserList] = useState(
- [] as Array
+ [] as Array,
)
const [roomList, setRoomList] = useState([] as Array>)
const [selectedUser, setSelectedUser] = useState()
@@ -26,7 +26,7 @@ function RoomAssingment() {
const [shiftPosition, setShiftPosition] = useState({ x: 0, y: 0 })
const [gender, setGender] = useState("man")
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
function onMouseMove(event: MouseEvent) {
setMousePoint([event.pageX, event.pageY])
@@ -47,14 +47,14 @@ function RoomAssingment() {
const unassignedUserList = response
.filter(
(retreatAttend) =>
- !retreatAttend.roomNumber || retreatAttend.roomNumber === 0
+ !retreatAttend.roomNumber || retreatAttend.roomNumber === 0,
)
.sort((a, b) => a.user.yearOfBirth - b.user.yearOfBirth)
setUnassignedUserList(unassignedUserList)
const room = [] as Array>
const assignedUserList = response.filter(
- (retreatAttend) => retreatAttend.roomNumber
+ (retreatAttend) => retreatAttend.roomNumber,
)
assignedUserList.map((retreatAttend) => {
const roomNumber = retreatAttend.roomNumber - 1
@@ -63,19 +63,19 @@ function RoomAssingment() {
} else {
room[roomNumber].push(retreatAttend)
room[roomNumber].sort(
- (a, b) => a.user.yearOfBirth - b.user.yearOfBirth
+ (a, b) => a.user.yearOfBirth - b.user.yearOfBirth,
)
}
setRoomList(room)
})
const maxNumber = Math.max(
- ...response.map((retreatAttend) => retreatAttend.roomNumber)
+ ...response.map((retreatAttend) => retreatAttend.roomNumber),
)
setMaxRoomNumber(maxNumber)
})
.catch(() => {
push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
})
}
@@ -213,7 +213,7 @@ function RoomAssingment() {
{roomNumber}번 방 (
{
userList.filter(
- (retreatAttend) => retreatAttend.user.gender === gender
+ (retreatAttend) => retreatAttend.user.gender === gender,
).length
}
)
@@ -299,7 +299,7 @@ function RoomAssingment() {
미배정(
{
unassignedUserList.filter(
- (roomNumber) => roomNumber.user.gender === gender
+ (roomNumber) => roomNumber.user.gender === gender,
).length
}
명)
diff --git a/client/src/app/retreat/admin/show-status-table/page.tsx b/client/src/app/retreat/admin/show-status-table/page.tsx
index 528623c..c6c8fad 100644
--- a/client/src/app/retreat/admin/show-status-table/page.tsx
+++ b/client/src/app/retreat/admin/show-status-table/page.tsx
@@ -6,14 +6,14 @@ import { useRouter } from "next/navigation"
import { get } from "@/config/api"
import { useEffect, useState } from "react"
import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
+import { useNotification } from "@/hooks/useNotification"
export default function ShowStatusTable() {
const router = useRouter()
const [statusFilter, setStatusFilter] = useState(CurrentStatus.arriveChurch)
const [allUserList, setAllUserList] = useState>([])
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error } = useNotification()
useEffect(() => {
fetchUserData()
@@ -25,25 +25,25 @@ export default function ShowStatusTable() {
const list: RetreatAttend[] = await get("/retreat/admin/get-all-user")
if (list) {
setAllUserList(
- list.sort((a, b) => a.user.yearOfBirth - b.user.yearOfBirth)
+ list.sort((a, b) => a.user.yearOfBirth - b.user.yearOfBirth),
)
}
} catch {
router.push("/retreat/admin")
- setNotificationMessage("권한이 없습니다.")
+ error("권한이 없습니다.")
return
}
}
function ArriveCurchUserList() {
const busUser = allUserList.filter(
- (user) => user.howToGo === HowToMove.together
+ (user) => user.howToGo === HowToMove.together,
)
const arriveUser = busUser.filter(
- (user) => user.currentStatus === CurrentStatus.arriveChurch
+ (user) => user.currentStatus === CurrentStatus.arriveChurch,
)
const notArriveUser = busUser.filter(
- (user) => user.currentStatus === CurrentStatus.null
+ (user) => user.currentStatus === CurrentStatus.null,
)
return (
@@ -78,11 +78,11 @@ export default function ShowStatusTable() {
function ArriveAuditoriumUserList() {
const arriveUser = allUserList.filter(
- (user) => user.currentStatus === CurrentStatus.arriveAuditorium
+ (user) => user.currentStatus === CurrentStatus.arriveAuditorium,
)
const notArriveUser = allUserList.filter(
- (user) => user.currentStatus !== CurrentStatus.arriveAuditorium
+ (user) => user.currentStatus !== CurrentStatus.arriveAuditorium,
)
return (
diff --git a/client/src/app/retreat/steps/fifthStep.tsx b/client/src/app/retreat/steps/fifthStep.tsx
index b47b7e4..1f60dc1 100644
--- a/client/src/app/retreat/steps/fifthStep.tsx
+++ b/client/src/app/retreat/steps/fifthStep.tsx
@@ -3,12 +3,11 @@
import { Stack, Box } from "@mui/material"
import useRetreat from "../hooks/useRetreat"
import RetreatButton from "../components/Button"
-import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
export default function FifthStep() {
const { isWorker, isHalf } = useRetreat()
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { success } = useNotification()
function calculateRetreatFee() {
if (isHalf) {
@@ -54,9 +53,9 @@ export default function FifthStep() {
label={"계좌 복사하기"}
onClick={() => {
navigator.clipboard.writeText(
- "3333342703455 카카오뱅크 성은비 " + calculateRetreatFee()
+ "3333342703455 카카오뱅크 성은비 " + calculateRetreatFee(),
)
- setNotificationMessage("계좌 정보가 복사되었습니다.")
+ success("계좌 정보가 복사되었습니다.")
}}
/>
diff --git a/client/src/app/retreat/steps/fourth.tsx b/client/src/app/retreat/steps/fourth.tsx
index 1a6d71c..820f68c 100644
--- a/client/src/app/retreat/steps/fourth.tsx
+++ b/client/src/app/retreat/steps/fourth.tsx
@@ -2,18 +2,16 @@
import { Stack, Box } from "@mui/material"
import useRetreat from "../hooks/useRetreat"
-import useAuth from "@/hooks/useAuth"
import RetreatButton from "../components/Button"
import { useEffect, useState } from "react"
import { User } from "@server/entity/user"
-import { useSetAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
export default function FourthStep() {
const [myInfo, setMyInfo] = useState(null)
const { saveRetreatAttend, getMyInfo, isWorker, isHalf, setStep } =
useRetreat()
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error, success } = useNotification()
useEffect(() => {
getMyInfo().then((data) => {
@@ -34,12 +32,12 @@ export default function FourthStep() {
async function saveRetreatAttendWrapper() {
try {
const { data } = await saveRetreatAttend()
- setNotificationMessage(data.result)
+ success(data.result)
setStep(5)
} catch (e) {
- setNotificationMessage(
+ error(
"참가 신청에 실패했습니다. 새로고침 후 다시 시도해주세요." +
- e.toString()
+ e.toString(),
)
}
}
diff --git a/client/src/components/form/UserInformationForm.tsx b/client/src/components/form/UserInformationForm.tsx
index 685569c..1161a60 100644
--- a/client/src/components/form/UserInformationForm.tsx
+++ b/client/src/components/form/UserInformationForm.tsx
@@ -2,10 +2,8 @@
import { post } from "@/config/api"
import InOutFrom from "./InOutForm"
-import { useSetAtom } from "jotai"
-import { useRouter } from "next/navigation"
import { HowToMove } from "@server/entity/types"
-import { NotificationMessage } from "@/state/notification"
+import { useNotification } from "@/hooks/useNotification"
import { InOutInfo } from "@server/entity/retreat/inOutInfo"
import { RetreatAttend } from "@server/entity/retreat/retreatAttend"
import { Dispatch, SetStateAction, useEffect, useState } from "react"
@@ -18,12 +16,11 @@ interface IProps {
}
export default function UserInformationForm(props: IProps) {
- const router = useRouter()
const [retreatAttend, setRetreatAttend] = useState(
- undefined
+ undefined,
)
const [inOutData, setInOutData] = useState>([])
- const setNotificationMessage = useSetAtom(NotificationMessage)
+ const { error, success } = useNotification()
useEffect(() => {
setRetreatAttend(props.retreatAttend)
@@ -39,11 +36,11 @@ export default function UserInformationForm(props: IProps) {
const submit = async () => {
if (!retreatAttend) {
- setNotificationMessage("수련회 접수 정보 없음.")
+ error("수련회 접수 정보 없음.")
return
}
if (!retreatAttend.howToGo) {
- setNotificationMessage("이동 방법을 선택해주세요.")
+ error("이동 방법을 선택해주세요.")
return
}
@@ -59,19 +56,17 @@ export default function UserInformationForm(props: IProps) {
}
if (saveResult.result !== "success") {
- setNotificationMessage(
- "접수중 오류가 발생하였습니다.\n다시 시도해주세요."
- )
+ error("접수중 오류가 발생하였습니다.\n다시 시도해주세요.")
return
}
if (attendTimeResult && attendTimeResult.result !== "success") {
- setNotificationMessage(
- "이동 정보 저장중에 문제가 발생하였습니다.\n시간, 장소. 이동 방법을 모두 입력해주세요."
+ error(
+ "이동 정보 저장중에 문제가 발생하였습니다.\n시간, 장소. 이동 방법을 모두 입력해주세요.",
)
return
}
- setNotificationMessage(`신청 내역이 저장이 되었습니다.`)
+ success(`신청 내역이 저장이 되었습니다.`)
props.reloadFunction()
props.setEditMode(false)
}
diff --git a/client/src/components/notification/notification.tsx b/client/src/components/notification/notification.tsx
index fd3cdb7..b88961a 100644
--- a/client/src/components/notification/notification.tsx
+++ b/client/src/components/notification/notification.tsx
@@ -1,6 +1,6 @@
-import { useEffect, useState } from "react"
+import { useEffect, useState, useCallback, useRef } from "react"
import { useAtom } from "jotai"
-import { NotificationMessage } from "@/state/notification"
+import { notificationAtom, NotificationType } from "@/state/notification"
import {
Alert,
Snackbar,
@@ -9,13 +9,14 @@ import {
Fade,
Box,
Typography,
+ Button,
} from "@mui/material"
import { TransitionProps } from "@mui/material/transitions"
import React from "react"
// Slide transition component for smoother animations
function SlideTransition(
- props: TransitionProps & { children: React.ReactElement }
+ props: TransitionProps & { children: React.ReactElement },
) {
return
}
@@ -24,59 +25,107 @@ interface NotificationItem {
id: number
content: string
isVisible: boolean
- severity?: "success" | "info" | "warning" | "error"
+ type: NotificationType
+ duration: number
+ action?: {
+ label: string
+ onClick: () => void
+ }
}
export default function Notification() {
- const [notificationMessage, setNotificationMessage] =
- useAtom(NotificationMessage)
+ const [notification, setNotification] = useAtom(notificationAtom)
const [notifications, setNotifications] = useState([])
const [nextId, setNextId] = useState(1)
+ const timerRefs = useRef