summaryrefslogtreecommitdiff
path: root/lib/pq/table
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:39:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:39:21 +0000
commit53ad72732f781e6c6d5ddb3776ea47aec010af8e (patch)
treee676287827f8634be767a674b8ad08b6ed7eb3e6 /lib/pq/table
parent3e4d15271322397764601dee09441af8a5b3adf5 (diff)
(최겸) PQ/실사 수정 및 개발
Diffstat (limited to 'lib/pq/table')
-rw-r--r--lib/pq/table/add-pq-dialog.tsx454
-rw-r--r--lib/pq/table/add-pq-list-dialog.tsx231
-rw-r--r--lib/pq/table/copy-pq-list-dialog.tsx244
-rw-r--r--lib/pq/table/delete-pq-list-dialog.tsx (renamed from lib/pq/table/delete-pqs-dialog.tsx)60
-rw-r--r--lib/pq/table/import-pq-button.tsx270
-rw-r--r--lib/pq/table/import-pq-handler.tsx145
-rw-r--r--lib/pq/table/pq-excel-template.tsx205
-rw-r--r--lib/pq/table/pq-lists-columns.tsx216
-rw-r--r--lib/pq/table/pq-lists-table.tsx170
-rw-r--r--lib/pq/table/pq-lists-toolbar.tsx61
-rw-r--r--lib/pq/table/pq-table-column.tsx185
-rw-r--r--lib/pq/table/pq-table-toolbar-actions.tsx87
-rw-r--r--lib/pq/table/pq-table.tsx127
-rw-r--r--lib/pq/table/update-pq-sheet.tsx264
14 files changed, 947 insertions, 1772 deletions
diff --git a/lib/pq/table/add-pq-dialog.tsx b/lib/pq/table/add-pq-dialog.tsx
deleted file mode 100644
index 2fac9e43..00000000
--- a/lib/pq/table/add-pq-dialog.tsx
+++ /dev/null
@@ -1,454 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { Plus } from "lucide-react"
-import { useRouter } from "next/navigation"
-
-import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Checkbox } from "@/components/ui/checkbox"
-import { useToast } from "@/hooks/use-toast"
-import { createPq, invalidatePqCache } from "../service"
-import { ProjectSelector } from "@/components/ProjectSelector"
-import { type Project } from "@/lib/rfqs/service"
-import { ScrollArea } from "@/components/ui/scroll-area"
-
-// PQ 생성을 위한 Zod 스키마 정의
-const createPqSchema = z.object({
- code: z.string().min(1, "Code is required"),
- checkPoint: z.string().min(1, "Check point is required"),
- groupName: z.string().min(1, "Group is required"),
- description: z.string().optional(),
- remarks: z.string().optional(),
- // 프로젝트별 PQ 여부 체크박스
- isProjectSpecific: z.boolean().default(false),
- // 프로젝트 관련 추가 필드는 isProjectSpecific가 true일 때만 필수
- contractInfo: z.string().optional(),
- additionalRequirement: z.string().optional(),
-});
-
-type CreatePqFormType = z.infer<typeof createPqSchema>;
-
-// 그룹 이름 옵션
-export const groupOptions = [
- "GENERAL",
- "Quality Management System",
- "Workshop & Environment",
- "Warranty",
-];
-
-// 설명 예시 텍스트
-const descriptionExample = `Address :
-Tel. / Fax :
-e-mail :`;
-
-interface AddPqDialogProps {
- currentProjectId?: number | null; // 현재 선택된 프로젝트 ID (옵션)
-}
-
-export function AddPqDialog({ currentProjectId }: AddPqDialogProps) {
- const [open, setOpen] = React.useState(false)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [selectedProject, setSelectedProject] = React.useState<Project | null>(null)
- const router = useRouter()
- const { toast } = useToast()
-
- // react-hook-form 설정
- const form = useForm<CreatePqFormType>({
- resolver: zodResolver(createPqSchema),
- defaultValues: {
- code: "",
- checkPoint: "",
- groupName: groupOptions[0],
- description: "",
- remarks: "",
- isProjectSpecific: !!currentProjectId, // 현재 프로젝트 ID가 있으면 기본값 true
- contractInfo: "",
- additionalRequirement: "",
- },
- })
-
- // 프로젝트별 PQ 여부 상태 감시
- const isProjectSpecific = form.watch("isProjectSpecific")
-
- // 현재 프로젝트 ID가 있으면 선택된 프로젝트 설정
- React.useEffect(() => {
- if (currentProjectId) {
- form.setValue("isProjectSpecific", true)
- }
- }, [currentProjectId, form])
-
- // 예시 텍스트를 description 필드에 채우는 함수
- const fillExampleText = () => {
- form.setValue("description", descriptionExample);
- };
-
- async function onSubmit(data: CreatePqFormType) {
- try {
- setIsSubmitting(true)
-
- // 서버 액션 호출용 데이터 준비
- const submitData = {
- ...data,
- projectId: data.isProjectSpecific ? selectedProject?.id || currentProjectId : null,
- }
-
- // 프로젝트별 PQ인데 프로젝트가 선택되지 않은 경우 검증
- if (data.isProjectSpecific && !submitData.projectId) {
- toast({
- title: "Error",
- description: "Please select a project",
- variant: "destructive",
- })
- setIsSubmitting(false)
- return
- }
-
- // 서버 액션 호출
- const result = await createPq(submitData)
-
- if (!result.success) {
- toast({
- title: "Error",
- description: result.message || "Failed to create PQ criteria",
- variant: "destructive",
- })
- return
- }
-
- await invalidatePqCache();
-
- // 성공 시 처리
- toast({
- title: "Success",
- description: result.message || "PQ criteria created successfully",
- })
-
- // 모달 닫고 폼 리셋
- form.reset()
- setSelectedProject(null)
- setOpen(false)
-
- // 페이지 새로고침
- router.refresh()
-
- } catch (error) {
- console.error('Error creating PQ criteria:', error)
- toast({
- title: "Error",
- description: "An unexpected error occurred",
- variant: "destructive",
- })
- } finally {
- setIsSubmitting(false)
- }
- }
-
- function handleDialogOpenChange(nextOpen: boolean) {
- if (!nextOpen) {
- form.reset()
- setSelectedProject(null)
- }
- setOpen(nextOpen)
- }
-
- // 프로젝트 선택 핸들러
- const handleProjectSelect = (project: Project | null) => {
- // project가 null인 경우 선택 해제를 의미
- if (project === null) {
- setSelectedProject(null);
- // 필요한 경우 추가 처리
- return;
- }
-
- // 기존 처리 - 프로젝트가 선택된 경우
- setSelectedProject(project);
- }
-
- return (
- <Dialog open={open} onOpenChange={handleDialogOpenChange}>
- {/* 모달을 열기 위한 버튼 */}
- <DialogTrigger asChild>
- <Button variant="default" size="sm">
- <Plus className="size-4" />
- Add PQ
- </Button>
- </DialogTrigger>
-
- <DialogContent className="sm:max-w-[600px]">
- <DialogHeader>
- <DialogTitle>Create New PQ Criteria</DialogTitle>
- <DialogDescription>
- 새 PQ 기준 정보를 입력하고 <b>Create</b> 버튼을 누르세요.
- </DialogDescription>
- </DialogHeader>
-
- {/* shadcn/ui Form을 이용해 react-hook-form과 연결 */}
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-2 flex flex-col">
- {/* 프로젝트별 PQ 여부 체크박스 */}
-
- <div className="flex-1 overflow-auto px-4 space-y-4">
- <FormField
- control={form.control}
- name="isProjectSpecific"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none">
- <FormLabel>프로젝트별 PQ 생성</FormLabel>
- <FormDescription>
- 특정 프로젝트에만 적용되는 PQ 항목을 생성합니다
- </FormDescription>
- </div>
- </FormItem>
- )}
- />
-
- {/* 프로젝트 선택 필드 (프로젝트별 PQ 선택 시에만 표시) */}
- {isProjectSpecific && (
- <div className="space-y-2">
- <FormLabel>Project <span className="text-destructive">*</span></FormLabel>
- <ProjectSelector
- selectedProjectId={currentProjectId || selectedProject?.id}
- onProjectSelect={handleProjectSelect}
- placeholder="프로젝트를 선택하세요"
- />
- <FormDescription>
- PQ 항목을 적용할 프로젝트를 선택하세요
- </FormDescription>
- </div>
- )}
-
- <div className="flex-1 overflow-auto px-2 py-2 space-y-4" style={{maxHeight:420}}>
-
-
- {/* Code 필드 */}
- <FormField
- control={form.control}
- name="code"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Code <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="예: 1-1, A.2.3"
- {...field}
- />
- </FormControl>
- <FormDescription>
- PQ 항목의 고유 코드를 입력하세요 (예: "1-1", "A.2.3")
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Check Point 필드 */}
- <FormField
- control={form.control}
- name="checkPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Check Point <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="검증 항목을 입력하세요"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Group Name 필드 (Select) */}
- <FormField
- control={form.control}
- name="groupName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Group <span className="text-destructive">*</span></FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- value={field.value}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="그룹을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {groupOptions.map((group) => (
- <SelectItem key={group} value={group}>
- {group}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormDescription>
- PQ 항목의 분류 그룹을 선택하세요
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Description 필드 - 예시 템플릿 버튼 추가 */}
- <FormField
- control={form.control}
- name="description"
- render={({ field }) => (
- <FormItem>
- <div className="flex items-center justify-between">
- <FormLabel>Description</FormLabel>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={fillExampleText}
- >
- 예시 채우기
- </Button>
- </div>
- <FormControl>
- <Textarea
- placeholder={`줄바꿈을 포함한 상세 설명을 입력하세요\n예:\n${descriptionExample}`}
- className="min-h-[120px] font-mono"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormDescription>
- 줄바꿈이 필요한 경우 Enter 키를 누르세요. 입력한 대로 저장됩니다.
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Remarks 필드 */}
- <FormField
- control={form.control}
- name="remarks"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Remarks</FormLabel>
- <FormControl>
- <Textarea
- placeholder="비고 사항을 입력하세요"
- className="min-h-[80px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 프로젝트별 PQ일 경우 추가 필드 */}
- {isProjectSpecific && (
- <>
- {/* 계약 정보 필드 */}
- <FormField
- control={form.control}
- name="contractInfo"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Contract Info</FormLabel>
- <FormControl>
- <Textarea
- placeholder="계약 관련 정보를 입력하세요"
- className="min-h-[80px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormDescription>
- 해당 프로젝트의 계약 관련 특이사항
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 추가 요구사항 필드 */}
- <FormField
- control={form.control}
- name="additionalRequirement"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Additional Requirements</FormLabel>
- <FormControl>
- <Textarea
- placeholder="추가 요구사항을 입력하세요"
- className="min-h-[80px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormDescription>
- 프로젝트별 추가 요구사항
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- </>
- )}
- </div>
-
- </div>
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => {
- form.reset();
- setSelectedProject(null);
- setOpen(false);
- }}
- >
- Cancel
- </Button>
- <Button
- type="submit"
- disabled={isSubmitting}
- >
- {isSubmitting ? "Creating..." : "Create"}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/pq/table/add-pq-list-dialog.tsx b/lib/pq/table/add-pq-list-dialog.tsx
new file mode 100644
index 00000000..c1899a29
--- /dev/null
+++ b/lib/pq/table/add-pq-list-dialog.tsx
@@ -0,0 +1,231 @@
+"use client"
+
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { DatePicker } from "@/components/ui/date-picker"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+import { Loader2, Plus } from "lucide-react"
+
+// 프로젝트 목록을 위한 임시 타입 (실제로는 projects에서 가져와야 함)
+interface Project {
+ id: number
+ name: string
+ code: string
+}
+
+const pqListFormSchema = z.object({
+ name: z.string().min(1, "PQ 목록 명을 입력해주세요"),
+ type: z.enum(["GENERAL", "PROJECT", "NON_INSPECTION"], {
+ required_error: "PQ 유형을 선택해주세요"
+ }),
+ projectId: z.number().optional().nullable(),
+ validTo: z.date().optional().nullable(),
+}).refine((data) => {
+ // 프로젝트 PQ인 경우 프로젝트 선택 필수
+ if (data.type === "PROJECT" && !data.projectId) {
+ return false
+ }
+ return true
+}, {
+ message: "프로젝트 PQ인 경우 프로젝트를 선택해야 합니다",
+ path: ["projectId"]
+})
+
+type PqListFormData = z.infer<typeof pqListFormSchema>
+
+interface PqListFormProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ initialData?: Partial<PqListFormData> & { id?: number }
+ projects?: Project[]
+ onSubmit: (data: PqListFormData & { id?: number }) => Promise<void>
+ isLoading?: boolean
+}
+
+const typeLabels = {
+ GENERAL: "일반 PQ",
+ PROJECT: "프로젝트 PQ",
+ NON_INSPECTION: "미실사 PQ"
+}
+
+export function AddPqDialog({
+ open,
+ onOpenChange,
+ initialData,
+ projects = [],
+ onSubmit,
+ isLoading = false
+}: PqListFormProps) {
+ const isEditing = !!initialData?.id
+
+ const form = useForm<PqListFormData>({
+ resolver: zodResolver(pqListFormSchema),
+ defaultValues: {
+ name: initialData?.name || "",
+ type: initialData?.type || "GENERAL",
+ projectId: initialData?.projectId || null,
+ validTo: initialData?.validTo || undefined,
+ }
+ })
+
+ const selectedType = form.watch("type")
+ const formState = form.formState
+
+ const handleSubmit = async (data: PqListFormData) => {
+ try {
+ await onSubmit({
+ ...data,
+ id: initialData?.id
+ })
+ form.reset()
+ onOpenChange(false)
+ } catch (error) {
+ // 에러는 상위 컴포넌트에서 처리
+ console.error("Failed to submit PQ list:", error)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Plus className="h-5 w-5" />
+ {isEditing ? "PQ 목록 수정" : "신규 PQ 목록 생성"}
+ </DialogTitle>
+ <DialogDescription>
+ {isEditing ? "PQ 목록 정보를 수정합니다." : "새로운 PQ 목록을 생성합니다."}
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
+ {/* PQ 유형 */}
+ <FormField
+ control={form.control}
+ name="type"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ PQ 유형 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="PQ 유형을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(typeLabels).map(([value, label]) => (
+ <SelectItem key={value} value={value}>
+ {label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ {/* PQ 목록 명 */}
+ <FormField
+ control={form.control}
+ name="name"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ PQ 리스트명 <span className="text-red-500">*</span>
+ </FormLabel>
+ <FormControl>
+ <Input
+ placeholder="예: General PQ v2.0, EPC-1234 Project PQ"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 프로젝트 선택 (프로젝트 PQ인 경우만) */}
+ {selectedType === "PROJECT" && (
+ <FormField
+ control={form.control}
+ name="projectId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 프로젝트 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select
+ onValueChange={(value) => field.onChange(parseInt(value))}
+ defaultValue={field.value?.toString()}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="프로젝트를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.id} value={project.id.toString()}>
+ {project.code} - {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 유효기간 (프로젝트 PQ인 경우) */}
+ {selectedType === "PROJECT" && (
+ <FormField
+ control={form.control}
+ name="validTo"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 유효일 <span className="text-red-500">*</span>
+ </FormLabel>
+ <FormControl>
+ <DatePicker
+ date={field.value ?? undefined}
+ onSelect={(date) => field.onChange(date ?? null)}
+ placeholder="유효일 선택"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 버튼들 */}
+ <div className="flex justify-end space-x-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading || !formState.isValid}>
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ {isEditing ? "수정" : "생성"}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/pq/table/copy-pq-list-dialog.tsx b/lib/pq/table/copy-pq-list-dialog.tsx
new file mode 100644
index 00000000..647ab1a3
--- /dev/null
+++ b/lib/pq/table/copy-pq-list-dialog.tsx
@@ -0,0 +1,244 @@
+"use client"
+
+// import { useState } from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { z } from "zod"
+import { Button } from "@/components/ui/button"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
+import { DatePicker } from "@/components/ui/date-picker"
+import { Loader2, Copy } from "lucide-react"
+import { Badge } from "@/components/ui/badge"
+import { Input } from "@/components/ui/input"
+// import { Card, CardContent } from "@/components/ui/card"
+
+interface PQList {
+ id: number
+ name: string
+ type: "GENERAL" | "PROJECT" | "NON_INSPECTION"
+ projectId?: number | null
+ criteriaCount?: number
+ createdAt: Date
+}
+
+interface Project {
+ id: number
+ name: string
+ code: string
+}
+
+const copyPqSchema = z.object({
+ sourcePqListId: z.number({
+ required_error: "복사할 PQ 목록을 선택해주세요"
+ }),
+ targetProjectId: z.number({
+ required_error: "대상 프로젝트를 선택해주세요"
+ }),
+ validTo: z.date(),
+ newName: z.string(),
+})
+
+type CopyPqFormData = z.infer<typeof copyPqSchema>
+
+interface CopyPqDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ pqLists: PQList[]
+ projects: Project[]
+ onCopy: (data: CopyPqFormData) => Promise<void>
+ isLoading?: boolean
+}
+
+const typeLabels = {
+ GENERAL: "일반 PQ",
+ PROJECT: "프로젝트 PQ",
+ NON_INSPECTION: "미실사 PQ"
+}
+
+const typeColors = {
+ GENERAL: "bg-blue-100 text-blue-800",
+ PROJECT: "bg-green-100 text-green-800",
+ NON_INSPECTION: "bg-orange-100 text-orange-800"
+}
+
+export function CopyPqDialog({
+ open,
+ onOpenChange,
+ pqLists,
+ projects,
+ onCopy,
+ isLoading = false
+}: CopyPqDialogProps) {
+ const form = useForm<CopyPqFormData>({
+ resolver: zodResolver(copyPqSchema),
+ })
+ const formState = form.formState
+
+ const selectedSourceId = form.watch("sourcePqListId")
+ const selectedPqList = pqLists.find(list => list.id === selectedSourceId)
+
+ const handleSubmit = async (data: CopyPqFormData) => {
+ try {
+ await onCopy(data)
+ form.reset()
+ onOpenChange(false)
+ } catch (error) {
+ // 에러는 상위 컴포넌트에서 처리
+ console.error("Failed to copy PQ list:", error)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-2xl">
+ <DialogHeader>
+ <DialogTitle className="flex items-center gap-2">
+ <Copy className="h-5 w-5" />
+ PQ 목록 불러오기
+ </DialogTitle>
+ <DialogDescription>
+ 기존 PQ 목록을 선택하여 새로운 프로젝트 PQ를 생성합니다.
+ 선택한 PQ의 모든 항목이 새 프로젝트로 복사됩니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
+ {/* 대상 프로젝트 선택 */}
+ <FormField
+ control={form.control}
+ name="targetProjectId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 대상 프로젝트 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select
+ onValueChange={(value) => field.onChange(parseInt(value))}
+ defaultValue={field.value?.toString()}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="PQ를 적용할 프로젝트를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.id} value={project.id.toString()}>
+ {project.code} - {project.name}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ {/* 복사할 PQ 목록 선택 */}
+ <FormField
+ control={form.control}
+ name="sourcePqListId"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 복사할 PQ 리스트 <span className="text-red-500">*</span>
+ </FormLabel>
+ <Select
+ onValueChange={(value) => field.onChange(parseInt(value))}
+ defaultValue={field.value?.toString()}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="복사할 PQ 리스트를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {pqLists.map((pqList) => (
+ <SelectItem key={pqList.id} value={pqList.id.toString()}>
+ <div className="flex items-center gap-2">
+ <Badge className={typeColors[pqList.type]}>
+ {typeLabels[pqList.type]}
+ </Badge>
+ <span>{pqList.name}</span>
+ {pqList.criteriaCount && (
+ <span className="text-xs text-muted-foreground">
+ ({pqList.criteriaCount}개 항목)
+ </span>
+ )}
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ {selectedPqList && (
+ <div className="text-sm text-muted-foreground mt-1">
+ 선택된 PQ 리스트: <strong>{selectedPqList.name}</strong>
+ {selectedPqList.criteriaCount && (
+ <span> ({selectedPqList.criteriaCount}개 항목)</span>
+ )}
+ </div>
+ )}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ {/* 새 PQ 목록 명 */}
+ <FormField
+ control={form.control}
+ name="newName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 새 PQ 리스트명 <span className="text-red-500">*</span>
+ </FormLabel>
+ <FormControl>
+ <Input {...field} value={field.value ?? ""} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ {/* 유효기간 설정 */}
+ <FormField
+ control={form.control}
+ name="validTo"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center gap-1">
+ 유효기간 <span className="text-red-500">*</span>
+ </FormLabel>
+ <FormControl>
+ <DatePicker
+ date={field.value ?? undefined}
+ onSelect={(date) => field.onChange(date ?? null)}
+ placeholder="유효기간 선택"
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 버튼들 */}
+ <div className="flex justify-end space-x-2">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading || !formState.isValid}>
+ {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+ 복사하여 생성
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file
diff --git a/lib/pq/table/delete-pqs-dialog.tsx b/lib/pq/table/delete-pq-list-dialog.tsx
index c6a2ce82..c9a6eb09 100644
--- a/lib/pq/table/delete-pqs-dialog.tsx
+++ b/lib/pq/table/delete-pq-list-dialog.tsx
@@ -3,7 +3,6 @@
import * as React from "react"
import { type Row } from "@tanstack/react-table"
import { Loader, Trash } from "lucide-react"
-import { toast } from "sonner"
import { useMediaQuery } from "@/hooks/use-media-query"
import { Button } from "@/components/ui/button"
@@ -27,39 +26,28 @@ import {
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"
-import { PqCriterias } from "@/db/schema/pq"
-import { removePqs } from "../service"
+import type { PQList } from "./pq-lists-columns"
-
-interface DeleteTasksDialogProps
+interface DeletePqListsDialogProps
extends React.ComponentPropsWithoutRef<typeof Dialog> {
- pqs: Row<PqCriterias>["original"][]
+ pqLists: Row<PQList>["original"][]
showTrigger?: boolean
onSuccess?: () => void
}
-export function DeletePqsDialog({
- pqs,
+export function DeletePqListsDialog({
+ pqLists,
showTrigger = true,
onSuccess,
...props
-}: DeleteTasksDialogProps) {
+}: DeletePqListsDialogProps) {
const [isDeletePending, startDeleteTransition] = React.useTransition()
const isDesktop = useMediaQuery("(min-width: 640px)")
function onDelete() {
startDeleteTransition(async () => {
- const { error } = await removePqs({
- ids: pqs.map((pq) => pq.id),
- })
-
- if (error) {
- toast.error(error)
- return
- }
-
+ // 상위 컴포넌트에서 삭제 로직을 처리하도록 콜백 호출
props.onOpenChange?.(false)
- toast.success("Tasks deleted")
onSuccess?.()
})
}
@@ -69,24 +57,25 @@ export function DeletePqsDialog({
<Dialog {...props}>
{showTrigger ? (
<DialogTrigger asChild>
- <Button variant="outline" size="sm">
+ <Button variant="destructive" size="sm">
<Trash className="mr-2 size-4" aria-hidden="true" />
- Delete ({pqs.length})
+ 삭제 ({pqLists.length})
</Button>
</DialogTrigger>
) : null}
<DialogContent>
<DialogHeader>
- <DialogTitle>Are you absolutely sure?</DialogTitle>
+ <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
<DialogDescription>
- This action cannot be undone. This will permanently delete your{" "}
- <span className="font-medium">{pqs.length}</span>
- {pqs.length === 1 ? " PQ" : " PQs"} from our servers.
+ 이 작업은 되돌릴 수 없습니다. 선택한{" "}
+ <span className="font-medium">{pqLists.length}개</span>의 PQ 목록이 영구적으로 삭제됩니다.
+ <br />
+ <span className="text-red-600 font-medium">삭제 시 해당 PQ 목록의 모든 하위 항목들도 함께 삭제됩니다.</span>
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:space-x-0">
<DialogClose asChild>
- <Button variant="outline">Cancel</Button>
+ <Button variant="outline">취소</Button>
</DialogClose>
<Button
aria-label="Delete selected rows"
@@ -100,7 +89,7 @@ export function DeletePqsDialog({
aria-hidden="true"
/>
)}
- Delete
+ 삭제
</Button>
</DialogFooter>
</DialogContent>
@@ -112,24 +101,25 @@ export function DeletePqsDialog({
<Drawer {...props}>
{showTrigger ? (
<DrawerTrigger asChild>
- <Button variant="outline" size="sm">
+ <Button variant="destructive" size="sm">
<Trash className="mr-2 size-4" aria-hidden="true" />
- Delete ({pqs.length})
+ 삭제 ({pqLists.length})
</Button>
</DrawerTrigger>
) : null}
<DrawerContent>
<DrawerHeader>
- <DrawerTitle>Are you absolutely sure?</DrawerTitle>
+ <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
<DrawerDescription>
- This action cannot be undone. This will permanently delete your{" "}
- <span className="font-medium">{pqs.length}</span>
- {pqs.length === 1 ? " task" : " pqs"} from our servers.
+ 이 작업은 되돌릴 수 없습니다. 선택한{" "}
+ <span className="font-medium">{pqLists.length}개</span>의 PQ 목록이 영구적으로 삭제됩니다.
+ <br />
+ <span className="text-red-600 font-medium">삭제 시 해당 PQ 목록의 모든 하위 항목들도 함께 삭제됩니다.</span>
</DrawerDescription>
</DrawerHeader>
<DrawerFooter className="gap-2 sm:space-x-0">
<DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
+ <Button variant="outline">취소</Button>
</DrawerClose>
<Button
aria-label="Delete selected rows"
@@ -140,7 +130,7 @@ export function DeletePqsDialog({
{isDeletePending && (
<Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
)}
- Delete
+ 삭제
</Button>
</DrawerFooter>
</DrawerContent>
diff --git a/lib/pq/table/import-pq-button.tsx b/lib/pq/table/import-pq-button.tsx
deleted file mode 100644
index 2fbf66d9..00000000
--- a/lib/pq/table/import-pq-button.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Upload } from "lucide-react"
-import { toast } from "sonner"
-import * as ExcelJS from 'exceljs'
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Progress } from "@/components/ui/progress"
-import { processFileImport } from "./import-pq-handler" // 별도 파일로 분리
-import { decryptWithServerAction } from "@/components/drm/drmUtils"
-
-interface ImportPqButtonProps {
- projectId?: number | null
- onSuccess?: () => void
-}
-
-export function ImportPqButton({ projectId, onSuccess }: ImportPqButtonProps) {
- const [open, setOpen] = React.useState(false)
- const [file, setFile] = React.useState<File | null>(null)
- const [isUploading, setIsUploading] = React.useState(false)
- const [progress, setProgress] = React.useState(0)
- const [error, setError] = React.useState<string | null>(null)
- const fileInputRef = React.useRef<HTMLInputElement>(null)
-
- // 파일 선택 처리
- const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- const selectedFile = e.target.files?.[0]
- if (!selectedFile) return
-
- if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) {
- setError("Excel 파일(.xlsx 또는 .xls)만 가능합니다.")
- return
- }
-
- setFile(selectedFile)
- setError(null)
- }
-
- // 데이터 가져오기 처리
- const handleImport = async () => {
- if (!file) {
- setError("가져올 파일을 선택해주세요.")
- return
- }
-
- try {
- setIsUploading(true)
- setProgress(0)
- setError(null)
-
- // DRM 복호화 처리 - 서버 액션 직접 호출
- let arrayBuffer: ArrayBuffer;
- try {
- setProgress(10);
- toast.info("파일 복호화 중...");
- arrayBuffer = await decryptWithServerAction(file);
- setProgress(30);
- } catch (decryptError) {
- console.error("파일 복호화 실패, 원본 파일 사용:", decryptError);
- toast.warning("파일 복호화에 실패하여 원본 파일을 사용합니다.");
- // 복호화 실패 시 원본 파일 사용
- arrayBuffer = await file.arrayBuffer();
- }
-
- // ExcelJS 워크북 로드
- const workbook = new ExcelJS.Workbook();
- await workbook.xlsx.load(arrayBuffer);
-
- // 첫 번째 워크시트 가져오기
- const worksheet = workbook.worksheets[0];
- if (!worksheet) {
- throw new Error("Excel 파일에 워크시트가 없습니다.");
- }
-
- // 헤더 행 번호 찾기 (보통 지침 행이 있으므로 헤더는 뒤에 위치)
- let headerRowIndex = 1;
- let headerRow: ExcelJS.Row | undefined;
- let headerValues: (string | null)[] = [];
-
- worksheet.eachRow((row, rowNumber) => {
- const values = row.values as (string | null)[];
- if (!headerRow && values.some(v => v === "Code" || v === "Check Point") && rowNumber > 1) {
- headerRowIndex = rowNumber;
- headerRow = row;
- headerValues = [...values];
- }
- });
-
- if (!headerRow) {
- throw new Error("Excel 파일에서 헤더 행을 찾을 수 없습니다.");
- }
-
- // 헤더를 기반으로 인덱스 매핑 생성
- const headerMapping: Record<string, number> = {};
- headerValues.forEach((value, index) => {
- if (typeof value === 'string') {
- headerMapping[value] = index;
- }
- });
-
- // 필수 헤더 확인
- const requiredHeaders = ["Code", "Check Point", "Group Name"];
- const missingHeaders = requiredHeaders.filter(header => !(header in headerMapping));
-
- if (missingHeaders.length > 0) {
- throw new Error(`다음 필수 헤더가 누락되었습니다: ${missingHeaders.join(", ")}`);
- }
-
- // 데이터 행 추출 (헤더 이후 행부터)
- const dataRows: Record<string, any>[] = [];
-
- worksheet.eachRow((row, rowNumber) => {
- if (rowNumber > headerRowIndex) {
- const rowData: Record<string, any> = {};
- const values = row.values as (string | null | undefined)[];
-
- Object.entries(headerMapping).forEach(([header, index]) => {
- rowData[header] = values[index] || "";
- });
-
- // 빈 행이 아닌 경우만 추가
- if (Object.values(rowData).some(value => value && value.toString().trim() !== "")) {
- dataRows.push(rowData);
- }
- }
- });
-
- if (dataRows.length === 0) {
- throw new Error("Excel 파일에 가져올 데이터가 없습니다.");
- }
-
- // 진행 상황 업데이트를 위한 콜백
- const updateProgress = (current: number, total: number) => {
- const percentage = Math.round((current / total) * 100);
- setProgress(percentage);
- };
-
- // 실제 데이터 처리는 별도 함수에서 수행
- const result = await processFileImport(
- dataRows,
- projectId,
- updateProgress
- );
-
- // 처리 완료
- toast.success(`${result.successCount}개의 PQ 항목이 성공적으로 가져와졌습니다.`);
-
- if (result.errorCount > 0) {
- toast.warning(`${result.errorCount}개의 항목은 처리할 수 없었습니다.`);
- }
-
- // 상태 초기화 및 다이얼로그 닫기
- setFile(null);
- setOpen(false);
-
- // 성공 콜백 호출
- if (onSuccess) {
- onSuccess();
- }
- } catch (error) {
- console.error("Excel 파일 처리 중 오류 발생:", error);
- setError(error instanceof Error ? error.message : "파일 처리 중 오류가 발생했습니다.");
- } finally {
- setIsUploading(false);
- }
- };
-
- // 다이얼로그 열기/닫기 핸들러
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen) {
- // 닫을 때 상태 초기화
- setFile(null)
- setError(null)
- setProgress(0)
- if (fileInputRef.current) {
- fileInputRef.current.value = ""
- }
- }
- setOpen(newOpen)
- }
-
- return (
- <>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- onClick={() => setOpen(true)}
- disabled={isUploading}
- >
- <Upload className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Import</span>
- </Button>
-
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle>PQ 항목 가져오기</DialogTitle>
- <DialogDescription>
- {projectId
- ? "프로젝트별 PQ 항목을 Excel 파일에서 가져옵니다."
- : "일반 PQ 항목을 Excel 파일에서 가져옵니다."}
- <br />
- 올바른 형식의 Excel 파일(.xlsx)을 업로드하세요.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4 py-4">
- <div className="flex items-center gap-4">
- <input
- type="file"
- ref={fileInputRef}
- className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-foreground file:font-medium"
- accept=".xlsx,.xls"
- onChange={handleFileChange}
- disabled={isUploading}
- />
- </div>
-
- {file && (
- <div className="text-sm text-muted-foreground">
- 선택된 파일: <span className="font-medium">{file.name}</span> ({(file.size / 1024).toFixed(1)} KB)
- </div>
- )}
-
- {isUploading && (
- <div className="space-y-2">
- <Progress value={progress} />
- <p className="text-sm text-muted-foreground text-center">
- {progress}% 완료
- </p>
- </div>
- )}
-
- {error && (
- <div className="text-sm font-medium text-destructive">
- {error}
- </div>
- )}
- </div>
-
- <DialogFooter>
- <Button
- variant="outline"
- onClick={() => setOpen(false)}
- disabled={isUploading}
- >
- 취소
- </Button>
- <Button
- onClick={handleImport}
- disabled={!file || isUploading}
- >
- {isUploading ? "처리 중..." : "가져오기"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- </>
- )
-} \ No newline at end of file
diff --git a/lib/pq/table/import-pq-handler.tsx b/lib/pq/table/import-pq-handler.tsx
deleted file mode 100644
index 13431ba7..00000000
--- a/lib/pq/table/import-pq-handler.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-"use client"
-
-import { z } from "zod"
-import { createPq } from "../service" // PQ 생성 서버 액션
-
-// PQ 데이터 검증을 위한 Zod 스키마
-const pqItemSchema = z.object({
- code: z.string().min(1, "Code is required"),
- checkPoint: z.string().min(1, "Check point is required"),
- groupName: z.string().min(1, "Group is required"),
- description: z.string().optional().nullable(),
- remarks: z.string().optional().nullable(),
- contractInfo: z.string().optional().nullable(),
- additionalRequirement: z.string().optional().nullable(),
-});
-
-// 지원하는 그룹 이름 목록
-const validGroupNames = [
- "GENERAL",
- "Quality Management System",
- "Workshop & Environment",
- "Warranty",
-];
-
-type ImportPqItem = z.infer<typeof pqItemSchema>;
-
-interface ProcessResult {
- successCount: number;
- errorCount: number;
- errors?: Array<{ row: number; message: string }>;
-}
-
-/**
- * Excel 파일에서 가져온 PQ 데이터를 처리하는 함수
- */
-export async function processFileImport(
- jsonData: any[],
- projectId: number | null | undefined,
- progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
- // 결과 카운터 초기화
- let successCount = 0;
- let errorCount = 0;
- const errors: Array<{ row: number; message: string }> = [];
-
- // 헤더 행과 지침 행 건너뛰기
- const dataRows = jsonData.filter(row => {
- // 행이 문자열로만 구성된 경우 지침 행으로 간주
- if (Object.values(row).every(val => typeof val === 'string' && !val.includes(':'))) {
- return false;
- }
- // 빈 행 건너뛰기
- if (Object.values(row).every(val => !val)) {
- return false;
- }
- return true;
- });
-
- // 데이터 행이 없으면 빈 결과 반환
- if (dataRows.length === 0) {
- return { successCount: 0, errorCount: 0 };
- }
-
- // 각 행에 대해 처리
- for (let i = 0; i < dataRows.length; i++) {
- const row = dataRows[i];
- const rowIndex = i + 1; // 사용자에게 표시할 행 번호는 1부터 시작
-
- // 진행 상황 콜백 호출
- if (progressCallback) {
- progressCallback(i + 1, dataRows.length);
- }
-
- try {
- // 데이터 정제
- const cleanedRow: ImportPqItem = {
- code: row.Code?.toString().trim() ?? "",
- checkPoint: row["Check Point"]?.toString().trim() ?? "",
- groupName: row["Group Name"]?.toString().trim() ?? "",
- description: row.Description?.toString() ?? "",
- remarks: row.Remarks?.toString() ?? "",
- contractInfo: row["Contract Info"]?.toString() ?? "",
- additionalRequirement: row["Additional Requirements"]?.toString() ?? "",
- };
-
- // 데이터 유효성 검사
- const validationResult = pqItemSchema.safeParse(cleanedRow);
-
- if (!validationResult.success) {
- const errorMessage = validationResult.error.errors.map(
- err => `${err.path.join('.')}: ${err.message}`
- ).join(', ');
-
- errors.push({ row: rowIndex, message: errorMessage });
- errorCount++;
- continue;
- }
-
- // 그룹 이름 유효성 검사
- if (!validGroupNames.includes(cleanedRow.groupName)) {
- errors.push({
- row: rowIndex,
- message: `Invalid group name: ${cleanedRow.groupName}. Must be one of: ${validGroupNames.join(', ')}`
- });
- errorCount++;
- continue;
- }
-
- // PQ 생성 서버 액션 호출
- const createResult = await createPq({
- ...cleanedRow,
- projectId: projectId || 0
- });
-
- if (createResult.success) {
- successCount++;
- } else {
- errors.push({
- row: rowIndex,
- message: createResult.message || "Unknown error"
- });
- errorCount++;
- }
- } catch (error) {
- console.error(`Row ${rowIndex} processing error:`, error);
- errors.push({
- row: rowIndex,
- message: error instanceof Error ? error.message : "Unknown error"
- });
- errorCount++;
- }
-
- // 비동기 작업 쓰로틀링
- if (i % 5 === 0) {
- await new Promise(resolve => setTimeout(resolve, 10));
- }
- }
-
- // 처리 결과 반환
- return {
- successCount,
- errorCount,
- errors: errors.length > 0 ? errors : undefined
- };
-} \ No newline at end of file
diff --git a/lib/pq/table/pq-excel-template.tsx b/lib/pq/table/pq-excel-template.tsx
deleted file mode 100644
index aa8c1b3a..00000000
--- a/lib/pq/table/pq-excel-template.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-"use client"
-
-import * as ExcelJS from 'exceljs';
-import { saveAs } from 'file-saver';
-import { toast } from 'sonner';
-
-/**
- * PQ 기준 Excel 템플릿을 다운로드하는 함수 (exceljs 사용)
- * @param isProjectSpecific 프로젝트별 PQ 템플릿 여부
- */
-export async function exportPqTemplate(isProjectSpecific: boolean = false) {
- try {
- // 워크북 생성
- const workbook = new ExcelJS.Workbook();
-
- // 워크시트 생성
- const sheetName = isProjectSpecific ? "Project PQ Template" : "General PQ Template";
- const worksheet = workbook.addWorksheet(sheetName);
-
- // 그룹 옵션 정의 - 드롭다운 목록에 사용
- const groupOptions = [
- "GENERAL",
- "Quality Management System",
- "Workshop & Environment",
- "Warranty",
- ];
-
- // 일반 PQ 필드 (기본 필드)
- const basicFields = [
- { header: "Code", key: "code", width: 90 },
- { header: "Check Point", key: "checkPoint", width: 180 },
- { header: "Group Name", key: "groupName", width: 150 },
- { header: "Description", key: "description", width: 240 },
- { header: "Remarks", key: "remarks", width: 180 },
- ];
-
- // 프로젝트별 PQ 추가 필드
- const projectFields = isProjectSpecific
- ? [
- { header: "Contract Info", key: "contractInfo", width: 180 },
- { header: "Additional Requirements", key: "additionalRequirement", width: 240 },
- ]
- : [];
-
- // 모든 필드 합치기
- const fields = [...basicFields, ...projectFields];
-
- // 지침 행 추가
- const instructionTitle = worksheet.addRow(["Instructions:"]);
- instructionTitle.font = { bold: true, size: 12 };
- worksheet.mergeCells(1, 1, 1, fields.length);
-
- const instructions = [
- "1. 'Code' 필드는 고유해야 합니다 (예: 1-1, A.2.3).",
- "2. 'Check Point'는 필수 항목입니다.",
- "3. 'Group Name'은 드롭다운 목록에서 선택하세요: GENERAL, Quality Management System, Workshop & Environment, Warranty",
- "4. 여러 줄 텍스트는 \\n으로 줄바꿈을 표시합니다.",
- "5. 아래 회색 배경의 예시 행은 참고용입니다. 실제 데이터 입력 전에 이 행을 수정하거나 삭제해야 합니다.",
- ];
-
- // 프로젝트별 PQ일 경우 추가 지침
- if (isProjectSpecific) {
- instructions.push(
- "6. 'Contract Info'와 'Additional Requirements'는 프로젝트별 세부 정보를 위한 필드입니다."
- );
- }
-
- // 지침 행 추가
- instructions.forEach((instruction, idx) => {
- const row = worksheet.addRow([instruction]);
- worksheet.mergeCells(idx + 2, 1, idx + 2, fields.length);
- row.font = { color: { argb: '00808080' } };
- });
-
- // 빈 행 추가
- worksheet.addRow([]);
-
- // 헤더 행 추가
- const headerRow = worksheet.addRow(fields.map(field => field.header));
- headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
- headerRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FF4472C4' }
- };
- headerRow.alignment = { vertical: 'middle', horizontal: 'center' };
-
- // 예시 행 표시를 위한 첫 번째 열 값 수정
- const exampleData: Record<string, string> = {
- code: "[예시 - 수정/삭제 필요] 1-1",
- checkPoint: "Selling / 1 year Property",
- groupName: "GENERAL",
- description: "Address :\nTel. / Fax :\ne-mail :",
- remarks: "Optional remarks",
- };
-
- // 프로젝트별 PQ인 경우 예시 데이터에 추가 필드 추가
- if (isProjectSpecific) {
- exampleData.contractInfo = "Contract details for this project";
- exampleData.additionalRequirement = "Additional technical requirements";
- }
-
- const exampleRow = worksheet.addRow(fields.map(field => exampleData[field.key] || ""));
- exampleRow.font = { italic: true };
- exampleRow.fill = {
- type: 'pattern',
- pattern: 'solid',
- fgColor: { argb: 'FFEDEDED' }
- };
- // 예시 행 첫 번째 셀에 코멘트 추가
- const codeCell = worksheet.getCell(exampleRow.number, 1);
- codeCell.note = '이 예시 행은 참고용입니다. 실제 데이터 입력 전에 수정하거나 삭제하세요.';
-
- // Group Name 열 인덱스 찾기 (0-based)
- const groupNameIndex = fields.findIndex(field => field.key === "groupName");
-
- // 열 너비 설정
- fields.forEach((field, index) => {
- const column = worksheet.getColumn(index + 1);
- column.width = field.width / 6.5; // ExcelJS에서는 픽셀과 다른 단위 사용
- });
-
- // 각 셀에 테두리 추가
- const headerRowNum = instructions.length + 3;
- const exampleRowNum = headerRowNum + 1;
-
- for (let i = 1; i <= fields.length; i++) {
- // 헤더 행에 테두리 추가
- worksheet.getCell(headerRowNum, i).border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
-
- // 예시 행에 테두리 추가
- worksheet.getCell(exampleRowNum, i).border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
- }
-
- // 사용자 입력용 빈 행 추가 (10개)
- for (let rowIdx = 0; rowIdx < 10; rowIdx++) {
- // 빈 행 추가
- const emptyRow = worksheet.addRow(Array(fields.length).fill(''));
- const currentRowNum = exampleRowNum + 1 + rowIdx;
-
- // 각 셀에 테두리 추가
- for (let colIdx = 1; colIdx <= fields.length; colIdx++) {
- const cell = worksheet.getCell(currentRowNum, colIdx);
- cell.border = {
- top: { style: 'thin' },
- left: { style: 'thin' },
- bottom: { style: 'thin' },
- right: { style: 'thin' }
- };
-
- // Group Name 열에 데이터 유효성 검사 (드롭다운) 추가
- if (colIdx === groupNameIndex + 1) {
- cell.dataValidation = {
- type: 'list',
- allowBlank: true,
- formulae: [`"${groupOptions.join(',')}"`],
- showErrorMessage: true,
- errorStyle: 'error',
- error: '유효하지 않은 그룹입니다',
- errorTitle: '입력 오류',
- prompt: '목록에서 선택하세요',
- promptTitle: '그룹 선택'
- };
- }
- }
- }
-
- // 예시 행이 있는 열에도 Group Name 드롭다운 적용
- const exampleGroupCell = worksheet.getCell(exampleRowNum, groupNameIndex + 1);
- exampleGroupCell.dataValidation = {
- type: 'list',
- allowBlank: true,
- formulae: [`"${groupOptions.join(',')}"`],
- showErrorMessage: true,
- errorStyle: 'error',
- error: '유효하지 않은 그룹입니다',
- errorTitle: '입력 오류',
- prompt: '목록에서 선택하세요',
- promptTitle: '그룹 선택'
- };
-
- // 워크북을 Excel 파일로 변환
- const buffer = await workbook.xlsx.writeBuffer();
-
- // 파일명 설정 및 저장
- const fileName = isProjectSpecific ? "project-pq-template.xlsx" : "general-pq-template.xlsx";
- const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
- saveAs(blob, fileName);
-
- toast.success(`${isProjectSpecific ? '프로젝트별' : '일반'} PQ 템플릿이 다운로드되었습니다.`);
- } catch (error) {
- console.error("템플릿 다운로드 중 오류 발생:", error);
- toast.error("템플릿 다운로드 중 오류가 발생했습니다.");
- }
-} \ No newline at end of file
diff --git a/lib/pq/table/pq-lists-columns.tsx b/lib/pq/table/pq-lists-columns.tsx
new file mode 100644
index 00000000..1c401fac
--- /dev/null
+++ b/lib/pq/table/pq-lists-columns.tsx
@@ -0,0 +1,216 @@
+"use client"
+
+import { ColumnDef } from "@tanstack/react-table"
+import { Badge } from "@/components/ui/badge"
+// import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
+import { DataTableRowAction } from "@/types/table"
+import { Ellipsis } from "lucide-react"
+import { formatDate } from "@/lib/utils"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu"
+import { Button } from "@/components/ui/button"
+import React from "react"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { Checkbox } from "@/components/ui/checkbox"
+
+export interface PQList {
+ id: number
+ name: string
+ type: "GENERAL" | "PROJECT" | "NON_INSPECTION"
+ projectId?: number | null
+ projectCode?: string | null
+ projectName?: string | null
+ isDeleted: boolean
+ validTo?: Date | null
+ createdBy?: string | null // 이제 사용자 이름(users.name)
+ createdAt: Date
+ updatedAt: Date
+ updatedBy?: string | null
+ criteriaCount?: number
+}
+
+const typeLabels = {
+ GENERAL: "일반 PQ",
+ PROJECT: "프로젝트 PQ",
+ NON_INSPECTION: "미실사 PQ"
+}
+
+const typeColors = {
+ GENERAL: "bg-blue-100 text-blue-800",
+ PROJECT: "bg-green-100 text-green-800",
+ NON_INSPECTION: "bg-orange-100 text-orange-800"
+}
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PQList> | null>>
+}
+export function createPQListsColumns({
+ setRowAction
+}: GetColumnsProps): ColumnDef<PQList>[] {
+ return [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={
+ table.getIsAllPageRowsSelected() ||
+ (table.getIsSomePageRowsSelected() && "indeterminate")
+ }
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-0.5"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-0.5"
+ />
+ ),
+ size:40,
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "name",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 리스트명" />
+ ),
+ cell: ({ row }) => (
+ <div className="max-w-[200px] truncate font-medium">
+ {row.getValue("name")}
+ </div>
+ ),
+ },
+ {
+ accessorKey: "type",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PQ 종류" />
+ ),
+ cell: ({ row }) => {
+ const type = row.getValue("type") as keyof typeof typeLabels
+ return (
+ <Badge className={typeColors[type]}>
+ {typeLabels[type]}
+ </Badge>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ },
+ {
+ accessorKey: "projectCode",
+ header: "프로젝트",
+ cell: ({ row }) => row.original.projectCode ?? "-",
+ },
+ {
+ accessorKey: "projectName",
+ header: "프로젝트명",
+ cell: ({ row }) => row.original.projectName ?? "-",
+ },
+ {
+ accessorKey: "validTo",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="유효일" />
+ ),
+ cell: ({ row }) => {
+ const validTo = row.getValue("validTo") as Date | null
+ const now = new Date()
+ const isExpired = validTo && validTo < now
+
+ const formattedDate = validTo ? formatDate(validTo, "ko-KR") : "-"
+
+ return (
+ <div className="text-sm">
+ <span className={isExpired ? "text-red-600 font-medium" : ""}>
+ {formattedDate}
+ </span>
+ {isExpired && (
+ <Badge variant="destructive" className="ml-2 text-xs">
+ 만료
+ </Badge>
+ )}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "isDeleted",
+ header: "상태",
+ cell: ({ row }) => {
+ const isDeleted = row.getValue("isDeleted") as boolean;
+ return (
+ <Badge variant={isDeleted ? "destructive" : "success"}>
+ {isDeleted ? "비활성" : "활성"}
+ </Badge>
+ );
+ },
+ },
+ {
+ accessorKey: "createdBy",
+ header: "생성자",
+ cell: ({ row }) => row.original.createdBy ?? "-",
+ },
+ {
+ accessorKey: "updatedBy",
+ header: "변경자",
+ cell: ({ row }) => row.original.updatedBy ?? "-",
+ },
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ cell: ({ row }) => formatDate(row.getValue("createdAt"), "ko-KR"),
+ },
+ {
+ accessorKey: "updatedAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="변경일" />
+ ),
+ cell: ({ row }) => formatDate(row.getValue("updatedAt"), "ko-KR"),
+ },
+ {
+ id: "actions",
+ cell: ({ row }) => (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-7 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "view" })}
+ >
+ 상세보기
+ </DropdownMenuItem>
+ {/* <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "delete" })}
+ className="text-destructive"
+ >
+ 삭제
+ <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
+ </DropdownMenuItem> */}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ ),
+ size: 40,
+ enableSorting: false,
+ enableHiding: false,
+ }
+
+ ]
+} \ No newline at end of file
diff --git a/lib/pq/table/pq-lists-table.tsx b/lib/pq/table/pq-lists-table.tsx
new file mode 100644
index 00000000..c5fd82a5
--- /dev/null
+++ b/lib/pq/table/pq-lists-table.tsx
@@ -0,0 +1,170 @@
+"use client"
+
+import * as React from "react"
+import { useRouter } from "next/navigation"
+import { toast } from "sonner"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { createPQListsColumns, type PQList } from "./pq-lists-columns"
+import {
+ createPQListAction,
+ deletePQListsAction,
+ copyPQListAction,
+ togglePQListsAction,
+} from "@/lib/pq/service"
+import { CopyPqDialog } from "./copy-pq-list-dialog"
+import { AddPqDialog } from "./add-pq-list-dialog"
+import { PQListsToolbarActions } from "./pq-lists-toolbar"
+import type { DataTableRowAction } from "@/types/table"
+
+interface Project {
+ id: number
+ name: string
+ code: string
+}
+
+interface PqListsTableProps {
+ promises: Promise<[{ data: PQList[]; pageCount: number }, Project[]]>
+}
+
+export function PqListsTable({ promises }: PqListsTableProps) {
+ const router = useRouter()
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<PQList> | null>(null)
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
+ const [copyDialogOpen, setCopyDialogOpen] = React.useState(false)
+ const [isPending, startTransition] = React.useTransition()
+
+ const [{ data, pageCount }, projects] = React.use(promises)
+ const activePqLists = data.filter((item) => !item.isDeleted)
+
+ const columns = React.useMemo(() => createPQListsColumns({ setRowAction }), [setRowAction])
+
+ const { table } = useDataTable({
+ data,
+ columns,
+ pageCount,
+ enablePinning: true,
+ enableAdvancedFilter: false,
+ initialState: {
+ sorting: [{ id: "createdAt", desc: true }],
+ columnPinning: { right: ["actions"] },
+ },
+ getRowId: (row) => String(row.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ const handleCreate = async (formData: {
+ name: string
+ type: "GENERAL" | "PROJECT" | "NON_INSPECTION"
+ projectId?: number | null
+ validTo?: Date | null
+ }) => {
+ startTransition(async () => {
+ const result = await createPQListAction(formData)
+ if (result.success) {
+ toast.success("PQ 목록이 생성되었습니다")
+ setCreateDialogOpen(false)
+ router.refresh()
+ } else {
+ toast.error(result.error || "PQ 목록 생성 실패")
+ }
+ })
+ }
+
+ const handleToggleActive = async (ids: number[], newIsDeleted: boolean) => {
+ startTransition(async () => {
+ const result = await togglePQListsAction(ids, newIsDeleted)
+ if (result.success) {
+ toast.success(newIsDeleted ? "PQ 목록이 비활성화되었습니다" : "PQ 목록이 활성화되었습니다")
+ router.refresh()
+ } else {
+ toast.error("PQ 목록 상태 변경 실패")
+ }
+ })
+ }
+
+ const handleDelete = async (ids: number[]) => {
+ startTransition(async () => {
+ const result = await deletePQListsAction(ids)
+ if (result.success) {
+ toast.success("PQ 목록이 삭제되었습니다")
+ router.refresh()
+ } else {
+ toast.error("PQ 목록 삭제 실패")
+ }
+ })
+ }
+
+ const handleCopy = async (copyData: {
+ sourcePqListId: number
+ targetProjectId: number
+ newName?: string
+ validTo?: Date | null
+ }) => {
+ startTransition(async () => {
+ const result = await copyPQListAction(copyData)
+ if (result.success) {
+ toast.success("PQ 목록이 복사되었습니다")
+ setCopyDialogOpen(false)
+ router.refresh()
+ } else {
+ toast.error("PQ 목록 복사 실패")
+ }
+ })
+ }
+
+ React.useEffect(() => {
+ if (!rowAction) return
+ const id = rowAction.row.original.id
+ switch (rowAction.type) {
+ case "view":
+ router.push(`/evcp/pq-criteria/${id}`)
+ break
+ case "delete":
+ handleDelete([id])
+ break
+ }
+ setRowAction(null)
+ }, [rowAction])
+
+ return (
+ <>
+ <DataTable table={table}>
+ <DataTableAdvancedToolbar
+ table={table}
+ filterFields={[]}
+ shallow={false}
+ >
+ <PQListsToolbarActions
+ table={table}
+ onAddClick={() => setCreateDialogOpen(true)}
+ onCopyClick={() => setCopyDialogOpen(true)}
+ onToggleActive={(rows, newIsDeleted) =>
+ handleToggleActive(rows.map((r) => r.id), newIsDeleted)
+ }
+ />
+ </DataTableAdvancedToolbar>
+ </DataTable>
+
+ <AddPqDialog
+ open={createDialogOpen}
+ onOpenChange={setCreateDialogOpen}
+ onSubmit={handleCreate}
+ projects={projects}
+ isLoading={isPending}
+ />
+
+ <CopyPqDialog
+ open={copyDialogOpen}
+ onOpenChange={setCopyDialogOpen}
+ pqLists={activePqLists}
+ projects={projects}
+ onCopy={handleCopy}
+ isLoading={isPending}
+ />
+ </>
+ )
+}
diff --git a/lib/pq/table/pq-lists-toolbar.tsx b/lib/pq/table/pq-lists-toolbar.tsx
new file mode 100644
index 00000000..3a85327d
--- /dev/null
+++ b/lib/pq/table/pq-lists-toolbar.tsx
@@ -0,0 +1,61 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Trash, CopyPlus, Plus } from "lucide-react"
+import { type Table } from "@tanstack/react-table"
+import type { PQList } from "./pq-lists-columns"
+// import { PqListForm } from "./add-pq-list-dialog"
+
+interface PQListsToolbarActionsProps {
+ table: Table<PQList>;
+ onAddClick: () => void;
+ onCopyClick: () => void;
+ onToggleActive: (rows: PQList[], newIsDeleted: boolean) => void;
+}
+
+export function PQListsToolbarActions({
+ table,
+ onAddClick,
+ onCopyClick,
+ onToggleActive,
+}: PQListsToolbarActionsProps) {
+ const selected = table.getFilteredSelectedRowModel().rows.map(r => r.original);
+ const allActive = selected.length > 0 && selected.every(item => !item.isDeleted);
+ const allDeleted = selected.length > 0 && selected.every(item => item.isDeleted);
+
+ let toggleLabel = "";
+ let newState: boolean | undefined;
+ if (selected.length > 0) {
+ if (allActive) {
+ toggleLabel = "비활성화";
+ newState = true;
+ } else if (allDeleted) {
+ toggleLabel = "활성화";
+ newState = false;
+ }
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ {selected.length > 0 && (allActive || allDeleted) && newState !== undefined && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => onToggleActive(selected, newState!)}
+ >
+ <Trash className="mr-2 h-4 w-4" />
+ {toggleLabel}
+ </Button>
+ )}
+ <Button size="sm" onClick={onAddClick}>
+ <Plus className="mr-2 h-4 w-4" />
+ 추가
+ </Button>
+ <Button size="sm" variant="outline" onClick={onCopyClick}>
+ <CopyPlus className="mr-2 h-4 w-4" />
+ 복사
+ </Button>
+ </div>
+ );
+}
diff --git a/lib/pq/table/pq-table-column.tsx b/lib/pq/table/pq-table-column.tsx
deleted file mode 100644
index b9317570..00000000
--- a/lib/pq/table/pq-table-column.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { ColumnDef } from "@tanstack/react-table"
-import { formatDate, formatDateTime } from "@/lib/utils"
-import { Checkbox } from "@/components/ui/checkbox"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { DataTableRowAction } from "@/types/table"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSeparator,
- DropdownMenuShortcut,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { Button } from "@/components/ui/button"
-import { Ellipsis } from "lucide-react"
-import { Badge } from "@/components/ui/badge"
-import { PqCriterias } from "@/db/schema/pq"
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PqCriterias> | null>>
-}
-
-export function getColumns({
- setRowAction,
-}: GetColumnsProps): ColumnDef<PqCriterias>[] {
- return [
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-0.5"
- />
- ),
- size:40,
- enableSorting: false,
- enableHiding: false,
- },
-
- {
- accessorKey: "groupName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Group Name" />
- ),
- cell: ({ row }) => <div>{row.getValue("groupName")}</div>,
- meta: {
- excelHeader: "Group Name"
- },
- enableResizing: true,
- minSize: 60,
- size: 100,
- },
- {
- accessorKey: "code",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Code" />
- ),
- cell: ({ row }) => <div>{row.getValue("code")}</div>,
- meta: {
- excelHeader: "Code"
- },
- enableResizing: true,
- minSize: 50,
- size: 100,
- },
- {
- accessorKey: "checkPoint",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Check Point" />
- ),
- cell: ({ row }) => <div>{row.getValue("checkPoint")}</div>,
- meta: {
- excelHeader: "Check Point"
- },
- enableResizing: true,
- minSize: 180,
- size: 180,
- },
-
- {
- accessorKey: "description",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Description" />
- ),
- cell: ({ row }) => {
- const text = row.getValue("description") as string
- return (
- <div style={{ whiteSpace: "pre-wrap" }}>
- {text}
- </div>
- )
- },
- meta: {
- excelHeader: "Description"
- },
- enableResizing: true,
- minSize: 180,
- size: 180,
- },
-
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Created At" />
- ),
- cell: ({ cell }) => formatDateTime(cell.getValue() as Date, "KR"),
- meta: {
- excelHeader: "created At"
- },
- enableResizing: true,
- minSize: 180,
- size: 180,
- },
- {
- accessorKey: "updatedAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Updated At" />
- ),
- cell: ({ cell }) => formatDateTime(cell.getValue() as Date, "KR"),
- meta: {
- excelHeader: "updated At"
- },
- enableResizing: true,
- minSize: 180,
- size: 180,
- },
- {
- id: "actions",
- enableHiding: false,
- cell: function Cell({ row }) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
-
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- aria-label="Open menu"
- variant="ghost"
- className="flex size-7 p-0 data-[state=open]:bg-muted"
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-40">
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
- >
- Edit
- </DropdownMenuItem>
-
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "delete" })}
- >
- Delete
- <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )
- },
- size: 40,
- }
- ]
-} \ No newline at end of file
diff --git a/lib/pq/table/pq-table-toolbar-actions.tsx b/lib/pq/table/pq-table-toolbar-actions.tsx
deleted file mode 100644
index 1790caf8..00000000
--- a/lib/pq/table/pq-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, FileDown, Upload } from "lucide-react"
-import { toast } from "sonner"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-
-import { DeletePqsDialog } from "./delete-pqs-dialog"
-import { AddPqDialog } from "./add-pq-dialog"
-import { PqCriterias } from "@/db/schema/pq"
-import { ImportPqButton } from "./import-pq-button"
-import { exportPqTemplate } from "./pq-excel-template"
-
-interface PqTableToolbarActionsProps {
- table: Table<PqCriterias>
- currentProjectId?: number
-}
-
-export function PqTableToolbarActions({
- table,
- currentProjectId
-}: PqTableToolbarActionsProps) {
- const [refreshKey, setRefreshKey] = React.useState(0)
- const isProjectSpecific = !!currentProjectId
-
- // Import 성공 후 테이블 갱신
- const handleImportSuccess = () => {
- setRefreshKey(prev => prev + 1)
- }
-
- return (
- <div className="flex items-center gap-2">
- {table.getFilteredSelectedRowModel().rows.length > 0 ? (
- <DeletePqsDialog
- pqs={table
- .getFilteredSelectedRowModel()
- .rows.map((row) => row.original)}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- />
- ) : null}
-
- <AddPqDialog currentProjectId={currentProjectId} />
-
- {/* Import 버튼 */}
- <ImportPqButton
- projectId={currentProjectId}
- onSuccess={handleImportSuccess}
- />
-
- {/* Export 드롭다운 메뉴 */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={() =>
- exportTableToExcel(table, {
- filename: isProjectSpecific ? `project-${currentProjectId}-pq-criteria` : "general-pq-criteria",
- excludeColumns: ["select", "actions"],
- })
- }
- >
- <FileDown className="mr-2 h-4 w-4" />
- <span>현재 데이터 내보내기</span>
- </DropdownMenuItem>
- <DropdownMenuItem onClick={() => exportPqTemplate(isProjectSpecific)}>
- <FileDown className="mr-2 h-4 w-4" />
- <span>{isProjectSpecific ? '프로젝트용' : '일반'} 템플릿 다운로드</span>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/pq/table/pq-table.tsx b/lib/pq/table/pq-table.tsx
deleted file mode 100644
index 99365ad5..00000000
--- a/lib/pq/table/pq-table.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-"use client"
-
-import * as React from "react"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { getPQs } from "../service"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { PqCriterias } from "@/db/schema/pq"
-import { DeletePqsDialog } from "./delete-pqs-dialog"
-import { PqTableToolbarActions } from "./pq-table-toolbar-actions"
-import { getColumns } from "./pq-table-column"
-import { UpdatePqSheet } from "./update-pq-sheet"
-
-interface DocumentListTableProps {
- promises: Promise<[Awaited<ReturnType<typeof getPQs>>]>
- currentProjectId?: number
-}
-
-export function PqsTable({
- promises,
- currentProjectId
-}: DocumentListTableProps) {
- // 1) 데이터를 가져옴 (server component -> use(...) pattern)
- const [{ data, pageCount }] = React.use(promises)
-
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<PqCriterias> | null>(null)
-
-
- const columns = React.useMemo(
- () => getColumns({ setRowAction }),
- [setRowAction]
- )
-
- // Filter fields
- const filterFields: DataTableFilterField<PqCriterias>[] = []
-
- const advancedFilterFields: DataTableAdvancedFilterField<PqCriterias>[] = [
- {
- id: "code",
- label: "Code",
- type: "text",
- },
- {
- id: "checkPoint",
- label: "Check Point",
- type: "text",
- },
- {
- id: "description",
- label: "Description",
- type: "text",
- },
- {
- id: "remarks",
- label: "Remarks",
- type: "text",
- },
- {
- id: "groupName",
- label: "Group Name",
- type: "text",
- },
- {
- id: "createdAt",
- label: "Created at",
- type: "date",
- },
- {
- id: "updatedAt",
- label: "Updated at",
- type: "date",
- },
- ]
-
- // useDataTable 훅으로 react-table 구성
- const { table } = useDataTable({
- data: data, // <-- 여기서 tableData 사용
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- // grouping:['groupName']
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- columnResizeMode: "onEnd",
-
- })
- return (
- <>
- <DataTable table={table} >
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <PqTableToolbarActions table={table} currentProjectId={currentProjectId}/>
- </DataTableAdvancedToolbar>
- </DataTable>
-
- <UpdatePqSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- pq={rowAction?.row.original ?? null}
- />
-
- <DeletePqsDialog
- open={rowAction?.type === "delete"}
- onOpenChange={() => setRowAction(null)}
- pqs={rowAction?.row.original ? [rowAction?.row.original] : []}
- showTrigger={false}
- onSuccess={() => rowAction?.row.toggleSelected(false)}
- />
- </>
- )
-} \ No newline at end of file
diff --git a/lib/pq/table/update-pq-sheet.tsx b/lib/pq/table/update-pq-sheet.tsx
deleted file mode 100644
index 4da3c264..00000000
--- a/lib/pq/table/update-pq-sheet.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Loader, Save } from "lucide-react"
-import { useForm } from "react-hook-form"
-import { toast } from "sonner"
-import { z } from "zod"
-import { useRouter } from "next/navigation"
-
-import { Button } from "@/components/ui/button"
-import {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-
-import { modifyPq } from "../service"
-import { groupOptions } from "./add-pq-dialog"
-
-// PQ 수정을 위한 Zod 스키마 정의
-const updatePqSchema = z.object({
- code: z.string().min(1, "Code is required"),
- checkPoint: z.string().min(1, "Check point is required"),
- groupName: z.string().min(1, "Group is required"),
- description: z.string().optional(),
- remarks: z.string().optional()
-});
-
-type UpdatePqSchema = z.infer<typeof updatePqSchema>;
-
-interface UpdatePqSheetProps
- extends React.ComponentPropsWithRef<typeof Sheet> {
- pq: {
- id: number;
- code: string;
- checkPoint: string;
- description: string | null;
- remarks: string | null;
- groupName: string | null;
- } | null
-}
-
-export function UpdatePqSheet({ pq, ...props }: UpdatePqSheetProps) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
- const router = useRouter()
-
- const form = useForm<UpdatePqSchema>({
- resolver: zodResolver(updatePqSchema),
- defaultValues: {
- code: pq?.code ?? "",
- checkPoint: pq?.checkPoint ?? "",
- groupName: pq?.groupName ?? groupOptions[0],
- description: pq?.description ?? "",
- remarks: pq?.remarks ?? "",
- },
- })
-
- // 폼 초기화 (pq가 변경될 때)
- React.useEffect(() => {
- if (pq) {
- form.reset({
- code: pq.code,
- checkPoint: pq.checkPoint,
- groupName: pq.groupName ?? groupOptions[0],
- description: pq.description ?? "",
- remarks: pq.remarks ?? "",
- });
- }
- }, [pq, form]);
-
- function onSubmit(input: UpdatePqSchema) {
- startUpdateTransition(async () => {
- if (!pq) return
-
- const result = await modifyPq({
- id: pq.id,
- ...input,
- })
-
- if (!result.success && 'error' in result) {
- toast.error(result.error)
- } else {
- toast.error("Failed to update PQ criteria")
- }
-
- form.reset()
- props.onOpenChange?.(false)
- toast.success("PQ criteria updated successfully")
- router.refresh()
- })
- }
- return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-md">
- <SheetHeader className="text-left">
- <SheetTitle>Update PQ Criteria</SheetTitle>
- <SheetDescription>
- Update the PQ criteria details and save the changes
- </SheetDescription>
- </SheetHeader>
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-4"
- >
- {/* Code 필드 */}
- <FormField
- control={form.control}
- name="code"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Code <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="예: 1-1, A.2.3"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Check Point 필드 */}
- <FormField
- control={form.control}
- name="checkPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Check Point <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="검증 항목을 입력하세요"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Group Name 필드 (Select) */}
- <FormField
- control={form.control}
- name="groupName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Group <span className="text-destructive">*</span></FormLabel>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- value={field.value}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="그룹을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {groupOptions.map((group) => (
- <SelectItem key={group} value={group}>
- {group}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Description 필드 */}
- <FormField
- control={form.control}
- name="description"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Description</FormLabel>
- <FormControl>
- <Textarea
- placeholder="상세 설명을 입력하세요"
- className="min-h-[120px] whitespace-pre-wrap"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormDescription>
- 줄바꿈이 필요한 경우 Enter 키를 누르세요. 입력한 대로 저장됩니다.
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Remarks 필드 */}
- <FormField
- control={form.control}
- name="remarks"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Remarks</FormLabel>
- <FormControl>
- <Textarea
- placeholder="비고 사항을 입력하세요"
- className="min-h-[80px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <SheetFooter className="gap-2 pt-2 sm:space-x-0">
- <SheetClose asChild>
- <Button
- type="button"
- variant="outline"
- onClick={() => form.reset()}
- >
- Cancel
- </Button>
- </SheetClose>
- <Button disabled={isUpdatePending}>
- {isUpdatePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- <Save className="mr-2 size-4" /> Save
- </Button>
- </SheetFooter>
- </form>
- </Form>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file