시작하기
기본 개념
우리 업무 환경과 협력사 사이에서 파일을 양방향으로 교환하기
협력사와 진행하는 업무에서는 주문서와 설계 자료처럼 협력사로 보내는 파일과 검사 결과, 정산 자료처럼 협력사에서 받는 파일이 함께 사용됩니다.
파일 교환은 우리 업무 환경과 협력사 시스템을 연결하고, 송신과 수신 파일을 각각의 경로와 조건에 따라 관리하는 방식입니다.
우리 업무 환경
│
주문서 · 설계 자료 │
▼
┌────────────────┐
│ File Exchange │
└────────────────┘
▲
검사 결과 · 정산 자료 │
│
협력사 시스템

협력사별로 교환할 파일과 경로를 구성하면 주문, 설계, 정산, 검사 등 업무에 맞는 양방향 파일 흐름을 관리할 수 있습니다.
교환 흐름
파일을 보내고 결과 파일을 받아 다음 업무까지 이어가기
내부에서 생성한 업무 파일은 협력사로 전송하고, 협력사에서 처리한 결과 파일은 다시 수신해 다음 업무에 활용합니다.
업무 파일 준비
│
▼
협력사로 전송
│
▼
협력사 업무 처리
│
▼
결과 파일 수신
│
▼
내부 후속 업무

송신과 수신을 하나의 업무 흐름으로 연결하면 파일이 협력사와 내부 업무 사이를 오가는 과정을 함께 관리할 수 있습니다.
업무 효과
협력사별 파일 교환을 정해진 업무 흐름으로 관리하기
협력사마다 필요한 파일과 업무가 다르더라도 협력사별 파일과 교환 경로를 구분해 업무에 맞는 흐름으로 활용할 수 있습니다.
| 업무 | 협력사로 전송 | 협력사에서 수신 | 활용 업무 |
|---|---|---|---|
| 주문 | 주문서 · 발주 자료 | 주문 결과 | 주문 처리 |
| 정산 | 정산 요청 자료 | 정산 결과 | 정산 확인 |
| 설계 | 설계 자료 | 수정 · 검토 자료 | 설계 검토 |
| 검사 | 검사 요청 자료 | 검사 결과 | 품질 확인 |
업무팀
파일 교환
업무에 필요한 파일을 선택해 협력사와 주고받기
업무팀은 주문, 정산, 설계, 검사 등 현재 진행하는 업무에 필요한 파일을 선택해 협력사별 파일 교환 작업을 실행합니다.
파일 선택
│
▼
협력사 선택
│
▼
파일 전송
│
▼
결과 파일 수신
수신된 파일은 검토, 수정, 승인 등 다음 업무에서 바로 활용할 수 있습니다.
교환 현황
협력사별 파일 교환 상태와 결과 확인하기
여러 협력사와 업무를 진행하는 경우 협력사별로 보낸 파일과 받은 파일의 처리 상태를 확인할 수 있습니다.
| 협력사 | 송신 파일 | 수신 파일 | 진행 상태 |
|---|---|---|---|
| 협력사 A | 주문서 | 주문 결과 | 완료 |
| 협력사 B | 설계 자료 | 수정 자료 | 진행 중 |
| 협력사 C | 검사 요청 | 검사 결과 | 완료 |

이를 통해 업무팀은 현재 어떤 협력사와 파일 교환이 진행되고 있는지 확인하고, 수신된 결과를 다음 업무에 활용할 수 있습니다.
IT 엔지니어
협력사 연결
협력사 시스템과 내부 파일 교환 환경 연결하기
먼저 파일을 교환할 협력사 시스템과 내부 파일 전송 환경을 연결합니다.
여러 협력사와 파일을 교환하는 경우 각 협력사를 개별 연결 대상으로 구성할 수 있습니다.
File Exchange
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Partner A Partner B Partner C
↕ ↕ ↕
주문 업무 설계 업무 정산 업무

교환 설정
송신·수신 경로와 실행 기준을 하나의 흐름으로 구성하기
협력사별로 내부에서 보낼 파일의 경로와 협력사에서 받을 파일의 경로를 설정합니다.
파일 이름과 경로, 유형을 기준으로 교환 대상을 구성하고, 정해진 시간이나 파일 준비·도착 조건에 따라 작업을 시작하도록 설정할 수 있습니다.
우리 시스템 협력사 시스템
/send/orders ───────── 전송 ────────► /receive/orders
/receive/results ◄───── 수신 ──────── /send/results
| 구분 | 우리 시스템 | 협력사 시스템 | 실행 기준 |
|---|---|---|---|
| 송신 | /send/orders | /receive/orders | 일정 · 파일 준비 |
| 수신 | /receive/results | /send/results | 일정 · 파일 도착 |

송신과 수신 작업을 함께 구성하면 협력사와의 파일 교환을 하나의 양방향 흐름으로 관리할 수 있습니다.
교환 흐름
여러 협력사의 파일 교환 작업을 업무별로 연결하기
각 협력사와의 파일 교환은 송신과 수신 작업을 조합해 하나의 업무 흐름으로 구성할 수 있습니다.
┌───────────────┐
│ 협력사별 파일 흐름 │
└───────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Partner A Partner B Partner C
│ │ │
주문 교환 설계 교환 정산 교환
│ │ │
└──────────┼──────────┘
▼
내부 후속 업무
운영 확인
협력사별 송신과 수신 결과를 함께 관리하기
Runs와 상세 실행 정보를 통해 각 협력사의 파일 교환 작업과 처리 결과를 확인할 수 있습니다.
송신과 수신 결과를 협력사별로 확인하면 현재 진행 중인 작업과 완료된 파일 교환 현황을 함께 관리할 수 있습니다.
Partner A
├── Send → Completed
└── Receive → Completed
Partner B
├── Send → Completed
└── Receive → In Progress

작업 중 확인이 필요한 경우 실행 상세 정보와 처리 기록을 기준으로 연결 상태와 파일 경로, 처리 결과를 확인한 후 필요한 작업을 다시 실행할 수 있습니다.
개발자
협력사 장비로 파일을 보내고 협력사가 올린 파일을 감시로 가져오기
연동 준비
공통 호출 코드와 경로 표기 준비하기
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)import base64
import time
def encode_path(device_id, raw_path):
normalized = str(raw_path or "").replace("\\", "/")
token = base64.b64encode(normalized.encode("utf-8")).decode("ascii")
return f"{device_id}_ino_{token}"
def now_iso():
return time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())전송 상태는 아래 값으로 판단합니다. 종료 상태는 다섯 개이고 성공에 해당하는 값은 완료(2)입니다.
| 상태 값 | 의미 | 종료 |
|---|---|---|
| 2 | 완료 | 예 |
| 4 | 오류 | 예 |
| 5 | 취소 | 예 |
| 9 | 부분 완료 | 예 |
| 99 | 실패 | 예 |
| 1 · 6 · 12 · 13 | 시작·전송 중·동기화 중·수신 중 | 아니오 |
협력사 구성
협력사별 장비와 경로를 데이터로 관리하기
협력사마다 경로와 업무가 다릅니다. 설정을 데이터로 두면 협력사가 늘어도 코드를 고치지 않습니다.
PARTNERS = {
"partner-a": {
"device": "device-partner-a",
"send": {"local": "/send/orders", "remote": "/receive/orders"},
"receive": {"remote": "/send/results", "local": "/receive/results"},
},
"partner-b": {
"device": "device-partner-b",
"send": {"local": "/send/design", "remote": "/receive/design"},
"receive": {"remote": "/send/review", "local": "/receive/review"},
},
}
INTERNAL = "device-hq-01"작업 공간을 협력사별로 나눠 쓴다면 요청마다 작업 공간 식별자를 함께 보냅니다. 한 협력사의 실수로 다른 협력사의 파일에 접근하는 상황을 막습니다.
파일 송신
내부 파일을 협력사 장비로 보내기
파일을 보낼 때는 sourceItem에 isDir: false로 명시합니다. sourcePaths는 모든 경로를 폴더로 취급합니다.
import os
def send_to_partner(partner_id, files):
partner = PARTNERS[partner_id]
transfer = api("POST", "/api/transfers/manual", {
"sourceDevice": INTERNAL,
"targetDevice": partner["device"],
"targetPath": partner["send"]["remote"],
"sourceItem": [{"path": p, "isDir": False} for p in files],
"sendAllFolder": False,
"transferOptions": {"target-action": "numbering"},
})
return transfer["monitorId"]
monitor_id = send_to_partner("partner-a", ["/send/orders/PO-2026-0901.xlsx"])폴더 단위로 보낼 때는 sourcePaths와 sendAllFolder: True를 씁니다.
협력사로 보내는 파일은 회차를 남겨야 하므로 numbering을 씁니다. 같은 주문서를 수정해 다시 전송하는 경우 이전 본이 사라지면 어느 것을 기준으로 처리했는지 확인할 수 없습니다.
결과 파일 수신
협력사가 올린 파일을 감지해 가져오기
협력사가 언제 파일을 올릴지 알 수 없으므로 실시간 감시 자동화로 구성합니다. transferType을 sync로 두고 transferOptions에 watchFolderType을 넣으면 파일이 도착하는 즉시 전송됩니다.
def create_receive_watch(partner_id, webhook=None):
partner = PARTNERS[partner_id]
remote = partner["receive"]["remote"]
local = f"{partner['receive']['local']}/{partner_id}"
name = f"receive {partner_id}"
body = {
"name": name,
"flowName": name,
"transferType": "sync",
"timezone": "Asia/Seoul",
"step": 1,
"isUpcoming": False,
"details": [
{
"senderId": partner["device"],
"receiverId": INTERNAL,
"sourceItem": [
{
"hash": encode_path(partner["device"], remote),
"filePath": remote,
"isDir": True,
}
],
"targetPath": encode_path(INTERNAL, local),
"step": 1,
"transferOptions": {
"noSchedule": True,
"target-action": "numbering",
"send-fileoption": {},
"syncType": 1,
"watchFolderType": 1, # 1 = on create, 2 = on modify
},
}
],
"schedules": [
{"type": "none", "startDateType": "now",
"startDate": now_iso(), "timezone": "Asia/Seoul"}
],
}
if webhook:
body["processors"] = [{
"category": "run",
"type": "http",
"config": {"url": webhook, "method": "POST"},
}]
return api("POST", "/api/automations", body)["automationId"]
receivers = {p: create_receive_watch(p, RECEIVE_HOOK) for p in PARTNERS}자동화 요청에서 반드시 지켜야 하는 항목이 네 개 있습니다.
| 항목 | 지정 방식 |
|---|---|
isUpcoming | 반드시 false. 서버 기본값 true는 요청에 담긴 일정을 무시하고 5분짜리 일회성 일정으로 대체합니다. triggerAutomation이 붙은 단계는 서버가 false로 강제하므로, 트리거가 없는 첫 단계에만 직접 지정하면 됩니다 |
step | 최상위와 details 양쪽에 넣습니다. 흐름 안에서의 홉 위치입니다 |
sourceItem | hash(경로 토큰)와 filePath(평문 경로)를 함께 넣습니다 |
syncType | transferOptions 안에 넣습니다. 1은 단방향, 2는 양방향입니다 |
네 항목 모두 누락해도 등록은 성공하고 실행 시점에 동작이 달라집니다. 반복 일정을 등록했는데 한 번만 실행되고 끝났다면 isUpcoming부터 확인합니다.
도착 경로에 협력사 식별자를 넣습니다. 협력사마다 result.xlsx처럼 이름이 같으므로 한 경로로 모으면 출처를 구분할 수 없습니다.
에이전트는 파일 크기 변화가 멈추면 쓰기 완료로 판단하고 이벤트를 전달합니다. 협력사가 큰 파일을 올리는 중에 잘린 파일을 가져오는 일은 생기지 않습니다.
수신 후 처리 연결
결과 파일이 도착하면 내부 업무 시작하기
호출은 전송 완료 후에 오며, 그 요청을 받는 엔드포인트는 다음과 같이 처리합니다.
def on_receive_hook(payload):
monitor_id = payload.get("monitorId")
if monitor_id:
detail = api("GET", f"/api/transfers/{monitor_id}")
if detail["status"] != STATUS_COMPLETE:
return notify_partner_failure(payload)
register_received_files(payload)같은 전송에 대해 알림이 여러 번 올 수 있으므로, 받는 쪽은 같은 이벤트를 여러 번 받아도 한 번만 처리되도록 만듭니다.
교환 현황 조회
협력사별 송신과 수신 상태 확인하기
from datetime import datetime, timedelta, timezone
def paginate(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 = api("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
def partner_summary(partner_id, days=7):
partner = PARTNERS[partner_id]
end = datetime.now(timezone.utc)
fmt = "%Y-%m-%dT%H:%M:%SZ"
rows = list(paginate(
f"/api/devices/{partner['device']}/transfer-history", params={
"startDate": (end - timedelta(days=days)).strftime(fmt),
"endDate": end.strftime(fmt),
}))
return {
"sent": len([r for r in rows if r.get("sourceDeviceName") == INTERNAL]),
"received": len([r for r in rows if r.get("targetDeviceName") == INTERNAL]),
"failed": len([r for r in rows if r.get("status") in NOT_SUCCEEDED]),
"rows": rows,
}
for partner_id in PARTNERS:
summary = partner_summary(partner_id)
print(f"{partner_id:14} sent {summary['sent']:>3}"
f" received {summary['received']:>3} failed {summary['failed']:>3}")전송 이력은 data.data 배열로, 페이징 정보는 data.pagination으로 반환됩니다.
예외 대응
전송 실패와 미수신을 구분해 처리하기
전송이 실패한 것과 협력사가 아직 파일을 올리지 않은 것은 다릅니다. 대응도 달라집니다.
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)def check_partner(partner_id, days=1):
partner = PARTNERS[partner_id]
summary = partner_summary(partner_id, days=days)
state = api("GET", f"/api/devices/{partner['device']}/connectivity") or {}
if not state.get("isConnected"):
return f"connection lost ({state.get('stateLabel')}) - recollect after recovery"
if summary["failed"]:
count = 0
for row in summary["rows"]:
if row.get("status") in NOT_SUCCEEDED and row.get("monitorId"):
count += retry_failed(row["monitorId"])
return f"transfer failed - retried {count} files"
if summary["received"] == 0:
return "nothing received - ask the partner"
return "ok"| 구분 | 증상 | 대응 |
|---|---|---|
| 전송 실패 | 전송 이력에 실패 상태 | 실패한 파일 재전송 |
| 미수신 | 전송 이력 자체가 없음 | 협력사에 확인 요청 |
| 연결 끊김 | isConnected가 거짓 | 연결 복구 후 밀린 파일 수집 |
| 확인 항목 | 확인 내용 |
|---|---|
| 송신 | 내부에서 협력사로 보낸 전송 |
| 수신 | 감시 자동화로 가져온 전송 |
| 도착 경로 | 협력사 식별자로 구분된 저장 위치 |
| 상태 | 전송 상태와 성공 여부 |
| 연결 | 협력사 장비의 연결 상태 |