데일리바이블 파일 만들기 가이드

데일리바이블 앱은 두 가지 파일을 가져올 수 있습니다 — .dbbible.json(성경 역본·주석성경)과 .dbstory.json(이야기 성경). 이 문서는 두 파일의 공식 포맷 명세입니다. 사람이 직접 만들어도 되고, 이 페이지 주소를 AI에게 주고 만들어 달라고 해도 됩니다.

저작권 안내 (반드시 읽어주세요)
본인이 적법하게 보유·이용할 수 있는 본문으로만 파일을 만들어 주세요. 저작권이 있는 역본(예: 개역개정, NIV 등)으로 만든 파일을 다른 사람에게 배포·공유하는 것은 저작권 침해입니다. 주석성경도 베이스 본문의 저작권을 그대로 따르므로, 공유는 공개도메인 본문(예: 개역한글)으로 만든 것만 하세요. 이 사이트는 저작권이 있는 역본 파일을 제공하지 않습니다.
이야기 성경(.dbstory.json)도 만들 수 있습니다. 성경을 이야기 단위로 다시 쓴 글(어린이용 이야기 성경 등)은 형식이 다릅니다 — 아래 이야기 성경 만들기를 보세요. 이 문서의 앞부분은 성경 역본(.dbbible.json)에 대한 설명입니다.

AI에게 만들어 달라고 하기

ChatGPT·Claude 등 파일을 만들 수 있는 AI에게 아래처럼 요청하세요.

다음 가이드 페이지를 읽고 포맷을 정확히 지켜서
데일리바이블 가져오기 파일(.dbbible.json)을 만들어 줘:
https://dailybible.kr/docs/translation-guide.html

[여기에 본인이 보유한 본문/주석 자료를 붙여넣거나 첨부]

- 역본 이름: (예: 개역한글+내주석)
- 역본 ID: (예: KRV-mynote)
- 페이지의 "기계 판독용 명세(JSON Schema)"와 "추가 규칙"을 기준으로 만들고,
  페이지의 "검증 스크립트(Python)"를 실행해 [오류] 0건을 확인한 뒤에만
  .dbbible.json 파일로 저장해 줘.

받은 파일은 앱의 설정 → 성경 가져오기에서 불러오면 됩니다. (안드로이드는 파일 관리자에서 파일을 탭해 "데일리바이블"로 여는 것도 가능)

JSON을 직접 다루기 어렵다면 역본 스튜디오에서 파일을 열어 화면으로 편집할 수도 있습니다 (열기 → 절·주석·메모 수정 → 내려받기).

파일 형식 요약

UTF-8 인코딩의 단일 JSON 파일이며, 최대 30MB입니다. 최상위 구조:

필드타입설명
formatstring"dailybible-translation@1" 또는 "dailybible-translation@2" (전체 문자열 — "@2"만 쓰면 안 됨). memo 필드나 제2경전 책이 하나라도 있으면 반드시 @2, 아니면 @1(구버전 호환) (필수, 고정값)
idstring역본 고유 ID. 영문/숫자/-/_ 1~32자. 같은 ID로 다시 가져오면 덮어씀
namestring앱에 표시될 이름. 자유 텍스트 (예: "개역한글+내주석")
languagestring언어 코드 ("ko", "en" 등). TTS 언어 선택에 사용
directionstring"ltr" 또는 "rtl"
booksarray포함된 책 목록. 66권 전체가 아니어도 됨(부분 정경 허용)
contentobject책 ID → 본문. books의 ID 집합과 정확히 일치해야 함

books 항목

{ "id": "GEN", "name": "창세기", "nameEn": "Genesis", "testament": "old", "chapters": 50 }

content 항목 (본문)

"GEN": {
  "bookId": "GEN",
  "bookName": "창세기",
  "chapters": [
    {
      "chapter": 1,
      "verses": [
        { "verse": null, "text": "천지 창조" },
        { "verse": 1, "text": "태초에 하나님이 천지를 창조하시니라",
          "note": "히브리어 '바라'(창조하다)는 오직 하나님을 주어로만 쓰인다." },
        { "verse": 2, "text": "땅이 혼돈하고 공허하며 …" }
      ]
    }
  ]
}

주석성경 만들기

공개도메인 본문(예: 개역한글)에 note를 붙이면 나만의 주석성경이 됩니다. 앱에서는 별도 역본으로 나타나 개역한글 ↔ 개역한글+내주석처럼 넘겨가며 읽을 수 있습니다.

66권 표준 책 ID

구약(39권):

GEN EXO LEV NUM DEU JOS JDG RUT 1SA 2SA 1KI 2KI 1CH 2CH EZR NEH EST JOB
PSA PRO ECC SNG ISA JER LAM EZK DAN HOS JOL AMO OBA JON MIC NAH HAB ZEP
HAG ZEC MAL

신약(27권):

MAT MRK LUK JHN ACT ROM 1CO 2CO GAL EPH PHP COL 1TH 2TH 1TI 2TI TIT PHM
HEB JAS 1PE 2PE 1JN 2JN 3JN JUD REV

가톨릭 정경 제2경전(@2 형식 전용) — 구약 영역에 배치되며, 표시 순서는 books 배열 순서를 따릅니다:

TOB JDT WIS SIR BAR 1MA 2MA
ESG LJE S3Y SUS BEL   (다니엘/에스델 추가부 — 본문 통합을 권장, 별도 책도 허용)

책 이름(name·bookName)은 교단 표기 그대로 자유롭게 쓰면 됩니다 — 가톨릭 역본이라면 "마태오 복음서", "탈출기", "요한 묵시록"처럼. ID는 내부 식별자일 뿐이고 화면에는 항상 이 이름이 표시됩니다.

완성 예시 (전체 파일)

{
  "format": "dailybible-translation@1",
  "id": "KRV-mynote",
  "name": "개역한글+내주석",
  "language": "ko",
  "direction": "ltr",
  "books": [
    { "id": "GEN", "name": "창세기", "nameEn": "Genesis", "testament": "old" }
  ],
  "content": {
    "GEN": {
      "bookId": "GEN",
      "bookName": "창세기",
      "chapters": [
        {
          "chapter": 1,
          "verses": [
            { "verse": 1, "text": "태초에 하나님이 천지를 창조하시니라",
              "note": "성경 전체의 서문. 하나님이 만물의 창조주이심을 선언한다." },
            { "verse": 2, "text": "땅이 혼돈하고 공허하며 흑암이 깊음 위에 있고 하나님의 신은 수면에 운행하시니라" }
          ]
        }
      ]
    }
  }
}

기계 판독용 명세 (AI용 JSON Schema)

AI/프로그램용: 파일을 생성하거나 검사할 때는 아래 스키마와 추가 규칙을 기준으로 삼으세요. (위 본문 설명과 동일한 내용의 형식 명세입니다 — 앱 검증기와 동기화됨, 2026-07-07 기준)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "dailybible .dbbible.json",
  "type": "object",
  "required": ["format", "id", "name", "books", "content"],
  "properties": {
    "format": { "enum": ["dailybible-translation@1", "dailybible-translation@2"] },
    "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,32}$" },
    "name": { "type": "string", "minLength": 1 },
    "language": { "type": "string" },
    "direction": { "enum": ["ltr", "rtl"] },
    "books": {
      "type": "array", "minItems": 1,
      "items": {
        "type": "object", "required": ["id"],
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "nameEn": { "type": "string" },
          "testament": { "enum": ["old", "new"] },
          "chapters": { "type": "integer", "minimum": 1 }
        }
      }
    },
    "content": {
      "type": "object", "minProperties": 1,
      "additionalProperties": {
        "type": "object", "required": ["bookId", "chapters"],
        "properties": {
          "bookId": { "type": "string" },
          "bookName": { "type": "string" },
          "chapters": {
            "type": "array", "minItems": 1,
            "items": {
              "type": "object", "required": ["chapter", "verses"],
              "properties": {
                "chapter": { "type": "integer", "minimum": 1 },
                "verses": {
                  "type": "array", "minItems": 1,
                  "items": {
                    "type": "object", "required": ["verse", "text"],
                    "properties": {
                      "verse": { "type": ["integer", "null"], "minimum": 0 },
                      "text": { "type": "string" },
                      "note": { "type": "string" },
                      "memo": { "type": "string" }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

스키마로 표현되지 않는 추가 규칙 — 모두 지켜야 앱이 파일을 받습니다:

  1. content의 키 집합은 books[].id 집합과 정확히 일치 (양방향)
  2. content["X"].bookId는 키 "X"와 동일
  3. 각 책의 chapter1부터 연속 (1, 2, 3, … 건너뛰기·중복 불가)
  4. 책 ID는 66권 표준 목록(위 참조)에 있어야 함 — 제2경전 ID는 @2 형식에서만 허용
  5. memo 필드가 하나라도 있거나 제2경전 책이 있으면 format은 반드시 "dailybible-translation@2"
  6. 파일 전체는 UTF-8, 30MB 이하 (초과 시 책을 나눠 여러 파일로)
  7. verse 특수값: 0 = 시편 표제, null = 번호 없는 소제목 행

검증 스크립트 (Python)

완성한 파일이 올바른지 아래 스크립트로 확인할 수 있습니다 (표준 라이브러리만 사용, Python 3.8+). AI에게 파일을 만들게 했다면, 이 스크립트를 실행해 [오류] 0건을 확인한 결과와 함께 달라고 하세요.

python3 validate_dbbible.py 내파일.dbbible.json

[오류]는 앱이 가져오기를 거부하는 문제, [경고]는 동작하지만 고치면 좋은 문제입니다.

#!/usr/bin/env python3
# 데일리바이블 .dbbible.json 검증 스크립트 (포맷 @1/@2 기준, 앱 검증기와 동일 규칙)
# 사용법: python3 validate_dbbible.py 파일.dbbible.json
# 결과: [오류] = 앱에서 가져오기 실패, [경고] = 동작하지만 권장 수정. 오류 0건이면 exit 0.
import json
import os
import re
import sys

CANON = set(
    "GEN EXO LEV NUM DEU JOS JDG RUT 1SA 2SA 1KI 2KI 1CH 2CH EZR NEH "
    "EST JOB PSA PRO ECC SNG ISA JER LAM EZK DAN HOS JOL AMO OBA JON "
    "MIC NAH HAB ZEP HAG ZEC MAL MAT MRK LUK JHN ACT ROM 1CO 2CO GAL "
    "EPH PHP COL 1TH 2TH 1TI 2TI TIT PHM HEB JAS 1PE 2PE 1JN 2JN 3JN "
    "JUD REV".split()
)
DEUTERO = set("TOB JDT WIS SIR BAR 1MA 2MA ESG LJE S3Y SUS BEL".split())
NT = set(
    "MAT MRK LUK JHN ACT ROM 1CO 2CO GAL EPH PHP COL 1TH 2TH 1TI 2TI "
    "TIT PHM HEB JAS 1PE 2PE 1JN 2JN 3JN JUD REV".split()
)
ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
MAX_BYTES = 30 * 1024 * 1024

errors = []
warnings = []


def err(msg):
    errors.append(msg)


def warn(msg):
    warnings.append(msg)


def is_int(v):
    return isinstance(v, int) and not isinstance(v, bool)


def finish(stats=None):
    for w in warnings:
        print("[경고]", w)
    for e in errors:
        print("[오류]", e)
    if errors:
        print("\n검증 실패 — 오류 %d건 (앱에서 가져오기가 거부됩니다)" % len(errors))
        sys.exit(1)
    if stats:
        print("검증 통과 ✓  책 %d권 · %d장 · %d절 (경고 %d건)" % (*stats, len(warnings)))
    sys.exit(0)


def check_verses(bid, ch_num, verses, state):
    if not isinstance(verses, list) or not verses:
        err("%s %d장: verses 배열이 비었습니다." % (bid, ch_num))
        return
    seen_nums = set()
    for v in verses:
        verse_ok = isinstance(v, dict) and (
            v.get("verse") is None or (is_int(v.get("verse")) and v["verse"] >= 0)
        )
        if not verse_ok or not isinstance(v.get("text"), str):
            err("%s %d장: 절 형식 오류 — verse는 null/0/양의 정수, text는 문자열이어야 합니다." % (bid, ch_num))
            return
        state["verses"] += 1
        if is_int(v.get("verse")):
            if v["verse"] in seen_nums:
                warn("%s %d장: 절 번호 중복(%d)" % (bid, ch_num, v["verse"]))
            seen_nums.add(v["verse"])
        if not v["text"].strip():
            warn("%s %d장 %s절: text가 비어 있습니다." % (bid, ch_num, v.get("verse")))
        if isinstance(v.get("memo"), str) and v["memo"].strip():
            state["needs_v2"] = True


def main(path):
    if not os.path.isfile(path):
        err("파일을 찾을 수 없습니다: %s" % path)
        finish()
    if os.path.getsize(path) > MAX_BYTES:
        err("파일이 30MB를 초과합니다 — 책을 나눠 여러 파일로 만드세요.")
    try:
        with open(path, encoding="utf-8", errors="strict") as f:
            raw = json.load(f)
    except UnicodeDecodeError:
        err("UTF-8 인코딩이 아닙니다.")
        finish()
    except json.JSONDecodeError as e:
        err("JSON 문법 오류: %s" % e)
        finish()
    if not isinstance(raw, dict):
        err("최상위가 JSON 객체가 아닙니다.")
        finish()

    fmt = raw.get("format")
    is_v2 = fmt == "dailybible-translation@2"
    if fmt not in ("dailybible-translation@1", "dailybible-translation@2"):
        err('format 값 오류: %r — 정확히 "dailybible-translation@1" 또는 "dailybible-translation@2"' % fmt)

    tid = raw.get("id")
    if not isinstance(tid, str) or not ID_RE.match(tid):
        err("id: 영문/숫자/-/_ 조합 1~32자여야 합니다.")
    if not isinstance(raw.get("name"), str) or not raw["name"].strip():
        err("name(역본 이름)이 비어 있습니다.")
    if not isinstance(raw.get("language"), str) or not raw.get("language"):
        warn("language가 없습니다 — TTS 언어 선택에 쓰이므로 'ko', 'en' 등을 권장합니다.")
    if raw.get("direction") not in ("ltr", "rtl"):
        warn("direction이 'ltr'/'rtl'이 아니어서 앱이 'ltr'로 처리합니다.")

    books = raw.get("books")
    content = raw.get("content")
    if not isinstance(books, list) or not books:
        err("books 배열이 비었습니다.")
        finish()
    if not isinstance(content, dict):
        err("content 객체가 없습니다.")
        finish()

    state = {"verses": 0, "needs_v2": False}
    seen = set()
    for i, b in enumerate(books):
        if not isinstance(b, dict) or not isinstance(b.get("id"), str):
            err("books[%d]: 객체 형식이 아니거나 id가 없습니다." % i)
            continue
        bid = b["id"]
        if bid in DEUTERO:
            state["needs_v2"] = True
        elif bid not in CANON:
            err("books[%d]: 알 수 없는 책 ID '%s' — 표준 ID(대문자, 예: GEN)를 쓰세요." % (i, bid))
        if bid in seen:
            err("중복된 책 ID: %s" % bid)
        seen.add(bid)
        t = b.get("testament")
        if t not in ("old", "new"):
            warn("%s: testament가 없거나 잘못돼 앱이 'old'로 처리합니다." % bid)
        elif bid in NT and t != "new":
            warn("%s: 신약 책인데 testament가 'new'가 아닙니다." % bid)
        elif (bid in CANON and bid not in NT or bid in DEUTERO) and t != "old":
            warn("%s: 구약 영역 책인데 testament가 'old'가 아닙니다." % bid)

    ckeys = set(content.keys())
    if ckeys != seen:
        only_books = sorted(seen - ckeys)
        only_content = sorted(ckeys - seen)
        if only_books:
            err("books에만 있고 content에 없는 책: %s" % ", ".join(only_books))
        if only_content:
            err("content에만 있고 books에 없는 책: %s" % ", ".join(only_content))

    n_chapters = 0
    for bid in sorted(ckeys & seen):
        book = content[bid]
        if not isinstance(book, dict):
            err("%s: content 값이 객체가 아닙니다." % bid)
            continue
        if book.get("bookId") != bid:
            err("%s: bookId(%r)가 content 키와 일치하지 않습니다." % (bid, book.get("bookId")))
        chapters = book.get("chapters")
        if not isinstance(chapters, list) or not chapters:
            err("%s: chapters 배열이 비었습니다." % bid)
            continue
        prev = 0
        for c in chapters:
            if not isinstance(c, dict) or not is_int(c.get("chapter")) or c["chapter"] < 1:
                err("%s: 장 번호가 양의 정수가 아닙니다." % bid)
                break
            if c["chapter"] != prev + 1:
                err("%s: 장 번호는 1부터 연속이어야 합니다 (%d장 다음이 %s장)." % (bid, prev, c["chapter"]))
                break
            prev = c["chapter"]
            n_chapters += 1
            check_verses(bid, prev, c.get("verses"), state)

    if state["needs_v2"] and fmt == "dailybible-translation@1":
        err('memo 필드나 제2경전 책이 있으면 format이 "dailybible-translation@2"여야 합니다.')
    if is_v2 and not state["needs_v2"]:
        warn("@2 전용 요소(memo·제2경전)가 없습니다 — 구버전 호환을 위해 @1을 권장합니다.")

    finish(stats=(len(seen), n_chapters, state["verses"]))


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("사용법: python3 validate_dbbible.py 파일.dbbible.json")
        sys.exit(2)
    main(sys.argv[1])

자주 하는 실수

이야기 성경 만들기 (.dbstory.json)

여기서부터는 이야기 성경 파일에 대한 설명입니다. 위에서 다룬 역본(.dbbible.json)과는 형식이 완전히 다릅니다 — 섞어 쓸 수 없습니다.

이야기 성경은 성경을 이야기 단위로 다시 쓴 글입니다. 절 번호가 없고, "본문을 옮긴 것"이 아니라 "다시 쓴 것"을 담습니다. 어린이용 이야기 성경, 새신자용 쉬운 이야기, 주일학교 교재 같은 것을 만들 때 씁니다.

역본 .dbbible.json이야기 성경 .dbstory.json
단위66권 / 장 / 절이야기 / 문단 (절 번호 없음)
앱에서성경 리더별도 이야기 리더로 열림
형광펜·메모·검색·묶음공유지원지원 안 함
음성 낭독(TTS)지원 (절 단위)지원 (문단 단위)
원문으로 가기출처(ref)를 탭하면 성경 본문으로 이동
삽화넣을 수 없음 (아래 참조)
최대 크기30MB5MB

번역본 선택 목록에서는 이야기 태그가 붙어 표시되며, 고르면 역본이 바뀌는 것이 아니라 이야기 리더가 열립니다.

이야기 파일 형식 요약

UTF-8 인코딩의 단일 JSON 파일이며, 최대 5MB입니다. 최상위 구조:

필드타입설명
formatstring"dailybible-story@1" (필수, 고정값 — 전체 문자열)
idstring컬렉션 고유 ID. 영문/숫자/-/_ 1~32자. 같은 ID로 다시 가져오면 덮어씀. 앱 기본 제공 ID(KIDS)는 쓸 수 없음
namestring앱에 표시될 이름 (필수). 예: "우리 교회 어린이 이야기"
languagestring언어 코드. 생략하면 "ko"로 처리. 음성 낭독 언어에 사용
descriptionstring선택. 목차 상단에 표시되는 한 줄 설명
licensestring선택. 예: "CC BY-SA 4.0"
styleProfilestring선택. 집필 기준 이름(예: "kids"). 앱 동작에는 영향 없음
storiesarray이야기 목록. 1편 이상 (필수)

stories 항목 (이야기)

{
  "id": "noah",
  "title": "노아와 큰 배",
  "ref": "GEN 6:5-9:17",
  "paragraphs": [
    { "text": "하나님이 세상을 내려다보셨어요. 사람들이 나쁜 일만 하고 있었어요.", "ref": "GEN 6:5-6" },
    { "text": "그런데 노아는 달랐어요. 노아는 하나님 말씀을 잘 들었어요.", "ref": "GEN 6:8-9" }
  ]
}
image 필드는 넣을 수 없습니다. 이야기에 image가 하나라도 있으면 앱이 파일 전체를 거부합니다. 삽화는 앱이 기본 제공하는 이야기에만 들어갑니다 — 가져온 파일이 그림을 실어 나르는 통로가 되면 남의 그림이 유통되기 때문입니다. 텍스트만 담아 주세요.

paragraphs 항목 (문단)

{ "text": "하나님이 말씀하셨어요. \"빛이 있어라.\" 그러자 빛이 생겼어요.", "ref": "GEN 1:3-4" }
문단은 배열 순서로만 식별됩니다. 이야기 성경이 형광펜·메모를 지원하지 않는 이유이기도 합니다 — 나중에 문단을 하나 끼워 넣으면 뒤 문단의 위치가 전부 밀리기 때문입니다.

출처(ref) 쓰는 법

가장 실수가 잦은 부분입니다. 책 ID는 위의 66권 표준 ID를 씁니다 (제2경전 ID는 이야기 출처로 쓸 수 없습니다).

쓰는 법
"GEN 1:1-5"창세기 1장 1~5절
"GEN 1:1"창세기 1장 1절 (한 절)
"GEN 6:5-9:17"창세기 6장 5절 ~ 9장 17절 (장을 걸치는 범위)
"GEN 7:12,17-18"창세기 7장 12절, 17~18절 (불연속 절)
"LUK 23:44,46;JHN 19:30"누가복음 23장 44·46절 요한복음 19장 30절 (여러 책 — 세미콜론으로 연결)

거부되는 것 — 하나라도 걸리면 파일 전체를 가져오지 못합니다:

하이픈은 ASCII - 를 권합니다. 문서 편집기가 하이픈을 en대시()로 자동 변환하는 일이 흔한데, en대시·em대시()·물결표(~)도 범위 구분자로 인정하므로 "GEN 6:5–9:17" 도 정상 동작합니다.

앱은 문법만 검사합니다 — 그 장·절이 실제로 존재하는지는 확인하지 않습니다. "GEN 99:1"은 형식상 통과하지만 "원문으로 가기"가 제대로 동작하지 않습니다.

완성 예시 (전체 파일)

{
  "format": "dailybible-story@1",
  "id": "MYCHURCH-KIDS",
  "name": "우리 교회 어린이 이야기",
  "language": "ko",
  "description": "성경 이야기를 어린이가 읽기 쉽게 다시 쓴 글이에요.",
  "license": "CC BY-SA 4.0",
  "styleProfile": "kids",
  "stories": [
    {
      "id": "creation",
      "title": "하나님이 세상을 만드셨어요",
      "ref": "GEN 1:1-2:3",
      "paragraphs": [
        { "text": "아주 오래오래전이었어요. 하나님이 하늘과 땅을 만드셨어요.", "ref": "GEN 1:1-2" },
        { "text": "하나님이 말씀하셨어요. \"빛이 있어라.\" 그러자 빛이 생겼어요.", "ref": "GEN 1:3-4" }
      ]
    },
    {
      "id": "noah",
      "title": "노아와 큰 배",
      "ref": "GEN 6:5-9:17",
      "paragraphs": [
        { "text": "하나님이 노아에게 말씀하셨어요. \"아주 큰 배를 만들어라.\"", "ref": "GEN 6:13-14" },
        { "text": "하늘에 무지개가 떴어요. 무지개는 하나님이 하신 약속의 표시예요.", "ref": "GEN 9:13-15" }
      ]
    }
  ]
}

AI에게 이야기 성경 만들어 달라고 하기

ChatGPT·Claude 등에게 아래처럼 요청하세요.

다음 가이드 페이지의 "이야기 성경 만들기" 부분을 읽고 포맷을 정확히 지켜서
데일리바이블 이야기 파일(.dbstory.json)을 만들어 줘:
https://dailybible.kr/docs/translation-guide.html#story

- 컬렉션 이름: (예: 우리 교회 어린이 이야기)
- 컬렉션 ID: (예: MYCHURCH-KIDS)   ← KIDS 는 앱이 쓰는 ID라 사용 불가
- 대상 독자: (예: 초등 저학년)
- 만들 이야기 목록: (예: 창조 / 노아의 방주 / 다윗과 골리앗 …)

저본 규칙 (반드시 지킬 것):
- 개역한글 등 저작권이 없는 본문만 참고할 것.
- 저작권이 있는 역본이나 시중에 나온 어린이 성경 본문을 넣지 말 것.
- 어느 한 역본의 문장 구조를 그대로 따라가지 말 것.

문체 규칙: 아래 "문체 지침"을 지킬 것.
출처 규칙: 각 이야기와 각 문단에 ref 를 넣을 것. 페이지의 "출처(ref) 쓰는 법"을 정확히 따를 것.

페이지의 "기계 판독용 명세(이야기 JSON Schema)"와 "추가 규칙"을 기준으로 만들고,
페이지의 "검증 스크립트(Python)"를 실행해 [오류] 0건을 확인한 뒤에만
.dbstory.json 파일로 저장해 줘.
저작권 — 이야기 성경도 같습니다. 저작권이 있는 역본이나 시중에 판매되는 어린이 성경 본문을 AI에게 넣어 "쉽게 고쳐 줘"라고 하면 그 결과물은 파생 저작물이 되어 본인 것이 아닙니다. 배포·공유할 수 없습니다. 공개도메인 본문(예: 개역한글)을 저본으로 새로 쓰게 하세요.

문체 지침 (어린이 대상일 때)

아래는 앱이 검사하지 않는 권장 기준입니다 (검증 스크립트는 [경고]로만 알려 줍니다). 그래도 AI에게 그대로 붙여 주면 결과가 크게 좋아집니다.

- 한 문장은 35자 이내, 한 문단은 2~4문장.
- 대명사 대신 이름을 반복한다. ("그가" → "모세가")
- 능동태, 과거형("~했어요"). 이중부정 금지.
- 어려운 한자어를 쉬운 말로: 의롭다 → 바르다 / 언약 → 약속 / 죄 → 나쁜 일
- 신학 용어는 첫 등장에서 한 문장으로 풀어 준다.
  예: "예수님이 다시 살아나셨어요. 이걸 부활이라고 해요."
- 죽음은 흐리지 말고 직접 말한다. "죽었어요", "돌아가셨어요".
  "잠들었어요", "하늘나라로 갔어요" 같은 완곡어법은 쓰지 않는다.
- 다만 피·상처 같은 잔혹한 묘사는 하지 않는다. 죽음은 한 문장으로 말하고 넘어간다.
- 죽음을 이야기의 마지막 문단으로 끝내지 않는다. (홍수 뒤엔 무지개, 십자가 뒤엔 부활)
문장 35자·문단 2~4문장은 연구 근거가 있는 값이 아니라 권장 기준입니다. 아이가 읽다 막히면 기준이 아니라 문장을 고치세요.
완곡어법을 쓰지 않는 것은 근거가 있습니다 — 아동 사별 도서 210권을 분석한 연구에서 약 1/4이 "잠들었다" 류 표현을 썼고, 연구진은 그것이 명료함이 아니라 혼란을 낳는다고 지적했습니다.

기계 판독용 명세 (이야기 JSON Schema)

AI/프로그램용: 파일을 생성하거나 검사할 때는 아래 스키마와 추가 규칙을 기준으로 삼으세요. (앱 검증기와 동기화됨, 2026-07-13 기준)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "dailybible .dbstory.json",
  "type": "object",
  "required": ["format", "id", "name", "stories"],
  "properties": {
    "format": { "const": "dailybible-story@1" },
    "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,32}$" },
    "name": { "type": "string", "minLength": 1 },
    "language": { "type": "string" },
    "description": { "type": "string" },
    "license": { "type": "string" },
    "styleProfile": { "type": "string" },
    "stories": {
      "type": "array", "minItems": 1,
      "items": {
        "type": "object",
        "required": ["id", "title", "paragraphs"],
        "not": { "required": ["image"] },
        "properties": {
          "id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,32}$" },
          "title": { "type": "string", "minLength": 1 },
          "ref": { "type": "string" },
          "paragraphs": {
            "type": "array", "minItems": 1,
            "items": {
              "type": "object",
              "required": ["text"],
              "properties": {
                "text": { "type": "string", "minLength": 1 },
                "ref": { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}

스키마로 표현되지 않는 추가 규칙 — 모두 지켜야 앱이 파일을 받습니다:

  1. format은 정확히 "dailybible-story@1". 역본 형식(dailybible-translation@1)을 쓰면 안 됩니다
  2. 컬렉션 id앱 기본 제공 이야기 ID와 겹치면 안 됨 (현재: KIDS)
  3. 이야기 id는 컬렉션 안에서 중복 불가
  4. 이야기에 image 필드가 있으면 파일 전체 거부
  5. ref는 "출처(ref) 쓰는 법"의 문법을 정확히 지켜야 하며, 세미콜론으로 이은 조각 중 하나라도 읽을 수 없으면 거부. 책 ID는 66권 정경만 (제2경전 불가)
  6. text는 공백만 있는 문자열 불가
  7. 파일 전체는 UTF-8, 5MB 이하
  8. 명세에 없는 필드는 무시됩니다(오류 아님) — 단 이야기의 image만 예외로 오류

검증 스크립트 (Python) — 이야기

완성한 파일이 올바른지 아래 스크립트로 확인할 수 있습니다 (표준 라이브러리만 사용, Python 3.8+). AI에게 파일을 만들게 했다면, 이 스크립트를 실행해 [오류] 0건을 확인한 결과와 함께 달라고 하세요.

python3 validate_dbstory.py 내파일.dbstory.json

[오류]는 앱이 가져오기를 거부하는 문제, [경고]는 동작하지만 고치면 좋은 문제입니다 (문체 경고는 styleProfile"kids"일 때만 표시됩니다).

#!/usr/bin/env python3
# 데일리바이블 .dbstory.json 검증 스크립트 (dailybible-story@1, 앱 검증기와 동일 규칙)
# 사용법: python3 validate_dbstory.py 파일.dbstory.json
# 결과: [오류] = 앱에서 가져오기 실패, [경고] = 동작하지만 권장 수정. 오류 0건이면 exit 0.
import json
import os
import re
import sys

CANON = set(
    "GEN EXO LEV NUM DEU JOS JDG RUT 1SA 2SA 1KI 2KI 1CH 2CH EZR NEH "
    "EST JOB PSA PRO ECC SNG ISA JER LAM EZK DAN HOS JOL AMO OBA JON "
    "MIC NAH HAB ZEP HAG ZEC MAL MAT MRK LUK JHN ACT ROM 1CO 2CO GAL "
    "EPH PHP COL 1TH 2TH 1TI 2TI TIT PHM HEB JAS 1PE 2PE 1JN 2JN 3JN "
    "JUD REV".split()
)
RESERVED_IDS = {"KIDS"}          # 앱이 기본 제공하는 이야기 컬렉션 ID
ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
PART_RE = re.compile(r"^([1-3]?[A-Za-z]{2,3})\s+(\d+):(.+)$")
CROSS_RE = re.compile(r"^(\d+)\s*[-–—−~〜~]\s*(\d+):(\d+)$")   # 앱과 같은 대시 집합
RANGE_RE = re.compile(r"^(\d+)\s*[-–—−~〜~]\s*(\d+)$")
MAX_BYTES = 5 * 1024 * 1024
MAX_SENT_CHARS = 35              # 문체 권장값(앱은 검사하지 않음)
MAX_PARA_SENTS = 4
EUPHEMISMS = ["잠들었어요", "잠들었다", "하늘나라로 갔어요", "먼 길을 떠났어요", "떠나갔어요", "눈을 감았어요"]

errors = []
warnings = []


def err(msg):
    errors.append(msg)


def warn(msg):
    warnings.append(msg)


def finish(stats=None):
    for w in warnings:
        print("[경고]", w)
    for e in errors:
        print("[오류]", e)
    if errors:
        print("\n검증 실패 — 오류 %d건 (앱에서 가져오기가 거부됩니다)" % len(errors))
        sys.exit(1)
    if stats:
        print("검증 통과 ✓  이야기 %d편 · 문단 %d개 · %d자 (경고 %d건)" % (*stats, len(warnings)))
    sys.exit(0)


def parse_verse_spec(spec):
    nums = set()
    for part in spec.split(","):
        part = part.strip()
        if not part:
            continue
        m = RANGE_RE.match(part)
        if m:
            a, b = int(m.group(1)), int(m.group(2))
            lo, hi = min(a, b), max(a, b)
            hi = min(hi, lo + 200)
            nums.update(range(lo, hi + 1))
            continue
        m2 = re.search(r"\d+", part)
        if m2:
            nums.add(int(m2.group(0)))
    return sorted(nums)


def check_ref(ref, where):
    """앱의 ref 검증기와 같은 규칙. 문제가 있으면 오류를 쌓는다."""
    chunks = [c.strip() for c in ref.split(";") if c.strip()]
    if not chunks:
        err("%s: ref 가 비어 있습니다." % where)
        return
    for chunk in chunks:
        m = PART_RE.match(chunk)
        if not m:
            err('%s: 출처를 읽을 수 없습니다 — "%s"' % (where, chunk))
            return
        bid = m.group(1).upper()
        if bid not in CANON:
            err('%s: 66권에 없는 책 ID "%s" (제2경전은 이야기 출처로 쓸 수 없습니다)' % (where, bid))
            return
        chapter = int(m.group(2))
        if chapter < 1:
            err("%s: 장 번호는 1 이상이어야 합니다 — %s" % (where, chunk))
            return
        spec = m.group(3).strip()
        cm = CROSS_RE.match(spec)
        if cm:
            start_v, end_ch, end_v = int(cm.group(1)), int(cm.group(2)), int(cm.group(3))
            if start_v < 1 or end_v < 1:
                err("%s: 절 번호는 1 이상이어야 합니다 — %s" % (where, chunk))
                return
            if end_ch <= chapter:
                err("%s: 끝 장이 시작 장보다 커야 합니다 — %s (같은 장이면 'GEN 1:1-5' 형태로)" % (where, chunk))
                return
            continue
        verses = parse_verse_spec(spec)
        if not verses or verses[0] < 1:
            err("%s: 절 번호를 읽을 수 없습니다 (0절 불가) — %s" % (where, chunk))
            return


def split_sentences(text):
    """큰따옴표를 아는 문장 분할 — 대사가 있어도 길이를 제대로 잰다."""
    out, buf = [], ""
    for ch in text:
        if ch in '"“”':
            if buf.strip():
                out.append(buf.strip())
            buf = ""
            continue
        buf += ch
        if ch in ".!?":
            if buf.strip():
                out.append(buf.strip())
            buf = ""
    if buf.strip():
        out.append(buf.strip())
    return out


def check_style(story_id, idx, text):
    sents = split_sentences(text)
    if len(sents) > MAX_PARA_SENTS:
        warn("%s 문단 %d: %d문장 (권장 %d문장 이하)" % (story_id, idx, len(sents), MAX_PARA_SENTS))
    for s in sents:
        if len(s) > MAX_SENT_CHARS:
            warn('%s 문단 %d: %d자 문장 (권장 %d자 이하) — "%s"' % (story_id, idx, len(s), MAX_SENT_CHARS, s))
    for e in EUPHEMISMS:
        if e in text:
            warn('%s 문단 %d: 죽음을 흐리는 표현 "%s" — 직접 말하세요("죽었어요").' % (story_id, idx, e))


def main(path):
    if not os.path.isfile(path):
        err("파일을 찾을 수 없습니다: %s" % path)
        finish()
    if os.path.getsize(path) > MAX_BYTES:
        err("파일이 5MB를 초과합니다 — 이야기를 나눠 여러 파일로 만드세요.")
    try:
        with open(path, encoding="utf-8", errors="strict") as f:
            raw = json.load(f)
    except UnicodeDecodeError:
        err("UTF-8 인코딩이 아닙니다.")
        finish()
    except json.JSONDecodeError as e:
        err("JSON 문법 오류: %s" % e)
        finish()
    if not isinstance(raw, dict):
        err("최상위가 JSON 객체가 아닙니다.")
        finish()

    fmt = raw.get("format")
    if fmt != "dailybible-story@1":
        if isinstance(fmt, str) and fmt.startswith("dailybible-translation@"):
            err("이건 역본 형식입니다. 이야기 파일의 format 은 정확히 \"dailybible-story@1\" 이어야 합니다.")
        else:
            err('format 값 오류: %r — 정확히 "dailybible-story@1"' % fmt)

    cid = raw.get("id")
    if not isinstance(cid, str) or not ID_RE.match(cid):
        err("id: 영문/숫자/-/_ 조합 1~32자여야 합니다.")
    elif cid in RESERVED_IDS:
        err("id '%s' 는 앱이 기본 제공하는 이야기 ID라 쓸 수 없습니다 — 다른 ID를 쓰세요." % cid)

    if not isinstance(raw.get("name"), str) or not raw["name"].strip():
        err("name(컬렉션 이름)이 비어 있습니다.")
    if not isinstance(raw.get("language"), str) or not raw.get("language"):
        warn("language 가 없습니다 — 앱이 'ko'로 처리합니다. 음성 낭독 언어에 쓰이므로 명시를 권장합니다.")

    stories = raw.get("stories")
    if not isinstance(stories, list) or not stories:
        err("stories 배열이 비었습니다.")
        finish()

    kids = raw.get("styleProfile") == "kids"
    seen = set()
    n_paras = 0
    n_chars = 0
    for i, s in enumerate(stories):
        if not isinstance(s, dict):
            err("stories[%d]: 객체가 아닙니다." % i)
            continue
        sid = s.get("id")
        if not isinstance(sid, str) or not ID_RE.match(sid):
            err("stories[%d]: 이야기 id 가 올바르지 않습니다(영문/숫자/-/_ 1~32자)." % i)
            continue
        if sid in seen:
            err("중복된 이야기 ID: %s" % sid)
        seen.add(sid)

        if not isinstance(s.get("title"), str) or not s["title"].strip():
            err("%s: title 이 비어 있습니다." % sid)
        if "image" in s:
            err("%s: 가져온 이야기에는 image 를 넣을 수 없습니다 — 필드를 지우세요." % sid)

        if isinstance(s.get("ref"), str) and s["ref"].strip():
            check_ref(s["ref"].strip(), "%s(이야기 출처)" % sid)
        else:
            warn("%s: 이야기 ref 가 없습니다 — 넣으면 '원문으로 가기' 버튼이 생깁니다." % sid)

        paras = s.get("paragraphs")
        if not isinstance(paras, list) or not paras:
            err("%s: paragraphs 배열이 비었습니다." % sid)
            continue
        for j, p in enumerate(paras, start=1):
            if not isinstance(p, dict) or not isinstance(p.get("text"), str) or not p["text"].strip():
                err("%s 문단 %d: text 가 비었거나 문자열이 아닙니다." % (sid, j))
                continue
            n_paras += 1
            n_chars += len(p["text"])
            if isinstance(p.get("ref"), str) and p["ref"].strip():
                check_ref(p["ref"].strip(), "%s 문단 %d" % (sid, j))
            if kids:
                check_style(sid, j, p["text"])

    finish(stats=(len(seen), n_paras, n_chars))


if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("사용법: python3 validate_dbstory.py 파일.dbstory.json")
        sys.exit(2)
    main(sys.argv[1])

앱에서 쓰는 법

이야기 성경에서 자주 하는 실수

데일리바이블 · dailybible.kr · Soli Deo Gloria