322 lines
8.9 KiB
Vue
322 lines
8.9 KiB
Vue
<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-btn
|
|
color="primary"
|
|
@click="openAssignDialog"
|
|
>
|
|
캐릭터 연결
|
|
</v-btn>
|
|
</v-toolbar>
|
|
|
|
<v-container>
|
|
<v-card
|
|
v-if="detail"
|
|
class="pa-4"
|
|
>
|
|
<v-row>
|
|
<v-col
|
|
cols="12"
|
|
md="4"
|
|
>
|
|
<v-img
|
|
:src="detail.imageUrl"
|
|
contain
|
|
height="240"
|
|
/>
|
|
</v-col>
|
|
<v-col
|
|
cols="12"
|
|
md="8"
|
|
>
|
|
<h2>{{ detail.title }}</h2>
|
|
<div class="mt-2">
|
|
콘텐츠 타입: {{ detail.contentType || '-' }}
|
|
</div>
|
|
<div>카테고리(장르): {{ detail.category || '-' }}</div>
|
|
<div>19금 여부: {{ detail.isAdult ? '예' : '아니오' }}</div>
|
|
<div>
|
|
원작 링크:
|
|
<a
|
|
v-if="detail.originalLink"
|
|
:href="detail.originalLink"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>{{ detail.originalLink }}</a>
|
|
<span v-else>-</span>
|
|
</div>
|
|
<div class="mt-2">
|
|
작품 소개:
|
|
</div>
|
|
<div style="white-space:pre-wrap;">
|
|
{{ detail.description || '-' }}
|
|
</div>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card>
|
|
|
|
<v-card class="pa-4 mt-6">
|
|
<div class="d-flex align-center mb-4">
|
|
<h3>연결된 캐릭터</h3>
|
|
<v-spacer />
|
|
</div>
|
|
<v-row>
|
|
<v-col
|
|
v-for="c in characters"
|
|
:key="c.id"
|
|
cols="12"
|
|
sm="6"
|
|
md="4"
|
|
lg="3"
|
|
>
|
|
<v-card>
|
|
<v-img
|
|
:src="c.imagePath"
|
|
height="180"
|
|
contain
|
|
/>
|
|
<v-card-title class="text-no-wrap">
|
|
{{ c.name }}
|
|
</v-card-title>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn
|
|
small
|
|
color="error"
|
|
@click="unassign([c.id])"
|
|
>
|
|
해제
|
|
</v-btn>
|
|
<v-spacer />
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row v-if="isLoadingCharacters">
|
|
<v-col class="text-center">
|
|
<v-progress-circular
|
|
indeterminate
|
|
color="primary"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card>
|
|
</v-container>
|
|
|
|
<v-dialog
|
|
v-model="assignDialog"
|
|
max-width="800"
|
|
>
|
|
<v-card>
|
|
<v-card-title>캐릭터 연결</v-card-title>
|
|
<v-card-text>
|
|
<v-text-field
|
|
v-model="searchKeyword"
|
|
label="캐릭터 검색"
|
|
outlined
|
|
dense
|
|
@input="onSearchInput"
|
|
/>
|
|
<v-data-table
|
|
v-model="selectedToAssign"
|
|
:headers="headers"
|
|
:items="searchResults"
|
|
:loading="searchLoading"
|
|
item-key="id"
|
|
show-select
|
|
:items-per-page="5"
|
|
>
|
|
<template v-slot:item.imageUrl="{ item }">
|
|
<v-img
|
|
:src="item.imagePath"
|
|
max-width="60"
|
|
max-height="60"
|
|
class="rounded-circle"
|
|
/>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn
|
|
text
|
|
@click="assignDialog = false"
|
|
>
|
|
취소
|
|
</v-btn>
|
|
<v-btn
|
|
color="primary"
|
|
:disabled="selectedToAssign.length===0"
|
|
@click="assign"
|
|
>
|
|
연결
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { getOriginal, getOriginalCharacters, assignCharactersToOriginal, unassignCharactersFromOriginal } from '@/api/original'
|
|
import { searchCharacters } from '@/api/character'
|
|
|
|
export default {
|
|
name: 'OriginalDetail',
|
|
data() {
|
|
return {
|
|
id: null,
|
|
detail: null,
|
|
characters: [],
|
|
page: 1,
|
|
hasMore: true,
|
|
isLoadingCharacters: false,
|
|
assignDialog: false,
|
|
searchKeyword: '',
|
|
searchLoading: false,
|
|
searchResults: [],
|
|
selectedToAssign: [],
|
|
headers: [
|
|
{ text: '이미지', value: 'imageUrl', sortable: false },
|
|
{ text: '이름', value: 'name' },
|
|
{ text: 'ID', value: 'id' }
|
|
],
|
|
debounceTimer: null
|
|
}
|
|
},
|
|
created() {
|
|
this.id = this.$route.query.id
|
|
if (!this.id) {
|
|
this.$dialog.notify.error('잘못된 접근입니다.');
|
|
this.$router.push('/original-work');
|
|
return;
|
|
}
|
|
this.loadDetail();
|
|
this.loadCharacters();
|
|
window.addEventListener('scroll', this.onScroll);
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener('scroll', this.onScroll);
|
|
},
|
|
methods: {
|
|
notifyError(message) { this.$dialog.notify.error(message) },
|
|
notifySuccess(message) { this.$dialog.notify.success(message) },
|
|
goBack() { this.$router.push('/original-work') },
|
|
async loadDetail() {
|
|
try {
|
|
const res = await getOriginal(this.id);
|
|
if (res.status === 200 && res.data.success === true) {
|
|
this.detail = res.data.data;
|
|
} else {
|
|
this.notifyError('상세 조회 실패');
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('상세 조회 실패');
|
|
}
|
|
},
|
|
async loadCharacters() {
|
|
if (this.isLoadingCharacters || !this.hasMore) return;
|
|
this.isLoadingCharacters = true;
|
|
try {
|
|
const res = await getOriginalCharacters(this.id, this.page);
|
|
if (res.status === 200 && res.data.success === true) {
|
|
const content = res.data.data?.content || [];
|
|
this.characters = this.characters.concat(content);
|
|
this.hasMore = content.length > 0;
|
|
this.page++;
|
|
} else {
|
|
this.notifyError('캐릭터 목록 조회 실패');
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('캐릭터 목록 조회 실패');
|
|
} finally {
|
|
this.isLoadingCharacters = false;
|
|
}
|
|
},
|
|
onScroll() {
|
|
const scrollPosition = window.innerHeight + window.scrollY;
|
|
const documentHeight = document.documentElement.offsetHeight;
|
|
if (scrollPosition >= documentHeight - 200 && !this.isLoadingCharacters && this.hasMore) {
|
|
this.loadCharacters();
|
|
}
|
|
},
|
|
openAssignDialog() {
|
|
this.assignDialog = true;
|
|
this.searchKeyword = '';
|
|
this.searchResults = [];
|
|
this.selectedToAssign = [];
|
|
},
|
|
onSearchInput() {
|
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
this.debounceTimer = setTimeout(this.search, 300);
|
|
},
|
|
async search() {
|
|
if (!this.searchKeyword || !this.searchKeyword.trim()) {
|
|
this.searchResults = [];
|
|
return;
|
|
}
|
|
this.searchLoading = true;
|
|
try {
|
|
const res = await searchCharacters(this.searchKeyword.trim(), 1, 20);
|
|
if (res.status === 200 && res.data.success === true) {
|
|
this.searchResults = res.data.data?.content || [];
|
|
} else {
|
|
this.notifyError('검색 실패');
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('검색 실패');
|
|
} finally {
|
|
this.searchLoading = false;
|
|
}
|
|
},
|
|
async assign() {
|
|
if (this.selectedToAssign.length === 0) return;
|
|
try {
|
|
const ids = this.selectedToAssign.map(x => x.id);
|
|
const res = await assignCharactersToOriginal(this.id, ids);
|
|
if (res.status === 200 && res.data.success === true) {
|
|
this.notifySuccess('연결되었습니다.');
|
|
this.assignDialog = false;
|
|
// 목록 초기화 후 재조회
|
|
this.characters = [];
|
|
this.page = 1;
|
|
this.hasMore = true;
|
|
this.loadCharacters();
|
|
} else {
|
|
this.notifyError('연결 실패');
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('연결 실패');
|
|
}
|
|
},
|
|
async unassign(ids) {
|
|
if (!ids || ids.length === 0) return;
|
|
try {
|
|
const res = await unassignCharactersFromOriginal(this.id, ids);
|
|
if (res.status === 200 && res.data.success === true) {
|
|
this.notifySuccess('해제되었습니다.');
|
|
this.characters = this.characters.filter(c => !ids.includes(c.id));
|
|
} else {
|
|
this.notifyError('해제 실패');
|
|
}
|
|
} catch (e) {
|
|
this.notifyError('해제 실패');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
</style>
|