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

time selector 리팩토링 #70

Merged
merged 10 commits into from
Sep 21, 2024
15 changes: 0 additions & 15 deletions src/components/icon/Hyphen/index.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions src/constants/timeSelector/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const TIME_LIST = Array.from({ length: 24 }, (_, i) => {
const AMPM = i < 12 ? '오전' : '오후';
let hour = i.toString().padStart(2, '0');

if (i >= 13) hour = (i - 12).toString().padStart(2, '0');

return `${AMPM} ${hour}시`;
});
49 changes: 49 additions & 0 deletions src/pages/Home/components/TimeCarousel.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import styled from '@emotion/styled';

export const ITEM_HEIGHT = 20;

const Carousel = styled.ol`
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 40px;
padding: 10px 0;
overflow-y: hidden;
touch-action: none;
cursor: grab;
user-select: none;
background-color: ${({ theme }) => theme.colors.blueGrey.A06};
border-radius: 4px;

&:first-of-type {
width: 40%;
min-width: 64px;
}

&::-webkit-scrollbar {
display: none;
}

& {
-ms-overflow-style: none; /* 인터넷 익스플로러 */
scrollbar-width: none; /* 파이어폭스 */
}
`;

const Item = styled.li`
${({ theme }) => theme.typo['body-2']}
width: 100%;
height: ${ITEM_HEIGHT}px;
color: ${({ theme }) => theme.colors.text.secondary};
text-align: center;

&.is-active {
color: ${({ theme }) => theme.colors.text.primary};
}
`;

export const S = {
Carousel,
Item,
};
98 changes: 98 additions & 0 deletions src/pages/Home/components/TimeCarousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useEffect, useRef, useState } from 'react';
import { ITEM_HEIGHT, S } from './TimeCarousel.style';
import { SelectedTime } from '..';

type TimeCarouselProps = {
times: string[];
type: keyof SelectedTime;
updateSelectedTime: (
key: keyof SelectedTime,
value: SelectedTime[keyof SelectedTime]
) => void;
};

const TimeCarousel = ({
times,
type,
updateSelectedTime,
}: TimeCarouselProps) => {
const [currentIndex, setCurrentIndex] = useState(
type === 'end' ? Math.min(times.length - 1, 8) : 0
);
const [userSelected, setUserSelected] = useState(times[currentIndex]);
const [isDragging, setIsDragging] = useState(false);
const [prevPageY, setPrevPageY] = useState(0);
const [prevScrollTop, setPrevScrollTop] = useState(0);
const carouselRef = useRef<HTMLOListElement>(null);
const itemsRef = useRef<Array<HTMLElement | null>>([]);

const handleDragging = (e: React.PointerEvent) => {
e.preventDefault();
if (!carouselRef.current || !isDragging) return;

const positionDiff = e.pageY - prevPageY;
carouselRef.current.scrollTop = prevScrollTop - positionDiff;

if (Math.abs(positionDiff) > ITEM_HEIGHT / 3) {
const index = Math.round(carouselRef.current.scrollTop / ITEM_HEIGHT);
setCurrentIndex(index);
}
};

const handleDragStart = (e: React.PointerEvent) => {
if (!carouselRef.current) return;
setIsDragging(true);
setPrevPageY(e.pageY);
setPrevScrollTop(carouselRef.current.scrollTop);
};

const handleDragStop = () => {
setIsDragging(false);
setUserSelected(times[currentIndex]);
updateSelectedTime(type, times[currentIndex]);
itemsRef.current[currentIndex]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
};

useEffect(() => {
let index = 0;
if (type === 'end') {
const selectedIndex = times.indexOf(userSelected);
index = selectedIndex >= 0 ? selectedIndex : 0;
}

setCurrentIndex(index);
updateSelectedTime(type, times[index]);
itemsRef.current[index]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}, [times]);

return (
<S.Carousel
ref={carouselRef}
onPointerMove={handleDragging}
onPointerDown={handleDragStart}
onPointerUp={handleDragStop}
onPointerLeave={handleDragStop}
>
{times.map((time, index) => (
<S.Item
key={time}
className={`${currentIndex === index && 'is-active'}`}
data-index={index}
ref={(el) => {
itemsRef.current[index] = el;
}}
>
{time}
</S.Item>
))}
</S.Carousel>
);
};

export default TimeCarousel;
102 changes: 24 additions & 78 deletions src/pages/Home/components/TimeSelector.style.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,41 @@
import styled from '@emotion/styled';
import { colors } from '@/styles/colors';
import { Button } from '@mui/material';
import { Button, css } from '@mui/material';
import forwardPropOption from '@/utils/emotionForwardPropOption';

const TimeSelector = styled.div`
const TimeSelector = styled.section`
position: fixed;
bottom: 0;
left: 50%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 768px;
margin: 0;
border: 1px 0 0 0;
border-color: ${colors.blueGrey[600]};
touch-action: none;
border-top: 1px solid ${({ theme }) => theme.colors.elevation.outlined};
transform: translateX(-50%);
`;

const TimeRange = styled.div`
position: relative; /* relative positioning 추가 */
position: relative;
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
width: 100%;
padding: 16px;
background-color: ${colors.white};
box-shadow: none;
`;

const DayList = styled.ul`
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 20%;
height: 40px; /* 보여질 스크롤 높이 */
padding: 8px 0;
overflow-y: auto;
background-color: ${colors.blueGrey['A06']};
border: none;
border-radius: 4px;
scroll-snap-type: y mandatory;

&::-webkit-scrollbar {
width: 0;
height: 0;
}
`;

const TimeList = styled.ul`
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
width: 40%;
height: 40px; /* 보여질 스크롤 높이 */
padding: 8px 0;
overflow-y: auto;
background-color: ${colors.blueGrey['A06']};
border: none;
border-radius: 4px;
scroll-snap-type: y mandatory;

&::-webkit-scrollbar {
width: 0;
height: 0;
}
`;

const Times = styled.li`
width: 100%;
height: 24px;
text-align: center;
opacity: 0.5;
scroll-snap-align: center;

&.highlight {
opacity: 1;
}

@media (max-width: 600px) {
${({ theme }) => theme.typo['body-2']};
line-height: 1.5;
}
const Hypen = styled.div`
flex-shrink: 0;
width: 14px;
height: 2px;
background-color: ${({ theme }) => theme.colors.primary.main};
`;

const CheckButton = styled(Button)`
const CheckButton = styled(Button, forwardPropOption)<{ $isChange: boolean }>`
box-sizing: border-box;
display: flex;
align-items: center;
Expand All @@ -96,20 +44,20 @@ const CheckButton = styled(Button)`
min-width: 40px;
height: 40px;
padding: 8px;
background-color: ${({ disabled }) =>
disabled ? colors.blueGrey['A12'] : colors.blueGrey[600]};
border-radius: 4px;

&:hover {
background-color: ${({ disabled }) =>
disabled ? colors.blueGrey['A12'] : colors.blueGrey[700]};
}
${({ $isChange, theme }) =>
css`
background-color: ${$isChange
? theme.colors.primary.main
: theme.colors.action.disabledBackground};

&:disabled {
cursor: not-allowed;
background-color: ${colors.blueGrey['A12']};
box-shadow: none;
}
&:hover {
background-color: ${$isChange
? theme.colors.primary.main
: theme.colors.action.disabledBackground};
}
`}
`;

export const C = {
Expand All @@ -119,7 +67,5 @@ export const C = {
export const S = {
TimeSelector,
TimeRange,
DayList,
TimeList,
Times,
Hypen,
};
Loading