Merge pull request '엔티티컬럼 표시설정 수정' (#297) from feature/screen-management into main

Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/297
This commit is contained in:
kjs 2025-12-17 17:41:49 +09:00
commit ffd31fc923
15 changed files with 90 additions and 59 deletions

View File

@ -52,3 +52,4 @@ export default router;

View File

@ -48,3 +48,4 @@ export default router;

View File

@ -64,3 +64,4 @@ export default router;

View File

@ -52,3 +52,4 @@ export default router;

View File

@ -186,8 +186,13 @@ export class EntityJoinService {
}
}
// 별칭 컬럼명 생성 (writer -> writer_name)
const aliasColumn = `${column.column_name}_name`;
// 🎯 별칭 컬럼명 생성 - 사용자가 선택한 displayColumns 기반으로 동적 생성
// 단일 컬럼: manager + user_name → manager_user_name
// 여러 컬럼: 첫 번째 컬럼 기준 (나머지는 개별 alias로 처리됨)
const firstDisplayColumn = displayColumns[0] || "name";
const aliasColumn = `${column.column_name}_${firstDisplayColumn}`;
logger.info(`🔧 별칭 컬럼명 생성: ${column.column_name} + ${firstDisplayColumn}${aliasColumn}`);
const joinConfig: EntityJoinConfig = {
sourceTable: tableName,

View File

@ -584,3 +584,4 @@ const result = await executeNodeFlow(flowId, {

View File

@ -357,3 +357,4 @@

View File

@ -343,3 +343,4 @@ const getComponentValue = (componentId: string) => {
3. **조건부 저장**: 특정 조건 만족 시에만 저장
4. **연쇄 저장**: 한 번의 클릭으로 여러 테이블에 저장

View File

@ -93,7 +93,7 @@ export default function TableManagementPage() {
const [createTableModalOpen, setCreateTableModalOpen] = useState(false);
const [addColumnModalOpen, setAddColumnModalOpen] = useState(false);
const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false);
// 테이블 복제 관련 상태
const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create");
const [duplicateSourceTable, setDuplicateSourceTable] = useState<string | null>(null);
@ -109,7 +109,7 @@ export default function TableManagementPage() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [tableToDelete, setTableToDelete] = useState<string>("");
const [isDeleting, setIsDeleting] = useState(false);
// 선택된 테이블 목록 (체크박스)
const [selectedTableIds, setSelectedTableIds] = useState<Set<string>>(new Set());
@ -459,11 +459,39 @@ export default function TableManagementPage() {
if (!selectedTable) return;
try {
// 🎯 Entity 타입인 경우 detailSettings에 엔티티 설정을 JSON으로 포함
let finalDetailSettings = column.detailSettings || "";
if (column.inputType === "entity" && column.referenceTable) {
// 기존 detailSettings를 파싱하거나 새로 생성
let existingSettings: Record<string, unknown> = {};
if (typeof column.detailSettings === "string" && column.detailSettings.trim().startsWith("{")) {
try {
existingSettings = JSON.parse(column.detailSettings);
} catch {
existingSettings = {};
}
}
// 엔티티 설정 추가
const entitySettings = {
...existingSettings,
entityTable: column.referenceTable,
entityCodeColumn: column.referenceColumn || "id",
entityLabelColumn: column.displayColumn || "name",
placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요",
searchable: existingSettings.searchable ?? true,
};
finalDetailSettings = JSON.stringify(entitySettings);
console.log("🔧 Entity 설정 JSON 생성:", entitySettings);
}
const columnSetting = {
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
columnLabel: column.displayName, // 사용자가 입력한 표시명
inputType: column.inputType || "text",
detailSettings: column.detailSettings || "",
detailSettings: finalDetailSettings,
codeCategory: column.codeCategory || "",
codeValue: column.codeValue || "",
referenceTable: column.referenceTable || "",
@ -487,7 +515,7 @@ export default function TableManagementPage() {
if (response.data.success) {
console.log("✅ 컬럼 설정 저장 성공");
// 🆕 Category 타입인 경우 컬럼 매핑 처리
console.log("🔍 카테고리 조건 체크:", {
isCategory: column.inputType === "category",
@ -547,7 +575,7 @@ export default function TableManagementPage() {
} else if (successCount > 0 && failCount > 0) {
toast.warning(`컬럼 설정 저장 성공. ${successCount}개 메뉴 매핑 성공, ${failCount}개 실패.`);
} else if (failCount > 0) {
toast.error(`컬럼 설정 저장 성공. 메뉴 매핑 생성 실패.`);
toast.error("컬럼 설정 저장 성공. 메뉴 매핑 생성 실패.");
}
} else {
toast.success("컬럼 설정이 저장되었습니다. (메뉴 매핑 없음)");
@ -680,9 +708,7 @@ export default function TableManagementPage() {
console.log("📊 전체 매핑 결과:", { totalSuccessCount, totalFailCount });
if (totalSuccessCount > 0) {
toast.success(
`테이블 설정 및 ${totalSuccessCount}개 카테고리 메뉴 매핑이 저장되었습니다.`
);
toast.success(`테이블 설정 및 ${totalSuccessCount}개 카테고리 메뉴 매핑이 저장되었습니다.`);
} else if (totalFailCount > 0) {
toast.warning(`테이블 설정은 저장되었으나 ${totalFailCount}개 메뉴 매핑 생성 실패.`);
} else {
@ -1000,14 +1026,15 @@ export default function TableManagementPage() {
.filter(
(table) =>
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
(table.displayName &&
table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
)
.every((table) => selectedTableIds.has(table.tableName))
}
onCheckedChange={handleSelectAll}
aria-label="전체 선택"
/>
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground text-sm">
{selectedTableIds.size > 0 && `${selectedTableIds.size}개 선택됨`}
</span>
</div>
@ -1047,9 +1074,9 @@ export default function TableManagementPage() {
<div
key={table.tableName}
className={`bg-card rounded-lg p-4 shadow-sm transition-all ${
selectedTable === table.tableName
? "shadow-md bg-muted/30"
: "hover:shadow-lg hover:bg-muted/20"
selectedTable === table.tableName
? "bg-muted/30 shadow-md"
: "hover:bg-muted/20 hover:shadow-lg"
}`}
style={
selectedTable === table.tableName
@ -1068,10 +1095,7 @@ export default function TableManagementPage() {
onClick={(e) => e.stopPropagation()}
/>
)}
<div
className="flex-1 cursor-pointer"
onClick={() => handleTableSelect(table.tableName)}
>
<div className="flex-1 cursor-pointer" onClick={() => handleTableSelect(table.tableName)}>
<h4 className="text-sm font-semibold">{table.displayName || table.tableName}</h4>
<p className="text-muted-foreground mt-1 text-xs">
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
@ -1147,7 +1171,10 @@ export default function TableManagementPage() {
) : (
<div className="flex flex-1 flex-col overflow-hidden">
{/* 컬럼 헤더 (고정) */}
<div className="text-foreground grid h-12 flex-shrink-0 items-center border-b px-6 py-3 text-sm font-semibold" style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}>
<div
className="text-foreground grid h-12 flex-shrink-0 items-center border-b px-6 py-3 text-sm font-semibold"
style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}
>
<div className="pr-4"></div>
<div className="px-4"></div>
<div className="pr-6"> </div>
@ -1171,7 +1198,7 @@ export default function TableManagementPage() {
className="bg-background hover:bg-muted/50 grid min-h-16 items-start border-b px-6 py-3 transition-colors"
style={{ gridTemplateColumns: "160px 200px 250px 1fr" }}
>
<div className="pr-4 pt-1">
<div className="pt-1 pr-4">
<div className="font-mono text-sm">{column.columnName}</div>
</div>
<div className="px-4">
@ -1226,9 +1253,9 @@ export default function TableManagementPage() {
<label className="text-muted-foreground mb-1 block text-xs">
(2)
</label>
<div className="border rounded-lg p-3 space-y-2 max-h-48 overflow-y-auto">
<div className="max-h-48 space-y-2 overflow-y-auto rounded-lg border p-3">
{secondLevelMenus.length === 0 ? (
<p className="text-xs text-muted-foreground">
<p className="text-muted-foreground text-xs">
2 . .
</p>
) : (
@ -1236,7 +1263,7 @@ export default function TableManagementPage() {
// menuObjid를 숫자로 변환하여 비교
const menuObjidNum = Number(menu.menuObjid);
const isChecked = (column.categoryMenus || []).includes(menuObjidNum);
return (
<div key={menu.menuObjid} className="flex items-center gap-2">
<input
@ -1253,15 +1280,15 @@ export default function TableManagementPage() {
prev.map((col) =>
col.columnName === column.columnName
? { ...col, categoryMenus: newMenus }
: col
)
: col,
),
);
}}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-2 focus:ring-ring"
className="text-primary focus:ring-ring h-4 w-4 rounded border-gray-300 focus:ring-2"
/>
<label
htmlFor={`category-menu-${column.columnName}-${menu.menuObjid}`}
className="text-xs cursor-pointer flex-1"
className="flex-1 cursor-pointer text-xs"
>
{menu.parentMenuName} {menu.menuName}
</label>
@ -1282,9 +1309,7 @@ export default function TableManagementPage() {
<>
{/* 참조 테이블 */}
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
<label className="text-muted-foreground mb-1 block text-xs"> </label>
<Select
value={column.referenceTable || "none"}
onValueChange={(value) =>
@ -1296,15 +1321,10 @@ export default function TableManagementPage() {
</SelectTrigger>
<SelectContent>
{referenceTableOptions.map((option, index) => (
<SelectItem
key={`entity-${option.value}-${index}`}
value={option.value}
>
<SelectItem key={`entity-${option.value}-${index}`} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-muted-foreground text-xs">
{option.value}
</span>
<span className="text-muted-foreground text-xs">{option.value}</span>
</div>
</SelectItem>
))}
@ -1315,9 +1335,7 @@ export default function TableManagementPage() {
{/* 조인 컬럼 */}
{column.referenceTable && column.referenceTable !== "none" && (
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
<label className="text-muted-foreground mb-1 block text-xs"> </label>
<Select
value={column.referenceColumn || "none"}
onValueChange={(value) =>
@ -1361,9 +1379,7 @@ export default function TableManagementPage() {
column.referenceColumn &&
column.referenceColumn !== "none" && (
<div className="w-48">
<label className="text-muted-foreground mb-1 block text-xs">
</label>
<label className="text-muted-foreground mb-1 block text-xs"> </label>
<Select
value={column.displayColumn || "none"}
onValueChange={(value) =>
@ -1408,7 +1424,7 @@ export default function TableManagementPage() {
column.referenceColumn !== "none" &&
column.displayColumn &&
column.displayColumn !== "none" && (
<div className="bg-primary/10 text-primary flex items-center gap-1 rounded px-2 py-1 text-xs w-48">
<div className="bg-primary/10 text-primary flex w-48 items-center gap-1 rounded px-2 py-1 text-xs">
<span></span>
<span className="truncate"> </span>
</div>
@ -1460,9 +1476,10 @@ export default function TableManagementPage() {
setDuplicateSourceTable(null);
}}
onSuccess={async (result) => {
const message = duplicateModalMode === "duplicate"
? "테이블이 성공적으로 복제되었습니다!"
: "테이블이 성공적으로 생성되었습니다!";
const message =
duplicateModalMode === "duplicate"
? "테이블이 성공적으로 복제되었습니다!"
: "테이블이 성공적으로 생성되었습니다!";
toast.success(message);
// 테이블 목록 새로고침
await loadTables();
@ -1516,13 +1533,10 @@ export default function TableManagementPage() {
{selectedTableIds.size > 0 ? (
<>
<strong>{selectedTableIds.size}</strong> ?
<br />
.
<br /> .
</>
) : (
<>
? .
</>
<> ? .</>
)}
</DialogDescription>
</DialogHeader>
@ -1600,4 +1614,3 @@ export default function TableManagementPage() {
</div>
);
}

View File

@ -137,3 +137,4 @@ export const useActiveTabOptional = () => {
return useContext(ActiveTabContext);
};

View File

@ -194,3 +194,4 @@ export function applyAutoFillToFormData(

View File

@ -4104,13 +4104,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
// 선택된 컬럼들의 값을 구분자로 조합
const values = displayColumns
.map((colName: string) => {
// 1. 먼저 직접 컬럼명으로 시도 (기본 테이블 컬럼인 경우)
let cellValue = rowData[colName];
// 🎯 백엔드 alias 규칙: ${sourceColumn}_${displayColumn}
// 예: manager 컬럼에서 user_name 선택 시 → manager_user_name
const joinedKey = `${column.columnName}_${colName}`;
let cellValue = rowData[joinedKey];
// 2. 없으면 ${sourceColumn}_${colName} 형식으로 시도 (조인 테이블 컬럼인 경우)
// fallback: 직접 컬럼명으로 시도 (기본 테이블 컬럼인 경우)
if (cellValue === null || cellValue === undefined) {
const joinedKey = `${column.columnName}_${colName}`;
cellValue = rowData[joinedKey];
cellValue = rowData[colName];
}
if (cellValue === null || cellValue === undefined) return "";

View File

@ -1686,3 +1686,4 @@ const 출고등록_설정: ScreenSplitPanel = {

View File

@ -533,3 +533,4 @@ const { data: config } = await getScreenSplitPanel(screenId);

View File

@ -520,3 +520,4 @@ function ScreenViewPage() {