v1.4.1: 관리자 미디어 업로드 한도·라이브 에디터 UX 개선
종류별 업로드 크기 한도와 413 안내를 추가하고, 임베드·미디어 라이브 프리뷰·제목 Enter 포커스·스크롤 동작을 보정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -37,6 +37,13 @@ const findFencedBlockEnd = (lines, startLine) => {
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 단독 URL 줄인지 확인한다.
|
||||
* @param {string} line - 마크다운 줄
|
||||
* @returns {boolean} 단독 URL 여부
|
||||
*/
|
||||
const isStandaloneUrlLine = (line) => /^https?:\/\/\S+$/i.test(String(line || '').trim())
|
||||
|
||||
/**
|
||||
* 갤러리 fenced 블록을 파싱한다.
|
||||
* @param {string[]} lines - 본문 줄 목록
|
||||
@@ -74,6 +81,17 @@ const resolveGalleryBlock = (lines, currentLine) => {
|
||||
* @returns {{ kind: 'embed', startLine: number, endLine: number, url: string }|null}
|
||||
*/
|
||||
const resolveEmbedBlock = (lines, currentLine) => {
|
||||
const standaloneUrl = String(lines[currentLine] || '').trim()
|
||||
|
||||
if (isStandaloneUrlLine(standaloneUrl)) {
|
||||
return {
|
||||
kind: 'embed',
|
||||
startLine: currentLine,
|
||||
endLine: currentLine,
|
||||
url: standaloneUrl
|
||||
}
|
||||
}
|
||||
|
||||
const embedStart = findFencedBlockStart(lines, currentLine, ':::embed')
|
||||
|
||||
if (embedStart === -1) {
|
||||
|
||||
@@ -81,7 +81,7 @@ const serializeLegacyBlock = (block = {}, index = 0, total = 1) => {
|
||||
const url = String(block.url || '').trim()
|
||||
|
||||
return url
|
||||
? { type, value: `:::embed\n${url}\n:::` }
|
||||
? { type, value: url }
|
||||
: null
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @property {string} label - 표시 이름
|
||||
* @property {string} description - 설명
|
||||
* @property {string[]} keywords - 검색 키워드
|
||||
* @property {'media-image'|'media-gallery'|'lines'} action - 실행 유형
|
||||
* @property {'media-image'|'media-gallery'|'media-video'|'media-audio'|'media-file'|'lines'} action - 실행 유형
|
||||
* @property {string[]} [lines] - 삽입할 마크다운 줄(action이 lines일 때)
|
||||
* @property {boolean} [showInDefaultMenu=true] - `/`만 입력했을 때 메뉴에 표시할지
|
||||
*/
|
||||
@@ -110,10 +110,31 @@ export const MARKDOWN_SLASH_COMMANDS = [
|
||||
{
|
||||
id: 'embed',
|
||||
label: '임베드',
|
||||
description: 'YouTube·X 등 외부 링크',
|
||||
description: 'YouTube·X 등 외부 URL 한 줄',
|
||||
keywords: ['embed', 'youtube', 'link', '임베드', '유튜브'],
|
||||
action: 'lines',
|
||||
lines: [':::embed', '', ':::']
|
||||
lines: ['https://']
|
||||
},
|
||||
{
|
||||
id: 'video',
|
||||
label: '비디오',
|
||||
description: '업로드 비디오 카드 선택',
|
||||
keywords: ['video', 'movie', 'mp4', '비디오', '영상'],
|
||||
action: 'media-video'
|
||||
},
|
||||
{
|
||||
id: 'audio',
|
||||
label: '오디오',
|
||||
description: '오디오 플레이어 카드 선택',
|
||||
keywords: ['audio', 'music', 'mp3', '오디오', '음악'],
|
||||
action: 'media-audio'
|
||||
},
|
||||
{
|
||||
id: 'file',
|
||||
label: '파일',
|
||||
description: '다운로드 파일 카드 선택',
|
||||
keywords: ['file', 'download', 'pdf', '파일', '다운로드'],
|
||||
action: 'media-file'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
113
lib/upload-size-limit.js
Normal file
113
lib/upload-size-limit.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 파일명에서 확장자를 추출한다. (브라우저·서버 공용, node:path 미사용)
|
||||
* @param {string} [filename] - 파일명
|
||||
* @returns {string} 소문자 확장자(점 포함, 예: `.mp4`)
|
||||
*/
|
||||
const getFileExtension = (filename = '') => {
|
||||
const normalized = String(filename).trim()
|
||||
const lastDot = normalized.lastIndexOf('.')
|
||||
|
||||
if (lastDot <= 0 || lastDot === normalized.length - 1) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return normalized.slice(lastDot).toLowerCase()
|
||||
}
|
||||
|
||||
/** @type {Record<string, string>} 업로드 종류 */
|
||||
export const UPLOAD_KIND = {
|
||||
image: 'image',
|
||||
video: 'video',
|
||||
audio: 'audio',
|
||||
document: 'document'
|
||||
}
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov'])
|
||||
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.m4a'])
|
||||
const IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif'])
|
||||
|
||||
/** @type {Record<string, string>} 업로드 종류 한글 라벨 */
|
||||
const UPLOAD_KIND_LABELS = {
|
||||
[UPLOAD_KIND.image]: '이미지',
|
||||
[UPLOAD_KIND.video]: '비디오',
|
||||
[UPLOAD_KIND.audio]: '오디오',
|
||||
[UPLOAD_KIND.document]: '파일'
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 업로드 크기 한도(바이트)를 만든다.
|
||||
* @param {Object} [overrides] - 종류별 한도 덮어쓰기
|
||||
* @returns {{ image: number, video: number, audio: number, document: number }} 종류별 한도
|
||||
*/
|
||||
export const buildDefaultUploadSizeLimits = (overrides = {}) => ({
|
||||
image: 10485760,
|
||||
video: 209715200,
|
||||
audio: 52428800,
|
||||
document: 52428800,
|
||||
...overrides
|
||||
})
|
||||
|
||||
/**
|
||||
* MIME·파일명으로 업로드 종류를 판별한다.
|
||||
* @param {string} [mimeType] - MIME 타입
|
||||
* @param {string} [filename] - 파일명
|
||||
* @returns {'image'|'video'|'audio'|'document'} 업로드 종류
|
||||
*/
|
||||
export const getUploadKind = (mimeType = '', filename = '') => {
|
||||
const extension = getFileExtension(filename)
|
||||
|
||||
if (mimeType.startsWith('video/') || VIDEO_EXTENSIONS.has(extension)) {
|
||||
return UPLOAD_KIND.video
|
||||
}
|
||||
|
||||
if (mimeType.startsWith('audio/') || AUDIO_EXTENSIONS.has(extension)) {
|
||||
return UPLOAD_KIND.audio
|
||||
}
|
||||
|
||||
if (mimeType.startsWith('image/') || IMAGE_EXTENSIONS.has(extension)) {
|
||||
return UPLOAD_KIND.image
|
||||
}
|
||||
|
||||
return UPLOAD_KIND.document
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 종류별 최대 바이트를 반환한다.
|
||||
* @param {'image'|'video'|'audio'|'document'} kind - 업로드 종류
|
||||
* @param {{ image: number, video: number, audio: number, document: number }} limits - 종류별 한도
|
||||
* @returns {number} 최대 바이트
|
||||
*/
|
||||
export const getMaxUploadBytesForKind = (kind, limits) => limits[kind] ?? limits.image
|
||||
|
||||
/**
|
||||
* 바이트를 사람이 읽기 쉬운 용량 문자열로 변환한다.
|
||||
* @param {number} bytes - 바이트
|
||||
* @returns {string} 용량 문자열
|
||||
*/
|
||||
export const formatUploadSizeLimit = (bytes) => {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return '0B'
|
||||
}
|
||||
|
||||
if (bytes >= 1073741824) {
|
||||
const gigabytes = bytes / 1073741824
|
||||
return `${Number.isInteger(gigabytes) ? gigabytes : gigabytes.toFixed(1)}GB`
|
||||
}
|
||||
|
||||
if (bytes >= 1048576) {
|
||||
return `${Math.round(bytes / 1048576)}MB`
|
||||
}
|
||||
|
||||
if (bytes >= 1024) {
|
||||
return `${Math.round(bytes / 1024)}KB`
|
||||
}
|
||||
|
||||
return `${bytes}B`
|
||||
}
|
||||
|
||||
/**
|
||||
* 업로드 종류 한글 라벨을 반환한다.
|
||||
* @param {'image'|'video'|'audio'|'document'} kind - 업로드 종류
|
||||
* @returns {string} 한글 라벨
|
||||
*/
|
||||
export const getUploadKindLabel = (kind) => UPLOAD_KIND_LABELS[kind] || UPLOAD_KIND_LABELS[UPLOAD_KIND.image]
|
||||
Reference in New Issue
Block a user