ERP 체험의 카드와 내역을 설명하다 보니, 숫자를 더하는 일보다 더하기 전에 무엇을 확인해야 하는지가 별도의 공유 주제가 되었다. 나는 금액이 없는 행을 0으로 덮지 않는 작은 검증기를 만들었다.
이 모듈은 기존 회사 프로그램을 추출한 코드가 아니다. 이번에 새로 작성한 합성 정수 원화 예제이며 화면·서버·저장소에 의존하지 않는다. 아래에서 오류 사례를 바꾸어 보고 구현과 테스트를 함께 받을 수 있다.
모든 행이 확인되어야 전체 합계를 내보낸다
입력은 거래 식별자 id, 금액 amount, 수금 배열 settlements를 가진 행의 목록이다. 원화 금액은 음수가 아닌 안전한 정수로 제한했다. 배열이 비어 있으면 확인된 내역이 없다는 뜻이지만 배열 자체가 없으면 아직 수금 자료가 확인되지 않은 것으로 취급한다. 금액도 명시적인 0과 누락을 구별한다. 입력을 임의로 보정하지 않는 것이 이 계약의 출발점이다.
정상 결과는 valid와 totals, 행별 details, 검토 사유 issues를 돌려준다. 문제가 하나라도 있으면 valid는 false이고 totals는 null이다. 정상 행의 내역을 보여 줄 수는 있어도 그것을 전체 장부의 확정 합계라고 부르지 않는다. 화면을 붙일 때는 null을 0으로 바꾸어 표시하지 말고 검토가 필요하다는 상태로 읽어야 한다.
의심스러운 행을 조용히 버리지 않는다
식별자는 40자 이내의 영문자·숫자·밑줄·하이픈으로 정했다. 같은 식별자가 두 번 오면 중복 여부를 해결할 때까지 합계를 보류한다. 실제 업무에서는 중복 수입이 아니라 다른 지점에서 같은 번호를 사용한 것일 수도 있다. 그렇다면 지점과 번호를 합친 식별자를 입력 계약에서 먼저 정해야 한다. 이 함수가 업무의 정체성을 추측하게 두지는 않았다.
수금이 원금보다 크면 over_settled 사유를 남긴다. 이것을 오류 메시지에서 곧바로 부정 거래라고 부르지 않는다. 실제 회사에서는 선수금이나 배분 차이일 수 있기 때문이다. 이 예제는 그런 업무 정책을 구현하지 않고 사람이나 상위 프로그램이 검토해야 하는 경계를 표시한다. 일부 행을 버리고 남은 합을 완성값처럼 반환하지 않는 점이 중요하다.
정수라는 조건은 입력과 누적 양쪽에 걸린다
금액 하나가 안전한 정수여도 여러 금액을 더한 결과는 범위를 벗어날 수 있다. 그래서 개별 수금, 행의 수금 합, 전체 원금·수금·잔액 합에 각각 조건을 건다. 소수나 문자열 숫자도 자동 변환하지 않는다. 화면의 입력 파서가 필요한 서비스라면 문자열을 어떤 규칙으로 정수로 바꿀지 별도 책임으로 두고 그 결과를 이 함수에 넘기는 편이 명확하다.
행과 행별 수금 수는 각각 10000개 이내로 제한한다. 이 상한은 서버의 전체 요청 크기 제한이나 데이터베이스 검증을 대신하지 않는다. 회사 자료를 받는 API라면 본문 크기, 권한과 처리 시간도 따로 제한해야 한다. 원화만 다루는 작은 모듈에 환율과 세금, 반품 배분까지 숨겨 넣지 않고 적용 범위를 읽을 수 있게 남겼다.
코드와 구현 살펴보기
독립 합성 검증기 전체 구현
// Independent synthetic teaching module; integer KRW only, no network or storage.
const money = n => Number.isSafeInteger(n) && n >= 0;
export function reconcileLedger(rows) {
if (!Array.isArray(rows) || rows.length > 10000) throw new TypeError('Expected at most 10000 rows');
const issues = [], details = [], seen = new Set();
let amount = 0, settled = 0, balance = 0;
for (let index = 0; index < rows.length; index++) {
const row = rows[index], codes = [];
if (!row || typeof row !== 'object' || Array.isArray(row)) {
issues.push({index, code:'row_shape'}); continue;
}
const validId = typeof row.id === 'string' && /^[A-Za-z0-9_-]{1,40}$/.test(row.id);
if (!validId) codes.push('invalid_id');
else if (seen.has(row.id)) codes.push('duplicate_id');
if (validId) seen.add(row.id);
if (!money(row.amount)) codes.push('unknown_or_invalid_amount');
if (!Array.isArray(row.settlements) || row.settlements.length > 10000) codes.push('invalid_settlements');
let paid = 0;
if (Array.isArray(row.settlements) && row.settlements.length <= 10000) {
for (const value of row.settlements) {
if (!money(value)) { codes.push('invalid_settlement'); break; }
if (!Number.isSafeInteger(paid + value)) { codes.push('settlement_overflow'); break; }
paid += value;
}
}
if (!codes.length && paid > row.amount) codes.push('over_settled');
const outstanding = codes.length ? null : row.amount - paid;
details.push({index, id:validId ? row.id : null, amount:money(row.amount) ? row.amount : null,
settled:codes.length ? null : paid, balance:outstanding, valid:!codes.length});
for (const code of codes) issues.push({index, code});
if (!codes.length) {
if (![amount + row.amount, settled + paid, balance + outstanding].every(Number.isSafeInteger)) {
issues.push({index, code:'total_overflow'});
} else { amount += row.amount; settled += paid; balance += outstanding; }
}
}
return {valid:!issues.length, totals:issues.length ? null : {amount, settled, balance}, details, issues};
}
export function syntheticRows() {
return [
{id:'S01', amount:24000000, settlements:[10000000,6800000]},
{id:'S02', amount:18000000, settlements:[12600000]},
{id:'S03', amount:6000000, settlements:[6000000]}
];
}정상 값과 검토 사유를 같은 자리에서 비교한다
아래 예시는 정상 합성 장부와 금액 누락, 중복, 초과 수금, 전체 정수 범위 초과를 선택해 볼 수 있다. 모든 행은 프로그램 안의 고정 예시다. 회사 파일을 업로드하거나 실데이터를 입력받지 않는다. 정상 장부는 원금 4800만원, 수금 3540만원, 잔액 1260만원이고 오류 사례에서는 totals가 null이 되는 것을 직접 읽을 수 있다.
화면에는 설명을 한국어와 영어로 제공하며 세부 검토 코드는 두 언어에서 같은 기계용 이름을 쓴다. 실제 업무 화면에 붙인다면 그 코드를 담당자가 이해할 수 있는 문장과 수정 위치로 연결해야 한다. 데모는 입력 원본을 바꾸어 결과를 새로 계산하고 textContent로 표시한다. 외부 요청이나 영구 저장 없이 계산 관계를 확인하는 데 집중했다.
압축 파일을 받은 다음 확인할 순서
ZIP에는 구현 파일, 독립 테스트, 한영 HTML 데모, 사용 범위와 재사용 허용 문서가 들어 있다. 코드를 다른 시스템에 붙이기 전에 압축을 풀고 node test.mjs를 실행한다. HTML은 필요한 코드가 안에 있으므로 별도 서버 없이 열어 볼 수 있다. 이 패키지는 외부 라이브러리를 설치하지 않는다. 수정·재배포·상업적 사용이 가능하며 무보증이라는 조건도 파일에 남겼다.
추출한 패키지에서 21개 테스트 그룹과 작은 금액·부분 수금 조합 861개를 확인했다. 한영 데모의 이벤트 연결도 별도로 평가했다. 브라우저 배치나 실제 기기의 검수 결과로 표현하지는 않는다. 실무 적용에서는 거래의 날짜와 인식 정책, 환불·선수금, 통화, 권한과 동시 수정, 저장소를 추가로 설계해야 한다. 이 검증기는 그 전체 ERP 중 금액 입력과 합계의 경계만 맡는다.