diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js index 7dbc428..d95b427 100644 --- a/plm-api/src/routes/app.js +++ b/plm-api/src/routes/app.js @@ -21,6 +21,7 @@ const oemMngController = require("../controllers/app/common/oemmng.controller"); const productgroupController = require("../controllers/app/common/productgroup.controller"); const productController = require("../controllers/app/common/product.controller"); const carMngController = require("../controllers/app/common/carmng.controller"); +const contractController = require("../controllers/app/contract/contract.controller"); router.use("/health", healthController); router.use("/auth", authController); @@ -41,5 +42,6 @@ router.use("/oemmng", oemMngController); router.use("/productgroup", productgroupController); router.use("/product", productController); router.use("/carmng", carMngController); +router.use("/contract", contractController); module.exports = router; diff --git a/plm-api/src/services/contract.service.js b/plm-api/src/services/contract.service.js index a5f5822..81f5624 100644 --- a/plm-api/src/services/contract.service.js +++ b/plm-api/src/services/contract.service.js @@ -1,5 +1,5 @@ // src/services/contract.service.js -const { Contract, ContractDetail, Company } = require("../models"); +const { Contract, ContractDetail, Company, ContractMgmt} = require("../models"); const alertService = require("./alert.service"); const { Op } = require("sequelize"); // const { getKoreanSubjectParticle } = require("../utils/koreanParticle"); @@ -114,6 +114,118 @@ class ContractService { }, }; } + + // 영업정보 목록 조회 수정 + async getContractList(page = 1, limit = 20) { + try { + const offset = (page - 1) * limit; + + const { count, rows } = await ContractMgmt.findAndCountAll({ + attributes: { + exclude: ['createdAt', 'updatedAt', 'created_at', 'updated_at'] // 모든 타임스탬프 필드 제외 + }, + order: [['created_at', 'DESC']], // created_at으로 수정 + limit: limit, + offset: offset, + raw: true + }); + + if (!rows || !Array.isArray(rows)) { + throw new Error('No data found'); + } + + // 데이터 형식 변환 + const formatted = rows.map(row => ({ + ...row, + // Date 형식 변환 및 검사 추가 + customer_info: row.customer_info ? new Date(row.customer_info).toISOString() : null, + duckil_info: row.duckil_info ? new Date(row.duckil_info).toISOString() : null, + register_date: row.register_date ? new Date(row.register_date).toISOString() : null, + request_date: row.request_date ? new Date(row.request_date).toISOString() : null, + submit_date: row.submit_date ? new Date(row.submit_date).toISOString() : null, + created_at: row.created_at ? new Date(row.created_at).toISOString() : null, + updated_at: row.updated_at ? new Date(row.updated_at).toISOString() : null, + + // 숫자 데이터 변환 + progress: row.progress ? parseFloat(row.progress) : null, + customer_total: row.customer_total ? parseInt(row.customer_total, 10) : null, + duckil_production: row.duckil_production ? parseInt(row.duckil_production, 10) : null, + first_quarter: row.first_quarter ? parseInt(row.first_quarter, 10) : null, + second_quarter: row.second_quarter ? parseInt(row.second_quarter, 10) : null, + third_quarter: row.third_quarter ? parseInt(row.third_quarter, 10) : null, + fourth_quarter: row.fourth_quarter ? parseInt(row.fourth_quarter, 10) : null, + })); + + return { + oemMngs: formatted, + total: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + pageSize: limit + }; + } catch (error) { + console.error('Contract Service Error:', error); // 에러 로깅 추가 + throw error; + } + } + + // 영업정보 생성 (데이터 타입 변환 추가) + async createContractMgmt(contractData) { + try { + const formattedData = this._formatContractData(contractData); + return await ContractMgmt.create(formattedData); + } catch (error) { + throw error; + } + } + + // 영업정보 수정 (데이터 타입 변환 추가) + async updateContractMgmt(id, updateData) { + try { + const contract = await ContractMgmt.findByPk(id); + if (!contract) throw new Error('Contract not found'); + + const formattedData = this._formatContractData(updateData); + await contract.update(formattedData); + return contract; + } catch (error) { + throw error; + } + } + + // 영업정보 삭제 + async deleteContractMgmt(id) { + try { + const contract = await ContractMgmt.findByPk(id); + if (!contract) throw new Error('Contract not found'); + + await contract.destroy(); + return true; + } catch (error) { + throw error; + } + } + + // 데이터 형식 변환 헬퍼 메서드 + _formatContractData(data) { + return { + ...data, + // 날짜 문자열을 Date 객체로 변환 + customer_info: data.customer_info ? new Date(data.customer_info) : null, + duckil_info: data.duckil_info ? new Date(data.duckil_info) : null, + register_date: data.register_date ? new Date(data.register_date) : null, + request_date: data.request_date ? new Date(data.request_date) : null, + submit_date: data.submit_date ? new Date(data.submit_date) : null, + // 숫자 문자열을 숫자로 변환 + progress: data.progress ? Number(data.progress) : null, + customer_total: data.customer_total ? Number(data.customer_total) : null, + duckil_production: data.duckil_production ? Number(data.duckil_production) : null, + first_quarter: data.first_quarter ? Number(data.first_quarter) : null, + second_quarter: data.second_quarter ? Number(data.second_quarter) : null, + third_quarter: data.third_quarter ? Number(data.third_quarter) : null, + fourth_quarter: data.fourth_quarter ? Number(data.fourth_quarter) : null, + }; + } } module.exports = new ContractService(); diff --git a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx index b463702..50c875d 100644 --- a/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx +++ b/plm-app/src/app/(admin)/common/codeCategoryMngList/page.tsx @@ -422,29 +422,6 @@ const CommonCodePage = () => {
{new Date(row.original.createdAt).toLocaleDateString()}
), }, - { - id: "isActive", - accessorKey: "isActive", - header: "활성화", - meta: { - width: "200px", - textAlign: "center" - }, - cell: ({ row }) => ( -
- - toggleActiveMutation.mutate({ - id: row.original.id, - isActive: checked - }) - } - onClick={(e) => e.stopPropagation()} - /> -
- ), - }, { id: "addSub", header: "하위요소", @@ -468,6 +445,29 @@ const CommonCodePage = () => { ), }, + { + id: "isActive", + accessorKey: "isActive", + header: "활성화", + meta: { + width: "200px", + textAlign: "center" + }, + cell: ({ row }) => ( +
+ + toggleActiveMutation.mutate({ + id: row.original.id, + isActive: checked + }) + } + onClick={(e) => e.stopPropagation()} + /> +
+ ), + }, { id: "actions", header: "삭제", diff --git a/plm-app/src/app/(admin)/common/menu/page.tsx b/plm-app/src/app/(admin)/common/menu/page.tsx index 83fb641..f380dec 100644 --- a/plm-app/src/app/(admin)/common/menu/page.tsx +++ b/plm-app/src/app/(admin)/common/menu/page.tsx @@ -373,27 +373,6 @@ const MenusPage = () => { textAlign: "center" } }, - { - id: "menu_type", - accessorKey: "menu_type", - header: "타입", - meta: { - width: "200px", - textAlign: "center" - }, - cell: ({ row }) => ( -
- - {row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"} - -
- ), - }, { id: "addSub", header: "하위요소", @@ -417,6 +396,27 @@ const MenusPage = () => { ), }, + { + id: "menu_type", + accessorKey: "menu_type", + header: "타입", + meta: { + width: "200px", + textAlign: "center" + }, + cell: ({ row }) => ( +
+ + {row.original.menu_type === "0" ? "관리자 메뉴" : "사용자 메뉴"} + +
+ ), + }, { id: "isActive", accessorKey: "isActive", diff --git a/plm-app/src/components/common/Table/index.tsx b/plm-app/src/components/common/Table/index.tsx index 70c2681..5e2add6 100644 --- a/plm-app/src/components/common/Table/index.tsx +++ b/plm-app/src/components/common/Table/index.tsx @@ -13,6 +13,8 @@ import { type OnChangeFn, type Row, type TableOptions, + type GroupingColumnDef, + type Cell, } from "@tanstack/react-table"; import { ChevronRight, ChevronDown } from "lucide-react"; @@ -44,10 +46,79 @@ interface PLMTableProps { onCheckedChange?: (checkedRows: T[]) => void; onPageSizeChange?: (newPageSize: number) => void; // 새로운 prop 추가 isTreeTable?: boolean; // 트리 테이블 여부를 결정하는 prop 추가 + renderExpandedRow?: (row: Row) => React.ReactNode; // 확장된 행 렌더링을 위한 prop + enableExpander?: boolean; // 확장 기능 활성화 여부 + treeConfig?: { // 트리 구조 설정 + getParentId?: (row: T) => string | null; + indent?: number; + }; + expanderConfig?: { // 행 확장 설정 + renderExpandedContent?: (row: Row) => React.ReactNode; + expandedContentStyle?: React.CSSProperties; + }; + onCellEdit?: (rowId: string, field: string, value: any) => Promise; + editableColumns?: string[]; // 수정 가능한 컬럼 목록 } type CustomExpandedState = Record; +// EditableCell 컴포넌트 추가 +const EditableCell = ({ + cell, + isEditable, + onCellEdit, +}: { + cell: Cell; + isEditable?: boolean; + onCellEdit?: (rowId: string, field: string, value: any) => Promise; +}) => { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(''); + const inputRef = useRef(null); + + const handleDoubleClick = () => { + if (!isEditable) return; + setIsEditing(true); + setEditValue(String(cell.getValue() || '')); + }; + + const handleSave = async () => { + if (onCellEdit) { + try { + await onCellEdit(cell.row.original.id, String(cell.column.id), editValue); + setIsEditing(false); + } catch (error) { + console.error('Failed to update cell:', error); + } + } + }; + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditing]); + + if (isEditing) { + return ( + setEditValue(e.target.value)} + onBlur={handleSave} + onKeyPress={(e) => e.key === 'Enter' && handleSave()} + /> + ); + } + + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); +}; + export function PLMTable({ columns: userColumns, data, @@ -57,6 +128,12 @@ export function PLMTable({ onCheckedChange, onPageSizeChange, isTreeTable = false, // 기본값은 false + renderExpandedRow, + enableExpander = false, + treeConfig, + expanderConfig, + onCellEdit, + editableColumns, }: PLMTableProps) { const [expanded, setExpanded] = useState({}); const [rowSelection, setRowSelection] = useState>({}); @@ -218,7 +295,7 @@ export function PLMTable({ + + ), + }); + } + // 나머지 컬럼들 추가 result.push(...userColumns); return result; - }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable]); + }, [userColumns, enableCheckbox, onCheckedChange, isTreeTable, enableExpander]); const handleExpandedChange: OnChangeFn = useCallback((updater) => { setExpanded(old => { @@ -307,77 +412,245 @@ export function PLMTable({ onPageSizeChange(newPageSize); } }; + const getHeaderDepth = (columns: GroupingColumnDef[]): number => { + let maxDepth = 1; + for (const column of columns) { + if ('columns' in column && Array.isArray(column.columns)) { + const subDepth = getHeaderDepth(column.columns as GroupingColumnDef[]) + 1; + maxDepth = Math.max(maxDepth, subDepth); + } + } + return maxDepth; + }; + + const defaultRenderExpandedRow = (row: Row) => { + const item = row.original; + return ( +
+ + {Object.entries(item).map(([key, value]) => { + // id나 기본 표시되는 컬럼들은 제외 + if (key === 'id' || columns.some(col => 'accessorKey' in col && col.accessorKey === key)) { + return null; + } + return ( + + + + + ); + })} +
{key}{String(value)}
+
+ ); + }; + + // 트리 구조 관련 로직 + const renderTreeCell = useCallback((row: Row) => { + if (!isTreeTable || !treeConfig) return null; + + return ( +
+ {/* 트리 라인 렌더링 */} +
+ ); + }, [isTreeTable, treeConfig]); + + // 행 확장 관련 로직 + const renderExpandedRowContent = useCallback((row: Row) => { + if (!enableExpander || !expanderConfig?.renderExpandedContent) return null; + + return ( +
+ {expanderConfig.renderExpandedContent(row)} +
+ ); + }, [enableExpander, expanderConfig]); + + // renderCell 함수를 컴포넌트로 교체 + const renderCell = useCallback((cell: Cell) => { + const isEditable = editableColumns?.includes(String(cell.column.id)); + return ( + + ); + }, [editableColumns, onCellEdit]); return (
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const isFirstCell = cell.column.id === 'expander'; - return ( - + ); + })} + + + {table.getRowModel().rows.map((row) => ( + + + {row.getVisibleCells().map((cell, cellIndex) => { + const isFirstCell = cell.column.id === 'expander'; + const isSecondCell = cellIndex === 1; // expander 다음 셀 확인 + const hasExpandButton = row.getCanExpand(); + + return ( + + ); + })} + + {/* 확장된 행 렌더링 */} + {row.getIsExpanded() && ( + + + + )} + + ))} + +
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {isFirstCell && Array.from({ length: row.depth }).map((_, index) => ( - + {/* text-xs에서 text-[13px]로 변경 */} + + {table.getHeaderGroups().map((headerGroup, groupIndex) => { + // 헤더 그룹의 전체 개수 + const totalHeaderGroups = table.getHeaderGroups().length; + + return ( + + {headerGroup.headers.map((header) => { + const isUtilityColumn = header.column.id === 'select' || header.column.id === 'expander'; + const isNestedHeader = header.column.columns?.length > 0; + + // 유틸리티 컬럼(체크박스, expander)은 첫 번째 행에서만 표시하고 전체 높이로 설정 + if (groupIndex > 0 && isUtilityColumn) { + return null; + } + + // 부모 컬럼이 없는 일반 컬럼은 첫 번째 행에서만 표시하고 전체 높이로 설정 + if (groupIndex > 0 && !header.column.parent && !isNestedHeader) { + return null; + } + + // 중첩된 컬럼의 자식은 마지막 행에서만 표시 + if (groupIndex < totalHeaderGroups - 1 && !isNestedHeader && header.column.parent) { + return null; + } + + // rowSpan 계산 + let rowSpan = 1; + if (isUtilityColumn || (!isNestedHeader && !header.column.parent)) { + rowSpan = totalHeaderGroups; // 전체 높이 + } else if (!isNestedHeader && header.column.parent) { + rowSpan = 1; // 자식 컬럼은 한 줄 + } else if (isNestedHeader) { + rowSpan = 1; // 그룹 헤더는 한 줄 + } + + return ( + - ))} - -
- ))} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
- - ); - })} -
+ > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} +
+ {isFirstCell && row.depth > 0 && ( + <> + {/* 수직 라인 */} + + {/* 수평 라인 */} + + + )} +
+ {renderCell(cell)} +
+
+
+ {/* 왼쪽 세로 선 */} +
+ {/* 확장된 내용을 선보다 오른쪽으로 이동 */} +
+ {renderExpandedRowContent(row)} +
+
+
+
-
+
{/* text-sm에서 text-[13px]로 변경 */}
페이지당 행: