summaryrefslogtreecommitdiff
path: root/lib/b-rfq/attachment
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/attachment')
-rw-r--r--lib/b-rfq/attachment/add-attachment-dialog.tsx355
-rw-r--r--lib/b-rfq/attachment/add-revision-dialog.tsx336
-rw-r--r--lib/b-rfq/attachment/attachment-columns.tsx286
-rw-r--r--lib/b-rfq/attachment/attachment-table.tsx190
-rw-r--r--lib/b-rfq/attachment/attachment-toolbar-action.tsx60
-rw-r--r--lib/b-rfq/attachment/confirm-documents-dialog.tsx141
-rw-r--r--lib/b-rfq/attachment/delete-attachment-dialog.tsx182
-rw-r--r--lib/b-rfq/attachment/request-revision-dialog.tsx205
-rw-r--r--lib/b-rfq/attachment/revision-dialog.tsx196
-rw-r--r--lib/b-rfq/attachment/tbe-request-dialog.tsx200
-rw-r--r--lib/b-rfq/attachment/vendor-responses-panel.tsx386
11 files changed, 0 insertions, 2537 deletions
diff --git a/lib/b-rfq/attachment/add-attachment-dialog.tsx b/lib/b-rfq/attachment/add-attachment-dialog.tsx
deleted file mode 100644
index 665e0f88..00000000
--- a/lib/b-rfq/attachment/add-attachment-dialog.tsx
+++ /dev/null
@@ -1,355 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { Plus ,X} from "lucide-react"
-import { toast } from "sonner"
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Dropzone,
- DropzoneDescription,
- DropzoneInput,
- DropzoneTitle,
- DropzoneUploadIcon,
- DropzoneZone,
-} from "@/components/ui/dropzone"
-import {
- FileList,
- FileListAction,
- FileListDescription,
- FileListHeader,
- FileListIcon,
- FileListInfo,
- FileListItem,
- FileListName,
- FileListSize,
-} from "@/components/ui/file-list"
-import { Button } from "@/components/ui/button"
-import { Textarea } from "@/components/ui/textarea"
-import { addRfqAttachmentRecord } from "../service"
-
-// 첨부파일 추가 폼 스키마 (단일 파일)
-const addAttachmentSchema = z.object({
- attachmentType: z.enum(["구매", "설계"], {
- required_error: "문서 타입을 선택해주세요.",
- }),
- description: z.string().optional(),
- file: z.instanceof(File, {
- message: "파일을 선택해주세요.",
- }),
-})
-
-type AddAttachmentFormData = z.infer<typeof addAttachmentSchema>
-
-interface AddAttachmentDialogProps {
- rfqId: number
-}
-
-export function AddAttachmentDialog({ rfqId }: AddAttachmentDialogProps) {
- const [open, setOpen] = React.useState(false)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [uploadProgress, setUploadProgress] = React.useState<number>(0)
-
- const form = useForm<AddAttachmentFormData>({
- resolver: zodResolver(addAttachmentSchema),
- defaultValues: {
- attachmentType: undefined,
- description: "",
- file: undefined,
- },
- })
-
- const selectedFile = form.watch("file")
-
- // 다이얼로그 닫기 핸들러
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen && !isSubmitting) {
- form.reset()
- }
- setOpen(newOpen)
- }
-
- // 파일 선택 처리
- const handleFileChange = (files: File[]) => {
- if (files.length === 0) return
-
- const file = files[0] // 첫 번째 파일만 사용
-
- // 파일 크기 검증
- const maxFileSize = 10 * 1024 * 1024 // 10MB
- if (file.size > maxFileSize) {
- toast.error(`파일이 너무 큽니다. (최대 10MB)`)
- return
- }
-
- form.setValue("file", file)
- form.clearErrors("file")
- }
-
- // 파일 제거
- const removeFile = () => {
- form.resetField("file")
- }
-
- // 파일 업로드 API 호출
- const uploadFile = async (file: File): Promise<{
- fileName: string
- originalFileName: string
- filePath: string
- fileSize: number
- fileType: string
- }> => {
- const formData = new FormData()
- formData.append("rfqId", rfqId.toString())
- formData.append("file", file)
-
- const response = await fetch("/api/upload/rfq-attachment", {
- method: "POST",
- body: formData,
- })
-
- if (!response.ok) {
- const error = await response.json()
- throw new Error(error.message || "파일 업로드 실패")
- }
-
- return response.json()
- }
-
- // 폼 제출
- const onSubmit = async (data: AddAttachmentFormData) => {
- setIsSubmitting(true)
- setUploadProgress(0)
-
- try {
- // 1단계: 파일 업로드
- setUploadProgress(30)
- const uploadedFile = await uploadFile(data.file)
-
- // 2단계: DB 레코드 생성 (시리얼 번호 자동 생성)
- setUploadProgress(70)
- const attachmentRecord = {
- rfqId,
- attachmentType: data.attachmentType,
- description: data.description,
- fileName: uploadedFile.fileName,
- originalFileName: uploadedFile.originalFileName,
- filePath: uploadedFile.filePath,
- fileSize: uploadedFile.fileSize,
- fileType: uploadedFile.fileType,
- }
-
- const result = await addRfqAttachmentRecord(attachmentRecord)
-
- setUploadProgress(100)
-
- if (result.success) {
- toast.success(result.message)
- form.reset()
- handleOpenChange(false)
- } else {
- toast.error(result.message)
- }
-
- } catch (error) {
- console.error("Upload error:", error)
- toast.error(error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- setUploadProgress(0)
- }
- }
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <Plus className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">새 첨부</span>
- </Button>
- </DialogTrigger>
-
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle>새 첨부파일 추가</DialogTitle>
- <DialogDescription>
- RFQ에 첨부할 문서를 업로드합니다. 시리얼 번호는 자동으로 부여됩니다.
- </DialogDescription>
- </DialogHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
- {/* 문서 타입 선택 */}
- <FormField
- control={form.control}
- name="attachmentType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>문서 타입</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="문서 타입을 선택하세요" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="구매">구매</SelectItem>
- <SelectItem value="설계">설계</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 설명 */}
- <FormField
- control={form.control}
- name="description"
- render={({ field }) => (
- <FormItem>
- <FormLabel>설명 (선택)</FormLabel>
- <FormControl>
- <Textarea
- placeholder="첨부파일에 대한 설명을 입력하세요"
- className="resize-none"
- rows={3}
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 파일 선택 - Dropzone (단일 파일) */}
- <FormField
- control={form.control}
- name="file"
- render={({ field }) => (
- <FormItem>
- <FormLabel>파일 선택</FormLabel>
- <FormControl>
- <div className="space-y-3">
- <Dropzone
- onDrop={(acceptedFiles) => {
- handleFileChange(acceptedFiles)
- }}
- accept={{
- 'application/pdf': ['.pdf'],
- 'application/msword': ['.doc'],
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
- 'application/vnd.ms-excel': ['.xls'],
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
- 'application/vnd.ms-powerpoint': ['.ppt'],
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
- 'application/zip': ['.zip'],
- 'application/x-rar-compressed': ['.rar']
- }}
- maxSize={10 * 1024 * 1024} // 10MB
- multiple={false} // 단일 파일만
- disabled={isSubmitting}
- >
- <DropzoneZone>
- <DropzoneUploadIcon />
- <DropzoneTitle>클릭하여 파일 선택 또는 드래그 앤 드롭</DropzoneTitle>
- <DropzoneDescription>
- PDF, DOC, XLS, PPT 등 (최대 10MB, 파일 1개)
- </DropzoneDescription>
- <DropzoneInput />
- </DropzoneZone>
- </Dropzone>
-
- {/* 선택된 파일 표시 */}
- {selectedFile && (
- <div className="space-y-2">
- <FileListHeader>
- 선택된 파일
- </FileListHeader>
- <FileList>
- <FileListItem className="flex items-center justify-between gap-3">
- <FileListIcon />
- <FileListInfo>
- <FileListName>{selectedFile.name}</FileListName>
- <FileListDescription>
- <FileListSize>{selectedFile.size}</FileListSize>
- </FileListDescription>
- </FileListInfo>
- <FileListAction
- onClick={removeFile}
- disabled={isSubmitting}
- >
- <X className="h-4 w-4" />
- </FileListAction>
- </FileListItem>
- </FileList>
- </div>
- )}
-
- {/* 업로드 진행률 */}
- {isSubmitting && uploadProgress > 0 && (
- <div className="space-y-2">
- <div className="flex justify-between text-sm">
- <span>업로드 진행률</span>
- <span>{uploadProgress}%</span>
- </div>
- <div className="w-full bg-gray-200 rounded-full h-2">
- <div
- className="bg-blue-600 h-2 rounded-full transition-all duration-300"
- style={{ width: `${uploadProgress}%` }}
- />
- </div>
- </div>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button type="submit" disabled={isSubmitting || !selectedFile}>
- {isSubmitting ? "업로드 중..." : "업로드"}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/add-revision-dialog.tsx b/lib/b-rfq/attachment/add-revision-dialog.tsx
deleted file mode 100644
index 1abefb02..00000000
--- a/lib/b-rfq/attachment/add-revision-dialog.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { Upload } from "lucide-react"
-import { toast } from "sonner"
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Dropzone,
- DropzoneDescription,
- DropzoneInput,
- DropzoneTitle,
- DropzoneUploadIcon,
- DropzoneZone,
-} from "@/components/ui/dropzone"
-import {
- FileList,
- FileListAction,
- FileListDescription,
- FileListHeader,
- FileListIcon,
- FileListInfo,
- FileListItem,
- FileListName,
- FileListSize,
-} from "@/components/ui/file-list"
-import { Button } from "@/components/ui/button"
-import { Textarea } from "@/components/ui/textarea"
-import { addRevisionToAttachment } from "../service"
-
-// 리비전 추가 폼 스키마
-const addRevisionSchema = z.object({
- revisionComment: z.string().optional(),
- file: z.instanceof(File, {
- message: "파일을 선택해주세요.",
- }),
-})
-
-type AddRevisionFormData = z.infer<typeof addRevisionSchema>
-
-interface AddRevisionDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- attachmentId: number
- currentRevision: string
- originalFileName: string
- onSuccess?: () => void
-}
-
-export function AddRevisionDialog({
- open,
- onOpenChange,
- attachmentId,
- currentRevision,
- originalFileName,
- onSuccess
-}: AddRevisionDialogProps) {
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [uploadProgress, setUploadProgress] = React.useState<number>(0)
-
- const form = useForm<AddRevisionFormData>({
- resolver: zodResolver(addRevisionSchema),
- defaultValues: {
- revisionComment: "",
- file: undefined,
- },
- })
-
- const selectedFile = form.watch("file")
-
- // 다이얼로그 닫기 핸들러
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen && !isSubmitting) {
- form.reset()
- }
- onOpenChange(newOpen)
- }
-
- // 파일 선택 처리
- const handleFileChange = (files: File[]) => {
- if (files.length === 0) return
-
- const file = files[0]
-
- // 파일 크기 검증
- const maxFileSize = 10 * 1024 * 1024 // 10MB
- if (file.size > maxFileSize) {
- toast.error(`파일이 너무 큽니다. (최대 10MB)`)
- return
- }
-
- form.setValue("file", file)
- form.clearErrors("file")
- }
-
- // 파일 제거
- const removeFile = () => {
- form.resetField("file")
- }
-
- // 파일 업로드 API 호출
- const uploadFile = async (file: File): Promise<{
- fileName: string
- originalFileName: string
- filePath: string
- fileSize: number
- fileType: string
- }> => {
- const formData = new FormData()
- formData.append("attachmentId", attachmentId.toString())
- formData.append("file", file)
- formData.append("isRevision", "true")
-
- const response = await fetch("/api/upload/rfq-attachment-revision", {
- method: "POST",
- body: formData,
- })
-
- if (!response.ok) {
- const error = await response.json()
- throw new Error(error.message || "파일 업로드 실패")
- }
-
- return response.json()
- }
-
- // 폼 제출
- const onSubmit = async (data: AddRevisionFormData) => {
- setIsSubmitting(true)
- setUploadProgress(0)
-
- try {
- // 1단계: 파일 업로드
- setUploadProgress(30)
- const uploadedFile = await uploadFile(data.file)
-
- // 2단계: DB 리비전 레코드 생성
- setUploadProgress(70)
- const result = await addRevisionToAttachment(attachmentId, {
- fileName: uploadedFile.fileName,
- originalFileName: uploadedFile.originalFileName,
- filePath: uploadedFile.filePath,
- fileSize: uploadedFile.fileSize,
- fileType: uploadedFile.fileType,
- revisionComment: data.revisionComment,
- })
-
- setUploadProgress(100)
-
- if (result.success) {
- toast.success(result.message)
- form.reset()
- handleOpenChange(false)
- onSuccess?.()
- } else {
- toast.error(result.message)
- }
-
- } catch (error) {
- console.error("Upload error:", error)
- toast.error(error instanceof Error ? error.message : "리비전 추가 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- setUploadProgress(0)
- }
- }
-
- // 다음 리비전 번호 계산
- const getNextRevision = (current: string) => {
- const match = current.match(/Rev\.(\d+)/)
- if (match) {
- const num = parseInt(match[1]) + 1
- return `Rev.${num}`
- }
- return "Rev.1"
- }
-
- const nextRevision = getNextRevision(currentRevision)
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="sm:max-w-[500px]">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Upload className="h-5 w-5" />
- 새 리비전 추가
- </DialogTitle>
- <DialogDescription>
- "{originalFileName}"의 새 버전을 업로드합니다.
- 현재 {currentRevision} → {nextRevision}
- </DialogDescription>
- </DialogHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
- {/* 리비전 코멘트 */}
- <FormField
- control={form.control}
- name="revisionComment"
- render={({ field }) => (
- <FormItem>
- <FormLabel>리비전 코멘트 (선택)</FormLabel>
- <FormControl>
- <Textarea
- placeholder={`${nextRevision} 업데이트 내용을 입력하세요`}
- className="resize-none"
- rows={3}
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 파일 선택 - Dropzone (단일 파일) */}
- <FormField
- control={form.control}
- name="file"
- render={({ field }) => (
- <FormItem>
- <FormLabel>새 파일 선택</FormLabel>
- <FormControl>
- <div className="space-y-3">
- <Dropzone
- onDrop={(acceptedFiles) => {
- handleFileChange(acceptedFiles)
- }}
- accept={{
- 'application/pdf': ['.pdf'],
- 'application/msword': ['.doc'],
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
- 'application/vnd.ms-excel': ['.xls'],
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
- 'application/vnd.ms-powerpoint': ['.ppt'],
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
- 'application/zip': ['.zip'],
- 'application/x-rar-compressed': ['.rar']
- }}
- maxSize={10 * 1024 * 1024} // 10MB
- multiple={false}
- disabled={isSubmitting}
- >
- <DropzoneZone>
- <DropzoneUploadIcon />
- <DropzoneTitle>클릭하여 파일 선택 또는 드래그 앤 드롭</DropzoneTitle>
- <DropzoneDescription>
- PDF, DOC, XLS, PPT 등 (최대 10MB, 파일 1개)
- </DropzoneDescription>
- <DropzoneInput />
- </DropzoneZone>
- </Dropzone>
-
- {/* 선택된 파일 표시 */}
- {selectedFile && (
- <div className="space-y-2">
- <FileListHeader>
- 선택된 파일 ({nextRevision})
- </FileListHeader>
- <FileList>
- <FileListItem>
- <FileListIcon />
- <FileListInfo>
- <FileListName>{selectedFile.name}</FileListName>
- <FileListDescription>
- <FileListSize>{selectedFile.size}</FileListSize>
- </FileListDescription>
- </FileListInfo>
- <FileListAction
- onClick={removeFile}
- disabled={isSubmitting}
- />
- </FileListItem>
- </FileList>
- </div>
- )}
-
- {/* 업로드 진행률 */}
- {isSubmitting && uploadProgress > 0 && (
- <div className="space-y-2">
- <div className="flex justify-between text-sm">
- <span>업로드 진행률</span>
- <span>{uploadProgress}%</span>
- </div>
- <div className="w-full bg-gray-200 rounded-full h-2">
- <div
- className="bg-blue-600 h-2 rounded-full transition-all duration-300"
- style={{ width: `${uploadProgress}%` }}
- />
- </div>
- </div>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button type="submit" disabled={isSubmitting || !selectedFile}>
- {isSubmitting ? "업로드 중..." : `${nextRevision} 추가`}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/attachment-columns.tsx b/lib/b-rfq/attachment/attachment-columns.tsx
deleted file mode 100644
index b726ebc8..00000000
--- a/lib/b-rfq/attachment/attachment-columns.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type ColumnDef } from "@tanstack/react-table"
-import {
- Ellipsis, FileText, Download, Eye,
- MessageSquare, Upload
-} from "lucide-react"
-
-import { formatDate, formatBytes } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- DropdownMenu, DropdownMenuContent, DropdownMenuItem,
- DropdownMenuSeparator, DropdownMenuTrigger
-} from "@/components/ui/dropdown-menu"
-import { Progress } from "@/components/ui/progress"
-import { RevisionDialog } from "./revision-dialog"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { AddRevisionDialog } from "./add-revision-dialog"
-
-interface GetAttachmentColumnsProps {
- onSelectAttachment: (attachment: any) => void
-}
-
-export function getAttachmentColumns({
- onSelectAttachment
-}: GetAttachmentColumnsProps): ColumnDef<any>[] {
-
- return [
- /** ───────────── 체크박스 ───────────── */
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-0.5"
- />
- ),
- size: 40,
- enableSorting: false,
- enableHiding: false,
- },
-
- /** ───────────── 문서 정보 ───────────── */
- {
- accessorKey: "serialNo",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="시리얼 번호" />
- ),
- cell: ({ row }) => (
- <Button
- variant="link"
- className="p-0 h-auto font-medium text-blue-600 hover:text-blue-800"
- onClick={() => onSelectAttachment(row.original)}
- >
- {row.getValue("serialNo") as string}
- </Button>
- ),
- size: 100,
- },
- {
- accessorKey: "attachmentType",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="문서 타입" />
- ),
- cell: ({ row }) => {
- const type = row.getValue("attachmentType") as string
- return (
- <Badge variant={type === "구매" ? "default" : "secondary"}>
- {type}
- </Badge>
- )
- },
- size:100
- },
- {
- accessorKey: "originalFileName",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="파일명" />
- ),
- cell: ({ row }) => {
- const fileName = row.getValue("originalFileName") as string
- return (
- <div className="flex items-center gap-2">
- <FileText className="h-4 w-4 text-muted-foreground" />
- <div className="min-w-0 flex-1">
- <div className="truncate font-medium" title={fileName}>
- {fileName}
- </div>
- </div>
- </div>
- )
- },
- size:250
- },
- {
- id: "currentRevision",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="리비전" />
- ),
- cell: ({ row }) => (
- <RevisionDialog
- attachmentId={row.original.id}
- currentRevision={row.original.currentRevision}
- originalFileName={row.original.originalFileName}
- />
- ),
- size: 100,
- },
- {
- accessorKey: "description",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="설명" />
- ),
- cell: ({ row }) => {
- const description = row.getValue("description") as string
- return description
- ? <div className="max-w-[200px] truncate" title={description}>{description}</div>
- : <span className="text-muted-foreground">-</span>
- },
- },
-
- /** ───────────── 파일 정보 ───────────── */
- // {
- // accessorKey: "fileSize",
- // header: ({ column }) => (
- // <DataTableColumnHeaderSimple column={column} title="파일 크기" />
- // ),
- // cell: ({ row }) => {
- // const size = row.getValue("fileSize") as number
- // return size ? formatBytes(size) : "-"
- // },
- // },
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="등록일" />
- ),
- cell: ({ row }) => {
- const created = row.getValue("createdAt") as Date
- const updated = row.original.updatedAt as Date
- return (
- <div>
- <div>{formatDate(created, "KR")}</div>
- <div className="text-xs text-muted-foreground">
- {row.original.createdByName}
- </div>
- {updated && new Date(updated) > new Date(created) && (
- <div className="text-xs text-blue-600">
- 수정: {formatDate(updated, "KR")}
- </div>
- )}
- </div>
- )
- },
- maxSize:150
- },
-
- /** ───────────── 벤더 응답 현황 ───────────── */
- {
- id: "vendorCount",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="벤더 수" />
- ),
- cell: ({ row }) => {
- const stats = row.original.responseStats
- return stats
- ? (
- <div className="text-center">
- <div className="font-medium">{stats.totalVendors}</div>
- <div className="text-xs text-muted-foreground">
- 활성: {stats.totalVendors - stats.waivedCount}
- </div>
- </div>
- )
- : <span className="text-muted-foreground">-</span>
- },
- },
- {
- id: "responseStatus",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="응답 현황" />
- ),
- cell: ({ row }) => {
- const stats = row.original.responseStats
- return stats
- ? (
- <div className="space-y-1">
- <div className="flex items-center gap-2">
- <div className="flex-1">
- <Progress value={stats.responseRate} className="h-2" />
- </div>
- <span className="text-sm font-medium">
- {stats.responseRate}%
- </span>
- </div>
- <div className="flex gap-2 text-xs">
- <span className="text-green-600">
- 응답: {stats.respondedCount}
- </span>
- <span className="text-orange-600">
- 대기: {stats.pendingCount}
- </span>
- {stats.waivedCount > 0 && (
- <span className="text-gray-500">
- 면제: {stats.waivedCount}
- </span>
- )}
- </div>
- </div>
- )
- : <span className="text-muted-foreground">-</span>
- },
- },
-
- /** ───────────── 액션 ───────────── */
- {
- id: "actions",
- enableHiding: false,
- cell: ({ row }) => {
- const [isAddRevisionOpen, setIsAddRevisionOpen] = React.useState(false)
-
- return (
- <>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- aria-label="Open menu"
- variant="ghost"
- className="flex size-8 p-0 data-[state=open]:bg-muted"
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-48">
- <DropdownMenuItem onClick={() => onSelectAttachment(row.original)}>
- <MessageSquare className="mr-2 h-4 w-4" />
- 벤더 응답 보기
- </DropdownMenuItem>
- <DropdownMenuItem
- onClick={() => row.original.filePath && window.open(row.original.filePath, "_blank")}
- >
- <Download className="mr-2 h-4 w-4" />
- 다운로드
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem onClick={() => setIsAddRevisionOpen(true)}>
- <Upload className="mr-2 h-4 w-4" />
- 새 리비전 추가
- </DropdownMenuItem>
- <DropdownMenuItem className="text-red-600">
- 삭제
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
-
- <AddRevisionDialog
- open={isAddRevisionOpen}
- onOpenChange={setIsAddRevisionOpen}
- attachmentId={row.original.id}
- currentRevision={row.original.currentRevision}
- originalFileName={row.original.originalFileName}
- onSuccess={() => window.location.reload()}
- />
- </>
- )
- },
- size: 40,
- },
- ]
-}
diff --git a/lib/b-rfq/attachment/attachment-table.tsx b/lib/b-rfq/attachment/attachment-table.tsx
deleted file mode 100644
index 4c547000..00000000
--- a/lib/b-rfq/attachment/attachment-table.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table"
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { VendorResponsesPanel } from "./vendor-responses-panel"
-import { Separator } from "@/components/ui/separator"
-import { FileText } from "lucide-react"
-import { getRfqAttachments, getVendorResponsesForAttachment } from "../service"
-import { getAttachmentColumns } from "./attachment-columns"
-import { RfqAttachmentsTableToolbarActions } from "./attachment-toolbar-action"
-
-interface RfqAttachmentsTableProps {
- promises: Promise<Awaited<ReturnType<typeof getRfqAttachments>>>
- rfqId: number
-}
-
-export function RfqAttachmentsTable({ promises, rfqId }: RfqAttachmentsTableProps) {
- const { data, pageCount } = React.use(promises)
-
- // 선택된 첨부파일과 벤더 응답 데이터
- const [selectedAttachment, setSelectedAttachment] = React.useState<any>(null)
- const [vendorResponses, setVendorResponses] = React.useState<any[]>([])
- const [isLoadingResponses, setIsLoadingResponses] = React.useState(false)
-
- const columns = React.useMemo(
- () => getAttachmentColumns({
- onSelectAttachment: setSelectedAttachment
- }),
- []
- )
-
- // 첨부파일 선택 시 벤더 응답 데이터 로드
- React.useEffect(() => {
- if (!selectedAttachment) {
- setVendorResponses([])
- return
- }
-
- const loadVendorResponses = async () => {
- setIsLoadingResponses(true)
- try {
- const responses = await getVendorResponsesForAttachment(
- selectedAttachment.id,
- 'INITIAL' // 또는 현재 RFQ 상태에 따라 결정
- )
- setVendorResponses(responses)
- } catch (error) {
- console.error('Failed to load vendor responses:', error)
- setVendorResponses([])
- } finally {
- setIsLoadingResponses(false)
- }
- }
-
- loadVendorResponses()
- }, [selectedAttachment])
-
- /**
- * 필터 필드 정의
- */
- const filterFields: DataTableFilterField<any>[] = [
- {
- id: "fileName",
- label: "파일명",
- placeholder: "파일명으로 검색...",
- },
- {
- id: "attachmentType",
- label: "문서 타입",
- options: [
- { label: "구매 문서", value: "구매", count: 0 },
- { label: "설계 문서", value: "설계", count: 0 },
- ],
- },
- {
- id: "fileType",
- label: "파일 형식",
- options: [
- { label: "PDF", value: "pdf", count: 0 },
- { label: "Excel", value: "xlsx", count: 0 },
- { label: "Word", value: "docx", count: 0 },
- { label: "기타", value: "기타", count: 0 },
- ],
- },
- ]
-
- /**
- * 고급 필터 필드
- */
- const advancedFilterFields: DataTableAdvancedFilterField<any>[] = [
- {
- id: "fileName",
- label: "파일명",
- type: "text",
- },
- {
- id: "originalFileName",
- label: "원본 파일명",
- type: "text",
- },
- {
- id: "serialNo",
- label: "시리얼 번호",
- type: "text",
- },
- {
- id: "description",
- label: "설명",
- type: "text",
- },
- {
- id: "attachmentType",
- label: "문서 타입",
- type: "multi-select",
- options: [
- { label: "구매 문서", value: "구매" },
- { label: "설계 문서", value: "설계" },
- ],
- },
- {
- id: "fileType",
- label: "파일 형식",
- type: "multi-select",
- options: [
- { label: "PDF", value: "pdf" },
- { label: "Excel", value: "xlsx" },
- { label: "Word", value: "docx" },
- { label: "기타", value: "기타" },
- ],
- },
- {
- id: "createdAt",
- label: "등록일",
- type: "date",
- },
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => originalRow.id.toString(),
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <div className="space-y-6">
- {/* 메인 테이블 */}
- <div className="h-full w-full">
- <DataTable table={table} className="h-full">
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <RfqAttachmentsTableToolbarActions table={table} rfqId={rfqId} />
- </DataTableAdvancedToolbar>
- </DataTable>
- </div>
-
- {/* 벤더 응답 현황 패널 */}
- {selectedAttachment && (
- <>
- <Separator />
- <VendorResponsesPanel
- attachment={selectedAttachment}
- responses={vendorResponses}
- isLoading={isLoadingResponses}
- onRefresh={() => {
- // 새로고침 로직
- if (selectedAttachment) {
- setSelectedAttachment({ ...selectedAttachment })
- }
- }}
- />
- </>
- )}
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/attachment-toolbar-action.tsx b/lib/b-rfq/attachment/attachment-toolbar-action.tsx
deleted file mode 100644
index e078ea66..00000000
--- a/lib/b-rfq/attachment/attachment-toolbar-action.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-
-import { AddAttachmentDialog } from "./add-attachment-dialog"
-import { ConfirmDocumentsDialog } from "./confirm-documents-dialog"
-import { TbeRequestDialog } from "./tbe-request-dialog"
-import { DeleteAttachmentsDialog } from "./delete-attachment-dialog"
-
-interface RfqAttachmentsTableToolbarActionsProps {
- table: Table<any>
- rfqId: number
-}
-
-export function RfqAttachmentsTableToolbarActions({
- table,
- rfqId
-}: RfqAttachmentsTableToolbarActionsProps) {
-
- // 선택된 행들 가져오기
- const selectedRows = table.getFilteredSelectedRowModel().rows
- const selectedAttachments = selectedRows.map((row) => row.original)
- const selectedCount = selectedRows.length
-
- return (
- <div className="flex items-center gap-2">
- {/** 선택된 로우가 있으면 삭제 다이얼로그 */}
- {selectedCount > 0 && (
- <DeleteAttachmentsDialog
- attachments={selectedAttachments}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- />
- )}
-
- {/** 새 첨부 추가 다이얼로그 */}
- <AddAttachmentDialog rfqId={rfqId} />
-
- {/** 문서 확정 다이얼로그 */}
- <ConfirmDocumentsDialog
- rfqId={rfqId}
- onSuccess={() => {
- // 성공 후 필요한 작업 (예: 페이지 새로고침)
- window.location.reload()
- }}
- />
-
- {/** TBE 요청 다이얼로그 (선택된 행이 있을 때만 활성화) */}
- <TbeRequestDialog
- rfqId={rfqId}
- attachments={selectedAttachments}
- onSuccess={() => {
- // 선택 해제 및 페이지 새로고침
- table.toggleAllRowsSelected(false)
- window.location.reload()
- }}
- />
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/confirm-documents-dialog.tsx b/lib/b-rfq/attachment/confirm-documents-dialog.tsx
deleted file mode 100644
index fccb4123..00000000
--- a/lib/b-rfq/attachment/confirm-documents-dialog.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Loader, FileCheck } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-
-import { confirmDocuments } from "../service"
-
-interface ConfirmDocumentsDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- rfqId: number
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function ConfirmDocumentsDialog({
- rfqId,
- showTrigger = true,
- onSuccess,
- ...props
-}: ConfirmDocumentsDialogProps) {
- const [isConfirmPending, startConfirmTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onConfirm() {
- startConfirmTransition(async () => {
- const result = await confirmDocuments(rfqId)
-
- if (!result.success) {
- toast.error(result.message)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success(result.message)
- onSuccess?.()
- })
- }
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <FileCheck className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">문서 확정</span>
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>문서를 확정하시겠습니까?</DialogTitle>
- <DialogDescription>
- 이 작업은 RFQ의 모든 첨부문서를 확정하고 상태를 "Doc. Confirmed"로 변경합니다.
- 확정 후에는 문서 수정이 제한될 수 있습니다.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="Confirm documents"
- onClick={onConfirm}
- disabled={isConfirmPending}
- >
- {isConfirmPending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 문서 확정
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <FileCheck className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">문서 확정</span>
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>문서를 확정하시겠습니까?</DrawerTitle>
- <DrawerDescription>
- 이 작업은 RFQ의 모든 첨부문서를 확정하고 상태를 "Doc. Confirmed"로 변경합니다.
- 확정 후에는 문서 수정이 제한될 수 있습니다.
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="Confirm documents"
- onClick={onConfirm}
- disabled={isConfirmPending}
- >
- {isConfirmPending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- 문서 확정
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/delete-attachment-dialog.tsx b/lib/b-rfq/attachment/delete-attachment-dialog.tsx
deleted file mode 100644
index b5471520..00000000
--- a/lib/b-rfq/attachment/delete-attachment-dialog.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Row } from "@tanstack/react-table"
-import { Loader, Trash } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-import { deleteRfqAttachments } from "../service"
-
-
-// 첨부파일 타입 (실제 타입에 맞게 조정 필요)
-type RfqAttachment = {
- id: number
- serialNo: string
- originalFileName: string
- attachmentType: string
- currentRevision: string
-}
-
-interface DeleteAttachmentsDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- attachments: Row<RfqAttachment>["original"][]
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function DeleteAttachmentsDialog({
- attachments,
- showTrigger = true,
- onSuccess,
- ...props
-}: DeleteAttachmentsDialogProps) {
- const [isDeletePending, startDeleteTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onDelete() {
- startDeleteTransition(async () => {
- const result = await deleteRfqAttachments({
- ids: attachments.map((attachment) => attachment.id),
- })
-
- if (!result.success) {
- toast.error(result.message)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success(result.message)
- onSuccess?.()
- })
- }
-
- const attachmentText = attachments.length === 1 ? "첨부파일" : "첨부파일들"
- const deleteWarning = `선택된 ${attachments.length}개의 ${attachmentText}과 모든 리비전이 영구적으로 삭제됩니다.`
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({attachments.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
- <DialogDescription className="space-y-2">
- <div>이 작업은 되돌릴 수 없습니다.</div>
- <div>{deleteWarning}</div>
- {attachments.length <= 3 && (
- <div className="mt-3 p-2 bg-gray-50 rounded-md">
- <div className="font-medium text-sm">삭제될 파일:</div>
- <ul className="text-sm text-gray-600 mt-1">
- {attachments.map((attachment) => (
- <li key={attachment.id} className="truncate">
- • {attachment.serialNo}: {attachment.originalFileName} ({attachment.currentRevision})
- </li>
- ))}
- </ul>
- </div>
- )}
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="Delete selected attachments"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 삭제
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({attachments.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
- <DrawerDescription className="space-y-2">
- <div>이 작업은 되돌릴 수 없습니다.</div>
- <div>{deleteWarning}</div>
- {attachments.length <= 3 && (
- <div className="mt-3 p-2 bg-gray-50 rounded-md">
- <div className="font-medium text-sm">삭제될 파일:</div>
- <ul className="text-sm text-gray-600 mt-1">
- {attachments.map((attachment) => (
- <li key={attachment.id} className="truncate">
- • {attachment.serialNo}: {attachment.originalFileName} ({attachment.currentRevision})
- </li>
- ))}
- </ul>
- </div>
- )}
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="Delete selected attachments"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- 삭제
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/request-revision-dialog.tsx b/lib/b-rfq/attachment/request-revision-dialog.tsx
deleted file mode 100644
index 90d5b543..00000000
--- a/lib/b-rfq/attachment/request-revision-dialog.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-// components/rfq/request-revision-dialog.tsx
-"use client";
-
-import { useState, useTransition } from "react";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Textarea } from "@/components/ui/textarea";
-import { useForm } from "react-hook-form";
-import { zodResolver } from "@hookform/resolvers/zod";
-import * as z from "zod";
-import { AlertTriangle, Loader2 } from "lucide-react";
-import { useToast } from "@/hooks/use-toast";
-import { requestRevision } from "../service";
-
-const revisionFormSchema = z.object({
- revisionReason: z
- .string()
- .min(10, "수정 요청 사유를 최소 10자 이상 입력해주세요")
- .max(500, "수정 요청 사유는 500자를 초과할 수 없습니다"),
-});
-
-type RevisionFormData = z.infer<typeof revisionFormSchema>;
-
-interface RequestRevisionDialogProps {
- responseId: number;
- attachmentType: string;
- serialNo: string;
- vendorName?: string;
- currentRevision: string;
- trigger?: React.ReactNode;
- onSuccess?: () => void;
-}
-
-export function RequestRevisionDialog({
- responseId,
- attachmentType,
- serialNo,
- vendorName,
- currentRevision,
- trigger,
- onSuccess,
-}: RequestRevisionDialogProps) {
- const [open, setOpen] = useState(false);
- const [isPending, startTransition] = useTransition();
- const { toast } = useToast();
-
- const form = useForm<RevisionFormData>({
- resolver: zodResolver(revisionFormSchema),
- defaultValues: {
- revisionReason: "",
- },
- });
-
- const handleOpenChange = (newOpen: boolean) => {
- setOpen(newOpen);
- // 다이얼로그가 닫힐 때 form 리셋
- if (!newOpen) {
- form.reset();
- }
- };
-
- const handleCancel = () => {
- form.reset();
- setOpen(false);
- };
-
- const onSubmit = async (data: RevisionFormData) => {
- startTransition(async () => {
- try {
- const result = await requestRevision(responseId, data.revisionReason);
-
- if (!result.success) {
- throw new Error(result.message);
- }
-
- toast({
- title: "수정 요청 완료",
- description: result.message,
- });
-
- setOpen(false);
- form.reset();
- onSuccess?.();
-
- } catch (error) {
- console.error("Request revision error:", error);
- toast({
- title: "수정 요청 실패",
- description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
- variant: "destructive",
- });
- }
- });
- };
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogTrigger asChild>
- {trigger || (
- <Button size="sm" variant="outline">
- <AlertTriangle className="h-3 w-3 mr-1" />
- 수정요청
- </Button>
- )}
- </DialogTrigger>
- <DialogContent className="max-w-lg">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <AlertTriangle className="h-5 w-5 text-orange-600" />
- 수정 요청
- </DialogTitle>
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
- <Badge variant="outline">{serialNo}</Badge>
- <span>{attachmentType}</span>
- <Badge variant="secondary">{currentRevision}</Badge>
- {vendorName && (
- <>
- <span>•</span>
- <span>{vendorName}</span>
- </>
- )}
- </div>
- </DialogHeader>
-
- <div className="space-y-4">
- <div className="bg-orange-50 border border-orange-200 rounded-lg p-4">
- <div className="flex items-start gap-2">
- <AlertTriangle className="h-4 w-4 text-orange-600 mt-0.5 flex-shrink-0" />
- <div className="text-sm text-orange-800">
- <p className="font-medium mb-1">수정 요청 안내</p>
- <p>
- 벤더에게 현재 제출된 응답에 대한 수정을 요청합니다.
- 수정 요청 후 벤더는 새로운 파일을 다시 제출할 수 있습니다.
- </p>
- </div>
- </div>
- </div>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
- <FormField
- control={form.control}
- name="revisionReason"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-base font-medium">
- 수정 요청 사유 <span className="text-red-500">*</span>
- </FormLabel>
- <FormControl>
- <Textarea
- placeholder="수정이 필요한 구체적인 사유를 입력해주세요...&#10;예: 제출된 도면에서 치수 정보가 누락되었습니다."
- className="resize-none"
- rows={4}
- disabled={isPending}
- {...field}
- />
- </FormControl>
- <div className="flex justify-between text-xs text-muted-foreground">
- <FormMessage />
- <span>{field.value?.length || 0}/500</span>
- </div>
- </FormItem>
- )}
- />
-
- <div className="flex justify-end gap-2 pt-2">
- <Button
- type="button"
- variant="outline"
- onClick={handleCancel}
- disabled={isPending}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isPending}
- // className="bg-orange-600 hover:bg-orange-700"
- >
- {isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
- {isPending ? "요청 중..." : "수정 요청"}
- </Button>
- </div>
- </form>
- </Form>
- </div>
- </DialogContent>
- </Dialog>
- );
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/revision-dialog.tsx b/lib/b-rfq/attachment/revision-dialog.tsx
deleted file mode 100644
index d26abedb..00000000
--- a/lib/b-rfq/attachment/revision-dialog.tsx
+++ /dev/null
@@ -1,196 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { History, Download, Upload } from "lucide-react"
-import { formatDate, formatBytes } from "@/lib/utils"
-import { getAttachmentRevisions } from "../service"
-import { AddRevisionDialog } from "./add-revision-dialog"
-
-interface RevisionDialogProps {
- attachmentId: number
- currentRevision: string
- originalFileName: string
-}
-
-export function RevisionDialog({
- attachmentId,
- currentRevision,
- originalFileName
-}: RevisionDialogProps) {
- const [open, setOpen] = React.useState(false)
- const [revisions, setRevisions] = React.useState<any[]>([])
- const [isLoading, setIsLoading] = React.useState(false)
- const [isAddRevisionOpen, setIsAddRevisionOpen] = React.useState(false)
-
- // 리비전 목록 로드
- const loadRevisions = async () => {
- setIsLoading(true)
- try {
- const result = await getAttachmentRevisions(attachmentId)
-
- if (result.success) {
- setRevisions(result.revisions)
- } else {
- console.error("Failed to load revisions:", result.message)
- }
- } catch (error) {
- console.error("Failed to load revisions:", error)
- } finally {
- setIsLoading(false)
- }
- }
-
- React.useEffect(() => {
- if (open) {
- loadRevisions()
- }
- }, [open, attachmentId])
-
- return (
- <>
- <Dialog open={open} onOpenChange={setOpen}>
- <DialogTrigger asChild>
- <Button variant="ghost" size="sm" className="gap-2">
- <History className="h-4 w-4" />
- {currentRevision}
- </Button>
- </DialogTrigger>
-
- <DialogContent className="sm:max-w-[800px]" style={{maxWidth:800}}>
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <History className="h-5 w-5" />
- 리비전 히스토리: {originalFileName}
- </DialogTitle>
- <DialogDescription>
- 이 문서의 모든 버전을 확인하고 관리할 수 있습니다.
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- {/* 새 리비전 추가 버튼 */}
- <div className="flex justify-end">
- <Button
- onClick={() => setIsAddRevisionOpen(true)}
- className="gap-2"
- >
- <Upload className="h-4 w-4" />
- 새 리비전 추가
- </Button>
- </div>
-
- {/* 리비전 목록 */}
- {isLoading ? (
- <div className="text-center py-8">리비전을 불러오는 중...</div>
- ) : (
- <div className="border rounded-lg">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>리비전</TableHead>
- <TableHead>파일명</TableHead>
- <TableHead>크기</TableHead>
- <TableHead>업로드 일시</TableHead>
- <TableHead>업로드자</TableHead>
- <TableHead>코멘트</TableHead>
- <TableHead>액션</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {revisions.map((revision) => (
- <TableRow key={revision.id}>
- <TableCell>
- <div className="flex items-center gap-2">
- <Badge
- variant={revision.isLatest ? "default" : "outline"}
- >
- {revision.revisionNo}
- </Badge>
- {revision.isLatest && (
- <Badge variant="secondary" className="text-xs">
- 최신
- </Badge>
- )}
- </div>
- </TableCell>
-
- <TableCell>
- <div>
- <div className="font-medium">{revision.originalFileName}</div>
- </div>
- </TableCell>
-
- <TableCell>
- {formatBytes(revision.fileSize)}
- </TableCell>
-
- <TableCell>
- {formatDate(revision.createdAt, "KR")}
- </TableCell>
-
- <TableCell>
- {revision.createdByName || "-"}
- </TableCell>
-
- <TableCell>
- <div className="max-w-[200px] truncate" title={revision.revisionComment}>
- {revision.revisionComment || "-"}
- </div>
- </TableCell>
-
- <TableCell>
- <Button
- variant="ghost"
- size="sm"
- className="gap-2"
- onClick={() => {
- // 파일 다운로드
- window.open(revision.filePath, '_blank')
- }}
- >
- <Download className="h-4 w-4" />
- 다운로드
- </Button>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- )}
- </div>
- </DialogContent>
- </Dialog>
-
- {/* 새 리비전 추가 다이얼로그 */}
- <AddRevisionDialog
- open={isAddRevisionOpen}
- onOpenChange={setIsAddRevisionOpen}
- attachmentId={attachmentId}
- currentRevision={currentRevision}
- originalFileName={originalFileName}
- onSuccess={() => {
- loadRevisions() // 리비전 목록 새로고침
- }}
- />
- </>
- )
- } \ No newline at end of file
diff --git a/lib/b-rfq/attachment/tbe-request-dialog.tsx b/lib/b-rfq/attachment/tbe-request-dialog.tsx
deleted file mode 100644
index 80b20e6f..00000000
--- a/lib/b-rfq/attachment/tbe-request-dialog.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { Loader, Send } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-
-import { requestTbe } from "../service"
-
-// 첨부파일 타입
-type RfqAttachment = {
- id: number
- serialNo: string
- originalFileName: string
- attachmentType: string
- currentRevision: string
-}
-
-interface TbeRequestDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- rfqId: number
- attachments: RfqAttachment[]
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function TbeRequestDialog({
- rfqId,
- attachments,
- showTrigger = true,
- onSuccess,
- ...props
-}: TbeRequestDialogProps) {
- const [isRequestPending, startRequestTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onRequest() {
- startRequestTransition(async () => {
- const attachmentIds = attachments.map(attachment => attachment.id)
- const result = await requestTbe(rfqId, attachmentIds)
-
- if (!result.success) {
- toast.error(result.message)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success(result.message)
- onSuccess?.()
- })
- }
-
- const attachmentCount = attachments.length
- const attachmentText = attachmentCount === 1 ? "문서" : "문서들"
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- disabled={attachmentCount === 0}
- >
- <Send className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- TBE 요청 {attachmentCount > 0 && `(${attachmentCount})`}
- </span>
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>TBE 요청을 전송하시겠습니까?</DialogTitle>
- <DialogDescription className="space-y-2">
- <div>
- 선택된 <span className="font-medium">{attachmentCount}개</span>의 {attachmentText}에 대해
- 벤더들에게 기술평가(TBE) 요청을 전송합니다.
- </div>
- <div>RFQ 상태가 "TBE started"로 변경됩니다.</div>
- {attachmentCount <= 5 && (
- <div className="mt-3 p-2 bg-gray-50 rounded-md">
- <div className="font-medium text-sm">TBE 요청 대상:</div>
- <ul className="text-sm text-gray-600 mt-1">
- {attachments.map((attachment) => (
- <li key={attachment.id} className="truncate">
- • {attachment.serialNo}: {attachment.originalFileName} ({attachment.currentRevision})
- </li>
- ))}
- </ul>
- </div>
- )}
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="Request TBE"
- onClick={onRequest}
- disabled={isRequestPending}
- >
- {isRequestPending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- TBE 요청 전송
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- disabled={attachmentCount === 0}
- >
- <Send className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- TBE 요청 {attachmentCount > 0 && `(${attachmentCount})`}
- </span>
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>TBE 요청을 전송하시겠습니까?</DrawerTitle>
- <DrawerDescription className="space-y-2">
- <div>
- 선택된 <span className="font-medium">{attachmentCount}개</span>의 {attachmentText}에 대해
- 벤더들에게 기술평가(TBE) 요청을 전송합니다.
- </div>
- <div>RFQ 상태가 "TBE started"로 변경됩니다.</div>
- {attachmentCount <= 5 && (
- <div className="mt-3 p-2 bg-gray-50 rounded-md">
- <div className="font-medium text-sm">TBE 요청 대상:</div>
- <ul className="text-sm text-gray-600 mt-1">
- {attachments.map((attachment) => (
- <li key={attachment.id} className="truncate">
- • {attachment.serialNo}: {attachment.originalFileName} ({attachment.currentRevision})
- </li>
- ))}
- </ul>
- </div>
- )}
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="Request TBE"
- onClick={onRequest}
- disabled={isRequestPending}
- >
- {isRequestPending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- TBE 요청 전송
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/attachment/vendor-responses-panel.tsx b/lib/b-rfq/attachment/vendor-responses-panel.tsx
deleted file mode 100644
index 0cbe2a08..00000000
--- a/lib/b-rfq/attachment/vendor-responses-panel.tsx
+++ /dev/null
@@ -1,386 +0,0 @@
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
-import {
- RefreshCw,
- Download,
- MessageSquare,
- Clock,
- CheckCircle2,
- XCircle,
- AlertCircle,
- FileText,
- Files,
- AlertTriangle
-} from "lucide-react"
-import { formatDate, formatFileSize } from "@/lib/utils"
-import { RequestRevisionDialog } from "./request-revision-dialog"
-
-interface VendorResponsesPanelProps {
- attachment: any
- responses: any[]
- isLoading: boolean
- onRefresh: () => void
-}
-
-// 파일 다운로드 핸들러
-async function handleFileDownload(filePath: string, fileName: string, fileId: number) {
- try {
- const params = new URLSearchParams({
- path: filePath,
- type: "vendor",
- responseFileId: fileId.toString(),
- });
-
- const response = await fetch(`/api/rfq-attachments/download?${params.toString()}`);
-
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error || `Download failed: ${response.status}`);
- }
-
- const blob = await response.blob();
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = fileName;
- document.body.appendChild(link);
- link.click();
-
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
-
- console.log("✅ 파일 다운로드 성공:", fileName);
- } catch (error) {
- console.error("❌ 파일 다운로드 실패:", error);
- alert(`파일 다운로드에 실패했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`);
- }
-}
-
-// 파일 목록 컴포넌트
-function FilesList({ files }: { files: any[] }) {
- if (files.length === 0) {
- return (
- <div className="text-center py-4 text-muted-foreground text-sm">
- 업로드된 파일이 없습니다.
- </div>
- );
- }
-
- return (
- <div className="space-y-2 max-h-64 overflow-y-auto">
- {files.map((file, index) => (
- <div key={file.id} className="flex items-center justify-between p-3 border rounded-lg bg-green-50 border-green-200">
- <div className="flex items-center gap-2 flex-1 min-w-0">
- <FileText className="h-4 w-4 text-green-600 flex-shrink-0" />
- <div className="min-w-0 flex-1">
- <div className="font-medium text-sm truncate" title={file.originalFileName}>
- {file.originalFileName}
- </div>
- <div className="text-xs text-muted-foreground">
- {formatFileSize(file.fileSize)} • {formatDate(file.uploadedAt)}
- </div>
- {file.description && (
- <div className="text-xs text-muted-foreground italic mt-1" title={file.description}>
- {file.description}
- </div>
- )}
- </div>
- </div>
- <Button
- size="sm"
- variant="ghost"
- onClick={() => handleFileDownload(file.filePath, file.originalFileName, file.id)}
- className="flex-shrink-0 ml-2"
- title="파일 다운로드"
- >
- <Download className="h-4 w-4" />
- </Button>
- </div>
- ))}
- </div>
- );
-}
-
-export function VendorResponsesPanel({
- attachment,
- responses,
- isLoading,
- onRefresh
-}: VendorResponsesPanelProps) {
-
- console.log(responses)
-
- const getStatusIcon = (status: string) => {
- switch (status) {
- case 'RESPONDED':
- return <CheckCircle2 className="h-4 w-4 text-green-600" />
- case 'NOT_RESPONDED':
- return <Clock className="h-4 w-4 text-orange-600" />
- case 'WAIVED':
- return <XCircle className="h-4 w-4 text-gray-500" />
- case 'REVISION_REQUESTED':
- return <AlertCircle className="h-4 w-4 text-yellow-600" />
- default:
- return <Clock className="h-4 w-4 text-gray-400" />
- }
- }
-
- const getStatusBadgeVariant = (status: string) => {
- switch (status) {
- case 'RESPONDED':
- return 'default'
- case 'NOT_RESPONDED':
- return 'secondary'
- case 'WAIVED':
- return 'outline'
- case 'REVISION_REQUESTED':
- return 'destructive'
- default:
- return 'secondary'
- }
- }
-
- if (isLoading) {
- return (
- <div className="space-y-4">
- <div className="flex items-center justify-between">
- <Skeleton className="h-6 w-48" />
- <Skeleton className="h-9 w-24" />
- </div>
- <div className="space-y-3">
- {Array.from({ length: 3 }).map((_, i) => (
- <Skeleton key={i} className="h-12 w-full" />
- ))}
- </div>
- </div>
- )
- }
-
- return (
- <div className="space-y-4">
- {/* 헤더 */}
- <div className="flex items-center justify-between">
- <div className="space-y-1">
- <h3 className="text-lg font-medium flex items-center gap-2">
- <MessageSquare className="h-5 w-5" />
- 벤더 응답 현황: {attachment.originalFileName}
- </h3>
- <div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
- <Badge variant="outline">
- {attachment.attachmentType}
- </Badge>
- <span>시리얼: {attachment.serialNo}</span>
- <span>등록: {formatDate(attachment.createdAt)}</span>
- {attachment.responseStats && (
- <Badge variant="secondary">
- 응답률: {attachment.responseStats.responseRate}%
- </Badge>
- )}
- </div>
- </div>
- <Button
- variant="outline"
- size="sm"
- onClick={onRefresh}
- className="flex items-center gap-2"
- >
- <RefreshCw className="h-4 w-4" />
- 새로고침
- </Button>
- </div>
-
- {/* 테이블 */}
- {responses.length === 0 ? (
- <div className="text-center py-8 text-muted-foreground border rounded-lg">
- 이 문서에 대한 벤더 응답 정보가 없습니다.
- </div>
- ) : (
- <div className="border rounded-lg">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>벤더</TableHead>
- <TableHead>국가</TableHead>
- <TableHead>응답 상태</TableHead>
- <TableHead>리비전</TableHead>
- <TableHead>요청일</TableHead>
- <TableHead>응답일</TableHead>
- <TableHead>응답 파일</TableHead>
- <TableHead>코멘트</TableHead>
- <TableHead className="w-[100px]">액션</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {responses.map((response) => (
- <TableRow key={response.id}>
- <TableCell className="font-medium">
- <div>
- <div>{response.vendorName}</div>
- <div className="text-xs text-muted-foreground">
- {response.vendorCode}
- </div>
- </div>
- </TableCell>
-
- <TableCell>
- {response.vendorCountry}
- </TableCell>
-
- <TableCell>
- <div className="flex items-center gap-2">
- {getStatusIcon(response.responseStatus)}
- <Badge variant={getStatusBadgeVariant(response.responseStatus)}>
- {response.responseStatus}
- </Badge>
- </div>
- </TableCell>
-
- <TableCell>
- <div className="text-sm">
- <div>현재: {response.currentRevision}</div>
- {response.respondedRevision && (
- <div className="text-muted-foreground">
- 응답: {response.respondedRevision}
- </div>
- )}
- </div>
- </TableCell>
-
- <TableCell>
- {formatDate(response.requestedAt)}
- </TableCell>
-
- <TableCell>
- {response.respondedAt ? formatDate(response.respondedAt) : '-'}
- </TableCell>
-
- {/* 응답 파일 컬럼 */}
- <TableCell>
- {response.totalFiles > 0 ? (
- <div className="flex items-center gap-2">
- <Badge variant="secondary" className="text-xs">
- {response.totalFiles}개
- </Badge>
- {response.totalFiles === 1 ? (
- // 파일이 1개면 바로 다운로드
- <Button
- variant="ghost"
- size="sm"
- className="h-8 w-8 p-0"
- onClick={() => {
- const file = response.files[0];
- handleFileDownload(file.filePath, file.originalFileName, file.id);
- }}
- title={response.latestFile?.originalFileName}
- >
- <Download className="h-4 w-4" />
- </Button>
- ) : (
- // 파일이 여러 개면 Popover로 목록 표시
- <Popover>
- <PopoverTrigger asChild>
- <Button
- variant="ghost"
- size="sm"
- className="h-8 w-8 p-0"
- title="파일 목록 보기"
- >
- <Files className="h-4 w-4" />
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-96" align="start">
- <div className="space-y-2">
- <div className="font-medium text-sm">
- 응답 파일 목록 ({response.totalFiles}개)
- </div>
- <FilesList files={response.files} />
- </div>
- </PopoverContent>
- </Popover>
- )}
- </div>
- ) : (
- <span className="text-muted-foreground text-sm">-</span>
- )}
- </TableCell>
-
- <TableCell>
- <div className="space-y-1 max-w-[200px]">
- {/* 벤더 응답 코멘트 */}
- {response.responseComment && (
- <div className="flex items-center gap-1">
- <div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" title="벤더 응답 코멘트"></div>
- <div className="text-xs text-blue-600 truncate" title={response.responseComment}>
- {response.responseComment}
- </div>
- </div>
- )}
-
- {/* 수정 요청 사유 */}
- {response.revisionRequestComment && (
- <div className="flex items-center gap-1">
- <div className="w-2 h-2 rounded-full bg-red-500 flex-shrink-0" title="수정 요청 사유"></div>
- <div className="text-xs text-red-600 truncate" title={response.revisionRequestComment}>
- {response.revisionRequestComment}
- </div>
- </div>
- )}
-
- {!response.responseComment && !response.revisionRequestComment && (
- <span className="text-muted-foreground text-sm">-</span>
- )}
- </div>
- </TableCell>
-
- {/* 액션 컬럼 - 수정 요청 기능으로 변경 */}
- <TableCell>
- <div className="flex items-center gap-1">
- {response.responseStatus === 'RESPONDED' && (
- <RequestRevisionDialog
- responseId={response.id}
- attachmentType={attachment.attachmentType}
- serialNo={attachment.serialNo}
- vendorName={response.vendorName}
- currentRevision={response.currentRevision}
- onSuccess={onRefresh}
- trigger={
- <Button
- variant="outline"
- size="sm"
- className="h-8 px-2"
- title="수정 요청"
- >
- <AlertTriangle className="h-3 w-3 mr-1" />
- 수정요청
- </Button>
- }
- />
- )}
-
- {response.responseStatus === 'REVISION_REQUESTED' && (
- <Badge variant="secondary" className="text-xs">
- 수정 요청됨
- </Badge>
- )}
-
- {(response.responseStatus === 'NOT_RESPONDED' || response.responseStatus === 'WAIVED') && (
- <span className="text-muted-foreground text-xs">-</span>
- )}
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </div>
- )}
- </div>
- )
-} \ No newline at end of file