diff --git a/src/api/character.js b/src/api/character.js index 915ea0c..9986da2 100644 --- a/src/api/character.js +++ b/src/api/character.js @@ -43,8 +43,7 @@ async function createCharacter(characterData) { gender: toNullIfBlank(characterData.gender), mbti: toNullIfBlank(characterData.mbti), characterType: toNullIfBlank(characterData.type), - originalTitle: toNullIfBlank(characterData.originalTitle), - originalLink: toNullIfBlank(characterData.originalLink), + originalWorkId: characterData.originalWorkId || null, speechPattern: toNullIfBlank(characterData.speechPattern), speechStyle: toNullIfBlank(characterData.speechStyle), appearance: toNullIfBlank(characterData.appearance), diff --git a/src/api/original.js b/src/api/original.js new file mode 100644 index 0000000..7844b3f --- /dev/null +++ b/src/api/original.js @@ -0,0 +1,87 @@ +import Vue from 'vue'; + +// 공통: 빈 문자열 -> null +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) + }; + 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 => { + const value = data[key]; + if (typeof value === 'string' || value === '') { + processed[key] = toNullIfBlank(value) + } else { + processed[key] = value + } + }) + 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 }) +} diff --git a/src/components/SideMenu.vue b/src/components/SideMenu.vue index 6c66ce0..722e4e7 100644 --- a/src/components/SideMenu.vue +++ b/src/components/SideMenu.vue @@ -122,6 +122,11 @@ export default { route: '/character/calculate', items: null }, + { + title: '원작', + route: '/original-work', + items: null + }, ] }) } else { diff --git a/src/router/index.js b/src/router/index.js index acf9fd3..7a01988 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -291,9 +291,24 @@ const routes = [ component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCurationDetail.vue') }, { - path: '/character/calculate', - name: 'CharacterCalculate', - component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterCalculateList.vue') + path: '/character/calculate', + name: 'CharacterCalculate', + 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') }, ] }, diff --git a/src/views/Chat/CharacterBanner.vue b/src/views/Chat/CharacterBanner.vue index 5b6f93c..f1a0e84 100644 --- a/src/views/Chat/CharacterBanner.vue +++ b/src/views/Chat/CharacterBanner.vue @@ -60,7 +60,7 @@ max-width="300" > diff --git a/src/views/Chat/CharacterForm.vue b/src/views/Chat/CharacterForm.vue index bafc043..731c1a8 100644 --- a/src/views/Chat/CharacterForm.vue +++ b/src/views/Chat/CharacterForm.vue @@ -206,29 +206,64 @@ - + - - + + @change="onOriginalChange" + > + + + + + + + + + + + + - - + + + + + + + + + + {{ selectedOriginal.title }} + + + + + 해제 + + @@ -1018,6 +1053,7 @@ + + diff --git a/src/views/Chat/OriginalForm.vue b/src/views/Chat/OriginalForm.vue new file mode 100644 index 0000000..f4e40de --- /dev/null +++ b/src/views/Chat/OriginalForm.vue @@ -0,0 +1,305 @@ + + + + + mdi-arrow-left + + + {{ isEdit ? '원작 수정' : '원작 등록' }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ isEdit ? '수정' : '등록' }} + + + + + + + + + + + diff --git a/src/views/Chat/OriginalList.vue b/src/views/Chat/OriginalList.vue new file mode 100644 index 0000000..56c2e3c --- /dev/null +++ b/src/views/Chat/OriginalList.vue @@ -0,0 +1,205 @@ + + + + + 원작 리스트 + + + 원작 등록 + + + + + + + + + + + + + + + + {{ item.title }} + + + + + 수정 + + + 삭제 + + + + + + + + + + 데이터가 없습니다. + + + + + + + + + + + + + + 삭제 확인 + + 정말 삭제하시겠습니까? + + + + 취소 + + + 삭제 + + + + + + + + + +