Compare commits
2 Commits
996616fc23
...
e9cad28c3a
Author | SHA1 | Date | |
---|---|---|---|
e9cad28c3a | |||
5c5b9c5433 |
@ -46,8 +46,12 @@ router.put(
|
|||||||
"/productUpdate/:id",
|
"/productUpdate/:id",
|
||||||
[
|
[
|
||||||
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
|
param("id").isUUID().withMessage("유효한 ID가 필요합니다"),
|
||||||
|
<<<<<<< HEAD
|
||||||
|
body("product_name").optional().notEmpty().withMessage("제품군 이름이 필요합니다."),
|
||||||
|
=======
|
||||||
body("product_code").optional().notEmpty().withMessage("제품 코드가 필요합니다."),
|
body("product_code").optional().notEmpty().withMessage("제품 코드가 필요합니다."),
|
||||||
body("product_name").optional().notEmpty().withMessage("제품 이름이 필요합니다."),
|
body("product_name").optional().notEmpty().withMessage("제품 이름이 필요합니다."),
|
||||||
|
>>>>>>> 996616fc2324b66b3ae033eced43180612cac2bf
|
||||||
body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"),
|
body("companyId").optional().isUUID().withMessage("유효한 회사 ID가 필요합니다"),
|
||||||
validate,
|
validate,
|
||||||
],
|
],
|
||||||
|
@ -152,7 +152,11 @@ class productService {
|
|||||||
|
|
||||||
await alertService.createAlert({
|
await alertService.createAlert({
|
||||||
type: "info",
|
type: "info",
|
||||||
|
<<<<<<< HEAD
|
||||||
|
message: `제품 ${productName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`,
|
||||||
|
=======
|
||||||
message: `제품군 ${productName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`,
|
message: `제품군 ${productName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`,
|
||||||
|
>>>>>>> 996616fc2324b66b3ae033eced43180612cac2bf
|
||||||
companyId: companyId,
|
companyId: companyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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<product>;
|
||||||
|
onSubmit: (data: Partial<product>) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductForm: React.FC<ProductFormProps> = ({
|
||||||
|
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 (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="product_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>
|
||||||
|
);
|
||||||
|
};
|
322
plm-app/src/app/(admin)/common/product/page.tsx
Normal file
322
plm-app/src/app/(admin)/common/product/page.tsx
Normal 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 { 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<product | 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 with pagination
|
||||||
|
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||||
|
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<product>) => {
|
||||||
|
// Include companyId in the user data
|
||||||
|
const WithCompanyId = {
|
||||||
|
...newproject,
|
||||||
|
companyId: user?.companyId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data } = await api.post<product>(
|
||||||
|
"/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<product>) => {
|
||||||
|
const { data } = await api.put<product>(
|
||||||
|
`/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<product>[] = [
|
||||||
|
{
|
||||||
|
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 }) => (
|
||||||
|
<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?.products && data.products.length > 0 ? (
|
||||||
|
<PLMTable
|
||||||
|
columns={columns}
|
||||||
|
data={filteredProducts || []} // 필터링된 데이터 사용
|
||||||
|
pageSize={currentPageSize}
|
||||||
|
onPageSizeChange={setCurrentPageSize}
|
||||||
|
onRowClick={(row: product) => {
|
||||||
|
setEditingUser(row);
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
enableCheckbox={true}
|
||||||
|
isTreeTable={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
등록된 제품이 없습니다.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product 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>
|
||||||
|
<ProductForm
|
||||||
|
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 ProductPage;
|
@ -44,7 +44,7 @@ export default function Home() {
|
|||||||
features: ["클린룸 에너지 관리", "유틸리티 최적화", "품질 연계 분석"],
|
features: ["클린룸 에너지 관리", "유틸리티 최적화", "품질 연계 분석"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
6ㅛ7ㅅ6 -
|
||||||
// 주요 기능 데이터
|
// 주요 기능 데이터
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
|
24
plm-app/src/types/common/product/product.ts
Normal file
24
plm-app/src/types/common/product/product.ts
Normal file
@ -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[];
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user