diff --git a/plm-api/src/services/oemmng.service.js b/plm-api/src/services/oemmng.service.js new file mode 100644 index 0000000..e6238d5 --- /dev/null +++ b/plm-api/src/services/oemmng.service.js @@ -0,0 +1,190 @@ +// src/services/oemmng.service.js +const { + User, + Company, + Branch, + Department, + UserRole, + Role, +} = require("../models"); +const { Op } = require("sequelize"); +const alertService = require("./alert.service"); + +class OemMngService { + async findAll(currentUser, page = 1, limit = 10) { + try { + // Initialize the where clause + let where = { + role: { [Op.ne]: "super_admin" }, // Exclude super_admin users + }; + + // company_admin은 자신의 회사 유저만 조회 가능 + if (currentUser.role === "company_admin") { + where.companyId = currentUser.companyId; + } + + const offset = (page - 1) * limit; + + // 전체 개수 조회 + const count = await User.count({ where }); + + const users = await User.findAll({ + where, + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + { + model: Branch, + attributes: ["id", "name"], + }, + { + model: Department, + attributes: ["id", "name"], + }, + { + model: Role, + through: { attributes: [] }, + attributes: ["id", "name", "description"], + }, + ], + order: [["createdAt", "DESC"]], + offset, + limit, + distinct: true, + }); + + // 인덱스 추가 + const usersWithIndex = users.map((user, index) => { + const userJson = user.toJSON(); + userJson.index = offset + index + 1; + return userJson; + }); + + return { + total: count, + totalPages: Math.ceil(count / limit), + currentPage: page, + users: usersWithIndex, + }; + } catch (error) { + console.error("Error in findAll:", error); + throw error; + } + } + + async findById(id) { + return await User.findByPk(id, { + include: [ + { + model: Company, + attributes: ["id", "name"], + }, + { + model: Branch, + attributes: ["id", "name"], + }, + { + model: Department, + attributes: ["id", "name"], + }, + ], + }); + } + + async createUser(userData, currentUser) { + const { roleId, ...userFields } = userData; + + const user = await User.create(userFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: user.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await UserRole.create({ + userId: user.id, + roleId: role.id, + }); + } + + await alertService.createAlert({ + type: "info", + message: `유저 ${user.name}이(가) ${currentUser.name}에 의해 생성되었습니다.`, + companyId: user.companyId, + }); + + return user; + } + + async updateUser(id, updateData, currentUser) { + const { roleId, ...userFields } = updateData; + + const user = await User.findByPk(id); + if (!user) throw new Error("User 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 updatedUser = await user.update(userFields); + + if (roleId) { + const role = await Role.findOne({ + where: { + id: roleId, + companyId: user.companyId, + }, + }); + + if (!role) { + throw new Error("Role not found or does not belong to the company"); + } + + await UserRole.upsert({ + userId: user.id, + roleId: role.id, + }); + } + + await alertService.createAlert({ + type: "info", + message: `유저 ${user.name}이(가) ${currentUser.name}에 의해 수정되었습니다.`, + companyId: user.companyId, + }); + + return updatedUser; + } + + async deleteUser(id, currentUser) { + const user = await User.findByPk(id); + if (!user) throw new Error("User not found"); + + const userName = user.name; + const companyId = user.companyId; + + await user.destroy(); + + await alertService.createAlert({ + type: "info", + message: `유저 ${userName}이(가) ${currentUser.name}에 의해 삭제되었습니다.`, + companyId: companyId, + }); + + return true; + } +} + +module.exports = new UserService(); diff --git a/plm-app/src/app/(admin)/common/oemmng/components/OemMngForm.tsx b/plm-app/src/app/(admin)/common/oemmng/components/OemMngForm.tsx new file mode 100644 index 0000000..1bf3365 --- /dev/null +++ b/plm-app/src/app/(admin)/common/oemmng/components/OemMngForm.tsx @@ -0,0 +1,166 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectTrigger, + SelectContent, + SelectItem, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/lib/api"; +import { useQuery } from "@tanstack/react-query"; +import { oemMng, Role } from "@/types/common/oemmng/oemmng"; +import { useAuthStore } from "@/stores/auth"; +import { AxiosError } from "axios"; +import { useToast } from "@/hooks/use-toast"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; + +import { createValidationSchema } from "@/lib/utils"; // createValidationSchema 함수 임포트 + +const fields = [ + { name: 'oem_code', required: true, type: 'string' }, + { name: 'oem_name', required: true, type: 'string' }, + { name: 'oem_no', required: true, type: 'string' }, + { name: 'isActive', required: false, type: 'boolean' }, +]; + +const formSchema = createValidationSchema(fields); + +type FormSchema = z.infer; + +interface OemFormProps { + initialData?: Partial; + onSubmit: (data: Partial) => void; + onCancel: () => void; +} + +export const OemMngForm: React.FC = ({ + initialData, + onSubmit, + onCancel, +}) => { + const { token, user } = useAuthStore(); + const { toast } = useToast(); + + // Initialize the form with react-hook-form and zod resolver + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + oem_code: initialData?.oem_code || "", + oem_name: initialData?.oem_name || "", + oem_no: initialData?.oem_no || "", + isActive: initialData?.isActive || false, + }, + }); + + // Reset form when initialData changes + React.useEffect(() => { + if (initialData) { + form.reset({ + oem_code: initialData.oem_code || "", + oem_name: initialData.oem_name || "", + oem_no: initialData.oem_no || "", + isActive: initialData.isActive || false, + }); + } + }, [initialData, form]); + + const handleSubmit = async (data: FormSchema) => { + try { + await onSubmit(data); + } catch (error) { + const err = error as AxiosError; + toast({ + title: "에러", + description: + (err.response?.data as { message: string })?.message || + "에러가 발생했습니다.", + variant: "destructive", + }); + } + }; + + return ( +
+ + ( + + OEM_CODE + + + + + + )} + /> + + ( + + OEM_NAME + + + + + + )} + /> + + ( + + 고객사번호 + + + + + + )} + /> + + ( + + + + + 활성화 여부 + + + )} + /> + +
+ + +
+ + + ); +}; \ No newline at end of file diff --git a/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx b/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx deleted file mode 100644 index e07aff1..0000000 --- a/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import React from "react"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import * as z from "zod"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -// import { Label } from "@/components/ui/label"; -import { - Select, - SelectTrigger, - SelectContent, - SelectItem, - SelectValue, -} from "@/components/ui/select"; -import { Switch } from "@/components/ui/switch"; -import { api } from "@/lib/api"; -import { useQuery } from "@tanstack/react-query"; -import { User, Role } from "@/types/user"; -import { useAuthStore } from "@/stores/auth"; -import { AxiosError } from "axios"; -import { useToast } from "@/hooks/use-toast"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; - -// Define the form schema with Zod -const formSchema = z.object({ - username: z.string().min(3, "아이디는 3자 이상이어야 합니다"), - password: z - .string() - .optional() - .refine((val) => !val || val.length >= 8, { - message: "비밀번호는 8자 이상이어야 합니다", - }), - name: z.string().min(2, "이름은 2자 이상이어야 합니다"), - email: z.string().email("올바른 이메일 형식이 아닙니다"), - phone: z.string().regex(/^[0-9-]+$/, "올바른 전화번호 형식이 아닙니다"), - role: z.enum(["company_admin", "branch_admin", "user"], { - required_error: "역할을 선택해주세요", - }), - roleId: z.string().uuid("올바른 권한 그룹을 선택해주세요"), - isActive: z.boolean(), - branchId: z.string().uuid("올바른 지점을 선택해주세요"), - departmentId: z.string().uuid("올바른 부서를 선택해주세요"), -}); - -type FormSchema = z.infer; - -interface UserFormProps { - initialData?: Partial; - onSubmit: (data: Partial) => void; - onCancel: () => void; -} - -export const UserForm: React.FC = ({ - initialData, - onSubmit, - onCancel, -}) => { - const { token, user } = useAuthStore(); - const { toast } = useToast(); - - // Initialize the form with react-hook-form and zod resolver - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - username: initialData?.username || "", - password: initialData?.password || "", - name: initialData?.name || "", - email: initialData?.email || "", - phone: initialData?.phone || "", - role: - (initialData?.role as "company_admin" | "branch_admin" | "user") || - "user", - roleId: initialData?.Roles?.[0]?.id || "", - isActive: initialData?.isActive || false, - branchId: initialData?.branchId || "", - departmentId: initialData?.departmentId || "", - }, - }); - - // Reset form when initialData changes - React.useEffect(() => { - if (initialData) { - form.reset({ - username: initialData.username || "", - password: initialData.password || "", - name: initialData.name || "", - email: initialData.email || "", - phone: initialData.phone || "", - role: - (initialData.role as "company_admin" | "branch_admin" | "user") || - "user", - roleId: initialData.Roles?.[0]?.id || "", - isActive: initialData.isActive || false, - branchId: initialData.branchId || "", - departmentId: initialData.departmentId || "", - }); - } - }, [initialData, form]); - - // Fetch available roles - const userRoles = [ - { value: "company_admin", label: "기업 관리자" }, - { value: "branch_admin", label: "지점 관리자" }, - { value: "user", label: "일반 유저" }, - ]; - - // Fetch branches - const { data: branches } = useQuery({ - queryKey: ["branches"], - queryFn: async () => { - const { data } = await api.get<{ id: string; name: string }[]>( - "/api/v1/admin/branches" - ); - return data; - }, - enabled: !!token, - }); - - // Fetch departments - const { data: departments } = useQuery({ - queryKey: ["departments", user?.companyId], - queryFn: async () => { - const { data } = await api.get<{ id: string; name: string }[]>( - `/api/v1/admin/departments/${user?.companyId}` - ); - return data; - }, - enabled: !!token && !!user?.companyId, - }); - - // Fetch roles - const { data: roles } = useQuery({ - queryKey: ["roles", user?.companyId], - queryFn: async () => { - const { data } = await api.get( - `/api/v1/admin/roles/${user?.companyId}` - ); - return data; - }, - enabled: !!token && !!user?.companyId, - }); - - const handleSubmit = async (data: FormSchema) => { - try { - await onSubmit(data); - } catch (error) { - const err = error as AxiosError; - toast({ - title: "에러", - description: - (err.response?.data as { message: string })?.message || - "에러가 발생했습니다.", - variant: "destructive", - }); - } - }; - - return ( -
- - ( - - 아이디 - - - - - - )} - /> - - {!initialData && ( - ( - - 비밀번호 - - - - - - )} - /> - )} - - ( - - 이름 - - - - - - )} - /> - - ( - - 이메일 - - - - - - )} - /> - - ( - - 전화번호 - - - - - - )} - /> - - ( - - 역할 - - - - )} - /> - - ( - - 권한 그룹 - - - - )} - /> - - ( - - - - - 활성화 여부 - - - )} - /> - - ( - - 지점 - - - - )} - /> - - ( - - 부서 - - - - )} - /> - -
- - -
- - - ); -}; diff --git a/plm-app/src/app/(admin)/common/oemmng/page.tsx b/plm-app/src/app/(admin)/common/oemmng/page.tsx index b569e4e..109f1ea 100644 --- a/plm-app/src/app/(admin)/common/oemmng/page.tsx +++ b/plm-app/src/app/(admin)/common/oemmng/page.tsx @@ -19,9 +19,9 @@ import { Plus, Edit, Trash2 } from "lucide-react"; import { ColumnDef } from "@tanstack/react-table"; import { useAuthStore } from "@/stores/auth"; import { AxiosError } from "axios"; -import { UserForm } from "./components/UserForm"; +import { OemMngForm } from "./components/OemMngForm"; import { Switch } from "@/components/ui/switch"; -import { User, PaginatedResponse } from "@/types/user"; +import { oemMng, PaginatedResponse } from "@/types/common/oemmng/oemmng"; const AccountsPage = () => { const { token, user } = useAuthStore(); @@ -159,46 +159,6 @@ const AccountsPage = () => { header: "업체/고객사코드드", cell: ({ row }) => row.original.Department?.name || "-", }, - { - accessorKey: "email", - header: "이메일", - }, - // { - // accessorKey: "phone", - // header: "전화번호", - // }, - { - accessorKey: "role", - header: "역할", - cell: ({ row }) => { - const role = row.original.role; - const roleMap: Record = { - super_admin: "슈퍼 관리자", - company_admin: "기업 관리자", - branch_admin: "지점 관리자", - user: "일반 고객사", - }; - return roleMap[role] || role; - }, - }, - { - accessorKey: "roles", - header: "권한 그룹", - cell: ({ row }) => { - const roles = row.original.Roles; - return (roles ?? []).map((role) => role.name).join(", "); - }, - }, - // { - // accessorKey: "Company.name", - // header: "회사", - // cell: ({ row }) => row.original.Company?.name || "-", - // }, - { - accessorKey: "Branch.name", - header: "지점", - cell: ({ row }) => row.original.Branch?.name || "-", - }, { accessorKey: "isActive", header: "활성화", @@ -309,7 +269,7 @@ const AccountsPage = () => { : "새로운 고객사를 생성합니다."} - { if (editingUser) { diff --git a/plm-app/src/lib/utils.ts b/plm-app/src/lib/utils.ts index 2768091..aa28ff4 100644 --- a/plm-app/src/lib/utils.ts +++ b/plm-app/src/lib/utils.ts @@ -1,5 +1,6 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { z, ZodTypeAny } from 'zod'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -8,3 +9,37 @@ export function cn(...inputs: ClassValue[]) { export function formatNumber(num: number): string { return new Intl.NumberFormat("ko-KR").format(num); } + +interface Field { + name: string; + required: boolean; + type: 'string' | 'number' | 'email' | 'phone' | 'boolean'; +} + +export const createValidationSchema = (fields: Field[]) => { + const schema: Record = {}; + + fields.forEach((field) => { + switch (field.type) { + case 'string': + schema[field.name] = z.string().min(field.required ? 1 : 0, `${field.name}은 필수입니다.`); + break; + case 'number': + schema[field.name] = z.number().min(field.required ? 1 : 0, `${field.name}은 필수입니다.`); + break; + case 'email': + schema[field.name] = z.string().email(`${field.name}은 유효한 이메일 형식이어야 합니다.`); + break; + case 'phone': + schema[field.name] = z.string().regex(/^[0-9-]+$/, `${field.name}은 올바른 전화번호 형식이어야 합니다.`); + break; + case 'boolean': + schema[field.name] = z.boolean(); + break; + default: + throw new Error(`Unsupported field type: ${field.type}`); + } + }); + + return z.object(schema); +}; \ No newline at end of file diff --git a/plm-app/src/types/common/oemmng/oemmng.ts b/plm-app/src/types/common/oemmng/oemmng.ts index bcb0767..4eabd8b 100644 --- a/plm-app/src/types/common/oemmng/oemmng.ts +++ b/plm-app/src/types/common/oemmng/oemmng.ts @@ -1,4 +1,4 @@ -// src/types/menu.ts + export interface oemMng { id: string; @@ -13,4 +13,11 @@ export interface oemMng { export interface oemMngResponse { success: boolean; data: oemMng[]; +} + +export interface PaginatedResponse { + total: number; + totalPages: number; + currentPage: number; + oemMngs: oemMng[]; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..fb57ccd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +