수량을 지웠는데 다시 1이 들어온다면 누구의 판단일까. 기존 상품 관리 프로그램에서 작은 기본값 처리가 입력 오류를 가리고 있었다. 나는 빈 입력을 보존하면서도 정상으로 진행하지 않게 하는 연결을 고쳤다.
처음에는 편집 입력과 준비서를 고쳤다. 후속 작업에서는 API로 받은 주문을 정리하는 단계에도 같은 기본값 추정이 남아 있었다. 아래의 추가 기록은 들어오는 자료가 검사 전에 바뀌지 않게 고친 과정이며, 실거래 주문을 처리한 결과는 아니다.
기본값은 새 작업을 시작할 때만 필요했다
새 주문을 만들 때 수량 1을 먼저 보여 주는 선택과, 사용자가 이미 입력한 수량을 지웠을 때 다시 1을 넣는 선택은 달랐다. 전자는 작성의 출발점이고 후자는 편집 내용의 변경이다. 나는 두 동작이 같은 기본값 처리에 묶인 부분부터 떼어 보았다.
실제 입력 콜백의 표현식을 대역으로 실행하자 빈 문자열과 0이 모두 1로 전달됐다. 수령 정보나 주문을 실제로 저장한 실험은 아니다. 화면에 연결된 입력 처리의 결과를 합성 이벤트로 확인한 것이다. 사용자가 수량을 잠시 비우고 다음 숫자를 적으려는 순간에도 프로그램이 먼저 의미를 결정하고 있었다.
새 주문의 명시적인 기본값 1은 유지했다. 대신 편집에서 들어온 빈값, 0, 소수 표현은 그대로 남겼다. 오류가 있는 초안을 보관한다는 것과 그 초안을 정상 업무로 진행시킨다는 것을 나누면, 입력을 지우지 않고도 진행 조건을 엄격하게 만들 수 있었다.
1보다 작지만 않으면 정상이라는 검사를 고쳤다
다음에는 입력창 밖에서 넘어오는 값을 보았다. 기존 검사는 숫자로 바꾼 값이 1보다 작은지만 확인했다. 실제 검사 함수와 준비서 생성기에 누락된 값, 숫자가 아닌 문자, 비정상 숫자와 소수 등을 각각 넣자 수량 문제 없이 준비 가능으로 나오는 반례가 있었다. 다른 필드는 합성 정상값으로 채워 수량 조건을 따로 살폈다.
준비서 쪽의 기본값 처리도 결과를 가렸다. 누락이나 비수치 입력은 1로 추정됐고, 무한대와 소수는 그 값이 남은 채 준비 가능으로 표시됐다. 빈칸을 채워 주는 편의처럼 보였지만 실제로는 확인하지 않은 수량을 만들어 내거나 지원하지 않는 수량을 정상으로 넘기는 두 문제였다.
검사는 무엇이 잘못됐는지 알려야 하고, 준비서는 그 판단을 뒤집지 않아야 했다. 나는 작은 조건문 하나의 통과 여부만 바꾸는 대신 입력에서 준비서까지 같은 반례를 보내기로 했다. 한 단계가 정상이라고 했다는 이유로 다음 단계의 값까지 정상이라고 가정하지 않았다.
허용할 수량의 뜻을 한 함수에 모았다
공통 해석 함수는 숫자 또는 비어 있지 않은 숫자형 문자열을 받는다. 숫자로 해석한 결과가 양의 안전정수이면 그 정수를 돌려주고, 나머지는 null로 돌려준다. 숫자가 아닌 타입을 먼저 걸러 불리언이나 배열을 수량처럼 읽지 않게 했다. 빈 문자열도 숫자로 바꾸기 전에 별도로 거절했다.
이 계약은 숫자 문자만 허용하는 엄격한 십진수 표기 검사가 아니다. 실제 회귀 검사에서 문자열 2.0은 정수 2, 문자열 2e2는 정수 200이 된다. 반면 1.5는 정수가 아니므로 거절된다. 입력창의 표기 규칙과 해석한 값의 정수 조건을 혼동하지 않으려고 허용한 예를 함께 남겼다.
안전정수라는 조건이 재고나 회사의 주문 상한을 알려 주지는 않는다. 이번 수정에는 상품별 최소 주문량, 포장 단위, 최대 발주량을 새로 넣지 않았다. 길이나 무게처럼 소수 수량을 쓰는 업무라면 이 정수 계약부터 다시 정해야 한다. 다른 프로그램에 옮길 때 필요한 것은 함수 이름의 복사가 아니라 단위와 허용 범위의 합의다.
준비서에는 모르는 수량을 그대로 모른다고 남겼다
검사 함수와 준비서 생성기는 같은 해석 함수를 호출한다. 잘못된 수량은 준비서에서 quantity가 null이고 validation.ready가 false이며, 보완 사유에 수량 확인이 남는다. 정상 수량은 숫자로 정리된다. 원본 주문 객체는 바꾸지 않으므로 검토하는 사람이 입력을 고칠 근거도 남아 있다.
여기서 null은 0개나 1개라는 뜻이 아니다. 정상 수량으로 확정할 수 없다는 표현이다. 이 준비서를 받는 다른 코드가 다시 기본값 1로 바꾸면 같은 오류가 다음 경계에서 되살아날 수 있다. 이번에 모든 외부 소비자의 호환성을 확인한 것은 아니므로 실제 연결 전에 null과 보완 사유를 어떻게 처리하는지 대조해야 한다.
기존 프로그램은 보완할 내용이 있는 준비서도 검토 목적으로 복사하거나 내려받을 수 있다. 그 동작은 유지했다. 준비서 생성, 검토 가능, 실제 발주 허용은 같은 상태가 아니다. 이번 수정은 클라이언트의 검사와 검토용 출력에 관한 것이며 서버 권한, 실제 발주나 결제의 차단을 새로 구현한 결과로 설명하지 않는다.
같은 반례를 실제 연결까지 보내 보았다
별도의 개발 단계에서 수량 검사 8개를 실행했다. 허용한 정수와 숫자형 문자열, 누락·빈값·0·음수·소수·비수치·비정상 타입, 준비서의 null과 보완 사유를 대조했다. 정상 입력의 다른 필드가 유지되고 반복 검사와 동결된 입력이 변하지 않는지도 확인했다.
입력 콜백은 실제 소스의 표현식을 추출해 상태 갱신 대역에 연결했다. 빈 문자열·0·소수 편집이 기존 주문 정규화를 거친 뒤에도 그대로 남고, 같은 입력이 검사와 준비서에서 거절되는지 이어서 보았다. 이 경로에서 정상 문자열 3은 입력 상태에 문자열로 남고 준비서에는 숫자 3으로 나온다. 편집과 해석의 책임이 어디에서 달라지는지 확인한 셈이다.
화면에서 타이핑하거나 실제 클립보드를 사용한 검사는 아니다. 첫 개발 작업에서는 기존 CSV 검사 19개, 격리한 빌드와 목업 API 검사 26개도 통과했지만 서로 다른 범위의 근거로 남겼다. 첫 집필은 그 결과와 소스를 대조했으며, 이번 API 입력 후속 보완 역시 원제품 시험을 다시 실행하지 않고 별도 개발의 기록을 읽었다. 독립 코드 재검수의 판단도 실행 시험과 별도 근거다.
복구 시험은 설치 환경이라는 조건을 드러냈다
첫 수량 수정에서는 전후 소스와 시험에 필요한 문맥을 압축하고 실제로 풀어 17개 파일의 해시를 대조했다. 그러나 처음 복구한 폴더에서 수량 시험을 실행했을 때는 의존 라이브러리를 찾지 못해 멈췄다. 소스가 똑같다는 확인과 그 소스를 실행할 환경이 있다는 확인은 달랐다.
첫 실패 기록과 압축본은 그대로 남겼다. 복구 폴더에서만 기존 설치 패키지를 참조하도록 연결한 뒤 복구본의 수량 검사 8개가 통과했다. 새 패키지를 설치하거나 원래 설치 환경을 고치지 않았고, 고정 의존성 목록과 설치 버전 정보도 따로 보관했다. 이것은 기존 런타임을 사용한 소스 복구 시험이다. 새 컴퓨터에 처음 설치하는 절차까지 검증했다는 뜻은 아니다.
개발 단계의 보호 대상 114개 파일은 바이트와 수정 시각이 유지됐다. 운영 자료와 기존 빌드, 서버와 어댑터를 포함한다. 고친 소스가 있다는 사실을 실행 중인 배포본까지 바뀌었다는 말로 넓히지 않았다. 복구 결과를 읽는 사람도 무엇을 복구했고 어떤 환경에서 확인했는지 알아야 다음 판단을 이어갈 수 있다.
검사에 도착하기 전에 수량이 바뀌고 있었다
편집창의 빈값을 남겨도 다른 입구에서 1을 만들어 넣으면 검사 함수는 누락을 알아볼 수 없다. 후속 검토에서 실제 API 주문 정규화 함수를 합성 입력으로 실행하자, 수량 필드가 없는 주문은 1이 됐다. 명시한 수량이 0이고 항목 목록에 4가 있으면 4로 바뀌었다. 사용자가 새 주문을 시작한 것이 아닌데 새 주문의 기본값을 빌려 쓰고 있었다.
항목 하나는 2이고 다른 항목은 숫자가 아닌 문자인 경우에는 2라는 부분합이 남았다. 개별 값은 안전정수여도 합계가 안전정수 범위를 넘는 반례도 있었다. 검토 준비서의 조건만 더 엄격하게 만드는 것으로는 이미 정상 숫자로 변한 앞단의 추정을 되돌릴 수 없었다. 그래서 값을 받는 단계에서부터 기존 수량 해석 함수를 연결했다.
필드가 없는 경우와 잘못된 값이 온 경우를 나눴다
받은 주문 자체에 quantity 필드가 있으면 먼저 그 값을 해석한다. 값이 0이나 빈 문자열, 불리언, 비정상 숫자라면 null로 남기고 항목 합계로 덮어쓰지 않는다. 값이 잘못됐다는 사실을 다른 필드의 그럴듯한 숫자로 감추지 않으려는 선택이다. 객체가 직접 가진 필드인지도 확인해, 값이 참인지 검사하는 조건과 구별했다.
quantity 필드 자체가 없을 때만 항목 목록을 대안 입력으로 살펴본다. 이 우선순위는 이번 연결의 계약이다. 유효한 상위 수량이 있을 때 항목 합계와 서로 일치하는지까지 검사한 것은 아니다. 숫자형 문자열을 허용하는 기존 규칙도 유지하므로 2.0과 지수 표현의 허용 여부를 다른 자료 계약과 섞어 읽지 않는다.
부분합은 주문 전체의 수량으로 내보내지 않았다
항목 목록을 사용할 때는 목록이 비어 있지 않고 모든 항목의 수량이 유효해야 한다. 하나라도 모르면 합계 전체를 null로 돌려준다. 잘못된 항목을 0으로 바꾸어 더하는 방식은 남아 있는 정상 항목의 합을 주문 전체처럼 보이게 한다. 항목 오류 수를 세는 일과 주문 수량을 확정하는 일을 같은 reduce 한 줄에 맡기지 않았다.
각 수량을 해석한 뒤에는 더할 때마다 합계가 안전정수인지 확인한다. 이것은 계산 표현의 한계에 관한 검사이지 발주 가능한 업무 상한이나 상품별 배분 정책이 아니다. 여러 상품이 섞인 주문을 어떤 상품과 연결하고 수량을 배분할지는 이번에 바꾸지 않았다. 아래 코드는 두 수량 해석 함수의 실제 구현이며, 외부 요청이나 주문 저장을 수행하지 않는다.
코드와 구현 살펴보기
실제 수량 해석과 API 입력 연결의 순수 함수
export function normalizeOrderQuantity(value) {
if (typeof value !== "number" && typeof value !== "string") return null;
if (typeof value === "string" && !value.trim()) return null;
const quantity = Number(value);
return Number.isSafeInteger(quantity) && quantity > 0 ? quantity : null;
}
// Imported orders never borrow the explicit default of a new local order.
export function normalizeApiOrderQuantity(order) {
if (!order || typeof order !== "object" || Array.isArray(order)) return null;
if (Object.hasOwn(order, "quantity")) return normalizeOrderQuantity(order.quantity);
if (!Array.isArray(order.items) || !order.items.length) return null;
let total = 0;
for (const item of order.items) {
const quantity = normalizeOrderQuantity(item?.quantity);
if (quantity === null || !Number.isSafeInteger(total + quantity)) return null;
total += quantity;
}
return total;
}
로컬 편집과 서버의 미확인을 함께 남겼다
새로 받은 수량이 확인되지 않았다고 이미 검토 중인 로컬 편집을 지우지는 않았다. 기존 병합은 localEdits가 있으면 편집한 필드를 우선한다. 이 정책을 유지하면서 서버에서 들어온 수량은 serverSnapshot.quantity에 null로 남고 syncConflict도 표시되는지 시험했다. 빈 문자열로 고쳐 둔 로컬 수량도 다시 1이나 서버의 항목 합계로 바뀌지 않았다.
로컬 편집이 없는 주문은 새 정규화 결과를 받아 수량이 null이면 기존 검사와 준비서에서 보완이 필요하다. 한편 로컬에 유효한 편집값이 남아 있는 경우와 서버 값이 미확인인 경우는 함께 존재할 수 있다. 충돌 표시 자체를 서버 잠금이나 발주 차단으로 설명하지 않았다. 무엇을 보존할지와 누가 어떤 근거로 진행을 허용할지는 여전히 다른 책임이다.
함수 하나가 아니라 정규화와 병합까지 이어 확인했다
후속 개발의 새 검사 16개는 공통 함수뿐 아니라 실제 앱의 정규화·병합 함수를 추출해 준비서까지 연결했다. 정상 상위 수량의 우선순위, 모든 정상 항목의 합계, 한 항목 오류, 빈 목록, 합계 범위와 로컬 편집 보존을 대조했다. 동결된 원본이 바뀌지 않는지와 새 로컬 주문의 명시적 기본값 1이 유지되는지도 확인했다.
후속 실행의 클라이언트 검사 43개는 기존 수량 8개, CSV 19개, 새 동기화 16개를 포함한 수다. 앞선 8개를 다시 더하지 않았다. 격리한 목업 API 26개와 별도 코드 재검수, 격리 빌드도 통과했지만 실제 공급자 동기화나 계정·주문 처리의 성공을 뜻하지 않는다. 이번 콘텐츠 보완은 그 소스와 결과를 읽었으며 원제품을 다시 실행하지 않았다.
백업에는 시험이 읽는 연결 소스도 필요했다
후속 복구 시험에서는 다른 누락을 발견했다. 첫 압축본에 기존 시험이 직접 읽는 입력 패널 소스 두 파일이 빠져 있었다. 원제품의 검사 실패가 아니라 복구 묶음을 구성한 문맥의 누락이었다. 첫 압축본과 파일 없음 오류 로그를 보존하고, 참조 소스를 포함한 새 전후 묶음으로 복구를 이어 갔다.
완전한 새 묶음에서 전후 27개 파일의 해시를 대조하고 이전 소스의 검사 27개, 수정 소스의 검사 43개를 실제로 실행했다. 기존 런타임은 복구 폴더에서만 참조했으며 새 설치를 검증한 것은 아니다. 후속 개발의 보호 대상 116개 파일과 기존 운영 자료·실행본도 보존됐다. 첫 수정의 의존 환경 문제와 이번 시험 참조 파일 누락을 같은 성공 숫자로 덮지 않고 각각의 원인과 해결 조건을 남겼다.
기존 모듈은 같은 원리를 다른 범위로 보여 준다
수량 검사 함수를 하나 더 압축해 공개할 필요는 없었다. 기존 업무 전환 모듈에는 잘못된 입력을 초안으로 보관하면서 준비를 막고, 수량을 바꾸면 현재 수정 번호에 맞춰 다시 확인하는 독립 합성 구현이 있다. 이번 수량 해석 수정 다음에 확인의 유효기간을 어떻게 다룰지 살펴볼 수 있는 자료다.
두 구현의 계약은 같지 않다. 공개 모듈은 길이를 제한한 문자열 초안과 1~20의 정수 표기를 사용한다. 이번 원제품의 숫자 해석 함수보다 범위와 표기가 좁고, 원제품에 그 전환 모듈을 설치한 것도 아니다. 따라서 2.0이나 지수 표기까지 같은 방식으로 통과한다고 생각하면 안 된다. 재사용은 이 차이를 읽고 자신의 업무에 맞춰 연결하는 일이다.
이번 보완에서도 기존 공개 ZIP의 바이트와 해시를 확인하고 새 추출 폴더에서 25개 검사와 4,320개 합성 행동 조합을 실행했다. 새 모듈이나 업로드는 없다. 이 상태 모델은 API 주문의 항목 합계를 계산하지 않으며 원제품에 설치한 것도 아니다. 공개 예제의 결과를 첫 수량 검사 8개나 후속 클라이언트 43개, 실제 브라우저 검수에 더하지 않는다. 입력 보존, 해석, 재확인과 실행의 책임을 구분하는 방법으로 재사용할 수 있다.