summaryrefslogtreecommitdiff
path: root/lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx')
-rw-r--r--lib/basic-contract/gtc-vendor/generate-variable-names-dialog.tsx348
1 files changed, 348 insertions, 0 deletions
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