Story(HotelStory) CMS API 연동 개발 가이드

Published: by Creative Commons Licence

Story(HotelStory) CMS API 연동 개발 가이드

HotelStory HUB와 자사 플랫폼 간의 외부 API 연동 개발 내용을 정리한 문서입니다.
Spring Boot 기반으로 구현되었으며, XML/JSON 혼용 방식으로 통신합니다.


시스템 개요

항목 내용
연동 대상 HotelStory HUB CMS
Story API Endpoint application properties에 설정 (비공개)
자사 서버 포트 application properties에 설정 (비공개)
인증 방식 XML 요청 본문 내 <Auth> 요소 포함 (AuthId / AuthKey)
인증 정보 설정 파일 또는 환경 변수에서 관리 (비공개)
DB MariaDB
이미지 스토리지 Naver Cloud Object Storage (S3 호환)
데이터 형식 XML (객실/요금/가용 Push), JSON (예약 관련)

공통 인증

모든 Story API 요청에는 XML 본문에 <Auth> 요소가 포함되어야 합니다.

<Auth>
  <AuthId>{설정값}</AuthId>
  <AuthKey>{설정값}</AuthKey>
</Auth>
  • StoryCallAuth: Story로 보내는 요청(예약 생성·수정·취소)의 Base 클래스
  • RequestCommonAuth: Story에서 받는 요청(객실·요금 조회, 가용·요금 Push)의 Base 클래스
  • 자사 내부 API 간 인증은 JWT 토큰 사용 (@NoAuthCheck 어노테이션으로 우회 가능)

API 목록

CMS Controller (/api/cms)

No API 명 Method Endpoint 방향 형식
5 객실 정보 API POST /api/cms/room-types HUB → 자사 XML
6 상품(요금제) 정보 API POST /api/cms/rate-plans HUB → 자사 XML
7.1.2 가용객실 수신 API POST /api/cms/push-availability HUB → 자사 XML
7.1.3 요금 수신 API POST /api/cms/push-rate HUB → 자사 XML
8.1 신규예약 API POST /api/cms/reservation-create 자사 → HUB JSON
8.2 예약수정 API PUT /api/cms/reservation-modify 자사 → HUB JSON
8.3 예약취소 API POST /api/cms/reservation-cancel 자사 → HUB JSON
9 예약조회 API POST /api/cms/reservation 자사 → HUB JSON
- 예약 목록조회(대사) GET /api/cms/reservation/sync 내부 조회 JSON

GSA Controller (/api/gsa)

No API 명 Method Endpoint 방향 형식
- 총판 숙소 업데이트 GET /api/gsa/upsert-gsa Story → DB 동기화 XML

API별 처리 프로세스


API 5 - 객실 정보 API POST /api/cms/room-types [XML]

HUB가 자사 플랫폼에 숙소의 객실 유형 목록을 조회하는 API입니다.

Request: RequestRoomTypeList

<RequestRoomTypeList>
  <Auth>
    <AuthId>{설정값}</AuthId>
    <AuthKey>{설정값}</AuthKey>
  </Auth>
  <PropertyId>{숙소코드}</PropertyId>
</RequestRoomTypeList>

처리 흐름

HUB → POST /api/cms/room-types
  → CmsController.getRoomTypesXML(RequestRoomTypeList)
  → CmsService.getRoomTypeByProductId(propertyId)
  → DB 조회 (story_room_type, product_room_mapper 등)
  → ResponseRoomTypeList 반환 (XML)

API 6 - 상품(요금제) 정보 API POST /api/cms/rate-plans [XML]

HUB가 자사 플랫폼에 숙소의 요금제 목록을 조회하는 API입니다.

Request: RequestRatePlanList

<RequestRatePlanList>
  <Auth>
    <AuthId>{설정값}</AuthId>
    <AuthKey>{설정값}</AuthKey>
  </Auth>
  <PropertyId>{숙소코드}</PropertyId>
</RequestRatePlanList>

처리 흐름

HUB → POST /api/cms/rate-plans
  → CmsController.getRatePlansXML(RequestRatePlanList)
  → CmsService.getRatePlanByProductId(propertyId)
  → DB 조회 (story_rateplan, product_rateplan_mapper 등)
  → ResponseRatePlanList 반환 (XML)

API 7.1.2 - 가용객실 수신 API POST /api/cms/push-availability [XML]

HUB에서 날짜별 객실 가용 수량(재고) 및 판매중단 여부를 자사 플랫폼으로 전송하는 API입니다.

Request: RequestPushAvailability

<RequestPushAvailability>
  <Auth> ... </Auth>
  <PropertyId>{숙소코드}</PropertyId>
  <AvailabilityList>
    <Availability>
      <RoomTypeId>{객실유형ID}</RoomTypeId>
      <Dates>
        <Date Allotment="5" Close="N">2024-04-01</Date>
        <Date Allotment="3" Close="N">2024-04-02</Date>
      </Dates>
    </Availability>
  </AvailabilityList>
</RequestPushAvailability>
필드 설명
PropertyId 숙소 코드
RoomTypeId 객실 유형 ID
Date.Allotment 가용 객실 수량
Date.Close 판매 중단 여부 (Y/N)
Date 날짜 (yyyy-MM-dd)

처리 흐름

HUB → POST /api/cms/push-availability
  → CmsController.pushAvailability(RequestPushAvailability)
  → CmsService.pushAvailability(request)
  → 가용객실 수량 DB 업데이트 (product_room_stock 등)
  → Push 수신 로그 저장 (push_availability_log)
  → ResponsePushAvailability 반환 (XML)

API 7.1.3 - 요금 수신 API POST /api/cms/push-rate [XML]

HUB에서 날짜별 요금 및 최소/최대 연박 조건을 자사 플랫폼으로 전송하는 API입니다.

Request: RequestPushRate

<RequestPushAvailability>
  <Auth> ... </Auth>
  <PropertyId>{숙소코드}</PropertyId>
  <AvailabilityList>
    <Availability>
      <RatePlanId>{요금제ID}</RatePlanId>
      <Dates>
        <Date Price="100000" Close="N" MinLos="1" MaxLos="7">2024-04-01</Date>
      </Dates>
    </Availability>
  </AvailabilityList>
</RequestPushAvailability>
필드 설명
RatePlanId 요금제 ID
Date.Price 해당 날짜 요금
Date.Close 판매 중단 여부 (Y/N)
Date.MinLos 최소 연박 수
Date.MaxLos 최대 연박 수

처리 흐름

HUB → POST /api/cms/push-rate
  → CmsController.pushRates(RequestPushRate)
  → CmsService.pushRate(request)
  → 날짜별 요금 DB 업데이트 (product_rateplan_price 등)
  → ResponsePushAvailability 반환 (XML)

API 8.1 - 신규예약 API POST /api/cms/reservation-create [JSON]

자사 플랫폼에서 HUB로 신규 예약을 전송하는 API입니다. @NoAuthCheck (JWT 인증 없음).

Request: RequestBooking

{
  "orderNum": "{예약번호}",
  "productCode": "{숙소코드}",
  "roomIdx": 1364,
  "ratePlanIdx": 5548,
  "ratePlanName": "테스트상품",
  "numberRooms": 1,
  "startDate": "2024-04-01",
  "endDate": "2024-04-02",
  "mealCode": "N",
  "price": 100000,
  "adultCount": 2,
  "childCount": 0,
  "customer": { ... },
  "occupant": { ... }
}
필드 설명
orderNum (ChannelBookingId) 자사 예약번호
productCode (PropertyId) 숙소 코드
roomIdx (RoomTypeId) 객실 유형 Idx
ratePlanIdx (RatePlanId) 요금제 Idx
mealCode 조식 포함 여부 (Y/N)
price 합계 요금 (ota_price)
customer 예약자 정보
occupant 투숙객 정보

처리 흐름

자사 플랫폼 결제 완료
  → POST /api/cms/reservation-create (JSON)
  → CmsController.reservation(RequestBooking)
  → CmsService.createReservation(request)
  → Story API 호출 (XML 변환 후 전송, Endpoint는 설정값)
  → Story 응답 수신 (BookingId 포함)
  → 예약 로그 저장 (reservation_log)
  → ResponseReservation 반환 (JSON)

API 8.2 - 예약수정 API PUT /api/cms/reservation-modify [JSON]

자사 플랫폼에서 HUB로 예약 수정을 전송하는 API입니다. @NoAuthCheck.

Request: RequestModify (RequestBooking과 유사한 구조)

{
  "orderNum": "{예약번호}",
  "roomIdx": 1364,
  "ratePlanIdx": 5548,
  "ratePlanName": "테스트상품",
  "numberRooms": 1,
  "startDate": "2024-04-01",
  "endDate": "2024-04-03",
  "mealCode": "Y",
  "price": 200000,
  "adultCount": 2,
  "childCount": 1,
  "customer": { ... },
  "occupant": { ... }
}

처리 흐름

자사 플랫폼 예약 수정
  → PUT /api/cms/reservation-modify (JSON)
  → CmsController.modifyReservation(RequestModify)
  → CmsService.modifyReservation(request)
  → Story API 호출 (XML 변환 후 전송, Endpoint는 설정값)
  → 예약 수정 로그 업데이트
  → ResponseModify 반환 (JSON)

API 8.3 - 예약취소 API POST /api/cms/reservation-cancel [JSON]

자사 플랫폼에서 HUB로 예약 취소를 전송하는 API입니다. @NoAuthCheck.

Request: RequestCancellation

{
  "orderNum": "{예약번호}",
  "bookingId": "{HUB예약번호}",
  "cancellationReason": "단순취소"
}
필드 설명
orderNum (ChannelBookingId) 자사 예약번호
bookingId (BookingId) HotelStory 예약번호
cancellationReason 취소 사유

처리 흐름

자사 플랫폼 취소 요청
  → POST /api/cms/reservation-cancel (JSON)
  → CmsController.cancelReservation(RequestCancellation)
  → CmsService.cancelReservation(request)
  → Story API 호출 (XML 변환 후 전송, Endpoint는 설정값)
  → 취소 결과 수신 및 로그 저장
  → ResponseCancellation 반환 (JSON)

API 9 - 예약조회 API POST /api/cms/reservation [JSON]

자사 플랫폼에서 HUB로 예약 내역을 조회하는 API입니다. @NoAuthCheck.

Request: RequestBookingList

{
  "orderNum": "{예약번호}",
  "productCode": "{숙소코드}",
  "bookingId": "",
  "dateType": "B",
  "startDate": "2024-04-01",
  "endDate": "2024-04-30"
}
필드 설명
orderNum 자사 예약번호 (선택)
productCode 숙소 코드 (선택)
bookingId Story 예약번호 (선택)
dateType 기간 검색 타입: B=예약일, C=투숙일
startDate / endDate 검색 기간

처리 흐름

자사 플랫폼 → POST /api/cms/reservation
  → CmsController.getReservation(RequestBookingList)
  → CmsService.getReservation(request)
  → Story API 호출 또는 로컬 DB 조회
  → ResponseBookingList 반환 (JSON)

예약 목록조회(대사) - GET /api/cms/reservation/sync

HUB 데이터와 자사 DB 데이터를 대사(reconciliation) 목적으로 조회하는 내부 API입니다. @NoAuthCheck.

처리 흐름

GET /api/cms/reservation/sync
  → CmsController.getStoryReservationList()
  → CmsService.getStoryReservationList()
  → Story reservation_log DB 전체 조회
  → 결과 반환 (JSON)

GSA - 총판 숙소 업데이트 GET /api/gsa/upsert-gsa

매일 새벽 1회 배치 실행으로 HotelStory의 전체 숙소 정보를 자사 DB에 동기화합니다.
@NoAuthCheck. WebClient를 활용한 병렬 처리(Reactor) 포함.

처리 흐름 상세

[배치 실행] GET /api/gsa/upsert-gsa
  ↓
1. Story PropertyList 요청
   Story CMS API 호출 (Endpoint는 설정값)
   RequestPropertyList (XML) → ResponsePropertyList (XML) 파싱
  ↓
2. 숙소 정보 upsert
   story_product INSERT/UPDATE
   story_product_change_log 변경이력 저장
  ↓
3. 숙소 설명 저장
   story_description upsert
  ↓
4. 취소정책 저장
   story_cancelpenalties upsert
  ↓
5. 숙소 이미지 저장
   story_photo upsert
  ↓
6. 객실유형 & 요금제 병렬 조회 (WebClient + Reactor Mono.zip)
   각 PropertyId 별로:
   - POST /api/cms/room-types → PMSResponseRoomTypeList
   - POST /api/cms/rate-plans → PMSResponseRatePlanList
   story_room_type upsert
   story_rateplan upsert
  ↓
7-1. 신규 숙소 → 자사 Product 생성
   findAllWithoutMappingStoryProduct()
   insertProduct() : product_code 채번 (건별 처리)
   insertProductMapper() : external_name=STORYGSA, dis_avail=0
  ↓
7-2. 변경 숙소 → 자사 Product 업데이트
   findAllModifyStoryProduct()
   updateProduct()
  ↓
7-3. 연동해제 숙소 → product_mapper avail=0 처리
   findAllDisableStoryProduct()
   updateProductMapper()
  ↓
8-1. 객실유형 동기화
   findAllWithoutMappingStoryProductRoom()
   insertProductRoomList()
   insertProductRoomMapperList()
   updateProductRoomList()
   updateProductRoomMapperList()
  ↓
8-2. 요금제 동기화
   findAllWithoutMappingStoryProductRatePlan()
   insertProductRatePlanList()
   insertProductRatePlanMapperList()
   updateProductRatePlanList()
   updateProductRatePlanMapperList()
  ↓
9. 취소정책 동기화
   insertCancelType()
   upsertProductRatePlanCancelType()
  ↓
10. S3 이미지 동기화
   story_photo ↔ upload_file 비교
   - 삭제: S3 파일 삭제 → upload_file 삭제
   - 신규: Naver Cloud Storage 업로드 → upload_file INSERT
   - sort 번호 재정렬

관련 DB 테이블 요약

테이블 설명
story_product Story 숙소 원본 데이터
story_product_change_log 숙소 변경 이력
story_description 숙소/객실/요금제 설명
story_cancelpenalties 취소 정책
story_photo 숙소 이미지 URL
story_room_type 객실 유형
story_rateplan 요금제
product 자사 상품
product_mapper 자사 상품-Story 매핑
product_room_type 자사 객실 유형
product_room_mapper 객실 유형 매핑
product_rateplan 자사 요금제
product_rateplan_mapper 요금제 매핑
product_rateplan_cancel_type 요금제별 취소정책
upload_file 이미지 파일 관리
reservation_log 예약 송수신 로그
push_availability_log 가용객실 수신 로그

기술 스택

항목 기술
Framework Spring Boot
HTTP Client RestTemplate, WebClient (Reactor)
XML 직렬화 JAXB
ORM MyBatis
인증 JWT (Spring Security)
DB MariaDB
이미지 저장소 Naver Cloud Object Storage (S3 호환 SDK)
병렬처리 Project Reactor (Mono.zip, Schedulers.boundedElastic)

주요 참고사항

  • XML ↔ JSON 혼용: 객실/요금/가용Push 는 XML, 예약 관련은 JSON으로 처리됩니다.
  • GSA 배치: 매일 1회 전체 데이터를 수신하므로 신규/변경/삭제 모두 처리해야 합니다. product_code 채번 때문에 Product 생성은 일괄 처리 불가, 건별로 처리합니다.
  • product 설명(product_detail_kr): 운영팀 논의 필요로 story_description DB 내재화만 하고 보류 상태입니다.
  • WebClient 병렬: Mono.zip을 사용하여 각 숙소의 객실유형/요금제 조회를 동시에 처리합니다.
  • S3 이미지 관리: story_photo ↔ upload_file 비교를 통해 증분 업로드/삭제를 처리합니다.