chore: restructure SRS for asset catalog and tag model redesign (dcy-001/002, REQ-001~008)
Architecture Redesign (May 19, 2026): - Separate admin-managed asset catalog from user portfolios - Eliminate portfolio as asset definition; clarify portfolio as user position - Introduce new asset_catalog + portfolio structure Discovery Documents: - dcy-001: asset-catalog-and-user-portfolio-model — admin asset management and user portfolio separation - dcy-002: tag-ownership-and-applicability-policy — tag model implementation strategy New Requirements (REQ-001~008): - REQ-001: tags master table introduction (Data, Proposed) - REQ-002: assets.tag_ids UUID[] column addition (Data, Proposed) - REQ-003: portfolios.tag_ids and portfolio_tags removal (Data, Proposed) - REQ-004: tags master HTTP API (Interface, Proposed) - REQ-005: tag apply/remove HTTP API (Interface, Proposed) - REQ-006: portfolio_tags data migration (Migration, Proposed) - REQ-007: pfhub tag CLI commands (Interface, Proposed) - REQ-008: tag model baseline docs update (Documentation, Proposed) Cleanup: - Remove archived SRS documents (REQ-001~029 old structure) - Remove archived batch documentation (BAT-001~010) - Remove archived discovery documents (dcy-001~006 old structure) - Remove archived release documentation (REL-001~006) Data Model Updates: - Add Deployment category to SRS structure - Add Data category for new data model requirements - Update app/cli/core/utils.py for new tag handling Documentation: - Create new SRS index with consolidated requirement structure - Update docs/discovery/index.md with new discovery references - Migrate from requirement-centric to domain-model-centric organization Rationale: - Existing single-portfolio model lacks admin catalog governance - New asset catalog enables central management of tradeable symbols - Tag system supports user-driven asset organization and discovery - Cleaner separation of concerns between admin operations and user trading
This commit is contained in:
-224
@@ -1,224 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-001_20260430_real-db-migration-validation-and-operations-readiness
|
||||
- 이슈명: 실DB 마이그레이션 검증 및 운영 준비
|
||||
- 작성 시각(KST): 2026-04-30 22:27
|
||||
- 마지막 갱신 시각(KST): 2026-04-30 23:01
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: docs/srs/Testing/req-001_local-db-validation-scenarios.md, docs/srs/Migration/req-002_alembic-runtime-schema-ownership.md, docs/srs/Documentation/req-003_local-db-operations-runbook-and-baseline-updates.md
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 미생성
|
||||
- 관련 TODO/메모: temp/plan.md
|
||||
- 해석:
|
||||
- bootstrap-baseline 완료 이후 active Discovery, REQ, Batch, Release가 없는 no-history 상태이므로 첫 real Discovery를 새로 생성한다.
|
||||
- 현재 저장소에는 init-db, migrate-db, Alembic migration 경로가 구현돼 있지만 로컬 docker compose 기반 DB 검증 기준과 legacy DB 비지원 범위는 아직 REQ 수준으로 정리되지 않았다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- docs/discovery/ 아래에는 전역 index만 있고 실DB 마이그레이션 검증 또는 운영 준비를 다루는 기존 Discovery 문서가 없다.
|
||||
- baseline 문서와 README는 실DB 검증이 남은 과제라고만 적고 있어, 이번 주제는 후속 Delivery 산출물로 연결될 독립 Discovery로 추적하는 편이 명확하다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- app/cli/commands/health.py 에는 init-db, migrate-db, health-check 명령이 구현돼 있고, app/db/migrations.py 는 Alembic upgrade head 와 legacy schema stamp 분기를 제공한다.
|
||||
- app/repositories/postgres.py 는 다수의 DB 호출 전에 ensure_schema() 를 실행해 런타임 중 테이블을 선제 생성하며, migrations/versions/20260410_0009_portfolio_comments_table.py 는 이 경로와 충돌하지 않도록 IF NOT EXISTS 기반으로 작성돼 있다.
|
||||
- docs/runtime-flows.md 와 docs/data-model.md 는 실DB 검증, 운영 절차, schema ownership 경계가 아직 확정되지 않았다고 기록한다.
|
||||
- 기대 상태
|
||||
- 로컬 docker compose 로 기동한 PostgreSQL 또는 TimescaleDB 환경을 기준으로 init-db 와 migrate-db 의 기대 동작, 실패 조건, 검증 증거가 정의돼 있어야 한다.
|
||||
- Alembic migration 책임과 runtime ensure_schema() 책임 경계가 명확해지고, 운영자가 어떤 절차로 DB를 준비하고 실패 시 어떻게 복구할지 문서로 추적 가능해야 한다.
|
||||
- 이해관계자
|
||||
- 백엔드 유지보수 담당자
|
||||
- DB 운영 또는 배포 담당자
|
||||
- REQ 승인자 및 release 검토자
|
||||
- 제약사항
|
||||
- 기존 Alembic revision chain 을 깨거나 임의 schema reset 을 전제로 한 해결책은 허용하지 않는다.
|
||||
- 검증 대상은 현재 코드와 migration 이 지원하는 PostgreSQL 또는 TimescaleDB 경로여야 하며, Redis 와 Loki 운영 확장은 이번 Discovery의 직접 범위가 아니다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 운영자가 새 PostgreSQL 또는 TimescaleDB 인스턴스를 준비한 뒤 pfhub init-db 와 health-check 로 서비스 기동 가능 상태를 검증하고 싶다.
|
||||
- 유지보수 담당자가 기존 schema 를 가진 환경에서 pfhub migrate-db 를 실행할 때 어떤 migration 이 안전한지와 어떤 사전 점검이 필요한지 알고 싶다.
|
||||
- release 검토자가 DB migration 실패 시 롤백 또는 재시도 판단 기준을 문서와 검증 로그로 확인하고 싶다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- 로컬 docker compose 기반 실DB 기준 init-db, migrate-db, health-check 검증 시나리오와 pass or fail 판단 기준을 정의한다.
|
||||
- Alembic migration 과 PostgresPortfolioRepository.ensure_schema() 사이의 schema ownership 경계를 정리하고, 로컬 runtime bootstrap 흔적과 충돌할 수 있는 revision 및 운영 리스크를 식별한다.
|
||||
- 검증 결과를 README, docs/runtime-flows.md, docs/project-structure.md 에 반영할 수 있도록 운영 준비 산출물과 문서 갱신 요구를 정의한다.
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- app/cli/commands/health.py 의 init-db, migrate-db, health-check 명령
|
||||
- app/db/migrations.py 의 bootstrap_or_upgrade_to_head 와 legacy schema 판정 로직
|
||||
- app/repositories/postgres.py 및 migrations/versions/*.py 의 schema 생성과 migration 충돌 지점
|
||||
- 문서 영향:
|
||||
- docs/runtime-flows.md 의 DB 준비 및 migration 실행 흐름
|
||||
- docs/project-structure.md 의 migration ownership 및 runtime bootstrap 책임 경계
|
||||
- README.md 의 운영 절차, 사전 조건, 검증 예시
|
||||
- 운영 영향:
|
||||
- fresh DB 초기화 절차, 기존 DB 업그레이드 절차, 실패 시 복구 기준
|
||||
- release 전 DB 점검 체크리스트와 검증 로그 수집 방식
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: 로컬 docker compose 로 기동한 PostgreSQL 또는 TimescaleDB 환경에서 init-db, migrate-db, health-check, 핵심 인증 또는 portfolio smoke flow 까지 이어지는 최소 검증 시나리오를 정의해야 한다.
|
||||
- FR-2: 로컬 docker compose 환경에서 fresh DB 또는 app runtime 이 일부 schema 를 먼저 만든 상태를 기준으로 Alembic upgrade 와 runtime ensure_schema() 가 충돌할 수 있는 지점을 식별하고, 사전 점검 또는 보정 규칙을 정의해야 한다.
|
||||
- FR-3: 검증 결과에 따라 app/repositories/postgres.py, app/db/migrations.py, migrations/versions/ 의 책임 경계를 문서화하고 docs/project-structure.md 와 docs/runtime-flows.md 갱신 요구를 명시해야 한다.
|
||||
- FR-4: 운영자가 DB 준비, migration 실행, 실패 대응, 재시도 여부를 판단할 수 있는 최소 runbook 초안을 정의해야 한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: 검증 절차는 로컬 docker compose 검증 환경에서 반복 실행 가능하고, 명령 순서와 기대 결과가 자동화 가능 수준으로 명확해야 한다.
|
||||
- NFR-2: migration 실패 시 원인 파악에 필요한 revision, 선행 조건, 보정 필요 사항이 로그 또는 문서 증거로 남아야 한다.
|
||||
- NFR-3: 운영 준비 요구사항은 현재 release 전 검토 체크리스트로 이어질 수 있도록 rollback 또는 재시도 기준을 포함해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- 로컬 docker compose 로 기동한 DB를 기준으로 init-db, migrate-db, health-check, bootstrap admin 생성, 로그인 또는 토큰 발급, portfolio 기본 조회 수준의 DB smoke validation 정의
|
||||
- Alembic revision chain 과 ensure_schema() 의 책임 경계 정리 및 관련 문서 갱신 요구 정의
|
||||
- Out of Scope
|
||||
- 공유 dev DB, staging, production-like 환경을 대상으로 한 검증 범위 확장
|
||||
- legacy DB schema support, legacy stamp 경로 검증, 과거 운영 DB 보정 절차 정의
|
||||
- Redis, Loki, 캐시, 로그 수집 파이프라인의 production hardening
|
||||
- ownership 기반 권한 모델 재설계나 외부 API versioning 정책 확정
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): runtime ensure_schema() 와 Alembic migration 이 동일 객체를 다른 순서로 다루면 실환경에서 migration 재실행 실패 또는 drift 가 발생할 수 있다.
|
||||
- 완화 방안: 충돌 가능 revision 을 식별하고, fresh/legacy 시나리오별 허용 순서와 금지 순서를 명시한다.
|
||||
- R-2 (Medium): 현재 코드에 남아 있는 legacy schema stamp 분기가 이번 범위에서도 지원되는 경로처럼 오해되면 범위 밖 검증이나 운영 판단이 섞일 수 있다.
|
||||
- 완화 방안: 이번 Discovery 와 후속 REQ 에서 legacy DB 는 비지원 범위임을 명시하고, 필요 시 별도 Discovery 로 분리한다.
|
||||
- R-3 (Medium): 운영 준비 범위가 너무 넓어지면 DB migration 검증 Discovery 가 Redis, Loki, 배포 자동화까지 확장되어 첫 반복 집중도가 떨어질 수 있다.
|
||||
- 완화 방안: DB 준비와 migration 실행 경로만 이번 Discovery의 핵심 대상으로 고정하고 주변 운영 항목은 후속 Discovery 후보로 분리한다.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: 주 검증 대상 DB 는 PostgreSQL 호환 TimescaleDB 또는 PostgreSQL 이며, 현행 psycopg 연결 방식과 Alembic 환경 설정을 그대로 사용한다.
|
||||
- A-2: init-db 와 migrate-db 는 운영자가 직접 실행하거나 automation 에서 호출하는 공식 DB 준비 진입점으로 유지한다.
|
||||
- A-3: legacy DB support 는 이번 반복의 성공 기준에 포함하지 않으며, 실DB 검증 결과가 현재 baseline 문서의 runtime 또는 structure 설명과 다르면 후속 Delivery 에서 해당 baseline 문서를 함께 갱신한다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: 첫 검증 반복에서 대상으로 삼을 환경 범위는 어디까지인가
|
||||
- 상태: Answered
|
||||
- 처리 방안: 사용자 결정에 따라 첫 검증 반복의 대상 환경은 로컬 docker compose 기반 DB로 고정한다.
|
||||
- 종료 조건: 완료. 이후 REQ 와 batch planning 에서는 shared dev DB, staging, production-like 환경을 기본 범위에서 제외한다.
|
||||
- OQ-2: legacy schema support 의 허용 범위를 어디까지 문서화해야 하는가
|
||||
- 상태: Answered
|
||||
- 처리 방안: 사용자 결정에 따라 legacy DB 는 이번 Discovery 와 후속 REQ 범위에서 지원하지 않는다.
|
||||
- 종료 조건: 완료. legacy DB 지원이 필요해지면 별도 Discovery 또는 change-req 로 분리한다.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: 로컬 docker compose 환경에서 fresh DB 와 app runtime 이 일부 schema 를 먼저 만든 DB 시나리오별로 실행 명령, 기대 결과, 실패 조건이 포함된 검증 시나리오 매트릭스가 완성된다.
|
||||
- S-2: Alembic 과 ensure_schema() 의 책임 경계, 위험 revision, 사전 점검 규칙이 REQ 로 인계 가능한 수준으로 정리된다.
|
||||
- S-3: 운영자가 release 전 참고할 수 있는 DB 준비 또는 rollback 판단 기준이 문서 초안으로 정의된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: 시나리오별 명령 순서와 기대 산출물 표 작성 여부 확인 / 데이터 출처: README.md, app/cli/commands/health.py, app/db/migrations.py
|
||||
- S-2 측정 방식: 충돌 지점과 ownership 경계가 파일 단위로 추적 가능한지 검토 / 데이터 출처: app/repositories/postgres.py, migrations/versions/*.py, docs/project-structure.md, docs/runtime-flows.md
|
||||
- S-3 측정 방식: 운영 준비 체크리스트와 rollback 판단 기준의 초안 존재 여부 확인 / 데이터 출처: Discovery 본문, README.md, 후속 batch planning 입력
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: 실DB 검증 대상 환경 범위는 로컬 docker compose 기반 DB로 확정되고, legacy support 는 제외 범위로 확정되어야 한다.
|
||||
- [ ] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: 시나리오 매트릭스, command log, baseline 문서, README 중 어떤 문서가 근거가 되는지 합의돼야 한다.
|
||||
- REQ handoff 판정: 차단 사항 아님. 현재 문서의 `# 7. 초기 성공 기준`에 측정 방식과 데이터 출처 초안이 정리돼 있어 REQ 초안 작성은 가능하고, 세부 artifact 형식은 REQ와 batch planning에서 확정한다.
|
||||
- [ ] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1, R-2 의 해소 주체와 REQ 또는 batch 에서 검증할 방법이 지정돼야 한다.
|
||||
- REQ handoff 판정: 차단 사항 아님. 리스크 자체와 완화 방향은 확정돼 있으므로 REQ 초안 작성은 가능하고, 구체 owner 지정은 REQ 승인 또는 batch planning에서 확정한다.
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Discovery 승인 또는 REQ handoff 를 확인할 담당자로 Justin 이 식별돼야 한다.
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md
|
||||
- 참조 문서:
|
||||
- README.md
|
||||
- app/cli/commands/health.py
|
||||
- app/db/migrations.py
|
||||
- docs/project-structure.md
|
||||
- docs/runtime-flows.md
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- 없음
|
||||
|
||||
## CONFIRM
|
||||
- 로컬 docker compose 기반 DB에서 init-db, migrate-db, health-check, 최소 인증 또는 portfolio smoke flow 성공과 검증 로그 기록을 이번 Discovery의 release gate로 확정한다.
|
||||
|
||||
## DATA
|
||||
- 없음
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 실DB 검증 범위, legacy DB 제외 범위, 승인자 Justin, release gate 기준이 정리되었고 Open 질문과 사용자 결정 항목이 해소됐다. 남은 체크 항목은 REQ drafting을 막지 않는 후속 확정 사항으로 분리했다.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-04-30 22:56
|
||||
|
||||
# 12. 작성 가이드
|
||||
템플릿 사용 시의 기본 규칙을 적는다. 실제 문서를 만들 때는 이 가이드를 기준으로 플레이스홀더를 치환한다.
|
||||
|
||||
- 이 파일은 템플릿이 아니라 실제 초안 문서다. 남은 플레이스홀더는 사용자 또는 승인자 확인이 필요한 항목만 유지한다.
|
||||
- 필요 없는 항목은 삭제하고, 부족한 항목은 같은 형식으로 추가한다.
|
||||
- Open Questions는 가정으로 대체하지 말고 그대로 유지한다.
|
||||
- `대체됨`은 현재 Discovery가 더 이상 기준 문서가 아닐 때 대체 Discovery 경로를 기록하고, 해당 사항이 없으면 `없음`으로 둔다.
|
||||
- `후속 Discovery 참조`는 범위를 이어받는 별도 Discovery가 생성된 경우 그 Discovery 경로를 기록하고, 없으면 `없음`으로 둔다.
|
||||
- Discovery Freeze는 문서 하단에 반드시 유지한다.
|
||||
@@ -0,0 +1,252 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: draft
|
||||
- 문서 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-19 (review-discovery 반영 — FR-2 URL prefix /v1/ 추가, S-6 측정 방식 추가, Section 3 삭제→비활성화, Section 8 S 범위 갱신, Handoff Rationale 갱신)
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: 없음
|
||||
|
||||
# 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=<id>` 파라미터 지정 시 해당 user의 portfolio를 반환.
|
||||
- 모든 토큰 타입에서 `?market=<code>` 파라미터를 지원하며, 지정 시 `<code> = 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=<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=<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 처리 완료
|
||||
- [ ] 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}}: 기존 portfolios에서 동일 `symbol`을 가진 행들을 하나의 asset catalog 항목으로 병합하고 각 행의 `market` 값을 `ARRAY_AGG(DISTINCT market)`으로 집약하는 migration 규칙(A-1)이 맞는지 확인. broker는 각 portfolio 행에 그대로 유지된다는 전제도 함께 확인.
|
||||
|
||||
## DATA
|
||||
- 없음
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 보류
|
||||
- Handoff Rationale: 초기 Discovery 초안 생성 완료. OQ-1~4 모두 Answered 처리 완료. confirm-discovery 후 REQ 초안 작성 가능.
|
||||
- Ready for REQ Drafting: false
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): TBD
|
||||
@@ -1,240 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-002_20260502_trex-basic-consumer-api-contract
|
||||
- 이슈명: trex-basic Consumer-Driven Contract 반영 — service-token runtime_id 발급 및 v1 API 엔드포인트 구현
|
||||
- 작성 시각(KST): 2026-05-02 00:00
|
||||
- 마지막 갱신 시각(KST): 2026-05-13 17:08
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: docs/discovery/dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment.md
|
||||
- 생성된 REQ 참조: REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, REQ-011
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 계약 문서: temp/portfolio-hub-api-contract.md (trex-basic 팀 작성 Consumer-Driven Contract)
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- trex-basic 팀이 portfolio-hub에 Consumer-Driven Contract를 전달했고, 4개 엔드포인트(GET /health, GET /v1/assets, GET /v1/user_info, POST /v1/assets/{asset_id}/ledger/trades)를 구현해야 한다. Q1(service-token 인증)과 Q2(runtime_id 발급 절차)는 이 Discovery에서 결정 사항으로 반영한다.
|
||||
- 현재 `/health`는 `status: ok`만 반환하고 `/v1/*` 엔드포인트는 전혀 없다. service-token 메커니즘(`token_type='service'`)은 이미 구현돼 있으나 `runtime_id` 개념이 없고, 브로커 인증 정보 저장 테이블도 없다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 dcy-001은 로컬 DB 마이그레이션 검증과 운영 준비를 다루며 외부 소비자 API 계약과는 완전히 별개 주제다.
|
||||
- `GET /v1/assets`, `GET /v1/user_info`, `POST /v1/assets/{asset_id}/ledger/trades`는 기존 어떤 Discovery/REQ에도 등장하지 않는 신규 엔드포인트다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `GET /health`는 `app/api/health.py`에서 `{"status": "ok"}`만 반환하며 계약이 요구하는 `service`, `version`, `checked_at` 필드가 없다.
|
||||
- service-token 메커니즘은 migration 0013(`20260423_0013_remove_bot_principals_service_tokens.py`)에서 `token_type='service'`로 완성됐고, CLI `pfhub service-token issue/list/revoke`도 `app/cli/commands/service_tokens.py`에 구현돼 있다. 그러나 발급 결과에 `runtime_id`가 포함되지 않는다.
|
||||
- `Portfolio` 모델(`app/domain/models.py`)에 `portfolio_id`, `broker`, `status`, `symbol`, `cash_balance`, `asset_quantity`, `active_strategy_id`가 있어 `/v1/assets` 응답 필드 일부와 대응하지만, `market`(KOSPI/KOSDAQ) 필드와 `strategy_params` JSON이 없다.
|
||||
- `StrategySet` 모델에 `config`(문자열) 필드가 있어 strategy_params 원본으로 사용 가능하나, Portfolio와의 연결 시 `strategy_params` 역할을 하는 구체적 구현은 없다.
|
||||
- `GET /v1/user_info`가 요구하는 `runtime_id`, `trading_enabled`, `brokers[].credential_payload` 데이터는 현재 어떤 테이블에도 존재하지 않는다.
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades`가 요구하는 `ledger_trades` 테이블이 없다.
|
||||
- 기대 상태
|
||||
- `GET /health`는 `status`, `service`, `version`, `checked_at`을 모두 반환한다.
|
||||
- `pfhub service-token issue` CLI와 대응 API 엔드포인트가 service-token과 함께 `runtime_id`를 생성·반환한다. 발급된 service-token으로 `/v1/*` 엔드포인트를 인증할 수 있고, `GET /v1/user_info` 응답에도 해당 `runtime_id`가 포함된다.
|
||||
- `GET /v1/assets`는 service-token bearer 인증으로 관리 자산 목록(portfolio → asset 뷰)과 전략 파라미터를 반환한다.
|
||||
- `GET /v1/user_info`는 service-token으로 인증한 사용자의 거래 가능 여부와 브로커별 credential_payload를 반환한다.
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades`는 체결 내역을 idempotent하게 기록하고, 중복 수신 시 `status=duplicate`를 반환한다.
|
||||
- 이해관계자
|
||||
- trex-basic 팀 (소비자, Consumer-Driven Contract 작성자)
|
||||
- portfolio-hub 백엔드 유지보수 담당자 (구현 주체)
|
||||
- REQ 승인자
|
||||
- 제약사항
|
||||
- service-token 인증은 기존 `token_type='service'` 메커니즘을 그대로 사용한다. 별도 인증 체계를 새로 만들지 않는다.
|
||||
- `GET /v1/user_info`는 `environment` 필드를 노출하지 않는다. portfolio-hub는 항상 live credential을 반환하며 paper/live 분기를 두지 않는다.
|
||||
- 기존 Alembic revision chain을 깨지 않는 IF NOT EXISTS 기반 migration을 추가한다(dcy-001/REQ-002 정책 준수).
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
|
||||
- trex-basic 데몬이 시작될 때 `GET /health`로 portfolio-hub 가용성을 확인하고, `GET /v1/assets?status=active`로 관리 자산 목록과 전략 파라미터를 읽은 뒤, `GET /v1/user_info`로 거래 가능 여부와 브로커 인증 정보를 획득해 거래를 시작한다.
|
||||
- 운영자가 `pfhub service-token issue <name> --user <username>` CLI로 service-token과 runtime_id를 한 번에 발급하고, 결과를 trex-basic 데몬 환경 변수에 주입한다. 또는 API 엔드포인트를 통해 동일한 발급을 수행한다.
|
||||
- trex-basic이 KIS 브로커로부터 체결을 확인한 뒤 `POST /v1/assets/{asset_id}/ledger/trades`로 체결 내역을 전송한다. 네트워크 오류로 재전송하더라도 동일 `(asset_id, order_id, execution_id)` 조합이면 중복 기록 없이 `status=duplicate`를 응답 받는다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- `GET /health` 응답 스키마 확장: `service`, `version`, `checked_at` 필드 추가
|
||||
- service-token 발급 시 `runtime_id` 동시 생성 및 반환: CLI(`pfhub service-token issue`)와 API 엔드포인트 양쪽에서 `runtime_id`를 함께 발급. `runtime_id`는 발급된 service-token에 연결되어 `GET /v1/user_info` 응답에서 그대로 반환
|
||||
- `/v1/assets`, `/v1/user_info`, `/v1/assets/{asset_id}/ledger/trades` 엔드포인트 신규 구현: 브로커 인증 정보 저장 테이블(`broker_credentials`), 거래 내역 테이블(`ledger_trades`), `portfolios` 테이블에 `market` 컬럼 추가를 포함한 DB 변경과 함께 구현
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `app/api/health.py`: 응답 스키마 확장
|
||||
- `app/api/` 신규 라우터: `v1_assets.py`, `v1_user_info.py`, `v1_ledger.py` (또는 통합 `v1.py`)
|
||||
- `app/cli/commands/service_tokens.py`: `issue` 커맨드에 `runtime_id` 생성·출력 추가
|
||||
- `app/domain/models.py`: `BrokerCredential`, `LedgerTrade` 도메인 모델 추가
|
||||
- `app/repositories/postgres.py`: 브로커 인증 정보 및 ledger 관련 DB 연산 추가
|
||||
- `app/services/`: `v1_service.py` 또는 기존 서비스에 assets/user_info/ledger 메서드 추가
|
||||
- `app/schemas/`: v1 응답 스키마 추가
|
||||
- `migrations/versions/`: `market` 컬럼, `runtime_id` 연결, `broker_credentials`, `ledger_trades` 테이블 migration 추가
|
||||
- `app/main.py`: `/v1` prefix 라우터 등록
|
||||
- 문서 영향:
|
||||
- `docs/interface-contract.md`: trex-basic 소비 엔드포인트 계약 기준 갱신 필요
|
||||
- `docs/data-model.md`: `broker_credentials`, `ledger_trades`, `runtime_id` 연결 모델 추가 필요
|
||||
- 운영 영향:
|
||||
- service-token 발급 절차 변경 — `runtime_id`가 함께 반환되므로 기존 운영 가이드 갱신 필요
|
||||
- 브로커 인증 정보(credential_payload)는 민감 데이터이므로 DB 수준 보안과 접근 정책 검토 필요
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: `GET /health` 응답에 `status`, `service`(고정값 `"portfolio-hub"`), `version`(앱 버전), `checked_at`(ISO 8601 KST) 필드가 포함되어야 한다. `status` 허용값은 `ok`, `degraded`, `failed`이며 정상 응답은 `ok`를 반환해야 한다.
|
||||
- FR-2: `pfhub service-token issue <name> --user <username>` CLI 커맨드와 대응하는 API 엔드포인트(POST /auth/service-tokens 또는 기존 POST /auth/tokens 확장)가 service-token 문자열과 함께 `runtime_id`(고유 식별자)를 생성·반환해야 한다. `runtime_id`는 service-token에 연결되어 DB에 저장되고, 해당 토큰으로 인증한 `GET /v1/user_info` 호출에서 그대로 반환되어야 한다.
|
||||
- FR-3: `GET /v1/assets`는 service-token bearer 인증으로 토큰 소유자(user)의 portfolio 목록을 asset 뷰로 반환해야 한다. 응답 항목은 `asset_id`, `symbol`, `market`, `status`, `cash`, `quantity`, `broker_type`, `strategy_id`, `strategy_params`, `updated_at`을 포함해야 하며, `status=active` query parameter로 필터링이 가능해야 한다. `strategy_params`는 해당 portfolio의 StrategySet config에서 파생한다. `strategy_id`는 서버에서 허용값 검증을 수행하지 않으며, 등록된 모든 값을 그대로 반환한다(OQ-1 Answered).
|
||||
- FR-4: `GET /v1/user_info`는 service-token bearer 인증으로 해당 사용자의 거래 가능 여부(`trading_enabled`, `disabled_reason`)와 브로커별 인증 정보 목록(`brokers[]`)을 반환해야 한다. `brokers[]`의 각 항목은 `broker`, `account_ref`, `credential_payload`, `credential_version`, `last_rotated_at`을 포함해야 한다. `runtime_id`는 인증 service-token에 연결된 값을 반환해야 한다.
|
||||
- FR-5: `POST /v1/assets/{asset_id}/ledger/trades`는 service-token bearer 인증으로 체결 내역을 기록해야 한다. 요청 본문(`execution_id`, `order_id`, `side`, `quantity`, `price`, `fee_amount`, `tax_amount`, `executed_at`)을 `ledger_trades` 테이블에 저장하고 201을 반환해야 한다. `(asset_id, order_id, execution_id)` 동일 조합의 재수신은 신규 행 없이 `status=duplicate`와 기존 `ledger_entry_id`를 200으로 반환해야 한다. 동일 키에 payload가 다른 경우 409를 반환해야 한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: `/v1/user_info`는 `environment` 필드를 포함하지 않는다. portfolio-hub는 항상 live credential을 반환하며, paper/live 분기 로직을 서버에서 관리하지 않는다.
|
||||
- NFR-2: `POST /v1/assets/{asset_id}/ledger/trades`의 idempotency 키는 `(asset_id, order_id, execution_id)` 복합 유니크 제약으로 DB에서 보장해야 한다. 애플리케이션 로직만으로 보장하지 않는다.
|
||||
- NFR-3: 브로커 인증 정보(`credential_payload`)는 DB에 평문 저장을 금지한다. Python `cryptography` 패키지의 Fernet(AES-128-CBC + HMAC-SHA256)으로 저장 전 암호화하고 조회 시 복호화한다. 암호화 키는 환경변수 `CREDENTIAL_ENCRYPTION_KEY`(base64 인코딩 32바이트)로 주입하며, 키가 없으면 서비스 기동을 거부해야 한다(OQ-3 Answered).
|
||||
- NFR-4: 신규 migration은 기존 `ensure_schema()` 런타임 경로와 충돌하지 않도록 IF NOT EXISTS 패턴을 사용해야 한다(REQ-002 준수).
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- `GET /health` 응답 스키마 확장
|
||||
- service-token 발급에 `runtime_id` 동시 생성 (CLI + API 양쪽)
|
||||
- `GET /v1/assets` 엔드포인트 구현 (portfolio → asset 뷰 매핑, strategy_params 포함)
|
||||
- `GET /v1/user_info` 엔드포인트 구현 (runtime_id, trading_enabled, brokers[].credential_payload 포함)
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades` 엔드포인트 구현 (idempotency 포함)
|
||||
- 필요한 DB migration 추가 (market 컬럼, runtime_id 연결, broker_credentials 테이블, ledger_trades 테이블)
|
||||
- Out of Scope
|
||||
- trex-basic 측 코드 변경 — portfolio-hub API 구현만 담당
|
||||
- paper/live 환경 분기 로직 — portfolio-hub는 live credential만 반환
|
||||
- rate limiting 구현 — trex-basic 계약의 Q6이지만 이번 반복에서 요구되지 않음; 향후 반복으로 Deferred
|
||||
- `strategy_id` 허용값 검증 로직 — 검증 없이 통과하는 것으로 확정(OQ-1 Answered). 화이트리스트 불필요
|
||||
- KIS 외 다른 브로커의 credential_payload 스키마 정의 — Q4, 향후 반복으로 Deferred
|
||||
- mock 서버 구현 — trex-basic 팀 자체 `scripts/mock_portfolio_hub.py` 활용
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): `broker_credentials`의 `credential_payload`(KIS app_key/app_secret)는 민감 데이터다. DB에 평문 저장 시 보안 사고 위험이 있다.
|
||||
- 완화 방안: 앱 레벨 Fernet 암호화(옵션 A)로 확정(OQ-3 Answered). `CREDENTIAL_ENCRYPTION_KEY` 환경변수 없이 서비스가 기동되지 않도록 startup validation을 구현한다.
|
||||
- R-2 (Medium): `GET /v1/assets`에서 `strategy_params`를 `StrategySet.config` JSON에서 파생할 때, config 스키마가 전략별로 다르면 응답 일관성이 깨질 수 있다.
|
||||
- 완화 방안: 설계 단계에서 strategy_params 파생 규칙을 확정한다. config가 없거나 파싱 실패 시 빈 객체(`{}`)를 반환하고 로그를 남기는 안전한 폴백을 구현한다.
|
||||
- R-3 (Medium): `portfolios` 테이블에 `market` 컬럼이 없어 기존 portfolio 데이터에 market 값이 없다. migration 후 기존 row의 market 값이 NULL 상태로 남는다.
|
||||
- 완화 방안: migration에서 `market` 컬럼을 nullable로 추가한다(기본값 없음, DECIDE_MARKET_DEFAULT Answered). NULL인 경우 API 응답에서 `null`을 그대로 반환하며, trex-basic 클라이언트가 null을 처리하도록 한다.
|
||||
- R-4 (Low): `GET /v1/user_info` 호출 시 service-token에 연결된 `runtime_id`가 없는 경우(기존 발급 토큰), 응답 처리 방식이 결정되지 않았다.
|
||||
- 완화 방안: 기존 service-token은 `runtime_id`가 없으므로 404 또는 409 응답으로 처리하거나, 런타임에 즉시 `runtime_id`를 생성해 연결하는 방안을 설계 단계에서 결정한다.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: service-token 인증은 기존 `token_type='service'` 메커니즘과 bearer token 헤더 파싱을 그대로 사용한다. 별도 인증 체계를 추가하지 않는다. (Q1 결정 사항)
|
||||
- A-2: `runtime_id`는 service-token 발급 시 함께 생성되며, 해당 service-token에 연결된다. 동일 사용자의 다른 service-token은 각기 다른 `runtime_id`를 가진다. (Q2 결정 사항)
|
||||
- A-3: `GET /v1/assets`의 `asset_id`는 기존 `portfolio_id`를 그대로 사용한다. 별도 asset 식별자 체계를 만들지 않는다.
|
||||
- **[철회 — 2026-05-11 REQ-018]**: REQ-016 UUID 마이그레이션 완료 이후 `portfolio_id`는 server-generated UUID로 변경된다. 따라서 `asset_id == portfolio_id (text)` 전제는 `asset_id == portfolio_id (UUID)`로 대체되었다. 가정 A-3의 "별도 asset 식별자 체계를 만들지 않는다"는 원칙은 유지되지만, 실제 값 형식이 UUID로 바뀐다는 점에서 원문 가정의 텍스트 동일성 가정은 철회한다.
|
||||
- A-4: `trading_enabled`는 현재 단계에서 `users.is_active`와 연동하는 단순 매핑으로 구현한다. 별도 trading 전용 상태 필드는 이번 반복 Out of Scope다.
|
||||
- A-5: trex-basic이 사용하는 KIS 브로커만 우선 지원하며, credential_payload 스키마는 `{"app_key": "...", "app_secret": "..."}` 형식으로 고정한다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: `strategy_id` 허용값 목록 — trex-basic 첫 실전 전략 ID가 확정되지 않았다. 현재 StrategySet에 있는 전략 ID 값을 검증 없이 통과시킬 것인지, 화이트리스트를 만들 것인지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: `strategy_id`는 어떤 값을 사용해도 가능하도록 서버에서 검증 없이 통과한다. 화이트리스트를 두지 않는다. FR-3에 반영.
|
||||
- 종료 조건: 완료.
|
||||
- OQ-2: `GET /health` `degraded` 상태 정의 — degraded 시 후속 엔드포인트를 계속 시도할지 여부. portfolio-hub 내부 degraded 판단 기준이 없다.
|
||||
- 상태: Answered
|
||||
- 처리 방안: `degraded` 값은 응답 스키마에 유효한 상태로 포함한다(FR-1 허용값 유지). 단, 이번 반복에서는 해당 상태를 반환하는 트리거 조건을 정의하지 않으며, 트리거 조건은 추후 구현 시 정의한다.
|
||||
- 종료 조건: 완료.
|
||||
- OQ-3: `credential_payload` DB 저장 암호화 방식 — `broker_credentials.credential_payload`는 KIS `app_key`/`app_secret`을 담는 JSON으로, portfolio-hub PostgreSQL DB에 저장된다. 평문 저장 시 DB 덤프·직접 접근 경로에서 인증 정보가 노출된다(R-1). 아래 세 가지 방식 중 하나를 선택해야 한다.
|
||||
- **옵션 A — 앱 레벨 AES 암호화 (Fernet)**: Python `cryptography` 패키지로 저장 전 암호화, 조회 시 복호화. 암호화 키는 환경변수 1개(`CREDENTIAL_ENCRYPTION_KEY`)로 주입. 인프라 변경 없음. 구현 복잡도 낮음. psql에서 payload 직접 읽기 불가. 현재 스택에 가장 단순하게 추가 가능.
|
||||
- **옵션 B — pgcrypto 컬럼 암호화**: PostgreSQL `pgcrypto` extension 활성화 후 `pgp_sym_encrypt`/`pgp_sym_decrypt`로 DB 내 암/복호화. 키는 쿼리 파라미터로 전달하거나 환경변수에서 주입. Docker Compose 환경에서 extension 활성화 필요. 앱 레벨 처리 없이 DB에서 완결되지만, 쿼리마다 키를 파라미터로 전달하는 패턴이 필요해 구현 복잡도가 옵션 A보다 높음.
|
||||
- **옵션 C — 외부 시크릿 관리자 (Vault, AWS Secrets Manager 등)**: DB에는 시크릿 참조 URI만 저장하고, 실제 credential은 외부 서비스에서 조회. 가장 강력하지만 별도 인프라 운영이 필요하며 현재 프로젝트 규모 대비 과잉 투자.
|
||||
- 상태: Answered
|
||||
- 처리 방안: **옵션 A(앱 레벨 AES/Fernet)** 채택. Python `cryptography` 패키지로 저장 전 암호화, 조회 시 복호화. 암호화 키는 환경변수 `CREDENTIAL_ENCRYPTION_KEY`로 주입. NFR-3과 R-1 완화 방안에 반영.
|
||||
- 종료 조건: 완료.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: `GET /health` 응답에 `status`, `service`, `version`, `checked_at` 필드가 모두 존재하고, `checked_at`이 ISO 8601 형식을 따른다.
|
||||
- S-2: `pfhub service-token issue <name> --user <username>` 실행 결과에 `token`과 `runtime_id`가 모두 포함되고, 대응 API 엔드포인트로도 동일하게 발급된다. 발급된 token으로 `GET /v1/user_info`를 호출하면 동일 `runtime_id`가 응답에 포함된다.
|
||||
- S-3: `GET /v1/assets`가 service-token 인증으로 portfolio 목록을 asset 뷰로 반환하고, 각 항목에 `strategy_params` 객체가 포함된다. `status=active` 필터가 정상 동작한다.
|
||||
- S-4: `GET /v1/user_info`가 `trading_enabled`, `runtime_id`, `brokers[].credential_payload`를 반환하며 `environment` 필드를 포함하지 않는다.
|
||||
- S-5: 동일 `(asset_id, order_id, execution_id)` 조합으로 `POST /v1/assets/{asset_id}/ledger/trades`를 두 번 호출할 때 첫 번째는 201, 두 번째는 200+`status=duplicate`를 반환하고 DB에는 단일 행만 존재한다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
- S-1 측정 방식: 자동화 테스트(pytest)로 `/health` 응답 JSON 필드 검증 / 데이터 출처: `tests/test_app.py` 또는 신규 `tests/test_health.py`
|
||||
- S-2 측정 방식: CLI 출력 파싱 + API 응답 JSON 검증 / 데이터 출처: `tests/test_service_tokens.py`(신규) 또는 `tests/test_auth.py` 확장
|
||||
- S-3 측정 방식: `GET /v1/assets` 응답 스키마 및 필터 동작 pytest 검증 / 데이터 출처: `tests/test_v1_assets.py`(신규)
|
||||
- S-4 측정 방식: `GET /v1/user_info` 응답 JSON 필드 존재·비존재 검증 / 데이터 출처: `tests/test_v1_user_info.py`(신규)
|
||||
- S-5 측정 방식: 동일 payload 2회 POST 후 응답 코드·status 및 DB row count 검증 / 데이터 출처: `tests/test_v1_ledger.py`(신규)
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: In Scope/Out of Scope 경계가 명확하고 trex-basic 계약 범위와 일치
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1 ~ S-5 모두 pytest 검증 가능한 형태로 기술됨
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1(credential_payload 암호화) OQ-3 Answered — 옵션 A(Fernet) 확정
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1, OQ-2, OQ-3 모두 Answered
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Confirmed By = Justin, confirm-discovery skill 확인 완료
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md
|
||||
- 참조 문서:
|
||||
- temp/portfolio-hub-api-contract.md (trex-basic Consumer-Driven Contract 원문)
|
||||
- docs/discovery/dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md (선행 Discovery)
|
||||
- app/api/health.py
|
||||
- app/cli/commands/service_tokens.py
|
||||
- app/domain/models.py
|
||||
- migrations/versions/20260423_0013_remove_bot_principals_service_tokens.py
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- ~~{{DECIDE_CREDENTIAL_ENCRYPTION}}~~: 옵션 A(앱 레벨 Fernet/AES) 채택 확정 (OQ-3 Answered)
|
||||
- ~~{{DECIDE_MARKET_DEFAULT}}~~: `portfolios.market` nullable 컬럼으로 추가, 기본값 없음, API 응답에서 null 그대로 반환 (Answered)
|
||||
|
||||
## CONFIRM
|
||||
- ~~{{CONFIRM_RUNTIME_ID_SCOPE}}~~: `runtime_id`는 service-token 1개당 1개로 발급되며 해당 service-token에 연결된다. 동일 사용자의 다른 service-token은 각기 다른 `runtime_id`를 가진다. (A-2 Confirmed)
|
||||
- ~~{{CONFIRM_TRADING_ENABLED_MAPPING}}~~: `trading_enabled`는 `users.is_active`와 단순 매핑으로 구현한다. 별도 trading 전용 상태 필드는 이번 반복 Out of Scope. (A-4 Confirmed)
|
||||
|
||||
## DATA
|
||||
- ~~{{DATA_STRATEGY_ID_LIST}}~~: `strategy_id` 화이트리스트 불필요 — 검증 없이 통과하는 것으로 확정 (OQ-1 Answered)
|
||||
- ~~{{DATA_BROKER_CREDENTIAL_SCHEMA_OTHER}}~~: KIS 외 다른 브로커의 `credential_payload` 스키마 — 이번 반복 Out of Scope로 확정. 향후 반복에서 별도 Discovery로 처리.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 모든 OQ(OQ-1, OQ-2, OQ-3)가 Answered 상태로 해소됨. CONFIRM 항목(runtime_id 발급 방식, trading_enabled 매핑)은 가정(A-2, A-4)으로 문서화되어 있으며 REQ 초안 작성 시 세부 검토 가능.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-05-02 02:00
|
||||
@@ -0,0 +1,239 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-002_20260519_tag-ownership-and-applicability-policy
|
||||
- 이슈명: 태그 소유권 및 적용 대상 정책 — admin/user 구분 태그 모델과 asset·portfolio 적용 정책 도입
|
||||
- 작성 시각(KST): 2026-05-19
|
||||
- 마지막 갱신 시각(KST): 2026-05-19 (draft-req 완료 — REQ-001~008 생성)
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: docs/discovery/dcy-001_20260519_asset-catalog-and-user-portfolio-model.md
|
||||
- 생성된 REQ 참조: REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 미생성
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- dcy-001에서 태그 정책을 별도 Discovery로 분리하기로 결정했다. 현재 portfolio_tags 테이블은 tag_value 자유 문자열로 태깅하며 소유권 개념이 없다.
|
||||
- 이번 Discovery에서 판단해야 할 핵심은 tags 마스터 테이블 도입 → admin/user 태그 생성 권한 분리 → asset_tags·portfolio_tags 적용 정책 정의 → 기존 데이터 마이그레이션 전략이다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 현재 SDLC 문서에 태그 소유권이나 tags 마스터 테이블 관련 REQ가 존재하지 않는다.
|
||||
- dcy-001은 태그 정책 전체를 Out of Scope로 명시하며 dcy-002로 이관했다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- portfolio_tags 테이블이 (portfolio_id, tag_value TEXT, created_at)로 구성되어 있으며 태그는 자유 문자열로 저장된다. 정규화된 tags 마스터 테이블이 없어 소유권·권한 제어가 불가능하다.
|
||||
- 사용자가 임의 문자열을 태그로 등록할 수 있으며, 동일한 개념의 태그가 서로 다른 문자열로 중복 생성될 수 있다.
|
||||
- asset_tags 테이블이 없다. dcy-001에서 assets 카탈로그를 도입하지만 태그 정책이 정의되지 않으면 assets에 태그를 적용할 수 없다.
|
||||
- 기대 상태
|
||||
- tags 마스터 테이블로 태그를 정규화하고 생성자(created_by_user_id)와 is_admin_tag를 기록해 소유권을 추적한다.
|
||||
- tags는 hard delete 없이 is_active = FALSE로 비활성화하며, 비활성 태그인지 조회로 확인할 수 있다.
|
||||
- assets.tag_ids UUID[], portfolios.tag_ids UUID[] 배열 컬럼으로 태그를 저장하고 junction 테이블은 사용하지 않는다.
|
||||
- admin이 생성한 태그(is_admin_tag = TRUE)만 assets에 적용 가능하고, portfolio에는 is_admin_tag = TRUE이거나 portfolio 소유자가 생성한 태그를 적용 가능하다.
|
||||
- 기존 portfolio_tags.tag_value 데이터를 새 스키마로 마이그레이션한다.
|
||||
- 이해관계자
|
||||
- 문서 작성자(AI agent)
|
||||
- 리뷰어/승인자 (시스템 소유자)
|
||||
- admin 사용자 (태그 생성·asset 태깅·portfolio 태깅)
|
||||
- 일반 사용자 (태그 생성·자신의 portfolio 태깅)
|
||||
- 제약사항
|
||||
- dcy-001의 assets 테이블이 먼저 확정되어야 assets.tag_ids 컬럼을 추가할 수 있다.
|
||||
- 기존 portfolio_tags.tag_value 마이그레이션 시 created_by 정보가 없어 기존 태그의 소유자를 소급 지정해야 한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
|
||||
- admin이 "코스피200" 태그를 생성하고 assets 카탈로그의 삼성전자 항목에 태깅한다.
|
||||
- 일반 사용자가 "장기보유" 태그를 생성하고 자신의 삼성전자 portfolio에 태깅한다. admin이 생성한 "코스피200"도 동일 portfolio에 추가 태깅한다.
|
||||
- 일반 사용자가 admin 전용 태그를 asset에 태깅하려 하면 HTTP 403 Forbidden을 반환한다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- tags 마스터 테이블 도입: (tag_id UUID PK, name TEXT NOT NULL UNIQUE, created_by_user_id FK → users.id, is_admin_tag BOOLEAN, is_active BOOLEAN, created_at TIMESTAMPTZ) — soft delete 방식으로 비활성화
|
||||
- assets.tag_ids UUID[] 컬럼 추가 — junction 테이블 없이 배열로 태그 참조 저장
|
||||
- portfolios.tag_ids UUID[] 컬럼 추가 및 기존 portfolio_tags 테이블 제거
|
||||
- 태그 CRUD API: POST /v1/tags, GET /v1/tags, PATCH /v1/tags/{tag_id}/deactivate (비활성화)
|
||||
- 기존 portfolio_tags.tag_value 데이터 마이그레이션 (tags 테이블 생성 후 portfolios.tag_ids 배열로 이전)
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- migrations/versions/ — tags 생성, assets.tag_ids 컬럼 추가, portfolios.tag_ids 컬럼 추가, portfolio_tags 테이블 제거 마이그레이션
|
||||
- app/schemas/v1.py — tags 관련 요청/응답 스키마 추가, AssetItem·PortfolioItem에 tag_ids 필드 추가
|
||||
- app/routers/ — /v1/tags 엔드포인트 추가
|
||||
- app/repositories/ — tags CRUD, assets/portfolios tag_ids 배열 조작 메서드 추가
|
||||
- app/cli/ — 기존 `pfhub portfolio tag add/list/remove` 명령어(portfolio_tags 자유 문자열 기반)를 교체하고, 새 `pfhub tag` 최상위 서브커맨드를 추가 (tags 마스터 테이블 기반)
|
||||
- 문서 영향:
|
||||
- docs/data-model.md — tags 테이블 추가, assets/portfolios 스키마에 tag_ids 컬럼 반영, portfolio_tags 테이블 제거
|
||||
- docs/interface-contract.md — /v1/tags API 명세 추가
|
||||
- 운영 영향:
|
||||
- 마이그레이션 실행 필요 (portfolio_tags.tag_value → tags 생성 후 portfolios.tag_ids 배열 이전, portfolio_tags 테이블 드롭)
|
||||
- 기존 portfolio_tags 기반 쿼리·API가 있다면 인터페이스 변경 영향
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: tags 마스터 테이블을 생성한다. 스키마: (tag_id UUID PK DEFAULT gen_random_uuid(), name TEXT NOT NULL, created_by_user_id TEXT NOT NULL REFERENCES users(id), is_admin_tag BOOLEAN NOT NULL DEFAULT FALSE, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()), UNIQUE(name); 태그 생성 시 요청자가 admin 권한이면 is_admin_tag = TRUE로 기록하며 이후 변경하지 않는다. 태그 삭제는 is_active = FALSE로 비활성화하며 hard delete는 사용하지 않는다.
|
||||
- FR-2: assets 테이블에 tag_ids UUID[] NOT NULL DEFAULT '{}' 컬럼을 추가한다. asset에 태그 적용 시 tags.is_admin_tag = TRUE이고 tags.is_active = TRUE인지 확인하고, 조건 불만족 시 HTTP 403을 반환한다. 비활성 태그(is_active = FALSE)는 적용 불가하며 목록 조회 시 별도 표시한다.
|
||||
- FR-3: portfolios 테이블에 tag_ids UUID[] NOT NULL DEFAULT '{}' 컬럼을 추가하고 기존 portfolio_tags 테이블을 제거한다. portfolio에 태그 적용 시 tags.is_active = TRUE이고, tags.is_admin_tag = TRUE이거나 tags.created_by_user_id가 해당 portfolio 소유자인지 확인하고, 조건 불만족 시 HTTP 403을 반환한다.
|
||||
- FR-4: 태그 HTTP API를 제공한다.
|
||||
- 태그 마스터 관리: POST /v1/tags (태그 생성 — trade-write 이상), GET /v1/tags (태그 목록 조회, is_active 필터 지원 — read 이상), PATCH /v1/tags/{tag_id}/deactivate (태그 비활성화 — admin 또는 태그 생성자 본인)
|
||||
- asset 태그 적용·해제: POST /v1/assets/{asset_id}/tags (body: {"tag_id": "..."}, asset 태깅 — admin), DELETE /v1/assets/{asset_id}/tags/{tag_id} (asset 태그 해제 — admin)
|
||||
- portfolio 태그 적용·해제: POST /v1/portfolios/{portfolio_id}/tags (body: {"tag_id": "..."}, portfolio 태깅 — trade-write 이상, 소유자 본인 또는 admin), DELETE /v1/portfolios/{portfolio_id}/tags/{tag_id} (portfolio 태그 해제 — trade-write 이상, 소유자 본인 또는 admin)
|
||||
- FR-5: 기존 portfolio_tags 데이터를 마이그레이션한다. tag_value를 소문자 정규화 후 tags 테이블에 INSERT(ON CONFLICT DO NOTHING, is_admin_tag = FALSE, created_by_user_id는 마이그레이션 실행 전 운영자가 확인해 지정하는 특정 admin 계정 ID로 설정)하고, 각 portfolio의 tag_ids 배열에 해당 tag_id를 추가한 뒤 portfolio_tags 테이블을 제거한다. 마이그레이션 스크립트는 실행 전 지정 admin 계정이 users 테이블에 존재하는지 사전 검증한다.
|
||||
- FR-6: 태그 CLI 명령어를 제공한다. `pfhub tag create <name>` (태그 생성), `pfhub tag list` (태그 목록, --inactive 플래그로 비활성 포함), `pfhub tag deactivate <tag_id>` (태그 비활성화), `pfhub tag apply asset <asset_id> <tag_id>` (asset 태깅), `pfhub tag apply portfolio <portfolio_id> <tag_id>` (portfolio 태깅), `pfhub tag remove asset <asset_id> <tag_id>` / `pfhub tag remove portfolio <portfolio_id> <tag_id>` (태그 해제)
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: tags.name은 UNIQUE 제약으로 전역 중복을 방지한다. 저장 전 애플리케이션 레이어에서 소문자로 정규화(str.lower())하며, DB 컬럼은 TEXT UNIQUE 그대로 사용한다. 동일 이름의 대소문자 변형은 중복으로 처리해 거부한다.
|
||||
- NFR-2: tags는 hard delete를 사용하지 않으므로 배열 참조 무결성은 애플리케이션 레이어에서 보장한다. tag_ids 배열에서 tag를 조회할 때 is_active = FALSE인 항목은 비활성 표시(archived)로 반환하고 조용히 제거하지 않는다. assets.tag_ids, portfolios.tag_ids에는 GIN 인덱스를 생성해 배열 조회 성능을 확보한다.
|
||||
- NFR-3: 적용 정책 위반(asset에 non-admin 생성 태그 적용 시도, portfolio에 타인 생성 태그 적용 시도) 시 HTTP 403 Forbidden을 반환한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- tags 마스터 테이블 도입 (is_admin_tag, is_active 포함)
|
||||
- assets.tag_ids UUID[] 컬럼 추가
|
||||
- portfolios.tag_ids UUID[] 컬럼 추가 및 portfolio_tags 테이블 제거
|
||||
- 태그 생성·목록·비활성화 HTTP API (/v1/tags)
|
||||
- assets/portfolios 태그 적용·해제 API
|
||||
- 태그 관리 CLI 명령어 (pfhub tag ...)
|
||||
- 기존 portfolio_tags.tag_value 데이터 마이그레이션
|
||||
- Out of Scope
|
||||
- 태그 이름 변경(rename) — 전파 영향 범위가 커서 별도 검토 필요
|
||||
- 태그 계층 구조(parent-child) — 단일 레이블 모델만 지원
|
||||
- 태그 검색/자동완성 API — 기본 목록 조회만 포함
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): 기존 portfolio_tags.tag_value 마이그레이션 시 created_by_user_id를 소급 지정할 정보가 없다. 잘못된 소유권 설정이 이후 적용 정책 오류로 이어질 수 있다.
|
||||
- 완화 방안: 마이그레이션 시 시스템 admin 계정 또는 별도 지정 사용자로 일괄 설정하고, 마이그레이션 완료 후 개별 태그 소유권 재지정 스크립트를 제공한다.
|
||||
- R-2 (Medium): dcy-001의 assets 테이블이 확정되기 전에 assets.tag_ids 컬럼을 추가하면 스키마 충돌이 발생할 수 있다.
|
||||
- 완화 방안: dcy-001 확정 및 assets 마이그레이션 완료 후 dcy-002 구현을 시작한다. batch 계획 시 의존 순서를 명시한다.
|
||||
- R-3 (Low): tags.name UNIQUE 제약 추가 후 기존 portfolio_tags.tag_value에 대소문자만 다른 동일 의미 태그가 존재할 수 있다.
|
||||
- 완화 방안: 마이그레이션 전 중복 tag_value를 조사하고, 소문자 정규화 또는 선택적 병합 스크립트를 실행한다.
|
||||
|
||||
## 가정
|
||||
|
||||
- A-1: 태그 생성자가 admin인지는 tags.is_admin_tag(생성 시점 스냅샷)로 판단한다. 이후 권한 변경과 무관하게 고정되므로 런타임 JOIN이 불필요하다.
|
||||
- A-2: 기존 portfolio_tags.tag_value 마이그레이션 시 충돌 방지를 위해 name을 소문자 정규화 후 UNIQUE 체크한다.
|
||||
- A-3: 태그 삭제는 is_active = FALSE로 처리(soft delete)하며 hard DELETE 및 CASCADE DELETE를 사용하지 않는다. assets.tag_ids, portfolios.tag_ids의 UUID[] 배열은 비활성 태그를 자동 제거하지 않고 조회 시 archived로 표시한다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: tags.name 대소문자 구분 여부 — case-sensitive(기본 TEXT UNIQUE)인지, case-insensitive(CITEXT 또는 lower() 인덱스 기반 UNIQUE)인지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: case-insensitive, 애플리케이션 레이어에서 소문자 정규화 후 저장 방식으로 결정. extension 의존성 없이 TEXT UNIQUE 제약을 그대로 사용한다.
|
||||
- 반영 위치: NFR-1 수정 완료.
|
||||
- OQ-2: 태그 적용 시 get-or-create 정책 — 태그 이름으로 적용 요청 시 tags 테이블에 없으면 자동 생성(get-or-create)할지, 먼저 태그를 생성한 뒤 tag_id로만 적용을 허용할지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: explicit-create 방식으로 결정. 태그 생성(POST /v1/tags)과 태그 적용(apply)을 별도 단계로 분리하며, 존재하지 않는 tag_id로 apply 요청 시 HTTP 404를 반환한다.
|
||||
- 반영 위치: FR-4(태그 생성·적용 분리 명시), FR-6(CLI create/apply 분리 설계) 현행 유지.
|
||||
- OQ-3: 마이그레이션 시 기존 portfolio_tags.tag_value의 created_by_user_id를 어느 admin 계정으로 지정할지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: 특정 admin 계정을 지정하는 방식으로 결정. 마이그레이션 실행 전 운영자가 대상 admin 계정 ID를 확인해 스크립트에 명시한다. is_admin_tag = FALSE로 설정(기존 태그는 사용자 포트폴리오에서 유래했으므로 asset 적용 대상으로 간주하지 않음).
|
||||
- 반영 위치: FR-5 수정 완료.
|
||||
- OQ-4: 태그 생성자가 admin인지 판단하는 방식 — 런타임 JOIN 방식과 생성 시점 스냅샷 방식 중 어느 쪽을 선택할지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: 생성 시점 스냅샷 방식으로 결정. tags 테이블에 is_admin_tag BOOLEAN NOT NULL DEFAULT FALSE 컬럼을 추가해 태그 생성 시 권한을 고정 기록한다.
|
||||
- 반영 위치: FR-1 스키마(is_admin_tag 컬럼 추가), FR-2 검증 로직(is_admin_tag = TRUE 조건), FR-3 검증 로직(is_admin_tag = TRUE OR 소유자 조건), A-1 수정 완료.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: tags 마스터 테이블이 생성되고 모든 태그가 created_by_user_id와 함께 정규화되어 저장된다.
|
||||
- S-2: asset_tags에 admin이 생성한 태그만 적용 가능하고, 비admin 생성 태그 적용 시 HTTP 403이 반환된다.
|
||||
- S-3: portfolios.tag_ids UUID[] 배열로 이전되고 기존 portfolio_tags 테이블이 제거되며, portfolio에 admin 또는 소유자 생성 태그만 적용 가능하다.
|
||||
- S-4: 기존 portfolio_tags.tag_value 데이터가 새 스키마로 누락 없이 마이그레이션된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: 마이그레이션 실행 후 tags 테이블 row 수가 기존 DISTINCT tag_value 수와 일치하는지 SQL로 확인 / 데이터 출처: 마이그레이션 전후 DB 쿼리 결과
|
||||
- S-2 측정 방식: 통합 테스트 — 비admin 사용자 토큰으로 asset 태깅 요청 시 403 응답 검증 / 데이터 출처: pytest 테스트 결과
|
||||
- S-3 측정 방식: 통합 테스트 — admin 태그 및 소유자 태그는 portfolio 태깅 성공, 타인 생성 태그 적용 시 403 검증 / 데이터 출처: pytest 테스트 결과
|
||||
- S-4 측정 방식: 마이그레이션 후 portfolio별 태그 목록이 기존 tag_value 목록과 일치하는지 SQL로 확인 / 데이터 출처: 마이그레이션 전후 DB 쿼리 결과
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: 사용자가 In/Out of Scope 목록을 검토하고 승인
|
||||
- 확인: confirm-discovery 실행(2026-05-19) — 시스템 소유자 승인
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: 각 성공 기준에 측정 방식과 데이터 출처가 명시됨
|
||||
- 확인: S-1~S-4 각각 SQL 검증/pytest 출처 명시됨 (# 7 참조)
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1(마이그레이션 소유권 문제) 해소 방안과 담당자 확정
|
||||
- 확인: R-1 완화 방안 명시됨; FR-5에서 사전 검증 절차 정의; 담당자 — 시스템 소유자(마이그레이션 실행 운영자)
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 사용자(시스템 소유자)가 CONFIRMED_BY로 지정되어 승인
|
||||
- 확인: Justin Ko (genichin@gmail.com)
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-002_20260519_tag-ownership-and-applicability-policy.md
|
||||
- 참조 문서:
|
||||
- docs/data-model.md
|
||||
- docs/interface-contract.md
|
||||
- docs/discovery/dcy-001_20260519_asset-catalog-and-user-portfolio-model.md
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE (완료)
|
||||
- ~~DECIDE_NAME_CASE~~: OQ-1 Answered — case-insensitive, 애플리케이션 레이어 str.lower() 정규화 후 TEXT UNIQUE 저장 (NFR-1 반영 완료)
|
||||
- ~~DECIDE_TAG_APPLY_POLICY~~: OQ-2 Answered — explicit-create, 미존재 tag_id apply 시 HTTP 404 (FR-4, FR-6 반영 완료)
|
||||
|
||||
## CONFIRM (완료)
|
||||
- ~~CONFIRM_MIGRATION_OWNER~~: OQ-3 Answered — 특정 admin 계정 지정, is_admin_tag=FALSE, 마이그레이션 전 계정 존재 사전 검증 (FR-5 반영 완료)
|
||||
|
||||
## DATA
|
||||
- 없음 — 기존 tag_value 중복 검사는 소문자 정규화 후 UNIQUE 체크 방식으로 접근법 확정(R-3, A-2, NFR-1). 실제 데이터 검증은 마이그레이션 스크립트 사전 검증 단계(FR-5)에서 수행.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: OQ-1~OQ-4 모두 Answered 완료. 범위 확정, Section 8 전체 체크, 저장소 사실 관계 검증 완료. REQ 초안 작성 진행 가능.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin Ko (genichin@gmail.com)
|
||||
- Confirmed At (KST): 2026-05-19
|
||||
@@ -1,191 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-003_20260502_portfolio-list-user-required
|
||||
- 이슈명: CLI `pfhub portfolio list` — `--user` 옵션 필수화 및 소유자 기반 조회 강제
|
||||
- 작성 시각(KST): 2026-05-02T00:00:00+09:00
|
||||
- 마지막 갱신 시각(KST): 2026-05-02T09:10:00+09:00
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조:
|
||||
- [REQ-013](../../docs/srs/Interface/req-013_portfolio-list-user-required.md) — CLI `portfolio list --user` 필수화 및 소유자 기반 조회
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 미생성
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- CLI `pfhub portfolio list`는 현재 `--user` 옵션이 없어 `owner_user_id=None`으로 서비스를 호출하며, DB에 저장된 전체 포트폴리오를 반환한다. HTTP API는 `_ownership_required` 설정 및 ADMIN 여부에 따라 소유자 필터를 적용하지만 CLI에는 동일한 접근 통제가 존재하지 않는다.
|
||||
- 이번 Discovery에서는 CLI `portfolio list` 명령에 `--user` 옵션을 필수로 지정하고, 해당 username으로 `owner_user_id`를 조회하여 소유자 기반 필터 조회를 강제하는 방향을 정의한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- dcy-001(실DB 마이그레이션 검증)과 dcy-002(trex-basic API 계약)는 모두 CLI `portfolio list` 인터페이스 및 소유자 필터 정책을 다루지 않는다.
|
||||
- 기존 REQ 목록(`docs/srs/index.md`, REQ-001~REQ-012)에 CLI `portfolio list` 접근 통제 관련 요구사항이 존재하지 않는다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `app/cli/commands/portfolio.py`의 `list_portfolios` 명령은 `--user` 옵션이 없으며, `owner_user_id=None`으로 `PortfolioService.list_portfolios`를 호출한다.
|
||||
- `owner_user_id=None`이면 `app/repositories/postgres.py`의 `list_portfolios`가 소유자 WHERE 절을 추가하지 않아 전체 포트폴리오를 반환한다.
|
||||
- HTTP API(`app/api/portfolios.py`)는 `_ownership_required` 설정 플래그와 ADMIN 권한 여부에 따라 소유자 필터를 조건부 적용하지만, CLI에는 동일한 로직이 없다.
|
||||
- 기대 상태
|
||||
- `pfhub portfolio list --user <username>` 형태로 `--user`를 필수 옵션으로 지정한다.
|
||||
- `--user` 없이 실행하면 명확한 오류 메시지와 함께 종료된다.
|
||||
- `--user`로 전달된 username을 users 테이블에서 조회하여 `owner_user_id`(UUID)로 변환한 뒤 조회한다.
|
||||
- 이해관계자
|
||||
- CLI 운영자(관리자 계정으로 pfhub CLI를 사용하는 사람)
|
||||
- 백엔드 개발자(CLI 인터페이스 및 서비스 레이어 관리자)
|
||||
- 제약사항
|
||||
- CLI는 기존 `PortfolioService.list_portfolios`와 `UserService` (또는 user repository)를 재사용해야 한다. 별도 API 호출 없이 직접 DB를 통해 username → user_id 변환이 이루어진다.
|
||||
- `pfhub_DB_ENABLED=false`(in-memory) 모드에서도 동일한 인터페이스 검증이 적용되어야 한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 관리자가 특정 사용자의 포트폴리오만 확인하기 위해 `pfhub portfolio list --user alice`를 실행한다.
|
||||
- 관리자가 `--user` 없이 `pfhub portfolio list`를 실행했을 때 "Error: --user 옵션이 필요합니다" 형태의 메시지를 받고 올바른 사용법을 인지한다.
|
||||
- 존재하지 않는 username으로 `pfhub portfolio list --user unknown`을 실행하면 사용자를 찾을 수 없다는 오류 메시지와 함께 종료된다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- CLI `portfolio list` 명령에 `--user <username>` 필수 옵션을 추가하고, 미입력 시 에러로 종료한다.
|
||||
- CLI 내부에서 username → owner_user_id(UUID) 변환 로직을 추가한다(user repository 또는 user service 직접 조회).
|
||||
- `PortfolioService.list_portfolios` 호출 시 변환된 `owner_user_id`를 항상 전달하도록 CLI 호출부를 수정한다.
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `app/cli/commands/portfolio.py` — `list_portfolios` 명령 시그니처 및 서비스 호출부 수정
|
||||
- `app/repositories/postgres.py` — `get_user_by_username`(line 419) 직접 호출. `UserService`에는 해당 메서드 없음 — repository를 통해 직접 조회
|
||||
- 문서 영향:
|
||||
- `README.md` — `pfhub portfolio list` 예제 라인에 `--user` 옵션 추가
|
||||
- `docs/runtime-flows.md` — CLI portfolio list 흐름 갱신(있을 경우)
|
||||
- 운영 영향:
|
||||
- 기존 스크립트에서 `pfhub portfolio list`를 `--user` 없이 호출하면 즉시 에러로 변경됨 → 스크립트 마이그레이션 필요
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: CLI `pfhub portfolio list` 명령에 `--user <username>` 옵션을 필수로 추가한다. `--user` 없이 실행하면 오류 메시지를 출력하고 비정상 종료(exit code 1)한다.
|
||||
- FR-2: CLI 내부에서 `--user`로 전달받은 username을 user 저장소에서 조회하여 `owner_user_id`(UUID)를 얻고, 이를 `PortfolioService.list_portfolios`의 `owner_user_id` 인자로 전달한다.
|
||||
- FR-3: `--user`로 전달된 username이 존재하지 않으면 "사용자를 찾을 수 없습니다" 형태의 오류 메시지를 출력하고 비정상 종료한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
- NFR-1: username → user_id 변환 실패 메시지는 기존 CLI 오류 출력 형식(Rich Console 또는 typer.echo)과 일관성을 유지한다.
|
||||
- NFR-2: `--user` 필수화는 in-memory 모드(`pfhub_DB_ENABLED=false`)에서도 동일하게 적용되어야 하며, 테스트에서 검증 가능해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- CLI `portfolio list` 명령의 `--user` 필수 옵션화
|
||||
- username → owner_user_id 변환 로직 (user repo/service 직접 조회)
|
||||
- README 예제 수정
|
||||
- Out of Scope
|
||||
- HTTP API `GET /portfolios`의 접근 통제 변경 (현행 유지)
|
||||
- `portfolio list`의 `--all` 관리자 전체 조회 옵션 추가 (후속 Discovery로 분리)
|
||||
- CLI에 HTTP API 방식의 `_ownership_required` 설정 플래그 도입 (이번 반복 외)
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (Medium): 기존 스크립트 또는 자동화에서 `pfhub portfolio list`를 `--user` 없이 호출하는 경우, 변경 후 즉시 에러로 중단된다.
|
||||
- 완화 방안: README에 변경 사항을 명확히 기재하고, 커밋 메시지에 breaking change 표기.
|
||||
- R-2 (Low): ~~in-memory 모드에서 username 조회를 위한 user repo 인터페이스가 충분히 구현되어 있지 않을 수 있다.~~ → **해소**: `FakeRepo.get_user_by_username`(`tests/test_cli.py:109`)과 `PortfolioRepository` Protocol stub(`portfolio_repository.py:16`) 모두 존재 확인.
|
||||
- 완화 방안: 해소 완료 — 추가 구현 불필요.
|
||||
|
||||
## 가정
|
||||
- A-1: CLI는 관리자 권한으로만 실행된다는 기존 설계를 유지한다. 따라서 `--user`로 타인의 포트폴리오도 조회 가능하며 CLI 레벨의 추가 권한 검사는 이번 범위에서 제외한다.
|
||||
- A-2: `get_user_by_username`이 이미 `PortfolioRepository` Protocol(`portfolio_repository.py:16`), `PostgresRepo`(`postgres.py:419`), `FakeRepo`(`tests/test_cli.py:109`) 모두에 구현되어 있음이 확인되었다.
|
||||
- A-3: `PortfolioService.list_portfolios`의 시그니처는 변경하지 않는다. CLI 호출부에서만 `owner_user_id`를 전달한다.
|
||||
|
||||
## 오픈 질문
|
||||
- OQ-1: in-memory FakeRepo에서 username → user_id 조회 메서드가 없는 경우 테스트를 어떻게 구성할지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: `tests/test_cli.py`의 `FakeRepo`에 `get_user_by_username(username) -> User | None` 메서드가 이미 구현되어 있음(line 109). `_users` dict에서 username 일치 항목을 선형 탐색하여 반환한다. 추가 stub 없이 기존 메서드를 그대로 사용하면 된다. `app/repositories/postgres.py`에도 동일 시그니처(line 419)가 구현되어 있어 in-memory/DB 모드 모두 지원 가능하다.
|
||||
- 종료 조건: 저장소 현황 검토 완료 — FakeRepo 및 PostgresRepo 양쪽 모두 `get_user_by_username` 존재 확인됨.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: `pfhub portfolio list` (without `--user`) 실행 시 exit code 1과 함께 오류 메시지가 출력된다.
|
||||
- S-2: `pfhub portfolio list --user alice` 실행 시 alice의 `owner_user_id`로 필터된 포트폴리오 목록만 반환된다.
|
||||
- S-3: 존재하지 않는 username으로 실행 시 "사용자를 찾을 수 없습니다" 형태의 오류 메시지와 함께 비정상 종료된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: pytest에서 `CliRunner.invoke`로 `--user` 없는 호출의 exit_code == 1 확인 / 데이터 출처: `tests/test_cli.py`
|
||||
- S-2 측정 방식: FakeRepo/DB에 복수 사용자 포트폴리오 세팅 후 CLI 결과에 alice 소유 항목만 존재하는지 검증 / 데이터 출처: `tests/test_cli.py`
|
||||
- S-3 측정 방식: 존재하지 않는 username 입력 시 exit_code != 0 및 오류 문자열 포함 여부 검증 / 데이터 출처: `tests/test_cli.py`
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: `--user` 필수화 In Scope, `--all` 옵션/HTTP API 변경 Out of Scope — #5에 명시. confirm-discovery 실행으로 확정.
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1/S-2/S-3 각각 pytest `CliRunner.invoke` 기반 검증 케이스로 기술됨. #7 참조.
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: 리스크 중 High 등급 없음(R-1: Medium, R-2: Low → 해소). R-1 완화 방안(README 명기, breaking change 표기)을 문서에 명시.
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ-1 Answered 처리 완료.
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 프로젝트 오너가 confirm-discovery 실행으로 확정. 2026-05-02.
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-003_20260502_portfolio-list-user-required.md
|
||||
- 참조 문서:
|
||||
- app/cli/commands/portfolio.py
|
||||
- app/services/portfolio_service.py
|
||||
- app/repositories/postgres.py
|
||||
- app/api/portfolios.py
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- ~~{{DECIDE_ALL_OPTION}}~~ → Resolved: `--all` 옵션은 #5 Out of Scope로 결정됨. 후속 Discovery로 분리.
|
||||
|
||||
## CONFIRM
|
||||
- ~~{{CONFIRM_BREAKING_CHANGE}}~~ → Resolved: R-1(Medium)에 문서화됨. 완화 방안(README 명기, breaking change 표기) 채택. confirm-discovery 실행으로 인지 확인.
|
||||
- ~~{{CONFIRM_SCOPE}}~~ → Resolved: In Scope / Out of Scope가 #5에 명시됨. confirm-discovery 실행으로 확정.
|
||||
|
||||
## DATA
|
||||
- ~~{{DATA_FAKEREPO_USERNAME_LOOKUP}}~~ → Answered: `FakeRepo.get_user_by_username`(`tests/test_cli.py:109`) 및 `PostgresRepo.get_user_by_username`(`app/repositories/postgres.py:419`) 모두 존재 확인 완료.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 저장소 현황 재확인 완료. 모든 C 유형 플레이스홀더 해소, OQ Open 없음, #8 체크리스트 전항 완료. REQ 초안 작성 준비 완료.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-05-02T09:00:00+09:00
|
||||
- 범위 참조: [# 5. 요구사항 목록](#5-요구사항-목록)
|
||||
- 성공 기준 참조: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 리스크 참조: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
-240
@@ -1,240 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution
|
||||
- 이슈명: portfolio_id UUID 교체 및 CLI 사용자 범위 기반 자산 식별 구조 전환
|
||||
- 작성 시각(KST): 2026-05-11 00:00
|
||||
- 마지막 갱신 시각(KST): 2026-05-11 (draft-req 완료 — REQ-016~019 생성)
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조:
|
||||
- [REQ-016](../srs/Migration/req-016_portfolios-uuid-schema-migration.md) — portfolios 테이블 UUID 전환 및 데이터 마이그레이션 (FR-1, FR-5, NFR-1)
|
||||
- [REQ-017](../srs/Interface/req-017_portfolio-cli-name-user-identification.md) — CLI portfolio 명령 name+user 조합 식별 방식 전환 (FR-2, FR-3, FR-3a, NFR-2)
|
||||
- [REQ-018](../srs/Interface/req-018_portfolio-http-api-uuid-path-param.md) — HTTP API portfolio path parameter UUID 전환 및 v1 asset_id UUID 반환 (FR-4)
|
||||
- [REQ-019](../srs/Documentation/req-019_portfolio-uuid-baseline-docs-update.md) — portfolio UUID 전환 관련 baseline 문서 갱신 (FR-6, NFR-3)
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 계약 문서: docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md (A-3: asset_id는 portfolio_id를 그대로 사용)
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- 현재 `portfolio_id`는 사용자가 직접 지정하는 전역 고유 문자열이어서 다중 사용자 환경에서 이름 충돌이 발생하고, CLI 뮤테이션 명령이 소유권 검증 없이 portfolio_id만으로 실행된다.
|
||||
- 이번 Discovery는 `portfolio_id`를 서버 생성 UUID로 교체하고 사용자 표시용 `name`을 `(owner_user_id, name)` 복합 유니크 제약으로 관리하는 구조 전환 전체 범위를 정의한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 dcy-001은 DB 마이그레이션 검증, dcy-002는 trex-basic 소비자 API 계약, dcy-003은 portfolio list --user 필수화를 다루며, portfolio_id 식별자 체계 전환을 다루는 Discovery가 없다.
|
||||
- `portfolio_id`를 UUID로 교체하고 `(owner_user_id, name)` 복합 유니크 제약을 도입하는 것은 DB 스키마, 도메인 모델, CLI 인터페이스, HTTP API 경로, 소비자 계약 전반에 영향하는 독립적인 구조 변경 주제다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `portfolios.portfolio_id`는 TEXT PRIMARY KEY로 사용자가 직접 지정하는 전역 단일 네임스페이스다. alice가 `005930:tv`를 먼저 생성하면 bob은 동일 이름을 사용할 수 없다.
|
||||
- CLI 뮤테이션 명령(`buy`, `sell`, `invest`, `transfer`, `reset`, `delete`, `status`, `update`)은 `portfolio_id`를 Argument로만 받고 소유자 검증을 하지 않는다. 권한 있는 운영자가 다른 사용자 소유의 portfolio를 portfolio_id만 알면 조작할 수 있다.
|
||||
- `ledger_entries`(portfolio_ledger), `portfolio_comments`, `portfolio_tags`, `ledger_trades` 테이블이 `portfolio_id` TEXT를 FK로 참조한다.
|
||||
- dcy-002 A-3에 따라 `GET /v1/assets`의 `asset_id` 응답 필드가 `portfolio_id`를 그대로 반환하므로, trex-basic 데몬이 `asset_id`로 ledger 기록 등 후속 API를 호출한다.
|
||||
- `Portfolio` dataclass(`app/domain/models.py`)와 PostgreSQL 레포지터리 전반이 `portfolio_id` 문자열을 기준으로 설계되어 있다.
|
||||
- 기대 상태
|
||||
- `portfolios.portfolio_id`는 서버가 발급하는 UUID(TEXT 또는 UUID 타입)로 교체된다. 사용자는 create 시 `name`만 지정하면 된다.
|
||||
- `name` 컬럼은 `(owner_user_id, name)` 복합 UNIQUE 제약으로 같은 사용자 내에서 유일하게 관리된다. 다른 사용자는 동일 `name`을 사용할 수 있다.
|
||||
- CLI 뮤테이션 명령은 `--user <username> <name>` 조합으로 portfolio를 식별하고, 내부적으로 UUID를 조회해 처리한다. 소유권 검증이 명령 실행 경로에 포함된다.
|
||||
- HTTP API 경로는 UUID를 path parameter로 사용하며, `GET /v1/assets`의 `asset_id`도 UUID를 반환한다.
|
||||
- 기존 데이터는 마이그레이션으로 새 UUID를 할당받고, 기존 `portfolio_id` 문자열은 `name` 컬럼으로 이전된다.
|
||||
- 이해관계자
|
||||
- portfolio-hub 유지보수 담당자 (구현 주체)
|
||||
- trex-basic 팀 (GET /v1/assets asset_id 필드 변경 영향을 받는 소비자)
|
||||
- CLI 운영자 (명령 인터페이스 변경 대상)
|
||||
- 제약사항
|
||||
- 기존 Alembic revision chain을 깨지 않는 IF NOT EXISTS 기반 migration을 추가한다(dcy-001/REQ-002 정책 준수).
|
||||
- dcy-002 A-3("asset_id는 portfolio_id를 그대로 사용")은 이번 Discovery에서 철회된다. trex-basic 팀에게 breaking change를 사전 통보해야 한다.
|
||||
- `name`은 owner 내에서 유일하며, 허용 문자는 `[a-zA-Z0-9._:-]`, 공백 불허, 1~100자, 대소문자 구분으로 확정됐다 (DATA_NAME_ALLOWED_CHARS Answered 2026-05-11).
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 운영자가 `pfhub portfolio create tv --symbol 005930 --user alice --cash-balance 10000000 --force`로 포트폴리오를 생성하면 서버가 UUID를 부여하고, 이후 `pfhub portfolio buy tv --user alice --quantity 10 --price 71000 --force`로 alice의 `tv` 포트폴리오를 정확히 조작한다. bob이 같은 이름 `tv`를 가진 포트폴리오를 생성해도 독립된 레코드로 관리된다.
|
||||
- trex-basic 데몬이 `GET /v1/assets`를 호출하면 `asset_id`로 UUID를 수신하고, 이후 `POST /v1/assets/{asset_id}/ledger/trades`에 UUID를 사용해 체결 내역을 기록한다. UUID 도입으로 asset_id가 전역 유일하게 보장된다.
|
||||
- 기존에 `005930:tv`라는 portfolio_id로 운영 중인 데이터가 있을 때, 마이그레이션 후 기존 레코드의 `name`이 `005930:tv`로 이전되고 새 UUID가 `portfolio_id`로 할당된다. ledger, comments 등 FK 참조도 새 UUID로 함께 갱신된다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- `portfolios.portfolio_id` PRIMARY KEY를 서버 생성 UUID로 교체하고 `name` TEXT 컬럼을 신규 추가: `(owner_user_id, name)` 복합 UNIQUE 제약 적용, 기존 portfolio_id 문자열을 name으로 이전하는 데이터 마이그레이션 포함
|
||||
- CLI 뮤테이션 명령 식별 방식 전환: 모든 뮤테이션 명령에서 `portfolio_id` Argument를 `<name>` + `--user <username>` 조합으로 교체하고, 내부에서 `(owner_user_id, name)` → UUID 조회 후 처리
|
||||
- HTTP API 경로 및 GET /v1/assets asset_id 전환: path parameter와 응답 필드를 UUID 기반으로 변경, dcy-002 A-3 철회 및 trex-basic 계약 갱신
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `migrations/versions/`: portfolios PK 교체, name 컬럼 추가, (owner_user_id, name) UNIQUE 제약, 기존 데이터 이전 마이그레이션
|
||||
- `app/domain/models.py`: Portfolio dataclass portfolio_id → UUID, name 필드 명확화
|
||||
- `app/repositories/postgres.py`: create_portfolio(서버 UUID 생성), get_portfolio_by_name_and_user() 신규, 모든 portfolio_id 참조 업데이트
|
||||
- `app/domain/services.py`: transfer, buy, sell 등 서비스 메서드 시그니처 — (user_id, name) → UUID 해석 추가
|
||||
- `app/cli/commands/portfolio.py`: status, buy, sell, invest, transfer, reset, delete, update 명령의 Argument 교체
|
||||
- `app/api/portfolios.py` (HTTP API): path parameter UUID 기반 전환
|
||||
- `app/api/v1_assets.py`: asset_id 응답 필드를 UUID로 반환
|
||||
- `app/schemas/portfolio.py`, `app/schemas/v1.py`: asset_id/portfolio_id 필드 타입 문서화
|
||||
- 문서 영향:
|
||||
- `docs/data-model.md`: portfolios 테이블 스키마 변경 반영
|
||||
- `docs/interface-contract.md`: GET /v1/assets asset_id 변경 반영
|
||||
- `docs/project-structure.md`: 갱신 필요 (repositories, services 변경 반영)
|
||||
- `docs/runtime-flows.md`: 갱신 필요 (CLI 명령 흐름, v1 API 흐름 변경 반영)
|
||||
- `docs/discovery/dcy-002_*`: A-3 철회 주석 추가
|
||||
- 운영 영향:
|
||||
- trex-basic 팀에게 asset_id 필드값이 UUID로 바뀜을 사전 통보 필요
|
||||
- CLI 운영 스크립트에서 portfolio_id 직접 지정 방식을 `--user + name` 방식으로 교체 필요
|
||||
- 기존 운영 runbook 갱신 필요
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: `portfolios.portfolio_id`를 서버 생성 UUID(TEXT)로 교체한다. 기존 사용자 지정 portfolio_id 문자열은 `portfolios.name` TEXT 컬럼으로 이전한다. `(owner_user_id, name)` 복합 UNIQUE 제약을 DB 레벨에서 보장한다.
|
||||
- FR-2: `portfolio create` CLI 명령은 `name` Argument와 `--user` 옵션을 필수로 받고, `portfolio_id`는 서버가 UUID로 발급한다. 사용자는 UUID를 직접 지정하거나 조회할 수 없다.
|
||||
- FR-3: CLI 뮤테이션 명령(`status`, `buy`, `sell`, `invest`, `transfer`, `reset`, `delete`, `update`)은 `<name>` Argument와 `--user <username>` 옵션으로 portfolio를 식별한다. 내부에서 `(owner_user_id, name)` → `portfolio_id(UUID)`를 조회한 뒤 처리한다. `name` + `--user` 조합으로 portfolio를 찾지 못하면 명확한 오류를 반환한다.
|
||||
- FR-3a: `name`은 생성 후 변경 가능하다. 단, 변경 시 동일 `owner_user_id` 내에 같은 `name`이 이미 존재하면 DB의 `(owner_user_id, name)` UNIQUE 제약으로 변경이 거부되고, 서비스 레이어는 사용자에게 명확한 오류 메시지(`이미 사용 중인 이름입니다: {name}`)를 반환한다.
|
||||
- FR-4: HTTP API의 portfolio 관련 path parameter를 UUID 기반으로 전환한다. `GET /v1/assets` 응답의 `asset_id` 필드는 UUID를 반환한다. dcy-002 A-3을 이번 Discovery에서 철회한다.
|
||||
- FR-5: 기존 데이터 마이그레이션 — 현재 `portfolio_id` 문자열 값을 `name` 컬럼으로 복사하고, 각 레코드에 새 UUID를 `portfolio_id`로 할당한다. `portfolio_ledger`, `portfolio_comments`, `portfolio_tags`, `ledger_trades` 등 FK 참조 테이블의 portfolio_id도 새 UUID로 함께 갱신한다.
|
||||
- FR-6: `docs/project-structure.md`와 `docs/runtime-flows.md`를 이번 변경 내용에 맞게 갱신한다(repositories/services 인터페이스 변경, CLI 흐름, v1 API 흐름 변경 반영).
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: 데이터 마이그레이션은 단일 Alembic revision으로 수행하며, IF NOT EXISTS 패턴을 준수해 재실행 시 오류가 없어야 한다(REQ-002 준수). FK 참조 테이블과의 갱신은 트랜잭션 내에서 atomic하게 처리한다.
|
||||
- NFR-2: CLI `(owner_user_id, name)` → UUID 조회는 단일 DB 쿼리로 처리하며, 조회 실패 시 명확한 오류 메시지(`포트폴리오를 찾을 수 없습니다: {name} (user: {username})`)를 출력해야 한다.
|
||||
- NFR-3: trex-basic 계약 변경(asset_id UUID 전환)은 portfolio-hub와 trex-basic을 동시에 배포하며 처리한다. 변경 일정 및 동시 배포 계획을 docs/interface-contract.md에 기록해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- `portfolios.portfolio_id` UUID 교체 및 `name` 컬럼 추가
|
||||
- `(owner_user_id, name)` 복합 UNIQUE 제약
|
||||
- 기존 데이터 마이그레이션 (FK 참조 포함)
|
||||
- CLI 뮤테이션 명령 식별 방식 전환 (`--user + name`)
|
||||
- HTTP API path parameter UUID 전환
|
||||
- `GET /v1/assets` asset_id UUID 반환
|
||||
- `docs/project-structure.md`, `docs/runtime-flows.md` 갱신
|
||||
- `docs/interface-contract.md`, `docs/data-model.md` 갱신
|
||||
- Out of Scope
|
||||
- trex-basic 측 코드 변경 — portfolio-hub API 변경만 담당
|
||||
- `name` 변경 이력 감사 로그 — 향후 반복으로 Deferred
|
||||
- `name` 허용 문자 유효성 검사 정책 — 확정됨. `[a-zA-Z0-9._:-]` 허용, 공백 불허, 1~100자, 대소문자 구분 (2026-05-11)
|
||||
- HTTP API에서 `(username, name)` 조합 조회 엔드포인트 추가 — 추가하지 않음. HTTP API는 `portfolio_id`(UUID)만으로 조회 (OQ-3 Answered 2026-05-11)
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): 기존 FK 참조 테이블(`portfolio_ledger`, `portfolio_comments`, `portfolio_tags`, `ledger_trades`)의 portfolio_id를 새 UUID로 일괄 갱신하는 마이그레이션 중 오류 발생 시 데이터 불일치가 생길 수 있다.
|
||||
- 완화 방안: 마이그레이션 전 DB 전체 백업 수행. 마이그레이션 스크립트를 단일 트랜잭션으로 작성하고, 검증 쿼리(FK 고아 레코드 0건 확인)를 마이그레이션 스크립트 마지막에 실행한다.
|
||||
- R-2 (High): `GET /v1/assets` `asset_id`가 UUID로 바뀌면 trex-basic 데몬의 `POST /v1/assets/{asset_id}/ledger/trades` 경로가 즉시 동작하지 않는다. 배포 타이밍 불일치 시 운영 장애가 발생한다.
|
||||
- 완화 방안: trex-basic과 portfolio-hub를 동시에 배포한다(OQ-1 Answered). 임시 호환 레이어 불필요. 두 저장소의 배포 파이프라인에서 릴리즈 타이밍을 조율한다.
|
||||
- R-3 (Medium): `name`이 mutable이므로 CLI 스크립트나 외부 참조에서 portfolio를 name으로 식별하던 코드가 name 변경 후 깨질 수 있다.
|
||||
- 완화 방안: name 변경 CLI 명령에 명시적 확인 절차(--force 또는 confirm 프롬프트)를 요구한다. DB의 `(owner_user_id, name)` UNIQUE 제약이 같은 사용자 내 중복 name 변경을 자동으로 차단한다(OQ-2 Answered).
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: `portfolio_id`(UUID)는 서버에서 `uuid.uuid4()`로 생성하며, 사용자가 직접 지정하거나 예측할 수 없다.
|
||||
- A-2: `name`은 `(owner_user_id, name)` 복합 UNIQUE 제약을 만족하면 되며, 전역 유일성은 요구하지 않는다.
|
||||
- A-3: 기존 `portfolio_id` 문자열(`005930:tv` 형식)은 데이터 마이그레이션 시 `name` 컬럼 값으로 그대로 이전한다. 변환 없이 복사한다.
|
||||
- A-4: HTTP API 경로에서 `(username, name)` 조합 조회 엔드포인트는 추가하지 않는다. HTTP API는 `portfolio_id`(UUID)만으로 portfolio를 식별한다. CLI만 `(owner_user_id, name)` → UUID 조회 경로를 사용한다(OQ-3 Answered 2026-05-11).
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: trex-basic 계약 호환성 전략 — `GET /v1/assets` asset_id가 UUID로 바뀔 때 trex-basic 측의 대응 준비 일정이 확정되기 전에 portfolio-hub를 먼저 배포할 수 있는지, 아니면 임시 호환 레이어(name으로 fallback 조회)가 필요한지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: trex-basic과 portfolio-hub를 동시에 변경·배포한다. 임시 호환 레이어 불필요.
|
||||
- 종료 조건: 완료 — 2026-05-11 사용자 확인으로 Answered 처리.
|
||||
- OQ-2: `name` mutable 정책 — 생성 이후 name 변경을 허용할지 여부. 허용할 경우 기존 CLI 스크립트 참조 안정성 확보 방안.
|
||||
- 상태: Answered
|
||||
- 처리 방안: name 변경 허용. 단, 동일 사용자 내 같은 name이 존재하면 DB의 `(owner_user_id, name)` UNIQUE 제약으로 변경 거부. FR-3a에 반영 완료.
|
||||
- 종료 조건: 완료 — 2026-05-11 사용자 확인으로 Answered 처리.
|
||||
- OQ-3: HTTP API에서 `(username, name)` 조합으로 portfolio를 조회하는 엔드포인트 추가 여부 — 외부 API 소비자가 UUID를 직접 관리해야 하는 부담을 줄이기 위해 `/portfolios?user=alice&name=tv` 형태의 조회 경로가 필요한지.
|
||||
- 상태: Answered
|
||||
- 처리 방안: 추가하지 않음. HTTP API는 `portfolio_id`(UUID)만으로 조회. A-4, Out of Scope 반영 완료.
|
||||
- 종료 조건: 완료 — 2026-05-11 사용자 확인으로 Answered 처리.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: `pfhub portfolio create tv --symbol 005930 --user alice --force` 실행 시 UUID가 portfolio_id로 발급되고, alice와 bob이 동일 name `tv`로 각각 portfolio를 생성할 수 있다.
|
||||
- S-2: `pfhub portfolio buy tv --user alice --quantity 10 --price 71000 --force`가 alice 소유의 `tv` portfolio에만 적용되고, `--user bob`으로는 alice의 portfolio를 조작할 수 없다.
|
||||
- S-3: 마이그레이션 후 기존 데이터의 `portfolio_ledger`, `portfolio_comments` 등 FK 참조 테이블에 고아 레코드(orphan)가 0건이고, `GET /v1/assets` 응답의 `asset_id`가 UUID 형식이다.
|
||||
- S-4: `docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`, `docs/interface-contract.md`가 변경 내용을 반영해 갱신된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: pytest — portfolio create 후 portfolio_id가 UUID 형식(`re.match(r'^[0-9a-f-]{36}$', ...)`)임을 검증. alice/bob 동일 name 독립 생성 테스트 / 데이터 출처: `tests/test_cli.py` 또는 신규 `tests/test_portfolio_uuid.py`
|
||||
- S-2 측정 방식: pytest — `--user bob`으로 alice 소유 portfolio 조작 시 오류 반환 검증 / 데이터 출처: `tests/test_cli.py` 소유권 검증 케이스
|
||||
- S-3 측정 방식: 마이그레이션 스크립트 내 검증 쿼리 실행 결과 0건 확인 + pytest `test_v1.py::test_v1_assets_*` asset_id UUID 형식 검증 / 데이터 출처: `migrations/versions/` 마이그레이션 스크립트, `tests/test_v1.py`
|
||||
- S-4 측정 방식: 각 baseline 문서에 CLI 흐름, repositories/services 인터페이스, v1 API asset_id UUID 내용이 포함됐는지 코드 리뷰로 확인 / 데이터 출처: `docs/project-structure.md`, `docs/runtime-flows.md`, `docs/data-model.md`, `docs/interface-contract.md`
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: OQ-3(HTTP API name 조회 엔드포인트) 결정 완료, Out of Scope 경계 승인
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-4 측정 방식이 pytest 또는 코드 리뷰로 추적 가능한 형태로 기술됨
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1(마이그레이션 데이터 무결성), R-2(trex-basic 계약 파괴) 완화 방안 확정 및 담당자 지정
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: Justin 또는 지정된 승인자가 confirm-discovery skill로 Freeze 확정
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성
|
||||
- 생성 경로: docs/discovery/dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution.md
|
||||
- 참조 문서:
|
||||
- docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md (A-3 철회 대상)
|
||||
- docs/discovery/dcy-003_20260502_portfolio-list-user-required.md (--user 필수화 선행 작업)
|
||||
- app/domain/models.py
|
||||
- app/cli/commands/portfolio.py
|
||||
- app/repositories/postgres.py
|
||||
- migrations/versions/20260403_0001_initial_portfolio_schema.py
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- ~~{{DECIDE_NAME_MUTABILITY}}~~: **Answered** — name mutable 허용. 동일 사용자 내 중복 name 변경은 DB UNIQUE 제약으로 거부. FR-3a 반영 완료. (OQ-2 2026-05-11)
|
||||
- ~~{{DECIDE_TREX_COMPAT_STRATEGY}}~~: **Answered** — trex-basic과 portfolio-hub 동시 배포로 확정. 임시 호환 레이어 불필요. (OQ-1 2026-05-11)
|
||||
|
||||
## CONFIRM
|
||||
- ~~{{CONFIRM_API_NAME_LOOKUP_SCOPE}}~~: **Answered** — `(username, name)` 조합 엔드포인트 추가 안 함. HTTP API는 `portfolio_id`(UUID)만으로 조회. (OQ-3 2026-05-11)
|
||||
- ~~{{CONFIRM_MIGRATION_STRATEGY}}~~: **Answered** — 기존 `portfolio_id` 문자열을 `name` 컬럼으로 변환 없이 그대로 복사. (A-3 2026-05-11)
|
||||
|
||||
## DATA
|
||||
- ~~{{DATA_NAME_ALLOWED_CHARS}}~~: **Answered** — `name` 허용 문자 `[a-zA-Z0-9._:-]`(영숫자 + `.` `_` `:` `-`), 공백 불허, 1~100자, 대소문자 구분 있음. (2026-05-11)
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 모든 OQ Answered 완료, 모든 CONFIRM/DATA 항목 확정. confirm-discovery 게이트 통과 — REQ 초안 작성으로 인계 가능.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-05-11
|
||||
@@ -1,236 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-005_20260511_production-admin-bootstrap-and-operator-access
|
||||
- 이슈명: 실배포 admin bootstrap 및 operator access 경로 정립
|
||||
- 작성 시각(KST): 2026-05-11 23:12
|
||||
- 마지막 갱신 시각(KST): 2026-05-12 14:50
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: REQ-020, REQ-021, REQ-022
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 TODO/메모: docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md
|
||||
- 관련 운영 문서: README.md "Production Operator Runbook"
|
||||
- 해석:
|
||||
- REL-001은 released 상태로 전환됐지만, admin 성공 경로와 trade-write 성공 경로는 후속 운영 smoke로 닫아야 한다. 본 문서는 production smoke를 canonical acceptance로, staging smoke를 optional preflight로 두는 기준과 evidence 기록 위치를 고정한다.
|
||||
- 이번 Discovery는 release blocker 해소보다는 released 이후 운영 hardening 항목으로서, 실배포 admin bootstrap, operator auth path, smoke credential provenance, 로그 접근 절차, 운영 책임 역할을 REQ handoff 가능한 수준으로 정리하기 위한 문서다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 dcy-001~004는 DB 검증, trex-basic consumer API, CLI `portfolio list --user`, portfolio UUID 전환을 다루며, 실배포 admin bootstrap 및 operator access를 독립 주제로 다루는 Discovery는 없다.
|
||||
- 이번 요청은 제품 기능 추가보다 운영 인증 경로, bootstrap 계정/토큰 provenance, release smoke credential 관리라는 별도 문제 정의를 가진다.
|
||||
|
||||
> **판정이 `완전 중복`인 경우**: 이하 섹션 작성을 중단하고 `# 9. 파일 처리 결과`에 중복 판정 사유와 참조 문서 경로를 기록한 뒤 문서를 닫는다. REQ 초안 작성으로 넘기지 않는다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- 실배포 URL `https://fourblocks2026.com/portfolio-hub/`에서 `GET /health`, `/docs`, `/openapi.json`은 정상 응답했고, read service-token으로 `GET /v1/assets`, `GET /v1/user_info`까지는 검증됐다.
|
||||
- 동일 토큰으로 `POST /auth/service-tokens`를 호출하면 `403 Insufficient permission`이 반환됐다. 이후 README에 Production Operator Runbook 초안이 추가되어 canonical admin path, smoke credential ownership, log path 기준선은 정리됐고, 후속 smoke는 production을 canonical acceptance 환경으로 수행하며 staging은 optional preflight로만 사용하기로 정리했다. 실행 evidence는 해당 release 문서에 남기고, 절차의 source of truth는 README runbook으로 유지한다.
|
||||
- `app/core/auth.py`는 env token 집합 또는 DB `auth_tokens` principal의 `permission`으로 권한을 해석하고, `app/api/auth_routes.py`의 `POST /auth/service-tokens`는 `require_admin`을 선행한다. 즉 운영자가 어떤 credential을 admin으로 다뤄야 하는지가 명확해야 smoke가 가능하다.
|
||||
- 현재 저장소 기준선상 프로덕션은 Linux/Ubuntu host에서 systemd로 `portfoliohub` uvicorn 서비스를 띄우고 Caddy를 reverse proxy로 사용하며, secret material은 host-side `EnvironmentFile=/opt/portfoliohub/.env`에서 주입된다. secret leakage 대응 owner는 repository-maintainers 역할 중 deployment secret custodian으로 두고, admin recovery owner는 production operator/release approver 역할로 둔다.
|
||||
- 기대 상태
|
||||
- 운영자는 실배포에서 admin auth context를 얻는 표준 경로를 알고 있어야 하며, 그 경로로 `POST /auth/service-tokens` 성공 smoke를 반복 가능하게 수행할 수 있어야 한다.
|
||||
- release 검증에는 public/no-auth, read service-token, trade-write service-token, admin auth context의 용도와 provenance가 문서로 분리돼 있어야 한다.
|
||||
- startup log, request log, auth failure(401), permission failure(403), resource failure(404)를 운영자가 구분해 증거로 남길 수 있어야 한다.
|
||||
- 이해관계자
|
||||
- repository-maintainers
|
||||
- 실배포 운영자 / 배포 승인자
|
||||
- trex-basic 연동 운영자
|
||||
- 보안 또는 secret 관리 담당자
|
||||
- 제약사항
|
||||
- 실제 credential 원문은 저장소, release 문서, Discovery 문서에 기록하지 않는다.
|
||||
- 기존 auth model(`read`, `trade-write`, `admin` 권한과 DB-issued service-token 구조)은 유지하되, 필요한 경우 bootstrap/runbook 또는 최소한의 운영 지원 기능만 추가한다.
|
||||
- REL-001이 released 전환된 이후의 후속 운영 hardening 범위를 다루므로, 이 Discovery는 운영 접근성과 검증 절차를 정리하는 데 집중하고 기존 consumer contract 범위를 임의로 확장하지 않는다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 운영자가 REL-001 배포 후 smoke를 수행할 때, admin 경로로 service token을 발급하고 read/trade-write 토큰을 분리해 `/auth/service-tokens`, `/v1/assets`, `/v1/user_info`, `/v1/assets/{asset_id}/ledger/trades`를 순서대로 검증한다.
|
||||
- 장애 또는 신규 환경 bootstrap 시 운영자가 `PORTFOLIOHUB_BOOTSTRAP_*` 기반 초기 admin 계정 생성 경로와 steady-state admin recovery 경로를 구분해 사용한다.
|
||||
- release evidence 작성 시 운영자가 raw secret 없이 token provenance, 권한 수준, 로그 접근 경로만 기록해 보안 사고 없이 검증 내역을 남긴다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- 실배포 admin bootstrap 및 steady-state operator access 경로를 정의한다. steady-state canonical path는 운영자 소유 admin 사용자 계정으로 로그인해 확보한 DB-issued admin session 또는 동일 컨텍스트에서 발급한 client token으로 두고, 해당 admin context로 release smoke용 read/trade-write service-token을 발급한다. `PORTFOLIOHUB_BOOTSTRAP_*`는 새 프로덕션 환경에서 `users` 테이블이 비어 있거나 disaster recovery 직후 admin 사용자 부재가 확인된 경우에만 허용하고, steady-state recovery는 운영자 개인 admin 로그인 경로를 우선 사용한다. env admin token은 로그인 경로가 모두 막혔을 때 `/opt/portfoliohub/.env`에서 꺼내는 break-glass fallback으로만 사용한다.
|
||||
- release 검증용 credential matrix를 정의한다. public/no-auth, read service-token, trade-write service-token, admin auth context의 용도와 발급/보관/폐기 절차를 분리한다. admin 로그인 자격증명은 운영자 개인별 소유로 두고 공유하지 않으며, read/trade-write smoke service-token은 환경 단위 운영 credential로 취급해 release approver 또는 repository-maintainers 역할이 공동 소유한다. 현재 프로덕션의 canonical secret material location은 host-side `EnvironmentFile=/opt/portfoliohub/.env`이며, raw secret은 이 경로와 동일 접근 통제 하의 운영 절차에서만 보관한다. release 문서에는 이름, token_prefix, permission, runtime_id, 발급/폐기 시각만 기록한다.
|
||||
- startup/request 로그 접근, auth/permission/resource 오류 판독, release evidence 기록 절차를 운영 runbook과 release 문서에 연결한다. canonical acceptance smoke는 production에서 수행하고, staging smoke는 optional preflight로만 사용한다. 절차의 source of truth는 README runbook으로 유지하고, 실행 evidence의 canonical record는 해당 release 문서에 남긴다. 현재 canonical log access path는 Linux/Ubuntu production host의 systemd journal에서 `portfoliohub` 서비스 로그와 Caddy reverse proxy 로그를 확인하는 경로다. 즉 `journalctl -u portfoliohub`와 `journalctl -u caddy`가 1차 운영 경로이며, Loki는 `LOKI_ENABLED`와 실제 수집 경로가 운영 환경에 명시적으로 구성된 경우에만 보조 집계 경로로 취급한다.
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `app/core/config.py` — bootstrap 환경 변수와 운영 환경 가정 재검토
|
||||
- `app/main.py` — bootstrap 초기 사용자 생성 경로의 운영 제약 재정리 필요 여부 검토
|
||||
- `app/core/auth.py`, `app/api/auth_routes.py`, `app/services/auth_service.py` — admin/read/trade-write auth path의 운영 의미 문서화 또는 최소 지원 기능 추가 가능성
|
||||
- `app/cli/commands/*` 또는 운영 보조 스크립트 — 필요 시 token inspection 또는 smoke helper 추가 검토
|
||||
- 문서 영향:
|
||||
- `README.md` — 실배포 bootstrap/admin/operator access runbook 추가 가능성
|
||||
- `docs/project-structure.md`, `docs/runtime-flows.md` — auth/bootstrap/runtime smoke 흐름 반영 가능성
|
||||
- `docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md` — release evidence와 운영 주의사항 정합화
|
||||
- 운영 영향:
|
||||
- secret manager 또는 배포 환경 변수 관리 절차 명시 필요
|
||||
- release smoke에 필요한 token 수명 주기 및 폐기 절차 정의 필요
|
||||
- 로그 접근 권한과 증적 수집 절차를 운영자 역할에 연결해야 함
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: 실배포에서 admin auth context를 확보하는 표준 경로를 정의한다. steady-state canonical path는 운영자 소유 admin 사용자 계정의 로그인으로 확보한 DB-issued admin session 또는 동일 컨텍스트에서 발급한 client token으로 하고, 이 경로로 `POST /auth/service-tokens`를 호출해 read/trade-write service-token을 발급하도록 명시한다. `PORTFOLIOHUB_BOOTSTRAP_*` 기반 bootstrap은 `users` 테이블이 비어 있는 신규 환경 또는 disaster recovery 직후 admin 사용자 부재가 확인된 경우에만 허용한다. steady-state recovery는 기존 운영자 admin 로그인 경로를 우선 사용하고, env admin token은 `/opt/portfoliohub/.env`에서만 꺼내는 break-glass fallback으로 제한한다.
|
||||
- FR-2: release 검증에 필요한 credential matrix를 정의한다. public/no-auth, read service-token, trade-write service-token, admin auth context 각각의 발급 주체, 보관 위치, 만료 정책, 폐기 절차를 문서화한다. admin 로그인 자격증명은 named operator가 개인별로 소유하고 공유하지 않으며, read/trade-write smoke service-token은 환경 단위 운영 credential로서 release approver 또는 repository-maintainers 역할이 공동 소유한다. 현재 프로덕션 secret material location은 `portfoliohub` systemd service의 `EnvironmentFile=/opt/portfoliohub/.env`로 정의하고, release evidence에는 name, token_prefix, permission, runtime_id, issued_at, revoked_at만 남긴다.
|
||||
- FR-3: REL-001과 같은 app-service release에서 startup log, request log, auth/permission/resource 오류를 수집하는 운영 runbook을 정의한다. canonical acceptance smoke는 production에서 수행하고, staging smoke는 optional preflight로만 사용한다. 절차의 source of truth는 README runbook으로 유지하고, 실행 evidence의 canonical record는 해당 release 문서에 남긴다. 기본 수집 경로는 Linux/Ubuntu production host의 systemd journal에서 `portfoliohub` uvicorn 서비스와 Caddy reverse proxy 로그를 조회하는 방식으로 하고, 구체 명령은 `journalctl -u portfoliohub`와 `journalctl -u caddy`를 사용한다. Loki는 운영 환경에서 실제 활성화가 확인된 경우에만 보조 조회 경로로 포함한다. 외부 URL만으로 로그 접근이 불가능한 점, 운영자가 어떤 콘솔 또는 CLI 권한으로 해당 로그를 조회하는지, release evidence에 남길 최소 필드(시각, 경로, 상태 코드, 소스 시스템)를 함께 문서화한다.
|
||||
- FR-4: 운영자가 DB 콘솔에 직접 들어가지 않고도 token provenance 또는 권한 수준을 확인할 수 있는 최소 지원 수단을 정의한다. 신규 관리 기능을 추가하기보다 기존 `pfhub service-token list`, `pfhub service-token issue`, `pfhub service-token rotate`, `pfhub user get`, `GET /v1/user_info`를 canonical support path로 문서화하고, 이름, token_prefix, token_id, user_id, permission, created_at, last_used_at, revoked_at, runtime_id를 provenance 확인 필드로 사용한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: raw credential, raw token, bootstrap password는 저장소와 release/discovery 문서에 남기지 않는다. 운영 문서에는 provenance와 마스킹된 메타데이터만 남긴다.
|
||||
- NFR-2: 운영 runbook은 401, 403, 404의 의미를 분리해 재현 가능하게 설명해야 하며, 권한 문제와 리소스 부재 문제를 혼동하지 않도록 해야 한다.
|
||||
- NFR-3: bootstrap 또는 recovery 경로는 idempotent해야 하며, 이미 사용자가 존재하는 환경에서 first-run bootstrap 로직이 반복 부작용을 만들지 않아야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- 실배포 admin bootstrap/steady-state access 경로 정의
|
||||
- read/trade-write/admin smoke credential 구분 및 운영 절차 정의
|
||||
- release evidence용 로그 접근 및 증적 수집 절차 정의
|
||||
- 필요한 최소 운영 지원 기능 또는 문서 변경 판정
|
||||
- Out of Scope
|
||||
- 권한 체계(`read`, `trade-write`, `admin`) 자체의 전면 재설계
|
||||
- 외부 사용자를 위한 신규 admin UI 또는 공개 운영 포털 추가
|
||||
- trex-basic consumer API 스키마 변경
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): admin/bootstrap credential을 ad-hoc 방식으로 공유하면 secret leakage가 발생할 수 있다.
|
||||
- 완화 방안: secret manager 또는 승인된 보관소만 사용하고, 저장소/문서에는 raw value를 남기지 않는다. 대응 owner는 repository-maintainers 역할 중 deployment secret custodian으로 둔다.
|
||||
- R-2 (Medium): first-run bootstrap 계정 경로만 정리하고 steady-state 운영 경로를 정의하지 않으면, 이미 사용자가 있는 실배포 환경에서는 같은 문제를 반복한다.
|
||||
- 완화 방안: bootstrap과 steady-state access/recovery를 별도 요구사항으로 분리해 정의한다. admin recovery owner는 production operator/release approver 역할로 둔다.
|
||||
- R-3 (Medium): 로그 접근 경로가 환경 의존적이면 release 증적 수집이 담당자 개인 지식에 묶인다.
|
||||
- 완화 방안: 최소한의 환경 계약(예: 어느 시스템에서 startup/request 로그를 확인하는지)을 문서화한다.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: 실배포 환경은 DB-backed `users` / `auth_tokens`를 사용하며, service-token은 DB principal 권한으로 해석된다.
|
||||
- A-2: admin 성공 경로는 현재 완전히 없는 것이 아니라, 운영자에게 문서화되지 않았거나 credential provenance가 정리되지 않은 상태다.
|
||||
- A-3: release 검증에는 최소한 public/no-auth, read service-token, trade-write service-token, admin auth context 4개 관점이 필요하다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- OQ-1: steady-state admin access의 표준 경로는 무엇인가? bootstrap admin 로그인, env admin token, DB-issued admin session/token, 별도 운영 절차 중 무엇을 canonical path로 삼을지.
|
||||
- 상태: Answered
|
||||
- 답변: steady-state canonical path는 운영자 소유 admin 사용자 계정으로 `/auth/login` 하여 확보한 DB-issued admin session 또는 동일 컨텍스트에서 발급한 client token이다. 운영자는 이 admin context로 `POST /auth/service-tokens`를 호출해 release smoke용 read/trade-write service-token을 발급한다. `PORTFOLIOHUB_BOOTSTRAP_*`는 `users` 테이블이 비어 있는 신규 환경 또는 disaster recovery 직후 admin 사용자 부재가 확인된 경우에만 사용하고, env admin token은 `/opt/portfoliohub/.env`에서만 꺼내는 break-glass fallback으로만 사용한다.
|
||||
- 처리 방안: FR-1과 운영 runbook에 canonical path, bootstrap path, break-glass fallback의 구분을 반영한다. 또한 프로덕션에 운영자용 admin 사용자 계정이 실제로 존재해야 하며, 해당 로그인 자격증명과 break-glass env token은 `portfoliohub` systemd service의 `EnvironmentFile=/opt/portfoliohub/.env` 관리 경로 안에서 통제된다는 운영 전제를 후속 문서에 명시한다.
|
||||
- 종료 조건: README 또는 후속 운영 문서에 canonical path와 fallback 절차가 반영되고, REL-001 후속 release smoke가 해당 경로로 재현되면 resolved 기록으로 이동한다.
|
||||
- OQ-2: startup/request 로그는 어디에서 확인하는가? 호스팅 환경, reverse proxy, Loki 등 실제 로그 경로와 접근 권한을 어떻게 운영자에게 부여하는가.
|
||||
- 상태: Answered
|
||||
- 답변: 현재 canonical log access path는 Linux/Ubuntu production host의 systemd journal이다. 애플리케이션 startup 및 runtime 로그는 `journalctl -u portfoliohub`, reverse proxy 또는 access 수준 로그는 `journalctl -u caddy`로 확인한다. Loki는 설정 항목은 존재하지만 현재 저장소에는 프로덕션 수집 경로와 명시적 runtime flow가 없으므로 기본 표준 경로로 간주하지 않고, 운영 환경에서 `LOKI_ENABLED`와 실제 수집 파이프라인이 확인된 경우에만 보조 집계 경로로 취급한다.
|
||||
- 처리 방안: FR-3과 운영 runbook에 로그 조회 채널을 systemd journal 기준으로 명시하고, startup log와 reverse proxy log를 각각 어떤 `journalctl` 명령으로 확인하는지 구분한다. 또한 release evidence에는 raw secret 없이 시각, 경로, 상태 코드, 소스 시스템, 상관 가능한 요청 식별자만 남긴다.
|
||||
- 종료 조건: README 또는 후속 운영 문서에 `journalctl -u portfoliohub`, `journalctl -u caddy` 기반 로그 조회 절차와 evidence 필드가 반영되고, REL-001 후속 release smoke에서 startup/request 로그 확인 근거가 같은 채널로 재현되면 resolved 기록으로 이동한다.
|
||||
- OQ-3: token provenance 확인 수단은 문서화만으로 충분한가, 아니면 최소한의 CLI/관리 기능이 필요한가.
|
||||
- 상태: Answered
|
||||
- 답변: runbook-only 접근만으로는 부족하지만, 신규 관리 기능을 추가할 필요는 없다. 현재 저장소의 기존 CLI와 API가 이미 최소 provenance 확인 수단을 제공하므로, 이를 canonical support path로 문서화하는 것으로 충분하다. 운영자는 `pfhub service-token list`로 이름, token_prefix, token_id, user_id, permission, created_at, last_used_at, revoked_at를 확인하고, 발급/rotate 시 반환되는 `runtime_id`와 `GET /v1/user_info`의 `runtime_id`를 대조해 실배포 smoke에 사용한 service-token provenance를 확인한다. `pfhub user get`은 사용자별 service token 이름 목록 확인 보조 수단으로 사용한다.
|
||||
- 처리 방안: FR-4와 운영 runbook에 기존 지원 수단의 사용 순서를 반영하고, README 또는 후속 운영 문서에서 `service-token list`와 `service-token rotate`를 명시한다. 추가 구현은 보류하고, 실제 smoke rehearsal에서 기존 수단만으로 provenance 판독이 부족할 때만 후속 REQ로 승격한다.
|
||||
- 종료 조건: README 또는 후속 운영 문서에 provenance 확인 절차와 필드가 반영되고, REL-001 후속 release smoke에서 기존 CLI/API만으로 사용 토큰의 permission 및 provenance를 재현 가능하게 확인하면 resolved 기록으로 이동한다.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: 운영자가 admin/read/trade-write/public 검증 경로를 혼동 없이 수행할 수 있는 runbook을 보유한다.
|
||||
- S-2: release 검증 시 `POST /auth/service-tokens` 성공 경로와 `POST /v1/assets/{asset_id}/ledger/trades` 권한 경로를 적절한 credential로 재현할 수 있다.
|
||||
- S-3: startup/request 로그 접근 경로와 release evidence 기록 방식이 REL 문서에 반영된다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: README runbook 리뷰 후 production smoke 기준 절차를 rehearsal하고 필요 시 staging에서 optional preflight 수행 / 데이터 출처: README.md, 해당 release 문서의 verification 기록
|
||||
- S-2 측정 방식: canonical acceptance로 production smoke에서 admin/read/trade-write 경로를 각각 실행하고, staging smoke는 사전 rehearsal로만 사용 / 데이터 출처: 해당 release 문서, HTTP 응답 로그, 운영자가 제공한 검증 기록
|
||||
- S-3 측정 방식: release 문서에 실행 evidence가 기록되고 baseline 문서에 로그 접근 및 증적 수집 항목이 반영됐는지 확인 / 데이터 출처: docs/releases/*.md, docs/runtime-flows.md, docs/project-structure.md, README.md
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: admin bootstrap, steady-state access, logging scope의 포함/제외 범위 합의
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: canonical acceptance smoke는 production, staging smoke는 optional preflight, 실행 evidence는 해당 release 문서에 기록
|
||||
- [x] 핵심 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: secret leakage owner는 repository-maintainers 역할의 deployment secret custodian, admin recovery owner는 production operator/release approver 역할
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록의 `Open` 상태가 0개
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: 운영 승인자 또는 repository-maintainers 중 확인자 지정
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성 후 갱신
|
||||
- 생성 경로: docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md
|
||||
- 참조 문서:
|
||||
- docs/releases/rel-001_20260502_trex-basic-v1-api-contract.md
|
||||
- app/core/auth.py
|
||||
- app/api/auth_routes.py
|
||||
- app/services/auth_service.py
|
||||
- README.md
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- 없음
|
||||
|
||||
## CONFIRM
|
||||
- 없음 (Confirmed By: Justin — 2026-05-12)
|
||||
|
||||
## DATA
|
||||
- baseline 반영 완료
|
||||
- canonical smoke environment: production
|
||||
- staging smoke role: optional preflight only
|
||||
- canonical evidence location: 해당 release 문서
|
||||
- procedure source of truth: README.md "Production Operator Runbook"
|
||||
- secret store location: production host의 `portfoliohub` systemd service가 참조하는 `EnvironmentFile=/opt/portfoliohub/.env`
|
||||
- production log stack: Linux/Ubuntu host + systemd-managed `portfoliohub` uvicorn service + Caddy reverse proxy, primary log commands `journalctl -u portfoliohub` 및 `journalctl -u caddy`
|
||||
- secret leakage owner: repository-maintainers 역할 중 deployment secret custodian
|
||||
- admin recovery owner: production operator/release approver 역할
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 핵심 운영 경로(OQ-1~3), smoke credential ownership policy, bootstrap 대 steady-state recovery 범위, canonical smoke/evidence 기준, secret store/log stack 기준선, 운영 책임 역할 모두 반영 완료. Freeze confirmer Justin 지정 완료. 오픈 질문 0개, C 유형 플레이스홀더 0개.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-05-12 14:35
|
||||
@@ -1,238 +0,0 @@
|
||||
# 0. 문서 상태
|
||||
이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다.
|
||||
|
||||
- 상태: confirmed
|
||||
- 문서 ID: dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment
|
||||
- 이슈명: daemon_id 도입 및 1자산 1데몬 자산 배정 모델 정립
|
||||
- 작성 시각(KST): 2026-05-13 17:08
|
||||
- 마지막 갱신 시각(KST): 2026-05-13 20:30
|
||||
- 대체됨: 없음
|
||||
- 후속 Discovery 참조: 없음
|
||||
- 생성된 REQ 참조: REQ-023, REQ-028
|
||||
|
||||
# 1. 계획 상태 요약
|
||||
현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다.
|
||||
|
||||
- 연계 문서 현황:
|
||||
- 관련 점수 파일: 미생성
|
||||
- 관련 TODO/메모: 없음
|
||||
- 해석:
|
||||
- dcy-002 계열의 v1 API와 service-token 모델은 `owner_user_id`와 `runtime_id`까지만 표현하고 있어, 동일 user 아래 여러 daemon이 자산을 나눠 맡는 운영 모델을 서버가 이해하지 못한다.
|
||||
- 이번 Discovery는 `runtime_id`를 런타임 식별자로 유지하면서 stable `daemon_id`와 exact-one asset assignment를 도입할지, 그리고 그 변화가 `/v1/assets`, `/v1/user_info`, `POST /v1/assets/{asset_id}/ledger/trades`, rotate, 운영 provenance에 미치는 범위를 정의한다.
|
||||
|
||||
# 2. 요구사항 판정 결과
|
||||
이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다.
|
||||
|
||||
- 판정: 신규
|
||||
- 근거
|
||||
- 기존 dcy-001~005 중 stable daemon identity와 `1개 자산 = 정확히 1개 데몬` 서버 강제 모델을 다루는 Discovery는 없다.
|
||||
- 이번 변경은 이미 Implemented인 REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-012의 Requirement와 Acceptance Criteria를 다시 써야 하는 구조 변경이어서 기존 Discovery의 미세 수정으로 처리할 수 없다.
|
||||
|
||||
> **판정이 `완전 중복`인 경우**: 이하 섹션 작성을 중단하고 `# 9. 파일 처리 결과`에 중복 판정 사유와 참조 문서 경로를 기록한 뒤 문서를 닫는다. REQ 초안 작성으로 넘기지 않는다.
|
||||
|
||||
# 3. 문제점의 요약
|
||||
문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다.
|
||||
|
||||
- 현재 상태
|
||||
- `GET /v1/assets`는 현재 service-token 소유 user의 portfolio만 반환하며, daemon assignment 차원을 전혀 갖지 않는다.
|
||||
- `runtime_id`는 service-token 발급 시 생성되어 `/v1/user_info`로 반환되지만 rotate 시 다시 바뀌는 런타임 식별자라 stable asset assignment key로 쓰기 어렵고, `/v1/user_info`에는 아직 `daemon_id`가 포함되지 않는다.
|
||||
- 현재 스키마와 도메인 모델에는 `daemons` 테이블, `auth_tokens.daemon_id`, `portfolios.managed_by_daemon_id`가 없다.
|
||||
- 태그와 owner scope는 존재하지만 `1개 자산은 정확히 1개 데몬만 담당`을 서버에서 강제하는 모델이나 API 계약은 없다.
|
||||
- `POST /v1/assets/{asset_id}/ledger/trades`는 유효한 service-token 인증과 asset 존재성 기준으로 동작하며, "해당 asset이 이 daemon에 배정됐는가"는 검증하지 않는다.
|
||||
- 기대 상태
|
||||
- service-token은 `runtime_id`와 별도로 stable `daemon_id`에 바인딩된다.
|
||||
- daemon create/update는 별도 daemon registry 관리 경로에서 수행되고, service-token issue/rotate는 existing daemon reference만 받아 binding한다.
|
||||
- 각 asset은 동일 owner scope 안에서 정확히 1개의 daemon에만 배정되고, `/v1/assets`와 ledger write는 이 배정 규칙을 서버에서 강제한다.
|
||||
- `GET /v1/user_info`는 현재 인증 service-token에 기록된 `daemon_id`와 runtime 세대 식별자인 `runtime_id`를 함께 반환하며, 전환 구간의 NULL backfill token은 `daemon_id = null`을 진단 정보로 노출한다.
|
||||
- rotate나 runtime 재기동 이후에도 `daemon_id`는 유지되고 `runtime_id`만 갱신되어 운영 provenance와 runtime identity가 분리된다.
|
||||
- 이해관계자
|
||||
- trex-basic 데몬 운영자
|
||||
- portfolio-hub repository-maintainers
|
||||
- release approver / production operator
|
||||
- user ownership 및 asset assignment policy 승인자
|
||||
- 제약사항
|
||||
- service-token bearer auth와 기존 `read`/`trade-write`/`admin` 권한 모델은 유지한다.
|
||||
- `1개 자산 = 정확히 1개 데몬` 규칙을 many-to-many 없이 모델링한다.
|
||||
- 신규 migration과 schema bootstrap은 기존 Alembic chain과 `IF NOT EXISTS` 정책을 깨지 않아야 한다.
|
||||
|
||||
## 주요 사용자 시나리오
|
||||
사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다.
|
||||
|
||||
- 동일 user가 `daemon-a`, `daemon-b` 두 개를 운영할 때, `daemon-a`용 service-token으로 `/v1/assets`를 호출하면 `daemon-a`에 배정된 asset만 반환되고 `daemon-b`의 asset은 반환되지 않는다.
|
||||
- `daemon-b`가 `daemon-a`에 배정된 asset_id로 `POST /v1/assets/{asset_id}/ledger/trades`를 호출하면 `403 Forbidden`으로 거부되고, 잘못된 ledger row는 생성되지 않는다.
|
||||
- 운영자가 `daemon-a`용 service-token을 rotate할 때 `runtime_id`는 새 값으로 갱신되지만 `daemon_id`와 asset assignment는 유지되어, daemon 재배정 없이 provenance만 이어서 확인할 수 있다.
|
||||
|
||||
# 4. 이번에 정의할 변경
|
||||
이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다.
|
||||
|
||||
- stable daemon identity 모델 추가: `daemons` registry, 별도 daemon create/update 관리 경로, service-token `daemon_id` binding, `runtime_id`와 `daemon_id`의 역할 분리
|
||||
- exact-one asset assignment 모델 추가: `portfolios`에 daemon assignment를 저장하고 daemon-scoped `/v1/assets` 조회와 ledger authorization로 `1자산 1데몬` 규칙을 강제
|
||||
- service-token issuance/rotate/provenance와 baseline 문서 갱신: daemon binding, 기존 asset/token의 NULL backfill, `/v1/user_info`와 CLI의 `daemon_id` 노출, `403` mismatch contract를 운영 절차와 함께 재정의
|
||||
|
||||
## 영향 범위
|
||||
변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다.
|
||||
|
||||
- 코드 영향:
|
||||
- `migrations/versions/`: `daemons` 테이블, `auth_tokens.daemon_id`, `portfolios.managed_by_daemon_id`, 관련 FK/check/backfill migration 추가
|
||||
- `app/domain/models.py`, `app/repositories/postgres.py`, `app/repositories/portfolio_repository.py`: daemon model, token binding, asset assignment, rotate invariants 반영
|
||||
- `app/services/auth_service.py`, `app/api/auth_routes.py`, `app/cli/commands/service_tokens.py`: service-token issue/list/rotate에 daemon binding 및 provenance 반영
|
||||
- `app/api/v1_assets.py`, `app/api/v1_user_info.py`, `app/api/v1_ledger.py`: daemon-scoped read/write와 diagnostics contract 반영
|
||||
- 문서 영향:
|
||||
- `docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md`: 후속 Discovery 참조 및 runtime_id 역할 재정렬 필요
|
||||
- `docs/srs/Interface/req-005_service-token-runtime-id-issuance.md`, `req-006_v1-assets-endpoint.md`, `req-007_v1-user-info-endpoint.md`, `req-008_v1-ledger-trades-endpoint.md`, `req-012_service-token-rotate-cli.md`: change-req 대상
|
||||
- `docs/srs/Migration/req-009_v1-api-db-schema-migration.md`, `docs/srs/Documentation/req-011_baseline-docs-update-for-trex-basic-contract.md`: change-req 대상
|
||||
- `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`, `README.md` 운영 provenance 문서: daemon assignment 반영 필요
|
||||
- 운영 영향:
|
||||
- daemon create/update는 별도 관리 경로에서 먼저 수행하고, service-token issue/rotate는 existing daemon 선택 또는 `daemon_id` 입력으로만 binding한다.
|
||||
- 기존 asset과 token은 초기 migration에서 daemon 필드를 `NULL`로 backfill하며, production operator가 명시적 reassignment inventory와 전환 절차를 준비하고 release approver 승인 이후에만 daemon-scoped enforcement를 켠다.
|
||||
- rotate 후 `runtime_id` 재조회와 `daemon_id` 불변성 확인을 운영 support path에 포함해야 한다.
|
||||
- daemon-scoped enforcement enablement gate는 `DATA_EXISTING_DEPLOYMENT_DAEMON_MAP` 완료, active service-token/asset의 explicit reassignment 또는 제외 처리 완료, zero-active-NULL/zero-mismatch dry-run, smoke 검증 통과를 required evidence로 삼는다.
|
||||
|
||||
# 5. 요구사항 목록
|
||||
실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다.
|
||||
|
||||
## 기능 요구사항
|
||||
- FR-1: service-token은 steady state에서 `runtime_id`와 별도로 stable `daemon_id`에 정확히 1:1로 바인딩되어야 한다. daemon provisioning은 별도 daemon create/update 관리 경로에서 수행되며, issue/rotate는 existing daemon만 참조하고 unknown daemon 자동 생성/업서트를 허용하지 않는다. `daemon_id`는 rotate 이후에도 유지되며, `runtime_id`는 런타임 식별자로서 갱신될 수 있어야 한다. 기존 token backfill은 `daemon_id = NULL`로 시작할 수 있고, 이 상태의 token은 daemon-scoped API에서 fail-closed로 취급한다.
|
||||
- FR-2: 각 asset(`portfolio`)은 steady state에서 동일 owner scope 안에 정확히 1개의 daemon에 배정되어야 한다. 스키마와 migration은 이 exact-one assignment를 DB 레벨에서 표현하고 강제해야 하며, 기존 asset backfill은 `managed_by_daemon_id = NULL`로 시작할 수 있고 이 상태의 asset은 daemon-assigned state로 간주하지 않는다.
|
||||
- FR-3: `GET /v1/assets`는 service-token 소유 user의 전체 asset이 아니라, 인증된 token에 바인딩된 daemon에 배정된 asset만 반환해야 한다.
|
||||
- FR-4: `POST /v1/assets/{asset_id}/ledger/trades`는 인증된 daemon에 배정된 asset에 대해서만 성공해야 하며, daemon assignment mismatch 또는 미배정 asset에는 `403 Forbidden`으로 응답하고 ledger row를 생성하지 않아야 한다.
|
||||
- FR-5: `GET /v1/user_info`는 `runtime_id`와 함께 현재 인증 service-token의 `daemon_id`를 반환해야 하며, backfill로 `daemon_id = NULL`인 legacy token에는 그 NULL 값을 진단용으로 노출해야 한다. `pfhub service-token issue/list/rotate` 출력은 모두 `daemon_id` 필드를 포함해야 하며, `POST /auth/service-tokens`와 CLI issue/rotate는 기존 daemon registry에 존재하는 daemon만 바인딩 대상으로 받아야 한다. 운영 provenance 절차는 `runtime_id`와 `daemon_id`의 역할 차이를 명확히 드러내야 한다.
|
||||
- FR-6: baseline 문서와 운영 runbook은 daemon provisioning/binding, asset assignment, rotate invariants, 기존 데이터 backfill 절차를 반영해야 한다.
|
||||
|
||||
## 비기능 요구사항
|
||||
성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다.
|
||||
|
||||
- NFR-1: `runtime_id`는 runtime identity, `daemon_id`는 stable logical identity로 역할이 분리되어야 하며, 어떤 API나 운영 절차도 `runtime_id`를 asset assignment key로 재사용하면 안 된다.
|
||||
- NFR-2: DB는 post-cutover steady state에서 `service-token -> daemon`, `asset -> daemon` 관계의 same-user consistency와 exact-one assignment를 FK/check/NOT NULL 제약으로 보장해야 한다. 전환 구간에서는 `auth_tokens.daemon_id`, `portfolios.managed_by_daemon_id`의 `NULL` backfill을 허용하되, explicit reassignment와 검증을 거쳐 exact-one assignment 상태로 수렴해야 하며, 누락 또는 mismatch 상황은 fail-open이 아닌 fail-closed로 처리해야 한다.
|
||||
- NFR-3: migration과 schema bootstrap은 기존 Alembic revision chain 및 `IF NOT EXISTS` 정책을 준수해야 하며, backfill이 완료되지 않은 상태에서 daemon-scoped enforcement가 부분 적용되지 않도록 해야 한다.
|
||||
|
||||
## 범위 경계
|
||||
이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다.
|
||||
|
||||
- In Scope
|
||||
- stable `daemon_id` 모델과 service-token binding
|
||||
- exact-one asset assignment 및 daemon-scoped `/v1/assets` / ledger authorization
|
||||
- service-token rotate/list/issue/provenance의 `runtime_id`/`daemon_id` 역할 재정의
|
||||
- 필요한 migration, backfill 정책, baseline 문서 및 운영 runbook 반영
|
||||
- Out of Scope
|
||||
- trex-basic 측 consumer 코드 변경 자체
|
||||
- runtime heartbeat, leader election, lease/lock 같은 distributed runtime control
|
||||
- `1자산 N데몬`, 예약 배정, 시간대별 assignment 같은 many-to-many 또는 temporal assignment 모델
|
||||
- 외부 사용자를 위한 daemon fleet 관리 UI 또는 공개 운영 포털 추가
|
||||
|
||||
# 6. 리스크/가정 목록
|
||||
불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다.
|
||||
|
||||
## 리스크
|
||||
- R-1 (High): 기존 asset과 token을 `NULL`로 backfill한 뒤 explicit daemon assignment가 늦어지면 기존 daemon이 `/v1/assets`와 ledger write에서 연쇄적으로 `403`을 맞아 운영 중단이 발생할 수 있다.
|
||||
- 완화 방안: production operator가 현재 배포 환경의 `user -> daemon -> asset` inventory와 explicit reassignment 계획을 준비하고, portfolio-hub repository-maintainers가 dry-run audit 결과를 검토한 뒤, release approver 승인 이후에만 daemon-scoped enforcement를 켠다.
|
||||
- R-2 (High): `runtime_id`와 `daemon_id`의 의미가 문서/코드/운영 절차마다 다르게 해석되면 rotate 이후 provenance와 asset assignment가 다시 흔들릴 수 있다.
|
||||
- 완화 방안: portfolio-hub repository-maintainers가 REQ와 테스트에서 "rotate는 runtime_id를 갱신하지만 daemon_id는 유지"를 불변식으로 명시하고, issue/list/rotate/user_info 출력 필드 정의를 함께 정렬한다.
|
||||
- R-3 (Medium): daemon-scoped enforcement를 켠 뒤에도 미배정 asset이나 daemon-less service-token이 남아 있으면 `/v1/assets` 또는 ledger write가 예고 없이 깨질 수 있다.
|
||||
- 완화 방안: production operator가 enforcement 이전 audit/report와 smoke 검증을 수행하고, release approver가 rollback approver를 겸해 on/off 판단을 승인한다.
|
||||
|
||||
## 가정
|
||||
아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다.
|
||||
|
||||
- A-1: 동일 user가 여러 daemon을 가질 수 있지만, 각 daemon은 정확히 1명의 owner user에 속한다.
|
||||
- A-2: `runtime_id`는 per-token runtime identity로 유지되며 issue/rotate 시 갱신될 수 있다.
|
||||
- A-3: service-token bearer auth와 기존 `read`/`trade-write`/`admin` 권한 모델은 유지하고, daemon assignment는 그 위에 추가되는 authorization 축이다.
|
||||
- A-4: `1개 자산 = 정확히 1개 데몬` 규칙은 태그나 외부 컨벤션이 아니라 DB-backed 서버 모델로 강제한다.
|
||||
|
||||
## 오픈 질문
|
||||
답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다.
|
||||
|
||||
- 현재 Open 상태 OQ는 없다. 기존 OQ-1~OQ-4의 결정 사항은 본문과 `# 10. 사용자 결정 필요 항목 요약`에 반영했다.
|
||||
- OQ-4: daemon-scoped enforcement enablement gate — `NULL` backfill 상태의 daemon-scoped enforcement는 다음 조건이 모두 충족될 때만 활성화한다: `DATA_EXISTING_DEPLOYMENT_DAEMON_MAP`가 활성 user/service-token/asset 전체를 커버하고, active service-token은 모두 valid `daemon_id`에 바인딩되거나 명시적으로 revoke/disable되며, active asset은 모두 정확히 1개 daemon에 재배정되거나 명시적으로 전환 대상에서 제외되고, dry-run audit에서 zero-active-NULL/zero-mismatch가 확인되며, smoke 검증이 통과해야 한다.
|
||||
- 상태: Answered
|
||||
- 처리 방안: production operator가 inventory/reassignment/smoke evidence를 준비하고, portfolio-hub repository-maintainers가 dry-run audit과 contract test evidence를 검토하며, release approver가 enforcement on/off와 rollback approver를 겸해 최종 승인한다.
|
||||
- 종료 조건: required evidence set, owner, approver가 운영 runbook과 후속 REQ Acceptance Criteria에 반영되면 유지 결정으로 닫는다.
|
||||
|
||||
### 오픈 질문 처리 기준
|
||||
- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다.
|
||||
- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다.
|
||||
- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다.
|
||||
- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다.
|
||||
- 처리 원칙 1: 질문만 남기지 말고 반드시 `상태`, `처리 방안`, `종료 조건`을 함께 적는다.
|
||||
- 처리 원칙 2: 답이 나온 질문은 본문 반영 없이 단순 삭제하지 않는다. 반영 위치를 먼저 갱신한다.
|
||||
- 처리 원칙 3: High 영향 질문은 `Deferred` 처리 후 처리 방안에 REQ 인계 추적 방식을 명시한다.
|
||||
|
||||
# 7. 초기 성공 기준
|
||||
이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다.
|
||||
|
||||
- S-1: 동일 user가 `daemon-a`, `daemon-b`를 운영할 때, 각 daemon의 service-token으로 호출한 `GET /v1/assets`는 해당 daemon에 배정된 asset만 반환한다.
|
||||
- S-2: `daemon-a`에 배정된 asset에 대해 `daemon-b`가 `POST /v1/assets/{asset_id}/ledger/trades`를 호출하면 `403 Forbidden`으로 거부되고 DB row가 생성되지 않으며, `daemon-a` 호출만 성공한다.
|
||||
- S-3: `GET /v1/user_info`와 `pfhub service-token issue/list/rotate`는 `daemon_id`를 노출하고, service-token rotate 이후 `runtime_id`는 변경되지만 `daemon_id` binding은 유지된다.
|
||||
- S-4: daemon create/update는 별도 registry 관리 경로에서만 수행되고, `POST /auth/service-tokens`와 CLI issue/rotate는 존재하지 않는 daemon 입력을 명시적으로 거부하며 자동 생성/업서트를 하지 않는다.
|
||||
- S-5: daemon-scoped enforcement는 inventory 완료, active token/asset reassignment 또는 제외 처리 완료, zero-active-NULL/zero-mismatch dry-run, smoke 검증 통과, release approver 승인 이후에만 활성화된다.
|
||||
- S-6: baseline 문서와 운영 runbook이 daemon provisioning, asset assignment, enforcement enablement gate, rotate invariants, backfill 절차를 반영하고 더 이상 `runtime_id`를 asset assignment key처럼 설명하지 않는다.
|
||||
|
||||
## 측정 방식 및 데이터 출처
|
||||
각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다.
|
||||
|
||||
- S-1 측정 방식: pytest로 동일 user 아래 2개 daemon과 2개 asset을 구성해 daemon-scoped `/v1/assets` 응답을 검증 / 데이터 출처: `tests/test_v1.py` 또는 신규 `tests/test_daemon_assignment.py`
|
||||
- S-2 측정 방식: pytest로 daemon mismatch ledger write를 호출해 `403` 응답과 DB row count 무변경을 검증 / 데이터 출처: `tests/test_v1.py` 또는 신규 `tests/test_daemon_assignment.py`
|
||||
- S-3 측정 방식: targeted auth/CLI/API 테스트로 rotate 전후 `runtime_id`/`daemon_id` invariants와 `daemon_id` diagnostics 노출을 검증 / 데이터 출처: `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_v1.py`
|
||||
- S-4 측정 방식: targeted auth/CLI/API 테스트로 unknown daemon binding 요청이 명시적으로 실패하고 daemon auto-create/upsert가 발생하지 않음을 검증 / 데이터 출처: `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_v1.py` 또는 신규 `tests/test_daemon_registry.py`
|
||||
- S-5 측정 방식: cutover checklist와 dry-run/smoke evidence로 active token/asset reassignment 완료, zero-active-NULL/zero-mismatch, release approver 승인 여부를 확인 / 데이터 출처: 운영 cutover checklist, audit report, smoke test log
|
||||
- S-6 측정 방식: baseline 문서와 운영 runbook에 daemon assignment, enforcement enablement gate, backfill 절차가 반영됐는지 코드 리뷰로 확인 / 데이터 출처: `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`, `README.md`
|
||||
|
||||
# 8. REQ로 넘기기 전 확인 체크
|
||||
REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다.
|
||||
|
||||
- [x] 범위(In Scope/Out of Scope) 확정
|
||||
- 기준: daemon provisioning path를 포함한 주요 정책 결정이 범위와 함께 본문 및 section 10에 반영되어야 함
|
||||
- [x] 성공 지표의 측정 방식/데이터 출처 확정
|
||||
- 기준: S-1~S-6이 pytest, 운영 evidence, 또는 문서 리뷰 evidence로 추적 가능한 수준으로 정리되어야 함
|
||||
- [x] High 영향 리스크 우선순위 및 해소 책임자 지정
|
||||
- 기준: R-1 backfill/reassignment owner는 production operator, R-2 runtime/daemon provenance owner는 portfolio-hub repository-maintainers, rollback approver는 release approver로 지정되어야 함
|
||||
- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인
|
||||
- 기준: OQ 목록에 `Open` 상태 항목이 0개이거나, 잔여 항목 전부 `Deferred` 처리 완료
|
||||
- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정
|
||||
- 기준: confirm-discovery 게이트에서 범위 승인자와 후속 REQ drafting owner가 지정되어야 함
|
||||
|
||||
# 9. 파일 처리 결과
|
||||
이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다.
|
||||
|
||||
- 처리 결과: 생성 후 갱신
|
||||
- 생성 경로: docs/discovery/dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment.md
|
||||
- 참조 문서:
|
||||
- docs/discovery/dcy-002_20260502_trex-basic-consumer-api-contract.md
|
||||
- docs/discovery/dcy-005_20260511_production-admin-bootstrap-and-operator-access.md
|
||||
- docs/srs/Interface/req-005_service-token-runtime-id-issuance.md
|
||||
- docs/srs/Interface/req-006_v1-assets-endpoint.md
|
||||
- docs/srs/Interface/req-007_v1-user-info-endpoint.md
|
||||
- docs/srs/Interface/req-008_v1-ledger-trades-endpoint.md
|
||||
- docs/srs/Migration/req-009_v1-api-db-schema-migration.md
|
||||
- docs/srs/Interface/req-012_service-token-rotate-cli.md
|
||||
|
||||
# 10. 사용자 결정 필요 항목 요약
|
||||
사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다.
|
||||
|
||||
## DECIDE
|
||||
- 없음
|
||||
|
||||
## CONFIRM
|
||||
- 없음 (Confirmed By: Justin — 2026-05-13)
|
||||
|
||||
## DATA
|
||||
- baseline 반영 완료
|
||||
- `DATA_EXISTING_DEPLOYMENT_DAEMON_MAP`: canonical acceptance environment는 production이고, staging은 optional preflight, local/local-test는 cutover evidence 대상에서 제외한다. 현재 저장소 기준으로 확인되는 external consumer daemon lineage는 `trex-basic` 1종이며, production operator는 실제 cutover checklist와 release evidence에 환경별 row를 `(environment, username 또는 user_id, daemon_slug, daemon_display_name, service_token_name, token_prefix, permission, asset_ids, state[active|excluded|revoked])` 형식으로 기록한다. raw secret 값은 저장소에 남기지 않고, production canonical secret material location은 host-side `EnvironmentFile=/opt/portfoliohub/.env` 운영 경로를 따른다.
|
||||
- `DATA_DAEMON_NAMING_RULES`: `daemon_display_name`은 trim된 non-empty human-readable label로 두고, `daemon_slug`는 owner user 내에서 unique한 stable identifier로 관리한다. 현재 consumer lineage 기준 권장 slug prefix는 `trex-basic`이며, slug 표기는 lowercase kebab-case를 사용한다. canonical operator 표기와 cutover evidence 표기는 `<username>/<daemon_slug>`로 통일하고, provenance 추적에는 `daemon_slug`, `service_token_name`, `token_prefix`를 함께 기록한다.
|
||||
|
||||
# 11. Discovery Freeze
|
||||
REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다.
|
||||
|
||||
## 섹션 참조
|
||||
- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록)
|
||||
- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록)
|
||||
- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준)
|
||||
- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크)
|
||||
- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약)
|
||||
|
||||
## Freeze 플래그
|
||||
- Handoff Decision: 승인
|
||||
- Handoff Rationale: 현재 저장소를 재확인한 결과 `/v1/assets`는 아직 owner-user scope만 사용하고, `/v1/user_info`는 `runtime_id`만 반환하며, `POST /v1/assets/{asset_id}/ledger/trades`는 daemon assignment 검증이 없고, service-token CLI도 daemon binding/diagnostics를 아직 다루지 않는다. 문서의 현재 상태 서술, FR/NFR, 성공 기준, section 10 DATA baseline은 이 현실과 일치하며, Open OQ와 C 유형 플레이스홀더는 0개다. cutover checklist row와 smoke evidence는 release 단계의 운영 검증 산출물로 남기고, REQ drafting handoff 자체를 막는 항목으로 두지 않는다.
|
||||
- Ready for REQ Drafting: true
|
||||
- Confirmed By: Justin
|
||||
- Confirmed At (KST): 2026-05-13 19:17
|
||||
@@ -6,9 +6,5 @@
|
||||
|
||||
| ID | 날짜 | 이슈명 | 문서 | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| dcy-001_20260430_real-db-migration-validation-and-operations-readiness | 20260430 | 실DB 마이그레이션 검증 및 운영 준비 | [Discovery](./dcy-001_20260430_real-db-migration-validation-and-operations-readiness.md) | confirmed |
|
||||
| dcy-002_20260502_trex-basic-consumer-api-contract | 20260502 | trex-basic Consumer-Driven Contract 반영 — service-token runtime_id 발급 및 v1 API 엔드포인트 구현 | [Discovery](./dcy-002_20260502_trex-basic-consumer-api-contract.md) | confirmed |
|
||||
| dcy-003_20260502_portfolio-list-user-required | 20260502 | CLI `pfhub portfolio list` — `--user` 옵션 필수화 및 소유자 기반 조회 강제 | [Discovery](./dcy-003_20260502_portfolio-list-user-required.md) | confirmed |
|
||||
| dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution | 20260511 | portfolio_id UUID 교체 및 CLI 사용자 범위 기반 자산 식별 구조 전환 | [Discovery](./dcy-004_20260511_portfolio-id-uuid-migration-and-user-scoped-name-resolution.md) | confirmed |
|
||||
| dcy-005_20260511_production-admin-bootstrap-and-operator-access | 20260511 | 실배포 admin bootstrap 및 operator access 경로 정립 | [Discovery](./dcy-005_20260511_production-admin-bootstrap-and-operator-access.md) | confirmed |
|
||||
| dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment | 20260513 | daemon_id 도입 및 1자산 1데몬 자산 배정 모델 정립 | [Discovery](./dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment.md) | confirmed |
|
||||
| dcy-001_20260519_asset-catalog-and-user-portfolio-model | 20260519 | 자산 관리 구조 재설계 — admin 관리 asset catalog와 user portfolio 분리 모델 도입 | [Discovery](./dcy-001_20260519_asset-catalog-and-user-portfolio-model.md) | draft |
|
||||
| dcy-002_20260519_tag-ownership-and-applicability-policy | 20260519 | 태그 소유권 및 적용 대상 정책 — admin/user 구분 태그 모델과 asset·portfolio 적용 정책 도입 | [Discovery](./dcy-002_20260519_tag-ownership-and-applicability-policy.md) | confirmed |
|
||||
|
||||
Reference in New Issue
Block a user