feat(ai-character): 댓글과 팬톡 운영 기능 구현

This commit is contained in:
Yu Sung
2026-08-01 01:30:43 +09:00
parent 20d38e38c0
commit 3faca90135
20 changed files with 2154 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
import { useRef, useState } from "react";
import { focusFirstInvalidControl } from "@/shared/lib/focus-first-invalid-control";
export function FanTalkReplyForm({ content, errorMessage, isSaving, onChange, onSubmit, submitLabel = "답변 등록" }: { readonly content: string; readonly errorMessage: string | null; readonly isSaving: boolean; readonly onChange: (content: string) => void; readonly onSubmit: () => void; readonly submitLabel?: string }) {
const errorId = "fan-talk-reply-error";
const formRef = useRef<HTMLFormElement>(null);
const [contentErrorMessage, setContentErrorMessage] = useState<string | null>(null);
const visibleErrorMessage = contentErrorMessage ?? errorMessage;
function submit() {
if (content.trim().length === 0) {
setContentErrorMessage("답변 내용을 입력해 주세요.");
queueMicrotask(() => focusFirstInvalidControl(formRef.current));
return;
}
setContentErrorMessage(null);
onSubmit();
}
return (
<form
className="flex flex-col gap-3"
onSubmit={(event) => {
event.preventDefault();
submit();
}}
ref={formRef}
>
<label className="flex flex-col gap-2 text-sm font-semibold">
<textarea aria-describedby={visibleErrorMessage === null ? undefined : errorId} aria-invalid={visibleErrorMessage === null ? undefined : true} className="min-h-32 rounded-md border border-input bg-card px-3 py-2 text-base font-normal text-foreground" onChange={(event) => {
const nextContent = event.currentTarget.value;
if (nextContent.trim().length > 0) {
setContentErrorMessage(null);
}
onChange(nextContent);
}} value={content} />
</label>
{visibleErrorMessage === null ? null : <p aria-label="답변 내용 오류" className="rounded-md border border-destructive bg-card p-3 text-sm font-semibold text-destructive" id={errorId} role="alert">{visibleErrorMessage}</p>}
<button className="w-fit rounded-md border border-input bg-primary px-4 py-2 font-semibold text-primary-foreground hover:bg-[var(--button-bg-hover)] active:bg-[var(--button-bg-active)] disabled:opacity-60" disabled={isSaving} type="submit">
{submitLabel}
</button>
</form>
);
}