summaryrefslogtreecommitdiff
path: root/lib/cbe/table
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-01 19:52:06 +0900
commit44b74ff4170090673b6eeacd8c528e0abf47b7aa (patch)
tree3f3824b4e2cb24536c1677188b4cae5b8909d3da /lib/cbe/table
parent4953e770929b82ef77da074f77071ebd0f428529 (diff)
(김준회) deprecated code 정리
Diffstat (limited to 'lib/cbe/table')
-rw-r--r--lib/cbe/table/cbe-table-columns.tsx241
-rw-r--r--lib/cbe/table/cbe-table-toolbar-actions.tsx72
-rw-r--r--lib/cbe/table/cbe-table.tsx192
-rw-r--r--lib/cbe/table/comments-sheet.tsx345
-rw-r--r--lib/cbe/table/invite-vendors-dialog.tsx428
5 files changed, 0 insertions, 1278 deletions
diff --git a/lib/cbe/table/cbe-table-columns.tsx b/lib/cbe/table/cbe-table-columns.tsx
deleted file mode 100644
index 552a0249..00000000
--- a/lib/cbe/table/cbe-table-columns.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { Download, Ellipsis, MessageSquare } from "lucide-react"
-import { toast } from "sonner"
-
-import { getErrorMessage } from "@/lib/handle-error"
-import { formatDate } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuRadioGroup,
- DropdownMenuRadioItem,
- DropdownMenuSub,
- DropdownMenuSubContent,
- DropdownMenuSubTrigger,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { useRouter } from "next/navigation"
-
-import { VendorWithCbeFields,vendorCbeColumnsConfig } from "@/config/vendorCbeColumnsConfig"
-
-
-type NextRouter = ReturnType<typeof useRouter>
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<
- React.SetStateAction<DataTableRowAction<VendorWithCbeFields> | null>
- >
- router: NextRouter
- openCommentSheet: (responseId: number) => void
- openVendorContactsDialog: (vendorId: number, vendor: VendorWithCbeFields) => void // 수정된 시그니처
-
-}
-
-/**
- * tanstack table 컬럼 정의 (중첩 헤더 버전)
- */
-export function getColumns({
- setRowAction,
- router,
- openCommentSheet,
- openVendorContactsDialog
-}: GetColumnsProps): ColumnDef<VendorWithCbeFields>[] {
- // ----------------------------------------------------------------
- // 1) Select 컬럼 (체크박스)
- // ----------------------------------------------------------------
- const selectColumn: ColumnDef<VendorWithCbeFields> = {
- 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,
- }
-
- // ----------------------------------------------------------------
- // 2) 그룹화(Nested) 컬럼 구성
- // ----------------------------------------------------------------
- const groupMap: Record<string, ColumnDef<VendorWithCbeFields>[]> = {}
-
- vendorCbeColumnsConfig.forEach((cfg) => {
- const groupName = cfg.group || "_noGroup"
- if (!groupMap[groupName]) {
- groupMap[groupName] = []
- }
-
- // childCol: ColumnDef<VendorWithTbeFields>
- const childCol: ColumnDef<VendorWithCbeFields> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- meta: {
- excelHeader: cfg.excelHeader,
- group: cfg.group,
- type: cfg.type,
- },
- // 셀 렌더링
- cell: ({ row, getValue }) => {
- // 1) 필드값 가져오기
- const val = getValue()
-
- if (cfg.id === "vendorName") {
- const vendor = row.original;
- const vendorId = vendor.vendorId;
-
- // 협력업체 이름을 클릭할 수 있는 버튼으로 렌더링
- const handleVendorNameClick = () => {
- if (vendorId) {
- openVendorContactsDialog(vendorId, vendor); // vendor 전체 객체 전달
- } else {
- toast.error("협력업체 ID를 찾을 수 없습니다.");
- }
- };
-
- return (
- <Button
- variant="link"
- className="p-0 h-auto text-left font-normal justify-start hover:underline"
- onClick={handleVendorNameClick}
- >
- {val as string}
- </Button>
- );
- }
-
-
- if (cfg.id === "vendorStatus") {
- const statusVal = row.original.vendorStatus
- if (!statusVal) return null
- // const Icon = getStatusIcon(statusVal)
- return (
- <Badge variant="outline">
- {statusVal}
- </Badge>
- )
- }
-
-
- if (cfg.id === "responseStatus") {
- const statusVal = row.original.responseStatus
- if (!statusVal) return null
- // const Icon = getStatusIcon(statusVal)
- const variant = statusVal ==="INVITED"?"default" :statusVal ==="DECLINED"?"destructive":statusVal ==="ACCEPTED"?"secondary":"outline"
- return (
- <Badge variant={variant}>
- {statusVal}
- </Badge>
- )
- }
-
- // 예) CBE Updated (날짜)
- if (cfg.id === "respondedAt") {
- const dateVal = val as Date | undefined
- if (!dateVal) return null
- return formatDate(dateVal, "KR")
- }
-
- // 그 외 필드는 기본 값 표시
- return val ?? ""
- },
- }
-
- groupMap[groupName].push(childCol)
- })
-
- // groupMap → nestedColumns
- const nestedColumns: ColumnDef<VendorWithCbeFields>[] = []
- Object.entries(groupMap).forEach(([groupName, colDefs]) => {
- if (groupName === "_noGroup") {
- nestedColumns.push(...colDefs)
- } else {
- nestedColumns.push({
- id: groupName,
- header: groupName,
- columns: colDefs,
- })
- }
- })
-
-// 댓글 칼럼
-const commentsColumn: ColumnDef<VendorWithCbeFields> = {
- id: "comments",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Comments" />
- ),
- cell: ({ row }) => {
- const vendor = row.original
- const commCount = vendor.comments?.length ?? 0
-
- function handleClick() {
- // setRowAction() 로 type 설정
- setRowAction({ row, type: "comments" })
- // 필요하면 즉시 openCommentSheet() 직접 호출
- openCommentSheet(vendor.responseId ?? 0)
- }
-
- return (
- <Button
- variant="ghost"
- size="sm"
- className="relative h-8 w-8 p-0 group"
- onClick={handleClick}
- aria-label={
- commCount > 0 ? `View ${commCount} comments` : "No comments"
- }
- >
- <MessageSquare className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
- {commCount > 0 && (
- <Badge
- variant="secondary"
- className="pointer-events-none absolute -top-1 -right-1 h-4 min-w-[1rem] p-0 text-[0.625rem] leading-none flex items-center justify-center"
- >
- {commCount}
- </Badge>
- )}
- <span className="sr-only">
- {commCount > 0 ? `${commCount} Comments` : "No Comments"}
- </span>
- </Button>
- )
- },
- enableSorting: false,
- minSize: 80,
-}
-// ----------------------------------------------------------------
-// 5) 최종 컬럼 배열 - Update to include the files column
-// ----------------------------------------------------------------
-return [
- selectColumn,
- ...nestedColumns,
- commentsColumn,
- // actionsColumn,
-]
-
-} \ No newline at end of file
diff --git a/lib/cbe/table/cbe-table-toolbar-actions.tsx b/lib/cbe/table/cbe-table-toolbar-actions.tsx
deleted file mode 100644
index 34b5b46c..00000000
--- a/lib/cbe/table/cbe-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, Upload } from "lucide-react"
-import { toast } from "sonner"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-
-
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-
-interface VendorsTableToolbarActionsProps {
- table: Table<VendorWithCbeFields>
- rfqId: number
-}
-
-export function VendorsTableToolbarActions({ table, rfqId }: VendorsTableToolbarActionsProps) {
- // 파일 input을 숨기고, 버튼 클릭 시 참조해 클릭하는 방식
- const fileInputRef = React.useRef<HTMLInputElement>(null)
-
- // 파일이 선택되었을 때 처리
-
- function handleImportClick() {
- // 숨겨진 <input type="file" /> 요소를 클릭
- fileInputRef.current?.click()
- }
-
- const uniqueRfqIds = table.getFilteredSelectedRowModel().rows.length > 0
- ? [...new Set(table.getFilteredSelectedRowModel().rows.map(row => row.original.rfqId))]
- : [];
-
-const hasMultipleRfqIds = uniqueRfqIds.length > 1;
-
-const invitationPossibeVendors = React.useMemo(() => {
- return table
- .getFilteredSelectedRowModel()
- .rows
- .map(row => row.original)
- .filter(vendor => vendor.commercialResponseStatus === null);
-}, [table.getFilteredSelectedRowModel().rows]);
-
-return (
- <div className="flex items-center gap-2">
- {invitationPossibeVendors.length > 0 && (
- <InviteVendorsDialog
- vendors={invitationPossibeVendors}
- rfqId={rfqId}
- onSuccess={() => table.toggleAllRowsSelected(false)}
- hasMultipleRfqIds={hasMultipleRfqIds}
- />
- )}
-
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "tasks",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/cbe/table/cbe-table.tsx b/lib/cbe/table/cbe-table.tsx
deleted file mode 100644
index 38a0a039..00000000
--- a/lib/cbe/table/cbe-table.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { toSentenceCase } from "@/lib/utils"
-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 { getColumns } from "./cbe-table-columns"
-import { CommentSheet, CbeComment } from "./comments-sheet"
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { fetchRfqAttachmentsbyCommentId, getAllCBE } from "@/lib/rfqs/service"
-import { VendorsTableToolbarActions } from "./cbe-table-toolbar-actions"
-import { InviteVendorsDialog } from "./invite-vendors-dialog"
-import { VendorContactsDialog } from "@/lib/rfqs/cbe-table/vendor-contact-dialog"
-import { useSession } from "next-auth/react" // Next-auth session hook 추가
-
-
-
-import { toast } from "sonner"
-
-interface VendorsTableProps {
- promises: Promise<[
- Awaited<ReturnType<typeof getAllCBE>>,
- ]>
-}
-
-export function AllCbeTable({ promises }: VendorsTableProps) {
-
- // Suspense로 받아온 데이터
- const [{ data, pageCount }] = React.use(promises)
- const { data: session } = useSession() // 세션 정보 가져오기
-
- const currentUserId = session?.user?.id ? parseInt(session.user.id, 10) : 0
- const currentUser = session?.user
-
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorWithCbeFields> | null>(null)
- // **router** 획득
- const router = useRouter()
- // 댓글 시트 관련 state
- const [initialComments, setInitialComments] = React.useState<CbeComment[]>([])
- const [isLoadingComments, setIsLoadingComments] = React.useState(false)
-
- const [commentSheetOpen, setCommentSheetOpen] = React.useState(false)
- const [selectedVendorId, setSelectedVendorId] = React.useState<number | null>(null)
- const [selectedCbeId, setSelectedCbeId] = React.useState<number | null>(null)
- const [isContactDialogOpen, setIsContactDialogOpen] = React.useState(false)
- const [selectedVendor, setSelectedVendor] = React.useState<VendorWithCbeFields | null>(null)
- const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null)
-
- // -----------------------------------------------------------
- // 특정 action이 설정될 때마다 실행되는 effect
- // -----------------------------------------------------------
- React.useEffect(() => {
- if (rowAction?.type === "comments") {
- // rowAction가 새로 세팅된 뒤 여기서 openCommentSheet 실행
- openCommentSheet(Number(rowAction.row.original.responseId))
- }
- }, [rowAction])
-
- // -----------------------------------------------------------
- // 댓글 시트 열기
- // -----------------------------------------------------------
- async function openCommentSheet(responseId: number) {
- setInitialComments([])
- setIsLoadingComments(true)
- const comments = rowAction?.row.original.comments
- const rfqId = rowAction?.row.original.rfqId
- const vendorId = rowAction?.row.original.vendorId
- try {
- if (comments && comments.length > 0) {
- const commentWithAttachments: CbeComment[] = await Promise.all(
- comments.map(async (c) => {
- const attachments = await fetchRfqAttachmentsbyCommentId(c.id)
-
- return {
- ...c,
- commentedBy: currentUserId, // DB나 API 응답에 있다고 가정
- attachments,
- }
- })
- )
- // 3) state에 저장 -> CommentSheet에서 initialComments로 사용
- setInitialComments(commentWithAttachments)
- }
-
- if(vendorId){ setSelectedVendorId(vendorId)}
- if(rfqId){ setSelectedRfqId(rfqId)}
- setSelectedCbeId(responseId)
- setCommentSheetOpen(true)
- }catch (error) {
- console.error("Error loading comments:", error)
- toast.error("Failed to load comments")
- } finally {
- // End loading regardless of success/failure
- setIsLoadingComments(false)
- }
-}
-
-const openVendorContactsDialog = (vendorId: number, vendor: VendorWithCbeFields) => {
- setSelectedVendorId(vendorId)
- setSelectedVendor(vendor)
- setIsContactDialogOpen(true)
-}
-
- // -----------------------------------------------------------
- // 테이블 컬럼
- // -----------------------------------------------------------
- const columns = React.useMemo(
- () => getColumns({ setRowAction, router, openCommentSheet, openVendorContactsDialog }),
- [setRowAction, router]
- )
-
- // -----------------------------------------------------------
- // 필터 필드
- // -----------------------------------------------------------
- const filterFields: DataTableFilterField<VendorWithCbeFields>[] = [
- // 예: 표준 필터
- ]
- const advancedFilterFields: DataTableAdvancedFilterField<VendorWithCbeFields>[] = [
- { id: "vendorName", label: "Vendor Name", type: "text" },
- { id: "vendorCode", label: "Vendor Code", type: "text" },
- { id: "respondedAt", label: "Updated at", type: "date" },
- ]
-
- // -----------------------------------------------------------
- // 테이블 생성 훅
- // -----------------------------------------------------------
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "respondedAt", desc: true }],
- columnPinning: { right: ["comments"] },
- },
- getRowId: (originalRow) => (`${originalRow.vendorId}${originalRow.rfqId}`),
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <>
- <DataTable table={table}>
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <VendorsTableToolbarActions table={table} rfqId={selectedRfqId ?? 0} />
- </DataTableAdvancedToolbar>
- </DataTable>
-
- {/* 댓글 시트 */}
- <CommentSheet
- currentUserId={currentUserId}
- open={commentSheetOpen}
- onOpenChange={setCommentSheetOpen}
- vendorId={selectedVendorId ?? 0}
- rfqId={selectedRfqId ?? 0}
- cbeId={selectedCbeId ?? 0}
- isLoading={isLoadingComments}
- initialComments={initialComments}
- />
-
- <InviteVendorsDialog
- vendors={rowAction?.row.original ? [rowAction?.row.original] : []}
- onOpenChange={() => setRowAction(null)}
- rfqId={selectedRfqId ?? 0}
- open={rowAction?.type === "invite"}
- showTrigger={false}
- currentUser={currentUser}
- />
-
- <VendorContactsDialog
- isOpen={isContactDialogOpen}
- onOpenChange={setIsContactDialogOpen}
- vendorId={selectedVendorId}
- vendor={selectedVendor}
- />
- </>
- )
-} \ No newline at end of file
diff --git a/lib/cbe/table/comments-sheet.tsx b/lib/cbe/table/comments-sheet.tsx
deleted file mode 100644
index b4647e7a..00000000
--- a/lib/cbe/table/comments-sheet.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm, useFieldArray } from "react-hook-form"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Loader, Download, X ,Loader2} from "lucide-react"
-import prettyBytes from "pretty-bytes"
-import { toast } from "sonner"
-
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import { Button } from "@/components/ui/button"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Textarea,
-} from "@/components/ui/textarea"
-
-import {
- Dropzone,
- DropzoneZone,
- DropzoneUploadIcon,
- DropzoneTitle,
- DropzoneDescription,
- DropzoneInput
-} from "@/components/ui/dropzone"
-
-import {
- Table,
- TableHeader,
- TableRow,
- TableHead,
- TableBody,
- TableCell
-} from "@/components/ui/table"
-
-// DB 스키마에서 필요한 타입들을 가져온다고 가정
-// (실제 프로젝트에 맞춰 import를 수정하세요.)
-import { formatDate } from "@/lib/utils"
-import { createRfqCommentWithAttachments } from "@/lib/rfqs/service"
-
-// 코멘트 + 첨부파일 구조 (단순 예시)
-// 실제 DB 스키마에 맞춰 조정
-export interface CbeComment {
- id: number
- commentText: string
- commentedBy?: number
- commentedByEmail?: string
- createdAt?: Date
- attachments?: {
- id: number
- fileName: string
- filePath: string
- }[]
-}
-
-interface CommentSheetProps extends React.ComponentPropsWithRef<typeof Sheet> {
- initialComments?: CbeComment[]
- currentUserId: number
- rfqId: number
- // tbeId?: number
- cbeId?: number
- vendorId: number
- onCommentsUpdated?: (comments: CbeComment[]) => void
- isLoading?: boolean // New prop
-}
-
-// 새 코멘트 작성 폼 스키마
-const commentFormSchema = z.object({
- commentText: z.string().min(1, "댓글을 입력하세요."),
- newFiles: z.array(z.any()).optional() // File[]
-})
-type CommentFormValues = z.infer<typeof commentFormSchema>
-
-const MAX_FILE_SIZE = 30e6 // 30MB
-
-export function CommentSheet({
- rfqId,
- vendorId,
- initialComments = [],
- currentUserId,
- // tbeId,
- cbeId,
- onCommentsUpdated,
- isLoading = false, // Default to false
- ...props
-}: CommentSheetProps) {
- const [comments, setComments] = React.useState<CbeComment[]>(initialComments)
- const [isPending, startTransition] = React.useTransition()
-
- React.useEffect(() => {
- setComments(initialComments)
- }, [initialComments])
-
-
- // RHF 세팅
- const form = useForm<CommentFormValues>({
- resolver: zodResolver(commentFormSchema),
- defaultValues: {
- commentText: "",
- newFiles: []
- }
- })
-
- // formFieldArray 예시 (파일 목록)
- const { fields: newFileFields, append, remove } = useFieldArray({
- control: form.control,
- name: "newFiles"
- })
-
- // (A) 기존 코멘트 렌더링
- function renderExistingComments() {
-
- if (isLoading) {
- return (
- <div className="flex justify-center items-center h-32">
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
- <span className="ml-2 text-sm text-muted-foreground">Loading comments...</span>
- </div>
- )
- }
-
- if (comments.length === 0) {
- return <p className="text-sm text-muted-foreground">No comments yet</p>
- }
-
- return (
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-1/2">Comment</TableHead>
- <TableHead>Attachments</TableHead>
- <TableHead>Created At</TableHead>
- <TableHead>Created By</TableHead>
- </TableRow>
- </TableHeader>
- <TableBody>
- {comments.map((c) => (
- <TableRow key={c.id}>
- <TableCell>{c.commentText}</TableCell>
- <TableCell>
- {/* 첨부파일 표시 */}
- {!c.attachments?.length && (
- <span className="text-sm text-muted-foreground">No files</span>
- )}
- {c.attachments?.length && (
- <div className="flex flex-col gap-1">
- {c.attachments.map((att) => (
- <div key={att.id} className="flex items-center gap-2">
- <a
- href={`/api/rfq-download?path=${encodeURIComponent(att.filePath)}`}
- download
- target="_blank"
- rel="noreferrer"
- className="inline-flex items-center gap-1 text-blue-600 underline"
- >
- <Download className="h-4 w-4" />
- {att.fileName}
- </a>
- </div>
- ))}
- </div>
- )}
- </TableCell>
- <TableCell> { c.createdAt ? formatDate(c.createdAt, "KR"): "-"}</TableCell>
- <TableCell>
- {c.commentedByEmail ?? "-"}
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- )
- }
-
- // 2) 새 파일 Drop
- function handleDropAccepted(files: File[]) {
- append(files)
- }
-
-
- // 3) 저장(Submit)
- async function onSubmit(data: CommentFormValues) {
-
- if (!rfqId) return
- startTransition(async () => {
- try {
- // console.log("rfqId", rfqId)
- // console.log("vendorId", vendorId)
- // console.log("cbeId", cbeId)
- // console.log("currentUserId", currentUserId)
- const res = await createRfqCommentWithAttachments({
- rfqId: rfqId,
- vendorId: vendorId, // 필요시 세팅
- commentText: data.commentText,
- commentedBy: currentUserId,
- evaluationId: null, // 필요시 세팅
- cbeId: cbeId,
- files: data.newFiles
- })
-
- if (!res.ok) {
- throw new Error("Failed to create comment")
- }
-
- toast.success("Comment created")
-
- // 새 코멘트를 다시 불러오거나,
- // 여기서는 임시로 "새로운 코멘트가 추가됐다" 라고 가정하여 클라이언트에서 상태 업데이트
- const newComment: CbeComment = {
- id: res.commentId, // 서버에서 반환된 commentId
- commentText: data.commentText,
- commentedBy: currentUserId,
- createdAt: res.createdAt,
- attachments: (data.newFiles?.map((f, idx) => ({
- id: Math.random() * 100000,
- fileName: f.name,
- filePath: "/uploads/" + f.name,
- })) || [])
- }
- setComments((prev) => [...prev, newComment])
- onCommentsUpdated?.([...comments, newComment])
-
- // 폼 리셋
- form.reset()
- } catch (err: any) {
- console.error(err)
- toast.error("Error: " + err.message)
- }
- })
- }
-
- return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col gap-6 sm:max-w-lg">
- <SheetHeader className="text-left">
- <SheetTitle>Comments</SheetTitle>
- <SheetDescription>
- 필요시 첨부파일과 함께 문의/코멘트를 남길 수 있습니다.
- </SheetDescription>
- </SheetHeader>
-
- {/* 기존 코멘트 목록 */}
- <div className="max-h-[300px] overflow-y-auto">
- {renderExistingComments()}
- </div>
-
- {/* 새 코멘트 작성 Form */}
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
- <FormField
- control={form.control}
- name="commentText"
- render={({ field }) => (
- <FormItem>
- <FormLabel>New Comment</FormLabel>
- <FormControl>
- <Textarea
- placeholder="Enter your comment..."
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Dropzone (파일 첨부) */}
- <Dropzone
- maxSize={MAX_FILE_SIZE}
- onDropAccepted={handleDropAccepted}
- onDropRejected={(rej) => {
- toast.error("File rejected: " + (rej[0]?.file?.name || ""))
- }}
- >
- {({ maxSize }) => (
- <DropzoneZone className="flex justify-center">
- <DropzoneInput />
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>Drop to attach files</DropzoneTitle>
- <DropzoneDescription>
- Max size: {prettyBytes(maxSize || 0)}
- </DropzoneDescription>
- </div>
- </div>
- </DropzoneZone>
- )}
- </Dropzone>
-
- {/* 선택된 파일 목록 */}
- {newFileFields.length > 0 && (
- <div className="flex flex-col gap-2">
- {newFileFields.map((field, idx) => {
- const file = form.getValues(`newFiles.${idx}`)
- if (!file) return null
- return (
- <div key={field.id} className="flex items-center justify-between border rounded p-2">
- <span className="text-sm">{file.name} ({prettyBytes(file.size)})</span>
- <Button
- variant="ghost"
- size="icon"
- type="button"
- onClick={() => remove(idx)}
- >
- <X className="h-4 w-4" />
- </Button>
- </div>
- )
- })}
- </div>
- )}
-
- <SheetFooter className="gap-2 pt-4">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- Cancel
- </Button>
- </SheetClose>
- <Button disabled={isPending}>
- {isPending && <Loader className="mr-2 h-4 w-4 animate-spin" />}
- Save
- </Button>
- </SheetFooter>
- </form>
- </Form>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file
diff --git a/lib/cbe/table/invite-vendors-dialog.tsx b/lib/cbe/table/invite-vendors-dialog.tsx
deleted file mode 100644
index 38edddc1..00000000
--- a/lib/cbe/table/invite-vendors-dialog.tsx
+++ /dev/null
@@ -1,428 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Loader, Send, User } from "lucide-react"
-import { toast } from "sonner"
-import { z } from "zod"
-
-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 { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- FormDescription,
-} from "@/components/ui/form"
-import { type Row } from "@tanstack/react-table"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-
-import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig"
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
-import { createCbeEvaluation } from "@/lib/rfqs/service"
-
-// 컴포넌트 내부에서 사용할 폼 스키마 정의
-const formSchema = z.object({
- paymentTerms: z.string().min(1, "지급 조건을 입력하세요"),
- incoterms: z.string().min(1, "Incoterms를 입력하세요"),
- deliverySchedule: z.string().min(1, "배송 일정을 입력하세요"),
- notes: z.string().optional(),
-})
-
-type FormValues = z.infer<typeof formSchema>
-
-interface InviteVendorsDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- rfqId: number
- vendors: Row<VendorWithCbeFields>["original"][]
- currentUserId?: number
- currentUser?: {
- id: string
- name?: string | null
- email?: string | null
- image?: string | null
- companyId?: number | null
- domain?: string | null
- }
- showTrigger?: boolean
- onSuccess?: () => void
- hasMultipleRfqIds?: boolean
-}
-
-export function InviteVendorsDialog({
- rfqId,
- vendors,
- currentUserId,
- currentUser,
- showTrigger = true,
- onSuccess,
- hasMultipleRfqIds,
- ...props
-}: InviteVendorsDialogProps) {
- const [files, setFiles] = React.useState<FileList | null>(null)
- const isDesktop = useMediaQuery("(min-width: 640px)")
- const [isSubmitting, setIsSubmitting] = React.useState(false)
-
- // 로컬 스키마와 폼 값을 사용하도록 수정
- const form = useForm<FormValues>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- paymentTerms: "",
- incoterms: "",
- deliverySchedule: "",
- notes: "",
- },
- mode: "onChange",
- })
-
- // 폼 상태 감시
- const { formState } = form
- const isValid = formState.isValid &&
- !!form.getValues("paymentTerms") &&
- !!form.getValues("incoterms") &&
- !!form.getValues("deliverySchedule")
-
- // 디버깅용 상태 트래킹
- React.useEffect(() => {
- const subscription = form.watch((value) => {
- // 폼 값이 변경될 때마다 실행되는 콜백
- console.log("Form values changed:", value);
- });
-
- return () => subscription.unsubscribe();
- }, [form]);
-
- async function onSubmit(data: FormValues) {
- try {
- setIsSubmitting(true)
-
- // 기본 FormData 생성
- const formData = new FormData()
-
- // rfqId 추가
- formData.append("rfqId", String(rfqId))
-
- // 폼 데이터 추가
- Object.entries(data).forEach(([key, value]) => {
- if (value !== undefined && value !== null) {
- formData.append(key, String(value))
- }
- })
-
- // 현재 사용자 ID 추가
- if (currentUserId) {
- formData.append("evaluatedBy", String(currentUserId))
- }
-
- // 협력업체 ID만 추가 (서버에서 연락처 정보를 조회)
- vendors.forEach((vendor) => {
- formData.append("vendorIds[]", String(vendor.vendorId))
- })
-
- // 파일 추가 (있는 경우에만)
- if (files && files.length > 0) {
- for (let i = 0; i < files.length; i++) {
- formData.append("files", files[i])
- }
- }
-
- // 서버 액션 호출
- const response = await createCbeEvaluation(formData)
-
- if (response.error) {
- toast.error(response.error)
- return
- }
-
- // 성공 처리
- toast.success(`${vendors.length}개 협력업체에 CBE 평가가 성공적으로 전송되었습니다!`)
- form.reset()
- setFiles(null)
- props.onOpenChange?.(false)
- onSuccess?.()
- } catch (error) {
- console.error(error)
- toast.error("CBE 평가 생성 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- function handleDialogOpenChange(nextOpen: boolean) {
- if (!nextOpen) {
- form.reset()
- setFiles(null)
- }
- props.onOpenChange?.(nextOpen)
- }
-
- // 필수 필드 라벨에 추가할 요소
- const RequiredLabel = (
- <span className="text-destructive ml-1 font-medium">*</span>
- )
-
- const formContent = (
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
- {/* 선택된 협력업체 정보 표시 */}
- <div className="space-y-2">
- <FormLabel>선택된 협력업체 ({vendors.length})</FormLabel>
- <ScrollArea className="h-20 border rounded-md p-2">
- <div className="flex flex-wrap gap-2">
- {vendors.map((vendor, index) => (
- <Badge key={index} variant="secondary" className="py-1">
- {vendor.vendorName || `협력업체 #${vendor.vendorCode}`}
- </Badge>
- ))}
- </div>
- </ScrollArea>
- <FormDescription>
- 선택된 모든 협력업체의 등록된 연락처에게 CBE 평가 알림이 전송됩니다.
- </FormDescription>
- </div>
-
- {/* 작성자 정보 (읽기 전용) */}
- {currentUser && (
- <div className="border rounded-md p-3 space-y-2">
- <FormLabel>작성자</FormLabel>
- <div className="flex items-center gap-3">
- {currentUser.image ? (
- <Avatar className="h-8 w-8">
- <AvatarImage src={currentUser.image} alt={currentUser.name || ""} />
- <AvatarFallback>
- {currentUser.name?.charAt(0) || <User className="h-4 w-4" />}
- </AvatarFallback>
- </Avatar>
- ) : (
- <Avatar className="h-8 w-8">
- <AvatarFallback>
- {currentUser.name?.charAt(0) || <User className="h-4 w-4" />}
- </AvatarFallback>
- </Avatar>
- )}
- <div>
- <p className="text-sm font-medium">{currentUser.name || "Unknown User"}</p>
- <p className="text-xs text-muted-foreground">{currentUser.email || ""}</p>
- </div>
- </div>
- </div>
- )}
-
- {/* 지급 조건 - 필수 필드 */}
- <FormField
- control={form.control}
- name="paymentTerms"
- render={({ field }) => (
- <FormItem>
- <FormLabel>
- 지급 조건{RequiredLabel}
- </FormLabel>
- <FormControl>
- <Input {...field} placeholder="예: Net 30" />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Incoterms - 필수 필드 */}
- <FormField
- control={form.control}
- name="incoterms"
- render={({ field }) => (
- <FormItem>
- <FormLabel>
- Incoterms{RequiredLabel}
- </FormLabel>
- <FormControl>
- <Input {...field} placeholder="예: FOB, CIF" />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 배송 일정 - 필수 필드 */}
- <FormField
- control={form.control}
- name="deliverySchedule"
- render={({ field }) => (
- <FormItem>
- <FormLabel>
- 배송 일정{RequiredLabel}
- </FormLabel>
- <FormControl>
- <Textarea
- {...field}
- placeholder="배송 일정 세부사항을 입력하세요"
- rows={3}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 비고 - 선택적 필드 */}
- <FormField
- control={form.control}
- name="notes"
- render={({ field }) => (
- <FormItem>
- <FormLabel>비고</FormLabel>
- <FormControl>
- <Textarea
- {...field}
- placeholder="추가 비고 사항을 입력하세요"
- rows={3}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 파일 첨부 (옵션) */}
- <div className="space-y-2">
- <FormLabel htmlFor="files">첨부 파일 (선택사항)</FormLabel>
- <Input
- id="files"
- type="file"
- multiple
- onChange={(e) => setFiles(e.target.files)}
- />
- {files && files.length > 0 && (
- <p className="text-sm text-muted-foreground">
- {files.length}개 파일이 첨부되었습니다
- </p>
- )}
- </div>
-
- {/* 필수 입력 항목 안내 */}
- <div className="text-sm text-muted-foreground">
- <span className="text-destructive">*</span> 표시는 필수 입력 항목입니다.
- </div>
-
- {/* 모바일에서는 Drawer 내부에서 버튼이 렌더링되므로 여기서는 숨김 */}
- {isDesktop && (
- <DialogFooter className="gap-2 pt-4">
- <DialogClose asChild>
- <Button
- type="button"
- variant="outline"
- >
- 취소
- </Button>
- </DialogClose>
- <Button
- type="submit"
- disabled={isSubmitting || !isValid}
- >
- {isSubmitting && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- {vendors.length > 1 ? `${vendors.length}개 협력업체에 전송` : "전송"}
- </Button>
- </DialogFooter>
- )}
- </form>
- </Form>
- )
- if (hasMultipleRfqIds) {
- toast.error("동일한 RFQ에 대해 선택해주세요");
- return;
- }
- // Desktop Dialog
- if (isDesktop) {
- return (
- <Dialog {...props} onOpenChange={handleDialogOpenChange}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Send className="mr-2 size-4" aria-hidden="true" />
- CBE 평가 전송 ({vendors.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent className="sm:max-w-[600px]">
- <DialogHeader>
- <DialogTitle>CBE 평가 생성 및 전송</DialogTitle>
- <DialogDescription>
- 선택한 {vendors.length}개 협력업체에 대한 상업 입찰 평가를 생성하고 알림을 전송합니다.
- </DialogDescription>
- </DialogHeader>
-
- {formContent}
- </DialogContent>
- </Dialog>
- )
- }
-
- // Mobile Drawer
- return (
- <Drawer {...props} onOpenChange={handleDialogOpenChange}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Send className="mr-2 size-4" aria-hidden="true" />
- CBE 평가 전송 ({vendors.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>CBE 평가 생성 및 전송</DrawerTitle>
- <DrawerDescription>
- 선택한 {vendors.length}개 협력업체에 대한 상업 입찰 평가를 생성하고 알림을 전송합니다.
- </DrawerDescription>
- </DrawerHeader>
-
- <div className="px-4">
- {formContent}
- </div>
-
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- onClick={form.handleSubmit(onSubmit)}
- disabled={isSubmitting || !isValid}
- >
- {isSubmitting && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- {vendors.length > 1 ? `${vendors.length}개 협력업체에 전송` : "전송"}
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file