[agent-pipeline] pipe-20260309112447-f5iu round-3
This commit is contained in:
parent
e41df3b922
commit
159e7768bb
|
|
@ -3,10 +3,8 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Plus, Search, Edit, Trash2, TestTube, Filter } from "lucide-react";
|
import { Plus, Search, Edit, Trash2, TestTube } from "lucide-react";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,9 +27,16 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
||||||
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||||
|
|
||||||
|
// API 응답에 실제로 포함되는 필드를 위한 확장 타입
|
||||||
|
type ExternalCallConfigWithDate = ExternalCallConfig & {
|
||||||
|
created_date?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function ExternalCallConfigsPage() {
|
export default function ExternalCallConfigsPage() {
|
||||||
const [configs, setConfigs] = useState<ExternalCallConfig[]>([]);
|
const [configs, setConfigs] = useState<ExternalCallConfigWithDate[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [filter, setFilter] = useState<ExternalCallConfigFilter>({
|
const [filter, setFilter] = useState<ExternalCallConfigFilter>({
|
||||||
|
|
@ -50,15 +55,17 @@ export default function ExternalCallConfigsPage() {
|
||||||
const fetchConfigs = async () => {
|
const fetchConfigs = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await ExternalCallConfigAPI.getConfigs({
|
const filterWithSearch: Record<string, string | undefined> = { ...filter };
|
||||||
...filter,
|
const trimmed = searchQuery.trim();
|
||||||
search: searchQuery.trim() || undefined,
|
if (trimmed) {
|
||||||
});
|
filterWithSearch.search = trimmed;
|
||||||
|
}
|
||||||
|
const response = await ExternalCallConfigAPI.getConfigs(filterWithSearch as ExternalCallConfigFilter);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setConfigs(response.data || []);
|
setConfigs((response.data || []) as ExternalCallConfigWithDate[]);
|
||||||
} else {
|
} else {
|
||||||
showErrorToast("외부 호출 설정 조회에 실패했습니다", response.message, {
|
showErrorToast("외부 호출 설정 조회에 실패했습니다", response.error, {
|
||||||
guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.",
|
guidance: "네트워크 연결을 확인하고 다시 시도해 주세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -72,9 +79,10 @@ export default function ExternalCallConfigsPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 초기 로드 및 필터/검색 변경 시 재조회
|
// 초기 로드 및 필터 변경 시 재조회
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfigs();
|
fetchConfigs();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [filter]);
|
}, [filter]);
|
||||||
|
|
||||||
// 검색 실행
|
// 검색 실행
|
||||||
|
|
@ -118,7 +126,7 @@ export default function ExternalCallConfigsPage() {
|
||||||
toast.success("외부 호출 설정이 삭제되었습니다.");
|
toast.success("외부 호출 설정이 삭제되었습니다.");
|
||||||
fetchConfigs();
|
fetchConfigs();
|
||||||
} else {
|
} else {
|
||||||
showErrorToast("외부 호출 설정 삭제에 실패했습니다", response.message, {
|
showErrorToast("외부 호출 설정 삭제에 실패했습니다", response.error, {
|
||||||
guidance: "잠시 후 다시 시도해 주세요.",
|
guidance: "잠시 후 다시 시도해 주세요.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -140,10 +148,10 @@ export default function ExternalCallConfigsPage() {
|
||||||
try {
|
try {
|
||||||
const response = await ExternalCallConfigAPI.testConfig(config.id);
|
const response = await ExternalCallConfigAPI.testConfig(config.id);
|
||||||
|
|
||||||
if (response.success && response.data?.success) {
|
if (response.success) {
|
||||||
toast.success(`테스트 성공: ${response.data.message}`);
|
toast.success(`테스트 성공: ${response.message || "정상"}`);
|
||||||
} else {
|
} else {
|
||||||
toast.error(`테스트 실패: ${response.data?.message || response.message}`);
|
toast.error(`테스트 실패: ${response.message || response.error || "알 수 없는 오류"}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("외부 호출 설정 테스트 오류:", error);
|
console.error("외부 호출 설정 테스트 오류:", error);
|
||||||
|
|
@ -171,244 +179,283 @@ export default function ExternalCallConfigsPage() {
|
||||||
return API_TYPE_OPTIONS.find((option) => option.value === apiType)?.label || apiType;
|
return API_TYPE_OPTIONS.find((option) => option.value === apiType)?.label || apiType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ResponsiveDataView 컬럼 정의
|
||||||
|
const columns: RDVColumn<ExternalCallConfigWithDate>[] = [
|
||||||
|
{
|
||||||
|
key: "config_name",
|
||||||
|
label: "설정명",
|
||||||
|
render: (_v, row) => <span className="font-medium">{row.config_name}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "call_type",
|
||||||
|
label: "호출 타입",
|
||||||
|
width: "120px",
|
||||||
|
render: (_v, row) => <Badge variant="outline">{getCallTypeLabel(row.call_type)}</Badge>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "api_type",
|
||||||
|
label: "API 타입",
|
||||||
|
width: "120px",
|
||||||
|
render: (_v, row) =>
|
||||||
|
row.api_type ? (
|
||||||
|
<Badge variant="secondary">{getApiTypeLabel(row.api_type)}</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "description",
|
||||||
|
label: "설명",
|
||||||
|
render: (_v, row) =>
|
||||||
|
row.description ? (
|
||||||
|
<span className="block max-w-xs truncate text-muted-foreground" title={row.description}>
|
||||||
|
{row.description}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "is_active",
|
||||||
|
label: "상태",
|
||||||
|
width: "80px",
|
||||||
|
render: (_v, row) => (
|
||||||
|
<Badge variant={row.is_active === "Y" ? "default" : "destructive"}>
|
||||||
|
{row.is_active === "Y" ? "활성" : "비활성"}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "created_date",
|
||||||
|
label: "생성일",
|
||||||
|
width: "120px",
|
||||||
|
render: (_v, row) =>
|
||||||
|
row.created_date ? new Date(row.created_date).toLocaleDateString() : "-",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 모바일 카드 필드 정의
|
||||||
|
const cardFields: RDVCardField<ExternalCallConfigWithDate>[] = [
|
||||||
|
{
|
||||||
|
label: "호출 타입",
|
||||||
|
render: (c) => <Badge variant="outline">{getCallTypeLabel(c.call_type)}</Badge>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "API 타입",
|
||||||
|
render: (c) =>
|
||||||
|
c.api_type ? (
|
||||||
|
<Badge variant="secondary">{getApiTypeLabel(c.api_type)}</Badge>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "설명",
|
||||||
|
render: (c) => (
|
||||||
|
<span className="max-w-[200px] truncate">{c.description || "-"}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "생성일",
|
||||||
|
render: (c) =>
|
||||||
|
c.created_date ? new Date(c.created_date).toLocaleDateString() : "-",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-background">
|
<div className="flex min-h-screen flex-col bg-background">
|
||||||
<div className="space-y-6 p-6">
|
<div className="space-y-6 p-4 sm:p-6">
|
||||||
{/* 페이지 헤더 */}
|
{/* 페이지 헤더 */}
|
||||||
<div className="space-y-2 border-b pb-4">
|
<div className="space-y-2 border-b pb-4">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">외부 호출 관리</h1>
|
<h1 className="text-3xl font-bold tracking-tight">외부 호출 관리</h1>
|
||||||
<p className="text-sm text-muted-foreground">Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.</p>
|
<p className="text-sm text-muted-foreground">Discord, Slack, 카카오톡 등 외부 호출 설정을 관리합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 검색 및 필터 영역 */}
|
{/* 검색 및 필터 영역 (반응형) */}
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
{/* 첫 번째 줄: 검색 + 추가 버튼 */}
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="relative w-full sm:w-[300px]">
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
<div className="w-full sm:w-[320px]">
|
<Input
|
||||||
<div className="relative">
|
placeholder="설정 이름 또는 설명으로 검색..."
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
value={searchQuery}
|
||||||
<Input
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="설정 이름 또는 설명으로 검색..."
|
onKeyPress={handleSearchKeyPress}
|
||||||
value={searchQuery}
|
className="h-10 pl-10 text-sm"
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
/>
|
||||||
onKeyPress={handleSearchKeyPress}
|
|
||||||
className="h-10 pl-10 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleSearch} variant="outline" className="h-10 gap-2 text-sm font-medium">
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
검색
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleAddConfig} className="h-10 gap-2 text-sm font-medium">
|
<Button onClick={handleSearch} variant="outline" className="h-10 gap-2 text-sm font-medium">
|
||||||
<Plus className="h-4 w-4" />
|
<Search className="h-4 w-4" />
|
||||||
새 외부 호출 추가
|
검색
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button onClick={handleAddConfig} className="h-10 gap-2 text-sm font-medium">
|
||||||
{/* 두 번째 줄: 필터 */}
|
<Plus className="h-4 w-4" />
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
새 외부 호출 추가
|
||||||
<Select
|
</Button>
|
||||||
value={filter.call_type || "all"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setFilter((prev) => ({
|
|
||||||
...prev,
|
|
||||||
call_type: value === "all" ? undefined : value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="호출 타입" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">전체</SelectItem>
|
|
||||||
{CALL_TYPE_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filter.api_type || "all"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setFilter((prev) => ({
|
|
||||||
...prev,
|
|
||||||
api_type: value === "all" ? undefined : value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="API 타입" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">전체</SelectItem>
|
|
||||||
{API_TYPE_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={filter.is_active || "Y"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setFilter((prev) => ({
|
|
||||||
...prev,
|
|
||||||
is_active: value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-10">
|
|
||||||
<SelectValue placeholder="상태" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 설정 목록 */}
|
{/* 필터 영역 */}
|
||||||
<div className="rounded-lg border bg-card shadow-sm">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||||
{loading ? (
|
<Select
|
||||||
// 로딩 상태
|
value={filter.call_type || "all"}
|
||||||
<div className="flex h-64 items-center justify-center">
|
onValueChange={(value) =>
|
||||||
<div className="text-sm text-muted-foreground">로딩 중...</div>
|
setFilter((prev) => ({
|
||||||
</div>
|
...prev,
|
||||||
) : configs.length === 0 ? (
|
call_type: value === "all" ? undefined : value,
|
||||||
// 빈 상태
|
}))
|
||||||
<div className="flex h-64 flex-col items-center justify-center">
|
}
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
>
|
||||||
<p className="text-sm text-muted-foreground">등록된 외부 호출 설정이 없습니다.</p>
|
<SelectTrigger className="h-10">
|
||||||
<p className="text-xs text-muted-foreground">새 외부 호출을 추가해보세요.</p>
|
<SelectValue placeholder="호출 타입" />
|
||||||
</div>
|
</SelectTrigger>
|
||||||
</div>
|
<SelectContent>
|
||||||
) : (
|
<SelectItem value="all">전체</SelectItem>
|
||||||
// 설정 테이블 목록
|
{CALL_TYPE_OPTIONS.map((option) => (
|
||||||
<Table>
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<TableHeader>
|
{option.label}
|
||||||
<TableRow className="border-b bg-muted/50 hover:bg-muted/50">
|
</SelectItem>
|
||||||
<TableHead className="h-12 text-sm font-semibold">설정명</TableHead>
|
))}
|
||||||
<TableHead className="h-12 text-sm font-semibold">호출 타입</TableHead>
|
</SelectContent>
|
||||||
<TableHead className="h-12 text-sm font-semibold">API 타입</TableHead>
|
</Select>
|
||||||
<TableHead className="h-12 text-sm font-semibold">설명</TableHead>
|
|
||||||
<TableHead className="h-12 text-sm font-semibold">상태</TableHead>
|
<Select
|
||||||
<TableHead className="h-12 text-sm font-semibold">생성일</TableHead>
|
value={filter.api_type || "all"}
|
||||||
<TableHead className="h-12 text-center text-sm font-semibold">작업</TableHead>
|
onValueChange={(value) =>
|
||||||
</TableRow>
|
setFilter((prev) => ({
|
||||||
</TableHeader>
|
...prev,
|
||||||
<TableBody>
|
api_type: value === "all" ? undefined : value,
|
||||||
{configs.map((config) => (
|
}))
|
||||||
<TableRow key={config.id} className="border-b transition-colors hover:bg-muted/50">
|
}
|
||||||
<TableCell className="h-16 text-sm font-medium">{config.config_name}</TableCell>
|
>
|
||||||
<TableCell className="h-16 text-sm">
|
<SelectTrigger className="h-10">
|
||||||
<Badge variant="outline">{getCallTypeLabel(config.call_type)}</Badge>
|
<SelectValue placeholder="API 타입" />
|
||||||
</TableCell>
|
</SelectTrigger>
|
||||||
<TableCell className="h-16 text-sm">
|
<SelectContent>
|
||||||
{config.api_type ? (
|
<SelectItem value="all">전체</SelectItem>
|
||||||
<Badge variant="secondary">{getApiTypeLabel(config.api_type)}</Badge>
|
{API_TYPE_OPTIONS.map((option) => (
|
||||||
) : (
|
<SelectItem key={option.value} value={option.value}>
|
||||||
<span className="text-muted-foreground">-</span>
|
{option.label}
|
||||||
)}
|
</SelectItem>
|
||||||
</TableCell>
|
))}
|
||||||
<TableCell className="h-16 text-sm">
|
</SelectContent>
|
||||||
<div className="max-w-xs">
|
</Select>
|
||||||
{config.description ? (
|
|
||||||
<span className="block truncate text-muted-foreground" title={config.description}>
|
<Select
|
||||||
{config.description}
|
value={filter.is_active || "Y"}
|
||||||
</span>
|
onValueChange={(value) =>
|
||||||
) : (
|
setFilter((prev) => ({
|
||||||
<span className="text-muted-foreground">-</span>
|
...prev,
|
||||||
)}
|
is_active: value,
|
||||||
</div>
|
}))
|
||||||
</TableCell>
|
}
|
||||||
<TableCell className="h-16 text-sm">
|
>
|
||||||
<Badge variant={config.is_active === "Y" ? "default" : "destructive"}>
|
<SelectTrigger className="h-10">
|
||||||
{config.is_active === "Y" ? "활성" : "비활성"}
|
<SelectValue placeholder="상태" />
|
||||||
</Badge>
|
</SelectTrigger>
|
||||||
</TableCell>
|
<SelectContent>
|
||||||
<TableCell className="h-16 text-sm text-muted-foreground">
|
{ACTIVE_STATUS_OPTIONS.map((option) => (
|
||||||
{config.created_date ? new Date(config.created_date).toLocaleDateString() : "-"}
|
<SelectItem key={option.value} value={option.value}>
|
||||||
</TableCell>
|
{option.label}
|
||||||
<TableCell className="h-16 text-sm">
|
</SelectItem>
|
||||||
<div className="flex justify-center gap-1">
|
))}
|
||||||
<Button
|
</SelectContent>
|
||||||
variant="ghost"
|
</Select>
|
||||||
size="icon"
|
</div>
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => handleTestConfig(config)}
|
{/* 설정 목록 (ResponsiveDataView) */}
|
||||||
title="테스트"
|
<ResponsiveDataView<ExternalCallConfigWithDate>
|
||||||
>
|
data={configs}
|
||||||
<TestTube className="h-4 w-4" />
|
columns={columns}
|
||||||
</Button>
|
keyExtractor={(c) => String(c.id || c.config_name)}
|
||||||
<Button
|
isLoading={loading}
|
||||||
variant="ghost"
|
emptyMessage="등록된 외부 호출 설정이 없습니다."
|
||||||
size="icon"
|
skeletonCount={5}
|
||||||
className="h-8 w-8"
|
cardTitle={(c) => c.config_name}
|
||||||
onClick={() => handleEditConfig(config)}
|
cardSubtitle={(c) => c.description || "설명 없음"}
|
||||||
title="편집"
|
cardHeaderRight={(c) => (
|
||||||
>
|
<Badge variant={c.is_active === "Y" ? "default" : "destructive"}>
|
||||||
<Edit className="h-4 w-4" />
|
{c.is_active === "Y" ? "활성" : "비활성"}
|
||||||
</Button>
|
</Badge>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
||||||
onClick={() => handleDeleteConfig(config)}
|
|
||||||
title="삭제"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
cardFields={cardFields}
|
||||||
|
renderActions={(c) => (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 flex-1 gap-2 text-sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleTestConfig(c);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TestTube className="h-4 w-4" />
|
||||||
|
테스트
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 flex-1 gap-2 text-sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEditConfig(c);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
편집
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive hover:bg-destructive/10 hover:text-destructive h-9 gap-2 text-sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteConfig(c);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
actionsWidth="200px"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 외부 호출 설정 모달 */}
|
{/* 외부 호출 설정 모달 */}
|
||||||
<ExternalCallConfigModal
|
<ExternalCallConfigModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={() => setIsModalOpen(false)}
|
onClose={() => setIsModalOpen(false)}
|
||||||
onSave={handleModalSave}
|
onSave={handleModalSave}
|
||||||
editingConfig={editingConfig}
|
editingConfig={editingConfig}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 삭제 확인 다이얼로그 */}
|
{/* 삭제 확인 다이얼로그 */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
<AlertDialogContent className="max-w-[95vw] sm:max-w-[500px]">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="text-base sm:text-lg">외부 호출 설정 삭제</AlertDialogTitle>
|
<AlertDialogTitle className="text-base sm:text-lg">외부 호출 설정 삭제</AlertDialogTitle>
|
||||||
<AlertDialogDescription className="text-xs sm:text-sm">
|
<AlertDialogDescription className="text-xs sm:text-sm">
|
||||||
"{configToDelete?.config_name}" 설정을 삭제하시겠습니까?
|
"{configToDelete?.config_name}" 설정을 삭제하시겠습니까?
|
||||||
<br />이 작업은 되돌릴 수 없습니다.
|
<br />이 작업은 되돌릴 수 없습니다.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter className="gap-2 sm:gap-0">
|
<AlertDialogFooter className="gap-2 sm:gap-0">
|
||||||
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
<AlertDialogCancel className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
||||||
취소
|
취소
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={confirmDeleteConfig}
|
onClick={confirmDeleteConfig}
|
||||||
className="h-8 flex-1 bg-destructive text-xs hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
|
className="h-8 flex-1 bg-destructive text-xs hover:bg-destructive/90 sm:h-10 sm:flex-none sm:text-sm"
|
||||||
>
|
>
|
||||||
삭제
|
삭제
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll to Top 버튼 */}
|
||||||
|
<ScrollToTop />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { formatErrorMessage } from "@/lib/utils/errorUtils";
|
import { formatErrorMessage } from "@/lib/utils/errorUtils";
|
||||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||||
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
import { ScrollToTop } from "@/components/common/ScrollToTop";
|
||||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
import { ExternalDbConnectionAPI, ExternalDbConnection } from "@/lib/api/externalDbConnection";
|
||||||
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection";
|
||||||
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView";
|
||||||
|
|
||||||
|
|
@ -56,9 +56,7 @@ export default function FlowManagementPage() {
|
||||||
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
||||||
// 데이터 소스 타입: "internal" (내부 DB), "external_db_숫자" (외부 DB), "restapi_숫자" (REST API)
|
// 데이터 소스 타입: "internal" (내부 DB), "external_db_숫자" (외부 DB), "restapi_숫자" (REST API)
|
||||||
const [selectedDbSource, setSelectedDbSource] = useState<string>("internal");
|
const [selectedDbSource, setSelectedDbSource] = useState<string>("internal");
|
||||||
const [externalConnections, setExternalConnections] = useState<
|
const [externalConnections, setExternalConnections] = useState<ExternalDbConnection[]>([]);
|
||||||
Array<{ id: number; connection_name: string; db_type: string }>
|
|
||||||
>([]);
|
|
||||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
||||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
||||||
|
|
||||||
|
|
@ -255,7 +253,7 @@ export default function FlowManagementPage() {
|
||||||
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
.map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) =>
|
||||||
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name,
|
||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter((v): v is string => Boolean(v));
|
||||||
setMultiDbTableLists(prev => ({ ...prev, [connectionId]: tableNames }));
|
setMultiDbTableLists(prev => ({ ...prev, [connectionId]: tableNames }));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -447,7 +445,7 @@ export default function FlowManagementPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Calling createFlowDefinition with:", requestData);
|
console.log("✅ Calling createFlowDefinition with:", requestData);
|
||||||
const response = await createFlowDefinition(requestData as Parameters<typeof createFlowDefinition>[0]);
|
const response = await createFlowDefinition(requestData as unknown as Parameters<typeof createFlowDefinition>[0]);
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
toast({
|
toast({
|
||||||
title: "생성 완료",
|
title: "생성 완료",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue