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
+2 -2
View File
@@ -172,7 +172,7 @@ def apply_asset_tag(
if not tag.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="비활성화된 태그는 적용할 수 없습니다")
repo.append_portfolio_tag_id(asset_id, body.tag_id)
repo.append_asset_tag_id(asset_id, body.tag_id)
except HTTPException:
raise
except ValueError as exc:
@@ -197,7 +197,7 @@ def remove_asset_tag(
repo.get(asset_id)
except ValueError:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"asset을 찾을 수 없습니다: {asset_id}")
repo.remove_portfolio_tag_id(asset_id, tag_id)
repo.remove_asset_tag_id(asset_id, tag_id)
except HTTPException:
raise
except ValueError as exc:
+30
View File
@@ -1766,6 +1766,36 @@ class PostgresPortfolioRepository:
if cur.rowcount == 0:
raise ValueError(f"portfolio를 찾을 수 없습니다: {portfolio_id}")
def append_asset_tag_id(self, asset_id: str, tag_id: str) -> None:
self.ensure_schema()
with self._connect().cursor() as cur:
cur.execute(
"""
UPDATE assets
SET tag_ids = array_append(tag_ids, %s::uuid)
WHERE asset_id = %s AND NOT (%s::uuid = ANY(tag_ids))
""",
(tag_id, asset_id, tag_id),
)
if cur.rowcount == 0:
cur.execute("SELECT asset_id FROM assets WHERE asset_id = %s", (asset_id,))
if cur.fetchone() is None:
raise ValueError(f"asset을 찾을 수 없습니다: {asset_id}")
def remove_asset_tag_id(self, asset_id: str, tag_id: str) -> None:
self.ensure_schema()
with self._connect().cursor() as cur:
cur.execute(
"""
UPDATE assets
SET tag_ids = array_remove(tag_ids, %s::uuid)
WHERE asset_id = %s
""",
(tag_id, asset_id),
)
if cur.rowcount == 0:
raise ValueError(f"asset을 찾을 수 없습니다: {asset_id}")
def rotate_service_token(self, name: str) -> tuple[str, AuthToken]:
from app.core.security import generate_token, hash_token, token_prefix as _token_prefix