ERP-node/PHASE_EXTERNAL_CONNECTION_R...

20 KiB

외부 커넥션 관리 REST API 지원 확장 계획서

📋 프로젝트 개요

목적

현재 외부 데이터베이스 연결만 관리하는 /admin/external-connections 페이지에 REST API 연결 관리 기능을 추가하여, DB와 REST API 커넥션을 통합 관리할 수 있도록 확장합니다.

현재 상황

  • 기존 기능: 외부 데이터베이스 연결 정보만 관리 (MySQL, PostgreSQL, Oracle, SQL Server, SQLite)
  • 기존 테이블: external_db_connections - DB 연결 정보 저장
  • 기존 UI: 단일 화면에서 DB 연결 목록 표시 및 CRUD 작업

요구사항

  1. 탭 전환: DB 연결 관리 ↔ REST API 연결 관리 간 탭 전환 UI
  2. REST API 관리: 요청 주소별 헤더(키-값 쌍) 관리
  3. 연결 테스트: REST API 호출이 정상 작동하는지 테스트 기능

🗄️ 데이터베이스 설계

신규 테이블: external_rest_api_connections

CREATE TABLE external_rest_api_connections (
    id SERIAL PRIMARY KEY,

    -- 기본 정보
    connection_name VARCHAR(100) NOT NULL UNIQUE,
    description TEXT,

    -- REST API 연결 정보
    base_url VARCHAR(500) NOT NULL,              -- 기본 URL (예: https://api.example.com)
    default_headers JSONB DEFAULT '{}',          -- 기본 헤더 정보 (키-값 쌍)

    -- 인증 설정
    auth_type VARCHAR(20) DEFAULT 'none',        -- none, api-key, bearer, basic, oauth2
    auth_config JSONB,                           -- 인증 관련 설정

    -- 고급 설정
    timeout INTEGER DEFAULT 30000,               -- 요청 타임아웃 (ms)
    retry_count INTEGER DEFAULT 0,               -- 재시도 횟수
    retry_delay INTEGER DEFAULT 1000,            -- 재시도 간격 (ms)

    -- 관리 정보
    company_code VARCHAR(20) DEFAULT '*',
    is_active CHAR(1) DEFAULT 'Y',
    created_date TIMESTAMP DEFAULT NOW(),
    created_by VARCHAR(50),
    updated_date TIMESTAMP DEFAULT NOW(),
    updated_by VARCHAR(50),

    -- 테스트 정보
    last_test_date TIMESTAMP,
    last_test_result CHAR(1),                    -- Y: 성공, N: 실패
    last_test_message TEXT
);

-- 인덱스
CREATE INDEX idx_rest_api_connections_company ON external_rest_api_connections(company_code);
CREATE INDEX idx_rest_api_connections_active ON external_rest_api_connections(is_active);
CREATE INDEX idx_rest_api_connections_name ON external_rest_api_connections(connection_name);

샘플 데이터

INSERT INTO external_rest_api_connections (
    connection_name, description, base_url, default_headers, auth_type, auth_config
) VALUES
(
    '기상청 API',
    '기상청 공공데이터 API',
    'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0',
    '{"Content-Type": "application/json", "Accept": "application/json"}',
    'api-key',
    '{"keyLocation": "query", "keyName": "serviceKey", "keyValue": "your-api-key-here"}'
),
(
    '사내 인사 시스템 API',
    '인사정보 조회용 내부 API',
    'https://hr.company.com/api/v1',
    '{"Content-Type": "application/json"}',
    'bearer',
    '{"token": "your-bearer-token-here"}'
);

🔧 백엔드 구현

1. 타입 정의

// backend-node/src/types/externalRestApiTypes.ts

export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";

export interface ExternalRestApiConnection {
  id?: number;
  connection_name: string;
  description?: string;
  base_url: string;
  default_headers: Record<string, string>;
  auth_type: AuthType;
  auth_config?: {
    // API Key
    keyLocation?: "header" | "query";
    keyName?: string;
    keyValue?: string;

    // Bearer Token
    token?: string;

    // Basic Auth
    username?: string;
    password?: string;

    // OAuth2
    clientId?: string;
    clientSecret?: string;
    tokenUrl?: string;
    accessToken?: string;
  };
  timeout?: number;
  retry_count?: number;
  retry_delay?: number;
  company_code: string;
  is_active: string;
  created_date?: Date;
  created_by?: string;
  updated_date?: Date;
  updated_by?: string;
  last_test_date?: Date;
  last_test_result?: string;
  last_test_message?: string;
}

export interface ExternalRestApiConnectionFilter {
  auth_type?: string;
  is_active?: string;
  company_code?: string;
  search?: string;
}

export interface RestApiTestRequest {
  id?: number;
  base_url: string;
  endpoint?: string; // 테스트할 엔드포인트 (선택)
  method?: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  auth_type?: AuthType;
  auth_config?: any;
  timeout?: number;
}

export interface RestApiTestResult {
  success: boolean;
  message: string;
  response_time?: number;
  status_code?: number;
  response_data?: any;
  error_details?: string;
}

2. 서비스 계층

// backend-node/src/services/externalRestApiConnectionService.ts

export class ExternalRestApiConnectionService {
  // CRUD 메서드
  static async getConnections(filter: ExternalRestApiConnectionFilter);
  static async getConnectionById(id: number);
  static async createConnection(data: ExternalRestApiConnection);
  static async updateConnection(
    id: number,
    data: Partial<ExternalRestApiConnection>
  );
  static async deleteConnection(id: number);

  // 테스트 메서드
  static async testConnection(
    testRequest: RestApiTestRequest
  ): Promise<RestApiTestResult>;
  static async testConnectionById(
    id: number,
    endpoint?: string
  ): Promise<RestApiTestResult>;

  // 헬퍼 메서드
  private static buildHeaders(
    connection: ExternalRestApiConnection
  ): Record<string, string>;
  private static validateConnectionData(data: ExternalRestApiConnection): void;
  private static encryptSensitiveData(authConfig: any): any;
  private static decryptSensitiveData(authConfig: any): any;
}

3. API 라우트

// backend-node/src/routes/externalRestApiConnectionRoutes.ts

// GET    /api/external-rest-api-connections          - 목록 조회
// GET    /api/external-rest-api-connections/:id      - 상세 조회
// POST   /api/external-rest-api-connections          - 새 연결 생성
// PUT    /api/external-rest-api-connections/:id      - 연결 수정
// DELETE /api/external-rest-api-connections/:id      - 연결 삭제
// POST   /api/external-rest-api-connections/test     - 연결 테스트 (신규)
// POST   /api/external-rest-api-connections/:id/test - ID로 테스트 (기존 연결)

4. 연결 테스트 구현

// REST API 연결 테스트 로직
static async testConnection(testRequest: RestApiTestRequest): Promise<RestApiTestResult> {
  const startTime = Date.now();

  try {
    // 헤더 구성
    const headers = { ...testRequest.headers };

    // 인증 헤더 추가
    if (testRequest.auth_type === 'bearer' && testRequest.auth_config?.token) {
      headers['Authorization'] = `Bearer ${testRequest.auth_config.token}`;
    } else if (testRequest.auth_type === 'basic') {
      const credentials = Buffer.from(
        `${testRequest.auth_config.username}:${testRequest.auth_config.password}`
      ).toString('base64');
      headers['Authorization'] = `Basic ${credentials}`;
    } else if (testRequest.auth_type === 'api-key') {
      if (testRequest.auth_config.keyLocation === 'header') {
        headers[testRequest.auth_config.keyName] = testRequest.auth_config.keyValue;
      }
    }

    // URL 구성
    let url = testRequest.base_url;
    if (testRequest.endpoint) {
      url = `${testRequest.base_url}${testRequest.endpoint}`;
    }

    // API Key가 쿼리에 있는 경우
    if (testRequest.auth_type === 'api-key' &&
        testRequest.auth_config.keyLocation === 'query') {
      const separator = url.includes('?') ? '&' : '?';
      url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`;
    }

    // HTTP 요청 실행
    const response = await fetch(url, {
      method: testRequest.method || 'GET',
      headers,
      signal: AbortSignal.timeout(testRequest.timeout || 30000),
    });

    const responseTime = Date.now() - startTime;
    const responseData = await response.json().catch(() => null);

    return {
      success: response.ok,
      message: response.ok ? '연결 성공' : `연결 실패 (${response.status})`,
      response_time: responseTime,
      status_code: response.status,
      response_data: responseData,
    };
  } catch (error) {
    return {
      success: false,
      message: '연결 실패',
      error_details: error instanceof Error ? error.message : '알 수 없는 오류',
    };
  }
}

🎨 프론트엔드 구현

1. 탭 구조 설계

// frontend/app/(main)/admin/external-connections/page.tsx

type ConnectionTabType = "database" | "rest-api";

const [activeTab, setActiveTab] = useState<ConnectionTabType>("database");

2. 메인 페이지 구조 개선

// 탭 헤더
<Tabs
  value={activeTab}
  onValueChange={(value) => setActiveTab(value as ConnectionTabType)}
>
  <TabsList className="grid w-[400px] grid-cols-2">
    <TabsTrigger value="database" className="flex items-center gap-2">
      <Database className="h-4 w-4" />
      데이터베이스 연결
    </TabsTrigger>
    <TabsTrigger value="rest-api" className="flex items-center gap-2">
      <Globe className="h-4 w-4" />
      REST API 연결
    </TabsTrigger>
  </TabsList>

  {/* 데이터베이스 연결 탭 */}
  <TabsContent value="database">
    <DatabaseConnectionList />
  </TabsContent>

  {/* REST API 연결 탭 */}
  <TabsContent value="rest-api">
    <RestApiConnectionList />
  </TabsContent>
</Tabs>

3. REST API 연결 목록 컴포넌트

// frontend/components/admin/RestApiConnectionList.tsx

export function RestApiConnectionList() {
  const [connections, setConnections] = useState<ExternalRestApiConnection[]>(
    []
  );
  const [searchTerm, setSearchTerm] = useState("");
  const [authTypeFilter, setAuthTypeFilter] = useState("ALL");
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [editingConnection, setEditingConnection] = useState<
    ExternalRestApiConnection | undefined
  >();

  // 테이블 컬럼:
  // - 연결명
  // - 기본 URL
  // - 인증 타입
  // - 헤더 수 (default_headers 개수)
  // - 상태 (활성/비활성)
  // - 마지막 테스트 (날짜 + 결과)
  // - 작업 (테스트/편집/삭제)
}

4. REST API 연결 설정 모달

// frontend/components/admin/RestApiConnectionModal.tsx

export function RestApiConnectionModal({
  isOpen,
  onClose,
  onSave,
  connection,
}: RestApiConnectionModalProps) {
  // 섹션 구성:
  // 1. 기본 정보
  //    - 연결명 (필수)
  //    - 설명
  //    - 기본 URL (필수)
  // 2. 헤더 관리 (키-값 추가/삭제)
  //    - 동적 입력 필드
  //    - + 버튼으로 추가
  //    - 각 행에 삭제 버튼
  // 3. 인증 설정
  //    - 인증 타입 선택 (none/api-key/bearer/basic/oauth2)
  //    - 선택된 타입별 설정 필드 표시
  // 4. 고급 설정 (접기/펼치기)
  //    - 타임아웃
  //    - 재시도 설정
  // 5. 테스트 섹션
  //    - 테스트 엔드포인트 입력 (선택)
  //    - 테스트 실행 버튼
  //    - 테스트 결과 표시
}

5. 헤더 관리 컴포넌트

// frontend/components/admin/HeadersManager.tsx

interface HeadersManagerProps {
  headers: Record<string, string>;
  onChange: (headers: Record<string, string>) => void;
}

export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
  const [headersList, setHeadersList] = useState<
    Array<{ key: string; value: string }>
  >(Object.entries(headers).map(([key, value]) => ({ key, value })));

  const addHeader = () => {
    setHeadersList([...headersList, { key: "", value: "" }]);
  };

  const removeHeader = (index: number) => {
    const newList = headersList.filter((_, i) => i !== index);
    setHeadersList(newList);
    updateParent(newList);
  };

  const updateHeader = (
    index: number,
    field: "key" | "value",
    value: string
  ) => {
    const newList = [...headersList];
    newList[index][field] = value;
    setHeadersList(newList);
    updateParent(newList);
  };

  const updateParent = (list: Array<{ key: string; value: string }>) => {
    const headersObject = list.reduce((acc, { key, value }) => {
      if (key.trim()) acc[key] = value;
      return acc;
    }, {} as Record<string, string>);
    onChange(headersObject);
  };

  // UI: 테이블 형태로 키-값 입력 필드 표시
  // 각 행: [키 입력] [값 입력] [삭제 버튼]
  // 하단: [+ 헤더 추가] 버튼
}

6. 인증 설정 컴포넌트

// frontend/components/admin/AuthenticationConfig.tsx

export function AuthenticationConfig({
  authType,
  authConfig,
  onChange,
}: AuthenticationConfigProps) {
  // authType에 따라 다른 입력 필드 표시
  // none: 추가 필드 없음
  // api-key:
  //   - 키 위치 (header/query)
  //   - 키 이름
  //   - 키 값
  // bearer:
  //   - 토큰 값
  // basic:
  //   - 사용자명
  //   - 비밀번호
  // oauth2:
  //   - Client ID
  //   - Client Secret
  //   - Token URL
  //   - Access Token (읽기전용, 자동 갱신)
}

7. API 클라이언트

// frontend/lib/api/externalRestApiConnection.ts

export class ExternalRestApiConnectionAPI {
  private static readonly BASE_URL = "/api/external-rest-api-connections";

  static async getConnections(filter?: ExternalRestApiConnectionFilter) {
    const params = new URLSearchParams();
    if (filter?.search) params.append("search", filter.search);
    if (filter?.auth_type && filter.auth_type !== "ALL") {
      params.append("auth_type", filter.auth_type);
    }
    if (filter?.is_active && filter.is_active !== "ALL") {
      params.append("is_active", filter.is_active);
    }

    const response = await fetch(`${this.BASE_URL}?${params}`);
    return this.handleResponse(response);
  }

  static async getConnectionById(id: number) {
    const response = await fetch(`${this.BASE_URL}/${id}`);
    return this.handleResponse(response);
  }

  static async createConnection(data: ExternalRestApiConnection) {
    const response = await fetch(this.BASE_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return this.handleResponse(response);
  }

  static async updateConnection(
    id: number,
    data: Partial<ExternalRestApiConnection>
  ) {
    const response = await fetch(`${this.BASE_URL}/${id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data),
    });
    return this.handleResponse(response);
  }

  static async deleteConnection(id: number) {
    const response = await fetch(`${this.BASE_URL}/${id}`, {
      method: "DELETE",
    });
    return this.handleResponse(response);
  }

  static async testConnection(
    testRequest: RestApiTestRequest
  ): Promise<RestApiTestResult> {
    const response = await fetch(`${this.BASE_URL}/test`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(testRequest),
    });
    return this.handleResponse(response);
  }

  static async testConnectionById(
    id: number,
    endpoint?: string
  ): Promise<RestApiTestResult> {
    const response = await fetch(`${this.BASE_URL}/${id}/test`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ endpoint }),
    });
    return this.handleResponse(response);
  }

  private static async handleResponse(response: Response) {
    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new Error(error.message || "요청 실패");
    }
    return response.json();
  }
}

📋 구현 순서

Phase 1: 데이터베이스 및 백엔드 기본 구조 (1일)

  • 데이터베이스 테이블 생성 (external_rest_api_connections)
  • 타입 정의 작성 (externalRestApiTypes.ts)
  • 서비스 계층 기본 CRUD 구현
  • API 라우트 기본 구현

Phase 2: 연결 테스트 기능 (1일)

  • 연결 테스트 로직 구현
  • 인증 타입별 헤더 구성 로직
  • 에러 처리 및 타임아웃 관리
  • 테스트 결과 저장 (last_test_date, last_test_result)

Phase 3: 프론트엔드 기본 UI (1-2일)

  • 탭 구조 추가 (Database / REST API)
  • REST API 연결 목록 컴포넌트
  • API 클라이언트 작성
  • 기본 CRUD UI 구현

Phase 4: 모달 및 상세 기능 (1-2일)

  • REST API 연결 설정 모달
  • 헤더 관리 컴포넌트 (키-값 동적 추가/삭제)
  • 인증 설정 컴포넌트 (타입별 입력 필드)
  • 고급 설정 섹션

Phase 5: 테스트 및 통합 (1일)

  • 연결 테스트 UI
  • 테스트 결과 표시
  • 에러 처리 및 사용자 피드백
  • 전체 기능 통합 테스트

Phase 6: 최적화 및 마무리 (0.5일)

  • 민감 정보 암호화 (API 키, 토큰, 비밀번호)
  • UI/UX 개선
  • 문서화

🧪 테스트 시나리오

1. REST API 연결 등록 테스트

  • 기본 정보 입력 (연결명, URL)
  • 헤더 추가/삭제
  • 각 인증 타입별 설정
  • 유효성 검증 (필수 필드, URL 형식)

2. 연결 테스트

  • 인증 없는 API 테스트
  • API Key (header/query) 테스트
  • Bearer Token 테스트
  • Basic Auth 테스트
  • 타임아웃 시나리오
  • 네트워크 오류 시나리오

3. 데이터 관리

  • 목록 조회 및 필터링
  • 연결 수정
  • 연결 삭제
  • 활성/비활성 전환

4. 통합 시나리오

  • DB 연결 탭 ↔ REST API 탭 전환
  • 여러 연결 등록 및 관리
  • 동시 테스트 실행

🔒 보안 고려사항

1. 민감 정보 암호화

// API 키, 토큰, 비밀번호 암호화
private static encryptSensitiveData(authConfig: any): any {
  if (!authConfig) return null;

  const encrypted = { ...authConfig };

  // 암호화 대상 필드
  if (encrypted.keyValue) {
    encrypted.keyValue = encrypt(encrypted.keyValue);
  }
  if (encrypted.token) {
    encrypted.token = encrypt(encrypted.token);
  }
  if (encrypted.password) {
    encrypted.password = encrypt(encrypted.password);
  }
  if (encrypted.clientSecret) {
    encrypted.clientSecret = encrypt(encrypted.clientSecret);
  }

  return encrypted;
}

2. 접근 권한 제어

  • 관리자 권한만 접근
  • 회사별 데이터 분리
  • API 호출 시 인증 토큰 검증

3. 테스트 요청 제한

  • Rate Limiting (1분에 최대 10회)
  • 타임아웃 설정 (최대 30초)
  • 동시 테스트 제한

📊 성능 최적화

1. 헤더 데이터 구조

// JSONB 필드 인덱싱 (PostgreSQL)
CREATE INDEX idx_rest_api_headers ON external_rest_api_connections
USING GIN (default_headers);

CREATE INDEX idx_rest_api_auth_config ON external_rest_api_connections
USING GIN (auth_config);

2. 캐싱 전략

  • 자주 사용되는 연결 정보 캐싱
  • 테스트 결과 임시 캐싱 (5분)

📚 향후 확장 가능성

1. 엔드포인트 관리

각 REST API 연결에 대해 자주 사용하는 엔드포인트를 사전 등록하여 빠른 호출 가능

2. 요청 템플릿

HTTP 메서드별 요청 바디 템플릿 관리

3. 응답 매핑

REST API 응답을 내부 데이터 구조로 변환하는 매핑 룰 설정

4. 로그 및 모니터링

  • API 호출 이력 기록
  • 응답 시간 모니터링
  • 오류율 추적

완료 체크리스트

백엔드

  • 데이터베이스 테이블 생성
  • 타입 정의
  • 서비스 계층 CRUD
  • 연결 테스트 로직
  • API 라우트
  • 민감 정보 암호화

프론트엔드

  • 탭 구조
  • REST API 연결 목록
  • 연결 설정 모달
  • 헤더 관리 컴포넌트
  • 인증 설정 컴포넌트
  • API 클라이언트
  • 연결 테스트 UI

테스트

  • 단위 테스트
  • 통합 테스트
  • 사용자 시나리오 테스트

문서

  • API 문서
  • 사용자 가이드
  • 배포 가이드

작성일: 2025-10-20
버전: 1.0
담당: AI Assistant