mhkim-node #412

Merged
kjs merged 25 commits from mhkim-node into main 2026-03-12 08:25:45 +09:00
2 changed files with 136 additions and 38 deletions
Showing only changes of commit cc61ef3ff4 - Show all commits

View File

@ -36,7 +36,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { toast } from "sonner";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { apiClient } from "@/lib/api/client";
import { getCategoryValues } from "@/lib/api/tableCategoryValue";
import { getCategoryValues, getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue";
export interface SplitPanelLayout2ComponentProps extends ComponentRendererProps {
// 추가 props
@ -1354,6 +1354,40 @@ export const SplitPanelLayout2Component: React.FC<SplitPanelLayout2ComponentProp
loadCategoryLabels();
}, [isDesignMode, config.leftPanel?.tableName, config.rightPanel?.tableName, config.leftPanel?.displayColumns, config.rightPanel?.displayColumns, config.rightPanel?.tabConfig?.tabSourceColumn, config.leftPanel?.tabConfig?.tabSourceColumn]);
// 데이터 로드 후 미해결 카테고리 코드를 batch API로 변환
useEffect(() => {
if (isDesignMode) return;
const allData = [...leftData, ...rightData];
if (allData.length === 0) return;
const unresolvedCodes = new Set<string>();
const checkValue = (v: unknown) => {
if (typeof v === "string" && (v.startsWith("CAT_") || v.startsWith("CATEGORY_"))) {
if (!categoryLabelMap[v]) unresolvedCodes.add(v);
}
};
for (const item of allData) {
for (const val of Object.values(item)) {
if (Array.isArray(val)) {
val.forEach(checkValue);
} else {
checkValue(val);
}
}
}
if (unresolvedCodes.size === 0) return;
const resolveMissingLabels = async () => {
const result = await getCategoryLabelsByCodes(Array.from(unresolvedCodes));
if (result.success && result.data && Object.keys(result.data).length > 0) {
setCategoryLabelMap((prev) => ({ ...prev, ...result.data }));
}
};
resolveMissingLabels();
}, [isDesignMode, leftData, rightData, categoryLabelMap]);
// 컴포넌트 언마운트 시 DataProvider 해제
useEffect(() => {
return () => {

View File

@ -1001,23 +1001,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={{
@ -1031,6 +1032,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");
@ -1981,43 +2005,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);
}
}
@ -2028,7 +2068,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
};
loadLeftCategoryMappings();
}, [componentConfig.leftPanel?.tableName, isDesignMode]);
}, [componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, isDesignMode]);
// 우측 테이블 카테고리 매핑 로드 (조인된 테이블 포함)
useEffect(() => {
@ -3720,9 +3760,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,
};
});
@ -3734,10 +3787,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);