78일차 데이터 품질 검증 자동화: 실습 15개(Q) · 기준 둘 · 강의(개요 30장 · 단계 문서 · 4단계 정리 · 05 바이브) · 요청서와 VIBE-Q(tcheck), 정답은 수업 뒤

This commit is contained in:
mcchae committed 2026-11-02 20:42:54 +09:00
1 parent fbed1b22f6
commit 4ec7ae123b
76 files changed
+11631

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
{
"webhook": "my-faq-check",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"after": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "이미 있는 id (FAQ-003)",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"rows.10.errors": "exists_id"
}
}
},
{
"name": "이미 있는 제목 · 쓰인 키워드는 경고",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"rows.11.errors": "same_title",
"rows.11.warnings": "keyword_used:vpn"
}
}
},
{
"name": "배치 1 · 통과 3건",
"request": "requests/faq-batch-1.json",
"expect": {
"equals": {
"valid": 3,
"invalid": 10
}
}
},
{
"name": "막 더한 FAQ도 기존 데이터",
"before": [
{
"url": "http://localhost:8800/faq/add",
"body": {
"id": "FAQ-201",
"title": "회의실 예약",
"keywords": [
"회의실"
],
"answer": "사내 포털의 '회의실 예약' 메뉴에서 예약합니다."
}
}
],
"request": "requests/faq-batch-2.json",
"expect": {
"contains": {
"rows.0.errors": "exists_id"
},
"equals": {
"rows.1.ok": true
}
}
}
]
}
+68
View File
@@ -0,0 +1,68 @@
{
"webhook": "my-faq-check",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "B-1 · 고칠 줄 10개, 줄 순서대로",
"request": "requests/faq-batch-1.json",
"expect": {
"equals": {
"fix_list.0.row": 3,
"fix_list.0.id": "FAQ-103",
"fix_list.9.id": "FAQ-112",
"fix_list.10": null
}
}
},
{
"name": "고칠 것은 사람이 읽는 말로",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"fix_list.0.fix": "제목을 써 주세요",
"fix_list.6.fix": "개인 전화번호",
"fix_list.8.fix": "이미 있는 id"
}
}
},
{
"name": "한 줄 요약",
"request": "requests/faq-batch-1.json",
"expect": {
"equals": {
"message": "B-1: 13줄 중 3줄 통과, 10줄 고칠 것"
}
}
},
{
"name": "B-2 · 메일 주소 한 줄만",
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"fix_list.0.id": "FAQ-209",
"fix_list.0.codes.0": "pii",
"fix_list.1": null,
"message": "B-2: 10줄 중 9줄 통과, 1줄 고칠 것"
}
}
},
{
"name": "앞의 칸은 그대로",
"request": "requests/faq-batch-1.json",
"expect": {
"equals": {
"valid": 3,
"invalid": 10
},
"contains": {
"rows.10.errors": "exists_id"
}
}
}
]
}
+76
View File
@@ -0,0 +1,76 @@
{
"webhook": "my-faq-check",
"cases": [
{
"name": "제목 40자는 통과, 41자는 너무 김",
"request": "requests/faq-edges.json",
"expect": {
"equals": {
"rows.0.ok": true,
"rows.1.ok": false
},
"contains": {
"rows.1.errors": "title_too_long"
}
}
},
{
"name": "공백만 있는 제목은 빈 값",
"request": "requests/faq-edges.json",
"expect": {
"contains": {
"rows.2.errors": "title_missing"
}
}
},
{
"name": "답 10자는 통과, 9자는 너무 짧음",
"request": "requests/faq-edges.json",
"expect": {
"equals": {
"rows.3.ok": true
},
"contains": {
"rows.4.errors": "answer_too_short"
}
}
},
{
"name": "하이픈 없는 휴대전화도 개인정보, 사무실 번호는 통과",
"request": "requests/faq-edges.json",
"expect": {
"equals": {
"rows.6.ok": true
},
"contains": {
"rows.5.errors": "pii"
}
}
},
{
"name": "사내 주소는 통과, 사내 주소처럼 보이는 바깥 링크는 막음",
"request": "requests/faq-edges.json",
"expect": {
"equals": {
"rows.7.ok": true
},
"contains": {
"rows.8.errors": "external_link"
}
}
},
{
"name": "소문자 id · 합계 10줄 중 4줄 통과",
"request": "requests/faq-edges.json",
"expect": {
"equals": {
"total": 10,
"valid": 4
},
"contains": {
"rows.9.errors": "id_format"
}
}
}
]
}
+61
View File
@@ -0,0 +1,61 @@
{
"webhook": "my-faq-check",
"cases": [
{
"name": "빈 값 (제목 · 답 · 키워드)",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"rows.2.errors": "title_missing",
"rows.4.errors": "answer_missing",
"rows.7.errors": "keywords_missing"
}
}
},
{
"name": "형식 (id · 답 길이)",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"rows.3.errors": "id_format",
"rows.5.errors": "answer_too_long"
}
}
},
{
"name": "배치 안 중복 · 개인정보 · 바깥 링크",
"request": "requests/faq-batch-1.json",
"expect": {
"contains": {
"rows.6.errors": "duplicate_in_batch",
"rows.8.errors": "pii",
"rows.9.errors": "external_link"
},
"equals": {
"rows.0.ok": true,
"total": 13
}
}
},
{
"name": "배치 2 · 메일 주소만 걸림, 사내 링크는 통과",
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"total": 10,
"valid": 9,
"codes.pii": 1,
"rows.8.ok": false,
"rows.6.ok": true
}
}
},
{
"name": "목록이 아닌 본문 → 400",
"request": "requests/not-a-list.json",
"expect": {
"status": 400
}
}
]
}
+82
View File
@@ -0,0 +1,82 @@
{
"webhook": "my-faq-import",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
}
],
"after": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "거절도 품질 기록",
"request": "requests/faq-batch-1.json",
"expect": {
"status": 422
}
},
{
"name": "배치 2 가져오기",
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"score": 0.8
}
}
},
{
"name": "품질 기록 두 줄 (거절 · 0.8)",
"url": "http://localhost:8800/quality-log/list",
"body": {},
"expect": {
"equals": {
"count": 2,
"items.0.rejected": true,
"items.1.score": 0.8,
"items.1.valid": 8,
"items.1.quarantined": 2
}
}
},
{
"name": "알림 두 건 (거절 · 낮은 점수)",
"url": "http://localhost:8800/alerts/list",
"body": {},
"expect": {
"equals": {
"count": 2
},
"contains": {
"alerts": "batch_rejected"
}
}
},
{
"name": "알림에 낮은 점수",
"url": "http://localhost:8800/alerts/list",
"body": {},
"expect": {
"contains": {
"alerts": "low_quality"
}
}
}
]
}
+56
View File
@@ -0,0 +1,56 @@
{
"webhook": "my-faq-import",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
}
],
"cases": [
{
"name": "거절된 배치는 LLM을 부르지 않음",
"request": "requests/faq-batch-1.json",
"expect": {
"status": 422,
"equals": {
"llm_calls": 0
}
}
},
{
"name": "B-2 · 규칙을 통과한 9줄만 LLM에",
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"llm_calls": 9,
"imported": 8
},
"contains": {
"quarantined_ids": "FAQ-210"
}
}
},
{
"name": "B-3 · 5줄만 LLM에",
"request": "requests/faq-batch-3.json",
"expect": {
"equals": {
"llm_calls": 5,
"imported": 5
}
}
}
]
}
+87
View File
@@ -0,0 +1,87 @@
{
"webhook": "my-faq-import",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
}
],
"cases": [
{
"name": "B-1 · 기본 기준 0.3 → 거절",
"request": "requests/faq-batch-1.json",
"expect": {
"status": 422,
"equals": {
"max_invalid_ratio": 0.3,
"invalid_ratio": 0.7692
}
}
},
{
"name": "B-1 · 기준을 0.8로 풀면 → 받음, 넣는 것은 2줄",
"request": "requests/faq-batch-1-loose.json",
"expect": {
"equals": {
"rejected": false,
"max_invalid_ratio": 0.8,
"imported": 2,
"quarantined": 11
}
}
},
{
"name": "FAQ만 처음으로 (B-1이 넣은 2줄을 비움, 격리함은 그대로)",
"url": "http://localhost:8800/faq-reset",
"body": {},
"expect": {
"equals": {
"cleared": 2
}
}
},
{
"name": "B-2 · 기준 0.05 → 거절",
"request": "requests/faq-batch-2-strict.json",
"expect": {
"status": 422,
"equals": {
"invalid_ratio": 0.1,
"max_invalid_ratio": 0.05
}
}
},
{
"name": "B-2 · 기준 0.1 · 나쁜 줄이 딱 10% → 받음",
"request": "requests/faq-batch-2-limit-0.1.json",
"expect": {
"equals": {
"rejected": false,
"max_invalid_ratio": 0.1
}
}
},
{
"name": "격리함이 13줄 (B-1 11 + B-2 2)",
"url": "http://localhost:8800/quarantine/list",
"body": {},
"expect": {
"equals": {
"size": 13
}
}
}
]
}
+118
View File
@@ -0,0 +1,118 @@
{
"webhook": "my-faq-import",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
}
],
"after": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "나쁜 배치는 통째로 거절 (422)",
"request": "requests/faq-batch-1.json",
"expect": {
"status": 422,
"equals": {
"batch": "B-1"
},
"range": {
"invalid_ratio": [
0.3,
1
]
}
}
},
{
"name": "배치 2 · 통과한 줄만 넣고 나쁜 줄은 격리",
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"rejected": false,
"total": 10
},
"contains": {
"imported_ids": "FAQ-201",
"quarantined_ids": "FAQ-209"
}
}
},
{
"name": "FAQ에 들어갔고 메일 주소는 안 들어감",
"url": "http://localhost:8800/faq/list",
"body": {},
"expect": {
"contains": {
"faqs": "FAQ-208"
},
"not_contains": {
"faqs": "security.desk"
}
}
},
{
"name": "격리함에 이유와 함께",
"url": "http://localhost:8800/quarantine/list",
"body": {},
"expect": {
"contains": {
"items": "\"pii\""
}
}
},
{
"name": "LLM · 제목과 답이 다른 줄 (FAQ-210)",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
}
],
"request": "requests/faq-batch-2.json",
"expect": {
"equals": {
"imported": 8,
"quarantined": 2,
"codes.llm_mismatch": 1
},
"contains": {
"quarantined_ids": "FAQ-210"
}
}
},
{
"name": "LLM · 규칙에 걸린 줄은 그대로 (FAQ-305)",
"request": "requests/faq-batch-3.json",
"expect": {
"equals": {
"imported": 5
},
"contains": {
"quarantined_ids": "FAQ-305"
}
}
}
]
}
+92
View File
@@ -0,0 +1,92 @@
{
"webhook": "my-quality-report",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
},
{
"webhook": "my-faq-import",
"request": "requests/faq-batch-1.json",
"allow_fail": true
},
{
"webhook": "my-faq-import",
"request": "requests/faq-batch-2.json"
},
{
"webhook": "my-faq-import",
"request": "requests/faq-batch-3.json"
},
{
"webhook": "my-faq-import",
"request": "requests/faq-batch-2-fixed.json"
}
],
"after": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "배치 네 개 · 평균 · 가장 나쁜 배치",
"body": {},
"expect": {
"equals": {
"count": 4,
"avg_score": 0.6583,
"worst_batch": "B-1",
"rejected_batches": 1
}
}
},
{
"name": "가장 많은 이유 · 흐름",
"body": {},
"expect": {
"equals": {
"top_codes.0.code": "pii",
"top_codes.0.count": 2,
"trend": "좋아짐"
}
}
},
{
"name": "배치마다 점수 순서",
"body": {},
"expect": {
"equals": {
"batches.1.score": 0.8,
"batches.2.score": 0.8333,
"batches.3.batch": "B-2-fix"
}
}
},
{
"name": "다른 데이터셋으로 거르기",
"body": {
"dataset": "employee"
},
"expect": {
"equals": {
"count": 0,
"avg_score": 0
}
}
}
]
}
+84
View File
@@ -0,0 +1,84 @@
{
"webhook": "my-quality-report",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
},
{
"webhook": "my-check",
"body": "requests/employees-new.json"
},
{
"webhook": "my-check",
"body": "requests/tickets-new.json"
},
{
"webhook": "my-faq-import",
"body": "requests/faq-batch-3.json"
}
],
"cases": [
{
"name": "검사도 품질 기록 (직원 · 티켓 · FAQ)",
"url": "http://localhost:8800/quality-log/list",
"body": {},
"expect": {
"equals": {
"count": 3,
"items.0.dataset": "employee",
"items.0.score": 0.4444,
"items.1.dataset": "ticket",
"items.2.dataset": "faq"
}
}
},
{
"name": "데이터셋마다 나눠 보기",
"body": {},
"expect": {
"equals": {
"count": 3,
"by_dataset.employee.avg_score": 0.4444,
"by_dataset.ticket.avg_score": 0.2222,
"by_dataset.faq.avg_score": 0.8333
}
}
},
{
"name": "가장 나쁜 데이터셋 · 그 이유",
"body": {},
"expect": {
"equals": {
"worst_dataset": "ticket",
"by_dataset.employee.top_code": "bad_format:email"
}
}
},
{
"name": "한 데이터셋만 (앞의 칸은 그대로)",
"body": {
"dataset": "ticket"
},
"expect": {
"equals": {
"count": 1,
"worst_batch": "TK-1",
"trend": "그대로"
}
}
}
]
}
+78
View File
@@ -0,0 +1,78 @@
{
"webhook": "my-faq-import",
"before": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
},
{
"url": "http://localhost:8800/quarantine-clear",
"body": {}
},
{
"url": "http://localhost:8800/quality-log-clear",
"body": {}
},
{
"url": "http://localhost:8800/alerts-clear",
"body": {}
},
{
"webhook": "my-faq-import",
"request": "requests/faq-batch-2.json"
}
],
"after": [
{
"url": "http://localhost:8800/faq-reset",
"body": {}
}
],
"cases": [
{
"name": "고친 두 줄 다시 가져오기",
"request": "requests/faq-batch-2-fixed.json",
"expect": {
"equals": {
"imported": 2,
"quarantined": 0,
"score": 1
}
}
},
{
"name": "격리함이 비었나",
"url": "http://localhost:8800/quarantine/list",
"body": {},
"expect": {
"equals": {
"size": 0
}
}
},
{
"name": "FAQ에 FAQ-209 · FAQ-210",
"url": "http://localhost:8800/faq/list",
"body": {},
"expect": {
"contains": {
"faqs": "FAQ-210"
},
"equals": {
"count": 16
}
}
},
{
"name": "품질 기록 · 점수 1",
"url": "http://localhost:8800/quality-log/list",
"body": {},
"expect": {
"equals": {
"count": 2,
"items.1.score": 1
}
}
}
]
}
+63
View File
@@ -0,0 +1,63 @@
{
"webhook": "my-check",
"cases": [
{
"name": "스키마 목록에 ticket이 있다",
"url": "http://localhost:8800/schemas/list",
"body": {},
"expect": {
"contains": {
"schemas": "ticket"
}
}
},
{
"name": "새 스키마 ticket · 워크플로우는 그대로 · 9줄 중 2줄 통과",
"request": "requests/tickets-new.json",
"expect": {
"equals": {
"schema": "ticket",
"total": 9,
"valid": 2,
"rows.0.ok": true
}
}
},
{
"name": "형식 · 목록 · 날짜 · 길이",
"request": "requests/tickets-new.json",
"expect": {
"contains": {
"rows.1.errors": "bad_format:ticket_id",
"rows.2.errors": "not_allowed:priority",
"rows.3.errors": "bad_format:opened",
"rows.4.errors": "too_short:title"
}
}
},
{
"name": "선택 칸 형식 · 중복 · 빈 칸, 선택 칸이 없어도 통과",
"request": "requests/tickets-new.json",
"expect": {
"equals": {
"rows.8.ok": true
},
"contains": {
"rows.5.errors": "bad_format:contact",
"rows.6.errors": "duplicate_in_batch",
"rows.7.errors": "missing:requester"
}
}
},
{
"name": "직원 CSV는 그대로 9줄 중 4줄",
"request": "requests/employees-new.json",
"expect": {
"equals": {
"schema": "employee",
"valid": 4
}
}
}
]
}
+108
View File
@@ -0,0 +1,108 @@
{
"webhook": "my-check",
"cases": [
{
"name": "직원 CSV · 9줄 중 4줄 통과",
"request": "requests/employees-new.json",
"expect": {
"equals": {
"schema": "employee",
"total": 9,
"valid": 4,
"rows.6.ok": true
}
}
},
{
"name": "직원 CSV · 줄마다 이유",
"request": "requests/employees-new.json",
"expect": {
"contains": {
"rows.2.errors": "bad_format:email",
"rows.3.errors": "not_allowed:dept",
"rows.4.errors": "bad_format:joined",
"rows.5.errors": "missing:name",
"rows.7.errors": "duplicate_in_batch"
}
}
},
{
"name": "같은 워크플로우로 FAQ 스키마",
"body": {
"schema": "faq",
"batch": "B-3",
"items": [
{
"id": "FAQ-301",
"title": "택배 수령",
"keywords": [
"택배"
],
"answer": "회사로 온 택배는 1층 안내 데스크에서 사원증을 보여 주고 받습니다."
},
{
"id": "FAQ-302",
"title": "주차 등록",
"keywords": [
"주차"
],
"answer": "주차 등록은 총무팀 포털에서 차량 번호를 적어 신청합니다."
},
{
"id": "FAQ-303",
"title": "명함 신청",
"keywords": [
"명함"
],
"answer": "명함은 인사 포털에서 신청하면 일주일 안에 자리로 배달됩니다."
},
{
"id": "FAQ-304",
"title": "사내 식당 메뉴",
"keywords": [
"식당",
"메뉴"
],
"answer": "사내 식당 메뉴는 매주 월요일 아침 사내 게시판에 올라옵니다."
},
{
"id": "FAQ-305",
"title": "교육 신청",
"keywords": [
"교육"
],
"answer": "짧"
},
{
"id": "FAQ-306",
"title": "건강검진",
"keywords": [
"건강검진"
],
"answer": "건강검진은 매년 9월에 인사팀이 안내하며 지정 병원에서 받습니다."
}
]
},
"expect": {
"equals": {
"schema": "faq",
"total": 6,
"valid": 5
},
"contains": {
"rows.4.errors": "too_short:answer"
}
}
},
{
"name": "모르는 스키마 → 400",
"body": {
"schema": "invoice",
"items": []
},
"expect": {
"status": 400
}
}
]
}