"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 사용 */} ( 상세항목 (선택사항) 조항의 실제 내용입니다. 텍스트와 이미지를 조합할 수 있으며, 하위 조항들을 그룹핑하는 제목용 조항인 경우 비워둘 수 있습니다. )} /> {/* 편집 사유 */} ( 편집 사유 (선택사항)