# req-112: portfolio-hub broker credential read 포트 메서드 + HTTP 어댑터 - Status: Implemented - Type: Integration - Priority: High - Owner: trex-core-dev ## Intent - 현재 `PortfolioHubReadAdapter.whoami()`는 `GET /v1/user_info`에서 `daemon_id`만 읽는다. dcy-036 C1에 따라 같은 엔드포인트가 daemon-scoped 단일 `broker` 객체(`account_ref`, `app_key`, `app_secret`, `credential_version`)와 `trading_enabled` 필드를 함께 반환하도록 확장된 portfolio-hub 계약(REQ-049, dcy-010)을 소비해야 한다. - 이 REQ는 트랜스포트 레이어 — 포트 인터페이스 정의, HTTP 어댑터 구현, 테스트용 stub — 만 다루며, KISConfig 구성 로직(REQ-113)과 로드 게이트(REQ-114)의 기반을 제공한다. ## Requirement - `trex_core/application/portfolio.py`에 `BrokerCredentialPayload` 데이터클래스를 추가한다: `account_ref: str`, `app_key: str`, `app_secret: str`, `credential_version: int | None = None`. - `PortfolioHubReadPort` 프로토콜에 `fetch_broker_credential() -> BrokerCredentialPayload | None` 메서드를 추가한다. - 반환 값 None: portfolio-hub 응답 `broker: null` (해당 daemon에 인증정보 없음). - `trading_enabled` 필드도 함께 읽어 반환해야 하므로, 포트 메서드는 `TradingContext` 데이터클래스를 반환한다: `broker: BrokerCredentialPayload | None`, `trading_enabled: bool`. - `PortfolioHubReadAdapter`의 기존 `whoami()` 메서드를 내부적으로 `GET /v1/user_info`를 공유하는 내부 헬퍼(`_fetch_user_info()`)로 리팩토링하고, `fetch_broker_credential()` 구현이 이를 재사용하도록 한다. 외부 동작은 변경하지 않는다(`whoami()` 시그니처·예외 동작 불변, 회귀 차단 NFR-1). - `fetch_broker_credential()`은 다음 규칙을 따른다: - `broker: null` 응답 → `TradingContext(broker=None, trading_enabled=trading_enabled)` 반환. - `credential_payload: null`(복호화 실패) → `TradingContext(broker=None, trading_enabled=trading_enabled)` 반환(정보 부재로 처리). - 네트워크 오류 / 비-2xx / JSON 파싱 실패 → `PortfolioHubUnavailableError` raise. - HTTP 403(`daemon_id` 없는 토큰) → `PortfolioHubUnavailableError` raise. - `trex_core/adapters/portfolio/stub_read.py`의 stub에도 `fetch_broker_credential()` 결정론 stub을 추가한다. - `app_secret` 필드는 어댑터 내에서 로그에 직접 출력하지 않는다(NFR-2 보안 — 마스킹). ## Acceptance Criteria 1. Given daemon_id에 매칭된 broker 인증정보가 있고 `trading_enabled: true`인 `/v1/user_info` 응답, When `fetch_broker_credential()` 호출, Then `TradingContext(broker=BrokerCredentialPayload(account_ref=..., app_key=..., app_secret=...), trading_enabled=True)` 반환. 2. Given `broker: null` 응답(인증정보 없음), When `fetch_broker_credential()` 호출, Then `TradingContext(broker=None, trading_enabled=<응답값>)` 반환 (예외 없음). 3. Given `credential_payload: null`(복호화 실패) 응답, When `fetch_broker_credential()` 호출, Then `TradingContext(broker=None, trading_enabled=<응답값>)` 반환 (예외 없음, 인증정보 부재로 처리). 4. Given 네트워크 오류 또는 비-2xx(403/500 등) 응답, When `fetch_broker_credential()` 호출, Then `PortfolioHubUnavailableError` raise. 5. Given `fetch_broker_credential()` 구현 이후, When `whoami()` 호출, Then 기존 `whoami()` 계약(daemon_id str 반환, PortfolioHubUnavailableError on failure)이 변경되지 않는다 (회귀 없음). 6. Given stub_read.py stub, When `fetch_broker_credential()` 호출, Then 미리 설정한 고정 `TradingContext`를 네트워크 없이 반환한다. 7. Given `fetch_broker_credential()` 구현, When 에러/성공 로그 출력 확인, Then `app_secret` 값이 평문으로 로그에 출력되지 않는다. 8. Given import sweep, When `trex_core/domain`을 검사, Then 신규 포트/어댑터 관련 IO/네트워크 의존이 domain에 없다 (도메인 순수성 불변). ## Impacted Area - Docs: 없음 (baseline 문서 갱신은 dcy-036 scope 밖) - Modules: - `trex_core/application/portfolio.py` — `BrokerCredentialPayload`, `TradingContext` 데이터클래스 신규 정의; `PortfolioHubReadPort`에 `fetch_broker_credential()` 메서드 추가 - `trex_core/adapters/portfolio/hub_read.py` — `_fetch_user_info()` 내부 헬퍼 추출, `fetch_broker_credential()` 구현 추가; `whoami()` 동작 보존 - `trex_core/adapters/portfolio/stub_read.py` — `fetch_broker_credential()` stub 추가 - Tests: - `tests/adapters/portfolio/test_hub_read.py` — AC-1~5, AC-7 케이스 추가 - `tests/adapters/portfolio/test_stub_read.py` — AC-6 stub 케이스 추가 - `tests/domain/test_domain_import_sweep.py` — AC-8 회귀(기존 sweep 재실행) ## Notes - 소비 계약: portfolio-hub `req-049_user-info-daemon-scoped-broker-delivery.md` (Implemented, dcy-010 FR-2). 응답 스키마: `broker: { account_ref, credential_payload: { app_key, app_secret } | null, credential_version, ... } | null` + `trading_enabled: bool`. - `TradingContext`를 별도 데이터클래스로 도입하는 이유: 포트 메서드가 broker 인증정보와 `trading_enabled`를 한 번의 호출로 함께 반환해야 불필요한 중복 HTTP 호출을 피할 수 있기 때문(원자적 읽기). - OQ-4(그날-시작 로드 실패 알람 trex-notifier 연동): Deferred — 알람 발신은 REQ-114 Notes에서 다룬다. - 선행 의존: portfolio-hub dcy-010 / REQ-049 배포 완료 여부에 따라 실 통합 테스트 가능 시점이 달라진다. CI 단계에서는 stub 기반 단위 테스트로 AC-1~8 검증. - Source: dcy-036 C1, FR-1, dcy-010 OQ-2 Answered. ## Change Log ### CHG-20260628-01 - Date: 2026-06-28 - Author: trex-core-dev #### Change Summary - 신규 생성 (Proposed) — dcy-036 C1 산출물. #### Intent - dcy-036 FR-1(그날-시작 portfolio-hub `/v1/user_info`에서 KIS 인증정보 fetch)의 트랜스포트 계층을 REQ로 정규화한다. KISConfig 구성(REQ-113) 및 로드 게이트(REQ-114)의 선행 의존. #### Acceptance Criteria Delta - Added: - AC-1~8 전체 (신규 초안) - Updated: 없음 - Removed: 없음 #### Impacted Area Delta - Docs: 없음 - Modules: - `portfolio.py` 포트 확장 (BrokerCredentialPayload, TradingContext, fetch_broker_credential) - `hub_read.py` 어댑터 확장 (_fetch_user_info 헬퍼, fetch_broker_credential 구현) - `stub_read.py` stub 확장 - Tests: - `test_hub_read.py`, `test_stub_read.py`, `test_domain_import_sweep.py` #### Delivery Trace Delta - Batches: 미정 (suggest-batch-reqs 대기) - Releases: 미정 #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: keep-current - Follow-up Action: confirm-req 승인 게이트 후 REQ-112~115 일괄 batch 배정 검토. #### Validation Plan - pytest로 AC-1~8 각 케이스 확인. app_secret 마스킹(AC-7)은 로그 캡처 검증. domain sweep(AC-8)은 기존 test_domain_import_sweep.py 재실행. ### CHG-20260628-02 - Date: 2026-06-28 - Author: 팀장(Claude) - confirm-req: `Proposed` → `Approved`. dcy-036 FR-1/C1 추적. TradingContext 원자 read(broker+trading_enabled 1회 호출) 설계 타당 — /v1/user_info 단일 응답에 정합. whoami 회귀보존(AC-5)·도메인 순수성(AC-8)·app_secret 마스킹(AC-7) 적절. blocker 없음. 선행 의존 portfolio-hub REQ-049(머지완료). ### CHG-20260628-03 - Date: 2026-06-28 - Author: 팀장(Claude) - confirm-req-implemented: `Approved` → `Implemented`. 근거: BAT-053 PR #34 머지(merge commit 01655ab). fetch 포트(`fetch_broker_credential()`, `BrokerCredentialPayload`, `TradingContext`) + HTTP 어댑터 + stub AC-1~8 전 케이스 pytest 검증 완료. QC PASS-WITH-NOTES(opus, blocker 0). 745 passed/0 회귀.