summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/client-data-table/data-table-filter-list.tsx23
-rw-r--r--components/client-data-table/table-filters.ts344
-rw-r--r--components/form-data-stat/form-data-stat-table.tsx15
-rw-r--r--components/form-data/form-data-table-columns.tsx3
-rw-r--r--components/form-data/spreadJS-dialog copy 2.tsx1002
-rw-r--r--components/form-data/spreadJS-dialog copy 3.tsx1916
-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.tsx539
-rw-r--r--components/form-data/spreadJS-dialog.tsx32
-rw-r--r--components/vendor-data/tag-table/tag-table-column.tsx2
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"