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] - 마이 페이지 구현 #236

Merged
merged 35 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
907e35a
feat: Tab 컴포넌트에 새로고침해도 선택된 tab 그대로이도록 수정
0jenn0 Aug 4, 2024
e1a7409
feat: AvatarCircle에 medium 사이즈 추가
0jenn0 Aug 4, 2024
b916835
test: AvatarCircle의 medium 사이즈 storybook 추가
0jenn0 Aug 4, 2024
b2ef02d
feat: 마이페이지 route 설정
0jenn0 Aug 4, 2024
d9e14a8
feat: Header Drawer에 마이페이지 route 처리
0jenn0 Aug 4, 2024
6b780c4
feat: 마이페이지 tab content에서 사용될 TabContent 컴포넌트 구현
0jenn0 Aug 4, 2024
6c6695c
fix: Tab 컴포넌트 unmount 될 때 선택된 탭이 첫번째 탭이 되도록 수정
0jenn0 Aug 4, 2024
fcd53a8
style: 필요없는 주석 삭제
0jenn0 Aug 4, 2024
3352e04
refactor: Tab 컴포넌트에 새로고침할 때 처음 버튼으로 돌아갔다가 최근껄로 보이는거 수정
0jenn0 Aug 5, 2024
536ee5d
feat: useInfiniteMyTravelPlans 구현
0jenn0 Aug 6, 2024
3b997d3
fix: renderItem type 수정
0jenn0 Aug 6, 2024
81877fe
feat: 날짜 문자열에 숫자로 일수 더해주는 util 함수 구현
0jenn0 Aug 6, 2024
1838659
fix: TravelPlanDetail 페이지를 useGetTravelPlan 반환 타입에 맞춰 수정
0jenn0 Aug 6, 2024
3c981b5
feat: MyTravelPlans 컴포넌트 구현
0jenn0 Aug 6, 2024
ea86c3a
feat: MyTravelogues 컴포넌트 구현
0jenn0 Aug 6, 2024
46ef7eb
feat: 마이 페이지 구현
0jenn0 Aug 6, 2024
b96c57b
Merge branch 'develop/fe' into feature/fe/#194
0jenn0 Aug 6, 2024
267c5f8
fix: 쓰지 않는 변수들 삭제
0jenn0 Aug 6, 2024
794de85
fix: type 이름 수정
0jenn0 Aug 6, 2024
e6d8c7d
fix: 여행기 등록페이지에서 type 맞춰서 수정
0jenn0 Aug 6, 2024
0f321c5
refactor: Tab 컴포넌트 상수 분리
0jenn0 Aug 7, 2024
117fe4b
refactor: state 초기값을 localStorage에서 가져오도록 수정
0jenn0 Aug 7, 2024
f872f57
style: border css 순서 수정
0jenn0 Aug 7, 2024
760705a
style: 색상 코드 하드코딩 대신 디자인 토큰값 사용
0jenn0 Aug 7, 2024
1ecb243
refactor: MyPage에서 필요없는 useEffect 제거
0jenn0 Aug 7, 2024
b74a499
style: font-weight값 수정
0jenn0 Aug 7, 2024
c1fac1d
style: 긴 css를 styled.ts로 분리
0jenn0 Aug 7, 2024
2649e6f
refactor: TabContent 컴포넌트 이름을 MyPageTapContent로 수정
0jenn0 Aug 7, 2024
937d37c
refactor: MyTravelogue 타입을 types폴더로 이동
0jenn0 Aug 7, 2024
14f500c
refactor: MyPageTabContent의 props명 수정
0jenn0 Aug 7, 2024
a0d5d6c
refactor: queries에서 try catch 제거
0jenn0 Aug 7, 2024
3d4a5b8
refactor: data 평탄화를 select로 리팩토링
0jenn0 Aug 7, 2024
31614a6
fix: useGetTravelPlan에서 id string만 받도록 수정
0jenn0 Aug 7, 2024
2b16133
Merge branch 'develop/fe' of https://github.com/woowacourse-teams/202…
jinyoung234 Aug 7, 2024
e9f82ce
refactor: queryKey 수정
jinyoung234 Aug 7, 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const meta = {
$size: {
control: {
type: "select",
options: ["small", "large"],
options: ["small", "medium", "large"],
},
},
profileImageUrl: { control: "text" },
Expand All @@ -36,6 +36,13 @@ export const Small: Story = {
},
};

export const Medium: Story = {
args: {
$size: "medium",
profileImageUrl: "https://i.pinimg.com/564x/c0/d6/5e/c0d65ef2ff5b3e752b70fe54d94d6206.jpg",
},
};

export const Large: Story = {
args: {
$size: "large",
Expand All @@ -50,6 +57,13 @@ export const WithDefaultAvatar: Story = {
},
};

export const MediumWithDefaultAvatar: Story = {
args: {
$size: "medium",
profileImageUrl: "https://invalid-image-url.jpg",
},
};

export const LargeWithDefaultAvatar: Story = {
args: {
$size: "large",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ import styled from "@emotion/styled";
import type { AvatarCircleSize } from "./AvatarCircle.type";

const getSize = ($size: AvatarCircleSize) => {
return $size === "small" ? "2.2rem" : "12.9rem";
if ($size === "small") return "2.2rem";
if ($size === "medium") return "6rem";
if ($size === "large") return "12.9rem";
};

const getIconSize = ($size: AvatarCircleSize) => {
return $size === "small" ? "1.5rem" : "10rem";
if ($size === "small") return "1.5rem";
if ($size === "medium") return "5rem";
if ($size === "large") return "10rem";
Comment on lines +6 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분 switch 문을 사용해도 좋을 거 같아요 아니면

const avatarCircleSize = {
small: css`2.2rem`;
}

이런식으로 하는 것도 좋아보이네요! 또 css로 해주는게 좀 더 의미 전달이 명확하다는 피드백을 받았는데 참고해보고 판단하면 될 것 같습니다!

};

export const FallbackIcon = styled.div<{ $size: AvatarCircleSize }>`
display: flex;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
height: 100%;

background-color: #d9d9d9;
justify-content: center;
align-items: center;

svg {
position: absolute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import type { AvatarCircleSize } from "./AvatarCircle.type";
interface AvatarCircleProps {
$size?: AvatarCircleSize;
profileImageUrl?: string;
imageAlt?: string;
}

const AvatarCircle = ({ $size = "small", profileImageUrl }: AvatarCircleProps) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

$ prefix는 @emotion/styled를 사용하는 영역에서 필요한 것이라고 이해했는데 <S.FallbackIcon $size={size}>의 형태가 되어야 하지 않을까 싶습니다!

const AvatarCircle = ({ $size = "small", profileImageUrl, imageAlt }: AvatarCircleProps) => {
const { imageError, handleImageError } = useImageError({ imageUrl: profileImageUrl });

return (
<S.AvatarCircleContainer $size={$size}>
{!imageError ? (
<img src={profileImageUrl} alt="사용자 프로필 이미지" onError={handleImageError} />
<img src={profileImageUrl} alt={imageAlt} onError={handleImageError} />
) : (
<S.FallbackIcon $size={$size}>
<svg
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type AvatarCircleSize = "small" | "large";
export type AvatarCircleSize = "small" | "medium" | "large";
4 changes: 2 additions & 2 deletions frontend/src/components/common/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const Header = () => {
const handleClickLogout = () => {
saveUser({ accessToken: "" });
};
const handleClickMyPage = () => navigate(ROUTE_PATHS_MAP.my);

return (
<Drawer>
Expand Down Expand Up @@ -60,8 +61,7 @@ const Header = () => {
<Drawer.Content>
<S.MenuList>
<Drawer.Trigger>
{/* TODO: 마이페이지 로직 필요함 */}
<S.MenuItem>마이페이지</S.MenuItem>
<S.MenuItem onClick={handleClickMyPage}>마이페이지</S.MenuItem>
</Drawer.Trigger>
<Drawer.Trigger>
{user?.accessToken ? (
Expand Down
22 changes: 15 additions & 7 deletions frontend/src/components/common/Tab/Tab.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { useRef, useState } from "react";
import { ComponentPropsWithoutRef, useRef, useState } from "react";

import { STORAGE_KEYS_MAP } from "@constants/storage";

import * as S from "./Tab.styled";
import { FIRST_TAB_INDEX, INITIAL_SCROLL_LEFT, INITIAL_START_X } from "./constants";

interface TabProps {
interface TabProps extends React.PropsWithChildren<ComponentPropsWithoutRef<"ul">> {
tabContent: (selectedIndex: number) => JSX.Element;
labels: string[];
}

const Tab = ({ labels, tabContent }: TabProps) => {
const [selectedIndex, setSelectedIndex] = useState(0);

const Tab = ({ labels, tabContent, ...props }: TabProps) => {
const [selectedIndex, setSelectedIndex] = useState(() =>
JSON.parse(
localStorage.getItem(STORAGE_KEYS_MAP.myPageSelectedTab) ?? FIRST_TAB_INDEX.toString(),
),
);
const tabRefs = useRef<(HTMLLIElement | null)[]>([]);
const tabListRef = useRef<HTMLUListElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [startX, setStartX] = useState(INITIAL_START_X);
const [scrollLeft, setScrollLeft] = useState(INITIAL_SCROLL_LEFT);

const handleClickTab = (index: number) => {
setSelectedIndex(index);
localStorage.setItem(STORAGE_KEYS_MAP.myPageSelectedTab, JSON.stringify(index));
};

const handleMouseDown = (e: React.MouseEvent) => {
Expand Down Expand Up @@ -54,6 +61,7 @@ const Tab = ({ labels, tabContent }: TabProps) => {
onMouseLeave={handleMouseLeave}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
{...props}
>
{labels.map((label, index) => (
<S.TabItem
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/common/Tab/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FIRST_TAB_INDEX = 0;
export const INITIAL_START_X = 0;
export const INITIAL_SCROLL_LEFT = 0;
45 changes: 45 additions & 0 deletions frontend/src/components/pages/my/MyPage.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";

import theme from "@styles/theme";
import { PRIMITIVE_COLORS } from "@styles/tokens";

export const Layout = styled.div`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
gap: ${(props) => props.theme.spacing.xl};
padding: ${(props) => props.theme.spacing.l};
`;

export const BoxButton = styled.button`
display: flex;
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};
border-radius: 10px;
`;

export const ColorButtonStyle = css`
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 6.8rem;
padding: ${theme.spacing.m};
border-radius: 10px;

background-color: ${PRIMITIVE_COLORS.blue[50]};
gap: ${theme.spacing.m};
`;

export const ListStyle = css`
li {
${theme.typography.mobile.body};
font-weight: 700;
}
`;
37 changes: 37 additions & 0 deletions frontend/src/components/pages/my/MyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { css } from "@emotion/react";

import { useUserProfile } from "@queries/useUserProfile";

import { AvatarCircle, Tab, Text } from "@components/common";

import * as S from "./MyPage.styled";
import MyTravelPlans from "./MyTravelPlans/MyTravelPlans";
import MyTravelogues from "./MyTravelogues/MyTravelogues";

const MyPage = () => {
const { data } = useUserProfile();

return (
<S.Layout>
<AvatarCircle $size="large" profileImageUrl={data?.profileImageUrl} />
<Text
textType="body"
css={css`
Copy link
Contributor

Choose a reason for hiding this comment

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

700으로 통일해주시면 좋을거 같슴다

font-weight: 700;
`}
>
{data?.nickname}
</Text>

<Tab
Copy link
Contributor

Choose a reason for hiding this comment

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

제가 render props로 바꿔놨는데도 잘 활용해주셨네요 :)

labels={["내 여행 계획", "내 여행기"]}
tabContent={(selectedIndex) => (
<>{selectedIndex === 0 ? <MyTravelPlans /> : <MyTravelogues />}</>
)}
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

엄청 깔끔하네요!

css={S.ListStyle}
/>
</S.Layout>
);
};

export default MyPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";

import theme from "@styles/theme";
import { PRIMITIVE_COLORS } from "@styles/tokens";

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

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

export const ColorButtonStyle = css`
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 6.8rem;
padding: ${theme.spacing.m};
border-radius: 10px;

background-color: ${PRIMITIVE_COLORS.blue[50]};
gap: ${theme.spacing.m};
`;

export const BoxButton = styled.button`
display: flex;
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};
border-radius: 10px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { css } from "@emotion/react";

import { IconButton } from "@components/common";

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

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

const ICON_BUTTON_TEXT = {
ADD_TRAVEL_PLAN: "새 여행 계획 추가하기",
ADD_TRAVELOGUE: "새 여행기 추가하기",
} as const;

interface MyPageTabContentProps<T extends { id: string }> {
iconButtonLabel: (typeof ICON_BUTTON_TEXT)[keyof typeof ICON_BUTTON_TEXT];
onClickIconButton: () => void;
contentDetail: T[];
renderItem: (item: T) => React.ReactNode;
}

const MyPageTabContent = <T extends { id: string }>({
contentDetail,
iconButtonLabel,
onClickIconButton,
renderItem,
}: React.PropsWithChildren<MyPageTabContentProps<T>>) => {
return (
<S.List>
<IconButton
size="16"
position="left"
iconType="plus"
color={SEMANTIC_COLORS.primary}
css={[
S.ColorButtonStyle,
css`
font-weight: 600;
`,
]}
onClick={onClickIconButton}
>
{iconButtonLabel}
</IconButton>

{contentDetail.map((item) => (
<S.BoxButton key={item.id}>{renderItem(item)}</S.BoxButton>
))}
</S.List>
);
};

export default MyPageTabContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";

import theme from "@styles/theme";

export const Layout = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
gap: ${(props) => props.theme.spacing.s};
width: 100%;
`;

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

export const iconButtonStyle = css`
display: flex;
gap: ${theme.spacing.s};
padding: ${theme.spacing.m};
border: 1px solid ${theme.colors.border};
border-radius: 10px;
${theme.typography.mobile.detail};
`;
Loading
Loading