포페런츠
경력근거: 여행상품 기획·판매 준비부터 예약·주문·출발·여행 후 리뷰·구독에 이르는 End-to-End 여행 관리 비즈니스 로직을 고도화합니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
아너드는 목적지와 교통을 파는 것이 아니라 버스 한 대와 여행비서 한 명을 통째로 잡아 5060 회원을 다시 부르는 구조라, 편성 판정에 회원 명부가 들어갑니다. 한 번 왔던 회원을 언제 다시 부를 수 있는지가 코스마다 다르고, 그 간격을 지키지 않으면 같은 사람에게 같은 곳을 또 권하게 됩니다. 이 포트폴리오는 예약 기록과 회원 명부를 코스 기준으로 붙여 출발 회차를 판정하는 기술을 보여줍니다. 시니어 여행 상품 기획의 현장 감각이나 여행업 규제에 대한 전문성은 주장하지 않습니다.
공고는 '여행상품 기획·판매 준비부터 예약·주문·출발·여행 후 리뷰·구독에 이르는 End-to-End 여행 관리 비즈니스 로직을 고도화합니다' 를 담당 업무로 적습니다. 편성은 그 사슬의 첫 칸이라, 여기서 잘못 잡은 날짜가 뒤 칸을 전부 흔듭니다.
'예약·결제·객실·실시간 트랜잭션·재고 또는 온라인과 오프라인이 맞닿는 복잡한 도메인' 경험을 우대합니다. 버스 한 대와 여행비서 한 명은 온라인 예약 수와 상관없이 통째로 잡히는 오프라인 재고라, 좌석 수가 판정의 한 축이 됩니다.
'주어진 요구사항과 화면을 구현하는 데 그치지 않고, 무엇을 왜 만들어야 하는가를 고민하며 팀과 함께 결정해본 분' 을 필수로 둡니다. 편성 회의에 올릴 판정 기준을 정하는 일이 곧 그 고민입니다.
다음 달 달력에 올릴 출발 회차를 지난 상품들의 예약 흐름·코스 기준·회원 명부와 맞춰 회차마다 판정합니다. 같은 코스 같은 달에 실제로 몇 명이 모였는지를 평균으로 내 최소 인원과 견주고, 그 코스가 정원을 채우는 데 걸린 날을 남은 날과 견줍니다. 편성한 좌석이 버스 좌석을 넘는지 보고, 그 지역에서 다시 부를 수 있는 회원이 몇 명 남았는지 셉니다. 취소된 회차는 평균에서 빼고, 채우는 데 걸린 날 기록이 없는 코스는 남은 날 비교를 건너뛴 사실을 화면에 함께 띄웁니다. 예약 기록과 회원 명부는 지원자가 국내 패키지 여행의 공개된 운영 방식으로 지어낸 것이며 실제 고객 정보를 쓰지 않습니다.
다음 달 여행 달력을 한 번에 짜는 아너드 상품 기획자 — 상품 하나에 버스 한 대와 여행비서 한 명이 통째로 묶여서 날짜를 잘못 잡으면 그 자리가 전부 빈 채로 나가는데, 손에 있는 건 지난 상품들의 예약 흐름과 회원 명부뿐이다
달력을 짤 때 기획자가 실제로 재는 것은 그 코스가 그 달에 얼마나 모였는가와 지금 다시 부를 사람이 남았는가 둘입니다. 두 값은 예약 기록과 회원 명부에 이미 들어 있으니 출발 회차마다 계산해 두면, 손볼 회차가 회의 전에 목록으로 먼저 뜬다는 가정입니다.
출발 회차와 지난 출발 기록, 코스 기준표, 회원 명부를 올리면 회차마다 판정이 붙고 사유별 묶음과 근거가 한 화면에 뜹니다. 1주차는 예약 시스템에 붙지 않고 운영 화면에서 내려받은 표를 그대로 읽는 것으로 시작합니다.
지난 두 시즌에 실제로 연 편성 90건을 넣어 판정과 그때의 결과를 맞춰 보고, 어긋난 건이 평균 계산 쪽인지 회원 수 계산 쪽인지 나눠 리포트로 냅니다. 2주차부터 판정 배치와 화면을 AWS 에 올려 기획자가 주소로 들어와 씁니다.
정원이 모자란 회차에 같은 달 다른 주말을 넣어 다시 계산하고, 지역을 바꿨을 때 다시 부를 회원이 얼마나 늘어나는지 함께 보여주는 단계입니다.
기획자가 출발 회차와 표 네 장을 올리면 회차마다 판정이 사유별로 뜨고, 어느 값이 그 판정을 만들었는지 그 자리에서 펼쳐집니다.
지난 두 시즌 편성 90건을 넣어 판정과 그때의 결과를 맞춰 일치율을 냅니다. 단, 예약 기록과 정답을 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
채우는 데 걸린 날이 비어 있는 코스, 마지막 여행일이 비어 있는 회원, 편성 좌석이 버스 좌석과 똑같은 회차를 섞은 입력 30종을 넣어도 화면이 죽지 않고 그 회차가 왜 일부 계산을 건너뛰었는지 읽힙니다.
열 이름이 빠지거나 날짜 형식이 뒤섞이거나 인코딩이 깨진 표 12종을 올려도 목록 대신 어느 줄이 왜 걸렸는지 이슈로 뜨고 나머지 회차는 그대로 판정됩니다.
진짜 성능 검증은 여기서 멈춥니다. 위 수치는 지어낸 예약 기록의 자가채점이며, 실제 적중률은 아너드의 실 예약 이력과 실 회원 명부가 있어야 측정됩니다. 이 포트폴리오는 그 대조를 바로 돌릴 수 있는 상태로 만듭니다.
일치율은 제가 지어낸 예약 기록과 회원 명부에 제가 매긴 정답을 맞춘 자가채점이라 성능 증명이 아닙니다. 실제 모객은 날씨와 명절 자리에도 흔들리므로, 면접에서는 이 수치 대신 데모가 도는 모습과 지난 시즌 기록을 받았을 때 무엇부터 대조할지를 말씀드립니다.
정답을 작성자가 붙였으니 잘 맞는 것이 당연합니다. 채우는 데 걸린 날이 비어 있는 코스와 마지막 여행일이 비어 있는 회원, 좌석과 편성이 똑같은 회차를 더미에 일부러 섞어 쉬운 줄만 푸는 함정을 피합니다.
지난해 평균이 낮았다는 사실이 올해도 안 모인다는 뜻은 아닙니다. 그해 날씨와 명절 위치가 다르면 같은 주말도 다르게 움직이므로, 화면은 판정과 함께 근거가 된 회차를 항상 펼쳐 보여 결론으로 읽히지 않게 둡니다.
다시 부를 회원 수는 명부에 적힌 지역과 마지막 여행일만 보고 센 값이라, 실제로 갈 마음이 있는지와는 다릅니다. 그 차이는 실제 모객 결과가 쌓여야 좁혀지므로 도입 단계 과제로 둡니다.
이 포트폴리오가 보여주는 것은 여러 표를 코스와 지역으로 붙여 규칙으로 판정하는 기술입니다. 시니어 여행 상품 기획의 현장 감각이나 여행업 규제 해석은 주장하지 않고, 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
실제 도입에서는 코스가 계절마다 갈라지고 버스도 대절 상황에 따라 좌석 수가 바뀝니다. 좌석 수를 회차마다 받는 구성과 성수기 보정은 도입 단계 과제로 분리합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
기획자가 다음 달 달력에 올려 본 출발 회차입니다. 판정 대상이며 한 행이 회차 한 자리입니다. 코스와 출발일, 걸어 둔 좌석 수, 모객 지역이 한 행에 있습니다. 편성 좌석이 버스 좌석과 똑같은 회차와 버스보다 큰 회차를 함께 담아 판정이 갈리게 두었습니다.
| slot_id | course_id | departure_date | seats_planned | region |
|---|---|---|---|---|
| SL-2610-01 | C-NAMHAE-3N | 2026-10-10 | 30 | 수도권 |
| SL-2610-02 | C-GANGNEUNG-2N | 2026-10-17 | 32 | 수도권 |
| SL-2610-03 | C-ANDONG-2N | 2026-10-24 | 30 | 수도권 |
| SL-2610-04 | C-GYEONGJU-2N | 2026-10-10 | 30 | 영남 |
| SL-2610-05 | C-JEONJU-1N | 2026-10-17 | 24 | 호남 |
아래 JSON Schema 를 그대로 만족하는 출발 회차 행을 60개 더 만들어 주세요. 코스를 열 개쯤 두고 10월과 11월 주말에 흩으며, 좌석 수는 20에서 40 사이로 둡니다. 지역은 다섯 권역에서 고르고 같은 코스를 다른 주말에 두 번 이상 두기도 합니다. 판정 결과는 넣지 마세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 좌석 수가 그 코스 버스 좌석과 똑같은 회차, (2) 출발까지 3주도 안 남은 회차, (3) 같은 주말에 같은 지역 회차가 둘. 생성한 행은 반드시 위 스키마를 통과해야 하며, 헤더가 빠지거나 인코딩이 깨진 형식 깨뜨리기 시도는 별도로 3건만 표시해 두세요.
{"type": "object", "properties": {"slot_id": {"type": "string"}, "course_id": {"type": "string"}, "departure_date": {"type": "string"}, "seats_planned": {"type": "integer", "minimum": 0}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}}, "required": ["slot_id", "course_id", "departure_date", "seats_planned", "region"]}{"type": "object", "properties": {"slot_id": {"type": "string"}, "course_id": {"type": "string"}, "departure_date": {"type": "string"}, "seats_planned": {"type": "integer", "minimum": 0}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}}, "required": ["slot_id", "course_id", "departure_date", "seats_planned", "region"]}지난해와 재작년에 실제로 나간 회차 기록입니다. 몇 명이 예약했는지, 정원을 채우는 데 며칠이 걸렸는지, 취소된 회차인지가 한 행에 있습니다. 취소된 회차와 채우는 데 걸린 날이 비어 있는 회차를 일부러 담아, 평균에서 빼는 자리와 남은 날 비교를 건너뛰는 자리가 데이터에서 드러나게 했습니다.
| course_id | departure_date | month | booked_count | days_to_fill | cancelled |
|---|---|---|---|---|---|
| C-NAMHAE-3N | 2025-10-11 | 10 | 16 | 41 | false |
| C-NAMHAE-3N | 2025-10-18 | 10 | 20 | 34 | false |
| C-NAMHAE-3N | 2025-10-25 | 10 | 18 | 38 | false |
| C-NAMHAE-3N | 2024-10-19 | 10 | 6 | — | true |
| C-GANGNEUNG-2N | 2025-10-11 | 10 | 26 | 25 | false |
아래 JSON Schema 를 그대로 만족하는 지난 출발 행을 140개 더 만들어 주세요. 코스마다 달별로 두세 회차씩 두 해치를 두고, 예약 인원은 6명에서 38명 사이로 흩습니다. 정원을 채우는 데 걸린 날은 14일에서 50일 사이로 두되 2할은 비웁니다. 취소된 회차는 1할쯤 두고 그때 예약 인원을 낮게 잡습니다. 판정 결과는 넣지 마세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 채우는 데 걸린 날이 비어 있는 회차, (2) 취소된 회차, (3) 같은 코스 같은 달인데 인원이 두 배 차이 나는 회차, (4) 한 코스의 회차가 한 건뿐인 달. 생성한 행은 반드시 위 스키마를 통과해야 합니다.
{"type": "object", "properties": {"course_id": {"type": "string"}, "departure_date": {"type": "string"}, "month": {"type": "integer", "minimum": 1, "maximum": 12}, "booked_count": {"type": "integer", "minimum": 0}, "days_to_fill": {"type": ["integer", "null"], "minimum": 0}, "cancelled": {"type": "boolean"}}, "required": ["course_id", "departure_date", "month", "booked_count", "days_to_fill", "cancelled"]}{"type": "object", "properties": {"course_id": {"type": "string"}, "departure_date": {"type": "string"}, "month": {"type": "integer", "minimum": 1, "maximum": 12}, "booked_count": {"type": "integer", "minimum": 0}, "days_to_fill": {"type": ["integer", "null"], "minimum": 0}, "cancelled": {"type": "boolean"}}, "required": ["course_id", "departure_date", "month", "booked_count", "days_to_fill", "cancelled"]}코스마다 정해 둔 기준표입니다. 최소 인원과 버스 좌석, 기본 모객 지역, 다시 부르기까지 비우는 날이 들어 있습니다. 판정에 쓰이는 기준은 출발 회차나 예약 기록에 붙어 있지 않고 전부 이 표에서 옵니다.
| course_id | course_name | min_headcount | bus_seats | region | rebook_interval_days |
|---|---|---|---|---|---|
| C-NAMHAE-3N | 남해 유채길 3박 | 24 | 32 | 수도권 | 120 |
| C-GANGNEUNG-2N | 강릉 바다열차 2박 | 20 | 28 | 수도권 | 90 |
| C-ANDONG-2N | 안동 하회 2박 | 24 | 36 | 수도권 | 120 |
| C-YEOSU-3N | 여수 밤바다 3박 | 22 | 28 | 수도권 | 150 |
| C-JEONJU-1N | 전주 한옥 1박 | 18 | 28 | 호남 | 60 |
아래 JSON Schema 를 그대로 만족하는 코스 기준 행을 30개 더 만들어 주세요. 최소 인원을 16명에서 30명 사이, 버스 좌석을 28석에서 45석 사이, 다시 부르기까지 비우는 날을 60일에서 180일 사이로 흩습니다. 밤이 길수록 최소 인원과 비우는 날을 크게 잡습니다. 판정 결과는 넣지 마세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 최소 인원과 버스 좌석 차이가 두 자리도 안 되는 코스, (2) 비우는 날이 180일로 긴 코스, (3) 출발 회차에 한 번도 안 나오는 코스. 생성한 행은 반드시 위 스키마를 통과해야 합니다.
{"type": "object", "properties": {"course_id": {"type": "string"}, "course_name": {"type": "string"}, "min_headcount": {"type": "integer", "minimum": 0}, "bus_seats": {"type": "integer", "minimum": 0}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}, "rebook_interval_days": {"type": "integer", "minimum": 0}}, "required": ["course_id", "course_name", "min_headcount", "bus_seats", "region", "rebook_interval_days"]}{"type": "object", "properties": {"course_id": {"type": "string"}, "course_name": {"type": "string"}, "min_headcount": {"type": "integer", "minimum": 0}, "bus_seats": {"type": "integer", "minimum": 0}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}, "rebook_interval_days": {"type": "integer", "minimum": 0}}, "required": ["course_id", "course_name", "min_headcount", "bus_seats", "region", "rebook_interval_days"]}아너드 회원 명부입니다. 사는 권역과 마지막으로 다녀온 날, 그때 간 코스가 한 행에 있습니다. 다시 부를 회원 수를 세는 근거이며, 이 데모의 명부는 여기 실린 248행이 전부입니다 — 화면에 뜨는 대상자 수는 모두 이 표를 그대로 세어 나온 값이라 사람이 다시 세어 볼 수 있습니다. 권역은 수도권이 절반을 넘고 강원처럼 열 명이 안 되는 권역도 그대로 담았으며, 아직 한 번도 안 다녀와 마지막 여행일이 비어 있는 회원을 1할쯤 두어 간격을 잴 수 없는 자리가 드러나게 했습니다.
| member_id | region | last_trip_date | last_course_id |
|---|---|---|---|
| M-10001 | 수도권 | 2025-05-04 | C-JEONJU-1N |
| M-10002 | 수도권 | 2026-06-04 | C-TONGYEONG-3N |
| M-10003 | 수도권 | 2025-10-01 | C-GANGNEUNG-2N |
| M-10004 | 수도권 | 2026-04-04 | C-DANYANG-2N |
| M-10005 | 수도권 | 2025-03-03 | C-TONGYEONG-3N |
아래 JSON Schema 를 그대로 만족하는 회원 행을 2,000개 더 만들어 주세요. 이 증강본은 1주차 화면이 쓰는 명부가 아니라, 2주차 평가 러너에서 명부가 열 배로 늘어도 대상자 세기가 버티는지 재는 데 씁니다. 권역은 수도권을 절반 넘게 두고 나머지를 네 권역에 흩으며, 마지막 여행일은 최근 열여덟 달 안에 두되 1할은 비웁니다. 마지막으로 간 코스는 코스 기준표에 있는 번호에서 고르고 마지막 여행일이 비면 함께 비웁니다. 실제 사람의 정보를 쓰지 말고 판정 결과도 넣지 마세요. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 마지막 여행일이 비어 있는 회원, (2) 지난달에 다녀와 간격이 한참 모자란 회원, (3) 이번 회차 코스를 이미 다녀온 회원, (4) 회원이 열 명도 안 되는 권역. 생성한 행은 반드시 위 스키마를 통과해야 합니다.
{"type": "object", "properties": {"member_id": {"type": "string"}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}, "last_trip_date": {"type": ["string", "null"]}, "last_course_id": {"type": ["string", "null"]}}, "required": ["member_id", "region", "last_trip_date", "last_course_id"]}{"type": "object", "properties": {"member_id": {"type": "string"}, "region": {"type": "string", "enum": ["수도권", "충청", "영남", "호남", "강원"]}, "last_trip_date": {"type": ["string", "null"]}, "last_course_id": {"type": ["string", "null"]}}, "required": ["member_id", "region", "last_trip_date", "last_course_id"]}| TypeScript | 출발 회차 판정 규칙부터 화면까지 한 언어로 씁니다. 인원과 좌석과 날 수가 섞이지 않도록 단위를 타입으로 박습니다. |
| NestJS | 출발 회차와 네 표를 받아 판정을 돌리는 API 서버입니다. 규칙 실행은 이쪽 한 곳에만 둡니다. |
| TypeORM | 코스 기준표와 지난 출발 기록을 코스 번호로 잇는 조회를 맡습니다. |
| MySQL | 출발 회차와 지난 출발 기록, 코스 기준표, 회원 명부를 담습니다. 지역과 마지막 여행일에 건 인덱스가 회원 수 세기 속도를 좌우합니다. |
| React | 판정 카드를 펼쳐 근거 값을 보여주는 화면 부분입니다. |
| Next.js | 기획자가 보는 다음 달 편성 화면입니다. 사유별 묶음과 근거 표를 한 화면에 둡니다. |
| React Query | 출발 회차를 고쳐 다시 계산할 때 화면이 멈추지 않도록 판정 결과를 다시 받아 옵니다. |
| Tailwind | 5060 고객을 상대하는 팀이 쓰는 화면이라 글자 크기와 대비를 넉넉하게 잡는 데 씁니다. |
| shadcn/ui | 판정 카드와 근거 표의 기본 구성 요소를 가져다 씁니다. |
| Docker | 판정 배치와 화면을 같은 이미지로 묶어 로컬과 배포가 같은 구성으로 돌게 합니다. |
| AWS | 2주차부터 판정 배치와 화면을 여기에 올립니다. |
| LLM | 판정이 정해진 뒤 편성 회의에 올릴 한 줄 설명을 씁니다. 판정 네 값에는 손대지 못합니다. |
| Claude | 회의용 설명 문장을 쓰는 기본 모델입니다. |
✗ fail → 지난 출발 기록이 출발 회차 코스의 절반도 덮지 못하면 기록을 먼저 채웁니다.
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 지난 두 시즌 편성 90건과 판정을 맞춰 보는 단계로 넘어갑니다.
✗ fail → 정원 미달 위험이 실제 취소와 자주 어긋나면 평균을 내는 기간을 다시 잡습니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 손볼 회차에 대안 날짜를 함께 내는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · slot_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
정원 미달 위험정원 미달버스 좌석 초과 편성좌석 초과다시 부를 회원 모자람재모객 부족그대로 연다그대로출발 회차 한 자리에 대한 편성 판정과 그 판정을 만든 숫자를 남기는 형식입니다. 판정 네 값은 평균과 대소 비교가 정하고 회의용 설명 문장만 모델이 씁니다. 화면은 이 판정 값으로 묶습니다.
{"type": "object", "properties": {"plan_no": {"type": "string"}, "slot_id": {"type": "string"}, "course_id": {"type": "string"}, "departure_date": {"type": "string"}, "verdict": {"type": "string", "enum": ["그대로 연다", "정원 미달 위험", "다시 부를 회원 모자람", "버스 좌석 초과 편성"]}, "avg_booked_prev": {"type": ["number", "null"], "minimum": 0, "description": "past_departures 에서 cancelled 가 거짓인 회차만 골라 같은 course_id·같은 month 의 booked_count 를 평균 낸 값이며, 그 달 지난 회차가 하나도 없으면 비웁니다"}, "days_left": {"type": "integer", "description": "판정 기준일 2026-09-08 부터 candidate_slots.departure_date 까지 남은 날입니다"}, "days_to_fill_median": {"type": ["integer", "null"], "description": "past_departures 에서 cancelled 가 거짓인 같은 course_id 회차의 days_to_fill 중앙값이며, 짝수 개라 .5 로 나오면 내림합니다. 값이 하나도 없으면 비웁니다"}, "rebookable_count": {"type": ["integer", "null"], "minimum": 0, "description": "member_roster 에서 region 이 그 회차의 region 과 같고 last_trip_date 에서 departure_date 까지 지난 날이 course_standard.rebook_interval_days 이상이며 last_course_id 가 이 course_id 가 아닌 회원 수입니다. 명부 248행을 그대로 세어 냅니다"}, "rebookable_needed": {"type": "integer", "minimum": 0, "description": "course_standard.min_headcount 의 세 배입니다"}, "evidence": {"type": "array", "items": {"type": "object", "properties": {"dataset": {"type": "string"}, "field": {"type": "string"}, "agg": {"type": ["string", "null"]}, "value": {"type": ["string", "number", "boolean", "null"]}}, "required": ["dataset", "field", "value"]}}, "summary": {"type": "string"}, "hedge": {"type": ["string", "null"]}}, "required": ["plan_no", "slot_id", "course_id", "departure_date", "verdict", "evidence", "summary"]}[
{
"plan_no": "SP-001",
"slot_id": "SL-2610-01",
"course_id": "C-NAMHAE-3N",
"departure_date": "2026-10-10",
"verdict": "정원 미달 위험",
"avg_booked_prev": 18,
"days_left": 32,
"days_to_fill_median": 38,
"rebookable_count": 101,
"rebookable_needed": 72,
"evidence": [
{
"dataset": "past_departures",
"field": "booked_count",
"agg": "avg",
"value": 18
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 24
},
{
"dataset": "past_departures",
"field": "days_to_fill",
"agg": "median",
"value": 38
},
{
"dataset": "candidate_slots",
"field": "departure_date",
"agg": null,
"value": "2026-10-10"
}
],
"summary": "남해 유채길은 지난 두 해 10월 회차가 평균 18명이라 최소 24명에 못 미쳤고, 출발까지 남은 32일은 이 코스가 정원을 채우는 데 걸린 38일보다 짧습니다.",
"hedge": null
},
{
"plan_no": "SP-002",
"slot_id": "SL-2610-02",
"course_id": "C-GANGNEUNG-2N",
"departure_date": "2026-10-17",
"verdict": "버스 좌석 초과 편성",
"avg_booked_prev": 25,
"days_left": 39,
"days_to_fill_median": 23,
"rebookable_count": 107,
"rebookable_needed": 60,
"evidence": [
{
"dataset": "candidate_slots",
"field": "seats_planned",
"agg": null,
"value": 32
},
{
"dataset": "course_standard",
"field": "bus_seats",
"agg": null,
"value": 28
},
{
"dataset": "past_departures",
"field": "booked_count",
"agg": "avg",
"value": 25
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 20
}
],
"summary": "강릉 바다열차에 32석을 걸었는데 이 코스가 쓰는 버스는 28석이라 네 자리가 갈 곳이 없습니다.",
"hedge": null
},
{
"plan_no": "SP-003",
"slot_id": "SL-2611-03",
"course_id": "C-TONGYEONG-3N",
"departure_date": "2026-11-14",
"verdict": "다시 부를 회원 모자람",
"avg_booked_prev": 29,
"days_left": 67,
"days_to_fill_median": 34,
"rebookable_count": 29,
"rebookable_needed": 78,
"evidence": [
{
"dataset": "member_roster",
"field": "member_id",
"agg": "count",
"value": 29
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 26
},
{
"dataset": "course_standard",
"field": "rebook_interval_days",
"agg": null,
"value": 150
},
{
"dataset": "candidate_slots",
"field": "region",
"agg": null,
"value": "영남"
},
{
"dataset": "past_departures",
"field": "days_to_fill",
"agg": "median",
"value": 34
}
],
"summary": "영남에서 다시 부를 수 있는 회원이 29명인데 통영 섬여행은 정원이 커서 78명은 있어야 합니다. 다섯 달을 비우는 코스라 최근 다녀온 회원이 빠지고, 통영을 이미 다녀온 회원도 뺀 수입니다.",
"hedge": "정원을 채우는 데 걸린 날 중앙값은 33일과 36일의 가운데라 34.5 인데, 정수로 적으려고 34로 내렸습니다."
},
{
"plan_no": "SP-004",
"slot_id": "SL-2611-01",
"course_id": "C-YEOSU-3N",
"departure_date": "2026-11-01",
"verdict": "그대로 연다",
"avg_booked_prev": 31,
"days_left": 54,
"days_to_fill_median": 24,
"rebookable_count": 102,
"rebookable_needed": 66,
"evidence": [
{
"dataset": "past_departures",
"field": "booked_count",
"agg": "avg",
"value": 31
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 22
},
{
"dataset": "candidate_slots",
"field": "seats_planned",
"agg": null,
"value": 28
},
{
"dataset": "course_standard",
"field": "bus_seats",
"agg": null,
"value": 28
},
{
"dataset": "member_roster",
"field": "member_id",
"agg": "count",
"value": 102
}
],
"summary": "여수 밤바다는 지난 11월 회차가 평균 31명이었고 28석 편성이 버스 좌석과 같으며 수도권에서 다시 부를 회원도 102명 남았습니다.",
"hedge": null
},
{
"plan_no": "SP-005",
"slot_id": "SL-2610-05",
"course_id": "C-JEONJU-1N",
"departure_date": "2026-10-17",
"verdict": "정원 미달 위험",
"avg_booked_prev": 16,
"days_left": 39,
"days_to_fill_median": null,
"rebookable_count": 16,
"rebookable_needed": 54,
"evidence": [
{
"dataset": "past_departures",
"field": "booked_count",
"agg": "avg",
"value": 16
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 18
},
{
"dataset": "past_departures",
"field": "days_to_fill",
"agg": null,
"value": null
},
{
"dataset": "candidate_slots",
"field": "departure_date",
"agg": null,
"value": "2026-10-17"
}
],
"summary": "전주 한옥은 지난 10월 회차가 평균 16명이라 최소 18명에 못 미칩니다.",
"hedge": "정원을 채우는 데 걸린 날 기록이 두 회차 모두 비어 있어 남은 날 비교는 하지 못했고 평균만으로 위험으로 두었습니다. 호남에서 다시 부를 회원도 16명뿐이지만 판정 순서상 정원 미달이 앞서 이 사유로 묶었습니다."
},
{
"plan_no": "SP-006",
"slot_id": "SL-2611-02",
"course_id": "C-SOKCHO-2N",
"departure_date": "2026-11-07",
"verdict": "다시 부를 회원 모자람",
"avg_booked_prev": 25,
"days_left": 60,
"days_to_fill_median": 24,
"rebookable_count": 9,
"rebookable_needed": 66,
"evidence": [
{
"dataset": "member_roster",
"field": "member_id",
"agg": "count",
"value": 9
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 22
},
{
"dataset": "course_standard",
"field": "rebook_interval_days",
"agg": null,
"value": 90
},
{
"dataset": "candidate_slots",
"field": "region",
"agg": null,
"value": "강원"
},
{
"dataset": "past_departures",
"field": "booked_count",
"agg": "avg",
"value": 25
}
],
"summary": "속초 설악은 지난 11월 평균 25명으로 최소 22명을 넘겼지만, 강원 권역 회원이 아홉 명뿐이라 필요한 66명에 한참 못 미칩니다. 모객 지역을 넓히지 않으면 이 회차는 열기 어렵습니다.",
"hedge": null
},
{
"plan_no": "SP-007",
"slot_id": "SL-2610-08",
"course_id": "C-TONGYEONG-3N",
"departure_date": "2026-10-31",
"verdict": "다시 부를 회원 모자람",
"avg_booked_prev": null,
"days_left": 53,
"days_to_fill_median": 34,
"rebookable_count": 29,
"rebookable_needed": 78,
"evidence": [
{
"dataset": "past_departures",
"field": "month",
"agg": null,
"value": 11
},
{
"dataset": "candidate_slots",
"field": "departure_date",
"agg": null,
"value": "2026-10-31"
},
{
"dataset": "member_roster",
"field": "member_id",
"agg": "count",
"value": 29
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 26
},
{
"dataset": "candidate_slots",
"field": "seats_planned",
"agg": null,
"value": 34
},
{
"dataset": "course_standard",
"field": "bus_seats",
"agg": null,
"value": 36
}
],
"summary": "통영 섬여행은 10월에 연 적이 없어 견줄 평균이 없고, 영남에서 다시 부를 회원 29명도 필요한 78명에 못 미칩니다. 편성한 34석은 36석 버스 안에 듭니다.",
"hedge": "지난 통영 회차가 모두 11월이라 10월 평균을 낼 수 없어 정원 미달 조건은 건너뛰었습니다. 인접한 달 회차로 대신 볼지는 기획자가 정합니다."
},
{
"plan_no": "SP-008",
"slot_id": "SL-2611-04",
"course_id": "C-ANDONG-2N",
"departure_date": "2026-11-14",
"verdict": "그대로 연다",
"avg_booked_prev": null,
"days_left": 67,
"days_to_fill_median": 28,
"rebookable_count": 104,
"rebookable_needed": 72,
"evidence": [
{
"dataset": "past_departures",
"field": "month",
"agg": null,
"value": 10
},
{
"dataset": "candidate_slots",
"field": "departure_date",
"agg": null,
"value": "2026-11-14"
},
{
"dataset": "member_roster",
"field": "member_id",
"agg": "count",
"value": 104
},
{
"dataset": "course_standard",
"field": "min_headcount",
"agg": null,
"value": 24
},
{
"dataset": "candidate_slots",
"field": "seats_planned",
"agg": null,
"value": 32
},
{
"dataset": "course_standard",
"field": "bus_seats",
"agg": null,
"value": 36
}
],
"summary": "안동 하회는 11월에 연 적이 없어 평균을 못 냈지만, 32석 편성이 36석 버스 안에 들고 수도권에서 다시 부를 회원도 104명 남아 걸리는 조건이 없습니다.",
"hedge": "11월 지난 회차가 없어 정원 미달 조건은 건너뛰었습니다. 10월 회차 평균 27명을 대신 볼지는 기획자가 정하고, 정원을 채우는 데 걸린 날 중앙값은 30일과 27일의 가운데라 28.5 인데 정수로 적으려고 28로 내렸습니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 enum 네 값 중 하나인지 확인합니다. 판정은 계산 규칙이 정하므로 모델이 다른 값을 쓰면 그 응답을 버립니다. 2. plan_no·slot_id·course_id·departure_date·verdict·evidence·summary 일곱 필드가 모두 채워졌는지 확인합니다. 3. avg_booked_prev 와 rebookable_count 가 0 이상인지 범위를 확인합니다. 4. avg_booked_prev 를 past_departures 에서 cancelled 가 거짓인 같은 course_id·같은 month 회차의 booked_count 평균으로 다시 계산하고 소수 첫째 자리에서 반올림합니다. 지난해와 재작년 회차를 함께 평균에 넣습니다 — 해마다 다른 날씨와 명절 자리는 숫자로 갈리지 않아 risks 에 적어 두고 기획자가 봅니다. 4-1. 같은 course_id·같은 month 로 남은 회차가 하나도 없으면 avg_booked_prev 를 비우고 정원 미달 위험 조건을 건너뜁니다. 남은 판정(버스 좌석 초과 편성 → 다시 부를 회원 모자람)만 보고, 셋 어디에도 안 걸리면 그대로 연다로 두되 '그 달 지난 기록이 없다' 는 사실을 hedge 에 남깁니다. SL-2610-08(통영 10월)·SL-2611-04(안동 11월)·SL-2611-05(강릉 11월)·SL-2611-06(남해 11월)이 여기에 해당합니다. 5. days_left 를 판정 기준일 2026-09-08 과 candidate_slots.departure_date 사이 날 수로 다시 계산합니다. 6. rebookable_needed 를 course_standard.min_headcount 의 세 배로 다시 계산합니다. 7. verdict 가 정원 미달 위험이면 avg_booked_prev 가 min_headcount 보다 작고 days_left 가 days_to_fill_median 보다 작은지 다시 계산합니다. 8. verdict 가 버스 좌석 초과 편성이면 candidate_slots.seats_planned 가 course_standard.bus_seats 보다 큰지 다시 계산합니다. 9. verdict 가 다시 부를 회원 모자람이면 rebookable_count 가 rebookable_needed 보다 작은지 다시 계산합니다. 10. rebookable_count 는 member_roster 를 그대로 세어 냅니다. 이 편의 명부는 인라인 248행이 전부이므로 화면에 뜬 대상자 수는 사람이 그 표를 세어 다시 확인할 수 있습니다. augment_prompt 로 만드는 증강본은 2주차 러너에서 규모가 커져도 세기가 버티는지 보는 데만 쓰고 1주차 판정에는 넣지 않습니다. 10-1. 같은 권역·같은 간격이면 출발일이 늦은 회차의 대상자 수가 이른 회차보다 적어질 수 없습니다. 러너는 이 단조성을 함께 검사해, 깨지면 명부나 코스 기준표가 어긋난 것으로 보고 판정을 내지 않습니다. 11. past_departures.days_to_fill 이 그 코스에서 하나도 없으면 days_to_fill_median 을 비우고, 남은 날 비교 없이 평균만으로 판정한 사실을 hedge 에 남겼는지 확인합니다. SL-2610-05 가 조건이 성립한 경우이고 SL-2610-01 이 성립하지 않은 경우입니다. 12. member_roster.last_trip_date 가 비어 있는 회원은 재방문 간격을 잴 수 없으므로 rebookable_count 에서 뺐는지 확인합니다. 13. evidence 의 field 이름이 candidate_slots·past_departures·course_standard·member_roster 에 실재하는 컬럼인지 대조합니다. 14. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 같은지 확인합니다. 여러 회차에서 낸 평균과 회원 수에는 agg 를 함께 적습니다. 15. summary 가 존댓말로 끝나는 한국어 문장인지 확인합니다. 16. 판정 순서는 정원 미달 위험, 버스 좌석 초과 편성, 다시 부를 회원 모자람 순이며 앞에서 걸리면 뒤를 보지 않습니다. 셋 어디에도 안 걸리면 그대로 연다입니다. 17. days_to_fill_median 은 정수 필드라 회차가 짝수 개여서 중앙값이 .5 로 나오면 내림합니다. 남은 날과 대소를 다투는 값이라 내림하면 정원 미달 위험이 덜 뜨는 쪽으로 기울며, 위험을 부풀리지 않는 쪽을 골랐습니다. 안동은 30일과 27일이라 28.5 에서 28 로, 통영은 33일과 36일이라 34.5 에서 34 로 내려갑니다. [평가 러너 · 2주차] 1. 지난 두 시즌에 실제로 연 편성 90건을 넣어 판정과 그때의 결과를 맞춰 봅니다. 정원 미달 위험으로 뜬 회차가 실제로 취소됐는지, 그대로 연다로 뜬 회차가 정원을 채웠는지 나눠 리포트에 답니다. [수동 검증 · 사람이 봅니다] 1. 명절과 단풍 성수기처럼 달력에서만 보이는 사정은 기획자가 봅니다. 이 화면은 숫자로 갈리는 자리까지만 냅니다. 2. 다시 부를 회원이 모자란 회차를 접을지 지역을 바꿔 열지는 기획자가 정합니다.