본문 바로가기

카테고리 없음

Fiesta Tour_2일차 ( 레퍼런스 조사 및 지도 코드 임시 구현 )

튜터링 내용

  • 기능만 보았을 때는 분량을 늘려야 한다
    • 그렇다면 디자인 할 화면이 너무 많아진다. 화면 수를 줄이는 방법은?
    • 기능 난이도를 높이는 방법도
  • 주제를 더 명확하게 하자
  • 기능을 먼저 기준으로 잡고 다음 화면을 정하는 방법도 있다

 

튜터링 내용에 따라 프로젝트 주제 구체화 , 기획 세분화 , 기능 분배

reference 조사

 

참조 사이트

https://www.myro.co.kr/planning/guam

 

MYRO - AI 여행 플래너

평균 10시간 이상이 소요되는 여행 일정 세부 스케줄링을 가고 싶은 장소만 장바구니에 담듯이 선택하면 인공지능이 최적의 일정을 자동으로 스케줄링 해주는 쉽고 간편한 AI 여행 플래너 서비

www.myro.co.kr

https://www.stubbyplanner.com/

 

상상속 유럽여행을 현실로, 스투비플래너

스투비플래너로 상상속 유럽여행을 현실로 만들어 보세요

www.stubbyplanner.com

https://www.wishbeen.co.kr/main

 

위시빈 ㅣ 150만개의 진짜 여행정보

프로 여행러들이 알려주는 국내·해외여행 꿀팁! 지도를 보며 쉽게 짜는 나만의 여행일정!

www.wishbeen.co.kr

https://mtravel.interpark.com/home

 

인터파크 투어

[W트립]보홀, 4/5/6일, 비그랜드 리조트+왕복픽업+호핑 또는 마사지 택1포함 완벽자유 3박5일인천 출발에어부산노쇼핑 판매가519,000원~

mtravel.interpark.com

https://www.getyourguide.com/

 

국내 관광지 정보 및 숙박, 행사 등의 정보를 이용하기 위해 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. 유저플로우 함께 확인하며 수정 작업
    • 로그인을 하지 않아도 메인페이지는 볼 수 있도록
    • 회원가입의 경우 비밀번호 찾기 등등 더 세부적인 화면이 필요할 것임
  2. 1차 기능 정리 및 분담
  • 메인페이지 
    • 맞춤형 추천 기능 - 메인에서 간단하게 가능
  • 로그인 / 회원가입 
    • 비밀번호 찾기 (후 순위 )
  • 일정 페이지
    • CRUD
    • 좋아요 or 북마크 ( 후 순위 )
    • 일정내에서 댓글 기능(CRUD)
    • 커뮤니티에서만 실시간 채팅 
    • 캘린더 기능 ( 후 순위 )
  • 결제 기능 ( 후 순위 )
    • 장바구니
  • 마이페이지 
    • 프로필 정보 수정
    • 예약 리스트 확인 ( 후 순위 )
    • 일정 리스트 확인
  • 여행 디테일 페이지 ( 여행 상세정보 까지만 ) 
    • 숙소,식당,카페 목록까지만
    • 지도 어떻게 보여줄지는 미정
    • 가게 상세페이지 ( 후 순위 )

오후

  1. 레퍼런스를 더 찾아보자.https://www.stubbyplanner.com/https://mtravel.interpark.com/home
  2. https://www.getyourguide.com/
  3. https://www.wishbeen.co.kr/main
  4. https://www.myro.co.kr/planning/guam
  • 여행(국내) 패키지 기능 정리 
    • 로그인 / 회원가입
    • 비밀번호 찾기
    • 소셜로그인 0
    • 백오피스(페이지)
  • 투어 상세 페이지 
    • 좋아요
    • 결제 기능
    • 방문 위치 지도
    • 글쓴이와 채팅버튼
  • 일정 작성 페이지 
    • CRUD
    • 지도 , 위치검색 ( TourAPI + kakaoMap )
    • 일정별 할 일 목록 + 예상 금액
    • 테마 해시태그 , 지역(태그) 날짜 등
  • 메인페이지
    • 레이아웃
    • 캘린더 검색 기능
    • 투어 리스트 ( 페이지네이션 ) - 인기순 최신순
    • 테마, 제목, 지역 검색
    • => 검색 결과 페이지
  • 마이페이지 
    • 프로필 정보 수정
    • 좋아요 리스트 확인
    • 내가 작성한 일정 리스트 확인
    • 결제한 일정 확인
    • 1:1~1:다 실시간 채팅 - 모든 페이지에서 아이콘으로 채팅 가능
  1. 내일 할 일
    • 디자이너 - 기획 논리 연결짓기, 유저 플로우 재작성