feat: v1 portfolio mutation, sub-resources, ledger path migration (BAT-008~011)

dcy-003/004 기반 v1 API surface 완성 및 BAT-008~011 구현·검증 완료:

- BAT-008 (REQ-019~021,024): 토큰 surface baseline 문서 갱신
- BAT-009 (REQ-025~028): POST/PATCH/DELETE/GET /v1/portfolios CRUD 신설
  - _require_write_token, _get_portfolio_or_404, soft delete, daemon-scoped list
- BAT-010 (REQ-030~032): portfolio tag/comment 서브 라우트 신설 + baseline 갱신
  - GET/POST/DELETE /v1/portfolios/{id}/tags
  - POST/GET/PATCH/DELETE /v1/portfolios/{id}/comments
- BAT-011 (REQ-029): ledger path migration
  - POST /v1/assets/{asset_id}/ledger/trades → /v1/portfolios/{portfolio_id}/ledger/trades
  - v1_ledger.py 제거, disabled 차단(409) 추가
  - test_v1.py / test_daemon_assignment.py 구 path 갱신
- 신규 tests: test_v1_portfolios_crud.py, test_v1_portfolios_list.py,
  test_v1_portfolios_tags_comments.py, test_v1_portfolios_ledger.py
- 전체 suite: 340 passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 07:25:39 +00:00
co-authored by Claude Sonnet 4.6
parent c815ce39b6
commit 4bf6b554a7
50 changed files with 3989 additions and 249 deletions
@@ -0,0 +1,50 @@
# BAT-010 Design
- Status: confirmed
- Batch: BAT-010
## Architecture Summary
- `app/api/v1_portfolios.py`에 portfolio 서브 리소스 라우트 7개(GET tags, POST/GET/PATCH/DELETE comments)를 추가하고 기존 POST/DELETE tag 라우트에 disabled 보호를 보강한다.
- 신규 v1 schema 5종(`PortfolioTagListResponse`, `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`)을 `app/schemas/v1.py`에 추가한다.
- `docs/` baseline 4종을 v1 portfolio surface에 맞게 갱신한다.
## Changed Areas
- Docs:
- `docs/interface-contract.md` — v1 portfolio CRUD + tag/comment 서브 라우트 섹션 추가; ledger path "pending REQ-029" 마킹
- `docs/runtime-flows.md` — portfolio mutation flow + GET 토큰 분기 + soft delete flow 추가
- `docs/data-model.md` — Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md` — app/api/v1_portfolios.py, app/schemas/v1.py 책임 기술 갱신
- Modules:
- `app/api/v1_portfolios.py` — 7개 라우트 추가 + 기존 2개 보강
- `app/schemas/v1.py` — 5개 schema 추가
- Interfaces:
- `GET /v1/portfolios/{portfolio_id}/tags` (신설)
- `POST /v1/portfolios/{portfolio_id}/comments` (신설)
- `GET /v1/portfolios/{portfolio_id}/comments` (신설)
- `PATCH /v1/portfolios/{portfolio_id}/comments/{comment_id}` (신설)
- `DELETE /v1/portfolios/{portfolio_id}/comments/{comment_id}` (신설)
- `POST /v1/portfolios/{portfolio_id}/tags` disabled 보호 보강 (기존 route)
- `DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}` disabled 보호 보강 (기존 route)
## Key Decisions
1. **GET tags 응답 schema**: `PortfolioTagListResponse(tags: list[TagItem], total: int)` — 기존 `TagItem` 재사용. `portfolio.tag_ids` → `repo.get_tags_by_ids(tag_ids)` → dict → TagItem list.
2. **Disabled 보호 위치**: ownership 검증 직후, mutation 수행 직전. `if portfolio.status == "disabled": raise HTTPException(409, "disabled portfolio")`. GET은 차단 없음.
3. **Comment 작성자 ownership**: portfolio ownership만 적용 (REQ-031 Notes 결정). `_get_portfolio_or_404` 헬퍼로 일괄 처리. PATCH/DELETE 시 comment 작성자 ID 별도 검증 없음 — portfolio owner이면 모든 comment 수정/삭제 가능.
4. **Comment schema**: v1 전용 클래스 신설. `PortfolioCommentItem.comment_id` = `PortfolioComment.id`(domain) 매핑.
5. **Comment PATCH/DELETE 404**: `repo.update_comment` / `repo.delete_comment`가 `ValueError("댓글을 찾을 수 없습니다")` 발생 시 HTTP 404로 변환.
6. **Auth guard**: 기존 tag 라우트는 `require_trade_write` + `token_source != "db"` 패턴 유지. 신규 comment 라우트는 `require_read` + `_require_write_token(auth)` (write) / inline service·client 차단 (GET) 패턴 적용.
7. **Import 추가**: `PortfolioComment` domain model import (v1_portfolios.py), schema 5종 import.
## Edge Cases
- `portfolio.tag_ids = []` → `get_tags_by_ids([])` → `{}` → `PortfolioTagListResponse(tags=[], total=0)` 정상
- comment 404: `portfolio_id`가 존재하지만 `comment_id`가 해당 portfolio의 comment가 아닌 경우 → repo `ValueError` → HTTP 404
- disabled portfolio + GET comment: 허용 (AC-8). disabled portfolio + GET tags: 허용 (별도 제한 없음)
- `_get_portfolio_or_404`가 404를 발생시키는 경우(비존재 portfolio): service/client 차단 체크 이전에 발생 가능 → 기존 404 응답이 먼저 반환됨 (AC-11 충족)
## Architecture Impact
- both (project-structure + runtime-flows 갱신)
## Reference Doc Update Plan
- project-structure.md: app/api/v1_portfolios.py에 신규 라우트 기술 추가; app/schemas/v1.py schema 목록 갱신
- runtime-flows.md: portfolio mutation flow (ownership + disabled 보호), GET /v1/portfolios 토큰 분기 flow, soft delete flow (status='disabled' + dependent data 보존) 추가
@@ -0,0 +1,33 @@
# BAT-010 Implementation
- Status: done
- Batch: BAT-010
## Plan Summary
- REQ-030: `apply_portfolio_tag` + `remove_portfolio_tag`에 `_require_write_token` + disabled 409 체크 추가; `get_portfolio_tags` 신설. `PortfolioTagListResponse` 스키마 추가.
- REQ-031: comment CRUD 4종 라우트 신설 (`create_portfolio_comment`, `list_portfolio_comments`, `update_portfolio_comment`, `delete_portfolio_comment`). v1 comment schema 5종 추가.
- REQ-032: baseline 4종 문서 갱신.
## Changed Files
- `app/schemas/v1.py` — 5개 schema 추가: `PortfolioTagListResponse`, `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`
- `app/api/v1_portfolios.py` — import 추가(`PortfolioComment`, 5 schemas, `TagItem`); `_comment_to_item` helper 추가; `apply_portfolio_tag`/`remove_portfolio_tag`에 `_require_write_token` + disabled 409 보강; `get_portfolio_tags` 신설; `create_portfolio_comment`, `list_portfolio_comments`, `update_portfolio_comment`, `delete_portfolio_comment` 4종 신설
- `tests/test_v1_portfolios_tags_comments.py` — 신규 (32 tests; REQ-030 AC-1~11 + REQ-031 AC-1~11 커버)
- `docs/interface-contract.md` — v1 Portfolio API 섹션 신규 추가; tag/comment 서브 라우트 명세 추가; ledger path "pending REQ-029" 마킹
- `docs/runtime-flows.md` — portfolio mutation flow, GET /v1/portfolios 분기 flow, soft delete flow, GET tags flow, comment CRUD flow 추가
- `docs/data-model.md` — Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md` — v1_portfolios.py 책임 갱신 + v1.py schema 목록 갱신
## Execution Log
1. `app/schemas/v1.py` — 5 schema 추가 (PortfolioTagListResponse 외)
2. `app/api/v1_portfolios.py` — import 갱신, `_comment_to_item` 추가, 기존 tag 라우트 보강, 신규 7개 라우트 추가
3. `tests/test_v1_portfolios_tags_comments.py` — 32 test 신규 작성
4. `.venv/bin/python -m pytest tests/test_v1_portfolios_tags_comments.py` → 32/32 PASSED
5. `.venv/bin/python -m pytest` (전체) → 330/330 PASSED (회귀 없음)
6. `docs/` 4종 갱신 (interface-contract, runtime-flows, data-model, project-structure)
## Validation
- `tests/test_v1_portfolios_tags_comments.py`: 32 tests PASSED
- Full suite: 330 tests PASSED, 2 warnings (deprecation, 무관)
## Remaining Risks
- R-3 (Low): `portfolio_comments` 테이블의 실 DB 존재 여부는 통합 테스트(실 DB)에서만 확인 가능. 단위 테스트는 MagicMock으로 커버됨.
@@ -0,0 +1,33 @@
# BAT-010: v1 portfolio sub-resources (tags/comments + baseline docs)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-003
- Included REQ: REQ-030, REQ-031, REQ-032
- Excluded REQ: REQ-029(ledger path migration — trex-basic 동시 배포 제약으로 별도 batch)
## Summary
- v1 portfolio 서브 리소스 라우트를 완성한다: `/v1/portfolios/{id}/tags`(GET 신설 + disabled 보호, REQ-030), `/v1/portfolios/{id}/comments`(CRUD 4종 신설, REQ-031).
- REQ-025~031 완료 후 baseline 문서(`interface-contract.md`, `runtime-flows.md`, `data-model.md`, `project-structure.md`) 정합성을 일괄 회복한다(REQ-032).
## Scope
- In Scope:
- `app/api/v1_portfolios.py` — `GET /v1/portfolios/{id}/tags` 신설, existing POST/DELETE tag routes disabled 보호 보강, comment CRUD 4종 서브라우트 신설
- `app/schemas/v1.py` — `PortfolioTagListResponse`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentItem`, `PortfolioCommentListResponse` 신규 (또는 기존 schema 재사용 확정)
- `docs/interface-contract.md`, `docs/runtime-flows.md`, `docs/data-model.md`, `docs/project-structure.md` — v1 portfolio surface + soft delete 정책 baseline 반영
- `tests/test_v1_portfolios_tags_comments.py` (신규) — REQ-030/031 AC 검증
- Out of Scope:
- `POST /v1/portfolios/{id}/ledger/trades` path 이전 (→ REQ-029, 별도 batch)
- interface-contract.md 구 path `/v1/assets/{id}/ledger/trades` 제거 명세 (→ REQ-029 범위, 본 batch에서는 pending 마킹만)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (Low): REQ-030의 POST/DELETE `/v1/portfolios/{id}/tags` 라우트는 BAT-004(REQ-005) 구현 시 `app/api/v1_portfolios.py`에 이미 존재. disabled portfolio 보호(AC-8) 누락 여부를 design 단계에서 감사 후 보강.
- R-2 (Low): REQ-031 comment 작성자 ownership 정책 — dcy-003 본문은 portfolio ownership을 기준으로 명시. comment 작성자 ownership(본인 comment만 수정/삭제 가능) 적용 여부는 design 단계에서 확정.
- R-3 (Low): REQ-032 interface-contract.md의 ledger path 관련 갱신은 REQ-029 미완료로 해당 섹션을 "pending REQ-029" 마킹 후 defer.
- 선행 완료: BAT-009(REQ-025~028 Implemented) — v1 portfolio CRUD 기반 인가/ownership 패턴 확립.
@@ -0,0 +1,76 @@
# BAT-010 Planning
- Status: confirmed
- Batch: BAT-010
- Profile: standard
## Included REQ
- REQ-030: portfolio tag 적용/해제/조회 v1 라우트 (Interface, Medium)
- REQ-031: portfolio comment CRUD v1 라우트 (Interface, Medium)
- REQ-032: portfolio v1 baseline 문서 갱신 (Documentation, Medium)
## Delivery Plan
### 구현 범위 및 순서
1. **REQ-030 기존 라우트 보강 + GET 신설**
- 기존 `POST /v1/portfolios/{id}/tags`, `DELETE /v1/portfolios/{id}/tags/{tag_id}` 라우트 감사 결과:
- ownership 검증 ✓, tag 정책 검증 ✓ (POST만)
- disabled portfolio 보호 **누락** → AC-8 충족을 위해 추가 필요
- `GET /v1/portfolios/{id}/tags` 신설: `portfolio.tag_ids` → `repo.get_tags_by_ids()` → `PortfolioTagListResponse`
- schema 추가: `PortfolioTagListResponse(tags: list[TagItem], total: int)`
2. **REQ-031 comment CRUD 구현**
- 신규 4개 라우트: `POST`, `GET`, `PATCH`, `DELETE /v1/portfolios/{id}/comments[/{comment_id}]`
- 기존 repo 메서드 재사용: `create_comment`, `list_comments`, `update_comment`, `delete_comment`
- schema 추가 (app/schemas/v1.py): `PortfolioCommentItem`, `PortfolioCommentRequest`, `PortfolioCommentUpdateRequest`, `PortfolioCommentListResponse`
- ownership 검증: `_get_portfolio_or_404` 헬퍼 재사용 (BAT-009 패턴)
- disabled 보호: POST/PATCH/DELETE → HTTP 409; GET → 허용
- comment 작성자 ownership: portfolio ownership만 적용 (REQ-031 Notes 결정)
3. **테스트 작성**
- `tests/test_v1_portfolios_tags_comments.py` 신규
- REQ-030 AC-1~11, REQ-031 AC-1~11 커버
- BAT-009의 `_with_repo` 패치 패턴 재사용
4. **REQ-032 baseline 문서 갱신**
- `docs/interface-contract.md`: v1 portfolio CRUD 엔드포인트 섹션 추가 (REQ-025~028), GET /v1/portfolios/{id}/tags 엔드포인트 추가, comment CRUD 엔드포인트 추가; POST /v1/assets/{asset_id}/ledger/trades 섹션 "pending REQ-029" 마킹
- `docs/runtime-flows.md`: portfolio mutation flow + GET /v1/portfolios 토큰 분기 + soft delete flow 추가
- `docs/data-model.md`: Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- `docs/project-structure.md`: app/api/v1_portfolios.py 책임 갱신 + app/schemas/v1.py schema 목록 갱신
### 선행 조건
- BAT-009 Implemented (REQ-025~028) ✓ — `_get_portfolio_or_404`, `_require_write_token`, `_close_repo` 헬퍼 재사용
- BAT-003 release-candidate (REQ-001/003): `portfolios.tag_ids UUID[]`, `repo.append_portfolio_tag_id/remove_portfolio_tag_id/get_tags_by_ids` ✓
- BAT-004 release-candidate (REQ-004/005): POST/DELETE /v1/portfolios/{id}/tags 이미 구현 ✓
- `app/repositories/portfolio_repository.py`: `create_comment`, `list_comments`, `update_comment`, `delete_comment` 추상 메서드 이미 정의 ✓
- `app/domain/models.py`: `PortfolioComment(id, portfolio_id, content, created_at, updated_at)` ✓
## Dependencies
- Internal:
- BAT-009(REQ-025~028): `_get_portfolio_or_404`, `_require_write_token` 헬퍼, `PortfolioItem` 스키마 패턴
- BAT-003(REQ-001~003): `portfolios.tag_ids`, `repo.get_tags_by_ids`
- BAT-004(REQ-005): POST/DELETE tag 라우트 (보강 대상)
- External:
- 없음
## Design Gate
- Design Required Before Implementation: yes
- Reason:
- REQ-031 comment 작성자 ownership 정책 확정: portfolio ownership만 적용 (결정 완료)
- REQ-030 GET 응답 schema 확정: `PortfolioTagListResponse(tags: list[TagItem], total: int)` (결정 완료)
- REQ-032 ledger path 마킹 방식 확정: "pending REQ-029" 주석 처리 (결정 완료)
- 모든 설계 gate가 사전 해소됨 → design.md 확정 후 구현 착수 가능
## Milestones
1. planning.md confirmed ✓ (현재 단계)
2. design.md confirmed — key decisions 기록
3. REQ-030 disabled 보호 보강 + GET 신설 + tests
4. REQ-031 comment CRUD 4종 + tests
5. REQ-032 baseline docs 4종 갱신
6. 전체 테스트 스위트 통과
7. verification.md 완성
## Risks
- R-1 (Low): 기존 POST/DELETE tag 라우트에 disabled 보호 추가 시 `test_tags_http_api.py` 회귀 가능 → 기존 테스트가 disabled portfolio를 사용하지 않으면 영향 없음; 확인 후 보강
- R-2 (Low): `repo.get_tags_by_ids(portfolio.tag_ids)` — tag_ids가 빈 배열일 경우 빈 dict 반환 → 빈 list 응답으로 정상 처리
- R-3 (Low): `update_comment`/`delete_comment` postgres 구현에서 `portfolio_comments` 테이블 존재 여부 확인 필요
@@ -0,0 +1,55 @@
# BAT-010 Verification
- Status: verified
- Batch: BAT-010
## Acceptance Mapping
### REQ-030 (portfolio tag routes)
- AC-1 (admin tag → POST 200): `test_post_tag_admin_tag_200` PASSED — `repo.append_portfolio_tag_id` called
- AC-2 (own tag → POST 200): `test_post_tag_own_tag_200` PASSED
- AC-3 (타 user non-admin tag → 403): `test_post_tag_other_user_non_admin_tag_403` PASSED
- AC-4 (inactive tag → 403): `test_post_tag_inactive_tag_403` PASSED
- AC-5 (admin + other portfolio + admin tag → 200): `test_post_tag_admin_other_portfolio_admin_tag_200` PASSED
- AC-6 (other portfolio → 403): `test_post_tag_other_portfolio_403` PASSED
- AC-7 (service/client → 403): `test_post_tag_service_token_403`, `test_post_tag_client_token_403` PASSED
- AC-8 (disabled + POST/DELETE → 409): `test_post_tag_disabled_portfolio_409`, `test_delete_tag_disabled_portfolio_409` PASSED
- AC-9 (DELETE own portfolio → 200): `test_delete_tag_own_portfolio_200` PASSED — `repo.remove_portfolio_tag_id` called
- AC-10 (GET tags → 200 + list): `test_get_portfolio_tags_200`, `test_get_portfolio_tags_empty_200` PASSED
- AC-11 (nonexistent portfolio → 404): `test_get_tags_nonexistent_portfolio_404` PASSED
### REQ-031 (portfolio comment routes)
- AC-1 (POST → 201): `test_post_comment_201` PASSED — comment_id, portfolio_id, content verified
- AC-2 (GET → 200 + list): `test_get_comments_200` PASSED — total=2 verified
- AC-3 (PATCH → 200 + updated): `test_patch_comment_200` PASSED — `repo.update_comment` called with correct args
- AC-4 (DELETE → 204): `test_delete_comment_204` PASSED — `repo.delete_comment` called
- AC-5 (other portfolio → 403): `test_post_comment_other_portfolio_403` PASSED
- AC-6 (service/client → 403): `test_post_comment_service_token_403`, `test_post_comment_client_token_403`, `test_get_comments_service_token_403` PASSED
- AC-7 (disabled + POST/PATCH/DELETE → 409): `test_post_comment_disabled_409`, `test_patch_comment_disabled_409`, `test_delete_comment_disabled_409` PASSED
- AC-8 (disabled + GET → 200): `test_get_comments_disabled_portfolio_200` PASSED
- AC-9 (admin + other portfolio → 201/200): `test_post_comment_admin_other_portfolio_201`, `test_get_comments_admin_other_portfolio_200` PASSED
- AC-10 (no auth → 401): `test_post_comment_no_auth_401` PASSED
- AC-11 (nonexistent comment/portfolio → 404): `test_patch_comment_nonexistent_404`, `test_delete_comment_nonexistent_404`, `test_post_comment_nonexistent_portfolio_404` PASSED
### REQ-032 (baseline docs)
- AC-1 (`docs/interface-contract.md` 갱신): v1 Portfolio API 섹션 신규 추가; GET/POST/DELETE /v1/portfolios/{id}/tags; POST/GET/PATCH/DELETE /v1/portfolios/{id}/comments; ledger "pending REQ-029" 마킹
- AC-2 (`docs/runtime-flows.md` 갱신): portfolio mutation flow, GET branching flow, soft delete + dependent data preservation flow, comment CRUD flow 추가
- AC-3 (`docs/data-model.md` 갱신): Portfolio.status='disabled' 의미 + 의존 데이터 보존 정책 추가
- AC-4 (`docs/project-structure.md` 갱신): v1_portfolios.py 책임 갱신 + v1.py schema 목록 갱신
- AC-5 (코드-문서 정합성): interface-contract 명세가 구현 라우트와 일치 확인 (7개 신규 라우트)
- AC-6 (ledger path "pending REQ-029" 마킹): interface-contract.md에 "pending REQ-029" 섹션 추가
## Evidence
- Test:
- `tests/test_v1_portfolios_tags_comments.py`: 32/32 PASSED (3.27s)
- Full suite: 330/330 PASSED (14.11s, 2 deprecation warnings — 무관)
- No regressions in existing tests (BAT-004 tag tests, BAT-009 portfolio CRUD/list tests)
- Manual:
- `app/api/v1_portfolios.py` 코드 검토: disabled 409 check는 ownership 검증 직후, mutation 직전 위치 확인; `_require_write_token` service/client 차단 확인; GET routes는 disabled 허용 확인
- Logs:
- Full suite output: 330 passed, 2 warnings in 14.11s
## Result
- Ready for Release: true
- Blocking Issues:
- 없음