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:
2026-06-27 12:03:33 +09:00
co-authored by Claude Opus 4.8
parent 853a9bdab8
commit 233cbc3df6
5 changed files with 74 additions and 4 deletions
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
+1
View File
@@ -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 참조 갱신.
+42
View File
@@ -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")