Skip to content

Commit

Permalink
[Feature/BAR-236] 저장하는 페이지 폴더 수정/삭제 기능 구현 (#66)
Browse files Browse the repository at this point in the history
* feat(domain/저장하는): 폴더 삭제 기능 구현

* feat(domain/저장하는): 폴더 수정 기능 구현

* refactor(EditFolder): 로그 제거

* refactor(WriteInput): 공통 Button 컴포넌트로 변경

* feat(Modal): section 태그로 변경

* feat(NotFoundArchiveCard): 저장된 템플릿이 없을 경우, 메인으로 이동
  • Loading branch information
dmswl98 authored Feb 18, 2024
1 parent 9451ebc commit 25b3666
Show file tree
Hide file tree
Showing 21 changed files with 430 additions and 57 deletions.
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}>
{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

0 comments on commit 25b3666

Please sign in to comment.