feat(chat): 캐릭터 폼에 JSON 내보내기/가져오기 기능 추가

- 툴바에 'JSON 다운로드/업로드' 버튼 추가
- buildSerializablePayload, exportToJson, onImportFileChange, applyImportedData 메서드 구현
- 이미지(image, imageUrl) 및 isActive는 직렬화/역직렬화에서 제외
- 업로드 시 버전 검증 및 길이/개수 제한, 중요도(1~10) 보정 적용
- 사용자 알림 메시지(성공/오류) 한글화
This commit is contained in:
Yu Sung 2025-09-06 00:58:00 +09:00
parent bc8833483a
commit 199049ab7c
1 changed files with 144 additions and 0 deletions

View File

@ -10,6 +10,9 @@
<v-spacer /> <v-spacer />
<v-toolbar-title>{{ isEdit ? '캐릭터 수정' : '캐릭터 등록' }}</v-toolbar-title> <v-toolbar-title>{{ isEdit ? '캐릭터 수정' : '캐릭터 등록' }}</v-toolbar-title>
<v-spacer /> <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-toolbar>
<v-container> <v-container>
@ -1630,6 +1633,147 @@ export default {
} finally { } finally {
this.isLoading = false; 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
};
} }
} }
} }