견적 기능을 처음 만들 때 수량 × 단가 = 금액으로 짰다. 가장 단순한 구조였고, 빨리 동작했다. 문제는 실제 영업 방식을 제대로 이해하지 못했다는 거였다.
주방상판은 면 단위로 거래된다. "일자형 2,400mm짜리 한 장"이 기본 단위다. 단가는 m당 얼마고, 치수는 mm로 입력된다. 기존 엑셀에서도 =B2/1000*C2 같은 수식이 곳곳에 박혀있었다.
수량×단가로는 이걸 표현할 수 없었다.
계산 엔진 재설계
개편 방향은 명확했다.
이전: 수량(식) × 단가 = 금액
개편: (가로mm ÷ 1000) × 단가(원/m) × 수량 = 금액
계산 로직은 순수 함수로 분리했다.
// lib/quote/calc.ts
export function lineItemAmount(it: LineItem): number {
if (it.width_mm) {
// mm 입력 → m 변환 후 단가 × 수량
return round((it.width_mm / 1000) * it.unit_price * it.qty);
}
return it.qty * it.unit_price; // 레거시 호환
}
ㄱ자형이면 상단과 측면을 따로 계산해야 한다. 각 행이 독립된 치수를 가지고, 합산하면 총액이 나온다. 이게 실제 주방상판 영업 방식과 맞다.
클라이언트 계산값을 믿지 않는다는 원칙도 여기서 세웠다. 프론트에서 계산한 금액을 그대로 DB에 저장하지 않고, 서버에서 동일 함수로 재계산해서 저장한다.
const parsed = quoteCalcInputSchema.safeParse(body);
if (!parsed.success) return Response.json({ error: parsed.error }, { status: 400 });
// 서버에서 직접 재계산
const total = calcQuoteTotal(parsed.data);
자재 슬롯 2개 비교 지원
견적을 넣다 보면 "이 고객한테 자재 A 가격이랑 자재 B 가격 동시에 제시해야 해"라는 경우가 생긴다. 같은 치수로 두 자재의 가격을 한 견적서에 나란히 보여주는 기능이다.
primary_slot 필드로 최종 확정 자재를 지정하고, 그 자재의 금액만 총액에 반영된다. 비교는 보여주되 계산은 하나로.
10% 할인 + 절사 이중 구조
"10% 할인 후 1,000원 단위로 절사해줘"라는 요청이 실제로 있었다. 정률 할인(%)과 절사(원 단위 수동 차감)를 동시에 지원해야 했다.
정가: 1,234,500원
→ 10% 할인: 1,111,050원
→ 1,000원 절사: 1,111,000원 (50원 차감)
두 필드를 독립적으로 두고 순서대로 적용한다.
자재 마스터 DB가 필요한 이유
견적 폼에서 자재명을 텍스트로 직접 입력하면 이런 일이 생긴다.
"벤타실버/삼성/20T"
"벤타 실버 삼성 20"
"삼성 벤타실버"
같은 자재인데 세 가지 이름이 됐다. 단가도 제각각이고, 어느 게 현재 가격인지도 모른다.
materials 테이블을 만들고 자재 이름 입력을 자동완성 드롭다운으로 바꿨다.
materials (
id, name, brand, category,
unit_price_per_sqm, -- 기본 단가 (원/m)
is_active, -- 단종 자재 비활성
sort_order
)
자재 선택 시 DB에 등록된 단가가 자동으로 채워진다. 단가가 바뀌면 materials 테이블 하나만 수정하면 이후 견적부터 반영된다.
단종 자재를 삭제하지 않고 비활성으로 숨기는 건 중요한 포인트다. 이전 견적서에서 해당 자재를 참조하고 있으면 하드 삭제하면 안 된다. 비활성 자재는 신규 견적 드롭다운에서만 빠지고, 과거 견적에서는 그대로 보인다.
자재 수십 종을 하나씩 입력하면 현실적이지 않으니 CSV 일괄 업로드도 함께 만들었다. 기존 엑셀 목록을 CSV로 변환해서 올리면 한 번에 등록된다.
견적서에 담당자 이름이 필요하다
B2B 견적서는 "담당: 홍길동" 표기가 법적 관행이다. 고객사가 견적서를 내부 결재에 올릴 때 담당자 이름이 필요하고, 문의가 생기면 담당자에게 바로 연락할 수 있어야 한다.
담당자 필드를 추가하고 견적서 헤더에 출력하도록 했다. 사소해 보이지만 실제 B2B 거래에서는 없으면 안 되는 항목이었다.
다음 편에서는 알림 시스템으로 넘어간다. 카카오 알림톡 연동이 생각보다 순탄하지 않았다.