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] - 마이페이지 좋아요 탭 및 검색 창 국가 탭 기능 구현 #556

Merged
merged 8 commits into from
Oct 22, 2024
2 changes: 1 addition & 1 deletion frontend/src/components/common/Tab/Tab.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const TabItem = styled.li<{ isSelected: boolean; $tabCount: number }>`
justify-content: center;
align-items: center;

padding: ${({ theme }) => theme.spacing.s} ${({ theme }) => theme.spacing.l};
padding: ${({ theme }) => theme.spacing.s};
border-bottom: 2px solid
${({ isSelected, theme }) => (isSelected ? `${theme.colors.primary}` : "transparent")};

Expand Down
45 changes: 45 additions & 0 deletions frontend/src/components/pages/my/MyLikes/MyLikes.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from "@emotion/styled";

export const Layout = styled.div`
display: flex;
width: 100%;
height: 100%;
gap: ${(props) => props.theme.spacing.m};
`;

export const List = styled.ul`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing.m};

width: 100%;
padding: 0 ${(props) => props.theme.spacing.l};
`;

export const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: ${(props) => props.theme.spacing.s};
`;

export const DetailContainer = styled.div`
display: flex;
align-items: center;
gap: ${(props) => props.theme.spacing.m};

width: 100%;
`;

export const BoxButton = styled.button`
display: flex;
gap: ${(props) => props.theme.spacing.m};
justify-content: flex-start;
Copy link
Contributor

Choose a reason for hiding this comment

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

요 녀석도 없어도 동일하게 동작합니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

justify-content의 기본값이 flex-start라서 그런거 같네요~!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

삭제했습니다!

align-items: center;

width: 100%;
padding: ${(props) => props.theme.spacing.m};
border: 1px solid ${(props) => props.theme.colors.border};
border-radius: 10px;
`;
102 changes: 102 additions & 0 deletions frontend/src/components/pages/my/MyLikes/MyLikes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

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

import type { UserResponse } from "@type/domain/user";

import useInfiniteMyLikes from "@queries/useInfiniteMyLikes";

import { AvatarCircle, Text } from "@components/common";
import MyPageTabContentSkeleton from "@components/pages/my/MyPageTabContentSkeleton/MyPageTabContentSkeleton";

import useIntersectionObserver from "@hooks/useIntersectionObserver";

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

import theme from "@styles/theme";

import MyPageTabContent from "../MyPageTabContent/MyPageTabContent";
import * as S from "./MyLikes.styled";

interface MyLikesProps {
userData: UserResponse;
}

const MyLikes = ({ userData }: MyLikesProps) => {
const { myLikes, fetchNextPage, status, isPaused, error } = useInfiniteMyLikes(userData);
const { lastElementRef } = useIntersectionObserver(fetchNextPage);

const navigate = useNavigate();

const handleClickTravelogue = (id: string) => {
navigate(ROUTE_PATHS_MAP.travelogue(Number(id)));
};

const handleClickIconButton = () => {
navigate(ROUTE_PATHS_MAP.root);
};

useEffect(() => {
if (isPaused) alert(ERROR_MESSAGE_MAP.network);
}, [isPaused]);

if (status === "pending") return <MyPageTabContentSkeleton />;

if (error) alert(error.message);

return (
<>
<MyPageTabContent
iconButtonType="search-icon"
iconButtonLabel="다른 여행기 구경하기"
onClickIconButton={handleClickIconButton}
contentDetail={myLikes}
renderItem={({ id, title, createdAt, thumbnailUrl, authorName }) => (
<S.Layout onClick={() => handleClickTravelogue(id)}>
<AvatarCircle size="medium" profileImageUrl={thumbnailUrl} />

<S.Container>
<Text
textType="body"
css={css`
font-weight: 500;
`}
>
{title}
</Text>
<S.DetailContainer>
<Text
textType="detail"
css={css`
color: ${theme.colors.text.secondary};
`}
>
{authorName}
</Text>
<Text
textType="detail"
css={css`
color: ${theme.colors.text.secondary};
`}
>
{createdAt}
</Text>
</S.DetailContainer>
</S.Container>
</S.Layout>
)}
/>

<div
ref={lastElementRef}
css={css`
height: 1px;
`}
/>
</>
);
};

export default MyLikes;
3 changes: 2 additions & 1 deletion frontend/src/components/pages/my/MyPage.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const Layout = styled.div`
gap: ${(props) => props.theme.spacing.xl};

width: 100%;
padding: ${(props) => props.theme.spacing.l};
padding: ${(props) => props.theme.spacing.l} 0;
`;

export const TabContentContainer = styled.div`
Expand All @@ -34,6 +34,7 @@ export const ProfileContainer = styled.div`
gap: ${({ theme }) => theme.spacing.m};

width: 100%;
padding: 0 ${(props) => props.theme.spacing.l};
`;

export const ProfileEditButtonContainer = styled.div`
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/pages/my/MyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,12 @@ const MyPage = () => {
onClose={editModal.handleCloseEditModal}
>
<S.Button onClick={profileImage.handleClickProfileImageEditButton}>
<Text textType="detail">앨범에서 선택</Text>
<Text textType="detail">프로필 사진 올리기</Text>
</S.Button>
{profileImage.profileImageUrl && (
<S.Button onClick={profileImage.handleClickProfileImageDeleteButton}>
<Text textType="detailBold" css={S.deleteTextColor}>
프로필 사진 삭제
프로필 사진 삭제하기
</Text>
</S.Button>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export const Container = styled.div`
export const List = styled.ul`
display: flex;
flex-direction: column;
width: 100%;
gap: ${(props) => props.theme.spacing.m};

width: 100%;
padding: 0 ${(props) => props.theme.spacing.l};
`;

export const ColorButtonStyle = css`
Expand All @@ -37,6 +39,7 @@ export const BoxButton = styled.button`
gap: ${(props) => props.theme.spacing.m};
justify-content: flex-start;
align-items: center;

width: 100%;
padding: ${(props) => props.theme.spacing.m};
border: 1px solid ${(props) => props.theme.colors.border};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { css } from "@emotion/react";

import { IconButton } from "@components/common";
import SVG_ICONS_MAP from "@components/common/Icon/svg-icons.json";

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

Expand All @@ -9,10 +10,12 @@ import * as S from "./MyPageTabContent.styled";
const ICON_BUTTON_TEXT = {
ADD_TRAVEL_PLAN: "새 여행 계획 추가하기",
ADD_TRAVELOGUE: "새 여행기 추가하기",
GO_ROOT_PAGE: "다른 여행기 구경하기",
} as const;

interface MyPageTabContentProps<T extends { id: string }> {
iconButtonLabel: (typeof ICON_BUTTON_TEXT)[keyof typeof ICON_BUTTON_TEXT];
iconButtonType?: keyof typeof SVG_ICONS_MAP;
onClickIconButton: () => void;
contentDetail: T[];
renderItem: (item: T) => React.ReactNode;
Expand All @@ -21,6 +24,7 @@ interface MyPageTabContentProps<T extends { id: string }> {
const MyPageTabContent = <T extends { id: string }>({
contentDetail,
iconButtonLabel,
iconButtonType = "plus",
onClickIconButton,
renderItem,
}: React.PropsWithChildren<MyPageTabContentProps<T>>) => {
Expand All @@ -29,7 +33,7 @@ const MyPageTabContent = <T extends { id: string }>({
<IconButton
size="16"
position="left"
iconType="plus"
iconType={iconButtonType}
color={SEMANTIC_COLORS.primary}
css={[
S.ColorButtonStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,3 @@ export const Container = styled.div`
align-items: flex-start;
gap: ${(props) => props.theme.spacing.s};
`;

export const TraveloguesList = styled.ul`
display: flex;
flex-direction: column;
width: 100%;
gap: ${(props) => props.theme.spacing.m};
`;
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const MyTravelogues = ({ userData }: MyTraveloguesProps) => {
color: ${theme.colors.text.secondary};
`}
>
게시일 {createdAt}
{createdAt}
</Text>
</S.Container>
</S.Layout>
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/pages/my/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ERROR_MESSAGE_MAP } from "@constants/errorMessage";

import MyLikes from "./MyLikes/MyLikes";
import MyTravelPlans from "./MyTravelPlans/MyTravelPlans";
import MyTravelogues from "./MyTravelogues/MyTravelogues";

export const TAB_CONTENT = [
{ label: "✈️ 내 여행 계획", component: MyTravelPlans },
{ label: "📝 내 여행기", component: MyTravelogues },
{ label: "❤️ 좋아요", component: MyLikes },
] as const;

export const IGNORED_ERROR_MESSAGES = [ERROR_MESSAGE_MAP.api.login, "Network Error"];
Copy link
Contributor

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
@@ -0,0 +1,22 @@
import { SearchFallback, Text } from "@components/common";

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

interface EmptySearchResultProps {
keyword: string | undefined;
}

const EmptySearchResult = ({ keyword }: EmptySearchResultProps) => {
return (
<S.Layout>
{keyword && (
<Text css={S.searchResultTextStyle} textType="title">{`"${keyword}" 검색 결과`}</Text>
)}
<S.SearchFallbackWrapper>
<SearchFallback title="휑" text="검색 결과가 없어요." />
</S.SearchFallbackWrapper>
</S.Layout>
);
};

export default EmptySearchResult;
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type { SearchType } from "@type/domain/travelogue";

import useInfiniteSearchTravelogues from "@queries/useInfiniteSearchTravelogues";

import { SearchFallback, Text } from "@components/common";
import { Text } from "@components/common";
import TravelogueCard from "@components/pages/main/TravelogueCard/TravelogueCard";
import TravelogueCardSkeleton from "@components/pages/main/TravelogueCard/skeleton/TravelogueCardSkeleton";

import useIntersectionObserver from "@hooks/useIntersectionObserver";

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

import EmptySearchResult from "./EmptySearchResult";
import * as S from "./TravelogueList.styled";

const SKELETON_COUNT = 5;
Expand All @@ -29,22 +30,13 @@ const TravelogueList = ({ keyword, searchType }: TravelogueListProps) => {
const { lastElementRef } = useIntersectionObserver(fetchNextPage);

if (travelogues.length === 0 && status === "success") {
return (
<S.Layout>
{keyword && (
<Text css={S.searchResultTextStyle} textType="title">{`"${keyword}" 검색 결과`}</Text>
)}
<S.SearchFallbackWrapper>
<SearchFallback title="휑" text="검색 결과가 없어요." />
</S.SearchFallbackWrapper>
</S.Layout>
);
return <EmptySearchResult keyword={keyword} />;
}

if (status === "error") {
error && alert(error.message);

return <SearchFallback title="휑" text="검색 결과가 없어요." />;
return <EmptySearchResult keyword={keyword} />;
}

if (isPaused) alert(ERROR_MESSAGE_MAP.network);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/pages/search/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const TAB_CONTENT = [
{ label: "제목", searchType: "TITLE" },
{ label: "작성자", searchType: "AUTHOR" },
{ label: "나라", searchType: "COUNTRY" },
] as const;
1 change: 1 addition & 0 deletions frontend/src/constants/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const API_ENDPOINT_MAP = {
profile: "/member/me/profile",
myTravelogues: "/member/me/travelogues",
myTravelPlans: "/member/me/travel-plans",
myLikes: "/member/me/likes",
searchTravelogues: "/travelogues/search",
reissueToken: "/login/reissue-token",
tags: "/tags",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/queryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const QUERY_KEYS_MAP = {
selectedSortingOption,
selectedTravelPeriodOption,
],
likes: () => [...QUERY_KEYS_MAP.travelogue.all, "likes"],
Copy link
Contributor

Choose a reason for hiding this comment

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

이 친구는 메서드로 만든 이유가 있을까요? 제가 보기엔 값으로 만들어도 충분하다는 생각이어서요!

Copy link
Contributor Author

@simorimi simorimi Oct 22, 2024

Choose a reason for hiding this comment

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

다른 부분들이 이러한 방식으로 구성되어 있어서 일관성을 위해서 해당 방식을 적용했습니다

},
travelPlan: {
all: ["travel-plans"],
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/queries/useDeleteUpdateHeart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export const useDeleteUpdateHeart = () => {
queryClient.invalidateQueries({
queryKey: QUERY_KEYS_MAP.travelogue.all,
});
queryClient.invalidateQueries({
queryKey: QUERY_KEYS_MAP.travelogue.likes(),
refetchType: "inactive",
});
},
onError: (error) => {
if (error instanceof ApiError && error.message === ERROR_MESSAGE_MAP.api.login) {
Expand Down
Loading
Loading