Compare commits

...

2 Commits

Author SHA1 Message Date
2efe5706d4 에러 수정정 2024-12-30 17:15:22 +09:00
c3864c1cda 제품군 관리 커밋밋 2024-12-30 16:46:27 +09:00
8 changed files with 783 additions and 1 deletions

View File

@ -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(
"/productgroupDelete/: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;

View File

@ -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;

View File

@ -18,6 +18,7 @@ const companiesController = require("../controllers/admin/companies/companies.co
const deviceController = require("../controllers/app/device/device.controller"); const deviceController = require("../controllers/app/device/device.controller");
const commonController = require("../controllers/app/common/common.controller"); const commonController = require("../controllers/app/common/common.controller");
const oemMngController = require("../controllers/app/common/oemmng.controller"); const oemMngController = require("../controllers/app/common/oemmng.controller");
const productgroupController = require("../controllers/app/common/productgroup.controller");
router.use("/health", healthController); router.use("/health", healthController);
router.use("/auth", authController); router.use("/auth", authController);
@ -35,5 +36,6 @@ router.use("/companies", companiesController);
router.use("/devices", deviceController); router.use("/devices", deviceController);
router.use("/common", commonController); router.use("/common", commonController);
router.use("/oemmng", oemMngController); router.use("/oemmng", oemMngController);
router.use("/productgroup", productgroupController);
module.exports = router; module.exports = router;

View File

@ -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.product_group_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();

View File

@ -1,4 +1,3 @@
// src/app/(admin)/users/accounts/page.tsx
"use client"; "use client";
import React, { useState, useCallback, useMemo } from "react"; import React, { useState, useCallback, useMemo } from "react";

View File

@ -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<productgroup>;
onSubmit: (data: Partial<productgroup>) => void;
onCancel: () => void;
}
export const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
initialData,
onSubmit,
onCancel,
}) => {
const form = useForm({
defaultValues: {
product_group_name: initialData?.product_group_name || "",
description: initialData?.description || "",
isActive: initialData?.isActive || false,
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="product_group_name"
validation={{
required: "제품군은 필수 입력값입니다",
maxLength: { value: 100, message: "최대 100자까지 입력 가능합니다" }
}}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel></FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</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,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<productgroup | 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 product groups with pagination
const { data, isLoading } = useQuery<PaginatedResponse>({
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<productgroup>) => {
// Include companyId in the user data
const groupWithCompanyId = {
...newGroup,
companyId: user?.companyId,
};
const { data } = await api.post<productgroup>(
"/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<productgroup>) => {
const { data } = await api.put<productgroup>(
`/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<productgroup>[] = [
{
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 }) => (
<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"> </h1>
<p className="text-sm text-gray-500">
.
</p>
</div>
<Button onClick={() => setIsOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="제품군명 또는 코드 검색"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 w-full"
/>
</div>
</div>
<div className="p-2">
<div className="h-[650px]">
{data?.productgroups && data.productgroups.length > 0 ? (
<PLMTable
columns={columns}
data={filteredProductGroups || []} // 필터링된 데이터 사용
pageSize={currentPageSize}
onPageSizeChange={setCurrentPageSize}
onRowClick={(row: productgroup) => {
setEditingUser(row);
setIsOpen(true);
}}
enableCheckbox={true}
isTreeTable={false}
/>
) : (
<div className="text-center py-12 text-muted-foreground">
.
</div>
)}
</div>
</div>
</div>
{/* Product Group 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 ? "제품군 수정" : "새 제품군"}</DialogTitle>
<DialogDescription>
{editingUser
? "기존 제품군 정보를 수정합니다."
: "새로운 제품군을 생성합니다."}
</DialogDescription>
</DialogHeader>
<ProductGroupForm
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 ProductGroupPage;

View File

@ -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[];
}