캐릭터 챗봇 #74
|
@ -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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue