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:
2026-05-20 01:16:13 +00:00
co-authored by Claude Sonnet 4.6
parent 158929b2d9
commit d4b841bc37
29 changed files with 2216 additions and 89 deletions
+24 -1
View File
@@ -13,7 +13,7 @@ from rich.panel import Panel
from rich.table import Table
from app.core.errors import FeatureNotReadyError
from app.domain.models import Daemon
from app.domain.models import Daemon, Tag
from app.schemas.auth import AuthTokenResponse
from app.schemas.portfolio import CommentResponse, PortfolioDetailResponse, PortfolioListItem
from app.schemas.user import UserResponse
@@ -231,6 +231,26 @@ def print_comment_table(console: Console, comments: list[CommentResponse]) -> No
console.print(table)
def print_tag_table(console: Console, tags: list[Tag]) -> None:
table = Table(title="Tags", expand=True)
table.add_column("ID", style="cyan", no_wrap=True)
table.add_column("Name", no_wrap=True)
table.add_column("Admin", no_wrap=True)
table.add_column("Active", no_wrap=True)
table.add_column("Created By", no_wrap=True)
table.add_column("Created At", no_wrap=True)
for tag in tags:
table.add_row(
tag.tag_id,
tag.name,
"yes" if tag.is_admin_tag else "no",
"yes" if tag.is_active else "no",
tag.created_by_user_id or "-",
tag.created_at.isoformat(timespec="seconds") if tag.created_at else "N/A",
)
console.print(table)
def print_comment_detail(console: Console, comment: CommentResponse) -> None:
console.print(
Panel.fit(
@@ -276,6 +296,9 @@ def render_output(console: Console, output_format: str, payload: Any, *, empty_m
if all(isinstance(item, AuthTokenResponse) for item in payload):
print_service_token_table(console, payload)
return
if all(isinstance(item, Tag) for item in payload):
print_tag_table(console, payload)
return
return
if isinstance(payload, PortfolioDetailResponse):