Skip to content

Commit

Permalink
Merge pull request #19 from TripMingle/feat/SWM-266
Browse files Browse the repository at this point in the history
[feat/SWM-266] 일정 모달창 개발하기
  • Loading branch information
kxmmxnzx authored Nov 7, 2024
2 parents 1f7e94f + aea7499 commit 7cc2322
Show file tree
Hide file tree
Showing 17 changed files with 643 additions and 14 deletions.
Binary file added public/icons/color_calendar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icons/map_flag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/api/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const getBoardDetail = async (boardId: number) => {
return getFetch(`/board?boardId=${boardId}`);
};

export const getBoardSchedule = async (boardId: number) => {
return getFetch(`/board/schedule?boardId=${boardId}`);
};

export const postBoard = async (data: BoardForm) => {
return postFetch(`/board`, data);
};
Expand Down
15 changes: 7 additions & 8 deletions src/app/[country]/board/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TripTypeButton } from '@/components/common/TripTypeButton';
import AttributeBox from '@/components/country/board/id/AttributeBox';
import { BoardDetailType } from '@/types/country/board';
import { headers } from 'next/headers';
import ScheduleButton from '@/components/country/board/id/ScheduleButton';

const Page = async ({
params,
Expand Down Expand Up @@ -110,14 +111,12 @@ const Page = async ({
) : (
<></>
)}
<div className={styles.infoContainer}>
<p className={styles.infoTitle}>여행 일정</p>
<div className={styles.infoContent}>
<span>
{boardDetail.startDate} ~ {boardDetail.endDate}
</span>
</div>
</div>
<ScheduleButton
startDate={boardDetail.startDate}
endDate={boardDetail.endDate}
boardId={boardDetail.boardId}
country={boardDetail.countryName}
/>
<div className={styles.infoContainer}>
<p className={styles.infoTitle}>내용</p>
<div className={styles.infoContent}>{boardDetail.content}</div>
Expand Down
18 changes: 18 additions & 0 deletions src/app/api/board/schedule/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getAccessToken } from '@/utils/server/token';
import { NextRequest } from 'next/server';

export const GET = async (req: NextRequest) => {
const baseurl = `${process.env.API_URL}`;
const pathname = `/board/schedule`;
const boardId = req.nextUrl.searchParams.get('boardId');
let accesstoken = await getAccessToken();
let token = accesstoken?.value || process.env.ACCESS_TOKEN;

return await fetch(`${baseurl}${pathname}/${boardId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
};
9 changes: 9 additions & 0 deletions src/components/common/ModalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ const ModalWrapper = ({ isOpen, closeModal, children }: Props) => {
closeModal();
};

useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = 'unset';
};
}
}, [isOpen]);

if (!isOpen) return <></>;

return createPortal(
Expand Down
56 changes: 56 additions & 0 deletions src/components/country/board/id/ScheduleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import useModal from '@/hooks/useModal';
import * as styles from '@/styles/country/board/id/schedule-button.css';
import Image from 'next/image';
import ScheduleModal from './ScheduleModal';

const ScheduleButton = ({
startDate,
endDate,
country,
boardId,
}: {
startDate: string;
endDate: string;
country: string;
boardId: number;
}) => {
const { isOpen, openModal, closeModal } = useModal();

const clickHandler = () => {
if (isOpen) closeModal();
else openModal();
};

return (
<div className={styles.infoContainer} onClick={clickHandler}>
<p className={styles.infoTitle}>여행 일정</p>
<button className={styles.scheduleButton}>
<div className={styles.dateContainer}>
<Image
className={styles.calendarIcon}
src="/icons/color_calendar.png"
width={18}
height={18}
alt="calendarIcon"
/>
<span className={styles.dateText}>
{startDate} ~ {endDate}
</span>
</div>
<div className={styles.explain}>눌러서 여행 일정 확인하기!</div>
</button>
<ScheduleModal
isOpen={isOpen}
closeModal={clickHandler}
boardId={boardId}
startDate={startDate}
endDate={endDate}
country={country}
/>
</div>
);
};

export default ScheduleButton;
31 changes: 31 additions & 0 deletions src/components/country/board/id/ScheduleList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ScheduleListItem from './ScheduleListItem';
import * as styles from '@/styles/write/thirdstep/schedule-list.css';
import { SchedulePlaceType } from '@/types/country/place';

type Props = {
placeList: { [key: string]: SchedulePlaceType[] };
mapHandler: (date: string, index: number) => void;
dateArray: string[];
};

const ScheduleList = ({ placeList, mapHandler, dateArray }: Props) => {
return (
<div className={styles.container}>
<ul className={styles.listContainer}>
{dateArray.map((date, index) => (
<li className={styles.listItemContainer} key={date}>
<span className={styles.listItemLine}></span>
<ScheduleListItem
day={index + 1}
date={date}
data={placeList[date]}
mapHandler={mapHandler}
/>
</li>
))}
</ul>
</div>
);
};

export default ScheduleList;
67 changes: 67 additions & 0 deletions src/components/country/board/id/ScheduleListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Image from 'next/image';
import * as styles from '@/styles/write/thirdstep/schedule-list-item.css';
import { SchedulePlaceType } from '@/types/country/place';
import { container, place } from '@/styles/write/thirdstep/schedule-place.css';

type Props = {
date: string;
day: number;
data: SchedulePlaceType[];
mapHandler: (date: string, index: number) => void;
};

const ScheduleListItem = ({ date, day, data, mapHandler }: Props) => {
return (
<div className={styles.container}>
<div className={styles.dateContainer}>
<span className={styles.dayCircle}></span>
<div className={styles.dayContainer}>
<Image
className={styles.airplaneIcon}
src="/icons/write_airplane.svg"
width={16}
height={16}
alt="airplane_icon"
/>
<span>Day {day}</span>
</div>
<span className={styles.date}>{date}</span>
<div className={styles.mapButton} onClick={() => mapHandler(date, -1)}>
<Image
className={styles.mapIcon}
src="/icons/mini_map.svg"
width={15}
height={15}
alt="mapIcon"
/>
<span>지도</span>
</div>
</div>
<div className={styles.schedulePlaceContainer}>
<ul className={styles.listOrderContainer}>
{data.map((element, index) => (
<li className={styles.listOrderItem} key={element.boardScheduleId}>
<span className={styles.listLine}></span>
<span className={styles.listNumber}>{index + 1}</span>
</li>
))}
</ul>

<ul className={styles.placeListContainer}>
{data.map((element, index) => (
<li
key={element.boardScheduleId}
onClick={() => mapHandler(date, index)}
>
<div className={container}>
<span className={place}>{element.placeName}</span>
</div>
</li>
))}
</ul>
</div>
</div>
);
};

export default ScheduleListItem;
135 changes: 135 additions & 0 deletions src/components/country/board/id/ScheduleMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import {
GoogleMap,
Libraries,
LoadScript,
OverlayView,
} from '@react-google-maps/api';
import { SchedulePlaceType } from '@/types/country/place';
import ScheduleMapMarker from '@/components/write/board/thirdstep/ScheduleMapMarker';
import { mapContainer } from '@/styles/country/board/id/schedule-modal.css';

type Props = {
placeList: { [key: string]: SchedulePlaceType[] };
date: string;
index: number;
country: string;
};

const libraries: Libraries = ['places'];

const ScheduleMap = ({ placeList, date, index, country }: Props) => {
const mapRef = useRef<google.maps.Map | null>(null);
const [countryCenter, setCountryCenter] = useState<google.maps.LatLngLiteral>(
{ lat: 0, lng: 0 },
);
const [center, setCenter] = useState<google.maps.LatLngLiteral>({
lat: 0,
lng: 0,
});
const [markers, setMarkers] = useState<google.maps.LatLngLiteral[]>([]);
const [zoom, setZoom] = useState<number>(6);

const onLoad = useCallback((map: google.maps.Map) => {
getCountryLocation();
mapRef.current = map;
}, []);

const defaultMapContainerStyle = {
width: '100%',
height: '100%',
};

const getCountryLocation = () => {
const geocoder = new window.google.maps.Geocoder();
// 나라 중심으로 처음에 지도 설정
geocoder.geocode({ address: country }, (results, status) => {
if (status === 'OK' && results && results[0]) {
const location = results[0].geometry.location;
setCountryCenter({
lat: location.lat(),
lng: location.lng(),
});
} else {
console.error('Geocode failed: ' + status);
}
});
};

useEffect(() => {
if (placeList[date] !== undefined) {
const dateMarkers = placeList[date].map((e) => {
return { lat: e.pointX, lng: e.pointY };
});
setMarkers(dateMarkers);
}
}, [date, placeList[date]]);

useEffect(() => {
let zoom = 15;
let lat = countryCenter.lat;
let lng = countryCenter.lng;

if (index === -1) {
// 지도 버튼 클릭했을 때 index = -1
index = 0;
zoom = 6;
}

if (placeList[date] !== undefined) {
if (placeList[date][index]) {
lat = placeList[date][index].pointX;
lng = placeList[date][index].pointY;
} else {
zoom = 6;
}
}
setZoom(zoom);
setCenter({ lat, lng });
}, [date, index]);

useEffect(() => {
if (mapRef.current) {
mapRef.current.setZoom(zoom);
}
}, [zoom]);

useEffect(() => {
if (mapRef.current) {
mapRef.current.panTo(center);
}
}, [center]);

return (
<LoadScript
googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAP_API_KEY || ''}
libraries={libraries}
>
<div className={mapContainer}>
<GoogleMap
mapContainerStyle={defaultMapContainerStyle}
center={countryCenter}
zoom={6}
onLoad={onLoad}
options={{
mapTypeControl: false,
fullscreenControl: false,
streetViewControl: false,
}}
>
{markers.map((marker, index) => (
<OverlayView
key={index}
position={marker}
mapPaneName={OverlayView.OVERLAY_MOUSE_TARGET}
>
<ScheduleMapMarker index={index} />
</OverlayView>
))}
</GoogleMap>
</div>
</LoadScript>
);
};

export default ScheduleMap;
Loading

0 comments on commit 7cc2322

Please sign in to comment.