feat(UniversalFormModal): 전용 API 저장 기능 및 사원+부서 통합 저장 API 구현

- CustomApiSaveConfig 타입 정의 (apiType, mainDeptFields, subDeptFields)

- saveWithCustomApi() 함수 추가로 테이블 직접 저장 대신 전용 API 호출

- adminController에 saveUserWithDept(), getUserWithDept() API 추가

- user_info + user_dept 트랜잭션 저장, 메인 부서 변경 시 자동 겸직 전환

- ConfigPanel에 전용 API 저장 설정 UI 추가

- SplitPanelLayout2: getColumnValue()로 조인 테이블 컬럼 값 추출 개선

- 검색 컬럼 선택 시 표시 컬럼 기반으로 변경
This commit is contained in:
SeongHyun Kim
2025-12-08 11:33:35 +09:00
parent a5055cae15
commit 892278853c
8 changed files with 1311 additions and 188 deletions

View File

@@ -279,12 +279,14 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
column_comment: c.displayName ?? c.column_comment ?? c.label ?? "",
}));
// 선택된 컬럼 추가 (테이블명으로 구분)
// 선택된 컬럼 추가 (테이블명으로 구분, 유니크 키 생성)
jt.selectColumns.forEach((selCol) => {
const col = transformedColumns.find((c: ColumnInfo) => c.column_name === selCol);
if (col) {
joinColumns.push({
...col,
// 유니크 키를 위해 테이블명_컬럼명 형태로 저장
column_name: `${jt.joinTable}.${col.column_name}`,
column_comment: col.column_comment ? `${col.column_comment} (${jt.joinTable})` : `${col.column_name} (${jt.joinTable})`,
});
}
@@ -727,8 +729,13 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
const currentColumns = side === "left"
? config.leftPanel?.displayColumns || []
: config.rightPanel?.displayColumns || [];
// 기본 테이블 설정 (메인 테이블)
const defaultTable = side === "left"
? config.leftPanel?.tableName
: config.rightPanel?.tableName;
updateConfig(path, [...currentColumns, { name: "", label: "" }]);
updateConfig(path, [...currentColumns, { name: "", label: "", sourceTable: defaultTable || "" }]);
};
// 표시 컬럼 삭제
@@ -1083,15 +1090,15 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
// 선택된 테이블의 컬럼만 필터링
const selectedSourceTable = col.sourceTable || config.rightPanel?.tableName;
const filteredColumns = rightColumns.filter((c) => {
// 조인 테이블 컬럼인지 확인 (column_comment에 테이블명 포함)
const isJoinColumn = c.column_comment?.includes("(") && c.column_comment?.includes(")");
// 조인 테이블 컬럼인지 확인 (column_name이 "테이블명.컬럼명" 형태)
const isJoinColumn = c.column_name.includes(".");
if (selectedSourceTable === config.rightPanel?.tableName) {
// 메인 테이블 선택 시: 조인 컬럼 아닌 것만
return !isJoinColumn;
} else {
// 조인 테이블 선택 시: 해당 테이블 컬럼만
return c.column_comment?.includes(`(${selectedSourceTable})`);
// 조인 테이블 선택 시: 해당 테이블 컬럼만 (테이블명.컬럼명 형태)
return c.column_name.startsWith(`${selectedSourceTable}.`);
}
});
@@ -1163,11 +1170,15 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
filteredColumns.map((c) => {
// 조인 컬럼의 경우 테이블명 제거하고 표시
const displayLabel = c.column_comment?.replace(/\s*\([^)]+\)$/, "") || c.column_name;
// 실제 컬럼명 (테이블명.컬럼명에서 컬럼명만 추출)
const actualColumnName = c.column_name.includes(".")
? c.column_name.split(".")[1]
: c.column_name;
return (
<SelectItem key={c.column_name} value={c.column_name}>
<span className="flex flex-col">
<span>{displayLabel}</span>
<span className="text-[10px] text-muted-foreground">{c.column_name}</span>
<span className="text-[10px] text-muted-foreground">{actualColumnName}</span>
</span>
</SelectItem>
);
@@ -1231,6 +1242,7 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
size="sm"
variant="ghost"
className="h-6 text-xs"
disabled={(config.rightPanel?.displayColumns || []).length === 0}
onClick={() => {
const current = config.rightPanel?.searchColumns || [];
updateConfig("rightPanel.searchColumns", [...current, { columnName: "", label: "" }]);
@@ -1240,36 +1252,99 @@ export const SplitPanelLayout2ConfigPanel: React.FC<SplitPanelLayout2ConfigPanel
</Button>
</div>
<p className="text-[10px] text-muted-foreground mb-2">
.
</p>
<div className="space-y-2">
{(config.rightPanel?.searchColumns || []).map((searchCol, index) => (
<div key={index} className="flex items-center gap-2">
<ColumnSelect
columns={rightColumns}
value={searchCol.columnName}
onValueChange={(value) => {
const current = [...(config.rightPanel?.searchColumns || [])];
current[index] = { ...current[index], columnName: value };
updateConfig("rightPanel.searchColumns", current);
}}
placeholder="컬럼 선택"
/>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 shrink-0 p-0"
onClick={() => {
const current = config.rightPanel?.searchColumns || [];
updateConfig(
"rightPanel.searchColumns",
current.filter((_, i) => i !== index)
);
}}
>
<X className="h-3 w-3" />
</Button>
{(config.rightPanel?.searchColumns || []).map((searchCol, index) => {
// 표시할 컬럼 정보를 가져와서 테이블명과 함께 표시
const displayColumns = config.rightPanel?.displayColumns || [];
// 유효한 컬럼만 필터링 (name이 있는 것만)
const validDisplayColumns = displayColumns.filter((dc) => dc.name && dc.name.trim() !== "");
// 현재 선택된 컬럼의 표시 정보
const selectedDisplayCol = validDisplayColumns.find((dc) => dc.name === searchCol.columnName);
const selectedColInfo = rightColumns.find((c) => c.column_name === searchCol.columnName);
const selectedLabel = selectedDisplayCol?.label ||
selectedColInfo?.column_comment?.replace(/\s*\([^)]+\)$/, "") ||
searchCol.columnName;
const selectedTableName = selectedDisplayCol?.sourceTable || config.rightPanel?.tableName || "";
const selectedTableLabel = tables.find((t) => t.table_name === selectedTableName)?.table_comment || selectedTableName;
return (
<div key={index} className="flex items-center gap-2">
<Select
value={searchCol.columnName || ""}
onValueChange={(value) => {
const current = [...(config.rightPanel?.searchColumns || [])];
current[index] = { ...current[index], columnName: value };
updateConfig("rightPanel.searchColumns", current);
}}
>
<SelectTrigger className="h-9 text-xs flex-1">
<SelectValue placeholder="컬럼 선택">
{searchCol.columnName ? (
<span className="flex items-center gap-1">
<span>{selectedLabel}</span>
<span className="text-[10px] text-muted-foreground">({selectedTableLabel})</span>
</span>
) : (
"컬럼 선택"
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{validDisplayColumns.length === 0 ? (
<SelectItem value="_empty" disabled>
</SelectItem>
) : (
validDisplayColumns.map((dc, dcIndex) => {
const colInfo = rightColumns.find((c) => c.column_name === dc.name);
const label = dc.label || colInfo?.column_comment?.replace(/\s*\([^)]+\)$/, "") || dc.name;
const tableName = dc.sourceTable || config.rightPanel?.tableName || "";
const tableLabel = tables.find((t) => t.table_name === tableName)?.table_comment || tableName;
const actualColName = dc.name.includes(".") ? dc.name.split(".")[1] : dc.name;
return (
<SelectItem key={`search-${dc.name}-${dcIndex}`} value={dc.name}>
<span className="flex flex-col">
<span className="flex items-center gap-1">
<span>{label}</span>
<span className="text-[10px] text-muted-foreground">({tableLabel})</span>
</span>
<span className="text-[10px] text-muted-foreground">{actualColName}</span>
</span>
</SelectItem>
);
})
)}
</SelectContent>
</Select>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 shrink-0 p-0"
onClick={() => {
const current = config.rightPanel?.searchColumns || [];
updateConfig(
"rightPanel.searchColumns",
current.filter((_, i) => i !== index)
);
}}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})}
{(config.rightPanel?.displayColumns || []).length === 0 && (
<div className="rounded-md border py-3 text-center text-xs text-muted-foreground">
</div>
))}
{(config.rightPanel?.searchColumns || []).length === 0 && (
)}
{(config.rightPanel?.displayColumns || []).length > 0 && (config.rightPanel?.searchColumns || []).length === 0 && (
<div className="rounded-md border py-3 text-center text-xs text-muted-foreground">
</div>