INNORIX
전송 빌더전송 파인더개발자리소스고객사
무료 시작하기
INNORIX

LET FILES
MOVE THEMSELVES

INNORIX는 모든 시스템과 환경에서 파일 이동과 자동화를 제공하는 엔터프라이즈 파일 인프라 기업입니다.
5,000개 이상의 기업 및 공공기관에서 사용하고 있습니다.

시작하기

  • 필요한 전송 만들기
  • 필요한 전송 찾기

주요 전송

  • 팀 업무 폴더 동기화
  • 고객에게 대용량 파일 전송
  • 여러 시스템 파일 탐색
  • FTP·SFTP·SCP·rsync 전환
  • 앱에 파일 전송 추가
  • 웹 업로드·다운로드 적용
  • AI·데이터 워크플로 구축
  • 모든 전송 둘러보기→

개발자

  • 개발자 센터
  • 구현 예제
  • API 빠른 시작
  • 개발자 가이드
  • API 레퍼런스
  • GitHub

리소스

  • 리소스 센터
  • 제품 가이드
  • 외부 서비스 연동
  • 배포 및 관리
  • 도움말 센터

고객사

  • 정부
  • 공공부문
  • 제조
  • 엔지니어링
  • 금융
  • 유통
  • IT/통신
  • 미디어
  • 의료
  • 교육

플랜

  • 가격 및 플랜

회사

INNORIX 소개

Exabyter를 찾고 계신가요?

이제 INNORIX Platform에 통합되었습니다

기타 INNORIX 제품

Al.bert — 스마트 교통 AI

글로벌 오피스

  • 미국 뉴욕
  • 대한민국 서울
  • 베트남 호찌민
  • 오피스 위치 보기→

(C)2026 INNORIX. All rights reserved.

  • 보안
  • 상태
  • 이용약관
  • 개인정보 처리방침
  • 쿠키 정책
  1. 가이드
  2. 폐쇄망·망분리 환경에서 파일을 승인 반입·반출하기

폐쇄망·망분리 환경에서 파일을 승인 반입·반출하기

INNORIX에서 에어갭 파일 전송을 구성하고 연결된 시스템 간 파일 작업을 일관되게 운영하는 방법을 알아보세요.

IT 엔지니어개발자
  • AI 학습 데이터와 모델 파일의 전체 흐름 자동화하기
  • AI 모델 파일을 여러 엣지 장비에 배포하기
  • 폐쇄망·망분리 환경에서 파일을 승인 반입·반출하기
  • 승인된 파일을 여러 지점에 동시에 배포하기
  • Azure Blob에서 사내 서버로 파일 전달하기
  • 데이터베이스 덤프, 백업, 아카이브 파일을 원격 보관처로 자동 전달하기
  • 지점·공장·엣지 장비의 파일을 중앙으로 수집하기
  • CI/CD 빌드 결과물을 여러 서버에 배포하기
  • 서로 다른 클라우드 스토리지 사이에서 파일 이동하기
  • 서로 다른 AWS 계정의 S3 버킷 사이에 파일 전송하기
  • 고객별 파일 작업 공간 제공하기
  • 고객에게 만료 조건이 있는 대용량 다운로드 링크 제공하기
  • 고객이 브라우저에서 대용량 파일을 업로드하게 하기
  • 고객 작업 공간으로 대용량 파일 직접 보내기
  • 데이터베이스 백업 파일을 오브젝트 스토리지에 보관하기
  • Datadog으로 파일 전송 실패와 복구 알림 받기
  • 웹, 앱, 업무 시스템에 파일 전송 기능 추가하기
  • 파일 도착 후 검증·변환·후속 작업 실행하기
  • NAS, 파일 서버의 대량 파일을 클라우드로 이전하기
  • FTP 배치 작업을 관리형 파일 흐름으로 전환하기
  • Google Cloud Storage에서 Amazon S3로 직접 전송하기
  • Grafana에서 파일 전송 상태 대시보드 만들기
  • 전송된 파일의 해시값으로 무결성 자동 검증하기
  • 새 파일이 생기면 지정한 곳으로 자동 전송하기
  • Kubernetes에서 오브젝트 스토리지로 결과 파일 보내기
  • Git 밖의 대용량 파일과 빌드 결과물 자동 전달하기
  • Git으로 관리하기 어려운 파일을 자동 전달하기
  • 분산 서버의 로그·진단 파일을 중앙으로 모으기
  • 미디어 원본과 처리 결과를 단계별로 자동 전달하기
  • 수신 파일을 Microsoft Defender로 검사하고 후속 처리하기
  • 여러 단계의 파일 전송을 하나의 흐름으로 자동화하기
  • 승인된 파일을 여러 팀·지점에 자동 배포하기
  • 협력사 파일을 업무 시스템으로 자동 분류하기
  • 협력사·공급망과 정기적으로 파일 교환하기
  • 매일·매주 반복되는 파일 전송 자동화하기
  • 네트워크 중단 후 파일 전송을 자동으로 재개하기
  • rsync 작업을 관리형 파일 흐름으로 전환하기
  • 정해진 시간에 반복 파일 전송 예약하기
  • 소프트웨어·펌웨어를 여러 장비에 배포하고 결과 확인하기
  • 소프트웨어 패키지를 여러 서버·지점에 배포하기
  • 팀 폴더의 변경 파일을 여러 장비에 자동 반영하기
  • 팀 업무 폴더를 여러 PC에 자동 반영하기
  • 여러 장비의 파일을 한곳에서 찾고 직접 전송하기
  • 웹사이트에 대용량 파일 업로드·다운로드 기능 추가하기
  • 채널을 구독해 새 파일을 자동으로 받기
  • Amazon S3에서 Azure Blob으로 직접 전송하기
  • Amazon S3에서 Cloudflare R2로 파일 이전하기
  • Amazon S3 파일을 Linux 서버로 자동 내려받기
  • SCP 셸 스크립트를 CLI 기반 전송으로 전환하기
  • SFTP 계정과 배치 전송을 중앙에서 관리하기
  • Slack·Teams에서 파일 전송을 실행하고 상태 알림 받기
  • Windows 폴더의 파일을 Amazon S3로 자동 업로드하기
  • Windows와 Linux 서버 사이에 파일을 직접 전송하기

시작하기#

기본 개념#

승인된 파일을 정해진 경로와 절차에 따라 반입·반출하기

폐쇄망과 외부망처럼 분리된 네트워크 환경에서는 파일의 이동 방향과 대상 경로, 처리 기준을 업무 환경에 맞게 관리합니다.

파일 반입·반출은 사용자가 전송할 파일을 요청하고, 설정된 승인과 검증 절차를 거쳐 지정된 환경으로 전송하는 방식으로 구성할 수 있습니다.

예를 들어 외부 환경에서 사용할 파일을 폐쇄망으로 반입하거나, 내부 업무 과정에서 생성된 결과 파일을 승인 절차에 따라 외부 환경으로 반출할 수 있습니다.

전송 흐름#

파일 요청부터 승인과 검증, 최종 반입·반출까지 이어가기

망분리 환경의 파일 전송은 파일을 선택하는 단계부터 대상 환경에 반영되는 과정까지 정해진 절차에 따라 진행됩니다.

파일 요청
   ↓

전송 대상 확인
   ↓

승인 기준 적용
   ↓

파일 검증
   ↓

반입 또는 반출
   ↓

전송 결과 기록

파일의 이동 방향에 따라 필요한 승인과 검증 기준을 적용하고, 처리된 결과는 실행 기록으로 확인할 수 있습니다.

이를 통해 내부와 외부 환경 사이의 파일 흐름을 업무 기준에 맞게 구성하고, 각 전송 작업의 처리 상태와 결과를 함께 관리할 수 있습니다.

운영 효과#

파일 이동과 검증, 처리 이력을 하나의 흐름으로 관리하기

망분리 환경에서는 파일을 전송하는 과정과 함께 어떤 파일이 어느 방향으로 처리되었는지 확인할 수 있는 운영 흐름이 필요합니다.

반입·반출 작업을 구성하면 파일 요청, 승인 결과, 검증 정보, 전송 상태를 하나의 실행 흐름으로 관리할 수 있습니다.

관리 항목 파일 반입·반출 흐름
전송 요청 사용자와 업무 기준에 따라 요청 관리
이동 방향 내부망과 외부망의 반입·반출 경로 관리
승인 처리 설정된 기준에 따라 승인 흐름 적용
파일 검증 검사 결과를 전송 작업과 연결
실행 기록 요청부터 최종 처리 결과까지 확인

이렇게 구성하면 파일 반입과 반출을 각각 관리하는 방식에서 확장해, 파일 요청부터 검증과 전송 결과 확인까지 이어지는 운영 흐름을 구성할 수 있습니다.

IT 엔지니어#

환경 구성#

내부망과 외부망의 전송 경로 연결하기

먼저 파일을 반입하거나 반출할 네트워크 환경과 전송 장비를 연결합니다.

각 환경에서 사용할 파일 경로와 대상 위치를 지정하고, 파일이 처리되는 방향에 따라 반입과 반출 흐름을 구성합니다.

외부 환경
    │
    │ 반입
    ▼
┌──────────┐
│ 승인·검증 │
└──────────┘
    │
    ▼
내부 환경

반출 작업은 내부 환경에서 생성된 파일을 승인과 검증 기준에 따라 외부 환경으로 연결합니다.

내부 환경
    │
    │ 반출
    ▼
┌──────────┐
│ 승인·검증 │
└──────────┘
    │
    ▼
외부 환경

승인·검증 설정#

파일과 사용자, 업무 기준에 따라 처리 절차 구성하기

파일 반입과 반출 작업에서는 어떤 파일을 어떤 기준으로 처리할지 설정합니다.

파일 유형, 사용자, 업무 목적, 전송 방향 등을 기준으로 승인 흐름을 구성하고, 필요한 경우 파일 검증 작업을 연결할 수 있습니다.

예를 들어 특정 확장자의 파일은 별도의 승인 절차를 거치도록 설정하거나, 지정된 업무 담당자의 승인 이후 다음 단계가 실행되도록 구성할 수 있습니다.

전송 요청
   │
   ├── 승인 기준 확인
   │
   ▼
승인 처리
   │
   ▼
파일 검증
   │
   ▼
전송 실행

반입·반출 흐름#

승인과 검증 결과를 기준으로 지정된 환경에 파일 반영하기

전송 환경과 처리 기준을 설정한 후에는 승인된 파일이 지정된 경로로 반입 또는 반출되도록 작업 흐름을 구성합니다.

반입과 반출은 각각 필요한 대상 환경과 저장 위치를 지정할 수 있으며, 파일 처리 결과는 하나의 실행 기록으로 관리할 수 있습니다.

반입 요청 ──→ 승인 ──→ 검증 ──→ 내부망 저장

반출 요청 ──→ 승인 ──→ 검증 ──→ 외부망 저장

두 흐름은 파일의 이동 방향과 대상 경로에 따라 구분하면서도, 승인·검증·실행 결과를 같은 운영 기준으로 관리할 수 있습니다.

이력 확인#

요청부터 최종 전송 결과까지 실행 기록으로 관리하기

반입·반출 작업이 실행되면 요청 정보와 승인 결과, 검증 상태, 파일 처리 결과를 실행 기록에서 확인합니다.

각 작업에서는 다음과 같은 정보를 함께 관리할 수 있습니다.

확인 항목 확인 내용
요청 정보 요청한 사용자와 파일
전송 방향 반입 또는 반출
승인 결과 승인 처리 상태
검증 결과 파일 검사와 처리 결과
전송 상태 현재 진행 상태와 완료 결과
실행 기록 작업별 처리 시간과 전체 이력
Request
   ↓

Approval
   ↓

Validation
   ↓

Transfer
   ↓

Completed

이를 통해 특정 파일이 어떤 절차를 거쳐 어느 환경으로 처리되었는지 실행 단위로 확인할 수 있습니다.

보안 운영#

접근 범위와 전송 정책을 기준으로 파일 흐름 관리하기

운영 과정에서는 사용자와 장비별 접근 범위, 전송 가능한 파일과 경로, 승인 및 검증 기준을 함께 관리합니다.

업무 환경이나 운영 기준이 변경되면 해당 정책을 조정하고, 변경된 기준이 이후 반입·반출 작업에 적용되도록 구성할 수 있습니다.

관리 영역 운영 기준
사용자 파일 요청과 작업 실행 범위
장비 연결 가능한 전송 환경
파일 처리 대상과 파일 유형
경로 반입·반출 대상 위치
승인 업무별 처리 절차
검증 파일 검사와 결과 기준

운영 대응#

처리 상태를 확인하고 필요한 작업 다시 실행하기

실행 결과에서 확인이 필요한 작업은 반입·반출 방향, 승인 상태, 검증 결과, 연결 상태와 대상 경로를 기준으로 처리 내용을 확인합니다.

실행 기록 확인
      ↓

처리 단계 확인
      ↓

승인·검증·연결 상태 확인
      ↓

환경 또는 정책 조정
      ↓

작업 재실행
      ↓

최종 결과 확인

재실행 이후에는 새로운 실행 기록을 통해 파일이 지정된 환경에 정상적으로 반영되었는지 확인할 수 있습니다.

이렇게 구성하면 망분리 환경의 파일 요청 → 승인 → 검증 → 반입·반출 → 실행 이력 관리를 하나의 흐름으로 연결할 수 있습니다. 각 단계의 처리 기준과 결과를 함께 관리하면서 내부와 외부 환경 사이의 파일 전송 운영 흐름을 구성할 수 있습니다.

개발자#

반입·반출 요청을 승인·검사·전송 단계로 연결하고 이력 남기기

연동 준비#

망별 클라이언트를 나누고 상태 값 준비하기

import os
import requests

BASE_URL = os.getenv("INNORIX_BASE_URL", "https://app.innorix.com").rstrip("/")
TOKEN = os.environ["INNORIX_ACCESS_TOKEN"]
WORKSPACE_ID = os.getenv("INNORIX_WORKSPACE_ID")   # optional; falls back to the current workspace

STATUS_COMPLETE = 2
TERMINAL = {2, 4, 5, 9, 99}          # complete / error / cancelled / partial / failed
NOT_SUCCEEDED = {4, 5, 9, 99}


def api(method, path, body=None, params=None):
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {TOKEN}",
    }

    if WORKSPACE_ID:
        headers["x-workspace-id"] = WORKSPACE_ID

    response = requests.request(
        method, BASE_URL + path,
        headers=headers, json=body, params=params, timeout=30,
    )

    payload = response.json() if response.content else {}

    if not response.ok:
        raise RuntimeError(payload.get("message") or f"HTTP {response.status_code}")

    return payload.get("data")


def is_terminal(detail):
    return detail.get("isTerminal", detail.get("status") in TERMINAL)
// InnorixClient.java
public static final String BASE_URL =
        env("INNORIX_BASE_URL", "https://app.innorix.com").replaceAll("/+
quot;
, ""); public static final String WORKSPACE_ID = env("INNORIX_WORKSPACE_ID", null); public static final int STATUS_COMPLETE = 2; // States the transfer no longer moves out of public static final Set<Integer> TERMINAL = Set.of(2, 4, 5, 9, 99); // Terminal states that are not a full success public static final Set<Integer> NOT_SUCCEEDED = Set.of(4, 5, 9, 99); private HttpRequest.Builder headers(HttpRequest.Builder builder) { builder.header("Content-Type", "application/json") .header("Authorization", "Bearer " + session.accessToken()); // When omitted the account's current workspace is used. if (workspaceId != null) builder.header("x-workspace-id", workspaceId); return builder; } /** Unwraps and returns data from the response. Throws ApiError on failure. */ public Object api(String method, String path, Object body, Map<String, Object> params) { Resp response = request(method, path, body, params); Object payload = null; try { payload = Json.parse(response.text()); } catch (RuntimeException ignored) { payload = null; } if (!response.ok()) { Map<String, Object> map = Json.asObj(payload); String message = Json.str(map, "message", Json.str(map, "error", "unknown error")); throw new ApiError(response.status, message, map); } return Json.get(payload, "data"); } /** Use the server flag when present, otherwise fall back to the status code. */ public static boolean isTerminal(Map<String, Object> record) { Boolean flag = Json.boolOrNull(record, "isTerminal"); if (flag != null) return flag; Integer status = Json.intOrNull(record, "status"); return status != null && TERMINAL.contains(status); }
// innorix-client.js
const BASE_URL = (process.env.INNORIX_BASE_URL
  || "https://app.innorix.com").replace(/\/+$/, "");
const TOKEN = process.env.INNORIX_ACCESS_TOKEN;
const WORKSPACE_ID = process.env.INNORIX_WORKSPACE_ID || null;

export const STATUS_COMPLETE = 2;
export const TERMINAL = new Set([2, 4, 5, 9, 99]);       // complete / error / cancelled / partial / failed
export const NOT_SUCCEEDED = new Set([4, 5, 9, 99]);

export async function api(method, path, body = null, params = null) {
  const url = new URL(BASE_URL + path);

  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value === undefined || value === null) continue;
      url.searchParams.set(key, String(value));
    }
  }

  const headers = {
    "Content-Type": "application/json",
    Authorization: `Bearer ${TOKEN}`,
  };

  // When omitted the account's current workspace is used.
  if (WORKSPACE_ID) headers["x-workspace-id"] = WORKSPACE_ID;

  const response = await fetch(url, {
    method,
    headers,
    body: body === null ? undefined : JSON.stringify(body),
    signal: AbortSignal.timeout(30_000),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(payload.message || `HTTP ${response.status}`);
  }

  return payload.data;
}

export function isTerminal(detail) {
  return detail.isTerminal !== undefined
    ? detail.isTerminal
    : TERMINAL.has(detail.status);
}
// InnorixClient.cs
public static readonly string BaseUrl =
    Env("INNORIX_BASE_URL", "https://app.innorix.com").TrimEnd('/');
public static readonly string WorkspaceIdFromEnv = Env("INNORIX_WORKSPACE_ID", null);

public const int StatusComplete = 2;
/// <summary>States the transfer no longer moves out of</summary>
public static readonly HashSet<int> Terminal = new HashSet<int> { 2, 4, 5, 9, 99 };
/// <summary>Terminal states that are not a full success</summary>
public static readonly HashSet<int> NotSucceeded = new HashSet<int> { 4, 5, 9, 99 };

// Applied on every request
request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Session.AccessToken);
// When omitted the account's current workspace is used.
if (WorkspaceId != null) request.Headers.TryAddWithoutValidation("x-workspace-id", WorkspaceId);

public async Task<JsonNode> ApiAsync(string method, string path, JsonNode body = null,
    IDictionary<string, object> parameters = null)
{
    Resp response = await RequestAsync(method, path, body, parameters).ConfigureAwait(false);
    JsonNode payload = null;

    try
    {
        payload = J.Parse(response.Text());
    }
    catch (Exception)
    {
        payload = null;
    }

    if (!response.Ok)
    {
        JsonObject map = J.AsObj(payload);
        string message = J.Str(map, "message", J.Str(map, "error", "unknown error"));
        throw new ApiError(response.Status, message, map);
    }

    return J.Get(payload, "data");
}

/// <summary>Use the server flag when present, otherwise fall back to the status code.</summary>
public static bool IsTerminal(JsonObject record)
{
    bool? flag = J.BoolOrNull(record, "isTerminal");
    if (flag != null) return flag.Value;

    int? status = J.IntOrNull(record, "status");
    return status != null && Terminal.Contains(status.Value);
}

전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.

상태 값 의미 종료
2 완료 예
4 오류 예
5 취소 예
9 부분 완료 예
99 실패 예
1 · 6 · 12 · 13 시작·전송 중·동기화 중·수신 중 아니오

내부망과 외부망을 작업 공간으로 나눠 쓴다면 요청마다 작업 공간 식별자를 지정합니다. 한쪽 코드가 다른 쪽 자원에 닿지 않도록 클라이언트를 분리합니다.

def client_for(workspace_id):
    def call(method, path, body=None, params=None):
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {TOKEN}",
            "x-workspace-id": workspace_id,
        }

        response = requests.request(method, BASE_URL + path, headers=headers,
                                    json=body, params=params, timeout=30)

        payload = response.json() if response.content else {}

        if response.status_code == 403:
            raise PermissionError(f"{workspace_id}: {path}")

        if not response.ok:
            raise RuntimeError(payload.get("message"))

        return payload.get("data")

    return call


external = client_for(EXTERNAL_WORKSPACE)   # external network
internal = client_for(INTERNAL_WORKSPACE)   # internal network
InnorixClient clientFor(String workspaceId) {
    // A separate client per workspace keeps the two networks isolated.
    return new InnorixClient(session, workspaceId, false);
}

InnorixClient external = clientFor(EXTERNAL_WORKSPACE);   // external network
InnorixClient internal = clientFor(INTERNAL_WORKSPACE);   // internal network
function clientFor(workspaceId) {
  // A separate client per workspace keeps the two networks isolated.
  return new Client(session, workspaceId);
}

const external = clientFor(EXTERNAL_WORKSPACE);   // external network
const internal = clientFor(INTERNAL_WORKSPACE);   // internal network
Client ClientFor(string workspaceId)
{
    // A separate client per workspace keeps the two networks isolated.
    return new Client(session, workspaceId);
}

Client external = ClientFor(ExternalWorkspace);   // external network
Client internal = ClientFor(InternalWorkspace);   // internal network

접근 권한이 없는 공간을 지정하면 403이 반환됩니다. 이를 별도 예외로 두면 정책 위반과 일반 오류를 구분해 기록할 수 있습니다.

요청 등록#

전송 요청을 업무 데이터로 만들고 경로 유효성 확인하기

반입과 반출은 방향이 다를 뿐 절차가 같습니다. 요청을 하나의 구조로 다루면 승인과 검증 로직을 공유할 수 있습니다.

import uuid


def create_request(user_id, direction, files, purpose):
    request_id = str(uuid.uuid4())

    db.insert("transfer_requests", {
        "id": request_id,
        "userId": user_id,
        "direction": direction,          # "import" or "export"
        "files": files,
        "purpose": purpose,
        "state": "pending",
    })

    return request_id
String createRequest(String userId, String direction, List<String> files, String purpose) {
    String requestId = UUID.randomUUID().toString();

    db.insert("transfer_requests", Json.newObj(
            "id", requestId,
            "userId", userId,
            "direction", direction,          // "import" or "export"
            "files", files,
            "purpose", purpose,
            "state", "pending"));

    return requestId;
}
import { randomUUID } from "node:crypto";

async function createRequest(userId, direction, files, purpose) {
  const requestId = randomUUID();

  await db.insert("transfer_requests", {
    id: requestId,
    userId,
    direction,          // "import" or "export"
    files,
    purpose,
    state: "pending",
  });

  return requestId;
}
async Task<string> CreateRequestAsync(string userId, string direction,
    IEnumerable<string> files, string purpose)
{
    string requestId = Guid.NewGuid().ToString();

    await db.InsertAsync("transfer_requests", new
    {
        id = requestId,
        userId,
        direction,          // "import" or "export"
        files,
        purpose,
        state = "pending",
    });

    return requestId;
}

요청 시점에 파일이 실제로 있는지 확인해 두면 승인 후 실행 단계에서 실패하는 것을 막습니다.

def validate_request(call, source_id, target_id, files, target_path):
    # sourceItems reads filePath, not path
    result = call("POST", "/api/transfers/validate-path", {
        "sourceId": source_id,
        "targetId": target_id,
        "sourceItems": [{"filePath": p} for p in files],
        "targetPath": target_path,
    }) or {}

    if result.get("invalidSourcePaths"):
        raise ValueError(f"missing source paths: {result['invalidSourcePaths']}")

    if result.get("validTargetPath") is False:
        raise ValueError(f"target path not found: {target_path}")

    return result
Map<String, Object> validateRequest(InnorixClient call, String sourceId, String targetId,
                                    List<String> files, String targetPath) {
    List<Object> items = new ArrayList<>();
    // sourceItems reads filePath, not path
    for (String p : files) items.add(Json.newObj("filePath", p));

    Map<String, Object> result = call.apiObj("POST", "/api/transfers/validate-path", Json.newObj(
            "sourceId", sourceId,
            "targetId", targetId,
            "sourceItems", items,
            "targetPath", targetPath));

    if (!Json.arrOf(result, "invalidSourcePaths").isEmpty()) {
        throw new IllegalArgumentException("missing source paths: "
                + Json.arrOf(result, "invalidSourcePaths"));
    }

    if (Boolean.FALSE.equals(Json.boolOrNull(result, "validTargetPath"))) {
        throw new IllegalArgumentException("target path not found: " + targetPath);
    }

    return result;
}
async function validateRequest(call, sourceId, targetId, files, targetPath) {
  const result = (await call("POST", "/api/transfers/validate-path", {
    sourceId,
    targetId,
    // sourceItems reads filePath, not path
    sourceItems: files.map((p) => ({ filePath: p })),
    targetPath,
  })) || {};

  if (result.invalidSourcePaths?.length) {
    throw new Error(`missing source paths: ${result.invalidSourcePaths}`);
  }

  if (result.validTargetPath === false) {
    throw new Error(`target path not found: ${targetPath}`);
  }

  return result;
}
async Task<JsonObject> ValidateRequestAsync(Client call, string sourceId,
    string targetId, IEnumerable<string> files, string targetPath)
{
    var items = new JsonArray();
    // sourceItems reads filePath, not path
    foreach (string p in files) items.Add(new JsonObject { ["filePath"] = p });

    JsonObject result = await call.ApiObjAsync("POST", "/api/transfers/validate-path",
        new JsonObject
        {
            ["sourceId"] = sourceId,
            ["targetId"] = targetId,
            ["sourceItems"] = items,
            ["targetPath"] = targetPath,
        });

    if (J.ArrOf(result, "invalidSourcePaths").Count > 0)
    {
        throw new ArgumentException(
            
quot;missing source paths: {J.ArrOf(result, "invalidSourcePaths")}"
); } if (J.BoolOrNull(result, "validTargetPath") == false) { throw new ArgumentException(
quot;target path not found: {targetPath}"
); } return result; }
기록 항목 내용
userId 요청한 사용자
direction 반입 또는 반출
files 전송 대상 파일과 경로
purpose 업무 목적
state 현재 처리 단계

승인 연동#

승인 시스템의 결과를 전송 실행 조건으로 쓰기

승인 결과를 받으면 다음 단계로 넘깁니다.

def on_approval(request_id, approved, approver):
    request = db.get("transfer_requests", request_id)

    db.update("transfer_requests", request_id, {
        "state": "approved" if approved else "rejected",
        "approver": approver,
    })

    if not approved:
        return None

    return send_for_scan(request)


def ensure_approved(request):
    if request["state"] not in ("approved", "validated"):
        raise PermissionError(f"request not approved: {request['id']}")
String onApproval(String requestId, boolean approved, String approver) {
    Map<String, Object> request = db.get("transfer_requests", requestId);

    db.update("transfer_requests", requestId, Json.newObj(
            "state", approved ? "approved" : "rejected",
            "approver", approver));

    if (!approved) return null;

    return sendForScan(request);
}

void ensureApproved(Map<String, Object> request) {
    String state = Json.str(request, "state");
    if (!"approved".equals(state) && !"validated".equals(state)) {
        throw new SecurityException("request not approved: " + Json.str(request, "id"));
    }
}
async function onApproval(requestId, approved, approver) {
  const request = await db.get("transfer_requests", requestId);

  await db.update("transfer_requests", requestId, {
    state: approved ? "approved" : "rejected",
    approver,
  });

  if (!approved) return null;

  return sendForScan(request);
}

function ensureApproved(request) {
  if (!["approved", "validated"].includes(request.state)) {
    throw new Error(`request not approved: ${request.id}`);
  }
}
async Task<string> OnApprovalAsync(string requestId, bool approved, string approver)
{
    var request = await db.GetAsync("transfer_requests", requestId);

    await db.UpdateAsync("transfer_requests", requestId, new
    {
        state = approved ? "approved" : "rejected",
        approver,
    });

    if (!approved) return null;

    return await SendForScanAsync(request);
}

void EnsureApproved(JsonObject request)
{
    string state = J.Str(request, "state");
    if (state != "approved" && state != "validated")
    {
        throw new UnauthorizedAccessException(
quot;request not approved: {J.Str(request, "id")}"
); } }

승인되지 않은 요청이 실행되지 않도록, 전송을 만드는 함수는 반드시 요청 상태를 먼저 확인합니다.

검사 연계#

검사 폴더로 먼저 보내고 결과 기다리기

파일 검사는 별도 시스템이 수행합니다. 검사 대상 폴더로 먼저 보내고 결과를 받아 다음 단계를 결정합니다.

def send_for_scan(request, scan_device, scan_path):
    ensure_approved(request)

    transfer = external("POST", "/api/transfers/manual", {
        "sourceDevice": request["sourceDevice"],
        "targetDevice": scan_device,
        "targetPath": f"{scan_path}/{request['id']}",
        "sourceItem": [{"path": p, "isDir": False} for p in request["files"]],
        "sendAllFolder": False,
        "checkIntegrity": True,
        "transferOptions": {"target-action": "numbering"},
    })

    db.update("transfer_requests", request["id"], {
        "scanMonitorId": transfer["monitorId"],
        "state": "scanning",
    })

    return transfer["monitorId"]
String sendForScan(Map<String, Object> request, String scanDevice, String scanPath) {
    ensureApproved(request);

    List<Object> items = new ArrayList<>();
    for (Object p : Json.arrOf(request, "files")) {
        items.add(Json.newObj("path", p, "isDir", false));
    }

    Map<String, Object> transfer = external.apiObj("POST", "/api/transfers/manual", Json.newObj(
            "sourceDevice", Json.str(request, "sourceDevice"),
            "targetDevice", scanDevice,
            "targetPath", scanPath + "/" + Json.str(request, "id"),
            "sourceItem", items,
            "sendAllFolder", false,
            "checkIntegrity", true,
            "transferOptions", Json.newObj("target-action", "numbering")));

    db.update("transfer_requests", Json.str(request, "id"), Json.newObj(
            "scanMonitorId", Json.str(transfer, "monitorId"),
            "state", "scanning"));

    return Json.str(transfer, "monitorId");
}
async function sendForScan(request, scanDevice, scanPath) {
  ensureApproved(request);

  const transfer = await external("POST", "/api/transfers/manual", {
    sourceDevice: request.sourceDevice,
    targetDevice: scanDevice,
    targetPath: `${scanPath}/${request.id}`,
    sourceItem: request.files.map((p) => ({ path: p, isDir: false })),
    sendAllFolder: false,
    checkIntegrity: true,
    transferOptions: { "target-action": "numbering" },
  });

  await db.update("transfer_requests", request.id, {
    scanMonitorId: transfer.monitorId,
    state: "scanning",
  });

  return transfer.monitorId;
}
async Task<string> SendForScanAsync(JsonObject request, string scanDevice, string scanPath)
{
    EnsureApproved(request);

    var items = new JsonArray();
    foreach (JsonNode p in J.ArrOf(request, "files"))
    {
        items.Add(new JsonObject { ["path"] = p?.DeepClone(), ["isDir"] = false });
    }

    JsonObject transfer = await external.ApiObjAsync("POST", "/api/transfers/manual",
        new JsonObject
        {
            ["sourceDevice"] = J.Str(request, "sourceDevice"),
            ["targetDevice"] = scanDevice,
            ["targetPath"] = 
quot;{scanPath}/{J.Str(request, "id")}"
, ["sourceItem"] = items, ["sendAllFolder"] = false, ["checkIntegrity"] = true, ["transferOptions"] = new JsonObject { ["target-action"] = "numbering" }, }); await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { scanMonitorId = J.Str(transfer, "monitorId"), state = "scanning", }); return J.Str(transfer, "monitorId"); }

에이전트는 파일 크기 변화가 멈춰 쓰기가 끝난 뒤에 전송을 완료 처리합니다. 검사 시스템이 아직 기록 중인 파일을 열어 잘못된 결과를 내는 일은 생기지 않습니다.

전송 자체의 무결성은 검증 API로 확인합니다.

def verify(monitor_id, timeout=1800, interval=10):
    api("POST", f"/api/transfers/{monitor_id}/verification", {})

    deadline = time.time() + timeout

    while time.time() < deadline:
        result = api("GET", f"/api/transfers/{monitor_id}/verification") or {}

        if result.get("verified"):
            return result

        time.sleep(interval)

    raise TimeoutError(monitor_id)
public Map<String, Object> verify(String monitorId, int timeoutSeconds, int intervalSeconds) {
    api("POST", "/api/transfers/" + monitorId + "/verification", Json.newObj());

    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> result =
                apiObj("GET", "/api/transfers/" + monitorId + "/verification");

        if (Json.bool(result, "verified", false)) return result;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "verification timeout: " + monitorId);
}
export async function verify(monitorId, { timeout = 1800, interval = 10 } = {}) {
  await api("POST", `/api/transfers/${monitorId}/verification`, {});

  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const result = (await api("GET", `/api/transfers/${monitorId}/verification`)) || {};

    if (result.verified) return result;

    await sleep(interval * 1000);
  }

  throw new Error(`verification timeout: ${monitorId}`);
}
/// <summary>Start integrity verification, then poll until the result is ready.</summary>
public async Task<JsonObject> VerifyAsync(string monitorId, int timeoutSeconds = 1800,
    int intervalSeconds = 10)
{
    await ApiAsync("POST", "/api/transfers/" + monitorId + "/verification",
        new JsonObject()).ConfigureAwait(false);

    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject result = await ApiObjAsync(
            "GET", "/api/transfers/" + monitorId + "/verification").ConfigureAwait(false);

        if (J.Bool(result, "verified", false)) return result;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "verification timeout: " + monitorId);
}

반입·반출 실행#

검사를 통과한 파일만 대상 망으로 보내기

def execute(request, scan_result):
    if not scan_result.get("passed"):
        db.update("transfer_requests", request["id"], {"state": "blocked"})
        return None

    verification = verify(request["scanMonitorId"])

    if not verification.get("checksumMatched"):
        db.update("transfer_requests", request["id"], {"state": "corrupted"})
        return None

    transfer = internal("POST", "/api/transfers/manual", {
        "sourceDevice": request["scanDevice"],
        "targetDevice": request["targetDevice"],
        "targetPath": request["targetPath"],
        "sourcePaths": [f"{request['scanPath']}/{request['id']}"],
        "sendAllFolder": True,
        "checkIntegrity": True,
        "transferOptions": {"target-action": "numbering"},
    })

    db.update("transfer_requests", request["id"], {
        "monitorId": transfer["monitorId"],
        "state": "transferring",
    })

    return transfer["monitorId"]
String execute(Map<String, Object> request, Map<String, Object> scanResult) {
    if (!Json.bool(scanResult, "passed", false)) {
        db.update("transfer_requests", Json.str(request, "id"), Json.newObj("state", "blocked"));
        return null;
    }

    Map<String, Object> verification = internal.verify(Json.str(request, "scanMonitorId"), 1800, 10);

    if (!Json.bool(verification, "checksumMatched", false)) {
        db.update("transfer_requests", Json.str(request, "id"), Json.newObj("state", "corrupted"));
        return null;
    }

    Map<String, Object> transfer = internal.apiObj("POST", "/api/transfers/manual", Json.newObj(
            "sourceDevice", Json.str(request, "scanDevice"),
            "targetDevice", Json.str(request, "targetDevice"),
            "targetPath", Json.str(request, "targetPath"),
            "sourcePaths", List.of(Json.str(request, "scanPath") + "/" + Json.str(request, "id")),
            "sendAllFolder", true,
            "checkIntegrity", true,
            "transferOptions", Json.newObj("target-action", "numbering")));

    db.update("transfer_requests", Json.str(request, "id"), Json.newObj(
            "monitorId", Json.str(transfer, "monitorId"),
            "state", "transferring"));

    return Json.str(transfer, "monitorId");
}
async function execute(request, scanResult) {
  if (!scanResult.passed) {
    await db.update("transfer_requests", request.id, { state: "blocked" });
    return null;
  }

  const verification = await verify(request.scanMonitorId);

  if (!verification.checksumMatched) {
    await db.update("transfer_requests", request.id, { state: "corrupted" });
    return null;
  }

  const transfer = await internal("POST", "/api/transfers/manual", {
    sourceDevice: request.scanDevice,
    targetDevice: request.targetDevice,
    targetPath: request.targetPath,
    sourcePaths: [`${request.scanPath}/${request.id}`],
    sendAllFolder: true,
    checkIntegrity: true,
    transferOptions: { "target-action": "numbering" },
  });

  await db.update("transfer_requests", request.id, {
    monitorId: transfer.monitorId,
    state: "transferring",
  });

  return transfer.monitorId;
}
async Task<string> ExecuteAsync(JsonObject request, JsonObject scanResult)
{
    if (!J.Bool(scanResult, "passed", false))
    {
        await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { state = "blocked" });
        return null;
    }

    JsonObject verification = await internal.VerifyAsync(J.Str(request, "scanMonitorId"));

    if (!J.Bool(verification, "checksumMatched", false))
    {
        await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { state = "corrupted" });
        return null;
    }

    JsonObject transfer = await internal.ApiObjAsync("POST", "/api/transfers/manual",
        new JsonObject
        {
            ["sourceDevice"] = J.Str(request, "scanDevice"),
            ["targetDevice"] = J.Str(request, "targetDevice"),
            ["targetPath"] = J.Str(request, "targetPath"),
            ["sourcePaths"] = new JsonArray { 
quot;{J.Str(request, "scanPath")}/{J.Str(request, "id")}"
}, ["sendAllFolder"] = true, ["checkIntegrity"] = true, ["transferOptions"] = new JsonObject { ["target-action"] = "numbering" }, }); await db.UpdateAsync("transfer_requests", J.Str(request, "id"), new { monitorId = J.Str(transfer, "monitorId"), state = "transferring", }); return J.Str(transfer, "monitorId"); }

각 단계에서 상태를 기록해 두면 어느 지점에서 멈췄는지 나중에 확인할 수 있습니다.

상태 의미
pending 승인 대기
approved 승인 완료
scanning 검사 진행 중
blocked 검사 미통과
corrupted 무결성 불일치
transferring 대상 망으로 전송 중
done · failed 최종 결과

결과 확정#

전송 결과를 요청 상태에 반영하기

def wait(monitor_id, timeout=3600, interval=3):
    deadline = time.time() + timeout

    while time.time() < deadline:
        detail = api("GET", f"/api/transfers/{monitor_id}")

        if is_terminal(detail):
            return detail

        time.sleep(interval)

    raise TimeoutError(monitor_id)


def failed_files(monitor_id):
    result = api("GET", f"/api/transfers/{monitor_id}/files", params={
        "state": "any", "size": 500,
    }) or {}

    return [r for r in (result.get("children") or [])
            if r.get("status") in NOT_SUCCEEDED]


def retry_failed(monitor_id):
    rows = failed_files(monitor_id)

    if not rows:
        return 0

    api("POST", f"/api/transfers/{monitor_id}/retry", {
        "filesRetry": [
            {"filePath": r["sourceFilePath"], "isDir": bool(r.get("isFolder"))}
            for r in rows
        ]
    })

    return len(rows)
public Map<String, Object> wait(String monitorId, int timeoutSeconds, int intervalSeconds) {
    long deadline = System.currentTimeMillis() + timeoutSeconds * 1000L;

    while (System.currentTimeMillis() < deadline) {
        Map<String, Object> detail = apiObj("GET", "/api/transfers/" + monitorId);

        if (isTerminal(detail)) return detail;

        sleep(intervalSeconds * 1000L);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public List<Map<String, Object>> failedFiles(String monitorId) {
    List<Map<String, Object>> failed = new ArrayList<>();

    for (Map<String, Object> row : transferFiles(monitorId)) {
        Integer status = Json.intOrNull(row, "status");
        if (status != null && NOT_SUCCEEDED.contains(status)) failed.add(row);
    }

    return failed;
}

public int retryFailed(String monitorId) {
    List<Map<String, Object>> rows = failedFiles(monitorId);
    if (rows.isEmpty()) return 0;

    List<Object> filesRetry = new ArrayList<>();

    for (Map<String, Object> row : rows) {
        String path = Json.str(row, "sourceFilePath");
        if (path == null) continue;
        filesRetry.add(Json.newObj("filePath", path, "isDir", Json.bool(row, "isFolder", false)));
    }

    api("POST", "/api/transfers/" + monitorId + "/retry", Json.newObj("filesRetry", filesRetry));

    return rows.size();
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function wait(monitorId, { timeout = 3600, interval = 3 } = {}) {
  const deadline = Date.now() + timeout * 1000;

  while (Date.now() < deadline) {
    const detail = await api("GET", `/api/transfers/${monitorId}`);

    if (isTerminal(detail)) return detail;

    await sleep(interval * 1000);
  }

  throw new Error(`timeout waiting for ${monitorId}`);
}

export async function failedFiles(monitorId) {
  const result = (await api("GET", `/api/transfers/${monitorId}/files`, null, {
    state: "any", size: 500,
  })) || {};

  return (result.children || []).filter((row) => NOT_SUCCEEDED.has(row.status));
}

export async function retryFailed(monitorId) {
  const rows = await failedFiles(monitorId);

  if (rows.length === 0) return 0;

  await api("POST", `/api/transfers/${monitorId}/retry`, {
    filesRetry: rows
      .filter((row) => row.sourceFilePath)
      .map((row) => ({ filePath: row.sourceFilePath, isDir: Boolean(row.isFolder) })),
  });

  return rows.length;
}
public async Task<JsonObject> WaitAsync(string monitorId, int timeoutSeconds = 3600,
    int intervalSeconds = 3)
{
    long deadline = Environment.TickCount64 + timeoutSeconds * 1000L;

    while (Environment.TickCount64 < deadline)
    {
        JsonObject detail = await ApiObjAsync("GET", "/api/transfers/" + monitorId)
            .ConfigureAwait(false);

        if (IsTerminal(detail)) return detail;

        await Task.Delay(intervalSeconds * 1000).ConfigureAwait(false);
    }

    throw new ApiError(0, "timeout waiting for " + monitorId);
}

public async Task<List<JsonObject>> FailedFilesAsync(string monitorId)
{
    List<JsonObject> rows = await TransferFilesAsync(monitorId).ConfigureAwait(false);

    return rows.Where(row =>
    {
        int? status = J.IntOrNull(row, "status");
        return status != null && NotSucceeded.Contains(status.Value);
    }).ToList();
}

/// <summary>Only callable once the transfer has reached a terminal state.</summary>
public async Task<int> RetryFailedAsync(string monitorId)
{
    List<JsonObject> rows = await FailedFilesAsync(monitorId).ConfigureAwait(false);
    if (rows.Count == 0) return 0;

    var filesRetry = new JsonArray();

    foreach (JsonObject row in rows)
    {
        string path = J.Str(row, "sourceFilePath");
        if (path == null) continue;

        filesRetry.Add(new JsonObject
        {
            ["filePath"] = path,
            ["isDir"] = J.Bool(row, "isFolder", false),
        });
    }

    await ApiAsync("POST", "/api/transfers/" + monitorId + "/retry",
        new JsonObject { ["filesRetry"] = filesRetry }).ConfigureAwait(false);

    return rows.Count;
}
def finalize(request_id):
    request = db.get("transfer_requests", request_id)
    detail = internal("GET", f"/api/transfers/{request['monitorId']}")

    if not is_terminal(detail):
        return None

    succeeded = detail["status"] == STATUS_COMPLETE

    db.update("transfer_requests", request_id, {
        "state": "done" if succeeded else "failed",
        "finalStatus": detail["status"],
    })

    return succeeded
Boolean finalize(String requestId) {
    Map<String, Object> request = db.get("transfer_requests", requestId);
    Map<String, Object> detail = internal.apiObj("GET",
            "/api/transfers/" + Json.str(request, "monitorId"));

    if (!InnorixClient.isTerminal(detail)) return null;

    boolean succeeded = Json.intOr(detail, "status", -1) == InnorixClient.STATUS_COMPLETE;

    db.update("transfer_requests", requestId, Json.newObj(
            "state", succeeded ? "done" : "failed",
            "finalStatus", Json.intOrNull(detail, "status")));

    return succeeded;
}
async function finalize(requestId) {
  const request = await db.get("transfer_requests", requestId);
  const detail = await internal("GET", `/api/transfers/${request.monitorId}`);

  if (!isTerminal(detail)) return null;

  const succeeded = detail.status === STATUS_COMPLETE;

  await db.update("transfer_requests", requestId, {
    state: succeeded ? "done" : "failed",
    finalStatus: detail.status,
  });

  return succeeded;
}
async Task<bool?> FinalizeAsync(string requestId)
{
    var request = await db.GetAsync("transfer_requests", requestId);
    JsonObject detail = await internal.ApiObjAsync("GET",
        
quot;/api/transfers/{J.Str(request, "monitorId")}"
); if (!InnorixClient.IsTerminal(detail)) return null; bool succeeded = J.Int(detail, "status", -1) == InnorixClient.StatusComplete; await db.UpdateAsync("transfer_requests", requestId, new { state = succeeded ? "done" : "failed", finalStatus = J.IntOrNull(detail, "status"), }); return succeeded; }

부분 완료(9)와 취소(5)도 종료 상태입니다. 종료됐다고 성공 처리하면 일부만 넘어간 요청이 완료로 기록됩니다.

이력 조회#

감사 대응용으로 반입·반출 기록 확인하기

전송 이력은 어떤 파일이 어느 방향으로 처리됐는지 확인하는 근거가 됩니다.

from datetime import datetime, timedelta, timezone


def paginate(call, path, params=None, limit=200, max_pages=50):
    query = dict(params or {})
    query["limit"] = limit
    cursor = None

    for _ in range(max_pages):
        if cursor:
            query["cursor"] = cursor

        result = call("GET", path, params=query) or {}

        for record in result.get("data") or []:
            yield record

        pagination = result.get("pagination") or {}

        if not pagination.get("hasMore"):
            return

        cursor = pagination.get("nextCursor")

        if not cursor:
            return


end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"

for row in paginate(internal, "/api/transfer-history", params={
    "startDate": (end - timedelta(days=30)).strftime(fmt),
    "endDate": end.strftime(fmt),
}):
    print(row.get("startDate"), row.get("statusName"),
          row.get("sourceDeviceName"), "->", row.get("targetDeviceName"))
Instant end = Instant.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
        .withZone(ZoneOffset.UTC);

List<Map<String, Object>> rows = internal.paginate("/api/transfer-history",
        Json.newObj("startDate", fmt.format(end.minus(30, ChronoUnit.DAYS)),
                    "endDate", fmt.format(end)), 200, 50);

for (Map<String, Object> row : rows) {
    System.out.println(Json.str(row, "startDate") + " "
            + Json.str(row, "statusName") + " "
            + Json.str(row, "sourceDeviceName") + " -> "
            + Json.str(row, "targetDeviceName"));
}
const end = new Date();
const fmt = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");

for await (const row of paginate(internal, "/api/transfer-history", {
  startDate: fmt(new Date(end.getTime() - 30 * 86400000)),
  endDate: fmt(end),
})) {
  console.log(row.startDate, row.statusName,
    row.sourceDeviceName, "->", row.targetDeviceName);
}
DateTime end = DateTime.UtcNow;
const string Fmt = "yyyy-MM-dd'T'HH:mm:ss'Z'";

List<JsonObject> rows = await internal.PaginateAsync("/api/transfer-history",
    new Dictionary<string, object>
    {
        ["startDate"] = end.AddDays(-30).ToString(Fmt),
        ["endDate"] = end.ToString(Fmt),
    });

foreach (JsonObject row in rows)
{
    Console.WriteLine(
quot;{J.Str(row, "startDate")} {J.Str(row, "statusName")} "
+
quot;{J.Str(row, "sourceDeviceName")} -> {J.Str(row, "targetDeviceName")}"
); }

감사 제출용으로 파일이 필요하면 CSV 내보내기를 씁니다.

params = {
    "periodDays": 30,
    "page": 1,
    "size": 10000,
    "filter": "[]",              # the server parses this as a JSON string, so send an empty array
    "sort": "startDate:desc",
}
Map<String, Object> params = Json.newObj(
        "periodDays", 30,
        "page", 1,
        "size", 10000,
        "filter", "[]",              // the server parses this as a JSON string, so send an empty array
        "sort", "startDate:desc");
const params = {
  periodDays: 30,
  page: 1,
  size: 10000,
  filter: "[]",              // the server parses this as a JSON string, so send an empty array
  sort: "startDate:desc",
};
var parameters = new Dictionary<string, object>
{
    ["periodDays"] = 30,
    ["page"] = 1,
    ["size"] = 10000,
    ["filter"] = "[]",              // the server parses this as a JSON string, so send an empty array
    ["sort"] = "startDate:desc",
};

전송의 monitorId와 업무 요청의 식별자를 함께 기록해 두면 실행 기록에서 요청까지, 요청에서 실행 기록까지 양방향으로 찾을 수 있습니다.

예외 대응#

중단된 요청을 단계별로 확인하고 재처리하기

검사 미통과와 전송 실패는 대응이 다릅니다. 전자는 파일 자체의 문제라 재전송으로 해결되지 않고, 후자는 재전송으로 회복됩니다.

def review(request_id):
    request = db.get("transfer_requests", request_id)
    state = request["state"]

    if state == "rejected":
        return "rejected - notify the requester with the reason"

    if state == "blocked":
        return "scan blocked - check the files and request again"

    if state == "corrupted":
        return "integrity mismatch - check the source"

    if state == "transferring":
        return f"retried {retry_failed(request['monitorId'])} failed files"

    return f"current state: {state}"
String review(String requestId) {
    Map<String, Object> request = db.get("transfer_requests", requestId);
    String state = Json.str(request, "state");

    return switch (state) {
        case "rejected" -> "rejected - notify the requester with the reason";
        case "blocked" -> "scan blocked - check the files and request again";
        case "corrupted" -> "integrity mismatch - check the source";
        case "transferring" -> "retried "
                + internal.retryFailed(Json.str(request, "monitorId")) + " failed files";
        default -> "current state: " + state;
    };
}
async function review(requestId) {
  const request = await db.get("transfer_requests", requestId);
  const { state } = request;

  if (state === "rejected") return "rejected - notify the requester with the reason";
  if (state === "blocked") return "scan blocked - check the files and request again";
  if (state === "corrupted") return "integrity mismatch - check the source";
  if (state === "transferring") {
    return `retried ${await retryFailed(request.monitorId)} failed files`;
  }
  return `current state: ${state}`;
}
async Task<string> ReviewAsync(string requestId)
{
    var request = await db.GetAsync("transfer_requests", requestId);
    string state = J.Str(request, "state");

    return state switch
    {
        "rejected" => "rejected - notify the requester with the reason",
        "blocked" => "scan blocked - check the files and request again",
        "corrupted" => "integrity mismatch - check the source",
        "transferring" => 
quot;retried {await internal.RetryFailedAsync(J.Str(request, "monitorId"))} failed files"
, _ =>
quot;current state: {state}"
, }; }
구분 증상 대응
승인 거절 rejected 요청자에게 사유 전달
검사 미통과 blocked 파일 확인 후 재요청
무결성 불일치 corrupted 원본 상태 확인
전송 실패 failed 실패한 파일 재전송
확인 항목 확인 내용
요청 승인 상태와 대상 파일
검사 검사 폴더 전송과 결과
무결성 파일 수와 체크섬 일치 여부
실행 대상 망 전송 상태
이력 감사 대응용 전송 기록
이전AI 모델 파일을 여러 엣지 장비에 배포하기다음승인된 파일을 여러 지점에 동시에 배포하기

이 페이지에서

  • 시작하기
  • 기본 개념
  • 전송 흐름
  • 운영 효과
  • IT 엔지니어
  • 환경 구성
  • 승인·검증 설정
  • 반입·반출 흐름
  • 이력 확인
  • 보안 운영
  • 운영 대응
  • 개발자
  • 연동 준비
  • 요청 등록
  • 승인 연동
  • 검사 연계
  • 반입·반출 실행
  • 결과 확정
  • 이력 조회
  • 예외 대응