튜터링 내용
- 기능만 보았을 때는 분량을 늘려야 한다
- 그렇다면 디자인 할 화면이 너무 많아진다. 화면 수를 줄이는 방법은?
- 기능 난이도를 높이는 방법도
- 주제를 더 명확하게 하자
- 기능을 먼저 기준으로 잡고 다음 화면을 정하는 방법도 있다
튜터링 내용에 따라 프로젝트 주제 구체화 , 기획 세분화 , 기능 분배
reference 조사
참조 사이트
https://www.myro.co.kr/planning/guam
https://www.stubbyplanner.com/
https://www.wishbeen.co.kr/main
https://mtravel.interpark.com/home
국내 관광지 정보 및 숙박, 행사 등의 정보를 이용하기 위해 TourAPI 사용
해당 정보의 위치를 활용하기 위해 kakaoMap API 사용
구현 코드
"use client";
import Image from "next/image";
import React, { useEffect, useState } from "react";
import xml2js from "xml2js";
interface Tour {
contentid: string;
title: string;
addr1: string;
firstimage?: string;
mapx: string;
mapy: string;
}
const HomePage: React.FC = () => {
const [tours, setTours] = useState<Tour[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const apiKey = 공공 데이터 포털 Key
const baseUrl = `http://apis.data.go.kr/B551011/KorService1/areaBasedList1?numOfRows=12&pageNo=1&MobileOS=ETC&MobileApp=AppTest&ServiceKey=${apiKey}&listYN=Y&arrange=A&contentTypeId=15&areaCode=1&sigunguCode=&cat1=A02&cat2=A0208&cat3=A02080200`;
const dateUrl = `http://apis.data.go.kr/B551011/KorService1/searchFestival1?eventStartDate=20240717&eventEndDate=20240730&ServiceKey=${apiKey}&listYN=Y&MobileOS=ETC&MobileApp=AppTest&arrange=A`;
const getTotalCount = async (url: string) => {
const res = await fetch(`${url}&numOfRows=1&pageNo=1`);
const text = await res.text();
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(text);
return parseInt(result.response.body[0].totalCount[0], 10);
};
const totalAreaBasedCount = await getTotalCount(baseUrl);
const totalFestivalCount = await getTotalCount(dateUrl);
const numOfRows = 12;
const totalAreaBasedPages = Math.ceil(totalAreaBasedCount / numOfRows);
const totalFestivalPages = Math.ceil(totalFestivalCount / numOfRows);
const fetchAllPages = async (url: string, totalPages: number) => {
const allData: any[] = [];
for (let page = 1; page <= totalPages; page++) {
const res = await fetch(
`${url}&numOfRows=${numOfRows}&pageNo=${page}`
);
const text = await res.text();
const parser = new xml2js.Parser();
const result = await parser.parseStringPromise(text);
const items = result.response.body[0].items[0].item;
allData.push(...items);
}
return allData;
};
const areaBasedItems = await fetchAllPages(
baseUrl,
totalAreaBasedPages
);
const festivalItems = await fetchAllPages(dateUrl, totalFestivalPages);
const festivalContentIds = new Set(
festivalItems.map((item: any) => item.contentid[0])
);
const filteredTours = areaBasedItems
.filter((item: any) => festivalContentIds.has(item.contentid[0]))
.map((item: any) => ({
contentid: item.contentid[0],
title: item.title[0],
addr1: item.addr1[0],
firstimage: item.firstimage
? item.firstimage[0].replace(/<\/?firstimage>/g, "")
: null,
mapx: item.mapx[0],
mapy: item.mapy[0],
}));
setTours(filteredTours);
} catch (error) {
if (error instanceof Error) {
setError(error.message);
} else {
setError("An unknown error occurred");
}
}
};
fetchData();
}, []);
useEffect(() => {
if (tours.length > 0) {
const script = document.createElement("script");
script.src = `//dapi.kakao.com/v2/maps/sdk.js?appkey=kakaoKey&autoload=false`;
script.async = true;
document.head.appendChild(script);
script.onload = () => {
if (window.kakao && window.kakao.maps) {
window.kakao.maps.load(() => {
const mapContainer = document.getElementById("map");
const mapOption = {
center: new window.kakao.maps.LatLng(37.5665, 126.978), // 지도의 중심좌표
level: 5,
};
const map = new window.kakao.maps.Map(mapContainer, mapOption);
tours.forEach((tour) => {
const markerPosition = new window.kakao.maps.LatLng(
tour.mapy,
tour.mapx
);
const marker = new window.kakao.maps.Marker({
position: markerPosition,
});
const infowindow = new window.kakao.maps.InfoWindow({
content: `<div style="padding:5px;">${tour.title}</div>`,
});
window.kakao.maps.event.addListener(marker, "mouseover", () =>
infowindow.open(map, marker)
);
window.kakao.maps.event.addListener(marker, "mouseout", () =>
infowindow.close()
);
marker.setMap(map);
});
});
}
};
return () => {
document.head.removeChild(script);
};
}
}, [tours]);
return (
<div>
<h1>Tour Information</h1>
{error ? (
<p>Error: {error}</p>
) : (
<ul className="ml-4 grid grid-cols-4">
{tours.map((tour) => (
<li key={tour.contentid}>
<h2 className="mt-4">{tour.title}</h2>
<p>{tour.addr1}</p>
{tour.firstimage && (
<Image
src={tour.firstimage}
alt={tour.title}
width="200"
height="200"
/>
)}
</li>
))}
</ul>
)}
<div
id="map"
style={{ width: "100%", height: "500px", marginTop: "20px" }}
></div>
</div>
);
};
export default HomePage;
실행 화면
서울에서 7/17기준 진행중인 연극만 가져와서 지도에 표시한 예시
위 코드를 활용하여 일정 관련과 메인 페이지 등 활용하면 긍정적인 결과 기대됨
회의 내용
오전
- 유저플로우 함께 확인하며 수정 작업
- 로그인을 하지 않아도 메인페이지는 볼 수 있도록
- 회원가입의 경우 비밀번호 찾기 등등 더 세부적인 화면이 필요할 것임
- 1차 기능 정리 및 분담
- 메인페이지
- 맞춤형 추천 기능 - 메인에서 간단하게 가능
- 로그인 / 회원가입
- 비밀번호 찾기 (후 순위 )
- 일정 페이지
- CRUD
- 좋아요 or 북마크 ( 후 순위 )
- 일정내에서 댓글 기능(CRUD)
- 커뮤니티에서만 실시간 채팅
- 캘린더 기능 ( 후 순위 )
- 결제 기능 ( 후 순위 )
- 장바구니
- 마이페이지
- 프로필 정보 수정
- 예약 리스트 확인 ( 후 순위 )
- 일정 리스트 확인
- 여행 디테일 페이지 ( 여행 상세정보 까지만 )
- 숙소,식당,카페 목록까지만
- 지도 어떻게 보여줄지는 미정
- 가게 상세페이지 ( 후 순위 )
오후
- 레퍼런스를 더 찾아보자.https://www.stubbyplanner.com/https://mtravel.interpark.com/home
- https://www.getyourguide.com/
- https://www.wishbeen.co.kr/main
- https://www.myro.co.kr/planning/guam
- 여행(국내) 패키지 기능 정리
- 로그인 / 회원가입
- 비밀번호 찾기
- 소셜로그인 0
- 백오피스(페이지)
- 투어 상세 페이지
- 좋아요
- 결제 기능
- 방문 위치 지도
- 글쓴이와 채팅버튼
- 일정 작성 페이지
- CRUD
- 지도 , 위치검색 ( TourAPI + kakaoMap )
- 일정별 할 일 목록 + 예상 금액
- 테마 해시태그 , 지역(태그) 날짜 등
- 메인페이지
- 레이아웃
- 캘린더 검색 기능
- 투어 리스트 ( 페이지네이션 ) - 인기순 최신순
- 테마, 제목, 지역 검색
- => 검색 결과 페이지
- 마이페이지
- 프로필 정보 수정
- 좋아요 리스트 확인
- 내가 작성한 일정 리스트 확인
- 결제한 일정 확인
- 1:1~1:다 실시간 채팅 - 모든 페이지에서 아이콘으로 채팅 가능
- 내일 할 일
- 디자이너 - 기획 논리 연결짓기, 유저 플로우 재작성