Skip to content

Commit

Permalink
Merge pull request #744 from woowacourse-teams/feature/#736
Browse files Browse the repository at this point in the history
프로필 바텀 시트 UI 구현
  • Loading branch information
jaeml06 authored Oct 24, 2024
2 parents 2eea62d + 939d887 commit 17eab02
Show file tree
Hide file tree
Showing 44 changed files with 1,274 additions and 124 deletions.
3 changes: 3 additions & 0 deletions frontend/src/apis/endPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const API_URL = {
},
kakaoOAuth: addBaseUrl('/auth/kakao/oauth', false),
myInfo: addBaseUrl('/member/mine', true),

profile: (darakbangMemberId: number) =>
addBaseUrl(`/members/${darakbangMemberId}/profile`, true),
};

const ENDPOINTS = {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/apis/gets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
GetChatRoomDetail,
GetChattingPreview,
GetDarakbangInviteCode,
GetDarakbangMemberProfile,
GetDarakbangMembers,
GetDarakbangMine,
GetDarakbangNameByCode,
Expand Down Expand Up @@ -237,3 +238,12 @@ export const getBetResult = async (betId: number) => {
const json: GetBetDetail = await response.json();
return json.data.nickname;
};

export const getDarakbangMemberProfile = async (darakbangMemberId: number) => {
const response = await ApiClient.getWithLastDarakbangId(
`/members/${darakbangMemberId}/profile`,
);

const json: GetDarakbangMemberProfile = await response.json();
return json.data;
};
12 changes: 11 additions & 1 deletion frontend/src/apis/responseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface GetMyRoleInDarakbang {
export interface GetDarakbangMembers {
data: {
responses: {
memberId: number;
darakbangMemberId: number;
nickname: string;
profile: string;
}[];
Expand Down Expand Up @@ -136,3 +136,13 @@ export interface PostBet {
betId: number;
};
}

export interface GetDarakbangMemberProfile {
data: {
darakbangMemberId: number;
name: string;
nickname: string;
url: string;
description: string;
};
}
30 changes: 30 additions & 0 deletions frontend/src/components/BottomSheet/BottomSheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react';
import BottomSheet from './BottomSheet';
import Button from '@_components/Button/Button';

const meta = {
component: BottomSheet,
title: 'Components/BottomSheet',
} satisfies Meta<typeof BottomSheet>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
isOpen: true,
onDimmerClick: () => {},
header: <BottomSheet.Header>Header</BottomSheet.Header>,
cta: (
<BottomSheet.CTA>
<Button shape="bar">CTA</Button>
</BottomSheet.CTA>
),
},
render: (args) => (
<BottomSheet {...args}>
<BottomSheet.Main>Content</BottomSheet.Main>
</BottomSheet>
),
};
136 changes: 136 additions & 0 deletions frontend/src/components/BottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Dimmer from '@_components/Dimmer/Dimmer';
import { PropsWithChildren, useEffect, useState } from 'react';
import BottomSheetContainer from './BottomSheetContainer/BottomSheetContainer';
import BottomSheetBody from './BottomSheetBody/BottomSheetBody';
import BottomSheetHandle from './BottomSheetHandle/BottomSheetHandle';

interface BottomSheetProps {
isOpen: boolean;
onDimmerClick: () => void;

header?: React.ReactNode;
cta?: React.ReactNode;

size?: 'small' | 'medium' | 'large' | 'full';
}

export default function BottomSheet(
props: PropsWithChildren<BottomSheetProps>,
) {
const { isOpen, onDimmerClick, header, cta, children, size } = props;

const [startY, setStartY] = useState(0); // 터치 시작 Y좌표
const [currentY, setCurrentY] = useState(window.innerHeight); // 초기에는 화면 아래에 위치
const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부
const [isClosing, setIsClosing] = useState(false); // 닫히는 애니메이션 여부

// Bottom Sheet가 열릴 때 애니메이션으로 위로 올라옴
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden'; // 스크롤 비활성화

// 열릴 때 100px 아래에서 0으로 부드럽게 올라오도록 애니메이션 시작
setTimeout(() => {
setCurrentY(0); // Y값을 0으로 변경 -> 밑에서 위로 올라오는 애니메이션
}, 10); // 딜레이를 줘야 애니메이션이 자연스럽게 동작
} else {
setCurrentY(window.innerHeight); // 닫힐 때 다시 화면 아래로
document.body.style.overflow = 'auto'; // 스크롤 활성화
}

return () => {
document.body.style.overflow = 'auto';
};
}, [isOpen]);

useEffect(() => {
if (!isOpen) {
setCurrentY(window.innerHeight); // 닫힐 때 높이를 초기화
setIsClosing(false); // 닫힘 애니메이션 초기화
}
}, [isOpen]);

// Dimmer 클릭 시 애니메이션으로 밑으로 내려간 후 닫힘
const handleDimmerClick = () => {
setIsClosing(true);
setCurrentY(window.innerHeight); // 화면 아래로 내려가는 애니메이션
setTimeout(() => {
onDimmerClick(); // 300ms 후 실제로 닫기
}, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음
};

// 드래그 시작
const handleTouchStart = (event: React.TouchEvent) => {
setStartY(event.touches[0].clientY);
setIsDragging(true);
};

// 드래그 중
const handleTouchMove = (event: React.TouchEvent) => {
if (!isDragging) return;

const currentTouchY = event.touches[0].clientY;
const deltaY = currentTouchY - startY;

// Y축으로만 움직임을 감지
if (deltaY > 0) {
setCurrentY(deltaY); // 드래그된 만큼 값을 저장
}
};

// 드래그 종료
const handleTouchEnd = () => {
if (!isDragging) return;
setIsDragging(false);

// 드래그가 일정 값 이상이면 Bottom Sheet를 닫음
if (currentY > 100) {
// 애니메이션으로 Bottom Sheet를 아래로 내리고 닫기
setIsClosing(true);
setCurrentY(window.innerHeight); // 화면 하단으로 내리는 애니메이션
setTimeout(() => {
onDimmerClick(); // 애니메이션 후 실제로 닫기
}, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음
} else {
setCurrentY(0); // 원래 위치로 되돌림
}
};

if (!isOpen && !isClosing) {
return null;
}

return (
<BottomSheetContainer>
<Dimmer onClick={handleDimmerClick} />
<BottomSheetBody currentY={currentY} size={size} isDragging={isDragging}>
<BottomSheetHandle
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
/>
{header}
{children}
{cta}
</BottomSheetBody>
</BottomSheetContainer>
);
}

BottomSheet.Header = function BottomSheetHeader(props: PropsWithChildren) {
const { children } = props;

return <div css={{ padding: '0 24px' }}>{children}</div>;
};

BottomSheet.Main = function BottomSheetMain(props: PropsWithChildren) {
const { children } = props;

return <div css={{ padding: '0px 24px' }}>{children}</div>;
};

BottomSheet.CTA = function BottomSheetCTA(props: PropsWithChildren) {
const { children } = props;

return <div css={{ padding: '0px 24px' }}>{children}</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { css, Theme } from '@emotion/react';

export const body = ({
theme,
currentY,
size,
isDragging,
}: {
theme: Theme;
currentY: number;
size?: 'small' | 'medium' | 'large' | 'full';
isDragging: boolean;
}) => css`
z-index: 2;
/* 터치 드래그에 따른 Y축 이동 */
transform: translateY(${currentY}px);
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
max-width: 600px;
height: ${size === 'medium'
? '50vh'
: size === 'large'
? '80vh'
: size === 'full'
? '100vh'
: 'auto'};
padding-bottom: 32px;
background-color: ${theme.colorPalette.white[100]};
border-radius: 28px 28px 0 0;
/* 터치 드래그에 따른 Y축 이동 */
transition: ${isDragging ? 'none' : 'transform 0.3s ease'};
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useTheme } from '@emotion/react';
import { PropsWithChildren } from 'react';
import * as S from './BottomSheetBody.style';

interface BottomSheetBodyProps {
currentY: number;
size?: 'small' | 'medium' | 'large' | 'full';
isDragging: boolean;
}

export default function BottomSheetBody(
props: PropsWithChildren<BottomSheetBodyProps>,
) {
const { currentY, size, isDragging, children } = props;

const theme = useTheme();

return (
<div css={S.body({ theme, currentY, size, isDragging })}>{children}</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { css } from '@emotion/react';

export const container = css`
position: fixed;
z-index: 1;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 100%;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PropsWithChildren } from 'react';
import * as S from './BottomSheetContainer.style';

export default function BottomSheetContainer(props: PropsWithChildren) {
const { children } = props;

return <div css={S.container}>{children}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { css } from '@emotion/react';

export const handleWrapper = css`
display: flex;
align-items: center;
justify-content: center;
height: 32px;
`;

export const handleBar = css`
width: 50px;
height: 6px;
background-color: black;
border-radius: 12px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as S from './BottomSheetHandle.style';

interface BottomSheetHandleProps {
onTouchStart: (event: React.TouchEvent) => void;
onTouchMove: (event: React.TouchEvent) => void;
onTouchEnd: () => void;
}

export default function BottomSheetHandle(props: BottomSheetHandleProps) {
const { onTouchStart, onTouchMove, onTouchEnd } = props;

return (
<div
css={S.handleWrapper}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
<div css={S.handleBar} />
</div>
);
}
22 changes: 22 additions & 0 deletions frontend/src/components/CloseButton/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import CloseIcon from '@_components/Icons/CloseIcon';
import { css } from '@emotion/react';
import { ButtonHTMLAttributes } from 'react';

interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}

export default function CloseButton(props: CloseButtonProps) {
const { ...rest } = props;

return (
<button
css={css`
padding: 0.4rem;
background: none;
border: none;
`}
{...rest}
>
<CloseIcon />
</button>
);
}
16 changes: 16 additions & 0 deletions frontend/src/components/Dimmer/Dimmer.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { css } from '@emotion/react';

export const dimmer = css`
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 100%;
background-color: rgb(0 0 0 / 20%);
`;
5 changes: 5 additions & 0 deletions frontend/src/components/Dimmer/Dimmer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as S from './Dimmer.style';

export default function Dimmer({ onClick }: { onClick: () => void }) {
return <div onClick={onClick} css={S.dimmer} />;
}
Loading

0 comments on commit 17eab02

Please sign in to comment.