From 18954df6565108a469fb1608ea3715dd9bb1b02d Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 1 Sep 2025 09:12:09 +0000
Subject: (대표님) 구매 기본계약, gtc 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gtc-vendor/generate-variable-names-dialog.tsx  | 348 +++++++++++++++++++++
 1 file changed, 348 insertions(+)
 create mode 100644 lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx

(limited to 'lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx')

diff --git a/lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx b/lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx
new file mode 100644
index 00000000..ef4ed9f9
--- /dev/null
+++ b/lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx
@@ -0,0 +1,348 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Badge } from "@/components/ui/badge"
+import { Switch } from "@/components/ui/switch"
+
+import {
+  Form,
+  FormControl,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+  FormDescription,
+} from "@/components/ui/form"
+import { Loader, Wand2, Info, Eye } from "lucide-react"
+import { toast } from "sonner"
+
+import { generateVariableNamesSchema, type GenerateVariableNamesSchema } from "@/lib/gtc-contract/gtc-clauses/validations"
+import { generateVariableNames, getGtcClausesTree } from "@/lib/gtc-contract/gtc-clauses/service"
+import { type GtcClauseTreeView } from "@/db/schema/gtc"
+import { useSession } from "next-auth/react"
+
+interface GenerateVariableNamesDialogProps
+  extends React.ComponentPropsWithRef<typeof Dialog> {
+  documentId: number
+  document: any
+}
+
+export function GenerateVariableNamesDialog({ 
+  documentId, 
+  document,
+  ...props 
+}: GenerateVariableNamesDialogProps) {
+  const [isGenerating, startGenerating] = React.useTransition()
+  const [clauses, setClauses] = React.useState<GtcClauseTreeView[]>([])
+  const [previewClauses, setPreviewClauses] = React.useState<GtcClauseTreeView[]>([])
+  const [showPreview, setShowPreview] = React.useState(false)
+  const { data: session } = useSession()
+
+  const currentUserId = React.useMemo(() => {
+    return session?.user?.id ? Number(session.user.id) : null
+  }, [session])
+
+  React.useEffect(() => {
+    if (props.open && documentId) {
+      loadClauses()
+    }
+  }, [props.open, documentId])
+
+  const loadClauses = async () => {
+    try {
+      const tree = await getGtcClausesTree(documentId)
+      const flatClauses = flattenTree(tree)
+      setClauses(flatClauses)
+    } catch (error) {
+      console.error("Error loading clauses:", error)
+    }
+  }
+
+  const form = useForm<GenerateVariableNamesSchema>({
+    resolver: zodResolver(generateVariableNamesSchema),
+    defaultValues: {
+      documentId,
+      prefix: "CLAUSE",
+      includeVendorCode: false,
+      vendorCode: "",
+    },
+  })
+
+  const watchedPrefix = form.watch("prefix")
+  const watchedIncludeVendorCode = form.watch("includeVendorCode")
+  const watchedVendorCode = form.watch("vendorCode")
+
+  // 미리보기 생성
+  React.useEffect(() => {
+    if (clauses.length > 0) {
+      generatePreview()
+    }
+  }, [clauses, watchedPrefix, watchedIncludeVendorCode, watchedVendorCode])
+
+  const generatePreview = () => {
+    const basePrefix = watchedIncludeVendorCode && watchedVendorCode
+      ? `${watchedVendorCode}_${watchedPrefix}`
+      : watchedPrefix
+
+      console.log(basePrefix,"basePrefix")
+
+    const updated = clauses.slice(0, 5).map(clause => {
+      console.log(clause.fullPath,"clause.fullPath")
+
+      const pathPrefix = clause.fullPath?.replace(/\./g, "_") || clause.itemNumber.replace(/\./g, "_")
+      const varPrefix = `${basePrefix}_${pathPrefix}`
+
+      return {
+        ...clause,
+        previewNumberVar: `${varPrefix}_NUMBER`,
+        previewSubtitleVar: `${varPrefix}_SUBTITLE`,
+        previewContentVar: `${varPrefix}_CONTENT`,
+      }
+    })
+
+    setPreviewClauses(updated as any)
+  }
+
+  async function onSubmit(data: GenerateVariableNamesSchema) {
+    startGenerating(async () => {
+      if (!currentUserId) {
+        toast.error("로그인이 필요합니다")
+        return
+      }
+
+      try {
+        const result = await generateVariableNames({
+          ...data,
+          updatedById: currentUserId
+        })
+        
+        if (result.error) {
+          toast.error(`에러: ${result.error}`)
+          return
+        }
+
+        form.reset()
+        props.onOpenChange?.(false)
+        toast.success("PDFTron 변수명이 생성되었습니다.")
+      } catch (error) {
+        toast.error("변수명 생성 중 오류가 발생했습니다.")
+      }
+    })
+  }
+
+  function handleDialogOpenChange(nextOpen: boolean) {
+    if (!nextOpen) {
+      form.reset()
+      setShowPreview(false)
+    }
+    props.onOpenChange?.(nextOpen)
+  }
+
+  const clausesWithoutVariables = clauses.filter(clause => !clause.hasAllVariableNames)
+  const clausesWithVariables = clauses.filter(clause => clause.hasAllVariableNames)
+
+  return (
+    <Dialog {...props} onOpenChange={handleDialogOpenChange}>
+      <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <Wand2 className="h-5 w-5" />
+            PDFTron 변수명 자동 생성
+          </DialogTitle>
+          <DialogDescription>
+            문서의 모든 조항에 대해 PDFTron 변수명을 자동으로 생성합니다.
+          </DialogDescription>
+        </DialogHeader>
+
+        {/* 문서 및 조항 현황 */}
+        <div className="space-y-4 p-4 bg-muted/50 rounded-lg">
+          <div className="flex items-center gap-2">
+            <Info className="h-4 w-4 text-muted-foreground" />
+            <span className="text-sm font-medium">문서 및 조항 현황</span>
+          </div>
+          
+          <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
+            <div>
+              <div className="font-medium text-muted-foreground mb-1">문서 타입</div>
+              <Badge variant={document?.type === "standard" ? "default" : "secondary"}>
+                {document?.type === "standard" ? "표준" : "프로젝트"}
+              </Badge>
+            </div>
+            
+            <div>
+              <div className="font-medium text-muted-foreground mb-1">총 조항 수</div>
+              <div>{clauses.length}개</div>
+            </div>
+            
+            <div>
+              <div className="font-medium text-muted-foreground mb-1">변수명 설정 완료</div>
+              <Badge variant="default">{clausesWithVariables.length}개</Badge>
+            </div>
+            
+            <div>
+              <div className="font-medium text-muted-foreground mb-1">변수명 미설정</div>
+              <Badge variant="destructive">{clausesWithoutVariables.length}개</Badge>
+            </div>
+          </div>
+
+          {document?.project && (
+            <div className="text-sm">
+              <span className="font-medium text-muted-foreground">프로젝트: </span>
+              {document.project.name} ({document.project.code})
+            </div>
+          )}
+        </div>
+
+        <Form {...form}>
+          <form onSubmit={form.handleSubmit(onSubmit)}>
+            <div className="space-y-4">
+              {/* 기본 접두사 */}
+              <FormField
+                control={form.control}
+                name="prefix"
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>기본 접두사</FormLabel>
+                    <FormControl>
+                      <Input 
+                        placeholder="CLAUSE"
+                        {...field} 
+                      />
+                    </FormControl>
+                    <FormDescription>
+                      모든 변수명의 시작에 사용될 접두사입니다.
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              {/* 벤더 코드 포함 여부 */}
+              <FormField
+                control={form.control}
+                name="includeVendorCode"
+                render={({ field }) => (
+                  <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                    <div className="space-y-0.5">
+                      <FormLabel className="text-base">벤더 코드 포함</FormLabel>
+                      <FormDescription>
+                        변수명에 벤더 코드를 포함시킵니다. (벤더별 GTC 용)
+                      </FormDescription>
+                    </div>
+                    <FormControl>
+                      <Switch
+                        checked={field.value}
+                        onCheckedChange={field.onChange}
+                      />
+                    </FormControl>
+                  </FormItem>
+                )}
+              />
+
+              {/* 벤더 코드 입력 */}
+              {watchedIncludeVendorCode && (
+                <FormField
+                  control={form.control}
+                  name="vendorCode"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>벤더 코드</FormLabel>
+                      <FormControl>
+                        <Input 
+                          placeholder="예: VENDOR_A, ABC_CORP 등"
+                          {...field} 
+                        />
+                      </FormControl>
+                      <FormDescription>
+                        변수명에 포함될 벤더 식별 코드입니다.
+                      </FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+                )}
+              />
+
+              {/* 미리보기 토글 */}
+              <div className="flex items-center justify-between">
+                <Button
+                  type="button"
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setShowPreview(!showPreview)}
+                >
+                  <Eye className="mr-2 h-4 w-4" />
+                  {showPreview ? "미리보기 숨기기" : "미리보기 보기"}
+                </Button>
+              </div>
+
+              {/* 미리보기 */}
+              {showPreview && (
+                <div className="space-y-3 p-4 border rounded-lg bg-muted/30">
+                  <div className="text-sm font-medium">변수명 미리보기 (상위 5개 조항)</div>
+                  <div className="space-y-2 max-h-64 overflow-y-auto">
+                    {previewClauses.map((clause: any) => (
+                      <div key={clause.id} className="p-2 bg-background rounded border text-xs">
+                        <div className="flex items-center gap-2 mb-1">
+                          <Badge variant="outline">{clause.itemNumber}</Badge>
+                          <span className="font-medium truncate">{clause.subtitle}</span>
+                        </div>
+                        <div className="space-y-1 text-muted-foreground">
+                          <div>채번: <code className="text-foreground">{clause.previewNumberVar}</code></div>
+                          <div>소제목: <code className="text-foreground">{clause.previewSubtitleVar}</code></div>
+                          <div>상세항목: <code className="text-foreground">{clause.previewContentVar}</code></div>
+                        </div>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              )}
+            </div>
+
+            <DialogFooter className="mt-6">
+              <Button
+                type="button"
+                variant="outline"
+                onClick={() => props.onOpenChange?.(false)}
+                disabled={isGenerating}
+              >
+                Cancel
+              </Button>
+              <Button type="submit" disabled={isGenerating}>
+                {isGenerating && (
+                  <Loader
+                    className="mr-2 size-4 animate-spin"
+                    aria-hidden="true"
+                  />
+                )}
+                Generate Variables
+              </Button>
+            </DialogFooter>
+          </form>
+        </Form>
+      </DialogContent>
+    </Dialog>
+  )
+}
+
+// 트리를 평면 배열로 변환하는 유틸리티 함수
+function flattenTree(tree: any[]): any[] {
+  const result: any[] = []
+  
+  function traverse(nodes: any[]) {
+    for (const node of nodes) {
+      result.push(node)
+      if (node.children && node.children.length > 0) {
+        traverse(node.children)
+      }
+    }
+  }
+  
+  traverse(tree)
+  return result
+}
\ No newline at end of file
-- 
cgit v1.2.3

