(재)여성기업종합지원센터
경력근거: 고객 미팅에서 니즈를 파악하고, 3일 안에 PoC를 만들어 다시 보여줍니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
앰플랩(amplab)은 고객 미팅에서 니즈를 파악해 사흘 안에 PoC 를 만들어 다시 보여주는 방식으로 일하고, 그 대상이 제조 현장입니다. 제조 고객사가 실제로 건네는 자료는 잘 정리된 API 응답이 아니라 ERP 에서 내려받은 발주 엑셀과 현장에서 손으로 적은 입고대장이라, 그 두 장을 사흘 안에 돌아가는 화면으로 바꾼 결과물이 이 회사의 일하는 속도와 같은 자리에 놓입니다. 다만 이 포트폴리오는 Next.js 와 Django 를 한 제품으로 잇는 기술을 보여주는 것이고, 제조 공정이나 원가 산정의 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
고객 미팅에서 니즈를 파악하고 사흘 안에 만들어 다시 보여주는 방식이라, 좁은 문제 하나를 끝까지 돌려 본 결과물이 서류에서 바로 읽힙니다.
주요업무 첫 항목이 제조 현장의 문제와 사용자 요구사항을 이해해 구조화하는 일이라, 현장이 쓰는 문서를 입력으로 받는 도구가 직무와 그대로 겹칩니다.
프런트는 Next.js·React·TypeScript, 백엔드는 Django 와 Django Ninja 로 못 박혀 있어 두 축을 하나로 이어 붙인 결과물이 필요합니다.
제조 고객사의 외주 가공 발주서와 입고대장을 받아 어긋난 건만 사유별로 내는 웹 관리자 페이지입니다. 발주 수량과 입고 수량, 업체 표기, 단가 기재 여부를 규칙으로 맞춰 보고 어느 줄에서 걸렸는지 원문을 보여줍니다.
발주서는 ERP 에서 내려받고 입고대장은 현장에서 손으로 적어, 월말마다 두 엑셀을 나란히 놓고 눈으로 맞춰 보던 자재 담당자입니다. 업체 이름이 조금씩 다르게 적혀 필터가 안 먹고, 분납으로 두 번 들어온 건은 매번 계산기를 두드립니다.
발주 수량과 입고 수량의 차이는 사람의 판단이 아니라 발주번호와 업체 표기를 맞춰 보는 규칙으로 결정됩니다. 담당자가 눈으로 훑던 자리를 사유 여섯 가지로 나눠 보여주면, 월말에 손대야 할 건만 남습니다.
자재 담당자가 발주서와 입고대장을 올려 수량 차이·미입고·업체 미매핑·단가 미기재를 사유별 목록으로 확인하고, 각 건의 근거 원문을 펼쳐 읽는 화면입니다.
정답을 붙인 28건으로 사유 분류 일치를 자동으로 재는 러너와, 담당자에게 물어볼 확인 목록을 따로 내는 화면입니다.
Django Channels 로 대조 진행률을 실시간으로 올리고, 회차별 결과를 쌓아 지난달과 이번달을 나란히 여는 화면입니다.
자재 담당자가 발주서와 입고대장 두 파일을 올리면 어긋난 건이 사유별로 나옵니다. 발주 16건과 입고 12건을 합친 28건에서 수량 차이·미입고·업체 미매핑·단가 미기재를 한 화면에서 읽습니다.
업체 표기가 흔들린 행과 단가가 비어 있는 행을 일부러 섞은 더미에서도 응답 형식이 그대로 유지됩니다. 정규화가 성공해 어긋난 값이 없는 행은 목록에 나오지 않습니다.
정답을 붙인 28건으로 사유 분류 일치를 냅니다. 단, 정답도 데이터도 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 사유 분류 정확도는 고객사의 실 발주서와 실 입고대장, 그리고 실제 자재 담당자가 있어야 측정됩니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실측 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 단가 미입력·업체 표기 흔들림·발주서에 없는 입고를 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
수량이 맞는다는 사실이 곧 품질이 맞는다는 뜻은 아닙니다. 불량 수량과 검수 결과는 따로 보는 자리라, 화면에서도 수량 대조와 품질 판정을 섞지 않습니다.
실제 도입에서는 고객사마다 발주 엑셀의 열 이름과 분납 표기 방식이 다릅니다. 열 매핑을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
이 포트폴리오는 Next.js 와 Django 를 잇는 기술을 보여줍니다. 제조 원가나 외주 단가 산정의 도메인 깊이는 주장하지 않습니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
제조 고객사가 ERP 에서 내려받은 외주 가공 발주서입니다. 대조의 왼쪽 축이고, 업체 등록 코드가 비어 있는 신규 업체와 단가가 아직 비어 있는 건이 섞여 있습니다.
| po_no | vendor_raw | part_no | process_raw | order_qty | unit_price | due_date |
|---|---|---|---|---|---|---|
| PO-2026-0101 | 대한정밀(주) | BRK-2210 | 밀링 | 500 | 3,200 | 2026-03-06 |
| PO-2026-0102 | 대한정밀 (주) | BRK-2215 | 밀링 | 300 | 3,400 | 2026-03-06 |
| PO-2026-0103 | 신광열처리 | SHF-0410 | 열처리 | 1,200 | 950 | 2026-03-09 |
| PO-2026-0104 | 신광열처리 | SHF-0412 | 열처리 | 800 | 950 | 2026-03-09 |
| PO-2026-0105 | 우진표면처리 | CVR-3301 | 도금 | 2,000 | 470 | 2026-03-11 |
아래 JSON Schema 에 정확히 맞는 외주 가공 발주 행 20건을 JSON 배열로 생성해 주세요. {"type":"object","properties":{"po_no":{"type":"string"},"vendor_raw":{"type":"string"},"part_no":{"type":"string"},"process_raw":{"type":"string"},"order_qty":{"type":"integer","minimum":0},"unit_price":{"type":["integer","null"],"minimum":0},"due_date":{"type":"string"},"remark":{"type":["string","null"]}},"required":["po_no","vendor_raw","part_no","process_raw","order_qty","due_date"]} 조건: po_no 는 PO-2026-01NN 형식으로 서로 다르게, 공정은 밀링·열처리·도금·기어절삭·선반·주조·단조 중에서 고릅니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 같은 업체를 '대한정밀(주)' 와 '대한정밀 (주)' 처럼 다르게 적은 행 2건, unit_price 가 null 인 행 1건, vendor_id 가 null 인 신규 업체 행 1건. 형식을 깨뜨리는 시도(열 이름 한글화, 숫자 칸에 '미정' 입력, UTF-8 BOM 이 붙은 CSV)는 데이터가 아니라 별도 3건의 파일로 만들어 읽기 계층을 시험합니다. JSON 배열만 출력해 주세요.{"type":"object","properties":{"po_no":{"type":"string"},"vendor_raw":{"type":"string"},"part_no":{"type":"string"},"process_raw":{"type":"string"},"order_qty":{"type":"integer","minimum":0},"unit_price":{"type":["integer","null"],"minimum":0},"due_date":{"type":"string"},"remark":{"type":["string","null"]}},"required":["po_no","vendor_raw","part_no","process_raw","order_qty","due_date"]}현장 검수 담당자가 손으로 적는 입고대장입니다. 대조의 오른쪽 축이고, 발주번호를 옮겨 적다 표기가 흔들린 행과 입고 수량을 아직 못 적은 행이 섞여 있습니다.
| receipt_no | po_no_raw | vendor_raw | received_qty | defect_qty | received_at | inspector |
|---|---|---|---|---|---|---|
| RC-2026-0301 | PO-2026-0101 | 대한정밀(주) | 480 | 0 | 2026-03-05 | 김선호 |
| RC-2026-0302 | PO-2026-0102 | 대한정밀 (주) | 300 | 0 | 2026-03-06 | 김선호 |
| RC-2026-0303 | PO-2026-0103 | 신광열처리 | 1,200 | 5 | 2026-03-09 | 박유정 |
| RC-2026-0304 | PO-2026-0105 | 우진표면처리 | 1,200 | 0 | 2026-03-10 | 박유정 |
| RC-2026-0305 | PO-2026-0105 | 우진표면처리 | 800 | 0 | 2026-03-12 | 박유정 |
아래 JSON Schema 에 정확히 맞는 입고대장 행 16건을 JSON 배열로 생성해 주세요. {"type":"object","properties":{"receipt_no":{"type":"string"},"po_no_raw":{"type":"string"},"vendor_raw":{"type":"string"},"received_qty":{"type":["integer","null"],"minimum":0},"defect_qty":{"type":["integer","null"],"minimum":0},"received_at":{"type":"string"},"inspector":{"type":"string"},"note_raw":{"type":["string","null"]}},"required":["receipt_no","po_no_raw","vendor_raw","received_at","inspector"]} 조건: po_no_raw 는 발주서의 po_no 를 옮겨 적은 값이고 receipt_no 는 RC-2026-03NN 형식으로 서로 다르게 씁니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: 같은 po_no_raw 가 두 행으로 들어온 분납 1쌍, received_qty 가 null 인 행 1건, 발주서에 없는 po_no_raw 행 1건, vendor_raw 표기가 발주서와 미세하게 다른 행 1건. 형식을 깨뜨리는 시도(날짜를 '3/5' 로 적기, 수량 칸에 '확인중' 입력, 머리글 행이 중간에 다시 나오는 엑셀)는 데이터가 아니라 별도 3건의 파일로 만들어 읽기 계층을 시험합니다. JSON 배열만 출력해 주세요.{"type":"object","properties":{"receipt_no":{"type":"string"},"po_no_raw":{"type":"string"},"vendor_raw":{"type":"string"},"received_qty":{"type":["integer","null"],"minimum":0},"defect_qty":{"type":["integer","null"],"minimum":0},"received_at":{"type":"string"},"inspector":{"type":"string"},"note_raw":{"type":["string","null"]}},"required":["receipt_no","po_no_raw","vendor_raw","received_at","inspector"]}계약된 외주 업체 목록입니다. 발주서·입고 기록에 적히는 업체 표기를 표준 식별자로 모으고, 계약이 끝난 업체를 함께 담았습니다.
| vendor_id | vendor_alias | process | contract_status |
|---|---|---|---|
| hanil_heat | 한일열처리 | 열처리 | active |
| hanil_heat | 한일 열처리 | 열처리 | active |
| daesung_plating | 대성도금 | 도금 | active |
| jinhung_cnc | 진흥정밀 | 절삭 | active |
| sunjin_press | 선진프레스 | 프레스 | active |
아래 JSON Schema 에 맞는 vendor_master 행을 더 만들어 주세요. {"type":"object","properties":{"vendor_id":{"type":"string"},"vendor_alias":{"type":"string"},"process":{"type":"string"},"contract_status":{"type":"string"}},"required":["vendor_id","vendor_alias","process","contract_status"]} vendor_alias 는 발주서나 입고 기록에 적힐 수 있는 업체 표기입니다.{"type":"object","properties":{"vendor_id":{"type":"string"},"vendor_alias":{"type":"string"},"process":{"type":"string"},"contract_status":{"type":"string"}},"required":["vendor_id","vendor_alias","process","contract_status"]}| Python | 엑셀을 읽어 발주와 입고를 맞춰 보는 대조 규칙을 씁니다 |
| Django | 업로드·대조·회차 조회를 담는 메인 웹 프레임워크입니다 |
| Django Ninja | 화면이 부르는 대조 실행과 결과 조회 REST API 를 만듭니다 |
| PostgreSQL | 발주·입고 원본과 대조 결과를 회차별로 쌓습니다 |
| Redis | 같은 파일을 다시 올리면 지난 대조 결과를 캐시에서 되돌려 재계산을 건너뜁니다 |
| Django Channels | 행이 많은 파일의 대조 진행률을 화면으로 실시간 전송합니다 |
| JWT | 고객사별 담당자 계정을 토큰으로 나눠 자기 사업장 자료만 열게 합니다 |
| Next.js | 사유별 목록 화면과 상세 패널을 서버 렌더링으로 그립니다 |
| React | 사유 배지·목록·근거 펼침 패널을 컴포넌트로 나눕니다 |
| TypeScript | Django Ninja 가 내는 응답 타입을 화면 컴포넌트까지 그대로 잇습니다 |
| Tailwind CSS | 사유 배지 색과 표 밀도를 화면 전체에서 같게 맞춥니다 |
| Framer | 1주차 구현 전에 담당자에게 보여줄 화면 흐름을 프로토타입으로 먼저 확인합니다 |
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 사유 분류가 사람 판단과 얼마나 맞는지 재는 러너를 붙입니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 진행률 실시간 전송과 회차 비교를 붙입니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
reason_code사유 라벨 · po_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
qty_gap입고 수량 부족qty_gap_unverifiable입고 수량 미기재price_missing단가 미기재vendor_unmapped업체 미매핑po_unmatched발주서에 없는 입고not_received미입고발주와 입고를 맞춰 본 뒤 담당자가 손봐야 할 건만 사유별로 냅니다. 어긋난 값이 없는 건은 목록에 나오지 않습니다.
{"type":"object","properties":{"po_no":{"type":"string"},"receipt_no":{"type":["string","null"]},"vendor_name":{"type":["string","null"]},"reason_code":{"type":"string","enum":["qty_gap","qty_gap_unverifiable","price_missing","vendor_unmapped","po_unmatched","not_received"]},"qty_gap":{"type":["integer","null"]},"summary":{"type":"string"},"evidence":{"type":"array","items":{"type":"object","properties":{"dataset":{"type":"string"},"field":{"type":"string"},"quote":{"type":"string"},"value":{"type":["number","string","null"]}},"required":["dataset","field"]}}},"required":["po_no","reason_code","summary","evidence"]}[
{
"po_no": "PO-2026-0101",
"receipt_no": "RC-2026-0301",
"vendor_name": "대한정밀(주)",
"reason_code": "qty_gap",
"qty_gap": 20,
"summary": "발주 500 개 가운데 480 개가 들어와 20 개가 모자랍니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "order_qty",
"quote": "500",
"value": 500
},
{
"dataset": "receiving_log",
"field": "received_qty",
"quote": "480",
"value": 480
}
]
},
{
"po_no": "PO-2026-0113",
"receipt_no": "RC-2026-0310",
"vendor_name": "신광열처리",
"reason_code": "qty_gap",
"qty_gap": 50,
"summary": "발주 950 개 가운데 900 개가 들어와 50 개가 모자랍니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "order_qty",
"quote": "950",
"value": 950
},
{
"dataset": "receiving_log",
"field": "received_qty",
"quote": "900",
"value": 900
}
]
},
{
"po_no": "PO-2026-0107",
"receipt_no": "RC-2026-0307",
"vendor_name": "한독기어",
"reason_code": "qty_gap_unverifiable",
"qty_gap": null,
"summary": "입고 수량이 비어 있어 모자란 개수를 낼 수 없습니다. 검수 담당자에게 수량을 물어야 합니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "order_qty",
"quote": "240",
"value": 240
},
{
"dataset": "receiving_log",
"field": "received_qty",
"value": null
},
{
"dataset": "receiving_log",
"field": "note_raw",
"quote": "수량 확인 중",
"value": "수량 확인 중"
}
]
},
{
"po_no": "PO-2026-0106",
"receipt_no": "RC-2026-0306",
"vendor_name": "우진표면처리",
"reason_code": "price_missing",
"qty_gap": null,
"summary": "수량은 1500 개로 맞지만 단가가 비어 있어 입고 금액을 세울 수 없습니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "unit_price",
"value": null
},
{
"dataset": "outsourcing_orders",
"field": "remark",
"quote": "단가 협의 중",
"value": "단가 협의 중"
},
{
"dataset": "receiving_log",
"field": "received_qty",
"quote": "1500",
"value": 1500
}
]
},
{
"po_no": "PO-2026-0109",
"receipt_no": "RC-2026-0308",
"vendor_name": null,
"reason_code": "vendor_unmapped",
"qty_gap": null,
"summary": "업체 등록 코드가 비어 있어 「미래CNC」를 업체 마스터에 붙이지 못했습니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "vendor_raw",
"quote": "미래CNC",
"value": "미래CNC"
},
{
"dataset": "outsourcing_orders",
"field": "vendor_raw",
"value": "미래CNC"
},
{
"dataset": "outsourcing_orders",
"field": "remark",
"quote": "신규 업체, 등록 코드 발급 전",
"value": "신규 업체, 등록 코드 발급 전"
}
]
},
{
"po_no": "PO-2026-0199",
"receipt_no": "RC-2026-0311",
"vendor_name": "동방기공",
"reason_code": "po_unmatched",
"qty_gap": null,
"summary": "입고대장에 적힌 발주번호가 발주서에 없어 어느 발주의 입고인지 붙이지 못했습니다.",
"evidence": [
{
"dataset": "receiving_log",
"field": "po_no_raw",
"quote": "PO-2026-0199",
"value": "PO-2026-0199"
},
{
"dataset": "receiving_log",
"field": "note_raw",
"quote": "발주서를 못 찾음",
"value": "발주서를 못 찾음"
}
]
},
{
"po_no": "PO-2026-0104",
"receipt_no": null,
"vendor_name": "신광열처리",
"reason_code": "not_received",
"qty_gap": null,
"summary": "납기가 지났는데 입고대장에 해당 발주번호가 한 줄도 없습니다.",
"evidence": [
{
"dataset": "outsourcing_orders",
"field": "po_no",
"quote": "PO-2026-0104",
"value": "PO-2026-0104"
},
{
"dataset": "outsourcing_orders",
"field": "due_date",
"quote": "2026-03-09",
"value": "2026-03-09"
}
]
}
][자동 검증 · 러너가 매 실행마다 겁니다] 1. reason_code 값이 여섯 값 목록에 있는지 확인합니다. 2. po_no·reason_code·summary·evidence 필수 필드가 모두 존재하는지 확인합니다. 3. qty_gap 이 evidence 의 order_qty 와 received_qty 차이와 일치하는지 다시 계산합니다. reason_code 가 qty_gap 이면 qty_gap 은 정수이고, 그 밖이면 qty_gap 은 null 입니다. 4. evidence 의 field 가 datasets 컬럼에 실재하는지 확인합니다. 5. evidence 의 quote 가 datasets 샘플의 원문 그대로인지 확인합니다. 6. received_qty 와 order_qty 가 0 이상 범위인지 확인합니다. 7. summary 가 존댓말 한 문장인지 확인합니다. [수동 검증 · 사람이 한 번 봅니다] 사유를 잘못 골랐는지, 담당자가 읽고 바로 손댈 수 있는 문장인지를 표본 10건에서 눈으로 봅니다. [정상으로 처리돼 출력이 없는 행] 이 산출물은 어긋난 건만 냅니다. 그래서 아래 행들은 처리에 성공했다는 뜻이고 목록에 나오지 않습니다. 분납으로 같은 발주번호가 두 번 들어온 중복 행 RC-2026-0304 와 RC-2026-0305 는 입고 수량 1200 과 800 을 합치면 발주 수량 2000 과 같아 어긋난 값이 없습니다. 업체명이 「대한정밀(주)」와 「대한정밀 (주)」로 흔들린 PO-2026-0102 와 RC-2026-0302 는 정규화가 성공해 같은 업체로 붙었고 수량도 300 개로 맞습니다.