팀리부뜨
경력근거: 인증·권한, 구독·크레딧, 결제 등 제품 운영에 필요한 핵심 도메인을 개발하고 개선합니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
팀리부뜨는 askyour.work 와 askyour.ai 를 직접 운영하면서 이 자리의 주요 업무로 '인증·권한, 구독·크레딧, 결제 등 제품 운영에 필요한 핵심 도메인을 개발하고 개선합니다'를 적었습니다. 크레딧을 사서 AI 작업을 돌리는 과금 방식이라 결제 한 건이 곧 지급 한 건이고, 지급이 어긋나면 문의로 돌아오거나 아무도 모르는 손해로 남습니다. 같은 목록에 '서비스 데이터의 일관성과 추적 가능성을 관리합니다'가 함께 있는 것도 결제와 원장을 맞추는 일이 이 자리의 몫이라는 뜻입니다. 다만 이 포트폴리오는 두 기록을 맞추는 규칙 설계와 판정 화면을 보여줍니다. askyour.work 의 실제 크레딧 정책과 결제사 연동 깊이는 주장하지 않습니다.
주요 업무에 '인증·권한, 구독·크레딧, 결제 등 제품 운영에 필요한 핵심 도메인을 개발하고 개선합니다'가 있습니다. 결제 한 건과 크레딧 지급 한 건을 맞추는 규칙이 그 도메인에서 가장 먼저 서야 하는 자리입니다.
'MongoDB, Elasticsearch 등 데이터 저장·검색 구조를 설계하고 서비스 데이터의 일관성과 추적 가능성을 관리합니다'가 같은 목록에 있습니다. 어느 주문이 왜 어긋났는지 근거 수치까지 남기는 판정 기록이 그 추적 가능성의 실물입니다.
자격요건이 'Python과 FastAPI 또는 유사한 웹 프레임워크를 활용한 서비스 개발'과 'REST API 설계, 데이터 모델링, 비동기 처리'를 요구합니다. 판정 API 는 FastAPI 로 만들고 원장 적재는 Kafka 로 비동기로 돌립니다.
결제사에서 내려받은 주문 내역과 크레딧 원장을 주문 번호로 맞추고, 결제 금액을 상품 단가로 나눈 묶음 수량에 상품 크레딧을 곱해 기대 지급량을 냅니다. 원장 기록은 사유와 부호로 나눠 구매 지급 합계와 환불 회수 합계를 따로 내고, 두 값을 견줘 주문마다 일곱 사유 중 하나를 정합니다. 여기까지가 상품 기준표와 원장 사유 구분이 하는 일이고, 모델은 사유를 바꾸지 못합니다. 모델이 받는 것은 이미 정해진 사유와 근거 수치이며, 그것을 운영 담당자가 문의 답변에 그대로 옮길 설명 문장으로 바꿉니다.
askyour.work(AU.W) 의 결제·크레딧 문의를 받는 팀리부뜨 운영 담당자입니다. '결제했는데 크레딧이 안 들어왔다'는 문의가 오면 결제사 주문 내역과 지급 기록을 주문 번호로 하나씩 맞춰 봐야 하고, 반대로 결제 없이 나간 지급은 문의 자체가 없어 눈치챌 방법이 없는 자리에 있습니다.
주문 내역과 원장이 서로 다른 시각에 각각 쌓여도, 상품 기준표로 기대 지급량을 계산하고 원장을 사유와 부호로 나눠 합계를 내면 주문마다 어긋난 자리와 사유가 규칙으로 정해집니다.
결제 내역과 원장을 CSV 로 받아 주문마다 기대 지급량과 구매 지급 합계를 내고, 일곱 사유로 나눈 판정 목록과 근거 수치를 보여주는 화면입니다. 팀리부뜨 내부 연동 없이 두 파일을 결제사 내려받기 형식으로 받습니다.
운영 담당자 2명이 결제 내역과 원장을 손으로 맞춰 매긴 사유를 도구 사유와 견줘 일치도를 내고, 화면에서 고친 상품 기준표로 같은 파일을 다시 돌립니다. 이 주차부터 화면과 판정 API 는 AWS EKS 에서 돕니다.
어긋난 주문마다 회수나 재지급 요청서를 뽑고, 운영 담당자가 고른 건만 내보낸 뒤 다음 파일에서 그 주문이 어떻게 판정되는지 한 바퀴 돌립니다.
운영 담당자가 결제 내역과 원장 파일을 올리면 주문마다 판정 사유와 근거 수치가 몇 초 안에 나옵니다.
주문 번호가 비어 있는 원장 기록과 사유가 섞인 주문 30종을 밀어 넣어도 목록이 끝까지 그려지고 판정 대상마다 사유가 하나씩 정해집니다.
화면에 실은 근거 값이 주문 행·상품 기준 행·원장 행의 값과 어긋나지 않습니다 100%.
측정: 근거 대조 규칙, 표본: 인라인 데이터와 증강분을 합친 판정 항목 30건
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 작성자가 만든 더미데이터 자가채점이며, 실제 지급 대조 정확도는 팀리부뜨의 결제·원장 운영 데이터와 운영 담당자 확인이 있어야 잽니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태까지 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실측 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 주문 번호가 비어 있는 지급 기록, 프로모션 지급과 만료 차감이 섞인 주문, 결제 금액이 상품 단가의 두 묶음인 주문 같은 악성·경계 케이스를 더미에 일부러 섞어 쉬운 문제만 푸는 게 아님을 보입니다.
이 포트폴리오는 결제 내역과 원장을 맞추는 규칙 설계와 판정 화면을 보여줍니다. askyour.work 의 실제 크레딧 정책과 결제사 연동 깊이는 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
지급이 결제대로라는 판정이 사용자가 크레딧을 제대로 썼다는 뜻은 아닙니다. 이 화면은 지급량과 회수 짝이 맞는지까지만 말하고 사용 내역은 다루지 않습니다.
실제 도입에서는 상품 구성과 원장 사유 값이 요금제 개편마다 달라집니다. 상품 기준표와 사유 구분을 운영 데이터로 다시 채우는 것이 도입 단계 과제입니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
결제사 관리자 화면에서 내려받은 크레딧 상품 주문 내역입니다. 승인이 끝난 주문만 들어 있어 상태는 결제완료와 환불 두 값이고, 같은 상품을 여러 묶음 산 주문은 결제 금액이 단가의 배수로 찍힙니다
| order_id | user_id | product_id | amount_paid | status | paid_at |
|---|---|---|---|---|---|
| ORD-3190 | U-2041 | CR-0500 | 5,500 | refunded | 2026-08-12T14:20 |
| ORD-3201 | U-2058 | CR-0500 | 5,500 | paid | 2026-08-14T09:05 |
| ORD-3205 | U-2063 | CR-1000 | 9,900 | refunded | 2026-08-15T11:40 |
| ORD-3211 | U-2072 | CR-1000 | 9,900 | paid | 2026-08-17T16:12 |
| ORD-3218 | U-2080 | CR-1000 | 9,900 | paid | 2026-08-18T10:31 |
{"type":"object","properties":{"order_id":{"type":"string"},"user_id":{"type":"string"},"product_id":{"type":"string"},"amount_paid":{"type":"number"},"status":{"type":"string","enum":["paid","refunded"]},"paid_at":{"type":"string"}},"required":["order_id","user_id","product_id","amount_paid","status","paid_at"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: amount_paid 가 credit_products.price 의 2배·3배인 여러 묶음 주문, 같은 user_id 가 며칠 사이에 여러 번 산 주문, status 가 refunded 인 주문(원장 쪽에 회수 기록이 있는 것과 없는 것을 모두 만든다), paid_at 이 원장 entered_at 보다 뒤인 주문. product_id 는 credit_products 에 실재하는 값만 쓰고, order_id 는 ORD- 로 시작하는 네 자리 일련번호로 둔다. 형식 깨뜨리기 시도 3건(금액에 콤마 표기, 상태 값 대소문자 흔들기, 날짜 형식 흔들기)은 별도 파일로 만들어라.{"type":"object","properties":{"order_id":{"type":"string"},"user_id":{"type":"string"},"product_id":{"type":"string"},"amount_paid":{"type":"number"},"status":{"type":"string","enum":["paid","refunded"]},"paid_at":{"type":"string"}},"required":["order_id","user_id","product_id","amount_paid","status","paid_at"]}크레딧 상품 한 묶음이 몇 크레딧이고 얼마인지 적어 둔 기준표입니다. 주문 행에는 이 값이 들어가지 않고 기대 지급량을 계산할 때 읽어 씁니다
| product_id | credits | price |
|---|---|---|
| CR-0100 | 100 | 1,200 |
| CR-0300 | 300 | 3,300 |
| CR-0500 | 500 | 5,500 |
| CR-0700 | 700 | 7,500 |
| CR-1000 | 1,000 | 9,900 |
{"type":"object","properties":{"product_id":{"type":"string"},"credits":{"type":"number"},"price":{"type":"number"}},"required":["product_id","credits","price"]} 쉬운 데이터만 만들지 말 것. credits 대비 price 가 일정하지 않게 만들어 큰 묶음일수록 크레딧당 단가가 싸지게 하고, credits 가 100 단위인 작은 상품과 10000 단위인 큰 상품을 함께 넣어라. price 는 100원 단위로 떨어지게 두되 credits 로 나누어떨어지지 않는 조합도 섞어, 크레딧당 단가로 되돌리는 계산을 못 쓰게 만들어라. product_id 는 CR- 다음에 크레딧 양을 네 자리로 적는다.{"type":"object","properties":{"product_id":{"type":"string"},"credits":{"type":"number"},"price":{"type":"number"}},"required":["product_id","credits","price"]}크레딧이 들어오고 나간 원장입니다. 사유는 구매 지급·프로모션 지급·환불 회수·만료 차감·관리자 조정 다섯 값이고, 프로모션 지급처럼 주문에서 나오지 않은 기록은 order_id 가 비어 있습니다. 회수와 차감은 delta_credits 가 마이너스로 적힙니다
| entry_id | order_id | reason | delta_credits | entered_at |
|---|---|---|---|---|
| LED-8801 | ORD-3190 | purchase_grant | 500 | 2026-08-12T14:21 |
| LED-8809 | ORD-3201 | purchase_grant | 500 | 2026-08-14T09:06 |
| LED-8814 | ORD-3205 | purchase_grant | 1,000 | 2026-08-15T11:41 |
| LED-8820 | ORD-3205 | refund_revoke | -1,000 | 2026-08-16T10:02 |
| LED-8827 | ORD-3211 | purchase_grant | 1,000 | 2026-08-17T16:13 |
{"type":"object","properties":{"entry_id":{"type":"string"},"order_id":{"type":["string","null"]},"reason":{"type":"string","enum":["purchase_grant","promo_grant","refund_revoke","expire_revoke","admin_adjust"]},"delta_credits":{"type":"number"},"entered_at":{"type":"string"}},"required":["entry_id","order_id","reason","delta_credits","entered_at"]} 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: order_id 가 null 인 promo_grant 행과 order_id 가 null 인 purchase_grant 행(앞은 판정 대상이 아니고 뒤는 주문 없는 지급이다), credit_orders 에 없는 order_id 를 단 purchase_grant 행, 한 주문에 purchase_grant 가 두 줄 들어와 합계가 상품 기준을 넘는 행 묶음, 한 주문에 purchase_grant 와 promo_grant 가 같은 시각에 들어온 행 묶음, refund_revoke 없이 expire_revoke 만 달린 환불 주문. delta_credits 는 지급이면 양수 회수·차감이면 마이너스로 적고, entered_at 은 그 주문의 paid_at 이후로 둔다. 형식 깨뜨리기 시도 3건(사유 값 오타, delta_credits 문자열 표기, entered_at 결측)은 별도 파일로 만들어라.{"type":"object","properties":{"entry_id":{"type":"string"},"order_id":{"type":["string","null"]},"reason":{"type":"string","enum":["purchase_grant","promo_grant","refund_revoke","expire_revoke","admin_adjust"]},"delta_credits":{"type":"number"},"entered_at":{"type":"string"}},"required":["entry_id","order_id","reason","delta_credits","entered_at"]}| Python | 결제 내역과 원장을 읽어 기대 지급량과 사유별 합계를 내는 판정 엔진 본체입니다 |
| FastAPI | 파일 두 장을 받아 판정 결과를 돌려주는 `POST /grant-verdicts` 와 상품 기준표 수정 API 를 맡습니다 |
| MongoDB | 주문·상품 기준표·원장과 판정 결과를 컬렉션으로 두고, 같은 파일을 다시 올리면 판정 판을 나눠 쌓습니다 |
| Redis | 판정 한 번에 되풀이해서 읽는 상품 기준표를 캐시에 두고, 재판정 요청이 몰릴 때 잠금으로 한 번만 돌게 합니다 |
| Kafka | 원장 적재를 큐로 받아 판정 작업을 비동기로 돌리고, 파일이 큰 날에도 API 응답이 밀리지 않게 합니다 |
| Elasticsearch | 판정 결과를 주문 번호·사용자·사유로 찾는 색인을 맡아 문의 한 건의 근거를 바로 꺼냅니다 |
| Docker | 판정 엔진과 화면을 각각 이미지로 만들어 같은 구성으로 어디서나 돌립니다 |
| Kubernetes | 2주차부터 판정 워커 수를 늘려 큰 파일을 나눠 처리하고, 죽은 워커를 다시 띄웁니다 |
| AWS | 2주차부터 EKS 와 S3 로 배포해 올린 파일과 판정 결과를 운영합니다 |
| LLM | 규칙이 정한 사유와 근거 수치를 받아 문의 답변에 쓸 설명 문장을 만듭니다 |
| OpenAI | GPT-4o-mini 로 3주차 회수·재지급 요청서 문안을 뽑습니다 |
| Claude | Claude Haiku 로 판정 설명 문장을 씁니다 |
| Gemini | 같은 프롬프트를 Gemini Flash 로도 돌려 모델을 바꿔도 같은 사유를 말하는지 살핍니다 |
✓ pass → 여기까지가 그 자체로 제출 가능한 완성품입니다. 더 키우려면 2주차에서 운영 담당자 판단과 견줍니다.
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 3주차 회수·재지급 요청서 사이클로 갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · case_key + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
granted_as_paid결제대로 지급granted_less_than_paid지급 모자람granted_more_than_paid지급 넘침paid_but_not_granted지급 없음granted_without_order주문 없는 지급refunded_but_not_revoked회수 없음·모자람revoked_without_refund환불 없는 회수판정 대상 하나의 카드입니다. 판정 사유와 기대 지급량·구매 지급 합계·환불 회수 합계, 그 계산에 쓰인 근거 값을 담습니다
{"type":"object","properties":{"case_key":{"type":"string"},"order_id":{"type":["string","null"]},"entry_id":{"type":["string","null"]},"verdict":{"type":"string","enum":["granted_as_paid","granted_less_than_paid","granted_more_than_paid","paid_but_not_granted","granted_without_order","refunded_but_not_revoked","revoked_without_refund"]},"expected_credits":{"type":["number","null"],"description":"credit_orders.amount_paid 를 credit_products.price 로 나눈 묶음 수량에 credit_products.credits 를 곱한 값입니다. 주문에 이어지지 않는 원장 기록은 null 입니다."},"granted_credits":{"type":["number","null"],"description":"같은 order_id 의 credit_ledger 기록 중 reason 이 purchase_grant 인 행의 delta_credits 합계입니다. 그런 기록이 없으면 null 입니다."},"revoked_credits":{"type":["number","null"],"description":"같은 order_id 의 credit_ledger 기록 중 reason 이 refund_revoke 인 행의 delta_credits 합계입니다. 그런 기록이 없으면 null 입니다. 환불 주문에서는 이 값의 절댓값을 granted_credits 와 견주어 회수가 모자란지 정합니다."},"credit_gap":{"type":["number","null"],"description":"granted_credits 에서 expected_credits 를 뺀 값입니다. 두 값 중 하나가 null 이면 null 입니다."},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"value":{"type":["string","number","null"]},"agg":{"type":"string","enum":["count","sum","avg","min","max"]}},"required":["field","value"]}},"summary":{"type":"string"}},"required":["case_key","order_id","entry_id","verdict","expected_credits","granted_credits","revoked_credits","credit_gap","evidence","summary"]}[
{
"case_key": "ORD-3201",
"order_id": "ORD-3201",
"entry_id": null,
"verdict": "granted_as_paid",
"expected_credits": 500,
"granted_credits": 500,
"revoked_credits": null,
"credit_gap": 0,
"evidence": [
{
"field": "order_id",
"value": "ORD-3201"
},
{
"field": "amount_paid",
"value": 5500
},
{
"field": "price",
"value": 5500
},
{
"field": "credits",
"value": 500
},
{
"field": "delta_credits",
"agg": "sum",
"value": 500
}
],
"summary": "5,500원 결제가 5,500원짜리 500크레딧 상품 한 묶음이라 기대 지급이 500크레딧이고, 구매 지급 합계도 500크레딧이라 결제대로 나갔습니다."
},
{
"case_key": "ORD-3211",
"order_id": "ORD-3211",
"entry_id": null,
"verdict": "granted_as_paid",
"expected_credits": 1000,
"granted_credits": 1000,
"revoked_credits": null,
"credit_gap": 0,
"evidence": [
{
"field": "order_id",
"value": "ORD-3211"
},
{
"field": "credits",
"value": 1000
},
{
"field": "delta_credits",
"agg": "sum",
"value": 1000
},
{
"field": "reason",
"value": "promo_grant"
},
{
"field": "delta_credits",
"value": 200
}
],
"summary": "첫 구매 보너스 200크레딧이 따로 들어와 있어 보너스를 뺀 구매 지급 합계 1,000크레딧을 상품 기준 1,000크레딧과 견줬고, 두 값이 같습니다."
},
{
"case_key": "ORD-3218",
"order_id": "ORD-3218",
"entry_id": null,
"verdict": "granted_less_than_paid",
"expected_credits": 1000,
"granted_credits": 500,
"revoked_credits": null,
"credit_gap": -500,
"evidence": [
{
"field": "order_id",
"value": "ORD-3218"
},
{
"field": "amount_paid",
"value": 9900
},
{
"field": "credits",
"value": 1000
},
{
"field": "entry_id",
"value": "LED-8833"
},
{
"field": "delta_credits",
"agg": "sum",
"value": 500
}
],
"summary": "9,900원 결제로 1,000크레딧 상품 한 묶음을 샀는데 구매 지급 합계가 500크레딧이라 500크레딧이 모자랍니다."
},
{
"case_key": "ORD-3236",
"order_id": "ORD-3236",
"entry_id": null,
"verdict": "granted_more_than_paid",
"expected_credits": 300,
"granted_credits": 600,
"revoked_credits": null,
"credit_gap": 300,
"evidence": [
{
"field": "order_id",
"value": "ORD-3236"
},
{
"field": "credits",
"value": 300
},
{
"field": "entry_id",
"value": "LED-8856"
},
{
"field": "entry_id",
"agg": "count",
"value": 2
},
{
"field": "delta_credits",
"agg": "sum",
"value": 600
}
],
"summary": "3,300원 결제로 300크레딧 상품 한 묶음을 샀는데 구매 지급 기록이 2건 들어와 합계가 600크레딧이라 300크레딧이 더 나갔습니다."
},
{
"case_key": "ORD-3224",
"order_id": "ORD-3224",
"entry_id": null,
"verdict": "paid_but_not_granted",
"expected_credits": 600,
"granted_credits": null,
"revoked_credits": null,
"credit_gap": null,
"evidence": [
{
"field": "order_id",
"value": "ORD-3224"
},
{
"field": "status",
"value": "paid"
},
{
"field": "amount_paid",
"value": 6600
},
{
"field": "price",
"value": 3300
},
{
"field": "credits",
"value": 300
},
{
"field": "entry_id",
"agg": "count",
"value": 0
}
],
"summary": "6,600원 결제가 3,300원짜리 300크레딧 상품 두 묶음이라 기대 지급이 600크레딧인데, 이 주문 번호로 들어온 구매 지급 기록이 하나도 없습니다."
},
{
"case_key": "LED-8845",
"order_id": null,
"entry_id": "LED-8845",
"verdict": "granted_without_order",
"expected_credits": null,
"granted_credits": 1000,
"revoked_credits": null,
"credit_gap": null,
"evidence": [
{
"field": "entry_id",
"value": "LED-8845"
},
{
"field": "order_id",
"value": null
},
{
"field": "reason",
"value": "purchase_grant"
},
{
"field": "delta_credits",
"value": 1000
},
{
"field": "entered_at",
"value": "2026-08-20T15:44"
}
],
"summary": "구매 지급 1,000크레딧이 들어왔는데 원장의 주문 번호 칸이 비어 있어 결제 내역에 짝이 되는 주문이 없습니다."
},
{
"case_key": "ORD-3247",
"order_id": "ORD-3247",
"entry_id": "LED-8879",
"verdict": "refunded_but_not_revoked",
"expected_credits": 1000,
"granted_credits": 1000,
"revoked_credits": -400,
"credit_gap": 0,
"evidence": [
{
"field": "order_id",
"value": "ORD-3247"
},
{
"field": "status",
"value": "refunded"
},
{
"field": "credits",
"value": 1000
},
{
"field": "delta_credits",
"agg": "sum",
"value": 1000
},
{
"field": "entry_id",
"value": "LED-8879"
},
{
"field": "reason",
"value": "refund_revoke"
},
{
"field": "delta_credits",
"agg": "sum",
"value": -400
}
],
"summary": "환불로 끝난 주문인데 지급한 1,000크레딧 가운데 회수는 400크레딧뿐이라 600크레딧이 사용자에게 그대로 남아 있습니다."
},
{
"case_key": "ORD-3241",
"order_id": "ORD-3241",
"entry_id": "LED-8867",
"verdict": "revoked_without_refund",
"expected_credits": 300,
"granted_credits": 300,
"revoked_credits": -300,
"credit_gap": 0,
"evidence": [
{
"field": "order_id",
"value": "ORD-3241"
},
{
"field": "status",
"value": "paid"
},
{
"field": "credits",
"value": 300
},
{
"field": "delta_credits",
"agg": "sum",
"value": 300
},
{
"field": "entry_id",
"value": "LED-8867"
},
{
"field": "reason",
"value": "refund_revoke"
},
{
"field": "delta_credits",
"value": -300
}
],
"summary": "결제 상태가 결제완료인데 원장에는 환불 회수 300크레딧이 들어와 있어, 사용자는 환불받지 못한 채 크레딧만 빠져나갔습니다."
}
][자동 검증 · 매 실행] 1. verdict 가 granted_as_paid·granted_less_than_paid·granted_more_than_paid·paid_but_not_granted·granted_without_order·refunded_but_not_revoked·revoked_without_refund 일곱 값 안에 있는지 봅니다. 2. case_key·order_id·entry_id·verdict·expected_credits·granted_credits·revoked_credits·credit_gap·evidence·summary 필드가 모든 판정 카드에 있는지 봅니다. 3. expected_credits 를 credit_orders.amount_paid ÷ credit_products.price 로 낸 묶음 수량에 credit_products.credits 를 곱해 다시 계산하고, granted_credits 는 같은 order_id 의 credit_ledger 기록 중 reason 이 purchase_grant 인 행의 delta_credits 합, revoked_credits 는 reason 이 refund_revoke 인 행의 delta_credits 합으로 다시 계산해 값이 같은지 봅니다. revoked_credits 는 카드에 실어 두기만 하는 값이 아니라 9번 판정 순서에서 구매 지급 합계와 견주는 값이라, 이 재계산이 틀리면 환불 주문의 사유가 바뀝니다. 4. credit_gap 이 granted_credits 에서 expected_credits 를 뺀 값과 같은지 봅니다. 두 값 중 하나가 null 이면 credit_gap 도 null 입니다. 5. 판정 카드가 선언한 JSON Schema 형식을 지키는지 봅니다. 6. agg 가 없는 evidence 의 value 가 주문 행·상품 기준 행·원장 행에 수록된 값 그대로인지 봅니다. 여러 행에서 낸 수치는 agg 에 sum·count 를 적습니다. 7. 판정 카드마다 evidence 항목이 3개 이상이고, evidence 의 field 가 credit_orders·credit_products·credit_ledger 의 컬럼에 있는지 봅니다. 8. summary 가 존댓말 한 문장으로 끝나는지 봅니다. 9. 판정 순서를 규칙 그대로 다시 밟습니다. 앞 단계에서 걸린 주문은 뒤 비교를 건너뛰므로 한 주문에는 사유가 하나만 붙습니다. 첫째, 같은 order_id 에 reason 이 refund_revoke 인 기록이 있고 credit_orders.status 가 refunded 가 아니면 verdict 는 revoked_without_refund 입니다. 둘째, status 가 refunded 인 주문은 refund_revoke 합계의 절댓값을 purchase_grant 합계와 견줍니다. 회수 기록이 아예 없어 절댓값이 0 이거나, 기록이 있어도 절댓값이 지급 합계보다 작으면 아직 돌려받지 못한 크레딧이 남아 있으므로 refunded_but_not_revoked 입니다. 지급 1,000 에 회수 -400 이면 600 이 남아 이 사유로 갑니다. 만료 차감(expire_revoke)은 환불 회수가 아니라 이 합계에서 뺍니다. 셋째, status 가 paid 인데 purchase_grant 기록이 없으면 paid_but_not_granted 입니다. 넷째, 앞의 셋에 걸리지 않은 주문만 granted_credits 를 expected_credits 와 견주어 적으면 granted_less_than_paid, 많으면 granted_more_than_paid, 같으면 granted_as_paid 입니다. 둘째에서 걸린 환불 주문은 이 단계에서 걸리면 뒤 비교를 건너뛴다는 규칙에 따라 넷째의 지급 금액 비교를 하지 않으므로, 회수가 모자란 환불 주문에서 지급까지 어긋났으면 그 사실은 근거 수치로만 남고 사유로는 나오지 않습니다. 10. 반대 방향도 밟습니다. credit_ledger 의 reason 이 purchase_grant 인데 order_id 가 비어 있거나 credit_orders.order_id 에 없으면 그 기록 하나가 granted_without_order 이고, reason 이 promo_grant·admin_adjust 인 기록은 주문 번호가 비어 있어도 판정 대상이 아닙니다. 주문 번호가 비어 있는 지급 기록과 프로모션 지급이 섞인 주문이 각각 samples 에 있습니다. [평가 러너 · 2주차] 운영 담당자 2명이 결제 내역과 원장을 직접 열어 매긴 결과와 도구 판정의 일치도(Cohen's κ: 우연히 맞은 몫을 뺀 일치 정도, 0~1)를 잽니다. [수동 검증 · 사람이 봅니다] 1. 관리자 조정으로 나간 크레딧을 구매 지급 합계에 셀지 뺄지는 팀리부뜨 운영 정책이 정합니다. 2. 쿠폰 할인이 들어간 주문처럼 결제 금액이 상품 단가로 나누어떨어지지 않는 건은 지금 규칙이 판정 대상에서 빼 두므로, 표본 20건을 눈으로 확인해 수량을 어떻게 셀지 정합니다. 3. 환불 회수가 지급 합계보다 많이 빠진 주문은 사용자가 곧바로 문의하는 방향이라 사유를 따로 두지 않았습니다. 회수 합계의 절댓값이 지급 합계보다 큰 건은 표본 20건을 눈으로 확인해 부분 환불 정책과 맞는지 정합니다.
근거: 인증·권한, 구독·크레딧, 결제 등 제품 운영에 필요한 핵심 도메인을 개발하고 개선합니다.