summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/form-data/form-data-table-columns.tsx218
-rw-r--r--components/form-data/spreadJS-dialog copy.tsx539
-rw-r--r--components/form-data/spreadJS-dialog.tsx498
-rw-r--r--components/information/information-button.tsx2
-rw-r--r--components/information/information-client.tsx2
-rw-r--r--components/notice/notice-client.tsx2
-rw-r--r--components/pq/pq-review-detail.tsx4
-rw-r--r--components/pq/pq-review-table.tsx2
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>
))