summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/form-data/spreadJS-dialog.tsx760
-rw-r--r--lib/pdftron/serverSDK/createBasicContractPdf.ts133
-rw-r--r--lib/vendors/table/request-pq-dialog.tsx267
-rw-r--r--pages/api/pdftron/createBasicContractPdf.ts16
4 files changed, 695 insertions, 481 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx
index 7ed861c2..1d0796fe 100644
--- a/components/form-data/spreadJS-dialog.tsx
+++ b/components/form-data/spreadJS-dialog.tsx
@@ -16,7 +16,7 @@ import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns";
// SpreadSheets를 동적으로 import (SSR 비활성화)
const SpreadSheets = dynamic(
() => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets),
- {
+ {
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-full">
@@ -77,6 +77,13 @@ interface ValidationError {
message: string;
}
+interface CellMapping {
+ attId: string;
+ cellAddress: string;
+ isEditable: boolean;
+ dataRowIndex?: number;
+}
+
interface TemplateViewDialogProps {
isOpen: boolean;
onClose: () => void;
@@ -110,7 +117,7 @@ export function TemplateViewDialog({
const [isPending, setIsPending] = React.useState(false);
const [hasChanges, setHasChanges] = React.useState(false);
const [currentSpread, setCurrentSpread] = React.useState<any>(null);
- const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]);
+ const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]);
const [isClient, setIsClient] = React.useState(false);
const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null);
const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]);
@@ -125,25 +132,25 @@ export function TemplateViewDialog({
// 사용 가능한 템플릿들을 필터링하고 설정
React.useEffect(() => {
if (!templateData) return;
-
+
let templates: TemplateItem[];
if (Array.isArray(templateData)) {
templates = templateData as TemplateItem[];
} else {
templates = [templateData as TemplateItem];
}
-
+
// CONTENT가 있는 템플릿들 필터링
const validTemplates = templates.filter(template => {
const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT;
const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT;
const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM";
-
+
return isValidType && (hasSpreadListContent || hasSpreadItemContent);
});
-
+
setAvailableTemplates(validTemplates);
-
+
// 첫 번째 유효한 템플릿을 기본으로 선택
if (validTemplates.length > 0 && !selectedTemplateId) {
setSelectedTemplateId(validTemplates[0].TMPL_ID);
@@ -159,7 +166,7 @@ export function TemplateViewDialog({
setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM');
setHasChanges(false);
setValidationErrors([]);
-
+
// SpreadSheets 재초기화
if (currentSpread) {
const template = availableTemplates.find(t => t.TMPL_ID === templateId);
@@ -175,12 +182,26 @@ export function TemplateViewDialog({
return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId);
}, [availableTemplates, selectedTemplateId]);
+ // 편집 가능한 필드 목록 계산
const editableFields = React.useMemo(() => {
- if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) {
- return [];
+ // SPREAD_ITEM인 경우: selectedRow의 TAG_NO로 확인
+ if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) {
+ if (!editableFieldsMap.has(selectedRow.TAG_NO)) {
+ return [];
+ }
+ return editableFieldsMap.get(selectedRow.TAG_NO) || [];
+ }
+
+ // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리
+ if (templateType === 'SPREAD_LIST' && tableData.length > 0) {
+ const firstRowTagNo = tableData[0]?.TAG_NO;
+ if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) {
+ return editableFieldsMap.get(firstRowTagNo) || [];
+ }
}
- return editableFieldsMap.get(selectedRow.TAG_NO) || [];
- }, [selectedRow?.TAG_NO, editableFieldsMap]);
+
+ return [];
+ }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]);
// 필드가 편집 가능한지 판별하는 함수
const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => {
@@ -189,15 +210,36 @@ export function TemplateViewDialog({
if (columnConfig?.shi === true) {
return false; // columnsJSON에서 shi가 true이면 편집 불가
}
-
+
// TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우)
if (attId === "TAG_NO" || attId === "TAG_DESC") {
return true;
}
+
+ // SPREAD_ITEM인 경우: editableFields 체크
+ if (templateType === 'SPREAD_ITEM') {
+ return editableFields.includes(attId);
+ }
- // SPREAD_LIST인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인)
+ // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려
+ if (templateType === 'SPREAD_LIST') {
+ // 기본적으로 editableFields에 포함되어야 함
+ if (!editableFields.includes(attId)) {
+ return false;
+ }
+
+ // rowData가 제공된 경우 해당 행의 shi 상태도 확인
+ if (rowData && rowData.shi === true) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // 기본적으로는 editableFields 체크
+ // return editableFields.includes(attId);
return true;
- }, [templateType, selectedRow, columnsJSON, editableFieldsMap]);
+ }, [templateType, columnsJSON, editableFields]);
// 편집 가능한 필드 개수 계산
const editableFieldsCount = React.useMemo(() => {
@@ -205,22 +247,22 @@ export function TemplateViewDialog({
}, [cellMappings]);
// 셀 주소를 행과 열로 변환하는 함수
- const parseCellAddress = (address: string): {row: number, col: number} | null => {
+ 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) - 1;
-
+
return { row, col };
};
@@ -248,7 +290,7 @@ export function TemplateViewDialog({
// 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음
break;
}
-
+
return null;
};
@@ -270,7 +312,7 @@ export function TemplateViewDialog({
// 단일 행 검증
const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
-
+
if (errorMessage) {
errors.push({
cellAddress: mapping.cellAddress,
@@ -281,312 +323,502 @@ export function TemplateViewDialog({
});
}
} else if (templateType === 'SPREAD_LIST') {
- // 복수 행 검증
- for (let i = 0; i < tableData.length; i++) {
- const targetRow = cellPos.row + i;
- const cellValue = activeSheet.getValue(targetRow, cellPos.col);
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
-
- if (errorMessage) {
- errors.push({
- cellAddress: `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`,
- attId: mapping.attId,
- value: cellValue,
- expectedType: columnConfig.type,
- message: errorMessage
- });
- }
+ // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴
+ const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
+ const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+
+ if (errorMessage) {
+ errors.push({
+ cellAddress: mapping.cellAddress,
+ attId: mapping.attId,
+ value: cellValue,
+ expectedType: columnConfig.type,
+ message: errorMessage
+ });
}
}
});
setValidationErrors(errors);
return errors;
- }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType, tableData]);
-
- // LIST 타입 컬럼에 드롭다운 설정
- const setupListValidation = React.useCallback((activeSheet: any, cellPos: {row: number, col: number}, options: string[], rowCount: number = 1) => {
- // ComboBox 셀 타입 생성
- const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox();
- comboBoxCellType.items(options);
- comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
-
- // 단일 셀 또는 범위에 적용
- for (let i = 0; i < rowCount; i++) {
- const targetRow = cellPos.row + i;
- activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType);
+ }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType]);
+
+ // ═══════════════════════════════════════════════════════════════════════════════
+ // 🛠️ 헬퍼 함수들
+ // ═══════════════════════════════════════════════════════════════════════════════
+
+ // 🎨 셀 스타일 생성
+ const createCellStyle = React.useCallback((isEditable: boolean) => {
+ const style = new GC.Spread.Sheets.Style();
+ if (isEditable) {
+ style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능)
+ } else {
+ style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용)
+ style.foreColor = "#6b7280";
+ }
+ return style;
+ }, []);
+
+ // 📋 최적화된 LIST 드롭다운 설정
+const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
+ try {
+ console.log(`🎯 Setting up DataValidation dropdown for ${rowCount} rows with options:`, options);
+
+ // 🚨 성능 임계점 확인
+ if (rowCount > 100) {
+ console.warn(`⚡ Large dataset (${rowCount} rows): Using simple validation only`);
+ setupSimpleValidation(activeSheet, cellPos, options, rowCount);
+ return;
+ }
+
+ // ✅ 1단계: options 철저하게 정규화 (이것이 에러 방지의 핵심!)
+ let safeOptions;
+ try {
+ safeOptions = options
+ .filter(opt => opt !== null && opt !== undefined && opt !== '') // null, undefined, 빈값 제거
+ .map(opt => {
+ // 모든 값을 안전한 문자열로 변환
+ let str = String(opt);
+ // 특수 문자나 문제 있는 문자 처리
+ str = str.replace(/[\r\n\t]/g, ' '); // 줄바꿈, 탭을 공백으로
+ str = str.replace(/[^\x20-\x7E\u00A1-\uFFFF]/g, ''); // 제어 문자 제거
+ return str.trim();
+ })
+ .filter(opt => opt.length > 0 && opt.length < 255) // 빈값과 너무 긴 값 제거
+ .filter((opt, index, arr) => arr.indexOf(opt) === index) // 중복 제거
+ .slice(0, 100); // 최대 100개로 제한
+
+ console.log(`📋 Original options:`, options);
+ console.log(`📋 Safe options:`, safeOptions);
+ } catch (filterError) {
+ console.error('❌ Options filtering failed:', filterError);
+ safeOptions = ['Option1', 'Option2']; // 안전한 폴백 옵션
+ }
+
+ if (safeOptions.length === 0) {
+ console.warn(`⚠️ No valid options found, using fallback`);
+ safeOptions = ['Please Select'];
+ }
+
+ // ✅ 2단계: DataValidation 생성 (엑셀 스타일 드롭다운)
+ let validator;
+ try {
+ validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
+ console.log(`✅ DataValidation validator created successfully`);
+ } catch (validatorError) {
+ console.error('❌ Failed to create validator:', validatorError);
+ return;
+ }
+
+ // ✅ 3단계: 셀/범위에 적용
+ try {
+ if (rowCount > 1) {
+ // 범위에 적용
+ const range = activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1);
+ range.dataValidator(validator);
+ console.log(`✅ DataValidation applied to range [${cellPos.row}, ${cellPos.col}, ${rowCount}, 1]`);
+ } else {
+ // 단일 셀에 적용
+ activeSheet.setDataValidator(cellPos.row, cellPos.col, validator);
+ console.log(`✅ DataValidation applied to single cell [${cellPos.row}, ${cellPos.col}]`);
+ }
+ } catch (applicationError) {
+ console.error('❌ Failed to apply DataValidation:', applicationError);
- // 추가로 데이터 검증도 설정
- const validator = GC.Spread.Sheets.DataValidation.createListValidator(options);
- activeSheet.setDataValidator(targetRow, cellPos.col, validator);
+ // 폴백: 개별 셀에 하나씩 적용
+ console.log('🔄 Trying individual cell application...');
+ for (let i = 0; i < Math.min(rowCount, 50); i++) {
+ try {
+ const individualValidator = GC.Spread.Sheets.DataValidation.createListValidator([...safeOptions]);
+ activeSheet.setDataValidator(cellPos.row + i, cellPos.col, individualValidator);
+ console.log(`✅ Individual DataValidation set for row ${cellPos.row + i}`);
+ } catch (individualError) {
+ console.warn(`⚠️ Failed individual cell ${cellPos.row + i}:`, individualError);
+ }
+ }
+ }
+
+ console.log(`✅ DataValidation dropdown setup completed`);
+
+ } catch (error) {
+ console.error('❌ DataValidation setup failed completely:', error);
+ console.error('Error stack:', error.stack);
+ console.log('🔄 Falling back to no validation');
+ }
+}, []);
+
+// ⚡ 단순 검증 설정 (드롭다운 없이 검증만)
+const setupSimpleValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => {
+ try {
+ console.log(`⚡ Setting up simple validation (no dropdown UI) for ${rowCount} rows`);
+
+ const safeOptions = options
+ .filter(opt => opt != null && opt !== '')
+ .map(opt => String(opt).trim())
+ .filter(opt => opt.length > 0);
+
+ if (safeOptions.length === 0) return;
+
+ const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions);
+
+ // 범위로 적용 시도
+ try {
+ activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1).dataValidator(validator);
+ console.log(`✅ Simple validation applied to range`);
+ } catch (rangeError) {
+ console.warn('Range validation failed, trying individual cells');
+ // 폴백: 개별 적용
+ for (let i = 0; i < Math.min(rowCount, 100); i++) {
+ try {
+ activeSheet.setDataValidator(cellPos.row + i, cellPos.col, validator);
+ } catch (individualError) {
+ // 개별 실패해도 계속
+ }
+ }
+ }
+
+ } catch (error) {
+ console.error('❌ Simple validation failed:', error);
+ }
+}, []);
+
+// ═══════════════════════════════════════════════════════════════════════════════
+// 🔍 디버깅용: 에러 발생 원인 추적
+// ═══════════════════════════════════════════════════════════════════════════════
+
+const debugDropdownError = (options: any[], attId: string) => {
+ console.group(`🔍 Debugging dropdown for ${attId}`);
+
+ console.log('Original options type:', typeof options);
+ console.log('Is array:', Array.isArray(options));
+ console.log('Length:', options?.length);
+ console.log('Raw options:', options);
+
+ if (Array.isArray(options)) {
+ options.forEach((opt, index) => {
+ console.log(`[${index}] Type: ${typeof opt}, Value: "${opt}", String: "${String(opt)}"`);
+
+ // 문제 있는 값 체크
+ if (opt === null) console.warn(` ⚠️ NULL value at index ${index}`);
+ if (opt === undefined) console.warn(` ⚠️ UNDEFINED value at index ${index}`);
+ if (typeof opt === 'object') console.warn(` ⚠️ OBJECT value at index ${index}:`, opt);
+ if (typeof opt === 'string' && opt.includes('\n')) console.warn(` ⚠️ NEWLINE in string at index ${index}`);
+ if (typeof opt === 'string' && opt.length === 0) console.warn(` ⚠️ EMPTY STRING at index ${index}`);
+ });
+ }
+
+ console.groupEnd();
+};
+
+ // 🚀 행 용량 확보
+ const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => {
+ const currentRowCount = activeSheet.getRowCount();
+ if (requiredRowCount > currentRowCount) {
+ activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
+ console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`);
}
}, []);
+ // 🛡️ 시트 보호 및 이벤트 설정
+ const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => {
+ console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`);
+
+ // 시트 보호 설정
+ activeSheet.options.isProtected = true;
+ activeSheet.options.protectionOptions = {
+ allowSelectLockedCells: true,
+ allowSelectUnlockedCells: true,
+ allowSort: false,
+ allowFilter: false,
+ allowEditObjects: false,
+ allowResizeRows: false,
+ allowResizeColumns: false
+ };
+
+ // 🎯 변경 감지 이벤트
+ const changeEvents = [
+ GC.Spread.Sheets.Events.CellChanged,
+ GC.Spread.Sheets.Events.ValueChanged,
+ GC.Spread.Sheets.Events.ClipboardPasted
+ ];
+
+ changeEvents.forEach(eventType => {
+ activeSheet.bind(eventType, () => {
+ console.log(`📝 ${eventType} detected`);
+ setHasChanges(true);
+ });
+ });
+
+ // 🚫 편집 시작 권한 확인 (수정됨)
+ activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => {
+ console.log(`🎯 EditStarting: Row ${info.row}, Col ${info.col}`);
+
+ // ✅ 정확한 매핑 찾기 (행/열 정확히 일치)
+ const exactMapping = mappings.find(m => {
+ const cellPos = parseCellAddress(m.cellAddress);
+ return cellPos && cellPos.row === info.row && cellPos.col === info.col;
+ });
+
+ if (!exactMapping) {
+ console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`);
+ return; // 매핑이 없으면 허용 (템플릿 영역 밖)
+ }
+
+ console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}`);
+
+ // 기본 편집 권한 확인
+ if (!exactMapping.isEditable) {
+ console.log(`🚫 Field ${exactMapping.attId} is not editable`);
+ toast.warning(`${exactMapping.attId} field is read-only`);
+ info.cancel = true;
+ return;
+ }
+
+ // SPREAD_LIST 개별 행 SHI 확인
+ if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) {
+ const dataRowIndex = exactMapping.dataRowIndex;
+
+ console.log(`🔍 Checking SHI for data row ${dataRowIndex}`);
+
+ if (dataRowIndex >= 0 && dataRowIndex < tableData.length) {
+ const rowData = tableData[dataRowIndex];
+ if (rowData?.shi === true) {
+ console.log(`🚫 Row ${dataRowIndex} is in SHI mode`);
+ toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`);
+ info.cancel = true;
+ return;
+ }
+ } else {
+ console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`);
+ }
+ }
+
+ console.log(`✅ Edit allowed for ${exactMapping.attId}`);
+ });
+
+ // ✅ 편집 완료 검증 (수정됨)
+ activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => {
+ console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}`);
+
+ // ✅ 정확한 매핑 찾기
+ const exactMapping = mappings.find(m => {
+ const cellPos = parseCellAddress(m.cellAddress);
+ return cellPos && cellPos.row === info.row && cellPos.col === info.col;
+ });
+
+ if (!exactMapping) {
+ console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`);
+ return;
+ }
+
+ const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId);
+ if (columnConfig) {
+ const cellValue = activeSheet.getValue(info.row, info.col);
+ console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`);
+
+ const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
+ const cell = activeSheet.getCell(info.row, info.col);
+
+ if (errorMessage) {
+ console.log(`❌ Validation failed: ${errorMessage}`);
+
+ // 🚨 에러 스타일 적용 (편집 가능 상태 유지)
+ const errorStyle = new GC.Spread.Sheets.Style();
+ errorStyle.backColor = "#fef2f2";
+ errorStyle.foreColor = "#dc2626";
+ errorStyle.borderLeft = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
+ errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
+ errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
+ errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick);
+
+ activeSheet.setStyle(info.row, info.col, errorStyle);
+ cell.locked(!exactMapping.isEditable);
+
+ toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}. Please correct the value.`, { duration: 5000 });
+ } else {
+ console.log(`✅ Validation passed`);
+
+ // ✅ 정상 스타일 복원
+ const normalStyle = createCellStyle(exactMapping.isEditable);
+ activeSheet.setStyle(info.row, info.col, normalStyle);
+ cell.locked(!exactMapping.isEditable);
+ }
+ }
+ });
+
+ console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`);
+ }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]);
+
+ // ═══════════════════════════════════════════════════════════════════════════════
+ // 🏗️ 메인 SpreadSheets 초기화 함수
+ // ═══════════════════════════════════════════════════════════════════════════════
+
const initSpread = React.useCallback((spread: any, template?: TemplateItem) => {
const workingTemplate = template || selectedTemplate;
if (!spread || !workingTemplate) return;
try {
+ // 🔄 초기 설정
setCurrentSpread(spread);
setHasChanges(false);
setValidationErrors([]);
- // SPR_LST_SETUP.CONTENT와 SPR_ITM_LST_SETUP.CONTENT 중에서 값이 있는 것을 찾아서 사용
+ // 📋 템플릿 콘텐츠 및 데이터 시트 추출
let contentJson = null;
let dataSheets = null;
-
- // SPR_LST_SETUP.CONTENT가 있으면 우선 사용
+
+ // SPR_LST_SETUP.CONTENT 우선 사용
if (workingTemplate.SPR_LST_SETUP?.CONTENT) {
contentJson = workingTemplate.SPR_LST_SETUP.CONTENT;
dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS;
- console.log('Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')');
- }
- // SPR_ITM_LST_SETUP.CONTENT가 있으면 사용
+ console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
+ }
+ // SPR_ITM_LST_SETUP.CONTENT 대안 사용
else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) {
contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT;
dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS;
- console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')');
+ console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME);
}
if (!contentJson) {
- console.warn('No CONTENT found in template:', workingTemplate.NAME);
+ console.warn('❌ No CONTENT found in template:', workingTemplate.NAME);
return;
}
- console.log(`Loading template content for: ${workingTemplate.NAME} (Type: ${workingTemplate.TMPL_TYPE})`);
+ // 🏗️ SpreadSheets 초기화
+ const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson;
- const jsonData = typeof contentJson === 'string'
- ? JSON.parse(contentJson)
- : contentJson;
-
- // 렌더링 일시 중단
+ // 성능을 위한 렌더링 일시 중단
spread.suspendPaint();
try {
- // 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}> = [];
+ const mappings: CellMapping[] = [];
+ // 🔄 각 데이터 시트의 매핑 정보 처리
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);
const columnConfig = columnsJSON.find(col => col.key === ATT_ID);
-
- mappings.push({
- attId: ATT_ID,
- cellAddress: IN,
- isEditable: isEditable
- });
-
- // 템플릿 타입에 따라 다른 데이터 처리
+
+ // 🎯 템플릿 타입별 데이터 처리
if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 단일 행 처리 (기존 로직)
+ // 📝 단일 행 처리 (SPREAD_ITEM)
+ const isEditable = isFieldEditable(ATT_ID);
+
+ // 매핑 정보 저장
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: IN,
+ isEditable: isEditable,
+ dataRowIndex: 0
+ });
+
const cell = activeSheet.getCell(cellPos.row, cellPos.col);
const value = selectedRow[ATT_ID];
- if (value !== undefined && value !== null) {
- cell.value(value);
- }
- if (value === undefined || value === null) {
- cell.value(null);
- }
+ // 값 설정
+ cell.value(value ?? null);
+
+ // 🎨 스타일 및 편집 권한 설정
+ cell.locked(!isEditable);
+ const style = createCellStyle(isEditable);
+ activeSheet.setStyle(cellPos.row, cellPos.col, style);
- // LIST 타입 컬럼에 드롭다운 설정
+ // 📋 LIST 타입 드롭다운 설정
if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
- setupListValidation(activeSheet, cellPos, columnConfig.options, 1);
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1);
}
- // 스타일 적용
- 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 === 'SPREAD_LIST' && tableData.length > 0) {
- // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움
+ // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨
+ console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`);
- // LIST 타입 컬럼에 드롭다운 설정 (모든 행에 대해)
- if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) {
- setupListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
- }
+ // 🚀 행 확장 (필요시)
+ ensureRowCapacity(activeSheet, cellPos.row + tableData.length);
+
+ // 📋 각 행마다 개별 매핑 생성
+ tableData.forEach((rowData, index) => {
+ const targetRow = cellPos.row + index;
+ const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`;
+ const cellEditable = isFieldEditable(ATT_ID, rowData);
+
+ // 개별 매핑 추가
+ mappings.push({
+ attId: ATT_ID,
+ cellAddress: targetCellAddress, // 각 행마다 다른 주소
+ isEditable: cellEditable,
+ dataRowIndex: index // 원본 데이터 인덱스
+ });
+
+ console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`);
+ });
- // 필요한 경우 행 추가 (tableData 길이만큼 충분히 확보)
- const currentRowCount = activeSheet.getRowCount();
- const requiredRowCount = cellPos.row + tableData.length;
- if (requiredRowCount > currentRowCount) {
- activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가
+ // 📋 LIST 타입 드롭다운 설정 (조건부)
+ if (columnConfig?.type === "LIST" && columnConfig.options) {
+ // 편집 가능한 행이 하나라도 있으면 드롭다운 설정
+ const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData));
+ if (hasEditableRows) {
+ setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length);
+ }
}
+ // 🎨 개별 셀 데이터 및 스타일 설정
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);
- }
-
- if (value === undefined || value === null) {
- cell.value(null);
- }
+ // 값 설정
+ cell.value(value ?? null);
+ console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`);
+ // 편집 권한 및 스타일 설정
const cellEditable = isFieldEditable(ATT_ID, rowData);
cell.locked(!cellEditable);
-
- // 스타일 적용
- const existingStyle = activeSheet.getStyle(targetRow, cellPos.col);
- const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style();
-
- if (cellEditable) {
- newStyle.backColor = "#f0fdf4";
- } else {
- newStyle.backColor = "#f9fafb";
- newStyle.foreColor = "#6b7280";
- }
-
- activeSheet.setStyle(targetRow, cellPos.col, newStyle);
+ const style = createCellStyle(cellEditable);
+ activeSheet.setStyle(targetRow, cellPos.col, style);
});
}
-
- console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`);
+
+ console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`);
}
}
});
}
});
-
- 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);
- });
-
- activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => {
- console.log('Value changed:', info);
- setHasChanges(true);
- });
-
- // 복사 붙여넣기 이벤트 추가
- activeSheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (event: any, info: any) => {
- console.log('Clipboard pasted:', 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) {
- // columnsJSON에서 해당 필드의 shi 확인
- const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
- if (columnConfig?.shi === true) {
- toast.warning(`${mapping.attId} field is read-only (Column configuration)`);
- info.cancel = true;
- return;
- }
-
- // SPREAD_LIST인 경우 해당 행의 데이터에서 shi 확인
- if (templateType === 'SPREAD_LIST') {
- 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;
- }
- }
-
- if (!mapping.isEditable) {
- toast.warning(`${mapping.attId} field is read-only`);
- info.cancel = true;
- }
- }
- });
- // 편집 종료 시 데이터 검증
- activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (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) {
- const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
- if (columnConfig) {
- const cellValue = activeSheet.getValue(info.row, info.col);
- const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options);
-
- if (errorMessage) {
- toast.warning(`Invalid value in ${mapping.attId}: ${errorMessage}`);
- // 스타일을 오류 상태로 변경
- const errorStyle = new GC.Spread.Sheets.Style();
- errorStyle.backColor = "#fef2f2";
- errorStyle.foreColor = "#dc2626";
- activeSheet.setStyle(info.row, info.col, errorStyle);
- } else {
- // 정상 스타일로 복원
- const cellEditable = isFieldEditable(mapping.attId);
- const normalStyle = new GC.Spread.Sheets.Style();
- normalStyle.backColor = cellEditable ? "#f0fdf4" : "#f9fafb";
- normalStyle.foreColor = cellEditable ? "#000000" : "#6b7280";
- activeSheet.setStyle(info.row, info.col, normalStyle);
- }
- }
- }
- });
+ // 💾 매핑 정보 저장 및 이벤트 설정
+ setCellMappings(mappings);
+ setupSheetProtectionAndEvents(activeSheet, mappings);
}
+
} finally {
+ // 렌더링 재개
spread.resumePaint();
}
} catch (error) {
- console.error('Error initializing spread:', error);
+ console.error('❌ Error initializing spread:', error);
toast.error('Failed to load template');
- if (spread && spread.resumePaint) {
+ if (spread?.resumePaint) {
spread.resumePaint();
}
}
- }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, setupListValidation, validateCellValue]);
+ }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]);
// 변경사항 저장 함수
const handleSaveChanges = React.useCallback(async () => {
@@ -608,7 +840,7 @@ export function TemplateViewDialog({
const activeSheet = currentSpread.getActiveSheet();
if (templateType === 'SPREAD_ITEM' && selectedRow) {
- // 단일 행 저장 (기존 로직)
+ // 단일 행 저장
const dataToSave = { ...selectedRow };
cellMappings.forEach(mapping => {
@@ -649,21 +881,21 @@ export function TemplateViewDialog({
// 각 매핑에 대해 해당 행의 값 확인
cellMappings.forEach(mapping => {
- // columnsJSON에서 해당 필드의 shi 확인
- const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
- const isColumnEditable = columnConfig?.shi !== true;
- const isRowEditable = originalRow.shi !== true;
-
- if (mapping.isEditable && isColumnEditable && isRowEditable) {
- 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;
+ if (mapping.dataRowIndex === i && mapping.isEditable) {
+ const columnConfig = columnsJSON.find(col => col.key === mapping.attId);
+ const isColumnEditable = columnConfig?.shi !== true;
+ const isRowEditable = originalRow.shi !== true;
+
+ if (isColumnEditable && isRowEditable) {
+ const cellPos = parseCellAddress(mapping.cellAddress);
+ if (cellPos) {
+ const cellValue = activeSheet.getValue(cellPos.row, cellPos.col);
+
+ // 값이 변경되었는지 확인
+ if (cellValue !== originalRow[mapping.attId]) {
+ dataToSave[mapping.attId] = cellValue;
+ hasRowChanges = true;
+ }
}
}
}
@@ -715,9 +947,9 @@ export function TemplateViewDialog({
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>
@@ -791,11 +1023,11 @@ export function TemplateViewDialog({
</div>
</DialogDescription>
</DialogHeader>
-
+
{/* SpreadSheets 컴포넌트 영역 */}
<div className="flex-1 overflow-hidden">
{selectedTemplate && isClient && isDataValid ? (
- <SpreadSheets
+ <SpreadSheets
key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`}
workbookInitialized={initSpread}
hostStyle={hostStyle}
@@ -823,10 +1055,10 @@ export function TemplateViewDialog({
<Button variant="outline" onClick={onClose}>
Close
</Button>
-
+
{hasChanges && (
- <Button
- variant="default"
+ <Button
+ variant="default"
onClick={handleSaveChanges}
disabled={isPending || validationErrors.length > 0}
>
@@ -845,8 +1077,8 @@ export function TemplateViewDialog({
)}
{validationErrors.length > 0 && (
- <Button
- variant="outline"
+ <Button
+ variant="outline"
onClick={validateAllData}
className="text-red-600 border-red-300 hover:bg-red-50"
>
diff --git a/lib/pdftron/serverSDK/createBasicContractPdf.ts b/lib/pdftron/serverSDK/createBasicContractPdf.ts
index a2e0b350..706508e6 100644
--- a/lib/pdftron/serverSDK/createBasicContractPdf.ts
+++ b/lib/pdftron/serverSDK/createBasicContractPdf.ts
@@ -1,4 +1,7 @@
const { PDFNet } = require("@pdftron/pdfnet-node");
+const fs = require('fs').promises;
+const path = require('path');
+import { file as tmpFile } from "tmp-promise";
type CreateBasicContractPdf = (
templateBuffer: Buffer,
@@ -15,99 +18,43 @@ export const createBasicContractPdf: CreateBasicContractPdf = async (
templateBuffer,
templateData
) => {
- const main = async () => {
- await PDFNet.initialize(process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY);
+ const result = await PDFNet.runWithCleanup(async () => {
console.log("🔄 PDFTron 기본계약서 PDF 변환 시작");
console.log("📝 템플릿 데이터:", JSON.stringify(templateData, null, 2));
- // 템플릿 데이터가 있는 경우 변수 치환 후 PDF 변환
- if (Object.keys(templateData).length > 0) {
- console.log("🔄 템플릿 변수 치환 시작");
-
- try {
- // createReport.ts 방식처럼 템플릿 변수 치환 (UTF-8 인코딩 지원)
- const options = new PDFNet.Convert.OfficeToPDFOptions();
-
- // UTF-8 인코딩 명시 설정 시도
- try {
- options.setCharset("UTF-8");
- console.log("✅ UTF-8 인코딩 설정 완료");
- } catch (charsetError) {
- console.warn("⚠️ UTF-8 인코딩 설정 실패, 기본 설정 사용:", charsetError);
- }
-
- // 템플릿 데이터를 UTF-8로 명시적으로 인코딩
- const templateDataJson = JSON.stringify(templateData, null, 2);
- const utf8TemplateData = Buffer.from(templateDataJson, 'utf8').toString('utf8');
- console.log("📝 UTF-8 인코딩된 템플릿 데이터:", utf8TemplateData);
-
- const tempPath = `/tmp/temp_template_${Date.now()}.docx`;
-
- // 파일도 UTF-8로 저장 (바이너리 데이터는 그대로 유지)
- require('fs').writeFileSync(tempPath, templateBuffer, { encoding: null }); // 바이너리로 저장
+ // 임시 파일 생성
+ const { path: tempDocxPath, cleanup } = await tmpFile({
+ postfix: ".docx",
+ });
+
+ try {
+ // 템플릿 버퍼를 임시 파일로 저장
+ await fs.writeFile(tempDocxPath, templateBuffer);
+
+ let resultDoc;
+
+ // 템플릿 데이터가 있는 경우 변수 치환, 없으면 단순 변환
+ if (templateData && Object.keys(templateData).length > 0) {
+ console.log("🔄 템플릿 변수 치환 시작");
- // Office 템플릿 생성 및 변수 치환
- const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath(
- tempPath,
- options
+ const template = await PDFNet.Convert.createOfficeTemplateWithPath(
+ tempDocxPath
);
-
- const filledDoc = await templateDoc.fillTemplateJson(utf8TemplateData);
-
- // 임시 파일 삭제
- require('fs').unlinkSync(tempPath);
-
- console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료");
-
- const buffer = await filledDoc.saveMemoryBuffer(
- PDFNet.SDFDoc.SaveOptions.e_linearized
+ resultDoc = await template.fillTemplateJson(
+ JSON.stringify(templateData)
);
-
- return {
- result: true,
- buffer,
- };
- } catch (templateError) {
- console.warn("⚠️ 템플릿 변수 치환 실패, 기본 변환 수행:", templateError);
-
- // 템플릿 처리 실패 시 기본 PDF 변환만 수행 (UTF-8 인코딩 적용)
- const fallbackOptions = new PDFNet.Convert.OfficeToPDFOptions();
- try {
- fallbackOptions.setCharset("UTF-8");
- } catch (charsetError) {
- console.warn("⚠️ 폴백 UTF-8 인코딩 설정 실패:", charsetError);
- }
+ console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료");
+ } else {
+ console.log("📄 단순 PDF 변환 수행");
- const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, fallbackOptions);
- const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf);
+ resultDoc = await PDFNet.Convert.office2PDF(tempDocxPath);
- const buffer = await templateDoc.saveMemoryBuffer(
- PDFNet.SDFDoc.SaveOptions.e_linearized
- );
-
- return {
- result: true,
- buffer,
- };
+ console.log("✅ 단순 PDF 변환 완료");
}
- } else {
- // 템플릿 데이터가 없는 경우 단순 변환 (UTF-8 인코딩 적용)
- console.log("📄 단순 PDF 변환 수행 (UTF-8 인코딩)");
-
- const simpleOptions = new PDFNet.Convert.OfficeToPDFOptions();
- try {
- simpleOptions.setCharset("UTF-8");
- console.log("✅ 단순 변환 UTF-8 인코딩 설정 완료");
- } catch (charsetError) {
- console.warn("⚠️ 단순 변환 UTF-8 인코딩 설정 실패:", charsetError);
- }
-
- const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, simpleOptions);
- const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf);
-
- const buffer = await templateDoc.saveMemoryBuffer(
+
+ const buffer = await resultDoc.saveMemoryBuffer(
PDFNet.SDFDoc.SaveOptions.e_linearized
);
@@ -115,23 +62,13 @@ export const createBasicContractPdf: CreateBasicContractPdf = async (
result: true,
buffer,
};
+
+ } finally {
+ // 임시 파일 정리
+ await cleanup();
}
- };
-
- const result = await PDFNet.runWithCleanup(
- main,
+ },
process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY
- )
- .catch((err: any) => {
- console.error("❌ PDFTron 기본계약서 PDF 변환 오류:", err);
- return {
- result: false,
- error: err,
- };
- })
- .then(async (data: any) => {
- return data;
- });
-
+ );
return result;
};
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx
index 1df2d72c..226a053f 100644
--- a/lib/vendors/table/request-pq-dialog.tsx
+++ b/lib/vendors/table/request-pq-dialog.tsx
@@ -43,6 +43,7 @@ import { useSession } from "next-auth/react"
import { DatePicker } from "@/components/ui/date-picker"
import { getALLBasicContractTemplates } from "@/lib/basic-contract/service"
import type { BasicContractTemplate } from "@/db/schema"
+// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음
interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
vendors: Row<Vendor>["original"][]
@@ -78,6 +79,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([])
const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
+
React.useEffect(() => {
if (type === "PROJECT") {
setIsLoadingProjects(true)
@@ -104,6 +106,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
setPqItems("")
setExtraNote("")
setSelectedTemplateIds([])
+
}
}, [props.open])
@@ -116,7 +119,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
startApproveTransition(async () => {
try {
// 1단계: PQ 생성
- console.log("🚀 1단계: PQ 생성 시작")
+ console.log("🚀 PQ 생성 시작")
const { error: pqError } = await requestPQVendors({
ids: vendors.map((v) => v.id),
userId: Number(session.user.id),
@@ -133,128 +136,156 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
toast.error(`PQ 생성 실패: ${pqError}`)
return
}
- console.log("✅ 1단계: PQ 생성 완료")
+ console.log("✅ PQ 생성 완료")
+ toast.success("PQ가 성공적으로 요청되었습니다")
- // 2단계 & 3단계: 기본계약서 템플릿이 선택된 경우에만 실행 (여러 템플릿 처리)
+ // 2단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리
if (selectedTemplateIds.length > 0) {
- console.log(`🚀 2단계 & 3단계: ${selectedTemplateIds.length}개 템플릿 처리 시작`)
+ const templates = basicContractTemplates.filter(t =>
+ selectedTemplateIds.includes(t.id)
+ )
- let successCount = 0
- let errorCount = 0
- const errors: string[] = []
-
- // 템플릿별로 반복 처리
- for (let i = 0; i < selectedTemplateIds.length; i++) {
- const templateId = selectedTemplateIds[i]
- const selectedTemplate = basicContractTemplates.find(t => t.id === templateId)
-
- if (!selectedTemplate) {
- console.error(`템플릿 ID ${templateId}를 찾을 수 없습니다`)
- errorCount++
- errors.push(`템플릿 ID ${templateId}를 찾을 수 없습니다`)
- continue
- }
-
- try {
- console.log(`📄 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 2단계: DOCX to PDF 변환 시작`)
-
- // 템플릿 파일을 가져와서 PDF로 변환
- const formData = new FormData()
-
- // 템플릿 파일 가져오기 (서버에서 파일 읽기)
- const templateResponse = await fetch('/api/basic-contract/get-template', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ templateId })
- })
-
- if (!templateResponse.ok) {
- throw new Error(`템플릿 파일을 가져올 수 없습니다: ${selectedTemplate.templateName}`)
- }
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 템플릿 파일 가져오기 완료`)
-
- const templateBlob = await templateResponse.blob()
- const templateFile = new File([templateBlob], selectedTemplate.fileName || 'template.docx')
-
- // 템플릿 데이터 생성 (첫 번째 협력업체 정보 기반)
- const firstVendor = vendors[0]
- const templateData = {
- // 영문 변수명으로 변경 (PDFTron이 한글 변수명을 지원하지 않음)
- vendor_name: firstVendor?.vendorName || '협력업체명',
- address: firstVendor?.address || '주소',
- representative_name: firstVendor?.representativeName || '대표자명',
- today_date: new Date().toLocaleDateString('ko-KR'),
- }
-
- console.log(`📝 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 생성된 템플릿 데이터:`, templateData)
-
- formData.append('templateFile', templateFile)
- formData.append('outputFileName', `${selectedTemplate.templateName}_converted.pdf`)
- formData.append('templateData', JSON.stringify(templateData))
-
- // PDF 변환 호출
- const pdfResponse = await fetch('/api/pdftron/createBasicContractPdf', {
- method: 'POST',
- body: formData,
- })
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 호출 완료`)
-
- if (!pdfResponse.ok) {
- const errorText = await pdfResponse.text()
- throw new Error(`PDF 변환 실패 (${selectedTemplate.templateName}): ${errorText}`)
- }
-
- const pdfBuffer = await pdfResponse.arrayBuffer()
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 완료`)
-
- // 3단계: 변환된 PDF로 기본계약 생성
- console.log(`📋 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 시작`)
- const { error: contractError } = await requestBasicContractInfo({
- vendorIds: vendors.map((v) => v.id),
- requestedBy: Number(session.user.id),
- templateId,
- pdfBuffer: new Uint8Array(pdfBuffer), // ArrayBuffer를 Uint8Array로 변환하여 전달
- })
-
- if (contractError) {
- console.error(`기본계약 생성 오류 (${selectedTemplate.templateName}):`, contractError)
- errorCount++
- errors.push(`${selectedTemplate.templateName}: ${contractError}`)
- } else {
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 완료`)
- successCount++
- }
- } catch (templateError) {
- console.error(`템플릿 처리 오류 (${selectedTemplate.templateName}):`, templateError)
- errorCount++
- errors.push(`${selectedTemplate.templateName}: ${templateError instanceof Error ? templateError.message : '알 수 없는 오류'}`)
- }
- }
-
- // 결과 토스트 메시지
- if (successCount > 0 && errorCount === 0) {
- toast.success(`PQ 요청 및 ${successCount}개 기본계약서 생성이 모두 완료되었습니다!`)
- } else if (successCount > 0 && errorCount > 0) {
- toast.success(`PQ는 성공적으로 요청되었습니다. ${successCount}개 기본계약서 성공, ${errorCount}개 실패`)
- console.error('기본계약서 생성 오류들:', errors)
- } else if (errorCount > 0) {
- toast.error(`PQ는 성공적으로 요청되었지만, 모든 기본계약서 생성이 실패했습니다`)
- console.error('기본계약서 생성 오류들:', errors)
- }
- } else {
- // 기본계약서 템플릿이 선택되지 않은 경우
- toast.success("PQ가 성공적으로 요청되었습니다")
+ console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿")
+ await processBasicContractsInBackground(templates, vendors)
}
-
+
+ // 완료 후 다이얼로그 닫기
props.onOpenChange?.(false)
onSuccess?.()
+
} catch (error) {
- console.error('전체 프로세스 오류:', error)
+ console.error('PQ 생성 오류:', error)
toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
}
})
}
+ // 백그라운드에서 기본계약서 처리
+ const processBasicContractsInBackground = async (templates: BasicContractTemplate[], vendors: any[]) => {
+ if (!session?.user?.id) {
+ toast.error("인증 정보가 없습니다")
+ return
+ }
+
+ try {
+ const totalContracts = templates.length * vendors.length
+ let processedCount = 0
+
+ // 각 벤더별로, 각 템플릿을 처리
+ for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) {
+ const vendor = vendors[vendorIndex]
+
+ // 벤더별 템플릿 데이터 생성
+ const templateData = {
+ vendor_name: vendor.vendorName || '협력업체명',
+ address: vendor.address || '주소',
+ representative_name: vendor.representativeName || '대표자명',
+ today_date: new Date().toLocaleDateString('ko-KR'),
+ }
+
+ console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData)
+
+ // 해당 벤더에 대해 각 템플릿을 순차적으로 처리
+ for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) {
+ const template = templates[templateIndex]
+ processedCount++
+
+ console.log(`📄 처리 중: ${vendor.vendorName} - ${template.templateName} (${processedCount}/${totalContracts})`)
+
+ // 개별 벤더에 대한 기본계약 생성
+ await processTemplate(template, templateData, [vendor])
+
+ console.log(`✅ 완료: ${vendor.vendorName} - ${template.templateName}`)
+ }
+ }
+
+ toast.success(`총 ${totalContracts}개 기본계약이 모두 생성되었습니다`)
+
+ } catch (error) {
+ console.error('기본계약 처리 중 오류:', error)
+ toast.error(`기본계약 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
+ }
+ }
+
+ const processTemplate = async (template: BasicContractTemplate, templateData: any, vendors: any[]) => {
+ try {
+ // 1. 템플릿 파일 가져오기
+ const templateResponse = await fetch('/api/basic-contract/get-template', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ templateId: template.id })
+ })
+
+ if (!templateResponse.ok) {
+ throw new Error(`템플릿 파일을 가져올 수 없습니다: ${template.templateName}`)
+ }
+
+ const templateBlob = await templateResponse.blob()
+
+ // 2. PDFTron을 사용해서 변수 치환 및 PDF 변환
+ // @ts-ignore
+ const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
+
+ // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음)
+ const tempDiv = document.createElement('div')
+ tempDiv.style.display = 'none'
+ document.body.appendChild(tempDiv)
+
+ const instance = await WebViewer(
+ {
+ path: "/pdftronWeb",
+ licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+ fullAPI: true,
+ },
+ tempDiv
+ )
+
+ try {
+ const { Core } = instance
+ const { createDocument } = Core
+
+ // 3. 템플릿 문서 생성 및 변수 치환
+ const templateDoc = await createDocument(templateBlob, {
+ filename: template.fileName || 'template.docx',
+ extension: 'docx',
+ })
+
+ console.log("🔄 변수 치환 시작:", templateData)
+ await templateDoc.applyTemplateValues(templateData)
+ console.log("✅ 변수 치환 완료")
+
+ // 4. PDF 변환
+ const fileData = await templateDoc.getFileData()
+ const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' })
+
+ console.log(`✅ PDF 변환 완료: ${template.templateName}`, `크기: ${pdfBuffer.byteLength} bytes`)
+
+ // 5. 기본계약 생성 요청
+ const { error: contractError } = await requestBasicContractInfo({
+ vendorIds: vendors.map((v) => v.id),
+ requestedBy: Number(session!.user.id),
+ templateId: template.id,
+ pdfBuffer: new Uint8Array(pdfBuffer),
+ })
+
+ if (contractError) {
+ throw new Error(contractError)
+ }
+
+ console.log(`✅ 기본계약 생성 완료: ${template.templateName}`)
+
+ } finally {
+ // 임시 WebViewer 정리
+ instance.UI.dispose()
+ document.body.removeChild(tempDiv)
+ }
+
+ } catch (error) {
+ console.error(`❌ 템플릿 처리 실패: ${template.templateName}`, error)
+ throw error
+ }
+ }
+
const dialogContent = (
<div className="space-y-4 py-2">
{/* 선택된 협력업체 정보 */}
@@ -309,7 +340,15 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
<Label htmlFor="dueDate">PQ 제출 마감일</Label>
<DatePicker
date={dueDate ? new Date(dueDate) : undefined}
- onSelect={(date?: Date) => setDueDate(date ? date.toISOString().slice(0, 10) : "")}
+ onSelect={(date?: Date) => {
+ if (date) {
+ // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지)
+ const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
+ setDueDate(kstDate.toISOString().slice(0, 10))
+ } else {
+ setDueDate("")
+ }
+ }}
placeholder="마감일 선택"
/>
</div>
@@ -421,6 +460,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
</Button>
</DialogFooter>
</DialogContent>
+
+
</Dialog>
)
}
@@ -452,6 +493,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro
</Button>
</DrawerFooter>
</DrawerContent>
+
+
</Drawer>
)
}
diff --git a/pages/api/pdftron/createBasicContractPdf.ts b/pages/api/pdftron/createBasicContractPdf.ts
index 1122c022..376d8540 100644
--- a/pages/api/pdftron/createBasicContractPdf.ts
+++ b/pages/api/pdftron/createBasicContractPdf.ts
@@ -294,13 +294,15 @@ export default async function handler(
// 4. 원본 파일 읽기
const originalBuffer = await fs.readFile(templateFile.filepath);
-
+ // const publicDir = path.join(process.cwd(), "public", "basicContract");
+ // const testBuffer = await fs.readFile(path.join(publicDir, "test123.docx"));
+ // console.log(testBuffer);
// 5. DRM 복호화 처리 (보안 검증 포함)
- console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`);
- const decryptedBuffer = await decryptBufferWithDRM(
- originalBuffer,
- templateFile.originalFilename || 'template.docx'
- );
+ // console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`);
+ // const decryptedBuffer = await decryptBufferWithDRM(
+ // originalBuffer,
+ // templateFile.originalFilename || 'template.docx'
+ // );
// 6. 복호화된 버퍼로 기본계약서 PDF 생성
console.log(`📄 [${requestId}] 기본계약서 PDF 생성 시작`);
@@ -308,7 +310,7 @@ export default async function handler(
result,
buffer: pdfBuffer,
error,
- } = await createBasicContractPdf(decryptedBuffer, templateData);
+ } = await createBasicContractPdf(originalBuffer, templateData);
if (result && pdfBuffer) {
console.log(`✅ [${requestId}] 기본계약서 PDF 생성 성공: ${outputFileName}`);