// 팀 워크플로우에 녹여내기
자체 정산 어드민에 매월 적재
매입·매출 세금계산서, 매출 현금영수증, 은행 입출금을 자체 어드민 백엔드로 매달 적재. 정산 매칭·중복 제거·확인은 어드민이 책임진다.
이런 분께 — 엑셀로 하던 정산을 자체 어드민·ERP 로 자동화하려는 재무팀. 어드민 백엔드의 REST 호출과 스케줄러만으로 끝낸다.
01사전 점검
아래 명령을 그대로 붙여넣어 지금 내 환경이 준비됐는지 확인한다.
어드민 백엔드 환경에 H6S_API_KEY 가 들어 있다
echo $H6S_API_KEYh6s_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.
# 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 데코레이터 안에서 그대로 호출해도 된다.
// 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 로 전환
// 직전 주의 월~일 범위로 바꾼다.
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);여러 은행 동시 수집
// 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 동일
];