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({