From cb38462164a1fde9bc4cdff8640904fb7ccefe0b Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 10:33:50 +0900 Subject: [PATCH 01/17] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/api/group-service.ts} | 4 ++-- apps/cs/src/entities/group/index.ts | 6 ++++++ .../group/model/group-interface.ts} | 0 .../pages/groups/model/groups-table-columns.tsx | 2 +- apps/cs/src/pages/groups/ui/group-form.tsx | 2 +- apps/cs/src/pages/groups/ui/groups-dialog.tsx | 17 ++++++++++------- apps/cs/src/pages/groups/ui/groups-section.tsx | 4 ++-- 7 files changed, 22 insertions(+), 13 deletions(-) rename apps/cs/src/{pages/groups/api/groups-service.ts => entities/group/api/group-service.ts} (94%) create mode 100644 apps/cs/src/entities/group/index.ts rename apps/cs/src/{pages/groups/model/groups-interface.ts => entities/group/model/group-interface.ts} (100%) diff --git a/apps/cs/src/pages/groups/api/groups-service.ts b/apps/cs/src/entities/group/api/group-service.ts similarity index 94% rename from apps/cs/src/pages/groups/api/groups-service.ts rename to apps/cs/src/entities/group/api/group-service.ts index 1ead02087..fd1e879fa 100644 --- a/apps/cs/src/pages/groups/api/groups-service.ts +++ b/apps/cs/src/entities/group/api/group-service.ts @@ -6,7 +6,7 @@ import type { GroupList, GroupResponseDto, GroupFormData, -} from "../model/groups-interface"; +} from "../model/group-interface"; export class GroupsApiService implements GroupService { // 그룹 목록 조회 @@ -56,4 +56,4 @@ export class GroupsApiService implements GroupService { } } -export const GroupsApi = new GroupsApiService(); +export const GroupApi = new GroupsApiService(); diff --git a/apps/cs/src/entities/group/index.ts b/apps/cs/src/entities/group/index.ts new file mode 100644 index 000000000..da1d72a41 --- /dev/null +++ b/apps/cs/src/entities/group/index.ts @@ -0,0 +1,6 @@ +export { GroupApi } from "./api/group-service"; +export { + groupFormSchema, + type GroupFormData, + type GroupListResponseDto, +} from "./model/group-interface"; diff --git a/apps/cs/src/pages/groups/model/groups-interface.ts b/apps/cs/src/entities/group/model/group-interface.ts similarity index 100% rename from apps/cs/src/pages/groups/model/groups-interface.ts rename to apps/cs/src/entities/group/model/group-interface.ts diff --git a/apps/cs/src/pages/groups/model/groups-table-columns.tsx b/apps/cs/src/pages/groups/model/groups-table-columns.tsx index 40cd9042c..70f91efa0 100644 --- a/apps/cs/src/pages/groups/model/groups-table-columns.tsx +++ b/apps/cs/src/pages/groups/model/groups-table-columns.tsx @@ -1,6 +1,6 @@ import { type ColumnDef } from "@tanstack/react-table"; import { Button } from "@common/components"; -import { type GroupListResponseDto } from "./groups-interface"; +import { type GroupListResponseDto } from "@/entities/group"; import { MenuAuthorityDialog } from "../ui/menu-authority-dialog"; import { GroupsDialog } from "../ui/groups-dialog"; diff --git a/apps/cs/src/pages/groups/ui/group-form.tsx b/apps/cs/src/pages/groups/ui/group-form.tsx index 3d8e4498f..423718435 100644 --- a/apps/cs/src/pages/groups/ui/group-form.tsx +++ b/apps/cs/src/pages/groups/ui/group-form.tsx @@ -21,8 +21,8 @@ import { } from "@/shared/ui/form"; import { CodeSelect } from "@/entities/code"; import { SiteSelect } from "@/entities/site"; +import type { GroupFormData } from "@/entities/group"; import { RoleSelect } from "./role-select"; -import type { GroupFormData } from "../model/groups-interface"; type GroupFormProps = { form: UseFormReturn; diff --git a/apps/cs/src/pages/groups/ui/groups-dialog.tsx b/apps/cs/src/pages/groups/ui/groups-dialog.tsx index c2032eb79..d21852886 100644 --- a/apps/cs/src/pages/groups/ui/groups-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/groups-dialog.tsx @@ -15,12 +15,15 @@ import { DialogTrigger, DialogClose, } from "@common/components"; -import { Form } from "@/shared/ui/form"; -import { ENDPOINT } from "@/shared/config"; +import { + GroupApi, + groupFormSchema, + type GroupFormData, +} from "@/entities/group"; import { useAlertStore, useMutationWrapper } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import { Form } from "@/shared/ui/form"; import { GroupForm } from "./group-form"; -import { GroupsApi } from "../api/groups-service"; -import { groupFormSchema, type GroupFormData } from "../model/groups-interface"; type GroupsDialogProps = { groupId?: string; @@ -39,12 +42,12 @@ const GroupsDialog = memo( const { data: group } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, groupId], - queryFn: () => GroupsApi.findGroupById(groupId!), + queryFn: () => GroupApi.findGroupById(groupId!), enabled: !!groupId, }); const { mutateAsync: createGroup } = useMutationWrapper({ - mutationFn: (data: GroupFormData) => GroupsApi.saveGroup(data), + mutationFn: (data: GroupFormData) => GroupApi.saveGroup(data), onSuccess: () => { alert("등록되었습니다.", () => { setOpen(false); @@ -55,7 +58,7 @@ const GroupsDialog = memo( const { mutateAsync: updateGroup } = useMutationWrapper({ mutationFn: (data: GroupFormData) => - GroupsApi.updateGroup(groupId ?? "", data), + GroupApi.updateGroup(groupId ?? "", data), onSuccess: () => { alert("수정되었습니다.", () => { setOpen(false); diff --git a/apps/cs/src/pages/groups/ui/groups-section.tsx b/apps/cs/src/pages/groups/ui/groups-section.tsx index 723eea84d..f5d1e026b 100644 --- a/apps/cs/src/pages/groups/ui/groups-section.tsx +++ b/apps/cs/src/pages/groups/ui/groups-section.tsx @@ -4,10 +4,10 @@ import { useState } from "react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; import { Button } from "@common/components"; +import { GroupApi } from "@/entities/group"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; import { columns } from "../model/groups-table-columns"; -import { GroupsApi } from "../api/groups-service"; import { GroupsDialog } from "./groups-dialog"; type GroupsSectionProps = { @@ -23,7 +23,7 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) { const query = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.GROUPS, pagination], queryFn: () => - GroupsApi.findPageGroup({ + GroupApi.findPageGroup({ page: pagination.pageIndex, size: pagination.pageSize, }), From b79f6ea603b8d484ff21ac737e435a70694db345 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 11:15:58 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cs/app/(with-base-layout)/admin/page.tsx | 2 + apps/cs/src/entities/user/api/user-service.ts | 12 ++ apps/cs/src/entities/user/index.ts | 2 + .../src/entities/user/model/user-interface.ts | 26 ++++ apps/cs/src/pages/admin/index.ts | 1 + .../src/pages/admin/model/table-columns.tsx | 41 +++++ apps/cs/src/pages/admin/ui/admin-page.tsx | 146 ++++++++++++++++++ 7 files changed, 230 insertions(+) create mode 100644 apps/cs/app/(with-base-layout)/admin/page.tsx create mode 100644 apps/cs/src/pages/admin/index.ts create mode 100644 apps/cs/src/pages/admin/model/table-columns.tsx create mode 100644 apps/cs/src/pages/admin/ui/admin-page.tsx diff --git a/apps/cs/app/(with-base-layout)/admin/page.tsx b/apps/cs/app/(with-base-layout)/admin/page.tsx new file mode 100644 index 000000000..a7633e201 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/admin/page.tsx @@ -0,0 +1,2 @@ +import { AdminPage } from "@/pages/admin"; +export default AdminPage; diff --git a/apps/cs/src/entities/user/api/user-service.ts b/apps/cs/src/entities/user/api/user-service.ts index d7718c523..482d51e16 100644 --- a/apps/cs/src/entities/user/api/user-service.ts +++ b/apps/cs/src/entities/user/api/user-service.ts @@ -16,6 +16,7 @@ import type { UserVerifyData, UserEmailData, UserIdData, + AdminUserList, } from "../model/user-interface"; export class UserApiService implements UserService { @@ -208,6 +209,17 @@ export class UserApiService implements UserService { ); return response.data; } + + // 관리자 목록 조회 + public async findPageAdminUser( + params: UserSearchParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/admin/list`, + { params }, + ); + return response.data; + } } export const UserApi = new UserApiService(); diff --git a/apps/cs/src/entities/user/index.ts b/apps/cs/src/entities/user/index.ts index 7240c1424..4ad247761 100644 --- a/apps/cs/src/entities/user/index.ts +++ b/apps/cs/src/entities/user/index.ts @@ -10,4 +10,6 @@ export { type UserDetailResponse, type UserPasswordUpdateData, type UserUpdateData, + type AdminUserList, + type AdminUserListResponse, } from "./model/user-interface"; diff --git a/apps/cs/src/entities/user/model/user-interface.ts b/apps/cs/src/entities/user/model/user-interface.ts index dd250bd9e..69320e8c7 100644 --- a/apps/cs/src/entities/user/model/user-interface.ts +++ b/apps/cs/src/entities/user/model/user-interface.ts @@ -103,6 +103,28 @@ export interface UserSttsStatsResponse { cnt: number; } +// 관리자 목록 응답 인터페이스 +export interface AdminUserListResponse { + userUnqId: string; + userId: string; + userName: string; + groupId: string; + groupNm: string; + userStateCode: string; + userStateCodeName: string; + laeSeCd: string; + laeSeNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + instSeCd: string; + instSeNm: string; + dtlInstCd: string; + dtlInstNm: string; + mdfr: string; + mdfrNm: string; + mdfcnDt: string; +} + // 사용자 수정 요청 스키마 export const userUpdateSchema = z.object({ userId: z.string().optional(), @@ -264,6 +286,7 @@ export type UserIdData = z.infer; // 페이지네이션 타입 정의 export type UserList = PageableList; export type UserSttsStatsList = UserSttsStatsResponse[]; +export type AdminUserList = PageableList; // 사용자 서비스 인터페이스 export interface UserService { @@ -332,4 +355,7 @@ export interface UserService { // 사용자 아이디 중복 체크 existsUserId: (data: UserIdData) => Promise; + + // 관리자 목록 조회 + findPageAdminUser: (params: UserSearchParams) => Promise; } diff --git a/apps/cs/src/pages/admin/index.ts b/apps/cs/src/pages/admin/index.ts new file mode 100644 index 000000000..b8f41285c --- /dev/null +++ b/apps/cs/src/pages/admin/index.ts @@ -0,0 +1 @@ +export { AdminPage } from "./ui/admin-page"; diff --git a/apps/cs/src/pages/admin/model/table-columns.tsx b/apps/cs/src/pages/admin/model/table-columns.tsx new file mode 100644 index 000000000..6b6574749 --- /dev/null +++ b/apps/cs/src/pages/admin/model/table-columns.tsx @@ -0,0 +1,41 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { AdminUserList } from "@/entities/user"; + +export const columns: ColumnDef[] = [ + { + id: "no", + header: "NO", + cell: ({ row, table }) => { + const totalRows = table.getRowCount(); + const currentPage = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + return totalRows - (currentPage * pageSize + row.index); + }, + }, + { + accessorKey: "userId", + header: "관리자ID", + }, + { + accessorKey: "userName", + header: "관리자명", + }, + { + accessorKey: "userStateCodeName", + header: "관리자구분", + }, + { + accessorKey: "groupNm", + header: "소속", + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"), + }, +]; diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx new file mode 100644 index 000000000..8cf329c78 --- /dev/null +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; +import { + Button, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import { UserApi, type UserSearchParams } from "@/entities/user"; +import { ENDPOINT } from "@/shared/config"; +import { useDataTable } from "@/shared/lib"; +import { columns } from "../model/table-columns"; +import { GroupApi } from "@/entities/group"; + +function AdminPage() { + const [adminQuery, setAdminQuery] = useState({ + keywordType: "", + keyword: "", + grpId: "", + }); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: groupOptions } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.GROUPS], + queryFn: () => + GroupApi.findPageGroup({ + delYn: "N", + mngrYn: "Y", + page: 0, + size: 99, + }), + select: (data) => + data.content.map((item) => ({ + groupId: item.groupId, + groupNm: item.groupNm, + })), + }); + + const query = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination], + queryFn: () => + UserApi.findPageAdminUser({ + ...adminQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + }); + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns, + state: { + pagination, + }, + manualPagination: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + query.refetch(); + }; + + return ( +
+
+
+
+ + +
+
+ + + + setAdminQuery({ ...adminQuery, keyword: e.target.value }) + } + placeholder="검색어 입력" + /> +
+ +
+
+
+
+
+

관리자 목록

+
+
+
{tableComponent}
+
+
+ ); +} + +export { AdminPage }; From ab433fd14b70470a71546cc93786b20f87371e54 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 11:27:28 +0900 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group-user/api/group-user-service.ts} | 2 +- apps/cs/src/entities/group-user/index.ts | 2 ++ .../group-user/model/group-user-interface.ts} | 0 apps/cs/src/pages/groups/model/group-users-table-columns.tsx | 2 +- apps/cs/src/pages/groups/ui/additional-user-dialog.tsx | 2 +- apps/cs/src/pages/groups/ui/group-users-section.tsx | 2 +- 6 files changed, 6 insertions(+), 4 deletions(-) rename apps/cs/src/{pages/groups/api/group-users-service.ts => entities/group-user/api/group-user-service.ts} (97%) create mode 100644 apps/cs/src/entities/group-user/index.ts rename apps/cs/src/{pages/groups/model/group-users-interface.ts => entities/group-user/model/group-user-interface.ts} (100%) diff --git a/apps/cs/src/pages/groups/api/group-users-service.ts b/apps/cs/src/entities/group-user/api/group-user-service.ts similarity index 97% rename from apps/cs/src/pages/groups/api/group-users-service.ts rename to apps/cs/src/entities/group-user/api/group-user-service.ts index 5af60a9ef..85355a88f 100644 --- a/apps/cs/src/pages/groups/api/group-users-service.ts +++ b/apps/cs/src/entities/group-user/api/group-user-service.ts @@ -8,7 +8,7 @@ import type { PageGroupUserList, GroupUserSaveData, GroupUserUpdateData, -} from "../model/group-users-interface"; +} from "../model/group-user-interface"; export class GroupUsersApiService implements GroupUserService { // 그룹 사용자 목록 조회 diff --git a/apps/cs/src/entities/group-user/index.ts b/apps/cs/src/entities/group-user/index.ts new file mode 100644 index 000000000..239786d57 --- /dev/null +++ b/apps/cs/src/entities/group-user/index.ts @@ -0,0 +1,2 @@ +export { GroupUsersApi } from "./api/group-user-service"; +export { type GroupUserListResponseDto } from "./model/group-user-interface"; diff --git a/apps/cs/src/pages/groups/model/group-users-interface.ts b/apps/cs/src/entities/group-user/model/group-user-interface.ts similarity index 100% rename from apps/cs/src/pages/groups/model/group-users-interface.ts rename to apps/cs/src/entities/group-user/model/group-user-interface.ts diff --git a/apps/cs/src/pages/groups/model/group-users-table-columns.tsx b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx index 1f36b9867..8d1054aac 100644 --- a/apps/cs/src/pages/groups/model/group-users-table-columns.tsx +++ b/apps/cs/src/pages/groups/model/group-users-table-columns.tsx @@ -1,6 +1,6 @@ import { Checkbox } from "@common/components"; import { type Table, type Row, type ColumnDef } from "@tanstack/react-table"; -import { type GroupUserListResponseDto } from "./group-users-interface"; +import { type GroupUserListResponseDto } from "@/entities/group-user"; export const columns: ColumnDef[] = [ { diff --git a/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx b/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx index 7de293232..6a415e19e 100644 --- a/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx @@ -28,9 +28,9 @@ import { type UserListResponse, type UserSearchParams, } from "@/entities/user"; +import { GroupUsersApi } from "@/entities/group-user"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useDataTable } from "@/shared/lib"; -import { GroupUsersApi } from "../api/group-users-service"; type AdditionalUserDialogProps = { groupId?: string; diff --git a/apps/cs/src/pages/groups/ui/group-users-section.tsx b/apps/cs/src/pages/groups/ui/group-users-section.tsx index 1193de1d1..cadbb6135 100644 --- a/apps/cs/src/pages/groups/ui/group-users-section.tsx +++ b/apps/cs/src/pages/groups/ui/group-users-section.tsx @@ -12,10 +12,10 @@ import { type RowSelectionState, } from "@tanstack/react-table"; import { Button } from "@common/components"; +import { GroupUsersApi } from "@/entities/group-user"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useDataTable } from "@/shared/lib"; import { columns } from "../model/group-users-table-columns"; -import { GroupUsersApi } from "../api/group-users-service"; import { AdditionalUserDialog } from "./additional-user-dialog"; type GroupUsersSectionProps = { From 46a96a04fb201f682dc6cdba1c4c307bc5cf86c0 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 12:02:09 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/ui/add-admin-dialog.tsx | 253 ++++++++++++++++++ apps/cs/src/pages/admin/ui/admin-page.tsx | 10 +- ...al-user-dialog.tsx => add-user-dialog.tsx} | 4 +- .../pages/groups/ui/group-users-section.tsx | 4 +- ...columns.tsx => add-user-table-columns.tsx} | 0 ...al-user-dialog.tsx => add-user-dialog.tsx} | 11 +- .../ui/user-organization-section.tsx | 4 +- 7 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 apps/cs/src/pages/admin/ui/add-admin-dialog.tsx rename apps/cs/src/pages/groups/ui/{additional-user-dialog.tsx => add-user-dialog.tsx} (97%) rename apps/cs/src/pages/organization/model/{additional-user-table-columns.tsx => add-user-table-columns.tsx} (100%) rename apps/cs/src/pages/organization/ui/{additional-user-dialog.tsx => add-user-dialog.tsx} (93%) diff --git a/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx new file mode 100644 index 000000000..1723f39f0 --- /dev/null +++ b/apps/cs/src/pages/admin/ui/add-admin-dialog.tsx @@ -0,0 +1,253 @@ +"use client"; + +import { useState } from "react"; +import { + keepPreviousData, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, + type ColumnDef, +} from "@tanstack/react-table"; +import { + Button, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter, + Input, + Checkbox, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Label, +} from "@common/components"; +import { + UserApi, + type UserListResponse, + type UserSearchParams, +} from "@/entities/user"; +import { GroupApi } from "@/entities/group"; +import { GroupUsersApi } from "@/entities/group-user"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useDataTable } from "@/shared/lib"; + +function AddAdminDialog() { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + + const [open, setOpen] = useState(false); + const [userSearchQuery, setUserSearchQuery] = useState({ + keywordType: "", + keyword: "", + }); + const [selectedGroupId, setSelectedGroupId] = useState(""); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + + const { data: groupOptions } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.GROUPS], + queryFn: () => + GroupApi.findPageGroup({ + delYn: "N", + mngrYn: "Y", + page: 0, + size: 99, + }), + select: (data) => + data.content.map((item) => ({ + groupId: item.groupId, + groupNm: item.groupNm, + })), + enabled: open, + }); + + const query = useQuery({ + queryKey: [ENDPOINT.USER_SERVICE.USERS, pagination, "normal-user"], + queryFn: () => + UserApi.findPageUser({ + ...userSearchQuery, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + enabled: open, + placeholderData: keepPreviousData, + }); + + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="행 선택" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "userId", + header: "사용자ID", + }, + { + accessorKey: "userName", + header: "이름", + }, + { + accessorKey: "ogdpSeNm", + header: "소속", + }, + { + accessorKey: "mphn", + header: "연락처", + }, + ]; + + const { tableComponent } = useDataTable({ + query, + tableOptions: { + columns: columns, + state: { pagination, rowSelection }, + manualPagination: true, + enableRowSelection: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getRowId: (row) => row.userUnqId, + }, + dataTableOptions: { + viewPagination: true, + }, + }); + + const handleAddAdmins = async () => { + const selectedRows = Object.keys(rowSelection); + if (selectedRows.length === 0 || !selectedGroupId) { + alert("그룹과 사용자를 선택해주세요."); + return; + } + + try { + const usersToAdd = selectedRows.map((userUnqId) => ({ + mode: "C", + groupId: selectedGroupId, + userUnqId, + })); + + await GroupUsersApi.saveGroupUser(usersToAdd); + + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + }); + alert("관리자 추가를 완료했습니다.", () => { + setOpen(false); + setRowSelection({}); + setSelectedGroupId(""); + }); + } catch (error) { + console.error(error); + } + }; + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); + setPagination({ pageIndex: 0, pageSize: 10 }); + query.refetch(); + }; + + return ( + + + + + + + 관리자 추가 + +
+ + + setUserSearchQuery({ + ...userSearchQuery, + keyword: e.target.value, + }) + } + className="!text-[1.3rem]" + placeholder="검색어 입력" + /> + +
+ {tableComponent} +
+
+ + +
+ + + + +
+
+ ); +} + +export { AddAdminDialog }; diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx index 8cf329c78..6507e98fa 100644 --- a/apps/cs/src/pages/admin/ui/admin-page.tsx +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -13,11 +13,12 @@ import { SelectTrigger, SelectValue, } from "@common/components"; +import { GroupApi } from "@/entities/group"; import { UserApi, type UserSearchParams } from "@/entities/user"; import { ENDPOINT } from "@/shared/config"; import { useDataTable } from "@/shared/lib"; import { columns } from "../model/table-columns"; -import { GroupApi } from "@/entities/group"; +import { AddAdminDialog } from "./add-admin-dialog"; function AdminPage() { const [adminQuery, setAdminQuery] = useState({ @@ -134,7 +135,12 @@ function AdminPage() {
-

관리자 목록

+

+ 관리자 목록(총 {query.data?.totalElements}명) +

+
+ +
{tableComponent}
diff --git a/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx similarity index 97% rename from apps/cs/src/pages/groups/ui/additional-user-dialog.tsx rename to apps/cs/src/pages/groups/ui/add-user-dialog.tsx index 6a415e19e..aafba70db 100644 --- a/apps/cs/src/pages/groups/ui/additional-user-dialog.tsx +++ b/apps/cs/src/pages/groups/ui/add-user-dialog.tsx @@ -36,7 +36,7 @@ type AdditionalUserDialogProps = { groupId?: string; }; -function AdditionalUserDialog({ groupId }: AdditionalUserDialogProps) { +function AddUserDialog({ groupId }: AdditionalUserDialogProps) { const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); @@ -188,4 +188,4 @@ function AdditionalUserDialog({ groupId }: AdditionalUserDialogProps) { ); } -export { AdditionalUserDialog }; +export { AddUserDialog }; diff --git a/apps/cs/src/pages/groups/ui/group-users-section.tsx b/apps/cs/src/pages/groups/ui/group-users-section.tsx index cadbb6135..b74ce4db9 100644 --- a/apps/cs/src/pages/groups/ui/group-users-section.tsx +++ b/apps/cs/src/pages/groups/ui/group-users-section.tsx @@ -16,7 +16,7 @@ import { GroupUsersApi } from "@/entities/group-user"; import { ENDPOINT } from "@/shared/config"; import { useAlertStore, useDataTable } from "@/shared/lib"; import { columns } from "../model/group-users-table-columns"; -import { AdditionalUserDialog } from "./additional-user-dialog"; +import { AddUserDialog } from "./add-user-dialog"; type GroupUsersSectionProps = { groupId?: string; @@ -95,7 +95,7 @@ function GroupUsersSection({ groupId }: GroupUsersSectionProps) {

그룹 사용자 목록

- + + diff --git a/apps/cs/src/pages/admin/ui/admin-page.tsx b/apps/cs/src/pages/admin/ui/admin-page.tsx index 6507e98fa..2599fa752 100644 --- a/apps/cs/src/pages/admin/ui/admin-page.tsx +++ b/apps/cs/src/pages/admin/ui/admin-page.tsx @@ -1,8 +1,14 @@ "use client"; import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { getCoreRowModel, type PaginationState } from "@tanstack/react-table"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { + getCoreRowModel, + type PaginationState, + type RowSelectionState, + type Table, + type Row, +} from "@tanstack/react-table"; import { Button, Input, @@ -12,15 +18,26 @@ import { SelectItem, SelectTrigger, SelectValue, + Checkbox, } from "@common/components"; import { GroupApi } from "@/entities/group"; -import { UserApi, type UserSearchParams } from "@/entities/user"; +import { + UserApi, + type UserSearchParams, + type AdminUserListResponse, +} from "@/entities/user"; import { ENDPOINT } from "@/shared/config"; -import { useDataTable } from "@/shared/lib"; +import { useDataTable, useAlertStore } from "@/shared/lib"; import { columns } from "../model/table-columns"; import { AddAdminDialog } from "./add-admin-dialog"; +import { GroupUsersApi } from "@/entities/group-user"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; function AdminPage() { + const queryClient = useQueryClient(); + const alert = useAlertStore((state) => state.setMessage); + const { confirm } = useConfirm(); + const [adminQuery, setAdminQuery] = useState({ keywordType: "", keyword: "", @@ -30,6 +47,7 @@ function AdminPage() { pageIndex: 0, pageSize: 10, }); + const [rowSelection, setRowSelection] = useState({}); const { data: groupOptions } = useQuery({ queryKey: [ENDPOINT.CMS_SERVICE.GROUPS], @@ -57,16 +75,47 @@ function AdminPage() { }), }); + const adminColumns = [ + { + id: "select", + header: ({ table }: { table: Table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="전체 선택" + /> + ), + cell: ({ row }: { row: Row }) => ( + row.toggleSelected(!!value)} + aria-label="행 선택" + /> + ), + enableSorting: false, + enableHiding: false, + }, + ...columns, + ]; + const { tableComponent } = useDataTable({ query, tableOptions: { - columns, + columns: adminColumns, state: { pagination, + rowSelection, }, manualPagination: true, + enableRowSelection: true, + enableMultiRowSelection: true, getCoreRowModel: getCoreRowModel(), onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getRowId: (row) => row.userUnqId, }, dataTableOptions: { viewPagination: true, @@ -78,6 +127,37 @@ function AdminPage() { query.refetch(); }; + const handleDeleteAdmins = async () => { + const selectedRows = Object.keys(rowSelection); + + if (!(await confirm("관리자를 삭제하시겠습니까?"))) { + return; + } + + try { + const usersToDelete = selectedRows.map((userUnqId) => { + const admin = query.data?.content.find( + (item) => item.userUnqId === userUnqId, + ); + return { + mode: "D", + groupId: admin?.groupId || "", + userUnqId, + }; + }); + + await GroupUsersApi.saveGroupUser(usersToDelete); + + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.USER_SERVICE.USERS], + }); + alert("관리자 삭제를 완료했습니다."); + setRowSelection({}); + } catch (error) { + console.error(error); + } + }; + return (
@@ -110,7 +190,7 @@ function AdminPage() { setAdminQuery({ ...adminQuery, keywordType: value }) } > - + @@ -119,7 +199,7 @@ function AdminPage() { setAdminQuery({ ...adminQuery, keyword: e.target.value }) @@ -140,6 +220,13 @@ function AdminPage() {
+
From 4ef352f77082759fb3399fd26f6fbb59a1169b51 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 17:13:47 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=ED=86=A0=EA=B8=80=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/components/src/ui/header/index.tsx | 4 +- .../common/components/src/ui/lnb/index.tsx | 100 +++++++++++++----- .../components/src/ui/sidebar/index.tsx | 4 +- .../components/src/ui/sidebar/sidebar.css | 22 +++- 4 files changed, 95 insertions(+), 35 deletions(-) diff --git a/packages/common/components/src/ui/header/index.tsx b/packages/common/components/src/ui/header/index.tsx index 41384a766..6e0f89393 100644 --- a/packages/common/components/src/ui/header/index.tsx +++ b/packages/common/components/src/ui/header/index.tsx @@ -20,6 +20,7 @@ import { DropdownMenuItem, } from "../dropdown-menu"; import { Button } from "../button"; +import { SidebarTrigger } from "../sidebar"; const { SITE_ID } = getConfig(); @@ -91,7 +92,8 @@ export function Header({ viewOnly }: { viewOnly?: boolean }) { return (
-
+
+
diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index eac98faaa..e2f12b0c1 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -12,6 +12,9 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar, } from "../sidebar"; import { Collapsible, @@ -79,7 +82,14 @@ export function LNB({ } }, [menuList, currentPath]); - function MenuItem({ item }: { item: MenuItem }) { + function MenuItem({ + item, + isSubMenu, + }: { + item: MenuItem; + isSubMenu?: boolean; + }) { + const { open: isSidebarOpen } = useSidebar(); const isMatched = isPathMatched(item.urlPath); const handleClick = () => { @@ -91,24 +101,37 @@ export function LNB({ return ; } + const MenuItem = isSubMenu ? SidebarMenuSubItem : SidebarMenuItem; + const MenuButton = isSubMenu ? SidebarMenuSubButton : SidebarMenuButton; + return ( - - + - - {item.menuKornNm} - - + + + {item.menuKornNm} + + + ); } function MenuGroup({ item }: { item: MenuItem }) { + const { open: isSidebarOpen } = useSidebar(); const isOpen = openMenuIds.includes(item.menuCd); const handleOpenChange = (open: boolean) => { @@ -127,30 +150,48 @@ export function LNB({ }; return ( - + -
- -
- - - {item.menuKornNm} - -
- -
-
+ +
+ + + {item.menuKornNm} + +
+ +
- + {item.children?.map((child) => ( - + ))} @@ -160,7 +201,10 @@ export function LNB({ } return ( - + {menuList?.map((item) => )} diff --git a/packages/common/components/src/ui/sidebar/index.tsx b/packages/common/components/src/ui/sidebar/index.tsx index d9545b8ae..2f9382784 100644 --- a/packages/common/components/src/ui/sidebar/index.tsx +++ b/packages/common/components/src/ui/sidebar/index.tsx @@ -28,7 +28,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "22rem"; const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_WIDTH_ICON = "6rem"; const SIDEBAR_KEYBOARD_SHORTCUT = "b"; type SidebarContextProps = { @@ -530,7 +530,7 @@ const SidebarMenuItem = React.forwardRef< SidebarMenuItem.displayName = "SidebarMenuItem"; const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-16 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", { variants: { variant: { diff --git a/packages/common/components/src/ui/sidebar/sidebar.css b/packages/common/components/src/ui/sidebar/sidebar.css index 7bd41248b..b41ea1ace 100644 --- a/packages/common/components/src/ui/sidebar/sidebar.css +++ b/packages/common/components/src/ui/sidebar/sidebar.css @@ -23,12 +23,19 @@ background: none !important; color: #fff !important; } - [data-sidebar="menu-button"]:hover { + [data-sidebar="trigger"]:hover, + [data-sidebar="menu-button"]:hover, + [data-sidebar="menu-sub-button"]:hover { background: rgba(0, 0, 0, 0.1) !important; } - [data-sidebar="menu-button"][data-matched="true"] { + [data-sidebar="trigger"][data-matched="true"], + [data-sidebar="menu-button"][data-matched="true"], + [data-sidebar="menu-sub-button"][data-matched="true"] { background: rgba(0, 0, 0, 0.2) !important; } + [data-sidebar="trigger"] > svg { + color: #fff !important; + } } [data-theme="red"] { @@ -58,11 +65,18 @@ background: none !important; color: #fff !important; } - [data-sidebar="menu-button"]:hover { + [data-sidebar="trigger"]:hover, + [data-sidebar="menu-button"]:hover, + [data-sidebar="menu-sub-button"]:hover { background: rgba(0, 0, 0, 0.1) !important; } - [data-sidebar="menu-button"][data-matched="true"] { + [data-sidebar="trigger"][data-matched="true"], + [data-sidebar="menu-button"][data-matched="true"], + [data-sidebar="menu-sub-button"][data-matched="true"] { background: rgba(0, 0, 0, 0.2) !important; } + [data-sidebar="trigger"] > svg { + color: #fff !important; + } } } From 6b8d24f95f93584e0225cd5ce8c05fad137854c6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 19 Jun 2025 17:28:15 +0900 Subject: [PATCH 07/17] =?UTF-8?q?chore:=20=EC=82=AC=EC=9D=B4=EB=93=9C?= =?UTF-8?q?=EB=B0=94=20=EB=8F=99=EC=9E=91=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/components/src/ui/lnb/index.tsx | 8 ++++++-- packages/common/components/src/ui/sidebar/index.tsx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/common/components/src/ui/lnb/index.tsx b/packages/common/components/src/ui/lnb/index.tsx index e2f12b0c1..40249fee5 100644 --- a/packages/common/components/src/ui/lnb/index.tsx +++ b/packages/common/components/src/ui/lnb/index.tsx @@ -89,10 +89,11 @@ export function LNB({ item: MenuItem; isSubMenu?: boolean; }) { - const { open: isSidebarOpen } = useSidebar(); + const { open: isSidebarOpen, setOpen } = useSidebar(); const isMatched = isPathMatched(item.urlPath); const handleClick = () => { + if (!isSidebarOpen) setOpen(true); setCookie("menu-code-key", item.menuCd.toString()); handleMenuClick?.(item); }; @@ -114,6 +115,7 @@ export function LNB({ )} data-matched={isMatched} onClick={handleClick} + tooltip={item.menuKornNm} > { + if (!isSidebarOpen) setOpen(true); if (open) { // 열 때는 부모들도 함께 열기 if (!openMenuIds.includes(item.menuCd)) { @@ -162,6 +165,7 @@ export function LNB({ "flex h-auto items-center py-4 pr-2 text-[1.4rem] group-data-[state=open]/collapsible:bg-transparent", !isSidebarOpen && "justify-center", )} + tooltip={item.menuKornNm} >