feat: enhance ButtonConfigPanel and V2ButtonConfigPanel with improved data handling
- Updated the ButtonConfigPanel to fetch a larger set of screens by modifying the API call to include a size parameter. - Enhanced the V2ButtonConfigPanel with new state variables and effects for managing data transfer field mappings, including loading available tables and their columns. - Implemented multi-table mapping logic to support complex data transfer actions, improving the flexibility and usability of the component. - Added a dedicated section for field mapping in the UI, allowing users to configure data transfer settings more effectively. These updates aim to enhance the functionality and user experience of the button configuration panels within the ERP system, enabling better data management and transfer capabilities. Made-with: Cursor
This commit is contained in:
parent
ec3cb8155f
commit
b4a5fb9aa3
|
|
@ -102,7 +102,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
const fetchScreens = async () => {
|
const fetchScreens = async () => {
|
||||||
try {
|
try {
|
||||||
setScreensLoading(true);
|
setScreensLoading(true);
|
||||||
const response = await apiClient.get("/screen-management/screens");
|
const response = await apiClient.get("/screen-management/screens?size=1000");
|
||||||
|
|
||||||
if (response.data.success && Array.isArray(response.data.data)) {
|
if (response.data.success && Array.isArray(response.data.data)) {
|
||||||
const screenList = response.data.data.map((screen: any) => ({
|
const screenList = response.data.data.map((screen: any) => ({
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,13 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
||||||
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
const [modalScreenOpen, setModalScreenOpen] = useState(false);
|
||||||
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
const [modalSearchTerm, setModalSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// 데이터 전달 필드 매핑 관련
|
||||||
|
const [availableTables, setAvailableTables] = useState<Array<{ name: string; label: string }>>([]);
|
||||||
|
const [mappingSourceColumnsMap, setMappingSourceColumnsMap] = useState<Record<string, Array<{ name: string; label: string }>>>({});
|
||||||
|
const [mappingTargetColumns, setMappingTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||||
|
const [fieldMappingOpen, setFieldMappingOpen] = useState(false);
|
||||||
|
const [activeMappingGroupIndex, setActiveMappingGroupIndex] = useState(0);
|
||||||
|
|
||||||
const showIconSettings = displayMode === "icon" || displayMode === "icon-text";
|
const showIconSettings = displayMode === "icon" || displayMode === "icon-text";
|
||||||
const currentActionIcons = actionIconMap[actionType] || [];
|
const currentActionIcons = actionIconMap[actionType] || [];
|
||||||
const isNoIconAction = noIconActions.has(actionType);
|
const isNoIconAction = noIconActions.has(actionType);
|
||||||
|
|
@ -330,6 +337,76 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
||||||
setIconSize(config.icon?.size || "보통");
|
setIconSize(config.icon?.size || "보통");
|
||||||
}, [config.icon?.name, config.icon?.type, config.icon?.size]);
|
}, [config.icon?.name, config.icon?.type, config.icon?.size]);
|
||||||
|
|
||||||
|
// 테이블 목록 로드 (데이터 전달 액션용)
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionType !== "transferData") return;
|
||||||
|
if (availableTables.length > 0) return;
|
||||||
|
|
||||||
|
const loadTables = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get("/table-management/tables");
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
const tables = response.data.data.map((t: any) => ({
|
||||||
|
name: t.tableName || t.name,
|
||||||
|
label: t.displayName || t.tableLabel || t.label || t.tableName || t.name,
|
||||||
|
}));
|
||||||
|
setAvailableTables(tables);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setAvailableTables([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadTables();
|
||||||
|
}, [actionType, availableTables.length]);
|
||||||
|
|
||||||
|
// 테이블 컬럼 로드 헬퍼
|
||||||
|
const loadTableColumns = useCallback(async (tableName: string): Promise<Array<{ name: string; label: string }>> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
||||||
|
if (response.data.success) {
|
||||||
|
let columnData = response.data.data;
|
||||||
|
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
|
||||||
|
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
|
||||||
|
if (Array.isArray(columnData)) {
|
||||||
|
return columnData.map((col: any) => ({
|
||||||
|
name: col.name || col.columnName,
|
||||||
|
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 멀티 테이블 매핑: 소스/타겟 테이블 컬럼 로드
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionType !== "transferData") return;
|
||||||
|
|
||||||
|
const multiTableMappings: Array<{ sourceTable: string }> = config.action?.dataTransfer?.multiTableMappings || [];
|
||||||
|
const targetTable = config.action?.dataTransfer?.targetTable;
|
||||||
|
|
||||||
|
const loadAll = async () => {
|
||||||
|
const sourceTableNames = multiTableMappings.map((m) => m.sourceTable).filter(Boolean);
|
||||||
|
const newMap: Record<string, Array<{ name: string; label: string }>> = {};
|
||||||
|
for (const tbl of sourceTableNames) {
|
||||||
|
if (!mappingSourceColumnsMap[tbl]) {
|
||||||
|
newMap[tbl] = await loadTableColumns(tbl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(newMap).length > 0) {
|
||||||
|
setMappingSourceColumnsMap((prev) => ({ ...prev, ...newMap }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetTable) {
|
||||||
|
const cols = await loadTableColumns(targetTable);
|
||||||
|
setMappingTargetColumns(cols);
|
||||||
|
} else {
|
||||||
|
setMappingTargetColumns([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadAll();
|
||||||
|
}, [actionType, config.action?.dataTransfer?.multiTableMappings, config.action?.dataTransfer?.targetTable, loadTableColumns]);
|
||||||
|
|
||||||
// 화면 목록 로드 (모달 액션용)
|
// 화면 목록 로드 (모달 액션용)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (actionType !== "modal" && actionType !== "navigate") return;
|
if (actionType !== "modal" && actionType !== "navigate") return;
|
||||||
|
|
@ -338,7 +415,7 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
||||||
const loadScreens = async () => {
|
const loadScreens = async () => {
|
||||||
setScreensLoading(true);
|
setScreensLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get("/screen-management/screens");
|
const response = await apiClient.get("/screen-management/screens?size=1000");
|
||||||
if (response.data.success && response.data.data) {
|
if (response.data.success && response.data.data) {
|
||||||
const screenList = response.data.data.map((s: any) => ({
|
const screenList = response.data.data.map((s: any) => ({
|
||||||
id: s.id || s.screenId,
|
id: s.id || s.screenId,
|
||||||
|
|
@ -521,6 +598,8 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
||||||
modalSearchTerm={modalSearchTerm}
|
modalSearchTerm={modalSearchTerm}
|
||||||
setModalSearchTerm={setModalSearchTerm}
|
setModalSearchTerm={setModalSearchTerm}
|
||||||
currentTableName={effectiveTableName}
|
currentTableName={effectiveTableName}
|
||||||
|
allComponents={allComponents}
|
||||||
|
handleUpdateProperty={handleUpdateProperty}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ─── 아이콘 설정 (접기) ─── */}
|
{/* ─── 아이콘 설정 (접기) ─── */}
|
||||||
|
|
@ -657,6 +736,26 @@ export const V2ButtonConfigPanel: React.FC<V2ButtonConfigPanelProps> = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 전달 필드 매핑 (transferData 액션 전용) */}
|
||||||
|
{actionType === "transferData" && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<TransferDataFieldMappingSection
|
||||||
|
config={config}
|
||||||
|
onChange={onChange}
|
||||||
|
availableTables={availableTables}
|
||||||
|
mappingSourceColumnsMap={mappingSourceColumnsMap}
|
||||||
|
setMappingSourceColumnsMap={setMappingSourceColumnsMap}
|
||||||
|
mappingTargetColumns={mappingTargetColumns}
|
||||||
|
fieldMappingOpen={fieldMappingOpen}
|
||||||
|
setFieldMappingOpen={setFieldMappingOpen}
|
||||||
|
activeMappingGroupIndex={activeMappingGroupIndex}
|
||||||
|
setActiveMappingGroupIndex={setActiveMappingGroupIndex}
|
||||||
|
loadTableColumns={loadTableColumns}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 제어 기능 */}
|
{/* 제어 기능 */}
|
||||||
{actionType !== "excel_upload" && actionType !== "multi_table_excel_upload" && (
|
{actionType !== "excel_upload" && actionType !== "multi_table_excel_upload" && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -699,6 +798,8 @@ const ActionDetailSection: React.FC<{
|
||||||
modalSearchTerm: string;
|
modalSearchTerm: string;
|
||||||
setModalSearchTerm: (term: string) => void;
|
setModalSearchTerm: (term: string) => void;
|
||||||
currentTableName?: string;
|
currentTableName?: string;
|
||||||
|
allComponents?: ComponentData[];
|
||||||
|
handleUpdateProperty?: (path: string, value: any) => void;
|
||||||
}> = ({
|
}> = ({
|
||||||
actionType,
|
actionType,
|
||||||
config,
|
config,
|
||||||
|
|
@ -711,6 +812,8 @@ const ActionDetailSection: React.FC<{
|
||||||
modalSearchTerm,
|
modalSearchTerm,
|
||||||
setModalSearchTerm,
|
setModalSearchTerm,
|
||||||
currentTableName,
|
currentTableName,
|
||||||
|
allComponents = [],
|
||||||
|
handleUpdateProperty,
|
||||||
}) => {
|
}) => {
|
||||||
const action = config.action || {};
|
const action = config.action || {};
|
||||||
|
|
||||||
|
|
@ -800,7 +903,7 @@ const ActionDetailSection: React.FC<{
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
<Command>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="화면 검색..."
|
placeholder="화면 검색..."
|
||||||
value={modalSearchTerm}
|
value={modalSearchTerm}
|
||||||
|
|
@ -812,8 +915,10 @@ const ActionDetailSection: React.FC<{
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{screens
|
{screens
|
||||||
.filter((s) =>
|
.filter((s) =>
|
||||||
|
!modalSearchTerm ||
|
||||||
s.name.toLowerCase().includes(modalSearchTerm.toLowerCase()) ||
|
s.name.toLowerCase().includes(modalSearchTerm.toLowerCase()) ||
|
||||||
s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase())
|
s.description?.toLowerCase().includes(modalSearchTerm.toLowerCase()) ||
|
||||||
|
String(s.id).includes(modalSearchTerm)
|
||||||
)
|
)
|
||||||
.map((screen) => (
|
.map((screen) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -951,6 +1056,190 @@ const ActionDetailSection: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case "transferData":
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<SendHorizontal className="h-4 w-4 text-primary" />
|
||||||
|
<span className="text-sm font-medium">데이터 전달 설정</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 컴포넌트 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">
|
||||||
|
소스 컴포넌트 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={action.dataTransfer?.sourceComponentId || ""}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const dt = { ...action.dataTransfer, sourceComponentId: v };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="데이터를 가져올 컴포넌트 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__auto__">
|
||||||
|
<span className="text-xs font-medium">자동 탐색 (현재 활성 테이블)</span>
|
||||||
|
</SelectItem>
|
||||||
|
{allComponents
|
||||||
|
.filter((comp: any) => {
|
||||||
|
const type = comp.componentType || comp.type || "";
|
||||||
|
return ["table-list", "repeater-field-group", "form-group", "data-table"].some((t) =>
|
||||||
|
type.includes(t)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((comp: any) => {
|
||||||
|
const compType = comp.componentType || comp.type || "unknown";
|
||||||
|
const compLabel = comp.label || comp.componentConfig?.title || comp.id;
|
||||||
|
return (
|
||||||
|
<SelectItem key={comp.id} value={comp.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium">{compLabel}</span>
|
||||||
|
<span className="text-muted-foreground text-[10px]">({compType})</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타겟 타입 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">
|
||||||
|
타겟 타입 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={action.dataTransfer?.targetType || "component"}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const dt = { ...action.dataTransfer, targetType: v };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="component">같은 화면의 컴포넌트</SelectItem>
|
||||||
|
<SelectItem value="splitPanel">분할 패널 반대편 화면</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타겟 컴포넌트 선택 */}
|
||||||
|
{action.dataTransfer?.targetType === "component" && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">
|
||||||
|
타겟 컴포넌트 <span className="text-destructive">*</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={action.dataTransfer?.targetComponentId || ""}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const dt = { ...action.dataTransfer, targetComponentId: v };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue placeholder="데이터를 받을 컴포넌트 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{allComponents
|
||||||
|
.filter((comp: any) => {
|
||||||
|
const type = comp.componentType || comp.type || "";
|
||||||
|
const isReceivable = ["table-list", "repeater-field-group", "form-group", "data-table"].some(
|
||||||
|
(t) => type.includes(t)
|
||||||
|
);
|
||||||
|
return isReceivable && comp.id !== action.dataTransfer?.sourceComponentId;
|
||||||
|
})
|
||||||
|
.map((comp: any) => {
|
||||||
|
const compType = comp.componentType || comp.type || "unknown";
|
||||||
|
const compLabel = comp.label || comp.componentConfig?.title || comp.id;
|
||||||
|
return (
|
||||||
|
<SelectItem key={comp.id} value={comp.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-medium">{compLabel}</span>
|
||||||
|
<span className="text-muted-foreground text-[10px]">({compType})</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 전달 모드 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">데이터 전달 모드</Label>
|
||||||
|
<Select
|
||||||
|
value={action.dataTransfer?.mode || "append"}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const dt = { ...action.dataTransfer, mode: v };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="append">추가 (Append)</SelectItem>
|
||||||
|
<SelectItem value="replace">교체 (Replace)</SelectItem>
|
||||||
|
<SelectItem value="merge">병합 (Merge)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 전달 후 초기화 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium">전달 후 소스 선택 초기화</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">데이터 전달 후 소스의 선택을 해제해요</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={action.dataTransfer?.clearAfterTransfer || false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const dt = { ...action.dataTransfer, clearAfterTransfer: checked };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 전달 전 확인 */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-medium">전달 전 확인 메시지</p>
|
||||||
|
<p className="text-[10px] text-muted-foreground">전달 전 확인 다이얼로그를 표시해요</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={action.dataTransfer?.confirmBeforeTransfer || false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const dt = { ...action.dataTransfer, confirmBeforeTransfer: checked };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{action.dataTransfer?.confirmBeforeTransfer && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">확인 메시지</Label>
|
||||||
|
<Input
|
||||||
|
value={action.dataTransfer?.confirmMessage || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const dt = { ...action.dataTransfer, confirmMessage: e.target.value };
|
||||||
|
updateActionConfig("dataTransfer", dt);
|
||||||
|
}}
|
||||||
|
placeholder="선택한 항목을 전달하시겠습니까?"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{commonMessageSection}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
case "event":
|
case "event":
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
||||||
|
|
@ -1373,6 +1662,386 @@ const IconSettingsSection: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─── 데이터 전달 필드 매핑 서브 컴포넌트 (고급 설정 내부) ───
|
||||||
|
const TransferDataFieldMappingSection: React.FC<{
|
||||||
|
config: Record<string, any>;
|
||||||
|
onChange: (config: Record<string, any>) => void;
|
||||||
|
availableTables: Array<{ name: string; label: string }>;
|
||||||
|
mappingSourceColumnsMap: Record<string, Array<{ name: string; label: string }>>;
|
||||||
|
setMappingSourceColumnsMap: React.Dispatch<React.SetStateAction<Record<string, Array<{ name: string; label: string }>>>>;
|
||||||
|
mappingTargetColumns: Array<{ name: string; label: string }>;
|
||||||
|
fieldMappingOpen: boolean;
|
||||||
|
setFieldMappingOpen: (open: boolean) => void;
|
||||||
|
activeMappingGroupIndex: number;
|
||||||
|
setActiveMappingGroupIndex: (index: number) => void;
|
||||||
|
loadTableColumns: (tableName: string) => Promise<Array<{ name: string; label: string }>>;
|
||||||
|
}> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
availableTables,
|
||||||
|
mappingSourceColumnsMap,
|
||||||
|
setMappingSourceColumnsMap,
|
||||||
|
mappingTargetColumns,
|
||||||
|
activeMappingGroupIndex,
|
||||||
|
setActiveMappingGroupIndex,
|
||||||
|
loadTableColumns,
|
||||||
|
}) => {
|
||||||
|
const [sourcePopoverOpen, setSourcePopoverOpen] = useState<Record<string, boolean>>({});
|
||||||
|
const [targetPopoverOpen, setTargetPopoverOpen] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const dataTransfer = config.action?.dataTransfer || {};
|
||||||
|
const multiTableMappings: Array<{ sourceTable: string; mappingRules: Array<{ sourceField: string; targetField: string }> }> =
|
||||||
|
dataTransfer.multiTableMappings || [];
|
||||||
|
|
||||||
|
const updateDataTransfer = (field: string, value: any) => {
|
||||||
|
const currentAction = config.action || {};
|
||||||
|
const currentDt = currentAction.dataTransfer || {};
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
action: {
|
||||||
|
...currentAction,
|
||||||
|
dataTransfer: { ...currentDt, [field]: value },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeGroup = multiTableMappings[activeMappingGroupIndex];
|
||||||
|
const activeSourceTable = activeGroup?.sourceTable || "";
|
||||||
|
const activeSourceColumns = mappingSourceColumnsMap[activeSourceTable] || [];
|
||||||
|
const activeRules = activeGroup?.mappingRules || [];
|
||||||
|
|
||||||
|
const updateGroupField = (field: string, value: any) => {
|
||||||
|
const mappings = [...multiTableMappings];
|
||||||
|
mappings[activeMappingGroupIndex] = { ...mappings[activeMappingGroupIndex], [field]: value };
|
||||||
|
updateDataTransfer("multiTableMappings", mappings);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<p className="text-sm font-medium">필드 매핑</p>
|
||||||
|
<p className="text-[11px] text-muted-foreground">
|
||||||
|
레이어별로 소스 테이블이 다를 때 각각 매핑 규칙을 설정해요
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타겟 테이블 (공통) */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">타겟 테이블 (공통)</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
|
||||||
|
{dataTransfer.targetTable
|
||||||
|
? availableTables.find((t) => t.name === dataTransfer.targetTable)?.label ||
|
||||||
|
dataTransfer.targetTable
|
||||||
|
: "타겟 테이블 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
|
<CommandList className="max-h-48">
|
||||||
|
<CommandEmpty className="py-3 text-xs">테이블을 찾을 수 없습니다</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{availableTables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.name}
|
||||||
|
value={`${table.label} ${table.name}`}
|
||||||
|
onSelect={() => updateDataTransfer("targetTable", table.name)}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check className={cn("mr-2 h-3 w-3", dataTransfer.targetTable === table.name ? "opacity-100" : "opacity-0")} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.label}</span>
|
||||||
|
{table.label !== table.name && <span className="text-[10px] text-muted-foreground">{table.name}</span>}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 소스 테이블 그룹 탭 + 추가 버튼 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">소스 테이블별 매핑</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
updateDataTransfer("multiTableMappings", [
|
||||||
|
...multiTableMappings,
|
||||||
|
{ sourceTable: "", mappingRules: [] },
|
||||||
|
]);
|
||||||
|
setActiveMappingGroupIndex(multiTableMappings.length);
|
||||||
|
}}
|
||||||
|
disabled={!dataTransfer.targetTable}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||||
|
소스 테이블 추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!dataTransfer.targetTable ? (
|
||||||
|
<div className="rounded-lg border border-dashed p-4 text-center">
|
||||||
|
<p className="text-xs text-muted-foreground">타겟 테이블을 먼저 선택하세요</p>
|
||||||
|
</div>
|
||||||
|
) : multiTableMappings.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-dashed p-4 text-center">
|
||||||
|
<p className="text-xs text-muted-foreground">소스 테이블을 추가하세요</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* 그룹 탭 */}
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{multiTableMappings.map((group, gIdx) => (
|
||||||
|
<div key={gIdx} className="flex items-center">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={activeMappingGroupIndex === gIdx ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs rounded-r-none"
|
||||||
|
onClick={() => setActiveMappingGroupIndex(gIdx)}
|
||||||
|
>
|
||||||
|
{group.sourceTable
|
||||||
|
? availableTables.find((t) => t.name === group.sourceTable)?.label || group.sourceTable
|
||||||
|
: `그룹 ${gIdx + 1}`}
|
||||||
|
{group.mappingRules?.length > 0 && (
|
||||||
|
<span className="ml-1.5 rounded-full bg-primary-foreground/20 px-1.5 text-[10px]">
|
||||||
|
{group.mappingRules.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={activeMappingGroupIndex === gIdx ? "default" : "outline"}
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7 rounded-l-none border-l-0"
|
||||||
|
onClick={() => {
|
||||||
|
const mappings = [...multiTableMappings];
|
||||||
|
mappings.splice(gIdx, 1);
|
||||||
|
updateDataTransfer("multiTableMappings", mappings);
|
||||||
|
if (activeMappingGroupIndex >= mappings.length) {
|
||||||
|
setActiveMappingGroupIndex(Math.max(0, mappings.length - 1));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 활성 그룹 편집 */}
|
||||||
|
{activeGroup && (
|
||||||
|
<div className="space-y-3 rounded-lg border p-3">
|
||||||
|
{/* 소스 테이블 선택 */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">소스 테이블</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs">
|
||||||
|
{activeSourceTable
|
||||||
|
? availableTables.find((t) => t.name === activeSourceTable)?.label || activeSourceTable
|
||||||
|
: "소스 테이블 선택"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="테이블 검색..." className="text-xs" />
|
||||||
|
<CommandList className="max-h-48">
|
||||||
|
<CommandEmpty className="py-3 text-xs">테이블을 찾을 수 없습니다</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{availableTables.map((table) => (
|
||||||
|
<CommandItem
|
||||||
|
key={table.name}
|
||||||
|
value={`${table.label} ${table.name}`}
|
||||||
|
onSelect={async () => {
|
||||||
|
updateGroupField("sourceTable", table.name);
|
||||||
|
if (!mappingSourceColumnsMap[table.name]) {
|
||||||
|
const cols = await loadTableColumns(table.name);
|
||||||
|
setMappingSourceColumnsMap((prev) => ({ ...prev, [table.name]: cols }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check className={cn("mr-2 h-3 w-3", activeSourceTable === table.name ? "opacity-100" : "opacity-0")} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{table.label}</span>
|
||||||
|
{table.label !== table.name && <span className="text-[10px] text-muted-foreground">{table.name}</span>}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 매핑 규칙 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-xs">매핑 규칙</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
updateGroupField("mappingRules", [
|
||||||
|
...activeRules,
|
||||||
|
{ sourceField: "", targetField: "" },
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
disabled={!activeSourceTable}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 h-3.5 w-3.5" />
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!activeSourceTable ? (
|
||||||
|
<p className="text-xs text-muted-foreground">소스 테이블을 먼저 선택하세요</p>
|
||||||
|
) : activeRules.length === 0 ? (
|
||||||
|
<p className="text-xs text-muted-foreground">매핑 없음 (동일 필드명 자동 매핑)</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{activeRules.map((rule: any, rIdx: number) => {
|
||||||
|
const keyS = `${activeMappingGroupIndex}-${rIdx}-s`;
|
||||||
|
const keyT = `${activeMappingGroupIndex}-${rIdx}-t`;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={rIdx}
|
||||||
|
className="grid items-center gap-1.5"
|
||||||
|
style={{ gridTemplateColumns: "1fr 16px 1fr 32px" }}
|
||||||
|
>
|
||||||
|
{/* 소스 필드 */}
|
||||||
|
<Popover
|
||||||
|
open={sourcePopoverOpen[keyS] || false}
|
||||||
|
onOpenChange={(open) => setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: open }))}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{rule.sourceField
|
||||||
|
? activeSourceColumns.find((c) => c.name === rule.sourceField)?.label || rule.sourceField
|
||||||
|
: "소스 컬럼"}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[220px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandList className="max-h-48">
|
||||||
|
<CommandEmpty className="py-3 text-xs">찾을 수 없습니다</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{activeSourceColumns.map((col) => (
|
||||||
|
<CommandItem
|
||||||
|
key={col.name}
|
||||||
|
value={`${col.label} ${col.name}`}
|
||||||
|
onSelect={() => {
|
||||||
|
const newRules = [...activeRules];
|
||||||
|
newRules[rIdx] = { ...newRules[rIdx], sourceField: col.name };
|
||||||
|
updateGroupField("mappingRules", newRules);
|
||||||
|
setSourcePopoverOpen((prev) => ({ ...prev, [keyS]: false }));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check className={cn("mr-2 h-3 w-3", rule.sourceField === col.name ? "opacity-100" : "opacity-0")} />
|
||||||
|
<span className="font-medium">{col.label}</span>
|
||||||
|
{col.label !== col.name && <span className="ml-1 text-muted-foreground">({col.name})</span>}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<ArrowRight className="mx-auto h-4 w-4 text-muted-foreground" />
|
||||||
|
|
||||||
|
{/* 타겟 필드 */}
|
||||||
|
<Popover
|
||||||
|
open={targetPopoverOpen[keyT] || false}
|
||||||
|
onOpenChange={(open) => setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: open }))}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" role="combobox" className="h-8 w-full justify-between text-xs overflow-hidden">
|
||||||
|
<span className="truncate">
|
||||||
|
{rule.targetField
|
||||||
|
? mappingTargetColumns.find((c) => c.name === rule.targetField)?.label || rule.targetField
|
||||||
|
: "타겟 컬럼"}
|
||||||
|
</span>
|
||||||
|
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[220px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
|
||||||
|
<CommandList className="max-h-48">
|
||||||
|
<CommandEmpty className="py-3 text-xs">찾을 수 없습니다</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{mappingTargetColumns.map((col) => (
|
||||||
|
<CommandItem
|
||||||
|
key={col.name}
|
||||||
|
value={`${col.label} ${col.name}`}
|
||||||
|
onSelect={() => {
|
||||||
|
const newRules = [...activeRules];
|
||||||
|
newRules[rIdx] = { ...newRules[rIdx], targetField: col.name };
|
||||||
|
updateGroupField("mappingRules", newRules);
|
||||||
|
setTargetPopoverOpen((prev) => ({ ...prev, [keyT]: false }));
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check className={cn("mr-2 h-3 w-3", rule.targetField === col.name ? "opacity-100" : "opacity-0")} />
|
||||||
|
<span className="font-medium">{col.label}</span>
|
||||||
|
{col.label !== col.name && <span className="ml-1 text-muted-foreground">({col.name})</span>}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* 삭제 */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => {
|
||||||
|
const newRules = [...activeRules];
|
||||||
|
newRules.splice(rIdx, 1);
|
||||||
|
updateGroupField("mappingRules", newRules);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
V2ButtonConfigPanel.displayName = "V2ButtonConfigPanel";
|
V2ButtonConfigPanel.displayName = "V2ButtonConfigPanel";
|
||||||
|
|
||||||
export default V2ButtonConfigPanel;
|
export default V2ButtonConfigPanel;
|
||||||
|
|
|
||||||
|
|
@ -6566,7 +6566,36 @@ export class ButtonActionExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dataTransfer 설정이 있는 경우
|
// dataTransfer 설정이 있는 경우
|
||||||
const { targetType, targetComponentId, targetScreenId, mappingRules, receiveMode } = dataTransfer;
|
const { targetType, targetComponentId, targetScreenId, receiveMode } = dataTransfer;
|
||||||
|
|
||||||
|
// multiTableMappings 우선: 소스 테이블에 맞는 매핑 규칙 선택
|
||||||
|
let mappingRules = dataTransfer.mappingRules;
|
||||||
|
const multiTableMappings = (dataTransfer as any).multiTableMappings as Array<{
|
||||||
|
sourceTable: string;
|
||||||
|
mappingRules: Array<{ sourceField: string; targetField: string }>;
|
||||||
|
}> | undefined;
|
||||||
|
|
||||||
|
if (multiTableMappings && multiTableMappings.length > 0) {
|
||||||
|
const sourceTableName = context.tableName || (dataTransfer as any).sourceTable;
|
||||||
|
const matchedGroup = multiTableMappings.find((g) => g.sourceTable === sourceTableName);
|
||||||
|
if (matchedGroup && matchedGroup.mappingRules?.length > 0) {
|
||||||
|
mappingRules = matchedGroup.mappingRules;
|
||||||
|
console.log("📋 [transferData] multiTableMappings 매핑 적용:", {
|
||||||
|
sourceTable: sourceTableName,
|
||||||
|
rules: matchedGroup.mappingRules,
|
||||||
|
});
|
||||||
|
} else if (!mappingRules || mappingRules.length === 0) {
|
||||||
|
// 매칭되는 그룹이 없고 기존 mappingRules도 없으면 첫 번째 그룹 사용
|
||||||
|
const fallback = multiTableMappings[0];
|
||||||
|
if (fallback?.mappingRules?.length > 0) {
|
||||||
|
mappingRules = fallback.mappingRules;
|
||||||
|
console.log("📋 [transferData] multiTableMappings 폴백 매핑 적용:", {
|
||||||
|
sourceTable: fallback.sourceTable,
|
||||||
|
rules: fallback.mappingRules,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (targetType === "component" && targetComponentId) {
|
if (targetType === "component" && targetComponentId) {
|
||||||
// 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행
|
// 같은 화면 내 컴포넌트로 전달 + 레이어 활성화 이벤트 병행
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue