휴멜로
경력근거: 웹과 Android/iOS 앱으로 서비스하며, 한국에 이어 일본, 대만, 북미에 진출해 두 달 만에 해외 매출 비중이 약 25%에 이르렀습니다.
티키타는 캐릭터 대화에 에피소드와 선택지, 이미지, 음성이 한 흐름으로 이어지고 크리에이터가 만든 작품이 소비된 만큼 틱백으로 정산되는 서비스입니다. 작품과 캐릭터가 4,000개를 넘었고, 한국에 이어 일본·대만·북미로 나가 두 달 만에 해외 매출 비중이 약 25%에 이르렀습니다. 선택지가 끊긴 화 하나는 그 뒤 화를 통째로 못 읽게 만들고, 그만큼 크리에이터의 틱백도 줄어듭니다. 목소리는 자체 음성 모델 DIVE 가 만들지만 화마다 언어별로 따로 올라오기 때문에, 지역을 열어 둔 작품에서 어느 화의 어느 언어가 비었는지는 사람이 세지 못합니다. 공고도 콘텐츠 운영 담당자와 함께 반복되는 편성과 리포트 업무를 어드민 기능으로 바꾸는 일을 주요 업무로 적습니다. 이 포트폴리오는 스토리 그래프를 따라가는 계산과 그 결과를 운영 화면으로 세우는 설계를 보여줍니다. 음성 합성 모델 자체의 품질이나 인터랙티브 스토리 기획의 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
채팅과 스토리 제작 화면 옆에 크리에이터 대시보드와 어드민 운영 화면이 나란히 적혀 있고, 콘텐츠 운영 담당자와 함께 반복되는 편성과 공지, 리포트 업무를 어드민 기능으로 바꾸는 일이 따로 한 줄입니다. 운영자가 실제로 들어와 쓰는 화면을 만드는 자리입니다.
API 와 데이터 모델, 인증·권한을 Supabase 의 Postgres 와 RLS, Edge Functions 위에서 구현하고 운영하는 일이 주요 업무입니다. 자격 요건도 관계형 DB 의 스키마 설계와 인덱스, 쿼리 성능을 실제로 다뤄 본 경험을 따로 묻습니다.
Claude Code 와 Codex 를 일상적으로 쓰되 생성된 코드의 권한 검사 누락 같은 문제를 직접 찾아 고쳐 본 경험을 자격 요건에 둡니다. Vitest 와 Playwright 로 회귀 버그를 잡아 본 경험도 우대라, 판정 규칙에 테스트가 붙어 있어야 이야기가 됩니다.
작품 하나의 화와 선택지를 시작 화부터 따라가, 독자가 닿지 못하는 화와 여기서 멈추는 화, 그리고 지역별로 목소리가 비어 있는 화를 사유별로 세우는 관리자 페이지입니다. 판정은 도달 계산과 개수 세기로 정해지고, 목소리를 채울 때 DIVE 에 넣을 대사 지문 초안만 모델이 씁니다.
티키타에 캐릭터와 스토리를 올리는 크리에이터와 한국·일본·대만·북미 편성을 맡은 콘텐츠 운영 담당자 — 화와 선택지가 수십 개로 늘고 지역마다 공개 범위가 달라 선택지를 손으로 다 따라가 볼 수 없다
독자가 중간에서 멈추는 이유는 이야기가 재미없어서가 아니라, 화가 늘어나는 속도를 선택지 연결이 따라가지 못하고 지역별 목소리가 뒤늦게 붙기 때문입니다. 시작 화에서 선택지를 끝까지 따라가 도달 목록을 만들고 지역이 요구하는 언어와 견주면, 크리에이터는 어느 화부터 손볼지 바로 정합니다.
크리에이터와 운영 담당자가 함께 보는 화면 하나가 돕니다. 화와 선택지, 공개 설정, 음성 목록을 넣으면 시작 화부터 도달 계산이 돌고, 닿지 못하는 화와 막다른 화, 목소리가 빈 화가 사유별로 묶여 근거와 함께 나옵니다. 음성 파일 자체는 열지 않고 길이와 언어 정보만 보고 시작하며, 파형 확인은 3주차로 미룹니다.
정답을 붙인 화 45개로 다섯 판정의 일치율을 자동으로 내고, Vitest 로 도달 계산을, Playwright 로 목록과 상세 화면을 회귀 검사합니다. Vercel 에 올려 2주차부터 크리에이터가 자기 작품 주소로 들어와 씁니다.
크리에이터가 올린 작품을 모두 한 목록에 놓고, 막힌 화가 몇 편에 걸쳐 몇 개인지와 지역별로 빠진 목소리가 얼마나 되는지를 함께 냅니다. 화별 대사 지문 초안을 붙여 목소리를 채우는 작업을 이어서 시작합니다.
크리에이터가 작품 하나를 넣으면 닿지 못하는 화와 막다른 화, 목소리가 빈 화가 근거와 함께 사유별로 묶여 나옵니다.
본문 언어가 비어 있는 화, 소리 길이가 0인 음성, 시작 화가 아직 없는 작품을 섞은 30종을 넣어도 화면이 죽지 않고 판정 다섯 갈래 중 하나로 떨어집니다.
선택지가 앞 화로 되돌아가며 도는 작품에서도 도달 계산이 멈추고 같은 화를 두 번 세지 않는 것을 보여줍니다.
정답을 붙인 화 45개로 다섯 판정의 일치율을 냅니다. 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이고, 어느 화에서 독자가 실제로 멈추는지는 티키타의 실제 열람 기록과 지역별 결제 기록이 있어야 측정됩니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 본문 언어가 비어 있는 화, 소리 길이가 0인 음성, 음성은 올라왔는데 일부 언어만 빈 화, 앞 화로 되돌아가며 도는 선택지, 시작 화가 없는 작품을 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다. 열두 화 가운데 열한 화에 음성 행이 있고 빠진 언어가 화마다 다릅니다.
나가는 선택지가 없다고 해서 잘못된 화는 아닙니다. 끝맺는 화도 같은 모양이라 판정은 길이 끊긴다는 사실까지만 말하고, 끝맺음인지 아닌지는 크리에이터가 정하는 자리를 남깁니다.
값을 치러야 닿는 화가 있다는 것 자체는 문제가 아니라 이 서비스의 구조입니다. 무료로 닿는 길이 하나도 없을 때만 알리고, 그 배치를 바꿀지는 콘텐츠 운영 담당자가 판단합니다.
실제 도입에서는 화가 수백 개로 늘고 작품마다 지역 편성이 다릅니다. 도달 계산을 조회 시점이 아니라 저장 시점으로 옮기고 지역 편성표를 설정으로 빼는 일은 도입 단계 과제로 분리합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
작품 「밤의 관제탑」의 화 목록입니다. 판정의 대상이며 본문 언어가 비어 있고 아직 공개하지 않은 화를 함께 넣었습니다.
| episode_id | story_id | episode_title | is_published | body_locale |
|---|---|---|---|---|
| EP-01 | ST-021 | 관제탑에 불이 켜진 밤 | true | ["ko","ja","zh-TW","en"] |
| EP-03 | ST-021 | 마지막 교신 기록 | true | ["ko","ja","zh-TW","en"] |
| EP-05 | ST-021 | 활주로 끝의 그림자 | true | ["ko","ja","zh-TW","en"] |
| EP-08 | ST-021 | 이름 없는 주파수 | true | ["ko","ja","zh-TW","en"] |
| EP-11 | ST-021 | 비상 착륙 지시 | true | ["ko","ja","en"] |
아래 JSON Schema 에 맞는 화 행을 더 만들어 주세요. {"type":"object","properties":{"episode_id":{"type":"string"},"story_id":{"type":"string"},"episode_title":{"type":"string"},"is_published":{"type":"boolean"},"body_locale":{"type":["array","null"],"items":{"type":"string"}}},"required":["episode_id","story_id","episode_title","is_published","body_locale"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) body_locale 이 null 이고 is_published 가 false 인 쓰다 만 화, (2) body_locale 에 ko 만 있는 화와 네 언어가 다 있는 화, (3) 어느 선택지도 가리키지 않는 화. 생성한 행은 반드시 위 스키마를 통과해야 하며, 타입을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"episode_id":{"type":"string"},"story_id":{"type":"string"},"episode_title":{"type":"string"},"is_published":{"type":"boolean"},"body_locale":{"type":["array","null"],"items":{"type":"string"}}},"required":["episode_id","story_id","episode_title","is_published","body_locale"]}화와 화를 잇는 선택지입니다. 도달 계산의 재료이며 값을 치러야만 지나갈 수 있는 선택지와 앞 화로 되돌아가는 선택지를 함께 넣었습니다.
| choice_id | from_episode_id | to_episode_id | choice_label | unlock_cost |
|---|---|---|---|---|
| CH-01 | EP-01 | EP-03 | 교신 기록을 먼저 연다 | 0 |
| CH-02 | EP-01 | EP-05 | 활주로로 내려간다 | 0 |
| CH-03 | EP-03 | EP-08 | 주파수를 맞춰 본다 | 0 |
| CH-04 | EP-05 | EP-08 | 무전기를 든다 | 0 |
| CH-05 | EP-05 | EP-11 | 관제탑으로 돌아간다 | 0 |
아래 JSON Schema 에 맞는 선택지 행을 더 만들어 주세요. {"type":"object","properties":{"choice_id":{"type":"string"},"from_episode_id":{"type":"string"},"to_episode_id":{"type":"string"},"choice_label":{"type":"string"},"unlock_cost":{"type":"integer","minimum":0}},"required":["choice_id","from_episode_id","to_episode_id","choice_label","unlock_cost"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) unlock_cost 가 0인 무료 선택지와 30·50처럼 값을 치르는 선택지, (2) 한 화로 들어오는 선택지가 모두 값을 치르는 구성, (3) 앞 화로 되돌아가는 선택지. 생성한 행은 반드시 위 스키마를 통과해야 하며, 타입을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"choice_id":{"type":"string"},"from_episode_id":{"type":"string"},"to_episode_id":{"type":"string"},"choice_label":{"type":"string"},"unlock_cost":{"type":"integer","minimum":0}},"required":["choice_id","from_episode_id","to_episode_id","choice_label","unlock_cost"]}작품마다 누가 올렸고 어느 화에서 시작하며 어느 지역에 열려 있는지를 적어 둔 기준표입니다. 시작 화가 아직 비어 있는 작품을 하나 넣었습니다.
| story_id | creator_id | story_title | entry_episode_id | release_regions |
|---|---|---|---|---|
| ST-021 | CR-118 | 밤의 관제탑 | EP-01 | ["KR","JP","TW","US"] |
| ST-022 | CR-118 | 해협의 등대지기 | EP-40 | ["KR","JP"] |
| ST-023 | CR-207 | 골목 끝 세탁소 | EP-51 | ["KR"] |
| ST-024 | CR-207 | 여름의 보충 수업 | EP-63 | ["KR","JP","TW","US"] |
| ST-025 | CR-311 | 달빛 서점의 손님 | EP-72 | ["KR","TW"] |
아래 JSON Schema 에 맞는 작품 공개 설정 행을 더 만들어 주세요. {"type":"object","properties":{"story_id":{"type":"string"},"creator_id":{"type":"string"},"story_title":{"type":"string"},"entry_episode_id":{"type":["string","null"]},"release_regions":{"type":"array","items":{"type":"string","enum":["KR","JP","TW","US"]}}},"required":["story_id","creator_id","story_title","entry_episode_id","release_regions"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) entry_episode_id 가 null 이라 시작 화가 아직 없는 작품, (2) release_regions 가 KR 하나뿐인 작품과 네 지역을 모두 연 작품, (3) 한 크리에이터가 여러 작품을 올린 경우. 생성한 행은 반드시 위 스키마를 통과해야 하며, 타입을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"story_id":{"type":"string"},"creator_id":{"type":"string"},"story_title":{"type":"string"},"entry_episode_id":{"type":["string","null"]},"release_regions":{"type":"array","items":{"type":"string","enum":["KR","JP","TW","US"]}}},"required":["story_id","creator_id","story_title","entry_episode_id","release_regions"]}화마다 언어별로 올라온 DIVE 음성입니다. 소리 길이가 0이라 파일만 있는 행과 언어 코드를 밑줄로 적은 행, 네 언어가 다 올라온 화와 일부 언어만 빈 화를 함께 넣었습니다. 열두 화 가운데 열한 화에 음성 행이 있고, 아직 쓰다 만 EP-27 만 행이 없습니다.
| episode_id | character_id | locale | duration_ms |
|---|---|---|---|
| EP-01 | CHR-01 | ko | 42,300 |
| EP-01 | CHR-01 | ja | 39,800 |
| EP-01 | CHR-01 | zh-TW | 41,200 |
| EP-01 | CHR-01 | en | 45,100 |
| EP-03 | CHR-02 | ko | 51,000 |
아래 JSON Schema 에 맞는 DIVE 음성 행을 더 만들어 주세요. {"type":"object","properties":{"episode_id":{"type":"string"},"character_id":{"type":"string"},"locale":{"type":"string"},"duration_ms":{"type":"integer","minimum":0}},"required":["episode_id","character_id","locale","duration_ms"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) duration_ms 가 0이라 파일만 있고 소리가 없는 행, (2) 같은 화에 ko 와 en 만 있고 ja·zh-TW 가 없는 구성, (3) 언어 코드를 zh_TW 처럼 밑줄로 적은 행, (4) 대상 화의 절반 이상은 음성 행이 있고 그중 일부만 언어가 비게 하라. 음성 행이 아예 없는 화만으로 판정이 갈리게 하지 말 것. 생성한 행은 반드시 위 스키마를 통과해야 하며, 타입을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"episode_id":{"type":"string"},"character_id":{"type":"string"},"locale":{"type":"string"},"duration_ms":{"type":"integer","minimum":0}},"required":["episode_id","character_id","locale","duration_ms"]}| TypeScript | 도달 계산과 판정 규칙, 화면을 한 언어로 묶습니다. 판정 결과 모양이 바뀌면 화면 쪽에서 바로 잡힙니다. |
| React | 사유별 목록과 화 상세, 선택지 흐름을 그리는 화면 조각입니다. |
| Next.js | App Router 로 작품별 경로를 나누고, 판정 계산은 서버에서 돌려 큰 작품에서도 화면이 늦지 않게 합니다. |
| Supabase | Postgres 와 Auth, RLS 로 크리에이터가 자기 작품만 보게 막습니다. 판정 계산을 Edge Functions 에 두고 작품 저장 시점에 함께 돌립니다. |
| PostgreSQL | 화와 선택지, 공개 설정, 음성 목록을 담습니다. from_episode_id 와 to_episode_id 에 인덱스를 걸어 도달 계산이 선택지 수에 비례해서만 늘게 합니다. |
| SQL | 지역별로 빠진 목소리를 세는 집계와 작품별 요약을 쿼리 한 벌로 뽑습니다. |
| Vitest | 도달 계산과 다섯 판정의 우선순위를 단위 테스트로 묶습니다. 되돌아가며 도는 선택지 구조가 회귀로 깨지지 않게 잡는 자리입니다. |
| Playwright | 목록에서 사유를 눌러 상세가 열리고 근거가 뜨는 흐름을 화면째로 검사합니다. |
| Vercel | 2주차부터 배포와 운영을 맡습니다. 크리에이터가 자기 작품 주소로 들어와 판정을 봅니다. |
| Git | 판정 규칙이 바뀔 때마다 정답 세트 결과를 같이 커밋해 어느 변경이 일치율을 움직였는지 되짚습니다. |
| CI/CD | 밀어 넣을 때마다 Vitest 와 Playwright 를 돌리고 통과분만 Vercel 로 내보냅니다. |
| Claude Code | 도달 계산과 판정 우선순위를 구현하고 경계 조건 테스트를 함께 씁니다. 생성된 쿼리는 권한 검사가 빠지지 않았는지 직접 읽고 고칩니다. |
| Codex | 선택지 그래프가 되돌아가며 도는 경우처럼 손으로 짜기 번거로운 테스트 데이터를 만들어 냅니다. |
| LLM | 목소리가 빈 화에 넣을 대사 지문 초안을 감정과 속도로 적어 둡니다. 어느 화가 막혔는지는 계산이 이미 정한 뒤입니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 판정이 실제로 맞았는지 정답 세트와 맞춰 보는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 다만 작품이 여러 편으로 늘면 도달 계산을 저장 시점으로 옮겨야 화면이 늦지 않으므로, 그 준비가 안 되면 3주차는 작품 목록까지만 합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · episode_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
네 지역 모두 공개 가능네 지역 공개 가능닿지 못하는 화닿지 못하는 화막다른 화막다른 화목소리 빠진 지역 있음목소리 빠진 지역값을 치러야만 닿는 화유료로만 닿는 화화 한 개가 네 지역에 그대로 나가도 되는지, 안 된다면 무엇이 막고 있는지를 근거와 함께 남기는 형식입니다. 판정은 계산이 정하고 대사 지문 초안만 모델이 씁니다.
{"type":"object","properties":{"episode_id":{"type":"string"},"story_id":{"type":"string"},"verdict":{"type":"string","enum":["네 지역 모두 공개 가능","닿지 못하는 화","막다른 화","목소리 빠진 지역 있음","값을 치러야만 닿는 화"]},"missing_locales":{"type":"array","items":{"type":"string"}},"voice_line_brief":{"type":["string","null"]},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":"string","enum":["count","sum","avg","min","max"]},"value":{"type":["string","integer","array","boolean","null"]}},"required":["field","value"]}},"reason":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["episode_id","story_id","verdict","missing_locales","evidence","reason"]}[
{
"episode_id": "EP-01",
"story_id": "ST-021",
"verdict": "네 지역 모두 공개 가능",
"missing_locales": [],
"voice_line_brief": null,
"evidence": [
{
"field": "entry_episode_id",
"value": "EP-01"
},
{
"field": "release_regions",
"value": [
"KR",
"JP",
"TW",
"US"
]
},
{
"field": "locale",
"agg": "count",
"value": 4
},
{
"field": "duration_ms",
"value": 42300
}
],
"reason": "시작 화라 들어오는 선택지 판정에서 빠지고, 나가는 선택지가 둘 있으며 네 지역이 요구하는 목소리가 모두 소리를 가지고 있습니다.",
"hedge": null
},
{
"episode_id": "EP-03",
"story_id": "ST-021",
"verdict": "네 지역 모두 공개 가능",
"missing_locales": [],
"voice_line_brief": null,
"evidence": [
{
"field": "locale",
"value": "zh_TW"
},
{
"field": "duration_ms",
"value": 50200
},
{
"field": "locale",
"agg": "count",
"value": 4
},
{
"field": "unlock_cost",
"value": 0
}
],
"reason": "대만 목소리가 zh_TW 로 적혀 있지만 밑줄과 붙임표를 같은 것으로 보고 견줘 zh-TW 로 읽었습니다. 무료로 닿는 선택지도 있고 나가는 길도 남아 있습니다.",
"hedge": null
},
{
"episode_id": "EP-27",
"story_id": "ST-021",
"verdict": "닿지 못하는 화",
"missing_locales": [
"ko",
"ja",
"zh-TW",
"en"
],
"voice_line_brief": null,
"evidence": [
{
"field": "is_published",
"value": false
},
{
"field": "body_locale",
"value": null
},
{
"field": "to_episode_id",
"agg": "count",
"value": 0
}
],
"reason": "시작 화에서 선택지를 따라가 모은 목록 어디에도 이 화가 없습니다. 이 화를 가리키는 선택지가 하나도 없어 독자가 닿을 길이 아직 없습니다.",
"hedge": null
},
{
"episode_id": "EP-19",
"story_id": "ST-021",
"verdict": "막다른 화",
"missing_locales": [
"zh-TW",
"en"
],
"voice_line_brief": null,
"evidence": [
{
"field": "is_published",
"value": true
},
{
"field": "from_episode_id",
"agg": "count",
"value": 0
},
{
"field": "episode_title",
"value": "다시 걸려 온 전화"
},
{
"field": "locale",
"agg": "count",
"value": 2
}
],
"reason": "공개된 화인데 여기서 나가는 선택지가 하나도 없어 독자가 읽다가 멈춥니다. 대만어와 영어 목소리도 아직 없지만, 사유가 겹칠 때는 앞선 값을 쓰기로 해 판정은 막다른 화로 둡니다.",
"hedge": "이야기를 여기서 끝낼 셈이었는지는 데이터로 알 수 없어, 판정은 길이 끊긴다는 사실까지만 말합니다."
},
{
"episode_id": "EP-08",
"story_id": "ST-021",
"verdict": "목소리 빠진 지역 있음",
"missing_locales": [
"ja",
"zh-TW"
],
"voice_line_brief": "주파수를 맞추며 숨을 죽이는 장면입니다. 낮은 톤에 말 사이 쉼을 길게 두고 읽습니다.",
"evidence": [
{
"field": "release_regions",
"value": [
"KR",
"JP",
"TW",
"US"
]
},
{
"field": "locale",
"value": "en"
},
{
"field": "locale",
"agg": "count",
"value": 2
}
],
"reason": "이 작품은 네 지역에 열려 있는데 이 화에는 ko 와 en 목소리만 있습니다. 일본과 대만 독자는 글은 읽어도 소리를 듣지 못합니다.",
"hedge": null
},
{
"episode_id": "EP-14",
"story_id": "ST-021",
"verdict": "목소리 빠진 지역 있음",
"missing_locales": [
"ja",
"zh-TW",
"en"
],
"voice_line_brief": "수첩을 넘기며 혼잣말하는 장면입니다. 속도를 조금 늦추고 문장 끝을 흐리듯 읽습니다.",
"evidence": [
{
"field": "duration_ms",
"value": 0
},
{
"field": "locale",
"value": "ja"
},
{
"field": "body_locale",
"value": [
"ko",
"ja",
"zh-TW",
"en"
]
}
],
"reason": "일본어 본문은 있는데 그 목소리 파일의 길이가 0이라 소리가 나지 않습니다. 대만어와 영어 목소리는 아예 올라오지 않아 빠진 언어가 셋입니다.",
"hedge": null
},
{
"episode_id": "EP-22",
"story_id": "ST-021",
"verdict": "값을 치러야만 닿는 화",
"missing_locales": [],
"voice_line_brief": null,
"evidence": [
{
"field": "unlock_cost",
"value": 30
},
{
"field": "unlock_cost",
"value": 50
},
{
"field": "to_episode_id",
"agg": "count",
"value": 2
}
],
"reason": "이 화로 들어오는 선택지가 둘인데 값이 각각 30과 50이라 무료로 닿는 길이 없습니다. 네 지역 목소리는 모두 올라와 있어 빠진 언어는 없고, 값을 치르지 않는 독자는 뒤쪽 화를 통째로 보지 못합니다.",
"hedge": null
}
][자동 검증 · 1주차] 1. verdict 값이 다섯 enum 안에 있는지 확인합니다. 2. episode_id 와 story_id, verdict, missing_locales, evidence, reason 이 모두 있는지 확인합니다. 3. unlock_cost 가 0 이상이고 duration_ms 가 0 이상인지 확인합니다. 4. story_release.entry_episode_id 에서 episode_choices 를 따라가 모은 화 목록에 그 episode_id 가 없으면 verdict 가 닿지 못하는 화인지 다시 계산합니다. 조건이 성립하는 사례는 EP-27 이고 성립하지 않는 사례는 EP-19 입니다. 5. 그 episode_id 를 to_episode_id 로 갖는 선택지를 모아 unlock_cost 가 0인 행이 하나도 없으면 verdict 가 값을 치러야만 닿는 화인지 다시 계산합니다. 다만 시작 화는 들어오는 선택지가 원래 없으므로 이 계산에서 뺍니다. 조건이 성립하는 사례는 EP-22 이고 성립하지 않는 사례는 EP-03 입니다. 6. is_published 가 참인데 그 episode_id 를 from_episode_id 로 쓰는 선택지 개수가 0이면 verdict 가 막다른 화인지 다시 계산합니다. 조건이 성립하는 사례는 EP-19 이고 성립하지 않는 사례는 EP-01 입니다. 7. release_regions 의 KR·JP·TW·US 를 ko·ja·zh-TW·en 으로 바꾼 목록에서, 그 episode_id 의 dive_voice_assets.locale 에 없거나 duration_ms 가 0인 언어를 모아 missing_locales 와 같은지 다시 계산합니다. 언어 코드는 밑줄과 붙임표를 같은 것으로 보고 견주고, 목록은 KR·JP·TW·US 차례로 적습니다. missing_locales 는 verdict 와 무관하게 모든 화에서 이 계산 결과를 그대로 적습니다. 음성 행이 하나도 없는 화는 요구 언어 넷이 모두 들어가고(EP-27), 다른 사유로 판정이 갈린 화도 빠진 언어가 있으면 그대로 적으며(EP-19), 요구 언어가 모두 소리를 가지면 빈 배열입니다(EP-22). 빠진 언어가 하나라도 남고 앞선 사유가 없으면 verdict 가 목소리 빠진 지역 있음인지 확인합니다. 조건이 성립하는 사례는 EP-14 이고 성립하지 않는 사례는 EP-03 입니다. 8. verdict 가 겹칠 때는 닿지 못하는 화, 값을 치러야만 닿는 화, 막다른 화, 목소리 빠진 지역 있음, 네 지역 모두 공개 가능 차례로 앞선 값을 씁니다. 9. evidence 의 field 가 화와 선택지, 공개 설정, 음성 네 표의 컬럼에 실재하는지 확인합니다. 10. agg 가 없는 evidence 항목의 value 가 수록된 행의 그 컬럼 값과 같은지 확인합니다. 11. 모든 reason 이 존댓말로 끝나는지 확인합니다. [수동 검증] 1. 나가는 선택지가 없는 화가 진짜 끝맺음인지 크리에이터에게 확인합니다. 데이터만으로는 갈라지지 않는 자리입니다. 2. 값을 치러야 닿는 화가 이야기 흐름상 그래도 되는 자리인지 콘텐츠 운영 담당자와 함께 봅니다. 3. voice_line_brief 의 감정과 속도가 그 화의 장면과 맞는지 사람이 읽고 고칩니다. [평가 러너 · 2주차] 1. 정답을 붙인 화 45개로 다섯 판정의 정밀도와 재현율을 냅니다. 2. 선택지가 되돌아가며 도는 작품과 한 줄로 이어지는 작품을 나눠 도달 계산이 얼마나 갈리는지 견줍니다.