# 0. 문서 상태 이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다. - 상태: confirmed - 문서 ID: dcy-001_20260519_asset-catalog-and-user-portfolio-model - 이슈명: 자산 관리 구조 재설계 — admin 관리 asset catalog와 user portfolio 분리 모델 도입 - 작성 시각(KST): 2026-05-19T00:00+09:00 - 마지막 갱신 시각(KST): 2026-05-20T12:00+09:00 (후속 Discovery 참조 추가 — dcy-003 옵션 B 결정으로 REQ-013 Deprecated 처리, 본 Discovery FR-5의 `GET /v1/assets` 통합 endpoint 결정은 dcy-003으로 회수됨) - 대체됨: 없음 - 후속 Discovery 참조: docs/discovery/dcy-003_20260520_user-portfolio-mutation-v1-api-migration.md (confirmed 2026-05-20 — path 명명 분리: `GET /v1/assets`는 admin catalog 전용으로 회수, `GET /v1/portfolios` 포지션 조회 신설. 본 Discovery FR-5의 `GET /v1/assets` 통합 endpoint 결정은 REQ-013 Deprecated로 처리 완료) - 생성된 REQ 참조: REQ-009, REQ-010, REQ-011, REQ-012, ~~REQ-013~~ (Deprecated 2026-05-20 — dcy-003 옵션 B 결정), REQ-014, REQ-015 # 1. 계획 상태 요약 현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다. - 연계 문서 현황: - 관련 점수 파일: 미생성 - 관련 TODO/메모: 없음 - 해석: - 현재 `portfolios` 테이블은 asset 정의(symbol, broker)와 user position(cash_balance, quantity)을 하나의 엔티티로 혼합하여 관리한다. 이로 인해 admin이 거래 가능한 종목을 중앙 관리하는 것이 불가능하고, user가 임의 symbol/broker 조합으로 portfolio를 생성할 수 있어 거버넌스 공백이 생긴다. - 이 Discovery는 portfolio-hub의 핵심 도메인 모델을 재설계하는 첫 번째 단계로, admin이 관리하는 asset catalog와 user가 구성하는 portfolio(포지션)를 명확히 분리하는 새로운 아키텍처를 정의한다. # 2. 요구사항 판정 결과 이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다. - 판정: 신규 - 근거 - 기존 SDLC 문서 전체(dcy-001~008, REQ-001~029)가 현재 단일 `portfolios` 모델 위에 정의되어 있었으며, 2026-05-19 아키텍처 전환 결정에 따라 폐기되었다. 새 모델은 기존 어떤 Discovery와도 중복되지 않는다. - asset catalog + user portfolio 분리 모델은 기존 SDLC에서 다룬 적 없는 신규 도메인 구조다. # 3. 문제점의 요약 문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다. - 현재 상태 - `portfolios` 테이블이 asset 정의(symbol, market/markets)와 user 포지션(broker, cash_balance, asset_quantity, strategy, tags, comments)을 하나의 엔티티로 관리한다. "portfolio를 만든다" = "거래 가능한 종목을 등록한다"와 동일한 행위다. - admin이 거래 가능한 종목 목록을 중앙 통제할 방법이 없다. user 또는 admin 누구나 임의 symbol/broker 조합으로 portfolio를 생성할 수 있다. - v1 API의 `asset_id`는 실제로는 `portfolio_id`(user 포지션 ID)로, "자산"과 "포지션"의 개념이 혼용된다. - daemon은 portfolio(포지션) 단위로 배정되어 있어 동일 종목을 여러 user가 운용할 때 daemon 관리 구조가 복잡해진다. - 기대 상태 - `assets` 테이블(admin 관리 catalog)에 거래 가능한 종목(symbol, market)이 등록되며, admin만 이 목록을 생성·수정·비활성화(deactivate)할 수 있다. broker는 catalog 속성이 아니다. - user는 `assets` catalog에서 종목을 선택하고 거래에 사용할 broker를 지정하여 자신의 `portfolio`(포지션)를 구성한다. 포지션에는 broker, cash_balance, asset_quantity, strategy, tags, comments, 거래 이력이 귀속된다. - v1 API의 `asset_id`는 catalog의 실제 asset을 참조하며 "자산"과 "포지션"의 개념이 명확히 분리된다. - daemon은 portfolio(포지션) 레벨로 배정된다. 하나의 daemon이 특정 user의 특정 portfolio들을 담당하며, 동일 종목이라도 user별로 별도 daemon 배정이 가능하다(`portfolios.managed_by_daemon_id` 유지). - 이해관계자 - admin: 거래 가능한 종목을 중앙에서 등록·관리하는 운영자 - user: catalog에서 종목을 선택해 자신의 포지션을 구성하는 투자자 - daemon(trex-basic 등): 배정된 자산의 거래 체결 내역을 기록하는 서비스 에이전트 - repository-maintainers: 새 아키텍처 기반으로 API/CLI/DB를 설계·구현하는 개발팀 - 제약사항 - 기존 `portfolios` 테이블 데이터(운영 중인 포지션)를 새 `assets` + `portfolios` 구조로 유실 없이 migration해야 한다. - Python/FastAPI/Typer/Postgres 기술 스택은 유지한다. - Alembic 기반 migration 절차는 유지한다. - dcy-002(태그 소유권 및 적용 대상 정책, confirmed)는 이 Discovery의 `assets`/`portfolios` 테이블 확정을 구현 선행 조건으로 한다. `assets.tag_ids UUID[]` 컬럼과 `portfolios.tag_ids UUID[]` 컬럼 추가는 dcy-002 구현 범위이며 이 Discovery에서 다루지 않는다. ## 주요 사용자 시나리오 사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다. - admin이 `AAPL (markets: ['NASDAQ'])`, `005930.KS (markets: ['KRX', 'NXT', 'SOR'])` 등을 asset catalog에 등록한다. `markets`가 빈 배열이면 현재 어떤 시장에서도 거래 불가 상태다. 등록되지 않은 종목은 user가 portfolio에 추가할 수 없다. - user가 catalog에 등록된 `005930.KS` 종목을 선택하고 사용할 broker(예: kiwoom)를 지정하여 자신의 portfolio(포지션)를 생성하고, cash_balance와 전략을 설정한다. - daemon이 `GET /v1/assets`를 호출하면 자신에게 `managed_by_daemon_id`로 배정된 portfolio 목록을 반환받고(각 항목의 `asset_id`는 catalog UUID 참조), 그 `asset_id`로 `POST /v1/assets/{asset_id}/ledger/trades`를 호출해 체결 내역을 기록한다. - admin이 catalog에서 특정 asset을 비활성화(`status='inactive'`)하면 해당 종목에 대한 신규 portfolio 생성은 차단되지만 기존 portfolio는 보존된다. - user는 동일 asset에 대해 전략 A, B로 각각 portfolio를 구성할 수 있다(OQ-2 결정: 복수 portfolio 허용). # 4. 이번에 정의할 변경 이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다. - `assets` 테이블(admin 관리 catalog) 신규 도입 — symbol, market, status 등 종목 정의 데이터를 admin이 관리하는 독립 엔티티로 분리한다. broker는 종목 자체의 속성이 아니므로 포함하지 않는다. - `portfolios` 테이블 재설계 — `asset_id FK → assets.asset_id`를 포함한 user 포지션 구조로 변경. symbol/market은 `assets`로 이전(market은 markets 배열로 집약)하고, broker는 portfolios에 유지하며, 나머지 포지션 데이터(cash_balance, quantity, strategy 등)는 그대로 남긴다. - 기존 `portfolios` 데이터 migration — 현재 portfolios 행을 `assets` catalog 항목과 `portfolios` 포지션 항목으로 분리 이전한다. ## 영향 범위 변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다. - 코드 영향: - `app/domain/models.py`: `Asset` 도메인 모델 신규 추가, `Portfolio` 모델에서 symbol/market 필드 제거 및 `asset_id` 참조로 대체. broker는 Portfolio 모델에 유지. - `app/repositories/postgres.py`: `assets` CRUD 메서드 신규 추가, `portfolios` 쿼리에서 `assets` JOIN 반영. - `app/repositories/portfolio_repository.py`: Protocol에 asset CRUD 선언 추가 - `app/api/v1_assets.py`: 토큰 타입(service/user/admin) 분기 로직 추가, `?user_id` 파라미터 추가(admin 전용), 응답 `asset_id`를 catalog UUID로 재정의 - `app/api/portfolios.py`: portfolio 생성 시 `asset_id` 지정 필수화. GET 목록 조회는 `GET /v1/assets`로 흡수되며 `GET /portfolios`는 점진적 deprecated 예정(mutation 경로는 별도 Discovery에서 v1으로 이전) - `app/services/portfolio_service.py`: create_portfolio에 `asset_id` 검증 추가. - `app/cli/commands/asset.py`: 신규 파일 — `pfhub asset create/list/update/deactivate` 커맨드 그룹 구현 (admin 전용) - `app/cli/main.py`: `app.add_typer(asset_app, name="asset")` 추가 - `app/cli/commands/portfolio.py`: portfolio create 시 `--asset-id` 또는 `--symbol --broker` 조합으로 catalog 참조 - `app/schemas/`: `AssetItem`, `CreatePortfolioRequest` 등 스키마 변경 - `migrations/versions/`: `assets` 테이블 생성 + `portfolios` 재설계(asset_id FK 추가, symbol/market 제거) + 데이터 이전 migration - ※ 태그 관련 변경(`Tag` 모델, `tags` 테이블, `asset_tags`/`portfolio_tags` 재설계, 태깅 검증 로직)은 모두 dcy-002 구현 범위 - 문서 영향: - `docs/data-model.md`: `Asset` 엔티티 추가, `Portfolio` 엔티티 재정의 - `docs/interface-contract.md`: v1 API asset catalog 기반 재정의 - `docs/runtime-flows.md`: admin asset 등록 → user portfolio 구성 → daemon 체결 기록 흐름 추가 - 운영 영향: - admin이 신규 종목을 portfolio에 추가하기 전 catalog에 먼저 등록해야 하는 운영 절차 추가 - 기존 운영 중인 portfolios에 대해 migration 전 admin 확인 절차 필요 # 5. 요구사항 목록 실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다. ## 기능 요구사항 - FR-1: `assets` 테이블 신규 도입 — 필드: `asset_id UUID PK`, `symbol TEXT NOT NULL UNIQUE`, `markets TEXT[] NOT NULL DEFAULT '{}'`, `display_name TEXT`, `status TEXT NOT NULL DEFAULT 'active'`, `created_at`, `updated_at`. admin만 CRUD 가능. `symbol` 단독 UNIQUE constraint 적용(`symbol`이 종목 식별자). `markets` 배열에 거래 가능한 시장 코드를 복수 개 저장하며, 빈 배열은 현재 어떤 시장에서도 거래 불가를 의미한다. `markets` 컬럼에 GIN 인덱스를 적용한다. 시장 코드 허용 값은 application-layer Python Enum으로 정의하며(예: `MarketCode.KRX`, `MarketCode.NXT`, `MarketCode.SOR`, `MarketCode.NASDAQ`, `MarketCode.NYSE`, `MarketCode.UPBIT`, `MarketCode.BITTHUMB`), API 수신 시 대문자 정규화 후 Enum 검증을 수행한다. DB CHECK constraint는 사용하지 않는다. broker는 종목 자체의 속성이 아니므로 포함하지 않는다. - FR-2: admin asset catalog CRUD 경로 제공 — admin CLI(`pfhub asset create/list/update/deactivate`) 및 HTTP API(`POST/GET/PATCH /v1/assets`, `PATCH /v1/assets/{asset_id}/deactivate`)를 통해 admin이 거래 가능한 종목을 등록·관리한다. asset 삭제는 hard DELETE 없이 `status='inactive'`로만 처리한다(soft delete). 비활성화된 asset을 참조하는 기존 portfolio는 그대로 보존되며 신규 portfolio 생성만 거부한다. - FR-3: `portfolios` 테이블 재설계 — `asset_id UUID NOT NULL` 컬럼 추가 및 `FOREIGN KEY (asset_id) REFERENCES assets(asset_id) ON DELETE RESTRICT`(DB-level FK constraint) 적용. `symbol`/`market` 컬럼 제거. `broker` 컬럼은 portfolios에 유지한다(user가 해당 포지션을 어느 증권사로 거래하는지는 포지션 레벨 속성). `(owner_user_id, asset_id)` unique constraint는 추가하지 않는다(동일 user가 동일 asset에 복수 portfolio 허용 — OQ-2 결정). `managed_by_daemon_id`는 기존과 동일하게 nullable soft reference로 유지하며, portfolio 생성 시 지정은 선택사항이고 NULL이면 fail-closed(ledger write, asset listing 불가)다. portfolio는 asset catalog에 등록된 종목만 참조할 수 있다. - FR-4: user portfolio 생성 시 catalog 검증 — `pfhub portfolio create` (CLI) 호출 시 지정한 `asset_id`가 catalog에 `status='active'`로 존재하는지 검증. 미등록 또는 inactive asset이면 거부한다. user portfolio CRUD HTTP API는 Out of Scope이며 별도 Discovery에서 동일한 catalog 검증 정책을 따른다. - FR-5: v1 API `GET /v1/assets` 재정의 — 토큰 타입에 따라 아래와 같이 동작하는 통합 portfolio 조회 엔드포인트로 재정의한다. 응답 항목의 `asset_id`는 catalog의 `assets.asset_id`(UUID)를 참조한다. - **service token (daemon)**: auth context에서 `daemon_id`를 자동 추출하여 `managed_by_daemon_id = auth.daemon_id` 필터링. `daemon_id`가 없으면 HTTP 403. - **user token (로그인 사용자)**: `owner_user_id = auth.user_id` 필터링. 본인 portfolio만 반환. - **admin token**: 기본은 `owner_user_id = auth.user_id`(본인 portfolio). `?user_id=` 파라미터 지정 시 해당 user의 portfolio를 반환. - 모든 토큰 타입에서 `?market=` 파라미터를 지원하며, 지정 시 ` = ANY(assets.markets)`인 portfolio만 반환. daemon은 현재 시간 기준 활성 시장 코드를 전달하여 거래 대상 portfolio를 필터링한다. - FR-6: 기존 `portfolios` 데이터 migration — 현재 portfolios 행의 `symbol`을 기준으로 중복 없이 `assets` catalog 항목을 생성하고, 동일 symbol의 모든 `market` 값을 `ARRAY_AGG(DISTINCT market)`으로 집약하여 `markets` 배열에 저장한다. 각 portfolio 행에 `asset_id FK`를 연결한다. `managed_by_daemon_id`는 기존 값을 그대로 유지한다. ## 비기능 요구사항 - NFR-1: 기존 `portfolios` 데이터 migration은 트랜잭션 내에서 원자적으로 실행되며 실패 시 롤백 가능해야 한다. - NFR-2: 새 구조에서 portfolio 조회 시 `assets JOIN portfolios` 성능이 기존 단일 테이블 조회 대비 허용 범위 내여야 한다 (인덱스 설계 포함). - NFR-3: admin이 아닌 user 또는 daemon이 asset catalog를 직접 생성·수정·비활성화하려 하면 HTTP 403으로 거부한다. ## 범위 경계 이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다. - In Scope - `assets` 테이블 도입 및 admin CRUD 경로 (CLI + HTTP API) - `portfolios` 테이블 재설계 (`asset_id FK` 추가, symbol/market 제거, broker 유지) - 기존 portfolios 데이터 migration - v1 `GET /v1/assets`의 catalog 기반 재정의 - `docs/data-model.md`, `docs/interface-contract.md`, `docs/runtime-flows.md` 갱신 - Out of Scope - user portfolio **mutation** HTTP API(POST 생성, PATCH 수정, DELETE 삭제, buy/sell/invest 등) — 별도 Discovery에서 `/v1/` 경로로 이전 예정 - `/portfolios` GET 목록 조회의 v1 이전 이후 `GET /portfolios` 엔드포인트 제거 — 별도 Discovery에서 처리 - 태그 소유권 모델 및 태깅 정책 전체 — dcy-002(confirmed) 구현 범위 - `assets.tag_ids UUID[]` 컬럼 추가 — dcy-002 구현 범위 - `portfolios.tag_ids UUID[]` 컬럼 추가 및 `portfolio_tags` 테이블 재설계/제거 — dcy-002 구현 범위 - `tags` 마스터 테이블 생성, 태그 CRUD API(/v1/tags), pfhub tag CLI — dcy-002 구현 범위 # 6. 리스크/가정 목록 불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다. ## 리스크 - R-1 (High): 기존 `portfolios` 데이터 migration 중 동일 `symbol`을 가진 여러 portfolio 행이 서로 다른 `market` 값을 가질 수 있다. 이를 하나의 asset 행으로 병합하고 `markets` 배열로 집약하는 로직이 필요하며, 집약 규칙이 명확하지 않으면 market 누락이 발생할 수 있다. - 완화 방안: migration 전 `SELECT symbol, ARRAY_AGG(DISTINCT market) FROM portfolios GROUP BY symbol`로 symbol별 market 집합을 파악하고, `ARRAY_AGG(DISTINCT market)` 결과를 `assets.markets`에 그대로 저장한다. market 값이 NULL인 기존 행은 빈 배열로 처리한다. - R-2 (High): `portfolios` 테이블에서 symbol/market 컬럼 제거는 이 컬럼들을 직접 참조하는 모든 쿼리, API response, CLI 출력을 수정해야 한다. 누락된 참조가 있으면 런타임 오류가 발생한다. - 완화 방안: migration 전 `grep -rn "\.symbol\|\.market" app/` 으로 참조 범위를 전수 조사하고 REQ Impacted Area에 명시한다. broker는 portfolios에 유지되므로 별도 조사 불필요. - R-3 (Medium): `managed_by_daemon_id IS NULL`인 기존 portfolio가 migration 후에도 계속 동작 불가(fail-closed) 상태로 남는다. migration 자체를 막지는 않지만, daemon 미배정 portfolio의 운영 가시성 확보가 필요하다. - 완화 방안: migration 전 `SELECT COUNT(*) FROM portfolios WHERE managed_by_daemon_id IS NULL`로 미배정 portfolio 현황을 파악하고, 필요한 경우 migration 전 daemon 배정 절차를 선행한다. ## 가정 - A-1: 현재 `portfolios` 테이블에서 동일 `symbol`을 가진 행들은 migration 시 하나의 `assets` catalog 항목으로 병합되며, 각 행의 `market` 값은 `ARRAY_AGG(DISTINCT market)`으로 집약되어 `assets.markets`에 저장된다. broker는 각 portfolio 행에 그대로 유지된다. - A-2: `assets` catalog의 `status` 필드는 최초 `active`/`inactive` 두 가지로 시작하며, 비활성 asset에 대한 신규 portfolio 생성은 거부한다. - A-3: 기존 `portfolios.name`은 user가 지정하는 포지션 이름으로 유지된다. asset catalog에는 별도 `display_name` 필드를 둔다. - A-4: portfolio 생성 시 `managed_by_daemon_id` 지정은 선택사항(nullable)이며, 미배정 portfolio는 fail-closed 상태로 운영된다(ledger write·asset listing 불가). daemon 배정은 portfolio 생성과 분리된 admin 작업으로 처리한다. ## 오픈 질문 답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다. - OQ-1: daemon 배정 단위 — daemon을 `assets` catalog 레벨(하나의 daemon이 모든 user의 해당 asset 거래 처리)에 배정할 것인가, `portfolios` 포지션 레벨(user별 별도 daemon 배정)에 배정할 것인가? - 상태: Answered - 결정: portfolio(포지션) 레벨 배정. `portfolios.managed_by_daemon_id` 방식 유지. 동일 종목이라도 user별 별도 daemon 배정 가능. - 반영 위치: Section 3 기대 상태(daemon 배정 단위 확정), FR-5(`GET /v1/assets` portfolio 기준 재정의) - OQ-2: 동일 asset에 대해 한 user가 여러 portfolio(포지션)를 가질 수 있는가? (예: 같은 종목을 전략 A, B로 각각 운용) - 상태: Answered - 결정: 허용. `(owner_user_id, asset_id)` unique constraint 추가하지 않는다. `managed_by_daemon_id`로 동일 asset의 복수 portfolio를 구별한다. - 반영 위치: FR-3(unique constraint 제외 및 managed_by_daemon_id 정책 명시) - OQ-3: asset catalog의 `DELETE` 동작 — catalog에서 asset을 삭제할 때 해당 asset을 참조하는 portfolios가 있으면 어떻게 처리하는가? (거부 / cascade 삭제 / soft delete) - 상태: Answered - 결정: soft delete(`status='inactive'`). hard DELETE 없음. 기존 portfolio 보존, 신규 portfolio 생성만 거부. - 반영 위치: FR-2(delete → deactivate로 변경, soft delete 정책 명시) - OQ-4: `assets.markets` 배열에 저장하는 시장 코드의 허용 값 목록과 정규화 방식을 어떻게 정의하는가? (예: 'KRX', 'NXT', 'SOR', 'NASDAQ', 'NYSE', 'UPBIT' 등 — 대소문자 기준, DB CHECK constraint vs application-layer 정규화 vs 참조 테이블) - 상태: Answered - 결정: application-layer Python Enum 정규화. 허용 값: KRX, NXT, SOR, NASDAQ, NYSE, UPBIT, BITTHUMB. API 수신 시 대문자 변환 후 Enum 검증. DB CHECK constraint 없음. 허용 값 변경 시 코드 배포만 필요. - 반영 위치: FR-1(`markets` 컬럼 설명에 Enum 검증 방식 추가) ### 오픈 질문 처리 기준 - `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다. - `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다. - `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다. - `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다. - 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다. - 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다. - 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다. # 7. 초기 성공 기준 이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다. - S-1: admin이 CLI 또는 HTTP API로 asset catalog에 종목을 등록하면 해당 `asset_id`가 생성되고, user가 그 `asset_id`로 portfolio를 생성할 수 있다. - S-2: catalog에 없거나 `status='inactive'`인 `asset_id`로 portfolio 생성을 시도하면 거부된다. - S-3: 기존 portfolios 데이터가 migration 완료 후 `assets` + `portfolios` 구조로 유실 없이 이전되며, 기존 portfolio 기능(조회, 업데이트, 거래 기록)이 새 구조에서 동일하게 동작한다. - S-4: v1 `GET /v1/assets`가 daemon에 `managed_by_daemon_id`로 배정된 portfolio 목록을 반환하며, 각 항목의 `asset_id`가 catalog의 UUID를 참조한다. - S-5: admin이 asset을 비활성화(`status='inactive'`)한 후 해당 `asset_id`로 신규 portfolio 생성을 시도하면 거부되고, 해당 asset을 참조하는 기존 portfolio는 보존된다. - S-6: user token으로 `GET /v1/assets`를 호출하면 해당 user의 portfolio 목록만 반환된다. admin token으로 `GET /v1/assets`를 호출하면 admin 본인의 portfolio 목록이 반환되고, `?user_id=` 파라미터 지정 시 해당 user의 portfolio 목록이 반환된다. ## 측정 방식 및 데이터 출처 각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다. - S-1 측정 방식: pytest로 admin asset create → user portfolio create(asset_id 지정) 시나리오 검증 / 데이터 출처: 신규 `tests/test_asset_catalog.py` - S-2 측정 방식: pytest로 미등록 asset_id로 portfolio create 시 오류 반환 확인 / 데이터 출처: 신규 `tests/test_asset_catalog.py` - S-3 측정 방식: migration 전후 portfolio 행 수 일치 및 기존 portfolio 기능 regression 테스트 통과 확인 / 데이터 출처: migration script 실행 결과 + 기존 테스트 suite - S-4 측정 방식: daemon service-token으로 `GET /v1/assets` 호출 시 `managed_by_daemon_id` 기반 portfolio 목록과 catalog UUID asset_id 반환 확인 / 데이터 출처: 신규 `tests/test_v1_asset_catalog.py` - S-5 측정 방식: pytest로 admin asset 비활성화 → 해당 `asset_id`로 신규 portfolio 생성 시 거부 + 기존 portfolio 존재 보존 확인 / 데이터 출처: 신규 `tests/test_asset_catalog.py` - S-6 측정 방식: pytest로 user token `GET /v1/assets` 호출 시 본인 portfolio만 반환 확인; admin token으로 본인 portfolio 반환 확인 + `?user_id=` 지정 시 해당 user portfolio 반환 확인 / 데이터 출처: 신규 `tests/test_v1_asset_catalog.py` # 8. REQ로 넘기기 전 확인 체크 REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다. - [x] 범위(In Scope/Out of Scope) 확정 - 기준: OQ-1(daemon 배정 단위), OQ-2(user당 동일 asset 복수 portfolio), OQ-3(asset 삭제 동작) 모두 Answered 처리되어 scope 경계 최종화됨 - [x] 성공 지표의 측정 방식/데이터 출처 확정 - 기준: S-1~6 측정 방식이 검증 가능한 pytest 시나리오로 연결됨 - [x] High 영향 리스크 우선순위 및 해소 책임자 지정 - 기준: R-1(migration 중복 처리 규칙), R-2(symbol/market 참조 전수 조사) 완화 방안 명시됨; draft-req 단계에서 REQ Impacted Area에 반영 예정 - [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 - 기준: OQ-1/2/3/4 모두 Answered 처리 완료 - [x] Freeze 확정 담당자(CONFIRMED_BY) 지정 - 기준: repository-maintainers 중 1명이 Confirmed By에 명시 # 9. 파일 처리 결과 이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다. - 처리 결과: 생성 - 생성 경로: docs/discovery/dcy-001_20260519_asset-catalog-and-user-portfolio-model.md - 참조 문서: - docs/data-model.md (현재 portfolios 모델 baseline — 이 Discovery로 재정의 예정) - docs/interface-contract.md (현재 v1 API 계약 — 이 Discovery로 재정의 예정) - docs/runtime-flows.md (현재 runtime flow baseline — 새 아키텍처 반영 예정) # 10. 사용자 결정 필요 항목 요약 사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다. ## DECIDE - ~~{{DECIDE_DAEMON_ASSIGNMENT_LEVEL}}~~: **Decided** — portfolio(포지션) 레벨 배정. `portfolios.managed_by_daemon_id` 유지. (OQ-1 Answered 2026-05-19) - ~~{{DECIDE_MULTI_PORTFOLIO_PER_ASSET}}~~: **Decided** — 허용. `(owner_user_id, asset_id)` unique constraint 없음. `managed_by_daemon_id`로 구별. (OQ-2 Answered 2026-05-19) - ~~{{DECIDE_ASSET_DELETE_BEHAVIOR}}~~: **Decided** — soft delete(`status='inactive'`). hard DELETE 없음. 기존 portfolio 보존, 신규 생성 거부. (OQ-3 Answered 2026-05-19) - ~~{{DECIDE_MARKET_CODE_NORMALIZATION}}~~: **Decided** — application-layer Python Enum 정규화. DB CHECK constraint 없음. 허용 값 변경 시 코드 배포만 필요. (OQ-4 Answered 2026-05-19) ## CONFIRM - ~~{{CONFIRM_MIGRATION_DEDUP_RULE}}~~: **Confirmed** — A-1(가정)에 동일 내용 명시됨. 동일 symbol → assets 단일 행 병합, market → ARRAY_AGG(DISTINCT market) 집약, broker → 각 portfolio 행 유지. (A-1 참조, 2026-05-19) ## DATA - 없음 # 11. Discovery Freeze REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다. ## 섹션 참조 - 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) - 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) - 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) - 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크) - 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약) ## Freeze 플래그 - Handoff Decision: 승인 - Handoff Rationale: OQ-1~4 모두 Answered, 플레이스홀더 전체 해소, 저장소 사실 관계 일치 확인. REQ 초안 작성 가능. - Ready for REQ Drafting: true - Confirmed By: Justin - Confirmed At (KST): 2026-05-19T00:00+09:00