$ uv run tools/flow.py call my-faq-check requests/tickets-new.json HTTP 200 · 0.1s { "batch": "TK-1", "total": 9, "valid": 0, "invalid": 9, "with_warnings": 0, "rows": [ { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] }, { "id": "", "ok": false, "errors": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "warnings": [] } ], "codes": { "id_format": 9, "keywords_missing": 9, "answer_missing": 9, "duplicate_in_batch": 8 }, "fix_list": [ { "row": 1, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요" ] }, { "row": 2, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 3, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 4, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 5, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 6, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 7, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 8, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] }, { "row": 9, "id": "", "codes": [ "id_format", "keywords_missing", "answer_missing", "duplicate_in_batch" ], "fix": [ "id를 FAQ-숫자 3자리로 (예: FAQ-104)", "키워드를 하나 이상 넣어 주세요", "답을 써 주세요", "같은 배치에 같은 id가 또 있습니다. 하나만 남겨 주세요" ] } ], "message": "TK-1: 9줄 중 0줄 통과, 9줄 고칠 것" } $ uv run tools/flow.py test cases/lab03-schema.json -v POST http://localhost:5678/webhook/my-check · 5 case(s) × 1 ✔ 스키마 목록에 ticket이 있다 · 0.0s → request POST http://localhost:8800/schemas/list {} ← response HTTP 200 { "schemas": [ "employee", "faq", "ticket" ] } ✔ 새 스키마 ticket · 워크플로우는 그대로 · 9줄 중 2줄 통과 · 0.1s → request POST /webhook/my-check { "schema": "ticket", "batch": "TK-1", "items": [ { "ticket_id": "T-2001", "title": "모니터가 켜지지 않음", "requester": "박서연", "priority": "normal", "opened": "2026-10-28", "contact": "010-2345-6789" }, { "ticket_id": "T-12", "title": "메일 첨부가 안 열림", "requester": "이준호", "priority": "low", "opened": "2026-10-28" }, { "ticket_id": "T-2003", "title": "VPN이 계속 끊김", "requester": "최유진", "priority": "urgent", "opened": "2026-10-29" }, { "ticket_id": "T-2004", "title": "키보드 교체 요청", "requester": "한지훈", "priority": "low", "opened": "2026-02-30" }, { "ticket_id": "T-2005", "title": "프린터", "requester": "김민지", "priority": "normal", "opened": "2026-10-29" }, { "ticket_id": "T-2006", "title": "공유 드라이브 권한", "requester": "박서연", "priority": "normal", "opened": "2026-10-30", "contact": "010-12-3456" }, { "ticket_id": "T-2001", "title": "모니터가 또 꺼짐", "requester": "박서연", "priority": "high", "opened": "2026-10-30" }, { "ticket_id": "T-2008", "title": "화상회의 카메라 대여", "requester": "", "priority": "low", "opened": "2026-10-30" }, { "ticket_id": "T-2009", "title": "새 직원 계정 만들기", "requester": "이준호", "priority": "high", "opened": "2026-10-30" } ] } ← response HTTP 200 { "schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [ { "row": 1, "key": "T-2001", "ok": true, "errors": [] }, { "row": 2, "key": "T-12", "ok": false, "errors": [ "bad_format:ticket_id" ] }, { "row": 3, "key": "T-2003", "ok": false, "errors": [ "not_allowed:priority" ] }, { "row": 4, "key": "T-2004", "ok": false, "errors": [ "bad_format:opened" ] }, { "row": 5, "key": "T-2005", "ok": false, "errors": [ "too_short:title" ] }, { "row": 6, "key": "T-2006", "ok": false, "errors": [ "bad_format:contact" ] }, { "row": 7, "key": "T-2001", "ok": false, "errors": [ "duplicate_in_batch" ] }, { "row": 8, "key": "T-2008", "ok": false, "errors": [ "missing:requester" ] }, { "row": 9, "key": "T-2009", "ok": true, "errors": [] } ], "codes": { "bad_format:ticket_id": 1, "not_allowed:priority": 1, "bad_format:opened": 1, "too_short:title": 1, "bad_format:contact": 1, "duplicate_in_batch": 1, "missing:requester": 1 } } ✔ 형식 · 목록 · 날짜 · 길이 · 0.1s → request POST /webhook/my-check { "schema": "ticket", "batch": "TK-1", "items": [ { "ticket_id": "T-2001", "title": "모니터가 켜지지 않음", "requester": "박서연", "priority": "normal", "opened": "2026-10-28", "contact": "010-2345-6789" }, { "ticket_id": "T-12", "title": "메일 첨부가 안 열림", "requester": "이준호", "priority": "low", "opened": "2026-10-28" }, { "ticket_id": "T-2003", "title": "VPN이 계속 끊김", "requester": "최유진", "priority": "urgent", "opened": "2026-10-29" }, { "ticket_id": "T-2004", "title": "키보드 교체 요청", "requester": "한지훈", "priority": "low", "opened": "2026-02-30" }, { "ticket_id": "T-2005", "title": "프린터", "requester": "김민지", "priority": "normal", "opened": "2026-10-29" }, { "ticket_id": "T-2006", "title": "공유 드라이브 권한", "requester": "박서연", "priority": "normal", "opened": "2026-10-30", "contact": "010-12-3456" }, { "ticket_id": "T-2001", "title": "모니터가 또 꺼짐", "requester": "박서연", "priority": "high", "opened": "2026-10-30" }, { "ticket_id": "T-2008", "title": "화상회의 카메라 대여", "requester": "", "priority": "low", "opened": "2026-10-30" }, { "ticket_id": "T-2009", "title": "새 직원 계정 만들기", "requester": "이준호", "priority": "high", "opened": "2026-10-30" } ] } ← response HTTP 200 { "schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [ { "row": 1, "key": "T-2001", "ok": true, "errors": [] }, { "row": 2, "key": "T-12", "ok": false, "errors": [ "bad_format:ticket_id" ] }, { "row": 3, "key": "T-2003", "ok": false, "errors": [ "not_allowed:priority" ] }, { "row": 4, "key": "T-2004", "ok": false, "errors": [ "bad_format:opened" ] }, { "row": 5, "key": "T-2005", "ok": false, "errors": [ "too_short:title" ] }, { "row": 6, "key": "T-2006", "ok": false, "errors": [ "bad_format:contact" ] }, { "row": 7, "key": "T-2001", "ok": false, "errors": [ "duplicate_in_batch" ] }, { "row": 8, "key": "T-2008", "ok": false, "errors": [ "missing:requester" ] }, { "row": 9, "key": "T-2009", "ok": true, "errors": [] } ], "codes": { "bad_format:ticket_id": 1, "not_allowed:priority": 1, "bad_format:opened": 1, "too_short:title": 1, "bad_format:contact": 1, "duplicate_in_batch": 1, "missing:requester": 1 } } ✔ 선택 칸 형식 · 중복 · 빈 칸, 선택 칸이 없어도 통과 · 0.0s → request POST /webhook/my-check { "schema": "ticket", "batch": "TK-1", "items": [ { "ticket_id": "T-2001", "title": "모니터가 켜지지 않음", "requester": "박서연", "priority": "normal", "opened": "2026-10-28", "contact": "010-2345-6789" }, { "ticket_id": "T-12", "title": "메일 첨부가 안 열림", "requester": "이준호", "priority": "low", "opened": "2026-10-28" }, { "ticket_id": "T-2003", "title": "VPN이 계속 끊김", "requester": "최유진", "priority": "urgent", "opened": "2026-10-29" }, { "ticket_id": "T-2004", "title": "키보드 교체 요청", "requester": "한지훈", "priority": "low", "opened": "2026-02-30" }, { "ticket_id": "T-2005", "title": "프린터", "requester": "김민지", "priority": "normal", "opened": "2026-10-29" }, { "ticket_id": "T-2006", "title": "공유 드라이브 권한", "requester": "박서연", "priority": "normal", "opened": "2026-10-30", "contact": "010-12-3456" }, { "ticket_id": "T-2001", "title": "모니터가 또 꺼짐", "requester": "박서연", "priority": "high", "opened": "2026-10-30" }, { "ticket_id": "T-2008", "title": "화상회의 카메라 대여", "requester": "", "priority": "low", "opened": "2026-10-30" }, { "ticket_id": "T-2009", "title": "새 직원 계정 만들기", "requester": "이준호", "priority": "high", "opened": "2026-10-30" } ] } ← response HTTP 200 { "schema": "ticket", "batch": "TK-1", "total": 9, "valid": 2, "invalid": 7, "rows": [ { "row": 1, "key": "T-2001", "ok": true, "errors": [] }, { "row": 2, "key": "T-12", "ok": false, "errors": [ "bad_format:ticket_id" ] }, { "row": 3, "key": "T-2003", "ok": false, "errors": [ "not_allowed:priority" ] }, { "row": 4, "key": "T-2004", "ok": false, "errors": [ "bad_format:opened" ] }, { "row": 5, "key": "T-2005", "ok": false, "errors": [ "too_short:title" ] }, { "row": 6, "key": "T-2006", "ok": false, "errors": [ "bad_format:contact" ] }, { "row": 7, "key": "T-2001", "ok": false, "errors": [ "duplicate_in_batch" ] }, { "row": 8, "key": "T-2008", "ok": false, "errors": [ "missing:requester" ] }, { "row": 9, "key": "T-2009", "ok": true, "errors": [] } ], "codes": { "bad_format:ticket_id": 1, "not_allowed:priority": 1, "bad_format:opened": 1, "too_short:title": 1, "bad_format:contact": 1, "duplicate_in_batch": 1, "missing:requester": 1 } } ✔ 직원 CSV는 그대로 9줄 중 4줄 · 0.1s → request POST /webhook/my-check { "schema": "employee", "batch": "HR-1", "csv": "name,dept,email,phone,joined\n오세훈,영업팀,sehoon.oh@example.com,010-2222-3333,2026-10-01\n윤하늘,재무팀,haneul.yoon@example.com,,2026-10-05\n강민수,개발팀,minsu.kang@example,010-4444-5555,2026-10-06\n서지아,마케팅팀,jia.seo@example.com,010-6666-7777,2026-10-07\n임도윤,인사팀,doyun.lim@example.com,010-8888-9999,2026-13-01\n,IT팀,noname@example.com,,2026-10-09\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n배수아,총무팀,sua.bae@example.com,02-123-4567,2026-10-12\n정다은,IT팀,daeun.jung@example.com,010-1111-2222,2026-10-14" } ← response HTTP 200 { "schema": "employee", "batch": "HR-1", "total": 9, "valid": 4, "invalid": 5, "rows": [ { "row": 1, "key": "sehoon.oh@example.com", "ok": true, "errors": [] }, { "row": 2, "key": "haneul.yoon@example.com", "ok": true, "errors": [] }, { "row": 3, "key": "minsu.kang@example", "ok": false, "errors": [ "bad_format:email" ] }, { "row": 4, "key": "jia.seo@example.com", "ok": false, "errors": [ "not_allowed:dept" ] }, { "row": 5, "key": "doyun.lim@example.com", "ok": false, "errors": [ "bad_format:joined" ] }, { "row": 6, "key": "noname@example.com", "ok": false, "errors": [ "missing:name" ] }, { "row": 7, "key": "sua.bae@example.com", "ok": true, "errors": [] }, { "row": 8, "key": "sua.bae@example.com", "ok": false, "errors": [ "duplicate_in_batch" ] }, { "row": 9, "key": "daeun.jung@example.com", "ok": true, "errors": [] } ], "codes": { "bad_format:email": 1, "not_allowed:dept": 1, "bad_format:joined": 1, "missing:name": 1, "duplicate_in_batch": 1 } } Passed 5/5 report: runs/lab03-schema-20261101-181021.json