From dedc797c53dc3d50439c2d568cd44ccfb1a9f1a2 Mon Sep 17 00:00:00 2001 From: chpark Date: Thu, 9 Jan 2025 19:06:39 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=81=EC=97=85=20WBS=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/salesmgmt/contractwbs.controller.js | 114 ++++++ plm-api/src/models/Company.js | 2 + plm-api/src/models/ContractWbs.js | 73 ++++ plm-api/src/routes/app.js | 2 + plm-api/src/services/contractwbs.service.js | 175 +++++++++ plm-app/src/app/(user)/layout.tsx | 32 ++ .../components/ContractWbsForm.tsx | 257 +++++++++++++ .../app/(user)/salesmgmt/contractwbs/page.tsx | 349 ++++++++++++++++++ plm-app/src/types/salesmgmt/contractwbs.ts | 23 ++ 9 files changed, 1027 insertions(+) create mode 100644 plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js create mode 100644 plm-api/src/models/ContractWbs.js create mode 100644 plm-api/src/services/contractwbs.service.js create mode 100644 plm-app/src/app/(user)/layout.tsx create mode 100644 plm-app/src/app/(user)/salesmgmt/contractwbs/components/ContractWbsForm.tsx create mode 100644 plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx create mode 100644 plm-app/src/types/salesmgmt/contractwbs.ts diff --git a/plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js b/plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js new file mode 100644 index 0000000..5bc95c2 --- /dev/null +++ b/plm-api/src/controllers/app/salesmgmt/contractwbs.controller.js @@ -0,0 +1,114 @@ +// 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 carMng +router.put( + "/carmngUpdate/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + body("car_code").optional().notEmpty().withMessage("car 코드가 필요합니다"), + 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.updateCarMng(req.params.id, req.body, req.user); + res.json(updatedcarMng); + } catch (error) { + next(error); + } + } +); + +// Delete CarMng +router.delete( + "/carmngDelete/: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; diff --git a/plm-api/src/models/Company.js b/plm-api/src/models/Company.js index d4690b5..58103f9 100644 --- a/plm-api/src/models/Company.js +++ b/plm-api/src/models/Company.js @@ -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" }); + } } diff --git a/plm-api/src/models/ContractWbs.js b/plm-api/src/models/ContractWbs.js new file mode 100644 index 0000000..190c8c0 --- /dev/null +++ b/plm-api/src/models/ContractWbs.js @@ -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; \ No newline at end of file diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js index 7dbc428..e9592be 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 contractWbsController = require("../controllers/app/salesmgmt/contractwbs.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("/contractwbs", contractWbsController); module.exports = router; diff --git a/plm-api/src/services/contractwbs.service.js b/plm-api/src/services/contractwbs.service.js new file mode 100644 index 0000000..89d8ab3 --- /dev/null +++ b/plm-api/src/services/contractwbs.service.js @@ -0,0 +1,175 @@ +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 updateCarMng(id, updateData, currentUser) { + const { roleId, ...carMngFields } = updateData; + + const carMng = await ContractWbs.findByPk(id); + if (!carMng) throw new Error("CarMng not found"); + + // company_admin은 특정 필드를 수정할 수 없음 (예: role을 super_admin으로 변경 불가) + if (currentUser.role === "company_admin") { + if (updateData.role && updateData.role === "super_admin") { + throw new Error("super_admin 역할을 부여할 수 없습니다"); + } + updateData.companyId = currentUser.companyId; + } + + const updatedCarMng = await ContractWbs.update(carMngFields); + + 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(); \ No newline at end of file diff --git a/plm-app/src/app/(user)/layout.tsx b/plm-app/src/app/(user)/layout.tsx new file mode 100644 index 0000000..3ede07b --- /dev/null +++ b/plm-app/src/app/(user)/layout.tsx @@ -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 ( + +
+ {/* 왼쪽 사이드바 */} + + {/* 오른쪽 메인 영역 */} +
+ {/* 상단 헤더 */} +
+ +
+ + {/* 메인 컨텐츠 영역 */} +
+ {children} +
+
+
+
+ ); +}; + +export default AdminLayout; diff --git a/plm-app/src/app/(user)/salesmgmt/contractwbs/components/ContractWbsForm.tsx b/plm-app/src/app/(user)/salesmgmt/contractwbs/components/ContractWbsForm.tsx new file mode 100644 index 0000000..c915b09 --- /dev/null +++ b/plm-app/src/app/(user)/salesmgmt/contractwbs/components/ContractWbsForm.tsx @@ -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; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const ContractWbsForm: React.FC = ({ + 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 ( +
+ +
+
+
+
+ 고객사 +
+ ( + + + + + + + )} + /> +
+
+
+
+
+ 제품군 +
+ ( + + + + + + + )} + /> +
+
+
+
+
+
+
+ ㅅㅅㅅ +
+ +
+
+
+
+
+ 활성화 +
+ ( + +
+ + + +
+ +
+ )} + /> +
+
+
+ + +
+
+
+
+ 모델명 +
+ ( + + + + + + + )} + /> +
+
+
+
+
+ 모델코드 +
+ ( + + + + + + + )} + /> +
+
+
+ + ( + +
+
+ 설명 +
+ + + +
+ +
+ )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx b/plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx new file mode 100644 index 0000000..ea0d094 --- /dev/null +++ b/plm-app/src/app/(user)/salesmgmt/contractwbs/page.tsx @@ -0,0 +1,349 @@ +"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(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({ + queryKey: ["contractWbs", page, pageSize], + queryFn: async () => { + const { data } = await api.get("/api/v1/app/contractwbs/contractwbsList", { + params: { + page, + limit: pageSize, + }, + }); + return { + ...data, + contractWbss: 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) => { + // Include companyId in the user data + const carWithCompanyId = { + ...newCar, + companyId: user?.companyId, + }; + + const { data } = await api.post( + "/api/v1/app/contractwbs/Create", + carWithCompanyId + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + 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 (carData: Partial) => { + const { data } = await api.put( + `/api/v1/app/contractwbs/carmngUpdate/${carData.id}`, + carData + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + setIsOpen(false); + setEditingUser(null); + toast({ + title: "차종 수정", + description: "차종 정보가 수정되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "차종 수정 실패", + 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/carmngDelete/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["carMngs"] }); + 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[] = [ + { + 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: "차종코드", + meta: { + width: "120px", + textAlign: "center", + }, + }, + { + accessorKey: "code.code_name", + header: "차종grade", + meta: { + width: "120px", + textAlign: "center", + }, + }, + { + accessorKey: "car_desc", + header: "설명", + meta: { + width: "200px", + textAlign: "center", + }, + }, + { + accessorKey: "isActive", + header: "활성화", + meta: { + width: "100px", + textAlign: "center", + }, + cell: ({ row }) => ( +
+ { + updateMutation.mutate({ id: row.original.id, isActive: value }); + }} + /> +
+ ), + }, + { + id: "actions", + header: "액션", + meta: { + width: "100px", + textAlign: "center", + }, + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + + if (isLoading) return
Loading...
; + + return ( +
+
+
+
+
+

영업 WBS관리

+

+ 영업 WBS를 관리합니다. +

+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 w-full" + /> +
+
+ +
+
+ {data?.resultData && data.resultData.length > 0 ? ( + <> + { + setEditingUser(row); + setIsOpen(true); + }} + enableCheckbox={true} + isTreeTable={false} + /> + + ) : ( +
+ 등록된 영업 WBS가 없습니다. +
+ )} +
+
+
+ + {/* User Create/Edit Dialog */} + { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + + + {editingUser ? "영업WBS 수정" : "신규 영업WBS"} + + {editingUser + ? "기존 영업 WBS를 수정합니다." + : "새로운 영업 WBS을 생성합니다."} + + + { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + + +
+ ); +}; + +export default Page; diff --git a/plm-app/src/types/salesmgmt/contractwbs.ts b/plm-app/src/types/salesmgmt/contractwbs.ts new file mode 100644 index 0000000..778ff94 --- /dev/null +++ b/plm-app/src/types/salesmgmt/contractwbs.ts @@ -0,0 +1,23 @@ +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[]; // contractWbss가 아닌 resultData로 수정 +} \ No newline at end of file