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;
}
+
+