(function () { // 내장 툴바(toolbar: false)는 안 쓴다 — EasyMDE 기본 툴바 아이콘은 Font Awesome CDN을 전제로 // 하는데, 이 앱은 CDN을 안 쓰는 게 원칙이라 대신 journal.html/journal_day_detail.html에서 // 직접 만든 텍스트 버튼이 아래 EDITOR_ACTIONS로 EasyMDE 인스턴스 메서드를 호출한다. function initEditors(root) { if (typeof EasyMDE === "undefined") return; var scope = root instanceof Element ? root : document; var textareas = scope.querySelectorAll("textarea.markdown-editor:not([data-easymde-initialized])"); textareas.forEach(function (textarea) { textarea.setAttribute("data-easymde-initialized", "true"); var easymde = new EasyMDE({ element: textarea, toolbar: false, spellChecker: false, autoDownloadFontAwesome: false, status: false, placeholder: textarea.getAttribute("placeholder") || "", minHeight: (textarea.getAttribute("rows") || 6) * 24 + "px", // 글머리 목록 버튼/Enter 자동 이어쓰기가 기본 "*" 대신 "-"를 쓰게 한다. // 서버 렌더링(app/markdown_utils.py)은 -/*/+ 전부 동일하게 처리하니 렌더링과는 무관하고 // 순수하게 에디터가 새로 만들어주는 글머리 기호에 대한 취향 설정이다. unorderedListStyle: "-", }); // CodeMirror는 원본 textarea와 실시간으로 값이 동기화되지 않는다(.save()를 명시적으로 // 불러야 함) — 매 변경마다 저장하고, htmx 미리보기(hx-trigger="input ...")와 // Alpine x-model이 둘 다 반응하도록 input 이벤트를 합성해서 던진다. easymde.codemirror.on("change", function () { easymde.codemirror.save(); textarea.dispatchEvent(new Event("input", { bubbles: true })); }); easymde.codemirror.on("paste", function (cm, event) { handleImagePaste(cm, event); }); textarea._easymde = easymde; }); } // 클립보드에 이미지가 있으면(스크린샷/사진 복사 등) 그대로 붙여넣기 대신 서버에 업로드하고 // 그 자리에 마크다운 이미지 문법(![](url))을 끼워넣는다. 이미지가 아니면 그냥 통과시켜서 // CodeMirror 기본 텍스트 붙여넣기가 그대로 동작하게 둔다. function handleImagePaste(cm, event) { var items = event.clipboardData && event.clipboardData.items; if (!items) return; var imageItem = null; for (var i = 0; i < items.length; i++) { if (items[i].type.indexOf("image/") === 0) { imageItem = items[i]; break; } } if (!imageItem) return; event.preventDefault(); var file = imageItem.getAsFile(); if (!file) return; var doc = cm.getDoc(); var from = doc.getCursor(); var placeholder = "![업로드 중...]()"; doc.replaceRange(placeholder, from); var to = { line: from.line, ch: from.ch + placeholder.length }; // 업로드가 끝나기 전에 사용자가 다른 곳에서 계속 타이핑해도(줄 추가 등) 자리를 잃지 // 않도록 고정 좌표 대신 CodeMirror 북마크로 추적한다. var startMark = doc.setBookmark(from); var endMark = doc.setBookmark(to); var formData = new FormData(); formData.append("file", file, file.name || "pasted-image.png"); fetch("/api/journal/paste-image", { method: "POST", body: formData }) .then(function (res) { if (!res.ok) return res.json().then(function (body) { throw new Error(body.detail || "업로드 실패"); }); return res.json(); }) .then(function (data) { var start = startMark.find(); var end = endMark.find(); if (start && end) { doc.replaceRange("![](" + data.url + ")", start, end); } }) .catch(function (err) { var start = startMark.find(); var end = endMark.find(); if (start && end) { doc.replaceRange("(이미지 붙여넣기 실패: " + err.message + ")", start, end); } }) .finally(function () { startMark.clear(); endMark.clear(); cm.save(); cm.getTextArea().dispatchEvent(new Event("input", { bubbles: true })); }); } document.addEventListener("DOMContentLoaded", function () { initEditors(document); }); document.body.addEventListener("htmx:afterSwap", function (evt) { initEditors(evt.target); }); })();