diff --git a/components/admin/AdminTagForm.vue b/components/admin/AdminTagForm.vue index b7a8b84..68b37b3 100644 --- a/components/admin/AdminTagForm.vue +++ b/components/admin/AdminTagForm.vue @@ -25,23 +25,32 @@ const props = defineProps({ const emit = defineEmits(['submit']) const slugTouched = ref(Boolean(props.initialTag.slug)) +const coverInputRef = ref(null) +const uploadingCover = ref(false) +const coverErrorMessage = ref('') const form = reactive({ name: props.initialTag.name || '', slug: props.initialTag.slug || '', description: props.initialTag.description || '', + headerImageUrl: props.initialTag.headerImageUrl || '', + headerTitle: props.initialTag.headerTitle || '', + headerText: props.initialTag.headerText || '', color: props.initialTag.color || '#15171a' }) /** * 태그 입력값을 저장 비교용 형태로 정규화한다. * @param {Object} tag - 태그 입력값 - * @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 정규화된 태그 입력값 + * @returns {{ name: string, slug: string, description: string, headerImageUrl: string, headerTitle: string, headerText: string, sortOrder: number, color: string, tagType: string }} 정규화된 태그 입력값 */ const normalizeTagPayload = (tag) => ({ name: String(tag.name || '').trim(), slug: toSlug(tag.slug || tag.name || ''), description: String(tag.description || '').trim(), + headerImageUrl: String(tag.headerImageUrl || '').trim(), + headerTitle: String(tag.headerTitle || '').trim(), + headerText: String(tag.headerText || '').trim(), sortOrder: Number(tag.sortOrder ?? 0), color: String(tag.color || '#15171a'), tagType: String(tag.tagType || props.defaultTagType) @@ -49,12 +58,15 @@ const normalizeTagPayload = (tag) => ({ /** * 현재 폼 입력값 - * @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 현재 저장 입력값 + * @returns {{ name: string, slug: string, description: string, headerImageUrl: string, headerTitle: string, headerText: string, sortOrder: number, color: string, tagType: string }} 현재 저장 입력값 */ const currentPayload = computed(() => normalizeTagPayload({ name: form.name, slug: form.slug || form.name, description: form.description, + headerImageUrl: form.headerImageUrl, + headerTitle: form.headerTitle, + headerText: form.headerText, sortOrder: props.initialTag.sortOrder ?? 0, color: form.color, tagType: props.initialTag.tagType || props.defaultTagType @@ -62,12 +74,15 @@ const currentPayload = computed(() => normalizeTagPayload({ /** * 최초 태그 입력값 - * @returns {{ name: string, slug: string, description: string, sortOrder: number, color: string, tagType: string }} 최초 저장 입력값 + * @returns {{ name: string, slug: string, description: string, headerImageUrl: string, headerTitle: string, headerText: string, sortOrder: number, color: string, tagType: string }} 최초 저장 입력값 */ const initialPayload = computed(() => normalizeTagPayload({ name: props.initialTag.name || '', slug: props.initialTag.slug || props.initialTag.name || '', description: props.initialTag.description || '', + headerImageUrl: props.initialTag.headerImageUrl || '', + headerTitle: props.initialTag.headerTitle || '', + headerText: props.initialTag.headerText || '', sortOrder: props.initialTag.sortOrder ?? 0, color: props.initialTag.color || '#15171a', tagType: props.initialTag.tagType || props.defaultTagType @@ -83,7 +98,7 @@ const hasChanges = computed(() => JSON.stringify(currentPayload.value) !== JSON. * 태그 저장 가능 여부 * @returns {boolean} 저장 가능 여부 */ -const canSubmit = computed(() => !props.saving && (!props.requireChanges || hasChanges.value)) +const canSubmit = computed(() => !props.saving && !uploadingCover.value && (!props.requireChanges || hasChanges.value)) /** * 문자열을 URL 슬러그로 변환 @@ -113,6 +128,56 @@ const touchSlug = () => { form.slug = toSlug(form.slug) } +/** + * 태그 헤더 이미지 선택 창을 연다. + * @returns {void} + */ +const openCoverPicker = () => { + coverInputRef.value?.click() +} + +/** + * 태그 헤더 이미지를 제거한다. + * @returns {void} + */ +const removeCoverImage = () => { + form.headerImageUrl = '' +} + +/** + * 태그 헤더 이미지를 업로드한다. + * @param {Event} event - 파일 입력 이벤트 + * @returns {Promise} 업로드 결과 + */ +const uploadCoverImage = async (event) => { + const input = event.target + const file = input instanceof HTMLInputElement ? input.files?.[0] : null + if (!file) { + return + } + + uploadingCover.value = true + coverErrorMessage.value = '' + + try { + const body = new FormData() + body.append('file', file) + const result = await $fetch('/admin/api/tags/cover', { + method: 'POST', + body + }) + + form.headerImageUrl = result.headerImageUrl || '' + } catch (error) { + coverErrorMessage.value = error?.data?.message || '태그 헤더 이미지를 업로드하지 못했습니다.' + } finally { + uploadingCover.value = false + if (input instanceof HTMLInputElement) { + input.value = '' + } + } +} + /** * 태그 입력값 제출 * @returns {void} @@ -178,6 +243,89 @@ const submitTag = () => { +
+
+
+

+ 태그 헤더 +

+

+ 이미지가 있으면 공개 태그 페이지의 기본 제목과 설명 대신 헤더 이미지를 표시합니다. +

+
+
+ + +
+
+ +
+ +
+ + +

+ {{ coverErrorMessage }} +

+ + + +