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

[오다은] Week19 #492

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ee5719b
feat: 모달 버튼 클릭 시 함수 실행되도록 prop 추가
O-daeun Jun 26, 2024
dad1a40
chore: React Query, devtools 설치
O-daeun Jun 26, 2024
82c04c6
feat: Query 세팅
O-daeun Jun 26, 2024
eaa7b83
refactor: 모달을 하나의 컴포넌트에서 다루던 부분을 모달 공통 레이아웃을 가지는 컴포넌트로 수정
O-daeun Jun 26, 2024
66e0388
feat: addFolder 함수 추가 및 폴더추가 로직 구현
O-daeun Jun 26, 2024
0eba22c
feat: 폴더 추가 기능 구현
O-daeun Jun 26, 2024
a08abf9
fix: api 호출 리턴값에 맞게 코드 수정
O-daeun Jun 26, 2024
8fb4227
fix: 폴더 이동 시 링크 새로 받아오게 수정
O-daeun Jun 26, 2024
a14edb6
style: eslint-disable-next-line react-hooks/exhaustive-deps 작성
O-daeun Jun 26, 2024
b0f0413
feat: 폴더에 추가하기 모달에 연결
O-daeun Jun 26, 2024
b757528
feat: 링크를 폴더에 추가하는 기능 구현
O-daeun Jun 27, 2024
32238d7
feat: FolderId param Context API로 구현
O-daeun Jun 28, 2024
75e4cb1
feat: 링크 삭제 모달 기능 구현
O-daeun Jun 28, 2024
f36b180
feat: 폴더 삭제 모달 기능 구현
O-daeun Jun 28, 2024
8b22898
feat: 폴더 이름 변경 구현
O-daeun Jun 29, 2024
1fee61b
feat: 폴더 공유 모달 기능 구현
O-daeun Jun 29, 2024
f0a65e4
feat: getFolders에 useQuery 적용
O-daeun Jun 30, 2024
79e7909
feat: folder 페이지 getLinks, getFolder 및 전체 useQuery 구현 완료
O-daeun Jun 30, 2024
ba6af43
refactor: shared페이지일 때 props folders 전달 안 하도록 선택사항으로 수정
O-daeun Jun 30, 2024
aeeaa26
feat: shared 페이지 api 요청 부분 react Query로 구현
O-daeun Jun 30, 2024
8ef9805
feat: 모달에서 api 요청 시 query.invalidateQueries 구현
O-daeun Jun 30, 2024
9d85f08
test: test코드 삭제
O-daeun Jun 30, 2024
1748b66
fix: type 에러 수정
O-daeun Jun 30, 2024
01a57ac
feat: ModalContext 구현
O-daeun Jul 1, 2024
1f6607a
refactor: folder페이지 모달 context로 적용 구현
O-daeun Jul 1, 2024
d14d83e
refactor: 모든 모달에 context API 적용
O-daeun Jul 1, 2024
24f48c8
fix: card kebab 버튼의 삭제/폴더에추가 클릭 시 여러 개 모달 띄워지는 오류 해결
O-daeun Jul 1, 2024
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
102 changes: 84 additions & 18 deletions apis/api.ts
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

api 주소 변경으로 인해 기존 GET 함수를 조금씩 수정하였고,
POST, PUT, DELETE 요청을 추가 구현하였습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

넵 ! 설명 감사드립니다. 한 번 꼼꼼히 봐볼게염

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AddLink, EditFolderName } from '@/interfaces/api';
import axios from 'axios';
Copy link
Collaborator

Choose a reason for hiding this comment

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

axios instance를 만들어서 사용해보는게 어떨까요 ?

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance


const BASIC_URL = 'https://bootcamp-api.codeit.kr/api';
const BASIC_URL = 'https://bootcamp-api.codeit.kr/api/linkbrary/v1';
Copy link
Collaborator

Choose a reason for hiding this comment

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

base URL은 환경 변수에 저장하시는게 좋습니다!

환경 변수(Environment Variable): process.env에 내장되며 앱이 실행될 때 적용할 수 있는 값입니다!

다음과 같이 적용할 수 있습니다:

// .env.development
REACT_APP_BASE_URL="http://localhost:3000"

// .env.production
REACT_APP_BASE_URL="http://myapi.com"

// 사용시
<a href={`${process.env.REACT_APP_BASE_URL}/myroute`}>URL</a>

왜 환경 변수에 저장해야 하나요?

개발(dev), 테스트(test), 실제 사용(prod) 등 다양한 환경에서 앱을 운영하게 되는 경우, 각 환경에 따라 다른 base URL을 사용해야 할 수 있습니다. 만약 코드 내에 하드코딩되어 있다면, 각 환경에 맞춰 앱을 배포할 때마다 코드를 변경해야 하며, 이는 매우 번거로운 작업이 됩니다. 하지만, 환경 변수를 .env.production, .env.development, .env.test와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

const apiUrl = `${process.env.REACT_APP_BASE_URL}/api`;

이러한 방식으로 환경 변수를 사용하면, 배포 환경에 따라 쉽게 URL을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

실제 코드 응용과 관련해서는 다음 한글 아티클을 참고해보세요! => 보러가기


export async function getUser() {
let response;
Expand All @@ -13,13 +14,13 @@ export async function getUser() {
} else {
return;
}
const result = response.data.data[0];
const result = response.data[0];
return result;
}

export async function getFolderUser(id: number) {
const response = await axios.get(`${BASIC_URL}/users/${id}`);
const result = response.data.data[0];
const result = response.data[0];
return result;
}

Expand All @@ -31,37 +32,102 @@ export async function postCheckDuplicateEmail(id: string) {
}

export async function postSignUp(id: string, pw: string) {
const response = await axios.post(`${BASIC_URL}/sign-up`, {
const response = await axios.post(`${BASIC_URL}/auth/sign-up`, {
email: id,
password: pw,
});
return response;
}

export async function postSignIn(id: string, pw: string) {
const response = await axios.post(`${BASIC_URL}/sign-in`, {
const response = await axios.post(`${BASIC_URL}/auth/sign-in`, {
email: id,
password: pw,
});
const result = response.data.data;
const result = response.data;
return result;
}

export async function getFolders(folderId: number, userId?: number | null) {
export async function getFolders(folderId: number) {
const queryParam = folderId === 0 ? '' : `/${folderId}`;
const userIdParam = userId ? `/users/${userId}` : '';
const response = await axios.get(
`${BASIC_URL}${userIdParam}/folders${queryParam}`
);
const result = response.data.data;
const response = await axios.get(`${BASIC_URL}/folders${queryParam}`, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

반환 타입을 제네릭을 통하여 다루면 어떨까요?

Suggested change
const response = await axios.get(`${BASIC_URL}/folders${queryParam}`, {
const response = await axios.get<GetFoldersResponse>(`${BASIC_URL}/folders${queryParam}`, {

이렇게 하면 getFolders의 반환 타입도 추론될 수 있습니다 😊

headers: {
Authorization: localStorage.accessToken,
},
});
const result = response.data;
return result;
}

export async function getLinks(userId: number | null, folderId: number) {
const queryParam = folderId === 0 ? '' : `?folderId=${folderId}`;
const response = await axios.get(
`${BASIC_URL}/users/${userId}/links${queryParam}`
);
const data = response.data.data;
export async function getLinks(folderId: number) {
const queryParam = folderId === 0 ? '/links' : `/folders/${folderId}/links`;
const response = await axios.get(`${BASIC_URL}${queryParam}`, {
headers: {
Authorization: localStorage.accessToken,
},
});
const data = response.data;
return data;
}

export async function addFolder(newFolderName: string) {
const response = await axios.post(
`${BASIC_URL}/folders`,
{
name: newFolderName,
},
{
headers: {
Authorization: localStorage.accessToken,
},
}
);
}

export async function addLink({ url, folderId }: AddLink) {
const response = await axios.post(
`${BASIC_URL}/links`,
{
url,
folderId,
},
{
headers: {
Authorization: localStorage.accessToken,
},
}
);
}

export async function deleteFolder(folderId: number) {
const response = await axios.delete(`${BASIC_URL}/folders/${folderId}`, {
headers: {
Authorization: localStorage.accessToken,
},
});
}

export async function deleteLink(linkId: number) {
const response = await axios.delete(`${BASIC_URL}/links/${linkId}`, {
headers: {
Authorization: localStorage.accessToken,
},
});
}

export async function putFolderName({
folderId,
newFolderName,
}: EditFolderName) {
const response = await axios.put(
`${BASIC_URL}/folders/${folderId}`,
{
name: newFolderName,
},
{
headers: {
Authorization: localStorage.accessToken,
},
}
);
}
Comment on lines +118 to +133
Copy link
Collaborator

Choose a reason for hiding this comment

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

만약 instance를 만드셨다면 accessToken은 인터셉터로 관리하실 수 있습니다 !

아래는 axios의 메써드인 interceptors를 통하여 미들웨어로 인가를 처리하는 예제예요 ! 한 번 확인해보시고 적용하시는 것도 고려해보세요 😊😊😊

instance.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken')?.replace(/"/gi, '');

    if (!accessToken) return config;
    config.headers.Authorization = accessToken;
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    alert(`ERROR: ${error.response.data.message} `);
    return Promise.reject(error);
  }
);

4 changes: 4 additions & 0 deletions components/Button/Button.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const StyledButton = css`
font-weight: 600;
font-size: 1.125rem;

&:disabled {
cursor: default;
}

@media (max-width: 767px) {
padding: 10px 0;
font-size: 0.875rem;
Expand Down
18 changes: 16 additions & 2 deletions components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,29 @@ interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
link?: string;
}

export default function Button({ text, className = '', link }: Props) {
export default function Button({
text,
className = '',
link,
onClick,
type,
disabled,
}: Props) {
Comment on lines +9 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

...rest를 사용해볼 수 있어요:

Suggested change
export default function Button({
text,
className = '',
link,
onClick,
type,
disabled,
}: Props) {
export default function Button({
text,
link,
children,
...rest
}: Props) {

위처럼 사용하고 버튼은 다음과 같이 작성합니다:

        <S.Button
          {...rest}
        >

어떤 장점이 있나요?

소프트웨어 5대 원칙은 '확장'에는 열려있어야 하며, '수정'에는 닫혀있어야 하는 '개방 폐쇄 원칙'이 있습니다.
만약 현재 버튼에 onFocus, role, onHover 등의 props가 추가로 필요하다면 버튼을 수정해야겠지만, 위처럼 ...rest를 활용한다면 리액트 버튼의 타입들이 추가될 때(onClick, type, 등등..) 수정할 필요가 없다는 장점이 있습니다 😊

Copy link
Collaborator

Choose a reason for hiding this comment

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

추가로 !

다음과 같이 리액트에서 제공하는 Attributes를 사용할 수도 있습니다:

import cn from 'classnames';
import { ButtonHTMLAttributes } from 'react';

interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'none';
}

export default function MelonButton({ className, variant, ...rest }: Props) {

return (
<>
{link ? (
<S.StyledLink href={link} className={className}>
{text}
</S.StyledLink>
) : (
<S.Button className={className}>{text}</S.Button>
<S.Button
className={className}
type={type}
onClick={onClick}
disabled={disabled}
>
{text}
</S.Button>
)}
</>
);
Expand Down
73 changes: 30 additions & 43 deletions components/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
import { ReactEventHandler, useState } from 'react';
import { MouseEvent, ReactEventHandler, useState } from 'react';
import Image, { StaticImageData } from 'next/image';
import Link from 'next/link';
import { formatDateToString, formatDateToAgo } from '@/utils/date';
import Modal from '../Modal/Modal';
import * as S from './Card.styled';
import star from '@/public/images/star_icon.png';
import kebab from '@/public/images/kebab_icon.png';
import defaultImage from '@/public/images/no-image.png';
import { FolderInterface } from '@/interfaces';
import AddLinkModal from '../Modal/Contents/AddLinkModal';
import DeleteLinkModal from '../Modal/Contents/DeleteLinkModal';
import { useModal, useSetModal } from '@/contexts/ModalContext';

interface Props {
item: {
id: number;
created_at: string;
url: string;
title: string;
image_source: string;
};
folderNames?: string[];
itemCountsInEachFolder?: number[];
folders?: FolderInterface[];
}

export default function Card({
item,
folderNames,
itemCountsInEachFolder,
}: Props) {
const { created_at, url, title, image_source } = item;
const [isVisibleKebabModal, setIsVisibleKebabModal] = useState(false);
const [isVisibledeleteCardModal, setIsVisibleDeleteCardModal] =
useState(false);
const [isVisibleAddInFolderModal, setIsVisibleAddInFolderModal] =
useState(false);
export default function Card({ item, folders }: Props) {
const { id, created_at, url, title, image_source } = item;
const [isOpenKebab, setIsOpenKebab] = useState(false);

const modal = useModal();
const setModal = useSetModal();
const dateBetween = formatDateToAgo(created_at);
const date = formatDateToString(created_at);

Expand All @@ -40,22 +37,22 @@ export default function Card({
e.currentTarget.src = defaultImage;
};

const handleStarClick = (e: React.MouseEvent) => {
const handleStarClick = (e: MouseEvent) => {
e.preventDefault();
console.log('별 클릭');
};
const handleKebabClick = (e: React.MouseEvent) => {
const handleKebabClick = (e: MouseEvent) => {
e.preventDefault();
setIsVisibleKebabModal(!isVisibleKebabModal);
setIsOpenKebab(!isOpenKebab);
};

const handleDeleteButtonClick = (e: React.MouseEvent) => {
const handleDeleteButtonClick = (e: MouseEvent) => {
e.preventDefault();
setIsVisibleDeleteCardModal(true);
setModal({ isOpen: true, content: `DeleteLinkModal ${id}` });
};
const handleAddFolderButtonClick = (e: React.MouseEvent) => {
const handleAddLinkButtonClick = (e: MouseEvent) => {
e.preventDefault();
setIsVisibleAddInFolderModal(true);
setModal({ isOpen: true, content: `AddLinkModal ${id}` });
};
Comment on lines +49 to +55
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

카드 내부에서 모달을 띄우니 보여지는 모든 카드의 모달이 중복으로 띄워지는 오류가 있어서 우선 구분할 수 있게 ${id}라는 조건을 추가해 두었습니다.
임시방편으로 해결하여서 추후에 포탈이나 다른 방식으로 모달을 띄우고 싶네욥!
기주님은 모달 띄우실 때 어떤 방법을 사용하시는 편인가요???

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 조건을 추가해서 문제를 해결하셨군요 😊
저 또한 다은님께서 해결하신 방법으로 컨텍스트로 만드는 편입니다 !
또한, 라우팅도 함께 필요한 모달의 경우 NextJs 앱라우터의 패러랠과 인터섹션 라우터로 만들고 있습니다 😊


return (
Expand All @@ -74,15 +71,15 @@ export default function Card({
<S.TextWrap>
<S.TextTopWrap>
<S.DateAgo>{dateBetween}</S.DateAgo>
{folderNames && (
{folders && (
<button onClick={handleKebabClick}>
<Image src={kebab} alt='더보기' width='21' height='17' />
</button>
)}
{isVisibleKebabModal && (
{isOpenKebab && (
<S.KebabModal>
<button onClick={handleDeleteButtonClick}>삭제하기</button>
<button onClick={handleAddFolderButtonClick}>
<button onClick={handleAddLinkButtonClick}>
폴더에 추가
</button>
</S.KebabModal>
Expand All @@ -91,30 +88,20 @@ export default function Card({
<S.Title>{title}</S.Title>
<S.Date>{date}</S.Date>
</S.TextWrap>
{folderNames && (
{folders && (
<S.Star onClick={handleStarClick}>
<Image src={star} alt='별' fill sizes='34px' />
</S.Star>
)}
</Link>
</S.Card>
{isVisibledeleteCardModal && (
<Modal
title='링크 삭제'
semiTitle={url}
button='삭제하기'
onClose={setIsVisibleDeleteCardModal}
/>
)}
{isVisibleAddInFolderModal && (
<Modal
title='폴더에 추가'
semiTitle={url}
folders={folderNames}
counts={itemCountsInEachFolder}
button='추가하기'
onClose={setIsVisibleAddInFolderModal}
/>

{modal.isOpen && modal.content === `DeleteLinkModal ${id}` ? (
<DeleteLinkModal link={url} linkId={id} />
) : modal.content === `AddLinkModal ${id}` ? (
<AddLinkModal link={url} folders={folders} />
) : (
''
)}
</>
);
Expand Down
18 changes: 4 additions & 14 deletions components/CardList/CardList.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,17 @@
import { LinkInterface } from '@/interfaces';
import { FolderInterface, LinkInterface } from '@/interfaces';
import Card from '../Card/Card';
import * as S from './CardList.styled';

interface Props {
items: LinkInterface[] | undefined;
folderNames?: string[];
itemCountsInEachFolder?: number[];
folders?: FolderInterface[];
}

export default function CardList({
items,
folderNames,
itemCountsInEachFolder,
}: Props) {
export default function CardList({ items, folders }: Props) {
return (
<S.List>
{items?.map((item) => (
<Card
key={item.id}
item={item}
folderNames={folderNames}
itemCountsInEachFolder={itemCountsInEachFolder}
/>
<Card key={item.id} item={item} folders={folders} />
))}
</S.List>
);
Expand Down
Loading
Loading