[agent-pipeline] pipe-20260329052843-hdtq round-1
This commit is contained in:
@@ -22,7 +22,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [apiConnections, setApiConnections] = useState<ExternalApiConnection[]>([]);
|
||||
const [selectedConnectionId, setSelectedConnectionId] = useState<string>(dataSource.external_connection_id || "");
|
||||
const [selectedConnectionId, setSelectedConnectionId] = useState<string>(dataSource.externalConnectionId || "");
|
||||
const [availableColumns, setAvailableColumns] = useState<string[]>([]); // API 테스트 후 발견된 컬럼 목록
|
||||
const [columnTypes, setColumnTypes] = useState<Record<string, string>>({}); // 컬럼 타입 정보
|
||||
const [sampleData, setSampleData] = useState<any[]>([]); // 샘플 데이터 (최대 3개)
|
||||
@@ -37,12 +37,12 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
loadApiConnections();
|
||||
}, []);
|
||||
|
||||
// dataSource.external_connection_id가 변경되면 selectedConnectionId 업데이트
|
||||
// dataSource.externalConnectionId가 변경되면 selectedConnectionId 업데이트
|
||||
useEffect(() => {
|
||||
if (dataSource.external_connection_id) {
|
||||
setSelectedConnectionId(dataSource.external_connection_id);
|
||||
if (dataSource.externalConnectionId) {
|
||||
setSelectedConnectionId(dataSource.externalConnectionId);
|
||||
}
|
||||
}, [dataSource.external_connection_id]);
|
||||
}, [dataSource.externalConnectionId]);
|
||||
|
||||
// 외부 커넥션 선택 핸들러
|
||||
const handleConnectionSelect = async (connectionId: string) => {
|
||||
@@ -67,7 +67,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
const updates: Partial<ChartDataSource> = {
|
||||
endpoint: fullEndpoint,
|
||||
external_connection_id: connectionId, // 외부 연결 ID 저장
|
||||
externalConnectionId: connectionId, // 외부 연결 ID 저장
|
||||
};
|
||||
|
||||
const headers: KeyValuePair[] = [];
|
||||
@@ -153,7 +153,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
updates.headers = headers;
|
||||
}
|
||||
if (queryParams.length > 0) {
|
||||
updates.query_params = queryParams;
|
||||
updates.queryParams = queryParams;
|
||||
}
|
||||
|
||||
onChange(updates);
|
||||
@@ -183,24 +183,24 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
// 쿼리 파라미터 추가
|
||||
const handleAddQueryParam = () => {
|
||||
const queryParams = dataSource.query_params || [];
|
||||
const queryParams = dataSource.queryParams || [];
|
||||
onChange({
|
||||
query_params: [...queryParams, { id: Date.now().toString(), key: "", value: "" }],
|
||||
queryParams: [...queryParams, { id: Date.now().toString(), key: "", value: "" }],
|
||||
});
|
||||
};
|
||||
|
||||
// 쿼리 파라미터 삭제
|
||||
const handleDeleteQueryParam = (id: string) => {
|
||||
const queryParams = (dataSource.query_params || []).filter((q) => q.id !== id);
|
||||
onChange({ query_params: queryParams });
|
||||
const queryParams = (dataSource.queryParams || []).filter((q) => q.id !== id);
|
||||
onChange({ queryParams });
|
||||
};
|
||||
|
||||
// 쿼리 파라미터 업데이트
|
||||
const handleUpdateQueryParam = (id: string, field: "key" | "value", value: string) => {
|
||||
const queryParams = (dataSource.query_params || []).map((q) =>
|
||||
const queryParams = (dataSource.queryParams || []).map((q) =>
|
||||
q.id === id ? { ...q, [field]: value } : q
|
||||
);
|
||||
onChange({ query_params: queryParams });
|
||||
onChange({ queryParams });
|
||||
};
|
||||
|
||||
// API 테스트
|
||||
@@ -215,7 +215,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
try {
|
||||
const queryParams: Record<string, string> = {};
|
||||
(dataSource.query_params || []).forEach((param) => {
|
||||
(dataSource.queryParams || []).forEach((param) => {
|
||||
if (param.key && param.value) {
|
||||
queryParams[param.key] = param.value;
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
headers,
|
||||
query_params: queryParams,
|
||||
body: bodyPayload,
|
||||
external_connection_id: dataSource.external_connection_id, // 외부 연결 ID 전달
|
||||
external_connection_id: dataSource.externalConnectionId, // 외부 연결 ID 전달
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -257,20 +257,20 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
try {
|
||||
const lines = text.split('\n').filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed &&
|
||||
!trimmed.startsWith('#') &&
|
||||
return trimmed &&
|
||||
!trimmed.startsWith('#') &&
|
||||
!trimmed.startsWith('=') &&
|
||||
!trimmed.startsWith('---');
|
||||
});
|
||||
|
||||
|
||||
if (lines.length === 0) return [];
|
||||
|
||||
|
||||
const result: any[] = [];
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const values = line.split(',').map(v => v.trim().replace(/,=$/g, ''));
|
||||
|
||||
|
||||
// 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명
|
||||
if (values.length >= 4) {
|
||||
const obj: any = {
|
||||
@@ -285,11 +285,11 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
description: values.slice(8).join(', ').trim() || '',
|
||||
name: values[3] || values[1] || values[0], // 하위 지역명 우선
|
||||
};
|
||||
|
||||
|
||||
result.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error("❌ 텍스트 파싱 오류:", error);
|
||||
@@ -299,29 +299,29 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
// JSON Path로 데이터 추출
|
||||
let data = result.data;
|
||||
|
||||
|
||||
// 텍스트 데이터 체크 (기상청 API 등)
|
||||
if (data && typeof data === 'object' && data.text && typeof data.text === 'string') {
|
||||
const parsedData = parseTextData(data.text);
|
||||
if (parsedData.length > 0) {
|
||||
data = parsedData;
|
||||
}
|
||||
} else if (dataSource.json_path) {
|
||||
const pathParts = dataSource.json_path.split(".");
|
||||
} else if (dataSource.jsonPath) {
|
||||
const pathParts = dataSource.jsonPath.split(".");
|
||||
for (const part of pathParts) {
|
||||
data = data?.[part];
|
||||
}
|
||||
}
|
||||
|
||||
const rows = Array.isArray(data) ? data : [data];
|
||||
|
||||
|
||||
console.log("📊 [최종 파싱된 데이터]", rows);
|
||||
|
||||
// 컬럼 목록 및 타입 추출
|
||||
if (rows.length > 0) {
|
||||
const columns = Object.keys(rows[0]);
|
||||
setAvailableColumns(columns);
|
||||
|
||||
|
||||
// 컬럼 타입 분석 (첫 번째 행 기준)
|
||||
const types: Record<string, string> = {};
|
||||
columns.forEach(col => {
|
||||
@@ -344,11 +344,11 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
}
|
||||
});
|
||||
setColumnTypes(types);
|
||||
|
||||
|
||||
// 샘플 데이터 저장 (최대 3개)
|
||||
setSampleData(rows.slice(0, 3));
|
||||
}
|
||||
|
||||
|
||||
// 위도/경도 또는 coordinates 필드 또는 지역 코드 체크
|
||||
const hasLocationData = rows.some((row) => {
|
||||
const hasLatLng = (row.lat || row.latitude) && (row.lng || row.longitude);
|
||||
@@ -358,25 +358,25 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
});
|
||||
|
||||
if (hasLocationData) {
|
||||
const markerCount = rows.filter(r =>
|
||||
((r.lat || r.latitude) && (r.lng || r.longitude)) ||
|
||||
const markerCount = rows.filter(r =>
|
||||
((r.lat || r.latitude) && (r.lng || r.longitude)) ||
|
||||
r.code || r.areaCode || r.regionCode
|
||||
).length;
|
||||
const polygonCount = rows.filter(r => r.coordinates && Array.isArray(r.coordinates)).length;
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `API 연결 성공 - 마커 ${markerCount}개, 영역 ${polygonCount}개 발견`
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `API 연결 성공 - 마커 ${markerCount}개, 영역 ${polygonCount}개 발견`
|
||||
});
|
||||
|
||||
|
||||
// 부모에게 테스트 결과 전달 (지도 미리보기용)
|
||||
if (onTestResult) {
|
||||
onTestResult(rows);
|
||||
}
|
||||
} else {
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `API 연결 성공 - ${rows.length}개 데이터 (위치 정보 없음)`
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: `API 연결 성공 - ${rows.length}개 데이터 (위치 정보 없음)`
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -494,13 +494,13 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
{/* JSON Path */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`jsonPath-\${dataSource.id}`} className="text-xs">
|
||||
<Label htmlFor={`jsonPath-${dataSource.id}`} className="text-xs">
|
||||
JSON Path (선택)
|
||||
</Label>
|
||||
<Input
|
||||
id={`jsonPath-\${dataSource.id}`}
|
||||
value={dataSource.json_path || ""}
|
||||
onChange={(e) => onChange({ json_path: e.target.value })}
|
||||
id={`jsonPath-${dataSource.id}`}
|
||||
value={dataSource.jsonPath || ""}
|
||||
onChange={(e) => onChange({ jsonPath: e.target.value })}
|
||||
placeholder="예: data.results"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
@@ -523,7 +523,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
추가
|
||||
</Button>
|
||||
</div>
|
||||
{(dataSource.query_params || []).map((param) => (
|
||||
{(dataSource.queryParams || []).map((param) => (
|
||||
<div key={param.id} className="flex gap-2">
|
||||
<Input
|
||||
value={param.key}
|
||||
@@ -596,8 +596,8 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
마커 종류
|
||||
</Label>
|
||||
<Select
|
||||
value={dataSource.marker_type || "circle"}
|
||||
onValueChange={(value) => onChange({ marker_type: value })}
|
||||
value={dataSource.markerType || "circle"}
|
||||
onValueChange={(value) => onChange({ markerType: value })}
|
||||
>
|
||||
<SelectTrigger id="marker-type" className="h-9 text-xs">
|
||||
<SelectValue placeholder="마커 선택" />
|
||||
@@ -616,7 +616,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<h5 className="text-xs font-semibold">🎨 지도 색상 선택</h5>
|
||||
|
||||
|
||||
{/* 색상 팔레트 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">색상</Label>
|
||||
@@ -631,19 +631,19 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
{ name: "청록", marker: "#06b6d4", polygon: "#06b6d4" },
|
||||
{ name: "분홍", marker: "#ec4899", polygon: "#ec4899" },
|
||||
].map((color) => {
|
||||
const isSelected = dataSource.marker_color === color.marker;
|
||||
const isSelected = dataSource.markerColor === color.marker;
|
||||
return (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => onChange({
|
||||
marker_color: color.marker,
|
||||
polygon_color: color.polygon,
|
||||
polygon_opacity: 0.5,
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})}
|
||||
className={`flex h-16 flex-col items-center justify-center gap-1 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
: "border-border bg-background hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
@@ -706,8 +706,8 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">메트릭 컬럼 선택</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{dataSource.selected_columns && dataSource.selected_columns.length > 0
|
||||
? `${dataSource.selected_columns.length}개 컬럼 선택됨`
|
||||
{dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? `${dataSource.selectedColumns.length}개 컬럼 선택됨`
|
||||
: "모든 컬럼 표시"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -715,7 +715,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selected_columns: availableColumns })}
|
||||
onClick={() => onChange({ selectedColumns: availableColumns })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
전체
|
||||
@@ -723,7 +723,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selected_columns: [] })}
|
||||
onClick={() => onChange({ selectedColumns: [] })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
해제
|
||||
@@ -744,16 +744,16 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
{/* 컬럼 카드 그리드 */}
|
||||
<div className="grid grid-cols-1 gap-2 max-h-80 overflow-y-auto">
|
||||
{availableColumns
|
||||
.filter(col =>
|
||||
!columnSearchTerm ||
|
||||
.filter(col =>
|
||||
!columnSearchTerm ||
|
||||
col.toLowerCase().includes(columnSearchTerm.toLowerCase())
|
||||
)
|
||||
.map((col) => {
|
||||
const isSelected =
|
||||
!dataSource.selected_columns ||
|
||||
dataSource.selected_columns.length === 0 ||
|
||||
dataSource.selected_columns.includes(col);
|
||||
|
||||
const isSelected =
|
||||
!dataSource.selectedColumns ||
|
||||
dataSource.selectedColumns.length === 0 ||
|
||||
dataSource.selectedColumns.includes(col);
|
||||
|
||||
const type = columnTypes[col] || "unknown";
|
||||
const typeIcon = {
|
||||
number: "🔢",
|
||||
@@ -763,7 +763,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
object: "📦",
|
||||
unknown: "❓"
|
||||
}[type];
|
||||
|
||||
|
||||
const typeColor = {
|
||||
number: "text-primary bg-primary/10",
|
||||
string: "text-muted-foreground bg-muted",
|
||||
@@ -777,20 +777,20 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<div
|
||||
key={col}
|
||||
onClick={() => {
|
||||
const currentSelected = dataSource.selected_columns && dataSource.selected_columns.length > 0
|
||||
? dataSource.selected_columns
|
||||
const currentSelected = dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? dataSource.selectedColumns
|
||||
: availableColumns;
|
||||
|
||||
|
||||
const newSelected = isSelected
|
||||
? currentSelected.filter(c => c !== col)
|
||||
: [...currentSelected, col];
|
||||
|
||||
onChange({ selected_columns: newSelected });
|
||||
|
||||
onChange({ selectedColumns: newSelected });
|
||||
}}
|
||||
className={`
|
||||
relative flex items-start gap-3 rounded-lg border p-3 cursor-pointer transition-all
|
||||
${isSelected
|
||||
? "border-primary bg-primary/5 shadow-sm"
|
||||
${isSelected
|
||||
? "border-primary bg-primary/5 shadow-sm"
|
||||
: "border-border bg-card hover:border-primary/50 hover:bg-muted/50"
|
||||
}
|
||||
`}
|
||||
@@ -799,8 +799,8 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className={`
|
||||
h-4 w-4 rounded border-2 flex items-center justify-center transition-colors
|
||||
${isSelected
|
||||
? "border-primary bg-primary"
|
||||
${isSelected
|
||||
? "border-primary bg-primary"
|
||||
: "border-border bg-background"
|
||||
}
|
||||
`}>
|
||||
@@ -820,7 +820,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
{typeIcon} {type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 샘플 데이터 */}
|
||||
{sampleData.length > 0 && (
|
||||
<div className="mt-1.5 text-xs text-muted-foreground">
|
||||
@@ -841,7 +841,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</div>
|
||||
|
||||
{/* 검색 결과 없음 */}
|
||||
{columnSearchTerm && availableColumns.filter(col =>
|
||||
{columnSearchTerm && availableColumns.filter(col =>
|
||||
col.toLowerCase().includes(columnSearchTerm.toLowerCase())
|
||||
).length === 0 && (
|
||||
<div className="text-center py-8 text-sm text-muted-foreground">
|
||||
@@ -863,8 +863,8 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</div>
|
||||
<Switch
|
||||
id="save-to-history"
|
||||
checked={dataSource.save_to_history || false}
|
||||
onCheckedChange={(checked) => onChange({ save_to_history: checked })}
|
||||
checked={dataSource.saveToHistory || false}
|
||||
onCheckedChange={(checked) => onChange({ saveToHistory: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -878,11 +878,11 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
다른 데이터 소스와 통합할 때 컬럼명을 통일할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
{dataSource.column_mapping && Object.keys(dataSource.column_mapping).length > 0 && (
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onChange({ column_mapping: {} })}
|
||||
onClick={() => onChange({ columnMapping: {} })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
초기화
|
||||
@@ -891,9 +891,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</div>
|
||||
|
||||
{/* 매핑 목록 */}
|
||||
{dataSource.column_mapping && Object.keys(dataSource.column_mapping).length > 0 && (
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(dataSource.column_mapping).map(([original, mapped]) => (
|
||||
{Object.entries(dataSource.columnMapping).map(([original, mapped]) => (
|
||||
<div key={original} className="flex items-center gap-2">
|
||||
{/* 원본 컬럼 (읽기 전용) */}
|
||||
<Input
|
||||
@@ -909,9 +909,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Input
|
||||
value={mapped}
|
||||
onChange={(e) => {
|
||||
const newMapping = { ...dataSource.column_mapping };
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
newMapping[original] = e.target.value;
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
placeholder="표시 이름"
|
||||
className="h-8 flex-1 text-xs"
|
||||
@@ -922,9 +922,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newMapping = { ...dataSource.column_mapping };
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
delete newMapping[original];
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
@@ -939,9 +939,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(col) => {
|
||||
const newMapping = { ...dataSource.column_mapping } || {};
|
||||
const newMapping = { ...dataSource.columnMapping } || {};
|
||||
newMapping[col] = col; // 기본값은 원본과 동일
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
@@ -949,7 +949,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns
|
||||
.filter(col => !dataSource.column_mapping || !dataSource.column_mapping[col])
|
||||
.filter(col => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map(col => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
@@ -973,9 +973,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</Label>
|
||||
|
||||
{/* 기존 팝업 필드 목록 */}
|
||||
{dataSource.popup_fields && dataSource.popup_fields.length > 0 && (
|
||||
{dataSource.popupFields && dataSource.popupFields.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{dataSource.popup_fields.map((field, index) => (
|
||||
{dataSource.popupFields.map((field, index) => (
|
||||
<div key={index} className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">필드 {index + 1}</span>
|
||||
@@ -983,9 +983,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields.splice(index, 1);
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
@@ -999,9 +999,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Select
|
||||
value={field.fieldName}
|
||||
onValueChange={(value) => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].fieldName = value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
@@ -1023,9 +1023,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Input
|
||||
value={field.label || ""}
|
||||
onChange={(e) => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].label = e.target.value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
placeholder="예: 차량 번호"
|
||||
className="h-8 w-full text-xs"
|
||||
@@ -1039,9 +1039,9 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
<Select
|
||||
value={field.format || "text"}
|
||||
onValueChange={(value: "text" | "date" | "datetime" | "number" | "url") => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].format = value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
@@ -1066,13 +1066,13 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields.push({
|
||||
fieldName: availableColumns[0] || "",
|
||||
label: "",
|
||||
format: "text",
|
||||
});
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
className="h-8 w-full gap-2 text-xs"
|
||||
disabled={availableColumns.length === 0}
|
||||
|
||||
@@ -34,10 +34,10 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
||||
|
||||
// 외부 DB 커넥션 목록 로드
|
||||
useEffect(() => {
|
||||
if (dataSource.connection_type === "external") {
|
||||
if (dataSource.connectionType === "external") {
|
||||
loadExternalConnections();
|
||||
}
|
||||
}, [dataSource.connection_type]);
|
||||
}, [dataSource.connectionType]);
|
||||
|
||||
const loadExternalConnections = async () => {
|
||||
setLoadingConnections(true);
|
||||
@@ -76,16 +76,16 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
||||
// dashboardApi 사용 (인증 토큰 자동 포함)
|
||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||
|
||||
if (dataSource.connection_type === "external" && dataSource.external_connection_id) {
|
||||
if (dataSource.connectionType === "external" && dataSource.externalConnectionId) {
|
||||
// 외부 DB
|
||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||
const result = await ExternalDbConnectionAPI.executeQuery(
|
||||
parseInt(dataSource.external_connection_id),
|
||||
parseInt(dataSource.externalConnectionId),
|
||||
dataSource.query,
|
||||
);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const rows = Array.isArray(result.data.rows) ? result.data.rows : [];
|
||||
const rows = Array.isArray(result.data) ? result.data : [];
|
||||
const rowCount = rows.length;
|
||||
|
||||
// 컬럼 목록 및 타입 추출
|
||||
@@ -195,8 +195,8 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">데이터베이스 연결</Label>
|
||||
<RadioGroup
|
||||
value={dataSource.connection_type || "current"}
|
||||
onValueChange={(value: "current" | "external") => onChange({ connection_type: value })}
|
||||
value={dataSource.connectionType || "current"}
|
||||
onValueChange={(value: "current" | "external") => onChange({ connectionType: value })}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="current" id={`current-${dataSource.id}`} />
|
||||
@@ -214,7 +214,7 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
||||
</div>
|
||||
|
||||
{/* 외부 DB 선택 */}
|
||||
{dataSource.connection_type === "external" && (
|
||||
{dataSource.connectionType === "external" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`external-conn-${dataSource.id}`} className="text-xs">
|
||||
외부 데이터베이스 선택 *
|
||||
@@ -225,8 +225,8 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
value={dataSource.external_connection_id || ""}
|
||||
onValueChange={(value) => onChange({ external_connection_id: value })}
|
||||
value={dataSource.externalConnectionId || ""}
|
||||
onValueChange={(value) => onChange({ externalConnectionId: value })}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="외부 DB 선택" />
|
||||
@@ -327,8 +327,8 @@ ORDER BY 하위부서수 DESC`,
|
||||
마커 종류
|
||||
</Label>
|
||||
<Select
|
||||
value={dataSource.marker_type || "circle"}
|
||||
onValueChange={(value) => onChange({ marker_type: value })}
|
||||
value={dataSource.markerType || "circle"}
|
||||
onValueChange={(value) => onChange({ markerType: value })}
|
||||
>
|
||||
<SelectTrigger id="marker-type" className="h-8 text-xs">
|
||||
<SelectValue placeholder="마커 선택" />
|
||||
@@ -364,16 +364,16 @@ ORDER BY 하위부서수 DESC`,
|
||||
{ name: "청록", marker: "#06b6d4", polygon: "#06b6d4" },
|
||||
{ name: "분홍", marker: "#ec4899", polygon: "#ec4899" },
|
||||
].map((color) => {
|
||||
const isSelected = dataSource.marker_color === color.marker;
|
||||
const isSelected = dataSource.markerColor === color.marker;
|
||||
return (
|
||||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({
|
||||
marker_color: color.marker,
|
||||
polygon_color: color.polygon,
|
||||
polygon_opacity: 0.5,
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})
|
||||
}
|
||||
className={`flex h-12 flex-col items-center justify-center gap-0.5 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
@@ -434,8 +434,8 @@ ORDER BY 하위부서수 DESC`,
|
||||
<div>
|
||||
<Label className="text-xs font-semibold">메트릭 컬럼 선택</Label>
|
||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
{dataSource.selected_columns && dataSource.selected_columns.length > 0
|
||||
? `${dataSource.selected_columns.length}개 선택됨`
|
||||
{dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? `${dataSource.selectedColumns.length}개 선택됨`
|
||||
: "모든 컬럼 표시"}
|
||||
</p>
|
||||
</div>
|
||||
@@ -443,7 +443,7 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selected_columns: availableColumns })}
|
||||
onClick={() => onChange({ selectedColumns: availableColumns })}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
전체
|
||||
@@ -451,7 +451,7 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selected_columns: [] })}
|
||||
onClick={() => onChange({ selectedColumns: [] })}
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
해제
|
||||
@@ -475,9 +475,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
.filter((col) => !columnSearchTerm || col.toLowerCase().includes(columnSearchTerm.toLowerCase()))
|
||||
.map((col) => {
|
||||
const isSelected =
|
||||
!dataSource.selected_columns ||
|
||||
dataSource.selected_columns.length === 0 ||
|
||||
dataSource.selected_columns.includes(col);
|
||||
!dataSource.selectedColumns ||
|
||||
dataSource.selectedColumns.length === 0 ||
|
||||
dataSource.selectedColumns.includes(col);
|
||||
|
||||
const type = columnTypes[col] || "unknown";
|
||||
const typeIcon = {
|
||||
@@ -503,15 +503,15 @@ ORDER BY 하위부서수 DESC`,
|
||||
key={col}
|
||||
onClick={() => {
|
||||
const currentSelected =
|
||||
dataSource.selected_columns && dataSource.selected_columns.length > 0
|
||||
? dataSource.selected_columns
|
||||
dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? dataSource.selectedColumns
|
||||
: availableColumns;
|
||||
|
||||
const newSelected = isSelected
|
||||
? currentSelected.filter((c) => c !== col)
|
||||
: [...currentSelected, col];
|
||||
|
||||
onChange({ selected_columns: newSelected });
|
||||
onChange({ selectedColumns: newSelected });
|
||||
}}
|
||||
className={`relative flex cursor-pointer items-start gap-2 rounded-lg border p-2 transition-all ${
|
||||
isSelected
|
||||
@@ -588,17 +588,17 @@ ORDER BY 하위부서수 DESC`,
|
||||
다른 데이터 소스와 통합할 때 컬럼명을 통일할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
{dataSource.column_mapping && Object.keys(dataSource.column_mapping).length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => onChange({ column_mapping: {} })} className="h-7 text-xs">
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => onChange({ columnMapping: {} })} className="h-7 text-xs">
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 매핑 목록 */}
|
||||
{dataSource.column_mapping && Object.keys(dataSource.column_mapping).length > 0 && (
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(dataSource.column_mapping).map(([original, mapped]) => (
|
||||
{Object.entries(dataSource.columnMapping).map(([original, mapped]) => (
|
||||
<div key={original} className="flex items-center gap-2">
|
||||
{/* 원본 컬럼 (읽기 전용) */}
|
||||
<Input value={original} disabled className="bg-muted h-8 flex-1 text-xs" />
|
||||
@@ -610,9 +610,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Input
|
||||
value={mapped}
|
||||
onChange={(e) => {
|
||||
const newMapping = { ...dataSource.column_mapping };
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
newMapping[original] = e.target.value;
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
placeholder="표시 이름"
|
||||
className="h-8 flex-1 text-xs"
|
||||
@@ -623,9 +623,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newMapping = { ...dataSource.column_mapping };
|
||||
const newMapping = { ...dataSource.columnMapping };
|
||||
delete newMapping[original];
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
@@ -640,9 +640,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(col) => {
|
||||
const newMapping = { ...dataSource.column_mapping } || {};
|
||||
const newMapping = { ...(dataSource.columnMapping || {}) };
|
||||
newMapping[col] = col; // 기본값은 원본과 동일
|
||||
onChange({ column_mapping: newMapping });
|
||||
onChange({ columnMapping: newMapping });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
@@ -650,7 +650,7 @@ ORDER BY 하위부서수 DESC`,
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns
|
||||
.filter((col) => !dataSource.column_mapping || !dataSource.column_mapping[col])
|
||||
.filter((col) => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map((col) => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
@@ -671,9 +671,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
</Label>
|
||||
|
||||
{/* 기존 팝업 필드 목록 */}
|
||||
{dataSource.popup_fields && dataSource.popup_fields.length > 0 && (
|
||||
{dataSource.popupFields && dataSource.popupFields.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{dataSource.popup_fields.map((field, index) => (
|
||||
{dataSource.popupFields.map((field, index) => (
|
||||
<div key={index} className="bg-muted/30 space-y-2 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">필드 {index + 1}</span>
|
||||
@@ -681,9 +681,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields.splice(index, 1);
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
@@ -697,9 +697,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Select
|
||||
value={field.fieldName}
|
||||
onValueChange={(value) => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].fieldName = value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
@@ -721,9 +721,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Input
|
||||
value={field.label || ""}
|
||||
onChange={(e) => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].label = e.target.value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
placeholder="예: 차량 번호"
|
||||
className="h-8 w-full text-xs"
|
||||
@@ -737,9 +737,9 @@ ORDER BY 하위부서수 DESC`,
|
||||
<Select
|
||||
value={field.format || "text"}
|
||||
onValueChange={(value: "text" | "date" | "datetime" | "number" | "url") => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields[index].format = value;
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
@@ -774,13 +774,13 @@ ORDER BY 하위부서수 DESC`,
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newFields = [...(dataSource.popup_fields || [])];
|
||||
const newFields = [...(dataSource.popupFields || [])];
|
||||
newFields.push({
|
||||
fieldName: availableColumns[0] || "",
|
||||
label: "",
|
||||
format: "text",
|
||||
});
|
||||
onChange({ popup_fields: newFields });
|
||||
onChange({ popupFields: newFields });
|
||||
}}
|
||||
className="h-8 w-full gap-2 text-xs"
|
||||
disabled={availableColumns.length === 0}
|
||||
|
||||
@@ -775,12 +775,12 @@ export default function CopyScreenModal({
|
||||
if (!screenData && screenId && screenName) {
|
||||
// allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성
|
||||
screenData = {
|
||||
screenId: screenId,
|
||||
screenName: screenName,
|
||||
screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||
tableName: tableName || '',
|
||||
screen_id: screenId,
|
||||
screen_name: screenName,
|
||||
screen_code: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||
table_name: tableName || '',
|
||||
description: '',
|
||||
companyCode: sourceGroupData.company_code || '',
|
||||
company_code: sourceGroupData.company_code || '',
|
||||
} as any;
|
||||
}
|
||||
return { screenId, displayOrder, screenRole, screenData };
|
||||
@@ -958,12 +958,12 @@ export default function CopyScreenModal({
|
||||
// allScreens에 없는 경우 (다른 회사 화면) - 그룹의 screens 정보로 최소한의 데이터 구성
|
||||
console.log(` ⚠️ allScreens에서 못 찾음, 그룹 정보 사용: ${screenId} - ${screenName}`);
|
||||
screenData = {
|
||||
screenId: screenId,
|
||||
screenName: screenName,
|
||||
screenCode: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||
tableName: tableName || '',
|
||||
screen_id: screenId,
|
||||
screen_name: screenName,
|
||||
screen_code: `SCREEN_${screenId}`, // 임시 코드 (실제 복사 시 새 코드 생성)
|
||||
table_name: tableName || '',
|
||||
description: '',
|
||||
companyCode: sourceGroup.company_code || '',
|
||||
company_code: sourceGroup.company_code || '',
|
||||
} as any;
|
||||
} else if (screenData) {
|
||||
console.log(` ✅ allScreens에서 찾음: ${screenId} - ${screenData.screen_name}`);
|
||||
|
||||
@@ -236,8 +236,8 @@ export const FileAttachmentDetailModal: React.FC<FileAttachmentDetailModalProps>
|
||||
// 서버 파일인 경우
|
||||
await downloadFile({
|
||||
fileId: file.objid,
|
||||
serverFilename: file.savedFileName,
|
||||
originalName: file.realFileName,
|
||||
server_filename: file.savedFileName,
|
||||
original_name: file.realFileName,
|
||||
});
|
||||
|
||||
toast.dismiss();
|
||||
|
||||
@@ -448,7 +448,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
if (newCodes.length > 0) {
|
||||
try {
|
||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
|
||||
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
||||
const response = await apiClient.post("/table-categories/labels-by-codes", { value_codes: newCodes });
|
||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
|
||||
if (response.data.success && response.data.data) {
|
||||
setCategoryCodeLabels((prev) => {
|
||||
@@ -2195,8 +2195,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
await downloadFile({
|
||||
fileId: fileInfo.objid || fileInfo.id,
|
||||
serverFilename: serverFilename,
|
||||
originalName: fileInfo.name,
|
||||
server_filename: serverFilename,
|
||||
original_name: fileInfo.name,
|
||||
});
|
||||
|
||||
toast.success(`${fileInfo.name} 다운로드가 완료되었습니다.`);
|
||||
|
||||
@@ -27,14 +27,11 @@ import {
|
||||
NumberTypeConfig,
|
||||
DateTypeConfig,
|
||||
SelectTypeConfig,
|
||||
RadioTypeConfig,
|
||||
CheckboxTypeConfig,
|
||||
TextareaTypeConfig,
|
||||
FileTypeConfig,
|
||||
CodeTypeConfig,
|
||||
EntityTypeConfig,
|
||||
ButtonTypeConfig,
|
||||
} from "@/types";
|
||||
import { ExtendedButtonTypeConfig } from "@/types/control-management";
|
||||
import { InteractiveDataTable } from "./InteractiveDataTable";
|
||||
import { FileUpload } from "./widgets/FileUpload";
|
||||
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||
@@ -130,8 +127,8 @@ const CascadingDropdownWrapper: React.FC<CascadingDropdownWrapperProps> = ({
|
||||
{options.length === 0 ? (
|
||||
<div className="text-muted-foreground px-2 py-4 text-center text-sm">
|
||||
{!parentValue
|
||||
? config.emptyParentMessage || "상위 항목을 먼저 선택하세요"
|
||||
: config.noOptionsMessage || "선택 가능한 항목이 없습니다"}
|
||||
? config?.emptyParentMessage || "상위 항목을 먼저 선택하세요"
|
||||
: config?.noOptionsMessage || "선택 가능한 항목이 없습니다"}
|
||||
</div>
|
||||
) : (
|
||||
options.map((option) => (
|
||||
@@ -336,14 +333,14 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
finalFormData,
|
||||
allComponents.filter(c => c.type === 'widget') as WidgetComponent[],
|
||||
tableColumns,
|
||||
{
|
||||
id: screenInfo.id,
|
||||
{
|
||||
id: screenInfo.id,
|
||||
screenName: screenInfo.tableName || "unknown",
|
||||
tableName: screenInfo.tableName,
|
||||
tableName: screenInfo.tableName || "",
|
||||
screenResolution: { width: 800, height: 600 },
|
||||
gridSettings: { size: 20, color: "#e0e0e0", opacity: 0.5 },
|
||||
description: "동적 화면"
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
enableRealTimeValidation: true,
|
||||
validationDelay: 300,
|
||||
@@ -381,7 +378,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const { generateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||
const response = await generateNumberingCode(ruleId);
|
||||
if (response.success && response.data) {
|
||||
return response.data.generated_code;
|
||||
return response.data.generatedCode;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("채번 규칙 코드 생성 실패:", error);
|
||||
@@ -417,7 +414,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
});
|
||||
|
||||
setPopupLayout(layout.components || []);
|
||||
setPopupScreenResolution(layout.screen_resolution || null);
|
||||
setPopupScreenResolution(layout.screenResolution || null);
|
||||
setPopupScreenInfo({
|
||||
id: popupScreen.screenId,
|
||||
tableName: screen.table_name
|
||||
@@ -489,7 +486,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
|
||||
|
||||
// 사용자 정보에서 필터 값 가져오기
|
||||
const userValue = user?.[userField];
|
||||
const userValue = (user as any)?.[userField];
|
||||
|
||||
if (userValue && sourceTable && filterColumn && displayColumn) {
|
||||
try {
|
||||
@@ -576,7 +573,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
if (comp.type === "datatable") {
|
||||
return (
|
||||
<InteractiveDataTable
|
||||
component={comp as DataTableComponent}
|
||||
component={comp as any}
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -666,7 +663,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
<RackStructureComponent
|
||||
config={rackConfig}
|
||||
formData={formData}
|
||||
tableName={tableName}
|
||||
tableName={screenInfo?.tableName}
|
||||
onChange={(locations: any[]) => {
|
||||
// 컴포넌트의 columnName을 키로 사용
|
||||
const fieldKey = (comp as any).columnName || "_rackStructureLocations";
|
||||
@@ -678,8 +675,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
const { widgetType, label: originalLabel, placeholder, required, readonly, columnName } = comp;
|
||||
const fieldName = columnName || comp.id;
|
||||
const { widgetType, label: originalLabel, placeholder, required, readonly, columnName } = comp as any;
|
||||
const fieldName = (columnName as string | undefined) || comp.id;
|
||||
const currentValue = formData[fieldName] || "";
|
||||
|
||||
// 🆕 엔티티 조인 컬럼은 읽기 전용으로 처리
|
||||
@@ -688,6 +685,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 다국어 라벨 적용 (langKey가 있으면 번역 텍스트 사용)
|
||||
const compLangKey = (comp as any).langKey;
|
||||
const translations: Record<string, string> = (typeof window !== "undefined" && (window as any).__TRANSLATIONS) || {};
|
||||
const label = compLangKey && translations[compLangKey] ? translations[compLangKey] : originalLabel;
|
||||
|
||||
const applyStyles = (element: React.ReactElement) => {
|
||||
@@ -697,9 +695,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
// size.width, size.height가 부모 컨테이너에서 적용되므로
|
||||
const { width, height, ...styleWithoutSize } = comp.style;
|
||||
|
||||
return React.cloneElement(element, {
|
||||
return React.cloneElement(element as React.ReactElement<any>, {
|
||||
style: {
|
||||
...element.props.style, // 기존 스타일 유지
|
||||
...(element.props as any).style, // 기존 스타일 유지
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 적용
|
||||
boxSizing: "border-box",
|
||||
},
|
||||
@@ -827,8 +825,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
step: config?.step,
|
||||
decimalPlaces: config?.decimalPlaces,
|
||||
thousandSeparator: config?.thousandSeparator,
|
||||
prefix: config?.prefix,
|
||||
suffix: config?.suffix,
|
||||
prefix: (config as any)?.prefix,
|
||||
suffix: (config as any)?.suffix,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -855,7 +853,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
case "textarea":
|
||||
case "text_area": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as TextareaTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as any;
|
||||
|
||||
console.log("📄 InteractiveScreenViewer - Textarea 위젯:", {
|
||||
componentId: widget.id,
|
||||
@@ -987,7 +985,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
case "checkbox":
|
||||
case "boolean": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CheckboxTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as any;
|
||||
|
||||
console.log("☑️ InteractiveScreenViewer - Checkbox 위젯:", {
|
||||
componentId: widget.id,
|
||||
@@ -1026,7 +1024,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
case "radio": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as RadioTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as any;
|
||||
|
||||
console.log("🔘 InteractiveScreenViewer - Radio 위젯:", {
|
||||
componentId: widget.id,
|
||||
@@ -1069,7 +1067,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{options.map((option, index) => (
|
||||
{(options as Array<{label: string; value: string; disabled?: boolean}>).map((option, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -1216,7 +1214,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
accept: config?.accept,
|
||||
multiple: config?.multiple,
|
||||
maxSize: config?.maxSize,
|
||||
preview: config?.preview,
|
||||
preview: config?.showPreview,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1306,7 +1304,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
};
|
||||
|
||||
const renderFilePreview = () => {
|
||||
if (!currentValue || !config?.preview) return null;
|
||||
if (!currentValue || !config?.showPreview) return null;
|
||||
|
||||
// 새로운 JSON 구조에서 파일 정보 추출
|
||||
const fileData = currentValue.files || [];
|
||||
@@ -1366,7 +1364,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
disabled={isReadonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
accept={config?.accept?.join(",")}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
@@ -1402,7 +1400,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
<>
|
||||
<Upload className="mx-auto h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{config?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'}
|
||||
{(config as any)?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'}
|
||||
</p>
|
||||
{(config?.accept || config?.maxSize) && (
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
@@ -1425,7 +1423,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
case "code": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as any;
|
||||
|
||||
console.log(`🔍 [InteractiveScreenViewer] Code 위젯 렌더링:`, {
|
||||
componentId: widget.id,
|
||||
@@ -1487,7 +1485,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
case "entity": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as EntityTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as any;
|
||||
|
||||
console.log("🏢 InteractiveScreenViewer - Entity 위젯:", {
|
||||
componentId: widget.id,
|
||||
@@ -1542,16 +1540,16 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
case "button": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||
const config = widget.webTypeConfig as ExtendedButtonTypeConfig | undefined;
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
// 프리뷰 모드에서는 버튼 동작 차단
|
||||
if (isPreviewMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actionType = config?.actionType || "save";
|
||||
|
||||
|
||||
const actionType: string = config?.actionType || "save";
|
||||
|
||||
try {
|
||||
switch (actionType) {
|
||||
case "save":
|
||||
@@ -1592,7 +1590,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error(`버튼 액션 실행 오류 (${actionType}):`, error);
|
||||
alert(`작업 중 오류가 발생했습니다: ${error.message}`);
|
||||
alert(`작업 중 오류가 발생했습니다: ${(error as any).message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1629,17 +1627,17 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 필수 항목 검증 (테이블 타입관리 NOT NULL 기반 + 기존 required 속성 폴백)
|
||||
const requiredFields = allComponents.filter(c => {
|
||||
const colName = c.columnName || c.id;
|
||||
const colName = (c as any).columnName || c.id;
|
||||
return (c.required || isColumnRequired(colName)) && colName;
|
||||
});
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const fieldName = field.columnName || field.id;
|
||||
const fieldName = (field as any).columnName || field.id;
|
||||
const value = currentFormData[fieldName];
|
||||
return !value || value.toString().trim() === "";
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
const fieldNames = missingFields.map(f => f.label || f.columnName || f.id).join(", ");
|
||||
const fieldNames = missingFields.map(f => f.label || (f as any).columnName || f.id).join(", ");
|
||||
alert(`다음 필수 항목을 입력해주세요: ${fieldNames}`);
|
||||
return;
|
||||
}
|
||||
@@ -1741,8 +1739,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}
|
||||
|
||||
// 테이블명 결정 (화면 정보에서 가져오거나 첫 번째 컴포넌트의 테이블명 사용)
|
||||
const tableName = screenInfo.tableName ||
|
||||
allComponents.find(c => c.columnName)?.tableName ||
|
||||
const tableName = screenInfo.tableName ||
|
||||
(allComponents.find(c => (c as any).columnName) as any)?.tableName ||
|
||||
"dynamic_form_data"; // 기본값
|
||||
|
||||
// 🆕 자동으로 작성자 정보 추가 (user.userId가 확실히 있음)
|
||||
@@ -1808,11 +1806,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 저장 후 데이터 초기화 (선택사항)
|
||||
if (onFormDataChange) {
|
||||
const resetData: Record<string, any> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
resetData[key] = "";
|
||||
onFormDataChange!(key, "");
|
||||
});
|
||||
onFormDataChange(resetData);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "저장에 실패했습니다.");
|
||||
@@ -1841,8 +1837,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}
|
||||
|
||||
// 테이블명 결정
|
||||
const tableName = screenInfo?.tableName ||
|
||||
allComponents.find(c => c.columnName)?.tableName ||
|
||||
const tableName = screenInfo?.tableName ||
|
||||
(allComponents.find(c => (c as any).columnName) as any)?.tableName ||
|
||||
"unknown_table";
|
||||
|
||||
if (!tableName || tableName === "unknown_table") {
|
||||
@@ -1861,11 +1857,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
|
||||
// 삭제 후 폼 초기화
|
||||
if (onFormDataChange) {
|
||||
const resetData: Record<string, any> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
resetData[key] = "";
|
||||
onFormDataChange!(key, "");
|
||||
});
|
||||
onFormDataChange(resetData);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "삭제에 실패했습니다.");
|
||||
@@ -1881,8 +1875,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
console.log("✏️ 수정 액션 실행");
|
||||
|
||||
// 버튼 컴포넌트의 수정 모달 설정 가져오기
|
||||
const editModalTitle = config?.editModalTitle || "";
|
||||
const editModalDescription = config?.editModalDescription || "";
|
||||
const editModalTitle = (config as any)?.editModalTitle || "";
|
||||
const editModalDescription = (config as any)?.editModalDescription || "";
|
||||
|
||||
console.log("📝 버튼 수정 모달 설정:", { editModalTitle, editModalDescription });
|
||||
|
||||
@@ -1926,11 +1920,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const handleResetAction = () => {
|
||||
if (confirm("모든 입력을 초기화하시겠습니까?")) {
|
||||
if (onFormDataChange) {
|
||||
const resetData: Record<string, any> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
resetData[key] = "";
|
||||
onFormDataChange!(key, "");
|
||||
});
|
||||
onFormDataChange(resetData);
|
||||
}
|
||||
// console.log("🔄 폼 초기화 완료");
|
||||
alert("입력이 초기화되었습니다.");
|
||||
@@ -2046,7 +2038,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}
|
||||
// console.log("⚡ 커스텀 액션 실행 완료");
|
||||
} catch (error) {
|
||||
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
|
||||
throw new Error(`커스텀 액션 실행 실패: ${(error as any).message}`);
|
||||
}
|
||||
} else {
|
||||
// console.log("⚡ 커스텀 액션이 설정되지 않았습니다.");
|
||||
@@ -2165,8 +2157,8 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<FileUpload
|
||||
component={fileComponent}
|
||||
onUpdateComponent={handleFileUpdate}
|
||||
component={fileComponent as any}
|
||||
onUpdateComponent={handleFileUpdate as any}
|
||||
userInfo={user} // 사용자 정보를 프롭으로 전달
|
||||
/>
|
||||
</div>
|
||||
@@ -2212,7 +2204,6 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const shouldShowLabel =
|
||||
!hideLabel &&
|
||||
(component.style?.labelDisplay ?? true) !== false &&
|
||||
component.style?.labelDisplay !== "false" &&
|
||||
(component.label || component.style?.labelText) &&
|
||||
!templateTypes.includes(component.type);
|
||||
|
||||
@@ -2237,9 +2228,9 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
fontSize: component.style?.labelFontSize || "14px",
|
||||
color: component.style?.labelColor || "hsl(var(--foreground))",
|
||||
fontWeight: component.style?.labelFontWeight || "500",
|
||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||
padding: component.style?.labelPadding || "0",
|
||||
borderRadius: component.style?.labelBorderRadius || "0",
|
||||
backgroundColor: (component.style as any)?.labelBackgroundColor || "transparent",
|
||||
padding: (component.style as any)?.labelPadding || "0",
|
||||
borderRadius: (component.style as any)?.labelBorderRadius || "0",
|
||||
...(isHorizontalLabel
|
||||
? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" }
|
||||
: { marginBottom: component.style?.labelMarginBottom || "4px" }),
|
||||
@@ -2504,7 +2495,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
style={labelStyle}
|
||||
>
|
||||
{labelText}
|
||||
{(component.required || component.componentConfig?.required || isColumnRequired(component.columnName || component.style?.columnName || "")) && <span className="ml-1 text-destructive">*</span>}
|
||||
{(component.required || component.componentConfig?.required || isColumnRequired((component as any).columnName || (component.style as any)?.columnName || "")) && <span className="ml-1 text-destructive">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
@@ -2523,7 +2514,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{(component.required || component.componentConfig?.required || isColumnRequired(component.columnName || component.style?.columnName || "")) && <span className="ml-1 text-destructive">*</span>}
|
||||
{(component.required || component.componentConfig?.required || isColumnRequired((component as any).columnName || (component.style as any)?.columnName || "")) && <span className="ml-1 text-destructive">*</span>}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DynamicComponentRenderer, isColumnRequiredByMeta } from "@/lib/registry
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils";
|
||||
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||
import { FlowVisibilityConfig } from "@/types/screen-management";
|
||||
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
|
||||
@@ -283,7 +283,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const { sourceTable, filterColumn, userField, displayColumn } = autoFillConfig;
|
||||
|
||||
// 사용자 정보에서 필터 값 가져오기
|
||||
const userValue = user?.[userField];
|
||||
const userValue = (user as any)?.[userField];
|
||||
|
||||
if (userValue && sourceTable && filterColumn && displayColumn) {
|
||||
try {
|
||||
@@ -314,18 +314,17 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const loadPopupScreen = async (screenId: number) => {
|
||||
try {
|
||||
setPopupLoading(true);
|
||||
const response = await screenApi.getScreenLayout(screenId);
|
||||
const layoutData = await screenApi.getLayout(screenId) as any;
|
||||
|
||||
if (response.success && response.data) {
|
||||
const screenData = response.data;
|
||||
setPopupLayout(screenData.components || []);
|
||||
if (layoutData) {
|
||||
setPopupLayout(layoutData.components || []);
|
||||
setPopupScreenResolution({
|
||||
width: screenData.screen_resolution?.width || 1200,
|
||||
height: screenData.screen_resolution?.height || 800,
|
||||
width: layoutData.screen_resolution?.width || 1200,
|
||||
height: layoutData.screen_resolution?.height || 800,
|
||||
});
|
||||
setPopupScreenInfo({
|
||||
id: screenData.id,
|
||||
tableName: screenData.table_name,
|
||||
id: layoutData.id || screenId,
|
||||
tableName: layoutData.table_name,
|
||||
});
|
||||
} else {
|
||||
toast.error("팝업 화면을 불러올 수 없습니다.");
|
||||
@@ -353,7 +352,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
// 동적 대화형 위젯 렌더링
|
||||
const renderInteractiveWidget = (comp: ComponentData) => {
|
||||
// 조건부 표시 평가 (기존 conditional 시스템)
|
||||
const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents);
|
||||
const conditionalResult = evaluateConditional(comp.conditional as any, formData, allComponents);
|
||||
|
||||
// 조건에 따라 숨김 처리
|
||||
if (!conditionalResult.visible) {
|
||||
@@ -393,7 +392,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
if (isDataTableComponent(comp)) {
|
||||
return (
|
||||
<InteractiveDataTable
|
||||
component={comp as DataTableComponent}
|
||||
component={comp as any as DataTableComponent}
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -476,7 +475,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
return React.cloneElement(element, {
|
||||
style: {
|
||||
...element.props.style,
|
||||
...(element.props as any).style,
|
||||
...styleWithoutSize, // width/height 제외한 스타일만 적용
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
@@ -556,7 +555,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
// 버튼 렌더링
|
||||
const renderButton = (comp: ComponentData) => {
|
||||
const config = (comp as any).webTypeConfig as ButtonTypeConfig | undefined;
|
||||
const config = (comp as any).webTypeConfig as any;
|
||||
const { label } = comp;
|
||||
|
||||
// 버튼 액션 핸들러들
|
||||
@@ -726,7 +725,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
}
|
||||
// console.log("⚡ 커스텀 액션 실행 완료");
|
||||
} catch (error) {
|
||||
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
|
||||
throw new Error(`커스텀 액션 실행 실패: ${(error as any).message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -744,7 +743,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
// 1. 대상 테이블의 컬럼 목록 조회 (자동 매핑용)
|
||||
let targetTableColumns: string[] = [];
|
||||
try {
|
||||
const { default: apiClient } = await import("@/lib/api/client");
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
const columnsResponse = await apiClient.get(
|
||||
`/table-management/tables/${quickInsertConfig.targetTable}/columns`,
|
||||
);
|
||||
@@ -865,7 +864,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
// 5. 중복 체크 (설정된 경우)
|
||||
if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
|
||||
try {
|
||||
const { default: apiClient } = await import("@/lib/api/client");
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
|
||||
// 중복 체크를 위한 검색 조건 구성
|
||||
const searchConditions: Record<string, any> = {};
|
||||
@@ -880,7 +879,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
// 기존 데이터 조회
|
||||
const checkResponse = await apiClient.post(`/table-management/tables/${quickInsertConfig.targetTable}/data`, {
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
size: 1,
|
||||
search: searchConditions,
|
||||
});
|
||||
|
||||
@@ -900,7 +899,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
// 6. API 호출
|
||||
try {
|
||||
const { default: apiClient } = await import("@/lib/api/client");
|
||||
const { apiClient } = await import("@/lib/api/client");
|
||||
|
||||
const response = await apiClient.post(
|
||||
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
|
||||
@@ -969,7 +968,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("버튼 액션 오류:", error);
|
||||
toast.error(error.message || "액션 실행 중 오류가 발생했습니다.");
|
||||
toast.error((error as any)?.message || "액션 실행 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1008,7 +1007,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
// 화면 ID 추출 (URL에서)
|
||||
const screenId =
|
||||
screenInfo?.screenId ||
|
||||
screenInfo?.id ||
|
||||
(typeof window !== "undefined" && window.location.pathname.includes("/screens/")
|
||||
? parseInt(window.location.pathname.split("/screens/")[1])
|
||||
: null);
|
||||
@@ -1021,7 +1020,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
componentConfig={{
|
||||
...comp.fileConfig,
|
||||
multiple: comp.fileConfig?.multiple !== false,
|
||||
accept: comp.fileConfig?.accept || "*/*",
|
||||
accept: (comp.fileConfig?.accept as string) || "*/*",
|
||||
maxSize: (comp.fileConfig?.maxSize || 10) * 1024 * 1024, // MB to bytes
|
||||
disabled: readonly,
|
||||
}}
|
||||
@@ -1124,11 +1123,10 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
// 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
||||
const compType = (component as any).componentType || "";
|
||||
const isV2InputComponent =
|
||||
type === "v2-input" || type === "v2-select" || type === "v2-date" ||
|
||||
const isV2InputComponent =
|
||||
compType === "v2-input" || compType === "v2-select" || compType === "v2-date";
|
||||
const hasVisibleLabel = isV2InputComponent &&
|
||||
style?.labelDisplay !== false && style?.labelDisplay !== "false" &&
|
||||
const hasVisibleLabel = isV2InputComponent &&
|
||||
style?.labelDisplay !== false &&
|
||||
(style?.labelText || (component as any).label);
|
||||
|
||||
// 라벨 위치에 따라 오프셋 계산 (좌/우 배치 시 세로 오프셋 불필요)
|
||||
@@ -1435,7 +1433,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
onFormDataChange={(fieldName, value) => {
|
||||
setPopupFormData((prev) => ({ ...prev, [fieldName]: value }));
|
||||
}}
|
||||
screenInfo={popupScreenInfo}
|
||||
screenInfo={popupScreenInfo ?? undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -10,13 +10,12 @@ import {
|
||||
ComponentData,
|
||||
LayoutData,
|
||||
GroupState,
|
||||
TableInfo,
|
||||
Position,
|
||||
ColumnInfo,
|
||||
GridSettings,
|
||||
ScreenResolution,
|
||||
SCREEN_RESOLUTIONS,
|
||||
} from "@/types/screen";
|
||||
import type { TableInfo, ColumnInfo } from "@/types/screen-legacy-backup";
|
||||
import { SCREEN_RESOLUTIONS } from "@/types/screen-management";
|
||||
import { generateComponentId } from "@/lib/utils/generateId";
|
||||
import {
|
||||
getComponentIdFromWebType,
|
||||
@@ -108,7 +107,7 @@ import DetailSettingsPanel from "./panels/DetailSettingsPanel";
|
||||
import ResolutionPanel from "./panels/ResolutionPanel";
|
||||
import { usePanelState, PanelConfig } from "@/hooks/usePanelState";
|
||||
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||
import { FlowVisibilityConfig } from "@/types/screen-management";
|
||||
import {
|
||||
areAllButtons,
|
||||
generateGroupId,
|
||||
@@ -172,6 +171,7 @@ export default function ScreenDesigner({
|
||||
// POP 모드 여부에 따른 API 분기
|
||||
const USE_POP_API = isPop;
|
||||
const [layout, setLayout] = useState<LayoutData>({
|
||||
screenId: 0,
|
||||
components: [],
|
||||
gridSettings: {
|
||||
columns: 12,
|
||||
@@ -181,6 +181,10 @@ export default function ScreenDesigner({
|
||||
showGrid: false, // 기본값 false로 변경
|
||||
gridColor: "#d1d5db",
|
||||
gridOpacity: 0.5,
|
||||
enabled: false,
|
||||
size: 16,
|
||||
color: "#d1d5db",
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -567,7 +571,7 @@ export default function ScreenDesigner({
|
||||
// 레이어의 condition_config에서 zone_id를 가져와서 zones에서 찾기
|
||||
const findZone = async () => {
|
||||
try {
|
||||
const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id, activeLayerId);
|
||||
const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id!, activeLayerId);
|
||||
const zoneId = layerData?.conditionConfig?.zone_id;
|
||||
if (zoneId) {
|
||||
const zone = zones.find(z => z.zone_id === zoneId);
|
||||
@@ -587,14 +591,14 @@ export default function ScreenDesigner({
|
||||
if (!selectedScreen?.screen_id) return;
|
||||
const loadOtherLayerComponents = async () => {
|
||||
try {
|
||||
const allLayers = await screenApi.getScreenLayers(selectedScreen.screen_id);
|
||||
const allLayers = await screenApi.getScreenLayers(selectedScreen.screen_id!);
|
||||
const currentLayerId = activeLayerIdRef.current || 1;
|
||||
const otherLayers = allLayers.filter((l: any) => l.layer_id !== currentLayerId && l.layer_id > 0);
|
||||
|
||||
const components: ComponentData[] = [];
|
||||
for (const layerInfo of otherLayers) {
|
||||
try {
|
||||
const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id, layerInfo.layer_id);
|
||||
const layerData = await screenApi.getLayerLayout(selectedScreen.screen_id!, layerInfo.layer_id);
|
||||
const rawComps = layerData?.components;
|
||||
if (rawComps && Array.isArray(rawComps)) {
|
||||
for (const comp of rawComps) {
|
||||
@@ -1549,7 +1553,7 @@ export default function ScreenDesigner({
|
||||
const loadLayout = async () => {
|
||||
try {
|
||||
// 🆕 화면에 할당된 메뉴 조회
|
||||
const menuInfo = await screenApi.getScreenMenu(selectedScreen.screen_id);
|
||||
const menuInfo = await screenApi.getScreenMenu(selectedScreen.screen_id!);
|
||||
if (menuInfo) {
|
||||
setMenuObjid(menuInfo.menuObjid);
|
||||
console.log("🔗 화면에 할당된 메뉴:", menuInfo);
|
||||
@@ -1561,7 +1565,7 @@ export default function ScreenDesigner({
|
||||
let response: any;
|
||||
if (USE_POP_API) {
|
||||
// POP 모드: screen_layouts_pop 테이블 사용
|
||||
const popResponse = await screenApi.getLayoutPop(selectedScreen.screen_id);
|
||||
const popResponse = await screenApi.getLayoutPop(selectedScreen.screen_id!);
|
||||
response = popResponse ? convertV2ToLegacy(popResponse) : null;
|
||||
console.log("📱 POP 레이아웃 로드:", popResponse?.components?.length || 0, "개 컴포넌트");
|
||||
} else if (USE_V2_API) {
|
||||
|
||||
@@ -306,22 +306,22 @@ export function TableSettingModal({
|
||||
// 초기 편집 상태 설정
|
||||
const initialEdits: Record<string, Partial<ColumnTypeInfo>> = {};
|
||||
columnsData.forEach((col) => {
|
||||
// referenceTable이 설정되어 있으면 inputType은 entity여야 함
|
||||
let effectiveInputType = col.inputType || "direct";
|
||||
if (col.referenceTable && effectiveInputType !== "entity") {
|
||||
// reference_table이 설정되어 있으면 input_type은 entity여야 함
|
||||
let effectiveInputType = col.input_type || "direct";
|
||||
if (col.reference_table && effectiveInputType !== "entity") {
|
||||
effectiveInputType = "entity";
|
||||
}
|
||||
// codeCategory/codeValue가 설정되어 있으면 inputType은 code여야 함
|
||||
if (col.codeCategory && effectiveInputType !== "code") {
|
||||
// code_category/code_value가 설정되어 있으면 input_type은 code여야 함
|
||||
if (col.code_category && effectiveInputType !== "code") {
|
||||
effectiveInputType = "code";
|
||||
}
|
||||
|
||||
initialEdits[col.columnName] = {
|
||||
displayName: col.displayName,
|
||||
inputType: effectiveInputType,
|
||||
referenceTable: col.referenceTable,
|
||||
referenceColumn: col.referenceColumn,
|
||||
displayColumn: col.displayColumn,
|
||||
|
||||
initialEdits[col.column_name] = {
|
||||
display_name: col.display_name,
|
||||
input_type: effectiveInputType,
|
||||
reference_table: col.reference_table,
|
||||
reference_column: col.reference_column,
|
||||
display_column: col.display_column,
|
||||
};
|
||||
});
|
||||
setEditedColumns(initialEdits);
|
||||
@@ -408,8 +408,8 @@ export function TableSettingModal({
|
||||
// 참조 테이블 변경 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
Object.values(editedColumns).forEach((col) => {
|
||||
if (col.referenceTable && col.referenceTable !== "none") {
|
||||
loadRefTableColumns(col.referenceTable);
|
||||
if (col.reference_table && col.reference_table !== "none") {
|
||||
loadRefTableColumns(col.reference_table);
|
||||
}
|
||||
});
|
||||
}, [editedColumns, loadRefTableColumns]);
|
||||
@@ -431,17 +431,17 @@ export function TableSettingModal({
|
||||
}));
|
||||
|
||||
// 입력 타입 변경 시 관련 필드 초기화
|
||||
if (field === "inputType") {
|
||||
if (field === "input_type") {
|
||||
// 엔티티가 아닌 다른 타입으로 변경하면 참조 설정 초기화
|
||||
if (value !== "entity") {
|
||||
setEditedColumns((prev) => ({
|
||||
...prev,
|
||||
[columnName]: {
|
||||
...prev[columnName],
|
||||
inputType: value,
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
input_type: value,
|
||||
reference_table: "",
|
||||
reference_column: "",
|
||||
display_column: "",
|
||||
},
|
||||
}));
|
||||
}
|
||||
@@ -451,22 +451,22 @@ export function TableSettingModal({
|
||||
...prev,
|
||||
[columnName]: {
|
||||
...prev[columnName],
|
||||
inputType: value,
|
||||
codeCategory: "",
|
||||
codeValue: "",
|
||||
input_type: value,
|
||||
code_category: "",
|
||||
code_value: "",
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 참조 테이블 변경 시 참조 컬럼 초기화
|
||||
if (field === "referenceTable") {
|
||||
if (field === "reference_table") {
|
||||
setEditedColumns((prev) => ({
|
||||
...prev,
|
||||
[columnName]: {
|
||||
...prev[columnName],
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
reference_column: "",
|
||||
display_column: "",
|
||||
},
|
||||
}));
|
||||
if (value && value !== "none") {
|
||||
@@ -482,7 +482,7 @@ export function TableSettingModal({
|
||||
// 변경된 컬럼들만 저장
|
||||
for (const [columnName, editedSettings] of Object.entries(editedColumns)) {
|
||||
// 기존 컬럼 정보 찾기
|
||||
const originalColumn = tableColumns.find((c) => c.columnName === columnName);
|
||||
const originalColumn = tableColumns.find((c) => c.column_name === columnName);
|
||||
if (!originalColumn) continue;
|
||||
|
||||
// 기존 값과 편집된 값 병합
|
||||
@@ -491,25 +491,25 @@ export function TableSettingModal({
|
||||
...editedSettings,
|
||||
};
|
||||
|
||||
// detailSettings 처리 (Entity 타입인 경우)
|
||||
let finalDetailSettings = mergedColumn.detailSettings || "";
|
||||
|
||||
// referenceTable이 설정되어 있으면 inputType을 entity로 자동 설정
|
||||
let currentInputType = (mergedColumn.inputType || "") as string;
|
||||
if (mergedColumn.referenceTable && currentInputType !== "entity") {
|
||||
// detail_settings 처리 (Entity 타입인 경우)
|
||||
let finalDetailSettings = mergedColumn.detail_settings || "";
|
||||
|
||||
// reference_table이 설정되어 있으면 input_type을 entity로 자동 설정
|
||||
let currentInputType = (mergedColumn.input_type || "") as string;
|
||||
if (mergedColumn.reference_table && currentInputType !== "entity") {
|
||||
currentInputType = "entity";
|
||||
}
|
||||
// codeCategory가 설정되어 있으면 inputType을 code로 자동 설정
|
||||
if (mergedColumn.codeCategory && currentInputType !== "code") {
|
||||
// code_category가 설정되어 있으면 input_type을 code로 자동 설정
|
||||
if (mergedColumn.code_category && currentInputType !== "code") {
|
||||
currentInputType = "code";
|
||||
}
|
||||
|
||||
if (currentInputType === "entity" && mergedColumn.referenceTable) {
|
||||
// 기존 detailSettings를 파싱하거나 새로 생성
|
||||
if (currentInputType === "entity" && mergedColumn.reference_table) {
|
||||
// 기존 detail_settings를 파싱하거나 새로 생성
|
||||
let existingSettings: Record<string, unknown> = {};
|
||||
if (typeof mergedColumn.detailSettings === "string" && mergedColumn.detailSettings.trim().startsWith("{")) {
|
||||
if (typeof mergedColumn.detail_settings === "string" && mergedColumn.detail_settings.trim().startsWith("{")) {
|
||||
try {
|
||||
existingSettings = JSON.parse(mergedColumn.detailSettings);
|
||||
existingSettings = JSON.parse(mergedColumn.detail_settings);
|
||||
} catch {
|
||||
existingSettings = {};
|
||||
}
|
||||
@@ -518,9 +518,9 @@ export function TableSettingModal({
|
||||
// 엔티티 설정 추가
|
||||
const entitySettings = {
|
||||
...existingSettings,
|
||||
entityTable: mergedColumn.referenceTable,
|
||||
entityCodeColumn: mergedColumn.referenceColumn || "id",
|
||||
entityLabelColumn: mergedColumn.displayColumn || "name",
|
||||
entityTable: mergedColumn.reference_table,
|
||||
entityCodeColumn: mergedColumn.reference_column || "id",
|
||||
entityLabelColumn: mergedColumn.display_column || "name",
|
||||
placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요",
|
||||
searchable: existingSettings.searchable ?? true,
|
||||
};
|
||||
@@ -552,14 +552,14 @@ export function TableSettingModal({
|
||||
// ColumnSettings 인터페이스에 맞게 데이터 구성
|
||||
const columnSetting: ColumnSettings = {
|
||||
columnName: columnName,
|
||||
columnLabel: mergedColumn.displayName || originalColumn.displayName || "",
|
||||
inputType: currentInputType || "text", // referenceTable/codeCategory가 설정된 경우 자동 보정된 값 사용
|
||||
columnLabel: mergedColumn.display_name || originalColumn.display_name || "",
|
||||
inputType: currentInputType || "text", // reference_table/code_category가 설정된 경우 자동 보정된 값 사용
|
||||
detailSettings: finalDetailSettings,
|
||||
codeCategory: mergedColumn.codeCategory || originalColumn.codeCategory || "",
|
||||
codeValue: mergedColumn.codeValue || originalColumn.codeValue || "",
|
||||
referenceTable: mergedColumn.referenceTable || "",
|
||||
referenceColumn: mergedColumn.referenceColumn || "",
|
||||
displayColumn: mergedColumn.displayColumn || "",
|
||||
codeCategory: mergedColumn.code_category || originalColumn.code_category || "",
|
||||
codeValue: mergedColumn.code_value || originalColumn.code_value || "",
|
||||
referenceTable: mergedColumn.reference_table || "",
|
||||
referenceColumn: mergedColumn.reference_column || "",
|
||||
displayColumn: mergedColumn.display_column || "",
|
||||
};
|
||||
|
||||
console.log("저장할 컬럼 설정:", columnSetting);
|
||||
@@ -590,11 +590,11 @@ export function TableSettingModal({
|
||||
const mergedColumns = useMemo(() => {
|
||||
const columnsMap = new Map<string, ColumnTypeInfo & { isPK?: boolean; isFK?: boolean }>();
|
||||
|
||||
// API에서 가져온 컬럼 정보 (camelCase)
|
||||
// API에서 가져온 컬럼 정보 (snake_case)
|
||||
tableColumns.forEach((tcol) => {
|
||||
columnsMap.set(tcol.columnName, {
|
||||
columnsMap.set(tcol.column_name, {
|
||||
...tcol,
|
||||
isPK: tcol.isPrimaryKey,
|
||||
isPK: tcol.is_primary_key,
|
||||
isFK: false, // 백엔드에서 isForeignKey를 제공하지 않으므로 false 기본값
|
||||
});
|
||||
});
|
||||
@@ -605,7 +605,7 @@ export function TableSettingModal({
|
||||
// 선택된 컬럼 정보
|
||||
const selectedColumnInfo = useMemo(() => {
|
||||
if (!selectedColumn) return null;
|
||||
return mergedColumns.find((c) => c.columnName === selectedColumn);
|
||||
return mergedColumns.find((c) => c.column_name === selectedColumn);
|
||||
}, [selectedColumn, mergedColumns]);
|
||||
|
||||
// 테이블 옵션
|
||||
@@ -613,9 +613,9 @@ export function TableSettingModal({
|
||||
() => [
|
||||
{ value: "none", label: "-- 선택 안함 --" },
|
||||
...tables.map((t) => ({
|
||||
value: t.tableName,
|
||||
label: t.displayName || t.tableName,
|
||||
description: t.tableName,
|
||||
value: t.table_name,
|
||||
label: t.display_name || t.table_name,
|
||||
description: t.table_name,
|
||||
})),
|
||||
],
|
||||
[tables]
|
||||
@@ -637,9 +637,9 @@ export function TableSettingModal({
|
||||
return [
|
||||
{ value: "", label: "-- 선택 안함 --" },
|
||||
...cols.map((c) => ({
|
||||
value: c.columnName,
|
||||
label: c.displayName || c.columnName,
|
||||
description: c.dataType,
|
||||
value: c.column_name,
|
||||
label: c.display_name || c.column_name,
|
||||
description: c.data_type,
|
||||
})),
|
||||
];
|
||||
};
|
||||
@@ -719,8 +719,8 @@ export function TableSettingModal({
|
||||
<ColumnListTab
|
||||
columns={tableColumns.map((col) => ({
|
||||
...col,
|
||||
isPK: col.columnName === "id" || col.columnName.endsWith("_id"),
|
||||
isFK: (col.inputType as string) === "entity",
|
||||
isPK: col.column_name === "id" || col.column_name.endsWith("_id"),
|
||||
isFK: (col.input_type as string) === "entity",
|
||||
}))}
|
||||
editedColumns={editedColumns}
|
||||
selectedColumn={selectedColumn}
|
||||
@@ -747,9 +747,9 @@ export function TableSettingModal({
|
||||
|
||||
{/* 우측: 상세 설정 (60%) */}
|
||||
<div className="flex w-[60%] min-h-0 flex-col rounded-lg border bg-muted/30 p-3">
|
||||
{selectedColumn && mergedColumns.find((c) => c.columnName === selectedColumn) ? (
|
||||
{selectedColumn && mergedColumns.find((c) => c.column_name === selectedColumn) ? (
|
||||
<ColumnDetailPanel
|
||||
columnInfo={mergedColumns.find((c) => c.columnName === selectedColumn)!}
|
||||
columnInfo={mergedColumns.find((c) => c.column_name === selectedColumn)!}
|
||||
editedColumn={editedColumns[selectedColumn] || {}}
|
||||
tableOptions={tableOptions}
|
||||
inputTypeOptions={inputTypeOptions}
|
||||
@@ -997,8 +997,8 @@ function ColumnListTab({
|
||||
const term = searchTerm.toLowerCase();
|
||||
return columns.filter(
|
||||
(col) =>
|
||||
col.columnName.toLowerCase().includes(term) ||
|
||||
(col.displayName || "").toLowerCase().includes(term)
|
||||
col.column_name.toLowerCase().includes(term) ||
|
||||
(col.display_name || "").toLowerCase().includes(term)
|
||||
);
|
||||
}, [columns, searchTerm]);
|
||||
|
||||
@@ -1045,15 +1045,15 @@ function ColumnListTab({
|
||||
) : (
|
||||
<div className="space-y-1 px-3 pb-3">
|
||||
{filteredColumns.map((col) => {
|
||||
const edited = editedColumns[col.columnName] || {};
|
||||
// editedColumns에서 inputType을 가져옴 (초기화 시 이미 보정됨)
|
||||
const inputType = (edited.inputType || col.inputType || "text") as string;
|
||||
const isSelected = selectedColumn === col.columnName;
|
||||
const edited = editedColumns[col.column_name] || {};
|
||||
// editedColumns에서 input_type을 가져옴 (초기화 시 이미 보정됨)
|
||||
const inputType = (edited.input_type || col.input_type || "text") as string;
|
||||
const isSelected = selectedColumn === col.column_name;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col.columnName}
|
||||
onClick={() => onSelectColumn(col.columnName)}
|
||||
key={col.column_name}
|
||||
onClick={() => onSelectColumn(col.column_name)}
|
||||
className={cn(
|
||||
"cursor-pointer rounded-lg border p-3 transition-colors",
|
||||
isSelected
|
||||
@@ -1067,7 +1067,7 @@ function ColumnListTab({
|
||||
{getInputTypeIcon(inputType)}
|
||||
</span>
|
||||
<span className="text-sm font-medium">
|
||||
{edited.displayName || col.displayName || col.columnName}
|
||||
{edited.display_name || col.display_name || col.column_name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
@@ -1077,8 +1077,8 @@ function ColumnListTab({
|
||||
PK
|
||||
</Badge>
|
||||
)}
|
||||
{/* 엔티티 타입이거나 referenceTable이 설정되어 있으면 조인 배지 표시 (FK와 동일 의미) */}
|
||||
{(inputType === "entity" || edited.referenceTable || col.referenceTable) && (
|
||||
{/* 엔티티 타입이거나 reference_table이 설정되어 있으면 조인 배지 표시 (FK와 동일 의미) */}
|
||||
{(inputType === "entity" || edited.reference_table || col.reference_table) && (
|
||||
<Badge variant="outline" className="bg-primary/10 text-primary text-[10px] px-1.5">
|
||||
<Link2 className="mr-0.5 h-2.5 w-2.5" />
|
||||
조인
|
||||
@@ -1087,7 +1087,7 @@ function ColumnListTab({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
<span className="font-mono">{col.columnName}</span>
|
||||
<span className="font-mono">{col.column_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1122,12 +1122,12 @@ function ColumnDetailPanel({
|
||||
loadingRefColumns,
|
||||
onColumnChange,
|
||||
}: ColumnDetailPanelProps) {
|
||||
const currentLabel = editedColumn.displayName ?? columnInfo.displayName ?? "";
|
||||
const currentRefTable = editedColumn.referenceTable ?? columnInfo.referenceTable ?? "";
|
||||
const currentRefColumn = editedColumn.referenceColumn ?? columnInfo.referenceColumn ?? "";
|
||||
const currentDisplayColumn = editedColumn.displayColumn ?? columnInfo.displayColumn ?? "";
|
||||
// editedColumn에서 inputType을 가져옴 (초기화 시 이미 보정됨)
|
||||
const currentInputType = (editedColumn.inputType ?? columnInfo.inputType ?? "text") as string;
|
||||
const currentLabel = editedColumn.display_name ?? columnInfo.display_name ?? "";
|
||||
const currentRefTable = editedColumn.reference_table ?? columnInfo.reference_table ?? "";
|
||||
const currentRefColumn = editedColumn.reference_column ?? columnInfo.reference_column ?? "";
|
||||
const currentDisplayColumn = editedColumn.display_column ?? columnInfo.display_column ?? "";
|
||||
// editedColumn에서 input_type을 가져옴 (초기화 시 이미 보정됨)
|
||||
const currentInputType = (editedColumn.input_type ?? columnInfo.input_type ?? "text") as string;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
@@ -1139,9 +1139,9 @@ function ColumnDetailPanel({
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="font-mono text-sm bg-muted px-2 py-0.5 rounded">
|
||||
{columnInfo.columnName}
|
||||
{columnInfo.column_name}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">{columnInfo.dataType}</span>
|
||||
<span className="text-xs text-muted-foreground">{columnInfo.data_type}</span>
|
||||
{columnInfo.isPK && (
|
||||
<Badge variant="outline" className="bg-amber-100 text-orange-700 text-[10px]">
|
||||
Primary Key
|
||||
@@ -1166,8 +1166,8 @@ function ColumnDetailPanel({
|
||||
<Label className="text-xs">표시 라벨</Label>
|
||||
<Input
|
||||
value={currentLabel}
|
||||
onChange={(e) => onColumnChange("displayName", e.target.value)}
|
||||
placeholder={columnInfo.columnName}
|
||||
onChange={(e) => onColumnChange("display_name", e.target.value)}
|
||||
placeholder={columnInfo.column_name}
|
||||
className="h-9 text-sm"
|
||||
/>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
@@ -1179,7 +1179,7 @@ function ColumnDetailPanel({
|
||||
<Label className="text-xs">입력 타입</Label>
|
||||
<Select
|
||||
value={currentInputType}
|
||||
onValueChange={(v) => onColumnChange("inputType", v)}
|
||||
onValueChange={(v) => onColumnChange("input_type", v)}
|
||||
>
|
||||
<SelectTrigger className="h-9 text-sm">
|
||||
<SelectValue placeholder="입력 타입 선택" />
|
||||
@@ -1210,7 +1210,7 @@ function ColumnDetailPanel({
|
||||
<Label className="text-xs">참조 테이블</Label>
|
||||
<SearchableSelect
|
||||
value={currentRefTable || "none"}
|
||||
onValueChange={(v) => onColumnChange("referenceTable", v === "none" ? "" : v)}
|
||||
onValueChange={(v) => onColumnChange("reference_table", v === "none" ? "" : v)}
|
||||
options={tableOptions}
|
||||
placeholder="테이블 선택"
|
||||
/>
|
||||
@@ -1228,7 +1228,7 @@ function ColumnDetailPanel({
|
||||
) : (
|
||||
<SearchableSelect
|
||||
value={currentRefColumn}
|
||||
onValueChange={(v) => onColumnChange("referenceColumn", v)}
|
||||
onValueChange={(v) => onColumnChange("reference_column", v)}
|
||||
options={getRefColumnOptions(currentRefTable)}
|
||||
placeholder="컬럼 선택"
|
||||
/>
|
||||
@@ -1248,7 +1248,7 @@ function ColumnDetailPanel({
|
||||
) : (
|
||||
<SearchableSelect
|
||||
value={currentDisplayColumn}
|
||||
onValueChange={(v) => onColumnChange("displayColumn", v)}
|
||||
onValueChange={(v) => onColumnChange("display_column", v)}
|
||||
options={getRefColumnOptions(currentRefTable)}
|
||||
placeholder="표시 컬럼 선택"
|
||||
/>
|
||||
@@ -1269,22 +1269,22 @@ function ColumnDetailPanel({
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">데이터 타입</span>
|
||||
<p className="font-mono">{columnInfo.dataType}</p>
|
||||
<p className="font-mono">{columnInfo.data_type}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">NULL 허용</span>
|
||||
<p>{columnInfo.isNullable === "YES" ? "예" : "아니오"}</p>
|
||||
<p>{columnInfo.is_nullable === "YES" ? "예" : "아니오"}</p>
|
||||
</div>
|
||||
{columnInfo.maxLength && (
|
||||
{columnInfo.max_length && (
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">최대 길이</span>
|
||||
<p>{columnInfo.maxLength}</p>
|
||||
<p>{columnInfo.max_length}</p>
|
||||
</div>
|
||||
)}
|
||||
{columnInfo.defaultValue && (
|
||||
{columnInfo.default_value && (
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">기본값</span>
|
||||
<p className="font-mono text-xs">{columnInfo.defaultValue}</p>
|
||||
<p className="font-mono text-xs">{columnInfo.default_value}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,29 @@ import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
||||
import { WidgetComponent, EntityTypeConfig } from "@/types/screen";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
|
||||
type LocalEntityConfig = EntityTypeConfig & {
|
||||
entityType?: string;
|
||||
displayFields?: string[];
|
||||
searchFields?: string[];
|
||||
valueField?: string;
|
||||
labelField?: string;
|
||||
searchable?: boolean;
|
||||
emptyMessage?: string;
|
||||
pageSize?: number;
|
||||
minSearchLength?: number;
|
||||
defaultValue?: string;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
apiEndpoint?: string;
|
||||
};
|
||||
|
||||
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
component,
|
||||
onUpdateComponent,
|
||||
onUpdateProperty,
|
||||
}) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||
const config = (widget.webTypeConfig as unknown as LocalEntityConfig) || {};
|
||||
|
||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
||||
const [referenceInfo, setReferenceInfo] = useState<{
|
||||
@@ -35,7 +51,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
});
|
||||
|
||||
// 로컬 상태 (UI 관련 설정만)
|
||||
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
||||
const [localConfig, setLocalConfig] = useState<LocalEntityConfig>({
|
||||
referenceTable: config.referenceTable || "",
|
||||
referenceColumn: config.referenceColumn || "",
|
||||
displayColumns: config.displayColumns || [],
|
||||
entityType: config.entityType || "",
|
||||
displayFields: config.displayFields || [],
|
||||
searchFields: config.searchFields || [],
|
||||
@@ -58,7 +77,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
useEffect(() => {
|
||||
const loadReferenceInfo = async () => {
|
||||
// 컴포넌트의 테이블명과 컬럼명이 있는 경우에만 조회
|
||||
const tableName = widget.tableName;
|
||||
const tableName = (widget as any).tableName;
|
||||
const columnName = widget.columnName;
|
||||
|
||||
if (!tableName || !columnName) {
|
||||
@@ -142,12 +161,15 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
};
|
||||
|
||||
loadReferenceInfo();
|
||||
}, [widget.tableName, widget.columnName]);
|
||||
}, [(widget as any).tableName, widget.columnName]);
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||
const currentConfig = (widget.webTypeConfig as unknown as LocalEntityConfig) || {};
|
||||
setLocalConfig({
|
||||
referenceTable: currentConfig.referenceTable || "",
|
||||
referenceColumn: currentConfig.referenceColumn || "",
|
||||
displayColumns: currentConfig.displayColumns || [],
|
||||
entityType: currentConfig.entityType || "",
|
||||
displayFields: currentConfig.displayFields || [],
|
||||
searchFields: currentConfig.searchFields || [],
|
||||
@@ -168,14 +190,14 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
}, [widget.webTypeConfig, referenceInfo.referenceColumn, referenceInfo.displayColumn]);
|
||||
|
||||
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||
const updateConfig = (field: keyof LocalEntityConfig, value: any) => {
|
||||
const newConfig = { ...localConfig, [field]: value };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
};
|
||||
|
||||
// 입력 필드용 업데이트 (로컬 상태만)
|
||||
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
||||
const updateConfigLocal = (field: keyof LocalEntityConfig, value: any) => {
|
||||
setLocalConfig({ ...localConfig, [field]: value });
|
||||
};
|
||||
|
||||
|
||||
@@ -11,16 +11,26 @@ import { Upload, File, X } from "lucide-react";
|
||||
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
||||
import { WidgetComponent, FileTypeConfig } from "@/types/screen";
|
||||
|
||||
type LocalFileConfig = FileTypeConfig & {
|
||||
dragAndDrop?: boolean;
|
||||
required?: boolean;
|
||||
readonly?: boolean;
|
||||
browseText?: string;
|
||||
uploadText?: string;
|
||||
maxFileSize?: number;
|
||||
acceptedTypes: string[];
|
||||
};
|
||||
|
||||
export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
component,
|
||||
onUpdateComponent,
|
||||
onUpdateProperty,
|
||||
}) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = (widget.webTypeConfig as FileTypeConfig) || {};
|
||||
const config = (widget.webTypeConfig as unknown as LocalFileConfig) || {};
|
||||
|
||||
// 로컬 상태
|
||||
const [localConfig, setLocalConfig] = useState<FileTypeConfig>({
|
||||
const [localConfig, setLocalConfig] = useState<LocalFileConfig>({
|
||||
multiple: config.multiple || false,
|
||||
maxFileSize: config.maxFileSize || 10, // MB
|
||||
maxFiles: config.maxFiles || 1,
|
||||
@@ -39,7 +49,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
const currentConfig = (widget.webTypeConfig as FileTypeConfig) || {};
|
||||
const currentConfig = (widget.webTypeConfig as unknown as LocalFileConfig) || {};
|
||||
setLocalConfig({
|
||||
multiple: currentConfig.multiple || false,
|
||||
maxFileSize: currentConfig.maxFileSize || 10,
|
||||
@@ -56,7 +66,7 @@ export const FileConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
}, [widget.webTypeConfig]);
|
||||
|
||||
// 설정 업데이트 핸들러
|
||||
const updateConfig = (field: keyof FileTypeConfig, value: any) => {
|
||||
const updateConfig = (field: keyof LocalFileConfig, value: any) => {
|
||||
const newConfig = { ...localConfig, [field]: value };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
FileComponent,
|
||||
WebTypeConfig,
|
||||
TableInfo,
|
||||
LayoutComponent,
|
||||
} from "@/types/screen";
|
||||
import { LayoutComponent } from "@/types/layout";
|
||||
// 레거시 ButtonConfigPanel 제거됨
|
||||
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
||||
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
|
||||
@@ -25,6 +25,17 @@ import { BaseInputType, getBaseInputType, getDetailTypes, DetailTypeOption } fro
|
||||
import { ConditionalConfigPanel } from "@/components/v2/ConditionalConfigPanel";
|
||||
import { ConditionalConfig } from "@/types/v2-components";
|
||||
|
||||
// TableInfo에 columns 추가 (런타임 데이터 형식)
|
||||
type ColumnInfoWithLabel = {
|
||||
columnName: string;
|
||||
columnLabel?: string;
|
||||
dataType: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
type TableInfoWithColumns = TableInfo & {
|
||||
columns?: ColumnInfoWithLabel[];
|
||||
};
|
||||
|
||||
// 새로운 컴포넌트 설정 패널들 import
|
||||
import { ButtonConfigPanel as NewButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
|
||||
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
|
||||
@@ -44,7 +55,7 @@ import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
|
||||
interface DetailSettingsPanelProps {
|
||||
selectedComponent?: ComponentData;
|
||||
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
||||
currentTable?: TableInfo; // 현재 화면의 테이블 정보
|
||||
currentTable?: TableInfoWithColumns; // 현재 화면의 테이블 정보
|
||||
currentTableName?: string; // 현재 화면의 테이블명
|
||||
tables?: TableInfo[]; // 전체 테이블 목록
|
||||
currentScreenCompanyCode?: string; // 현재 편집 중인 화면의 회사 코드
|
||||
@@ -68,7 +79,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
componentType: selectedComponent?.type,
|
||||
currentTableName,
|
||||
currentTable: currentTable?.tableName,
|
||||
selectedComponentTableName: selectedComponent?.tableName,
|
||||
selectedComponentTableName: (selectedComponent as any)?.tableName,
|
||||
});
|
||||
// console.log(`🔍 DetailSettingsPanel webTypes 로드됨:`, webTypes?.length, "개");
|
||||
// console.log(`🔍 webTypes:`, webTypes);
|
||||
@@ -355,7 +366,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
)}
|
||||
|
||||
{/* 카드 레이아웃 설정 */}
|
||||
{layoutComponent.layoutType === "card-layout" && (
|
||||
{(layoutComponent.layoutType as string) === "card-layout" && (
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-foreground">카드 설정</h4>
|
||||
|
||||
@@ -365,7 +376,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
<h5 className="text-xs font-medium text-foreground">테이블 컬럼 매핑</h5>
|
||||
{currentTable && (
|
||||
<span className="bg-accent text-primary rounded px-2 py-1 text-xs">
|
||||
테이블: {currentTable.table_name}
|
||||
테이블: {currentTable.tableName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -678,7 +689,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
)}
|
||||
|
||||
{/* 존 목록 - 카드 레이아웃은 데이터 기반이므로 존 관리 불필요 */}
|
||||
{layoutComponent.layoutType !== "card-layout" && (
|
||||
{(layoutComponent.layoutType as string) !== "card-layout" && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium text-foreground">존 목록</h4>
|
||||
<div className="space-y-2">
|
||||
@@ -850,11 +861,11 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
||||
|
||||
const handleUpdateProperty = (path: string, value: any) => {
|
||||
onUpdateProperty(selectedComponent.id, path, value);
|
||||
onUpdateProperty(selectedComponent!.id, path, value);
|
||||
};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig.config", newConfig);
|
||||
onUpdateProperty(selectedComponent!.id, "componentConfig.config", newConfig);
|
||||
};
|
||||
|
||||
// 🆕 ComponentRegistry에서 ConfigPanel 가져오기
|
||||
@@ -874,11 +885,11 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
|
||||
// 래퍼 컴포넌트: 새 ConfigPanel 인터페이스를 기존 패턴에 맞춤
|
||||
// Section Card, Section Paper 등 신규 컴포넌트는 componentConfig 바로 아래에 설정 저장
|
||||
const config = currentConfig || definition.defaultProps?.componentConfig || {};
|
||||
|
||||
const config = currentConfig || (definition as any).defaultProps?.componentConfig || {};
|
||||
|
||||
const handleConfigChange = (newConfig: any) => {
|
||||
// componentConfig 전체를 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", newConfig);
|
||||
onUpdateProperty(selectedComponent!.id, "componentConfig", newConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1008,14 +1019,14 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
}
|
||||
|
||||
// 레이아웃 컴포넌트 처리
|
||||
if (selectedComponent.type === "layout") {
|
||||
return renderLayoutConfig(selectedComponent as LayoutComponent);
|
||||
if ((selectedComponent as any).type === "layout") {
|
||||
return renderLayoutConfig(selectedComponent as unknown as LayoutComponent);
|
||||
}
|
||||
|
||||
if (
|
||||
selectedComponent.type !== "widget" &&
|
||||
selectedComponent.type !== "file" &&
|
||||
selectedComponent.type !== "button" &&
|
||||
(selectedComponent as any).type !== "button" &&
|
||||
selectedComponent.type !== "component"
|
||||
) {
|
||||
return (
|
||||
@@ -1070,26 +1081,27 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
}
|
||||
|
||||
// 레거시 버튼을 새로운 컴포넌트 시스템으로 강제 변환
|
||||
if (selectedComponent.type === "button") {
|
||||
if ((selectedComponent as any).type === "button") {
|
||||
// console.log("🔄 레거시 버튼을 새로운 컴포넌트 시스템으로 변환:", selectedComponent);
|
||||
|
||||
const anyComp = selectedComponent as any;
|
||||
// 레거시 버튼을 새로운 시스템으로 변환
|
||||
const convertedComponent = {
|
||||
...selectedComponent,
|
||||
...anyComp,
|
||||
type: "component" as const,
|
||||
componentConfig: {
|
||||
type: "button-primary",
|
||||
webType: "button",
|
||||
...selectedComponent.componentConfig,
|
||||
...anyComp.componentConfig,
|
||||
},
|
||||
};
|
||||
|
||||
// 변환된 컴포넌트로 DB 업데이트
|
||||
onUpdateProperty(selectedComponent.id, "type", "component");
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig", convertedComponent.componentConfig);
|
||||
onUpdateProperty(anyComp.id, "type", "component");
|
||||
onUpdateProperty(anyComp.id, "componentConfig", convertedComponent.componentConfig);
|
||||
|
||||
// 변환된 컴포넌트로 처리 계속
|
||||
selectedComponent = convertedComponent;
|
||||
selectedComponent = convertedComponent as unknown as ComponentData;
|
||||
}
|
||||
|
||||
// 새로운 컴포넌트 시스템 처리 (type: "component")
|
||||
@@ -1118,7 +1130,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
// 세부 타입 변경 핸들러
|
||||
const handleDetailTypeChange = (newDetailType: string) => {
|
||||
setLocalComponentDetailType(newDetailType);
|
||||
onUpdateProperty(selectedComponent.id, "componentConfig.webType", newDetailType);
|
||||
onUpdateProperty(selectedComponent!.id, "componentConfig.webType", newDetailType);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1148,12 +1160,12 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedComponent.columnName && (
|
||||
{(selectedComponent as any).columnName && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
컬럼:
|
||||
</span>
|
||||
<span className="text-xs text-foreground">{selectedComponent.columnName}</span>
|
||||
<span className="text-xs text-foreground">{(selectedComponent as any).columnName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1171,7 +1183,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
// console.log("🔍 selectedComponent 전체:", selectedComponent);
|
||||
return config;
|
||||
})()}
|
||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||
screenTableName={(selectedComponent as any).tableName || currentTable?.tableName || currentTableName}
|
||||
tableColumns={(() => {
|
||||
// console.log("🔍 DetailSettingsPanel tableColumns 전달:", {
|
||||
// currentTable,
|
||||
@@ -1354,7 +1366,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
const widget = selectedComponent as WidgetComponent;
|
||||
|
||||
// 현재 웹타입의 기본 입력 타입 추출
|
||||
const currentBaseInputType = getBaseInputType(widget.widgetType);
|
||||
const currentBaseInputType = getBaseInputType(widget.widgetType as any);
|
||||
|
||||
// 선택 가능한 세부 타입 목록
|
||||
const availableDetailTypes = getDetailTypes(currentBaseInputType);
|
||||
|
||||
@@ -250,7 +250,7 @@ export const dataApi = {
|
||||
|
||||
const requestBody = {
|
||||
table_name: tableName,
|
||||
parentKeys,
|
||||
parent_keys: parentKeys,
|
||||
records,
|
||||
delete_orphans: options?.deleteOrphans ?? true, // 기본값: true (기존 동작 유지)
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ export const checkRelationshipNameDuplicate = async (relationshipName: string, e
|
||||
|
||||
const response = await apiClient.get("/dataflow-diagrams", {
|
||||
params: {
|
||||
searchTerm: relationshipName,
|
||||
search_term: relationshipName,
|
||||
page: 1,
|
||||
size: 100, // 충분히 큰 수로 설정
|
||||
},
|
||||
|
||||
@@ -146,8 +146,8 @@ export class DynamicFormApi {
|
||||
|
||||
const response = await apiClient.patch(`/dynamic-form/${id}/partial`, {
|
||||
table_name: tableName,
|
||||
originalData,
|
||||
newData,
|
||||
original_data: originalData,
|
||||
new_data: newData,
|
||||
});
|
||||
|
||||
console.log("✅ 폼 데이터 부분 업데이트 성공:", response.data);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ComponentRendererProps } from "../../types";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { AccordionBasicConfig, AccordionItem, DataSourceConfig, ContentFieldConfig } from "./types";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { getAdaptiveLabelColor } from "@/lib/utils/darkModeColor";
|
||||
@@ -159,7 +159,7 @@ const CustomAccordion: React.FC<CustomAccordionProps> = ({
|
||||
typeof item.content === "string"
|
||||
? item.content
|
||||
: Array.isArray(item.content)
|
||||
? item.content.join("\n") // 배열인 경우 줄바꿈으로 연결
|
||||
? (item.content as any).join("\n") // 배열인 경우 줄바꿈으로 연결
|
||||
: typeof item.content === "object"
|
||||
? Object.entries(item.content)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
@@ -402,7 +402,7 @@ const useAccordionData = (
|
||||
console.warn("⚠️ 테이블 API 호출 실패, 실제 화면에서도 더미 데이터로 대체:", apiError);
|
||||
console.log("📊 테이블 API 오류 상세:", {
|
||||
targetTableName,
|
||||
error: apiError.message,
|
||||
error: (apiError as Error).message,
|
||||
dataSource,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
@@ -464,7 +464,7 @@ const useAccordionData = (
|
||||
console.warn("⚠️ 엔드포인트 API 호출 실패, 실제 화면에서도 더미 데이터로 대체:", apiError);
|
||||
console.log("📊 엔드포인트 API 오류 상세:", {
|
||||
apiEndpoint: dataSource.apiEndpoint,
|
||||
error: apiError.message,
|
||||
error: (apiError as Error).message,
|
||||
dataSource,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
@@ -572,7 +572,7 @@ export const AccordionBasicComponent: React.FC<AccordionBasicComponentProps> = (
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (isDesignMode) {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
page: 1,
|
||||
size: 1,
|
||||
search: { [statusKeyField]: userId },
|
||||
autoFilter: true,
|
||||
auto_filter: true,
|
||||
});
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
+37
-37
@@ -25,6 +25,7 @@ import {
|
||||
RepeatScreenModalProps,
|
||||
CardData,
|
||||
CardColumnConfig,
|
||||
CardRowConfig,
|
||||
GroupedCardData,
|
||||
CardRowData,
|
||||
AggregationConfig,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
FooterButtonConfig,
|
||||
TableDataSourceConfig,
|
||||
TableCrudConfig,
|
||||
JoinCondition,
|
||||
} from "./types";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -184,7 +186,7 @@ export function RepeatScreenModalComponent({
|
||||
const cardId = keyParts.slice(0, -1).join("-"); // contentRowId를 제외한 나머지가 cardId
|
||||
|
||||
// contentRow 찾기
|
||||
const contentRow = contentRows.find((r) => key.includes(r.id));
|
||||
const contentRow = contentRows.find((r: CardContentRowConfig) => key.includes(r.id));
|
||||
if (!contentRow?.tableDataSource?.enabled) continue;
|
||||
|
||||
// 🆕 v3.13: 해당 카드의 대표 데이터 찾기 (joinConditions의 targetKey 값을 가져오기 위해)
|
||||
@@ -206,11 +208,11 @@ export function RepeatScreenModalComponent({
|
||||
if (dirtyRows.length === 0) continue;
|
||||
|
||||
// 저장할 필드만 추출
|
||||
const editableFields = (contentRow.tableColumns || []).filter((col) => col.editable).map((col) => col.field);
|
||||
const editableFields = (contentRow.tableColumns || []).filter((col: TableColumnConfig) => col.editable).map((col: TableColumnConfig) => col.field);
|
||||
|
||||
// 🆕 v3.13: joinConditions에서 sourceKey (저장 대상 테이블의 FK 컬럼) 추출
|
||||
const joinConditions = contentRow.tableDataSource.joinConditions || [];
|
||||
const joinKeys = joinConditions.map((cond) => cond.sourceKey);
|
||||
const joinKeys = joinConditions.map((cond: JoinCondition) => cond.sourceKey);
|
||||
|
||||
const allowedFields = [...new Set([...editableFields, ...joinKeys])];
|
||||
|
||||
@@ -453,7 +455,7 @@ export function RepeatScreenModalComponent({
|
||||
const loadExternalTableData = async () => {
|
||||
// contentRows에서 외부 테이블 데이터 소스가 있는 table 타입 행 찾기
|
||||
const tableRowsWithExternalSource = contentRows.filter(
|
||||
(row) => row.type === "table" && row.tableDataSource?.enabled,
|
||||
(row: CardContentRowConfig) => row.type === "table" && row.tableDataSource?.enabled,
|
||||
);
|
||||
|
||||
if (tableRowsWithExternalSource.length === 0) return;
|
||||
@@ -687,7 +689,7 @@ export function RepeatScreenModalComponent({
|
||||
if (groupedCardsData.length === 0) return;
|
||||
|
||||
// 외부 테이블 집계 또는 formula가 있는지 확인
|
||||
const hasExternalAggregation = grouping.aggregations.some((agg) => {
|
||||
const hasExternalAggregation = grouping.aggregations.some((agg: AggregationConfig) => {
|
||||
const sourceType = agg.sourceType || "column";
|
||||
if (sourceType === "formula") return true; // formula는 외부 테이블 참조 가능
|
||||
if (sourceType === "column") {
|
||||
@@ -701,7 +703,7 @@ export function RepeatScreenModalComponent({
|
||||
|
||||
// contentRows에서 외부 테이블 데이터 소스가 있는 모든 table 타입 행 찾기
|
||||
const tableRowsWithExternalSource = contentRows.filter(
|
||||
(row) => row.type === "table" && row.tableDataSource?.enabled,
|
||||
(row: CardContentRowConfig) => row.type === "table" && row.tableDataSource?.enabled,
|
||||
);
|
||||
|
||||
if (tableRowsWithExternalSource.length === 0) return;
|
||||
@@ -723,7 +725,7 @@ export function RepeatScreenModalComponent({
|
||||
// 집계 재계산
|
||||
const newAggregations: Record<string, number> = {};
|
||||
|
||||
grouping.aggregations!.forEach((agg) => {
|
||||
grouping.aggregations!.forEach((agg: AggregationConfig) => {
|
||||
const sourceType = agg.sourceType || "column";
|
||||
|
||||
if (sourceType === "column") {
|
||||
@@ -864,11 +866,11 @@ export function RepeatScreenModalComponent({
|
||||
const response = await allocateNumberingCode(rowNumbering.numberingRuleId, userInputCode, newRowData);
|
||||
|
||||
if (response.success && response.data) {
|
||||
newRowData[rowNumbering.targetColumn] = response.data.generated_code;
|
||||
newRowData[rowNumbering.targetColumn] = response.data.generatedCode;
|
||||
|
||||
console.log("[RepeatScreenModal] 자동 채번 완료:", {
|
||||
column: rowNumbering.targetColumn,
|
||||
generated_code: response.data.generated_code,
|
||||
generated_code: response.data.generatedCode,
|
||||
});
|
||||
} else {
|
||||
console.warn("[RepeatScreenModal] 채번 실패:", response);
|
||||
@@ -940,9 +942,8 @@ export function RepeatScreenModalComponent({
|
||||
// tableColumns에서 editable: true인 필드만 추가 (읽기 전용 컬럼 제외)
|
||||
if (contentRow.tableColumns) {
|
||||
contentRow.tableColumns.forEach((col) => {
|
||||
// editable이 명시적으로 true이거나, editable이 undefined가 아니고 false가 아닌 경우
|
||||
// 또는 inputType이 있는 경우 (입력 가능한 컬럼)
|
||||
if (col.field && (col.editable === true || col.inputType)) {
|
||||
// editable이 명시적으로 true인 경우 (입력 가능한 컬럼)
|
||||
if (col.field && col.editable === true) {
|
||||
allowedFields.add(col.field);
|
||||
}
|
||||
});
|
||||
@@ -961,7 +962,6 @@ export function RepeatScreenModalComponent({
|
||||
contentRow.tableColumns?.map((c) => ({
|
||||
field: c.field,
|
||||
editable: c.editable,
|
||||
inputType: c.inputType,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -1218,7 +1218,7 @@ export function RepeatScreenModalComponent({
|
||||
// 기존 DB 데이터인 경우 (id가 있는 경우) 즉시 삭제
|
||||
if (targetRow?._originalData?.id) {
|
||||
try {
|
||||
const contentRow = contentRows.find((r) => r.id === contentRowId);
|
||||
const contentRow = contentRows.find((r: CardContentRowConfig) => r.id === contentRowId);
|
||||
const targetTable = contentRow?.tableCrud?.targetTable || contentRow?.tableDataSource?.sourceTable;
|
||||
|
||||
if (!targetTable) {
|
||||
@@ -1401,7 +1401,7 @@ export function RepeatScreenModalComponent({
|
||||
// 1단계: 기본 테이블 컬럼 집계만 (외부 테이블 데이터는 아직 없음)
|
||||
const aggregations: Record<string, number> = {};
|
||||
if (groupingConfig.aggregations) {
|
||||
groupingConfig.aggregations.forEach((agg) => {
|
||||
groupingConfig.aggregations.forEach((agg: AggregationConfig) => {
|
||||
const sourceType = agg.sourceType || "column";
|
||||
|
||||
if (sourceType === "column") {
|
||||
@@ -1714,8 +1714,8 @@ export function RepeatScreenModalComponent({
|
||||
// 집계값 재계산
|
||||
const newAggregations: Record<string, number> = {};
|
||||
if (grouping?.aggregations) {
|
||||
grouping.aggregations.forEach((agg) => {
|
||||
newAggregations[agg.resultField] = calculateAggregation(updatedRows, agg);
|
||||
grouping.aggregations.forEach((agg: AggregationConfig) => {
|
||||
newAggregations[agg.resultField] = calculateAggregation(agg, updatedRows, [], newAggregations, card._representativeData);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1735,7 +1735,7 @@ export function RepeatScreenModalComponent({
|
||||
|
||||
const matches = title.match(/\{(\w+)\}/g);
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
matches.forEach((match: string) => {
|
||||
const field = match.slice(1, -1);
|
||||
const value = data[field] || "";
|
||||
title = title.replace(match, String(value));
|
||||
@@ -1779,7 +1779,7 @@ export function RepeatScreenModalComponent({
|
||||
for (const [key, rows] of Object.entries(externalTableData)) {
|
||||
// key 형식: cardId-contentRowId
|
||||
const [cardId, contentRowId] = key.split("-").slice(0, 2);
|
||||
const contentRow = contentRows.find((r) => r.id === contentRowId || key.includes(r.id));
|
||||
const contentRow = contentRows.find((r: CardContentRowConfig) => r.id === contentRowId || key.includes(r.id));
|
||||
|
||||
if (!contentRow?.tableDataSource?.enabled) continue;
|
||||
|
||||
@@ -2101,10 +2101,10 @@ export function RepeatScreenModalComponent({
|
||||
if (isDesignMode) {
|
||||
// 행 타입별 개수 계산
|
||||
const rowTypeCounts = {
|
||||
header: contentRows.filter((r) => r.type === "header").length,
|
||||
aggregation: contentRows.filter((r) => r.type === "aggregation").length,
|
||||
table: contentRows.filter((r) => r.type === "table").length,
|
||||
fields: contentRows.filter((r) => r.type === "fields").length,
|
||||
header: contentRows.filter((r: CardContentRowConfig) => r.type === "header").length,
|
||||
aggregation: contentRows.filter((r: CardContentRowConfig) => r.type === "aggregation").length,
|
||||
table: contentRows.filter((r: CardContentRowConfig) => r.type === "table").length,
|
||||
fields: contentRows.filter((r: CardContentRowConfig) => r.type === "fields").length,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -2263,7 +2263,7 @@ export function RepeatScreenModalComponent({
|
||||
<CardContent className="space-y-4">
|
||||
{/* 🆕 v3: contentRows 기반 렌더링 */}
|
||||
{useNewLayout ? (
|
||||
contentRows.map((contentRow, rowIndex) => (
|
||||
contentRows.map((contentRow: CardContentRowConfig, rowIndex: number) => (
|
||||
<div key={contentRow.id || `crow-${rowIndex}`}>
|
||||
{contentRow.type === "table" && contentRow.tableDataSource?.enabled ? (
|
||||
// 🆕 v3.1: 외부 테이블 데이터 소스 사용
|
||||
@@ -2294,8 +2294,8 @@ export function RepeatScreenModalComponent({
|
||||
<TableRow className="bg-muted/50">
|
||||
{/* 🆕 v3.13: hidden 컬럼 필터링 */}
|
||||
{(contentRow.tableColumns || [])
|
||||
.filter((col) => !col.hidden)
|
||||
.map((col) => (
|
||||
.filter((col: TableColumnConfig) => !col.hidden)
|
||||
.map((col: TableColumnConfig) => (
|
||||
<TableHead
|
||||
key={col.id}
|
||||
style={{ width: col.width }}
|
||||
@@ -2315,7 +2315,7 @@ export function RepeatScreenModalComponent({
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={
|
||||
(contentRow.tableColumns?.filter((col) => !col.hidden)?.length || 0) +
|
||||
(contentRow.tableColumns?.filter((col: TableColumnConfig) => !col.hidden)?.length || 0) +
|
||||
(contentRow.tableCrud?.allowDelete ? 1 : 0)
|
||||
}
|
||||
className="text-muted-foreground py-8 text-center"
|
||||
@@ -2335,8 +2335,8 @@ export function RepeatScreenModalComponent({
|
||||
>
|
||||
{/* 🆕 v3.13: hidden 컬럼 필터링 */}
|
||||
{(contentRow.tableColumns || [])
|
||||
.filter((col) => !col.hidden)
|
||||
.map((col) => (
|
||||
.filter((col: TableColumnConfig) => !col.hidden)
|
||||
.map((col: TableColumnConfig) => (
|
||||
<TableCell
|
||||
key={`${row._rowId}-${col.id}`}
|
||||
className={cn(
|
||||
@@ -2446,7 +2446,7 @@ export function RepeatScreenModalComponent({
|
||||
<>
|
||||
{tableLayout?.headerRows && tableLayout.headerRows.length > 0 && (
|
||||
<div className="bg-muted/30 space-y-3 rounded-lg p-4">
|
||||
{tableLayout.headerRows.map((row, rowIndex) => (
|
||||
{tableLayout.headerRows.map((row: CardRowConfig, rowIndex: number) => (
|
||||
<div
|
||||
key={row.id || `hrow-${rowIndex}`}
|
||||
className={cn(
|
||||
@@ -2458,7 +2458,7 @@ export function RepeatScreenModalComponent({
|
||||
)}
|
||||
style={{ gap: row.gap || "16px" }}
|
||||
>
|
||||
{row.columns.map((col, colIndex) => (
|
||||
{row.columns.map((col: CardColumnConfig, colIndex: number) => (
|
||||
<div key={col.id || `hcol-${colIndex}`} style={{ width: col.width }}>
|
||||
{renderHeaderColumn(col, card, grouping?.aggregations || [])}
|
||||
</div>
|
||||
@@ -2473,7 +2473,7 @@ export function RepeatScreenModalComponent({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
{tableLayout.tableColumns.map((col) => (
|
||||
{tableLayout.tableColumns.map((col: TableColumnConfig) => (
|
||||
<TableHead
|
||||
key={col.id}
|
||||
style={{ width: col.width }}
|
||||
@@ -2487,7 +2487,7 @@ export function RepeatScreenModalComponent({
|
||||
<TableBody>
|
||||
{card._rows.map((row) => (
|
||||
<TableRow key={row._rowId} className={cn(row._isDirty && "bg-primary/5")}>
|
||||
{tableLayout.tableColumns.map((col) => (
|
||||
{tableLayout.tableColumns.map((col: TableColumnConfig) => (
|
||||
<TableCell
|
||||
key={`${row._rowId}-${col.id}`}
|
||||
className={cn("text-sm", col.align && `text-${col.align}`)}
|
||||
@@ -2525,7 +2525,7 @@ export function RepeatScreenModalComponent({
|
||||
!footerConfig.alignment && "justify-end",
|
||||
)}
|
||||
>
|
||||
{footerConfig.buttons.map((btn) => (
|
||||
{footerConfig.buttons.map((btn: FooterButtonConfig) => (
|
||||
<Button
|
||||
key={btn.id}
|
||||
variant={btn.variant || "default"}
|
||||
@@ -2608,7 +2608,7 @@ export function RepeatScreenModalComponent({
|
||||
<CardContent className="space-y-4">
|
||||
{/* 🆕 v3: contentRows 기반 렌더링 */}
|
||||
{useNewLayout
|
||||
? contentRows.map((contentRow, rowIndex) => (
|
||||
? contentRows.map((contentRow: CardContentRowConfig, rowIndex: number) => (
|
||||
<div key={contentRow.id || `crow-${rowIndex}`}>
|
||||
{renderSimpleContentRow(contentRow, card, (value, field) =>
|
||||
handleCardDataChange(card._cardId, field, value),
|
||||
@@ -2616,7 +2616,7 @@ export function RepeatScreenModalComponent({
|
||||
</div>
|
||||
))
|
||||
: // 레거시: cardLayout 사용
|
||||
cardLayout.map((row, rowIndex) => (
|
||||
cardLayout.map((row: CardRowConfig, rowIndex: number) => (
|
||||
<div
|
||||
key={row.id || `row-${rowIndex}`}
|
||||
className={cn(
|
||||
@@ -2625,7 +2625,7 @@ export function RepeatScreenModalComponent({
|
||||
)}
|
||||
style={{ gap: row.gap || "16px" }}
|
||||
>
|
||||
{row.columns.map((col, colIndex) => (
|
||||
{row.columns.map((col: CardColumnConfig, colIndex: number) => (
|
||||
<div key={col.id || `col-${colIndex}`} style={{ width: col.width }}>
|
||||
{renderColumn(col, card, (value) => handleCardDataChange(card._cardId, col.field, value))}
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@ export interface CardContentRowConfig {
|
||||
|
||||
// === aggregation 타입일 때 ===
|
||||
aggregationFields?: AggregationDisplayConfig[]; // 표시할 집계 필드들
|
||||
aggregationLayout?: "horizontal" | "grid"; // 집계 레이아웃 (가로 나열 / 그리드)
|
||||
aggregationLayout?: "horizontal" | "grid" | "vertical"; // 집계 레이아웃 (가로 나열 / 그리드 / 세로)
|
||||
aggregationColumns?: number; // grid일 때 컬럼 수 (기본: 4)
|
||||
|
||||
// === table 타입일 때 ===
|
||||
|
||||
+5
-602
@@ -188,8 +188,8 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
const { getCategoryValues } = await import("@/lib/api/tableCategoryValue");
|
||||
const response = await getCategoryValues(fieldSourceTable, field.name, false);
|
||||
|
||||
if (response.success && response.data && response.data.length > 0) {
|
||||
newOptions[field.name] = response.data.map((item: any) => ({
|
||||
if (response.success && 'data' in response && response.data && response.data.length > 0) {
|
||||
newOptions[field.name] = (response.data as any[]).map((item: any) => ({
|
||||
label: item.value_label,
|
||||
value: item.value_code,
|
||||
}));
|
||||
@@ -588,7 +588,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
sourceData = items[0]?.originalData || {};
|
||||
}
|
||||
|
||||
componentConfig.parentDataMapping.forEach((mapping) => {
|
||||
componentConfig.parentDataMapping!.forEach((mapping) => {
|
||||
let value: any;
|
||||
|
||||
// 수정 모드: originalData의 targetField 값 우선 사용
|
||||
@@ -985,6 +985,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
if (!componentConfig.autoCalculation) return 0;
|
||||
|
||||
const { inputFields } = componentConfig.autoCalculation;
|
||||
if (!inputFields) return 0;
|
||||
|
||||
// 기본 단가
|
||||
const basePrice = parseFloat(entry[inputFields.basePrice] || "0");
|
||||
@@ -1075,6 +1076,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
// 가격 관련 필드가 변경되면 자동 계산
|
||||
if (componentConfig.autoCalculation) {
|
||||
const { inputFields, targetField } = componentConfig.autoCalculation;
|
||||
if (!inputFields) return item;
|
||||
const priceRelatedFields = [
|
||||
inputFields.basePrice,
|
||||
inputFields.discountType,
|
||||
@@ -2064,611 +2066,12 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
);
|
||||
};
|
||||
|
||||
// 🔧 기존 renderGridLayout (백업 - 사용 안 함)
|
||||
const renderGridLayout_OLD = () => {
|
||||
return (
|
||||
<div className="bg-card space-y-4 p-4">
|
||||
{/* Modal 모드: 추가 버튼 */}
|
||||
{isModalMode && !isEditing && items.length === 0 && (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-muted-foreground mb-4 text-sm">항목을 추가하려면 추가 버튼을 클릭하세요</p>
|
||||
<Button type="button" onClick={() => setIsEditing(true)} size="sm" className="text-xs sm:text-sm">
|
||||
+ 추가
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal 모드: 편집 중인 항목 (입력창 표시) */}
|
||||
{isModalMode &&
|
||||
isEditing &&
|
||||
editingItemId &&
|
||||
(() => {
|
||||
const editingItem = items.find((item) => item.id === editingItemId);
|
||||
if (!editingItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-primary border-2 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-sm font-semibold">
|
||||
<span>
|
||||
품목:{" "}
|
||||
{getFieldValue(editingItem.originalData, componentConfig.displayColumns?.[0]?.name || "") ||
|
||||
"항목"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditingItemId(null);
|
||||
setEditingDetailId(null);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 원본 데이터 요약 */}
|
||||
<div className="text-muted-foreground bg-muted rounded p-2 text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => editingItem.originalData[col.name])
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
|
||||
{/* 🆕 이미 입력된 상세 항목들 표시 */}
|
||||
{editingItem.details.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium">입력된 항목 ({editingItem.details.length}개)</div>
|
||||
{editingItem.details.map((detail, idx) => (
|
||||
<div
|
||||
key={detail.id}
|
||||
className="bg-muted/30 flex items-center justify-between rounded border p-2 text-xs"
|
||||
>
|
||||
<span>
|
||||
{idx + 1}. {detail[componentConfig.additionalFields?.[0]?.name] || "입력됨"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDetail(editingItem.id, detail.id)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 입력 필드 */}
|
||||
{componentConfig.additionalFields &&
|
||||
componentConfig.additionalFields.length > 0 &&
|
||||
editingDetailId &&
|
||||
(() => {
|
||||
// 현재 편집 중인 detail 찾기 (없으면 빈 객체)
|
||||
const currentDetail = editingItem.details.find((d) => d.id === editingDetailId) || {
|
||||
id: editingDetailId,
|
||||
};
|
||||
return renderFieldsByGroup(editingItem.id, editingDetailId, currentDetail);
|
||||
})()}
|
||||
|
||||
{/* 액션 버튼들 */}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAddDetail(editingItem.id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
+ 이 품목에 추가 입력
|
||||
</Button>
|
||||
<Button type="button" variant="default" onClick={handleNextItem} size="sm" className="text-xs">
|
||||
다음 품목
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 저장된 항목들 (inline 모드 또는 modal 모드에서 편집 완료된 항목) */}
|
||||
{items.map((item, index) => {
|
||||
// Modal 모드에서 현재 편집 중인 항목은 위에서 렌더링하므로 스킵
|
||||
if (isModalMode && isEditing && item.id === editingItemId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Modal 모드: 작은 요약 카드
|
||||
if (isModalMode) {
|
||||
return (
|
||||
<Card key={item.id} className="bg-muted/50 border shadow-sm">
|
||||
<CardContent className="p-3">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 text-sm font-semibold">
|
||||
{index + 1}.{" "}
|
||||
{getFieldValue(item.originalData, componentConfig.displayColumns?.[0]?.name || "") || "항목"}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => getFieldValue(item.originalData, col.name))
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
setEditingItemId(item.id);
|
||||
const newDetailId = `detail-${Date.now()}`;
|
||||
setEditingDetailId(newDetailId);
|
||||
}}
|
||||
className="h-7 text-xs text-amber-600"
|
||||
>
|
||||
추가 입력
|
||||
</Button>
|
||||
{componentConfig.allowRemove && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
className="h-7 w-7 text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* 🆕 입력된 상세 항목들 표시 */}
|
||||
{item.details && item.details.length > 0 && (
|
||||
<div className="border-primary mt-2 space-y-1 border-l-2 pl-4">
|
||||
{item.details.map((detail, detailIdx) => (
|
||||
<div key={detail.id} className="text-primary text-xs">
|
||||
{detailIdx + 1}. {detail[componentConfig.additionalFields?.[0]?.name] || "입력됨"}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline 모드: 각 품목마다 여러 상세 항목 표시
|
||||
return (
|
||||
<Card key={item.id} className="border shadow-sm">
|
||||
<CardContent className="space-y-3 p-4">
|
||||
{/* 제목 (품명) */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-base font-semibold">
|
||||
{index + 1}.{" "}
|
||||
{getFieldValue(item.originalData, componentConfig.displayColumns?.[0]?.name || "") || "항목"}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newDetailId = `detail-${Date.now()}`;
|
||||
handleAddDetail(item.id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
+ 상세 입력 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 원본 데이터 요약 (작은 텍스트, | 구분자) */}
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => getFieldValue(item.originalData, col.name))
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
|
||||
{/* 🆕 각 상세 항목 표시 */}
|
||||
{item.details && item.details.length > 0 ? (
|
||||
<div className="border-primary space-y-3 border-l-2 pl-4">
|
||||
{item.details.map((detail, detailIdx) => (
|
||||
<Card key={detail.id} className="border-dashed">
|
||||
<CardContent className="space-y-2 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium">상세 항목 {detailIdx + 1}</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDetail(item.id, detail.id)}
|
||||
className="h-6 w-6 p-0 text-destructive"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
{/* 입력 필드들 */}
|
||||
{renderFieldsByGroup(item.id, detail.id, detail)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground pl-4 text-xs italic">아직 입력된 상세 항목이 없습니다.</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Modal 모드: 하단 추가 버튼 (항목이 있을 때) */}
|
||||
{isModalMode && !isEditing && items.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 새 항목 추가 로직은 여기서 처리하지 않고, 기존 항목이 있으면 첫 항목을 편집 모드로
|
||||
setIsEditing(true);
|
||||
setEditingItemId(items[0]?.id || null);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full border-dashed text-xs sm:text-sm"
|
||||
>
|
||||
+ 추가
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 기존 테이블 레이아웃 (사용 안 함, 삭제 예정)
|
||||
const renderOldGridLayout = () => {
|
||||
return (
|
||||
<div className="bg-card overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-background">
|
||||
{componentConfig.showIndex && (
|
||||
<TableHead className="h-12 w-12 px-4 py-3 text-center text-xs font-semibold sm:text-sm">#</TableHead>
|
||||
)}
|
||||
|
||||
{/* 원본 데이터 컬럼 */}
|
||||
{componentConfig.displayColumns?.map((col) => (
|
||||
<TableHead key={col.name} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
||||
{col.label || col.name}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
{/* 추가 입력 필드 컬럼 */}
|
||||
{componentConfig.additionalFields?.map((field) => (
|
||||
<TableHead key={field.name} className="h-12 px-4 py-3 text-xs font-semibold sm:text-sm">
|
||||
{field.label}{(field.required || isColumnRequiredByMeta(componentConfig.targetTable, field.name)) && <span className="text-orange-500">*</span>}
|
||||
</TableHead>
|
||||
))}
|
||||
|
||||
{componentConfig.allowRemove && (
|
||||
<TableHead className="h-12 w-20 px-4 py-3 text-center text-xs font-semibold sm:text-sm">작업</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow key={item.id} className="bg-background hover:bg-muted/50 transition-colors">
|
||||
{/* 인덱스 번호 */}
|
||||
{componentConfig.showIndex && (
|
||||
<TableCell className="h-14 px-4 py-3 text-center text-xs font-medium sm:text-sm">
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
)}
|
||||
|
||||
{/* 원본 데이터 표시 */}
|
||||
{componentConfig.displayColumns?.map((col) => (
|
||||
<TableCell key={col.name} className="h-14 px-4 py-3 text-xs sm:text-sm">
|
||||
{getFieldValue(item.originalData, col.name) || "-"}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
{/* 추가 입력 필드 */}
|
||||
{componentConfig.additionalFields?.map((field) => (
|
||||
<TableCell key={field.name} className="h-14 px-4 py-3">
|
||||
{renderField(field, item)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
||||
{/* 삭제 버튼 */}
|
||||
{componentConfig.allowRemove && (
|
||||
<TableCell className="h-14 px-4 py-3 text-center">
|
||||
{!componentConfig.disabled && !componentConfig.readonly && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive h-7 w-7 sm:h-8 sm:w-8"
|
||||
title="항목 제거"
|
||||
>
|
||||
<X className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 🆕 Card 레이아웃 렌더링 (Grid와 동일)
|
||||
const renderCardLayout = () => {
|
||||
return renderGridLayout();
|
||||
};
|
||||
|
||||
// 🔧 기존 renderCardLayout (백업 - 사용 안 함)
|
||||
const renderCardLayout_OLD = () => {
|
||||
const isModalMode = componentConfig.inputMode === "modal";
|
||||
return (
|
||||
<div className="bg-card space-y-3 p-4">
|
||||
{/* Modal 모드: 추가 버튼 */}
|
||||
{isModalMode && !isEditing && items.length === 0 && (
|
||||
<div className="py-8 text-center">
|
||||
<p className="text-muted-foreground mb-4 text-sm">항목을 추가하려면 추가 버튼을 클릭하세요</p>
|
||||
<Button type="button" onClick={() => setIsEditing(true)} size="sm" className="text-xs sm:text-sm">
|
||||
+ 추가
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal 모드: 편집 중인 항목 (입력창 표시) */}
|
||||
{isModalMode &&
|
||||
isEditing &&
|
||||
editingItemId &&
|
||||
(() => {
|
||||
const editingItem = items.find((item) => item.id === editingItemId);
|
||||
if (!editingItem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-primary border-2 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center justify-between text-sm font-semibold">
|
||||
<span>
|
||||
품목:{" "}
|
||||
{getFieldValue(editingItem.originalData, componentConfig.displayColumns?.[0]?.name || "") ||
|
||||
"항목"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditingItemId(null);
|
||||
setEditingDetailId(null);
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
취소
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* 원본 데이터 요약 */}
|
||||
<div className="text-muted-foreground bg-muted rounded p-2 text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => editingItem.originalData[col.name])
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
|
||||
{/* 🆕 이미 입력된 상세 항목들 표시 */}
|
||||
{editingItem.details.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-medium">입력된 항목 ({editingItem.details.length}개)</div>
|
||||
{editingItem.details.map((detail, idx) => (
|
||||
<div
|
||||
key={detail.id}
|
||||
className="bg-muted/30 flex items-center justify-between rounded border p-2 text-xs"
|
||||
>
|
||||
<span>
|
||||
{idx + 1}. {detail[componentConfig.additionalFields?.[0]?.name] || "입력됨"}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDetail(editingItem.id, detail.id)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 추가 입력 필드 */}
|
||||
{componentConfig.additionalFields &&
|
||||
componentConfig.additionalFields.length > 0 &&
|
||||
editingDetailId &&
|
||||
(() => {
|
||||
// 현재 편집 중인 detail 찾기 (없으면 빈 객체)
|
||||
const currentDetail = editingItem.details.find((d) => d.id === editingDetailId) || {
|
||||
id: editingDetailId,
|
||||
};
|
||||
return renderFieldsByGroup(editingItem.id, editingDetailId, currentDetail);
|
||||
})()}
|
||||
|
||||
{/* 액션 버튼들 */}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleAddDetail(editingItem.id)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
+ 이 품목에 추가 입력
|
||||
</Button>
|
||||
<Button type="button" variant="default" onClick={handleNextItem} size="sm" className="text-xs">
|
||||
다음 품목
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 저장된 항목들 (inline 모드 또는 modal 모드에서 편집 완료된 항목) */}
|
||||
{items.map((item, index) => {
|
||||
// Modal 모드에서 현재 편집 중인 항목은 위에서 렌더링하므로 스킵
|
||||
if (isModalMode && isEditing && item.id === editingItemId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Modal 모드: 작은 요약 카드
|
||||
if (isModalMode) {
|
||||
return (
|
||||
<Card key={item.id} className="bg-muted/50 border shadow-sm">
|
||||
<CardContent className="flex items-center justify-between p-3">
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 text-sm font-semibold">
|
||||
{index + 1}.{" "}
|
||||
{getFieldValue(item.originalData, componentConfig.displayColumns?.[0]?.name || "") || "항목"}
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => getFieldValue(item.originalData, col.name))
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(true);
|
||||
setEditingItemId(item.id);
|
||||
}}
|
||||
className="h-7 text-xs text-amber-600"
|
||||
>
|
||||
수정
|
||||
</Button>
|
||||
{componentConfig.allowRemove && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveItem(item.id)}
|
||||
className="text-destructive h-7 text-xs"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline 모드: 각 품목마다 여러 상세 항목 표시
|
||||
return (
|
||||
<Card key={item.id} className="border shadow-sm">
|
||||
<CardContent className="space-y-3 p-4">
|
||||
{/* 제목 (품명) */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-base font-semibold">
|
||||
{index + 1}.{" "}
|
||||
{getFieldValue(item.originalData, componentConfig.displayColumns?.[0]?.name || "") || "항목"}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newDetailId = `detail-${Date.now()}`;
|
||||
handleAddDetail(item.id);
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
+ 상세 입력 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 원본 데이터 요약 (작은 텍스트, | 구분자) */}
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{componentConfig.displayColumns
|
||||
?.map((col) => getFieldValue(item.originalData, col.name))
|
||||
.filter(Boolean)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
|
||||
{/* 🆕 각 상세 항목 표시 */}
|
||||
{item.details && item.details.length > 0 ? (
|
||||
<div className="border-primary space-y-3 border-l-2 pl-4">
|
||||
{item.details.map((detail, detailIdx) => (
|
||||
<Card key={detail.id} className="border-dashed">
|
||||
<CardContent className="space-y-2 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium">상세 항목 {detailIdx + 1}</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveDetail(item.id, detail.id)}
|
||||
className="h-6 w-6 p-0 text-destructive"
|
||||
>
|
||||
X
|
||||
</Button>
|
||||
</div>
|
||||
{/* 입력 필드들 */}
|
||||
{renderFieldsByGroup(item.id, detail.id, detail)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground pl-4 text-xs italic">아직 입력된 상세 항목이 없습니다.</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Modal 모드: 하단 추가 버튼 (항목이 있을 때) */}
|
||||
{isModalMode && !isEditing && items.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// 새 항목 추가 로직은 여기서 처리하지 않고, 기존 항목이 있으면 첫 항목을 편집 모드로
|
||||
setIsEditing(true);
|
||||
setEditingItemId(items[0]?.id || null);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full border-dashed text-xs sm:text-sm"
|
||||
>
|
||||
+ 추가
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={componentStyle} className={cn("space-y-4", className)} onClick={handleClick}>
|
||||
{/* 레이아웃에 따라 렌더링 */}
|
||||
|
||||
@@ -978,9 +978,9 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
// repeater 계열 우선 탐색
|
||||
for (const [id, receiver] of allReceivers) {
|
||||
if (
|
||||
receiver.componentType === "repeater-field-group" ||
|
||||
receiver.componentType === "v2-repeater" ||
|
||||
receiver.componentType === "repeater"
|
||||
(receiver.componentType as string) === "repeater-field-group" ||
|
||||
(receiver.componentType as string) === "v2-repeater" ||
|
||||
(receiver.componentType as string) === "repeater"
|
||||
) {
|
||||
effectiveTargetComponentId = id;
|
||||
console.log(`✅ [ButtonPrimary] 리피터 자동 발견: ${id} (${receiver.componentType})`);
|
||||
@@ -991,7 +991,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
// repeater가 없으면 소스가 아닌 첫 번째 DataReceiver 사용
|
||||
if (!effectiveTargetComponentId) {
|
||||
for (const [id, receiver] of allReceivers) {
|
||||
if (receiver.componentType === "table-list" || receiver.componentType === "data-table") {
|
||||
if ((receiver.componentType as string) === "table-list" || (receiver.componentType as string) === "data-table") {
|
||||
effectiveTargetComponentId = id;
|
||||
console.log(`✅ [ButtonPrimary] DataReceiver 자동 발견: ${id} (${receiver.componentType})`);
|
||||
break;
|
||||
@@ -1289,7 +1289,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
splitPanelContext: splitPanelContext
|
||||
? {
|
||||
selectedLeftData: splitPanelContext.selectedLeftData,
|
||||
refreshRightPanel: splitPanelContext.refreshRightPanel,
|
||||
}
|
||||
: undefined,
|
||||
} as ButtonActionContext;
|
||||
|
||||
+47
-48
@@ -162,7 +162,7 @@ function SortableColumnRow({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : isEntityJoin ? (
|
||||
<Link2 className="text-primary h-3 w-3 shrink-0" title="Entity 조인 컬럼" />
|
||||
<Link2 className="text-primary h-3 w-3 shrink-0" aria-label="Entity 조인 컬럼" />
|
||||
) : (
|
||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||
)}
|
||||
@@ -353,7 +353,7 @@ const ScreenSelector: React.FC<{
|
||||
const { screenApi } = await import("@/lib/api/screen");
|
||||
const response = await screenApi.getScreens({ page: 1, size: 1000 });
|
||||
setScreens(
|
||||
response.data.map((s) => ({ screen_id: s.screen_id, screen_name: s.screen_name, screen_code: s.screen_code })),
|
||||
response.data.map((s) => ({ screen_id: s.screen_id!, screen_name: s.screen_name!, screen_code: s.screen_code! })),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("화면 목록 로드 실패:", error);
|
||||
@@ -546,16 +546,16 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<CommandGroup className="max-h-[200px] overflow-auto">
|
||||
{availableRightTables.map((table) => (
|
||||
<CommandItem
|
||||
key={table.table_name}
|
||||
value={`${table.display_name || ""} ${table.table_name}`}
|
||||
onSelect={() => updateTab({ tableName: table.table_name, columns: [] })}
|
||||
key={table.tableName}
|
||||
value={`${table.displayName || ""} ${table.tableName}`}
|
||||
onSelect={() => updateTab({ tableName: table.tableName, columns: [] })}
|
||||
>
|
||||
<Check
|
||||
className={cn("mr-2 h-4 w-4", tab.tableName === table.table_name ? "opacity-100" : "opacity-0")}
|
||||
className={cn("mr-2 h-4 w-4", tab.tableName === table.tableName ? "opacity-100" : "opacity-0")}
|
||||
/>
|
||||
{table.display_name || table.table_name}
|
||||
{table.display_name && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">({table.table_name})</span>
|
||||
{table.displayName || table.tableName}
|
||||
{table.displayName && (
|
||||
<span className="text-muted-foreground ml-2 text-xs">({table.tableName})</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -656,7 +656,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
</SelectItem>
|
||||
{leftTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -689,7 +689,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
</SelectItem>
|
||||
{tabColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -862,11 +862,11 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
}}
|
||||
availableChildColumns={tabColumns.map((c) => ({
|
||||
columnName: c.columnName,
|
||||
columnLabel: c.columnLabel || c.columnName,
|
||||
columnLabel: c.displayName || c.columnName,
|
||||
}))}
|
||||
availableParentColumns={leftTableColumns.map((c) => ({
|
||||
columnName: c.columnName,
|
||||
columnLabel: c.columnLabel || c.columnName,
|
||||
columnLabel: c.displayName || c.columnName,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
@@ -891,14 +891,14 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
updateTab({
|
||||
columns: [
|
||||
...selectedColumns,
|
||||
{ name: column.columnName, label: column.columnLabel || column.columnName, width: 20 },
|
||||
{ name: column.columnName, label: column.displayName || column.columnName, width: 20 },
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -939,7 +939,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<SelectContent>
|
||||
{tabColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -962,7 +962,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<SelectContent>
|
||||
{leftTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1077,7 +1077,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||
<Link2 className="text-primary h-3 w-3 shrink-0" />
|
||||
<span className="text-primary truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1136,7 +1136,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
newColumns[colIndex] = {
|
||||
...col,
|
||||
name: value,
|
||||
label: selectedCol?.columnLabel || value,
|
||||
label: selectedCol?.displayName || value,
|
||||
};
|
||||
updateTab({ addModalColumns: newColumns });
|
||||
}}
|
||||
@@ -1147,7 +1147,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<SelectContent>
|
||||
{tabColumns.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1245,7 +1245,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<SelectContent>
|
||||
{tabColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1267,7 +1267,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
<SelectContent>
|
||||
{tabColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1344,7 +1344,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
value={tab.editButton?.buttonLabel || ""}
|
||||
onChange={(e) => {
|
||||
updateTab({
|
||||
editButton: { ...tab.editButton, enabled: true, buttonLabel: e.target.value || undefined },
|
||||
editButton: { ...tab.editButton, mode: tab.editButton?.mode || "auto", enabled: true, buttonLabel: e.target.value || undefined },
|
||||
});
|
||||
}}
|
||||
placeholder="수정"
|
||||
@@ -1357,7 +1357,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
value={tab.editButton?.buttonVariant || "ghost"}
|
||||
onValueChange={(value: "default" | "outline" | "ghost") => {
|
||||
updateTab({
|
||||
editButton: { ...tab.editButton, enabled: true, buttonVariant: value },
|
||||
editButton: { ...tab.editButton, mode: tab.editButton?.mode || "auto", enabled: true, buttonVariant: value },
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -1389,12 +1389,12 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
? [...current, col.columnName]
|
||||
: current.filter((c) => c !== col.columnName);
|
||||
updateTab({
|
||||
editButton: { ...tab.editButton, enabled: true, groupByColumns: newColumns },
|
||||
editButton: { ...tab.editButton, mode: tab.editButton?.mode || "auto", enabled: true, groupByColumns: newColumns },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`tab-${tabIndex}-groupby-${col.columnName}`} className="text-[10px]">
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@@ -1449,7 +1449,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||
value={tab.addButton?.buttonLabel || ""}
|
||||
onChange={(e) => {
|
||||
updateTab({
|
||||
addButton: { ...tab.addButton, enabled: true, buttonLabel: e.target.value || undefined },
|
||||
addButton: { ...tab.addButton, mode: tab.addButton?.mode || "auto", enabled: true, buttonLabel: e.target.value || undefined },
|
||||
});
|
||||
}}
|
||||
placeholder="추가"
|
||||
@@ -1644,10 +1644,10 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
const columnsResponse = await tableTypeApi.getColumns(tableName);
|
||||
console.log(`📊 테이블 ${tableName} 컬럼 응답:`, columnsResponse);
|
||||
|
||||
const columns: ColumnInfo[] = (columnsResponse || []).map((col: any) => ({
|
||||
const columns = (columnsResponse || []).map((col: any) => ({
|
||||
tableName: col.table_name || tableName,
|
||||
columnName: col.column_name,
|
||||
columnLabel: col.display_name || col.column_label || col.column_name,
|
||||
displayName: col.display_name || col.column_label || col.column_name,
|
||||
dataType: col.data_type,
|
||||
webType: col.web_type,
|
||||
input_type: col.input_type,
|
||||
@@ -1662,7 +1662,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
referenceTable: col.reference_table, // 🆕 참조 테이블
|
||||
referenceColumn: col.reference_column, // 🆕 참조 컬럼
|
||||
displayColumn: col.display_column, // 🆕 표시 컬럼
|
||||
}));
|
||||
})) as ColumnInfo[];
|
||||
|
||||
console.log(`✅ 테이블 ${tableName} 컬럼 ${columns.length}개 로드됨:`, columns);
|
||||
setLoadedTableColumns((prev) => ({ ...prev, [tableName]: columns }));
|
||||
@@ -1732,13 +1732,13 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
|
||||
try {
|
||||
const refColumnsResponse = await tableTypeApi.getColumns(refTableName);
|
||||
const refColumns: ColumnInfo[] = (refColumnsResponse || []).map((col: any) => ({
|
||||
const refColumns = (refColumnsResponse || []).map((col: any) => ({
|
||||
tableName: col.table_name || refTableName,
|
||||
columnName: col.column_name,
|
||||
columnLabel: col.display_name || col.column_label || col.column_name,
|
||||
displayName: col.display_name || col.column_label || col.column_name,
|
||||
dataType: col.data_type,
|
||||
input_type: col.input_type,
|
||||
}));
|
||||
})) as ColumnInfo[];
|
||||
|
||||
referenceTableData.push({ tableName: refTableName, columns: refColumns });
|
||||
console.log(` ✅ 참조 테이블 ${refTableName} 컬럼 ${refColumns.length}개 로드됨`);
|
||||
@@ -1776,8 +1776,8 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
// 🆕 좌측/우측 테이블이 모두 선택되면 엔티티 관계 자동 감지
|
||||
const [autoDetectedRelations, setAutoDetectedRelations] = useState<
|
||||
Array<{
|
||||
left_column: string;
|
||||
right_column: string;
|
||||
leftColumn: string;
|
||||
rightColumn: string;
|
||||
direction: "left_to_right" | "right_to_left";
|
||||
inputType: string;
|
||||
displayColumn?: string;
|
||||
@@ -1815,11 +1815,10 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
relation: {
|
||||
...config.rightPanel?.relation,
|
||||
type: "join",
|
||||
useMultipleKeys: true,
|
||||
keys: [
|
||||
{
|
||||
leftColumn: firstRel.left_column,
|
||||
rightColumn: firstRel.right_column,
|
||||
leftColumn: firstRel.leftColumn,
|
||||
rightColumn: firstRel.rightColumn,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2398,7 +2397,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
...selectedColumns,
|
||||
{
|
||||
name: column.columnName,
|
||||
label: column.columnLabel || column.columnName,
|
||||
label: column.displayName || column.columnName,
|
||||
width: 20,
|
||||
},
|
||||
],
|
||||
@@ -2407,7 +2406,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
>
|
||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -2488,7 +2487,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
/>
|
||||
<Link2 className="text-primary h-3 w-3 shrink-0" />
|
||||
<span className="text-primary truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -2521,7 +2520,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
(col) =>
|
||||
({
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel || col.columnName,
|
||||
columnLabel: col.displayName || col.columnName,
|
||||
dataType: col.dataType || "text",
|
||||
input_type: (col as any).input_type,
|
||||
}) as any,
|
||||
@@ -3076,7 +3075,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
...selectedColumns,
|
||||
{
|
||||
name: column.columnName,
|
||||
label: column.columnLabel || column.columnName,
|
||||
label: column.displayName || column.columnName,
|
||||
width: 20,
|
||||
},
|
||||
],
|
||||
@@ -3085,7 +3084,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
>
|
||||
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -3163,7 +3162,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
/>
|
||||
<Link2 className="text-primary h-3 w-3 shrink-0" />
|
||||
<span className="text-primary truncate text-xs">
|
||||
{column.columnLabel || column.columnName}
|
||||
{column.displayName || column.columnName}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -3198,7 +3197,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
(col) =>
|
||||
({
|
||||
columnName: col.columnName,
|
||||
columnLabel: col.columnLabel || col.columnName,
|
||||
columnLabel: col.displayName || col.columnName,
|
||||
dataType: col.dataType || "text",
|
||||
input_type: (col as any).input_type,
|
||||
}) as any,
|
||||
@@ -3254,7 +3253,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
<SelectContent>
|
||||
{rightTableColumns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -3323,7 +3322,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
)
|
||||
.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName}>
|
||||
{col.columnLabel || col.columnName}
|
||||
{col.displayName || col.columnName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -111,7 +111,7 @@ function resolveStatusMappings(src: TimelineDataSource): StatusValueMapping[] {
|
||||
if (src.statusMappings && src.statusMappings.length > 0) return src.statusMappings;
|
||||
|
||||
// 레거시 호환: 기존 statusValues 객체가 있으면 변환
|
||||
const sv = (src as Record<string, unknown>).statusValues as Record<string, string> | undefined;
|
||||
const sv = (src as unknown as Record<string, unknown>).statusValues as Record<string, string> | undefined;
|
||||
return [
|
||||
{ dbValue: sv?.waiting || "waiting", label: "대기", semantic: "pending" as const },
|
||||
{ dbValue: sv?.accepted || "accepted", label: "접수", semantic: "active" as const },
|
||||
@@ -428,7 +428,7 @@ export function PopCardListV2Component({
|
||||
[VIRTUAL_SUB_SEMANTIC]: matched.semantic || "pending",
|
||||
[VIRTUAL_SUB_PROCESS]: matched.processName,
|
||||
[VIRTUAL_SUB_SEQ]: matched.seqNo,
|
||||
};
|
||||
} as RowData;
|
||||
})
|
||||
.filter((row): row is RowData => row !== null);
|
||||
|
||||
@@ -600,7 +600,7 @@ export function PopCardListV2Component({
|
||||
[VIRTUAL_SUB_SEMANTIC]: matched.semantic || "pending",
|
||||
[VIRTUAL_SUB_PROCESS]: matched.processName,
|
||||
[VIRTUAL_SUB_SEQ]: matched.seqNo,
|
||||
};
|
||||
} as RowData;
|
||||
})
|
||||
.filter((row): row is RowData => row !== null);
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ function getConnectedComponentInfo(
|
||||
}
|
||||
if (Array.isArray(compCfg.listColumns)) {
|
||||
for (const lc of compCfg.listColumns) {
|
||||
if (lc.columnName) displayedColumns.add(lc.columnName);
|
||||
if (lc.column_name) displayedColumns.add(lc.column_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,8 +357,8 @@ function FilterConnectionSection({ cfg, update, showFieldName, fixedFilterMode,
|
||||
for (const res of results) {
|
||||
if (res.success && res.data?.columns) {
|
||||
for (const col of res.data.columns) {
|
||||
if (!seen.has(col.columnName)) {
|
||||
seen.add(col.columnName);
|
||||
if (!seen.has(col.column_name)) {
|
||||
seen.add(col.column_name);
|
||||
allCols.push(col);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +380,7 @@ function FilterConnectionSection({ cfg, update, showFieldName, fixedFilterMode,
|
||||
const displayed: ColumnTypeInfo[] = [];
|
||||
const others: ColumnTypeInfo[] = [];
|
||||
for (const col of targetColumns) {
|
||||
if (connInfo.displayedColumns.has(col.columnName)) {
|
||||
if (connInfo.displayedColumns.has(col.column_name)) {
|
||||
displayed.push(col);
|
||||
} else {
|
||||
others.push(col);
|
||||
@@ -406,15 +406,15 @@ function FilterConnectionSection({ cfg, update, showFieldName, fixedFilterMode,
|
||||
};
|
||||
|
||||
const renderColumnCheckbox = (col: ColumnTypeInfo) => (
|
||||
<div key={col.columnName} className="flex items-center gap-2">
|
||||
<div key={col.column_name} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`filter_col_${col.columnName}`}
|
||||
checked={selectedFilterCols.includes(col.columnName)}
|
||||
onCheckedChange={() => toggleFilterColumn(col.columnName)}
|
||||
id={`filter_col_${col.column_name}`}
|
||||
checked={selectedFilterCols.includes(col.column_name)}
|
||||
onCheckedChange={() => toggleFilterColumn(col.column_name)}
|
||||
/>
|
||||
<Label htmlFor={`filter_col_${col.columnName}`} className="text-[10px]">
|
||||
{col.displayName || col.columnName}
|
||||
<span className="ml-1 text-muted-foreground">({col.columnName})</span>
|
||||
<Label htmlFor={`filter_col_${col.column_name}`} className="text-[10px]">
|
||||
{col.display_name || col.column_name}
|
||||
<span className="ml-1 text-muted-foreground">({col.column_name})</span>
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
@@ -837,7 +837,7 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
className="h-8 w-full justify-between text-xs"
|
||||
>
|
||||
{mc.tableName
|
||||
? tables.find((t) => t.tableName === mc.tableName)?.displayName || mc.tableName
|
||||
? tables.find((t) => t.table_name === mc.tableName)?.displayName || mc.tableName
|
||||
: "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
@@ -850,11 +850,11 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<CommandGroup>
|
||||
{tables.map((t) => (
|
||||
<CommandItem
|
||||
key={t.tableName}
|
||||
value={`${t.displayName || ""} ${t.tableName}`}
|
||||
key={t.table_name}
|
||||
value={`${t.display_name || ""} ${t.table_name}`}
|
||||
onSelect={() => {
|
||||
updateModal({
|
||||
tableName: t.tableName,
|
||||
tableName: t.table_name,
|
||||
displayColumns: [],
|
||||
searchColumns: [],
|
||||
displayField: "",
|
||||
@@ -868,13 +868,13 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-3 w-3",
|
||||
mc.tableName === t.tableName ? "opacity-100" : "opacity-0"
|
||||
mc.tableName === t.table_name ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{t.displayName || t.tableName}</span>
|
||||
{t.displayName && t.displayName !== t.tableName && (
|
||||
<span className="text-[9px] text-muted-foreground">{t.tableName}</span>
|
||||
<span className="font-medium">{t.display_name || t.table_name}</span>
|
||||
{t.display_name && t.display_name !== t.table_name && (
|
||||
<span className="text-[9px] text-muted-foreground">{t.table_name}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
@@ -900,15 +900,15 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
) : (
|
||||
<div className="max-h-32 space-y-1 overflow-y-auto rounded border p-2">
|
||||
{columns.map((col) => (
|
||||
<div key={col.columnName} className="flex items-center gap-2">
|
||||
<div key={col.column_name} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`disp_${col.columnName}`}
|
||||
checked={mc.displayColumns?.includes(col.columnName) ?? false}
|
||||
onCheckedChange={() => toggleArrayItem("displayColumns", col.columnName)}
|
||||
id={`disp_${col.column_name}`}
|
||||
checked={mc.displayColumns?.includes(col.column_name) ?? false}
|
||||
onCheckedChange={() => toggleArrayItem("displayColumns", col.column_name)}
|
||||
/>
|
||||
<Label htmlFor={`disp_${col.columnName}`} className="text-[10px]">
|
||||
{col.displayName || col.columnName}
|
||||
<span className="ml-1 text-muted-foreground">({col.columnName})</span>
|
||||
<Label htmlFor={`disp_${col.column_name}`} className="text-[10px]">
|
||||
{col.display_name || col.column_name}
|
||||
<span className="ml-1 text-muted-foreground">({col.column_name})</span>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
@@ -922,7 +922,7 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<Label className="text-[10px]">컬럼 헤더 라벨</Label>
|
||||
<div className="space-y-1 rounded border p-2">
|
||||
{selectedDisplayCols.map((colName) => {
|
||||
const colInfo = columns.find((c) => c.columnName === colName);
|
||||
const colInfo = columns.find((c) => c.column_name === colName);
|
||||
const defaultLabel = colInfo?.displayName || colName;
|
||||
return (
|
||||
<div key={colName} className="flex items-center gap-2">
|
||||
@@ -950,15 +950,15 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<Label className="text-[10px]">검색 대상 컬럼</Label>
|
||||
<div className="max-h-24 space-y-1 overflow-y-auto rounded border p-2">
|
||||
{columns.map((col) => (
|
||||
<div key={col.columnName} className="flex items-center gap-2">
|
||||
<div key={col.column_name} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`search_${col.columnName}`}
|
||||
checked={mc.searchColumns?.includes(col.columnName) ?? false}
|
||||
onCheckedChange={() => toggleArrayItem("searchColumns", col.columnName)}
|
||||
id={`search_${col.column_name}`}
|
||||
checked={mc.searchColumns?.includes(col.column_name) ?? false}
|
||||
onCheckedChange={() => toggleArrayItem("searchColumns", col.column_name)}
|
||||
/>
|
||||
<Label htmlFor={`search_${col.columnName}`} className="text-[10px]">
|
||||
{col.displayName || col.columnName}
|
||||
<span className="ml-1 text-muted-foreground">({col.columnName})</span>
|
||||
<Label htmlFor={`search_${col.column_name}`} className="text-[10px]">
|
||||
{col.display_name || col.column_name}
|
||||
<span className="ml-1 text-muted-foreground">({col.column_name})</span>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
@@ -1034,8 +1034,8 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__" className="text-xs text-muted-foreground">선택 안 함</SelectItem>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||
{col.displayName || col.columnName} ({col.columnName})
|
||||
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||
{col.display_name || col.column_name} ({col.column_name})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -1058,8 +1058,8 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__" className="text-xs text-muted-foreground">선택 안 함</SelectItem>
|
||||
{columns.map((col) => (
|
||||
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||
{col.displayName || col.columnName} ({col.columnName})
|
||||
<SelectItem key={col.column_name} value={col.column_name} className="text-xs">
|
||||
{col.display_name || col.column_name} ({col.column_name})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import { optimizedButtonDataflowService } from "../optimizedButtonDataflowService";
|
||||
import { dataflowConfigCache } from "../dataflowCache";
|
||||
import { dataflowJobQueue } from "../dataflowJobQueue";
|
||||
import { ButtonActionType, ButtonTypeConfig } from "@/types/screen";
|
||||
import { ButtonActionType, ExtendedButtonTypeConfig } from "@/types/screen";
|
||||
|
||||
// Mock API client
|
||||
jest.mock("@/lib/api/client", () => ({
|
||||
@@ -22,6 +22,17 @@ jest.mock("@/lib/api/client", () => ({
|
||||
}));
|
||||
|
||||
describe("🔥 Button Dataflow Performance Tests", () => {
|
||||
const mockButtonConfig: ExtendedButtonTypeConfig = {
|
||||
actionType: "save" as ButtonActionType,
|
||||
enableDataflowControl: true,
|
||||
dataflowTiming: "after",
|
||||
dataflowConfig: {
|
||||
controlMode: "relationship",
|
||||
selectedDiagramId: 1,
|
||||
selectedRelationshipId: 1,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// 캐시 초기화
|
||||
dataflowConfigCache.clearAllCache();
|
||||
@@ -88,17 +99,6 @@ describe("🔥 Button Dataflow Performance Tests", () => {
|
||||
});
|
||||
|
||||
describe("⚡ Button Execution Performance", () => {
|
||||
const mockButtonConfig: ButtonTypeConfig = {
|
||||
actionType: "save" as ButtonActionType,
|
||||
enableDataflowControl: true,
|
||||
dataflowTiming: "after",
|
||||
dataflowConfig: {
|
||||
controlMode: "simple",
|
||||
selectedDiagramId: 1,
|
||||
selectedRelationshipId: "rel-123",
|
||||
},
|
||||
};
|
||||
|
||||
it("should execute button action in under 200ms", async () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
@@ -145,10 +145,8 @@ describe("🔥 Button Dataflow Performance Tests", () => {
|
||||
...mockButtonConfig,
|
||||
dataflowTiming: "before" as const,
|
||||
dataflowConfig: {
|
||||
controlMode: "advanced" as const,
|
||||
controlMode: "relationship" as const,
|
||||
directControl: {
|
||||
sourceTable: "test_table",
|
||||
triggerType: "insert" as const,
|
||||
conditions: [
|
||||
{
|
||||
id: "cond1",
|
||||
|
||||
@@ -72,7 +72,7 @@ export class OptimizedButtonDataflowService {
|
||||
static async executeButtonWithDataflow(
|
||||
buttonId: string,
|
||||
actionType: ButtonActionType,
|
||||
buttonConfig: ExtendedExtendedButtonTypeConfig,
|
||||
buttonConfig: ExtendedButtonTypeConfig,
|
||||
contextData: Record<string, any>,
|
||||
companyCode: string,
|
||||
): Promise<OptimizedExecutionResult> {
|
||||
|
||||
@@ -334,7 +334,7 @@ export const useFlowEditorStore = create<FlowEditorState>((set, get) => {
|
||||
node.id === id
|
||||
? {
|
||||
...node,
|
||||
data: { ...node.data, ...data },
|
||||
data: { ...node.data, ...data } as FlowNode["data"],
|
||||
}
|
||||
: node,
|
||||
),
|
||||
@@ -740,7 +740,7 @@ function performFlowValidation(nodes: FlowNode[], edges: FlowEdge[]): Validation
|
||||
if (!hasIncoming && !hasOutgoing && !isSourceNode(node.type)) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `노드 "${node.data.displayName || node.id}"가 연결되어 있지 않습니다.`,
|
||||
message: `노드 "${(node.data as any).displayName || node.id}"가 연결되어 있지 않습니다.`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
@@ -752,7 +752,7 @@ function performFlowValidation(nodes: FlowNode[], edges: FlowEdge[]): Validation
|
||||
if (!hasInput) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `액션 노드 "${node.data.displayName || node.id}"에 입력 데이터가 없습니다.`,
|
||||
message: `액션 노드 "${(node.data as any).displayName || node.id}"에 입력 데이터가 없습니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
@@ -842,7 +842,7 @@ function detectCycles(nodes: FlowNode[], edges: FlowEdge[]): string[][] {
|
||||
const cycle = path.slice(cycleStart).concat(neighbor);
|
||||
const nodeNames = cycle.map((id) => {
|
||||
const node = nodes.find((n) => n.id === id);
|
||||
return node?.data.displayName || id;
|
||||
return (node?.data as any)?.displayName || id;
|
||||
});
|
||||
cycles.push(nodeNames);
|
||||
return true;
|
||||
@@ -868,47 +868,48 @@ function detectCycles(nodes: FlowNode[], edges: FlowEdge[]): string[][] {
|
||||
*/
|
||||
function validateNodeProperties(node: FlowNode): ValidationResult["errors"] {
|
||||
const errors: ValidationResult["errors"] = [];
|
||||
const data = node.data as any;
|
||||
|
||||
switch (node.type) {
|
||||
case "tableSource":
|
||||
if (!node.data.tableName || node.data.tableName.trim() === "") {
|
||||
if (!data.tableName || data.tableName.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `테이블 소스 노드 "${node.data.displayName || node.id}": 테이블명이 필요합니다.`,
|
||||
message: `테이블 소스 노드 "${data.displayName || node.id}": 테이블명이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "externalDBSource":
|
||||
if (!node.data.connectionName || node.data.connectionName.trim() === "") {
|
||||
if (!data.connectionName || data.connectionName.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `외부 DB 소스 노드 "${node.data.displayName || node.id}": 연결 이름이 필요합니다.`,
|
||||
message: `외부 DB 소스 노드 "${data.displayName || node.id}": 연결 이름이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (!node.data.tableName || node.data.tableName.trim() === "") {
|
||||
if (!data.tableName || data.tableName.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `외부 DB 소스 노드 "${node.data.displayName || node.id}": 테이블명이 필요합니다.`,
|
||||
message: `외부 DB 소스 노드 "${data.displayName || node.id}": 테이블명이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "restAPISource":
|
||||
if (!node.data.url || node.data.url.trim() === "") {
|
||||
if (!data.url || data.url.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `REST API 소스 노드 "${node.data.displayName || node.id}": URL이 필요합니다.`,
|
||||
message: `REST API 소스 노드 "${data.displayName || node.id}": URL이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (!node.data.method) {
|
||||
if (!data.method) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `REST API 소스 노드 "${node.data.displayName || node.id}": HTTP 메서드가 필요합니다.`,
|
||||
message: `REST API 소스 노드 "${data.displayName || node.id}": HTTP 메서드가 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
@@ -917,27 +918,27 @@ function validateNodeProperties(node: FlowNode): ValidationResult["errors"] {
|
||||
case "insertAction":
|
||||
case "updateAction":
|
||||
case "deleteAction":
|
||||
if (!node.data.targetTable || node.data.targetTable.trim() === "") {
|
||||
if (!data.targetTable || data.targetTable.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${node.data.displayName || node.id}": 타겟 테이블이 필요합니다.`,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${data.displayName || node.id}": 타겟 테이블이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (node.type === "insertAction" || node.type === "updateAction") {
|
||||
if (!node.data.fieldMappings || node.data.fieldMappings.length === 0) {
|
||||
if (!data.fieldMappings || data.fieldMappings.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${node.data.displayName || node.id}": 최소 하나의 필드 매핑이 필요합니다.`,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${data.displayName || node.id}": 최소 하나의 필드 매핑이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (node.type === "updateAction" || node.type === "deleteAction") {
|
||||
if (!node.data.whereConditions || node.data.whereConditions.length === 0) {
|
||||
if (!data.whereConditions || data.whereConditions.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${node.data.displayName || node.id}": WHERE 조건이 필요합니다.`,
|
||||
message: `${getActionTypeName(node.type)} 노드 "${data.displayName || node.id}": WHERE 조건이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
@@ -945,64 +946,54 @@ function validateNodeProperties(node: FlowNode): ValidationResult["errors"] {
|
||||
break;
|
||||
|
||||
case "upsertAction":
|
||||
if (!node.data.targetTable || node.data.targetTable.trim() === "") {
|
||||
if (!data.targetTable || data.targetTable.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `UPSERT 액션 노드 "${node.data.displayName || node.id}": 타겟 테이블이 필요합니다.`,
|
||||
message: `UPSERT 액션 노드 "${data.displayName || node.id}": 타겟 테이블이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (!node.data.conflictKeys || node.data.conflictKeys.length === 0) {
|
||||
if (!data.conflictKeys || data.conflictKeys.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `UPSERT 액션 노드 "${node.data.displayName || node.id}": 충돌 키(ON CONFLICT)가 필요합니다.`,
|
||||
message: `UPSERT 액션 노드 "${data.displayName || node.id}": 충돌 키(ON CONFLICT)가 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (!node.data.fieldMappings || node.data.fieldMappings.length === 0) {
|
||||
if (!data.fieldMappings || data.fieldMappings.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `UPSERT 액션 노드 "${node.data.displayName || node.id}": 최소 하나의 필드 매핑이 필요합니다.`,
|
||||
message: `UPSERT 액션 노드 "${data.displayName || node.id}": 최소 하나의 필드 매핑이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "condition":
|
||||
if (!node.data.conditions || node.data.conditions.length === 0) {
|
||||
if (!data.conditions || data.conditions.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `조건 노드 "${node.data.displayName || node.id}": 최소 하나의 조건이 필요합니다.`,
|
||||
message: `조건 노드 "${data.displayName || node.id}": 최소 하나의 조건이 필요합니다.`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "fieldMapping":
|
||||
if (!node.data.mappings || node.data.mappings.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `필드 매핑 노드 "${node.data.displayName || node.id}": 최소 하나의 매핑이 필요합니다.`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "dataTransform":
|
||||
if (!node.data.transformations || node.data.transformations.length === 0) {
|
||||
if (!data.transformations || data.transformations.length === 0) {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `데이터 변환 노드 "${node.data.displayName || node.id}": 최소 하나의 변환 규칙이 필요합니다.`,
|
||||
message: `데이터 변환 노드 "${data.displayName || node.id}": 최소 하나의 변환 규칙이 필요합니다.`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "log":
|
||||
if (!node.data.message || node.data.message.trim() === "") {
|
||||
if (!data.message || data.message.trim() === "") {
|
||||
errors.push({
|
||||
nodeId: node.id,
|
||||
message: `로그 노드 "${node.data.displayName || node.id}": 로그 메시지가 필요합니다.`,
|
||||
message: `로그 노드 "${data.displayName || node.id}": 로그 메시지가 필요합니다.`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,6 +97,9 @@ export interface ButtonActionConfig {
|
||||
dataSourceId?: string; // modalDataStore에서 데이터를 가져올 ID
|
||||
fieldMappings?: Array<{ sourceField: string; targetField: string }>; // 필드 매핑
|
||||
|
||||
// 버튼 라벨
|
||||
label?: string;
|
||||
|
||||
// 확인 메시지
|
||||
confirmMessage?: string;
|
||||
successMessage?: string;
|
||||
@@ -1194,7 +1197,7 @@ export class ButtonActionExecutor {
|
||||
if (Object.keys(rawSplitPanelData).length > 0) {
|
||||
}
|
||||
|
||||
const dataWithUserInfo = {
|
||||
const dataWithUserInfo: Record<string, any> = {
|
||||
...cleanedSplitPanelData, // 정리된 분할 패널 부모 데이터 먼저 적용
|
||||
...formData, // 폼 데이터가 우선 (덮어쓰기 가능)
|
||||
writer: formData.writer || writerValue, // ✅ 입력값 우선, 없으면 userId
|
||||
@@ -1741,7 +1744,7 @@ export class ButtonActionExecutor {
|
||||
if (!saveResult.success) {
|
||||
const errorMsg =
|
||||
saveResult.message ||
|
||||
saveResult.error?.message ||
|
||||
(saveResult as any).error?.message ||
|
||||
"저장에 실패했습니다.";
|
||||
toast.error(errorMsg);
|
||||
return false;
|
||||
@@ -1865,7 +1868,7 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
|
||||
// 메인 폼 데이터 구성 (사용자 정보 포함)
|
||||
const mainFormData = {
|
||||
const mainFormData: Record<string, any> = {
|
||||
...formData,
|
||||
writer: formData.writer || context.userId,
|
||||
created_by: context.userId,
|
||||
@@ -2105,7 +2108,7 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
|
||||
const existingResponse = await DynamicFormApi.getTableData(tableName, {
|
||||
search: searchParams,
|
||||
search: searchParams as any,
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
});
|
||||
@@ -2187,7 +2190,7 @@ export class ButtonActionExecutor {
|
||||
successCount++;
|
||||
} else {
|
||||
errorCount++;
|
||||
const errorMsg = result.message || result.error || "알 수 없는 오류";
|
||||
const errorMsg = result.message || (result as any).error || "알 수 없는 오류";
|
||||
errors.push(errorMsg);
|
||||
console.error(`❌ [handleRackStructureBatchSave] 저장 실패 (${i + 1}):`, errorMsg);
|
||||
}
|
||||
@@ -2456,7 +2459,7 @@ export class ButtonActionExecutor {
|
||||
const existingMainId = mainRecordIdFromParent || formData.id;
|
||||
const isMainUpdate = existingMainId !== undefined && existingMainId !== null && existingMainId !== "";
|
||||
|
||||
const mainRowToSave = { ...commonFieldsData, ...userInfo };
|
||||
const mainRowToSave: Record<string, any> = { ...commonFieldsData, ...userInfo };
|
||||
|
||||
// 메타데이터 제거
|
||||
Object.keys(mainRowToSave).forEach((key) => {
|
||||
@@ -3057,7 +3060,7 @@ export class ButtonActionExecutor {
|
||||
console.log("다중 데이터 삭제:", { tableName, screenId, id: deleteId });
|
||||
|
||||
// screenId 전달하여 제어관리 실행 가능하도록 함
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName, screenId);
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deleteId, tableName, screenId || "");
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(`ID ${deleteId} 삭제 실패: ${deleteResult.message}`);
|
||||
}
|
||||
@@ -3392,7 +3395,7 @@ export class ButtonActionExecutor {
|
||||
const matches = finalTitle.match(/\{([^}]+)\}/g);
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
matches.forEach((match: string) => {
|
||||
const path = match.slice(1, -1);
|
||||
const [tableName, columnName] = path.split(".");
|
||||
if (tableName && columnName) {
|
||||
@@ -3418,7 +3421,7 @@ export class ButtonActionExecutor {
|
||||
const isPassDataMode = passSelectedData && selectedData.length > 0;
|
||||
|
||||
// 🔧 isEditMode 옵션이 명시적으로 true인 경우에만 수정 모드로 처리
|
||||
const useAsEditData = config.isEditMode === true;
|
||||
const useAsEditData = (config as any).isEditMode === true;
|
||||
|
||||
const modalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
@@ -4315,7 +4318,7 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
} else if (config.dataflowConfig?.controlMode === "relationship" && config.dataflowConfig?.relationshipConfig) {
|
||||
// 🔥 table-selection 모드일 때 선택된 행 데이터를 formData에 병합
|
||||
let mergedFormData = { ...context.formData } || {};
|
||||
let mergedFormData = { ...context.formData };
|
||||
|
||||
if (
|
||||
controlDataSource === "table-selection" &&
|
||||
@@ -4334,7 +4337,7 @@ export class ButtonActionExecutor {
|
||||
enableDataflowControl: true, // 관계 기반 제어가 설정된 경우 활성화
|
||||
};
|
||||
|
||||
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(buttonConfig, mergedFormData, {
|
||||
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(buttonConfig as any, mergedFormData, {
|
||||
buttonId: context.buttonId || "unknown",
|
||||
screenId: context.screenId || "unknown",
|
||||
userId: context.userId || "unknown",
|
||||
@@ -4356,7 +4359,7 @@ export class ButtonActionExecutor {
|
||||
console.error("❌ 관계 실행 실패:", executionResult);
|
||||
showErrorToast(
|
||||
config.errorMessage || "관계 실행에 실패했습니다",
|
||||
executionResult.message || executionResult.error,
|
||||
(executionResult as any).message || (executionResult as any).error,
|
||||
{ guidance: "관계 설정과 데이터를 확인해 주세요." }
|
||||
);
|
||||
return false;
|
||||
@@ -4597,7 +4600,7 @@ export class ButtonActionExecutor {
|
||||
};
|
||||
|
||||
const executionResult = await ImprovedButtonActionExecutor.executeButtonAction(
|
||||
buttonConfig,
|
||||
buttonConfig as any,
|
||||
context.formData || {},
|
||||
{
|
||||
buttonId: context.buttonId || "unknown",
|
||||
@@ -4613,7 +4616,7 @@ export class ButtonActionExecutor {
|
||||
// 성공 토스트는 save 액션에서 이미 표시했으므로 추가로 표시하지 않음
|
||||
} else {
|
||||
console.error("❌ 저장 후 제어 실행 실패:", executionResult);
|
||||
showErrorToast("저장은 완료되었으나 연결된 제어 실행에 실패했습니다", executionResult.message || executionResult.error, {
|
||||
showErrorToast("저장은 완료되었으나 연결된 제어 실행에 실패했습니다", (executionResult as any).message || (executionResult as any).error, {
|
||||
guidance: "제어 관계 설정을 확인해 주세요. 데이터는 정상 저장되었습니다.",
|
||||
});
|
||||
}
|
||||
@@ -5106,7 +5109,7 @@ export class ButtonActionExecutor {
|
||||
// 헤더와 컬럼 매핑
|
||||
columnLabels = {};
|
||||
downloadResponse.data.columns.forEach((col: string, index: number) => {
|
||||
columnLabels![col] = downloadResponse.data.headers[index] || col;
|
||||
columnLabels![col] = downloadResponse.data!.headers[index] || col;
|
||||
});
|
||||
} else {
|
||||
showErrorToast("마스터-디테일 데이터 조회에 실패했습니다", null, {
|
||||
@@ -5145,7 +5148,7 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
|
||||
// 파일명 생성
|
||||
let defaultFileName = relationResponse.data.masterTable || "데이터";
|
||||
let defaultFileName = (relationResponse.data as any).master_table || "데이터";
|
||||
if (typeof window !== "undefined") {
|
||||
const menuName = localStorage.getItem("currentMenuName");
|
||||
if (menuName) defaultFileName = menuName;
|
||||
@@ -5425,9 +5428,9 @@ export class ButtonActionExecutor {
|
||||
|
||||
const categoryColumnsResponse = await getCategoryColumns(context.tableName);
|
||||
|
||||
if (categoryColumnsResponse.success && categoryColumnsResponse.data) {
|
||||
if (categoryColumnsResponse.success && "data" in categoryColumnsResponse && categoryColumnsResponse.data) {
|
||||
// 백엔드에서 정의된 카테고리 컬럼들
|
||||
categoryColumns = categoryColumnsResponse.data
|
||||
categoryColumns = (categoryColumnsResponse as any).data
|
||||
.map((col: any) => col.column_name || col.name)
|
||||
.filter(Boolean); // undefined 제거
|
||||
|
||||
@@ -5436,10 +5439,10 @@ export class ButtonActionExecutor {
|
||||
try {
|
||||
const valuesResponse = await getCategoryValues(context.tableName, columnName, false);
|
||||
|
||||
if (valuesResponse.success && valuesResponse.data) {
|
||||
if (valuesResponse.success && "data" in valuesResponse && valuesResponse.data) {
|
||||
// valueCode → valueLabel 매핑
|
||||
categoryMap[columnName] = {};
|
||||
valuesResponse.data.forEach((catValue: any) => {
|
||||
(valuesResponse as any).data.forEach((catValue: any) => {
|
||||
const code = catValue.value_code || catValue.category_value_id;
|
||||
const label = catValue.value_label || catValue.label || code;
|
||||
if (code) {
|
||||
@@ -5548,20 +5551,20 @@ export class ButtonActionExecutor {
|
||||
// 업로드 후 제어: excelAfterUploadFlows를 우선 사용 (통합된 설정)
|
||||
// masterDetailExcel.afterUploadFlows는 레거시 호환성을 위해 fallback으로만 사용
|
||||
const afterUploadFlows =
|
||||
config.excelAfterUploadFlows?.length > 0
|
||||
(config.excelAfterUploadFlows?.length ?? 0) > 0
|
||||
? config.excelAfterUploadFlows
|
||||
: config.masterDetailExcel?.afterUploadFlows;
|
||||
: (config as any).masterDetailExcel?.afterUploadFlows;
|
||||
|
||||
// masterDetailExcel 설정이 명시적으로 있을 때만 간단 모드 (디테일만 업로드)
|
||||
// 설정이 없으면 기본 모드 (마스터+디테일 둘 다 업로드)
|
||||
if (config.masterDetailExcel) {
|
||||
if ((config as any).masterDetailExcel) {
|
||||
masterDetailExcelConfig = {
|
||||
...config.masterDetailExcel,
|
||||
...(config as any).masterDetailExcel,
|
||||
// 분할 패널에서 감지한 테이블 정보로 덮어쓰기
|
||||
masterTable: relationResponse.data.masterTable,
|
||||
detailTable: relationResponse.data.detailTable,
|
||||
masterKeyColumn: relationResponse.data.masterKeyColumn,
|
||||
detailFkColumn: relationResponse.data.detailFkColumn,
|
||||
masterTable: relationResponse.data.master_table,
|
||||
detailTable: relationResponse.data.detail_table,
|
||||
masterKeyColumn: relationResponse.data.master_key_column,
|
||||
detailFkColumn: relationResponse.data.detail_fk_column,
|
||||
// 채번은 ExcelUploadModal에서 마스터 테이블 기반 자동 감지
|
||||
// 업로드 후 제어 설정 (통합: excelAfterUploadFlows 우선)
|
||||
afterUploadFlows,
|
||||
@@ -5729,10 +5732,7 @@ export class ButtonActionExecutor {
|
||||
onScanSuccess: (barcode: string) => {
|
||||
// 대상 필드에 값 입력
|
||||
if (config.barcodeTargetField && context.onFormDataChange) {
|
||||
context.onFormDataChange({
|
||||
...context.formData,
|
||||
[config.barcodeTargetField]: barcode,
|
||||
});
|
||||
context.onFormDataChange(config.barcodeTargetField, barcode);
|
||||
}
|
||||
|
||||
toast.success(`바코드 스캔 완료: ${barcode}`);
|
||||
@@ -5950,6 +5950,7 @@ export class ButtonActionExecutor {
|
||||
private static currentTripId: string | null = null;
|
||||
private static trackingContext: ButtonActionContext | null = null;
|
||||
private static trackingConfig: ButtonActionConfig | null = null;
|
||||
private static trackingUserId: string | null = null;
|
||||
|
||||
/**
|
||||
* 연속 위치 추적 시작
|
||||
@@ -6089,7 +6090,7 @@ export class ButtonActionExecutor {
|
||||
console.log("⚠️ [handleTrackingStop] trackingIntervalId 없음 - DB 상태 기반 종료 진행");
|
||||
} else {
|
||||
// 타이머 정리 (추적 중인 경우에만)
|
||||
clearInterval(this.trackingIntervalId);
|
||||
clearInterval(this.trackingIntervalId!);
|
||||
this.trackingIntervalId = null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user