summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx283
-rw-r--r--components/form-data/form-data-table-columns.tsx101
-rw-r--r--components/form-data/form-data-table.tsx60
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}