Merge pull request 'lhj' (#284) from lhj into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/284
This commit is contained in:
commit
e8bc770439
|
|
@ -390,9 +390,11 @@ export interface RowDetailPopupConfig {
|
||||||
// 추가 데이터 조회 설정
|
// 추가 데이터 조회 설정
|
||||||
additionalQuery?: {
|
additionalQuery?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
queryMode?: "table" | "custom"; // 조회 모드: table(테이블 조회), custom(커스텀 쿼리)
|
||||||
tableName: string; // 조회할 테이블명 (예: vehicles)
|
tableName: string; // 조회할 테이블명 (예: vehicles)
|
||||||
matchColumn: string; // 매칭할 컬럼 (예: id)
|
matchColumn: string; // 매칭할 컬럼 (예: id)
|
||||||
sourceColumn?: string; // 클릭한 행에서 가져올 컬럼 (기본: matchColumn과 동일)
|
sourceColumn?: string; // 클릭한 행에서 가져올 컬럼 (기본: matchColumn과 동일)
|
||||||
|
customQuery?: string; // 커스텀 쿼리 ({id}, {vehicle_number} 등 파라미터 사용)
|
||||||
// 팝업에 표시할 컬럼 목록 (비어있으면 전체 표시)
|
// 팝업에 표시할 컬럼 목록 (비어있으면 전체 표시)
|
||||||
displayColumns?: DisplayColumnConfig[];
|
displayColumns?: DisplayColumnConfig[];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
|
||||||
checked={popupConfig.additionalQuery?.enabled || false}
|
checked={popupConfig.additionalQuery?.enabled || false}
|
||||||
onCheckedChange={(enabled) =>
|
onCheckedChange={(enabled) =>
|
||||||
updatePopupConfig({
|
updatePopupConfig({
|
||||||
additionalQuery: { ...popupConfig.additionalQuery, enabled, tableName: "", matchColumn: "" },
|
additionalQuery: { ...popupConfig.additionalQuery, enabled, queryMode: "table", tableName: "", matchColumn: "" },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
aria-label="추가 데이터 조회 활성화"
|
aria-label="추가 데이터 조회 활성화"
|
||||||
|
|
@ -167,116 +167,230 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
|
||||||
|
|
||||||
{popupConfig.additionalQuery?.enabled && (
|
{popupConfig.additionalQuery?.enabled && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{/* 조회 모드 선택 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">테이블명</Label>
|
<Label className="text-xs">조회 모드</Label>
|
||||||
<Input
|
<Select
|
||||||
value={popupConfig.additionalQuery?.tableName || ""}
|
value={popupConfig.additionalQuery?.queryMode || "table"}
|
||||||
onChange={(e) =>
|
onValueChange={(value: "table" | "custom") =>
|
||||||
updatePopupConfig({
|
updatePopupConfig({
|
||||||
additionalQuery: { ...popupConfig.additionalQuery!, tableName: e.target.value },
|
additionalQuery: { ...popupConfig.additionalQuery!, queryMode: value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
placeholder="vehicles"
|
>
|
||||||
className="mt-1 h-8 text-xs"
|
<SelectTrigger className="mt-1 h-8 text-xs">
|
||||||
/>
|
<SelectValue />
|
||||||
</div>
|
</SelectTrigger>
|
||||||
<div>
|
<SelectContent>
|
||||||
<Label className="text-xs">매칭 컬럼 (조회 테이블)</Label>
|
<SelectItem value="table">테이블 조회</SelectItem>
|
||||||
<Input
|
<SelectItem value="custom">커스텀 쿼리</SelectItem>
|
||||||
value={popupConfig.additionalQuery?.matchColumn || ""}
|
</SelectContent>
|
||||||
onChange={(e) =>
|
</Select>
|
||||||
updatePopupConfig({
|
|
||||||
additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="id"
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">소스 컬럼 (클릭한 행)</Label>
|
|
||||||
<Input
|
|
||||||
value={popupConfig.additionalQuery?.sourceColumn || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updatePopupConfig({
|
|
||||||
additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="비워두면 매칭 컬럼과 동일"
|
|
||||||
className="mt-1 h-8 text-xs"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 표시할 컬럼 선택 (다중 선택 + 라벨 편집) */}
|
{/* 테이블 조회 모드 */}
|
||||||
|
{(popupConfig.additionalQuery?.queryMode || "table") === "table" && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">테이블명</Label>
|
||||||
|
<Input
|
||||||
|
value={popupConfig.additionalQuery?.tableName || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, tableName: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="vehicles"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">매칭 컬럼 (조회 테이블)</Label>
|
||||||
|
<Input
|
||||||
|
value={popupConfig.additionalQuery?.matchColumn || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, matchColumn: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="id"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">소스 컬럼 (클릭한 행)</Label>
|
||||||
|
<Input
|
||||||
|
value={popupConfig.additionalQuery?.sourceColumn || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="비워두면 매칭 컬럼과 동일"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 커스텀 쿼리 모드 */}
|
||||||
|
{popupConfig.additionalQuery?.queryMode === "custom" && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">소스 컬럼 (클릭한 행)</Label>
|
||||||
|
<Input
|
||||||
|
value={popupConfig.additionalQuery?.sourceColumn || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, sourceColumn: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="id"
|
||||||
|
className="mt-1 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">쿼리에서 사용할 파라미터 컬럼</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">커스텀 쿼리</Label>
|
||||||
|
<textarea
|
||||||
|
value={popupConfig.additionalQuery?.customQuery || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, customQuery: e.target.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={`SELECT
|
||||||
|
v.vehicle_number AS "차량번호",
|
||||||
|
ROUND(SUM(ts.loaded_distance_km)::NUMERIC, 2) AS "운행거리"
|
||||||
|
FROM vehicles v
|
||||||
|
LEFT JOIN transport_statistics ts ON v.id = ts.vehicle_id
|
||||||
|
WHERE v.id = {id}
|
||||||
|
GROUP BY v.id;`}
|
||||||
|
className="mt-1 h-32 w-full rounded-md border border-input bg-background px-3 py-2 text-xs font-mono ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
/>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{"{id}"}, {"{vehicle_number}"} 등 클릭한 행의 컬럼값을 파라미터로 사용
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 표시할 컬럼 선택 - 테이블 모드와 커스텀 쿼리 모드 분기 */}
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs">표시할 컬럼 선택</Label>
|
<Label className="text-xs">표시할 컬럼 선택</Label>
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
{/* 테이블 모드: 기존 쿼리 결과에서 선택 */}
|
||||||
<Button variant="outline" className="mt-1 h-8 w-full justify-between text-xs">
|
{popupConfig.additionalQuery?.queryMode !== "custom" && (
|
||||||
<span className="truncate">
|
<>
|
||||||
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0
|
<Popover>
|
||||||
? `${popupConfig.additionalQuery?.displayColumns?.length}개 선택됨`
|
<PopoverTrigger asChild>
|
||||||
: "전체 표시 (클릭하여 선택)"}
|
<Button variant="outline" className="mt-1 h-8 w-full justify-between text-xs">
|
||||||
</span>
|
<span className="truncate">
|
||||||
<ChevronDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0
|
||||||
</Button>
|
? `${popupConfig.additionalQuery?.displayColumns?.length}개 선택됨`
|
||||||
</PopoverTrigger>
|
: "전체 표시 (클릭하여 선택)"}
|
||||||
<PopoverContent className="w-72 p-2" align="start">
|
</span>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<ChevronDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
<span className="text-xs font-medium">컬럼 선택</span>
|
</Button>
|
||||||
<Button
|
</PopoverTrigger>
|
||||||
variant="ghost"
|
<PopoverContent className="w-72 p-2" align="start">
|
||||||
size="sm"
|
<div className="mb-2 flex items-center justify-between">
|
||||||
className="h-6 text-xs"
|
<span className="text-xs font-medium">컬럼 선택</span>
|
||||||
onClick={() =>
|
<Button
|
||||||
updatePopupConfig({
|
variant="ghost"
|
||||||
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: [] },
|
size="sm"
|
||||||
})
|
className="h-6 text-xs"
|
||||||
}
|
onClick={() =>
|
||||||
>
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-48 space-y-1 overflow-y-auto">
|
|
||||||
{/* 쿼리 결과 컬럼 목록 */}
|
|
||||||
{queryResult?.columns.map((col) => {
|
|
||||||
const currentColumns = popupConfig.additionalQuery?.displayColumns || [];
|
|
||||||
const existingConfig = currentColumns.find((c) =>
|
|
||||||
typeof c === 'object' ? c.column === col : c === col
|
|
||||||
);
|
|
||||||
const isSelected = !!existingConfig;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={col}
|
|
||||||
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted"
|
|
||||||
onClick={() => {
|
|
||||||
const newColumns = isSelected
|
|
||||||
? currentColumns.filter((c) =>
|
|
||||||
typeof c === 'object' ? c.column !== col : c !== col
|
|
||||||
)
|
|
||||||
: [...currentColumns, { column: col, label: col } as DisplayColumnConfig];
|
|
||||||
updatePopupConfig({
|
updatePopupConfig({
|
||||||
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: [] },
|
||||||
});
|
})
|
||||||
}}
|
}
|
||||||
>
|
>
|
||||||
<Checkbox checked={isSelected} className="h-3 w-3" />
|
초기화
|
||||||
<span className="text-xs">{col}</span>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="max-h-48 space-y-1 overflow-y-auto">
|
||||||
})}
|
{/* 쿼리 결과 컬럼 목록 */}
|
||||||
{(!queryResult?.columns || queryResult.columns.length === 0) && (
|
{queryResult?.columns.map((col) => {
|
||||||
<p className="text-muted-foreground py-2 text-center text-xs">
|
const currentColumns = popupConfig.additionalQuery?.displayColumns || [];
|
||||||
쿼리를 먼저 실행해주세요
|
const existingConfig = currentColumns.find((c) =>
|
||||||
</p>
|
typeof c === 'object' ? c.column === col : c === col
|
||||||
|
);
|
||||||
|
const isSelected = !!existingConfig;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={col}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-muted"
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = isSelected
|
||||||
|
? currentColumns.filter((c) =>
|
||||||
|
typeof c === 'object' ? c.column !== col : c !== col
|
||||||
|
)
|
||||||
|
: [...currentColumns, { column: col, label: col } as DisplayColumnConfig];
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox checked={isSelected} className="h-3 w-3" />
|
||||||
|
<span className="text-xs">{col}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{(!queryResult?.columns || queryResult.columns.length === 0) && (
|
||||||
|
<p className="text-muted-foreground py-2 text-center text-xs">
|
||||||
|
쿼리를 먼저 실행해주세요
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">비워두면 모든 컬럼이 표시됩니다</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 커스텀 쿼리 모드: 직접 입력 방식 */}
|
||||||
|
{popupConfig.additionalQuery?.queryMode === "custom" && (
|
||||||
|
<>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
커스텀 쿼리의 결과 컬럼이 자동으로 표시됩니다.
|
||||||
|
쿼리에서 AS "라벨명" 형태로 alias를 지정하면 해당 라벨로 표시됩니다.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 gap-1 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || []), { column: "", label: "" }];
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-3 w-3" />
|
||||||
|
컬럼 추가 (선택사항)
|
||||||
|
</Button>
|
||||||
|
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() =>
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: [] },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
초기화
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</>
|
||||||
</Popover>
|
)}
|
||||||
<p className="text-muted-foreground mt-1 text-xs">비워두면 모든 컬럼이 표시됩니다</p>
|
|
||||||
|
|
||||||
{/* 선택된 컬럼 라벨 편집 */}
|
{/* 선택된 컬럼 라벨 편집 (테이블 모드) */}
|
||||||
{(popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
{popupConfig.additionalQuery?.queryMode !== "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
||||||
<div className="mt-3 space-y-2">
|
<div className="mt-3 space-y-2">
|
||||||
<Label className="text-xs">컬럼 라벨 설정</Label>
|
<Label className="text-xs">컬럼 라벨 설정</Label>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|
@ -321,6 +435,63 @@ export function ListWidgetSection({ queryResult, config, onConfigChange }: ListW
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 커스텀 쿼리 모드: 직접 입력 컬럼 편집 */}
|
||||||
|
{popupConfig.additionalQuery?.queryMode === "custom" && (popupConfig.additionalQuery?.displayColumns?.length || 0) > 0 && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<Label className="text-xs">표시할 컬럼 직접 입력</Label>
|
||||||
|
<p className="text-muted-foreground text-xs">커스텀 쿼리 결과의 컬럼명을 직접 입력하세요</p>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{popupConfig.additionalQuery?.displayColumns?.map((colConfig, index) => {
|
||||||
|
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
||||||
|
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
value={column}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
|
||||||
|
newColumns[index] = { column: e.target.value, label: label || e.target.value };
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="컬럼명 (쿼리 결과)"
|
||||||
|
className="h-7 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newColumns = [...(popupConfig.additionalQuery?.displayColumns || [])];
|
||||||
|
newColumns[index] = { column, label: e.target.value };
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="표시 라벨"
|
||||||
|
className="h-7 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
const newColumns = (popupConfig.additionalQuery?.displayColumns || []).filter(
|
||||||
|
(_, i) => i !== index
|
||||||
|
);
|
||||||
|
updatePopupConfig({
|
||||||
|
additionalQuery: { ...popupConfig.additionalQuery!, displayColumns: newColumns },
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -64,22 +64,35 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
|
|
||||||
// 추가 데이터 조회 설정이 있으면 실행
|
// 추가 데이터 조회 설정이 있으면 실행
|
||||||
const additionalQuery = config.rowDetailPopup?.additionalQuery;
|
const additionalQuery = config.rowDetailPopup?.additionalQuery;
|
||||||
if (additionalQuery?.enabled && additionalQuery.tableName && additionalQuery.matchColumn) {
|
if (additionalQuery?.enabled) {
|
||||||
const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
|
const queryMode = additionalQuery.queryMode || "table";
|
||||||
const matchValue = row[sourceColumn];
|
|
||||||
|
// 커스텀 쿼리 모드
|
||||||
if (matchValue !== undefined && matchValue !== null) {
|
if (queryMode === "custom" && additionalQuery.customQuery) {
|
||||||
setDetailPopupLoading(true);
|
setDetailPopupLoading(true);
|
||||||
try {
|
try {
|
||||||
const query = `
|
// 쿼리에서 {컬럼명} 형태의 파라미터를 실제 값으로 치환
|
||||||
SELECT *
|
let query = additionalQuery.customQuery;
|
||||||
FROM ${additionalQuery.tableName}
|
// console.log("🔍 [ListWidget] 커스텀 쿼리 파라미터 치환 시작");
|
||||||
WHERE ${additionalQuery.matchColumn} = '${matchValue}'
|
// console.log("🔍 [ListWidget] 클릭한 행 데이터:", row);
|
||||||
LIMIT 1;
|
// console.log("🔍 [ListWidget] 행 컬럼 목록:", Object.keys(row));
|
||||||
`;
|
|
||||||
|
Object.keys(row).forEach((key) => {
|
||||||
|
const value = row[key];
|
||||||
|
const placeholder = new RegExp(`\\{${key}\\}`, "g");
|
||||||
|
// SQL 인젝션 방지를 위해 값 이스케이프
|
||||||
|
const safeValue = typeof value === "string"
|
||||||
|
? value.replace(/'/g, "''")
|
||||||
|
: value;
|
||||||
|
query = query.replace(placeholder, String(safeValue ?? ""));
|
||||||
|
// console.log(`🔍 [ListWidget] 치환: {${key}} → ${safeValue}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log("🔍 [ListWidget] 최종 쿼리:", query);
|
||||||
|
|
||||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||||
const result = await dashboardApi.executeQuery(query);
|
const result = await dashboardApi.executeQuery(query);
|
||||||
|
// console.log("🔍 [ListWidget] 쿼리 결과:", result);
|
||||||
|
|
||||||
if (result.success && result.rows.length > 0) {
|
if (result.success && result.rows.length > 0) {
|
||||||
setAdditionalDetailData(result.rows[0]);
|
setAdditionalDetailData(result.rows[0]);
|
||||||
|
|
@ -87,12 +100,43 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
setAdditionalDetailData({});
|
setAdditionalDetailData({});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("추가 데이터 로드 실패:", error);
|
console.error("커스텀 쿼리 실행 실패:", error);
|
||||||
setAdditionalDetailData({});
|
setAdditionalDetailData({});
|
||||||
} finally {
|
} finally {
|
||||||
setDetailPopupLoading(false);
|
setDetailPopupLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 테이블 조회 모드
|
||||||
|
else if (queryMode === "table" && additionalQuery.tableName && additionalQuery.matchColumn) {
|
||||||
|
const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
|
||||||
|
const matchValue = row[sourceColumn];
|
||||||
|
|
||||||
|
if (matchValue !== undefined && matchValue !== null) {
|
||||||
|
setDetailPopupLoading(true);
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT *
|
||||||
|
FROM ${additionalQuery.tableName}
|
||||||
|
WHERE ${additionalQuery.matchColumn} = '${matchValue}'
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||||
|
const result = await dashboardApi.executeQuery(query);
|
||||||
|
|
||||||
|
if (result.success && result.rows.length > 0) {
|
||||||
|
setAdditionalDetailData(result.rows[0]);
|
||||||
|
} else {
|
||||||
|
setAdditionalDetailData({});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("추가 데이터 로드 실패:", error);
|
||||||
|
setAdditionalDetailData({});
|
||||||
|
} finally {
|
||||||
|
setDetailPopupLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[config.rowDetailPopup],
|
[config.rowDetailPopup],
|
||||||
|
|
@ -190,22 +234,34 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
const getDefaultFieldGroups = (row: Record<string, any>, additional: Record<string, any> | null): FieldGroup[] => {
|
const getDefaultFieldGroups = (row: Record<string, any>, additional: Record<string, any> | null): FieldGroup[] => {
|
||||||
const groups: FieldGroup[] = [];
|
const groups: FieldGroup[] = [];
|
||||||
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
|
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
|
||||||
|
const queryMode = config.rowDetailPopup?.additionalQuery?.queryMode || "table";
|
||||||
|
|
||||||
|
// 커스텀 쿼리 모드일 때는 additional 데이터를 우선 사용
|
||||||
|
// row와 additional을 병합하되, 커스텀 쿼리 결과(additional)가 우선
|
||||||
|
const mergedData = queryMode === "custom" && additional && Object.keys(additional).length > 0
|
||||||
|
? { ...row, ...additional } // additional이 row를 덮어씀
|
||||||
|
: row;
|
||||||
|
|
||||||
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
|
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
|
||||||
let basicFields: { column: string; label: string }[] = [];
|
let basicFields: { column: string; label: string }[] = [];
|
||||||
|
|
||||||
if (displayColumns && displayColumns.length > 0) {
|
if (displayColumns && displayColumns.length > 0) {
|
||||||
// DisplayColumnConfig 형식 지원
|
// DisplayColumnConfig 형식 지원
|
||||||
|
// 커스텀 쿼리 모드일 때는 mergedData에서 컬럼 확인
|
||||||
basicFields = displayColumns
|
basicFields = displayColumns
|
||||||
.map((colConfig) => {
|
.map((colConfig) => {
|
||||||
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
||||||
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
|
const label = typeof colConfig === 'object' ? colConfig.label : colConfig;
|
||||||
return { column, label };
|
return { column, label };
|
||||||
})
|
})
|
||||||
.filter((item) => item.column in row);
|
.filter((item) => item.column in mergedData);
|
||||||
} else {
|
} else {
|
||||||
// 전체 컬럼
|
// 전체 컬럼 - 커스텀 쿼리 모드일 때는 additional 컬럼만 표시
|
||||||
basicFields = Object.keys(row).map((key) => ({ column: key, label: key }));
|
if (queryMode === "custom" && additional && Object.keys(additional).length > 0) {
|
||||||
|
basicFields = Object.keys(additional).map((key) => ({ column: key, label: key }));
|
||||||
|
} else {
|
||||||
|
basicFields = Object.keys(row).map((key) => ({ column: key, label: key }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.push({
|
groups.push({
|
||||||
|
|
@ -220,8 +276,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) {
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가
|
// 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가 (테이블 모드일 때만)
|
||||||
if (additional && Object.keys(additional).length > 0) {
|
if (queryMode === "table" && additional && Object.keys(additional).length > 0) {
|
||||||
// 운행 정보
|
// 운행 정보
|
||||||
if (additional.last_trip_start || additional.last_trip_end) {
|
if (additional.last_trip_start || additional.last_trip_end) {
|
||||||
groups.push({
|
groups.push({
|
||||||
|
|
|
||||||
|
|
@ -96,22 +96,35 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
|
|
||||||
// 추가 데이터 조회 설정이 있으면 실행
|
// 추가 데이터 조회 설정이 있으면 실행
|
||||||
const additionalQuery = config.rowDetailPopup?.additionalQuery;
|
const additionalQuery = config.rowDetailPopup?.additionalQuery;
|
||||||
if (additionalQuery?.enabled && additionalQuery.tableName && additionalQuery.matchColumn) {
|
if (additionalQuery?.enabled) {
|
||||||
const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
|
const queryMode = additionalQuery.queryMode || "table";
|
||||||
const matchValue = row[sourceColumn];
|
|
||||||
|
// 커스텀 쿼리 모드
|
||||||
if (matchValue !== undefined && matchValue !== null) {
|
if (queryMode === "custom" && additionalQuery.customQuery) {
|
||||||
setDetailPopupLoading(true);
|
setDetailPopupLoading(true);
|
||||||
try {
|
try {
|
||||||
const query = `
|
// 쿼리에서 {컬럼명} 형태의 파라미터를 실제 값으로 치환
|
||||||
SELECT *
|
let query = additionalQuery.customQuery;
|
||||||
FROM ${additionalQuery.tableName}
|
// console.log("🔍 [ListTestWidget] 커스텀 쿼리 파라미터 치환 시작");
|
||||||
WHERE ${additionalQuery.matchColumn} = '${matchValue}'
|
// console.log("🔍 [ListTestWidget] 클릭한 행 데이터:", row);
|
||||||
LIMIT 1;
|
// console.log("🔍 [ListTestWidget] 행 컬럼 목록:", Object.keys(row));
|
||||||
`;
|
|
||||||
|
Object.keys(row).forEach((key) => {
|
||||||
|
const value = row[key];
|
||||||
|
const placeholder = new RegExp(`\\{${key}\\}`, "g");
|
||||||
|
// SQL 인젝션 방지를 위해 값 이스케이프
|
||||||
|
const safeValue = typeof value === "string"
|
||||||
|
? value.replace(/'/g, "''")
|
||||||
|
: value;
|
||||||
|
query = query.replace(placeholder, String(safeValue ?? ""));
|
||||||
|
// console.log(`🔍 [ListTestWidget] 치환: {${key}} → ${safeValue}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log("🔍 [ListTestWidget] 최종 쿼리:", query);
|
||||||
|
|
||||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||||
const result = await dashboardApi.executeQuery(query);
|
const result = await dashboardApi.executeQuery(query);
|
||||||
|
// console.log("🔍 [ListTestWidget] 쿼리 결과:", result);
|
||||||
|
|
||||||
if (result.success && result.rows.length > 0) {
|
if (result.success && result.rows.length > 0) {
|
||||||
setAdditionalDetailData(result.rows[0]);
|
setAdditionalDetailData(result.rows[0]);
|
||||||
|
|
@ -119,12 +132,43 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
setAdditionalDetailData({});
|
setAdditionalDetailData({});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("추가 데이터 로드 실패:", err);
|
console.error("커스텀 쿼리 실행 실패:", err);
|
||||||
setAdditionalDetailData({});
|
setAdditionalDetailData({});
|
||||||
} finally {
|
} finally {
|
||||||
setDetailPopupLoading(false);
|
setDetailPopupLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 테이블 조회 모드
|
||||||
|
else if (queryMode === "table" && additionalQuery.tableName && additionalQuery.matchColumn) {
|
||||||
|
const sourceColumn = additionalQuery.sourceColumn || additionalQuery.matchColumn;
|
||||||
|
const matchValue = row[sourceColumn];
|
||||||
|
|
||||||
|
if (matchValue !== undefined && matchValue !== null) {
|
||||||
|
setDetailPopupLoading(true);
|
||||||
|
try {
|
||||||
|
const query = `
|
||||||
|
SELECT *
|
||||||
|
FROM ${additionalQuery.tableName}
|
||||||
|
WHERE ${additionalQuery.matchColumn} = '${matchValue}'
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||||
|
const result = await dashboardApi.executeQuery(query);
|
||||||
|
|
||||||
|
if (result.success && result.rows.length > 0) {
|
||||||
|
setAdditionalDetailData(result.rows[0]);
|
||||||
|
} else {
|
||||||
|
setAdditionalDetailData({});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("추가 데이터 로드 실패:", err);
|
||||||
|
setAdditionalDetailData({});
|
||||||
|
} finally {
|
||||||
|
setDetailPopupLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[config.rowDetailPopup],
|
[config.rowDetailPopup],
|
||||||
|
|
@ -222,13 +266,21 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
const getDefaultFieldGroups = (row: Record<string, any>, additional: Record<string, any> | null): FieldGroup[] => {
|
const getDefaultFieldGroups = (row: Record<string, any>, additional: Record<string, any> | null): FieldGroup[] => {
|
||||||
const groups: FieldGroup[] = [];
|
const groups: FieldGroup[] = [];
|
||||||
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
|
const displayColumns = config.rowDetailPopup?.additionalQuery?.displayColumns;
|
||||||
|
const queryMode = config.rowDetailPopup?.additionalQuery?.queryMode || "table";
|
||||||
|
|
||||||
|
// 커스텀 쿼리 모드일 때는 additional 데이터를 우선 사용
|
||||||
|
// row와 additional을 병합하되, 커스텀 쿼리 결과(additional)가 우선
|
||||||
|
const mergedData = queryMode === "custom" && additional && Object.keys(additional).length > 0
|
||||||
|
? { ...row, ...additional } // additional이 row를 덮어씀
|
||||||
|
: row;
|
||||||
|
|
||||||
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
|
// 기본 정보 그룹 - displayColumns가 있으면 해당 컬럼만, 없으면 전체
|
||||||
const allKeys = Object.keys(row).filter((key) => !key.startsWith("_")); // _source 등 내부 필드 제외
|
const allKeys = Object.keys(mergedData).filter((key) => !key.startsWith("_")); // _source 등 내부 필드 제외
|
||||||
let basicFields: { column: string; label: string }[] = [];
|
let basicFields: { column: string; label: string }[] = [];
|
||||||
|
|
||||||
if (displayColumns && displayColumns.length > 0) {
|
if (displayColumns && displayColumns.length > 0) {
|
||||||
// DisplayColumnConfig 형식 지원
|
// DisplayColumnConfig 형식 지원
|
||||||
|
// 커스텀 쿼리 모드일 때는 mergedData에서 컬럼 확인
|
||||||
basicFields = displayColumns
|
basicFields = displayColumns
|
||||||
.map((colConfig) => {
|
.map((colConfig) => {
|
||||||
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
const column = typeof colConfig === 'object' ? colConfig.column : colConfig;
|
||||||
|
|
@ -237,8 +289,14 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
})
|
})
|
||||||
.filter((item) => allKeys.includes(item.column));
|
.filter((item) => allKeys.includes(item.column));
|
||||||
} else {
|
} else {
|
||||||
// 전체 컬럼
|
// 전체 컬럼 - 커스텀 쿼리 모드일 때는 additional 컬럼만 표시
|
||||||
basicFields = allKeys.map((key) => ({ column: key, label: key }));
|
if (queryMode === "custom" && additional && Object.keys(additional).length > 0) {
|
||||||
|
basicFields = Object.keys(additional)
|
||||||
|
.filter((key) => !key.startsWith("_"))
|
||||||
|
.map((key) => ({ column: key, label: key }));
|
||||||
|
} else {
|
||||||
|
basicFields = allKeys.map((key) => ({ column: key, label: key }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
groups.push({
|
groups.push({
|
||||||
|
|
@ -253,8 +311,8 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가
|
// 추가 데이터가 있고 vehicles 테이블인 경우 운행/공차 정보 추가 (테이블 모드일 때만)
|
||||||
if (additional && Object.keys(additional).length > 0) {
|
if (queryMode === "table" && additional && Object.keys(additional).length > 0) {
|
||||||
// 운행 정보
|
// 운행 정보
|
||||||
if (additional.last_trip_start || additional.last_trip_end) {
|
if (additional.last_trip_start || additional.last_trip_end) {
|
||||||
groups.push({
|
groups.push({
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
setTripInfoLoading(identifier);
|
setTripInfoLoading(identifier);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// user_id 또는 vehicle_number로 조회
|
// user_id 또는 vehicle_number로 조회 (시간은 KST로 변환)
|
||||||
const query = `SELECT
|
const query = `SELECT
|
||||||
id, vehicle_number, user_id,
|
id, vehicle_number, user_id,
|
||||||
last_trip_start, last_trip_end, last_trip_distance, last_trip_time,
|
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||||
last_empty_start, last_empty_end, last_empty_distance, last_empty_time,
|
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||||
|
last_trip_distance, last_trip_time,
|
||||||
|
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||||
|
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||||
|
last_empty_distance, last_empty_time,
|
||||||
departure, arrival, status
|
departure, arrival, status
|
||||||
FROM vehicles
|
FROM vehicles
|
||||||
WHERE user_id = '${identifier}'
|
WHERE user_id = '${identifier}'
|
||||||
|
|
@ -277,12 +281,16 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
if (identifiers.length === 0) return;
|
if (identifiers.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 모든 마커의 운행/공차 정보를 한 번에 조회
|
// 모든 마커의 운행/공차 정보를 한 번에 조회 (시간은 KST로 변환)
|
||||||
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
const placeholders = identifiers.map((_, i) => `$${i + 1}`).join(", ");
|
||||||
const query = `SELECT
|
const query = `SELECT
|
||||||
id, vehicle_number, user_id,
|
id, vehicle_number, user_id,
|
||||||
last_trip_start, last_trip_end, last_trip_distance, last_trip_time,
|
(last_trip_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_start,
|
||||||
last_empty_start, last_empty_end, last_empty_distance, last_empty_time,
|
(last_trip_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_trip_end,
|
||||||
|
last_trip_distance, last_trip_time,
|
||||||
|
(last_empty_start AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_start,
|
||||||
|
(last_empty_end AT TIME ZONE 'Asia/Seoul')::timestamp as last_empty_end,
|
||||||
|
last_empty_distance, last_empty_time,
|
||||||
departure, arrival, status
|
departure, arrival, status
|
||||||
FROM vehicles
|
FROM vehicles
|
||||||
WHERE user_id IN (${identifiers.map(id => `'${id}'`).join(", ")})
|
WHERE user_id IN (${identifiers.map(id => `'${id}'`).join(", ")})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue