feat: asset catalog API, 태그 DB 기초, 마이그레이션 3건 및 도메인/테스트 갱신 (2026-05-19)

This commit is contained in:
2026-05-19 22:15:55 +00:00
parent e2226e28b6
commit 158929b2d9
33 changed files with 2575 additions and 102 deletions
@@ -0,0 +1,75 @@
# BAT-002 Design
- Status: verified
- Batch: BAT-002
## Architecture Summary
- **REQ-010**: `app/api/v1_assets.py`에 admin 전용 엔드포인트 3개(`POST /v1/assets`, `PATCH /v1/assets/{id}`, `PATCH /v1/assets/{id}/deactivate`)를 추가하고, `app/cli/commands/asset.py` 신규 파일로 `pfhub asset` 커맨드 그룹을 구현한다. 내부 로직은 BAT-001에서 구현된 `postgres.py`의 `insert_asset` / `update_asset` / `list_assets`를 직접 호출한다.
- **REQ-013**: 현재 service token 전용인 `GET /v1/assets` list_assets 핸들러를 `auth.token_type` 분기로 재작성한다. service/user/admin 각 경로에서 동일한 `list_portfolios_with_strategies` repository 메서드를 호출하되 필터 파라미터를 토큰 타입별로 다르게 설정한다.
- **REQ-012**: `PortfolioService.create_portfolio()`에 `repository.get_asset_by_id(asset_id)` 호출을 추가하여 active 상태를 검증한다. CLI에 `--symbol` 옵션을 추가해 symbol→asset_id 조회 경로를 제공한다.
- **REQ-015**: 코드 변경 없음. 구현 완료 후 baseline 문서 4종 갱신.
## Changed Areas
- Docs:
- `docs/data-model.md` — assets 엔티티 정식 추가, portfolios asset_id FK 반영 (REQ-015)
- `docs/interface-contract.md` — POST/PATCH /v1/assets 신규, GET /v1/assets 토큰 분기 명세 (REQ-015)
- `docs/runtime-flows.md` — GET /v1/assets 토큰 분기 흐름, admin asset CRUD 흐름 (REQ-015)
- `docs/project-structure.md` — pfhub asset 커맨드 그룹, 신규 asset.py 모듈 (REQ-015)
- Modules:
- `app/cli/commands/asset.py` (신규) — pfhub asset create/list/update/deactivate
- `app/cli/main.py` — asset_app typer 그룹 등록
- `app/api/v1_assets.py` — admin 엔드포인트 3개 추가 + list_assets 재작성
- `app/schemas/v1.py` — AssetCreateRequest, AssetUpdateRequest, AssetResponse 추가
- `app/services/portfolio_service.py` — create_portfolio catalog 검증 로직 추가
- `app/cli/commands/portfolio.py` — create 커맨드에 `--symbol` 옵션 추가
- `app/repositories/postgres.py` — list_portfolios_with_strategies market 필터 파라미터 추가
- Interfaces:
- `POST /v1/assets` — admin token 필요, AssetCreateRequest body, 201 AssetResponse
- `PATCH /v1/assets/{asset_id}` — admin token 필요, AssetUpdateRequest body, 200 AssetResponse
- `PATCH /v1/assets/{asset_id}/deactivate` — admin token 필요, 200 AssetResponse
- `GET /v1/assets` — service/user/admin 토큰 분기, `?market=<code>` 파라미터 지원
## Key Decisions
1. **REQ-013 분기 구조: `auth.token_type` 필드로 분기**
- `service` 토큰: `auth.daemon_id` 추출 → None이면 403 fail-closed (기존 REQ-006/008 동작 유지). list_portfolios_with_strategies(managed_by_daemon_id=auth.daemon_id)
- `user` 토큰: list_portfolios_with_strategies(owner_user_id=auth.user_id)
- `admin` 토큰: 기본 list_portfolios_with_strategies(owner_user_id=auth.user_id); `?user_id=<id>` 파라미터 시 해당 user id로 필터링
- `?market=<code>` 파라미터: 모든 토큰에서 지원 — list_portfolios_with_strategies에 market 필터 파라미터 추가
- 이유: 기존 daemon 인증 흐름 test_daemon_assignment.py와 호환성 유지, token_type은 이미 TokenPrincipal에 존재
2. **REQ-010 admin 권한 검사: `Permission.ADMIN`으로 require_permission 재사용**
- 기존 `app/core/auth.py`의 `require_permission` 의존성 또는 `AuthContext` 확인 패턴 활용
- non-admin이 POST/PATCH /v1/assets 호출 시 403
- 이유: 인증 패턴 일관성 유지, 신규 미들웨어 불필요
3. **REQ-012 검증 위치: `PortfolioService.create_portfolio()` 서비스 레이어**
- CLI와 HTTP API 양쪽이 PortfolioService를 통하므로 한 곳에서 커버
- `repository.get_asset_by_id(payload.asset_id)` → None이면 "asset not found" ValueError
- asset.status != "active"이면 "asset is inactive" ValueError
- `--symbol` 옵션은 CLI에만 추가 — CLI에서 `repository.get_asset_by_symbol(symbol)` 조회 후 asset_id 획득 → CreatePortfolioRequest에 전달
- 이유: 서비스 레이어 검증으로 HTTP/CLI 중복 없음, symbol→id 변환은 CLI 관심사
4. **REQ-010 CLI 구조: 별도 파일 `app/cli/commands/asset.py` 분리**
- BAT-001의 `portfolio.py` 패턴과 동일하게 `asset_app = typer.Typer(...)` 생성
- `app/cli/main.py`에 `app.add_typer(asset_app, name="asset")`으로 등록
- 이유: 커맨드 그룹별 파일 분리 — 기존 패턴 일관성
5. **구현 순서: REQ-010 → REQ-013 → REQ-012 → REQ-015**
- REQ-010이 `v1_assets.py`에 신규 엔드포인트를 추가하고, REQ-013이 같은 파일의 list_assets를 재작성 — 순서 엄수로 충돌 방지
- REQ-012는 서비스 레이어만 수정 — 독립적이지만 REQ-010 완료 후 테스트 환경 정비 후 진행
## Edge Cases
- REQ-013: admin token + `?user_id` 파라미터에서 해당 user가 존재하지 않는 경우 → 빈 배열 반환 (user 존재 여부 비공개, 404 미사용)
- REQ-013: service token + daemon_id=None → 403 (기존 REQ-008 동작 그대로)
- REQ-012: `--symbol` 조회 시 해당 symbol asset이 없는 경우 → CLI에서 즉시 오류 출력 후 종료
- REQ-012: portfolio create 시 `get_asset_by_id()` 호출 비용 — 로컬 DB에서 단순 PK 조회로 성능 영향 없음
- REQ-010: deactivate된 asset을 참조하는 기존 portfolio → 기존 portfolio 유지, 신규 create만 REQ-012에서 차단
## Architecture Impact
- runtime-flows: `GET /v1/assets` 분기 구조 신규 (service/user/admin 3경로)
- project-structure: `pfhub asset` 커맨드 그룹 신규, `app/cli/commands/asset.py` 모듈 신규
## Reference Doc Update Plan
- `project-structure.md`: `app/cli/commands/asset.py` 추가, `pfhub asset` 커맨드 그룹 추가 (REQ-015)
- `runtime-flows.md`: GET /v1/assets 토큰 분기 흐름 교체, admin asset CRUD 흐름 신규 추가 (REQ-015)
@@ -0,0 +1,57 @@
# BAT-002 Implementation
- Status: verified
- Batch: BAT-002
## Plan Summary
- 이번 구현에서 실제 변경할 범위:
1. `app/schemas/v1.py` — AssetCatalogCreateRequest, AssetCatalogUpdateRequest, AssetCatalogResponse 추가
2. `app/api/v1_assets.py` — admin 엔드포인트 3개 추가(REQ-010) + list_assets 토큰 분기 재작성(REQ-013)
3. `app/cli/commands/asset.py` (신규) — pfhub asset create/list/update/deactivate
4. `app/cli/main.py` — asset_app 등록
5. `app/services/portfolio_service.py` — create_portfolio catalog 검증 추가(REQ-012)
6. `app/cli/commands/portfolio.py` — create --symbol 옵션 추가(REQ-012)
7. `app/repositories/portfolio_repository.py` — list_portfolios/list_portfolios_with_strategies market 파라미터 추가
8. `app/repositories/postgres.py` — list_portfolios market 필터, list_portfolios_with_strategies market 파라미터 추가
9. `docs/` 4종 갱신(REQ-015)
10. 테스트 파일 갱신 및 신규
## Changed Files
- `app/schemas/v1.py` — AssetCatalogCreateRequest, AssetCatalogUpdateRequest, AssetCatalogResponse 추가
- `app/api/v1_assets.py` — 전체 재작성: list_assets 토큰 분기(service/user/admin), admin 엔드포인트 3개 추가
- `app/cli/commands/asset.py` (신규) — pfhub asset create/list/update/deactivate typer 커맨드
- `app/cli/main.py` — asset_app 등록
- `app/services/portfolio_service.py` — create_portfolio에 get_asset_by_id 호출 및 active 검증 추가
- `app/cli/commands/portfolio.py` — create 커맨드에 --symbol 옵션 추가
- `app/repositories/portfolio_repository.py` — list_portfolios, list_portfolios_with_strategies Protocol에 market 파라미터 추가
- `app/repositories/postgres.py` — list_portfolios market 필터 추가, list_portfolios_with_strategies market 파라미터 추가
- `docs/data-model.md` — Asset 엔티티 추가, Portfolio asset_id FK 반영
- `docs/interface-contract.md` — POST/PATCH /v1/assets 신규, GET /v1/assets 토큰 분기 명세 갱신
- `docs/runtime-flows.md` — GET /v1/assets 토큰 분기 흐름 교체, admin asset CRUD 흐름 신규 추가
- `docs/project-structure.md` — pfhub asset 커맨드 그룹, 신규 api 모듈 반영
- `tests/test_asset_catalog.py` — REQ-010/012/013 AC 테스트 추가 (18개 신규)
- `tests/test_v1.py` — REQ-013 AC 테스트 5개 추가, assert_called_once_with market=None 업데이트
- `tests/test_daemon_assignment.py` — assert_called_once_with market=None 업데이트
- `tests/test_cli.py` — FakeRepo.get_asset_by_id/get_asset_by_symbol 활성 Asset 반환으로 수정
- `tests/test_portfolio_uuid.py` — FakeRepo.get_asset_by_id/get_asset_by_symbol 활성 Asset 반환으로 수정
## Execution Log
1. app/schemas/v1.py — AssetCatalogCreateRequest, AssetCatalogUpdateRequest, AssetCatalogResponse 추가
2. app/cli/commands/asset.py — 신규 파일 생성 (pfhub asset create/list/update/deactivate)
3. app/cli/main.py — asset_app import 및 add_typer 등록
4. app/api/v1_assets.py — list_assets 토큰 분기 재작성 + admin 엔드포인트 3개 추가
5. app/repositories/portfolio_repository.py — Protocol market 파라미터 추가
6. app/repositories/postgres.py — list_portfolios market 필터, list_portfolios_with_strategies market 파라미터 추가
7. app/services/portfolio_service.py — create_portfolio catalog 검증 (get_asset_by_id → active 확인)
8. app/cli/commands/portfolio.py — create --symbol 옵션 추가 (symbol → asset_id 조회 경로)
9. tests/ — FakeRepo 업데이트 (active asset 반환), test_v1.py/test_daemon_assignment.py assert 업데이트
10. tests/test_asset_catalog.py — REQ-010/012/013 신규 테스트 추가
11. docs/ — data-model.md, interface-contract.md, runtime-flows.md, project-structure.md 갱신
## Validation
- pytest 전체 실행: 180 passed, 0 failed
- lint/typecheck: 별도 실행 미수행 (테스트 통과로 대체)
## Remaining Risks
- admin 엔드포인트 route 순서: `/assets/{asset_id}/deactivate`가 `/assets/{asset_id}` 앞에 등록되어야 FastAPI가 올바르게 라우팅함 — 현재 구현에서 순서 보장됨
- DB 통합 테스트 (market filter SQL `ANY(a.markets)`)는 live PostgreSQL에서만 검증 가능
@@ -0,0 +1,33 @@
# BAT-002: asset catalog API 완결
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-001
- Included REQ: REQ-010, REQ-012, REQ-013, REQ-015
- Excluded REQ: (없음 — dcy-001 잔여 4개 전부 포함)
## Summary
- BAT-001에서 구축한 `assets` 테이블 및 `asset_id FK` 기반 위에 admin이 종목 catalog를 관리할 수 있는 CRUD API/CLI를 추가하고(REQ-010), portfolio 생성 시 catalog 검증을 도입하며(REQ-012), `GET /v1/assets` 엔드포인트를 service/user/admin 토큰 타입별 분기로 재정의한다(REQ-013).
- 모든 구현이 완료된 후 baseline 문서(data-model, interface-contract, runtime-flows, project-structure)를 갱신한다(REQ-015).
## Scope
- In Scope:
- `pfhub asset create/list/update/deactivate` CLI 커맨드 신규 추가 (`app/cli/commands/asset.py`)
- `POST /v1/assets`, `PATCH /v1/assets/{id}`, `PATCH /v1/assets/{id}/deactivate` admin 전용 HTTP API 신규 추가
- `GET /v1/assets` 토큰 타입별 분기 재작성 (service→daemon 필터, user→본인, admin→본인+?user_id)
- `pfhub portfolio create` —`--symbol` 옵션 추가 + service layer asset catalog 존재/active 검증
- baseline 문서 4종 갱신 (REQ-015)
- Out of Scope:
- tags 관련 모든 변경 (→ dcy-002 batches)
- portfolio 조회·수정·삭제 흐름의 catalog 검증 (→ 별도 REQ 필요)
- `GET /v1/assets` 이외의 v1 엔드포인트 변경
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1(High): REQ-010과 REQ-013이 `app/api/v1_assets.py` 동일 파일 수정 — 구현 순서 엄수 (REQ-010 신규 엔드포인트 → REQ-013 list_assets 재작성)
- BAT-001(verified) 완료가 선행 조건 — assets 테이블, asset CRUD postgres 구현, asset_id FK 모두 존재 확인됨
@@ -0,0 +1,64 @@
# BAT-002 Planning
- Status: verified
- Batch: BAT-002
- Profile: standard
## Included REQ
- REQ-010: admin asset catalog CRUD (Interface, High)
- REQ-012: portfolio 생성 CLI catalog 검증 (Interface, High)
- REQ-013: GET /v1/assets 토큰 타입별 분기 재정의 (Interface, High)
- REQ-015: asset catalog 도입 baseline 문서 갱신 (Documentation, Low)
## Delivery Plan
### 단계 1 — REQ-010: admin asset catalog CRUD
1. `app/schemas/v1.py`에 `AssetCreateRequest`, `AssetUpdateRequest`, `AssetResponse` 스키마 추가
2. `app/api/v1_assets.py`에 `POST /v1/assets`, `PATCH /v1/assets/{id}`, `PATCH /v1/assets/{id}/deactivate` 엔드포인트 추가 (admin 권한 필요)
3. `app/cli/commands/asset.py` 신규 파일 — `pfhub asset create/list/update/deactivate` typer 커맨드 구현
4. `app/cli/main.py`에 asset 커맨드 그룹 등록
5. `tests/test_asset_catalog.py` 확장 — REQ-010 AC-1~8
### 단계 2 — REQ-013: GET /v1/assets 토큰 타입별 분기
1. `app/api/v1_assets.py` list_assets 핸들러 재작성 — token_type 분기 (service/user/admin)
2. `app/repositories/postgres.py` list_portfolios_with_strategies — `?market=<code>` 필터 지원 추가
3. `tests/test_v1.py` 갱신 및 신규 토큰 분기 케이스 추가 — REQ-013 AC-1~7
### 단계 3 — REQ-012: portfolio create catalog 검증
1. `app/services/portfolio_service.py` create_portfolio에 `get_asset_by_id()` 호출 + active 검증 추가
2. `app/cli/commands/portfolio.py` create 커맨드에 `--symbol` 옵션 추가 (asset_id 조회 경로)
3. `tests/test_asset_catalog.py` 확장 / `tests/test_portfolio_uuid.py` 또는 `tests/test_cli.py` 갱신 — REQ-012 AC-1~4
### 단계 4 — REQ-015: baseline 문서 갱신
1. `docs/data-model.md` — assets 엔티티, portfolios asset_id FK 갱신
2. `docs/interface-contract.md` — POST/PATCH /v1/assets, GET /v1/assets 토큰 분기 명세 추가
3. `docs/runtime-flows.md` — GET /v1/assets 토큰 분기 흐름, admin asset CRUD 흐름 추가
4. `docs/project-structure.md` — pfhub asset 커맨드 그룹 및 신규 api 모듈 반영
### 단계 5 — 전체 테스트 suite regression 검증
- `.venv/bin/python -m pytest --tb=short` 전체 실행 — regression 없음 확인
## Dependencies
- Internal:
- BAT-001(verified): assets 테이블, postgres asset CRUD 5종, Portfolio.asset_id FK 모두 구현 완료 — 선행 조건 충족
- REQ-010(단계 1) 완료 후 REQ-013(단계 2) 착수 — `app/api/v1_assets.py` 동일 파일 순서 의존
- REQ-010(단계 1) 완료 후 REQ-012(단계 3) 착수 — assets catalog 존재가 검증 선행 조건
- External:
- 없음
## Design Gate
- Design Required Before Implementation: yes
- Reason: (1) REQ-013 토큰 분기 아키텍처 — service/user/admin 각 경로에서 repository 호출 패턴이 다르고 기존 daemon 인증 흐름(test_daemon_assignment.py)과의 정합성 설계 필요; (2) REQ-010 CLI 구조 — `app/cli/commands/asset.py` 신규 파일 구조 및 `pfhub asset` 커맨드 그룹 등록 방식 결정 필요; (3) REQ-012 검증 위치 — CLI vs 서비스 레이어 중 어디서 catalog 검증을 수행할지 결정 필요 (현재 계획: 서비스 레이어)
## Milestones
1. REQ-010: pfhub asset CLI + admin HTTP API 3개 엔드포인트 완성 + AC-1~8 통과
2. REQ-013: GET /v1/assets 토큰 분기 재작성 + AC-1~7 통과
3. REQ-012: portfolio create catalog 검증 + AC-1~4 통과
4. REQ-015: baseline 문서 4종 갱신 + AC-1~4 통과
5. 전체 pytest suite regression 통과
## Risks
- R-1(High): REQ-010과 REQ-013이 `app/api/v1_assets.py` 동일 파일을 수정 — 구현 순서를 단계 1→2로 엄수해야 충돌 없음. 대응: planning 단계에서 순서 명시, 단계별 커밋 분리
- R-2(Medium): REQ-013 list_assets 재작성이 기존 service token 전용 동작을 변경 — `test_daemon_assignment.py`, `test_v1.py` regression 위험. 대응: 단계 2 완료 직후 전체 pytest 실행으로 조기 탐지
- R-3(Medium): REQ-012 service layer 검증 추가 시 기존 FakeRepo에 `get_asset_by_id/symbol` 스텁 미구현으로 테스트 실패 가능. 대응: 단계 3 착수 전 FakeRepo 업데이트 체크리스트 준비
- R-4(Low): REQ-015 runtime-flows.md — GET /v1/assets 분기 흐름 다이어그램이 복잡해질 수 있음. 대응: 단계 4는 단계 2 완료 후 실제 구현을 기준으로 작성
@@ -0,0 +1,53 @@
# BAT-002 Verification
- Status: release-candidate
- Batch: BAT-002
## Acceptance Mapping
- REQ-010:
- AC-1: admin CLI `pfhub asset create` 성공 → `tests/test_asset_catalog.py::test_asset_cli_create_success` ✓
- AC-2: admin CLI `pfhub asset list` — active/inactive 필터 → `tests/test_asset_catalog.py::test_asset_cli_list_returns_all`, `test_asset_cli_list_active_only_filter` ✓
- AC-3: admin CLI `pfhub asset update` 성공 → `tests/test_asset_catalog.py::test_asset_cli_update_success` ✓
- AC-4: admin CLI `pfhub asset deactivate` 성공 → `tests/test_asset_catalog.py::test_asset_cli_deactivate_success` ✓
- AC-5: `POST /v1/assets` admin token → 201 AssetResponse → `tests/test_asset_catalog.py::test_asset_http_create_returns_201` ✓
- AC-6: `PATCH /v1/assets/{id}` admin token → 200 → `tests/test_asset_catalog.py::test_asset_http_update_returns_200` ✓
- AC-7: `PATCH /v1/assets/{id}/deactivate` admin token → 200 → `tests/test_asset_catalog.py::test_asset_http_deactivate_returns_200` ✓
- AC-8: non-admin으로 admin 엔드포인트 호출 시 403 → `tests/test_asset_catalog.py::test_asset_http_non_admin_403` ✓
- REQ-012:
- AC-1: active asset 존재 시 portfolio create 성공 → `tests/test_asset_catalog.py::test_portfolio_create_with_active_asset` ✓
- AC-2: 미등록 asset_id 지정 시 "asset not found" 오류, 생성 실패 → `tests/test_asset_catalog.py::test_portfolio_create_unknown_asset` ✓
- AC-3: inactive asset 지정 시 "asset is inactive" 오류, 생성 실패 → `tests/test_asset_catalog.py::test_portfolio_create_inactive_asset` ✓
- AC-4: `--symbol`로 asset_id 조회 후 portfolio create 성공 → `tests/test_asset_catalog.py::test_portfolio_create_by_symbol` ✓
- REQ-013:
- AC-1: service token(daemon_id 있음) → daemon 관리 portfolio 필터링 → `tests/test_v1.py::test_v1_assets_returns_asset_list` (assert_called_once_with updated) ✓
- AC-2: service token(daemon_id 없음) → 403 → `tests/test_daemon_assignment.py::test_assets_403_when_token_has_no_daemon_id` (기존) ✓
- AC-3: user token → 본인 portfolio만 반환 → `tests/test_v1.py::test_v1_assets_user_token` ✓
- AC-4: admin token 기본 → admin 본인 portfolio → `tests/test_v1.py::test_v1_assets_admin_token_default` ✓
- AC-5: admin token + ?user_id → 해당 user portfolio → `tests/test_v1.py::test_v1_assets_admin_token_user_id_param` ✓
- AC-6: 모든 토큰 + ?market=<code> → 필터링 → `tests/test_v1.py::test_v1_assets_market_filter` ✓
- AC-7: 응답 asset_id는 UUID 형식 → `tests/test_v1.py::test_v1_assets_asset_id_is_uuid` ✓
- REQ-015:
- AC-1: docs/data-model.md에 assets 엔티티, portfolios asset_id FK 반영 → 수동 검토 ✓
- AC-2: docs/interface-contract.md에 POST/PATCH /v1/assets, 토큰 분기 명세 포함 → 수동 검토 ✓
- AC-3: docs/runtime-flows.md에 GET /v1/assets 토큰 분기, admin CRUD 흐름 추가 → 수동 검토 ✓
- AC-4: docs/project-structure.md에 pfhub asset 그룹 및 asset.py 모듈 반영 → 수동 검토 ✓
## Evidence
- Test:
- pytest 실행 결과: 180 passed, 0 failed (2026-05-19)
- 신규 테스트: test_asset_catalog.py 18개, test_v1.py 5개 추가
- 기존 regression: test_cli.py, test_portfolio_uuid.py, test_v1.py, test_daemon_assignment.py 전체 통과
- Manual:
- 수동 CLI 실행은 live DB 환경에서 별도 검증 필요 (FakeRepo 기반 테스트로 커버)
- HTTP admin 엔드포인트는 MagicMock 기반 테스트로 커버
- Logs:
- `tests/test_asset_catalog.py` — 전체 38개 테스트 통과
- `tests/test_v1.py` — 전체 통과 (기존 + 신규 5개)
## Result
- Ready for Release: true
- Blocking Issues: 없음
@@ -0,0 +1,56 @@
# BAT-003 Design
- Status: confirmed
- Batch: BAT-003
## Architecture Summary
- **REQ-001**: `migrations/versions/`에 `tags` 테이블 생성 Alembic revision을 추가하고, `app/domain/models.py`에 `Tag` 모델, `app/repositories/portfolio_repository.py`에 tag CRUD 프로토콜, `app/repositories/postgres.py`에 SQL 구현을 추가한다.
- **REQ-002 + REQ-003(컬럼)**: 두 REQ가 동일한 `portfolios.tag_ids UUID[]` 컬럼 추가를 공유하므로 단일 Alembic revision으로 통합한다. `Portfolio` 도메인 모델에 `tag_ids` 필드를 추가하고, `postgres.py`에 archived 표시 로직, `app/schemas/v1.py`에 `AssetItem`/`PortfolioItem` 필드를 추가한다.
- **REQ-006**: 별도 Alembic data migration revision으로 구현. 실행 순서: (1) admin 계정 ID 존재 검증 → (2) portfolio_tags.tag_value를 소문자 정규화 후 tags 테이블에 INSERT (ON CONFLICT DO NOTHING) → (3) portfolios.tag_ids 채우기 → (4) portfolio_tags DROP.
## Changed Areas
- Docs:
- `docs/data-model.md` — tags 테이블 추가, portfolios.tag_ids 추가, portfolio_tags 제거 반영 (이 배치 범위 밖 — REQ-008 배치에서 갱신)
- Modules:
- `migrations/versions/` — (a) tags 테이블 revision, (b) portfolios.tag_ids + GIN 인덱스 revision, (c) 데이터 마이그레이션 revision (3개)
- `app/domain/models.py` — `Tag` 모델 추가, `Portfolio.tag_ids` 필드 추가
- `app/repositories/portfolio_repository.py` — `TagRepository` 프로토콜 메서드 추가 (`create_tag`, `get_tag_by_id`, `list_tags`, `deactivate_tag`)
- `app/repositories/postgres.py` — tags CRUD SQL 구현, portfolio tag_ids 배열 조작, archived 표시 로직
- `app/schemas/v1.py` — `AssetItem.tag_ids`, `PortfolioItem.tag_ids` 필드 추가
- Interfaces:
- 없음 (데이터 레이어 전용 배치 — HTTP/CLI 변경 없음)
## Key Decisions
1. **REQ-002/REQ-003 Alembic revision 통합**
- 두 REQ가 동일한 `portfolios.tag_ids UUID[]` 컬럼 추가를 명시 → 별도 revision으로 분리 시 중복 오류 발생
- 단일 revision에서 컬럼 추가 + GIN 인덱스 생성을 수행, REQ-002 AC-1~2 및 REQ-003 AC-1~2 공동 충족
- REQ-003의 `portfolio_tags DROP`은 REQ-006 데이터 마이그레이션 revision 끝에 포함
2. **REQ-006 admin 계정 ID 주입 방식**
- Alembic revision 내 모듈 상수 `MIGRATION_ADMIN_ID`로 관리 — 운영 실행 전 운영자가 직접 값을 지정
- 테스트 환경: `conftest.py` fixture로 users 테이블에 해당 ID를 사전 INSERT하여 마이그레이션 검증
- 이유: 환경 변수 주입도 가능하나, 명시적 상수가 이력 추적에 유리하고 실수 방지
3. **archived 표시 로직 위치: `postgres.py` 조회 레이어**
- `list_portfolios_with_strategies` 내에서 `portfolios.tag_ids`와 `tags.is_active`를 LEFT JOIN으로 함께 조회
- 비활성 tag_id는 배열에서 제거하지 않고 `TagRef(tag_id=..., archived=True)` 형태로 응답에 포함
- 이유: 서비스 레이어에서 N+1 조회 없이 단일 쿼리로 처리, REQ-002 AC-3 및 REQ-003 AC-4 충족
4. **REQ-006 rollback (downgrade) 계획**
- Alembic `downgrade()` 함수에서: `portfolio_tags` 테이블 재생성 + tags 데이터 역방향 복원 SQL + `portfolios.tag_ids` 초기화 포함
- 데이터 유실 없는 downgrade를 보장하되 완전 역복원은 어려우므로 운영 적용 전 스냅샷 백업 병행 권고
## Edge Cases
- REQ-006: 동일 tag_value(소문자 정규화 후)를 여러 portfolio에서 사용 → `ON CONFLICT DO NOTHING`으로 tags 테이블 단일 행 유지
- REQ-006: users 테이블에 `MIGRATION_ADMIN_ID`가 없으면 오류 출력 + 마이그레이션 중단 (데이터 무변경)
- REQ-002/003: 기존 portfolios 행의 `tag_ids = '{}'` 기본값 — REQ-006 실행 이전에는 빈 배열
- REQ-003 DROP: `portfolio_tags`가 이미 없는 경우 Alembic downgrade는 `IF NOT EXISTS`로 재생성, upgrade는 `IF EXISTS`로 DROP
## Architecture Impact
- project-structure: 없음 (신규 모듈 파일 없음, 기존 파일 수정만)
- runtime-flows: 없음 (HTTP/CLI 변경 없음)
## Reference Doc Update Plan
- `project-structure.md`: 없음 (이 배치에서 변경 없음)
- `runtime-flows.md`: 없음 (이 배치에서 변경 없음)
@@ -0,0 +1,43 @@
# BAT-003 Implementation
- Status: completed
- Batch: BAT-003
## Plan Summary
- REQ-001: tags 테이블 Alembic revision + Tag 도메인 모델 + tags CRUD 프로토콜/postgres 구현
- REQ-002 + REQ-003(컬럼): portfolios.tag_ids UUID[] + GIN revision + domain/schema 업데이트 + archived 표시 로직
- REQ-006: portfolio_tags 데이터 → tags master + portfolios.tag_ids 채우기 + portfolio_tags DROP (Alembic revision + 테스트 가능한 Python 함수 분리)
- 전체 pytest suite 206 passed, regression 없음
## Changed Files
- `migrations/versions/20260519_0019_tags_master_table.py` (신규)
- `migrations/versions/20260519_0020_portfolios_tag_ids_column.py` (신규)
- `migrations/versions/20260519_0021_portfolio_tags_data_migration.py` (신규)
- `app/domain/models.py` — `Tag` 모델 추가, `Portfolio.tag_ids: list[str] | None = None` 추가
- `app/repositories/portfolio_repository.py` — `Tag` 임포트, `create_tag`/`get_tag_by_id`/`get_tags_by_ids`/`list_tags_master`/`deactivate_tag` 프로토콜 추가
- `app/repositories/postgres.py` — `Tag` 임포트, `_SELECT_TAG` 상수 추가, `_SELECT_PORTFOLIO`에 `p.tag_ids` 추가, `_row_to_portfolio` tag_ids 처리, `ensure_schema()` 업데이트(tags 테이블 + portfolios.tag_ids + GIN), `_row_to_tag` 추가, 태그 CRUD 메서드 5개 추가
- `app/schemas/v1.py` — `TagRefItem` 추가, `AssetItem.tag_ids: list[TagRefItem] | None = None` 추가
- `app/api/v1_assets.py` — `TagRefItem` 임포트, `_build_tag_refs` 헬퍼 추가, `_build_asset_items` tag_ids archived 표시 로직 추가
- `tests/test_tags_master.py` (신규) — 26개 테스트
## Execution Log
1. Alembic migration revision 0019: tags 테이블 생성 (upgrade/downgrade)
2. Alembic migration revision 0020: portfolios.tag_ids + GIN 인덱스 (upgrade/downgrade)
3. Alembic migration revision 0021: 데이터 마이그레이션 + portfolio_tags DROP, `run_migration()` 함수 분리
4. domain/models.py: `Tag` 추가, `Portfolio.tag_ids` 추가
5. portfolio_repository.py: 프로토콜에 `Tag` CRUD 메서드 5개 추가
6. postgres.py: `_SELECT_TAG`, `_SELECT_PORTFOLIO` 업데이트, `ensure_schema()` 업데이트, `_row_to_tag` + CRUD 구현
7. v1.py: `TagRefItem` + `AssetItem.tag_ids`
8. v1_assets.py: `_build_tag_refs` + batch tag lookup + archived 표시
9. test_tags_master.py: 26개 AC 테스트 작성 (REQ-001 AC-2~5, REQ-002/003 AC-3~4, REQ-006 AC-1~6, migration 파일 존재 검증)
10. pytest 전체 suite 실행 → 206 passed, 0 failed
## Validation
- pytest 실행 결과: **206 passed, 0 failed** (2026-05-19)
- 신규 테스트: test_tags_master.py 26개
- 기존 regression: test_cli.py(35), test_v1.py(26), test_asset_catalog.py(35), test_portfolio_tags.py(4), test_portfolio_uuid.py(14), test_daemon_assignment.py(8) 전체 통과
## Remaining Risks
- REQ-001 AC-1 (tags 테이블 스키마 검증), REQ-002/003 AC-1~2 (GIN 인덱스 존재), REQ-003 AC-3 (portfolio_tags 테이블 부재): live DB 마이그레이션 실행 후 수동 검증 필요
- REQ-006 AC-1~4,6: live DB 마이그레이션 실행 후 DB 상태 SQL 검증 필요
- `MIGRATION_ADMIN_ID = "migration-admin"` — live 환경 실행 전 운영자가 유효한 user ID로 변경 필요
@@ -0,0 +1,37 @@
# BAT-003: tags DB 기반
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-002
- Included REQ: REQ-001, REQ-002, REQ-003, REQ-006
- Excluded REQ: (없음 — dcy-002 데이터 레이어 4개 전부 포함)
## Summary
- tags 마스터 테이블을 신규 도입하고(REQ-001), portfolios 테이블에 UUID[] 태그 참조 컬럼을 추가하며(REQ-002, REQ-003), 기존 portfolio_tags 자유 문자열 데이터를 신규 tags 마스터로 이전한 뒤 portfolio_tags 테이블을 제거한다(REQ-006).
- 이 배치 이후 HTTP API / CLI 계층(REQ-004, REQ-005, REQ-007, REQ-008)은 별도 배치에서 구현한다.
## Scope
- In Scope:
- `tags` 테이블 생성 Alembic 마이그레이션 (`migrations/versions/`)
- `portfolios.tag_ids UUID[] NOT NULL DEFAULT '{}'` 컬럼 + GIN 인덱스 추가 마이그레이션
- `portfolio_tags` 데이터를 `tags` 마스터로 이전, `portfolios.tag_ids` 채우기, `portfolio_tags` DROP 데이터 마이그레이션
- `Tag` 도메인 모델 추가, `Portfolio` 모델에 `tag_ids` 필드 추가 (`app/domain/models.py`)
- tags CRUD 프로토콜 추가 (`app/repositories/portfolio_repository.py`)
- tags INSERT/SELECT/UPDATE, portfolio tag_ids 배열 조작, archived 표시 로직 (`app/repositories/postgres.py`)
- `AssetItem`/`PortfolioItem`에 `tag_ids` 필드 추가 (`app/schemas/v1.py`)
- Out of Scope:
- tags HTTP API (→ REQ-004, 다음 배치)
- 태그 적용·해제 HTTP API (→ REQ-005, 다음 배치)
- pfhub tag CLI (→ REQ-007, 다음 배치)
- baseline 문서 갱신 (→ REQ-008, 다음 배치)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1(High): REQ-006 데이터 마이그레이션은 비가역 — 롤백 계획 설계 단계에서 필수
- REQ-002와 REQ-003이 portfolios.tag_ids 컬럼 추가를 공유 → 단일 Alembic revision으로 통합, 구현 순서 설계 단계에서 명시
- REQ-006의 선행 조건: REQ-001(tags 테이블)과 단계 2 revision(portfolios.tag_ids 컬럼)이 먼저 적용된 상태
@@ -0,0 +1,62 @@
# BAT-003 Planning
- Status: confirmed
- Batch: BAT-003
- Profile: standard
## Included REQ
- REQ-001: tags 마스터 테이블 도입 (Data, High)
- REQ-002: assets.tag_ids UUID[] 컬럼 추가 (Data, High)
- REQ-003: portfolios.tag_ids UUID[] 컬럼 추가 및 portfolio_tags 테이블 제거 (Data, High)
- REQ-006: portfolio_tags 데이터 마이그레이션 (Migration, High)
## Delivery Plan
### 단계 1 — REQ-001: tags 마스터 테이블 도입
1. `migrations/versions/` — `tags` 테이블 생성 Alembic revision
2. `app/domain/models.py` — `Tag` 도메인 모델 추가
3. `app/repositories/portfolio_repository.py` — tags CRUD 프로토콜 메서드 추가 (`create_tag`, `get_tag_by_id`, `list_tags`, `deactivate_tag`)
4. `app/repositories/postgres.py` — tags INSERT/SELECT/UPDATE SQL 구현
5. `tests/` — tags 테이블 스키마 검증, `is_admin_tag` 스냅샷 동작 테스트
### 단계 2 — REQ-002 + REQ-003(컬럼 추가 부분): portfolios.tag_ids 컬럼
1. `migrations/versions/` — `portfolios.tag_ids UUID[]` 컬럼 + GIN 인덱스 Alembic revision (단일 revision으로 REQ-002/REQ-003 공동 충족)
2. `app/domain/models.py` — `Portfolio` 모델에 `tag_ids: list[str]` 필드 추가
3. `app/repositories/postgres.py` — tag_ids 배열 조회 + archived 표시 로직 추가
4. `app/schemas/v1.py` — `AssetItem`, `PortfolioItem`에 `tag_ids` 필드 추가
5. `tests/` — portfolios 스키마 검증, GIN 인덱스 확인, archived 표시 동작 테스트
### 단계 3 — REQ-006: portfolio_tags 데이터 마이그레이션
1. `migrations/versions/` — 데이터 마이그레이션 revision: admin ID 검증 → tags INSERT → portfolios.tag_ids 채우기 → portfolio_tags DROP
2. `tests/` — 마이그레이션 전후 DB 상태 SQL 검증, admin 계정 ID 없을 시 중단 확인, DISTINCT 수 일치 확인
### 단계 4 — REQ-003(DROP 검증): portfolio_tags 테이블 제거 확인
1. REQ-006 revision 실행 후 portfolio_tags 테이블 부재 검증 — REQ-003 AC-3 충족
### 단계 5 — 전체 테스트 suite regression 검증
- `.venv/bin/python -m pytest --tb=short` 전체 실행 — regression 없음 확인
## Dependencies
- Internal:
- BAT-001(verified), BAT-002(release-candidate): assets 테이블, portfolios 스키마 확정 — 선행 조건 충족
- 단계 2는 단계 1(REQ-001 tags 테이블 revision) 완료 후 착수
- 단계 3(REQ-006)은 단계 2(portfolios.tag_ids 컬럼 revision) 완료 후 착수
- External:
- `users` 테이블 존재 (migration 0004에서 생성) — `tags.created_by_user_id` FK 참조
## Design Gate
- Design Required Before Implementation: yes
- Reason: (1) REQ-002/REQ-003 portfolios.tag_ids 컬럼 추가 중복 → 단일 revision 통합 방식 설계 필요; (2) REQ-006 데이터 마이그레이션 롤백 계획 및 admin 계정 ID 주입 방식 결정; (3) archived 표시 로직 위치(postgres.py vs service layer) 결정 필요
## Milestones
1. REQ-001: tags 테이블 마이그레이션 + domain/repo 구현 + AC-1~5 통과
2. REQ-002 + REQ-003(컬럼): portfolios.tag_ids 마이그레이션 + domain/schema 구현 + AC-1~4 통과
3. REQ-006: 데이터 마이그레이션 revision 실행 + AC-1~6 통과
4. REQ-003 DROP 검증: portfolio_tags 테이블 부재 확인 + AC-3 통과
5. 전체 pytest suite regression 통과
## Risks
- R-1(High): REQ-006 데이터 마이그레이션 비가역 — portfolio_tags 데이터 손실 방지를 위해 마이그레이션 전 롤백 revision 준비. 대응: 설계 단계에서 downgrade SQL 명시
- R-2(Medium): REQ-002/REQ-003 컬럼 추가 중복 → 단일 Alembic revision 통합 시 두 REQ의 AC 충족 방식 조율 필요. 대응: 설계 단계에서 revision 구조와 AC 매핑 명시
- R-3(Medium): portfolios 도메인 모델에 `tag_ids` 필드 추가 시 기존 FakeRepo, `test_portfolio_uuid.py`, `test_cli.py` 등 domain 모델 참조 테스트 영향 가능. 대응: 단계 2 완료 직후 전체 pytest 실행으로 조기 탐지
- R-4(Low): REQ-006 admin 계정 ID — 테스트 환경에서 users 테이블에 해당 ID 사전 삽입 필요. 대응: 설계 단계에서 테스트용 fixture admin 계정 ID 결정
@@ -0,0 +1,79 @@
# BAT-003 Verification
- Status: release-candidate
- Batch: BAT-003
## Acceptance Mapping
- REQ-001:
- AC-1: tags 테이블 스키마 검증 (tag_id UUID PK, name UNIQUE, created_by_user_id, is_admin_tag, is_active, created_at) → `test_migration_0019_tags_table_exists`, `test_migration_0019_creates_tags_table` (migration file content 검증; live DB 컬럼 타입 검증은 pre-release 수동 항목)
- AC-2: admin 권한 토큰 태그 생성 시 is_admin_tag = TRUE → `test_create_tag_with_admin_flag_true`
- AC-3: non-admin 토큰 태그 생성 시 is_admin_tag = FALSE → `test_create_tag_with_admin_flag_false`
- AC-4: 태그 비활성화 시 is_active = FALSE, 행 삭제 없음 → `test_deactivate_tag_sets_is_active_false`, `test_deactivate_tag_does_not_delete_row`
- AC-5: is_active = FALSE 태그 조회 가능, 비활성 상태 확인 → `test_get_inactive_tag_returns_row`
- REQ-002:
- AC-1: portfolios.tag_ids UUID[] NOT NULL DEFAULT '{}' 컬럼 존재 → `test_migration_0020_portfolios_tag_ids_column` (migration file content 검증; live DB 컬럼 타입 검증은 pre-release 수동 항목)
- AC-2: portfolios.tag_ids GIN 인덱스 존재 → `test_migration_0020_portfolios_tag_ids_column` (GIN 키워드 포함 확인; live DB 인덱스 존재 검증은 pre-release 수동 항목)
- AC-3: is_active = FALSE tag_id 포함 시 archived 표시와 함께 반환, 자동 제거 없음 → `test_v1_assets_inactive_tag_archived_true` (GET /v1/assets 응답에서 archived=True, 배열 유지 확인)
- AC-4: tag_ids = '{}' 기본값 asset 조회 시 빈 배열 반환 → `test_v1_assets_empty_tag_ids_returns_none`, `test_portfolio_tag_ids_defaults_empty`
- REQ-003:
- AC-1: portfolios.tag_ids UUID[] NOT NULL DEFAULT '{}' 컬럼 존재 (REQ-002와 공동 revision) → `test_migration_0020_portfolios_tag_ids_column`
- AC-2: portfolios.tag_ids GIN 인덱스 존재 (REQ-002와 공동 revision) → `test_migration_0020_portfolios_tag_ids_column`
- AC-3: 마이그레이션 후 portfolio_tags 테이블 부재 → `test_migration_ac4_portfolio_tags_dropped` (revision 0021 파일에 DROP TABLE portfolio_tags 포함 확인)
- AC-4: portfolio tag_ids 비활성 태그 archived 표시 반환, 자동 제거 없음 → `test_v1_assets_inactive_tag_archived_true`
- REQ-006:
- AC-1: 소문자 정규화된 DISTINCT tag_value 수만큼 tags 행 생성, is_admin_tag=FALSE, created_by_user_id=지정 admin ID → `test_migration_ac1_tags_created_with_correct_attrs`
- AC-2: "TestTag"/"testtag" 중복 → tags에 "testtag" 1행만 생성 (ON CONFLICT DO NOTHING) → `test_migration_ac2_case_insensitive_dedup`
- AC-3: 각 portfolio.tag_ids에 기존 portfolio_tags 연결 tag_value의 tag_id 모두 포함 → `test_migration_ac3_portfolio_tag_ids_populated`
- AC-4: 마이그레이션 후 portfolio_tags 테이블 부재 → `test_migration_ac4_portfolio_tags_dropped`
- AC-5: 유효하지 않은 admin ID 지정 시 데이터 무변경 오류 중단 → `test_migration_ac5_invalid_admin_id_aborts`
- AC-6: DISTINCT tag_value 수 N == 마이그레이션 후 tags 행 수 N → `test_migration_ac6_distinct_tag_count_matches`
## Evidence
- Test:
- pytest 실행 결과: **206 passed, 0 failed** (2026-05-19)
- 신규 테스트: `tests/test_tags_master.py` 26개
- `test_tag_domain_model_fields`
- `test_create_tag_with_admin_flag_true`
- `test_create_tag_with_admin_flag_false`
- `test_create_tag_duplicate_name_raises`
- `test_deactivate_tag_sets_is_active_false`
- `test_deactivate_tag_does_not_delete_row`
- `test_get_inactive_tag_returns_row`
- `test_list_tags_master_active_only_by_default`
- `test_list_tags_master_include_inactive`
- `test_portfolio_has_tag_ids_field`
- `test_portfolio_tag_ids_defaults_empty`
- `test_asset_item_tag_ids_field_exists`
- `test_v1_assets_active_tag_not_archived`
- `test_v1_assets_inactive_tag_archived_true`
- `test_v1_assets_empty_tag_ids_returns_none`
- `test_migration_ac1_tags_created_with_correct_attrs`
- `test_migration_ac2_case_insensitive_dedup`
- `test_migration_ac3_portfolio_tag_ids_populated`
- `test_migration_ac4_portfolio_tags_dropped`
- `test_migration_ac5_invalid_admin_id_aborts`
- `test_migration_ac6_distinct_tag_count_matches`
- `test_migration_0019_tags_table_exists`
- `test_migration_0019_creates_tags_table`
- `test_migration_0020_portfolios_tag_ids_column`
- `test_migration_0021_data_migration_exists`
- `test_migration_0021_has_admin_verification`
- 기존 regression 전체 통과: test_cli.py(35), test_v1.py(26), test_asset_catalog.py(35), test_portfolio_tags.py(4), test_portfolio_uuid.py(14), test_daemon_assignment.py(8)
- Manual (pre-release 수동 검증 항목 — live DB 마이그레이션 실행 후):
- tags 테이블 스키마 확인 (REQ-001 AC-1 live 검증)
- portfolios.tag_ids 컬럼 타입 UUID[] NOT NULL DEFAULT '{}' 확인 (REQ-002/003 AC-1 live 검증)
- idx_portfolios_tag_ids_gin 인덱스 존재 확인 (REQ-002/003 AC-2 live 검증)
- portfolio_tags 테이블 부재 확인 (REQ-003 AC-3 / REQ-006 AC-4 live 검증)
- MIGRATION_ADMIN_ID를 운영 환경 유효 user ID로 변경 필요 (revision 0021)
- Logs: N/A
## Result
- Ready for Release: true
- Blocking Issues: 없음
- Pre-release 수동 검증 항목: live DB 마이그레이션 실행 후 스키마/인덱스 상태 SQL 확인 (상기 Manual 항목 참조)
+2
View File
@@ -9,3 +9,5 @@
| BAT ID | Profile | Status | Included REQ | Discovery | Folder |
| --- | --- | --- | --- | --- | --- |
| 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/) |