- 사건 하나는 이미지까지 담은 JSON 파일 하나예요.
- 잠긴 증거(
unlocks)가 있으면 형식 버전 2, 없으면 1이에요. - 이미지는 파일 이름을 키로, 내용을 Base64 문자열로 넣어요. 사건 안의 이미지 이름과 키가 같아야 해요.
- 공공 사건기록보관소에는 15MB까지 올릴 수 있어요.
1. 파일 한눈에 보기
| 항목 | 내용 |
|---|---|
| 확장자 | .casedoc(Casebook Case Document). 예전 확장자 .sherlockcase도 열 수 있지만, 새로 저장할 때는 .casedoc을 써요. |
| 내용 | UTF-8 JSON 객체 하나. 폴더가 아니라 파일 하나라서 AirDrop이나 파일 앱으로 그대로 주고받을 수 있어요. |
| 크기 | 공공 사건기록보관소에 올릴 수 있는 파일은 15MB(15,728,640바이트)까지예요. 넘으면 "사건 파일이 너무 커요. 15MB 이하로 줄여 주세요."라고 안내해요. 기기 안에서 열고 플레이하는 데는 이 제한이 없어요. |
| 모르는 키 | 앱은 아래 표에 없는 키를 읽을 때 무시하고, 다시 저장하면 그 키는 사라져요. |
맨 위 객체의 키는 다섯 개예요.
| 키 | 타입 | 필수 | 설명 |
|---|---|---|---|
formatVersion | 정수 | 필수 | 형식 버전. 2장을 보세요. |
caseFile | 객체 | 필수 | 사건 내용. 3장을 보세요. |
images | 객체(이름 → Base64) | 필수 | 이미지 파일. 이미지가 없으면 빈 객체 {}. 8장을 보세요. |
author | 객체 | 선택 | 사건을 만든 사람 { "id": 문자열, "name": 문자열 }. 앱이 채우는 칸이라 직접 만들 때는 넣지 않아요. 없으면 가져온 사람이 고칠 수 있는 내 사건으로 들어가요. |
modifiedAt | 숫자 | 선택 | 사건 내용이 마지막으로 바뀐 시각(2001년 1월 1일 UTC부터 센 초). 앱이 편집할 때 기록하니 직접 만들 때는 넣지 않아요. 없으면 파일의 수정 날짜로 대신해요. |
2. 형식 버전
| 버전 | 언제 쓰나요 |
|---|---|
| 1 | 잠긴 증거가 없는 사건. caseFile에 unlocks 키를 넣지 않아요. |
| 2 | unlocks가 하나라도 있는 사건(결정적 증거 해금). 지금 앱이 읽을 수 있는 가장 높은 버전이에요. |
- 앱은 저장할 때마다 내용에 맞춰 버전을 다시 정해요. 잠긴 증거를 모두 빼면 1로, 하나라도 더하면 2로 저장해요.
- 앱이 읽을 수 있는 버전보다 높은 파일을 열면 "더 새로운 버전의 편집기로 만든 사건이에요. 앱을 업데이트한 뒤 다시 가져와 주세요."라고 안내해요. 이때 제목 · 장르 · 현장 배경만 읽어서 업데이트 안내 화면에 보여 줘요.
- 형식 버전 2(잠긴 증거)는 Casebook 26.10.1 이상에서 열려요. 26.10.0은 형식 버전 1까지 읽어요.
- 그래서 잠긴 증거가 없는 사건은 1로 두어야 형식 버전 2를 모르는 예전 앱에서도 열려요.
3. caseFile
사건 내용이에요. "선택"이라고 적힌 키는 빼도 되고, 빼면 기본값으로 읽어요. 필수 키도 빈 문자열이나 빈 배열은 넣을 수 있지만, 9장의 검사에 걸릴 수 있어요.
| 키 | 타입 | 필수 | 설명 |
|---|---|---|---|
id | 문자열 | 필수 | 사건 ID. 예: case.glass-greenhouse |
title | 문자열 | 필수 | 사건 제목 |
clientName | 문자열 | 필수 | 의뢰인 이름 |
briefing | 문자열 | 필수 | 목록 · 둘러보기에 보이는 짧은 3인칭 소개(2~3문장) |
clientRequest | 문자열 | 선택(기본 "") | 의뢰인이 직접 말하듯 쓴 의뢰 내용. 조사 화면의 '사건 개요' 탭에 보여요. 비어 있으면 briefing을 대신 보여 줘요. |
clientPortraitImageName | 문자열 | 선택(기본 "") | 의뢰인 사진의 이미지 이름. 비어 있으면 이름 첫 글자로 그려요. |
genre | 문자열 | 선택(기본 murder) | 장르. 6장의 일곱 값 중 하나 |
backdropImageName | 문자열 | 필수 | 사건 현장 전경의 이미지 이름. 증거 탭 배너와 몰입 공간 배경에 써요. |
sceneEvidence | 배열 | 필수 | 현장 증거. 4장 |
documentEvidence | 배열 | 필수(빈 배열 가능) | 문서 증거. 4장 |
interviews | 배열 | 필수 | 관계자와 진술. 5장 |
motiveOptions | 배열 | 선택(기본: 장르 기본 선택지) | '왜' 선택지. 6장 |
methodOptions | 배열 | 선택(기본: 장르 기본 선택지) | '어떻게' 선택지. 6장 |
solution | 객체 | 필수 | 정답. 6장 |
hints | 객체 | 필수 | 힌트 세 문장. 6장 |
story | 문자열 | 필수 | 사건의 실제 전말. 해결했거나 힌트를 보고도 못 풀었을 때 보여요. |
unlocks | 배열 | 선택(형식 버전 2) | 잠긴 증거. 7장. 없으면 키를 넣지 않아요. |
4. 증거
현장 증거(sceneEvidence의 항목). 키는 모두 필수예요.
| 키 | 타입 | 설명 |
|---|---|---|
id | 문자열 | 증거 ID. 예: evidence.cut-glass |
imageName | 문자열 | 증거 사진의 이미지 이름 |
caption | 문자열 | 짧은 이름. 화면과 검사 안내에 이 이름이 나와요. |
detail | 문자열 | 자세한 설명 |
immersivePlacement | 객체 { x, y, z }(숫자) | Apple Vision Pro 몰입 공간에서 사용자 기준으로 놓이는 위치(미터) |
문서 증거(documentEvidence의 항목). 키는 모두 필수예요.
| 키 | 타입 | 설명 |
|---|---|---|
id | 문자열 | 증거 ID. 예: document.ledger |
title | 문자열 | 문서 제목 |
body | 문자열 | 문서 내용 |
5. 관계자와 진술
interviews의 항목이에요. 키는 모두 필수예요. 앱은 관계자 전원을 범인 후보로 보여 줘요.
| 키 | 타입 | 설명 |
|---|---|---|
id | 문자열 | 관계자 ID. 예: person.tom |
personName | 문자열 | 이름 |
role | 문자열 | witness(목격자) · suspect(용의자) · client(의뢰인) 중 하나 |
portraitImageName | 문자열 | 프로필 사진의 이미지 이름 |
transcript | 배열 | 진술. 한 줄은 { "id": 문자열, "speaker": 문자열, "text": 문자열 } |
speaker는 holmes(탐정의 질문) 또는 interviewee(관계자의 답)예요.
6. 장르 · 선택지 · 정답 · 힌트
장르마다 추리 문항 세 개(누가 · 왜 · 어떻게)의 이름과 기본 선택지가 달라요. motiveOptions · methodOptions를 빼면 아래 기본 선택지로 읽어요.
genre | 화면 이름 | 문항 이름 | 기본 '왜' id | 기본 '어떻게' id |
|---|---|---|---|---|
murder | 살인·사망 | 범인 · 동기 · 범행 수법 | revenge · greed · jealousy · selfDefense · coverUp | poison · bluntForce · stagedAccident · strangulation · firearm |
theft | 도난·절도 | 범인 · 동기 · 수법 | greed · revenge · collectorObsession · debt · coverUp | forgerySwap · insiderKey · breakIn · divertedInTransit · fakeTheft |
disappearance | 실종·행방불명 | 열쇠를 쥔 인물 · 사라진 이유 · 실종의 진상 | escapeThreat · keepSecret · elopement · inheritanceDispute · witnessedCrime | voluntaryHiding · kidnapping · accident · concealedMurder · assumedIdentity |
fraud | 협박·사기·사칭 | 범인 · 목적 · 수법 | extortion · inheritanceStatus · revenge · hideSecret · romance | blackmailLetter · impersonation · forgedDocument · investmentFraud · conspiracy |
cipher | 암호·문서·정보 | 작성자·유출자 · 목적 · 숨긴 방법 | sellSecrets · contactAccomplice · coverUp · prepareBlackmail · frameSomeone | substitutionCipher · bookCipher · invisibleInk · forgedDocument · hiddenLetters |
lockedRoom | 저택·밀실·기이한 공간 | 범인 · 동기 · 밀실 트릭 | revenge · greed · jealousy · coverUp · inheritanceDispute | secretPassage · clockTampering · mechanism · lockedFromOutside · crimeAfterDiscovery |
ghostStory | 괴담 | 배후 인물 · 목적 · 괴현상의 정체 | driveAway · seizeEstate · coverUp · revenge · prank | lightAndShadow · soundDevice · disguise · animal · mirrorIllusion |
선택지(motiveOptions · methodOptions의 항목)는 { "id": 문자열, "title": 문자열 }이에요. id는 정답 비교와 저장에 쓰고, 화면에는 title이 보여요. 기본 선택지를 고치거나 더해도 돼요.
정답(solution). 키는 모두 필수예요.
| 키 | 타입 | 설명 |
|---|---|---|
culpritID | 문자열 | 범인인 관계자의 id |
motive | 문자열 | motiveOptions 중 정답의 id |
method | 문자열 | methodOptions 중 정답의 id |
keyEvidenceIDs | 문자열 배열 | 결정적 증거(현장 · 문서 증거)의 id. 순서는 상관없어요. |
힌트(hints)는 { "culprit": 문자열, "motive": 문자열, "method": 문자열 }이에요. 장르마다 문항 이름은 달라도 키 이름은 같아요. 채점과 힌트를 보여 주는 방식은 사건 제작 가이드 1장에 있어요.
7. 잠긴 증거(unlocks)
처음에는 잠겨 있다가 단서를 확인하거나 답을 맞히면 열리는 증거예요. 하나라도 있으면 형식 버전 2예요.
| 키 | 타입 | 필수 | 설명 |
|---|---|---|---|
evidenceID | 문자열 | 필수 | 잠글 현장 증거 또는 문서 증거의 id |
from | 문자열 배열 | 선택(기본 []) | 확인해야 하는 단서(현장 · 문서 증거, 관계자)의 id. 증거는 상세 화면을 열면, 관계자는 진술을 열면 확인한 것으로 봐요. |
needCount | 정수 | 선택 | from 중 몇 개를 확인하면 열리는지. 없으면 전부 |
answer | 객체 | 선택 | 답을 맞혀야 열리는 잠금이면 그 문제. from 조건을 채운 뒤(비어 있으면 처음부터) 입력칸이 나와요. |
answer의 키예요.
| 키 | 타입 | 필수 | 설명 |
|---|---|---|---|
prompt | 문자열 | 필수 | 입력칸 위에 보이는 질문. 예: "금고의 번호는?" |
accepted | 문자열 배열 | 필수 | 정답으로 받는 값들 |
match | 문자열 | 선택(기본 text) | 비교 방법. 앱이 모르는 값도 text로 읽어요. |
match | 비교 방법 | 쓰는 곳 |
|---|---|---|
digits | 숫자만 남겨 비교해요(전각 숫자도 0~9로 맞춰요). | 금고 번호, 연도 |
text | 대소문자 · 공백 · 문장부호 · 기호를 무시해요. | 낱말, 이름 |
exact | 앞뒤 공백만 무시하고 그대로 비교해요. | 정확히 맞혀야 하는 답 |
세 방법 모두 한글은 자모가 나뉘어 들어와도 같은 글자로 봐요.
8. 이미지
images는 이미지 이름을 키로, 이미지 파일 내용을 Base64 문자열로 담은 객체예요.- 키는
caseFile안의 이미지 이름(backdropImageName·clientPortraitImageName· 현장 증거의imageName· 관계자의portraitImageName)과 똑같아야 해요. 이름만 있고images에 그 키가 없으면 그 자리가 빈 칸으로 보여요. - 이미지 이름은 비어 있으면 안 되고,
.으로 시작하거나/·\·:를 넣을 수 없어요. 어기면 "사건 파일 안의 이미지 이름이 올바르지 않아요"라며 파일을 열지 않아요. - 사건에서 쓰지 않는 이미지는 앱이 저장할 때 빼요.
- 사진 없이 만들 때는
images를{}로, 이미지 이름 칸을 모두""로 두고, 가져온 뒤 앱 편집기에서 사진을 넣어요.
9. 앱이 검사하는 것
플레이할 수 없는 문제. 편집기의 '확인할 점'에 나오고, 하나라도 있으면 플레이할 수 없어요.
title·story가 비어 있지 않아요.- 현장 증거가 하나 이상, 관계자가 한 명 이상이에요.
solution.culpritID가 관계자 중 한 명의id예요.motiveOptions·methodOptions는 각각 두 개 이상이고,title이 빈 것이 없고, 같은 배열 안에서id가 겹치지 않아요.solution.motive·solution.method가 각 선택지에 있는id예요.solution.keyEvidenceIDs가 하나 이상이고, 모두 있는 증거의id예요.- 현장 증거 · 문서 증거 · 관계자의
id가 서로 겹치지 않아요. hints의 세 문장이 모두 비어 있지 않아요.
unlocks가 있으면 아래도 검사해요.
evidenceID는 있는 현장 · 문서 증거이고, 한 증거는 한 번만 잠가요.from에는 있는 증거 · 관계자만 넣고, 잠긴 증거 자신이나 같은 단서를 두 번 넣지 않아요.answer가 없으면from이 비면 안 돼요.needCount는 1 이상,from개수 이하예요.answer.prompt가 비어 있지 않고,accepted가 하나 이상이며 비교 방법을 적용해도 비지 않아요(digits면 숫자가 있어야 해요).- 막다른 길이 없어요. 처음부터 열린 증거와 모든 관계자에서 시작해 확인을 이어 가면 모든 잠금이 열려요(답 잠금은 조건을 채우면 열 수 있다고 봐요).
사진이 빠진 곳. 플레이는 되지만 그 자리가 빈 칸으로 보여요. 편집기의 '확인할 점'과 공공 사건기록보관소에 올리기 전 검사에 나와요.
- 현장 배경 · 현장 증거 · 관계자 사진의 이름이 비어 있을 때
- 이름은 있는데
images에 그 이미지가 없거나 비어 있을 때(의뢰인 사진도 이때는 알려요. 의뢰인 사진은 이름을 비워 두면 기본 아이콘을 써요.)
10. 예시
형식 버전 2(잠긴 증거 하나)인 지어낸 사건이에요. images의 값은 줄여 적었어요. 실제 파일에는 이미지 파일 전체를 Base64로 넣어야 열려요.
{
"formatVersion": 2,
"caseFile": {
"id": "case.glass-greenhouse",
"title": "유리 온실의 사라진 난초",
"clientName": "큐 식물원 관리인 해리엇 웨스트",
"briefing": "식물원 유리 온실에서 하룻밤 사이에 귀한 난초가 사라졌다. 문은 안에서 잠겨 있었다.",
"clientRequest": "",
"clientPortraitImageName": "",
"genre": "theft",
"backdropImageName": "greenhouse.jpg",
"sceneEvidence": [
{
"id": "evidence.cut-glass",
"imageName": "cut-glass.jpg",
"caption": "잘린 유리창",
"detail": "지붕 가까운 유리 한 장이 안쪽에서 잘려 있다.",
"immersivePlacement": { "x": 0, "y": 0.1, "z": -1 }
}
],
"documentEvidence": [
{
"id": "document.ledger",
"title": "출입 장부",
"body": "밤 10시 이후 서명은 정원사 톰 한 사람뿐이다."
}
],
"interviews": [
{
"id": "person.tom",
"personName": "톰 베이커 (정원사)",
"role": "suspect",
"portraitImageName": "tom.jpg",
"transcript": [
{ "id": "l1", "speaker": "holmes", "text": "어젯밤 온실 열쇠는 누가 가지고 있었나요?" },
{ "id": "l2", "speaker": "interviewee", "text": "제가요. 하지만 10시에 문을 잠그고 나왔습니다." }
]
},
{
"id": "person.lucy",
"personName": "루시 그레이 (수집가)",
"role": "suspect",
"portraitImageName": "lucy.jpg",
"transcript": [
{ "id": "l1", "speaker": "holmes", "text": "그 난초를 사려고 하셨다고요?" },
{ "id": "l2", "speaker": "interviewee", "text": "값을 불렀다가 거절당했어요. 그뿐이에요." }
]
}
],
"motiveOptions": [
{ "id": "greed", "title": "금전적 이득" },
{ "id": "collectorObsession", "title": "수집 집착" }
],
"methodOptions": [
{ "id": "insiderKey", "title": "내부자의 열쇠" },
{ "id": "breakIn", "title": "외부 침입" }
],
"solution": {
"culpritID": "person.tom",
"motive": "greed",
"method": "insiderKey",
"keyEvidenceIDs": ["document.ledger"]
},
"hints": {
"culprit": "출입 장부에 남은 이름을 다시 보세요.",
"motive": "난초가 누구에게 팔릴 수 있었는지 생각해 보세요.",
"method": "잘린 유리는 어느 쪽에서 잘렸나요?"
},
"story": "정원사 톰은 수집가에게 난초를 팔기로 하고 열쇠로 온실에 들어갔다. 침입처럼 보이려고 유리를 안쪽에서 잘랐지만, 출입 장부의 서명이 그를 가리켰다.",
"unlocks": [
{
"evidenceID": "document.ledger",
"from": ["evidence.cut-glass", "person.tom"],
"answer": { "prompt": "장부 상자의 번호는?", "accepted": ["1887"], "match": "digits" }
}
]
},
"images": {
"greenhouse.jpg": "/9j/4AAQSkZJRg...",
"cut-glass.jpg": "/9j/4AAQSkZJRg...",
"tom.jpg": "/9j/4AAQSkZJRg...",
"lucy.jpg": "/9j/4AAQSkZJRg..."
}
}