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-table.tsx50
-rw-r--r--components/form-data/spreadJS-dialog.tsx654
2 files changed, 424 insertions, 280 deletions
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index 57913192..d964b17b 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -317,10 +317,6 @@ export default function DynamicTable({
// 새로 추가된 Template 가져오기 함수
const handleGetTemplate = async () => {
- if (selectedRowCount !== 1) {
- toast.error("Please select exactly one row to view template");
- return;
- }
if (!projectCode) {
toast.error("Project code is not available");
@@ -329,12 +325,12 @@ export default function DynamicTable({
try {
setIsLoadingTemplate(true);
-
+
const templateResult = await fetchTemplateFromSEDP(projectCode, formCode);
-
+
setTemplateData(templateResult);
setTemplateDialogOpen(true);
-
+
toast.success("Template data loaded successfully");
} catch (error) {
console.error("Error fetching template:", error);
@@ -818,7 +814,7 @@ export default function DynamicTable({
variant="outline"
size="sm"
onClick={handleGetTemplate}
- disabled={isAnyOperationPending || selectedRowCount !== 1}
+ disabled={isAnyOperationPending}
>
{isLoadingTemplate ? (
<Loader className="mr-2 size-4 animate-spin" />
@@ -826,13 +822,9 @@ export default function DynamicTable({
<Eye className="mr-2 size-4" />
)}
View Template
- {selectedRowCount === 1 && (
- <span className="ml-2 text-xs bg-green-100 text-green-700 px-2 py-1 rounded">
- 1
- </span>
- )}
</Button>
+
{/* COMPARE WITH SEDP 버튼 */}
<Button
variant="outline"
@@ -920,19 +912,33 @@ export default function DynamicTable({
isOpen={templateDialogOpen}
onClose={() => setTemplateDialogOpen(false)}
templateData={templateData}
- selectedRow={selectedRowsData[0]}
+ selectedRow={selectedRowsData[0]} // SPR_ITM_LST_SETUP용
+ tableData={tableData} // SPR_LST_SETUP용 - 새로 추가
formCode={formCode}
contractItemId={contractItemId}
editableFieldsMap={editableFieldsMap}
onUpdateSuccess={(updatedValues) => {
- // SpreadSheets에서 업데이트된 값을 테이블에 반영
- const tagNo = updatedValues.TAG_NO;
- if (tagNo) {
- setTableData(prev =>
- prev.map(item =>
- item.TAG_NO === tagNo ? updatedValues : item
- )
- );
+ // 업데이트 로직도 수정해야 함 - 단일 행 또는 복수 행 처리
+ if (Array.isArray(updatedValues)) {
+ // SPR_LST_SETUP의 경우 - 복수 행 업데이트
+ const updatedData = [...tableData];
+ updatedValues.forEach(updatedItem => {
+ const index = updatedData.findIndex(item => item.TAG_NO === updatedItem.TAG_NO);
+ if (index !== -1) {
+ updatedData[index] = updatedItem;
+ }
+ });
+ setTableData(updatedData);
+ } else {
+ // SPR_ITM_LST_SETUP의 경우 - 단일 행 업데이트
+ const tagNo = updatedValues.TAG_NO;
+ if (tagNo) {
+ setTableData(prev =>
+ prev.map(item =>
+ item.TAG_NO === tagNo ? updatedValues : item
+ )
+ );
+ }
}
}}
/>
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 8be9d175..c106f926 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -2,45 +2,31 @@
import * as React from "react";
import dynamic from "next/dynamic";
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
- DialogFooter,
- DialogDescription,
-} from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { GenericData } from "./export-excel-form";
import * as GC from "@mescius/spread-sheets";
import { toast } from "sonner";
import { updateFormDataInDB } from "@/lib/forms/services";
import { Loader, Save } from "lucide-react";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
-
-// Dynamically load the SpreadSheets component (disable SSR)
+import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css';
+
+// SpreadSheets를 동적으로 import (SSR 비활성화)
const SpreadSheets = dynamic(
- () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets),
- {
+ () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets),
+ {
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-full">
<Loader className="mr-2 h-4 w-4 animate-spin" />
Loading SpreadSheets...
</div>
- ),
+ )
}
);
-// Apply license key on the client only
-if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) {
+// 라이센스 키 설정을 클라이언트에서만 실행
+if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) {
GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE;
}
@@ -85,11 +71,12 @@ interface TemplateViewDialogProps {
isOpen: boolean;
onClose: () => void;
templateData: TemplateItem[] | any;
- selectedRow: GenericData;
+ selectedRow?: GenericData; // SPR_ITM_LST_SETUP용
+ tableData?: GenericData[]; // SPR_LST_SETUP용
formCode: string;
contractItemId: number;
- editableFieldsMap?: Map<string, string[]>; // editable field info per tag
- onUpdateSuccess?: (updatedValues: Record<string, any>) => void;
+ editableFieldsMap?: Map<string, string[]>;
+ onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void;
}
export function TemplateViewDialog({
@@ -97,345 +84,496 @@ export function TemplateViewDialog({
onClose,
templateData,
selectedRow,
+ tableData = [],
formCode,
contractItemId,
editableFieldsMap = new Map(),
- onUpdateSuccess,
+ onUpdateSuccess
}: TemplateViewDialogProps) {
- /* ------------------------- local state ------------------------- */
- const [hostStyle] = React.useState({ width: "100%", height: "100%" });
+ const [hostStyle, setHostStyle] = React.useState({
+ width: '100%',
+ height: '100%'
+ });
+
const [isPending, setIsPending] = React.useState(false);
const [hasChanges, setHasChanges] = React.useState(false);
- const [currentSpread, setCurrentSpread] = React.useState<GC.Spread.Sheets.Workbook | null>(
- null
- );
- const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>("");
- const [cellMappings, setCellMappings] = React.useState<
- Array<{ attId: string; cellAddress: string; isEditable: boolean }>
- >([]);
+ const [currentSpread, setCurrentSpread] = React.useState<any>(null);
+ const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]);
const [isClient, setIsClient] = React.useState(false);
+ const [templateType, setTemplateType] = React.useState<'SPR_LST_SETUP' | 'SPR_ITM_LST_SETUP' | null>(null);
- // Render only on client side
+ // 클라이언트 사이드에서만 렌더링되도록 보장
React.useEffect(() => {
setIsClient(true);
}, []);
- /* ------------------------- helpers ------------------------- */
- // Normalize template list and keep only those with CONTENT
- const normalizedTemplates = React.useMemo((): TemplateItem[] => {
- if (!templateData) return [];
-
- const list = Array.isArray(templateData)
- ? (templateData as TemplateItem[])
- : ([templateData] as TemplateItem[]);
-
- return list.filter(
- (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT
- );
+ // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것 찾기
+ const { normalizedTemplate, detectedTemplateType } = React.useMemo(() => {
+ if (!templateData) return { normalizedTemplate: null, detectedTemplateType: null };
+
+ let templates: TemplateItem[];
+ if (Array.isArray(templateData)) {
+ templates = templateData as TemplateItem[];
+ } else {
+ templates = [templateData as TemplateItem];
+ }
+
+ // CONTENT가 있는 템플릿 찾기
+ for (const template of templates) {
+ if (template.SPR_LST_SETUP?.CONTENT) {
+ return { normalizedTemplate: template, detectedTemplateType: 'SPR_LST_SETUP' as const };
+ }
+ if (template.SPR_ITM_LST_SETUP?.CONTENT) {
+ return { normalizedTemplate: template, detectedTemplateType: 'SPR_ITM_LST_SETUP' as const };
+ }
+ }
+
+ return { normalizedTemplate: null, detectedTemplateType: null };
}, [templateData]);
- // Choose currently selected template
- const selectedTemplate = React.useMemo(() => {
- if (!selectedTemplateId) return normalizedTemplates[0];
- return (
- normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) ||
- normalizedTemplates[0]
- );
- }, [normalizedTemplates, selectedTemplateId]);
-
- // Editable fields for the current TAG_NO
- const editableFields = React.useMemo(() => {
- if (!selectedRow?.TAG_NO) return [];
- return editableFieldsMap.get(selectedRow.TAG_NO) || [];
- }, [selectedRow?.TAG_NO, editableFieldsMap]);
-
- const isFieldEditable = React.useCallback(
- (attId: string) => {
- // TAG_NO and TAG_DESC are always editable
- if (attId === "TAG_NO" || attId === "TAG_DESC") return true;
- if (!selectedRow?.TAG_NO) return false;
- return editableFields.includes(attId);
- },
- [selectedRow?.TAG_NO, editableFields]
- );
-
- /** Convert a cell address like "M1" into {row:0,col:12}. */
- const parseCellAddress = (addr: string): { row: number; col: number } | null => {
- if (!addr) return null;
- const match = addr.match(/^([A-Z]+)(\d+)$/);
+ // 템플릿 타입 설정
+ React.useEffect(() => {
+ setTemplateType(detectedTemplateType);
+ }, [detectedTemplateType]);
+
+ // 필드가 편집 가능한지 판별하는 함수
+ const isFieldEditable = React.useCallback((attId: string) => {
+ // TAG_NO와 TAG_DESC는 기본적으로 편집 가능
+ if (attId === "TAG_NO" || attId === "TAG_DESC") {
+ return true;
+ }
+
+ // SPR_ITM_LST_SETUP인 경우 selectedRow.shi 확인
+ if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+ return selectedRow.shi !== true;
+ }
+
+ // SPR_LST_SETUP인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인)
+ return true;
+ }, [templateType, selectedRow]);
+
+ // 편집 가능한 필드 개수 계산
+ const editableFieldsCount = React.useMemo(() => {
+ return cellMappings.filter(m => m.isEditable).length;
+ }, [cellMappings]);
+
+ // 셀 주소를 행과 열로 변환하는 함수
+ const parseCellAddress = (address: string): {row: number, col: number} | null => {
+ if (!address || address.trim() === "") return null;
+
+ const match = address.match(/^([A-Z]+)(\d+)$/);
if (!match) return null;
+
const [, colStr, rowStr] = match;
+
let col = 0;
for (let i = 0; i < colStr.length; i++) {
col = col * 26 + (colStr.charCodeAt(i) - 65 + 1);
}
col -= 1;
- const row = parseInt(rowStr, 10) - 1;
+
+ const row = parseInt(rowStr) - 1;
+
return { row, col };
};
- // Auto‑select first template
- React.useEffect(() => {
- if (normalizedTemplates.length && !selectedTemplateId) {
- setSelectedTemplateId(normalizedTemplates[0].TMPL_ID);
- }
- }, [normalizedTemplates, selectedTemplateId]);
-
- /* ------------------------- init spread ------------------------- */
- const initSpread = React.useCallback(
- (spread: GC.Spread.Sheets.Workbook | undefined) => {
- if (!spread || !selectedTemplate || !selectedRow) return;
+ const initSpread = React.useCallback((spread: any) => {
+ if (!spread || !normalizedTemplate) return;
+ try {
setCurrentSpread(spread);
setHasChanges(false);
- // Pick content JSON and data‑sheet mapping
- const contentJson =
- selectedTemplate.SPR_LST_SETUP?.CONTENT ??
- selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT;
- const dataSheets =
- selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ??
- selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS;
- if (!contentJson) return;
-
- // Prepare shared styles once
- const editableStyle = new GC.Spread.Sheets.Style();
- editableStyle.backColor = "#f0fdf4";
- editableStyle.locked = false;
+ // 템플릿 타입에 따라 CONTENT와 DATA_SHEETS 가져오기
+ let contentJson = null;
+ let dataSheets = null;
+
+ if (templateType === 'SPR_LST_SETUP') {
+ contentJson = normalizedTemplate.SPR_LST_SETUP.CONTENT;
+ dataSheets = normalizedTemplate.SPR_LST_SETUP.DATA_SHEETS;
+ console.log('Using SPR_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME);
+ } else if (templateType === 'SPR_ITM_LST_SETUP') {
+ contentJson = normalizedTemplate.SPR_ITM_LST_SETUP.CONTENT;
+ dataSheets = normalizedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
+ console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', normalizedTemplate.NAME);
+ }
- const readOnlyStyle = new GC.Spread.Sheets.Style();
- readOnlyStyle.backColor = "#f9fafb";
- readOnlyStyle.foreColor = "#6b7280";
- readOnlyStyle.locked = true;
+ if (!contentJson) {
+ console.warn('No CONTENT found in template:', normalizedTemplate.NAME);
+ return;
+ }
- const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson;
+ console.log(`Loading template content for: ${normalizedTemplate.NAME} (Type: ${templateType})`);
+
+ const jsonData = typeof contentJson === 'string'
+ ? JSON.parse(contentJson)
+ : contentJson;
- const sheet = spread.getActiveSheet();
+ // 렌더링 일시 중단
+ spread.suspendPaint();
- /* -------- batch load + style -------- */
- sheet.suspendPaint();
- sheet.suspendCalcService(true);
try {
- spread.fromJSON(jsonObj);
- sheet.options.isProtected = false;
-
- const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = [];
-
- if (dataSheets?.length) {
- dataSheets.forEach((ds) => {
- ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => {
- if (!IN) return;
- const pos = parseCellAddress(IN);
- if (!pos) return;
- const editable = isFieldEditable(ATT_ID);
- mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable });
- });
+ // fromJSON으로 템플릿 구조 로드
+ spread.fromJSON(jsonData);
+
+ // 활성 시트 가져오기
+ const activeSheet = spread.getActiveSheet();
+
+ // 시트 보호 먼저 해제
+ activeSheet.options.isProtected = false;
+
+ // MAP_CELL_ATT 정보를 사용해서 데이터 매핑
+ if (dataSheets && dataSheets.length > 0) {
+ const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = [];
+
+ dataSheets.forEach(dataSheet => {
+ if (dataSheet.MAP_CELL_ATT) {
+ dataSheet.MAP_CELL_ATT.forEach(mapping => {
+ const { ATT_ID, IN } = mapping;
+
+ if (IN && IN.trim() !== "") {
+ const cellPos = parseCellAddress(IN);
+ if (cellPos) {
+ const isEditable = isFieldEditable(ATT_ID);
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: IN,
+ isEditable: isEditable
+ });
+
+ // 템플릿 타입에 따라 다른 데이터 처리
+ if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+ // 단일 행 처리 (기존 로직)
+ const cell = activeSheet.getCell(cellPos.row, cellPos.col);
+ const value = selectedRow[ATT_ID];
+ if (value !== undefined && value !== null) {
+ cell.value(value);
+ }
+
+ // 스타일 적용
+ cell.locked(!isEditable);
+ const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col);
+ const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style();
+
+ if (isEditable) {
+ newStyle.backColor = "#f0fdf4";
+ } else {
+ newStyle.backColor = "#f9fafb";
+ newStyle.foreColor = "#6b7280";
+ }
+
+ activeSheet.setStyle(cellPos.row, cellPos.col, newStyle);
+
+ } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) {
+ // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움
+ tableData.forEach((rowData, index) => {
+ const targetRow = cellPos.row + index;
+ const cell = activeSheet.getCell(targetRow, cellPos.col);
+ const value = rowData[ATT_ID];
+
+ if (value !== undefined && value !== null) {
+ cell.value(value);
+ }
+
+ // 개별 행의 편집 가능 여부 확인 (shi 필드 기준)
+ const rowEditable = isEditable && (rowData.shi !== true);
+ cell.locked(!rowEditable);
+
+ // 스타일 적용
+ const existingStyle = activeSheet.getStyle(targetRow, cellPos.col);
+ const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style();
+
+ if (rowEditable) {
+ newStyle.backColor = "#f0fdf4";
+ } else {
+ newStyle.backColor = "#f9fafb";
+ newStyle.foreColor = "#6b7280";
+ }
+
+ activeSheet.setStyle(targetRow, cellPos.col, newStyle);
+ });
+ }
+
+ console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`);
+ }
+ }
+ });
+ }
+ });
+
+ setCellMappings(mappings);
+
+ // 시트 보호 설정
+ activeSheet.options.isProtected = true;
+ activeSheet.options.protectionOptions = {
+ allowSelectLockedCells: true,
+ allowSelectUnlockedCells: true,
+ allowSort: false,
+ allowFilter: false,
+ allowEditObjects: false,
+ allowResizeRows: false,
+ allowResizeColumns: false
+ };
+
+ // 이벤트 리스너 추가
+ activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => {
+ console.log('Cell changed:', info);
+ setHasChanges(true);
});
- }
- // Apply values + style in chunks for large templates
- const CHUNK = 500;
- let idx = 0;
- const applyChunk = () => {
- const end = Math.min(idx + CHUNK, mappings.length);
- for (; idx < end; idx++) {
- const { attId, cellAddress, isEditable } = mappings[idx];
- const pos = parseCellAddress(cellAddress)!;
- if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) {
- sheet.setValue(pos.row, pos.col, selectedRow[attId]);
- }
- sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle);
- }
- if (idx < mappings.length) {
- requestAnimationFrame(applyChunk);
- } else {
- // enable protection & events after styling done
- sheet.options.isProtected = true;
- sheet.options.protectionOptions = {
- allowSelectLockedCells: true,
- allowSelectUnlockedCells: true,
- } as any;
-
- // Cell/value change events
- sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true));
- sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true));
-
- // Prevent editing read‑only fields
- sheet.bind(
- GC.Spread.Sheets.Events.EditStarting,
- (event: any, info: any) => {
- const map = mappings.find((m) => {
- const pos = parseCellAddress(m.cellAddress);
- return pos && pos.row === info.row && pos.col === info.col;
- });
- if (map && !map.isEditable) {
- toast.warning(`${map.attId} field is read‑only`);
+ activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => {
+ console.log('Value changed:', info);
+ setHasChanges(true);
+ });
+
+ // 편집 시작 시 읽기 전용 셀 확인
+ activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
+ const mapping = mappings.find(m => {
+ const cellPos = parseCellAddress(m.cellAddress);
+ return cellPos && cellPos.row <= info.row && cellPos.col === info.col;
+ });
+
+ if (mapping) {
+ // SPR_LST_SETUP인 경우 해당 행의 데이터에서 shi 확인
+ if (templateType === 'SPR_LST_SETUP') {
+ const dataRowIndex = info.row - parseCellAddress(mapping.cellAddress)!.row;
+ const rowData = tableData[dataRowIndex];
+ if (rowData && rowData.shi === true) {
+ toast.warning(`Row ${dataRowIndex + 1}: ${mapping.attId} field is read-only (SHI mode)`);
info.cancel = true;
+ return;
}
}
- );
-
- setCellMappings(mappings);
- sheet.resumeCalcService(false);
- sheet.resumePaint();
- }
- };
- applyChunk();
- } catch (err) {
- console.error(err);
- toast.error("Failed to load template");
- sheet.resumeCalcService(false);
- sheet.resumePaint();
+
+ if (!mapping.isEditable) {
+ toast.warning(`${mapping.attId} field is read-only`);
+ info.cancel = true;
+ }
+ }
+ });
+ }
+ } finally {
+ spread.resumePaint();
}
- },
- [selectedTemplate, selectedRow, isFieldEditable]
- );
- /* ------------------------- handlers ------------------------- */
- const handleTemplateChange = (id: string) => {
- setSelectedTemplateId(id);
- setHasChanges(false);
- if (currentSpread) {
- // re‑init after a short tick so component remounts SpreadSheets
- setTimeout(() => initSpread(currentSpread), 50);
+ } catch (error) {
+ console.error('Error initializing spread:', error);
+ toast.error('Failed to load template');
+ if (spread && spread.resumePaint) {
+ spread.resumePaint();
+ }
}
- };
+ }, [normalizedTemplate, templateType, selectedRow, tableData, isFieldEditable]);
+ // 변경사항 저장 함수
const handleSaveChanges = React.useCallback(async () => {
- if (!currentSpread || !hasChanges || !selectedRow) {
+ if (!currentSpread || !hasChanges) {
toast.info("No changes to save");
return;
}
- setIsPending(true);
-
try {
- const sheet = currentSpread.getActiveSheet();
- const payload: Record<string, any> = { ...selectedRow };
+ setIsPending(true);
+
+ const activeSheet = currentSpread.getActiveSheet();
- cellMappings.forEach((m) => {
- if (m.isEditable) {
- const pos = parseCellAddress(m.cellAddress);
- if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col);
+ if (templateType === 'SPR_ITM_LST_SETUP' && selectedRow) {
+ // 단일 행 저장 (기존 로직)
+ const dataToSave = { ...selectedRow };
+
+ cellMappings.forEach(mapping => {
+ if (mapping.isEditable) {
+ const cellPos = parseCellAddress(mapping.cellAddress);
+ if (cellPos) {
+ const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
+ dataToSave[mapping.attId] = cellValue;
+ }
+ }
+ });
+
+ dataToSave.TAG_NO = selectedRow.TAG_NO;
+
+ const { success, message } = await updateFormDataInDB(
+ formCode,
+ contractItemId,
+ dataToSave
+ );
+
+ if (!success) {
+ toast.error(message);
+ return;
}
- });
- payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO
+ toast.success("Changes saved successfully!");
+ onUpdateSuccess?.(dataToSave);
+
+ } else if (templateType === 'SPR_LST_SETUP' && tableData.length > 0) {
+ // 복수 행 저장
+ const updatedRows: GenericData[] = [];
+ let saveCount = 0;
+
+ for (let i = 0; i < tableData.length; i++) {
+ const originalRow = tableData[i];
+ const dataToSave = { ...originalRow };
+ let hasRowChanges = false;
+
+ // 각 매핑에 대해 해당 행의 값 확인
+ cellMappings.forEach(mapping => {
+ if (mapping.isEditable && originalRow.shi !== true) { // shi가 true인 행은 편집 불가
+ const cellPos = parseCellAddress(mapping.cellAddress);
+ if (cellPos) {
+ const targetRow = cellPos.row + i;
+ const cellValue = activeSheet.getValue(targetRow, cellPos.col);
+
+ // 값이 변경되었는지 확인
+ if (cellValue !== originalRow[mapping.attId]) {
+ dataToSave[mapping.attId] = cellValue;
+ hasRowChanges = true;
+ }
+ }
+ }
+ });
- const { success, message } = await updateFormDataInDB(
- formCode,
- contractItemId,
- payload
- );
+ // 변경사항이 있는 행만 저장
+ if (hasRowChanges) {
+ dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록
- if (!success) {
- toast.error(message);
- return;
+ const { success } = await updateFormDataInDB(
+ formCode,
+ contractItemId,
+ dataToSave
+ );
+
+ if (success) {
+ updatedRows.push(dataToSave);
+ saveCount++;
+ }
+ } else {
+ updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지
+ }
+ }
+
+ if (saveCount > 0) {
+ toast.success(`${saveCount} rows saved successfully!`);
+ onUpdateSuccess?.(updatedRows);
+ } else {
+ toast.info("No changes to save");
+ }
}
- toast.success("Changes saved successfully!");
- onUpdateSuccess?.({ ...selectedRow, ...payload });
setHasChanges(false);
- } catch (err) {
- console.error(err);
+
+ } catch (error) {
+ console.error("Error saving changes:", error);
toast.error("An unexpected error occurred while saving");
} finally {
setIsPending(false);
}
- }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]);
+ }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings]);
- /* ------------------------- render ------------------------- */
if (!isOpen) return null;
+ // 데이터 유효성 검사
+ const isDataValid = templateType === 'SPR_ITM_LST_SETUP' ? !!selectedRow : tableData.length > 0;
+ const dataCount = templateType === 'SPR_ITM_LST_SETUP' ? 1 : tableData.length;
+
return (
<Dialog open={isOpen} onOpenChange={onClose}>
- <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{ maxWidth: "80vw" }}>
+ <DialogContent
+ className="w-[80%] max-w-none h-[80vh] flex flex-col"
+ style={{maxWidth:"80vw"}}
+ >
<DialogHeader className="flex-shrink-0">
- <DialogTitle>SEDP Template – {formCode}</DialogTitle>
+ <DialogTitle>SEDP Template - {formCode}</DialogTitle>
<DialogDescription>
- {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || "N/A"}`}
- {hasChanges && <span className="ml-2 text-orange-600 font-medium">• Unsaved changes</span>}
+ {templateType && (
+ <span className="font-medium text-blue-600">
+ Template Type: {templateType === 'SPR_LST_SETUP' ? 'List View' : 'Item View'}
+ </span>
+ )}
+ {templateType === 'SPR_ITM_LST_SETUP' && selectedRow && (
+ <span className="ml-2">• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span>
+ )}
+ {templateType === 'SPR_LST_SETUP' && (
+ <span className="ml-2">• {dataCount} rows</span>
+ )}
+ {hasChanges && (
+ <span className="ml-2 text-orange-600 font-medium">
+ • Unsaved changes
+ </span>
+ )}
<br />
<div className="flex items-center gap-4 mt-2">
<span className="text-xs text-muted-foreground">
- <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1" />
+ <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span>
Editable fields
</span>
<span className="text-xs text-muted-foreground">
- <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1" />
- Read‑only fields
+ <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span>
+ Read-only fields
</span>
- {!!cellMappings.length && (
+ {cellMappings.length > 0 && (
<span className="text-xs text-blue-600">
- {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable
+ {editableFieldsCount} of {cellMappings.length} fields editable
</span>
)}
</div>
</DialogDescription>
</DialogHeader>
-
- {/* Template selector */}
- {normalizedTemplates.length > 1 && (
- <div className="flex-shrink-0 px-4 py-2 border-b">
- <div className="flex items-center gap-2">
- <label className="text-sm font-medium">Template:</label>
- <Select value={selectedTemplateId} onValueChange={handleTemplateChange}>
- <SelectTrigger className="w-64">
- <SelectValue placeholder="Select a template" />
- </SelectTrigger>
- <SelectContent>
- {normalizedTemplates.map((t) => (
- <SelectItem key={t.TMPL_ID} value={t.TMPL_ID}>
- <div className="flex flex-col">
- <span>{t.NAME || `Template ${t.TMPL_ID.slice(0, 8)}`}</span>
- <span className="text-xs text-muted-foreground">{t.TMPL_TYPE}</span>
- </div>
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <span className="text-xs text-muted-foreground">({normalizedTemplates.length} templates available)</span>
- </div>
- </div>
- )}
-
- {/* Spreadsheet */}
+
+ {/* SpreadSheets 컴포넌트 영역 */}
<div className="flex-1 overflow-hidden">
- {selectedTemplate && isClient ? (
- <SpreadSheets key={selectedTemplateId} workbookInitialized={initSpread} hostStyle={hostStyle} />
+ {normalizedTemplate && isClient && isDataValid ? (
+ <SpreadSheets
+ key={`${templateType}-${normalizedTemplate.TMPL_ID}`}
+ workbookInitialized={initSpread}
+ hostStyle={hostStyle}
+ />
) : (
<div className="flex items-center justify-center h-full text-muted-foreground">
{!isClient ? (
<>
- <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading...
+ <Loader className="mr-2 h-4 w-4 animate-spin" />
+ Loading...
</>
- ) : (
+ ) : !normalizedTemplate ? (
"No template available"
+ ) : !isDataValid ? (
+ `No ${templateType === 'SPR_ITM_LST_SETUP' ? 'selected row' : 'data'} available`
+ ) : (
+ "Template not ready"
)}
</div>
)}
</div>
- {/* footer */}
<DialogFooter className="flex-shrink-0">
<Button variant="outline" onClick={onClose}>
Close
</Button>
+
{hasChanges && (
- <Button variant="default" onClick={handleSaveChanges} disabled={isPending}>
+ <Button
+ variant="default"
+ onClick={handleSaveChanges}
+ disabled={isPending}
+ >
{isPending ? (
<>
- <Loader className="mr-2 h-4 w-4 animate-spin" /> Saving...
+ <Loader className="mr-2 h-4 w-4 animate-spin" />
+ Saving...
</>
) : (
<>
- <Save className="mr-2 h-4 w-4" /> Save Changes
+ <Save className="mr-2 h-4 w-4" />
+ Save Changes
</>
)}
</Button>
)}
+
</DialogFooter>
</DialogContent>
</Dialog>
);
-}
+} \ No newline at end of file