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

지역 저장 삭제 구현 #23

Merged
merged 19 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d0e20b3
feat: 검색 목록 아이템 클릭 시, 해당 지역 저장 구현
presentKey Aug 21, 2024
fc9559e
feat: Region 타입 분리
presentKey Aug 21, 2024
dbe13de
feat: 사용자 지역 선택 영역 마크업
presentKey Aug 21, 2024
c98fd6d
feat: 지역 버튼 롱클릭, 숏클릭 구현
presentKey Aug 21, 2024
966a897
feat: plus 아이콘 클릭 시, search 페이지 이동
presentKey Aug 21, 2024
8b99acd
feat: 지역 삭제 구현
presentKey Aug 21, 2024
53826fe
design: 사용자 지역 선택 영역 스타일 적용
presentKey Aug 21, 2024
2abb7d4
design: 지역 삭제 버튼 스타일 적용
presentKey Aug 21, 2024
c24acff
rename: Location Icon 폴더명 변경
presentKey Aug 21, 2024
c071991
design: input 및 icon 색상 변경
presentKey Aug 21, 2024
5639528
feat: emotionForwordPropOption utils 구현
presentKey Aug 22, 2024
dc65e28
feat: 스크롤 활성 여부에 따른 white gradient 스타일 적용
presentKey Aug 22, 2024
321abda
feat: 숏클릭, 롱클릭 로직 변경
presentKey Aug 22, 2024
6dc5ad9
Merge branch 'develop' of https://github.com/FashionForecast/FashionF…
presentKey Aug 22, 2024
1a82c47
Merge branch 'develop' of https://github.com/FashionForecast/FashionF…
presentKey Aug 22, 2024
275a0bb
Merge branch 'develop' of https://github.com/FashionForecast/FashionF…
presentKey Aug 22, 2024
b55366c
feat: default 지역 설정
presentKey Aug 22, 2024
05636ec
rename: 파일명 변경
presentKey Aug 22, 2024
e32bf21
chore: 오타 수정
presentKey Aug 22, 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
2 changes: 1 addition & 1 deletion src/components/icon/Cancel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CancelIcon = () => {
>
<path
d='M10 0C4.47 0 0 4.47 0 10C0 15.53 4.47 20 10 20C15.53 20 20 15.53 20 10C20 4.47 15.53 0 10 0ZM15 13.59L13.59 15L10 11.41L6.41 15L5 13.59L8.59 10L5 6.41L6.41 5L10 8.59L13.59 5L15 6.41L11.41 10L15 13.59Z'
fill='black'
fill='#323941'
fillOpacity='0.56'
/>
</svg>
Expand Down
2 changes: 1 addition & 1 deletion src/components/icon/Check/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const CheckIcon = () => {
>
<path
d='M8.79508 15.875L4.62508 11.705L3.20508 13.115L8.79508 18.705L20.7951 6.70504L19.3851 5.29504L8.79508 15.875Z'
fill='black'
fill='#323941'
fillOpacity='0.56'
/>
</svg>
Expand Down
18 changes: 18 additions & 0 deletions src/components/icon/Location/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const LocationIcon = () => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='20'
viewBox='0 0 14 20'
fill='none'
>
<path
d='M7 0C3.13 0 0 3.13 0 7C0 12.25 7 20 7 20C7 20 14 12.25 14 7C14 3.13 10.87 0 7 0ZM7 9.5C5.62 9.5 4.5 8.38 4.5 7C4.5 5.62 5.62 4.5 7 4.5C8.38 4.5 9.5 5.62 9.5 7C9.5 8.38 8.38 9.5 7 9.5Z'
fill='#627384'
/>
</svg>
);
};

export default LocationIcon;
15 changes: 15 additions & 0 deletions src/components/icon/Plus/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const PlusIcon = () => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 14 14'
fill='none'
>
<path d='M14 8H8V14H6V8H0V6H6V0H8V6H14V8Z' fill='#627384' />
</svg>
);
};

export default PlusIcon;
18 changes: 18 additions & 0 deletions src/components/icon/TrashCan/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const TrashCan = () => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='18'
viewBox='0 0 14 18'
fill='none'
>
<path
d='M1 16C1 17.1 1.9 18 3 18H11C12.1 18 13 17.1 13 16V4H1V16ZM14 1H10.5L9.5 0H4.5L3.5 1H0V3H14V1Z'
fill='#D62D36'
/>
</svg>
);
};

export default TrashCan;
15 changes: 15 additions & 0 deletions src/components/layout/RootLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import { Outlet } from 'react-router-dom';
import * as S from './style';

import { MY_REGIONS } from '@/constants/localStorage/key';

export default function RootLayout() {
setDefaultRegion();

return (
<S.Main>
<Outlet />
</S.Main>
);
}

function setDefaultRegion() {
const saved = localStorage.getItem(MY_REGIONS);

if (!saved) {
localStorage.setItem(
MY_REGIONS,
JSON.stringify([{ region: '서울특별시 종로구', nx: 37, ny: 126 }])
);
}
}
1 change: 1 addition & 0 deletions src/constants/localStorage/key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const MY_REGIONS = 'myRegions';
13 changes: 13 additions & 0 deletions src/pages/Home/components/RegionButton.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from '@emotion/styled';
import { Chip } from '@mui/material';

export const Button = styled.button`
padding: 0;
background-color: inherit;
`;

export const LocationChip = styled(Chip)`
&:has(svg) {
padding-left: 8px;
}
`;
49 changes: 49 additions & 0 deletions src/pages/Home/components/RegionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import LocationIcon from '@/components/icon/Location';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import * as S from './RegionButton.style';

type LocationButtonProps = {
region: string;
isSelected?: boolean;
onRegionClick: (region: string) => void;
};

const RegionButton = ({
region,
isSelected = false,
onRegionClick,
}: LocationButtonProps) => {
const [clickStartTime, setClickStartTime] = useState(0);
const navigate = useNavigate();

const handlePointerUp = () => {
if (isSelected) return;

const clickDuration = Date.now() - clickStartTime;

if (clickDuration < 1000) {
onRegionClick(region); // 숏클릭
} else {
navigate('/search', { state: { region } }); // 롱클릭
}
};

const handlePointerDown = () => {
if (isSelected) return;

setClickStartTime(Date.now());
};

return (
<S.Button onPointerUp={handlePointerUp} onPointerDown={handlePointerDown}>
<S.LocationChip
label={region}
variant={isSelected ? 'filled' : 'outlined'}
icon={isSelected ? <LocationIcon /> : undefined}
/>
</S.Button>
);
};

export default RegionButton;
74 changes: 74 additions & 0 deletions src/pages/Home/components/RegionSelector.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import forwardPropOption from '@/utils/emotionForwardPropOption';
import styled from '@emotion/styled';
import { Chip, css } from '@mui/material';
import { Link } from 'react-router-dom';

export const Section = styled.section`
position: relative;
display: flex;
gap: 8px;
align-items: center;
padding: 8px 16px;
margin-bottom: 8px;
overflow-x: auto;
white-space: nowrap;

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

scrollbar-width: none;
-ms-overflow-style: none;
`;

export const LinkWrap = styled(Link, forwardPropOption)<{
$isScrollActive: boolean;
}>`
position: sticky;
right: 0;
display: inline-block;
width: 48px;
height: 32px;
background-color: ${(props) => props.theme.colors.white};

${({ $isScrollActive }) =>
$isScrollActive &&
css`
&::after {
position: absolute;
top: 0;
left: -16px;
width: 16px;
height: 100%;
content: '';
background: linear-gradient(
270deg,
#fff 0%,
rgb(255 255 255 / 0%) 100%
);
}
`}

&::before {
position: absolute;
top: 0;
right: -16px;
width: 16px;
height: 100%;
content: '';
background-color: ${(props) => props.theme.colors.white};
}
`;

export const PlusChip = styled(Chip)`
position: relative;
width: 32px;
height: 32px;

& .MuiChip-label {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
`;
42 changes: 42 additions & 0 deletions src/pages/Home/components/RegionSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import PlusIcon from '@/components/icon/Plus';
import RegionButton from './RegionButton';
import { Region } from '@/types/region';
import * as S from './RegionSelector.style';
import { useEffect, useRef, useState } from 'react';

type LocationSelectorProps = {
regions: Region[];
onRegionClick: (region: string) => void;
};

const RegionSelector = ({ regions, onRegionClick }: LocationSelectorProps) => {
const sectionRef = useRef<HTMLElement>(null);
const [isScrollActive, setIsScrollActive] = useState(false);

useEffect(() => {
const sectionEl = sectionRef.current;

if (sectionEl) {
setIsScrollActive(sectionEl.scrollWidth > sectionEl.clientWidth);
}
}, []);

return (
<S.Section ref={sectionRef}>
{regions.map(({ region }, index) => (
<RegionButton
key={region}
region={region}
isSelected={index === 0}
onRegionClick={onRegionClick}
/>
))}

<S.LinkWrap to={'/search'} $isScrollActive={isScrollActive}>
<S.PlusChip variant='outlined' label={<PlusIcon />} />
</S.LinkWrap>
</S.Section>
);
};

export default RegionSelector;
25 changes: 25 additions & 0 deletions src/pages/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,44 @@ import { decrement, increment } from '@/redux/slice/EXAMPLE_counterSlice';
import { getWeather } from '@/service/weather';
import { Button } from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import RegionSelector from './components/RegionSelector';
import { useState } from 'react';
import { Region } from '@/types/region';
import { MY_REGIONS } from '@/constants/localStorage/key';

const Home = () => {
const [regions, setRegions] = useState<Region[]>(
JSON.parse(localStorage.getItem(MY_REGIONS) || '[]')
);
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
const { data, isError } = useQuery({
queryKey: ['weather'],
queryFn: getWeather,
});

const handleRegionClick = (region: string) => {
setRegions((prev) => {
if (prev[0].region === region) {
return prev;
}

const list = [...prev];
const targetIndex = list.findIndex((v) => v.region === region);

[list[0], list[targetIndex]] = [list[targetIndex], list[0]];

localStorage.setItem(MY_REGIONS, JSON.stringify(list));

return list;
});
};

return (
<div>
<div>
<RegionSelector regions={regions} onRegionClick={handleRegionClick} />
<Button
variant='contained'
aria-label='Increment value'
Expand Down
35 changes: 31 additions & 4 deletions src/pages/Search/components/RegionItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import CheckIcon from '@/components/icon/Check';
import * as S from './RegionItem.style';
import { IconButton } from '@mui/material';
import { MY_REGIONS } from '@/constants/localStorage/key';
import { Region } from '@/types/region';

type RegionItemProps = {
region: string;
type RegionItemProps = Region & {
keyword: string;
myRegions: Region[];
setNewMyRegions: (newRegions: Region[]) => void;
};

const RegionItem = ({ region, keyword }: RegionItemProps) => {
//TODO: alert 제거
const RegionItem = ({
region,
keyword,
nx,
ny,
myRegions,
setNewMyRegions,
}: RegionItemProps) => {
const [before, match, after] = splitText(region, keyword);
const handleSaveClick = () => {
if (myRegions.some((item) => item.region === region)) {
alert('해당 지역이 이미 저장되어 있습니다.');
return;
}

if (myRegions.length >= 3) {
alert('3개 이상 저장할 수 없습니다.');
return;
}

const addRegion = [...myRegions, { region, nx, ny }];
setNewMyRegions(addRegion);
localStorage.setItem(MY_REGIONS, JSON.stringify(addRegion));
alert(`${region} 지역을 저장했습니다.`);
};

return (
<S.Item divider>
<S.Item divider onClick={handleSaveClick}>
<span>
{before}
<strong>{match}</strong>
Expand Down
Loading