From 68101a18112a9f2a6a4b343670549f62bab9a367 Mon Sep 17 00:00:00 2001
From: chpark <chpark@gdnsi.com>
Date: Thu, 19 Dec 2024 18:33:03 +0900
Subject: [PATCH] =?UTF-8?q?=EA=B3=A0=EA=B0=9D=EC=82=AC=20=ED=95=98?=
 =?UTF-8?q?=EB=8A=94=EC=A4=91=EC=A4=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../common/oemmng/components/UserForm.tsx     | 369 ++++++++++++++++++
 .../src/app/(admin)/common/oemmng/page.tsx    | 332 ++++++++++++++++
 plm-app/src/types/common/oemmng/oemmng.ts     |  16 +
 3 files changed, 717 insertions(+)
 create mode 100644 plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx
 create mode 100644 plm-app/src/app/(admin)/common/oemmng/page.tsx
 create mode 100644 plm-app/src/types/common/oemmng/oemmng.ts

diff --git a/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx b/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx
new file mode 100644
index 0000000..e07aff1
--- /dev/null
+++ b/plm-app/src/app/(admin)/common/oemmng/components/UserForm.tsx
@@ -0,0 +1,369 @@
+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>
+  );
+};
diff --git a/plm-app/src/app/(admin)/common/oemmng/page.tsx b/plm-app/src/app/(admin)/common/oemmng/page.tsx
new file mode 100644
index 0000000..b569e4e
--- /dev/null
+++ b/plm-app/src/app/(admin)/common/oemmng/page.tsx
@@ -0,0 +1,332 @@
+// src/app/(admin)/users/accounts/page.tsx
+"use client";
+
+import React, { useState, useCallback } 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 } from "lucide-react";
+import { ColumnDef } from "@tanstack/react-table";
+import { useAuthStore } from "@/stores/auth";
+import { AxiosError } from "axios";
+import { UserForm } from "./components/UserForm";
+import { Switch } from "@/components/ui/switch";
+import { User, PaginatedResponse } from "@/types/user";
+
+const AccountsPage = () => {
+  const { token, user } = useAuthStore();
+  const [isOpen, setIsOpen] = React.useState(false);
+  const [editingUser, setEditingUser] = React.useState<User | null>(null);
+  const [page, setPage] = useState(1);
+  const [pageSize, setPageSize] = useState(10);
+  const { toast } = useToast();
+  const queryClient = useQueryClient();
+
+  // Fetch users with pagination
+  const { data, isLoading } = useQuery<PaginatedResponse>({
+    queryKey: ["users", page, pageSize],
+    queryFn: async () => {
+      const { data } = await api.get("/api/v1/admin/users", {
+        params: {
+          page,
+          limit: pageSize,
+        },
+      });
+      return {
+        ...data,
+        users: data.users.map((user: User) => ({
+          ...user,
+          Roles: user.Roles || [],
+        })),
+      };
+    },
+    enabled: !!token,
+  });
+
+  const handlePageSizeChange = useCallback((newPageSize: number) => {
+    setPageSize(newPageSize);
+    setPage(1);
+  }, []);
+
+  // Create user mutation
+  const createMutation = useMutation({
+    mutationFn: async (newUser: Partial<User>) => {
+      // Include companyId in the user data
+      const userWithCompanyId = {
+        ...newUser,
+        companyId: user?.companyId,
+      };
+
+      const { data } = await api.post<User>(
+        "/api/v1/admin/users",
+        userWithCompanyId
+      );
+      return data;
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["users"] });
+      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 (userData: Partial<User>) => {
+      const { data } = await api.put<User>(
+        `/api/v1/admin/users/${userData.id}`,
+        userData
+      );
+      return data;
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["users"] });
+      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/admin/users/${id}`);
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ["users"] });
+      toast({
+        title: "고객사 삭제",
+        description: "고객사가 삭제되었습니다.",
+      });
+    },
+    onError: (error: AxiosError) => {
+      toast({
+        title: "고객사 삭제 실패",
+        description: (error.response?.data as { message: string }).message,
+        variant: "destructive",
+      });
+    },
+  });
+
+  // Table columns
+  const columns: ColumnDef<User>[] = [
+    {
+      id: "index",
+      header: "No",
+      cell: ({ row }) => row.original.index,
+      size: 60,
+    },
+    // {
+    //   accessorKey: "username",
+    //   header: "아이디",
+    // },
+    {
+      accessorKey: "name",
+      header: "업체명/고객사사",
+    },
+    {
+      accessorKey: "Department.name",
+      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<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",
+      header: "활성화",
+      cell: ({ row }) => (
+        <Switch
+          checked={row.original.isActive}
+          onCheckedChange={(value) => {
+            updateMutation.mutate({ id: row.original.id, isActive: value });
+          }}
+        />
+      ),
+    },
+    {
+      id: "actions",
+      header: "액션",
+      cell: ({ row }) => (
+        <div className="flex items-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="container mx-auto py-6">
+      {/* Header */}
+      <div className="flex justify-between items-center mb-6">
+        <div className="space-y-1">
+          <h1 className="text-3xl font-bold">고객사 관리</h1>
+          <p className="text-muted-foreground">
+            고객사를 관리하고 권한을 설정합니다.
+          </p>
+        </div>
+        <Button onClick={() => setIsOpen(true)}>
+          <Plus className="mr-2 h-4 w-4" />
+          고객사 추가
+        </Button>
+      </div>
+
+      {/* Users Table */}
+      <Card>
+        <CardHeader>
+          <CardTitle>고객사 목록</CardTitle>
+        </CardHeader>
+        <CardContent>
+          {data?.users && data.users.length > 0 ? (
+            <>
+              <DataTable
+                columns={columns}
+                data={data.users}
+                pagination={{
+                  pageIndex: page - 1,
+                  pageSize,
+                  pageCount: data.totalPages,
+                  rowCount: data.total,
+                  onPageChange: (newPage) => setPage(newPage + 1),
+                  onPageSizeChange: handlePageSizeChange,
+                }}
+              />
+              <div className="text-sm text-muted-foreground mt-2">
+                총 {data.total}명의 고객사
+              </div>
+            </>
+          ) : (
+            <div className="text-center py-12 text-muted-foreground">
+              등록된 고객사가 없습니다.
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 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>
+          <UserForm
+            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 AccountsPage;
diff --git a/plm-app/src/types/common/oemmng/oemmng.ts b/plm-app/src/types/common/oemmng/oemmng.ts
new file mode 100644
index 0000000..bcb0767
--- /dev/null
+++ b/plm-app/src/types/common/oemmng/oemmng.ts
@@ -0,0 +1,16 @@
+// src/types/menu.ts
+
+export interface oemMng {
+  id: string;
+  oem_code?: string | null;
+  oem_name?: string | null;
+  writer?: string | null;
+  regdate?: Date | null;
+  status?: string | null;
+  oem_no?: string | null;
+}
+
+export interface oemMngResponse {
+  success: boolean;
+  data: oemMng[];
+}
\ No newline at end of file