벙커키즈
경력근거: 유저가 직접 캐릭터를 만들고, 시나리오를 설계하고, AI 캐릭터와 몰입감 넘치는 대화를 나누는 서비스입니다
※ 실제 산출물이 아닌 AI 예측 시안입니다.
벙커키즈의 위프는 유저가 직접 캐릭터를 만들고 시나리오를 설계해 AI 캐릭터와 대화하는 서비스이고, 서사를 관리하는 채팅 엔진을 Go 로 돌리며 한국과 일본을 비롯한 9개국에서 서비스합니다. 창작자가 만든 분기 그래프가 제품의 재료 자체라서, 그 그래프가 어디서 끊기는지는 위프처럼 유저가 이야기를 짜 올리는 제품에서만 생기는 문제입니다. 판정 순서를 채팅 엔진이 분기를 고르는 순서에 맞춰 두면 발행 전 화면과 실제 대화가 같은 답을 냅니다. 다만 이 포트폴리오는 그래프 탐색과 조건식 해석, 그 결과를 읽는 화면을 보여줍니다. 캐릭터 대사 품질이나 모델 라우팅 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
위프는 유저가 캐릭터를 만들고 시나리오를 설계해 AI 캐릭터와 대화하는 서비스라고 공고가 밝혔습니다. 그 시나리오를 짜는 사람이 발행 전에 막힌 자리를 보는 화면이 없으면 손해가 독자에게 그대로 갑니다.
Python 으로 LLM 파이프라인을, Go 로 캐릭터의 서사를 관리하는 채팅 엔진을 만든다고 적혀 있습니다. 그 엔진이 분기를 고르는 순서와 같은 순서로 조건을 평가해야 판정이 실제 대화와 어긋나지 않습니다.
장애가 나면 원인을 찾고 자동화된 도구로 재발을 막는다고 일하는 방식에 적었습니다. 발행 파이프라인에 판정을 붙여 막힌 분기가 있으면 발행 전에 표시하는 3주차가 그 방식에 그대로 붙습니다.
창작자가 저장한 장면 목록과 분기 목록을 받아 시작 장면에서 분기를 따라 걸어 보고, 독자가 들어갈 수 없는 장면과 어떤 엔딩에도 닿지 않는 장면을 짚습니다. 분기 쪽은 도착 장면이 목록에 서 있는지 보고, 같은 장면에서 갈라지는 조건식을 플래그 값 범위로 풀어, 뒤 순번 분기가 앞 순번에 다 삼켜져 어떤 독자에게도 안 걸리는지 따집니다. 발행 버튼을 누르기 전에 막힌 자리가 장면 이름으로 나와, 어디를 손볼지 창작자가 그 자리에서 정합니다.
위프에서 캐릭터와 시나리오를 만들어 발행하는 창작자
시나리오가 커지면 장면과 분기가 수십 개로 늘어 창작자가 머릿속으로 길을 다 따라가지 못합니다. 시작 장면에서 걸어 본 결과와 조건식이 겹치는 자리를 함께 내면, 발행 전에 손볼 곳이 다섯 갈래로 나뉘고 독자가 도중에 멈추는 일이 줄어듭니다.
장면 목록과 분기 목록, 플래그 정의를 올리면 장면과 분기마다 다섯 판정 중 하나가 붙고 사유별로 묶여 나오는 관리자 페이지입니다. 조건식은 플래그 비교와 and·not 까지만 읽는 작은 해석기로 시작하고, 대화 로그나 모델 호출은 다루지 않습니다.
장면과 분기 180건에 손으로 매긴 정답 판정을 붙여 자동 판정과 대조하고 사유별 일치율을 냅니다. 2주차부터 GCP 로 배포해 창작자가 웹 주소로 들어와 자기 시나리오를 올려 봅니다.
판정을 발행 파이프라인의 검사 단계로 붙여 시나리오를 저장할 때마다 자동으로 돌리고, 막힌 자리가 남아 있으면 발행 화면에 그 목록을 세워 두는 단계입니다.
창작자가 장면 목록과 분기 목록, 플래그 정의를 올리면 독자가 멈추는 자리가 사유별로 묶여 화면에 나오는 과정을 시연합니다.
장면과 분기 180건에 손으로 매긴 정답 판정을 붙여 대조하고 사유별 일치율을 냅니다. 단, 정답과 시나리오를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
망가진 입력 30종을 먼저 넣어 봅니다. 조건식이 문법에 어긋나거나 플래그 정의가 없거나 장면끼리 서로를 가리켜 길이 도는 시나리오를 올려도 화면이 서고, 풀지 못한 조건은 판정 자리 대신 hedge 로 내려갑니다.
장면 200개·분기 600개짜리 시나리오를 올렸을 때 판정이 끝나는 데 걸리는 시간을 재고, 저장할 때마다 돌려도 되는 범위인지 봅니다.
잰 값은 여기까지입니다. 일치율은 제가 짠 시나리오에 제가 매긴 정답을 붙여 나온 숫자이고, 독자가 실제로 덜 이탈했는지는 위프에 올라간 실제 작품과 독자 진행 기록을 넣어야 나옵니다. 그 측정을 붙이자마자 돌릴 수 있게 평가 러너와 정답 서식을 미리 갖춰 둡니다.
사유별 일치율은 제가 짠 시나리오를 제가 채점한 값이라 성능 근거가 못 됩니다. 면접에서는 화면이 도는 범위와, 실제 작품이 오면 바로 재볼 수 있다는 준비 상태까지만 말합니다.
장면도 정답 판정도 제가 적었으니 맞아떨어지는 것이 당연합니다. 플래그 목록이 비어 있는 장면과 조건 없는 기본 분기, 도착 장면이 지워진 분기를 일부러 섞어 두어 깔끔한 그래프만 통과시키는 화면이 되지 않게 했습니다.
이 포트폴리오는 그래프 탐색과 조건식 해석, 그 결과를 읽는 화면을 보여줍니다. 캐릭터 대사 품질이나 모델 라우팅의 깊이는 주장하지 않습니다. 판정은 탐색과 구간 비교가 하고 모델은 창작자가 읽을 문장만 씁니다.
끝에 못 가는 장면이 언제나 결함은 아닙니다. 창작자가 열린 결말로 남겨 둔 자리도 같은 모양이라, 이 판정은 확정이 아니라 창작자가 열어 볼 후보로 냅니다.
실제 위프의 조건식은 여기서 읽는 문법보다 넓고 캐릭터 기억이나 대화 흐름에 딸린 조건도 있습니다. 못 읽는 문법을 만나면 그 분기를 판정 대상에서 빼고 목록에 따로 세우며, 문법을 넓히는 일은 운영 과제로 둡니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
창작자가 저장한 장면 목록입니다. 판정의 대상 표이며, 쓸 수 있는 플래그가 적히지 않은 장면을 담아 조건 겹침 판정을 건너뛰는 경로를 보여줍니다.
| scene_id | title | available_flags | is_entry | is_ending |
|---|---|---|---|---|
| SC-01 | 부두의 첫 만남 | trust | true | false |
| SC-03 | 선원의 귓속말 | trust,route_south | false | false |
| SC-05 | 창고 뒷골목 | trust,has_key,route_south | false | false |
| SC-07 | 등대지기의 방 | trust,has_key | false | false |
| SC-11 | 밀수선 갑판 | — | false | false |
아래 JSON Schema 에 맞는 장면 행을 더 만들어 주세요. {"type":"object","properties":{"scene_id":{"type":"string"},"title":{"type":"string"},"available_flags":{"type":["string","null"]},"is_entry":{"type":"boolean"},"is_ending":{"type":"boolean"}},"required":["scene_id","title","available_flags","is_entry","is_ending"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) available_flags 가 null 인 장면, (2) 엔딩으로 표시했는데 나가는 분기가 남아 있는 장면, (3) 시작 장면이 둘 이상인 시나리오, (4) 제목이 서로 비슷해 사람이 헷갈리는 장면 쌍. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"scene_id":{"type":"string"},"title":{"type":"string"},"available_flags":{"type":["string","null"]},"is_entry":{"type":"boolean"},"is_ending":{"type":"boolean"}},"required":["scene_id","title","available_flags","is_entry","is_ending"]}장면과 장면을 잇는 분기 목록입니다. 조건식과 순번이 판정의 재료이며, 도착 장면이 목록에 서 있지 않은 분기와 조건이 겹치는 분기 쌍을 일부러 담았습니다.
| branch_id | from_scene | to_scene | condition_expr | order_no |
|---|---|---|---|---|
| BR-01 | SC-01 | SC-03 | trust >= 2 | 1 |
| BR-02 | SC-01 | SC-07 | — | 2 |
| BR-05 | SC-03 | SC-05 | route_south | 1 |
| BR-06 | SC-03 | SC-36 | not route_south | 2 |
| BR-09 | SC-05 | SC-13 | has_key | 1 |
아래 JSON Schema 에 맞는 분기 행을 더 만들어 주세요. {"type":"object","properties":{"branch_id":{"type":"string"},"from_scene":{"type":"string"},"to_scene":{"type":"string"},"condition_expr":{"type":["string","null"]},"order_no":{"type":"integer","minimum":1}},"required":["branch_id","from_scene","to_scene","condition_expr","order_no"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) condition_expr 이 null 인 기본 분기, (2) 같은 from_scene 에서 조건이 동시에 참이 될 수 있는 분기 쌍, (3) scene_states 에 없는 to_scene 을 가리키는 분기, (4) 두 장면이 서로를 가리켜 길이 도는 분기 쌍. 조건식은 플래그 비교와 and·not 까지만 쓰고, 생성한 행은 반드시 위 스키마를 통과해야 합니다.{"type":"object","properties":{"branch_id":{"type":"string"},"from_scene":{"type":"string"},"to_scene":{"type":"string"},"condition_expr":{"type":["string","null"]},"order_no":{"type":"integer","minimum":1}},"required":["branch_id","from_scene","to_scene","condition_expr","order_no"]}조건식에 쓰는 플래그의 종류와 값 범위를 적은 기준 표입니다. 조건 겹침은 이 범위 위에서 두 조건이 동시에 참이 되는 값이 있는지로 정합니다.
| flag_key | flag_kind | min_value | max_value | flag_label |
|---|---|---|---|---|
| trust | 정수 | 0 | 5 | 선장 신뢰도 |
| has_key | 참거짓 | — | — | 창고 열쇠 소지 |
| route_south | 참거짓 | — | — | 남쪽 항로 선택 |
| gold | 정수 | 0 | 999 | 소지 금화 |
| wounded | 참거짓 | — | — | 부상 여부 |
아래 JSON Schema 에 맞는 플래그 정의 행을 더 만들어 주세요. {"type":"object","properties":{"flag_key":{"type":"string"},"flag_kind":{"type":"string","enum":["정수","참거짓"]},"min_value":{"type":["integer","null"]},"max_value":{"type":["integer","null"]},"flag_label":{"type":"string"}},"required":["flag_key","flag_kind","min_value","max_value","flag_label"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 참거짓 플래그라 min_value 와 max_value 가 null 인 줄, (2) 값 범위가 한 칸뿐인 정수 플래그, (3) 조건식에는 나오는데 정의가 없는 플래그를 만들어 볼 수 있게 이름을 비슷하게 지은 줄. 생성한 행은 반드시 위 스키마를 통과해야 하며, 한 flag_key 는 한 줄만 두세요.{"type":"object","properties":{"flag_key":{"type":"string"},"flag_kind":{"type":"string","enum":["정수","참거짓"]},"min_value":{"type":["integer","null"]},"max_value":{"type":["integer","null"]},"flag_label":{"type":"string"}},"required":["flag_key","flag_kind","min_value","max_value","flag_label"]}| Python | 장면 그래프를 시작점에서 걸어 보는 탐색기와 조건식 해석기의 언어입니다. 조건식을 플래그 값 구간으로 바꿔 두 분기가 겹치는지 푸는 계산이 여기 있습니다. |
| Go | 채팅 엔진이 실제 대화에서 분기를 고르는 순서를 그대로 옮긴 판정 실행기입니다. 발행 파이프라인이 부르는 검사 명령을 이쪽에 둡니다. |
| TypeScript | 저작 스튜디오 화면과 서버가 주고받는 판정 결과의 타입을 한 곳에 정의해 화면과 서버가 같은 모양을 씁니다. |
| React | 사유별로 묶은 목록과 장면 하나를 펼쳐 보는 상세, 분기 그래프 미리보기를 그리는 화면입니다. |
| Node.js | 시나리오 파일을 올리고 판정 결과를 화면에 넘기는 저작 스튜디오 API 서버입니다. |
| RDBMS 데이터 모델링 | 장면·분기·플래그를 한 스키마로 두고 분기 표에 from_scene 과 order_no 유일 제약을 걸어 같은 장면에서 순번이 겹치지 않게 합니다. |
| GCP | 2주차부터 배포와 운영을 맡습니다. 판정 결과와 올린 시나리오 원본을 Cloud Storage 에 남기고 Cloud SQL 로 저장소를 띄웁니다. |
| AWS | 발행 이력을 되짚을 수 있게 판정 결과 스냅샷을 S3 에 함께 쌓아 두 클라우드 중 어디서도 읽히게 합니다. |
| CI/CD | 3주차에 판정을 발행 파이프라인의 검사 단계로 붙여 시나리오를 저장할 때마다 자동으로 돌립니다. |
| LLM | 판정이 정해진 뒤 막힌 자리를 창작자가 쓰는 장면 이름으로 되짚는 한 문장을 씁니다. |
✗ fail → 못 읽은 조건식이 절반을 넘으면 문법을 좁혀 다시 정하고 그때까지는 그 분기를 판정 대상에서 뺍니다.
✓ pass → 2주차 정답 대조와 GCP 배포로 넘어갑니다.
✗ fail → 어긋난 자리를 모아 판정 차례와 조건 겹침 기준을 다시 봅니다.
✓ pass → 3주차 발행 파이프라인 연결로 넘어갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · target_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
끝까지 닿음끝까지 닿음어디서도 안 닿음들어갈 길 없음없는 장면을 가리킴도착 장면 없음조건이 겹침조건 겹침끝에 못 감엔딩 없음시나리오의 장면 하나 또는 분기 하나가 그래프 탐색과 조건식 해석을 거쳐 어떤 판정을 받는지 남기는 형식입니다. 판정은 규칙이 정하고 요약 문장만 모델이 씁니다.
{"type":"object","properties":{"target_no":{"type":"string"},"target_kind":{"type":"string","enum":["장면","분기"]},"scene_id":{"type":["string","null"]},"branch_id":{"type":["string","null"]},"verdict":{"type":"string","enum":["끝까지 닿음","어디서도 안 닿음","없는 장면을 가리킴","조건이 겹침","끝에 못 감"]},"reached_from_entry":{"type":"boolean","description":"is_entry 장면에서 from_scene 과 to_scene 을 따라간 자리에 그 장면이 들어오면 참입니다. 분기이면 그 분기의 from_scene 을 기준으로 봅니다."},"reaches_ending":{"type":["boolean","null"],"description":"그 장면에서 분기를 따라간 끝에 is_ending 장면이 하나라도 나오면 참입니다. 애초에 닿지 않는 장면과 분기 판정은 비웁니다."},"overlapping_branch_id":{"type":["string","null"],"description":"같은 from_scene 에서 갈라지는 분기 중 조건이 동시에 참이 될 수 있는 상대의 branch_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":["target_no","target_kind","verdict","reached_from_entry","evidence","summary"]}[
{
"target_no": "SC-19",
"target_kind": "장면",
"scene_id": "SC-19",
"branch_id": null,
"verdict": "어디서도 안 닿음",
"reached_from_entry": false,
"reaches_ending": null,
"overlapping_branch_id": null,
"evidence": [
{
"field": "scene_id",
"value": "SC-19"
},
{
"field": "title",
"value": "선장의 고백"
},
{
"field": "is_entry",
"value": false
},
{
"field": "to_scene",
"value": 0,
"agg": "count"
}
],
"summary": "선장의 고백은 어느 분기의 도착 장면으로도 적혀 있지 않고 시작 장면도 아니라 독자가 들어갈 길이 없습니다.",
"hedge": null
},
{
"target_no": "BR-42",
"target_kind": "분기",
"scene_id": "SC-07",
"branch_id": "BR-42",
"verdict": "없는 장면을 가리킴",
"reached_from_entry": true,
"reaches_ending": null,
"overlapping_branch_id": null,
"evidence": [
{
"field": "branch_id",
"value": "BR-42"
},
{
"field": "from_scene",
"value": "SC-07"
},
{
"field": "to_scene",
"value": "SC-88"
},
{
"field": "condition_expr",
"value": "trust >= 5"
}
],
"summary": "등대지기의 방에서 신뢰도 5로 갈라지는 분기가 장면 목록에 없는 SC-88 을 가리켜 그 자리에서 이야기가 끊깁니다.",
"hedge": "장면을 지우고 분기를 그대로 둔 자리라 지운 장면을 되살릴지 분기를 옮길지는 창작자가 정합니다."
},
{
"target_no": "BR-13",
"target_kind": "분기",
"scene_id": "SC-05",
"branch_id": "BR-13",
"verdict": "조건이 겹침",
"reached_from_entry": true,
"reaches_ending": null,
"overlapping_branch_id": "BR-14",
"evidence": [
{
"field": "condition_expr",
"value": "trust >= 3"
},
{
"field": "condition_expr",
"value": "trust >= 3 and has_key"
},
{
"field": "available_flags",
"value": "trust,has_key,route_south"
},
{
"field": "max_value",
"value": 5
},
{
"field": "order_no",
"value": 3
}
],
"summary": "창고 뒷골목에서 신뢰도 3 이상이면서 열쇠를 든 독자는 BR-13 과 BR-14 조건을 동시에 만족해 앞 순번만 계속 나옵니다.",
"hedge": null
},
{
"target_no": "SC-27",
"target_kind": "장면",
"scene_id": "SC-27",
"branch_id": null,
"verdict": "끝에 못 감",
"reached_from_entry": true,
"reaches_ending": false,
"overlapping_branch_id": null,
"evidence": [
{
"field": "scene_id",
"value": "SC-27"
},
{
"field": "title",
"value": "폐선 창고"
},
{
"field": "to_scene",
"value": "SC-31"
},
{
"field": "is_ending",
"value": false
}
],
"summary": "폐선 창고에 들어온 독자는 부서진 무전기까지 간 뒤 어떤 엔딩에도 닿지 못한 채 멈춥니다.",
"hedge": null
},
{
"target_no": "SC-01",
"target_kind": "장면",
"scene_id": "SC-01",
"branch_id": null,
"verdict": "끝까지 닿음",
"reached_from_entry": true,
"reaches_ending": true,
"overlapping_branch_id": null,
"evidence": [
{
"field": "scene_id",
"value": "SC-01"
},
{
"field": "title",
"value": "부두의 첫 만남"
},
{
"field": "is_entry",
"value": true
},
{
"field": "is_ending",
"agg": "count",
"value": 3
}
],
"summary": "부두의 첫 만남에서 출발한 독자는 세 엔딩 가운데 하나에 닿습니다.",
"hedge": null
},
{
"target_no": "BR-21",
"target_kind": "분기",
"scene_id": "SC-11",
"branch_id": "BR-21",
"verdict": "끝까지 닿음",
"reached_from_entry": true,
"reaches_ending": null,
"overlapping_branch_id": null,
"evidence": [
{
"field": "branch_id",
"value": "BR-21"
},
{
"field": "from_scene",
"value": "SC-11"
},
{
"field": "to_scene",
"value": "SC-34"
},
{
"field": "available_flags",
"value": null
},
{
"field": "condition_expr",
"value": "trust >= 2"
}
],
"summary": "밀수선 갑판에서 항구의 아침으로 이어지는 분기라 엔딩까지 길이 이어집니다.",
"hedge": "밀수선 갑판에 쓸 수 있는 플래그가 적혀 있지 않아 같은 장면에서 갈라지는 BR-22 와 조건이 겹치는지는 따지지 않았습니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 다섯 값 중 하나이고 target_kind 가 장면과 분기 두 값 중 하나인지 확인합니다. 규칙이 정한 값이라 모델이 새 값을 지어내면 그 응답을 버립니다. 2. target_no·target_kind·verdict·reached_from_entry·evidence·summary 필드가 빠짐없이 존재하는지 확인합니다. 3. order_no 가 1 이상인지 확인합니다. 4. 어디서도 안 닿음은 그 scene_id 가 어느 story_branches.to_scene 에도 없고 is_entry 도 거짓인지 다시 계산합니다. 5. 없는 장면을 가리킴은 그 분기의 to_scene 이 scene_states.scene_id 어디에도 없는지 다시 계산합니다. 6. 조건이 겹침은 같은 from_scene 의 분기를 order_no 순으로 놓고, 뒤 순번 분기의 조건이 참이 되는 플래그 값 조합이 앞 순번 분기들의 조건에 남김없이 걸리는지 다시 계산합니다. 남는 조합이 하나도 없으면 그 뒤 순번 분기는 어떤 독자에게도 걸리지 않으므로, 바로 앞 순번 분기 행에 조건이 겹침을 붙이고 overlapping_branch_id 에 그 뒤 순번 branch_id 를 적습니다. 조건이 겹치기만 하고 뒤 순번이 걸릴 조합이 남아 있으면 순번이 그 자리를 가르는 것이라 판정하지 않습니다 — 순번이 있는 분기 그래프에서 조건이 겹치는 것 자체는 정상입니다. 값 조합은 그 장면의 available_flags 에 적힌 플래그만 flag_spec 의 값 범위로 훑습니다. 다만 available_flags 가 비어 있으면 겹침을 따지지 않고 그 사실을 hedge 에 남깁니다. 7. 끝에 못 감은 is_entry 장면에서 분기를 따라 그 장면까지 들어온 뒤, 그 장면에서 다시 따라간 어느 길에도 is_ending 장면이 없는지 다시 계산합니다. 8. evidence 의 field 이름이 입력에 있는 컬럼인지 대조합니다. 없는 이름을 인용하면 요약이 지어낸 문장이라는 뜻이라 규칙이 만든 기본 문장으로 되돌립니다. 9. evidence 의 value 가 수록된 행의 원문 값과 같은지 확인합니다. 여러 행을 세어 얻은 수치는 agg 를 함께 적습니다. 10. summary 가 존댓말 한 문장으로 끝나는지 확인합니다. [평가 러너 · 2주차] 손으로 매긴 정답 판정 180건과 자동 판정을 대조해 사유별 일치율을 내고, 어긋난 자리는 따로 모아 조건식 문법을 넓힐지 정합니다. 무작위로 만든 시나리오에 막힌 자리를 일부러 심어 두고 심은 개수와 잡은 개수를 견주는 일도 이 러너에서 합니다. [수동 검증 · 사람이 봅니다] 1. 조건이 겹침으로 걸린 분기 20건을 열어 창작자가 일부러 앞 순번이 먼저 걸리게 두려 한 것인지 판단합니다. 2. 끝에 못 감으로 걸린 장면 중 열린 결말로 남기려 한 자리가 있는지 보고 판정 차례를 다시 봅니다.