feat(character): 캐릭터 폼에 성격 특성, 세계관, 기억 기능 개선

- 성격 특성(personalities): 제목(trait)과 설명(description) 입력 UI 구현, 최대 10개
- 세계관(배경)(backgrounds): 제목(topic)과 설명(description) 입력 UI 구현, 최대 10개
- 기억(memories): 제목(title), 기억(content), 감정(emotion) 입력 UI 구현, 최대 20개
This commit is contained in:
Yu Sung 2025-08-07 18:17:59 +09:00
parent 72b1627f3f
commit 7f56d0b423
1 changed files with 345 additions and 39 deletions

View File

@ -163,7 +163,7 @@
<div
class="caption grey--text text--darken-1 custom-caption"
>
태그를 입력하고 엔터 또는 스페이스바누르면 추가됩니다. (50 이내, 최대 20, 띄어쓰기 불가)
태그를 입력하고 엔터누르면 추가됩니다. (50 이내, 최대 20)
</div>
</v-col>
</v-row>
@ -178,31 +178,6 @@
</v-col>
</v-row>
<!-- 세계관 (배경이야기) -->
<v-row>
<v-col cols="12">
<v-textarea
v-model="character.worldView"
label="세계관 (배경이야기)"
outlined
auto-grow
rows="4"
/>
</v-col>
</v-row>
<!-- 성격 특성 -->
<v-row>
<v-col cols="12">
<v-textarea
v-model="character.personality"
label="성격 특성"
outlined
auto-grow
rows="4"
/>
</v-col>
</v-row>
<!-- 말투/특징적 표현 -->
<v-row>
@ -575,6 +550,191 @@
</v-col>
</v-row>
<!-- 세계관 (배경이야기) -->
<v-row>
<v-col cols="12">
<v-divider class="my-4" />
<h3 class="mb-2">
세계관 (배경이야기)
</h3>
<v-row>
<v-col cols="12">
<v-row>
<v-col cols="5">
<v-text-field
v-model="newBackgroundTopic"
label="주제 (최대 100자)"
outlined
dense
counter="100"
:rules="[v => v.length <= 100 || '최대 100자까지 입력 가능합니다']"
@keyup.enter="addBackground"
/>
</v-col>
<v-col cols="6">
<v-textarea
v-model="newBackgroundDescription"
label="배경 설명 (최대 1000자)"
outlined
dense
auto-grow
rows="2"
counter="1000"
:rules="[v => v.length <= 1000 || '최대 1000자까지 입력 가능합니다']"
/>
</v-col>
<v-col cols="1">
<v-btn
color="primary"
class="mt-1"
block
:disabled="!newBackgroundTopic.trim() || !newBackgroundDescription.trim() || backgrounds.length >= 10"
@click="addBackground"
>
추가
</v-btn>
</v-col>
</v-row>
</v-col>
</v-row>
<v-card
outlined
class="memory-container"
>
<v-list
v-if="backgrounds.length > 0"
class="memory-list"
>
<v-list-item
v-for="(background, index) in backgrounds"
:key="index"
class="memory-item"
>
<v-list-item-content>
<v-list-item-title class="memory-text font-weight-bold">
{{ background.topic }}
</v-list-item-title>
<v-list-item-subtitle class="memory-text mt-1">
{{ background.description }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn
small
color="error"
class="delete-btn"
@click="removeBackground(index)"
>
삭제
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
<v-card-text
v-else
class="text-center grey--text"
>
세계관 정보가 없습니다. 입력창에서 세계관 정보를 추가해주세요. (최대 10)
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 성격 특성 -->
<v-row>
<v-col cols="12">
<v-divider class="my-4" />
<h3 class="mb-2">
성격 특성
</h3>
<v-row>
<v-col cols="12">
<v-row>
<v-col cols="5">
<v-text-field
v-model="newPersonalityTrait"
label="특성 제목 (최대 100자)"
outlined
dense
counter="100"
:rules="[v => v.length <= 100 || '최대 100자까지 입력 가능합니다']"
@keyup.enter="addPersonality"
/>
</v-col>
<v-col cols="6">
<v-textarea
v-model="newPersonalityDescription"
label="특성 설명 (최대 500자)"
outlined
dense
auto-grow
rows="2"
counter="500"
:rules="[v => v.length <= 500 || '최대 500자까지 입력 가능합니다']"
/>
</v-col>
<v-col cols="1">
<v-btn
color="primary"
class="mt-1"
block
:disabled="!newPersonalityTrait.trim() || !newPersonalityDescription.trim() || personalities.length >= 10"
@click="addPersonality"
>
추가
</v-btn>
</v-col>
</v-row>
</v-col>
</v-row>
<v-card
outlined
class="memory-container"
>
<v-list
v-if="personalities.length > 0"
class="memory-list"
>
<v-list-item
v-for="(personality, index) in personalities"
:key="index"
class="memory-item"
>
<v-list-item-content>
<v-list-item-title class="memory-text font-weight-bold">
{{ personality.trait }}
</v-list-item-title>
<v-list-item-subtitle class="memory-text mt-1">
{{ personality.description }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn
small
color="error"
class="delete-btn"
@click="removePersonality(index)"
>
삭제
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list>
<v-card-text
v-else
class="text-center grey--text"
>
성격 특성이 없습니다. 입력창에서 성격 특성을 추가해주세요. (최대 10)
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 기억/메모리 -->
<v-row>
<v-col cols="12">
@ -586,12 +746,36 @@
<v-row>
<v-col cols="12">
<v-row>
<v-col cols="11">
<v-col cols="4">
<v-text-field
v-model="newMemory"
label="새 기억 추가"
v-model="newMemoryTitle"
label="제목 (최대 100자)"
outlined
dense
counter="100"
:rules="[v => v.length <= 100 || '최대 100자까지 입력 가능합니다']"
/>
</v-col>
<v-col cols="4">
<v-textarea
v-model="newMemoryContent"
label="기억 내용 (최대 1000자)"
outlined
dense
auto-grow
rows="2"
counter="1000"
:rules="[v => v.length <= 1000 || '최대 1000자까지 입력 가능합니다']"
/>
</v-col>
<v-col cols="3">
<v-text-field
v-model="newMemoryEmotion"
label="감정 (최대 50자)"
outlined
dense
counter="50"
:rules="[v => v.length <= 50 || '최대 50자까지 입력 가능합니다']"
@keyup.enter="addMemory"
/>
</v-col>
@ -600,7 +784,7 @@
color="primary"
class="mt-1"
block
:disabled="!newMemory.trim()"
:disabled="!newMemoryTitle.trim() || !newMemoryContent.trim() || memories.length >= 20"
@click="addMemory"
>
추가
@ -624,9 +808,18 @@
class="memory-item"
>
<v-list-item-content>
<v-list-item-title class="memory-text">
{{ memory }}
<v-list-item-title class="memory-text font-weight-bold">
{{ memory.title }}
</v-list-item-title>
<v-list-item-subtitle class="memory-text mt-1">
{{ memory.content }}
</v-list-item-subtitle>
<v-list-item-subtitle
v-if="memory.emotion"
class="memory-text mt-1 font-italic"
>
감정: {{ memory.emotion }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-btn
@ -644,7 +837,7 @@
v-else
class="text-center grey--text"
>
기억이 없습니다. 입력창에서 기억을 추가해주세요.
기억이 없습니다. 입력창에서 기억을 추가해주세요. (최대 20)
</v-card-text>
</v-card>
</v-col>
@ -702,17 +895,25 @@ export default {
isFormValid: false,
isEdit: false,
previewImage: null,
newMemory: '',
newMemoryTitle: '',
newMemoryContent: '',
newMemoryEmotion: '',
newRelationship: '',
newHobby: '',
newValue: '',
newGoal: '',
newPersonalityTrait: '',
newPersonalityDescription: '',
newBackgroundTopic: '',
newBackgroundDescription: '',
tags: [],
memories: [],
relationships: [],
hobbies: [],
values: [],
goals: [],
personalities: [],
backgrounds: [],
character: {
id: null,
name: '',
@ -841,9 +1042,40 @@ export default {
},
addMemory() {
if (this.newMemory.trim()) {
this.memories.unshift(this.newMemory.trim());
this.newMemory = '';
if (this.newMemoryTitle.trim() && this.newMemoryContent.trim()) {
if (this.memories.length >= 20) {
this.notifyError('기억은 최대 20개까지 등록 가능합니다.');
return;
}
//
let title = this.newMemoryTitle.trim();
let content = this.newMemoryContent.trim();
let emotion = this.newMemoryEmotion.trim();
if (title.length > 100) {
title = title.substring(0, 100);
}
if (content.length > 1000) {
content = content.substring(0, 1000);
}
if (emotion.length > 50) {
emotion = emotion.substring(0, 50);
}
//
this.memories.unshift({
title,
content,
emotion
});
//
this.newMemoryTitle = '';
this.newMemoryContent = '';
this.newMemoryEmotion = '';
}
},
@ -931,6 +1163,76 @@ export default {
this.goals.splice(index, 1);
},
addPersonality() {
if (this.newPersonalityTrait.trim() && this.newPersonalityDescription.trim()) {
if (this.personalities.length >= 10) {
this.notifyError('성격 특성은 최대 10개까지 등록 가능합니다.');
return;
}
//
let trait = this.newPersonalityTrait.trim();
let description = this.newPersonalityDescription.trim();
if (trait.length > 100) {
trait = trait.substring(0, 100);
}
if (description.length > 500) {
description = description.substring(0, 500);
}
//
this.personalities.unshift({
trait,
description
});
//
this.newPersonalityTrait = '';
this.newPersonalityDescription = '';
}
},
removePersonality(index) {
this.personalities.splice(index, 1);
},
addBackground() {
if (this.newBackgroundTopic.trim() && this.newBackgroundDescription.trim()) {
if (this.backgrounds.length >= 10) {
this.notifyError('세계관 정보는 최대 10개까지 등록 가능합니다.');
return;
}
//
let topic = this.newBackgroundTopic.trim();
let description = this.newBackgroundDescription.trim();
if (topic.length > 100) {
topic = topic.substring(0, 100);
}
if (description.length > 1000) {
description = description.substring(0, 1000);
}
//
this.backgrounds.unshift({
topic,
description
});
//
this.newBackgroundTopic = '';
this.newBackgroundDescription = '';
}
},
removeBackground(index) {
this.backgrounds.splice(index, 1);
},
async loadCharacter(id) {
this.isLoading = true;
try {
@ -947,13 +1249,15 @@ export default {
image: null //
};
// , , , , ,
// , , , , , , ,
this.tags = data.tags || [];
this.memories = data.memories || [];
this.relationships = data.relationships || [];
this.hobbies = data.hobbies || [];
this.values = data.values || [];
this.goals = data.goals || [];
this.personalities = data.personalities || [];
this.backgrounds = data.backgrounds || [];
} else {
this.notifyError('캐릭터 정보를 불러올 수 없습니다.');
}
@ -976,13 +1280,15 @@ export default {
this.isLoading = true;
try {
// , , , , ,
// , , , , , , ,
this.character.tags = this.tags.filter(tag => tag && typeof tag === 'string' && tag.trim());
this.character.memories = [...this.memories];
this.character.relationships = [...this.relationships];
this.character.hobbies = [...this.hobbies];
this.character.values = [...this.values];
this.character.goals = [...this.goals];
this.character.personalities = [...this.personalities];
this.character.backgrounds = [...this.backgrounds];
let response;