diff --git a/src/views/Content/ContentMainTopBanner.vue b/src/views/Content/ContentMainTopBanner.vue index 5ee3e4f..e4596fc 100644 --- a/src/views/Content/ContentMainTopBanner.vue +++ b/src/views/Content/ContentMainTopBanner.vue @@ -290,6 +290,47 @@ + + + + + 이미지 크롭 (1:1) + + +
+ crop +
+
+ + + + 취소 + + + 확인 + + +
+
+ import Draggable from "vuedraggable"; import debounce from "lodash/debounce"; +import Cropper from 'cropperjs' import * as seriesApi from "@/api/audio_content_series" import * as memberApi from "@/api/member"; @@ -343,6 +385,11 @@ export default { is_modify: false, show_write_dialog: false, show_delete_confirm_dialog: false, + // 이미지 크롭용 상태 + show_crop_dialog: false, + cropper: null, + crop_src: null, + selected_file_for_crop: null, selected_banner: {}, banner: {type: 'CREATOR', tab_id: 1, lang: 'ko'}, banners: [], @@ -386,14 +433,101 @@ export default { }, methods: { - imageAdd(payload) { + async imageAdd(payload) { const file = payload; - if (file) { - this.banner.thumbnail_image_url = URL.createObjectURL(file) - URL.revokeObjectURL(file) - } else { + // 파일 해제 시 초기화 + if (!file) { + if (this.banner.thumbnail_image_url) { + URL.revokeObjectURL(this.banner.thumbnail_image_url) + } + this.banner.thumbnail_image = null this.banner.thumbnail_image_url = null + return } + + // CropperJS 다이얼로그 오픈 흐름 + if (this.crop_src) { + URL.revokeObjectURL(this.crop_src) + } + this.selected_file_for_crop = file + this.crop_src = URL.createObjectURL(file) + this.show_crop_dialog = true + + this.$nextTick(() => { + this.initCropper() + }) + }, + + initCropper() { + const imageEl = this.$refs.cropperImage + if (!imageEl) return + // 기존 인스턴스 제거 + if (this.cropper) { + this.cropper.destroy() + this.cropper = null + } + this.cropper = new Cropper(imageEl, { + aspectRatio: 1, + viewMode: 1, + autoCropArea: 1, + movable: true, + zoomable: true, + scalable: false, + rotatable: false, + responsive: true, + background: false, + }) + }, + + destroyCropper() { + if (this.cropper) { + this.cropper.destroy() + this.cropper = null + } + }, + + async handleCropConfirm() { + if (!this.cropper || !this.selected_file_for_crop) return + try { + this.is_loading = true + const mime = this.selected_file_for_crop.type && /^image\//.test(this.selected_file_for_crop.type) + ? this.selected_file_for_crop.type + : 'image/png' + const canvas = this.cropper.getCroppedCanvas() + await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (!blob) return reject(new Error('크롭된 이미지를 생성할 수 없습니다.')) + const croppedFile = new File([blob], this.selected_file_for_crop.name || 'image.png', { type: mime }) + // 기존 미리보기 URL 해제 + if (this.banner.thumbnail_image_url) { + URL.revokeObjectURL(this.banner.thumbnail_image_url) + } + this.banner.thumbnail_image = croppedFile + this.banner.thumbnail_image_url = URL.createObjectURL(croppedFile) + resolve() + }, mime) + }) + this.show_crop_dialog = false + } catch (e) { + this.notifyError('이미지 크롭 중 오류가 발생했습니다. 다시 시도해 주세요.') + } finally { + this.is_loading = false + this.cleanupCropDialog() + } + }, + + handleCropCancel() { + this.show_crop_dialog = false + this.cleanupCropDialog() + }, + + cleanupCropDialog() { + this.destroyCropper() + if (this.crop_src) { + URL.revokeObjectURL(this.crop_src) + this.crop_src = null + } + this.selected_file_for_crop = null }, cancel() { @@ -818,3 +952,30 @@ export default { margin-top: 10px; } + +