# 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`, `owner_user_id`, `broker`, `status`, `cash_balance`, ..., `tag_ids`, `managed_by_daemon_id`, ...). `asset_id`는 catalog의 `assets.asset_id`(UUID) 참조. - 응답: 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)를 참조한다. ## 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 토큰 분기 추가) - `app/api/v1_assets.py` — 기존 `list_assets` 함수에서 portfolio 조회 로직 제거(catalog 전용 회수). REQ-013 Deprecated 후속 정리. - `app/schemas/v1.py` — `PortfolioItem`, `PortfolioListResponse` 신규 - `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` 분기 - Tests: - 신규 `tests/test_v1_portfolios_list.py` — AC-1~11 검증 ## 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-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` 시나리오