252 lines
8.5 KiB
Markdown
252 lines
8.5 KiB
Markdown
# REST API UI 구현 패턴
|
|
|
|
UPDATE, DELETE, UPSERT 노드에 적용할 REST API UI 패턴입니다.
|
|
|
|
## 1. Import 추가
|
|
|
|
```typescript
|
|
import { Database, Globe, Link2 } from "lucide-react";
|
|
import { getTestedExternalConnections, getExternalTables, getExternalColumns } from "@/lib/api/nodeExternalConnections";
|
|
import type { ExternalConnection, ExternalTable, ExternalColumn } from "@/lib/api/nodeExternalConnections";
|
|
```
|
|
|
|
## 2. 상태 변수 추가
|
|
|
|
```typescript
|
|
// 타겟 타입 상태
|
|
const [targetType, setTargetType] = useState<"internal" | "external" | "api">(data.targetType || "internal");
|
|
|
|
// 외부 DB 관련 상태
|
|
const [externalConnections, setExternalConnections] = useState<ExternalConnection[]>([]);
|
|
const [externalConnectionsLoading, setExternalConnectionsLoading] = useState(false);
|
|
const [selectedExternalConnectionId, setSelectedExternalConnectionId] = useState<number | undefined>(data.externalConnectionId);
|
|
const [externalTables, setExternalTables] = useState<ExternalTable[]>([]);
|
|
const [externalTablesLoading, setExternalTablesLoading] = useState(false);
|
|
const [externalTargetTable, setExternalTargetTable] = useState(data.externalTargetTable);
|
|
const [externalColumns, setExternalColumns] = useState<ExternalColumn[]>([]);
|
|
const [externalColumnsLoading, setExternalColumnsLoading] = useState(false);
|
|
|
|
// REST API 관련 상태
|
|
const [apiEndpoint, setApiEndpoint] = useState(data.apiEndpoint || "");
|
|
const [apiMethod, setApiMethod] = useState<"PUT" | "PATCH" | "DELETE">(data.apiMethod || "PUT");
|
|
const [apiAuthType, setApiAuthType] = useState<"none" | "basic" | "bearer" | "apikey">(data.apiAuthType || "none");
|
|
const [apiAuthConfig, setApiAuthConfig] = useState(data.apiAuthConfig || {});
|
|
const [apiHeaders, setApiHeaders] = useState<Record<string, string>>(data.apiHeaders || {});
|
|
const [apiBodyTemplate, setApiBodyTemplate] = useState(data.apiBodyTemplate || "");
|
|
```
|
|
|
|
## 3. 타겟 타입 선택 UI (기본 정보 섹션 내부)
|
|
|
|
기존 "타겟 테이블" 입력 필드 위에 추가:
|
|
|
|
```tsx
|
|
{/* 🔥 타겟 타입 선택 */}
|
|
<div>
|
|
<Label className="mb-2 block text-xs font-medium">타겟 선택</Label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
{/* 내부 데이터베이스 */}
|
|
<button
|
|
onClick={() => handleTargetTypeChange("internal")}
|
|
className={cn(
|
|
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
|
|
targetType === "internal"
|
|
? "border-blue-500 bg-blue-50"
|
|
: "border-gray-200 hover:border-gray-300"
|
|
)}
|
|
>
|
|
<Database className={cn("h-5 w-5", targetType === "internal" ? "text-blue-600" : "text-gray-400")} />
|
|
<span className={cn("text-xs font-medium", targetType === "internal" ? "text-blue-700" : "text-gray-600")}>
|
|
내부 DB
|
|
</span>
|
|
{targetType === "internal" && (
|
|
<Check className="absolute right-2 top-2 h-4 w-4 text-blue-600" />
|
|
)}
|
|
</button>
|
|
|
|
{/* 외부 데이터베이스 */}
|
|
<button
|
|
onClick={() => handleTargetTypeChange("external")}
|
|
className={cn(
|
|
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
|
|
targetType === "external"
|
|
? "border-green-500 bg-green-50"
|
|
: "border-gray-200 hover:border-gray-300"
|
|
)}
|
|
>
|
|
<Globe className={cn("h-5 w-5", targetType === "external" ? "text-green-600" : "text-gray-400")} />
|
|
<span className={cn("text-xs font-medium", targetType === "external" ? "text-green-700" : "text-gray-600")}>
|
|
외부 DB
|
|
</span>
|
|
{targetType === "external" && (
|
|
<Check className="absolute right-2 top-2 h-4 w-4 text-green-600" />
|
|
)}
|
|
</button>
|
|
|
|
{/* REST API */}
|
|
<button
|
|
onClick={() => handleTargetTypeChange("api")}
|
|
className={cn(
|
|
"relative flex flex-col items-center gap-2 rounded-lg border-2 p-3 transition-all",
|
|
targetType === "api"
|
|
? "border-purple-500 bg-purple-50"
|
|
: "border-gray-200 hover:border-gray-300"
|
|
)}
|
|
>
|
|
<Link2 className={cn("h-5 w-5", targetType === "api" ? "text-purple-600" : "text-gray-400")} />
|
|
<span className={cn("text-xs font-medium", targetType === "api" ? "text-purple-700" : "text-gray-600")}>
|
|
REST API
|
|
</span>
|
|
{targetType === "api" && (
|
|
<Check className="absolute right-2 top-2 h-4 w-4 text-purple-600" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
## 4. REST API 설정 UI (타겟 타입이 "api"일 때)
|
|
|
|
기존 테이블 선택 UI를 조건부로 변경하고, REST API UI 추가:
|
|
|
|
```tsx
|
|
{/* 내부 DB 설정 */}
|
|
{targetType === "internal" && (
|
|
<div>
|
|
{/* 기존 타겟 테이블 Combobox */}
|
|
</div>
|
|
)}
|
|
|
|
{/* 외부 DB 설정 (INSERT 노드 참고) */}
|
|
{targetType === "external" && (
|
|
<div className="space-y-4">
|
|
{/* 외부 커넥션 선택, 테이블 선택, 컬럼 표시 */}
|
|
</div>
|
|
)}
|
|
|
|
{/* REST API 설정 */}
|
|
{targetType === "api" && (
|
|
<div className="space-y-4">
|
|
{/* API 엔드포인트 */}
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs font-medium">API 엔드포인트</Label>
|
|
<Input
|
|
placeholder="https://api.example.com/v1/users/{id}"
|
|
value={apiEndpoint}
|
|
onChange={(e) => {
|
|
setApiEndpoint(e.target.value);
|
|
updateNode(nodeId, { apiEndpoint: e.target.value });
|
|
}}
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* HTTP 메서드 (UPDATE: PUT/PATCH, DELETE: DELETE만) */}
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs font-medium">HTTP 메서드</Label>
|
|
<Select
|
|
value={apiMethod}
|
|
onValueChange={(value) => {
|
|
setApiMethod(value);
|
|
updateNode(nodeId, { apiMethod: value });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-8 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{/* UPDATE 노드: PUT, PATCH */}
|
|
<SelectItem value="PUT">PUT</SelectItem>
|
|
<SelectItem value="PATCH">PATCH</SelectItem>
|
|
{/* DELETE 노드: DELETE만 */}
|
|
{/* <SelectItem value="DELETE">DELETE</SelectItem> */}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 인증 방식, 인증 정보, 커스텀 헤더 (INSERT와 동일) */}
|
|
|
|
{/* 요청 바디 템플릿 (DELETE는 제외) */}
|
|
<div>
|
|
<Label className="mb-1.5 block text-xs font-medium">
|
|
요청 바디 템플릿
|
|
<span className="ml-1 text-gray-500">{`{{fieldName}}`}으로 소스 필드 참조</span>
|
|
</Label>
|
|
<textarea
|
|
placeholder={`{\n "id": "{{id}}",\n "name": "{{name}}"\n}`}
|
|
value={apiBodyTemplate}
|
|
onChange={(e) => {
|
|
setApiBodyTemplate(e.target.value);
|
|
updateNode(nodeId, { apiBodyTemplate: e.target.value });
|
|
}}
|
|
className="w-full rounded border p-2 font-mono text-xs"
|
|
rows={8}
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
소스 데이터의 필드명을 {`{{필드명}}`} 형태로 참조할 수 있습니다.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
## 5. 필드 매핑 섹션 조건부 렌더링
|
|
|
|
```tsx
|
|
{/* 필드 매핑 (REST API 타입에서는 숨김) */}
|
|
{targetType !== "api" && (
|
|
<div>
|
|
{/* 기존 필드 매핑 UI */}
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
## 6. handleTargetTypeChange 함수
|
|
|
|
```typescript
|
|
const handleTargetTypeChange = (newType: "internal" | "external" | "api") => {
|
|
setTargetType(newType);
|
|
updateNode(nodeId, {
|
|
targetType: newType,
|
|
// 타입별로 필요한 데이터만 유지
|
|
...(newType === "internal" && {
|
|
targetTable: data.targetTable,
|
|
targetConnection: data.targetConnection,
|
|
displayName: data.displayName,
|
|
}),
|
|
...(newType === "external" && {
|
|
externalConnectionId: data.externalConnectionId,
|
|
externalConnectionName: data.externalConnectionName,
|
|
externalDbType: data.externalDbType,
|
|
externalTargetTable: data.externalTargetTable,
|
|
externalTargetSchema: data.externalTargetSchema,
|
|
}),
|
|
...(newType === "api" && {
|
|
apiEndpoint: data.apiEndpoint,
|
|
apiMethod: data.apiMethod,
|
|
apiAuthType: data.apiAuthType,
|
|
apiAuthConfig: data.apiAuthConfig,
|
|
apiHeaders: data.apiHeaders,
|
|
apiBodyTemplate: data.apiBodyTemplate,
|
|
}),
|
|
});
|
|
};
|
|
```
|
|
|
|
## 노드별 차이점
|
|
|
|
### UPDATE 노드
|
|
- HTTP 메서드: `PUT`, `PATCH`
|
|
- WHERE 조건 필요
|
|
- 요청 바디 템플릿 필요
|
|
|
|
### DELETE 노드
|
|
- HTTP 메서드: `DELETE` 만
|
|
- WHERE 조건 필요
|
|
- 요청 바디 템플릿 **불필요** (쿼리 파라미터로 ID 전달)
|
|
|
|
### UPSERT 노드
|
|
- HTTP 메서드: `POST`, `PUT`, `PATCH`
|
|
- Conflict Keys 필요
|
|
- 요청 바디 템플릿 필요
|
|
|