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/BAR-236] 저장하는 페이지 폴더 수정/삭제 기능 구현 #66

Merged
merged 6 commits into from
Feb 18, 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
6 changes: 4 additions & 2 deletions src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export const http = {
param?: ParamType,
options?: AxiosRequestConfig,
): Promise<ResponseType> => instance.put(url, param, options),
delete: <ResponseType>(url: string): Promise<ResponseType> =>
instance.delete(url),
delete: <ParamType, ResponseType>(
url: string,
param?: ParamType,
): Promise<ResponseType> => instance.delete(url, param && param),
};
7 changes: 7 additions & 0 deletions src/api/memoFolder/constants/queryKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const MEMO_FOLDERS = 'memo-folders';

export const MEMO_FOLDERS_KEY = {
all: [MEMO_FOLDERS] as const,
list: () => [...MEMO_FOLDERS_KEY.all, 'list'] as const,
item: (args: unknown[]) => [...MEMO_FOLDERS_KEY.list(), ...args] as const,
};
30 changes: 28 additions & 2 deletions src/api/memoFolder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,33 @@ import { http } from '@api/http';

import { type Folder } from './types';

export const getMemoFolders = () => http.get<Folder[]>('/memo-folders');
const API_URL = '/memo-folders';

export const getMemoFolders = () => http.get<Folder[]>(API_URL);

export const postMemoFolders = (folderName: string) =>
http.post('/memo-folders', { folderName });
http.post(API_URL, { folderName });

export const patchMemoFolders = ({
memoFolderId,
folderName,
}: {
memoFolderId: number;
folderName: string;
}) =>
http.patch(API_URL, {
memoFolderId,
folderName,
});

export const deleteMemoFolders = ({
memoFolderId,
deleteAllMemo,
}: {
memoFolderId: number;
deleteAllMemo: boolean;
}) =>
http.delete(API_URL, {
memoFolderId,
deleteAllMemo,
});
25 changes: 25 additions & 0 deletions src/api/memoFolder/mutations/useDeleteMemoFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { useToastStore } from '@stores/toast';

import { deleteMemoFolders } from '..';
import { MEMO_FOLDERS_KEY } from '../constants/queryKey';

const useDeleteMemoFolder = () => {
const { showToast } = useToastStore();

const queryClient = useQueryClient();

return useMutation({
mutationFn: deleteMemoFolders,
onSuccess: () => {
showToast({ message: '선택한 폴더가 삭제되었어요' });

queryClient.invalidateQueries({
queryKey: MEMO_FOLDERS_KEY.all,
});
},
});
};

export default useDeleteMemoFolder;
25 changes: 25 additions & 0 deletions src/api/memoFolder/mutations/useUpdateMemoFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { useToastStore } from '@stores/toast';

import { patchMemoFolders } from '..';
import { MEMO_FOLDERS_KEY } from '../constants/queryKey';

const useUpdateMemoFolder = () => {
const { showToast } = useToastStore();

const queryClient = useQueryClient();

return useMutation({
mutationFn: patchMemoFolders,
onSuccess: () => {
showToast({ message: '폴더 이름이 수정되었어요' });

queryClient.invalidateQueries({
queryKey: MEMO_FOLDERS_KEY.all,
});
},
});
};

export default useUpdateMemoFolder;
5 changes: 4 additions & 1 deletion src/components/Button/style.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { sprinkles } from '@styles/sprinkles.css';
import { COLORS } from '@styles/tokens';

export const button = recipe({
base: {
whiteSpace: 'nowrap',
},
variants: {
state: {
default: {
Expand All @@ -24,7 +27,7 @@ export const button = recipe({
M: [
sprinkles({ typography: '15/Title/Medium' }),
{
padding: '15px 24px',
padding: '15px 20px',
borderRadius: '8px',
},
],
Expand Down
2 changes: 1 addition & 1 deletion src/components/Dropdown/style.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const menuList = recipe({
borderRadius: '12px',
boxShadow: '0px 8px 15px 0px rgba(28, 28, 28, 0.08)',
backgroundColor: COLORS['Grey/White'],
zIndex: 100,
zIndex: 50,
},
variants: {
size: {
Expand Down
5 changes: 3 additions & 2 deletions src/components/Input/WriteInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ChangeEvent, HTMLAttributes, KeyboardEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { assignInlineVars } from '@vanilla-extract/dynamic';

import Button from '@components/Button';
import Icon from '@components/Icon';
import { MAIN_INPUT_MAX_LENGTH } from '@constants/config';
import type { UseInputReturn } from '@hooks/useInput';
Expand Down Expand Up @@ -104,14 +105,14 @@ const WriteInput = ({
&nbsp;/&nbsp;500자
</span>
)}
<button disabled={!isValid} onClick={onSubmit}>
<Button type="button" disabled={!isValid} onClick={onSubmit}>
<Icon
icon="submit"
width={48}
height={48}
color={isValid ? COLORS['Blue/Default'] : undefined}
/>
</button>
</Button>
</div>
</div>
</div>
Expand Down
17 changes: 12 additions & 5 deletions src/components/Modal/components/ModalContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import { useModalStore } from '@stores/modal';

import Portal from '../../Portal';
import * as styles from '../style.css';
import { ModalBody, ModalFooter, ModalHeader } from './ModalLayout';

type ModalSizeType = 'login' | 'common';

interface ModalContainerProps {
interface ModalRootProps {
type?: ModalSizeType;
}

const ModalContainer = ({
const ModalRoot = ({
children,
type = 'common',
}: PropsWithChildren<ModalContainerProps>) => {
}: PropsWithChildren<ModalRootProps>) => {
const dimmedRef = useRef<HTMLDivElement>(null);

const pathname = usePathname();
Expand Down Expand Up @@ -59,14 +60,20 @@ const ModalContainer = ({
return (
<Portal id={PORTAL_ID.MODAL}>
<div className={styles.dimmed} ref={dimmedRef} />
<div className={styles.modalStyle({ type })}>
<section className={styles.modalStyle({ type })}>
<Button className={styles.closeButton} onClick={closeModal}>
<Icon icon="close" width={closeIconSize} height={closeIconSize} />
</Button>
{children}
</div>
</section>
</Portal>
);
};

const ModalContainer = Object.assign(ModalRoot, {
Header: ModalHeader,
Body: ModalBody,
Footer: ModalFooter,
});

export default ModalContainer;
34 changes: 34 additions & 0 deletions src/components/Modal/components/ModalLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type HTMLAttributes, type PropsWithChildren } from 'react';

import * as styles from '../style.css';

interface ModalProps extends HTMLAttributes<HTMLDivElement> {}

export const ModalHeader = ({
children,
...props
}: PropsWithChildren<ModalProps>) => {
return (
<header {...props} className={styles.modalHeader}>
Copy link
Contributor

Choose a reason for hiding this comment

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

보통 모달에는 header/main/footer 태그를 안 사용하지 않나요?

Copy link
Member Author

Choose a reason for hiding this comment

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

이것도 하나의 section으로 볼 수 있을 거 같아서 해두었습니다..!

image

{children}
</header>
);
};

export const ModalBody = ({
children,
...props
}: PropsWithChildren<ModalProps>) => {
return (
<main {...props} className={styles.modalBody}>
{children}
</main>
);
};

export const ModalFooter = ({
children,
...props
}: PropsWithChildren<ModalProps>) => {
return <footer {...props}>{children}</footer>;
};
6 changes: 6 additions & 0 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import DeleteFolderModal from '@domain/저장하는/components/DeleteFolderModal';
import { useModalStore } from '@stores/modal';

import DeleteArticle from './modals/DeleteArticle';
import EditFolder from './modals/EditFolder';
import Login from './modals/Login';
import MakeFolder from './modals/MakeFolder';

Expand All @@ -13,6 +15,10 @@ const Modal = () => {

if (type === 'makeFolder') return <MakeFolder />;

if (type === 'editFolder') return <EditFolder />;

if (type === 'deleteFolder') return <DeleteFolderModal />;

return null;
};

Expand Down
107 changes: 107 additions & 0 deletions src/components/Modal/modals/EditFolder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { type ChangeEvent, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { AxiosError } from 'axios';
import clsx from 'clsx';

import useUpdateMemoFolder from '@api/memoFolder/mutations/useUpdateMemoFolder';
import Button from '@components/Button';
import Icon from '@components/Icon';
import { useModalStore } from '@stores/modal';
import { COLORS } from '@styles/tokens';

import ModalContainer from '../components/ModalContainer';
import * as styles from '../style.css';

const EditFolder = () => {
const queryClient = useQueryClient();

const { closeModal, memoFolderId, folderName } = useModalStore();
const [value, setValue] = useState(folderName);
const [errorMessage, setErrorMessage] = useState('');

const { mutateAsync } = useUpdateMemoFolder();

const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setErrorMessage('');
setValue(e.target.value);
};

const handleFolderNameEdit = async () => {
if (value.length > 10) return setErrorMessage('10자 내로 입력해주세요!');

await mutateAsync(
{ memoFolderId, folderName: value },
{
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ['memo-folders'],
});
closeModal();
},
onError: (e) => {
if (!(e instanceof AxiosError) || !e.response) throw e;
if (
(e.response.data as { errorCode: string; message: string })
.errorCode === 'MF01'
)
return setErrorMessage('이미 사용 중인 폴더 이름이에요!');
throw e;
},
},
);
};

return (
<ModalContainer>
<strong className={styles.makeFolderTitle}>폴더 수정하기</strong>
<label htmlFor="makeFolder" className={styles.makeFolderDescription}>
폴더 이름
</label>
<div className={styles.makeFolderInputWrapper}>
<input
id="makeFolder"
className={clsx(
styles.makeFolderInput,
errorMessage && styles.errorInput,
)}
value={value}
onChange={handleInputChange}
/>
</div>
{errorMessage && (
<span className={styles.errorMessage}>
<div className={styles.errorIcon}>
<Icon width={20} height={20} icon="error" />
</div>
{errorMessage}
</span>
)}
<div className={styles.makeFolderButtonWrapper}>
<Button
className={styles.button}
style={assignInlineVars({
[styles.buttonColor]: COLORS['Grey/600'],
[styles.buttonBackgroundColor]: COLORS['Grey/150'],
})}
onClick={closeModal}
>
취소
</Button>
<Button
className={clsx(styles.button, !value && styles.buttonDisabled)}
disabled={!value}
style={assignInlineVars({
[styles.buttonColor]: COLORS['Grey/White'],
[styles.buttonBackgroundColor]: COLORS['Blue/Default'],
})}
onClick={handleFolderNameEdit}
>
저장하기
</Button>
</div>
</ModalContainer>
);
};

export default EditFolder;
Loading