diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-19 07:51:27 +0000 |
| commit | 9ecdfb23fe3df6a5df86782385002c562dfc1198 (patch) | |
| tree | 4188cb7e6bf2c862d9c86a59d79946bd41217227 /components | |
| parent | b67861fbb424c7ad47ad1538f75e2945bd8890c5 (diff) | |
(대표님) rfq 히스토리, swp 등
Diffstat (limited to 'components')
| -rw-r--r-- | components/client-data-table/data-table-filter-list.tsx | 23 | ||||
| -rw-r--r-- | components/client-data-table/table-filters.ts | 344 | ||||
| -rw-r--r-- | components/form-data-stat/form-data-stat-table.tsx | 15 | ||||
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 3 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog copy 2.tsx | 1002 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog copy 3.tsx | 1916 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog copy 5.tsx (renamed from components/form-data/spreadJS-dialog copy 4.tsx) | 6 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog copy.tsx | 539 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 32 | ||||
| -rw-r--r-- | components/vendor-data/tag-table/tag-table-column.tsx | 2 |
10 files changed, 392 insertions, 3490 deletions
diff --git a/components/client-data-table/data-table-filter-list.tsx b/components/client-data-table/data-table-filter-list.tsx index f06d837e..6a9da8ba 100644 --- a/components/client-data-table/data-table-filter-list.tsx +++ b/components/client-data-table/data-table-filter-list.tsx @@ -84,25 +84,16 @@ export function ClientDataTableAdvancedFilter<TData>({ // 3) Sync to table React.useEffect(() => { const newColumnFilters = filters.map((f) => { - // If it's numeric, transform f.value from string → number - if (f.type === "number") { - return { - id: String(f.id), - value: { - operator: f.operator, - inputValue: parseFloat(String(f.value)), - } - } - } - else { - // For text, date, boolean, etc., it's fine to keep value as a string or whatever - return { - id: String(f.id), - value: f.value, + // 모든 타입에 대해 operator와 value를 함께 전달 + return { + id: String(f.id), + value: { + operator: f.operator, + value: f.type === "number" ? parseFloat(String(f.value)) : f.value, } } }) - + table.setColumnFilters(newColumnFilters) }, [filters, joinOperator, table]) diff --git a/components/client-data-table/table-filters.ts b/components/client-data-table/table-filters.ts new file mode 100644 index 00000000..44391999 --- /dev/null +++ b/components/client-data-table/table-filters.ts @@ -0,0 +1,344 @@ +import { Row } from "@tanstack/react-table" + +export type FilterOperator = + | "iLike" | "notILike" | "eq" | "ne" | "isEmpty" | "isNotEmpty" + | "lt" | "lte" | "gt" | "gte" | "isBetween" | "isRelativeToToday" + +export type ColumnType = "text" | "number" | "date" | "boolean" | "select" | "multi-select" + +interface FilterValue { + operator: FilterOperator + value: any +} + +/** + * 글로벌 필터 함수 생성 + * @param type - 컬럼 타입 + * @returns 필터 함수 + */ +export const createFilterFn = (type: ColumnType) => { + return <TData>(row: Row<TData>, columnId: string, filterValue: FilterValue) => { + const cellValue = row.getValue(columnId) + const { operator, value } = filterValue + + // 공통 처리: isEmpty/isNotEmpty + if (operator === "isEmpty") { + if (type === "multi-select") { + return !cellValue || (Array.isArray(cellValue) && cellValue.length === 0) + } + return cellValue == null || cellValue === "" || cellValue === undefined + } + + if (operator === "isNotEmpty") { + if (type === "multi-select") { + return cellValue != null && Array.isArray(cellValue) && cellValue.length > 0 + } + return cellValue != null && cellValue !== "" && cellValue !== undefined + } + + // value가 없고 isEmpty/isNotEmpty가 아닌 경우 + if ((value === "" || value == null) && operator !== "isEmpty" && operator !== "isNotEmpty") { + return true + } + + // 타입별 처리 + switch (type) { + case "text": { + const cellStr = String(cellValue || "").toLowerCase() + const filterStr = String(value || "").toLowerCase() + + switch (operator) { + case "iLike": + return cellStr.includes(filterStr) + case "notILike": + return !cellStr.includes(filterStr) + case "eq": + return cellStr === filterStr + case "ne": + return cellStr !== filterStr + default: + return true + } + } + + case "number": { + const cellNum = cellValue != null ? Number(cellValue) : null + const filterNum = value != null ? Number(value) : null + + if (cellNum == null || filterNum == null) { + return false + } + + switch (operator) { + case "eq": + return cellNum === filterNum + case "ne": + return cellNum !== filterNum + case "lt": + return cellNum < filterNum + case "lte": + return cellNum <= filterNum + case "gt": + return cellNum > filterNum + case "gte": + return cellNum >= filterNum + default: + return true + } + } + + case "date": { + const cellDate = cellValue ? new Date(cellValue as string | Date) : null + + if (!cellDate || isNaN(cellDate.getTime())) { + return false + } + + switch (operator) { + case "eq": { + if (!value) return false + const filterDate = new Date(value) + return cellDate.toDateString() === filterDate.toDateString() + } + + case "ne": { + if (!value) return true + const filterDate = new Date(value) + return cellDate.toDateString() !== filterDate.toDateString() + } + + case "lt": { + if (!value) return false + const filterDate = new Date(value) + return cellDate < filterDate + } + + case "lte": { + if (!value) return false + const filterDate = new Date(value) + filterDate.setHours(23, 59, 59, 999) // 그 날의 끝까지 포함 + return cellDate <= filterDate + } + + case "gt": { + if (!value) return false + const filterDate = new Date(value) + return cellDate > filterDate + } + + case "gte": { + if (!value) return false + const filterDate = new Date(value) + filterDate.setHours(0, 0, 0, 0) // 그 날의 시작부터 포함 + return cellDate >= filterDate + } + + case "isBetween": { + if (!Array.isArray(value) || value.length !== 2) return false + const [startDate, endDate] = value + if (!startDate || !endDate) return false + const start = new Date(startDate) + const end = new Date(endDate) + start.setHours(0, 0, 0, 0) + end.setHours(23, 59, 59, 999) + return cellDate >= start && cellDate <= end + } + + case "isRelativeToToday": { + const today = new Date() + today.setHours(0, 0, 0, 0) + const tomorrow = new Date(today) + tomorrow.setDate(tomorrow.getDate() + 1) + + // value는 상대적 날짜 지정자 (예: "today", "yesterday", "thisWeek", "lastWeek", "thisMonth", "lastMonth") + switch (value) { + case "today": + return cellDate >= today && cellDate < tomorrow + + case "yesterday": { + const yesterday = new Date(today) + yesterday.setDate(yesterday.getDate() - 1) + return cellDate >= yesterday && cellDate < today + } + + case "tomorrow": { + const dayAfterTomorrow = new Date(tomorrow) + dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 1) + return cellDate >= tomorrow && cellDate < dayAfterTomorrow + } + + case "thisWeek": { + const startOfWeek = new Date(today) + startOfWeek.setDate(today.getDate() - today.getDay()) // 일요일부터 시작 + const endOfWeek = new Date(startOfWeek) + endOfWeek.setDate(startOfWeek.getDate() + 6) + endOfWeek.setHours(23, 59, 59, 999) + return cellDate >= startOfWeek && cellDate <= endOfWeek + } + + case "lastWeek": { + const startOfLastWeek = new Date(today) + startOfLastWeek.setDate(today.getDate() - today.getDay() - 7) + const endOfLastWeek = new Date(startOfLastWeek) + endOfLastWeek.setDate(startOfLastWeek.getDate() + 6) + endOfLastWeek.setHours(23, 59, 59, 999) + return cellDate >= startOfLastWeek && cellDate <= endOfLastWeek + } + + case "thisMonth": { + const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1) + const endOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0) + endOfMonth.setHours(23, 59, 59, 999) + return cellDate >= startOfMonth && cellDate <= endOfMonth + } + + case "lastMonth": { + const startOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1) + const endOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0) + endOfLastMonth.setHours(23, 59, 59, 999) + return cellDate >= startOfLastMonth && cellDate <= endOfLastMonth + } + + case "thisYear": { + const startOfYear = new Date(today.getFullYear(), 0, 1) + const endOfYear = new Date(today.getFullYear(), 11, 31) + endOfYear.setHours(23, 59, 59, 999) + return cellDate >= startOfYear && cellDate <= endOfYear + } + + case "lastYear": { + const startOfLastYear = new Date(today.getFullYear() - 1, 0, 1) + const endOfLastYear = new Date(today.getFullYear() - 1, 11, 31) + endOfLastYear.setHours(23, 59, 59, 999) + return cellDate >= startOfLastYear && cellDate <= endOfLastYear + } + + default: + // 숫자가 오면 일 단위 상대 날짜로 처리 (예: "7" = 7일 이내, "-7" = 7일 전) + const days = parseInt(value) + if (!isNaN(days)) { + if (days > 0) { + // 미래 n일 이내 + const futureDate = new Date(today) + futureDate.setDate(futureDate.getDate() + days) + return cellDate >= today && cellDate <= futureDate + } else if (days < 0) { + // 과거 n일 이내 + const pastDate = new Date(today) + pastDate.setDate(pastDate.getDate() + days) + return cellDate >= pastDate && cellDate <= today + } + } + return true + } + } + + default: + return true + } + } + + case "boolean": { + const cellBool = cellValue === true || cellValue === "true" || cellValue === 1 + const filterBool = value === true || value === "true" + + switch (operator) { + case "eq": + return cellBool === filterBool + case "ne": + return cellBool !== filterBool + default: + return true + } + } + + case "select": { + const cellStr = String(cellValue || "") + const filterStr = String(value || "") + + switch (operator) { + case "eq": + return cellStr === filterStr + case "ne": + return cellStr !== filterStr + default: + return true + } + } + + case "multi-select": { + const cellArray = Array.isArray(cellValue) ? cellValue : [] + const filterArray = Array.isArray(value) ? value : [] + + switch (operator) { + case "eq": + // 선택된 모든 값들이 포함되어 있는지 확인 + return filterArray.every(v => cellArray.includes(v)) + case "ne": + // 선택된 값들 중 하나라도 포함되어 있지 않은지 확인 + return !filterArray.some(v => cellArray.includes(v)) + default: + return true + } + } + + default: + return true + } + } +} + +/** + * AND/OR 조건으로 여러 필터 결합 + * @param filters - 필터 배열 + * @param joinOperator - 결합 연산자 ("and" | "or") + */ +export const combineFilters = <TData>( + row: Row<TData>, + filters: Array<{ + columnId: string + filterValue: FilterValue + type: ColumnType + }>, + joinOperator: "and" | "or" = "and" +): boolean => { + if (filters.length === 0) return true + + if (joinOperator === "and") { + return filters.every(filter => { + const filterFn = createFilterFn(filter.type) + return filterFn(row, filter.columnId, filter.filterValue) + }) + } else { + return filters.some(filter => { + const filterFn = createFilterFn(filter.type) + return filterFn(row, filter.columnId, filter.filterValue) + }) + } +} + +/** + * 테이블 전체에 대한 커스텀 필터 함수 + * ClientDataTableAdvancedFilter와 함께 사용 + */ +export const globalFilterFn = <TData>( + row: Row<TData>, + columnId: string, + filterValue: any +): boolean => { + // filterValue가 객체 형태로 전달되는 경우를 처리 + if (filterValue && typeof filterValue === 'object' && 'filters' in filterValue) { + const { filters, joinOperator } = filterValue + return combineFilters(row, filters, joinOperator) + } + + // 단일 필터의 경우 + if (filterValue && typeof filterValue === 'object' && 'operator' in filterValue) { + // 컬럼 타입을 추론하거나 전달받아야 함 + // 기본적으로 text로 처리 + const filterFn = createFilterFn("text") + return filterFn(row, columnId, filterValue) + } + + return true +}
\ No newline at end of file diff --git a/components/form-data-stat/form-data-stat-table.tsx b/components/form-data-stat/form-data-stat-table.tsx index a56a4e88..1f313a2f 100644 --- a/components/form-data-stat/form-data-stat-table.tsx +++ b/components/form-data-stat/form-data-stat-table.tsx @@ -16,6 +16,7 @@ import { Progress } from "@/components/ui/progress"; import { getVendorFormStatus, getProjectsWithContracts } from "@/lib/forms/stat"; import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command"; +import { createFilterFn } from "@/components/client-data-table/table-filters"; // 타입 정의 interface VendorFormStatus { @@ -216,6 +217,8 @@ export function VendorFormStatusTable({ { accessorKey: "vendorName", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="벤더명" />, + filterFn: createFilterFn("text"), + cell: ({ row }) => ( <div className="font-medium">{row.original.vendorName}</div> ), @@ -226,6 +229,8 @@ export function VendorFormStatusTable({ { accessorKey: "formCount", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="Form 개수" />, + filterFn: createFilterFn("number"), + cell: ({ row }) => ( <div className="text-center"> <Badge variant="outline">{row.original.formCount}</Badge> @@ -237,6 +242,8 @@ export function VendorFormStatusTable({ { accessorKey: "tagCount", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="Tag 개수" />, + filterFn: createFilterFn("number"), + cell: ({ row }) => ( <div className="text-center"> <Badge variant="outline">{row.original.tagCount}</Badge> @@ -248,6 +255,8 @@ export function VendorFormStatusTable({ { accessorKey: "totalFields", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="전체 필드" />, + filterFn: createFilterFn("number"), + cell: ({ row }) => ( <div className="text-center font-mono"> {row.original.totalFields.toLocaleString()} @@ -259,6 +268,8 @@ export function VendorFormStatusTable({ { accessorKey: "completedFields", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="완료 필드" />, + filterFn: createFilterFn("number"), + cell: ({ row }) => ( <div className="text-center font-mono"> {row.original.completedFields.toLocaleString()} @@ -270,6 +281,8 @@ export function VendorFormStatusTable({ { accessorKey: "completionRate", header: ({ column }) => <ClientDataTableColumnHeaderSimple column={column} title="완료율" />, + filterFn: createFilterFn("number"), + cell: ({ row }) => { const rate = row.original.completionRate; return ( @@ -293,6 +306,8 @@ export function VendorFormStatusTable({ { id: "progress", header: "진행 상태", + filterFn: createFilterFn("number"), + cell: ({ row }) => { const { completedFields, totalFields } = row.original; return ( diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx index 598b66c6..2c6b6a30 100644 --- a/components/form-data/form-data-table-columns.tsx +++ b/components/form-data/form-data-table-columns.tsx @@ -19,6 +19,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from 'sonner'; +import { createFilterFn } from "@/components/client-data-table/table-filters"; /** row 액션 관련 타입 */ export interface DataTableRowAction<TData> { @@ -251,6 +252,8 @@ function createColumnDef(col: DataTableColumnJSON, isInGroup: boolean = false): /> ), + filterFn: col.type === 'NUMBER' ? createFilterFn("number") : col.type === 'LIST' ? createFilterFn("multi-select"):createFilterFn("text"), + meta: { excelHeader: col.label, minWidth: 80, diff --git a/components/form-data/spreadJS-dialog copy 2.tsx b/components/form-data/spreadJS-dialog copy 2.tsx deleted file mode 100644 index 520362ff..00000000 --- a/components/form-data/spreadJS-dialog copy 2.tsx +++ /dev/null @@ -1,1002 +0,0 @@ -"use client"; - -import * as React from "react"; -import dynamic from "next/dynamic"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -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, AlertTriangle } from "lucide-react"; -import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; -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"> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading SpreadSheets... - </div> - ) - } -); - -// 라이센스 키 설정을 클라이언트에서만 실행 -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { - GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; -} - -interface TemplateItem { - TMPL_ID: string; - NAME: string; - TMPL_TYPE: string; - SPR_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; - GRD_LST_SETUP: { - REG_TYPE_ID: string; - SPR_ITM_IDS: Array<string>; - ATTS: Array<{}>; - }; - SPR_ITM_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; -} - -interface ValidationError { - cellAddress: string; - attId: string; - value: any; - expectedType: ColumnType; - message: string; -} - -interface CellMapping { - attId: string; - cellAddress: string; - isEditable: boolean; - dataRowIndex?: number; -} - -interface TemplateViewDialogProps { - isOpen: boolean; - onClose: () => void; - templateData: TemplateItem[] | any; - selectedRow?: GenericData; // SPREAD_ITEM용 - tableData?: GenericData[]; // SPREAD_LIST용 - formCode: string; - columnsJSON: DataTableColumnJSON[] - contractItemId: number; - editableFieldsMap?: Map<string, string[]>; - onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void; -} - -export function TemplateViewDialog({ - isOpen, - onClose, - templateData, - selectedRow, - tableData = [], - formCode, - contractItemId, - columnsJSON, - editableFieldsMap = new Map(), - onUpdateSuccess -}: TemplateViewDialogProps) { - 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<any>(null); - 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[]>([]); - const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]); - - // 클라이언트 사이드에서만 렌더링되도록 보장 - React.useEffect(() => { - setIsClient(true); - }, []); - - // 사용 가능한 템플릿들을 필터링하고 설정 - 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); - setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); - } - }, [templateData, selectedTemplateId]); - - // 선택된 템플릿 변경 처리 - const handleTemplateChange = (templateId: string) => { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (template) { - setSelectedTemplateId(templateId); - setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); - setHasChanges(false); - setValidationErrors([]); - - // SpreadSheets 재초기화 - if (currentSpread) { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (template) { - initSpread(currentSpread, template); - } - } - } - }; - - // 현재 선택된 템플릿 가져오기 - const selectedTemplate = React.useMemo(() => { - return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); - }, [availableTemplates, selectedTemplateId]); - - // 편집 가능한 필드 목록 계산 - const editableFields = React.useMemo(() => { - // 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 []; - }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); - - // 필드가 편집 가능한지 판별하는 함수 - const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { - // columnsJSON에서 해당 attId의 shi 값 확인 - const columnConfig = columnsJSON.find(col => col.key === attId); - 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인 경우: 개별 행의 편집 가능성도 고려 - 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, columnsJSON, editableFields]); - - // 편집 가능한 필드 개수 계산 - 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) - 1; - - return { row, col }; - }; - - // 데이터 타입 검증 함수 - const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { - if (value === undefined || value === null || value === "") { - return null; // 빈 값은 별도 required 검증에서 처리 - } - - switch (columnType) { - case "NUMBER": - if (isNaN(Number(value))) { - return "Value must be a valid number"; - } - break; - case "LIST": - if (options && !options.includes(String(value))) { - return `Value must be one of: ${options.join(", ")}`; - } - break; - case "STRING": - // STRING 타입은 대부분의 값을 허용 - break; - default: - // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 - break; - } - - return null; - }; - - // 전체 데이터 검증 함수 - const validateAllData = React.useCallback(() => { - if (!currentSpread || !selectedTemplate) return []; - - const activeSheet = currentSpread.getActiveSheet(); - const errors: ValidationError[] = []; - - cellMappings.forEach(mapping => { - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (!columnConfig) return; - - const cellPos = parseCellAddress(mapping.cellAddress); - if (!cellPos) return; - - if (templateType === 'SPREAD_ITEM') { - // 단일 행 검증 - 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 - }); - } - } else if (templateType === 'SPREAD_LIST') { - // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 - 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]); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🛠️ 헬퍼 함수들 - // ═══════════════════════════════════════════════════════════════════════════════ - - // 🎨 셀 스타일 생성 - 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; - }, []); - - -// 🎯 간소화된 드롭다운 설정 - setupSimpleValidation 완전 제거 - -const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { - try { - console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options); - - // ✅ options 정규화 - const safeOptions = options - .filter(opt => opt !== null && opt !== undefined && opt !== '') - .map(opt => String(opt).trim()) - .filter(opt => opt.length > 0) - .filter((opt, index, arr) => arr.indexOf(opt) === index) - .slice(0, 20); - - if (safeOptions.length === 0) { - console.warn(`⚠️ No valid options found, skipping`); - return; - } - - console.log(`📋 Safe options:`, safeOptions); - - // ✅ DataValidation용 문자열 준비 - const optionsString = safeOptions.join(','); - - // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성! - for (let i = 0; i < rowCount; i++) { - try { - const targetRow = cellPos.row + i; - - // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성 - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(safeOptions); // 배열로 전달 - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - - // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성 - const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString); - - // ComboBox + DataValidation 둘 다 적용 - activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); - activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator); - - // 셀 잠금 해제 - const cell = activeSheet.getCell(targetRow, cellPos.col); - cell.locked(false); - - console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`); - - } catch (cellError) { - console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError); - } - } - - console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); - - } catch (error) { - console.error('❌ Dropdown setup failed:', error); - } -}, []); - // 🚀 행 용량 확보 - 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([]); - - // 📋 템플릿 콘텐츠 및 데이터 시트 추출 - let contentJson = null; - let dataSheets = null; - - // 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); - } - // 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); - } - - if (!contentJson) { - console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); - return; - } - - // 🏗️ SpreadSheets 초기화 - const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - - // 성능을 위한 렌더링 일시 중단 - spread.suspendPaint(); - - try { - // 템플릿 구조 로드 - spread.fromJSON(jsonData); - const activeSheet = spread.getActiveSheet(); - - // 시트 보호 해제 (편집을 위해) - activeSheet.options.isProtected = false; - - // 📊 셀 매핑 및 데이터 처리 - if (dataSheets && dataSheets.length > 0) { - 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 columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - // 🎯 템플릿 타입별 데이터 처리 - 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]; - - // 값 설정 - cell.value(value ?? null); - - // 🎨 스타일 및 편집 권한 설정 - cell.locked(!isEditable); - const style = createCellStyle(isEditable); - activeSheet.setStyle(cellPos.row, cellPos.col, style); - - // 📋 LIST 타입 드롭다운 설정 - if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); - } - - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 - console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - - // 🚀 행 확장 (필요시) - 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'})`); - }); - - // 📋 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]; - - // 값 설정 - cell.value(value ?? null); - console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); - - // 편집 권한 및 스타일 설정 - const cellEditable = isFieldEditable(ATT_ID, rowData); - cell.locked(!cellEditable); - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, cellPos.col, style); - }); - } - - console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); - } - } - }); - } - }); - - // 💾 매핑 정보 저장 및 이벤트 설정 - setCellMappings(mappings); - setupSheetProtectionAndEvents(activeSheet, mappings); - } - - } finally { - // 렌더링 재개 - spread.resumePaint(); - } - - } catch (error) { - console.error('❌ Error initializing spread:', error); - toast.error('Failed to load template'); - if (spread?.resumePaint) { - spread.resumePaint(); - } - } - }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]); - - // 변경사항 저장 함수 - const handleSaveChanges = React.useCallback(async () => { - if (!currentSpread || !hasChanges) { - toast.info("No changes to save"); - return; - } - - // 저장 전 데이터 검증 - const errors = validateAllData(); - if (errors.length > 0) { - toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`); - return; - } - - try { - setIsPending(true); - - const activeSheet = currentSpread.getActiveSheet(); - - if (templateType === 'SPREAD_ITEM' && 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; - } - - toast.success("Changes saved successfully!"); - onUpdateSuccess?.(dataToSave); - - } else if (templateType === 'SPREAD_LIST' && 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.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; - } - } - } - } - }); - - // 변경사항이 있는 행만 저장 - if (hasRowChanges) { - dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록 - - 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"); - } - } - - setHasChanges(false); - setValidationErrors([]); - - } catch (error) { - console.error("Error saving changes:", error); - toast.error("An unexpected error occurred while saving"); - } finally { - setIsPending(false); - } - }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData]); - - if (!isOpen) return null; - - // 데이터 유효성 검사 - const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; - const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; - - return ( - <Dialog open={isOpen} onOpenChange={onClose}> - <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> - <DialogDescription> - <div className="space-y-3"> - {/* 템플릿 선택 */} - {availableTemplates.length > 1 && ( - <div className="flex items-center gap-4"> - <span className="text-sm font-medium">Template:</span> - <Select value={selectedTemplateId} onValueChange={handleTemplateChange}> - <SelectTrigger className="w-64"> - <SelectValue placeholder="Select a template" /> - </SelectTrigger> - <SelectContent> - {availableTemplates.map(template => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - {template.NAME} ({template.TMPL_TYPE}) - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - )} - - {/* 템플릿 정보 */} - {selectedTemplate && ( - <div className="flex items-center gap-4 text-sm"> - <span className="font-medium text-blue-600"> - Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'} - </span> - {templateType === 'SPREAD_ITEM' && selectedRow && ( - <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span> - )} - {templateType === 'SPREAD_LIST' && ( - <span>• {dataCount} rows</span> - )} - {hasChanges && ( - <span className="text-orange-600 font-medium"> - • Unsaved changes - </span> - )} - {validationErrors.length > 0 && ( - <span className="text-red-600 font-medium flex items-center"> - <AlertTriangle className="w-4 h-4 mr-1" /> - {validationErrors.length} validation errors - </span> - )} - </div> - )} - - {/* 범례 */} - <div className="flex items-center gap-4 text-xs"> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span> - Editable fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span> - Read-only fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-red-100 border border-red-300 mr-1"></span> - Validation errors - </span> - {cellMappings.length > 0 && ( - <span className="text-blue-600"> - {editableFieldsCount} of {cellMappings.length} fields editable - </span> - )} - </div> - </div> - </DialogDescription> - </DialogHeader> - - {/* SpreadSheets 컴포넌트 영역 */} - <div className="flex-1 overflow-hidden"> - {selectedTemplate && isClient && isDataValid ? ( - <SpreadSheets - key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} - 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... - </> - ) : !selectedTemplate ? ( - "No template available" - ) : !isDataValid ? ( - `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available` - ) : ( - "Template not ready" - )} - </div> - )} - </div> - - <DialogFooter className="flex-shrink-0"> - <div className="flex items-center gap-2"> - <Button variant="outline" onClick={onClose}> - Close - </Button> - - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending || validationErrors.length > 0} - > - {isPending ? ( - <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... - </> - ) : ( - <> - <Save className="mr-2 h-4 w-4" /> - Save Changes - </> - )} - </Button> - )} - - {validationErrors.length > 0 && ( - <Button - variant="outline" - onClick={validateAllData} - className="text-red-600 border-red-300 hover:bg-red-50" - > - <AlertTriangle className="mr-2 h-4 w-4" /> - Check Errors ({validationErrors.length}) - </Button> - )} - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -}
\ No newline at end of file diff --git a/components/form-data/spreadJS-dialog copy 3.tsx b/components/form-data/spreadJS-dialog copy 3.tsx deleted file mode 100644 index 1ea8232b..00000000 --- a/components/form-data/spreadJS-dialog copy 3.tsx +++ /dev/null @@ -1,1916 +0,0 @@ -"use client"; - -import * as React from "react"; -import dynamic from "next/dynamic"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -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, AlertTriangle } from "lucide-react"; -import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; -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"> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading SpreadSheets... - </div> - ) - } -); - -// 라이센스 키 설정을 클라이언트에서만 실행 -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { - GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; -} - -interface TemplateItem { - TMPL_ID: string; - NAME: string; - TMPL_TYPE: string; - SPR_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; - GRD_LST_SETUP: { - REG_TYPE_ID: string; - SPR_ITM_IDS: Array<string>; - ATTS: Array<{}>; - }; - SPR_ITM_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; -} - -interface ValidationError { - cellAddress: string; - attId: string; - value: any; - expectedType: ColumnType; - message: string; -} - -interface CellMapping { - attId: string; - cellAddress: string; - isEditable: boolean; - dataRowIndex?: number; -} - -interface TemplateViewDialogProps { - isOpen: boolean; - onClose: () => void; - templateData: TemplateItem[] | any; - selectedRow?: GenericData; // SPREAD_ITEM용 - tableData?: GenericData[]; // SPREAD_LIST용 - formCode: string; - columnsJSON: DataTableColumnJSON[] - contractItemId: number; - editableFieldsMap?: Map<string, string[]>; - onUpdateSuccess?: (updatedValues: Record<string, any> | GenericData[]) => void; -} - -export function TemplateViewDialog({ - isOpen, - onClose, - templateData, - selectedRow, - tableData = [], - formCode, - contractItemId, - columnsJSON, - editableFieldsMap = new Map(), - onUpdateSuccess -}: TemplateViewDialogProps) { - 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<any>(null); - const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]); - const [isClient, setIsClient] = React.useState(false); - const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null); - const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); - const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]); - - const determineTemplateType = React.useCallback((template: TemplateItem): 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null => { - // 1. SPREAD_LIST: TMPL_TYPE이 SPREAD_LIST이고 SPR_LST_SETUP.CONTENT가 있음 - if (template.TMPL_TYPE === "SPREAD_LIST" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { - return 'SPREAD_LIST'; - } - - // 2. SPREAD_ITEM: TMPL_TYPE이 SPREAD_ITEM이고 SPR_ITM_LST_SETUP.CONTENT가 있음 - if (template.TMPL_TYPE === "SPREAD_ITEM" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { - return 'SPREAD_ITEM'; - } - - // 3. GRD_LIST: GRD_LST_SETUP이 있고 columnsJSON이 있음 (동적 테이블) - if (template.GRD_LST_SETUP && columnsJSON.length > 0) { - return 'GRD_LIST'; - } - - return null; // 유효하지 않은 템플릿 - }, [columnsJSON]); - - const isValidTemplate = React.useCallback((template: TemplateItem): boolean => { - return determineTemplateType(template) !== null; - }, [determineTemplateType]); - - - // 클라이언트 사이드에서만 렌더링되도록 보장 - React.useEffect(() => { - setIsClient(true); - }, []); - - // 사용 가능한 템플릿들을 필터링하고 설정 - React.useEffect(() => { - if (!templateData) return; - - let templates: TemplateItem[]; - if (Array.isArray(templateData)) { - templates = templateData as TemplateItem[]; - } else { - templates = [templateData as TemplateItem]; - } - - // 유효한 템플릿들만 필터링 - const validTemplates = templates.filter(isValidTemplate); - - setAvailableTemplates(validTemplates); - - // 첫 번째 유효한 템플릿을 기본으로 선택 - if (validTemplates.length > 0 && !selectedTemplateId) { - const firstTemplate = validTemplates[0]; - const templateTypeToSet = determineTemplateType(firstTemplate); - - setSelectedTemplateId(firstTemplate.TMPL_ID); - setTemplateType(templateTypeToSet); - } - }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]); - - - // 선택된 템플릿 변경 처리 - const handleTemplateChange = (templateId: string) => { - const template = availableTemplates.find(t => t.TMPL_ID === templateId); - if (template) { - const templateTypeToSet = determineTemplateType(template); - - setSelectedTemplateId(templateId); - setTemplateType(templateTypeToSet); - setHasChanges(false); - setValidationErrors([]); - - // SpreadSheets 재초기화 - if (currentSpread && template) { - initSpread(currentSpread, template); - } - } - }; - // 현재 선택된 템플릿 가져오기 - const selectedTemplate = React.useMemo(() => { - return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); - }, [availableTemplates, selectedTemplateId]); - - - // 편집 가능한 필드 목록 계산 - const editableFields = React.useMemo(() => { - // 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 또는 GRD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 - if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { - const firstRowTagNo = tableData[0]?.TAG_NO; - if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) { - return editableFieldsMap.get(firstRowTagNo) || []; - } - } - - return []; - }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); - - // 필드가 편집 가능한지 판별하는 함수 - const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { - // columnsJSON에서 해당 attId의 shi 값 확인 - const columnConfig = columnsJSON.find(col => col.key === attId); - if (columnConfig?.shi === true) { - return false; // columnsJSON에서 shi가 true이면 편집 불가 - } - - // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) - if (attId === "TAG_NO" || attId === "TAG_DESC") { - return false; - } - - if (attId === "status") { - return false; - } - - // SPREAD_ITEM인 경우: editableFields 체크 - // if (templateType === 'SPREAD_ITEM') { - // return editableFields.includes(attId); - // } - - // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려 - if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - // 기본적으로 editableFields에 포함되어야 함 - // if (!editableFields.includes(attId)) { - // return false; - // } - - // rowData가 제공된 경우 해당 행의 shi 상태도 확인 - if (rowData && rowData.shi === true) { - return false; - } - - return true; - } - - return true; - }, [templateType, columnsJSON, editableFields]); - - // 편집 가능한 필드 개수 계산 - 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) - 1; - - return { row, col }; - }; - - // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용) - const getCellAddress = (row: number, col: number): string => { - let colStr = ''; - let colNum = col; - while (colNum >= 0) { - colStr = String.fromCharCode((colNum % 26) + 65) + colStr; - colNum = Math.floor(colNum / 26) - 1; - } - return colStr + (row + 1); - }; - - // 데이터 타입 검증 함수 - const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { - if (value === undefined || value === null || value === "") { - return null; // 빈 값은 별도 required 검증에서 처리 - } - - switch (columnType) { - case "NUMBER": - if (isNaN(Number(value))) { - return "Value must be a valid number"; - } - break; - case "LIST": - if (options && !options.includes(String(value))) { - return `Value must be one of: ${options.join(", ")}`; - } - break; - case "STRING": - // STRING 타입은 대부분의 값을 허용 - break; - default: - // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 - break; - } - - return null; - }; - - // 전체 데이터 검증 함수 - const validateAllData = React.useCallback(() => { - if (!currentSpread || !selectedTemplate) return []; - - const activeSheet = currentSpread.getActiveSheet(); - const errors: ValidationError[] = []; - - cellMappings.forEach(mapping => { - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (!columnConfig) return; - - const cellPos = parseCellAddress(mapping.cellAddress); - if (!cellPos) return; - - if (templateType === 'SPREAD_ITEM') { - // 단일 행 검증 - 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 - }); - } - } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 - 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]); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🛠️ 헬퍼 함수들 - // ═══════════════════════════════════════════════════════════════════════════════ - - // 🎨 셀 스타일 생성 - 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; - }, []); - - // 🎯 드롭다운 설정 - const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { - try { - console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options); - - // ✅ options 정규화 - const safeOptions = options - .filter(opt => opt !== null && opt !== undefined && opt !== '') - .map(opt => String(opt).trim()) - .filter(opt => opt.length > 0) - .filter((opt, index, arr) => arr.indexOf(opt) === index) - .slice(0, 20); - - if (safeOptions.length === 0) { - console.warn(`⚠️ No valid options found, skipping`); - return; - } - - console.log(`📋 Safe options:`, safeOptions); - - // ✅ DataValidation용 문자열 준비 - const optionsString = safeOptions.join(','); - - // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성! - for (let i = 0; i < rowCount; i++) { - try { - const targetRow = cellPos.row + i; - - // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성 - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(safeOptions); // 배열로 전달 - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - - // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성 - const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString); - - // ComboBox + DataValidation 둘 다 적용 - activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); - activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator); - - // 셀 잠금 해제 - const cell = activeSheet.getCell(targetRow, cellPos.col); - cell.locked(false); - - console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`); - - } catch (cellError) { - console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError); - } - } - - console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); - - } catch (error) { - console.error('❌ Dropdown setup failed:', error); - } - }, []); - - // 🛡️ 안전한 시트 검증 함수 추가 -const validateActiveSheet = React.useCallback((activeSheet: any, functionName: string = 'unknown') => { - console.log(`🔍 Validating activeSheet for ${functionName}:`); - - if (!activeSheet) { - console.error(`❌ activeSheet is null/undefined in ${functionName}`); - return false; - } - - console.log(`✅ activeSheet exists (type: ${typeof activeSheet})`); - console.log(`✅ constructor: ${activeSheet.constructor?.name}`); - - // 핵심 메서드들 존재 여부 확인 - const requiredMethods = ['getRowCount', 'getColumnCount', 'setRowCount', 'setColumnCount', 'getCell', 'getValue', 'setStyle']; - const missingMethods = requiredMethods.filter(method => typeof activeSheet[method] !== 'function'); - - if (missingMethods.length > 0) { - console.error(`❌ Missing methods in ${functionName}:`, missingMethods); - console.log(`📋 Available methods:`, Object.getOwnPropertyNames(activeSheet).filter(prop => typeof activeSheet[prop] === 'function').slice(0, 20)); - return false; - } - - console.log(`✅ All required methods available for ${functionName}`); - return true; -}, []); -// 🛡️ 안전한 ActiveSheet 가져오기 함수 -const getSafeActiveSheet = React.useCallback((spread: any, functionName: string = 'unknown') => { - console.log(`🔍 Getting safe activeSheet for ${functionName}`); - - if (!spread) { - console.error(`❌ Spread is null/undefined in ${functionName}`); - return null; - } - - try { - // 현재 활성 시트 가져오기 - let activeSheet = spread.getActiveSheet(); - - if (!activeSheet) { - console.warn(`⚠️ ActiveSheet is null, attempting to get first sheet in ${functionName}`); - - // 첫 번째 시트 시도 - const sheetCount = spread.getSheetCount(); - console.log(`📊 Total sheets: ${sheetCount}`); - - if (sheetCount > 0) { - activeSheet = spread.getSheet(0); - if (activeSheet) { - spread.setActiveSheetIndex(0); - console.log(`✅ Successfully got first sheet in ${functionName}`); - } - } - } - - if (!activeSheet) { - console.error(`❌ Failed to get any valid sheet in ${functionName}`); - return null; - } - - // 시트 유효성 검증 - const validation = validateActiveSheet(activeSheet, functionName); - if (!validation) { - console.error(`❌ Sheet validation failed in ${functionName}`); - return null; - } - - console.log(`✅ Got valid activeSheet for ${functionName}: ${activeSheet.name?.() || 'unnamed'}`); - return activeSheet; - - } catch (error) { - console.error(`❌ Error getting activeSheet in ${functionName}:`, error); - return null; - } -}, [validateActiveSheet]); - -// 🛡️ 수정된 ensureRowCapacity 함수 -const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { - try { - // 🔍 상세한 null/undefined 체크 - if (!activeSheet) { - console.error('❌ activeSheet is null/undefined in ensureRowCapacity'); - return false; - } - - console.log('🔍 ActiveSheet validation in ensureRowCapacity:'); - console.log(' - Type:', typeof activeSheet); - console.log(' - Constructor:', activeSheet.constructor?.name); - console.log(' - Is null:', activeSheet === null); - console.log(' - Is undefined:', activeSheet === undefined); - - // 🔍 메서드 존재 여부 확인 - if (typeof activeSheet.getRowCount !== 'function') { - console.error('❌ getRowCount method does not exist on activeSheet'); - console.log('📋 Available properties:', Object.getOwnPropertyNames(activeSheet).slice(0, 20)); - return false; - } - - // 🔍 시트 상태 확인 - const currentRowCount = activeSheet.getRowCount(); - console.log(`📊 Current row count: ${currentRowCount} (type: ${typeof currentRowCount})`); - - if (typeof currentRowCount !== 'number' || isNaN(currentRowCount)) { - console.error('❌ getRowCount returned invalid value:', currentRowCount); - return false; - } - - if (requiredRowCount > currentRowCount) { - // 🔍 setRowCount 메서드 확인 - if (typeof activeSheet.setRowCount !== 'function') { - console.error('❌ setRowCount method does not exist on activeSheet'); - return false; - } - - const newRowCount = requiredRowCount + 10; - activeSheet.setRowCount(newRowCount); - console.log(`📈 Expanded sheet: ${currentRowCount} → ${newRowCount} rows`); - - // 🔍 설정 후 검증 - const verifyRowCount = activeSheet.getRowCount(); - console.log(`✅ Verified new row count: ${verifyRowCount}`); - - return true; - } else { - console.log(`✅ Sheet already has sufficient rows: ${currentRowCount} >= ${requiredRowCount}`); - return true; - } - - } catch (error) { - console.error('❌ Error in ensureRowCapacity:', error); - console.error('❌ Error stack:', error.stack); - return false; - } -}, []); - -// 🛡️ 안전한 컬럼 용량 확보 함수 -const ensureColumnCapacity = React.useCallback((activeSheet: any, requiredColumnCount: number) => { - try { - // 🔍 상세한 null/undefined 체크 - if (!activeSheet) { - console.error('❌ activeSheet is null/undefined in ensureColumnCapacity'); - return false; - } - - console.log('🔍 ActiveSheet validation in ensureColumnCapacity:'); - console.log(' - Type:', typeof activeSheet); - console.log(' - Constructor:', activeSheet.constructor?.name); - console.log(' - Is null:', activeSheet === null); - console.log(' - Is undefined:', activeSheet === undefined); - - // 🔍 메서드 존재 여부 확인 - if (typeof activeSheet.getColumnCount !== 'function') { - console.error('❌ getColumnCount method does not exist on activeSheet'); - console.log('📋 Available properties:', Object.getOwnPropertyNames(activeSheet).slice(0, 20)); - return false; - } - - const currentColumnCount = activeSheet.getColumnCount(); - console.log(`📊 Current column count: ${currentColumnCount} (type: ${typeof currentColumnCount})`); - - if (typeof currentColumnCount !== 'number' || isNaN(currentColumnCount)) { - console.error('❌ getColumnCount returned invalid value:', currentColumnCount); - return false; - } - - if (requiredColumnCount > currentColumnCount) { - if (typeof activeSheet.setColumnCount !== 'function') { - console.error('❌ setColumnCount method does not exist on activeSheet'); - return false; - } - - const newColumnCount = requiredColumnCount + 10; - activeSheet.setColumnCount(newColumnCount); - console.log(`📈 Expanded columns: ${currentColumnCount} → ${newColumnCount}`); - - // 🔍 설정 후 검증 - const verifyColumnCount = activeSheet.getColumnCount(); - console.log(`✅ Verified new column count: ${verifyColumnCount}`); - - return true; - } else { - console.log(`✅ Sheet already has sufficient columns: ${currentColumnCount} >= ${requiredColumnCount}`); - return true; - } - - } catch (error) { - console.error('❌ Error in ensureColumnCapacity:', error); - console.error('❌ Error stack:', error.stack); - return false; - } -}, []); - - -// 🎯 텍스트 너비 계산 함수들 (createGrdListTable 함수 위에 추가) -const measureTextWidth = React.useCallback((text: string, fontSize: number = 12, fontFamily: string = 'Arial'): number => { - // Canvas를 사용한 정확한 텍스트 너비 측정 - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - if (!context) return text.length * 8; // fallback - - context.font = `${fontSize}px ${fontFamily}`; - const metrics = context.measureText(text || ''); - return Math.ceil(metrics.width); -}, []); - -const calculateColumnWidth = React.useCallback(( - headerText: string, - dataValues: any[] = [], - minWidth: number = 80, - maxWidth: number = 300, - padding: number = 20 -): number => { - // 헤더 텍스트 너비 계산 - const headerWidth = measureTextWidth(headerText, 12, 'Arial'); - - // 데이터 값들의 최대 너비 계산 - let maxDataWidth = 0; - if (dataValues.length > 0) { - maxDataWidth = Math.max( - ...dataValues - .slice(0, 10) // 성능을 위해 처음 10개만 샘플링 - .map(value => measureTextWidth(String(value || ''), 11, 'Arial')) - ); - } - - // 헤더와 데이터 중 더 큰 너비 + 패딩 적용 - const calculatedWidth = Math.max(headerWidth, maxDataWidth) + padding; - - // 최소/최대 너비 제한 적용 - return Math.min(Math.max(calculatedWidth, minWidth), maxWidth); -}, [measureTextWidth]); - -const setOptimalColumnWidths = React.useCallback((activeSheet: any, columns: any[], startCol: number, tableData: any[]) => { - console.log('🎨 Setting optimal column widths...'); - - columns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - - // 해당 컬럼의 데이터 값들 추출 - const dataValues = tableData.map(row => row[column.key]).filter(val => val != null); - - // 최적 너비 계산 - const optimalWidth = calculateColumnWidth( - column.label || column.key, - dataValues, - column.type === 'NUMBER' ? 100 : 80, // 숫자는 좀 더 넓게 - column.type === 'STRING' ? 250 : 200, // 문자열은 더 넓게 - column.type === 'LIST' ? 30 : 20 // 드롭다운은 여유 패딩 - ); - - // 컬럼 너비 설정 - activeSheet.setColumnWidth(targetCol, optimalWidth); - - console.log(`📏 Column ${targetCol} (${column.key}): width set to ${optimalWidth}px`); - }); -}, [calculateColumnWidth]); - - // 🔍 컬럼 그룹 분석 함수 - const analyzeColumnGroups = React.useCallback((columns: DataTableColumnJSON[]) => { - const groups: Array<{ - head: string; - isGroup: boolean; - columns: DataTableColumnJSON[]; - }> = []; - - let i = 0; - while (i < columns.length) { - const currentCol = columns[i]; - - // head가 없거나 빈 문자열인 경우 단일 컬럼으로 처리 - if (!currentCol.head || !currentCol.head.trim()) { - groups.push({ - head: '', - isGroup: false, - columns: [currentCol] - }); - i++; - continue; - } - - // 같은 head를 가진 연속된 컬럼들을 찾기 - const groupHead = currentCol.head.trim(); - const groupColumns: DataTableColumnJSON[] = [currentCol]; - let j = i + 1; - - while (j < columns.length && columns[j].head && columns[j].head.trim() === groupHead) { - groupColumns.push(columns[j]); - j++; - } - - // 그룹 추가 - groups.push({ - head: groupHead, - isGroup: groupColumns.length > 1, - columns: groupColumns - }); - - i = j; // 다음 그룹으로 이동 - } - - return { groups }; - }, []); - - -// 🆕 수정된 createGrdListTable 함수 -// 🆕 개선된 GRD_LIST용 동적 테이블 생성 함수 -const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => { - console.log('🏗️ Creating GRD_LIST table'); - - // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용 - const visibleColumns = columnsJSON - .filter(col => col.hidden !== true) - .sort((a, b) => { - const seqA = a.seq !== undefined ? a.seq : 999999; - const seqB = b.seq !== undefined ? b.seq : 999999; - return seqA - seqB; - }); - - console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`)); - console.log(`📊 Total visible columns: ${visibleColumns.length}`); - - if (visibleColumns.length === 0) { - console.warn('❌ No visible columns found in columnsJSON'); - return []; - } - - // ⭐ 컬럼 용량 확보 - const startCol = 1; - const requiredColumnCount = startCol + visibleColumns.length; - ensureColumnCapacity(activeSheet, requiredColumnCount); - - // 테이블 생성 시작 - const mappings: CellMapping[] = []; - - // 🔍 그룹 헤더 분석 - const groupInfo = analyzeColumnGroups(visibleColumns); - const hasGroups = groupInfo.groups.length > 0; - - // 헤더 행 계산: 그룹이 있으면 2행, 없으면 1행 - const groupHeaderRow = 0; - const columnHeaderRow = hasGroups ? 1 : 0; - const dataStartRow = hasGroups ? 2 : 1; - - // 🎨 헤더 스타일 생성 - const groupHeaderStyle = new GC.Spread.Sheets.Style(); - groupHeaderStyle.backColor = "#1e40af"; - groupHeaderStyle.foreColor = "#ffffff"; - groupHeaderStyle.font = "bold 13px Arial"; - groupHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; - groupHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; - - const columnHeaderStyle = new GC.Spread.Sheets.Style(); - columnHeaderStyle.backColor = "#3b82f6"; - columnHeaderStyle.foreColor = "#ffffff"; - columnHeaderStyle.font = "bold 12px Arial"; - columnHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; - columnHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; - - let currentCol = startCol; - - // 🏗️ 그룹 헤더 및 컬럼 헤더 생성 - if (hasGroups) { - // 그룹 헤더가 있는 경우 - groupInfo.groups.forEach(group => { - if (group.isGroup) { - // 그룹 헤더 생성 및 병합 - const groupStartCol = currentCol; - const groupEndCol = currentCol + group.columns.length - 1; - - // 그룹 헤더 셀 설정 - const groupHeaderCell = activeSheet.getCell(groupHeaderRow, groupStartCol); - groupHeaderCell.value(group.head); - - // 그룹 헤더 병합 - if (group.columns.length > 1) { - activeSheet.addSpan(groupHeaderRow, groupStartCol, 1, group.columns.length); - } - - // 그룹 헤더 스타일 적용 - for (let col = groupStartCol; col <= groupEndCol; col++) { - activeSheet.setStyle(groupHeaderRow, col, groupHeaderStyle); - activeSheet.getCell(groupHeaderRow, col).locked(true); - } - - console.log(`📝 Group Header [${groupHeaderRow}, ${groupStartCol}-${groupEndCol}]: ${group.head}`); - - // 그룹 내 개별 컬럼 헤더 생성 - group.columns.forEach((column, index) => { - const colIndex = groupStartCol + index; - const columnHeaderCell = activeSheet.getCell(columnHeaderRow, colIndex); - columnHeaderCell.value(column.label); - activeSheet.setStyle(columnHeaderRow, colIndex, columnHeaderStyle); - columnHeaderCell.locked(true); - - console.log(`📝 Column Header [${columnHeaderRow}, ${colIndex}]: ${column.label}`); - }); - - currentCol += group.columns.length; - } else { - // 그룹이 아닌 단일 컬럼 - const column = group.columns[0]; - - // 그룹 헤더 행에는 빈 셀 - const groupHeaderCell = activeSheet.getCell(groupHeaderRow, currentCol); - groupHeaderCell.value(""); - activeSheet.setStyle(groupHeaderRow, currentCol, groupHeaderStyle); - groupHeaderCell.locked(true); - - // 컬럼 헤더 생성 - const columnHeaderCell = activeSheet.getCell(columnHeaderRow, currentCol); - columnHeaderCell.value(column.label); - activeSheet.setStyle(columnHeaderRow, currentCol, columnHeaderStyle); - columnHeaderCell.locked(true); - - console.log(`📝 Single Column [${columnHeaderRow}, ${currentCol}]: ${column.label}`); - currentCol++; - } - }); - } else { - // 그룹이 없는 경우 - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - const columnConfig = columnsJSON.find(col => col.key === column.key); - - // 📋 각 행마다 개별 셀 설정 - tableData.forEach((rowData, rowIndex) => { - const targetRow = dataStartRow + rowIndex; - const cell = activeSheet.getCell(targetRow, targetCol); - const value = rowData[column.key]; - const cellEditable = isFieldEditable(column.key, rowData); - - // 🔧 새로 추가: 셀 타입 및 편집기 설정 - if (columnConfig) { - setupCellTypeAndEditor(activeSheet, { row: targetRow, col: targetCol }, columnConfig, cellEditable, 1); - } - - // 값 설정 - cell.value(value ?? null); - - // 스타일 설정 - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, targetCol, style); - - // 개별 매핑 추가 - mappings.push({ - attId: column.key, - cellAddress: getCellAddress(targetRow, targetCol), - isEditable: cellEditable, - dataRowIndex: rowIndex - }); - }); - }); - } - - // 🔄 데이터 행 및 매핑 생성 (SPREAD_LIST 방식과 동일한 로직) - const dataRowCount = tableData.length; - ensureRowCapacity(activeSheet, dataStartRow + dataRowCount); - - // 📋 각 컬럼별로 매핑 생성 (SPREAD_LIST와 동일한 방식) - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - - console.log(`🔄 Processing column ${column.key} with ${dataRowCount} rows`); - - // 📋 각 행마다 개별 매핑 생성 (SPREAD_LIST와 동일) - tableData.forEach((rowData, rowIndex) => { - const targetRow = dataStartRow + rowIndex; - const cellAddress = getCellAddress(targetRow, targetCol); - - // 🛡️ readonly 체크 (SPREAD_LIST와 동일한 로직) - const cellEditable = isFieldEditable(column.key, rowData); - - // 개별 매핑 추가 - mappings.push({ - attId: column.key, - cellAddress: cellAddress, - isEditable: cellEditable, - dataRowIndex: rowIndex - }); - - console.log(`📝 Mapping ${column.key} Row ${rowIndex}: ${cellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); - }); - - // 📋 LIST 타입 드롭다운 설정 (편집 가능한 행이 있는 경우만) - if (column.type === "LIST" && column.options) { - const hasEditableRows = tableData.some((rowData) => isFieldEditable(column.key, rowData)); - if (hasEditableRows) { - const cellPos = { row: dataStartRow, col: targetCol }; - setupOptimizedListValidation(activeSheet, cellPos, column.options, dataRowCount); - console.log(`📋 Dropdown set for ${column.key}: ${hasEditableRows ? 'Has editable rows' : 'All readonly'}`); - } - } - }); - - // 🎨 개별 셀 데이터 및 스타일 설정 (SPREAD_LIST와 동일한 방식) - tableData.forEach((rowData, rowIndex) => { - const targetRow = dataStartRow + rowIndex; - - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - const cell = activeSheet.getCell(targetRow, targetCol); - const value = rowData[column.key]; - - // 값 설정 - cell.value(value ?? null); - - // 🛡️ 편집 권한 및 스타일 재확인 (SPREAD_LIST와 동일) - const cellEditable = isFieldEditable(column.key, rowData); - cell.locked(!cellEditable); - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, targetCol, style); - - // 🔍 디버깅: readonly 상태 로깅 - if (!cellEditable) { - const columnConfig = columnsJSON.find(col => col.key === column.key); - const reasons = []; - - if (columnConfig?.shi === true) { - reasons.push('column.shi=true'); - } - if (rowData.shi === true) { - reasons.push('row.shi=true'); - } - if (!editableFields.includes(column.key) && column.key !== "TAG_NO" && column.key !== "TAG_DESC") { - reasons.push('not in editableFields'); - } - - console.log(`🔒 ReadOnly [${targetRow}, ${targetCol}] ${column.key}: ${reasons.join(', ')}`); - } - }); - }); - - // 🎨 컬럼 너비 자동 설정 - setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData); - - console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`); - console.log(`📊 Readonly analysis:`); - console.log(` Total cells: ${mappings.length}`); - console.log(` Editable cells: ${mappings.filter(m => m.isEditable).length}`); - console.log(` Readonly cells: ${mappings.filter(m => !m.isEditable).length}`); - - return mappings; -}, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, ensureColumnCapacity, setupOptimizedListValidation, setOptimalColumnWidths, editableFields, getCellAddress, analyzeColumnGroups]); - -// 🛡️ 추가: readonly 상태 확인 헬퍼 함수 -const analyzeReadonlyStatus = React.useCallback((column: DataTableColumnJSON, rowData: GenericData) => { - const reasons: string[] = []; - - // 1. 컬럼 자체가 readonly인지 확인 - if (column.shi === true) { - reasons.push('Column marked as readonly (shi=true)'); - } - - // 2. 행 자체가 readonly인지 확인 - if (rowData.shi === true) { - reasons.push('Row marked as readonly (shi=true)'); - } - - // 3. editableFields에 포함되지 않은 경우 - if (!editableFields.includes(column.key) && column.key !== "TAG_NO" && column.key !== "TAG_DESC") { - reasons.push('Not in editable fields list'); - } - - // 4. 특수 필드 체크 - if (column.key === "TAG_NO" || column.key === "TAG_DESC") { - // TAG_NO와 TAG_DESC는 기본 편집 가능하지만 다른 조건들은 적용됨 - if (column.shi === true || rowData.shi === true) { - // 다른 readonly 조건이 있으면 적용 - } else { - return { isEditable: true, reasons: ['Default editable field'] }; - } - } - - const isEditable = reasons.length === 0; - - return { - isEditable, - reasons: isEditable ? ['Editable'] : reasons - }; -}, [editableFields]); - - - -// 🛡️ 수정된 시트 보호 및 이벤트 설정 함수 -const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { - console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); - - // 🔧 1단계: 먼저 시트 보호를 완전히 해제하고 강력한 잠금 해제 실행 - console.log('🔓 Step 1: Forcing unlock all editable cells...'); - activeSheet.options.isProtected = false; - - // 🔧 2단계: 모든 편집 가능한 셀에 대해 강제 잠금 해제 및 CellType 설정 - mappings.forEach((mapping, index) => { - if (!mapping.isEditable) return; - - const cellPos = parseCellAddress(mapping.cellAddress); - if (!cellPos) return; - - try { - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - - // 강제 잠금 해제 - cell.locked(false); - - // CellType 명시적 설정 - if (columnConfig?.type === "LIST" && columnConfig.options) { - // LIST 타입: ComboBox 설정 - const comboBox = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBox.items(columnConfig.options); - comboBox.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - activeSheet.setCellType(cellPos.row, cellPos.col, comboBox); - console.log(`📋 ComboBox set for ${mapping.attId} at ${mapping.cellAddress}`); - } else { - // 다른 모든 타입: 기본 텍스트 편집기 설정 - const textCellType = new GC.Spread.Sheets.CellTypes.Text(); - activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); - console.log(`📝 Text editor set for ${mapping.attId} at ${mapping.cellAddress}`); - - // NUMBER 타입인 경우에만 validation 추가 (편집은 가능하게) - if (columnConfig?.type === "NUMBER") { - const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( - GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, - -999999999, 999999999, true - ); - numberValidator.showInputMessage(false); - numberValidator.showErrorMessage(false); - activeSheet.setDataValidator(cellPos.row, cellPos.col, numberValidator); - } - } - - // 편집 가능 스타일 명확히 표시 - const editableStyle = createCellStyle(true); - activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle); - - console.log(`🔓 Forced unlock: ${mapping.attId} at ${mapping.cellAddress}`); - - } catch (error) { - console.error(`❌ Error processing cell ${mapping.cellAddress}:`, error); - } - }); - - // 🔧 3단계: 시트 보호 재설정 (편집 허용하는 설정으로) - activeSheet.options.isProtected = true; - activeSheet.options.protectionOptions = { - allowSelectLockedCells: true, - allowSelectUnlockedCells: true, - allowSort: false, - allowFilter: false, - allowEditObjects: true, // ✅ 편집 객체 허용 - allowResizeRows: false, - allowResizeColumns: false, - allowFormatCells: false, - allowInsertRows: false, - allowInsertColumns: false, - allowDeleteRows: false, - allowDeleteColumns: false - }; - - // 🔧 4단계: 편집 테스트 실행 - console.log('🧪 Testing cell editability...'); - const editableMapping = mappings.find(m => m.isEditable); - if (editableMapping) { - const cellPos = parseCellAddress(editableMapping.cellAddress); - if (cellPos) { - try { - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - const testValue = 'TEST_' + Math.random().toString(36).substr(2, 5); - const originalValue = cell.value(); - - console.log(`🧪 Testing ${editableMapping.attId} at ${editableMapping.cellAddress}`); - console.log(`🧪 Locked status: ${cell.locked()}`); - - // 직접 값 설정 테스트 - cell.value(testValue); - const newValue = cell.value(); - - if (newValue === testValue) { - console.log('✅ Cell edit test PASSED'); - cell.value(originalValue); // 원래 값 복원 - } else { - console.log(`❌ Cell edit test FAILED: ${newValue} !== ${testValue}`); - } - } catch (testError) { - console.error('❌ Edit test error:', testError); - } - } - } - - // 🎯 변경 감지 이벤트 - 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}, isEditable: ${exactMapping.isEditable}`); - - // 🔍 추가 디버깅: 셀의 실제 상태 확인 - const cell = activeSheet.getCell(info.row, info.col); - const isLocked = cell.locked(); - const cellValue = cell.value(); - - console.log(`🔍 Cell state check:`, { - attId: exactMapping.attId, - isEditable: exactMapping.isEditable, - isLocked: isLocked, - currentValue: cellValue - }); - - // 🔧 추가: EditStarting 시점에서도 강제 잠금 해제 재시도 - if (exactMapping.isEditable && isLocked) { - console.log(`🔓 Re-unlocking cell during EditStarting...`); - cell.locked(false); - - // CellType도 재설정 - const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); - if (columnConfig?.type !== "LIST") { - const textCellType = new GC.Spread.Sheets.CellTypes.Text(); - activeSheet.setCellType(info.row, info.col, textCellType); - } - } - - // 기본 편집 권한 확인 - 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 또는 GRD_LIST 개별 행 SHI 확인 - if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_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}, New value: ${activeSheet.getValue(info.row, 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); - } - } - - // 🔄 변경 상태 업데이트 - setHasChanges(true); - }); - - // 🔧 5단계: 설정 완료 후 1초 뒤에 추가 잠금 해제 실행 (안전장치) - setTimeout(() => { - console.log('🔄 Running safety unlock after 1 second...'); - mappings.forEach(mapping => { - if (!mapping.isEditable) return; - - const cellPos = parseCellAddress(mapping.cellAddress); - if (!cellPos) return; - - try { - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - if (cell.locked()) { - console.log(`🔓 Safety unlock: ${mapping.attId}`); - cell.locked(false); - } - } catch (error) { - console.error(`❌ Safety unlock error for ${mapping.cellAddress}:`, error); - } - }); - }, 1000); - - console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); - console.log(`🔓 Editable cells: ${mappings.filter(m => m.isEditable).length}`); - console.log(`🔒 Readonly cells: ${mappings.filter(m => !m.isEditable).length}`); -}, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); - -// 🔧 셀 타입 및 편집기 설정 함수 (initSpread 함수 내부에 추가) -const setupCellTypeAndEditor = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, columnConfig: DataTableColumnJSON, isEditable: boolean, rowCount: number = 1) => { - console.log(`🔧 Setting up cell type for ${columnConfig.key} (${columnConfig.type}) at [${cellPos.row}, ${cellPos.col}]`); - - try { - // 편집 가능한 셀에만 적절한 셀 타입 설정 - if (isEditable) { - for (let i = 0; i < rowCount; i++) { - const targetRow = cellPos.row + i; - const cell = activeSheet.getCell(targetRow, cellPos.col); - - // 셀 잠금 해제 - cell.locked(false); - - switch (columnConfig.type) { - case "LIST": - // 드롭다운은 기존 setupOptimizedListValidation 함수에서 처리 - break; - - case "NUMBER": - // 숫자 입력용 셀 타입 설정 - const numberCellType = new GC.Spread.Sheets.CellTypes.Text(); - activeSheet.setCellType(targetRow, cellPos.col, numberCellType); - - // 숫자 validation 설정 (선택사항) - const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( - GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, - -999999999, 999999999, true - ); - numberValidator.showInputMessage(true); - numberValidator.inputTitle("Number Input"); - numberValidator.inputMessage("Please enter a valid number"); - activeSheet.setDataValidator(targetRow, cellPos.col, numberValidator); - break; - - case "STRING": - default: - // 기본 텍스트 입력용 셀 타입 설정 - const textCellType = new GC.Spread.Sheets.CellTypes.Text(); - activeSheet.setCellType(targetRow, cellPos.col, textCellType); - break; - } - - console.log(`✅ Cell type set for [${targetRow}, ${cellPos.col}]: ${columnConfig.type}`); - } - } else { - // 읽기 전용 셀 설정 - for (let i = 0; i < rowCount; i++) { - const targetRow = cellPos.row + i; - const cell = activeSheet.getCell(targetRow, cellPos.col); - cell.locked(true); - } - } - - } catch (error) { - console.error(`❌ Error setting cell type for ${columnConfig.key}:`, error); - } -}, []); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 🏗️ 메인 SpreadSheets 초기화 함수 - // ═══════════════════════════════════════════════════════════════════════════════ - -// 🛡️ 수정된 initSpread 함수 - activeSheet 참조 문제 해결 -const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { - const workingTemplate = template || selectedTemplate; - if (!spread || !workingTemplate) { - console.error('❌ Invalid spread or template in initSpread'); - return; - } - - try { - // 🔄 초기 설정 - setCurrentSpread(spread); - setHasChanges(false); - setValidationErrors([]); - - // 성능을 위한 렌더링 일시 중단 - spread.suspendPaint(); - - try { - // ⚠️ 초기 activeSheet 가져오기 - let activeSheet = getSafeActiveSheet(spread, 'initSpread-initial'); - if (!activeSheet) { - throw new Error('Failed to get initial activeSheet'); - } - - // 시트 보호 해제 (편집을 위해) - activeSheet.options.isProtected = false; - - let mappings: CellMapping[] = []; - - // 🆕 GRD_LIST 처리 - if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) { - console.log('🏗️ Processing GRD_LIST template'); - - // 기본 워크북 설정 - spread.clearSheets(); - spread.addSheet(0); - const sheet = spread.getSheet(0); - sheet.name('Data'); - spread.setActiveSheet('Data'); - - // 동적 테이블 생성 - mappings = createGrdListTable(sheet, workingTemplate); - - } else { - // 🔍 SPREAD_LIST 및 SPREAD_ITEM 처리 - let contentJson = null; - let dataSheets = null; - - // 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 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 for template:', workingTemplate.NAME); - } - - if (!contentJson) { - throw new Error(`No template content found for ${workingTemplate.NAME}`); - } - - if (!dataSheets || dataSheets.length === 0) { - throw new Error(`No data sheets configuration found for ${workingTemplate.NAME}`); - } - - console.log('🔍 Template info:', { - templateName: workingTemplate.NAME, - templateType: templateType, - dataSheetsCount: dataSheets.length, - hasSelectedRow: !!selectedRow, - tableDataLength: tableData.length - }); - - // 🏗️ SpreadSheets 템플릿 로드 - const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - - console.log('📥 Loading template JSON...'); - spread.fromJSON(jsonData); - console.log('✅ Template JSON loaded'); - - // ⚠️ 중요: 템플릿 로드 후 activeSheet 다시 가져오기 - activeSheet = getSafeActiveSheet(spread, 'initSpread-after-fromJSON'); - if (!activeSheet) { - throw new Error('ActiveSheet became null after loading template'); - } - - console.log('🔍 Active sheet after template load:', { - name: activeSheet.name?.() || 'unnamed', - rowCount: activeSheet.getRowCount(), - colCount: activeSheet.getColumnCount() - }); - - // 시트 보호 다시 해제 (템플릿 로드 후 다시 설정될 수 있음) - activeSheet.options.isProtected = false; - - // 📊 데이터 매핑 및 로딩 처리 - console.log(`🔄 Processing ${dataSheets.length} data sheets`); - - dataSheets.forEach((dataSheet, sheetIndex) => { - console.log(`📋 Processing data sheet ${sheetIndex}:`, { - sheetName: dataSheet.SHEET_NAME, - mappingCount: dataSheet.MAP_CELL_ATT?.length || 0 - }); - - if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) { - dataSheet.MAP_CELL_ATT.forEach((mapping, mappingIndex) => { - const { ATT_ID, IN } = mapping; - - if (!ATT_ID || !IN || IN.trim() === "") { - console.warn(`⚠️ Invalid mapping: ATT_ID=${ATT_ID}, IN=${IN}`); - return; - } - - const cellPos = parseCellAddress(IN); - if (!cellPos) { - console.warn(`⚠️ Invalid cell address: ${IN}`); - return; - } - - const columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - // 🎯 템플릿 타입별 데이터 처리 - if (templateType === 'SPREAD_ITEM' && selectedRow) { - console.log(`📝 Processing SPREAD_ITEM for ${ATT_ID}`); - - const isEditable = isFieldEditable(ATT_ID); - const value = selectedRow[ATT_ID]; - - // 매핑 정보 저장 - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable, - dataRowIndex: 0 - }); - - // ⚠️ 안전한 셀 참조 및 값 설정 - try { - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - console.log(`🔄 Setting SPREAD_ITEM cell [${cellPos.row}, ${cellPos.col}] ${ATT_ID}: "${value}"`); - - // 🔧 새로 추가: 셀 타입 및 편집기 설정 - setupCellTypeAndEditor(activeSheet, cellPos, columnConfig, isEditable, 1); - - // 값 설정 - cell.value(value ?? null); - - // 스타일 설정 - const style = createCellStyle(isEditable); - activeSheet.setStyle(cellPos.row, cellPos.col, style); - - // LIST 타입 드롭다운 설정 (기존 코드 유지) - if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); - } - - console.log(`✅ SPREAD_ITEM cell set successfully`); - } catch (cellError) { - console.error(`❌ Error setting SPREAD_ITEM cell:`, cellError); - } - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - console.log(`📊 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - - // 🚀 행 확장 - 안전한 방법으로 - const requiredRows = cellPos.row + tableData.length; - console.log(`🚀 Ensuring ${requiredRows} rows for SPREAD_LIST`); - - // ⚠️ activeSheet 유효성 재검증 - const currentActiveSheet = getSafeActiveSheet(spread, 'ensureRowCapacity'); - if (!currentActiveSheet) { - console.error(`❌ ActiveSheet is null before ensureRowCapacity`); - return; - } - - if (!ensureRowCapacity(currentActiveSheet, requiredRows)) { - console.error(`❌ Failed to ensure row capacity for ${requiredRows} rows`); - return; - } - - // activeSheet 참조 업데이트 - activeSheet = currentActiveSheet; - - // 매핑 생성 - tableData.forEach((rowData, index) => { - const targetRow = cellPos.row + index; - const targetCellAddress = getCellAddress(targetRow, cellPos.col); - const cellEditable = isFieldEditable(ATT_ID, rowData); - - mappings.push({ - attId: ATT_ID, - cellAddress: targetCellAddress, - isEditable: cellEditable, - dataRowIndex: index - }); - }); - - // 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; - - try { - const cell = activeSheet.getCell(targetRow, cellPos.col); - const value = rowData[ATT_ID]; - const cellEditable = isFieldEditable(ATT_ID, rowData); - - console.log(`🔄 Setting SPREAD_LIST Row ${index} ${ATT_ID}: "${value}"`); - - // 🔧 새로 추가: 각 셀에 대한 타입 및 편집기 설정 - setupCellTypeAndEditor(activeSheet, { row: targetRow, col: cellPos.col }, columnConfig, cellEditable, 1); - - // 값 설정 - cell.value(value ?? null); - - // 스타일 설정 - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, cellPos.col, style); - - } catch (cellError) { - console.error(`❌ Error setting SPREAD_LIST cell Row ${index}:`, cellError); - } -}); - - - console.log(`✅ SPREAD_LIST processing completed for ${ATT_ID}`); - } - }); - } - }); - } - - // 💾 매핑 정보 저장 및 이벤트 설정 - setCellMappings(mappings); - - // ⚠️ 최종 activeSheet 재확인 후 이벤트 설정 - const finalActiveSheet = getSafeActiveSheet(spread, 'setupSheetProtectionAndEvents'); - if (finalActiveSheet) { - setupSheetProtectionAndEvents(finalActiveSheet, mappings); - } else { - console.error('❌ Failed to get activeSheet for events setup'); - } - - console.log(`✅ Template initialization completed with ${mappings.length} mappings`); - - } finally { - // 렌더링 재개 - spread.resumePaint(); - } - - } catch (error) { - console.error('❌ Error initializing spread:', error); - // toast.error(`Failed to load template: ${error.message}`); - if (spread?.resumePaint) { - spread.resumePaint(); - } - } -}, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable, getCellAddress, getSafeActiveSheet, validateActiveSheet]); - // 변경사항 저장 함수 - const handleSaveChanges = React.useCallback(async () => { - if (!currentSpread || !hasChanges) { - toast.info("No changes to save"); - return; - } - - // 저장 전 데이터 검증 - const errors = validateAllData(); - if (errors.length > 0) { - toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`); - return; - } - - try { - setIsPending(true); - - const activeSheet = currentSpread.getActiveSheet(); - - if (templateType === 'SPREAD_ITEM' && 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; - } - - toast.success("Changes saved successfully!"); - onUpdateSuccess?.(dataToSave); - - } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { - // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리) - 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.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; - } - } - } - } - }); - - // 변경사항이 있는 행만 저장 - if (hasRowChanges) { - dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록 - - 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"); - } - } - - setHasChanges(false); - setValidationErrors([]); - - } catch (error) { - console.error("Error saving changes:", error); - toast.error("An unexpected error occurred while saving"); - } finally { - setIsPending(false); - } - }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData]); - - if (!isOpen) return null; - - // 데이터 유효성 검사 - const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; - const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; - - - return ( - <Dialog open={isOpen} onOpenChange={onClose}> - <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> - <DialogDescription> - <div className="space-y-3"> - {/* 템플릿 선택 */} - {availableTemplates.length > 1 && ( - <div className="flex items-center gap-4"> - <span className="text-sm font-medium">Template:</span> - <Select value={selectedTemplateId} onValueChange={handleTemplateChange}> - <SelectTrigger className="w-64"> - <SelectValue placeholder="Select a template" /> - </SelectTrigger> - <SelectContent> - {availableTemplates.map(template => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - {template.NAME} ({ - template.TMPL_TYPE - }) - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - )} - - {/* 템플릿 정보 */} - {selectedTemplate && ( - <div className="flex items-center gap-4 text-sm"> - <span className="font-medium text-blue-600"> - Template Type: { - templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : - templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : - 'Grid List View (GRD_LIST)' - } - </span> - {templateType === 'SPREAD_ITEM' && selectedRow && ( - <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span> - )} - {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && ( - <span>• {dataCount} rows</span> - )} - {hasChanges && ( - <span className="text-orange-600 font-medium"> - • Unsaved changes - </span> - )} - {validationErrors.length > 0 && ( - <span className="text-red-600 font-medium flex items-center"> - <AlertTriangle className="w-4 h-4 mr-1" /> - {validationErrors.length} validation errors - </span> - )} - </div> - )} - - {/* 범례 */} - <div className="flex items-center gap-4 text-xs"> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1"></span> - Editable fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1"></span> - Read-only fields - </span> - <span className="text-muted-foreground"> - <span className="inline-block w-3 h-3 bg-red-100 border border-red-300 mr-1"></span> - Validation errors - </span> - {cellMappings.length > 0 && ( - <span className="text-blue-600"> - {editableFieldsCount} of {cellMappings.length} fields editable - </span> - )} - </div> - </div> - </DialogDescription> - </DialogHeader> - - {/* SpreadSheets 컴포넌트 영역 */} - <div className="flex-1 overflow-hidden"> - {selectedTemplate && isClient && isDataValid ? ( - <SpreadSheets - key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} - 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... - </> - ) : !selectedTemplate ? ( - "No template available" - ) : !isDataValid ? ( - `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available` - ) : ( - "Template not ready" - )} - </div> - )} - </div> - - <DialogFooter className="flex-shrink-0"> - <div className="flex items-center gap-2"> - <Button variant="outline" onClick={onClose}> - Close - </Button> - - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending || validationErrors.length > 0} - > - {isPending ? ( - <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... - </> - ) : ( - <> - <Save className="mr-2 h-4 w-4" /> - Save Changes - </> - )} - </Button> - )} - - {validationErrors.length > 0 && ( - <Button - variant="outline" - onClick={validateAllData} - className="text-red-600 border-red-300 hover:bg-red-50" - > - <AlertTriangle className="mr-2 h-4 w-4" /> - Check Errors ({validationErrors.length}) - </Button> - )} - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -}
\ No newline at end of file diff --git a/components/form-data/spreadJS-dialog copy 4.tsx b/components/form-data/spreadJS-dialog copy 5.tsx index 14f4d3ea..fbeceaf3 100644 --- a/components/form-data/spreadJS-dialog copy 4.tsx +++ b/components/form-data/spreadJS-dialog copy 5.tsx @@ -12,6 +12,7 @@ import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save, AlertTriangle } from "lucide-react"; import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns"; +import { setupSpreadJSLicense } from "@/lib/spread-js/license-utils"; const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), @@ -26,8 +27,9 @@ const SpreadSheets = dynamic( } ); -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { - GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; +// 도메인별 라이선스 설정 +if (typeof window !== 'undefined') { + setupSpreadJSLicense(GC); } interface TemplateItem { diff --git a/components/form-data/spreadJS-dialog copy.tsx b/components/form-data/spreadJS-dialog copy.tsx deleted file mode 100644 index 5a51c2b5..00000000 --- a/components/form-data/spreadJS-dialog copy.tsx +++ /dev/null @@ -1,539 +0,0 @@ -"use client"; - -import * as React from "react"; -import dynamic from "next/dynamic"; -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'; - -// 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"> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Loading SpreadSheets... - </div> - ) - } -); - -// 라이센스 키 설정을 클라이언트에서만 실행 -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { - GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; -} - -interface TemplateItem { - TMPL_ID: string; - NAME: string; - TMPL_TYPE: string; - SPR_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; - GRD_LST_SETUP: { - REG_TYPE_ID: string; - SPR_ITM_IDS: Array<string>; - ATTS: Array<{}>; - }; - SPR_ITM_LST_SETUP: { - ACT_SHEET: string; - HIDN_SHEETS: Array<string>; - CONTENT?: string; - DATA_SHEETS: Array<{ - SHEET_NAME: string; - REG_TYPE_ID: string; - MAP_CELL_ATT: Array<{ - ATT_ID: string; - IN: string; - }>; - }>; - }; -} - -interface TemplateViewDialogProps { - isOpen: boolean; - onClose: () => void; - templateData: TemplateItem[] | any; - selectedRow: GenericData; - formCode: string; - contractItemId: number; - editableFieldsMap?: Map<string, string[]>; // 편집 가능 필드 정보 - onUpdateSuccess?: (updatedValues: Record<string, any>) => void; -} - -export function TemplateViewDialog({ - isOpen, - onClose, - templateData, - selectedRow, - formCode, - contractItemId, - editableFieldsMap = new Map(), - onUpdateSuccess -}: TemplateViewDialogProps) { - 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<any>(null); - const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); - const [isClient, setIsClient] = React.useState(false); - - // 클라이언트 사이드에서만 렌더링되도록 보장 - React.useEffect(() => { - setIsClient(true); - }, []); - - // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링 - const normalizedTemplates = React.useMemo((): TemplateItem[] => { - if (!templateData) return []; - - let templates: TemplateItem[]; - if (Array.isArray(templateData)) { - templates = templateData as TemplateItem[]; - } else { - templates = [templateData as TemplateItem]; - } - - return templates.filter(template => { - const sprContent = template.SPR_LST_SETUP?.CONTENT; - const sprItmContent = template.SPR_ITM_LST_SETUP?.CONTENT; - return sprContent || sprItmContent; - }); - }, [templateData]); - - // 선택된 템플릿 가져오기 - const selectedTemplate = React.useMemo(() => { - if (!selectedTemplateId) return normalizedTemplates[0]; - return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0]; - }, [normalizedTemplates, selectedTemplateId]); - - // 현재 TAG의 편집 가능한 필드 목록 가져오기 - const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { - return []; - } - return editableFieldsMap.get(selectedRow.TAG_NO) || []; - }, [selectedRow?.TAG_NO, editableFieldsMap]); - - // 필드가 편집 가능한지 판별하는 함수 - const isFieldEditable = React.useCallback((attId: string) => { - // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 - if (attId === "TAG_NO" || attId === "TAG_DESC") { - return true; - } - - // editableFieldsMap이 있으면 해당 리스트에 있는지 확인 - if (selectedRow?.TAG_NO && editableFieldsMap.has(selectedRow.TAG_NO)) { - return editableFields.includes(attId); - } - - return false; - }, [selectedRow?.TAG_NO, editableFieldsMap, editableFields]); - - // 셀 주소를 행과 열로 변환하는 함수 (예: "M1" -> {row: 0, col: 12}) - 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; - - // 열 문자를 숫자로 변환 (A=0, B=1, ..., Z=25, AA=26, ...) - let col = 0; - for (let i = 0; i < colStr.length; i++) { - col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); - } - col -= 1; // 0-based index로 변환 - - const row = parseInt(rowStr) - 1; // 0-based index로 변환 - - return { row, col }; - }; - - // 템플릿 변경 시 기본 선택 - React.useEffect(() => { - if (normalizedTemplates.length > 0 && !selectedTemplateId) { - setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); - } - }, [normalizedTemplates, selectedTemplateId]); - - const initSpread = React.useCallback((spread: any) => { - if (!spread || !selectedTemplate || !selectedRow) return; - - try { - setCurrentSpread(spread); - setHasChanges(false); - - // CONTENT 찾기 - let contentJson = null; - let dataSheets = null; - - if (selectedTemplate.SPR_LST_SETUP?.CONTENT) { - contentJson = selectedTemplate.SPR_LST_SETUP.CONTENT; - dataSheets = selectedTemplate.SPR_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); - } else if (selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT) { - contentJson = selectedTemplate.SPR_ITM_LST_SETUP.CONTENT; - dataSheets = selectedTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', selectedTemplate.NAME); - } - - if (!contentJson) { - console.warn('No CONTENT found in template:', selectedTemplate.NAME); - return; - } - - console.log('Loading template content for:', selectedTemplate.NAME); - - 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}> = []; - - 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 - }); - - // 셀 객체 가져오기 - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - - // selectedRow에서 해당 값 가져와서 셀에 설정 - const value = selectedRow[ATT_ID]; - if (value !== undefined && value !== null) { - cell.value(value); - } - - // 편집 권한 설정 - cell.locked(!isEditable); - - // 즉시 스타일 적용 (기존 스타일 보존하면서) - const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); - if (existingStyle) { - // 기존 스타일 복사 - const newStyle = Object.assign(new GC.Spread.Sheets.Style(), existingStyle); - - // 편집 권한에 따라 배경색만 변경 - if (isEditable) { - newStyle.backColor = "#f0fdf4"; // 연한 녹색 - } else { - newStyle.backColor = "#f9fafb"; // 연한 회색 - newStyle.foreColor = "#6b7280"; // 회색 글자 - } - - // 스타일 적용 - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } else { - // 기존 스타일이 없는 경우 새로운 스타일 생성 - const newStyle = new GC.Spread.Sheets.Style(); - if (isEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } - - console.log(`Mapped ${ATT_ID} (${value}) 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); - }); - - 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 && !mapping.isEditable) { - toast.warning(`${mapping.attId} field is read-only`); - info.cancel = true; - } - }); - } - } finally { - // 렌더링 재개 (모든 변경사항이 한번에 화면에 표시됨) - spread.resumePaint(); - } - - } catch (error) { - console.error('Error initializing spread:', error); - toast.error('Failed to load template'); - // 에러 발생 시에도 렌더링 재개 - if (spread && spread.resumePaint) { - spread.resumePaint(); - } - } - }, [selectedTemplate, selectedRow, isFieldEditable]); - - // 템플릿 변경 핸들러 - const handleTemplateChange = (templateId: string) => { - setSelectedTemplateId(templateId); - setHasChanges(false); - - if (currentSpread) { - setTimeout(() => { - initSpread(currentSpread); - }, 100); - } - }; - - // 변경사항 저장 함수 - const handleSaveChanges = React.useCallback(async () => { - if (!currentSpread || !hasChanges || !selectedRow) { - toast.info("No changes to save"); - return; - } - - try { - setIsPending(true); - - const activeSheet = currentSpread.getActiveSheet(); - const dataToSave = { ...selectedRow }; - - // cellMappings를 사용해서 편집 가능한 셀의 값만 추출 - 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; - } - } - }); - - // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정 - dataToSave.TAG_NO = selectedRow.TAG_NO; - - console.log('Data to save (TAG_NO preserved):', dataToSave); - - const { success, message } = await updateFormDataInDB( - formCode, - contractItemId, - dataToSave - ); - - if (!success) { - toast.error(message); - return; - } - - toast.success("Changes saved successfully!"); - - const updatedData = { - ...selectedRow, - ...dataToSave, - }; - - onUpdateSuccess?.(updatedData); - setHasChanges(false); - - } catch (error) { - console.error("Error saving changes:", error); - toast.error("An unexpected error occurred while saving"); - } finally { - setIsPending(false); - } - }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess, cellMappings]); - - if (!isOpen) return null; - - return ( - <Dialog open={isOpen} onOpenChange={onClose}> - <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> - <DialogDescription> - {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} - {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> - 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"></span> - Read-only fields - </span> - {cellMappings.length > 0 && ( - <span className="text-xs text-blue-600"> - {cellMappings.filter(m => m.isEditable).length} of {cellMappings.length} fields editable - </span> - )} - </div> - </DialogDescription> - </DialogHeader> - - {/* 템플릿 선택 UI */} - {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((template) => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - <div className="flex flex-col"> - <span>{template.NAME || `Template ${template.TMPL_ID.slice(0, 8)}`}</span> - <span className="text-xs text-muted-foreground">{template.TMPL_TYPE}</span> - </div> - </SelectItem> - ))} - </SelectContent> - </Select> - <span className="text-xs text-muted-foreground"> - ({normalizedTemplates.length} templates available) - </span> - </div> - </div> - )} - - {/* SpreadSheets 컴포넌트 영역 */} - <div className="flex-1 overflow-hidden"> - {selectedTemplate && isClient ? ( - <SpreadSheets - key={selectedTemplateId} - 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... - </> - ) : ( - "No template available" - )} - </div> - )} - </div> - - <DialogFooter className="flex-shrink-0"> - <Button variant="outline" onClick={onClose}> - Close - </Button> - - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending} - > - {isPending ? ( - <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... - </> - ) : ( - <> - <Save className="mr-2 h-4 w-4" /> - Save Changes - </> - )} - </Button> - )} - - </DialogFooter> - </DialogContent> - </Dialog> - ); -}
\ No newline at end of file diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index fbeceaf3..19c9a616 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -362,15 +362,20 @@ const editableFieldsCount = React.useMemo(() => { }); }, []); - const createCellStyle = React.useCallback((isEditable: boolean) => { - const style = new GC.Spread.Sheets.Style(); + const createCellStyle = React.useCallback((activeSheet: any, row: number, col: number, isEditable: boolean) => { + // 기존 스타일 가져오기 (없으면 새로 생성) + const existingStyle = activeSheet.getStyle(row, col) || new GC.Spread.Sheets.Style(); + + // backColor만 수정 if (isEditable) { - style.backColor = "#bbf7d0"; + existingStyle.backColor = "#bbf7d0"; } else { - style.backColor = "#e5e7eb"; - style.foreColor = "#4b5563"; + existingStyle.backColor = "#e5e7eb"; + // 읽기 전용일 때만 텍스트 색상 변경 (선택사항) + existingStyle.foreColor = "#4b5563"; } - return style; + + return existingStyle; }, []); const setBatchStyles = React.useCallback(( @@ -379,14 +384,11 @@ const editableFieldsCount = React.useMemo(() => { ) => { console.log(`🎨 Setting ${stylesToSet.length} styles in batch`); - const editableStyle = createCellStyle(true); - const readonlyStyle = createCellStyle(false); - // 🔧 개별 셀별로 스타일과 잠금 상태 설정 (편집 권한 보장) stylesToSet.forEach(({row, col, isEditable}) => { try { const cell = activeSheet.getCell(row, col); - const style = isEditable ? editableStyle : readonlyStyle; + const style = createCellStyle(activeSheet, row, col, isEditable); activeSheet.setStyle(row, col, style); cell.locked(!isEditable); // 편집 가능하면 잠금 해제 @@ -854,14 +856,14 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat } // 편집 가능 스타일 재적용 - const editableStyle = createCellStyle(true); + const editableStyle = createCellStyle(activeSheet, cellPos.row, cellPos.col, true); activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle); console.log(`🔓 Cell [${cellPos.row}, ${cellPos.col}] ${mapping.attId} set as EDITABLE`); } else { // 읽기 전용 셀 cell.locked(true); - const readonlyStyle = createCellStyle(false); + const readonlyStyle = createCellStyle(activeSheet, cellPos.row, cellPos.col, false); activeSheet.setStyle(cellPos.row, cellPos.col, readonlyStyle); } } catch (error) { @@ -972,7 +974,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}`, { duration: 5000 }); } else { // ✅ 정상 스타일 복원 - const normalStyle = createCellStyle(exactMapping.isEditable); + const normalStyle = createCellStyle(activeSheet, info.row, info.col, exactMapping.isEditable); activeSheet.setStyle(info.row, info.col, normalStyle); cell.locked(!exactMapping.isEditable); } @@ -1132,7 +1134,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat const cell = activeSheet.getCell(cellPos.row, cellPos.col); cell.value(value ?? null); - const style = createCellStyle(isEditable); + const style = createCellStyle(activeSheet, cellPos.row, cellPos.col, isEditable); activeSheet.setStyle(cellPos.row, cellPos.col, style); const columnConfig = columnsJSON.find(col => col.key === ATT_ID); @@ -1173,7 +1175,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat setIsInitializing(false); setLoadingProgress(null); } - }, [selectedTemplate, templateType, selectedRow, tableData, updateProgress, getSafeActiveSheet, createGrdListTableOptimized, setBatchValues, setBatchStyles, setupSheetProtectionAndEvents, setCellMappings]); + }, [selectedTemplate, templateType, selectedRow, tableData, updateProgress, getSafeActiveSheet, createGrdListTableOptimized, setBatchValues, setBatchStyles, setupSheetProtectionAndEvents, setCellMappings, createCellStyle, isFieldEditable, columnsJSON, setupOptimizedListValidation, parseCellAddress, ensureRowCapacity, getCellAddress]); const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges) { diff --git a/components/vendor-data/tag-table/tag-table-column.tsx b/components/vendor-data/tag-table/tag-table-column.tsx index a22611cf..6f0d977f 100644 --- a/components/vendor-data/tag-table/tag-table-column.tsx +++ b/components/vendor-data/tag-table/tag-table-column.tsx @@ -24,6 +24,7 @@ import { } from "@/components/ui/dropdown-menu" import { Ellipsis } from "lucide-react" import { Tag } from "@/types/vendorData" +import { createFilterFn } from "@/components/client-data-table/table-filters" export interface DataTableRowAction<TData> { @@ -70,6 +71,7 @@ export function getColumns({ header: ({ column }) => ( <ClientDataTableColumnHeaderSimple column={column} title="Tag No." /> ), + filterFn: createFilterFn("text"), cell: ({ row }) => <div className="w-20">{row.getValue("tagNo")}</div>, meta: { excelHeader: "Tag No" |
