ERP-node/frontend/docs/REST_API_UI_PATTERN.md

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