데이터전달 모달열기 액션에 컬럼 매핑기능 추가
This commit is contained in:
parent
274078ef2c
commit
ec65ad6b9e
|
|
@ -2239,10 +2239,27 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
calculatedWidth: `${Math.round(widthPercent * 100) / 100}%`,
|
||||
});
|
||||
|
||||
// 🆕 라벨을 기반으로 기본 columnName 생성 (한글 → 스네이크 케이스)
|
||||
// 예: "창고코드" → "warehouse_code" 또는 그대로 유지
|
||||
const generateDefaultColumnName = (label: string): string => {
|
||||
// 한글 라벨의 경우 그대로 사용 (나중에 사용자가 수정 가능)
|
||||
// 영문의 경우 스네이크 케이스로 변환
|
||||
if (/[가-힣]/.test(label)) {
|
||||
// 한글이 포함된 경우: 공백을 언더스코어로, 소문자로 변환
|
||||
return label.replace(/\s+/g, "_").toLowerCase();
|
||||
}
|
||||
// 영문의 경우: 카멜케이스/파스칼케이스를 스네이크 케이스로 변환
|
||||
return label
|
||||
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
||||
.replace(/\s+/g, "_")
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const newComponent: ComponentData = {
|
||||
id: generateComponentId(),
|
||||
type: "component", // ✅ 새 컴포넌트 시스템 사용
|
||||
label: component.name,
|
||||
columnName: generateDefaultColumnName(component.name), // 🆕 기본 columnName 자동 생성
|
||||
widgetType: component.webType,
|
||||
componentType: component.id, // 새 컴포넌트 시스템의 ID (DynamicComponentRenderer용)
|
||||
position: snappedPosition,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
const [mappingSourceSearch, setMappingSourceSearch] = useState<Record<number, string>>({});
|
||||
const [mappingTargetSearch, setMappingTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
// 🆕 openModalWithData 전용 필드 매핑 상태
|
||||
const [modalSourceColumns, setModalSourceColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalTargetColumns, setModalTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalSourceSearch, setModalSourceSearch] = useState<Record<number, string>>({});
|
||||
const [modalTargetSearch, setModalTargetSearch] = useState<Record<number, string>>({});
|
||||
|
||||
// 🎯 플로우 위젯이 화면에 있는지 확인
|
||||
const hasFlowWidget = useMemo(() => {
|
||||
const found = allComponents.some((comp: any) => {
|
||||
|
|
@ -318,6 +326,88 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
loadColumns();
|
||||
}, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]);
|
||||
|
||||
// 🆕 openModalWithData 소스/타겟 테이블 컬럼 로드
|
||||
useEffect(() => {
|
||||
const actionType = config.action?.type;
|
||||
if (actionType !== "openModalWithData") return;
|
||||
|
||||
const loadModalMappingColumns = async () => {
|
||||
// 소스 테이블: 현재 화면의 분할 패널 또는 테이블에서 감지
|
||||
// allComponents에서 split-panel-layout 또는 table-list 찾기
|
||||
let sourceTableName: string | null = null;
|
||||
|
||||
for (const comp of allComponents) {
|
||||
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
||||
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
|
||||
// 분할 패널의 좌측 테이블명
|
||||
sourceTableName = (comp as any).componentConfig?.leftPanel?.tableName ||
|
||||
(comp as any).componentConfig?.leftTableName;
|
||||
break;
|
||||
}
|
||||
if (compType === "table-list") {
|
||||
sourceTableName = (comp as any).componentConfig?.tableName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 소스 테이블 컬럼 로드
|
||||
if (sourceTableName) {
|
||||
try {
|
||||
const response = await apiClient.get(`/table-management/tables/${sourceTableName}/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)) {
|
||||
const columns = columnData.map((col: any) => ({
|
||||
name: col.name || col.columnName,
|
||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||
}));
|
||||
setModalSourceColumns(columns);
|
||||
console.log(`✅ [openModalWithData] 소스 테이블(${sourceTableName}) 컬럼 로드:`, columns.length);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 타겟 화면의 테이블 컬럼 로드
|
||||
const targetScreenId = config.action?.targetScreenId;
|
||||
if (targetScreenId) {
|
||||
try {
|
||||
// 타겟 화면 정보 가져오기
|
||||
const screenResponse = await apiClient.get(`/screen-management/screens/${targetScreenId}`);
|
||||
if (screenResponse.data.success && screenResponse.data.data) {
|
||||
const targetTableName = screenResponse.data.data.tableName;
|
||||
if (targetTableName) {
|
||||
const columnResponse = await apiClient.get(`/table-management/tables/${targetTableName}/columns`);
|
||||
if (columnResponse.data.success) {
|
||||
let columnData = columnResponse.data.data;
|
||||
if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns;
|
||||
if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data;
|
||||
|
||||
if (Array.isArray(columnData)) {
|
||||
const columns = columnData.map((col: any) => ({
|
||||
name: col.name || col.columnName,
|
||||
label: col.displayName || col.label || col.columnLabel || col.name || col.columnName,
|
||||
}));
|
||||
setModalTargetColumns(columns);
|
||||
console.log(`✅ [openModalWithData] 타겟 테이블(${targetTableName}) 컬럼 로드:`, columns.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("타겟 화면 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadModalMappingColumns();
|
||||
}, [config.action?.type, config.action?.targetScreenId, allComponents]);
|
||||
|
||||
// 화면 목록 가져오기 (현재 편집 중인 화면의 회사 코드 기준)
|
||||
useEffect(() => {
|
||||
const fetchScreens = async () => {
|
||||
|
|
@ -1024,6 +1114,194 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
|||
SelectedItemsDetailInput 컴포넌트가 있는 화면을 선택하세요
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 🆕 필드 매핑 설정 (소스 컬럼 → 타겟 컬럼) */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">필드 매핑 (선택사항)</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px]"
|
||||
onClick={() => {
|
||||
const currentMappings = config.action?.fieldMappings || [];
|
||||
const newMapping = { sourceField: "", targetField: "" };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", [...currentMappings, newMapping]);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
소스 테이블의 컬럼명이 타겟 화면의 입력 필드 컬럼명과 다를 때 매핑을 설정하세요.
|
||||
<br />
|
||||
예: warehouse_code → warehouse_id (분할 패널의 창고코드를 모달의 창고ID에 매핑)
|
||||
</p>
|
||||
|
||||
{/* 컬럼 로드 상태 표시 */}
|
||||
{modalSourceColumns.length > 0 || modalTargetColumns.length > 0 ? (
|
||||
<div className="text-[10px] text-muted-foreground bg-muted/50 p-2 rounded">
|
||||
소스 컬럼: {modalSourceColumns.length}개 / 타겟 컬럼: {modalTargetColumns.length}개
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-amber-600 bg-amber-50 p-2 rounded dark:bg-amber-950/20">
|
||||
분할 패널 또는 테이블 컴포넌트와 대상 화면을 설정하면 컬럼 목록이 로드됩니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(config.action?.fieldMappings || []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed p-3 text-center">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
매핑이 없으면 같은 이름의 컬럼끼리 자동으로 매핑됩니다.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{(config.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2 rounded-md border bg-background p-2">
|
||||
{/* 소스 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={modalSourcePopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-7 w-full justify-between text-xs"
|
||||
>
|
||||
{mapping.sourceField
|
||||
? modalSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
|
||||
: "소스 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
className="h-8 text-xs"
|
||||
value={modalSourceSearch[index] || ""}
|
||||
onValueChange={(value) => setModalSourceSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalSourceColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.label} ${col.name}`}
|
||||
onSelect={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings[index] = { ...mappings[index], sourceField: col.name };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
setModalSourcePopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.sourceField === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span>{col.label}</span>
|
||||
{col.label !== col.name && (
|
||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
|
||||
{/* 타겟 필드 선택 (Combobox) */}
|
||||
<div className="flex-1">
|
||||
<Popover
|
||||
open={modalTargetPopoverOpen[index] || false}
|
||||
onOpenChange={(open) => setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="h-7 w-full justify-between text-xs"
|
||||
>
|
||||
{mapping.targetField
|
||||
? modalTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
|
||||
: "타겟 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
className="h-8 text-xs"
|
||||
value={modalTargetSearch[index] || ""}
|
||||
onValueChange={(value) => setModalTargetSearch((prev) => ({ ...prev, [index]: value }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty className="py-2 text-center text-xs">컬럼을 찾을 수 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalTargetColumns.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={`${col.label} ${col.name}`}
|
||||
onSelect={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings[index] = { ...mappings[index], targetField: col.name };
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
setModalTargetPopoverOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
className="text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mapping.targetField === col.name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span>{col.label}</span>
|
||||
{col.label !== col.name && (
|
||||
<span className="ml-1 text-muted-foreground">({col.name})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
const mappings = [...(config.action?.fieldMappings || [])];
|
||||
mappings.splice(index, 1);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", mappings);
|
||||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -584,20 +584,23 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{selectedComponent.type === "widget" && (
|
||||
{(selectedComponent.type === "widget" || selectedComponent.type === "component") && (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="columnName" className="text-xs font-medium">
|
||||
컬럼명 (읽기 전용)
|
||||
컬럼명 (필드명)
|
||||
</Label>
|
||||
<Input
|
||||
id="columnName"
|
||||
value={selectedComponent.columnName || ""}
|
||||
readOnly
|
||||
placeholder="데이터베이스 컬럼명"
|
||||
className="bg-muted/50 text-muted-foreground h-8"
|
||||
title="컬럼명은 변경할 수 없습니다"
|
||||
onChange={(e) => onUpdateProperty("columnName", e.target.value)}
|
||||
placeholder="formData에서 사용할 필드명"
|
||||
className="h-8"
|
||||
title="분할 패널에서 데이터를 전달받을 때 사용되는 필드명입니다"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
분할 패널에서 데이터를 전달받을 때 매핑되는 필드명
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface ButtonActionConfig {
|
|||
popupWidth?: number;
|
||||
popupHeight?: number;
|
||||
dataSourceId?: string; // 🆕 modalDataStore에서 데이터를 가져올 ID (openModalWithData용)
|
||||
fieldMappings?: Array<{ sourceField: string; targetField: string }>; // 🆕 필드 매핑 (openModalWithData용)
|
||||
|
||||
// 확인 메시지
|
||||
confirmMessage?: string;
|
||||
|
|
@ -1548,10 +1549,27 @@ export class ButtonActionExecutor {
|
|||
}
|
||||
|
||||
// 🆕 부모 화면의 선택된 데이터 가져오기 (excludeFilter에서 사용)
|
||||
const parentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};
|
||||
const rawParentData = dataRegistry[dataSourceId]?.[0]?.originalData || dataRegistry[dataSourceId]?.[0] || {};
|
||||
|
||||
// 🆕 필드 매핑 적용 (소스 컬럼 → 타겟 컬럼)
|
||||
let parentData = { ...rawParentData };
|
||||
if (config.fieldMappings && Array.isArray(config.fieldMappings) && config.fieldMappings.length > 0) {
|
||||
console.log("🔄 [openModalWithData] 필드 매핑 적용:", config.fieldMappings);
|
||||
|
||||
config.fieldMappings.forEach((mapping: { sourceField: string; targetField: string }) => {
|
||||
if (mapping.sourceField && mapping.targetField && rawParentData[mapping.sourceField] !== undefined) {
|
||||
// 타겟 필드에 소스 필드 값 복사
|
||||
parentData[mapping.targetField] = rawParentData[mapping.sourceField];
|
||||
console.log(` ✅ ${mapping.sourceField} → ${mapping.targetField}: ${rawParentData[mapping.sourceField]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log("📦 [openModalWithData] 부모 데이터 전달:", {
|
||||
dataSourceId,
|
||||
parentData,
|
||||
rawParentData,
|
||||
mappedParentData: parentData,
|
||||
fieldMappings: config.fieldMappings,
|
||||
});
|
||||
|
||||
// 🆕 전역 모달 상태 업데이트를 위한 이벤트 발생 (URL 파라미터 포함)
|
||||
|
|
|
|||
Loading…
Reference in New Issue