summaryrefslogtreecommitdiff
path: root/lib/gtc-contract/status/clone-gtc-document-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gtc-contract/status/clone-gtc-document-dialog.tsx')
-rw-r--r--lib/gtc-contract/status/clone-gtc-document-dialog.tsx383
1 files changed, 383 insertions, 0 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