diff options
Diffstat (limited to 'lib/gtc-contract/status')
| -rw-r--r-- | lib/gtc-contract/status/clone-gtc-document-dialog.tsx | 383 | ||||
| -rw-r--r-- | lib/gtc-contract/status/create-gtc-document-dialog.tsx | 98 | ||||
| -rw-r--r-- | lib/gtc-contract/status/gtc-contract-table.tsx | 13 | ||||
| -rw-r--r-- | lib/gtc-contract/status/gtc-documents-table-columns.tsx | 35 |
4 files changed, 493 insertions, 36 deletions
diff --git a/lib/gtc-contract/status/clone-gtc-document-dialog.tsx b/lib/gtc-contract/status/clone-gtc-document-dialog.tsx new file mode 100644 index 00000000..1e56f2f7 --- /dev/null +++ b/lib/gtc-contract/status/clone-gtc-document-dialog.tsx @@ -0,0 +1,383 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +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 { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover" +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, + CommandEmpty, +} from "@/components/ui/command" +import { Check, ChevronsUpDown, Loader, Copy } from "lucide-react" +import { cn } from "@/lib/utils" +import { toast } from "sonner" + +import { cloneGtcDocumentSchema, type CloneGtcDocumentSchema } from "@/lib/gtc-contract/validations" +import { cloneGtcDocument, getAvailableProjectsForGtcExcluding, hasStandardGtcDocument } from "@/lib/gtc-contract/service" +import { type ProjectForFilter } from "@/lib/gtc-contract/service" +import { type GtcDocumentWithRelations } from "@/db/schema/gtc" +import { useSession } from "next-auth/react" +import { Input } from "@/components/ui/input" +import { useRouter } from "next/navigation" + +interface CloneGtcDocumentDialogProps { + sourceDocument: GtcDocumentWithRelations + open?: boolean + onOpenChange?: (open: boolean) => void +} + +export function CloneGtcDocumentDialog({ + sourceDocument, + open: controlledOpen, + onOpenChange: controlledOnOpenChange +}: CloneGtcDocumentDialogProps) { + const [internalOpen, setInternalOpen] = React.useState(false) + const [projects, setProjects] = React.useState<ProjectForFilter[]>([]) + const [isClonePending, startCloneTransition] = React.useTransition() + const { data: session } = useSession() + const router = useRouter() + const [defaultType, setDefaultType] = React.useState<"standard" | "project">("standard") + + const isControlled = controlledOpen !== undefined + const open = isControlled ? controlledOpen! : internalOpen + const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen + + const currentUserId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null + }, [session]) + + + + + const form = useForm<CloneGtcDocumentSchema>({ + resolver: zodResolver(cloneGtcDocumentSchema), + defaultValues: { + sourceDocumentId: sourceDocument.id, + type: sourceDocument.type, + projectId: sourceDocument.projectId, + title: sourceDocument.title || "", + editReason: "", + }, + }) + + const resetForm = React.useCallback((type: "standard" | "project") => { + form.reset({ + sourceDocumentId: sourceDocument.id, + type, + projectId: sourceDocument.projectId, + title: sourceDocument.title || "", + editReason: "", + }) + }, [form, sourceDocument]) + + React.useEffect(() => { + if (open) { + // 표준 GTC 존재 여부와 사용 가능한 프로젝트 동시 조회 + Promise.all([ + hasStandardGtcDocument(), + getAvailableProjectsForGtcExcluding(sourceDocument.projectId || undefined) + ]).then(([hasStandard, availableProjects]) => { + const initialType = hasStandard ? "project" : "standard" + setDefaultType(initialType) + setProjects(availableProjects) + + // 폼 기본값 설정: 원본 문서 타입을 우선으로 하되, 표준이 이미 있고 원본도 표준이면 프로젝트로 변경 + const targetType = hasStandard && sourceDocument.type === "standard" ? "project" : sourceDocument.type + resetForm(targetType) + }) + } + }, [open, sourceDocument.projectId, sourceDocument.type, resetForm]) + + + const watchedType = form.watch("type") + + React.useEffect(() => { + // 소스 문서가 변경되면 폼 기본값 업데이트 (다이얼로그가 열려있을 때만) + if (open) { + hasStandardGtcDocument().then((hasStandard) => { + const targetType = hasStandard && sourceDocument.type === "standard" ? "project" : sourceDocument.type + resetForm(targetType) + }) + } + }, [sourceDocument, resetForm, open]) + + async function onSubmit(data: CloneGtcDocumentSchema) { + startCloneTransition(async () => { + if (!currentUserId) { + toast.error("로그인이 필요합니다") + return + } + + try { + const result = await cloneGtcDocument({ + ...data, + createdById: currentUserId + }) + + if (result.error) { + toast.error(`에러: ${result.error}`) + return + } + + resetForm(sourceDocument.type) + setOpen(false) + router.refresh() + + toast.success("GTC 문서가 복제되었습니다.") + } catch (error) { + toast.error("문서 복제 중 오류가 발생했습니다.") + } + }) + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + // 다이얼로그 닫을 때는 원본 문서 정보로 리셋 + resetForm(sourceDocument.type) + } + setOpen(nextOpen) + } + + const DialogWrapper = isControlled ? React.Fragment : Dialog + + return ( + <DialogWrapper> + {!isControlled && ( + <DialogTrigger asChild> + <Button variant="outline" size="sm"> + <Copy className="mr-2 h-4 w-4" /> + 복제하기 + </Button> + </DialogTrigger> + )} + + <Dialog open={open} onOpenChange={handleDialogOpenChange}> + <DialogContent className="max-w-md"> + <DialogHeader> + <DialogTitle>GTC 문서 복제</DialogTitle> + <DialogDescription> + 기존 문서를 복제하여 새로운 문서를 생성합니다. <br /> + <span className="font-medium text-foreground"> + 원본: {sourceDocument.title || `${sourceDocument.type === 'standard' ? '표준' : '프로젝트'} GTC v${sourceDocument.revision}`} + </span> + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)}> + <div className="space-y-4 py-4"> + {/* 구분 (Type) */} + <FormField + control={form.control} + name="type" + render={({ field }) => ( + <FormItem> + <FormLabel>구분</FormLabel> + <FormControl> + <Select + onValueChange={(value) => { + field.onChange(value) + // 표준으로 변경시 프로젝트 ID 초기화 + if (value === "standard") { + form.setValue("projectId", null) + } + }} + value={field.value} + > + <SelectTrigger> + <SelectValue placeholder="구분을 선택하세요" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="standard">표준</SelectItem> + <SelectItem value="project">프로젝트</SelectItem> + </SelectContent> + </Select> + </FormControl> + {defaultType === "project" && sourceDocument.type === "standard" && ( + <FormDescription> + 표준 GTC 문서가 이미 존재합니다. 복제시에는 프로젝트 타입을 권장합니다. + </FormDescription> + )} + <FormMessage /> + </FormItem> + )} + /> + + {/* 프로젝트 선택 (프로젝트 타입인 경우만) */} + {watchedType === "project" && ( + <FormField + control={form.control} + name="projectId" + render={({ field }) => { + const selectedProject = projects.find( + (p) => p.id === field.value + ) + const [popoverOpen, setPopoverOpen] = React.useState(false) + + return ( + <FormItem> + <FormLabel>프로젝트</FormLabel> + <FormControl> + <Popover + open={popoverOpen} + onOpenChange={setPopoverOpen} + modal={true} + > + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={popoverOpen} + className="w-full justify-between" + > + {selectedProject + ? `${selectedProject.name} (${selectedProject.code})` + : "프로젝트를 선택하세요..."} + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + + <PopoverContent className="w-full p-0"> + <Command> + <CommandInput + placeholder="프로젝트 검색..." + className="h-9" + /> + <CommandList> + <CommandEmpty> + {projects.length === 0 + ? "사용 가능한 프로젝트가 없습니다." + : "프로젝트를 찾을 수 없습니다." + } + </CommandEmpty> + <CommandGroup> + {projects.map((project) => { + const label = `${project.name} (${project.code})` + return ( + <CommandItem + key={project.id} + value={label} + onSelect={() => { + field.onChange(project.id) + setPopoverOpen(false) + }} + > + {label} + <Check + className={cn( + "ml-auto h-4 w-4", + selectedProject?.id === project.id + ? "opacity-100" + : "opacity-0" + )} + /> + </CommandItem> + ) + })} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + </FormControl> + <FormMessage /> + </FormItem> + ) + }} + /> + )} + + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>GTC 제목 (선택사항)</FormLabel> + <FormControl> + <Input + placeholder="GTC 제목을 입력하세요..." + {...field} + /> + </FormControl> + <FormDescription> + 워드의 제목으로 사용됩니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 편집 사유 */} + <FormField + control={form.control} + name="editReason" + render={({ field }) => ( + <FormItem> + <FormLabel>복제 사유 (선택사항)</FormLabel> + <FormControl> + <Textarea + placeholder="복제 사유를 입력하세요..." + {...field} + rows={3} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isClonePending} + > + Cancel + </Button> + <Button type="submit" disabled={isClonePending}> + {isClonePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + 복제하기 + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + </DialogWrapper> + ) +}
\ No newline at end of file diff --git a/lib/gtc-contract/status/create-gtc-document-dialog.tsx b/lib/gtc-contract/status/create-gtc-document-dialog.tsx index 003e4d51..174bb8dd 100644 --- a/lib/gtc-contract/status/create-gtc-document-dialog.tsx +++ b/lib/gtc-contract/status/create-gtc-document-dialog.tsx @@ -41,28 +41,45 @@ import { cn } from "@/lib/utils" import { toast } from "sonner" import { createGtcDocumentSchema, type CreateGtcDocumentSchema } from "@/lib/gtc-contract/validations" -import { createGtcDocument, getProjectsForSelect } from "@/lib/gtc-contract/service" -import { type Project } from "@/db/schema/projects" +import { createGtcDocument, getAvailableProjectsForGtc, hasStandardGtcDocument } from "@/lib/gtc-contract/service" +import { type ProjectForFilter } from "@/lib/gtc-contract/service" import { useSession } from "next-auth/react" import { Input } from "@/components/ui/input" -import { useRouter } from "next/navigation"; +import { useRouter } from "next/navigation" export function CreateGtcDocumentDialog() { const [open, setOpen] = React.useState(false) - const [projects, setProjects] = React.useState<Project[]>([]) + const [projects, setProjects] = React.useState<ProjectForFilter[]>([]) const [isCreatePending, startCreateTransition] = React.useTransition() + const [defaultType, setDefaultType] = React.useState<"standard" | "project">("standard") const { data: session } = useSession() - const router = useRouter(); + const router = useRouter() const currentUserId = React.useMemo(() => { return session?.user?.id ? Number(session.user.id) : null; - }, [session]); - + }, [session]) React.useEffect(() => { if (open) { - getProjectsForSelect().then((res) => { - setProjects(res) + // 표준 GTC 존재 여부와 사용 가능한 프로젝트 동시 조회 + Promise.all([ + hasStandardGtcDocument(), + getAvailableProjectsForGtc() + ]).then(([hasStandard, availableProjects]) => { + const initialType = hasStandard ? "project" : "standard" + setDefaultType(initialType) + setProjects(availableProjects) + + // 폼 기본값 설정 (setTimeout으로 다음 틱에 실행) + setTimeout(() => { + form.reset({ + type: initialType, + projectId: null, + title: "", + revision: 0, + editReason: "", + }) + }, 0) }) } }, [open]) @@ -78,11 +95,21 @@ export function CreateGtcDocumentDialog() { }, }) + const resetForm = React.useCallback((type: "standard" | "project") => { + form.reset({ + type, + projectId: null, + title: "", + revision: 0, + editReason: "", + }) + }, [form]) + + const watchedType = form.watch("type") async function onSubmit(data: CreateGtcDocumentSchema) { startCreateTransition(async () => { - if (!currentUserId) { toast.error("로그인이 필요합니다") return @@ -99,9 +126,9 @@ export function CreateGtcDocumentDialog() { return } - form.reset() + resetForm(defaultType) setOpen(false) - router.refresh(); + router.refresh() toast.success("GTC 문서가 생성되었습니다.") } catch (error) { @@ -112,7 +139,7 @@ export function CreateGtcDocumentDialog() { function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { - form.reset() + resetForm(defaultType) } setOpen(nextOpen) } @@ -122,15 +149,15 @@ export function CreateGtcDocumentDialog() { <DialogTrigger asChild> <Button variant="default" size="sm"> <Plus className="mr-2 h-4 w-4" /> - Add GTC Document + GTC 추가 </Button> </DialogTrigger> <DialogContent className="max-w-md"> <DialogHeader> - <DialogTitle>Create New GTC Document</DialogTitle> + <DialogTitle>새 GTC 만들기</DialogTitle> <DialogDescription> - 새 GTC 문서 정보를 입력하고 <b>Create</b> 버튼을 누르세요. + 새 GTC 문서 정보를 입력하고 <b>생성</b> 버튼을 누르세요. </DialogDescription> </DialogHeader> @@ -159,11 +186,26 @@ export function CreateGtcDocumentDialog() { <SelectValue placeholder="구분을 선택하세요" /> </SelectTrigger> <SelectContent> - <SelectItem value="standard">표준</SelectItem> - <SelectItem value="project">프로젝트</SelectItem> + {/* 기본값에 따라 순서 조정 */} + {defaultType === "project" ? ( + <> + <SelectItem value="project">프로젝트</SelectItem> + <SelectItem value="standard">표준</SelectItem> + </> + ) : ( + <> + <SelectItem value="standard">표준</SelectItem> + <SelectItem value="project">프로젝트</SelectItem> + </> + )} </SelectContent> </Select> </FormControl> + {defaultType === "project" && ( + <FormDescription> + 표준 GTC 문서가 이미 존재합니다. 새 문서는 프로젝트 타입을 권장합니다. + </FormDescription> + )} <FormMessage /> </FormItem> )} @@ -198,7 +240,9 @@ export function CreateGtcDocumentDialog() { > {selectedProject ? `${selectedProject.name} (${selectedProject.code})` - : "프로젝트를 선택하세요..."} + : projects.length === 0 + ? "사용 가능한 프로젝트가 없습니다" + : "프로젝트를 선택하세요..."} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> @@ -210,7 +254,12 @@ export function CreateGtcDocumentDialog() { className="h-9" /> <CommandList> - <CommandEmpty>프로젝트를 찾을 수 없습니다.</CommandEmpty> + <CommandEmpty> + {projects.length === 0 + ? "모든 프로젝트에 이미 GTC 문서가 있습니다." + : "프로젝트를 찾을 수 없습니다." + } + </CommandEmpty> <CommandGroup> {projects.map((project) => { const label = `${project.name} (${project.code})` @@ -241,6 +290,9 @@ export function CreateGtcDocumentDialog() { </PopoverContent> </Popover> </FormControl> + <FormDescription> + {projects.length === 0 && "이미 GTC 문서가 있는 프로젝트는 표시되지 않습니다."} + </FormDescription> <FormMessage /> </FormItem> ) @@ -256,7 +308,7 @@ export function CreateGtcDocumentDialog() { <FormLabel>GTC 제목 (선택사항)</FormLabel> <FormControl> <Input - placeholder="GTC 제목를 입력하세요..." + placeholder="GTC 제목을 입력하세요..." {...field} /> </FormControl> @@ -295,7 +347,7 @@ export function CreateGtcDocumentDialog() { onClick={() => setOpen(false)} disabled={isCreatePending} > - Cancel + 취소 </Button> <Button type="submit" disabled={isCreatePending}> {isCreatePending && ( @@ -304,7 +356,7 @@ export function CreateGtcDocumentDialog() { aria-hidden="true" /> )} - Create + 생성 </Button> </DialogFooter> </form> diff --git a/lib/gtc-contract/status/gtc-contract-table.tsx b/lib/gtc-contract/status/gtc-contract-table.tsx index 0fb637b6..ce3a2c7a 100644 --- a/lib/gtc-contract/status/gtc-contract-table.tsx +++ b/lib/gtc-contract/status/gtc-contract-table.tsx @@ -27,6 +27,7 @@ import { UpdateGtcDocumentSheet } from "./update-gtc-document-sheet" import { CreateGtcDocumentDialog } from "./create-gtc-document-dialog" import { CreateNewRevisionDialog } from "./create-new-revision-dialog" import { useRouter } from "next/navigation" +import { CloneGtcDocumentDialog } from "./clone-gtc-document-dialog" interface GtcDocumentsTableProps { promises: Promise< @@ -42,6 +43,8 @@ export function GtcDocumentsTable({ promises }: GtcDocumentsTableProps) { const [{ data, pageCount }, projects, users] = React.use(promises) const router = useRouter() + console.log(data) + const [rowAction, setRowAction] = React.useState<DataTableRowAction<GtcDocumentWithRelations> | null>(null) @@ -169,6 +172,16 @@ export function GtcDocumentsTable({ promises }: GtcDocumentsTableProps) { originalDocument={rowAction?.row.original ?? null} /> + + {/* 복제 다이얼로그 */} + {rowAction?.type === "clone" && ( + <CloneGtcDocumentDialog + sourceDocument={rowAction.row.original} + open={true} + onOpenChange={() => setRowAction(null)} + /> + )} + {/* <CreateGtcDocumentDialog /> */} </> ) diff --git a/lib/gtc-contract/status/gtc-documents-table-columns.tsx b/lib/gtc-contract/status/gtc-documents-table-columns.tsx index cd02a3e5..89415284 100644 --- a/lib/gtc-contract/status/gtc-documents-table-columns.tsx +++ b/lib/gtc-contract/status/gtc-documents-table-columns.tsx @@ -24,7 +24,7 @@ import { type GtcDocumentWithRelations } from "@/db/schema/gtc" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<GtcDocumentWithRelations> | null>> - router: AppRouterInstance // ← 추가 + router: AppRouterInstance } /** GTC Documents 테이블 컬럼 정의 (그룹 헤더 제거) */ @@ -75,12 +75,13 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef accessorKey: "project", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트" />, cell: ({ row }) => { - const project = row.original.project - if (!project) return <span className="text-muted-foreground">-</span> + const projectName = row.original.projectName + const projectCode = row.original.projectCode + if (!projectName) return <span className="text-muted-foreground">-</span> return ( <div className="flex flex-col min-w-0"> - <span className="font-medium truncate">{project.name}</span> - <span className="text-xs text-muted-foreground">{project.code}</span> + <span className="font-medium truncate">{projectName}</span> + <span className="text-xs text-muted-foreground">{projectCode}</span> </div> ) }, @@ -195,6 +196,10 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef setRowAction({ row, type: "createRevision" }) } + const handleClone = () => { + setRowAction({ row, type: "clone" }) + } + return ( <DropdownMenu> <DropdownMenuTrigger asChild> @@ -206,26 +211,30 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef <Ellipsis className="size-4" aria-hidden /> </Button> </DropdownMenuTrigger> - <DropdownMenuContent align="end" className="w-48"> + <DropdownMenuContent align="end" className="w-44"> <DropdownMenuItem onSelect={handleViewDetails}> - <Eye className="mr-2 h-4 w-4" /> - 상세 + {/* <Eye className="mr-2 h-4 w-4" /> */} + 상세보기 </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem onSelect={() => setRowAction({ row, type: "update" })}> - 수정 + 수정하기 </DropdownMenuItem> <DropdownMenuItem onSelect={handleCreateNewRevision}> - 새 리비전 생성 + 리비전 생성하기 + </DropdownMenuItem> + + <DropdownMenuItem onSelect={handleClone}> + 복제하기 </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem onSelect={() => setRowAction({ row, type: "delete" })}> - 삭제 - <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> + 삭제하기 + {/* <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut> */} </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> @@ -241,4 +250,4 @@ export function getColumns({ setRowAction, router }: GetColumnsProps): ColumnDef ...auditColumns, actionsColumn, ] -} +}
\ No newline at end of file |
