하위메뉴 추가및 고객사 메뉴적용용

This commit is contained in:
pgb 2024-12-27 20:25:07 +09:00
parent 8fca4f8357
commit 4aa0090068
6 changed files with 294 additions and 124 deletions

View File

@ -87,7 +87,7 @@ router.get("/codes", async (req, res, next) => {
}); });
// 공통코드 생성/수정 // 공통코드 생성/수정
router.post("/codes", async (req, res, next) => { // URL 경로 변경 "/code" -> "/codes" router.post("/code", async (req, res, next) => { // URL 경로 수정
try { try {
const codeData = req.body; const codeData = req.body;
console.log('Received code data:', codeData); console.log('Received code data:', codeData);
@ -95,7 +95,7 @@ router.post("/codes", async (req, res, next) => { // URL 경로 변경 "/code"
res.json({ res.json({
success: true, success: true,
data: result data: result
}); // 응답 형식 통일 });
} catch (error) { } catch (error) {
console.error('Save code error:', error); console.error('Save code error:', error);
next(error); next(error);

View File

@ -153,17 +153,13 @@ const saveCode = async (codeData) => {
}; };
let result; let result;
// ID가 있으면 수정, 없으면 생성
if (codeData.id) { if (codeData.id) {
// 수정 await CommCode.update(codeFields, {
result = await CommCode.update(codeFields, { where: { id: codeData.id }
where: { id: codeData.id },
returning: true, // 업데이트된 데이터 반환
}); });
result = await CommCode.findByPk(codeData.id);
const updatedCode = await CommCode.findByPk(codeData.id);
result = updatedCode;
} else { } else {
// 생성
result = await CommCode.create(codeFields); result = await CommCode.create(codeFields);
} }

View File

@ -69,6 +69,7 @@ interface CodeFormDialogProps {
codeData?: CodeFormData; codeData?: CodeFormData;
onSave: (data: CodeFormData) => void; onSave: (data: CodeFormData) => void;
codes: CommonCode[]; codes: CommonCode[];
title: string; // title prop 추가
} }
// CodeFormDialog 컴포넌트 수정 // CodeFormDialog 컴포넌트 수정
@ -77,7 +78,8 @@ const CodeFormDialog: React.FC<CodeFormDialogProps> = ({
onClose, onClose,
codeData, codeData,
onSave, onSave,
codes codes,
title, // title prop 사용
}) => { }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
@ -125,9 +127,7 @@ const CodeFormDialog: React.FC<CodeFormDialogProps> = ({
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]"> <DialogContent className="sm:max-w-[600px]">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>{title}</DialogTitle>
{codeData ? "공통 코드 수정" : "공통 코드 등록"}
</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSave)} className="space-y-4">
@ -249,6 +249,7 @@ const CommonCodePage = () => {
const [codeToDelete, setCodeToDelete] = useState<string | null>(null); const [codeToDelete, setCodeToDelete] = useState<string | null>(null);
const [selectedRows, setSelectedRows] = useState<CommonCode[]>([]); const [selectedRows, setSelectedRows] = useState<CommonCode[]>([]);
const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가 const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가
const [parentCode, setParentCode] = useState<CommonCode | null>(null); // 상위 코드 상태 추가
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { toast } = useToast(); const { toast } = useToast();
@ -306,8 +307,22 @@ const CommonCodePage = () => {
// handleSave 함수 수정 // handleSave 함수 수정
const handleSave = (data: CodeFormData) => { const handleSave = (data: CodeFormData) => {
console.log('Form data submitted:', data); // 디버깅용 // 하위 코드 추가인 경우
saveCodeMutation.mutate(data); const saveData = { ...data };
if (parentCode) {
saveData.parent_code_id = parentCode.id;
} else if (saveData.parent_code_id === 'root') {
saveData.parent_code_id = null;
}
// ID 제거 (새로운 항목 추가 시)
if (parentCode || !selectedCode) {
delete saveData.id;
}
console.log('Saving code data:', saveData);
saveCodeMutation.mutate(saveData);
}; };
const deleteCodeMutation = useMutation({ const deleteCodeMutation = useMutation({
@ -430,6 +445,29 @@ const CommonCodePage = () => {
</div> </div>
), ),
}, },
{
id: "addSub",
header: "하위요소",
meta: { width: "100px", textAlign: "center" },
cell: ({ row }) => (
<div className="text-center">
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
onClick={(e) => {
e.stopPropagation(); // 이벤트 전파 중지
// 상위 코드 설정 및 빈 코드 데이터로 다이얼로그 열기
setSelectedCode(null);
setParentCode(row.original);
setIsOpen(true);
}}
>
</Button>
</div>
),
},
{ {
id: "actions", id: "actions",
header: "삭제", header: "삭제",
@ -512,6 +550,7 @@ const CommonCodePage = () => {
}} }}
enableCheckbox={true} // 체크박스 활성화 enableCheckbox={true} // 체크박스 활성화
onCheckedChange={setSelectedRows} // 체크된 행들 처리 onCheckedChange={setSelectedRows} // 체크된 행들 처리
isTreeTable={true} // 트리 테이블 활성화
/> />
</div> </div>
</div> </div>
@ -522,8 +561,25 @@ const CommonCodePage = () => {
onClose={() => { onClose={() => {
setIsOpen(false); setIsOpen(false);
setSelectedCode(null); setSelectedCode(null);
setParentCode(null);
}} }}
codeData={selectedCode ? { ...selectedCode, description: selectedCode.description || "" } : undefined} codeData={
parentCode
? {
code_cd: "",
code_name: "",
description: "",
parent_code_id: parentCode.id,
isActive: true,
ext_val: "",
order_num: 0
}
: selectedCode
? { ...selectedCode, description: selectedCode.description || "" }
: undefined
}
// DialogTitle 부분 수정
title={parentCode ? '하위 코드 추가' : (selectedCode ? '코드 수정' : '새 코드 추가')}
onSave={handleSave} onSave={handleSave}
codes={codes || []} codes={codes || []}
/> />

View File

@ -77,6 +77,7 @@ interface MenuFormDialogProps {
menuData?: DBMenuItem; menuData?: DBMenuItem;
onSave: (menu: DBMenuItem) => void; onSave: (menu: DBMenuItem) => void;
menus: DBMenuItem[]; menus: DBMenuItem[];
title: string; // 추가
} }
const getMenuList = (menus: DBMenuItem[], depth = 0): { id: string; label: string; isChild: boolean }[] => { const getMenuList = (menus: DBMenuItem[], depth = 0): { id: string; label: string; isChild: boolean }[] => {
@ -96,6 +97,7 @@ const MenuFormDialog: React.FC<MenuFormDialogProps> = ({
menuData, menuData,
onSave, onSave,
menus, menus,
title, // 추가
}) => { }) => {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
@ -147,7 +149,7 @@ const MenuFormDialog: React.FC<MenuFormDialogProps> = ({
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]"> <DialogContent className="sm:max-w-[600px]">
<DialogHeader> <DialogHeader>
<DialogTitle>{menuData ? '메뉴 수정' : '새 메뉴 추가'}</DialogTitle> <DialogTitle>{title}</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSave)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSave)} className="space-y-4">
@ -290,7 +292,9 @@ const MenuFormDialog: React.FC<MenuFormDialogProps> = ({
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" onClick={onClose}></Button> <Button type="button" variant="outline" onClick={onClose}></Button>
<Button type="submit">{menuData ? '수정' : '등록'}</Button> <Button type="submit">
{menuData ? '수정' : '등록'} {/* parentMenu 대신 codeData 사용 */}
</Button>
</DialogFooter> </DialogFooter>
</form> </form>
</Form> </Form>
@ -307,6 +311,7 @@ const MenusPage = () => {
const [menuToDelete, setMenuToDelete] = useState<string | null>(null); const [menuToDelete, setMenuToDelete] = useState<string | null>(null);
const [selectedRows, setSelectedRows] = useState<DBMenuItem[]>([]); const [selectedRows, setSelectedRows] = useState<DBMenuItem[]>([]);
const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가 const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가
const [parentMenu, setParentMenu] = useState<DBMenuItem | null>(null); // 상위 메뉴 상태 추가
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { toast } = useToast(); const { toast } = useToast();
@ -389,6 +394,29 @@ const MenusPage = () => {
</div> </div>
), ),
}, },
{
id: "addSub",
header: "하위요소",
meta: { width: "100px", textAlign: "center" },
cell: ({ row }) => (
<div className="text-center">
<Button
variant="outline"
size="sm"
className="h-8 px-2 text-xs font-medium text-blue-600 hover:text-blue-700 hover:bg-blue-50 border-blue-200"
onClick={(e) => {
e.stopPropagation(); // 이벤트 전파 중지
// 상위 메뉴 설정 및 빈 메뉴 데이터로 다이얼로그 열기
setSelectedMenu(null);
setParentMenu(row.original);
setIsOpen(true);
}}
>
</Button>
</div>
),
},
{ {
id: "isActive", id: "isActive",
accessorKey: "isActive", accessorKey: "isActive",
@ -506,12 +534,17 @@ const MenusPage = () => {
const handleMenuSave = (menu: DBMenuItem) => { const handleMenuSave = (menu: DBMenuItem) => {
const { children, ...saveData } = menu; const { children, ...saveData } = menu;
// parent_id가 'root'인 경우 null로 변환
const finalData = { // 하위 메뉴 추가인 경우
...saveData, if (parentMenu) {
parent_id: saveData.parent_id === 'root' ? null : saveData.parent_id saveData.parent_id = parentMenu.id;
}; saveData.menu_type = parentMenu.menu_type; // 상위 메뉴의 타입을 따름
saveMenuMutation.mutate(finalData); } else if (saveData.parent_id === 'root') {
saveData.parent_id = null;
}
console.log('Saving menu data:', saveData); // 디버깅용
saveMenuMutation.mutate(saveData);
}; };
const handleDeleteClick = (id: string) => { const handleDeleteClick = (id: string) => {
@ -525,14 +558,39 @@ const MenusPage = () => {
} }
}; };
// 검색 로직 수정
const filteredMenus = useMemo(() => { const filteredMenus = useMemo(() => {
if (!menus || !searchQuery) return menus || []; if (!menus || !searchQuery) return menus || [];
const searchLower = searchQuery.toLowerCase(); const searchLower = searchQuery.toLowerCase();
return menus.filter(menu =>
(menu.menu_name_kor?.toLowerCase() || '').includes(searchLower) || // 재귀적으로 메뉴와 그 자식들을 검색하는 함수
(menu.menu_url?.toLowerCase() || '').includes(searchLower) const filterMenusRecursive = (items: DBMenuItem[]): DBMenuItem[] => {
); return items.reduce<DBMenuItem[]>((acc, menu) => {
const matchesSearch =
(menu.menu_name_kor?.toLowerCase() || '').includes(searchLower) ||
(menu.menu_name_eng?.toLowerCase() || '').includes(searchLower) ||
(menu.menu_url?.toLowerCase() || '').includes(searchLower);
if (matchesSearch) {
// 검색어와 일치하는 경우 해당 메뉴 포함
acc.push(menu);
} else if (menu.children && menu.children.length > 0) {
// 자식 메뉴들을 검색
const filteredChildren = filterMenusRecursive(menu.children);
if (filteredChildren.length > 0) {
// 자식 중 검색어와 일치하는 것이 있으면 부모도 포함
acc.push({
...menu,
children: filteredChildren
});
}
}
return acc;
}, []);
};
return filterMenusRecursive(menus);
}, [menus, searchQuery]); }, [menus, searchQuery]);
if (isLoading) { if (isLoading) {
@ -584,14 +642,38 @@ const MenusPage = () => {
}} }}
enableCheckbox={true} // 체크박스 활성화 enableCheckbox={true} // 체크박스 활성화
onCheckedChange={setSelectedRows} // 체크된 행들 처리 onCheckedChange={setSelectedRows} // 체크된 행들 처리
/> </div> isTreeTable={true} // 트리 테이블 활성화
/>
</div>
</div> </div>
</div> </div>
<MenuFormDialog <MenuFormDialog
isOpen={isOpen} isOpen={isOpen}
onClose={() => setIsOpen(false)} onClose={() => {
menuData={selectedMenu || undefined} setIsOpen(false);
setSelectedMenu(null);
setParentMenu(null);
}}
menuData={
parentMenu
? {
id: '',
menu_type: parentMenu.menu_type,
parent_id: parentMenu.id,
menu_name_kor: '',
menu_name_eng: '',
menu_url: '',
seq: '0',
isActive: true
}
: selectedMenu || undefined
}
title={
parentMenu
? "하위 메뉴 추가"
: (selectedMenu ? "메뉴 수정" : "새 메뉴 추가")
}
onSave={handleMenuSave} onSave={handleMenuSave}
menus={menus || []} menus={menus || []}
/> />

View File

@ -1,7 +1,7 @@
// src/app/(admin)/users/accounts/page.tsx // src/app/(admin)/users/accounts/page.tsx
"use client"; "use client";
import React, { useState, useCallback } from "react"; import React, { useState, useCallback, useMemo } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DataTable } from "@/components/ui/data-table"; import { DataTable } from "@/components/ui/data-table";
@ -15,13 +15,15 @@ import {
DialogDescription, DialogDescription,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { Plus, Edit, Trash2 } from "lucide-react"; import { Plus, Edit, Trash2, Search } from "lucide-react";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { OemMngForm } from "./components/OemMngForm"; import { OemMngForm } from "./components/OemMngForm";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { oemMng, PaginatedResponse } from "@/types/common/oemmng/oemmng"; import { oemMng, PaginatedResponse } from "@/types/common/oemmng/oemmng";
import { PLMTable } from "@/components/common/Table";
import { Input } from "@/components/ui/input";
const OemMngPage = () => { const OemMngPage = () => {
const { token, user } = useAuthStore(); const { token, user } = useAuthStore();
@ -31,13 +33,14 @@ const OemMngPage = () => {
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const { toast } = useToast(); const { toast } = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가
const [searchQuery, setSearchQuery] = useState(""); // 검색어 상태 추가
// Fetch OEMs with pagination // Fetch OEMs with pagination
const { data, isLoading } = useQuery<PaginatedResponse>({ const { data, isLoading } = useQuery<PaginatedResponse>({
queryKey: ["oemMngs", page, pageSize], queryKey: ["oemMngs", page, pageSize],
queryFn: async () => { queryFn: async () => {
const { data } = await api.get("/api/v1/app/oemmng/oemmngList", { const { data } = await api.get("/api/v1/app/oemmng/oemmngList", {
params: { params: {
page, page,
limit: pageSize, limit: pageSize,
@ -53,12 +56,21 @@ const OemMngPage = () => {
enabled: !!token, enabled: !!token,
}); });
const handlePageSizeChange = useCallback((newPageSize: number) => { const handlePageSizeChange = useCallback((newPageSize: number) => {
setPageSize(newPageSize); setPageSize(newPageSize);
setPage(1); setPage(1);
}, []); }, []);
// 필터링된 데이터 생성
const filteredOemMngs = useMemo(() => {
if (!data?.oemMngs || !searchQuery) return data?.oemMngs;
return data.oemMngs.filter(oem =>
oem.oem_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
oem.oem_name?.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [data?.oemMngs, searchQuery]);
// Create user mutation // Create user mutation
const createMutation = useMutation({ const createMutation = useMutation({
mutationFn: async (newOem: Partial<oemMng>) => { mutationFn: async (newOem: Partial<oemMng>) => {
@ -96,7 +108,6 @@ const OemMngPage = () => {
mutationFn: async (oemData: Partial<oemMng>) => { mutationFn: async (oemData: Partial<oemMng>) => {
const { data } = await api.put<oemMng>( const { data } = await api.put<oemMng>(
`/api/v1/app/oemmng/oemmngUpdate/${oemData.id}`, `/api/v1/app/oemmng/oemmngUpdate/${oemData.id}`,
oemData oemData
); );
return data; return data;
@ -140,44 +151,60 @@ const OemMngPage = () => {
}, },
}); });
// Table columns // Table columns 수정
const columns: ColumnDef<oemMng>[] = [ const columns: ColumnDef<oemMng>[] = [
{ {
id: "index", id: "index",
header: "No", header: "No",
cell: ({ row }) => row.original.index, cell: ({ row }) => row.original.index,
size: 60, meta: {
width: "60px",
textAlign: "center",
},
}, },
// {
// accessorKey: "username",
// header: "아이디",
// },
{ {
accessorKey: "oem_code", accessorKey: "oem_code",
header: "업체명/고객사", header: "업체명/고객사",
size: 120, meta: {
width: "120px",
textAlign: "center",
},
}, },
{ {
accessorKey: "oem_name", accessorKey: "oem_name",
header: "OEM 이름", header: "OEM 이름",
meta: {
width: "200px",
textAlign: "center",
},
}, },
{ {
accessorKey: "isActive", accessorKey: "isActive",
header: "활성화", header: "활성화",
meta: {
width: "100px",
textAlign: "center",
},
cell: ({ row }) => ( cell: ({ row }) => (
<Switch <div className="text-center">
checked={row.original.isActive} <Switch
onCheckedChange={(value) => { checked={row.original.isActive}
updateMutation.mutate({ id: row.original.id, isActive: value }); onCheckedChange={(value) => {
}} updateMutation.mutate({ id: row.original.id, isActive: value });
/> }}
/>
</div>
), ),
}, },
{ {
id: "actions", id: "actions",
header: "액션", header: "액션",
meta: {
width: "100px",
textAlign: "center",
},
cell: ({ row }) => ( cell: ({ row }) => (
<div className="flex items-center gap-2"> <div className="flex items-center justify-center gap-2">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -207,52 +234,57 @@ const OemMngPage = () => {
if (isLoading) return <div>Loading...</div>; if (isLoading) return <div>Loading...</div>;
return ( return (
<div className="container mx-auto py-6"> <div className="container mx-auto py-4">
{/* Header */} <div className="bg-white rounded-lg shadow">
<div className="flex justify-between items-center mb-6"> <div className="p-4 border-b border-gray-200">
<div className="space-y-1"> <div className="flex justify-between items-center mb-3">
<h1 className="text-3xl font-bold"> </h1> <div>
<p className="text-muted-foreground"> <h1 className="text-2xl font-semibold text-gray-900"> </h1>
. <p className="text-sm text-gray-500">
</p> .
</div> </p>
<Button onClick={() => setIsOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
{/* OEMs Table */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
</CardHeader>
<CardContent>
{data?.oemMngs && data.oemMngs.length > 0 ? (
<>
<DataTable
columns={columns}
data={data.oemMngs}
pagination={{
pageIndex: page - 1,
pageSize,
pageCount: data.totalPages,
rowCount: data.total,
onPageChange: (newPage) => setPage(newPage + 1),
onPageSizeChange: handlePageSizeChange,
}}
/>
<div className="text-sm text-muted-foreground mt-2">
{data.total}
</div>
</>
) : (
<div className="text-center py-12 text-muted-foreground">
.
</div> </div>
)} <Button onClick={() => setIsOpen(true)}>
</CardContent> <Plus className="mr-2 h-4 w-4" />
</Card>
</Button>
</div>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="고객사명 또는 업체명 검색"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 w-full"
/>
</div>
</div>
<div className="p-4">
<div className="h-[650px]">
{data?.oemMngs && data.oemMngs.length > 0 ? (
<>
<PLMTable
columns={columns}
data={filteredOemMngs || []} // 필터링된 데이터 사용
pageSize={currentPageSize}
onPageSizeChange={setCurrentPageSize}
onRowClick={(row: oemMng) => {
setEditingUser(row);
setIsOpen(true);
}}
enableCheckbox={true}
isTreeTable={false}
/>
</>
) : (
<div className="text-center py-12 text-muted-foreground">
.
</div>
)}
</div>
</div>
</div>
{/* User Create/Edit Dialog */} {/* User Create/Edit Dialog */}
<Dialog <Dialog

View File

@ -43,6 +43,7 @@ interface PLMTableProps<T extends BaseRow> {
enableCheckbox?: boolean; enableCheckbox?: boolean;
onCheckedChange?: (checkedRows: T[]) => void; onCheckedChange?: (checkedRows: T[]) => void;
onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가 onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가
isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가
} }
type CustomExpandedState = Record<string | number, boolean>; type CustomExpandedState = Record<string | number, boolean>;
@ -55,6 +56,7 @@ export function PLMTable<T extends BaseRow>({
enableCheckbox = false, enableCheckbox = false,
onCheckedChange, onCheckedChange,
onPageSizeChange, onPageSizeChange,
isTreeTable = false, // 기본값은 false
}: PLMTableProps<T>) { }: PLMTableProps<T>) {
const [expanded, setExpanded] = useState<CustomExpandedState>({}); const [expanded, setExpanded] = useState<CustomExpandedState>({});
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({}); const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
@ -204,39 +206,41 @@ export function PLMTable<T extends BaseRow>({
}); });
} }
// 확장/축소 버튼 컬럼 // 확장/축소 버튼 컬럼 - 트리 테이블인 경우에만 추가
result.push({ if (isTreeTable) {
id: 'expander', result.push({
meta: { width: "40px" }, id: 'expander',
header: () => null, meta: { width: "40px" },
cell: ({ row }) => ( header: () => null,
<div className="flex justify-center"> cell: ({ row }) => (
{row.getCanExpand() && ( <div className="flex justify-center">
<Button {row.getCanExpand() && (
variant="ghost" <Button
size="icon" variant="ghost"
className="h-8 w-8 p-0" size="icon"
onClick={(e) => { className="h-8 w-8 p-0"
e.stopPropagation(); onClick={(e) => {
row.toggleExpanded(); e.stopPropagation();
}} row.toggleExpanded();
> }}
{row.getIsExpanded() ? ( >
<ChevronDown className="h-4 w-4" /> {row.getIsExpanded() ? (
) : ( <ChevronDown className="h-4 w-4" />
<ChevronRight className="h-4 w-4" /> ) : (
)} <ChevronRight className="h-4 w-4" />
</Button> )}
)} </Button>
</div> )}
), </div>
}); ),
});
}
// 나머지 컬럼들 추가 // 나머지 컬럼들 추가
result.push(...userColumns); result.push(...userColumns);
return result; return result;
}, [userColumns, enableCheckbox, onCheckedChange]); }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]);
const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => { const handleExpandedChange: OnChangeFn<ExpandedState> = useCallback((updater) => {
setExpanded(old => { setExpanded(old => {