에디터 미디어 업로드 검증 로직 분리

This commit is contained in:
2026-07-24 17:55:33 +09:00
parent 6cc820d2a2
commit e3f98cb820
7 changed files with 143 additions and 61 deletions

View File

@@ -23,12 +23,14 @@ import {
} from '../../lib/markdown-editor-history.js'
import {
createMediaBlockMarkdown,
findUploadSizeLimitError,
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
getMediaItemKind,
getMediaPickerPreviewUrl,
isMediaItemSelectableForTarget,
isUploadFileAllowedForMediaTarget,
resolveSelectedMediaItems,
resolveUploadFetchErrorMessage,
toggleMediaSelectionState
} from '../../lib/markdown-editor-media.js'
import {
@@ -49,9 +51,6 @@ import {
DEFAULT_UPLOAD_MAX_FILE_COUNT,
DEFAULT_UPLOAD_MAX_TOTAL_SIZE,
formatUploadSizeLimit,
getUploadFilesTotalSize,
getMaxUploadBytesForKind,
getUploadKind,
normalizeUploadMaxFileCount,
normalizeUploadMaxTotalSize
} from '../../lib/upload-size-limit.js'
@@ -3268,66 +3267,18 @@ const applyMediaSelection = () => {
closeMediaPicker()
}
/**
* 업로드 API 오류 메시지를 사용자용 문장으로 변환한다.
* @param {unknown} error - fetch 오류
* @returns {string} 오류 메시지
*/
const resolveUploadFetchErrorMessage = (error) => {
const responseMessage = error?.data?.message || error?.response?._data?.message
if (typeof responseMessage === 'string' && responseMessage.trim()) {
return responseMessage.trim()
}
if (error?.statusCode === 413 || error?.status === 413) {
return '업로드 가능한 파일 크기를 초과했습니다.'
}
if (error instanceof Error && error.message) {
return error.message
}
return '파일 업로드에 실패했습니다.'
}
/**
* 선택 파일이 업로드 한도를 넘지 않는지 검사한다.
* @param {FileList|Array<File>} files - 업로드 파일 목록
* @returns {string|null} 초과 시 오류 메시지, 아니면 null
*/
const findUploadSizeLimitError = (files) => {
const fileList = Array.from(files || [])
if (fileList.length > uploadMaxFileCount.value) {
return `한 번에 업로드할 수 있는 파일은 최대 ${uploadMaxFileCount.value}개입니다.`
}
const uploadTotalSize = getUploadFilesTotalSize(fileList)
if (uploadTotalSize > uploadMaxTotalSize.value) {
return `한 번에 업로드 가능한 전체 용량을 초과했습니다. (최대 ${formatUploadSizeLimit(uploadMaxTotalSize.value)})`
}
for (const file of fileList) {
const uploadKind = getUploadKind(file.type, file.name)
const maxBytes = getMaxUploadBytesForKind(uploadKind, uploadSizeLimits.value)
if (file.size > maxBytes) {
return `파일 크기가 너무 큽니다. (최대 ${formatUploadSizeLimit(maxBytes)})`
}
}
return null
}
/**
* 미디어 파일을 업로드한다.
* @param {FileList|Array<File>} files - 업로드 파일 목록
* @returns {Promise<Array<Object>>} 업로드된 파일 목록
*/
const uploadMediaFiles = async (files) => {
const sizeLimitError = findUploadSizeLimitError(files)
const sizeLimitError = findUploadSizeLimitError({
files,
maxFileCount: uploadMaxFileCount.value,
maxTotalSize: uploadMaxTotalSize.value,
uploadSizeLimits: uploadSizeLimits.value
})
if (sizeLimitError) {
throw new Error(sizeLimitError)

View File

@@ -48,7 +48,7 @@
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
| lib/markdown-editor-history.js | 관리자 Markdown-first 에디터 undo/redo 히스토리 항목 생성, 레거시 문자열 항목 본문 추출, 선택 범위 보정 |
| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 순서·토글·항목 해석, fenced 미디어 블록 생성 |
| lib/markdown-editor-media.js | 관리자 Markdown-first 에디터 미디어 선택 모달의 미디어 종류 판별, 대상별 선택 가능 여부, 미리보기 URL, 업로드 MIME 허용, 선택 순서·토글·항목 해석, 업로드 오류·한도 검증, fenced 미디어 블록 생성 |
| lib/markdown-block-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 |
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |

View File

@@ -1,5 +1,10 @@
# 업데이트 이력
## v1.11.71
- 관리자 Markdown 에디터 미디어 업로드 API 오류 메시지 변환과 파일 개수·총량·개별 크기 한도 검증 로직을 공통 helper로 분리.
- 미디어 업로드 오류 메시지와 업로드 한도 검증 단위 테스트 추가.
## v1.11.70
- 관리자 Markdown 에디터 미디어 선택 모달의 선택 순서·단일/갤러리 선택 토글·선택 URL 기반 항목 해석 로직을 공통 helper로 분리.

View File

@@ -5,6 +5,12 @@ import {
isPostCardThumbnailUrl,
withGeneratedImageCacheVersion
} from './post-card-thumbnail-url.js'
import {
formatUploadSizeLimit,
getMaxUploadBytesForKind,
getUploadFilesTotalSize,
getUploadKind
} from './upload-size-limit.js'
/**
* 파일 크기를 표시용 문자열로 변환한다.
@@ -233,3 +239,65 @@ export const resolveSelectedMediaItems = (selectedUrls, selectedItemMap, mediaIt
.map((url) => itemMap[url] || items.find((item) => item?.url === url))
.filter(Boolean)
}
/**
* 업로드 API 오류 메시지를 사용자용 문장으로 변환한다.
* @param {unknown} error - fetch 오류
* @returns {string} 오류 메시지
*/
export const resolveUploadFetchErrorMessage = (error) => {
const responseMessage = error?.data?.message || error?.response?._data?.message
if (typeof responseMessage === 'string' && responseMessage.trim()) {
return responseMessage.trim()
}
if (error?.statusCode === 413 || error?.status === 413) {
return '업로드 가능한 파일 크기를 초과했습니다.'
}
if (error instanceof Error && error.message) {
return error.message
}
return '파일 업로드에 실패했습니다.'
}
/**
* 선택 파일이 업로드 한도를 넘지 않는지 검사한다.
* @param {Object} options - 업로드 한도 검사 옵션
* @param {FileList|Array<File>} options.files - 업로드 파일 목록
* @param {number} options.maxFileCount - 최대 파일 수
* @param {number} options.maxTotalSize - 최대 전체 용량
* @param {{ image: number, video: number, audio: number, document: number }} options.uploadSizeLimits - 종류별 업로드 한도
* @returns {string|null} 초과 시 오류 메시지, 아니면 null
*/
export const findUploadSizeLimitError = ({
files,
maxFileCount,
maxTotalSize,
uploadSizeLimits
}) => {
const fileList = Array.from(files || [])
if (fileList.length > maxFileCount) {
return `한 번에 업로드할 수 있는 파일은 최대 ${maxFileCount}개입니다.`
}
const uploadTotalSize = getUploadFilesTotalSize(fileList)
if (uploadTotalSize > maxTotalSize) {
return `한 번에 업로드 가능한 전체 용량을 초과했습니다. (최대 ${formatUploadSizeLimit(maxTotalSize)})`
}
for (const file of fileList) {
const uploadKind = getUploadKind(file.type, file.name)
const maxBytes = getMaxUploadBytesForKind(uploadKind, uploadSizeLimits)
if (file.size > maxBytes) {
return `파일 크기가 너무 큽니다. (최대 ${formatUploadSizeLimit(maxBytes)})`
}
}
return null
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.11.70",
"version": "1.11.71",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.11.70",
"version": "1.11.71",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.11.70",
"version": "1.11.71",
"private": true,
"type": "module",
"imports": {

View File

@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import test from 'node:test'
import {
createMediaBlockMarkdown,
findUploadSizeLimitError,
formatMediaFileSize,
getGallerySelectionOrder,
getMediaItemKind,
@@ -9,8 +10,10 @@ import {
isMediaItemSelectableForTarget,
isUploadFileAllowedForMediaTarget,
resolveSelectedMediaItems,
resolveUploadFetchErrorMessage,
toggleMediaSelectionState
} from '../lib/markdown-editor-media.js'
import { buildDefaultUploadSizeLimits } from '../lib/upload-size-limit.js'
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
assert.equal(formatMediaFileSize(512), '512 B')
@@ -127,3 +130,58 @@ test('선택된 URL 순서대로 미디어 항목을 해석한다', () => {
{ url: '/b.png', source: 'list' }
])
})
test('업로드 API 오류를 사용자 메시지로 변환한다', () => {
assert.equal(resolveUploadFetchErrorMessage({
data: {
message: ' 서버 오류 '
}
}), '서버 오류')
assert.equal(resolveUploadFetchErrorMessage({ statusCode: 413 }), '업로드 가능한 파일 크기를 초과했습니다.')
assert.equal(resolveUploadFetchErrorMessage(new Error('네트워크 오류')), '네트워크 오류')
assert.equal(resolveUploadFetchErrorMessage({}), '파일 업로드에 실패했습니다.')
})
test('업로드 파일 한도 초과 메시지를 계산한다', () => {
const uploadSizeLimits = buildDefaultUploadSizeLimits({
image: 100
})
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 10 },
{ name: 'b.png', type: 'image/png', size: 10 }
],
maxFileCount: 1,
maxTotalSize: 1000,
uploadSizeLimits
}), '한 번에 업로드할 수 있는 파일은 최대 1개입니다.')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 60 },
{ name: 'b.png', type: 'image/png', size: 60 }
],
maxFileCount: 5,
maxTotalSize: 100,
uploadSizeLimits
}), '한 번에 업로드 가능한 전체 용량을 초과했습니다. (최대 100B)')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 101 }
],
maxFileCount: 5,
maxTotalSize: 1000,
uploadSizeLimits
}), '파일 크기가 너무 큽니다. (최대 100B)')
assert.equal(findUploadSizeLimitError({
files: [
{ name: 'a.png', type: 'image/png', size: 100 }
],
maxFileCount: 5,
maxTotalSize: 1000,
uploadSizeLimits
}), null)
})