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

feat: 예약 가능 시간에 따라 맵에서 공간을 활성화/비활성화로 보이게 하는 기능 구현 #899

Merged
merged 2 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions frontend/src/components/Switch/Switch.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const SwitchButton = styled.button<{ selected: boolean }>`
font-size: 1rem;
background-color: ${({ theme, selected }) => (selected ? theme.primary[500] : theme.gray[100])};
color: ${({ theme, selected }) => (selected ? theme.white : theme.black[500])};
cursor: pointer;

&:last-of-type {
margin-right: 0;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/constants/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { ReactNode } from 'react';
import GuestMain from 'pages/GuestMain/GuestMain';
import GuestMapContainer from 'pages/GuestMap/GuestMapContainer';
import ManagerMapList from 'pages/ManagerMapList/ManagerMapList';
import PATH from './path';

Expand Down Expand Up @@ -55,7 +56,7 @@ export const PUBLIC_ROUTES: Route[] = [
},
{
path: PATH.GUEST_MAP,
component: <GuestMap />,
component: <GuestMapContainer />,
},
{
path: PATH.GUEST_RESERVATION,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useTimePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { MouseEventHandler, useState } from 'react';
import { Step } from 'components/TimePicker/TimePicker';
import { Midday, Range, Time } from 'types/time';

type SelectedTime = keyof Range | null;
export type SelectedTime = keyof Range | null;

interface Props {
initialStartTime?: Date;
Expand Down
41 changes: 0 additions & 41 deletions frontend/src/pages/GuestMap/GuestMap.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,6 @@ export const MapContainer = styled.div`
position: relative;
`;

export const MapItem = styled.div<{ width: number; height: number }>`
flex: 1;
padding: 1.5rem;
min-width: ${({ width }) => width}px;
min-height: ${({ height }) => height}px;
display: flex;
justify-content: center;
align-items: center;

@media (max-width: ${({ width }) => width}px) {
position: absolute;
top: 0;
left: 0;
}
`;

export const Space = styled.g`
cursor: pointer;
`;

export const SpaceRect = styled.rect`
&:hover {
opacity: 0.5;
}
`;

export const SpacePolygon = styled.polygon`
&:hover {
opacity: 0.5;
}
`;

export const SpaceAreaText = styled.text`
dominant-baseline: middle;
text-anchor: middle;
fill: ${({ theme }) => theme.black[700]};
font-size: 1rem;
pointer-events: none;
user-select: none;
`;

export const SelectBox = styled.div`
display: flex;
flex-direction: column;
Expand Down
165 changes: 44 additions & 121 deletions frontend/src/pages/GuestMap/GuestMap.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
import { AxiosError } from 'axios';
import dayjs, { Dayjs } from 'dayjs';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useMutation } from 'react-query';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { deleteGuestReservation } from 'api/guestReservation';
import Header from 'components/Header/Header';
import { EDITOR } from 'constants/editor';
import MESSAGE from 'constants/message';
import PALETTE from 'constants/palette';
import PATH, { HREF } from 'constants/path';
import useGuestMap from 'hooks/query/useGuestMap';
import { HREF } from 'constants/path';
import useGuestReservations from 'hooks/query/useGuestReservations';
import useGuestSpaces from 'hooks/query/useGuestSpaces';
import { AccessTokenContext } from 'providers/AccessTokenProvider';
import { Area, MapDrawing, MapItem, Reservation, ScrollPosition, Space } from 'types/common';
import { DrawingAreaShape } from 'types/editor';
import { Area, MapItem, Reservation, ScrollPosition, Space } from 'types/common';
import { GuestPageURLParams } from 'types/guest';
import { ErrorResponse } from 'types/response';
import { formatDate } from 'utils/datetime';
import { getPolygonCenterPoint } from 'utils/editor';
import { isNullish } from 'utils/type';
import * as Styled from './GuestMap.styles';
import { GuestMapFormContext } from './providers/GuestMapFormProvider';
import Aside from './units/Aside';
import GuestMapDrawing from './units/GuestMapDrawing';
import LoginPopup from './units/LoginPopup';
import PasswordInputModal from './units/PasswordInputModal';

export const SWITCH_LABEL_LIST = ['예약하기', '예약현황'];

export interface GuestMapState {
spaceId?: Space['id'];
targetDate?: Dayjs;
scrollPosition?: ScrollPosition;
}

const GuestMap = (): JSX.Element => {
interface GuestMapProps {
map: MapItem;
}

const GuestMap = ({ map }: GuestMapProps): JSX.Element => {
const { accessToken } = useContext(AccessTokenContext);
const { setSelectedSpaceId: setSelectedSpaceIdForm } = useContext(GuestMapFormContext);

const [passwordInputModalOpen, setPasswordInputModalOpen] = useState(false);
const [selectedReservation, setSelectedReservation] = useState<Reservation>();
Expand All @@ -49,34 +53,16 @@ const GuestMap = (): JSX.Element => {
const targetDate = location.state?.targetDate;
const scrollPosition = location.state?.scrollPosition;

const [map, setMap] = useState<MapItem | null>(null);
const mapDrawing = map?.mapDrawing;
useGuestMap(
{ sharingMapId },
{
onError: () => {
history.replace(PATH.NOT_FOUND);
},
onSuccess: (response) => {
const mapData = response.data;

try {
setMap({
...mapData,
mapDrawing: JSON.parse(mapData.mapDrawing) as MapDrawing,
});
} catch (error) {
alert(MESSAGE.GUEST_MAP.MAP_DRAWING_PARSE_ERROR);
}
},
retry: false,
}
);
const mapDrawing = map.mapDrawing;

const [spaceList, setSpaceList] = useState<Space[]>([]);
const [selectedSpaceId, setSelectedSpaceId] = useState<Space['id'] | null>(spaceId ?? null);
const [date, setDate] = useState(targetDate ? dayjs(targetDate).tz() : dayjs().tz());

const [selectedSwitchLabel, setSelectedSwitchLabel] = useState<typeof SWITCH_LABEL_LIST[number]>(
SWITCH_LABEL_LIST[0]
);

const spaces = useMemo(() => {
const result: { [key: string]: Space } = {};
spaceList.forEach((item) => (result[item.id] = item));
Expand All @@ -85,9 +71,9 @@ const GuestMap = (): JSX.Element => {
}, [spaceList]);

useGuestSpaces(
{ mapId: map?.mapId as number },
{ mapId: map.mapId },
{
enabled: map?.mapId !== undefined,
enabled: map.mapId !== undefined,
onSuccess: (response) => {
const { spaces } = response.data;

Expand All @@ -102,7 +88,7 @@ const GuestMap = (): JSX.Element => {

const getReservations = useGuestReservations(
{
mapId: map?.mapId as number,
mapId: map.mapId,
spaceId: selectedSpaceId as number,
date: formatDate(date),
},
Expand All @@ -123,8 +109,16 @@ const GuestMap = (): JSX.Element => {
},
});

const handleClickSwitch = (label: typeof SWITCH_LABEL_LIST[number]) => {
setSelectedSwitchLabel(label);
};

const handleClickSpaceArea = (spaceId: number) => {
setSelectedSpaceId(spaceId);
if (selectedSwitchLabel === SWITCH_LABEL_LIST[0]) {
setSelectedSpaceIdForm?.(`${spaceId}`);
} else if (selectedSwitchLabel === SWITCH_LABEL_LIST[1]) {
setSelectedSpaceId(spaceId);
}
};

const handleEdit = (reservation: Reservation) => {
Expand All @@ -133,7 +127,7 @@ const GuestMap = (): JSX.Element => {
history.push({
pathname: HREF.GUEST_RESERVATION_EDIT(sharingMapId),
state: {
mapId: map?.mapId,
mapId: map.mapId,
spaceId: spaces[selectedSpaceId].id,
reservation,
selectedDate: formatDate(date),
Expand All @@ -143,22 +137,22 @@ const GuestMap = (): JSX.Element => {
};

const deleteLoginReservation = (reservationId: number) => {
if (typeof map?.mapId !== 'number' || selectedSpaceId === null) return;
if (typeof map.mapId !== 'number' || selectedSpaceId === null) return;

if (!window.confirm(MESSAGE.GUEST_MAP.DELETE_CONFIRM)) return;

removeReservation.mutate({
mapId: map?.mapId,
mapId: map.mapId,
spaceId: selectedSpaceId,
reservationId: reservationId,
});
};

const handleDeleteGuestReservation = (passwordInput: string) => {
if (typeof map?.mapId !== 'number' || selectedSpaceId === null) return;
if (typeof map.mapId !== 'number' || selectedSpaceId === null) return;

removeReservation.mutate({
mapId: map?.mapId,
mapId: map.mapId,
spaceId: selectedSpaceId,
password: passwordInput,
reservationId: Number(selectedReservation?.id),
Expand All @@ -180,7 +174,7 @@ const GuestMap = (): JSX.Element => {
history.push({
pathname: HREF.GUEST_RESERVATION(sharingMapId),
state: {
mapId: map?.mapId,
mapId: map.mapId,
spaceId: spaces[selectedSpaceId].id,
selectedDate: formatDate(date),
scrollPosition: { x: mapRef?.current?.scrollLeft, y: mapRef?.current?.scrollTop },
Expand All @@ -205,87 +199,16 @@ const GuestMap = (): JSX.Element => {
}, [targetDate]);

return (
<Styled.Page>
{map && <Aside map={map} />}
<>
<Aside map={map} selectedLabel={selectedSwitchLabel} onClickSwitch={handleClickSwitch} />
<Styled.MapContainer ref={mapRef}>
<Header onClickLogin={() => setLoginPopupOpen(true)} />
{mapDrawing && (
<Styled.MapItem width={mapDrawing.width} height={mapDrawing.height}>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width={mapDrawing.width}
height={mapDrawing.height}
>
{/* Note: 맵을 그리는 부분 */}
{mapDrawing.mapElements.map((element) =>
element.type === 'polyline' ? (
<polyline
key={`polyline-${element.id}`}
points={element.points.join(' ')}
stroke={element.stroke}
strokeWidth={EDITOR.STROKE_WIDTH}
strokeLinecap="round"
/>
) : (
<rect
key={`rect-${element.id}`}
x={element?.x}
y={element?.y}
width={element?.width}
height={element?.height}
stroke={element.stroke}
fill="none"
strokeWidth={EDITOR.STROKE_WIDTH}
/>
)
)}

{/* Note: 공간을 그리는 부분 */}
{spaceList.length > 0 &&
spaceList.map(({ id, area, color, name }) => (
<Styled.Space
key={`area-${id}`}
data-testid={id}
onClick={() => handleClickSpaceArea(id)}
>
{area.shape === DrawingAreaShape.Rect && (
<>
<Styled.SpaceRect
x={area.x}
y={area.y}
width={area.width}
height={area.height}
fill={color ?? PALETTE.RED[200]}
opacity="0.3"
/>
<Styled.SpaceAreaText
x={area.x + area.width / 2}
y={area.y + area.height / 2}
>
{name}
</Styled.SpaceAreaText>
</>
)}
{area.shape === DrawingAreaShape.Polygon && (
<>
<Styled.SpacePolygon
points={area.points.map(({ x, y }) => `${x},${y}`).join(' ')}
fill={color ?? PALETTE.RED[200]}
opacity="0.3"
/>
<Styled.SpaceAreaText
x={getPolygonCenterPoint(area.points).x}
y={getPolygonCenterPoint(area.points).y}
>
{name}
</Styled.SpaceAreaText>
</>
)}
</Styled.Space>
))}
</svg>
</Styled.MapItem>
<GuestMapDrawing
mapDrawing={mapDrawing}
spaceList={spaceList}
onClickSpaceArea={handleClickSpaceArea}
/>
)}
</Styled.MapContainer>

Expand All @@ -302,7 +225,7 @@ const GuestMap = (): JSX.Element => {
onLogin={handleLogin}
/>
)}
</Styled.Page>
</>
);
};

Expand Down
Loading