제어관리 데이터 저장기능

This commit is contained in:
kjs
2025-09-26 13:52:32 +09:00
parent 2a4e379dc4
commit 9454e3a81f
17 changed files with 1417 additions and 781 deletions

View File

@@ -24,8 +24,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
// API import
import { getColumnsFromConnection } from "@/lib/api/multiConnection";
// API import (컬럼 로드는 중앙에서 관리)
// 타입 import
import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection";
@@ -40,6 +39,9 @@ interface MultiActionConfigStepProps {
toTable?: TableInfo;
fromConnection?: Connection;
toConnection?: Connection;
// 컬럼 정보 (중앙에서 관리) 🔧 추가
fromColumns?: ColumnInfo[];
toColumns?: ColumnInfo[];
// 제어 조건 관련
controlConditions: any[];
onUpdateControlCondition: (index: number, condition: any) => void;
@@ -47,12 +49,14 @@ interface MultiActionConfigStepProps {
onAddControlCondition: () => void;
// 액션 그룹 관련
actionGroups: ActionGroup[];
groupsLogicalOperator?: "AND" | "OR";
onUpdateActionGroup: (groupId: string, updates: Partial<ActionGroup>) => void;
onDeleteActionGroup: (groupId: string) => void;
onAddActionGroup: () => void;
onAddActionToGroup: (groupId: string) => void;
onUpdateActionInGroup: (groupId: string, actionId: string, updates: Partial<SingleAction>) => void;
onDeleteActionFromGroup: (groupId: string, actionId: string) => void;
onSetGroupsLogicalOperator?: (operator: "AND" | "OR") => void;
// 필드 매핑 관련
fieldMappings: FieldMapping[];
onCreateMapping: (fromField: ColumnInfo, toField: ColumnInfo) => void;
@@ -60,6 +64,8 @@ interface MultiActionConfigStepProps {
// 네비게이션
onNext: () => void;
onBack: () => void;
// 컬럼 로드 액션
onLoadColumns?: () => Promise<void>;
}
/**
@@ -75,55 +81,41 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
toTable,
fromConnection,
toConnection,
fromColumns = [], // 🔧 중앙에서 관리되는 컬럼 정보
toColumns = [], // 🔧 중앙에서 관리되는 컬럼 정보
controlConditions,
onUpdateControlCondition,
onDeleteControlCondition,
onAddControlCondition,
actionGroups,
groupsLogicalOperator = "AND",
onUpdateActionGroup,
onDeleteActionGroup,
onAddActionGroup,
onAddActionToGroup,
onUpdateActionInGroup,
onDeleteActionFromGroup,
onSetGroupsLogicalOperator,
fieldMappings,
onCreateMapping,
onDeleteMapping,
onNext,
onBack,
onLoadColumns,
}) => {
const [fromColumns, setFromColumns] = useState<ColumnInfo[]>([]);
const [toColumns, setToColumns] = useState<ColumnInfo[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(["group_1"])); // 첫 번째 그룹은 기본 열림
const [activeTab, setActiveTab] = useState<"control" | "actions" | "mapping">("control"); // 현재 활성 탭
// 컬럼 정보 로드
// 컬럼 로딩 상태 확인
const isColumnsLoaded = fromColumns.length > 0 && toColumns.length > 0;
// 컴포넌트 마운트 시 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
if (!fromConnection || !toConnection || !fromTable || !toTable) {
return;
}
try {
setIsLoading(true);
const [fromCols, toCols] = await Promise.all([
getColumnsFromConnection(fromConnection.id, fromTable.tableName),
getColumnsFromConnection(toConnection.id, toTable.tableName),
]);
setFromColumns(Array.isArray(fromCols) ? fromCols : []);
setToColumns(Array.isArray(toCols) ? toCols : []);
} catch (error) {
console.error("❌ 컬럼 정보 로드 실패:", error);
toast.error("필드 정보를 불러오는데 실패했습니다.");
} finally {
setIsLoading(false);
}
};
loadColumns();
}, [fromConnection, toConnection, fromTable, toTable]);
if (!isColumnsLoaded && fromConnection && toConnection && fromTable && toTable && onLoadColumns) {
console.log("🔄 MultiActionConfigStep: 컬럼 로드 시작");
onLoadColumns();
}
}, [isColumnsLoaded, fromConnection?.id, toConnection?.id, fromTable?.tableName, toTable?.tableName]);
// 그룹 확장/축소 토글
const toggleGroupExpansion = (groupId: string) => {
@@ -171,13 +163,10 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
group.actions.some((action) => action.actionType === "insert" && action.isEnabled),
);
// 탭 정보
// 탭 정보 (컬럼 매핑 탭 제거)
const tabs = [
{ id: "control" as const, label: "제어 조건", icon: "🎯", description: "전체 제어 실행 조건" },
{ id: "actions" as const, label: "액션 설정", icon: "⚙️", description: "액션 그룹 및 실행 조건" },
...(hasInsertActions
? [{ id: "mapping" as const, label: "컬럼 매핑", icon: "🔗", description: "INSERT 액션 필드 매핑" }]
: []),
];
return (
@@ -280,265 +269,405 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
</Button>
</div>
{/* 액션 그룹 목록 */}
<div className="space-y-4">
{actionGroups.map((group, groupIndex) => (
<div key={group.id} className="bg-card rounded-lg border">
{/* 그룹 헤더 */}
<Collapsible
open={expandedGroups.has(group.id)}
onOpenChange={() => toggleGroupExpansion(group.id)}
>
<CollapsibleTrigger asChild>
<div className="hover:bg-muted/50 flex cursor-pointer items-center justify-between p-4">
<div className="flex items-center gap-3">
{expandedGroups.has(group.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<div className="flex items-center gap-2">
<Input
value={group.name}
onChange={(e) => onUpdateActionGroup(group.id, { name: e.target.value })}
className="h-8 w-40"
onClick={(e) => e.stopPropagation()}
/>
<Badge className={getLogicalOperatorColor(group.logicalOperator)}>
{group.logicalOperator}
</Badge>
<Badge variant={group.isEnabled ? "default" : "secondary"}>
{group.actions.length}
</Badge>
</div>
</div>
<div className="flex items-center gap-2">
{/* 그룹 논리 연산자 선택 */}
<Select
value={group.logicalOperator}
onValueChange={(value: "AND" | "OR") =>
onUpdateActionGroup(group.id, { logicalOperator: value })
}
>
<SelectTrigger className="h-8 w-20" onClick={(e) => e.stopPropagation()}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
{/* 그룹 활성화/비활성화 */}
<Switch
checked={group.isEnabled}
onCheckedChange={(checked) => onUpdateActionGroup(group.id, { isEnabled: checked })}
onClick={(e) => e.stopPropagation()}
/>
{/* 그룹 삭제 */}
{actionGroups.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onDeleteActionGroup(group.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CollapsibleTrigger>
{/* 그룹 내용 */}
<CollapsibleContent>
<div className="bg-muted/20 border-t p-4">
{/* 액션 추가 버튼 */}
<div className="mb-4 flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => onAddActionToGroup(group.id)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 액션 목록 */}
<div className="space-y-3">
{group.actions.map((action, actionIndex) => (
<div key={action.id} className="rounded-md border bg-white p-3">
{/* 액션 헤더 */}
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-lg">{getActionTypeIcon(action.actionType)}</span>
<Input
value={action.name}
onChange={(e) =>
onUpdateActionInGroup(group.id, action.id, { name: e.target.value })
}
className="h-8 w-32"
/>
<Select
value={action.actionType}
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
onUpdateActionInGroup(group.id, action.id, { actionType: value })
}
>
<SelectTrigger className="h-8 w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="insert">INSERT</SelectItem>
<SelectItem value="update">UPDATE</SelectItem>
<SelectItem value="delete">DELETE</SelectItem>
<SelectItem value="upsert">UPSERT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Switch
checked={action.isEnabled}
onCheckedChange={(checked) =>
onUpdateActionInGroup(group.id, action.id, { isEnabled: checked })
}
/>
{group.actions.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => onDeleteActionFromGroup(group.id, action.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
{/* 액션 조건 설정 */}
<ActionConditionBuilder
actionType={action.actionType}
fromColumns={fromColumns}
toColumns={toColumns}
conditions={action.conditions}
fieldMappings={action.fieldMappings}
onConditionsChange={(conditions) =>
onUpdateActionInGroup(group.id, action.id, { conditions })
}
onFieldMappingsChange={(fieldMappings) =>
onUpdateActionInGroup(group.id, action.id, { fieldMappings })
}
/>
</div>
))}
</div>
{/* 그룹 로직 설명 */}
<div className="mt-4 rounded-md bg-blue-50 p-3">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 text-blue-600" />
<div className="text-sm">
<div className="font-medium text-blue-900">{group.logicalOperator} </div>
<div className="text-blue-700">
{group.logicalOperator === "AND"
? "이 그룹의 모든 액션이 실행 가능한 조건일 때만 실행됩니다."
: "이 그룹의 액션 중 하나라도 실행 가능한 조건이면 해당 액션만 실행됩니다."}
</div>
</div>
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
{/* 그룹 간 연결선 (마지막 그룹이 아닌 경우) */}
{groupIndex < actionGroups.length - 1 && (
<div className="flex justify-center py-2">
<div className="text-muted-foreground flex items-center gap-2 text-xs">
<div className="bg-border h-px w-8"></div>
<span> </span>
<div className="bg-border h-px w-8"></div>
</div>
</div>
)}
{/* 그룹 간 논리 연산자 선택 */}
{actionGroups.length > 1 && (
<div className="rounded-md border bg-blue-50 p-3">
<div className="mb-2 flex items-center gap-2">
<h4 className="text-sm font-medium text-blue-900"> </h4>
</div>
))}
</div>
</div>
)}
{activeTab === "mapping" && hasInsertActions && (
<div className="space-y-4">
{/* 컬럼 매핑 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<h3 className="text-lg font-medium"> </h3>
<Badge variant="outline" className="text-xs">
{fieldMappings.length}
</Badge>
</div>
<div className="text-muted-foreground text-sm">INSERT </div>
</div>
{/* 컬럼 매핑 캔버스 */}
{isLoading ? (
<div className="flex h-64 items-center justify-center">
<div className="text-muted-foreground"> ...</div>
</div>
) : fromColumns.length > 0 && toColumns.length > 0 ? (
<div className="rounded-lg border bg-white p-4">
<FieldMappingCanvas
fromFields={fromColumns}
toFields={toColumns}
mappings={fieldMappings}
onCreateMapping={onCreateMapping}
onDeleteMapping={onDeleteMapping}
/>
</div>
) : (
<div className="flex h-64 flex-col items-center justify-center space-y-3 rounded-lg border border-dashed">
<AlertTriangle className="text-muted-foreground h-8 w-8" />
<div className="text-muted-foreground"> .</div>
<div className="text-muted-foreground text-xs">
FROM : {fromColumns.length}, TO : {toColumns.length}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<input
type="radio"
id="groups-and"
name="groups-operator"
checked={groupsLogicalOperator === "AND"}
onChange={() => onSetGroupsLogicalOperator?.("AND")}
className="h-4 w-4"
/>
<label htmlFor="groups-and" className="text-sm text-blue-800">
<span className="font-medium">AND</span> -
</label>
</div>
<div className="flex items-center gap-2">
<input
type="radio"
id="groups-or"
name="groups-operator"
checked={groupsLogicalOperator === "OR"}
onChange={() => onSetGroupsLogicalOperator?.("OR")}
className="h-4 w-4"
/>
<label htmlFor="groups-or" className="text-sm text-blue-800">
<span className="font-medium">OR</span> -
</label>
</div>
</div>
</div>
)}
{/* 매핑되지 않은 필드 처리 옵션 */}
<div className="rounded-md border bg-yellow-50 p-4">
<h4 className="mb-3 flex items-center gap-2 font-medium text-yellow-800">
<AlertTriangle className="h-4 w-4" />
</h4>
<div className="space-y-3 text-sm">
<div className="flex items-center gap-2">
<input type="radio" id="empty" name="unmapped-strategy" defaultChecked className="h-4 w-4" />
<label htmlFor="empty" className="text-yellow-700">
(NULL )
</label>
{/* 액션 그룹 목록 */}
<div className="space-y-4">
{actionGroups.map((group, groupIndex) => (
<div key={group.id}>
{/* 그룹 간 논리 연산자 표시 (첫 번째 그룹 제외) */}
{groupIndex > 0 && (
<div className="my-2 flex items-center justify-center">
<div
className={`rounded px-2 py-1 text-xs font-medium ${
groupsLogicalOperator === "AND"
? "bg-green-100 text-green-800"
: "bg-orange-100 text-orange-800"
}`}
>
{groupsLogicalOperator}
</div>
</div>
)}
<div className="bg-card rounded-lg border">
{/* 그룹 헤더 */}
<Collapsible
open={expandedGroups.has(group.id)}
onOpenChange={() => toggleGroupExpansion(group.id)}
>
<CollapsibleTrigger asChild>
<div className="hover:bg-muted/50 flex cursor-pointer items-center justify-between p-4">
<div className="flex items-center gap-3">
{expandedGroups.has(group.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<div className="flex items-center gap-2">
<Input
value={group.name}
onChange={(e) => onUpdateActionGroup(group.id, { name: e.target.value })}
className="h-8 w-40"
onClick={(e) => e.stopPropagation()}
/>
<Badge className={getLogicalOperatorColor(group.logicalOperator)}>
{group.logicalOperator}
</Badge>
<Badge variant={group.isEnabled ? "default" : "secondary"}>
{group.actions.length}
</Badge>
</div>
</div>
<div className="flex items-center gap-2">
{/* 그룹 논리 연산자 선택 */}
<Select
value={group.logicalOperator}
onValueChange={(value: "AND" | "OR") =>
onUpdateActionGroup(group.id, { logicalOperator: value })
}
>
<SelectTrigger className="h-8 w-20" onClick={(e) => e.stopPropagation()}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
{/* 그룹 활성화/비활성화 */}
<Switch
checked={group.isEnabled}
onCheckedChange={(checked) => onUpdateActionGroup(group.id, { isEnabled: checked })}
onClick={(e) => e.stopPropagation()}
/>
{/* 그룹 삭제 */}
{actionGroups.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onDeleteActionGroup(group.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CollapsibleTrigger>
{/* 그룹 내용 */}
<CollapsibleContent>
<div className="bg-muted/20 border-t p-4">
{/* 액션 추가 버튼 */}
<div className="mb-4 flex justify-end">
<Button
variant="outline"
size="sm"
onClick={() => onAddActionToGroup(group.id)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 액션 목록 */}
<div className="space-y-3">
{group.actions.map((action, actionIndex) => (
<div key={action.id} className="rounded-md border bg-white p-3">
{/* 액션 헤더 */}
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-lg">{getActionTypeIcon(action.actionType)}</span>
<Input
value={action.name}
onChange={(e) =>
onUpdateActionInGroup(group.id, action.id, { name: e.target.value })
}
className="h-8 w-32"
/>
<Select
value={action.actionType}
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
onUpdateActionInGroup(group.id, action.id, { actionType: value })
}
>
<SelectTrigger className="h-8 w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="insert">INSERT</SelectItem>
<SelectItem value="update">UPDATE</SelectItem>
<SelectItem value="delete">DELETE</SelectItem>
<SelectItem value="upsert">UPSERT</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Switch
checked={action.isEnabled}
onCheckedChange={(checked) =>
onUpdateActionInGroup(group.id, action.id, { isEnabled: checked })
}
/>
{group.actions.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => onDeleteActionFromGroup(group.id, action.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
{/* 액션 조건 설정 */}
{isColumnsLoaded ? (
<ActionConditionBuilder
actionType={action.actionType}
fromColumns={fromColumns}
toColumns={toColumns}
conditions={action.conditions}
fieldMappings={(() => {
// 필드값 설정용: FieldValueMapping 타입만 필터링
const fieldValueMappings = (action.fieldMappings || []).filter(
(mapping) =>
mapping.valueType && // valueType이 있고
!mapping.fromField && // fromField가 없고
!mapping.toField, // toField가 없으면 FieldValueMapping
);
console.log("📋 ActionConditionBuilder에 전달되는 필드값 설정:", {
allMappings: action.fieldMappings,
filteredFieldValueMappings: fieldValueMappings,
});
return fieldValueMappings;
})()}
columnMappings={
// 컬럼 매핑용: FieldMapping 타입만 필터링
(action.fieldMappings || []).filter(
(mapping) =>
mapping.fromField &&
mapping.toField &&
mapping.fromField.columnName &&
mapping.toField.columnName,
)
}
onConditionsChange={(conditions) =>
onUpdateActionInGroup(group.id, action.id, { conditions })
}
onFieldMappingsChange={(newFieldMappings) => {
// 필드값 설정만 업데이트, 컬럼 매핑은 유지
const existingColumnMappings = (action.fieldMappings || []).filter(
(mapping) =>
mapping.fromField &&
mapping.toField &&
mapping.fromField.columnName &&
mapping.toField.columnName,
);
console.log("🔄 필드값 설정 업데이트:", {
existingColumnMappings,
newFieldMappings,
combined: [...existingColumnMappings, ...newFieldMappings],
});
onUpdateActionInGroup(group.id, action.id, {
fieldMappings: [...existingColumnMappings, ...newFieldMappings],
});
}}
/>
) : (
<div className="text-muted-foreground flex items-center justify-center py-4">
...
</div>
)}
{/* INSERT 액션일 때만 필드 매핑 UI 표시 */}
{action.actionType === "insert" && isColumnsLoaded && (
<div className="mt-4 space-y-3">
<Separator />
<div className="flex items-center justify-between">
<h5 className="text-sm font-medium"> </h5>
<Badge variant="outline" className="text-xs">
{action.fieldMappings?.length || 0}
</Badge>
</div>
{/* 컬럼 매핑 캔버스 */}
<div className="rounded-lg border bg-white p-3">
<FieldMappingCanvas
fromFields={fromColumns}
toFields={toColumns}
mappings={
// 컬럼 매핑만 FieldMappingCanvas에 전달
(action.fieldMappings || []).filter(
(mapping) =>
mapping.fromField &&
mapping.toField &&
mapping.fromField.columnName &&
mapping.toField.columnName,
)
}
onCreateMapping={(fromField, toField) => {
const newMapping = {
id: `${fromField.columnName}_to_${toField.columnName}_${Date.now()}`,
fromField,
toField,
isValid: true,
validationMessage: undefined,
};
// 기존 필드값 설정은 유지하고 새 컬럼 매핑만 추가
const existingFieldValueMappings = (action.fieldMappings || []).filter(
(mapping) =>
mapping.valueType && // valueType이 있고
!mapping.fromField && // fromField가 없고
!mapping.toField, // toField가 없으면 FieldValueMapping
);
const existingColumnMappings = (action.fieldMappings || []).filter(
(mapping) =>
mapping.fromField &&
mapping.toField &&
mapping.fromField.columnName &&
mapping.toField.columnName,
);
onUpdateActionInGroup(group.id, action.id, {
fieldMappings: [
...existingFieldValueMappings,
...existingColumnMappings,
newMapping,
],
});
}}
onDeleteMapping={(mappingId) => {
// 컬럼 매핑만 삭제하고 필드값 설정은 유지
const remainingMappings = (action.fieldMappings || []).filter(
(mapping) => mapping.id !== mappingId,
);
onUpdateActionInGroup(group.id, action.id, {
fieldMappings: remainingMappings,
});
}}
/>
</div>
{/* 매핑되지 않은 필드 처리 옵션 */}
<div className="rounded-md border bg-yellow-50 p-3">
<h6 className="mb-2 flex items-center gap-1 text-xs font-medium text-yellow-800">
<AlertTriangle className="h-3 w-3" />
</h6>
<div className="space-y-2 text-xs">
<div className="flex items-center gap-2">
<input
type="radio"
id={`empty-${action.id}`}
name={`unmapped-${action.id}`}
defaultChecked
className="h-3 w-3"
/>
<label htmlFor={`empty-${action.id}`} className="text-yellow-700">
(NULL )
</label>
</div>
<div className="flex items-center gap-2">
<input
type="radio"
id={`default-${action.id}`}
name={`unmapped-${action.id}`}
className="h-3 w-3"
/>
<label htmlFor={`default-${action.id}`} className="text-yellow-700">
</label>
</div>
<div className="flex items-center gap-2">
<input
type="radio"
id={`skip-${action.id}`}
name={`unmapped-${action.id}`}
className="h-3 w-3"
/>
<label htmlFor={`skip-${action.id}`} className="text-yellow-700">
</label>
</div>
</div>
</div>
</div>
)}
</div>
))}
</div>
{/* 그룹 로직 설명 */}
<div className="mt-4 rounded-md bg-blue-50 p-3">
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 text-blue-600" />
<div className="text-sm">
<div className="font-medium text-blue-900">{group.logicalOperator} </div>
<div className="text-blue-700">
{group.logicalOperator === "AND"
? "이 그룹의 모든 액션이 실행 가능한 조건일 때만 실행됩니다."
: "이 그룹의 액션 중 하나라도 실행 가능한 조건이면 해당 액션만 실행됩니다."}
</div>
</div>
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
</div>
<div className="flex items-center gap-2">
<input type="radio" id="default" name="unmapped-strategy" className="h-4 w-4" />
<label htmlFor="default" className="text-yellow-700">
( DEFAULT )
</label>
</div>
<div className="flex items-center gap-2">
<input type="radio" id="skip" name="unmapped-strategy" className="h-4 w-4" />
<label htmlFor="skip" className="text-yellow-700">
(INSERT )
</label>
</div>
</div>
))}
</div>
</div>
)}