feature/screen-management #55

Merged
kjs merged 10 commits from feature/screen-management into main 2025-09-24 10:50:19 +09:00
9 changed files with 293 additions and 51 deletions
Showing only changes of commit de6c7a8008 - Show all commits

View File

@ -74,7 +74,10 @@ export class EntityJoinController {
typeof screenEntityConfigs === "string"
? JSON.parse(screenEntityConfigs)
: screenEntityConfigs;
logger.info("화면별 엔티티 설정 파싱 완료:", parsedScreenEntityConfigs);
logger.info(
"화면별 엔티티 설정 파싱 완료:",
parsedScreenEntityConfigs
);
} catch (error) {
logger.warn("화면별 엔티티 설정 파싱 오류:", error);
parsedScreenEntityConfigs = {};
@ -365,14 +368,16 @@ export class EntityJoinController {
);
// 현재 display_column으로 사용 중인 컬럼 제외
const currentDisplayColumn =
config.displayColumn || config.displayColumns[0];
const availableColumns = columns.filter(
(col) => col.columnName !== config.displayColumn
(col) => col.columnName !== currentDisplayColumn
);
return {
joinConfig: config,
tableName: config.referenceTable,
currentDisplayColumn: config.displayColumn,
currentDisplayColumn: currentDisplayColumn,
availableColumns: availableColumns.map((col) => ({
columnName: col.columnName,
columnLabel: col.displayName || col.columnName,
@ -390,7 +395,8 @@ export class EntityJoinController {
return {
joinConfig: config,
tableName: config.referenceTable,
currentDisplayColumn: config.displayColumn,
currentDisplayColumn:
config.displayColumn || config.displayColumns[0],
availableColumns: [],
error: error instanceof Error ? error.message : "Unknown error",
};

View File

@ -20,7 +20,7 @@ export class EntityJoinService {
* @param screenEntityConfigs ()
*/
async detectEntityJoins(
tableName: string,
tableName: string,
screenEntityConfigs?: Record<string, any>
): Promise<EntityJoinConfig[]> {
try {
@ -57,7 +57,7 @@ export class EntityJoinService {
const screenConfig = screenEntityConfigs?.[column.column_name];
let displayColumns: string[] = [];
let separator = " - ";
if (screenConfig && screenConfig.displayColumns) {
// 화면에서 설정된 표시 컬럼들 사용
displayColumns = screenConfig.displayColumns;
@ -66,8 +66,11 @@ export class EntityJoinService {
// 기존 설정된 단일 표시 컬럼 사용
displayColumns = [column.display_column];
} else {
// 기본값: reference_column 사용
displayColumns = [column.reference_column];
// 화면에서 설정하도록 빈 배열로 초기화 (테이블 타입 관리에서 표시 컬럼 설정 제거)
displayColumns = [];
console.log(
`🎯 표시 컬럼을 화면에서 설정하도록 초기화: ${column.column_name} (테이블 타입 관리에서 표시 컬럼 설정 제거됨)`
);
}
// 별칭 컬럼명 생성 (writer -> writer_name)
@ -153,16 +156,18 @@ export class EntityJoinService {
const joinColumns = joinConfigs
.map((config) => {
const alias = aliasMap.get(config.referenceTable);
const displayColumns = config.displayColumns || [config.displayColumn];
const displayColumns = config.displayColumns || [
config.displayColumn,
];
const separator = config.separator || " - ";
if (displayColumns.length === 1) {
// 단일 컬럼인 경우
return `COALESCE(${alias}.${displayColumns[0]}, '') AS ${config.aliasColumn}`;
} else {
// 여러 컬럼인 경우 CONCAT으로 연결
const concatParts = displayColumns
.map(col => `COALESCE(${alias}.${col}, '')`)
.map((col) => `COALESCE(${alias}.${col}, '')`)
.join(`, '${separator}', `);
return `CONCAT(${concatParts}) AS ${config.aliasColumn}`;
}
@ -236,7 +241,7 @@ export class EntityJoinService {
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
config.displayColumn || config.displayColumns[0]
);
return cachedData ? "cache" : "join";

View File

@ -2044,7 +2044,10 @@ export class TableManagementService {
}
// Entity 조인 설정 감지 (화면별 엔티티 설정 전달)
let joinConfigs = await entityJoinService.detectEntityJoins(tableName, options.screenEntityConfigs);
let joinConfigs = await entityJoinService.detectEntityJoins(
tableName,
options.screenEntityConfigs
);
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
if (
@ -2068,8 +2071,10 @@ export class TableManagementService {
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (writer)
referenceTable: additionalColumn.sourceTable, // 참조 테이블 (user_info)
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (user_id)
displayColumn: additionalColumn.sourceColumn, // 표시할 컬럼 (email)
displayColumns: [additionalColumn.sourceColumn], // 표시할 컬럼들 (email)
displayColumn: additionalColumn.sourceColumn, // 하위 호환성
aliasColumn: additionalColumn.joinAlias, // 별칭 (writer_email)
separator: " - ", // 기본 구분자
};
joinConfigs.push(additionalJoinConfig);
@ -2243,7 +2248,7 @@ export class TableManagementService {
await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
config.displayColumn || config.displayColumns[0]
);
}
@ -2430,7 +2435,7 @@ export class TableManagementService {
const lookupValue = referenceCacheService.getLookupValue(
config.referenceTable,
config.referenceColumn,
config.displayColumn,
config.displayColumn || config.displayColumns[0],
String(sourceValue)
);
@ -2724,7 +2729,7 @@ export class TableManagementService {
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
config.displayColumn || config.displayColumns[0]
);
if (cachedData && cachedData.size > 0) {
@ -2808,7 +2813,7 @@ export class TableManagementService {
const cachedData = await referenceCacheService.getCachedReference(
config.referenceTable,
config.referenceColumn,
config.displayColumn
config.displayColumn || config.displayColumns[0]
);
if (cachedData) {
@ -2847,7 +2852,7 @@ export class TableManagementService {
const hitRate = referenceCacheService.getCacheHitRate(
config.referenceTable,
config.referenceColumn,
config.displayColumn
config.displayColumn || config.displayColumns[0]
);
totalHitRate += hitRate;
}

View File

@ -171,7 +171,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
{/* 표시 컬럼들 (다중 선택) */}
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
{/* 현재 선택된 표시 컬럼들 */}
<div className="space-y-2">
{localValues.displayColumns.map((column, index) => (
@ -183,7 +183,7 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
</Button>
</div>
))}
{localValues.displayColumns.length === 0 && (
<div className="text-sm text-gray-500 italic"> </div>
)}
@ -197,20 +197,19 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
placeholder="컬럼명 입력 (예: user_name, dept_name)"
className="flex-1"
/>
<Button
size="sm"
onClick={addDisplayColumn}
<Button
size="sm"
onClick={addDisplayColumn}
disabled={!newDisplayColumn.trim() || localValues.displayColumns.includes(newDisplayColumn.trim())}
>
<Plus className="h-3 w-3 mr-1" />
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<div className="text-xs text-gray-500">
"{localValues.separator || ' - '}"
<br />
: 이름{localValues.separator || ' - '}
"{localValues.separator || " - "}"
<br /> : 이름{localValues.separator || " - "}
</div>
</div>
@ -261,7 +260,6 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
/>
</div>
{/* 필터 관리 */}
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
@ -328,7 +326,10 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
<div className="mt-2 text-xs text-gray-500">
: {localValues.referenceTable || "없음"}, : {localValues.referenceColumn}
<br />
: {localValues.displayColumns.length > 0 ? localValues.displayColumns.join(localValues.separator || ' - ') : "없음"}
:{" "}
{localValues.displayColumns.length > 0
? localValues.displayColumns.join(localValues.separator || " - ")
: "없음"}
</div>
</div>
@ -337,14 +338,11 @@ export const EntityTypeConfigPanel: React.FC<EntityTypeConfigPanelProps> = ({ co
<div className="text-sm font-medium text-blue-900"> </div>
<div className="mt-1 text-xs text-blue-800">
<strong> </strong>:
<br />
<strong> </strong>: ( ID)
<br />
<strong> </strong>: ( )
<br /> <strong> </strong>: ( ID)
<br /> <strong> </strong>: ( )
<br />
<br />
: 사용자 "이름" "이름 - 부서명"
<br /> : 사용자 "이름" "이름 - 부서명"
</div>
</div>
</div>

View File

@ -67,6 +67,7 @@ export const entityJoinApi = {
sourceColumn: string;
joinAlias: string;
}>;
screenEntityConfigs?: Record<string, any>; // 🎯 화면별 엔티티 설정
} = {},
): Promise<EntityJoinResponse> => {
const searchParams = new URLSearchParams();
@ -93,6 +94,7 @@ export const entityJoinApi = {
...params,
search: params.search ? JSON.stringify(params.search) : undefined,
additionalJoinColumns: params.additionalJoinColumns ? JSON.stringify(params.additionalJoinColumns) : undefined,
screenEntityConfigs: params.screenEntityConfigs ? JSON.stringify(params.screenEntityConfigs) : undefined, // 🎯 화면별 엔티티 설정
},
});
return response.data.data;

View File

@ -192,12 +192,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용
let displayLabel = column.displayName || column.columnName;
// Entity 타입이고 display_column이 있는 경우
if (column.webType === "entity" && column.displayColumn) {
// 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 displayColumn 사용
displayLabel = column.displayColumnLabel || column.displayColumn;
// Entity 타입 경우
if (column.webType === "entity") {
// 백엔드에서 받은 displayColumnLabel을 사용하거나, 없으면 기본값 사용
displayLabel = column.displayColumnLabel || column.displayColumn || `${column.columnName}_name`;
console.log(
`🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn})`,
`🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (${column.displayColumn || "기본값"})`,
);
}
@ -260,7 +260,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
joinAlias: col.entityJoinInfo!.joinAlias,
}));
// 🎯 화면별 엔티티 표시 설정 생성
const screenEntityConfigs: Record<string, any> = {};
entityJoinColumns.forEach((col) => {
if (col.entityDisplayConfig) {
const sourceColumn = col.entityJoinInfo!.sourceColumn;
screenEntityConfigs[sourceColumn] = {
displayColumns: col.entityDisplayConfig.displayColumns,
separator: col.entityDisplayConfig.separator || " - ",
};
}
});
console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns);
console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs);
const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, {
page: currentPage,
@ -329,6 +342,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortOrder: sortDirection,
enableEntityJoin: true, // 🎯 Entity 조인 활성화
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼
screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정
});
if (result) {

View File

@ -58,8 +58,17 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
}>;
}>;
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<Record<string, {
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
selectedColumns: string[];
separator: string;
}>>({});
// 화면 테이블명이 있으면 자동으로 설정
useEffect(() => {
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
@ -228,30 +237,38 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
handleChange("columns", [...(config.columns || []), newColumn]);
};
// Entity 조인 컬럼 추가
const addEntityJoinColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
// 🎯 엔티티 컬럼 추가 (컬럼 설정 패널에서 표시 컬럼 선택)
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) return;
// 기본 표시명으로 엔티티 컬럼 추가 (컬럼 설정 패널에서 나중에 표시 컬럼 조합 선택)
const newColumn: ColumnConfig = {
columnName: joinColumn.joinAlias,
displayName: joinColumn.columnLabel, // 라벨명만 사용
displayName: joinColumn.columnLabel,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
isEntityJoin: true, // Entity 조인 컬럼임을 표시
isEntityJoin: true,
entityJoinInfo: {
sourceTable: joinColumn.tableName,
sourceTable: config.selectedTable || "",
sourceColumn: joinColumn.columnName,
joinAlias: joinColumn.joinAlias,
},
// 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능)
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || "",
joinTable: joinColumn.tableName,
},
};
handleChange("columns", [...(config.columns || []), newColumn]);
console.log("🔗 Entity 조인 컬럼 추가됨:", newColumn);
console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn);
};
// 컬럼 제거
@ -267,6 +284,90 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
handleChange("columns", updatedColumns);
};
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return;
const { sourceTable, joinTable } = column.entityDisplayConfig;
const configKey = `${column.columnName}`;
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[configKey]) return;
try {
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
const [sourceResult, joinResult] = await Promise.all([
entityJoinApi.getReferenceTableColumns(sourceTable),
entityJoinApi.getReferenceTableColumns(joinTable),
]);
const sourceColumns = sourceResult.columns || [];
const joinColumns = joinResult.columns || [];
setEntityDisplayConfigs(prev => ({
...prev,
[configKey]: {
sourceColumns,
joinColumns,
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
} catch (error) {
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
}
};
// 🎯 엔티티 표시 컬럼 선택 토글
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
const configKey = `${columnName}`;
const config = entityDisplayConfigs[configKey];
if (!config) return;
const newSelectedColumns = config.selectedColumns.includes(selectedColumn)
? config.selectedColumns.filter(col => col !== selectedColumn)
: [...config.selectedColumns, selectedColumn];
setEntityDisplayConfigs(prev => ({
...prev,
[configKey]: {
...prev[configKey],
selectedColumns: newSelectedColumns,
},
}));
// 컬럼 설정 업데이트
updateColumn(columnName, {
entityDisplayConfig: {
...config.entityDisplayConfig,
displayColumns: newSelectedColumns,
},
});
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
const config = entityDisplayConfigs[configKey];
if (!config) return;
setEntityDisplayConfigs(prev => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
// 컬럼 설정 업데이트
updateColumn(columnName, {
entityDisplayConfig: {
...config.entityDisplayConfig,
separator,
},
});
};
// 컬럼 순서 변경
const moveColumn = (columnName: string, direction: "up" | "down") => {
const columns = [...(config.columns || [])];
@ -820,6 +921,108 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
/>
</div>
{/* 🎯 엔티티 타입 컬럼일 때 표시 컬럼 선택 UI */}
{column.isEntityJoin && column.entityDisplayConfig && (
<div className="col-span-2 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium"> </Label>
<Button
variant="outline"
size="sm"
onClick={() => loadEntityDisplayConfig(column)}
className="h-6 text-xs"
>
<Plus className="h-3 w-3 mr-1" />
</Button>
</div>
{entityDisplayConfigs[column.columnName] && (
<div className="space-y-3">
{/* 구분자 설정 */}
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={entityDisplayConfigs[column.columnName].separator}
onChange={(e) => updateEntityDisplaySeparator(column.columnName, e.target.value)}
className="h-7 text-xs"
placeholder=" - "
/>
</div>
{/* 기본 테이블 컬럼 */}
<div className="space-y-1">
<Label className="text-xs text-blue-600">
: {column.entityDisplayConfig.sourceTable}
</Label>
<div className="grid grid-cols-2 gap-1 max-h-20 overflow-y-auto">
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
<div key={col.columnName} className="flex items-center space-x-1">
<Checkbox
id={`source-${column.columnName}-${col.columnName}`}
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(col.columnName)}
onCheckedChange={() => toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="h-3 w-3"
/>
<Label
htmlFor={`source-${column.columnName}-${col.columnName}`}
className="text-xs cursor-pointer flex-1"
>
{col.displayName}
</Label>
</div>
))}
</div>
</div>
{/* 조인 테이블 컬럼 */}
<div className="space-y-1">
<Label className="text-xs text-green-600">
: {column.entityDisplayConfig.joinTable}
</Label>
<div className="grid grid-cols-2 gap-1 max-h-20 overflow-y-auto">
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
<div key={col.columnName} className="flex items-center space-x-1">
<Checkbox
id={`join-${column.columnName}-${col.columnName}`}
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(col.columnName)}
onCheckedChange={() => toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="h-3 w-3"
/>
<Label
htmlFor={`join-${column.columnName}-${col.columnName}`}
className="text-xs cursor-pointer flex-1"
>
{col.displayName}
</Label>
</div>
))}
</div>
</div>
{/* 선택된 컬럼 미리보기 */}
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="flex flex-wrap gap-1 rounded bg-gray-50 p-2 text-xs">
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
<React.Fragment key={colName}>
<Badge variant="secondary" className="text-xs">
{colName}
</Badge>
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
<span className="text-gray-400">{entityDisplayConfigs[column.columnName].separator}</span>
)}
</React.Fragment>
))}
</div>
</div>
)}
</div>
)}
</div>
)}
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
@ -1018,7 +1221,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
<Button
size="sm"
variant="outline"
onClick={() => addEntityJoinColumn(matchingJoinColumn)}
onClick={() => addEntityColumn(matchingJoinColumn)}
className="text-xs"
>
<Plus className="mr-1 h-3 w-3" />
@ -1057,7 +1260,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
key={index}
variant={isAlreadyAdded ? "secondary" : "outline"}
className="cursor-pointer text-xs"
onClick={() => !isAlreadyAdded && addEntityJoinColumn(column)}
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
>
{column.columnLabel}
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
@ -1325,6 +1528,7 @@ export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
</ScrollArea>
</TabsContent>
</Tabs>
</div>
);
};

View File

@ -57,6 +57,14 @@ export interface ColumnConfig {
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
// 🎯 엔티티 컬럼 표시 설정 (화면별 동적 설정)
entityDisplayConfig?: {
displayColumns: string[]; // 표시할 컬럼들 (기본 테이블 + 조인 테이블)
separator?: string; // 구분자 (기본: " - ")
sourceTable?: string; // 기본 테이블명
joinTable?: string; // 조인 테이블명
};
// 컬럼 고정 관련 속성
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서

View File

@ -197,7 +197,7 @@ export interface EntityTypeConfig {
searchColumns?: string[];
filters?: Record<string, unknown>;
placeholder?: string;
displayFormat?: 'simple' | 'detailed' | 'custom'; // 표시 형식
displayFormat?: "simple" | "detailed" | "custom"; // 표시 형식
separator?: string; // 여러 컬럼 표시 시 구분자 (기본: ' - ')
}