더블유클럽
경력근거: 경제력 서류 인증과 간편 인증으로 신뢰를 검증하고, 만남보장 정책으로 약속부터 만남까지 책임집니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
더블유클럽 공고는 회사를 '만남보장 정책으로 약속부터 만남까지 책임집니다' 로 소개하고, 누적 만남 11만 건 이상을 앞에 둡니다. 약속을 책임진다는 말은 상대가 약속을 깨면 만남권 값을 돌려준다는 뜻이라, 환불을 정하는 기준이 회원 사이의 약속 기록과 만남 뒤 평가에 있습니다. 결제한 물건을 돌려받는 쇼핑몰 환불과 달리 두 회원의 행동이 엇갈리는 곳에서 판정이 갈리고, 이 구조는 컨시어지가 약속을 잡고 두 회원이 서로 평가하는 W클럽 흐름이 있어야 성립합니다. 공고가 결제·매칭 영역을 내부 개발자 코드 리뷰를 거쳐 배포한다고 적은 만큼, 환불 판정 규칙을 테스트와 SQL 재계산 결과를 붙여 리뷰에 올리는 방식도 이 자리의 일하는 방식과 맞습니다. 이 포트폴리오는 약속 기록을 시각순으로 읽어 환불 여부를 정하는 Nest.js 규칙과 Next.js 관리자 페이지를 보여줍니다. 매칭 업계의 환불 약관이나 소비자 분쟁 기준에 대한 전문성은 주장하지 않습니다.
공고는 W클럽을 만남보장 정책으로 약속부터 만남까지 책임지는 서비스로 소개합니다. 그 약속이 깨졌을 때 누구에게 얼마를 돌려줄지는 약속 기록과 만남 뒤 평가를 읽어야 정해지고, 주요업무의 실사용자 CS·데이터에서 문제를 찾는 일이 바로 그 환불 문의에서 시작합니다.
이 자리는 Claude Code·Codex 로 기획부터 구현까지 직접 만들고 테스트와 화면 캡처로 검증해 배포합니다. 판정 규칙의 초안은 AI 에 맡기되 취소 뒤 재확정·48시간 정각 같은 만남권을 Vitest 로 고정하고, 같은 판정을 SQL 로 다시 계산하고, Playwright 캡처를 PR 에 붙여 결제 영역 코드 리뷰를 받습니다.
주요업무에 매주 출시 내역과 지표 변화를 숫자로 공유하는 일이 있고, 우대사항에 SQL 로 데이터를 직접 뽑는 역량이 있습니다. 대표가 받는 주간 환불 금액과 판정별 장수를 SQL 로 뽑아 화면 숫자와 맞추는 것이 이 편의 3주차입니다.
W클럽 남성 회원은 컨시어지가 맺어 준 상대와 만나려고 만남권 99,000원을 내고, 상대가 약속을 깨면 만남보장에 따라 그 금액을 돌려받습니다. 이 관리자 페이지는 만남권 결제 기록, 약속이 확정되고 취소된 기록, 만남 뒤 두 회원이 남긴 평가 세 표를 받아 만남권마다 다섯 판정 중 하나와 환불 금액을 붙입니다. 약속 기록을 시각순으로 읽어 마지막에 누가 취소했는지 가리고, 마지막 확정 약속에서 몇 시간이 지났는지 세고, 두 평가가 서로 맞는지 봅니다. CS 매니저는 상대 취소 묶음으로 환불을 처리하고 본인 취소 묶음으로 환불 문의에 답하며, 컨시어지는 평가가 엇갈린 만남권을 두 회원에게 확인하고, 대표는 화면 위의 이번 주 환불 예정 금액을 받습니다.
W클럽 회원의 약속과 환불 문의를 맡는 컨시어지 CS 매니저와, 매주 환불 금액을 숫자로 보고받는 대표. 같은 관리자 화면을 보고 CS 매니저는 환불 처리와 회원 안내를, 컨시어지는 평가가 엇갈린 만남을 두 회원에게 확인한다
지금 만남보장 환불은 회원이 '상대가 취소했어요' 하고 문의해야 CS 매니저가 그 만남권의 약속 기록을 찾아보는 순서로 움직입니다. 취소했다가 다시 확정한 약속, 한 번 상대가 취소한 뒤 회원이 취소한 약속처럼 기록이 여러 줄인 만남권은 마지막 줄을 잘못 읽기 쉽고, 문의하지 않은 회원의 상대 취소는 아무도 보지 않습니다. 세 표를 만남권 번호로 잇고 마지막 이벤트와 마지막 확정 시각, 두 평가를 규칙으로 읽으면 환불할 만남권과 환불하지 않을 만남권이 문의 전에 정해지고 대표에게 올릴 환불 금액도 같은 표에서 나옵니다.
CS 매니저가 만남권 결제·약속 이벤트·만남 후 평가 세 파일을 올리면 만남권마다 다섯 판정 중 하나와 환불 금액, 마지막으로 취소한 회원, 약속에서 지난 시간이 서는 Next.js 관리자 페이지입니다. 이벤트를 시각순으로 읽어 마지막 상태를 고르는 규칙, 일정 변경 뒤의 마지막 확정 시각, 48시간 계산, 판정 차례, 약속 이벤트가 0건인 만남권 처리가 이 주에 들어갑니다. 화면 위에는 이번 주 환불 예정 금액이 섭니다.
더미 만남권을 400장으로 늘리고, 같은 판정을 SQL 쿼리로 한 번 더 짜서 TypeScript 판정과 장마다 판정·환불 금액이 같은지 봅니다. 48시간 정각, 취소 뒤 재확정, 상대 취소 뒤 본인 취소 같은 만남권은 Vitest 로 고정하고, Playwright 로 판정 화면을 캡처해 바뀐 화면을 PR 에 붙입니다. 결제와 닿는 규칙이라 테스트 결과와 캡처를 붙인 PR 을 리뷰 받은 뒤에만 합칩니다.
CS 매니저가 만남권 하나를 열면 Claude 가 쓴 환불·환불 불가 안내문 초안이 뜨고, 컨시어지에게는 평가가 엇갈린 두 회원에게 보낼 확인 질문 초안이 뜹니다. 대표에게는 판정별 장수와 환불 금액, 지난주 대비 증감을 담은 주간 보고가 나오고, 평가 없음 회원이 앱에서 바로 답하는 한 화면을 Expo 로 만들어 평가가 들어오면 판정이 다시 계산되는 흐름을 보입니다.
CS 매니저가 세 파일을 올리면 T-30412 와 T-30433 이 상대 취소 환불 묶음에 99,000원·89,100원으로 서고, 화면 위 이번 주 환불 예정 금액이 188,100원으로 나옵니다.
컨시어지가 엇갈림 묶음에서 T-30501 을 펼쳐 만남권을 낸 회원은 만났다고, 상대는 상대가 안 나왔다고 남긴 두 평가와 약속 시각을 읽고 누구에게 먼저 연락할지 정합니다.
약속 두 시간 전 상대 취소로 평가 없음에도 걸리는 T-30433, 취소했다가 다시 만난 T-30468, 정확히 48시간인 T-30527 과 30분 넘긴 T-30533, 약속 이벤트가 0건인 T-30560 을 한 파일에 넣어도 만남권마다 판정이 하나씩 서고, 칸이 빠지거나 시각 형식이 깨진 줄은 줄 번호를 알린 뒤 나머지 만남권만 판정합니다.
2주차에는 만남권 400장의 판정과 환불 금액을 SQL 로 다시 계산해 TypeScript 결과와 다른 장 수를 띄웁니다. 다른 장은 만남권 번호와 두 판정을 나란히 띄워 어느 규칙에서 갈렸는지 읽습니다.
위 수치는 더미데이터 자가채점이며, 실제로 환불이 늦거나 빠진 만남권이 얼마나 줄었는지는 W클럽의 약속 기록과 CS 환불 문의 이력이 있어야 측정됩니다. 세 표가 만남권 번호·이벤트·평가 몇 칸만 받게 해 두어, W클럽 어드민에서 한 주치 약속 기록과 평가를 내려받으면 그 주 환불 예정 금액이 곧바로 나옵니다.
만남권 14장과 400장의 기대 판정은 제가 적었고, 두 구현이 같은 답을 내는 것은 규칙이 제 설계대로 돈다는 뜻입니다. 면접에서는 실제 환불 분쟁을 줄였다고 말하지 않고, 규칙과 테스트를 PR 리뷰에 올린 방식을 설명합니다.
마지막 취소 이벤트만 읽는 구현은 취소했다 다시 만난 T-30468 을 본인 취소로 올리고, 48시간을 '이상'으로 비교하는 구현은 정확히 48.0시간인 T-30527 을 평가 없음으로 넘깁니다. 취소보다 평가를 먼저 따지면 T-30433 의 89,100원 환불이 평가 없음에 묻힙니다. 세 만남권 중 하나라도 기대 판정과 다르게 나오면 그 주 환불 예정 금액이 어긋나므로, CS 매니저가 안내를 보내기 전에 Vitest 가 먼저 멈춥니다.
48시간 기준과 '두 회원 모두 안 만났다' 를 엇갈림으로 보는 해석은 제가 정한 값입니다. 실제 운영에서 평가를 며칠 기다리는지, 첫 만남 뒤 만족하지 못한 회원의 환불을 어떻게 정하는지는 대표가 정할 일이라, 기준값과 차례만 바꾸면 되게 규칙 한 곳에 모았습니다.
환불 금액을 낸 금액으로만 계산해 쿠폰 금액을 어떻게 되돌릴지, 결제 취소를 실제로 실행할지는 다루지 않습니다. PG 환불 실행은 가맹점 계정이 필요해 이 편은 환불할 만남권과 금액을 정하는 데서 멈춥니다.
이 편은 이벤트 기록을 접어 환불 여부를 정하는 백엔드 규칙과 관리자 화면을 보여주며, 매칭 서비스 환불 약관이나 소비자 분쟁 조정 기준에 대한 전문성은 주장하지 않습니다.
occurred_at 이 같은 초에 찍힌 확정과 취소가 있으면 어느 쪽이 마지막인지 기록만으로 정해지지 않습니다. 실제 이벤트 표에 순번 칸이 있는지 먼저 보고, 없으면 그 만남권은 CS 매니저에게 따로 띄워야 합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
W클럽 회원이 낸 만남권 결제 기록입니다. 만남권 하나는 컨시어지가 맺어 준 상대 한 명과의 약속 하나에 쓰이고, 쿠폰을 써서 89,100원을 낸 만남권과 결제만 하고 아직 약속이 잡히지 않은 만남권을 섞었습니다.
| ticket_id | member_id | partner_id | price | paid_at |
|---|---|---|---|---|
| T-30412 | M-41207 | F-52318 | 99,000 | 2026-09-20T21:05:00+09:00 |
| T-30433 | M-41355 | F-52540 | 89,100 | 2026-09-19T13:22:00+09:00 |
| T-30455 | M-41410 | F-52602 | 99,000 | 2026-09-21T09:48:00+09:00 |
| T-30468 | M-41502 | F-52677 | 99,000 | 2026-09-18T22:40:00+09:00 |
| T-30501 | M-41588 | F-52731 | 99,000 | 2026-09-19T20:15:00+09:00 |
아래 JSON Schema 에 맞는 W클럽 만남권 결제 행을 더 만들어 주세요. {"type":"object","properties":{"ticket_id":{"type":"string","description":"만남권 번호입니다. 컨시어지가 맺어 준 두 회원의 약속 하나에 만남권 하나가 쓰입니다."},"member_id":{"type":"string","description":"만남권을 낸 회원입니다. 남성 회원이 만남권을 냅니다."},"partner_id":{"type":"string","description":"약속 상대 회원입니다."},"price":{"type":"integer","minimum":0,"description":"실제로 낸 금액(원)입니다. 정가는 99000 이고 쿠폰을 쓰면 줄어듭니다."},"paid_at":{"type":"string","description":"+09:00 오프셋이 붙은 ISO 8601 결제 시각입니다."}},"required":["ticket_id","member_id","partner_id","price","paid_at"]} member_id 는 M-, partner_id 는 F- 로 시작하고, 정가는 99000 이며 열에 한 장꼴로 10% 쿠폰을 쓴 89100 을 둡니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 결제한 지 하루가 안 돼 약속 이벤트가 0건인 만남권, (2) 같은 회원이 같은 주에 다른 상대로 만남권을 두 장 낸 경우, (3) 쿠폰 금액으로 낸 만남권이 상대 취소로 끝나 환불 금액이 정가와 달라지는 경우. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"ticket_id":{"type":"string","description":"만남권 번호입니다. 컨시어지가 맺어 준 두 회원의 약속 하나에 만남권 하나가 쓰입니다."},"member_id":{"type":"string","description":"만남권을 낸 회원입니다. 남성 회원이 만남권을 냅니다."},"partner_id":{"type":"string","description":"약속 상대 회원입니다."},"price":{"type":"integer","minimum":0,"description":"실제로 낸 금액(원)입니다. 정가는 99000 이고 쿠폰을 쓰면 줄어듭니다."},"paid_at":{"type":"string","description":"+09:00 오프셋이 붙은 ISO 8601 결제 시각입니다."}},"required":["ticket_id","member_id","partner_id","price","paid_at"]}만남권마다 약속이 확정되고 취소된 기록을 시각순으로 쌓은 표입니다. 취소했다가 같은 상대와 다시 확정한 만남권, 상대가 한 번 취소한 뒤 다시 잡은 약속을 회원이 취소한 만남권, 일정을 바꿔 다시 확정한 만남권을 넣었습니다.
| ticket_id | event | actor_id | meet_at | occurred_at |
|---|---|---|---|---|
| T-30412 | confirmed | F-52318 | 2026-09-26T19:00:00+09:00 | 2026-09-21T20:12:00+09:00 |
| T-30412 | cancelled | F-52318 | — | 2026-09-25T22:10:00+09:00 |
| T-30433 | confirmed | F-52540 | 2026-09-24T19:30:00+09:00 | 2026-09-20T18:05:00+09:00 |
| T-30433 | cancelled | F-52540 | — | 2026-09-24T17:40:00+09:00 |
| T-30455 | confirmed | F-52602 | 2026-09-27T14:00:00+09:00 | 2026-09-22T21:30:00+09:00 |
아래 JSON Schema 에 맞는 W클럽 약속 이벤트 행을 더 만들어 주세요. {"type":"object","properties":{"ticket_id":{"type":"string"},"event":{"type":"string","enum":["confirmed","cancelled"],"description":"confirmed 는 두 회원이 약속 시각을 확정했거나 일정을 바꿔 다시 확정한 것, cancelled 는 한쪽이 약속을 취소한 것입니다."},"actor_id":{"type":"string","description":"그 이벤트를 일으킨 회원입니다."},"meet_at":{"type":["string","null"],"description":"확정된 약속 시각입니다. cancelled 이벤트는 null 입니다."},"occurred_at":{"type":"string","description":"이벤트가 기록된 시각입니다. 같은 만남권의 이벤트는 이 시각 순서로 읽습니다."}},"required":["ticket_id","event","actor_id","meet_at","occurred_at"]} 만남권 하나에 confirmed 가 먼저 오고, 약속은 확정 사흘 안팎 뒤 저녁에 잡히며, cancelled 이벤트의 meet_at 은 null 입니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 약속 두 시간 전 상대 회원의 취소, (2) 회원이 취소한 뒤 같은 상대와 다시 confirmed 된 만남권, (3) 상대가 먼저 취소하고 다시 잡은 약속을 회원이 취소한 만남권, (4) 취소 없이 일정만 바꿔 confirmed 가 두 번 있는 만남권, (5) 판정 기준 시각에서 정확히 48시간 전에 잡힌 약속. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"ticket_id":{"type":"string"},"event":{"type":"string","enum":["confirmed","cancelled"],"description":"confirmed 는 두 회원이 약속 시각을 확정했거나 일정을 바꿔 다시 확정한 것, cancelled 는 한쪽이 약속을 취소한 것입니다."},"actor_id":{"type":"string","description":"그 이벤트를 일으킨 회원입니다."},"meet_at":{"type":["string","null"],"description":"확정된 약속 시각입니다. cancelled 이벤트는 null 입니다."},"occurred_at":{"type":"string","description":"이벤트가 기록된 시각입니다. 같은 만남권의 이벤트는 이 시각 순서로 읽습니다."}},"required":["ticket_id","event","actor_id","meet_at","occurred_at"]}만남 뒤 두 회원이 앱에서 남긴 평가 중 '상대를 만났나요' 답만 모은 표입니다. 한쪽은 만났다고, 다른 쪽은 상대가 안 나왔다고 한 만남권, 둘 다 안 만났다고 한 만남권, 한쪽만 남기거나 둘 다 남기지 않은 만남권을 넣었습니다.
| ticket_id | reviewer_id | met | submitted_at |
|---|---|---|---|
| T-30468 | M-41502 | true | 2026-09-25T23:10:00+09:00 |
| T-30468 | F-52677 | true | 2026-09-26T09:02:00+09:00 |
| T-30501 | M-41588 | true | 2026-09-23T22:40:00+09:00 |
| T-30501 | F-52731 | false | 2026-09-24T10:05:00+09:00 |
| T-30519 | M-41620 | true | 2026-09-24T00:15:00+09:00 |
아래 JSON Schema 에 맞는 W클럽 만남 후 평가 행을 더 만들어 주세요. {"type":"object","properties":{"ticket_id":{"type":"string"},"reviewer_id":{"type":"string","description":"평가를 남긴 회원입니다. 만남권 하나에 두 회원이 한 건씩 남깁니다."},"met":{"type":"boolean","description":"만남 후 평가의 첫 질문 '상대를 만났나요' 에 대한 답입니다. false 는 상대가 나오지 않았다는 뜻입니다."},"submitted_at":{"type":"string","description":"+09:00 오프셋이 붙은 ISO 8601 시각입니다."}},"required":["ticket_id","reviewer_id","met","submitted_at"]} 평가는 약속 시각 몇 시간 뒤부터 다음 날 오전 사이에 들어오고, 만남권 하나에 두 회원이 한 건씩 남깁니다. 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 한쪽 met=true, 다른 쪽 met=false, (2) 두 회원 모두 met=false, (3) 한쪽만 평가를 남긴 만남권, (4) 평가가 0건인 만남권, (5) 약속 시각 48시간을 몇 분 넘겨 들어온 평가. 생성한 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"ticket_id":{"type":"string"},"reviewer_id":{"type":"string","description":"평가를 남긴 회원입니다. 만남권 하나에 두 회원이 한 건씩 남깁니다."},"met":{"type":"boolean","description":"만남 후 평가의 첫 질문 '상대를 만났나요' 에 대한 답입니다. false 는 상대가 나오지 않았다는 뜻입니다."},"submitted_at":{"type":"string","description":"+09:00 오프셋이 붙은 ISO 8601 시각입니다."}},"required":["ticket_id","reviewer_id","met","submitted_at"]}| TypeScript | 이벤트 시각순 정렬, 마지막 확정 시각 고르기, 48시간 계산, 판정 차례를 담은 규칙 모듈과 화면 코드의 언어입니다. |
| Node.js | 세 파일을 읽어 만남권 번호로 잇고 판정을 계산하는 작업이 도는 런타임입니다. |
| NestJS | 파일 업로드 API, 환불 판정 서비스, 3주차 안내문 초안과 주간 보고 생성을 모듈로 나눠 둡니다. |
| Next.js | CS 매니저·컨시어지·대표가 같이 보는 판정별 만남권 목록과 만남권 펼침 화면입니다. |
| SQL | 2주차에 ROW_NUMBER 로 만남권별 마지막 이벤트와 마지막 확정을 고르는 쿼리로 판정을 한 번 더 계산하고, 대표 보고의 주간 환불 금액도 이 쿼리로 뽑습니다. |
| React Native | 3주차에 평가가 비어 있는 회원이 '상대를 만났나요' 에 바로 답하는 한 화면을 Expo 로 만듭니다. |
| Android OS·iOS | 3주차 평가 화면을 Android 에뮬레이터와 iOS 시뮬레이터에서 띄워 두 화면을 캡처로 남깁니다. |
| AI/인공지능 | 3주차에 Claude 가 판정과 취소 기록을 받아 회원 안내문과 컨시어지 확인 질문 초안을 씁니다. |
| Claude Code·Codex | 판정 규칙과 화면의 초안을 맡기고, 48시간 정각·취소 뒤 재확정·두 판정 동시 해당 테스트와 SQL 재계산을 통과한 코드만 합칩니다. |
| A/B 테스트 | 3주차 평가 화면의 질문 문구 두 가지를 회원 번호로 나눠 배정하는 코드와, 문구별 평가 없음 비율을 내는 SQL 을 둡니다. 효과 측정은 실제 회원이 있어야 하므로 하지 않습니다. |
✗ fail → T-30468 이 본인 취소로 뜨면 마지막 취소 이벤트가 아니라 마지막 이벤트를 읽는지부터 보고, T-30527 이 평가 없음으로 뜨면 48시간 비교가 '이상' 으로 들어갔는지 봅니다.
✓ pass → CS 매니저가 이 화면만으로 이번 주 환불할 만남권을 고르고 대표에게 환불 예정 금액을 넘길 수 있으면 1주차로 충분합니다. 이어간다면 SQL 재계산과 PR 리뷰 흐름이 2주차 일입니다.
✗ fail → 두 계산이 다른 만남권은 같은 occurred_at 을 가진 이벤트 두 줄의 순서를 어떻게 정했는지부터 맞추고, 안내문 초안은 미룹니다.
✓ pass → 회원 안내문 초안과 대표 주간 환불 보고로 3주차를 엽니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · ticket_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
상대가 약속을 취소해 환불상대 취소 · 환불만남 평가가 서로 엇갈림평가 엇갈림약속 날이 지났는데 평가가 없음평가 없음본인이 취소해 환불 안 됨본인 취소만남 성사만남 성사만남권 한 장이 약속 이벤트 정렬, 마지막 취소자 대조, 48시간 계산, 두 평가 비교를 거쳐 어떤 판정과 환불 금액을 받는지 남기는 형식입니다. 판정과 금액은 규칙이 채우고, 3주차 안내문 초안은 이 형식을 입력으로 받습니다.
{"type":"object","properties":{"ticket_id":{"type":"string"},"member_id":{"type":"string"},"partner_id":{"type":"string"},"verdict":{"type":"string","enum":["만남 성사","상대가 약속을 취소해 환불","본인이 취소해 환불 안 됨","만남 평가가 서로 엇갈림","약속 날이 지났는데 평가가 없음"]},"as_of":{"type":"string","description":"판정 기준 시각입니다."},"price":{"type":"integer","minimum":0},"refund_amount":{"type":["integer","null"],"minimum":0,"description":"상대가 약속을 취소해 환불이면 meeting_tickets.price, 본인이 취소해 환불 안 됨과 만남 성사는 0 입니다. 평가가 엇갈리거나 평가가 없으면 컨시어지 확인 전이라 null 입니다."},"last_event":{"type":"string","enum":["confirmed","cancelled"],"description":"occurred_at 순서로 읽은 마지막 이벤트입니다."},"last_actor_id":{"type":"string"},"last_meet_at":{"type":["string","null"],"description":"마지막 confirmed 이벤트의 meet_at 입니다."},"hours_since_meet":{"type":["number","null"],"description":"as_of 에서 last_meet_at 을 뺀 시간(소수 첫째 자리)입니다. last_meet_at 이 null 이면 null 입니다."},"missing_reviewers":{"type":"array","items":{"type":"string"},"description":"member_id·partner_id 중 meet_reviews.reviewer_id 에 없는 회원입니다."},"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":["ticket_id","member_id","partner_id","verdict","as_of","price","refund_amount","last_event","last_actor_id","last_meet_at","hours_since_meet","missing_reviewers","evidence","summary"]}[
{
"ticket_id": "T-30412",
"member_id": "M-41207",
"partner_id": "F-52318",
"verdict": "상대가 약속을 취소해 환불",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": 99000,
"last_event": "cancelled",
"last_actor_id": "F-52318",
"last_meet_at": "2026-09-26T19:00:00+09:00",
"hours_since_meet": 39,
"missing_reviewers": [
"M-41207",
"F-52318"
],
"evidence": [
{
"field": "event",
"value": "cancelled"
},
{
"field": "actor_id",
"value": "F-52318"
},
{
"field": "partner_id",
"value": "F-52318"
},
{
"field": "price",
"value": 99000
}
],
"summary": "9월 26일 저녁 약속을 상대 회원이 하루 전 밤 10시 10분에 취소해, 만남보장에 따라 낸 금액 99,000원을 환불합니다.",
"hedge": "약속 시각에서 39시간이 지났지만 취소된 약속이라 평가가 비어 있어도 평가 없음으로 보지 않습니다."
},
{
"ticket_id": "T-30433",
"member_id": "M-41355",
"partner_id": "F-52540",
"verdict": "상대가 약속을 취소해 환불",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 89100,
"refund_amount": 89100,
"last_event": "cancelled",
"last_actor_id": "F-52540",
"last_meet_at": "2026-09-24T19:30:00+09:00",
"hours_since_meet": 86.5,
"missing_reviewers": [
"M-41355",
"F-52540"
],
"evidence": [
{
"field": "event",
"value": "cancelled"
},
{
"field": "actor_id",
"value": "F-52540"
},
{
"field": "meet_at",
"value": "2026-09-24T19:30:00+09:00"
},
{
"field": "price",
"value": 89100
},
{
"field": "reviewer_id",
"agg": "count",
"value": 0
}
],
"summary": "9월 24일 저녁 7시 반 약속을 상대 회원이 두 시간 전에 취소해, 쿠폰을 쓰고 낸 89,100원을 환불합니다.",
"hedge": "약속 시각에서 86.5시간이 지났고 평가도 0건이라 평가 없음 조건에도 걸리지만, 취소 판정이 먼저입니다. 환불 금액은 정가가 아니라 실제로 낸 금액입니다."
},
{
"ticket_id": "T-30455",
"member_id": "M-41410",
"partner_id": "F-52602",
"verdict": "본인이 취소해 환불 안 됨",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": 0,
"last_event": "cancelled",
"last_actor_id": "M-41410",
"last_meet_at": "2026-09-27T14:00:00+09:00",
"hours_since_meet": 20,
"missing_reviewers": [
"M-41410",
"F-52602"
],
"evidence": [
{
"field": "event",
"value": "cancelled"
},
{
"field": "actor_id",
"value": "M-41410"
},
{
"field": "member_id",
"value": "M-41410"
}
],
"summary": "만남권을 낸 회원이 9월 25일 아침에 27일 약속을 직접 취소해 만남보장 환불 대상이 아닙니다.",
"hedge": "환불 문의가 들어오면 CS 매니저가 취소 시각과 취소한 쪽을 근거로 안내합니다."
},
{
"ticket_id": "T-30468",
"member_id": "M-41502",
"partner_id": "F-52677",
"verdict": "만남 성사",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": 0,
"last_event": "confirmed",
"last_actor_id": "F-52677",
"last_meet_at": "2026-09-25T19:00:00+09:00",
"hours_since_meet": 63,
"missing_reviewers": [],
"evidence": [
{
"field": "event",
"value": "confirmed"
},
{
"field": "meet_at",
"value": "2026-09-25T19:00:00+09:00"
},
{
"field": "met",
"agg": "count",
"value": 2
}
],
"summary": "9월 21일에 회원이 한 번 취소했지만 다음 날 25일 저녁으로 다시 확정했고, 두 회원 모두 만났다고 평가했습니다.",
"hedge": "취소 이벤트가 기록에 남아 있어도 마지막 이벤트가 확정이라 본인 취소로 보지 않습니다."
},
{
"ticket_id": "T-30501",
"member_id": "M-41588",
"partner_id": "F-52731",
"verdict": "만남 평가가 서로 엇갈림",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": null,
"last_event": "confirmed",
"last_actor_id": "F-52731",
"last_meet_at": "2026-09-23T19:00:00+09:00",
"hours_since_meet": 111,
"missing_reviewers": [],
"evidence": [
{
"field": "reviewer_id",
"value": "M-41588"
},
{
"field": "met",
"value": true
},
{
"field": "reviewer_id",
"value": "F-52731"
},
{
"field": "met",
"value": false
}
],
"summary": "9월 23일 약속에 대해 만남권을 낸 회원은 만났다고, 상대 회원은 상대가 나오지 않았다고 평가했습니다.",
"hedge": "누가 맞는지는 기록으로 정할 수 없어 컨시어지가 두 회원에게 확인하기 전까지 환불 금액을 비워 둡니다."
},
{
"ticket_id": "T-30519",
"member_id": "M-41620",
"partner_id": "F-52809",
"verdict": "약속 날이 지났는데 평가가 없음",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": null,
"last_event": "confirmed",
"last_actor_id": "F-52809",
"last_meet_at": "2026-09-23T19:00:00+09:00",
"hours_since_meet": 111,
"missing_reviewers": [
"F-52809"
],
"evidence": [
{
"field": "meet_at",
"value": "2026-09-23T19:00:00+09:00"
},
{
"field": "reviewer_id",
"value": "M-41620"
},
{
"field": "reviewer_id",
"agg": "count",
"value": 1
}
],
"summary": "9월 23일 저녁 약속에서 111시간이 지났는데 상대 회원 F-52809 의 평가가 아직 없습니다.",
"hedge": "만남권을 낸 회원은 만났다고 남겼지만 한쪽 평가만으로는 성사로 닫지 않습니다."
},
{
"ticket_id": "T-30533",
"member_id": "M-41790",
"partner_id": "F-52901",
"verdict": "약속 날이 지났는데 평가가 없음",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": null,
"last_event": "confirmed",
"last_actor_id": "F-52901",
"last_meet_at": "2026-09-26T09:30:00+09:00",
"hours_since_meet": 48.5,
"missing_reviewers": [
"M-41790",
"F-52901"
],
"evidence": [
{
"field": "meet_at",
"value": "2026-09-26T09:30:00+09:00"
},
{
"field": "reviewer_id",
"agg": "count",
"value": 0
}
],
"summary": "9월 26일 아침 9시 반 약속에서 48.5시간이 지났는데 두 회원 모두 평가를 남기지 않았습니다.",
"hedge": "기준 48시간을 30분 넘긴 만남권입니다. 30분 늦은 T-30527 은 정확히 48시간이라 아직 판정하지 않습니다."
},
{
"ticket_id": "T-30540",
"member_id": "M-41815",
"partner_id": "F-52944",
"verdict": "만남 성사",
"as_of": "2026-09-28T10:00:00+09:00",
"price": 99000,
"refund_amount": 0,
"last_event": "confirmed",
"last_actor_id": "F-52944",
"last_meet_at": "2026-09-21T18:00:00+09:00",
"hours_since_meet": 160,
"missing_reviewers": [],
"evidence": [
{
"field": "event",
"value": "confirmed"
},
{
"field": "met",
"value": true
},
{
"field": "met",
"agg": "count",
"value": 2
}
],
"summary": "9월 21일 저녁 약속 뒤 두 회원이 모두 만났다고 평가해 만남권이 쓰였습니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 만남 성사·상대가 약속을 취소해 환불·본인이 취소해 환불 안 됨·만남 평가가 서로 엇갈림·약속 날이 지났는데 평가가 없음 다섯 값 중 하나인지 확인합니다. 2. ticket_id·verdict·as_of·price·refund_amount·last_event·last_meet_at·hours_since_meet·missing_reviewers·evidence·summary 필드가 빠짐없이 존재하는지 확인합니다. 3. price 와 refund_amount 가 0 이상이고 hours_since_meet 가 0 이상인지 확인합니다. 4. hours_since_meet 를 as_of 에서 last_meet_at 을 뺀 시간으로 다시 계산하고, last_meet_at 은 그 ticket_id 의 confirmed 이벤트 중 occurred_at 이 가장 늦은 줄의 meet_at 으로 다시 고릅니다. 5. 마지막 이벤트가 cancelled 이고 actor_id 가 partner_id 와 같으면 verdict 는 상대가 약속을 취소해 환불이고 refund_amount 는 price 입니다. 6. 마지막 이벤트가 cancelled 이고 actor_id 가 member_id 와 같으면 verdict 는 본인이 취소해 환불 안 됨이고 refund_amount 는 0 입니다. 7. 앞 조건에 걸리지 않고 meet_reviews 두 건의 met 가 서로 다르거나 둘 다 false 이면 verdict 는 만남 평가가 서로 엇갈림이고 refund_amount 는 null 입니다. 8. 앞 조건에 걸리지 않고 hours_since_meet 가 48 을 넘으며 missing_reviewers 가 비어 있지 않으면 verdict 는 약속 날이 지났는데 평가가 없음이고 refund_amount 는 null 입니다. 9. 앞 조건에 걸리지 않고 두 회원의 met 가 모두 true 이면 verdict 는 만남 성사이고 refund_amount 는 0 입니다. 10. evidence 의 field 이름이 meeting_tickets·appointment_events·meet_reviews 중 하나에 있는 컬럼인지 대조합니다. 11. evidence 의 value 가 수록된 행의 원문 값과 같은지 확인합니다. 여러 줄을 센 수는 agg 를 적습니다. 12. summary 가 존댓말로 끝나는지 확인합니다. [판정 차례 · 두 판정이 함께 걸릴 때] 만남권 하나에는 판정을 하나만 남깁니다. ① 상대가 약속을 취소해 환불 → ② 본인이 취소해 환불 안 됨 → ③ 만남 평가가 서로 엇갈림 → ④ 약속 날이 지났는데 평가가 없음 → ⑤ 만남 성사. T-30433 은 상대 취소와 평가 없음(약속 뒤 86.5시간, 평가 0건)에 함께 걸리고 ①이 남습니다. 취소된 약속은 만날 일이 없으니 평가를 기다리지 않습니다. [판정 대상 · 목록에 올리지 않는 만남권] 다음 둘은 판정 목록에 올리지 않고 화면 위 집계에만 셉니다. 1. 약속 이벤트가 0건인 만남권은 약속을 잡기 전입니다. T-30560 은 9월 27일 밤에 결제해 아직 컨시어지가 약속을 잡지 않았고, paid_at 부터 센 대기 시간만 집계에 보입니다. 2. 마지막 약속 시각에서 48시간이 넘지 않았고 평가가 한쪽만 있거나 0건인 만남권은 평가를 기다리는 중입니다. T-30527 은 9월 26일 오전 10시 약속이라 as_of 에 정확히 48.0시간이 되며, 48시간을 '넘었는지' 묻는 규칙이라 아직 판정하지 않습니다. 판정 대상이 0장인 주(업로드한 만남권이 모두 위 둘에 해당하거나 파일이 비어 있는 주)는 다섯 묶음을 0으로 두고 환불 예정 금액을 0원으로 보여줍니다. [하드 케이스 · 판정이 갈리는 이유] T-30468 은 회원이 한 번 취소했다가 같은 상대와 다시 확정하고 만났습니다. 마지막 이벤트가 confirmed 라 본인 취소가 아닙니다. T-30563 은 상대가 먼저 한 번 취소했지만 다시 잡은 약속을 회원이 취소해 마지막 취소의 actor_id 가 회원이므로 본인 취소입니다. T-30546 은 일정을 9월 24일로 바꿔 다시 확정한 만남권이라 48시간을 처음 약속(9월 20일)이 아니라 마지막 확정 시각부터 셉니다. T-30558 은 두 회원이 모두 met=false 로 남겼습니다(판정은 엇갈림, 아래 '확정 카드 판정식과 다른 점' 2). T-30563·T-30546·T-30558 의 기대 출력은 위 규칙 5~9 로 재계산되므로 samples 에 따로 싣지 않습니다. [확정 카드 판정식과 다른 점] 1. 카드의 본인 취소 식은 '마지막 cancelled 이벤트의 actor_id' 만 봅니다. 그대로 쓰면 취소 뒤 다시 확정해 만난 T-30468 도 본인 취소가 됩니다. 이 편은 상대 취소 식과 같게 '그 만남권의 마지막 이벤트가 cancelled 일 때' 로 읽습니다. 2. 카드의 엇갈림 식은 '한쪽 met 는 참, 다른 쪽은 거짓' 입니다. 두 회원이 모두 met=false 로 남긴 경우는 카드 다섯 값 어디에도 들지 않아, 약속 기록(확정)과 두 평가가 엇갈린 것으로 보고 같은 판정에 넣습니다. 누가 안 나왔는지 컨시어지가 두 회원에게 확인해야 하는 점이 같습니다. 3. 카드는 만남 성사의 식을 적지 않았습니다. 이 편은 '취소로 끝나지 않았고 두 회원의 met 가 모두 true' 로 정합니다. 그래서 48시간이 지나지 않았고 평가가 모자란 만남권은 성사가 아니라 판정 대상 밖(평가를 기다리는 중)입니다. [출처 · 회사 용어] 만남권·환불·만남 후 평가·컨시어지·99,000원은 채용공고가 아니라 회사 홈페이지(https://www.wclub.co.kr, 2026-10-06 확인)에서 가져왔습니다. 홈페이지는 '못만나면 100% 환불', 남성 회원 만남권 99,000원, 상대 취소와 첫 만남 뒤 만족하지 못한 경우 환불, 만남 뒤 서로 평가, 컨시어지 심사, 일방 취소 페널티를 밝힙니다. 공고에서 온 말은 '만남보장 정책으로 약속부터 만남까지 책임집니다' 한 문장입니다. 48시간 기준과 쿠폰 금액 89,100원은 판정이 도는지 보이려고 정한 값이며 W클럽 정책이 아닙니다. [평가 러너 · 2주차] 같은 판정을 SQL(ROW_NUMBER 로 만남권별 마지막 이벤트와 마지막 확정을 고르는 쿼리)로 한 번 더 짜서 더미 만남권 400장의 판정과 환불 금액을 TypeScript 판정과 장마다 맞춰 보고, 다른 장은 따로 모읍니다. 판정 차례·48시간 정각·취소 뒤 재확정 같은 하드 케이스는 제가 적은 기대 판정을 Vitest 로 고정합니다. [수동 검증 · 사람이 봅니다] 1. 엇갈림과 평가 없음 만남권의 환불 여부는 컨시어지가 두 회원에게 확인한 뒤 CS 매니저가 정합니다. 2. 48시간 기준이 W클럽 운영과 맞는지는 대표가 정합니다.