Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
[FE] feat: Notion으로 부터 업로드 할 수 있는 기능 구현 (#166)
Browse files Browse the repository at this point in the history
* feat: 노션 글 업로드 API 추가

* feat: `Modal` 컴포넌트 구현

* feat: `useModal` 훅 구현

* refactor: 아이콘 이름 수정

* test: `Modal` 스토리 작성

* test: `FileUploader` 스토리 작성

* feat: `FileUploader` 컴포넌트 구현

* refactor: `useFileUpload` 훅 수정

* feat: `useFileDragAndDrop` 훅 구현

* feat: Add post 버튼 클릭 시 모달이 띄워지도록 수정

* feat: 가져오기 아이콘 추가

* feat: `FileUploadModal` 구현

* refactor: 헤더 크기 시안에 맞게 수정

* feat: 파일 업로드 시 카테고리 아이디 추가

* refactor: 모달 backdrop 상수화

* refactor: 불필요한 함수 인자 제거 및 css inset 수정

* design: `Input` 컴포넌트 placeholder 색상 변경

* refactor: `Input` 컴포넌트로 기존의 노션 인풋 교체

* refactor: 테스트를 위한 불필요한 버튼 제거

* fix: 카테고리 API에 headers content type 추가

* fix: 글 업로드 시 categoryId 넘기는 방식 수정
  • Loading branch information
nangkyeonglim authored Aug 3, 2023
1 parent 3971a56 commit a9b29a4
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 42 deletions.
10 changes: 9 additions & 1 deletion frontend/src/apis/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { AddCategoriesRequest, PatchCategoryArgs } from 'types/apis/category';

// POST: 카테고리 추가
export const addCategory = (body: AddCategoriesRequest) =>
http.post(categoryURL, { body: JSON.stringify(body) });
http.post(categoryURL, {
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});

// GET: 카테고리 목록 조회
export const getCategories = () => http.get(categoryURL);
Expand All @@ -17,6 +22,9 @@ export const getWritingsInCategory = (categoryId: number | null) =>
export const patchCategory = ({ categoryId, body }: PatchCategoryArgs) =>
http.patch(`${categoryURL}/${categoryId}`, {
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});

// DELETE: 카테고리 삭제
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/apis/writings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { writingURL } from 'constants/apis/url';
import { http } from './fetch';
import type { AddWritingRequest, PublishWritingArgs } from 'types/apis/writings';
import type {
AddNotionWritingRequest,
AddWritingRequest,
PublishWritingArgs,
} from 'types/apis/writings';

// 글 생성(글 업로드): POST
export const addWriting = (body: AddWritingRequest) =>
Expand All @@ -11,6 +15,15 @@ export const addWriting = (body: AddWritingRequest) =>
// },
});

// 노션 - 글 생성(글 업로드): POST
export const addNotionWriting = (body: AddNotionWritingRequest) =>
http.post(`${writingURL}/notion`, {
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json',
},
});

// 글 조회: GET
export const getWriting = (writingId: number) => http.get(`${writingURL}/${writingId}`);

Expand Down
9 changes: 9 additions & 0 deletions frontend/src/assets/icons/import.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: 2 additions & 1 deletion frontend/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export { ReactComponent as TagIcon } from './tag.svg';
export { ReactComponent as LeftArrowHeadIcon } from './left-arrow-head.svg';
export { ReactComponent as TistoryLogoIcon } from './tistory-logo.svg';
export { ReactComponent as MediumLogoIcon } from './medium-logo.svg';
export { ReactComponent as CloseRounded } from './close-rounded.svg';
export { ReactComponent as CloseIcon } from './close-rounded.svg';
export { ReactComponent as SidebarLeftIcon } from './sidebar-left.svg';
export { ReactComponent as SidebarRightIcon } from './sidebar-right.svg';
export { ReactComponent as SettingIcon } from './setting.svg';
Expand All @@ -18,3 +18,4 @@ export { ReactComponent as ArrowRightIcon } from './arrow-right.svg';
export { ReactComponent as DeleteIcon } from './delete.svg';
export { ReactComponent as PencilIcon } from './pencil.svg';
export { ReactComponent as WritingIcon } from './writing.svg';
export { ReactComponent as ImportIcon } from './import.svg';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from '@storybook/react';
import FileUploader from './FileUploader';

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

export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {
render: () => {
const onFileSelect = (file: FormData | null) => {
if (file) alert('파일이 선택되었습니다!');
};
return <FileUploader onFileSelect={onFileSelect} />;
},
};
77 changes: 77 additions & 0 deletions frontend/src/components/@common/FileUploader/FileUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { InputHTMLAttributes, useEffect } from 'react';
import { css, styled } from 'styled-components';
import { useFileUpload } from 'hooks/useFileUpload';
import { useFileDragAndDrop } from 'hooks/@common/useFileDragAndDrop';
import { ImportIcon } from 'assets/icons';

type Props = {
accept?: InputHTMLAttributes<HTMLInputElement>['accept'];
width?: string;
height?: string;
onFileSelect: (file: FormData | null) => void;
};

const FileUploader = ({ accept = '*', width = '30rem', height = '10rem', onFileSelect }: Props) => {
const { onFileChange, openFinder, selectedFile } = useFileUpload(accept);
const { dragRef, isDragging } = useFileDragAndDrop({ onFileChange });

useEffect(() => {
onFileSelect(selectedFile);
}, [selectedFile]);

return (
<button ref={dragRef} onClick={openFinder}>
<S.Description $isDragging={isDragging} $width={width} $height={height}>
<ImportIcon />
드래그하거나 클릭해서 업로드
</S.Description>
</button>
);
};

export default FileUploader;

const S = {
Description: styled.div<{ $isDragging: boolean; $width: string; $height: string }>`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
${({ $width, $height }) => {
return css`
width: ${$width};
height: ${$height};
`;
}};
border: 2px dashed ${({ theme }) => theme.color.gray6};
background-color: ${({ theme }) => theme.color.gray4};
font-size: 1.3rem;
color: ${({ theme }) => theme.color.gray7};
transition: all 0.2s ease-in-out;
${({ $isDragging, theme }) => {
return (
$isDragging &&
css`
border: 2px dashed ${theme.color.primary};
background-color: ${theme.color.gray5};
`
);
}}
&:hover {
background-color: ${({ theme }) => theme.color.gray5};
}
`,
SpinnerWrapper: styled.div<{ $width: string; $height: string }>`
${({ $width, $height }) => {
return css`
width: ${$width};
height: ${$height};
`;
}};
display: flex;
justify-content: center;
`,
};
4 changes: 4 additions & 0 deletions frontend/src/components/@common/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ const S = {
${({ $size }) => genSizeStyle($size)};
${({ $variant, $isError }) => genVariantStyle($variant, $isError)};
&::placeholder {
color: ${({ theme }) => theme.color.gray6};
}
`,
SupportingText: styled.p<{ $isError: boolean | undefined }>`
color: ${({ $isError, theme }) => ($isError ? theme.color.red6 : theme.color.gray7)};
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/components/@common/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Meta, StoryObj } from '@storybook/react';
import Modal from './Modal';
import Button from 'components/@common/Button/Button';
import { useModal } from 'hooks/@common/useModal';
import { styled } from 'styled-components';

const meta = {
title: 'common/Modal',
component: Modal,
argTypes: {
children: {
control: false,
},
isOpen: {
control: false,
},
},
args: {
isOpen: false,
},
} satisfies Meta<typeof Modal>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {
render: ({ ...rest }) => {
const { isOpen, openModal, closeModal } = useModal();
return (
<>
<Button variant='secondary' onClick={openModal}>
Open Modal
</Button>
<Modal {...rest} isOpen={isOpen} closeModal={closeModal}>
<ModalContent>
<h1>모달</h1>
<p>내용을 마음껏 써주세요.</p>
</ModalContent>
</Modal>
</>
);
},
};

const ModalContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
height: 30rem;
p {
margin-top: 13rem;
font-size: 1.3rem;
}
`;
81 changes: 81 additions & 0 deletions frontend/src/components/@common/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useCallback, useEffect } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { createPortal } from 'react-dom';
import { styled } from 'styled-components';
import { CloseIcon } from 'assets/icons';

type Props = {
isOpen: boolean;
closeModal: () => void;
} & ComponentPropsWithoutRef<'dialog'>;

const Modal = ({ isOpen = true, closeModal, children, ...rest }: Props) => {
const onKeyDownEscape = useCallback(
(event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
closeModal();
},
[closeModal],
);

useEffect(() => {
if (isOpen) {
window.addEventListener('keydown', onKeyDownEscape);
document.body.style.overflow = 'hidden';
}

return () => {
window.removeEventListener('keydown', onKeyDownEscape);
document.body.style.overflow = 'auto';
};
}, [isOpen, onKeyDownEscape]);

return createPortal(
<S.ModalWrapper>
{isOpen && (
<>
<S.Backdrop onClick={closeModal} />
<S.Content {...rest}>
<S.CloseButton type='button' onClick={closeModal}>
<CloseIcon width={24} height={24} />
</S.CloseButton>
{children}
</S.Content>
</>
)}
</S.ModalWrapper>,
document.body,
);
};

export default Modal;

const S = {
ModalWrapper: styled.div`
position: relative;
z-index: 9999;
`,
Backdrop: styled.div`
position: fixed;
inset: 0;
background: ${({ theme }) => theme.color.modalBackdrop};
`,
Content: styled.dialog`
position: fixed;
inset: 50% auto auto 50%;
display: flex;
justify-content: center;
min-width: 20vw;
max-height: 80vh;
overflow: auto;
padding: 2.5rem;
border: none;
border-radius: 8px;
background-color: ${({ theme }) => theme.color.gray1};
transform: translate(-50%, -50%);
`,
CloseButton: styled.button`
position: absolute;
inset: 2.5rem 2.5rem auto auto;
`,
};
6 changes: 3 additions & 3 deletions frontend/src/components/@common/Tag/Tag.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CloseRounded } from 'assets/icons';
import { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
import { ComponentPropsWithoutRef } from 'react';
import { styled } from 'styled-components';
import { CloseIcon } from 'assets/icons';

type Props = {
removable?: boolean;
Expand All @@ -10,7 +10,7 @@ const Tag = ({ removable = true, children, ...rest }: Props) => {
return (
<S.Tag {...rest}>
#{children}
{removable && <CloseRounded width={14} height={14} />}
{removable && <CloseIcon width={14} height={14} />}
</S.Tag>
);
};
Expand Down
Loading

0 comments on commit a9b29a4

Please sign in to comment.