미디어 업로드와 태그 표시 수정

This commit is contained in:
2026-06-09 16:14:47 +09:00
parent e6669439f3
commit ed30926250
12 changed files with 162 additions and 32 deletions

View File

@@ -130,6 +130,13 @@ const mediaPickerAccept = computed(() => {
return '.pdf,.zip,.txt,.csv,.docx,.xlsx,.pptx'
})
/**
* 게시물 카드용 파생 썸네일 URL인지 확인한다.
* @param {string|null|undefined} url - 미디어 URL
* @returns {boolean} 카드 썸네일 여부
*/
const isPostCardThumbnailMediaUrl = (url) => /^\/uploads\/posts\/\d{4}\/\d{2}\/thumbs\/[^/?#]+-card\.webp$/i.test(String(url || ''))
/** 작성 textarea 최소 높이(px) */
const MIN_TEXTAREA_HEIGHT_PX = 620
@@ -1530,6 +1537,10 @@ const getMediaItemKind = (item) => {
* @returns {boolean} 선택 가능 여부
*/
const isMediaItemSelectableForTarget = (item) => {
if (isPostCardThumbnailMediaUrl(item?.url)) {
return false
}
const kind = getMediaItemKind(item)
if (['image', 'gallery', 'active-gallery'].includes(mediaPickerTarget.value)) {
@@ -2946,7 +2957,7 @@ const uploadMediaFiles = async (files) => {
* @returns {Promise<void>}
*/
const uploadAndInsert = async (files, target = 'image') => {
if (!files?.length) {
if (!files?.length || isUploading.value) {
return
}
@@ -2993,7 +3004,7 @@ const handleFileInput = async (event, target) => {
* @returns {Promise<void>}
*/
const uploadFromMediaModal = async (files) => {
if (!files?.length) {
if (!files?.length || isUploading.value) {
return
}
@@ -3024,6 +3035,11 @@ const uploadFromMediaModal = async (files) => {
* @returns {Promise<void>}
*/
const handleMediaModalDrop = async (event) => {
if (isUploading.value) {
event.preventDefault()
return
}
const files = Array.from(event.dataTransfer?.files || []).filter(isUploadFileAllowedForPicker)
if (!files.length) {
@@ -3095,6 +3111,10 @@ const mediaPickerUploadHint = computed(() => {
* @returns {Promise<void>}
*/
const handlePaste = async (event) => {
if (isUploading.value) {
return
}
const imageFiles = Array.from(event.clipboardData?.files || []).filter((file) => file.type.startsWith('image/'))
if (!imageFiles.length) {
@@ -3122,6 +3142,11 @@ const handlePaste = async (event) => {
* @returns {Promise<void>}
*/
const handleDrop = async (event) => {
if (isUploading.value) {
event.preventDefault()
return
}
const imageFiles = Array.from(event.dataTransfer?.files || []).filter((file) => file.type.startsWith('image/'))
if (!imageFiles.length) {
@@ -3413,24 +3438,29 @@ const handleKeydown = (event) => {
</template>
<div
v-else
class="admin-markdown-editor__media-upload-zone grid min-h-[420px] place-items-center rounded border border-dashed border-[#cfd5da] bg-[#fafafa] text-center"
class="admin-markdown-editor__media-upload-zone relative grid min-h-[420px] place-items-center overflow-hidden rounded border border-dashed border-[#cfd5da] bg-[#fafafa] text-center"
:class="isUploading ? 'admin-markdown-editor__media-upload-zone--uploading select-none border-[#8e9cac] bg-[#f3f5f7]' : ''"
@dragover.prevent
@drop.prevent="handleMediaModalDrop"
>
<div class="admin-markdown-editor__media-upload-inner grid gap-3 px-6">
<p class="admin-markdown-editor__media-upload-title text-lg font-semibold text-[#15171a]">
파일을 끌어 업로드
{{ isUploading ? '업로드 중입니다' : '파일을 끌어 업로드' }}
</p>
<p class="admin-markdown-editor__media-upload-or text-sm text-[#6b7280]">
또는
{{ isUploading ? '잠시만 기다려 주세요.' : '또는' }}
</p>
<label class="admin-markdown-editor__media-upload-button mx-auto inline-flex h-10 cursor-pointer items-center justify-center rounded border border-[#2b78d0] px-8 text-sm font-semibold text-[#1f6fbf] transition-colors hover:bg-blue-50">
<label
class="admin-markdown-editor__media-upload-button mx-auto inline-flex h-10 items-center justify-center rounded border border-[#2b78d0] px-8 text-sm font-semibold text-[#1f6fbf] transition-colors hover:bg-blue-50"
:class="isUploading ? 'cursor-not-allowed border-[#c8ced3] text-[#8e9cac] hover:bg-transparent' : 'cursor-pointer'"
>
{{ isUploading ? '업로드 중' : '파일 선택' }}
<input
class="sr-only"
type="file"
:accept="mediaPickerAccept"
:multiple="isGalleryMediaPicker"
:disabled="isUploading"
@change="uploadFromMediaModal($event.target.files); $event.target.value = ''"
>
</label>
@@ -3438,6 +3468,17 @@ const handleKeydown = (event) => {
{{ mediaPickerUploadHint }}
</p>
</div>
<div
v-if="isUploading"
class="admin-markdown-editor__media-upload-overlay pointer-events-none absolute inset-0 grid place-items-center bg-white/72 backdrop-blur-[1px]"
aria-live="polite"
>
<div class="admin-markdown-editor__media-upload-loading grid w-full max-w-sm gap-4 px-8 text-center">
<span class="admin-markdown-editor__media-upload-spinner mx-auto size-8 rounded-full border-2 border-[#d7dde2] border-t-[#15171a]" aria-hidden="true" />
<strong class="text-sm font-semibold text-[#15171a]">업로드 중입니다</strong>
<span class="admin-markdown-editor__media-upload-skeleton h-2 overflow-hidden rounded-full bg-[#e3e6e8]" aria-hidden="true" />
</div>
</div>
</div>
</div>
@@ -3480,4 +3521,49 @@ const handleKeydown = (event) => {
.admin-markdown-editor__gutter::-webkit-scrollbar {
display: none;
}
.admin-markdown-editor__media-upload-zone--uploading::before {
position: absolute;
inset: 0;
content: '';
background: linear-gradient(110deg, transparent 0%, rgba(255, 255, 255, 0.72) 45%, transparent 72%);
transform: translateX(-100%);
animation: admin-markdown-editor-upload-sheen 1.4s ease-in-out infinite;
}
.admin-markdown-editor__media-upload-spinner {
animation: admin-markdown-editor-upload-spin 0.8s linear infinite;
}
.admin-markdown-editor__media-upload-skeleton::before {
display: block;
width: 40%;
height: 100%;
content: '';
border-radius: inherit;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.86), transparent);
animation: admin-markdown-editor-upload-skeleton 1.15s ease-in-out infinite;
}
@keyframes admin-markdown-editor-upload-spin {
to {
transform: rotate(360deg);
}
}
@keyframes admin-markdown-editor-upload-sheen {
to {
transform: translateX(100%);
}
}
@keyframes admin-markdown-editor-upload-skeleton {
from {
transform: translateX(-120%);
}
to {
transform: translateX(260%);
}
}
</style>