캐릭터 챗봇 #74
|
@ -154,6 +154,52 @@ async function updateCharacterBannerOrder(bannerIds) {
|
||||||
return Vue.axios.put('/admin/chat/banner/orders', {ids: bannerIds})
|
return Vue.axios.put('/admin/chat/banner/orders', {ids: bannerIds})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 리스트
|
||||||
|
async function getCharacterImageList(characterId, page = 1, size = 20) {
|
||||||
|
return Vue.axios.get('/admin/chat/character/image/list', {
|
||||||
|
params: { characterId, page: page - 1, size }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 상세
|
||||||
|
async function getCharacterImage(imageId) {
|
||||||
|
return Vue.axios.get(`/admin/chat/character/image/${imageId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 등록
|
||||||
|
async function createCharacterImage(imageData) {
|
||||||
|
const formData = new FormData()
|
||||||
|
if (imageData.image) formData.append('image', imageData.image)
|
||||||
|
const requestData = {
|
||||||
|
characterId: imageData.characterId,
|
||||||
|
imagePriceCan: imageData.imagePriceCan,
|
||||||
|
messagePriceCan: imageData.messagePriceCan,
|
||||||
|
isAdult: imageData.isAdult,
|
||||||
|
triggers: imageData.triggers || []
|
||||||
|
}
|
||||||
|
formData.append('request', JSON.stringify(requestData))
|
||||||
|
return Vue.axios.post('/admin/chat/character/image/register', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 수정 (트리거만 수정)
|
||||||
|
async function updateCharacterImage(imageData) {
|
||||||
|
const imageId = imageData.imageId
|
||||||
|
const payload = { triggers: imageData.triggers || [] }
|
||||||
|
return Vue.axios.put(`/admin/chat/character/image/${imageId}/triggers`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 삭제
|
||||||
|
async function deleteCharacterImage(imageId) {
|
||||||
|
return Vue.axios.delete(`/admin/chat/character/image/${imageId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 이미지 순서 변경
|
||||||
|
async function updateCharacterImageOrder(characterId, imageIds) {
|
||||||
|
return Vue.axios.put('/admin/chat/character/image/orders', { characterId, ids: imageIds })
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCharacterList,
|
getCharacterList,
|
||||||
searchCharacters,
|
searchCharacters,
|
||||||
|
@ -164,5 +210,11 @@ export {
|
||||||
createCharacterBanner,
|
createCharacterBanner,
|
||||||
updateCharacterBanner,
|
updateCharacterBanner,
|
||||||
deleteCharacterBanner,
|
deleteCharacterBanner,
|
||||||
updateCharacterBannerOrder
|
updateCharacterBannerOrder,
|
||||||
|
getCharacterImageList,
|
||||||
|
getCharacterImage,
|
||||||
|
createCharacterImage,
|
||||||
|
updateCharacterImage,
|
||||||
|
deleteCharacterImage,
|
||||||
|
updateCharacterImageOrder
|
||||||
}
|
}
|
||||||
|
|
|
@ -270,6 +270,16 @@ const routes = [
|
||||||
name: 'CharacterBanner',
|
name: 'CharacterBanner',
|
||||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue')
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterBanner.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/character/images',
|
||||||
|
name: 'CharacterImageList',
|
||||||
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/character/images/form',
|
||||||
|
name: 'CharacterImageForm',
|
||||||
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageForm.vue')
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,306 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>{{ isEdit ? '이미지 수정' : '이미지 등록' }}</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-container>
|
||||||
|
<v-card class="pa-4">
|
||||||
|
<v-form
|
||||||
|
ref="form"
|
||||||
|
v-model="isFormValid"
|
||||||
|
>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
v-show="!isEdit"
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-file-input
|
||||||
|
v-if="!isEdit"
|
||||||
|
v-model="form.image"
|
||||||
|
label="이미지 (800x1000 비율 권장)"
|
||||||
|
accept="image/*"
|
||||||
|
prepend-icon="mdi-camera"
|
||||||
|
show-size
|
||||||
|
truncate-length="15"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:rules="imageRules"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
v-if="previewImage || form.imageUrl"
|
||||||
|
cols="12"
|
||||||
|
:md="isEdit ? 12 : 6"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<v-img
|
||||||
|
:src="previewImage || form.imageUrl"
|
||||||
|
max-height="240"
|
||||||
|
:aspect-ratio="0.8"
|
||||||
|
contain
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="form.soloPurchasePriceCan"
|
||||||
|
label="이미지 단독 구매 가격(캔)"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-text-field
|
||||||
|
v-model.number="form.messagePurchasePriceCan"
|
||||||
|
label="메시지에서 구매 가격(캔)"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-switch
|
||||||
|
v-model="form.adult"
|
||||||
|
label="성인 이미지 여부"
|
||||||
|
inset
|
||||||
|
:disabled="isEdit"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-combobox
|
||||||
|
v-model="triggers"
|
||||||
|
label="트리거 단어 입력"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
small-chips
|
||||||
|
deletable-chips
|
||||||
|
outlined
|
||||||
|
dense
|
||||||
|
:rules="triggerRules"
|
||||||
|
@keydown.space.prevent="addTrigger"
|
||||||
|
>
|
||||||
|
<template v-slot:selection="{ attrs, item, select, selected }">
|
||||||
|
<v-chip
|
||||||
|
v-bind="attrs"
|
||||||
|
:input-value="selected"
|
||||||
|
close
|
||||||
|
@click="select"
|
||||||
|
@click:close="removeTrigger(item)"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-combobox>
|
||||||
|
<div class="caption grey--text text--darken-1">
|
||||||
|
트리거를 입력하고 엔터를 누르면 추가됩니다. (20자 이내, 최소 5개, 최대 10개)
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
:disabled="!canSubmit || isSubmitting"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
@click="save"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { createCharacterImage, updateCharacterImage, getCharacterImage } from '@/api/character'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterImageForm',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEdit: !!this.$route.query.imageId,
|
||||||
|
isSubmitting: false,
|
||||||
|
isFormValid: false,
|
||||||
|
characterId: Number(this.$route.query.characterId),
|
||||||
|
imageId: this.$route.query.imageId ? Number(this.$route.query.imageId) : null,
|
||||||
|
form: {
|
||||||
|
image: null,
|
||||||
|
imageUrl: '',
|
||||||
|
soloPurchasePriceCan: null,
|
||||||
|
messagePurchasePriceCan: null,
|
||||||
|
adult: false
|
||||||
|
},
|
||||||
|
previewImage: null,
|
||||||
|
triggers: [],
|
||||||
|
triggerRules: [
|
||||||
|
v => (v && v.length >= 5 && v.length <= 10) || '트리거는 최소 5개, 최대 10개까지 등록 가능합니다'
|
||||||
|
],
|
||||||
|
imageRules: [
|
||||||
|
v => !!v || '이미지를 선택하세요'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canSubmit() {
|
||||||
|
const triggersValid = this.triggers && this.triggers.length >= 5 && this.triggers.length <= 10
|
||||||
|
if (this.isEdit) return triggersValid
|
||||||
|
return !!this.form.image && triggersValid
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'form.image'(newVal) {
|
||||||
|
if (!this.isEdit) {
|
||||||
|
if (newVal) this.createImagePreview(newVal)
|
||||||
|
else this.previewImage = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (!this.characterId) {
|
||||||
|
this.notifyError('캐릭터 ID가 없습니다.')
|
||||||
|
this.goBack();
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isEdit && this.imageId) {
|
||||||
|
this.loadDetail()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
notifyError(m) { this.$dialog.notify.error(m) },
|
||||||
|
notifySuccess(m) { this.$dialog.notify.success(m) },
|
||||||
|
goBack() {
|
||||||
|
this.$router.push({ path: '/character/images', query: { characterId: this.characterId, name: this.$route.query.name || '' } })
|
||||||
|
},
|
||||||
|
createImagePreview(file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = e => { this.previewImage = e.target.result }
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
},
|
||||||
|
addTrigger(e) {
|
||||||
|
const value = (e.target.value || '').trim()
|
||||||
|
if (!value) return
|
||||||
|
if (value.length > 20) {
|
||||||
|
this.notifyError('트리거는 20자 이내여야 합니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.triggers.length >= 10) {
|
||||||
|
this.notifyError('트리거는 최대 10개까지 등록 가능합니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.triggers.includes(value)) this.triggers.push(value)
|
||||||
|
e.target.value = ''
|
||||||
|
},
|
||||||
|
removeTrigger(item) {
|
||||||
|
this.triggers = this.triggers.filter(t => t !== item)
|
||||||
|
},
|
||||||
|
async loadDetail() {
|
||||||
|
try {
|
||||||
|
const resp = await getCharacterImage(this.imageId)
|
||||||
|
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||||
|
const d = resp.data.data
|
||||||
|
// 수정 시 트리거만 노출하며 나머지는 비활성화
|
||||||
|
this.form.imageUrl = d.imageUrl
|
||||||
|
this.form.soloPurchasePriceCan = d.imagePriceCan
|
||||||
|
this.form.messagePurchasePriceCan = d.messagePriceCan
|
||||||
|
this.form.adult = d.isAdult
|
||||||
|
this.triggers = d.triggers || []
|
||||||
|
} else {
|
||||||
|
this.notifyError('이미지 정보를 불러오지 못했습니다.')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('이미지 상세 오류:', e)
|
||||||
|
this.notifyError('이미지 정보를 불러오지 못했습니다.')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
if (this.isSubmitting) return
|
||||||
|
// 트리거 개수 검증: 최소 5개, 최대 10개
|
||||||
|
if (!this.triggers || this.triggers.length < 5 || this.triggers.length > 10) {
|
||||||
|
this.notifyError('트리거는 최소 5개, 최대 10개여야 합니다.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.isSubmitting = true
|
||||||
|
try {
|
||||||
|
if (this.isEdit) {
|
||||||
|
const resp = await updateCharacterImage({ imageId: this.imageId, triggers: this.triggers })
|
||||||
|
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||||
|
this.notifySuccess('수정되었습니다.')
|
||||||
|
this.goBack()
|
||||||
|
} else {
|
||||||
|
this.notifyError('수정에 실패했습니다.')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resp = await createCharacterImage({
|
||||||
|
characterId: this.characterId,
|
||||||
|
image: this.form.image,
|
||||||
|
imagePriceCan: this.form.soloPurchasePriceCan,
|
||||||
|
messagePriceCan: this.form.messagePurchasePriceCan,
|
||||||
|
isAdult: this.form.adult,
|
||||||
|
triggers: this.triggers
|
||||||
|
})
|
||||||
|
if (resp && resp.status === 200 && resp.data && resp.data.success === true) {
|
||||||
|
this.notifySuccess('등록되었습니다.')
|
||||||
|
this.goBack()
|
||||||
|
} else {
|
||||||
|
this.notifyError('등록에 실패했습니다.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('이미지 저장 오류:', e)
|
||||||
|
this.notifyError('작업 중 오류가 발생했습니다.')
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -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>
|
|
@ -141,6 +141,16 @@
|
||||||
수정
|
수정
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="info"
|
||||||
|
:disabled="is_loading"
|
||||||
|
@click="goToImageList(item)"
|
||||||
|
>
|
||||||
|
이미지
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-btn
|
<v-btn
|
||||||
small
|
small
|
||||||
|
@ -306,6 +316,13 @@ export default {
|
||||||
this.$router.push('/character/form');
|
this.$router.push('/character/form');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
goToImageList(item) {
|
||||||
|
this.$router.push({
|
||||||
|
path: '/character/images',
|
||||||
|
query: { characterId: item.id, name: item.name }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
showEditDialog(item) {
|
showEditDialog(item) {
|
||||||
// 페이지로 이동하면서 id 전달
|
// 페이지로 이동하면서 id 전달
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|
Loading…
Reference in New Issue