diff --git a/app/cli/commands/portfolio.py b/app/cli/commands/portfolio.py index bf69c2c..07e0a19 100644 --- a/app/cli/commands/portfolio.py +++ b/app/cli/commands/portfolio.py @@ -22,7 +22,7 @@ from app.schemas.portfolio import ( ) from app.services.portfolio_service import PortfolioService -_NAME_RE = re.compile(r"^[a-zA-Z0-9._:\-]{1,100}$") +_NAME_RE = re.compile(r"^[a-zA-Z0-9가-힣 ._:\-]{1,100}$") portfolio_app = typer.Typer(help="Portfolio lifecycle and trade commands.") comment_app = typer.Typer(help="Portfolio comment history commands.") @@ -72,7 +72,7 @@ def _confirm(force: bool, message: str) -> None: def _validate_portfolio_name(name: str, ctx_console=None) -> None: if not _NAME_RE.match(name): - msg = f"유효하지 않은 이름입니다: {name!r} — [a-zA-Z0-9._:-] 문자만 허용하며 1~100자여야 합니다." + msg = f"유효하지 않은 이름입니다: {name!r} — 한글·영문·숫자·공백·._:- 만 허용하며 1~100자여야 합니다." if ctx_console is not None: ctx_console.print(f"Error: {msg}") raise typer.Exit(code=1) diff --git a/app/schemas/portfolio.py b/app/schemas/portfolio.py index 9fdf46f..f04d15f 100644 --- a/app/schemas/portfolio.py +++ b/app/schemas/portfolio.py @@ -9,7 +9,7 @@ import re as _re from pydantic import BaseModel, ConfigDict, Field, field_validator -_PORTFOLIO_NAME_RE = _re.compile(r"^[a-zA-Z0-9._:\-]{1,100}$") +_PORTFOLIO_NAME_RE = _re.compile(r"^[a-zA-Z0-9가-힣 ._:\-]{1,100}$") class StrategySetResponse(BaseModel): @@ -90,7 +90,7 @@ class CreatePortfolioRequest(PortfolioBase): def validate_name(cls, v: str) -> str: if not _PORTFOLIO_NAME_RE.match(v): raise ValueError( - f"이름은 [a-zA-Z0-9._:-] 문자만 허용하며 1~100자여야 합니다: {v!r}" + f"이름은 한글·영문·숫자·공백·._:- 만 사용 가능하며 1~100자여야 합니다: {v!r}" ) return v diff --git a/docs/srs/Interface/req-025_v1-portfolios-create.md b/docs/srs/Interface/req-025_v1-portfolios-create.md index 5d99610..0ddafdf 100644 --- a/docs/srs/Interface/req-025_v1-portfolios-create.md +++ b/docs/srs/Interface/req-025_v1-portfolios-create.md @@ -56,6 +56,33 @@ ## Change Log +### CHG-20260627-03 +- Date: 2026-06-27 +- Author: portfolio-hub-dev + +#### Change Summary +- `_PORTFOLIO_NAME_RE` 정규식 완화: 한글(가-힣) + 공백 허용 추가. + +#### Intent +- 포트폴리오 name은 `(owner_user_id, name)` 조회 키로만 쓰이며 wire 식별자가 아니므로 UTF-8 한글이 안전하다. 사용자 가독성 향상을 위해 한글 이름과 공백을 허용한다. + +#### Acceptance Criteria Delta +- No change (기존 AC 그대로 유지) + +#### Impacted Area Delta +- `app/schemas/portfolio.py`: `_PORTFOLIO_NAME_RE` → `^[a-zA-Z0-9가-힣 ._:\-]{1,100}$`, `validate_name` 에러 메시지 갱신. +- `tests/test_portfolio_uuid.py`: 한글 name 통과(`test_name_validation_accepts_korean`), 기존 ASCII name 통과(`test_name_validation_accepts_ascii_unchanged`), 101자 초과 거부(`test_name_validation_rejects_over_100_chars`) 케이스 추가. + +#### Delivery Trace Delta +- Batches: minor-change (batch-lite 불필요 단일 저위험 변경) +- Releases: pending + +#### Revalidation Impact +- Reverification Needed: no (기존 AC 충족 범위 확장만이며 거부 규칙 약화 없음 — `!` 등 비허용 문자는 여전히 거부) +- Existing Implementation Invalidated: no +- Status Recommendation: keep-implemented +- Follow-up Action: 없음 + ### CHG-20260524-02 - Date: 2026-05-24 - Author: Hermes Agent diff --git a/docs/srs/index.md b/docs/srs/index.md index 2d4c40b..be8c1f9 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -129,6 +129,7 @@ ## Recent Change Log Summary +- 2026-06-27: REQ-025 name 검증 완화(CHG-20260627-03) — `_PORTFOLIO_NAME_RE` 한글(가-힣)+공백 허용(`^[a-zA-Z0-9가-힣 ._:\-]{1,100}$`). minor-change. 기존 AC 유지, 테스트 3개 추가. - 2026-06-27: REQ-044/045 `Approved` → `Implemented` — BAT-019 PR #16 main 머지(`7f15c6b`) QC PASS. 472 passed/0 failed. REQ-045 env토큰(user_id=None) 처리 Notes 정정: "HTTP 403" → "빈 목록 [] HTTP 200"(change-req CHG-20260627-02). - 2026-06-27: REQ-044/045 신규 (Proposed) — dcy-009 FR-1(GET /v1/assets require_read 완화)·FR-2(사용자 가용 데몬 목록 엔드포인트 신설) REQ 초안 작성. 엔드포인트 경로·스키마(OQ-2) 및 가용 데몬 의미(OQ-3)는 design Deferred. - 2026-06-26: REQ-041 WAC 공식 change-req(CHG-20260626-01) — 매수 수수료 cost basis 편입(사장님 결정). 신 공식: `running_wac = (running_wac*qty + price*qty + buy_fee) / new_total_qty`. `calculate_realized_pnl`과 `calculate_current_wac` 양 함수 적용. AC-9/AC-10 추가. 신규 테스트 9개 포함 456 passed / 0 failed. REQ-040/REQ-043 Notes 참조 갱신. diff --git a/tests/test_portfolio_uuid.py b/tests/test_portfolio_uuid.py index df614e9..f48c3e9 100644 --- a/tests/test_portfolio_uuid.py +++ b/tests/test_portfolio_uuid.py @@ -563,6 +563,48 @@ def test_alice_cannot_delete_bobs_portfolio() -> None: assert len(repo._portfolios) == 1, "Bob's portfolio must not be deleted" +# --------------------------------------------------------------------------- +# name 검증: 한글 허용 (change-req CHG-20260627-03 in REQ-025) +# --------------------------------------------------------------------------- + +def test_name_validation_accepts_korean() -> None: + """한글 이름이 포함된 포트폴리오를 허용한다.""" + repo = FakeRepo() + repo.create_user("alice", permission="read") + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + ["portfolio", "create", "삼성전자 포트폴리오", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"], + ) + + assert result.exit_code == 0, result.output + + +def test_name_validation_accepts_ascii_unchanged() -> None: + """기존 ASCII name은 완화 후에도 통과한다.""" + repo = FakeRepo() + repo.create_user("alice", permission="read") + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + ["portfolio", "create", "my-portfolio_v1:test.01", "--asset-id", _TEST_ASSET_ID, "--user", "alice", "--force"], + ) + + assert result.exit_code == 0, result.output + + +def test_name_validation_rejects_over_100_chars() -> None: + """101자 초과 이름은 거부한다.""" + from app.schemas.portfolio import CreatePortfolioRequest + import pytest + + long_name = "가" * 101 + with pytest.raises(Exception): + CreatePortfolioRequest(name=long_name, asset_id="some-uuid") + + def test_update_new_name_renames_portfolio() -> None: repo = FakeRepo() repo.create_user("alice", permission="read")