feat(content): 메인 상단 배너 썸네일 이미지 크롭 기능 추가
- CropperJS 기반 크롭 다이얼로그 추가 - 이미지 선택 후 크롭하여 미리보기/업로드 파일에 반영 - 크롭퍼 초기화/해제 로직 및 에러 처리 추가 - 관련 스타일 및 UI 구성 정비
This commit is contained in:
@@ -290,6 +290,47 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 이미지 크롭 다이얼로그 (CropperJS) -->
|
||||
<v-dialog
|
||||
v-model="show_crop_dialog"
|
||||
max-width="900px"
|
||||
persistent
|
||||
>
|
||||
<v-card class="cropper-card">
|
||||
<v-card-title>
|
||||
이미지 크롭 (1:1)
|
||||
</v-card-title>
|
||||
<v-card-text class="cropper-card-text">
|
||||
<div class="cropper-canvas-wrap">
|
||||
<img
|
||||
v-if="crop_src"
|
||||
ref="cropperImage"
|
||||
:src="crop_src"
|
||||
alt="crop"
|
||||
class="cropper-dialog-img"
|
||||
>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions v-show="!is_loading">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="handleCropCancel"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="blue darken-1"
|
||||
text
|
||||
@click="handleCropConfirm"
|
||||
>
|
||||
확인
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="show_delete_confirm_dialog"
|
||||
max-width="400px"
|
||||
@@ -325,6 +366,7 @@
|
||||
<script>
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* CropperJS 기본 스타일 */
|
||||
@import '~cropperjs/dist/cropper.css';
|
||||
|
||||
.cropper-card {
|
||||
max-height: 80vh; /* 뷰포트 대비 제한하여 하단 버튼이 항상 보이도록 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cropper-card-text {
|
||||
flex: 1;
|
||||
overflow: auto; /* 내부 스크롤로 이미지만 스크롤되게 함 */
|
||||
}
|
||||
|
||||
.cropper-canvas-wrap {
|
||||
width: 100%;
|
||||
height: 60vh; /* 이미지 표시 영역 고정 */
|
||||
}
|
||||
|
||||
.cropper-dialog-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user