feat: REQ-005 assets table tag apply/remove SQL alignment (BAT-013)

- REQ-005 CHG-20260521-02 follow-up: clarify AC-1/AC-5 to target assets table
- Add append_asset_tag_id/remove_asset_tag_id on PostgresPortfolioRepository
- Replace internal calls in apply_asset_tag/remove_asset_tag routes (HTTP contract unchanged)
- 7 new regression tests: method existence, SQL target table, error branches, idempotency guard
- Update existing AC-1/AC-5 assertions to verify new methods + portfolios methods uncalled
- Documentation: runtime-flows.md flow body + change log; rel-001 includes BAT-013
- REQ-005 transition: Approved → Implemented (CHG-20260521-03) via BAT-013 verification
- Total tests: 362 → 369 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 00:04:33 +09:00
co-authored by Claude Opus 4.7
parent 9038f41010
commit 58899b1a94
15 changed files with 509 additions and 17 deletions
@@ -17,11 +17,11 @@
- `DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}` — portfolio에서 태그 해제; trade-write 이상, 소유자 본인 또는 admin
## Acceptance Criteria
1. Given admin 토큰, admin이 생성한 활성 tag_id, When `POST /v1/assets/{asset_id}/tags`, Then HTTP 200, 해당 tag_id가 asset의 tag_ids 배열에 추가된다.
1. Given admin 토큰, admin이 생성한 활성 tag_id, When `POST /v1/assets/{asset_id}/tags`, Then HTTP 200, 해당 tag_id가 **assets 테이블의** tag_ids 컬럼 배열에 추가된다 (portfolios 테이블이 아님).
2. Given admin 토큰, is_admin_tag=FALSE인 tag_id, When `POST /v1/assets/{asset_id}/tags`, Then HTTP 403 Forbidden이 반환된다.
3. Given admin 토큰, is_active=FALSE인 tag_id, When `POST /v1/assets/{asset_id}/tags`, Then HTTP 403 Forbidden이 반환된다.
4. Given non-admin 토큰, When `POST /v1/assets/{asset_id}/tags`, Then HTTP 403 Forbidden이 반환된다.
5. Given admin 토큰, When `DELETE /v1/assets/{asset_id}/tags/{tag_id}`, Then HTTP 200, tag_id가 asset의 tag_ids 배열에서 제거된다.
5. Given admin 토큰, When `DELETE /v1/assets/{asset_id}/tags/{tag_id}`, Then HTTP 200, tag_id가 **assets 테이블의** tag_ids 컬럼 배열에서 제거된다 (portfolios 테이블이 아님).
6. Given trade-write 토큰(portfolio 소유자), 소유자 본인이 생성한 tag_id (is_admin_tag=FALSE, is_active=TRUE), When `POST /v1/portfolios/{portfolio_id}/tags`, Then HTTP 200, 태그가 portfolio의 tag_ids에 추가된다.
7. Given trade-write 토큰(portfolio 소유자), admin이 생성한 활성 tag_id, When `POST /v1/portfolios/{portfolio_id}/tags`, Then HTTP 200, 태그가 추가된다.
8. Given trade-write 토큰(user A, 소유자), 다른 user B가 생성한 non-admin tag_id, When `POST /v1/portfolios/{portfolio_id}/tags`, Then HTTP 403 Forbidden이 반환된다.
@@ -32,11 +32,11 @@
## Impacted Area
- Docs: docs/interface-contract.md (/v1/assets/{id}/tags, /v1/portfolios/{id}/tags 엔드포인트 명세 추가)
- Modules:
- app/api/v1_assets.py — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가
- app/api/v1_assets.py — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가; CHG-20260521-02에서 라우트 내부 호출을 assets 테이블 대상 repository 메서드로 교체
- app/api/portfolios.py — POST/DELETE /v1/portfolios/{portfolio_id}/tags 라우트 추가 (또는 별도 파일)
- app/schemas/v1.py — TagApplyRequest 스키마 추가
- app/services/ — 태그 적용/해제 서비스 로직, 정책 검증
- app/repositories/postgres.py — tag_ids 배열 append/remove SQL (array_append, array_remove 또는 동등)
- app/repositories/postgres.py — tag_ids 배열 append/remove SQL (array_append, array_remove 또는 동등). assets 대상은 `append_asset_tag_id`/`remove_asset_tag_id`, portfolios 대상은 `append_portfolio_tag_id`/`remove_portfolio_tag_id`로 분리(CHG-20260521-02).
- Tests: 정책 위반 시 403, 존재하지 않는 tag_id 시 404, admin/non-admin 구분 검증
## Notes
@@ -48,6 +48,93 @@
## Change Log
### CHG-20260521-03
- Date: 2026-05-21
- Author: Justin Ko
#### Change Summary
- Status: Approved → Implemented (CHG-20260521-02로 명문화된 AC-1/AC-5 의도가 BAT-013 verification에서 evidence 확보)
#### Intent
- BAT-013 confirm-batch-verification 완료. assets 테이블 tag_ids 정합화 범위(`append_asset_tag_id`/`remove_asset_tag_id` 신규 + 라우트 호출 교체)에 대한 AC-1·AC-5 신규 evidence + AC-2~4·EC-1 회귀 + AC-6~11(portfolios 경로) 무영향 회귀 모두 확인.
#### Acceptance Criteria Delta
- No change
#### Impacted Area Delta
- No change
#### Delivery Trace Delta
- Batches:
- BAT-013 (release-candidate) — primary trace, assets.tag_ids mutation 정합화 범위 AC-1·AC-5 + EC-1 evidence
- BAT-004 (release-candidate) — AC-2~4·AC-6~11 회귀 evidence 유지; AC-1·AC-5 evidence는 BAT-013에서 superseded (mock 단언이 신규 메서드로 갱신됨)
- Releases:
- REL-001 — BAT-004 trace는 REQ-004 측면에서 유효; REQ-005 측면의 신규 evidence는 BAT-013에서 확보. REL-001에 BAT-013 포함 여부는 release 갱신 시 결정 (또는 신규 REL에 BAT-013 포함).
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: implemented
- Follow-up Action:
- REL-001 release 문서에 BAT-013 trace 반영 (또는 신규 REL 발의 시 BAT-013 포함)
- CLI 동일 mismatch 후속 REQ 발의: `pfhub tag apply asset` / `pfhub tag remove asset`도 `append_portfolio_tag_id`/`remove_portfolio_tag_id` 호출 (REQ-007/008 augmentation 또는 신규 REQ)
- 실 PostgreSQL `assets.tag_ids` mutation 회귀: REL 배포 직후 smoke로 admin POST → GET /v1/assets에서 tag_ids 채워짐 확인
#### Validation Plan
- `tests/test_tags_http_api.py::test_apply_asset_tag_admin_success` (갱신) — `append_asset_tag_id` 호출 단언 + portfolios 미호출 단언
- `tests/test_tags_http_api.py::test_remove_asset_tag_admin_success` (갱신) — `remove_asset_tag_id` 호출 단언 + portfolios 미호출 단언
- `tests/test_asset_catalog.py::test_postgres_repository_has_append_asset_tag_id_method` / `test_postgres_repository_has_remove_asset_tag_id_method` — 메서드 존재 검증
- `tests/test_asset_catalog.py::test_append_asset_tag_id_targets_assets_table_not_portfolios` / `test_remove_asset_tag_id_targets_assets_table_not_portfolios` — SQL 대상 테이블 검증
- `tests/test_asset_catalog.py::test_append_asset_tag_id_raises_when_asset_not_found` / `test_remove_asset_tag_id_raises_when_asset_not_found` — asset 부재 ValueError
- `tests/test_asset_catalog.py::test_append_asset_tag_id_idempotent_guard_present` — 중복 방지 가드
- 전체 회귀: 369 passed in 11.48s
### CHG-20260521-02
- Date: 2026-05-21
- Author: Justin Ko
#### Change Summary
- Status: Implemented → Approved
- AC-1, AC-5의 "asset의 tag_ids 배열" 의도를 "assets 테이블의 tag_ids 컬럼"으로 명문화. 기존 구현이 portfolios 테이블을 대상화하고 있어 AC 의미 충족 미달.
#### Intent
- BAT-012 implementation 단계에서 REL-001 Feedback Handoff 항목으로 발견 (rel-001 117행): `apply_asset_tag`([app/api/v1_assets.py:175](../../../app/api/v1_assets.py#L175))/`remove_asset_tag`([app/api/v1_assets.py:200](../../../app/api/v1_assets.py#L200))가 `repo.append_portfolio_tag_id`/`repo.remove_portfolio_tag_id`를 호출하여 실제로는 **portfolios 테이블**을 update.
- BAT-012로 `assets.tag_ids` 컬럼이 도입됐으나 apply/remove 경로가 정합되지 않아 신설 컬럼이 실제로 채워지지 않는 상태.
- BAT-004 verification이 portfolios.tag_ids 변경 결과로 통과한 흔적(또는 asset_id == portfolio_id 동일 UUID fixture)으로 추정되며 AC-1/AC-5의 진정한 의도(assets 테이블의 tag_ids)는 검증되지 않았다.
- dcy-002 FR-2 — "admin이 assets에 태그를 부여" — 원래 의도에 맞춰 구현·검증 경로를 정합화한다.
#### Acceptance Criteria Delta
- Modified:
- AC-1: "해당 tag_id가 **asset의** tag_ids 배열에 추가된다" → "해당 tag_id가 **assets 테이블의** tag_ids 컬럼 배열에 추가된다 (portfolios 테이블이 아님)"
- AC-5: "tag_id가 **asset의** tag_ids 배열에서 제거된다" → "tag_id가 **assets 테이블의** tag_ids 컬럼 배열에서 제거된다 (portfolios 테이블이 아님)"
- 기타 AC (2,3,4,6~11): 변경 없음.
#### Impacted Area Delta
- Modules added/modified:
- `app/repositories/postgres.py` — `append_asset_tag_id(asset_id, tag_id)`, `remove_asset_tag_id(asset_id, tag_id)` 메서드 신규. SQL은 assets 테이블 대상 `array_append`/`array_remove` UUID[].
- `app/api/v1_assets.py` — `apply_asset_tag`/`remove_asset_tag` 라우트의 `append_portfolio_tag_id`/`remove_portfolio_tag_id` 호출을 신규 assets 대상 메서드로 교체.
- Tests added:
- `tests/test_asset_catalog.py` (또는 `tests/test_v1.py`) — POST /v1/assets/{id}/tags 후 `assets.tag_ids`에 tag_id가 실제 추가되는지 + portfolios.tag_ids는 변동 없는지 회귀 확인. DELETE 동일.
- Docs:
- 변경 없음 (interface-contract.md/runtime-flows.md의 라우트 명세는 동일).
#### Delivery Trace Delta
- Batches:
- BAT-004 (asset/portfolio tag apply-remove HTTP API) — AC-1/AC-5 evidence 무효(portfolios 테이블 대상화로 의미 미충족). BAT-004의 REQ-004 trace는 영향 없음.
- 신규 batch (TBD) — append_asset_tag_id/remove_asset_tag_id 도입 + 라우트 교체 + 회귀 테스트.
- Releases:
- REL-001 — BAT-004 의 REQ-005 trace는 본 augmentation으로 부분 무효. 본 REQ 재구현 결과는 후속 release(REL-002 등) 또는 REL-001 추가 batch 편입 시 trace.
#### Revalidation Impact
- Reverification Needed: yes
- Existing Implementation Invalidated: yes (POST/DELETE /v1/assets/{asset_id}/tags 내부 SQL이 portfolios 테이블을 대상화, AC-1/AC-5 의미상 미충족)
- Status Recommendation: revert-to-approved
- Follow-up Action: 신규 batch 발의 → repository 메서드 추가 → 라우트 교체 → pytest 회귀 (DB 인스턴스로 assets.tag_ids mutation 검증) → confirm-req-implemented 재진입
#### Validation Plan
- pytest로 신규 회귀 케이스 추가: POST /v1/assets/{id}/tags 200 후 PostgresRepository.get(asset_id).tag_ids 배열에 tag_id 포함 확인; DELETE 후 제거 확인. portfolios 테이블은 동일 호출에 대해 무변동 확인.
- BAT-004 기존 AC-1, AC-5 케이스가 portfolios.tag_ids 의존 fixture에 묻혀 통과했는지 점검; assets 전용 fixture로 교체.
### CHG-20260521-01
- Date: 2026-05-21
- Author: Justin Ko