Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -1098,23 +1098,24 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
return formatNumberValue(value, format);
|
||||
}
|
||||
|
||||
// 🆕 카테고리 매핑 찾기 (여러 키 형태 시도)
|
||||
// 카테고리 매핑 찾기 (여러 키 형태 시도)
|
||||
// 1. 전체 컬럼명 (예: "item_info.material")
|
||||
// 2. 컬럼명만 (예: "material")
|
||||
// 3. 전역 폴백: 모든 매핑에서 value 검색
|
||||
let mapping = categoryMappings[columnName];
|
||||
|
||||
if (!mapping && columnName.includes(".")) {
|
||||
// 조인된 컬럼의 경우 컬럼명만으로 다시 시도
|
||||
const simpleColumnName = columnName.split(".").pop() || columnName;
|
||||
mapping = categoryMappings[simpleColumnName];
|
||||
}
|
||||
|
||||
if (mapping && mapping[String(value)]) {
|
||||
const categoryData = mapping[String(value)];
|
||||
const displayLabel = categoryData.label || String(value);
|
||||
const strValue = String(value);
|
||||
|
||||
if (mapping && mapping[strValue]) {
|
||||
const categoryData = mapping[strValue];
|
||||
const displayLabel = categoryData.label || strValue;
|
||||
const displayColor = categoryData.color || "#64748b";
|
||||
|
||||
// 배지로 표시
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
@@ -1128,6 +1129,29 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
);
|
||||
}
|
||||
|
||||
// 전역 폴백: 컬럼명으로 매핑을 못 찾았을 때, 전체 매핑에서 값 검색
|
||||
if (!mapping && (strValue.startsWith("CAT_") || strValue.startsWith("CATEGORY_"))) {
|
||||
for (const key of Object.keys(categoryMappings)) {
|
||||
const m = categoryMappings[key];
|
||||
if (m && m[strValue]) {
|
||||
const categoryData = m[strValue];
|
||||
const displayLabel = categoryData.label || strValue;
|
||||
const displayColor = categoryData.color || "#64748b";
|
||||
return (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: displayColor,
|
||||
borderColor: displayColor,
|
||||
}}
|
||||
className="text-white"
|
||||
>
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체)
|
||||
if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) {
|
||||
return formatDateValue(value, "YYYY-MM-DD");
|
||||
@@ -1247,10 +1271,44 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
console.log("🔗 [분할패널] API 응답 첫 번째 데이터:", result.data[0]);
|
||||
}
|
||||
|
||||
// 좌측 패널 dataFilter 클라이언트 사이드 적용
|
||||
let filteredLeftData = result.data || [];
|
||||
const leftDataFilter = componentConfig.leftPanel?.dataFilter;
|
||||
if (leftDataFilter?.enabled && leftDataFilter.filters?.length > 0) {
|
||||
const matchFn = leftDataFilter.matchType === "any" ? "some" : "every";
|
||||
filteredLeftData = filteredLeftData.filter((item: any) => {
|
||||
return leftDataFilter.filters[matchFn]((cond: any) => {
|
||||
const val = item[cond.columnName];
|
||||
switch (cond.operator) {
|
||||
case "equals":
|
||||
return val === cond.value;
|
||||
case "not_equals":
|
||||
return val !== cond.value;
|
||||
case "in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return arr.includes(val);
|
||||
}
|
||||
case "not_in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return !arr.includes(val);
|
||||
}
|
||||
case "contains":
|
||||
return String(val || "").includes(String(cond.value));
|
||||
case "is_null":
|
||||
return val === null || val === undefined || val === "";
|
||||
case "is_not_null":
|
||||
return val !== null && val !== undefined && val !== "";
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 가나다순 정렬 (좌측 패널의 표시 컬럼 기준)
|
||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||
if (leftColumn && result.data.length > 0) {
|
||||
result.data.sort((a, b) => {
|
||||
if (leftColumn && filteredLeftData.length > 0) {
|
||||
filteredLeftData.sort((a, b) => {
|
||||
const aValue = String(a[leftColumn] || "");
|
||||
const bValue = String(b[leftColumn] || "");
|
||||
return aValue.localeCompare(bValue, "ko-KR");
|
||||
@@ -1258,7 +1316,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}
|
||||
|
||||
// 계층 구조 빌드
|
||||
const hierarchicalData = buildHierarchy(result.data);
|
||||
const hierarchicalData = buildHierarchy(filteredLeftData);
|
||||
setLeftData(hierarchicalData);
|
||||
} catch (error) {
|
||||
console.error("좌측 데이터 로드 실패:", error);
|
||||
@@ -1317,7 +1375,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
case "equals":
|
||||
return value === cond.value;
|
||||
case "notEquals":
|
||||
case "not_equals":
|
||||
return value !== cond.value;
|
||||
case "in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return arr.includes(value);
|
||||
}
|
||||
case "not_in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return !arr.includes(value);
|
||||
}
|
||||
case "contains":
|
||||
return String(value || "").includes(String(cond.value));
|
||||
case "is_null":
|
||||
@@ -1634,7 +1701,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
case "equals":
|
||||
return value === cond.value;
|
||||
case "notEquals":
|
||||
case "not_equals":
|
||||
return value !== cond.value;
|
||||
case "in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return arr.includes(value);
|
||||
}
|
||||
case "not_in": {
|
||||
const arr = Array.isArray(cond.value) ? cond.value : [cond.value];
|
||||
return !arr.includes(value);
|
||||
}
|
||||
case "contains":
|
||||
return String(value || "").includes(String(cond.value));
|
||||
case "is_null":
|
||||
@@ -2026,43 +2102,59 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
loadRightTableColumns();
|
||||
}, [componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.additionalTabs, isDesignMode]);
|
||||
|
||||
// 좌측 테이블 카테고리 매핑 로드
|
||||
// 좌측 테이블 카테고리 매핑 로드 (조인된 테이블 포함)
|
||||
useEffect(() => {
|
||||
const loadLeftCategoryMappings = async () => {
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (!leftTableName || isDesignMode) return;
|
||||
|
||||
try {
|
||||
// 1. 컬럼 메타 정보 조회
|
||||
const columnsResponse = await tableTypeApi.getColumns(leftTableName);
|
||||
const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category");
|
||||
|
||||
if (categoryColumns.length === 0) {
|
||||
setLeftCategoryMappings({});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 각 카테고리 컬럼에 대한 값 조회
|
||||
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
|
||||
const tablesToLoad = new Set<string>([leftTableName]);
|
||||
|
||||
for (const col of categoryColumns) {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
// 좌측 패널 컬럼 설정에서 조인된 테이블 추출
|
||||
const leftColumns = componentConfig.leftPanel?.columns || [];
|
||||
leftColumns.forEach((col: any) => {
|
||||
const colName = col.name || col.columnName;
|
||||
if (colName && colName.includes(".")) {
|
||||
const joinTableName = colName.split(".")[0];
|
||||
tablesToLoad.add(joinTableName);
|
||||
}
|
||||
});
|
||||
|
||||
// 각 테이블에 대해 카테고리 매핑 로드
|
||||
for (const tableName of tablesToLoad) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values?includeInactive=true`);
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category");
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
valueMap[item.value_code || item.valueCode] = {
|
||||
label: item.value_label || item.valueLabel,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
mappings[columnName] = valueMap;
|
||||
console.log(`✅ 좌측 카테고리 매핑 로드 [${columnName}]:`, valueMap);
|
||||
for (const col of categoryColumns) {
|
||||
const columnName = col.columnName || col.column_name;
|
||||
try {
|
||||
const response = await apiClient.get(`/table-categories/${tableName}/${columnName}/values?includeInactive=true`);
|
||||
|
||||
if (response.data.success && response.data.data) {
|
||||
const valueMap: Record<string, { label: string; color?: string }> = {};
|
||||
response.data.data.forEach((item: any) => {
|
||||
valueMap[item.value_code || item.valueCode] = {
|
||||
label: item.value_label || item.valueLabel,
|
||||
color: item.color,
|
||||
};
|
||||
});
|
||||
|
||||
// 조인된 테이블은 "테이블명.컬럼명" 형태로도 저장
|
||||
const mappingKey = tableName === leftTableName ? columnName : `${tableName}.${columnName}`;
|
||||
mappings[mappingKey] = valueMap;
|
||||
|
||||
// 컬럼명만으로도 접근 가능하도록 추가 저장
|
||||
mappings[columnName] = { ...(mappings[columnName] || {}), ...valueMap };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`좌측 카테고리 값 조회 실패 [${tableName}.${columnName}]:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`좌측 카테고리 값 조회 실패 [${columnName}]:`, error);
|
||||
console.error(`좌측 카테고리 테이블 컬럼 조회 실패 [${tableName}]:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2073,7 +2165,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
};
|
||||
|
||||
loadLeftCategoryMappings();
|
||||
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
|
||||
}, [componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, isDesignMode]);
|
||||
|
||||
// 우측 테이블 카테고리 매핑 로드 (조인된 테이블 포함)
|
||||
useEffect(() => {
|
||||
@@ -3740,9 +3832,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
displayFields = configuredColumns.slice(0, 2).map((col: any) => {
|
||||
const colName = typeof col === "string" ? col : col.name || col.columnName;
|
||||
const colLabel = typeof col === "object" ? col.label : leftColumnLabels[colName] || colName;
|
||||
const rawValue = getEntityJoinValue(item, colName);
|
||||
// 카테고리 매핑이 있으면 라벨로 변환
|
||||
let displayValue = rawValue;
|
||||
if (rawValue != null && rawValue !== "") {
|
||||
const strVal = String(rawValue);
|
||||
let mapping = leftCategoryMappings[colName];
|
||||
if (!mapping && colName.includes(".")) {
|
||||
mapping = leftCategoryMappings[colName.split(".").pop() || colName];
|
||||
}
|
||||
if (mapping && mapping[strVal]) {
|
||||
displayValue = mapping[strVal].label;
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: colLabel,
|
||||
value: item[colName],
|
||||
value: displayValue,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3754,10 +3859,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const keys = Object.keys(item).filter(
|
||||
(k) => k !== "id" && k !== "ID" && k !== "children" && k !== "level" && shouldShowField(k),
|
||||
);
|
||||
displayFields = keys.slice(0, 2).map((key) => ({
|
||||
label: leftColumnLabels[key] || key,
|
||||
value: item[key],
|
||||
}));
|
||||
displayFields = keys.slice(0, 2).map((key) => {
|
||||
const rawValue = item[key];
|
||||
let displayValue = rawValue;
|
||||
if (rawValue != null && rawValue !== "") {
|
||||
const strVal = String(rawValue);
|
||||
const mapping = leftCategoryMappings[key];
|
||||
if (mapping && mapping[strVal]) {
|
||||
displayValue = mapping[strVal].label;
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: leftColumnLabels[key] || key,
|
||||
value: displayValue,
|
||||
};
|
||||
});
|
||||
|
||||
if (index === 0) {
|
||||
console.log(" ⚠️ 설정된 컬럼 없음, 자동 선택:", displayFields);
|
||||
|
||||
@@ -1955,7 +1955,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
|
||||
{/* ===== 기본 설정 모달 ===== */}
|
||||
<Dialog open={activeModal === "basic"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">기본 설정</DialogTitle>
|
||||
<DialogDescription className="text-xs">패널 관계 타입 및 레이아웃을 설정합니다</DialogDescription>
|
||||
@@ -2033,7 +2033,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
|
||||
{/* ===== 좌측 패널 모달 ===== */}
|
||||
<Dialog open={activeModal === "left"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">좌측 패널 설정</DialogTitle>
|
||||
<DialogDescription className="text-xs">마스터 데이터 표시 및 필터링을 설정합니다</DialogDescription>
|
||||
@@ -2715,7 +2715,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
|
||||
{/* ===== 우측 패널 모달 ===== */}
|
||||
<Dialog open={activeModal === "right"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">우측 패널 설정</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
@@ -3651,7 +3651,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
|
||||
{/* ===== 추가 탭 모달 ===== */}
|
||||
<Dialog open={activeModal === "tabs"} onOpenChange={(open) => !open && setActiveModal(null)}>
|
||||
<DialogContent className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogContent container={null} className="max-h-[85vh] max-w-2xl overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base">추가 탭 설정</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
|
||||
Reference in New Issue
Block a user