마지막으로 승차권 예매를 해보자!
예매 request는 내용도 많고 복잡하지만... 침착하게 분석해보자
Body data를 분석하다보니, 입석+좌석이나 환승의 경우 데이터가 너무 복잡해서, 직행 열차만 다룬다.
- URL : https://etk.srail.kr/hpg/hra/01/checkUserInfo.do
- Method : Post
Body Data
- rsvTpCd : 예약 타입 코드 (01 - 일반 예약, 05 - 대기)
- jobId : ??? (1101 - 일반 예약, 1102 - 대기)
- jrnyTpCd : [추정] 여행 타입 코드 - 항상 '11'
- jrnyCnt : [추정] 여정 개수 - 항상 '1'
- totPrnb : 총 탑승 인원
- stndFlg : [추정] 입석 여부 - 항상 'N'
- trnOrdrNo1, jrnySqno1, runDt1, trnNo1, trnGpCd1, stlbTrnClsfCd1, dptDt1, dptTm1, dptRsStnCd1, dptStnConsOrdr1, dptStnRunOrdr1, arvRsStnCd1, arvStnConsOrdr1, arvStnRunOrdr1
- scarYn1 : 좌석 지정 여부 - 항상 'N'
- scarGridcnt1, scarNo1, seatNo1_1, seatNo1_2, seatNo1_3, seatNo1_4, seatNo1_5, seatNo1_6, seatNo1_7, seatNo1_8, seatNo1_9 : 좌석 지정이 없으므로 모두 공백
- psrmClCd1 : 좌석 등급 (1 - 일반실, 2 - 특실)
- smkSeatAttCd1 : [추정] 항상 000
- dirSeatAttCd1 : [추정] 항상 000
- locSeatAttCd1 : 좌석위치 (000 - default, 011 - 1인석, 012 - 창측좌석, 013 - 내측좌석)
- rqSeatAttCd1 : 좌석속성 (015 - 일반, 021 - 휠체어, 028 - 전동휠체어)
- etcSeatAttCd1 : [추정] 항상 000
- jrnyTpCd1, jrnyTpCd2, trnOrdrNo2, jrnySqno2, runDt2, trnNo2, trnGpCd2, stlbTrnClsfCd2, dptDt2, dptTm2, dptRsStnCd2, dptStnConsOrdr2, dptStnRunOrdr2, arvRsStnCd2, arvStnConsOrdr2, arvStnRunOrdr2, scarYn2, scarGridcnt2, scarNo2, seatNo2_1, seatNo2_2, seatNo2_3, seatNo2_4, seatNo2_5, seatNo2_6, seatNo2_7, seatNo2_8, seatNo2_9, psrmClCd2, smkSeatAttCd2, dirSeatAttCd2, locSeatAttCd2, rqSeatAttCd2, etcSeatAttCd2 : 모두 공백 (환승 없음)
- psgGridcnt=1
- psgTpCd1=1
- psgInfoPerPrnb1=1
- psgTpCd2=
- psgInfoPerPrnb2=
- psgTpCd3=
- psgInfoPerPrnb3=
- psgTpCd4=
- psgInfoPerPrnb4=
- psgTpCd5=
- psgInfoPerPrnb5=
- mutMrkVrfCd=
- reqTime=1704796578230
- crossYn=N
def book_ticket(self, adult, child, senior, svrDsb, mldDsb, train_schedule, locSeatAttCd, rqSeatAttCd,
isReservation = False, isBusiness = False):
check_user_url = "https://etk.srail.kr/hpg/hra/01/checkUserInfo.do"
psgGridcnt = 0
psgGrid = []
if adult > 0:
psgGridcnt += 1
psgGrid.append(["1", str(adult)])
if child > 0:
psgGridcnt += 1
psgGrid.append(["5", str(child)])
if senior > 0:
psgGridcnt += 1
psgGrid.append(["4", str(senior)])
if svrDsb > 0:
psgGridcnt += 1
psgGrid.append(["2", str(svrDsb)])
if mldDsb > 0:
psgGridcnt += 1
psgGrid.append(["3", str(mldDsb)])
body = {
"rsvTpCd": "05" if isReservation else "01",
"jobId": "1102" if isReservation else "1101",
"jrnyTpCd": "11",
"jrnyCnt": "1",
"totPrnb": str(adult+child+senior+svrDsb+mldDsb),
"stndFlg": "N",
"trnOrdrNo1": train_schedule['trnOrdrNo'],
"jrnySqno1": train_schedule['jrnySqno'],
"runDt1": train_schedule['runDt'],
"trnNo1": train_schedule['trnNo'],
"trnGpCd1": train_schedule['trnGpCd'],
"stlbTrnClsfCd1": train_schedule['stlbTrnClsfCd'],
"dptDt1": train_schedule['dptDt'],
"dptTm1": train_schedule['dptTm'],
"dptRsStnCd1": train_schedule['dptRsStnCd'],
"dptStnConsOrdr1": train_schedule['dptStnConsOrdr'],
"dptStnRunOrdr1": train_schedule['dptStnRunOrdr'],
"arvRsStnCd1": train_schedule['arvRsStnCd'],
"arvStnConsOrdr1": train_schedule['arvStnConsOrdr'],
"arvStnRunOrdr1": train_schedule['arvStnRunOrdr'],
"scarYn1": "N",
"scarGridcnt1": "",
"scarNo1": "",
"seatNo1_1": "",
"seatNo1_2": "",
"seatNo1_3": "",
"seatNo1_4": "",
"seatNo1_5": "",
"seatNo1_6": "",
"seatNo1_7": "",
"seatNo1_8": "",
"seatNo1_9": "",
"psrmClCd1": "2" if isBusiness else "1",
"smkSeatAttCd1": "000",
"dirSeatAttCd1": "000",
"locSeatAttCd1": locSeatAttCd, # 좌석위치 (000 - default, 011 - 1인석, 012 - 창측좌석, 013 - 내측좌석)
"rqSeatAttCd1": rqSeatAttCd, # 좌석속성 (015 - 일반, 021 - 휠체어, 028 - 전동휠체어)
"etcSeatAttCd1": "000",
"jrnyTpCd1": "",
"jrnyTpCd2": "",
"trnOrdrNo2": "",
"jrnySqno2": "",
"runDt2": "",
"trnNo2": "",
"trnGpCd2": "",
"stlbTrnClsfCd2": "",
"dptDt2": "",
"dptTm2": "",
"dptRsStnCd2": "",
"dptStnConsOrdr2": "",
"dptStnRunOrdr2": "",
"arvRsStnCd2": "",
"arvStnConsOrdr2": "",
"arvStnRunOrdr2": "",
"scarYn2": "",
"scarGridcnt2": "",
"scarNo2": "",
"seatNo2_1": "",
"seatNo2_2": "",
"seatNo2_3": "",
"seatNo2_4": "",
"seatNo2_5": "",
"seatNo2_6": "",
"seatNo2_7": "",
"seatNo2_8": "",
"seatNo2_9": "",
"psrmClCd2": "",
"smkSeatAttCd2": "",
"dirSeatAttCd2": "",
"locSeatAttCd2": "",
"rqSeatAttCd2": "",
"etcSeatAttCd2": "",
"psgGridcnt": psgGridcnt,
"psgTpCd1": "" if len(psgGrid) < 1 else psgGrid[0][0],
"psgInfoPerPrnb1": "" if len(psgGrid) < 1 else psgGrid[0][1],
"psgTpCd2": "" if len(psgGrid) < 2 else psgGrid[1][0],
"psgInfoPerPrnb2": "" if len(psgGrid) < 2 else psgGrid[1][1],
"psgTpCd3": "" if len(psgGrid) < 3 else psgGrid[2][0],
"psgInfoPerPrnb3": "" if len(psgGrid) < 3 else psgGrid[2][1],
"psgTpCd4": "" if len(psgGrid) < 4 else psgGrid[3][0],
"psgInfoPerPrnb4": "" if len(psgGrid) < 4 else psgGrid[3][1],
"psgTpCd5": "" if len(psgGrid) < 5 else psgGrid[4][0],
"psgInfoPerPrnb5": "" if len(psgGrid) < 5 else psgGrid[4][1],
"mutMrkVrfCd": "",
"reqTime": str(int(time.time())*1000),
"crossYn": "N"
}
res = self.session.post(check_user_url, data=body)
print(res.text)
예매 Requet를 checkUserInfo.do로 보내는데, 요청을 보내보면 redirection 링크를 돌려받는데, 케이스가 두 가지로 나뉘는 것을 볼 수있다.
1. 로그인이 된 경우
location.replace('/hpg/hra/02/requestReservationInfo.do?pageId=TK0101030000');
2. 로그인이 안 된 경우
location.replace('/cmc/01/selectLoginForm.do?pageId=TK0701000000&rsvTpCd=~~~');
checkUserInfo.do는 로그인 여부만 확인하고, 실제 예약은 requestReservationInfo.do에서 진행됨을 알 수 있다.
이를 참고하여 살을 덧붙여보자.
check_user_res = self.session.post(check_user_url, data=body)
if 'selectLoginForm' in check_user_res.text:
if not self.login():
raise '예약 중 로그인 재시도 실패' # 로그인 실패
check_user_res = self.session.post('https://etk.srail.kr/hpg/hra/01/checkUserInfo.do', data=body)
if 'requestReservationInfo' in check_user_res.text:
reservation_res = self.session.post(reservation_url, data=body)
else:
raise f'예약 중 재로그인 후 예상치 못한 user check fail - \n{check_user_res.text}'
elif 'requestReservationInfo' in check_user_res.text:
reservation_res = self.session.post(reservation_url, data=body)
else:
raise f'예약 중 예상치 못한 user check fail - \n{check_user_res.text}'
print(reservation_res.text)
예약 요청을 하고 나면 다시 리디렉션 메세지를 받는다.
location.replace('confirmReservationInfo.do?pageId=null');
리디렉션 페이지에서 에러나는 경우는 두 가지로 확인된다.
1. 잔여석없음
2. 예약대기자한도수초과
위 케이스 외에는 정상 예매가 됐다고 판단하자.
if 'confirmReservationInfo' in reservation_res.text:
confirm_res = self.session.post(confirm_url, data=body)
else:
raise f'예약 중 reservation fail - \n{check_user_res.text}'
if "잔여석없음" in confirm_res.text:
raise "잔여석 없음"
elif "예약대기자한도수초과" in confirm_res.text:
raise "예약대기자 한도수 초과"
print("예약 성공")
return True
완성된 코드다...!
제일 아래 main 함수에서 코드만 조금 만져주면, 예약까지 문제없이 가능하다.
import re
import time
import requests
from bs4 import BeautifulSoup
class SRT:
def __init__(self, login_type, login_id, login_pwd):
# login_type - 1: 회원번호, 2: 이메일, 3: 휴대전화번호
# login_id - 회원번호/이메일/휴대전화번호
# login_pwd - 비밀번호
self.session = requests.Session()
self.session.get("https://etk.srail.kr/main.do")
self.login_type = login_type
self.login_id = login_id
self.login_pwd = login_pwd
self.stations = self.fetch_stations()
def login(self):
login_url = "https://etk.srail.kr/cmc/01/selectLoginInfo.do"
body = {
"rsvTpCd": "",
"goUrl": "",
"from": "",
"srchDvCd": self.login_type,
"srchDvNm": self.login_id,
"hmpgPwdCphd": self.login_pwd
}
res = self.session.post(login_url, data=body)
if "location.replace('/main.do')" in res.text:
return True
else:
return False
def is_logged_in(self):
res = self.session.get("https://etk.srail.kr/main.do")
if '로그아웃' in res.text:
return True
else:
return False
def fetch_schedule(self, dptRsStnCdNm, arvRsStnCdNm, dptDt, dptTm, adult, child, senior, svrDsb, mldDsb,
chtnDvCd='1', locSeatAttCd1='000', rqSeatAttCd1='015', trnGpCd='300', dlayTnumAplFlg='Y'):
schedule_url = "https://etk.srail.kr/hpg/hra/01/selectScheduleList.do"
body = {
"dptRsStnCd": self.stations[dptRsStnCdNm], # 출발역 코드 (e.g. 0551)
"arvRsStnCd": self.stations[arvRsStnCdNm], # 도착역 코드
"stlbTrnClsfCd": "05", # [추정] 항상 '05' - ~~ Train Classification Code 같은데, 변하지 않음
"psgNum": str(adult+child+senior+svrDsb+mldDsb), # 총 승객 인원
"seatAttCd": rqSeatAttCd1, # [추정] 좌석 속성, rqSeatAttCd1와 같은 값인 것으로 보임
"isRequest": "Y", # [추정] 항상 'Y'
"dptRsStnCdNm": dptRsStnCdNm, # 출발역 이름
"arvRsStnCdNm": arvRsStnCdNm, # 도착역 이름
"dptDt": dptDt, # 출발 날짜 (e.g. 20240108)
"dptTm": dptTm, # 출발 시간 (e.g. 194500)
"chtnDvCd": chtnDvCd, # 여정경로 (1 - 직통, 2 - 환승, 3 - 왕복)
"psgInfoPerPrnb1": str(adult), # 어른 인원
"psgInfoPerPrnb5": str(child), # 어린이 인원
"psgInfoPerPrnb4": str(senior), # 노인 인원
"psgInfoPerPrnb2": str(svrDsb), # 중증장애인 인원
"psgInfoPerPrnb3": str(mldDsb), # 경증장애인 인원
"locSeatAttCd1": locSeatAttCd1, # 좌석위치 (000 - default, 011 - 1인석, 012 - 창측좌석, 013 - 내측좌석)
"rqSeatAttCd1": rqSeatAttCd1, # 좌석속성 (015 - 일반, 021 - 휠체어, 028 - 전동휠체어)
"trnGpCd": trnGpCd, # 차종구분 (109 - 전체, 300 - SRT, 900 - SRT+KTX)
"dlayTnumAplFlg": dlayTnumAplFlg, # 지연열차포함 (Y - 포함, N - 미포함)
}
res = self.session.post(schedule_url, data=body)
soup = BeautifulSoup(res.text, 'html.parser')
trains = soup.find_all("td", {"class": "trnNo"})
result = []
for tr in trains:
schedule_info = dict()
schedule_info["trnOrdrNo"] = tr.find("input", {"name": re.compile(r'trnOrdrNo')})['value']
schedule_info["jrnySqno"] = tr.find("input", {"name": re.compile(r'jrnySqno')})['value']
schedule_info["runDt"] = tr.find("input", {"name": re.compile(r'runDt')})['value']
schedule_info["trnNo"] = tr.find("input", {"name": re.compile(r'trnNo')})['value']
schedule_info["trnGpCd"] = tr.find("input", {"name": re.compile(r'trnGpCd')})['value']
schedule_info["stlbTrnClsfCd"] = tr.find("input", {"name": re.compile(r'stlbTrnClsfCd')})['value']
schedule_info["dptDt"] = tr.find("input", {"name": re.compile(r'dptDt')})['value']
schedule_info["dptTm"] = tr.find("input", {"name": re.compile(r'dptTm')})['value']
schedule_info["dptRsStnCd"] = tr.find("input", {"name": re.compile(r'dptRsStnCd')})['value']
schedule_info["dptRsStnCdNm"] = tr.find("input", {"name": re.compile(r'dptRsStnCdNm')})['value']
schedule_info["dptStnConsOrdr"] = tr.find("input", {"name": re.compile(r'dptStnConsOrdr')})['value']
schedule_info["dptStnRunOrdr"] = tr.find("input", {"name": re.compile(r'dptStnRunOrdr')})['value']
schedule_info["arvRsStnCd"] = tr.find("input", {"name": re.compile(r'arvRsStnCd')})['value']
schedule_info["arvRsStnCdNm"] = tr.find("input", {"name": re.compile(r'arvRsStnCdNm')})['value']
schedule_info["arvStnConsOrdr"] = tr.find("input", {"name": re.compile(r'arvStnConsOrdr')})['value']
schedule_info["arvStnRunOrdr"] = tr.find("input", {"name": re.compile(r'arvStnRunOrdr')})['value']
schedule_info["seatAttCd"] = tr.find("input", {"name": re.compile(r'seatAttCd')})['value']
schedule_info["scarGridcnt"] = tr.find("input", {"name": re.compile(r'scarGridcnt')})['value']
schedule_info["scarNo"] = tr.find("input", {"name": re.compile(r'scarNo')})['value']
schedule_info["seatNo_1"] = tr.find("input", {"name": re.compile(r'seatNo_1')})['value']
schedule_info["seatNo_2"] = tr.find("input", {"name": re.compile(r'seatNo_2')})['value']
schedule_info["seatNo_3"] = tr.find("input", {"name": re.compile(r'seatNo_3')})['value']
schedule_info["seatNo_4"] = tr.find("input", {"name": re.compile(r'seatNo_4')})['value']
schedule_info["seatNo_5"] = tr.find("input", {"name": re.compile(r'seatNo_5')})['value']
schedule_info["seatNo_6"] = tr.find("input", {"name": re.compile(r'seatNo_6')})['value']
schedule_info["seatNo_7"] = tr.find("input", {"name": re.compile(r'seatNo_7')})['value']
schedule_info["seatNo_8"] = tr.find("input", {"name": re.compile(r'seatNo_8')})['value']
schedule_info["seatNo_9"] = tr.find("input", {"name": re.compile(r'seatNo_9')})['value']
schedule_info["trainDiscGenRt"] = tr.find("input", {"name": re.compile(r'trainDiscGenRt')})['value']
schedule_info["rcvdAmt"] = tr.find("input", {"name": re.compile(r'rcvdAmt')})['value']
schedule_info["rcvdFare"] = tr.find("input", {"name": re.compile(r'rcvdFare')})['value']
schedule_info["trnNstpLeadInfo"] = tr.find("input", {"name": re.compile(r'trnNstpLeadInfo')})['value']
result.append(schedule_info)
return result
def fetch_stations(self):
res = self.session.get("https://etk.srail.kr/hpg/hra/01/selectMapInfo.do")
soup = BeautifulSoup(res.text, 'html.parser')
stations = soup.find_all("a", {"class": re.compile(r'map')})
result = dict()
for station in stations:
station_split = station['onclick'].split("'")
result[station_split[3]] = station_split[1]
return result
def book_ticket(self, adult, child, senior, svrDsb, mldDsb, train_schedule, locSeatAttCd, rqSeatAttCd,
isReservation = False, isBusiness = False):
check_user_url = "https://etk.srail.kr/hpg/hra/01/checkUserInfo.do"
reservation_url = "https://etk.srail.kr/hpg/hra/02/requestReservationInfo.do"
confirm_url = "https://etk.srail.kr/hpg/hra/02/confirmReservationInfo.do"
psgGridcnt = 0
psgGrid = []
if adult > 0:
psgGridcnt += 1
psgGrid.append(["1", str(adult)])
if child > 0:
psgGridcnt += 1
psgGrid.append(["5", str(child)])
if senior > 0:
psgGridcnt += 1
psgGrid.append(["4", str(senior)])
if svrDsb > 0:
psgGridcnt += 1
psgGrid.append(["2", str(svrDsb)])
if mldDsb > 0:
psgGridcnt += 1
psgGrid.append(["3", str(mldDsb)])
body = {
"rsvTpCd": "05" if isReservation else "01",
"jobId": "1102" if isReservation else "1101",
"jrnyTpCd": "11",
"jrnyCnt": "1",
"totPrnb": str(adult+child+senior+svrDsb+mldDsb),
"stndFlg": "N",
"trnOrdrNo1": train_schedule['trnOrdrNo'],
"jrnySqno1": train_schedule['jrnySqno'],
"runDt1": train_schedule['runDt'],
"trnNo1": train_schedule['trnNo'],
"trnGpCd1": train_schedule['trnGpCd'],
"stlbTrnClsfCd1": train_schedule['stlbTrnClsfCd'],
"dptDt1": train_schedule['dptDt'],
"dptTm1": train_schedule['dptTm'],
"dptRsStnCd1": train_schedule['dptRsStnCd'],
"dptStnConsOrdr1": train_schedule['dptStnConsOrdr'],
"dptStnRunOrdr1": train_schedule['dptStnRunOrdr'],
"arvRsStnCd1": train_schedule['arvRsStnCd'],
"arvStnConsOrdr1": train_schedule['arvStnConsOrdr'],
"arvStnRunOrdr1": train_schedule['arvStnRunOrdr'],
"scarYn1": "N",
"scarGridcnt1": "",
"scarNo1": "",
"seatNo1_1": "",
"seatNo1_2": "",
"seatNo1_3": "",
"seatNo1_4": "",
"seatNo1_5": "",
"seatNo1_6": "",
"seatNo1_7": "",
"seatNo1_8": "",
"seatNo1_9": "",
"psrmClCd1": "2" if isBusiness else "1",
"smkSeatAttCd1": "000",
"dirSeatAttCd1": "000",
"locSeatAttCd1": locSeatAttCd, # 좌석위치 (000 - default, 011 - 1인석, 012 - 창측좌석, 013 - 내측좌석)
"rqSeatAttCd1": rqSeatAttCd, # 좌석속성 (015 - 일반, 021 - 휠체어, 028 - 전동휠체어)
"etcSeatAttCd1": "000",
"jrnyTpCd1": "",
"jrnyTpCd2": "",
"trnOrdrNo2": "",
"jrnySqno2": "",
"runDt2": "",
"trnNo2": "",
"trnGpCd2": "",
"stlbTrnClsfCd2": "",
"dptDt2": "",
"dptTm2": "",
"dptRsStnCd2": "",
"dptStnConsOrdr2": "",
"dptStnRunOrdr2": "",
"arvRsStnCd2": "",
"arvStnConsOrdr2": "",
"arvStnRunOrdr2": "",
"scarYn2": "",
"scarGridcnt2": "",
"scarNo2": "",
"seatNo2_1": "",
"seatNo2_2": "",
"seatNo2_3": "",
"seatNo2_4": "",
"seatNo2_5": "",
"seatNo2_6": "",
"seatNo2_7": "",
"seatNo2_8": "",
"seatNo2_9": "",
"psrmClCd2": "",
"smkSeatAttCd2": "",
"dirSeatAttCd2": "",
"locSeatAttCd2": "",
"rqSeatAttCd2": "",
"etcSeatAttCd2": "",
"psgGridcnt": psgGridcnt,
"psgTpCd1": "" if len(psgGrid) < 1 else psgGrid[0][0],
"psgInfoPerPrnb1": "" if len(psgGrid) < 1 else psgGrid[0][1],
"psgTpCd2": "" if len(psgGrid) < 2 else psgGrid[1][0],
"psgInfoPerPrnb2": "" if len(psgGrid) < 2 else psgGrid[1][1],
"psgTpCd3": "" if len(psgGrid) < 3 else psgGrid[2][0],
"psgInfoPerPrnb3": "" if len(psgGrid) < 3 else psgGrid[2][1],
"psgTpCd4": "" if len(psgGrid) < 4 else psgGrid[3][0],
"psgInfoPerPrnb4": "" if len(psgGrid) < 4 else psgGrid[3][1],
"psgTpCd5": "" if len(psgGrid) < 5 else psgGrid[4][0],
"psgInfoPerPrnb5": "" if len(psgGrid) < 5 else psgGrid[4][1],
"mutMrkVrfCd": "",
"reqTime": str(int(time.time())*1000),
"crossYn": "N"
}
check_user_res = self.session.post(check_user_url, data=body)
if 'selectLoginForm' in check_user_res.text:
if not self.login():
raise '예약 중 로그인 재시도 실패' # 로그인 실패
check_user_res = self.session.post('https://etk.srail.kr/hpg/hra/01/checkUserInfo.do', data=body)
if 'requestReservationInfo' in check_user_res.text:
reservation_res = self.session.post(reservation_url, data=body)
else:
raise f'예약 중 재로그인 후 user check fail - \n{check_user_res.text}'
elif 'requestReservationInfo' in check_user_res.text:
reservation_res = self.session.post(reservation_url, data=body)
else:
raise f'예약 중 user check fail - \n{check_user_res.text}'
if 'confirmReservationInfo' in reservation_res.text:
confirm_res = self.session.post(confirm_url, data=body)
else:
raise f'예약 중 reservation fail - \n{check_user_res.text}'
if "잔여석없음" in confirm_res.text:
raise "잔여석 없음"
elif "예약대기자한도수초과" in confirm_res.text:
raise "예약대기자 한도수 초과"
print("예약 성공")
return True
if __name__ == "__main__":
srt = SRT('1', '', '')
srt.login()
schedules = srt.fetch_schedule('수서', '부산', '20240112', '053100', 1, 0, 0, 0, 0)
srt.book_ticket(1, 0, 0, 0, 0, schedules[0], '000', '015', True)
'프로젝트 > SRT&KTX 매진표 예매' 카테고리의 다른 글
SRT&KTX 기차표 매크로 예매 - (6) 아이디/비밀번호 저장하기 (0) | 2024.01.12 |
---|---|
SRT&KTX 기차표 매크로 예매 - (5) UI 만들기 (PyQt5) (0) | 2024.01.12 |
SRT&KTX 기차표 매크로 예매 - (3) SRT 승차권 조회 (1) | 2024.01.09 |
SRT&KTX 기차표 매크로 예매 - (2) SRT 로그인 (1) | 2024.01.08 |
SRT&KTX 기차표 매크로 예매 - (1) 계획 및 목표 (1) | 2024.01.08 |