diff options
Diffstat (limited to 'lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx')
| -rw-r--r-- | lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx b/lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx new file mode 100644 index 00000000..aae0396b --- /dev/null +++ b/lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx @@ -0,0 +1,361 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Loader, Info } from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" + +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Badge } from "@/components/ui/badge" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription, +} from "@/components/ui/form" + +import { type GtcClauseTreeView } from "@/db/schema/gtc" +import { updateGtcClauseSchema, type UpdateGtcClauseSchema } from "@/lib/gtc-contract/gtc-clauses/validations" +import { updateGtcClause } from "@/lib/gtc-contract/gtc-clauses/service" +import { useSession } from "next-auth/react" +import { MarkdownImageEditor } from "./markdown-image-editor" + +interface ClauseImage { + id: string + url: string + fileName: string + size: number + savedName?: string + mimeType?: string + width?: number + height?: number + hash?: string +} + + +export interface UpdateGtcClauseSheetProps + extends React.ComponentPropsWithRef<typeof Sheet> { + gtcClause: GtcClauseTreeView | null + documentId: number +} + +export function UpdateGtcClauseSheet({ gtcClause, documentId, ...props }: UpdateGtcClauseSheetProps) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + const { data: session } = useSession() + const [images, setImages] = React.useState<ClauseImage[]>([]) + const [rawFiles, setRawFiles] = React.useState<File[]>([]) + const [removedImageIds, setRemovedImageIds] = React.useState<string[]>([]) + + + const currentUserId = React.useMemo(() => { + return session?.user?.id ? Number(session.user.id) : null + }, [session]) + + const form = useForm<UpdateGtcClauseSchema>({ + resolver: zodResolver(updateGtcClauseSchema), + defaultValues: { + itemNumber: "", + category: "", + subtitle: "", + content: "", + // numberVariableName: "", + // subtitleVariableName: "", + // contentVariableName: "", + editReason: "", + isActive: true, + }, + }) + + React.useEffect(() => { + if (gtcClause) { + form.reset({ + itemNumber: gtcClause.itemNumber, + category: gtcClause.category || "", + subtitle: gtcClause.subtitle, + content: gtcClause.content || "", + editReason: "", + isActive: gtcClause.isActive, + }) + // ✅ 초기 이미지 세팅 + setImages((gtcClause.images as any[]) || []) + setRawFiles([]) + setRemovedImageIds([]) + } + }, [gtcClause, form]) + + + + async function onSubmit(input: UpdateGtcClauseSchema) { + startUpdateTransition(async () => { + if (!gtcClause || !currentUserId) { + toast.error("조항 정보를 찾을 수 없습니다.") + return + } + + try { + const result = await updateGtcClause(gtcClause.id, { + ...input, + images: images, // 이미지 배열 추가 + updatedById: currentUserId, + }) + + if (result.error) { + toast.error(result.error) + return + } + + form.reset() + props.onOpenChange?.(false) + toast.success("GTC 조항이 업데이트되었습니다!") + } catch (error) { + toast.error("조항 업데이트 중 오류가 발생했습니다.") + } + }) + } + + const getDepthBadge = (depth: number) => { + const levels = ["1단계", "2단계", "3단계", "4단계", "5단계+"] + return levels[depth] || levels[4] + } + + const handleContentImageChange = (content: string, newImages: ClauseImage[]) => { + form.setValue("content", content) + setImages(newImages) + } + + + return ( + <Sheet {...props}> + <SheetContent className="flex flex-col sm:max-w-xl h-full"> + <SheetHeader className="text-left flex-shrink-0"> + <SheetTitle>GTC 조항 수정</SheetTitle> + <SheetDescription> + 조항 정보를 수정하고 변경사항을 저장하세요 + </SheetDescription> + </SheetHeader> + + {/* 조항 정보 표시 */} + <div className="space-y-2 p-3 bg-muted/50 rounded-lg flex-shrink-0"> + <div className="text-sm font-medium">현재 조항 정보</div> + <div className="text-xs text-muted-foreground space-y-1"> + <div className="flex items-center gap-2"> + <span>위치:</span> + <Badge variant="outline"> + {getDepthBadge(gtcClause?.depth || 0)} + </Badge> + {gtcClause?.fullPath && ( + <span className="font-mono">{gtcClause.fullPath}</span> + )} + </div> + {gtcClause?.parentItemNumber && ( + <div>부모 조항: {gtcClause.parentItemNumber} - {gtcClause.parentSubtitle}</div> + )} + {gtcClause?.childrenCount > 0 && ( + <div>하위 조항: {gtcClause.childrenCount}개</div> + )} + </div> + </div> + + <Form {...form}> + <form + onSubmit={form.handleSubmit(onSubmit)} + className="flex flex-col flex-1 min-h-0" + > + {/* 스크롤 가능한 폼 내용 영역 */} + <div className="flex-1 overflow-y-auto px-1"> + <div className="space-y-4 py-2"> + {/* 채번 */} + <FormField + control={form.control} + name="itemNumber" + render={({ field }) => ( + <FormItem> + <FormLabel>채번 *</FormLabel> + <FormControl> + <Input + placeholder="예: 1, 1.1, 2.3.1, A, B-1 등" + {...field} + /> + </FormControl> + <FormDescription> + 조항의 번호입니다. 영문, 숫자, 점(.), 하이픈(-), 언더스코어(_)를 사용할 수 있습니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* 분류 */} + <FormField + control={form.control} + name="category" + render={({ field }) => ( + <FormItem> + <FormLabel>분류</FormLabel> + <FormControl> + <Input + placeholder="예: 일반조항, 특수조항, 기술조항 등" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 소제목 */} + <FormField + control={form.control} + name="subtitle" + render={({ field }) => ( + <FormItem> + <FormLabel>소제목 *</FormLabel> + <FormControl> + <Input + placeholder="예: PREAMBLE, DEFINITIONS, GENERAL CONDITIONS 등" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 상세항목 */} + <FormField + control={form.control} + name="content" + render={({ field }) => ( + <FormItem> + <FormLabel>상세항목 (선택사항)</FormLabel> + <FormControl> + <MarkdownImageEditor + content={field.value || ""} + images={images} + onChange={handleContentImageChange} + placeholder="조항의 상세 내용을 입력하세요... 이미지를 추가하려면 '이미지 추가' 버튼을 클릭하세요." + rows={8} + /> + </FormControl> + <FormDescription> + 조항의 실제 내용입니다. 텍스트와 이미지를 조합할 수 있으며, 하위 조항들을 그룹핑하는 제목용 조항인 경우 비워둘 수 있습니다. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + {/* PDFTron 변수명 섹션 */} + {/* <div className="space-y-3 p-3 border rounded-lg"> + <div className="flex items-center gap-2"> + <Info className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm font-medium">PDFTron 변수명 설정</span> + {gtcClause?.hasAllVariableNames && ( + <Badge variant="default" className="text-xs">설정됨</Badge> + )} + </div> + + <div className="grid grid-cols-1 gap-3"> + <FormField + control={form.control} + name="numberVariableName" + render={({ field }) => ( + <FormItem> + <FormLabel>채번 변수명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="subtitleVariableName" + render={({ field }) => ( + <FormItem> + <FormLabel>소제목 변수명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="contentVariableName" + render={({ field }) => ( + <FormItem> + <FormLabel>상세항목 변수명</FormLabel> + <FormControl> + <Input {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> */} + + {/* 편집 사유 */} + <FormField + control={form.control} + name="editReason" + render={({ field }) => ( + <FormItem> + <FormLabel>편집 사유 (권장)</FormLabel> + <FormControl> + <Textarea + placeholder="수정 사유를 입력하세요..." + {...field} + rows={3} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + </div> + + <SheetFooter className="gap-2 pt-2 sm:space-x-0"> + <SheetClose asChild> + <Button type="button" variant="outline"> + Cancel + </Button> + </SheetClose> + + <Button type="submit" disabled={isUpdatePending}> + {isUpdatePending && ( + <Loader + className="mr-2 size-4 animate-spin" + aria-hidden="true" + /> + )} + Save + </Button> + </SheetFooter> + </form> + </Form> + </SheetContent> + </Sheet> + ) +}
\ No newline at end of file |
