에디터 미디어 업로드 검증 로직 분리
This commit is contained in:
@@ -23,12 +23,14 @@ import {
|
|||||||
} from '../../lib/markdown-editor-history.js'
|
} from '../../lib/markdown-editor-history.js'
|
||||||
import {
|
import {
|
||||||
createMediaBlockMarkdown,
|
createMediaBlockMarkdown,
|
||||||
|
findUploadSizeLimitError,
|
||||||
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
|
getGallerySelectionOrder as getGallerySelectionOrderFromSelection,
|
||||||
getMediaItemKind,
|
getMediaItemKind,
|
||||||
getMediaPickerPreviewUrl,
|
getMediaPickerPreviewUrl,
|
||||||
isMediaItemSelectableForTarget,
|
isMediaItemSelectableForTarget,
|
||||||
isUploadFileAllowedForMediaTarget,
|
isUploadFileAllowedForMediaTarget,
|
||||||
resolveSelectedMediaItems,
|
resolveSelectedMediaItems,
|
||||||
|
resolveUploadFetchErrorMessage,
|
||||||
toggleMediaSelectionState
|
toggleMediaSelectionState
|
||||||
} from '../../lib/markdown-editor-media.js'
|
} from '../../lib/markdown-editor-media.js'
|
||||||
import {
|
import {
|
||||||
@@ -49,9 +51,6 @@ import {
|
|||||||
DEFAULT_UPLOAD_MAX_FILE_COUNT,
|
DEFAULT_UPLOAD_MAX_FILE_COUNT,
|
||||||
DEFAULT_UPLOAD_MAX_TOTAL_SIZE,
|
DEFAULT_UPLOAD_MAX_TOTAL_SIZE,
|
||||||
formatUploadSizeLimit,
|
formatUploadSizeLimit,
|
||||||
getUploadFilesTotalSize,
|
|
||||||
getMaxUploadBytesForKind,
|
|
||||||
getUploadKind,
|
|
||||||
normalizeUploadMaxFileCount,
|
normalizeUploadMaxFileCount,
|
||||||
normalizeUploadMaxTotalSize
|
normalizeUploadMaxTotalSize
|
||||||
} from '../../lib/upload-size-limit.js'
|
} from '../../lib/upload-size-limit.js'
|
||||||
@@ -3268,66 +3267,18 @@ const applyMediaSelection = () => {
|
|||||||
closeMediaPicker()
|
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 - 업로드 파일 목록
|
* @param {FileList|Array<File>} files - 업로드 파일 목록
|
||||||
* @returns {Promise<Array<Object>>} 업로드된 파일 목록
|
* @returns {Promise<Array<Object>>} 업로드된 파일 목록
|
||||||
*/
|
*/
|
||||||
const uploadMediaFiles = async (files) => {
|
const uploadMediaFiles = async (files) => {
|
||||||
const sizeLimitError = findUploadSizeLimitError(files)
|
const sizeLimitError = findUploadSizeLimitError({
|
||||||
|
files,
|
||||||
|
maxFileCount: uploadMaxFileCount.value,
|
||||||
|
maxTotalSize: uploadMaxTotalSize.value,
|
||||||
|
uploadSizeLimits: uploadSizeLimits.value
|
||||||
|
})
|
||||||
|
|
||||||
if (sizeLimitError) {
|
if (sizeLimitError) {
|
||||||
throw new Error(sizeLimitError)
|
throw new Error(sizeLimitError)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
|
| lib/navigation-editor-tree.js | 관리자 메뉴 UI·서버 `renumberSortOrderByTree`가 쓰는 `buildNavigationEditorTree`, 관리자 상단 네비 평면 표용 `flattenNavigationEditorWrappers` |
|
||||||
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
| lib/markdown-content-normalizer.js | 관리자 Markdown-first 전환 후 레거시 블록 배열·객체 본문 값을 저장용 마크다운 문자열로 변환 |
|
||||||
| lib/markdown-editor-history.js | 관리자 Markdown-first 에디터 undo/redo 히스토리 항목 생성, 레거시 문자열 항목 본문 추출, 선택 범위 보정 |
|
| 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-parser.js | 공개 Markdown 본문 문자열을 제목·문단·이미지·갤러리·fenced 블록·인용·목록·코드·표 렌더링 블록으로 변환 |
|
||||||
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
| lib/markdown-block-context.js | 관리자 Markdown textarea·라이브 편집 포커스 위치 기준 이미지·갤러리·링크·iframe·HTML·인용·콜아웃·코드·토글 블록 설정 패널 대상 판별, 인용 옵션 공통 파서 연결, 코드 fenced 블록 여닫는 펜스 쌍 판별 |
|
||||||
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |
|
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출, `allowfullscreen`을 `allow=fullscreen` 권한으로 정규화 |
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.71
|
||||||
|
|
||||||
|
- 관리자 Markdown 에디터 미디어 업로드 API 오류 메시지 변환과 파일 개수·총량·개별 크기 한도 검증 로직을 공통 helper로 분리.
|
||||||
|
- 미디어 업로드 오류 메시지와 업로드 한도 검증 단위 테스트 추가.
|
||||||
|
|
||||||
## v1.11.70
|
## v1.11.70
|
||||||
|
|
||||||
- 관리자 Markdown 에디터 미디어 선택 모달의 선택 순서·단일/갤러리 선택 토글·선택 URL 기반 항목 해석 로직을 공통 helper로 분리.
|
- 관리자 Markdown 에디터 미디어 선택 모달의 선택 순서·단일/갤러리 선택 토글·선택 URL 기반 항목 해석 로직을 공통 helper로 분리.
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ import {
|
|||||||
isPostCardThumbnailUrl,
|
isPostCardThumbnailUrl,
|
||||||
withGeneratedImageCacheVersion
|
withGeneratedImageCacheVersion
|
||||||
} from './post-card-thumbnail-url.js'
|
} 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))
|
.map((url) => itemMap[url] || items.find((item) => item?.url === url))
|
||||||
.filter(Boolean)
|
.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
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.70",
|
"version": "1.11.71",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.70",
|
"version": "1.11.71",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.70",
|
"version": "1.11.71",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import {
|
import {
|
||||||
createMediaBlockMarkdown,
|
createMediaBlockMarkdown,
|
||||||
|
findUploadSizeLimitError,
|
||||||
formatMediaFileSize,
|
formatMediaFileSize,
|
||||||
getGallerySelectionOrder,
|
getGallerySelectionOrder,
|
||||||
getMediaItemKind,
|
getMediaItemKind,
|
||||||
@@ -9,8 +10,10 @@ import {
|
|||||||
isMediaItemSelectableForTarget,
|
isMediaItemSelectableForTarget,
|
||||||
isUploadFileAllowedForMediaTarget,
|
isUploadFileAllowedForMediaTarget,
|
||||||
resolveSelectedMediaItems,
|
resolveSelectedMediaItems,
|
||||||
|
resolveUploadFetchErrorMessage,
|
||||||
toggleMediaSelectionState
|
toggleMediaSelectionState
|
||||||
} from '../lib/markdown-editor-media.js'
|
} from '../lib/markdown-editor-media.js'
|
||||||
|
import { buildDefaultUploadSizeLimits } from '../lib/upload-size-limit.js'
|
||||||
|
|
||||||
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
|
test('미디어 파일 크기를 표시 문자열로 변환한다', () => {
|
||||||
assert.equal(formatMediaFileSize(512), '512 B')
|
assert.equal(formatMediaFileSize(512), '512 B')
|
||||||
@@ -127,3 +130,58 @@ test('선택된 URL 순서대로 미디어 항목을 해석한다', () => {
|
|||||||
{ url: '/b.png', source: 'list' }
|
{ 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)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user