fix(portfolio): name 검증에 한글+공백 허용 (minor-change, CHG-20260627-03)
_PORTFOLIO_NAME_RE + CLI _NAME_RE를 ^[a-zA-Z0-9가-힣 ._:-]{1,100}$로 완화. name은 (owner,name) 조회 키일 뿐 wire 식별자 아님. 475 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 참조 갱신.
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user