From 94a989ea57eb17d4cac62a281129078111a7a683 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 5 Aug 2025 15:59:58 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=BA=90=EB=A6=AD=ED=84=B0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=8F=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/character.js | 102 ++++- src/router/index.js | 5 + src/views/Chat/CharacterForm.vue | 620 +++++++++++++++++++++++++++++++ src/views/Chat/CharacterList.vue | 275 +++----------- 4 files changed, 766 insertions(+), 236 deletions(-) create mode 100644 src/views/Chat/CharacterForm.vue diff --git a/src/api/character.js b/src/api/character.js index 1fc22b9..0333ddc 100644 --- a/src/api/character.js +++ b/src/api/character.js @@ -1,16 +1,112 @@ import Vue from 'vue'; // 캐릭터 리스트 -async function getCharacterList() { - return Vue.axios.get('/api/admin/characters') +async function getCharacterList(page = 1, size = 10) { + return Vue.axios.get('/api/admin/characters', { + params: { page, size } + }) +} + +// 캐릭터 검색 +async function searchCharacters(keyword, page = 1, size = 10) { + return Vue.axios.get('/api/admin/characters/search', { + params: { keyword, page, size } + }) +} + +// 캐릭터 상세 조회 +async function getCharacter(id) { + return Vue.axios.get(`/api/admin/characters/${id}`) } // 캐릭터 등록 +async function createCharacter(characterData) { + const formData = new FormData() + + // 기본 필드 추가 + formData.append('name', characterData.name) + formData.append('description', characterData.description) + formData.append('isActive', characterData.isActive) + + // 추가 필드가 있는 경우 추가 + if (characterData.personality) formData.append('personality', characterData.personality) + if (characterData.gender) formData.append('gender', characterData.gender) + if (characterData.birthDate) formData.append('birthDate', characterData.birthDate) + if (characterData.mbti) formData.append('mbti', characterData.mbti) + if (characterData.ageRestricted !== undefined) formData.append('ageRestricted', characterData.ageRestricted) + if (characterData.worldView) formData.append('worldView', characterData.worldView) + if (characterData.relationships) formData.append('relationships', characterData.relationships) + if (characterData.speechPattern) formData.append('speechPattern', characterData.speechPattern) + if (characterData.systemPrompt) formData.append('systemPrompt', characterData.systemPrompt) + + // 태그와 메모리는 배열이므로 JSON 문자열로 변환 + if (characterData.tags && characterData.tags.length > 0) { + formData.append('tags', JSON.stringify(characterData.tags)) + } + + if (characterData.memories && characterData.memories.length > 0) { + formData.append('memories', JSON.stringify(characterData.memories)) + } + + // 이미지가 있는 경우 추가 + if (characterData.image) formData.append('image', characterData.image) + + return Vue.axios.post('/api/admin/characters', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} // 캐릭터 수정 +async function updateCharacter(characterData) { + const formData = new FormData() + + // 기본 필드 추가 + formData.append('name', characterData.name) + formData.append('description', characterData.description) + formData.append('isActive', characterData.isActive) + + // 추가 필드가 있는 경우 추가 + if (characterData.personality) formData.append('personality', characterData.personality) + if (characterData.gender) formData.append('gender', characterData.gender) + if (characterData.birthDate) formData.append('birthDate', characterData.birthDate) + if (characterData.mbti) formData.append('mbti', characterData.mbti) + if (characterData.ageRestricted !== undefined) formData.append('ageRestricted', characterData.ageRestricted) + if (characterData.worldView) formData.append('worldView', characterData.worldView) + if (characterData.relationships) formData.append('relationships', characterData.relationships) + if (characterData.speechPattern) formData.append('speechPattern', characterData.speechPattern) + if (characterData.systemPrompt) formData.append('systemPrompt', characterData.systemPrompt) + + // 태그와 메모리는 배열이므로 JSON 문자열로 변환 + if (characterData.tags && characterData.tags.length > 0) { + formData.append('tags', JSON.stringify(characterData.tags)) + } + + if (characterData.memories && characterData.memories.length > 0) { + formData.append('memories', JSON.stringify(characterData.memories)) + } + + // 이미지가 있는 경우 추가 + if (characterData.image) formData.append('image', characterData.image) + + return Vue.axios.put(`/api/admin/characters/${characterData.id}`, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} // 캐릭터 삭제 +async function deleteCharacter(id) { + return Vue.axios.delete(`/api/admin/characters/${id}`) +} export { - getCharacterList + getCharacterList, + searchCharacters, + getCharacter, + createCharacter, + updateCharacter, + deleteCharacter } diff --git a/src/router/index.js b/src/router/index.js index 1f1270c..05ac4f3 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -260,6 +260,11 @@ const routes = [ name: 'CharacterList', component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterList.vue') }, + { + path: '/character/form', + name: 'CharacterForm', + component: () => import(/* webpackChunkName: "character" */ '../views/Chat/CharacterForm.vue') + }, // TODO: CharacterBanner 컴포넌트가 구현되면 아래 라우트 주석 해제 // { // path: '/character/banner', diff --git a/src/views/Chat/CharacterForm.vue b/src/views/Chat/CharacterForm.vue new file mode 100644 index 0000000..b383725 --- /dev/null +++ b/src/views/Chat/CharacterForm.vue @@ -0,0 +1,620 @@ + + + + + diff --git a/src/views/Chat/CharacterList.vue b/src/views/Chat/CharacterList.vue index ec5b4ad..e8f3c9b 100644 --- a/src/views/Chat/CharacterList.vue +++ b/src/views/Chat/CharacterList.vue @@ -139,77 +139,6 @@ - - - - - {{ is_edit ? '캐릭터 수정' : '캐릭터 추가' }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 취소 - - - 저장 - - - - import VueShowMoreText from 'vue-show-more-text' +import { getCharacterList, searchCharacters, deleteCharacter as apiDeleteCharacter } from '@/api/character' export default { name: "CharacterList", @@ -255,21 +185,10 @@ export default { data() { return { is_loading: false, - show_dialog: false, show_delete_confirm_dialog: false, - is_edit: false, page: 1, total_page: 0, search_word: '', - character: { - id: null, - name: '', - description: '', - image: null, - imageUrl: '', - isActive: true, - createdAt: '' - }, characters: [], selected_character: {} } @@ -289,47 +208,18 @@ export default { }, showAddDialog() { - this.is_edit = false - this.character = { - id: null, - name: '', - description: '', - image: null, - imageUrl: '', - isActive: true, - createdAt: '' - } - this.show_dialog = true + // 페이지로 이동 + this.$router.push('/character/form'); }, showEditDialog(item) { - this.is_edit = true - this.selected_character = item - this.character = { - id: item.id, - name: item.name, - description: item.description, - image: null, - imageUrl: item.imageUrl, - isActive: item.isActive, - createdAt: item.createdAt - } - this.show_dialog = true + // 페이지로 이동하면서 id 전달 + this.$router.push({ + path: '/character/form', + query: { id: item.id } + }); }, - closeDialog() { - this.show_dialog = false - this.character = { - id: null, - name: '', - description: '', - image: null, - imageUrl: '', - isActive: true, - createdAt: '' - } - this.selected_character = {} - }, deleteConfirm(item) { this.selected_character = item @@ -341,68 +231,21 @@ export default { this.selected_character = {} }, - async saveCharacter() { - if ( - this.character.name === null || - this.character.name === undefined || - this.character.name.trim().length <= 0 - ) { - this.notifyError("이름을 입력하세요") - return - } - - if ( - this.character.description === null || - this.character.description === undefined || - this.character.description.trim().length <= 0 - ) { - this.notifyError("설명을 입력하세요") - return - } - - if (this.is_loading) return; - - this.is_loading = true - - try { - // 여기에 API 호출 코드를 추가합니다. - // 예시: - // const res = this.is_edit - // ? await api.updateCharacter(this.character) - // : await api.createCharacter(this.character); - - // API 호출이 없으므로 임시로 성공 처리 - setTimeout(() => { - this.closeDialog() - this.notifySuccess(this.is_edit ? '수정되었습니다.' : '추가되었습니다.') - this.getCharacters() - this.is_loading = false - }, 1000) - } catch (e) { - this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') - this.is_loading = false - } - }, async deleteCharacter() { if (this.is_loading) return; this.is_loading = true try { - // 여기에 API 호출 코드를 추가합니다. - // 예시: - // const res = await api.deleteCharacter(this.selected_character.id); - - // API 호출이 없으므로 임시로 성공 처리 - setTimeout(() => { - this.closeDeleteDialog() - this.notifySuccess('삭제되었습니다.') - this.getCharacters() - this.is_loading = false - }, 1000) + await apiDeleteCharacter(this.selected_character.id); + this.closeDeleteDialog(); + this.notifySuccess('삭제되었습니다.'); + await this.getCharacters(); } catch (e) { - this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') - this.is_loading = false + console.error('캐릭터 삭제 오류:', e); + this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); + } finally { + this.is_loading = false; } }, @@ -418,38 +261,22 @@ export default { async getCharacters() { this.is_loading = true try { - // 여기에 API 호출 코드를 추가합니다. - // 예시: - // const res = await api.getCharacters(this.page); + const response = await getCharacterList(this.page); - // API 호출이 없으므로 임시 데이터 생성 - setTimeout(() => { - // 임시 데이터 - const mockData = { - totalCount: 15, - items: Array.from({ length: 10 }, (_, i) => ({ - id: i + 1 + (this.page - 1) * 10, - name: `캐릭터 ${i + 1 + (this.page - 1) * 10}`, - description: `이것은 캐릭터 ${i + 1 + (this.page - 1) * 10}에 대한 설명입니다. 이 캐릭터는 다양한 특성을 가지고 있습니다.`, - imageUrl: 'https://via.placeholder.com/150', - isActive: Math.random() > 0.3, - createdAt: new Date().toISOString().split('T')[0] - })) - } + if (response && response.data) { + const data = response.data; + this.characters = data.items || []; - const total_page = Math.ceil(mockData.totalCount / 10) - this.characters = mockData.items - - if (total_page <= 0) - this.total_page = 1 - else - this.total_page = total_page - - this.is_loading = false - }, 500) + const total_page = Math.ceil((data.totalCount || 0) / 10); + this.total_page = total_page <= 0 ? 1 : total_page; + } else { + throw new Error('응답 데이터가 없습니다.'); + } } catch (e) { - this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') - this.is_loading = false + console.error('캐릭터 목록 조회 오류:', e); + this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); + } finally { + this.is_loading = false; } }, @@ -466,40 +293,22 @@ export default { } else { this.is_loading = true try { - // 여기에 API 호출 코드를 추가합니다. - // 예시: - // const res = await api.searchCharacters(this.search_word, this.page); + const response = await searchCharacters(this.search_word, this.page); - // API 호출이 없으므로 임시 데이터 생성 - setTimeout(() => { - // 검색 결과 임시 데이터 - const filteredItems = Array.from({ length: 3 }, (_, i) => ({ - id: i + 1, - name: `${this.search_word} 캐릭터 ${i + 1}`, - description: `이것은 ${this.search_word} 캐릭터 ${i + 1}에 대한 설명입니다.`, - imageUrl: 'https://via.placeholder.com/150', - isActive: true, - createdAt: new Date().toISOString().split('T')[0] - })) + if (response && response.data) { + const data = response.data; + this.characters = data.items || []; - const mockData = { - totalCount: 3, - items: filteredItems - } - - const total_page = Math.ceil(mockData.totalCount / 10) - this.characters = mockData.items - - if (total_page <= 0) - this.total_page = 1 - else - this.total_page = total_page - - this.is_loading = false - }, 500) + const total_page = Math.ceil((data.totalCount || 0) / 10); + this.total_page = total_page <= 0 ? 1 : total_page; + } else { + throw new Error('응답 데이터가 없습니다.'); + } } catch (e) { - this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.') - this.is_loading = false + console.error('캐릭터 검색 오류:', e); + this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); + } finally { + this.is_loading = false; } } }