From c228a89c2834ee63b209bad608837c39643f350e Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 28 Jul 2025 11:44:16 +0000 Subject: (대표님) 의존성 docx 추가, basicContract API, gtc(계약일반조건), 벤더평가 esg 평가데이터 내보내기 개선, S-EDP 피드백 대응(CLS_ID, ITEM NO 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gtc-clauses/table/create-gtc-clause-dialog.tsx | 442 +++++++++++++++++++++ 1 file changed, 442 insertions(+) create mode 100644 lib/gtc-contract/gtc-clauses/table/create-gtc-clause-dialog.tsx (limited to 'lib/gtc-contract/gtc-clauses/table/create-gtc-clause-dialog.tsx') diff --git a/lib/gtc-contract/gtc-clauses/table/create-gtc-clause-dialog.tsx b/lib/gtc-contract/gtc-clauses/table/create-gtc-clause-dialog.tsx new file mode 100644 index 00000000..b65e5261 --- /dev/null +++ b/lib/gtc-contract/gtc-clauses/table/create-gtc-clause-dialog.tsx @@ -0,0 +1,442 @@ +"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 { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover" +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, + CommandEmpty, +} from "@/components/ui/command" +import { Check, ChevronsUpDown, Loader, Plus, Info } from "lucide-react" +import { cn } from "@/lib/utils" +import { toast } from "sonner" + +import { createGtcClauseSchema, type CreateGtcClauseSchema } from "@/lib/gtc-contract/gtc-clauses/validations" +import { createGtcClause, getGtcClausesTree } from "@/lib/gtc-contract/gtc-clauses/service" +import { type GtcClauseTreeView } from "@/db/schema/gtc" +import { useSession } from "next-auth/react" +import { MarkdownImageEditor } from "./markdown-image-editor" + +interface ClauseImage { + id: string + url: string + fileName: string + size: number +} + +interface CreateGtcClauseDialogProps { + documentId: number + document: any + parentClause?: GtcClauseTreeView | null + onSuccess?: () => void + open?: boolean + onOpenChange?: (open: boolean) => void + showTrigger?: boolean +} + +export function CreateGtcClauseDialog({ + documentId, + document, + parentClause = null, + onSuccess, + open: controlledOpen, + onOpenChange: controlledOnOpenChange, + showTrigger = true +}: CreateGtcClauseDialogProps) { + const [internalOpen, setInternalOpen] = React.useState(false) + + // controlled vs uncontrolled 모드 + const isControlled = controlledOpen !== undefined + const open = isControlled ? controlledOpen : internalOpen + const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen + const [parentClauses, setParentClauses] = React.useState([]) + const [isCreatePending, startCreateTransition] = React.useTransition() + const { data: session } = useSession() + + // ✅ 이미지 상태 추가 + const [images, setImages] = React.useState([]) + + const currentUserId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null + }, [session]) + + React.useEffect(() => { + if (open) { + loadParentClauses() + } + }, [open, documentId]) + + const loadParentClauses = async () => { + try { + const tree = await getGtcClausesTree(documentId) + setParentClauses(flattenTree(tree)) + } catch (error) { + console.error("Error loading parent clauses:", error) + } + } + + const form = useForm({ + resolver: zodResolver(createGtcClauseSchema), + defaultValues: { + documentId, + parentId: parentClause?.id || null, + itemNumber: "", + category: "", + subtitle: "", + content: "", + sortOrder: 0, + editReason: "", + }, + }) + + // ✅ 이미지와 콘텐츠 변경 핸들러 + const handleContentImageChange = (content: string, newImages: ClauseImage[]) => { + form.setValue("content", content) + setImages(newImages) + } + + async function onSubmit(data: CreateGtcClauseSchema) { + startCreateTransition(async () => { + if (!currentUserId) { + toast.error("로그인이 필요합니다") + return + } + + try { + // ✅ 이미지 데이터도 함께 전송 + const result = await createGtcClause({ + ...data, + images: images, // 이미지 배열 추가 + createdById: currentUserId + }) + + if (result.error) { + toast.error(`에러: ${result.error}`) + return + } + + form.reset() + setImages([]) // ✅ 이미지 상태 초기화 + setOpen(false) + toast.success("GTC 조항이 생성되었습니다.") + onSuccess?.() + } catch (error) { + toast.error("조항 생성 중 오류가 발생했습니다.") + } + }) + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + setImages([]) // ✅ 다이얼로그 닫을 때 이미지 상태 초기화 + } + setOpen(nextOpen) + } + + const selectedParent = parentClauses.find(c => c.id === form.watch("parentId")) + + return ( + + {showTrigger && ( + + + + )} + + {/* ✅ 너비 확장 */} + + + {parentClause ? "하위 조항 생성" : "새 조항 생성"} + + + 새 GTC 조항 정보를 입력하고 Create 버튼을 누르세요. 이미지를 포함할 수 있습니다. + + + + {/* 문서 정보 표시 */} +
+
문서 정보
+
+
구분: {document?.type === "standard" ? "표준" : "프로젝트"}
+ {document?.project && ( +
프로젝트: {document.project.name} ({document.project.code})
+ )} +
리비전: v{document?.revision}
+
+
+ +
+ + {/* 스크롤 가능한 폼 내용 영역 */} +
+
+ {/* 부모 조항 선택 */} + { + const [popoverOpen, setPopoverOpen] = React.useState(false) + + return ( + + 부모 조항 (선택사항) + + + + + + + + + + + 조항을 찾을 수 없습니다. + + {/* 최상위 조항 옵션 */} + { + field.onChange(null) + setPopoverOpen(false) + }} + > + 최상위 조항 + + + + {parentClauses.map((clause) => { + const label = `${clause.itemNumber} - ${clause.subtitle}` + return ( + { + field.onChange(clause.id) + setPopoverOpen(false) + }} + > +
+ + {label} + +
+ +
+ ) + })} +
+
+
+
+
+
+ +
+ ) + }} + /> + + {/* 채번 */} + ( + + 채번 * + + + + + 조항의 번호입니다. 영문, 숫자, 점(.), 하이픈(-), 언더스코어(_)를 사용할 수 있습니다. + + + + )} + /> + + {/* 분류 */} + ( + + 분류 (선택사항) + + + + + + )} + /> + + {/* 소제목 */} + ( + + 소제목 * + + + + + 조항의 제목입니다. 문서에서 헤더로 표시됩니다. + + + + )} + /> + + {/* ✅ 상세항목 - MarkdownImageEditor 사용 */} + ( + + 상세항목 (선택사항) + + + + + 조항의 실제 내용입니다. 텍스트와 이미지를 조합할 수 있으며, 하위 조항들을 그룹핑하는 제목용 조항인 경우 비워둘 수 있습니다. + + + + )} + /> + + {/* 편집 사유 */} + ( + + 편집 사유 (선택사항) + +