에스투더블유
경력근거: OCR·문서 파싱·요약·정보 추출로 비정형 문서를 구조화 데이터로 가공
에스투더블유 FDE(AI 엔지니어링)는 고객의 핵심 문제를 정의하고 요구사항을 정량 지표로 환산해 해결 전략을 세우며, 문제를 지표로 정의하고 원인을 설명 가능하게 만들어 개선을 제어하는 방식으로 일합니다. 고객사 ERP 추출 엑셀 두 장을 받아 품목별 오차와 편향을 계산하고 그 원인을 규칙으로 나눠 다음 달 영향 수량을 내는 이 도구는 그 핵심(정량 환산·원인의 설명 가능성·Rule Engine 기반 판정·리눅스 CLI 실행)을 정면으로 보여줍니다. 수요예측 모델링·재고 정책·SCM 운영 같은 공급망 도메인 전문성은 포트폴리오로 증명되지 않으므로 합성 ERP 추출본으로 기술만 보이고, 그 깊이는 면접에서 솔직히 다룹니다. 지저분한 엑셀을 표준 스키마로 맞춰 붙이고 숫자는 전부 산술로 확정해 LLM 은 매핑 후보와 문안만 맡기는 구조는 일반적인 LLM 활용 맥락으로는 그대로 만들 수 없습니다.
고객의 핵심 문제를 정의하고 요구사항을 정량 지표로 환산하는 일이 주요업무입니다. '계획이 늘 크다'는 현장 말을 품목별 오차 수량과 편향으로 환산하는 이 도구가 정면으로 정렬됩니다.
Rule Engine/RBS 로 문제를 풀고 기존 기법의 성능과 실패 원인을 정량 분석해 개선 방향을 제시합니다. 계획의 오차를 원인별로 나눠 수치로 지목하는 이 도구와 정렬됩니다.
Python 으로 데이터 처리 코드를 써서 재현 가능한 실험을 하고 리눅스·CLI 환경에서 개발합니다. 화면 없이 터미널에서 돌려 같은 입력에 같은 수치가 나오는 이 도구와 정렬됩니다.
고객사가 ERP 에서 내려받은 월별 수요계획 엑셀과 출하 실적 엑셀 두 장을 인자로 받아, 품목·기간 키로 맞춰 붙인 뒤 계획 대비 실적의 오차와 편향을 계산하는 CLI입니다. 오차는 상시 과다계획·상시 과소계획·단월 급변·미매칭·단위 환산 불가로 나눠 원인을 지목하고, 다음 달 계획을 그대로 쓸 때 생길 과잉 재고나 결품 수량을 함께 냅니다. 수요를 새로 예측하는 모델이 아니라 이미 나와 있는 계획과 실적 사이의 차이를 산술로 계산해 원인과 다음 달 영향을 답하는 도구입니다. 결과는 터미널 요약과 md 리포트 파일로 나오고 화면은 없습니다.
에스투더블유 FDE 가 파견 나간 고객사(식품 제조·유통사)의 수요계획 담당자입니다. 매달 ERP 에서 계획과 실적을 따로 내려받아 엑셀에서 품목을 눈으로 맞춰 보며 '이번에도 계획이 컸다'고 말하지만, 어느 품목에서 얼마나 컸는지, 늘 그런 건지 지난달만 그런 건지, 다음 달에 재고가 얼마나 남을지는 수치로 답하지 못하는 사람입니다.
고객사 계획·실적 엑셀을 표준 스키마로 맞춰 붙이고 오차 계산과 원인 분해 규칙을 얹으면, (a) 감으로 말하던 '계획이 늘 크다'를 품목별 수량과 비율로 답하고, (b) 오차의 원인을 상시 편향·단월 급변·미매칭으로 나눠 보이며, (c) 다음 달 계획을 그대로 쓸 때의 과잉·결품 수량을 미리 냅니다.
계획 엑셀과 실적 엑셀을 인자로 받아 품목·기간 정합, 오차·편향 계산, 원인 분해, 다음 달 영향 수량까지 내고 터미널 요약과 md 리포트 파일을 떨어뜨리는 동작하는 CLI입니다. 엑셀은 합성 ERP 추출본 더미로 시작하고, 중간 산출물은 로컬 Parquet 파일 한 곳에만 둡니다.
더미에 심은 오차 원인 정답과 담당자 2명이 매긴 라벨을 도구 결과와 맞춰 일치율과 어긋난 사례를 정리해 내는 비교 실행 모드입니다.
여러 사업장의 월간 엑셀을 Airflow 로 매달 자동 실행하고, DuckDB 와 Spark 로 사업장·품목군별 오차 추이를 집계해 리포트 끝에 붙이는 배치입니다.
품목 40종·12개월치 계획 엑셀과 실적 엑셀 두 장을 넣으면 품목별 오차표가 터미널에 뜨고 담당자에게 그대로 보낼 md 리포트 파일이 몇 초 안에 떨어지는 걸 시연합니다
일부러 만든 지저분한 입력 25종(품목코드 표기 흔들림·기간 표기 혼용·소계 행·환산표에 없는 단위·한쪽에만 있는 품목)을 넣어도 계산이 중단되지 않고, 계산에서 뺀 행은 사유를 달고 리포트 끝의 확인 목록에 남습니다
더미에 심은 오차 원인 18건 중 도구가 같은 원인으로 지목한 게 16건(약 89%)입니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 '성능 검증'이 아니라 '데모 조건에서의 동작'임을 명시합니다
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 전부 작성자가 만든 더미데이터 자가채점이며, 실제 정확도는 에스투더블유가 계약한 고객사의 실 ERP 추출본과 실 수요계획 담당자가 있어야 측정 가능합니다. 이 도구는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다
이건 고객사·실데이터 없이 혼자 만든 포트폴리오입니다. 수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 품목코드 표기 흔들림, 기간 표기 혼용, 소계 행, 환산표에 없는 단위, 한쪽에만 있는 품목 같은 경계 케이스를 더미에 일부러 섞어, 깨끗한 표만 계산하는 게 아님을 보여줍니다
이 포트폴리오가 보여주는 것은 고객사 엑셀을 표준 스키마로 맞춰 붙이고 오차·편향을 계산해 원인을 규칙으로 분해하는 기술입니다. 수요예측 모델링·재고 정책·SCM 운영 같은 공급망 도메인 전문성은 포트폴리오로 증명되지 않으므로 주장하지 않으며, 그 깊이는 실제 수요계획 담당자의 몫이고 면접에서 모르면 모른다고 말하는 편이 낫습니다. 데모는 합성 ERP 추출본으로 기술만 보여줍니다
LLM 이 낸 헤더 매핑은 그럴듯해도 틀릴 수 있습니다. 매핑마다 원본 헤더 문자열을 인용하게 하고 표준 필드 사전·값 타입으로 다시 검증해, 통과하지 못하면 계산에 넣지 않고 unmapped 로 격리해 확인 목록에 올립니다
단위와 품목코드 체계가 회사마다 다릅니다. 환산 계수가 등록되지 않은 단위는 추측하지 않고 '환산 불가'로 계산에서 빼며, 실제 도입 시에는 고객사의 코드 체계·환산표·상시 편향 임계값을 다시 맞춰야 합니다(도입 단계 과제로 분리)
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사가 ERP 에서 내려받은 월별 수요계획 엑셀의 행. 정합·계산 규칙이 가장 먼저 읽는 표입니다. 품목코드 표기 흔들림·기간 표기 혼용·소계 행·환산표에 없는 단위를 일부러 섞고, 실적 쪽에 짝이 없는 품목도 넣어 미매칭을 재현합니다.
| row_id | item_code | item_name | period | plan_qty | unit | plant | source_sheet |
|---|---|---|---|---|---|---|---|
| p_001 | A-1002 | 냉장 밀키트 400g | 2026-01 | 1,200 | EA | P1 | 수요계획_2026상반기 |
| p_002 | A-1002 | 냉장 밀키트 400g | 2026-02 | 1,300 | EA | P1 | 수요계획_2026상반기 |
| p_003 | A-1002 | 냉장 밀키트 400g | 2026-03 | 1,250 | EA | P1 | 수요계획_2026상반기 |
| p_004 | B 2010 | 상온 소스 180ml | 2026-01 | 800 | BOX | P1 | 수요계획_2026상반기 |
| p_005 | B-2010 | 상온 소스 180ml | 2026-02 | 850 | BOX | P1 | 수요계획_2026상반기 |
아래 Zod 스키마에 맞는 수요계획 엑셀 행 70개를 JSON 배열로 생성합니다. 품목 12~15종, 기간 2026-01부터 2026-06까지, 사업장 P1·P2 로 정상 행 위주에 지저분한 행을 섞습니다.
{"type":"object","properties":{"row_id":{"type":"string"},"item_code":{"type":"string"},"item_name":{"type":"string"},"period":{"type":"string"},"plan_qty":{"type":"number"},"unit":{"type":"string","enum":["EA","BOX","KG"]},"plant":{"type":"string"},"source_sheet":{"type":"string"}},"required":["row_id","item_code","item_name","period","plan_qty","unit","plant","source_sheet"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라(깨끗한 표만 계산하는 함정 회피): (1) 같은 품목의 코드 표기 흔들림('B-2010'/'B 2010'/'b-2010') 3건 이상, (2) 기간 표기 혼용('2026/01'·'2026.01'·'26-01') 3건 이상, (3) item_code 가 '소계'·'합계'인 집계 행 2건(계산에서 빠져야 함), (4) unit 이 BOX·KG 라 환산표가 있어야 EA 로 계산되는 행 5건 이상, (5) 실적 쪽에 짝이 없어 미매칭이 될 품목 2종. plan_qty 는 품목별로 월마다 조금씩 흔들리게 하고, 일부 품목은 실적보다 늘 크게(상시 과다계획), 다른 품목은 한 달만 크게 어긋나도록(단월 급변) 값을 잡아라. JSON 배열만.{"type":"object","properties":{"row_id":{"type":"string"},"item_code":{"type":"string"},"item_name":{"type":"string"},"period":{"type":"string"},"plan_qty":{"type":"number"},"unit":{"type":"string","enum":["EA","BOX","KG"]},"plant":{"type":"string"},"source_sheet":{"type":"string"}},"required":["row_id","item_code","item_name","period","plan_qty","unit","plant","source_sheet"]}같은 고객사 ERP 에서 내려받은 출하 실적 엑셀의 행. 계획과 품목·기간 키로 맞춰 붙여 오차를 계산하는 짝 데이터입니다. 반품이 있어 순출하를 따로 계산해야 하고, 계획에 없는 신규 품목과 계획에만 있는 품목을 함께 넣어 미매칭을 재현합니다.
| row_id | item_code | period | actual_qty | return_qty | unit | plant | source_sheet |
|---|---|---|---|---|---|---|---|
| a_001 | A-1002 | 2026-01 | 1,010 | 20 | EA | P1 | 출하실적_2026Q1 |
| a_002 | A-1002 | 2026-02 | 1,080 | 15 | EA | P1 | 출하실적_2026Q1 |
| a_003 | A-1002 | 2026-03 | 1,120 | 0 | EA | P1 | 출하실적_2026Q1 |
| a_004 | B-2010 | 2026-01 | 760 | 5 | BOX | P1 | 출하실적_2026Q1 |
| a_005 | B-2010 | 2026-02 | 900 | 0 | BOX | P1 | 출하실적_2026Q1 |
아래 Zod 스키마에 맞는 출하 실적 엑셀 행 70개를 JSON 배열로 생성합니다. demand_plan_rows 와 같은 품목·기간·사업장 축을 쓰되 완전히 겹치지는 않게 합니다.
{"type":"object","properties":{"row_id":{"type":"string"},"item_code":{"type":"string"},"period":{"type":"string"},"actual_qty":{"type":"number"},"return_qty":{"type":"number"},"unit":{"type":"string","enum":["EA","BOX","KG"]},"plant":{"type":"string"},"source_sheet":{"type":"string"}},"required":["row_id","item_code","period","actual_qty","return_qty","unit","plant","source_sheet"]}
쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 계획에 없는 신규 품목 2종(미매칭이 되어야 함), (2) 계획에는 있는데 실적이 통째로 빠진 품목 2종, (3) return_qty 가 0 이 아니라 순출하(actual_qty - return_qty)로 계산해야 하는 행 8건 이상, (4) 계획과 unit 이 다르게 적힌 행 2건(환산 규칙이 필요), (5) 한 달만 계획 대비 40% 이상 튀는 단월 급변 3건. 상시 과다계획 품목은 매달 실적이 계획보다 꾸준히 작게, 상시 과소계획 품목은 꾸준히 크게 잡아 편향이 재현되도록 하라. JSON 배열만.{"type":"object","properties":{"row_id":{"type":"string"},"item_code":{"type":"string"},"period":{"type":"string"},"actual_qty":{"type":"number"},"return_qty":{"type":"number"},"unit":{"type":"string","enum":["EA","BOX","KG"]},"plant":{"type":"string"},"source_sheet":{"type":"string"}},"required":["row_id","item_code","period","actual_qty","return_qty","unit","plant","source_sheet"]}| Python | 엑셀 적재·정합·오차 계산·원인 분해·CLI 명령을 구현하는 언어입니다(Typer 로 서브커맨드를 나눕니다) |
| pandas | 계획·실적 엑셀을 읽어 품목·기간 키로 맞춰 붙이고 오차·편향을 계산하는 처리층입니다 |
| NumPy | 절대오차·편향합·품목별 기여도 같은 수치를 계산합니다 |
| Pydantic | 표준 필드 스키마와 단위 환산표를 검증해 잘못 매핑된 열이 계산에 들어오지 못하게 막습니다 |
| DuckDB | 정규화한 계획·실적 파일을 서버 없이 그 자리에서 SQL 로 집계합니다 |
| Parquet | 정규화 결과를 다음 달 실행에서 다시 쓰도록 로컬 파일로 남깁니다 |
| Linux · CLI | 고객사 폐쇄망 서버의 터미널에서 그대로 실행하는 실행 환경입니다 |
| Docker | 폐쇄망에 반입할 단일 이미지로 도구와 의존성을 묶습니다 |
| Airflow | 여러 사업장의 월간 엑셀 처리를 매달 자동 실행합니다(3주차) |
| Spark | 사업장·품목군이 늘었을 때 오차 추이를 대량 집계합니다(3주차) |
| Langfuse | 헤더 매핑과 리포트 문안 생성에 쓴 LLM 호출의 토큰·지연·실패를 기록합니다 |
| Claude | 엑셀 헤더의 표준 필드 매핑 후보를 내고 계산 결과를 담당자용 문장으로 옮기는 LLM 입니다 |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 2주차(원인 지목이 맞았는지 확인)로 진행합니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우면 3주차(월간 배치 자동화 + 사업장별 오차 추이)로 진행합니다.
품목 1건의 오차 판정 카드. 계획·실적 합계와 오차, 원인, 다음 달 영향 수량을 한 스키마로 담아 터미널 요약과 md 리포트에 그대로 씁니다. 수요를 새로 예측하는 값이 아니라 이미 있는 두 수치의 차이를 계산한 결과입니다.
{"type":"object","properties":{"finding_id":{"type":"string"},"item_code":{"type":"string"},"period_range":{"type":"string"},"plan_total":{"type":"number"},"actual_total":{"type":"number"},"gap_qty":{"type":"number"},"gap_pct":{"type":["number","null"]},"bias":{"type":"string","enum":["over_plan","under_plan","matched"]},"cause":{"type":"string","enum":["persistent_over_plan","persistent_under_plan","single_month_spike","unmatched_item","unit_unconvertible","no_signal"]},"next_month_impact_qty":{"type":["number","null"]},"impact_kind":{"type":"string","enum":["excess_stock","stockout","none","unknown"]},"reason":{"type":"string","minLength":20},"evidence":{"type":"string","minLength":8},"hedge":{"type":["string","null"]}},"required":["finding_id","item_code","period_range","plan_total","actual_total","gap_qty","gap_pct","bias","cause","next_month_impact_qty","impact_kind","reason","evidence","hedge"]}[
{
"input_ref": "A-1002 (2026-01~03, 상시 과다계획)",
"output": {
"finding_id": "f_001",
"item_code": "A-1002",
"period_range": "2026-01~2026-03",
"plan_total": 3750,
"actual_total": 3175,
"gap_qty": 575,
"gap_pct": 15.3,
"bias": "over_plan",
"cause": "persistent_over_plan",
"next_month_impact_qty": 190,
"impact_kind": "excess_stock",
"reason": "세 달 모두 계획이 순출하보다 컸고 월평균 192개씩 남았습니다. 다음 달 계획을 그대로 쓰면 같은 폭의 과잉 재고가 생깁니다.",
"evidence": "plan_total=3750, actual_total=3175(반품 35 차감), 3개월 연속 over_plan",
"hedge": "월평균 편향을 그대로 적용한 값이라 판촉이 잡히면 달라집니다"
}
},
{
"input_ref": "C-3005 (2026-02 단월 급변)",
"output": {
"finding_id": "f_002",
"item_code": "C-3005",
"period_range": "2026-01~2026-02",
"plan_total": 820,
"actual_total": 1045,
"gap_qty": -225,
"gap_pct": -27.4,
"bias": "under_plan",
"cause": "single_month_spike",
"next_month_impact_qty": null,
"impact_kind": "unknown",
"reason": "2026-01 오차는 5개로 작았는데 2026-02 에만 220개가 몰려 어긋났습니다. 상시 편향이 아니라 한 달짜리 급변이라 추세로 보지 않습니다.",
"evidence": "2026-01 gap=-5, 2026-02 gap=-220",
"hedge": "단월 급변이라 다음 달 영향은 추정하지 않고 담당자 확인이 필요합니다"
}
},
{
"input_ref": "G-7020 (계획에 없는 품목)",
"output": {
"finding_id": "f_003",
"item_code": "G-7020",
"period_range": "2026-02",
"plan_total": 0,
"actual_total": 250,
"gap_qty": -250,
"gap_pct": null,
"bias": "under_plan",
"cause": "unmatched_item",
"next_month_impact_qty": null,
"impact_kind": "unknown",
"reason": "실적에는 250개가 있는데 계획 엑셀에 같은 품목코드가 없습니다. 계획 누락인지 코드 표기가 다른 것인지 확정할 수 없어 계산에서 빼고 확인 목록에 올립니다.",
"evidence": "plan_total=0, actual row a_010 존재",
"hedge": "오차율은 분모가 0이라 계산하지 않습니다"
}
},
{
"input_ref": "E-5001 (환산표에 없는 단위)",
"output": {
"finding_id": "f_004",
"item_code": "E-5001",
"period_range": "2026-02",
"plan_total": 300,
"actual_total": 268,
"gap_qty": 32,
"gap_pct": null,
"bias": "matched",
"cause": "unit_unconvertible",
"next_month_impact_qty": null,
"impact_kind": "unknown",
"reason": "계획과 실적이 모두 BOX 인데 이 품목의 BOX 당 낱개 수가 환산표에 없어 EA 기준 수량으로 맞출 수 없습니다. 추측하지 않고 계산에서 빼 확인 목록에 올립니다.",
"evidence": "unit=BOX, 환산표에 E-5001 계수 없음",
"hedge": "환산 계수가 등록되면 다시 계산합니다"
}
}
][자동 검증] 1. Pydantic 으로 출력 형식(필드·enum)이 스키마를 통과하는지 확인합니다. 2. gap_qty 가 plan_total 에서 순출하 합계(actual_qty 합에서 return_qty 합을 뺀 값)를 뺀 값과 소수점 오차 없이 일치하는지 다시 계산해 대조합니다. 3. cause 가 원인 분해 규칙(연속 편향 개월 수·단월 기여도 비중·키 매칭 실패·환산표 미등록)과 일치하는지 봅니다. 4. unmatched_item 과 unit_unconvertible 은 next_month_impact_qty 가 반드시 null 이 되도록 강제합니다. 5. 리포트 문안에 등장하는 숫자가 계산 결과 토큰 집합 안에 있는지 대조해 LLM 이 지어낸 수치를 차단합니다. [수동 검증] 1. 담당자 2명이 비동기로 원인을 라벨링해 심은 정답과의 일치율을 보고 어긋난 사례를 임계값 문제와 환산표 문제로 나눕니다. 2. 전체의 5% 를 사람이 직접 점검합니다.