diff options
Diffstat (limited to 'components/form-data')
| -rw-r--r-- | components/form-data/form-data-report-batch-dialog.tsx | 283 | ||||
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 101 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 60 |
3 files changed, 309 insertions, 135 deletions
diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx index ef921a91..53f8c489 100644 --- a/components/form-data/form-data-report-batch-dialog.tsx +++ b/components/form-data/form-data-report-batch-dialog.tsx @@ -51,6 +51,7 @@ import { import { Button } from "@/components/ui/button"; import { getReportTempList, getOrigin } from "@/lib/forms/services"; import { DataTableColumnJSON } from "./form-data-table-columns"; +import { PublishDialog } from "./publish-dialog"; const MAX_FILE_SIZE = 3000000; @@ -87,6 +88,10 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ const [selectTemp, setSelectTemp] = useState<string>(""); const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [isUploading, setIsUploading] = useState(false); + + // Add new state for publish dialog + const [publishDialogOpen, setPublishDialogOpen] = useState<boolean>(false); + const [generatedFileBlob, setGeneratedFileBlob] = useState<Blob | null>(null); useEffect(() => { updateReportTempList(packageId, formId, setTempList); @@ -125,48 +130,43 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ setSelectedFiles(updatedFiles); }; + // Create and download document const submitData = async () => { setIsUploading(true); try { const origin = await getOrigin(); - const targetFiles = selectedFiles[0]; const reportDatas = reportData.map((c) => { const reportValue = stringifyAllValues(c); - const reportValueMapping: { [key: string]: any } = {}; columnsJSON.forEach((c2) => { const { key } = c2; - - // const objKey = label.split(" ").join("_"); - reportValueMapping[key] = reportValue?.[key] ?? ""; }); return reportValueMapping; }); + const formData = new FormData(); formData.append("file", targetFiles); formData.append("customFileName", `${formCode}.pdf`); formData.append("reportDatas", JSON.stringify(reportDatas)); formData.append("reportTempPath", selectTemp); - const reqeustCreateReport = await fetch( + const requestCreateReport = await fetch( `${origin}/api/pdftron/createVendorDataReports`, { method: "POST", body: formData } ); - if (reqeustCreateReport.ok) { - const blob = await reqeustCreateReport.blob(); - + if (requestCreateReport.ok) { + const blob = await requestCreateReport.blob(); saveAs(blob, `${formCode}.pdf`); - toastMessage.success("Report 다운로드 완료!"); } else { - const err = await reqeustCreateReport.json(); + const err = await requestCreateReport.json(); console.error("에러:", err); throw new Error(err.message); } @@ -184,100 +184,179 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ } }; + // New function to prepare the file for publishing + const prepareFileForPublishing = async () => { + setIsUploading(true); + + try { + const origin = await getOrigin(); + const targetFiles = selectedFiles[0]; + + const reportDatas = reportData.map((c) => { + const reportValue = stringifyAllValues(c); + const reportValueMapping: { [key: string]: any } = {}; + + columnsJSON.forEach((c2) => { + const { key } = c2; + reportValueMapping[key] = reportValue?.[key] ?? ""; + }); + + return reportValueMapping; + }); + + const formData = new FormData(); + formData.append("file", targetFiles); + formData.append("customFileName", `${formCode}.pdf`); + formData.append("reportDatas", JSON.stringify(reportDatas)); + formData.append("reportTempPath", selectTemp); + + const requestCreateReport = await fetch( + `${origin}/api/pdftron/createVendorDataReports`, + { method: "POST", body: formData } + ); + + if (requestCreateReport.ok) { + const blob = await requestCreateReport.blob(); + setGeneratedFileBlob(blob); + setPublishDialogOpen(true); + toastMessage.success("문서가 생성되었습니다. 발행 정보를 입력해주세요."); + } else { + const err = await requestCreateReport.json(); + console.error("에러:", err); + throw new Error(err.message); + } + } catch (err) { + console.error(err); + toast({ + title: "Error", + description: "문서 생성 중 오류가 발생했습니다.", + variant: "destructive", + }); + } finally { + setIsUploading(false); + } + }; + return ( - <Dialog open={open} onOpenChange={onClose}> - <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}> - <DialogHeader> - <DialogTitle>Vendor Document Create</DialogTitle> - <DialogDescription> - Vendor Document Template을 선택하신 후 갑지를 업로드하여 주시기 - 바랍니다. - </DialogDescription> - </DialogHeader> - <div className="h-[60px]"> - <Label>Vendor Document Template Select</Label> - <Select value={selectTemp} onValueChange={setSelectTemp}> - <SelectTrigger className="w-[100%]"> - <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." /> - </SelectTrigger> - <SelectContent> - {tempList.map((c) => { - const { fileName, filePath } = c; - - return ( - <SelectItem key={filePath} value={filePath}> - {fileName} - </SelectItem> - ); - })} - </SelectContent> - </Select> - </div> - <div> - <Label>Vendor Document Cover Page Upload(.docx)</Label> - <Dropzone - maxSize={MAX_FILE_SIZE} - multiple={false} - accept={{ accept: [".docx"] }} - onDropAccepted={handleDropAccepted} - onDropRejected={handleDropRejected} - disabled={isUploading} - > - {({ maxSize }) => ( - <> - <DropzoneZone className="flex justify-center"> - <DropzoneInput /> - <div className="flex items-center gap-6"> - <DropzoneUploadIcon /> - <div className="grid gap-0.5"> - <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle> - <DropzoneDescription> - 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} - {maxSize ? prettyBytes(maxSize) : "무제한"} - </DropzoneDescription> + <> + <Dialog open={open} onOpenChange={onClose}> + <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}> + <DialogHeader> + <DialogTitle>Vendor Document Create</DialogTitle> + <DialogDescription> + Vendor Document Template을 선택하신 후 갑지를 업로드하여 주시기 + 바랍니다. + </DialogDescription> + </DialogHeader> + <div className="h-[60px]"> + <Label>Vendor Document Template Select</Label> + <Select value={selectTemp} onValueChange={setSelectTemp}> + <SelectTrigger className="w-[100%]"> + <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." /> + </SelectTrigger> + <SelectContent> + {tempList.map((c) => { + const { fileName, filePath } = c; + + return ( + <SelectItem key={filePath} value={filePath}> + {fileName} + </SelectItem> + ); + })} + </SelectContent> + </Select> + </div> + <div> + <Label>Vendor Document Cover Page Upload(.docx)</Label> + <Dropzone + maxSize={MAX_FILE_SIZE} + multiple={false} + accept={{ accept: [".docx"] }} + onDropAccepted={handleDropAccepted} + onDropRejected={handleDropRejected} + disabled={isUploading} + > + {({ maxSize }) => ( + <> + <DropzoneZone className="flex justify-center"> + <DropzoneInput /> + <div className="flex items-center gap-6"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle> + <DropzoneDescription> + 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} + {maxSize ? prettyBytes(maxSize) : "무제한"} + </DropzoneDescription> + </div> </div> - </div> - </DropzoneZone> - <Label className="text-xs text-muted-foreground"> - 여러 파일을 선택할 수 있습니다. - </Label> - </> - )} - </Dropzone> - </div> - - {selectedFiles.length > 0 && ( - <div className="grid gap-2"> - <div className="flex items-center justify-between"> - <h6 className="text-sm font-semibold"> - 선택된 파일 ({selectedFiles.length}) - </h6> - <Badge variant="secondary">{selectedFiles.length}개 파일</Badge> - </div> - <ScrollArea> - <UploadFileItem - selectedFiles={selectedFiles} - removeFile={removeFile} - isUploading={isUploading} - /> - </ScrollArea> + </DropzoneZone> + <Label className="text-xs text-muted-foreground"> + 여러 파일을 선택할 수 있습니다. + </Label> + </> + )} + </Dropzone> </div> - )} - - <DialogFooter> - <Button - disabled={ - selectedFiles.length === 0 || - selectTemp.length === 0 || - isUploading - } - onClick={submitData} - > - {isUploading && <Loader2 />}Vendor Document Create - </Button> - </DialogFooter> - </DialogContent> - </Dialog> + + {selectedFiles.length > 0 && ( + <div className="grid gap-2"> + <div className="flex items-center justify-between"> + <h6 className="text-sm font-semibold"> + 선택된 파일 ({selectedFiles.length}) + </h6> + <Badge variant="secondary">{selectedFiles.length}개 파일</Badge> + </div> + <ScrollArea> + <UploadFileItem + selectedFiles={selectedFiles} + removeFile={removeFile} + isUploading={isUploading} + /> + </ScrollArea> + </div> + )} + + <DialogFooter> + {/* Add the new Publish button */} + <Button + onClick={prepareFileForPublishing} + disabled={ + selectedFiles.length === 0 || + selectTemp.length === 0 || + isUploading + } + variant="outline" + className="mr-2" + > + {isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + Publish + </Button> + <Button + disabled={ + selectedFiles.length === 0 || + selectTemp.length === 0 || + isUploading + } + onClick={submitData} + > + {isUploading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + Create Vendor Document + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* Add the PublishDialog component */} + <PublishDialog + open={publishDialogOpen} + onOpenChange={setPublishDialogOpen} + packageId={packageId} + formCode={formCode} + fileBlob={generatedFileBlob || undefined} + /> + </> ); }; diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx index a1fbcae1..de479efb 100644 --- a/components/form-data/form-data-table-columns.tsx +++ b/components/form-data/form-data-table-columns.tsx @@ -1,6 +1,7 @@ import type { ColumnDef, Row } from "@tanstack/react-table"; import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Ellipsis } from "lucide-react"; import { formatDate } from "@/lib/utils"; import { @@ -17,6 +18,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from 'sonner'; + /** row 액션 관련 타입 */ export interface DataTableRowAction<TData> { row: Row<TData>; @@ -39,8 +41,8 @@ export interface DataTableColumnJSON { uom?: string; uomId?: string; shi?: boolean; - } + /** * getColumns 함수에 필요한 props * - TData: 테이블에 표시할 행(Row)의 타입 @@ -52,20 +54,77 @@ interface GetColumnsProps<TData> { >; setReportData: React.Dispatch<React.SetStateAction<{ [key: string]: any }[]>>; tempCount: number; + // 체크박스 선택 관련 props + selectedRows?: Record<string, boolean>; + onRowSelectionChange?: (updater: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void; } /** * getColumns 함수 * 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정 - * 2) 마지막에 "Action" 칼럼(예: update 버튼) 추가 + * 2) 체크박스 컬럼 추가 (showBatchSelection이 true일 때) + * 3) 마지막에 "Action" 칼럼(예: update 버튼) 추가 */ export function getColumns<TData extends object>({ columnsJSON, setRowAction, setReportData, tempCount, + selectedRows = {}, + onRowSelectionChange, }: GetColumnsProps<TData>): ColumnDef<TData>[] { - // (1) 기본 컬럼들 + const columns: ColumnDef<TData>[] = []; + + // (1) 체크박스 컬럼 (항상 표시) + const selectColumn: ColumnDef<TData> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => { + table.toggleAllPageRowsSelected(!!value); + + // 모든 행 선택/해제 + if (onRowSelectionChange) { + const allRowsSelection: Record<string, boolean> = {}; + table.getRowModel().rows.forEach((row) => { + allRowsSelection[row.id] = !!value; + }); + onRowSelectionChange(allRowsSelection); + } + }} + aria-label="Select all" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => { + row.toggleSelected(!!value); + + // 개별 행 선택 상태 업데이트 + if (onRowSelectionChange) { + onRowSelectionChange(prev => ({ + ...prev, + [row.id]: !!value + })); + } + }} + aria-label="Select row" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }; + columns.push(selectColumn); + + // (2) 기본 컬럼들 const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({ accessorKey: col.key, header: ({ column }) => ( @@ -82,7 +141,7 @@ export function getColumns<TData extends object>({ maxWidth: col.key === "TAG_NO" ? 120 : 150, isReadOnly: col.shi === true, // shi 정보를 메타데이터에 저장 }, - // (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능 + // (3) 실제 셀(cell) 렌더링: type에 따라 분기 가능 cell: ({ row }) => { const cellValue = row.getValue(col.key); @@ -109,14 +168,6 @@ export function getColumns<TData extends object>({ </div> ); - // case "date": - // // 예: 날짜 포맷팅 - // // 실제론 dayjs / date-fns 등으로 포맷 - // if (!cellValue) return <div></div> - // const dateString = cellValue as string - // if (!dateString) return null - // return formatDate(new Date(dateString)) - case "LIST": // 예: select인 경우 label만 표시 return ( @@ -144,7 +195,9 @@ export function getColumns<TData extends object>({ }, })); - // (3) 액션 칼럼 - update 버튼 예시 + columns.push(...baseColumns); + + // (4) 액션 칼럼 - update 버튼 예시 const actionColumn: ColumnDef<TData> = { id: "update", header: "", @@ -162,12 +215,6 @@ export function getColumns<TData extends object>({ <DropdownMenuContent align="end" className="w-40"> <DropdownMenuItem onSelect={() => { - // 행에 있는 모든 필드가 읽기 전용인지 확인할 수도 있습니다 (선택 사항) - // const allColumnsReadOnly = columnsJSON.every(col => col.shi === true); - // if(allColumnsReadOnly) { - // toast.info("이 항목은 읽기 전용입니다."); - // return; - // } setRowAction({ row, type: "update" }); }} > @@ -176,11 +223,11 @@ export function getColumns<TData extends object>({ <DropdownMenuItem onSelect={() => { if(tempCount > 0){ - const { original } = row; - setReportData([original]); - } else { - toast.error("업로드된 Template File이 없습니다."); - } + const { original } = row; + setReportData([original]); + } else { + toast.error("업로드된 Template File이 없습니다."); + } }} > Create Document @@ -192,6 +239,8 @@ export function getColumns<TData extends object>({ enablePinning: true, }; - // (4) 최종 반환 - return [...baseColumns, actionColumn]; + columns.push(actionColumn); + + // (5) 최종 반환 + return columns; }
\ No newline at end of file diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 05278375..0a76e145 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -131,9 +131,14 @@ export default function DynamicTable({ React.useState<DataTableRowAction<GenericData> | null>(null); const [tableData, setTableData] = React.useState<GenericData[]>(dataJSON); + // 배치 선택 관련 상태 + const [selectedRows, setSelectedRows] = React.useState<Record<string, boolean>>({}); + // Update tableData when dataJSON changes React.useEffect(() => { setTableData(dataJSON); + // 데이터가 변경되면 선택 상태 초기화 + setSelectedRows({}); }, [dataJSON]); // 폴링 상태 관리를 위한 ref @@ -207,9 +212,27 @@ export default function DynamicTable({ } }, [projectId]); + // 선택된 행들의 실제 데이터 가져오기 + const getSelectedRowsData = React.useCallback(() => { + const selectedIndices = Object.keys(selectedRows).filter(key => selectedRows[key]); + return selectedIndices.map(index => tableData[parseInt(index)]).filter(Boolean); + }, [selectedRows, tableData]); + + // 선택된 행 개수 계산 + const selectedRowCount = React.useMemo(() => { + return Object.values(selectedRows).filter(Boolean).length; + }, [selectedRows]); + const columns = React.useMemo( - () => getColumns<GenericData>({ columnsJSON, setRowAction, setReportData, tempCount }), - [columnsJSON, setRowAction, setReportData, tempCount] + () => getColumns<GenericData>({ + columnsJSON, + setRowAction, + setReportData, + tempCount, + selectedRows, + onRowSelectionChange: setSelectedRows + }), + [columnsJSON, setRowAction, setReportData, tempCount, selectedRows] ); function mapColumnTypeToAdvancedFilterType( @@ -518,13 +541,22 @@ export default function DynamicTable({ } } - // Handle batch document check + // Handle batch document with smart selection logic const handleBatchDocument = () => { - if (tempCount > 0) { - setBatchDownDialog(true); - } else { + if (tempCount === 0) { toast.error("업로드된 Template File이 없습니다."); + return; } + + // 선택된 항목이 있으면 선택된 것만, 없으면 전체 사용 + const selectedData = getSelectedRowsData(); + if (selectedData.length > 0) { + toast.info(`선택된 ${selectedData.length}개 항목으로 배치 문서를 생성합니다.`); + } else { + toast.info(`전체 ${tableData.length}개 항목으로 배치 문서를 생성합니다.`); + } + + setBatchDownDialog(true); }; return ( @@ -534,6 +566,15 @@ export default function DynamicTable({ columns={columns} advancedFilterFields={advancedFilterFields} > + {/* 선택된 항목 수 표시 (선택된 항목이 있을 때만) */} + {selectedRowCount > 0 && ( + <div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md"> + <p className="text-sm text-blue-700"> + {selectedRowCount}개 항목이 선택되었습니다. 배치 문서는 선택된 항목만으로 생성됩니다. + </p> + </div> + )} + {/* 버튼 그룹 */} <div className="flex items-center gap-2"> {/* 태그 관리 드롭다운 */} @@ -583,6 +624,11 @@ export default function DynamicTable({ <DropdownMenuItem onClick={handleBatchDocument} disabled={isAnyOperationPending}> <FileOutput className="mr-2 h-4 w-4" /> Batch Document + {selectedRowCount > 0 && ( + <span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded"> + {selectedRowCount} + </span> + )} </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> @@ -748,7 +794,7 @@ export default function DynamicTable({ open={batchDownDialog} setOpen={setBatchDownDialog} columnsJSON={columnsJSON} - reportData={tableData} + reportData={selectedRowCount > 0 ? getSelectedRowsData() : tableData} packageId={contractItemId} formCode={formCode} formId={formId} |
