Skip to content

JWJung-99/trifly

 
 

Repository files navigation

TriFly

Frame 30

배포 링크 https://trifly.vercel.app


팀 노션   |   요구사항 정의서   |   figma   |   개발 위키

📄 목차


✍🏻 프로젝트 개요

TriFly는 항공권 예약 사이트입니다. 다양한 검색 조건을 설정하여 나에게 맞는 항공권을 검색할 수 있습니다. 항공권 구매 후 좌석 지정도 가능합니다. 발권된 티켓을 나만의 티켓으로 커스텀하고 내 항공 여행 기록을 확인할 수 있습니다.


🚀 핵심 기능

어떤 기기로든 편리하게 항공권을 검색할 수 있어요.

사용자의 환경을 고려하여 구현된 항공권 검색 UI를 이용하여 손쉽게 항공권을 검색할 수 있어요.

모바일

원하는 정보를 바로 필터링할 수 있어요.

직항, 출발/도착 시간, 원하는 항공사, 가격대 등을 선택하여 내게 딱 맞는 항공편을 조회해보세요.

모바일

선택한 항공권을 구매할 수 있어요.

약관 동의부터 정보 입력, 결제까지 항공권 구매 프로세스를 경험할 수 있어요.

  • 항공편 상세 조회 : 경유지 정보, 항공기 정보, 수하물 정보, 공동운항 정보
  • 운임 상세 조회 : 탑승자 연령대 별 항공료, 유류할증료, 제세공과금 등의 요금 상세
모바일

항공편에 따라 원하는 좌석을 선택할 수 있어요.

탑승하는 항공기의 좌석 배치도를 확인하고 원하는 좌석을 선택할 수 있어요.

모바일

나만의 티켓을 꾸미고 기록을 확인할 수 있어요.

티켓을 커스텀하여 나만의 티켓을 만들어 보세요. '발자국' 메뉴에서 내 여행 기록도 확인할 수 있어요.

모바일

💻 담당 기능

이소정

[ 로그인 / 회원가입 ]

  • 이메일 로그인 및 회원가입
  • 소셜 로그인 및 회원가입 (카카오, 구글)

[ 항공권 구매 ]

  • 항공편 정보 및 약관 동의
  • 탑승객 정보 입력 및 결제
  • 예약 상세 내역 및 티켓 확인

[ 발자국 ]

  • 여행 기록 통계 및 연도별 티켓 조회
  • 포토 티켓 커스텀 및 저장

[ 공통 ]

  • 항공권 검색 로딩
  • 아코디언
  • 프로그레스 바
  • 포토 티켓

전희선

[ 항공편 좌석 선택 ]

  • 항공편 기종별 좌석 렌더링
  • 탑승자별 좌석 선택

[ 예약 내역 ]

  • 예약 목록 조회
  • 예약 상세 조회 및 E-Ticket 출력

[ 공통 ]

  • 푸터
  • 로딩
  • 에러

정진욱

[ 메인 ]

  • 항공권 검색
  • “나만의 티켓 꾸미기” 배너

[ 항공권 검색 결과 ]

  • 항공권 검색 결과 조회
  • 항공권 검색 결과 필터링

[ 공통 ]

  • 헤더
  • 버튼 & 링크
  • 모달

⚙️ 기술 스택

분류 기술 스택

프론트엔드

패키지 매니저

배포

협업


🏛️ 서비스 구조

요구사항 정의서

요구사항 정의서

플로우 차트

로그인 & 회원가입
플로우 차트
포토티켓
플로우 차트
항공권 구매
플로우 차트

🔎 FE 기술적 도전

반응형 UI

  • 공통으로 사용하는 CSS variable을 등록해 사용하면서 스타일 변경사항을 한 번에 적용 가능하였고, 반응형 작업을 수월하게 진행할 수 있었습니다.
  • rem, vw 등 상대적 단위를 사용하여 디바이스 너비에 유연하게 대처했습니다.
responsive.scss
:root {
  --header-height: 7.2rem;

  --layout-pc: 1080px;
  --layout-padding: 3rem --layout-header-bottom: 4rem;

  --title-max: 2.4rem;
  --title-extra: 2rem;
  --title-large: 1.8rem;
  --title-big: 1.6rem;
  --title-medium: 1.4rem;
  --title-small: 1.2rem;
  --title-min: 1rem;
}

@media (max-width: 1023px) {
  :root {
    --layout-padding: 1.6rem;
    --header-height: 5.6rem;

    --title-max: 2rem;
    --title-extra: 1.8rem;
    --title-large: 1.6rem;
    --title-big: 1.4rem;
    --title-medium: 1.2rem;
    --title-small: 1.1rem;
    --title-min: 0.8rem;

    min-width: 320px;
  }

  .mo {
    display: block;
  }

  .pc {
    display: none;
  }
}
반응형

타입 지정

  • 오픈 마켓 api, 아마데우스 api 요청과 응답의 타입을 미리 지정하였습니다.
  • 사용하는 api의 응답 데이터의 depth가 깊고, 양이 방대한 상황에서 코드를 작성할 때부터 지정된 타입을 확인하여 에러를 미연에 방지하고, 효율적인 코드를 작성할 수 있었습니다.
  • 개발 전에 반복적으로 사용되는 데이터의 타입을 따로 지정 후 extends, Pick, Omit 등의 키워드를 활용하여 확장성과 재사용성을 높였습니다.
아마데우스 데이터 타입 지정
타입 자동 완성 타입 오류 방지

아마데우스 api 사용

아마데우스에서 제공하는 무료 api인 self-service api를 활용하였습니다.

아마데우스

  • 여행, 항공 업계에서 세계 최대 규모의 발권 시스템 제작 기업으로, 전 세계 120여개 항공사에서 사용 중입니다.
  • 우리나라에서 대표적으로 대한항공과 아시아나항공에서 이용하고 있습니다.
아마데우스 토큰
const fetchAuth = async (): Promise<string> => {
  const url = `${AMADEUS_API_SERVER}/v1/security/oauth2/token`;
  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: AMADEUS_CLIENT_ID,
      client_secret: AMADEUS_CLIENT_SECRET,
    }),
  });

  const resJson: ITokenSuccess = await res.json();

  if (!resJson.access_token) {
    throw new Error("Amadeus access_token을 불러올 수 없습니다!");
  }

  const accessToken = resJson.access_token;

  return accessToken;
};

항공권 검색 (flight-offers-search)

검색 조건을 query 에 담아 api를 호출하였습니다.

const fetchTicketSearch = async (
  query: string,
): Promise<OffersSearchData[]> => {
  let accessToken = await fetchAuth();

  const url = `${AMADEUS_API_SERVER}/v2/shopping/flight-offers?${query}`;

  try {
    const res = await fetch(url, {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
      next: { revalidate: 0 },
    });

    if (res.status === 401) {
      accessToken = await fetchAuth();

      const reRes = await fetch(url, {
        headers: {
          Authorization: `Bearer ${accessToken}`,
        },
        next: { revalidate: 0 },
      });

      const reResJson: OffersSearch = await reRes.json();

      if (!reResJson.meta) {
        throw new Error("검색에 실패했습니다.");
      }

      return reResJson.data;
    }

    const resJson: OffersSearch = await res.json();

    if (!resJson.meta) {
      throw new Error("검색에 실패했습니다.");
    }

    return resJson.data;
  } catch (e) {
    console.error(e);
    throw new Error("오류가 발생했습니다.");
  }
};

항공편 상세 조회 (flight-offers-pricing)

서버 액션을 생성하여 사용자가 선택한 항공권에 대한 여정, 가격 등 상세 정보를 요청하였습니다.

const flightPriceAction = async (
  flightOffers: OffersSearchData,
): Promise<TravelerPricing[]> => {
  let accessToken = await fetchAuth();
  const url = `${AMADEUS_API_SERVER}/v1/shopping/flight-offers/pricing`;

  const request = {
    data: {
      type: "flight-offers-pricing",
      flightOffers: [flightOffers],
    },
  };

  try {
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-HTTP-Method-Override": "GET",
        Authorization: `Bearer ${accessToken}`,
      },
      body: JSON.stringify(request),
    });

    if (res.status === 401) {
      accessToken = await fetchAuth();

      const reRes = await fetch(url, {
        headers: {
          "Content-Type": "application/json",
          "X-HTTP-Method-Override": "GET",
          Authorization: `Bearer ${accessToken}`,
        },
        body: JSON.stringify(flightOffers),
      });

      const reResJson: OffersPrice = await reRes.json();

      if (!reResJson.data) {
        throw new Error("에러가 발생했습니다.");
      }

      return reResJson.data.flightOffers[0].travelerPricings;
    }

    const resJson: OffersPrice = await res.json();
    if (!resJson.data) {
      throw new Error("에러가 발생했습니다.");
    }

    return resJson.data.flightOffers[0].travelerPricings;
  } catch (e) {
    console.error(e);
    throw new Error("오류가 발생했습니다.");
  }
};

항공권 조회 항공권 상세
  • 한계
    • self-service api의 경우 배포용 prod 버전에서는 예약 내역 생성, 실시간 좌석 배치도 불러오기 기능을 사용하기 위해 공급업체와 계약을 체결해야 하는 어려움이 있었습니다.
    • 따라서 test 계정으로 좌석 조회 api를 호출하여 seatmap 객체를 생성해두고 동적으로 좌석 배치도를 불러와 사용하였습니다.
seatmap 데이터

항공권 필터링

  • 조건을 설정하여 api로부터 받아온 항공권 데이터를 정렬 및 필터링하였습니다.
    • 검색 조건 : 경유 여부, 출발/도착 시간, 항공 동맹체 및 항공사
    • 정렬 조건 : 가격, 출발시간, 도착시간
항공권 필터링 로직

useCallback hook으로 filter를 setting하는 함수를 메모이제이션하여 컴포넌트가 렌더링되더라도 함수가 초기화되는 것을 방지하였습니다.

const handleFilterChange = useCallback((newFilters: IFilterProps) => {
  setFilters((prevFilters) => ({
    ...prevFilters,
    ...newFilters,
  }));
}, []);

filtering할 항목이 변경되면 기존 데이터 배열에서 filter 메서드를 이용하여 조건에 맞는 항공편을 찾아 상태를 업데이트하였습니다.

const applyFilters = () => {
  let newFilteredData = [...data];

  if (filters) {
    if (filters."필터") {
      const airlines = filters."필터";
      newFilteredData = newFilteredData.filter((offer) =>
        offer.itineraries.every((itinerary) =>
          itinerary.segments.every((segment) =>
            "비교 로직"
          )
        )
      );
    }
  }

  setFilteredData(newFilteredData);
}
항공권 필터링

좌석 배치도

  • 항공기 좌석 배열(예: 3-4-3, 2-3 등)과 날개 좌석, 비상구, 화장실, 갤리 등 시설 정보를 동적으로 받아 좌석 배치도를 구현하였습니다.
    • 기내 x, y 좌표를 사용해 정확한 위치를 시각적으로 배치하여 구체적인 레이아웃을 제공합니다.
  • 탑승자의 좌석을 배열로 관리하였습니다.
    • 여러 탑승객이 선택한 좌석을 상태 값으로 관리하여 좌석을 취소하거나 재선택하여도 해당 탑승객에게 좌석이 반영되도록 구현하였습니다.
좌석배치도

웹 접근성 고려

  • 헤딩 태그의 계층적 사용 및 시맨틱 태그 사용을 통해 데이터를 그룹핑하여 전달력을 높였습니다.
웹 접근성 고려
  • 아코디언 컴포넌트에 aria-controls, aria-expanded 속성을 활용하여 아코디언의 제어 상태를 스크린 리더기에서 확인할 수 있도록 하였습니다.

캔버스

  • 캔버스 한 획 지우기

    • 획을 그릴 때마다 배열에 담고, 한 획 지우기를 누르면 마지막 획을 배열에서 제거했습니다.
    • 남은 획들을 다시 캔버스에 그림으로서 한 획 지우기를 구현하였습니다.
  • 캔버스에서 꾸민 영역을 티켓과 사진 두 가지로 분류해서 저장하였습니다.

    • 수정된 이미지가 삽입된 티켓 전체를 파일로 저장할 수 있습니다.
    • 수정한 이미지를 구매 내역(order)에도 저장하여 추후에도 확인이 가능하도록 구현했습니다.
한 획 지우기 커스텀한 티켓 저장하기

🧡 팀원 소개

이소정 전희선 정진욱
@s0zzang @heesun2 @JWJung-99

About

항공 예약 시스템

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 92.0%
  • SCSS 8.0%