feat: apply rel-001 feedback cleanup

This commit is contained in:
2026-06-05 13:57:46 +09:00
parent 84de34c301
commit 2470c0a00c
28 changed files with 992 additions and 32 deletions
@@ -0,0 +1,32 @@
# BAT-015 Design
- Status: confirmed
- Batch: BAT-015
## Architecture Summary
- REL-001 잔여 정합화 3건을 단일 batch로 묶어 처리한다. DB schema 마이그레이션 1건(REQ-033), Pydantic 스키마 강화 1건(REQ-034), Health 응답 + baseline 문서 갱신 1건(REQ-035). 변경 면적은 좁지만 cross-cutting nature(데이터 + 인증 + 운영)로 한 batch에 응집된다.
## Changed Areas
- Docs: `docs/data-model.md`(ledger_trades 컬럼), `docs/interface-contract.md`(/health 응답), `docs/runtime-flows.md`(health probe 의미 명문화 — baseline 갱신)
- Modules: `app/repositories/postgres.py`(ledger_trades SQL), `app/domain/models.py`(LedgerTrade dataclass, 있다면), `app/api/v1_portfolios.py`(ledger handler 응답 매핑, 있다면), `app/schemas/auth.py`(LoginRequest model_config), `app/api/health.py`(HealthResponse + handler), `migrations/versions/`(신규 revision)
- Interfaces: `POST /v1/portfolios/{id}/ledger/trades` 응답 schema 일관성, `POST /auth/login` 422 거부 동작, `GET /health` 응답 확장
## Key Decisions
1. **REQ-033 마이그레이션 전략**: 단일 `ALTER TABLE ledger_trades RENAME COLUMN asset_id TO portfolio_id` 트랜잭션 사용. 별도 컬럼 추가 + backfill + drop 패턴은 채택하지 않음(데이터 의미가 이미 portfolio_id로 일관되어 backfill 불요, dcy-005 A-1 가정 + 사용자 confirm).
2. **REQ-034 enforcement 위치**: `LoginRequest.model_config = ConfigDict(extra="forbid")` 스키마 단계에서 처리. `AuthService.login`에 별도 검증 로직 추가 안 함(스키마 단계에서 Pydantic 표준 422 응답이 더 명확).
3. **REQ-035 db_enabled 의미**: `settings.db_enabled`(환경변수) 값 노출. actual DB ping은 본 batch 범위 밖이며 `docs/runtime-flows.md`에 의미 명문화로 해소.
4. **baseline 갱신 위치**: `docs/runtime-flows.md`의 "Health Probe Flow"(또는 유사 섹션)에 `db_enabled` 의미 노트 1단락 추가. `docs/project-structure.md`는 영향 없음.
5. **마이그레이션 revision 번호**: 기존 head(`20260521_0023`) 다음으로 `20260601_NNNN_ledger_trades_portfolio_id_rename.py`. revision 본문은 `op.execute("ALTER TABLE ... RENAME COLUMN ...")` 또는 `op.alter_column(...)` 패턴.
## Edge Cases
- REQ-034: 기존 `LoginRequest`에 정의된 필드만 보내는 client는 영향 없음. `username`/`password` 외 어떤 키든 422로 거부됨.
- REQ-035: `PORTFOLIOHUB_DB_ENABLED` 환경변수가 미설정(default `false`)인 경우 `db_enabled: false` 응답. 본 워크스페이스 `.env`는 `true`로 설정되어 있음.
- REQ-033: `ledger_trades`에 데이터가 없는 환경(개발 초기)에서도 ALTER COLUMN RENAME은 즉시 완료. 대량 데이터 환경 staging은 dry-run에서 측정.
- 3건 통합: 동일 release에 묶일 예정이지만 각 변경이 서로 의존하지 않으므로 implementation 순서 자유. planning에서 권장 순서(REQ-034 → REQ-035 → REQ-033) 명시.
## Architecture Impact
- runtime-flows
## Reference Doc Update Plan
- project-structure.md: none (구조 변경 없음, 기존 모듈에 한정된 변경)
- runtime-flows.md: REQ-035에서 `/health` probe flow 섹션에 "`db_enabled`는 환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값 노출이며 actual DB connectivity ping이 아니다. production-grade readiness probe는 별도 `/readiness` endpoint 분리로 후속 처리"의 의미 명문화 노트 1단락 추가
@@ -0,0 +1,53 @@
# BAT-015 Implementation
- Status: completed
- Batch: BAT-015
## Plan Summary
- 변경 순서: REQ-034 → REQ-035 → REQ-033 (planning 권장 순서대로 진행)
- 각 REQ 구현 후 부분 회귀 (pytest 전체 + 해당 영역 smoke)
- 최종 batch 통합 회귀 + 로컬 smoke
## Changed Files
- REQ-034:
- `app/schemas/auth.py` — `LoginRequest.model_config = ConfigDict(extra="forbid")` 추가
- `tests/test_auth.py` — `test_login_rejects_no_expiry_extra_field`, `test_login_rejects_arbitrary_extra_field` 신규
- REQ-035:
- `app/api/health.py` — `HealthResponse.db_enabled` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 노출
- `tests/test_app.py` — `test_healthcheck`에 db_enabled 확인 추가, `test_health_exposes_db_enabled_true`, `test_health_exposes_db_enabled_false` 신규
- `docs/runtime-flows.md` — health endpoint 섹션(line 97)에 `db_enabled` 의미 명문화 노트 추가
- `docs/interface-contract.md` — `/health` payload schema에 `db_enabled` 필드 추가 + 의미 노트
- REQ-033:
- `migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py` — 신규 alembic revision (ALTER COLUMN RENAME with has_table guard)
- `app/domain/models.py` — `LedgerTrade.asset_id` → `portfolio_id`
- `app/repositories/portfolio_repository.py` — `insert_ledger_trade` interface 시그니처 갱신
- `app/repositories/postgres.py` — `insert_ledger_trade` INSERT/SELECT/RETURNING SQL 컬럼명 + `_row_to_ledger_trade` 매핑
- `app/schemas/v1.py` — `LedgerTradeResponse.asset_id` → `portfolio_id`
- `app/api/v1_portfolios.py` — ledger handler에서 `insert_ledger_trade(portfolio_id=...)` + 응답 매핑
- `tests/test_v1_portfolios_ledger.py` — `LedgerTrade(portfolio_id=...)` + 응답 `body["portfolio_id"]` 검증
- `tests/test_v1.py` — `_make_ledger_trade` + `body["portfolio_id"]` 검증
- `tests/test_daemon_assignment.py` — mock `trade.portfolio_id` 갱신
- `docs/data-model.md` — LedgerTrade entity, Key Fields, Uniqueness Constraints, State Rules의 `asset_id` 표기 모두 `portfolio_id`로 갱신 + revision 20260601_0024 명시
- `docs/interface-contract.md` — `POST /v1/portfolios/{id}/ledger/trades` 응답 schema에 `portfolio_id` 명시 + REQ-033 cross-reference
## Execution Log
1. REQ-034: `LoginRequest`에 `extra="forbid"` 적용 → `tests/test_auth.py` 16 passed
2. REQ-035: `HealthResponse` 확장 + 핸들러 → `tests/test_app.py` 9 passed → baseline 갱신
3. REQ-033: alembic revision 20260601_0024 작성 → `alembic upgrade head` 적용(local DB에 `ledger_trades` 테이블 부재로 `has_table` guard에 의해 안전 skip) → repository/도메인/스키마/라우트 정합 → `tests/test_v1_portfolios_ledger.py` 10 passed
4. 통합 회귀 1차: 5 failed 발견 (test_v1.py + test_daemon_assignment.py 잔존 `asset_id` mock) → 갱신 → 통합 회귀 2차: **372 passed, 0 failed**
5. 로컬 smoke:
- GET /health → HTTP 200 + `db_enabled: true` (REQ-035 AC-1)
- POST /auth/login + `no_expiry: true` → HTTP 422 + `extra_forbidden` (REQ-034 AC-2)
- codebase grep `ledger_trades.*asset_id` → 0건 (REQ-033 AC-3)
- `docs/runtime-flows.md` baseline 노트 grep → 매치 (REQ-035 AC-4)
- `docs/interface-contract.md` /health payload grep → 매치 (REQ-035 AC-5)
## Validation
- pytest 전체: **372 passed, 0 failed** (BAT-015 base 368 → +4: test_auth 2건 + test_app 2건 신규)
- 로컬 smoke 5건 모두 통과
- AC 매핑은 verification.md에서 정리
- 실 DB AC (REQ-033 AC-1 `\d ledger_trades`, AC-4 staging dry-run lock 측정): 본 워크스페이스 `ledger_trades` 테이블 미생성 상태로 N/A. REL-002 staging 시점에 별도 수집.
## Remaining Risks
- R-실 DB ledger_trades 테이블 미생성: REQ-033 마이그레이션은 `has_table` guard로 안전하지만, 본 워크스페이스에서 ledger_trades 적재 흐름이 활용되지 않는 상태. BAT-011 release-candidate verification은 mock 기반이라 이 gap이 release 단계에서 발견되지 않았음. **본 batch 범위 밖**, 별도 후속 항목으로 등록 후보(REL-002 또는 후속 Discovery).
- (workspace_purpose 메모리: 본 워크스페이스 개발용 → prod 영향 검증 N/A)
@@ -0,0 +1,34 @@
# BAT-015: REL-001 Feedback 정합화 (ledger 컬럼명 / LoginRequest 엄격화 / health db_enabled)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-005
- Included REQ: REQ-033, REQ-034, REQ-035
- Excluded REQ: 없음
## Summary
- dcy-005(rel-001-feedback-cleanup) 산출물 3건을 단일 batch로 묶어 일괄 정합화한다. 3건 모두 변경 면적이 좁고 같은 release(REL-002 분리 예정) target으로 일관된 정합화 묶음 trigger를 가진다.
## Scope
- In Scope:
- REQ-033: `ledger_trades.asset_id` → `portfolio_id` 컬럼명 정합화 (Alembic 신규 revision + postgres.py SQL 갱신 + 도메인 모델/라우트 정합)
- REQ-034: `LoginRequest`에 `model_config = ConfigDict(extra="forbid")` 적용 + pytest 422 회귀
- REQ-035: `HealthResponse`에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러 갱신 + `docs/runtime-flows.md` baseline 명문화 + `docs/interface-contract.md` /health 응답 schema 갱신
- Out of Scope:
- `ledger_trades` 의미 재정의(asset 단위 vs portfolio 단위 — REQ-033 portfolio_id 의미 확정 가정)
- session token TTL 정책 변경(REQ-021 본문 자체 갱신은 본 batch 범위 밖)
- `/readiness` endpoint 분리(production-grade readiness probe, 별도 후속 Discovery로 처리)
- REL-001 verification 재실행 (별도 REL-002로 분리 — dcy-005 OQ-3 결정)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- 공통 trigger: REL-001 Feedback Handoff 3건 정합화 묶음
- 공통 release: REL-002 분리 배포 예정(dcy-005 OQ-3 결정, 2026-06-01)
- REQ-033 마이그레이션 staging dry-run을 verification 단계에서 수행해 운영 lock 시간 < 1초(NFR-1) 측정
- REQ-035는 baseline 문서(`docs/runtime-flows.md`) 갱신 요구를 포함하므로 design 단계에서 architecture impact를 `runtime-flows`로 표기
- 본 워크스페이스는 개발용이므로 prod 운영 영향 검증은 release 단계에서 별도 수행 (memory: workspace_purpose)
@@ -0,0 +1,50 @@
# BAT-015 Planning
- Status: confirmed
- Batch: BAT-015
- Profile: standard
## Included REQ
- REQ-033 (Migration): ledger_trades.asset_id → portfolio_id 컬럼명 정합화
- REQ-034 (Interface): LoginRequest 추가 필드 거부 enforcement
- REQ-035 (Interface): GET /health 응답에 db_enabled 필드 추가
## Delivery Plan
- 순서: REQ-034 → REQ-035 → REQ-033
- REQ-034 우선: 1줄 schema 변경 + pytest 422 회귀 (저위험·빠른 evidence)
- REQ-035 후속: HealthResponse 확장 + baseline 문서 갱신 (저위험·문서 변경 포함)
- REQ-033 마지막: Migration 동반 (가장 큰 변경, alembic revision + DB schema, staging dry-run 필요)
- 선행 조건:
- dcy-005 confirmed (2026-06-01) ✓
- REQ-033/034/035 Approved (2026-06-01) ✓
- 본 워크스페이스 개발용 메모리 확인 — prod 영향 검증은 release 단계 ✓
- batch 통합 회귀: 3건 변경 후 전체 pytest 회귀 + 로컬 smoke (`/health`, `/auth/login`, `/v1/portfolios/{id}/ledger/trades`)
## Dependencies
- Internal:
- dcy-005: Discovery 본문에 OQ-1/OQ-2/OQ-3 결정 명문화 완료
- REQ-021: 본 batch가 REQ-021 AC-5 follow-up을 해소(REQ-034). REQ-021 본문 변경은 본 batch 범위 밖이며 cross-reference만 추가.
- REQ-029: 본 batch가 REQ-029 Notes follow-up을 해소(REQ-033). REQ-029 본문 변경은 본 batch 범위 밖.
- REL-001: 별도 release로 분리 배포 진행 중. 본 batch는 REL-002로 분리.
- External:
- 없음 (외부 client/integration 의존 없음)
## Design Gate
- Design Required Before Implementation: yes
- Reason: REQ-033이 DB schema 마이그레이션을 동반하고 REQ-035가 baseline 문서(`docs/runtime-flows.md`) 갱신을 요구한다. Architecture impact가 있으므로 design 단계에서 architecture 결정과 baseline 갱신 계획을 명문화한 뒤 implementation 진입.
## Milestones
1. Design 단계: REQ-033 마이그레이션 전략 결정(ALTER COLUMN RENAME vs 별도 컬럼 추가+backfill), REQ-035 baseline 문서 갱신 위치 결정
2. Implementation 단계: REQ-034 → REQ-035 → REQ-033 순서로 구현
3. Verification 단계: pytest 회귀(전체 369+ passed 기대) + REQ-033 staging dry-run + 로컬 smoke (`/health` 응답, `/auth/login` 422, `/v1/portfolios/{id}/ledger/trades` 회귀)
4. confirm-batch-verification 후 confirm-req-implemented 3건 일괄 전환
## Risks
- R-1 (Low): REQ-033 ALTER COLUMN RENAME이 ledger_trades 테이블에 lock을 유발해 운영 영향 발생.
- 대응: staging dry-run으로 lock 시간 측정(< 1초 권장), 짧은 단일 트랜잭션 패턴 적용. 본 워크스페이스는 개발용이라 prod 영향 검증은 release 단계.
- R-2 (Low): REQ-034 `extra="forbid"`가 기존 ignored field를 보내던 외부 client를 silently 깨뜨림.
- 대응: 본 워크스페이스 개발용이라 외부 monitoring 영향 없음. 회귀 테스트에서 기존 username/password만 보내는 케이스 통과 확인.
- R-3 (Low): REQ-035 `HealthResponse` 확장이 strict JSON schema validation 사용 monitoring 도구를 깨뜨림.
- 대응: 본 워크스페이스 개발용이라 영향 없음. prod 배포 시 사전 점검(REL-002 verification).
- R-4 (Low): 3건 변경이 통합 회귀에서 예상치 못한 상호 영향 발생.
- 대응: 각 REQ 구현 후 pytest 회귀 incremental 실행, batch 마지막에 전체 회귀.
@@ -0,0 +1,60 @@
# BAT-015 Verification
- Status: confirmed
- Batch: BAT-015
## Acceptance Mapping
### REQ-033: ledger_trades.asset_id → portfolio_id 컬럼명 정합화
- AC-1 (`\d ledger_trades`에 `portfolio_id` 존재, `asset_id` 부재): **Deferred to REL-002 staging** — 로컬 DB에 `ledger_trades` 테이블이 미생성 상태(BAT-011 release-candidate는 mock 기반 verification으로 진행됨). 마이그레이션 `20260601_0024`는 `has_table` 가드로 안전 skip되며, 코드 작성은 완료([migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py:36-40](../../../migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py#L36-L40)). 실 DB 검증은 REL-002 staging에서 수행.
- AC-2 (INSERT/SELECT 회귀): `tests/test_v1_portfolios_ledger.py` 10 PASSED (mock 기반). `insert_ledger_trade(portfolio_id=...)` 시그니처 + 응답 schema `LedgerTradeResponse.portfolio_id` 일관 검증.
- AC-3 (codebase grep `ledger_trades\..*asset_id` 0건): PASSED. `grep -rn "ledger_trades\..*asset_id\|asset_id.*ledger_trades\|insert_ledger_trade.*asset_id\|trade\.asset_id" app/` → 0건 (2026-06-05 implementation smoke).
- AC-4 (마이그레이션 lock < 1초, NFR-1): **Deferred to REL-002 staging** — workspace_purpose 메모리 정합(개발용 워크스페이스). staging dry-run으로 측정.
- AC-5 (전체 pytest 회귀 통과): PASSED. **372 passed, 0 failed** (5월 21일 base 368 → +4 누적: test_auth +2, test_app +2).
### REQ-034: LoginRequest 추가 필드 거부 enforcement
- AC-1 (정상 username/password → HTTP 200): `tests/test_auth.py` 기존 회귀 16 PASSED (정상 login flow는 기존 케이스로 회귀 보호).
- AC-2 (`no_expiry: true` → HTTP 422 + `extra_forbidden`): `tests/test_auth.py::test_login_rejects_no_expiry_extra_field` PASSED. 로컬 smoke 응답 `{"detail":[{"type":"extra_forbidden","loc":["body","no_expiry"],"msg":"Extra inputs are not permitted","input":true}]}` 확인 (2026-06-05).
- AC-3 (임의 추가 필드 → HTTP 422): `tests/test_auth.py::test_login_rejects_arbitrary_extra_field` PASSED.
- AC-4 (grep `extra.*forbid` app/schemas/auth.py 매치): PASSED. [app/schemas/auth.py:14](../../../app/schemas/auth.py#L14) `model_config = ConfigDict(extra="forbid")` 매치.
- AC-5 (전체 pytest 회귀 통과): PASSED. 372 passed, 0 failed.
### REQ-035: GET /health 응답에 db_enabled 필드 추가
- AC-1 (PORTFOLIOHUB_DB_ENABLED=true → `db_enabled: true`): `tests/test_app.py::test_health_exposes_db_enabled_true` PASSED. 로컬 smoke 응답 `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-06-05T13:44:04.724864+09:00","db_enabled":true}` 확인 (2026-06-05).
- AC-2 (PORTFOLIOHUB_DB_ENABLED=false → `db_enabled: false`): `tests/test_app.py::test_health_exposes_db_enabled_false` PASSED.
- AC-3 (5필드 노출 + 기존 4필드 회귀): `tests/test_app.py::test_healthcheck` 갱신 PASSED (`db_enabled` 추가 검증 포함). 기존 `status/service/version/checked_at` 동작 회귀 유지.
- AC-4 (baseline grep `db_enabled.*설정값|setting value|ping|readiness` docs/runtime-flows.md): PASSED. [docs/runtime-flows.md:97](../../../docs/runtime-flows.md#L97)에 의미 명문화 노트 1단락 추가.
- AC-5 (interface-contract grep `/health.*db_enabled` docs/interface-contract.md): PASSED. [docs/interface-contract.md:48](../../../docs/interface-contract.md#L48) `GET /health payload` schema에 `db_enabled` 필드 추가.
## Evidence
- Test:
- 전체 pytest: **372 passed, 0 failed, 3 warnings** (2026-06-05, deprecation warnings는 starlette HTTP_422_UNPROCESSABLE_ENTITY alias, 기능 무관)
- 신규 회귀 4건: `test_login_rejects_no_expiry_extra_field`, `test_login_rejects_arbitrary_extra_field`, `test_health_exposes_db_enabled_true`, `test_health_exposes_db_enabled_false`
- 영향 받은 기존 회귀 갱신: `test_v1_portfolios_ledger.py` (portfolio_id 응답 필드), `test_v1.py::test_v1_ledger_trade_*` (`_make_ledger_trade` + body 키), `test_daemon_assignment.py::test_ledger_trade_201_when_daemon_matches` (mock attribute), `test_app.py::test_healthcheck` (`db_enabled` 필드 확인 추가)
- Manual:
- 2026-06-05 13:44 KST 로컬 smoke 5건 (implementation.md Execution Log step 5 참조):
- GET /health → 200 + `db_enabled: true`
- POST /auth/login + `no_expiry: true` → 422 + extra_forbidden
- codebase grep ledger_trades.asset_id → 0건
- docs/runtime-flows.md db_enabled 의미 노트 grep → 매치
- docs/interface-contract.md /health payload `db_enabled` grep → 매치
- Logs:
- alembic upgrade head (2026-06-05): `Running upgrade 20260521_0023 -> 20260601_0024, Rename ledger_trades.asset_id to portfolio_id for column-name semantic alignment (REQ-033)` — `has_table` guard로 안전 skip 확인
## Architecture Impact Synchronization (Baseline)
- design.md Architecture Impact: `runtime-flows`
- Reference Doc Update Plan 이행:
- `docs/project-structure.md`: 갱신 불요 (영향 없음, 기존 모듈 한정 변경) — 변경 없음 확인 ✓
- `docs/runtime-flows.md`: `db_enabled` 의미 명문화 노트 [line 97](../../../docs/runtime-flows.md#L97) 추가 ✓
- 추가 baseline 갱신: `docs/data-model.md` LedgerTrade entity 컬럼명/제약 갱신, `docs/interface-contract.md` /health payload + LedgerTradeResponse schema 갱신
## Result
- Ready for Release: true
- Blocking Issues:
- 없음 (Deferred 2건은 blocker 아님, REL-002 staging에 이연 evidence)
- Deferred Evidence (release execution 시 수집):
- REQ-033 AC-1 (`\d ledger_trades` 실 DB 컬럼 검증) — REL-002 staging
- REQ-033 AC-4 (마이그레이션 lock < 1초 staging dry-run) — REL-002 staging
- 본 워크스페이스 ledger_trades 테이블 미생성 상태 자체는 본 batch 범위 밖 (BAT-011 mock 기반 verification의 후속 항목 후보)
+1
View File
@@ -22,3 +22,4 @@
| 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/) |
| BAT-013 | batch-lite | release-candidate | REQ-005 | dcy-002 | [bat-013](./bat-013_20260521_asset-tag-apply-remove-fix/) |
| BAT-014 | batch-lite | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 | dcy-003 | [bat-014](./bat-014_20260524_portfolioitem-symbol-response-contract/) |
| BAT-015 | standard | release-candidate | REQ-033, REQ-034, REQ-035 | dcy-005 | [bat-015](./bat-015_20260601_rel-001-feedback-cleanup/) |