팀 노션 | 요구사항 정의서 | figma | 개발 위키
TriFly는 항공권 예약 사이트입니다. 다양한 검색 조건을 설정하여 나에게 맞는 항공권을 검색할 수 있습니다. 항공권 구매 후 좌석 지정도 가능합니다. 발권된 티켓을 나만의 티켓으로 커스텀하고 내 항공 여행 기록을 확인할 수 있습니다.
사용자의 환경을 고려하여 구현된 항공권 검색 UI를 이용하여 손쉽게 항공권을 검색할 수 있어요.
웹 | 모바일 |
---|---|
직항, 출발/도착 시간, 원하는 항공사, 가격대 등을 선택하여 내게 딱 맞는 항공편을 조회해보세요.
웹 | 모바일 |
---|---|
약관 동의부터 정보 입력, 결제까지 항공권 구매 프로세스를 경험할 수 있어요.
- 항공편 상세 조회 : 경유지 정보, 항공기 정보, 수하물 정보, 공동운항 정보
- 운임 상세 조회 : 탑승자 연령대 별 항공료, 유류할증료, 제세공과금 등의 요금 상세
웹 | 모바일 |
---|---|
탑승하는 항공기의 좌석 배치도를 확인하고 원하는 좌석을 선택할 수 있어요.
웹 | 모바일 |
---|---|
티켓을 커스텀하여 나만의 티켓을 만들어 보세요. '발자국' 메뉴에서 내 여행 기록도 확인할 수 있어요.
웹 | 모바일 |
---|---|
[ 로그인 / 회원가입 ]
- 이메일 로그인 및 회원가입
- 소셜 로그인 및 회원가입 (카카오, 구글)
[ 항공권 구매 ]
- 항공편 정보 및 약관 동의
- 탑승객 정보 입력 및 결제
- 예약 상세 내역 및 티켓 확인
[ 발자국 ]
- 여행 기록 통계 및 연도별 티켓 조회
- 포토 티켓 커스텀 및 저장
[ 공통 ]
- 항공권 검색 로딩
- 아코디언
- 프로그레스 바
- 포토 티켓
[ 항공편 좌석 선택 ]
- 항공편 기종별 좌석 렌더링
- 탑승자별 좌석 선택
[ 예약 내역 ]
- 예약 목록 조회
- 예약 상세 조회 및 E-Ticket 출력
[ 공통 ]
- 푸터
- 로딩
- 에러
[ 메인 ]
- 항공권 검색
- “나만의 티켓 꾸미기” 배너
[ 항공권 검색 결과 ]
- 항공권 검색 결과 조회
- 항공권 검색 결과 필터링
[ 공통 ]
- 헤더
- 버튼 & 링크
- 모달
분류 | 기술 스택 |
---|---|
프론트엔드 |
|
패키지 매니저 |
|
배포 |
|
협업 |
로그인 & 회원가입 |
---|
포토티켓 |
---|
항공권 구매 |
---|
- 공통으로 사용하는 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인 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 |