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:
2026-05-21 23:37:08 +09:00
co-authored by Claude Opus 4.7
parent d01039e98f
commit 9038f41010
19 changed files with 867 additions and 47 deletions
@@ -0,0 +1,48 @@
# BAT-012 Design
- Status: confirmed
- Batch: BAT-012
## Architecture Summary
- assets 테이블에 단일 컬럼(`tag_ids UUID[]`)·GIN 인덱스 추가, `AssetCatalogResponse`에 `tag_ids: list[TagRefItem]` 필드 추가, GET /v1/assets 응답에 archived 합성 로직 삽입을 함께 처리한다.
- archived 표시 포맷은 기존 `app/schemas/v1.py:50-52`의 `TagRefItem(tag_id, archived)` 객체 배열 패턴을 그대로 재사용한다(REQ-003 portfolios.tag_ids 응답과 동일).
- GET /v1/assets 라우트 본체는 이미 admin catalog 조회로 재정의되어 있음(`app/api/v1_assets.py:48-73`, REQ-010 AC-1~8 + AC-9~12의 코드 경로 존재). 본 batch는 (a) 응답에 `tag_ids` 필드 합류, (b) AC-9~12 자동 테스트 신설, (c) tag_ids 컬럼 도입의 backing migration·repository 정합을 책임진다.
## Changed Areas
- Docs:
- `docs/data-model.md` — assets 엔티티 스키마에 `tag_ids UUID[]` 추가 (별도 docs sync REQ로 분리 가능)
- `docs/interface-contract.md` — GET /v1/assets 응답 본문에 `tag_ids` 필드 명세 추가
- Modules:
- `migrations/versions/20260521_NNNN_assets_tag_ids_column.py` (신규) — `tag_ids UUID[] NOT NULL DEFAULT '{}'` + `idx_assets_tag_ids_gin`
- `app/domain/models.py:32-39` — `Asset` dataclass에 `tag_ids: list[str]` 필드 추가 (기본값 빈 리스트)
- `app/repositories/postgres.py:82-92` — `_SELECT_ASSET` SQL에 `COALESCE(tag_ids, '{}'::uuid[])` 추가
- `app/repositories/postgres.py:454-466` — `_row_to_asset`에서 tag_ids 파싱
- `app/schemas/v1.py:234-241` — `AssetCatalogResponse`에 `tag_ids: list[TagRefItem]` 필드 추가 (기본값 빈 리스트)
- `app/api/v1_assets.py:29-38` — `_asset_response` 헬퍼에 tags 마스터 is_active 조회 + archived 합성 로직 추가
- Interfaces:
- HTTP: `GET /v1/assets` / `POST /v1/assets` / `PATCH /v1/assets/{asset_id}` / `PATCH /v1/assets/{asset_id}/deactivate` 응답 본문에 `tag_ids: [{tag_id, archived}]` 필드 등장
## Key Decisions
1. **archived 표시 포맷 — `TagRefItem` 객체 배열 채택**: 기존 `app/schemas/v1.py:50-52`의 `class TagRefItem { tag_id: str; archived: bool = False }`를 그대로 재사용. REQ-003 portfolios에서 채택한 형식과 정합. `AssetCatalogResponse.tag_ids: list[TagRefItem] = Field(default_factory=list)`.
2. **list_assets 시그니처 — 변경 없음**: 기존 `list_assets() -> list[Asset]`을 그대로 유지. status/market 필터는 라우트 레이어에서 이미 in-memory 필터링(`app/api/v1_assets.py:67-70`)으로 동작 중. 운영 카탈로그 규모(수십~수백 row) 기준 in-memory 필터로 충분. 향후 카탈로그 폭증 시 repository 보강은 별도 REQ로.
3. **GET /v1/assets 라우트 본체 — 변경 없음**: 라우트는 이미 admin catalog로 재정의되어 있음. 본 batch는 응답 schema에 `tag_ids` 필드 추가와 archived 합성만 책임. REQ-010 AC-9~12 코드 경로는 존재하지만 **자동 테스트가 없음** — 본 batch에서 신규 테스트로 메움.
4. **BAT-002 REQ-010 trace 정리 — BAT-012 우선 적용**: BAT-012 verification 통과 후 confirm-req-implemented 단계에서 REQ-010 trace를 BAT-012로 갱신하고 BAT-002 trace 표기를 `superseded by BAT-012`로 명시. BAT-002의 REQ-012/REQ-015 trace는 영향 없음.
5. **archived 합성 위치 — API 레이어 헬퍼**: `_asset_response`에서 `tag_ids: list[str]`을 `list[TagRefItem]`으로 변환. tags 마스터 is_active 조회는 `repo.get_tags_by_ids(tag_ids)` 활용(이미 존재: `app/repositories/postgres.py:1700-1705`). 단일 자산 응답 N개 호출 회피를 위해 `list_catalog_assets`에서는 모든 자산의 tag_ids union을 한 번에 조회 후 매핑(N+1 회피).
6. **`POST /v1/assets/{id}/tags` 라우트 정합화 — 본 batch 범위 밖**: 현행 apply/remove 라우트가 `repo.append_portfolio_tag_id`를 호출하여 portfolios 테이블을 잘못 대상화하는 잠재 버그(REQ-005 follow-up)는 BAT-012 범위 밖. REQ-002 AC-3/AC-4 검증은 DB seed로 직접 수행. apply/remove 정합화는 별도 REQ/Batch에서 처리.
7. **Migration revision down_revision — 신규 head**: alembic heads 출력을 implementation 진입 시 확인하고 가장 최신 revision을 down_revision으로 지정. 동시에 진행 중인 다른 batch 없음(BAT-009~011 모두 release-candidate).
## Edge Cases
- `tag_ids = []`: 응답 `tag_ids: []` (빈 배열, AC-4).
- `tag_ids`에 마스터에 없는 UUID 포함: `get_tags_by_ids` 결과에 없으면 archived 합성 시 `archived=True`로 처리 또는 무시. **결정**: archived=True로 노출(이력 유지 원칙 — REQ-002 Intent "조용히 제거하지 않고 archived 상태로 반환").
- `tag_ids`에 is_active=FALSE tag_id 포함: archived=True 응답 (AC-3).
- `status=all` + `market=KRX` 동시 지정: 두 필터 AND. 빈 결과면 `{ items: [], total: 0 }` 200.
- `market` 파라미터에 MarketCode Enum 외 값: schema validation으로 422 — 기존 라우트는 임의 문자열을 받고 비교만 하므로 본 batch에서 명시적 validation 추가 여부 결정 필요. **결정**: 본 batch는 기존 동작 유지(임의 문자열 허용, 매칭 없으면 빈 결과). MarketCode 검증은 별도 REQ로.
- assets 테이블 비어 있음: `{ items: [], total: 0 }` 200.
- non-admin 토큰(user/service/client): `require_admin` 의존성이 403 처리(AC-12).
## Architecture Impact
- **runtime-flows** — GET /v1/assets 응답에 tag_ids/archived 합성 흐름이 추가됨. 한 줄 갱신 필요.
## Reference Doc Update Plan
- project-structure.md: none
- runtime-flows.md: GET /v1/assets 흐름 설명에 "tag_ids 배열을 응답으로 노출하고 tags 마스터 is_active를 조회해 archived 합성" 1줄 추가 — 본 batch implementation에서 함께 처리.
@@ -0,0 +1,47 @@
# BAT-012 Implementation
- Status: completed
- Batch: BAT-012
## Plan Summary
- planning.md Delivery Plan에 따라 (1) Alembic revision → (2) 도메인 모델 → (3) repository → (4) schemas → (5) GET 라우트 헬퍼 보강 → (6) 테스트 → (7) docs 갱신 순서로 구현 완료.
- GET /v1/assets 라우트 본체는 사전 augmentation으로 이미 admin catalog 조회로 재정의되어 있었음. 본 batch는 (a) `tag_ids` 응답 필드 합류, (b) archived 합성 N+1 회피 로직, (c) 부재했던 AC-9~12 자동 테스트 신설을 책임.
## Changed Files
- `migrations/versions/20260521_0023_assets_tag_ids_column.py` (신규) — assets.tag_ids UUID[] + idx_assets_tag_ids_gin
- `app/domain/models.py` — `Asset` dataclass에 `tag_ids: list[str] = field(default_factory=list)` 필드 추가
- `app/repositories/postgres.py`
- `_SELECT_ASSET`에 `COALESCE(tag_ids, '{}'::uuid[])` 컬럼 추가
- `_row_to_asset`에서 `tag_ids=[str(t) for t in row[7]] if len(row) > 7 ...` 파싱
- `insert_asset` RETURNING 절에 `COALESCE(tag_ids, '{}'::uuid[])` 추가
- `app/schemas/v1.py` — `AssetCatalogResponse`에 `tag_ids: list[TagRefItem] = Field(default_factory=list)`
- `app/api/v1_assets.py`
- `TagRefItem` import
- `_build_tag_refs(tag_ids, tag_active_map)` 신규 헬퍼 (archived 합성: 마스터에 없거나 is_active=False면 archived=True)
- `_asset_response(asset, tag_active_map=None)` 시그니처 보강 (단일 응답 경로에서는 tag_ids 비어있지 않을 때만 DB 조회)
- `list_catalog_assets`에서 모든 자산의 tag_ids union을 한 번에 `get_tags_by_ids`로 조회 후 매핑 (N+1 회피)
- `tests/test_asset_catalog.py` — REQ-002 AC-1~4 + REQ-010 AC-9~12 신규 17건 추가
- `docs/runtime-flows.md` — GET /v1/assets 흐름 재정의 + Change Log entry
## Execution Log
1. alembic heads 확인 → 최신 revision `20260520_0022` 파악, 새 revision `20260521_0023`으로 down_revision 지정
2. 마이그레이션 파일 작성 (REQ-002 AC-1, AC-2): assets.tag_ids 컬럼 + idx_assets_tag_ids_gin
3. `Asset` dataclass에 tag_ids 필드 추가 (`dataclasses.field` import 보강)
4. `_SELECT_ASSET` SQL + `_row_to_asset` + `insert_asset` RETURNING 모두 tag_ids 합류
5. `AssetCatalogResponse`에 tag_ids 필드 추가 (default_factory=list)
6. `_build_tag_refs` 헬퍼 신설 + `_asset_response`에 archived 합성 로직 + `list_catalog_assets`에서 batch 조회로 N+1 회피
7. 17건 테스트 추가 (migration file structure 2건, Asset/schema 4건, `_build_tag_refs` 단위 3건, GET integration 8건)
8. `docs/runtime-flows.md` GET /v1/assets 흐름 재작성 + Change Log entry 추가
9. `python -m pytest tests/test_asset_catalog.py -x` → 50/50 PASSED
10. `python -m pytest` 전체 회귀 → 362/362 PASSED (기존 330 + 신규 32)
## Validation
- `python -m pytest tests/test_asset_catalog.py` — **50 passed in 1.51s** (기존 33 + 신규 17)
- `python -m pytest` 전체 suite — **362 passed in 13.06s** (기존 330 + 신규 32, 회귀 0)
- Migration 파일 구조 검증은 in-test로 충분 (assert add_column / index / nullable=False / `'{}'::uuid[]` / postgresql_using=gin)
- DB-integration AC(실제 컬럼/인덱스 존재, archived row 동작)은 REL-001 후속 release 배포 시 alembic upgrade 결과로 최종 확인. CHG-20260521-01 BAT-001 패턴과 동일.
## Remaining Risks
- R-1 잔존 (Low): `POST /v1/assets/{asset_id}/tags` / `DELETE /v1/assets/{asset_id}/tags/{tag_id}` 라우트가 여전히 `append_portfolio_tag_id`/`remove_portfolio_tag_id`를 호출하여 portfolios 테이블을 잘못 대상화 (REQ-005 follow-up). 본 batch는 schema/응답 합류와 GET 회귀 봉합만 책임. apply/remove 라우트 정합화는 별도 REQ/Batch로 처리. 본 batch verification에서는 DB seed 기반 GET 검증으로 AC-3/AC-4 충족.
- R-2 잔존 (Low): BAT-002의 REQ-010 trace는 confirm-batch-verification 단계에서 BAT-012 우선 적용으로 정리 예정 (index.md Notes에 명시).
- R-3 잔존 (Negligible): assets.tag_ids 컬럼·GIN 인덱스 추가 시 lock — assets 테이블 규모가 작아 운영 영향 미미.
@@ -0,0 +1,37 @@
# BAT-012: assets tag_ids 컬럼 + admin GET /v1/assets 재정의
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-002 (REQ-002), dcy-001 + dcy-003 augmentation (REQ-010)
- Included REQ: REQ-002, REQ-010
- Excluded REQ: none
## Summary
- REQ-002 본문 재정의(CHG-20260521-02: portfolios.tag_ids → assets.tag_ids) 후속으로 `assets.tag_ids UUID[]` 컬럼 + `idx_assets_tag_ids_gin` GIN 인덱스를 도입한다.
- REQ-010 augmentation(CHG-20260521-02)에서 미구현으로 남은 `GET /v1/assets` admin catalog 재정의(AC-9~12)를 동시에 구현한다.
- 두 REQ가 `app/schemas/v1.py`(AssetCatalogItem 신설 + tag_ids 필드 합류), `app/repositories/postgres.py`(list_assets 필터 보강 + archived 표시), `tests/test_asset_catalog.py` 변경점을 공유하므로 한 batch에서 sequence를 잡아 처리한다.
## Scope
- In Scope:
- `migrations/versions/` — `assets.tag_ids UUID[] NOT NULL DEFAULT '{}'` 컬럼 + `idx_assets_tag_ids_gin` GIN 인덱스 신규 Alembic revision
- `app/domain/models.py` — `Asset` 도메인 모델에 `tag_ids: list[UUID]` 필드 추가
- `app/repositories/postgres.py` — assets 조회 시 tag_ids 배열 archived 표시 로직 (REQ-001 tags 마스터의 is_active 활용); `list_assets`에 status/market 필터 옵션 보강
- `app/api/v1_assets.py` — `GET /v1/assets` 라우트를 token_type 분기 portfolio 조회에서 admin permission 검증 + `list_assets` 호출로 재정의 (REL-001 smoke 회귀 봉합)
- `app/schemas/v1.py` — `AssetCatalogItem`, `AssetCatalogListResponse` 신규 정의 (tag_ids 필드 포함)
- `tests/test_asset_catalog.py` — REQ-002 AC-1~4 (schema, GIN, archived, 기본값) + REQ-010 AC-9~12 (admin GET 200, status/market 필터, non-admin 403) 신규 케이스
- `tests/test_v1.py` — 구 GET /v1/assets portfolio 응답 기대 케이스 정리
- Out of Scope:
- `assets.tag_ids`에 대한 apply/remove HTTP API — REQ-005에서 이미 정의된 `POST /v1/assets/{asset_id}/tags`는 본 batch가 의존성 토대를 제공하지만 라우트 자체의 회귀 검증은 별도 verification 트랙
- `pfhub asset tag` CLI 노출 — REQ-007 범위 또는 후속 REQ
- admin asset catalog 응답에 `markets`/`status` 외 필터 신설 — 본 batch는 AC 명시 항목만
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 선행 완료: REQ-009(assets 테이블, BAT-001) ✅, REQ-001(tags 마스터, BAT-003) ✅, REQ-005(태그 apply/remove API, BAT-004) ✅
- Trace 정리: REQ-010은 BAT-002(release-candidate)에도 trace가 잔존. 본 batch verification 통과 후 confirm-req-implemented 단계에서 BAT-012 trace를 우선 적용하고 BAT-002의 REQ-010 trace는 augmentation superseded로 표기한다.
- REQ-002의 BAT-003 trace는 CHG-20260521-02에서 무효 처리됨 — 본 batch가 REQ-002의 유일한 active trace.
@@ -0,0 +1,79 @@
# BAT-012 Planning
- Status: confirmed
- Batch: BAT-012
- Profile: standard
## Included REQ
- REQ-002: `assets.tag_ids UUID[]` 컬럼 + `idx_assets_tag_ids_gin` 도입 (Data, dcy-002 FR-2)
- REQ-010: `GET /v1/assets` admin catalog 재정의 augmentation (Interface, dcy-001 FR-2 + dcy-003 옵션 B 후속 — CHG-20260521-02 AC-9~12)
## Delivery Plan
1. **Migration 도입 (REQ-002 AC-1, AC-2)**
- `migrations/versions/20260521_NNNN_assets_tag_ids_column.py` 신규 revision
- `op.add_column("assets", sa.Column("tag_ids", sa.ARRAY(sa.UUID()), nullable=False, server_default=sa.text("ARRAY[]::uuid[]")))`
- `op.create_index("idx_assets_tag_ids_gin", "assets", ["tag_ids"], postgresql_using="gin")`
- downgrade에서 인덱스 → 컬럼 순으로 제거
2. **도메인 모델 보강 (REQ-002 AC-3, AC-4)**
- `app/domain/models.py` — `Asset` 모델에 `tag_ids: list[UUID]` 필드 추가 (기본값 빈 리스트)
3. **Repository 보강 (REQ-002 AC-3, REQ-010 AC-10, AC-11)**
- `app/repositories/postgres.py`
- assets 조회 시 `tag_ids`를 그대로 로드하고, archived 표시 응답은 schema/API 계층에서 tags 마스터 is_active를 조회해 합성 (REQ-003에서 portfolios용으로 이미 도입된 archived 표시 헬퍼 재사용)
- `list_assets(status: str | None, market: str | None) -> list[Asset]` 시그니처 보강 — status 필터(active/inactive/all 의미는 라우트에서 변환) + `<market> = ANY(markets)` 필터
4. **응답 스키마 신설 (REQ-010 AC-9, REQ-002 AC-4 합류)**
- `app/schemas/v1.py`
- `AssetCatalogItem`: `asset_id, symbol, markets, display_name, status, tag_ids (각 tag_id에 archived 표시 메타 동반), created_at, updated_at`
- `AssetCatalogListResponse`: `{ items: list[AssetCatalogItem] }`
5. **GET /v1/assets 라우트 재정의 (REQ-010 AC-9~12)**
- `app/api/v1_assets.py`
- 기존 token_type 분기(daemon/user/admin portfolio 조회) 제거
- admin permission 검증 → 실패 시 HTTP 403
- query param `status` (active|inactive|all, 기본 active), `market` (선택, MarketCode 1개) 파싱
- `list_assets` 호출 후 `AssetCatalogListResponse`로 직렬화
6. **테스트 신설/갱신**
- `tests/test_asset_catalog.py`
- REQ-002 AC-1: Alembic 적용 후 `tag_ids` 컬럼 존재 (SQLAlchemy Inspector)
- REQ-002 AC-2: `idx_assets_tag_ids_gin` 존재
- REQ-002 AC-3: is_active=FALSE tag_id 포함 시 archived 표시 동반 반환
- REQ-002 AC-4: 빈 배열 기본값 반환
- REQ-010 AC-9: admin GET 200 + `AssetCatalogListResponse` shape (portfolio 필드 부재)
- REQ-010 AC-10: `status=all`/`status=inactive` 필터 동작
- REQ-010 AC-11: `market=KRX` 필터 동작
- REQ-010 AC-12: non-admin (user/service/client) 호출 시 HTTP 403
- `tests/test_v1.py`
- 구 GET /v1/assets에서 portfolio 응답을 기대하는 케이스 정리 (GET /v1/portfolios로 이전 또는 catalog 응답 schema로 교체)
7. **로컬 smoke 재실행 (REL-001 회귀 확인)**
- admin session token으로 `GET /v1/assets` → HTTP 200 + asset catalog 응답
- service token으로 `GET /v1/assets` → HTTP 403
- REL-001 smoke 체크리스트의 해당 line 재확인
## Dependencies
- Internal:
- REQ-009 (Implemented, BAT-001) — assets 테이블 ✅
- REQ-001 (Implemented, BAT-003) — tags 마스터 테이블 + is_active 컬럼 ✅
- REQ-003 (Implemented, BAT-003) — portfolios.tag_ids archived 표시 헬퍼 재사용 가능 ✅
- REQ-005 (Implemented, BAT-004) — `POST /v1/assets/{asset_id}/tags` 라우트가 본 batch의 컬럼 도입 후 실제 저장 가능
- External:
- 없음 (trex-basic 등 외부 클라이언트 영향 없음)
## Design Gate
- Design Required Before Implementation: **yes**
- Reason:
1. archived 표시 응답 포맷 결정 — `tag_ids: [uuid]` + 별도 `tag_status_map: {uuid: 'active'|'archived'}` vs `tags: [{tag_id, status}]` 객체 배열. REQ-003 portfolios.tag_ids에서 이미 채택한 형식과 정합 필요.
2. `AssetCatalogItem`에 `tag_ids` 필드를 어느 구조로 노출할지 → 위 결정과 연동.
3. GET /v1/assets 재정의로 인한 기존 token_type 분기 코드의 호출 그래프 영향 — service-token/user-token 클라이언트 호출 패턴 회귀 영향 분석 (특히 trex-basic이 GET /v1/assets를 호출했는지 BAT-011 후속으로 확인).
4. BAT-002의 REQ-010 trace 정리 방식 — BAT-002 종결 시점 정책 결정.
## Milestones
1. design.md confirmed — archived 표시 포맷, list_assets 시그니처, BAT-002 trace 정리 정책 결정
2. Alembic revision 추가 + 도메인 모델/repository 보강 완료 (REQ-002 AC-1~4 자가검증)
3. AssetCatalogItem/AssetCatalogListResponse + GET 라우트 재정의 완료 (REQ-010 AC-9~12 자가검증)
4. `tests/test_asset_catalog.py` 신규 케이스 전부 pass + `tests/test_v1.py` 회귀 통과
5. 로컬 smoke 재실행으로 REL-001 회귀 봉합 확인
6. confirm-batch-verification 통과 후 BAT-012 → release-candidate, REQ-002/REQ-010 → Implemented
## Risks
- R-1 (Medium): `GET /v1/assets` 라우트 동작 변경 — REL-001 smoke에서 이미 회귀(HTTP 403 "Unsupported token type") 발생한 surface. service-token/user-token이 GET /v1/assets를 호출하는 코드 경로가 잔존하면 본 batch 배포 후 회귀가 다시 발생. 완화: design 단계에서 `grep -rn "GET.*/v1/assets" app/ tests/` 호출 그래프 전수 조사 + 회귀 테스트 명시.
- R-2 (Medium): BAT-002와 BAT-012 사이 REQ-010 trace 중복 — confirm-req-implemented 시 BAT-012 verification을 우선 적용하고 BAT-002의 REQ-010 trace를 augmentation superseded로 표기하지 않으면 release 트레이스가 모호해짐. 완화: design.md `Key Decisions`에 trace 정리 정책 명문화.
- R-3 (Low): assets 테이블에 `tag_ids` 컬럼 + GIN 인덱스 추가 시 lock — assets 테이블 규모가 작아(카탈로그 수십~수백 row 수준) 운영 영향 미미. 완화: 변경 없음.
- R-4 (Low): tags 마스터 is_active 조회로 archived 표시 합성 시 N+1 query 가능 — REQ-003 portfolios에서 이미 도입한 batch 조회 패턴 재사용. 완화: design에서 동일 헬퍼 사용 명시.
@@ -0,0 +1,44 @@
# BAT-012 Verification
- Status: confirmed
- Batch: BAT-012
## Acceptance Mapping
- REQ-002 (assets.tag_ids UUID[] 컬럼):
- AC-1 (`tag_ids UUID[] NOT NULL DEFAULT '{}'` 컬럼 존재) ↔ `tests/test_asset_catalog.py::test_migration_assets_tag_ids_adds_column_and_gin_index` (migration 파일 구조에서 `add_column`/`UUID`/`nullable=False`/`'{}'::uuid[]` 확인). 실 DB 컬럼 존재는 REL 배포 시 alembic upgrade로 최종 확인.
- AC-2 (`idx_assets_tag_ids_gin` GIN 인덱스 존재) ↔ 동일 테스트의 `postgresql_using="gin"` + `idx_assets_tag_ids_gin` assertion. 실 DB 인덱스 존재는 REL 배포 시 alembic upgrade로 최종 확인.
- AC-3 (is_active=FALSE tag_id 포함 시 archived 표시 동반 반환) ↔ `tests/test_asset_catalog.py::test_get_v1_assets_archived_flag_for_inactive_tag` (실제 API 응답으로 `{tag_id, archived: True}` 확인). 단위는 `test_build_tag_refs_archived_for_inactive_tags`.
- AC-4 (빈 배열 기본값) ↔ `tests/test_asset_catalog.py::test_asset_model_tag_ids_defaults_to_empty_list` + `test_asset_catalog_response_schema_has_tag_ids_field` (default_factory=list) + `test_get_v1_assets_admin_returns_catalog_response`의 a-2 자산 `tag_ids == []` 검증.
- REQ-010 (admin asset catalog CRUD + GET 재정의 augmentation):
- AC-1~8 (기존 CLI/HTTP CRUD + non-admin 403): BAT-002에서 이미 통과. 본 batch 회귀 suite로 회귀 확인 — `tests/test_asset_catalog.py::test_asset_cli_create_success / test_asset_cli_list_returns_all / test_asset_cli_list_active_only_filter / test_asset_cli_update_success / test_asset_cli_deactivate_success / test_asset_http_create_returns_201 / test_asset_http_update_returns_200 / test_asset_http_deactivate_returns_200 / test_asset_http_non_admin_403` 9건 PASS.
- AC-9 (admin GET 200 + AssetCatalogListResponse shape) ↔ `test_get_v1_assets_admin_returns_catalog_response` (status_code=200, `items`/`total` 키 + portfolio 필드 부재 확인).
- AC-10 (`status=all`/`status=inactive` 필터 + 기본 active) ↔ `test_get_v1_assets_status_filter_all_includes_inactive` + `test_get_v1_assets_status_filter_inactive_only` + `test_get_v1_assets_status_filter_default_active_only` + `test_get_v1_assets_status_filter_rejects_unknown_value` (422 반환).
- AC-11 (`market=KRX` 필터) ↔ `test_get_v1_assets_market_filter` (`markets`에 `KRX` 포함된 자산만 응답).
- AC-12 (non-admin → HTTP 403) ↔ `test_get_v1_assets_user_token_returns_403` + `test_get_v1_assets_service_token_returns_403`.
## Evidence
- Test:
- `python -m pytest tests/test_asset_catalog.py` → **50 passed in 1.51s** (기존 33 + 신규 17)
- `python -m pytest` 전체 suite → **362 passed in 13.06s** (기존 330 + 신규 32, 회귀 0)
- Manual:
- 로컬 smoke 미실행 — pytest TestClient 기반 통합 테스트로 admin token + service/user token 분기 모두 자동 검증. REL 배포 시 alembic upgrade 결과로 DB-integration AC 최종 확인.
- Logs:
- pytest 출력은 implementation.md Execution Log 항목 9~10 참조.
## Architecture Impact Sync
- design.md `Architecture Impact: runtime-flows` 명시됨.
- `docs/runtime-flows.md` 갱신 완료:
- `Flow: v1 API — GET /v1/assets` 본문을 token_type 분기 portfolio 조회 → admin asset catalog 조회 + tag_ids/archived 합성으로 재작성
- Change Log entry 추가 (2026-05-21 KST BAT-012)
- `docs/project-structure.md`: Architecture Impact = none, 갱신 불요.
- `docs/data-model.md` (assets 스키마에 tag_ids 컬럼 명시) 및 `docs/interface-contract.md` (GET /v1/assets 응답에 tag_ids 필드 명세) 추가 docs sync는 별도 docs REQ로 분리 (본 batch verification blocker 아님).
## Result
- Ready for Release: true
- Blocking Issues:
- 없음 (본 batch 범위 내 AC 전수 evidence 확보)
- Follow-up (release 또는 후속 REQ):
- REQ-005 follow-up: `POST/DELETE /v1/assets/{asset_id}/tags`가 portfolios 테이블을 잘못 대상화하는 잠재 버그 — 별도 REQ/Batch에서 정합화.
- BAT-002의 REQ-010 trace 정리: confirm-req-implemented 단계에서 BAT-012 trace 우선 적용 + BAT-002 표기를 `superseded by BAT-012`로 갱신.
- `docs/data-model.md` / `docs/interface-contract.md` docs sync REQ 발의 필요.
+1
View File
@@ -19,3 +19,4 @@
| BAT-009 | standard | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028 | dcy-003 | [bat-009](./bat-009_20260521_v1-portfolios-crud-core/) |
| BAT-010 | standard | release-candidate | REQ-030, REQ-031, REQ-032 | dcy-003 | [bat-010](./bat-010_20260521_v1-portfolios-sub-resources/) |
| BAT-011 | standard | release-candidate | REQ-029 | dcy-003 | [bat-011](./bat-011_20260521_v1-portfolios-ledger-path-migration/) |
| BAT-012 | standard | release-candidate | REQ-002, REQ-010 | dcy-001, dcy-002, dcy-003 | [bat-012](./bat-012_20260521_assets-tagging-and-admin-get-redefinition/) |