Skip to content

Commit

Permalink
chore: develop merge
Browse files Browse the repository at this point in the history
  • Loading branch information
hae-on committed Oct 13, 2023
2 parents 630c1d0 + 43cf12a commit fba407e
Show file tree
Hide file tree
Showing 27 changed files with 465 additions and 44 deletions.
4 changes: 4 additions & 0 deletions frontend/.storybook/preview-body.html
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@
d="M14.485 9.47a.75.75 0 0 0-1.063 1.06c1.164 1.168 1.133 3.279-.303 4.72l-4.847 4.866c-1.435 1.44-3.533 1.47-4.694.304c-1.164-1.168-1.132-3.28.303-4.72l2.424-2.433a.75.75 0 0 0-1.063-1.059l-2.424 2.433c-1.911 1.92-2.151 4.982-.303 6.838c1.85 1.858 4.907 1.615 6.82-.304l4.847-4.867c1.911-1.918 2.151-4.982.303-6.837Z"
></path>
</g>
<symbol id="plane" width="32" height="32" viewBox="0 0 256 256">
<path
d="M232 127.89a16 16 0 0 1-8.18 14L55.91 237.9A16.14 16.14 0 0 1 48 240a16 16 0 0 1-15.05-21.34l27.35-79.95a4 4 0 0 1 3.79-2.71H136a8 8 0 0 0 8-8.53a8.19 8.19 0 0 0-8.26-7.47H64.16a4 4 0 0 1-3.79-2.7l-27.44-80a16 16 0 0 1 22.92-19.23l168 95.89a16 16 0 0 1 8.15 13.93Z"
/>
</symbol>
</svg>
</div>
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/components/Common/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ interface InputProps extends ComponentPropsWithRef<'input'> {
* Input 컴포넌트의 너비값입니다.
*/
customWidth?: string;
/**
* Input 컴포넌트의 최소 너비값입니다.
*/
minWidth?: string;
/**
* Input value에 에러가 있는지 여부입니다.
*/
Expand All @@ -24,12 +28,12 @@ interface InputProps extends ComponentPropsWithRef<'input'> {

const Input = forwardRef(
(
{ customWidth = '300px', isError = false, rightIcon, errorMessage, ...props }: InputProps,
{ customWidth = '300px', minWidth, isError = false, rightIcon, errorMessage, ...props }: InputProps,
ref: ForwardedRef<HTMLInputElement>
) => {
return (
<>
<InputContainer customWidth={customWidth}>
<InputContainer customWidth={customWidth} minWidth={minWidth}>
<CustomInput ref={ref} isError={isError} {...props} />
{rightIcon && <IconWrapper>{rightIcon}</IconWrapper>}
</InputContainer>
Expand All @@ -43,11 +47,12 @@ Input.displayName = 'Input';

export default Input;

type InputContainerStyleProps = Pick<InputProps, 'customWidth'>;
type InputContainerStyleProps = Pick<InputProps, 'customWidth' | 'minWidth'>;
type CustomInputStyleProps = Pick<InputProps, 'isError'>;

const InputContainer = styled.div<InputContainerStyleProps>`
position: relative;
min-width: ${({ minWidth }) => minWidth ?? 0};
max-width: ${({ customWidth }) => customWidth};
text-align: center;
`;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Common/Svg/SvgIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const SVG_ICON_VARIANTS = [
'pencil',
'camera',
'link',
'plane',
] as const;
export type SvgIconVariant = (typeof SVG_ICON_VARIANTS)[number];

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/Common/Svg/SvgSprite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ const SvgSprite = () => {
<path d="M14.485 9.47a.75.75 0 0 0-1.063 1.06c1.164 1.168 1.133 3.279-.303 4.72l-4.847 4.866c-1.435 1.44-3.533 1.47-4.694.304c-1.164-1.168-1.132-3.28.303-4.72l2.424-2.433a.75.75 0 0 0-1.063-1.059l-2.424 2.433c-1.911 1.92-2.151 4.982-.303 6.838c1.85 1.858 4.907 1.615 6.82-.304l4.847-4.867c1.911-1.918 2.151-4.982.303-6.837Z" />
</g>
</symbol>
<symbol id="plane" viewBox="0 0 256 256">
<path d="M232 127.89a16 16 0 0 1-8.18 14L55.91 237.9A16.14 16.14 0 0 1 48 240a16 16 0 0 1-15.05-21.34l27.35-79.95a4 4 0 0 1 3.79-2.71H136a8 8 0 0 0 8-8.53a8.19 8.19 0 0 0-8.26-7.47H64.16a4 4 0 0 1-3.79-2.7l-27.44-80a16 16 0 0 1 22.92-19.23l168 95.89a16 16 0 0 1 8.15 13.93Z" />
</symbol>
</svg>
);
};
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Common/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ToastStyleProps = Pick<ToastProps, 'isError'> & { isAnimating?: boolean };

const ToastWrapper = styled.div<ToastStyleProps>`
position: relative;
width: 100%;
width: calc(100% - 20px);
height: 55px;
max-width: 560px;
border-radius: 10px;
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/Recipe/CommentForm/CommentForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';

import CommentForm from './CommentForm';

const meta: Meta<typeof CommentForm> = {
title: 'recipe/CommentForm',
component: CommentForm,
};

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

export const Default: Story = {};
101 changes: 101 additions & 0 deletions frontend/src/components/Recipe/CommentForm/CommentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Button, Spacing, Text, Textarea, useTheme } from '@fun-eat/design-system';
import type { ChangeEventHandler, FormEventHandler } from 'react';
import { useState } from 'react';
import styled from 'styled-components';

import { SvgIcon } from '@/components/Common';
import { useToastActionContext } from '@/hooks/context';
import { useRecipeCommentMutation } from '@/hooks/queries/recipe';

interface CommentFormProps {
recipeId: number;
}

const MAX_COMMENT_LENGTH = 200;

const CommentForm = ({ recipeId }: CommentFormProps) => {
const [commentValue, setCommentValue] = useState('');
const { mutate } = useRecipeCommentMutation(recipeId);

const theme = useTheme();
const { toast } = useToastActionContext();

const handleCommentInput: ChangeEventHandler<HTMLTextAreaElement> = (e) => {
setCommentValue(e.target.value);
};

const handleSubmitComment: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();

mutate(
{ comment: commentValue },
{
onSuccess: () => {
setCommentValue('');
toast.success('댓글이 등록되었습니다.');
},
onError: (error) => {
if (error instanceof Error) {
toast.error(error.message);
return;
}

toast.error('댓글을 등록하는데 오류가 발생했습니다.');
},
}
);
};

return (
<CommentFormContainer>
<Form onSubmit={handleSubmitComment}>
<CommentTextarea
placeholder="댓글을 입력하세요. (200자)"
value={commentValue}
onChange={handleCommentInput}
maxLength={MAX_COMMENT_LENGTH}
/>
<SubmitButton variant="transparent" disabled={commentValue.length === 0}>
<SvgIcon
variant="plane"
width={30}
height={30}
color={commentValue.length === 0 ? theme.colors.gray2 : theme.colors.gray4}
/>
</SubmitButton>
</Form>
<Spacing size={8} />
<Text size="xs" color={theme.textColors.info} align="right">
{commentValue.length}자 / {MAX_COMMENT_LENGTH}
</Text>
</CommentFormContainer>
);
};

export default CommentForm;

const CommentFormContainer = styled.div`
position: fixed;
bottom: 0;
width: calc(100% - 40px);
max-width: 540px;
padding: 16px 0;
background: ${({ theme }) => theme.backgroundColors.default};
`;

const Form = styled.form`
display: flex;
gap: 4px;
justify-content: space-around;
align-items: center;
`;

const CommentTextarea = styled(Textarea)`
height: 50px;
padding: 8px;
font-size: 1.4rem;
`;

const SubmitButton = styled(Button)`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
`;
18 changes: 18 additions & 0 deletions frontend/src/components/Recipe/CommentItem/CommentItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react';

import CommentItem from './CommentItem';

import comments from '@/mocks/data/comments.json';

const meta: Meta<typeof CommentItem> = {
title: 'recipe/CommentItem',
component: CommentItem,
args: {
recipeComment: comments.comments[0],
},
};

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

export const Default: Story = {};
50 changes: 50 additions & 0 deletions frontend/src/components/Recipe/CommentItem/CommentItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Divider, Spacing, Text, useTheme } from '@fun-eat/design-system';
import styled from 'styled-components';

import type { Comment } from '@/types/recipe';
import { getFormattedDate } from '@/utils/date';

interface CommentItemProps {
recipeComment: Comment;
}

const CommentItem = ({ recipeComment }: CommentItemProps) => {
const theme = useTheme();
const { author, comment, createdAt } = recipeComment;

return (
<>
<AuthorWrapper>
<AuthorProfileImage src={author.profileImage} alt={`${author.nickname}님의 프로필`} width={32} height={32} />
<div>
<Text size="xs" color={theme.textColors.info}>
{author.nickname}
</Text>
<Text size="xs" color={theme.textColors.info}>
{getFormattedDate(createdAt)}
</Text>
</div>
</AuthorWrapper>
<CommentContent size="sm">{comment}</CommentContent>
<Divider variant="disabled" />
<Spacing size={16} />
</>
);
};

export default CommentItem;

const AuthorWrapper = styled.div`
display: flex;
gap: 12px;
align-items: center;
`;

const AuthorProfileImage = styled.img`
border: 1px solid ${({ theme }) => theme.colors.primary};
border-radius: 50%;
`;

const CommentContent = styled(Text)`
margin: 16px 0;
`;
13 changes: 13 additions & 0 deletions frontend/src/components/Recipe/CommentList/CommentList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';

import CommentList from './CommentList';

const meta: Meta<typeof CommentList> = {
title: 'recipe/CommentList',
component: CommentList,
};

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

export const Default: Story = {};
35 changes: 35 additions & 0 deletions frontend/src/components/Recipe/CommentList/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Heading, Spacing } from '@fun-eat/design-system';
import { useRef } from 'react';

import CommentItem from '../CommentItem/CommentItem';

import { useIntersectionObserver } from '@/hooks/common';
import { useInfiniteRecipeCommentQuery } from '@/hooks/queries/recipe';

interface CommentListProps {
recipeId: number;
}

const CommentList = ({ recipeId }: CommentListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);

const { fetchNextPage, hasNextPage, data } = useInfiniteRecipeCommentQuery(Number(recipeId));
useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

const comments = data.pages.flatMap((page) => page.comments);

return (
<section>
<Heading as="h3" size="lg">
댓글 ({comments.length}개)
</Heading>
<Spacing size={12} />
{comments.map((comment) => (
<CommentItem key={comment.id} recipeComment={comment} />
))}
<div ref={scrollRef} style={{ height: '1px' }} aria-hidden />
</section>
);
};

export default CommentList;
3 changes: 3 additions & 0 deletions frontend/src/components/Recipe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ export { default as RecipeItem } from './RecipeItem/RecipeItem';
export { default as RecipeList } from './RecipeList/RecipeList';
export { default as RecipeRegisterForm } from './RecipeRegisterForm/RecipeRegisterForm';
export { default as RecipeFavorite } from './RecipeFavorite/RecipeFavorite';
export { default as CommentItem } from './CommentItem/CommentItem';
export { default as CommentForm } from './CommentForm/CommentForm';
export { default as CommentList } from './CommentList/CommentList';
8 changes: 7 additions & 1 deletion frontend/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ export const CATEGORY_TYPE = {

export const IMAGE_MAX_SIZE = 5 * 1024 * 1024;

export const ENVIRONMENT = window.location.href.includes('dev') ? 'dev' : 'prod';
export const ENVIRONMENT = window.location.href.includes('dev')
? 'dev'
: process.env.NODE_ENV === 'production'
? 'prod'
: 'local';

export const IMAGE_URL =
ENVIRONMENT === 'dev' ? process.env.S3_DEV_CLOUDFRONT_PATH : process.env.S3_PROD_CLOUDFRONT_PATH;

export const PRODUCT_PATH_LOCAL_STORAGE_KEY = `funeat-last-product-path-${ENVIRONMENT}`;
2 changes: 1 addition & 1 deletion frontend/src/contexts/ToastContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,6 @@ const ToastContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: calc(100% - 20px);
width: 100%;
transform: translate(0, -10px);
`;
1 change: 0 additions & 1 deletion frontend/src/hooks/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,3 @@ export { default as useTabMenu } from './useTabMenu';
export { default as useScrollRestoration } from './useScrollRestoration';
export { default as useToast } from './useToast';
export { default as useGA } from './useGA';

2 changes: 2 additions & 0 deletions frontend/src/hooks/queries/recipe/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { default as useRecipeDetailQuery } from './useRecipeDetailQuery';
export { default as useRecipeRegisterFormMutation } from './useRecipeRegisterFormMutation';
export { default as useRecipeFavoriteMutation } from './useRecipeFavoriteMutation';
export { default as useInfiniteRecipesQuery } from './useInfiniteRecipesQuery';
export { default as useInfiniteRecipeCommentQuery } from './useInfiniteRecipeCommentQuery';
export { default as useRecipeCommentMutation } from './useRecipeCommentMutation';
36 changes: 36 additions & 0 deletions frontend/src/hooks/queries/recipe/useInfiniteRecipeCommentQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useSuspendedInfiniteQuery } from '../useSuspendedInfiniteQuery';

import { recipeApi } from '@/apis';
import type { CommentResponse } from '@/types/response';

interface PageParam {
lastId: number;
totalElements: number | null;
}

const fetchRecipeComments = async (pageParam: PageParam, recipeId: number) => {
const { lastId, totalElements } = pageParam;
const response = await recipeApi.get({
params: `/${recipeId}/comments`,
queries: `?lastId=${lastId}&totalElements=${totalElements}`,
});
const data: CommentResponse = await response.json();
return data;
};

const useInfiniteRecipeCommentQuery = (recipeId: number) => {
return useSuspendedInfiniteQuery(
['recipeComment', recipeId],
({ pageParam = { lastId: 0, totalElements: null } }) => fetchRecipeComments(pageParam, recipeId),
{
getNextPageParam: (prevResponse: CommentResponse) => {
const lastId = prevResponse.comments[prevResponse.comments.length - 1].id;
const totalElements = prevResponse.totalElements;
const lastCursor = { lastId: lastId, totalElements: totalElements };
return prevResponse.hasNext ? lastCursor : undefined;
},
}
);
};

export default useInfiniteRecipeCommentQuery;
Loading

0 comments on commit fba407e

Please sign in to comment.