// back-office

월말 정산 자동 적재

매달 같은 자료를 손으로 내려받아 정산 시스템에 넣고 있다면. 월말 입출금·승인내역을 회사 내부 시스템에 자동으로 채웁니다.

// 상황

이런 상황이라면

매달 같은 다운로드

월말마다 은행과 카드에서 같은 자료를 손으로 내려받아 정산 시스템에 붙입니다.

붙여 넣다 생기는 실수

수기 입력은 누락·중복·오타를 부릅니다.

마감이 사람에 묶인다

담당자가 자리에 없으면 마감이 밀립니다.

// 흐름

어떻게 동작하나

  1. 01

    자격증명 등록

    정산에 쓰는 은행·법인카드 자격증명을 등록합니다.

  2. 02

    월말 자동 수집

    스케줄에 맞춰 지난달 입출금·승인내역을 받습니다.

  3. 03

    정산 시스템에 적재

    표준 데이터 형식으로 온 결과를 내부 정산 DB·어드민에 그대로 넣습니다.

// schemas

사용 데이터 형식

이 시나리오에서 받는 데이터입니다. 기관이 달라도 같은 모양으로 옵니다. 필드 전체 명세는 각 데이터 형식 문서에서 확인할 수 있습니다.

// 결과물

손에 남는 것

  • 월말 입출금·카드 승인내역 자동 적재
  • 수기 다운로드·붙여넣기 제거
  • 정해진 시각에 빠짐없이 실행
  • 담당자 부재와 무관한 마감

// 실행 가이드

그대로 따라 하면 됩니다

사전 점검부터 검증까지 이 페이지에서 끝납니다. 실행 방식이 여럿이면 하나만 고르면 됩니다.

01사전 점검

아래 명령을 붙여 넣어 현재 환경이 준비됐는지 확인합니다.

  • 어드민 백엔드 환경에 H6S_API_KEY 가 들어 있다

    echo $H6S_API_KEY

    h6s_live_... 로 시작하는 키가 출력된다

    대안으로 콘솔(https://h6s.ai)에서 발급 → 어드민 백엔드의 secret 매니저(.env · k8s secret · AWS Secrets Manager 등)에 등록

  • 워크스페이스에 은행·홈택스 자격증명이 매칭돼 있다

    h6s credentials list

    은행(CB_*) 1건 + 홈택스(HOMETAX) 1건이 보인다

    대안으로 h6s credentials create --interactive --cert

  • 어드민 백엔드에 incoming 엔드포인트가 준비돼 있다

    curl -s -o /dev/null -w "%{http_code}" -X POST $ADMIN_INGEST_URL -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"schema":"bank.transactions.cb.v1","month":"2026-04","records":[]}'

    200 또는 204

    대안으로 어드민 백엔드에 schema / month / records 를 받는 라우트 추가 (예: NestJS @Post('settlement/incoming') · Next.js route handler · Spring @PostMapping)

02실행

어드민 백엔드의 cron 핸들러가 매월 1일 09:00 KST 에 실행되는 시퀀스입니다. data-job 생성 → polling → 결과 수신 → 어드민 incoming 엔드포인트 POST 순서로 진행됩니다.

terminal
# 1) 어드민 백엔드의 스케줄러가 매월 1일 트리거.
#    (자체 cron · k8s CronJob · EventBridge · 백엔드 background job 어느 쪽이든)

# 2) 4개 schema 의 data-job 을 만든다. 아래는 첫 호출 예시.
curl -s -X POST https://api.h6s.ai/api/v1/data-jobs \
  -H "Authorization: Bearer $H6S_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"providerCode":"CB_KB","schema":"bank.transactions.cb.v1","params":{"dateRangeStart":"2026-04-01","dateRangeEnd":"2026-04-30"}}'
# 같은 호출을 hometax.tax-invoices.sales.v1 / .purchase.v1 / hometax.cash-receipts.sales.v1 까지 반복 (총 4건).

# 3) 각 응답의 id 로 status 가 SUCCEEDED 될 때까지 polling.
curl -s https://api.h6s.ai/api/v1/data-jobs/$JOB_ID \
  -H "Authorization: Bearer $H6S_API_KEY"

# 4) 결과(ContractRecord 배열) 수신.
curl -s https://api.h6s.ai/api/v1/data-jobs/$JOB_ID/results \
  -H "Authorization: Bearer $H6S_API_KEY"

# 5) 받은 records 를 그대로 자체 어드민 백엔드로 POST.
#    도메인 검증·중복 제거·정산 매칭은 어드민이 책임진다.
curl -s -X POST $ADMIN_INGEST_URL \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@results.json"

위 시퀀스의 한 파일 구현체입니다. Next.js admin 의 route handler 또는 NestJS @Cron 데코레이터 안에서 호출할 수 있습니다.

terminal
// monthly-ingest.mjs 파일. 어드민 백엔드의 매월 1일 cron 핸들러.
// Next.js admin route, NestJS @Cron, k8s CronJob 어디서 호출해도 동작한다.

const H6S_API_KEY = process.env.H6S_API_KEY;
const ADMIN_INGEST_URL = process.env.ADMIN_INGEST_URL;
const ADMIN_TOKEN = process.env.ADMIN_TOKEN;

const now = new Date();
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));
const end = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 0));
const iso = (d) => d.toISOString().slice(0, 10);
const month = iso(start).slice(0, 7);

const jobs = [
  { providerCode: "CB_KB",   schema: "bank.transactions.cb.v1" },
  { providerCode: "HOMETAX", schema: "hometax.tax-invoices.sales.v1" },
  { providerCode: "HOMETAX", schema: "hometax.tax-invoices.purchase.v1" },
  { providerCode: "HOMETAX", schema: "hometax.cash-receipts.sales.v1" },
];

const h6s = (path, init = {}) =>
  fetch(`https://api.h6s.ai/api/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${H6S_API_KEY}`,
      "Content-Type": "application/json",
      ...(init.headers ?? {}),
    },
  }).then(async (r) => {
    if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
    return r.json();
  });

const wait = (ms) => new Promise((r) => setTimeout(r, ms));

for (const { providerCode, schema } of jobs) {
  const created = await h6s("/data-jobs", {
    method: "POST",
    body: JSON.stringify({
      providerCode,
      schema,
      params: { dateRangeStart: iso(start), dateRangeEnd: iso(end) },
    }),
  });

  let status = created.status;
  while (status !== "SUCCEEDED" && status !== "FAILED") {
    await wait(5_000);
    ({ status } = await h6s(`/data-jobs/${created.id}`));
  }
  if (status === "FAILED") throw new Error(`${schema} job ${created.id} failed`);

  const { results } = await h6s(`/data-jobs/${created.id}/results`);

  await fetch(ADMIN_INGEST_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${ADMIN_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ schema, month, records: results }),
  });
}

ContractRecord 셰이프와 schema id 는 /api-reference 의 카탈로그가 진실 원천입니다. 어드민 백엔드의 DTO 가 그 셰이프와 1:1 매핑되도록 유지합니다.

03검증

  • 스케줄러가 한 번 돌면 어드민 백엔드 로그에 4개 schema 의 incoming 호출이 남습니다 (bank · sales · purchase · cash-receipt).
  • 어드민 UI에서 그 월을 열면 4개 데이터가 정산 매칭 화면에 표시됩니다. 매칭·확인은 어드민 안에서 진행합니다.
  • 재실행해도 어드민의 중복 제거 룰에 따라 같은 행이 두 번 들어가지 않습니다.

04흔한 에러

NO_API_KEY

어드민 백엔드 환경에 H6S_API_KEY 가 없거나 빈 문자열입니다.

해결 secret 매니저에 H6S_API_KEY 등록 후 백엔드 재시작.

CREDENTIAL_INSUFFICIENT_FOR_PROVIDER

은행(CB_KB) 또는 홈택스(HOMETAX) 에 매칭되는 자격증명이 워크스페이스에 없습니다.

해결 h6s credentials create --interactive --cert (공동인증서 1개로 전 기관 공용).

data-job polling 이 끝나지 않음

수집이 오래 걸리는데 어드민 핸들러가 동기적으로 대기 중입니다.

해결 cron 핸들러의 timeout 을 늘리거나, 핸들러는 data-job 생성만 하고 적재는 별도 큐/웹훅으로 분리.

어드민 incoming 이 422 로 거절

어드민의 DTO 가 ContractRecord 셰이프와 어긋났다 (필드명·타입 mismatch).

해결 /api-reference 의 schema 카탈로그와 어드민 DTO 를 다시 맞춥니다. 신규 필드는 어드민의 unknown-field 정책으로 흡수합니다.

4개 중 일부만 SUCCEEDED

특정 schema만 자격증명·세션 문제로 실패했습니다. 위 데모는 첫 실패에서 예외를 던지고 멈춥니다.

해결 운영에서는 try/catch로 schema별로 감싸 성공한 것은 적재하고 실패한 것만 재시도 큐에 적재합니다.

05변형

같은 사례에서 자주 바꾸는 옵션. 다른 사례는 아래 이전/다음에서.

주간 cadence 로 전환

terminal
// 직전 주의 월~일 범위로 바꾼다.
const now = new Date();
const day = now.getUTCDay() || 7;             // 일=7
const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - day));
const start = new Date(end.getTime() - 6 * 24 * 60 * 60 * 1000);

여러 은행 동시 수집

terminal
// jobs 배열에 provider 별로 복제. 같은 schema, 다른 providerCode.
const jobs = [
  { providerCode: "CB_KB",      schema: "bank.transactions.cb.v1" },
  { providerCode: "CB_SHINHAN", schema: "bank.transactions.cb.v1" },
  { providerCode: "CB_IBK",     schema: "bank.transactions.cb.v1" },
  { providerCode: "HOMETAX",    schema: "hometax.tax-invoices.sales.v1" },
  // ... 나머지 schema 동일
];
전체 가이드 페이지로 보기

// 더 보기

관련 시나리오

// faq

자주 묻는 질문

정해진 날짜에 자동으로 돌릴 수 있나요?

스케줄러나 GitHub Actions 로 매월 같은 수집 요청을 자동 실행할 수 있습니다.

은행과 카드를 같이 받나요?

네. 입출금내역과 법인카드 승인내역을 같은 흐름에서 받습니다.

우리 정산 시스템에 어떻게 넣나요?

결과는 표준 데이터 형식으로 오므로 내부 적재 API 에 한 번만 매핑하면 됩니다.

// related

함께 보면 좋은 흐름

같은 데이터를 제품 연동, 내부 시스템, 자동화 작업 중 어디에 둘지에 따라 구현 방식이 달라집니다.

headless

첫 수집은 작게 시작합니다

가입하고 API 키를 만드세요. 짧은 기간의 데이터로 응답 형식을 먼저 확인할 수 있습니다.