diff --git a/plm-api/src/controllers/app/common/common.controller.js b/plm-api/src/controllers/app/common/common.controller.js index db5f599..73898c4 100644 --- a/plm-api/src/controllers/app/common/common.controller.js +++ b/plm-api/src/controllers/app/common/common.controller.js @@ -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 { const codeData = req.body; console.log('Received code data:', codeData); @@ -95,7 +95,7 @@ router.post("/codes", async (req, res, next) => { // URL 경로 변경 "/code" res.json({ success: true, data: result - }); // 응답 형식 통일 + }); } catch (error) { console.error('Save code error:', error); next(error); diff --git a/plm-api/src/services/common.service.js b/plm-api/src/services/common.service.js index f6eb080..4b43891 100644 --- a/plm-api/src/services/common.service.js +++ b/plm-api/src/services/common.service.js @@ -153,17 +153,13 @@ const saveCode = async (codeData) => { }; let result; + // ID가 있으면 수정, 없으면 생성 if (codeData.id) { - // 수정 - result = await CommCode.update(codeFields, { - where: { id: codeData.id }, - returning: true, // 업데이트된 데이터 반환 + await CommCode.update(codeFields, { + where: { id: codeData.id } }); - - const updatedCode = await CommCode.findByPk(codeData.id); - result = updatedCode; + result = await CommCode.findByPk(codeData.id); } else { - // 생성 result = await CommCode.create(codeFields); } diff --git a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx index 5d034bf..72afb37 100644 --- a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx +++ b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx @@ -69,6 +69,7 @@ interface CodeFormDialogProps { codeData?: CodeFormData; onSave: (data: CodeFormData) => void; codes: CommonCode[]; + title: string; // title prop 추가 } // CodeFormDialog 컴포넌트 수정 @@ -77,7 +78,8 @@ const CodeFormDialog: React.FC = ({ onClose, codeData, onSave, - codes + codes, + title, // title prop 사용 }) => { const [searchText, setSearchText] = useState(""); @@ -125,9 +127,7 @@ const CodeFormDialog: React.FC = ({ - - {codeData ? "공통 코드 수정" : "공통 코드 등록"} - + {title}
@@ -249,6 +249,7 @@ const CommonCodePage = () => { const [codeToDelete, setCodeToDelete] = useState(null); const [selectedRows, setSelectedRows] = useState([]); const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가 + const [parentCode, setParentCode] = useState(null); // 상위 코드 상태 추가 const queryClient = useQueryClient(); const { toast } = useToast(); @@ -306,8 +307,22 @@ const CommonCodePage = () => { // handleSave 함수 수정 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({ @@ -430,6 +445,29 @@ const CommonCodePage = () => { ), }, + { + id: "addSub", + header: "하위요소", + meta: { width: "100px", textAlign: "center" }, + cell: ({ row }) => ( +
+ +
+ ), + }, { id: "actions", header: "삭제", @@ -512,6 +550,7 @@ const CommonCodePage = () => { }} enableCheckbox={true} // 체크박스 활성화 onCheckedChange={setSelectedRows} // 체크된 행들 처리 + isTreeTable={true} // 트리 테이블 활성화 /> @@ -522,8 +561,25 @@ const CommonCodePage = () => { onClose={() => { setIsOpen(false); 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} codes={codes || []} /> diff --git a/plm-app/src/app/(admin)/common/menu/page.tsx b/plm-app/src/app/(admin)/common/menu/page.tsx index 41670d7..2980614 100644 --- a/plm-app/src/app/(admin)/common/menu/page.tsx +++ b/plm-app/src/app/(admin)/common/menu/page.tsx @@ -77,6 +77,7 @@ interface MenuFormDialogProps { menuData?: DBMenuItem; onSave: (menu: DBMenuItem) => void; menus: DBMenuItem[]; + title: string; // 추가 } const getMenuList = (menus: DBMenuItem[], depth = 0): { id: string; label: string; isChild: boolean }[] => { @@ -96,6 +97,7 @@ const MenuFormDialog: React.FC = ({ menuData, onSave, menus, + title, // 추가 }) => { const [searchText, setSearchText] = useState(""); @@ -147,7 +149,7 @@ const MenuFormDialog: React.FC = ({ - {menuData ? '메뉴 수정' : '새 메뉴 추가'} + {title} @@ -290,7 +292,9 @@ const MenuFormDialog: React.FC = ({ - + @@ -307,6 +311,7 @@ const MenusPage = () => { const [menuToDelete, setMenuToDelete] = useState(null); const [selectedRows, setSelectedRows] = useState([]); const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가 + const [parentMenu, setParentMenu] = useState(null); // 상위 메뉴 상태 추가 const queryClient = useQueryClient(); const { toast } = useToast(); @@ -389,6 +394,29 @@ const MenusPage = () => { ), }, + { + id: "addSub", + header: "하위요소", + meta: { width: "100px", textAlign: "center" }, + cell: ({ row }) => ( +
+ +
+ ), + }, { id: "isActive", accessorKey: "isActive", @@ -506,12 +534,17 @@ const MenusPage = () => { const handleMenuSave = (menu: DBMenuItem) => { const { children, ...saveData } = menu; - // parent_id가 'root'인 경우 null로 변환 - const finalData = { - ...saveData, - parent_id: saveData.parent_id === 'root' ? null : saveData.parent_id - }; - saveMenuMutation.mutate(finalData); + + // 하위 메뉴 추가인 경우 + if (parentMenu) { + saveData.parent_id = parentMenu.id; + saveData.menu_type = parentMenu.menu_type; // 상위 메뉴의 타입을 따름 + } else if (saveData.parent_id === 'root') { + saveData.parent_id = null; + } + + console.log('Saving menu data:', saveData); // 디버깅용 + saveMenuMutation.mutate(saveData); }; const handleDeleteClick = (id: string) => { @@ -525,14 +558,39 @@ const MenusPage = () => { } }; + // 검색 로직 수정 const filteredMenus = useMemo(() => { if (!menus || !searchQuery) return menus || []; 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((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]); if (isLoading) { @@ -584,14 +642,38 @@ const MenusPage = () => { }} enableCheckbox={true} // 체크박스 활성화 onCheckedChange={setSelectedRows} // 체크된 행들 처리 - /> + isTreeTable={true} // 트리 테이블 활성화 + /> + setIsOpen(false)} - menuData={selectedMenu || undefined} + onClose={() => { + 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} menus={menus || []} /> diff --git a/plm-app/src/app/(admin)/common/oemmng/page.tsx b/plm-app/src/app/(admin)/common/oemmng/page.tsx index c63c2b4..fab00e2 100644 --- a/plm-app/src/app/(admin)/common/oemmng/page.tsx +++ b/plm-app/src/app/(admin)/common/oemmng/page.tsx @@ -1,7 +1,7 @@ // src/app/(admin)/users/accounts/page.tsx "use client"; -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useMemo } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DataTable } from "@/components/ui/data-table"; @@ -15,13 +15,15 @@ import { DialogDescription, } from "@/components/ui/dialog"; 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 { useAuthStore } from "@/stores/auth"; import { AxiosError } from "axios"; import { OemMngForm } from "./components/OemMngForm"; import { Switch } from "@/components/ui/switch"; import { oemMng, PaginatedResponse } from "@/types/common/oemmng/oemmng"; +import { PLMTable } from "@/components/common/Table"; +import { Input } from "@/components/ui/input"; const OemMngPage = () => { const { token, user } = useAuthStore(); @@ -31,13 +33,14 @@ const OemMngPage = () => { const [pageSize, setPageSize] = useState(10); const { toast } = useToast(); const queryClient = useQueryClient(); + const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가 + const [searchQuery, setSearchQuery] = useState(""); // 검색어 상태 추가 // Fetch OEMs with pagination const { data, isLoading } = useQuery({ queryKey: ["oemMngs", page, pageSize], queryFn: async () => { const { data } = await api.get("/api/v1/app/oemmng/oemmngList", { - params: { page, limit: pageSize, @@ -53,12 +56,21 @@ const OemMngPage = () => { enabled: !!token, }); - const handlePageSizeChange = useCallback((newPageSize: number) => { setPageSize(newPageSize); 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 const createMutation = useMutation({ mutationFn: async (newOem: Partial) => { @@ -96,7 +108,6 @@ const OemMngPage = () => { mutationFn: async (oemData: Partial) => { const { data } = await api.put( `/api/v1/app/oemmng/oemmngUpdate/${oemData.id}`, - oemData ); return data; @@ -140,44 +151,60 @@ const OemMngPage = () => { }, }); - // Table columns + // Table columns 수정 const columns: ColumnDef[] = [ { id: "index", header: "No", cell: ({ row }) => row.original.index, - size: 60, + meta: { + width: "60px", + textAlign: "center", + }, }, - // { - // accessorKey: "username", - // header: "아이디", - // }, { accessorKey: "oem_code", header: "업체명/고객사", - size: 120, + meta: { + width: "120px", + textAlign: "center", + }, }, { accessorKey: "oem_name", header: "OEM 이름", + meta: { + width: "200px", + textAlign: "center", + }, }, { accessorKey: "isActive", header: "활성화", + meta: { + width: "100px", + textAlign: "center", + }, cell: ({ row }) => ( - { - updateMutation.mutate({ id: row.original.id, isActive: value }); - }} - /> +
+ { + updateMutation.mutate({ id: row.original.id, isActive: value }); + }} + /> +
), }, { id: "actions", header: "액션", + meta: { + width: "100px", + textAlign: "center", + }, cell: ({ row }) => ( -
+
-
- - {/* OEMs Table */} - - - 고객사 목록 - - - {data?.oemMngs && data.oemMngs.length > 0 ? ( - <> - setPage(newPage + 1), - onPageSizeChange: handlePageSizeChange, - }} - /> -
- 총 {data.total}명의 고객사 -
- - ) : ( -
- 등록된 고객사가 없습니다. +
+
+
+
+
+

고객사 관리

+

+ 고객사를 관리하고 권한을 설정합니다. +

- )} - - + +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 w-full" + /> +
+
+ +
+
+ {data?.oemMngs && data.oemMngs.length > 0 ? ( + <> + { + setEditingUser(row); + setIsOpen(true); + }} + enableCheckbox={true} + isTreeTable={false} + /> + + ) : ( +
+ 등록된 고객사가 없습니다. +
+ )} +
+
+
{/* User Create/Edit Dialog */} { enableCheckbox?: boolean; onCheckedChange?: (checkedRows: T[]) => void; onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가 + isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가 } type CustomExpandedState = Record; @@ -55,6 +56,7 @@ export function PLMTable({ enableCheckbox = false, onCheckedChange, onPageSizeChange, + isTreeTable = false, // 기본값은 false }: PLMTableProps) { const [expanded, setExpanded] = useState({}); const [rowSelection, setRowSelection] = useState>({}); @@ -204,39 +206,41 @@ export function PLMTable({ }); } - // 확장/축소 버튼 컬럼 - result.push({ - id: 'expander', - meta: { width: "40px" }, - header: () => null, - cell: ({ row }) => ( -
- {row.getCanExpand() && ( - - )} -
- ), - }); + // 확장/축소 버튼 컬럼 - 트리 테이블인 경우에만 추가 + if (isTreeTable) { + result.push({ + id: 'expander', + meta: { width: "40px" }, + header: () => null, + cell: ({ row }) => ( +
+ {row.getCanExpand() && ( + + )} +
+ ), + }); + } // 나머지 컬럼들 추가 result.push(...userColumns); return result; - }, [userColumns, enableCheckbox, onCheckedChange]); + }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]); const handleExpandedChange: OnChangeFn = useCallback((updater) => { setExpanded(old => {