diff --git a/plm-api/src/controllers/app/common/product.controller.js b/plm-api/src/controllers/app/common/product.controller.js new file mode 100644 index 0000000..fa0bbfb --- /dev/null +++ b/plm-api/src/controllers/app/common/product.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/product.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("/productList", 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 product +router.post( + "/productCreate", + [ + body("product_name").notEmpty().withMessage("유효한 ID가 필요합니다"), + body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const product = await productService.createProduct(req.body, req.user); + res.status(201).json(product); + } catch (error) { + next(error); + } + } +); + +// Update productUp +router.put( + "/productUpdate/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + body("product_name").optional().notEmpty().withMessage("제품군 이름이 필요합니다."), + body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const product = await productService.findById(req.params.id); + + if (!product) { + return res.status(404).json({ message: "고객사를 찾을 수 없습니다" }); + } + + // company_admin은 자신의 회사 고객사만 수정 가능 + if ( + req.user.role === "company_admin" && + product.companyId !== req.user.companyId + ) { + return res.status(403).json({ + message: "다른 회사의 제품군을 수정할 수 없습니다", + }); + } + + const updatedProduct = await productService.updateProduct(req.params.id, req.body, req.user); + res.json(updatedProduct); + } catch (error) { + next(error); + } + } +); + +// Delete productUp +router.delete( + "/productDelete/:id", + [ + param("id").isUUID().withMessage("유효한 ID가 필요합니다"), + validate, + ], + async (req, res, next) => { + try { + const product = await productService.findById(req.params.id); + + if (!product) { + return res.status(404).json({ message: "제품군을 찾을 수 없습니다" }); + } + + // company_admin은 자신의 회사 고객사만 삭제 가능 + if ( + req.user.role === "company_admin" && + product.companyId !== req.user.companyId + ) { + return res.status(403).json({ + message: "다른 회사의 제품군을 삭제할 수 없습니다", + }); + } + + await productService.deleteProduct(req.params.id, req.user); + res.status(204).end(); + } catch (error) { + next(error); + } + } +); + +module.exports = router; diff --git a/plm-api/src/routes/app.js b/plm-api/src/routes/app.js index 87c2517..8ec4c3c 100644 --- a/plm-api/src/routes/app.js +++ b/plm-api/src/routes/app.js @@ -19,6 +19,7 @@ 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"); +const productController = require("../controllers/app/common/product.controller"); router.use("/health", healthController); router.use("/auth", authController); @@ -37,5 +38,6 @@ router.use("/devices", deviceController); router.use("/common", commonController); router.use("/oemmng", oemMngController); router.use("/productgroup", productgroupController); +router.use("/product", productController); module.exports = router; diff --git a/plm-api/src/services/product.service.js b/plm-api/src/services/product.service.js new file mode 100644 index 0000000..456958b --- /dev/null +++ b/plm-api/src/services/product.service.js @@ -0,0 +1,163 @@ +const { + Product, + Company, + Role, +} = require("../models"); +//const { Op } = require("sequelize"); +const alertService = require("./alert.service"); + +class productService { + 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 Product.count({ where }); + + const products = await Product.findAll({ + where, + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + ], + order: [["createdAt", "DESC"]], + offset, + limit, + distinct: true, + }); + + // 인덱스 추가 + const productsWithIndex = products.map((product, index) => { + const productJson = product.toJSON(); + productJson.index = offset + index + 1; + return productJson; + }); + + return { + total: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + products: productsWithIndex, + }; + } catch (error) { + console.error("Error in findAll:", error); + throw error; + } + } + + async findById(id) { + return await Product.findByPk(id, { + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + ], + }); + } + + async createProduct(productData, currentUser) { + const { roleId, ...productFields } = productData; + + // 등록자 정보 추가 + productFields.writer = currentUser.name; + productFields.regdate = new Date(); + + const product = await Product.create(productFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: product.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await product.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `제품군 ${product.name}이(가) ${currentUser.name}에 의해 생성되었습니다.`, + companyId: product.companyId, + }); + + return product; + } + + async updateProduct(id, updateData, currentUser) { + const { roleId, ...productFields } = updateData; + + const product = await Product.findByPk(id); + if (!product) throw new Error("Product 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 updatedProduct = await product.update(productFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: product.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await product.addRole(role); + } + + await alertService.createAlert({ + type: "info", + message: `제품군 ${product.name}이(가) ${currentUser.name}에 의해 수정되었습니다.`, + companyId: product.companyId, + }); + + return updatedProduct; + } + + async deleteProduct(id, currentUser) { + const product = await Product.findByPk(id); + if (!product) throw new Error("Product not found"); + + const productName = product.product_name; + const companyId = product.companyId; + + await product.destroy(); + + await alertService.createAlert({ + type: "info", + message: `제품 ${productName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`, + companyId: companyId, + }); + + return true; + } +} + +module.exports = new productService(); \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/product/components/ProductForm.tsx b/plm-app/src/app/(admin)/common/product/components/ProductForm.tsx new file mode 100644 index 0000000..2da0729 --- /dev/null +++ b/plm-app/src/app/(admin)/common/product/components/ProductForm.tsx @@ -0,0 +1,100 @@ +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 { product} from "@/types/common/product/product"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +interface ProductFormProps { + initialData?: Partial; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const ProductForm: React.FC = ({ + initialData, + onSubmit, + onCancel, +}) => { + const form = useForm({ + defaultValues: { + product_code: initialData?.product_code || "", + product_name: initialData?.product_name || "", + product_desc: initialData?.product_desc || "", + isActive: initialData?.isActive || false, + }, + }); + + return ( +
+ + ( + + 제품군 + + + + + + )} + /> + + ( + + 설명 + + + + + + )} + /> + ( + +
+ 활성화 + + + +
+ +
+ )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/product/page.tsx b/plm-app/src/app/(admin)/common/product/page.tsx new file mode 100644 index 0000000..6fe2e66 --- /dev/null +++ b/plm-app/src/app/(admin)/common/product/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 { ProductForm } from "./components/ProductForm"; +import { Switch } from "@/components/ui/switch"; +import { product, PaginatedResponse } from "@/types/common/product/product"; +import { PLMTable } from "@/components/common/Table"; +import { Input } from "@/components/ui/input"; + +const ProductPage = () => { + 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 with pagination + const { data, isLoading } = useQuery({ + queryKey: ["products", page, pageSize], + queryFn: async () => { + const { data } = await api.get("/api/v1/app/product/productList", { + params: { + page, + limit: pageSize, + }, + }); + return { + ...data, + products: data.products.map((products: product) => ({ + ...products, + })), + }; + }, + enabled: !!token, + }); + + const handlePageSizeChange = useCallback((newPageSize: number) => { + setPageSize(newPageSize); + setPage(1); + }, []); + + // 필터링된 데이터 생성 + const filteredProducts = useMemo(() => { + if (!data?.products || !searchQuery) return data?.products; + + return data.products.filter(group => + group.product_name?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [data?.products, searchQuery]); + + // Create product mutation + const createMutation = useMutation({ + mutationFn: async (newproject: Partial) => { + // Include companyId in the user data + const WithCompanyId = { + ...newproject, + companyId: user?.companyId, + }; + + const { data } = await api.post( + "/api/v1/app/product/productCreate", + WithCompanyId + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["products"] }); + setIsOpen(false); + toast({ + title: "제품 생성", + description: "새로운 제품이 생성되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "제품 생성 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Update product mutation + const updateMutation = useMutation({ + mutationFn: async (Data: Partial) => { + const { data } = await api.put( + `/api/v1/app/product/productUpdate/${Data.id}`, + Data + ); + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["products"] }); + setIsOpen(false); + setEditingUser(null); + toast({ + title: "제품 수정", + description: "제품 정보가 수정되었습니다.", + }); + }, + onError: (error: AxiosError) => { + toast({ + title: "제품 수정 실패", + description: (error.response?.data as { message: string }).message, + variant: "destructive", + }); + }, + }); + + // Delete product mutation + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + await api.delete(`/api/v1/app/product/productDelete/${id}`); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["products"] }); + 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_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?.products && data.products.length > 0 ? ( + { + setEditingUser(row); + setIsOpen(true); + }} + enableCheckbox={true} + isTreeTable={false} + /> + ) : ( +
+ 등록된 제품이 없습니다. +
+ )} +
+
+
+ + {/* Product 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 ProductPage; diff --git a/plm-app/src/app/page.tsx b/plm-app/src/app/page.tsx index 2391fe7..3636057 100644 --- a/plm-app/src/app/page.tsx +++ b/plm-app/src/app/page.tsx @@ -44,7 +44,7 @@ export default function Home() { features: ["클린룸 에너지 관리", "유틸리티 최적화", "품질 연계 분석"], }, ]; - +6ㅛ7ㅅ6 - // 주요 기능 데이터 const features = [ { diff --git a/plm-app/src/types/common/product/product.ts b/plm-app/src/types/common/product/product.ts new file mode 100644 index 0000000..a94b9ec --- /dev/null +++ b/plm-app/src/types/common/product/product.ts @@ -0,0 +1,24 @@ + + +export interface product { + id: string; + index: number; + product_group_id?: string | null; + product_code?: string | null; + product_name?: string | null; + product_desc?: string | null; + writer?: string | null; + isActive: boolean; +} + +export interface productResponse { + success: boolean; + data: product[]; +} + +export interface PaginatedResponse { + total: number; + totalPages: number; + currentPage: number; + products: product[]; +} \ No newline at end of file