Compare commits
74 Commits
test
...
18b59b5598
Author | SHA1 | Date | |
---|---|---|---|
18b59b5598 | |||
5fcdd7f06d | |||
1e149f7e41 | |||
aca3767a24 | |||
d51655f15e | |||
47dd32939f | |||
2e1891ab08 | |||
99d70cc8f7 | |||
9f1675e82d | |||
c2838be2ed | |||
b5c2941c0d | |||
d5c01d8d23 | |||
7118b0649a | |||
8f5346581e | |||
e43f2e30be | |||
397fd267e0 | |||
fe4b88350b | |||
537474e162 | |||
b5abdf3cf5 | |||
a2e457b5e8 | |||
05ddd417cd | |||
e70426af68 | |||
81b33e1322 | |||
588fcfbe90 | |||
ff2c126382 | |||
702daca29f | |||
8e9008a3c1 | |||
5c0c00aad4 | |||
e0949c6d73 | |||
0449bac8d5 | |||
d412c15c9d | |||
ed16a6ddad | |||
f06e2d41e0 | |||
7505269db3 | |||
15eeb6943d | |||
7e7ed46cea | |||
fd01786649 | |||
c48c1c2f09 | |||
9bcf3a3cdb | |||
4c5b987d98 | |||
f168403048 | |||
82ee1584e7 | |||
65cb918389 | |||
784baf9a2f | |||
7a85ac41cc | |||
9d4c9437cf | |||
68845aeae1 | |||
bbdca29337 | |||
c14c041daa | |||
a515a144eb | |||
54a6773905 | |||
d97087b4e9 | |||
ddb2449053 | |||
8aca07cdf7 | |||
0ba845d95a | |||
64b1fd5395 | |||
639bea70fa | |||
6a89ba059b | |||
ff83041585 | |||
e660be0bf4 | |||
62cdd57069 | |||
f8346ed5ef | |||
9656b9a9d1 | |||
97a58266bb | |||
8fc0cfa345 | |||
22f9c2287d | |||
9284f7d5c3 | |||
e6f27a4529 | |||
6a33d1c024 | |||
3b83789c15 | |||
55f0ab9af3 | |||
9b168a6112 | |||
c47937933e | |||
4744fe7d9a |
@@ -24,7 +24,7 @@ async function getCalculateCommunityPost(startDate, endDate, page, size) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getSettlementRatio(page) {
|
async function getSettlementRatio(page) {
|
||||||
return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20");
|
return Vue.axios.get('/admin/calculate/ratio?page=' + (page - 1) + "&size=20'");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createCreatorSettlementRatio(creatorSettlementRatio) {
|
async function createCreatorSettlementRatio(creatorSettlementRatio) {
|
||||||
@@ -57,21 +57,6 @@ async function getCalculateCommunityByCreator(startDate, endDate, page, size) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateCreatorSettlementRatio(creatorSettlementRatio) {
|
|
||||||
const request = {
|
|
||||||
memberId: creatorSettlementRatio.creator_id,
|
|
||||||
subsidy: creatorSettlementRatio.subsidy,
|
|
||||||
liveSettlementRatio: creatorSettlementRatio.liveSettlementRatio,
|
|
||||||
contentSettlementRatio: creatorSettlementRatio.contentSettlementRatio,
|
|
||||||
communitySettlementRatio: creatorSettlementRatio.communitySettlementRatio
|
|
||||||
};
|
|
||||||
return Vue.axios.post('/admin/calculate/ratio/update', request);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteCreatorSettlementRatio(memberId) {
|
|
||||||
return Vue.axios.post('/admin/calculate/ratio/delete/' + memberId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getCalculateLive,
|
getCalculateLive,
|
||||||
getCalculateContent,
|
getCalculateContent,
|
||||||
@@ -80,8 +65,6 @@ export {
|
|||||||
getCalculateCommunityPost,
|
getCalculateCommunityPost,
|
||||||
getSettlementRatio,
|
getSettlementRatio,
|
||||||
createCreatorSettlementRatio,
|
createCreatorSettlementRatio,
|
||||||
updateCreatorSettlementRatio,
|
|
||||||
deleteCreatorSettlementRatio,
|
|
||||||
getCalculateLiveByCreator,
|
getCalculateLiveByCreator,
|
||||||
getCalculateContentByCreator,
|
getCalculateContentByCreator,
|
||||||
getCalculateCommunityByCreator
|
getCalculateCommunityByCreator
|
||||||
|
@@ -7,20 +7,13 @@ async function getCharacterList(page = 1, size = 20) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 캐릭터 검색 (배너용 기존 함수)
|
// 캐릭터 검색
|
||||||
async function searchCharacters(searchTerm, page = 1, size = 20) {
|
async function searchCharacters(searchTerm, page = 1, size = 20) {
|
||||||
return Vue.axios.get('/admin/chat/banner/search-character', {
|
return Vue.axios.get('/admin/chat/banner/search-character', {
|
||||||
params: { searchTerm, page: page - 1, size }
|
params: { searchTerm, page: page - 1, size }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 캐릭터 리스트 검색 (요구사항: /admin/chat/character/search)
|
|
||||||
async function searchCharacterList(searchTerm, page = 1, size = 20) {
|
|
||||||
return Vue.axios.get('/admin/chat/character/search', {
|
|
||||||
params: { searchTerm, page: page - 1, size }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 캐릭터 상세 조회
|
// 캐릭터 상세 조회
|
||||||
async function getCharacter(id) {
|
async function getCharacter(id) {
|
||||||
return Vue.axios.get(`/admin/chat/character/${id}`)
|
return Vue.axios.get(`/admin/chat/character/${id}`)
|
||||||
@@ -50,7 +43,8 @@ async function createCharacter(characterData) {
|
|||||||
gender: toNullIfBlank(characterData.gender),
|
gender: toNullIfBlank(characterData.gender),
|
||||||
mbti: toNullIfBlank(characterData.mbti),
|
mbti: toNullIfBlank(characterData.mbti),
|
||||||
characterType: toNullIfBlank(characterData.type),
|
characterType: toNullIfBlank(characterData.type),
|
||||||
originalWorkId: characterData.originalWorkId || null,
|
originalTitle: toNullIfBlank(characterData.originalTitle),
|
||||||
|
originalLink: toNullIfBlank(characterData.originalLink),
|
||||||
speechPattern: toNullIfBlank(characterData.speechPattern),
|
speechPattern: toNullIfBlank(characterData.speechPattern),
|
||||||
speechStyle: toNullIfBlank(characterData.speechStyle),
|
speechStyle: toNullIfBlank(characterData.speechStyle),
|
||||||
appearance: toNullIfBlank(characterData.appearance),
|
appearance: toNullIfBlank(characterData.appearance),
|
||||||
@@ -264,7 +258,6 @@ async function getCharacterCalculateList({ startDateStr, endDateStr, sort = 'TOT
|
|||||||
export {
|
export {
|
||||||
getCharacterList,
|
getCharacterList,
|
||||||
searchCharacters,
|
searchCharacters,
|
||||||
searchCharacterList,
|
|
||||||
getCharacter,
|
getCharacter,
|
||||||
createCharacter,
|
createCharacter,
|
||||||
updateCharacter,
|
updateCharacter,
|
||||||
|
@@ -1,87 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
// 공통: 값 그대로 전달 (빈 문자열 유지)
|
|
||||||
function toNullIfBlank(value) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value.trim() === '' ? null : value;
|
|
||||||
}
|
|
||||||
return value === '' ? null : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 리스트
|
|
||||||
export async function getOriginalList(page = 1, size = 20) {
|
|
||||||
return Vue.axios.get('/admin/chat/original/list', {
|
|
||||||
params: { page: page - 1, size }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 등록
|
|
||||||
export async function createOriginal(data) {
|
|
||||||
const formData = new FormData();
|
|
||||||
if (data.image) formData.append('image', data.image);
|
|
||||||
const request = {
|
|
||||||
title: toNullIfBlank(data.title),
|
|
||||||
contentType: toNullIfBlank(data.contentType),
|
|
||||||
category: toNullIfBlank(data.category),
|
|
||||||
isAdult: !!data.isAdult,
|
|
||||||
description: toNullIfBlank(data.description),
|
|
||||||
originalLink: toNullIfBlank(data.originalLink), // 원천 원작 링크
|
|
||||||
originalWork: toNullIfBlank(data.originalWork),
|
|
||||||
writer: toNullIfBlank(data.writer),
|
|
||||||
studio: toNullIfBlank(data.studio),
|
|
||||||
originalLinks: Array.isArray(data.originalLinks) ? data.originalLinks : [],
|
|
||||||
tags: Array.isArray(data.tags) ? data.tags : []
|
|
||||||
};
|
|
||||||
formData.append('request', JSON.stringify(request));
|
|
||||||
return Vue.axios.post('/admin/chat/original/register', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 수정
|
|
||||||
export async function updateOriginal(data, image = null) {
|
|
||||||
const formData = new FormData();
|
|
||||||
if (image) formData.append('image', image);
|
|
||||||
const processed = {};
|
|
||||||
Object.keys(data).forEach(key => {
|
|
||||||
processed[key] = data[key];
|
|
||||||
})
|
|
||||||
formData.append('request', JSON.stringify(processed));
|
|
||||||
return Vue.axios.put('/admin/chat/original/update', formData, {
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 삭제
|
|
||||||
export async function deleteOriginal(id) {
|
|
||||||
return Vue.axios.delete(`/admin/chat/original/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 상세
|
|
||||||
export async function getOriginal(id) {
|
|
||||||
return Vue.axios.get(`/admin/chat/original/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 속 캐릭터 조회
|
|
||||||
export async function getOriginalCharacters(id, page = 1, size = 20) {
|
|
||||||
return Vue.axios.get(`/admin/chat/original/${id}/characters`, {
|
|
||||||
params: { page: page - 1, size }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작 검색
|
|
||||||
export async function searchOriginals(searchTerm) {
|
|
||||||
return Vue.axios.get('/admin/chat/original/search', {
|
|
||||||
params: { searchTerm }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작에 캐릭터 연결
|
|
||||||
export async function assignCharactersToOriginal(id, characterIds = []) {
|
|
||||||
return Vue.axios.post(`/admin/chat/original/${id}/assign-characters`, { characterIds })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원작에서 캐릭터 연결 해제
|
|
||||||
export async function unassignCharactersFromOriginal(id, characterIds = []) {
|
|
||||||
return Vue.axios.post(`/admin/chat/original/${id}/unassign-characters`, { characterIds })
|
|
||||||
}
|
|
@@ -122,11 +122,6 @@ export default {
|
|||||||
route: '/character/calculate',
|
route: '/character/calculate',
|
||||||
items: null
|
items: null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '원작',
|
|
||||||
route: '/original-work',
|
|
||||||
items: null
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@@ -291,24 +291,9 @@ const routes = [
|
|||||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue')
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/character/calculate',
|
path: '/character/calculate',
|
||||||
name: 'CharacterCalculate',
|
name: 'CharacterCalculate',
|
||||||
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
|
component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue')
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/original-work',
|
|
||||||
name: 'OriginalList',
|
|
||||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalList.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/original-work/form',
|
|
||||||
name: 'OriginalForm',
|
|
||||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalForm.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/original-work/detail',
|
|
||||||
name: 'OriginalDetail',
|
|
||||||
component: () => import(/* webpackChunkName: "original" */ '../views/Chat/OriginalDetail.vue')
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@@ -60,7 +60,7 @@
|
|||||||
max-width="300"
|
max-width="300"
|
||||||
>
|
>
|
||||||
<v-img
|
<v-img
|
||||||
:src="banner.imagePath"
|
:src="banner.imageUrl"
|
||||||
height="200"
|
height="200"
|
||||||
contain
|
contain
|
||||||
/>
|
/>
|
||||||
|
@@ -206,64 +206,29 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- 원작 선택 -->
|
<!-- 원작 정보 -->
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col
|
||||||
<v-autocomplete
|
cols="12"
|
||||||
v-model="selectedOriginalId"
|
md="6"
|
||||||
:items="originalOptions"
|
>
|
||||||
:loading="originalLoading"
|
<v-text-field
|
||||||
:search-input.sync="originalSearchTerm"
|
v-model="character.originalTitle"
|
||||||
item-text="title"
|
label="원작명"
|
||||||
item-value="id"
|
|
||||||
label="원작 검색 후 선택"
|
|
||||||
hide-no-data
|
|
||||||
hide-selected
|
|
||||||
clearable
|
|
||||||
outlined
|
outlined
|
||||||
dense
|
dense
|
||||||
@change="onOriginalChange"
|
/>
|
||||||
>
|
|
||||||
<template v-slot:item="{ item, on, attrs }">
|
|
||||||
<v-list-item
|
|
||||||
v-bind="attrs"
|
|
||||||
v-on="on"
|
|
||||||
>
|
|
||||||
<v-list-item-avatar>
|
|
||||||
<v-img :src="item.imageUrl" />
|
|
||||||
</v-list-item-avatar>
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title v-text="item.title" />
|
|
||||||
<v-list-item-subtitle v-text="item.category" />
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
</template>
|
|
||||||
</v-autocomplete>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
<v-col
|
||||||
<v-row v-if="selectedOriginal">
|
cols="12"
|
||||||
<v-col cols="12">
|
md="6"
|
||||||
<div class="d-flex align-center">
|
>
|
||||||
<v-avatar
|
<v-text-field
|
||||||
size="60"
|
v-model="character.originalLink"
|
||||||
class="mr-3"
|
label="원작링크"
|
||||||
>
|
outlined
|
||||||
<v-img :src="selectedOriginal.imageUrl" />
|
dense
|
||||||
</v-avatar>
|
/>
|
||||||
<div>
|
|
||||||
<div class="subtitle-1">
|
|
||||||
{{ selectedOriginal.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
text
|
|
||||||
@click="clearSelectedOriginal"
|
|
||||||
>
|
|
||||||
해제
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
@@ -1053,7 +1018,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {getCharacter, createCharacter, updateCharacter} from '@/api/character';
|
import {getCharacter, createCharacter, updateCharacter} from '@/api/character';
|
||||||
import { searchOriginals } from '@/api/original';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CharacterForm",
|
name: "CharacterForm",
|
||||||
@@ -1091,13 +1055,6 @@ export default {
|
|||||||
personalities: [],
|
personalities: [],
|
||||||
backgrounds: [],
|
backgrounds: [],
|
||||||
originalCharacter: null, // 원본 캐릭터 데이터 저장용
|
originalCharacter: null, // 원본 캐릭터 데이터 저장용
|
||||||
// 원작 선택 상태
|
|
||||||
selectedOriginalId: null,
|
|
||||||
selectedOriginal: null,
|
|
||||||
originalOptions: [],
|
|
||||||
originalSearchTerm: '',
|
|
||||||
originalLoading: false,
|
|
||||||
originalDebounce: null,
|
|
||||||
character: {
|
character: {
|
||||||
id: null,
|
id: null,
|
||||||
name: '',
|
name: '',
|
||||||
@@ -1109,7 +1066,6 @@ export default {
|
|||||||
age: '',
|
age: '',
|
||||||
mbti: '',
|
mbti: '',
|
||||||
characterType: '',
|
characterType: '',
|
||||||
originalWorkId: null,
|
|
||||||
originalTitle: '',
|
originalTitle: '',
|
||||||
originalLink: '',
|
originalLink: '',
|
||||||
speechPattern: '',
|
speechPattern: '',
|
||||||
@@ -1209,14 +1165,6 @@ export default {
|
|||||||
this.previewImage = null;
|
this.previewImage = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
originalSearchTerm(val) {
|
|
||||||
if (this.originalDebounce) clearTimeout(this.originalDebounce);
|
|
||||||
if (!val || !val.trim()) {
|
|
||||||
this.originalOptions = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.originalDebounce = setTimeout(this.searchOriginalWorks, 300);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1237,55 +1185,6 @@ export default {
|
|||||||
this.$dialog.notify.success(message);
|
this.$dialog.notify.success(message);
|
||||||
},
|
},
|
||||||
|
|
||||||
async searchOriginalWorks() {
|
|
||||||
try {
|
|
||||||
this.originalLoading = true;
|
|
||||||
const term = (this.originalSearchTerm || '').trim();
|
|
||||||
if (!term) {
|
|
||||||
this.originalOptions = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await searchOriginals(term);
|
|
||||||
if (res && res.status === 200 && res.data && res.data.success === true) {
|
|
||||||
const data = res.data.data;
|
|
||||||
const items = (data && data.content) ? data.content : (Array.isArray(data) ? data : []);
|
|
||||||
this.originalOptions = items || [];
|
|
||||||
} else {
|
|
||||||
this.originalOptions = [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.originalOptions = [];
|
|
||||||
} finally {
|
|
||||||
this.originalLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onOriginalChange(val) {
|
|
||||||
if (!val) {
|
|
||||||
this.selectedOriginal = null;
|
|
||||||
this.selectedOriginalId = null;
|
|
||||||
this.character.originalWorkId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const id = Number(val);
|
|
||||||
const found = (this.originalOptions || []).find(o => Number(o.id) === id);
|
|
||||||
if (found) {
|
|
||||||
this.selectedOriginal = { id: Number(found.id), title: found.title, imageUrl: found.imageUrl };
|
|
||||||
} else if (this.selectedOriginal && Number(this.selectedOriginal.id) === id) {
|
|
||||||
// keep current selectedOriginal
|
|
||||||
} else {
|
|
||||||
this.selectedOriginal = { id };
|
|
||||||
}
|
|
||||||
this.selectedOriginalId = id;
|
|
||||||
this.character.originalWorkId = id;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearSelectedOriginal() {
|
|
||||||
this.selectedOriginal = null;
|
|
||||||
this.selectedOriginalId = null;
|
|
||||||
this.character.originalWorkId = null;
|
|
||||||
},
|
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
this.$router.push('/character');
|
this.$router.push('/character');
|
||||||
},
|
},
|
||||||
@@ -1567,11 +1466,10 @@ export default {
|
|||||||
// 로드된 캐릭터 데이터에서 null을 빈 문자열로 변환 (UI 표시용)
|
// 로드된 캐릭터 데이터에서 null을 빈 문자열로 변환 (UI 표시용)
|
||||||
normalizeCharacterData(data) {
|
normalizeCharacterData(data) {
|
||||||
const result = { ...data };
|
const result = { ...data };
|
||||||
// 기본값 보정
|
|
||||||
if (result.originalWorkId == null) result.originalWorkId = null;
|
|
||||||
const simpleFields = [
|
const simpleFields = [
|
||||||
'name', 'systemPrompt', 'description', 'age', 'gender', 'mbti',
|
'name', 'systemPrompt', 'description', 'age', 'gender', 'mbti',
|
||||||
'characterType', 'speechPattern', 'speechStyle', 'appearance', 'imageUrl'
|
'characterType', 'originalTitle', 'originalLink', 'speechPattern',
|
||||||
|
'speechStyle', 'appearance', 'imageUrl'
|
||||||
];
|
];
|
||||||
simpleFields.forEach(f => {
|
simpleFields.forEach(f => {
|
||||||
if (result[f] == null) result[f] = '';
|
if (result[f] == null) result[f] = '';
|
||||||
@@ -1591,7 +1489,8 @@ export default {
|
|||||||
gender: this.character.gender,
|
gender: this.character.gender,
|
||||||
mbti: this.character.mbti,
|
mbti: this.character.mbti,
|
||||||
characterType: this.character.characterType,
|
characterType: this.character.characterType,
|
||||||
originalWorkId: this.character.originalWorkId,
|
originalTitle: this.character.originalTitle,
|
||||||
|
originalLink: this.character.originalLink,
|
||||||
speechPattern: this.character.speechPattern,
|
speechPattern: this.character.speechPattern,
|
||||||
speechStyle: this.character.speechStyle,
|
speechStyle: this.character.speechStyle,
|
||||||
appearance: this.character.appearance,
|
appearance: this.character.appearance,
|
||||||
@@ -1614,7 +1513,7 @@ export default {
|
|||||||
|
|
||||||
// 기본 필드 비교
|
// 기본 필드 비교
|
||||||
const simpleFields = [
|
const simpleFields = [
|
||||||
'name', 'description', 'age', 'gender', 'mbti', 'characterType', 'originalWorkId',
|
'name', 'description', 'age', 'gender', 'mbti', 'characterType', 'originalTitle', 'originalLink',
|
||||||
'speechPattern', 'speechStyle', 'isActive'
|
'speechPattern', 'speechStyle', 'isActive'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1664,15 +1563,6 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 특수 규칙: 기존에 원작이 연결되어 있었고, 해제(선택 제거)한 경우 서버 규약에 따라 0으로 전송
|
|
||||||
if (this.isEdit && ('originalWorkId' in changedFields)) {
|
|
||||||
const prev = this.originalCharacter && this.originalCharacter.originalWorkId;
|
|
||||||
const curr = changedFields.originalWorkId;
|
|
||||||
if ((curr === null || curr === undefined || curr === '') && (prev !== null && prev !== undefined && Number(prev) > 0)) {
|
|
||||||
changedFields.originalWorkId = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changedFields;
|
return changedFields;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1696,18 +1586,6 @@ export default {
|
|||||||
image: null // 파일 입력은 초기화
|
image: null // 파일 입력은 초기화
|
||||||
};
|
};
|
||||||
|
|
||||||
// 원작 선택 UI 반영
|
|
||||||
if (this.character.originalWork) {
|
|
||||||
const d = this.character.originalWork;
|
|
||||||
this.selectedOriginal = d ? { id: Number(d.id), title: d.title, imageUrl: d.imageUrl } : null;
|
|
||||||
this.selectedOriginalId = d ? Number(d.id) : null;
|
|
||||||
this.character.originalWorkId = d ? Number(d.id) : null;
|
|
||||||
this.originalCharacter.originalWorkId = d ? Number(d.id) : null;
|
|
||||||
} else {
|
|
||||||
this.selectedOriginal = null;
|
|
||||||
this.selectedOriginalId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 태그, 메모리, 인물관계, 취미, 가치관, 목표, 성격 특성, 세계관 설정
|
// 태그, 메모리, 인물관계, 취미, 가치관, 목표, 성격 특성, 세계관 설정
|
||||||
this.tags = data.tags || [];
|
this.tags = data.tags || [];
|
||||||
this.memories = data.memories || [];
|
this.memories = data.memories || [];
|
||||||
@@ -1750,11 +1628,6 @@ export default {
|
|||||||
this.character.personalities = [...this.personalities];
|
this.character.personalities = [...this.personalities];
|
||||||
this.character.backgrounds = [...this.backgrounds];
|
this.character.backgrounds = [...this.backgrounds];
|
||||||
|
|
||||||
// 선택된 원작 기준으로 originalWorkId 최종 반영
|
|
||||||
if (this.selectedOriginalId !== null) {
|
|
||||||
this.character.originalWorkId = this.selectedOriginalId;
|
|
||||||
}
|
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
if (this.isEdit) {
|
if (this.isEdit) {
|
||||||
@@ -1877,7 +1750,8 @@ export default {
|
|||||||
gender: str(data.gender),
|
gender: str(data.gender),
|
||||||
mbti: str(data.mbti),
|
mbti: str(data.mbti),
|
||||||
characterType: str(data.characterType),
|
characterType: str(data.characterType),
|
||||||
originalWorkId: data.originalWorkId == null ? null : Number(data.originalWorkId),
|
originalTitle: str(data.originalTitle),
|
||||||
|
originalLink: str(data.originalLink),
|
||||||
speechPattern: str(data.speechPattern),
|
speechPattern: str(data.speechPattern),
|
||||||
speechStyle: str(data.speechStyle),
|
speechStyle: str(data.speechStyle),
|
||||||
appearance: str(data.appearance)
|
appearance: str(data.appearance)
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<v-container>
|
<v-container>
|
||||||
<v-row align="center">
|
<v-row>
|
||||||
<v-col cols="4">
|
<v-col cols="4">
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -19,29 +19,6 @@
|
|||||||
캐릭터 추가
|
캐릭터 추가
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
|
||||||
cols="8"
|
|
||||||
class="d-flex justify-end align-center"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="searchTerm"
|
|
||||||
label="검색어"
|
|
||||||
placeholder="캐릭터명, 태그, mbti 검색"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
hide-details
|
|
||||||
style="max-width: 320px;"
|
|
||||||
class="mr-2"
|
|
||||||
@keyup.enter="onSearch"
|
|
||||||
/>
|
|
||||||
<v-btn
|
|
||||||
:style="{ backgroundColor: '#3bb9f1', color: 'white' }"
|
|
||||||
:disabled="is_loading"
|
|
||||||
@click="onSearch"
|
|
||||||
>
|
|
||||||
검색
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col>
|
<v-col>
|
||||||
@@ -267,7 +244,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getCharacterList, updateCharacter, searchCharacterList } from '@/api/character'
|
import { getCharacterList, updateCharacter } from '@/api/character'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CharacterList",
|
name: "CharacterList",
|
||||||
@@ -283,8 +260,7 @@ export default {
|
|||||||
page: 1,
|
page: 1,
|
||||||
total_page: 0,
|
total_page: 0,
|
||||||
characters: [],
|
characters: [],
|
||||||
selected_character: {},
|
selected_character: {}
|
||||||
searchTerm: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -393,18 +369,10 @@ export default {
|
|||||||
await this.getCharacters()
|
await this.getCharacters()
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearch() {
|
|
||||||
this.page = 1;
|
|
||||||
this.getCharacters();
|
|
||||||
},
|
|
||||||
|
|
||||||
async getCharacters() {
|
async getCharacters() {
|
||||||
this.is_loading = true
|
this.is_loading = true
|
||||||
try {
|
try {
|
||||||
const hasSearch = this.searchTerm && this.searchTerm.trim() !== '';
|
const response = await getCharacterList(this.page);
|
||||||
const response = hasSearch
|
|
||||||
? await searchCharacterList(this.searchTerm.trim(), this.page, 20)
|
|
||||||
: await getCharacterList(this.page);
|
|
||||||
|
|
||||||
if (response && response.status === 200) {
|
if (response && response.status === 200) {
|
||||||
if (response.data.success === true) {
|
if (response.data.success === true) {
|
||||||
|
@@ -1,356 +0,0 @@
|
|||||||
<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>원천 원작: {{ detail.originalWork || '-' }}</div>
|
|
||||||
<div class="mt-1">
|
|
||||||
원천 원작 링크:
|
|
||||||
<a
|
|
||||||
v-if="detail.originalLink"
|
|
||||||
:href="detail.originalLink"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>{{ detail.originalLink }}</a>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</div>
|
|
||||||
<div>글/그림: {{ detail.writer || '-' }}</div>
|
|
||||||
<div>제작사: {{ detail.studio || '-' }}</div>
|
|
||||||
<div class="mt-1">
|
|
||||||
원작 링크:
|
|
||||||
<template v-if="detail.originalLinks && detail.originalLinks.length">
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-for="(link, idx) in detail.originalLinks"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>{{ link }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1">
|
|
||||||
태그:
|
|
||||||
<template v-if="detail.tags && detail.tags.length">
|
|
||||||
<v-chip
|
|
||||||
v-for="(t, i) in detail.tags"
|
|
||||||
:key="i"
|
|
||||||
small
|
|
||||||
class="mr-1 mb-1"
|
|
||||||
>
|
|
||||||
{{ t }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
<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>
|
|
@@ -1,505 +0,0 @@
|
|||||||
<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
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-file-input
|
|
||||||
v-model="form.image"
|
|
||||||
label="이미지"
|
|
||||||
accept="image/*"
|
|
||||||
prepend-icon="mdi-camera"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:class="{ 'required-asterisk': !isEdit }"
|
|
||||||
:rules="imageRules"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
v-if="previewImage || form.imageUrl"
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<div class="text-center">
|
|
||||||
<v-avatar size="150">
|
|
||||||
<v-img
|
|
||||||
:src="previewImage || form.imageUrl"
|
|
||||||
contain
|
|
||||||
/>
|
|
||||||
</v-avatar>
|
|
||||||
</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.title"
|
|
||||||
label="제목"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:rules="[v=>!!v||'제목은 필수입니다']"
|
|
||||||
class="required-asterisk"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.contentType"
|
|
||||||
label="콘텐츠 타입"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:rules="contentTypeRules"
|
|
||||||
:class="{ 'required-asterisk': !isEdit }"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.category"
|
|
||||||
label="카테고리(장르)"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:rules="categoryRules"
|
|
||||||
:class="{ 'required-asterisk': !isEdit }"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 추가 메타 정보 (요구 순서: 글/그림, 제작사, 원천원작, 원천 원작 링크) -->
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.writer"
|
|
||||||
label="글/그림"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.studio"
|
|
||||||
label="제작사"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.originalWork"
|
|
||||||
label="원천 원작"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="6"
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
v-model="form.originalLink"
|
|
||||||
label="원천 원작 링크"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
:rules="originalLinkRules"
|
|
||||||
:class="{ 'required-asterisk': !isEdit }"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
md="12"
|
|
||||||
>
|
|
||||||
<v-switch
|
|
||||||
v-model="form.isAdult"
|
|
||||||
label="19금 여부"
|
|
||||||
inset
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 원작 링크(여러 개) 추가 -->
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-divider class="my-4" />
|
|
||||||
<h3 class="mb-2">
|
|
||||||
원작 링크
|
|
||||||
</h3>
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="11">
|
|
||||||
<v-text-field
|
|
||||||
v-model="newOriginalLink"
|
|
||||||
label="원작 링크 추가"
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
@keyup.enter="addOriginalLink"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="1">
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
class="mt-1"
|
|
||||||
block
|
|
||||||
:disabled="!newOriginalLink || !newOriginalLink.trim()"
|
|
||||||
@click="addOriginalLink"
|
|
||||||
>
|
|
||||||
추가
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
<v-card
|
|
||||||
outlined
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
<v-list v-if="form.originalLinks && form.originalLinks.length > 0">
|
|
||||||
<v-list-item
|
|
||||||
v-for="(link, idx) in form.originalLinks"
|
|
||||||
:key="idx"
|
|
||||||
>
|
|
||||||
<v-list-item-content>
|
|
||||||
<v-list-item-title class="text-truncate">
|
|
||||||
{{ link }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action>
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
color="error"
|
|
||||||
@click="removeOriginalLink(idx)"
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
<v-card-text
|
|
||||||
v-else
|
|
||||||
class="grey--text"
|
|
||||||
>
|
|
||||||
추가된 원작 링크가 없습니다.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- 태그 -->
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-divider class="my-4" />
|
|
||||||
<h3 class="mb-2">
|
|
||||||
태그
|
|
||||||
</h3>
|
|
||||||
<v-combobox
|
|
||||||
v-model="form.tags"
|
|
||||||
multiple
|
|
||||||
chips
|
|
||||||
small-chips
|
|
||||||
deletable-chips
|
|
||||||
outlined
|
|
||||||
dense
|
|
||||||
label="태그를 입력 후 엔터로 추가"
|
|
||||||
@keydown.space.prevent="onTagSpace"
|
|
||||||
>
|
|
||||||
<template v-slot:selection="{ attrs, item, select, selected }">
|
|
||||||
<v-chip
|
|
||||||
v-bind="attrs"
|
|
||||||
:input-value="selected"
|
|
||||||
close
|
|
||||||
@click="select"
|
|
||||||
@click:close="removeTag(item)"
|
|
||||||
>
|
|
||||||
{{ item }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-combobox>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-textarea
|
|
||||||
v-model="form.description"
|
|
||||||
label="작품 소개"
|
|
||||||
outlined
|
|
||||||
rows="4"
|
|
||||||
:rules="descriptionRules"
|
|
||||||
:class="{ 'required-asterisk': !isEdit }"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
|
||||||
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
:disabled="!canSubmit"
|
|
||||||
@click="onSubmit"
|
|
||||||
>
|
|
||||||
{{ isEdit ? '수정' : '등록' }}
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-form>
|
|
||||||
</v-card>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { createOriginal, updateOriginal, getOriginal } from '@/api/original'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'OriginalForm',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isEdit: false,
|
|
||||||
isFormValid: false,
|
|
||||||
previewImage: null,
|
|
||||||
newOriginalLink: '',
|
|
||||||
form: {
|
|
||||||
id: null,
|
|
||||||
image: null,
|
|
||||||
imageUrl: null,
|
|
||||||
title: '',
|
|
||||||
contentType: '',
|
|
||||||
category: '',
|
|
||||||
isAdult: false,
|
|
||||||
description: '',
|
|
||||||
originalLink: '', // 원천 원작 링크(파라미터명 유지)
|
|
||||||
originalWork: '',
|
|
||||||
writer: '',
|
|
||||||
studio: '',
|
|
||||||
originalLinks: [], // 추가 원작 링크들
|
|
||||||
tags: []
|
|
||||||
},
|
|
||||||
originalInitial: null,
|
|
||||||
imageRules: [v => (this.isEdit ? true : (!!v || '이미지를 선택하세요'))],
|
|
||||||
contentTypeRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '콘텐츠 타입은 필수입니다'))],
|
|
||||||
categoryRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '카테고리는 필수입니다'))],
|
|
||||||
originalLinkRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '원천 원작 링크는 필수입니다'))],
|
|
||||||
descriptionRules: [v => (this.isEdit ? true : (!!(v && v.toString().trim()) || '작품 소개는 필수입니다'))]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
imageChanged() {
|
|
||||||
return !!this.form.image;
|
|
||||||
},
|
|
||||||
hasNonImageChanges() {
|
|
||||||
if (!this.isEdit || !this.originalInitial) return false;
|
|
||||||
const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
|
|
||||||
const basicChanged = fields.some(f => this.form[f] !== this.originalInitial[f]);
|
|
||||||
const arraysChanged = !this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)
|
|
||||||
|| !this.arraysEqual(this.form.tags, this.originalInitial.tags);
|
|
||||||
return basicChanged || arraysChanged;
|
|
||||||
},
|
|
||||||
hasEditChanges() {
|
|
||||||
return this.imageChanged || this.hasNonImageChanges;
|
|
||||||
},
|
|
||||||
canSubmit() {
|
|
||||||
if (this.isEdit) return this.hasEditChanges && !!(this.form.title && this.form.title.toString().trim());
|
|
||||||
const required = [this.form.image, this.form.title, this.form.contentType, this.form.category, this.form.originalLink, this.form.description];
|
|
||||||
return required.every(v => !!(v && (v.toString ? v.toString().trim() : v)));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'form.image': {
|
|
||||||
handler(newImage) {
|
|
||||||
if (newImage) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => { this.previewImage = e.target.result }
|
|
||||||
reader.readAsDataURL(newImage)
|
|
||||||
} else {
|
|
||||||
this.previewImage = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
if (this.$route.query.id) {
|
|
||||||
this.isEdit = true;
|
|
||||||
this.load(this.$route.query.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
notifyError(message) { this.$dialog.notify.error(message) },
|
|
||||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
|
||||||
goBack() { this.$router.push('/original-work') },
|
|
||||||
arraysEqual(a, b) {
|
|
||||||
const arrA = Array.isArray(a) ? a : [];
|
|
||||||
const arrB = Array.isArray(b) ? b : [];
|
|
||||||
if (arrA.length !== arrB.length) return false;
|
|
||||||
for (let i = 0; i < arrA.length; i++) {
|
|
||||||
if (arrA[i] !== arrB[i]) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
addOriginalLink() {
|
|
||||||
if (!this.newOriginalLink || !this.newOriginalLink.trim()) return;
|
|
||||||
const val = this.newOriginalLink.trim();
|
|
||||||
if (!this.form.originalLinks) this.form.originalLinks = [];
|
|
||||||
if (!this.form.originalLinks.includes(val)) {
|
|
||||||
this.form.originalLinks.push(val);
|
|
||||||
}
|
|
||||||
this.newOriginalLink = '';
|
|
||||||
},
|
|
||||||
removeOriginalLink(index) {
|
|
||||||
if (!this.form.originalLinks) return;
|
|
||||||
this.form.originalLinks.splice(index, 1);
|
|
||||||
},
|
|
||||||
onTagSpace() {
|
|
||||||
// CharacterForm의 태그 방식과 유사: 마지막 항목을 공백 기준으로 확정
|
|
||||||
if (!Array.isArray(this.form.tags)) this.form.tags = [];
|
|
||||||
const last = this.form.tags[this.form.tags.length - 1];
|
|
||||||
if (typeof last === 'string' && last.trim()) {
|
|
||||||
let processed = last.trim().replace(/\s+/g, '');
|
|
||||||
if (processed.length > 50) processed = processed.substring(0, 50);
|
|
||||||
this.form.tags.splice(this.form.tags.length - 1, 1, processed);
|
|
||||||
this.$nextTick(() => this.form.tags.push(''));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
removeTag(item) {
|
|
||||||
if (!Array.isArray(this.form.tags)) return;
|
|
||||||
const idx = this.form.tags.indexOf(item);
|
|
||||||
if (idx >= 0) this.form.tags.splice(idx, 1);
|
|
||||||
},
|
|
||||||
async load(id) {
|
|
||||||
try {
|
|
||||||
const res = await getOriginal(id);
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
const d = res.data.data;
|
|
||||||
this.form = {
|
|
||||||
id: d.id,
|
|
||||||
image: null,
|
|
||||||
imageUrl: d.imageUrl,
|
|
||||||
title: d.title || '',
|
|
||||||
contentType: d.contentType || '',
|
|
||||||
category: d.category || '',
|
|
||||||
isAdult: !!d.isAdult,
|
|
||||||
description: d.description || '',
|
|
||||||
originalLink: d.originalLink || '',
|
|
||||||
originalWork: d.originalWork || '',
|
|
||||||
writer: d.writer || '',
|
|
||||||
studio: d.studio || '',
|
|
||||||
originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
|
|
||||||
tags: Array.isArray(d.tags) ? d.tags.slice() : []
|
|
||||||
}
|
|
||||||
this.originalInitial = {
|
|
||||||
id: d.id,
|
|
||||||
imageUrl: d.imageUrl,
|
|
||||||
title: d.title || '',
|
|
||||||
contentType: d.contentType || '',
|
|
||||||
category: d.category || '',
|
|
||||||
isAdult: !!d.isAdult,
|
|
||||||
description: d.description || '',
|
|
||||||
originalLink: d.originalLink || '',
|
|
||||||
originalWork: d.originalWork || '',
|
|
||||||
writer: d.writer || '',
|
|
||||||
studio: d.studio || '',
|
|
||||||
originalLinks: Array.isArray(d.originalLinks) ? d.originalLinks.slice() : [],
|
|
||||||
tags: Array.isArray(d.tags) ? d.tags.slice() : []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.notifyError('상세 조회 실패');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError('상세 조회 실패');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async onSubmit() {
|
|
||||||
try {
|
|
||||||
const isValid = this.$refs.form ? this.$refs.form.validate() : true;
|
|
||||||
if (!isValid) {
|
|
||||||
this.notifyError(this.isEdit ? '입력을 확인해주세요.' : '필수 항목을 모두 입력해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isEdit) {
|
|
||||||
const fields = ['title', 'contentType', 'category', 'isAdult', 'description', 'originalLink', 'originalWork', 'writer', 'studio'];
|
|
||||||
const patch = { id: this.form.id };
|
|
||||||
if (this.originalInitial) {
|
|
||||||
fields.forEach(f => {
|
|
||||||
if (this.form[f] !== this.originalInitial[f]) {
|
|
||||||
patch[f] = this.form[f];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!this.arraysEqual(this.form.originalLinks, this.originalInitial.originalLinks)) {
|
|
||||||
patch.originalLinks = this.form.originalLinks;
|
|
||||||
}
|
|
||||||
if (!this.arraysEqual(this.form.tags, this.originalInitial.tags)) {
|
|
||||||
patch.tags = this.form.tags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const image = this.form.image || null;
|
|
||||||
if (Object.keys(patch).length === 1 && !image) {
|
|
||||||
this.notifyError('변경된 내용이 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await updateOriginal(patch, image);
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
this.notifySuccess('수정되었습니다.');
|
|
||||||
this.$router.push('/original-work');
|
|
||||||
} else {
|
|
||||||
this.notifyError('수정 실패');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await createOriginal(this.form);
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
this.notifySuccess('등록되었습니다.');
|
|
||||||
this.$router.push('/original-work');
|
|
||||||
} else {
|
|
||||||
this.notifyError('등록 실패');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError(this.isEdit ? '수정 실패' : '등록 실패');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.required-asterisk >>> .v-label::after { content: ' *'; color: #ff5252; }
|
|
||||||
</style>
|
|
@@ -1,205 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-toolbar dark>
|
|
||||||
<v-spacer />
|
|
||||||
<v-toolbar-title>원작 리스트</v-toolbar-title>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
dark
|
|
||||||
@click="goToCreate"
|
|
||||||
>
|
|
||||||
원작 등록
|
|
||||||
</v-btn>
|
|
||||||
</v-toolbar>
|
|
||||||
|
|
||||||
<v-container>
|
|
||||||
<v-row v-if="isLoading && originals.length === 0">
|
|
||||||
<v-col class="text-center">
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="primary"
|
|
||||||
size="64"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col
|
|
||||||
v-for="item in originals"
|
|
||||||
:key="item.id"
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
lg="3"
|
|
||||||
>
|
|
||||||
<v-card
|
|
||||||
class="mx-auto"
|
|
||||||
max-width="344"
|
|
||||||
style="cursor:pointer;"
|
|
||||||
@click="openDetail(item)"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
:src="item.imageUrl"
|
|
||||||
height="200"
|
|
||||||
contain
|
|
||||||
/>
|
|
||||||
<v-card-title class="text-no-wrap">
|
|
||||||
{{ item.title }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
color="primary"
|
|
||||||
@click.stop="editOriginal(item)"
|
|
||||||
>
|
|
||||||
수정
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
color="error"
|
|
||||||
@click.stop="confirmDelete(item)"
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer />
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row v-if="!isLoading && originals.length === 0">
|
|
||||||
<v-col class="text-center">
|
|
||||||
데이터가 없습니다.
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-row v-if="isLoading && originals.length > 0">
|
|
||||||
<v-col class="text-center">
|
|
||||||
<v-progress-circular
|
|
||||||
indeterminate
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<v-dialog
|
|
||||||
v-model="deleteDialog"
|
|
||||||
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
|
|
||||||
@click="deleteDialog = false"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
color="error"
|
|
||||||
text
|
|
||||||
@click="deleteItem"
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { getOriginalList, deleteOriginal } from '@/api/original'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'OriginalList',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isLoading: false,
|
|
||||||
originals: [],
|
|
||||||
page: 1,
|
|
||||||
hasMore: true,
|
|
||||||
deleteDialog: false,
|
|
||||||
selected: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.loadMore();
|
|
||||||
window.addEventListener('scroll', this.handleScroll);
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
notifyError(message) { this.$dialog.notify.error(message) },
|
|
||||||
notifySuccess(message) { this.$dialog.notify.success(message) },
|
|
||||||
async loadMore() {
|
|
||||||
if (this.isLoading || !this.hasMore) return;
|
|
||||||
this.isLoading = true;
|
|
||||||
try {
|
|
||||||
const res = await getOriginalList(this.page);
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
const content = res.data.data?.content || [];
|
|
||||||
this.originals = this.originals.concat(content);
|
|
||||||
this.hasMore = content.length > 0;
|
|
||||||
this.page++;
|
|
||||||
} else {
|
|
||||||
this.notifyError('원작 목록 조회 실패');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError('원작 목록 조회 실패');
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleScroll() {
|
|
||||||
const scrollPosition = window.innerHeight + window.scrollY;
|
|
||||||
const documentHeight = document.documentElement.offsetHeight;
|
|
||||||
if (scrollPosition >= documentHeight - 200 && !this.isLoading && this.hasMore) {
|
|
||||||
this.loadMore();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
goToCreate() {
|
|
||||||
this.$router.push('/original-work/form');
|
|
||||||
},
|
|
||||||
editOriginal(item) {
|
|
||||||
this.$router.push({ path: '/original-work/form', query: { id: item.id } });
|
|
||||||
},
|
|
||||||
openDetail(item) {
|
|
||||||
this.$router.push({ path: '/original-work/detail', query: { id: item.id } });
|
|
||||||
},
|
|
||||||
confirmDelete(item) {
|
|
||||||
this.selected = item;
|
|
||||||
this.deleteDialog = true;
|
|
||||||
},
|
|
||||||
async deleteItem() {
|
|
||||||
if (!this.selected) return;
|
|
||||||
try {
|
|
||||||
const res = await deleteOriginal(this.selected.id);
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
this.notifySuccess('삭제되었습니다.');
|
|
||||||
this.originals = this.originals.filter(x => x.id !== this.selected.id);
|
|
||||||
} else {
|
|
||||||
this.notifyError('삭제 실패');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError('삭제 실패');
|
|
||||||
} finally {
|
|
||||||
this.deleteDialog = false;
|
|
||||||
this.selected = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.text-no-wrap { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
||||||
</style>
|
|
@@ -53,24 +53,6 @@
|
|||||||
<template v-slot:item.communitySettlementRatio="{ item }">
|
<template v-slot:item.communitySettlementRatio="{ item }">
|
||||||
{{ item.communitySettlementRatio }}%
|
{{ item.communitySettlementRatio }}%
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item.actions="{ item }">
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
color="primary"
|
|
||||||
text
|
|
||||||
@click="openEdit(item)"
|
|
||||||
>
|
|
||||||
수정
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
small
|
|
||||||
color="red"
|
|
||||||
text
|
|
||||||
@click="confirmDelete(item)"
|
|
||||||
>
|
|
||||||
삭제
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -91,20 +73,13 @@
|
|||||||
persistent
|
persistent
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>{{ is_edit ? '크리에이터 정산비율 수정' : '크리에이터 정산비율' }}</v-card-title>
|
<v-card-title>크리에이터 정산비율</v-card-title>
|
||||||
<v-card-text v-show="!is_edit">
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="creator_settlement_ratio.creator_id"
|
v-model="creator_settlement_ratio.creator_id"
|
||||||
label="크리에이터 번호"
|
label="크리에이터 번호"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text v-show="is_edit">
|
|
||||||
<v-text-field
|
|
||||||
v-model="creator_settlement_ratio.nickname"
|
|
||||||
disabled
|
|
||||||
label="크리에이터 닉네임"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="creator_settlement_ratio.subsidy"
|
v-model="creator_settlement_ratio.subsidy"
|
||||||
@@ -143,7 +118,7 @@
|
|||||||
text
|
text
|
||||||
@click="validate"
|
@click="validate"
|
||||||
>
|
>
|
||||||
{{ is_edit ? '수정하기' : '등록하기' }}
|
등록하기
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -167,8 +142,6 @@ export default {
|
|||||||
items: [],
|
items: [],
|
||||||
creator_settlement_ratio: {},
|
creator_settlement_ratio: {},
|
||||||
show_write_dialog: false,
|
show_write_dialog: false,
|
||||||
is_edit: false,
|
|
||||||
editing_item_id: null,
|
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
text: '닉네임',
|
text: '닉네임',
|
||||||
@@ -200,12 +173,6 @@ export default {
|
|||||||
sortable: false,
|
sortable: false,
|
||||||
value: 'communitySettlementRatio',
|
value: 'communitySettlementRatio',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: '관리',
|
|
||||||
align: 'center',
|
|
||||||
sortable: false,
|
|
||||||
value: 'actions',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -224,16 +191,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
showWriteDialog() {
|
showWriteDialog() {
|
||||||
this.is_edit = false
|
|
||||||
this.editing_item_id = null
|
|
||||||
this.creator_settlement_ratio = {}
|
|
||||||
this.show_write_dialog = true
|
this.show_write_dialog = true
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.creator_settlement_ratio = {}
|
this.creator_settlement_ratio = {}
|
||||||
this.is_edit = false
|
|
||||||
this.editing_item_id = null
|
|
||||||
this.show_write_dialog = false
|
this.show_write_dialog = false
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -263,11 +225,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.is_edit) {
|
this.createCreatorSettlementRatio();
|
||||||
this.updateCreatorSettlementRatio();
|
|
||||||
} else {
|
|
||||||
this.createCreatorSettlementRatio();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createCreatorSettlementRatio() {
|
async createCreatorSettlementRatio() {
|
||||||
@@ -295,71 +253,6 @@ export default {
|
|||||||
this.is_loading = false
|
this.is_loading = false
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateCreatorSettlementRatio() {
|
|
||||||
if (this.is_loading) return;
|
|
||||||
this.is_loading = true
|
|
||||||
try {
|
|
||||||
// 수정은 생성과 동일한 파라미터를 전송 (memberId 기준)
|
|
||||||
const payload = { ...this.creator_settlement_ratio }
|
|
||||||
const res = await api.updateCreatorSettlementRatio(payload)
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
this.cancel()
|
|
||||||
this.notifySuccess(res.data.message || '수정되었습니다.')
|
|
||||||
this.items = []
|
|
||||||
await this.getSettlementRatio()
|
|
||||||
} else {
|
|
||||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
||||||
} finally {
|
|
||||||
this.is_loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openEdit(item) {
|
|
||||||
this.is_edit = true
|
|
||||||
this.editing_item_id = null
|
|
||||||
this.creator_settlement_ratio = {
|
|
||||||
creator_id: item.memberId,
|
|
||||||
nickname: item.nickname,
|
|
||||||
subsidy: item.subsidy,
|
|
||||||
liveSettlementRatio: item.liveSettlementRatio,
|
|
||||||
contentSettlementRatio: item.contentSettlementRatio,
|
|
||||||
communitySettlementRatio: item.communitySettlementRatio,
|
|
||||||
}
|
|
||||||
this.show_write_dialog = true
|
|
||||||
},
|
|
||||||
|
|
||||||
async confirmDelete(item) {
|
|
||||||
try {
|
|
||||||
const ok = await this.$dialog.confirm({ text: '삭제하시겠습니까?', title: '확인', actions: { false: '취소', true: '삭제' } })
|
|
||||||
if (!ok) return
|
|
||||||
} catch (e) {
|
|
||||||
// 일부 구현체는 confirm이 boolean이 아닌 경우가 있음
|
|
||||||
}
|
|
||||||
this.deleteCreatorSettlementRatio(item)
|
|
||||||
},
|
|
||||||
|
|
||||||
async deleteCreatorSettlementRatio(item) {
|
|
||||||
if (this.is_loading) return;
|
|
||||||
this.is_loading = true
|
|
||||||
try {
|
|
||||||
const memberId = item.memberId
|
|
||||||
const res = await api.deleteCreatorSettlementRatio(memberId)
|
|
||||||
if (res.status === 200 && res.data.success === true) {
|
|
||||||
this.notifySuccess(res.data.message || '삭제되었습니다.')
|
|
||||||
this.items = this.items.filter(x => (x.memberId) !== memberId)
|
|
||||||
} else {
|
|
||||||
this.notifyError(res.data.message || '알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.')
|
|
||||||
} finally {
|
|
||||||
this.is_loading = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async getSettlementRatio() {
|
async getSettlementRatio() {
|
||||||
this.is_loading = true
|
this.is_loading = true
|
||||||
|
|
||||||
@@ -386,6 +279,10 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async next() {
|
async next() {
|
||||||
|
if (this.search_word.length < 2) {
|
||||||
|
this.search_word = ''
|
||||||
|
}
|
||||||
|
|
||||||
await this.getSettlementRatio()
|
await this.getSettlementRatio()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user