From c3864c1cda99e46e844e28a980626a575cfe044d Mon Sep 17 00:00:00 2001 From: chpark Date: Mon, 30 Dec 2024 16:46:27 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=9C=ED=92=88=EA=B5=B0=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=BB=A4=EB=B0=8B=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/common/productgroup.controller.js | 112 ++++++ plm-api/src/models/ProductGroup.js | 62 ++++ plm-api/src/routes/app.js | 2 + plm-api/src/services/productgroup.service.js | 163 +++++++++ .../src/app/(admin)/common/oemmng/page.tsx | 1 - .../components/ProductGroupForm.tsx | 99 ++++++ .../app/(admin)/common/productgroup/page.tsx | 322 ++++++++++++++++++ .../types/common/productgroup/productgroup.ts | 23 ++ 8 files changed, 783 insertions(+), 1 deletion(-) create mode 100644 plm-api/src/controllers/app/common/productgroup.controller.js create mode 100644 plm-api/src/models/ProductGroup.js create mode 100644 plm-api/src/services/productgroup.service.js create mode 100644 plm-app/src/app/(admin)/common/productgroup/components/ProductGroupForm.tsx create mode 100644 plm-app/src/app/(admin)/common/productgroup/page.tsx create mode 100644 plm-app/src/types/common/productgroup/productgroup.ts diff --git a/plm-api/src/controllers/app/common/productgroup.controller.js b/plm-api/src/controllers/app/common/productgroup.controller.js new file mode 100644 index 0000000..84e8cfd --- /dev/null +++ b/plm-api/src/controllers/app/common/productgroup.controller.js @@ -0,0 +1,112 @@ +// src/controllers/admin/users/users.controller.js +const express = require("express"); +const router = express.Router(); +const productService = require("../../../services/productgroup.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("/productgroupList", async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const result = await productService.findAll(req.user, page, limit); + res.json(result); + } catch (error) { + next(error); + } +}); + +// Create productgroup +router.post( + "/productgroupCreate", + [ + body("product_group_name").notEmpty().withMessage("유효한 ID가 필요합니다"), + body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const productGroup = await productService.createProductGroup(req.body, req.user); + res.status(201).json(productGroup); + } catch (error) { + next(error); + } + } +); + +// Update productgroupUp +router.put( + "/productgroupUpdate/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + body("product_group_name").optional().notEmpty().withMessage("제품군 이름이 필요합니다."), + body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const productGroup = await productService.findById(req.params.id); + + if (!productGroup) { + return res.status(404).json({ message: "고객사를 찾을 수 없습니다" }); + } + + // company_admin은 자신의 회사 고객사만 수정 가능 + if ( + req.user.role === "company_admin" && + productGroup.companyId !== req.user.companyId + ) { + return res.status(403).json({ + message: "다른 회사의 고객사를 수정할 수 없습니다", + }); + } + + const updatedProductGroup = await productService.updateProductGroup(req.params.id, req.body, req.user); + res.json(updatedProductGroup); + } catch (error) { + next(error); + } + } +); + +// Delete productgroupUp +router.delete( + "/productgroupUpDelete/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const productGroup = await productService.findById(req.params.id); + + if (!productGroup) { + return res.status(404).json({ message: "고객사를 찾을 수 없습니다" }); + } + + // company_admin은 자신의 회사 고객사만 삭제 가능 + if ( + req.user.role === "company_admin" && + productGroup.companyId !== req.user.companyId + ) { + return res.status(403).json({ + message: "다른 회사의 고객사를 삭제할 수 없습니다", + }); + } + + await productService.deleteProductGroup(req.params.id, req.user); + res.status(204).end(); + } catch (error) { + next(error); + } + } +); + +module.exports = router; diff --git a/plm-api/src/models/ProductGroup.js b/plm-api/src/models/ProductGroup.js new file mode 100644 index 0000000..d6c7ce3 --- /dev/null +++ b/plm-api/src/models/ProductGroup.js @@ -0,0 +1,62 @@ +// models/ProductGroup.js + +const { Model, DataTypes } = require("sequelize"); + +class ProductGroup extends Model { + static init(sequelize) { + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + product_group_name: { + type: DataTypes.STRING(64), + allowNull: true, + }, + description: { + type: DataTypes.STRING(1000), + allowNull: true, + }, + writer: { + type: DataTypes.STRING(32), + allowNull: true, + }, + regdate: { + type: DataTypes.DATE, + allowNull: true, + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + }, + // 새로 추가할 필드들 + companyId: { + type: DataTypes.UUID, + comment: "회사 ID", + }, + }, + { + sequelize, + modelName: 'ProductGroup', + tableName: 'product_group_mng1', + timestamps: true, + indexes: [ + { + fields: ['id'], + }, + ], + } + ); + return this; + } + + static associate(models) { + // OemMng이 Company에 속함 + this.belongsTo(models.Company, { foreignKey: "companyId" }); + } + +} + +module.exports = ProductGroup; \ No newline at end of file diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js index afb3f5b..87c2517 100644 --- a/plm-api/src/routes/app.js +++ b/plm-api/src/routes/app.js @@ -18,6 +18,7 @@ const companiesController = require("../controllers/admin/companies/companies.co const deviceController = require("../controllers/app/device/device.controller"); const commonController = require("../controllers/app/common/common.controller"); const oemMngController = require("../controllers/app/common/oemmng.controller"); +const productgroupController = require("../controllers/app/common/productgroup.controller"); router.use("/health", healthController); router.use("/auth", authController); @@ -35,5 +36,6 @@ router.use("/companies", companiesController); router.use("/devices", deviceController); router.use("/common", commonController); router.use("/oemmng", oemMngController); +router.use("/productgroup", productgroupController); module.exports = router; diff --git a/plm-api/src/services/productgroup.service.js b/plm-api/src/services/productgroup.service.js new file mode 100644 index 0000000..5d1345d --- /dev/null +++ b/plm-api/src/services/productgroup.service.js @@ -0,0 +1,163 @@ +const { + ProductGroup, + Company, + Role, +} = require("../models"); +//const { Op } = require("sequelize"); +const alertService = require("./alert.service"); + +class productgroupService { + async findAll(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 ProductGroup.count({ where }); + + const productgroups = await ProductGroup.findAll({ + where, + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + ], + order: [["createdAt", "DESC"]], + offset, + limit, + distinct: true, + }); + + // 인덱스 추가 + const productGroupsWithIndex = productgroups.map((productgroup, index) => { + const productgroupJson = productgroup.toJSON(); + productgroupJson.index = offset + index + 1; + return productgroupJson; + }); + + return { + total: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + productgroups: productGroupsWithIndex, + }; + } catch (error) { + console.error("Error in findAll:", error); + throw error; + } + } + + async findById(id) { + return await ProductGroup.findByPk(id, { + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + ], + }); + } + + async createProductGroup(productgroupData, currentUser) { + const { roleId, ...productgroupFields } = productgroupData; + + // 등록자 정보 추가 + productgroupFields.writer = currentUser.name; + productgroupFields.regdate = new Date(); + + const productgroup = await ProductGroup.create(productgroupFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: productgroup.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await productgroup.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `제품군 ${productgroup.name}이(가) ${currentUser.name}에 의해 생성되었습니다.`, + companyId: productgroup.companyId, + }); + + return productgroup; + } + + async updateProductGroup(id, updateData, currentUser) { + const { roleId, ...productgroupFields } = updateData; + + const productgroup = await ProductGroup.findByPk(id); + if (!productgroup) throw new Error("ProductGroup 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 updatedProductGroup = await productgroup.update(productgroupFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: productgroup.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await productgroup.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `제품군 ${productgroup.name}이(가) ${currentUser.name}에 의해 수정되었습니다.`, + companyId: productgroup.companyId, + }); + + return updatedProductGroup; + } + + async deleteProductGroup(id, currentUser) { + const productgroup = await ProductGroup.findByPk(id); + if (!productgroup) throw new Error("ProductGroup not found"); + + const productgroupName = productgroup.name; + const companyId = productgroup.companyId; + + await productgroup.destroy(); + + await alertService.createAlert({ + type: "info", + message: `제품군 ${productgroupName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`, + companyId: companyId, + }); + + return true; + } +} + +module.exports = new productgroupService(); \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/oemmng/page.tsx b/plm-app/src/app/(admin)/common/oemmng/page.tsx index c7e82ba..dc918be 100644 --- a/plm-app/src/app/(admin)/common/oemmng/page.tsx +++ b/plm-app/src/app/(admin)/common/oemmng/page.tsx @@ -1,4 +1,3 @@ -// src/app/(admin)/users/accounts/page.tsx "use client"; import React, { useState, useCallback, useMemo } from "react"; diff --git a/plm-app/src/app/(admin)/common/productgroup/components/ProductGroupForm.tsx b/plm-app/src/app/(admin)/common/productgroup/components/ProductGroupForm.tsx new file mode 100644 index 0000000..3d53977 --- /dev/null +++ b/plm-app/src/app/(admin)/common/productgroup/components/ProductGroupForm.tsx @@ -0,0 +1,99 @@ +import React 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 { productgroup} from "@/types/common/productgroup/productgroup"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +interface ProductGroupFormProps { + initialData?: Partial; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const ProductGroupForm: React.FC = ({ + initialData, + onSubmit, + onCancel, +}) => { + const form = useForm({ + defaultValues: { + product_group_name: initialData?.product_group_name || "", + description: initialData?.description || "", + isActive: initialData?.isActive || false, + }, + }); + + return ( +
+ + ( + + 제품군 + + + + + + )} + /> + + ( + + 설명 + + + + + + )} + /> + ( + +
+ 활성화 + + + +
+ +
+ )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/productgroup/page.tsx b/plm-app/src/app/(admin)/common/productgroup/page.tsx new file mode 100644 index 0000000..70b6a53 --- /dev/null +++ b/plm-app/src/app/(admin)/common/productgroup/page.tsx @@ -0,0 +1,322 @@ +"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 { ProductGroupForm } from "./components/ProductGroupForm"; +import { Switch } from "@/components/ui/switch"; +import { productgroup, PaginatedResponse } from "@/types/common/productgroup/productgroup"; +import { PLMTable } from "@/components/common/Table"; +import { Input } from "@/components/ui/input"; + +const ProductGroupPage = () => { + 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 product groups with pagination + const { data, isLoading } = useQuery({ + queryKey: ["productgroups", page, pageSize], + queryFn: async () => { + const { data } = await api.get("/api/v1/app/productgroup/productgroupList", { + params: { + page, + limit: pageSize, + }, + }); + return { + ...data, + productgroups: data.productgroups.map((productgroups: productgroup) => ({ + ...productgroups, + })), + }; + }, + enabled: !!token, + }); + + const handlePageSizeChange = useCallback((newPageSize: number) => { + setPageSize(newPageSize); + setPage(1); + }, []); + + // 필터링된 데이터 생성 + const filteredProductGroups = useMemo(() => { + if (!data?.productgroups || !searchQuery) return data?.productgroups; + + return data.productgroups.filter(group => + group.product_group_name?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [data?.productgroups, searchQuery]); + + // Create product group mutation + const createMutation = useMutation({ + mutationFn: async (newGroup: Partial) => { + // Include companyId in the user data + const groupWithCompanyId = { + ...newGroup, + companyId: user?.companyId, + }; + + const { data } = await api.post( + "/api/v1/app/productgroup/productgroupCreate", + groupWithCompanyId + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["productgroups"] }); + setIsOpen(false); + toast({ + title: "제품군 생성", + description: "새로운 제품군이 생성되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "제품군 생성 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Update product group mutation + const updateMutation = useMutation({ + mutationFn: async (groupData: Partial) => { + const { data } = await api.put( + `/api/v1/app/productgroup/productgroupUpdate/${groupData.id}`, + groupData + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["productgroups"] }); + setIsOpen(false); + setEditingUser(null); + toast({ + title: "제품군 수정", + description: "제품군 정보가 수정되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "제품군 수정 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Delete product group mutation + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + await api.delete(`/api/v1/app/productgroup/productgroupDelete/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["productGroups"] }); + toast({ + title: "제품군 삭제", + description: "제품군이 삭제되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "제품군 삭제 실패", + 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: "product_group_name", + header: "제품군명", + meta: { + width: "120px", + textAlign: "center", + }, + }, + { + accessorKey: "description", + 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 ( +
+
+
+
+
+

제품군 관리

+

+ 제품군을 관리하고 권한을 설정합니다. +

+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-8 w-full" + /> +
+
+ +
+
+ {data?.productgroups && data.productgroups.length > 0 ? ( + { + setEditingUser(row); + setIsOpen(true); + }} + enableCheckbox={true} + isTreeTable={false} + /> + ) : ( +
+ 등록된 제품군이 없습니다. +
+ )} +
+
+
+ + {/* Product Group Create/Edit Dialog */} + { + setIsOpen(open); + if (!open) setEditingUser(null); + }} + > + + + {editingUser ? "제품군 수정" : "새 제품군"} + + {editingUser + ? "기존 제품군 정보를 수정합니다." + : "새로운 제품군을 생성합니다."} + + + { + if (editingUser) { + updateMutation.mutate({ id: editingUser.id, ...data }); + } else { + createMutation.mutate(data); + } + }} + onCancel={() => { + setIsOpen(false); + setEditingUser(null); + }} + /> + + +
+ ); +}; + +export default ProductGroupPage; diff --git a/plm-app/src/types/common/productgroup/productgroup.ts b/plm-app/src/types/common/productgroup/productgroup.ts new file mode 100644 index 0000000..97b1c4e --- /dev/null +++ b/plm-app/src/types/common/productgroup/productgroup.ts @@ -0,0 +1,23 @@ + + +export interface productgroup { + id: string; + index: number; + product_group_name?: string | null; + description?: string | null; + writer?: string | null; + regdate?: Date | null; + isActive: boolean; +} + +export interface productgroupResponse { + success: boolean; + data: productgroup[]; +} + +export interface PaginatedResponse { + total: number; + totalPages: number; + currentPage: number; + productgroups: productgroup[]; +} \ No newline at end of file