summaryrefslogtreecommitdiff
path: root/components/form-data-plant/form-data-table-columns.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
commitf7f5069a2209cfa39b65f492f32270a5f554bed0 (patch)
tree933c731ec2cb7d8bc62219a0aeed45a5e97d5f15 /components/form-data-plant/form-data-table-columns.tsx
parentd49ad5dee1e5a504e1321f6db802b647497ee9ff (diff)
(대표님) EDP 해양 관련 개발 사항들
Diffstat (limited to 'components/form-data-plant/form-data-table-columns.tsx')
-rw-r--r--components/form-data-plant/form-data-table-columns.tsx546
1 files changed, 546 insertions, 0 deletions
diff --git a/components/form-data-plant/form-data-table-columns.tsx b/components/form-data-plant/form-data-table-columns.tsx
new file mode 100644
index 00000000..d453f6c2
--- /dev/null
+++ b/components/form-data-plant/form-data-table-columns.tsx
@@ -0,0 +1,546 @@
+import type { ColumnDef, Row } from "@tanstack/react-table";
+import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Badge } from "@/components/ui/badge";
+import { Ellipsis } from "lucide-react";
+import { formatDate } from "@/lib/utils";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { toast } from 'sonner';
+import { createFilterFn } from "@/components/client-data-table/table-filters";
+
+/** row 액션 관련 타입 */
+export interface DataTableRowAction<TData> {
+ row: Row<TData>;
+ type: "open" | "edit" | "update" | "delete";
+}
+
+/** 컬럼 타입 (필요에 따라 확장) */
+export type ColumnType = "STRING" | "NUMBER" | "LIST";
+
+export interface DataTableColumnJSON {
+ key: string;
+ /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */
+ label: string;
+
+ /** UI 표시용 label (예: 단위를 함께 표시) */
+ displayLabel?: string;
+
+ type: ColumnType;
+ options?: string[];
+ uom?: string;
+ uomId?: string;
+ shi?: string;
+
+ /** 템플릿에서 가져온 추가 정보 */
+ hidden?: boolean; // true이면 컬럼 숨김
+ seq?: number; // 정렬 순서
+ head?: string; // 헤더 텍스트 (우선순위 가장 높음)
+}
+
+// Register 인터페이스 추가
+export interface Register {
+ PROJ_NO: string;
+ TYPE_ID: string;
+ EP_ID: string;
+ DESC: string;
+ REMARK: string | null;
+ NEW_TAG_YN: boolean;
+ ALL_TAG_YN: boolean;
+ VND_YN: boolean;
+ SEQ: number;
+ CMPLX_YN: boolean;
+ CMPL_SETT: any | null;
+ MAP_ATT: Array<{ ATT_ID: string; [key: string]: any }>;
+ MAP_CLS_ID: string[];
+ MAP_OPER: any | null;
+ LNK_ATT: any[];
+ JOIN_TABLS: any[];
+ DELETED: boolean;
+ CRTER_NO: string;
+ CRTE_DTM: string;
+ CHGER_NO: string | null;
+ CHGE_DTM: string | null;
+ _id: string;
+}
+
+/**
+ * getColumns 함수에 필요한 props
+ * - TData: 테이블에 표시할 행(Row)의 타입
+ */
+interface GetColumnsProps<TData> {
+ columnsJSON: DataTableColumnJSON[];
+ setRowAction: React.Dispatch<
+ React.SetStateAction<DataTableRowAction<TData> | null>
+ >;
+ setReportData: React.Dispatch<React.SetStateAction<{ [key: string]: any }[]>>;
+ tempCount: number;
+ // 체크박스 선택 관련 props
+ selectedRows?: Record<string, boolean>;
+ onRowSelectionChange?: (updater: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void;
+ // 새로 추가: templateData
+ templateData?: any;
+ // 새로 추가: registers (필수 필드 체크용)
+ registers?: Register[];
+}
+
+/**
+ * 셀 주소(예: "A1", "B1", "AA1")에서 컬럼 순서를 추출하는 함수
+ * A=0, B=1, C=2, ..., Z=25, AA=26, AB=27, ...
+ */
+function getColumnOrderFromCellAddress(cellAddress: string): number {
+ if (!cellAddress || typeof cellAddress !== 'string') {
+ return 999999; // 유효하지 않은 경우 맨 뒤로
+ }
+
+ // 셀 주소에서 알파벳 부분만 추출 (예: "A1" -> "A", "AA1" -> "AA")
+ const match = cellAddress.match(/^([A-Z]+)/);
+ if (!match) {
+ return 999999;
+ }
+
+ const columnLetters = match[1];
+ let result = 0;
+
+ // 알파벳을 숫자로 변환 (26진법과 유사하지만 0이 없는 체계)
+ for (let i = 0; i < columnLetters.length; i++) {
+ const charCode = columnLetters.charCodeAt(i) - 65 + 1; // A=1, B=2, ..., Z=26
+ result = result * 26 + charCode;
+ }
+
+ return result - 1; // 0부터 시작하도록 조정
+}
+
+/**
+ * templateData에서 SPREAD_LIST의 컬럼 순서 정보를 추출하여 seq를 업데이트하는 함수
+ */
+function updateSeqFromTemplate(columnsJSON: DataTableColumnJSON[], templateData: any): DataTableColumnJSON[] {
+ if (!templateData) {
+ return columnsJSON; // templateData가 없으면 원본 그대로 반환
+ }
+
+ // templateData가 배열인지 단일 객체인지 확인
+ let templates: any[];
+ if (Array.isArray(templateData)) {
+ templates = templateData;
+ } else {
+ templates = [templateData];
+ }
+
+ // SPREAD_LIST 타입의 템플릿 찾기
+ const spreadListTemplate = templates.find(template =>
+ template.TMPL_TYPE === 'SPREAD_LIST' &&
+ template.SPR_LST_SETUP?.DATA_SHEETS
+ );
+
+ if (!spreadListTemplate) {
+ return columnsJSON; // SPREAD_LIST 템플릿이 없으면 원본 그대로 반환
+ }
+
+ // MAP_CELL_ATT에서 ATT_ID와 IN 매핑 정보 추출
+ const cellMappings = new Map<string, string>(); // key: ATT_ID, value: IN (셀 주소)
+
+ spreadListTemplate.SPR_LST_SETUP.DATA_SHEETS.forEach((dataSheet: any) => {
+ if (dataSheet.MAP_CELL_ATT) {
+ dataSheet.MAP_CELL_ATT.forEach((mapping: any) => {
+ if (mapping.ATT_ID && mapping.IN) {
+ cellMappings.set(mapping.ATT_ID, mapping.IN);
+ }
+ });
+ }
+ });
+
+ // columnsJSON을 복사하여 seq 값 업데이트
+ const updatedColumns = columnsJSON.map(column => {
+ const cellAddress = cellMappings.get(column.key);
+ if (cellAddress) {
+ // 셀 주소에서 컬럼 순서 추출
+ const newSeq = getColumnOrderFromCellAddress(cellAddress);
+ console.log(`🔄 Updating seq for ${column.key}: ${column.seq} -> ${newSeq} (from ${cellAddress})`);
+
+ return {
+ ...column,
+ seq: newSeq
+ };
+ }
+ return column; // 매핑이 없으면 원본 그대로
+ });
+
+ return updatedColumns;
+}
+
+/**
+ * Register의 MAP_ATT에 해당 ATT_ID가 있는지 확인하는 함수
+ * 필수 필드인지 체크
+ */
+function isRequiredField(attId: string, registers?: Register[]): boolean {
+ if (!registers || registers.length === 0) {
+ return false;
+ }
+
+ // 모든 레지스터의 MAP_ATT를 확인
+ return registers.some(register =>
+ register.MAP_ATT &&
+ register.MAP_ATT.some(att => att.ATT_ID === attId)
+ );
+}
+
+/**
+ * status 값에 따라 Badge variant를 결정하는 헬퍼 함수
+ */
+function getStatusBadgeVariant(status: string): "default" | "secondary" | "destructive" | "outline" {
+ const statusStr = String(status).toLowerCase();
+
+ switch (statusStr) {
+ case 'NEW':
+ case 'New':
+ return 'default'; // 초록색 계열
+ case 'Updated or Modified':
+ return 'secondary'; // 노란색 계열
+ case 'inactive':
+ case 'rejected':
+ case 'failed':
+ case 'cancelled':
+ return 'destructive'; // 빨간색 계열
+ default:
+ return 'outline'; // 기본 회색 계열
+ }
+}
+
+/**
+ * 헤더 텍스트를 결정하는 헬퍼 함수
+ * displayLabel이 있으면 사용, 없으면 label 사용
+ * 필수 필드인 경우 빨간색 * 추가
+ */
+function getHeaderText(col: DataTableColumnJSON, isRequired: boolean): React.ReactNode {
+ const baseText = col.displayLabel && col.displayLabel.trim() ? col.displayLabel : col.label;
+
+ if (isRequired) {
+ return (
+ <span>
+ {baseText}
+ <span style={{ color: 'red', marginLeft: '2px' }}>*</span>
+ </span>
+ );
+ }
+
+ return baseText;
+}
+
+/**
+ * 컬럼들을 seq 순서대로 배치하면서 연속된 같은 head를 가진 컬럼들을 그룹으로 묶는 함수
+ */
+function groupColumnsByHead(columns: DataTableColumnJSON[], registers?: Register[]): ColumnDef<any>[] {
+ const result: ColumnDef<any>[] = [];
+ let i = 0;
+
+ while (i < columns.length) {
+ const currentCol = columns[i];
+
+ // head가 없거나 빈 문자열인 경우 일반 컬럼으로 처리
+ if (!currentCol.head || !currentCol.head.trim()) {
+ result.push(createColumnDef(currentCol, false, registers));
+ 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++;
+ }
+
+ // 그룹에 컬럼이 하나만 있으면 일반 컬럼으로 처리
+ if (groupColumns.length === 1) {
+ result.push(createColumnDef(currentCol, false, registers));
+ } else {
+ // 그룹 컬럼 생성 (구분선 스타일 적용)
+ const groupColumn: ColumnDef<any> = {
+ id: `group-${groupHead.replace(/\s+/g, '-')}`,
+ header: groupHead,
+ columns: groupColumns.map(col => createColumnDef(col, true, registers)),
+ meta: {
+ isGroupColumn: true,
+ groupBorders: true, // 그룹 구분선 표시 플래그
+ }
+ };
+ result.push(groupColumn);
+ }
+
+ i = j; // 다음 그룹으로 이동
+ }
+
+ return result;
+}
+
+/**
+ * 개별 컬럼 정의를 생성하는 헬퍼 함수
+ */
+function createColumnDef(col: DataTableColumnJSON, isInGroup: boolean = false, registers?: Register[]): ColumnDef<any> {
+ const isRequired = isRequiredField(col.key, registers);
+
+ return {
+ accessorKey: col.key,
+ header: ({ column }) => (
+ <ClientDataTableColumnHeaderSimple
+ column={column}
+ title={getHeaderText(col, isRequired)}
+ />
+ ),
+
+ filterFn: col.type === 'NUMBER' ? createFilterFn("number") : col.type === 'LIST' ? createFilterFn("multi-select"):createFilterFn("text"),
+
+ meta: {
+ excelHeader: col.label,
+ minWidth: 80,
+ paddingFactor: 1.2,
+ maxWidth: col.key === "TAG_NO" ? 120 : 150,
+ isReadOnly: col.shi === true,
+ isInGroup, // 그룹 내 컬럼인지 표시
+ groupBorders: isInGroup, // 그룹 구분선 표시 플래그
+ isRequired, // 필수 필드 표시
+ },
+
+ cell: ({ row }) => {
+ const cellValue = row.getValue(col.key);
+
+ // SHI 필드만 읽기 전용으로 처리
+ const isReadOnly = col.shi === true;
+
+ // 그룹 구분선 스타일 클래스 추가
+ const groupBorderClass = isInGroup ? "group-column-border" : "";
+ const readOnlyClass = isReadOnly ? "read-only-cell" : "";
+ const combinedClass = [groupBorderClass, readOnlyClass].filter(Boolean).join(" ");
+
+ const cellStyle = {
+ ...(isReadOnly && { backgroundColor: '#f5f5f5', color: '#666', cursor: 'not-allowed' }),
+ ...(isInGroup && {
+ borderLeft: '2px solid #e2e8f0',
+ borderRight: '2px solid #e2e8f0',
+ position: 'relative' as const
+ })
+ };
+
+ // 툴팁 메시지 설정 (SHI 필드만)
+ const tooltipMessage = isReadOnly ? "SHI 전용 필드입니다" : "";
+
+ // status 컬럼인 경우 Badge 적용
+ if (col.key === "status") {
+ const statusValue = String(cellValue ?? "");
+ const badgeVariant = getStatusBadgeVariant(statusValue);
+
+ return (
+ <div
+ className={combinedClass}
+ style={cellStyle}
+ title={tooltipMessage}
+ >
+ <Badge variant={badgeVariant}>
+ {statusValue}
+ </Badge>
+ </div>
+ );
+ }
+
+ // 데이터 타입별 처리
+ switch (col.type) {
+ case "NUMBER":
+ return (
+ <div
+ className={combinedClass}
+ style={cellStyle}
+ title={tooltipMessage}
+ >
+ {cellValue ? Number(cellValue).toLocaleString() : ""}
+ </div>
+ );
+
+ case "LIST":
+ return (
+ <div
+ className={combinedClass}
+ style={cellStyle}
+ title={tooltipMessage}
+ >
+ {String(cellValue ?? "")}
+ </div>
+ );
+
+ case "STRING":
+ default:
+ return (
+ <div
+ className={combinedClass}
+ style={cellStyle}
+ title={tooltipMessage}
+ >
+ {String(cellValue ?? "")}
+ </div>
+ );
+ }
+ },
+ };
+}
+
+/**
+ * getColumns 함수
+ * 1) columnsJSON 배열을 필터링 (hidden이 true가 아닌 것들만)
+ * 2) seq에 따라 정렬
+ * 3) seq 순서를 유지하면서 연속된 같은 head를 가진 컬럼들을 그룹으로 묶기
+ * 4) 체크박스 컬럼 추가
+ * 5) 마지막에 "Action" 칼럼 추가
+ */
+export function getColumns<TData extends object>({
+ columnsJSON,
+ setRowAction,
+ setReportData,
+ tempCount,
+ selectedRows = {},
+ onRowSelectionChange,
+ templateData, // 새로 추가된 매개변수
+ registers, // 필수 필드 체크를 위한 레지스터 데이터
+}: GetColumnsProps<TData>): ColumnDef<TData>[] {
+ const columns: ColumnDef<TData>[] = [];
+
+ // (0) templateData에서 SPREAD_LIST인 경우 seq 값 업데이트
+ const processedColumnsJSON = updateSeqFromTemplate(columnsJSON, templateData);
+
+ // (1) 컬럼 필터링 및 정렬
+ const visibleColumns = processedColumnsJSON
+ .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;
+ });
+
+ console.log('📊 Final column order after template processing:',
+ visibleColumns.map(c => `${c.key}(seq:${c.seq})`));
+
+ // (2) 체크박스 컬럼 (항상 표시)
+ 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);
+
+ // (3) 기본 컬럼들 (seq 순서를 유지하면서 head에 따라 그룹핑 처리)
+ const groupedColumns = groupColumnsByHead(visibleColumns, registers);
+ columns.push(...groupedColumns);
+
+ // (4) 액션 칼럼 - update 버튼 예시
+ const actionColumn: ColumnDef<TData> = {
+ id: "update",
+ header: "",
+ cell: ({ row }) => (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => {
+ setRowAction({ row, type: "update" });
+ }}
+ >
+ Edit
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onSelect={() => {
+ if(tempCount > 0){
+ const { original } = row;
+ setReportData([original]);
+ } else {
+ toast.error("업로드된 Template File이 없습니다.");
+ }
+ }}
+ >
+ Create Document
+ </DropdownMenuItem>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ onSelect={() => {
+ setRowAction({ row, type: "delete" });
+ }}
+ className="text-red-600 focus:text-red-600"
+ >
+ Delete
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ ),
+ size: 40,
+ enablePinning: true,
+ };
+
+ columns.push(actionColumn);
+
+ // (5) 최종 반환
+ return columns;
+} \ No newline at end of file