Skip to content

Commit 1b834ca

Browse files
authored
Merge pull request #49 from Nexters/feat/#48
[Feat/#48] 채팅 페이지 QA
2 parents 1397418 + b3f8f03 commit 1b834ca

7 files changed

+138
-20
lines changed

src/chat/components/AcceptRejectButtons.tsx

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { useChatMessagesContext } from "@/chat/hooks/useChatMessagesStore";
22
import { useSendChatMessage } from "@/chat/hooks/useSendChatMessage";
33
import { delay } from "@/shared/utils/delay";
4+
import { motion } from "framer-motion";
45
import { useParams } from "next/navigation";
56
import { useState } from "react";
67
import { css } from "styled-components";
8+
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
79
import { useTarotCardDeckDisplayContext } from "../hooks/useTarotCardDeckDisplayStore";
810
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
911
import ChipButton from "./ChipButton";
12+
1013
export default function AcceptRejectButtons() {
11-
const { addMessage, deleteMessage, editMessage, state: messages } = useChatMessagesContext();
14+
const { addMessage, deleteMessage, editMessage } = useChatMessagesContext();
1215
const { mutate: sendChatMessage } = useSendChatMessage();
1316
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
1417
const {
@@ -18,19 +21,19 @@ export default function AcceptRejectButtons() {
1821
focus: focusTextField,
1922
} = useTextFieldInChatDisplayContext();
2023
const { show: showTarotCardDeck } = useTarotCardDeckDisplayContext();
24+
const { isVisible: isAcceptRejectButtonsVisible, hide: hideAcceptRejectButtons } =
25+
useAcceptRejectButtonDisplayContext();
2126
const { chatId } = useParams<{ chatId: string }>();
2227

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

26-
const isSystemRepliedQuestion =
27-
messages[messages.length - 1]?.type === "SYSTEM_TAROT_QUESTION_REPLY";
28-
2931
if (!chatId) throw new Error("chatId가 Dynamic Route에서 전달 되어야 합니다.");
3032

3133
const handleAcceptClick = async () => {
3234
setIsButtonDisabled(true);
3335
hideTextField();
36+
hideAcceptRejectButtons();
3437
addMessage({
3538
messageId: Math.random(),
3639
type: "USER_NORMAL",
@@ -93,6 +96,7 @@ export default function AcceptRejectButtons() {
9396
};
9497

9598
const handleRejectClick = async () => {
99+
hideAcceptRejectButtons();
96100
setIsButtonDisabled(true);
97101
disableTextField();
98102
addMessage({
@@ -149,10 +153,13 @@ export default function AcceptRejectButtons() {
149153
setIsButtonDisabled(false);
150154
};
151155

152-
if (!isSystemRepliedQuestion) return null;
156+
if (!isAcceptRejectButtonsVisible) return null;
153157

154158
return (
155-
<div
159+
<motion.div
160+
initial={{ opacity: 0 }}
161+
animate={{ opacity: 1 }}
162+
transition={{ duration: 0.5 }}
156163
css={css`
157164
display: flex;
158165
gap: 8px;
@@ -175,6 +182,6 @@ export default function AcceptRejectButtons() {
175182
>
176183
{rejectMessage}
177184
</ChipButton>
178-
</div>
185+
</motion.div>
179186
);
180187
}

src/chat/components/Chat.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { ChatMessagesProvider } from "@/chat/hooks/useChatMessagesStore";
44
import { TextFieldInChatDisplayProvider } from "@/chat/hooks/useTextFieldInChatDisplayStore";
5+
import { AcceptRejectButtonDisplayProvider } from "../hooks/useAcceptRejectButtonDisplayStore";
56
import { TarotCardDeckDisplayDisplayProvider } from "../hooks/useTarotCardDeckDisplayStore";
67
import ChatRoom from "./ChatRoom";
78

@@ -11,7 +12,9 @@ export default function Chat() {
1112
<ChatMessagesProvider>
1213
<TextFieldInChatDisplayProvider>
1314
<TarotCardDeckDisplayDisplayProvider>
14-
<ChatRoom />
15+
<AcceptRejectButtonDisplayProvider>
16+
<ChatRoom />
17+
</AcceptRejectButtonDisplayProvider>
1518
</TarotCardDeckDisplayDisplayProvider>
1619
</TextFieldInChatDisplayProvider>
1720
</ChatMessagesProvider>

src/chat/components/ChatRoom.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,49 @@ import { useEffect } from "react";
1414
import { css } from "styled-components";
1515
import { useStickToBottom } from "use-stick-to-bottom";
1616
import { SendChatMessageRequest } from "../apis/sendChatMessage";
17+
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
1718
import { useSendChatMessage } from "../hooks/useSendChatMessage";
1819
import { useTarotCardDeckDisplayContext } from "../hooks/useTarotCardDeckDisplayStore";
1920
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
2021
import ChatCardSelect from "./ChatCardSelect";
2122
import ChatHeader from "./ChatHeader";
23+
2224
export default function ChatRoom() {
2325
const { chatId } = useParams<{ chatId: string }>();
2426
const searchParams = useSearchParams();
2527
const initialMessage = searchParams.get("message");
2628
const { data } = useChatMessages(Number(chatId));
27-
const { scrollRef, contentRef } = useStickToBottom();
29+
const { scrollRef, contentRef } = useStickToBottom({
30+
initial: "instant",
31+
resize: "instant",
32+
});
33+
2834
const {
2935
copyServerState,
3036
state: messages,
3137
addMessage,
3238
editMessage,
3339
deleteMessage,
3440
} = useChatMessagesContext();
35-
const { isVisible: isTarotCardDeckVisible } = useTarotCardDeckDisplayContext();
41+
const { isVisible: isTarotCardDeckVisible, show: showTarotCardDeck } =
42+
useTarotCardDeckDisplayContext();
3643
const {
3744
isVisible: isTextFieldVisible,
3845
enable: enableTextField,
3946
disable: disableTextField,
4047
focus: focusTextField,
48+
hide: hideTextField,
4149
} = useTextFieldInChatDisplayContext();
4250
const { mutate: sendChatMessage } = useSendChatMessage();
4351
const pathname = usePathname();
4452
const router = useRouter();
53+
const { show: showAcceptRejectButtons } = useAcceptRejectButtonDisplayContext();
4554

4655
useEffect(() => {
4756
if (!data) return;
4857
copyServerState(data.messages);
4958
if (!initialMessage) return;
50-
console.log(initialMessage);
59+
5160
router.replace(pathname);
5261
const message = JSON.parse(initialMessage) as SendChatMessageRequest;
5362

@@ -72,7 +81,6 @@ export default function ChatRoom() {
7281

7382
sendChatMessage(JSON.parse(initialMessage), {
7483
onSuccess: async (data) => {
75-
console.log(data);
7684
deleteMessage(loadingMessageId);
7785

7886
addMessage({
@@ -114,6 +122,20 @@ export default function ChatRoom() {
114122
});
115123
}, [data]);
116124

125+
if (
126+
!isTarotCardDeckVisible &&
127+
messages.length > 0 &&
128+
messages[messages.length - 1].type === "SYSTEM_TAROT_QUESTION_ACCEPTANCE_REPLY"
129+
) {
130+
hideTextField();
131+
showTarotCardDeck();
132+
}
133+
134+
if (messages.length > 0 && messages[messages.length - 1].type === "SYSTEM_TAROT_QUESTION_REPLY") {
135+
disableTextField();
136+
showAcceptRejectButtons();
137+
}
138+
117139
if (!chatId) throw new Error("chatId가 Dynamic Route에서 전달 되어야 합니다.");
118140
if (!data) return null;
119141

@@ -160,9 +182,8 @@ export default function ChatRoom() {
160182
/>
161183
);
162184
})}
185+
<AcceptRejectButtons />
163186
</div>
164-
165-
<AcceptRejectButtons />
166187
</div>
167188
{isTarotCardDeckVisible && <ChatCardSelect />}
168189
{isTextFieldVisible && (

src/chat/components/TextFieldInChat.tsx

+32-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { delay } from "@/shared/utils/delay";
66
import { useParams } from "next/navigation";
77
import { useState } from "react";
88
import { css } from "styled-components";
9+
import { useAcceptRejectButtonDisplayContext } from "../hooks/useAcceptRejectButtonDisplayStore";
910
import { useTextFieldInChatDisplayContext } from "../hooks/useTextFieldInChatDisplayStore";
1011
import TextareaAutoSize from "./TextareaAutoSize";
1112

@@ -21,6 +22,16 @@ export default function TextFieldInChat() {
2122
textareaRef,
2223
focus: focusTextField,
2324
} = useTextFieldInChatDisplayContext();
25+
const { show: showAcceptRejectButtons } = useAcceptRejectButtonDisplayContext();
26+
const [isComposing, setIsComposing] = useState(false);
27+
28+
const handleCompositionStart = () => {
29+
setIsComposing(true);
30+
};
31+
32+
const handleCompositionEnd = () => {
33+
setIsComposing(false);
34+
};
2435

2536
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
2637
const value = e.target.value;
@@ -29,8 +40,15 @@ export default function TextFieldInChat() {
2940
}
3041
};
3142

32-
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
33-
e.preventDefault();
43+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
44+
if (e.key === "Enter" && e.shiftKey) return;
45+
if (e.key === "Enter" && !isComposing) {
46+
e.preventDefault();
47+
submit();
48+
}
49+
};
50+
51+
const submit = async () => {
3452
setMessage("");
3553
disableTextField();
3654

@@ -82,6 +100,7 @@ export default function TextFieldInChat() {
82100

83101
if (data.type === "SYSTEM_TAROT_QUESTION_REPLY") {
84102
disableTextField();
103+
showAcceptRejectButtons();
85104
return;
86105
}
87106
},
@@ -102,8 +121,15 @@ export default function TextFieldInChat() {
102121
}
103122
);
104123
};
124+
125+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
126+
e.preventDefault();
127+
submit();
128+
};
105129
const maxMessageLength = 300;
106130

131+
const isOnlyWhiteSpace = message.trim().length === 0;
132+
107133
return (
108134
<form
109135
onSubmit={handleSubmit}
@@ -121,10 +147,13 @@ export default function TextFieldInChat() {
121147
maxRows={8}
122148
maxLength={maxMessageLength}
123149
textareaRef={textareaRef}
150+
onCompositionStart={handleCompositionStart}
151+
onCompositionEnd={handleCompositionEnd}
152+
onKeyDown={handleKeyDown}
124153
/>
125154
<button
126155
type="submit"
127-
disabled={isTextFieldDisabled}
156+
disabled={isTextFieldDisabled || isOnlyWhiteSpace}
128157
css={css`
129158
position: absolute;
130159
right: 12px;

src/chat/components/TextFieldInChatOverview.tsx

+28-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export default function TextFieldInChatOverview() {
1212
const { mutate: createChatRoom } = useCreateChatRoom();
1313
const router = useRouter();
1414
const [isMessageSent, setIsMessageSent] = useState(false);
15+
const [isComposing, setIsComposing] = useState(false);
16+
17+
const handleCompositionStart = () => {
18+
setIsComposing(true);
19+
};
20+
21+
const handleCompositionEnd = () => {
22+
setIsComposing(false);
23+
};
1524

1625
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
1726
const value = e.target.value;
@@ -20,8 +29,14 @@ export default function TextFieldInChatOverview() {
2029
}
2130
};
2231

23-
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
24-
e.preventDefault();
32+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
33+
if (e.key === "Enter" && e.shiftKey) return;
34+
if (e.key === "Enter" && !isComposing) {
35+
e.preventDefault();
36+
submit();
37+
}
38+
};
39+
const submit = () => {
2540
setMessage("");
2641
setIsMessageSent(true);
2742

@@ -37,9 +52,16 @@ export default function TextFieldInChatOverview() {
3752
},
3853
});
3954
};
55+
56+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
57+
e.preventDefault();
58+
submit();
59+
};
4060
const maxMessageLength = 300;
4161
const disabled = isMessageSent;
4262

63+
const isOnlyWhiteSpace = message.trim().length === 0;
64+
4365
return (
4466
<form
4567
onSubmit={handleSubmit}
@@ -51,6 +73,9 @@ export default function TextFieldInChatOverview() {
5173
<TextareaAutoSize
5274
value={message}
5375
onChange={handleChange}
76+
onKeyDown={handleKeyDown}
77+
onCompositionStart={handleCompositionStart}
78+
onCompositionEnd={handleCompositionEnd}
5479
disabled={disabled}
5580
placeholder="오늘의 운세는 어떨까?"
5681
minRows={1}
@@ -60,7 +85,7 @@ export default function TextFieldInChatOverview() {
6085
/>
6186
<button
6287
type="submit"
63-
disabled={disabled}
88+
disabled={disabled || isOnlyWhiteSpace}
6489
css={css`
6590
position: absolute;
6691
right: 12px;

src/chat/components/TextareaAutoSize.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Props = {
1010
maxLength: number;
1111
value: string;
1212
textareaRef?: RefObject<HTMLTextAreaElement>;
13+
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
1314
} & TextareaAutosizeProps;
1415

1516
export default function TextareaAutoSize({
@@ -20,6 +21,7 @@ export default function TextareaAutoSize({
2021
maxLength,
2122
autoFocus,
2223
textareaRef,
24+
onKeyDown,
2325
}: Props) {
2426
const textareaMinHeight = 52;
2527
const [isSingleLineTextarea, setIsSingleLineTextarea] = useState(true);
@@ -37,6 +39,7 @@ export default function TextareaAutoSize({
3739
maxRows={8}
3840
autoFocus={autoFocus}
3941
ref={textareaRef}
42+
onKeyDown={onKeyDown}
4043
onHeightChange={(height) => {
4144
const isSingleLine = height <= textareaMinHeight;
4245
setIsSingleLineTextarea(isSingleLine);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createContext, ReactNode, useContext, useState } from "react";
2+
3+
type AcceptRejectButtonDisplayContextType = {
4+
isVisible: boolean;
5+
show: () => void;
6+
hide: () => void;
7+
};
8+
9+
const AcceptRejectButtonDisplayContext = createContext<AcceptRejectButtonDisplayContextType | null>(
10+
null
11+
);
12+
13+
export const useAcceptRejectButtonDisplayContext = () => {
14+
const context = useContext(AcceptRejectButtonDisplayContext);
15+
if (!context) throw new Error("AcceptRejectButtonDisplayContext not found");
16+
return context;
17+
};
18+
19+
export const AcceptRejectButtonDisplayProvider = ({ children }: { children: ReactNode }) => {
20+
const [isVisible, setIsVisible] = useState(false);
21+
22+
const show = () => setIsVisible(true);
23+
const hide = () => setIsVisible(false);
24+
25+
return (
26+
<AcceptRejectButtonDisplayContext.Provider value={{ isVisible, show, hide }}>
27+
{children}
28+
</AcceptRejectButtonDisplayContext.Provider>
29+
);
30+
};

0 commit comments

Comments
 (0)