feat: REQ-002/REQ-010 implementation — assets.tag_ids column + GET /v1/assets admin catalog (BAT-012)
- REQ-002 scope redefinition: portfolios.tag_ids → assets.tag_ids UUID[] column with GIN index
- REQ-010 augmentation: GET /v1/assets redesigned as admin catalog endpoint (admin-only, no token_type branching)
- Asset tagging infrastructure: tag_ids column, archived flag synthesis for inactive tags, list_assets N+1 optimization
- DB migration 20260521_0023: assets.tag_ids UUID[] NOT NULL DEFAULT '{}', idx_assets_tag_ids_gin
- Test suite: 17 new test cases for assets.tag_ids + GET /v1/assets AC-9~12 (362 tests total)
- Documentation: runtime flows, release baseline, batch verification all updated
- BAT-012 added to REL-001 Included Batch and marked release-candidate
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+46
-7
@@ -15,6 +15,7 @@ from app.schemas.v1 import (
|
||||
AssetCatalogUpdateRequest,
|
||||
TagApplyRequest,
|
||||
TagApplyResponse,
|
||||
TagRefItem,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,13 +27,39 @@ def _get_repo(settings=None):
|
||||
return get_portfolio_repository_from_settings(settings or get_settings())
|
||||
|
||||
|
||||
def _asset_response(asset) -> AssetCatalogResponse:
|
||||
def _build_tag_refs(tag_ids: list[str], tag_active_map: dict[str, bool]) -> list[TagRefItem]:
|
||||
"""Convert raw tag_id list into TagRefItem array with archived flag synthesized.
|
||||
|
||||
tag_ids not present in tag_active_map are surfaced as archived=True so the array
|
||||
is never silently pruned (REQ-002 Intent: "조용히 제거하지 않고 archived 상태로 반환").
|
||||
"""
|
||||
refs: list[TagRefItem] = []
|
||||
for tid in tag_ids:
|
||||
active = tag_active_map.get(tid, False)
|
||||
refs.append(TagRefItem(tag_id=tid, archived=not active))
|
||||
return refs
|
||||
|
||||
|
||||
def _asset_response(asset, tag_active_map: dict[str, bool] | None = None) -> AssetCatalogResponse:
|
||||
tag_ids = getattr(asset, "tag_ids", []) or []
|
||||
if tag_active_map is None and tag_ids:
|
||||
# 단일 응답 경로: 필요한 tag_ids만 1회 조회
|
||||
repo = _get_repo()
|
||||
try:
|
||||
tags = repo.get_tags_by_ids(tag_ids)
|
||||
finally:
|
||||
if callable(getattr(type(repo), "close", None)):
|
||||
repo.close()
|
||||
tag_active_map = {tid: t.is_active for tid, t in tags.items()}
|
||||
elif tag_active_map is None:
|
||||
tag_active_map = {}
|
||||
return AssetCatalogResponse(
|
||||
asset_id=asset.asset_id,
|
||||
symbol=asset.symbol,
|
||||
markets=asset.markets,
|
||||
display_name=asset.display_name,
|
||||
status=asset.status,
|
||||
tag_ids=_build_tag_refs(tag_ids, tag_active_map),
|
||||
created_at=asset.created_at,
|
||||
updated_at=asset.updated_at,
|
||||
)
|
||||
@@ -60,16 +87,28 @@ def list_catalog_assets(
|
||||
repo = _get_repo()
|
||||
try:
|
||||
assets = repo.list_assets()
|
||||
|
||||
if status_filter != "all":
|
||||
assets = [a for a in assets if a.status == status_filter]
|
||||
if market is not None:
|
||||
assets = [a for a in assets if market in (a.markets or [])]
|
||||
|
||||
all_tag_ids: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for a in assets:
|
||||
for tid in (getattr(a, "tag_ids", []) or []):
|
||||
if tid not in seen:
|
||||
seen.add(tid)
|
||||
all_tag_ids.append(tid)
|
||||
tag_active_map: dict[str, bool] = {}
|
||||
if all_tag_ids:
|
||||
tags = repo.get_tags_by_ids(all_tag_ids)
|
||||
tag_active_map = {tid: t.is_active for tid, t in tags.items()}
|
||||
finally:
|
||||
if callable(getattr(type(repo), "close", None)):
|
||||
repo.close()
|
||||
|
||||
if status_filter != "all":
|
||||
assets = [a for a in assets if a.status == status_filter]
|
||||
if market is not None:
|
||||
assets = [a for a in assets if market in (a.markets or [])]
|
||||
|
||||
items = [_asset_response(a) for a in assets]
|
||||
items = [_asset_response(a, tag_active_map) for a in assets]
|
||||
return AssetCatalogListResponse(items=items, total=len(items))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user