From bfba296b913a78db143b10ec494a58d533f7e22b Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Sun, 7 Jun 2026 19:17:44 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=97=90=EB=94=94=ED=84=B0=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=3D=20=EC=84=9C=EB=B2=84=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C(URL)=20+=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=20=EB=A9=94=EC=8B=9C=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PC 에디터: πŸ–Ό β†’ μ••μΆ• β†’ μ„œλ²„ μ—…λ‘œλ“œ β†’ 본문에 ![](/api/images/{id}) μ‚½μž…(base64 제거) - Toast UI(μ›Ή/APK): addImageBlobHook 으둜 λ™μΌν•˜κ²Œ μ„œλ²„ μ—…λ‘œλ“œ(base64 λŒ€μ²΄) - μ—…λ‘œλ“œ 쀑/μ‹€νŒ¨ μ•ˆλ‚΄ λ©”μ‹œμ§€ ν‘œμ‹œ Co-Authored-By: Claude Opus 4.8 --- src/api/boardApi.js | 9 ++++ src/components/editor/MarkdownEditor.vue | 54 ++++++++++++++++++------ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/api/boardApi.js b/src/api/boardApi.js index ef0b725..bc68a89 100644 --- a/src/api/boardApi.js +++ b/src/api/boardApi.js @@ -26,6 +26,15 @@ export const boardApi = { remove(id) { return http.delete(`/board/posts/${id}`) }, + // λ³Έλ¬Έ 인라인 이미지 μ—…λ‘œλ“œ(μ„œλ²„ DB μ €μž₯) β†’ { url } (예: /api/images/123) + uploadImage(blob) { + const fd = new FormData() + fd.append('image', blob, 'image.jpg') + return http.post('/board/images', fd, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 30000, + }) + }, tags() { return http.get('/board/tags') }, diff --git a/src/components/editor/MarkdownEditor.vue b/src/components/editor/MarkdownEditor.vue index e49f48a..7bf94e8 100644 --- a/src/components/editor/MarkdownEditor.vue +++ b/src/components/editor/MarkdownEditor.vue @@ -7,6 +7,7 @@ import 'prismjs/themes/prism-tomorrow.css' import '@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css' import MarkdownViewer from '@/components/editor/MarkdownViewer.vue' import { imageToBlob } from '@/utils/receiptOcr' +import { boardApi } from '@/api/boardApi' const props = defineProps({ modelValue: { type: String, default: '' }, @@ -104,32 +105,37 @@ function insertLink() { }) } -// 이미지: 파일 선택 β†’ λ¦¬μ‚¬μ΄μ¦ˆ/μ••μΆ•(JPEG) β†’ base64 μž„λ² λ“œ (μ›Ή 에디터와 λ™μΌν•˜κ²Œ 본문에 포함, 별도 μ„œλ²„ μ €μž₯ μ—†μŒ) +// 이미지: 파일 β†’ λ¦¬μ‚¬μ΄μ¦ˆ/μ••μΆ•(JPEG) β†’ μ„œλ²„ μ—…λ‘œλ“œ(DB μ €μž₯) β†’ λ³Έλ¬Έμ—” /api/images/{id} URL 만 μ‚½μž… const imgInput = ref(null) const imgBusy = ref(false) +const imgMsg = ref('') +let imgMsgTimer = null +function flashImgMsg(msg) { + imgMsg.value = msg + if (imgMsgTimer) clearTimeout(imgMsgTimer) + imgMsgTimer = setTimeout(() => (imgMsg.value = ''), 4000) +} function pickImage() { imgInput.value?.click() } -function blobToDataUrl(blob) { - return new Promise((resolve, reject) => { - const r = new FileReader() - r.onload = () => resolve(r.result) - r.onerror = reject - r.readAsDataURL(blob) - }) +// μ••μΆ• ν›„ μ—…λ‘œλ“œ β†’ URL λ°˜ν™˜ (PC textareaΒ·Toast UI 곡용) +async function uploadCompressed(fileOrBlob) { + const blob = await imageToBlob(fileOrBlob, 1280) // κΈ΄ λ³€ 1280px, JPEG 0.85 + const { url } = await boardApi.uploadImage(blob) + return url } async function onImagePick(e) { const file = e.target.files?.[0] e.target.value = '' if (!file) return imgBusy.value = true + imgMsg.value = '이미지 μ—…λ‘œλ“œ 쀑…' try { - const blob = await imageToBlob(file, 1280) // κΈ΄ λ³€ 1280px, JPEG 0.85 μ••μΆ• - const dataUrl = await blobToDataUrl(blob) + const url = await uploadCompressed(file) const ta = taRef.value const s = ta ? ta.selectionStart : text.value.length const alt = file.name.replace(/\.[^.]+$/, '') - const md = `![${alt}](${dataUrl})` + const md = `![${alt}](${url})` text.value = text.value.slice(0, s) + md + text.value.slice(s) nextTick(() => { if (!ta) return @@ -137,8 +143,9 @@ async function onImagePick(e) { const pos = s + md.length ta.setSelectionRange(pos, pos) }) - } catch { - /* 이미지 처리 μ‹€νŒ¨ λ¬΄μ‹œ */ + imgMsg.value = '' + } catch (err) { + flashImgMsg(err?.response?.data?.message || '이미지 μ—…λ‘œλ“œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.') } finally { imgBusy.value = false } @@ -167,6 +174,18 @@ onMounted(() => { emit('update:modelValue', lastEmitted) }, }, + hooks: { + // 이미지 μΆ”κ°€ μ‹œ base64 λŒ€μ‹  μ„œλ²„ μ—…λ‘œλ“œ β†’ URL μ‚½μž… + addImageBlobHook: async (blob, callback) => { + try { + const url = await uploadCompressed(blob) + callback(url, '') + } catch { + /* μ—…λ‘œλ“œ μ‹€νŒ¨ μ‹œ λ―Έμ‚½μž… */ + } + return false + }, + }, }) }) @@ -216,6 +235,7 @@ onBeforeUnmount(() => { +

{{ imgMsg }}