refactor: 코드 정리 및 가독성 향상
- numberingRuleController.ts에서 API 엔드포인트의 코드 스타일을 일관되게 정리하여 가독성을 높였습니다. - 불필요한 줄바꿈을 제거하고, 코드 블록을 명확하게 정리하여 유지보수성을 개선했습니다. - tableManagementService.ts와 ButtonConfigPanel.tsx에서 코드 정리를 통해 일관성을 유지하고, 가독성을 향상시켰습니다. - 전반적으로 코드의 깔끔함을 유지하고, 향후 개발 시 이해하기 쉽게 개선했습니다.
This commit is contained in:
@@ -51,13 +51,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
}) => {
|
||||
// 🔧 component가 없는 경우 방어 처리
|
||||
if (!component) {
|
||||
return (
|
||||
<div className="p-4 text-sm text-muted-foreground">
|
||||
컴포넌트 정보를 불러올 수 없습니다.
|
||||
</div>
|
||||
);
|
||||
return <div className="text-muted-foreground p-4 text-sm">컴포넌트 정보를 불러올 수 없습니다.</div>;
|
||||
}
|
||||
|
||||
|
||||
// 🔧 component에서 직접 읽기 (useMemo 제거)
|
||||
const config = component.componentConfig || {};
|
||||
const currentAction = component.componentConfig?.action || {};
|
||||
@@ -122,7 +118,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
const [modalActionTargetTable, setModalActionTargetTable] = useState<string | null>(null);
|
||||
const [modalActionSourceColumns, setModalActionSourceColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalActionTargetColumns, setModalActionTargetColumns] = useState<Array<{ name: string; label: string }>>([]);
|
||||
const [modalActionFieldMappings, setModalActionFieldMappings] = useState<Array<{ sourceField: string; targetField: string }>>([]);
|
||||
const [modalActionFieldMappings, setModalActionFieldMappings] = useState<
|
||||
Array<{ sourceField: string; targetField: string }>
|
||||
>([]);
|
||||
const [modalFieldMappingSourceOpen, setModalFieldMappingSourceOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalFieldMappingTargetOpen, setModalFieldMappingTargetOpen] = useState<Record<number, boolean>>({});
|
||||
const [modalFieldMappingSourceSearch, setModalFieldMappingSourceSearch] = useState<Record<number, string>>({});
|
||||
@@ -353,7 +351,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
useEffect(() => {
|
||||
const actionType = config.action?.type;
|
||||
if (actionType !== "modal") return;
|
||||
|
||||
|
||||
const autoDetect = config.action?.autoDetectDataSource;
|
||||
if (!autoDetect) {
|
||||
// 데이터 전달이 비활성화되면 상태 초기화
|
||||
@@ -363,19 +361,19 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
setModalActionTargetColumns([]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const targetScreenId = config.action?.targetScreenId;
|
||||
if (!targetScreenId) return;
|
||||
|
||||
|
||||
const loadModalActionMappingData = async () => {
|
||||
// 1. 소스 테이블 감지 (현재 화면)
|
||||
let sourceTableName: string | null = currentTableName || null;
|
||||
|
||||
|
||||
// allComponents에서 분할패널/테이블리스트/통합목록 감지
|
||||
for (const comp of allComponents) {
|
||||
const compType = comp.componentType || (comp as any).componentConfig?.type;
|
||||
const compConfig = (comp as any).componentConfig || {};
|
||||
|
||||
|
||||
if (compType === "split-panel-layout" || compType === "screen-split-panel") {
|
||||
sourceTableName = compConfig.leftPanel?.tableName || compConfig.tableName || null;
|
||||
if (sourceTableName) break;
|
||||
@@ -389,9 +387,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
if (sourceTableName) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setModalActionSourceTable(sourceTableName);
|
||||
|
||||
|
||||
// 2. 대상 화면의 테이블 조회
|
||||
let targetTableName: string | null = null;
|
||||
try {
|
||||
@@ -405,9 +403,9 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
} catch (error) {
|
||||
console.error("대상 화면 정보 로드 실패:", error);
|
||||
}
|
||||
|
||||
|
||||
setModalActionTargetTable(targetTableName);
|
||||
|
||||
|
||||
// 3. 소스 테이블 컬럼 로드
|
||||
if (sourceTableName) {
|
||||
try {
|
||||
@@ -416,7 +414,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
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,
|
||||
@@ -429,7 +427,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
console.error("소스 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 4. 대상 테이블 컬럼 로드
|
||||
if (targetTableName) {
|
||||
try {
|
||||
@@ -438,7 +436,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
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,
|
||||
@@ -451,7 +449,7 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
console.error("대상 테이블 컬럼 로드 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 5. 기존 필드 매핑 로드 또는 자동 매핑 생성
|
||||
const existingMappings = config.action?.fieldMappings || [];
|
||||
if (existingMappings.length > 0) {
|
||||
@@ -461,10 +459,16 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
setModalActionFieldMappings([]); // 빈 배열 = 자동 매핑
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadModalActionMappingData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config.action?.type, config.action?.autoDetectDataSource, config.action?.targetScreenId, currentTableName, allComponents]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
config.action?.type,
|
||||
config.action?.autoDetectDataSource,
|
||||
config.action?.targetScreenId,
|
||||
currentTableName,
|
||||
allComponents,
|
||||
]);
|
||||
|
||||
// 🆕 현재 테이블 컬럼 로드 (그룹화 컬럼 선택용)
|
||||
useEffect(() => {
|
||||
@@ -818,25 +822,25 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
<SelectItem value="navigate">페이지 이동</SelectItem>
|
||||
<SelectItem value="modal">모달 열기</SelectItem>
|
||||
<SelectItem value="transferData">데이터 전달</SelectItem>
|
||||
|
||||
|
||||
{/* 엑셀 관련 */}
|
||||
<SelectItem value="excel_download">엑셀 다운로드</SelectItem>
|
||||
<SelectItem value="excel_upload">엑셀 업로드</SelectItem>
|
||||
|
||||
|
||||
{/* 고급 기능 */}
|
||||
<SelectItem value="quickInsert">즉시 저장</SelectItem>
|
||||
<SelectItem value="control">제어 흐름</SelectItem>
|
||||
|
||||
|
||||
{/* 특수 기능 (필요 시 사용) */}
|
||||
<SelectItem value="barcode_scan">바코드 스캔</SelectItem>
|
||||
<SelectItem value="operation_control">운행알림 및 종료</SelectItem>
|
||||
|
||||
|
||||
{/* 이벤트 버스 */}
|
||||
<SelectItem value="event">이벤트 발송</SelectItem>
|
||||
|
||||
|
||||
{/* 복사 */}
|
||||
<SelectItem value="copy">복사 (품목코드 초기화)</SelectItem>
|
||||
|
||||
|
||||
{/* 🔒 숨김 처리 - 기존 시스템 호환성 유지, UI에서만 숨김
|
||||
<SelectItem value="openRelatedModal">연관 데이터 버튼 모달 열기</SelectItem>
|
||||
<SelectItem value="openModalWithData">(deprecated) 데이터 전달 + 모달 열기</SelectItem>
|
||||
@@ -985,10 +989,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Label htmlFor="auto-detect-data-source" className="text-sm cursor-pointer">
|
||||
<Label htmlFor="auto-detect-data-source" className="cursor-pointer text-sm">
|
||||
선택된 데이터 전달
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
TableList/SplitPanel에서 선택된 데이터를 모달에 자동으로 전달합니다
|
||||
</p>
|
||||
</div>
|
||||
@@ -996,11 +1000,11 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
|
||||
{/* 🆕 필드 매핑 UI (데이터 전달 활성화 + 테이블이 다른 경우) */}
|
||||
{component.componentConfig?.action?.autoDetectDataSource === true && (
|
||||
<div className="mt-4 space-y-3 rounded-lg border bg-background p-3">
|
||||
<div className="bg-background mt-4 space-y-3 rounded-lg border p-3">
|
||||
{/* 테이블 정보 표시 */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-3 w-3 text-muted-foreground" />
|
||||
<Database className="text-muted-foreground h-3 w-3" />
|
||||
<span className="text-muted-foreground">소스:</span>
|
||||
<span className="font-medium">{modalActionSourceTable || "감지 중..."}</span>
|
||||
</div>
|
||||
@@ -1012,171 +1016,210 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 테이블이 같으면 자동 매핑 안내 */}
|
||||
{modalActionSourceTable && modalActionTargetTable && modalActionSourceTable === modalActionTargetTable && (
|
||||
<div className="rounded-md bg-green-50 p-2 text-xs text-green-700 dark:bg-green-950/30 dark:text-green-400">
|
||||
동일한 테이블입니다. 컬럼명이 같은 필드는 자동으로 매핑됩니다.
|
||||
</div>
|
||||
)}
|
||||
{modalActionSourceTable &&
|
||||
modalActionTargetTable &&
|
||||
modalActionSourceTable === modalActionTargetTable && (
|
||||
<div className="rounded-md bg-green-50 p-2 text-xs text-green-700 dark:bg-green-950/30 dark:text-green-400">
|
||||
동일한 테이블입니다. 컬럼명이 같은 필드는 자동으로 매핑됩니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 테이블이 다르면 필드 매핑 UI 표시 */}
|
||||
{modalActionSourceTable && modalActionTargetTable && modalActionSourceTable !== modalActionTargetTable && (
|
||||
<div className="space-y-2">
|
||||
<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-xs"
|
||||
onClick={() => {
|
||||
const newMappings = [...(component.componentConfig?.action?.fieldMappings || []), { sourceField: "", targetField: "" }];
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(component.componentConfig?.action?.fieldMappings || []).length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
컬럼명이 다른 경우 매핑을 추가하세요. 매핑이 없으면 동일 컬럼명만 전달됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(component.componentConfig?.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{/* 소스 필드 선택 */}
|
||||
<Popover
|
||||
open={modalFieldMappingSourceOpen[index] || false}
|
||||
onOpenChange={(open) => setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 flex-1 justify-between text-xs">
|
||||
{mapping.sourceField
|
||||
? modalActionSourceColumns.find((c) => c.name === mapping.sourceField)?.label || mapping.sourceField
|
||||
: "소스 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
value={modalFieldMappingSourceSearch[index] || ""}
|
||||
onValueChange={(val) => setModalFieldMappingSourceSearch((prev) => ({ ...prev, [index]: val }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalActionSourceColumns
|
||||
.filter((col) =>
|
||||
col.name.toLowerCase().includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()) ||
|
||||
col.label.toLowerCase().includes((modalFieldMappingSourceSearch[index] || "").toLowerCase())
|
||||
)
|
||||
.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={col.name}
|
||||
onSelect={() => {
|
||||
const newMappings = [...(component.componentConfig?.action?.fieldMappings || [])];
|
||||
newMappings[index] = { ...newMappings[index], sourceField: col.name };
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", mapping.sourceField === col.name ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">{col.label}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{col.name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<span className="text-xs text-muted-foreground">→</span>
|
||||
|
||||
{/* 대상 필드 선택 */}
|
||||
<Popover
|
||||
open={modalFieldMappingTargetOpen[index] || false}
|
||||
onOpenChange={(open) => setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: open }))}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 flex-1 justify-between text-xs">
|
||||
{mapping.targetField
|
||||
? modalActionTargetColumns.find((c) => c.name === mapping.targetField)?.label || mapping.targetField
|
||||
: "대상 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
value={modalFieldMappingTargetSearch[index] || ""}
|
||||
onValueChange={(val) => setModalFieldMappingTargetSearch((prev) => ({ ...prev, [index]: val }))}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalActionTargetColumns
|
||||
.filter((col) =>
|
||||
col.name.toLowerCase().includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()) ||
|
||||
col.label.toLowerCase().includes((modalFieldMappingTargetSearch[index] || "").toLowerCase())
|
||||
)
|
||||
.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={col.name}
|
||||
onSelect={() => {
|
||||
const newMappings = [...(component.componentConfig?.action?.fieldMappings || [])];
|
||||
newMappings[index] = { ...newMappings[index], targetField: col.name };
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", mapping.targetField === col.name ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">{col.label}</span>
|
||||
<span className="text-[10px] text-muted-foreground">{col.name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
{modalActionSourceTable &&
|
||||
modalActionTargetTable &&
|
||||
modalActionSourceTable !== modalActionTargetTable && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">필드 매핑</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-destructive hover:bg-destructive/10"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => {
|
||||
const newMappings = (component.componentConfig?.action?.fieldMappings || []).filter((_: any, i: number) => i !== index);
|
||||
const newMappings = [
|
||||
...(component.componentConfig?.action?.fieldMappings || []),
|
||||
{ sourceField: "", targetField: "" },
|
||||
];
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
매핑 추가
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(component.componentConfig?.action?.fieldMappings || []).length === 0 && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
컬럼명이 다른 경우 매핑을 추가하세요. 매핑이 없으면 동일 컬럼명만 전달됩니다.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(component.componentConfig?.action?.fieldMappings || []).map((mapping: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{/* 소스 필드 선택 */}
|
||||
<Popover
|
||||
open={modalFieldMappingSourceOpen[index] || false}
|
||||
onOpenChange={(open) =>
|
||||
setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: open }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 flex-1 justify-between text-xs">
|
||||
{mapping.sourceField
|
||||
? modalActionSourceColumns.find((c) => c.name === mapping.sourceField)?.label ||
|
||||
mapping.sourceField
|
||||
: "소스 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
value={modalFieldMappingSourceSearch[index] || ""}
|
||||
onValueChange={(val) =>
|
||||
setModalFieldMappingSourceSearch((prev) => ({ ...prev, [index]: val }))
|
||||
}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalActionSourceColumns
|
||||
.filter(
|
||||
(col) =>
|
||||
col.name
|
||||
.toLowerCase()
|
||||
.includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()) ||
|
||||
col.label
|
||||
.toLowerCase()
|
||||
.includes((modalFieldMappingSourceSearch[index] || "").toLowerCase()),
|
||||
)
|
||||
.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={col.name}
|
||||
onSelect={() => {
|
||||
const newMappings = [
|
||||
...(component.componentConfig?.action?.fieldMappings || []),
|
||||
];
|
||||
newMappings[index] = { ...newMappings[index], sourceField: col.name };
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
setModalFieldMappingSourceOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
mapping.sourceField === col.name ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">{col.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">{col.name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
|
||||
{/* 대상 필드 선택 */}
|
||||
<Popover
|
||||
open={modalFieldMappingTargetOpen[index] || false}
|
||||
onOpenChange={(open) =>
|
||||
setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: open }))
|
||||
}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 flex-1 justify-between text-xs">
|
||||
{mapping.targetField
|
||||
? modalActionTargetColumns.find((c) => c.name === mapping.targetField)?.label ||
|
||||
mapping.targetField
|
||||
: "대상 컬럼 선택"}
|
||||
<ChevronsUpDown className="ml-1 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="컬럼 검색..."
|
||||
value={modalFieldMappingTargetSearch[index] || ""}
|
||||
onValueChange={(val) =>
|
||||
setModalFieldMappingTargetSearch((prev) => ({ ...prev, [index]: val }))
|
||||
}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>컬럼을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{modalActionTargetColumns
|
||||
.filter(
|
||||
(col) =>
|
||||
col.name
|
||||
.toLowerCase()
|
||||
.includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()) ||
|
||||
col.label
|
||||
.toLowerCase()
|
||||
.includes((modalFieldMappingTargetSearch[index] || "").toLowerCase()),
|
||||
)
|
||||
.map((col) => (
|
||||
<CommandItem
|
||||
key={col.name}
|
||||
value={col.name}
|
||||
onSelect={() => {
|
||||
const newMappings = [
|
||||
...(component.componentConfig?.action?.fieldMappings || []),
|
||||
];
|
||||
newMappings[index] = { ...newMappings[index], targetField: col.name };
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
setModalFieldMappingTargetOpen((prev) => ({ ...prev, [index]: false }));
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
mapping.targetField === col.name ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-medium">{col.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">{col.name}</span>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:bg-destructive/10 h-7 w-7 p-0"
|
||||
onClick={() => {
|
||||
const newMappings = (component.componentConfig?.action?.fieldMappings || []).filter(
|
||||
(_: any, i: number) => i !== index,
|
||||
);
|
||||
setModalActionFieldMappings(newMappings);
|
||||
onUpdateProperty("componentConfig.action.fieldMappings", newMappings);
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1185,9 +1228,10 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
{/* 🆕 데이터 전달 + 모달 열기 액션 설정 (deprecated - 하위 호환성 유지) */}
|
||||
{component.componentConfig?.action?.type === "openModalWithData" && (
|
||||
<div className="mt-4 space-y-4 rounded-lg border bg-amber-50 p-4 dark:bg-amber-950/20">
|
||||
<h4 className="text-sm font-medium text-foreground">데이터 전달 + 모달 설정</h4>
|
||||
<h4 className="text-foreground text-sm font-medium">데이터 전달 + 모달 설정</h4>
|
||||
<p className="text-xs text-amber-600 dark:text-amber-400">
|
||||
이 옵션은 "모달 열기" 액션으로 통합되었습니다. 새 개발에서는 "모달 열기" + "선택된 데이터 전달"을 사용하세요.
|
||||
이 옵션은 "모달 열기" 액션으로 통합되었습니다. 새 개발에서는 "모달 열기" + "선택된 데이터 전달"을
|
||||
사용하세요.
|
||||
</p>
|
||||
|
||||
{/* 🆕 블록 기반 제목 빌더 */}
|
||||
@@ -3546,8 +3590,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
<div className="bg-muted/50 mt-4 space-y-4 rounded-lg border p-4">
|
||||
<h4 className="text-foreground text-sm font-medium">이벤트 발송 설정</h4>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
버튼 클릭 시 V2 이벤트 버스를 통해 이벤트를 발송합니다.
|
||||
다른 컴포넌트나 서비스에서 이 이벤트를 수신하여 처리할 수 있습니다.
|
||||
버튼 클릭 시 V2 이벤트 버스를 통해 이벤트를 발송합니다. 다른 컴포넌트나 서비스에서 이 이벤트를 수신하여
|
||||
처리할 수 있습니다.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -3597,11 +3641,13 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
type="number"
|
||||
className="h-8 text-xs"
|
||||
placeholder="3"
|
||||
value={component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.leadTimeDays || 3}
|
||||
value={
|
||||
component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.leadTimeDays || 3
|
||||
}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty(
|
||||
"componentConfig.action.eventConfig.eventPayload.config.scheduling.leadTimeDays",
|
||||
parseInt(e.target.value) || 3
|
||||
parseInt(e.target.value) || 3,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -3613,11 +3659,14 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
type="number"
|
||||
className="h-8 text-xs"
|
||||
placeholder="100"
|
||||
value={component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling?.maxDailyCapacity || 100}
|
||||
value={
|
||||
component.componentConfig?.action?.eventConfig?.eventPayload?.config?.scheduling
|
||||
?.maxDailyCapacity || 100
|
||||
}
|
||||
onChange={(e) => {
|
||||
onUpdateProperty(
|
||||
"componentConfig.action.eventConfig.eventPayload.config.scheduling.maxDailyCapacity",
|
||||
parseInt(e.target.value) || 100
|
||||
parseInt(e.target.value) || 100,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -3625,8 +3674,8 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
|
||||
|
||||
<div className="rounded-md bg-blue-50 p-2 dark:bg-blue-950/20">
|
||||
<p className="text-xs text-blue-800 dark:text-blue-200">
|
||||
<strong>동작 방식:</strong> 테이블에서 선택된 데이터를 기반으로 스케줄을 자동 생성합니다.
|
||||
생성 전 미리보기 확인 다이얼로그가 표시됩니다.
|
||||
<strong>동작 방식:</strong> 테이블에서 선택된 데이터를 기반으로 스케줄을 자동 생성합니다. 생성 전
|
||||
미리보기 확인 다이얼로그가 표시됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function TabsWidget({
|
||||
const [selectedTab, setSelectedTab] = useState<string>(getInitialTab());
|
||||
const [visibleTabs, setVisibleTabs] = useState<ExtendedTabItem[]>(tabs as ExtendedTabItem[]);
|
||||
const [mountedTabs, setMountedTabs] = useState<Set<string>>(() => new Set([getInitialTab()]));
|
||||
|
||||
|
||||
// 🆕 화면 진입 시 첫 번째 탭 자동 선택 및 마운트
|
||||
useEffect(() => {
|
||||
// 현재 선택된 탭이 유효하지 않거나 비어있으면 첫 번째 탭 선택
|
||||
@@ -92,7 +92,7 @@ export function TabsWidget({
|
||||
});
|
||||
}
|
||||
}, [tabs]); // tabs가 변경될 때마다 실행
|
||||
|
||||
|
||||
// screenId 기반 화면 로드 상태
|
||||
const [screenLayouts, setScreenLayouts] = useState<Record<string, ComponentData[]>>({});
|
||||
const [screenLoadingStates, setScreenLoadingStates] = useState<Record<string, boolean>>({});
|
||||
@@ -109,23 +109,28 @@ export function TabsWidget({
|
||||
for (const tab of visibleTabs) {
|
||||
const extTab = tab as ExtendedTabItem;
|
||||
// screenId가 있고, 아직 로드하지 않았으며, 인라인 컴포넌트가 없는 경우만 로드
|
||||
if (extTab.screenId && !screenLayouts[tab.id] && !screenLoadingStates[tab.id] && (!extTab.components || extTab.components.length === 0)) {
|
||||
setScreenLoadingStates(prev => ({ ...prev, [tab.id]: true }));
|
||||
if (
|
||||
extTab.screenId &&
|
||||
!screenLayouts[tab.id] &&
|
||||
!screenLoadingStates[tab.id] &&
|
||||
(!extTab.components || extTab.components.length === 0)
|
||||
) {
|
||||
setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true }));
|
||||
try {
|
||||
const layoutData = await screenApi.getLayout(extTab.screenId);
|
||||
if (layoutData && layoutData.components) {
|
||||
setScreenLayouts(prev => ({ ...prev, [tab.id]: layoutData.components }));
|
||||
setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`탭 "${tab.label}" 화면 로드 실패:`, error);
|
||||
setScreenErrors(prev => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." }));
|
||||
setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." }));
|
||||
} finally {
|
||||
setScreenLoadingStates(prev => ({ ...prev, [tab.id]: false }));
|
||||
setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadScreenLayouts();
|
||||
}, [visibleTabs, screenLayouts, screenLoadingStates]);
|
||||
|
||||
@@ -180,11 +185,7 @@ export function TabsWidget({
|
||||
const getTabsListClass = () => {
|
||||
const baseClass = orientation === "vertical" ? "flex-col" : "";
|
||||
const variantClass =
|
||||
variant === "pills"
|
||||
? "bg-muted p-1 rounded-lg"
|
||||
: variant === "underline"
|
||||
? "border-b"
|
||||
: "bg-muted p-1";
|
||||
variant === "pills" ? "bg-muted p-1 rounded-lg" : variant === "underline" ? "border-b" : "bg-muted p-1";
|
||||
return `${baseClass} ${variantClass}`;
|
||||
};
|
||||
|
||||
@@ -192,47 +193,47 @@ export function TabsWidget({
|
||||
const renderTabContent = (tab: ExtendedTabItem) => {
|
||||
const extTab = tab as ExtendedTabItem;
|
||||
const inlineComponents = tab.components || [];
|
||||
|
||||
|
||||
// 1. screenId가 있고 인라인 컴포넌트가 없는 경우 -> 화면 로드 방식
|
||||
if (extTab.screenId && inlineComponents.length === 0) {
|
||||
// 로딩 중
|
||||
if (screenLoadingStates[tab.id]) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2 text-muted-foreground">화면을 불러오는 중...</span>
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2">화면을 불러오는 중...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 에러 발생
|
||||
if (screenErrors[tab.id]) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-destructive/50 bg-destructive/5">
|
||||
<div className="border-destructive/50 bg-destructive/5 flex h-full w-full items-center justify-center rounded border-2 border-dashed">
|
||||
<p className="text-destructive text-sm">{screenErrors[tab.id]}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 화면 레이아웃이 로드된 경우
|
||||
const loadedComponents = screenLayouts[tab.id];
|
||||
if (loadedComponents && loadedComponents.length > 0) {
|
||||
return renderScreenComponents(loadedComponents);
|
||||
}
|
||||
|
||||
|
||||
// 아직 로드되지 않은 경우
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 2. 인라인 컴포넌트가 있는 경우 -> 기존 v2 방식
|
||||
if (inlineComponents.length > 0) {
|
||||
return renderInlineComponents(tab, inlineComponents);
|
||||
}
|
||||
|
||||
|
||||
// 3. 둘 다 없는 경우
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
@@ -246,22 +247,17 @@ export function TabsWidget({
|
||||
// screenId로 로드한 화면 컴포넌트 렌더링
|
||||
const renderScreenComponents = (components: ComponentData[]) => {
|
||||
// InteractiveScreenViewerDynamic 동적 로드
|
||||
const InteractiveScreenViewerDynamic = require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic;
|
||||
|
||||
const InteractiveScreenViewerDynamic =
|
||||
require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic;
|
||||
|
||||
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
||||
const maxBottom = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
300
|
||||
);
|
||||
const maxRight = Math.max(
|
||||
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
|
||||
400
|
||||
);
|
||||
|
||||
const maxBottom = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)), 300);
|
||||
const maxRight = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), 400);
|
||||
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
className="relative h-full w-full overflow-auto"
|
||||
style={{
|
||||
style={{
|
||||
minHeight: maxBottom + 20,
|
||||
minWidth: maxRight + 20,
|
||||
}}
|
||||
@@ -295,17 +291,17 @@ export function TabsWidget({
|
||||
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
||||
const maxBottom = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
300 // 최소 높이
|
||||
300, // 최소 높이
|
||||
);
|
||||
const maxRight = Math.max(
|
||||
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
|
||||
400 // 최소 너비
|
||||
400, // 최소 너비
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
minHeight: maxBottom + 20,
|
||||
minWidth: maxRight + 20,
|
||||
}}
|
||||
@@ -319,7 +315,7 @@ export function TabsWidget({
|
||||
className={cn(
|
||||
"absolute",
|
||||
isDesignMode && "cursor-move",
|
||||
isDesignMode && isSelected && "ring-2 ring-primary ring-offset-2"
|
||||
isDesignMode && isSelected && "ring-primary ring-2 ring-offset-2",
|
||||
)}
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
@@ -380,9 +376,7 @@ export function TabsWidget({
|
||||
<TabsTrigger value={tab.id} disabled={tab.disabled} className="relative pr-8">
|
||||
{tab.label}
|
||||
{tab.components && tab.components.length > 0 && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
({tab.components.length})
|
||||
</span>
|
||||
<span className="text-muted-foreground ml-1 text-xs">({tab.components.length})</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
{allowCloseable && (
|
||||
@@ -390,7 +384,7 @@ export function TabsWidget({
|
||||
onClick={(e) => handleCloseTab(tab.id, e)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 p-0 hover:bg-destructive/10"
|
||||
className="hover:bg-destructive/10 absolute top-1/2 right-1 h-5 w-5 -translate-y-1/2 p-0"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user