병합 완료

This commit is contained in:
pgb 2025-01-16 17:50:55 +09:00
commit 3a3ec5f447
9 changed files with 1035 additions and 2 deletions

View File

@ -0,0 +1,113 @@
// src/controllers/admin/users/users.controller.js
const express = require("express");
const router = express.Router();
const Service = require("../../../services/contractwbs.service");
const authMiddleware = require("../../../middleware/auth.middleware");
const roleCheck = require("../../../middleware/roleCheck.middleware");
const { body, param } = require("express-validator");
const validate = require("../../../middleware/validator.middleware");
router.use(authMiddleware);
router.use(roleCheck(["super_admin", "company_admin"]));
// 고객사 목록 조회
router.get("/contractwbsList", async (req, res, next) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const result = await Service.selectList(req.user, page, limit);
res.json(result);
} catch (error) {
next(error);
}
});
// Create WBS
router.post(
"/Create",
[
body("oem_id").notEmpty().withMessage("고객사가 필요합니다"),
body("product_group_id").notEmpty().withMessage("제품군이 필요합니다"),
body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"),
validate,
],
async (req, res, next) => {
try {
const carMng = await Service.create(req.body, req.user);
res.status(201).json(carMng);
} catch (error) {
next(error);
}
}
);
// Update
router.put(
"/Update/:id",
[
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
body("oem_id").optional().notEmpty().withMessage("고객사가 필요합니다"),
body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"),
validate,
],
async (req, res, next) => {
try {
const carMng = await Service.findById(req.params.id);
if (!carMng) {
return res.status(404).json({ message: "고객사를 찾을 수 없습니다" });
}
// company_admin은 자신의 회사 고객사만 수정 가능
if (
req.user.role === "company_admin" &&
carMng.companyId !== req.user.companyId
) {
return res.status(403).json({
message: "다른 회사의 고객사를 수정할 수 없습니다",
});
}
const updatedcarMng = await Service.update(req.params.id, req.body, req.user);
res.json(updatedcarMng);
} catch (error) {
next(error);
}
}
);
// Delete CarMng
router.delete(
"/Delete/:id",
[
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
validate,
],
async (req, res, next) => {
try {
const carMng = await Service.findById(req.params.id);
if (!carMng) {
return res.status(404).json({ message: "고객사를 찾을 수 없습니다" });
}
// company_admin은 자신의 회사 고객사만 삭제 가능
if (
req.user.role === "company_admin" &&
carMng.companyId !== req.user.companyId
) {
return res.status(403).json({
message: "다른 회사의 고객사를 삭제할 수 없습니다",
});
}
await Service.deleteCarMng(req.params.id, req.user);
res.status(204).end();
} catch (error) {
next(error);
}
}
);
module.exports = router;

View File

@ -70,6 +70,8 @@ class Company extends Model {
this.hasMany(models.OemMng, { foreignKey: "companyId" });
this.hasMany(models.ProductGroup, { foreignKey: "companyId" });
this.hasMany(models.Product, { foreignKey: "companyId" });
this.hasMany(models.ContractWbs, { foreignKey: "companyId" });
}
}

View File

@ -0,0 +1,73 @@
// models/ContractWbs.js
const { Model, DataTypes } = require("sequelize");
class ContractWbs extends Model {
static init(sequelize) {
super.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
// 고객사 ID
oem_id: {
type: DataTypes.UUID,
comment: "고객사 ID",
},
// 고객사 ID
product_group_id: {
type: DataTypes.UUID,
comment: "제품군 ID",
},
writer: {
type: DataTypes.STRING(32),
allowNull: true,
comment: "작성자",
},
isActive: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
// 새로 추가할 필드들
companyId: {
type: DataTypes.UUID,
comment: "회사 ID",
},
},
{
sequelize,
modelName: 'ContractWbs',
tableName: 'contractwbs',
timestamps: true,
indexes: [
{
fields: ['id'],
},
],
}
);
return this;
}
static associate(models) {
// car가 Company에 속함
this.belongsTo(models.Company, { foreignKey: "companyId" });
// wbs가 OEM에 속함
this.belongsTo(models.OemMng, {
foreignKey: "oem_id",
as: "oem" // alias 설정
});
// wbs가 제품군에 속함
this.belongsTo(models.ProductGroup, {
foreignKey: "product_group_id",
as: "productgroup" // alias 설정
});
}
}
module.exports = ContractWbs;

View File

@ -21,7 +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");
const contractWbsController = require("../controllers/app/salesmgmt/contractwbs.controller");
router.use("/health", healthController);
router.use("/auth", authController);
@ -42,6 +42,6 @@ router.use("/oemmng", oemMngController);
router.use("/productgroup", productgroupController);
router.use("/product", productController);
router.use("/carmng", carMngController);
router.use("/contract", contractController);
router.use("/contractwbs", contractWbsController);
module.exports = router;

View File

@ -0,0 +1,181 @@
const {
ContractWbs,
Company,
Role,
OemMng,
ProductGroup,
} = require("../models");
//const { Op } = require("sequelize");
const alertService = require("./alert.service");
class ContractWbsService {
async selectList(currentUser, page = 1, limit = 10) {
try {
// Initialize the where clause
let where = {};
// company_admin은 자신의 회사 유저만 조회 가능
if (currentUser.role === "company_admin") {
where.companyId = currentUser.companyId;
}
// isActive 필터 추가
//where.isActive = { [Op.eq]: true };
const offset = (page - 1) * limit;
// 전체 개수 조회
const count = await ContractWbs.count({ where });
const resultData = await ContractWbs.findAll({
where,
include: [
{
model: Company,
attributes: ["id", "name"],
},
{
model: OemMng,
as: "oem",
attributes: ["oem_name"],
},
{
model: ProductGroup,
as: "productgroup",
attributes: ["product_group_name"],
},
],
order: [["createdAt", "DESC"]],
offset,
limit,
distinct: true,
});
// 인덱스 추가
const WithIndex = resultData.map((car, index) => {
const Json = car.toJSON();
Json.index = offset + index + 1;
return Json;
});
return {
total: count,
totalPages: Math.ceil(count / limit),
currentPage: page,
resultData: WithIndex,
};
} catch (error) {
console.error("Error in findAll:", error);
throw error;
}
}
async findById(id) {
return await ContractWbs.findByPk(id, {
include: [
{
model: Company,
attributes: ["id", "name"],
},
],
});
}
async create(SaveData, currentUser) {
const { roleId, ...Fields } = SaveData;
// 등록자 정보 추가
Fields.writer = currentUser.name;
Fields.regdate = new Date();
const ResultData = await ContractWbs.create(Fields);
if (roleId) {
const role = await Role.findOne({
where: {
id: roleId,
companyId: ResultData.companyId,
},
});
if (!role) {
throw new Error("Role not found or does not belong to the company");
}
await ContractWbs.addRole(role);
}
await alertService.createAlert({
type: "info",
message: `영업WBS가 ${currentUser.name}에 의해 생성되었습니다.`,
companyId: ResultData.companyId,
});
return ResultData;
}
async update(id, updateData, currentUser) {
const { roleId, ...carMngFields } = updateData;
const carMng = await ContractWbs.findByPk(id);
if (!carMng) throw new Error("CarMng not found");
// company_admin 권한 체크
if (currentUser.role === "company_admin") {
if (updateData.role && updateData.role === "super_admin") {
throw new Error("super_admin 역할을 부여할 수 없습니다");
}
updateData.companyId = currentUser.companyId;
}
// where 조건을 포함하여 업데이트
await ContractWbs.update(carMngFields, {
where: { id: id }
});
// 업데이트된 데이터 조회
const updatedCarMng = await ContractWbs.findByPk(id);
if (roleId) {
const role = await Role.findOne({
where: {
id: roleId,
companyId: carMng.companyId,
},
});
if (!role) {
throw new Error("Role not found or does not belong to the company");
}
await ContractWbs.addRole(role);
}
await alertService.createAlert({
type: "info",
message: `영업 WBS가 ${currentUser.name}에 의해 수정되었습니다.`,
companyId: carMng.companyId,
});
return updatedCarMng;
}
async deleteData(id, currentUser) {
const carMng = await ContractWbs.findByPk(id);
if (!carMng) throw new Error("CarMng not found");
const carMngName = carMng.name;
const companyId = carMng.companyId;
await carMng.destroy();
await alertService.createAlert({
type: "info",
message: `영업 WBS가 ${currentUser.name}에 의해 삭제되었습니다.`,
companyId: companyId,
});
return true;
}
}
module.exports = new ContractWbsService();

View File

@ -0,0 +1,32 @@
// src/app/(admin)/layout.tsx
import React from "react";
import AdminGuard from "@/components/auth/AdminGuard";
import { SideNav } from "@/components/layout/SideNav";
import { TopNav } from "@/components/layout/TopNav";
const AdminLayout = ({ children }: { children: React.ReactNode }) => {
return (
<AdminGuard>
<div className="h-screen flex">
{/* 왼쪽 사이드바 */}
<aside className="w-64 h-screen flex-shrink-0 bg-gray-800">
<SideNav />
</aside>
{/* 오른쪽 메인 영역 */}
<div className="flex-1 flex flex-col">
{/* 상단 헤더 */}
<header className="h-16 bg-white border-b">
<TopNav />
</header>
{/* 메인 컨텐츠 영역 */}
<main className="flex-1 overflow-auto bg-gray-50 p-2">
{children}
</main>
</div>
</div>
</AdminGuard>
);
};
export default AdminLayout;

View File

@ -0,0 +1,257 @@
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { contractWbs} from "@/types/salesmgmt/contractwbs";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; // Select 컴포넌트 임포트 추가
import { api } from "@/lib/api"; // API 호출을 위한 임포트 추가
interface CarFormProps {
initialData?: Partial<contractWbs>;
onSubmit: (data: Partial<contractWbs>) => void;
onCancel: () => void;
}
export const ContractWbsForm: React.FC<CarFormProps> = ({
initialData,
onSubmit,
onCancel,
}) => {
const form = useForm({
defaultValues: {
oem_id: initialData?.oem_id || "",
product_group_id: initialData?.product_group_id || "",
isActive: initialData?.isActive || false,
},
});
const [productGroups, setProductGroups] = useState([]);
const [oems, setoems] = useState([]);
useEffect(() => {
const fetchSelectOptions = async (modelName, orderField) => {
try {
const response = await api.get("/api/v1/app/common/select-options", {
params: { modelName, orderField },
});
console.log("Fetched select options:", response.data.data); // 디버깅 로그 추가
setProductGroups(response.data.data);
} catch (error) {
console.error("Failed to fetch select options:", error);
}
};
const fetchSelectOptions1 = async (modelName, orderField) => {
try {
const response = await api.get("/api/v1/app/common/select-options", {
params: { modelName, orderField },
});
console.log("Fetched select options:", response.data.data); // 디버깅 로그 추가
setoems(response.data.data);
console.log(response.data.data);
} catch (error) {
console.error("Failed to fetch select options:", error);
}
};
fetchSelectOptions("OemMng", "oem_name");
fetchSelectOptions1("ProductGroup","product_group_name");
}, []);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="flex items-center gap-4">
<div className="w-24 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormField
control={form.control}
name="oem_id"
validation={{
required: "고객사는 필수 입력값입니다",
}}
render={({ field }) => (
<FormItem>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="고객사를 선택하세요" />
</SelectTrigger>
<SelectContent>
{productGroups.map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.oem_name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div>
<div className="flex items-center gap-4">
<div className="w-36 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormField
control={form.control}
name="product_group_id"
validation={{
required: "제품군은 필수 입력값입니다",
}}
render={({ field }) => (
<FormItem>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="제품군를 선택하세요" />
</SelectTrigger>
<SelectContent>
{oems.map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.product_group_name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="flex items-center gap-4">
<div className="w-24 p-2 text-center">
<FormLabel></FormLabel>
</div>
</div>
</div>
<div>
<div className="flex items-center gap-4">
<div className="w-36 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="flex items-center gap-4">
<div className="w-24 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormField
control={form.control}
name="model_name"
validation={{
required: "모델명은 필수 입력값입니다",
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
}}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<div>
<div className="flex items-center gap-4">
<div className="w-36 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormField
control={form.control}
name="model_code"
validation={{
required: "모델코드는 필수 입력값입니다",
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
}}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
<FormField
control={form.control}
name="car_desc"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-4">
<div className="w-24 p-2 text-center">
<FormLabel></FormLabel>
</div>
<FormControl className="flex-1">
<Input {...field} />
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit">
{initialData ? "수정" : "저장"}
</Button>
</div>
</form>
</Form>
);
};

View File

@ -0,0 +1,353 @@
"use client";
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";
import { api } from "@/lib/api";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { Plus, Edit, Trash2, Search } from "lucide-react";
import { ColumnDef } from "@tanstack/react-table";
import { useAuthStore } from "@/stores/auth";
import { AxiosError } from "axios";
import { ContractWbsForm } from "./components/ContractWbsForm";
import { Switch } from "@/components/ui/switch";
import { contractWbs, PaginatedResponse } from "@/types/salesmgmt/contractwbs";
import { PLMTable } from "@/components/common/Table";
import { Input } from "@/components/ui/input";
const Page = () => {
const { token, user } = useAuthStore();
const [isOpen, setIsOpen] = React.useState(false);
const [editingUser, setEditingUser] = React.useState<contractWbs | null>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const { toast } = useToast();
const queryClient = useQueryClient();
const [currentPageSize, setCurrentPageSize] = useState(20); // 페이지 크기 상태 추가
const [searchQuery, setSearchQuery] = useState(""); // 검색어 상태 추가
// Fetch CARs with pagination
const { data, isLoading } = useQuery<PaginatedResponse>({
queryKey: ["contractWbs", page, pageSize],
queryFn: async () => {
const { data } = await api.get("/api/v1/app/contractwbs/contractwbsList", {
params: {
page,
limit: pageSize,
},
});
return {
...data,
resultData: data.resultData.map((wbs: contractWbs) => ({ // resultData 사용
...wbs,
})),
};
},
enabled: !!token,
});
const handlePageSizeChange = useCallback((newPageSize: number) => {
setPageSize(newPageSize);
setPage(1);
}, []);
// 필터링된 데이터 생성
const filteredWbsList = useMemo(() => {
if (!data?.resultData || !searchQuery) return data?.resultData;
return data.resultData.filter(wbs =>
wbs.oem_id?.toLowerCase().includes(searchQuery.toLowerCase()) ||
wbs.product_group_id?.toLowerCase().includes(searchQuery.toLowerCase())
);
}, [data?.resultData, searchQuery]);
// Create user mutation
const createMutation = useMutation({
mutationFn: async (newCar: Partial<contractWbs>) => {
// Include companyId in the user data
const carWithCompanyId = {
...newCar,
companyId: user?.companyId,
};
const { data } = await api.post<contractWbs>(
"/api/v1/app/contractwbs/Create",
carWithCompanyId
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
setIsOpen(false);
toast({
title: "영업 WBS 생성",
description: "새로운 영업 WBS 생성되었습니다.",
});
},
onError: (error: AxiosError) => {
toast({
title: "영업 WBS 생성 실패",
description: (error.response?.data as { message: string }).message,
variant: "destructive",
});
},
});
// Update user mutation
const updateMutation = useMutation({
mutationFn: async (contractWbsData: Partial<contractWbs>) => {
const { data } = await api.put<contractWbs>(
`/api/v1/app/contractwbs/Update/${contractWbsData.id}`,
contractWbsData
);
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
setIsOpen(false);
setEditingUser(null);
toast({
title: "영업 WBS 수정",
description: "영업 WBS 정보가 수정되었습니다.",
});
},
onError: (error: AxiosError) => {
toast({
title: "영업 WBS 수정 실패",
description: (error.response?.data as { message: string }).message,
variant: "destructive",
});
},
});
// Delete user mutation
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
await api.delete(`/api/v1/app/contractwbs/Delete/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["contractWbs"] }); // carMngs에서 contractWbs로 수정
toast({
title: "영업 WBS 삭제",
description: "영업 WBS가 삭제되었습니다.",
});
},
onError: (error: AxiosError) => {
toast({
title: "영업 WBS 삭제 실패",
description: (error.response?.data as { message: string }).message,
variant: "destructive",
});
},
});
// Table columns 수정
const columns: ColumnDef<contractWbs>[] = [
{
id: "index",
header: "No",
cell: ({ row }) => row.original.index,
meta: {
width: "60px",
textAlign: "center",
},
},
{
accessorKey: "oem.oem_name", // 중첩 객체 접근 방식 확인
header: "고객사",
meta: {
width: "120px",
textAlign: "center",
},
},
{
accessorKey: "productgroup.product_group_name", // 제품군 정보 표시
header: "제품군",
meta: {
width: "120px",
textAlign: "center",
},
},
{
accessorKey: "car_code",
header: "WBS",
meta: {
width: "120px",
textAlign: "center",
},
},
{
accessorKey: "writer",
header: "등록자",
meta: {
width: "120px",
textAlign: "center",
},
},
{
accessorKey: "updatedAt",
header: "등록일",
meta: {
width: "200px",
textAlign: "center",
},
cell: ({ row }) => {
const date = new Date(row.original.updatedAt);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
},
{
accessorKey: "isActive",
header: "상태",
meta: {
width: "100px",
textAlign: "center",
},
cell: ({ row }) => (
<div className="text-center">
<Switch
checked={row.original.isActive}
onCheckedChange={(value) => {
updateMutation.mutate({ id: row.original.id, isActive: value });
}}
/>
</div>
),
},
{
id: "actions",
header: "액션",
meta: {
width: "100px",
textAlign: "center",
},
cell: ({ row }) => (
<div className="flex items-center justify-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingUser(row.original);
setIsOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
if (confirm("정말 삭제하시겠습니까?")) {
deleteMutation.mutate(row.original.id);
}
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
),
},
];
if (isLoading) return <div>Loading...</div>;
return (
<div className="w-full h-full">
<div className="bg-white rounded-lg shadow">
<div className="p-2 border-b border-gray-200">
<div className="flex justify-between items-center mb-3">
<div>
<h1 className="text-2xl font-semibold text-gray-900"> WBS관리</h1>
<p className="text-sm text-gray-500">
WBS를 .
</p>
</div>
<Button onClick={() => setIsOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
WBS
</Button>
</div>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="WBS 검색"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 w-full"
/>
</div>
</div>
<div className="p-2">
<div className="h-[650px]">
{data?.resultData && data.resultData.length > 0 ? (
<>
<PLMTable
columns={columns}
data={filteredWbsList || []} // 필터링된 데이터 사용
pageSize={currentPageSize}
onPageSizeChange={setCurrentPageSize}
onRowClick={(row: contractWbs) => {
setEditingUser(row);
setIsOpen(true);
}}
enableCheckbox={true}
isTreeTable={false}
/>
</>
) : (
<div className="text-center py-12 text-muted-foreground">
WBS가 .
</div>
)}
</div>
</div>
</div>
{/* User Create/Edit Dialog */}
<Dialog
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open);
if (!open) setEditingUser(null);
}}
>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingUser ? "영업WBS 수정" : "신규 영업WBS"}</DialogTitle>
<DialogDescription>
{editingUser
? "기존 영업 WBS를 수정합니다."
: "새로운 영업 WBS을 생성합니다."}
</DialogDescription>
</DialogHeader>
<ContractWbsForm
initialData={editingUser || undefined}
onSubmit={(data) => {
if (editingUser) {
updateMutation.mutate({ id: editingUser.id, ...data });
} else {
createMutation.mutate(data);
}
}}
onCancel={() => {
setIsOpen(false);
setEditingUser(null);
}}
/>
</DialogContent>
</Dialog>
</div>
);
};
export default Page;

View File

@ -0,0 +1,22 @@
export interface contractWbs {
id: string;
index: number;
oem_id?: string | null;
product_group_id?: string | null;
writer?: string | null;
regdate?: Date | null;
isActive: boolean;
companyId: string;
count?: number;
}
export interface contractWbsResponse {
success: boolean;
data: contractWbs[];
}
export interface PaginatedResponse {
total: number;
totalPages: number;
currentPage: number;
resultData: contractWbs[];