iipbs_cs #16

Merged
chanii8 merged 44 commits from iipbs_cs into main 2025-04-30 17:09:18 +09:00
11 changed files with 507 additions and 15 deletions
Showing only changes of commit 086a81aa4b - Show all commits
+2
View File
@@ -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 };