Files
vexplor/frontend/docs/REST_API_UI_PATTERN.md
2025-10-02 17:51:15 +09:00

8.5 KiB

REST API UI 구현 패턴

UPDATE, DELETE, UPSERT 노드에 적용할 REST API UI 패턴입니다.

1. Import 추가

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. 상태 변수 추가

// 타겟 타입 상태
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 (기본 정보 섹션 내부)

기존 "타겟 테이블" 입력 필드 위에 추가:

{/* 🔥 타겟 타입 선택 */}
<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 추가:

{/* 내부 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. 필드 매핑 섹션 조건부 렌더링

{/* 필드 매핑 (REST API 타입에서는 숨김) */}
{targetType !== "api" && (
  <div>
    {/* 기존 필드 매핑 UI */}
  </div>
)}

6. handleTargetTypeChange 함수

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