Skip to content

Commit 1397418

Browse files
authored
Merge pull request #46 from Nexters/feat/#43
[ Feat/#43 ] 카드 선택 시 툴팁 추가 & shadow 디자인 적용
2 parents c09f3a5 + 34cc538 commit 1397418

File tree

27 files changed

+246
-146
lines changed

27 files changed

+246
-146
lines changed

.prettierrc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"printWidth": 100,
3+
"tabWidth": 2,
4+
"trailingComma": "es5",
5+
"bracketSpacing": true,
6+
"semi": true,
7+
"useTabs": false,
8+
"endOfLine": "lf",
9+
"bracketSameLine": false,
10+
"arrowParens": "always"
11+
}

next.config.mjs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ const nextConfig = {
66

77
webpack(config) {
88
// Grab the existing rule that handles SVG imports
9-
const fileLoaderRule = config.module.rules.find((rule) =>
10-
rule.test?.test?.(".svg"),
11-
);
9+
const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.(".svg"));
1210

1311
config.module.rules.push(
1412
// Reapply the existing rule, but only for svg imports ending in ?url
@@ -23,7 +21,7 @@ const nextConfig = {
2321
issuer: fileLoaderRule.issuer,
2422
resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url
2523
use: ["@svgr/webpack"],
26-
},
24+
}
2725
);
2826
// Modify the file loader rule to ignore *.svg, since we have it handled now.
2927
fileLoaderRule.exclude = /\.svg$/i;

src/app/api/v1/chat/room/message/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import { SendChatMessageRequest } from "@/chat/apis/sendChatMessage";
23
import { NextRequest, NextResponse } from "next/server";
34

src/app/api/v1/tarot/question/recommends/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export async function GET() {
1111
},
1212
{
1313
recommendQuestionId: 2,
14-
question: "상반기에 취업할 수 있을까? 상반기에 취업할 수 있을까? 상반기에 취업할 수 있을까?",
14+
question:
15+
"상반기에 취업할 수 있을까? 상반기에 취업할 수 있을까? 상반기에 취업할 수 있을까?",
1516
referenceCount: 222,
1617
},
1718
{

src/app/chats/[chatId]/tarot-reading/[resultId]/page.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ export default function TarotReadingResultPage() {
1818
{!tarotInteractation && <ChatHeader />}
1919
<MainContent>
2020
{tarotInteractation || isLoading ? (
21-
<TarotInteraction
22-
setTarotInteractation={setTarotInteractation}
23-
tarotId={data?.tarot}
24-
/>
21+
<TarotInteraction setTarotInteractation={setTarotInteractation} tarotId={data?.tarot} />
2522
) : (
2623
<TarotResult />
2724
)}

src/app/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { Metadata } from "next";
1212
export const metadata: Metadata = {
1313
title: "타로냥 - 고양이 타로술사",
1414
description: "고양이 타로술사 타로냥이 당신의 질문에 답해주는 AI 타로 서비스입니다.",
15-
keywords: "AI 타로, 타로냥, 고양이 타로술사, 타로, 온라인 타로, 타로 서비스, 무료 타로, 운세, 타로 리딩",
15+
keywords:
16+
"AI 타로, 타로냥, 고양이 타로술사, 타로, 온라인 타로, 타로 서비스, 무료 타로, 운세, 타로 리딩",
1617
openGraph: {
1718
type: "website",
1819
url: "https://tarotmeow.vercel.app",

src/chat/components/AcceptRejectButtons.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export default function AcceptRejectButtons() {
2323
const rejectMessage = "아니, 얘기 더 들어봐";
2424
const acceptMessage = "좋아! 타로 볼래";
2525

26-
const isSystemRepliedQuestion = messages[messages.length - 1]?.type === "SYSTEM_TAROT_QUESTION_REPLY";
26+
const isSystemRepliedQuestion =
27+
messages[messages.length - 1]?.type === "SYSTEM_TAROT_QUESTION_REPLY";
2728

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

@@ -158,10 +159,20 @@ export default function AcceptRejectButtons() {
158159
margin-top: 76px;
159160
`}
160161
>
161-
<ChipButton type="button" disabled={isButtonDisabled} color="primary02" onClick={handleAcceptClick}>
162+
<ChipButton
163+
type="button"
164+
disabled={isButtonDisabled}
165+
color="primary02"
166+
onClick={handleAcceptClick}
167+
>
162168
{acceptMessage}
163169
</ChipButton>
164-
<ChipButton type="button" disabled={isButtonDisabled} color="grey30" onClick={handleRejectClick}>
170+
<ChipButton
171+
type="button"
172+
disabled={isButtonDisabled}
173+
color="grey30"
174+
onClick={handleRejectClick}
175+
>
165176
{rejectMessage}
166177
</ChipButton>
167178
</div>

src/chat/components/Card.tsx

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
"use client";
22

3+
import Image from "next/image";
4+
import styled, { css } from "styled-components";
35
import CardBack from "@/shared/assets/images/cardBack.webp";
46
import { cubicBezier, easeOut } from "motion";
5-
import { div } from "motion/react-client";
6-
import Image from "next/image";
7-
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
8-
import styled from "styled-components";
7+
import { motion } from "framer-motion";
8+
import { useState } from "react";
9+
import { Dispatch, SetStateAction, useRef, useEffect } from "react";
910
import { CardPickState } from "../types/CardPickState";
1011
import { DeckState } from "../types/DeckState";
12+
import * as Tooltip from "@radix-ui/react-tooltip";
13+
1114
interface PropTypes {
1215
idx: number;
1316
deckState: DeckState;
@@ -61,6 +64,9 @@ const Card = ({ idx, deckState, setDeckState, onClick, cardPickState }: PropType
6164
const onAnimationEnd = () => {
6265
setIsCardShadow(true);
6366
setDeckState("Spread");
67+
68+
if (cardPickState[idx] === "Pick") {
69+
}
6470
};
6571

6672
const handleClickCard = () => {
@@ -84,32 +90,90 @@ const Card = ({ idx, deckState, setDeckState, onClick, cardPickState }: PropType
8490
}, []);
8591

8692
return (
87-
<CardAnimationWrapper
88-
ref={cardRef}
89-
variants={cardVariants}
90-
animate={getCardAnimation()}
91-
onClick={handleClickCard}
92-
onAnimationComplete={onAnimationEnd}
93-
>
94-
<CardWrapper src={CardBack} alt="카드 뒷면 이미지" isCardShadow={isCardShadow} />
95-
</CardAnimationWrapper>
93+
<Tooltip.Provider>
94+
<Tooltip.Root open={cardPickState[idx] === "Pick"}>
95+
<CardAnimationWrapper
96+
ref={cardRef}
97+
variants={cardVariants}
98+
animate={getCardAnimation()}
99+
onClick={handleClickCard}
100+
onAnimationComplete={onAnimationEnd}
101+
>
102+
<Tooltip.Trigger asChild>
103+
<CardWrapper
104+
src={CardBack}
105+
alt="카드 뒷면 이미지"
106+
isCardShadow={isCardShadow}
107+
cardPickState={cardPickState[idx]}
108+
/>
109+
</Tooltip.Trigger>
110+
111+
<Tooltip.Content
112+
css={css`
113+
background-color: ${({ theme }) => theme.colors.grey90};
114+
color: ${({ theme }) => theme.colors.white};
115+
padding: 6px 8px;
116+
border-radius: 8px;
117+
${({ theme }) => theme.fonts.body1}
118+
text-align: center;
119+
cursor: pointer;
120+
transform: rotate(-16deg);
121+
border: 1px solid #24292f;
122+
& > span {
123+
left: 107.5px !important;
124+
}
125+
`}
126+
>
127+
<Tooltip.Arrow
128+
width={16}
129+
height={10}
130+
css={css`
131+
fill: ${({ theme }) => theme.colors.grey90};
132+
`}
133+
/>
134+
이 카드를 뽑으려면 한 번 더 터치해줘냥
135+
</Tooltip.Content>
136+
</CardAnimationWrapper>
137+
</Tooltip.Root>
138+
</Tooltip.Provider>
96139
);
97140
};
98141

99142
export default Card;
100143

101-
const CardAnimationWrapper = styled(div)`
144+
const CardAnimationWrapper = styled(motion.div)`
102145
width: 100px;
103146
height: 160px;
104147
position: absolute;
105148
106149
cursor: pointer;
150+
151+
& > [data-radix-popper-content-wrapper] {
152+
transform: translate(-89.5px, -54px) !important;
153+
154+
opacity: 0;
155+
animation: fadeIn 0.5s ease-out 0.5s forwards;
156+
}
157+
158+
@keyframes fadeIn {
159+
from {
160+
opacity: 0;
161+
}
162+
to {
163+
opacity: 1;
164+
}
165+
}
107166
`;
108167

109-
const CardWrapper = styled(Image)<{ isCardShadow: boolean }>`
168+
const CardWrapper = styled(Image)<{ isCardShadow: boolean; cardPickState: CardPickState }>`
110169
border-radius: 8px;
111170
112-
box-shadow: ${({ isCardShadow }) => (isCardShadow ? "-8px 0px 12px 0px rgba(0, 0, 0, 0.15)" : "")};
171+
box-shadow: ${({ isCardShadow, cardPickState }) =>
172+
!isCardShadow
173+
? ""
174+
: cardPickState === "Pick"
175+
? "0px 4px 20px 0px rgba(255, 247, 171, 0.40)"
176+
: "-8px 0px 12px 0px rgba(0, 0, 0, 0.15)"};
113177
114178
width: 100px;
115179
height: 160px;

src/chat/components/ChatAvatar.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import ChatBotProfileImage from "@/shared/assets/images/chatbot-profile.webp";
22
import Image from "next/image";
33
import { css } from "styled-components";
44
export default function ChatAvatar() {
5-
65
return (
76
<Image
87
src={ChatBotProfileImage}

src/chat/components/ChatBubbleGroup.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ export default function ChatBubbleGroup({ message }: Props) {
1414
}
1515

1616
const addIdToMessages = (messages: string[]) => {
17-
return messages.map((answer) => ({ messageId: Math.random(), sender: "SYSTEM", message: answer }));
17+
return messages.map((answer) => ({
18+
messageId: Math.random(),
19+
sender: "SYSTEM",
20+
message: answer,
21+
}));
1822
};
1923
return addIdToMessages(message.answers).map((answer) => {
2024
return <ChatBubble key={answer.messageId} sender={"SYSTEM"} message={answer.message} />;

src/chat/components/ChatCardSelect.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ const ChatCardSelect = () => {
2020
const { chatId } = useParams<{ chatId: string }>();
2121
const { mutate: selectTarotCard } = useSelectTarotCard();
2222
const ITEMS_PER_LOAD = 15;
23-
const [items, setItems] = useState<CardPickState[]>(Array.from({ length: ITEMS_PER_LOAD }, () => "Default"));
23+
const [items, setItems] = useState<CardPickState[]>(
24+
Array.from({ length: ITEMS_PER_LOAD }, () => "Default")
25+
);
2426
const router = useRouter();
2527
const [isCardPicked, setIsCardPicked] = useState(false);
2628

@@ -59,7 +61,10 @@ const ChatCardSelect = () => {
5961
(entries) => {
6062
entries.forEach((entry) => {
6163
if (entry.isIntersecting) {
62-
setItems((prev) => [...prev, ...Array.from({ length: ITEMS_PER_LOAD }, () => "Default" as CardPickState)]);
64+
setItems((prev) => [
65+
...prev,
66+
...Array.from({ length: ITEMS_PER_LOAD }, () => "Default" as CardPickState),
67+
]);
6368
}
6469
});
6570
},
@@ -77,7 +82,11 @@ const ChatCardSelect = () => {
7782

7883
return (
7984
<>
80-
<CardDeckWrapper initial={{ opacity: 0, y: 200 }} animate={{ opacity: 1, y: 0 }} transition={riseUpCardDeck}>
85+
<CardDeckWrapper
86+
initial={{ opacity: 0, y: 200 }}
87+
animate={{ opacity: 1, y: 0 }}
88+
transition={riseUpCardDeck}
89+
>
8190
{items.map((_, idx) => (
8291
<Card
8392
key={idx}
@@ -110,12 +119,13 @@ const InfinteScrollTrigger = styled.div<{ pos: number }>`
110119
const CardDeckWrapper = styled(motion.div)`
111120
display: flex;
112121
justify-content: center;
113-
align-items: center;
114-
122+
align-items: end;
123+
padding-bottom: 44px;
115124
width: 100%;
116-
height: 400px;
125+
height: 310px;
117126
position: relative;
118127
overflow-x: scroll;
128+
overflow-y: visible;
119129
120130
-ms-overflow-style: none; /* IE, Edge */
121131
scrollbar-width: none; /* Firefox */

src/chat/components/ChatHeader.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,9 @@ export default function ChatHeader() {
108108
`}
109109
>
110110
<Toast.Provider>
111-
<Toast.Root
112-
open={toastOpen}
113-
onOpenChange={setToastOpen}
114-
duration={3000}
115-
>
111+
<Toast.Root open={toastOpen} onOpenChange={setToastOpen} duration={3000}>
116112
<Toast.Title>
117-
링크 복사 완료! 타로냥을 알리고 싶은 친구에게 링크를 전송해
118-
주세요.
113+
링크 복사 완료! 타로냥을 알리고 싶은 친구에게 링크를 전송해 주세요.
119114
</Toast.Title>
120115
</Toast.Root>
121116
<Toast.Viewport />

src/chat/components/ChatRoom.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ export default function ChatRoom() {
2525
const initialMessage = searchParams.get("message");
2626
const { data } = useChatMessages(Number(chatId));
2727
const { scrollRef, contentRef } = useStickToBottom();
28-
const { copyServerState, state: messages, addMessage, editMessage, deleteMessage } = useChatMessagesContext();
28+
const {
29+
copyServerState,
30+
state: messages,
31+
addMessage,
32+
editMessage,
33+
deleteMessage,
34+
} = useChatMessagesContext();
2935
const { isVisible: isTarotCardDeckVisible } = useTarotCardDeckDisplayContext();
3036
const {
3137
isVisible: isTextFieldVisible,
@@ -146,7 +152,13 @@ export default function ChatRoom() {
146152
if (message.sender === "SYSTEM") {
147153
return <ChatBubbleGroup key={message.messageId} message={message} />;
148154
}
149-
return <ChatBubble key={message.messageId} sender={message.sender} message={message.answers[0]} />;
155+
return (
156+
<ChatBubble
157+
key={message.messageId}
158+
sender={message.sender}
159+
message={message.answers[0]}
160+
/>
161+
);
150162
})}
151163
</div>
152164

src/chat/hooks/useChatMessagesStore.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ const chatMessagesReducer = (state: MessageType[], action: Action) => {
4040
case actionTypes.DELETE_MESSAGE:
4141
return state.filter((message) => message.messageId !== action.payload);
4242
case actionTypes.EDIT_MESSAGE:
43-
return state.map((message) => (message.messageId === action.payload.messageId ? action.payload : message));
43+
return state.map((message) =>
44+
message.messageId === action.payload.messageId ? action.payload : message
45+
);
4446
default:
4547
return state;
4648
}
@@ -84,7 +86,9 @@ export const ChatMessagesProvider = ({ children }: { children: React.ReactNode }
8486
}, []);
8587

8688
return (
87-
<ChatMessagesContext.Provider value={{ state, addMessage, copyServerState, deleteMessage, editMessage }}>
89+
<ChatMessagesContext.Provider
90+
value={{ state, addMessage, copyServerState, deleteMessage, editMessage }}
91+
>
8892
{children}
8993
</ChatMessagesContext.Provider>
9094
);

src/chat/hooks/useTarotCardDeckDisplayStore.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ const TarotCardDeckDisplayContext = createContext<TarotCardDeckDisplayContextTyp
1111
export const useTarotCardDeckDisplayContext = () => {
1212
const context = useContext(TarotCardDeckDisplayContext);
1313
if (!context) {
14-
throw new Error("useTarotCardDeckDisplayContext는 TarotCardDeckDisplayProvider 내부에서 사용해야 합니다.");
14+
throw new Error(
15+
"useTarotCardDeckDisplayContext는 TarotCardDeckDisplayProvider 내부에서 사용해야 합니다."
16+
);
1517
}
1618
return context;
1719
};

0 commit comments

Comments
 (0)