캐릭터 챗봇 #74
|
@ -200,6 +200,53 @@ async function updateCharacterImageOrder(characterId, imageIds) {
|
||||||
return Vue.axios.put('/admin/chat/character/image/orders', { characterId, ids: imageIds })
|
return Vue.axios.put('/admin/chat/character/image/orders', { characterId, ids: imageIds })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 캐릭터 큐레이션 목록
|
||||||
|
async function getCharacterCurationList() {
|
||||||
|
return Vue.axios.get('/admin/chat/character/curation/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 큐레이션 등록
|
||||||
|
async function createCharacterCuration({ title, isAdult, isActive }) {
|
||||||
|
return Vue.axios.post('/admin/chat/character/curation/register', { title, isAdult, isActive })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 큐레이션 수정
|
||||||
|
// payload: { id: Long, title?, isAdult?, isActive? }
|
||||||
|
async function updateCharacterCuration(payload) {
|
||||||
|
return Vue.axios.put('/admin/chat/character/curation/update', payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 큐레이션 삭제
|
||||||
|
async function deleteCharacterCuration(curationId) {
|
||||||
|
return Vue.axios.delete(`/admin/chat/character/curation/${curationId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 캐릭터 큐레이션 정렬 순서 변경
|
||||||
|
async function updateCharacterCurationOrder(ids) {
|
||||||
|
return Vue.axios.put('/admin/chat/character/curation/reorder', { ids })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 큐레이션에 캐릭터 등록 (다중 등록)
|
||||||
|
// characterIds: Array<Long>
|
||||||
|
async function addCharacterToCuration(curationId, characterIds) {
|
||||||
|
return Vue.axios.post(`/admin/chat/character/curation/${curationId}/characters`, { characterIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 큐레이션에서 캐릭터 삭제
|
||||||
|
async function removeCharacterFromCuration(curationId, characterId) {
|
||||||
|
return Vue.axios.delete(`/admin/chat/character/curation/${curationId}/characters/${characterId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 큐레이션 내 캐릭터 정렬 순서 변경
|
||||||
|
async function updateCurationCharactersOrder(curationId, characterIds) {
|
||||||
|
return Vue.axios.put(`/admin/chat/character/curation/${curationId}/characters/reorder`, { characterIds })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 큐레이션 캐릭터 목록 조회 (가정된 엔드포인트)
|
||||||
|
async function getCharactersInCuration(curationId) {
|
||||||
|
return Vue.axios.get(`/admin/chat/character/curation/${curationId}/characters`)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCharacterList,
|
getCharacterList,
|
||||||
searchCharacters,
|
searchCharacters,
|
||||||
|
@ -216,5 +263,15 @@ export {
|
||||||
createCharacterImage,
|
createCharacterImage,
|
||||||
updateCharacterImage,
|
updateCharacterImage,
|
||||||
deleteCharacterImage,
|
deleteCharacterImage,
|
||||||
updateCharacterImageOrder
|
updateCharacterImageOrder,
|
||||||
|
// Character Curation
|
||||||
|
getCharacterCurationList,
|
||||||
|
createCharacterCuration,
|
||||||
|
updateCharacterCuration,
|
||||||
|
deleteCharacterCuration,
|
||||||
|
updateCharacterCurationOrder,
|
||||||
|
addCharacterToCuration,
|
||||||
|
removeCharacterFromCuration,
|
||||||
|
updateCurationCharactersOrder,
|
||||||
|
getCharactersInCuration
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,6 +111,11 @@ export default {
|
||||||
title: '캐릭터 리스트',
|
title: '캐릭터 리스트',
|
||||||
route: '/character',
|
route: '/character',
|
||||||
items: null
|
items: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '큐레이션',
|
||||||
|
route: '/character/curation',
|
||||||
|
items: null
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -280,6 +280,16 @@ const routes = [
|
||||||
name: 'CharacterImageForm',
|
name: 'CharacterImageForm',
|
||||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageForm.vue')
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterImageForm.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/character/curation',
|
||||||
|
name: 'CharacterCuration',
|
||||||
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCuration.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/character/curation/detail',
|
||||||
|
name: 'CharacterCurationDetail',
|
||||||
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue')
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,341 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>캐릭터 큐레이션</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-container>
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="4"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
dark
|
||||||
|
@click="showWriteDialog"
|
||||||
|
>
|
||||||
|
큐레이션 등록
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="curations"
|
||||||
|
:loading="isLoading"
|
||||||
|
item-key="id"
|
||||||
|
class="elevation-1"
|
||||||
|
hide-default-footer
|
||||||
|
disable-pagination
|
||||||
|
>
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<draggable
|
||||||
|
v-model="props.items"
|
||||||
|
tag="tbody"
|
||||||
|
@end="onDragEnd(props.items)"
|
||||||
|
>
|
||||||
|
<tr
|
||||||
|
v-for="item in props.items"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<td @click="goDetail(item)">
|
||||||
|
{{ item.title }}
|
||||||
|
</td>
|
||||||
|
<td @click="goDetail(item)">
|
||||||
|
<h3 v-if="item.isAdult">
|
||||||
|
O
|
||||||
|
</h3>
|
||||||
|
<h3 v-else>
|
||||||
|
X
|
||||||
|
</h3>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<v-row>
|
||||||
|
<v-col class="text-center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="primary"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="showModifyDialog(item)"
|
||||||
|
>
|
||||||
|
수정
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
<v-col class="text-center">
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="error"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="confirmDelete(item)"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</draggable>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- 등록/수정 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showDialog"
|
||||||
|
max-width="600px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
<span class="headline">{{ isModify ? '큐레이션 수정' : '큐레이션 등록' }}</span>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="form.title"
|
||||||
|
label="제목"
|
||||||
|
outlined
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-checkbox
|
||||||
|
v-model="form.isAdult"
|
||||||
|
label="19금"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
@click="closeDialog"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:disabled="!isFormValid || isSubmitting"
|
||||||
|
@click="saveCuration"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 삭제 확인 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showDeleteDialog"
|
||||||
|
max-width="400px"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
큐레이션 삭제
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>"{{ selectedCuration && selectedCuration.title }}"을(를) 삭제하시겠습니까?</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="deleteCuration"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
import {
|
||||||
|
getCharacterCurationList,
|
||||||
|
createCharacterCuration,
|
||||||
|
updateCharacterCuration,
|
||||||
|
deleteCharacterCuration,
|
||||||
|
updateCharacterCurationOrder
|
||||||
|
} from '@/api/character';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterCuration',
|
||||||
|
components: { draggable },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
curations: [],
|
||||||
|
headers: [
|
||||||
|
{ text: '제목', align: 'center', sortable: false, value: 'title' },
|
||||||
|
{ text: '19금', align: 'center', sortable: false, value: 'isAdult' },
|
||||||
|
{ text: '관리', align: 'center', sortable: false, value: 'management' }
|
||||||
|
],
|
||||||
|
showDialog: false,
|
||||||
|
isModify: false,
|
||||||
|
form: { id: null, title: '', isAdult: false },
|
||||||
|
selectedCuration: null,
|
||||||
|
showDeleteDialog: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isFormValid() {
|
||||||
|
return this.form.title && this.form.title.trim().length > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.loadCurations();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
notifyError(message) { this.$dialog.notify.error(message); },
|
||||||
|
notifySuccess(message) { this.$dialog.notify.success(message); },
|
||||||
|
|
||||||
|
async loadCurations() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getCharacterCurationList();
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.curations = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '목록을 불러오지 못했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('목록을 불러오지 못했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onDragEnd(items) {
|
||||||
|
const ids = items.map(i => i.id);
|
||||||
|
this.updateOrders(ids);
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateOrders(ids) {
|
||||||
|
try {
|
||||||
|
const res = await updateCharacterCurationOrder(ids);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('순서가 변경되었습니다.');
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('순서 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goDetail(item) {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'CharacterCurationDetail',
|
||||||
|
params: { curationId: item.id, title: item.title, isAdult: item.isAdult }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showWriteDialog() {
|
||||||
|
this.isModify = false;
|
||||||
|
this.form = { id: null, title: '', isAdult: false };
|
||||||
|
this.showDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
showModifyDialog(item) {
|
||||||
|
this.isModify = true;
|
||||||
|
this.form = { id: item.id, title: item.title, isAdult: item.isAdult };
|
||||||
|
this.showDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
this.showDialog = false;
|
||||||
|
this.form = { id: null, title: '', isAdult: false };
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveCuration() {
|
||||||
|
if (this.isSubmitting || !this.isFormValid) return;
|
||||||
|
this.isSubmitting = true;
|
||||||
|
try {
|
||||||
|
if (this.isModify) {
|
||||||
|
const payload = { id: this.form.id };
|
||||||
|
if (this.form.title) payload.title = this.form.title;
|
||||||
|
payload.isAdult = this.form.isAdult;
|
||||||
|
const res = await updateCharacterCuration(payload);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('수정되었습니다.');
|
||||||
|
this.closeDialog();
|
||||||
|
await this.loadCurations();
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '수정에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await createCharacterCuration({
|
||||||
|
title: this.form.title,
|
||||||
|
isAdult: this.form.isAdult,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('등록되었습니다.');
|
||||||
|
this.closeDialog();
|
||||||
|
await this.loadCurations();
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '등록에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('저장 중 오류가 발생했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(item) {
|
||||||
|
this.selectedCuration = item;
|
||||||
|
this.showDeleteDialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteCuration() {
|
||||||
|
if (!this.selectedCuration) return;
|
||||||
|
this.isSubmitting = true;
|
||||||
|
try {
|
||||||
|
const res = await deleteCharacterCuration(this.selectedCuration.id);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('삭제되었습니다.');
|
||||||
|
this.showDeleteDialog = false;
|
||||||
|
await this.loadCurations();
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('삭제에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
|
@ -0,0 +1,429 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-toolbar dark>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>{{ title }}</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-container>
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col
|
||||||
|
cols="4"
|
||||||
|
class="text-right"
|
||||||
|
>
|
||||||
|
19금 :
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="8">
|
||||||
|
{{ isAdult ? 'O' : 'X' }}
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row class="mb-4">
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="4"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
dark
|
||||||
|
@click="openAddDialog"
|
||||||
|
>
|
||||||
|
캐릭터 등록
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<draggable
|
||||||
|
v-model="characters"
|
||||||
|
class="row"
|
||||||
|
style="width: 100%"
|
||||||
|
:options="{ animation: 150 }"
|
||||||
|
@end="onDragEnd"
|
||||||
|
>
|
||||||
|
<v-col
|
||||||
|
v-for="ch in characters"
|
||||||
|
:key="ch.id"
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="3"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-img
|
||||||
|
:src="ch.imageUrl"
|
||||||
|
height="200"
|
||||||
|
contain
|
||||||
|
/>
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
{{ ch.name }}
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-text class="text-center">
|
||||||
|
{{ ch.description }}
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="error"
|
||||||
|
@click="confirmRemove(ch)"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</draggable>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="isLoading && characters.length === 0">
|
||||||
|
<v-col class="text-center">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
color="primary"
|
||||||
|
size="48"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="!isLoading && characters.length === 0">
|
||||||
|
<v-col class="text-center">
|
||||||
|
등록된 캐릭터가 없습니다.
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- 등록 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showAddDialog"
|
||||||
|
max-width="700px"
|
||||||
|
persistent
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>캐릭터 등록</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="searchWord"
|
||||||
|
label="캐릭터 검색"
|
||||||
|
outlined
|
||||||
|
@keyup.enter="search"
|
||||||
|
/>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
small
|
||||||
|
class="mb-2"
|
||||||
|
@click="search"
|
||||||
|
>
|
||||||
|
검색
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-row v-if="searchResults.length > 0 || addList.length > 0">
|
||||||
|
<v-col>
|
||||||
|
검색결과
|
||||||
|
<v-simple-table>
|
||||||
|
<template v-slot:default>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">
|
||||||
|
이름
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in searchResults"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="primary"
|
||||||
|
@click="addItem(item)"
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
<v-col v-if="addList.length > 0">
|
||||||
|
추가할 캐릭터
|
||||||
|
<v-simple-table>
|
||||||
|
<template>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center">
|
||||||
|
이름
|
||||||
|
</th>
|
||||||
|
<th />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="item in addList"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>
|
||||||
|
<v-btn
|
||||||
|
small
|
||||||
|
color="error"
|
||||||
|
@click="removeItem(item)"
|
||||||
|
>
|
||||||
|
제거
|
||||||
|
</v-btn>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
</v-simple-table>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-alert
|
||||||
|
v-else-if="searchPerformed"
|
||||||
|
type="info"
|
||||||
|
outlined
|
||||||
|
>
|
||||||
|
검색결과가 없습니다.
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
@click="closeAddDialog"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="blue darken-1"
|
||||||
|
:disabled="addList.length === 0 || isSubmitting"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
@click="addItemInCuration"
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 삭제 확인 다이얼로그 -->
|
||||||
|
<v-dialog
|
||||||
|
v-model="showDeleteDialog"
|
||||||
|
max-width="420px"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">
|
||||||
|
캐릭터 삭제
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>"{{ targetCharacter && targetCharacter.name }}"을(를) 큐레이션에서 삭제할까요?</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="removeTarget"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from 'vuedraggable';
|
||||||
|
import {
|
||||||
|
getCharactersInCuration,
|
||||||
|
addCharacterToCuration,
|
||||||
|
removeCharacterFromCuration,
|
||||||
|
updateCurationCharactersOrder,
|
||||||
|
searchCharacters
|
||||||
|
} from '@/api/character';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterCurationDetail',
|
||||||
|
components: { draggable },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: false,
|
||||||
|
isSubmitting: false,
|
||||||
|
curationId: null,
|
||||||
|
title: '',
|
||||||
|
isAdult: false,
|
||||||
|
characters: [],
|
||||||
|
showAddDialog: false,
|
||||||
|
showDeleteDialog: false,
|
||||||
|
targetCharacter: null,
|
||||||
|
searchWord: '',
|
||||||
|
searchResults: [],
|
||||||
|
searchPerformed: false,
|
||||||
|
addList: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.curationId = this.$route.params.curationId;
|
||||||
|
this.title = this.$route.params.title;
|
||||||
|
this.isAdult = this.$route.params.isAdult;
|
||||||
|
await this.loadCharacters();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
notifyError(message) { this.$dialog.notify.error(message); },
|
||||||
|
notifySuccess(message) { this.$dialog.notify.success(message); },
|
||||||
|
|
||||||
|
goBack() { this.$router.push({ name: 'CharacterCuration' }); },
|
||||||
|
|
||||||
|
async loadCharacters() {
|
||||||
|
this.isLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await getCharactersInCuration(this.curationId);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.characters = res.data.data || [];
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '캐릭터 목록을 불러오지 못했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('캐릭터 목록을 불러오지 못했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
openAddDialog() {
|
||||||
|
this.showAddDialog = true;
|
||||||
|
this.searchWord = '';
|
||||||
|
this.searchResults = [];
|
||||||
|
this.addList = [];
|
||||||
|
this.searchPerformed = false;
|
||||||
|
},
|
||||||
|
closeAddDialog() {
|
||||||
|
this.showAddDialog = false;
|
||||||
|
this.searchWord = '';
|
||||||
|
this.searchResults = [];
|
||||||
|
this.addList = [];
|
||||||
|
this.searchPerformed = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async search() {
|
||||||
|
if (!this.searchWord || this.searchWord.length < 2) {
|
||||||
|
this.notifyError('검색어를 2글자 이상 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await searchCharacters(this.searchWord);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
const data = res.data.data;
|
||||||
|
const list = data.content || [];
|
||||||
|
const existingIds = new Set(this.characters.map(c => c.id));
|
||||||
|
const pendingIds = new Set(this.addList.map(c => c.id));
|
||||||
|
this.searchResults = list.filter(item => !existingIds.has(item.id) && !pendingIds.has(item.id));
|
||||||
|
this.searchPerformed = true;
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '검색에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('검색에 실패했습니다.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addItem(item) {
|
||||||
|
// 검색결과에서 제거하고 추가 목록에 삽입 (중복 방지)
|
||||||
|
if (!this.addList.find(t => t.id === item.id)) {
|
||||||
|
this.addList.push(item);
|
||||||
|
}
|
||||||
|
this.searchResults = this.searchResults.filter(t => t.id !== item.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem(item) {
|
||||||
|
this.addList = this.addList.filter(t => t.id !== item.id);
|
||||||
|
// 제거 시 검색결과에 다시 추가
|
||||||
|
if (!this.searchResults.find(t => t.id === item.id)) {
|
||||||
|
this.searchResults.push(item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async addItemInCuration() {
|
||||||
|
if (!this.addList || this.addList.length === 0) return;
|
||||||
|
this.isSubmitting = true;
|
||||||
|
try {
|
||||||
|
const ids = this.addList.map(i => i.id);
|
||||||
|
const res = await addCharacterToCuration(this.curationId, ids);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess(`${this.addList.length}명 추가되었습니다.`);
|
||||||
|
this.closeAddDialog();
|
||||||
|
await this.loadCharacters();
|
||||||
|
} else {
|
||||||
|
this.notifyError((res.data && res.data.message) || '추가에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('추가에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmRemove(item) { this.targetCharacter = item; this.showDeleteDialog = true; },
|
||||||
|
|
||||||
|
async removeTarget() {
|
||||||
|
if (!this.targetCharacter) return;
|
||||||
|
this.isSubmitting = true;
|
||||||
|
try {
|
||||||
|
const res = await removeCharacterFromCuration(this.curationId, this.targetCharacter.id);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('삭제되었습니다.');
|
||||||
|
this.showDeleteDialog = false;
|
||||||
|
await this.loadCharacters();
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('삭제에 실패했습니다.');
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onDragEnd() {
|
||||||
|
try {
|
||||||
|
const ids = this.characters.map(c => c.id);
|
||||||
|
const res = await updateCurationCharactersOrder(this.curationId, ids);
|
||||||
|
if (res.status === 200 && res.data && res.data.success === true) {
|
||||||
|
this.notifySuccess('순서가 변경되었습니다.');
|
||||||
|
} else {
|
||||||
|
this.notifyError(res.data.message || '순서 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyError('순서 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
Loading…
Reference in New Issue