모드하우스
경력근거: 앱 하나에 디지털 포토카드의 발행과 거래, 팬 투표, 온체인 지갑, 전 세계의 다양한 수단의 결제가 함께 들어있습니다
공고는 앱 하나에 디지털 포토카드의 발행과 거래, 팬 투표, 온체인 지갑, 전 세계의 다양한 수단의 결제가 함께 들어 있다고 적고, 그래서 기능 하나에 서버와 앱과 컨트랙트가 같이 걸린다고 밝힙니다. 이 네 가지가 한 화면에서 만나는 계산이 바로 표 집계입니다. tripleS 는 타이틀곡과 유닛 구성을 팬 투표로 정하고, COSMO 유저의 70%가 해외라 결제 취소도 통화별로 흩어져 들어옵니다. 다른 회사 데이터로는 포토카드 소유와 다국가 결제와 투표 상한이 한 표 위에서 겹치는 이 사슬을 만들지 못합니다. 다만 이 포트폴리오는 시각을 축으로 소유와 결제를 되짚어 판정을 만드는 서버 구현을 보여줍니다. 온체인 지갑 운영이나 EVM 컨트랙트 감사의 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고는 백엔드와 프런트엔드를 나눠 뽑던 방식을 접고 Product Engineer 한 자리로 합쳤다고 밝힙니다. 기능 하나에 서버와 앱, 때로는 컨트랙트가 같이 걸리니 어디까지 자기 손으로 끝낼지가 그대로 업무 범위가 됩니다.
컴백과 투표, 신규 발행 시점에 전 세계 요청이 동시에 들어오고 결과도 즉시 보여야 한다고 적혀 있습니다. 회차 마감처럼 시각이 걸린 계산은 몰리는 순간에 틀리기 쉬운 자리입니다.
언어와 결제 수단, 정책이 다른 국가를 같은 코드베이스에서 지원한다고 밝히면서 EVM 기반 스마트 컨트랙트 개발도 주요업무에 둡니다. 결제와 온체인 기록이 같은 기능 안에서 만나는 구조입니다.
회차를 마감하기 전에 접수된 표를 하나씩 오브젝트 소유 기록과 결제 기록까지 거슬러 올라가, 집계에 넣을 표와 뺄 표를 사유별로 세워 줍니다. 빼는 판단은 지갑 이전 시각을 견주고 상한을 넘은 표 수를 세는 규칙이 내리고, 클로드는 그 표가 지갑 때문에 빠졌는지 결제 때문에 빠졌는지를 팬 공지에 그대로 옮길 한 문장으로 바꿉니다.
tripleS·ARTMS·idntt 투표 회차를 열고 결과를 발표하는 COSMO 운영 담당자 — 포토카드가 지갑 사이를 오가고 해외 결제가 취소되는 사이에, 이 표를 집계에 넣어도 되는지 팬에게 근거를 대야 한다
회차 마감이 조마조마한 이유는 표가 많아서가 아니라, 표 하나가 걸린 이유가 지갑인지 결제인지 표 수인지 갈라져 있지 않아서입니다. 접수 시각을 축으로 소유와 결제를 되짚고 사유를 네 갈래로 세우면, 담당자가 사유마다 처리하고 발표로 넘어갑니다.
운영 담당자가 회차 마감 직전에 여는 화면 하나가 돕니다. 회차를 고르면 접수된 표가 유효와 지갑, 결제, 표 수 초과 네 갈래로 갈라지고 표마다 근거 값이 뜹니다. 오브젝트 소유와 결제 기록은 사내 API 가 준 스냅샷을 읽는 것으로 시작하고, 체인에서 직접 읽어 오는 일은 3주차로 미룹니다.
정답을 붙인 표 60건으로 판정이 일치하는지 자동으로 맞춰 보고, 어긋난 유형을 회차 리포트로 냅니다. 2주차부터 AWS EKS 로 배포해 담당자가 사내 주소로 들어와 씁니다.
Viem 으로 오브젝트 이전 기록을 체인에서 읽어 사내 스냅샷과 견주고, 마감 직전에 판정이 뒤집힌 표를 화면 맨 위에 세웁니다.
운영 담당자가 회차를 고르면 접수된 표가 유효와 지갑, 결제, 표 수 초과 네 갈래로 갈라지고, 빠진 표마다 어느 값이 근거인지 그 자리에서 읽습니다.
결제 상태가 비어 있거나 이전 기록이 접수 시각과 몇 초 차이로 붙은 입력 30종을 넣어도 화면이 죽지 않고, 판정을 못 내린 표는 보류로 남습니다.
회차를 마감하면 유효 표와 제외 표의 합이 접수 총수와 맞아떨어집니다.
정답을 붙인 표 60건으로 판정 일치율을 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 판정 적중률은 모드하우스의 실제 GRAVITY 접수 기록과 결제 원장이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 결측·악성·경계 케이스를 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
표가 빠졌다는 판정이 곧 부정 투표를 뜻하지는 않습니다. 선물이나 거래로 오브젝트를 넘기다 시각이 몇 분 어긋나기도 하므로 근거 값을 항상 같이 보여 마지막은 사람이 정합니다.
판정 이름 지갑에 없는 오브젝트는 지갑이 바뀐 표와 다른 아티스트 오브젝트로 던진 표를 함께 담습니다. 오브젝트가 제 지갑에 그대로 있는 표도 이 이름으로 빠지므로, 화면과 팬 공지에는 이름 대신 요약에 적은 사유를 씁니다. 사유를 쪼개 이름을 따로 두는 일은 발표 문구를 확정하는 도입 단계 과제로 미룹니다.
결제와 환불을 실행하는 일은 가맹점 계약이 있어야 다룰 수 있어 이 포트폴리오의 범위 밖입니다. 여기서는 이미 끝난 결제 기록을 읽는 데까지만 다룹니다.
이 포트폴리오는 시각을 축으로 소유와 결제를 되짚는 서버 구현을 보여줍니다. 온체인 지갑 운영과 EVM 컨트랙트 감사의 전문성은 주장하지 않습니다.
실제 도입에서는 오브젝트 이전이 체인에서 일어나 블록이 확정된 시각과 서버가 받은 시각이 어긋납니다. 어느 시각을 마감 기준으로 삼을지는 도입 단계 과제로 분리합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
회차에 접수된 표 원장입니다. 상한을 넘겨 던진 표와 오브젝트를 넘긴 뒤에 던진 표를 일부러 섞어 두었습니다.
| 투표id | 유저id | 지갑주소 | 투표항목 | 사용오브젝트id | 접수시각 |
|---|---|---|---|---|---|
| V-8801 | U-4471 | 0x3a41c9 | ASSEMBLE25 타이틀곡 | OBJ-1101 | 2026-09-06T19:02:00 |
| V-8802 | U-4471 | 0x3a41c9 | ASSEMBLE25 타이틀곡 | OBJ-1102 | 2026-09-06T19:04:00 |
| V-8842 | U-4471 | 0x3a41c9 | ASSEMBLE25 타이틀곡 | OBJ-1177 | 2026-09-06T20:55:00 |
| V-9013 | U-4471 | 0x3a41c9 | ASSEMBLE25 타이틀곡 | OBJ-2204 | 2026-09-06T20:12:00 |
| V-9020 | U-5520 | 0x7c08e2 | Club Icarus 수록곡 | OBJ-3310 | 2026-09-05T18:40:00 |
아래 JSON Schema 에 맞는 투표 접수 행을 더 만들어 주세요. {"type":"object","properties":{"투표id":{"type":"string"},"유저id":{"type":"string"},"지갑주소":{"type":"string"},"투표항목":{"type":"string"},"사용오브젝트id":{"type":"string"},"접수시각":{"type":"string"}},"required":["투표id","유저id","지갑주소","투표항목","사용오브젝트id","접수시각"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 한 유저id 가 같은 투표항목에 1인최대표수보다 많이 던진 묶음, (2) 접수시각이 집계기준시각을 몇 분 앞뒤로 걸치는 표, (3) 대상 아티스트가 다른 오브젝트를 쓴 표, (4) 지갑주소가 오브젝트보유의 보유지갑주소와 어긋나는 표. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"투표id":{"type":"string"},"유저id":{"type":"string"},"지갑주소":{"type":"string"},"투표항목":{"type":"string"},"사용오브젝트id":{"type":"string"},"접수시각":{"type":"string"}},"required":["투표id","유저id","지갑주소","투표항목","사용오브젝트id","접수시각"]}오브젝트가 지금 어느 지갑에 있는지와 마지막으로 손이 바뀐 시각을 담습니다. 한 번도 옮기지 않은 오브젝트는 이전시각이 비어 있습니다.
| 오브젝트id | 보유지갑주소 | 아티스트 | 발행회차 | 이전시각 |
|---|---|---|---|---|
| OBJ-1101 | 0x3a41c9 | tripleS | 1 | — |
| OBJ-1102 | 0x3a41c9 | tripleS | 1 | — |
| OBJ-1177 | 0x9f22b5 | tripleS | 2 | 2026-09-06T20:41:00 |
| OBJ-2204 | 0x3a41c9 | tripleS | 2 | — |
| OBJ-3310 | 0x7c08e2 | ARTMS | 3 | — |
아래 JSON Schema 에 맞는 오브젝트 보유 행을 더 만들어 주세요. {"type":"object","properties":{"오브젝트id":{"type":"string"},"보유지갑주소":{"type":"string"},"아티스트":{"type":"string","enum":["tripleS","ARTMS","idntt"]},"발행회차":{"type":"integer"},"이전시각":{"type":["string","null"]}},"required":["오브젝트id","보유지갑주소","아티스트","발행회차"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 이전시각이 비어 있는 최초 보유 행, (2) 이전시각이 투표 접수 직전인 행과 직후인 행을 짝으로, (3) 같은 지갑이 한 회차 오브젝트를 여럿 들고 있는 묶음, (4) 아티스트가 tripleS·ARTMS·idntt 로 섞인 행. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"오브젝트id":{"type":"string"},"보유지갑주소":{"type":"string"},"아티스트":{"type":"string","enum":["tripleS","ARTMS","idntt"]},"발행회차":{"type":"integer"},"이전시각":{"type":["string","null"]}},"required":["오브젝트id","보유지갑주소","아티스트","발행회차"]}오브젝트를 살 때 남은 결제 기록입니다. 취소·환불로 적혔는데 취소시각이 비어 있는 건을 일부러 넣었습니다.
| 오브젝트id | 결제수단 | 결제통화 | 결제상태 | 취소시각 |
|---|---|---|---|---|
| OBJ-1101 | 카드 | KRW | 승인 | — |
| OBJ-1102 | 카드 | KRW | 승인 | — |
| OBJ-1177 | 애플 인앱 | USD | 승인 | — |
| OBJ-2204 | 카드 | EUR | 취소 | 2026-09-06T18:30:00 |
| OBJ-3310 | 구글 인앱 | USD | 승인 | — |
아래 JSON Schema 에 맞는 결제 행을 더 만들어 주세요. {"type":"object","properties":{"오브젝트id":{"type":"string"},"결제수단":{"type":"string","enum":["카드","구글 인앱","애플 인앱","페이팔"]},"결제통화":{"type":"string","enum":["KRW","USD","EUR","JPY"]},"결제상태":{"type":"string","enum":["승인","취소","환불"]},"취소시각":{"type":["string","null"]}},"required":["오브젝트id","결제수단","결제통화","결제상태"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 결제상태가 취소·환불인데 취소시각이 비어 있는 건, (2) 취소시각이 집계기준시각보다 뒤인 건, (3) 결제통화가 KRW·USD·EUR·JPY 로 갈리고 결제수단도 카드·인앱·페이팔로 갈리는 묶음, (4) 승인 뒤 환불이 이어진 같은 오브젝트의 흐름. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"오브젝트id":{"type":"string"},"결제수단":{"type":"string","enum":["카드","구글 인앱","애플 인앱","페이팔"]},"결제통화":{"type":"string","enum":["KRW","USD","EUR","JPY"]},"결제상태":{"type":"string","enum":["승인","취소","환불"]},"취소시각":{"type":["string","null"]}},"required":["오브젝트id","결제수단","결제통화","결제상태"]}회차마다 정한 대상 아티스트와 1인 최대 표 수, 집계 기준 시각을 담은 기준표입니다. 판정의 잣대는 전부 이 표에서 옵니다.
| 투표항목 | 대상아티스트 | 1인최대표수 | 집계기준시각 |
|---|---|---|---|
| ASSEMBLE25 타이틀곡 | tripleS | 3 | 2026-09-06T21:00:00 |
| ASSEMBLE25 유닛 구성 | tripleS | 5 | 2026-09-06T21:00:00 |
| Club Icarus 수록곡 | ARTMS | 3 | 2026-09-05T21:00:00 |
| unevermet 활동 콘셉트 | idntt | 2 | 2026-09-07T21:00:00 |
| 팬클럽 굿즈 디자인 | tripleS | 4 | 2026-09-08T21:00:00 |
아래 JSON Schema 에 맞는 투표 규칙 행을 더 만들어 주세요. {"type":"object","properties":{"투표항목":{"type":"string"},"대상아티스트":{"type":"string","enum":["tripleS","ARTMS","idntt"]},"1인최대표수":{"type":"integer"},"집계기준시각":{"type":"string"}},"required":["투표항목","대상아티스트","1인최대표수","집계기준시각"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 1인최대표수가 1 인 회차와 8 이상인 회차, (2) 같은 날 같은 시각에 마감되는 회차 둘, (3) 대상아티스트가 idntt 인 신규 회차, (4) 집계기준시각이 자정 직전인 회차. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"투표항목":{"type":"string"},"대상아티스트":{"type":"string","enum":["tripleS","ARTMS","idntt"]},"1인최대표수":{"type":"integer"},"집계기준시각":{"type":"string"}},"required":["투표항목","대상아티스트","1인최대표수","집계기준시각"]}| TypeScript | 판정 규칙과 화면을 같은 언어로 씁니다. 접수 시각과 이전 시각을 다루는 타입을 한 벌로 두어 서버와 화면이 같은 모양을 봅니다. |
| NestJS | 회차 집계 API 서버입니다. 지갑·결제·표 수 세 규칙을 모듈로 나눠 두고 판정 실행은 이쪽 한 곳에만 둡니다. |
| React | 운영 담당자가 보는 집계판입니다. 사유별 묶음과 근거 값을 한 화면에 둡니다. |
| React Native | COSMO 앱이 이 규격을 그대로 쓰도록, 판정 응답을 앱 투표 화면의 안내 문구로 옮기는 어댑터를 붙입니다. |
| MySQL | 투표 접수와 오브젝트 보유 스냅샷을 담는 저장소입니다. 접수시각과 이전시각에 복합 인덱스를 걸어 회차 단위 조회를 받칩니다. |
| PostgreSQL | 회차별 판정 결과와 감사 기록을 따로 담습니다. 발표 뒤에도 어느 표가 왜 빠졌는지 그대로 남습니다. |
| Redis | 마감 직전에 몰리는 조회를 회차 단위로 캐시하고, 판정이 다시 돌면 그 회차 키만 비웁니다. |
| AWS | 2주차부터 배포와 운영을 맡습니다. 회차 리포트와 스냅샷 원본을 S3 에 남깁니다. |
| EKS | 집계 API 와 판정 배치를 같은 클러스터에 올려, 마감 시각에 배치만 따로 늘립니다. |
| Viem | 3주차에 오브젝트 이전 기록을 체인에서 읽어 사내 스냅샷과 견줍니다. |
| Solidity | 이전 기록을 읽을 때 쓰는 오브젝트 컨트랙트의 이벤트 시그니처를 확인하는 데까지 다룹니다. |
| LLM | 사유가 정해진 뒤 그 표가 왜 빠졌는지를 팬 공지에 붙일 한 문장으로 옮깁니다. 빼고 말고는 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 판정이 실제로 맞았는지 정답을 붙인 표와 맞춰 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 오브젝트 이전 기록을 체인에서 직접 읽어 사내 스냅샷과 견주는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
판정사유 라벨 · 투표id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
유효 표유효지갑에 없는 오브젝트지갑·아티스트 불일치결제가 취소된 오브젝트결제 취소1인 표 수 초과표 수 초과접수된 표 하나를 집계에 넣을지 뺄지 사유와 근거 값으로 남기는 형식입니다. 판정은 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"투표id":{"type":"string"},"투표항목":{"type":"string"},"판정":{"type":"string","enum":["유효 표","지갑에 없는 오브젝트","결제가 취소된 오브젝트","1인 표 수 초과"]},"초과표수":{"type":["integer","null"],"description":"그 유저의 그 투표항목 접수 표 수에서 투표규칙.1인최대표수 를 뺀 값. 판정이 1인 표 수 초과일 때만 채우고 나머지는 비웁니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string","enum":["투표기록","오브젝트보유","결제내역","투표규칙"]},"field":{"type":"string"},"agg":{"type":["string","null"],"enum":["count","sum","avg","min","max",null]},"value":{"type":["string","integer","null"]}},"required":["source","field","value"]}},"요약":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["투표id","투표항목","판정","evidence","요약"]}[
{
"투표id": "V-8801",
"투표항목": "ASSEMBLE25 타이틀곡",
"판정": "유효 표",
"초과표수": null,
"evidence": [
{
"source": "투표기록",
"field": "사용오브젝트id",
"value": "OBJ-1101"
},
{
"source": "투표기록",
"field": "지갑주소",
"value": "0x3a41c9"
},
{
"source": "오브젝트보유",
"field": "보유지갑주소",
"value": "0x3a41c9"
},
{
"source": "결제내역",
"field": "결제상태",
"value": "승인"
}
],
"요약": "지갑에 그대로 있는 오브젝트로 던진 표라 집계에 넣습니다.",
"hedge": null
},
{
"투표id": "V-9021",
"투표항목": "Club Icarus 수록곡",
"판정": "유효 표",
"초과표수": null,
"evidence": [
{
"source": "투표기록",
"field": "사용오브젝트id",
"value": "OBJ-3311"
},
{
"source": "투표기록",
"field": "접수시각",
"value": "2026-09-05T19:05:00"
},
{
"source": "오브젝트보유",
"field": "이전시각",
"value": "2026-09-05T19:30:00"
}
],
"요약": "오브젝트가 접수 25분 뒤에 다른 지갑으로 넘어가, 던질 때는 본인 것이었습니다.",
"hedge": null
},
{
"투표id": "V-8842",
"투표항목": "ASSEMBLE25 타이틀곡",
"판정": "지갑에 없는 오브젝트",
"초과표수": null,
"evidence": [
{
"source": "투표기록",
"field": "지갑주소",
"value": "0x3a41c9"
},
{
"source": "오브젝트보유",
"field": "보유지갑주소",
"value": "0x9f22b5"
},
{
"source": "오브젝트보유",
"field": "이전시각",
"value": "2026-09-06T20:41:00"
},
{
"source": "투표기록",
"field": "접수시각",
"value": "2026-09-06T20:55:00"
}
],
"요약": "지갑이 바뀐 사유입니다. 접수 14분 전에 오브젝트가 다른 지갑으로 넘어가 이 표는 집계에서 뺍니다.",
"hedge": null
},
{
"투표id": "V-9088",
"투표항목": "ASSEMBLE25 타이틀곡",
"판정": "지갑에 없는 오브젝트",
"초과표수": null,
"evidence": [
{
"source": "투표기록",
"field": "사용오브젝트id",
"value": "OBJ-3310"
},
{
"source": "오브젝트보유",
"field": "아티스트",
"value": "ARTMS"
},
{
"source": "투표규칙",
"field": "대상아티스트",
"value": "tripleS"
}
],
"요약": "대상 아티스트가 다른 사유입니다. 오브젝트는 제 지갑에 그대로 있지만 ARTMS 오브젝트로 tripleS 회차에 던진 표라 집계에서 뺍니다.",
"hedge": "판정 이름은 지갑에 없는 오브젝트 하나지만 여기서 걸린 것은 지갑이 아니라 대상 아티스트입니다."
},
{
"투표id": "V-9013",
"투표항목": "ASSEMBLE25 타이틀곡",
"판정": "결제가 취소된 오브젝트",
"초과표수": null,
"evidence": [
{
"source": "결제내역",
"field": "결제상태",
"value": "취소"
},
{
"source": "결제내역",
"field": "결제통화",
"value": "EUR"
},
{
"source": "결제내역",
"field": "취소시각",
"value": "2026-09-06T18:30:00"
},
{
"source": "투표규칙",
"field": "집계기준시각",
"value": "2026-09-06T21:00:00"
},
{
"source": "투표기록",
"field": "투표항목",
"value": "ASSEMBLE25 타이틀곡"
},
{
"source": "결제내역",
"field": "오브젝트id",
"agg": "count",
"value": 1
}
],
"요약": "EUR 카드 결제가 마감 두 시간 반 전에 취소돼 이 표는 집계에서 뺍니다. ASSEMBLE25 타이틀곡 회차에서 결제가 풀린 표는 이 한 건입니다.",
"hedge": null
},
{
"투표id": "V-9120",
"투표항목": "unevermet 활동 콘셉트",
"판정": "결제가 취소된 오브젝트",
"초과표수": null,
"evidence": [
{
"source": "투표기록",
"field": "사용오브젝트id",
"value": "OBJ-4405"
},
{
"source": "결제내역",
"field": "결제상태",
"value": "환불"
},
{
"source": "결제내역",
"field": "취소시각",
"value": null
},
{
"source": "투표규칙",
"field": "집계기준시각",
"value": "2026-09-07T21:00:00"
}
],
"요약": "JPY 카드 결제가 환불로 남아 있어 이 표는 집계에서 뺍니다. 취소시각이 비어 있어도 결제가 풀린 오브젝트는 집계에 넣지 않는 것이 규칙입니다.",
"hedge": "취소시각이 비어 있어 마감 전 환불인지 가리지 못했습니다. 마감 뒤 환불로 확인되면 이 표는 유효 표로 되돌아갑니다."
},
{
"투표id": "V-9122",
"투표항목": "unevermet 활동 콘셉트",
"판정": "1인 표 수 초과",
"초과표수": 1,
"evidence": [
{
"source": "투표규칙",
"field": "1인최대표수",
"value": 2
},
{
"source": "투표기록",
"field": "투표id",
"agg": "count",
"value": 3
},
{
"source": "투표기록",
"field": "접수시각",
"value": "2026-09-07T15:02:00"
}
],
"요약": "같은 사람이 이 회차에 세 표를 던져 상한 두 표를 넘긴 만큼, 가장 늦게 들어온 한 표를 뺍니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. 판정이 유효 표·지갑에 없는 오브젝트·결제가 취소된 오브젝트·1인 표 수 초과 네 값 중 하나인지 확인합니다. 2. evidence 의 field 가 네 입력 표의 컬럼에 실재하는 이름인지 대조합니다. 3. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 행 값과 글자 그대로 같은지 확인합니다. 4. 초과표수가 그 유저의 그 투표항목 접수 표 수에서 1인최대표수를 뺀 값과 같은지 다시 계산합니다. 5. 판정이 결제가 취소된 오브젝트이면 evidence 에 결제상태와 집계기준시각 두 필드가 존재하는지 확인합니다. 6. 취소시각이 비어 있는 건이면 hedge 필드가 빈칸이 아니라 실재하는지 확인합니다. 7. 요약이 존댓말 한 문장으로 끝나는지 확인합니다. 8. agg 가 count 인 evidence 는 그 표와 같은 투표항목 회차 안에서 센 값인지 확인합니다. 범위가 되는 투표항목을 evidence 에 함께 담습니다. V-9013 의 결제내역.오브젝트id count 는 ASSEMBLE25 타이틀곡 회차에서 결제가 취소·환불로 남은 표 수라 1입니다. 9. 판정이 지갑에 없는 오브젝트이면 evidence 에 사유를 가리는 필드 쌍이 있는지 확인합니다. 지갑이 바뀐 사유는 투표기록.지갑주소와 오브젝트보유.보유지갑주소를, 대상 아티스트가 다른 사유는 오브젝트보유.아티스트와 투표규칙.대상아티스트를 함께 담습니다. [판정 순서 · 규칙] 지갑을 먼저 보고 결제를 다음에 보며 1인 표 수 초과를 마지막에 셉니다. 다만 늦은 순으로 뽑힌 초과 표가 이미 앞의 두 사유로 빠졌으면 그 표는 앞 사유를 유지하고, 대신 다른 표를 초과로 올리지 않습니다. 지갑 갈래는 두 대조를 함께 봅니다. 접수시각 기준으로 오브젝트보유.보유지갑주소가 투표기록.지갑주소와 다르면(이전시각이 접수시각보다 뒤이면 던질 때는 본인 것이라 걸리지 않습니다), 또는 오브젝트보유.아티스트가 투표규칙.대상아티스트와 다르면 지갑에 없는 오브젝트입니다. 판정 이름은 하나지만 두 사유가 들어오므로 어느 쪽인지는 evidence 와 요약에서 가려 보입니다. 결제 갈래는 결제상태가 취소·환불인 표에서 취소시각을 그 회차의 집계기준시각과 견줍니다. 취소시각이 기준보다 앞이면 결제가 취소된 오브젝트입니다(V-9013). 기준보다 뒤이면 마감 시점에는 결제가 살아 있었으므로 이 갈래로 빼지 않고 다음 갈래로 넘깁니다. 취소시각이 비어 있으면 언제 풀렸는지 가릴 수 없지만, 결제가 취소·환불로 남은 표를 집계에 넣으면 발표한 결과를 되돌려야 하므로 결제가 취소된 오브젝트로 두고 취소시각이 비어 있다는 것과 마감 뒤 환불이면 판정이 뒤집힌다는 것을 hedge 에 적습니다(V-9120). 결제내역에 짝이 되는 행이 아예 없으면 결제 갈래로 빼지 않고 다음 갈래로 넘깁니다. [평가 러너 · 2주차] 정답을 붙인 표 60건으로 판정 일치율과 사유별 적중률을 냅니다. [수동 검증 · 사람이 봅니다] 1. 지갑 이전과 접수가 1분 안팎으로 붙은 표를 20건 뽑아, 어느 시각을 앞으로 볼지 사람 눈으로 확인합니다. 2. 한 표가 지갑과 결제 두 사유에 걸릴 때 팬 공지에 어느 쪽을 적는 편이 덜 오해되는지 판단합니다.