feat(content): 메인 상단 배너 썸네일 이미지 크롭 기능 추가
- CropperJS 기반 크롭 다이얼로그 추가 - 이미지 선택 후 크롭하여 미리보기/업로드 파일에 반영 - 크롭퍼 초기화/해제 로직 및 에러 처리 추가 - 관련 스타일 및 UI 구성 정비
This commit is contained in:
@@ -290,6 +290,47 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</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-dialog
|
||||||
v-model="show_delete_confirm_dialog"
|
v-model="show_delete_confirm_dialog"
|
||||||
max-width="400px"
|
max-width="400px"
|
||||||
@@ -325,6 +366,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Draggable from "vuedraggable";
|
import Draggable from "vuedraggable";
|
||||||
import debounce from "lodash/debounce";
|
import debounce from "lodash/debounce";
|
||||||
|
import Cropper from 'cropperjs'
|
||||||
|
|
||||||
import * as seriesApi from "@/api/audio_content_series"
|
import * as seriesApi from "@/api/audio_content_series"
|
||||||
import * as memberApi from "@/api/member";
|
import * as memberApi from "@/api/member";
|
||||||
@@ -343,6 +385,11 @@ export default {
|
|||||||
is_modify: false,
|
is_modify: false,
|
||||||
show_write_dialog: false,
|
show_write_dialog: false,
|
||||||
show_delete_confirm_dialog: false,
|
show_delete_confirm_dialog: false,
|
||||||
|
// 이미지 크롭용 상태
|
||||||
|
show_crop_dialog: false,
|
||||||
|
cropper: null,
|
||||||
|
crop_src: null,
|
||||||
|
selected_file_for_crop: null,
|
||||||
selected_banner: {},
|
selected_banner: {},
|
||||||
banner: {type: 'CREATOR', tab_id: 1, lang: 'ko'},
|
banner: {type: 'CREATOR', tab_id: 1, lang: 'ko'},
|
||||||
banners: [],
|
banners: [],
|
||||||
@@ -386,14 +433,101 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
imageAdd(payload) {
|
async imageAdd(payload) {
|
||||||
const file = payload;
|
const file = payload;
|
||||||
if (file) {
|
// 파일 해제 시 초기화
|
||||||
this.banner.thumbnail_image_url = URL.createObjectURL(file)
|
if (!file) {
|
||||||
URL.revokeObjectURL(file)
|
if (this.banner.thumbnail_image_url) {
|
||||||
} else {
|
URL.revokeObjectURL(this.banner.thumbnail_image_url)
|
||||||
|
}
|
||||||
|
this.banner.thumbnail_image = null
|
||||||
this.banner.thumbnail_image_url = 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() {
|
cancel() {
|
||||||
@@ -818,3 +952,30 @@ export default {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</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