headless
전체 활용 사례

// 팀 워크플로우에 녹여내기

자체 정산 어드민에 매월 적재

매입·매출 세금계산서, 매출 현금영수증, 은행 입출금을 자체 어드민 백엔드로 매달 적재. 정산 매칭·중복 제거·확인은 어드민이 책임진다.

REST API매월 1일 09:00 KST

이런 분께 엑셀로 하던 정산을 자체 어드민·ERP 로 자동화하려는 재무팀. 어드민 백엔드의 REST 호출과 스케줄러만으로 끝낸다.

terminal
[2026-05-01] bank.transactions.cb.v1 124 records → admin 200[2026-05-01] hometax.tax-invoices.sales.v1 38 records → admin 200[2026-05-01] hometax.tax-invoices.purchase.v1 52 records → admin 200[2026-05-01] hometax.cash-receipts.sales.v1 17 records → admin 200 ✓ done

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 에 도는 시퀀스. 5단계로 끝난다 — 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 만 자격증명·세션 문제로 실패. 위 데모는 첫 실패에서 throw 로 멈춘다.

해결 운영에서는 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 동일
];
다음매출 대사 (세금계산서 ↔ 입금)