Skip to content

[Feat/#48] 채팅 페이지 QA #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 11, 2025
25 changes: 17 additions & 8 deletions src/chat/components/AcceptRejectButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { useChatMessagesContext } from "@/chat/hooks/useChatMessagesStore";
import { useSendChatMessage } from "@/chat/hooks/useSendChatMessage";
import { delay } from "@/shared/utils/delay";
import { motion } from "framer-motion";
import { useParams } from "next/navigation";
import { useState } from "react";
import { css } from "styled-components";
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
import { useTarotCardDeckDisplayContext } from "../hooks/useTarotCardDeckDisplayStore";
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
import ChipButton from "./ChipButton";
export default function AcceptRejectButtons() {
const { addMessage, deleteMessage, editMessage, state: messages } = useChatMessagesContext();

interface Props {
show: boolean;
}
export default function AcceptRejectButtons({ show }: Props) {
const { addMessage, deleteMessage, editMessage } = useChatMessagesContext();
const { mutate: sendChatMessage } = useSendChatMessage();
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const {
Expand All @@ -18,19 +24,18 @@ export default function AcceptRejectButtons() {
focus: focusTextField,
} = useTextFieldInChatDisplayContext();
const { show: showTarotCardDeck } = useTarotCardDeckDisplayContext();
const { hide: hideAcceptRejectButtons } = useAcceptRejectButtonDisplayContext();
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();
hideAcceptRejectButtons();
addMessage({
messageId: Math.random(),
type: "USER_NORMAL",
Expand Down Expand Up @@ -93,6 +98,7 @@ export default function AcceptRejectButtons() {
};

const handleRejectClick = async () => {
hideAcceptRejectButtons();
setIsButtonDisabled(true);
disableTextField();
addMessage({
Expand Down Expand Up @@ -149,10 +155,13 @@ export default function AcceptRejectButtons() {
setIsButtonDisabled(false);
};

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

return (
<div
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
css={css`
display: flex;
gap: 8px;
Expand All @@ -175,6 +184,6 @@ export default function AcceptRejectButtons() {
>
{rejectMessage}
</ChipButton>
</div>
</motion.div>
);
}
5 changes: 4 additions & 1 deletion src/chat/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ChatMessagesProvider } from "@/chat/hooks/useChatMessagesStore";
import { TextFieldInChatDisplayProvider } from "@/chat/hooks/useTextFieldInChatDisplayStore";
import { AcceptRejectButtonDisplayProvider } from "../hooks/useAcceptRejectButtonDisplayStore";
import { TarotCardDeckDisplayDisplayProvider } from "../hooks/useTarotCardDeckDisplayStore";
import ChatRoom from "./ChatRoom";

Expand All @@ -11,7 +12,9 @@ export default function Chat() {
<ChatMessagesProvider>
<TextFieldInChatDisplayProvider>
<TarotCardDeckDisplayDisplayProvider>
<ChatRoom />
<AcceptRejectButtonDisplayProvider>
<ChatRoom />
</AcceptRejectButtonDisplayProvider>
Comment on lines +15 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3) 준근님만의 Provider를 도입하는 기준이 궁금해요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기준이 있지는 않고 복잡한 상태를 좁은 화면에서 관리해야 하는 문제가 있어서 reducer와 context를 같이 사용했어

</TarotCardDeckDisplayDisplayProvider>
</TextFieldInChatDisplayProvider>
</ChatMessagesProvider>
Expand Down
34 changes: 28 additions & 6 deletions src/chat/components/ChatRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,50 @@ import { useEffect } from "react";
import { css } from "styled-components";
import { useStickToBottom } from "use-stick-to-bottom";
import { SendChatMessageRequest } from "../apis/sendChatMessage";
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
import { useSendChatMessage } from "../hooks/useSendChatMessage";
import { useTarotCardDeckDisplayContext } from "../hooks/useTarotCardDeckDisplayStore";
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
import ChatCardSelect from "./ChatCardSelect";
import ChatHeader from "./ChatHeader";

export default function ChatRoom() {
const { chatId } = useParams<{ chatId: string }>();
const searchParams = useSearchParams();
const initialMessage = searchParams.get("message");
const { data } = useChatMessages(Number(chatId));
const { scrollRef, contentRef } = useStickToBottom();
const { scrollRef, contentRef } = useStickToBottom({
initial: "instant",
resize: "instant",
});

const {
copyServerState,
state: messages,
addMessage,
editMessage,
deleteMessage,
} = useChatMessagesContext();
const { isVisible: isTarotCardDeckVisible } = useTarotCardDeckDisplayContext();
const { isVisible: isTarotCardDeckVisible, show: showTarotCardDeck } =
useTarotCardDeckDisplayContext();
const {
isVisible: isTextFieldVisible,
enable: enableTextField,
disable: disableTextField,
focus: focusTextField,
hide: hideTextField,
} = useTextFieldInChatDisplayContext();
const { mutate: sendChatMessage } = useSendChatMessage();
const pathname = usePathname();
const router = useRouter();
const { isVisible: isAcceptRejectButtonsVisible, show: showAcceptRejectButtons } =
useAcceptRejectButtonDisplayContext();

useEffect(() => {
if (!data) return;
copyServerState(data.messages);
if (!initialMessage) return;
console.log(initialMessage);

router.replace(pathname);
const message = JSON.parse(initialMessage) as SendChatMessageRequest;

Expand All @@ -72,7 +82,6 @@ export default function ChatRoom() {

sendChatMessage(JSON.parse(initialMessage), {
onSuccess: async (data) => {
console.log(data);
deleteMessage(loadingMessageId);

addMessage({
Expand Down Expand Up @@ -114,6 +123,20 @@ export default function ChatRoom() {
});
}, [data]);

if (
!isTarotCardDeckVisible &&
messages.length > 0 &&
messages[messages.length - 1].type === "SYSTEM_TAROT_QUESTION_ACCEPTANCE_REPLY"
) {
hideTextField();
showTarotCardDeck();
}

if (messages.length > 0 && messages[messages.length - 1].type === "SYSTEM_TAROT_QUESTION_REPLY") {
disableTextField();
showAcceptRejectButtons();
}

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

Expand Down Expand Up @@ -160,9 +183,8 @@ export default function ChatRoom() {
/>
);
})}
<AcceptRejectButtons show={isAcceptRejectButtonsVisible} />
</div>

<AcceptRejectButtons />
</div>
{isTarotCardDeckVisible && <ChatCardSelect />}
{isTextFieldVisible && (
Expand Down
33 changes: 30 additions & 3 deletions src/chat/components/TextFieldInChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { delay } from "@/shared/utils/delay";
import { useParams } from "next/navigation";
import { useState } from "react";
import { css } from "styled-components";
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
import TextareaAutoSize from "./TextareaAutoSize";

Expand All @@ -21,6 +22,16 @@ export default function TextFieldInChat() {
textareaRef,
focus: focusTextField,
} = useTextFieldInChatDisplayContext();
const { show: showAcceptRejectButtons } = useAcceptRejectButtonDisplayContext();
const [isComposing, setIsComposing] = useState(false);

const handleCompositionStart = () => {
setIsComposing(true);
};

const handleCompositionEnd = () => {
setIsComposing(false);
};

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
Expand All @@ -29,8 +40,7 @@ export default function TextFieldInChat() {
}
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const submit = async () => {
setMessage("");
disableTextField();

Expand Down Expand Up @@ -82,6 +92,7 @@ export default function TextFieldInChat() {

if (data.type === "SYSTEM_TAROT_QUESTION_REPLY") {
disableTextField();
showAcceptRejectButtons();
return;
}
},
Expand All @@ -102,8 +113,15 @@ export default function TextFieldInChat() {
}
);
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
submit();
};
const maxMessageLength = 300;

const isOnlyWhiteSpace = message.trim().length === 0;

return (
<form
onSubmit={handleSubmit}
Expand All @@ -121,10 +139,19 @@ export default function TextFieldInChat() {
maxRows={8}
maxLength={maxMessageLength}
textareaRef={textareaRef}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) return;
if (e.key === "Enter" && !isComposing) {
e.preventDefault();
submit();
}
}}
/>
<button
type="submit"
disabled={isTextFieldDisabled}
disabled={isTextFieldDisabled || isOnlyWhiteSpace}
css={css`
position: absolute;
right: 12px;
Expand Down
30 changes: 27 additions & 3 deletions src/chat/components/TextFieldInChatOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export default function TextFieldInChatOverview() {
const { mutate: createChatRoom } = useCreateChatRoom();
const router = useRouter();
const [isMessageSent, setIsMessageSent] = useState(false);
const [isComposing, setIsComposing] = useState(false);

const handleCompositionStart = () => {
setIsComposing(true);
};

const handleCompositionEnd = () => {
setIsComposing(false);
};

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
Expand All @@ -20,8 +29,7 @@ export default function TextFieldInChatOverview() {
}
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const submit = () => {
setMessage("");
setIsMessageSent(true);

Expand All @@ -37,9 +45,16 @@ export default function TextFieldInChatOverview() {
},
});
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
submit();
};
const maxMessageLength = 300;
const disabled = isMessageSent;

const isOnlyWhiteSpace = message.trim().length === 0;

return (
<form
onSubmit={handleSubmit}
Expand All @@ -51,6 +66,15 @@ export default function TextFieldInChatOverview() {
<TextareaAutoSize
value={message}
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) return;
if (e.key === "Enter" && !isComposing) {
e.preventDefault();
submit();
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
disabled={disabled}
placeholder="오늘의 운세는 어떨까?"
minRows={1}
Expand All @@ -60,7 +84,7 @@ export default function TextFieldInChatOverview() {
/>
<button
type="submit"
disabled={disabled}
disabled={disabled || isOnlyWhiteSpace}
css={css`
position: absolute;
right: 12px;
Expand Down
3 changes: 3 additions & 0 deletions src/chat/components/TextareaAutoSize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Props = {
maxLength: number;
value: string;
textareaRef?: RefObject<HTMLTextAreaElement>;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
} & TextareaAutosizeProps;

export default function TextareaAutoSize({
Expand All @@ -20,6 +21,7 @@ export default function TextareaAutoSize({
maxLength,
autoFocus,
textareaRef,
onKeyDown,
}: Props) {
const textareaMinHeight = 52;
const [isSingleLineTextarea, setIsSingleLineTextarea] = useState(true);
Expand All @@ -37,6 +39,7 @@ export default function TextareaAutoSize({
maxRows={8}
autoFocus={autoFocus}
ref={textareaRef}
onKeyDown={onKeyDown}
onHeightChange={(height) => {
const isSingleLine = height <= textareaMinHeight;
setIsSingleLineTextarea(isSingleLine);
Expand Down
30 changes: 30 additions & 0 deletions src/chat/hooks/useAcceptRejectButtonDisplayStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createContext, ReactNode, useContext, useState } from "react";

type AcceptRejectButtonDisplayContextType = {
isVisible: boolean;
show: () => void;
hide: () => void;
};

Comment on lines +3 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2) provider가 많아지면서 hide를 의미하는 다양한 provider가 생긴것 같아요 이렇게 되면 컴포넌트 내에서 불러올 때 hide의 이름을 재정의 해주어야하고, 그러면 여러 컴포넌트에서 각각이 다른 이름을 사용하게 되어서 혼란을 일으킬 수 있을 것 같아요!
이부분을 Context 의미에 맞는 이름으로 변경하면 좋을 것 같아요!

hide를 각각 다르게 정의하고 사용하는 부분 코드 예시

    hide: hideTextField,
    focus: focusTextField,
  } = useTextFieldInChatDisplayContext();
  const { hide: hideAcceptRejectButtons } 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로바이더들을 동일한 인터페이스로 맞춰 사용하기 편하게 만드려고 했는데 그런 단점도 있겠네 재훈이는 useQuery 커스텀 훅을 만드는 경우에도 아래 처럼 이름을 변경해서 반환하나?

const { customData } = useCustomQuery

const AcceptRejectButtonDisplayContext = createContext<AcceptRejectButtonDisplayContextType | null>(
null
);

export const useAcceptRejectButtonDisplayContext = () => {
const context = useContext(AcceptRejectButtonDisplayContext);
if (!context) throw new Error("AcceptRejectButtonDisplayContext not found");
return context;
};

export const AcceptRejectButtonDisplayProvider = ({ children }: { children: ReactNode }) => {
const [isVisible, setIsVisible] = useState(false);

const show = () => setIsVisible(true);
const hide = () => setIsVisible(false);

return (
<AcceptRejectButtonDisplayContext.Provider value={{ isVisible, show, hide }}>
{children}
</AcceptRejectButtonDisplayContext.Provider>
);
};