Skip to content
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

채팅을 위한 컴포넌트 구현 #164

Merged
merged 15 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Configuration } from 'webpack';
import type { StorybookConfig } from '@storybook/react-webpack5';
import path from 'path';
import { Configuration } from 'webpack';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
Expand Down Expand Up @@ -57,12 +57,14 @@ const config: StorybookConfig = {
if (config.module?.rules) {
config.module = config.module || {};
config.module.rules = config.module.rules || [];

const imageRule = config.module.rules.find((rule) =>
rule?.['test']?.test('.svg'),
);
if (imageRule) {
imageRule['exclude'] = /\.svg$/;
}

config.module.rules.push({
test: /\.svg$/i,
oneOf: [
Expand Down
Binary file added frontend/src/common/assets/default_profile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/src/common/assets/meatball.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/common/assets/plus.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/common/assets/x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
105 changes: 105 additions & 0 deletions frontend/src/components/ChatList/ChatList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { Meta, StoryObj } from '@storybook/react';

import ChatList from './ChatList';

const meta: Meta<typeof ChatList> = {
component: ChatList,
};

export default meta;
type Story = StoryObj<typeof ChatList>;

export const Default: Story = {
args: {
chats: [
{
sender: '상돌',
time: '14:00',
message: '안나야 공은 찰 줄 아냐',
isMyMessage: false,
},
{
sender: '안나',
time: '14:04',
message: '시비걸꺼면 나가라',
isMyMessage: true,
},
{
sender: '안나',
time: '14:04',
message: '지하돌아',
isMyMessage: true,
},
{
sender: '테바',
time: '14:06',
message: '여러분~ 싸우지 마세요',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
{
sender: '테바',
time: '14:07',
message:
'두 분 다 강퇴 당하고 싶지 않으시면 서로 말 예쁘게 해주시고 ~ 각자 괜찮은 시간이나 좀 남겨주세요!',
isMyMessage: false,
},
],
},

decorators: (Story) => {
return (
<div style={{ height: '200px' }}>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

부모의 height에 따라 컴포넌트의 사이즈가 변경되는 지 확인하기 위한 부분입니다.

<Story />
</div>
);
},
};
17 changes: 17 additions & 0 deletions frontend/src/components/ChatList/ChatList.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Theme, css } from '@emotion/react';

export const list = ({ theme }: { theme: Theme }) => css`
overflow-y: scroll;
display: flex;
flex-direction: column;
gap: 1rem;

height: 100%;
padding: 2rem;

background-color: ${theme.colorPalette.grey[100]};

&::-webkit-scrollbar {
display: none;
}
`;
23 changes: 23 additions & 0 deletions frontend/src/components/ChatList/ChatList.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

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

어떤 컴포넌트는 Chat이고 어떤 컴포넌트는 Chatting인데 두개를 나누는 기준은 하나의 메세지를 담는 컴포넌트면 Chat, 아니면 대화가 위주가 되면 Chatting이라고 두었습니다.

ChatList는 Chat이 여러 개 있어서 ChatList라고 했어요.

근데 이 컴포넌트가 대화가 위주가 된다는 관점도 있어서 Chatting 이라는 방안이 존재합니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ChatMessage, {
ChatMessageProps,
} from '@_components/ChatMessage/ChatMessage';

import { list } from './ChatList.style';
import { useTheme } from '@emotion/react';

interface ChatListProps {
chats: ChatMessageProps[];
}
Comment on lines +6 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.

ChatListProps의 chats에 ChatMessageProps[]가 들어가는게 좀 특이한데, 아직 서버와 chat 데이터 구조가 정해지지 않아서 임시로 정해둔걸까요?

그 데이터 구조를 type으로 선언해두고 chats: Chat[]로 interface를 선언하는게 자연스럽다 생각이 듭니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네 아직 서버에서 오는 값이 정해지지 않아서 이렇게 놔뒀어요

값이 정해진다면 업데이트하겠습니다


export default function ChatList(props: ChatListProps) {
const { chats } = props;
const theme = useTheme();

return (
<div css={list({ theme })}>
{chats.map((chat) => (
<ChatMessage key={chat.message + chat.time} {...chat} />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

추후 chatid로 변경할 예정

))}
</div>
);
}
20 changes: 20 additions & 0 deletions frontend/src/components/ChatMessage/ChatMessage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';

import ChatMessage from './ChatMessage';

const meta: Meta<typeof ChatMessage> = {
component: ChatMessage,
};

export default meta;
type Story = StoryObj<typeof ChatMessage>;

export const Default: Story = {
args: {
sender: '테바',
time: '12:12',
imageUrl: '',
message: '여러분~ 싸우지 마세요',
isMyMessage: false,
},
};
53 changes: 53 additions & 0 deletions frontend/src/components/ChatMessage/ChatMessage.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Theme, css } from '@emotion/react';

export const ChatMessageStyle = ({
isMyMessage,
}: {
isMyMessage: boolean;
}) => css`
display: flex;
flex-direction: ${isMyMessage ? 'row-reverse' : 'row'};
gap: 1rem;
`;

export const messageContainer = ({
isMyMessage,
}: {
isMyMessage: boolean;
}) => css`
display: flex;
flex-direction: column;
${isMyMessage && 'align-items: flex-end'}
`;

export const senderStyle = ({ theme }: { theme: Theme }) => css`
${theme.typography.Medium}
color:${theme.colorPalette.grey[900]};
`;

export const messageStyle = ({
theme,
isMyMessage,
}: {
theme: Theme;
isMyMessage: boolean;
}) => css`
${theme.typography.b4};
display: inline-block;

max-width: 25rem;
padding: 10px;

word-break: break-all;

background-color: ${isMyMessage
? theme.colorPalette.yellow[200]
: theme.colorPalette.orange[100]};
border-radius: ${isMyMessage ? '12px' : 0} ${isMyMessage ? 0 : '12px'} 12px
12px;
`;

export const timeStyle = ({ theme }: { theme: Theme }) => css`
${theme.typography.c3}
color:${theme.colorPalette.grey[400]};
`;
36 changes: 36 additions & 0 deletions frontend/src/components/ChatMessage/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
ChatMessageStyle,
messageContainer,
messageStyle,
senderStyle,
timeStyle,
} from './ChatMessage.style';

import UserPreview from '@_components/UserPreview/UserPreview';
import { formatHhmmToKoreanWithPrefix } from '@_utils/formatters';
import { useTheme } from '@emotion/react';

export interface ChatMessageProps {
sender: string;
message: string;
isMyMessage: boolean;
time: string;
imageUrl?: string;
}

export default function ChatMessage(props: ChatMessageProps) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 메시지를 하나만 구현해서 내가 작성한 글이면 오른쪽 정렬, 아니면 왼쪽정렬하게 구현했어요.

근데 컴포넌트를 내가 작성한 채팅, 상대방이 작성한 컴포넌트로 분리할 수도 있겠다는 생각도 드네요

이건 차차 흘러가는 부분에 따라 함께 고민해보아요

Copy link
Contributor

Choose a reason for hiding this comment

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

저는 이것도 괜찮다고 생각해요 ui가 다르면 분리하는게 맞을것 같은데 같으니 boolean으로 재사용할 수 있어서 좋은 것 같아요. 저도 고민해보겠습니다

Copy link
Contributor

Choose a reason for hiding this comment

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

저도 하나로 두는 것이 괜찮을 것 같아요~
방향과 색상에만 차이가 있고, 나머지는 모두 통일성이 있어야 하기 때문에 하나의 컴포넌트로 두면 더 관리하기 편할 것 같습니다. 만약 말풍선 UI가 바뀐다면 둘 다 동일하게 바뀔 것으로 자연스레 예측이 되니까요!

const { sender, message, isMyMessage, time, imageUrl } = props;
Copy link
Contributor

Choose a reason for hiding this comment

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

컴포넌트 이름으로 그냥 Chat은 어떨까요? 지금 ChatMessage를 사용하는 상위 컴포넌트가 ChatList인 만큼 컴포넌트의 관계를 더 예측하기 편할 것 같습니다 :)

const theme = useTheme();
return (
<div css={ChatMessageStyle({ isMyMessage })}>
<UserPreview imageUrl={imageUrl} />
<div css={messageContainer({ isMyMessage })}>
<span css={senderStyle({ theme })}>{sender}</span>
<div css={messageStyle({ theme, isMyMessage })}>{message}</div>
<span css={timeStyle({ theme })}>
{formatHhmmToKoreanWithPrefix(time)}
</span>
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions frontend/src/components/ChattingFooter/ChattingFooter.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';

import ChattingFooter from './ChattingFooter';

const meta: Meta<typeof ChattingFooter> = {
component: ChattingFooter,
};

export default meta;
type Story = StoryObj<typeof ChattingFooter>;

export const Default: Story = {
args: { onSubmit: (string: string) => alert(string) },
};
47 changes: 47 additions & 0 deletions frontend/src/components/ChattingFooter/ChattingFooter.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Theme, css } from '@emotion/react';

export const footer = ({ theme }: { theme: Theme }) => css`
display: flex;
align-items: center;
justify-content: space-around;

height: 7rem;

background-color: ${theme.colorPalette.white[100]};
box-shadow: 0 -10px 15px rgb(0 0 0 / 20%);
`;

export const menuButton = ({ theme }: { theme: Theme }) => css`
width: 4.5rem;
height: 4.5rem;

background-color: ${theme.colorPalette.orange[300]};
border: 0;
border-radius: 50%;
`;

export const messageForm = ({ theme }: { theme: Theme }) => css`
display: flex;
align-items: center;
justify-content: space-between;

width: 80%;
height: 70%;
padding: 1rem 2rem;

background: ${theme.colorPalette.grey[200]};
border-radius: 50px;
`;

export const messageInput = ({ theme }: { theme: Theme }) => css`
${theme.typography.s2}
width: 100%;
background: rgb(0 0 0 / 0%);
border: 0;
outline: none;
`;

export const sendingButton = css`
background: rgb(0 0 0 / 0%);
border: 0;
`;
Loading
Loading