feat(chat): 캐릭터 리스트, 추가/수정 폼, 배너
- response의 데이터 구조에 맞춰서 코드 수정
This commit is contained in:
parent
a3e82a81f8
commit
ba248f7680
|
@ -19,6 +19,14 @@ async function getCharacter(id) {
|
||||||
return Vue.axios.get(`/admin/chat/character/${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) {
|
async function createCharacter(characterData) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
@ -28,18 +36,18 @@ async function createCharacter(characterData) {
|
||||||
|
|
||||||
// 나머지 데이터는 JSON 문자열로 변환하여 request 필드에 추가
|
// 나머지 데이터는 JSON 문자열로 변환하여 request 필드에 추가
|
||||||
const requestData = {
|
const requestData = {
|
||||||
name: characterData.name,
|
name: toNullIfBlank(characterData.name),
|
||||||
systemPrompt: characterData.systemPrompt,
|
systemPrompt: toNullIfBlank(characterData.systemPrompt),
|
||||||
description: characterData.description,
|
description: toNullIfBlank(characterData.description),
|
||||||
age: characterData.age,
|
age: toNullIfBlank(characterData.age),
|
||||||
gender: characterData.gender,
|
gender: toNullIfBlank(characterData.gender),
|
||||||
mbti: characterData.mbti,
|
mbti: toNullIfBlank(characterData.mbti),
|
||||||
characterType: characterData.type,
|
characterType: toNullIfBlank(characterData.type),
|
||||||
originalTitle: characterData.originalTitle,
|
originalTitle: toNullIfBlank(characterData.originalTitle),
|
||||||
originalLink: characterData.originalLink,
|
originalLink: toNullIfBlank(characterData.originalLink),
|
||||||
speechPattern: characterData.speechPattern,
|
speechPattern: toNullIfBlank(characterData.speechPattern),
|
||||||
speechStyle: characterData.conversationStyle,
|
speechStyle: toNullIfBlank(characterData.speechStyle),
|
||||||
appearance: characterData.appearance,
|
appearance: toNullIfBlank(characterData.appearance),
|
||||||
tags: characterData.tags || [],
|
tags: characterData.tags || [],
|
||||||
hobbies: characterData.hobbies || [],
|
hobbies: characterData.hobbies || [],
|
||||||
values: characterData.values || [],
|
values: characterData.values || [],
|
||||||
|
@ -66,9 +74,18 @@ async function updateCharacter(characterData, image = null) {
|
||||||
// 이미지가 있는 경우에만 FormData에 추가
|
// 이미지가 있는 경우에만 FormData에 추가
|
||||||
if (image) formData.append('image', image)
|
if (image) formData.append('image', image)
|
||||||
|
|
||||||
// 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가
|
// 변경된 데이터만 JSON 문자열로 변환하여 request 필드에 추가 ('' -> null 변환)
|
||||||
// characterData는 이미 변경된 필드만 포함하고 있음
|
// 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, {
|
return Vue.axios.put(`/admin/chat/character/update`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -197,7 +197,9 @@
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col>
|
<v-col>
|
||||||
<div class="font-weight-medium">선택된 캐릭터: {{ selectedCharacter.name }}</div>
|
<div class="font-weight-medium">
|
||||||
|
선택된 캐릭터: {{ selectedCharacter.name }}
|
||||||
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
@ -356,8 +358,9 @@ export default {
|
||||||
try {
|
try {
|
||||||
const response = await getCharacterBannerList(this.page);
|
const response = await getCharacterBannerList(this.page);
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
const newBanners = response.data.content || [];
|
const data = response.data.data;
|
||||||
|
const newBanners = data.content || [];
|
||||||
this.banners = [...this.banners, ...newBanners];
|
this.banners = [...this.banners, ...newBanners];
|
||||||
|
|
||||||
// 더 불러올 데이터가 있는지 확인
|
// 더 불러올 데이터가 있는지 확인
|
||||||
|
@ -458,8 +461,9 @@ export default {
|
||||||
try {
|
try {
|
||||||
const response = await searchCharacters(this.searchKeyword);
|
const response = await searchCharacters(this.searchKeyword);
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
this.searchResults = response.data.content || [];
|
const data = response.data.data;
|
||||||
|
this.searchResults = data.content || [];
|
||||||
this.searchPerformed = true;
|
this.searchPerformed = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -482,19 +486,27 @@ export default {
|
||||||
try {
|
try {
|
||||||
if (this.isEdit) {
|
if (this.isEdit) {
|
||||||
// 배너 수정
|
// 배너 수정
|
||||||
await updateCharacterBanner({
|
const response = await updateCharacterBanner({
|
||||||
image: this.bannerForm.image,
|
image: this.bannerForm.image,
|
||||||
characterId: this.selectedCharacter.id,
|
characterId: this.selectedCharacter.id,
|
||||||
bannerId: this.bannerForm.bannerId
|
bannerId: this.bannerForm.bannerId
|
||||||
});
|
});
|
||||||
this.notifySuccess('배너가 수정되었습니다.');
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
|
this.notifySuccess('배너가 수정되었습니다.');
|
||||||
|
} else {
|
||||||
|
this.notifyError('배너 수정을 실패했습니다.');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 배너 추가
|
// 배너 추가
|
||||||
await createCharacterBanner({
|
const response = await createCharacterBanner({
|
||||||
image: this.bannerForm.image,
|
image: this.bannerForm.image,
|
||||||
characterId: this.selectedCharacter.id
|
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;
|
this.isSubmitting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteCharacterBanner(this.selectedBanner.id);
|
const response = await deleteCharacterBanner(this.selectedBanner.id);
|
||||||
this.notifySuccess('배너가 삭제되었습니다.');
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
this.showDeleteDialog = false;
|
this.notifySuccess('배너가 삭제되었습니다.');
|
||||||
this.refreshBanners();
|
this.showDeleteDialog = false;
|
||||||
|
this.refreshBanners();
|
||||||
|
} else {
|
||||||
|
this.notifyError('배너 삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('배너 삭제 오류:', error);
|
console.error('배너 삭제 오류:', error);
|
||||||
this.notifyError('배너 삭제에 실패했습니다.');
|
this.notifyError('배너 삭제에 실패했습니다.');
|
||||||
|
@ -538,8 +554,12 @@ export default {
|
||||||
// 드래그 앤 드롭으로 순서 변경 후 API 호출
|
// 드래그 앤 드롭으로 순서 변경 후 API 호출
|
||||||
try {
|
try {
|
||||||
const bannerIds = this.banners.map(banner => banner.id);
|
const bannerIds = this.banners.map(banner => banner.id);
|
||||||
await updateCharacterBannerOrder(bannerIds);
|
const response = await updateCharacterBannerOrder(bannerIds);
|
||||||
this.notifySuccess('배너 순서가 변경되었습니다.');
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
|
this.notifySuccess('배너 순서가 변경되었습니다.');
|
||||||
|
} else {
|
||||||
|
this.notifyError('배너 순서 변경에 실패했습니다.');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('배너 순서 변경 오류:', error);
|
console.error('배너 순서 변경 오류:', error);
|
||||||
this.notifyError('배너 순서 변경에 실패했습니다.');
|
this.notifyError('배너 순서 변경에 실패했습니다.');
|
||||||
|
|
|
@ -135,7 +135,7 @@
|
||||||
md="6"
|
md="6"
|
||||||
>
|
>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="character.type"
|
v-model="character.characterType"
|
||||||
:items="typeOptions"
|
:items="typeOptions"
|
||||||
label="캐릭터 유형"
|
label="캐릭터 유형"
|
||||||
outlined
|
outlined
|
||||||
|
@ -235,7 +235,7 @@
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-textarea
|
<v-textarea
|
||||||
v-model="character.conversationStyle"
|
v-model="character.speechStyle"
|
||||||
label="대화 스타일 (최대 200자)"
|
label="대화 스타일 (최대 200자)"
|
||||||
outlined
|
outlined
|
||||||
auto-grow
|
auto-grow
|
||||||
|
@ -962,11 +962,11 @@ export default {
|
||||||
gender: '',
|
gender: '',
|
||||||
age: '',
|
age: '',
|
||||||
mbti: '',
|
mbti: '',
|
||||||
type: '',
|
characterType: '',
|
||||||
originalTitle: '',
|
originalTitle: '',
|
||||||
originalLink: '',
|
originalLink: '',
|
||||||
speechPattern: '',
|
speechPattern: '',
|
||||||
conversationStyle: '',
|
speechStyle: '',
|
||||||
appearance: '',
|
appearance: '',
|
||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -1277,6 +1277,27 @@ export default {
|
||||||
this.backgrounds.splice(index, 1);
|
this.backgrounds.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 공백과 null을 동일하게 취급하는 비교 함수
|
||||||
|
areEqualConsideringBlankNull(a, b) {
|
||||||
|
const a1 = (a === null || a === undefined) ? '' : a;
|
||||||
|
const b1 = (b === null || b === undefined) ? '' : b;
|
||||||
|
return a1 === b1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 로드된 캐릭터 데이터에서 null을 빈 문자열로 변환 (UI 표시용)
|
||||||
|
normalizeCharacterData(data) {
|
||||||
|
const result = { ...data };
|
||||||
|
const simpleFields = [
|
||||||
|
'name', 'systemPrompt', 'description', 'age', 'gender', 'mbti',
|
||||||
|
'characterType', 'originalTitle', 'originalLink', 'speechPattern',
|
||||||
|
'speechStyle', 'appearance', 'imageUrl'
|
||||||
|
];
|
||||||
|
simpleFields.forEach(f => {
|
||||||
|
if (result[f] == null) result[f] = '';
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
// 변경된 필드만 추출하는 함수
|
// 변경된 필드만 추출하는 함수
|
||||||
getChangedFields() {
|
getChangedFields() {
|
||||||
if (!this.originalCharacter || !this.isEdit) {
|
if (!this.originalCharacter || !this.isEdit) {
|
||||||
|
@ -1288,11 +1309,11 @@ export default {
|
||||||
age: this.character.age,
|
age: this.character.age,
|
||||||
gender: this.character.gender,
|
gender: this.character.gender,
|
||||||
mbti: this.character.mbti,
|
mbti: this.character.mbti,
|
||||||
type: this.character.type,
|
characterType: this.character.characterType,
|
||||||
originalTitle: this.character.originalTitle,
|
originalTitle: this.character.originalTitle,
|
||||||
originalLink: this.character.originalLink,
|
originalLink: this.character.originalLink,
|
||||||
speechPattern: this.character.speechPattern,
|
speechPattern: this.character.speechPattern,
|
||||||
speechStyle: this.character.conversationStyle,
|
speechStyle: this.character.speechStyle,
|
||||||
appearance: this.character.appearance,
|
appearance: this.character.appearance,
|
||||||
tags: this.character.tags || [],
|
tags: this.character.tags || [],
|
||||||
hobbies: this.character.hobbies || [],
|
hobbies: this.character.hobbies || [],
|
||||||
|
@ -1313,26 +1334,21 @@ export default {
|
||||||
|
|
||||||
// 기본 필드 비교
|
// 기본 필드 비교
|
||||||
const simpleFields = [
|
const simpleFields = [
|
||||||
'name', 'description', 'age', 'gender', 'mbti', 'type', 'originalTitle', 'originalLink',
|
'name', 'description', 'age', 'gender', 'mbti', 'characterType', 'originalTitle', 'originalLink',
|
||||||
'speechPattern', 'isActive'
|
'speechPattern', 'speechStyle', 'isActive'
|
||||||
];
|
];
|
||||||
|
|
||||||
simpleFields.forEach(field => {
|
simpleFields.forEach(field => {
|
||||||
if (this.character[field] !== this.originalCharacter[field]) {
|
if (!this.areEqualConsideringBlankNull(this.character[field], this.originalCharacter[field])) {
|
||||||
changedFields[field] = this.character[field];
|
changedFields[field] = this.character[field];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 특수 필드 매핑 처리 (conversationStyle은 API에서 speechStyle로 사용됨)
|
if (!this.areEqualConsideringBlankNull(this.character.systemPrompt, this.originalCharacter.systemPrompt)) {
|
||||||
if (this.character.conversationStyle !== this.originalCharacter.conversationStyle) {
|
|
||||||
changedFields.speechStyle = this.character.conversationStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.character.systemPrompt !== this.originalCharacter.systemPrompt) {
|
|
||||||
changedFields.systemPrompt = this.character.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;
|
changedFields.appearance = this.character.appearance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1377,16 +1393,17 @@ export default {
|
||||||
const response = await getCharacter(id);
|
const response = await getCharacter(id);
|
||||||
|
|
||||||
// API 응답에서 캐릭터 정보 설정
|
// API 응답에서 캐릭터 정보 설정
|
||||||
if (response && response.success === true && response.data) {
|
if (response && response.status === 200 && response.data.success === true) {
|
||||||
const data = response.data;
|
const data = response.data.data;
|
||||||
|
|
||||||
// 원본 데이터 저장 (깊은 복사)
|
// 원본 데이터 저장 (깊은 복사)
|
||||||
this.originalCharacter = JSON.parse(JSON.stringify(data));
|
this.originalCharacter = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
// 기본 데이터 설정
|
// 기본 데이터 설정 (null 값을 UI 표시를 위해 빈 문자열로 변환)
|
||||||
|
const normalized = this.normalizeCharacterData(data);
|
||||||
this.character = {
|
this.character = {
|
||||||
...this.character, // 기본 구조 유지
|
...this.character, // 기본 구조 유지
|
||||||
...data, // API 응답 데이터로 덮어쓰기
|
...normalized, // API 응답 데이터(정규화)로 덮어쓰기
|
||||||
image: null // 파일 입력은 초기화
|
image: null // 파일 입력은 초기화
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1444,11 +1461,11 @@ export default {
|
||||||
response = await createCharacter(characterWithoutId);
|
response = await createCharacter(characterWithoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.success === true && response.data) {
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
this.notifySuccess(this.isEdit ? '캐릭터가 수정되었습니다.' : '캐릭터가 등록되었습니다.');
|
this.notifySuccess(this.isEdit ? '캐릭터가 수정되었습니다.' : '캐릭터가 등록되었습니다.');
|
||||||
this.goBack();
|
this.goBack();
|
||||||
} else {
|
} else {
|
||||||
this.notifyError('응답 데이터가 없습니다.');
|
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('캐릭터 저장 오류:', e);
|
console.error('캐릭터 저장 오류:', e);
|
||||||
|
|
|
@ -380,14 +380,18 @@ export default {
|
||||||
try {
|
try {
|
||||||
const response = await getCharacterList(this.page);
|
const response = await getCharacterList(this.page);
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.status === 200) {
|
||||||
const data = response.data;
|
if (response.data.success === true) {
|
||||||
this.characters = data.content || [];
|
const data = response.data.data;
|
||||||
|
this.characters = data.content || [];
|
||||||
|
|
||||||
const total_page = Math.ceil((data.totalCount || 0) / 20);
|
const total_page = Math.ceil((data.totalCount || 0) / 20);
|
||||||
this.total_page = total_page <= 0 ? 1 : total_page;
|
this.total_page = total_page <= 0 ? 1 : total_page;
|
||||||
|
} else {
|
||||||
|
this.notifyError('응답 데이터가 없습니다.');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notifyError('응답 데이터가 없습니다.');
|
this.notifyError('알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('캐릭터 목록 조회 오류:', e);
|
console.error('캐릭터 목록 조회 오류:', e);
|
||||||
|
@ -412,8 +416,8 @@ export default {
|
||||||
try {
|
try {
|
||||||
const response = await searchCharacters(this.search_word, this.page);
|
const response = await searchCharacters(this.search_word, this.page);
|
||||||
|
|
||||||
if (response && response.data) {
|
if (response && response.status === 200 && response.data && response.data.success === true) {
|
||||||
const data = response.data;
|
const data = response.data.data;
|
||||||
this.characters = data.content || [];
|
this.characters = data.content || [];
|
||||||
|
|
||||||
const total_page = Math.ceil((data.totalCount || 0) / 20);
|
const total_page = Math.ceil((data.totalCount || 0) / 20);
|
||||||
|
|
Loading…
Reference in New Issue