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

[Feature] - 여행기 장소 이미지 업로드 중일 경우, spinner를 보여주도록 리팩터링 #332

Merged
merged 12 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
previewUrls: [],
previewImageStates: [],
fileInputRef: React.createRef(),
onImageChange: () => {},
onDeleteImage: () => {},
Expand All @@ -35,18 +35,21 @@ export const Default: Story = {
export const WithImages: Story = {
args: {
...Default.args,
previewUrls: [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg",
previewImageStates: [
{ url: "https://example.com/image1.jpg", isLoading: false },
{ url: "https://example.com/image2.jpg", isLoading: false },
{ url: "https://example.com/image3.jpg", isLoading: false },
],
},
};

export const WithManyImages: Story = {
export const WithLoadingImages: Story = {
args: {
...Default.args,
previewUrls: Array(7).fill("https://example.com/image.jpg"),
previewImageStates: Array.from({ length: 3 }, (_, index) => ({
url: `https://example.com/image${index}.jpg`,
isLoading: true,
})),
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import styled from "@emotion/styled";

import { PRIMITIVE_COLORS } from "@styles/tokens";

export const MultiImageUploadContainer = styled.div`
display: flex;
justify-content: flex-start;
Expand Down Expand Up @@ -91,14 +93,14 @@ export const MultiImageUploadDeleteButton = styled.button`
justify-content: center;
align-items: center;
position: absolute;
top: -1rem;
top: -0.6rem;
right: -1rem;
width: 2rem;
height: 2rem;
border: 1px solid ${(props) => props.theme.colors.border};
border-radius: 50%;

background-color: #fff;
background-color: ${PRIMITIVE_COLORS.white};

svg {
width: 0.8rem;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import React from "react";

import Spinner from "@components/common/Spinner/Spinner";

import { useDragScroll } from "@hooks/index";

import * as S from "./MultiImageUpload.styled";

const MAX_PICTURES_COUNT = 10;

interface MultiImageUploadProps extends React.ComponentPropsWithoutRef<"div"> {
previewUrls: string[];
previewImageStates: { url: string; isLoading: boolean }[];
fileInputRef: React.RefObject<HTMLInputElement>;
onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onDeleteImage: (index: number) => void;
onButtonClick: () => void;
}

const MultiImageUpload = ({
previewUrls,
previewImageStates,
fileInputRef,
onImageChange,
onDeleteImage,
Expand All @@ -24,7 +26,35 @@ const MultiImageUpload = ({
}: MultiImageUploadProps) => {
const { scrollRef, onMouseDown, onMouseUp, onMouseMove, isDragging } = useDragScroll();

const hasPictures = previewUrls.length !== 0;
const hasPictures = previewImageStates.length !== 0;

const renderImageOrSpinner = (imageState: { url: string; isLoading: boolean }, index: number) => (
<S.MultiImageUploadPictureWrapper key={index}>
<S.MultiImageUploadDeleteButton onClick={() => onDeleteImage(index)}>
<svg
Copy link
Contributor

Choose a reason for hiding this comment

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

IconButton,Icon 컴포넌트 없을 때 svg 크기 조절이 안되서 svg 이미지를 이렇게 하드코딩으로 넣어놨었는데요..! 😅
지금은 지니가 잘 만들어준 IconButton,Icon 컴포넌트가 있기때문에 IconButton 사용하면 코드가 훨 간결해 질 것 같습니다!
iconType="x-icon"으로 하시면 될거에요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

요거 안그래도 적용해보려고 시도해보았슴다,, ㅜㅜ 분명 storybook이나 다른 앱에선 아이콘 조절이 잘 되는데, 여기서는 이미지 크기가 깨지더라구여.. ㅜ

width="11"
height="11"
viewBox="0 0 11 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.1002 11L0 9.9L4.40079 5.5L0 1.1L1.1002 0L5.50098 4.4L9.90177 0L11.002 1.1L6.60118 5.5L11.002 9.9L9.90177 11L5.50098 6.6L1.1002 11Z"
fill="#616161"
/>
</svg>
</S.MultiImageUploadDeleteButton>
{imageState.isLoading ? (
<Spinner variants="circle" size={40} />
) : (
<S.MultiImageUploadPicture
src={imageState.url}
alt={`업로드된 이미지 ${index + 1}`}
draggable="false"
/>
)}
</S.MultiImageUploadPictureWrapper>
);
Comment on lines +31 to +57
Copy link
Contributor

Choose a reason for hiding this comment

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

기존에 has로 나눠지던 거 하나로 추상화한 거 좋네요!


return (
<S.MultiImageUploadContainer {...props}>
Expand All @@ -51,7 +81,7 @@ const MultiImageUpload = ({
</S.MultiImageUploadSVGWrapper>

<p>
{previewUrls.length} / {MAX_PICTURES_COUNT}
{previewImageStates.length} / {MAX_PICTURES_COUNT}
</p>
Comment on lines 83 to 85
Copy link
Contributor

Choose a reason for hiding this comment

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

사소한거긴한데 Text detailBold 컴포넌트 이용하면 좋을 거 같다는 생각이 듭니다! 제 tag merge 되고 바꾸면 더 좋을 거 같네요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

머지 되고 반영할 부분들 찾아서 반영하면 좋을거같슴다~

</S.MultiImageUploadPictureAddButton>
<S.MultiImageUploadHiddenInput
Expand All @@ -72,29 +102,7 @@ const MultiImageUpload = ({
onMouseLeave={onMouseUp}
$isDragging={isDragging}
>
{previewUrls.map((previewUrl, index) => (
<S.MultiImageUploadPictureWrapper key={previewUrl}>
<S.MultiImageUploadDeleteButton onClick={() => onDeleteImage(index)}>
<svg
width="11"
height="11"
viewBox="0 0 11 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.1002 11L0 9.9L4.40079 5.5L0 1.1L1.1002 0L5.50098 4.4L9.90177 0L11.002 1.1L6.60118 5.5L11.002 9.9L9.90177 11L5.50098 6.6L1.1002 11Z"
fill="#616161"
/>
</svg>
</S.MultiImageUploadDeleteButton>
<S.MultiImageUploadPicture
src={previewUrl}
alt={`업로드된 이미지 ${index + 1}`}
draggable="false"
/>
</S.MultiImageUploadPictureWrapper>
))}
{previewImageStates.map((imageState, index) => renderImageOrSpinner(imageState, index))}
</S.ImageScrollContainer>
</S.MultiImageUploadPictureContainer>
)}
Expand Down Expand Up @@ -138,7 +146,7 @@ const MultiImageUpload = ({
</S.MultiImageUploadSVGWrapper>

<p>
{previewUrls.length} / {MAX_PICTURES_COUNT}
{previewImageStates.length} / {MAX_PICTURES_COUNT}
</p>
Comment on lines 148 to 150
Copy link
Contributor

Choose a reason for hiding this comment

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

얘도요!

</S.MultiImageUploadPictureAddButton>
<S.MultiImageUploadHiddenInput
Expand Down
23 changes: 22 additions & 1 deletion frontend/src/components/common/Spinner/Spinner.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,32 @@ const meta = {
viewport: {
defaultViewport: "desktop",
},
docs: {
description: {
component:
"두 가지 variants(tturi, circle)가 있는 Spinner 컴포넌트이며, tturi의 경우 size를 70~100 정도로 사용해야합니다.",
Copy link
Contributor

Choose a reason for hiding this comment

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

세심한 description 🥹 👍

},
},
},
argTypes: {
variants: {
options: ["tturi", "circle"],
control: "select",
},
size: {
control: "range",
},
},
tags: ["autodocs"],
} satisfies Meta<typeof Spinner>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};
export const Default: Story = {
args: {
variants: "tturi",
size: 100,
},
};
42 changes: 32 additions & 10 deletions frontend/src/components/common/Spinner/Spinner.styled.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { keyframes } from "@emotion/react";
import { css, keyframes } from "@emotion/react";
import styled from "@emotion/styled";

import { SpinnerVariants } from "@components/common/Spinner/Spinner.type";

import theme from "@styles/theme";
import { PRIMITIVE_COLORS } from "@styles/tokens";

const rotate = keyframes`
from {
transform: rotate(0deg);
Expand All @@ -10,15 +15,32 @@ const rotate = keyframes`
}
`;

export const Wrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
const createSpinnerVariants = ($variants: SpinnerVariants, $size: number) => {
return {
tturi: css`
display: flex;
justify-content: center;
align-items: center;

svg {
width: 10rem;
height: 10rem;
svg {
width: ${$size / 10}rem;
height: ${$size / 10}rem;

animation: ${rotate} 0.5s linear infinite;
}
animation: ${rotate} 0.5s linear infinite;
}
`,
circle: css`
width: ${$size / 10}rem;
height: ${$size / 10}rem;
border: ${$size / 130}rem solid ${theme.colors.border};
border-top: ${$size / 130}rem solid ${PRIMITIVE_COLORS.blue[500]};
border-radius: 50%;

animation: ${rotate} 1s linear infinite;
`,
}[$variants];
};

export const LoadingSpinner = styled.div<{ $variants: SpinnerVariants; $size: number }>`
${({ $variants, $size }) => createSpinnerVariants($variants, $size)}
`;
15 changes: 11 additions & 4 deletions frontend/src/components/common/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { SpinnerVariants } from "@components/common/Spinner/Spinner.type";

import { Tturi } from "@assets/svg";

import * as S from "./Spinner.styled";

const Spinner = () => {
interface SpinnerProps {
variants?: SpinnerVariants;
size?: number;
}

const Spinner = ({ variants = "tturi", size = 100 }: SpinnerProps) => {
return (
<S.Wrapper>
<Tturi />
</S.Wrapper>
<S.LoadingSpinner $size={size} $variants={variants}>
{variants === "tturi" && <Tturi />}
</S.LoadingSpinner>
);
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/common/Spinner/Spinner.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type SpinnerVariants = "tturi" | "circle";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MAX_IMAGE_UPLOAD_COUNT = 10;
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { useRef } from "react";

import { css } from "@emotion/react";

import { MutateOptions } from "@tanstack/react-query";

import { MultiImageUpload } from "@components/common";
import { useTravelogueMultiImageUpload } from "@components/pages/travelogueRegister/TravelogueMultiImageUpload/hooks/useMultiImageUpload";

import { ERROR_MESSAGE_MAP } from "@constants/errorMessage";

const TravelogueMultiImageUpload = ({
dayIndex,
placeIndex,
imageUrls,
onRequestAddImage,
onChangeImageUrls,
onDeleteImageUrls,
}: {
export interface TravelogueMultiImageUploadProps {
imageUrls: string[];
dayIndex: number;
placeIndex: number;
Expand All @@ -25,32 +15,33 @@ const TravelogueMultiImageUpload = ({
) => Promise<string[]>;
onChangeImageUrls: (dayIndex: number, placeIndex: number, imgUrls: string[]) => void;
onDeleteImageUrls: (dayIndex: number, targetPlaceIndex: number, imageIndex: number) => void;
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
}

const handleButtonClick = () => {
fileInputRef.current?.click();
};
const TravelogueMultiImageUpload = ({
dayIndex,
placeIndex,
imageUrls,
onRequestAddImage,
onChangeImageUrls,
onDeleteImageUrls,
}: TravelogueMultiImageUploadProps) => {
const { imageStates, fileInputRef, handleChangeImage, handleClickButton, handleDeleteImage } =
useTravelogueMultiImageUpload({
imageUrls,
dayIndex,
placeIndex,
onRequestAddImage,
onChangeImageUrls,
onDeleteImageUrls,
});

return (
<MultiImageUpload
previewUrls={imageUrls}
previewImageStates={imageStates}
fileInputRef={fileInputRef}
onImageChange={async (e) => {
const files = Array.from(e.target.files as FileList);

if (imageUrls.length + files.length > 10) {
alert(ERROR_MESSAGE_MAP.imageUpload);
return;
}

const imgUrls = await onRequestAddImage(files);
onChangeImageUrls(dayIndex, placeIndex, imgUrls);
}}
onDeleteImage={(imageIndex) => {
onDeleteImageUrls(dayIndex, placeIndex, imageIndex);
}}
onButtonClick={handleButtonClick}
onImageChange={handleChangeImage}
onDeleteImage={handleDeleteImage}
onButtonClick={handleClickButton}
css={css`
margin-bottom: 1.6rem;
`}
Expand Down
Loading
Loading