거지같아아

This commit is contained in:
chpark 2024-12-20 18:25:22 +09:00
parent 68101a1811
commit 241c1f94e4
7 changed files with 406 additions and 413 deletions

View File

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

View File

@ -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<typeof formSchema>;
interface OemFormProps {
initialData?: Partial<oemMng>;
onSubmit: (data: Partial<User>) => void;
onCancel: () => void;
}
export const OemMngForm: React.FC<OemFormProps> = ({
initialData,
onSubmit,
onCancel,
}) => {
const { token, user } = useAuthStore();
const { toast } = useToast();
// Initialize the form with react-hook-form and zod resolver
const form = useForm<FormSchema>({
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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="oem_code"
render={({ field }) => (
<FormItem>
<FormLabel>OEM_CODE</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="oem_name"
render={({ field }) => (
<FormItem>
<FormLabel>OEM_NAME</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="oem_no"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel> </FormLabel>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit"></Button>
</div>
</form>
</Form>
);
};

View File

@ -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<typeof formSchema>;
interface UserFormProps {
initialData?: Partial<User>;
onSubmit: (data: Partial<User>) => void;
onCancel: () => void;
}
export const UserForm: React.FC<UserFormProps> = ({
initialData,
onSubmit,
onCancel,
}) => {
const { token, user } = useAuthStore();
const { toast } = useToast();
// Initialize the form with react-hook-form and zod resolver
const form = useForm<FormSchema>({
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<Role[]>({
queryKey: ["roles", user?.companyId],
queryFn: async () => {
const { data } = await api.get<Role[]>(
`/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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!initialData && (
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="password" {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input type="email" {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="role"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select
onValueChange={field.onChange}
value={field.value || "user"}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="역할을 선택하세요" />
</SelectTrigger>
</FormControl>
<SelectContent>
{userRoles.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="roleId"
render={({ field }) => (
<FormItem>
<FormLabel> </FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="권한 그룹을 선택하세요" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles?.map((role) => (
<SelectItem key={role.id} value={role.id}>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="isActive"
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel> </FormLabel>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="branchId"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="지점을 선택하세요" />
</SelectTrigger>
</FormControl>
<SelectContent>
{branches?.map((branch) => (
<SelectItem key={branch.id} value={branch.id}>
{branch.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="departmentId"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="부서를 선택하세요" />
</SelectTrigger>
</FormControl>
<SelectContent>
{departments?.map((dept) => (
<SelectItem key={dept.id} value={dept.id}>
{dept.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-2">
<Button type="button" variant="outline" onClick={onCancel}>
</Button>
<Button type="submit"></Button>
</div>
</form>
</Form>
);
};

View File

@ -19,9 +19,9 @@ import { Plus, Edit, Trash2 } from "lucide-react";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { UserForm } from "./components/UserForm"; import { OemMngForm } from "./components/OemMngForm";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { User, PaginatedResponse } from "@/types/user"; import { oemMng, PaginatedResponse } from "@/types/common/oemmng/oemmng";
const AccountsPage = () => { const AccountsPage = () => {
const { token, user } = useAuthStore(); const { token, user } = useAuthStore();
@ -159,46 +159,6 @@ const AccountsPage = () => {
header: "업체/고객사코드드", header: "업체/고객사코드드",
cell: ({ row }) => row.original.Department?.name || "-", 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<string, string> = {
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", accessorKey: "isActive",
header: "활성화", header: "활성화",
@ -309,7 +269,7 @@ const AccountsPage = () => {
: "새로운 고객사를 생성합니다."} : "새로운 고객사를 생성합니다."}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<UserForm <OemMngForm
initialData={editingUser || undefined} initialData={editingUser || undefined}
onSubmit={(data) => { onSubmit={(data) => {
if (editingUser) { if (editingUser) {

View File

@ -1,5 +1,6 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { z, ZodTypeAny } from 'zod';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -8,3 +9,37 @@ export function cn(...inputs: ClassValue[]) {
export function formatNumber(num: number): string { export function formatNumber(num: number): string {
return new Intl.NumberFormat("ko-KR").format(num); 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<string, ZodTypeAny> = {};
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);
};

View File

@ -1,4 +1,4 @@
// src/types/menu.ts
export interface oemMng { export interface oemMng {
id: string; id: string;
@ -13,4 +13,11 @@ export interface oemMng {
export interface oemMngResponse { export interface oemMngResponse {
success: boolean; success: boolean;
data: oemMng[]; data: oemMng[];
}
export interface PaginatedResponse {
total: number;
totalPages: number;
currentPage: number;
oemMngs: oemMng[];
} }

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1