콘텐츠로 이동

srt_mobile_api.client

SrtClient — 이 패키지에 하나뿐인 진입점.

폼을 만드는 일은 srt_mobile_api.payloads, HTML 을 값으로 바꾸는 일은 srt_mobile_api.parsers 에 있고 이 모듈은 그 둘을 라우트 하나에 엮습니다.

공개 메서드는 읽기와 상태변경(6개: reserve, reserve_transfer, cancel, pay_with_card, refund, register_discount_coupon) 둘로 나뉩니다. 후자는 require_mutation_consent 로 시작하고, srt_mobile_api.safety 가 전송 직전에 다시 검사합니다.

SrtClient

SrtClient(config=None, *, transport=None, clock=None, sleep=None)

SRT 모바일 앱(v2.0.41)이 쓰는 HTTP API 를 그대로 호출하는 클라이언트.

리버스 엔지니어링한 앱 경로에 앱과 같은 헤더·폼으로 요청을 보내고, 응답을 models 의 불변 데이터클래스로 돌려줍니다.

인자 없이 SrtClient() 로 만들면 바로 동작합니다. SrtConfig 는 타임아웃·User-Agent·device key 를 바꿀 때만 넘기며, 정규 SRT 오리진 두 개 말고는 가리키지 못합니다. transport/clock/sleep 은 테스트용 주입점입니다.

조회는 login 뒤에 바로 부르면 됩니다. 상태를 바꾸는 여섯 메서드 (reserve, reserve_transfer, cancel, pay_with_card, refund, register_discount_coupon)는 MutationConsent 를 키워드로 반드시 받고, 동의가 없으면 아무것도 만들기 전에 SrtMutationNotAllowedError 로 막힙니다. 기본 동의는 dry_run=True 라서 보낼 폼을 마스킹해 담은 MutationPreview 만 돌려주고 통신하지 않습니다.

정리는 close 로 합니다. with 문은 지원하지 않으며, 쿠키까지 버리려면 clear_session 을 따로 부르면 됩니다.

from srt_mobile_api import PassengerCounts, SrtClient, TrainSearchQuery

client = SrtClient()
client.login("you@example.com", "password")
try:
    query = TrainSearchQuery(
        departure_station_code="0551",   # 수서
        arrival_station_code="0015",     # 동대구
        departure_date="20260809",       # YYYYMMDD
        departure_time="060000",         # HHMMSS, 이 시각부터
        passengers=PassengerCounts(adult=1),
    )
    for train in client.search_trains(query).trains:
        print(train.train_no, train.departure_time, train.arrival_time)
finally:
    client.close()

config instance-attribute

config = config or SrtConfig()

http instance-attribute

http = SrtHttpClient(self.config, transport=transport)

session instance-attribute

session = SrtSessionClient(self.http)

close

close()

내부 HTTP 커넥션 풀을 닫습니다.

세션 쿠키와 저장된 SrtSession 은 지우지 않습니다 — 그것은 clear_session 의 일입니다. 서버로는 아무것도 보내지 않습니다.

with 문은 지원하지 않으므로 직접 부르거나 try/finally 로 감쌉니다.

login

login(login_id, password, *, login_type=None)

아이디와 비밀번호로 로그인하고 SrtSession 을 돌려줍니다.

login_id 는 이메일·휴대폰번호·회원번호 중 아무거나 됩니다. 형태를 보고 srchDvCd 를 고릅니다 — 이메일이면 "2", 01X 로 시작하는 휴대폰이면 "3"(하이픈은 떼고 보냅니다), 그 외에는 회원번호로 보고 "1". login_type 을 주면 그 값이 이깁니다.

호출하면 먼저 기존 쿠키를 버립니다. 응답의 userMap.RTNCD"Y" 여야 성공이고, 그 뒤 /main/main.do/ara/ara0101v.do 를 실제로 읽어 인증됐는지 확인한 다음에야 세션을 저장합니다.

실패는 전부 SrtAuthError 입니다 — 없는 회원과 틀린 비밀번호를 서버가 구분해 주지 않습니다. IP 차단은 그 하위 클래스인 SrtIpBlockedError 이고, 어떤 이유로 실패하든 세션은 다시 비워집니다.

clear_session

clear_session()

쿠키 항아리를 비우고 저장된 세션을 버립니다.

서버로는 아무것도 보내지 않습니다. 로그아웃 요청이 아니라 클라이언트 쪽 상태만 지우는 것이라, 서버 세션은 스스로 만료될 때까지 남아 있습니다.

logout

logout()

clear_session 의 다른 이름 — 로컬 쿠키와 세션만 버립니다.

서버에 로그아웃 요청을 보내지 않습니다. 이름 때문에 서버 세션까지 끊긴다고 읽히기 쉬워서 적어 둡니다.

get_main

get_main()

로그인 뒤 메인 화면의 HTML 을 파싱 없이 돌려줍니다.

GET /main/main.do?deviceId=<device_key>. 반환 HtmlPage 에는 원본 raw 와 태그를 걷어낸 text 뿐이고 구조화된 필드는 없습니다.

로그인 세션이 필요합니다. 서버가 로그인 페이지를 돌려주면 HTTP 계층이 SrtSessionExpiredError 를 내고 저장된 세션도 같이 비워집니다.

get_booking_page

get_booking_page()

승차권 예매 화면(GET /ara/ara0101v.do)의 HTML 을 파싱 없이 돌려줍니다.

검색·예약 폼이 서버에서 렌더링돼 실려 오는 페이지입니다. 이 클라이언트는 검색과 예약 때 이 경로를 Referer 로 쓰고, 여기 박혀 있는 MY SRT 메뉴에서 get_discount_couponsget_public_discounts 의 경로를 찾았습니다.

반환은 파싱되지 않은 HtmlPage 입니다. 로그인 세션이 필요하고, 미인증 응답은 SrtSessionExpiredError 로 올라옵니다.

get_notice_list

get_notice_list()

공지사항 응답을 서버가 준 dict 모양 그대로 돌려줍니다.

POST /main/noticeList.do (pageId=MB0101000000). 모양을 바꾸지 않는 구버전 인터페이스이고, 타입이 붙은 행이 필요하면 get_typed_notice_list 를 씁니다 — 요청은 완전히 같고 파싱만 다릅니다.

공지가 하나도 없으면 noticeList 가 빈 리스트인 정상 응답이며 예외가 아닙니다.

get_typed_notice_list

get_typed_notice_list()

같은 공지 조회를 NoticeListResult 로 돌려줍니다.

get_notice_list 와 요청은 완전히 같습니다. 차이는 반환뿐 — 각 행이 불변 Notice 이고, SUBJ/BODY/CREATE_DATE/ IS_MAIN/IS_NOTICE/PAGE_ID 가 문자열이 아니면 SrtProtocolError 로 거릅니다. 서버 응답 전체는 raw 에 남습니다.

공지가 없으면 빈 notices 가 정상입니다.

get_ticket_list

get_ticket_list(page_no=0)

로그인한 계정의 승차권 확인 페이지 HTML 을 파싱 없이 돌려줍니다.

GET /atc/selectListAtc14017_n.do?pageNo=<page_no>, 발권이 끝난 승차권을 보여 주는 화면입니다. 예약을 구조화된 행으로 받으려면 get_reservations 를 씁니다 — 그쪽은 이웃 경로 Atc14016 을 POST 해 JSON 을 받는 다른 읽기입니다.

반환 HtmlPage 에는 승차권이 파싱돼 있지 않습니다.

로그인이 필요하고, 이 읽기는 응답이 로그인 페이지인지 명시적으로 확인해 SrtSessionExpiredError 를 냅니다. 만료된 세션이 빈 승차권 목록으로 읽히는 일을 막기 위한 것입니다.

get_discount_coupons

get_discount_coupons()

계정이 보유한 할인쿠폰 목록을 읽습니다.

GET /apa/selectListApa03020_n.do, 파라미터 없음. 반환은 DiscountCouponList 이고 로그인이 필요합니다.

쿠폰이 하나도 없으면 빈 목록이 정상 반환입니다 — 페이지가 "보유한 쿠폰이 없습니다." 라고 말하는 경우입니다. 목록도 없고 그 문구도 없거나 둘이 동시에 있으면 SrtProtocolError 입니다.

같은 페이지에 등록 폼이 있지만 등록은 다른 경로 (POST /arb/selectListArb02A01_n.do)이고 register_discount_coupon 의 일입니다.

v2.0.41 번들에는 /apa/ 라우트가 없고 서버가 렌더링하는 MY SRT 메뉴에서 찾은 경로입니다. 확인된 것은 빈 상태뿐이고 채워진 상태는 미검증입니다 — 필드명 근거는 DiscountCoupon 참고.

get_public_discounts

get_public_discounts()

계정이 승인받은 공공할인 자격을 읽습니다.

GET /common/ARA/ARA0301V/view.do, 할인 승차권 페이지, 파라미터 없음. 반환은 PublicDiscountPage 이고 로그인이 필요합니다.

자격이 하나도 없는 계정도 정상 반환입니다is_eligibleFalse 로 올 뿐입니다. 자격을 가진 계정의 응답은 미검증입니다.

검색은 하지 않습니다. 이 페이지가 곧 검색 폼이고, 검색은 search_public_discount_trains 가 따로 보냅니다.

공공할인의 승객 어휘는 PassengerCounts 보다 넓습니다 — 청소년(04) 아래에는 어느 commCode.js 에도 없는 psgTpCd6 이 있고, discounts 에 기록만 해 두었습니다.

get_reservations

get_reservations(page_no=0)

계정의 예약/발권 목록을 구조화된 행으로 읽습니다.

POST /atc/selectListAtc14016_n.do. 반환은 SrtReservationListResult 이고 로그인이 필요합니다.

예약을 열거할 수 있는 유일한 읽기입니다. PNR 을 잃어버린 홀드로 돌아가는 길이기도 해서, 파서는 모양이 예상과 달라도 PNR 만은 건져냅니다 (parse_reservation_list_response). HTML 을 주는 get_ticket_list(Atc14017 GET)와는 다른 읽기입니다.

page_nopageNo 로 나가고, 전체 페이지 수는 결과의 total_page_count (totPageCnt)입니다.

예약이 없으면 빈 결과가 정상입니다. 서버는 strResult="SUCC" / IRZ000005 / "조회할 자료가 없습니다." 를 trainListMap: [] 와 함께 보냅니다. 채워진 모양은 미검증이고 행 필드명은 srtgo(srt.py:1069-1082) 출처입니다.

get_station_selector

get_station_selector(departure_name, arrival_name, departure_code, arrival_code)

역 선택 팝업 HTML(POST /common/ARA/ARA0501P/view.do).

역 코드·이름 표는 stations 에 이미 있으므로 역을 고르기 위해 이 호출을 할 필요는 없습니다. 로그인 필요.

get_station_map_selector

get_station_map_selector()

노선도 방식 역 선택 팝업 HTML(POST /common/ARA/ARA0502P/view.do).

인자 없음 — 고정값. 역 코드는 stations 에서 얻는 편이 낫습니다. 로그인 필요.

get_date_selector

get_date_selector(date)

날짜 선택 팝업 HTML(POST /common/ARA/ARA0401P/view.do).

dateYYYYMMDD, 아니면 ValueError. 로그인 필요.

get_passenger_selector

get_passenger_selector(passengers)

승차인원 선택 팝업 HTML(POST /common/ARA/ARA0901P/view.do).

검색·예약과 무관합니다. 로그인 필요.

get_seat_option_selector

get_seat_option_selector(*, request_seat_attr_code='015', location_seat_attr_code='000', seat_name='일반/기본')

좌석 위치·속성 선택 팝업 HTML(POST /common/ARA/ARA0701P/view.do).

기본값은 앱의 초기 상태와 동일. 실제 좌석 확인은 get_seat_page / get_seat_grid. 로그인 필요.

get_train_group_selector

get_train_group_selector(train_group_code='109', train_group_name='전체')

열차 종류 선택 팝업 HTML(POST /common/ARA/ARA0201V/view.do).

기본 "109"/"전체"TrainSearchQuery 기본값과 같습니다. 로그인 필요.

get_mutual_verification

get_mutual_verification()

상호검증값(mutMrkVrfCd)을 발급받습니다.

POST /ara/selectListAra10130_n.do, 본문 없음. 앱은 이 값을 코레일 연계 (KTX) 예약 분기에서 씁니다(ara1001l.js:229-241). 이 라이브러리의 예약 경로는 쓰지 않으므로 발급까지가 전부입니다.

로그인이 필요 없습니다 — 다른 읽기와 달리 공개 경로입니다.

반환은 MutualVerificationResult 입니다. strResult"FAIL" 이면 SrtAppError 계열, mutMrkVrfCd 가 비어 있으면 SrtProtocolError 입니다. msgCd 는 정보성이라 없어도 되고, 모델에 없는 wctNo/uuid/cgPsIdraw 로 닿을 수 있습니다.

search_trains

search_trains(query)

직통 개인 검색 — 한 페이지 분량의 열차 행을 돌려줍니다.

query 의 역 코드, 날짜(YYYYMMDD), 시각(HHMMSS, 그 시각부터), 인원으로 POST /ara/selectListAra10007_n.do 를 보내고 TrainSearchResult 를 돌려줍니다. 로그인이 필요합니다.

결과가 없으면 빈 목록이 아니라 예외입니다SrtNoResultsError. 직통은 없지만 환승이 되는 구간이면 그 하위 클래스인 SrtNoDirectTrainError(WRD000061)가 오고, 같은 querysearch_transfer_trains 에 넘기면 됩니다.

인원이 10명 이상이면 요청 전에 ValueError 입니다 (ara0101v.js:562-567). 그때는 search_group_trains 를 쓰면 됩니다.

NetFunnel act_10 대기열은 앱처럼 통과하고, NET000001 이 오면 한 번만 다시 시도합니다. 다음 페이지는 iter_train_search_pages 입니다.

search_group_trains

search_group_trains(query)

단체(10명 이상) 검색 — 같은 질의를 단체 경로로 보냅니다.

POST /ara/selectListAra10082_n.do. 경로를 가르는 것은 grpDv 하나이고 (ara1001l.js:174-181), 본문은 grpDv="1"psgNum 말고는 search_trains 와 같습니다. 반환도 같은 TrainSearchResult 입니다. 로그인이 필요합니다.

인원이 10명 미만이면 요청 전에 ValueError — 앱도 클라이언트에서 막습니다(ara0101v.js:551-554).

이 라이브러리는 단체 예약은 하지 않습니다. 잔여석과 운임 조회까지가 전부이고, 단체 예약 경로는 등록조차 돼 있지 않습니다.

빈 결과가 예외라는 것과 NetFunnel 처리는 search_trains 와 같습니다.

search_public_discount_trains

search_public_discount_trains(query, discount, *, page_cursor='')

할인 승차권 검색 — 계정이 승인받은 공공할인이 붙는 열차를 찾습니다.

POST /ara/selectListAra10131_n.do. 읽기이며 일반 검색 ajax 와 같은 자격으로 READ_ONLY_ROUTES 에 자기 23필드 계약과 함께 등록돼 있습니다. 반환은 TrainSearchResult 이고 로그인이 필요합니다.

한 번도 보내 본 적이 없습니다. 필드명·값·응답 키는 서버가 렌더링해 주는 할인 승차권 페이지에서 읽은 것이라 요청 모양에는 근거가 있고 효과에는 없습니다.

discountPBL_DISC_CD 는 알려진 값 범위(01 다자녀 … 06 3세대 동행할인, 그리고 07/08 분기)이고 TGT_DTRM_YN 은 늘 "Y" 입니다. PBL_DISC_NM 은 ajax 폼에 필드가 없어 전송되지 않고, PBL_DISC_MG_NO 는 서버가 발급하는 승인번호라 호출자가 넘깁니다(기본 "").

승객 타입 구성은 전송되지 않습니다. 이 폼은 인원 합계 psgNum 만 싣고 psgTpCd/psgInfoPerPrnb 가 없어서 청소년·유아는 합계만 바꿉니다.

page_cursorgdNo 이고 첫 페이지는 "" 입니다. 종료 조건 (trainListMap[0].fllwPgExt)을 본 적이 없어 자동으로 돌지 않습니다. NetFunnel 자리는 잡았다 반납하지만 이 경로의 폼에는 netfunnelKey 필드가 없어 키는 나가지 않습니다.

알 수 없는 할인 코드이거나 다자녀·3세대 동행할인인데 인원이 3명 미만이면 어떤 I/O 도 하기 전에 ValueError 입니다(페이지 규칙 rsv071).

search_transfer_trains

search_transfer_trains(query)

환승 검색 — 짝지어진 TransferItinerary 를 돌려줍니다.

요청은 search_trains 와 거의 같습니다. 경로가 같고, 본문 차이는 chtnDvCd"1" 에서 "2" 로 바뀌는 것뿐이며, 하이드레이션 GET 에 jrnyTpCd="14"/jrnyCnt="2" 가 더 붙습니다(ara0101v.js:302-303). 로그인이 필요합니다.

별도 메서드인 이유는 결과입니다. 서버가 주는 행은 한 여정의 절반 인데 표시가 없어서, 그대로 reserve 에 넘기면 환승역까지만 끊긴 승차권이 나옵니다. 그래서 짝짓기를 거친 TransferItineraryreserve_transfer 쌍으로 갈라 두었습니다.

반환 TransferSearchResult 에는 짝지어진 itineraries, 짝이 맞지 않아 이유와 함께 남겨 둔 unpaired, 손대지 않은 원본 search 가 함께 들어 있습니다. 규칙은 pair_transfer_itineraries 참고.

행은 있는데 하나도 짝지어지지 않으면 SrtProtocolError 입니다. 페이징은 지원하지 않습니다 — 둘째 커서 fllwPgExt2 는 늘 null 이라 용도를 모릅니다.

여기 닿는 자연스러운 길은 직통 검색의 WRD000061, 즉 SrtNoDirectTrainError 입니다.

iter_train_search_pages

iter_train_search_pages(query, *, group=False, max_pages=10)

검색 결과를 페이지 단위로 게으르게 넘기는 제너레이터를 돌려줍니다.

첫 페이지는 search_trains(group=Truesearch_group_trains)와 같은 요청이고, 이어지는 페이지는 직전 페이지 마지막 열차의 출발시각을 dptTm 커서로 밀어 올려 다시 POST 합니다. TrainSearchResult 를 한 페이지씩 내놓습니다.

max_pages 는 양의 정수여야 하고(bool 은 거부) 아니면 ValueError 입니다. 이 검사만은 제너레이터를 돌리기 전 호출 즉시 일어납니다. 기본 10 페이지에서 멈추며, 그전이라도 서버가 다음 페이지 없음을 말하거나 페이지가 비면 끝납니다. 커서가 반복되거나 나아가지 않으면 SrtProtocolError 입니다.

NetFunnel 자리는 걷기 전체가 끝날 때 반납됩니다. 도중에 제너레이터를 버려도(GeneratorExit) 반납됩니다.

환승 검색에는 쓸 수 없습니다 — search_transfer_trains 참고.

get_seat_page

get_seat_page(train, cabin_class='1', seat_count=None, *, passengers=None, seat_attr_code='015')

좌석 선택 1단계 — 호차 목록이 담긴 좌석선택 페이지를 읽습니다.

POST /arc/selectListArc02012_n.do. train 은 검색이 돌려준 완전한 TrainSummary 행이어야 합니다. 반환 SeatSelectionPage 는 HTML 에 cars(호차 옵션)가 붙은 것이고, 호차 하나의 좌석 배치는 get_seat_grid 가 이어서 읽습니다.

choiceSeatCount 는 상수가 아니라 일행 인원입니다 — 앱은 lfn_getRsv("totPrnb") 를 보냅니다(ara1001l.js:1511). 그래서 passengers 를 넘기면 거기서 인원을 뽑습니다. seat_count 는 명시적 override 이고 둘 다 주면 이쪽이 이깁니다. 둘 다 없으면 "1".

로그인이 필요합니다.

get_seat_grid

get_seat_grid(train, car_number, cabin_class='1', seat_count=None, *, passengers=None, seat_attr_code='015')

좌석 선택 2단계 — 호차 하나의 좌석배치도를 읽습니다.

POST /arc/selectListArc02011_n.do. get_seat_page 가 "어느 호차에 자리가 있나" 를 답한다면 이쪽은 "어느 좌석이고, 고를 수 있나" 를 답합니다. 반환 SeatGridchooseSeatDesignation 을 만들어 reservedesignated_seats 에 넘길 수 있습니다.

car_numbercarscar_number 값이고, passengers/seat_count/cabin_classget_seat_page 에 준 것과 같게 주면 됩니다.

열차번호를 다섯 자리로 0 채움해 보냅니다 — 그 하나가 좌석배치도와 147바이트짜리 경고 껍데기를 가릅니다 (SEAT_TRAIN_NUMBER_LENGTH).

서버가 거절하면("0#<메시지>") SrtSeatUnavailableError 이지 프로토콜 오류가 아닙니다. 로그인이 필요하고, 응답이 로그인 페이지면 SrtSessionExpiredError 입니다.

get_timetable

get_timetable(train)

열차 하나의 정차역별 시간표를 읽습니다.

POST /ara/selectListAra12009_n.do, 어느 열차인지는 검색 행 train 이 말합니다. 반환 TimetablePage 는 HTML 에 정차역 행 (TimetableRow)이 붙은 것입니다.

역 이름은 HTML 안에 없습니다. 서버는 코드만 내려보내고 WebView 가 getStationNameByCode('0551') 로 채웁니다. 이 파서는 앱과 같은 표를 담은 srt_mobile_api.stations 로 이름을 해결합니다.

로그인이 필요합니다.

get_fare

get_fare(train, passengers=None)

열차 한 다리의 운임·요금 표를 읽습니다.

POST /ara/selectListAra13010_n.do. passengers 를 생략하면 성인 1명 (PassengerCounts 의 기본값)으로 묻습니다. 반환은 FarePage.

둘째 다리 표는 읽지 않습니다. 이 페이지는 환승 여정용이라 trainPayInfo1 (첫 다리)과 trainPayInfo22(둘째 다리) 두 표를 렌더링하는데, 이 요청은 한 다리만 말할 수 있어 둘째 표는 금액이 0원 인 자리표시자로 옵니다. 그것까지 읽으면 "어른 일반실 → 0" 처럼 돈에 대해 조용히 틀린 답이 나옵니다.

로그인이 필요합니다.

reserve

reserve(train, *, consent, passengers=None, seat_type=GENERAL_FIRST, window_seat=None, netfunnel_key=None, standby=False, round_trip=False, designated_seats=None, seat_attr_code=None)

열차 한 편에 개인예약 홀드를 만듭니다. consent 게이트.

POST /arc/selectListArc05013_n.do. 기본 dry_run=TrueMutationPreview 만 돌려주고 네트워크 무사용. dry_run=False 면 실전송 → SrtReservationHold.

재시도 없음(이중예약 방지). 파싱 실패해도 PNR 있으면 최소 홀드 건짐. standby = 예약대기(jobId=1102, ara0101v.js:90, ara1001l.js:1445-1448). round_trip = rtnDv=1; 두 번째 요청을 보내지 않으므로 각각 호출. designated_seats = 좌석지정(jobId=1103). 확인된 조합: 성인 1명·1여정·일반실·직통. 나머지 미검증.

reserve_transfer

reserve_transfer(itinerary, *, consent, passengers=None, seat_type=GENERAL_FIRST, window_seat=None, netfunnel_key=None, seat_attr_code=None)

환승 예약 — 두 다리를 한 요청으로 예약합니다. consent 게이트.

경로도 consent 카테고리도 reserve 와 같습니다 (/arc/selectListArc05013_n.do, "reserve"). jrnyTpCd="14" + jrnyCnt="2" (ara0101v.js:302-303). 전송 경로·안전 속성 전부 동일.

인자가 TransferItinerary 인 것은 이음새 검사를 생성 시점에 하기 위해서입니다. round_trip·standby·단체·좌석지정 조합 불가.

2026-07-31 실서버 확인. 동대구(0015)→목포(0041), 열차 304 + 653, 오송 환승, 성인 1명으로 SUCC/IRR000018 을 받았습니다(PNR 320260733059548). 유추였던 슬롯 2 키 5개가 그대로 받아들여졌습니다. 확인된 것은 1인·일반실·직통 아님·편도 한 건입니다.

취소는 jrnyCnt="2" 로 해야 합니다. 이 예약을 PNR 문자열만으로 취소하면 canceljrnyCnt="1" 을 보내고 서버가 FAIL/ERR800052 로 거절합니다 — 홀드는 그대로 남습니다. 홀드 객체를 넘기면 자기 값을 기억하므로 맞고, PNR 만 있으면 journey_count="2" 를 직접 줘야 합니다.

환승 조회 결과에는 코레일 열차가 섞여 옵니다. 같은 날 부산→익산 의 환승 5개 중 3개는 구간 하나 이상이 KTX(stlbTrnClsfCd="00") 또는 KTX-산천("07") 이었고, 그런 여정을 넘기면 personal_reservation_payloadreservation requires an SRT train (service_class_code '17') 로 전송 전에 거절합니다. 예약할 수 있는 것은 양쪽 구간이 다 "17" 인 여정뿐입니다.

cancel

cancel(reservation, *, consent, journey_count=None)

미결제 예약을 취소합니다. consent 게이트.

POST /ard/selectListArd02045_n.do, 본문 pnrNo/jrnyCnt/rsvChgTno. reservationSrtReservationHold 또는 PNR 문자열. journey_count 생략 시 홀드가 기억하는 값(환승 "2"), 맨 PNR 이면 "1". 기본 dry_run=True.

register_discount_coupon

register_discount_coupon(coupon_number, coupon_password, *, consent)

할인쿠폰을 계정에 등록합니다(할인쿠폰 등록). consent 게이트가 있고, 전송은 막혀 있습니다.

POST /arb/selectListArb02A01_n.do, 본문은 {dscp_no, dscp_pwd}. get_discount_coupons 가 읽는 페이지의 나머지 절반으로, 그 페이지의 couponReg()#couponInfo 를 여기로 보내며 resultMap[0].RTNCD/.MSG 를 읽습니다.

consent 카테고리는 다섯 번째인 "coupon" 입니다. 쿠폰 등록은 소지 크리덴셜을 소진하는 일이라 reservepayment 에 얹지 않았습니다. require_mutation_consent(consent, "coupon") 과 인증 세션이 필요하고, 두 검사 모두 dry_run 분기보다 앞입니다.

보낼 수 없습니다. "coupon"SRT_LIVE_MUTATION_CATEGORIES 밖이라 dry_run=False 로 불러도 전송 계층이 SrtMutationNotAllowedError 로 거절합니다.

미리보기는 dscp_nodscp_pwd 를 모두 가립니다 (SENSITIVE_KEYS). 입력 검증은 페이지 자신의 규칙으로, 번호는 숫자만 최대 10자리, 비밀번호는 최대 4자이고 둘 다 비어 있으면 안 됩니다.

성공은 요청이 접수됐다는 뜻이지 쿠폰이 보인다는 뜻이 아닙니다(쿠폰등록 요청을 완료하였습니다 … 바로 조회 되지 않을 수 있습니다). 등록 직후 get_discount_coupons 가 아무것도 못 봐도 정상일 수 있습니다.

경로·필드명·응답 키는 서버가 렌더링하는 실제 페이지에서 읽었습니다. v2.0.41 번들에는 /arb/ 라우트가 없고, 번들이 뒷받침하는 것은 어휘뿐입니다 (js/common/messages.js:111-113, sub/main.html:475DSCP_YN).

pay_with_card

pay_with_card(reservation, card, *, consent, passenger_count=None, settlement_date=None)

미결제 PNR 을 카드로 결제합니다. consent 게이트.

POST /ata/selectListAta09036_n.do, 31개 필드. 세션에 회원번호 필수. reservationget_reservations 행. 금액은 rcvdAmt 에서만. 기본 dry_run=True → 민감정보 가린 미리보기. dry_run=Falserequire_card_kind_claim 추가 필요.

확인된 조합: 1여정·성인 1명·일반실·개인카드·일시불(SUCC/IRT000000). Ata09* 전체가 v2.0.41 번들 0-hit. 출처가 얇은 경로입니다.

get_refund_ticket_info

get_refund_ticket_info(pnr_no)

발권된 승차권의 환불 식별 정보를 읽습니다 — 환불 2단계 중 1단계.

POST /atc/getListAtc14087.do, 본문 없음. 승차권을 지목하는 것은 Referer 로 실려 가는 /common/ATC/ATC0201L/view.do?pnrNo=<PNR> 입니다. 응답의 outDataSets.dsOutput1[0]SrtRefundTicketInfo 가 되고, 그대로 refund 에 넘기면 됩니다. 로그인이 필요합니다.

consent 게이트가 아니라 읽기 경로를 탑니다 (READ_ONLY_ROUTES). 그 분류는 증명이 아니라 추론입니다 — 서버가 부작용 없다고 말해 준 적은 없습니다.

pnr_no 는 ASCII 영숫자와 하이픈 1~32자만 받고, 어기면 요청 전에 ValueError 입니다. Referer URL 에 그대로 끼워 넣는 값이라 다른 문자는 질의 파라미터를 몰래 붙일 수 있습니다. 이 Referer 는 이 요청에만 붙습니다.

없는 PNR 도 오류가 아니라 정상 업무 응답(WRT300005, "조회자료가 없습니다.")으로 돌아옵니다. 경로는 v2.0.41 번들에 없고 참조 구현 하나만 이걸 압니다.

refund

refund(ticket_info, *, consent)

발권까지 끝난 승차권을 환불합니다 — 2단계 흐름의 둘째. consent 게이트가 있습니다.

POST /atc/selectListAtc02063_n.do, 7개 필드. require_mutation_consent(consent, "refund") 와 인증 세션이 필요하고, 두 검사 모두 dry_run 분기보다 앞입니다. 성공하면 SrtRefundResult 입니다.

ticket_infoget_refund_ticket_info 가 준 것입니다. 두 단계를 일부러 합치지 않았습니다 — 이 메서드가 스스로 1단계를 부르지 않으므로 거절당한 환불은 아무 요청도 남기지 않고, dry run 이 통신을 전혀 하지 않습니다.

dry_run=True(기본)면 반환비밀번호·구매자명·PNR 을 가리고 판매 식별자는 남긴 MutationPreview 만 돌려줍니다.

확인된 것은 1여정·성인 1명 한 건이고(SUCC/IRT200277), 성공 뒤 계정에는 예약도 승차권도 남지 않습니다.

출처는 pay_with_card 보다도 얇습니다 — 참조 구현 하나에만 있고 Atc02063 은 v2.0.41 번들에서 0-hit 입니다. 필드명 tkRetPwd·psgNm 은 srtgo 철자이고 서버가 그대로 받습니다. 앱 쪽 철자 retPwd/buyPsNm 은 로컬 승차권 캐시 핸들러의 것이라 이 요청과 무관합니다.