실제 제작 소스에서 찾은 질문을 독립 합성 코드로 다시 만들었다. 전체 구현과 실행 예제, 테스트를 함께 공개하며 원래 업체의 프로그램과 자료는 연결하지 않는다.

양품만 재고로 들어가고 납품만 빠진다

start는 목표 수량을 만들고 apply는 생산과 납품 이벤트를 받는다. 양품은 재고에 더하고 불량은 별도 누계로 남기며 불량이 있으면 사유가 필요하다. 납품은 보유 양품을 넘을 수 없다. 목표를 넘는 양품도 이 예제에서는 거절한다. 실제 현장의 초과생산 정책은 통합 시 다시 정해야 한다.

생산 완료와 납품 완료는 다른 상태다. 목표만큼 생산해도 납품 전이면 배송 상태는 시작 전이다. 함수는 입력 상태를 복사하며 실제 장부와 창고를 바꾸지 않는다. 이벤트 배열은 합성 메모리 기록이지 인증된 감사 로그가 아니다.

계산 모집단을 화면 수보다 먼저 정한다

dashboard는 전체 전달 행의 위험 수를 계산한 뒤 위험 우선·식별자 순으로 정렬하고 표시 수를 제한한다. 1000개 중 뒤쪽 세 항목만 위험이고 화면은 두 개만 보여도 전체 위험 수는 3이다. 위험 여부 자체는 호출자가 선언한 값이며 이 함수가 공정의 지연을 진단하지 않는다.

calendarAverage는 오늘을 포함한 최근 달력 5일의 양품을 합해 5로 나눈다. 기록 없는 날도 분모에 남는다. 구간 안의 기록이 전혀 없으면 null, 0 기록이 있으면 0이다. 작업일 달력과 휴일, 설비 가동시간은 모른다.

입력 대상을 눈에 보이는 상태로 둔다

keypad는 양품·불량 중 선택한 버퍼에 숫자, 지우기와 뒤로 지우기를 적용한다. 아래 예제는 두 버퍼와 불량 사유, 납품 수량을 합성 상태에 연결한다. 입력에 실패해도 재고가 중간에 바뀌지 않는지 이벤트 대역에서 확인했다. 실제 태블릿에서 장갑을 끼고 사용한 검수는 아니다.

기존 제조 시제품은 데이터베이스 서비스지만 이번 모듈은 순수 메모리다. 영속 저장과 동시 사용자, 중복 요청은 구현하지 않았다. 필요한 경우 기존 원자 처리 모듈과 별도 통합해야 하며 이 예제의 새 기능처럼 보고하지 않는다.

코드와 구현 살펴보기

독립 합성 전체 구현

function qty(n){if(!Number.isSafeInteger(n)||n<0||n>1000000)throw Error('Bounded whole quantity required');return n;}
export function start(target){qty(target);if(!target)throw Error('Positive target');return{target,good:0,defect:0,delivered:0,inventory:0,events:[]};}
export function apply(state,event){if(!state||!Array.isArray(state.events)||state.events.length>=1000)throw Error('Bounded state required');for(const k of['target','good','defect','delivered','inventory'])qty(state[k]);if(!state.target||state.good>state.target||state.delivered>state.good||state.inventory!==state.good-state.delivered)throw Error('State invariant');const s=structuredClone(state);if(event.kind==='production'){qty(event.good);qty(event.defect);if(!event.good&&!event.defect)throw Error('Empty production');if(event.defect&&(typeof event.reason!=='string'||!event.reason.trim()||event.reason.length>100))throw Error('Defect reason required');if(s.good+event.good>s.target||s.defect+event.defect>1000000)throw Error('Limit exceeded');s.good+=event.good;s.defect+=event.defect;s.inventory+=event.good;}else if(event.kind==='delivery'){qty(event.quantity);if(!event.quantity||event.quantity>s.inventory)throw Error('Delivery exceeds available good stock');s.delivered+=event.quantity;s.inventory-=event.quantity;}else throw Error('Unknown event');s.events.push(structuredClone(event));return s;}
export function status(s){return{production:s.good===s.target?'complete':'in_progress',delivery:s.delivered===s.target?'complete':s.delivered?'partial':'not_started',inventory:s.inventory,defect:s.defect};}
export function dashboard(rows,limit){if(!Number.isSafeInteger(limit)||limit<1||limit>1000)throw Error('Bounded limit');if(new Set(rows.map(r=>r.id)).size!==rows.length)throw Error('Duplicate ID');for(const r of rows)if(typeof r.id!=='string'||!r.id||typeof r.risk!=='boolean')throw Error('Declared risk and ID required');const all=[...rows].sort((a,b)=>Number(b.risk)-Number(a.risk)||(a.id<b.id?-1:a.id>b.id?1:0));return{population:all.length,riskCount:all.filter(r=>r.risk).length,rows:all.slice(0,limit),hasMore:all.length>limit};}
function day(s){if(typeof s!=='string'||!/^\d{4}-\d{2}-\d{2}$/.test(s))throw Error('Civil date required');const n=Date.parse(s+'T00:00:00Z');if(!Number.isFinite(n)||new Date(n).toISOString().slice(0,10)!==s)throw Error('Invalid date');return n;}
export function calendarAverage(rows,today){const end=day(today),start=end-4*86400000;let total=0,count=0;for(const r of rows){const d=day(r.date);qty(r.good);if(d>=start&&d<=end){total+=r.good;count++;}}if(!Number.isSafeInteger(total))throw Error('Total overflow');return{calendarDays:5,total,averagePerCalendarDay:count?total/5:null,observations:count,workingDayCalendar:false};}
export function keypad(value,key){if(typeof value!=='string'||!/^\d{0,6}$/.test(value))throw Error('Bounded digit buffer');if(key==='C')return'';if(key==='backspace')return value.slice(0,-1);if(!/^[0-9]$/.test(key))throw Error('Digit key required');if(value.length===6)return value;return(value==='0'?'':value)+key;}

완료와 재고를 같은 숫자로 쓰지 않는다

목표 10개에서 양품 6개와 불량 2개를 기록하면 재고는 6개다. 세 개를 납품하면 재고는 3개이고 양품 누계는 그대로 6개다. 이후 네 개를 더 만들면 생산은 완료지만 전체 납품은 아니다. 같은 완료 표지 하나로 이 상태를 덮지 않는다.

이 반례는 실행 기록과 테스트로 다시 확인할 수 있다. 다른 업무에 적용할 때는 이름만 바꾸지 말고 입력의 의미와 실패 정책이 같은지 먼저 확인해야 한다. 작은 독립 함수는 운영 프로그램 전체를 대신하지 않는다.

공개 코드와 ZIP은 같은 검토 파일을 사용한다. 실제 업체의 자료를 가져오거나 외부 시스템을 실행하지 않은 상태에서도 핵심 판단을 확인할 수 있도록 만들었다. 새로운 연결을 추가하면 그 연결의 결과는 별도로 기록해야 한다.

직접 확인한 실행과 남겨 둔 경계

소스 폴더와 ZIP을 새로 푼 폴더에서 각각 25개 테스트가 통과했다. 실행 예제의 실제 반환값도 records.json으로 남겼다. 공개 페이지의 코드는 내려받기 구현과 대조할 수 있다. 과거 프로젝트의 완료 보고와 테스트 수치를 이번 실행의 증거로 가져오지 않았다.

아래 화면은 실제 인라인 코드로 합성 상태를 바꾼다. 이벤트 스크립트는 DOM 대역에서도 실행해 결과를 확인했다. 이 검수는 실제 브라우저의 렌더링과 물리적 기기 조작을 포함하지 않는다. 실서비스 통합의 결과는 별도 근거로 확인해야 한다.

이 프레임 안의 합성 상태만 바꿉니다. 실제 회사 업무나 외부 전송과 연결되지 않습니다.

내려받아 적용하기 전에

ZIP에는 전체 코어와 테스트, 실행기, 저장된 결과와 사용 안내가 있다. 라이브 예제는 두 언어 HTML도 포함한다. 수정과 재사용이 가능한 새 합성 코드이며 고객 원본 이미지, 실데이터와 계정 정보는 없다. 실제 프로그램을 실행하는 지시와 외부 전송도 넣지 않았다.

적용할 때는 먼저 단위와 입력 출처, 실패한 경우의 안내를 실제 업무에 맞춰 정해야 한다. 테스트 통과가 모든 입력 사실과 운영 권한을 보장하지는 않는다. 필요한 인증·저장·전송과 화면 검수는 작은 계약 바깥의 일로 남긴다. 나는 재사용할 코드와 함께 어떤 판단을 다시 해야 하는지도 읽을 수 있게 공개한다.

구현·테스트·실행 예제 ZIP 받기
독립 합성 코드, 수정·재사용 가능