Merge pull request 'feature/screen-management' (#351) from feature/screen-management into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/351
This commit is contained in:
commit
4801ee5ca4
|
|
@ -47,8 +47,13 @@ export interface SplitPanelConfig {
|
||||||
columns: Array<{ name: string; label: string; width?: number }>;
|
columns: Array<{ name: string; label: string; width?: number }>;
|
||||||
relation?: {
|
relation?: {
|
||||||
type: string;
|
type: string;
|
||||||
foreignKey: string;
|
foreignKey?: string;
|
||||||
leftColumn: string;
|
leftColumn?: string;
|
||||||
|
// 복합키 지원 (새로운 방식)
|
||||||
|
keys?: Array<{
|
||||||
|
leftColumn: string;
|
||||||
|
rightColumn: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -210,8 +215,21 @@ class MasterDetailExcelService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 분할 패널의 relation 정보가 있으면 우선 사용
|
// 2. 분할 패널의 relation 정보가 있으면 우선 사용
|
||||||
let masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn;
|
// 🔥 keys 배열을 우선 사용 (새로운 복합키 지원 방식)
|
||||||
let detailFkColumn = splitPanel.rightPanel.relation?.foreignKey;
|
let masterKeyColumn: string | undefined;
|
||||||
|
let detailFkColumn: string | undefined;
|
||||||
|
|
||||||
|
const relationKeys = splitPanel.rightPanel.relation?.keys;
|
||||||
|
if (relationKeys && relationKeys.length > 0) {
|
||||||
|
// keys 배열에서 첫 번째 키 사용
|
||||||
|
masterKeyColumn = relationKeys[0].leftColumn;
|
||||||
|
detailFkColumn = relationKeys[0].rightColumn;
|
||||||
|
logger.info(`keys 배열에서 관계 정보 사용: ${masterKeyColumn} -> ${detailFkColumn}`);
|
||||||
|
} else {
|
||||||
|
// 하위 호환성: 기존 leftColumn/foreignKey 사용
|
||||||
|
masterKeyColumn = splitPanel.rightPanel.relation?.leftColumn;
|
||||||
|
detailFkColumn = splitPanel.rightPanel.relation?.foreignKey;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. relation 정보가 없으면 column_labels에서 Entity 관계 조회
|
// 3. relation 정보가 없으면 column_labels에서 Entity 관계 조회
|
||||||
if (!masterKeyColumn || !detailFkColumn) {
|
if (!masterKeyColumn || !detailFkColumn) {
|
||||||
|
|
|
||||||
|
|
@ -187,71 +187,68 @@ class TableCategoryValueService {
|
||||||
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
logger.info("형제 메뉴 OBJID 목록", { menuObjid, siblingObjids });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 카테고리 값 조회 (형제 메뉴 포함)
|
// 2. 카테고리 값 조회 (메뉴 스코프 또는 형제 메뉴 포함)
|
||||||
let query: string;
|
let query: string;
|
||||||
let params: any[];
|
let params: any[];
|
||||||
|
|
||||||
|
const baseSelect = `
|
||||||
|
SELECT
|
||||||
|
value_id AS "valueId",
|
||||||
|
table_name AS "tableName",
|
||||||
|
column_name AS "columnName",
|
||||||
|
value_code AS "valueCode",
|
||||||
|
value_label AS "valueLabel",
|
||||||
|
value_order AS "valueOrder",
|
||||||
|
parent_value_id AS "parentValueId",
|
||||||
|
depth,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
icon,
|
||||||
|
is_active AS "isActive",
|
||||||
|
is_default AS "isDefault",
|
||||||
|
company_code AS "companyCode",
|
||||||
|
menu_objid AS "menuObjid",
|
||||||
|
created_at AS "createdAt",
|
||||||
|
updated_at AS "updatedAt",
|
||||||
|
created_by AS "createdBy",
|
||||||
|
updated_by AS "updatedBy"
|
||||||
|
FROM table_column_category_values
|
||||||
|
WHERE table_name = $1
|
||||||
|
AND column_name = $2
|
||||||
|
`;
|
||||||
|
|
||||||
if (companyCode === "*") {
|
if (companyCode === "*") {
|
||||||
// 최고 관리자: 모든 카테고리 값 조회
|
// 최고 관리자: menuObjid가 있으면 해당 메뉴(및 형제 메뉴)의 값만 조회
|
||||||
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
if (menuObjid && siblingObjids.length > 0) {
|
||||||
query = `
|
query = baseSelect + ` AND menu_objid = ANY($3::numeric[])`;
|
||||||
SELECT
|
params = [tableName, columnName, siblingObjids];
|
||||||
value_id AS "valueId",
|
logger.info("최고 관리자 메뉴 스코프 카테고리 값 조회", { menuObjid, siblingObjids });
|
||||||
table_name AS "tableName",
|
} else if (menuObjid) {
|
||||||
column_name AS "columnName",
|
query = baseSelect + ` AND menu_objid = $3`;
|
||||||
value_code AS "valueCode",
|
params = [tableName, columnName, menuObjid];
|
||||||
value_label AS "valueLabel",
|
logger.info("최고 관리자 단일 메뉴 카테고리 값 조회", { menuObjid });
|
||||||
value_order AS "valueOrder",
|
} else {
|
||||||
parent_value_id AS "parentValueId",
|
// menuObjid 없으면 모든 값 조회 (중복 가능)
|
||||||
depth,
|
query = baseSelect;
|
||||||
description,
|
params = [tableName, columnName];
|
||||||
color,
|
logger.info("최고 관리자 전체 카테고리 값 조회 (menuObjid 없음)");
|
||||||
icon,
|
}
|
||||||
is_active AS "isActive",
|
|
||||||
is_default AS "isDefault",
|
|
||||||
company_code AS "companyCode",
|
|
||||||
menu_objid AS "menuObjid",
|
|
||||||
created_at AS "createdAt",
|
|
||||||
updated_at AS "updatedAt",
|
|
||||||
created_by AS "createdBy",
|
|
||||||
updated_by AS "updatedBy"
|
|
||||||
FROM table_column_category_values
|
|
||||||
WHERE table_name = $1
|
|
||||||
AND column_name = $2
|
|
||||||
`;
|
|
||||||
params = [tableName, columnName];
|
|
||||||
logger.info("최고 관리자 카테고리 값 조회");
|
|
||||||
} else {
|
} else {
|
||||||
// 일반 회사: 자신의 카테고리 값만 조회
|
// 일반 회사: 자신의 회사 + menuObjid로 필터링
|
||||||
// 메뉴 스코프 제거: 같은 테이블.컬럼 조합은 모든 메뉴에서 공유
|
if (menuObjid && siblingObjids.length > 0) {
|
||||||
query = `
|
query = baseSelect + ` AND company_code = $3 AND menu_objid = ANY($4::numeric[])`;
|
||||||
SELECT
|
params = [tableName, columnName, companyCode, siblingObjids];
|
||||||
value_id AS "valueId",
|
logger.info("회사별 메뉴 스코프 카테고리 값 조회", { companyCode, menuObjid, siblingObjids });
|
||||||
table_name AS "tableName",
|
} else if (menuObjid) {
|
||||||
column_name AS "columnName",
|
query = baseSelect + ` AND company_code = $3 AND menu_objid = $4`;
|
||||||
value_code AS "valueCode",
|
params = [tableName, columnName, companyCode, menuObjid];
|
||||||
value_label AS "valueLabel",
|
logger.info("회사별 단일 메뉴 카테고리 값 조회", { companyCode, menuObjid });
|
||||||
value_order AS "valueOrder",
|
} else {
|
||||||
parent_value_id AS "parentValueId",
|
// menuObjid 없으면 회사 전체 조회 (중복 가능하지만 회사별로 제한)
|
||||||
depth,
|
query = baseSelect + ` AND company_code = $3`;
|
||||||
description,
|
params = [tableName, columnName, companyCode];
|
||||||
color,
|
logger.info("회사별 카테고리 값 조회 (menuObjid 없음)", { companyCode });
|
||||||
icon,
|
}
|
||||||
is_active AS "isActive",
|
|
||||||
is_default AS "isDefault",
|
|
||||||
company_code AS "companyCode",
|
|
||||||
menu_objid AS "menuObjid",
|
|
||||||
created_at AS "createdAt",
|
|
||||||
updated_at AS "updatedAt",
|
|
||||||
created_by AS "createdBy",
|
|
||||||
updated_by AS "updatedBy"
|
|
||||||
FROM table_column_category_values
|
|
||||||
WHERE table_name = $1
|
|
||||||
AND column_name = $2
|
|
||||||
AND company_code = $3
|
|
||||||
`;
|
|
||||||
params = [tableName, columnName, companyCode];
|
|
||||||
logger.info("회사별 카테고리 값 조회", { companyCode });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!includeInactive) {
|
if (!includeInactive) {
|
||||||
|
|
|
||||||
|
|
@ -705,43 +705,27 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 기존 단일 테이블 업로드 로직
|
// 기존 단일 테이블 업로드 로직
|
||||||
console.log("📊 단일 테이블 업로드 시작:", {
|
|
||||||
tableName,
|
|
||||||
uploadMode,
|
|
||||||
numberingRuleId,
|
|
||||||
numberingTargetColumn,
|
|
||||||
dataCount: filteredData.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
|
||||||
// 🆕 단일 테이블 채번 설정 확인
|
// 단일 테이블 채번 설정 확인
|
||||||
const hasNumbering = numberingRuleId && numberingTargetColumn;
|
const hasNumbering = numberingRuleId && numberingTargetColumn;
|
||||||
console.log("📊 채번 설정:", { hasNumbering, numberingRuleId, numberingTargetColumn });
|
|
||||||
|
|
||||||
for (const row of filteredData) {
|
for (const row of filteredData) {
|
||||||
try {
|
try {
|
||||||
let dataToSave = { ...row };
|
let dataToSave = { ...row };
|
||||||
|
|
||||||
// 🆕 채번 적용: 각 행마다 채번 API 호출
|
// 채번 적용: 각 행마다 채번 API 호출
|
||||||
if (hasNumbering && uploadMode === "insert") {
|
if (hasNumbering && uploadMode === "insert") {
|
||||||
try {
|
try {
|
||||||
const { apiClient } = await import("@/lib/api/client");
|
const { apiClient } = await import("@/lib/api/client");
|
||||||
console.log(`📊 채번 API 호출: /numbering-rules/${numberingRuleId}/allocate`);
|
|
||||||
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`);
|
const numberingResponse = await apiClient.post(`/numbering-rules/${numberingRuleId}/allocate`);
|
||||||
console.log(`📊 채번 API 응답:`, numberingResponse.data);
|
|
||||||
// 응답 구조: { success: true, data: { generatedCode: "..." } }
|
|
||||||
const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code;
|
const generatedCode = numberingResponse.data?.data?.generatedCode || numberingResponse.data?.data?.code;
|
||||||
if (numberingResponse.data?.success && generatedCode) {
|
if (numberingResponse.data?.success && generatedCode) {
|
||||||
dataToSave[numberingTargetColumn] = generatedCode;
|
dataToSave[numberingTargetColumn] = generatedCode;
|
||||||
console.log(`✅ 채번 적용: ${numberingTargetColumn} = ${generatedCode}`);
|
|
||||||
} else {
|
|
||||||
console.warn(`⚠️ 채번 실패: 응답에 코드 없음`, numberingResponse.data);
|
|
||||||
}
|
}
|
||||||
} catch (numError) {
|
} catch (numError) {
|
||||||
console.error("채번 오류:", numError);
|
console.error("채번 오류:", numError);
|
||||||
// 채번 실패 시에도 계속 진행 (채번 컬럼만 비워둠)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
@ -42,7 +43,7 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { tableTypeApi } from "@/lib/api/screen";
|
import { tableTypeApi } from "@/lib/api/screen";
|
||||||
import { commonCodeApi } from "@/lib/api/commonCode";
|
import { commonCodeApi } from "@/lib/api/commonCode";
|
||||||
import { getCurrentUser, UserInfo } from "@/lib/api/client";
|
import { apiClient, getCurrentUser, UserInfo } from "@/lib/api/client";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup";
|
import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -100,11 +101,7 @@ const CascadingDropdownInForm: React.FC<CascadingDropdownInFormProps> = ({
|
||||||
const isDisabled = !parentValue || loading;
|
const isDisabled = !parentValue || loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select value={value || ""} onValueChange={(newValue) => onChange?.(newValue)} disabled={isDisabled}>
|
||||||
value={value || ""}
|
|
||||||
onValueChange={(newValue) => onChange?.(newValue)}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
<SelectTrigger className={className}>
|
<SelectTrigger className={className}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -187,7 +184,17 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
const splitPanelContext = useSplitPanelContext(); // 분할 패널 컨텍스트
|
||||||
const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용)
|
const screenContext = useScreenContextOptional(); // 화면 컨텍스트 (좌측/우측 위치 확인용)
|
||||||
const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치
|
const splitPanelPosition = screenContext?.splitPanelPosition; // 분할 패널 내 위치
|
||||||
|
|
||||||
|
// URL에서 menuObjid 가져오기 (카테고리 값 조회 시 필요)
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const menuObjid = useMemo(() => {
|
||||||
|
// 1. ScreenContext에서 가져오기
|
||||||
|
if (screenContext?.menuObjid) return screenContext.menuObjid;
|
||||||
|
// 2. URL 쿼리에서 가져오기
|
||||||
|
const urlMenuObjid = searchParams.get("menuObjid");
|
||||||
|
return urlMenuObjid ? parseInt(urlMenuObjid) : undefined;
|
||||||
|
}, [screenContext?.menuObjid, searchParams]);
|
||||||
|
|
||||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
const [searchValues, setSearchValues] = useState<Record<string, any>>({});
|
||||||
|
|
@ -199,7 +206,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const hasInitializedWidthsRef = useRef(false);
|
const hasInitializedWidthsRef = useRef(false);
|
||||||
const columnRefs = useRef<Record<string, HTMLTableCellElement | null>>({});
|
const columnRefs = useRef<Record<string, HTMLTableCellElement | null>>({});
|
||||||
const isResizingRef = useRef(false);
|
const isResizingRef = useRef(false);
|
||||||
|
|
||||||
// TableOptions 상태
|
// TableOptions 상태
|
||||||
const [filters, setFilters] = useState<TableFilter[]>([]);
|
const [filters, setFilters] = useState<TableFilter[]>([]);
|
||||||
const [grouping, setGrouping] = useState<string[]>([]);
|
const [grouping, setGrouping] = useState<string[]>([]);
|
||||||
|
|
@ -236,14 +243,19 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
|
||||||
|
|
||||||
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}})
|
// 카테고리 값 매핑 캐시 (컬럼명 -> {코드 -> {라벨, 색상}})
|
||||||
const [categoryMappings, setCategoryMappings] = useState<Record<string, Record<string, { label: string; color?: string }>>>({});
|
const [categoryMappings, setCategoryMappings] = useState<
|
||||||
|
Record<string, Record<string, { label: string; color?: string }>>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
// 카테고리 코드 라벨 캐시 (CATEGORY_* 코드 -> 라벨)
|
||||||
|
const [categoryCodeLabels, setCategoryCodeLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// 테이블 등록 (Context에 등록)
|
// 테이블 등록 (Context에 등록)
|
||||||
const tableId = `datatable-${component.id}`;
|
const tableId = `datatable-${component.id}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!component.tableName || !component.columns) return;
|
if (!component.tableName || !component.columns) return;
|
||||||
|
|
||||||
registerTable({
|
registerTable({
|
||||||
tableId,
|
tableId,
|
||||||
label: component.title || "데이터 테이블",
|
label: component.title || "데이터 테이블",
|
||||||
|
|
@ -320,7 +332,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
||||||
const { targetTable, filterColumn, filterValue } = event.detail || {};
|
const { targetTable, filterColumn, filterValue } = event.detail || {};
|
||||||
|
|
||||||
// 이 테이블이 대상 테이블인지 확인
|
// 이 테이블이 대상 테이블인지 확인
|
||||||
if (targetTable === component.tableName) {
|
if (targetTable === component.tableName) {
|
||||||
console.log("📌 [InteractiveDataTable] RelatedDataButtons 필터 적용:", {
|
console.log("📌 [InteractiveDataTable] RelatedDataButtons 필터 적용:", {
|
||||||
|
|
@ -365,8 +377,10 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
for (const col of categoryColumns) {
|
for (const col of categoryColumns) {
|
||||||
try {
|
try {
|
||||||
|
// menuObjid가 있으면 쿼리 파라미터로 전달 (메뉴별 카테고리 색상 적용)
|
||||||
|
const queryParams = menuObjid ? `?menuObjid=${menuObjid}` : "";
|
||||||
const response = await apiClient.get(
|
const response = await apiClient.get(
|
||||||
`/table-categories/${component.tableName}/${col.columnName}/values`
|
`/table-categories/${component.tableName}/${col.columnName}/values${queryParams}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
|
|
@ -379,7 +393,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
mappings[col.columnName] = mapping;
|
mappings[col.columnName] = mapping;
|
||||||
console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping);
|
console.log(`✅ 카테고리 매핑 로드 성공 [${col.columnName}]:`, mapping, { menuObjid });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error);
|
console.error(`❌ 카테고리 값 로드 실패 [${col.columnName}]:`, error);
|
||||||
|
|
@ -394,7 +408,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
loadCategoryMappings();
|
loadCategoryMappings();
|
||||||
}, [component.tableName, component.columns, getColumnWebType]);
|
}, [component.tableName, component.columns, getColumnWebType, menuObjid]);
|
||||||
|
|
||||||
// 파일 상태 확인 함수
|
// 파일 상태 확인 함수
|
||||||
const checkFileStatus = useCallback(
|
const checkFileStatus = useCallback(
|
||||||
|
|
@ -583,13 +597,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
// 없으면 테이블 타입 관리에서 설정된 값 찾기
|
// 없으면 테이블 타입 관리에서 설정된 값 찾기
|
||||||
const tableColumn = tableColumns.find((col) => col.columnName === columnName);
|
const tableColumn = tableColumns.find((col) => col.columnName === columnName);
|
||||||
|
|
||||||
// input_type 우선 사용 (category 등)
|
// input_type 우선 사용 (category 등)
|
||||||
const inputType = (tableColumn as any)?.input_type || (tableColumn as any)?.inputType;
|
const inputType = (tableColumn as any)?.input_type || (tableColumn as any)?.inputType;
|
||||||
if (inputType) {
|
if (inputType) {
|
||||||
return inputType;
|
return inputType;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 없으면 webType 사용
|
// 없으면 webType 사용
|
||||||
return tableColumn?.webType || "text";
|
return tableColumn?.webType || "text";
|
||||||
},
|
},
|
||||||
|
|
@ -696,19 +710,19 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
let linkedFilterValues: Record<string, any> = {};
|
let linkedFilterValues: Record<string, any> = {};
|
||||||
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
|
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
|
||||||
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
|
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
|
||||||
|
|
||||||
if (splitPanelContext) {
|
if (splitPanelContext) {
|
||||||
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
|
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
|
||||||
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
|
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
|
||||||
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
|
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
|
||||||
(filter) => filter.targetColumn?.startsWith(component.tableName + ".") ||
|
(filter) =>
|
||||||
filter.targetColumn === component.tableName
|
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 좌측 데이터 선택 여부 확인
|
// 좌측 데이터 선택 여부 확인
|
||||||
hasSelectedLeftData = splitPanelContext.selectedLeftData &&
|
hasSelectedLeftData =
|
||||||
Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
||||||
|
|
||||||
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
|
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
|
||||||
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
|
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
|
||||||
const tableSpecificFilters: Record<string, any> = {};
|
const tableSpecificFilters: Record<string, any> = {};
|
||||||
|
|
@ -727,7 +741,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
}
|
}
|
||||||
linkedFilterValues = tableSpecificFilters;
|
linkedFilterValues = tableSpecificFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
||||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||||
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
||||||
|
|
@ -739,9 +753,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 필터 적용
|
// 🆕 RelatedDataButtons 필터 적용
|
||||||
let relatedButtonFilterValues: Record<string, any> = {};
|
const relatedButtonFilterValues: Record<string, any> = {};
|
||||||
if (relatedButtonFilter) {
|
if (relatedButtonFilter) {
|
||||||
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
|
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
|
||||||
}
|
}
|
||||||
|
|
@ -752,16 +766,16 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
...linkedFilterValues,
|
...linkedFilterValues,
|
||||||
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
|
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🔍 데이터 조회 시작:", {
|
console.log("🔍 데이터 조회 시작:", {
|
||||||
tableName: component.tableName,
|
tableName: component.tableName,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
linkedFilterValues,
|
linkedFilterValues,
|
||||||
relatedButtonFilterValues,
|
relatedButtonFilterValues,
|
||||||
mergedSearchParams,
|
mergedSearchParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||||
page,
|
page,
|
||||||
size: pageSize,
|
size: pageSize,
|
||||||
|
|
@ -769,11 +783,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("✅ 데이터 조회 완료:", {
|
console.log("✅ 데이터 조회 완료:", {
|
||||||
tableName: component.tableName,
|
tableName: component.tableName,
|
||||||
dataLength: result.data.length,
|
dataLength: result.data.length,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
page: result.page
|
page: result.page,
|
||||||
});
|
});
|
||||||
|
|
||||||
setData(result.data);
|
setData(result.data);
|
||||||
|
|
@ -781,6 +795,45 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
setTotalPages(result.totalPages);
|
setTotalPages(result.totalPages);
|
||||||
setCurrentPage(result.page);
|
setCurrentPage(result.page);
|
||||||
|
|
||||||
|
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
|
||||||
|
const detectAndLoadCategoryLabels = async () => {
|
||||||
|
const categoryCodes = new Set<string>();
|
||||||
|
result.data.forEach((row: Record<string, any>) => {
|
||||||
|
Object.values(row).forEach((value) => {
|
||||||
|
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
||||||
|
categoryCodes.add(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
|
||||||
|
|
||||||
|
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
|
||||||
|
const newCodes = Array.from(categoryCodes);
|
||||||
|
|
||||||
|
if (newCodes.length > 0) {
|
||||||
|
try {
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
|
||||||
|
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
setCategoryCodeLabels((prev) => {
|
||||||
|
const newLabels = {
|
||||||
|
...prev,
|
||||||
|
...response.data.data,
|
||||||
|
};
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
|
||||||
|
return newLabels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("카테고리 라벨 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
detectAndLoadCategoryLabels();
|
||||||
|
|
||||||
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
|
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
|
||||||
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
|
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
|
||||||
const primaryKeyField = Object.keys(rowData)[0];
|
const primaryKeyField = Object.keys(rowData)[0];
|
||||||
|
|
@ -916,18 +969,18 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
try {
|
try {
|
||||||
const columns = await tableTypeApi.getColumns(component.tableName);
|
const columns = await tableTypeApi.getColumns(component.tableName);
|
||||||
setTableColumns(columns);
|
setTableColumns(columns);
|
||||||
|
|
||||||
// 🆕 전체 컬럼 목록 설정
|
// 🆕 전체 컬럼 목록 설정
|
||||||
const columnNames = columns.map(col => col.columnName);
|
const columnNames = columns.map((col) => col.columnName);
|
||||||
setAllAvailableColumns(columnNames);
|
setAllAvailableColumns(columnNames);
|
||||||
|
|
||||||
// 🆕 컬럼명 -> 라벨 매핑 생성
|
// 🆕 컬럼명 -> 라벨 매핑 생성
|
||||||
const labels: Record<string, string> = {};
|
const labels: Record<string, string> = {};
|
||||||
columns.forEach(col => {
|
columns.forEach((col) => {
|
||||||
labels[col.columnName] = col.displayName || col.columnName;
|
labels[col.columnName] = col.displayName || col.columnName;
|
||||||
});
|
});
|
||||||
setColumnLabels(labels);
|
setColumnLabels(labels);
|
||||||
|
|
||||||
// 🆕 localStorage에서 필터 설정 복원
|
// 🆕 localStorage에서 필터 설정 복원
|
||||||
if (user?.userId && component.componentId) {
|
if (user?.userId && component.componentId) {
|
||||||
const storageKey = `table-search-filter-${user.userId}-${component.componentId}`;
|
const storageKey = `table-search-filter-${user.userId}-${component.componentId}`;
|
||||||
|
|
@ -983,28 +1036,31 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
// 행 선택 핸들러
|
// 행 선택 핸들러
|
||||||
const handleRowSelect = useCallback((rowIndex: number, isSelected: boolean) => {
|
const handleRowSelect = useCallback(
|
||||||
setSelectedRows((prev) => {
|
(rowIndex: number, isSelected: boolean) => {
|
||||||
const newSet = new Set(prev);
|
setSelectedRows((prev) => {
|
||||||
if (isSelected) {
|
const newSet = new Set(prev);
|
||||||
newSet.add(rowIndex);
|
if (isSelected) {
|
||||||
} else {
|
newSet.add(rowIndex);
|
||||||
newSet.delete(rowIndex);
|
} else {
|
||||||
|
newSet.delete(rowIndex);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용)
|
||||||
|
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
||||||
|
if (isSelected && data[rowIndex]) {
|
||||||
|
splitPanelContext.setSelectedLeftData(data[rowIndex]);
|
||||||
|
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]);
|
||||||
|
} else if (!isSelected) {
|
||||||
|
splitPanelContext.setSelectedLeftData(null);
|
||||||
|
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return newSet;
|
},
|
||||||
});
|
[data, splitPanelContext, splitPanelPosition],
|
||||||
|
);
|
||||||
// 분할 패널 좌측에서 선택 시 컨텍스트에 데이터 저장 (연결 필터용)
|
|
||||||
if (splitPanelContext && splitPanelPosition === "left" && !splitPanelContext.disableAutoDataTransfer) {
|
|
||||||
if (isSelected && data[rowIndex]) {
|
|
||||||
splitPanelContext.setSelectedLeftData(data[rowIndex]);
|
|
||||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 저장:", data[rowIndex]);
|
|
||||||
} else if (!isSelected) {
|
|
||||||
splitPanelContext.setSelectedLeftData(null);
|
|
||||||
console.log("🔗 [InteractiveDataTable] 좌측 선택 데이터 초기화");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [data, splitPanelContext, splitPanelPosition]);
|
|
||||||
|
|
||||||
// 전체 선택/해제 핸들러
|
// 전체 선택/해제 핸들러
|
||||||
const handleSelectAll = useCallback(
|
const handleSelectAll = useCallback(
|
||||||
|
|
@ -1586,7 +1642,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상세 설정에서 옵션 목록 가져오기
|
// 상세 설정에서 옵션 목록 가져오기
|
||||||
const options = detailSettings?.options || [];
|
const options = detailSettings?.options || [];
|
||||||
if (options.length > 0) {
|
if (options.length > 0) {
|
||||||
|
|
@ -1713,7 +1769,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 셀렉트 (동적 import)
|
// 카테고리 셀렉트 (동적 import)
|
||||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
const {
|
||||||
|
CategorySelectComponent,
|
||||||
|
} = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
|
|
@ -1841,7 +1899,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 상세 설정에서 옵션 목록 가져오기
|
// 상세 설정에서 옵션 목록 가져오기
|
||||||
const optionsAdd = detailSettings?.options || [];
|
const optionsAdd = detailSettings?.options || [];
|
||||||
if (optionsAdd.length > 0) {
|
if (optionsAdd.length > 0) {
|
||||||
|
|
@ -2013,7 +2071,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
|
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 셀렉트 (동적 import)
|
// 카테고리 셀렉트 (동적 import)
|
||||||
const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
const {
|
||||||
|
CategorySelectComponent,
|
||||||
|
} = require("@/lib/registry/components/category-select/CategorySelectComponent");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
|
|
@ -2151,8 +2211,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
const actualWebType = getColumnWebType(column.columnName);
|
const actualWebType = getColumnWebType(column.columnName);
|
||||||
|
|
||||||
// 파일 타입 컬럼 처리 (가상 파일 컬럼 포함)
|
// 파일 타입 컬럼 처리 (가상 파일 컬럼 포함)
|
||||||
const isFileColumn =
|
const isFileColumn = actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn;
|
||||||
actualWebType === "file" || column.columnName === "file_path" || column.isVirtualFileColumn;
|
|
||||||
|
|
||||||
// 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리)
|
// 파일 타입 컬럼은 파일 아이콘으로 표시 (컬럼별 파일 관리)
|
||||||
if (isFileColumn && rowData) {
|
if (isFileColumn && rowData) {
|
||||||
|
|
@ -2197,25 +2256,25 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
case "category": {
|
case "category": {
|
||||||
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
// 카테고리 타입: 배지로 표시 (배지 없음 옵션 지원)
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
|
|
||||||
const mapping = categoryMappings[column.columnName];
|
const mapping = categoryMappings[column.columnName];
|
||||||
const categoryData = mapping?.[String(value)];
|
const categoryData = mapping?.[String(value)];
|
||||||
|
|
||||||
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값만 텍스트로 표시
|
// 매핑 데이터가 있으면 라벨과 색상 사용, 없으면 코드값만 텍스트로 표시
|
||||||
const displayLabel = categoryData?.label || String(value);
|
const displayLabel = categoryData?.label || String(value);
|
||||||
const displayColor = categoryData?.color;
|
const displayColor = categoryData?.color;
|
||||||
|
|
||||||
// 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
|
// 배지 없음 옵션: color가 없거나, "none"이거나, 매핑 데이터가 없으면 텍스트만 표시
|
||||||
if (!displayColor || displayColor === "none" || !categoryData) {
|
if (!displayColor || displayColor === "none" || !categoryData) {
|
||||||
return <span className="text-sm">{displayLabel}</span>;
|
return <span className="text-sm">{displayLabel}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: displayColor,
|
backgroundColor: displayColor,
|
||||||
borderColor: displayColor
|
borderColor: displayColor,
|
||||||
}}
|
}}
|
||||||
className="text-white"
|
className="text-white"
|
||||||
>
|
>
|
||||||
{displayLabel}
|
{displayLabel}
|
||||||
|
|
@ -2255,8 +2314,41 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
return String(value);
|
// 카테고리 코드 패턴 감지 (CATEGORY_로 시작하는 값)
|
||||||
|
const strValue = String(value);
|
||||||
|
if (strValue.startsWith("CATEGORY_")) {
|
||||||
|
// 1. categoryMappings에서 해당 코드 검색 (색상 정보 포함)
|
||||||
|
for (const columnName of Object.keys(categoryMappings)) {
|
||||||
|
const mapping = categoryMappings[columnName];
|
||||||
|
const categoryData = mapping?.[strValue];
|
||||||
|
if (categoryData) {
|
||||||
|
// 색상이 있으면 배지로, 없으면 텍스트로 표시
|
||||||
|
if (categoryData.color && categoryData.color !== "none") {
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
style={{
|
||||||
|
backgroundColor: categoryData.color,
|
||||||
|
borderColor: categoryData.color,
|
||||||
|
}}
|
||||||
|
className="text-white"
|
||||||
|
>
|
||||||
|
{categoryData.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <span className="text-sm">{categoryData.label}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. categoryCodeLabels에서 검색 (API로 조회한 라벨)
|
||||||
|
const cachedLabel = categoryCodeLabels[strValue];
|
||||||
|
if (cachedLabel) {
|
||||||
|
return <span className="text-sm">{cachedLabel}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(value);
|
return String(value);
|
||||||
|
|
@ -2392,15 +2484,12 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
{visibleColumns.length > 0 ? (
|
{visibleColumns.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-hidden rounded-lg border border-gray-200/60 bg-white shadow-sm">
|
<div className="overflow-hidden rounded-lg border border-gray-200/60 bg-white shadow-sm">
|
||||||
<Table style={{ tableLayout: 'fixed' }}>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<TableHeader className="bg-gradient-to-b from-muted/50 to-muted border-b-2 border-primary/20">
|
<TableHeader className="from-muted/50 to-muted border-primary/20 border-b-2 bg-gradient-to-b">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
|
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
|
||||||
{component.enableDelete && (
|
{component.enableDelete && (
|
||||||
<TableHead
|
<TableHead className="px-4" style={{ width: "48px", minWidth: "48px", maxWidth: "48px" }}>
|
||||||
className="px-4"
|
|
||||||
style={{ width: '48px', minWidth: '48px', maxWidth: '48px' }}
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedRows.size === data.length && data.length > 0}
|
checked={selectedRows.size === data.length && data.length > 0}
|
||||||
onCheckedChange={handleSelectAll}
|
onCheckedChange={handleSelectAll}
|
||||||
|
|
@ -2409,74 +2498,74 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
)}
|
)}
|
||||||
{visibleColumns.map((column: DataTableColumn, columnIndex) => {
|
{visibleColumns.map((column: DataTableColumn, columnIndex) => {
|
||||||
const columnWidth = columnWidths[column.id];
|
const columnWidth = columnWidths[column.id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead
|
<TableHead
|
||||||
key={column.id}
|
key={column.id}
|
||||||
ref={(el) => (columnRefs.current[column.id] = el)}
|
ref={(el) => (columnRefs.current[column.id] = el)}
|
||||||
className="relative px-4 font-bold text-foreground/90 select-none text-center hover:bg-muted/70 transition-colors"
|
className="text-foreground/90 hover:bg-muted/70 relative px-4 text-center font-bold transition-colors select-none"
|
||||||
style={{
|
style={{
|
||||||
width: columnWidth ? `${columnWidth}px` : undefined,
|
width: columnWidth ? `${columnWidth}px` : undefined,
|
||||||
userSelect: 'none'
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{column.label}
|
{column.label}
|
||||||
{/* 리사이즈 핸들 */}
|
{/* 리사이즈 핸들 */}
|
||||||
{columnIndex < visibleColumns.length - 1 && (
|
{columnIndex < visibleColumns.length - 1 && (
|
||||||
<div
|
<div
|
||||||
className="absolute right-0 top-0 h-full w-2 cursor-col-resize hover:bg-blue-500 z-20"
|
className="absolute top-0 right-0 z-20 h-full w-2 cursor-col-resize hover:bg-blue-500"
|
||||||
style={{ marginRight: '-4px', paddingLeft: '4px', paddingRight: '4px' }}
|
style={{ marginRight: "-4px", paddingLeft: "4px", paddingRight: "4px" }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const thElement = columnRefs.current[column.id];
|
const thElement = columnRefs.current[column.id];
|
||||||
if (!thElement) return;
|
if (!thElement) return;
|
||||||
|
|
||||||
isResizingRef.current = true;
|
isResizingRef.current = true;
|
||||||
|
|
||||||
const startX = e.clientX;
|
const startX = e.clientX;
|
||||||
const startWidth = columnWidth || thElement.offsetWidth;
|
const startWidth = columnWidth || thElement.offsetWidth;
|
||||||
|
|
||||||
// 드래그 중 텍스트 선택 방지
|
// 드래그 중 텍스트 선택 방지
|
||||||
document.body.style.userSelect = 'none';
|
document.body.style.userSelect = "none";
|
||||||
document.body.style.cursor = 'col-resize';
|
document.body.style.cursor = "col-resize";
|
||||||
|
|
||||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
moveEvent.preventDefault();
|
moveEvent.preventDefault();
|
||||||
|
|
||||||
const diff = moveEvent.clientX - startX;
|
const diff = moveEvent.clientX - startX;
|
||||||
const newWidth = Math.max(80, startWidth + diff);
|
const newWidth = Math.max(80, startWidth + diff);
|
||||||
|
|
||||||
// 직접 DOM 스타일 변경 (리렌더링 없음)
|
// 직접 DOM 스타일 변경 (리렌더링 없음)
|
||||||
if (thElement) {
|
if (thElement) {
|
||||||
thElement.style.width = `${newWidth}px`;
|
thElement.style.width = `${newWidth}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
// 최종 너비를 state에 저장
|
// 최종 너비를 state에 저장
|
||||||
if (thElement) {
|
if (thElement) {
|
||||||
const finalWidth = Math.max(80, thElement.offsetWidth);
|
const finalWidth = Math.max(80, thElement.offsetWidth);
|
||||||
setColumnWidths(prev => ({ ...prev, [column.id]: finalWidth }));
|
setColumnWidths((prev) => ({ ...prev, [column.id]: finalWidth }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 텍스트 선택 복원
|
// 텍스트 선택 복원
|
||||||
document.body.style.userSelect = '';
|
document.body.style.userSelect = "";
|
||||||
document.body.style.cursor = '';
|
document.body.style.cursor = "";
|
||||||
|
|
||||||
// 약간의 지연 후 리사이즈 플래그 해제
|
// 약간의 지연 후 리사이즈 플래그 해제
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isResizingRef.current = false;
|
isResizingRef.current = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2504,10 +2593,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
<TableRow key={rowIndex} className="transition-all duration-200 hover:bg-orange-100">
|
<TableRow key={rowIndex} className="transition-all duration-200 hover:bg-orange-100">
|
||||||
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
|
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
|
||||||
{component.enableDelete && (
|
{component.enableDelete && (
|
||||||
<TableCell
|
<TableCell className="px-4" style={{ width: "48px", minWidth: "48px", maxWidth: "48px" }}>
|
||||||
className="px-4"
|
|
||||||
style={{ width: '48px', minWidth: '48px', maxWidth: '48px' }}
|
|
||||||
>
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedRows.has(rowIndex)}
|
checked={selectedRows.has(rowIndex)}
|
||||||
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
|
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
|
||||||
|
|
@ -2517,10 +2603,10 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
{visibleColumns.map((column: DataTableColumn) => {
|
{visibleColumns.map((column: DataTableColumn) => {
|
||||||
const isNumeric = column.widgetType === "number" || column.widgetType === "decimal";
|
const isNumeric = column.widgetType === "number" || column.widgetType === "decimal";
|
||||||
return (
|
return (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={column.id}
|
key={column.id}
|
||||||
className="px-4 text-sm font-medium text-gray-900 whitespace-nowrap overflow-hidden text-ellipsis"
|
className="overflow-hidden px-4 text-sm font-medium text-ellipsis whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: isNumeric ? 'right' : 'left' }}
|
style={{ textAlign: isNumeric ? "right" : "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(row[column.columnName], column, row)}
|
{formatCellValue(row[column.columnName], column, row)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import type { SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||||
interface ScreenContextValue {
|
interface ScreenContextValue {
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
menuObjid?: number; // 메뉴 OBJID (카테고리 값 조회 시 필요)
|
||||||
splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치 (left/right)
|
splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치 (left/right)
|
||||||
|
|
||||||
// 🆕 폼 데이터 (RepeaterFieldGroup 등 컴포넌트 데이터 저장)
|
// 🆕 폼 데이터 (RepeaterFieldGroup 등 컴포넌트 데이터 저장)
|
||||||
|
|
@ -39,6 +40,7 @@ const ScreenContext = createContext<ScreenContextValue | null>(null);
|
||||||
interface ScreenContextProviderProps {
|
interface ScreenContextProviderProps {
|
||||||
screenId?: number;
|
screenId?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
|
menuObjid?: number; // 메뉴 OBJID
|
||||||
splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치
|
splitPanelPosition?: SplitPanelPosition; // 🆕 분할 패널 위치
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +51,7 @@ interface ScreenContextProviderProps {
|
||||||
export function ScreenContextProvider({
|
export function ScreenContextProvider({
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
menuObjid,
|
||||||
splitPanelPosition,
|
splitPanelPosition,
|
||||||
children,
|
children,
|
||||||
}: ScreenContextProviderProps) {
|
}: ScreenContextProviderProps) {
|
||||||
|
|
@ -112,6 +115,7 @@ export function ScreenContextProvider({
|
||||||
() => ({
|
() => ({
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
menuObjid,
|
||||||
splitPanelPosition,
|
splitPanelPosition,
|
||||||
formData,
|
formData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
|
|
@ -127,6 +131,7 @@ export function ScreenContextProvider({
|
||||||
[
|
[
|
||||||
screenId,
|
screenId,
|
||||||
tableName,
|
tableName,
|
||||||
|
menuObjid,
|
||||||
splitPanelPosition,
|
splitPanelPosition,
|
||||||
formData,
|
formData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const [rightCategoryMappings, setRightCategoryMappings] = useState<
|
const [rightCategoryMappings, setRightCategoryMappings] = useState<
|
||||||
Record<string, Record<string, { label: string; color?: string }>>
|
Record<string, Record<string, { label: string; color?: string }>>
|
||||||
>({}); // 우측 카테고리 매핑
|
>({}); // 우측 카테고리 매핑
|
||||||
|
|
||||||
|
// 카테고리 코드 라벨 캐시 (CATEGORY_* 코드 -> 라벨)
|
||||||
|
const [categoryCodeLabels, setCategoryCodeLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// 추가 모달 상태
|
// 추가 모달 상태
|
||||||
|
|
@ -713,6 +717,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 카테고리 코드 패턴 감지 (CATEGORY_로 시작하는 값)
|
||||||
|
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
||||||
|
const cachedLabel = categoryCodeLabels[value];
|
||||||
|
if (cachedLabel) {
|
||||||
|
return <span className="text-sm">{cachedLabel}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체)
|
// 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체)
|
||||||
if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) {
|
if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) {
|
||||||
return formatDateValue(value, "YYYY-MM-DD");
|
return formatDateValue(value, "YYYY-MM-DD");
|
||||||
|
|
@ -734,7 +746,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
// 일반 값
|
// 일반 값
|
||||||
return String(value);
|
return String(value);
|
||||||
},
|
},
|
||||||
[formatDateValue, formatNumberValue],
|
[formatDateValue, formatNumberValue, categoryCodeLabels],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 좌측 데이터 로드
|
// 좌측 데이터 로드
|
||||||
|
|
@ -1079,6 +1091,49 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 🆕 카테고리 코드 라벨 로드 (rightData 변경 시)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCategoryCodeLabels = async () => {
|
||||||
|
if (!rightData) return;
|
||||||
|
|
||||||
|
const categoryCodes = new Set<string>();
|
||||||
|
|
||||||
|
// rightData가 배열인 경우 (조인 모드)
|
||||||
|
const dataArray = Array.isArray(rightData) ? rightData : [rightData];
|
||||||
|
|
||||||
|
dataArray.forEach((row: Record<string, any>) => {
|
||||||
|
if (row) {
|
||||||
|
Object.values(row).forEach((value) => {
|
||||||
|
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
||||||
|
categoryCodes.add(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
|
||||||
|
const newCodes = Array.from(categoryCodes).filter((code) => !categoryCodeLabels[code]);
|
||||||
|
|
||||||
|
if (newCodes.length > 0) {
|
||||||
|
try {
|
||||||
|
console.log("🏷️ [SplitPanel] 카테고리 코드 라벨 조회:", newCodes);
|
||||||
|
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
console.log("🏷️ [SplitPanel] 카테고리 라벨 조회 결과:", response.data.data);
|
||||||
|
setCategoryCodeLabels((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...response.data.data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("카테고리 라벨 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCategoryCodeLabels();
|
||||||
|
}, [rightData]);
|
||||||
|
|
||||||
// 🆕 추가 탭 데이터 로딩 함수
|
// 🆕 추가 탭 데이터 로딩 함수
|
||||||
const loadTabData = useCallback(
|
const loadTabData = useCallback(
|
||||||
async (tabIndex: number, leftItem: any) => {
|
async (tabIndex: number, leftItem: any) => {
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,9 @@ export function TableSectionRenderer({
|
||||||
// 소스 테이블의 컬럼 라벨 (API에서 동적 로드)
|
// 소스 테이블의 컬럼 라벨 (API에서 동적 로드)
|
||||||
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 카테고리 타입 컬럼의 옵션 (column.type === "category")
|
||||||
|
const [categoryOptionsMap, setCategoryOptionsMap] = useState<Record<string, { value: string; label: string }[]>>({});
|
||||||
|
|
||||||
// 외부 데이터(groupedData) 처리: 데이터 전달 모달열기 액션으로 전달받은 데이터를 초기 테이블 데이터로 설정
|
// 외부 데이터(groupedData) 처리: 데이터 전달 모달열기 액션으로 전달받은 데이터를 초기 테이블 데이터로 설정
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 외부 데이터 소스가 활성화되지 않았거나, groupedData가 없으면 스킵
|
// 외부 데이터 소스가 활성화되지 않았거나, groupedData가 없으면 스킵
|
||||||
|
|
@ -511,6 +514,46 @@ export function TableSectionRenderer({
|
||||||
loadColumnLabels();
|
loadColumnLabels();
|
||||||
}, [tableConfig.source.tableName, tableConfig.source.columnLabels]);
|
}, [tableConfig.source.tableName, tableConfig.source.columnLabels]);
|
||||||
|
|
||||||
|
// 카테고리 타입 컬럼의 옵션 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCategoryOptions = async () => {
|
||||||
|
const sourceTableName = tableConfig.source.tableName;
|
||||||
|
if (!sourceTableName) return;
|
||||||
|
if (!tableConfig.columns) return;
|
||||||
|
|
||||||
|
// 카테고리 타입인 컬럼만 필터링
|
||||||
|
const categoryColumns = tableConfig.columns.filter((col) => col.type === "category");
|
||||||
|
if (categoryColumns.length === 0) return;
|
||||||
|
|
||||||
|
const newOptionsMap: Record<string, { value: string; label: string }[]> = {};
|
||||||
|
|
||||||
|
for (const col of categoryColumns) {
|
||||||
|
// 소스 필드 또는 필드명으로 카테고리 값 조회
|
||||||
|
const actualColumnName = col.sourceField || col.field;
|
||||||
|
if (!actualColumnName) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
||||||
|
const result = await getCategoryValues(sourceTableName, actualColumnName, false);
|
||||||
|
|
||||||
|
if (result && result.success && Array.isArray(result.data)) {
|
||||||
|
const options = result.data.map((item: any) => ({
|
||||||
|
value: item.valueCode || item.value_code || item.value || "",
|
||||||
|
label: item.valueLabel || item.displayLabel || item.display_label || item.label || item.valueCode || item.value_code || item.value || "",
|
||||||
|
}));
|
||||||
|
newOptionsMap[col.field] = options;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`카테고리 옵션 로드 실패 (${col.field}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCategoryOptionsMap((prev) => ({ ...prev, ...newOptionsMap }));
|
||||||
|
};
|
||||||
|
|
||||||
|
loadCategoryOptions();
|
||||||
|
}, [tableConfig.source.tableName, tableConfig.columns]);
|
||||||
|
|
||||||
// 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우)
|
// 조건부 테이블: 동적 옵션 로드 (optionSource 설정이 있는 경우)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isConditionalMode) return;
|
if (!isConditionalMode) return;
|
||||||
|
|
@ -952,9 +995,15 @@ export function TableSectionRenderer({
|
||||||
baseColumn.selectOptions = dynamicSelectOptionsMap[col.field];
|
baseColumn.selectOptions = dynamicSelectOptionsMap[col.field];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 카테고리 타입인 경우 옵션 적용 및 select 타입으로 변환
|
||||||
|
if (col.type === "category" && categoryOptionsMap[col.field]) {
|
||||||
|
baseColumn.type = "select"; // RepeaterTable에서 select로 렌더링
|
||||||
|
baseColumn.selectOptions = categoryOptionsMap[col.field];
|
||||||
|
}
|
||||||
|
|
||||||
return baseColumn;
|
return baseColumn;
|
||||||
});
|
});
|
||||||
}, [tableConfig.columns, dynamicSelectOptionsMap]);
|
}, [tableConfig.columns, dynamicSelectOptionsMap, categoryOptionsMap]);
|
||||||
|
|
||||||
// 원본 계산 규칙 (조건부 계산 포함)
|
// 원본 계산 규칙 (조건부 계산 포함)
|
||||||
const originalCalculationRules: TableCalculationRule[] = useMemo(
|
const originalCalculationRules: TableCalculationRule[] = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -308,12 +308,29 @@ export function UniversalFormModalConfigPanel({
|
||||||
column_comment?: string;
|
column_comment?: string;
|
||||||
inputType?: string;
|
inputType?: string;
|
||||||
input_type?: string;
|
input_type?: string;
|
||||||
}) => ({
|
isNullable?: string;
|
||||||
name: c.columnName || c.column_name || "",
|
is_nullable?: string;
|
||||||
type: c.dataType || c.data_type || "text",
|
}) => {
|
||||||
label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "",
|
const colName = c.columnName || c.column_name || "";
|
||||||
inputType: c.inputType || c.input_type || "text",
|
const dataType = c.dataType || c.data_type || "text";
|
||||||
}),
|
const inputType = c.inputType || c.input_type || "text";
|
||||||
|
const displayName = c.displayName || c.columnComment || c.column_comment || colName;
|
||||||
|
const isNullable = c.isNullable || c.is_nullable || "YES";
|
||||||
|
|
||||||
|
return {
|
||||||
|
// camelCase (기존 호환성)
|
||||||
|
name: colName,
|
||||||
|
type: dataType,
|
||||||
|
label: displayName,
|
||||||
|
inputType: inputType,
|
||||||
|
// snake_case (TableSectionSettingsModal 호환성)
|
||||||
|
column_name: colName,
|
||||||
|
data_type: dataType,
|
||||||
|
is_nullable: isNullable,
|
||||||
|
comment: displayName,
|
||||||
|
input_type: inputType,
|
||||||
|
};
|
||||||
|
},
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,12 @@ interface TableColumnSettingsModalProps {
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
column: TableColumnConfig;
|
column: TableColumnConfig;
|
||||||
sourceTableName: string; // 소스 테이블명
|
sourceTableName: string; // 소스 테이블명
|
||||||
sourceTableColumns: { column_name: string; data_type: string; comment?: string }[];
|
sourceTableColumns: { column_name: string; data_type: string; comment?: string; input_type?: string }[];
|
||||||
formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함)
|
formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함)
|
||||||
sections: { id: string; title: string }[]; // 섹션 목록
|
sections: { id: string; title: string }[]; // 섹션 목록
|
||||||
onSave: (updatedColumn: TableColumnConfig) => void;
|
onSave: (updatedColumn: TableColumnConfig) => void;
|
||||||
tables: { table_name: string; comment?: string }[];
|
tables: { table_name: string; comment?: string }[];
|
||||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>;
|
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>;
|
||||||
onLoadTableColumns: (tableName: string) => void;
|
onLoadTableColumns: (tableName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,6 +103,18 @@ export function TableColumnSettingsModal({
|
||||||
return tableColumns[externalTableName] || [];
|
return tableColumns[externalTableName] || [];
|
||||||
}, [tableColumns, externalTableName]);
|
}, [tableColumns, externalTableName]);
|
||||||
|
|
||||||
|
// 소스 필드 기준으로 카테고리 타입인지 확인
|
||||||
|
const actualSourceField = localColumn.sourceField || localColumn.field;
|
||||||
|
const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField);
|
||||||
|
const isCategoryColumn = sourceColumnInfo?.input_type === "category";
|
||||||
|
|
||||||
|
// 카테고리 컬럼인 경우 타입을 자동으로 category로 설정
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCategoryColumn && localColumn.type !== "category") {
|
||||||
|
updateColumn({ type: "category" });
|
||||||
|
}
|
||||||
|
}, [isCategoryColumn, localColumn.type]);
|
||||||
|
|
||||||
// 컬럼 업데이트 함수
|
// 컬럼 업데이트 함수
|
||||||
const updateColumn = (updates: Partial<TableColumnConfig>) => {
|
const updateColumn = (updates: Partial<TableColumnConfig>) => {
|
||||||
setLocalColumn((prev) => ({ ...prev, ...updates }));
|
setLocalColumn((prev) => ({ ...prev, ...updates }));
|
||||||
|
|
@ -574,10 +586,11 @@ export function TableColumnSettingsModal({
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">타입</Label>
|
<Label className="text-xs">타입</Label>
|
||||||
<Select
|
<Select
|
||||||
value={localColumn.type}
|
value={isCategoryColumn ? "category" : localColumn.type}
|
||||||
onValueChange={(value: any) => updateColumn({ type: value })}
|
onValueChange={(value: any) => updateColumn({ type: value })}
|
||||||
|
disabled={isCategoryColumn}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs mt-1">
|
<SelectTrigger className={cn("h-8 text-xs mt-1", isCategoryColumn && "opacity-70 cursor-not-allowed")}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -588,6 +601,9 @@ export function TableColumnSettingsModal({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{isCategoryColumn && (
|
||||||
|
<p className="text-[10px] text-blue-600 mt-0.5">테이블 타입 관리에서 카테고리로 설정됨</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">너비</Label>
|
<Label className="text-xs">너비</Label>
|
||||||
|
|
|
||||||
|
|
@ -706,15 +706,15 @@ interface ColumnSettingItemProps {
|
||||||
col: TableColumnConfig;
|
col: TableColumnConfig;
|
||||||
index: number;
|
index: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[];
|
saveTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[];
|
||||||
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
displayColumns: string[]; // 검색 설정에서 선택한 표시 컬럼 목록
|
||||||
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 소스 테이블 컬럼
|
sourceTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 소스 테이블 컬럼
|
||||||
sourceTableName: string; // 소스 테이블명
|
sourceTableName: string; // 소스 테이블명
|
||||||
externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string }[]; // 외부 데이터 테이블 컬럼
|
externalTableColumns: { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]; // 외부 데이터 테이블 컬럼
|
||||||
externalTableName?: string; // 외부 데이터 테이블명
|
externalTableName?: string; // 외부 데이터 테이블명
|
||||||
externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부
|
externalDataEnabled?: boolean; // 외부 데이터 소스 활성화 여부
|
||||||
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
|
tables: { table_name: string; comment?: string }[]; // 전체 테이블 목록
|
||||||
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string }[]>; // 테이블별 컬럼
|
tableColumns: Record<string, { column_name: string; data_type: string; is_nullable: string; comment?: string; input_type?: string }[]>; // 테이블별 컬럼
|
||||||
sections: { id: string; title: string }[]; // 섹션 목록
|
sections: { id: string; title: string }[]; // 섹션 목록
|
||||||
formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록
|
formFields: { columnName: string; label: string; sectionId?: string }[]; // 폼 필드 목록
|
||||||
tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용
|
tableConfig: TableSectionConfig; // 현재 행 필드 목록 표시용
|
||||||
|
|
@ -755,6 +755,18 @@ function ColumnSettingItem({
|
||||||
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
|
const [parentFieldSearchOpen, setParentFieldSearchOpen] = useState(false);
|
||||||
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
const [lookupTableOpenMap, setLookupTableOpenMap] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// 소스 필드 기준으로 카테고리 타입인지 확인
|
||||||
|
const actualSourceField = col.sourceField || col.field;
|
||||||
|
const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField);
|
||||||
|
const isCategoryColumn = sourceColumnInfo?.input_type === "category";
|
||||||
|
|
||||||
|
// 카테고리 컬럼인 경우 타입을 자동으로 category로 설정
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCategoryColumn && col.type !== "category") {
|
||||||
|
onUpdate({ type: "category" });
|
||||||
|
}
|
||||||
|
}, [isCategoryColumn, col.type, onUpdate]);
|
||||||
|
|
||||||
// 조회 옵션 추가
|
// 조회 옵션 추가
|
||||||
const addLookupOption = () => {
|
const addLookupOption = () => {
|
||||||
const newOption: LookupOption = {
|
const newOption: LookupOption = {
|
||||||
|
|
@ -1117,8 +1129,12 @@ function ColumnSettingItem({
|
||||||
{/* 타입 */}
|
{/* 타입 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">타입</Label>
|
<Label className="text-xs">타입</Label>
|
||||||
<Select value={col.type} onValueChange={(value: any) => onUpdate({ type: value })}>
|
<Select
|
||||||
<SelectTrigger className="h-8 text-xs mt-1">
|
value={isCategoryColumn ? "category" : col.type}
|
||||||
|
onValueChange={(value: any) => onUpdate({ type: value })}
|
||||||
|
disabled={isCategoryColumn}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={cn("h-8 text-xs mt-1", isCategoryColumn && "opacity-70 cursor-not-allowed")}>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -1129,6 +1145,9 @@ function ColumnSettingItem({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{isCategoryColumn && (
|
||||||
|
<p className="text-[10px] text-blue-600 mt-0.5">테이블 타입 관리에서 카테고리로 설정됨</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 너비 */}
|
{/* 너비 */}
|
||||||
|
|
|
||||||
|
|
@ -899,6 +899,7 @@ export const TABLE_COLUMN_TYPE_OPTIONS = [
|
||||||
{ value: "number", label: "숫자" },
|
{ value: "number", label: "숫자" },
|
||||||
{ value: "date", label: "날짜" },
|
{ value: "date", label: "날짜" },
|
||||||
{ value: "select", label: "선택(드롭다운)" },
|
{ value: "select", label: "선택(드롭다운)" },
|
||||||
|
{ value: "category", label: "카테고리" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// 값 매핑 타입 옵션
|
// 값 매핑 타입 옵션
|
||||||
|
|
|
||||||
|
|
@ -4835,19 +4835,7 @@ export class ButtonActionExecutor {
|
||||||
*/
|
*/
|
||||||
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
private static async handleExcelUpload(config: ButtonActionConfig, context: ButtonActionContext): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
console.log("📤 엑셀 업로드 모달 열기:", {
|
// 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
|
||||||
config,
|
|
||||||
context,
|
|
||||||
userId: context.userId,
|
|
||||||
tableName: context.tableName,
|
|
||||||
screenId: context.screenId,
|
|
||||||
// 채번 설정 디버깅
|
|
||||||
numberingRuleId: config.excelNumberingRuleId,
|
|
||||||
numberingTargetColumn: config.excelNumberingTargetColumn,
|
|
||||||
afterUploadFlows: config.excelAfterUploadFlows,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 마스터-디테일 구조 확인 (화면에 분할 패널이 있으면 자동 감지)
|
|
||||||
let isMasterDetail = false;
|
let isMasterDetail = false;
|
||||||
let masterDetailRelation: any = null;
|
let masterDetailRelation: any = null;
|
||||||
let masterDetailExcelConfig: any = undefined;
|
let masterDetailExcelConfig: any = undefined;
|
||||||
|
|
@ -4870,6 +4858,10 @@ export class ButtonActionExecutor {
|
||||||
detailTable: relationResponse.data.detailTable,
|
detailTable: relationResponse.data.detailTable,
|
||||||
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||||
detailFkColumn: relationResponse.data.detailFkColumn,
|
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||||
|
// 채번 규칙 ID 추가 (excelNumberingRuleId를 numberingRuleId로 매핑)
|
||||||
|
numberingRuleId: config.masterDetailExcel.numberingRuleId || config.excelNumberingRuleId,
|
||||||
|
// 업로드 후 제어 설정 추가
|
||||||
|
afterUploadFlows: config.masterDetailExcel.afterUploadFlows || config.excelAfterUploadFlows,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// 버튼 설정이 없으면 분할 패널 정보만 사용
|
// 버튼 설정이 없으면 분할 패널 정보만 사용
|
||||||
|
|
@ -4879,6 +4871,10 @@ export class ButtonActionExecutor {
|
||||||
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||||
detailFkColumn: relationResponse.data.detailFkColumn,
|
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||||
simpleMode: true, // 기본값으로 간단 모드 사용
|
simpleMode: true, // 기본값으로 간단 모드 사용
|
||||||
|
// 채번 규칙 ID 추가 (excelNumberingRuleId 사용)
|
||||||
|
numberingRuleId: config.excelNumberingRuleId,
|
||||||
|
// 업로드 후 제어 설정 추가
|
||||||
|
afterUploadFlows: config.excelAfterUploadFlows,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue