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