mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
* feat(srt): 좌석 조회와 탐색 우선순위 추가 SRT search 결과의 stable train_id로 객차별 좌석을 조회하고, 특정 호차/좌석 확인과 탐색 우선순위 옵션을 제공한다. Constraint: SRT와 KTX는 별도 upstream 표면이므로 SRT HTML 파서와 테스트를 분리함 Rejected: KTX 좌석 helper 공유 | Korail API와 SRT 웹 좌석선택 HTML 계약이 달라 혼용하면 파서 안정성이 낮아짐 Confidence: medium Scope-risk: moderate Directive: SRT 좌석선택 HTML에서 노출되지 않는 속성은 추정하지 말고 명시적으로 처리할 것 Tested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_srt_booking scripts.test_ktx_booking; python3 -m py_compile scripts/srt_booking.py scripts/srt_seats.py scripts/test_srt_booking.py Not-tested: 실제 예약 API에 우선순위 좌석 선택을 연결하는 흐름 * fix(srt): 좌석 조회 JSON 출력 안정화 SRT 대기열 메시지가 stdout에 섞여 seats JSON을 깨는 실제 표면 문제를 막고, 누락된 좌석 방향/위치 속성을 unknown으로 정규화한다. Constraint: issue #303 범위는 예약 부작용이 없는 좌석 조회 보조 흐름으로 제한됨 Rejected: 실제 예약 subcommand 추가 | 좌석 선점/예약은 외부 부작용이라 이번 acceptance criteria에 포함되지 않음 Confidence: high Scope-risk: narrow Directive: SRTrain upstream 출력이 추가되더라도 helper stdout은 JSON 전용으로 유지할 것 Tested: RED→GREEN in .omo/ulw-loop/evidence/srt-c002-red-green-tests.txt; live SRT tmux QA in .omo/ulw-loop/evidence/srt-c001-live-search-seats.txt; npm run ci in .omo/ulw-loop/evidence/srt-c003-regression-ci.txt Not-tested: 실제 예약/결제/취소 부작용 흐름 * test(srt): split seat helper regression coverage --------- Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>
128 lines
4 KiB
Python
128 lines
4 KiB
Python
from __future__ import annotations
|
|
|
|
|
|
SEAT_HTML = "\n".join([
|
|
'<li class="scar-01 off"><strong>일반실<br />1호차</strong></li>',
|
|
'<li class="scar-04 on"><a href="#none" onclick="selectScarInfo(\'0004\'); return false;"><strong>일반실<br />4호차</strong></a></li>',
|
|
'<li class="scar-05"><a href="#none" onclick="selectScarInfo(\'0005\'); return false;"><strong>일반실<br />5호차</strong></a></li>',
|
|
'<li class="scar-03 off"><strong>특실<br />3호차</strong></li>',
|
|
'<a href="#none" onclick="selectSeatInfo(this, \'23\', \'6C\'); return false;">6C<strong><em>(정방향, 내측)</em></strong></a>',
|
|
'<a href="#none" onclick="selectSeatInfo(this, \'11\', \'3A\'); return false;">3A<strong><em>(역방향, 창측)</em></strong></a>',
|
|
"<span>5C<strong><em>(정방향, 내측, 선택불가)</em></strong></span>",
|
|
])
|
|
|
|
SPECIAL_SEAT_HTML = "\n".join([
|
|
'<li class="scar-03 on"><a href="#none" onclick="selectScarInfo(\'0003\'); return false;"><strong>특실<br />3호차</strong></a></li>',
|
|
'<li class="scar-05 off"><strong>일반실<br />5호차</strong></li>',
|
|
'<a href="#none" onclick="selectSeatInfo(this, \'31\', \'1A\'); return false;">1A<strong><em>(정방향, 1인석)</em></strong></a>',
|
|
"<span>2C<strong><em>(역방향, 내측, 선택불가)</em></strong></span>",
|
|
])
|
|
|
|
|
|
class FakeTrain:
|
|
train_number = "313"
|
|
dep_date = "20260610"
|
|
dep_time = "080000"
|
|
arr_date = "20260610"
|
|
arr_time = "103400"
|
|
train_code = "17"
|
|
train_name = "SRT"
|
|
dep_station_code = "0551"
|
|
dep_station_name = "수서"
|
|
arr_station_code = "0020"
|
|
arr_station_name = "부산"
|
|
dep_station_run_order = "000001"
|
|
arr_station_run_order = "000007"
|
|
general_seat_state = "예약가능"
|
|
special_seat_state = "매진"
|
|
reserve_wait_possible_code = "-2"
|
|
|
|
def general_seat_available(self) -> bool:
|
|
return True
|
|
|
|
def special_seat_available(self) -> bool:
|
|
return False
|
|
|
|
def reserve_standby_available(self) -> bool:
|
|
return False
|
|
|
|
|
|
class FakeResponse:
|
|
def __init__(self, text: str) -> None:
|
|
self.text = text
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self) -> None:
|
|
self.calls: list[dict[str, str]] = []
|
|
|
|
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
|
|
self.calls.append(params)
|
|
car = params["scarNo1"] or "0004"
|
|
return FakeResponse(SEAT_HTML.replace("scar-04 on", f"scar-{car[-2:]} on"))
|
|
|
|
|
|
class FakeClient:
|
|
def __init__(self, train: FakeTrain) -> None:
|
|
self.train = train
|
|
self._session = FakeSession()
|
|
|
|
def search_train(
|
|
self,
|
|
_dep: str,
|
|
_arr: str,
|
|
_date: str,
|
|
_time: str,
|
|
_time_limit: str | None = None,
|
|
available_only: bool = True,
|
|
) -> list[FakeTrain]:
|
|
return [self.train]
|
|
|
|
|
|
class NoisySession(FakeSession):
|
|
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
|
|
print("접속자가 많아 대기열에 들어갑니다.")
|
|
return super().get(_url, params)
|
|
|
|
|
|
class NoisyClient(FakeClient):
|
|
def __init__(self, train: FakeTrain) -> None:
|
|
self.train = train
|
|
self._session = NoisySession()
|
|
|
|
def search_train(
|
|
self,
|
|
_dep: str,
|
|
_arr: str,
|
|
_date: str,
|
|
_time: str,
|
|
_time_limit: str | None = None,
|
|
available_only: bool = True,
|
|
) -> list[FakeTrain]:
|
|
print("대기인원: 6명")
|
|
return [self.train]
|
|
|
|
|
|
class EmptyClient(FakeClient):
|
|
def search_train(
|
|
self,
|
|
_dep: str,
|
|
_arr: str,
|
|
_date: str,
|
|
_time: str,
|
|
_time_limit: str | None = None,
|
|
available_only: bool = True,
|
|
) -> list[FakeTrain]:
|
|
return []
|
|
|
|
|
|
class SpecialSession(FakeSession):
|
|
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
|
|
self.calls.append(params)
|
|
return FakeResponse(SPECIAL_SEAT_HTML)
|
|
|
|
|
|
class SpecialClient(FakeClient):
|
|
def __init__(self, train: FakeTrain) -> None:
|
|
self.train = train
|
|
self._session = SpecialSession()
|