feat(content): 메인 상단 배너 썸네일 이미지 크롭 기능 추가

- CropperJS 기반 크롭 다이얼로그 추가
- 이미지 선택 후 크롭하여 미리보기/업로드 파일에 반영
- 크롭퍼 초기화/해제 로직 및 에러 처리 추가
- 관련 스타일 및 UI 구성 정비
This commit is contained in:
Yu Sung
2026-05-28 15:00:17 +09:00
parent 90377bdb3c
commit 1581b735c1

View File

@@ -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>