feat(chat): 캐릭터 폼에 JSON 내보내기/가져오기 기능 추가
- 툴바에 'JSON 다운로드/업로드' 버튼 추가 - buildSerializablePayload, exportToJson, onImportFileChange, applyImportedData 메서드 구현 - 이미지(image, imageUrl) 및 isActive는 직렬화/역직렬화에서 제외 - 업로드 시 버전 검증 및 길이/개수 제한, 중요도(1~10) 보정 적용 - 사용자 알림 메시지(성공/오류) 한글화
This commit is contained in:
parent
bc8833483a
commit
199049ab7c
|
@ -10,6 +10,9 @@
|
|||
<v-spacer />
|
||||
<v-toolbar-title>{{ isEdit ? '캐릭터 수정' : '캐릭터 등록' }}</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn small outlined color="primary" @click="exportToJson">JSON 다운로드</v-btn>
|
||||
<input ref="importInput" type="file" accept="application/json,.json" style="display:none" @change="onImportFileChange" />
|
||||
<v-btn small color="primary" class="ml-2" @click="$refs.importInput.click()">JSON 업로드</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container>
|
||||
|
@ -1630,6 +1633,147 @@ export default {
|
|||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ===== JSON 내보내기/가져오기 =====
|
||||
buildSerializablePayload() {
|
||||
const c = {
|
||||
...this.character,
|
||||
tags: this.tags.filter(t => t && typeof t === 'string' && t.trim()),
|
||||
hobbies: [...this.hobbies],
|
||||
values: [...this.values],
|
||||
goals: [...this.goals],
|
||||
relationships: [...this.relationships],
|
||||
personalities: [...this.personalities],
|
||||
backgrounds: [...this.backgrounds],
|
||||
memories: [...this.memories]
|
||||
};
|
||||
|
||||
// 이미지 및 상태 관련 필드 제외
|
||||
delete c.image;
|
||||
delete c.imageUrl;
|
||||
delete c.isActive;
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
...c
|
||||
};
|
||||
},
|
||||
|
||||
exportToJson() {
|
||||
try {
|
||||
const payload = this.buildSerializablePayload();
|
||||
const json = JSON.stringify(payload, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json;charset=utf-8' });
|
||||
|
||||
const filenameBase = (this.character.name || 'character').replace(/\s+/g, '_');
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = `${filenameBase}.character.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
this.notifySuccess('JSON 파일로 내보냈습니다.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.notifyError('내보내기 중 오류가 발생했습니다.');
|
||||
}
|
||||
},
|
||||
|
||||
onImportFileChange(e) {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const text = reader.result;
|
||||
const data = JSON.parse(text);
|
||||
this.applyImportedData(data);
|
||||
this.notifySuccess('JSON 데이터를 불러왔습니다.');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.notifyError('유효한 JSON 파일이 아닙니다.');
|
||||
} finally {
|
||||
// 동일 파일 재업로드 허용
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
reader.readAsText(file, 'utf-8');
|
||||
},
|
||||
|
||||
applyImportedData(data) {
|
||||
// 1) 버전/형식 검증
|
||||
if (!data || (data.version !== 1 && data.version !== undefined)) {
|
||||
throw new Error('지원되지 않는 파일 버전입니다.');
|
||||
}
|
||||
|
||||
// 2) 안전한 추출(helper)
|
||||
const arr = (v) => Array.isArray(v) ? v : [];
|
||||
const str = (v) => (v == null ? '' : String(v));
|
||||
|
||||
// 3) 이미지 및 상태 관련 필드 강제 제거 (파일 포맷 요구사항)
|
||||
if (data) {
|
||||
delete data.image;
|
||||
delete data.imageUrl;
|
||||
delete data.isActive;
|
||||
}
|
||||
|
||||
// 4) 기본 필드 주입
|
||||
const patch = {
|
||||
name: str(data.name),
|
||||
description: str(data.description),
|
||||
systemPrompt: str(data.systemPrompt),
|
||||
age: str(data.age),
|
||||
gender: str(data.gender),
|
||||
mbti: str(data.mbti),
|
||||
characterType: str(data.characterType),
|
||||
originalTitle: str(data.originalTitle),
|
||||
originalLink: str(data.originalLink),
|
||||
speechPattern: str(data.speechPattern),
|
||||
speechStyle: str(data.speechStyle),
|
||||
appearance: str(data.appearance)
|
||||
};
|
||||
|
||||
// 5) 배열 필드 주입 + 간단 검증/정제
|
||||
const sanitizeString = (s, max) => String(s || '').trim().substring(0, max);
|
||||
|
||||
this.tags = arr(data.tags).map(t => sanitizeString(t, 50)).slice(0, 20);
|
||||
this.hobbies = arr(data.hobbies).map(h => sanitizeString(h, 100)).slice(0, 10);
|
||||
this.values = arr(data.values).map(v => sanitizeString(v, 100)).slice(0, 10);
|
||||
this.goals = arr(data.goals).map(g => sanitizeString(g, 200)).slice(0, 10);
|
||||
|
||||
this.memories = arr(data.memories).map(m => ({
|
||||
title: sanitizeString(m.title, 100),
|
||||
content: sanitizeString(m.content, 1000),
|
||||
emotion: sanitizeString(m.emotion, 50)
|
||||
})).slice(0, 20);
|
||||
|
||||
this.relationships = arr(data.relationships).map(r => ({
|
||||
personName: sanitizeString(r.personName, 10),
|
||||
relationshipName: sanitizeString(r.relationshipName, 20),
|
||||
description: sanitizeString(r.description, 500),
|
||||
importance: Math.max(1, Math.min(10, parseInt(r.importance || 1))),
|
||||
relationshipType: sanitizeString(r.relationshipType, 10),
|
||||
currentStatus: sanitizeString(r.currentStatus, 10)
|
||||
})).slice(0, 10);
|
||||
|
||||
this.personalities = arr(data.personalities).map(p => ({
|
||||
trait: sanitizeString(p.trait, 100),
|
||||
description: sanitizeString(p.description, 500)
|
||||
})).slice(0, 10);
|
||||
|
||||
this.backgrounds = arr(data.backgrounds).map(b => ({
|
||||
topic: sanitizeString(b.topic, 100),
|
||||
description: sanitizeString(b.description, 1000)
|
||||
})).slice(0, 10);
|
||||
|
||||
// 6) character에 반영 (image는 반드시 null 유지)
|
||||
this.character = {
|
||||
...this.character,
|
||||
...patch,
|
||||
image: null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue