feat(character): 캐릭터 등록/수정 폼 개선
- 생년월일을 나이로 변경 (숫자만 입력 가능) - 태그 기능 개선 (50자 이내, 20개 제한, 띄어쓰기 불가, 스페이스바로 등록) - 인물관계 추가 (최대 200자, 최대 10개) - 취미 목록 추가 (최대 100자, 최대 10개) - 가치관 목록 추가 (최대 100자, 최대 10개) - 목표 추가 (최대 200자, 최대 10개) - 말투/특징적 표현 필드 추가 (최대 500자) - 대화 스타일 추가 (최대 200자) - 외모 설명 추가 (최대 1000자) - 연령제한 제거 - UI 순서 변경 (채팅형태 입력 UI를 아래로 이동) - 채팅형태 UI 개선 (최근 입력이 위쪽에 표시)
This commit is contained in:
parent
13c85bb2a8
commit
72b1627f3f
|
@ -99,35 +99,21 @@
|
|||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- 생년월일 -->
|
||||
<!-- 나이 -->
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-menu
|
||||
v-model="birthDateMenu"
|
||||
:close-on-content-click="false"
|
||||
transition="scale-transition"
|
||||
offset-y
|
||||
min-width="auto"
|
||||
>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-text-field
|
||||
v-model="formattedBirthDate"
|
||||
label="생년월일"
|
||||
readonly
|
||||
outlined
|
||||
dense
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
/>
|
||||
</template>
|
||||
<v-date-picker
|
||||
v-model="character.birthDate"
|
||||
locale="ko"
|
||||
@input="birthDateMenu = false"
|
||||
/>
|
||||
</v-menu>
|
||||
<v-text-field
|
||||
v-model="character.age"
|
||||
label="나이"
|
||||
type="number"
|
||||
min="0"
|
||||
outlined
|
||||
dense
|
||||
:rules="ageRules"
|
||||
@input="validateNumberInput"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
@ -145,20 +131,6 @@
|
|||
dense
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- 연령제한 - 스위치 형태 -->
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<v-switch
|
||||
v-model="character.ageRestricted"
|
||||
label="연령제한"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mt-4"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 태그 - 입력 후 띄어쓰기 할 때마다 Chip 형태로 변경 -->
|
||||
|
@ -173,6 +145,7 @@
|
|||
deletable-chips
|
||||
outlined
|
||||
dense
|
||||
:rules="tagRules"
|
||||
@keydown.space.prevent="addTag"
|
||||
>
|
||||
<template v-slot:selection="{ attrs, item, select, selected }">
|
||||
|
@ -190,7 +163,7 @@
|
|||
<div
|
||||
class="caption grey--text text--darken-1 custom-caption"
|
||||
>
|
||||
태그를 입력하고 엔터를 누르면 추가됩니다.
|
||||
태그를 입력하고 엔터 또는 스페이스바를 누르면 추가됩니다. (50자 이내, 최대 20개, 띄어쓰기 불가)
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -218,19 +191,6 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 인물 관계 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="character.relationships"
|
||||
label="인물 관계"
|
||||
outlined
|
||||
auto-grow
|
||||
rows="4"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 성격 특성 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
|
@ -249,10 +209,42 @@
|
|||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="character.speechPattern"
|
||||
label="말투/특징적 표현"
|
||||
label="말투/특징적 표현 (최대 500자)"
|
||||
outlined
|
||||
auto-grow
|
||||
rows="4"
|
||||
counter="500"
|
||||
:rules="[v => v.length <= 500 || '최대 500자까지 입력 가능합니다']"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 대화 스타일 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="character.conversationStyle"
|
||||
label="대화 스타일 (최대 200자)"
|
||||
outlined
|
||||
auto-grow
|
||||
rows="3"
|
||||
counter="200"
|
||||
:rules="[v => v.length <= 200 || '최대 200자까지 입력 가능합니다']"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 외모 설명 -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="character.appearance"
|
||||
label="외모 설명 (최대 1000자)"
|
||||
outlined
|
||||
auto-grow
|
||||
rows="5"
|
||||
counter="1000"
|
||||
:rules="[v => v.length <= 1000 || '최대 1000자까지 입력 가능합니다']"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -275,6 +267,314 @@
|
|||
</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="11">
|
||||
<v-text-field
|
||||
v-model="newRelationship"
|
||||
label="새 인물 관계 추가 (최대 200자)"
|
||||
outlined
|
||||
dense
|
||||
counter="200"
|
||||
:rules="[v => v.length <= 200 || '최대 200자까지 입력 가능합니다']"
|
||||
@keyup.enter="addRelationship"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-1"
|
||||
block
|
||||
:disabled="!newRelationship.trim() || relationships.length >= 10"
|
||||
@click="addRelationship"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card
|
||||
outlined
|
||||
class="memory-container"
|
||||
>
|
||||
<v-list
|
||||
v-if="relationships.length > 0"
|
||||
class="memory-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(relationship, index) in relationships"
|
||||
:key="index"
|
||||
class="memory-item"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="memory-text">
|
||||
{{ relationship }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="removeRelationship(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="11">
|
||||
<v-text-field
|
||||
v-model="newHobby"
|
||||
label="새 취미 추가 (최대 100자)"
|
||||
outlined
|
||||
dense
|
||||
counter="100"
|
||||
:rules="[v => v.length <= 100 || '최대 100자까지 입력 가능합니다']"
|
||||
@keyup.enter="addHobby"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-1"
|
||||
block
|
||||
:disabled="!newHobby.trim() || hobbies.length >= 10"
|
||||
@click="addHobby"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card
|
||||
outlined
|
||||
class="memory-container"
|
||||
>
|
||||
<v-list
|
||||
v-if="hobbies.length > 0"
|
||||
class="memory-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(hobby, index) in hobbies"
|
||||
:key="index"
|
||||
class="memory-item"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="memory-text">
|
||||
{{ hobby }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="removeHobby(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="11">
|
||||
<v-text-field
|
||||
v-model="newValue"
|
||||
label="새 가치관 추가 (최대 100자)"
|
||||
outlined
|
||||
dense
|
||||
counter="100"
|
||||
:rules="[v => v.length <= 100 || '최대 100자까지 입력 가능합니다']"
|
||||
@keyup.enter="addValue"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-1"
|
||||
block
|
||||
:disabled="!newValue.trim() || values.length >= 10"
|
||||
@click="addValue"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card
|
||||
outlined
|
||||
class="memory-container"
|
||||
>
|
||||
<v-list
|
||||
v-if="values.length > 0"
|
||||
class="memory-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(value, index) in values"
|
||||
:key="index"
|
||||
class="memory-item"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="memory-text">
|
||||
{{ value }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="removeValue(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="11">
|
||||
<v-text-field
|
||||
v-model="newGoal"
|
||||
label="새 목표 추가 (최대 200자)"
|
||||
outlined
|
||||
dense
|
||||
counter="200"
|
||||
:rules="[v => v.length <= 200 || '최대 200자까지 입력 가능합니다']"
|
||||
@keyup.enter="addGoal"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="1">
|
||||
<v-btn
|
||||
color="primary"
|
||||
class="mt-1"
|
||||
block
|
||||
:disabled="!newGoal.trim() || goals.length >= 10"
|
||||
@click="addGoal"
|
||||
>
|
||||
추가
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card
|
||||
outlined
|
||||
class="memory-container"
|
||||
>
|
||||
<v-list
|
||||
v-if="goals.length > 0"
|
||||
class="memory-list"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="(goal, index) in goals"
|
||||
:key="index"
|
||||
class="memory-item"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title class="memory-text">
|
||||
{{ goal }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
small
|
||||
color="error"
|
||||
class="delete-btn"
|
||||
@click="removeGoal(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">
|
||||
|
@ -402,10 +702,17 @@ export default {
|
|||
isFormValid: false,
|
||||
isEdit: false,
|
||||
previewImage: null,
|
||||
birthDateMenu: false,
|
||||
newMemory: '',
|
||||
newRelationship: '',
|
||||
newHobby: '',
|
||||
newValue: '',
|
||||
newGoal: '',
|
||||
tags: [],
|
||||
memories: [],
|
||||
relationships: [],
|
||||
hobbies: [],
|
||||
values: [],
|
||||
goals: [],
|
||||
character: {
|
||||
id: null,
|
||||
name: '',
|
||||
|
@ -415,17 +722,25 @@ export default {
|
|||
isActive: true,
|
||||
createdAt: '',
|
||||
gender: '',
|
||||
birthDate: null,
|
||||
age: '',
|
||||
mbti: '',
|
||||
ageRestricted: false,
|
||||
worldView: '',
|
||||
relationships: '',
|
||||
personality: '',
|
||||
speechPattern: '',
|
||||
conversationStyle: '',
|
||||
appearance: '',
|
||||
systemPrompt: '',
|
||||
tags: [],
|
||||
memories: []
|
||||
},
|
||||
ageRules: [
|
||||
v => !!v || '나이를 입력하세요',
|
||||
v => (v && !isNaN(v) && parseInt(v) >= 0) || '유효한 나이를 입력하세요'
|
||||
],
|
||||
tagRules: [
|
||||
v => v.length <= 20 || '태그는 최대 20개까지 등록 가능합니다'
|
||||
],
|
||||
nameRules: [
|
||||
v => !!v || '이름을 입력하세요',
|
||||
v => (v && v.trim().length > 0) || '이름을 입력하세요'
|
||||
|
@ -448,10 +763,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
formattedBirthDate() {
|
||||
if (!this.character.birthDate) return '';
|
||||
return this.character.birthDate;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -487,6 +798,13 @@ export default {
|
|||
this.$router.push('/character');
|
||||
},
|
||||
|
||||
validateNumberInput() {
|
||||
// 숫자만 입력 가능하도록 처리
|
||||
if (this.character.age && isNaN(this.character.age)) {
|
||||
this.character.age = this.character.age.replace(/[^0-9]/g, '');
|
||||
}
|
||||
},
|
||||
|
||||
createImagePreview(file) {
|
||||
if (!file) return;
|
||||
|
||||
|
@ -500,10 +818,21 @@ export default {
|
|||
addTag() {
|
||||
const lastTag = this.tags[this.tags.length - 1];
|
||||
if (lastTag && typeof lastTag === 'string' && lastTag.trim()) {
|
||||
this.tags.splice(this.tags.length - 1, 1, lastTag.trim());
|
||||
this.$nextTick(() => {
|
||||
this.tags.push('');
|
||||
});
|
||||
// 띄어쓰기 제거 및 50자 제한
|
||||
let processedTag = lastTag.trim().replace(/\s+/g, '');
|
||||
if (processedTag.length > 50) {
|
||||
processedTag = processedTag.substring(0, 50);
|
||||
}
|
||||
|
||||
// 태그 개수 제한 (20개)
|
||||
if (this.tags.length <= 20) {
|
||||
this.tags.splice(this.tags.length - 1, 1, processedTag);
|
||||
this.$nextTick(() => {
|
||||
this.tags.push('');
|
||||
});
|
||||
} else {
|
||||
this.notifyError('태그는 최대 20개까지 등록 가능합니다.');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -513,7 +842,7 @@ export default {
|
|||
|
||||
addMemory() {
|
||||
if (this.newMemory.trim()) {
|
||||
this.memories.push(this.newMemory.trim());
|
||||
this.memories.unshift(this.newMemory.trim());
|
||||
this.newMemory = '';
|
||||
}
|
||||
},
|
||||
|
@ -522,6 +851,86 @@ export default {
|
|||
this.memories.splice(index, 1);
|
||||
},
|
||||
|
||||
addRelationship() {
|
||||
if (this.newRelationship.trim()) {
|
||||
if (this.relationships.length >= 10) {
|
||||
this.notifyError('인물 관계는 최대 10개까지 등록 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newRelationship.length > 200) {
|
||||
this.newRelationship = this.newRelationship.substring(0, 200);
|
||||
}
|
||||
|
||||
this.relationships.unshift(this.newRelationship.trim());
|
||||
this.newRelationship = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeRelationship(index) {
|
||||
this.relationships.splice(index, 1);
|
||||
},
|
||||
|
||||
addHobby() {
|
||||
if (this.newHobby.trim()) {
|
||||
if (this.hobbies.length >= 10) {
|
||||
this.notifyError('취미는 최대 10개까지 등록 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newHobby.length > 100) {
|
||||
this.newHobby = this.newHobby.substring(0, 100);
|
||||
}
|
||||
|
||||
this.hobbies.unshift(this.newHobby.trim());
|
||||
this.newHobby = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeHobby(index) {
|
||||
this.hobbies.splice(index, 1);
|
||||
},
|
||||
|
||||
addValue() {
|
||||
if (this.newValue.trim()) {
|
||||
if (this.values.length >= 10) {
|
||||
this.notifyError('가치관은 최대 10개까지 등록 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newValue.length > 100) {
|
||||
this.newValue = this.newValue.substring(0, 100);
|
||||
}
|
||||
|
||||
this.values.unshift(this.newValue.trim());
|
||||
this.newValue = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeValue(index) {
|
||||
this.values.splice(index, 1);
|
||||
},
|
||||
|
||||
addGoal() {
|
||||
if (this.newGoal.trim()) {
|
||||
if (this.goals.length >= 10) {
|
||||
this.notifyError('목표는 최대 10개까지 등록 가능합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.newGoal.length > 200) {
|
||||
this.newGoal = this.newGoal.substring(0, 200);
|
||||
}
|
||||
|
||||
this.goals.unshift(this.newGoal.trim());
|
||||
this.newGoal = '';
|
||||
}
|
||||
},
|
||||
|
||||
removeGoal(index) {
|
||||
this.goals.splice(index, 1);
|
||||
},
|
||||
|
||||
async loadCharacter(id) {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
|
@ -538,9 +947,13 @@ 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 || [];
|
||||
} else {
|
||||
this.notifyError('캐릭터 정보를 불러올 수 없습니다.');
|
||||
}
|
||||
|
@ -563,9 +976,13 @@ 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];
|
||||
|
||||
let response;
|
||||
|
||||
|
|
Loading…
Reference in New Issue