차종관리 커밋 함함
This commit is contained in:
parent
fbda37d9c2
commit
74f6c9ba83
114
plm-api/src/controllers/app/common/carmng.controller.js
Normal file
114
plm-api/src/controllers/app/common/carmng.controller.js
Normal file
@ -0,0 +1,114 @@
|
||||
// src/controllers/admin/users/users.controller.js
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const carMngService = require("../../../services/carmng.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("/carmngList", async (req, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
|
||||
const result = await carMngService.findAll(req.user, page, limit);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Create carMng
|
||||
router.post(
|
||||
"/carmngCreate",
|
||||
[
|
||||
body("car_code").notEmpty().withMessage("차량 코드가 필요합니다"),
|
||||
body("oem_id").notEmpty().withMessage("고객사가 필요합니다"),
|
||||
body("companyId").isUUID().withMessage("유효한 회사 ID가 필요합니다"),
|
||||
validate,
|
||||
],
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const carMng = await carMngService.createCarMng(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 carMngService.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 carMngService.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 carMngService.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 carMngService.deleteCarMng(req.params.id, req.user);
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = router;
|
96
plm-api/src/models/CarMng.js
Normal file
96
plm-api/src/models/CarMng.js
Normal file
@ -0,0 +1,96 @@
|
||||
// models/CarMng.js
|
||||
|
||||
const { Model, DataTypes } = require("sequelize");
|
||||
|
||||
class CarMng extends Model {
|
||||
static init(sequelize) {
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
// 고객사 ID
|
||||
oem_id: {
|
||||
type: DataTypes.UUID,
|
||||
comment: "고객사 ID",
|
||||
},
|
||||
// 차종 코드
|
||||
car_code:{
|
||||
type: DataTypes.STRING(128),
|
||||
allowNull: true,
|
||||
comment: "차종코드",
|
||||
},
|
||||
model_name: {
|
||||
type: DataTypes.STRING(128),
|
||||
allowNull: true,
|
||||
comment: "모델명명",
|
||||
},
|
||||
model_code: {
|
||||
type: DataTypes.STRING(128),
|
||||
allowNull: true,
|
||||
comment: "모델코드",
|
||||
},
|
||||
// 차량gride ID
|
||||
grade_id: {
|
||||
type: DataTypes.UUID,
|
||||
comment: "차량gride",
|
||||
},
|
||||
car_desc: {
|
||||
type: DataTypes.STRING(128),
|
||||
allowNull: true,
|
||||
comment: "제품 설명",
|
||||
},
|
||||
|
||||
writer: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: true,
|
||||
comment: "작성자",
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
},
|
||||
// 새로 추가할 필드들
|
||||
companyId: {
|
||||
type: DataTypes.UUID,
|
||||
comment: "회사 ID",
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'CarMng',
|
||||
tableName: 'car_mng1',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
fields: ['id'],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
static associate(models) {
|
||||
// car가 Company에 속함
|
||||
this.belongsTo(models.Company, { foreignKey: "companyId" });
|
||||
|
||||
// car가 OEM에 속함
|
||||
this.belongsTo(models.OemMng, {
|
||||
foreignKey: "oem_id",
|
||||
as: "oem" // alias 설정
|
||||
});
|
||||
|
||||
// car_code와 CommCode 연결
|
||||
this.belongsTo(models.CommCode, {
|
||||
foreignKey: "grade_id",
|
||||
targetKey: "id", // CommCode의 id 필드와 매핑
|
||||
as: "code" // alias 설정
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = CarMng;
|
@ -59,6 +59,12 @@ class OemMng extends Model {
|
||||
static associate(models) {
|
||||
// OemMng이 Company에 속함
|
||||
this.belongsTo(models.Company, { foreignKey: "companyId" });
|
||||
|
||||
// OemMng이 여러 CarMng를 가질 수 있음
|
||||
this.hasMany(models.CarMng, {
|
||||
foreignKey: "oem_id",
|
||||
as: "cars" // alias 설정
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ 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");
|
||||
const carMngController = require("../controllers/app/common/carmng.controller");
|
||||
|
||||
router.use("/health", healthController);
|
||||
router.use("/auth", authController);
|
||||
@ -39,5 +40,6 @@ router.use("/common", commonController);
|
||||
router.use("/oemmng", oemMngController);
|
||||
router.use("/productgroup", productgroupController);
|
||||
router.use("/product", productController);
|
||||
router.use("/carmng", carMngController);
|
||||
|
||||
module.exports = router;
|
||||
|
175
plm-api/src/services/carmng.service.js
Normal file
175
plm-api/src/services/carmng.service.js
Normal file
@ -0,0 +1,175 @@
|
||||
const {
|
||||
CarMng,
|
||||
Company,
|
||||
Role,
|
||||
OemMng,
|
||||
CommCode,
|
||||
} = require("../models");
|
||||
//const { Op } = require("sequelize");
|
||||
const alertService = require("./alert.service");
|
||||
|
||||
class CarMngService {
|
||||
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 CarMng.count({ where });
|
||||
|
||||
const carMngs = await CarMng.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Company,
|
||||
attributes: ["id", "name"],
|
||||
},
|
||||
{
|
||||
model: OemMng,
|
||||
as: "oem",
|
||||
attributes: ["oem_name"],
|
||||
},
|
||||
{
|
||||
model: CommCode,
|
||||
as: "code",
|
||||
attributes: ["code_name"],
|
||||
},
|
||||
],
|
||||
order: [["createdAt", "DESC"]],
|
||||
offset,
|
||||
limit,
|
||||
distinct: true,
|
||||
});
|
||||
|
||||
// 인덱스 추가
|
||||
const carMngsWithIndex = carMngs.map((car, index) => {
|
||||
const carJson = car.toJSON();
|
||||
carJson.index = offset + index + 1;
|
||||
return carJson;
|
||||
});
|
||||
|
||||
return {
|
||||
total: count,
|
||||
totalPages: Math.ceil(count / limit),
|
||||
currentPage: page,
|
||||
carMngs: carMngsWithIndex,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error in findAll:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return await CarMng.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Company,
|
||||
attributes: ["id", "name"],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async createCarMng(carMngData, currentUser) {
|
||||
const { roleId, ...carMngFields } = carMngData;
|
||||
|
||||
// 등록자 정보 추가
|
||||
carMngFields.writer = currentUser.name;
|
||||
carMngFields.regdate = new Date();
|
||||
|
||||
const carMng = await CarMng.create(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 carMng.addRole(role);
|
||||
}
|
||||
|
||||
await alertService.createAlert({
|
||||
type: "info",
|
||||
message: `고객사 ${carMng.car_code}이(가) ${currentUser.name}에 의해 생성되었습니다.`,
|
||||
companyId: carMng.companyId,
|
||||
});
|
||||
|
||||
return carMng;
|
||||
}
|
||||
|
||||
async updateCarMng(id, updateData, currentUser) {
|
||||
const { roleId, ...carMngFields } = updateData;
|
||||
|
||||
const carMng = await CarMng.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 carMng.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 carMng.addRole(role);
|
||||
}
|
||||
|
||||
await alertService.createAlert({
|
||||
type: "info",
|
||||
message: `고객사 ${carMng.name}이(가) ${currentUser.name}에 의해 수정되었습니다.`,
|
||||
companyId: carMng.companyId,
|
||||
});
|
||||
|
||||
return updatedCarMng;
|
||||
}
|
||||
|
||||
async deleteCarMng(id, currentUser) {
|
||||
const carMng = await CarMng.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: `고객사 ${carMngName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`,
|
||||
companyId: companyId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CarMngService();
|
275
plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx
Normal file
275
plm-app/src/app/(admin)/common/carmng/components/CarMngForm.tsx
Normal file
@ -0,0 +1,275 @@
|
||||
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 { carMng} from "@/types/common/carmng/carmng";
|
||||
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<carMng>;
|
||||
onSubmit: (data: Partial<carMng>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const CarMngForm: React.FC<CarFormProps> = ({
|
||||
initialData,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
car_code: initialData?.car_code || "",
|
||||
oem_id: initialData?.oem_id || "",
|
||||
model_name: initialData?.model_name || "",
|
||||
model_code: initialData?.model_code || "",
|
||||
grade_id: initialData?.grade_id || "",
|
||||
car_desc: initialData?.car_desc || "",
|
||||
isActive: initialData?.isActive || false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const [productGroups, setProductGroups] = useState([]);
|
||||
const [codeOptions, setCodeOptions] = 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 fetchCodeSelectOptions = async (parentCodeName) => {
|
||||
try {
|
||||
const response = await api.get("/api/v1/app/common/code-select-options", {
|
||||
params: { parentCodeName },
|
||||
});
|
||||
console.log("Fetched code select options:", response.data.data); // 디버깅 로그 추가
|
||||
setCodeOptions(response.data.data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch code select options:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchSelectOptions("OemMng", "oem_name");
|
||||
fetchCodeSelectOptions("GRADE"); // parentCodeName을 실제 부모 코드명으로 대체
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 p-2 text-center">
|
||||
<FormLabel>고객사</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="oem_id"
|
||||
validation={{
|
||||
required: "고객사는는 필수 입력값입니다",
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="고객사를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{productGroups.map((group) => (
|
||||
<SelectItem key={group.id} value={group.id}>
|
||||
{group.oem_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-36 p-2 text-center">
|
||||
<FormLabel>차종/PROJ코드</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="car_code"
|
||||
validation={{
|
||||
required: "차종 코드는 필수 입력값입니다",
|
||||
maxLength: { value: 20, message: "최대 20자까지 입력 가능합니다" }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 p-2 text-center">
|
||||
<FormLabel>모델명</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model_name"
|
||||
validation={{
|
||||
required: "모델명은 필수 입력값입니다",
|
||||
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-36 p-2 text-center">
|
||||
<FormLabel>모델코드</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="model_code"
|
||||
validation={{
|
||||
required: "모델코드는 필수 입력값입니다",
|
||||
maxLength: { value: 64, message: "최대 64자까지 입력 가능합니다" }
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 p-2 text-center">
|
||||
<FormLabel>Grade</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="grade_id"
|
||||
validation={{
|
||||
required: "코드 옵션은 필수 입력값입니다",
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Grade를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{codeOptions.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.code_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-36 p-2 text-center">
|
||||
<FormLabel>활성화</FormLabel>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="car_desc"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-24 p-2 text-center">
|
||||
<FormLabel>설명</FormLabel>
|
||||
</div>
|
||||
<FormControl className="flex-1">
|
||||
<Input {...field} />
|
||||
</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>
|
||||
);
|
||||
};
|
341
plm-app/src/app/(admin)/common/carmng/page.tsx
Normal file
341
plm-app/src/app/(admin)/common/carmng/page.tsx
Normal file
@ -0,0 +1,341 @@
|
||||
"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 { CarMngForm } from "./components/CarMngForm";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { carMng, PaginatedResponse } from "@/types/common/carmng/carmng";
|
||||
import { PLMTable } from "@/components/common/Table";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const CarMngPage = () => {
|
||||
const { token, user } = useAuthStore();
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [editingUser, setEditingUser] = React.useState<carMng | 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 CARs with pagination
|
||||
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||
queryKey: ["carMngs", page, pageSize],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get("/api/v1/app/carmng/carmngList", {
|
||||
params: {
|
||||
page,
|
||||
limit: pageSize,
|
||||
},
|
||||
});
|
||||
return {
|
||||
...data,
|
||||
carMngs: data.carMngs.map((carMngs: carMng) => ({
|
||||
...carMngs,
|
||||
})),
|
||||
};
|
||||
},
|
||||
enabled: !!token,
|
||||
});
|
||||
|
||||
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setPage(1);
|
||||
}, []);
|
||||
|
||||
// 필터링된 데이터 생성
|
||||
const filteredCarMngs = useMemo(() => {
|
||||
if (!data?.carMngs || !searchQuery) return data?.carMngs;
|
||||
|
||||
return data.carMngs.filter(car =>
|
||||
car.car_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
car.oem_id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [data?.carMngs, searchQuery]);
|
||||
|
||||
// Create user mutation
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (newCar: Partial<carMng>) => {
|
||||
// Include companyId in the user data
|
||||
const carWithCompanyId = {
|
||||
...newCar,
|
||||
companyId: user?.companyId,
|
||||
};
|
||||
|
||||
const { data } = await api.post<carMng>(
|
||||
"/api/v1/app/carmng/carmngCreate",
|
||||
carWithCompanyId
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["carMngs"] });
|
||||
setIsOpen(false);
|
||||
toast({
|
||||
title: "차종 생성",
|
||||
description: "새로운 차종이 생성되었습니다.",
|
||||
});
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
toast({
|
||||
title: "차종 생성 실패",
|
||||
description: (error.response?.data as { message: string }).message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Update user mutation
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (carData: Partial<carMng>) => {
|
||||
const { data } = await api.put<carMng>(
|
||||
`/api/v1/app/carmng/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/carmng/carmngDelete/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["carMngs"] });
|
||||
toast({
|
||||
title: "차종 삭제",
|
||||
description: "차종이 삭제되었습니다.",
|
||||
});
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
toast({
|
||||
title: "차종 삭제 실패",
|
||||
description: (error.response?.data as { message: string }).message,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Table columns 수정
|
||||
const columns: ColumnDef<carMng>[] = [
|
||||
{
|
||||
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: "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 }) => (
|
||||
<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">
|
||||
차종/PJT를 관리합니다.
|
||||
</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?.carMngs && data.carMngs.length > 0 ? (
|
||||
<>
|
||||
<PLMTable
|
||||
columns={columns}
|
||||
data={filteredCarMngs || []} // 필터링된 데이터 사용
|
||||
pageSize={currentPageSize}
|
||||
onPageSizeChange={setCurrentPageSize}
|
||||
onRowClick={(row: carMng) => {
|
||||
setEditingUser(row);
|
||||
setIsOpen(true);
|
||||
}}
|
||||
enableCheckbox={true}
|
||||
isTreeTable={false}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
등록된 차종이 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User 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>
|
||||
<CarMngForm
|
||||
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 CarMngPage;
|
@ -290,7 +290,7 @@ const CommonCodePage = () => {
|
||||
const saveCodeMutation = useMutation({
|
||||
mutationFn: async (codeData: CodeFormData) => {
|
||||
console.log('Saving code data:', codeData);
|
||||
const response = await api.post("/api/v1/app/common/codes", codeData); // URL 경로 변경
|
||||
const response = await api.post("/api/v1/app/common/code", codeData); // URL 경로 변경
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
@ -510,7 +510,7 @@ const CommonCodePage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-4">
|
||||
<div className="w-full h-full">
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
|
@ -602,7 +602,7 @@ const MenusPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-4">
|
||||
<div className="w-full h-full">
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
|
26
plm-app/src/types/common/carmng/carmng.ts
Normal file
26
plm-app/src/types/common/carmng/carmng.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export interface carMng {
|
||||
id: string;
|
||||
index: number;
|
||||
oem_id?: string | null;
|
||||
car_code?: string | null;
|
||||
model_name?: string | null;
|
||||
model_code?: string | null;
|
||||
grade_id?: string | null;
|
||||
car_desc?: string | null;
|
||||
writer?: string | null;
|
||||
regdate?: Date | null;
|
||||
isActive: boolean;
|
||||
companyId: string;
|
||||
}
|
||||
|
||||
export interface carMngResponse {
|
||||
success: boolean;
|
||||
data: carMng[];
|
||||
}
|
||||
|
||||
export interface PaginatedResponse {
|
||||
total: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
carMngs: carMng[];
|
||||
}
|
Loading…
Reference in New Issue
Block a user