Skip to content

Commit

Permalink
Merge pull request #27 from Nexters/feat/#21
Browse files Browse the repository at this point in the history
[Feat/#21] 채팅 입력창 컴포넌트 구현
  • Loading branch information
Jxxunnn authored Jan 28, 2025
2 parents 0c5bc59 + 04a34f5 commit 5d4d9be
Show file tree
Hide file tree
Showing 13 changed files with 918 additions and 1,123 deletions.
1,718 changes: 663 additions & 1,055 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@
"lint": "next lint"
},
"dependencies": {
"@next/third-parties": "^15.1.6",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-toast": "^1.2.5",
"@radix-ui/react-tooltip": "^1.1.7",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@next/third-parties": "^15.1.6",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-query-devtools": "^5.63.0",
"axios": "^1.7.9",
"next": "14.2.23",
"react": "^18",
"react-dom": "^18",
"react-textarea-autosize": "^8.5.7",
"styled-components": "^6.1.14",
"styled-reset": "^4.5.2",
"zod": "^3.24.1"
Expand Down
16 changes: 5 additions & 11 deletions src/auth/utils/createUserKeyCookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,14 @@
import { cookies } from "next/headers";

export const createUserKeyCookie = () => {
const userKey = crypto.randomUUID();
if (cookies().get("userKey")) {
return cookies().get("userKey")?.value;
const uuid = crypto.randomUUID();
if (cookies().get("guestId")) {
return cookies().get("guestId")?.value;
}
cookies().set("userKey", userKey, {
cookies().set("guestId", uuid, {
path: "/",
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 365,
/**
* TODO: 백엔드 서버와 도메인을 통합한 후 설정값 변경
*/
sameSite: "none",
});

return userKey;
return uuid;
};
50 changes: 34 additions & 16 deletions src/chat/components/ChatOverview/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,46 @@
'use client';
"use client";

import QuickQuestionPickerBox from '@/chat/components/QuickQuestionPickerBox';
import MainContent from '@/shared/components/MainContent';
import { css } from 'styled-components';
import QuickQuestionPickerBox from "@/chat/components/QuickQuestionPickerBox";
import FullscreenOverflowDivider from "@/shared/components/FullscreenOverflowDivider";
import MainContent from "@/shared/components/MainContent";
import { css } from "styled-components";
import ChatTextField from "../ChatTextField";

export default function ChatOverview() {
return (
<MainContent>
<h1
css={css`
margin-top: 170px;
text-align: center;
${(props) => props.theme.fonts.headline2}
`}
>
AI 타로 술사, 타로냥에게
<br /> 무엇이든 물어봐라냥
</h1>
<div
css={css`
margin-top: 32px;
flex: 1;
`}
>
<QuickQuestionPickerBox />
<h1
css={css`
margin-top: 170px;
text-align: center;
${(props) => props.theme.fonts.headline2}
`}
>
AI 타로 술사, 타로냥에게
<br /> 무엇이든 물어봐라냥
</h1>
<div
css={css`
margin-top: 32px;
`}
>
<QuickQuestionPickerBox />
</div>
</div>
<div>
<FullscreenOverflowDivider />
<div
css={css`
padding: 16px 20px;
`}
>
<ChatTextField />
</div>
</div>
</MainContent>
);
Expand Down
152 changes: 152 additions & 0 deletions src/chat/components/ChatTextField/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"use client";
import { createUserKeyCookie } from "@/auth/utils/createUserKeyCookie";
import { useCreateChatRoom } from "@/chat/hooks/useCreateChatRoom";
import { useSendChatMessage } from "@/chat/hooks/useSendChatMesasge";
import ArrowUpIcon from "@/shared/assets/icons/arrow-up-default.svg";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useRouter } from "next/navigation";
import { useState } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { css } from "styled-components";

export default function ChatTextField() {
const [message, setMessage] = useState("");
const [isSingleLineTextarea, setIsSingleLineTextarea] = useState(true);
const { mutate: createChatRoom, isPending: isCreatingChatRoom } = useCreateChatRoom();
const { mutate: sendChatMessage, isPending: isSendingChatMessage } = useSendChatMessage();
const router = useRouter();
const textareaMinHeight = 52;

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
if (value.length <= maxMessageLength) {
setMessage(value);
}
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setMessage("");
await createUserKeyCookie();
createChatRoom(undefined, {
onSuccess: (data) => {
sendChatMessage(
{
roomId: data.roomId,
message: message,
intent: "NORMAL",
},
{
onSuccess: () => {
router.push(`/chats/${data.roomId}`);
},
}
);
},
});
};
const maxMessageLength = 300;
const disabled = isCreatingChatRoom || isSendingChatMessage;

return (
<form
onSubmit={handleSubmit}
css={css`
position: relative;
border-radius: 8px;
`}
>
<Tooltip.Provider>
<Tooltip.Root open={message.length === maxMessageLength}>
<Tooltip.Trigger asChild>
<TextareaAutosize
value={message}
onChange={handleChange}
placeholder="오늘의 운세는 어떨까?"
minRows={1}
maxRows={8}
onHeightChange={(height) => {
const isSingleLine = height <= textareaMinHeight;
setIsSingleLineTextarea(isSingleLine);
}}
css={css`
border: none;
resize: none;
padding: 10px 56px 10px 12px;
width: 100%;
height: ${isSingleLineTextarea ? `${textareaMinHeight}px` : "auto"};
min-height: ${textareaMinHeight}px;
border-radius: 8px;
${({ theme }) => theme.fonts.body3}
line-height: ${isSingleLineTextarea ? "32px" : "24px"};
background-color: ${({ theme }) => theme.colors.grey00};
color: ${({ theme }) => theme.colors.grey90};
caret-color: ${({ theme }) => theme.colors.grey90};
&::placeholder {
color: ${({ theme }) => theme.colors.grey30};
}
&:focus-visible {
outline: none;
}
${disabled &&
css`
color: ${({ theme }) => theme.colors.grey40};
background-color: ${({ theme }) => theme.colors.grey10};
`}
`}
/>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content
css={css`
background-color: ${({ theme }) => theme.colors.grey90};
color: ${({ theme }) => theme.colors.white};
padding: 6px 8px;
border-radius: 8px;
${({ theme }) => theme.fonts.body1}
text-align: center;
`}
>
<Tooltip.Arrow
width={16}
height={10}
css={css`
fill: ${({ theme }) => theme.colors.grey90};
`}
/>
미안하지만 고양이가 알아들을 수 있는
<br />
글자 수는 300자까지야 냥
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
<button
type="submit"
disabled={disabled}
css={css`
position: absolute;
right: 12px;
bottom: 12px;
padding: 4px;
border-radius: 4px;
background-color: ${({ theme }) => theme.colors.primary03};
&:disabled {
background-color: ${({ theme }) => theme.colors.grey20};
pointer-events: none;
}
`}
aria-label="메세지 전송"
>
<ArrowUpIcon
width={24}
height={24}
css={css`
display: block;
color: ${({ theme }) => theme.colors.grey00};
`}
/>
</button>
</form>
);
}
24 changes: 12 additions & 12 deletions src/chat/components/QuickQuestionPickerBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { createUserKeyCookie } from '@/auth/utils/createUserKeyCookie';
import { useCreateChatRoom } from '@/chat/hooks/useCreateChatRoom';
import { useSendChatMessage } from '@/chat/hooks/useSendChatMesasge';
import { TarotQuestionRecommendListData } from '@/tarot/apis/getTarotQuestionRecommends';
import { useTarotQuestionRecommends } from '@/tarot/hooks/useTarotQuestionRecommends';
import { useRouter } from 'next/navigation';
import { css } from 'styled-components';
import QuickQuestionPicker from '../QuickQuestionPicker';
import RefreshQuickQuestionButton from '../RefreshQuickQuestionButton';
import { createUserKeyCookie } from "@/auth/utils/createUserKeyCookie";
import { useCreateChatRoom } from "@/chat/hooks/useCreateChatRoom";
import { useSendChatMessage } from "@/chat/hooks/useSendChatMesasge";
import { TarotQuestionRecommendListData } from "@/tarot/apis/getTarotQuestionRecommends";
import { useTarotQuestionRecommends } from "@/tarot/hooks/useTarotQuestionRecommends";
import { useRouter } from "next/navigation";
import { css } from "styled-components";
import QuickQuestionPicker from "../QuickQuestionPicker";
import RefreshQuickQuestionButton from "../RefreshQuickQuestionButton";

export default function QuickQuestionPickerBox() {
const { data } = useTarotQuestionRecommends();
const { mutate: createChatRoom } = useCreateChatRoom();
const { mutateAsync: sendChatMessage } = useSendChatMessage();
const { mutate: sendChatMessage } = useSendChatMessage();
const router = useRouter();

if (!data) return null;

const adaptQuestionRecommends = (data: TarotQuestionRecommendListData) => {
const colors = ['primary03', 'grey10', 'primary01', 'grey60'] as const;
const colors = ["primary03", "grey10", "primary01", "grey60"] as const;
return data.questions.map((question, i) => ({
...question,
color: colors[i],
Expand All @@ -29,7 +29,7 @@ export default function QuickQuestionPickerBox() {
{
roomId: data.roomId,
message: question.question,
intent: 'RECOMMEND_QUESTION',
intent: "RECOMMEND_QUESTION",
referenceQuestionId: question.recommendQuestionId,
},
{
Expand Down
13 changes: 8 additions & 5 deletions src/chat/components/RefreshQuickQuestionButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useQueryClient } from '@tanstack/react-query';
import { css } from 'styled-components';

import RotateIcon from "@/shared/assets/icons/rotate.svg";
import { useQueryClient } from "@tanstack/react-query";
import { css } from "styled-components";
export default function RefreshQuickQuestionButton() {
const queryClient = useQueryClient();

const handleClick = () => {
queryClient.invalidateQueries({ queryKey: ['tarotQuestionRecommends'] });
queryClient.invalidateQueries({ queryKey: ["tarotQuestionRecommends"] });
};

return (
Expand All @@ -17,6 +17,9 @@ export default function RefreshQuickQuestionButton() {
border: none;
background-color: transparent;
color: ${(props) => props.theme.colors.grey60};
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
`}
>
Expand All @@ -27,7 +30,7 @@ export default function RefreshQuickQuestionButton() {
>
추천 질문 변경
</span>
{/* TODO: 아이콘 추가 */}
<RotateIcon />
</button>
);
}
4 changes: 2 additions & 2 deletions src/shared/assets/icons/arrow-up-default.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/shared/assets/icons/rotate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/shared/components/FullscreenOverflowDivider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { css } from "styled-components";

export default function FullscreenOverflowDivider() {
return (
<hr
css={css`
margin: 0;
block-size: 1px;
border: none;
width: 100%;
background-color: ${(props) => props.theme.colors.grey10};
box-shadow: 0 0 0 100vmax ${(props) => props.theme.colors.grey10};
clip-path: inset(0px -100vmax);
`}
/>
);
}
Loading

0 comments on commit 5d4d9be

Please sign in to comment.