summaryrefslogtreecommitdiff
path: root/lib/gtc-contract/gtc-clauses/table/update-gtc-clause-sheet.tsx
diff options
context:
space:
mode:
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.tsx361
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