')
+ }
+ closeList()
+ return out.join('\n')
+}
+
function renderStep2() {
setActiveStep(2)
clearPage()
@@ -193,7 +358,7 @@ function renderStep2() {
if (state.mode === 'multi') renderStep2Role()
else renderStep4()
})
- section.querySelector('#back').addEventListener('click', renderStep1)
+ section.querySelector('#back').addEventListener('click', renderAgreement)
}
function renderStep2Role() {
diff --git a/installer/styles.css b/installer/styles.css
index be0f6bf..9a0f92a 100644
--- a/installer/styles.css
+++ b/installer/styles.css
@@ -155,6 +155,49 @@ main {
.toggleRow { display: flex; align-items: center; gap: 10px; margin: 8px 0; }
+/* 약관 동의 페이지 — 탭 + 약관 본문 박스. */
+.tabBar { display: flex; gap: 6px; margin: 12px 0 0; flex-wrap: wrap; }
+.tabBtn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 6px 14px;
+ border-radius: 8px 8px 0 0;
+ cursor: pointer;
+ font-size: 13px;
+}
+.tabBtn.active {
+ background: var(--bg-card);
+ border-bottom-color: var(--bg-card);
+ color: var(--accent, #6cf);
+ font-weight: 600;
+}
+.agreementBody {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ padding: 14px 18px;
+ border-radius: 0 10px 10px 10px;
+ max-height: 320px;
+ overflow-y: auto;
+ font-size: 13px;
+ line-height: 1.65;
+}
+.agreementBody h1, .agreementBody h2, .agreementBody h3 { margin: 12px 0 6px; }
+.agreementBody h1 { font-size: 17px; }
+.agreementBody h2 { font-size: 15px; }
+.agreementBody h3 { font-size: 14px; }
+.agreementBody p { margin: 6px 0; }
+.agreementBody ul, .agreementBody ol { margin: 6px 0; padding-left: 22px; }
+.agreementBody li { margin: 2px 0; }
+.agreementBody code { background: rgba(255,255,255,0.08); padding: 1px 4px; border-radius: 3px; font-family: 'Consolas', monospace; }
+.agreementBody pre { background: rgba(0,0,0,0.3); padding: 8px 10px; border-radius: 6px; overflow-x: auto; }
+.agreementBody pre code { background: none; padding: 0; }
+.agreementBody blockquote { margin: 6px 0; padding-left: 10px; border-left: 3px solid var(--border); color: #aab; }
+.agreementBody details { margin: 6px 0; }
+.agreementBody details > summary { cursor: pointer; padding: 4px 0; }
+.agreementBody hr { border: none; border-top: 1px solid var(--border); margin: 10px 0; }
+.agreementBody a { color: var(--accent, #6cf); }
+
.modalOverlay {
position: fixed;
inset: 0;
diff --git a/locales/installer-rp/ko-kr.json b/locales/installer-rp/ko-kr.json
index fd927bc..07436e3 100644
--- a/locales/installer-rp/ko-kr.json
+++ b/locales/installer-rp/ko-kr.json
@@ -14,6 +14,7 @@
},
"common": {
"next": "다음",
+ "back": "이전",
"cancel": "취소",
"confirm": "확인",
"openFolder": "리소스팩 폴더 열기",
@@ -30,6 +31,17 @@
"step1": {
"heading": "음악퀴즈 선택"
},
+ "agreement": {
+ "heading": "약관 동의",
+ "intro": "리소스팩을 설치하기 전에 아래 약관을 모두 확인하고 동의해 주세요.",
+ "tabResourcepack": "리소스팩 약관",
+ "tabInstaller": "리소스팩 설치기 약관",
+ "loading": "약관을 불러오는 중...",
+ "loadFailed": "약관 로드 실패: {{message}}",
+ "agreeAll": "위 모든 약관(리소스팩·설치기)에 동의합니다.",
+ "agreeRequired": "약관에 동의해야 다음 단계로 진행할 수 있습니다.",
+ "cancelling": "취소 중…"
+ },
"step2": {
"heading": "리소스팩 설치",
"description": "음악·사진을 받아 리소스팩을 만들고 %appdata%/.mc_custom/resourcepacks/ 에 자동 설치합니다.",
diff --git a/locales/installer/ko-kr.json b/locales/installer/ko-kr.json
index a49d90b..81b63a0 100644
--- a/locales/installer/ko-kr.json
+++ b/locales/installer/ko-kr.json
@@ -30,6 +30,17 @@
"logViewer": {
"title": "설치 로그"
},
+ "agreement": {
+ "heading": "약관 동의",
+ "intro": "설치 전에 아래 약관을 모두 확인하고 동의해 주세요.",
+ "tabMap": "맵 약관",
+ "tabMod": "모드 약관",
+ "tabInstaller": "설치기 약관",
+ "loading": "약관을 불러오는 중...",
+ "loadFailed": "약관 로드 실패: {{message}}",
+ "agreeAll": "위 모든 약관(맵·모드·설치기)에 동의합니다.",
+ "agreeRequired": "약관에 동의해야 다음 단계로 진행할 수 있습니다."
+ },
"step1": {
"heading": "설치할 음악퀴즈 선택",
"loading": "목록을 불러오는 중...",
diff --git a/locales/server/ko-kr.json b/locales/server/ko-kr.json
index a3a6972..eac1bd8 100644
--- a/locales/server/ko-kr.json
+++ b/locales/server/ko-kr.json
@@ -37,6 +37,7 @@
"browserTitle": "관리자 대시보드",
"editList": "음악목록 수정",
"editDatapack": "데이터팩 수정",
+ "editTerms": "약관 수정",
"addPack": "음악퀴즈 추가",
"deletePack": "음악퀴즈 삭제",
"emptyHint": "등록된 음악퀴즈가 없습니다. \"음악퀴즈 추가\" 버튼으로 새로 만들어 보세요.",
@@ -132,6 +133,31 @@
"ramOrderInvalid": "클라이언트 최소 램은 권장 램보다 클 수 없습니다.",
"fabricLoaderRequired": "Fabric 로더 버전을 선택해 주세요."
},
+ "terms": {
+ "browserTitle": "약관 수정",
+ "title": "약관 수정",
+ "hint": "수정할 약관을 선택하세요. 사이트에서 저장한 내용은 인스톨러가 약관 동의 화면에서 사용합니다.",
+ "editorBrowserTitle": "{{label}} 편집",
+ "editorTitle": "{{label}}",
+ "save": "약관 저장",
+ "saving": "저장 중…",
+ "saved": "저장 완료",
+ "saveFailed": "저장 실패: {{message}}",
+ "preview": "미리보기",
+ "edit": "편집",
+ "slashHint": "/ 를 입력해 블록 종류를 선택하거나 #, - 를 직접 입력할 수 있습니다.",
+ "slashHeading1": "큰 제목",
+ "slashHeading2": "중간 제목",
+ "slashHeading3": "작은 제목",
+ "slashText": "내용",
+ "slashBullet": "글머리 기호",
+ "slashNumbered": "번호 매기기",
+ "slashToggle": "토글",
+ "slashDivider": "구분선",
+ "slashQuote": "인용",
+ "slashCode": "코드",
+ "leaveConfirm": "저장하지 않은 변경사항이 있습니다.\n저장 없이 이 페이지를 떠나시겠습니까?"
+ },
"datapack": {
"browserTitle": "데이터팩 수정",
"title": "데이터팩 수정",
diff --git a/manifest/terms/installer-rp.md b/manifest/terms/installer-rp.md
new file mode 100644
index 0000000..2e72991
--- /dev/null
+++ b/manifest/terms/installer-rp.md
@@ -0,0 +1,27 @@
+# 리소스팩 설치기(exe) 안내 및 약관
+
+**1.** 이 설치기는 리소스팩(음악·사진)의 간편한 빌드 및 설치를 위한 프로그램입니다.
+- 설치기를 통해 설치되는 리소스팩은 리소스팩 약관을 따릅니다.
+- 설치기 사용 전 리소스팩 약관을 반드시 확인하세요.
+
+**2.** 이 설치기는 반드시 개인이 사용하여야 하며 비영리적인 목적으로 사용하여야 합니다.
+- 어떠한 경우에도 영리적인 목적으로 사용할 수 없으며, 허가를 받을 수도 없습니다.
+
+**3.** 설치기에 대한 2차 창작 및 2차 배포는 금지됩니다.
+- 소스코드의 무단 복제, 수정, 재배포는 엄격히 금지됩니다.
+- 설치기를 타 플랫폼 또는 제3자에게 재배포하는 행위는 금지됩니다.
+
+**4.** 설치기의 소스코드는 GitHub를 통해 공개되어 있습니다.
+- 소스코드 열람은 허용되나, 이를 기반으로 한 파생 프로그램 제작 및 배포는 금지됩니다.
+- 버그 제보 및 기여는 허용됩니다.
+
+**5.** 설치기는 음악·이미지 다운로드를 위해 외부 도구(yt-dlp, ffmpeg)를 자동으로 받아 사용합니다. 각 도구는 해당 프로젝트의 라이선스를 따릅니다.
+- yt-dlp: https://github.com/yt-dlp/yt-dlp
+- ffmpeg: https://ffmpeg.org/
+
+**6.** 이 설치기의 저작권은 제작자에게 있으며, 무단 사용 시 저작권법에 의해 처벌받을 수 있습니다.
+
+Copyright (c) 2026. All rights reserved.
+
+This software is protected under a Custom License.
+Unauthorized reproduction, modification, or distribution of this software is strictly prohibited and may result in legal action under applicable copyright laws.
diff --git a/manifest/terms/installer.md b/manifest/terms/installer.md
new file mode 100644
index 0000000..02e94d2
--- /dev/null
+++ b/manifest/terms/installer.md
@@ -0,0 +1,29 @@
+# 설치기(exe) 안내 및 약관
+
+**1.** 이 설치기는 맵, 모드, 리소스팩의 간편한 설치를 위한 프로그램입니다.
+- 설치기를 통해 설치되는 각 콘텐츠(맵, 모드, 리소스팩)는 각각의 약관을 따릅니다.
+- 설치기 사용 전 각 콘텐츠의 약관을 반드시 확인하세요.
+
+**2.** 이 설치기는 반드시 개인이 사용하여야 하며 비영리적인 목적으로 사용하여야 합니다.
+- 어떠한 경우에도 영리적인 목적으로 사용할 수 없으며, 허가를 받을 수도 없습니다.
+
+**3.** 설치기에 대한 2차 창작 및 2차 배포는 금지됩니다.
+- 소스코드의 무단 복제, 수정, 재배포는 엄격히 금지됩니다.
+- 설치기를 타 플랫폼 또는 제3자에게 재배포하는 행위는 금지됩니다.
+
+**4.** 설치기의 소스코드는 GitHub를 통해 공개되어 있습니다.
+- 소스코드 열람은 허용되나, 이를 기반으로 한 파생 프로그램 제작 및 배포는 금지됩니다.
+- 버그 제보 및 기여는 허용됩니다.
+
+**5.** 설치기에 포함된 외부 모드(Fabric API, Modmenu)는 각 모드의 라이선스를 따르며, 설치기는 해당 모드들을 공식 배포처에서 다운로드합니다.
+- Fabric API: https://www.curseforge.com/minecraft/mc-mods/fabric-api
+- Modmenu: https://www.curseforge.com/minecraft/mc-mods/modmenu
+
+**6.** 이 설치기의 저작권은 제작자에게 있으며, 무단 사용 시 저작권법에 의해 처벌받을 수 있습니다.
+
+Copyright (c) 2026. All rights reserved.
+
+This software is protected under a Custom License.
+Unauthorized reproduction, modification, or distribution of this software is strictly prohibited and may result in legal action under applicable copyright laws.
+
+All rights reserved (ARR). No part of this software may be reproduced, distributed, or transmitted in any form or by any means without the prior written permission of the copyright holder.
diff --git a/manifest/terms/map.md b/manifest/terms/map.md
new file mode 100644
index 0000000..243c33e
--- /dev/null
+++ b/manifest/terms/map.md
@@ -0,0 +1,22 @@
+# 맵(Map) 안내 및 약관
+
+**1.** 이 맵은 마인크래프트 인게임에서 시스템에 따라 재생되는 노래를 듣고 제목을 맞추는 Windows PC 기반 JE 26.1.2 전용 맵입니다.
+- 이번 노래퀴즈 주제는 "게임"이며, 임의의 게임 OST/BGM을 듣고 게임의 이름을 맞추어야 합니다.
+- JE 버전이 다를 경우 플레이가 불가능할 수도 있습니다. 버전을 반드시 확인하세요.
+
+**2.** 이 맵은 반드시 개인이 사용하여야 하며 비영리적인 목적으로 사용하여야 합니다.
+- 어떠한 경우에도 영리적인 목적으로 사용할 수 없으며, 허가를 받을 수도 없습니다.
+
+**3.** 맵에 대한 2차 창작은 금지합니다. 2차 배포는 이 글을 통하여 배포하되 허가가 필요합니다.
+- 맵에는 배경용 타 맵 제작자의 라이선스도 포함되어 있습니다. 무단 배포는 엄격히 금지합니다.
+
+**4.** 맵 플레이에는 50Mbps 이상의 기본적인 인터넷 속도를 요구합니다.
+- 또한 8코어 이상의 CPU와 16GB 이상의 램 용량을 권장합니다.
+- 위 사양을 충족하지 못할 경우 원활한 플레이가 어려울 수 있습니다.
+
+**5.** 맵에는 배경용 제3자의 맵이 사용되었습니다.
+- 출처: https://www.planetminecraft.com/project/liyue-harbour-from-genshin-impact-in-minecraft-1-1-scale/
+- 저작자: SkyBlock Squad
+- 해당 맵은 저작자의 허가를 받아 사용하였습니다.
+
+This work is licensed under CC BY-NC-ND 4.0
diff --git a/manifest/terms/mod.md b/manifest/terms/mod.md
new file mode 100644
index 0000000..c29d355
--- /dev/null
+++ b/manifest/terms/mod.md
@@ -0,0 +1,19 @@
+# 모드(Mod) 안내 및 약관
+
+**1.** 더 향상된 플레이를 위하여 모드가 포함되어 있습니다.
+- 모드는 설치기를 통하여 자동 설치됩니다.
+- 자동 설치가 제대로 되지 않을 경우 수동 설치를 권장합니다.
+
+**2.** Fabric 기반 26.1.2 모드를 사용하였습니다.
+- 저희가 제작한 chat_answer, video_player 모드는 제작자의 소유입니다.
+- 두 모드에 대한 2차 창작 및 2차 배포는 금지됩니다.
+
+**3.** 모드는 반드시 개인이 사용하여야 하며 비영리적인 목적으로 사용하여야 합니다.
+- 어떠한 경우에도 영리적인 목적으로 사용할 수 없습니다.
+
+**4.** 원활한 플레이를 위해 Sodium, Iris Shaders 모드를 함께 사용하는 것을 권장합니다.
+- 최적화 및 쉐이더 적용을 하기 위한 의도이며, 필수 사항은 아닙니다.
+- Sodium: https://www.curseforge.com/minecraft/mc-mods/sodium
+- Iris Shaders: https://www.curseforge.com/minecraft/mc-mods/irisshaders
+
+This work is licensed under CC BY-NC-ND 4.0
diff --git a/manifest/terms/resourcepack.md b/manifest/terms/resourcepack.md
new file mode 100644
index 0000000..89ecb5e
--- /dev/null
+++ b/manifest/terms/resourcepack.md
@@ -0,0 +1,13 @@
+# 리소스팩(ResourcePack) 안내 및 약관
+
+**1.** 리소스팩은 맵 플레이를 위한 필수 요소입니다.
+- 노래가 나오지 않는 경우 리소스팩의 적용 여부를 반드시 확인하세요.
+
+**2.** 리소스팩은 절대 2차 창작하거나 2차 배포해서는 안 되며, 어느 누구에게도 전달해서는 안 됩니다.
+- 영리적인 목적으로 사용할 수 없습니다.
+- 리소스팩에 포함된 음악의 저작권은 각 원저작자에게 있습니다.
+- 리소스팩은 이 맵 플레이 전용으로만 사용하여야 합니다.
+
+Copyright (c) 2026. All rights reserved.
+All music and audio files included in this resource pack are excluded from this license.
+The copyright of all such content belongs to their respective original copyright holders.
diff --git a/package.json b/package.json
index cd69785..4cda4c6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "minecraft-music-quiz-installer",
- "version": "0.2.6",
+ "version": "0.3.0",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js",
"scripts": {
diff --git a/public/termsEditor.css b/public/termsEditor.css
new file mode 100644
index 0000000..5e79a38
--- /dev/null
+++ b/public/termsEditor.css
@@ -0,0 +1,95 @@
+/* Notion 스타일 약관 편집기 전용 스타일.
+ * 텍스트영역과 미리보기 영역을 동일한 폭/타이포로 보여 주어 입력 ↔ 미리보기
+ * 전환 시 시각적 점프가 최소화되도록 한다. 슬래시 메뉴는 caret 좌표 위에
+ * 절대 위치로 띄운다. */
+
+.termsEditorWrap {
+ position: relative;
+ margin-top: 12px;
+}
+
+.termsEditor {
+ width: 100%;
+ min-height: 60vh;
+ padding: 16px 18px;
+ border: 1px solid #d5d5d5;
+ border-radius: 8px;
+ background: #fff;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 14px;
+ line-height: 1.7;
+ resize: vertical;
+ box-sizing: border-box;
+ outline: none;
+ white-space: pre-wrap;
+}
+
+.termsEditor:focus {
+ border-color: #5b8def;
+ box-shadow: 0 0 0 2px rgba(91, 141, 239, 0.2);
+}
+
+.termsPreview {
+ min-height: 60vh;
+ padding: 16px 18px;
+ border: 1px solid #d5d5d5;
+ border-radius: 8px;
+ background: #fafafa;
+ font-size: 14px;
+ line-height: 1.7;
+ box-sizing: border-box;
+}
+
+.termsPreview h1 { font-size: 22px; margin: 12px 0 8px; }
+.termsPreview h2 { font-size: 18px; margin: 10px 0 6px; }
+.termsPreview h3 { font-size: 15px; margin: 8px 0 4px; }
+.termsPreview p { margin: 6px 0; }
+.termsPreview ul, .termsPreview ol { margin: 6px 0; padding-left: 22px; }
+.termsPreview li { margin: 2px 0; }
+.termsPreview hr { border: none; border-top: 1px solid #e0e0e0; margin: 12px 0; }
+.termsPreview blockquote {
+ margin: 8px 0; padding: 4px 12px; border-left: 3px solid #ddd; color: #555;
+}
+.termsPreview code {
+ background: #eee; padding: 1px 5px; border-radius: 4px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 13px;
+}
+.termsPreview pre {
+ background: #f0f0f0; padding: 10px 12px; border-radius: 6px; overflow: auto;
+}
+.termsPreview pre code { background: transparent; padding: 0; }
+.termsPreview a { color: #2664d8; text-decoration: underline; word-break: break-all; }
+.termsPreview details {
+ margin: 6px 0; border: 1px solid #e0e0e0; border-radius: 6px;
+ background: #fff; padding: 4px 10px;
+}
+.termsPreview details > summary { cursor: pointer; font-weight: 600; padding: 4px 0; }
+
+/* 슬래시 자동완성 메뉴 — 노션 느낌으로 caret 좌표 위에 띄움. */
+.slashMenu {
+ position: absolute;
+ z-index: 50;
+ min-width: 220px;
+ max-height: 280px;
+ overflow-y: auto;
+ background: #fff;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+ padding: 4px;
+ font-size: 13px;
+}
+
+.slashMenu .slashItem {
+ display: flex; flex-direction: column;
+ padding: 6px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+}
+.slashMenu .slashItem:hover,
+.slashMenu .slashItem.active {
+ background: #eef2ff;
+}
+.slashMenu .slashItem strong { font-size: 13px; }
+.slashMenu .slashItem span { color: #888; font-size: 11px; }
diff --git a/public/termsEditor.js b/public/termsEditor.js
new file mode 100644
index 0000000..75e7fd7
--- /dev/null
+++ b/public/termsEditor.js
@@ -0,0 +1,385 @@
+/* 약관(Markdown) 편집기.
+ * - 기본은 textarea: 사용자가 직접 #, - 등을 입력할 수 있다.
+ * - "/" 를 줄 맨 앞 또는 빈 공간 다음에 입력하면 슬래시 메뉴를 띄워
+ * 제목/내용/글머리/번호/토글/구분선/인용/코드 블록을 선택해 자동 삽입한다.
+ * (사용자가 #, - 같은 기호를 외울 필요 없이 명령어로 입력 가능)
+ * - 미리보기 탭에서 작은 markdown → HTML 렌더러로 결과를 보여 준다.
+ */
+(function () {
+ 'use strict'
+
+ var editor = document.getElementById('editor')
+ var preview = document.getElementById('preview')
+ var slashMenu = document.getElementById('slashMenu')
+ var status = document.getElementById('status')
+ var dirtyMark = document.getElementById('dirty-mark')
+ var saveBtn = document.getElementById('saveBtn')
+ var tabBtns = document.querySelectorAll('.tabBar .tabBtn')
+
+ editor.value = INITIAL || ''
+ var dirty = false
+ function setDirty(v) {
+ dirty = v
+ dirtyMark.hidden = !v
+ }
+
+ // ─── markdown 미리 보기용 미니 렌더러 ────────────────────────────────
+ // 정식 markdown 파서는 아니지만, 본 편집기가 만들어 내는 형태(#, ##, ###,
+ // - , 1. , > , ---, ``` , 토글 details) 정도는 충실히 처리한다.
+ function escHtml(s) {
+ return s.replace(/&/g, '&').replace(//g, '>')
+ }
+ function inline(s) {
+ s = escHtml(s)
+ // code `x`
+ s = s.replace(/`([^`]+)`/g, '$1')
+ // bold **x**
+ s = s.replace(/\*\*([^*]+)\*\*/g, '$1')
+ // italic *x*
+ s = s.replace(/(^|\W)\*([^*\n]+)\*(?=\W|$)/g, '$1$2')
+ // links [text](url) — also auto-link bare http(s)
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ s = s.replace(/(^|[\s(])(https?:\/\/[^\s)]+)/g, function (m, p, u) {
+ return p + '' + u + ''
+ })
+ return s
+ }
+ function renderMd(src) {
+ var lines = src.replace(/\r\n/g, '\n').split('\n')
+ var out = []
+ var i = 0
+ var stackList = null // 'ul' | 'ol' | null
+ function closeList() { if (stackList) { out.push('' + stackList + '>'); stackList = null } }
+ while (i < lines.length) {
+ var line = lines[i]
+ // 코드 블록 ```lang
+ var fence = /^```(\w*)\s*$/.exec(line)
+ if (fence) {
+ closeList()
+ var code = []
+ i += 1
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
+ code.push(lines[i]); i += 1
+ }
+ if (i < lines.length) i += 1
+ out.push('
' + escHtml(code.join('\n')) + '
')
+ continue
+ }
+ // 토글 (자체 구문) :::toggle 제목 ... :::
+ var togStart = /^:::toggle\s+(.+)$/.exec(line)
+ if (togStart) {
+ closeList()
+ var summary = togStart[1]
+ var body = []
+ i += 1
+ while (i < lines.length && !/^:::\s*$/.test(lines[i])) {
+ body.push(lines[i]); i += 1
+ }
+ if (i < lines.length) i += 1
+ out.push('' + inline(summary) + '' + renderMd(body.join('\n')) + '')
+ continue
+ }
+ // 헤딩
+ var h = /^(#{1,6})\s+(.*)$/.exec(line)
+ if (h) {
+ closeList()
+ var level = h[1].length
+ out.push('' + inline(h[2]) + '')
+ i += 1; continue
+ }
+ // hr
+ if (/^---+\s*$/.test(line)) {
+ closeList()
+ out.push(''); i += 1; continue
+ }
+ // 인용 >
+ if (/^>\s?/.test(line)) {
+ closeList()
+ var q = []
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
+ q.push(lines[i].replace(/^>\s?/, '')); i += 1
+ }
+ out.push('
' + renderMd(q.join('\n')) + '
')
+ continue
+ }
+ // 번호 목록
+ var ol = /^\s*\d+\.\s+(.*)$/.exec(line)
+ if (ol) {
+ if (stackList !== 'ol') { closeList(); out.push(''); stackList = 'ol' }
+ out.push('
' + inline(ol[1]) + '
')
+ i += 1; continue
+ }
+ // 불릿
+ var ul = /^\s*[-*]\s+(.*)$/.exec(line)
+ if (ul) {
+ if (stackList !== 'ul') { closeList(); out.push('
'); stackList = 'ul' }
+ out.push('
' + inline(ul[1]) + '
')
+ i += 1; continue
+ }
+ // 빈 줄
+ if (/^\s*$/.test(line)) { closeList(); i += 1; continue }
+ // 일반 문단
+ closeList()
+ var para = [line]; i += 1
+ while (i < lines.length && !/^\s*$/.test(lines[i])
+ && !/^(#{1,6})\s+/.test(lines[i])
+ && !/^\s*[-*]\s+/.test(lines[i])
+ && !/^\s*\d+\.\s+/.test(lines[i])
+ && !/^>/.test(lines[i])
+ && !/^---+\s*$/.test(lines[i])
+ && !/^```/.test(lines[i])
+ && !/^:::/.test(lines[i])) {
+ para.push(lines[i]); i += 1
+ }
+ out.push('