Skip to content

Commit

Permalink
Merge pull request #31 from Nexters/feat/#18
Browse files Browse the repository at this point in the history
[Feat/#18] 채팅 페이지 기능 QA
  • Loading branch information
ljh0608 authored Jan 31, 2025
2 parents 87f78c6 + 0587dc8 commit 90f7d8e
Show file tree
Hide file tree
Showing 18 changed files with 347 additions and 113 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"react-textarea-autosize": "^8.5.7",
"styled-components": "^6.1.14",
"styled-reset": "^4.5.2",
"use-stick-to-bottom": "^1.0.44",
"zod": "^3.24.1"
},
"devDependencies": {
Expand Down
67 changes: 51 additions & 16 deletions src/chat/components/AcceptRejectButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { useChatMessagesContext } from "@/chat/hooks/useChatMessagesStore";
import { useSendChatMessage } from "@/chat/hooks/useSendChatMesasge";
import { useSendChatMessage } from "@/chat/hooks/useSendChatMessage";
import { delay } from "@/shared/utils/delay";
import { useParams } from "next/navigation";
import { useState } from "react";
import { css } from "styled-components";
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
import ChipButton from "./ChipButton";

type Props = {
open: boolean;
};

export default function AcceptRejectButtons({ open }: Props) {
const { addMessage, deleteMessage } = useChatMessagesContext();
const { mutate: sendChatMessage, isPending: isSendingChatMessage } = useSendChatMessage();
export default function AcceptRejectButtons() {
const { addMessage, deleteMessage, editMessage, state: messages } = useChatMessagesContext();
const { mutate: sendChatMessage } = useSendChatMessage();
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const {
enable: enableTextField,
disable: disableTextField,
hide: hideTextField,
focus: focusTextField,
} = useTextFieldInChatDisplayContext();
const { chatId } = useParams<{ chatId: string }>();

const rejectMessage = "아니, 얘기 더 들어봐";
const acceptMessage = "좋아! 타로 볼래";

const isSystemRepliedQuestion = messages[messages.length - 1]?.type === "SYSTEM_TAROT_QUESTION_REPLY";

if (!chatId) throw new Error("chatId가 Dynamic Route에서 전달 되어야 합니다.");

const handleAcceptClick = async () => {
setIsButtonDisabled(true);
hideTextField();
addMessage({
messageId: Math.random(),
type: "USER_NORMAL",
Expand All @@ -46,21 +54,34 @@ export default function AcceptRejectButtons({ open }: Props) {
intent: "TAROT_ACCEPT",
},
{
onSuccess: (data) => {
onSuccess: async (data) => {
deleteMessage(loadingMessageId);

addMessage({
messageId: data.messageId,
type: data.type,
sender: data.sender,
answers: data.answers,
answers: [data.answers[0]],
});

for (let index = 1; index < data.answers.length; index++) {
await delay(1000);
editMessage({
messageId: data.messageId,
type: data.type,
sender: data.sender,
answers: data.answers.slice(0, index + 1),
});
}
},
}
);
setIsButtonDisabled(false);
};

const handleRejectClick = async () => {
setIsButtonDisabled(true);
disableTextField();
addMessage({
messageId: Math.random(),
type: "USER_NORMAL",
Expand All @@ -87,21 +108,35 @@ export default function AcceptRejectButtons({ open }: Props) {
intent: "TAROT_DECLINE",
},
{
onSuccess: (data) => {
onSuccess: async (data) => {
deleteMessage(loadingMessageId);

addMessage({
messageId: data.messageId,
type: data.type,
sender: data.sender,
answers: data.answers,
answers: [data.answers[0]],
});

for (let index = 1; index < data.answers.length; index++) {
await delay(1000);
editMessage({
messageId: data.messageId,
type: data.type,
sender: data.sender,
answers: data.answers.slice(0, index + 1),
});
}
enableTextField();
await delay(1);
focusTextField();
},
}
);
setIsButtonDisabled(false);
};

if (!open) return null;
if (!isSystemRepliedQuestion) return null;

return (
<div
Expand All @@ -111,10 +146,10 @@ export default function AcceptRejectButtons({ open }: Props) {
margin-top: 76px;
`}
>
<ChipButton type="button" disabled={isSendingChatMessage} color="primary02" onClick={handleAcceptClick}>
<ChipButton type="button" disabled={isButtonDisabled} color="primary02" onClick={handleAcceptClick}>
{acceptMessage}
</ChipButton>
<ChipButton type="button" disabled={isSendingChatMessage} color="grey30" onClick={handleRejectClick}>
<ChipButton type="button" disabled={isButtonDisabled} color="grey30" onClick={handleRejectClick}>
{rejectMessage}
</ChipButton>
</div>
Expand Down
11 changes: 5 additions & 6 deletions src/chat/components/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
"use client";

import { ChatMessagesProvider } from "@/chat/hooks/useChatMessagesStore";
import ChatHeader from "./ChatHeader";
import { TextFieldInChatDisplayProvider } from "@/chat/hooks/useTextFieldInChatDisplayStore";
import ChatRoom from "./ChatRoom";

export default function Chat() {
// TODO: 채팅 메세지 목록 프리페치 SSR 필요
return (
<>
<ChatHeader />
<ChatMessagesProvider>
<ChatMessagesProvider>
<TextFieldInChatDisplayProvider>
<ChatRoom />
</ChatMessagesProvider>
</>
</TextFieldInChatDisplayProvider>
</ChatMessagesProvider>
);
}
1 change: 1 addition & 0 deletions src/chat/components/ChatBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default function ChatBubble({ sender, message, card, loading }: Props) {
background-color: ${({ theme }) => theme.colors.grey10};
${({ theme }) => theme.fonts.body3}
border-radius: 8px;
width: fit-content;
max-width: 260px;
color: ${({ theme }) => theme.colors.grey90};
white-space: pre-wrap;
Expand Down
2 changes: 0 additions & 2 deletions src/chat/components/ChatBubbleGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import ChatBubble from "./ChatBubble";

type Props = {
message: MessageType;
isJustSent: boolean;
};

export default function ChatBubbleGroup({ message }: Props) {
// TODO: 응답을 새로 받은 경우에만 메세지를 순차적으로 렌더링
const renderMessage = (message: MessageType) => {
if (message.tarotName) {
return <ChatBubble key={message.messageId} sender={"SYSTEM"} card={message.tarotName} />;
Expand Down
23 changes: 20 additions & 3 deletions src/chat/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ import KebobMenuIcon from "@/shared/assets/icons/kebab-menu.svg";
import BottomSheet from "@/shared/components/BottomSheet";
import HeaderContent from "@/shared/components/HeaderContent";
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
import { useRouter } from "next/navigation";
import { css } from "styled-components";
import { useCreateChatRoom } from "../hooks/useCreateChatRoom";

export default function ChatHeader() {
const { mutate: createChatRoom } = useCreateChatRoom();
const router = useRouter();

const handleResetChatClick = () => {
createChatRoom(undefined, {
onSuccess: (data) => {
router.push(`/chats/${data.roomId}`);
},
});
};

return (
<HeaderContent
divider
/**
* FIXME: 구분선이 추가되어야 하나 쌓임 맥락에 의해 전체 화면 오버플로우 되지 않는 문제
* 레이아웃 구조 개선 후 divider prop 적용 필요
* */
sticky
endAction={
<BottomSheet.Root>
Expand Down Expand Up @@ -47,8 +63,9 @@ export default function ChatHeader() {
<button type="button">친구에게 타로냥 알리기</button>
</li>
<li>
{/* TODO: 메뉴 버튼 액션 추가 */}
<button type="button">새 대화 시작하기</button>
<button type="button" onClick={handleResetChatClick}>
새 대화 시작하기
</button>
</li>
</ul>
</BottomSheet.Content>
Expand Down
Loading

0 comments on commit 90f7d8e

Please sign in to comment.