diff options
Diffstat (limited to 'lib/pq/pq-criteria/add-pq-dialog.tsx')
| -rw-r--r-- | lib/pq/pq-criteria/add-pq-dialog.tsx | 829 |
1 files changed, 484 insertions, 345 deletions
diff --git a/lib/pq/pq-criteria/add-pq-dialog.tsx b/lib/pq/pq-criteria/add-pq-dialog.tsx index 660eb360..144e5ce4 100644 --- a/lib/pq/pq-criteria/add-pq-dialog.tsx +++ b/lib/pq/pq-criteria/add-pq-dialog.tsx @@ -1,346 +1,485 @@ -"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 { useToast } from "@/hooks/use-toast"
-import { createPqCriteria } from "../service"
-
-// 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"),
- subGroupName: z.string().optional(),
- description: z.string().optional(),
- remarks: z.string().optional(),
- inputFormat: z.string().default("TEXT"),
-
-});
-
-type CreatePqFormType = z.infer<typeof createPqSchema>;
-
-// 그룹 이름 옵션
-export const groupOptions = [
- "GENERAL",
- "QMS",
- "Warranty",
- "HSE+",
- "기타",
-];
-
-// 입력 형식 옵션
-const inputFormatOptions = [
- { value: "TEXT", label: "텍스트" },
- { value: "FILE", label: "파일" },
- { value: "EMAIL", label: "이메일" },
- { value: "PHONE", label: "전화번호" },
- { value: "FAX", label: "팩스번호" },
- { value: "NUMBER", label: "숫자" },
- { value: "NUMBER_WITH_UNIT", label: "숫자+단위" },
- { value: "TEXT_FILE", label: "텍스트 + 파일" },
-];
-
-interface AddPqDialogProps {
- pqListId: number;
-}
-
-export function AddPqDialog({ pqListId }: AddPqDialogProps) {
- const [open, setOpen] = React.useState(false)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const router = useRouter()
- const { toast } = useToast()
-
- // react-hook-form 설정
- const form = useForm<CreatePqFormType>({
- resolver: zodResolver(createPqSchema),
- defaultValues: {
- code: "",
- checkPoint: "",
- groupName: groupOptions[0],
- subGroupName: "",
- description: "",
- remarks: "",
- inputFormat: "TEXT",
-
- },
- })
- const formState = form.formState
-
- async function onSubmit(data: CreatePqFormType) {
- try {
- setIsSubmitting(true)
-
- // 서버 액션 호출
- const result = await createPqCriteria(pqListId, data)
-
- if (!result.success) {
- toast({
- title: "오류",
- description: result.message || "PQ 항목 생성에 실패했습니다",
- variant: "destructive",
- })
- return
- }
-
- // 성공 시 처리
- toast({
- title: "성공",
- description: result.message || "PQ 항목이 성공적으로 생성되었습니다",
- })
-
- // 모달 닫고 폼 리셋
- form.reset()
- setOpen(false)
-
- // 페이지 새로고침
- router.refresh()
-
- } catch (error) {
- console.error('Error creating PQ criteria:', error)
- toast({
- title: "오류",
- description: "예상치 못한 오류가 발생했습니다",
- variant: "destructive",
- })
- } finally {
- setIsSubmitting(false)
- }
- }
-
- function handleDialogOpenChange(nextOpen: boolean) {
- if (!nextOpen) {
- form.reset()
- }
- setOpen(nextOpen)
- }
-
- 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] max-h-[80vh] flex flex-col">
- <DialogHeader>
- <DialogTitle>PQ 항목 생성</DialogTitle>
- <DialogDescription>
- 새 PQ 항목을 추가합니다.
- </DialogDescription>
- </DialogHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 overflow-auto space-y-4">
- <div className="space-y-4 px-1">
- {/* Group Name 필드 */}
- <FormField
- control={form.control}
- name="groupName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>대분류 <span className="text-destructive">*</span></FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="그룹을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {groupOptions.map((group) => (
- <SelectItem key={group} value={group}>
- {group}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Sub Group Name 필드 */}
- <FormField
- control={form.control}
- name="subGroupName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>소분류</FormLabel>
- <FormControl>
- <Input
- placeholder="서브 그룹명을 입력하세요"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormDescription>
- 세부 분류를 위한 서브 그룹명을 입력하세요 (선택사항)
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* Code 필드 */}
- <FormField
- control={form.control}
- name="code"
- render={({ field }) => (
- <FormItem>
- <FormLabel>일련번호 <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="예: 1-1, A.2.3"
- {...field}
- />
- </FormControl>
- <FormDescription>
- PQ 항목의 고유 코드를 입력하세요
- </FormDescription>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* Check Point 필드 */}
- <FormField
- control={form.control}
- name="checkPoint"
- render={({ field }) => (
- <FormItem>
- <FormLabel>PQ 항목 <span className="text-destructive">*</span></FormLabel>
- <FormControl>
- <Input
- placeholder="PQ 항목을 입력하세요"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Input Format 필드 */}
- <FormField
- control={form.control}
- name="inputFormat"
- render={({ field }) => (
- <FormItem>
- <FormLabel>협력업체 입력사항 <span className="text-destructive">*</span></FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="입력 형식을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {inputFormatOptions.map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Description 필드 */}
- <FormField
- control={form.control}
- name="description"
- render={({ field }) => (
- <FormItem>
- <FormLabel>설명</FormLabel>
- <FormControl>
- <Textarea
- placeholder="상세 설명을 입력하세요"
- className="min-h-[100px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Remarks 필드 */}
- <FormField
- control={form.control}
- name="remarks"
- render={({ field }) => (
- <FormItem>
- <FormLabel>비고</FormLabel>
- <FormControl>
- <Textarea
- placeholder="비고 사항을 입력하세요"
- className="min-h-[80px]"
- {...field}
- value={field.value || ""}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => {
- form.reset();
- setOpen(false);
- }}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isSubmitting || !formState.isValid}
- >
- {isSubmitting ? "생성 중..." : "생성"}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
+"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 { useToast } from "@/hooks/use-toast" +import { createPqCriteria } from "../service" +import { uploadPqCriteriaFileAction } from "@/lib/pq/service" +import { Dropzone, DropzoneInput, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription } from "@/components/ui/dropzone" +import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction } from "@/components/ui/file-list" +import { X, Loader2 } from "lucide-react" + +// 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"), + subGroupName: z.string().optional(), + description: z.string().optional(), + remarks: z.string().optional(), + inputFormat: z.string().default("TEXT"), + type: z.string().optional(), +}); + +type CreatePqFormType = z.infer<typeof createPqSchema>; + +// 그룹 이름 옵션 +export const groupOptions = [ + "GENERAL", + "QMS", + "Warranty", + "HSE+", + "기타", +]; + +// 입력 형식 옵션 +const inputFormatOptions = [ + { value: "TEXT", label: "텍스트" }, + { value: "FILE", label: "파일" }, + { value: "EMAIL", label: "이메일" }, + { value: "PHONE", label: "전화번호" }, + { value: "FAX", label: "팩스번호" }, + { value: "NUMBER", label: "숫자" }, + { value: "NUMBER_WITH_UNIT", label: "숫자+단위" }, + { value: "TEXT_FILE", label: "텍스트 + 파일" }, +]; + +const typeOptions = [ + { value: "내자", label: "내자" }, + { value: "외자", label: "외자" }, + { value: "내외자", label: "내외자" }, +]; + +interface AddPqDialogProps { + pqListId: number; +} + +export function AddPqDialog({ pqListId }: AddPqDialogProps) { + const [open, setOpen] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [isUploading, setIsUploading] = React.useState(false) + const [uploadedFiles, setUploadedFiles] = React.useState< + { fileName: string; url: string; size?: number; originalFileName?: string }[] + >([]) + const router = useRouter() + const { toast } = useToast() + + // react-hook-form 설정 + const form = useForm<CreatePqFormType>({ + resolver: zodResolver(createPqSchema), + defaultValues: { + code: "", + checkPoint: "", + groupName: groupOptions[0], + subGroupName: "", + description: "", + remarks: "", + inputFormat: "TEXT", + type: "내외자", + }, + }) + const formState = form.formState + + async function onSubmit(data: CreatePqFormType) { + try { + setIsSubmitting(true) + + // 서버 액션 호출 + const result = await createPqCriteria(pqListId, { + ...data, + attachments: uploadedFiles, + }) + + if (!result.success) { + toast({ + title: "오류", + description: result.message || "PQ 항목 생성에 실패했습니다", + variant: "destructive", + }) + return + } + + // 성공 시 처리 + toast({ + title: "성공", + description: result.message || "PQ 항목이 성공적으로 생성되었습니다", + }) + + // 모달 닫고 폼 리셋 + form.reset() + setUploadedFiles([]) + setOpen(false) + + // 페이지 새로고침 + router.refresh() + + } catch (error) { + console.error('Error creating PQ criteria:', error) + toast({ + title: "오류", + description: "예상치 못한 오류가 발생했습니다", + variant: "destructive", + }) + } finally { + setIsSubmitting(false) + } + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + setUploadedFiles([]) + } + setOpen(nextOpen) + } + + const handleUpload = async (files: File[]) => { + try { + setIsUploading(true) + for (const file of files) { + const uploaded = await uploadPqCriteriaFileAction(file) + setUploadedFiles((prev) => [...prev, uploaded]) + } + toast({ + title: "업로드 완료", + description: "첨부파일이 업로드되었습니다.", + }) + } catch (error) { + console.error(error) + toast({ + title: "업로드 실패", + description: "첨부파일 업로드 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsUploading(false) + } + } + + 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] max-h-[80vh] flex flex-col"> + <DialogHeader> + <DialogTitle>PQ 항목 생성</DialogTitle> + <DialogDescription> + 새 PQ 항목을 추가합니다. + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex-1 overflow-auto space-y-4"> + <div className="space-y-4 px-1"> + {/* Group Name 필드 */} + <FormField + control={form.control} + name="groupName" + render={({ field }) => ( + <FormItem> + <FormLabel>대분류 <span className="text-destructive">*</span></FormLabel> + <Select onValueChange={field.onChange} defaultValue={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="그룹을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {groupOptions.map((group) => ( + <SelectItem key={group} value={group}> + {group} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* Sub Group Name 필드 */} + <FormField + control={form.control} + name="subGroupName" + render={({ field }) => ( + <FormItem> + <FormLabel>소분류</FormLabel> + <FormControl> + <Input + placeholder="서브 그룹명을 입력하세요" + {...field} + value={field.value || ""} + /> + </FormControl> + <FormDescription> + 세부 분류를 위한 서브 그룹명을 입력하세요 (선택사항) + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + {/* Code 필드 */} + <FormField + control={form.control} + name="code" + render={({ field }) => ( + <FormItem> + <FormLabel>일련번호 <span className="text-destructive">*</span></FormLabel> + <FormControl> + <Input + placeholder="예: 1-1, A.2.3" + {...field} + /> + </FormControl> + <FormDescription> + PQ 항목의 고유 코드를 입력하세요 + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + {/* Check Point 필드 */} + <FormField + control={form.control} + name="checkPoint" + render={({ field }) => ( + <FormItem> + <FormLabel>PQ 항목 <span className="text-destructive">*</span></FormLabel> + <FormControl> + <Input + placeholder="PQ 항목을 입력하세요" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* Type 필드 */} + <FormField + control={form.control} + name="type" + render={({ field }) => ( + <FormItem> + <FormLabel>내/외자 구분</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="구분을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {typeOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormDescription>미선택 시 기본값은 내외자입니다.</FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* Input Format 필드 */} + <FormField + control={form.control} + name="inputFormat" + render={({ field }) => ( + <FormItem> + <FormLabel>협력업체 입력사항 <span className="text-destructive">*</span></FormLabel> + <Select onValueChange={field.onChange} defaultValue={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="입력 형식을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + {inputFormatOptions.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 첨부 파일 업로드 */} + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <FormLabel>첨부 파일</FormLabel> + {isUploading && ( + <div className="flex items-center text-xs text-muted-foreground"> + <Loader2 className="mr-1 h-3 w-3 animate-spin" /> 업로드 중... + </div> + )} + </div> + <Dropzone + maxSize={6e8} + onDropAccepted={(files) => handleUpload(files)} + onDropRejected={() => + toast({ + title: "업로드 실패", + description: "파일 크기/형식을 확인하세요.", + variant: "destructive", + }) + } + disabled={isUploading} + > + {() => ( + <FormItem> + <DropzoneZone className="flex justify-center h-28"> + <FormControl> + <DropzoneInput /> + </FormControl> + <div className="flex items-center gap-4"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle> + <DropzoneDescription>PDF, 이미지, 문서 (최대 600MB)</DropzoneDescription> + </div> + </div> + </DropzoneZone> + <FormDescription>기준 문서 첨부가 필요한 경우 업로드하세요.</FormDescription> + </FormItem> + )} + </Dropzone> + + {uploadedFiles.length > 0 && ( + <div className="space-y-2"> + <p className="text-sm font-medium">첨부된 파일 ({uploadedFiles.length})</p> + <FileList> + {uploadedFiles.map((file, idx) => ( + <FileListItem key={idx}> + <FileListHeader> + <FileListInfo> + <FileListName>{file.originalFileName || file.fileName}</FileListName> + {file.size && ( + <FileListDescription>{`${file.size} bytes`}</FileListDescription> + )} + </FileListInfo> + <FileListAction + onClick={() => + setUploadedFiles((prev) => prev.filter((_, i) => i !== idx)) + } + > + <X className="h-4 w-4" /> + <span className="sr-only">Remove</span> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </div> + )} + </div> + + {/* Description 필드 */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명</FormLabel> + <FormControl> + <Textarea + placeholder="상세 설명을 입력하세요" + className="min-h-[100px]" + {...field} + value={field.value || ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* Remarks 필드 */} + <FormField + control={form.control} + name="remarks" + render={({ field }) => ( + <FormItem> + <FormLabel>비고</FormLabel> + <FormControl> + <Textarea + placeholder="비고 사항을 입력하세요" + className="min-h-[80px]" + {...field} + value={field.value || ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => { + form.reset(); + setOpen(false); + }} + > + 취소 + </Button> + <Button + type="submit" + disabled={isSubmitting || !formState.isValid} + > + {isSubmitting ? "생성 중..." : "생성"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) }
\ No newline at end of file |
