From ba248f7680b74c804383a272d296550644e1cce4 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 12 Aug 2025 21:09:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8,=20=EC=B6=94=EA=B0=80/=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8F=BC,=20=EB=B0=B0=EB=84=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - response의 데이터 구조에 맞춰서 코드 수정 --- src/api/character.js | 45 ++++++++++++++------- src/views/Chat/CharacterBanner.vue | 50 ++++++++++++++++------- src/views/Chat/CharacterForm.vue | 65 +++++++++++++++++++----------- src/views/Chat/CharacterList.vue | 20 +++++---- 4 files changed, 119 insertions(+), 61 deletions(-) diff --git a/src/api/character.js b/src/api/character.js index b090cd9..e4f9eb1 100644 --- a/src/api/character.js +++ b/src/api/character.js @@ -19,6 +19,14 @@ async function getCharacter(id) { return Vue.axios.get(`/admin/chat/character/${id}`) } +// 내부 헬퍼: 빈 문자열을 null로 변환 +function toNullIfBlank(value) { + if (typeof value === 'string') { + return value.trim() === '' ? null : value; + } + return value === '' ? null : value; +} + // 캐릭터 등록 async function createCharacter(characterData) { const formData = new FormData() @@ -28,18 +36,18 @@ async function createCharacter(characterData) { // 나머지 데이터는 JSON 문자열로 변환하여 request 필드에 추가 const requestData = { - name: characterData.name, - systemPrompt: characterData.systemPrompt, - description: characterData.description, - age: characterData.age, - gender: characterData.gender, - mbti: characterData.mbti, - characterType: characterData.type, - originalTitle: characterData.originalTitle, - originalLink: characterData.originalLink, - speechPattern: characterData.speechPattern, - speechStyle: characterData.conversationStyle, - appearance: characterData.appearance, + name: toNullIfBlank(characterData.name), + systemPrompt: toNullIfBlank(characterData.systemPrompt), + description: toNullIfBlank(characterData.description), + age: toNullIfBlank(characterData.age), + gender: toNullIfBlank(characterData.gender), + mbti: toNullIfBlank(characterData.mbti), + characterType: toNullIfBlank(characterData.type), + originalTitle: toNullIfBlank(characterData.originalTitle), + originalLink: toNullIfBlank(characterData.originalLink), + speechPattern: toNullIfBlank(characterData.speechPattern), + speechStyle: toNullIfBlank(characterData.speechStyle), + appearance: toNullIfBlank(characterData.appearance), tags: characterData.tags || [], hobbies: characterData.hobbies || [], values: characterData.values || [], @@ -66,9 +74,18 @@ async function updateCharacter(characterData, image = null) { // 이미지가 있는 경우에만 FormData에 추가 if (image) formData.append('image', image) - // 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가 + // 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가 ('' -> null 변환) // characterData는 이미 변경된 필드만 포함하고 있음 - formData.append('request', JSON.stringify(characterData)) + const processed = {} + Object.keys(characterData).forEach(key => { + const value = characterData[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/character/update`, formData, { headers: { diff --git a/src/views/Chat/CharacterBanner.vue b/src/views/Chat/CharacterBanner.vue index 5ea4bb6..5858852 100644 --- a/src/views/Chat/CharacterBanner.vue +++ b/src/views/Chat/CharacterBanner.vue @@ -197,7 +197,9 @@ -
선택된 캐릭터: {{ selectedCharacter.name }}
+
+ 선택된 캐릭터: {{ selectedCharacter.name }} +
@@ -356,8 +358,9 @@ export default { try { const response = await getCharacterBannerList(this.page); - if (response && response.data) { - const newBanners = response.data.content || []; + if (response && response.status === 200 && response.data && response.data.success === true) { + const data = response.data.data; + const newBanners = data.content || []; this.banners = [...this.banners, ...newBanners]; // 더 불러올 데이터가 있는지 확인 @@ -458,8 +461,9 @@ export default { try { const response = await searchCharacters(this.searchKeyword); - if (response && response.data) { - this.searchResults = response.data.content || []; + if (response && response.status === 200 && response.data && response.data.success === true) { + const data = response.data.data; + this.searchResults = data.content || []; this.searchPerformed = true; } } catch (error) { @@ -482,19 +486,27 @@ export default { try { if (this.isEdit) { // 배너 수정 - await updateCharacterBanner({ + const response = await updateCharacterBanner({ image: this.bannerForm.image, characterId: this.selectedCharacter.id, bannerId: this.bannerForm.bannerId }); - this.notifySuccess('배너가 수정되었습니다.'); + if (response && response.status === 200 && response.data && response.data.success === true) { + this.notifySuccess('배너가 수정되었습니다.'); + } else { + this.notifyError('배너 수정을 실패했습니다.'); + } } else { // 배너 추가 - await createCharacterBanner({ + const response = await createCharacterBanner({ image: this.bannerForm.image, characterId: this.selectedCharacter.id }); - this.notifySuccess('배너가 추가되었습니다.'); + if (response && response.status === 200 && response.data && response.data.success === true) { + this.notifySuccess('배너가 추가되었습니다.'); + } else { + this.notifyError('배너 추가를 실패했습니다.'); + } } // 다이얼로그 닫고 배너 목록 새로고침 @@ -514,10 +526,14 @@ export default { this.isSubmitting = true; try { - await deleteCharacterBanner(this.selectedBanner.id); - this.notifySuccess('배너가 삭제되었습니다.'); - this.showDeleteDialog = false; - this.refreshBanners(); + const response = await deleteCharacterBanner(this.selectedBanner.id); + if (response && response.status === 200 && response.data && response.data.success === true) { + this.notifySuccess('배너가 삭제되었습니다.'); + this.showDeleteDialog = false; + this.refreshBanners(); + } else { + this.notifyError('배너 삭제에 실패했습니다.'); + } } catch (error) { console.error('배너 삭제 오류:', error); this.notifyError('배너 삭제에 실패했습니다.'); @@ -538,8 +554,12 @@ export default { // 드래그 앤 드롭으로 순서 변경 후 API 호출 try { const bannerIds = this.banners.map(banner => banner.id); - await updateCharacterBannerOrder(bannerIds); - this.notifySuccess('배너 순서가 변경되었습니다.'); + const response = await updateCharacterBannerOrder(bannerIds); + if (response && response.status === 200 && response.data && response.data.success === true) { + this.notifySuccess('배너 순서가 변경되었습니다.'); + } else { + this.notifyError('배너 순서 변경에 실패했습니다.'); + } } catch (error) { console.error('배너 순서 변경 오류:', error); this.notifyError('배너 순서 변경에 실패했습니다.'); diff --git a/src/views/Chat/CharacterForm.vue b/src/views/Chat/CharacterForm.vue index 9c638aa..c8af1b1 100644 --- a/src/views/Chat/CharacterForm.vue +++ b/src/views/Chat/CharacterForm.vue @@ -135,7 +135,7 @@ md="6" > { + if (result[f] == null) result[f] = ''; + }); + return result; + }, + // 변경된 필드만 추출하는 함수 getChangedFields() { if (!this.originalCharacter || !this.isEdit) { @@ -1288,11 +1309,11 @@ export default { age: this.character.age, gender: this.character.gender, mbti: this.character.mbti, - type: this.character.type, + characterType: this.character.characterType, originalTitle: this.character.originalTitle, originalLink: this.character.originalLink, speechPattern: this.character.speechPattern, - speechStyle: this.character.conversationStyle, + speechStyle: this.character.speechStyle, appearance: this.character.appearance, tags: this.character.tags || [], hobbies: this.character.hobbies || [], @@ -1313,26 +1334,21 @@ export default { // 기본 필드 비교 const simpleFields = [ - 'name', 'description', 'age', 'gender', 'mbti', 'type', 'originalTitle', 'originalLink', - 'speechPattern', 'isActive' + 'name', 'description', 'age', 'gender', 'mbti', 'characterType', 'originalTitle', 'originalLink', + 'speechPattern', 'speechStyle', 'isActive' ]; simpleFields.forEach(field => { - if (this.character[field] !== this.originalCharacter[field]) { + if (!this.areEqualConsideringBlankNull(this.character[field], this.originalCharacter[field])) { changedFields[field] = this.character[field]; } }); - // 특수 필드 매핑 처리 (conversationStyle은 API에서 speechStyle로 사용됨) - if (this.character.conversationStyle !== this.originalCharacter.conversationStyle) { - changedFields.speechStyle = this.character.conversationStyle; - } - - if (this.character.systemPrompt !== this.originalCharacter.systemPrompt) { + if (!this.areEqualConsideringBlankNull(this.character.systemPrompt, this.originalCharacter.systemPrompt)) { changedFields.systemPrompt = this.character.systemPrompt; } - if (this.character.appearance !== this.originalCharacter.appearance) { + if (!this.areEqualConsideringBlankNull(this.character.appearance, this.originalCharacter.appearance)) { changedFields.appearance = this.character.appearance; } @@ -1377,16 +1393,17 @@ export default { const response = await getCharacter(id); // API 응답에서 캐릭터 정보 설정 - if (response && response.success === true && response.data) { - const data = response.data; + if (response && response.status === 200 && response.data.success === true) { + const data = response.data.data; // 원본 데이터 저장 (깊은 복사) this.originalCharacter = JSON.parse(JSON.stringify(data)); - // 기본 데이터 설정 + // 기본 데이터 설정 (null 값을 UI 표시를 위해 빈 문자열로 변환) + const normalized = this.normalizeCharacterData(data); this.character = { ...this.character, // 기본 구조 유지 - ...data, // API 응답 데이터로 덮어쓰기 + ...normalized, // API 응답 데이터(정규화)로 덮어쓰기 image: null // 파일 입력은 초기화 }; @@ -1444,11 +1461,11 @@ export default { response = await createCharacter(characterWithoutId); } - if (response && response.success === true && response.data) { - this.notifySuccess(this.isEdit ? '캐릭터가 수정되었습니다.' : '캐릭터가 등록되었습니다.'); - this.goBack(); + if (response && response.status === 200 && response.data && response.data.success === true) { + this.notifySuccess(this.isEdit ? '캐릭터가 수정되었습니다.' : '캐릭터가 등록되었습니다.'); + this.goBack(); } else { - this.notifyError('응답 데이터가 없습니다.'); + this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); } } catch (e) { console.error('캐릭터 저장 오류:', e); diff --git a/src/views/Chat/CharacterList.vue b/src/views/Chat/CharacterList.vue index 043db06..93136eb 100644 --- a/src/views/Chat/CharacterList.vue +++ b/src/views/Chat/CharacterList.vue @@ -380,14 +380,18 @@ export default { try { const response = await getCharacterList(this.page); - if (response && response.data) { - const data = response.data; - this.characters = data.content || []; + if (response && response.status === 200) { + if (response.data.success === true) { + const data = response.data.data; + this.characters = data.content || []; - const total_page = Math.ceil((data.totalCount || 0) / 20); - this.total_page = total_page <= 0 ? 1 : total_page; + const total_page = Math.ceil((data.totalCount || 0) / 20); + this.total_page = total_page <= 0 ? 1 : total_page; + } else { + this.notifyError('응답 데이터가 없습니다.'); + } } else { - this.notifyError('응답 데이터가 없습니다.'); + this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.'); } } catch (e) { console.error('캐릭터 목록 조회 오류:', e); @@ -412,8 +416,8 @@ export default { try { const response = await searchCharacters(this.search_word, this.page); - if (response && response.data) { - const data = response.data; + if (response && response.status === 200 && response.data && response.data.success === true) { + const data = response.data.data; this.characters = data.content || []; const total_page = Math.ceil((data.totalCount || 0) / 20);