feat(character-image): 캐릭터 이미지 관리(목록/등록/수정/삭제/정렬) 추가
This commit is contained in:
325
src/views/Chat/CharacterImageList.vue
Normal file
325
src/views/Chat/CharacterImageList.vue
Normal file
@@ -0,0 +1,325 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar dark>
|
||||
<v-btn
|
||||
icon
|
||||
@click="goBack"
|
||||
>
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>캐릭터 이미지 관리</v-toolbar-title>
|
||||
<v-spacer />
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
<v-row class="align-center mb-4">
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<div class="subtitle-1">
|
||||
캐릭터: {{ characterName || characterId }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
class="text-right"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
dark
|
||||
@click="goToAdd"
|
||||
>
|
||||
이미지 추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 로딩 -->
|
||||
<v-row v-if="isLoading && images.length === 0">
|
||||
<v-col class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="48"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 목록 -->
|
||||
<draggable
|
||||
v-if="images.length > 0"
|
||||
v-model="images"
|
||||
class="image-grid"
|
||||
:options="{ animation: 150 }"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<div
|
||||
v-for="img in images"
|
||||
:key="img.id"
|
||||
class="image-card"
|
||||
>
|
||||
<v-card>
|
||||
<div class="image-wrapper">
|
||||
<v-img
|
||||
:src="img.imageUrl"
|
||||
:aspect-ratio="0.8"
|
||||
contain
|
||||
/>
|
||||
<div
|
||||
v-if="img.isAdult"
|
||||
class="ribbon"
|
||||
>
|
||||
성인
|
||||
</div>
|
||||
</div>
|
||||
<v-card-text class="pt-2">
|
||||
<div class="price-row d-flex align-center">
|
||||
<div class="price-label">
|
||||
단독 :
|
||||
</div>
|
||||
<div class="price-value">
|
||||
{{ img.imagePriceCan }} 캔
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-row d-flex align-center">
|
||||
<div class="price-label">
|
||||
메시지 :
|
||||
</div>
|
||||
<div class="price-value">
|
||||
{{ img.messagePriceCan }} 캔
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<v-chip
|
||||
v-for="(t, i) in (img.triggers || [])"
|
||||
:key="i"
|
||||
small
|
||||
class="ma-1"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
>
|
||||
{{ t }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
small
|
||||
color="primary"
|
||||
@click="goToEdit(img)"
|
||||
>
|
||||
수정
|
||||
</v-btn>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
@click="confirmDelete(img)"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<!-- 데이터 없음 -->
|
||||
<v-row v-if="!isLoading && images.length === 0">
|
||||
<v-col class="text-center grey--text">
|
||||
등록된 이미지가 없습니다.
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<v-dialog
|
||||
v-model="showDeleteDialog"
|
||||
max-width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
이미지 삭제
|
||||
</v-card-title>
|
||||
<v-card-text>삭제하시겠습니까?</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="blue darken-1"
|
||||
@click="showDeleteDialog = false"
|
||||
>
|
||||
취소
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
color="red darken-1"
|
||||
:loading="isSubmitting"
|
||||
@click="deleteImage"
|
||||
>
|
||||
삭제
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCharacterImageList, deleteCharacterImage, updateCharacterImageOrder } from '@/api/character'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
name: 'CharacterImageList',
|
||||
components: { draggable },
|
||||
data() {
|
||||
return {
|
||||
isLoading: false,
|
||||
isSubmitting: false,
|
||||
images: [],
|
||||
characterId: null,
|
||||
characterName: this.$route.query.name || '',
|
||||
showDeleteDialog: false,
|
||||
selectedImage: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.characterId = Number(this.$route.query.characterId)
|
||||
if (!this.characterId) {
|
||||
this.notifyError('캐릭터 ID가 없습니다.');
|
||||
this.goBack()
|
||||
return
|
||||
}
|
||||
this.loadImages()
|
||||
},
|
||||
methods: {
|
||||
notifyError(message) { this.$dialog.notify.error(message) },
|
||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
||||
goBack() { this.$router.push('/character') },
|
||||
async loadImages() {
|
||||
this.isLoading = true
|
||||
try {
|
||||
const resp = await getCharacterImageList(this.characterId, 1, 20)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
const data = resp.data.data
|
||||
this.images = (data.content || data || [])
|
||||
} else {
|
||||
this.notifyError('이미지 목록을 불러오지 못했습니다.')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 목록 오류:', e)
|
||||
this.notifyError('이미지 목록 조회 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
}
|
||||
},
|
||||
goToAdd() {
|
||||
this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, name: this.characterName } })
|
||||
},
|
||||
goToEdit(img) {
|
||||
this.$router.push({ path: '/character/images/form', query: { characterId: this.characterId, imageId: img.id, name: this.characterName } })
|
||||
},
|
||||
confirmDelete(img) {
|
||||
this.selectedImage = img
|
||||
this.showDeleteDialog = true
|
||||
},
|
||||
async deleteImage() {
|
||||
if (!this.selectedImage || this.isSubmitting) return
|
||||
this.isSubmitting = true
|
||||
try {
|
||||
const resp = await deleteCharacterImage(this.selectedImage.id)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('삭제되었습니다.')
|
||||
this.showDeleteDialog = false
|
||||
await this.loadImages()
|
||||
} else {
|
||||
this.notifyError('삭제에 실패했습니다.')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 삭제 오류:', e)
|
||||
this.notifyError('삭제 중 오류가 발생했습니다.')
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
},
|
||||
async onDragEnd() {
|
||||
try {
|
||||
const ids = this.images.map(img => img.id)
|
||||
const resp = await updateCharacterImageOrder(this.characterId, ids)
|
||||
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||
this.notifySuccess('이미지 순서가 변경되었습니다.')
|
||||
} else {
|
||||
this.notifyError('이미지 순서 변경에 실패했습니다.')
|
||||
await this.loadImages()
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('이미지 순서 변경 오류:', e)
|
||||
this.notifyError('이미지 순서 변경에 실패했습니다.')
|
||||
await this.loadImages()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
.image-card {
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 1264px) {
|
||||
.image-grid { grid-template-columns: repeat(4, 1fr); }
|
||||
}
|
||||
@media (max-width: 960px) {
|
||||
.image-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.image-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
/* Image wrapper for overlays */
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Ribbon style for adult indicator */
|
||||
.ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: #e53935; /* red darken-1 */
|
||||
color: #fff;
|
||||
padding: 6px 20px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
text-transform: none;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Price rows styling */
|
||||
.price-row {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.price-label {
|
||||
width: 72px; /* 긴 쪽 기준으로 라벨 고정폭 */
|
||||
text-align: left;
|
||||
color: rgba(0,0,0,0.6);
|
||||
font-weight: 700;
|
||||
}
|
||||
.price-value {
|
||||
flex: 1;
|
||||
font-weight: 700;
|
||||
color: rgba(0,0,0,0.87);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user