ERP-node/frontend/docs/REST_API_UI_PATTERN.md

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 필요
- 요청 바디 템플릿 필요