diff --git a/client/src/app/admin/page.tsx b/client/src/app/admin/page.tsx
index 58d65577..910de29e 100644
--- a/client/src/app/admin/page.tsx
+++ b/client/src/app/admin/page.tsx
@@ -25,6 +25,7 @@ import AttendanceChart from "@/app/admin/AttendanceChart"
interface DashboardData {
totalUsers: number
+ communityUsers: number
totalCommunities: number
statistics: {
weekly: {
@@ -64,6 +65,11 @@ interface DashboardData {
date: string
memo: string
}>
+ villagesWithoutAttendance: Array<{
+ leader: { id: string; name: string }
+ villageId: number
+ villageName: string
+ }>
lastUpdated: string
}
@@ -179,6 +185,26 @@ function index() {
등록된 인원
+
+ (장결 포함)
+
+
+
+
+
+
+
+
+
+
+
+
+ {dashboardData.communityUsers}
+
+
+ 등록된 인원
+
+ (장결 재외)
@@ -235,6 +261,46 @@ function index() {
+
+
+ 출석 미등록 마을
+
+
+ {dashboardData.villagesWithoutAttendance.map((village) => (
+
+
+ {village.villageName}
+
+
+ ))}
+
+ {dashboardData.villagesWithoutAttendance.length === 0 && (
+
+ 없음
+
+ )}
+
+
diff --git a/client/src/app/leader/newcomer/management/NewcomerFilter.tsx b/client/src/app/leader/newcomer/management/NewcomerFilter.tsx
index 995ae49a..211e4d97 100644
--- a/client/src/app/leader/newcomer/management/NewcomerFilter.tsx
+++ b/client/src/app/leader/newcomer/management/NewcomerFilter.tsx
@@ -1,4 +1,4 @@
-import { Box, Button, MenuItem, Stack, TextField } from "@mui/material"
+import { Box, Button, MenuItem, Select, Stack, TextField } from "@mui/material"
interface NewcomerFilterProps {
filterName: string
@@ -36,15 +36,102 @@ export default function NewcomerFilter({
bgcolor="#fafafa"
boxShadow="0 2px 4px rgba(0,0,0,0.1)"
>
-
+
- 필터
+ 조회 조건
+
+
+
+
+ 이름:
+
+ setFilterName(e.target.value)}
+ variant="outlined"
+ sx={{ flex: 1 }}
+ />
+
+
+
+
+ 성별:
+
+
+
+
+
+
+ 상태:
+
+
+
+
+
+
+ 생년:
+
+ setFilterMinYear(e.target.value)}
+ variant="outlined"
+ type="number"
+ sx={{ width: "90px" }}
+ />
+
+ ~
+
+ setFilterMaxYear(e.target.value)}
+ variant="outlined"
+ type="number"
+ sx={{ width: "90px" }}
+ />
+
+
-
-
-
-
- 이름:
-
- setFilterName(e.target.value)}
- variant="outlined"
- sx={{ flex: 1 }}
- />
-
-
-
-
- 성별:
-
-
- setFilterGender(e.target.value as "" | "man" | "woman")
- }
- variant="outlined"
- sx={{ flex: 1 }}
- >
-
-
-
-
-
-
-
-
- 상태:
-
- setFilterStatus(e.target.value)}
- variant="outlined"
- sx={{ flex: 1 }}
- >
-
-
-
-
-
-
-
-
-
-
- 생년:
-
- setFilterMinYear(e.target.value)}
- variant="outlined"
- type="number"
- sx={{ width: "90px" }}
- />
-
- ~
-
- setFilterMaxYear(e.target.value)}
- variant="outlined"
- type="number"
- sx={{ width: "90px" }}
- />
-
-
)
}
diff --git a/client/src/app/leader/newcomer/management/NewcomerForm.tsx b/client/src/app/leader/newcomer/management/NewcomerForm.tsx
index 458ecbd2..90925352 100644
--- a/client/src/app/leader/newcomer/management/NewcomerForm.tsx
+++ b/client/src/app/leader/newcomer/management/NewcomerForm.tsx
@@ -68,7 +68,7 @@ export default function NewcomerForm({
flex={1}
gap="12px"
p="16px"
- m="12px"
+ fontSize="14px"
border="1px solid #e0e0e0"
borderRadius="12px"
bgcolor="#ffffff"
diff --git a/client/src/app/leader/newcomer/management/NewcomerTable.tsx b/client/src/app/leader/newcomer/management/NewcomerTable.tsx
index 5a541baf..f6dc1413 100644
--- a/client/src/app/leader/newcomer/management/NewcomerTable.tsx
+++ b/client/src/app/leader/newcomer/management/NewcomerTable.tsx
@@ -61,7 +61,8 @@ export default function NewcomerTable({
width="50%"
flex={1}
border="1px solid #ccc"
- maxHeight="calc(100vh - 100px)"
+ borderRadius="8px"
+ maxHeight="calc(100vh - 230px)"
overflow="auto"
>
diff --git a/client/src/app/leader/newcomer/management/page.tsx b/client/src/app/leader/newcomer/management/page.tsx
index b2a211fd..d10a6232 100644
--- a/client/src/app/leader/newcomer/management/page.tsx
+++ b/client/src/app/leader/newcomer/management/page.tsx
@@ -325,7 +325,20 @@ export default function NewcomerManagement() {
return (
-
+
+
-
,
@@ -90,6 +91,7 @@ export default function Header() {
path: "/leader/retreat-attendance-for-same-age",
type: "menu",
})
+ */
if (authUserData?.role.VillageLeader) {
DrawerItems.push({
diff --git a/server/src/entity/types.ts b/server/src/entity/types.ts
index f64117f8..e7769122 100644
--- a/server/src/entity/types.ts
+++ b/server/src/entity/types.ts
@@ -32,6 +32,10 @@ export enum PermissionType {
retreatEditTeamScore = "retreatEditTeamScore",
retreatDeleteUser = "retreatDeleteUser",
retreatMediaManage = "retreatMediaManage",
+
+ // 새가족 관련 권한
+ MANAGE_NEWCOMER_INFORMATION = "MANAGE_NEWCOMER_INFORMATION",
+ MANAGE_NEWCOMER_EDUCATION = "MANAGE_NEWCOMER_EDUCATION",
}
export function permissionTypeToString(
@@ -68,6 +72,10 @@ export function permissionTypeToString(
return "수련회 참가자 삭제"
case PermissionType.retreatMediaManage:
return "수련회 미디어 관리"
+ case PermissionType.MANAGE_NEWCOMER_INFORMATION:
+ return "새가족 정보 관리"
+ case PermissionType.MANAGE_NEWCOMER_EDUCATION:
+ return "새가족 교육 관리"
default:
return "기타"
}
diff --git a/server/src/routes/admin/dashboard.ts b/server/src/routes/admin/dashboard.ts
index ec740b28..56094d5b 100644
--- a/server/src/routes/admin/dashboard.ts
+++ b/server/src/routes/admin/dashboard.ts
@@ -57,6 +57,12 @@ router.get("/", async (req: Request, res: Response) => {
name: Not(IsNull()),
},
})
+ const communityUsers = await userDatabase.count({
+ where: {
+ community: Not(IsNull()),
+ name: Not(IsNull()),
+ },
+ })
const totalCommunities = await communityDatabase.count()
// 이번 주 예배 일정 조회
@@ -83,7 +89,7 @@ router.get("/", async (req: Request, res: Response) => {
const weeklyAttendance =
weeklySchedules.length > 0
? await attendDataDatabase.find({
- relations: ["worshipSchedule", "user"],
+ relations: ["worshipSchedule", "user", "user.community"],
where: {
worshipSchedule: {
id: In(weeklySchedules.map((s) => s.id)),
@@ -105,6 +111,43 @@ router.get("/", async (req: Request, res: Response) => {
})
: []
+ // 출석 등록하지 않은 마을 정보
+ const areaWithAttendance = await communityDatabase.find({
+ where: {
+ parent: IsNull(),
+ },
+ relations: {
+ parent: true,
+ },
+ })
+ const villageIdsWithAttendance = await communityDatabase.find({
+ where: {
+ parent: In(areaWithAttendance.map((area) => area.id)),
+ },
+ relations: {
+ parent: true,
+ leader: true,
+ },
+ })
+
+ const attendanceVillageIds = new Set(
+ weeklyAttendance
+ .map((attendance) => attendance.user?.community?.id)
+ .filter((villageId): villageId is number => villageId !== undefined),
+ )
+ const villagesWithoutAttendance = villageIdsWithAttendance
+ .filter((village) => !attendanceVillageIds.has(village.id))
+ .map((village) => ({
+ villageId: village.id,
+ villageName: village.name,
+ leader: village.leader
+ ? {
+ id: village.leader.id,
+ name: village.leader.name,
+ }
+ : null,
+ }))
+
// 새가족 등록자 수 계산 함수
const countNewFamilyRegistrants = async (scheduleIds: number[]) => {
if (scheduleIds.length === 0) return 0
@@ -258,6 +301,7 @@ router.get("/", async (req: Request, res: Response) => {
const dashboardData = {
totalUsers,
+ communityUsers,
totalCommunities,
statistics: {
weekly: weeklyStats,
@@ -265,6 +309,7 @@ router.get("/", async (req: Request, res: Response) => {
last4Weeks: last4WeeksStats,
},
recentAbsentees: absenteeInfo,
+ villagesWithoutAttendance,
lastUpdated: new Date().toISOString(),
}
diff --git a/server/src/routes/newcomer/newcomerRouter.ts b/server/src/routes/newcomer/newcomerRouter.ts
index a05442c8..a790aa12 100644
--- a/server/src/routes/newcomer/newcomerRouter.ts
+++ b/server/src/routes/newcomer/newcomerRouter.ts
@@ -8,8 +8,12 @@ import {
userDatabase,
worshipScheduleDatabase,
} from "../../model/dataSource"
-import { checkJwt } from "../../util/util"
-import { EducationLecture, NewcomerStatus } from "../../entity/types"
+import { checkJwt, hasPermission, hasPermissionFromReq } from "../../util/util"
+import {
+ EducationLecture,
+ NewcomerStatus,
+ PermissionType,
+} from "../../entity/types"
const router = express.Router()
@@ -35,6 +39,16 @@ router.post("/", async (req, res) => {
return
}
+ const hasPermission = await hasPermissionFromReq(
+ req,
+ PermissionType.MANAGE_NEWCOMER_INFORMATION,
+ )
+
+ if (!hasPermission) {
+ res.status(403).send({ error: "권한이 없습니다." })
+ return
+ }
+
const {
name,
yearOfBirth,
@@ -122,6 +136,16 @@ router.put("/:id", async (req, res) => {
return
}
+ const hasPermission = await hasPermissionFromReq(
+ req,
+ PermissionType.MANAGE_NEWCOMER_INFORMATION,
+ )
+
+ if (!hasPermission) {
+ res.status(403).send({ error: "권한이 없습니다." })
+ return
+ }
+
const { id } = req.params
const {
name,
@@ -223,6 +247,16 @@ router.delete("/:id", async (req, res) => {
return
}
+ const hasPermission = await hasPermissionFromReq(
+ req,
+ PermissionType.MANAGE_NEWCOMER_INFORMATION,
+ )
+
+ if (!hasPermission) {
+ res.status(403).send({ error: "권한이 없습니다." })
+ return
+ }
+
const { id } = req.params
try {
@@ -246,6 +280,16 @@ router.get("/", async (req, res) => {
return
}
+ const hasPermission = await hasPermissionFromReq(
+ req,
+ PermissionType.MANAGE_NEWCOMER_INFORMATION,
+ )
+
+ if (!hasPermission) {
+ res.status(403).send({ error: "권한이 없습니다." })
+ return
+ }
+
try {
const status = req.query.status as NewcomerStatus | undefined
@@ -310,6 +354,16 @@ router.get("/education", async (req, res) => {
return
}
+ const hasPermission = await hasPermissionFromReq(
+ req,
+ PermissionType.MANAGE_NEWCOMER_EDUCATION,
+ )
+
+ if (!hasPermission) {
+ res.status(403).send({ error: "권한이 없습니다." })
+ return
+ }
+
try {
const status = req.query.status as NewcomerStatus | undefined
@@ -334,6 +388,12 @@ router.get("/education", async (req, res) => {
order: {
createdAt: "DESC",
},
+ select: {
+ id: true,
+ name: true,
+ yearOfBirth: true,
+ gender: true,
+ },
})
// 최근 8주간의 예배 스케줄 조회
@@ -382,13 +442,9 @@ router.get("/education", async (req, res) => {
name: newcomer.name,
yearOfBirth: newcomer.yearOfBirth,
gender: newcomer.gender,
- phone: newcomer.phone,
- address: newcomer.address,
- occupation: newcomer.occupation,
visitPath: newcomer.visitPath,
registrationMotivation: newcomer.registrationMotivation,
faithLevel: newcomer.faithLevel,
- previousChurch: newcomer.previousChurch,
carNumber: newcomer.carNumber,
birthday: newcomer.birthday,
registrationDate: newcomer.registrationDate,
diff --git a/server/src/util/util.ts b/server/src/util/util.ts
index ef3ed27e..c51b145f 100644
--- a/server/src/util/util.ts
+++ b/server/src/util/util.ts
@@ -16,6 +16,9 @@ export const hashCode = function (content: string) {
.digest("hex")
}
+/**
+ * @deprecated use hasPermissionFromReq instead
+ */
export async function hasPermission(
token: string | undefined,
permissionType: PermissionType,