diff --git a/apps/cs/src/entities/code/index.ts b/apps/cs/src/entities/code/index.ts index 0b6633f4b..dc74bbc47 100644 --- a/apps/cs/src/entities/code/index.ts +++ b/apps/cs/src/entities/code/index.ts @@ -1 +1,3 @@ +export { CodeApi } from "./api/code-service"; +export type { ICode } from "./model/code-interface"; export { CodeSelect } from "./ui/code-select"; diff --git a/apps/cs/src/entities/user-organization/api/user-organization-service.ts b/apps/cs/src/entities/user-organization/api/user-organization-service.ts index daa6e2ffe..cbd17363f 100644 --- a/apps/cs/src/entities/user-organization/api/user-organization-service.ts +++ b/apps/cs/src/entities/user-organization/api/user-organization-service.ts @@ -1,17 +1,39 @@ import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { - UserOrganization, + TreeUserOrganization, + UserOrganizationParams, + UserOrganizationList, UserOrganizationService, + SearchUserParams, + SearchUserList, } from "../model/user-organization-interface"; export class UserOrganizationApiService implements UserOrganizationService { - public async getUserOrganizationTreeList(): Promise { - const response = await client.get( + public async getUserOrganizationList( + params: UserOrganizationParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`, + { params }, + ); + return response.data; + } + + public async getUserOrganizationTreeList(): Promise { + const response = await client.get( `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`, ); return response.data; } + + public async searchUser(params: SearchUserParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`, + { params }, + ); + return response.data; + } } export const UserOrganizationApi = new UserOrganizationApiService(); diff --git a/apps/cs/src/entities/user-organization/index.ts b/apps/cs/src/entities/user-organization/index.ts index 47463958b..df4ed2321 100644 --- a/apps/cs/src/entities/user-organization/index.ts +++ b/apps/cs/src/entities/user-organization/index.ts @@ -1,5 +1,8 @@ export { UserOrganizationApi } from "./api/user-organization-service"; -export type { - User, - UserOrganization, +export { + userOrganizationFormSchema, + type TreeUserOrganization, + type UserOrganizationFormData, + type User, + type SearchUser, } from "./model/user-organization-interface"; diff --git a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts index c788b44e0..bbacd1a10 100644 --- a/apps/cs/src/entities/user-organization/model/user-organization-interface.ts +++ b/apps/cs/src/entities/user-organization/model/user-organization-interface.ts @@ -1,18 +1,81 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface UserOrganizationParams extends PageableParams { + keywordType?: string; + keyword?: string; + ognzSn?: number; + userUnqId?: string; + jbpsSeCd?: string; + jbgdSeCd?: string; +} + +export interface SearchUserParams extends PageableParams { + userNm?: string; + ogdpSeCd?: string; +} + export interface User { + ognzSn: number; + userUnqId: string; + userUnqNm: string; + jbpsSeCd: string; + jbpsSeNm: string; + jbgdSeCd: string; + jbgdSeNm: string; + sortSeq: number; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; + mode: string; +} + +export interface SearchUser { + userUnqId: string; + userNm: string; + ogdpSeCd: string; + ogdpSeNm: string; + regDt: string; +} + +export interface TreeUser { userUnqId: string; userNm: string; jbpsSeCd: string; jbgdSeCd: string; } -export interface UserOrganization { +export interface TreeUserOrganization { ognzSn: number; upOgnzSn: number; ognzNm: string; - users: User[]; - children: UserOrganization[]; + users: TreeUser[]; + children: TreeUserOrganization[]; } +export const userOrganizationFormSchema = z.object({ + userOrganization: z.array( + z.object({ + mode: z.string(), + ognzSn: z.number(), + userUnqId: z.string(), + jbpsSeCd: z.string(), + jbgdSeCd: z.string(), + sortSeq: z.number(), + }), + ), +}); + +export type UserOrganizationList = PageableList; +export type SearchUserList = PageableList; +export type UserOrganizationFormData = z.infer< + typeof userOrganizationFormSchema +>; + export interface UserOrganizationService { - getUserOrganizationTreeList(): Promise; + getUserOrganizationList( + params: UserOrganizationParams, + ): Promise; + getUserOrganizationTreeList(): Promise; + searchUser(params: SearchUserParams): Promise; } diff --git a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx index 505532296..8478533fa 100644 --- a/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx +++ b/apps/cs/src/pages/menu/ui/organization-search-dialog.tsx @@ -22,7 +22,7 @@ import { cn } from "@common/components/lib"; import { UserOrganizationApi, type User, - type UserOrganization, + type TreeUserOrganization, } from "@/entities/user-organization"; import { ENDPOINT } from "@/shared/config"; import { @@ -30,7 +30,7 @@ import { generateUserOrganizationTreeData, } from "../lib/utils"; -type TreeModel = UserOrganization | User; +type TreeModel = TreeUserOrganization | User; interface OrganizationSearchDialogProps { handleUserSelect: (user: User) => void; diff --git a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx index e9a9a6d4d..c5517824b 100644 --- a/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx +++ b/apps/cs/src/pages/module/model/role-authorizations-table-columns.tsx @@ -1,7 +1,7 @@ -import { type ColumnDef } from "@tanstack/react-table"; -import type { RoleAuthorizations } from "./role-authorizations-interface"; -import { Checkbox } from "@common/components/ui"; +import type { ColumnDef } from "@tanstack/react-table"; import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components/ui"; +import type { RoleAuthorizations } from "./role-authorizations-interface"; import type { RoleAuthorizationsFormData } from "./role-authorizations-interface"; export const createColumns = ( diff --git a/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx b/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx new file mode 100644 index 000000000..0b6e1e1e3 --- /dev/null +++ b/apps/cs/src/pages/organization/model/additional-user-table-columns.tsx @@ -0,0 +1,39 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components/ui"; +import type { SearchUser } from "@/entities/user-organization"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + accessorKey: "userNm", + header: "이름", + }, + { + accessorKey: "ogdpSeNm", + header: "소속", + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD HH:mm:ss"), + }, +]; diff --git a/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx b/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx new file mode 100644 index 000000000..2378c92d2 --- /dev/null +++ b/apps/cs/src/pages/organization/model/user-organization-table-columns.tsx @@ -0,0 +1,91 @@ +import dayjs from "dayjs"; +import type { UseFormReturn } from "react-hook-form"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox, Select } from "@common/components/ui"; +import type { + User, + UserOrganizationFormData, +} from "@/entities/user-organization"; + +export const createColumns = ( + form: UseFormReturn, + renderJobPositionSelect: ( + props: React.ComponentPropsWithoutRef, + ) => React.ReactNode, + renderJobGradeSelect: ( + props: React.ComponentPropsWithoutRef, + ) => React.ReactNode, +): ColumnDef[] => { + return [ + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + id: "no", + header: "NO", + cell: ({ row }) => row.index + 1, + }, + { + accessorKey: "userUnqNm", + header: "이름", + }, + { + accessorKey: "jbpsSeCd", + header: "직위", + cell: ({ row }) => + renderJobPositionSelect({ + defaultValue: row.original.jbpsSeCd, + value: form.watch(`userOrganization.${row.index}.jbpsSeCd`), + onValueChange: (value) => { + const mode = form.watch(`userOrganization.${row.index}.mode`); + if (mode === "R") + form.setValue(`userOrganization.${row.index}.mode`, "U"); + form.setValue(`userOrganization.${row.index}.jbpsSeCd`, value); + }, + }), + }, + { + accessorKey: "jbgdSeCd", + header: "직급", + cell: ({ row }) => + renderJobGradeSelect({ + defaultValue: row.original.jbgdSeCd, + value: form.watch(`userOrganization.${row.index}.jbgdSeCd`), + onValueChange: (value) => { + const mode = form.watch(`userOrganization.${row.index}.mode`); + if (mode === "R") + form.setValue(`userOrganization.${row.index}.mode`, "U"); + form.setValue(`userOrganization.${row.index}.jbgdSeCd`, value); + }, + }), + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + if (!row.original.mdfcnDt) return ""; + return dayjs(row.original.mdfcnDt).format("YYYY-MM-DD"); + }, + }, + ]; +}; diff --git a/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx new file mode 100644 index 000000000..c9181c578 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/additional-user-dialog.tsx @@ -0,0 +1,110 @@ +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DataTable, + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + Input, + DialogFooter, + DialogClose, +} from "@common/components/ui"; +import { + UserOrganizationApi, + type SearchUser, +} from "@/entities/user-organization"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/additional-user-table-columns"; + +type AdditionalUserDialogProps = { + onAddUsers: (user: SearchUser[]) => void; +} & React.ComponentProps; + +function AdditionalUserDialog({ + onAddUsers, + ...props +}: AdditionalUserDialogProps) { + const [open, setOpen] = useState(false); + const [userNm, setUserNm] = useState(""); + const [rowSelection, setRowSelection] = useState({}); + + const { data: userList, refetch } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION], + queryFn: () => UserOrganizationApi.searchUser({ userNm }), + enabled: false, + }); + + const table = useReactTable({ + data: userList?.content ?? [], + columns, + state: { rowSelection }, + getCoreRowModel: getCoreRowModel(), + onRowSelectionChange: setRowSelection, + }); + + const handleAddUsers = () => { + const selectedRows = table.getSelectedRowModel().rows; + const userData = selectedRows.map((row) => row.original); + if (!userData) return; + onAddUsers(userData); + }; + + const handleCancel = () => { + setRowSelection({}); + setUserNm(""); + }; + + useEffect(() => { + if (open) refetch(); + }, [open]); + + return ( + + + + + + + 콘텐츠 조회 + +
+
+ setUserNm(e.target.value)} + /> + +
+
+ +
+
+ + + + + + +
+
+ ); +} + +export { AdditionalUserDialog }; diff --git a/apps/cs/src/pages/organization/ui/organization-page.tsx b/apps/cs/src/pages/organization/ui/organization-page.tsx index 0be3770cc..a0979db53 100644 --- a/apps/cs/src/pages/organization/ui/organization-page.tsx +++ b/apps/cs/src/pages/organization/ui/organization-page.tsx @@ -20,6 +20,7 @@ import { import { ENDPOINT } from "@/shared/config"; import { useAlertStore } from "@/shared/lib/use-alert-store"; import { OrganizationForm } from "./organization-form"; +import { UserOrganizationSection } from "./user-organization-section"; function OrganizationPage() { const queryClient = useQueryClient(); @@ -226,6 +227,7 @@ function OrganizationPage() { handleUpdate={updateOrganization} handleDelete={deleteOrganization} /> + ); diff --git a/apps/cs/src/pages/organization/ui/user-organization-section.tsx b/apps/cs/src/pages/organization/ui/user-organization-section.tsx new file mode 100644 index 000000000..3bdfe4f19 --- /dev/null +++ b/apps/cs/src/pages/organization/ui/user-organization-section.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from "react"; +import { useFieldArray, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, keepPreviousData } from "@tanstack/react-query"; +import { useReactTable, getCoreRowModel } from "@tanstack/react-table"; +import { cn } from "@common/components/lib"; +import { Button, DataTable } from "@common/components/ui"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components/ui"; +import { + UserOrganizationApi, + userOrganizationFormSchema, + type UserOrganizationFormData, + type User, + type SearchUser, +} from "@/entities/user-organization"; +import { CodeApi, type ICode } from "@/entities/code"; +import { ENDPOINT } from "@/shared/config"; +import { createColumns } from "../model/user-organization-table-columns"; +import { AdditionalUserDialog } from "./additional-user-dialog"; + +type UserOrganizationSectionProps = { + ognzSn?: number | null; +}; + +function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { + const [userList, setUserList] = useState([]); + + const { data } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn], + queryFn: () => + UserOrganizationApi.getUserOrganizationList({ ognzSn: ognzSn! }), + enabled: !!ognzSn, + placeholderData: keepPreviousData, + }); + + const { data: jobPositionCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JPBSCLCD"], + queryFn: () => CodeApi.getCode("JPBSCLCD"), + }); + + const { data: jobGradeCodes } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.CODE, "JDGDCLCD"], + queryFn: () => CodeApi.getCode("JDGDCLCD"), + }); + + const form = useForm({ + resolver: zodResolver(userOrganizationFormSchema), + values: { + userOrganization: [ + ...(data?.content ?? []).map((user) => ({ + mode: user.mode, + ognzSn: user.ognzSn, + userUnqId: user.userUnqId, + jbpsSeCd: user.jbpsSeCd, + jbgdSeCd: user.jbgdSeCd, + sortSeq: user.sortSeq, + })), + ], + }, + }); + + const { append } = useFieldArray({ + control: form.control, + name: "userOrganization", + }); + + const table = useReactTable({ + data: data?.content ?? [], + columns: createColumns( + form, + (props) => , + (props) => , + ), + getCoreRowModel: getCoreRowModel(), + }); + + const handleAddUsers = (users: SearchUser[]) => { + const newForm = users.map((user) => ({ + mode: "C", + ognzSn: ognzSn!, + userUnqId: user.userUnqId, + jbpsSeCd: "", + jbgdSeCd: "", + sortSeq: 0, + })); + append(newForm); + + const newUserList = users.map((user) => ({ + ognzSn: ognzSn!, + userUnqId: user.userUnqId, + userUnqNm: user.userNm, + jbpsSeCd: "", + jbpsSeNm: "", + jbgdSeCd: "", + jbgdSeNm: "", + sortSeq: 0, + mdfcnDt: "", + mdfr: "", + mdfrNm: "", + mode: "C", + })); + setUserList([...newUserList, ...userList]); + }; + + useEffect(() => { + if (data?.content) setUserList(data.content); + }, [data]); + + return ( +
+
+

소속원 목록

+
+ + + +
+
+
+ +
+
+ ); +} + +type CodeSelectProps = React.ComponentPropsWithoutRef & { + codeData?: ICode[]; +}; +const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => { + if (!codeData) return null; + + const defaultValueText = + codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ?? + "선택"; + + return ( + + ); +}; + +export { UserOrganizationSection };