feat(ai-character): 커뮤니티 댓글 첫 답글 작성 지원

This commit is contained in:
Yu Sung
2026-08-06 17:43:43 +09:00
parent e82e209300
commit 00f06b992f
7 changed files with 785 additions and 7 deletions

View File

@@ -167,7 +167,7 @@ export function CommentThread({ apiClient, canMutate = true, target }: { readonl
<div className="flex flex-col gap-3">
{roots.data.items.map((comment) => (
<div className="flex flex-col gap-3" key={comment.id}>
<CommentItem canDelete={canMutate} canEdit={canMutate && canEdit(comment, requestTarget)} comment={comment} isSaving={isSaving} onDelete={() => void runMutation(() => deleteComment(apiClient, requestTarget, { commentId: comment.id }))} onEdit={(nextComment) => void runMutation(() => updateComment(apiClient, requestTarget, { commentId: comment.id, request: { comment: nextComment } }))} onShowReplies={() => toggleReplies(comment.id)} replyActionLabel={comment.replyCount > 0 || expandedRootIds.includes(comment.id) ? "답글 보기" : requestTarget.kind === "audio" && canMutate ? "답글 작성" : undefined} />
<CommentItem canDelete={canMutate} canEdit={canMutate && canEdit(comment, requestTarget)} comment={comment} isSaving={isSaving} onDelete={() => void runMutation(() => deleteComment(apiClient, requestTarget, { commentId: comment.id }))} onEdit={(nextComment) => void runMutation(() => updateComment(apiClient, requestTarget, { commentId: comment.id, request: { comment: nextComment } }))} onShowReplies={() => toggleReplies(comment.id)} replyActionLabel={comment.replyCount > 0 || expandedRootIds.includes(comment.id) ? "답글 보기" : canMutate ? "답글 작성" : undefined} />
{renderReplies(comment)}
</div>
))}

View File

@@ -135,7 +135,7 @@ function getFormForControl(control: HTMLElement): HTMLFormElement {
throw new Error("expected parent form");
}
test("CommentThread opens the existing reply form for a first Audio reply only", async () => {
test("CommentThread opens the existing reply form for a first Audio reply", async () => {
// Given
const requests: CapturedRequest[] = [];
render(<CommentThread apiClient={createThreadClient(requests)} target={target} />);
@@ -154,21 +154,29 @@ test("CommentThread opens the existing reply form for a first Audio reply only",
expect(within(repliesRegion).queryByRole("button", { name: /답글 작성/ })).not.toBeInTheDocument();
});
test("CommentThread keeps first-reply entry out of Community and read-only Audio roots", async () => {
test("CommentThread creates a first Community reply and keeps read-only Community roots closed", async () => {
// Given
const communityRequests: CapturedRequest[] = [];
const readOnlyRequests: CapturedRequest[] = [];
// When
const { unmount } = render(<CommentThread apiClient={createThreadClient(communityRequests)} target={communityTarget} />);
expect(await screen.findByText("AI 루트 댓글")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "AI 루트 댓글 답글 작성" }));
const repliesRegion = await screen.findByRole("region", { name: "AI 루트 댓글 답글" });
await waitFor(() => expect(communityRequests.filter((request) => request.method === undefined && request.path.includes("/1102/replies?page=0&size=20"))).toHaveLength(1));
fireEvent.change(within(repliesRegion).getByLabelText("AI 루트 댓글에 답글"), { target: { value: " 커뮤니티 첫 답글 " } });
fireEvent.click(within(repliesRegion).getByRole("button", { name: "답글 등록" }));
// Then
expect(await screen.findByText("AI 루트 댓글")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "AI 루트 댓글 답글 작성" })).not.toBeInTheDocument();
await waitFor(() => expect(communityRequests.filter((request) => request.method === "POST")).toEqual([
{ body: JSON.stringify({ comment: "커뮤니티 첫 답글", parentId: 1102, isSecret: false }), method: "POST", path: "/api/v2/admin/ai-characters/101/community-posts/7001/comments" },
]));
expect(within(repliesRegion).queryByRole("button", { name: /답글 작성/ })).not.toBeInTheDocument();
unmount();
// When
render(<CommentThread apiClient={createThreadClient(readOnlyRequests)} canMutate={false} target={target} />);
render(<CommentThread apiClient={createThreadClient(readOnlyRequests)} canMutate={false} target={communityTarget} />);
// Then
expect(await screen.findByText("AI 루트 댓글")).toBeInTheDocument();