REST API→DB 토큰 배치 및 auth_tokens 저장 구현

This commit is contained in:
dohyeons
2025-11-27 11:32:19 +09:00
parent ed56e14aa2
commit 707328e765
16 changed files with 1459 additions and 1964 deletions

View File

@@ -52,7 +52,8 @@ export default function BatchManagementNewPage() {
const [fromApiUrl, setFromApiUrl] = useState("");
const [fromApiKey, setFromApiKey] = useState("");
const [fromEndpoint, setFromEndpoint] = useState("");
const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원
const [fromApiMethod, setFromApiMethod] = useState<'GET' | 'POST' | 'PUT' | 'DELETE'>('GET');
const [fromApiBody, setFromApiBody] = useState(""); // Request Body (JSON)
// REST API 파라미터 설정
const [apiParamType, setApiParamType] = useState<'none' | 'url' | 'query'>('none');
@@ -83,6 +84,8 @@ export default function BatchManagementNewPage() {
// API 필드 → DB 컬럼 매핑
const [apiFieldMappings, setApiFieldMappings] = useState<Record<string, string>>({});
// API 필드별 JSON 경로 오버라이드 (예: "response.access_token")
const [apiFieldPathOverrides, setApiFieldPathOverrides] = useState<Record<string, string>>({});
// 배치 타입 상태
const [batchType, setBatchType] = useState<BatchType>('restapi-to-db');
@@ -303,8 +306,15 @@ export default function BatchManagementNewPage() {
// REST API 데이터 미리보기
const previewRestApiData = async () => {
if (!fromApiUrl || !fromApiKey || !fromEndpoint) {
toast.error("API URL, API Key, 엔드포인트를 모두 입력해주세요.");
// API URL, 엔드포인트는 항상 필수
if (!fromApiUrl || !fromEndpoint) {
toast.error("API URL과 엔드포인트를 모두 입력해주세요.");
return;
}
// GET 메서드일 때만 API 키 필수
if (fromApiMethod === "GET" && !fromApiKey) {
toast.error("GET 메서드에서는 API 키를 입력해주세요.");
return;
}
@@ -313,7 +323,7 @@ export default function BatchManagementNewPage() {
const result = await BatchManagementAPI.previewRestApiData(
fromApiUrl,
fromApiKey,
fromApiKey || "",
fromEndpoint,
fromApiMethod,
// 파라미터 정보 추가
@@ -322,7 +332,9 @@ export default function BatchManagementNewPage() {
paramName: apiParamName,
paramValue: apiParamValue,
paramSource: apiParamSource
} : undefined
} : undefined,
// Request Body 추가 (POST/PUT/DELETE)
(fromApiMethod === 'POST' || fromApiMethod === 'PUT' || fromApiMethod === 'DELETE') ? fromApiBody : undefined
);
console.log("API 미리보기 결과:", result);
@@ -370,31 +382,54 @@ export default function BatchManagementNewPage() {
// 배치 타입별 검증 및 저장
if (batchType === 'restapi-to-db') {
const mappedFields = Object.keys(apiFieldMappings).filter(field => apiFieldMappings[field]);
const mappedFields = Object.keys(apiFieldMappings).filter(
(field) => apiFieldMappings[field]
);
if (mappedFields.length === 0) {
toast.error("최소 하나의 API 필드를 DB 컬럼에 매핑해주세요.");
return;
}
// API 필드 매핑을 배치 매핑 형태로 변환
const apiMappings = mappedFields.map(apiField => ({
from_connection_type: 'restapi' as const,
from_table_name: fromEndpoint, // API 엔드포인트
from_column_name: apiField, // API 필드명
from_api_url: fromApiUrl,
from_api_key: fromApiKey,
from_api_method: fromApiMethod,
// API 파라미터 정보 추가
from_api_param_type: apiParamType !== 'none' ? apiParamType : undefined,
from_api_param_name: apiParamType !== 'none' ? apiParamName : undefined,
from_api_param_value: apiParamType !== 'none' ? apiParamValue : undefined,
from_api_param_source: apiParamType !== 'none' ? apiParamSource : undefined,
to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external',
to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id,
to_table_name: toTable,
to_column_name: apiFieldMappings[apiField], // 매핑된 DB 컬럼
mapping_type: 'direct' as const
}));
const apiMappings = mappedFields.map((apiField) => {
const toColumnName = apiFieldMappings[apiField]; // 매핑된 DB 컬럼 (예: access_token)
// 기본은 상위 필드 그대로 사용하되,
// 사용자가 JSON 경로를 직접 입력한 경우 해당 경로를 우선 사용
let fromColumnName = apiField;
const overridePath = apiFieldPathOverrides[apiField];
if (overridePath && overridePath.trim().length > 0) {
fromColumnName = overridePath.trim();
}
return {
from_connection_type: "restapi" as const,
from_table_name: fromEndpoint, // API 엔드포인트
from_column_name: fromColumnName, // API 필드명 또는 중첩 경로
from_api_url: fromApiUrl,
from_api_key: fromApiKey,
from_api_method: fromApiMethod,
from_api_body:
fromApiMethod === "POST" ||
fromApiMethod === "PUT" ||
fromApiMethod === "DELETE"
? fromApiBody
: undefined,
// API 파라미터 정보 추가
from_api_param_type: apiParamType !== "none" ? apiParamType : undefined,
from_api_param_name: apiParamType !== "none" ? apiParamName : undefined,
from_api_param_value: apiParamType !== "none" ? apiParamValue : undefined,
from_api_param_source:
apiParamType !== "none" ? apiParamSource : undefined,
to_connection_type:
toConnection?.type === "internal" ? "internal" : "external",
to_connection_id:
toConnection?.type === "internal" ? undefined : toConnection?.id,
to_table_name: toTable,
to_column_name: toColumnName, // 매핑된 DB 컬럼
mapping_type: "direct" as const,
};
});
console.log("REST API 배치 설정 저장:", {
batchName,
@@ -645,13 +680,19 @@ export default function BatchManagementNewPage() {
/>
</div>
<div>
<Label htmlFor="fromApiKey">API *</Label>
<Label htmlFor="fromApiKey">
API
{fromApiMethod === "GET" && <span className="text-red-500 ml-0.5">*</span>}
</Label>
<Input
id="fromApiKey"
value={fromApiKey}
onChange={(e) => setFromApiKey(e.target.value)}
placeholder="ak_your_api_key_here"
/>
<p className="text-xs text-gray-500 mt-1">
GET , POST/PUT/DELETE일 .
</p>
</div>
</div>
@@ -673,12 +714,33 @@ export default function BatchManagementNewPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET ( )</SelectItem>
<SelectItem value="POST">POST ( /)</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Request Body (POST/PUT/DELETE용) */}
{(fromApiMethod === 'POST' || fromApiMethod === 'PUT' || fromApiMethod === 'DELETE') && (
<div>
<Label htmlFor="fromApiBody">Request Body (JSON)</Label>
<Textarea
id="fromApiBody"
value={fromApiBody}
onChange={(e) => setFromApiBody(e.target.value)}
placeholder='{"username": "myuser", "token": "abc"}'
className="min-h-[100px]"
rows={5}
/>
<p className="text-xs text-gray-500 mt-1">
API JSON .
</p>
</div>
)}
{/* API 파라미터 설정 */}
<div className="space-y-4">
<div className="border-t pt-4">
@@ -771,7 +833,10 @@ export default function BatchManagementNewPage() {
)}
</div>
{fromApiUrl && fromApiKey && fromEndpoint && (
{/* API URL + 엔드포인트는 필수, GET일 때만 API 키 필수 */}
{fromApiUrl &&
fromEndpoint &&
(fromApiMethod !== "GET" || fromApiKey) && (
<div className="space-y-3">
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700">API </div>
@@ -786,7 +851,11 @@ export default function BatchManagementNewPage() {
: ''
}
</div>
<div className="text-xs text-gray-500 mt-1">Headers: X-API-Key: {fromApiKey.substring(0, 10)}...</div>
{fromApiKey && (
<div className="text-xs text-gray-500 mt-1">
Headers: X-API-Key: {fromApiKey.substring(0, 10)}...
</div>
)}
{apiParamType !== 'none' && apiParamName && apiParamValue && (
<div className="text-xs text-blue-600 mt-1">
: {apiParamName} = {apiParamValue} ({apiParamSource === 'static' ? '고정값' : '동적값'})
@@ -993,10 +1062,28 @@ export default function BatchManagementNewPage() {
<div className="flex-1">
<div className="font-medium text-sm">{apiField}</div>
<div className="text-xs text-gray-500">
{fromApiData.length > 0 && fromApiData[0][apiField] !== undefined
? `예: ${String(fromApiData[0][apiField]).substring(0, 30)}${String(fromApiData[0][apiField]).length > 30 ? '...' : ''}`
: 'API 필드'
}
{fromApiData.length > 0 && fromApiData[0][apiField] !== undefined
? `예: ${String(fromApiData[0][apiField]).substring(0, 30)}${
String(fromApiData[0][apiField]).length > 30 ? "..." : ""
}`
: "API 필드"}
</div>
{/* JSON 경로 오버라이드 입력 */}
<div className="mt-1.5">
<Input
value={apiFieldPathOverrides[apiField] || ""}
onChange={(e) =>
setApiFieldPathOverrides((prev) => ({
...prev,
[apiField]: e.target.value,
}))
}
placeholder="JSON 경로 (예: response.access_token)"
className="h-7 text-xs"
/>
<p className="text-[11px] text-gray-500 mt-0.5">
"{apiField}" , .
</p>
</div>
</div>

View File

@@ -580,22 +580,60 @@ export default function BatchEditPage() {
</div>
{mappings.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>API URL</Label>
<Input value={mappings[0]?.from_api_url || ''} readOnly />
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label>API URL</Label>
<Input value={mappings[0]?.from_api_url || ""} readOnly />
</div>
<div>
<Label>API </Label>
<Input
value={mappings[0]?.from_table_name || ""}
readOnly
/>
</div>
<div>
<Label>HTTP </Label>
<Input
value={mappings[0]?.from_api_method || "GET"}
readOnly
/>
</div>
<div>
<Label> </Label>
<Input
value={mappings[0]?.to_table_name || ""}
readOnly
/>
</div>
</div>
{/* Request Body (JSON) 편집 UI */}
<div>
<Label>API </Label>
<Input value={mappings[0]?.from_table_name || ''} readOnly />
</div>
<div>
<Label>HTTP </Label>
<Input value={mappings[0]?.from_api_method || 'GET'} readOnly />
</div>
<div>
<Label> </Label>
<Input value={mappings[0]?.to_table_name || ''} readOnly />
<Label>Request Body (JSON)</Label>
<Textarea
rows={5}
className="font-mono text-sm"
placeholder='{"id": "wace", "pwd": "wace!$%Pwdmo^^"}'
value={mappings[0]?.from_api_body || ""}
onChange={(e) => {
const value = e.target.value;
setMappings((prev) => {
if (prev.length === 0) return prev;
const updated = [...prev];
updated[0] = {
...updated[0],
from_api_body: value,
} as any;
return updated;
});
}}
/>
<p className="text-xs text-muted-foreground mt-1.5">
POST JSON Request Body를 .
.
</p>
</div>
</div>
)}