diff options
Diffstat (limited to 'lib/gtc-contract/gtc-clauses/table/generate-variable-names-dialog.tsx')
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/table/generate-variable-names-dialog.tsx | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/lib/gtc-contract/gtc-clauses/table/generate-variable-names-dialog.tsx b/lib/gtc-contract/gtc-clauses/table/generate-variable-names-dialog.tsx new file mode 100644 index 00000000..ef4ed9f9 --- /dev/null +++ b/lib/gtc-contract/gtc-clauses/table/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 |
