부스터스
경력근거: 수기/엑셀로 흩어져 있던 업무를 시스템과 데이터 파이프라인으로 전환합니다
공고가 이 자리의 일로 '수기/엑셀로 흩어져 있던 업무를 시스템과 데이터 파이프라인으로 전환합니다' 와 '담당 도메인(마케팅/세일즈/SCM/재무 중)의 현업과 밀착해 업무를 분석' 을 적었습니다. 여러 채널로 파는 브랜드에서 재고와 광고는 서로 다른 팀이 보는데 어긋나면 손해는 함께 납니다. 이 포트폴리오는 그 두 숫자를 한 표에서 견주고, 팔 수 없는 상품에 나가는 광고비를 액수로 보여줍니다. 'SQL로 직접 데이터를 뜯어보고 분석할 수 있는 분' 을 앞세운 것도 판정마다 근거 행 값을 싣는 이 설계와 맞습니다. 이 포트폴리오가 보여주는 것은 재고 배분을 데이터로 두고 채널 수량과 맞춰 어긋남을 가르는 기술이며, 수요 예측이나 광고 운영 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
'수기/엑셀로 흩어져 있던 업무를 시스템과 데이터 파이프라인으로 전환합니다'. 재고 대조가 그 엑셀에 남아 있던 일입니다.
'담당 도메인(마케팅/세일즈/SCM/재무 중)의 현업과 밀착해 업무를 분석'. 광고와 재고가 만나는 자리를 골랐습니다.
'SQL로 직접 데이터를 뜯어보고 분석할 수 있는 분'. 판정 근거를 행 값으로 남겨 그 확인이 되게 했습니다.
채널에 걸어 둔 판매 수량이 창고에 실제로 있는 만큼인지 상품마다 가릅니다. 재고 배분표에 채널 품번과 창고 품번, 세트 하나에 든 낱개 수, 창고 수량, 안전 재고를 두고 채널에 걸린 수량과 맞춰, 팔 수 있는 양을 계산해 그 안이면 맞음으로 넘기고 넘치면 품절 위험으로 그 상품의 하루 광고비를 함께 냅니다. 일주일 판매 대비 재고가 너무 많으면 과잉으로 따로 묶고, 수량이 안 잡힌 줄과 배분표에 없는 품번·채널은 각각 다른 사유로 남깁니다. 재고와 판매 수치는 실 운영 자료 대신 지원자가 공개 채널 명세 양식으로 만든 것을 가정합니다.
여러 채널로 파는 브랜드 회사의 공급망 현업 한가운데 앉은 엔지니어입니다. 채널마다 재고를 따로 걸어 두고 창고 수량은 엑셀로 관리해, 품절 주문이 들어와 고객에게 취소 연락을 하고 나서야 어긋난 것을 알던 자리에 있습니다.
채널 품번마다 창고 품번과 세트 구성, 안전 재고를 배분표로 두고 걸린 수량과 맞추면, 팔 수 없는데 걸어 둔 상품이 매일 아침 목록으로 나오고 그 상품에 나가는 광고비가 함께 보인다는 가정입니다. 배분표에 없는 품번과 채널은 미매핑으로 빠지므로 아직 배분을 정하지 않은 자리가 그대로 드러납니다.
채널 노출 수량과 재고 배분표를 받아 상품마다 팔 수 있는 양을 계산하고 맞음·품절 위험·재고 과잉을 판정하며, 판정하지 못하면 수량 없음·품번 미매핑·채널 미등록 중 어느 쪽인지 붙여 채널별로 묶은 정합 리포트와 품절 위험 상품의 하루 광고비 합계를 터미널과 Markdown 파일로 내는 CLI 가 도는 완결품입니다.
같은 창고 품번을 여러 채널 상품이 나눠 쓰는 경우를 묶어 낱개 기준으로 다시 계산하고, 공개 양식으로 만든 상품 1500건에 손으로 계산한 정답 판정과 대조해 일치율을 냅니다.
품절 위험으로 걸린 상품을 광고비가 큰 순서로 세워 오늘 멈출 목록을 내고, 재고 과잉 상품은 소진까지 걸리는 주 수를 붙여 판매팀에 넘기는 단계입니다.
현업에 들어간 엔지니어가 채널 수량과 배분표를 넣으면 상품마다 정합 여부와 품절 위험 광고비가 채널별로 묶여 나오는 과정을 시연합니다.
공개 양식으로 만든 상품 1500건과 손으로 계산한 정답 판정을 대조해 일치율을 냅니다. 단, 정답과 수치를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
수량이 비었거나 채널 표기가 흔들리거나 배분표에 없는 품번이 섞인 입력 30종을 넣어도 출력 형식이 안 깨지고 사유별로 빠지는 비율을 보여줍니다.
안전 재고와 과잉 기준을 바꿨을 때 품절 위험 건수와 광고비 합계가 어떻게 움직이는지 회귀로 확인합니다.
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 작성자가 만든 자료의 자가채점이며, 실제 품절 취소 건수와 절감한 광고비는 부스터스의 실 재고와 실 광고 집행이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태까지 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아니므로, 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 붙였으니 잘 맞는 게 당연하므로, 창고 수량 결측과 낱개를 나눠 쓰는 세트, 배분표에 없는 기획 상품을 일부러 섞어 쉬운 줄만 푸는 함정을 피합니다.
이 포트폴리오가 보여주는 것은 재고 배분을 데이터로 두고 채널 수량과 맞춰 어긋남을 가르는 기술이며, 수요 예측이나 광고 운영 전문성은 주장하지 않습니다. 계산은 나눗셈과 뺄셈이 하고 LLM 은 담당자가 읽을 사유 문장만 맡습니다.
창고 재고는 오프라인 출고로도 줄어 이 계산과 어긋날 수 있어서, 판정을 확정이 아니라 그 시점의 진단으로 냅니다.
실제 도입 때는 채널과 기획 세트가 계속 늘어나므로, 배분표를 이어서 채우는 일을 운영 과제로 둡니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
채널마다 내려받은 노출 상품 목록입니다. 목록 번호와 채널 표기, 채널 품번, 채널에 걸어 둔 수량, 그 상품에 하루 쓰는 광고비, 최근 일주일 판매량, 내려받은 날을 한 행에 둡니다. 채널 표기는 내려받은 파일마다 'smartstore'와 'Smart Store'처럼 흔들립니다. 걸어 둔 수량이 빈 행, 판매가 거의 없는데 재고가 많은 행, 배분표에 없는 기획 세트와 신규 채널을 일부러 섞어 두어 사유가 갈리게 둡니다.
| listing_no | channel_raw | channel_sku_raw | listed_qty | daily_ad_krw | sold_7d | snapped_at |
|---|---|---|---|---|---|---|
| CL-001 | smartstore | BRD-POUCH-3SET | 40 | 180,000 | 63 | 2026-08-25 |
| CL-002 | smartstore | BRD-POUCH-1 | 120 | 40,000 | 22 | 2026-08-25 |
| CL-003 | Smart Store | BRD-CUBE-L | 85 | 60,000 | 40 | 2026-08-25 |
| CL-004 | amazon | EQ-SERUM-30-2P | 60 | 320,000 | 88 | 2026-08-25 |
| CL-005 | amazon | EQ-SERUM-30 | 200 | 150,000 | 40 | 2026-08-25 |
아래 JSON Schema 를 그대로 만족하는 채널 노출 행을 1500개 더 만들어 주세요. 채널을 대여섯 곳 두고 품번을 낱개와 세트로 섞으며, 채널 표기는 몇 행에서 띄어쓰기와 대소문자를 다르게 둡니다. 하루 광고비를 만원에서 40만원 사이로 두고 일주일 판매량은 상품 성격에 맞게 흩어 놓습니다. 2할은 걸어 둔 수량이나 광고비를 비우고, 몇 행은 판매가 한 자릿수인데 걸어 둔 수량을 세 자릿수로 두세요. 배분표에 없는 품번과 채널도 몇 행 넣습니다. 판정 결과나 팔 수 있는 양은 넣지 마세요.
{"type": "object", "properties": {"listing_no": {"type": "string"}, "channel_raw": {"type": "string"}, "channel_sku_raw": {"type": "string"}, "listed_qty": {"type": ["integer", "null"]}, "daily_ad_krw": {"type": ["integer", "null"]}, "sold_7d": {"type": ["integer", "null"]}, "snapped_at": {"type": "string"}}, "required": ["listing_no", "channel_raw", "channel_sku_raw", "listed_qty", "daily_ad_krw", "sold_7d", "snapped_at"]}{"type": "object", "properties": {"listing_no": {"type": "string"}, "channel_raw": {"type": "string"}, "channel_sku_raw": {"type": "string"}, "listed_qty": {"type": ["integer", "null"]}, "daily_ad_krw": {"type": ["integer", "null"]}, "sold_7d": {"type": ["integer", "null"]}, "snapped_at": {"type": "string"}}, "required": ["listing_no", "channel_raw", "channel_sku_raw", "listed_qty", "daily_ad_krw", "sold_7d", "snapped_at"]}채널 품번과 창고 재고를 잇는 배분표입니다. 배분 번호와 채널, 채널 품번, 창고 품번, 세트 하나에 든 낱개 수, 창고 수량, 안전 재고, 담당을 한 행에 둡니다. 같은 창고 품번을 여러 채널 상품이 나눠 쓰므로 한 상품만 보면 재고가 남아 보여도 함께 보면 모자랍니다. 창고 수량이 안 잡힌 행을 일부러 두고, 채널에 걸린 품번 중 일부는 넣지 않아 미매핑이 도출되게 둡니다.
| stock_no | channel_raw | channel_sku_raw | base_sku | unit_per_set | warehouse_qty | safety_qty | owner_name |
|---|---|---|---|---|---|---|---|
| WS-01 | smartstore | BRD-POUCH-3SET | BRD-POUCH | 3 | 36 | 2 | SCM |
| WS-02 | smartstore | BRD-POUCH-1 | BRD-POUCH | 1 | 36 | 5 | SCM |
| WS-03 | smartstore | BRD-CUBE-L | BRD-CUBE-L | 1 | 420 | 10 | SCM |
| WS-04 | amazon | EQ-SERUM-30-2P | EQ-SERUM-30 | 2 | 90 | 5 | SCM |
| WS-05 | amazon | EQ-SERUM-30 | EQ-SERUM-30 | 1 | 90 | 10 | SCM |
아래 JSON Schema 를 그대로 만족하는 배분 행을 60개 더 만들어 주세요. 같은 창고 품번을 여러 채널 상품이 나눠 쓰도록 두고 세트 상품에는 낱개 수를 둘에서 다섯 사이로 정합니다. 안전 재고는 2 에서 20 사이로 두고 몇 행은 창고 수량을 비웁니다. 채널에 걸린 품번 중 일부는 일부러 넣지 마세요 — 미매핑이 있어야 배분이 안 정해진 자리가 드러납니다. 판정 결과는 넣지 마세요.
{"type": "object", "properties": {"stock_no": {"type": "string"}, "channel_raw": {"type": "string"}, "channel_sku_raw": {"type": "string"}, "base_sku": {"type": "string"}, "unit_per_set": {"type": "integer"}, "warehouse_qty": {"type": ["integer", "null"]}, "safety_qty": {"type": "integer"}, "owner_name": {"type": "string"}}, "required": ["stock_no", "channel_raw", "channel_sku_raw", "base_sku", "unit_per_set", "warehouse_qty", "safety_qty", "owner_name"]}{"type": "object", "properties": {"stock_no": {"type": "string"}, "channel_raw": {"type": "string"}, "channel_sku_raw": {"type": "string"}, "base_sku": {"type": "string"}, "unit_per_set": {"type": "integer"}, "warehouse_qty": {"type": ["integer", "null"]}, "safety_qty": {"type": "integer"}, "owner_name": {"type": "string"}}, "required": ["stock_no", "channel_raw", "channel_sku_raw", "base_sku", "unit_per_set", "warehouse_qty", "safety_qty", "owner_name"]}| Kotlin | 채널 수량과 배분표를 맞춰 팔 수 있는 양을 계산하는 판정기를 맡습니다. |
| Spring Batch | 채널마다 내려받은 재고 파일을 묶음으로 읽어 매일 아침 돌립니다. |
| MySQL | 채널 노출 수량과 창고 재고, 판정 결과를 담습니다. |
| SQL | 채널별 어긋남 건수와 품절 위험 상품의 광고비 합계를 집계합니다. |
✗ fail → 미매핑이 절반을 넘으면 배분표를 먼저 채웁니다.
✓ pass → 2주차 낱개 공유 계산으로 넘어갑니다.
✗ fail → 어긋난 상품을 모아 안전 재고와 세트 구성을 다시 봅니다.
✓ pass → 3주차 광고 중지 목록으로 넘어갑니다.
채널 상품 하나와 재고 배분이 만나는 자리마다 걸어 둔 수량이 창고와 맞는지 정합니다. 어긋나면 품절 위험·재고 과잉·수량 없음·품번 미매핑·채널 미등록 중 어느 쪽인지 싣고, 품절 위험이면 그 상품 하루 광고비를 함께 냅니다. 리포트는 이 사유 코드로 묶습니다.
{"type": "object", "properties": {"verdict_no": {"type": "string"}, "listing_no": {"type": "string"}, "channel_raw": {"type": "string"}, "channel_sku_raw": {"type": "string"}, "stock_no": {"type": ["string", "null"]}, "gap_code": {"type": "string", "enum": ["in_sync", "oversell_risk", "dead_stock", "sku_unmapped", "qty_missing", "channel_unregistered"]}, "sellable_qty": {"type": ["integer", "null"], "description": "warehouse_qty 를 unit_per_set 으로 나눈 뒤 safety_qty 를 뺀 팔 수 있는 세트 수입니다. 창고 재고가 잡힌 줄에만 채웁니다."}, "exposed_ad_krw": {"type": ["integer", "null"], "description": "품절 위험으로 판정된 줄의 daily_ad_krw 를 그대로 적은 하루 노출 광고비입니다."}, "base_sku": {"type": ["string", "null"]}, "owner_name": {"type": ["string", "null"]}, "evidence": {"type": "array", "items": {"type": "object", "properties": {"field": {"type": "string"}, "agg": {"type": ["string", "null"]}, "value": {}}, "required": ["field", "value"]}}, "summary": {"type": "string"}, "hedge": {"type": ["string", "null"]}}, "required": ["verdict_no", "listing_no", "channel_raw", "channel_sku_raw", "gap_code", "evidence", "summary"]}[
{
"verdict_no": "GV-001",
"listing_no": "CL-002",
"channel_raw": "smartstore",
"channel_sku_raw": "BRD-POUCH-1",
"stock_no": "WS-02",
"gap_code": "oversell_risk",
"sellable_qty": 31,
"exposed_ad_krw": 40000,
"base_sku": "BRD-POUCH",
"owner_name": "SCM",
"evidence": [
{
"field": "listed_qty",
"agg": null,
"value": 120
},
{
"field": "warehouse_qty",
"agg": null,
"value": 36
}
],
"summary": "120개를 걸어 두었는데 창고에서 팔 수 있는 것은 31개라 품절 뒤에도 광고가 돕니다.",
"hedge": null
},
{
"verdict_no": "GV-002",
"listing_no": "CL-001",
"channel_raw": "smartstore",
"channel_sku_raw": "BRD-POUCH-3SET",
"stock_no": "WS-01",
"gap_code": "oversell_risk",
"sellable_qty": 10,
"exposed_ad_krw": 180000,
"base_sku": "BRD-POUCH",
"owner_name": "SCM",
"evidence": [
{
"field": "unit_per_set",
"agg": null,
"value": 3
},
{
"field": "safety_qty",
"agg": null,
"value": 2
}
],
"summary": "세 개들이라 창고 36개로 만들 수 있는 세트는 10개인데 40세트를 걸어 두었습니다.",
"hedge": "같은 낱개를 쓰는 다른 줄과 재고를 나눠 쓰므로 함께 봐야 합니다."
},
{
"verdict_no": "GV-003",
"listing_no": "CL-003",
"channel_raw": "Smart Store",
"channel_sku_raw": "BRD-CUBE-L",
"stock_no": "WS-03",
"gap_code": "in_sync",
"sellable_qty": 410,
"exposed_ad_krw": null,
"base_sku": "BRD-CUBE-L",
"owner_name": "SCM",
"evidence": [
{
"field": "channel_raw",
"agg": null,
"value": "Smart Store"
},
{
"field": "listed_qty",
"agg": null,
"value": 85
}
],
"summary": "채널 표기가 달리 왔으나 접으면 같은 곳이고 걸어 둔 수량이 팔 수 있는 양 안입니다.",
"hedge": null
},
{
"verdict_no": "GV-004",
"listing_no": "CL-007",
"channel_raw": "coupang",
"channel_sku_raw": "EQ-CREAM-50",
"stock_no": "WS-07",
"gap_code": "dead_stock",
"sellable_qty": 590,
"exposed_ad_krw": null,
"base_sku": "EQ-CREAM-50",
"owner_name": "SCM",
"evidence": [
{
"field": "sold_7d",
"agg": null,
"value": 3
},
{
"field": "warehouse_qty",
"agg": null,
"value": 600
}
],
"summary": "일주일에 세 개 팔리는데 창고에 600개가 있어 이 속도로는 넉 달치가 남습니다.",
"hedge": null
},
{
"verdict_no": "GV-005",
"listing_no": "CL-011",
"channel_raw": "amazon",
"channel_sku_raw": "EQ-CREAM-50",
"stock_no": "WS-09",
"gap_code": "qty_missing",
"sellable_qty": null,
"exposed_ad_krw": null,
"base_sku": "EQ-CREAM-50",
"owner_name": "SCM",
"evidence": [
{
"field": "warehouse_qty",
"agg": null,
"value": null
},
{
"field": "listed_qty",
"agg": null,
"value": 70
}
],
"summary": "창고 재고가 안 잡혀 걸어 둔 수량이 맞는지 견줄 수 없습니다.",
"hedge": null
},
{
"verdict_no": "GV-006",
"listing_no": "CL-008",
"channel_raw": "shopify",
"channel_sku_raw": "BRD-POUCH-1",
"stock_no": "WS-08",
"gap_code": "qty_missing",
"sellable_qty": 31,
"exposed_ad_krw": null,
"base_sku": "BRD-POUCH",
"owner_name": "SCM",
"evidence": [
{
"field": "listed_qty",
"agg": null,
"value": null
},
{
"field": "warehouse_qty",
"agg": null,
"value": 36
}
],
"summary": "채널에 걸어 둔 수량을 못 읽어 어긋났는지 판단하지 못합니다.",
"hedge": null
},
{
"verdict_no": "GV-007",
"listing_no": "CL-010",
"channel_raw": "smartstore",
"channel_sku_raw": "BRD-TRAVEL-KIT",
"stock_no": null,
"gap_code": "sku_unmapped",
"sellable_qty": null,
"exposed_ad_krw": null,
"base_sku": null,
"owner_name": null,
"evidence": [
{
"field": "channel_sku_raw",
"agg": null,
"value": "BRD-TRAVEL-KIT"
},
{
"field": "stock_no",
"agg": "count",
"value": 12
}
],
"summary": "새 기획 세트라 창고 품번이 안 붙어 재고를 견줄 상대가 없습니다.",
"hedge": null
},
{
"verdict_no": "GV-008",
"listing_no": "CL-009",
"channel_raw": "qoo10",
"channel_sku_raw": "EQ-SERUM-30",
"stock_no": null,
"gap_code": "channel_unregistered",
"sellable_qty": null,
"exposed_ad_krw": null,
"base_sku": null,
"owner_name": null,
"evidence": [
{
"field": "channel_raw",
"agg": null,
"value": "qoo10"
},
{
"field": "stock_no",
"agg": "count",
"value": 12
}
],
"summary": "재고 배분표 열두 행에 없는 채널이라 이 채널 몫을 정해 두지 않았습니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. gap_code 가 enum 여섯 값 중 하나인지 확인합니다. 2. verdict_no·listing_no·channel_raw·channel_sku_raw·gap_code·evidence·summary 가 모두 채워졌는지 확인합니다. 3. listing_no 가 채널 노출 목록에 실재하는 번호인지 대조합니다. 4. stock_no 가 있으면 배분표에 실재하는 행 번호인지 대조합니다. 5. stock_no 가 있으면 base_sku·owner_name 이 그 배분 행의 값과 같은지 대조합니다. 6. channel_raw 를 띄어쓰기와 대소문자를 접어 배분표와 맞추고, 그 채널이 배분표에 한 행도 없으면 gap_code 가 channel_unregistered 인지 확인합니다. 7. 채널은 있는데 그 품번이 없으면 gap_code 가 sku_unmapped 인지 확인합니다. 8. sellable_qty 를 warehouse_qty 를 unit_per_set 으로 나눈 몫에서 safety_qty 를 빼 다시 계산합니다. 9. listed_qty 나 warehouse_qty 가 비어 있으면 gap_code 가 qty_missing 인지 확인합니다. 10. listed_qty 가 sellable_qty 를 넘으면 gap_code 가 oversell_risk 인지 다시 계산하고, exposed_ad_krw 가 daily_ad_krw 와 같은지 대조합니다. 11. sold_7d 가 0 보다 크고 sellable_qty 를 sold_7d 로 나눈 값이 12 주를 넘으면 gap_code 가 dead_stock 인지 다시 계산합니다. 12. gap_code 가 in_sync 이면 listed_qty 가 sellable_qty 이하이고 과잉 조건에도 안 걸리는지 다시 확인합니다. 13. evidence 의 field 이름이 채널 목록과 배분표에 실재하는 컬럼인지 대조합니다. 14. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 같은지 확인합니다. 15. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 공개 양식으로 만든 상품 1500건에 손으로 계산한 정답 판정과 대조해 일치율과 사유 코드별 분포를 내고, 품절 위험으로 걸린 상품의 하루 광고비 합계를 함께 냅니다. [수동 검증 · 사람이 봅니다] 1. 새 기획 세트의 창고 품번과 낱개 구성을 공급망 담당이 등록합니다. 2. 품절 위험 상품의 광고를 오늘 멈출지 마케팅 담당이 정합니다.