iipbs_cs #16
@@ -1 +1,3 @@
|
||||
export { CodeApi } from "./api/code-service";
|
||||
export type { ICode } from "./model/code-interface";
|
||||
export { CodeSelect } from "./ui/code-select";
|
||||
|
||||
@@ -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<UserOrganization[]> {
|
||||
const response = await client.get<UserOrganization[]>(
|
||||
public async getUserOrganizationList(
|
||||
params: UserOrganizationParams,
|
||||
): Promise<UserOrganizationList> {
|
||||
const response = await client.get<UserOrganizationList>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getUserOrganizationTreeList(): Promise<TreeUserOrganization[]> {
|
||||
const response = await client.get<TreeUserOrganization[]>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/treeuser`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async searchUser(params: SearchUserParams): Promise<SearchUserList> {
|
||||
const response = await client.get<SearchUserList>(
|
||||
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}/user-popup`,
|
||||
{ params },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserOrganizationApi = new UserOrganizationApiService();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<User>;
|
||||
export type SearchUserList = PageableList<SearchUser>;
|
||||
export type UserOrganizationFormData = z.infer<
|
||||
typeof userOrganizationFormSchema
|
||||
>;
|
||||
|
||||
export interface UserOrganizationService {
|
||||
getUserOrganizationTreeList(): Promise<UserOrganization[]>;
|
||||
getUserOrganizationList(
|
||||
params: UserOrganizationParams,
|
||||
): Promise<UserOrganizationList>;
|
||||
getUserOrganizationTreeList(): Promise<TreeUserOrganization[]>;
|
||||
searchUser(params: SearchUserParams): Promise<SearchUserList>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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<SearchUser>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(checked) => 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"),
|
||||
},
|
||||
];
|
||||
@@ -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<UserOrganizationFormData>,
|
||||
renderJobPositionSelect: (
|
||||
props: React.ComponentPropsWithoutRef<typeof Select>,
|
||||
) => React.ReactNode,
|
||||
renderJobGradeSelect: (
|
||||
props: React.ComponentPropsWithoutRef<typeof Select>,
|
||||
) => React.ReactNode,
|
||||
): ColumnDef<User>[] => {
|
||||
return [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(checked) =>
|
||||
table.toggleAllPageRowsSelected(!!checked)
|
||||
}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(checked) => 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");
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -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<typeof Dialog>;
|
||||
|
||||
function AdditionalUserDialog({
|
||||
onAddUsers,
|
||||
...props
|
||||
}: AdditionalUserDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [userNm, setUserNm] = useState("");
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
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 (
|
||||
<Dialog {...props} open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" color="gray">
|
||||
사용자 추가
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>콘텐츠 조회</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="card card-border !m-0 flex w-full items-center gap-2">
|
||||
<Input
|
||||
placeholder="검색어를 입력하세요."
|
||||
className="!text-[1.3rem]"
|
||||
value={userNm}
|
||||
onChange={(e) => setUserNm(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => refetch()}>
|
||||
<i className="diveicon di-search" />
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[40rem] w-full overflow-y-auto">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="!justify-center">
|
||||
<DialogClose asChild>
|
||||
<Button color="white" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={handleAddUsers}>추가</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AdditionalUserDialog };
|
||||
@@ -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}
|
||||
/>
|
||||
<UserOrganizationSection ognzSn={selectedOrganization?.ognzSn} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<User[]>([]);
|
||||
|
||||
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<UserOrganizationFormData>({
|
||||
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) => <CodeSelect codeData={jobPositionCodes} {...props} />,
|
||||
(props) => <CodeSelect codeData={jobGradeCodes} {...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 (
|
||||
<div className={cn("card card-border", !ognzSn && "hidden")}>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">소속원 목록</h3>
|
||||
<div className="flex gap-2">
|
||||
<AdditionalUserDialog onAddUsers={handleAddUsers} />
|
||||
<Button size="sm" color="red">
|
||||
선택삭제
|
||||
</Button>
|
||||
<Button size="sm">저장</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<DataTable table={table} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
|
||||
codeData?: ICode[];
|
||||
};
|
||||
const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => {
|
||||
if (!codeData) return null;
|
||||
|
||||
const defaultValueText =
|
||||
codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ??
|
||||
"선택";
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="w-[12rem]">
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeData.map((item) => (
|
||||
<SelectItem key={item.lwpoCd} value={item.lwpoCd}>
|
||||
{item.lwpoCdNm}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export { UserOrganizationSection };
|
||||
Reference in New Issue
Block a user