diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-09 12:19:05 +0000 |
| commit | 6d654b1ba2c19e0bf1745b636908e3b00a0f02c7 (patch) | |
| tree | f6d48c0d3a65b428a828acea5db65db8e7bf0db8 /components | |
| parent | 44794a8628997c0d979adb5bd6711cd848b3e397 (diff) | |
(대표님) 20250709 변경사항 (약 18시 30분까지)
Diffstat (limited to 'components')
| -rw-r--r-- | components/form-data/form-data-table-columns.tsx | 218 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog copy.tsx | 539 | ||||
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 498 | ||||
| -rw-r--r-- | components/information/information-button.tsx | 2 | ||||
| -rw-r--r-- | components/information/information-client.tsx | 2 | ||||
| -rw-r--r-- | components/notice/notice-client.tsx | 2 | ||||
| -rw-r--r-- | components/pq/pq-review-detail.tsx | 4 | ||||
| -rw-r--r-- | components/pq/pq-review-table.tsx | 2 |
8 files changed, 893 insertions, 374 deletions
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx index 3749fe02..930e113b 100644 --- a/components/form-data/form-data-table-columns.tsx +++ b/components/form-data/form-data-table-columns.tsx @@ -42,6 +42,11 @@ export interface DataTableColumnJSON { uom?: string; uomId?: string; shi?: boolean; + + /** 템플릿에서 가져온 추가 정보 */ + hidden?: boolean; // true이면 컬럼 숨김 + seq?: number; // 정렬 순서 + head?: string; // 헤더 텍스트 (우선순위 가장 높음) } /** @@ -87,79 +92,70 @@ function getStatusBadgeVariant(status: string): "default" | "secondary" | "destr } /** - * getColumns 함수 - * 1) columnsJSON 배열을 순회하면서 accessorKey / header / cell 등을 설정 - * 2) 체크박스 컬럼 추가 (showBatchSelection이 true일 때) - * 3) 마지막에 "Action" 칼럼(예: update 버튼) 추가 + * 헤더 텍스트를 결정하는 헬퍼 함수 + * displayLabel이 있으면 사용, 없으면 label 사용 */ -export function getColumns<TData extends object>({ - columnsJSON, - setRowAction, - setReportData, - tempCount, - selectedRows = {}, - onRowSelectionChange, - // editableFieldsMap 매개변수 제거됨 -}: GetColumnsProps<TData>): ColumnDef<TData>[] { - const columns: ColumnDef<TData>[] = []; +function getHeaderText(col: DataTableColumnJSON): string { + if (col.displayLabel && col.displayLabel.trim()) { + return col.displayLabel; + } + return col.label; +} - // (1) 체크박스 컬럼 (항상 표시) - const selectColumn: ColumnDef<TData> = { - id: "select", - header: ({ table }) => ( - <Checkbox - checked={ - table.getIsAllPageRowsSelected() || - (table.getIsSomePageRowsSelected() && "indeterminate") - } - onCheckedChange={(value) => { - table.toggleAllPageRowsSelected(!!value); - - // 모든 행 선택/해제 - if (onRowSelectionChange) { - const allRowsSelection: Record<string, boolean> = {}; - table.getRowModel().rows.forEach((row) => { - allRowsSelection[row.id] = !!value; - }); - onRowSelectionChange(allRowsSelection); - } - }} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={(value) => { - row.toggleSelected(!!value); - - // 개별 행 선택 상태 업데이트 - if (onRowSelectionChange) { - onRowSelectionChange(prev => ({ - ...prev, - [row.id]: !!value - })); - } - }} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - enablePinning: true, - size: 40, - }; - columns.push(selectColumn); +/** + * 컬럼들을 head 값에 따라 그룹핑하는 헬퍼 함수 + */ +function groupColumnsByHead(columns: DataTableColumnJSON[]): ColumnDef<any>[] { + const groupedColumns: ColumnDef<any>[] = []; + const groupMap = new Map<string, DataTableColumnJSON[]>(); + const ungroupedColumns: DataTableColumnJSON[] = []; + + // head 값에 따라 컬럼들을 그룹핑 + columns.forEach(col => { + if (col.head && col.head.trim()) { + const groupKey = col.head.trim(); + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []); + } + groupMap.get(groupKey)!.push(col); + } else { + ungroupedColumns.push(col); + } + }); + + // 그룹핑된 컬럼들 처리 + groupMap.forEach((groupColumns, groupHeader) => { + if (groupColumns.length === 1) { + // 그룹에 컬럼이 하나만 있으면 일반 컬럼으로 처리 + ungroupedColumns.push(groupColumns[0]); + } else { + // 그룹 컬럼 생성 + const groupColumn: ColumnDef<any> = { + header: groupHeader, + columns: groupColumns.map(col => createColumnDef(col)) + }; + groupedColumns.push(groupColumn); + } + }); + + // 그룹핑되지 않은 컬럼들 처리 + ungroupedColumns.forEach(col => { + groupedColumns.push(createColumnDef(col)); + }); + + return groupedColumns; +} - // (2) 기본 컬럼들 - const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({ +/** + * 개별 컬럼 정의를 생성하는 헬퍼 함수 + */ +function createColumnDef(col: DataTableColumnJSON): ColumnDef<any> { + return { accessorKey: col.key, header: ({ column }) => ( <ClientDataTableColumnHeaderSimple column={column} - title={col.displayLabel || col.label} + title={getHeaderText(col)} /> ), @@ -240,11 +236,93 @@ export function getColumns<TData extends object>({ ); } }, - })); + }; +} + +/** + * getColumns 함수 + * 1) columnsJSON 배열을 필터링 (hidden이 true가 아닌 것들만) + * 2) seq에 따라 정렬 + * 3) head 값에 따라 컬럼 그룹핑 + * 4) 체크박스 컬럼 추가 + * 5) 마지막에 "Action" 칼럼 추가 + */ +export function getColumns<TData extends object>({ + columnsJSON, + setRowAction, + setReportData, + tempCount, + selectedRows = {}, + onRowSelectionChange, + // editableFieldsMap 매개변수 제거됨 +}: GetColumnsProps<TData>): ColumnDef<TData>[] { + const columns: ColumnDef<TData>[] = []; + + // (0) 컬럼 필터링 및 정렬 + const visibleColumns = columnsJSON + .filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만 + .sort((a, b) => { + // seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄 + const seqA = a.seq !== undefined ? a.seq : 999999; + const seqB = b.seq !== undefined ? b.seq : 999999; + return seqA - seqB; + }); + + // (1) 체크박스 컬럼 (항상 표시) + const selectColumn: ColumnDef<TData> = { + id: "select", + header: ({ table }) => ( + <Checkbox + checked={ + table.getIsAllPageRowsSelected() || + (table.getIsSomePageRowsSelected() && "indeterminate") + } + onCheckedChange={(value) => { + table.toggleAllPageRowsSelected(!!value); + + // 모든 행 선택/해제 + if (onRowSelectionChange) { + const allRowsSelection: Record<string, boolean> = {}; + table.getRowModel().rows.forEach((row) => { + allRowsSelection[row.id] = !!value; + }); + onRowSelectionChange(allRowsSelection); + } + }} + aria-label="Select all" + className="translate-y-[2px]" + /> + ), + cell: ({ row }) => ( + <Checkbox + checked={row.getIsSelected()} + onCheckedChange={(value) => { + row.toggleSelected(!!value); + + // 개별 행 선택 상태 업데이트 + if (onRowSelectionChange) { + onRowSelectionChange(prev => ({ + ...prev, + [row.id]: !!value + })); + } + }} + aria-label="Select row" + className="translate-y-[2px]" + /> + ), + enableSorting: false, + enableHiding: false, + enablePinning: true, + size: 40, + }; + columns.push(selectColumn); - columns.push(...baseColumns); + // (2) 기본 컬럼들 (head에 따라 그룹핑 처리) + const groupedColumns = groupColumnsByHead(visibleColumns); + columns.push(...groupedColumns); - // (4) 액션 칼럼 - update 버튼 예시 + // (3) 액션 칼럼 - update 버튼 예시 const actionColumn: ColumnDef<TData> = { id: "update", header: "", @@ -297,6 +375,6 @@ export function getColumns<TData extends object>({ columns.push(actionColumn); - // (5) 최종 반환 + // (4) 최종 반환 return columns; }
\ No newline at end of file diff --git a/components/form-data/spreadJS-dialog copy.tsx b/components/form-data/spreadJS-dialog copy.tsx new file mode 100644 index 00000000..5a51c2b5 --- /dev/null +++ b/components/form-data/spreadJS-dialog copy.tsx @@ -0,0 +1,539 @@ +"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 5a51c2b5..8be9d175 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -2,7 +2,14 @@ import * as React from "react"; import dynamic from "next/dynamic"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; @@ -16,24 +23,24 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; +import "@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css"; -// SpreadSheets를 동적으로 import (SSR 비활성화) +// Dynamically load the SpreadSheets component (disable SSR) const SpreadSheets = dynamic( - () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), - { + () => import("@mescius/spread-sheets-react").then((mod) => mod.SpreadSheets), + { ssr: false, loading: () => ( <div className="flex items-center justify-center h-full"> <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading SpreadSheets... </div> - ) + ), } ); -// 라이센스 키 설정을 클라이언트에서만 실행 -if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { +// Apply license key on the client only +if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } @@ -81,7 +88,7 @@ interface TemplateViewDialogProps { selectedRow: GenericData; formCode: string; contractItemId: number; - editableFieldsMap?: Map<string, string[]>; // 편집 가능 필드 정보 + editableFieldsMap?: Map<string, string[]>; // editable field info per tag onUpdateSuccess?: (updatedValues: Record<string, any>) => void; } @@ -93,310 +100,232 @@ export function TemplateViewDialog({ formCode, contractItemId, editableFieldsMap = new Map(), - onUpdateSuccess + onUpdateSuccess, }: TemplateViewDialogProps) { - const [hostStyle, setHostStyle] = React.useState({ - width: '100%', - height: '100%' - }); - + /* ------------------------- local state ------------------------- */ + const [hostStyle] = 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 [currentSpread, setCurrentSpread] = React.useState<GC.Spread.Sheets.Workbook | null>( + null + ); const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); - const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); + const [cellMappings, setCellMappings] = React.useState< + Array<{ attId: string; cellAddress: string; isEditable: boolean }> + >([]); const [isClient, setIsClient] = React.useState(false); - // 클라이언트 사이드에서만 렌더링되도록 보장 + // Render only on client side React.useEffect(() => { setIsClient(true); }, []); - // 템플릿 데이터를 배열로 정규화하고 CONTENT가 있는 것만 필터링 + /* ------------------------- helpers ------------------------- */ + // Normalize template list and keep only those with 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; - }); + + const list = Array.isArray(templateData) + ? (templateData as TemplateItem[]) + : ([templateData] as TemplateItem[]); + + return list.filter( + (t) => t.SPR_LST_SETUP?.CONTENT || t.SPR_ITM_LST_SETUP?.CONTENT + ); }, [templateData]); - // 선택된 템플릿 가져오기 + // Choose currently selected template const selectedTemplate = React.useMemo(() => { if (!selectedTemplateId) return normalizedTemplates[0]; - return normalizedTemplates.find(t => t.TMPL_ID === selectedTemplateId) || normalizedTemplates[0]; + return ( + normalizedTemplates.find((t) => t.TMPL_ID === selectedTemplateId) || + normalizedTemplates[0] + ); }, [normalizedTemplates, selectedTemplateId]); - // 현재 TAG의 편집 가능한 필드 목록 가져오기 + // Editable fields for the current TAG_NO const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { - return []; - } + if (!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)) { + const isFieldEditable = React.useCallback( + (attId: string) => { + // TAG_NO and TAG_DESC are always editable + if (attId === "TAG_NO" || attId === "TAG_DESC") return true; + if (!selectedRow?.TAG_NO) return false; return editableFields.includes(attId); - } - - 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+)$/); + }, + [selectedRow?.TAG_NO, editableFields] + ); + + /** Convert a cell address like "M1" into {row:0,col:12}. */ + const parseCellAddress = (addr: string): { row: number; col: number } | null => { + if (!addr) return null; + const match = addr.match(/^([A-Z]+)(\d+)$/); 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로 변환 - + col -= 1; + const row = parseInt(rowStr, 10) - 1; return { row, col }; }; - // 템플릿 변경 시 기본 선택 + // Auto‑select first template React.useEffect(() => { - if (normalizedTemplates.length > 0 && !selectedTemplateId) { + if (normalizedTemplates.length && !selectedTemplateId) { setSelectedTemplateId(normalizedTemplates[0].TMPL_ID); } }, [normalizedTemplates, selectedTemplateId]); - const initSpread = React.useCallback((spread: any) => { - if (!spread || !selectedTemplate || !selectedRow) return; + /* ------------------------- init spread ------------------------- */ + const initSpread = React.useCallback( + (spread: GC.Spread.Sheets.Workbook | undefined) => { + 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); - } + // Pick content JSON and data‑sheet mapping + const contentJson = + selectedTemplate.SPR_LST_SETUP?.CONTENT ?? + selectedTemplate.SPR_ITM_LST_SETUP?.CONTENT; + const dataSheets = + selectedTemplate.SPR_LST_SETUP?.DATA_SHEETS ?? + selectedTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; + if (!contentJson) return; - if (!contentJson) { - console.warn('No CONTENT found in template:', selectedTemplate.NAME); - return; - } + // Prepare shared styles once + const editableStyle = new GC.Spread.Sheets.Style(); + editableStyle.backColor = "#f0fdf4"; + editableStyle.locked = false; - console.log('Loading template content for:', selectedTemplate.NAME); - - const jsonData = typeof contentJson === 'string' - ? JSON.parse(contentJson) - : contentJson; + const readOnlyStyle = new GC.Spread.Sheets.Style(); + readOnlyStyle.backColor = "#f9fafb"; + readOnlyStyle.foreColor = "#6b7280"; + readOnlyStyle.locked = true; - // 렌더링 일시 중단 (성능 향상) - spread.suspendPaint(); + const jsonObj = typeof contentJson === "string" ? JSON.parse(contentJson) : contentJson; - 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); - }); + const sheet = spread.getActiveSheet(); - // 편집 시작 시 읽기 전용 셀 확인 - 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; + /* -------- batch load + style -------- */ + sheet.suspendPaint(); + sheet.suspendCalcService(true); + try { + spread.fromJSON(jsonObj); + sheet.options.isProtected = false; + + const mappings: Array<{ attId: string; cellAddress: string; isEditable: boolean }> = []; + + if (dataSheets?.length) { + dataSheets.forEach((ds) => { + ds.MAP_CELL_ATT?.forEach(({ ATT_ID, IN }) => { + if (!IN) return; + const pos = parseCellAddress(IN); + if (!pos) return; + const editable = isFieldEditable(ATT_ID); + mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: editable }); }); - - 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(); + // Apply values + style in chunks for large templates + const CHUNK = 500; + let idx = 0; + const applyChunk = () => { + const end = Math.min(idx + CHUNK, mappings.length); + for (; idx < end; idx++) { + const { attId, cellAddress, isEditable } = mappings[idx]; + const pos = parseCellAddress(cellAddress)!; + if (selectedRow[attId] !== undefined && selectedRow[attId] !== null) { + sheet.setValue(pos.row, pos.col, selectedRow[attId]); + } + sheet.setStyle(pos.row, pos.col, isEditable ? editableStyle : readOnlyStyle); + } + if (idx < mappings.length) { + requestAnimationFrame(applyChunk); + } else { + // enable protection & events after styling done + sheet.options.isProtected = true; + sheet.options.protectionOptions = { + allowSelectLockedCells: true, + allowSelectUnlockedCells: true, + } as any; + + // Cell/value change events + sheet.bind(GC.Spread.Sheets.Events.ValueChanged, () => setHasChanges(true)); + sheet.bind(GC.Spread.Sheets.Events.CellChanged, () => setHasChanges(true)); + + // Prevent editing read‑only fields + sheet.bind( + GC.Spread.Sheets.Events.EditStarting, + (event: any, info: any) => { + const map = mappings.find((m) => { + const pos = parseCellAddress(m.cellAddress); + return pos && pos.row === info.row && pos.col === info.col; + }); + if (map && !map.isEditable) { + toast.warning(`${map.attId} field is read‑only`); + info.cancel = true; + } + } + ); + + setCellMappings(mappings); + sheet.resumeCalcService(false); + sheet.resumePaint(); + } + }; + applyChunk(); + } catch (err) { + console.error(err); + toast.error("Failed to load template"); + sheet.resumeCalcService(false); + sheet.resumePaint(); } - } - }, [selectedTemplate, selectedRow, isFieldEditable]); + }, + [selectedTemplate, selectedRow, isFieldEditable] + ); - // 템플릿 변경 핸들러 - const handleTemplateChange = (templateId: string) => { - setSelectedTemplateId(templateId); + /* ------------------------- handlers ------------------------- */ + const handleTemplateChange = (id: string) => { + setSelectedTemplateId(id); setHasChanges(false); - if (currentSpread) { - setTimeout(() => { - initSpread(currentSpread); - }, 100); + // re‑init after a short tick so component remounts SpreadSheets + setTimeout(() => initSpread(currentSpread), 50); } }; - // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges || !selectedRow) { toast.info("No changes to save"); return; } + setIsPending(true); + 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; - } + const sheet = currentSpread.getActiveSheet(); + const payload: Record<string, any> = { ...selectedRow }; + + cellMappings.forEach((m) => { + if (m.isEditable) { + const pos = parseCellAddress(m.cellAddress); + if (pos) payload[m.attId] = sheet.getValue(pos.row, pos.col); } }); - // TAG_NO는 절대 변경되지 않도록 원본 값으로 강제 설정 - dataToSave.TAG_NO = selectedRow.TAG_NO; - - console.log('Data to save (TAG_NO preserved):', dataToSave); + payload.TAG_NO = selectedRow.TAG_NO; // never change TAG_NO const { success, message } = await updateFormDataInDB( formCode, contractItemId, - dataToSave + payload ); if (!success) { @@ -405,60 +334,47 @@ export function TemplateViewDialog({ } toast.success("Changes saved successfully!"); - - const updatedData = { - ...selectedRow, - ...dataToSave, - }; - - onUpdateSuccess?.(updatedData); + onUpdateSuccess?.({ ...selectedRow, ...payload }); setHasChanges(false); - - } catch (error) { - console.error("Error saving changes:", error); + } catch (err) { + console.error(err); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } - }, [currentSpread, hasChanges, formCode, contractItemId, selectedRow, onUpdateSuccess, cellMappings]); + }, [currentSpread, hasChanges, selectedRow, cellMappings, formCode, contractItemId, onUpdateSuccess]); + /* ------------------------- render ------------------------- */ 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"}} - > + <DialogContent className="w-[80%] max-w-none h-[80vh] flex flex-col" style={{ maxWidth: "80vw" }}> <DialogHeader className="flex-shrink-0"> - <DialogTitle>SEDP Template - {formCode}</DialogTitle> + <DialogTitle>SEDP Template – {formCode}</DialogTitle> <DialogDescription> - {selectedRow && `Selected TAG_NO: ${selectedRow.TAG_NO || 'N/A'}`} - {hasChanges && ( - <span className="ml-2 text-orange-600 font-medium"> - • Unsaved changes - </span> - )} + {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> + <span className="inline-block w-3 h-3 bg-green-100 border border-green-400 mr-1" /> 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 className="inline-block w-3 h-3 bg-gray-100 border border-gray-300 mr-1" /> + Read‑only fields </span> - {cellMappings.length > 0 && ( + {!!cellMappings.length && ( <span className="text-xs text-blue-600"> - {cellMappings.filter(m => m.isEditable).length} of {cellMappings.length} fields editable + {cellMappings.filter((m) => m.isEditable).length} of {cellMappings.length} fields editable </span> )} </div> </DialogDescription> </DialogHeader> - {/* 템플릿 선택 UI */} + {/* Template selector */} {normalizedTemplates.length > 1 && ( <div className="flex-shrink-0 px-4 py-2 border-b"> <div className="flex items-center gap-2"> @@ -468,37 +384,30 @@ export function TemplateViewDialog({ <SelectValue placeholder="Select a template" /> </SelectTrigger> <SelectContent> - {normalizedTemplates.map((template) => ( - <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> + {normalizedTemplates.map((t) => ( + <SelectItem key={t.TMPL_ID} value={t.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> + <span>{t.NAME || `Template ${t.TMPL_ID.slice(0, 8)}`}</span> + <span className="text-xs text-muted-foreground">{t.TMPL_TYPE}</span> </div> </SelectItem> ))} </SelectContent> </Select> - <span className="text-xs text-muted-foreground"> - ({normalizedTemplates.length} templates available) - </span> + <span className="text-xs text-muted-foreground">({normalizedTemplates.length} templates available)</span> </div> </div> )} - - {/* SpreadSheets 컴포넌트 영역 */} + + {/* Spreadsheet */} <div className="flex-1 overflow-hidden"> {selectedTemplate && isClient ? ( - <SpreadSheets - key={selectedTemplateId} - workbookInitialized={initSpread} - hostStyle={hostStyle} - /> + <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... + <Loader className="mr-2 h-4 w-4 animate-spin" /> Loading... </> ) : ( "No template available" @@ -507,33 +416,26 @@ export function TemplateViewDialog({ )} </div> + {/* footer */} <DialogFooter className="flex-shrink-0"> <Button variant="outline" onClick={onClose}> Close </Button> - {hasChanges && ( - <Button - variant="default" - onClick={handleSaveChanges} - disabled={isPending} - > + <Button variant="default" onClick={handleSaveChanges} disabled={isPending}> {isPending ? ( <> - <Loader className="mr-2 h-4 w-4 animate-spin" /> - Saving... + <Loader className="mr-2 h-4 w-4 animate-spin" /> Saving... </> ) : ( <> - <Save className="mr-2 h-4 w-4" /> - Save Changes + <Save className="mr-2 h-4 w-4" /> Save Changes </> )} </Button> )} - </DialogFooter> </DialogContent> </Dialog> ); -}
\ No newline at end of file +} diff --git a/components/information/information-button.tsx b/components/information/information-button.tsx index f8707439..5a9dc4d4 100644 --- a/components/information/information-button.tsx +++ b/components/information/information-button.tsx @@ -174,7 +174,7 @@ export function InformationButton({ {notice.title}
</h5>
<div className="flex items-center gap-3 text-xs text-gray-500">
- <span>{formatDate(notice.createdAt)}</span>
+ <span>{formatDate(notice.createdAt, "KR")}</span>
{notice.authorName && (
<span>{notice.authorName}</span>
)}
diff --git a/components/information/information-client.tsx b/components/information/information-client.tsx index 513b8f20..69835599 100644 --- a/components/information/information-client.tsx +++ b/components/information/information-client.tsx @@ -308,7 +308,7 @@ export function InformationClient({ initialData = [] }: InformationClientProps) </Badge>
</TableCell>
<TableCell>
- {formatDate(information.createdAt)}
+ {formatDate(information.createdAt, "KR")}
</TableCell>
<TableCell className="text-right">
<Button
diff --git a/components/notice/notice-client.tsx b/components/notice/notice-client.tsx index fab0d758..e32a40c9 100644 --- a/components/notice/notice-client.tsx +++ b/components/notice/notice-client.tsx @@ -347,7 +347,7 @@ export function NoticeClient({ initialData = [], currentUserId }: NoticeClientPr </Badge>
</TableCell>
<TableCell>
- {formatDate(notice.createdAt)}
+ {formatDate(notice.createdAt, "KR")}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
diff --git a/components/pq/pq-review-detail.tsx b/components/pq/pq-review-detail.tsx index e636caae..4f897a2b 100644 --- a/components/pq/pq-review-detail.tsx +++ b/components/pq/pq-review-detail.tsx @@ -447,7 +447,7 @@ export default function VendorPQAdminReview({ </div> <p className="text-sm mt-1">{comment.comment}</p> <p className="text-xs text-muted-foreground mt-1"> - {formatDate(comment.createdAt)} + {formatDate(comment.createdAt, "KR")} </p> </div> <Button @@ -847,7 +847,7 @@ function ItemCommentButton({ item, onCommentAdded }: ItemCommentButtonProps) { <p className="font-medium">{log.reviewerName}</p> <p>{log.reviewerComment}</p> <p className="text-xs text-muted-foreground"> - {formatDate(log.createdAt)} + {formatDate(log.createdAt, "KR")} </p> </div> ))} diff --git a/components/pq/pq-review-table.tsx b/components/pq/pq-review-table.tsx index 08b4de61..ce30bac0 100644 --- a/components/pq/pq-review-table.tsx +++ b/components/pq/pq-review-table.tsx @@ -313,7 +313,7 @@ function ItemReviewButton({ answerId, checkPoint, onCommentAdded }: ItemReviewBu <p className="font-medium">{log.reviewerName}</p> <p>{log.reviewerComment}</p> <p className="text-xs text-muted-foreground"> - {formatDate(log.createdAt)} + {formatDate(log.createdAt, "KR")} </p> </div> )) |
