journal: paste image from clipboard, cap embedded image size, fix media persistence
- Paste-to-embed: pasting an image into the markdown editor uploads it and
inserts  at the cursor. Unlike gallery attachments these aren't
tied to a journal_entry (the entry may not exist yet while composing), so
they're stored per-user under app/media/journal/{user_id}/pasted/ with no
DB row, served through an ownership-scoped route, and never cleaned up
automatically when an entry is deleted -- an accepted tradeoff at this
app's personal scale.
- The markdown sanitizer was stripping all <img> tags (not on the bleach
allowlist), which would have silently deleted every pasted image on save;
added img/src/alt/title while keeping event-handler attributes blocked.
- Cap embedded image width in both the editor pane and the rendered preview
so a large pasted photo can't overflow its card.
- Fix real data loss risk found while testing this: docker-compose.yml had
no volume for app/media, so every container recreate during a deploy wiped
uploaded photos, and deploy_sftp.py was syncing app/media/ (runtime user
data, not source) into the remote build context. Added the volume mount
and excluded media/ from the sync script. Recovered and relocated the
real attachments that had already landed in the wrong place on the NAS
during earlier deploys this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,10 +32,70 @@
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
easymde.codemirror.on("paste", function (cm, event) {
|
||||
handleImagePaste(cm, event);
|
||||
});
|
||||
|
||||
textarea._easymde = easymde;
|
||||
});
|
||||
}
|
||||
|
||||
// 클립보드에 이미지가 있으면(스크린샷/사진 복사 등) 그대로 붙여넣기 대신 서버에 업로드하고
|
||||
// 그 자리에 마크다운 이미지 문법()을 끼워넣는다. 이미지가 아니면 그냥 통과시켜서
|
||||
// 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("", 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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user