테이블 타입관리 ui개선

This commit is contained in:
kjs
2025-12-03 16:39:47 +09:00
parent eb5ea411c9
commit 4e29f92268
5 changed files with 491 additions and 334 deletions

View File

@@ -4,11 +4,12 @@ import React, { useEffect, useState, useMemo, useCallback } from "react";
import { ComponentRendererProps } from "@/types/component";
import { CardDisplayConfig } from "./types";
import { tableTypeApi } from "@/lib/api/screen";
import { getFullImageUrl } from "@/lib/api/client";
import { getFullImageUrl, apiClient } from "@/lib/api/client";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
import { useModalDataStore } from "@/stores/modalDataStore";
@@ -51,6 +52,14 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const [loadedTableData, setLoadedTableData] = useState<any[]>([]);
const [loadedTableColumns, setLoadedTableColumns] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 카테고리 매핑 상태 (카테고리 코드 -> 라벨/색상)
const [columnMeta, setColumnMeta] = useState<
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
>({});
const [categoryMappings, setCategoryMappings] = useState<
Record<string, Record<string, { label: string; color?: string }>>
>({});
// 선택된 카드 상태 (Set으로 변경하여 테이블 리스트와 동일하게)
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
@@ -120,44 +129,78 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
const tableNameToUse = tableName || component.componentConfig?.tableName || 'user_info'; // 기본 테이블명 설정
if (!tableNameToUse) {
// console.log("📋 CardDisplay: 테이블명이 설정되지 않음", {
// tableName,
// componentTableName: component.componentConfig?.tableName,
// });
return;
}
// console.log("📋 CardDisplay: 사용할 테이블명", {
// tableName,
// componentTableName: component.componentConfig?.tableName,
// finalTableName: tableNameToUse,
// });
try {
setLoading(true);
// console.log(`📋 CardDisplay: ${tableNameToUse} 테이블 데이터 로딩 시작`);
// 테이블 데이터 컬럼 정보를 병렬로 로드
const [dataResponse, columnsResponse] = await Promise.all([
// 테이블 데이터, 컬럼 정보, 입력 타입 정보를 병렬로 로드
const [dataResponse, columnsResponse, inputTypesResponse] = await Promise.all([
tableTypeApi.getTableData(tableNameToUse, {
page: 1,
size: 50, // 카드 표시용으로 적당한 개수
}),
tableTypeApi.getColumns(tableNameToUse),
tableTypeApi.getColumnInputTypes(tableNameToUse),
]);
// console.log(`📋 CardDisplay: ${tableNameToUse} 데이터 로딩 완료`, {
// total: dataResponse.total,
// dataLength: dataResponse.data.length,
// columnsLength: columnsResponse.length,
// sampleData: dataResponse.data.slice(0, 2),
// sampleColumns: columnsResponse.slice(0, 3),
// });
setLoadedTableData(dataResponse.data);
setLoadedTableColumns(columnsResponse);
// 컬럼 메타 정보 설정 (inputType 포함)
const meta: Record<string, { webType?: string; codeCategory?: string; inputType?: string }> = {};
inputTypesResponse.forEach((item: any) => {
meta[item.columnName || item.column_name] = {
webType: item.webType || item.web_type,
inputType: item.inputType || item.input_type,
codeCategory: item.codeCategory || item.code_category,
};
});
console.log("📋 [CardDisplay] 컬럼 메타 정보:", meta);
setColumnMeta(meta);
// 카테고리 타입 컬럼 찾기 및 매핑 로드
const categoryColumns = Object.entries(meta)
.filter(([_, m]) => m.inputType === "category")
.map(([columnName]) => columnName);
console.log("📋 [CardDisplay] 카테고리 컬럼:", categoryColumns);
if (categoryColumns.length > 0) {
const mappings: Record<string, Record<string, { label: string; color?: string }>> = {};
for (const columnName of categoryColumns) {
try {
console.log(`📋 [CardDisplay] 카테고리 매핑 로드 시작: ${tableNameToUse}/${columnName}`);
const response = await apiClient.get(`/table-categories/${tableNameToUse}/${columnName}/values`);
console.log(`📋 [CardDisplay] 카테고리 API 응답 [${columnName}]:`, response.data);
if (response.data.success && response.data.data) {
const mapping: Record<string, { label: string; color?: string }> = {};
response.data.data.forEach((item: any) => {
// API 응답 형식: valueCode, valueLabel (camelCase)
const code = item.valueCode || item.value_code || item.category_code || item.code || item.value;
const label = item.valueLabel || item.value_label || item.category_name || item.name || item.label || code;
// color가 null/undefined/"none"이면 undefined로 유지 (배지 없음)
const rawColor = item.color ?? item.badge_color;
const color = (rawColor && rawColor !== "none") ? rawColor : undefined;
mapping[code] = { label, color };
console.log(`📋 [CardDisplay] 매핑 추가: ${code} -> ${label} (color: ${color})`);
});
mappings[columnName] = mapping;
}
} catch (error) {
console.error(`❌ CardDisplay: 카테고리 매핑 로드 실패 [${columnName}]`, error);
}
}
console.log("📋 [CardDisplay] 최종 카테고리 매핑:", mappings);
setCategoryMappings(mappings);
}
} catch (error) {
console.error(`❌ CardDisplay: ${tableNameToUse} 데이터 로딩 실패`, error);
console.error(`❌ CardDisplay: 데이터 로딩 실패`, error);
setLoadedTableData([]);
setLoadedTableColumns([]);
} finally {
@@ -392,10 +435,58 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
return text.substring(0, maxLength) + "...";
};
// 컬럼 매핑에서 값 가져오기
const getColumnValue = (data: any, columnName?: string) => {
// 컬럼 값을 문자열로 가져오기 (카테고리 타입인 경우 매핑된 라벨 반환)
const getColumnValueAsString = (data: any, columnName?: string): string => {
if (!columnName) return "";
return data[columnName] || "";
const value = data[columnName];
if (value === null || value === undefined || value === "") return "";
// 카테고리 타입인 경우 매핑된 라벨 반환
const meta = columnMeta[columnName];
if (meta?.inputType === "category") {
const mapping = categoryMappings[columnName];
const valueStr = String(value);
const categoryData = mapping?.[valueStr];
return categoryData?.label || valueStr;
}
return String(value);
};
// 컬럼 매핑에서 값 가져오기 (카테고리 타입인 경우 배지로 표시)
const getColumnValue = (data: any, columnName?: string): React.ReactNode => {
if (!columnName) return "";
const value = data[columnName];
if (value === null || value === undefined || value === "") return "";
// 카테고리 타입인 경우 매핑된 라벨과 배지로 표시
const meta = columnMeta[columnName];
if (meta?.inputType === "category") {
const mapping = categoryMappings[columnName];
const valueStr = String(value);
const categoryData = mapping?.[valueStr];
const displayLabel = categoryData?.label || valueStr;
const displayColor = categoryData?.color;
// 색상이 없거나(null/undefined), 빈 문자열이거나, "none"이면 일반 텍스트로 표시 (배지 없음)
if (!displayColor || displayColor === "none") {
return displayLabel;
}
return (
<Badge
style={{
backgroundColor: displayColor,
borderColor: displayColor,
}}
className="text-white text-xs"
>
{displayLabel}
</Badge>
);
}
return String(value);
};
// 컬럼명을 라벨로 변환하는 헬퍼 함수
@@ -516,16 +607,16 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
</div>
) : (
displayData.map((data, index) => {
// 타이틀, 서브타이틀, 설명 값 결정 (원래 카드 레이아웃과 동일한 로직)
// 타이틀, 서브타이틀, 설명 값 결정 (문자열로 가져와서 표시)
const titleValue =
getColumnValue(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title");
getColumnValueAsString(data, componentConfig.columnMapping?.titleColumn) || getAutoFallbackValue(data, "title");
const subtitleValue =
getColumnValue(data, componentConfig.columnMapping?.subtitleColumn) ||
getColumnValueAsString(data, componentConfig.columnMapping?.subtitleColumn) ||
getAutoFallbackValue(data, "subtitle");
const descriptionValue =
getColumnValue(data, componentConfig.columnMapping?.descriptionColumn) ||
getColumnValueAsString(data, componentConfig.columnMapping?.descriptionColumn) ||
getAutoFallbackValue(data, "description");
// 이미지 컬럼 자동 감지 (image_path, photo 등) - 대소문자 무시
@@ -628,27 +719,33 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
</div>
)}
{/* 카드 액션 */}
<div className="mt-2 flex justify-end space-x-2">
<button
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardView(data);
}}
>
</button>
<button
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardEdit(data);
}}
>
</button>
</div>
{/* 카드 액션 - 설정에 따라 표시 */}
{(componentConfig.cardStyle?.showActions ?? true) && (
<div className="mt-2 flex justify-end space-x-2">
{(componentConfig.cardStyle?.showViewButton ?? true) && (
<button
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardView(data);
}}
>
</button>
)}
{(componentConfig.cardStyle?.showEditButton ?? true) && (
<button
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={(e) => {
e.stopPropagation();
handleCardEdit(data);
}}
>
</button>
)}
</div>
)}
</div>
</div>
);
@@ -672,16 +769,48 @@ export const CardDisplayComponent: React.FC<CardDisplayComponentProps> = ({
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Object.entries(selectedData)
.filter(([key, value]) => value !== null && value !== undefined && value !== '')
.map(([key, value]) => (
<div key={key} className="bg-muted rounded-lg p-3">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">
{key.replace(/_/g, ' ')}
.map(([key, value]) => {
// 카테고리 타입인 경우 배지로 표시
const meta = columnMeta[key];
let displayValue: React.ReactNode = String(value);
if (meta?.inputType === "category") {
const mapping = categoryMappings[key];
const valueStr = String(value);
const categoryData = mapping?.[valueStr];
const displayLabel = categoryData?.label || valueStr;
const displayColor = categoryData?.color;
// 색상이 있고 "none"이 아닌 경우에만 배지로 표시
if (displayColor && displayColor !== "none") {
displayValue = (
<Badge
style={{
backgroundColor: displayColor,
borderColor: displayColor,
}}
className="text-white"
>
{displayLabel}
</Badge>
);
} else {
// 배지 없음: 일반 텍스트로 표시
displayValue = displayLabel;
}
}
return (
<div key={key} className="bg-muted rounded-lg p-3">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-1">
{getColumnLabel(key)}
</div>
<div className="text-sm font-medium text-foreground break-words">
{displayValue}
</div>
</div>
<div className="text-sm font-medium text-foreground break-words">
{String(value)}
</div>
</div>
))
);
})
}
</div>