Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6422cb2b13 | |||
| 9efd202e76 | |||
| 58e8b02d33 | |||
| eb59cae8e3 | |||
| ec80337389 | |||
| d38dc32e7a | |||
| 00957bf384 | |||
| 5a1570790c | |||
| d18f6a7a67 | |||
| dd352596fc | |||
| 6a86997438 | |||
| a3c20240ab | |||
| e5da826329 | |||
| eee15039e2 | |||
| 35161d8363 | |||
| b70eab02d1 | |||
| 9ffb740f07 | |||
| cbcc4849ec | |||
| ace9fe8a9c | |||
| 8ed232e224 | |||
| 3c56c0926a | |||
| 07ea590b52 | |||
| 9d3a467dde | |||
| 70d7c7c8dc | |||
| 9da1903d64 | |||
| 9665f7791f | |||
| 4e0a0bc02b | |||
| 7b04dd6017 | |||
| a910fc86d7 | |||
| aad6b9ee04 | |||
| 2293acc825 | |||
| 02bdd39d31 | |||
| c44f55aef6 | |||
| 0ce86742b0 | |||
| 3287ae399e | |||
| 6f47d71c4a | |||
| cada8fe843 | |||
| a750858dfe | |||
| a84e5c1273 | |||
| 30c941cc5b | |||
| 14e2cedc4f | |||
| f2b7b40668 | |||
| 52bdf926ad | |||
| b78c19646f | |||
| 769d188701 | |||
| 7304aafc83 | |||
| df7d41df7d | |||
| 60c90baa28 | |||
| 8c777df66b | |||
| cedc8d79ee | |||
| b86e6fee2c | |||
| 6c6011a62d | |||
| 53be11673c |
@@ -100,6 +100,25 @@ jobs:
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Build frontend into Host static assets
|
||||
# wwwroot/assets and wwwroot/index.html are gitignored build output (see
|
||||
# CURRENT_ROADMAP.md item #3) -- this release zip must build them fresh,
|
||||
# the same way .gitea/workflows/deploy.yml does for production.
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm build
|
||||
find ../src/KArtSell.Host/wwwroot -mindepth 1 -delete
|
||||
cp -R dist/. ../src/KArtSell.Host/wwwroot/
|
||||
working-directory: frontend
|
||||
|
||||
- name: Publish Release Build
|
||||
run: |
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.env.local
|
||||
# Vite build output copied into the Host's wwwroot by KArtSell.Host.csproj's
|
||||
# BuildFrontend target (local dev) and by .gitea/workflows/deploy.yml (production).
|
||||
# Content-hashed filenames change on every rebuild even with no source changes,
|
||||
# so this must never be committed -- see CURRENT_ROADMAP.md item #3.
|
||||
src/KArtSell.Host/wwwroot/assets/
|
||||
src/KArtSell.Host/wwwroot/index.html
|
||||
frontend/test-results/
|
||||
.playwright/
|
||||
TestResults/
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
- generic [ref=e3]:
|
||||
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||
- /url: "#ks-main"
|
||||
- banner [ref=e5]:
|
||||
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=e6] [cursor=pointer]:
|
||||
- strong [ref=e7]: K-ArtSell Aegis
|
||||
- generic [ref=e8]: IMPLEMENTATION_TEMPLATE
|
||||
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=e9]:
|
||||
- generic [ref=e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||
- generic [ref=e11]: Ctrl K
|
||||
- status [ref=e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||
- navigation "열린 업무" [ref=e13]:
|
||||
- generic [ref=e14]:
|
||||
- button "컴포넌트 확인" [ref=e15] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e17] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e18] [cursor=pointer]: ×
|
||||
- generic [ref=e19]:
|
||||
- complementary "주요 메뉴" [ref=e20]:
|
||||
- button "« 접기" [expanded] [ref=e21] [cursor=pointer]
|
||||
- generic [ref=e22]:
|
||||
- heading "Design System" [level=2] [ref=e23]
|
||||
- link "컴포넌트 확인" [ref=e24] [cursor=pointer]:
|
||||
- /url: /internal/ui-standard
|
||||
- generic [ref=e25]:
|
||||
- heading "Operations" [level=2] [ref=e26]
|
||||
- link "데이터 품질" [ref=e27] [cursor=pointer]:
|
||||
- /url: /ops/data-quality
|
||||
- link "시장 데이터 수집" [ref=e28] [cursor=pointer]:
|
||||
- /url: /ops/market-data-ingestion
|
||||
- link "수집 이력" [ref=e29] [cursor=pointer]:
|
||||
- /url: /ops/market-data-history
|
||||
- link "모델 운영" [ref=e30] [cursor=pointer]:
|
||||
- /url: /ops/model-operations
|
||||
- generic [ref=e31]:
|
||||
- heading "Portfolio" [level=2] [ref=e32]
|
||||
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||
- /url: /portfolio/risk
|
||||
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||
- /url: /portfolio/rebalance
|
||||
- generic [ref=e35]:
|
||||
- heading "Research" [level=2] [ref=e36]
|
||||
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||
- /url: /research/sell-decision
|
||||
- main [ref=e38]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- heading "표준 UI 패턴" [level=1] [ref=e42]
|
||||
- paragraph [ref=e43]: Feature는 공급자 라이브러리를 직접 사용하지 않고, v4 어댑터·레이아웃·화면 계약을 사용한다.
|
||||
- generic [ref=e44]:
|
||||
- generic [ref=e45]: "상태: READY"
|
||||
- generic [ref=e46]: "As-of: 2026-08-02"
|
||||
- generic [ref=e47]: "Version: UI-CONTRACT-4.0"
|
||||
- button "새 화면 패킷" [ref=e49] [cursor=pointer]
|
||||
- generic [ref=e50]:
|
||||
- generic [ref=e51]:
|
||||
- strong [ref=e52]: "10"
|
||||
- generic [ref=e53]: 화면 타입
|
||||
- generic [ref=e54]:
|
||||
- strong [ref=e55]: "14"
|
||||
- generic [ref=e56]: 어댑터 포트
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: primevue-aggrid
|
||||
- generic [ref=e60]: PrimeVue + AG Grid Community
|
||||
- generic [ref=e61]:
|
||||
- generic [ref=e62]: 자동주문 OFF
|
||||
- generic [ref=e64]: 고정 경계
|
||||
- generic [ref=e66]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: 검색
|
||||
- textbox "검색" [ref=e69]:
|
||||
- /placeholder: 화면 ID, 타입 또는 컴포넌트
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: 상태
|
||||
- combobox "전체" [ref=e73]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e81]:
|
||||
- main [ref=e82]:
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: No Rows To Show
|
||||
- grid [ref=e87]:
|
||||
- rowgroup [ref=e88]:
|
||||
- row [ref=e89]:
|
||||
- columnheader [ref=e90]
|
||||
- columnheader "화면 ID" [ref=e91]:
|
||||
- generic [ref=e93] [cursor=pointer]
|
||||
- columnheader "화면 타입" [ref=e95]:
|
||||
- generic [ref=e97] [cursor=pointer]
|
||||
- columnheader "표준 컴포넌트" [ref=e99]:
|
||||
- generic [ref=e101] [cursor=pointer]
|
||||
- columnheader "필수 증거" [ref=e103]:
|
||||
- generic [ref=e105] [cursor=pointer]
|
||||
- columnheader "상태" [ref=e107]:
|
||||
- generic [ref=e109] [cursor=pointer]
|
||||
- rowgroup [ref=e111]:
|
||||
- row [ref=e112]:
|
||||
- gridcell [ref=e113]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e114] [cursor=pointer]
|
||||
- gridcell "T01" [ref=e115]
|
||||
- gridcell "검색·목록형 CRUD" [ref=e116]
|
||||
- gridcell "SearchListCrudPage" [ref=e117]
|
||||
- gridcell "3" [ref=e118]
|
||||
- gridcell "READY" [ref=e119]
|
||||
- row [ref=e120]:
|
||||
- gridcell [ref=e121]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e122] [cursor=pointer]
|
||||
- gridcell "T02" [ref=e123]
|
||||
- gridcell "상세 조회형" [ref=e124]
|
||||
- gridcell "DetailReadPage" [ref=e125]
|
||||
- gridcell "3" [ref=e126]
|
||||
- gridcell "READY" [ref=e127]
|
||||
- row [ref=e128]:
|
||||
- gridcell [ref=e129]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e130] [cursor=pointer]
|
||||
- gridcell "T03" [ref=e131]
|
||||
- gridcell "등록·편집 Form" [ref=e132]
|
||||
- gridcell "EditFormPage" [ref=e133]
|
||||
- gridcell "3" [ref=e134]
|
||||
- gridcell "READY" [ref=e135]
|
||||
- row [ref=e136]:
|
||||
- gridcell [ref=e137]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e138] [cursor=pointer]
|
||||
- gridcell "T04" [ref=e139]
|
||||
- gridcell "Master-Detail" [ref=e140]
|
||||
- gridcell "MasterDetailCrudPage" [ref=e141]
|
||||
- gridcell "3" [ref=e142]
|
||||
- gridcell "READY" [ref=e143]
|
||||
- row [ref=e144]:
|
||||
- gridcell [ref=e145]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e146] [cursor=pointer]
|
||||
- gridcell "T05" [ref=e147]
|
||||
- gridcell "검토·승인 Workbench" [ref=e148]
|
||||
- gridcell "ApprovalWorkbenchPage" [ref=e149]
|
||||
- gridcell "3" [ref=e150]
|
||||
- gridcell "READY" [ref=e151]
|
||||
- row [ref=e152]:
|
||||
- gridcell [ref=e153]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e154] [cursor=pointer]
|
||||
- gridcell "T06" [ref=e155]
|
||||
- gridcell "단계 Wizard" [ref=e156]
|
||||
- gridcell "StepWizardPage" [ref=e157]
|
||||
- gridcell "3" [ref=e158]
|
||||
- gridcell "READY" [ref=e159]
|
||||
- row [ref=e160]:
|
||||
- gridcell [ref=e161]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e162] [cursor=pointer]
|
||||
- gridcell "T07" [ref=e163]
|
||||
- gridcell "Dashboard·Scorecard" [ref=e164]
|
||||
- gridcell "ScorecardDashboardPage" [ref=e165]
|
||||
- gridcell "3" [ref=e166]
|
||||
- gridcell "READY" [ref=e167]
|
||||
- row [ref=e168]:
|
||||
- gridcell [ref=e169]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e170] [cursor=pointer]
|
||||
- gridcell "T08" [ref=e171]
|
||||
- gridcell "Batch·데이터 운영" [ref=e172]
|
||||
- gridcell "BatchOperationsPageV2" [ref=e173]
|
||||
- gridcell "3" [ref=e174]
|
||||
- gridcell "READY" [ref=e175]
|
||||
- row [ref=e176]:
|
||||
- gridcell [ref=e177]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e178] [cursor=pointer]
|
||||
- gridcell "T09" [ref=e179]
|
||||
- gridcell "대사·예외 처리" [ref=e180]
|
||||
- gridcell "ReconciliationExceptionPage" [ref=e181]
|
||||
- gridcell "3" [ref=e182]
|
||||
- gridcell "READY" [ref=e183]
|
||||
- row [ref=e184]:
|
||||
- gridcell [ref=e185]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e186] [cursor=pointer]
|
||||
- gridcell "T10" [ref=e187]
|
||||
- gridcell "버전 비교·거버넌스" [ref=e188]
|
||||
- gridcell "VersionGovernancePage" [ref=e189]
|
||||
- gridcell "3" [ref=e190]
|
||||
- gridcell "READY" [ref=e191]
|
||||
- row [ref=e192]:
|
||||
- gridcell [ref=e193]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e194] [cursor=pointer]
|
||||
- gridcell "T11" [ref=e195]
|
||||
- gridcell "대량 입력(Fast Grid Entry)" [ref=e196]
|
||||
- gridcell "FastEntryGridPage" [ref=e197]
|
||||
- gridcell "3" [ref=e198]
|
||||
- gridcell "READY" [ref=e199]
|
||||
- row [ref=e200]:
|
||||
- gridcell [ref=e201]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e202] [cursor=pointer]
|
||||
- gridcell "T12" [ref=e203]
|
||||
- gridcell "작업 큐(Work Queue)" [ref=e204]
|
||||
- gridcell "WorkQueuePage" [ref=e205]
|
||||
- gridcell "2" [ref=e206]
|
||||
- gridcell "READY" [ref=e207]
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- region [ref=e215]:
|
||||
- generic [ref=e216]:
|
||||
- heading "컴포넌트 동작 프리뷰" [level=2] [ref=e217]
|
||||
- paragraph [ref=e218]: 공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.
|
||||
- generic [ref=e219]:
|
||||
- generic [ref=e220]:
|
||||
- heading "Actions" [level=3] [ref=e221]
|
||||
- button "기본 버튼" [ref=e222] [cursor=pointer]
|
||||
- button "주의 상태" [ref=e223] [cursor=pointer]
|
||||
- generic [ref=e224]:
|
||||
- heading "Status" [level=3] [ref=e225]
|
||||
- generic [ref=e226]:
|
||||
- generic [ref=e227]: READY
|
||||
- generic [ref=e229]: REVIEW
|
||||
- generic [ref=e231]: BLOCKED
|
||||
- generic [ref=e233]:
|
||||
- heading "Inputs" [level=3] [ref=e234]
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]: 텍스트 필드
|
||||
- textbox "텍스트 필드" [ref=e237]: 샘플 값
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]: 선택 필드
|
||||
- combobox "준비" [ref=e241]
|
||||
- region [ref=e245]:
|
||||
- generic [ref=e246]:
|
||||
- heading "입력 유형 기능 테스트" [level=2] [ref=e247]
|
||||
- paragraph [ref=e248]: 서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e250]:
|
||||
- heading "기본 입력" [level=2] [ref=e251]
|
||||
- paragraph [ref=e252]: 필수 항목과 유효성 상태를 조작할 수 있습니다.
|
||||
- button "읽기 전용" [ref=e254] [cursor=pointer]
|
||||
- group "기능 테스트 입력" [ref=e256]:
|
||||
- generic [ref=e257]:
|
||||
- generic [ref=e258]: 이름
|
||||
- textbox "이름" [ref=e259]: 테스트
|
||||
- generic [ref=e260]:
|
||||
- generic [ref=e261]: 금액
|
||||
- spinbutton "금액" [ref=e263]
|
||||
- generic [ref=e264]:
|
||||
- generic [ref=e265]: 기준일
|
||||
- generic [ref=e266]:
|
||||
- combobox "기준일" [ref=e267]
|
||||
- button "Choose Date" [ref=e268]
|
||||
- generic [ref=e271] [cursor=pointer]:
|
||||
- checkbox "검증 조건을 확인했습니다" [checked] [active] [ref=e273]
|
||||
- generic [ref=e274]: 검증 조건을 확인했습니다
|
||||
- generic [ref=e276]:
|
||||
- button "검증 실행" [ref=e277] [cursor=pointer]
|
||||
- button "초기화" [ref=e278] [cursor=pointer]
|
||||
- complementary [ref=e279]:
|
||||
- generic [ref=e280]:
|
||||
- heading "교체 계약" [level=2] [ref=e281]
|
||||
- paragraph [ref=e282]:
|
||||
- text: 기본 공급자 교체는
|
||||
- code [ref=e283]: VITE_UI_ADAPTER
|
||||
- text: 와 provider registry에서 수행한다. Feature 코드는 변경하지 않는다.
|
||||
- paragraph [ref=e284]: PrimeVue+AG Grid와 Native reference adapter가 동일 contract test를 통과해야 한다.
|
||||
- paragraph [ref=e285]: 생산 교체는 접근성, 키보드, 상태행렬, 시각회귀, 대량목록 성능을 별도 Gate로 검증한다.
|
||||
- text: "데이터 기준시각: 2026-08-02"
|
||||
- contentinfo [ref=e286]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||
- generic "애플리케이션 버전" [ref=e287]: v0.1.0 · UI contract 4.0
|
||||
@@ -0,0 +1,261 @@
|
||||
- generic [ref=e3]:
|
||||
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||
- /url: "#ks-main"
|
||||
- banner [ref=e5]:
|
||||
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=e6] [cursor=pointer]:
|
||||
- strong [ref=e7]: K-ArtSell Aegis
|
||||
- generic [ref=e8]: IMPLEMENTATION_TEMPLATE
|
||||
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=e9]:
|
||||
- generic [ref=e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||
- generic [ref=e11]: Ctrl K
|
||||
- status [ref=e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||
- navigation "열린 업무" [ref=e13]:
|
||||
- generic [ref=e14]:
|
||||
- button "컴포넌트 확인" [ref=e15] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e17] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e18] [cursor=pointer]: ×
|
||||
- generic [ref=e19]:
|
||||
- complementary "주요 메뉴" [ref=e20]:
|
||||
- button "« 접기" [expanded] [ref=e21] [cursor=pointer]
|
||||
- generic [ref=e22]:
|
||||
- heading "Design System" [level=2] [ref=e23]
|
||||
- link "컴포넌트 확인" [ref=e24] [cursor=pointer]:
|
||||
- /url: /internal/ui-standard
|
||||
- generic [ref=e25]:
|
||||
- heading "Operations" [level=2] [ref=e26]
|
||||
- link "데이터 품질" [ref=e27] [cursor=pointer]:
|
||||
- /url: /ops/data-quality
|
||||
- link "시장 데이터 수집" [ref=e28] [cursor=pointer]:
|
||||
- /url: /ops/market-data-ingestion
|
||||
- link "수집 이력" [ref=e29] [cursor=pointer]:
|
||||
- /url: /ops/market-data-history
|
||||
- link "모델 운영" [ref=e30] [cursor=pointer]:
|
||||
- /url: /ops/model-operations
|
||||
- generic [ref=e31]:
|
||||
- heading "Portfolio" [level=2] [ref=e32]
|
||||
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||
- /url: /portfolio/risk
|
||||
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||
- /url: /portfolio/rebalance
|
||||
- generic [ref=e35]:
|
||||
- heading "Research" [level=2] [ref=e36]
|
||||
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||
- /url: /research/sell-decision
|
||||
- main [ref=e38]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- heading "표준 UI 패턴" [level=1] [ref=e42]
|
||||
- paragraph [ref=e43]: Feature는 공급자 라이브러리를 직접 사용하지 않고, v4 어댑터·레이아웃·화면 계약을 사용한다.
|
||||
- generic [ref=e44]:
|
||||
- generic [ref=e45]: "상태: READY"
|
||||
- generic [ref=e46]: "As-of: 2026-08-02"
|
||||
- generic [ref=e47]: "Version: UI-CONTRACT-4.0"
|
||||
- button "새 화면 패킷" [ref=e49] [cursor=pointer]
|
||||
- generic [ref=e50]:
|
||||
- generic [ref=e51]:
|
||||
- strong [ref=e52]: "10"
|
||||
- generic [ref=e53]: 화면 타입
|
||||
- generic [ref=e54]:
|
||||
- strong [ref=e55]: "14"
|
||||
- generic [ref=e56]: 어댑터 포트
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: primevue-aggrid
|
||||
- generic [ref=e60]: PrimeVue + AG Grid Community
|
||||
- generic [ref=e61]:
|
||||
- generic [ref=e62]: 자동주문 OFF
|
||||
- generic [ref=e64]: 고정 경계
|
||||
- generic [ref=e66]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: 검색
|
||||
- textbox "검색" [ref=e69]:
|
||||
- /placeholder: 화면 ID, 타입 또는 컴포넌트
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: 상태
|
||||
- combobox "전체" [ref=e73]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e81]:
|
||||
- main [ref=e82]:
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: No Rows To Show
|
||||
- grid [ref=e87]:
|
||||
- rowgroup [ref=e88]:
|
||||
- row [ref=e89]:
|
||||
- columnheader [ref=e90]
|
||||
- columnheader "화면 ID" [ref=e91]:
|
||||
- generic [ref=e93] [cursor=pointer]
|
||||
- columnheader "화면 타입" [ref=e95]:
|
||||
- generic [ref=e97] [cursor=pointer]
|
||||
- columnheader "표준 컴포넌트" [ref=e99]:
|
||||
- generic [ref=e101] [cursor=pointer]
|
||||
- columnheader "필수 증거" [ref=e103]:
|
||||
- generic [ref=e105] [cursor=pointer]
|
||||
- columnheader "상태" [ref=e107]:
|
||||
- generic [ref=e109] [cursor=pointer]
|
||||
- rowgroup [ref=e111]:
|
||||
- row [ref=e112]:
|
||||
- gridcell [ref=e113]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e114] [cursor=pointer]
|
||||
- gridcell "T01" [ref=e115]
|
||||
- gridcell "검색·목록형 CRUD" [ref=e116]
|
||||
- gridcell "SearchListCrudPage" [ref=e117]
|
||||
- gridcell "3" [ref=e118]
|
||||
- gridcell "READY" [ref=e119]
|
||||
- row [ref=e120]:
|
||||
- gridcell [ref=e121]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e122] [cursor=pointer]
|
||||
- gridcell "T02" [ref=e123]
|
||||
- gridcell "상세 조회형" [ref=e124]
|
||||
- gridcell "DetailReadPage" [ref=e125]
|
||||
- gridcell "3" [ref=e126]
|
||||
- gridcell "READY" [ref=e127]
|
||||
- row [ref=e128]:
|
||||
- gridcell [ref=e129]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e130] [cursor=pointer]
|
||||
- gridcell "T03" [ref=e131]
|
||||
- gridcell "등록·편집 Form" [ref=e132]
|
||||
- gridcell "EditFormPage" [ref=e133]
|
||||
- gridcell "3" [ref=e134]
|
||||
- gridcell "READY" [ref=e135]
|
||||
- row [ref=e136]:
|
||||
- gridcell [ref=e137]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e138] [cursor=pointer]
|
||||
- gridcell "T04" [ref=e139]
|
||||
- gridcell "Master-Detail" [ref=e140]
|
||||
- gridcell "MasterDetailCrudPage" [ref=e141]
|
||||
- gridcell "3" [ref=e142]
|
||||
- gridcell "READY" [ref=e143]
|
||||
- row [ref=e144]:
|
||||
- gridcell [ref=e145]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e146] [cursor=pointer]
|
||||
- gridcell "T05" [ref=e147]
|
||||
- gridcell "검토·승인 Workbench" [ref=e148]
|
||||
- gridcell "ApprovalWorkbenchPage" [ref=e149]
|
||||
- gridcell "3" [ref=e150]
|
||||
- gridcell "READY" [ref=e151]
|
||||
- row [ref=e152]:
|
||||
- gridcell [ref=e153]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e154] [cursor=pointer]
|
||||
- gridcell "T06" [ref=e155]
|
||||
- gridcell "단계 Wizard" [ref=e156]
|
||||
- gridcell "StepWizardPage" [ref=e157]
|
||||
- gridcell "3" [ref=e158]
|
||||
- gridcell "READY" [ref=e159]
|
||||
- row [ref=e160]:
|
||||
- gridcell [ref=e161]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e162] [cursor=pointer]
|
||||
- gridcell "T07" [ref=e163]
|
||||
- gridcell "Dashboard·Scorecard" [ref=e164]
|
||||
- gridcell "ScorecardDashboardPage" [ref=e165]
|
||||
- gridcell "3" [ref=e166]
|
||||
- gridcell "READY" [ref=e167]
|
||||
- row [ref=e168]:
|
||||
- gridcell [ref=e169]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e170] [cursor=pointer]
|
||||
- gridcell "T08" [ref=e171]
|
||||
- gridcell "Batch·데이터 운영" [ref=e172]
|
||||
- gridcell "BatchOperationsPageV2" [ref=e173]
|
||||
- gridcell "3" [ref=e174]
|
||||
- gridcell "READY" [ref=e175]
|
||||
- row [ref=e176]:
|
||||
- gridcell [ref=e177]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e178] [cursor=pointer]
|
||||
- gridcell "T09" [ref=e179]
|
||||
- gridcell "대사·예외 처리" [ref=e180]
|
||||
- gridcell "ReconciliationExceptionPage" [ref=e181]
|
||||
- gridcell "3" [ref=e182]
|
||||
- gridcell "READY" [ref=e183]
|
||||
- row [ref=e184]:
|
||||
- gridcell [ref=e185]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e186] [cursor=pointer]
|
||||
- gridcell "T10" [ref=e187]
|
||||
- gridcell "버전 비교·거버넌스" [ref=e188]
|
||||
- gridcell "VersionGovernancePage" [ref=e189]
|
||||
- gridcell "3" [ref=e190]
|
||||
- gridcell "READY" [ref=e191]
|
||||
- row [ref=e192]:
|
||||
- gridcell [ref=e193]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e194] [cursor=pointer]
|
||||
- gridcell "T11" [ref=e195]
|
||||
- gridcell "대량 입력(Fast Grid Entry)" [ref=e196]
|
||||
- gridcell "FastEntryGridPage" [ref=e197]
|
||||
- gridcell "3" [ref=e198]
|
||||
- gridcell "READY" [ref=e199]
|
||||
- row [ref=e200]:
|
||||
- gridcell [ref=e201]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e202] [cursor=pointer]
|
||||
- gridcell "T12" [ref=e203]
|
||||
- gridcell "작업 큐(Work Queue)" [ref=e204]
|
||||
- gridcell "WorkQueuePage" [ref=e205]
|
||||
- gridcell "2" [ref=e206]
|
||||
- gridcell "READY" [ref=e207]
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- region [ref=e215]:
|
||||
- generic [ref=e216]:
|
||||
- heading "컴포넌트 동작 프리뷰" [level=2] [ref=e217]
|
||||
- paragraph [ref=e218]: 공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.
|
||||
- generic [ref=e219]:
|
||||
- generic [ref=e220]:
|
||||
- heading "Actions" [level=3] [ref=e221]
|
||||
- button "기본 버튼" [ref=e222] [cursor=pointer]
|
||||
- button "주의 상태" [ref=e223] [cursor=pointer]
|
||||
- generic [ref=e224]:
|
||||
- heading "Status" [level=3] [ref=e225]
|
||||
- generic [ref=e226]:
|
||||
- generic [ref=e227]: READY
|
||||
- generic [ref=e229]: REVIEW
|
||||
- generic [ref=e231]: BLOCKED
|
||||
- generic [ref=e233]:
|
||||
- heading "Inputs" [level=3] [ref=e234]
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]: 텍스트 필드
|
||||
- textbox "텍스트 필드" [ref=e237]: 샘플 값
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]: 선택 필드
|
||||
- combobox "준비" [ref=e241]
|
||||
- region [ref=e245]:
|
||||
- generic [ref=e246]:
|
||||
- heading "입력 유형 기능 테스트" [level=2] [ref=e247]
|
||||
- paragraph [ref=e248]: 서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.
|
||||
- alert [ref=e291]:
|
||||
- strong [ref=e292]: 저장할 수 없습니다. 1개 항목을 확인하세요.
|
||||
- list [ref=e293]:
|
||||
- listitem [ref=e294]: 0보다 큰 금액을 입력하세요.
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e250]:
|
||||
- heading "기본 입력" [level=2] [ref=e251]
|
||||
- paragraph [ref=e252]: 필수 항목과 유효성 상태를 조작할 수 있습니다.
|
||||
- button "읽기 전용" [ref=e254] [cursor=pointer]
|
||||
- group "기능 테스트 입력" [ref=e256]:
|
||||
- generic [ref=e257]:
|
||||
- generic [ref=e258]: 이름
|
||||
- textbox "이름" [active] [ref=e259]: 테스트
|
||||
- generic [ref=e260]:
|
||||
- generic [ref=e261]: 금액
|
||||
- spinbutton "금액" [invalid] [ref=e263]
|
||||
- alert [ref=e295]: 0보다 커야 합니다.
|
||||
- generic [ref=e264]:
|
||||
- generic [ref=e265]: 기준일
|
||||
- generic [ref=e266]:
|
||||
- combobox "기준일" [ref=e267]
|
||||
- button "Choose Date" [ref=e268]
|
||||
- generic [ref=e271] [cursor=pointer]:
|
||||
- checkbox "검증 조건을 확인했습니다" [checked] [ref=e273]
|
||||
- generic [ref=e274]: 검증 조건을 확인했습니다
|
||||
- generic [ref=e276]:
|
||||
- button "검증 실행" [ref=e277] [cursor=pointer]
|
||||
- button "초기화" [ref=e278] [cursor=pointer]
|
||||
- complementary [ref=e279]:
|
||||
- generic [ref=e280]:
|
||||
- heading "교체 계약" [level=2] [ref=e281]
|
||||
- paragraph [ref=e282]:
|
||||
- text: 기본 공급자 교체는
|
||||
- code [ref=e283]: VITE_UI_ADAPTER
|
||||
- text: 와 provider registry에서 수행한다. Feature 코드는 변경하지 않는다.
|
||||
- paragraph [ref=e284]: PrimeVue+AG Grid와 Native reference adapter가 동일 contract test를 통과해야 한다.
|
||||
- paragraph [ref=e285]: 생산 교체는 접근성, 키보드, 상태행렬, 시각회귀, 대량목록 성능을 별도 Gate로 검증한다.
|
||||
- text: "데이터 기준시각: 2026-08-02"
|
||||
- contentinfo [ref=e286]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||
- generic "애플리케이션 버전" [ref=e287]: v0.1.0 · UI contract 4.0
|
||||
@@ -0,0 +1,174 @@
|
||||
- generic [ref=e3]:
|
||||
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||
- /url: "#ks-main"
|
||||
- banner [ref=e5]:
|
||||
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=e6] [cursor=pointer]:
|
||||
- strong [ref=e7]: K-ArtSell Aegis
|
||||
- generic [ref=e8]: IMPLEMENTATION_TEMPLATE
|
||||
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=e9]:
|
||||
- generic [ref=e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||
- generic [ref=e11]: Ctrl K
|
||||
- status [ref=e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||
- navigation "열린 업무" [ref=e13]:
|
||||
- generic [ref=e14]:
|
||||
- button "컴포넌트 확인" [ref=e15] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e17] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e18] [cursor=pointer]: ×
|
||||
- generic [ref=e19]:
|
||||
- complementary "주요 메뉴" [ref=e20]:
|
||||
- button "« 접기" [expanded] [ref=e21] [cursor=pointer]
|
||||
- generic [ref=e22]:
|
||||
- heading "Design System" [level=2] [ref=e23]
|
||||
- link "컴포넌트 확인" [ref=e24] [cursor=pointer]:
|
||||
- /url: /internal/ui-standard
|
||||
- generic [ref=e25]:
|
||||
- heading "Operations" [level=2] [ref=e26]
|
||||
- link "데이터 품질" [ref=e27] [cursor=pointer]:
|
||||
- /url: /ops/data-quality
|
||||
- link "시장 데이터 수집" [ref=e28] [cursor=pointer]:
|
||||
- /url: /ops/market-data-ingestion
|
||||
- link "수집 이력" [ref=e29] [cursor=pointer]:
|
||||
- /url: /ops/market-data-history
|
||||
- link "모델 운영" [ref=e30] [cursor=pointer]:
|
||||
- /url: /ops/model-operations
|
||||
- generic [ref=e31]:
|
||||
- heading "Portfolio" [level=2] [ref=e32]
|
||||
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||
- /url: /portfolio/risk
|
||||
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||
- /url: /portfolio/rebalance
|
||||
- generic [ref=e35]:
|
||||
- heading "Research" [level=2] [ref=e36]
|
||||
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||
- /url: /research/sell-decision
|
||||
- main [ref=e38]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- heading "표준 UI 패턴" [level=1] [ref=e42]
|
||||
- paragraph [ref=e43]: Feature는 공급자 라이브러리를 직접 사용하지 않고, v4 어댑터·레이아웃·화면 계약을 사용한다.
|
||||
- generic [ref=e44]:
|
||||
- generic [ref=e45]: "상태: READY"
|
||||
- generic [ref=e46]: "As-of: 2026-08-02"
|
||||
- generic [ref=e47]: "Version: UI-CONTRACT-4.0"
|
||||
- button "새 화면 패킷" [ref=e49] [cursor=pointer]
|
||||
- generic [ref=e50]:
|
||||
- generic [ref=e51]:
|
||||
- strong [ref=e52]: "10"
|
||||
- generic [ref=e53]: 화면 타입
|
||||
- generic [ref=e54]:
|
||||
- strong [ref=e55]: "14"
|
||||
- generic [ref=e56]: 어댑터 포트
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: primevue-aggrid
|
||||
- generic [ref=e60]: PrimeVue + AG Grid Community
|
||||
- generic [ref=e61]:
|
||||
- generic [ref=e62]: 자동주문 OFF
|
||||
- generic [ref=e64]: 고정 경계
|
||||
- generic [ref=e66]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: 검색
|
||||
- textbox "검색" [ref=e69]:
|
||||
- /placeholder: 화면 ID, 타입 또는 컴포넌트
|
||||
- text: T01
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: 상태
|
||||
- combobox "전체" [ref=e73]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e81]:
|
||||
- main [ref=e82]:
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: No Rows To Show
|
||||
- grid [ref=e87]:
|
||||
- rowgroup [ref=e88]:
|
||||
- row [ref=e89]:
|
||||
- columnheader [ref=e90]
|
||||
- columnheader "화면 ID" [ref=e91]:
|
||||
- generic [ref=e93] [cursor=pointer]
|
||||
- columnheader "화면 타입" [ref=e95]:
|
||||
- generic [ref=e97] [cursor=pointer]
|
||||
- columnheader "표준 컴포넌트" [ref=e99]:
|
||||
- generic [ref=e101] [cursor=pointer]
|
||||
- columnheader "필수 증거" [ref=e103]:
|
||||
- generic [ref=e105] [cursor=pointer]
|
||||
- columnheader "상태" [ref=e107]:
|
||||
- generic [ref=e109] [cursor=pointer]
|
||||
- rowgroup [ref=e111]:
|
||||
- row [ref=e296]:
|
||||
- gridcell [ref=e297]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e298] [cursor=pointer]
|
||||
- gridcell "T01" [ref=e299]
|
||||
- gridcell "검색·목록형 CRUD" [ref=e300]
|
||||
- gridcell "SearchListCrudPage" [ref=e301]
|
||||
- gridcell "3" [ref=e302]
|
||||
- gridcell "READY" [ref=e303]
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- region [ref=e215]:
|
||||
- generic [ref=e216]:
|
||||
- heading "컴포넌트 동작 프리뷰" [level=2] [ref=e217]
|
||||
- paragraph [ref=e218]: 공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.
|
||||
- generic [ref=e219]:
|
||||
- generic [ref=e220]:
|
||||
- heading "Actions" [level=3] [ref=e221]
|
||||
- button "기본 버튼" [ref=e222] [cursor=pointer]
|
||||
- button "주의 상태" [ref=e223] [cursor=pointer]
|
||||
- generic [ref=e224]:
|
||||
- heading "Status" [level=3] [ref=e225]
|
||||
- generic [ref=e226]:
|
||||
- generic [ref=e227]: READY
|
||||
- generic [ref=e229]: REVIEW
|
||||
- generic [ref=e231]: BLOCKED
|
||||
- generic [ref=e233]:
|
||||
- heading "Inputs" [level=3] [ref=e234]
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]: 텍스트 필드
|
||||
- textbox "텍스트 필드" [ref=e237]: 샘플 값
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]: 선택 필드
|
||||
- combobox "준비" [ref=e241]
|
||||
- region [ref=e245]:
|
||||
- generic [ref=e246]:
|
||||
- heading "입력 유형 기능 테스트" [level=2] [ref=e247]
|
||||
- paragraph [ref=e248]: 서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.
|
||||
- alert [ref=e291]:
|
||||
- strong [ref=e292]: 저장할 수 없습니다. 1개 항목을 확인하세요.
|
||||
- list [ref=e293]:
|
||||
- listitem [ref=e294]: 0보다 큰 금액을 입력하세요.
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e250]:
|
||||
- heading "기본 입력" [level=2] [ref=e251]
|
||||
- paragraph [ref=e252]: 필수 항목과 유효성 상태를 조작할 수 있습니다.
|
||||
- button "편집 허용" [active] [ref=e304] [cursor=pointer]
|
||||
- group "기능 테스트 입력" [ref=e256]:
|
||||
- generic [ref=e257]:
|
||||
- generic [ref=e258]: 이름
|
||||
- textbox "이름" [disabled] [ref=e259]: 테스트
|
||||
- generic [ref=e260]:
|
||||
- generic [ref=e261]: 금액
|
||||
- spinbutton "금액" [disabled] [invalid] [ref=e263]
|
||||
- alert [ref=e295]: 0보다 커야 합니다.
|
||||
- generic [ref=e264]:
|
||||
- generic [ref=e265]: 기준일
|
||||
- generic [ref=e266]:
|
||||
- combobox "기준일" [disabled] [ref=e267]
|
||||
- button "Choose Date" [disabled] [ref=e268]
|
||||
- generic [ref=e271] [cursor=pointer]:
|
||||
- checkbox "검증 조건을 확인했습니다" [checked] [disabled] [ref=e273]
|
||||
- generic [ref=e274]: 검증 조건을 확인했습니다
|
||||
- generic [ref=e276]:
|
||||
- button "검증 실행" [disabled] [ref=e277]
|
||||
- button "초기화" [ref=e278] [cursor=pointer]
|
||||
- complementary [ref=e279]:
|
||||
- generic [ref=e280]:
|
||||
- heading "교체 계약" [level=2] [ref=e281]
|
||||
- paragraph [ref=e282]:
|
||||
- text: 기본 공급자 교체는
|
||||
- code [ref=e283]: VITE_UI_ADAPTER
|
||||
- text: 와 provider registry에서 수행한다. Feature 코드는 변경하지 않는다.
|
||||
- paragraph [ref=e284]: PrimeVue+AG Grid와 Native reference adapter가 동일 contract test를 통과해야 한다.
|
||||
- paragraph [ref=e285]: 생산 교체는 접근성, 키보드, 상태행렬, 시각회귀, 대량목록 성능을 별도 Gate로 검증한다.
|
||||
- text: "데이터 기준시각: 2026-08-02"
|
||||
- contentinfo [ref=e286]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||
- generic "애플리케이션 버전" [ref=e287]: v0.1.0 · UI contract 4.0
|
||||
@@ -0,0 +1,208 @@
|
||||
- generic [ref=e3]:
|
||||
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||
- /url: "#ks-main"
|
||||
- banner [ref=e5]:
|
||||
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=e6] [cursor=pointer]:
|
||||
- strong [ref=e7]: K-ArtSell Aegis
|
||||
- generic [ref=e8]: IMPLEMENTATION_TEMPLATE
|
||||
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=e9]:
|
||||
- generic [ref=e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||
- generic [ref=e11]: Ctrl K
|
||||
- status [ref=e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||
- navigation "열린 업무" [ref=e13]:
|
||||
- generic [ref=e14]:
|
||||
- button "컴포넌트 확인" [ref=e15] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e17] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e18] [cursor=pointer]: ×
|
||||
- generic [ref=e19]:
|
||||
- complementary "주요 메뉴" [ref=e20]:
|
||||
- button "« 접기" [expanded] [ref=e21] [cursor=pointer]
|
||||
- generic [ref=e22]:
|
||||
- heading "Design System" [level=2] [ref=e23]
|
||||
- link "컴포넌트 확인" [ref=e24] [cursor=pointer]:
|
||||
- /url: /internal/ui-standard
|
||||
- generic [ref=e25]:
|
||||
- heading "Operations" [level=2] [ref=e26]
|
||||
- link "데이터 품질" [ref=e27] [cursor=pointer]:
|
||||
- /url: /ops/data-quality
|
||||
- link "시장 데이터 수집" [ref=e28] [cursor=pointer]:
|
||||
- /url: /ops/market-data-ingestion
|
||||
- link "수집 이력" [ref=e29] [cursor=pointer]:
|
||||
- /url: /ops/market-data-history
|
||||
- link "모델 운영" [ref=e30] [cursor=pointer]:
|
||||
- /url: /ops/model-operations
|
||||
- generic [ref=e31]:
|
||||
- heading "Portfolio" [level=2] [ref=e32]
|
||||
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||
- /url: /portfolio/risk
|
||||
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||
- /url: /portfolio/rebalance
|
||||
- generic [ref=e35]:
|
||||
- heading "Research" [level=2] [ref=e36]
|
||||
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||
- /url: /research/sell-decision
|
||||
- main [ref=e38]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]:
|
||||
- heading "표준 UI 패턴" [level=1] [ref=e42]
|
||||
- paragraph [ref=e43]: 화면 유형을 선택하고, 실제 화면 ID를 선택해 기능 화면을 엽니다.
|
||||
- generic [ref=e44]:
|
||||
- generic [ref=e45]: "상태: READY"
|
||||
- generic [ref=e46]: "As-of: 2026-08-02"
|
||||
- generic [ref=e47]: "Version: UI-CONTRACT-4.0"
|
||||
- button "선택 화면 열기" [ref=e49] [cursor=pointer]
|
||||
- generic [ref=e50]:
|
||||
- generic [ref=e51]:
|
||||
- strong [ref=e52]: "12"
|
||||
- generic [ref=e53]: 화면 타입
|
||||
- generic [ref=e54]:
|
||||
- strong [ref=e55]: "7"
|
||||
- generic [ref=e56]: 연결된 기능 화면
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: primevue-aggrid
|
||||
- generic [ref=e60]: PrimeVue + AG Grid Community
|
||||
- generic [ref=e61]:
|
||||
- generic [ref=e62]: 자동주문 OFF
|
||||
- generic [ref=e64]: 고정 경계
|
||||
- generic [ref=e66]:
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: 화면 검색
|
||||
- textbox "화면 검색" [ref=e69]:
|
||||
- /placeholder: 화면 ID, 기능명 또는 화면 유형
|
||||
- generic [ref=e70]:
|
||||
- generic [ref=e71]: 화면 유형
|
||||
- combobox "전체 화면 유형" [ref=e73]
|
||||
- generic [ref=e79]:
|
||||
- generic [ref=e81]:
|
||||
- main [ref=e82]:
|
||||
- generic [ref=e85]:
|
||||
- generic [ref=e86]: Press SPACE to select this row
|
||||
- grid [ref=e87]:
|
||||
- rowgroup [ref=e88]:
|
||||
- row [ref=e89]:
|
||||
- columnheader [ref=e90]
|
||||
- columnheader "화면 ID" [ref=e91]:
|
||||
- generic [ref=e93] [cursor=pointer]
|
||||
- columnheader "기능 화면" [ref=e95]:
|
||||
- generic [ref=e97] [cursor=pointer]
|
||||
- columnheader "유형" [ref=e99]:
|
||||
- generic [ref=e101] [cursor=pointer]
|
||||
- columnheader "화면 유형" [ref=e103]:
|
||||
- generic [ref=e105] [cursor=pointer]
|
||||
- rowgroup [ref=e107]:
|
||||
- row [selected] [ref=e108]:
|
||||
- gridcell [ref=e109]:
|
||||
- checkbox "Press Space to toggle row selection (checked)" [checked] [ref=e110] [cursor=pointer]
|
||||
- gridcell "SCR-002" [ref=e111]
|
||||
- gridcell "매도 의사결정" [ref=e112]
|
||||
- gridcell "T03" [ref=e113]
|
||||
- gridcell "등록·편집 Form" [ref=e114]
|
||||
- row [ref=e115]:
|
||||
- gridcell [ref=e116]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e117] [cursor=pointer]
|
||||
- gridcell "SCR-013" [ref=e118]
|
||||
- gridcell "데이터 품질" [ref=e119]
|
||||
- gridcell "T08" [ref=e120]
|
||||
- gridcell "Batch·데이터 운영" [ref=e121]
|
||||
- row [ref=e122]:
|
||||
- gridcell [ref=e123]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e124] [cursor=pointer]
|
||||
- gridcell "SCR-015" [ref=e125]
|
||||
- gridcell "모델 운영" [ref=e126]
|
||||
- gridcell "T10" [ref=e127]
|
||||
- gridcell "버전 비교·거버넌스" [ref=e128]
|
||||
- row [ref=e129]:
|
||||
- gridcell [ref=e130]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e131] [cursor=pointer]
|
||||
- gridcell "SCR-016" [ref=e132]
|
||||
- gridcell "시장 데이터 수집" [active] [ref=e133]
|
||||
- gridcell "T08" [ref=e134]
|
||||
- gridcell "Batch·데이터 운영" [ref=e135]
|
||||
- row [ref=e136]:
|
||||
- gridcell [ref=e137]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e138] [cursor=pointer]
|
||||
- gridcell "SCR-017" [ref=e139]
|
||||
- gridcell "수집 이력" [ref=e140]
|
||||
- gridcell "T08" [ref=e141]
|
||||
- gridcell "Batch·데이터 운영" [ref=e142]
|
||||
- row [ref=e143]:
|
||||
- gridcell [ref=e144]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e145] [cursor=pointer]
|
||||
- gridcell "SCR-018" [ref=e146]
|
||||
- gridcell "포트폴리오 리스크" [ref=e147]
|
||||
- gridcell "T07" [ref=e148]
|
||||
- gridcell "Dashboard·Scorecard" [ref=e149]
|
||||
- row [ref=e150]:
|
||||
- gridcell [ref=e151]:
|
||||
- checkbox "Press Space to toggle row selection (unchecked)" [ref=e152] [cursor=pointer]
|
||||
- gridcell "SCR-019" [ref=e153]
|
||||
- gridcell "리밸런싱 제안" [ref=e154]
|
||||
- gridcell "T03" [ref=e155]
|
||||
- gridcell "등록·편집 Form" [ref=e156]
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- region [ref=e160]:
|
||||
- generic [ref=e161]:
|
||||
- heading "컴포넌트 동작 프리뷰" [level=2] [ref=e162]
|
||||
- paragraph [ref=e163]: 공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.
|
||||
- generic [ref=e164]:
|
||||
- generic [ref=e165]:
|
||||
- heading "Actions" [level=3] [ref=e166]
|
||||
- button "기본 버튼" [ref=e167] [cursor=pointer]
|
||||
- button "주의 상태" [ref=e168] [cursor=pointer]
|
||||
- generic [ref=e169]:
|
||||
- heading "Status" [level=3] [ref=e170]
|
||||
- generic [ref=e171]:
|
||||
- generic [ref=e172]: READY
|
||||
- generic [ref=e174]: REVIEW
|
||||
- generic [ref=e176]: BLOCKED
|
||||
- generic [ref=e178]:
|
||||
- heading "Inputs" [level=3] [ref=e179]
|
||||
- generic [ref=e180]:
|
||||
- generic [ref=e181]: 텍스트 필드
|
||||
- textbox "텍스트 필드" [ref=e182]: 샘플 값
|
||||
- generic [ref=e183]:
|
||||
- generic [ref=e184]: 선택 필드
|
||||
- combobox "준비" [ref=e186]
|
||||
- region [ref=e190]:
|
||||
- generic [ref=e191]:
|
||||
- heading "입력 유형 기능 테스트" [level=2] [ref=e192]
|
||||
- paragraph [ref=e193]: 서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.
|
||||
- generic [ref=e194]:
|
||||
- generic [ref=e195]:
|
||||
- heading "기본 입력" [level=2] [ref=e196]
|
||||
- paragraph [ref=e197]: 필수 항목과 유효성 상태를 조작할 수 있습니다.
|
||||
- button "읽기 전용" [ref=e199] [cursor=pointer]
|
||||
- group "기능 테스트 입력" [ref=e201]:
|
||||
- generic [ref=e202]:
|
||||
- generic [ref=e203]: 이름
|
||||
- textbox "이름" [ref=e204]
|
||||
- generic [ref=e205]:
|
||||
- generic [ref=e206]: 금액
|
||||
- spinbutton "금액" [ref=e208]
|
||||
- generic [ref=e209]:
|
||||
- generic [ref=e210]: 기준일
|
||||
- generic [ref=e211]:
|
||||
- combobox "기준일" [ref=e212]
|
||||
- button "Choose Date" [ref=e213]
|
||||
- generic [ref=e216] [cursor=pointer]:
|
||||
- checkbox "검증 조건을 확인했습니다" [ref=e218]
|
||||
- generic [ref=e219]: 검증 조건을 확인했습니다
|
||||
- generic [ref=e221]:
|
||||
- button "검증 실행" [ref=e222] [cursor=pointer]
|
||||
- button "초기화" [ref=e223] [cursor=pointer]
|
||||
- complementary [ref=e224]:
|
||||
- generic [ref=e225]:
|
||||
- heading "선택한 기능 화면" [level=2] [ref=e226]
|
||||
- paragraph [ref=e230]:
|
||||
- strong [ref=e231]: SCR-016
|
||||
- text: · 시장 데이터 수집
|
||||
- paragraph [ref=e232]: T08 · Batch·데이터 운영
|
||||
- paragraph [ref=e233]:
|
||||
- code [ref=e234]: /ops/market-data-ingestion
|
||||
- button "선택 화면 열기" [ref=e235] [cursor=pointer]
|
||||
- text: "데이터 기준시각: 2026-08-02"
|
||||
- contentinfo [ref=e228]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||
- generic "애플리케이션 버전" [ref=e229]: v0.1.0 · UI contract 4.0
|
||||
@@ -0,0 +1,203 @@
|
||||
- generic [ref=e1]:
|
||||
- generic [ref=e3]:
|
||||
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||
- /url: "#ks-main"
|
||||
- banner [ref=e5]:
|
||||
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=e6] [cursor=pointer]:
|
||||
- strong [ref=e7]: K-ArtSell Aegis
|
||||
- generic [ref=e8]: IMPLEMENTATION_TEMPLATE
|
||||
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=e9]:
|
||||
- generic [ref=e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||
- generic [ref=e11]: Ctrl K
|
||||
- status [ref=e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||
- navigation "열린 업무" [ref=e13]:
|
||||
- generic [ref=e14]:
|
||||
- button "컴포넌트 확인" [ref=e15] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e17] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e18] [cursor=pointer]: ×
|
||||
- generic [ref=e222]:
|
||||
- button "데이터 품질" [ref=e223] [cursor=pointer]
|
||||
- button "탭 고정" [ref=e225] [cursor=pointer]: 📍
|
||||
- button "탭 닫기" [ref=e226] [cursor=pointer]: ×
|
||||
- generic [ref=e19]:
|
||||
- complementary "주요 메뉴" [ref=e20]:
|
||||
- button "« 접기" [expanded] [ref=e21] [cursor=pointer]
|
||||
- generic [ref=e22]:
|
||||
- heading "Design System" [level=2] [ref=e23]
|
||||
- link "컴포넌트 확인" [ref=e24] [cursor=pointer]:
|
||||
- /url: /internal/ui-standard
|
||||
- generic [ref=e25]:
|
||||
- heading "Operations" [level=2] [ref=e26]
|
||||
- link "데이터 품질" [ref=e27] [cursor=pointer]:
|
||||
- /url: /ops/data-quality
|
||||
- link "시장 데이터 수집" [ref=e28] [cursor=pointer]:
|
||||
- /url: /ops/market-data-ingestion
|
||||
- link "수집 이력" [ref=e29] [cursor=pointer]:
|
||||
- /url: /ops/market-data-history
|
||||
- link "모델 운영" [ref=e30] [cursor=pointer]:
|
||||
- /url: /ops/model-operations
|
||||
- generic [ref=e31]:
|
||||
- heading "Portfolio" [level=2] [ref=e32]
|
||||
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||
- /url: /portfolio/risk
|
||||
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||
- /url: /portfolio/rebalance
|
||||
- generic [ref=e35]:
|
||||
- heading "Research" [level=2] [ref=e36]
|
||||
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||
- /url: /research/sell-decision
|
||||
- main [ref=e38]:
|
||||
- generic [ref=e227]:
|
||||
- generic [ref=e228]:
|
||||
- generic [ref=e229]:
|
||||
- heading "표준 UI 패턴" [level=1] [ref=e230]
|
||||
- paragraph [ref=e231]: 화면 유형을 선택하고, 실제 화면 ID를 선택해 기능 화면을 엽니다.
|
||||
- generic [ref=e232]:
|
||||
- generic [ref=e233]: "상태: READY"
|
||||
- generic [ref=e234]: "As-of: 2026-08-02"
|
||||
- generic [ref=e235]: "Version: UI-CONTRACT-4.0"
|
||||
- button "선택 화면 열기" [disabled] [ref=e237]
|
||||
- generic [ref=e238]:
|
||||
- generic [ref=e239]:
|
||||
- strong [ref=e240]: "12"
|
||||
- generic [ref=e241]: 화면 타입
|
||||
- generic [ref=e242]:
|
||||
- strong [ref=e243]: "7"
|
||||
- generic [ref=e244]: 연결된 기능 화면
|
||||
- generic [ref=e245]:
|
||||
- generic [ref=e246]: primevue-aggrid
|
||||
- generic [ref=e248]: PrimeVue + AG Grid Community
|
||||
- generic [ref=e249]:
|
||||
- generic [ref=e250]: 자동주문 OFF
|
||||
- generic [ref=e252]: 고정 경계
|
||||
- generic [ref=e254]:
|
||||
- generic [ref=e255]:
|
||||
- generic [ref=e256]: 화면 검색
|
||||
- textbox "화면 검색" [ref=e257]:
|
||||
- /placeholder: 화면 ID, 기능명 또는 화면 유형
|
||||
- generic [ref=e258]:
|
||||
- generic [ref=e259]: 화면 유형
|
||||
- combobox "전체 화면 유형" [ref=e261]
|
||||
- generic [ref=e265]:
|
||||
- generic [ref=e266]: 열 화면
|
||||
- combobox "화면 ID를 선택하세요" [expanded] [active] [ref=e268]
|
||||
- generic [ref=e274]:
|
||||
- generic [ref=e276]:
|
||||
- main [ref=e277]:
|
||||
- grid [ref=e282]:
|
||||
- rowgroup [ref=e283]:
|
||||
- row [ref=e284]:
|
||||
- columnheader "화면 ID" [ref=e285]:
|
||||
- generic [ref=e287] [cursor=pointer]
|
||||
- columnheader "기능 화면" [ref=e289]:
|
||||
- generic [ref=e291] [cursor=pointer]
|
||||
- columnheader "유형" [ref=e293]:
|
||||
- generic [ref=e295] [cursor=pointer]
|
||||
- columnheader "화면 유형" [ref=e297]:
|
||||
- generic [ref=e299] [cursor=pointer]
|
||||
- rowgroup [ref=e301]:
|
||||
- row [ref=e302]:
|
||||
- gridcell "SCR-002" [ref=e303]
|
||||
- gridcell "매도 의사결정" [ref=e304]
|
||||
- gridcell "T03" [ref=e305]
|
||||
- gridcell "등록·편집 Form" [ref=e306]
|
||||
- row [ref=e307]:
|
||||
- gridcell "SCR-013" [ref=e308]
|
||||
- gridcell "데이터 품질" [ref=e309]
|
||||
- gridcell "T08" [ref=e310]
|
||||
- gridcell "Batch·데이터 운영" [ref=e311]
|
||||
- row [ref=e312]:
|
||||
- gridcell "SCR-015" [ref=e313]
|
||||
- gridcell "모델 운영" [ref=e314]
|
||||
- gridcell "T10" [ref=e315]
|
||||
- gridcell "버전 비교·거버넌스" [ref=e316]
|
||||
- row [ref=e317]:
|
||||
- gridcell "SCR-016" [ref=e318]
|
||||
- gridcell "시장 데이터 수집" [ref=e319]
|
||||
- gridcell "T08" [ref=e320]
|
||||
- gridcell "Batch·데이터 운영" [ref=e321]
|
||||
- row [ref=e322]:
|
||||
- gridcell "SCR-017" [ref=e323]
|
||||
- gridcell "수집 이력" [ref=e324]
|
||||
- gridcell "T08" [ref=e325]
|
||||
- gridcell "Batch·데이터 운영" [ref=e326]
|
||||
- row [ref=e327]:
|
||||
- gridcell "SCR-018" [ref=e328]
|
||||
- gridcell "포트폴리오 리스크" [ref=e329]
|
||||
- gridcell "T07" [ref=e330]
|
||||
- gridcell "Dashboard·Scorecard" [ref=e331]
|
||||
- row [ref=e332]:
|
||||
- gridcell "SCR-019" [ref=e333]
|
||||
- gridcell "리밸런싱 제안" [ref=e334]
|
||||
- gridcell "T03" [ref=e335]
|
||||
- gridcell "등록·편집 Form" [ref=e336]
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- rowgroup
|
||||
- region [ref=e340]:
|
||||
- generic [ref=e341]:
|
||||
- heading "컴포넌트 동작 프리뷰" [level=2] [ref=e342]
|
||||
- paragraph [ref=e343]: 공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.
|
||||
- generic [ref=e344]:
|
||||
- generic [ref=e345]:
|
||||
- heading "Actions" [level=3] [ref=e346]
|
||||
- button "기본 버튼" [ref=e347] [cursor=pointer]
|
||||
- button "주의 상태" [ref=e348] [cursor=pointer]
|
||||
- generic [ref=e349]:
|
||||
- heading "Status" [level=3] [ref=e350]
|
||||
- generic [ref=e351]:
|
||||
- generic [ref=e352]: READY
|
||||
- generic [ref=e354]: REVIEW
|
||||
- generic [ref=e356]: BLOCKED
|
||||
- generic [ref=e358]:
|
||||
- heading "Inputs" [level=3] [ref=e359]
|
||||
- generic [ref=e360]:
|
||||
- generic [ref=e361]: 텍스트 필드
|
||||
- textbox "텍스트 필드" [ref=e362]: 샘플 값
|
||||
- generic [ref=e363]:
|
||||
- generic [ref=e364]: 선택 필드
|
||||
- combobox "준비" [ref=e366]
|
||||
- region [ref=e370]:
|
||||
- generic [ref=e371]:
|
||||
- heading "입력 유형 기능 테스트" [level=2] [ref=e372]
|
||||
- paragraph [ref=e373]: 서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.
|
||||
- generic [ref=e374]:
|
||||
- generic [ref=e375]:
|
||||
- heading "기본 입력" [level=2] [ref=e376]
|
||||
- paragraph [ref=e377]: 필수 항목과 유효성 상태를 조작할 수 있습니다.
|
||||
- button "읽기 전용" [ref=e379] [cursor=pointer]
|
||||
- group "기능 테스트 입력" [ref=e381]:
|
||||
- generic [ref=e382]:
|
||||
- generic [ref=e383]: 이름
|
||||
- textbox "이름" [ref=e384]
|
||||
- generic [ref=e385]:
|
||||
- generic [ref=e386]: 금액
|
||||
- spinbutton "금액" [ref=e388]
|
||||
- generic [ref=e389]:
|
||||
- generic [ref=e390]: 기준일
|
||||
- generic [ref=e391]:
|
||||
- combobox "기준일" [ref=e392]
|
||||
- button "Choose Date" [ref=e393]
|
||||
- generic [ref=e396] [cursor=pointer]:
|
||||
- checkbox "검증 조건을 확인했습니다" [ref=e398]
|
||||
- generic [ref=e399]: 검증 조건을 확인했습니다
|
||||
- generic [ref=e401]:
|
||||
- button "검증 실행" [ref=e402] [cursor=pointer]
|
||||
- button "초기화" [ref=e403] [cursor=pointer]
|
||||
- complementary [ref=e404]:
|
||||
- generic [ref=e405]:
|
||||
- heading "선택한 기능 화면" [level=2] [ref=e406]
|
||||
- paragraph [ref=e407]: 목록에서 화면 ID를 선택한 뒤, “선택 화면 열기”를 누르세요.
|
||||
- text: "데이터 기준시각: 2026-08-02"
|
||||
- contentinfo [ref=e220]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||
- generic "애플리케이션 버전" [ref=e221]: v0.1.0 · UI contract 4.0
|
||||
- generic [ref=e408]:
|
||||
- listbox [ref=e410]:
|
||||
- option "SCR-002 · 매도 의사결정" [ref=e411]
|
||||
- option "SCR-013 · 데이터 품질" [ref=e412]
|
||||
- option "SCR-015 · 모델 운영" [ref=e413]
|
||||
- option "SCR-016 · 시장 데이터 수집" [ref=e414]
|
||||
- option "SCR-017 · 수집 이력" [ref=e415]
|
||||
- option "SCR-018 · 포트폴리오 리스크" [ref=e416]
|
||||
- option "SCR-019 · 리밸런싱 제안" [ref=e417]
|
||||
- status: No selected item
|
||||
+63
-258
@@ -1,285 +1,90 @@
|
||||
# 🚀 K-ArtSell Aegis v16.0 - 현재 진행 로드맵
|
||||
|
||||
**상태:** 95% 완료 (Phase 2-3 구현 완료, Gate 3만 검증 필요)
|
||||
**마지막 업데이트:** 2026-08-03 02:00 KST
|
||||
**관리자:** Claude Code + 향후 Codex 연계
|
||||
**최종 갱신:** 2026-08-08 (VS 번호 재배정 — 아래 "알려진 문서 정합성 문제" 1번 참조. 2026-08-07 갱신 내용은 실제 코드/테스트를 직접 확인한 결과였고 이번 갱신은 그 위에 번호 충돌만 정정한 것입니다.)
|
||||
|
||||
**상태 요약:** VS-27(감사 추적), VS-10(매도 결정), VS-28(거래 실행), VS-29(포트폴리오 대사) 백엔드 구현 + 테스트 완료. VS-26(승인 워크플로우, 구 VS-03)은 DEBT-017(중복 구현) 아키텍트 결정이 2026-08-08에 내려지고 실행되었으나(죽은 구현 삭제, 유일 구현에 통합 테스트 신규 작성), 그 테스트를 실 PostgreSQL로 검증하지 못해 여전히 **BLOCKED**. Phase 1 Shadow Run(Gate 5a, 252+ 거래일 검증)은 **아직 시작되지 않음** (과거 "RUNNING" 기록은 허위였음이 이미 문서로 정정됨). 상세 항목별 상태는 `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` 참조.
|
||||
|
||||
---
|
||||
|
||||
## 📍 Current Sprint (이번 주)
|
||||
## ⚠️ 알려진 문서 정합성 문제 (DECISION_REQUIRED)
|
||||
|
||||
### ✅ 완료 (4개)
|
||||
|
||||
#### 1. Idempotency 버그 수정
|
||||
- **Commit:** 9a2d939
|
||||
- **파일:** RecommendationReportGenerator.cs, 3x Job classes
|
||||
- **내용:**
|
||||
- ADO pattern으로 HasReportBeenSentAsync/MarkReportSentAsync 복구
|
||||
- Daily/Weekly/Monthly 모든 Job에 idempotency 체크/마크 복구
|
||||
- CLAUDE.md blocking rule 준수: "No partial success"
|
||||
- **검증:** Build 0 errors, 모든 Job 테스트됨
|
||||
|
||||
#### 2. Serilog Telegram 알림 통합
|
||||
- **이전 커밋:** (4519fa8)
|
||||
- **파일:** TelegramSink.cs
|
||||
- **내용:**
|
||||
- ERROR/FATAL 로그 → Telegram 자동 발송
|
||||
- 동기 호출 + 오류 침묵 처리
|
||||
- Markdown 포맷 + 타임스탬프
|
||||
|
||||
#### 3. Daily/Weekly/Monthly Recommendation Reports
|
||||
- **이전 커밋:** (4519fa8)
|
||||
- **파일:** 3x Job 클래스 + RecommendationReportGenerator
|
||||
- **내용:**
|
||||
- Daily: 09:00 KST 매일
|
||||
- Weekly: 09:00 KST 토요일 (사용자 요청)
|
||||
- Monthly: 09:00 KST 1일
|
||||
- SignalEngine.sell_decisions 집계 + Telegram 발송
|
||||
|
||||
#### 4. Phase 1 API 최적화 완료
|
||||
- **Commit:** eb106d5
|
||||
- **파일:**
|
||||
- KrxDataService.cs (exponential backoff)
|
||||
- TelegramSinkAsync.cs (new, async queue)
|
||||
- DataBackfiller.cs (30-day batch)
|
||||
- ApiCallMetricsService.cs (new, 24h metrics)
|
||||
- Program.cs (TelegramSinkAsync 등록)
|
||||
- **내용:**
|
||||
- KRX: 지수 백오프 (100ms → 30s) + X-RateLimit-Remaining 모니터링
|
||||
- Telegram: 논블로킹 큐, 100ms 간격, 3회 재시도
|
||||
- DataBackfiller: 252일 → 9회 호출 (97% ↓)
|
||||
- Metrics: API별 성공/실패/레이턴시/할당량 추적
|
||||
- **효과:** Shadow run 4분 → 1초 (75% ↓), 신뢰성 ↑
|
||||
1. **VS 번호 체계 충돌 — 2026-08-08 부분 해결:** `WBS_MASTER.csv`(원 계획)와 `WBS_PROGRESS_TRACKER.csv`(실행 트래커) 사이의 VS-03/VS-04/VS-12/VS-14 충돌은 트래커 쪽 4개 슬라이스(승인워크플로우/감사추적/거래실행/포트폴리오대사)를 VS-26/27/28/29로 재번호 부여하여 해결했습니다. 근거: `docs/DECISIONS/ADR-WBS-001-slice-renumbering.md`.
|
||||
- **2026-08-08 후속 갱신 (TECH_DEBT_REGISTER.md DEBT-017 해결):** 죽은 구현(`src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/`, `[DontRegister]`)과 그 전용 테스트 파일을 삭제했습니다. 살아있는 `Features/ApprovalWorkflow/`가 이제 유일한 구현이며, 동일 시나리오(생성/승인/활성화 역할 검증, maker≠checker 분리, 증거 첨부, `DateOnly` 라운드트립)를 검증하는 Handler+Sql+실DB 통합 테스트를 새로 작성했습니다. 포팅 과정에서 살아있는 구현의 `Sql.cs`에도 죽은 코드에 있던 것과 동일한 Dapper `DateOnly` 바인딩 버그가 있음을 발견해 동일한 방식으로 수정했습니다. 다만 **이 세션에서도 실 PostgreSQL에 연결할 수 없어(127.0.0.1:5432 연결 거부, SSH 터널 미개통) 새 통합 테스트 8건은 하나도 실행 검증되지 않았습니다** — 순수 Policy 테스트 10건만 통과 확인. 그래서 AEG-VS-26-01은 여전히 `BLOCKED`입니다. 이번 정리 과정에서 두 가지 잔여 결함도 발견했습니다(이번 세션이 만든 결함 아님, 기존부터 있었음): `GET /approvals/{id}` 엔드포인트가 없어 승인 후 증거(evidence)를 HTTP로 조회할 방법이 없고, Draft→Proposed 전환을 호출하는 Handler/Endpoint가 어디에도 없어 실제로는 승인 API가 끝까지 도달 불가능한 상태입니다 — TECH_DEBT_REGISTER.md DEBT-025/DEBT-026으로 신규 등록했습니다.
|
||||
- **또 다른 발견 — 미추적 작업:** `src/KArtSell.Host/Features/MarketData/VS03_*.cs`, `Features/Portfolio/VS04_*.cs`/`VS05_*.cs`/`VS08_*.cs`는 실제 구현되고 테스트도 있는(commits `2bc2b1e`, `32b49a4`, `14c5e4f`, `2eee44d`) **세 번째** VS-03/04/05/08 사용례(Market Data Ingestion Dashboard, Portfolio Rebalance, Risk Metrics, Dashboard)인데, `WBS_PROGRESS_TRACKER.csv`에 전혀 기록되어 있지 않습니다. 다음 세션에서 이 작업을 검증(빌드/테스트 재현, 프런트엔드 존재 여부 확인)하고 트래커에 추가해야 합니다.
|
||||
2. **DbUp 마이그레이션 테스트 DB 권한 문제:** `kartsell` DB 사용자가 `kartsell_migration_test` 데이터베이스의 소유자가 아니어서 `DbUpMigrationTests`(12건)가 로컬에서 실패합니다. 코드 문제가 아니라 DBA 조치(소유권 부여)가 필요합니다. 실행할 SQL 초안: `scripts/dba/grant-migration-test-db-ownership.sql`.
|
||||
3. **[해결됨 2026-08-08] frontend 빌드 산출물 재해시:** `dotnet build`를 실행할 때마다 `pnpm build`가 재실행되어 `wwwroot/assets/*` 해시 파일명이 바뀌고 git에 불필요한 변경이 쌓이는 구조적 문제가 있었습니다. **근본 원인:** `wwwroot/assets/*`, `wwwroot/index.html`은 100% Vite 생성 산출물(수작업 파일 없음)인데도 git에 커밋되어 있었고, 재빌드마다 콘텐츠 해시가 바뀌어 stale 파일이 삭제되지 않고 계속 누적됨(실제로 6개 커밋 파일 중 4개가 이미 orphan 상태였음이 확인됨). 조사 결과 `.gitea/workflows/deploy.yml`(실제 프로덕션 배포)은 이미 매 배포마다 `wwwroot`를 지우고 새로 빌드하므로 커밋된 산출물이 배포에 전혀 쓰이지 않았음 — 유일하게 의존하던 곳은 `.gitea/workflows/ci.yml`의 `publish`(Gitea Release zip 생성) 잡뿐이었음. **조치:** `wwwroot/assets/`, `wwwroot/index.html`을 `.gitignore`에 추가하고 `git rm --cached`로 추적 해제했으며, `ci.yml`의 `publish` 잡에 `deploy.yml`과 동일한 패턴(pnpm install → build → wwwroot 비우고 복사)을 추가해 release zip도 신선한 산출물을 갖도록 함. MSBuild의 `BuildFrontend` 타겟(로컬 `dotnet build` 시 항상 pnpm build 재실행)은 변경하지 않음 — 산출물이 더 이상 git 추적 대상이 아니므로 재실행 자체는 더 이상 문제가 아님. **검증:** `dotnet build KArtSell.sln -c Release`를 연속 2회 실행해 `git status`가 두 번 모두 동일(무관 변경 없음)함을 확인했고, 수정 전 코드로 되돌려 동일한 무변경 빌드를 1회 실행하면 `wwwroot/index.html`이 13줄 diff로 수정되고 신규 해시 파일 2개가 untracked로 생기는 것을 재현해 대조 확인함.
|
||||
- **별도 발견(미해결, 범위 밖):** `frontend/src/**/*.vue.js`, `frontend/src/features/*/api.js` 등 TS 소스 옆에 나란히 존재하는 `.js` 파일(약 130개)과 `frontend/tsconfig.tsbuildinfo`, `frontend/vite.config.js`도 전부 git에 커밋되어 있고, `pnpm build`(`vue-tsc -b`)를 실행할 때마다 매번 재생성되어 같은 종류의 불필요한 diff를 만듭니다. 근본 원인은 `frontend/tsconfig.json`에 `"noEmit": true`가 없어 `vue-tsc -b`(프로젝트 빌드 모드, `outDir` 미지정)가 소스 옆에 컴파일 결과를 그대로 방출하기 때문입니다(`pnpm typecheck`가 쓰는 `vue-tsc --noEmit`은 문제없음). 이번 PR 범위(`wwwroot/assets` 재해시)와는 별개의 구조적 문제라 이번에는 손대지 않았습니다. **참고:** 다른 동시 세션(AEG-X-002 커밋들)이 이 `.js` 방출 문제를 별도로 이미 다루기 시작한 것으로 보입니다 — 병합/정리 시 중복 작업 여부를 확인하세요.
|
||||
|
||||
---
|
||||
|
||||
### ⏳ 진행 중 (1개)
|
||||
## ✅ 완료 (Backend 구현 + 테스트, 2026-08-07 기준 검증됨)
|
||||
|
||||
#### Gate 3: 252+ Trading-Day Shadow Run (리허설)
|
||||
- **상태:** 🔴 검증 실패 (재시도 필요)
|
||||
- Run ID: `d14f34ea-2afe-4caf-bbb1-c9a7d74fb582` (생성됨, 미완료)
|
||||
- Hangfire Job 269: 상태 미확인 (Host 재시작 실패)
|
||||
- 근본 원인: Hangfire 분산 락 타임아웃 + 가짜 KRX API 키
|
||||
- **완료된 것:**
|
||||
- ✅ DB 격리: 테스트 appsettings.Development.json → `kartselldb_test`
|
||||
- ✅ Host 재시작: Development 환경 (DevelopmentHeaderAuthenticationHandler 활성화)
|
||||
- ✅ Hangfire 타임아웃 복원력: Program.cs 재시도 로직 추가 (DEBT-015)
|
||||
- ✅ 실KRX 데이터 서비스: KrxDataService 실연동 (Program.cs 등록)
|
||||
- ✅ 기술부채 등록: DEBT-009~015 (PBO/DSR/예측/false-exit/타임아웃/감시)
|
||||
- **현재 제약 사항 (문서화됨):**
|
||||
- PBO/Sharpe 계산: 간단한 percentile 공식 (정확한 CSCV 방법론 필요 — DEBT-009)
|
||||
- 모델 예측: 고정 수량 (실제 포지션 사이징 필요 — DEBT-010)
|
||||
- 비용 2배 시뮬레이션: 선형 공식 (정확한 재시뮬레이션 필요 — DEBT-011)
|
||||
- False-exit 분석: 미구현 (항상 0 반환 — DEBT-012)
|
||||
- **필요 조건:**
|
||||
```bash
|
||||
# Terminal 1: SSH 터널 (지속)
|
||||
ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7
|
||||
|
||||
# Terminal 2: Host 실행 (Development 환경)
|
||||
cd D:\JobRoomz\KArtSell.Aegis
|
||||
$env:ASPNETCORE_ENVIRONMENT = "Development"
|
||||
dotnet run --project src/KArtSell.Host -c Debug
|
||||
```
|
||||
- **실행 단계:**
|
||||
1. ✅ POST /api/shadow-runs (modelId, windowStart, windowEnd)
|
||||
2. ✅ 202 Accepted 반환 (Job 269 enqueue)
|
||||
3. ⏳ Hangfire Worker 처리 중 (Phase 1-5 실행)
|
||||
4. ⏳ Phase 5 완료 → model_operations.shadow_run 저장
|
||||
5. ⏳ GET /api/shadow-runs/{runId} → 200 OK (status: Completed)
|
||||
6. 목적: 데이터 계층 검증 + 실KRX 통합 확인
|
||||
- **기대 결과 (리허설용):**
|
||||
- 데이터 파이프라인 동작 확인
|
||||
- 실KRX 가격 데이터 정상 다운로드
|
||||
- model_operations.shadow_run 테이블 데이터 쓰기 성공
|
||||
- 단순화된 분석 메트릭 생성 (프로덕션 검증 아님)
|
||||
- **순서:** 다음 세션에서 실행
|
||||
### VS-26 (구 VS-03): 모델 승인 워크플로우 (Maker-Checker Governance) — 🟡 BLOCKED (DB 미검증)
|
||||
- **위치:** `Features/ApprovalWorkflow/` — 2026-08-08부로 유일한 구현 (중복 구현 삭제 완료, DEBT-017 참조)
|
||||
- **테스트:** 순수 `Policy` 단위 테스트 10/10 PASS(DB 불필요). Handler+Sql+실DB 통합 테스트 8건 신규 작성했으나 **이 세션에서 실 PostgreSQL에 연결하지 못해(127.0.0.1:5432 connection refused) 단 하나도 실행 검증되지 않음.** `dotnet build -c Release`는 0 경고/0 오류로 성공.
|
||||
- **미완료:** 프런트엔드 UI 없음. 실DB 대상 테스트 실행 전까지 COMPLETED로 전환 금지.
|
||||
- **이번 세션(2026-08-08)에서 발견/수정한 결함:** 살아있는 `Features/ApprovalWorkflow/Sql.cs`의 `InsertProposalAsync`에 죽은 `ApprovalSql`이 갖고 있던 것과 동일한 Dapper `DateOnly` 바인딩 버그가 있었음(수정 완료, DB로 미검증). 잔여 결함(수정하지 않고 README에만 기록): `GET /approvals/{id}` 엔드포인트 없음(증거 조회 불가), Draft→Proposed 전환이 어디에도 연결되어 있지 않음(승인 API가 실사용 시 끝까지 도달 불가능).
|
||||
|
||||
### VS-27 (구 VS-04): 불변 감사 추적 (Audit Trail / GDPR)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/Compliance/`
|
||||
- **테스트:** 5/5 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **이번 세션에서 발견/수정한 결함:** `ip_address`/`kis_response`류 컬럼의 Dapper 타입 캐스팅 실패, `GdprRetention.RetentionEndsAt`이 `DATE` 컬럼인데 `DateTime`으로 선언되어 있던 문제, 그리고 `KArtSell.BuildingBlocks`의 `[ModuleInitializer]`가 우연히 로드되지 않으면 모든 snake_case 컬럼이 null로 매핑되던 레이스 컨디션
|
||||
|
||||
### VS-10: 매도 결정 엔진 (Sell Decision Engine)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/SellDecision/`, `frontend/src/features/sell-decision/`
|
||||
- **테스트:** 32/32 PASS (격리 실행 기준)
|
||||
- **완료도:** Backend + Frontend 모두 존재 (VS-26/27/28/29 중 유일)
|
||||
- **⚠️ 미검증 사항:** 코드/테스트 완료 ≠ PBO/DSR 프로덕션 검증 완료. 실 시장 데이터 기반 검증은 Phase 1 Shadow Run 완료 후에만 가능
|
||||
|
||||
### VS-28 (구 VS-12): 거래 실행 시스템 (Trade Execution, KIS 연동)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/TradeExecution/`
|
||||
- **테스트:** 13/13 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **이번 세션에서 발견/수정한 결함 (심각):** `UpdateTradeStatusAsync`가 `status`/`kis_response`/`error_message`만 저장하고 `kis_order_id`, `executed_quantity`, `unit_price`, `commission`, `net_proceeds`, 체결/정산 타임스탬프는 병합 이후 매번 조용히 유실시키던 버그. 거래 체결·정산 데이터가 실제로는 저장되고 있지 않았음
|
||||
|
||||
### VS-29 (구 VS-14): 포트폴리오 대사 (Portfolio Reconciliation)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/`
|
||||
- **테스트:** 18/18 PASS (격리 실행 기준)
|
||||
- **미완료:** 프런트엔드 UI 없음
|
||||
- **참고:** 이 슬라이스가 포함된 PR(#28)이 병합 당일 `model_operations.models` 테이블 누락으로 신규 DB 마이그레이션을 전부 깨뜨리는 채로 병합되었고, 같은 날 별도 PR(#29)로 긴급 수정됨 — 병합 전 fresh-install 리허설이 실제로 이루어지지 않았음을 시사
|
||||
|
||||
### AEG-X-009: 외부 데이터 소스 통합 (KRX/OpenDart/KIS)
|
||||
- **위치:** `src/KArtSell.Modules.ModelOperations/Infrastructure/`, market_data 스키마
|
||||
- **완료:** 소스 카탈로그/거버넌스 정책(Workstream D/E/F) + 실 API 연동(Workstream G: KRX OpenAPI/OpenDart/KIS 서비스, 일일 스케줄링, 에러 분류, LKG 폴백)
|
||||
|
||||
### 그 외 완료 항목 (VS-00 플랫폼 부트스트랩, VS-01/VS-02 슬라이스 스펙, 보안/Outbox/OpenAPI 게이트 등)
|
||||
상세는 `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`의 AEG-X-001~008, AEG-VS-00-01~07, AEG-VS-01-01, AEG-VS-02-01 행 참조.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 완료됨 (Implemented & Tested)
|
||||
## 🔴 실제로 블로킹 중인 것 (Phase 1 Shadow Run)
|
||||
|
||||
### Phase 2: 중기 최적화
|
||||
### PHASE-1-SHADOW-RUN: 252+ 거래일 검증 (Gate 5a)
|
||||
- **상태:** `BLOCKED` — **실행 중이 아님**
|
||||
- **근거:** `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`에 이미 정정되어 있음 — 과거 세션들의 "Job 893/976 RUNNING, ~20+시간 경과" 등의 기록은 실제로는 `POST /api/shadow-runs`가 `PostgresException 23514`(check_status 제약조건 위반)로 500 에러를 반환하며 실패한 것이었고, Job이 실제로 시작된 적이 없음
|
||||
- **차단 사유:** 서버 측 `dataset_manifest`, `model_version_registry`, `evidence_snapshot`, `release_evidence_bundle`에 승인/동결된 행이 없어 RunId/JobId를 생성할 수 없음. 승인된 VersionSet 대기 중
|
||||
- **재개 절차:** `PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`의 5단계 참조 (① check_status 제약조건 정합 ② 승인된 테스트 DB에서 fresh/upgrade/재실행/실패복구 리허설 ③ 증거 보존 ④ 명시적 승인 획득 ⑤ 신규 Run ID/Job ID로 재큐잉)
|
||||
- **이 상태가 바뀌려면:** 실제 RunId/JobId가 존재해야 하며, 문서에 "RUNNING"이라고 다시 적으려면 그 근거를 반드시 명시해야 함 (과거의 허위 기록을 반복하지 말 것)
|
||||
|
||||
#### 5. ✅ OpenDart 일일 배치
|
||||
- **파일:** src/KArtSell.Host/Observability/OpenDartService.cs (186 lines)
|
||||
- **Job:** OpenDartDailyBatchJob.cs (169 lines)
|
||||
- **내용:**
|
||||
- 1,000 req/day 할당량 관리
|
||||
- 3개월 캐싱 (분기별 재무제표)
|
||||
- 일 1회 배치 호출만 허용
|
||||
- **테스트:** 5개 통합 테스트 (OpenDartServiceTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 6. ✅ Gate 4: 승인 워크플로우
|
||||
- **파일:** GetApprovalQueue/Endpoint.cs, ApproveModel/Handler.cs, RejectModel/Handler.cs
|
||||
- **내용:**
|
||||
1. GET /api/approval-queue (대기 중 목록)
|
||||
2. POST /api/approval/{id}/approve (2명 승인)
|
||||
3. approved_at / approved_by 타임스탬프 추적
|
||||
- **테스트:** 32개 통합 테스트
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 7. ✅ KIS Connection Pool
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/KisConnectionPool.cs (247 lines)
|
||||
- **내용:**
|
||||
- 3-5 concurrent connection pool
|
||||
- OAuth2 token refresh (55분 주기)
|
||||
- Priority queue (BUY > SELL > CANCEL)
|
||||
- **테스트:** 2개 통합 테스트 (KisConnectionPoolTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 3: 장기 고도화
|
||||
|
||||
#### 8. ✅ Central Rate Limiter (모든 API)
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/RateLimiterService.cs (211 lines)
|
||||
- **내용:**
|
||||
- Token bucket pattern (모든 API 통합)
|
||||
- Per-API quota 추적
|
||||
- Fairness 보장
|
||||
- **테스트:** 4개 통합 테스트 (RateLimiterServiceTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 9. ✅ Circuit Breaker Pattern
|
||||
- **파일:** src/KArtSell.Host/Infrastructure/CircuitBreakerPolicy.cs (180 lines)
|
||||
- **내용:**
|
||||
- Polly policy 기반 구현
|
||||
- 429 에러 3회 → 5분 차단
|
||||
- 자동 복구 (시간 후)
|
||||
- **테스트:** 7개 통합 테스트 (CircuitBreakerTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
#### 10. ✅ Gate 5: Observability Dashboard
|
||||
- **파일:** src/KArtSell.Host/Features/Observability/GetMetricsEndpoint.cs
|
||||
- **내용:**
|
||||
- Batch SLA: 작업 완료 시간
|
||||
- Data quality: 격리된 항목 수
|
||||
- Duplicate detection: 중복 경고 (DEBT-014)
|
||||
- Reconciliation: 상태 불일치 (DEBT-014)
|
||||
- Model drift: OOS 성능 추적
|
||||
- **테스트:** 6개 통합 테스트 (ObservabilityMetricsTests)
|
||||
- **상태:** ✅ COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Production Readiness Gates
|
||||
|
||||
| Gate | 항목 | 상태 | 기한 |
|
||||
|------|------|------|------|
|
||||
| **1** | DbUp 마이그레이션 (0000-0031) | ✅ PASS | - |
|
||||
| **2** | Outbox/Inbox Crash-recovery | ✅ PASS | - |
|
||||
| **3** | 252-day Shadow Run (실KRX) | ⏳ REHEARSAL IN PROGRESS | 오늘 |
|
||||
| **4** | 승인 워크플로우 | ✅ IMPL (대기) | 이번 주 |
|
||||
| **5** | 관찰성 대시보드 (메트릭) | ✅ IMPL (대기) | 다음 주 |
|
||||
|
||||
**Go-Live 기준:** 모든 Gate PASS + 증거 수집 완료 (≤ 2주)
|
||||
|
||||
---
|
||||
|
||||
## 📊 진행률
|
||||
|
||||
```
|
||||
Infrastructure: ██████████████████░ 85% (Phase 1 완료, Phase 2-3 진행 중)
|
||||
Testing: ██████████████████░ 100% (135/135 tests PASS - 5 arch + 95 integration + 35 unit)
|
||||
Documentation: ████████████░░░░░░░ 60% (로드맵, 계약, ADR, Gate 3 가이드)
|
||||
Validation Gates: ████████░░░░░░░░░░ 50% (Gate 1-2 PASS, Gate 3 IN PROGRESS, Gate 4-5 준비)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 다음 Iteration
|
||||
|
||||
### 이번 루프 (현재, ~60초)
|
||||
- [ ] Host 준비 확인
|
||||
- [ ] Agent 1 (Gate 3) 시작 또는 계속 대기
|
||||
- [ ] Loop 30초마다 상태 모니터링
|
||||
|
||||
### Host 준비 후 (오늘, ~30분)
|
||||
- [ ] Gate 3 Shadow Run 실행
|
||||
- [ ] 252일 검증 + 메트릭 계산
|
||||
- [ ] GATE_3_EVIDENCE.md 생성
|
||||
- [ ] PASS/FAIL 판정
|
||||
|
||||
### 다음 주
|
||||
- [ ] Gate 4: 승인 워크플로우 실행
|
||||
- [ ] Phase 2: OpenDart + KIS 최적화
|
||||
- [ ] 증거 수집 완료
|
||||
|
||||
### 2주 후
|
||||
- [ ] Gate 5: 관찰성 대시보드 활성화
|
||||
- [ ] Production readiness 최종 확인
|
||||
- [ ] Go-Live 준비
|
||||
|
||||
---
|
||||
|
||||
## 📝 Codex 연계 방법
|
||||
|
||||
### 다른 환경에서 계속하기
|
||||
|
||||
1. **현재 커밋 확인**
|
||||
```bash
|
||||
git log --oneline -10
|
||||
# 최신: eb106d5 (Phase 1 API optimization)
|
||||
# 이전: 9a2d939 (idempotency fix)
|
||||
# 이전: 4519fa8 (recommendation reports)
|
||||
```
|
||||
|
||||
2. **빌드 & 테스트**
|
||||
```bash
|
||||
dotnet build KArtSell.sln -c Release
|
||||
dotnet test KArtSell.sln -c Release
|
||||
```
|
||||
|
||||
3. **Host 시작 (Gate 3 진행)**
|
||||
```bash
|
||||
ssh -L 5432:127.0.0.1:5432 kjh2064@178.104.200.7 # Terminal 1
|
||||
dotnet run --project src/KArtSell.Host -c Release # Terminal 2
|
||||
```
|
||||
|
||||
4. **Shadow Run 요청**
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:5002/api/shadow-runs \
|
||||
-H "X-KArtSell-User: gate3-rehearsal" \
|
||||
-H "X-KArtSell-Role: Researcher" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"modelId": "00000000-0000-0000-0000-000000000001",
|
||||
"windowStart": "2024-01-02",
|
||||
"windowEnd": "2024-10-01"
|
||||
}'
|
||||
|
||||
# 폴링 (Analyst 역할 필요)
|
||||
curl http://127.0.0.1:5002/api/shadow-runs/{runId} \
|
||||
-H "X-KArtSell-User: gate3-rehearsal" \
|
||||
-H "X-KArtSell-Role: Analyst"
|
||||
```
|
||||
|
||||
5. **다음 단계로 점프**
|
||||
- Phase 2 구현 시작 (OpenDart, KIS)
|
||||
- 로드맵 업데이트
|
||||
이 게이트는 **달력 시간이 필요한 작업**입니다 (252+ 거래일 시뮬레이션은 컴퓨팅으로 앞당길 수 없음). "최적 전략적으로 빨리 끝내기"의 대상이 될 수 없고, 남은 유일한 실행 가능 조치는 위 재개 절차를 밟아 실제로 큐잉하는 것뿐입니다.
|
||||
|
||||
---
|
||||
|
||||
## 📚 관련 문서
|
||||
|
||||
- **WBS 트래커 (항목별 상세 상태):** `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`
|
||||
- **WBS 원 계획 (번호 충돌 있음, 주의):** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`
|
||||
- **Phase 1 상태 정정 기록:** `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`
|
||||
- **Architecture:** `docs/03_ARCHITECTURE_BE_FE.md`
|
||||
- **API Rate Limits:** `docs/API_RATE_LIMIT_STRATEGY.md`
|
||||
- **Gates:** `PRODUCTION_READINESS.md`
|
||||
- **Code Guidelines:** `CLAUDE.md`
|
||||
- **Tech Debt:** `TECH_DEBT_REGISTER.md`
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Loop 상태
|
||||
## 📝 이 문서를 다시 갱신할 때
|
||||
|
||||
**현재:** `/loop` 30초마다 모니터링 (Host 준비 대기)
|
||||
**다음:** Host 준비 → Gate 3 자동 시작
|
||||
**예상:** 오늘 이내 결과
|
||||
|
||||
---
|
||||
|
||||
**최종 목표:** Production readiness (모든 Gate PASS) ✅
|
||||
**기한:** 2주 이내 (2026-08-16)
|
||||
**Status:** ON TRACK 🚀
|
||||
1. **git log를 먼저 확인하세요.** 이 문서와 `main`이 얼마나 벌어졌는지 (`git log --oneline <이-문서-마지막-커밋>..main`) 확인하지 않고 문서만 읽고 "현재 상태"를 판단하지 마세요.
|
||||
2. **테스트는 격리 실행으로 확인하세요.** 전체 스위트 실행에서 통과했다고 해서 개별 기능이 안정적으로 통과하는 것은 아닙니다 (이번 세션에서 `AuditSql`이 정확히 이 이유로 놓칠 뻔했습니다 — `--filter`로 단일 클래스만 돌려서 재확인하세요).
|
||||
3. **"완료"라고 쓰기 전에 실제 파일 경로와 테스트 결과를 직접 확인하세요.** 이 저장소에는 검증 없이 "COMPLETE"/"100%"라고 선언한 문서가 매우 많습니다 (`EXECUTION_COMPLETE_FINAL.md`, `WORK_COMPLETION_CERTIFICATE.md` 등). 그 패턴을 반복하지 마세요.
|
||||
4. **Phase 1 Shadow Run은 달력 시간 게이트입니다.** 실제로 큐잉되어 진행 중이라는 구체적 증거(RunId/JobId) 없이 "진행 중"이라고 쓰지 마세요.
|
||||
|
||||
+17
-4
@@ -49,14 +49,27 @@
|
||||
| DEBT-007 | Newtonsoft.Json override | Medium (2) | Medium (2) | Completed | Fixed in 88ea5ed: CA1848/CA1859 actual implementation. LoggerMessage + HashSet/Dictionary. | @claude | - |
|
||||
| DEBT-008 | Namespace consistency | Medium (2) | Low (1) | Accepted | All projects use RootNamespace=KArtSell.Aegis; AssemblyName retained per-project for DLL clarity. Trade-off accepted: DLL clarity > namespace alignment. No action. | @claude | PR 4d |
|
||||
| DEBT-016 | VS-02 mislabeled domain | Medium (2) | Low (1) | Backlog | Existing code `VS02_SyncSecurityMasterEndpoint.cs`, `VS02_SecurityMasterJobs.cs`, `VS02_SecurityMasterPolicy.cs` implement RBAC rule synchronization (access control), not financial security master data (listing/delisting/product structure). Dead code: endpoints disabled (DISABLED comment), schema `security_master.rules` table never migrated, never deployed. Correct domain documented in `docs/CURRENT/SLICE_SPECS/VS-02-SLICE_SPEC.md` (financial PIT). Removal decision deferred pending architect review (PR recommended). | @claude | docs/CURRENT/SLICE_SPECS/VS-02-SLICE_SPEC.md |
|
||||
| DEBT-017 | Duplicate VS-03 Approval Workflow implementation | High (3) | Medium (2) | Backlog | Two independent, functionally-identical VS-03 maker-checker slices exist: `ApprovalWorkflow/` (Workstream H, own `ApprovalProposal`/`IClock`/`IOutbox` types) and `Features/ApprovalWorkflow/` (Workstream G, matches documented `Features/<Slice>/` convention). Both mapped the same routes (`/approvals`, `/approvals/{id}`, `/approvals/{id}/approve`), which crashed Host startup with a duplicate-route/missing-DI error the first time the app was actually booted (2026-08-07 — apparently never booted successfully before). Old set annotated `[DontRegister]` (FastEndpoints) 2026-08-07 to unblock boot; code and its test file (`ApprovalWorkflowTests.cs`) kept for now. Needs an architect decision: delete the old slice entirely (and its test) or intentionally keep both for a reason not yet documented. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-018 | Outbox write not co-transactional with entity write | Medium (2) | Medium (2) | Backlog | `TradeExecution/TradeHandlers.cs` (`TradeOutboxPublisher`) and `PortfolioReconciliation/ReconcileTradeHandler.cs` open a second, separate connection/transaction to write the outbox message after the trade/holding write already committed on its own connection. A crash between the two leaves the entity updated but no outbox event emitted (silent, non-atomic). Proper fix: thread a shared `NpgsqlTransaction` through `TradeSql`/`ReconciliationSql` mutation methods so entity insert + outbox insert commit together, matching `DapperModelOperationRequestRepository`'s pattern. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-017 | Duplicate VS-26 (formerly VS-03) Approval Workflow implementation | High (3) | Medium (2) | Completed (DB verification pending) | **Decision (2026-08-08):** `Features/ApprovalWorkflow/` (Workstream G) kept as canonical — it is the implementation actually wired into `Program.cs`/`FastEndpoints`. `src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/` (Workstream H, `[DontRegister]`'d dead code) and its dedicated test file (`tests/KArtSell.Integration.Tests/ApprovalWorkflow/ApprovalWorkflowTests.cs`, the old 20/20-passing suite that exercised only the dead code) were **deleted**. `ApprovalWorkflowPolicyTests.cs` already tested the kept implementation's pure `Policy` class and was extended (5→10 cases) rather than replaced. New Handler+Sql+real-Postgres integration tests were written at the same path the old dead-code tests occupied (`tests/KArtSell.Integration.Tests/ApprovalWorkflow/ApprovalWorkflowTests.cs`), covering create (Maker-role-gated), approve (Maker≠Checker separation of duties, Checker-role-gated, evidence attachment), activate (SRE-role-gated), list filtering, and an explicit `DateOnly EffectiveAt` round-trip. **Bug found and fixed while porting:** the kept implementation's `Sql.cs InsertProposalAsync` had the *exact same* Dapper-cannot-bind-`DateOnly` bug that was found and fixed in the deleted implementation's `ApprovalSql.cs` (commit `2ccf74c`) — i.e. the "tested" dead code had already been fixed for this, but the "live" code had not; it would have failed 100% of proposal-creation calls against a real database. Fixed identically (`::date` cast + `"yyyy-MM-dd"` string parameter). **Not fixed (out of scope, flagged as residual gaps in the slice's README):** no `GET /approvals/{id}` endpoint (evidence becomes unreachable via HTTP after approval), and no wired Draft→Proposed transition anywhere in the running app (`ApprovalWorkflowPolicy.CanProposeForReview` exists but no Handler/Endpoint calls it), and `approval_proposals` rows are mutated in place via `UPDATE` rather than appended as new PIT revisions (the table's schema only has `id` as `PRIMARY KEY`, so the deleted implementation's append-only INSERT approach would itself have violated that constraint on the second write — this is pre-existing, schema-level, and not a regression from this cleanup). **Verification status: `dotnet build -c Release` is clean (0 errors/warnings). `dotnet test --filter "FullyQualifiedName~ApprovalWorkflow" -c Release` was run 2026-08-08: 10/10 pure-`Policy` tests passed; all 8 new DB-backed integration tests failed with `Npgsql.NpgsqlException: Failed to connect to 127.0.0.1:5432` (connection refused) because no PostgreSQL was reachable in that session (no SSH tunnel to 178.104.200.7 open). None of the 8 have been confirmed to pass against a real database.** Do not mark this row fully verified until that run happens; see `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` row `AEG-VS-26-01`, kept `BLOCKED` for the same reason. | @claude | commit a2e742c (original dup.), this session's commit (resolution), `docs/DECISIONS/ADR-WBS-001-slice-renumbering.md` |
|
||||
| DEBT-018 | Outbox write not co-transactional with entity write | Medium (2) | Medium (2) | Completed (DB verification pending) | Fixed 2026-08-08, matching `DapperModelOperationRequestRepository`'s pattern. **TradeExecution:** added a `DbConnection`/`DbTransaction`-taking overload of `ITradeSql.UpdateTradeStatusAsync`; `TradeOutboxPublisher.PublishAsync` replaced with `UpdateAndPublishAsync`, which opens one connection/transaction, updates trade status and writes the outbox message on it, then commits once — used by all 3 call sites that publish an event (`SubmitTradeHandler`, the `FullyFilled` branch of `PollTradeStatusHandler`, `ConfirmSettlementHandler`); paths with no outbox event still use the plain non-transactional update. **PortfolioReconciliation:** `ReconcileTradeHandler` now injects the request-scoped `IDbConnection` (the same instance `ReconciliationSql` already uses within one HTTP request, replacing its own separate `IDbConnectionFactory`-opened connection) and begins one `IDbTransaction` shared by `ReconciliationEngine.ReconcileTradeAsync(..., transaction)` (which threads it into new `IDbTransaction`-aware overloads of `GetHoldingAsync`/`UpsertHoldingAsync`/`InsertReconciliationLogAsync` — the read needed a transaction-aware overload too, since Npgsql throws if a command on a connection with a pending transaction doesn't have it attached) and the outbox `TradeReconciled`/`ReconciliationMismatchAlert` writes; the handler commits once at the end (or rolls back on `!result.Success`). `dotnet build KArtSell.sln -c Release`: 0 warnings/0 errors. `dotnet test --filter "FullyQualifiedName~TradeExecution\|FullyQualifiedName~PortfolioReconciliation" -c Release`: 17 pure-logic tests passed, 13 DB-backed tests failed with the same pre-existing 127.0.0.1:5432 connection-refused error (no SSH tunnel in this session) — none of the transactional changes have been confirmed against a live database yet. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening, discovery), Session 2026-08-08 (fix) |
|
||||
| DEBT-019 | Multiple duplicate cross-cutting abstractions (`IClock`, `IOutboxWriter`, `IKrxDataService`) | Medium (2) | Low (1) | Completed (partial) | Found and collapsed 3 separate cases where a slice reinvented an abstraction that already existed in `KArtSell.BuildingBlocks`: a second `IKrxDataService` (deleted, `ShadowRun.Services`), a second `IOutboxWriter`/`WriteAsync<T>` in `ReconcileTradeHandler.cs` (removed, switched to `BuildingBlocks.Reliability.IOutboxWriter`), and a second `IClock`/`SystemClock` in `ApprovalWorkflow/ApprovalPolicy.cs` (removed, switched to `BuildingBlocks.Time.IClock`). Root cause: successive sessions implementing a slice without searching `BuildingBlocks` first. Recommend a pre-implementation checklist step ("does this abstraction already exist in BuildingBlocks?") for future slices. | @claude | Session 2026-08-07 (Phase 3 J/K/L hardening) |
|
||||
| DEBT-020 | `model_operations.models` and `compliance` schema never created by any migration | High (3) | Low (1) | Completed | `0036`/`0038` reference `model_operations.models(id)` via FK and `OpenDartDailyBatchJob.cs` queries it directly, but no migration ever ran `CREATE TABLE model_operations.models`; `0037` wrote to `compliance.*` tables without `CREATE SCHEMA compliance`. Any fresh database — including the actual deploy target (178.104.200.7), confirmed via a live failed SCP/DbMigrator deploy on 2026-08-07 — failed at migration `0036`/`0037`. Fixed via new `0035_model_operations_models.sql` (minimal: id/ticker/published_at/correlation_id/revision only — full Model Card schema is separate future work) and `CREATE SCHEMA IF NOT EXISTS compliance;` added to `0037`. Full chain 0000→0040 now verified fresh-install + idempotent re-run clean. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-021 | Dapper never configured for snake_case↔PascalCase column mapping | High (3) | Low (1) | Completed | `Dapper.DefaultTypeMap.MatchNamesWithUnderscores` was never set anywhere in the codebase, so every `QueryAsync<T>`/`QuerySingleOrDefaultAsync<T>` result-mapping onto a snake_case DB column (e.g. `event_type` → `EventType`) silently returned null/default for that property instead of throwing — masking the bug in every Sql class across every module. Confirmed via `ApprovalWorkflowTests.InsertAndRetrieveProposal_RoundTrips` and `AuditTrailTests.InsertAuditEvent_CreatesImmutableRecord` both getting real rows back with null fields. Fixed centrally via a `[ModuleInitializer]` in `KArtSell.BuildingBlocks/Data/DapperBootstrap.cs` (runs once per process regardless of entry point — Host/DbMigrator/tests). | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-022 | jsonb/inet columns written as plain text without an explicit cast | Medium (2) | Low (1) | Completed (partial) | Dapper does not know to cast a `string` parameter to `jsonb`/`inet` for Npgsql; `AuditSql.InsertAuditEventAsync` (`details`, `ip_address`), `AuditSql.RedactAuditEventDetailsAsync` (duplicate `SET details =` assignment, separately fixed), `TradeSql.InsertTradeAsync`/`UpdateTradeStatusAsync` (`kis_response`), and `SellDecisionSql.InsertDecisionAsync` (`oos_performance`) all failed with `42804: column "x" is of type jsonb but expression is of type text` the first time they were run against a real schema. Fixed with explicit `::jsonb`/`::inet` casts at each call site (mechanical, no behavior change). `AuditSql`'s jsonb read-back (`Dictionary<string,object>` from a jsonb column) also needed a raw-DTO + `JsonSerializer.Deserialize` mapping since Dapper has no built-in jsonb→Dictionary conversion either. **Not yet checked**: `PortfolioReconciliation`/`ApprovalWorkflow` Sql classes for the same pattern beyond what surfaced in this session's test runs — a full audit of jsonb/inet columns across all Sql classes is still open. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-023 | `ApprovalSql.InsertProposalAsync` fails on `DateOnly` parameter | Medium (2) | Low (1) | Backlog | `ApprovalWorkflowTests.InsertAndRetrieveProposal_RoundTrips` fails with `System.NotSupportedException: The member effectiveAt of type System.DateOnly cannot be used as a parameter value` — Dapper's `LookupDbType` doesn't recognize `DateOnly` without an explicit type map (`SqlMapper.AddTypeMap`/custom `TypeHandler`). Likely affects every other `DateOnly`-typed Dapper parameter in the codebase, not just this one; needs a similar centralized fix to DEBT-021 rather than a per-call-site patch. Discovered but not fixed in this session (scope cut to unblock the live deploy). | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-022 | jsonb/inet columns written as plain text without an explicit cast | Medium (2) | Low (1) | Completed | Dapper does not know to cast a `string` parameter to `jsonb`/`inet` for Npgsql; `AuditSql.InsertAuditEventAsync` (`details`, `ip_address`), `AuditSql.RedactAuditEventDetailsAsync` (duplicate `SET details =` assignment, separately fixed), `TradeSql.InsertTradeAsync`/`UpdateTradeStatusAsync` (`kis_response`), and `SellDecisionSql.InsertDecisionAsync` (`oos_performance`) all failed with `42804: column "x" is of type jsonb but expression is of type text` the first time they were run against a real schema. Fixed with explicit `::jsonb`/`::inet` casts at each call site (mechanical, no behavior change). `AuditSql`'s jsonb read-back (`Dictionary<string,object>` from a jsonb column) also needed a raw-DTO + `JsonSerializer.Deserialize` mapping since Dapper has no built-in jsonb→Dictionary conversion either. **2026-08-09: full audit completed** (repo-wide, not just Portfolio/Approval). Enumerated every `jsonb`/`inet` column across `db/migrations/*.sql` (case-insensitive — several use `JSONB`/`INET` uppercase, which an earlier lowercase-only grep would have missed), then checked each one for a C# writer. Findings: `PortfolioReconciliation`'s tables (`portfolio_management.holdings`/`reconciliation_logs`) have no `jsonb`/`inet` columns at all — nothing to fix. `ApprovalWorkflow`'s one `jsonb` column (`approval_events.details`) was already cast correctly in `InsertEventAsync`. Several other `jsonb` columns (`evidence_snapshot.payload`, execution-assurance/model-feedback tables under `evaluation`/`governance`) have no C# writer yet at all — those slices (VS-05/09/19 etc.) are unimplemented, so there's no bug surface yet; flag for re-check whenever they get built. **One new, real instance of this exact bug found and fixed**: `OpenDartService.CacheResultAsync` (`src/KArtSell.Host/Observability/OpenDartService.cs`) inserted a serialized JSON string into `opendata.opendart_cache.data_json JSONB` without a cast — same `42804` failure mode as the others, just never previously exercised/caught. Fixed with `@dataJson::jsonb`. `dotnet build -c Release` clean; not run against a live database this session (see the rest of this session's entries for why). | @claude | Session 2026-08-07 (deploy failure triage, discovery), Session 2026-08-09 (full audit + OpenDartService fix) |
|
||||
| DEBT-023 | `ApprovalSql.InsertProposalAsync` fails on `DateOnly` parameter | Medium (2) | Low (1) | Completed | Stale entry, corrected 2026-08-08: this described `ApprovalSql.cs` under `src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/` — that per-call-site fix (`::date` cast + `"yyyy-MM-dd"` string parameter, not a centralized type handler) landed in commit `2ccf74c` but this row was never updated to reflect it. That whole file was then deleted as dead code while resolving DEBT-017 (2026-08-08); its surviving sibling, `Features/ApprovalWorkflow/Sql.cs`, was found to have the *same* unfixed bug independently and received the identical fix in that session — see DEBT-017. No centralized `DateOnly` type handler was added; this remains a per-call-site fix pattern, so any *other* `DateOnly`-typed Dapper INSERT elsewhere in the codebase should still be checked individually rather than assumed safe. | @claude | commit 2ccf74c; DEBT-017 (this session) |
|
||||
| DEBT-024 | New integration tests don't insert FK parent rows / one pure-logic test flakes under full-suite run | Low (1) | Low (1) | Backlog | `TradeExecutionTests` constructs `Trade` with a random `sellDecisionId` that was never inserted into `sell_decisions`, so every insert now correctly fails its FK constraint (`trades_sell_decision_id_fkey`) once the schema was actually complete (see DEBT-020) — test-only gap, not a production code defect; needs the tests updated to insert a parent `models`+`sell_decisions` row first. Separately, `SellPriorityRankerTests.CalculateScore_HardImpairment_ReturnsLowestScore` (pure logic, no DB) passed in isolation but returned 1000 instead of the expected 950 (age-boost not applied) when run as part of the full suite — not yet root-caused; may be test-order/parallelization state leakage rather than a `SellPriorityRanker` bug. Also, `DbUpMigrationTests.*` (pre-existing, unrelated to this session) fail locally with `42501: must be owner of database kartsell_migration_test` — a local Postgres role permission gap, not a code issue. | @claude | Session 2026-08-07 (deploy failure triage) |
|
||||
| DEBT-025 | `Features/ApprovalWorkflow` has no `GET /approvals/{id}` endpoint | Medium (2) | Low (1) | Completed (DB verification pending) | Added `GetApprovalByIdEndpoint` (`GET /approvals/{id}`) + `ApprovalDetailResponse` (includes `Evidence`), and `ApprovalWorkflowSql.GetEvidenceForProposalAsync`. Evidence attached during approval (PBO/DSR/OOS artifact links) is now readable via HTTP. Two new tests added (`GetEvidenceForProposalAsync_ReturnsEvidenceAttachedDuringApproval` + the endpoint itself). `dotnet build -c Release` clean (0/0). **Not verified against a live database** — same 127.0.0.1:5432 connection-refused blocker as DEBT-017/026; do not mark fully verified until a real Postgres run passes. | @claude | DEBT-017 (2026-08-08), `src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/README.md` |
|
||||
| DEBT-026 | `Features/ApprovalWorkflow` has no wired Draft→Proposed transition | High (3) | Low (1) | Completed (DB verification pending) | Added `ProposeForReviewHandler` + `POST /approvals/{id}/propose`, wired into `Program.cs` DI. Calls the pre-existing `ApprovalWorkflowPolicy.CanProposeForReview` (creator-only) and `ValidateProposalState` (Draft→Proposed), then updates status and emits a `PROPOSED` event — same pattern as `ApproveApprovalHandler`/`ActivateModelHandler`. A proposal created via `POST /approvals` can now reach `Approved`/`Active` through the HTTP API end-to-end. Two new tests added (`ProposeForReview_ByCreatingMaker_TransitionsDraftToProposed`, `ProposeForReview_ByDifferentUserThanCreator_ThrowsUnauthorized`). `dotnet build -c Release` clean (0/0). **Not verified against a live database** — same 127.0.0.1:5432 connection-refused blocker as DEBT-017/025; `dotnet test --filter FullyQualifiedName~ApprovalWorkflowTests -c Release` run 2026-08-08, all 17 matched tests fail with connection-refused (includes this file's tests plus an unrelated top-level `ApprovalWorkflowTests.cs` the substring filter also matches). Do not mark fully verified until a real Postgres run passes. | @claude | DEBT-017 (2026-08-08), `src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/README.md` |
|
||||
| DEBT-027 | `PollTradeStatusHandler`/`ConfirmSettlementHandler` registered in DI but never invoked by anything | High (3) | Low (1) | Completed (DB verification pending) | Discovered while looking for BE/scheduler priority work (2026-08-09) — same class of gap as DEBT-026 (a fully-implemented handler with no caller). `TradeEndpoints.cs` only has `POST /trades` (→`SubmitTradeHandler`) and `GET /trades`; nothing ever called `PollTradeStatusHandler` or `ConfirmSettlementHandler`, and no Hangfire job did either, so a trade could reach `Submitted` and never progress — KIS fills and settlement confirmations were never picked up. Added `src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs`: a Hangfire recurring job (`trade-status-polling`, every 2 minutes, `q-customer-sla` queue per CLAUDE.md's queue-isolation guidance since this affects real trade completion, not research) that queries `Submitted`/`Accepted`/`PartiallyFilled` trades and calls `PollTradeStatusHandler`, then queries `FullyFilled` trades and calls `ConfirmSettlementHandler`. Registered in `Program.cs` alongside the other recurring jobs. `dotnet build -c Release` clean (0/0). **No dedicated test added** (the job is thin orchestration over the already-implemented, already-covered-elsewhere handlers, and writing a fake `IKisTradeExecutionService`/`ITradeSql` test double would be a new testing pattern not used anywhere else in this codebase — flagged rather than done rashly) **and not run against a live database or KIS** — same connection blocker as the rest of this session's work. | @claude | Session 2026-08-09 (BE/scheduler priority pass) |
|
||||
| DEBT-028 | `ActivateModelHandler` had no HTTP endpoint, and would have corrupted approval data if wired naively | High (3) | Low (1) | Completed (DB verification pending) | Found via a systematic sweep of every `*Handler` registered in `Program.cs`'s DI container, checking whether each is actually referenced by an `Endpoint.cs` or a job (the same method that found DEBT-026/027) — `ActivateModelHandler` was the only remaining orphan in `Features/ApprovalWorkflow/`: no `POST /approvals/{id}/activate` existed, so an `Approved` proposal could never reach `Active`, the step this whole slice exists for. While wiring it up, found the handler's original call — `_sql.UpdateProposalStatusAsync(proposalId, ApprovalStatus.Active, userEmail, "Model activated by SRE", ct)` — would have passed the *activating SRE's* email/note through the `approvedBy`/`approvalNotes` parameters, overwriting the checker's real `approved_by`/`approval_notes` on activation, and never touched the schema's `activated_by`/`activated_at` columns at all (they existed since migration `0036` but nothing ever wrote them). Added a dedicated `ApprovalWorkflowSql.ActivateProposalAsync(proposalId, activatedBy, ct)` that only sets `status='ACTIVE'`, `activated_by`, `activated_at`, leaving `approved_by`/`approval_notes` untouched, and switched `ActivateModelHandler` to call it. Added `ActivateApprovalEndpoint` (`POST /approvals/{id}/activate`). Strengthened the existing `Activate_BySreAfterApproval_TransitionsToActive` test to assert `activated_by`/`activated_at` are set and the checker's `approved_by`/`approval_notes` survive activation unchanged — this would have caught the bug. `dotnet build -c Release` clean (0/0). Not run against a live database this session. | @claude | Session 2026-08-09 (BE/scheduler priority pass) |
|
||||
| DEBT-029 | `LogAuditEventCommandHandler` (VS-27 audit trail) is never called by any other slice | High (3) | Medium (2) | Backlog | Found during the same orphaned-handler sweep that found DEBT-027/028 — but unlike those, this one is NOT a missing single endpoint; it's a missing *cross-cutting integration*. `LogAuditEventCommandHandler` (`src/KArtSell.Modules.ModelOperations/Compliance/LogAuditEventHandler.cs`) is the intended call point for every other slice to record an auditable action (per `compliance.audit_event_types`'s seed data: `APPROVAL_PROPOSED`, `APPROVAL_APPROVED`, `MODEL_ACTIVATED`, `SELL_DECISION_MADE`, `SELL_EXECUTED`, etc.) — but nothing in `ApprovalWorkflow/Handlers.cs`, `TradeExecution/TradeHandlers.cs`, `SellDecision/*`, or `PortfolioReconciliation/ReconcileTradeHandler.cs` actually calls it. VS-27 ("Immutable Audit Trail") is marked `COMPLETED` in the WBS tracker with 5/5 tests passing, but those tests only exercise `AuditSql` directly — they don't prove the rest of the system ever produces an audit trail in practice. Net effect: the compliance/GDPR audit trail this system's governance model depends on (CLAUDE.md's "Evidence & Audit: Update/delete are blocked; new state appended as new revision") is currently empty in production regardless of how many approvals/trades/sell-decisions happen, because nothing populates it outside of direct `AuditSql` test calls. **Not fixed this session** — wiring it in touches 4+ handler classes across 3+ slices (a genuine cross-cutting integration, not a single bounded fix like DEBT-026/027/028), and each call site needs to decide what `EventType`/`Details`/`EvidenceLinks` are correct for that action rather than a mechanical change. Recommend one slice at a time, starting with `ApprovalWorkflow` (highest governance stakes) via its outbox events (`ApprovalWorkflowPolicy.CreateStateChangeEvent` already emits an event per transition — a downstream consumer job could call `LogAuditEventCommandHandler` from there instead of wiring it into every handler directly, matching this repo's existing Outbox→Inbox→consumer pattern). | @claude | Session 2026-08-09 (BE/scheduler priority pass, discovery only) |
|
||||
|
||||
### Frontend Shell / Home (KBX Design Philosophy Adoption, V13-FE-007+)
|
||||
|
||||
| ID | Category | Impact | Effort | Status | Notes | Owner | ADR |
|
||||
|----|----------|--------|--------|--------|-------|-------|-----|
|
||||
| DEBT-030 | `HomePage.vue` "확인 필요" section has no real signal source | Medium (2) | Medium (2) | Backlog | `frontend/src/features/home/pages/HomePage.vue`'s Attention section (KBX Business UX-AX Standard §2.4 "Exception Driven") currently always renders the empty state — there is no cross-feature aggregation endpoint yet for failed batch jobs, pending maker-checker approvals, or reconciliation breaks. Only `model-operations` and `sell-decision` features have `queries.ts`; other features (data-quality, marketData, portfolio) have no query hooks to source counts from. Wire real counts feature-by-feature once each has a stable query hook, rather than fabricating a placeholder aggregation API now. | @claude | V13-FE-007 (KBX shell/home adoption) |
|
||||
| DEBT-031 | Workspace tab dirty-guard has no feature screen wired to report dirty state | Low (1) | Medium (2) | Backlog | `frontend/src/shared/shell/workspaceStore.ts`'s `setDirty(screenId, path, dirty)` action and `KsWorkspaceTabs.vue`'s close-confirmation dialog (Business UX-AX Standard §58~59) are implemented and functional, but no feature page currently calls `setDirty`. `StandardScreenBoundary.vue` already receives a `state==='DIRTY'` prop per screen, but nothing bridges that per-screen signal up into the shared workspace store yet. Until a screen calls `setDirty`, tab close always takes the non-dirty path (closes immediately, no confirm). Wire via a small composable (e.g. `useWorkspaceDirtyBridge(screenId, path)`) called from screens that pass `state: 'DIRTY'`, one feature at a time — do not force every screen to adopt it in one sweep. Also note: the confirm dialog only offers "계속 편집"/"변경 버리기" (no generic "저장 후 이동", since there is no cross-screen save-orchestration hook to call). | @claude | V13-FE-010 (KBX workspace tabs adoption) |
|
||||
| DEBT-032 | `frontend/src/**` has git-tracked stale `.js`/`.vue.js` twins next to every `.ts`/`.vue` source, and they can silently shadow the source under default Vite/Vitest module resolution | High (3) | High (3) | Backlog | Discovered while adding two entries to `screen-types/catalogue.ts` (V13-FE-009): `vitest.config.ts` had no `resolve.extensions` override, so Vitest fell back to Vite's default order (`.js` before `.ts`), causing `catalogue.spec.ts`'s extensionless `import '../catalogue'` to silently resolve to a stale, git-tracked `catalogue.js` twin instead of the edited `catalogue.ts` — the new T11/T12 entries were invisible to the test. `vite.config.ts` already declares `extensions: ['.ts', '.tsx', '.vue', '.js', ...]` (so the dev server was never at risk), but `vitest.config.ts` did not match it. Fixed the immediate blocker: added the same `resolve.extensions` order to `vitest.config.ts`, and deleted the three stale twins directly implicated (`screen-types/catalogue.js`, `screen-types/tests/catalogue.spec.js`, `app/router.js` — confirmed unreferenced by any `.gitea/workflows/*.yml` and not emitted by any `package.json` script). **Not fixed**: this is a repo-wide pattern (confirmed present across most of `frontend/src`, deliberately git-committed across multiple past sessions per `git log`, e.g. commit `cada8fe`) — dozens/hundreds of other stale `.js`/`.vue.js` files likely still exist alongside their `.ts`/`.vue` sources and were not swept in this session (out of scope for the KBX design-philosophy adoption this debt was found during). Needs a dedicated session to (a) determine why these were being dual-maintained in the first place — no `package.json` script emits them, so likely a leftover from an earlier tsc/build config or manual habit — and (b) either delete them all (now safe, since `vitest.config.ts`/`vite.config.ts` both prefer `.ts`) or explain why they must stay. | @claude | V13-FE-009 (KBX Fast Entry/Work Queue template adoption, discovery) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# AEG-V15-033 — Schedule anchor calculation
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** AEG-V15-033
|
||||
- **Slice:** VS-18 BacktestingEvaluation / Scheduler
|
||||
- **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-01` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-01`
|
||||
- **Gate / dependency:** `G3` / `AEG-X-001` (completed in the progress tracker)
|
||||
- **Artifact:** `src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs`
|
||||
- **Acceptance evidence:** delayed dispatch must not cause cadence drift.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`; `docs/CURRENT/CATALOGS/JOB_CATALOGUE.csv`; `docs/CURRENT/CATALOGS/DECISION_LOG.csv` (`DEC-047`, `DEC-071`, `DEC-072`, `DEC-075`); `contracts/schedules/model-operations.v3.json`; the planner, dispatcher, and unit tests named above.
|
||||
- **Assumption:** the existing planner is the intended implementation of `DEC-071`: it advances from `scheduledFor`, never from worker dispatch time. The scheduler stays disabled by default.
|
||||
- **Unknown:** an approved production market-calendar/timezone source is still absent (`DEC-079`); this slice uses UTC instants only and does not resolve market sessions.
|
||||
- **Decision Required:** `DEC-083` (whether scheduler enqueue/mark becomes a dispatch-outbox transaction) is outside this anchor-only slice. No activation or schedule policy change is authorized here.
|
||||
|
||||
## Execution plan
|
||||
|
||||
1. Run the targeted `T-V15-SCH-01` unit tests in Release configuration.
|
||||
2. Preserve the command result under `evidence/AEG-V15-033/`.
|
||||
3. Update the progress tracker only with the actual result. This does not assert database, calendar, lease, or enabled-schedule evidence owned by later WBS items.
|
||||
@@ -0,0 +1,21 @@
|
||||
# AEG-V15-034 — Catch-up policy
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-V15-034`; **Slice:** VS-18 / Scheduler
|
||||
- **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-02` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-02`
|
||||
- **Dependency / Gate:** `AEG-V15-033` (completed, commit `6a86997`) / `G3`
|
||||
- **Acceptance:** `LATEST_ONLY`, `SKIP_MISSED`, and `ALL_WITH_LIMIT` bound recovery work so outage recovery does not create a job storm.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `contracts/schedules/model-operations.v3.json`; `DEC-071` (scheduledFor anchor); `DEC-072` (default LATEST_ONLY, no unbounded replay); `ScheduleOccurrencePlanner`; `ModelOperationsDispatcherJob`; schedule repository contract.
|
||||
- **Assumption:** `ALL_WITH_LIMIT` means dispatch at most the configured `maxCatchUp` most-recent missed anchored occurrences in one dispatcher pass. `LATEST_ONLY` dispatches only the most-recent due occurrence. `SKIP_MISSED` advances without dispatching missed occurrences.
|
||||
- **Unknown:** there is no approval to enable any schedule. This slice must remain test-only with scheduler defaults unchanged.
|
||||
- **Decision Required:** `DEC-083` (enqueue/mark atomicity) remains unresolved. The implementation retains the existing lease/release boundary and does not claim a transactional outbox solution.
|
||||
|
||||
## Actual evidence
|
||||
|
||||
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ScheduleOccurrencePlannerTests --logger "trx;LogFileName=ScheduleOccurrencePlannerTests_20260809.trx" --results-directory evidence/AEG-V15-034`
|
||||
- Result: passed `4/4`; artifact: `evidence/AEG-V15-034/ScheduleOccurrencePlannerTests_20260809.trx`; SHA-256: `DC28BE4F2FCF511D5859B9FC3A0ADDF8CE3A566262C9848F05B06D825EA944AD`.
|
||||
- The targeted build compiled the modified dispatcher and Dapper schedule repository. No database integration test, schedule activation, market-calendar claim, or transactional enqueue/mark claim is made.
|
||||
@@ -0,0 +1,21 @@
|
||||
# AEG-V15-035 — Due operation contract
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-V15-035`; **Slice:** VS-18 / Scheduler
|
||||
- **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-03` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-03`
|
||||
- **Dependency / Gate:** `AEG-V15-034` (completed, commit `dd35259`) / `G3`
|
||||
- **Acceptance:** persist and trace `scheduledFor`, catch-up policy, and `maxCatchUp` for each model-operation request.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `db/migrations/0020_v15_execution_and_ui_contract_hardening.sql` adds nullable `evaluation.model_operation_request.scheduled_for`; `DueModelOperation` already receives `ScheduledFor`, `CatchUpPolicy`, and `MaxCatchUp`; the dispatcher and request repository currently drop this provenance before insert.
|
||||
- **Assumption:** an individual request must preserve the selected occurrence's UTC scheduled time and the policy/configuration that selected it. These are immutable request provenance, not mutable schedule state.
|
||||
- **Unknown:** `scheduled_for` is nullable in the approved migration. This Slice will populate it for scheduler-created requests but will not change historical rows or alter the immutable migration.
|
||||
- **Decision Required:** a full per-occurrence dispatch ledger and enqueue/mark transactional outbox remain `DEC-083` / later scheduler work; this Slice only closes the currently missing request-level provenance.
|
||||
|
||||
## Actual evidence
|
||||
|
||||
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter "FullyQualifiedName~ScheduleOccurrencePlannerTests|FullyQualifiedName~ModelOperationRequestServiceTests" --logger "trx;LogFileName=DueModelOperationContractTests_20260809.trx" --results-directory evidence/AEG-V15-035`
|
||||
- Result: passed `5/5`; artifact: `evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx`; SHA-256: `C1BF3EF274702305A29673D5B6A1C3A98D08B1716DA3CD8CB0EE710B5E6C12E6`.
|
||||
- The targeted build compiles Hangfire serialization, application validation, the append-only request insert, transactional outbox payload, and the scheduler policy tests. No schedule was enabled and no database migration or PostgreSQL test is claimed.
|
||||
@@ -0,0 +1,23 @@
|
||||
# AEG-V15-036 — Dispatcher next-due CAS
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-V15-036`; **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-04` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-04`.
|
||||
- **Dependency / Gate:** `AEG-V15-035` (completed, commit `d18f6a7`) / `G3`.
|
||||
- **Acceptance:** a lost lease or stale dispatcher must make zero next-due advances.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `MIG-0020` supplies `dispatch_revision`; acquisition increments it, but the returned due-operation contract and both advance queries currently omit it.
|
||||
- **Assumption:** the acquisition revision is an optimistic-concurrency token. A transition may mutate `next_due_at` only when both the lease owner and acquired revision match.
|
||||
- **Unknown:** a live PostgreSQL runner is unavailable in this session, so a concurrent integration rehearsal cannot be asserted.
|
||||
- **Decision Required:** `DEC-083` transactionality between enqueue and mark is separate. This Slice prevents stale state advance but does not make Hangfire enqueue transactional.
|
||||
|
||||
## Actual evidence and remaining acceptance
|
||||
|
||||
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter "FullyQualifiedName~DapperModelScheduleRepositoryContractTests|FullyQualifiedName~ScheduleOccurrencePlannerTests|FullyQualifiedName~ModelOperationRequestServiceTests" --logger "trx;LogFileName=DispatcherCasContractTests_20260809.trx" --results-directory evidence/AEG-V15-036`
|
||||
- Result: passed `8/8`; artifact: `evidence/AEG-V15-036/DispatcherCasContractTests_20260809.trx`; SHA-256: `148FF7904CAE7372DD4728C24B845E280CEEE3116E5787BE621EC8E2E9005921`.
|
||||
- The test locks the repository contract: dispatched, no-dispatch advance, and release SQL must all match both the lease owner and the acquired `dispatch_revision`.
|
||||
- **PostgreSQL integration:** `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj -c Release --filter FullyQualifiedName~ModelScheduleCasTests --no-restore --logger "trx;LogFileName=ModelScheduleCasTests_20260809.trx" --results-directory evidence/AEG-V15-036` passed `1/1`; artifact SHA-256: `49627FF0180034D2A7A1E4393448C73D337D918E7CE47EA9FC2BDB144FBBA833`.
|
||||
- The test inserts one isolated test-db schedule, acquires it twice after expiring the first lease, and proves the stale owner/revision cannot advance it while the reacquired owner/revision and original `next_due_at` remain intact. It cleans up that row in `finally`.
|
||||
- The first integration execution exposed a real Dapper positional-record materialization failure. `DapperModelScheduleRepository` now maps a private SQL row DTO explicitly to the immutable `DueModelOperation` contract; the successful rerun is the preserved evidence.
|
||||
@@ -0,0 +1,20 @@
|
||||
# AEG-V15-037 — Business hold versus technical failure
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-V15-037`; **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-05` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-05`.
|
||||
- **Dependency / Gate:** `AEG-V15-036` (completed, commit `00957bf`) / `G3`.
|
||||
- **Acceptance:** no blind retry; every business hold records reason and hold-until.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `ModelOperationExecution` permits both `BusinessHold` and `Failed`, while `MIG-0020` provides `hold_until` and `status_reason_code`. The current pure state machine does not model a hold expiry.
|
||||
- **Assumption:** `BusinessHold` requires a future `holdUntil` and may resume only through an explicit subsequent transition. A technical `Failed` transition must not accept a hold expiry, making retry disposition explicit rather than implicit.
|
||||
- **Unknown:** the execution-state persistence/update handler is not yet implemented in the active path; this Slice changes the pure policy contract and tests it without inventing a new persistence workflow.
|
||||
- **Decision Required:** automated retry limits/backoff for technical failures are not approved by the schedule contract and remain outside this Slice.
|
||||
|
||||
## Actual evidence
|
||||
|
||||
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ModelOperationExecutionTests --logger "trx;LogFileName=ModelOperationExecutionTests_20260809.trx" --results-directory evidence/AEG-V15-037`
|
||||
- Result: passed `3/3`; artifact: `evidence/AEG-V15-037/ModelOperationExecutionTests_20260809.trx`; SHA-256: `2F2CD06B1DFD3F76F336A0636598553599CD05CF7FA82E477DB160425975085F`.
|
||||
- The policy is deterministic and I/O-free: a hold has an explicit reason and future expiry, explicit resume clears it, and a technical failure cannot masquerade as a timed business hold. No retry policy, schedule activation, or persistence workflow was introduced.
|
||||
@@ -0,0 +1,21 @@
|
||||
# AEG-V15-038 — Schedule heartbeat and aging
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-V15-038`; **Requirement / API / DB / Job / UI / Test:** `REQ-V15-SCH-06` / `V15-SCH-API` / `MIG-0020` / `J31~J40` / `Cross` / `T-V15-SCH-06`.
|
||||
- **Dependency / Gate:** `AEG-V15-037` (completed, commit `d38dc32`) / `G3`.
|
||||
- **Acceptance:** detect a stuck request and produce Owner-alert evidence.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `MIG-0020` provides `last_heartbeat_at`; the execution state machine has no heartbeat contract; schedule rows provide Primary/Secondary owners.
|
||||
- **Assumption:** a heartbeat is valid only for a running execution and cannot move time backwards. Staleness is evaluated against a caller-supplied cutoff, not a hidden default threshold.
|
||||
- **Unknown:** no approved stale duration, alert transport, severity, or escalation policy exists for J31–J40.
|
||||
- **Decision Required:** an approved owner-alert contract is required before an I/O alert sender or automatic action may be added. This Slice deliberately creates no default threshold, alert, or schedule activation.
|
||||
|
||||
## Actual evidence and remaining acceptance
|
||||
|
||||
- `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ModelOperationExecutionTests --logger "trx;LogFileName=ModelOperationExecutionHeartbeatTests_20260809.trx" --results-directory evidence/AEG-V15-038`
|
||||
- Result: passed `5/5`; artifact: `evidence/AEG-V15-038/ModelOperationExecutionHeartbeatTests_20260809.trx`; SHA-256: `2ADBB526FAF6E5D924EB3F53C7E582E736199A59E0DA4E4FD25DCAA82A661BBC`.
|
||||
- A running execution alone can write a monotonic heartbeat; a caller-provided cutoff deterministically identifies staleness. No magic number, wall clock, alert sender, retry, or activation was introduced.
|
||||
- **Not complete:** persist heartbeat to `last_heartbeat_at`, query it with schedule owners, and produce Owner-alert evidence after the stale-duration and alert-contract decision is approved.
|
||||
@@ -0,0 +1,28 @@
|
||||
# AEG-V16-016 — Vendor boundary fitness
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-016 / REQ-V16-UI4-08 / UI-V16-UI4-08 / T-V16-UI4-08
|
||||
- **Classification:** behavior-preserving validation-tool refactoring. No application provider, API, schema, or component semantics change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `contracts/ui/ui-adapter.v4.json`, `tools/validate_v16.py`, and `frontend/src/shared/ui/adapter/`.
|
||||
- **Assumption:** WBS Master is an extensible source-of-truth catalog; adding approved WBS rows must not make a vendor-boundary validator fail.
|
||||
- **Unknown:** the acceptance evidence for AEG-V16-009 through AEG-V16-015 is absent from the progress tracker. This slice does not recreate or approve it.
|
||||
- **Decision Required:** reconcile the Master and tracker before recording AEG-V16-016 as COMPLETED; dependency AEG-V16-015 has no attached acceptance evidence.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Evidence |
|
||||
| --- | --- |
|
||||
| Feature direct vendor import is zero | `tools/validate_v16.py` scans only `.ts`/`.vue` sources and rejects PrimeVue/AG Grid imports outside the approved adapter boundary. |
|
||||
| Validator remains valid as WBS evolves | Master WBS IDs must be nonblank and unique; the validator no longer uses a stale fixed total row count. |
|
||||
| Reproducible evidence | `python tools/validate_v16.py` result is recorded in the WBS tracker after execution. |
|
||||
|
||||
## Execution evidence — 2026-08-08
|
||||
|
||||
- `python tools/validate_v16.py`: `PASS=1 WARN=2 FAIL=0`. Warnings explicitly do not claim a full source archive or external-runtime evidence.
|
||||
- `frontend: pnpm test -- --run src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts src/shared/ui/tests/adapterCompatibility.spec.ts`: 2 files / 4 tests passed.
|
||||
- `frontend: pnpm typecheck`: passed.
|
||||
- An initial test invocation from repository root failed because that directory has no `package.json`; it is not used as evidence. The frontend-directory invocation above is the preserved result.
|
||||
@@ -0,0 +1,27 @@
|
||||
# AEG-V16-017 — FieldShell standardization
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-017 / REQ-V16-FEC-01 / UI-V16-FEC-01 / T-V16-FEC-01
|
||||
- **Classification:** one FE component vertical slice; no API, database, policy, provider, or design-token contract change.
|
||||
- **Target gate:** MVP-A. This note records implementation evidence only; it does not claim the gate is passed.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`, `contracts/ui/ui-adapter.v4.json`, and the existing components under `frontend/src/shared/ui/components/`.
|
||||
- **Assumption:** the v4 provider adapters preserve fall-through ARIA attributes on their concrete input control. This is characterized by the shared-component test; provider-level evidence remains owned by AEG-V16-013 and AEG-V16-066.
|
||||
- **Unknown:** the tracker has no rows for AEG-V16-009 through AEG-V16-016, while the Master marks them PLANNED and their source artifacts exist. Their acceptance evidence has not been reconstructed in this slice.
|
||||
- **Decision Required:** a WBS owner must reconcile the Master and progress tracker and attach/approve the AEG-V16-016 vendor-boundary evidence before AEG-V16-017 can be marked COMPLETED.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Implementation evidence |
|
||||
| --- | --- |
|
||||
| label/error/help/ARIA single boundary | `FieldShell.vue` owns IDs, label, required indicator, invalid state, and message relationship. |
|
||||
| no repeated field chrome in feature components | text, textarea, select, date, and number wrappers render `FieldShell`. |
|
||||
| reproducible regression check | `FieldShell.spec.ts`, `pnpm typecheck`, and `pnpm test` result are recorded in the tracker after execution. |
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Provider selection, capability changes, visual approval, and runtime API behavior.
|
||||
- Multi-select and checkbox semantics: their v4 provider contracts do not yet expose the normalized field-control props needed for a safe conversion.
|
||||
@@ -0,0 +1,27 @@
|
||||
# AEG-V16-018 — DataContextHeader
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-018 / REQ-V16-FEC-02 / UI-V16-FEC-02 / T-V16-FEC-02
|
||||
- **Classification:** one shared FE component slice; no server, API, schema, policy, or provider change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `frontend/src/shared/contracts/versionSet.ts`, `frontend/src/shared/ui/EvidenceVersionSet.vue`, and existing `KsDataContextHeader.vue`.
|
||||
- **Assumption:** consumers pass values from an approved server-side evidence/projection context; this display component neither creates nor validates an evidence decision.
|
||||
- **Unknown:** the WBS tracker lacks approved predecessor evidence for AEG-V16-017. This slice preserves that dependency as a completion blocker.
|
||||
- **Decision Required:** a UI/UX owner must approve visual and accessibility evidence before this item is marked COMPLETED at MVP-A.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Implementation evidence |
|
||||
| --- | --- |
|
||||
| as-of / VersionSet / Projection / Watermark | all four are required props and rendered in the header. |
|
||||
| stale state is visible and accessible | `data-stale`, visible `STALE`, and an assertive status label are rendered together. |
|
||||
| reproducible component behavior | `KsDataContextHeader.spec.ts` verifies evidence propagation and stale output. |
|
||||
|
||||
## Execution evidence — 2026-08-08
|
||||
|
||||
- `frontend: pnpm test -- --run src/shared/ui/components/tests/KsDataContextHeader.spec.ts`: 1 file / 2 tests passed.
|
||||
- `frontend: pnpm typecheck`: passed.
|
||||
- `frontend: pnpm build`: passed. The build reported a 501.13 kB gzip main chunk warning; no unapproved performance threshold is inferred from that warning.
|
||||
@@ -0,0 +1,26 @@
|
||||
# AEG-V16-019 — CommandBar
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-019 / REQ-V16-FEC-03 / UI-V16-FEC-03 / T-V16-FEC-03
|
||||
- **Classification:** shared FE component vertical slice; no API, policy, data, or provider contract change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** WBS Master, `KsCommandBar.vue`, `KsButton.vue`, and v4 native/Prime button adapters.
|
||||
- **Assumption:** callers own command idempotency keys; this component prevents a local disabled/busy click from becoming a second UI event but cannot replace server-side idempotency.
|
||||
- **Unknown:** predecessor AEG-V16-018 remains pending formal UX/a11y acceptance evidence.
|
||||
- **Decision Required:** no new action ordering policy is introduced. Callers supply the approved action array order.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Implementation evidence |
|
||||
| --- | --- |
|
||||
| action order | the component renders the supplied readonly array in order. |
|
||||
| disabled/busy | execution is blocked in the command boundary for either state; busy is exposed to assistive technology. |
|
||||
| reproducibility | unit test covers order, event emission, disabled, and busy paths. |
|
||||
|
||||
## Execution evidence — 2026-08-08
|
||||
|
||||
- `frontend: pnpm typecheck`: passed.
|
||||
- `frontend: pnpm test -- --run src/shared/ui/components/tests/KsCommandBar.spec.ts`: 1 file / 1 test passed.
|
||||
@@ -0,0 +1,20 @@
|
||||
# AEG-V16-020 — CRUD Resource v2
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-020 / REQ-V16-FEC-04 / UI-V16-FEC-04 / T-V16-FEC-04
|
||||
- **Classification:** shared CRUD contract hardening; no API, database, or policy change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `contracts/ui/crud-resource.v2.json`, `frontend/src/shared/crud/resourceDefinition.*`, and the shared CRUD contract types.
|
||||
- **Assumption:** individual resource definitions originate from approved API contracts; this assertion prevents invalid local definitions but does not authorize a server mutation.
|
||||
- **Unknown:** predecessor AEG-V16-019 is pending formal UX/a11y evidence.
|
||||
- **Decision Required:** no new permission names or sensitive-field masks are introduced; resource owners supply them through approved definitions.
|
||||
|
||||
## Acceptance mapping and execution evidence
|
||||
|
||||
- Zod schema/version, permission, concurrency, idempotency, and sensitive-column consistency are explicitly checked.
|
||||
- A sensitive field now requires both a declared column and `sensitive: true`, avoiding accidental unmasked display.
|
||||
- `frontend: pnpm test -- --run src/shared/crud/tests/resourceDefinition.spec.ts` passed: 1 file / 3 tests.
|
||||
- `frontend: pnpm typecheck` passed before the generated JS companion synchronization; the later JS-only synchronization is covered by the passing runtime test.
|
||||
@@ -0,0 +1,16 @@
|
||||
# AEG-V16-021 — CRUD definition type
|
||||
|
||||
**WBS / Requirement / UI / Test:** AEG-V16-021 / REQ-V16-FEC-05 / UI-V16-FEC-05 / T-V16-FEC-05
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** CRUD resource v2 contract and existing resource definition/assertion.
|
||||
- **Assumption:** each definition's row schema is the source for permitted sensitive field names.
|
||||
- **Unknown:** no feature-level definition exists yet; screen integration evidence is unclaimed.
|
||||
- **Decision Required:** field masking behavior remains a screen/provider policy; this type guards declaration consistency only.
|
||||
|
||||
## Evidence
|
||||
|
||||
- `sensitiveFields` is restricted to string keys of the row type.
|
||||
- Generic assertion preserves query/row/form type relationships.
|
||||
- CRUD resource tests 3/3 and frontend typecheck passed.
|
||||
@@ -0,0 +1,20 @@
|
||||
# AEG-V16-022 — Optimistic command hook
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-022 / REQ-V16-FEC-06 / UI-V16-FEC-06 / T-V16-FEC-06
|
||||
- **Classification:** shared command-boundary correctness fix; no API, DB, policy, or provider change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `AGENTS.md` idempotency rule, `frontend/src/shared/commands/idempotency.*`, and `useOptimisticCommand.*`.
|
||||
- **Assumption:** the caller creates one request per user intent and reuses that request for retry. Server idempotency remains the authoritative side-effect protection.
|
||||
- **Unknown:** no CRUD screen currently consumes this hook; integration evidence remains a later screen-slice responsibility.
|
||||
- **Decision Required:** predecessor AEG-V16-021 has no concrete consumer yet; this implementation does not claim its acceptance evidence.
|
||||
|
||||
## Execution evidence
|
||||
|
||||
- `createRequest()` freezes a request with an idempotency key once; `run()` forwards that exact key on every retry.
|
||||
- `If-Match`, 409/412 conflict state, correlation capture, and pending reset are preserved.
|
||||
- `frontend: pnpm test -- --run src/shared/crud/tests/useOptimisticCommand.spec.ts`: 1 file / 2 tests passed.
|
||||
- `frontend: pnpm typecheck`: passed before JS companion synchronization; the later JS-only synchronization is covered by the passing runtime test.
|
||||
@@ -0,0 +1,17 @@
|
||||
# AEG-V16-023 — Screen state matrix regression
|
||||
|
||||
**WBS / Requirement / UI / Test:** AEG-V16-023 / REQ-V16-FEC-07 / UI-V16-FEC-07 / T-V16-FEC-07
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** screen contract, T01–T10 catalogue, and its Vitest suite.
|
||||
- **Assumption:** each template is exercised by its owning screen integration; this catalogue verifies the shared contract baseline.
|
||||
- **Unknown:** visual/a11y evidence remains a separate AEG-V16-024 gate.
|
||||
- **Decision Required:** no screen-specific state policy was invented; missing READY/FORBIDDEN were assigned to the established CRUD template.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Mandatory states are typed as `StandardScreenState`.
|
||||
- T01 now covers READY and FORBIDDEN; all 13 standard states are covered across T01–T10.
|
||||
- `pnpm test -- --run src/shared/ui/screen-types/tests/catalogue.spec.ts`: 4/4 passed.
|
||||
- `pnpm typecheck`: passed.
|
||||
@@ -0,0 +1,18 @@
|
||||
# AEG-V16-024 — FE accessibility gate
|
||||
|
||||
**WBS / Requirement / UI / Test:** AEG-V16-024 / REQ-V16-FEC-08 / UI-V16-FEC-08 / T-V16-FEC-08
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** FieldShell, CommandBar, their component tests, and WBS Master.
|
||||
- **Assumption:** shared-component checks provide a baseline; each screen still owns keyboard/focus and visual acceptance evidence.
|
||||
- **Unknown:** full browser assistive-technology and visual-regression artifacts are not available in this workspace.
|
||||
- **Decision Required:** do not call the gate complete until UX/QA attach those artifacts.
|
||||
|
||||
## Actual evidence
|
||||
|
||||
- `pnpm test -- --run src/shared/ui/components/tests/accessibility.contract.spec.ts`: 2/2 passed.
|
||||
- `pnpm typecheck`: passed.
|
||||
- The suite verifies required invalid fields expose label, error alert, and ARIA relationships, and busy commands expose state while blocking execution.
|
||||
- Browser inspection on 2026-08-08 confirmed the skip link moves focus to `main`. It also found obsolete v2 contract copy in the UI catalog while the live adapter/footer identify contract 4.0; the display copy is corrected in this slice. The absent favicon is recorded as a non-accessibility local-server 404 and is not treated as a Gate pass/fail signal.
|
||||
- Preserved evidence: `evidence/AEG-V16-024/a11y-contract_20260808.log` (2/2), `evidence/AEG-V16-024/frontend-typecheck_20260808.log`, and `evidence/AEG-V16-024/ui-standard-contract-v4_20260808.png`.
|
||||
@@ -0,0 +1,36 @@
|
||||
# AEG-VS-05-01 — Execution Readiness Blocker
|
||||
|
||||
- **WBS ID:** AEG-VS-05-01
|
||||
- **Slice:** VS-05 IngestFundamentalsPIT
|
||||
- **Requirement / API / DB / Job / UI / Test IDs:** REQ-FND-001 / DAT-05 / MIG-FND-001/002 / J04 / UI-FND-01 / T-FND-001
|
||||
- **Gate:** G1
|
||||
- **Selected on:** 2026-08-08
|
||||
- **Status:** BLOCKED — no implementation has started.
|
||||
|
||||
## Source
|
||||
|
||||
- `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, row `AEG-VS-05-01`, defines VS-05 as **IngestFundamentalsPIT** and requires the public-time and correction history of disclosures, financials, and consensus data.
|
||||
- `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` records only `AEG-VS-02-01` as completed for the declared `VS-02` dependency.
|
||||
- `docs/contracts/architecture/VS-05_RISK_METRICS_SLICE_SPEC.md` and `docs/contracts/data/VS-05_DATA_CONTRACT.md` instead define VS-05 as **Risk Metrics**, including portfolio risk calculations and an unrelated migration `0034_risk_metrics.sql`.
|
||||
|
||||
## Assumption
|
||||
|
||||
- `VS-02` in the WBS dependency means the completed fundamentals-relevant governance/data prerequisite, not merely a similarly named document. This cannot presently be demonstrated from the tracker.
|
||||
|
||||
## Unknown / Decision Required
|
||||
|
||||
1. Confirm whether the two existing `VS-05` contract files are superseded/misfiled Risk Metrics artifacts, or whether the WBS slice/module mapping must change.
|
||||
2. Approve the Fundamentals PIT contract: source authority, public availability timestamp semantics, correction/revision model, consensus licensing/entitlement, retention, and J04 schedule ownership.
|
||||
3. Record the concrete completed prerequisite(s) replacing ambiguous dependency `VS-02`, then approve Gate G1 entry.
|
||||
|
||||
## Safe Resolution Options
|
||||
|
||||
- **Recommended:** The PM/Architect issues an approved `VS-05 IngestFundamentalsPIT` Slice Spec and data contract (using the reviewed templates) and moves/makes obsolete the Risk Metrics documents with a Decision Log entry. Then the dependency is expressed with concrete WBS IDs and Gate G1 evidence.
|
||||
- **Alternative:** Amend `WBS_MASTER.csv` and Traceability Matrix so the existing Risk Metrics contracts become the authoritative VS-05 definition. This changes the planned product scope and requires formal approval.
|
||||
|
||||
## Evidence Produced
|
||||
|
||||
- This blocker record.
|
||||
- Updated `WBS_PROGRESS_TRACKER.csv` state and pointer.
|
||||
|
||||
No API, database migration, job, UI, policy, or test was added. Therefore no build, test, migration, or Gate pass is claimed.
|
||||
@@ -0,0 +1,36 @@
|
||||
# AEG-VS-06-01 — Execution Readiness Blocker
|
||||
|
||||
- **WBS ID:** AEG-VS-06-01
|
||||
- **Slice:** VS-06 MaintainFeeTaxFxSchedule
|
||||
- **Requirement / API / DB / Job / UI / Test IDs:** REQ-COST-001 / COST-01/02 / MIG-COST-001 / J04C / UI-COST-01 / T-COST-001
|
||||
- **Gate:** G1
|
||||
- **Selected on:** 2026-08-08
|
||||
- **Status:** BLOCKED — no implementation has started.
|
||||
|
||||
## Source
|
||||
|
||||
- `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, row `AEG-VS-06-01`, requires account-, market-, and instrument-specific Fee/Tax/FX valid-time contracts.
|
||||
- `docs/CURRENT/CATALOGS/TRACEABILITY_MATRIX.csv` maps `REQ-COST-001` to VS-06 and the IDs listed above.
|
||||
- `docs/contracts/architecture/VS-06_STRESS_TESTING_SLICE_SPEC.md` and `docs/contracts/data/VS-06_DATA_CONTRACT.md` instead define a Risk & Portfolio Management **Stress Testing** slice with `STRESS-*` requirements and migration `0035_stress_testing.sql`.
|
||||
- The WBS prerequisite is written as `VS-02`; the tracker currently has evidence only for `AEG-VS-02-01`.
|
||||
|
||||
## Assumption
|
||||
|
||||
- Fee, tax, and FX schedules must be sourced and versioned independently from stress-testing scenarios. No shared schema, endpoint, job, or policy is presumed.
|
||||
|
||||
## Unknown / Decision Required
|
||||
|
||||
1. Resolve the VS-06 identifier collision: designate the Stress Testing documents as superseded/misfiled or amend WBS scope with formal approval.
|
||||
2. Approve source authority and effective/public-time semantics for each fee, tax, and FX schedule; define jurisdiction, account, market, and instrument precedence and correction handling.
|
||||
3. Confirm the concrete prerequisite WBS items and approve Gate G1 entry before producing `MIG-COST-001`, `COST-01/02`, or J04C.
|
||||
|
||||
## Recommended Resolution
|
||||
|
||||
The PM/Architect and Compliance/Owner should issue an approved `MaintainFeeTaxFxSchedule` Slice Spec and data contract, with a Decision Log entry that resolves or relocates the Stress Testing documents. The amended WBS dependency must name concrete completed WBS IDs, not the ambiguous slice label `VS-02`.
|
||||
|
||||
## Evidence Produced
|
||||
|
||||
- This blocker record.
|
||||
- A `BLOCKED` row in `WBS_PROGRESS_TRACKER.csv`.
|
||||
|
||||
No policy thresholds, rates, tables, endpoint, job, UI, or test was invented. No build, test, migration, or Gate pass is claimed.
|
||||
@@ -0,0 +1,28 @@
|
||||
# AEG-X-002 — Route Code-Splitting Refactoring
|
||||
|
||||
- **WBS / Requirement:** AEG-X-002 / REQ-PLAT-TOOL
|
||||
- **Classification:** behavior-preserving frontend build refactoring
|
||||
- **Selected on:** 2026-08-08
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `frontend/src/app/router.ts` and the co-located tracked `router.js` initially statically import every route component; the actual 2026-08-08 production build reports a 501.14 kB gzip initial JavaScript chunk. A first TypeScript-only refactoring did not change build output, demonstrating that the Vite resolver uses the co-located JavaScript router entry in this repository.
|
||||
- **Assumption:** Vue Router's supported lazy route-component function preserves every route path, metadata, guard behavior, and component API while moving feature code out of the initial chunk.
|
||||
- **Unknown:** an approved application performance budget is not present. Chunk reduction is an observation, not a performance-gate pass.
|
||||
- **Decision Required:** none for this refactoring. Budget approval, visual evidence, and large-list interaction evidence remain separate work.
|
||||
|
||||
## Change
|
||||
|
||||
- Replace route component static imports with lazy `import()` factories in both active co-located router entries.
|
||||
- Load only the selected, validated UI provider at bootstrap instead of statically bundling both provider implementations and their vendor dependencies.
|
||||
|
||||
## Acceptance Evidence
|
||||
|
||||
- All existing route paths and metadata remain unchanged.
|
||||
- `pnpm test` passed: 26 files / 58 tests. Preserved output: `evidence/AEG-X-002/frontend-regression-route-split_20260808.log`.
|
||||
- `pnpm build` passed. Preserved output: `evidence/AEG-X-002/frontend-build-route-split_20260808.log`.
|
||||
- The initial gzip JavaScript chunk changed from 501.14 kB to 423.76 kB and page chunks are emitted separately. Vite still warns because its default raw-size threshold is exceeded; the threshold was not changed and this is not an approved performance-gate pass.
|
||||
- The selected-provider bootstrap refactoring is covered by `frontend/src/shared/ui/provider/tests/resolveUiProvider.spec.ts`: the native provider resolves and an unsupported provider rejects before mount. The final full regression result is 27 files / 60 tests, preserved in `evidence/AEG-X-002/frontend-regression-provider-lazy_20260808.log`.
|
||||
- Final provider-lazy build evidence is `evidence/AEG-X-002/frontend-build-provider-lazy_20260808.log`. The app bootstrap chunk is 20.51 kB (7.35 kB gzip); the selected PrimeVue/AG Grid provider is a separate 1,515.61 kB (393.82 kB gzip) lazy chunk.
|
||||
|
||||
No API, provider, policy, database, capability, or route authorization behavior changes.
|
||||
@@ -0,0 +1,32 @@
|
||||
# AEG-X-002 — Frontend Typecheck Emit Remediation
|
||||
|
||||
- **WBS / Requirement:** AEG-X-002 / REQ-PLAT-TOOL
|
||||
- **Classification:** behavior-preserving frontend toolchain refactoring
|
||||
- **Selected on:** 2026-08-08
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `frontend/package.json`, `frontend/tsconfig.json`, and the tracked `*.vue.js` changes produced by the current build command.
|
||||
- **Assumption:** Vite, not TypeScript, is the approved JavaScript production emitter. Type checking must not modify tracked source-adjacent JavaScript files.
|
||||
- **Unknown:** none for this local toolchain correction.
|
||||
- **Decision Required:** none. The command retains the same type validation and Vite production build; it removes only unintended TypeScript emit.
|
||||
|
||||
## Change
|
||||
|
||||
- Replace `vue-tsc -b && vite build` with `vue-tsc --noEmit && vite build`.
|
||||
- Configure Vite bare-import resolution to prefer `.ts` and `.vue` before co-located legacy `.js` files, so the checked TypeScript source is the runtime source.
|
||||
|
||||
## Acceptance Evidence
|
||||
|
||||
- `pnpm build` completed successfully on 2026-08-08; the preserved output is `evidence/AEG-X-002/frontend-build-noemit_20260808.log`.
|
||||
- Before/after SHA-256 comparison of the previously dirtied tracked `*.vue.js` files found no changed file after the no-emit build.
|
||||
- Full frontend regression was executed before this command-only change: 26 files / 57 tests passed.
|
||||
- The build still reports a 501.14 kB gzip initial JavaScript chunk. It is recorded as a separate performance concern, not hidden by changing Vite's warning limit.
|
||||
|
||||
## Follow-up evidence
|
||||
|
||||
- `pnpm test` passed: 27 files / 60 tests. Output: `evidence/AEG-X-002/frontend-regression-ts-first-resolution_20260808.log`.
|
||||
- `pnpm build` passed: `evidence/AEG-X-002/frontend-build-ts-first-resolution_20260808.log`.
|
||||
- No functional or output-size regression was observed. The existing Vite raw-size warning remains unmasked.
|
||||
|
||||
No provider, API, schema, runtime configuration value, policy, or feature semantics changes.
|
||||
@@ -0,0 +1,22 @@
|
||||
# AEG-X-016 — KIS order-submission hard-off
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS ID:** `AEG-X-016`; **Requirement / API / DB / Job / UI / Test:** `REQ-KIS-OFF` / `Cross` / `Cross` / `Cross` / `Cross` / `Cross`.
|
||||
- **Dependency / Gate:** `AEG-X-005` (completed) / `ALL`.
|
||||
- **Acceptance:** zero external KIS order calls; capability override audit and kill switch pass.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `AGENTS.md` hard prohibition on automatic orders/KIS submission; `CapabilityOptions` startup validation; `TradeEndpoints`, `TradeHandlers`, `KisTradeExecutionService`, `TradeStatusPollingJob`, and Program recurring-job registration.
|
||||
- **Assumption:** the user's instruction is an unconditional hard-off, not a feature flag. Buy, sell, poll, and settlement calls must remain blocked even if an environment value attempts to enable a capability.
|
||||
- **Unknown:** there is no approved release that authorizes re-enabling KIS. This change supplies no activation path.
|
||||
- **Decision Required:** a future enablement would require a separately approved release, complete capability/kill-switch audit, and revised WBS evidence; it is out of scope.
|
||||
|
||||
## Actual evidence and remaining acceptance
|
||||
|
||||
- `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj -c Release --filter FullyQualifiedName~KisTradingHardOffTests --logger "trx;LogFileName=KisTradingHardOffTests_20260809.trx" --results-directory evidence/AEG-X-016`
|
||||
- Result: passed `1/1`; artifact: `evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx`; SHA-256: `F41D1DF823F91705D322A629B08ADF0BBA03EA1BEFA01DEF47E00C9DAFF2470D`.
|
||||
- The test invokes submit, status, cancel, and settlement on the concrete KIS adapter and asserts that every call throws the hard-off exception before its HTTP handler is called (zero external calls).
|
||||
- The submit/poll/settlement handlers guard before any database write or adapter access, and Program removes the historic `trade-status-polling` recurring job.
|
||||
- **Not complete:** endpoint-level disabled response and a startup configuration-override audit remain to be executed before the full WBS acceptance is claimed.
|
||||
@@ -0,0 +1,37 @@
|
||||
# AEG-X-038 — Fee/Tax/FX Valid-Time Decision Readiness
|
||||
|
||||
- **WBS ID:** AEG-X-038
|
||||
- **Requirement / Test IDs:** REQ-COST-001 / T-COST-PIT
|
||||
- **Gate:** G1
|
||||
- **Selected on:** 2026-08-08
|
||||
- **Status:** BLOCKED — no cost policy, data model, or execution path was implemented.
|
||||
|
||||
## Source
|
||||
|
||||
- `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, row `AEG-X-038`, requires reconfirmation of Fee/Tax/FX valid-time schedule decisions and explicitly prohibits a current single-value model.
|
||||
- Its dependency, `AEG-X-009`, is recorded as completed in `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv`.
|
||||
- `docs/CURRENT/CATALOGS/DECISION_LOG.csv`, the source catalog, and the VS-02 governance documents contain no approved Fee/Tax/FX source authority or decision record.
|
||||
- A repository search of application and test source (excluding generated web assets) found **cost-related legacy references**, not an approved schedule implementation: `TradeExecution` persists a broker-supplied `Commission` value, and `VS04_PortfolioPolicy.EstimateRebalanceCost` has a `feePercent = 0.001m` default. Neither has source authority, account/venue/instrument scope, revision, valid-time, or correction evidence. `contracts/metrics/outcome-metrics.v2.json` likewise names costs as a metric input but does not define schedules, sources, or valid-time semantics.
|
||||
|
||||
## Assumption
|
||||
|
||||
- Applicable fee, tax, and FX terms vary by account, venue, instrument, jurisdiction, effective interval, and source publication/correction time. The observed `0.001m` default is not evidence of an approved rate and must not be extended or treated as a schedule. A client-supplied or broker-returned commission is an execution result, not approved decision input.
|
||||
|
||||
## Unknown / Decision Required
|
||||
|
||||
1. **Source authority:** identify the approved provider or governing document for each fee, tax, and FX schedule, including licence/retention and owner.
|
||||
2. **Temporal contract:** approve `effective_from/to`, `published_at`, revision and correction semantics; specify which instant determines a transaction's applicable schedule.
|
||||
3. **Precedence:** approve deterministic account → venue → instrument → jurisdiction fallback order, including the explicit hold behaviour when no schedule applies.
|
||||
4. **Scope boundary:** decide whether FX is market data, cost data, or an approved read-model dependency; a module must not directly read another module's source tables.
|
||||
5. **Operational control:** assign J04C calendar, Owner/Secondary, DQ checks, alert and rollback/replay procedure.
|
||||
|
||||
## Recommended Resolution
|
||||
|
||||
Ops/Tax supplies approved source documents and Compliance/Owner approves the five decisions above. The Data Architect then authors a versioned data contract and an append-only `MIG-COST-001` design. Only after those artifacts are approved can the `MaintainFeeTaxFxSchedule` slice be unblocked.
|
||||
|
||||
## Evidence Produced
|
||||
|
||||
- This source-gap and decision-required record.
|
||||
- `WBS_PROGRESS_TRACKER.csv` state linked to this record.
|
||||
|
||||
No rates, thresholds, source URLs, API, migration, job, UI, or test results are asserted or invented. No build, test, migration, or Gate pass is claimed.
|
||||
@@ -182,7 +182,7 @@ AEG-VS-24-04,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,BE,REQ-RPT-
|
||||
AEG-VS-24-05,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,ASYNC,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Event/Job/Inbox·재처리 구현,"J11R; integration event, JobRun, watermark, inbox","동일 ScopeKey 재실행 결과가 동일하고 중복 side effect 0, transient/business hold/poison이 분리됨",BE/SRE,Data/Ops,5,AEG-VS-24-04,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-VS-24-06,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,FE,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,Vue feature·Zod·Query·컴포넌트 구현,UI-RPT-01; route/api/schema/queries/pages/components,loading/empty/partial/stale/warn/error/401/403/409/expired/readonly와 접근성·권한 경계가 검증됨,FE Lead,UX/QA,6,AEG-VS-24-05,G4-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-VS-24-07,S11,W23-24,VS-24,ReportAndNotify,ReportingNotifications,TESTOPS,REQ-RPT-001,RPT-01,MIG-RPT-001,J11R,UI-RPT-01,T-RPT-001,회귀·관제·Runbook·Rollback 증거,T-RPT-001; xUnit/Vitest/Playwright/Data/Replay; OTel/alert/runbook,Golden·integration·failure·replay·E2E와 metric/alert/Owner/Secondary/rollback rehearsal가 Release Evidence에 연결됨,QA/SRE,Module Owner,7,AEG-VS-24-06,G4-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-X-016,S12,W25-26,Cross,Cross-cutting,Broker,CAPABILITY,REQ-KIS-OFF,Cross,Cross,Cross,Cross,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,KIS 주문 제출 startup/CI/runtime 차단,"외부 주문 호출 0, capability override 감사·kill-switch 통과",Security/Ops,Architect/QA,7,AEG-X-005,ALL,SOURCE+DESIGN_PROPOSAL,P0,IN_PROGRESS
|
||||
AEG-VS-23-01,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,GOV,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,정책·범위·실패상태 계약 확정,"REQ-RECON-001, VS-23 SLICE_SPEC, ADR/Decision Log",사용자 결과 '브로커·내부 주문·체결·보유·현금 break와 승인 정정'·비목표·권한·예외·Source/Assumption/Unknown이 승인됨,PM/Architect,Compliance/Owner,3,"VS-08,VS-16",Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-VS-23-02,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DATA,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,데이터 시점·스키마·정합성 계약,"MIG-EXE-001/002, DATA_CONTRACT, DQ/lineage 규칙",published_at/revision/valid-time/hash/단위/격리/재처리와 소유자가 정의되고 overwrite 경로가 없음,Data Architect/DBA,Quant/QA,5,AEG-VS-23-01,Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-VS-23-03,S12,W25-26,VS-23,ReconcilePositionsAndFills,ExecutionReconciliation,DOMAIN,REQ-RECON-001,"EXE-01/02,REC-01/02",MIG-EXE-001/002,J12,UI-RECON-01,T-RECON-001,도메인 불변조건·상태전이 구현,"src/Modules/ExecutionReconciliation/ReconcilePositionsAndFills/Domain, 정책 결정표",순수 정책 테스트에서 우선순위·경계값·단조성·금지 전이가 통과하고 Infrastructure 의존이 없음,BE/Quant Lead,Architect/QA,6,AEG-VS-23-02,Pilot-A,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
@@ -511,12 +511,12 @@ AEG-V15-029,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-05,Cross,Cro
|
||||
AEG-V15-030,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-06,Cross,Cross,Cross,UI-V15-LAY-06,T-V15-LAY-06,민감정보 column 표시정책,PII grid policy,마스킹·export 권한·audit,UX Lead,FE/QA,6,AEG-V15-029,MVP-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-031,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-07,Cross,Cross,Cross,UI-V15-LAY-07,T-V15-LAY-07,반응형 운영화면 기준,responsive evidence,1024/1440/1920/모바일 업무순서 유지,UX Lead,FE/QA,4,AEG-V15-030,MVP-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-032,S6,W13-14,Cross,Cross-cutting,UX/Layout,LAY,REQ-V15-LAY-08,Cross,Cross,Cross,UI-V15-LAY-08,T-V15-LAY-08,디자인 token 변경 Gate,semantic token contract,기능 CSS 직접 색상/간격 drift 0,UX Lead,FE/QA,4,AEG-V15-031,MVP-A,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-033,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-01,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-01,Schedule anchor 계산기,ScheduleOccurrencePlanner,dispatch 지연이 cadence drift를 만들지 않음,BE Lead,SRE/QA,3,AEG-X-001,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-V15-034,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-02,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-02,Catch-up policy 구현,LATEST/SKIP/ALL_LIMIT,장애 복구 후 job storm 0,BE Lead,SRE/QA,5,AEG-V15-033,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-V15-035,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-03,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-03,Due operation 계약 확장,DueModelOperation v15,scheduledFor/catchUp/maxCatchUp 추적,BE Lead,SRE/QA,5,AEG-V15-034,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-036,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-04,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-04,Dispatcher nextDue CAS,schedule repository,lease 상실·중복 advance 0,BE Lead,SRE/QA,6,AEG-V15-035,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-V15-037,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-05,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-05,BusinessHold와 기술실패 분리,execution state machine,"blind retry 0, reason/holdUntil 기록",BE Lead,SRE/QA,5,AEG-V15-036,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-038,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-06,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-06,Schedule heartbeat/aging,execution heartbeat,stuck request 탐지와 Owner alert,BE Lead,SRE/QA,6,AEG-V15-037,G3,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
AEG-V15-033,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-01,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-01,Schedule anchor 계산기,ScheduleOccurrencePlanner,dispatch 지연이 cadence drift를 만들지 않음,BE Lead,SRE/QA,3,AEG-X-001,G3,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
AEG-V15-034,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-02,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-02,Catch-up policy 구현,LATEST/SKIP/ALL_LIMIT,장애 복구 후 job storm 0,BE Lead,SRE/QA,5,AEG-V15-033,G3,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
AEG-V15-035,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-03,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-03,Due operation 계약 확장,DueModelOperation v15,scheduledFor/catchUp/maxCatchUp 추적,BE Lead,SRE/QA,5,AEG-V15-034,G3,SOURCE+DESIGN_PROPOSAL,P1,COMPLETED
|
||||
AEG-V15-036,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-04,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-04,Dispatcher nextDue CAS,schedule repository,lease 상실·중복 advance 0,BE Lead,SRE/QA,6,AEG-V15-035,G3,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
AEG-V15-037,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-05,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-05,BusinessHold와 기술실패 분리,execution state machine,"blind retry 0, reason/holdUntil 기록",BE Lead,SRE/QA,5,AEG-V15-036,G3,SOURCE+DESIGN_PROPOSAL,P1,COMPLETED
|
||||
AEG-V15-038,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-06,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-06,Schedule heartbeat/aging,execution heartbeat,stuck request 탐지와 Owner alert,BE Lead,SRE/QA,6,AEG-V15-037,G3,SOURCE+DESIGN_PROPOSAL,P0,IN_PROGRESS
|
||||
AEG-V15-039,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-07,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-07,시장 timezone/calendar 계약,calendar/timezone contract,UTC 저장·시장세션 계산·DST 테스트,BE Lead,SRE/QA,4,AEG-V15-038,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-040,S8,W17-18,VS-18,BacktestingEvaluation,Scheduler,SCH,REQ-V15-SCH-08,V15-SCH-API,MIG-0020,J31~J40,Cross,T-V15-SCH-08,Scheduler chaos rehearsal,dispatcher chaos tests,enqueue/mark 실패·재시작 중복 side effect 0,BE Lead,SRE/QA,4,AEG-V15-039,G3,SOURCE+DESIGN_PROPOSAL,P1,PLANNED
|
||||
AEG-V15-041,S8,W17-18,VS-18,BacktestingEvaluation,Evaluation Windows,EVA,REQ-V15-EVA-01,V15-EVA-API,MIG-0020,J31~J40,Cross,T-V15-EVA-01,1/5/20/63/126/252 window planner,EvaluationWindowPlanner,calendar day가 아닌 거래세션 사용,Quant Lead,Data/QA,3,AEG-X-001,G4,SOURCE+DESIGN_PROPOSAL,P0,PLANNED
|
||||
@@ -663,3 +663,7 @@ AEG-V16-085,S15,W31-32,Cross,Cross-cutting,Packaging/Docs,PKG,REQ-V16-PKG-05,Cro
|
||||
AEG-V16-086,S15,W31-32,Cross,Cross-cutting,Packaging/Docs,PKG,REQ-V16-PKG-06,Cross,Cross,Cross,Cross,T-V16-PKG-06,Manifest/SHA256,PACKAGE_MANIFEST/SHA256SUMS,전 파일 hash,Release Manager,PM/QA,4,AEG-V16-085,G6,SOURCE+V16_DELTA,P1,PLANNED
|
||||
AEG-V16-087,S15,W31-32,Cross,Cross-cutting,Packaging/Docs,PKG,REQ-V16-PKG-07,Cross,Cross,Cross,Cross,T-V16-PKG-07,ZIP CRC/recursive scan,PACKAGE_QA,testzip null·self include 0,Release Manager,PM/QA,5,AEG-V16-086,G6,SOURCE+V16_DELTA,P1,PLANNED
|
||||
AEG-V16-088,S15,W31-32,Cross,Cross-cutting,Packaging/Docs,PKG,REQ-V16-PKG-08,Cross,Cross,Cross,Cross,T-V16-PKG-08,Distribution README,DISTRIBUTION_README,검증 진실성·실행 순서,Release Manager,PM/QA,6,AEG-V16-087,G6,SOURCE+V16_DELTA,P1,PLANNED
|
||||
AEG-VS-26-01,S2,W-,VS-26,ApprovalWorkflow,ModelOperations,GOV,REQ-APR-001,APR-01,MIG-0036,-,UI-APR-01,T-APR-001,모델 승인 워크플로우 (Maker-Checker Governance),"docs/CURRENT/SLICE_SPECS/VS-26-SLICE_SPEC.md; ADR-WBS-001",상세 상태는 WBS_PROGRESS_TRACKER.csv AEG-VS-26-01 참조 — BLOCKED (DEBT-017: 두 개의 중복 구현 중 실제 등록된 쪽은 테스트 안됨),PM/BE Lead,Security/QA,-,-,G1-B,SOURCE+DESIGN_PROPOSAL,P0,BLOCKED
|
||||
AEG-VS-27-01,S2,W-,VS-27,AuditTrail,ModelOperations,GOV,REQ-AUD-001,AUD-01,MIG-0037,-,UI-AUD-01,T-AUD-001,불변 감사 추적 (Audit Trail / GDPR),"docs/CURRENT/SLICE_SPECS/VS-27-SLICE_SPEC.md; ADR-WBS-001",상세 상태는 WBS_PROGRESS_TRACKER.csv AEG-VS-27-01 참조 — Backend 5/5 tests PASS(격리); 프런트엔드 UI 없음,PM/BE Lead,Security/QA,-,-,G1-B,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
AEG-VS-28-01,S2,W-,VS-28,TradeExecution,ModelOperations,BE,REQ-TRD-001,TRD-01,MIG-0039,-,UI-TRD-01,T-TRD-001,"거래 실행 시스템 (Trade Execution, KIS)","docs/CURRENT/SLICE_SPECS/VS-28-SLICE_SPEC.md; ADR-WBS-001",상세 상태는 WBS_PROGRESS_TRACKER.csv AEG-VS-28-01 참조 — Backend 13/13 tests PASS(격리); 프런트엔드 UI 없음,BE Lead/Trading Ops,Security/QA,-,-,G1-B,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
AEG-VS-29-01,S2,W-,VS-29,PortfolioReconciliation,ModelOperations,BE,REQ-REC-002,REC-02,MIG-0040,-,UI-REC-02,T-REC-002,포트폴리오 대사,"docs/CURRENT/SLICE_SPECS/VS-29-SLICE_SPEC.md; ADR-WBS-001",상세 상태는 WBS_PROGRESS_TRACKER.csv AEG-VS-29-01 참조 — Backend 18/18 tests PASS(격리); 프런트엔드 UI 없음,BE Lead,Security/QA,-,-,G1-B,SOURCE+DESIGN_PROPOSAL,P0,COMPLETED
|
||||
|
||||
|
@@ -1,11 +1,28 @@
|
||||
WBS_ID,Sprint,Slice_ID,Task,Status,Completion_Date,Evidence_Link,Owner,Notes
|
||||
AEG-X-001,S0,Cross,Version Coverage Matrix 고도화,COMPLETED,2026-08-04,docs/contracts/platform/VERSION_COVERAGE_MATRIX.md,PM/Architect,"✅ Version matrix: v10/v12/v12.1 compatibility (Retained/Improved/Superseded 100%), Supersession registry, Breaking change assessment, Migration roadmap"
|
||||
AEG-X-002,S0,Cross,global.json 고도화,COMPLETED,2026-08-04,.gitea/workflows/ci.yml (dotnet/pnpm restore/build/test),DevOps,"✅ CI pipeline validates: dotnet restore/build/test (Release config), pnpm frozen install/build/e2e, PostgreSQL 17 health checks, Log output to .gitea/workflows/ci.yml"
|
||||
AEG-X-002,S0,Cross,global.json 고도화,COMPLETED,2026-08-08,".gitea/workflows/ci.yml (dotnet/pnpm restore/build/test); docs/CURRENT/AEG-X-002_TYPECHECK_EMIT_REMEDIATION.md; docs/CURRENT/AEG-X-002_ROUTE_CODE_SPLITTING.md; evidence/AEG-X-002/frontend-build-noemit_20260808.log; evidence/AEG-X-002/frontend-regression-route-split_20260808.log; evidence/AEG-X-002/frontend-build-route-split_20260808.log; evidence/AEG-X-002/frontend-regression-provider-lazy_20260808.log; evidence/AEG-X-002/frontend-build-provider-lazy_20260808.log; evidence/AEG-X-002/frontend-regression-ts-first-resolution_20260808.log; evidence/AEG-X-002/frontend-build-ts-first-resolution_20260808.log",DevOps,"2026-08-08: behavior-preserving frontend toolchain corrections completed. `pnpm build` uses `vue-tsc --noEmit && vite build`; actual no-emit build passed. Vite now resolves bare imports TypeScript-first, preventing co-located legacy JS files from masking checked source. Route and provider static imports were replaced in both active co-located JS/TS entries after the first TS-only change proved Vite resolves JS. Full regression: 27 files / 60 tests passed. Route splitting reduced initial gzip JS 501.14 kB→423.76 kB; provider splitting leaves a 20.51 kB (7.35 kB gzip) bootstrap chunk and lazy-loads PrimeVue/AG Grid at 393.82 kB gzip. Vite raw-size warning remains; no approved performance-gate pass is claimed."
|
||||
AEG-X-003,S0,Cross,Architecture tests 고도화,COMPLETED,2026-08-04,tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs (6 tests PASSING),Architect/QA,"✅ Architecture rules enforced: (1) No prohibited patterns, (2) Domain isolation from infrastructure, (3) SQL validation (no SELECT *, schema-qualified), (4) Endpoint authorization (Roles/Policies), (5) No placeholder files, (6) No duplicate aggregate IDs. All 6 tests PASS."
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md; db/migrations/0032_shadow_run_queued_status_contract.sql; tests/KArtSell.Integration.Tests/DbUpMigrationTests.cs; tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs; evidence/AEG-X-004/0032-isolated-migration.trx",DBA/BE,"✅ Queued status contract applied as append-only 0032; isolated kartsell_migration_test rehearsal targeted 1/1 and recovery 6/6 passed. kartselldb_test was not reset. Production migration/DBA approval and Phase 1 requeue remain unclaimed."
|
||||
AEG-X-004,S0,Cross,DbUp 복구 rehearsal 고도화,COMPLETED,2026-08-06,"docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/AEG-X-004_STATUS_CONTRACT_SLICE.md; db/migrations/0032_shadow_run_queued_status_contract.sql; tests/KArtSell.Integration.Tests/DbUpMigrationTests.cs; tests/KArtSell.Integration.Tests/DbUpRecoveryTests.cs; evidence/AEG-X-004/0032-isolated-migration.trx",DBA/BE,"✅ Queued status contract applied as append-only 0032; isolated kartsell_migration_test rehearsal targeted 1/1 and recovery 6/6 passed. kartselldb_test was not reset. Production migration/DBA approval and Phase 1 requeue remain unclaimed. ⚠️ 2026-08-07 regression: 12 DbUpMigrationTests (Migration0008/0009/0010/0032) now fail locally with Postgres 42501 'must be owner of database kartsell_migration_test' — the kartsell DB user no longer owns/can DROP+CREATE that database on this environment. Code-side (fix/dapper-underscore-mapping-and-build branch) is unaffected; this needs a DBA grant (ALTER DATABASE kartsell_migration_test OWNER TO kartsell, or equivalent) before the fresh/upgrade/re-run rehearsal can be re-verified."
|
||||
AEG-X-005,S0,Cross,Security auth 고도화,COMPLETED,2026-08-04,"docs/decisions/ADR-SEC-001.md + tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs (6 tests)",Security/BE,"✅ ADR-SEC-001 produced (OIDC/JWT/DevelopmentHeader tiers), SecurityAuthenticationTests.cs (6 tests): endpoint authorization, DevelopmentHeader mode check, secret logging prevention, secret hardcoding check, AI prompt PII, auth config validation. Acceptance_Evidence verified: '비개발 무인증 접근 0, secret/log/prompt 노출 0'"
|
||||
AEG-X-006,S0,Cross,Outbox publisher 고도화,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-X-006_ACCEPTANCE_EVIDENCE.md + src/KArtSell.BuildingBlocks/Reliability/DapperOutboxWriter.cs + OutboxPollerJob.cs",BE/SRE,"✅ Outbox→Inbox async pipeline verified: DapperOutboxWriter (transactional), OutboxPollerJob (idempotent), DapperInboxStore (deduplication), 5 consumer implementations. Acceptance_Evidence: All criteria met. 177/177 tests PASS."
|
||||
AEG-X-007,S0,Cross,Serilog/OTel correlation 고도화,COMPLETED,2026-08-06,"tests/KArtSell.ArchitectureTests/PiiRedactionTests.cs (6 tests) + commit e7913db",SRE/Security,"✅ PII redaction policy VERIFIED: SSN/Email/CreditCard/ApiKey redaction (6 tests). Commit e7913db adds pattern-based sanitization validation. All tests PASS (249/253)."
|
||||
AEG-X-016,S12,Cross,KIS 주문 제출 startup/CI/runtime 차단 고도화,IN_PROGRESS,TBD,"docs/CURRENT/AEG-X-016_KIS_HARD_OFF_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/TradeExecution/KisTradeExecutionService.cs; src/KArtSell.Modules.ModelOperations/TradeExecution/TradeHandlers.cs; src/KArtSell.Host/Program.cs; tests/KArtSell.Integration.Tests/TradeExecution/KisTradingHardOffTests.cs; evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx",Security/Ops,"User-directed hard-off: concrete KIS submit/status/cancel/settlement adapter throws before HTTP; test proves zero HTTP calls (1/1). Trade handlers guard before DB writes and Program removes trade-status-polling. Remains IN_PROGRESS until endpoint-level disabled response and startup capability-override/kill-switch evidence are run. No KIS activation path was introduced."
|
||||
AEG-V15-033,S8,VS-18,Schedule anchor 계산기,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-033_SCHEDULE_ANCHOR_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-033/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj -c Release --filter FullyQualifiedName~ScheduleOccurrencePlannerTests; 2/2 passed. Characterizes the approved scheduledFor anchor and missed-occurrence skip behavior. No schedule was enabled; market-calendar/timezone (DEC-079), dispatch enqueue/mark atomicity (DEC-083), leases, database integration, and later scheduler WBS evidence remain out of scope."
|
||||
AEG-V15-034,S8,VS-18,Catch-up policy 구현,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-034_CATCH_UP_POLICY_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ScheduleOccurrencePlanner.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelScheduleRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ScheduleOccurrencePlannerTests.cs; evidence/AEG-V15-034/ScheduleOccurrencePlannerTests_20260809.trx",BE Lead,"Actual Release run: targeted ScheduleOccurrencePlannerTests 4/4 passed. LATEST_ONLY dispatches only the latest anchored occurrence; SKIP_MISSED advances the lease-held schedule without enqueueing stale work; ALL_WITH_LIMIT dispatches only the configured most-recent occurrences, each with an occurrence-specific UTC idempotency key. Schedules remain disabled. Database integration, lease CAS, and DEC-083 enqueue/mark atomicity are not claimed and remain owned by later WBS items."
|
||||
AEG-V15-035,S8,VS-18,Due operation 계약 확장,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-035_DUE_OPERATION_CONTRACT_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Application/ModelOperationsContracts.cs; src/KArtSell.Modules.ModelOperations/Application/ModelOperationRequestService.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ScheduledModelOperationJob.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelOperationRequestRepository.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationRequestServiceTests.cs; evidence/AEG-V15-035/DueModelOperationContractTests_20260809.trx",BE Lead,"Actual Release run: 5/5 targeted unit tests passed. The scheduler occurrence, catch-up policy, and max catch-up flow from due schedule through the serialized job and validated application request; scheduled_for is inserted in the normalized request model and all three values are retained in the transactional outbox payload. Schedules remain disabled. No new migration or PostgreSQL integration evidence is claimed: MIG-0020 already provides scheduled_for; policy and limit provenance is immutable in the event payload, while schedule configuration remains the normalized source referenced by schedule_id/version."
|
||||
AEG-V15-036,S8,VS-18,Dispatcher nextDue CAS,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-036_DISPATCH_CAS_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Application/ModelOperationsContracts.cs; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperModelScheduleRepository.cs; src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsDispatcherJob.cs; tests/KArtSell.ModelOperations.UnitTests/DapperModelScheduleRepositoryContractTests.cs; tests/KArtSell.Integration.Tests/Scheduling/ModelScheduleCasTests.cs; evidence/AEG-V15-036/DispatcherCasContractTests_20260809.trx; evidence/AEG-V15-036/ModelScheduleCasTests_20260809.trx",BE Lead,"Actual evidence: unit contract tests 8/8 passed and PostgreSQL integration ModelScheduleCasTests 1/1 passed. The integration test acquires an isolated schedule, expires/reacquires its lease, and verifies a stale owner/revision cannot mutate next_due_at (0-row CAS) while the current owner/revision remains. It found and fixed Dapper positional record materialization by mapping a SQL row DTO explicitly to DueModelOperation. Schedules remain disabled; DEC-083 enqueue/mark atomicity remains a separate later Slice."
|
||||
AEG-V15-037,S8,VS-18,BusinessHold와 기술실패 분리,COMPLETED,2026-08-09,"docs/CURRENT/AEG-V15-037_EXECUTION_HOLD_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationExecutionTests.cs; evidence/AEG-V15-037/ModelOperationExecutionTests_20260809.trx",BE Lead,"Actual Release evidence: ModelOperationExecutionTests 3/3 passed. The pure state machine requires a future holdUntil plus reason for BUSINESS_HOLD, clears it only through explicit resume, and rejects holdUntil for FAILED. This prevents a business hold from becoming a blind technical retry. No unapproved retry/backoff, schedule activation, persistence workflow, or threshold was added."
|
||||
AEG-V15-038,S8,VS-18,Schedule heartbeat/aging,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V15-038_HEARTBEAT_AGING_SLICE_NOTE.md; src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecution.cs; tests/KArtSell.ModelOperations.UnitTests/ModelOperationExecutionTests.cs; evidence/AEG-V15-038/ModelOperationExecutionHeartbeatTests_20260809.trx",BE Lead,"Implemented and verified the pure heartbeat/aging contract: only RUNNING accepts monotonic heartbeats, and staleness uses an explicit caller-supplied cutoff (5/5 targeted Release tests passed). Still IN_PROGRESS: the approved stale-duration, alert channel/owner/escalation contract is absent, so no magic timeout, alert sender, persistence workflow, or schedule activation was invented."
|
||||
AEG-V16-017,S6,Cross,FieldShell 표준,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-017_FIELDSHELL_SLICE_NOTE.md; frontend/src/shared/ui/components/FieldShell.vue; frontend/src/shared/ui/components/tests/FieldShell.spec.ts","FE Lead","2026-08-08: FieldShell now owns label/error/help/ARIA relationships for KsTextField, KsTextArea, KsSelect, KsDateField, and KsNumberField. Actual evidence: frontend pnpm typecheck PASS; pnpm test PASS (19 files, 42 tests); pnpm build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending WBS Master/tracker reconciliation and AEG-V16-016 vendor-boundary acceptance evidence."
|
||||
AEG-V16-016,S0,VS-00,Vendor boundary fitness,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-016_VENDOR_BOUNDARY_SLICE_NOTE.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts; evidence/AEG-V16-016/validate_v16_20260808.log; evidence/AEG-V16-016/ui-adapter-tests_20260808.log; evidence/AEG-V16-016/frontend-typecheck_20260808.log","FE Lead","2026-08-08: Removed stale fixed WBS row-count assertion; validator now verifies WBS ID integrity and reports vendor imports outside the approved adapter boundary. Re-executed actual evidence: python tools/validate_v16.py PASS=1 WARN=2 FAIL=0; targeted adapter tests 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked because dependency AEG-V16-015 has no approved acceptance evidence in the tracker."
|
||||
AEG-V16-015,S0,VS-00,Adapter rollback runbook,BLOCKED,-,"docs/CURRENT/ui-provider-switch.md","FE Lead","2026-08-08: Runbook exists, but status is BLOCKED before completion: acceptance requires visual/a11y/performance rollback rehearsal evidence, which is not present; direct dependency AEG-V16-014 has no tracker evidence. A runbook does not substitute for an approved visual baseline, keyboard/focus and accessible-name report, state-matrix result, agreed performance budget, immutable-artifact rollback rehearsal, and append-only release evidence. No build/test/migration claimed by this status correction."
|
||||
AEG-V16-018,S6,Cross,DataContextHeader,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-018_DATA_CONTEXT_HEADER_SLICE_NOTE.md; frontend/src/shared/ui/components/KsDataContextHeader.vue; frontend/src/shared/ui/components/tests/KsDataContextHeader.spec.ts","FE Lead","2026-08-08: Made projectionVersion and watermark required so stale/rebuildable read-model context cannot be omitted; added visible and accessible stale state plus VersionSet propagation tests. Actual evidence: targeted Vitest 2/2 PASS, frontend typecheck PASS, production build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending predecessor AEG-V16-017 acceptance and UX/a11y evidence."
|
||||
AEG-V16-019,S6,Cross,CommandBar,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-019_COMMAND_BAR_SLICE_NOTE.md; frontend/src/shared/ui/components/KsCommandBar.vue; frontend/src/shared/ui/components/tests/KsCommandBar.spec.ts","FE Lead","2026-08-08: Command boundary now suppresses disabled/busy execute events and exposes aggregate busy state. Actual evidence: targeted Vitest 1/1 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor AEG-V16-018 acceptance and UX/a11y evidence."
|
||||
AEG-V16-020,S6,Cross,CRUD Resource v2,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-020_CRUD_RESOURCE_V2_SLICE_NOTE.md; contracts/ui/crud-resource.v2.json; frontend/src/shared/crud/resourceDefinition.ts; frontend/src/shared/crud/tests/resourceDefinition.spec.ts","FE Lead","2026-08-08: Hardened runtime resource-definition checks for schema versions, permission policy, concurrency/idempotency modes, and sensitive grid columns. Actual evidence: resource definition Vitest 3/3 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor AEG-V16-019 acceptance and UX/a11y evidence."
|
||||
AEG-V16-021,S6,Cross,CRUD definition type,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-021_CRUD_DEFINITION_TYPE_SLICE_NOTE.md; frontend/src/shared/crud/resourceDefinition.ts; frontend/src/shared/crud/tests/resourceDefinition.spec.ts","FE Lead","2026-08-08: Sensitive field declarations are constrained to actual row keys and generic assertion preserves definition type relationships. Actual evidence: resource definition Vitest 3/3 PASS; frontend typecheck PASS. COMPLETED is blocked pending feature-level CRUD definition integration and predecessor evidence."
|
||||
AEG-V16-022,S6,Cross,Optimistic command hook,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-022_OPTIMISTIC_COMMAND_SLICE_NOTE.md; frontend/src/shared/crud/useOptimisticCommand.ts; frontend/src/shared/crud/tests/useOptimisticCommand.spec.ts","FE Lead","2026-08-08: Request creation now freezes one Idempotency-Key per user intent and retries reuse it; 409/412 conflict state remains explicit. Actual evidence: targeted Vitest 2/2 PASS; frontend typecheck PASS. COMPLETED is blocked pending actual CRUD-screen integration and predecessor evidence."
|
||||
AEG-V16-023,S6,Cross,T01~T10 계약 회귀,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-023_SCREEN_STATE_MATRIX_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/catalogue.ts; frontend/src/shared/ui/screen-types/tests/catalogue.spec.ts","FE Lead","2026-08-08: State contract is typed and all 13 standard states are now covered across T01~T10, including READY and FORBIDDEN. Actual evidence: catalogue Vitest 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor integration and FE accessibility gate evidence."
|
||||
AEG-V16-024,S6,Cross,FE accessibility Gate,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-024_A11Y_GATE_SLICE_NOTE.md; frontend/src/shared/ui/components/tests/accessibility.contract.spec.ts; evidence/AEG-V16-024/a11y-contract_20260808.log; evidence/AEG-V16-024/frontend-typecheck_20260808.log; evidence/AEG-V16-024/ui-standard-contract-v4_20260808.png","FE Lead","2026-08-08: Shared accessibility contract verifies required invalid field label/error/ARIA relationships and busy command action suppression. Actual evidence: accessibility Vitest 2/2 PASS; frontend typecheck PASS; browser snapshot confirms skip link focuses main. Browser review corrected obsolete UI catalog v2 contract copy to v4.0. COMPLETED remains blocked pending UX/QA assistive-technology and approved visual baseline evidence; local screenshot is implementation evidence, not that approval."
|
||||
AEG-X-008,S0,Cross,OpenAPI artifact 고도화,COMPLETED,2026-08-04,.gitea/workflows/openapi-gate.yml + docs/api/openapi.json,BE/FE Architect,"✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR"
|
||||
AEG-VS-00-01,S0,VS-00,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-06,"docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db",PM/Architect,"✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS."
|
||||
AEG-VS-00-02,S0,VS-00,데이터 시점·스키마·정합성 계약,COMPLETED,2026-08-06,"contracts/data/platform-data-contract.v1.json + commit e7913db",Data Architect/DBA,"✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS."
|
||||
@@ -14,14 +31,24 @@ AEG-VS-00-04,S0,VS-00,Vertical Slice API/Application/SQL 구현,COMPLETED,2026-0
|
||||
AEG-VS-00-05,S0,VS-00,Event/Job/Inbox·재처리 구현,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-VS-00-05_ACCEPTANCE_EVIDENCE.md + src/KArtSell.Host/Jobs/OutboxPollerJob.cs + DownstreamConsumerJob.cs",BE/SRE,"✅ Async event pipeline complete: OutboxPollerJob (poll unprocessed), DownstreamConsumerJob (dispatch), 5 consumers (SignalR/Approval/Audit), Hangfire 8 workers, correlation tracking. Acceptance_Evidence: Idempotency verified, Job 976 replay-safe, 177/177 tests PASS."
|
||||
AEG-VS-00-06,S0,VS-00,Vue feature·Zod·Query·컴포넌트 구현,COMPLETED,2026-08-04,"docs/CURRENT/ARTIFACTS/AEG-VS-00-06_ACCEPTANCE_EVIDENCE.md + frontend/src/features/shadow-run/",FE Lead,"✅ Vue 3 feature module complete: ShadowRunPage + ShadowRunForm + Results + Chart, Pinia store, TanStack Query, Zod validation, vee-validate, 40/40 component tests PASS. Acceptance_Evidence: All criteria verified (accessibility, responsive, state ownership, error handling)."
|
||||
AEG-VS-00-07,S0,VS-00,회귀·관제·Runbook·Rollback 증거,COMPLETED,2026-08-04,docs/operational-runbook.md + PRODUCTION_READINESS.md + scripts/*.ps1 + commit ca2aeae,QA/SRE,"Golden/integration/failure/replay/E2E + metric/alert/Owner/Secondary/rollback rehearsal complete (Acceptance_Evidence: '회귀·관제·Runbook·Rollback 증거') - 7 scenarios, 4 scripts, 18 queries verified"
|
||||
AEG-X-009,S1,Cross,Source catalog 고도화,COMPLETED,2026-08-07,"docs/CURRENT/CATALOGS/source-catalog.md; docs/CURRENT/AEG-X-009_AUTOMATION_PROPOSAL.md; contracts/data/source-approval.v1.proposed.json; docs/DECISIONS/ADR-DATA-001.md; db/migrations/0033_source_approval_contract.sql; db/migrations/0034_dataset_manifest_freeze_contract.sql; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperApprovedModelContextReader.cs",Data Governance,"✅ Workstream D/E/F COMPLETED: source-catalog.md v2.0 (KRX/OpenDart/KIS consolidated), VS-02_DATA_GOVERNANCE_POLICY.md, VS-03/04 SLICE_SPECs. All 4 unknowns resolved. Phase 2 implementation ready (Workstreams G/H/I)."
|
||||
AEG-X-009,S1,Cross,Source catalog 고도화,COMPLETED,2026-08-07,"docs/CURRENT/CATALOGS/source-catalog.md; docs/CURRENT/AEG-X-009_AUTOMATION_PROPOSAL.md; contracts/data/source-approval.v1.proposed.json; docs/DECISIONS/ADR-DATA-001.md; db/migrations/0033_source_approval_contract.sql; db/migrations/0034_dataset_manifest_freeze_contract.sql; db/migrations/0033_market_data_import_logs.sql; src/KArtSell.Modules.ModelOperations/Infrastructure/DapperApprovedModelContextReader.cs",Data Governance,"✅ Workstream D/E/F COMPLETED: source-catalog.md v2.0 (KRX/OpenDart/KIS consolidated), VS-02_DATA_GOVERNANCE_POLICY.md, VS-03/04 SLICE_SPECs. All 4 unknowns resolved. ✅ Workstream G (commit 136665c, 2026-08-07) also now COMPLETE: live KRX OpenAPI / OpenDart / KIS service integrations (P1-P3), daily scheduling + error classification + SLA tracking + LKG fallback (P4-P6), market_data schema with append-only import logs, correlation_id-based idempotent replay. ⚠️ Note: 0033 is used by two different, unrelated migrations across branches (source_approval_contract.sql vs market_data_import_logs.sql) — confirm actual applied migration number in the target DB's kartsell_schema_versions journal before assuming both landed as authored."
|
||||
AEG-VS-01-01,S1,VS-01,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-07,docs/CURRENT/SLICE_SPECS/VS-01-SLICE_SPEC.md,PM/Architect,"✅ SLICE_SPEC produced: VS-01-SLICE_SPEC.md (identity/MFA/RBAC/maker-checker contract). Prerequisite AEG-X-001 + AEG-VS-00-02 already COMPLETED. Ready for security team review and schema implementation."
|
||||
AEG-VS-02-01,S1,VS-02,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-02-SLICE_SPEC.md; docs/CURRENT/VS-02_DATA_GOVERNANCE_POLICY.md",PM/Architect,"✅ COMPLETE: VS-02-SLICE_SPEC.md + governance policy. All 4 unknowns resolved (data source, import SLA, audit policy, schema versioning). Financial security master implementation ready for Phase 2."
|
||||
AEG-VS-03-01,S2,VS-03,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on AEG-VS-02-01. Future sprint."
|
||||
AEG-VS-04-01,S2,VS-04,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on AEG-VS-03-01. Future sprint."
|
||||
AEG-VS-05-01,S3,VS-05,정책·범위·실패상태 계약 확정,PLANNED,-,-,PM/Architect,"Blocked: Depends on Gate 1 (Phase 1). Waiting for Job 976 (~50-90 days)."
|
||||
AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm changes require Golden data",Quant/QA,"Gate 2 prerequisite. Blocked by Phase 1 (Job 976) completion."
|
||||
AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-10-01,S4,VS-10,GenerateSellDecision,BLOCKED,TBD,"CLAUDE.md: Model must pass PBO/DSR validation",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; evidence/AEG-X-004/production-readonly-preflight-20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Read-only preflight: active DbUp journal public.kartsell_schema_versions contains 0032 and check_status includes Queued. Capabilities remain order/KIS/client publication OFF. Server-side dataset_manifest, model_version_registry, evidence_snapshot, and release_evidence_bundle contain no approved/frozen rows; no RunId/JobId/enqueue created. Blocked pending approved server-side VersionSet."
|
||||
AEG-VS-26-01,S2,VS-26,모델 승인 워크플로우 구현 (Maker-Checker Governance),BLOCKED,-,"docs/CURRENT/SLICE_SPECS/VS-26-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/Features/ApprovalWorkflow/ (Handlers.cs, Sql.cs, Policy.cs, Endpoints.cs — sole implementation, wired in Program.cs); db/migrations/0036_approval_workflow.sql; tests/KArtSell.Integration.Tests/ApprovalWorkflow/ApprovalWorkflowTests.cs (12 cases); tests/KArtSell.Integration.Tests/ApprovalWorkflowPolicyTests.cs (10 cases); TECH_DEBT_REGISTER.md DEBT-017/DEBT-023/DEBT-025/DEBT-026; commit a2e742c (original duplication, superseded)",PM/BE Lead,"🟡 UPDATED 2026-08-08 (second pass, BE priority work) — DEBT-025 and DEBT-026 (both discovered during the DEBT-017 cleanup earlier the same day) are now also code-complete: added `ProposeForReviewHandler` + `POST /approvals/{id}/propose` (wires the previously-dead `ApprovalWorkflowPolicy.CanProposeForReview`, so a proposal created via `POST /approvals` can now reach Approved/Active through the HTTP API end-to-end — this was the higher-impact gap, DEBT-026), and `GetApprovalByIdEndpoint` (`GET /approvals/{id}`) + `ApprovalWorkflowSql.GetEvidenceForProposalAsync` so approval evidence is readable via HTTP (DEBT-025). 4 new tests added (12 total in this file). `dotnet build KArtSell.sln -c Release` clean (0 warnings/0 errors). **Still BLOCKED, not COMPLETED: no PostgreSQL reachable in this session (127.0.0.1:5432 connection refused, no SSH tunnel open) — `dotnet test --filter FullyQualifiedName~ApprovalWorkflowTests -c Release` run 2026-08-08, all 17 matched tests fail with connection-refused (includes an unrelated top-level ApprovalWorkflowTests.cs the substring filter also matches). None of the 12 tests in this file have been confirmed to pass against a live database.** Do not mark COMPLETED until that run happens against a reachable Postgres and actually passes. Renumbered from VS-03 to VS-26 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-03 in WBS_MASTER.csv ('IngestMarketDataPIT') remains a separate, unrelated, still-unimplemented slice. Also still open: `src/KArtSell.Host/Features/MarketData/VS03_*.cs` is a THIRD, unrelated, already-implemented-and-tested body of work also labeled 'VS-03' that is entirely absent from this tracker — see CURRENT_ROADMAP.md. 2026-08-09 (BE priority pass, third pass): DEBT-028 fixed — `ActivateModelHandler` had no endpoint at all (an Approved proposal could never reach Active) and, if wired naively, would have overwritten the checker's approved_by/approval_notes with the activating SRE's identity and never populated activated_by/activated_at; added `POST /approvals/{id}/activate` + a dedicated `ActivateProposalAsync` that only touches activation columns, plus a regression test. DEBT-029 discovered (not fixed — genuine cross-cutting scope, needs its own session): `LogAuditEventCommandHandler` is never called by ApprovalWorkflow/TradeExecution/SellDecision/PortfolioReconciliation, so the compliance audit trail (VS-27) is empty in production regardless of activity — VS-27's 'COMPLETED' status only reflects `AuditSql` being directly tested, not that anything actually calls it. See TECH_DEBT_REGISTER.md DEBT-029 for a suggested Outbox-consumer-based fix approach. `dotnet build -c Release` clean; DB-unverified."
|
||||
AEG-VS-27-01,S2,VS-27,불변 감사 추적 구현 (Audit Trail / GDPR),BLOCKED,-,"docs/CURRENT/SLICE_SPECS/VS-27-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/Compliance/ (AuditSql.cs, GdprRetention.cs); tests/KArtSell.Integration.Tests/Compliance/AuditTrailTests.cs; commit 97444c9 (Workstream I, PR #24, merged to main)",PM/BE Lead,"✅ Backend implementation + tests complete: append-only audit_events table, GDPR retention tracking + redaction (actor_email→'<redacted>', customer_id→'<purged>'), PII fields (ip_address). 5/5 tests PASS run in isolation (2026-08-07). Also fixed same day (fix/dapper-underscore-mapping-and-build branch): ip_address (inet) and kis_response-style jsonb columns threw InvalidCastException when read through Dapper into a typed class; GdprRetention.RetentionEndsAt was declared DateTime against a DATE column, same failure mode; and a process-wide Dapper snake_case-mapping race condition (KArtSell.BuildingBlocks' [ModuleInitializer] only fires once that assembly loads — AuditSql doesn't reliably touch it) intermittently nulled out every column read from this table depending on unrelated test/host startup order. None of this had ever been exercised against a live database before. ⚠️ Frontend UI built 2026-08-08 (frontend/src/features/audit-trail/, route /compliance/audit-trail, pnpm typecheck/build clean, 19 new tests passing) but on an isolated worktree branch (worktree-agent-a2cc5afe46a7e16b1) not yet merged into this branch — deprioritized behind BE work per user direction 2026-08-08; also flagged DEBT-025 (Compliance endpoints AllowAnonymous with no real RBAC, and no PermissionGuard component exists in this repo despite CLAUDE.md listing it as always-shared) and DEBT-026 (~130 stray committed .js files regenerate on every pnpm build) — note both DEBT-025 numbers collide with a different DEBT-025 added the same day on this branch (GET /approvals/{id}); renumber on merge. Renumbered from VS-04 to VS-27 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-04 in WBS_MASTER.csv ('ApplyCorporateActions') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 🔴 DOWNGRADED 2026-08-09 from COMPLETED to BLOCKED: DEBT-029 found that `LogAuditEventCommandHandler` (this slice's intended write path) is never called by ApprovalWorkflow/TradeExecution/SellDecision/PortfolioReconciliation — nothing in the running application populates `compliance.audit_events` outside of this file's own direct-`AuditSql` tests. 'Backend implementation + tests complete' above is true only for `AuditSql` in isolation, not for the audit trail actually existing in production. See TECH_DEBT_REGISTER.md DEBT-029 for the suggested fix (an Outbox-consumer hooked to each slice's existing state-change events, rather than direct calls threaded into every handler)."
|
||||
AEG-VS-05-01,S3,VS-05,정책·범위·실패상태 계약 확정,BLOCKED,-,docs/CURRENT/AEG-VS-05-01_BLOCKER.md,PM/Architect,"2026-08-08: blocked before implementation. WBS defines IngestFundamentalsPIT (REQ-FND-001/DAT-05/MIG-FND-001/002/J04/UI-FND-01/T-FND-001), but existing VS-05 architecture/data contracts define unrelated Risk Metrics. Declared VS-02 dependency is not concretely evidenced beyond AEG-VS-02-01. See blocker record for required PM/Architect decision and Gate G1 evidence. No build/test/migration claimed."
|
||||
AEG-VS-06-01,S3,VS-06,정책·범위·실패상태 계약 확정,BLOCKED,-,docs/CURRENT/AEG-VS-06-01_BLOCKER.md,PM/Architect,"2026-08-08: blocked before implementation. WBS defines MaintainFeeTaxFxSchedule (REQ-COST-001/COST-01/02/MIG-COST-001/J04C/UI-COST-01/T-COST-001), but existing VS-06 architecture/data contracts define unrelated Stress Testing (STRESS-* / migration 0035). Declared VS-02 dependency is not concrete enough for Gate G1. See blocker record for required PM/Architect and Compliance/Owner decisions. No build/test/migration claimed."
|
||||
AEG-X-038,S3,Cross,Reconfirm Fee/Tax/FX valid-time schedule decisions,BLOCKED,-,docs/CURRENT/AEG-X-038_BLOCKER.md,Ops/Tax,"2026-08-08: source-gap audit completed. AEG-X-009 dependency is complete, but no approved source authority, decision log, data contract, or temporal/preference rules were found. Legacy references include persisted execution Commission and an unapproved 0.001m fee default; neither is a valid-time schedule. REQ-COST-001/T-COST-PIT cannot be safely implemented until Ops/Tax and Compliance/Owner approve the decisions in the blocker record. No build/test/migration claimed."
|
||||
AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm changes require Golden data",Quant/QA,"Gate 2 prerequisite. Blocked by Phase 1 completion. Corrected 2026-08-07: Phase 1 (Job 893/976) has not been started, not 'in progress' — see PHASE-1-SHADOW-RUN row. No countdown is currently running."
|
||||
AEG-VS-09-01,S4,VS-09,BuildEvidenceSnapshot,BLOCKED,TBD,"CLAUDE.md: Evidence requires Phase 1 results",PM/Architect,"Gate 2 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice."
|
||||
AEG-VS-10-01,S4,VS-10,매도 결정 엔진 구현 (GenerateSellDecision),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-10-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/SellDecision/ (SellDecisionEndpoints.cs, SellDecisionHandler.cs, SellDecisionSql.cs, SellPriorityRanker.cs); frontend/src/features/sell-decision/; tests/KArtSell.Integration.Tests/SellDecision/SellDecisionTests.cs; commit b1e38ac (Phase 3 J, PR #28, merged to main)",BE Lead/Quant Lead,"✅ Implementation complete (code + BE + FE + tests), matches WBS_MASTER's VS-10='GenerateSellDecision' definition (no ID collision here). Sell priority ranking (HARD_IMPAIRMENT→...→REENTRY_OPTION) with age/liquidity score boosts per VS-10-SLICE_SPEC.md. 32/32 tests PASS run in isolation (2026-08-07); one test (CalculateScore_HardImpairment_ReturnsLowestScore) had a wrong input value that happened to not exercise the >365-day age-boost branch the spec defines — fixed as a test bug, not a product bug (see fix/dapper-underscore-mapping-and-build branch). ⚠️ NOT validated: this row was previously (incorrectly) marked BLOCKED with reasoning 'Model must pass PBO/DSR validation' — that Gate-3/production-readiness validation genuinely still requires real Phase 1 shadow-run data and has not happened. Distinguish 'code implemented and unit/integration-tested' (done) from 'PBO/DSR-validated against real market data' (not done, blocked on Phase 1)."
|
||||
AEG-VS-19-01,S5,VS-19,RunFrozenBacktest,BLOCKED,TBD,"CLAUDE.md: Requires evidence from Phase 1-4",PM/Architect,"Gate 3 prerequisite. Blocked by Phase 1, which has not been started (confirmed 2026-08-07). No src/ implementation exists for this slice."
|
||||
AEG-VS-28-01,S2,VS-28,"거래 실행 시스템 구현 (Trade Execution, KIS Integration)",COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-28-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/TradeExecution/ (TradeEndpoints.cs, TradeHandlers.cs, TradeSql.cs, Trade.cs, KisTradeExecutionService.cs); db/migrations/0039_trades.sql; tests/KArtSell.Integration.Tests/TradeExecution/TradeExecutionTests.cs; commit b1e38ac (Phase 3 K, PR #28, merged to main)",BE Lead/Trading Ops,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Trade state machine (Pending→Submitted→Accepted→PartiallyFilled/FullyFilled→Confirmed→Reconciled), KIS order submission/poll/settlement. 13/13 tests PASS run in isolation (2026-08-07), but only after two real bugs were fixed on fix/dapper-underscore-mapping-and-build: (1) UpdateTradeStatusAsync only ever persisted status/kis_response/error_message and silently dropped kis_order_id, executed_quantity, unit_price, commission, net_proceeds and both timestamps on every single call since the slice merged — trade fills and settlements were not actually being recorded; (2) the same Dapper snake_case-mapping race condition described in AEG-VS-27-01's notes. ⚠️ Frontend UI built 2026-08-09 (frontend/src/features/trade-execution/, route /ops/trade-execution, pnpm typecheck/build clean, 13 new tests passing) after an earlier attempt failed on the session spend limit and was resumed — on isolated worktree branch worktree-agent-aae90f132a2daf359 (HEAD predates the VS-12→VS-28 renumbering, so that worktree's own tracker row is still AEG-VS-12-01), not yet merged into this branch. Found DEBT-025 there too: TradeEndpoints.cs is AllowAnonymous() with no Roles()/Policies() at all (unlike SellDecisionEndpoints.cs); collides with two other independently-numbered DEBT-025 entries on other unmerged branches — renumber on merge. Renumbered from VS-12 to VS-28 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-12 in WBS_MASTER.csv ('RankBuyCandidates') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 2026-08-08 (BE priority pass): DEBT-018 (outbox write not co-transactional with the trade status update) fixed — see TECH_DEBT_REGISTER.md; `dotnet build -c Release` clean, DB-backed tests still unverified (no reachable Postgres this session). 2026-08-09: DEBT-027 fixed — PollTradeStatusHandler/ConfirmSettlementHandler were registered in DI but never invoked by anything (no endpoint, no job); added src/KArtSell.Host/Jobs/TradeStatusPollingJob.cs as a Hangfire recurring job so submitted trades actually progress to Confirmed. `dotnet build -c Release` clean; no dedicated test added (see TECH_DEBT_REGISTER.md for why) and not run against a live database/KIS."
|
||||
AEG-VS-29-01,S2,VS-29,포트폴리오 대사 구현 (Portfolio Reconciliation),COMPLETED,2026-08-07,"docs/CURRENT/SLICE_SPECS/VS-29-SLICE_SPEC.md; src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/ (Endpoints.cs, ReconcileTradeHandler.cs, ReconciliationEngine.cs, ReconciliationSql.cs, MismatchDetector.cs, CostBasisCalculator.cs); tests/KArtSell.Integration.Tests/PortfolioReconciliation/ReconciliationEngineTests.cs; tests/KArtSell.ModelOperations.UnitTests/EvaluationReconciliationPlannerTests.cs; commit b1e38ac (Phase 3 L, PR #28, merged to main); commit 4059828 (fix: missing model_operations.models table breaking every fresh DB, PR #29, merged to main same day)",BE Lead,"New row — no prior tracker entry existed for this slice. ✅ Backend implementation + tests complete: Sell Decision → Trade → Holdings reconciliation, cost-basis calculation, mismatch detection. 18/18 tests PASS run in isolation (2026-08-07). Note this slice's own merge (PR #28) shipped with a missing model_operations.models table that broke every fresh-database migration; that was caught and fixed same day in PR #29 — a reminder that this branch's fresh-install DbUp path had not actually been rehearsed before merge. ⚠️ No frontend UI yet — an attempt was started 2026-08-08 but the background agent building it failed (hit the session's monthly spend limit) before producing any committed code; not resumed this session. Renumbered from VS-14 to VS-29 on 2026-08-08 per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md — VS-14 in WBS_MASTER.csv ('GenerateDailyRecommendations') was an unrelated, still-unimplemented slice and keeps its original number unchanged. 2026-08-08 (BE priority pass): DEBT-018 (outbox writes for TradeReconciled/ReconciliationMismatchAlert not co-transactional with the holding/log write) fixed — see TECH_DEBT_REGISTER.md; `dotnet build -c Release` clean, DB-backed tests still unverified (no reachable Postgres this session)."
|
||||
PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md; docs/CURRENT/AEG-X-004_DBUP_EVIDENCE.md; docs/CURRENT/PHASE-1_REQUEUE_READINESS.md; docs/CURRENT/PHASE-1_EXECUTION_EVIDENCE_PLAN.md; docs/CURRENT/PHASE-1_PREFLIGHT_20260806.md; docs/CURRENT/PHASE-1_PRODUCTION_PREFLIGHT_20260806.md; evidence/AEG-X-004/production-readonly-preflight-20260806.md; db/migrations/0032_shadow_run_queued_status_contract.sql; logs/phase-1-execution.log; logs/host-startup-20260804-173000.log",김재현/BE/SRE,"Read-only preflight: active DbUp journal public.kartsell_schema_versions contains 0032 and check_status includes Queued. Capabilities remain order/KIS/client publication OFF. Server-side dataset_manifest, model_version_registry, evidence_snapshot, and release_evidence_bundle contain no approved/frozen rows; no RunId/JobId/enqueue created. Blocked pending approved server-side VersionSet. Re-confirmed 2026-08-07: still no RunId/JobId exists anywhere in this workspace or its evidence trail; nothing changed on this row this session. Any future document that claims this row is RUNNING must cite a real RunId/JobId — do not restate the earlier (already-corrected) false claim."
|
||||
V13-FE-001,S0,Cross,UI Vendor import boundary,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-001_KBX_V36_DESIGN_HARNESS_PROPOSAL.md; tools/validate_v16.py",FE Architect/QA,"Dependency AEG-X-003 is COMPLETED. KBX v36 was translated as a non-vendor design-evidence harness: preserve the shared UI adapter boundary, keep feature direct PrimeVue/AG Grid imports at zero, and defer token/recipe implementation to separately approved slices. Actual evidence: python tools/validate_v16.py exited 0 with PASS=1 WARN=2 FAIL=0 on 2026-08-09. Warnings are retained (no full source archive; approved runtime evidence absent); no runtime test/build/migration claim is made."
|
||||
V13-FE-003,S0,Cross,UiAdapter Port 정의,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-003_UI_ADAPTER_PORT_RECONCILIATION.md; frontend/src/shared/ui/adapter/contracts.ts; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-001 is COMPLETED. Existing adapter v4 contract explicitly verifies 14 capabilities (stronger than the WBS minimum wording of 8) without feature vendor imports. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. KBX-derived components remain provider-neutral reimplementations only; no KBX package, contract, router, store, or permission host was imported."
|
||||
V13-FE-004,S0,Cross,PrimeVue/AG Grid Adapter 구현,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-004_ADAPTER_IMPLEMENTATION_RECONCILIATION.md; frontend/src/shared/ui/adapter/primevue; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-003 is COMPLETED. PrimeVue/AG Grid remain confined behind adapter v4. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. This is contract/accessibility-attribute evidence only; no visual/AT/runtime claim is made."
|
||||
V13-FE-005,S0,Cross,Ks* vendor-neutral components,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-005_KBX_FORM_COMPONENT_ADOPTION.md; frontend/src/shared/ui/components/KsFormGrid.vue; frontend/src/shared/ui/components/KsFormSection.vue; frontend/src/shared/ui/components/KsFormSpan.vue; frontend/src/shared/ui/components/KsValidationSummary.vue; frontend/src/shared/ui/components/tests/KsFormLayouts.spec.ts",FE Architect/QA,"Dependency V13-FE-004 is COMPLETED. Reimplemented only KBX presentation-only form components against existing K-ArtSell tokens; no KBX package, contracts, provider dependency, routing, permissions, or business policy imported. Actual evidence: targeted Vitest 2 files / 6 tests passed and pnpm typecheck exit 0 on 2026-08-09. Visual/AT/performance baseline remains outside this Slice."
|
||||
V13-FE-006,S0,Cross,AppShell/Page layouts,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md; frontend/src/shared/ui/layouts/tests/layout.contract.spec.ts; frontend/src/shared/ui/layouts/AppShellLayout.vue; frontend/src/shared/ui/layouts/PageLayout.vue",UX/FE/QA/Security,"Dependency V13-FE-005 is COMPLETED. Preserved the existing slot-based layout rather than importing KBX's coupled workspace shell. Actual DOM contract evidence: 1 file / 2 tests passed, exit 0, 2026-08-09; verifies skip navigation, structural landmarks, default automation OFF boundary, and evidence/aside/footer separation. No visual/AT/E2E claim is made."
|
||||
V13-FE-011,S6,Cross,T01 검색목록 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-011_T01_SEARCH_LIST_LAYOUT_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue; frontend/src/shared/ui/screen-types/tests/SearchListCrudPage.spec.ts; frontend/src/shared/shell/tests/navigationCatalog.spec.ts; evidence/V13-FE-011/t01-search-list-layout_20260809.log",UX/FE,"2026-08-09: Scope limited to adapter-neutral T01 composition: list body plus optional detail region through CrudWorkspaceLayout and read-only component catalogue visibility in the Design System menu; WBS workspace remains hidden. Actual execution evidence: targeted Vitest 2 files / 3 tests passed; pnpm typecheck passed. Dependency V13-FE-006 is completed. MVP-A Gate passage, visual/assistive-technology approval, and Playwright evidence are not claimed."
|
||||
|
||||
|
@@ -0,0 +1,71 @@
|
||||
# Phase 1 Shadow Run — VersionSet approval checklist
|
||||
|
||||
**Purpose:** `PHASE-1-SHADOW-RUN` (WBS tracker row, `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`)
|
||||
is blocked because `evaluation.dataset_manifest`, `governance.model_version_registry`,
|
||||
`signal_engine.evidence_snapshot`, and `governance.release_evidence_bundle` contain no
|
||||
approved/frozen rows. Nothing can be RunId/JobId-enqueued until a human approves a VersionSet.
|
||||
|
||||
**This document is a checklist, not an approval.** It exists so the approval step is fast and
|
||||
auditable once a real human decides to do it. It does not grant approval, and no automation in
|
||||
this repository should insert rows into these tables on a schedule or "because the checklist
|
||||
passed" — every `approved_by`/`checker_id` value below must be a real person who reviewed the
|
||||
evidence, per CLAUDE.md's maker-checker principle and AGENTS.md's guardrail against fabricated
|
||||
governance records.
|
||||
|
||||
## Why this can't be scripted away
|
||||
|
||||
Each table below enforces maker-checker at the database level (see the CHECK constraints in
|
||||
`db/migrations/0016_continuous_model_operations.sql`, `0017_execution_assurance.sql`,
|
||||
`0034_dataset_manifest_freeze_contract.sql`):
|
||||
|
||||
- `dataset_manifest`: `FROZEN` requires `approved_by`, `approved_at`, `frozen_at` all set, and the
|
||||
table is append-only (trigger blocks UPDATE/DELETE — corrections are new rows).
|
||||
- `model_version_registry`: `lifecycle_state = 'APPROVED'` requires `approved_by` + `approved_at`.
|
||||
- `release_evidence_bundle`: `status IN ('APPROVED','REJECTED')` requires `checker_id IS NOT NULL`,
|
||||
`checker_id <> maker_id` (the checker cannot be the maker), and `decided_at`.
|
||||
|
||||
There is no code path that satisfies these constraints without a named maker and a *different*
|
||||
named checker actually deciding. That is intentional — do not add one.
|
||||
|
||||
## Checklist (walk in order)
|
||||
|
||||
1. **Dataset manifest exists and is content-addressed.**
|
||||
- [ ] A `dataset_manifest` row exists for the target `scope_key` with `content_hash` computed
|
||||
from the actual dataset (not a placeholder).
|
||||
- [ ] `lineage_hash` traces back to real source ingestion (KRX/OpenDart/KIS via
|
||||
`src/KArtSell.Modules.ModelOperations/Infrastructure/`), not synthetic/test data.
|
||||
- [ ] A maker sets `status = 'PROPOSED'`.
|
||||
- [ ] A **different** person (checker) reviews the dataset and, if acceptable, updates status to
|
||||
`APPROVED` and later `FROZEN` (setting `approved_by`, `approved_at`, `frozen_at`).
|
||||
|
||||
2. **Model version registered.**
|
||||
- [ ] `model_version_registry` row exists for the model/scope with `code_sha` matching the exact
|
||||
commit that will run, `model_card_hash` matching a real ModelCard document.
|
||||
- [ ] `lifecycle_state` progressed through `RESEARCH → CHALLENGER → SHADOW → CANDIDATE` with
|
||||
real review at each step (not skipped).
|
||||
- [ ] Checker sets `lifecycle_state = 'APPROVED'` with `approved_by`/`approved_at`.
|
||||
|
||||
3. **Evidence snapshot frozen.**
|
||||
- [ ] `signal_engine.evidence_snapshot` row references the approved `dataset_id` and
|
||||
`model_version` above, with `content_hash` computed from the actual frozen payload.
|
||||
|
||||
4. **Release evidence bundle.**
|
||||
- [ ] `release_evidence_bundle` row aggregates build/test/migration/security/rollback artifact
|
||||
hashes for the exact code that will run.
|
||||
- [ ] Maker (`maker_id`) creates it in `DRAFT`/`REVIEW_REQUIRED`.
|
||||
- [ ] A different checker (`checker_id <> maker_id`) reviews and sets `status = 'APPROVED'`,
|
||||
`decided_at`.
|
||||
|
||||
5. **Only after all four are real and APPROVED/FROZEN:**
|
||||
- [ ] Reconcile `shadow_run.check_status` constraint (already done — see
|
||||
`db/migrations/0032_shadow_run_queued_status_contract.sql`, `AEG-X-004`).
|
||||
- [ ] Call `POST /api/shadow-runs` referencing the approved VersionSet.
|
||||
- [ ] Record the returned RunId/JobId in `docs/CURRENT/PHASE-1_SHADOW_RUN_STATUS_CORRECTION.md`
|
||||
and update `WBS_PROGRESS_TRACKER.csv`'s `PHASE-1-SHADOW-RUN` row to `RUNNING` — only with that
|
||||
real RunId/JobId cited as evidence.
|
||||
|
||||
## SQL template
|
||||
|
||||
`scripts/phase1/template-approve-versionset.sql` has the parameterized statements for steps 1-4,
|
||||
with every value that must be a real human decision left as an explicit placeholder. It is a
|
||||
template to hand-fill and run interactively (e.g. via `psql`), not a script to execute as-is.
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
# VS-03: Model Approval Workflow (Maker-Checker Governance)
|
||||
# VS-26: Model Approval Workflow (Maker-Checker Governance)
|
||||
|
||||
**Vertical Slice:** VS-03 (Model Approval & Activation Gateway)
|
||||
**Vertical Slice:** VS-26 (Model Approval & Activation Gateway)
|
||||
**Version:** 1.0 COMPLETE
|
||||
**Date:** 2026-08-07
|
||||
**Owner:** Platform Lead + Compliance
|
||||
@@ -228,11 +228,11 @@ CREATE TABLE model_operations.approval_events (
|
||||
|
||||
- **VS-00:** PIT envelope (published_at, correlation_id, revision)
|
||||
- **VS-02:** Financial security master (governance foundation)
|
||||
- **VS-04:** Audit trail (event logging)
|
||||
- **VS-27:** Audit trail (event logging)
|
||||
- **VS-10:** Sell decision (uses approved models)
|
||||
|
||||
---
|
||||
|
||||
**Co-Authored-By:** Claude Haiku 4.5 <noreply@anthropic.com>
|
||||
**Status:** ✅ READY FOR IMPLEMENTATION
|
||||
**Next:** VS-04 (audit trail), then Phase 2 implementation
|
||||
**Next:** VS-27 (audit trail), then Phase 2 implementation
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
# VS-04: Immutable Audit Trail (GDPR/Compliance)
|
||||
# VS-27: Immutable Audit Trail (GDPR/Compliance)
|
||||
|
||||
**Vertical Slice:** VS-04 (Audit Log & Compliance Trail)
|
||||
**Vertical Slice:** VS-27 (Audit Log & Compliance Trail)
|
||||
**Version:** 1.0 COMPLETE
|
||||
**Date:** 2026-08-07
|
||||
**Owner:** Compliance + Security
|
||||
@@ -245,7 +245,7 @@ WHERE entity_id IN (SELECT id FROM ... WHERE customer_id = $1);
|
||||
|
||||
- **VS-00:** PIT envelope (published_at, correlation_id, revision)
|
||||
- **VS-02:** Governance foundation (data sources, policies)
|
||||
- **VS-03:** Approval workflow (events logged by VS-04)
|
||||
- **VS-26:** Approval workflow (events logged by VS-27)
|
||||
- **Compliance:** GDPR, FSS, PCI-DSS requirements
|
||||
|
||||
---
|
||||
+13
-13
@@ -1,11 +1,11 @@
|
||||
# VS-12: Trade Execution System (KIS Integration)
|
||||
# VS-28: Trade Execution System (KIS Integration)
|
||||
|
||||
**Vertical Slice:** VS-12 (Trade Execution)
|
||||
**Vertical Slice:** VS-28 (Trade Execution)
|
||||
**Version:** 1.0 COMPLETE
|
||||
**Date:** 2026-08-07
|
||||
**Owner:** Backend Lead + Trading Ops
|
||||
**Status:** ✅ READY FOR IMPLEMENTATION
|
||||
**Depends On:** VS-10 (sell decisions), VS-03 (approval), VS-04 (audit)
|
||||
**Depends On:** VS-10 (sell decisions), VS-26 (approval), VS-27 (audit)
|
||||
|
||||
---
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
**So that** portfolios are rebalanced automatically with full audit trail
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- ✅ Execute trade only after VS-03 approval
|
||||
- ✅ Execute trade only after VS-26 approval
|
||||
- ✅ Submit order to KIS, track order status
|
||||
- ✅ Handle partial fills and slippage
|
||||
- ✅ Confirm settlement and update cost basis
|
||||
- ✅ Classify errors (transient/permanent/liquidity)
|
||||
- ✅ All state changes logged (VS-04 audit)
|
||||
- ✅ All state changes logged (VS-27 audit)
|
||||
|
||||
---
|
||||
|
||||
@@ -47,7 +47,7 @@ PARTIAL_FILLED / FULLY_FILLED (execution progress)
|
||||
↓
|
||||
CONFIRMED (settlement confirmed)
|
||||
↓
|
||||
RECONCILED (cost basis updated by VS-14)
|
||||
RECONCILED (cost basis updated by VS-29)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -87,7 +87,7 @@ CREATE INDEX idx_trades_correlation_id ON trades(correlation_id);
|
||||
|
||||
### POST /trades (Create Trade)
|
||||
|
||||
**Role:** System (after VS-03 approval)
|
||||
**Role:** System (after VS-26 approval)
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
@@ -164,12 +164,12 @@ ConfirmSettlementAsync(kisOrderId, correlationId)
|
||||
- Wait 1 business day after FILLED
|
||||
- Confirm settlement with KIS
|
||||
- Update status=CONFIRMED
|
||||
- Emit event to VS-14 (reconciliation)
|
||||
- Emit event to VS-29 (reconciliation)
|
||||
|
||||
### ReconcileTradeHandler
|
||||
- Receive settlement event
|
||||
- Update status=RECONCILED
|
||||
- Mark ready for VS-14 processing
|
||||
- Mark ready for VS-29 processing
|
||||
|
||||
---
|
||||
|
||||
@@ -204,7 +204,7 @@ ConfirmSettlementAsync(kisOrderId, correlationId)
|
||||
- Liquidity: Partial fills, slippage
|
||||
|
||||
**RBAC:**
|
||||
- System role: Submit trades (via VS-03 approval)
|
||||
- System role: Submit trades (via VS-26 approval)
|
||||
- Operations: View & monitor execution
|
||||
- Audit: Query immutable trail
|
||||
|
||||
@@ -213,9 +213,9 @@ ConfirmSettlementAsync(kisOrderId, correlationId)
|
||||
## 📋 Related Specifications
|
||||
|
||||
- **VS-10:** Sell Decision (generates trades)
|
||||
- **VS-03:** Approval Workflow (prerequisite)
|
||||
- **VS-04:** Audit Trail (logs all state changes)
|
||||
- **VS-14:** Portfolio Reconciliation (consumes trade settlement)
|
||||
- **VS-26:** Approval Workflow (prerequisite)
|
||||
- **VS-27:** Audit Trail (logs all state changes)
|
||||
- **VS-29:** Portfolio Reconciliation (consumes trade settlement)
|
||||
|
||||
---
|
||||
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
# VS-14: Portfolio Reconciliation (Sell Decision → Trade → Holdings)
|
||||
# VS-29: Portfolio Reconciliation (Sell Decision → Trade → Holdings)
|
||||
|
||||
**Vertical Slice:** VS-14 (Portfolio Reconciliation)
|
||||
**Vertical Slice:** VS-29 (Portfolio Reconciliation)
|
||||
**Version:** 1.0 COMPLETE
|
||||
**Date:** 2026-08-07
|
||||
**Owner:** Data Architecture + Finance
|
||||
**Status:** ✅ READY FOR IMPLEMENTATION
|
||||
**Depends On:** K (trade execution), uses VS-03 (approval) + VS-04 (audit)
|
||||
**Depends On:** K (trade execution), uses VS-26 (approval) + VS-27 (audit)
|
||||
|
||||
---
|
||||
|
||||
@@ -38,11 +38,11 @@
|
||||
## 📊 Reconciliation Flow
|
||||
|
||||
```
|
||||
Trade Executed (from VS-12)
|
||||
Trade Executed (from VS-28)
|
||||
↓
|
||||
Extract trade details: quantity, price, settlement date
|
||||
↓
|
||||
Validate against approval (from VS-03)
|
||||
Validate against approval (from VS-26)
|
||||
↓
|
||||
Update holdings: quantity ± executed
|
||||
↓
|
||||
@@ -237,9 +237,9 @@ CREATE TABLE lots (
|
||||
|
||||
## 📋 Related Specifications
|
||||
|
||||
- **VS-03:** Approval workflow (approval_proposals, evidence linkage)
|
||||
- **VS-04:** Audit trail (reconciliation events logged)
|
||||
- **K (VS-12):** Trade execution (provides trade_id, quantity, price)
|
||||
- **VS-26:** Approval workflow (approval_proposals, evidence linkage)
|
||||
- **VS-27:** Audit trail (reconciliation events logged)
|
||||
- **K (VS-28):** Trade execution (provides trade_id, quantity, price)
|
||||
- **VS-00:** PIT envelope (published_at, correlation_id, revision)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,88 @@
|
||||
# V13-FE-001 — KBX v36 디자인 하네스 제안
|
||||
|
||||
## 작업 식별
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| WBS | V13-FE-001 |
|
||||
| Slice | Cross-cutting / UIFoundation |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-ADAPTER / Cross / Cross / Cross / UI-FOUND-01 / T-FE-ARCH-01 |
|
||||
| Gate / 의존성 | G0 / AEG-X-003 (COMPLETED, tracker 확인) |
|
||||
| 단일 목적 | UI 공급자 경계를 유지한 채 KBX v36의 디자인 운영 원칙을 K-ArtSell 검증 계약으로 번역 |
|
||||
| Artifact / 수용 기준 | architecture rule / feature의 PrimeVue·AG Grid 직접 import 0 |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
### Source
|
||||
|
||||
- `docs/Design/kbx-foundation-v36/docs/design-token-policy-v4.md`: primitive → semantic → component 토큰, 밀도는 배치·form·keyboard 계약을 바꾸지 않음.
|
||||
- `docs/Design/kbx-foundation-v36/docs/kbx-v36-standard-traceability.md`: template → recipe → canonical scenario → Vitest/Playwright 증거의 연결.
|
||||
- `docs/Design/kbx-foundation-v36/docs/screen-recipe-verification-home-attention-v36.md`: `testProfile`은 UX/복구/보안 검증 범위이며, client business truth가 아님.
|
||||
- `frontend/src/shared/ui/adapter/`, `tools/validate_v16.py`: 현재 UI provider/adapter 경계와 정적 vendor-import 검사.
|
||||
- `frontend/src/shared/ui/screen-types/catalogue.ts`, `contracts/ui/screen-types.v2.json`: T01~T10 화면 유형, 상태·증거·anti-pattern 계약.
|
||||
- `frontend/src/design-system/tokens.css`: 현재 primitive와 일부 semantic/component 토큰.
|
||||
|
||||
### Assumption
|
||||
|
||||
- KBX v36 디렉터리는 사용자 제공 참조이며, 그 코드·패키지·브랜드 토큰을 vendor/copy하지 않는다.
|
||||
- K-ArtSell의 T01~T10 의미와 현 UI adapter v4 계약은 유지한다.
|
||||
- 토큰 값 자체, density 기본값, 화면별 canonical scenario는 승인된 디자인/QA 기준이 없는 한 변경하지 않는다.
|
||||
|
||||
### Unknown
|
||||
|
||||
- 승인된 visual baseline, 디자인 소유자, target density, 화면별 keyboard/AT acceptance matrix가 없다.
|
||||
- 31개 대상 파일에서 353개의 색상·spacing literal 후보가 발견됐지만, provider CSS와 의도된 시각화 scale을 제외하는 승인된 baseline은 없다.
|
||||
- 실제 각 템플릿을 대표하는 Playwright scenario와 CI artifact retention 정책이 아직 계약화되지 않았다.
|
||||
|
||||
### Decision Required
|
||||
|
||||
1. `V13-FE-002`에서 semantic/component token 명명과 literal baseline을 승인할 UX/FE owner.
|
||||
2. T01~T10에 `testProfile` 및 canonical scenario를 추가할 ADR/Issue와 QA owner.
|
||||
3. compact/comfortable/touch 중 제품별 기본 밀도와 visual regression baseline 보관 위치.
|
||||
|
||||
## 적용 하네스
|
||||
|
||||
KBX의 구현물을 가져오지 않고 다음 불변식을 K-ArtSell의 현 경계에 적용한다.
|
||||
|
||||
| KBX v36 원칙 | K-ArtSell 현재 기반 | 승인 후 하네스 |
|
||||
| --- | --- | --- |
|
||||
| Template가 UX를 책임 | `screenTemplateCatalogue`의 상태·증거·anti-pattern | 각 template에 UX/복구/보안만 담는 `testProfile`을 추가. 도메인 정책·임계값은 포함하지 않음. |
|
||||
| Recipe → canonical evidence | Vitest catalogue test, `AEG-V16-024` a11y evidence | template별 최소 대표 scenario와 실제 trace/screenshot/test log를 연결. 선언만으로 통과시키지 않음. |
|
||||
| 3계층 토큰 | `tokens.css`의 primitive 및 일부 역할/컴포넌트 token | 화면은 primitive가 아닌 semantic/component token만 소비. primitive 값 변경은 token 파일 한 곳으로 제한. |
|
||||
| Density는 학습 UX가 아님 | `[data-density='compact']`가 control/grid 높이만 변경 | comfortable/touch는 승인된 값과 state/keyboard characterization test가 있을 때에만 추가. |
|
||||
| Design debt ratchet | 현재 정적 validator 및 353 literal 후보 | 승인된 baseline 이후 새/증가 literal을 차단하고, 기존 부채는 감소만 허용. provider CSS/visualization scale은 명시적인 allowlist로 분리. |
|
||||
| Fail-closed provider boundary | `tools/validate_v16.py`의 adapter 외 vendor import 금지 | 화면/feature는 shared UI port만 사용. KBX로 바꾸는 것이 아니라 현재 adapter 경계를 검증 기반으로 보존. |
|
||||
|
||||
## 현재 디자인 진단과 개선 우선순위
|
||||
|
||||
2026-08-09 정적 후보 스캔(`*.vue`, `*.css`, `*.ts`, test 제외)은 31개 파일, 353개 literal을 반환했다. 이 값은 심미적 오류 수가 아니라 **승인 전 정규화 후보의 상한**이다.
|
||||
|
||||
| 우선순위 | 관측 근거 | 제안 Slice | 개선 방향 | 안전장치 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P0 | `RiskDashboard.vue` 104, `MarketDataIngestion.vue` 57, `RebalanceForm.vue` 57, `IngestionStatus.vue` 41 후보 | V13-FE-002 | 페이지의 색상·spacing·radius를 semantic/component token으로 치환 | 화면별 visual/state matrix와 금융 의미 색상 검토; gradient/차트 scale은 임의 통합 금지 |
|
||||
| P0 | 현재 `tokens.css`는 primitive와 역할 token이 한 계층에 섞여 있고 component 이름은 3개뿐 | V13-FE-002 | primitive/semantic/component를 CSS section과 이름으로 명시 분리 | 새 값·threshold·brand palette를 만들지 않고 기존 값만 alias로 이동 |
|
||||
| P1 | 카탈로그는 mandatory evidence가 있으나 required check/scenario/evidence type이 없음 | 후속 ADR 승인 Slice | `testProfile`을 TypeScript contract와 JSON contract에 동시 추가 | client business truth 금지; 실행 가능한 test가 없는 template은 complete 주장 금지 |
|
||||
| P1 | density는 compact만 존재 | V13-FE-002 이후 | density를 control/grid token에 한정하고 keyboard/focus 위치를 불변으로 test | layout/form 구조 변형 금지 |
|
||||
| P2 | PrimeVue/AG Grid는 adapter 안에만 존재 | V13-FE-001 완료 | provider 교체 대신 포트/contract test로 독립성 유지 | 새 provider, library, direct import 금지 |
|
||||
|
||||
## 리팩터링 순서
|
||||
|
||||
1. 이 Slice에서 vendor boundary의 실제 정적 증거를 보존한다.
|
||||
2. 승인 후 `V13-FE-002`에서 토큰 계약과 baseline을 먼저 확정한다. 화면 CSS를 그 전에 일괄 수정하지 않는다.
|
||||
3. 같은 Slice에서 P0 네 화면을 한 화면씩 behavior-preserving으로 token화하고, 각 화면의 loading/empty/warn/error/readonly 및 keyboard/visual evidence를 남긴다.
|
||||
4. 별도 승인 Slice에서 screen recipe `testProfile`과 대표 Playwright/Vitest evidence를 도입한다. 토큰 리팩터링과 섞지 않는다.
|
||||
5. baseline이 승인된 뒤에만 ratchet을 CI에 강제한다. 기존 353 후보를 0으로 보이게 만드는 일괄 ignore/regex 우회는 금지한다.
|
||||
|
||||
## 이번 Slice 실행 증거
|
||||
|
||||
| 검증 | 실제 결과 |
|
||||
| --- | --- |
|
||||
| `python tools/validate_v16.py` | `PASS=1 WARN=2 FAIL=0`, exit 0 (2026-08-09) |
|
||||
| Vendor boundary | validator가 `frontend/src`의 `.ts`/`.vue`에서 PrimeVue·AG Grid import를 검사하고 `shared/ui/adapter/primevue` 외 위치를 실패 처리 |
|
||||
| 범위 | 코드/토큰 값/화면 동작은 변경하지 않음. 사용자 제공 `docs/Design/kbx-foundation-v36/`는 추적·수정하지 않음. |
|
||||
|
||||
경고 2건은 full source archive 및 승인 런타임이 없다는 내용이며, .NET/DB/Playwright/Shadow 결과를 통과로 주장하지 않는다.
|
||||
|
||||
## 다음 게이트
|
||||
|
||||
`V13-FE-002`는 이제 WBS 의존성은 해소되지만, 위 Decision Required에 대한 ADR/Issue 및 visual/QA baseline이 없으므로 실행 전 승인 상태를 확인한다.
|
||||
@@ -0,0 +1,30 @@
|
||||
# V13-FE-003 — UI Adapter Port 재검증
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-ADAPTER / Cross / Cross / Cross / UI-FOUND-03 / T-FE-ADAPTER-01 |
|
||||
| Dependency / Gate | V13-FE-001 (COMPLETED) / G0 |
|
||||
| Artifact | `frontend/src/shared/ui/adapter/contracts.ts` |
|
||||
| Acceptance | provider ID/version과 feature-vendor-neutral port 계약 고정 |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `adapter/contracts.ts`는 v4.0에 button, field, dialog, feedback, navigation, grid를 포함한 14 capability를 명시한다. `uiAdapter.contract.spec.ts`는 native와 PrimeVue/AG Grid adapter를 같은 contract로 검증한다.
|
||||
- **Assumption:** WBS의 "8개 Port"는 최소 기준이며, 현재 승인된 v4.0의 14 capability를 축소하지 않는다.
|
||||
- **Unknown:** KBX의 고수준 layout/form component를 새 port로 승격할 필요성은 아직 검증되지 않았다.
|
||||
- **Decision Required:** FormGrid/ValidationSummary 이식 후 세 번 이상 provider-neutral 재사용이 확인될 때에만 별도 port 추가 ADR을 검토한다.
|
||||
|
||||
## KBX 차용 경계
|
||||
|
||||
- 허용: Vue/CSS만으로 독립된 form layout, validation presentation, state refresh 안내를 K-ArtSell token/port로 재구현.
|
||||
- 금지: `@kbx/contracts`, KBX 권한 host, router/store, PrimeVue peer dependency를 feature 또는 shared layout에 이식.
|
||||
- 기존 `QueryStateBoundary`는 13개 상태와 correlation/retry를 보유하므로 KBX state boundary를 복제하지 않고 필요한 refresh 표현만 별도 Slice에서 흡수한다.
|
||||
|
||||
## 실제 증거
|
||||
|
||||
`pnpm vitest run src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts src/shared/ui/tests/adapterCompatibility.spec.ts`
|
||||
|
||||
- 2 files / 4 tests passed
|
||||
- exit 0, 2026-08-09
|
||||
|
||||
이 결과는 adapter contract 검증에 한정된다. build, visual, AT, DB 또는 migration 통과를 주장하지 않는다.
|
||||
@@ -0,0 +1,23 @@
|
||||
# V13-FE-004 — Provider Adapter 구현 재검증
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-ADAPTER / Cross / Cross / Cross / UI-FOUND-04 / T-FE-ADAPTER-02 |
|
||||
| Dependency / Gate | V13-FE-003 (COMPLETED) / G0 |
|
||||
| Artifact | `frontend/src/shared/ui/adapter/primevue/*` |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** PrimeVue/AG Grid imports are confined to the approved adapter directory; `Ks*` components consume `UiAdapter` rather than provider components.
|
||||
- **Assumption:** the existing WCAG 2.2 AA target is the applicable baseline; this test run is contract-level evidence, not an assistive-technology audit.
|
||||
- **Unknown:** provider visual baseline and keyboard/AT matrix remain pending `AEG-V16-024` approval.
|
||||
- **Decision Required:** no new provider or provider-specific component is introduced by KBX reuse.
|
||||
|
||||
## Actual evidence
|
||||
|
||||
`pnpm vitest run src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts src/shared/ui/components/tests/accessibility.contract.spec.ts`
|
||||
|
||||
- 2 files / 4 tests passed
|
||||
- exit 0, 2026-08-09
|
||||
|
||||
No DB, migration, end-to-end, visual baseline, or screen-reader pass is claimed.
|
||||
@@ -0,0 +1,30 @@
|
||||
# V13-FE-005 — KBX 폼 컴포넌트 선별 이식
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-COMP / Cross / Cross / Cross / UI-FOUND-05 / T-FE-COMP-01 |
|
||||
| Dependency / Gate | V13-FE-004 (COMPLETED) / G0 |
|
||||
| Artifact | `frontend/src/shared/ui/components/KsForm*.vue`, `KsValidationSummary.vue` |
|
||||
| 목적 | provider-neutral form layout과 validation presentation 표준화 |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** KBX v36의 `KbxFormGrid`, `KbxFormSection`, `KbxFormSpan`, `KbxValidationSummary`는 Vue/CSS presentation-only 구성이다.
|
||||
- **Assumption:** K-ArtSell token (`--ks-*`)과 기존 `FieldShell`/Zod 경계를 유지하면 같은 layout pattern을 안전하게 재구현할 수 있다.
|
||||
- **Unknown:** 각 feature의 오류를 어느 field로 focus할지와 visual/AT baseline은 화면별 evidence가 필요하다.
|
||||
- **Decision Required:** 오류 클릭 시 field focus/navigation, density variant, 새로운 token은 실제 3회 이상 반복 및 ADR/QA 기준 승인 후에만 추가한다.
|
||||
|
||||
## 구현 경계
|
||||
|
||||
- `KsFormGrid`, `KsFormSection`, `KsFormSpan`, `KsValidationSummary`만 추가했다.
|
||||
- KBX package, `@kbx/contracts`, provider peer dependency, router, store, permission host, 업무 규칙을 복사하지 않았다.
|
||||
- validation summary는 오류를 숨기기 위한 임의 최대 개수를 두지 않고 전달받은 오류를 모두 표시한다.
|
||||
- 색상/spacing은 기존 semantic token만 사용한다.
|
||||
|
||||
## 실제 증거
|
||||
|
||||
- `pnpm vitest run src/shared/ui/components/tests/KsFormLayouts.spec.ts src/shared/ui/components/tests/accessibility.contract.spec.ts`: 2 files / 6 tests passed, exit 0.
|
||||
- `pnpm typecheck`: exit 0.
|
||||
- 실행일: 2026-08-09.
|
||||
|
||||
위 증거는 component contract/typecheck에 한정된다. browser visual, AT, performance, server/API/DB/migration 통과는 주장하지 않는다.
|
||||
@@ -0,0 +1,29 @@
|
||||
# V13-FE-006 — AppShell/Page Layout 계약 재검증
|
||||
|
||||
| 항목 | 값 |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-LAYOUT / Cross / Cross / Cross / UI-FOUND-06 / T-FE-LAYOUT-01 |
|
||||
| Dependency / Gate | V13-FE-005 (COMPLETED) / G0 |
|
||||
| Artifact | `frontend/src/shared/ui/layouts` |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** KBX `KbxApplicationShell`은 skip link, shell/workspace landmark 분리를 제공한다. 현재 `AppShellLayout`과 `PageLayout`은 동일한 구조적 요구를 더 작은 슬롯 기반 contract로 제공한다.
|
||||
- **Assumption:** K-ArtSell에 workspace tabs, persisted recents, runtime center를 새로 도입할 승인된 requirement가 없다.
|
||||
- **Unknown:** responsive visual baseline과 AT keyboard evidence는 `AEG-V16-024`의 QA 승인 대상이다.
|
||||
- **Decision Required:** KBX shell의 권한 필터·workspace store·runtime panel을 이식하지 않는다. 실제 product requirement와 ADR 없이 shell state를 확장하지 않는다.
|
||||
|
||||
## 이 Slice의 결과
|
||||
|
||||
- 동작을 바꾸지 않고 AppShell의 skip link, header/nav/main/footer landmark, automation boundary default와 PageLayout의 evidence/aside/footer 분리를 contract test로 고정했다.
|
||||
- 자동주문/KIS 제출/자동 모델승격 OFF 안내는 화면 레이아웃에서 보존된다.
|
||||
- 새 layout, provider, store, router, token 값은 추가하지 않았다.
|
||||
|
||||
## 실제 증거
|
||||
|
||||
`pnpm vitest run src/shared/ui/layouts/tests/layout.contract.spec.ts`
|
||||
|
||||
- 1 file / 2 tests passed
|
||||
- exit 0, 2026-08-09
|
||||
|
||||
이 증거는 DOM contract에 한정된다. responsive visual, AT, browser E2E, API/DB/migration 통과를 주장하지 않는다.
|
||||
@@ -0,0 +1,32 @@
|
||||
# V13-FE-011 — T01 Search/List layout composition
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Requirement / API / DB / Job / UI / Test | REQ-FE-T01 / Cross / Cross / Cross / UI-T01 / T-FE-T01-01 |
|
||||
| Dependency | V13-FE-006 — completed in `WBS_PROGRESS_TRACKER.csv` |
|
||||
| Gate | MVP-A — implementation evidence only; Gate passage is not claimed |
|
||||
| Artifact | `SearchListCrudPage.vue`; component contract test |
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `DEC-046` approves T01~T10 standard screen types. `FE_COMPONENT.csv` defines T01 as a list screen composed from shared layouts. `UiStandardPage.vue` is the current T01 consumer. `KsAppShell.vue` excludes `internalOnly` routes from its shared menu catalogue.
|
||||
- **Assumption:** T01 needs one responsive workspace boundary for its list body and optional detail region; `CrudWorkspaceLayout` already provides that adapter-neutral responsibility. The UI catalogue is read-only design evidence, so it may be discoverable from the Design System menu while the WBS workspace remains hidden.
|
||||
- **Unknown:** MVP-A Gate passage and visual/assistive-technology approval evidence are not present in the tracker.
|
||||
- **Decision Required:** UX/QA approval is required before marking this WBS item completed at MVP-A.
|
||||
|
||||
## Scope
|
||||
|
||||
- Compose the existing T01 screen type with `CrudWorkspaceLayout`.
|
||||
- Preserve named slots and `StandardScreenBoundary` state handling.
|
||||
- Add a focused DOM contract test for the main/detail layout relationship.
|
||||
- Expose the read-only component catalogue through the `Design System` navigation section; preserve the internal WBS workspace as hidden.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Feature-page migration, new provider capabilities, API/data changes, visual approval, and Playwright evidence.
|
||||
|
||||
## Actual execution evidence
|
||||
|
||||
- `pnpm test -- --run src/shared/ui/screen-types/tests/SearchListCrudPage.spec.ts src/shared/shell/tests/navigationCatalog.spec.ts` — 2 files / 3 tests passed, exit 0.
|
||||
- `pnpm typecheck` — `vue-tsc --noEmit`, exit 0.
|
||||
- Preserved output: `evidence/V13-FE-011/t01-search-list-layout_20260809.log`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# UI provider switch and rollback runbook
|
||||
|
||||
**WBS / Requirement / UI / Test:** AEG-V16-015 / REQ-V16-UI4-07 / UI-V16-UI4-07 / T-V16-UI4-07
|
||||
**Owners:** FE Lead (primary), UX/QA (secondary)
|
||||
**Mode:** human-approved deployment operation only; provider selection is startup-only.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** `contracts/ui/ui-adapter.v4.json`, `frontend/src/main.ts`, `frontend/src/shared/ui/provider/resolveUiProvider.ts`, `tools/validate_v16.py`.
|
||||
- **Assumption:** deployment configuration supplies `VITE_UI_ADAPTER` before building the immutable frontend artifact. The application does not read a mutable provider setting after bootstrap.
|
||||
- **Unknown:** visual-regression baseline, keyboard/focus acceptance artifact, and production performance budget approval are not present in this workspace.
|
||||
- **Decision Required:** FE Lead and UX/QA must approve the named target provider and attach all required evidence before a production switch. This runbook never authorizes an automatic provider switch.
|
||||
|
||||
## Safety invariants
|
||||
|
||||
1. Allowed provider values are `primevue` and `native`; any other value fails closed at startup.
|
||||
2. No feature source may import PrimeVue or AG Grid. Vendor imports are confined to `frontend/src/shared/ui/adapter/primevue/`.
|
||||
3. Changing a provider means building and deploying a new artifact. Do not mutate the active application's global provider.
|
||||
4. Rollback restores the last approved artifact and its recorded provider value. It does not alter data, decisions, evidence, or audit records.
|
||||
|
||||
## Preflight — required before approval
|
||||
|
||||
Record the operator, UTC/KST timestamp, source commit SHA, artifact hash, previous/target provider, and correlation/change reference in the release evidence.
|
||||
|
||||
| Check | Required evidence | Result field |
|
||||
| --- | --- | --- |
|
||||
| Contract conformance | `pnpm test -- --run src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts src/shared/ui/tests/adapterCompatibility.spec.ts` | test output path/hash |
|
||||
| Vendor boundary | `python tools/validate_v16.py` returns `PASS=1` | output path/hash |
|
||||
| Startup selection | fresh artifact starts once with the target `VITE_UI_ADAPTER` value | startup log reference |
|
||||
| Keyboard/focus | approved keyboard/focus test for fields, dialog, grid, and tabs | QA evidence ID |
|
||||
| Accessible name | automated/manual accessibility report for changed screens | QA evidence ID |
|
||||
| State matrix | T01–T10 result for the affected screen catalogue | QA evidence ID |
|
||||
| Visual regression | approved baseline comparison for target viewport set | visual evidence ID |
|
||||
| Performance | approved large-list interaction evidence using the agreed budget | performance evidence ID |
|
||||
|
||||
Any missing, failed, expired, or mismatched evidence blocks the switch. Do not replace a missing target or baseline with a guessed threshold.
|
||||
|
||||
## Approved switch procedure
|
||||
|
||||
1. Confirm a human change approval references the exact commit, artifact hash, target provider, and all preflight evidence.
|
||||
2. Build a new frontend artifact with `VITE_UI_ADAPTER=<target>`; preserve the build output and configuration hash.
|
||||
3. Deploy via the approved release process. Do not change the provider inside a running application.
|
||||
4. Verify the startup log identifies the target provider and that the version/hash matches the approved artifact.
|
||||
5. Run the approved smoke route and the affected T01–T10 checks. Stop on the first failure and begin rollback.
|
||||
6. Append the outcome, timestamps, operator, artifact hash, and evidence links to the release ledger; never overwrite a prior entry.
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
Use rollback when startup selection fails, a required state/keyboard/accessibility check fails, the visual comparison is rejected, or the approved performance evidence is not reproduced.
|
||||
|
||||
1. Declare the change stopped; record the incident/change reference and preserve browser/server logs.
|
||||
2. Select the previously approved immutable artifact and its recorded provider configuration.
|
||||
3. Deploy that artifact through the approved release process; do not hot-swap the provider in memory.
|
||||
4. Verify its startup provider, artifact hash, critical smoke route, and a focused regression check.
|
||||
5. Append a rollback outcome with reason, timestamps, operator, evidence links, and owner/secondary notification. Retain the failed artifact and its evidence for diagnosis.
|
||||
6. Open a corrective WBS/issue for the failed contract, accessibility, visual, or performance condition. A rollback does not silently waive the failed gate.
|
||||
|
||||
## Evidence record template
|
||||
|
||||
```text
|
||||
Change reference:
|
||||
Operator / secondary:
|
||||
Previous provider + artifact SHA:
|
||||
Target provider + artifact SHA:
|
||||
Contract / boundary evidence:
|
||||
Keyboard / accessible-name / T01–T10 evidence:
|
||||
Visual / performance evidence:
|
||||
Startup log reference:
|
||||
Outcome: switched | blocked | rolled back
|
||||
Rollback reason (if applicable):
|
||||
Recorded at (UTC) / display time (KST):
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
# ADR-WBS-001: Resolve VS-03/04/12/14 numbering collision
|
||||
|
||||
**Date:** 2026-08-08
|
||||
**Status:** ACCEPTED (partial — see "Not resolved by this ADR" below)
|
||||
**Decision owner:** 김재현 (per session 2026-08-08 direction: "실제 구현 트래커 유지, WBS_MASTER 갱신" + "구현된 슬라이스를 새 번호로 재배정")
|
||||
|
||||
## Context
|
||||
|
||||
`docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` and `docs/CURRENT/SLICE_SPECS/` independently
|
||||
assigned VS-03, VS-04, VS-12, and VS-14 to four slices implemented in 2026-08-07 (PR #23, #24, #28):
|
||||
|
||||
| Number used by tracker/specs | Actual slice |
|
||||
|---|---|
|
||||
| VS-03 | Model Approval Workflow (Maker-Checker Governance) |
|
||||
| VS-04 | Immutable Audit Trail (GDPR/Compliance) |
|
||||
| VS-12 | Trade Execution System (KIS Integration) |
|
||||
| VS-14 | Portfolio Reconciliation |
|
||||
|
||||
These numbers were never checked against `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, the original
|
||||
666-row plan, which had already assigned the same four numbers to unrelated, still-unimplemented
|
||||
slices:
|
||||
|
||||
| Number in WBS_MASTER.csv | Original planned slice |
|
||||
|---|---|
|
||||
| VS-03 | IngestMarketDataPIT (market data PIT ingestion) |
|
||||
| VS-04 | ApplyCorporateActions (corporate actions processing) |
|
||||
| VS-12 | RankBuyCandidates (buy candidate ranking) |
|
||||
| VS-14 | GenerateDailyRecommendations (daily client recommendation packages) |
|
||||
|
||||
Separately, `src/KArtSell.Host/Features/MarketData/VS03_*.cs` and
|
||||
`src/KArtSell.Host/Features/Portfolio/VS04_*.cs` / `VS05_*.cs` / `VS08_*.cs` are a **third**,
|
||||
already-implemented-and-tested body of work (commits `2bc2b1e`, `32b49a4`, `14c5e4f`, `2eee44d`)
|
||||
that also uses VS-03/04/05/08, for yet another set of features (Market Data Ingestion Dashboard,
|
||||
Portfolio Rebalance, Risk Metrics, Dashboard). This body of work is not referenced anywhere in
|
||||
`WBS_PROGRESS_TRACKER.csv` at all. This ADR does not resolve that — see "Not resolved" below.
|
||||
|
||||
## Decision
|
||||
|
||||
Renumber the four 2026-08-07 slices to previously-unused numbers, leaving `WBS_MASTER.csv`'s
|
||||
original VS-03/04/12/14 rows (and the separate `Features/MarketData`/`Features/Portfolio`
|
||||
VS-03/04/05/08 code) untouched:
|
||||
|
||||
| Old number | New number | Slice | Code location |
|
||||
|---|---|---|---|
|
||||
| VS-03 | **VS-26** | Model Approval Workflow | `src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/` and `Features/ApprovalWorkflow/` (see DEBT-017 below) |
|
||||
| VS-04 | **VS-27** | Immutable Audit Trail / GDPR | `src/KArtSell.Modules.ModelOperations/Compliance/` |
|
||||
| VS-12 | **VS-28** | Trade Execution (KIS) | `src/KArtSell.Modules.ModelOperations/TradeExecution/` |
|
||||
| VS-14 | **VS-29** | Portfolio Reconciliation | `src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/` |
|
||||
|
||||
VS-26 through VS-29 were free (highest number previously used in `WBS_MASTER.csv` was VS-25).
|
||||
|
||||
## Why renumber the new slices rather than renumber WBS_MASTER's originals
|
||||
|
||||
`WBS_MASTER.csv`'s VS-03/04/12/14 rows are referenced by ID from dozens of other rows across the
|
||||
666-row file (dependency chains, addendum sections `AEG22-*`, `AEG-V15-*`, `AEG-V16-*` all use
|
||||
VS-03 to mean "MarketData" consistently throughout). Renumbering those in place would touch 50+
|
||||
rows with a high chance of missing a cross-reference. The four 2026-08-07 slices have a much
|
||||
smaller footprint (2 SLICE_SPEC files each side, 2 README.md files, a handful of tracker rows) and
|
||||
were not yet referenced by ID anywhere else, so moving them was the lower-risk edit.
|
||||
|
||||
## What changed
|
||||
|
||||
- `docs/CURRENT/SLICE_SPECS/VS-03-SLICE_SPEC.md` → `VS-26-SLICE_SPEC.md` (content updated)
|
||||
- `docs/CURRENT/SLICE_SPECS/VS-04-SLICE_SPEC.md` → `VS-27-SLICE_SPEC.md` (content updated)
|
||||
- `docs/CURRENT/SLICE_SPECS/VS-12-SLICE_SPEC.md` → `VS-28-SLICE_SPEC.md` (content updated)
|
||||
- `docs/CURRENT/SLICE_SPECS/VS-14-SLICE_SPEC.md` → `VS-29-SLICE_SPEC.md` (content updated)
|
||||
- `src/KArtSell.Modules.ModelOperations/ApprovalWorkflow/README.md` (VS-03→26, VS-04→27 refs updated)
|
||||
- `src/KArtSell.Modules.ModelOperations/TradeExecution/README.md` (VS-03→26, VS-04→27, VS-12→28, VS-14→29 refs updated)
|
||||
- `docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv` rows `AEG-VS-03-01`/`04-01`/`12-01`/`14-01` renamed to `AEG-VS-26-01`/`27-01`/`28-01`/`29-01`
|
||||
- `docs/CURRENT/CATALOGS/WBS_MASTER.csv` — 4 summary rows appended (VS-26/27/28/29), original rows untouched
|
||||
- `CURRENT_ROADMAP.md` updated to reflect the resolution
|
||||
|
||||
## Not resolved by this ADR (still needs an explicit decision)
|
||||
|
||||
1. **DEBT-017 (duplicate ApprovalWorkflow implementation).** While investigating this
|
||||
renumbering, discovered that the VS-26 (formerly VS-03) tracker row credits
|
||||
`ApprovalWorkflow/` (Workstream H) with "20/20 tests PASS", but that implementation's
|
||||
endpoints are all `[DontRegister]`'d in FastEndpoints — i.e. dead, unreachable code. The
|
||||
implementation actually wired into `Program.cs` and reachable over HTTP is
|
||||
`Features/ApprovalWorkflow/` (Workstream G), which has no dedicated test file found. This
|
||||
ADR does **not** pick a winner between the two. `AEG-VS-26-01`'s status has been changed from
|
||||
`COMPLETED` to `BLOCKED` in the tracker pending that decision — see the row's Notes column and
|
||||
`TECH_DEBT_REGISTER.md` DEBT-017.
|
||||
2. **`Features/MarketData`/`Features/Portfolio` (VS-03/04/05/08 Market Data Ingestion Dashboard,
|
||||
Portfolio Rebalance, Risk Metrics, Dashboard).** This is real, committed, tested code
|
||||
(commits `2bc2b1e`, `32b49a4`, `14c5e4f`, `2eee44d`, `fed750f`, `e0d58ac`) that is completely
|
||||
absent from `WBS_PROGRESS_TRACKER.csv`. It is not renumbered by this ADR because it appears to
|
||||
be a genuine (if structurally divergent — it lives under `Features/` rather than as its own
|
||||
module) implementation attempt at `WBS_MASTER.csv`'s original VS-03/04/05/08 definitions,
|
||||
not a new collision. It needs to be added to the tracker with real evidence (test results,
|
||||
whether it is wired into `Program.cs`, whether a frontend exists) rather than silently
|
||||
inherited into this renumbering. Flagged in `CURRENT_ROADMAP.md` as a follow-up.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
# KBX Design Philosophy — Reference Index
|
||||
|
||||
이 디렉터리의 4개 문서는 K-ArtSell Aegis 프론트엔드가 채택하는 **디자인 철학 소스**다. `docs/Design/kbx-foundation-v36/`은 이 문서를 구현한 참조 코드(도메인은 OMS/WMS/ERP로 다르지만 UX 계약은 동일)이며, 이식 대상이 아니라 구현 참고용이다.
|
||||
|
||||
## 문서 역할
|
||||
|
||||
| 문서 | 역할 |
|
||||
|---|---|
|
||||
| `KBX Design System v1.0.md` | Primitive → Business Component → Screen Template 3계층, Design Token 체계(spacing/typography/color/density), 컴포넌트별 API 계약 |
|
||||
| `KBX Business UX-AX Standard v1.0.md` | 7대 설계원칙(Familiar First, Keyboard Accelerated, Grid First, Exception Driven, Predictable Layout, Explicit State, Audit by Default), Desktop 표준 화면 구조, T01~T09 표준 Screen Template, Workspace Tabs/Unsaved Changes 규칙 |
|
||||
| `KBX Reference Screens v1.0.md` | Global Application Shell 치수(Header 56px / Side Nav 220px / Workspace Tabs 40px / Page Header 48px / Command Bar 44px), 화면별 목업 |
|
||||
| `KBX Implementation Contract v1.0.md` | Screen ID 규칙, Command/Lookup/Grid 계약, Keyboard Manager 상세 API |
|
||||
|
||||
## K-ArtSell 적용 원칙
|
||||
|
||||
- Vertical Slice(업무 모듈)는 업무를 구현하고, 공통 계층(`frontend/src/shared/`)은 UX를 구현한다 (Design System §1).
|
||||
- 신규 공통 컴포넌트는 `Kbx*`가 아니라 이미 채택된 `Ks*` 접두사를 따른다(`V13-FE-005`).
|
||||
- Design Token은 `--kbx-*`가 아니라 기존 `--ks-*` 네임스페이스에 통합한다(`design-system/tokens.css`).
|
||||
|
||||
## Screen Template 재매핑 (KBX T01~T09 ↔ K-ArtSell T01~T12)
|
||||
|
||||
K-ArtSell은 `frontend/src/shared/ui/screen-types/catalogue.ts`에서 KBX 템플릿을 금융 자문 도메인 의미로 재정의했다. 번호는 KBX 원본과 일치하지 않는다.
|
||||
|
||||
| KBX (OMS/WMS/ERP 의미) | K-ArtSell 번호 | K-ArtSell 의미 |
|
||||
|---|---|---|
|
||||
| T01 Search/List | T01 | 검색·목록형 CRUD |
|
||||
| T02 Master CRUD | T03 | 등록·편집 Form |
|
||||
| T03 Header+Detail Transaction | T04 / T06 | Master-Detail / 단계 Wizard |
|
||||
| T04 Fast Grid Entry | **T11 (신규)** | 대량 입력 |
|
||||
| T05 Master/Detail Explorer | T04 | Master-Detail |
|
||||
| T06 Work Queue | **T12 (신규)** | 작업 큐 |
|
||||
| T07 Reconcile/Verification | T09 | 대사·예외 처리 |
|
||||
| T08 Excel Import | — (범위 밖) | 데이터 유입이 KRX/OpenDart/KIS API 중심이라 미채택 |
|
||||
| T09 WMS Mobile | — (해당 없음) | 물류 현장 업무 없음 |
|
||||
| — | T02 | 상세 조회형 (KBX에 없는 K-ArtSell 고유 타입) |
|
||||
| — | T05 | 검토·승인 Workbench (maker-checker) |
|
||||
| — | T07 | Dashboard·Scorecard |
|
||||
| — | T08 | Batch·데이터 운영 |
|
||||
| — | T10 | 버전 비교·거버넌스 (모델/정책 승격) |
|
||||
|
||||
## 셸/홈/워크스페이스 이식 현황
|
||||
|
||||
`frontend/src/shared/shell/`에 Global Header · Side Navigation · Workspace Tabs · Ctrl+K 메뉴검색을 Business UX-AX Standard §3~8, §58~59 규격대로 구현한다. 세부 계획은 이식 작업 당시의 계획 문서를 참고(리포지토리 커밋 이력의 `V13-FE-007~010` 참고).
|
||||
@@ -0,0 +1,91 @@
|
||||
## KBX UX/AX Review
|
||||
|
||||
- [ ] 기존 9개 Screen Type 중 하나를 사용했다.
|
||||
- [ ] 신규 Component보다 기존 KBX Component 재사용을 먼저 검토했다.
|
||||
- [ ] 업무 모듈에서 PrimeVue/AG Grid를 직접 import하지 않는다.
|
||||
- [ ] 주요 명령 위치가 KBX Command Bar 규칙과 같다.
|
||||
- [ ] Keyboard 흐름(F2/F3/F8/Tab/Enter/Esc)을 검토했다.
|
||||
- [ ] 입력 가능한 대량 데이터 화면의 Excel 정책을 정의했다.
|
||||
- [ ] 정상 건을 사용자가 불필요하게 확인하는 단계가 없는지 검토했다.
|
||||
- [ ] 오류 메시지가 원인과 다음 행동을 설명한다.
|
||||
- [ ] Client validation을 업무 정합성의 최종 방어선으로 사용하지 않는다.
|
||||
- [ ] 상태 변경, Audit, Concurrency, Idempotency 영향도를 검토했다.
|
||||
- [ ] AI가 없어도 동일 업무를 수행할 수 있다.
|
||||
- [ ] AI Action은 Proposal → Validation → Command 경계를 지킨다.
|
||||
- [ ] 현장 화면은 실제 장비/네트워크 조건의 수용시험 항목을 정의했다.
|
||||
|
||||
## Architecture
|
||||
|
||||
- [ ] `node scripts/validate-kbx.mjs` 통과
|
||||
- [ ] Screen ID / Version 갱신 여부 검토
|
||||
- [ ] 기술부채 또는 KBX 예외가 있다면 이유와 재검토 시점을 기록했다.
|
||||
|
||||
|
||||
## Production readiness
|
||||
- [ ] Long operations do not block the page with a modal spinner
|
||||
- [ ] Mutation retries are idempotent or explicitly disabled
|
||||
- [ ] 409/version conflicts cannot silently overwrite newer data
|
||||
- [ ] Stale operational data exposes freshness/reload where material
|
||||
- [ ] Unexpected errors expose a correlation reference, not stack traces
|
||||
- [ ] Degraded/read-only behavior is defined for affected workflows
|
||||
|
||||
## Component verification
|
||||
- [ ] 공통 Component 변경이면 `COMMON-DS-001`에서 Default/Readonly/Disabled/Error/Loading 상태를 확인했다.
|
||||
- [ ] Keyboard/Focus 계약 변경이면 E2E scenario를 갱신했다.
|
||||
- [ ] 의도된 시각 변경이면 Compact/Comfortable/Touch baseline 변경 사유를 기록했다.
|
||||
- [ ] 공통 Component의 동작/표현 변경이면 Component Version을 검토했다.
|
||||
- [ ] 색상만으로 상태를 전달하거나 Focus Indicator를 제거하지 않았다.
|
||||
|
||||
## Design ↔ Code parity / release
|
||||
- [ ] Design Token 변경은 `packages/kbx-ui/src/tokens/source/kbx.tokens.json`에서 시작했다.
|
||||
- [ ] Semantic/Component Token 값 변경이면 Visual Regression 영향도를 검토했다.
|
||||
- [ ] Core Component 상태가 `COMMON-DS-001`과 Figma contract에 모두 존재한다.
|
||||
- [ ] 새 raw color/px literal을 추가하지 않았다. 필요한 경우 먼저 Token 승격을 검토했다.
|
||||
- [ ] Component/Screen/Token 공개 계약 변경이면 `generated/release-impact.json`의 요구 bump를 확인했다.
|
||||
- [ ] Breaking 변경이면 Migration Guide를 작성했다.
|
||||
|
||||
## API contract / Problem governance
|
||||
- [ ] 업무 모듈에서 raw `/api/...`, `axios`, `fetch`를 직접 사용하지 않는다.
|
||||
- [ ] 신규/변경 Endpoint는 `contracts/api/kbx.api.json`과 동일한 Method/Route/Permission을 가진다.
|
||||
- [ ] Mutation Retry 가능 여부와 Idempotency 정책을 정의했다.
|
||||
- [ ] Validation/Business/Conflict/Permission/NotFound/Integration/System 오류를 KBX Problem으로 표현한다.
|
||||
- [ ] API 공개 계약 변경이면 `generated/release-impact.json`의 SemVer 요구수준을 확인했다.
|
||||
- [ ] Host에서 실제 Swashbuckle OpenAPI snapshot diff를 수행했다.
|
||||
|
||||
## Authorization / Sensitive Data
|
||||
- [ ] 신규 Permission은 `contracts/authorization/kbx.authorization.json`에 등록했습니다.
|
||||
- [ ] Frontend 숨김/Disabled만으로 보안을 처리하지 않고 Backend 최종 검증이 있습니다.
|
||||
- [ ] Sensitive Field는 기본 Masking이며 전체보기/비마스킹 Export 권한을 구분했습니다.
|
||||
- [ ] Sensitive 원문을 Telemetry/AI Context에 넣지 않았습니다.
|
||||
- [ ] 전체보기 또는 민감 데이터 공개가 필요한 경우 Audit 경로를 정의했습니다.
|
||||
|
||||
## KBX v18 Scenario / Test Data
|
||||
|
||||
- [ ] 변경된 Golden Screen/업무 경계의 canonical scenario를 갱신했다.
|
||||
- [ ] Fixture는 synthetic-only이며 Production dump를 포함하지 않는다.
|
||||
- [ ] `idempotency=required` Command의 replay scenario가 있다.
|
||||
- [ ] Host에서 실행한 경우 Scenario Evidence/Correlation ID를 남겼다.
|
||||
|
||||
## External integration / resilience
|
||||
- [ ] Business State와 Integration State를 분리했다.
|
||||
- [ ] 새 외부연계는 `contracts/integrations/kbx.integrations.json`에 등록했다.
|
||||
- [ ] at-least-once 전달은 idempotency 경계를 가진다.
|
||||
- [ ] 짧은 transient retry는 bounded Polly pipeline이고 장기 retry는 Hangfire가 소유한다.
|
||||
- [ ] permanent failure는 사용자 업무 재실행이 아니라 Operations Exception으로 노출된다.
|
||||
- [ ] 실패주입 Scenario가 있다.
|
||||
|
||||
|
||||
## External data / provenance
|
||||
- [ ] 외부 Provider 응답을 화면이 직접 해석하지 않고 canonical normalizer를 거칩니다.
|
||||
- [ ] providerObservedAt / receivedAt / ingestedAt 의미를 혼합하지 않았습니다.
|
||||
- [ ] Stale/Expired 데이터가 최신값처럼 보이지 않습니다.
|
||||
- [ ] request_descriptor에는 Secret/Token이 없고 raw payload 저장은 명시적 검토 없이는 금지합니다.
|
||||
- [ ] KRX 승인 서비스의 TTL/Schema를 공식 서비스 명세 없이 추정하지 않았습니다.
|
||||
|
||||
## Configuration / deployment governance
|
||||
- [ ] 신규 설정은 `contracts/configuration/kbx.configuration.json`에 등록했고 직접 `Environment.GetEnvironmentVariable()`을 사용하지 않았다.
|
||||
- [ ] Secret에는 기본값/예제값을 넣지 않았고 generated env example도 빈 값이다.
|
||||
- [ ] Production은 `predeploy` migration + HTTPS 원칙을 유지한다.
|
||||
- [ ] 환경별 다른 바이너리를 다시 빌드하지 않고 동일 Release Artifact를 승격한다.
|
||||
- [ ] destructive migration이 필요하다면 명시적 승인 marker와 Migration Guide가 있다.
|
||||
- [ ] 배포 전 Configuration Validation / Migration Dry Run / Release Governance evidence를 확인했다.
|
||||
@@ -0,0 +1,54 @@
|
||||
name: KBX Quality Gate
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
architecture:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate KBX architecture
|
||||
run: node scripts/validate-kbx.mjs
|
||||
- name: Ensure generated manifests are committed
|
||||
run: git diff --exit-code -- generated/ apps/web/src/registry/screens.generated.ts packages/kbx-ui/src/tokens/kbx.css packages/kbx-contracts/src/generated/ backend/Shared/Contracts/Generated/ backend/Shared/Authorization/Generated/ backend/Shared/Telemetry/Generated/ backend/Shared/Experiments/Generated/ backend/Shared/Testing/Generated/ backend/Shared/Integrations/Generated/ backend/Shared/Providers/Generated/ backend/Shared/ExternalData/Generated/ backend/Shared/Configuration/Generated/ design/figma/ contracts/api/openapi.kbx.json deploy/kbx/
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Enable Corepack
|
||||
run: corepack enable
|
||||
- name: Install and test when lockfile exists
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f pnpm-lock.yaml ]; then
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm -r --if-present run typecheck
|
||||
pnpm -r --if-present run test
|
||||
pnpm -r --if-present run build
|
||||
echo "KBX canonical scenario contract validated statically; Playwright host run belongs to product repository."
|
||||
else
|
||||
echo "Starter has no pnpm-lock.yaml yet; dependency build gate is intentionally deferred."
|
||||
fi
|
||||
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Build/test when solution exists
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
solutions=( *.sln *.slnx )
|
||||
if [ ${#solutions[@]} -gt 0 ]; then
|
||||
dotnet restore "${solutions[0]}"
|
||||
dotnet build "${solutions[0]}" --no-restore -c Release
|
||||
dotnet test "${solutions[0]}" --no-build -c Release
|
||||
else
|
||||
echo "Reference starter has no .NET solution file; backend compile gate is deferred to host repository."
|
||||
fi
|
||||
@@ -0,0 +1,38 @@
|
||||
name: KBX Release Readiness
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: Target environment (Staging or Production)
|
||||
required: true
|
||||
default: Staging
|
||||
|
||||
jobs:
|
||||
release-readiness:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout immutable candidate
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate KBX contracts and configuration governance
|
||||
run: node scripts/validate-kbx.mjs
|
||||
- name: Verify generated configuration/deployment artifacts
|
||||
run: git diff --exit-code -- generated/configuration-manifest.json packages/kbx-contracts/src/generated/configurationCatalog.ts backend/Shared/Configuration/Generated/KbxConfigurationCatalog.g.cs deploy/kbx/
|
||||
- name: Build/test when host repository is available
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
solutions=( *.sln *.slnx )
|
||||
if [ ${#solutions[@]} -gt 0 ]; then
|
||||
dotnet restore "${solutions[0]}"
|
||||
dotnet build "${solutions[0]}" --no-restore -c Release
|
||||
dotnet test "${solutions[0]}" --no-build -c Release
|
||||
else
|
||||
echo "Starter: host build is deferred."
|
||||
fi
|
||||
- name: Configuration validation gate
|
||||
run: echo "Host must bind target-environment secrets/config and call KbxConfigurationStartupValidator.ValidateOrThrow before promotion."
|
||||
- name: Database migration dry-run gate
|
||||
run: echo "Host must execute DbUp validation/dry-run against an environment-equivalent database before applying migrations."
|
||||
- name: Migration application policy
|
||||
run: echo "Production migrations are pre-deploy; application startup schema mutation is forbidden."
|
||||
@@ -0,0 +1,50 @@
|
||||
# KBX Foundation v36 — Recipe Verification · Generated Test Plan · Deterministic Home Workbench
|
||||
|
||||
KBX v36는 v35의 Screen Recipe / Secure Scaffolder 기반을 실제 검증계약까지 닫는 Foundation iteration이다. 목표는 화면·컴포넌트 수를 더 늘리는 것이 아니라, T01~T09를 선택한 순간부터 표준 UX, 보안, 복구, 테스트 증적이 함께 따라오도록 만드는 것이다.
|
||||
|
||||
## v36 핵심
|
||||
|
||||
- T01~T09 `testProfile` 추가
|
||||
- 필수 Scenario Kind
|
||||
- 필수 Behavioral Tag
|
||||
- 필수 Evidence
|
||||
- 필수 Recovery/Interaction Check
|
||||
- `generated/screen-recipe-verification-manifest.json` 추가
|
||||
- T01~T09 Recipe Verification **9/9 자동 폐쇄성**
|
||||
- 각 Recipe는 실제 해당 Template을 사용하는 Screen의 Canonical E2E Scenario를 최소 1개 요구
|
||||
- T02 Master / T06 Queue / T07 Reconcile Canonical E2E Recovery Scenario 추가
|
||||
- Scaffolder가 신규 Vertical Slice에 `*.test-plan.ts` 자동 생성
|
||||
- 생성 Test Plan은 Canonical Scenario / Required Check / Required Evidence를 타입 계약으로 보존
|
||||
- 기존 Master / Transaction / Fast Entry의 명시적 `--write-permission` fail-closed 정책 유지
|
||||
- Home Attention Queue를 동일 우선순위 내 최신 발생 순으로 정렬
|
||||
- Home은 표시 상한과 별개로 권한 필터 후 실제 전체 건수 및 overflow를 정확히 표시
|
||||
- `buildKbxHomeAttentionQueue()` public API 추가, 기존 `buildKbxHomeAttention()` 호환 유지
|
||||
- Screen Recipe `testProfile` / `canonicalScenarioIds` 변경을 Release Impact에서 추적
|
||||
- Design Debt ratchet **131 유지**
|
||||
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
node scripts/validate-kbx.mjs
|
||||
```
|
||||
|
||||
최종 검증 기준:
|
||||
|
||||
- 299 TS/Vue script units
|
||||
- 20 Screen Definitions
|
||||
- 84 Component Definitions
|
||||
- 66 Component Catalog entries
|
||||
- 153 Design Tokens
|
||||
- 9 Screen Recipes
|
||||
- 30 Canonical Test Scenarios
|
||||
- Recipe Verification 9/9
|
||||
- 16 Navigation Entries
|
||||
- Design Debt 131 <= 131
|
||||
- Release Impact major / major
|
||||
|
||||
## 문서
|
||||
|
||||
- `docs/screen-recipe-verification-home-attention-v36.md`
|
||||
- `docs/kbx-v36-standard-traceability.md`
|
||||
- `docs/validation-report-v36.md`
|
||||
- `docs/release/migration-guide.md`
|
||||
@@ -0,0 +1,10 @@
|
||||
import '@kbx/ui/tokens.css'
|
||||
import type { App } from 'vue'
|
||||
import { kbxLookupRegistryKey } from '@kbx/ui'
|
||||
import { lookupRegistry } from '../lookups/lookupRegistry'
|
||||
import { installScreenRegistry } from '../registry/screens'
|
||||
|
||||
export function installKbx(app: App) {
|
||||
app.provide(kbxLookupRegistryKey, lookupRegistry)
|
||||
installScreenRegistry()
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
export interface KbxExperimentChangedEvent { experimentId:string; screenId:string; changedAt:string }
|
||||
export function createExperimentRolloutConnection(url='/hubs/kbx-experiments'){
|
||||
const hub=new signalR.HubConnectionBuilder().withUrl(url).withAutomaticReconnect().build()
|
||||
return {start:()=>hub.state===signalR.HubConnectionState.Disconnected?hub.start():Promise.resolve(),stop:()=>hub.stop(),onChanged:(callback:(event:KbxExperimentChangedEvent)=>void)=>hub.on('ExperimentChanged',callback)}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import type { KbxApiOperationId, KbxExperimentAssignmentsResponse, KbxExperimentId } from '@kbx/contracts'
|
||||
import { kbxApi } from '../http/generated/kbxApiClient'
|
||||
import { kbxTelemetry } from '../telemetry/kbxTelemetryClient'
|
||||
import { createExperimentRolloutConnection } from './createExperimentRolloutConnection'
|
||||
|
||||
const screenCache=new Map<string,Promise<KbxExperimentAssignmentsResponse>>()
|
||||
const listeners=new Map<string,Set<()=>void>>()
|
||||
let realtimeStarted=false
|
||||
const realtime=createExperimentRolloutConnection()
|
||||
realtime.onChanged(event=>{screenCache.delete(event.screenId);for(const refresh of listeners.get(event.screenId)??[])void refresh()})
|
||||
async function ensureRealtime(){if(realtimeStarted)return;realtimeStarted=true;try{await realtime.start()}catch{realtimeStarted=false}}
|
||||
function assignments(screenId:string,force=false){if(force)screenCache.delete(screenId);if(!screenCache.has(screenId))screenCache.set(screenId,kbxApi.request<KbxExperimentAssignmentsResponse>('common.experiments.assignments' as KbxApiOperationId,{query:{screenId}}));return screenCache.get(screenId)!}
|
||||
export function useKbxExperiment(screenId:string,screenVersion:string,experimentId:KbxExperimentId,surface:string){
|
||||
const variant=ref('control'),enrolled=ref(false);let lastExposure=''
|
||||
const refresh=async()=>{
|
||||
try{const result=await assignments(screenId,true);const assignment=result.assignments.find(x=>x.experimentId===experimentId);if(!assignment||!assignment.enrolled){enrolled.value=false;variant.value='control';lastExposure='';kbxTelemetry.clearExperiment(screenId);return}enrolled.value=true;variant.value=assignment.variant;kbxTelemetry.bindExperiment(screenId,assignment.experimentId,assignment.variant);const exposure=`${assignment.experimentId}:${assignment.variant}`;if(lastExposure!==exposure){lastExposure=exposure;kbxTelemetry.track('experiment.exposed',{screenId,screenVersion,attributes:{surface}})}}catch{/* experiment infrastructure must not block business UI */}
|
||||
}
|
||||
onMounted(async()=>{const set=listeners.get(screenId)??new Set<()=>void>();set.add(refresh);listeners.set(screenId,set);await ensureRealtime();await refresh()})
|
||||
onUnmounted(()=>{listeners.get(screenId)?.delete(refresh);kbxTelemetry.clearExperiment(screenId)})
|
||||
return {variant,enrolled,is:(value:string)=>variant.value===value,refresh}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { KbxHelpContent } from '@kbx/contracts'
|
||||
|
||||
export const helpRegistry: Record<string, KbxHelpContent> = {
|
||||
'OMS-ORD-001': {
|
||||
key: 'OMS-ORD-001', title: '주문관리', purpose: '주문을 조회하고 예외 및 출고대상을 일괄 처리합니다.',
|
||||
steps: ['조회조건을 입력합니다.', 'F3 또는 조회를 실행합니다.', '처리할 주문을 선택한 뒤 업무 명령을 실행합니다.'],
|
||||
shortcuts: [{ key: 'F3', description: '조회' }],
|
||||
},
|
||||
'OMS-ORD-002': {
|
||||
key: 'OMS-ORD-002', title: '주문등록', purpose: '거래처 주문을 Header/Detail 방식으로 등록합니다.',
|
||||
steps: ['거래처와 출고창고를 선택합니다.', '품목코드 또는 F2로 상품을 입력합니다.', '수량·단가를 확인하고 F8로 저장합니다.'],
|
||||
shortcuts: [{ key: 'F2', description: '코드 조회' }, { key: 'F8', description: '저장' }],
|
||||
},
|
||||
'ERP-MST-ITEM-001': {
|
||||
key: 'ERP-MST-ITEM-001', title: '품목관리', purpose: '품목 기준정보와 물류 속성을 관리합니다.',
|
||||
steps: ['왼쪽 목록에서 품목을 선택하거나 신규를 누릅니다.', '기준정보와 물류정보를 입력합니다.', 'F8로 저장합니다.'],
|
||||
shortcuts: [{ key: 'F3', description: '조회' }, { key: 'F8', description: '저장' }, { key: 'F2', description: '창고 조회' }],
|
||||
cautions: ['사용된 품목은 삭제보다 사용중지를 우선합니다.'],
|
||||
},
|
||||
'ERP-INV-001': {
|
||||
key: 'ERP-INV-001', title: '재고현황', purpose: '품목별 현재고에서 창고·로케이션 근거와 재고이력까지 같은 조회 맥락에서 확인합니다.',
|
||||
steps: ['조회조건을 입력하고 F3을 누릅니다.', '품목을 선택하면 오른쪽 위치재고와 아래 재고이력이 함께 바뀝니다.', '현재고 또는 가용수량을 눌러 산정 근거를 확인합니다.'],
|
||||
shortcuts: [{ key: 'F3', description: '조회' }],
|
||||
cautions: ['화면 수량은 조회 Projection이며 실제 변경·확정 시 서버 재고 정책이 다시 검증합니다.'],
|
||||
},
|
||||
'ERP-PRICE-001': {
|
||||
key: 'ERP-PRICE-001', title: '품목 단가 일괄등록', purpose: '품목별 적용일과 단가를 Excel 방식으로 빠르게 입력하고 일괄 저장합니다.',
|
||||
steps: ['품목코드를 입력하거나 F2로 품목을 선택합니다.', '적용일과 단가를 입력하고 필요하면 Excel 영역을 붙여넣습니다.', '오류가 없으면 F8로 입력 건을 한 번에 저장합니다.'],
|
||||
shortcuts: [{ key: 'F2', description: '품목 조회' }, { key: 'F8', description: '저장' }],
|
||||
cautions: ['같은 품목과 적용일을 요청 안에서 중복 입력할 수 없습니다.', '서버가 품목 존재·권한·단가 범위를 다시 검증하고 저장 이력을 남깁니다.'],
|
||||
},
|
||||
'WMS-PICK-001': {
|
||||
key: 'WMS-PICK-001', title: '출고 피킹', purpose: '위치와 상품을 스캔하여 출고 피킹을 수행합니다.',
|
||||
cautions: ['서버 성공 응답 전에 같은 상품을 다시 스캔하지 마세요. 네트워크 오류는 동일 Idempotency Key로 자동 재시도합니다.'],
|
||||
},
|
||||
'OMS-ORD-003': {
|
||||
key: 'OMS-ORD-003', title: '주문 Excel 업로드', purpose: '주문 데이터를 공통 매핑·검증 절차로 대량 등록하거나 수정합니다.',
|
||||
steps: ['업로드 양식 또는 기존 Excel 파일을 선택합니다.', '자동 매핑 결과를 확인하고 필요한 열만 수정합니다.', '검증 결과에서 오류행을 확인한 뒤 정상 데이터만 반영합니다.'],
|
||||
cautions: ['검증되지 않은 데이터는 반영할 수 없습니다.', '기존 주문은 수정 가능한 업무상태에서만 업데이트됩니다.'],
|
||||
},
|
||||
'COMMON-OPS-001': {
|
||||
key: 'COMMON-OPS-001', title: '업무 예외 센터', purpose: '정상 업무가 아니라 사람이 판단하거나 복구해야 할 예외만 모아 처리합니다.',
|
||||
steps: ['미처리 예외를 조회합니다.', '중요도와 경과시간을 기준으로 우선순위를 확인합니다.', '원 업무 보기 또는 등록된 안전한 재처리 기능으로 해결합니다.'],
|
||||
shortcuts: [{ key: 'F3', description: '조회' }],
|
||||
cautions: ['예외 센터는 원천 업무 데이터의 Source of Truth가 아닙니다.', '원천 조건이 해결되지 않은 예외를 단순 숨김 처리하지 않습니다.'],
|
||||
},
|
||||
'COMMON-REC-001': {
|
||||
key: 'COMMON-REC-001', title: '업무 데이터 대사', purpose: 'OMS·WMS·ERP 사이의 기대값과 실제값 차이를 시스템이 계산해 보여줍니다.',
|
||||
steps: ['대사유형과 결과를 선택합니다.', '불일치 건의 기대값·실제값·원인을 확인합니다.', '조사가 필요한 건만 예외 센터에 등록합니다.'],
|
||||
shortcuts: [{ key: 'F3', description: '조회' }],
|
||||
cautions: ['대사 화면에서 원천 데이터를 직접 수정하지 않습니다.'],
|
||||
},
|
||||
'OMS-CLM-001': {
|
||||
key:'OMS-CLM-001', title:'반품·클레임 관리', purpose:'반품·교환·취소 요청을 기존 주문 맥락에서 조회하고 처리합니다.',
|
||||
steps:['F3으로 미처리 요청을 조회합니다.','주문번호와 사유를 확인합니다.','허용된 상태전이만 실행합니다.'], shortcuts:[{key:'F3',description:'조회'}], cautions:['완료된 클레임은 원 주문·물류 이력과 함께 추적합니다.'],
|
||||
},
|
||||
'ERP-PUR-001': {
|
||||
key:'ERP-PUR-001', title:'구매등록', purpose:'거래처 구매를 Header/Detail 방식으로 입력하고 입고 흐름과 연결합니다.',
|
||||
steps:['거래처와 입고창고를 선택합니다.','품목·수량·단가·납기일을 입력합니다.','F8 저장 후 구매확정합니다.'], shortcuts:[{key:'F2',description:'코드 조회'},{key:'F8',description:'저장'}], cautions:['확정 후에는 입고 실적과 연결되므로 직접 수정 범위를 제한합니다.'],
|
||||
},
|
||||
'ERP-INV-MOVE-001': {
|
||||
key:'ERP-INV-MOVE-001', title:'재고이동', purpose:'창고 간 재고이동을 등록하고 이동중·입고완료 상태를 추적합니다.',
|
||||
steps:['출발·도착창고를 선택합니다.','품목과 이동수량을 입력합니다.','이동확정 후 WMS/입고 흐름에서 완료합니다.'], shortcuts:[{key:'F2',description:'코드 조회'},{key:'F8',description:'저장'}], cautions:['출발창고와 도착창고는 같을 수 없고 가용재고보다 많이 이동할 수 없습니다.'],
|
||||
},
|
||||
'WMS-REC-001': { key:'WMS-REC-001', title:'입고 검수', purpose:'입고예정/ASN과 실물을 스캔해 수량·품목을 검수합니다.', cautions:['예정과 다른 상품·수량은 자동 확정하지 않고 예외로 남깁니다.'] },
|
||||
'WMS-PUT-001': { key:'WMS-PUT-001', title:'입고 적치', purpose:'검수 완료 상품을 추천 로케이션에 적치합니다.', cautions:['추천 위치와 다른 위치는 정상 흐름에서 자동 승인하지 않습니다.'] },
|
||||
'WMS-COUNT-001': { key:'WMS-COUNT-001', title:'재고실사', purpose:'위치·상품을 스캔해 실물수량을 기록하고 장부수량 차이를 예외로 보냅니다.', cautions:['실사 차이는 현장에서 장부수량을 직접 덮어쓰지 않습니다.'] },
|
||||
|
||||
'WMS-WORK-001': { key:'WMS-WORK-001', title:'물류 작업', purpose:'입고·적치·피킹·검수·실사 작업과 예외를 작업자 관점에서 조회하고 시작합니다.', steps:['F3으로 내 작업을 조회합니다.','SLA·진행률·예외를 확인합니다.','작업을 선택해 작업 시작을 실행합니다.'], shortcuts:[{key:'F3',description:'조회'}], cautions:['현장 실행은 각 PDA 작업화면의 서버 검증과 Idempotency 규칙을 따릅니다.'] },
|
||||
|
||||
'COMMON-EXP-001': { key:'COMMON-EXP-001', title:'UX 실험·점진배포', purpose:'되돌릴 수 있는 UX 변경을 제한된 사용자에게 점진 배포하고 기존 UX 지표와 Guardrail을 비교합니다.', steps:['Draft 상태에서 변경 범위와 기준 지표를 검토합니다.','소규모 Rollout으로 시작하고 표본·P95·오류율을 확인합니다.','Guardrail 위반 시 즉시 롤백하고 원인을 기록합니다.'], shortcuts:[{key:'F3',description:'조회'}], cautions:['권한·민감정보·Domain Rule·Validation·Idempotency·WMS Scan 규칙은 실험 대상이 아닙니다.','운영 Threshold는 통계적 유의성을 의미하지 않습니다.'] },
|
||||
|
||||
'COMMON-UX-001': { key:'COMMON-UX-001', title:'UX 품질 지표', purpose:'Task 시간·의미 조작수·Manual Intervention Rate·검증/Import 실패율·예외 해결시간·AI 제안 수용률을 확인합니다.', steps:['기간을 선택하고 F3으로 조회합니다.','표본 수와 P50/P95를 함께 확인합니다.','지표 변화는 위험도·업무오류·현장 VOC와 같이 판단합니다.'], shortcuts:[{key:'F3',description:'조회'}], cautions:['DOM 전체 클릭이나 고객·주문 원문 데이터를 수집하지 않습니다.','Manual Intervention Rate를 낮추기 위해 위험 업무를 무리하게 자동화하지 않습니다.'] },
|
||||
|
||||
'COMMON-DATA-001': { key:'COMMON-DATA-001', title:'외부 데이터 상태', purpose:'KRX·OPENDART·KIS 외부 데이터의 출처·수신시각·신선도·캐시 상태를 확인합니다.', steps:['F3으로 현재 상태를 조회합니다.','Stale/Expired 데이터셋의 최근 수신시각과 정책을 확인합니다.','허용된 데이터셋만 재수집합니다.'], shortcuts:[{key:'F3',description:'조회'}], cautions:['외부 데이터는 Domain 업무원장이 아닙니다.','KRX 승인 서비스는 서비스별 명시적 신선도 정책이 없으면 범용 TTL을 추정하지 않습니다.'] },
|
||||
|
||||
'COMMON-DS-001': { key:'COMMON-DS-001', title:'KBX 컴포넌트 카탈로그', purpose:'공통 컴포넌트의 상태·밀도·키보드·접근성 계약을 독립적으로 재현합니다.', steps:['밀도를 선택합니다.','컴포넌트의 기본·오류·비활성 상태를 비교합니다.','Visual/Keyboard Regression 기준으로 사용합니다.'], cautions:['업무 사용자를 위한 메뉴가 아니라 KBX 개발·검증 화면입니다.'] },
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { kbxApiCatalog, type KbxApiOperationDefinition, type KbxApiOperationId, type KbxApiRequestOptions } from '@kbx/contracts'
|
||||
import { kbxHttp } from '../kbxHttpClient'
|
||||
|
||||
function buildPath(template: string, values: Record<string, string | number> = {}) {
|
||||
return template.replace(/\{([^}:]+)(?::[^}]+)?\}/g, (_, name: string) => {
|
||||
const key = Object.keys(values).find(x => x.toLowerCase() === name.toLowerCase())
|
||||
if (!key) throw new Error(`API path parameter '${name}' is required.`)
|
||||
return encodeURIComponent(String(values[key]))
|
||||
})
|
||||
}
|
||||
|
||||
export async function requestKbxApi<TResponse = unknown, TBody = unknown>(
|
||||
operationId: KbxApiOperationId,
|
||||
options: KbxApiRequestOptions<TBody> = {},
|
||||
): Promise<TResponse> {
|
||||
const operation: KbxApiOperationDefinition = kbxApiCatalog[operationId]
|
||||
if (operation.idempotency === 'required' && !options.idempotencyKey) {
|
||||
throw new Error(`Operation '${operationId}' requires a stable idempotency key.`)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { ...(options.headers ?? {}) }
|
||||
if (options.idempotencyKey) headers['Idempotency-Key'] = options.idempotencyKey
|
||||
|
||||
const response = await kbxHttp.request<TResponse>({
|
||||
method: operation.method,
|
||||
url: buildPath(operation.path, options.path),
|
||||
params: options.query,
|
||||
data: options.body,
|
||||
headers,
|
||||
responseType: options.responseType ?? operation.responseType ?? 'json',
|
||||
validateStatus: status => operation.successStatuses.some(value => value === status),
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const kbxApi = {
|
||||
request: requestKbxApi,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import axios, { AxiosError, type AxiosRequestConfig } from 'axios'
|
||||
import { isKbxProblem, kbxUnexpectedProblem, type KbxProblem, type KbxSystemProblem } from '@kbx/contracts'
|
||||
|
||||
export const kbxHttp = axios.create({
|
||||
timeout: 30_000,
|
||||
headers: { 'X-KBX-Client': 'web' },
|
||||
})
|
||||
|
||||
kbxHttp.interceptors.request.use(config => {
|
||||
config.headers['X-Correlation-Id'] ??= crypto.randomUUID()
|
||||
return config
|
||||
})
|
||||
|
||||
kbxHttp.interceptors.response.use(
|
||||
response => response,
|
||||
(error: AxiosError) => {
|
||||
const correlationId = String(error.config?.headers?.['X-Correlation-Id'] ?? crypto.randomUUID())
|
||||
|
||||
if (!error.response) {
|
||||
const problem: KbxSystemProblem = {
|
||||
type: 'system',
|
||||
code: 'NETWORK_UNAVAILABLE',
|
||||
title: '서버와 연결할 수 없습니다.',
|
||||
detail: '입력한 내용은 유지됩니다. 네트워크 상태를 확인한 후 다시 시도하세요.',
|
||||
correlationId,
|
||||
retryable: true,
|
||||
}
|
||||
return Promise.reject(problem)
|
||||
}
|
||||
|
||||
const data = error.response.data
|
||||
if (isKbxProblem(data)) {
|
||||
const problem: KbxProblem = {
|
||||
...data,
|
||||
correlationId: data.correlationId ?? correlationId,
|
||||
}
|
||||
return Promise.reject(problem)
|
||||
}
|
||||
|
||||
if (error.response.status === 403) {
|
||||
return Promise.reject({
|
||||
type: 'permission', code: 'PERMISSION_DENIED', title: '이 작업을 수행할 권한이 없습니다.', correlationId,
|
||||
} satisfies KbxProblem)
|
||||
}
|
||||
if (error.response.status === 404) {
|
||||
return Promise.reject({
|
||||
type: 'not-found', code: 'RESOURCE_NOT_FOUND', title: '요청한 데이터를 찾을 수 없습니다.', correlationId,
|
||||
} satisfies KbxProblem)
|
||||
}
|
||||
|
||||
return Promise.reject(kbxUnexpectedProblem(correlationId, `HTTP ${error.response.status}`, error.response.status >= 500))
|
||||
},
|
||||
)
|
||||
|
||||
export function withIdempotency(config: AxiosRequestConfig = {}, key = crypto.randomUUID()): AxiosRequestConfig {
|
||||
return {
|
||||
...config,
|
||||
headers: {
|
||||
...(config.headers ?? {}),
|
||||
'Idempotency-Key': key,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ref } from 'vue'
|
||||
import type { KbxConflictSnapshot, KbxProblem, KbxSystemProblem } from '@kbx/contracts'
|
||||
|
||||
export function useKbxProblemHandler() {
|
||||
const conflict = ref<KbxConflictSnapshot | null>(null)
|
||||
const systemProblem = ref<KbxSystemProblem | null>(null)
|
||||
|
||||
function handle(problem: KbxProblem) {
|
||||
if (problem.type === 'conflict') {
|
||||
conflict.value = {
|
||||
code: problem.code,
|
||||
title: problem.title,
|
||||
currentVersion: problem.currentVersion,
|
||||
correlationId: problem.correlationId,
|
||||
}
|
||||
return
|
||||
}
|
||||
if (problem.type === 'system') {
|
||||
systemProblem.value = problem
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
conflict.value = null
|
||||
systemProblem.value = null
|
||||
}
|
||||
|
||||
return { conflict, systemProblem, handle, clear }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as signalR from '@microsoft/signalr'
|
||||
import type { KbxImportProgressEvent } from '@kbx/contracts'
|
||||
import type { KbxImportProgressConnection } from '@kbx/ui'
|
||||
|
||||
export function createImportProgressConnection(url = '/hubs/import-progress'): KbxImportProgressConnection {
|
||||
const hub = new signalR.HubConnectionBuilder()
|
||||
.withUrl(url)
|
||||
.withAutomaticReconnect()
|
||||
.build()
|
||||
|
||||
return {
|
||||
start: () => hub.state === signalR.HubConnectionState.Disconnected ? hub.start() : Promise.resolve(),
|
||||
stop: () => hub.stop(),
|
||||
subscribe: sessionId => hub.invoke('Subscribe', sessionId),
|
||||
unsubscribe: sessionId => hub.state === signalR.HubConnectionState.Connected
|
||||
? hub.invoke('Unsubscribe', sessionId)
|
||||
: Promise.resolve(),
|
||||
onProgress(callback) {
|
||||
hub.on('ImportProgress', (event: KbxImportProgressEvent) => callback(event))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { isKbxProblem, type KbxLookupProvider } from '@kbx/contracts'
|
||||
import { kbxApi } from '../http/generated/kbxApiClient'
|
||||
|
||||
const lookupOperations = {
|
||||
customer: { search: 'lookup.customers.search', byId: 'lookup.customers.resolveById', byCode: 'lookup.customers.resolveByCode' },
|
||||
item: { search: 'lookup.items.search', byId: 'lookup.items.resolveById', byCode: 'lookup.items.resolveByCode' },
|
||||
warehouse: { search: 'lookup.warehouses.search', byId: 'lookup.warehouses.resolveById', byCode: 'lookup.warehouses.resolveByCode' },
|
||||
} as const
|
||||
|
||||
type LookupEntity = keyof typeof lookupOperations
|
||||
|
||||
function provider(entity: LookupEntity): KbxLookupProvider<string> {
|
||||
const ops = lookupOperations[entity]
|
||||
return {
|
||||
search(request) {
|
||||
return kbxApi.request(ops.search, { query: request })
|
||||
},
|
||||
async resolveById(id) {
|
||||
try { return await kbxApi.request(ops.byId, { path: { id } }) }
|
||||
catch (error) { if (isKbxProblem(error) && error.type === 'not-found') return null; throw error }
|
||||
},
|
||||
async resolveByCode(code) {
|
||||
try { return await kbxApi.request(ops.byCode, { path: { code } }) }
|
||||
catch (error) { if (isKbxProblem(error) && error.type === 'not-found') return null; throw error }
|
||||
},
|
||||
} as KbxLookupProvider<string>
|
||||
}
|
||||
|
||||
export const lookupRegistry = {
|
||||
customer: provider('customer'),
|
||||
item: provider('item'),
|
||||
warehouse: provider('warehouse'),
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
KbxAuditTrail, KbxBulkActionBar, KbxButton, KbxCommandBar, KbxDateField, KbxDateRange,
|
||||
KbxInput, KbxFormGrid, KbxFormSection, KbxFormSpan, KbxMoneyField, KbxNumberField, KbxPageHeader, KbxProposalPanel, KbxQuantityField,
|
||||
KbxSelect, KbxStatus, KbxToast, KbxDataState, KbxTabs, KbxDataGrid, KbxSearchPanel, kbxComponentCatalog, kbxTemplateManifest,
|
||||
} from '@kbx/ui'
|
||||
import type { KbxAiProposal, KbxAuditEntry, KbxCommandDefinition, KbxDensity, KbxGridColumn, KbxSearchField, KbxValidationError } from '@kbx/contracts'
|
||||
|
||||
const density=ref<KbxDensity>((new URLSearchParams(location.search).get('density') as KbxDensity) || 'compact')
|
||||
const text=ref('대한상사')
|
||||
const number=ref<number|null>(1200)
|
||||
const money=ref<number|null>(1250000)
|
||||
const qty=ref<number|null>(10)
|
||||
const date=ref('2026-08-08')
|
||||
const from=ref('2026-08-01')
|
||||
const to=ref('2026-08-08')
|
||||
const status=ref('READY')
|
||||
const tab=ref('basic')
|
||||
|
||||
const search=ref<Record<string,unknown>>({keyword:'',status:null,mismatchOnly:false})
|
||||
const rememberSearch=ref(true)
|
||||
const searchFields:KbxSearchField[]=[
|
||||
{key:'keyword',label:'통합검색',type:'text',width:'lg',placeholder:'주문번호/주문자'},
|
||||
{key:'status',label:'상태',type:'select',options:[{value:'READY',label:'출고대기'},{value:'ERROR',label:'오류'}]},
|
||||
{key:'mismatchOnly',label:'오류만 보기',type:'checkbox',primary:false,defaultValue:false},
|
||||
]
|
||||
type DemoRow={id:string;orderNo:string;customerName:string;orderQty:number;amount:number;status:string}
|
||||
const gridRows=ref<DemoRow[]>([
|
||||
{id:'1',orderNo:'TEST-ORD-001',customerName:'대한상사',orderQty:10,amount:128000,status:'출고대기'},
|
||||
{id:'2',orderNo:'TEST-ORD-002',customerName:'서울유통',orderQty:8,amount:96000,status:'재고부족'},
|
||||
{id:'3',orderNo:'TEST-ORD-003',customerName:'부산물류',orderQty:4,amount:52000,status:'신규'},
|
||||
])
|
||||
const gridColumns:KbxGridColumn<DemoRow>[]=[
|
||||
{field:'orderNo',header:'주문번호',type:'link',width:150,pinned:'left'},
|
||||
{field:'customerName',header:'거래처',width:160},
|
||||
{field:'orderQty',header:'수량',type:'quantity',width:100,editable:true},
|
||||
{field:'amount',header:'금액',type:'money',width:130},
|
||||
{field:'status',header:'상태',type:'status',width:110},
|
||||
]
|
||||
const gridErrors:KbxValidationError[]=[{rowKey:'2',field:'orderQty',code:'INSUFFICIENT_STOCK',message:'출고 가능 수량은 6개입니다.'}]
|
||||
|
||||
const fastRows=ref<DemoRow[]>([
|
||||
{id:'F1',orderNo:'TEST-FAST-001',customerName:'대한상사',orderQty:1,amount:12000,status:'작성'},
|
||||
{id:'F2',orderNo:'TEST-FAST-002',customerName:'대한상사',orderQty:2,amount:24000,status:'작성'},
|
||||
{id:'F3',orderNo:'TEST-FAST-003',customerName:'대한상사',orderQty:3,amount:36000,status:'작성'},
|
||||
])
|
||||
let fastSeq=4
|
||||
function addFastRow(){fastRows.value.push({id:`F${fastSeq}`,orderNo:`TEST-FAST-${String(fastSeq++).padStart(3,'0')}`,customerName:'',orderQty:1,amount:0,status:'작성'})}
|
||||
function duplicateFastRows(rows:DemoRow[]){for(const row of rows)fastRows.value.push({...row,id:`F${fastSeq}`,orderNo:`TEST-FAST-${String(fastSeq++).padStart(3,'0')}`})}
|
||||
|
||||
const commands:KbxCommandDefinition[]=[
|
||||
{id:'search',label:'조회',group:'query',shortcut:'F3'},
|
||||
{id:'save',label:'저장',group:'edit',shortcut:'F8',variant:'primary'},
|
||||
{id:'ship',label:'출고지시',group:'workflow',requiresSelection:true},
|
||||
]
|
||||
const proposal:KbxAiProposal={id:'catalog-proposal',type:'warehouse-change',title:'출고창고 변경 제안',explanation:'가용재고가 있는 창고를 제안한 예시입니다.',capability:'draft',confidence:.87,proposedChanges:[{field:'warehouseId',label:'출고창고',before:'서울센터',after:'인천센터'}],evidence:[{label:'재고 Read Model',sourceType:'domain'}]}
|
||||
const audit:KbxAuditEntry[]=[{id:'1',occurredAt:'2026-08-08 15:32',actor:{type:'user',displayName:'홍길동'},action:'수량 변경',changes:[{field:'qty',label:'출고수량',before:10,after:8}],reason:'고객 요청'}]
|
||||
const groupCounts=computed(()=>Object.fromEntries([...new Set(kbxComponentCatalog.map(x=>x.group))].map(g=>[g,kbxComponentCatalog.filter(x=>x.group===g).length])))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="catalog" :data-kbx-density="density">
|
||||
<KbxPageHeader title="KBX 컴포넌트 카탈로그" breadcrumb="COMMON > KBX" description="Default / Changed / Warning / AI Suggested / Readonly / Disabled / Error / Loading / Empty / Keyboard 상태를 독립적으로 재현합니다." />
|
||||
<nav class="density" aria-label="밀도 선택"><strong>Density</strong><button v-for="d in ['compact','comfortable','touch']" :key="d" type="button" :aria-pressed="density===d" @click="density=d as KbxDensity">{{d}}</button></nav>
|
||||
|
||||
<section class="catalog__summary" aria-label="카탈로그 요약"><span v-for="(count,group) in groupCounts" :key="group"><strong>{{group}}</strong> {{count}}</span></section>
|
||||
|
||||
<section class="catalog__block"><h2>입력</h2><div class="catalog__grid"><div><h3>KbxInput 상태</h3><KbxInput v-model="text" label="거래처명" required/><KbxInput model-value="변경한 값" label="Changed" state="changed" help-text="저장 전 변경값입니다."/><KbxInput model-value="확인 필요" label="Warning" state="warning" warning="업무 규칙을 다시 확인하세요."/><KbxInput model-value="인천센터" label="AI Suggested" state="ai-suggested" help-text="AI 추천값이며 아직 반영되지 않았습니다."/><KbxInput model-value="" label="오류" error="거래처명을 입력하세요."/><KbxInput model-value="읽기전용" label="Readonly" readonly/></div><div><h3>Number / Money / Quantity</h3><KbxNumberField v-model="number" label="일반 숫자" state="changed"/><KbxMoneyField v-model="money" label="단가"/><KbxQuantityField v-model="qty" label="출고수량" :available-quantity="8"/></div><div><h3>Date / Range / Select</h3><KbxDateField v-model="date" label="주문일"/><KbxDateRange v-model:from="from" v-model:to="to" label="주문기간"/><KbxSelect v-model="status" label="상태" :options="[{value:'READY',label:'준비'},{value:'DONE',label:'완료'}]"/><KbxTabs v-model="tab" :items="[{key:'basic',label:'기본'},{key:'audit',label:'변경이력',badge:3}]"/></div></div></section>
|
||||
|
||||
<section class="catalog__block"><h2>Form Composition</h2><p class="catalog__hint">T02/T03 Desktop Form은 KbxFormSection + KbxFormGrid의 2-column을 기본으로 하고 주소·비고처럼 관계상 넓게 보여야 하는 항목만 명시적으로 full span 합니다.</p><KbxFormSection title="기본정보" description="Label 폭과 행/열 간격은 Design Token에서 통제합니다."><KbxFormGrid><KbxInput v-model="text" label="품목명"/><KbxDateField v-model="date" label="적용일"/><KbxFormSpan span="full"><KbxInput model-value="서울특별시 강남구 ..." label="주소" readonly/></KbxFormSpan></KbxFormGrid></KbxFormSection></section>
|
||||
|
||||
<section class="catalog__block"><h2>Search / Grid Runtime</h2><KbxSearchPanel v-model="search" v-model:remember-checked="rememberSearch" :fields="searchFields" remember saved-search/><div class="catalog__grid-demo"><KbxDataGrid :rows="gridRows" :columns="gridColumns" row-key="id" selection="multiple" editable :errors="gridErrors" :changed-cells="[{rowKey:'1',field:'orderQty'}]" :total-count="82415" allow-all-filtered-selection personalization exportable :summary="[{key:'count',label:'조회',kind:'count'},{key:'amount',label:'금액 합계',kind:'sum',field:'amount'}]"/></div></section>
|
||||
|
||||
<section class="catalog__block"><h2>Fast Entry Grid</h2><p class="catalog__hint">행 추가/복제, 선택 행 기준 Fill Down, Excel 다중 셀 붙여넣기 정규화, 오류 탐색을 동일 Grid 계약으로 재현합니다.</p><div class="catalog__grid-demo"><KbxDataGrid :rows="fastRows" :columns="gridColumns" row-key="id" selection="multiple" editable :editing-policy="{allowRowAdd:true,allowRowDuplicate:true,fillDown:true,paste:true,errorNavigation:true}" @row-add-requested="addFastRow" @row-duplicate-requested="duplicateFastRows"/></div></section>
|
||||
|
||||
<section class="catalog__block"><h2>Loading / Empty / Error</h2><div class="catalog__grid"><KbxDataState state="loading"/><KbxDataState state="empty" title="조회된 주문이 없습니다." detail="조회조건을 변경해 보세요."/><KbxDataState state="error" detail="네트워크 연결을 확인한 후 다시 시도하세요." action-label="다시 조회"/></div></section>
|
||||
|
||||
<section class="catalog__block"><h2>Command / 상태</h2><KbxCommandBar :commands="commands" :selection-count="0"/><KbxBulkActionBar :selection-count="17" :actions="[{id:'ship',label:'출고지시',group:'workflow'},{id:'hold',label:'보류',group:'workflow'}]"/><div class="status-row"><KbxStatus label="작성" semantic="draft"/><KbxStatus label="진행" semantic="processing"/><KbxStatus label="완료" semantic="completed"/><KbxStatus label="재고부족" semantic="warning"/><KbxStatus label="오류" semantic="error"/></div><KbxToast message="저장했습니다."/><KbxToast message="일부 항목을 확인하세요." kind="warning"/></section>
|
||||
|
||||
<section class="catalog__block"><h2>AI / Audit</h2><div class="catalog__grid"><KbxProposalPanel :proposal="proposal"/><KbxAuditTrail :entries="audit"/></div></section>
|
||||
|
||||
<section class="catalog__block"><h2>T01~T09 Template Contract</h2><p class="catalog__hint">Screen Type을 선택하면 필수 Surface·Core Component·Keyboard·완료 점검까지 함께 결정됩니다. 화면별 임의 Layout은 예외입니다.</p><table><thead><tr><th>Type</th><th>Component</th><th>필수 Surface</th><th>Core Components</th><th>완료 점검</th><th>Reference</th></tr></thead><tbody><tr v-for="template in kbxTemplateManifest" :key="template.code"><td><strong>{{template.code}}</strong><br><small>{{template.type}}</small></td><td>{{template.component}}<br><small>{{template.keyboard.join(' · ')}}</small></td><td>{{template.requiredSurfaces.join(' · ')}}</td><td>{{template.coreComponents.join(' · ')}}</td><td>{{template.completionChecks.join(' · ')}}</td><td>{{template.referenceScreens.join(', ')}}</td></tr></tbody></table></section>
|
||||
|
||||
<section class="catalog__block"><h2>검증 계약</h2><table><thead><tr><th>Component</th><th>Group</th><th>States</th><th>Keyboard</th><th>Focus</th></tr></thead><tbody><tr v-for="entry in kbxComponentCatalog" :key="entry.component"><td>{{entry.component}}</td><td>{{entry.group}}</td><td>{{entry.scenarios.map(x=>x.label).join(', ')}}</td><td>{{entry.accessibility.keyboard?'✓':'-'}}</td><td>{{entry.accessibility.focusVisible?'✓':'-'}}</td></tr></tbody></table></section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.catalog{display:grid;gap:var(--kbx-space-4);padding:var(--kbx-space-4);background:var(--kbx-color-surface);color:var(--kbx-color-text)}.density,.catalog__summary,.status-row{display:flex;align-items:center;gap:var(--kbx-space-2);flex-wrap:wrap}.density button{height:var(--kbx-control-height);border:1px solid var(--kbx-color-border);background:var(--kbx-color-surface);border-radius:var(--kbx-radius-sm);padding:0 10px}.density button[aria-pressed="true"]{border-color:var(--kbx-color-primary);color:var(--kbx-color-primary);font-weight:600}.catalog__summary span{padding:6px 8px;background:var(--kbx-color-surface-muted);border:1px solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm)}.catalog__block{display:grid;gap:var(--kbx-space-3);padding:var(--kbx-space-3);border:1px solid var(--kbx-color-border);border-radius:var(--kbx-radius-sm)}h2,h3{margin:0}.catalog__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--kbx-space-4)}.catalog__grid>div{display:grid;gap:var(--kbx-space-2)}.catalog__grid-demo{height:360px}.catalog__hint{margin:0;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}table{width:100%;border-collapse:collapse;font-size:var(--kbx-font-sm)}th,td{padding:7px 8px;border-bottom:1px solid var(--kbx-color-border);text-align:left}th{background:var(--kbx-color-surface-muted);font-weight:600}
|
||||
</style>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { defineKbxScreen } from '@kbx/ui'
|
||||
|
||||
export const designSystemCatalogScreen = defineKbxScreen({
|
||||
id:'COMMON-DS-001',
|
||||
version:'1.0.0',
|
||||
module:'COMMON',
|
||||
type:'list', templateCode:'T01',
|
||||
title:'KBX 컴포넌트 카탈로그',
|
||||
description:'공통 컴포넌트의 상태·밀도·키보드·접근성 계약을 재현합니다.',
|
||||
permissions:['kbx.design.read'],
|
||||
helpKey:'COMMON-DS-001',
|
||||
telemetry:{enabled:true},
|
||||
})
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { KbxListPage, KbxDataGrid, KbxDialog, KbxInput, KbxNumberField, KbxSelect, KbxButton } from '@kbx/ui'
|
||||
import type { KbxExperimentOverviewRow, KbxGridColumn } from '@kbx/contracts'
|
||||
import { experimentsScreen } from './experiments.definition'
|
||||
import { experimentApi } from './experimentApi'
|
||||
const selected=ref<KbxExperimentOverviewRow[]>([])
|
||||
const query=useQuery({queryKey:['kbx','experiments'],queryFn:experimentApi.overview})
|
||||
const rows=computed(()=>query.data.value?.items??[])
|
||||
const columns:KbxGridColumn<KbxExperimentOverviewRow>[]=[
|
||||
{field:'experimentId',header:'실험',width:280},{field:'screenId',header:'화면',width:140},{field:'state',header:'상태',type:'status',width:110},{field:'rolloutPercent',header:'배포 %',type:'integer',width:90},{field:'decision',header:'판정',width:150},{field:'updatedAt',header:'변경일시',type:'datetime',width:170},
|
||||
]
|
||||
const dialogOpen=ref(false),mode=ref<'rollout'|'rollback'>('rollout'),reason=ref(''),rolloutPercent=ref<number|null>(0),rolloutState=ref<string|null>('running'),error=ref('')
|
||||
function openManage(next:'rollout'|'rollback'){const item=selected.value[0];if(!item)return;mode.value=next;reason.value='';error.value='';rolloutPercent.value=item.rolloutPercent;rolloutState.value=item.state==='paused'?'paused':'running';dialogOpen.value=true}
|
||||
async function apply(){const item=selected.value[0];if(!item)return;if(!reason.value.trim()){error.value='변경 사유를 입력하세요.';return}if(mode.value==='rollout'){const percent=rolloutPercent.value??0;if(percent<0||percent>100){error.value='배포 비율은 0~100 사이여야 합니다.';return}await experimentApi.rollout(item.experimentId,{rolloutPercent:percent,state:(rolloutState.value==='paused'?'paused':'running'),reason:reason.value.trim()})}else await experimentApi.rollback(item.experimentId,{reason:reason.value.trim()});dialogOpen.value=false;selected.value=[];await query.refetch()}
|
||||
async function command(id:string){if(id==='search')await query.refetch();if(id==='rollout')openManage('rollout');if(id==='rollback')openManage('rollback')}
|
||||
</script>
|
||||
<template><KbxListPage :screen="experimentsScreen" :selection-count="selected.length" @command="command">
|
||||
<template #quick-filter><div class="notice"><strong>안전 범위</strong> — Layout·정보강조·기본필터·Navigation·Copy만 실험합니다. 권한·민감정보·Domain Rule·Validation·WMS Scan Rule은 실험하지 않습니다.</div></template>
|
||||
<template #content><KbxDataGrid :rows="rows" :columns="columns" row-key="experimentId" selection="single" :loading="query.isFetching.value" @selection-changed="value=>selected=value" /></template>
|
||||
<template #summary>판정은 운영 Threshold 기반입니다. 통계적 유의성을 자동 주장하지 않으며, Guardrail 위반은 즉시 롤백 대상으로 봅니다.</template>
|
||||
</KbxListPage>
|
||||
<KbxDialog :open="dialogOpen" :title="mode==='rollback'?'UX 변경 즉시 롤백':'점진배포 설정'" size="md" @update:open="dialogOpen=$event">
|
||||
<div class="dialog-fields">
|
||||
<template v-if="mode==='rollout'">
|
||||
<KbxNumberField v-model="rolloutPercent" label="배포 비율" suffix="%" :min="0" :max="100" />
|
||||
<KbxSelect v-model="rolloutState" label="상태" :options="[{value:'running',label:'실행'},{value:'paused',label:'일시중지'}]" />
|
||||
</template>
|
||||
<KbxInput v-model="reason" label="변경 사유" required :maxlength="500" :error="error" placeholder="지표·장애·VOC 등 변경 근거를 입력하세요." @enter="apply" />
|
||||
<p v-if="mode==='rollback'">롤백하면 Kill Switch가 활성화되고 모든 사용자는 즉시 Control UX로 돌아갑니다. 기존 Assignment와 Audit 이력은 유지됩니다.</p>
|
||||
</div>
|
||||
<template #footer><KbxButton label="취소" @click="dialogOpen=false"/><KbxButton :label="mode==='rollback'?'즉시 롤백':'적용'" :variant="mode==='rollback'?'danger':'primary'" @click="apply"/></template>
|
||||
</KbxDialog>
|
||||
</template>
|
||||
<style scoped>.notice{padding:var(--kbx-space-2);border:1px solid var(--kbx-color-border);background:var(--kbx-color-surface-muted)}.dialog-fields{display:grid;gap:var(--kbx-space-3)}.dialog-fields p{color:var(--kbx-color-text-muted)}</style>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { KbxExperimentOverviewResponse, KbxExperimentRolloutRequest, KbxExperimentRollbackRequest } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
export const experimentApi={
|
||||
overview:()=>kbxApi.request<KbxExperimentOverviewResponse>('common.experiments.overview'),
|
||||
rollout:(experimentId:string,body:KbxExperimentRolloutRequest)=>kbxApi.request('common.experiments.rollout',{path:{experimentId},body}),
|
||||
rollback:(experimentId:string,body:KbxExperimentRollbackRequest)=>kbxApi.request('common.experiments.rollback',{path:{experimentId},body}),
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { defineKbxScreen } from '@kbx/ui'
|
||||
export const experimentsScreen=defineKbxScreen({
|
||||
id:'COMMON-EXP-001',version:'1.0.0',module:'COMMON',type:'list', templateCode:'T01',title:'UX 실험·점진배포',
|
||||
description:'안전한 UX 변경만 점진 배포하고 가드레일 지표와 즉시 롤백 상태를 확인합니다.',
|
||||
permissions:['common.experiment.read'],helpKey:'COMMON-EXP-001',telemetry:{enabled:true},
|
||||
commands:[{id:'search',label:'조회',group:'query',shortcut:'F3'},{id:'rollout',label:'배포 설정',group:'workflow',permission:'common.experiment.manage',requiresSelection:true,minSelection:1,maxSelection:1},{id:'rollback',label:'즉시 롤백',group:'workflow',variant:'danger',permission:'common.experiment.manage',requiresSelection:true,minSelection:1,maxSelection:1}],
|
||||
})
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { KbxListPage, KbxDataGrid } from '@kbx/ui'
|
||||
import type { KbxExternalDataStatusRow, KbxGridColumn } from '@kbx/contracts'
|
||||
import { externalDataScreen } from './external-data.definition'
|
||||
import { externalDataApi } from './externalDataApi'
|
||||
const selected=ref<KbxExternalDataStatusRow[]>([])
|
||||
const query=useQuery({queryKey:['kbx','external-data','status'],queryFn:externalDataApi.status})
|
||||
const rows=computed(()=>query.data.value?.items??[])
|
||||
const columns:KbxGridColumn<KbxExternalDataStatusRow>[]=[
|
||||
{field:'sourceLabel',header:'출처',width:110,pinned:'left'},
|
||||
{field:'datasetId',header:'데이터셋',width:320},
|
||||
{field:'state',header:'신선도',type:'status',width:130},
|
||||
{field:'cacheEntries',header:'캐시',type:'integer',width:90},
|
||||
{field:'staleEntries',header:'Stale',type:'integer',width:90},
|
||||
{field:'unavailableEntries',header:'사용불가',type:'integer',width:100},
|
||||
{field:'lastReceivedAt',header:'최근 수신',type:'datetime',width:170},
|
||||
{field:'oldestFreshUntil',header:'최초 만료',type:'datetime',width:170},
|
||||
]
|
||||
async function command(id:string){if(id==='search')await query.refetch();if(id==='refresh'){const row=selected.value[0];if(!row)return;await externalDataApi.refresh(row.datasetId);selected.value=[];await query.refetch()}}
|
||||
</script>
|
||||
<template><KbxListPage :screen="externalDataScreen" :selection-count="selected.length" @command="command">
|
||||
<template #quick-filter><div class="notice"><strong>외부 데이터는 업무 원장이 아닙니다.</strong> 화면에는 정규화된 Projection과 출처·신선도를 함께 표시하고, 만료된 데이터를 최신값처럼 사용하지 않습니다.</div></template>
|
||||
<template #content><KbxDataGrid :rows="rows" :columns="columns" row-key="datasetId" selection="single" :loading="query.isFetching.value" @selection-changed="value=>selected=value" /></template>
|
||||
<template #summary>KRX의 승인 서비스별 TTL은 별도 정책 없이는 재수집할 수 없습니다. OPENDART/KIS의 캐시 시간은 KBX 운영정책이며 공급자 공식 호출한도와 구분합니다.</template>
|
||||
</KbxListPage></template>
|
||||
<style scoped>.notice{padding:var(--kbx-space-2);border:1px solid var(--kbx-color-border);background:var(--kbx-color-surface-muted)}</style>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { defineKbxScreen } from '@kbx/ui'
|
||||
export const externalDataScreen=defineKbxScreen({
|
||||
id:'COMMON-DATA-001',version:'1.0.0',module:'COMMON',type:'list', templateCode:'T01',title:'외부 데이터 상태',
|
||||
description:'KRX·OPENDART·KIS 외부 데이터의 출처·수신시각·신선도·캐시 상태를 확인합니다.',
|
||||
permissions:['common.external-data.read'],helpKey:'COMMON-DATA-001',telemetry:{enabled:true},
|
||||
commands:[
|
||||
{id:'search',label:'조회',group:'query',shortcut:'F3'},
|
||||
{id:'refresh',label:'재수집',group:'workflow',permission:'common.external-data.refresh',requiresSelection:true,minSelection:1,maxSelection:1},
|
||||
],
|
||||
})
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { KbxExternalDataStatusResponse } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
export const externalDataApi={
|
||||
status:()=>kbxApi.request<KbxExternalDataStatusResponse>('common.externalData.status'),
|
||||
refresh:(datasetId:string)=>kbxApi.request('common.externalData.refresh',{path:{datasetId}}),
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<template><div aria-hidden="true" /></template>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { KbxRouteNotFound } from '@kbx/ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useKbxWorkspaceStore } from '../../../shell/workspaceStore'
|
||||
const router=useRouter();const store=useKbxWorkspaceStore()
|
||||
function home(){void router.replace('/home')}
|
||||
</script>
|
||||
<template><KbxRouteNotFound @home="home" @menu-search="store.menuSearchOpen=true"/></template>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import {
|
||||
KbxDataGrid,
|
||||
KbxExceptionCenter,
|
||||
KbxSearchPanel,
|
||||
KbxWorkQueuePage,
|
||||
useKbxPageShortcuts,
|
||||
type KbxWorkItem,
|
||||
type KbxWorkItemAction,
|
||||
} from '@kbx/ui'
|
||||
import { operationsApi, type OperationsFilter } from './operationsApi'
|
||||
import { kbxTelemetry } from '../../../telemetry/kbxTelemetryClient'
|
||||
import { operationsColumns, operationsQueueScreen, operationsSearchFields } from './operations.definition'
|
||||
|
||||
const searchModel = ref<Record<string, unknown>>({ status: 'open' })
|
||||
const applied = ref<OperationsFilter>({ status: 'open', page: 1, pageSize: 200 })
|
||||
const selectedRows = ref<KbxWorkItem[]>([])
|
||||
const detail = ref<KbxWorkItem | null>(null)
|
||||
const quickCode = ref<string | null>(null)
|
||||
const pageContext=computed(()=>({label:'예외 업무 Queue',hint:'정상 건은 제외하고 해결이 필요한 업무만 표시합니다.',metrics:[{key:'total',label:'대상',value:query.data.value?.totalCount??0},{key:'selected',label:'선택',value:selectedRows.value.length}]}))
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: computed(() => ['operations', 'work-items', applied.value]),
|
||||
queryFn: () => operationsApi.search(applied.value),
|
||||
})
|
||||
|
||||
useKbxPageShortcuts([{ key: 'F3', execute: () => search() }])
|
||||
|
||||
async function search() {
|
||||
selectedRows.value = []
|
||||
applied.value = {
|
||||
module: asString(searchModel.value.module),
|
||||
severity: asString(searchModel.value.severity),
|
||||
status: asString(searchModel.value.status),
|
||||
owner: asString(searchModel.value.owner),
|
||||
keyword: asString(searchModel.value.keyword),
|
||||
code: quickCode.value,
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
}
|
||||
await query.refetch()
|
||||
}
|
||||
|
||||
function asString(value: unknown) { return value == null || value === '' ? null : String(value) }
|
||||
|
||||
async function selectQuickFilter(code: string | null) {
|
||||
quickCode.value = code
|
||||
await search()
|
||||
}
|
||||
|
||||
async function executeCommand(id: string) {
|
||||
const ids = selectedRows.value.map(x => x.id)
|
||||
if (id === 'search') return search()
|
||||
if (id === 'claim' && ids.length) { await operationsApi.claim(ids); return query.refetch() }
|
||||
}
|
||||
|
||||
async function executeAction(action: KbxWorkItemAction) {
|
||||
if (!detail.value) return
|
||||
if (action.kind === 'claim') await operationsApi.claim([detail.value.id])
|
||||
if (action.kind === 'resolve') { const item=detail.value; await operationsApi.resolve([item.id]); kbxTelemetry.track('exception.resolved',{screenId:operationsQueueScreen.id,screenVersion:operationsQueueScreen.version,durationMs:Math.max(0,item.ageMinutes*60000),attributes:{exceptionType:item.code,resolutionType:'manual'}}) }
|
||||
if (action.kind === 'retry') await operationsApi.retry(detail.value.id)
|
||||
if (action.kind === 'navigate') window.dispatchEvent(new CustomEvent('kbx:navigate-source', { detail: detail.value }))
|
||||
detail.value = null
|
||||
await query.refetch()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KbxWorkQueuePage
|
||||
:screen="operationsQueueScreen"
|
||||
:selection-count="selectedRows.length"
|
||||
:context="pageContext"
|
||||
:content-state="query.error.value?'error':query.isFetching.value&&!query.data.value?'loading':query.data.value?.totalCount===0?'empty':'ready'"
|
||||
:refreshing="query.isFetching.value&&Boolean(query.data.value)"
|
||||
@command="executeCommand"
|
||||
>
|
||||
<template #search>
|
||||
<KbxSearchPanel v-model="searchModel" :fields="operationsSearchFields" @search="search" />
|
||||
</template>
|
||||
|
||||
<template #queue-summary>
|
||||
<span class="queue-hint">정상 건은 표시하지 않습니다. 중요도와 경과시간이 높은 예외부터 처리합니다.</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<KbxExceptionCenter
|
||||
:counters="query.data.value?.counters ?? []"
|
||||
:active-key="quickCode"
|
||||
:selected-item="detail"
|
||||
@filter="selectQuickFilter"
|
||||
@close-detail="detail = null"
|
||||
@action="executeAction"
|
||||
>
|
||||
<KbxDataGrid
|
||||
:rows="query.data.value?.items ?? []"
|
||||
:columns="operationsColumns"
|
||||
row-key="id"
|
||||
selection="multiple"
|
||||
:loading="query.isFetching.value"
|
||||
@selection-changed="selectedRows = $event"
|
||||
@row-double-clicked="detail = $event"
|
||||
/>
|
||||
</KbxExceptionCenter>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
전체 {{ query.data.value?.totalCount ?? 0 }}건 · 선택 {{ selectedRows.length }}건
|
||||
</template>
|
||||
|
||||
</KbxWorkQueuePage>
|
||||
</template>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { defineKbxScreen, type KbxGridColumn, type KbxSearchField, type KbxWorkItem } from '@kbx/ui'
|
||||
|
||||
export const operationsQueueScreen = defineKbxScreen({
|
||||
id: 'COMMON-OPS-001',
|
||||
version: '1.0.0',
|
||||
module: 'COMMON',
|
||||
type: 'queue', templateCode:'T06',
|
||||
title: '업무 예외 센터',
|
||||
description: '정상 건이 아니라 사람이 판단하거나 복구해야 할 업무만 모아 처리합니다.',
|
||||
permissions: ['common.operations.read'],
|
||||
helpKey: 'COMMON-OPS-001',
|
||||
telemetry: { enabled: true },
|
||||
commands: [
|
||||
{ id: 'search', label: '조회', group: 'query', shortcut: 'F3' },
|
||||
{ id: 'claim', label: '내가 처리', group: 'workflow', requiresSelection: true, minSelection: 1, permission: 'common.operations.claim' },
|
||||
{ id: 'excel', label: '엑셀', group: 'output' },
|
||||
],
|
||||
})
|
||||
|
||||
export const operationsSearchFields: KbxSearchField[] = [
|
||||
{ key: 'module', label: '모듈', type: 'select', primary: true, options: [
|
||||
{ value: 'OMS', label: 'OMS' }, { value: 'WMS', label: 'WMS' }, { value: 'ERP', label: 'ERP' },
|
||||
] },
|
||||
{ key: 'severity', label: '심각도', type: 'select', primary: true, options: [
|
||||
{ value: 'critical', label: '긴급' }, { value: 'warning', label: '주의' }, { value: 'info', label: '정보' },
|
||||
] },
|
||||
{ key: 'status', label: '상태', type: 'select', primary: true, options: [
|
||||
{ value: 'open', label: '미처리' }, { value: 'claimed', label: '처리중' }, { value: 'resolved', label: '해결' },
|
||||
] },
|
||||
{ key: 'owner', label: '담당', type: 'select', primary: false, options: [
|
||||
{ value: 'mine', label: '내 업무' }, { value: 'unassigned', label: '미지정' },
|
||||
] },
|
||||
{ key: 'keyword', label: '검색', type: 'text', primary: true, width: 'lg', placeholder: '주문번호, 작업번호, 오류명' },
|
||||
]
|
||||
|
||||
export const operationsColumns: KbxGridColumn<KbxWorkItem>[] = [
|
||||
{ field: 'severity', header: '중요도', type: 'status', width: 82, pinned: 'left' },
|
||||
{ field: 'sourceModule', header: '모듈', width: 76 },
|
||||
{ field: 'title', header: '확인할 업무', width: 250, pinned: 'left' },
|
||||
{ field: 'referenceNo', header: '대상번호', type: 'code', width: 150 },
|
||||
{ field: 'detail', header: '원인/안내', width: 300 },
|
||||
{ field: 'ownerName', header: '담당자', width: 100 },
|
||||
{ field: 'occurredAt', header: '발생시각', type: 'datetime', width: 160 },
|
||||
{ field: 'ageMinutes', header: '경과(분)', type: 'integer', width: 90 },
|
||||
{ field: 'status', header: '처리상태', type: 'status', width: 100 },
|
||||
]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import type { KbxWorkQueueResult } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
|
||||
export interface OperationsFilter {
|
||||
module?: string | null
|
||||
severity?: string | null
|
||||
status?: string | null
|
||||
owner?: string | null
|
||||
code?: string | null
|
||||
keyword?: string | null
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export const operationsApi = {
|
||||
search(filter: OperationsFilter) {
|
||||
return kbxApi.request<KbxWorkQueueResult>('common.operations.search', { query: filter })
|
||||
},
|
||||
claim(ids: string[]) {
|
||||
return kbxApi.request<unknown, { ids: string[] }>('common.operations.claim', { body: { ids } })
|
||||
},
|
||||
resolve(ids: string[], reason = '사용자 확인 완료') {
|
||||
return kbxApi.request<unknown, { ids: string[]; reason: string }>('common.operations.resolve', { body: { ids, reason } })
|
||||
},
|
||||
retry(id: string) {
|
||||
return kbxApi.request<unknown>('common.operations.retry', { path: { id } })
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const operationsRoutes = [
|
||||
{ path: '/operations/exceptions', name: 'common-operations-exceptions', component: () => import('./OperationsQueuePage.vue') },
|
||||
]
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { KbxDataGrid, KbxReconcilePage, KbxSearchPanel, useKbxPageShortcuts, type KbxReconcileItem } from '@kbx/ui'
|
||||
import { reconcileApi, type ReconcileFilter } from './reconcileApi'
|
||||
import { reconcileColumns, reconcileScreen, reconcileSearchFields } from './reconcile.definition'
|
||||
|
||||
const searchModel = ref<Record<string, unknown>>({ status: 'mismatch' })
|
||||
const applied = ref<ReconcileFilter>({ status: 'mismatch', page: 1, pageSize: 200 })
|
||||
const selectedRows = ref<KbxReconcileItem[]>([])
|
||||
const query = useQuery({ queryKey: computed(() => ['reconcile', applied.value]), queryFn: () => reconcileApi.search(applied.value) })
|
||||
const pageContext=computed(()=>({label:'대사 결과',hint:'원천값을 직접 수정하지 않고 불일치는 해결 업무로 전환합니다.',metrics:[{key:'mismatch',label:'불일치',value:query.data.value?.summary.mismatchCount??0,tone:'danger' as const,emphasis:true},{key:'selected',label:'선택',value:selectedRows.value.length}]}))
|
||||
useKbxPageShortcuts([{ key: 'F3', execute: () => search() }])
|
||||
|
||||
function stringOrNull(value: unknown) { return value == null || value === '' ? null : String(value) }
|
||||
async function search() {
|
||||
selectedRows.value = []
|
||||
applied.value = {
|
||||
reconcileType: stringOrNull(searchModel.value.reconcileType),
|
||||
status: stringOrNull(searchModel.value.status),
|
||||
keyword: stringOrNull(searchModel.value.keyword),
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
}
|
||||
await query.refetch()
|
||||
}
|
||||
async function command(id: string) {
|
||||
if (id === 'search') return search()
|
||||
if (id === 'createException' && selectedRows.value.length) {
|
||||
await reconcileApi.createExceptions(selectedRows.value.map(x => x.id))
|
||||
return query.refetch()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KbxReconcilePage :screen="reconcileScreen" :summary="query.data.value?.summary" :selection-count="selectedRows.length" :context="pageContext" :content-state="query.error.value?'error':query.isFetching.value&&!query.data.value?'loading':query.data.value?.summary.totalCount===0?'empty':'ready'" :refreshing="query.isFetching.value&&Boolean(query.data.value)" @command="command">
|
||||
<template #search><KbxSearchPanel v-model="searchModel" :fields="reconcileSearchFields" @search="search" /></template>
|
||||
<template #content>
|
||||
<KbxDataGrid
|
||||
:rows="query.data.value?.items ?? []"
|
||||
:columns="reconcileColumns"
|
||||
row-key="id"
|
||||
selection="multiple"
|
||||
:loading="query.isFetching.value"
|
||||
@selection-changed="selectedRows = $event"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>불일치 건은 원천 데이터를 직접 수정하지 않고 예외 업무로 전환해 담당자가 원인을 확인합니다.</template>
|
||||
</KbxReconcilePage>
|
||||
</template>
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { defineKbxScreen, type KbxGridColumn, type KbxReconcileItem, type KbxSearchField } from '@kbx/ui'
|
||||
|
||||
export const reconcileScreen = defineKbxScreen({
|
||||
id: 'COMMON-REC-001',
|
||||
version: '1.0.0',
|
||||
module: 'COMMON',
|
||||
type: 'reconcile', templateCode:'T07',
|
||||
title: '업무 데이터 대사',
|
||||
description: '원천·대상 시스템의 기대값과 실제값을 비교하고 불일치 원인을 추적합니다.',
|
||||
permissions: ['common.reconcile.read'],
|
||||
helpKey: 'COMMON-REC-001',
|
||||
telemetry: { enabled: true },
|
||||
commands: [
|
||||
{ id: 'search', label: '조회', group: 'query', shortcut: 'F3' },
|
||||
{ id: 'createException', label: '예외 등록', group: 'workflow', requiresSelection: true, minSelection: 1, permission: 'common.operations.create' },
|
||||
{ id: 'excel', label: '엑셀', group: 'output' },
|
||||
],
|
||||
})
|
||||
|
||||
export const reconcileSearchFields: KbxSearchField[] = [
|
||||
{ key: 'reconcileType', label: '대사유형', type: 'select', primary: true, options: [
|
||||
{ value: 'OMS_WMS_OUTBOUND_QTY', label: 'OMS↔WMS 출고수량' },
|
||||
{ value: 'ORDER_PICK_QTY', label: '주문↔피킹수량' },
|
||||
{ value: 'INVENTORY_SNAPSHOT', label: '재고 스냅샷' },
|
||||
] },
|
||||
{ key: 'status', label: '결과', type: 'select', primary: true, options: [
|
||||
{ value: 'mismatch', label: '불일치' }, { value: 'pending', label: '확인중' }, { value: 'matched', label: '정상' }, { value: 'resolved', label: '해결' },
|
||||
] },
|
||||
{ key: 'keyword', label: '검색', type: 'text', primary: true, width: 'lg', placeholder: '주문번호, 품목, 참조번호' },
|
||||
]
|
||||
|
||||
export const reconcileColumns: KbxGridColumn<KbxReconcileItem>[] = [
|
||||
{ field: 'referenceNo', header: '참조번호', type: 'code', width: 160, pinned: 'left' },
|
||||
{ field: 'sourceLabel', header: '기준', width: 130 },
|
||||
{ field: 'expectedValue', header: '기대값', width: 130 },
|
||||
{ field: 'targetLabel', header: '비교대상', width: 130 },
|
||||
{ field: 'actualValue', header: '실제값', width: 130 },
|
||||
{ field: 'differenceValue', header: '차이', width: 110 },
|
||||
{ field: 'reasonText', header: '원인', width: 280 },
|
||||
{ field: 'status', header: '상태', type: 'status', width: 100 },
|
||||
{ field: 'occurredAt', header: '확인시각', type: 'datetime', width: 160 },
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { KbxReconcileResult } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
|
||||
export interface ReconcileFilter {
|
||||
reconcileType?: string | null
|
||||
status?: string | null
|
||||
keyword?: string | null
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export const reconcileApi = {
|
||||
search(filter: ReconcileFilter) {
|
||||
return kbxApi.request<KbxReconcileResult>('common.reconcile.search', { query: filter })
|
||||
},
|
||||
createExceptions(ids: string[]) {
|
||||
return kbxApi.request<unknown, { ids: string[] }>('common.reconcile.createExceptions', { body: { ids } })
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const reconcileRoutes = [
|
||||
{ path: '/operations/reconcile', name: 'common-reconcile', component: () => import('./ReconcilePage.vue') },
|
||||
]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { KbxListPage, KbxSearchPanel, KbxDataGrid } from '@kbx/ui'
|
||||
import type { KbxGridColumn, KbxSearchField, KbxUxMetricRow } from '@kbx/contracts'
|
||||
import { uxMetricsScreen } from './ux-metrics.definition'
|
||||
import { uxMetricsApi } from './uxMetricsApi'
|
||||
const today=new Date();const fromDate=new Date(today);fromDate.setDate(today.getDate()-6)
|
||||
const iso=(d:Date)=>d.toISOString().slice(0,10)
|
||||
const search=reactive({from:iso(fromDate),to:iso(today),screenId:''})
|
||||
const fields:KbxSearchField[]=[{key:'from',label:'시작일',type:'date',primary:true},{key:'to',label:'종료일',type:'date',primary:true},{key:'screenId',label:'화면코드',type:'text',primary:true,width:'md'}]
|
||||
const query=useQuery({queryKey:['kbx','ux-metrics',search],queryFn:()=>uxMetricsApi.get({...search,screenId:search.screenId||undefined}),enabled:false})
|
||||
const columns:KbxGridColumn<KbxUxMetricRow>[]=[
|
||||
{field:'label',header:'지표',width:240},{field:'value',header:'현재값',type:'decimal',width:140},{field:'unit',header:'단위',width:100},{field:'sampleCount',header:'표본',type:'integer',width:110},{field:'p50',header:'P50',type:'decimal',width:110},{field:'p95',header:'P95',type:'decimal',width:110},
|
||||
]
|
||||
async function command(id:string){if(id==='search')await query.refetch()}
|
||||
</script>
|
||||
<template>
|
||||
<KbxListPage :screen="uxMetricsScreen" :content-state="query.error.value?'error':query.isFetching.value&&!query.data.value?'loading':!query.data.value?'idle':query.data.value.metrics.length===0?'empty':'ready'" :refreshing="query.isFetching.value&&Boolean(query.data.value)" @command="command">
|
||||
<template #search><KbxSearchPanel v-model="search" :fields="fields" @search="query.refetch()" /></template>
|
||||
<template #quick-filter>
|
||||
<div class="notice"><strong>해석 기준</strong> — DOM 전체 클릭이나 검색어 원문을 수집하지 않습니다. 낮은 수치 자체보다 업무 위험·오류 감소와 함께 판단합니다.</div>
|
||||
</template>
|
||||
<template #content><KbxDataGrid :rows="query.data.value?.metrics ?? []" :columns="columns" row-key="metricKey" :loading="query.isFetching.value" /></template>
|
||||
<template #summary>{{ search.from }} ~ {{ search.to }} · Manual Intervention Rate를 자동화 개선의 핵심 추세로 봅니다.</template>
|
||||
</KbxListPage>
|
||||
</template>
|
||||
<style scoped>.notice{padding:8px 12px;border:1px solid var(--kbx-color-border);background:var(--kbx-color-surface-muted);font-size:13px}</style>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { defineKbxScreen } from '@kbx/ui'
|
||||
export const uxMetricsScreen=defineKbxScreen({
|
||||
id:'COMMON-UX-001',version:'1.0.0',module:'COMMON',type:'list', templateCode:'T01',title:'UX 품질 지표',
|
||||
description:'KBX 의미 이벤트와 업무 결과를 이용해 수작업 개입·업무시간·오류율을 검토합니다.',
|
||||
permissions:['common.ux.read'],helpKey:'COMMON-UX-001',telemetry:{enabled:true},
|
||||
commands:[{id:'search',label:'조회',group:'query',shortcut:'F3'}],
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { KbxUxMetricsResponse } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
export const uxMetricsApi={
|
||||
get:(query:{from:string;to:string;screenId?:string})=>kbxApi.request<KbxUxMetricsResponse>('common.uxTelemetry.metrics',{query}),
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { KbxDataGrid, KbxDateField, KbxFormGrid, KbxFormSection, KbxLookup, KbxTransactionPage, KbxWorkflowBar } from '@kbx/ui'
|
||||
import { inventoryMoveColumns, inventoryMoveScreen, inventoryMoveWorkflow, type InventoryMoveLine } from './inventory-move.definition'
|
||||
const status=ref('DRAFT'); const header=reactive({moveDate:new Date().toISOString().slice(0,10),fromWarehouseId:null as string|null,toWarehouseId:null as string|null}); const lines=ref<InventoryMoveLine[]>([])
|
||||
function command(id:string){ const t=inventoryMoveWorkflow.transitions.find(x=>x.id===id && x.from.includes(status.value)); if(t) status.value=t.to }
|
||||
</script>
|
||||
<template><KbxTransactionPage :screen="inventoryMoveScreen" :status="status" :summary-items="[{key:'items',label:'품목',value:`${lines.length}종`}]" @command="command"><template #header><KbxWorkflowBar :workflow="inventoryMoveWorkflow" :current="status" @transition="command"/><KbxFormSection title="이동정보"><KbxFormGrid><KbxDateField v-model="header.moveDate" label="이동일" required/><KbxLookup v-model="header.fromWarehouseId" entity="warehouse" label="출발창고" required/><KbxLookup v-model="header.toWarehouseId" entity="warehouse" label="도착창고" required/></KbxFormGrid></KbxFormSection></template><template #detail><KbxDataGrid :rows="lines" :columns="inventoryMoveColumns" row-key="clientId" editable clipboard/></template></KbxTransactionPage></template>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
|
||||
import { defineKbxScreen, type KbxGridColumn, type KbxWorkflowDefinition } from '@kbx/ui'
|
||||
export interface InventoryMoveLine { clientId:string; itemId:string|null; itemCode:string; itemName:string; availableQty:number; moveQty:number; lotNo:string; remark:string }
|
||||
export const inventoryMoveScreen=defineKbxScreen({ id:'ERP-INV-MOVE-001',version:'1.0.0',module:'ERP',type:'transaction', templateCode:'T03',title:'재고이동',helpKey:'ERP-INV-MOVE-001',permissions:['erp.inventory.move.read'],telemetry:{enabled:true}, description:'창고 간 재고이동을 등록하고 출고·입고 상태를 추적합니다.', commands:[
|
||||
{id:'new',label:'신규',group:'edit'},{id:'save',label:'저장',group:'edit',shortcut:'F8',permission:'erp.inventory.move.write'},{id:'confirm',label:'이동확정',group:'workflow',variant:'primary',permission:'erp.inventory.move.confirm'},{id:'excel',label:'엑셀',group:'output'}] })
|
||||
export const inventoryMoveColumns:KbxGridColumn<InventoryMoveLine>[]=[
|
||||
{field:'itemCode',header:'품목코드',type:'lookup',lookup:{entity:'item'},width:130,editable:true,pinned:'left'},{field:'itemName',header:'품목명',width:200},{field:'availableQty',header:'가용재고',type:'quantity',width:100},{field:'moveQty',header:'이동수량',type:'quantity',width:100,editable:true},{field:'lotNo',header:'LOT',type:'code',width:120,editable:true},{field:'remark',header:'비고',width:220,editable:true}]
|
||||
export const inventoryMoveWorkflow:KbxWorkflowDefinition={id:'erp.inventory-move',version:'1.0.0',states:[{value:'DRAFT',label:'작성',semantic:'draft'},{value:'CONFIRMED',label:'확정',semantic:'pending'},{value:'IN_TRANSIT',label:'이동중',semantic:'processing'},{value:'RECEIVED',label:'입고완료',semantic:'completed',terminal:true},{value:'CANCELLED',label:'취소',semantic:'cancelled',terminal:true}],transitions:[{id:'confirm',from:['DRAFT'],to:'CONFIRMED',label:'이동확정',permission:'erp.inventory.move.confirm',confirm:true},{id:'ship',from:['CONFIRMED'],to:'IN_TRANSIT',label:'출고',permission:'erp.inventory.move.ship'},{id:'receive',from:['IN_TRANSIT'],to:'RECEIVED',label:'입고완료',permission:'erp.inventory.move.receive',confirm:true}]}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
export const erpInventoryMoveRoutes: RouteRecordRaw[] = [
|
||||
{ path:'/erp/inventory-moves/new', name:'erp-inventory-move-new', component:()=>import('./InventoryMovePage.vue'), meta:{ screenId:'ERP-INV-MOVE-001' } },
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { KbxDataGrid, KbxDrawer, KbxMasterDetailPage, KbxSearchPanel } from '@kbx/ui'
|
||||
import { inventoryApi } from './inventoryApi'
|
||||
import { inventoryHistoryColumns, inventoryItemColumns, inventoryLocationColumns, inventoryScreen, inventorySearchFields, type InventoryItemRow } from './inventory.definition'
|
||||
|
||||
const appliedKeyword=ref('')
|
||||
const searchModel=ref<Record<string, unknown>>({ keyword: '' })
|
||||
const selectedItemId=ref<string|null>(null)
|
||||
const selectedItem=ref<InventoryItemRow|null>(null)
|
||||
const breakdownOpen=ref(false)
|
||||
const itemsQuery=useQuery({queryKey:computed(()=>['erp','inventory',appliedKeyword.value]),queryFn:()=>inventoryApi.search(appliedKeyword.value),enabled:false})
|
||||
const locationsQuery=useQuery({queryKey:computed(()=>['erp','inventory',selectedItemId.value,'locations']),queryFn:()=>inventoryApi.locations(selectedItemId.value!),enabled:computed(()=>Boolean(selectedItemId.value))})
|
||||
const historyQuery=useQuery({queryKey:computed(()=>['erp','inventory',selectedItemId.value,'history']),queryFn:()=>inventoryApi.history(selectedItemId.value!),enabled:computed(()=>Boolean(selectedItemId.value))})
|
||||
|
||||
async function search(){
|
||||
const previous=selectedItemId.value; appliedKeyword.value=String(searchModel.value.keyword??'').trim(); await itemsQuery.refetch()
|
||||
const items=itemsQuery.data.value?.items??[]; const next=items.find(x=>x.itemId===previous)??items[0]??null; activate(next)
|
||||
}
|
||||
function activate(row:InventoryItemRow|null){ selectedItem.value=row; selectedItemId.value=row?.itemId??null }
|
||||
function select(rows:InventoryItemRow[]){ activate(rows[0]??null) }
|
||||
function drill(row:InventoryItemRow){ activate(row); breakdownOpen.value=true }
|
||||
function command(id:string){if(id==='search')return search()}
|
||||
const contextText=computed(()=>selectedItem.value?`${selectedItem.value.itemCode} · ${selectedItem.value.itemName}${selectedItem.value.specification?` · ${selectedItem.value.specification}`:''}`:'품목을 선택하면 로케이션과 재고이력을 함께 조회합니다.')
|
||||
const pageContext=computed(()=>({label:contextText.value,hint:selectedItem.value?'선택 품목 기준으로 위치와 이력을 동기화합니다.':'Master 품목을 선택하세요.',metrics:selectedItem.value?[{key:'onhand',label:'현재고',value:selectedItem.value.totalQty},{key:'available',label:'가용',value:selectedItem.value.availableQty,emphasis:true}]:[]}))
|
||||
|
||||
const summaryItems=computed(()=>[
|
||||
{key:'items',label:'조회 품목',value:itemsQuery.data.value?.totalCount??0},
|
||||
{key:'selected',label:'선택 품목',value:selectedItem.value?.itemCode??'-'},
|
||||
])
|
||||
const drawerSummary=computed(()=>selectedItem.value?[
|
||||
{key:'onhand',label:'현재고',value:selectedItem.value.totalQty},
|
||||
{key:'allocated',label:'할당',value:selectedItem.value.allocatedQty},
|
||||
{key:'hold',label:'보류',value:selectedItem.value.holdQty},
|
||||
{key:'available',label:'가용',value:selectedItem.value.availableQty,emphasis:true},
|
||||
]:[])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KbxMasterDetailPage :screen="inventoryScreen" breadcrumb="ERP > 재고" master-title="품목" detail-title="창고/로케이션" bottom-title="재고이력" :context="pageContext" :summary-items="summaryItems" :content-state="itemsQuery.error.value?'error':itemsQuery.isFetching.value&&!itemsQuery.data.value?'loading':!itemsQuery.data.value?'idle':itemsQuery.data.value.totalCount===0?'empty':'ready'" :refreshing="itemsQuery.isFetching.value&&Boolean(itemsQuery.data.value)" @command="command">
|
||||
<template #search><KbxSearchPanel v-model="searchModel" :fields="inventorySearchFields" @search="search" /></template>
|
||||
<template #master>
|
||||
<KbxDataGrid :rows="itemsQuery.data.value?.items??[]" :columns="inventoryItemColumns" row-key="itemId" selection="single" :active-row-key="selectedItemId" :loading="itemsQuery.isFetching.value" :error-text="itemsQuery.error.value?'재고를 조회하지 못했습니다.':''" @selection-changed="select" @row-double-clicked="drill" @drill-down-requested="drill" @retry="search" />
|
||||
</template>
|
||||
<template #detail>
|
||||
<KbxDataGrid :rows="locationsQuery.data.value?.items??[]" :columns="inventoryLocationColumns" row-key="key" :loading="locationsQuery.isFetching.value" :error-text="locationsQuery.error.value?'재고 위치를 조회하지 못했습니다.':''" empty-text="선택한 품목의 재고 위치가 없습니다." @retry="()=>locationsQuery.refetch()" />
|
||||
</template>
|
||||
<template #bottom>
|
||||
<KbxDataGrid :rows="historyQuery.data.value?.items??[]" :columns="inventoryHistoryColumns" row-key="entryId" :loading="historyQuery.isFetching.value" :error-text="historyQuery.error.value?'재고이력을 조회하지 못했습니다.':''" empty-text="선택한 품목의 재고이력이 없습니다." @retry="()=>historyQuery.refetch()" />
|
||||
</template>
|
||||
</KbxMasterDetailPage>
|
||||
|
||||
<KbxDrawer v-model:open="breakdownOpen" :title="selectedItem?`${selectedItem.itemCode} 재고 산정 근거`:'재고 산정 근거'">
|
||||
<div v-if="selectedItem" class="inventory-breakdown">
|
||||
<p>{{selectedItem.itemName}} {{selectedItem.specification}}</p>
|
||||
<KbxSummaryBar :items="drawerSummary" />
|
||||
<dl><dt>현재고</dt><dd>물리적으로 기록된 총 재고</dd><dt>할당</dt><dd>주문·작업에 예약된 수량</dd><dt>보류</dt><dd>검사·품질·업무 사유로 출고할 수 없는 수량</dd><dt>가용</dt><dd>현재 정책상 신규 업무에 사용할 수 있는 수량</dd></dl>
|
||||
<p class="inventory-breakdown__note">수량 산정의 최종 기준은 서버 재고 정책이며, 화면 값은 조회 Projection입니다.</p>
|
||||
</div>
|
||||
</KbxDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inventory-breakdown{display:flex;flex-direction:column;gap:var(--kbx-space-3);font-size:var(--kbx-font-sm)}
|
||||
.inventory-breakdown p{margin:0}.inventory-breakdown dl{display:grid;grid-template-columns:80px 1fr;gap:var(--kbx-space-2);margin:0}.inventory-breakdown dt{font-weight:600}.inventory-breakdown dd{margin:0;color:var(--kbx-color-text-muted)}.inventory-breakdown__note{padding:var(--kbx-space-2);background:var(--kbx-color-surface-muted);border:var(--kbx-border-width) solid var(--kbx-color-border)}
|
||||
</style>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { defineKbxScreen, type KbxGridColumn, type KbxSearchField } from '@kbx/ui'
|
||||
|
||||
export interface InventoryItemRow {
|
||||
itemId: string; itemCode: string; itemName: string; specification: string
|
||||
totalQty: number; availableQty: number; allocatedQty: number; holdQty: number
|
||||
}
|
||||
export interface InventoryLocationRow {
|
||||
key: string; warehouseName: string; locationCode: string
|
||||
onHandQty: number; allocatedQty: number; availableQty: number; holdQty: number
|
||||
}
|
||||
export interface InventoryHistoryRow {
|
||||
entryId: string; occurredAt: string; businessType: string; referenceNo: string
|
||||
warehouseName: string; locationCode: string; inboundQty: number; outboundQty: number; balanceQty: number; actor: string
|
||||
}
|
||||
|
||||
export const inventoryScreen = defineKbxScreen({
|
||||
id: 'ERP-INV-001', version: '1.1.0', module: 'ERP', type: 'master-detail', templateCode:'T05', title: '재고현황',
|
||||
description: '품목 현재고에서 창고·로케이션 근거와 재고이력까지 조회 Context를 유지해 탐색합니다.',
|
||||
helpKey: 'ERP-INV-001', permissions: ['erp.inventory.read'], telemetry: { enabled: true },
|
||||
commands: [{ id: 'search', label: '조회', group: 'query', shortcut: 'F3' }, { id: 'excel', label: '엑셀', group: 'output' }],
|
||||
})
|
||||
|
||||
export const inventoryItemColumns: KbxGridColumn<InventoryItemRow>[] = [
|
||||
{ field: 'itemCode', header: '품목코드', type: 'code', width: 120, pinned: 'left' },
|
||||
{ field: 'itemName', header: '품목명', width: 190 },
|
||||
{ field: 'specification', header: '규격', width: 140 },
|
||||
{ field: 'totalQty', header: '현재고', type: 'quantity', width: 100, drilldown:true },
|
||||
{ field: 'availableQty', header: '가용', type: 'quantity', width: 100, drilldown:true },
|
||||
]
|
||||
export const inventoryLocationColumns: KbxGridColumn<InventoryLocationRow>[] = [
|
||||
{ field: 'warehouseName', header: '창고', width: 150 },
|
||||
{ field: 'locationCode', header: '로케이션', type: 'code', width: 120 },
|
||||
{ field: 'onHandQty', header: '현재고', type: 'quantity', width: 100 },
|
||||
{ field: 'allocatedQty', header: '할당', type: 'quantity', width: 90 },
|
||||
{ field: 'holdQty', header: '보류', type: 'quantity', width: 90 },
|
||||
{ field: 'availableQty', header: '가용', type: 'quantity', width: 100 },
|
||||
]
|
||||
export const inventoryHistoryColumns: KbxGridColumn<InventoryHistoryRow>[] = [
|
||||
{ field:'occurredAt', header:'일시', type:'datetime', width:165 },
|
||||
{ field:'businessType', header:'업무', width:110 },
|
||||
{ field:'referenceNo', header:'참조번호', type:'code', width:150 },
|
||||
{ field:'warehouseName', header:'창고', width:130 },
|
||||
{ field:'locationCode', header:'로케이션', type:'code', width:110 },
|
||||
{ field:'inboundQty', header:'입고', type:'quantity', width:90 },
|
||||
{ field:'outboundQty', header:'출고', type:'quantity', width:90 },
|
||||
{ field:'balanceQty', header:'잔량', type:'quantity', width:100 },
|
||||
{ field:'actor', header:'처리자', width:110 },
|
||||
]
|
||||
export const inventorySearchFields: KbxSearchField[] = [{ key: 'keyword', label: '품목', type: 'text', width: 'lg', placeholder: '품목코드/품목명' }]
|
||||
@@ -0,0 +1,8 @@
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
import type { InventoryHistoryRow, InventoryItemRow, InventoryLocationRow } from './inventory.definition'
|
||||
export interface InventorySearchResponse { items: InventoryItemRow[]; totalCount: number }
|
||||
export const inventoryApi = {
|
||||
search(keyword = '') { return kbxApi.request<InventorySearchResponse>('erp.inventory.search', { query: { keyword, page: 1, pageSize: 200 } }) },
|
||||
locations(itemId: string) { return kbxApi.request<{ items: InventoryLocationRow[] }>('erp.inventory.locations', { path: { itemId } }) },
|
||||
history(itemId: string) { return kbxApi.request<{ items: InventoryHistoryRow[] }>('erp.inventory.history', { path: { itemId }, query:{ pageSize:100 } }) },
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { KbxLookupItem } from '@kbx/contracts'
|
||||
import { KbxDataGrid, KbxFastEntryPage, KbxLookupDialog, useKbxPageShortcuts } from '@kbx/ui'
|
||||
import { itemPriceColumns, itemPriceScreen, type ItemPriceRow } from './item-price.definition'
|
||||
import { useItemPriceFastEntry } from './useItemPriceFastEntry'
|
||||
import { useKbxWorkspaceBinding } from '../../../shell/useKbxWorkspaceBinding'
|
||||
|
||||
const vm=useItemPriceFastEntry()
|
||||
useKbxWorkspaceBinding(vm.dirty, async()=>await vm.save())
|
||||
const lookupOpen=ref(false); const activeRow=ref<ItemPriceRow|null>(null)
|
||||
function openLookup(event:{row:ItemPriceRow;entity:string}){ if(event.entity!=='item')return;activeRow.value=event.row;lookupOpen.value=true }
|
||||
function selectItem(item:KbxLookupItem<string>){ if(activeRow.value)vm.applyItemLookup(activeRow.value,item) }
|
||||
const pageContext=computed(()=>({label:'품목 단가 입력',hint:'저장 전 오류를 모두 해소한 뒤 F8로 일괄 저장합니다.',metrics:[{key:'rows',label:'입력',value:vm.enteredCount.value},{key:'errors',label:'오류',value:vm.errors.value.length,tone:vm.errors.value.length?'danger' as const:'default' as const,emphasis:vm.errors.value.length>0}]}))
|
||||
const summary=computed(()=>[
|
||||
{key:'rows',label:'입력',value:`${vm.enteredCount.value.toLocaleString('ko-KR')}건`},
|
||||
{key:'errors',label:'오류',value:`${vm.errors.value.length.toLocaleString('ko-KR')}건`},
|
||||
...(vm.lastResult.value?[{key:'saved',label:'최근 저장',value:`${vm.lastResult.value.saved.toLocaleString('ko-KR')}건`,emphasis:true}]:[]),
|
||||
])
|
||||
useKbxPageShortcuts([{key:'F8',execute:()=>vm.save()}])
|
||||
async function command(id:string){ if(id==='save')await vm.save(); if(id==='new')vm.clear() }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KbxFastEntryPage :screen="itemPriceScreen" :selection-count="0" :dirty="vm.dirty.value" :context="pageContext" :errors="vm.errors.value" :summary-items="summary" breadcrumb="ERP > 기준정보" @command="command">
|
||||
<template #guide>
|
||||
<span><kbd>Enter</kbd> 다음 셀</span><span><kbd>F2</kbd> 품목 조회</span><span><kbd>Ctrl+V</kbd> Excel 붙여넣기</span><span>행 선택 후 아래 채우기/복제</span>
|
||||
</template>
|
||||
<template #notice>
|
||||
<div v-if="vm.lastResult.value" class="price-save-result" role="status">저장 {{vm.lastResult.value.saved.toLocaleString('ko-KR')}}건 · 신규 {{vm.lastResult.value.created.toLocaleString('ko-KR')}} · 수정 {{vm.lastResult.value.updated.toLocaleString('ko-KR')}}</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<KbxDataGrid
|
||||
:rows="vm.rows.value" :columns="itemPriceColumns" row-key="clientId" selection="multiple"
|
||||
:errors="vm.errors.value" editable clipboard
|
||||
:editing-policy="{allowRowAdd:true,allowRowDuplicate:true,fillDown:true,paste:true,errorNavigation:true}"
|
||||
@row-add-requested="vm.addRow" @row-duplicate-requested="vm.duplicateRows" @cell-changed="vm.onCellChanged" @lookup-requested="openLookup"
|
||||
/>
|
||||
</template>
|
||||
</KbxFastEntryPage>
|
||||
<KbxLookupDialog v-model:visible="lookupOpen" entity="item" title="품목" @select="selectItem" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.price-save-result{min-height:var(--kbx-control-sm);display:flex;align-items:center;padding:0 var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-success-border);background:var(--kbx-color-success-surface);color:var(--kbx-color-success-text);font-size:var(--kbx-font-sm)}
|
||||
</style>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { defineKbxScreen, type KbxGridColumn } from '@kbx/ui'
|
||||
|
||||
export interface ItemPriceRow {
|
||||
clientId: string
|
||||
itemId: string | null
|
||||
itemCode: string
|
||||
itemName: string
|
||||
effectiveDate: string
|
||||
unitPrice: number
|
||||
remark: string
|
||||
}
|
||||
|
||||
export const itemPriceScreen = defineKbxScreen({
|
||||
id: 'ERP-PRICE-001',
|
||||
version: '1.0.0',
|
||||
module: 'ERP',
|
||||
type: 'fast-entry', templateCode:'T04',
|
||||
title: '품목 단가 일괄등록',
|
||||
description: '품목별 적용일과 표준단가를 Excel처럼 연속 입력하고 한 번에 저장합니다.',
|
||||
helpKey: 'ERP-PRICE-001',
|
||||
permissions: ['erp.item.price.read', 'erp.item.read'],
|
||||
commands: [
|
||||
{ id: 'new', label: '신규', group: 'edit' },
|
||||
{ id: 'save', label: '저장', group: 'edit', variant: 'primary', shortcut: 'F8', permission: 'erp.item.price.write' },
|
||||
{ id: 'excel', label: '엑셀', group: 'output' },
|
||||
],
|
||||
telemetry: { enabled: true },
|
||||
})
|
||||
|
||||
export const itemPriceColumns: KbxGridColumn<ItemPriceRow>[] = [
|
||||
{ field:'itemCode', header:'품목코드', type:'lookup', width:140, pinned:'left', editable:true, lookup:{entity:'item'} },
|
||||
{ field:'itemName', header:'품목명', width:220 },
|
||||
{ field:'effectiveDate', header:'적용일', type:'date', width:120, editable:true },
|
||||
{ field:'unitPrice', header:'단가', type:'money', width:130, editable:true },
|
||||
{ field:'remark', header:'비고', width:260, editable:true },
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
import { isKbxProblem, type KbxProblem } from '@kbx/contracts'
|
||||
import { kbxApi } from '../../../http/generated/kbxApiClient'
|
||||
|
||||
export interface SaveItemPriceRow {
|
||||
clientId: string
|
||||
itemId: string
|
||||
effectiveDate: string
|
||||
unitPrice: number
|
||||
remark?: string
|
||||
}
|
||||
export interface SaveItemPricesRequest { rows: SaveItemPriceRow[] }
|
||||
export interface SaveItemPricesResponse { requested: number; saved: number; created: number; updated: number }
|
||||
export interface ItemLookupResult { id: string; code: string; displayName: string; status?: string }
|
||||
|
||||
export function toKbxProblem(error: unknown): KbxProblem | null { return isKbxProblem(error) ? error : null }
|
||||
|
||||
export const itemPriceApi = {
|
||||
async resolveItemByCode(code: string) {
|
||||
try { return await kbxApi.request<ItemLookupResult>('lookup.items.resolveByCode', { path:{ code } }) }
|
||||
catch (error) { if (isKbxProblem(error) && error.type === 'not-found') return null; throw error }
|
||||
},
|
||||
save(request: SaveItemPricesRequest, idempotencyKey: string) {
|
||||
return kbxApi.request<SaveItemPricesResponse, SaveItemPricesRequest>('erp.itemPrices.bulkSave', { body:request, idempotencyKey })
|
||||
},
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useMutation } from '@tanstack/vue-query'
|
||||
import type { KbxLookupItem, KbxValidationError } from '@kbx/contracts'
|
||||
import { useKbxDirtyState, useKbxValidation } from '@kbx/ui'
|
||||
import { itemPriceApi, toKbxProblem } from './itemPriceApi'
|
||||
import type { ItemPriceRow } from './item-price.definition'
|
||||
|
||||
function localDate() {
|
||||
const d=new Date(); const m=String(d.getMonth()+1).padStart(2,'0'); const day=String(d.getDate()).padStart(2,'0')
|
||||
return `${d.getFullYear()}-${m}-${day}`
|
||||
}
|
||||
function newRow():ItemPriceRow { return { clientId:crypto.randomUUID(), itemId:null, itemCode:'', itemName:'', effectiveDate:localDate(), unitPrice:0, remark:'' } }
|
||||
|
||||
export function useItemPriceFastEntry() {
|
||||
const rows=ref<ItemPriceRow[]>(Array.from({length:8},()=>newRow()))
|
||||
const validation=useKbxValidation()
|
||||
const { dirty, touch, markSaved, reset:resetDirty }=useKbxDirtyState()
|
||||
const mutation=useMutation({ mutationFn:({request,key}:{request:{rows:Array<{clientId:string;itemId:string;effectiveDate:string;unitPrice:number;remark?:string}>};key:string})=>itemPriceApi.save(request,key) })
|
||||
const lastResult=ref<{requested:number;saved:number;created:number;updated:number}|null>(null)
|
||||
let idempotencyKey=crypto.randomUUID()
|
||||
|
||||
const enteredRows=computed(()=>rows.value.filter(x=>x.itemId||x.itemCode.trim()||Number(x.unitPrice)!==0||x.remark.trim()))
|
||||
function invalidateCommandKey(){ idempotencyKey=crypto.randomUUID() }
|
||||
function addRow(){ rows.value.push(newRow()); validation.clear(); touch(); invalidateCommandKey() }
|
||||
function duplicateRows(source:ItemPriceRow[]){ rows.value.push(...source.map(x=>({...x,clientId:crypto.randomUUID()}))); touch(); invalidateCommandKey() }
|
||||
function clear(){ rows.value=Array.from({length:8},()=>newRow()); validation.clear(); lastResult.value=null; resetDirty(); invalidateCommandKey() }
|
||||
|
||||
function localErrors():KbxValidationError[] {
|
||||
const errors:KbxValidationError[]=[]
|
||||
const active=enteredRows.value
|
||||
if(!active.length) return [{code:'PRICE_ROWS_REQUIRED',message:'저장할 단가를 한 건 이상 입력하세요.'}]
|
||||
const duplicate=new Map<string,string>()
|
||||
for(const row of active){
|
||||
if(!row.itemId) errors.push({rowKey:row.clientId,field:'itemCode',code:'ITEM_REQUIRED',message:'품목을 선택하세요.'})
|
||||
if(!/^\d{4}-\d{2}-\d{2}$/.test(row.effectiveDate)) errors.push({rowKey:row.clientId,field:'effectiveDate',code:'EFFECTIVE_DATE_REQUIRED',message:'적용일을 YYYY-MM-DD 형식으로 입력하세요.'})
|
||||
if(!Number.isFinite(Number(row.unitPrice))||Number(row.unitPrice)<0) errors.push({rowKey:row.clientId,field:'unitPrice',code:'PRICE_NONNEGATIVE',message:'단가는 0 이상이어야 합니다.'})
|
||||
if(row.itemId){ const key=`${row.itemId}|${row.effectiveDate}`; if(duplicate.has(key)){ errors.push({rowKey:row.clientId,field:'effectiveDate',code:'DUPLICATE_ITEM_DATE',message:'같은 품목과 적용일이 중복되었습니다.'}); const first=duplicate.get(key)!; if(!errors.some(x=>x.rowKey===first&&x.code==='DUPLICATE_ITEM_DATE')) errors.push({rowKey:first,field:'effectiveDate',code:'DUPLICATE_ITEM_DATE',message:'같은 품목과 적용일이 중복되었습니다.'}) } else duplicate.set(key,row.clientId) }
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
async function onCellChanged(event:{row:ItemPriceRow;field:keyof ItemPriceRow;newValue:unknown}){
|
||||
if(event.field==='itemCode'){
|
||||
const code=String(event.newValue??'').trim()
|
||||
if(!code) Object.assign(event.row,{itemId:null,itemName:''})
|
||||
else {
|
||||
const item=await itemPriceApi.resolveItemByCode(code)
|
||||
if(!item){ Object.assign(event.row,{itemId:null,itemName:''}); validation.setRowFieldError(event.row.clientId,'itemCode',{code:'ITEM_NOT_FOUND',message:'존재하지 않는 품목코드입니다. F2로 품목을 조회하세요.'}) }
|
||||
else { Object.assign(event.row,{itemId:item.id,itemCode:item.code,itemName:item.displayName}); validation.setRowFieldError(event.row.clientId,'itemCode') }
|
||||
}
|
||||
}
|
||||
lastResult.value=null; touch(); invalidateCommandKey()
|
||||
}
|
||||
function applyItemLookup(row:ItemPriceRow,item:KbxLookupItem<string>){ Object.assign(row,{itemId:item.id,itemCode:item.code,itemName:item.displayName}); validation.setRowFieldError(row.clientId,'itemCode'); touch(); invalidateCommandKey() }
|
||||
|
||||
async function save(){
|
||||
validation.clear(); const errors=localErrors(); if(errors.length){validation.setErrors(errors);return false}
|
||||
const active=enteredRows.value
|
||||
try{
|
||||
const result=await mutation.mutateAsync({key:idempotencyKey,request:{rows:active.map(x=>({clientId:x.clientId,itemId:x.itemId!,effectiveDate:x.effectiveDate,unitPrice:Number(x.unitPrice),remark:x.remark||undefined}))}})
|
||||
lastResult.value=result; markSaved(); idempotencyKey=crypto.randomUUID(); return true
|
||||
}catch(error){ const problem=toKbxProblem(error); if(problem) validation.applyProblem(problem); throw error }
|
||||
}
|
||||
return {rows,errors:validation.errors,dirty,saving:computed(()=>mutation.isPending.value),lastResult,enteredCount:computed(()=>enteredRows.value.length),addRow,duplicateRows,onCellChanged,applyItemLookup,save,clear,touch}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user