From 18954df6565108a469fb1608ea3715dd9bb1b02d Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 1 Sep 2025 09:12:09 +0000
Subject: (대표님) 구매 기본계약, gtc 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../gtc-vendor/markdown-image-editor.tsx           | 360 +++++++++++++++++++++
 1 file changed, 360 insertions(+)
 create mode 100644 lib/basic-contract/gtc-vendor/markdown-image-editor.tsx

(limited to 'lib/basic-contract/gtc-vendor/markdown-image-editor.tsx')

diff --git a/lib/basic-contract/gtc-vendor/markdown-image-editor.tsx b/lib/basic-contract/gtc-vendor/markdown-image-editor.tsx
new file mode 100644
index 00000000..422d8475
--- /dev/null
+++ b/lib/basic-contract/gtc-vendor/markdown-image-editor.tsx
@@ -0,0 +1,360 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
+import { Upload, X, Image as ImageIcon, Eye, EyeOff } from "lucide-react"
+import { toast } from "sonner"
+import { cn } from "@/lib/utils"
+
+interface ClauseImage {
+  id: string
+  url: string
+  fileName: string
+  size: number
+}
+
+interface MarkdownImageEditorProps {
+  content: string
+  images: ClauseImage[]
+  onChange: (content: string, images: ClauseImage[]) => void
+  placeholder?: string
+  rows?: number
+  className?: string
+}
+
+export function MarkdownImageEditor({ 
+  content, 
+  images, 
+  onChange, 
+  placeholder = "텍스트를 입력하고, 이미지를 삽입하려면 '이미지 추가' 버튼을 클릭하세요.",
+  rows = 6,
+  className 
+}: MarkdownImageEditorProps) {
+  const [imageUploadOpen, setImageUploadOpen] = React.useState(false)
+  const [showPreview, setShowPreview] = React.useState(false)
+  const [uploading, setUploading] = React.useState(false)
+  const textareaRef = React.useRef<HTMLTextAreaElement>(null)
+
+  // 이미지 업로드 핸들러
+  const handleImageUpload = async (file: File) => {
+    if (!file.type.startsWith('image/')) {
+      toast.error('이미지 파일만 업로드 가능합니다.')
+      return
+    }
+
+    if (file.size > 5 * 1024 * 1024) { // 5MB 제한
+      toast.error('파일 크기는 5MB 이하여야 합니다.')
+      return
+    }
+
+    setUploading(true)
+    try {
+      // 실제 구현에서는 서버로 업로드
+      const uploadedUrl = await uploadImage(file)
+      const imageId = `image${Date.now()}`
+      
+      // 이미지 배열에 추가
+      const newImages = [...images, {
+        id: imageId,
+        url: uploadedUrl,
+        fileName: file.name,
+        size: file.size,
+      }]
+      
+      // 커서 위치에 이미지 참조 삽입
+      const imageRef = `![${imageId}]`
+      const textarea = textareaRef.current
+      
+      if (textarea) {
+        const start = textarea.selectionStart
+        const end = textarea.selectionEnd
+        const newContent = content.substring(0, start) + imageRef + content.substring(end)
+        
+        onChange(newContent, newImages)
+        
+        // 커서 위치를 이미지 참조 뒤로 이동
+        setTimeout(() => {
+          textarea.focus()
+          textarea.setSelectionRange(start + imageRef.length, start + imageRef.length)
+        }, 0)
+      } else {
+        // 텍스트 끝에 추가
+        const newContent = content + (content ? '\n\n' : '') + imageRef
+        onChange(newContent, newImages)
+      }
+      
+      toast.success('이미지가 추가되었습니다.')
+      setImageUploadOpen(false)
+    } catch (error) {
+      toast.error('이미지 업로드에 실패했습니다.')
+      console.error('Image upload error:', error)
+    } finally {
+      setUploading(false)
+    }
+  }
+
+  // 이미지 제거
+  const removeImage = (imageId: string) => {
+    // 이미지 배열에서 제거
+    const newImages = images.filter(img => img.id !== imageId)
+    
+    // 텍스트에서 이미지 참조 제거
+    const imageRef = `![${imageId}]`
+    const newContent = content.replace(new RegExp(`\\!\\[${imageId}\\]`, 'g'), '')
+    
+    onChange(newContent, newImages)
+    toast.success('이미지가 제거되었습니다.')
+  }
+
+  // 커서 위치에 텍스트 삽입
+  const insertAtCursor = (text: string) => {
+    const textarea = textareaRef.current
+    if (!textarea) return
+
+    const start = textarea.selectionStart
+    const end = textarea.selectionEnd
+    const newContent = content.substring(0, start) + text + content.substring(end)
+    
+    onChange(newContent, images)
+    
+    // 커서 위치 조정
+    setTimeout(() => {
+      textarea.focus()
+      textarea.setSelectionRange(start + text.length, start + text.length)
+    }, 0)
+  }
+
+  return (
+    <div className={cn("space-y-3", className)}>
+      {/* 에디터 툴바 */}
+      <div className="flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          <Button
+            type="button"
+            variant="outline"
+            size="sm"
+            onClick={() => setImageUploadOpen(true)}
+            disabled={uploading}
+          >
+            <Upload className="h-4 w-4 mr-1" />
+            {uploading ? '업로드 중...' : '이미지 추가'}
+          </Button>
+          
+          <Button
+            type="button"
+            variant="outline"
+            size="sm"
+            onClick={() => setShowPreview(!showPreview)}
+          >
+            {showPreview ? (
+              <>
+                <EyeOff className="h-4 w-4 mr-1" />
+                편집
+              </>
+            ) : (
+              <>
+                <Eye className="h-4 w-4 mr-1" />
+                미리보기
+              </>
+            )}
+          </Button>
+        </div>
+        
+        {images.length > 0 && (
+          <span className="text-xs text-muted-foreground">
+            {images.length}개 이미지 첨부됨
+          </span>
+        )}
+      </div>
+
+      {/* 에디터 영역 */}
+      {showPreview ? (
+        /* 미리보기 모드 */
+        <div className="border rounded-lg p-4 bg-muted/10 min-h-[200px]">
+          <div className="space-y-3">
+            {renderMarkdownPreview(content, images)}
+          </div>
+        </div>
+      ) : (
+        /* 편집 모드 */
+        <div className="relative">
+          <Textarea
+            ref={textareaRef}
+            value={content}
+            onChange={(e) => onChange(e.target.value, images)}
+            placeholder={placeholder}
+            rows={rows}
+            className="font-mono text-sm resize-none"
+          />
+          
+          {/* 이미지 참조 안내 */}
+          {content.includes('![image') && (
+            <div className="absolute bottom-2 right-2 text-xs text-muted-foreground bg-background/80 px-2 py-1 rounded">
+              ![imageXXX] = 이미지 삽입 위치
+            </div>
+          )}
+        </div>
+      )}
+
+      {/* 첨부된 이미지 목록 */}
+      {images.length > 0 && !showPreview && (
+        <div className="space-y-2">
+          <div className="text-sm font-medium">첨부된 이미지:</div>
+          <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
+            {images.map((img) => (
+              <div key={img.id} className="relative group">
+                <img
+                  src={img.url}
+                  alt={img.fileName}
+                  className="w-full h-16 object-cover rounded border"
+                />
+                <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded flex items-center justify-center">
+                  <div className="text-white text-xs text-center">
+                    <div className="font-medium">![{img.id}]</div>
+                    <div className="truncate max-w-16" title={img.fileName}>
+                      {img.fileName}
+                    </div>
+                  </div>
+                </div>
+                <Button
+                  type="button"
+                  variant="destructive"
+                  size="sm"
+                  className="absolute -top-1 -right-1 h-5 w-5 p-0 opacity-0 group-hover:opacity-100"
+                  onClick={() => removeImage(img.id)}
+                >
+                  <X className="h-3 w-3" />
+                </Button>
+              </div>
+            ))}
+          </div>
+        </div>
+      )}
+
+      {/* 이미지 업로드 다이얼로그 */}
+      <Dialog open={imageUploadOpen} onOpenChange={setImageUploadOpen}>
+        <DialogContent className="max-w-md">
+          <DialogHeader>
+            <DialogTitle>이미지 추가</DialogTitle>
+          </DialogHeader>
+          <div className="space-y-4">
+            <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-6">
+              <div className="flex flex-col items-center justify-center text-center">
+                <ImageIcon className="h-8 w-8 text-muted-foreground mb-2" />
+                <p className="text-sm text-muted-foreground mb-4">
+                  이미지를 선택하거나 드래그해서 업로드하세요
+                </p>
+                <input
+                  type="file"
+                  accept="image/*"
+                  onChange={(e) => {
+                    const file = e.target.files?.[0]
+                    if (file) handleImageUpload(file)
+                  }}
+                  className="hidden"
+                  id="image-upload"
+                  disabled={uploading}
+                />
+                <label
+                  htmlFor="image-upload"
+                  className={cn(
+                    "cursor-pointer inline-flex items-center gap-2 text-sm bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md",
+                    uploading && "opacity-50 cursor-not-allowed"
+                  )}
+                >
+                  <Upload className="h-4 w-4" />
+                  파일 선택
+                </label>
+              </div>
+            </div>
+            <div className="text-xs text-muted-foreground space-y-1">
+              <div>• 지원 형식: JPG, PNG, GIF, WebP</div>
+              <div>• 최대 크기: 5MB</div>
+              <div>• 현재 커서 위치에 삽입됩니다</div>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </div>
+  )
+}
+
+// 마크다운 미리보기 렌더링
+function renderMarkdownPreview(content: string, images: ClauseImage[]) {
+  if (!content.trim()) {
+    return (
+      <p className="text-muted-foreground italic">
+        내용을 입력하세요...
+      </p>
+    )
+  }
+
+  const parts = content.split(/(\![a-zA-Z0-9_]+\])/)
+  
+  return parts.map((part, index) => {
+    // 이미지 참조인지 확인
+    const imageMatch = part.match(/^!\[(.+)\]$/)
+    
+    if (imageMatch) {
+      const imageId = imageMatch[1]
+      const image = images.find(img => img.id === imageId)
+      
+      if (image) {
+        return (
+          <div key={index} className="my-3">
+            <img
+              src={image.url}
+              alt={image.fileName}
+              className="max-w-full h-auto rounded border shadow-sm"
+              style={{ maxHeight: '400px' }}
+            />
+            <p className="text-xs text-muted-foreground mt-1 text-center">
+              {image.fileName} ({formatFileSize(image.size)})
+            </p>
+          </div>
+        )
+      } else {
+        return (
+          <div key={index} className="my-2 p-2 bg-yellow-50 border border-yellow-200 rounded">
+            <p className="text-xs text-yellow-700">
+              ⚠️ 이미지를 찾을 수 없음: {part}
+            </p>
+          </div>
+        )
+      }
+    } else {
+      // 일반 텍스트 (줄바꿈 처리)
+      return part.split('\n').map((line, lineIndex) => (
+        <p key={`${index}-${lineIndex}`} className="text-sm">
+          {line || '\u00A0'} {/* 빈 줄 처리 */}
+        </p>
+      ))
+    }
+  })
+}
+
+// 파일 크기 포맷팅
+function formatFileSize(bytes: number): string {
+  if (bytes === 0) return '0 Bytes'
+  
+  const k = 1024
+  const sizes = ['Bytes', 'KB', 'MB', 'GB']
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+// 임시 이미지 업로드 함수 (실제 구현 필요)
+async function uploadImage(file: File): Promise<string> {
+  // TODO: 실제 서버 업로드 로직 구현
+  // 현재는 임시로 ObjectURL 반환
+  return new Promise((resolve) => {
+    const reader = new FileReader()
+    reader.onload = (e) => {
+      resolve(e.target?.result as string)
+    }
+    reader.readAsDataURL(file)
+  })
+}
\ No newline at end of file
-- 
cgit v1.2.3

