feat: 태그 HTTP API(BAT-004) 및 pfhub tag CLI + baseline 문서 갱신(BAT-005)
BAT-004 (REQ-004, REQ-005):
- app/api/v1_tags.py: POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{id}/deactivate
- app/api/v1_portfolios.py: POST/DELETE /v1/portfolios/{id}/tags
- app/api/v1_assets.py: POST/DELETE /v1/assets/{id}/tags 엔드포인트 추가
- app/schemas/v1.py: TagItem, TagCreateRequest, TagListResponse, TagApplyRequest/Response 추가
- app/repositories/portfolio_repository.py, postgres.py: append/remove_portfolio_tag_id 추가
- app/main.py: v1_portfolios_router, v1_tags_router 등록
- tests/test_tags_http_api.py: 22개 HTTP API 테스트
BAT-005 (REQ-007, REQ-008):
- app/cli/commands/tag.py: pfhub tag create/list/deactivate/apply/remove 7개 커맨드 신규
- app/cli/main.py: tag_app 등록
- app/cli/commands/portfolio.py: pfhub portfolio tag add/list/remove 완전 제거(breaking change)
- app/cli/core/utils.py: print_tag_table + list[Tag] render 분기 추가
- tests/test_tags_cli.py: REQ-007 AC-1~9 커버 24개 CLI 테스트 신규
- tests/test_cli.py: 제거된 portfolio tag 테스트 2개 삭제
- docs/data-model.md, interface-contract.md, project-structure.md, runtime-flows.md: 태그 모델 baseline 갱신(REQ-008)
- docs/batches/: BAT-004, BAT-005 전체 산출물(release-candidate)
247 passed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# BAT-004 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-004
|
||||
|
||||
## Architecture Summary
|
||||
- REQ-004: `app/api/v1_tags.py` 신규 라우터 파일(prefix="/v1"), POST /v1/tags / GET /v1/tags / PATCH /v1/tags/{tag_id}/deactivate 엔드포인트.
|
||||
- REQ-005(asset side): 기존 `app/api/v1_assets.py`에 POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가. v1 API에서 asset_id = portfolio_id이므로 두 엔드포인트 모두 `portfolios.tag_ids`를 조작.
|
||||
- REQ-005(portfolio side): `app/api/v1_portfolios.py` 신규 라우터 파일(prefix="/v1/portfolios"), POST/DELETE /v1/portfolios/{portfolio_id}/tags 엔드포인트. 기존 `portfolios.py`(prefix="/portfolios")와 path prefix가 달라 충돌 없음.
|
||||
- 정책 검증 로직은 서비스 레이어 없이 라우터에서 직접 처리(현재 아키텍처 패턴 유지).
|
||||
- tag_ids 배열 조작: `append_portfolio_tag_id` / `remove_portfolio_tag_id` 신규 메서드를 postgres.py에 추가.
|
||||
|
||||
## Changed Areas
|
||||
- Docs: docs/interface-contract.md (엔드포인트 명세 — REQ-008 배치에서 일괄 갱신)
|
||||
- Modules:
|
||||
- `app/api/v1_tags.py` — 신규 파일 (POST/GET/PATCH /v1/tags)
|
||||
- `app/api/v1_portfolios.py` — 신규 파일 (POST/DELETE /v1/portfolios/{portfolio_id}/tags)
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
- `app/repositories/portfolio_repository.py` — append_portfolio_tag_id, remove_portfolio_tag_id 프로토콜 추가
|
||||
- `app/repositories/postgres.py` — append_portfolio_tag_id, remove_portfolio_tag_id 구현 추가
|
||||
- `app/main.py` — v1_tags, v1_portfolios 라우터 등록
|
||||
- `tests/test_tags_http_api.py` — 신규 테스트 파일
|
||||
- Interfaces:
|
||||
- POST /v1/tags — 태그 생성 (trade-write 이상, DB token 필수)
|
||||
- GET /v1/tags — 태그 목록 (read 이상, ?is_active 필터, DB token 필수)
|
||||
- PATCH /v1/tags/{tag_id}/deactivate — 태그 비활성화 (admin 또는 생성자 본인, DB token 필수)
|
||||
- POST /v1/assets/{asset_id}/tags — asset(portfolio)에 태그 적용 (admin, DB token 필수)
|
||||
- DELETE /v1/assets/{asset_id}/tags/{tag_id} — asset에서 태그 해제 (admin, DB token 필수)
|
||||
- POST /v1/portfolios/{portfolio_id}/tags — portfolio에 태그 적용 (trade-write, 소유자/admin, DB token 필수)
|
||||
- DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id} — portfolio에서 태그 해제 (trade-write, 소유자/admin, DB token 필수)
|
||||
|
||||
## Key Decisions
|
||||
1. **v1_tags.py 신규 파일 분리**: tags 도메인 전용 라우터를 별도 파일로 분리해 v1_assets.py(asset 카탈로그 CRUD)와 관심사 분리. 이후 REQ-007 CLI 추가 시 충돌 없음.
|
||||
2. **v1_portfolios.py 신규 파일**: REQ-005 portfolio tag 엔드포인트는 `/v1/portfolios` prefix. 기존 `portfolios.py`는 `/portfolios` prefix(v1 아님)이므로 새 라우터 파일 생성이 필요. 두 파일의 path가 완전히 달라 충돌 없음.
|
||||
3. **asset_id = portfolio_id (v1 API 규약)**: `/v1/assets/{asset_id}/tags`는 catalog asset 테이블이 아니라 portfolios 테이블의 tag_ids를 조작. v1 API에서 portfolio_id를 asset_id로 노출하는 기존 규약 준수. `repo.get(asset_id)` 로 portfolio 존재 확인.
|
||||
4. **소문자 정규화 위치**: POST /v1/tags 라우터에서 `name = body.name.strip().lower()`. DB는 TEXT UNIQUE 그대로(BAT-003 설계 일관성).
|
||||
5. **정책 검증 위치**: 라우터에서 직접 `repo.get_tag_by_id()` 호출 후 조건 검사 → 서비스 레이어 없이 라우터 직접 처리(현재 아키텍처 패턴 유지).
|
||||
6. **array_append 멱등성**: `UPDATE portfolios SET tag_ids = array_append(tag_ids, %s::uuid) WHERE portfolio_id = %s AND NOT (%s::uuid = ANY(tag_ids))` — tag_id가 이미 배열에 있으면 UPDATE 행 0건 → rowcount=0인 경우 portfolio 존재를 별도 확인, 존재하면 멱등 처리(200 반환).
|
||||
7. **deactivate 멱등성**: `repo.get_tag_by_id(tag_id)` 먼저 호출 → is_active=FALSE이면 바로 현재 상태 반환(200). is_active=TRUE이면 `repo.deactivate_tag(tag_id)` 실행. tag 존재하지 않으면 404.
|
||||
8. **auth permission 분기(v1 패턴)**: 모든 v1 엔드포인트는 `auth.token_source != "db"` 이면 403 반환(기존 v1_assets.py 패턴 일관성 유지). permission_dependency(Depends) 사용해 최소 권한 확인 후 라우터에서 세부 조건 추가 확인.
|
||||
|
||||
## Edge Cases
|
||||
- 존재하지 않는 tag_id → PATCH /deactivate → 404 (get_tag_by_id 결과 None)
|
||||
- 존재하지 않는 portfolio_id → POST/DELETE /assets|portfolios/{id}/tags → 404
|
||||
- non-admin 비생성자 → PATCH /deactivate → 403
|
||||
- is_admin_tag=FALSE tag_id → POST /v1/assets/{asset_id}/tags → 403
|
||||
- is_active=FALSE tag_id → POST /v1/assets/{asset_id}/tags 또는 /portfolios → 403
|
||||
- 다른 user 생성 non-admin tag → POST /v1/portfolios/{id}/tags (소유자 토큰) → 403
|
||||
- 이미 tag_ids 배열에 있는 tag_id 재적용 → 200 멱등 반환 (array_append 조건부 + rowcount 분기)
|
||||
- 이미 is_active=FALSE → PATCH /deactivate 재요청 → 200 멱등 반환
|
||||
- env token (token_source="env") → 403 DB token required (v1 패턴 일관성)
|
||||
|
||||
## Architecture Impact
|
||||
- both (project-structure: 신규 라우터 파일 2개, runtime-flows: 태그 HTTP 흐름 신규)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: REQ-008 배치에서 갱신 (v1_tags.py, v1_portfolios.py 추가, pfhub tag 그룹 추가)
|
||||
- runtime-flows.md: REQ-008 배치에서 갱신 (POST/GET/PATCH /v1/tags 흐름, POST/DELETE /v1/assets|portfolios/{id}/tags 흐름)
|
||||
|
||||
## Baseline Comparison (vs project-structure.md / runtime-flows.md)
|
||||
- project-structure.md 현재: app/api/ — v1_assets.py, v1_user_info.py, v1_ledger.py만 v1 라우터로 기록됨. v1_tags.py, v1_portfolios.py 추가 필요.
|
||||
- runtime-flows.md 현재: GET /v1/assets, GET /v1/user_info, POST ledger/trades, POST /auth/service-tokens 흐름만 기록됨. tags CRUD HTTP 흐름, 태그 적용/해제 HTTP 흐름 추가 필요.
|
||||
- 두 baseline 문서 갱신은 REQ-008 배치에서 수행.
|
||||
@@ -0,0 +1,39 @@
|
||||
# BAT-004 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-004
|
||||
|
||||
## Plan Summary
|
||||
- REQ-004: v1_tags.py 신규 라우터 + TagItem/TagCreateRequest/TagListResponse 스키마 + main.py 등록 + 소문자 정규화 + 권한 분기 + 비활성화 멱등성
|
||||
- REQ-005: v1_assets.py asset 태그 라우트 + v1_portfolios.py portfolio 태그 라우트 + TagApplyRequest/TagApplyResponse 스키마 + postgres.py array_append/remove + 정책 검증
|
||||
- 전체 pytest suite 228 passed, regression 없음
|
||||
|
||||
## Changed Files
|
||||
- `app/api/v1_tags.py` (신규) — POST/GET/PATCH /v1/tags
|
||||
- `app/api/v1_portfolios.py` (신규) — POST/DELETE /v1/portfolios/{portfolio_id}/tags
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가, TagApplyRequest/TagApplyResponse 임포트 추가
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
- `app/repositories/portfolio_repository.py` — append_portfolio_tag_id, remove_portfolio_tag_id 프로토콜 추가
|
||||
- `app/repositories/postgres.py` — append_portfolio_tag_id, remove_portfolio_tag_id 구현 추가
|
||||
- `app/main.py` — v1_tags, v1_portfolios 라우터 등록
|
||||
- `tests/test_tags_http_api.py` (신규) — 22개 테스트
|
||||
|
||||
## Execution Log
|
||||
1. app/schemas/v1.py: TagItem, TagCreateRequest, TagListResponse, TagApplyRequest, TagApplyResponse 추가
|
||||
2. app/repositories/portfolio_repository.py: append_portfolio_tag_id / remove_portfolio_tag_id 프로토콜 추가
|
||||
3. app/repositories/postgres.py: append_portfolio_tag_id (array_append + idempotent rowcount 분기) / remove_portfolio_tag_id 구현 추가
|
||||
4. app/api/v1_tags.py 신규: create_tag(소문자 정규화, is_admin_tag 분기), list_tags(is_active 필터), deactivate_tag(admin/creator 검증, 멱등성)
|
||||
5. app/api/v1_portfolios.py 신규: apply_portfolio_tag(소유자/admin 확인, 태그 정책 검증), remove_portfolio_tag(소유자/admin 확인)
|
||||
6. app/api/v1_assets.py: apply_asset_tag(admin only, is_admin_tag=TRUE AND is_active=TRUE), remove_asset_tag(admin only) 추가
|
||||
7. app/main.py: v1_tags, v1_portfolios 라우터 등록
|
||||
8. tests/test_tags_http_api.py: 22개 AC 테스트 작성 (REQ-004 AC-1~11, REQ-005 AC-1~11)
|
||||
9. pytest 전체 suite 실행 → 228 passed, 0 failed
|
||||
|
||||
## Validation
|
||||
- pytest 실행 결과: **228 passed, 0 failed** (2026-05-20)
|
||||
- 신규 테스트: test_tags_http_api.py 22개 (전체 통과)
|
||||
- 기존 regression: test_cli.py, test_v1.py, test_asset_catalog.py, test_portfolio_tags.py, test_portfolio_uuid.py, test_daemon_assignment.py, test_tags_master.py 전체 통과
|
||||
|
||||
## Remaining Risks
|
||||
- PATCH /v1/tags/{tag_id}/deactivate: 비생성자 non-admin 시 403 확인은 unit test로 커버. live DB에서 auth.user_id가 올바르게 채워지는지는 운영 환경 연동 테스트 필요.
|
||||
- append_portfolio_tag_id의 idempotent 처리(rowcount=0 분기): 단위 테스트로 커버되지 않았으며 live DB에서 정상 동작 확인 필요.
|
||||
@@ -0,0 +1,36 @@
|
||||
# BAT-004: tags HTTP API
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-002
|
||||
- Included REQ: REQ-004, REQ-005
|
||||
- Excluded REQ: (없음 — dcy-002 HTTP API 2개 전부 포함)
|
||||
|
||||
## Summary
|
||||
- tags 마스터에 대한 HTTP API(POST/GET/PATCH /v1/tags)를 신규 라우터로 추가하고(REQ-004), asset 및 portfolio에 태그를 적용·해제하는 HTTP API(POST/DELETE /v1/assets/{id}/tags, POST/DELETE /v1/portfolios/{id}/tags)를 구현한다(REQ-005).
|
||||
- 이 배치 이후 CLI 계층(REQ-007)과 baseline 문서 갱신(REQ-008)은 별도 배치에서 수행한다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `app/api/v1_tags.py` 신규 라우터 (POST/GET/PATCH /v1/tags) — REQ-004
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse, TagApplyRequest 스키마 추가
|
||||
- `app/repositories/portfolio_repository.py` — insert_tag, get_tags, deactivate_tag 등 프로토콜 보완
|
||||
- `app/repositories/postgres.py` — 태그 CRUD SQL 구현 보완, tag_ids array_append/array_remove SQL
|
||||
- `app/api/v1_assets.py` — POST/DELETE /v1/assets/{asset_id}/tags 라우트 추가 — REQ-005
|
||||
- `app/api/portfolios.py` — POST/DELETE /v1/portfolios/{portfolio_id}/tags 라우트 추가 — REQ-005
|
||||
- `app/main.py` — v1_tags 라우터 등록
|
||||
- Out of Scope:
|
||||
- pfhub tag CLI (→ REQ-007, 다음 배치)
|
||||
- baseline 문서 갱신 (→ REQ-008, 다음 배치)
|
||||
- DB 스키마 변경 (→ BAT-003 완료)
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- R-1(Medium): 권한 분기(trade-write/admin/owner) 조건이 복잡 — 정책 오류 시 AC 위반 위험
|
||||
- REQ-004와 REQ-005는 같은 tags 도메인 HTTP 레이어이며 정책 검증 로직을 공유 → 함께 구현·검증
|
||||
- 선행 의존: BAT-003(release-candidate) — tags 테이블, portfolios.tag_ids, assets.tag_ids 완료 상태
|
||||
@@ -0,0 +1,46 @@
|
||||
# BAT-004 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-004
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-004: tags 마스터 HTTP API (POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{tag_id}/deactivate)
|
||||
- REQ-005: 태그 적용·해제 HTTP API (POST/DELETE /v1/assets/{asset_id}/tags, POST/DELETE /v1/portfolios/{portfolio_id}/tags)
|
||||
|
||||
## Delivery Plan
|
||||
1. **REQ-004 — v1_tags.py 신규 라우터**
|
||||
- `app/api/v1_tags.py` 파일 생성 — POST/GET/PATCH /v1/tags 엔드포인트
|
||||
- `app/schemas/v1.py` — TagItem, TagCreateRequest, TagListResponse 추가
|
||||
- `app/main.py` — v1_tags 라우터 등록
|
||||
- 이름 소문자 정규화(`str.lower()`)는 라우터 레이어에서 수행
|
||||
- 권한: POST는 trade-write 이상, GET은 read 이상, PATCH는 admin 또는 생성자 본인
|
||||
- 비활성화 멱등 처리(이미 is_active=FALSE → 200 반환)
|
||||
2. **REQ-005 — 태그 적용·해제 라우트 추가**
|
||||
- `app/api/v1_assets.py` — POST /v1/assets/{asset_id}/tags, DELETE /v1/assets/{asset_id}/tags/{tag_id}
|
||||
- `app/api/portfolios.py` — POST /v1/portfolios/{portfolio_id}/tags, DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id}
|
||||
- `app/schemas/v1.py` — TagApplyRequest 추가
|
||||
- `app/repositories/postgres.py` — tag_ids 배열 append/remove SQL (array_append, array_remove)
|
||||
- 정책 검증: asset → is_admin_tag=TRUE AND is_active=TRUE, portfolio → is_active=TRUE AND (is_admin_tag=TRUE OR created_by_user_id=소유자)
|
||||
3. **전체 pytest 실행 — regression 확인**
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-003(release-candidate): tags 테이블, portfolios.tag_ids UUID[], assets.tag_ids UUID[], create_tag/get_tag_by_id/get_tags_by_ids/list_tags_master/deactivate_tag 프로토콜 및 postgres 구현 완료
|
||||
- app/core/auth.py: require_read, require_trade_write, require_admin 기존 permission dependency 재사용
|
||||
- External:
|
||||
- 없음
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 신규 라우터 파일(v1_tags.py) 추가, 권한 분기(trade-write/admin/owner) 설계, 정책 검증 로직 위치 결정(라우터 직접 vs 서비스 레이어), array_append/remove 멱등성 처리 방식 결정이 필요하다.
|
||||
|
||||
## Milestones
|
||||
1. REQ-004 라우터 구현 완료 + 스키마 추가 + tests
|
||||
2. REQ-005 apply/remove 라우트 구현 완료 + policy 검증 + tests
|
||||
3. 전체 pytest suite 통과 (regression 없음)
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): 권한 분기(trade-write/admin/owner) 조건 복잡 — PATCH /deactivate에서 admin vs 생성자 본인 구분, portfolio 태그 적용에서 owner_user_id 확인 오류 가능성
|
||||
- R-2 (Low): array_append 중복 허용 여부 — 동일 tag_id를 이미 포함한 배열에 재적용 시 멱등 처리 설계 필요
|
||||
- R-3 (Low): assets.tag_ids 컬럼이 BAT-003 ensure_schema()에 추가됨 — REQ-002 live DB 마이그레이션 완료 여부 확인 필요 (테스트 환경은 ensure_schema로 자동 생성)
|
||||
@@ -0,0 +1,70 @@
|
||||
# BAT-004 Verification
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-004
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-004:
|
||||
- AC-1: POST /v1/tags {"name": "TestTag"} → HTTP 201, name="testtag"(소문자 정규화) → `test_create_tag_lowercase_normalization`
|
||||
- AC-2: admin 토큰 → is_admin_tag=TRUE → `test_create_tag_admin_is_admin_tag_true`
|
||||
- AC-3: non-admin trade-write 토큰 → is_admin_tag=FALSE → `test_create_tag_non_admin_is_admin_tag_false`
|
||||
- AC-4: 동일 이름(대소문자 변형) 중복 → HTTP 409 → `test_create_tag_duplicate_name_returns_409`
|
||||
- AC-5: GET /v1/tags → HTTP 200, 활성 태그 목록 → `test_list_tags_active_only`
|
||||
- AC-6: GET /v1/tags?is_active=false → 비활성 태그만 반환 → `test_list_tags_inactive_filter`
|
||||
- AC-7: admin 토큰 PATCH /deactivate → HTTP 200, is_active=FALSE → `test_deactivate_tag_admin`
|
||||
- AC-8: 생성자(non-admin) 토큰 본인 태그 비활성화 → HTTP 200 → `test_deactivate_tag_creator`
|
||||
- AC-9: non-admin 비생성자 PATCH /deactivate → HTTP 403 → `test_deactivate_tag_non_creator_returns_403`
|
||||
- AC-10: 존재하지 않는 tag_id PATCH /deactivate → HTTP 404 → `test_deactivate_nonexistent_tag_returns_404`
|
||||
- AC-11: 이미 is_active=FALSE 태그 재요청 → HTTP 200 멱등 → `test_deactivate_already_inactive_tag_idempotent`
|
||||
|
||||
- REQ-005:
|
||||
- AC-1: admin + admin 생성 활성 tag → POST /v1/assets/{id}/tags → HTTP 200, tag_ids 추가 → `test_apply_asset_tag_admin_success`
|
||||
- AC-2: admin + is_admin_tag=FALSE tag → HTTP 403 → `test_apply_asset_tag_non_admin_tag_returns_403`
|
||||
- AC-3: admin + is_active=FALSE tag → HTTP 403 → `test_apply_asset_tag_inactive_returns_403`
|
||||
- AC-4: non-admin 토큰 → POST /v1/assets/{id}/tags → HTTP 403 → `test_apply_asset_tag_non_admin_token_returns_403`
|
||||
- AC-5: admin → DELETE /v1/assets/{id}/tags/{tag_id} → HTTP 200, tag_id 제거 → `test_remove_asset_tag_admin_success`
|
||||
- AC-6: 소유자, 본인 생성 non-admin 활성 tag → POST /v1/portfolios/{id}/tags → HTTP 200 → `test_apply_portfolio_tag_owner_own_tag`
|
||||
- AC-7: 소유자, admin 생성 활성 tag → POST /v1/portfolios/{id}/tags → HTTP 200 → `test_apply_portfolio_tag_owner_admin_tag`
|
||||
- AC-8: 소유자, 타 user 생성 non-admin tag → HTTP 403 → `test_apply_portfolio_tag_other_user_non_admin_tag_returns_403`
|
||||
- AC-9: 소유자/admin → DELETE /v1/portfolios/{id}/tags/{tag_id} → HTTP 200 → `test_remove_portfolio_tag_owner_success`
|
||||
- AC-10: 존재하지 않는 tag_id → HTTP 404 → `test_apply_portfolio_tag_nonexistent_tag_returns_404`
|
||||
- AC-11: 존재하지 않는 portfolio_id → HTTP 404 → `test_apply_portfolio_tag_nonexistent_portfolio_returns_404`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Test:
|
||||
- pytest 실행 결과: **228 passed, 0 failed** (2026-05-20)
|
||||
- 신규 테스트: `tests/test_tags_http_api.py` 22개
|
||||
- `test_create_tag_lowercase_normalization`
|
||||
- `test_create_tag_admin_is_admin_tag_true`
|
||||
- `test_create_tag_non_admin_is_admin_tag_false`
|
||||
- `test_create_tag_duplicate_name_returns_409`
|
||||
- `test_list_tags_active_only`
|
||||
- `test_list_tags_inactive_filter`
|
||||
- `test_deactivate_tag_admin`
|
||||
- `test_deactivate_tag_creator`
|
||||
- `test_deactivate_tag_non_creator_returns_403`
|
||||
- `test_deactivate_nonexistent_tag_returns_404`
|
||||
- `test_deactivate_already_inactive_tag_idempotent`
|
||||
- `test_apply_asset_tag_admin_success`
|
||||
- `test_apply_asset_tag_non_admin_tag_returns_403`
|
||||
- `test_apply_asset_tag_inactive_returns_403`
|
||||
- `test_apply_asset_tag_non_admin_token_returns_403`
|
||||
- `test_remove_asset_tag_admin_success`
|
||||
- `test_apply_portfolio_tag_owner_own_tag`
|
||||
- `test_apply_portfolio_tag_owner_admin_tag`
|
||||
- `test_apply_portfolio_tag_other_user_non_admin_tag_returns_403`
|
||||
- `test_remove_portfolio_tag_owner_success`
|
||||
- `test_apply_portfolio_tag_nonexistent_tag_returns_404`
|
||||
- `test_apply_portfolio_tag_nonexistent_portfolio_returns_404`
|
||||
- 기존 regression 전체 통과: test_cli.py, test_v1.py, test_asset_catalog.py, test_portfolio_tags.py, test_portfolio_uuid.py, test_daemon_assignment.py, test_tags_master.py
|
||||
- Manual (pre-release 수동 검증 항목 — live DB 연동 시):
|
||||
- POST /v1/tags 실제 DB token으로 소문자 정규화 확인
|
||||
- append_portfolio_tag_id idempotent 처리(tag_id 중복 적용 시 200 반환) 확인
|
||||
- Logs: N/A
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
- Pre-release 수동 검증: live DB 연동 환경에서 auth.user_id 정상 채움 확인, append idempotent 처리 확인
|
||||
@@ -0,0 +1,44 @@
|
||||
# BAT-005 Design
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-005
|
||||
|
||||
## Architecture Summary
|
||||
- REQ-007: `app/cli/commands/tag.py` 신규 파일에 `tag_app = typer.Typer()` 생성 후 `pfhub tag` 7개 커맨드 구현. CLI는 HTTP API를 거치지 않고 기존 repo 메서드를 직접 호출(현재 CLI 아키텍처 패턴 유지). apply/remove는 HTTP 오류를 repo ValueError로 전환해 표시.
|
||||
- `pfhub portfolio tag add/list/remove`는 `portfolio.py`에서 완전 삭제(deprecated 래퍼 없음) — 기존 테스트(test_portfolio_tags.py) 조정 필요.
|
||||
- REQ-008: 4개 baseline 문서를 코드 현황에 맞게 갱신. 모듈/테스트 변경 없음.
|
||||
|
||||
## Changed Areas
|
||||
- Docs:
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids, portfolio_tags DROP 반영
|
||||
- `docs/interface-contract.md` — /v1/tags, /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 추가
|
||||
- `docs/project-structure.md` — tag.py 신규, pfhub tag 커맨드 그룹 추가, pfhub portfolio tag 제거
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름 신규, /v1/tags HTTP 흐름 신규
|
||||
- Modules:
|
||||
- `app/cli/commands/tag.py` — 신규 파일
|
||||
- `app/cli/main.py` — tag_app 등록
|
||||
- `app/cli/commands/portfolio.py` — tag_app, tag_add, tag_list, tag_remove 완전 삭제
|
||||
- `tests/test_portfolio_tags.py` — pfhub portfolio tag CLI 테스트 제거 또는 조정
|
||||
- Interfaces:
|
||||
- 신규: pfhub tag create/list/deactivate/apply/remove 7개 커맨드
|
||||
- 제거: pfhub portfolio tag add/list/remove 3개 커맨드
|
||||
|
||||
## Key Decisions
|
||||
1. **HTTP API 직접 호출 vs repo 메서드**: 기존 CLI 아키텍처는 AppContext를 통해 repo를 주입받아 직접 repo 메서드를 호출. pfhub tag CLI도 동일 패턴 유지 — HTTP 클라이언트 별도 없음. apply/remove는 `repo.append_portfolio_tag_id` / `repo.remove_portfolio_tag_id` 직접 호출, 정책 검증은 repo가 아닌 HTTP API 레이어에서 하는 것이 원칙이나 CLI는 repo 직접 호출이므로 정책 검증 로직을 CLI 레이어에서도 수행.
|
||||
2. **pfhub portfolio tag 완전 삭제**: deprecated 래퍼 없이 완전 제거. REQ-007 AC-9 "명령어가 존재하지 않는다(Typer error)" 충족.
|
||||
3. **apply/remove sub-group**: `pfhub tag apply`와 `pfhub tag remove`는 각각 `asset`/`portfolio` 두 sub-command를 갖는 nested Typer group으로 구현.
|
||||
4. **--force 확인**: `pfhub tag deactivate`, `pfhub tag remove` 두 커맨드에서 `--force` 플래그가 없으면 실행 거부(기존 CLI 패턴 일관성).
|
||||
5. **test_portfolio_tags.py 조정**: 기존 `pfhub portfolio tag add/list/remove` 테스트를 제거. 이 파일의 나머지 테스트(존재하면)는 유지.
|
||||
|
||||
## Edge Cases
|
||||
- `pfhub tag create <name>` 중복 이름 → repo ValueError → CLI 오류 메시지 출력 후 non-zero exit
|
||||
- `pfhub tag deactivate <tag_id>` --force 없이 실행 → "확인이 필요합니다" 오류
|
||||
- `pfhub tag apply asset/portfolio` 정책 위반 → repo/HTTP 403 오류 → CLI 오류 메시지 출력
|
||||
- `pfhub portfolio tag add/list/remove` 실행 → Typer "No such command" 오류 (제거 후)
|
||||
|
||||
## Architecture Impact
|
||||
- both (project-structure: tag.py 신규, runtime-flows: pfhub tag 흐름 신규)
|
||||
|
||||
## Reference Doc Update Plan
|
||||
- project-structure.md: REQ-008에서 직접 갱신 (이 배치 내부에서 처리)
|
||||
- runtime-flows.md: REQ-008에서 직접 갱신 (이 배치 내부에서 처리)
|
||||
@@ -0,0 +1,37 @@
|
||||
# BAT-005 Implementation
|
||||
|
||||
- Status: completed
|
||||
- Batch: BAT-005
|
||||
|
||||
## Plan Summary
|
||||
- REQ-007: tag.py 신규 CLI 커맨드 그룹 + main.py 등록 + portfolio.py tag 커맨드 제거
|
||||
- REQ-008: baseline 문서 4개 갱신 (data-model, interface-contract, project-structure, runtime-flows)
|
||||
|
||||
## Changed Files
|
||||
- `app/cli/commands/tag.py` — 신규: tag_app (create/list/deactivate), apply_app (asset/portfolio), remove_app (asset/portfolio) 7개 커맨드
|
||||
- `app/cli/main.py` — tag_app import 및 `app.add_typer(tag_app, name="tag")` 추가
|
||||
- `app/cli/commands/portfolio.py` — tag_app Typer, tag_add, tag_list, tag_remove 완전 삭제; PortfolioTagMutationRequest import 제거
|
||||
- `app/cli/core/utils.py` — print_tag_table() 추가; render_output()에 list[Tag] 분기 추가
|
||||
- `tests/test_cli.py` — test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거
|
||||
- `tests/test_tags_cli.py` — 신규: FakeTagRepo 기반 24개 테스트 (REQ-007 AC-1~9 커버)
|
||||
- `docs/data-model.md` — REQ-008 AC-1: Entity Tag 신규, portfolios.tag_ids, portfolio_tags 제거 반영
|
||||
- `docs/interface-contract.md` — REQ-008 AC-2: Tag Management API 섹션 신규 (7개 엔드포인트)
|
||||
- `docs/project-structure.md` — REQ-008 AC-3: tag.py/v1_tags.py/v1_portfolios.py/tag schemas 반영
|
||||
- `docs/runtime-flows.md` — REQ-008 AC-4: pfhub tag CLI 흐름, /v1/tags, /v1/assets|portfolios/{id}/tags 흐름 신규
|
||||
|
||||
## Execution Log
|
||||
1. planning.md, design.md status → confirmed; index.md, docs/batches/index.md → in-delivery
|
||||
2. app/cli/commands/tag.py 신규 생성 (태그 7개 커맨드, _with_repo 패턴)
|
||||
3. app/cli/main.py에 tag_app 등록
|
||||
4. app/cli/commands/portfolio.py에서 tag_app 관련 코드 완전 삭제
|
||||
5. app/cli/core/utils.py에 print_tag_table + list[Tag] render 분기 추가
|
||||
6. tests/test_cli.py에서 test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거
|
||||
7. tests/test_tags_cli.py 신규 생성 (FakeTagRepo, 24개 테스트)
|
||||
8. pytest 247 passed 확인
|
||||
9. docs/data-model.md, interface-contract.md, project-structure.md, runtime-flows.md 갱신
|
||||
|
||||
## Validation
|
||||
- pytest 247 passed (8.47s) — regression 없음, 신규 24개 테스트 전체 통과
|
||||
|
||||
## Remaining Risks
|
||||
- 없음 — REQ-007, REQ-008 구현 및 검증 완료
|
||||
@@ -0,0 +1,35 @@
|
||||
# BAT-005: pfhub tag CLI 및 baseline docs 갱신
|
||||
|
||||
- Status: release-candidate
|
||||
- Profile: standard
|
||||
- Source Discovery: dcy-002
|
||||
- Included REQ: REQ-007, REQ-008
|
||||
- Excluded REQ: (없음 — dcy-002 잔여 2개 전부 포함)
|
||||
|
||||
## Summary
|
||||
- tags 마스터 기반의 `pfhub tag` 최상위 CLI 커맨드 그룹을 신규 추가하고(REQ-007), 기존 `pfhub portfolio tag add/list/remove` 커맨드를 제거한다. 이후 dcy-002 전체 구현 완료를 반영해 baseline 문서 4개를 갱신한다(REQ-008).
|
||||
- 이 배치가 완료되면 dcy-002(태그 소유권 및 적용 대상 정책) 관련 REQ-001~008 전체 구현이 종료된다.
|
||||
|
||||
## Scope
|
||||
- In Scope:
|
||||
- `app/cli/commands/tag.py` 신규 파일 — `pfhub tag create/list/deactivate/apply/remove` 커맨드
|
||||
- `app/cli/main.py` — `tag_app` 등록 추가 (REQ-007)
|
||||
- `app/cli/commands/portfolio.py` — `tag_app`, `tag_add`, `tag_list`, `tag_remove` 제거 (REQ-007)
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids, portfolio_tags 제거 반영 (REQ-008)
|
||||
- `docs/interface-contract.md` — /v1/tags, /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 추가 (REQ-008)
|
||||
- `docs/project-structure.md` — pfhub tag 커맨드 그룹 추가, pfhub portfolio tag 제거 반영 (REQ-008)
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름, /v1/tags HTTP 흐름 추가 (REQ-008)
|
||||
- Out of Scope:
|
||||
- HTTP API 변경 (→ BAT-004 완료)
|
||||
- DB 스키마 변경 (→ BAT-003 완료)
|
||||
|
||||
## Documents
|
||||
- Planning: [planning.md](./planning.md)
|
||||
- Design: [design.md](./design.md)
|
||||
- Implementation: [implementation.md](./implementation.md)
|
||||
- Verification: [verification.md](./verification.md)
|
||||
|
||||
## Notes
|
||||
- R-1 (Medium): `pfhub portfolio tag add/list/remove` 제거는 기존 운영 스크립트에 breaking change — 운영자 사전 고지 필요
|
||||
- REQ-007 선행 의존: REQ-004(tags HTTP API), REQ-005(태그 적용/해제 HTTP API) 모두 BAT-004(release-candidate) 완료 상태
|
||||
- REQ-008은 REQ-001~007 전체 구현 완료 후 일괄 반영 → 이 배치가 마지막 실행 단계
|
||||
@@ -0,0 +1,50 @@
|
||||
# BAT-005 Planning
|
||||
|
||||
- Status: confirmed
|
||||
- Batch: BAT-005
|
||||
- Profile: standard
|
||||
|
||||
## Included REQ
|
||||
- REQ-007: pfhub tag CLI 명령어 (pfhub tag create/list/deactivate/apply/remove + pfhub portfolio tag 제거)
|
||||
- REQ-008: 태그 모델 도입 baseline 문서 갱신 (data-model, interface-contract, project-structure, runtime-flows)
|
||||
|
||||
## Delivery Plan
|
||||
1. **REQ-007 — pfhub tag CLI 신규 구현**
|
||||
- `app/cli/commands/tag.py` 신규 파일 생성
|
||||
- `pfhub tag create <name>` — POST /v1/tags 호출 (trade-write)
|
||||
- `pfhub tag list [--inactive]` — GET /v1/tags 호출 (is_active 필터)
|
||||
- `pfhub tag deactivate <tag_id> --force` — PATCH /v1/tags/{tag_id}/deactivate 호출
|
||||
- `pfhub tag apply asset <asset_id> <tag_id>` — POST /v1/assets/{asset_id}/tags 호출 (admin)
|
||||
- `pfhub tag apply portfolio <portfolio_id> <tag_id>` — POST /v1/portfolios/{portfolio_id}/tags 호출
|
||||
- `pfhub tag remove asset <asset_id> <tag_id> --force` — DELETE /v1/assets/{asset_id}/tags/{tag_id} 호출
|
||||
- `pfhub tag remove portfolio <portfolio_id> <tag_id> --force` — DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id} 호출
|
||||
- `app/cli/main.py` — tag_app 등록 (`app.add_typer(tag_app, name="tag")`)
|
||||
- `app/cli/commands/portfolio.py` — tag_app Typer, tag_add, tag_list, tag_remove 제거
|
||||
2. **REQ-008 — baseline 문서 갱신**
|
||||
- `docs/data-model.md` — tags 테이블 스키마, portfolios.tag_ids UUID[], portfolio_tags 제거 반영
|
||||
- `docs/interface-contract.md` — /v1/tags (POST/GET/PATCH), /v1/assets/{id}/tags (POST/DELETE), /v1/portfolios/{id}/tags (POST/DELETE) 명세 추가
|
||||
- `docs/project-structure.md` — app/cli/commands/tag.py 추가, pfhub tag 커맨드 그룹 추가, portfolio tag 제거 반영
|
||||
- `docs/runtime-flows.md` — pfhub tag CLI 흐름, /v1/tags HTTP 흐름 추가
|
||||
3. **전체 pytest 실행 — regression 확인**
|
||||
|
||||
## Dependencies
|
||||
- Internal:
|
||||
- BAT-003(release-candidate): tags 테이블, portfolios.tag_ids, Tag 도메인 모델 완료
|
||||
- BAT-004(release-candidate): POST/GET/PATCH /v1/tags, POST/DELETE /v1/assets|portfolios/{id}/tags HTTP API 완료
|
||||
- 기존 CLI 패턴(AppContext 통해 settings/repository 주입, --force 확인) 준수
|
||||
- External:
|
||||
- 없음
|
||||
|
||||
## Design Gate
|
||||
- Design Required Before Implementation: yes
|
||||
- Reason: 기존 `pfhub portfolio tag add/list/remove` 커맨드 제거 방식(완전 삭제 vs deprecated 표시), tag.py 커맨드 구조(apply/remove sub-group 설계), HTTP API 호출 방식(직접 repo 메서드 vs HTTP client) 결정 필요.
|
||||
|
||||
## Milestones
|
||||
1. REQ-007 CLI 구현 완료 + pfhub portfolio tag 제거 + tests
|
||||
2. REQ-008 baseline 문서 4개 갱신 완료
|
||||
3. 전체 pytest suite 통과 (regression 없음)
|
||||
|
||||
## Risks
|
||||
- R-1 (Medium): `pfhub portfolio tag add/list/remove` 제거는 운영 환경 breaking change — 기존 스크립트/자동화 사용 여부 사전 확인 권고
|
||||
- R-2 (Low): pfhub tag CLI는 HTTP API를 직접 호출하지 않고 repo 메서드를 통해 구현 — repo 메서드가 이미 BAT-003/004에서 완성되어 있으므로 위험 낮음
|
||||
- R-3 (Low): REQ-007 AC-9(pfhub portfolio tag 제거 확인) 테스트 패턴 — CLI 테스트 파일에서 명령어 부재 확인 방식 설계 필요
|
||||
@@ -0,0 +1,40 @@
|
||||
# BAT-005 Verification
|
||||
|
||||
- Status: release-candidate
|
||||
- Batch: BAT-005
|
||||
|
||||
## Acceptance Mapping
|
||||
|
||||
- REQ-007:
|
||||
- AC-1: `pfhub tag create "코스피200"` → 태그 생성 성공 → `test_tag_create_success`, `test_tag_create_with_user`
|
||||
- AC-2: `pfhub tag list` → 활성 태그 목록만 출력 → `test_tag_list_shows_active_tags`
|
||||
- AC-3: `pfhub tag list --inactive` → 비활성 포함 전체 목록 출력 → `test_tag_list_inactive_shows_all`
|
||||
- AC-4: `pfhub tag deactivate <tag_id> --force` → 비활성화; 재실행 멱등 → `test_tag_deactivate_with_force`, `test_tag_deactivate_already_inactive_is_idempotent`, `test_tag_deactivate_without_force_aborts`, `test_tag_deactivate_nonexistent_fails`
|
||||
- AC-5: `pfhub tag apply asset <asset_id> <tag_id>` → admin+active 태그만 허용 → `test_tag_apply_asset_success`, `test_tag_apply_asset_non_admin_tag_fails`, `test_tag_apply_asset_inactive_tag_fails`
|
||||
- AC-6: `pfhub tag apply portfolio <portfolio_id> <tag_id>` → admin 태그 또는 소유자 태그만 허용 → `test_tag_apply_portfolio_admin_tag_success`, `test_tag_apply_portfolio_owner_tag_success`, `test_tag_apply_portfolio_policy_violation_fails`
|
||||
- AC-7: `pfhub tag remove asset <asset_id> <tag_id> --force` → 태그 제거 → `test_tag_remove_asset_with_force`, `test_tag_remove_asset_without_force_aborts`
|
||||
- AC-8: `pfhub tag remove portfolio <portfolio_id> <tag_id> --force` → 태그 제거 → `test_tag_remove_portfolio_with_force`
|
||||
- AC-9: `pfhub portfolio tag add/list/remove` → 명령어 없음(Typer error) → `test_portfolio_tag_subcommand_does_not_exist`, `test_portfolio_tag_list_does_not_exist`, `test_portfolio_tag_remove_does_not_exist`
|
||||
|
||||
- REQ-008:
|
||||
- AC-1: docs/data-model.md에 tags 테이블 스키마, portfolios.tag_ids UUID[], portfolio_tags 제거 반영 → 파일 내용 검증 완료
|
||||
- AC-2: docs/interface-contract.md에 /v1/tags (POST/GET/PATCH), /v1/assets/{id}/tags, /v1/portfolios/{id}/tags 명세 포함 → 파일 내용 검증 완료
|
||||
- AC-3: docs/project-structure.md에 pfhub tag 커맨드 그룹, app/cli/commands/tag.py 반영 → 파일 내용 검증 완료
|
||||
- AC-4: docs/runtime-flows.md에 pfhub tag CLI 흐름, /v1/tags HTTP 흐름, /v1/assets|portfolios/{id}/tags 흐름 포함 → 파일 내용 검증 완료
|
||||
|
||||
## Evidence
|
||||
- Test:
|
||||
- pytest 실행 결과: 247 passed (tests/test_tags_cli.py 24개 신규 포함, tests/test_cli.py 기존 32개 유지)
|
||||
- 신규 테스트 파일: tests/test_tags_cli.py (REQ-007 AC-1~9 커버, FakeTagRepo 기반)
|
||||
- regression: test_cli_tag_add_and_list, test_cli_tag_remove_flow 제거 (pfhub portfolio tag 커맨드 제거와 함께)
|
||||
- Manual:
|
||||
- docs/data-model.md: Entity Tag 섹션 신규, portfolios.tag_ids UUID[] 추가, portfolio_tags 제거 반영
|
||||
- docs/interface-contract.md: Tag Management API 섹션 신규 (7개 엔드포인트 명세)
|
||||
- docs/project-structure.md: app/cli/commands/tag.py, app/api/v1_tags.py/v1_portfolios.py, v1.py tag schemas 반영
|
||||
- docs/runtime-flows.md: pfhub tag CLI 흐름, v1 tag management 흐름, v1 tag apply/remove 흐름 신규
|
||||
- Logs:
|
||||
- `.venv/bin/python -m pytest -q` → 247 passed in 8.47s
|
||||
|
||||
## Result
|
||||
- Ready for Release: true
|
||||
- Blocking Issues: 없음
|
||||
@@ -11,3 +11,5 @@
|
||||
| BAT-001 | standard | verified | REQ-009, REQ-011, REQ-014 | dcy-001 | [bat-001](./bat-001_20260519_asset-catalog-db-foundation/) |
|
||||
| BAT-002 | standard | release-candidate | REQ-010, REQ-012, REQ-013, REQ-015 | dcy-001 | [bat-002](./bat-002_20260519_asset-catalog-api-completion/) |
|
||||
| BAT-003 | standard | release-candidate | REQ-001, REQ-002, REQ-003, REQ-006 | dcy-002 | [bat-003](./bat-003_20260519_tags-db-foundation/) |
|
||||
| BAT-004 | standard | release-candidate | REQ-004, REQ-005 | dcy-002 | [bat-004](./bat-004_20260520_tags-http-api/) |
|
||||
| BAT-005 | standard | release-candidate | REQ-007, REQ-008 | dcy-002 | [bat-005](./bat-005_20260520_pfhub-tag-cli-and-docs/) |
|
||||
|
||||
Reference in New Issue
Block a user