feat(REQ-014): add portfolio-create-user-required requirement and implementation

- Add docs/srs/Interface/req-014_portfolio-create-user-required.md with acceptance criteria
- Create docs/batches/bat-004_20260502_portfolio-create-user-required/ delivery batch
- Enhance app/cli/commands/portfolio.py: validate user_id parameter before portfolio creation
- Add comprehensive CLI tests in tests/test_cli.py for portfolio create validation flows
- Update docs/batches/index.md and docs/srs/index.md with new requirement/batch references
- Update README.md documentation
This commit is contained in:
2026-05-02 13:45:46 +00:00
parent 2f686f79ce
commit a111fdd83f
10 changed files with 368 additions and 16 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ pfhub service-token issue permanent --user alice --no-expiry
pfhub service-token revoke nightly --force
pfhub portfolio list --user <username>
pfhub portfolio status 005930:tv --recent 10
pfhub portfolio create 005930:tv --symbol 005930 --cash-balance 10000000 --force
pfhub portfolio create 005930:tv --symbol 005930 --user <username> --cash-balance 10000000 --force
pfhub portfolio buy 005930:tv --quantity 10 --price 71000 --fee 5000 --force
pfhub portfolio sell 005930:tv --quantity 5 --price 72000 --tax 3000 --force
pfhub portfolio invest 005930:tv --type deposit --amount 50000 --force
+13 -4
View File
@@ -119,6 +119,7 @@ def portfolio_status(
def create_portfolio(
ctx: typer.Context,
portfolio_id: str = typer.Argument(..., help="Portfolio identifier."),
user: str | None = typer.Option(None, "--user", help="Owner username (required)."),
name: str | None = typer.Option(None, help="Asset display name."),
symbol: str = typer.Option(..., help="Trading symbol."),
broker: str = typer.Option("kis", help="Broker code."),
@@ -131,10 +132,16 @@ def create_portfolio(
strategy_params: str = typer.Option("{}", help="Strategy params as JSON object."),
force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."),
) -> None:
if user is None:
ctx.obj.console.print("Error: --user 옵션은 필수입니다.")
raise typer.Exit(code=1)
_confirm(force, f"Create portfolio {portfolio_id} for symbol {symbol}?")
detail = _with_service(
ctx,
lambda service, app_context: service.create_portfolio(
def _callback(service, repository, app_context):
owner = repository.get_user_by_username(user)
if owner is None:
raise ValueError(f"사용자를 찾을 수 없습니다: {user}")
return service.create_portfolio(
CreatePortfolioRequest(
portfolio_id=portfolio_id,
name=name,
@@ -147,9 +154,11 @@ def create_portfolio(
profit_buffer=profit_buffer,
strategy_name=strategy_name,
strategy_params=_parse_strategy_params(strategy_params),
)
),
owner_user_id=owner.id,
)
detail = _with_service_and_repo(ctx, _callback)
render_output(ctx.obj.console, ctx.obj.output_format, detail)
@@ -0,0 +1,59 @@
# BAT-004 Implementation
- Status: completed
- Batch: BAT-004
- Profile: batch-lite
## Plan Summary
- CLI `pfhub portfolio create`에 `--user <username>` 필수 옵션 추가
- BAT-003에서 도입한 `_with_service_and_repo` 헬퍼 재사용
- `repository.get_user_by_username(user)` 호출 → None이면 ValueError → exit code 1
- `service.create_portfolio(payload, owner_user_id=owner.id)` 실제 user UUID 전달
- README 예제 라인 갱신
- 기존 테스트 3개 수정 + 신규 3개 추가
## Changed Files
| File | Change |
|------|--------|
| `app/cli/commands/portfolio.py` | `create_portfolio` 함수에 `--user` 옵션 추가, `_with_service` → `_with_service_and_repo` 교체, username→UUID 조회 후 `owner_user_id` 전달 |
| `tests/test_cli.py` | `test_cli_create_buy_and_delete_flow`, `test_cli_write_command_prompts_before_create`, `test_cli_write_command_can_abort` 수정; AC 1~3 커버 신규 3개 케이스 추가 |
| `README.md` | `pfhub portfolio create` → `pfhub portfolio create ... --user <username>` 갱신 |
## Execution Log
### 구현 결정 사항
- `--user` 옵션: `typer.Option(None, "--user", ...)` + 수동 None 체크로 exit code 1 보장 (BAT-003과 동일 패턴)
- `_confirm` 호출 전에 None 체크를 배치하여 owner 미지정 시 확인 프롬프트 없이 즉시 종료
- callback 내 username → user_id 조회: `repository.get_user_by_username(user)` → None이면 `ValueError`
- `service.create_portfolio(payload, owner_user_id=owner.id)`: `portfolio_service.py` 시그니처 변경 불필요 (기존 키워드 인자 그대로 활용)
### 기존 테스트 수정
- `test_cli_create_buy_and_delete_flow`: alice user 생성 + `--user alice` 추가, `owner_user_id` 검증 BUY 전에 배치 (delete 후 `_portfolio`가 None이 되므로)
- `test_cli_write_command_prompts_before_create`: alice user 생성 + `--user alice` 추가
- `test_cli_write_command_can_abort`: alice user 생성 + `--user alice` 추가
## Validation
```
pytest tests/test_cli.py -k "test_cli_portfolio_create or test_cli_create or test_cli_write_command" -v
```
```
6 passed
```
```
pytest tests/test_cli.py -v
```
```
44 passed in 1.37s (전체 회귀, 기존 테스트 전원 통과)
```
## Remaining Risks
- 없음
@@ -0,0 +1,42 @@
# BAT-004 Index
- Status: release-candidate
- Batch: BAT-004
- Profile: batch-lite
- Created: 2026-05-02
## Included REQ
| REQ | 제목 | Type | Priority |
|-----|------|------|----------|
| REQ-014 | CLI `portfolio create --user` 필수화 및 소유자 설정 | Interface | Medium |
## Source Discovery
- minor-change fast path (Discovery 없음)
## Scope Summary
- CLI `pfhub portfolio create` 명령에 `--user <username>` 필수 옵션 추가
- `_with_service_and_repo` 헬퍼(REQ-013 도입) 재사용, `repository.get_user_by_username(user)` 호출
- `PortfolioService.create_portfolio(owner_user_id=owner.id, ...)` 실제 UUID 전달
- `--user` 미입력 시 exit code 1, 미존재 username 시 exit code 1
- README 예제 갱신
- DB 스키마 변경 없음, HTTP API 변경 없음
## Impacted Area
- Docs:
- `README.md` — `pfhub portfolio create` 예제 라인 `--user <username>` 추가
- Modules:
- `app/cli/commands/portfolio.py` — `create_portfolio` 명령 수정
- Tests:
- `tests/test_cli.py` — 기존 `test_cli_create_buy_and_delete_flow` 수정 + AC 1~4 커버 케이스 추가
## Stage Documents
| Stage | File | Status |
|-------|------|--------|
| Planning | [planning.md](./planning.md) | confirmed |
| Implementation | [implementation.md](./implementation.md) | completed |
| Verification | [verification.md](./verification.md) | verified |
@@ -0,0 +1,50 @@
# BAT-004 Planning
- Status: confirmed
- Batch: BAT-004
- Profile: batch-lite
## Included REQ
- REQ-014: CLI `portfolio create --user` 필수화 및 소유자 설정
## Out of Scope
- HTTP API `POST /portfolios` owner 설정 변경
- `owner_user_id` 기본값(`"system"`) 서비스 레이어 변경
## 구현 순서
### 1단계: CLI 명령 수정
- `app/cli/commands/portfolio.py` — `create_portfolio` 명령 수정
- `user: str | None = typer.Option(None, "--user", help="Owner username (required).")` 추가
- `_with_service_and_repo` 콜백으로 교체
- callback 내부:
- `user is None` → `console.print(...)` + `raise typer.Exit(code=1)`
- `repository.get_user_by_username(user)` → `None`이면 `raise ValueError(...)`
- `service.create_portfolio(payload, owner_user_id=owner.id)` 호출
### 2단계: README 수정
- `README.md` — `pfhub portfolio create` 예제 라인에 `--user alice` 추가
### 3단계: 테스트
- `tests/test_cli.py`
- `test_cli_create_buy_and_delete_flow`: `--user alice` 추가, repo에 alice 생성
- `test_cli_write_command_prompts_before_create`: 동일 수정
- `test_cli_write_command_can_abort`: 동일 수정
- `test_cli_portfolio_create_without_user_exits_error` (신규)
- `test_cli_portfolio_create_with_valid_user_sets_owner` (신규)
- `test_cli_portfolio_create_with_unknown_user_exits_error` (신규)
## Dependencies
- Internal: `_with_service_and_repo` (BAT-003 도입, 이미 존재)
- External: 없음
## Design Gate
- Design Required Before Implementation: no
- Reason: 기존 `_with_service_and_repo` 패턴 재사용. 구조/스키마 변경 없음.
@@ -0,0 +1,65 @@
# BAT-004 Verification
- Status: verified
- Batch: BAT-004
- Profile: batch-lite
## Architecture Impact Check
- Design Gate (from planning.md): no — DB 스키마 변경 없음, HTTP API 변경 없음
- Actual Architecture Impact: none confirmed — `app/cli/commands/portfolio.py` 내 기존 `_with_service_and_repo` 헬퍼 재사용, Protocol stub/구현체 변경 없음
- Baseline Document Sync Required: no
## Acceptance Mapping
### AC 1: `--user` 없이 실행 → exit code 1 + 오류 메시지
| 항목 | 내용 |
|------|------|
| Evidence | `test_cli_portfolio_create_without_user_exits_error` |
| Verification | `runner.invoke(app, ["portfolio", "create", "005930:tv", "--symbol", "005930", "--force"])` → `exit_code == 1`, `"--user" in result.stdout` |
| Result | ✅ PASSED |
### AC 2: `--user alice` → 생성된 포트폴리오의 `owner_user_id`가 alice UUID로 설정됨
| 항목 | 내용 |
|------|------|
| Evidence | `test_cli_portfolio_create_with_valid_user_sets_owner` |
| Verification | alice user 생성 → `--user alice --force` 실행 → `repo._portfolio.owner_user_id == user.id` 확인 |
| Result | ✅ PASSED |
### AC 3: 존재하지 않는 username → 오류 메시지 + exit code 1
| 항목 | 내용 |
|------|------|
| Evidence | `test_cli_portfolio_create_with_unknown_user_exits_error` |
| Verification | `--user nobody` → `exit_code == 1`, `"nobody" in result.stdout` |
| Result | ✅ PASSED |
### AC 4: in-memory 모드에서도 동일 동작
| 항목 | 내용 |
|------|------|
| Evidence | `test_cli_portfolio_create_with_valid_user_sets_owner` (FakeRepo 사용) |
| Verification | FakeRepo(in-memory) 환경에서 alice UUID가 `owner_user_id`로 설정됨 확인 |
| Result | ✅ PASSED |
### AC 5: 오류 형식이 기존 CLI와 일관성 유지
| 항목 | 내용 |
|------|------|
| Evidence | 코드 검토 — BAT-003 `list_portfolios` 구현과 동일한 패턴 사용 |
| Verification | `--user` 미입력 → `ctx.obj.console.print(...)` + `raise typer.Exit(code=1)`, 미등록 user → `ValueError` → `execute_command` 핸들러 동일 경로 |
| Result | ✅ 충족 |
## Test Execution
```
pytest tests/test_cli.py -v
44 passed in 1.37s
```
## Regression
- BAT-003 추가 테스트(portfolio list 관련 4개) 전원 PASSED — 회귀 없음
- 기존 `test_cli_create_buy_and_delete_flow`, `test_cli_write_command_prompts_before_create`, `test_cli_write_command_can_abort` 수정 후 전원 PASSED
+1
View File
@@ -11,3 +11,4 @@
| BAT-001 | standard | release-candidate | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
| BAT-002 | batch-lite | release-candidate | REQ-012 | minor-change | [bat-002_20260502_service-token-rotate-cli](./bat-002_20260502_service-token-rotate-cli/) |
| BAT-003 | batch-lite | release-candidate | REQ-013 | dcy-003 | [bat-003_20260502_portfolio-list-user-required](./bat-003_20260502_portfolio-list-user-required/) |
| BAT-004 | batch-lite | release-candidate | REQ-014 | minor-change | [bat-004_20260502_portfolio-create-user-required](./bat-004_20260502_portfolio-create-user-required/) |
@@ -0,0 +1,78 @@
# REQ-014: CLI `portfolio create --user` 필수화 및 소유자 설정
- Status: Implemented
- Type: Interface
- Priority: Medium
- Owner: repository-maintainers
## Intent
- CLI `pfhub portfolio create`는 현재 `--user` 옵션이 없어 `owner_user_id="system"` 고정값으로 포트폴리오가 생성된다.
- 생성 시 `--user <username>` 을 필수로 받아 실제 소유자의 `owner_user_id`(UUID)로 포트폴리오가 생성되도록 강제한다.
- REQ-013(`portfolio list --user` 필수화)과 대칭을 이루는 변경으로, 소유자 기반 포트폴리오 관리 정책을 CLI 전반에 일관되게 적용한다.
## Requirement
- CLI `pfhub portfolio create` 명령은 `--user <username>` 옵션을 필수로 받아야 한다.
- `--user` 없이 실행하면 오류 메시지를 출력하고 exit code 1로 종료해야 한다.
- `--user`로 전달된 username을 `repository.get_user_by_username(username)`으로 조회하여 `owner_user_id`(UUID)를 얻고, 이를 `PortfolioService.create_portfolio(owner_user_id=...)` 호출에 전달해야 한다.
- 존재하지 않는 username이 전달되면 기존 CLI 오류 출력 형식과 일관된 오류 메시지를 출력하고 비정상 종료해야 한다.
## Acceptance Criteria
1. Given `--user` 옵션 없이 `pfhub portfolio create <id> --symbol X --force`를 실행할 때, When 명령이 실행되면, Then 오류 메시지가 출력되고 exit code 1로 종료된다.
2. Given 유효한 username `alice`로 `pfhub portfolio create <id> --symbol X --user alice --force`를 실행할 때, When 명령이 실행되면, Then 생성된 포트폴리오의 `owner_user_id`가 alice의 UUID로 설정된다.
3. Given 존재하지 않는 username으로 실행할 때, When 명령이 실행되면, Then "사용자를 찾을 수 없습니다" 형태의 오류 메시지가 출력되고 exit code 1로 종료된다.
4. Given in-memory 모드에서 `--user alice`로 실행할 때, When 명령이 실행되면, Then DB 모드와 동일하게 alice의 UUID가 `owner_user_id`로 설정된다.
5. Given 오류 발생 시, When 오류 메시지가 출력될 때, Then 메시지 형식은 기존 CLI 오류 출력 형식(Rich Console)과 일관성을 유지한다.
## Impacted Area
- Docs:
- `README.md` — `pfhub portfolio create` 예제 라인에 `--user <username>` 옵션 추가
- Modules:
- `app/cli/commands/portfolio.py` — `create_portfolio` 명령 시그니처에 `--user` 필수 옵션 추가, `_with_service_and_repo` 콜백 사용, `owner_user_id` 전달
- Tests:
- `tests/test_cli.py` — 기존 `test_cli_create_buy_and_delete_flow` 수정 + AC 1~4 커버 케이스 추가
## Notes
- `PortfolioService.create_portfolio` 시그니처(`owner_user_id: str = "system"`)는 변경하지 않는다. CLI 호출부에서만 실제 UUID를 전달한다.
- REQ-013에서 도입한 `_with_service_and_repo` 헬퍼를 그대로 사용한다. 추가 헬퍼 불필요.
- minor-change fast path 적용: Discovery 생략, Approved 직행. 범위는 CLI 옵션 1개 추가이며 기존 서비스/repository 인터페이스 변경 없음.
- Source Discovery: 없음 (minor-change fast path)
## Change Log
### CHG-20260502-01
- Date: 2026-05-02
- Author: repository-maintainers
#### Change Summary
- REQ-014 최초 생성 (Approved 직행). minor-change fast path 적용. REQ-012, REQ-013 선례 동일 경로.
#### Intent
- `pfhub portfolio create`의 소유자 설정 누락 문제를 CLI 레벨에서 해결.
### CHG-20260502-02
- Date: 2026-05-02
- Author: repository-maintainers
#### Change Summary
- BAT-004 verification(44 passed) 기반으로 Approved → Implemented 전환.
#### Intent
- 구현 및 검증 증거 확보 완료 후 상태 동기화.
#### Acceptance Criteria Delta
- Added: AC 1~5 신규 작성
#### Impacted Area Delta
- Docs: README.md create 예제 갱신
- Modules: app/cli/commands/portfolio.py create_portfolio 수정
- Tests: tests/test_cli.py 케이스 추가/수정
#### Delivery Trace Delta
- Batches: none (batch 미생성)
- Releases: none
#### Revalidation Impact
- Reverification Needed: no
- Existing Implementation Invalidated: no
- Status Recommendation: keep-current
- Follow-up Action: draft-batch 진행
+4 -7
View File
@@ -74,7 +74,7 @@
## Next Requirement ID
- Current: `REQ-014`
- Current: `REQ-015`
- Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다.
## Requirement Register
@@ -94,15 +94,12 @@
| REQ-011 | trex-basic 계약 반영을 위한 baseline 문서 갱신 | Documentation | Approved | Medium | repository-maintainers | [REQ-011](./Documentation/req-011_baseline-docs-update-for-trex-basic-contract.md) |
| REQ-012 | service-token rotate CLI 명령 구현 | Interface | Approved | Medium | repository-maintainers | [REQ-012](./Interface/req-012_service-token-rotate-cli.md) |
| REQ-013 | CLI `portfolio list --user` 필수화 및 소유자 기반 조회 | Interface | Implemented | Medium | repository-maintainers | [REQ-013](./Interface/req-013_portfolio-list-user-required.md) |
| REQ-014 | CLI `portfolio create --user` 필수화 및 소유자 설정 | Interface | Implemented | Medium | repository-maintainers | [REQ-014](./Interface/req-014_portfolio-create-user-required.md) |
## Recent Change Log Summary
- 2026-05-02: REQ-014 Implemented 전환 — BAT-004 verification evidence 확인 (44 passed)
- 2026-05-02: REQ-014 Approved 직행 (minor-change) — CLI `portfolio create --user` 필수화 및 소유자 설정
- 2026-05-02: REQ-013 Implemented 전환 — BAT-003 verification evidence 확인 (41 passed)
- 2026-05-02: REQ-013 Approved 전환 — CLI `portfolio list --user` 필수화 및 소유자 기반 조회 (source Discovery: dcy-003)
- 2026-05-02: REQ-013 Proposed 생성 — CLI `portfolio list --user` 필수화 및 소유자 기반 조회 (source Discovery: dcy-003)
- 2026-05-02: REQ-012 Approved 직행 (minor-change) — service-token rotate CLI 명령 구현
- 2026-05-02: REQ-011 Approved 전환 — trex-basic 계약 반영을 위한 baseline 문서 갱신 (source Discovery: dcy-002)
- 2026-05-02: REQ-010 Approved 전환 — credential_payload Fernet 암호화 및 startup validation (source Discovery: dcy-002)
- 2026-05-02: REQ-009 Approved 전환 — v1 API DB 스키마 migration 추가 (source Discovery: dcy-002)
- 2026-05-02: REQ-008 Approved 전환 — POST /v1/assets/{asset_id}/ledger/trades 구현 (source Discovery: dcy-002)
- 2026-05-02: REQ-007 Approved 전환 — GET /v1/user_info 엔드포인트 구현 (source Discovery: dcy-002)
+51
View File
@@ -485,6 +485,7 @@ def test_cli_status_supports_json_output() -> None:
def test_cli_create_buy_and_delete_flow() -> None:
repo = FakeRepo()
user = repo.create_user("alice", permission="read")
runner = build_runner(repo)
create_result = runner.invoke(
@@ -495,6 +496,8 @@ def test_cli_create_buy_and_delete_flow() -> None:
"005930:tv",
"--symbol",
"005930",
"--user",
"alice",
"--cash-balance",
"1000000",
"--strategy-params",
@@ -518,6 +521,7 @@ def test_cli_create_buy_and_delete_flow() -> None:
],
)
assert repo.entries[0].action == "BUY"
assert repo._portfolio.owner_user_id == user.id
delete_result = runner.invoke(runner.app, ["portfolio", "delete", "005930:tv", "--keep-ledger", "--force"])
assert create_result.exit_code == 0
@@ -528,6 +532,7 @@ def test_cli_create_buy_and_delete_flow() -> None:
def test_cli_write_command_prompts_before_create() -> None:
repo = FakeRepo()
repo.create_user("alice", permission="read")
runner = build_runner(repo)
result = runner.invoke(
@@ -538,6 +543,8 @@ def test_cli_write_command_prompts_before_create() -> None:
"005930:tv",
"--symbol",
"005930",
"--user",
"alice",
"--cash-balance",
"1000000",
],
@@ -550,6 +557,7 @@ def test_cli_write_command_prompts_before_create() -> None:
def test_cli_write_command_can_abort() -> None:
repo = FakeRepo()
repo.create_user("alice", permission="read")
runner = build_runner(repo)
result = runner.invoke(
@@ -560,6 +568,8 @@ def test_cli_write_command_can_abort() -> None:
"005930:tv",
"--symbol",
"005930",
"--user",
"alice",
"--cash-balance",
"1000000",
],
@@ -570,6 +580,47 @@ def test_cli_write_command_can_abort() -> None:
assert repo._portfolio is None
def test_cli_portfolio_create_without_user_exits_error() -> None:
repo = FakeRepo()
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["portfolio", "create", "005930:tv", "--symbol", "005930", "--force"],
)
assert result.exit_code == 1
assert "--user" in result.stdout
def test_cli_portfolio_create_with_valid_user_sets_owner() -> None:
repo = FakeRepo()
user = repo.create_user("alice", permission="read")
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["portfolio", "create", "005930:tv", "--symbol", "005930", "--user", "alice", "--force"],
)
assert result.exit_code == 0
assert repo._portfolio is not None
assert repo._portfolio.owner_user_id == user.id
def test_cli_portfolio_create_with_unknown_user_exits_error() -> None:
repo = FakeRepo()
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["portfolio", "create", "005930:tv", "--symbol", "005930", "--user", "nobody", "--force"],
)
assert result.exit_code == 1
assert "nobody" in result.stdout
def test_cli_health_check_reports_repository() -> None:
repo = FakeRepo(_portfolio())
runner = build_runner(repo)