# REQ-028: GET /v1/portfolios 신설 (토큰 분기 조회) - Status: Implemented - Type: Interface - Priority: High - Owner: Justin Ko ## Intent - v1 API에 portfolio 조회 경로(`GET /v1/portfolios`)를 신설해 service/user/admin 세 principal이 단일 엔드포인트로 분기 조회할 수 있도록 한다. - dcy-001 FR-5(REQ-013 Deprecated 처리)에서 회수된 `GET /v1/assets` 통합 endpoint 정의를 본 신규 path로 이전한다(dcy-003 옵션 B 결정). ## Requirement - 신규 라우트 `GET /v1/portfolios` (Bearer 인증 필수). - **토큰 분기 규칙** (dcy-003 OQ-5 결정): - `token_type='service'`: daemon-bound 조회. `daemon_id is None`이면 HTTP 403 fail-closed. 그 외 `managed_by_daemon_id = auth.daemon_id` 필터 + `?market=` 지원. - `token_type='client'`: HTTP 403 fail-closed (dcy-004 REQ-016 폐지 확정 정합). - `token_type ∈ {session, None(env)}`: - `permission='admin'` → 기본 `owner_user_id = auth.user_id`(본인 portfolio) 반환. `?user_id=` 지정 시 해당 user의 portfolio 반환. - `permission ∈ {read, trade-write}` → `owner_user_id = auth.user_id` 필터, 본인 portfolio만 반환. - **`?market=` 필터**: 모든 토큰 타입에서 지원. ` = ANY(assets.markets)`인 portfolio만 반환(catalog와 join). - **`?include_disabled=true` 옵션** (dcy-003 R-2 완화): 기본 응답은 `status='disabled'`인 portfolio 제외. opt-in 시에만 포함. - 응답 항목 schema: 신규 `PortfolioItem` (`portfolio_id`, `asset_id`, `symbol`, `display_name`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조. - `GET /v1/portfolios` 의 각 item은 반드시 `portfolios JOIN assets` 결과를 기반으로 직렬화되어야 한다. 특히 `symbol`/`display_name` 은 linked asset row의 `assets.symbol`/`assets.display_name` 을 그대로 노출해야 하며, 클라이언트 캐시나 후속 catalog lookup으로 복원하는 모델은 정식 응답 계약이 아니다. `assets.display_name`이 NULL이면 응답 `display_name`도 `null`. - 응답: HTTP 200 + `PortfolioListResponse` (`portfolios: list[PortfolioItem]`, `total: int`). ## Acceptance Criteria 1. Given service token(daemon-bound, `daemon_id` 있음), When `GET /v1/portfolios`, Then `managed_by_daemon_id = auth.daemon_id`인 portfolio 목록만 반환. 2. Given service token(`daemon_id` 없음), When `GET /v1/portfolios`, Then HTTP 403 fail-closed. 3. Given session/env token + `permission ∈ {read, trade-write}`, When `GET /v1/portfolios`, Then `owner_user_id = auth.user_id`인 portfolio 목록만 반환. 4. Given admin token (no `?user_id`), When `GET /v1/portfolios`, Then admin 본인의 portfolio 목록 반환. 5. Given admin token + `?user_id=`, When `GET /v1/portfolios`, Then `` user의 portfolio 목록 반환. 6. Given 임의 토큰 + `?market=KRX`, When `GET /v1/portfolios?market=KRX`, Then `'KRX' = ANY(assets.markets)`인 portfolio만 반환. 7. Given disabled portfolio 1건 + active portfolio 1건 보유 user, When `GET /v1/portfolios` (`?include_disabled` 없음), Then active 1건만 반환. 8. Given AC-7 환경 + `?include_disabled=true`, When `GET /v1/portfolios?include_disabled=true`, Then 2건 모두 반환. 9. Given client token, When `GET /v1/portfolios`, Then HTTP 403 fail-closed. 10. Given Bearer token 없음, When `GET /v1/portfolios`, Then HTTP 401. 11. Given 응답 portfolio 항목, When `asset_id` 필드 확인, Then `assets.asset_id`(catalog UUID)를 참조한다. 12. Given `GET /v1/portfolios` 응답 item, When `symbol` 필드를 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.symbol` 과 일치한다. 13. Given `GET /v1/portfolios` 응답 item, When `display_name` 필드를 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.display_name`과 일치한다. 참조 asset의 `display_name`이 NULL이면 응답 필드도 `null`. ## Impacted Area - Docs: - `docs/interface-contract.md` — GET /v1/portfolios 명세 + `?market`/`?user_id`/`?include_disabled` 파라미터 (REQ-032) - `docs/runtime-flows.md` — 토큰 분기 조회 flow + daemon-scoped 필터 (REQ-032) - Modules: - `app/api/v1_portfolios.py` — GET 라우트 (REQ-025의 인증 헬퍼 재사용 + GET 전용 service 토큰 분기 추가); `_portfolio_to_item` 매핑에 `display_name` 포함 (CHG-20260601-01) - `app/api/v1_assets.py` — 기존 `list_assets` 함수에서 portfolio 조회 로직 제거(catalog 전용 회수). REQ-013 Deprecated 후속 정리. - `app/domain/models.py` — `Portfolio` dataclass에 `display_name` 필드 추가 (CHG-20260601-01, asset join 결과 매핑) - `app/schemas/v1.py` — `PortfolioItem`, `PortfolioListResponse` 신규; `PortfolioItem`에 `display_name` 필드 추가 (CHG-20260601-01) - `app/services/portfolio_service.py:list_portfolios` — token-type별 분기 + `?market`/`?include_disabled` 처리. dcy-001 REQ-013 Deprecated 후속 작업 흡수. - `app/repositories/postgres.py:list_portfolios_with_strategies` — `managed_by_daemon_id` 필터 + `markets` array 필터 + `include_disabled` 분기; `assets.display_name` join 보강 (CHG-20260601-01) - Tests: - 신규 `tests/test_v1_portfolios_list.py` — AC-1~11 검증; `display_name` 응답 검증 (AC-13) ## Notes - 본 REQ는 dcy-001 REQ-013 Deprecated(CHG-20260520-01)의 후속 신규 REQ. 토큰 분기 정책은 REQ-013의 본문을 path만 변경해 흡수. - `GET /v1/assets`(catalog 조회 의미)는 본 REQ 범위 밖이며 dcy-001 batch design 단계에서 REQ-010 보강 또는 신규 REQ로 처리. - service token이 NULL `daemon_id`로 호출하면 fail-closed. NULL daemon legacy token 운영 정리는 dcy-004 REQ-023(`migrate-null-daemon` CLI)로 처리. - `?include_disabled` 기본 false 정책은 R-2 완화의 핵심. REQ-027 soft delete와 정합. - Source: dcy-003 FR-5, OQ-5, NFR-1, NFR-3, R-2 완화. dcy-001 REQ-013 후속. ## Change Log ### CHG-20260601-02 - Date: 2026-06-01 - Author: Justin Ko #### Change Summary - Status: Approved → Implemented (CHG-20260601-01 augmentation에 대한 verification evidence 확보) #### Intent - CHG-20260601-01에서 신설된 AC-13(`display_name` 응답 joined `assets.display_name`)이 구현·테스트·로컬 smoke 모두 evidence 확보됨. BAT-009 verification.md에 AC-13 매핑 누적 기록되었고 release-candidate 상태이므로 `confirm-req-implemented` 게이트 통과. #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: - BAT-009 (release-candidate) — augmentation 누적 verification 갱신 - Releases: - REL-001 — local smoke evidence 기록 ([rel-001:55](../../releases/rel-001_20260521_v1-api-full-surface.md#L55)) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: implemented - Follow-up Action: REL-001 실 prod/staging 배포 시 GET /v1/portfolios 응답에 `display_name` 노출 최종 확인 (배포 후 smoke evidence) ### CHG-20260601-01 - Date: 2026-06-01 - Author: Justin Ko #### Change Summary - Status: Implemented → Approved (응답 schema에 `display_name` 필드 추가 augmentation. portfolio가 참조하는 asset의 display_name을 join하여 응답에 포함.) #### Intent - 현행 `GET /v1/portfolios` 응답은 `asset_id`, `symbol`만 노출하고 `display_name`은 클라이언트가 별도 `GET /v1/assets` catalog 조회로 조립해야 한다. UI/CLI 표시 경로에서 N+1 호출 패턴이 발생. 본 augmentation으로 응답 item에 `display_name`을 포함시켜 단일 호출로 표시 가능하도록 한다. 응답 schema는 확장형(필드 추가)이라 기존 클라이언트 호환 유지. #### Acceptance Criteria Delta - Added: - AC-13 — Given `GET /v1/portfolios` 응답 item, When `display_name` 필드 확인, Then 값은 같은 row의 `asset_id`가 가리키는 joined `assets.display_name`과 일치한다. 참조 asset의 `display_name`이 NULL이면 응답 필드도 `null`. - 기존 AC-1~12: 변경 없음. #### Impacted Area Delta - Modules added: - `app/domain/models.py` — `Portfolio` dataclass에 `display_name: str | None = None` 필드 추가 (asset join 결과를 매핑하는 뷰 캐시) - `app/schemas/v1.py` — `PortfolioItem`에 `display_name: str | None = None` 필드 추가 - `app/api/v1_portfolios.py` — `_portfolio_to_item` 매핑에 `display_name` 포함 - `app/repositories/postgres.py` — `list_portfolios` 등 portfolio fetching 경로에 `LEFT JOIN assets`로 `display_name` 보강 - Tests added/updated: - `tests/test_v1_portfolios_list.py` — `_make_portfolio` 헬퍼에 `display_name` 인자 추가 + 응답 검증 케이스 (이미 부분 작업 흔적) #### Delivery Trace Delta - Batches: - BAT-009 (v1-portfolios-crud-core) — augmentation 누적, verification 재실행 필요 - Releases: - REL-001 — scope 잔존, 본 augmentation 포함 (`docs/releases/rel-001_20260521_v1-api-full-surface.md`) #### Revalidation Impact - Reverification Needed: yes - Existing Implementation Invalidated: no (응답 schema 확장. 기존 호출자 backwards-compat. 다만 repo join 회귀 검증 필요) - Status Recommendation: revert-to-approved - Follow-up Action: 구현 → pytest 회귀 → 로컬 smoke로 GET /v1/portfolios 응답에 `display_name` 포함 확인 → confirm-req-implemented 재진입 #### Validation Plan - pytest로 AC-13 검증; 로컬 smoke로 admin/session/service token GET /v1/portfolios 응답에 `display_name` 필드 포함 확인. ### CHG-20260524-02 - Date: 2026-05-24 - Author: Hermes Agent #### Change Summary - Status: Approved → Implemented #### Intent - BAT-014 verification 완료. GET `/v1/portfolios` 응답이 `portfolios JOIN assets` 결과에서 `symbol` 을 직접 반환함이 코드/테스트/문서로 재검증되었다. #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: BAT-014 (release-candidate); BAT-009 response-contract evidence superseded by BAT-014 - Releases: pending #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: promote-to-implemented - Follow-up Action: release 단계에서 smoke 확인 ### CHG-20260524-01 - Date: 2026-05-24 - Author: Hermes Agent #### Change Summary - `GET /v1/portfolios` 응답을 joined asset result 기반으로 명확히 고정하고 `PortfolioItem.symbol` 계약을 추가하기 위해 REQ-028을 augment. 상태를 Implemented → Approved로 되돌림. #### Intent - trex-orbit 자동매매 데몬은 `GET /v1/portfolios` 응답만으로 외부 시세 API 호출 대상 symbol을 바로 얻어야 한다. 기존 계약은 `asset_id`만 제공해 클라이언트 측 catalog cache를 강제했으므로, server-side asset join 결과(`assets.symbol`)를 직접 응답에 포함하도록 명시한다. #### Acceptance Criteria Delta - Added: - AC-12: GET 응답 item의 `symbol` 은 joined `assets.symbol` 과 일치해야 함 #### Impacted Area Delta - Docs: `docs/interface-contract.md` GET `/v1/portfolios` 예시/필드 정의에 `symbol` 및 join 기반 응답 설명 추가 필요 (REQ-032 후속) - Modules: `app/schemas/v1.py`, `app/api/v1_portfolios.py` (`_portfolio_to_item`), `app/repositories/postgres.py` (`_SELECT_PORTFOLIO` join 결과를 응답까지 전달) - Tests: `tests/test_v1_portfolios_list.py` 에 list 응답 `symbol` 검증 추가 필요 #### Delivery Trace Delta - Batches: BAT-009 evidence partially superseded; follow-up BAT-014 예정 - Releases: REL-001 response-contract evidence 일부 재검토 필요 #### Revalidation Impact - Reverification Needed: yes - Existing Implementation Invalidated: yes (response schema evidence partial) - Status Recommendation: revert-to-approved - Follow-up Action: BAT-014에서 GET `/v1/portfolios` 응답이 asset join 결과를 직접 반환하는지 회귀 검증 #### Validation Plan - `tests/test_v1_portfolios_list.py` 에서 각 item의 `symbol` 필드 존재와 `asset_id`가 가리키는 joined asset row의 `symbol` 일치 여부를 검증한다. ### CHG-20260521-01 - Date: 2026-05-21 - Author: Justin Ko #### Change Summary - Status: Approved → Implemented #### Intent - BAT-009 confirm-batch-verification 완료. tests/test_v1_portfolios_list.py AC-1~11 전 케이스 pytest 통과 (2026-05-21, 298 PASSED). 토큰 분기 + status filter + daemon filter 모두 검증. #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: BAT-009 (release-candidate) - Releases: TBD #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: promote-to-implemented - Follow-up Action: 없음 ### CHG-20260520-02 - Date: 2026-05-20 - Author: Justin Ko #### Change Summary - Status: Proposed → Approved #### Intent - 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC 구체성 확인 완료, blocker 없음 #### Acceptance Criteria Delta - No change #### Impacted Area Delta - No change #### Delivery Trace Delta - Batches: none (미정) - Releases: none (미정) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: promote-to-approved - Follow-up Action: batch 계획 편입 ### CHG-20260520-01 - Date: 2026-05-20 - Author: Justin Ko #### Change Summary - 최초 생성 — dcy-003 confirm-discovery 후 draft-req 실행. dcy-003 FR-5 산출물 + REQ-013 Deprecated 후속. #### Intent - v1 portfolio 조회 신설 + 토큰 분기 정책 path 이전 + R-2 disabled 가시성 완화. #### Acceptance Criteria Delta - Added: - AC-1~11 최초 작성 #### Impacted Area Delta - Docs: docs/interface-contract.md, docs/runtime-flows.md (REQ-032) - Modules: app/api/v1_portfolios.py, app/api/v1_assets.py(catalog 회수), app/schemas/v1.py, app/services/portfolio_service.py, app/repositories/postgres.py — 최초 지정 - Tests: tests/test_v1_portfolios_list.py (신규) — 최초 지정 #### Delivery Trace Delta - Batches: none (미정) - Releases: none (미정) #### Revalidation Impact - Reverification Needed: no - Existing Implementation Invalidated: no - Status Recommendation: keep-current - Follow-up Action: confirm-req 후 batch 계획 편입; dcy-001 REQ-013 Deprecated 정리 후속 #### Validation Plan - pytest TestClient로 AC-1~11 검증; 토큰 분기 + `?market` + `?user_id` + `?include_disabled` 시나리오