feat(ai-character): 캐릭터 관리 기능 구현

This commit is contained in:
Yu Sung
2026-08-01 01:30:28 +09:00
parent a1cae336d9
commit 3550a2dd03
19 changed files with 3308 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
import type { CreateCharacterParams, UpdateCharacterParams } from "@/features/characters/api/character-api";
import type { CharacterDetail } from "@/features/characters/model/types";
import type { BackgroundDraft, CharacterOptionalFieldsValue, MemoryDraft, RelationshipDraft, TraitDraft } from "@/features/characters/components/CharacterOptionalFields";
type RelationshipRequest = NonNullable<CreateCharacterParams["request"]["relationships"]>[number];
type TraitRequest = NonNullable<CreateCharacterParams["request"]["personalities"]>[number];
type BackgroundRequest = NonNullable<CreateCharacterParams["request"]["backgrounds"]>[number];
type MemoryRequest = NonNullable<CreateCharacterParams["request"]["memories"]>[number];
type CreateOptionalRequest = Partial<CreateCharacterParams["request"]>;
type MutableCreateOptionalRequest = { -readonly [Key in keyof CreateOptionalRequest]: CreateOptionalRequest[Key] };
function emptyRelationship(): RelationshipDraft {
return { personName: "", relationshipName: "", description: "", importance: "", relationshipType: "", currentStatus: "" };
}
function emptyTrait(): TraitDraft {
return { trait: "", description: "" };
}
function emptyBackground(): BackgroundDraft {
return { topic: "", description: "" };
}
function emptyMemory(): MemoryDraft {
return { title: "", content: "", emotion: "" };
}
export function createEmptyCharacterOptionalFields(): CharacterOptionalFieldsValue {
return { age: "", gender: "", mbti: "", speechPattern: "", speechStyle: "", appearance: "", region: "", originalTitle: "", originalLink: "", originalTitleTouched: false, originalLinkTouched: false, characterType: "", tags: "", hobbies: "", values: "", goals: "", relationships: [emptyRelationship()], personalities: [emptyTrait()], backgrounds: [emptyBackground()], memories: [emptyMemory()] };
}
export function characterOptionalFieldsFromDetail(character: CharacterDetail): CharacterOptionalFieldsValue {
return {
age: character.age === null ? "" : String(character.age), gender: character.gender ?? "", mbti: character.mbti ?? "", speechPattern: character.speechPattern ?? "", speechStyle: character.speechStyle ?? "", appearance: character.appearance ?? "", region: character.region, originalTitle: "", originalLink: "", originalTitleTouched: false, originalLinkTouched: false, characterType: character.characterType,
tags: character.tags.join("\n"), hobbies: character.hobbies.join("\n"), values: character.values.join("\n"), goals: character.goals.join("\n"),
relationships: character.relationships.length === 0 ? [emptyRelationship()] : character.relationships.map((row) => ({ ...row, importance: String(row.importance) })), personalities: character.personalities.length === 0 ? [emptyTrait()] : character.personalities, backgrounds: character.backgrounds.length === 0 ? [emptyBackground()] : character.backgrounds, memories: character.memories.length === 0 ? [emptyMemory()] : character.memories,
};
}
function trimOptional(value: string): string | undefined {
const trimmed = value.trim();
return trimmed.length === 0 ? undefined : trimmed;
}
function trimNullable(value: string): string | null {
return trimOptional(value) ?? null;
}
function splitList(value: string): readonly string[] {
return value.split(/[\n,]/).map((item) => item.trim()).filter((item) => item.length > 0);
}
function hasRelationship(row: RelationshipDraft): boolean {
return [row.personName, row.relationshipName, row.description, row.importance, row.relationshipType, row.currentStatus].some((value) => value.trim().length > 0);
}
function relationships(rows: readonly RelationshipDraft[]): readonly RelationshipRequest[] {
return rows.filter(hasRelationship).map((row) => {
const importance = Number.parseInt(row.importance.trim(), 10);
return {
personName: row.personName.trim(),
relationshipName: row.relationshipName.trim(),
description: row.description.trim(),
importance: Number.isNaN(importance) ? 0 : importance,
relationshipType: row.relationshipType.trim(),
currentStatus: row.currentStatus.trim(),
};
});
}
function traits(rows: readonly TraitDraft[]): readonly TraitRequest[] {
return rows.filter((row) => row.trait.trim().length > 0 || row.description.trim().length > 0).map((row) => ({ trait: row.trait.trim(), description: row.description.trim() }));
}
function backgrounds(rows: readonly BackgroundDraft[]): readonly BackgroundRequest[] {
return rows.filter((row) => row.topic.trim().length > 0 || row.description.trim().length > 0).map((row) => ({ topic: row.topic.trim(), description: row.description.trim() }));
}
function memories(rows: readonly MemoryDraft[]): readonly MemoryRequest[] {
return rows.filter((row) => row.title.trim().length > 0 || row.content.trim().length > 0 || row.emotion.trim().length > 0).map((row) => ({ title: row.title.trim(), content: row.content.trim(), emotion: row.emotion.trim() }));
}
export function toCreateCharacterOptionalRequest(value: CharacterOptionalFieldsValue): CreateOptionalRequest {
const request: MutableCreateOptionalRequest = {};
const age = trimOptional(value.age);
const gender = trimOptional(value.gender);
const mbti = trimOptional(value.mbti);
const speechPattern = trimOptional(value.speechPattern);
const speechStyle = trimOptional(value.speechStyle);
const appearance = trimOptional(value.appearance);
const region = trimOptional(value.region);
const originalTitle = trimOptional(value.originalTitle);
const originalLink = trimOptional(value.originalLink);
const characterType = trimOptional(value.characterType);
const tags = splitList(value.tags);
const hobbies = splitList(value.hobbies);
const values = splitList(value.values);
const goals = splitList(value.goals);
const relationshipRows = relationships(value.relationships);
const personalityRows = traits(value.personalities);
const backgroundRows = backgrounds(value.backgrounds);
const memoryRows = memories(value.memories);
if (age !== undefined) request.age = age;
if (gender !== undefined) request.gender = gender;
if (mbti !== undefined) request.mbti = mbti;
if (speechPattern !== undefined) request.speechPattern = speechPattern;
if (speechStyle !== undefined) request.speechStyle = speechStyle;
if (appearance !== undefined) request.appearance = appearance;
if (region !== undefined) request.region = region;
if (originalTitle !== undefined) request.originalTitle = originalTitle;
if (originalLink !== undefined) request.originalLink = originalLink;
if (characterType !== undefined) request.characterType = characterType;
if (tags.length > 0) request.tags = tags;
if (hobbies.length > 0) request.hobbies = hobbies;
if (values.length > 0) request.values = values;
if (goals.length > 0) request.goals = goals;
if (relationshipRows.length > 0) request.relationships = relationshipRows;
if (personalityRows.length > 0) request.personalities = personalityRows;
if (backgroundRows.length > 0) request.backgrounds = backgroundRows;
if (memoryRows.length > 0) request.memories = memoryRows;
return request;
}
export function toUpdateCharacterOptionalRequest(value: CharacterOptionalFieldsValue): UpdateCharacterParams["request"] {
const tags = splitList(value.tags);
const hobbies = splitList(value.hobbies);
const values = splitList(value.values);
const goals = splitList(value.goals);
const relationshipRows = relationships(value.relationships);
const personalityRows = traits(value.personalities);
const backgroundRows = backgrounds(value.backgrounds);
const memoryRows = memories(value.memories);
return {
age: trimNullable(value.age), gender: trimNullable(value.gender), mbti: trimNullable(value.mbti), speechPattern: trimNullable(value.speechPattern), speechStyle: trimNullable(value.speechStyle), appearance: trimNullable(value.appearance), ...(value.originalTitleTouched ? { originalTitle: trimNullable(value.originalTitle) } : {}), ...(value.originalLinkTouched ? { originalLink: trimNullable(value.originalLink) } : {}), characterType: trimNullable(value.characterType),
tags: tags.length === 0 ? null : tags, hobbies: hobbies.length === 0 ? null : hobbies, values: values.length === 0 ? null : values, goals: goals.length === 0 ? null : goals,
relationships: relationshipRows.length === 0 ? null : relationshipRows, personalities: personalityRows.length === 0 ? null : personalityRows, backgrounds: backgroundRows.length === 0 ? null : backgroundRows, memories: memoryRows.length === 0 ? null : memoryRows,
};
}