This commit is contained in:
2025-07-14 15:54:27 +09:00
7 changed files with 111 additions and 72 deletions
+22 -18
View File
@@ -1,7 +1,7 @@
import { memo } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
@@ -37,7 +37,7 @@ type ManagementLwpoCodeDialogProps = {
upCd?: string;
children?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onOpenChange?: (open: boolean) => void;
onAfterSave?: () => void; // 콜백 추가
};
@@ -47,21 +47,20 @@ const LwpoCodeManagementDialog = memo(
upCd,
children,
open,
onOpenChange,
onOpenChange,
onAfterSave, // 콜백 사용
...props
}: ManagementLwpoCodeDialogProps) => {
const isRegistration = !lwpoCd;
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
// 상세 데이터
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const { data: detailData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, lwpoCd],
queryFn: () => LwpoCodeApi.lwpoCodeDetail(upCd!, lwpoCd!),
enabled: !!upCd && !isRegistration && open,
});
const { data: upCodeData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.UP_CODE, upCd],
@@ -71,22 +70,25 @@ const LwpoCodeManagementDialog = memo(
const { mutateAsync: createLwpoCode } = useMutationWrapper({
mutationFn: (data: LwpoCodeFormData) => LwpoCodeApi.createLwpoCode(data),
onSuccess: async () => {
onSuccess: () => {
alert("등록되었습니다.", () => {
onOpenChange?.(false);
if (onAfterSave) onAfterSave();
});
onOpenChange?.(false);
onAfterSave?.();
});
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd],
});
},
});
const { mutateAsync: updateLwpoCode } = useMutationWrapper({
mutationFn: (data: LwpoCodeFormData) =>
LwpoCodeApi.updateLwpoCode(upCd!, lwpoCd!, data),
onSuccess: async () => {
onSuccess: () => {
alert("수정되었습니다.", () => {
onOpenChange?.(false);
if (onAfterSave) onAfterSave();
});
onOpenChange?.(false);
onAfterSave?.();
});
},
});
@@ -104,7 +106,6 @@ const LwpoCodeManagementDialog = memo(
delYn: "N",
},
});
const handleRegister = async () => {
if (!upCd) {
@@ -119,7 +120,7 @@ const LwpoCodeManagementDialog = memo(
upCd: upCd,
sortSeq: Number(rawValues.sortSeq) || 0,
};
await createLwpoCode(values);
await createLwpoCode(values);
}
};
const handleSave = async () => {
@@ -135,7 +136,7 @@ const LwpoCodeManagementDialog = memo(
upCd: upCd,
sortSeq: Number(rawValues.sortSeq) || 0,
};
await updateLwpoCode(values);
await updateLwpoCode(values);
}
};
@@ -289,6 +290,9 @@ const LwpoCodeManagementDialog = memo(
{...field}
type="number"
value={field.value ?? 0}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
className="!text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
@@ -24,7 +24,7 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
pageIndex: 0,
pageSize: 10,
});
const query = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, upCd, searchParams, pagination],
queryFn: () =>
@@ -34,17 +34,16 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
page: pagination.pageIndex,
size: pagination.pageSize,
}),
enabled: !!upCd ,
enabled: !!upCd,
placeholderData: keepPreviousData,
});
});
// 등록/수정 후 목록 재조회 콜백
const handleAfterSave = () => {
const handleAfterSave = () => {
query.refetch();
};
const columns = getColumns(handleAfterSave);
const { mutateAsync: codeRefreshAll } = useMutation({
mutationFn: LwpoCodeApi.codeRefreshAll,
onSuccess: () => {
@@ -123,9 +122,9 @@ function LwpoCodeSection({ upCd }: LwpoCodeSectionProps) {
코드갱신
</Button>
<LwpoCodeManagementDialog
open={dialogOpen}
upCd={upCd}
onOpenChange={setDialogOpen}
open={dialogOpen}
onOpenChange={setDialogOpen}
onAfterSave={handleAfterSave}
>
<Button>등록</Button>
@@ -4,15 +4,11 @@ import { type GroupListResponseDto } from "@/entities/group";
import { MenuAuthorityDialog } from "../ui/menu-authority-dialog";
import { GroupsDialog } from "../ui/groups-dialog";
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface TableMeta<TData> {
onDelete?: (groupId: string , row?: TData) => void;
}
}
export const columns: ColumnDef<GroupListResponseDto>[] = [
export const createColumns = ({
onDelete,
}: {
onDelete: (groupId: string) => void;
}): ColumnDef<GroupListResponseDto>[] => [
{
accessorKey: "groupId",
header: "그룹코드",
@@ -28,12 +24,10 @@ export const columns: ColumnDef<GroupListResponseDto>[] = [
{
id: "actions",
header: "관리",
cell: ({ row, table }) => {
const onDelete = table.options.meta?.onDelete as (groupId: string) => void;
cell: ({ row }) => {
const handleDelete = (e: React.MouseEvent) => {
e.stopPropagation();
if (onDelete) onDelete(row.original.groupId);
onDelete(row.original.groupId);
};
return (
+10 -9
View File
@@ -1,13 +1,17 @@
"use client";
import { useState } from "react";
import { keepPreviousData, useQuery, useQueryClient } from "@tanstack/react-query";
import {
keepPreviousData,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
import { Button, Input, Label } from "@common/components";
import { GroupApi , type GroupSearchParams} from "@/entities/group";
import { ENDPOINT } from "@/shared/config";
import { useDataTable } from "@/shared/lib";
import { columns } from "../model/groups-table-columns";
import { createColumns } from "../model/groups-table-columns";
import { GroupsDialog } from "./groups-dialog";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
@@ -40,8 +44,8 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
size: pagination.pageSize,
}),
placeholderData: keepPreviousData,
});
const { confirm } = useConfirm();
});
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
const queryClient = useQueryClient();
const { mutateAsync: deleteGroup } = useMutationWrapper({
@@ -82,22 +86,19 @@ function GroupsSection({ handleSelectedGroup }: GroupsSectionProps) {
};
const handleRowClick = (row: { groupId: string }) => {
table.setRowSelection({ [row.groupId]: true });
handleSelectedGroup(row.groupId); // 1. 리스트에서 선택된 groupId 저장
handleSelectedGroup(row.groupId); // 1. 리스트에서 선택된 groupId 저장
};
const { tableComponent, tableInstance: table } = useDataTable({
query,
tableOptions: {
columns,
columns: createColumns({ onDelete: handleDelete }),
state: { pagination },
manualPagination: true,
enableMultiRowSelection: false,
getCoreRowModel: getCoreRowModel(),
onPaginationChange: setPagination,
getRowId: (row) => row.groupId,
meta: {
onDelete: handleDelete,
},
},
dataTableOptions: {
onRowClick: handleRowClick,
+1
View File
@@ -153,6 +153,7 @@ export const filterUserOrganizationTreeDataByKeyword = (
): NodeModel<TreeUserOrganization | TreeUser>[] => {
return Array.from(
treeData.reduce((matchedNodes, node) => {
if (node.data && "ognzSn" in node.data) return matchedNodes; // 조직 노드는 필터링 제외
if (!node.text.includes(keyword)) return matchedNodes;
matchedNodes.add(node);
@@ -12,7 +12,9 @@ import {
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -52,10 +54,22 @@ function OrganizationSearchDialog({
});
const handleSelectNode = (node: NodeModel<TreeModel>) => {
if (node.data && "ognzSn" in node.data) return;
setSelectedNode(node);
handleUserSelect(node.data as TreeUser);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
setTree(filterUserOrganizationTreeDataByKeyword(treeData ?? [], keyword));
};
const handleReset = () => {
setKeyword("");
setTree(treeData ?? []);
};
useEffect(() => {
setTree(treeData ?? []);
}, [treeData]);
@@ -73,21 +87,25 @@ function OrganizationSearchDialog({
<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
className="h-[2.5rem] !text-[1.3rem]"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button
size="sm"
onClick={() =>
setTree(filterUserOrganizationTreeDataByKeyword(tree, keyword))
}
>
<i className="diveicon di-search text-base" />
조회
</Button>
<div className="card card-border !m-0 w-full">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<Input
className="h-[2.5rem] !text-[1.3rem]"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Button type="submit" size="sm">
조회
</Button>
<Button
type="button"
size="sm"
color="white"
onClick={handleReset}
>
초기화
</Button>
</form>
</div>
<div className="h-[36rem] overflow-auto">
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
@@ -116,6 +134,16 @@ function OrganizationSearchDialog({
</DndProvider>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button color="white" onClick={() => setSelectedNode(null)}>
취소
</Button>
</DialogClose>
<DialogClose asChild>
<Button disabled={!selectedNode}>확인</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
+21 -9
View File
@@ -3,6 +3,7 @@
import dayjs from "dayjs";
import { usePathname, useRouter } from "next/navigation";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Table,
@@ -18,10 +19,11 @@ import { useAlertStore, useMutationWrapper } from "@/shared/lib";
function UserDetail({ userUnqId }: { userUnqId: string }) {
const router = useRouter();
const pathname = usePathname();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const { data } = useQuery({
const { data, refetch } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
queryFn: () => UserApi.findByUserUnqId(userUnqId),
enabled: !!userUnqId,
@@ -31,9 +33,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.lockUser(userUnqId),
onSuccess: () => {
alert("계정 잠금이 활성화 되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
});
refetch();
},
});
@@ -41,9 +41,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.unlockUser(userUnqId),
onSuccess: () => {
alert("계정 잠금이 해제되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
});
refetch();
},
});
@@ -51,12 +49,26 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
mutationFn: () => UserApi.applyUser(userUnqId),
onSuccess: () => {
alert("사용자 승인이 완료되었습니다.");
refetch();
},
});
const { mutateAsync: deleteUser } = useMutationWrapper({
mutationFn: () => UserApi.deleteUser(userUnqId),
onSuccess: () => {
alert("회원이 삭제되었습니다.", router.back);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
queryKey: [ENDPOINT.USER_SERVICE.USERS],
});
},
});
const handleDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await deleteUser();
}
};
return (
<div className="card card-border !mt-6">
<div className="card-header">
@@ -164,7 +176,7 @@ function UserDetail({ userUnqId }: { userUnqId: string }) {
<Button size="lg" onClick={() => router.push(`${pathname}/modify`)}>
수정
</Button>
<Button size="lg" color="red">
<Button size="lg" color="red" onClick={handleDelete}>
삭제
</Button>
</div>