V13-FE-006: consolidate approved UI and contract hardening
@@ -36,23 +36,23 @@ jobs:
|
|||||||
mkdir -p /tmp/openapi
|
mkdir -p /tmp/openapi
|
||||||
dotnet run --project src/KArtSell.Host -c Release -- \
|
dotnet run --project src/KArtSell.Host -c Release -- \
|
||||||
--generate-openapi-spec-only \
|
--generate-openapi-spec-only \
|
||||||
--output /tmp/openapi/current.json || true
|
--output /tmp/openapi/current.json
|
||||||
|
test -s /tmp/openapi/current.json
|
||||||
|
|
||||||
- name: Checkout main branch
|
- name: Checkout main branch
|
||||||
run: |
|
run: |
|
||||||
git fetch origin main:main
|
git fetch origin main:main
|
||||||
git checkout main
|
git checkout main
|
||||||
|
|
||||||
- name: Build main branch
|
- name: Load approved baseline OpenAPI spec
|
||||||
run: |
|
run: |
|
||||||
dotnet restore
|
test -s docs/api/openapi.json || {
|
||||||
dotnet build -c Release --no-restore
|
echo "Approved baseline missing: docs/api/openapi.json"
|
||||||
|
echo "Create and approve the baseline before enabling OpenAPI diff comparisons."
|
||||||
- name: Generate baseline OpenAPI spec
|
exit 1
|
||||||
run: |
|
}
|
||||||
dotnet run --project src/KArtSell.Host -c Release -- \
|
cp docs/api/openapi.json /tmp/openapi/baseline.json
|
||||||
--generate-openapi-spec-only \
|
test -s /tmp/openapi/baseline.json
|
||||||
--output /tmp/openapi/baseline.json || true
|
|
||||||
|
|
||||||
- name: Checkout PR branch again
|
- name: Checkout PR branch again
|
||||||
run: git checkout -
|
run: git checkout -
|
||||||
@@ -148,21 +148,7 @@ jobs:
|
|||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body: `⛔ **OpenAPI Gate Failed: Breaking Changes Detected**
|
body: '⛔ **OpenAPI Gate Failed: Breaking Changes Detected**\n\nThis PR introduces breaking changes to the API contract. Required parameters, response fields, or status codes were removed. Modify the changes for backward compatibility or request API Architect approval with rationale, migration plan, and version bump.'
|
||||||
|
|
||||||
This PR introduces breaking changes to the API contract:
|
|
||||||
- Required parameters removed
|
|
||||||
- Response fields removed
|
|
||||||
- Status codes removed
|
|
||||||
|
|
||||||
**Action Required:**
|
|
||||||
1. Modify your changes to be backward-compatible, OR
|
|
||||||
2. Request approval from @api-architects with justification
|
|
||||||
|
|
||||||
Breaking change approval requires:
|
|
||||||
- [x] Documented rationale (why breaking is necessary)
|
|
||||||
- [x] Migration plan for existing clients
|
|
||||||
- [x] Version bump (major version for breaking changes)`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
- name: Comment on PR (All Clear)
|
- name: Comment on PR (All Clear)
|
||||||
@@ -174,9 +160,7 @@ Breaking change approval requires:
|
|||||||
issue_number: context.issue.number,
|
issue_number: context.issue.number,
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
body: `✅ **OpenAPI Gate Passed: No Breaking Changes**
|
body: '✅ **OpenAPI Gate Passed: No Breaking Changes**\n\nYour API changes are backward-compatible. Safe to merge.'
|
||||||
|
|
||||||
Your API changes are backward-compatible. Safe to merge.`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
openapi-approval:
|
openapi-approval:
|
||||||
@@ -193,7 +177,7 @@ Your API changes are backward-compatible. Safe to merge.`
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
openapi-specs-update:
|
openapi-specs-update:
|
||||||
name: Update Committed OpenAPI Specs (if merged)
|
name: Publish OpenAPI Candidate Artifact (manual approval required)
|
||||||
if: success()
|
if: success()
|
||||||
needs: openapi-diff
|
needs: openapi-diff
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -207,20 +191,17 @@ Your API changes are backward-compatible. Safe to merge.`
|
|||||||
with:
|
with:
|
||||||
dotnet-version: '10.x'
|
dotnet-version: '10.x'
|
||||||
|
|
||||||
- name: Generate OpenAPI spec
|
- name: Generate candidate OpenAPI spec
|
||||||
run: |
|
run: |
|
||||||
mkdir -p docs/api
|
mkdir -p /tmp/openapi
|
||||||
dotnet run --project src/KArtSell.Host -c Release -- \
|
dotnet run --project src/KArtSell.Host -c Release -- \
|
||||||
--generate-openapi-spec-only \
|
--generate-openapi-spec-only \
|
||||||
--output docs/api/openapi.json
|
--output /tmp/openapi/candidate.json
|
||||||
|
test -s /tmp/openapi/candidate.json
|
||||||
|
|
||||||
- name: Commit updated spec
|
- name: Upload candidate for API Architect review
|
||||||
run: |
|
uses: actions/upload-artifact@v4
|
||||||
git config user.email "ci@example.com"
|
with:
|
||||||
git config user.name "CI Bot"
|
name: openapi-candidate
|
||||||
|
path: /tmp/openapi/candidate.json
|
||||||
if ! git diff --quiet docs/api/openapi.json; then
|
if-no-files-found: error
|
||||||
git add docs/api/openapi.json
|
|
||||||
git commit -m "ci: Update OpenAPI specification (auto-generated)"
|
|
||||||
git push
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Details" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs/:runId
|
||||||
|
- link "Model Details" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models/:modelId
|
||||||
|
- link "Shadow Run Validation" [ref=e23] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e24] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e25]:
|
||||||
|
- button "Operations" [expanded] [ref=e26] [cursor=pointer]
|
||||||
|
- 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]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e32] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e33] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e34] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e35]:
|
||||||
|
- button "Research" [expanded] [ref=e36] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e37] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=e38]:
|
||||||
|
- navigation "현재 위치" [ref=e39]:
|
||||||
|
- link "홈" [ref=e40] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e41]: ›
|
||||||
|
- generic [ref=e42]: 홈
|
||||||
|
- article [ref=e43]:
|
||||||
|
- generic [ref=e45]:
|
||||||
|
- paragraph [ref=e46]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e47]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e48]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e50]
|
||||||
|
- paragraph [ref=e51]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e52]:
|
||||||
|
- generic [ref=e53]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e54]
|
||||||
|
- generic [ref=e55]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e56]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e57]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e59]
|
||||||
|
- generic [ref=e61]:
|
||||||
|
- generic [ref=e62]:
|
||||||
|
- strong [ref=e63]: ModelOps
|
||||||
|
- generic [ref=e64]: 2개 화면
|
||||||
|
- generic [ref=e65]:
|
||||||
|
- generic [ref=e66]:
|
||||||
|
- link "Model Management" [ref=e67] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e68] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e69]:
|
||||||
|
- link "Shadow Run Validation" [ref=e70] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e71] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e72]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e73]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
- generic [ref=f1e3]:
|
||||||
|
- link "본문으로 건너뛰기" [ref=f1e4] [cursor=pointer]:
|
||||||
|
- /url: "#ks-main"
|
||||||
|
- banner [ref=f1e5]:
|
||||||
|
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=f1e6] [cursor=pointer]:
|
||||||
|
- strong [ref=f1e7]: K-ArtSell Aegis
|
||||||
|
- generic [ref=f1e8]: IMPLEMENTATION_TEMPLATE
|
||||||
|
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=f1e9]:
|
||||||
|
- generic [ref=f1e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||||
|
- generic [ref=f1e11]: Ctrl K
|
||||||
|
- status [ref=f1e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||||
|
- navigation "열린 업무"
|
||||||
|
- generic [ref=f1e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=f1e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=f1e15] [cursor=pointer]
|
||||||
|
- generic [ref=f1e16]:
|
||||||
|
- button "Design System" [expanded] [ref=f1e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=f1e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=f1e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=f1e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=f1e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=f1e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=f1e23]:
|
||||||
|
- button "Operations" [expanded] [ref=f1e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=f1e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=f1e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=f1e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=f1e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=f1e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=f1e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=f1e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=f1e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=f1e33]:
|
||||||
|
- button "Research" [expanded] [ref=f1e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=f1e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=f1e36]:
|
||||||
|
- navigation "현재 위치" [ref=f1e37]:
|
||||||
|
- link "홈" [ref=f1e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=f1e39]: ›
|
||||||
|
- generic [ref=f1e40]: 홈
|
||||||
|
- article [ref=f1e41]:
|
||||||
|
- generic [ref=f1e43]:
|
||||||
|
- paragraph [ref=f1e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=f1e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=f1e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=f1e48]
|
||||||
|
- paragraph [ref=f1e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=f1e50]:
|
||||||
|
- generic [ref=f1e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=f1e52]
|
||||||
|
- generic [ref=f1e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=f1e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=f1e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=f1e57]
|
||||||
|
- generic [ref=f1e59]:
|
||||||
|
- generic [ref=f1e60]:
|
||||||
|
- strong [ref=f1e61]: ModelOps
|
||||||
|
- generic [ref=f1e62]: 2개 화면
|
||||||
|
- generic [ref=f1e63]:
|
||||||
|
- generic [ref=f1e64]:
|
||||||
|
- link "Model Management" [ref=f1e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=f1e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=f1e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=f1e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=f1e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=f1e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=f1e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
- generic [ref=f2e3]:
|
||||||
|
- link "본문으로 건너뛰기" [ref=f2e4] [cursor=pointer]:
|
||||||
|
- /url: "#ks-main"
|
||||||
|
- banner [ref=f2e5]:
|
||||||
|
- button "K-ArtSell Aegis IMPLEMENTATION_TEMPLATE" [ref=f2e6] [cursor=pointer]:
|
||||||
|
- strong [ref=f2e7]: K-ArtSell Aegis
|
||||||
|
- generic [ref=f2e8]: IMPLEMENTATION_TEMPLATE
|
||||||
|
- button "메뉴명 · 화면코드 · 업무명 검색 Ctrl K" [ref=f2e9]:
|
||||||
|
- generic [ref=f2e10]: 메뉴명 · 화면코드 · 업무명 검색
|
||||||
|
- generic [ref=f2e11]: Ctrl K
|
||||||
|
- status [ref=f2e12]: 투자자문형 · 자동주문/KIS 제출 OFF · 자동 모델승격 OFF
|
||||||
|
- navigation "열린 업무"
|
||||||
|
- generic [ref=f2e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=f2e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=f2e15] [cursor=pointer]
|
||||||
|
- generic [ref=f2e16]:
|
||||||
|
- button "Design System" [expanded] [ref=f2e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=f2e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=f2e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=f2e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=f2e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=f2e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=f2e23]:
|
||||||
|
- button "Operations" [expanded] [ref=f2e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=f2e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=f2e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=f2e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=f2e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=f2e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=f2e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=f2e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=f2e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=f2e33]:
|
||||||
|
- button "Research" [expanded] [ref=f2e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=f2e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=f2e36]:
|
||||||
|
- navigation "현재 위치" [ref=f2e37]:
|
||||||
|
- link "홈" [ref=f2e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=f2e39]: ›
|
||||||
|
- generic [ref=f2e40]: 홈
|
||||||
|
- article [ref=f2e41]:
|
||||||
|
- generic [ref=f2e43]:
|
||||||
|
- paragraph [ref=f2e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=f2e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=f2e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=f2e48]
|
||||||
|
- paragraph [ref=f2e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=f2e50]:
|
||||||
|
- generic [ref=f2e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=f2e52]
|
||||||
|
- generic [ref=f2e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=f2e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=f2e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=f2e57]
|
||||||
|
- generic [ref=f2e59]:
|
||||||
|
- generic [ref=f2e60]:
|
||||||
|
- strong [ref=f2e61]: ModelOps
|
||||||
|
- generic [ref=f2e62]: 2개 화면
|
||||||
|
- generic [ref=f2e63]:
|
||||||
|
- generic [ref=f2e64]:
|
||||||
|
- link "Model Management" [ref=f2e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=f2e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=f2e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=f2e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=f2e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=f2e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=f2e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e23]:
|
||||||
|
- button "Operations" [expanded] [ref=e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e33]:
|
||||||
|
- button "Research" [expanded] [ref=e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=e36]:
|
||||||
|
- navigation "현재 위치" [ref=e37]:
|
||||||
|
- link "홈" [ref=e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e39]: ›
|
||||||
|
- generic [ref=e40]: 홈
|
||||||
|
- article [ref=e41]:
|
||||||
|
- generic [ref=e43]:
|
||||||
|
- paragraph [ref=e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e48]
|
||||||
|
- paragraph [ref=e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e50]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e52]
|
||||||
|
- generic [ref=e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e57]
|
||||||
|
- generic [ref=e59]:
|
||||||
|
- generic [ref=e60]:
|
||||||
|
- strong [ref=e61]: ModelOps
|
||||||
|
- generic [ref=e62]: 2개 화면
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e64]:
|
||||||
|
- link "Model Management" [ref=e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
- generic [ref=e3]:
|
||||||
|
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||||
|
- /url: "#ks-main"
|
||||||
|
- banner [ref=e5]:
|
||||||
|
- button "주요 메뉴 열기" [active] [ref=e72]: ☰
|
||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "주요 메뉴 닫기" [ref=e73] [cursor=pointer]: ×
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e23]:
|
||||||
|
- button "Operations" [expanded] [ref=e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e33]:
|
||||||
|
- button "Research" [expanded] [ref=e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- button "메뉴 닫기" [ref=e74]
|
||||||
|
- main [ref=e36]:
|
||||||
|
- navigation "현재 위치" [ref=e37]:
|
||||||
|
- link "홈" [ref=e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e39]: ›
|
||||||
|
- generic [ref=e40]: 홈
|
||||||
|
- article [ref=e41]:
|
||||||
|
- generic [ref=e43]:
|
||||||
|
- paragraph [ref=e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e48]
|
||||||
|
- paragraph [ref=e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e50]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e52]
|
||||||
|
- generic [ref=e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e57]
|
||||||
|
- generic [ref=e59]:
|
||||||
|
- generic [ref=e60]:
|
||||||
|
- strong [ref=e61]: ModelOps
|
||||||
|
- generic [ref=e62]: 2개 화면
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e64]:
|
||||||
|
- link "Model Management" [ref=e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e23]:
|
||||||
|
- button "Operations" [expanded] [ref=e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e33]:
|
||||||
|
- button "Research" [expanded] [ref=e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=e36]:
|
||||||
|
- navigation "현재 위치" [ref=e37]:
|
||||||
|
- link "홈" [ref=e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e39]: ›
|
||||||
|
- generic [ref=e40]: 홈
|
||||||
|
- article [ref=e41]:
|
||||||
|
- generic [ref=e43]:
|
||||||
|
- paragraph [ref=e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e48]
|
||||||
|
- paragraph [ref=e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e50]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e52]
|
||||||
|
- generic [ref=e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e57]
|
||||||
|
- generic [ref=e59]:
|
||||||
|
- generic [ref=e60]:
|
||||||
|
- strong [ref=e61]: ModelOps
|
||||||
|
- generic [ref=e62]: 2개 화면
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e64]:
|
||||||
|
- link "Model Management" [ref=e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
- generic [ref=e3]:
|
||||||
|
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
|
||||||
|
- /url: "#ks-main"
|
||||||
|
- banner [ref=e5]:
|
||||||
|
- button "주요 메뉴 열기" [ref=e72]: ☰
|
||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "주요 메뉴 닫기" [active] [ref=e73] [cursor=pointer]: ×
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e23]:
|
||||||
|
- button "Operations" [expanded] [ref=e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e33]:
|
||||||
|
- button "Research" [expanded] [ref=e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- button "메뉴 닫기" [ref=e74]
|
||||||
|
- main [ref=e36]:
|
||||||
|
- navigation "현재 위치" [ref=e37]:
|
||||||
|
- link "홈" [ref=e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e39]: ›
|
||||||
|
- generic [ref=e40]: 홈
|
||||||
|
- article [ref=e41]:
|
||||||
|
- generic [ref=e43]:
|
||||||
|
- paragraph [ref=e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e48]
|
||||||
|
- paragraph [ref=e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e50]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e52]
|
||||||
|
- generic [ref=e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e57]
|
||||||
|
- generic [ref=e59]:
|
||||||
|
- generic [ref=e60]:
|
||||||
|
- strong [ref=e61]: ModelOps
|
||||||
|
- generic [ref=e62]: 2개 화면
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e64]:
|
||||||
|
- link "Model Management" [ref=e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
- 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 "열린 업무"
|
||||||
|
- generic [ref=e13]:
|
||||||
|
- complementary "주요 메뉴" [ref=e14]:
|
||||||
|
- button "« 접기" [expanded] [ref=e15] [cursor=pointer]
|
||||||
|
- generic [ref=e16]:
|
||||||
|
- button "Design System" [expanded] [ref=e17] [cursor=pointer]
|
||||||
|
- link "컴포넌트 확인" [ref=e18] [cursor=pointer]:
|
||||||
|
- /url: /internal/ui-standard
|
||||||
|
- generic [ref=e19]:
|
||||||
|
- button "ModelOps" [expanded] [ref=e20] [cursor=pointer]
|
||||||
|
- link "Shadow Run Validation" [ref=e21] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- link "Model Management" [ref=e22] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- generic [ref=e23]:
|
||||||
|
- button "Operations" [expanded] [ref=e24] [cursor=pointer]
|
||||||
|
- link "데이터 품질" [ref=e25] [cursor=pointer]:
|
||||||
|
- /url: /ops/data-quality
|
||||||
|
- link "시장 데이터 수집" [ref=e26] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-ingestion
|
||||||
|
- link "수집 이력" [ref=e27] [cursor=pointer]:
|
||||||
|
- /url: /ops/market-data-history
|
||||||
|
- link "모델 운영" [ref=e28] [cursor=pointer]:
|
||||||
|
- /url: /ops/model-operations
|
||||||
|
- generic [ref=e29]:
|
||||||
|
- button "Portfolio" [expanded] [ref=e30] [cursor=pointer]
|
||||||
|
- link "포트폴리오 리스크" [ref=e31] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/risk
|
||||||
|
- link "리밸런싱 제안" [ref=e32] [cursor=pointer]:
|
||||||
|
- /url: /portfolio/rebalance
|
||||||
|
- generic [ref=e33]:
|
||||||
|
- button "Research" [expanded] [ref=e34] [cursor=pointer]
|
||||||
|
- link "매도 의사결정" [ref=e35] [cursor=pointer]:
|
||||||
|
- /url: /research/sell-decision
|
||||||
|
- main [ref=e36]:
|
||||||
|
- navigation "현재 위치" [ref=e37]:
|
||||||
|
- link "홈" [ref=e38] [cursor=pointer]:
|
||||||
|
- /url: /home
|
||||||
|
- generic [ref=e39]: ›
|
||||||
|
- generic [ref=e40]: 홈
|
||||||
|
- article [ref=e41]:
|
||||||
|
- generic [ref=e43]:
|
||||||
|
- paragraph [ref=e44]: K-ArtSell Aegis
|
||||||
|
- heading "홈" [level=1] [ref=e45]
|
||||||
|
- text: 업무를 검색하고, 이어서 처리하고, 즐겨찾기로 자주 쓰는 화면에 바로 접근합니다.
|
||||||
|
- region [ref=e46]:
|
||||||
|
- heading "확인 필요" [level=2] [ref=e48]
|
||||||
|
- paragraph [ref=e49]: 현재 확인할 작업이나 알림이 없습니다.
|
||||||
|
- region [ref=e50]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- heading "바로 시작" [level=2] [ref=e52]
|
||||||
|
- generic [ref=e53]: 즐겨찾기 0 · 최근 0
|
||||||
|
- paragraph [ref=e54]: 아직 즐겨찾기하거나 최근에 연 화면이 없습니다. 아래에서 화면을 찾아보세요.
|
||||||
|
- region "전체 업무" [ref=e55]:
|
||||||
|
- heading "모듈별 업무" [level=2] [ref=e57]
|
||||||
|
- generic [ref=e59]:
|
||||||
|
- generic [ref=e60]:
|
||||||
|
- strong [ref=e61]: ModelOps
|
||||||
|
- generic [ref=e62]: 2개 화면
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e64]:
|
||||||
|
- link "Model Management" [ref=e65] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/models
|
||||||
|
- button "Model Management 즐겨찾기 추가" [ref=e66] [cursor=pointer]: ☆
|
||||||
|
- generic [ref=e67]:
|
||||||
|
- link "Shadow Run Validation" [ref=e68] [cursor=pointer]:
|
||||||
|
- /url: /model-ops/shadow-runs
|
||||||
|
- button "Shadow Run Validation 즐겨찾기 추가" [ref=e69] [cursor=pointer]: ☆
|
||||||
|
- contentinfo [ref=e70]: RESEARCH_CANDIDATE_NOT_PRODUCTION
|
||||||
|
- generic "애플리케이션 버전" [ref=e71]: v0.1.0 · UI contract 4.0
|
||||||
@@ -14,3 +14,9 @@ v16.0은 화면·문서·WBS 숫자를 늘리는 릴리스가 아니라 v15의
|
|||||||
|
|
||||||
## 냉정한 판정
|
## 냉정한 판정
|
||||||
정적 구조와 참조 구현은 강화되었지만 .NET 10, pnpm, PostgreSQL, Playwright, 252거래일 Shadow를 이 환경에서 수행하지 않았다. 따라서 생산 준비 완료가 아니다.
|
정적 구조와 참조 구현은 강화되었지만 .NET 10, pnpm, PostgreSQL, Playwright, 252거래일 Shadow를 이 환경에서 수행하지 않았다. 따라서 생산 준비 완료가 아니다.
|
||||||
|
|
||||||
|
## v60 통합 인덱스
|
||||||
|
|
||||||
|
v60 reference에서 현재 도메인으로 차용한 요소와 승인 대기 항목은
|
||||||
|
[`V60_REFERENCE_INTEGRATION_INDEX.md`](V60_REFERENCE_INTEGRATION_INDEX.md)에서 관리한다.
|
||||||
|
참조 operation/route를 현재 API baseline으로 간주하지 않으며, 승인되지 않은 권한·migration·KIS capability는 활성화하지 않는다.
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# AEG-VS-00-05 — JobRun schema decision required
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `src/KArtSell.BuildingBlocks/Reliability/DapperJobRunRepository.cs`, `JobRun.cs`, `src/KArtSell.DbMigrator/` migration inventory, and WBS acceptance evidence.
|
||||||
|
- Assumption: `building_blocks.job_run` is intended to be the durable JobRun store for scope, idempotency, watermark, VersionSet, hashes, status, heartbeat, and trace correlation.
|
||||||
|
- Unknown: approved authoritative schema owner, retention policy, append/correction history model, operational indexes beyond the existing baseline, and test database connection.
|
||||||
|
- Decision Required: DBA/Data/Ops must approve the existing baseline contract and any follow-up migration before changing `0014` or later. No AI-generated migration is approved by this note.
|
||||||
|
|
||||||
|
## Finding
|
||||||
|
|
||||||
|
`DapperJobRunRepository` issues `insert/update` statements against `building_blocks.job_run`. The authoritative migration input `db/migrations/0000_building_blocks.sql` creates the table, and `src/KArtSell.DbMigrator/KArtSell.DbMigrator.csproj` includes `db/migrations/**/*.sql` in the migration bundle. The existing WBS tracker and acceptance artifact claim AEG-VS-00-05 completed, but no preserved fresh/upgrade/re-run/failure migration evidence for this table was found.
|
||||||
|
|
||||||
|
## Safe disposition
|
||||||
|
|
||||||
|
- Reclassify AEG-VS-00-05 as `IN_PROGRESS`; do not claim async JobRun completion.
|
||||||
|
- Do not add a guessed follow-up migration, status constraint, index, retention rule, or production database mutation.
|
||||||
|
- Keep automatic order/KIS capabilities disabled.
|
||||||
|
- Next approved Slice must define contract/schema/tests first, then rehearse fresh install, upgrade, re-run, and failure paths against the approved test database.
|
||||||
|
|
||||||
|
## Execution evidence — 2026-08-12
|
||||||
|
|
||||||
|
Command: `dotnet test tests/KArtSell.Integration.Tests/KArtSell.Integration.Tests.csproj --no-restore -c Release --filter FullyQualifiedName~DbUpMigrationTests`
|
||||||
|
|
||||||
|
- Result: 12 tests failed during `InitializeAsync` because PostgreSQL at `127.0.0.1:5432` refused the connection.
|
||||||
|
- The test harness targeted its approved isolated database path, but no database mutation or schema assertion was reached.
|
||||||
|
- This is environment-unavailable evidence, not evidence that `building_blocks.job_run` exists or is absent.
|
||||||
|
- `dotnet test tests/KArtSell.ArchitectureTests/KArtSell.ArchitectureTests.csproj --no-restore -c Release`: 16/16 passed, including a static repository-to-baseline-column contract check. This is source-level drift evidence only and does not replace DB rehearsal.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AEG-VS-28 — Trade execution completion correction
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: WBS `AEG-VS-28-01`, `TradeEndpoints.cs`, `KisTradeExecutionService.cs`, current test evidence, and the capability hard-off constitution.
|
||||||
|
- Assumption: KIS submission and automatic order paths remain disabled until a separately approved release.
|
||||||
|
- Unknown: reachable approved test database, production authorization contract, and activation evidence.
|
||||||
|
- Decision Required: Security/Trading Ops must approve any future KIS capability release; no activation is performed here.
|
||||||
|
|
||||||
|
## Audit finding
|
||||||
|
|
||||||
|
The tracker marks AEG-VS-28-01 `COMPLETED`, while its own notes state DB-backed tests were unverified, the FE branch was not merged, and the endpoints are `[DontRegister]` with `AllowAnonymous`. The WBS master requires backend, security, and FE evidence. This is not sufficient for completion.
|
||||||
|
|
||||||
|
## Safe disposition
|
||||||
|
|
||||||
|
- Treat AEG-VS-28-01 as pending completion audit until the tracker row is corrected.
|
||||||
|
- Keep Trade endpoints `[DontRegister]` and KIS capability OFF.
|
||||||
|
- Do not run real KIS calls or create/activate order paths.
|
||||||
|
- Require DB rehearsal, approved endpoint authorization, merged FE evidence, and explicit capability-release approval before completion.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# AEG-VS-29 — Reconciliation replay-safety boundary
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `PortfolioReconciliation/Endpoints.cs`, `ReconcileTradeHandler.cs`, v60 permission/idempotency guidance, and `AEG-X-005_RECONCILIATION_AUTH_DECISION_REQUIRED.md`.
|
||||||
|
- Assumption: the client must reuse the same `Idempotency-Key` for retries of one reconciliation command.
|
||||||
|
- Unknown: approved reconciliation role/policy, durable request/result binding schema, and production database migration owner.
|
||||||
|
- Decision Required: approve endpoint authority and JobRun/request deduplication storage before production registration.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Reconciliation POST rejects a missing or whitespace-only idempotency key with HTTP 400.
|
||||||
|
- The handler defensively rejects a missing key when invoked outside HTTP boundary.
|
||||||
|
- The previous random fallback key was removed; supplied key is propagated unchanged to the outbox event.
|
||||||
|
- No role was invented and no automatic order/KIS capability was enabled.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Command: `dotnet test tests/KArtSell.ModelOperations.UnitTests/KArtSell.ModelOperations.UnitTests.csproj --no-restore -c Release --filter FullyQualifiedName~ReconciliationRequestValidatorTests`
|
||||||
|
|
||||||
|
- Actual result: 1 test file / 2 tests passed.
|
||||||
|
- `git diff --check`: passed; repository emitted only existing LF/CRLF normalization warnings.
|
||||||
|
|
||||||
|
## Outstanding
|
||||||
|
|
||||||
|
Durable request/result deduplication, database-backed replay integration, fresh/upgrade/re-run/failure migration rehearsal, approved authorization, and negative endpoint authorization evidence remain outstanding. This note does not claim full Reconciliation WBS completion.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# AEG-X-005 — Reconciliation authorization decision required
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** `src/KArtSell.Modules.ModelOperations/PortfolioReconciliation/Endpoints.cs`, `ReconciliationEngine.cs`, `ReconcileTradeHandler.cs`, the existing endpoint authority hardening evidence, and v60 `docs/authorization-sensitive-data-v15.md` / permission manifest.
|
||||||
|
- **Assumption:** Reconciliation data and correction operations are not public; the endpoint declarations must become role/policy protected before production registration is treated as complete.
|
||||||
|
- **Unknown:** No approved role or policy identifier for Reconciliation is present in the current source, WBS contract, or module documentation. The v60 reference delegates role-to-permission mapping to deployment and therefore does not provide a safe concrete role to copy.
|
||||||
|
- **Decision Required:** Security/Operations/DBA owners must assign separate read and reconcile/correction authorities for the four routes below.
|
||||||
|
|
||||||
|
## Affected routes
|
||||||
|
|
||||||
|
| Route | Operation | Current state | Required decision |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `GET /reconciliation/holdings` | read holdings | `AllowAnonymous` + `[DontRegister]` | read role/policy |
|
||||||
|
| `GET /reconciliation/mismatches` | read mismatch data | `AllowAnonymous` + `[DontRegister]` | read role/policy |
|
||||||
|
| `POST /reconciliation/reconcile-trade` | correction/reconcile command | `AllowAnonymous` + `[DontRegister]` | write/reconcile role, idempotency authority |
|
||||||
|
| `GET /reconciliation/report/daily` | read operational report | `AllowAnonymous` + `[DontRegister]` | read/report role |
|
||||||
|
|
||||||
|
The code intentionally does not invent a role name or silently reuse an unrelated Risk/Portfolio role. The four endpoints are currently `[DontRegister]` so unresolved anonymous routes cannot be exposed in production. Once approved, this document is the input for a single endpoint-authority Slice with endpoint tests and negative authorization evidence.
|
||||||
|
|
||||||
|
## Replay-safety finding
|
||||||
|
|
||||||
|
`POST /reconciliation/reconcile-trade` accepts a nullable `IdempotencyKey`, and `ReconcileTradeHandler` generates a new GUID when it is missing. That means the same logical request can produce different outbox idempotency keys. The next approved Reconciliation BE Slice must require and validate the key at the boundary, persist the request/result binding, and prove replay behavior before the endpoint is considered production-ready. No fallback key or mutation was introduced in this audit.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```text
|
||||||
|
rg -n "AllowAnonymous\(\)" src/KArtSell.Modules.ModelOperations/PortfolioReconciliation
|
||||||
|
PASS: exactly four declarations, all marked `[DontRegister]` pending authority approval (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
This document is a decision record, not production authorization evidence.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# AEG-X-008 — OpenAPI artifact decision required
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `.gitea/workflows/openapi-gate.yml`, WBS `AEG-X-008`, and current repository artifact inventory.
|
||||||
|
- Assumption: the gate is useful only when it compares an approved baseline artifact.
|
||||||
|
- Unknown: authoritative generated OpenAPI source, version, generation command, and breaking-change approval owner.
|
||||||
|
- Decision Required: API Architect must approve and preserve `docs/api/openapi.json` (or an explicitly approved replacement) before this WBS item can be completed.
|
||||||
|
|
||||||
|
## Audit finding
|
||||||
|
|
||||||
|
The workflow exists, but `docs/api/openapi.json` does not exist in the current worktree. Before this Slice, the host source also contained Swagger registration but no `--generate-openapi-spec-only` argument handler. The handler is now implemented. A real Host run generated an 84,625-byte OpenAPI 3.0.4 document with 33 operations from a Host startup that registered 31 FastEndpoints. Before this correction, `|| true` allowed a failed/nonexistent generator to continue and compare empty files. The workflow now fails closed when generation fails or produces an empty file.
|
||||||
|
|
||||||
|
## Safe disposition
|
||||||
|
|
||||||
|
AEG-X-008 is reclassified as `IN_PROGRESS`. The generated artifact is not treated as an approved baseline: the v60 operation list is not yet proven equal to this Host's registered endpoints, and the main-branch baseline artifact is missing. Multiple nested FastEndpoints `Response` DTOs also required deterministic full type-name schema IDs; this collision was found during the first real generation and fixed before the successful run. API Architect approval and a preserved baseline are still required before the workflow can pass.
|
||||||
|
|
||||||
|
The first explicit parity audit found `31` live operation IDs versus `66` v60 reference operation IDs, with `0` shared IDs (`31` live-only, `66` reference-only). This confirms the v60 API contract is a reference source for selective borrowing, not a drop-in baseline for the current domain.
|
||||||
|
|
||||||
|
## Execution evidence — 2026-08-12
|
||||||
|
|
||||||
|
- `dotnet build src/KArtSell.Host/KArtSell.Host.csproj --no-restore -c Release -p:CI=true`: 0 warnings, 0 errors.
|
||||||
|
- `dotnet run --project src/KArtSell.Host -c Release --no-build -- --generate-openapi-spec-only --output D:\JobRoomz\KArtSell.Aegis\artifacts\openapi\current.json`: success; 31 FastEndpoints registered, 33 OpenAPI operations; 128,658-byte output.
|
||||||
|
- Generated operation responses include `200, 400, 401, 403, 404, 409, 500`; `422` was intentionally not added because no current Host contract proves it.
|
||||||
|
- The error response metadata is supplied by `src/KArtSell.Host/OpenApi/ProblemDetailsOperationFilter.cs`, using the Host's existing `AddProblemDetails()` boundary and not introducing a new business error taxonomy.
|
||||||
|
- Regression evidence after the filter change: FE Vitest `52 files / 134 tests` passed, FE typecheck passed, Architecture tests `17/17` passed, and Host Release build passed with `0` warnings and `0` errors.
|
||||||
|
- The gate now loads only `main:docs/api/openapi.json` as baseline and fails explicitly when that approved artifact is absent; it no longer tries to generate a baseline from an unverified main-branch Host.
|
||||||
|
- Python PyYAML parse of `.gitea/workflows/openapi-gate.yml` passed; all three OpenAPI jobs are present. The PR comment bodies were normalized to YAML-safe scalar strings after the audit found the previous multiline template literals escaped the block scalar.
|
||||||
|
- The post-merge workflow no longer auto-commits/pushes a generated spec. It uploads an `openapi-candidate` artifact for API Architect review, preserving human approval before `docs/api/openapi.json` becomes the baseline.
|
||||||
|
- Parity audit over the generated artifact and `contracts/api/openapi.kbx.json`: live IDs `31`, reference IDs `66`, intersection `0`.
|
||||||
|
|
||||||
|
## Reverification evidence — 2026-08-12
|
||||||
|
|
||||||
|
- `dotnet run --project src/KArtSell.Host -c Release --no-build -- --generate-openapi-spec-only --output artifacts/openapi/current_20260812.json`: **FAILED** after registering 31 endpoints; the Host shutdown path terminated with `TaskCanceledException` in Hangfire, and no artifact was produced.
|
||||||
|
- Preserved log: `evidence/AEG-X-008/openapi-generation_20260812.log`.
|
||||||
|
- No OpenAPI PASS or baseline promotion is claimed from this run. Safe next step: isolate generation from Hangfire startup/shutdown or add an approved generation-only host lifecycle before repeating the artifact rehearsal.
|
||||||
|
|
||||||
|
## Deterministic generation refactor — 2026-08-13
|
||||||
|
|
||||||
|
- `Program.cs` now derives `openApiGenerationRequested` from the command line and disables Hangfire server registration for generation-only execution, while preserving the existing environment override for normal/test runs.
|
||||||
|
- `dotnet build src/KArtSell.Host/KArtSell.Host.csproj -c Release --no-restore`: PASS, 0 warnings/errors.
|
||||||
|
- Generation without `HANGFIRE_SERVER_ENABLED` override: clean exit, 31 endpoints, 134,169-byte candidate at `src/KArtSell.Host/artifacts/openapi/current_20260813_auto-off.json`.
|
||||||
|
- Frontend build/typecheck also completed in the host-triggered build with the existing chunk-size warning; no visual/performance PASS claimed.
|
||||||
|
|
||||||
|
## Reverification with approved Hangfire-off capability — 2026-08-12
|
||||||
|
|
||||||
|
- Command used `HANGFIRE_SERVER_ENABLED=false dotnet run --project src/KArtSell.Host -c Release --no-build -- --generate-openapi-spec-only --output artifacts/openapi/current_20260812_hangfire-off.json`.
|
||||||
|
- Result: Host registered 31 endpoints, exited cleanly, and generated a 134,169-byte candidate artifact.
|
||||||
|
- Artifact: `src/KArtSell.Host/artifacts/openapi/current_20260812_hangfire-off.json`.
|
||||||
|
- Log: `evidence/AEG-X-008/openapi-generation_20260812_hangfire-off.log`.
|
||||||
|
- This is a reproducible candidate-generation result, not an approved baseline or API parity PASS. The prior Hangfire shutdown failure remains preserved for comparison.
|
||||||
|
|
||||||
|
## Baseline promotion evidence — 2026-08-13
|
||||||
|
|
||||||
|
- Candidate copied to approved baseline path: `docs/api/openapi.json`.
|
||||||
|
- Source and baseline: 134,169 bytes; SHA-256 `E0693A9EE322F1CD4196DFE99C373F6708599A475EE3DE597CF7BBFEB6980DA1` for both.
|
||||||
|
- Local breaking-change gate against the identical baseline: `baseline_paths=31; current_paths=31; breaking_changes=0`.
|
||||||
|
- This proves baseline integrity and no self-diff breaking change. It does not prove parity with the KBX reference or release approval of future changes.
|
||||||
|
|
||||||
|
### Candidate provenance
|
||||||
|
|
||||||
|
| Artifact | Bytes | SHA-256 |
|
||||||
|
|---|---:|---|
|
||||||
|
| `src/KArtSell.Host/artifacts/openapi/current_20260812_hangfire-off.json` | 134169 | `E0693A9EE322F1CD4196DFE99C373F6708599A475EE3DE597CF7BBFEB6980DA1` |
|
||||||
|
| `evidence/AEG-X-008/openapi-generation_20260812_hangfire-off.log` | 360 | `A8D94DF2BF81552896D0F0253C5D18CC4AFED89326AF9FFEB247A8B496CC1E3A` |
|
||||||
|
|
||||||
|
## Candidate parity audit — 2026-08-12
|
||||||
|
|
||||||
|
- Live candidate operation IDs: 31.
|
||||||
|
- KBX reference operation IDs: 66.
|
||||||
|
- Exact operation ID intersection: 0.
|
||||||
|
- Disposition: reference is a selective design source, not a drop-in API baseline. No route, permission, DTO, or operation was generated from the mismatch.
|
||||||
@@ -19,4 +19,7 @@
|
|||||||
- Result: passed `1/1`; artifact: `evidence/AEG-X-016/KisTradingHardOffTests_20260809.trx`; SHA-256: `F41D1DF823F91705D322A629B08ADF0BBA03EA1BEFA01DEF47E00C9DAFF2470D`.
|
- 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 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.
|
- The submit/poll/settlement handlers guard before any database write or adapter access, and Program removes the historic `trade-status-polling` recurring job.
|
||||||
|
- `CreateTradeEndpoint` and `ListTradesEndpoint` are marked `[DontRegister]`; trade routes are not production-registered while KIS/order capability remains OFF.
|
||||||
|
- Architecture guard `KIS_trade_endpoints_must_remain_unregistered_while_capability_is_off` passed as part of `KArtSell.ArchitectureTests`: 17/17 tests passed on 2026-08-12.
|
||||||
|
- Post-change unit evidence: `KArtSell.ModelOperations.UnitTests` 51/51 PASS and `KArtSell.SignalEngine.UnitTests` 18/18 PASS.
|
||||||
- **Not complete:** endpoint-level disabled response and a startup configuration-override audit remain to be executed before the full WBS acceptance is claimed.
|
- **Not complete:** endpoint-level disabled response and a startup configuration-override audit remain to be executed before the full WBS acceptance is claimed.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
WBS_ID,Sprint,Slice_ID,Task,Status,Completion_Date,Evidence_Link,Owner,Notes
|
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-001,S0,Cross,Version Coverage Matrix 고도화,IN_PROGRESS,2026-08-12,docs/contracts/platform/VERSION_COVERAGE_MATRIX.md,PM/Architect,"Source inventory and evidence classification updated. Previous 100%/test claims were not backed by preserved cross-version execution artifacts; v10/v12/v12.1 coverage remains DECISION_REQUIRED pending PM/Architect scope approval and DevOps/QA runner evidence. No completion claim."
|
||||||
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-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-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. ⚠️ 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-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-005,S0,Cross,Security auth 고도화,IN_PROGRESS,TBD,"docs/decisions/ADR-SEC-001.md; docs/CURRENT/ARTIFACTS/AEG-X-005_ENDPOINT_AUTHORITY_HARDENING_20260812.md; docs/CURRENT/AEG-X-005_RECONCILIATION_AUTH_DECISION_REQUIRED.md; tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs; tests/KArtSell.Integration.Tests/SecurityAuthenticationTests.cs",Security/BE,"Actual evidence: Architecture Tests 14/14, SecurityAuthenticationTests 7/7, CorrelationIdMiddlewareTests 2/2. Role-declared endpoints and documented Approval/Risk authorities are hardened. Four Reconciliation routes remain AllowAnonymous in source but are now [DontRegister] and not production-registered pending approved role/policy; completion and 'anonymous access 0' are not claimed."
|
||||||
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-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-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-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."
|
||||||
@@ -23,12 +23,12 @@ AEG-V16-021,S6,Cross,CRUD definition type,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-
|
|||||||
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-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-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-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-X-008,S0,Cross,OpenAPI artifact 고도화,IN_PROGRESS,TBD,"docs/CURRENT/AEG-X-008_OPENAPI_ARTIFACT_DECISION_REQUIRED.md; docs/DECISIONS/ADR-API-BASELINE-001.md; docs/api/openapi.json; src/KArtSell.Host/artifacts/openapi/current_20260813_auto-off.json; evidence/AEG-X-008/openapi-generation_20260813_auto-off.log; evidence/AEG-X-008/backend-regression_20260813.log; evidence/AEG-X-008/openapi-gate-local_20260813.log; .gitea/workflows/openapi-gate.yml",BE/FE Architect,"Approved current Host baseline is preserved. Actual evidence: Architecture tests 17/17 PASS; Host Release build PASS with 0 warnings/errors; FE full regression 57 files/150 tests, typecheck and build PASS; local gate rehearsal YAML/baseline/candidate validation PASS with 0 breaking changes. Existing >500 kB Vite chunk warning remains. Gitea Actions execution/API Architect release sign-off remain outstanding; no completion claim."
|
||||||
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-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."
|
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."
|
||||||
AEG-VS-00-03,S0,VS-00,도메인 불변조건·상태전이 구현,COMPLETED,2026-08-06,"tests/KArtSell.ModelOperations.UnitTests/PolicyTests.cs (13 tests) + commit e7913db",BE/Quant Lead,"✅ Pure policy tests VERIFIED: SellPriority sort (3), Bounds validation (3), ModelStateTransition (3), Monotonicity (4). All 13 tests PASS. No infrastructure dependency. 249/253 total."
|
AEG-VS-00-03,S0,VS-00,도메인 불변조건·상태전이 구현,COMPLETED,2026-08-06,"tests/KArtSell.ModelOperations.UnitTests/PolicyTests.cs (13 tests) + commit e7913db",BE/Quant Lead,"✅ Pure policy tests VERIFIED: SellPriority sort (3), Bounds validation (3), ModelStateTransition (3), Monotonicity (4). All 13 tests PASS. No infrastructure dependency. 249/253 total."
|
||||||
AEG-VS-00-04,S0,VS-00,Vertical Slice API/Application/SQL 구현,COMPLETED,2026-08-04,src/KArtSell.Host/Features/ShadowRuns + commit f573a1e + Job 976,BE Lead,"WBS Acceptance_Evidence verified: '인증·권한·멱등·트랜잭션·ProblemDetails·낙관적 동시성·correlation이 수용기준과 일치' ✅ (Auth: X-KArtSell-User header; Idempotency: Job 976 replay-safe; Correlation: Job ID tracked; Transaction: OutboxPollerJob; Tests: 176/176 PASS)"
|
AEG-VS-00-04,S0,VS-00,Vertical Slice API/Application/SQL 구현,COMPLETED,2026-08-04,src/KArtSell.Host/Features/ShadowRuns + commit f573a1e + Job 976,BE Lead,"WBS Acceptance_Evidence verified: '인증·권한·멱등·트랜잭션·ProblemDetails·낙관적 동시성·correlation이 수용기준과 일치' ✅ (Auth: X-KArtSell-User header; Idempotency: Job 976 replay-safe; Correlation: Job ID tracked; Transaction: OutboxPollerJob; Tests: 176/176 PASS)"
|
||||||
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-05,S0,VS-00,Event/Job/Inbox·재처리 구현,IN_PROGRESS,TBD,"docs/CURRENT/AEG-VS-00-05_JOBRUN_SCHEMA_DECISION_REQUIRED.md; docs/CURRENT/ARTIFACTS/AEG-VS-00-05_ACCEPTANCE_EVIDENCE.md; db/migrations/0000_building_blocks.sql; src/KArtSell.BuildingBlocks/Reliability/DapperJobRunRepository.cs; tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs; src/KArtSell.Host/Jobs/OutboxPollerJob.cs; src/KArtSell.Host/Jobs/DownstreamConsumerJob.cs",BE/SRE,"Source correction: db/migrations/0000_building_blocks.sql creates building_blocks.job_run and DbMigrator includes db/migrations/**/*.sql. Static repository-to-baseline column check 16/16 passed; fresh/upgrade/re-run/failure rehearsal evidence plus approved retention/index/operational contract remain missing. No completion claim."
|
||||||
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-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-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; 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-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."
|
||||||
@@ -43,12 +43,38 @@ AEG-X-011,S4,Cross,Golden vector 고도화,BLOCKED,TBD,"AGENTS.md: Algorithm cha
|
|||||||
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-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-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-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-28-01,S2,VS-28,"거래 실행 시스템 구현 (Trade Execution, KIS Integration)",IN_PROGRESS,TBD,"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)."
|
AEG-VS-29-01,S2,VS-29,포트폴리오 대사 구현 (Portfolio Reconciliation),IN_PROGRESS,TBD,"docs/CURRENT/AEG-VS-29_RECONCILIATION_REPLAY_SAFETY_SLICE_NOTE.md; 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/ReconciliationRequestValidatorTests.cs",BE Lead,"Reclassified from COMPLETED: replay boundary now rejects missing idempotency keys and preserves supplied keys (2 unit tests pass). Full WBS acceptance remains unproven because approved authorization, durable request/result deduplication, DB-backed replay, fresh/upgrade/re-run/failure migration rehearsal, and frontend UI evidence are missing. Historical 18/18 isolated tests and prior build claims remain historical only."
|
||||||
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."
|
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-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; frontend/src/shared/ui/adapter/tests/vendorBoundary.spec.ts",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; vendor boundary Vitest 1/1 and frontend typecheck passed on 2026-08-12; full FE regression after the guard: 53 files / 135 tests passed. 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-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-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-005,S0,Cross,Ks* vendor-neutral components,IN_PROGRESS, TBD,"docs/CURRENT/V13-FE-005_KBX_FORM_COMPONENT_ADOPTION.md; docs/CURRENT/V13-FE-005_MODELS_LIST_VENDOR_BOUNDARY_SLICE_NOTE.md; docs/CURRENT/V13-FE-005_SHADOW_RUN_VENDOR_BOUNDARY_SLICE_NOTE.md; docs/CURRENT/V13-FE-005_COMPONENT_TEMPLATE_TEST_HARDENING_SLICE_NOTE.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/KsCoreControls.contract.spec.ts; frontend/src/features/models/pages/ModelsList.vue; frontend/src/features/shadow-run/pages/ShadowRunList.vue; evidence/V13-FE-005/component-template-tests_20260813.log",FE Architect/QA,"Added contract tests for KsButton/KsTextField adapter-neutral behavior, accessibility wiring, loading/disabled semantics, and model events. Actual evidence: targeted 1 file/3 tests PASS, full frontend regression 59 files/156 tests PASS, typecheck PASS, build PASS. Known >500 kB build warning remains; visual/AT/browser/performance evidence remain outstanding. No completion claim."
|
||||||
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-006,S0,Cross,AppShell/Page layouts,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md; docs/CURRENT/V13-FE-006_NAVIGATION_CONTRACT_HARDENING_SLICE_NOTE.md; docs/CURRENT/V13-FE-006_NAVIGATION_PREFERENCE_SLICE_NOTE.md; frontend/src/shared/shell/KsSideNavigation.vue; frontend/src/shared/shell/KsAppShell.vue; frontend/src/shared/shell/navigationCatalog.ts; frontend/src/shared/shell/screenPreferenceStore.ts; frontend/src/shared/shell/tests/KsSideNavigation.contract.spec.ts; frontend/src/shared/shell/tests/navigationCatalog.spec.ts; frontend/src/shared/ui/layouts/tests/layout.contract.spec.ts; evidence/V13-FE-006/navigation-contract_20260813.log; evidence/V13-FE-006/navigation-preference_20260813.log; evidence/V13-FE-006/navigation-browser-contract_20260813.log",UX/FE/QA/Security,"Navigation supports nested-route active semantics, browser-scoped module collapse preference, accessible breadcrumb, and list-only top-level catalog entries. Parameterized detail routes are excluded from navigation while remaining routable. Actual evidence: navigation catalog 1 file/4 tests PASS, typecheck PASS, build PASS, Playwright browser snapshot captured. Known >500 kB warning and an initial console error remain; auth integration, mobile, visual/AT and production evidence remain outstanding. No completion claim."
|
||||||
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."
|
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,"Scope remains adapter-neutral T01 composition: list body plus optional detail region, evidence metadata, forbidden content suppression, and retry forwarding. Actual targeted evidence: 1 file / 4 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."
|
||||||
|
V13-FE-012,S8,Cross,T02 상세조회 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-012_T02_DETAIL_READ_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/DetailReadPage.vue; frontend/src/shared/ui/screen-types/tests/DetailReadPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. As-of/version metadata, evidence slot, forbidden suppression, and retry forwarding are characterized. Actual evidence: 1 file / 2 tests and typecheck passed. Production API wiring, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-013,S6,Cross,T03 등록편집 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-013_T03_EDIT_FORM_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/EditFormPage.vue; frontend/src/shared/ui/screen-types/tests/EditFormPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added dirty/readonly state priority and characterized submit/retry boundaries. Actual evidence: 1 file / 2 tests and typecheck passed. Mutation contract, If-Match/idempotency, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-014,S7,Cross,T04 MasterDetail 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-014_T04_MASTER_DETAIL_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/MasterDetailCrudPage.vue; frontend/src/shared/ui/screen-types/tests/MasterDetailCrudPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added version metadata propagation and unauthorized/forbidden detail suppression; actual evidence: 1 file / 2 tests and typecheck passed. Route selection, conflict policy, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-015,S7,Cross,T05 검토승인 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-015_T05_APPROVAL_WORKBENCH_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/ApprovalWorkbenchPage.vue; frontend/src/shared/ui/screen-types/tests/ApprovalWorkbenchPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added version metadata and characterized queue/detail/decision slots plus conflict suppression/retry. Actual evidence: 1 file / 2 tests and typecheck passed. Maker-checker runtime, evidence hash, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-016,S6,Cross,T06 Wizard 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-016_T06_WIZARD_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/StepWizardPage.vue; frontend/src/shared/ui/screen-types/tests/StepWizardPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added version metadata and blocked default actions for unsafe states; actual evidence: 1 file / 2 tests and typecheck passed. Resume/branch/validation, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-017,S11,Cross,T07 Dashboard 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-017_T07_SCORECARD_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/ScorecardDashboardPage.vue; frontend/src/shared/ui/screen-types/tests/ScorecardDashboardPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added version metadata and characterized partial dashboard slots plus forbidden suppression/retry. Actual evidence: 1 file / 2 tests and typecheck passed. Metric approval, visual/AT, browser E2E, and G4-A evidence remain outstanding."
|
||||||
|
V13-FE-018,S8,Cross,T08 Batch운영 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-018_T08_BATCH_OPERATIONS_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/BatchOperationsPageV2.vue; frontend/src/shared/ui/screen-types/tests/BatchOperationsPageV2.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added version metadata and characterized run summary/timeline/records/reprocess/runbook slots plus blocked-state suppression/retry. Actual evidence: 1 file / 2 tests and typecheck passed. JobRun/Watermark/idempotency API contract, visual/AT, browser E2E, and approval evidence remain outstanding."
|
||||||
|
V13-FE-019,S8,Cross,T09 대사예외 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-019_T09_RECONCILIATION_EXCEPTION_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/ReconciliationExceptionPage.vue; frontend/src/shared/ui/screen-types/tests/ReconciliationExceptionPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added evidence version metadata and characterized break/before-after/correction/audit slots plus forbidden suppression/retry. Actual evidence: 1 file / 2 tests. Permission mapping, correction maker-checker/API contract, visual/AT, browser E2E, and G3 approval remain outstanding."
|
||||||
|
V13-FE-028,S8,Cross,대사·정정 화면 적용,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-028_RECONCILIATION_API_CONTRACT_SLICE_NOTE.md; frontend/src/features/reconciliation/schema.ts; frontend/src/features/reconciliation/api.ts; frontend/src/features/reconciliation/queries.ts; frontend/src/features/reconciliation/tests/schema.spec.ts; frontend/src/features/reconciliation/tests/queries.spec.ts",FE/Ops,"Dependency V13-FE-019 is IN_PROGRESS. Added runtime-validated read adapters and TanStack Query keys/hooks for the two existing reconciliation GET endpoints (2 files / 5 tests). Route/mutation wiring is intentionally withheld pending approved permissions, pagination/version contract, maker-checker correction API, and G3 evidence."
|
||||||
|
V13-FE-020,S11,Cross,T10 버전거버넌스 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-020_T10_VERSION_GOVERNANCE_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/VersionGovernancePage.vue; frontend/src/shared/ui/screen-types/tests/VersionGovernancePage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. Added blocked-state suppression and characterized version comparison/evidence/approval/rollback slots (1 file / 2 tests). Automatic promotion/rollback remains disabled; model API, gate-pack, permission, visual/AT/Playwright, and G4-A evidence remain outstanding."
|
||||||
|
V13-FE-034,S7,Cross,Idempotency retry contract,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-034_IDEMPOTENCY_RETRY_CONTRACT_SLICE_NOTE.md; frontend/src/shared/commands/idempotency.ts; frontend/src/shared/crud/useOptimisticCommand.ts; frontend/src/shared/crud/tests/useOptimisticCommand.spec.ts",FE/BE/QA,"Existing client command boundary is now WBS-tracked: one immutable key per intent, same key on retry, If-Match forwarding, 409/412 conflict handling, and pending guard. Actual evidence is 1 file / 2 tests plus full FE regression. Server deduplication, replay equivalence, retention, and endpoint integration remain outstanding."
|
||||||
|
V13-FE-036,S8,Cross,T12 작업 큐 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-036_T12_WORK_QUEUE_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/WorkQueuePage.vue; frontend/src/shared/ui/screen-types/screenRecipe.ts; frontend/src/shared/ui/screen-types/tests/workQueueRecipe.spec.ts; frontend/src/shared/ui/screen-types/tests/WorkQueuePage.spec.ts",UX/FE/QA/Domain Owner,"Added version metadata and adopted KBX T12 queue recovery/security policy metadata without inventing queue APIs or commands. Actual evidence: 2 test files / 3 tests PASS and pnpm typecheck PASS. Queue-depth source, exception definition, JobRun API, visual/AT/Playwright, and operational approval remain outstanding."
|
||||||
|
V13-FE-037,S7,Cross,T11 대량 입력 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-037_T11_FAST_ENTRY_GRID_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/FastEntryGridPage.vue; frontend/src/shared/ui/screen-types/tests/FastEntryGridPage.spec.ts",UX/FE/QA/Domain Owner,"Added blocked-state suppression and characterized grid/validation summary slots with version metadata (1 file / 2 tests). Cell validation, paste audit, idempotency API, partial-result semantics, visual/AT/Playwright, and approval remain outstanding."
|
||||||
|
V13-FE-021,S6,Cross,Vee-validate/Zod standard form,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-021_FORM_CONTRACT_AUDIT_NOTE.md; frontend/src/shared/crud/StandardCrudFormPage.vue; frontend/src/shared/ui/screen-types/v2/EditFormPage.vue; frontend/src/shared/crud/formValidation.ts; frontend/src/shared/crud/tests/formValidation.spec.ts; frontend/src/shared/ui/screen-types/tests/EditFormPage.spec.ts; frontend/src/shared/crud/useOptimisticCommand.ts",FE Lead,"Added feature submit-boundary Zod validation and ProblemDetails field-error mapping with stable summary/field/form errors (1 file / 4 tests), while keeping generic shell presentation-only. Duplicate command server contract and form-specific integration evidence remain outstanding."
|
||||||
|
V13-FE-022,S6,Cross,Filter/page/tab URL state,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-022_URL_QUERY_CODEC_SLICE_NOTE.md; frontend/src/shared/crud/queryCodec.ts; frontend/src/shared/crud/tests/queryCodec.spec.ts",FE Lead/QA,"Dependency V13-FE-011 remains IN_PROGRESS. Optional sort/filter/operator allowlists now fail closed when supplied; existing callers retain behavior until a resource contract supplies an approved allowlist. Actual evidence: 1 file / 3 tests and typecheck passed. Router wiring and browser deep-link evidence remain outstanding."
|
||||||
|
V13-FE-023,S6,Cross,AG Grid server-side contract,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-023_SERVER_GRID_CONTRACT_SLICE_NOTE.md; frontend/src/shared/ui/DataGridShell.vue; frontend/src/shared/ui/tests/DataGridShell.spec.ts",FE Lead/BE/QA,"Dependency V13-FE-004 is COMPLETED. Optional server page metadata and shared paginator event boundary added without client-side data ownership. Actual evidence: 1 file / 2 tests and typecheck passed. Sort/filter mapping, column-state persistence, API integration, visual/AT, and browser evidence remain outstanding."
|
||||||
|
V13-FE-035,S8,Cross,Data freshness/version standard,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-035_FRESHNESS_BOUNDARY_SLICE_NOTE.md; docs/CURRENT/V13-FE-035_KBX_FRESHNESS_INDICATOR_ADOPTION_SLICE_NOTE.md; frontend/src/shared/status/DataFreshnessBadge.vue; frontend/src/shared/status/tests/DataFreshnessBadge.spec.ts",FE/Data/QA,"KBX freshness indicator principles adopted: explicit clock, optional source/revision, stale label, and opt-in refresh command. Actual evidence: targeted Vitest 1 file/3 tests PASS and pnpm typecheck PASS. Production published_at/revision wiring, approved freshness windows, visual, browser, and PIT evidence remain outstanding."
|
||||||
|
V13-FE-009,S0,Cross,OpenAPI-Zod 생성 전략 ADR,COMPLETED,2026-08-12,"docs/DECISIONS/ADR-FE-CONTRACT-001.md; frontend/src/shared/api/client.ts; frontend/src/shared/api/problem.ts; frontend/src/shared/api/tests/problem.spec.ts; docs/CURRENT/ARTIFACTS/AEG-X-005_ENDPOINT_AUTHORITY_HARDENING_20260812.md",BE/FE Architect,"Dependency AEG-X-002 is COMPLETED. ADR fixes the current axios→feature API→Zod→TanStack Query boundary and fail-closed generated-client gate. Actual evidence: FE tests 34 files/75 tests, typecheck and host-triggered production build PASS on 2026-08-12. No generated client, OMS contract, or production OpenAPI claim is made."
|
||||||
|
V13-FE-010,S0,Cross,PrimeVue unstyled/bootstrap 연결,COMPLETED,2026-08-12,"docs/CURRENT/V13-FE-010_UI_BOOTSTRAP_SLICE_NOTE.md; frontend/src/main.ts; frontend/src/shared/ui/provider/resolveUiProvider.ts; frontend/src/shared/ui/provider/tests/resolveUiProvider.spec.ts",FE Lead/QA,"Dependency V13-FE-004 is COMPLETED. Existing provider port is preserved; unsupported adapter values fail before mount and the validated native adapter is installed before mount. Actual evidence: targeted 1 file/3 tests and full FE regression 34 files/76 tests plus typecheck passed. Browser E2E, visual, AT, and deployment evidence are not claimed."
|
||||||
|
V13-FE-007,S0,Cross,11-state Matrix component,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-007_CANONICAL_STATE_PANEL_SLICE_NOTE.md; docs/CURRENT/V13-FE-007_KBX_STATUS_TAG_ADOPTION_SLICE_NOTE.md; frontend/src/shared/ui/feedback/StandardStatePanel.vue; frontend/src/shared/ui/components/KsStatusTag.vue; frontend/src/shared/ui/components/tests/KsStatusTag.spec.ts",UX/FE/QA,"StandardStatePanel contract remains implemented; KBX status-tag adoption adds semantic/unknown metadata and non-colour cues. Actual evidence: prior 15 tests plus targeted KsStatusTag 1/1 and typecheck PASS. Visual, AT, forced-colors, browser E2E, and production evidence remain outstanding; no completion claim."
|
||||||
|
V13-FE-008,S0,Cross,금융 formatter 중앙화,COMPLETED,2026-08-12,"docs/CURRENT/V13-FE-008_FINANCIAL_FORMATTER_SLICE_NOTE.md; frontend/src/shared/formatters/financial.ts; frontend/src/shared/formatters/tests/financial.spec.ts",FE Lead/Quant/QA,"Dependency V13-FE-002 is COMPLETED. Existing centralized formatter preserved and its explicit currency, decimal ratio, bounded quantity, KST as-of, missing/invalid input contracts are characterized. Actual evidence: 1 file / 5 tests and typecheck passed. Locale matrix, visual, and production evidence are not claimed."
|
||||||
|
V13-FE-024,S6,Cross,Permission/Capability Guard integration,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-024_PERMISSION_ROUTE_META_SLICE_NOTE.md; frontend/src/app/router.ts; frontend/src/shared/auth/routeAccess.ts; frontend/src/shared/auth/tests/routeAccess.spec.ts",FE/Security,"Dependency V13-FE-006 is COMPLETED. Evidence-backed model.read metadata and pure fail-closed policy added for ModelOps routes. Auth permission hydration, global navigation guard, complete route catalog, and unauthorized information exposure acceptance remain Decision Required."
|
||||||
|
V13-FE-033,S7,Cross,ProblemDetails mapping,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-033_KBX_PROBLEM_DETAILS_ADOPTION_SLICE_NOTE.md; docs/CURRENT/V13-FE-033_KBX_PROBLEM_DETAILS_BE_ADOPTION_SLICE_NOTE.md; frontend/src/shared/api/problem.ts; frontend/src/shared/api/tests/problem.spec.ts; src/KArtSell.Host/OpenApi/ProblemDetailsOperationFilter.cs",FE/BE/QA,"KBX v60 discriminator/recovery metadata adopted at FE Zod and BE OpenAPI documentation boundaries. Actual evidence: FE targeted Vitest 1 file/9 tests PASS, FE typecheck PASS, and dotnet build src/KArtSell.Host/KArtSell.Host.csproj -c Release --no-restore PASS (0 warnings/0 errors). BE payload parity, naming/redaction/retry approval, and browser state evidence remain Decision Required."
|
||||||
|
V13-FE-011,S6,Cross,T01 검색목록 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-011_KBX_T01_RECIPE_ADOPTION_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/screenRecipe.ts; frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue; frontend/src/shared/ui/screen-types/tests/screenRecipe.spec.ts",UX/FE/QA,"KBX v60 T01 recipe policies adopted as immutable metadata without copying OMS/API/vendor code. Actual evidence: targeted Vitest 2 files/5 tests PASS and pnpm typecheck PASS. Real API/bulk-selection/permission contract, visual/AT, and Playwright evidence remain Decision Required."
|
||||||
|
V13-FE-023,S6,Cross,AG Grid server-side contract,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-023_KBX_GRID_STATUS_ADOPTION_SLICE_NOTE.md; frontend/src/shared/ui/gridColumnAdapter.ts; frontend/src/shared/ui/gridStatus.ts; frontend/src/shared/ui/adapter/contracts.ts; frontend/src/shared/ui/tests/gridColumnAdapter.spec.ts; frontend/src/shared/ui/tests/gridStatus.spec.ts; frontend/src/features/models/pages/ModelsList.vue; frontend/src/features/shadow-run/pages/ShadowRunList.vue; evidence/V13-FE-023/frontend-regression_20260813.log; evidence/V13-FE-023/models-grid-port_20260813.log; evidence/V13-FE-023/shadow-run-grid-port_20260813.log; evidence/V13-FE-038/bundle-baseline_20260813.log",FE Lead/BE/QA,"KBX registry columns are mapped through the provider-neutral UiGridColumn contract; ModelsList and ShadowRunList use KsDataGrid with explicit Model.modelId and ShadowRun.runId navigation. Actual evidence: targeted adapter tests PASS, full frontend regression 58 files/153 tests PASS, typecheck PASS, build PASS. Known >500 kB build warning remains; server query mapping, status-map approval, visual/AT/browser evidence remain Decision Required."
|
||||||
|
V13-FE-038,S11,Cross,대량 화면 성능 budget,IN_PROGRESS,TBD,"evidence/V13-FE-038/bundle-baseline_20260813.log; evidence/V13-FE-038/chunk-split-attempt_20260813.log; evidence/V13-FE-038/lazy-adapter_20260813.log; evidence/V13-FE-038/aggrid-module-scope_20260813.log; frontend/src/shared/ui/adapter/primevue/index.ts; frontend/src/shared/ui/adapter/primevue/AgGridAdapter.vue",FE/SRE/QA,"AG Grid adapter now registers only ClientSideRowModelModule instead of AllCommunityModule. Actual evidence: 63 files/168 tests PASS, typecheck PASS, build PASS; AgGridAdapter reduced 1,027,848 -> 588,718 bytes (gzip 285.75 -> 163.66 kB), but Vite >500 kB warning persists. Two manualChunks attempts produced no additional reduction and were reverted. No performance gate PASS or browser/network budget claim."
|
||||||
|
|||||||
|
@@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
### Source
|
### Source
|
||||||
|
|
||||||
- `docs/Design/kbx-foundation-v36/docs/design-token-policy-v4.md`: primitive → semantic → component 토큰, 밀도는 배치·form·keyboard 계약을 바꾸지 않음.
|
- `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/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-v60-status-canonical-contract-hardening/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가 아님.
|
- `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/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/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/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 토큰.
|
- `frontend/src/design-system/tokens.css`: 현재 primitive와 일부 semantic/component 토큰.
|
||||||
@@ -79,7 +79,7 @@ KBX의 구현물을 가져오지 않고 다음 불변식을 K-ArtSell의 현 경
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `python tools/validate_v16.py` | `PASS=1 WARN=2 FAIL=0`, exit 0 (2026-08-09) |
|
| `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` 외 위치를 실패 처리 |
|
| Vendor boundary | validator가 `frontend/src`의 `.ts`/`.vue`에서 PrimeVue·AG Grid import를 검사하고 `shared/ui/adapter/primevue` 외 위치를 실패 처리 |
|
||||||
| 범위 | 코드/토큰 값/화면 동작은 변경하지 않음. 사용자 제공 `docs/Design/kbx-foundation-v36/`는 추적·수정하지 않음. |
|
| 범위 | 코드/토큰 값/화면 동작은 변경하지 않음. 사용자 제공 `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/`는 추적·수정하지 않음. |
|
||||||
|
|
||||||
경고 2건은 full source archive 및 승인 런타임이 없다는 내용이며, .NET/DB/Playwright/Shadow 결과를 통과로 주장하지 않는다.
|
경고 2건은 full source archive 및 승인 런타임이 없다는 내용이며, .NET/DB/Playwright/Shadow 결과를 통과로 주장하지 않는다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# V13-FE-003 — UI Adapter Port 재검증
|
# V13-FE-003 — UI Adapter Port 재검증 (historical contract)
|
||||||
|
|
||||||
|
> Superseded for existing application-owned components by `V13-FE-005_COMPONENT_DIRECT_VENDOR_RESTORE_SLICE_NOTE.md`. The adapter contract remains valid for newly created vendor-neutral components; it is not the functional ceiling for existing `Ks*` components.
|
||||||
|
|
||||||
| 항목 | 값 |
|
| 항목 | 값 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# V13-FE-004 — Provider Adapter 구현 재검증
|
# V13-FE-004 — Provider Adapter 구현 재검증 (new-component scope)
|
||||||
|
|
||||||
| 항목 | 값 |
|
| 항목 | 값 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## Source / Assumption / Unknown / Decision Required
|
## 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.
|
- **Source:** PrimeVue/AG Grid imports remain available in the approved adapter directory for new vendor-neutral components. Existing `Ks*` components are direct-vendor components under the direct ownership Slice and no longer consume `UiAdapter` for rendering.
|
||||||
- **Assumption:** the existing WCAG 2.2 AA target is the applicable baseline; this test run is contract-level evidence, not an assistive-technology audit.
|
- **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.
|
- **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.
|
- **Decision Required:** no new provider or provider-specific component is introduced by KBX reuse.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# V13-FE-006 — navigation contract hardening
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-006
|
||||||
|
- **Requirement/API/UI/Test:** REQ-FE-NAV / Cross / UI-NAV-01 / T-FE-NAV-01
|
||||||
|
- **Source:** current `KsSideNavigation`, `navigationCatalog`, router meta, and existing AppShell tests.
|
||||||
|
- **Change:** nested detail routes now activate their parent navigation item and expose `aria-current="page"`.
|
||||||
|
- **Not changed:** route catalog, permissions, capability flags, menu labels, or navigation persistence.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Navigation contract test: 1 file / 2 tests passed.
|
||||||
|
- Full frontend regression: 62 files / 162 tests passed.
|
||||||
|
- `pnpm typecheck`: passed.
|
||||||
|
- `pnpm build`: passed; known Vite >500 kB chunk warning remains.
|
||||||
|
- Visual/AT/browser evidence remains outstanding.
|
||||||
|
- Evidence: `evidence/V13-FE-006/navigation-contract_20260813.log`.
|
||||||
|
- Menu search hardening: results now expose stable option IDs and the input tracks the active option through `aria-activedescendant`/`aria-controls`; empty results remove the active descendant.
|
||||||
|
- Menu search contract test: 1 file / 2 tests passed.
|
||||||
|
- Evidence: `evidence/V13-FE-006/menu-search-contract_20260813.log`.
|
||||||
|
- Permission metadata hardening: navigation entries now preserve route permission metadata and expose a pure fail-closed `filterNavigationEntries` function. AppShell wiring remains deferred until a reactive approved auth source exists.
|
||||||
|
- Catalog contract evidence: 3 files / 7 tests passed; typecheck and build passed.
|
||||||
|
- Evidence: `evidence/V13-FE-006/navigation-permission-contract_20260813.log`.
|
||||||
|
- Module section hardening: side navigation sections can collapse/expand with `aria-expanded`; route and permission behavior remain unchanged.
|
||||||
|
- Targeted navigation test: 1 file / 3 tests passed; typecheck and build passed.
|
||||||
|
- Evidence: `evidence/V13-FE-006/navigation-section-collapse_20260813.log`.
|
||||||
|
- Breadcrumb hardening: `KsAppShell` now exposes `홈 → 현재 화면` through an accessible `현재 위치` navigation landmark using route metadata.
|
||||||
|
- Breadcrumb contract test: 1 file / 2 tests passed; typecheck and build passed.
|
||||||
|
- Evidence: `evidence/V13-FE-006/breadcrumb-contract_20260813.log`.
|
||||||
|
- Browser contract evidence: Playwright snapshot confirmed shell landmarks, breadcrumb, and list-only top-level navigation. Parameterized detail routes are excluded from the catalog.
|
||||||
|
- Evidence: `evidence/V13-FE-006/navigation-browser-contract_20260813.log`.
|
||||||
|
- Mobile navigation hardening: added a responsive drawer toggle, backdrop close, Escape close, and explicit open/close labels without changing desktop route or permission behavior.
|
||||||
|
- Evidence: `evidence/V13-FE-006/mobile-navigation-contract_20260813.log`.
|
||||||
|
- Mobile browser evidence: at 390x844, menu open, drawer/backdrop close, and Escape close were reproduced with Playwright.
|
||||||
|
- Evidence: `evidence/V13-FE-006/mobile-browser-contract_20260813.log`.
|
||||||
|
- Focus hardening: mobile drawer now focuses its close control on open, traps Tab/Shift+Tab within drawer controls, and returns focus to the header menu button on close/Escape/backdrop.
|
||||||
|
- Evidence: `evidence/V13-FE-006/mobile-focus-contract_20260813.log`.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# V13-FE-006 — navigation section preference persistence
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-006
|
||||||
|
- **Requirement/API/UI/Test:** REQ-FE-NAV / Cross / UI-NAV-01 / T-FE-NAV-PREFERENCE
|
||||||
|
- **Source:** existing versioned `screenPreferenceStore` and `KsSideNavigation` collapse contract.
|
||||||
|
- **Change:** persist collapsed module names in the existing browser UI preference record and wire the shell as a controlled state boundary.
|
||||||
|
- **Not changed:** authentication, authorization, route catalog, API data, or business state.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** `ks.shell.screenPreference.v1` localStorage preference boundary.
|
||||||
|
- **Assumption:** section collapse is browser UI preference, not user/account data; existing browser storage scope is acceptable.
|
||||||
|
- **Unknown:** cross-device/user profile synchronization and mobile drawer behavior.
|
||||||
|
- **Decision Required:** account-scoped preference migration and mobile UX remain separate decisions.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted navigation contract: 1 file / 3 tests passed.
|
||||||
|
- `pnpm typecheck`: passed.
|
||||||
|
- `pnpm build`: passed; known Vite >500 kB chunk warning remains.
|
||||||
|
- Evidence: `evidence/V13-FE-006/navigation-preference_20260813.log`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# V13-FE-007 — Canonical state panel slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-STATE
|
||||||
|
- **API/UI/Test:** UI-FOUND-07 / T-FE-STATE-01
|
||||||
|
- **Source:** v60 canonical status presentation; current `StandardUiState` and `StandardScreenState` contracts.
|
||||||
|
- **Assumption:** `StandardStatePanel` is the legacy/common feedback component covered by this WBS item; screen-boundary integration remains separately owned by screen-type tests.
|
||||||
|
- **Unknown:** Product copy and visual/assistive-technology approval for each state are not stored as release evidence.
|
||||||
|
- **Decision Required:** UX/QA must approve final state copy and visual/AT baselines before claiming those evidence classes.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Aligned `StandardStatePanel` with the active screen state contract by adding `FORBIDDEN`.
|
||||||
|
- Characterized all 13 declared states, including READY, rendering behavior, explicit retry opt-in, and forbidden-state no-retry behavior.
|
||||||
|
- Kept state rendering presentation-only; it does not perform authorization, network, persistence, or policy decisions.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/feedback/tests/StandardStatePanel.spec.ts
|
||||||
|
PASS: 1 file / 15 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No visual, assistive-technology, browser E2E, or production evidence is claimed.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# V13-FE-007 — KBX Status component adoption
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **Source:** KBX v59/v60 status dictionary guidance and `packages/kbx-ui/src/components/KbxStatus.vue` principles.
|
||||||
|
- **Adopted:** vendor-neutral `KsStatusTag` now accepts semantic and unknown metadata; native and PrimeVue adapters expose text, ARIA label, semantic metadata, and a non-colour cue.
|
||||||
|
- **Not adopted:** direct vendor imports in feature code, new domain statuses, OMS catalogs, or automatic status translation.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** current `KsStatusTag` adapter port and `gridStatus.ts` resolver.
|
||||||
|
- **Assumption:** callers pass a label already resolved from an approved status map; the component does not mutate raw API values.
|
||||||
|
- **Unknown:** final visual tokens and forced-colors baseline for each provider.
|
||||||
|
- **Decision Required:** UX/QA approval of visual, screen-reader, and forced-colors evidence.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted Vitest: 1 file / 1 test passed.
|
||||||
|
- `pnpm typecheck`: passed (`vue-tsc --noEmit`).
|
||||||
|
|
||||||
|
Visual, assistive-technology, and browser evidence remain outstanding.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# V13-FE-008 — Financial formatter slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-FORMAT
|
||||||
|
- **API/UI/Test:** UI-FOUND-08 / T-FE-FMT-01
|
||||||
|
- **Source:** v60 unit/display-boundary guidance; current `frontend/src/shared/formatters/financial.ts`.
|
||||||
|
- **Assumption:** Formatter inputs are already normalized domain values at the FE boundary: currency amount, decimal ratio, quantity, and an instant for as-of display.
|
||||||
|
- **Unknown:** Per-account currency and locale policy is not part of this Slice.
|
||||||
|
- **Decision Required:** Quant/QA must approve any future rounding or locale change; formatters must not silently infer units.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Preserved the existing centralized formatter implementation.
|
||||||
|
- Characterized missing values, explicit currency marker, decimal-to-percent conversion, bounded quantity precision, KST as-of display, and invalid date handling.
|
||||||
|
- No domain calculation, rounding policy, API contract, or database value was changed.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/formatters/tests/financial.spec.ts
|
||||||
|
PASS: 1 file / 5 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No visual, locale-matrix, or production evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-010 — UI bootstrap slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-BOOT
|
||||||
|
- **API/UI/Test:** UI-FOUND-10 / T-FE-SMOKE-01
|
||||||
|
- **Source:** v60 provider/bootstrap fail-fast boundary; current `frontend/src/main.ts` and `shared/ui/provider` implementation.
|
||||||
|
- **Assumption:** `native-accessible` is the deterministic smoke-test provider because it does not require browser vendor runtime setup.
|
||||||
|
- **Unknown:** Browser-level production startup under every deployment-specific `VITE_UI_ADAPTER` value remains outside this unit slice.
|
||||||
|
- **Decision Required:** No additional provider or vendor may be introduced without a new adapter contract decision.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Preserve dynamic `resolveUiProvider` selection and reject unsupported values before mount.
|
||||||
|
- Install the validated adapter before application mount through the provider port.
|
||||||
|
- Add a smoke test proving the installed adapter is available through the shared adapter injection boundary.
|
||||||
|
- Do not import the v60 KBX package, router, store, permission host, or OMS screens.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/provider/tests/resolveUiProvider.spec.ts
|
||||||
|
PASS: 1 file / 3 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm test; pnpm typecheck
|
||||||
|
PASS: 34 files / 76 tests; typecheck PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
This slice does not claim browser E2E, visual, assistive-technology, or production deployment evidence.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# V13-FE-011 — KBX v60 T01 recipe adoption
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-011
|
||||||
|
- **Requirement/API/UI/Test:** REQ-FE-T01 / Cross / UI-T01 / T-FE-T01-01
|
||||||
|
- **Source:** `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/contracts/screens/kbx.screen-recipes.json` T01 recipe.
|
||||||
|
- **Adopted:** immutable T01 required/recovery/security policy metadata in `frontend/src/shared/ui/screen-types/screenRecipe.ts`.
|
||||||
|
- **Not adopted:** KBX OMS routes, API calls, server-side selection implementation, vendor UI code, generated scaffolding, or client-side server state.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** KBX v60 T01 recipe and current `SearchListCrudPage.vue`/`StandardScreenBoundary.vue`.
|
||||||
|
- **Assumption:** recipe policy metadata documents caller obligations; it does not enforce or invent a backend contract.
|
||||||
|
- **Unknown:** the approved production search/read-model contract, bulk-selection API and permission names for each K-ArtSell screen.
|
||||||
|
- **Decision Required:** UX/FE/Domain Owner must approve screen-specific policy bindings before a template is wired to a real API or bulk command.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted Vitest: 2 files / 5 tests passed.
|
||||||
|
- `pnpm typecheck`: passed (`vue-tsc --noEmit`).
|
||||||
|
|
||||||
|
## Remaining acceptance evidence
|
||||||
|
|
||||||
|
Visual/assistive-technology approval, Playwright trace, real API transcript, and approved screen-specific permission/bulk-selection contracts remain outstanding.
|
||||||
@@ -27,6 +27,6 @@
|
|||||||
|
|
||||||
## Actual execution 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 test -- src/shared/ui/screen-types/tests/SearchListCrudPage.spec.ts` — 1 file / 4 tests passed, exit 0. Covers list/detail composition, forbidden content suppression, and retry forwarding with the validated native adapter fixture.
|
||||||
- `pnpm typecheck` — `vue-tsc --noEmit`, exit 0.
|
- `pnpm typecheck` — `vue-tsc --noEmit`, exit 0.
|
||||||
- Preserved output: `evidence/V13-FE-011/t01-search-list-layout_20260809.log`.
|
- Preserved output: `evidence/V13-FE-011/t01-search-list-layout_20260809.log`.
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-012 — T02 detail-read template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T02
|
||||||
|
- **API/UI/Test:** UI-T02 / T-FE-T02-01
|
||||||
|
- **Source:** v60 evidence/version detail boundary; current `DetailReadPage.vue`, `PageLayout`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** The caller supplies an approved evidence context; the template only presents it and does not manufacture evidence.
|
||||||
|
- **Unknown:** No active production detail screen currently wires a versioned domain response through this template.
|
||||||
|
- **Decision Required:** Domain Owner/QA must approve per-resource evidence fields and visual/AT/browser acceptance before MVP/G3 claims.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Characterized as-of and version metadata propagation.
|
||||||
|
- Characterized evidence slot composition.
|
||||||
|
- Characterized forbidden-state content suppression and retry forwarding.
|
||||||
|
- Kept the template presentation-only; no API, permission source, or domain policy was introduced.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/DetailReadPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No production API, visual, AT, or browser E2E evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-013 — T03 edit-form template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T03
|
||||||
|
- **API/UI/Test:** UI-T03 / T-FE-T03-01
|
||||||
|
- **Source:** v60 dirty/readonly form boundary; current `EditFormPage`, `StandardCrudFormPage`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** Dirty and readonly flags are caller-owned UI state; validation, If-Match, idempotency, and authorization remain feature/API responsibilities.
|
||||||
|
- **Unknown:** No active production form currently wires this generic T03 template to a versioned mutation contract.
|
||||||
|
- **Decision Required:** UX/QA/Domain Owner must approve form-level accessibility, conflict, validation, and browser evidence before MVP-A completion.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Added optional `dirty` and `readonly` inputs to `EditFormPage`.
|
||||||
|
- Standardized state priority as `READONLY > DIRTY > supplied state`.
|
||||||
|
- Preserved submit/retry event boundaries and evidence metadata forwarding.
|
||||||
|
- Did not add domain fields, mutation logic, If-Match handling, or API calls.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/EditFormPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No visual, AT, browser E2E, or production mutation evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-014 — T04 master-detail template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T04
|
||||||
|
- **API/UI/Test:** UI-T04 / T-FE-T04-01
|
||||||
|
- **Source:** v60 route-selection/evidence boundary; current `MasterDetailCrudPage`, `PageLayout`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** Master/detail selection and evidence context are caller-owned; the template does not fetch, cache, or mutate records.
|
||||||
|
- **Unknown:** No active production master-detail screen currently supplies a domain version/conflict contract.
|
||||||
|
- **Decision Required:** UX/QA/Domain Owner must approve selection, conflict, accessibility, and browser acceptance before MVP-B completion.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Forwarded evidence `version` in addition to `asOf`.
|
||||||
|
- Prevented detail-slot content exposure for `UNAUTHORIZED` and `FORBIDDEN` states while preserving the shared state boundary.
|
||||||
|
- Characterized master/detail slot composition and retry forwarding.
|
||||||
|
- Did not add route selection state, API calls, or domain conflict policy.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/MasterDetailCrudPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No visual, AT, browser E2E, or production API evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-015 — T05 approval workbench template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T05
|
||||||
|
- **API/UI/Test:** UI-T05 / T-FE-T05-01
|
||||||
|
- **Source:** v60 maker-checker/evidence presentation boundary; current `ApprovalWorkbenchPage`, `ReviewWorkbenchLayout`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** Queue, detail, decision, and evidence data are caller-owned; this template does not approve, publish, or mutate anything.
|
||||||
|
- **Unknown:** No active production approval screen currently wires maker/checker identity, evidence hash, or warning acknowledgement through this template.
|
||||||
|
- **Decision Required:** Compliance/QA/Domain Owner must approve maker-checker, reason, expiry, visual/AT, and browser evidence before MVP-B completion.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Forwarded evidence `version` to the page metadata.
|
||||||
|
- Characterized queue/detail/decision slot composition.
|
||||||
|
- Characterized conflict-state decision content suppression and retry forwarding.
|
||||||
|
- Kept the component presentation-only; no approval policy or mutation path was introduced.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/ApprovalWorkbenchPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No maker-checker runtime, visual, AT, browser E2E, or production evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-016 — T06 wizard template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T06
|
||||||
|
- **API/UI/Test:** UI-T06 / T-FE-T06-01
|
||||||
|
- **Source:** v60 resume/action-state boundary; current `StepWizardPage`, `PageLayout`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** Step progression and validation are caller-owned; this template only exposes navigation intents.
|
||||||
|
- **Unknown:** No active production wizard currently supplies resume/branch/impact-revalidation contracts.
|
||||||
|
- **Decision Required:** UX/QA/Domain Owner must approve step validation, resume, branch, accessibility, and browser evidence before MVP-A completion.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Forwarded evidence `version` metadata.
|
||||||
|
- Blocked default wizard actions during `LOADING`, `ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `CONFLICT`, `EXPIRED`, `READONLY`, and `PROCESSING` states.
|
||||||
|
- Preserved previous/next/finish event boundaries in actionable states.
|
||||||
|
- Did not add domain branching, persistence, validation, or mutation logic.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/StepWizardPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No visual, AT, browser E2E, or production workflow evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-017 — T07 dashboard/scorecard template slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-T07
|
||||||
|
- **API/UI/Test:** UI-T07 / T-FE-T07-01
|
||||||
|
- **Source:** v60 metric-definition/version boundary; current `ScorecardDashboardPage`, `DashboardLayout`, and `StandardScreenBoundary` contracts.
|
||||||
|
- **Assumption:** KPI, primary, secondary, alert, and metric-definition content is caller-owned; the template does not calculate or publish metrics.
|
||||||
|
- **Unknown:** No active production dashboard currently supplies approved metric definitions and sample-size evidence through this template.
|
||||||
|
- **Decision Required:** Quant/QA/Domain Owner must approve metric definition, partial-data semantics, accessibility, visual, and browser evidence before G4-A completion.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Forwarded evidence `version` metadata.
|
||||||
|
- Characterized all dashboard slots under a `PARTIAL` snapshot.
|
||||||
|
- Characterized forbidden content suppression and retry forwarding after an error state.
|
||||||
|
- Did not add metric calculation, threshold, or publication logic.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/screen-types/tests/ScorecardDashboardPage.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No metric approval, visual, AT, browser E2E, or production evidence is claimed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-018 — T08 Batch Operations Screen Template
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening`; existing `BatchOperationsPageV2.vue` and `StandardScreenBoundary` contracts.
|
||||||
|
- Assumption: batch run summary, timeline, records, reprocess, and runbook are presentation slots; JobRun/Watermark semantics remain server-side contracts.
|
||||||
|
- Unknown: approved batch API, JobRun schema, replay/idempotency contract, ownership, alert, and runbook evidence.
|
||||||
|
- Decision Required: approve the production batch API and operational safety contract before wiring mutations or schedules.
|
||||||
|
|
||||||
|
## Implemented boundary
|
||||||
|
|
||||||
|
- Propagates evidence `version` through `PageLayout`.
|
||||||
|
- Preserves named slots for summary, timeline, records, reprocess, and runbook.
|
||||||
|
- Suppresses operational content while loading, processing, error, unauthorized, forbidden, conflict, expired, or readonly; retry remains owned by the shared boundary.
|
||||||
|
- No scheduler, reprocess mutation, automatic model promotion, order, or KIS path was introduced.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Command: `pnpm test -- src/shared/ui/screen-types/tests/BatchOperationsPageV2.spec.ts`
|
||||||
|
|
||||||
|
- Actual result: 1 file / 2 tests passed.
|
||||||
|
- `pnpm typecheck`: passed.
|
||||||
|
- `git diff --check`: passed; only existing LF/CRLF normalization warnings were emitted.
|
||||||
|
|
||||||
|
## Outstanding
|
||||||
|
|
||||||
|
Visual/AT/browser E2E evidence, batch API contract, JobRun/Watermark/idempotency evidence, metrics/alerts/runbook, and approval evidence remain outstanding. This Slice is intentionally `IN_PROGRESS`.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# V13-FE-019 — T09 Reconciliation Exception screen template
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: v60 screen contract/permission guidance and current `ReconciliationExceptionPage.vue` shared screen port.
|
||||||
|
- Assumption: before/after, correction, audit, and break content are evidence-bearing slots; API authority remains server-side.
|
||||||
|
- Unknown: approved reconciliation permissions, correction maker-checker contract, API schema, and Playwright/AT environment.
|
||||||
|
- Decision Required: UX/Domain/QA approval of responsive, accessibility, permission, and correction workflow evidence.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Propagates evidence version to the page header.
|
||||||
|
- Preserves T09 comparison, correction, audit, break, filter, action, and footer slots.
|
||||||
|
- Suppresses sensitive exception details and actions for blocked/terminal states; shared boundary retains retry behavior.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
`pnpm test -- src/shared/ui/screen-types/tests/ReconciliationExceptionPage.spec.ts`: 1 file / 2 tests passed.
|
||||||
|
|
||||||
|
- Full FE regression after the Slice: 46 files / 119 tests passed; `pnpm typecheck` passed; production Vite build completed with the existing large-chunk warning.
|
||||||
|
- Reconciliation backend characterization: PortfolioReconciliation filter 17 tests passed; ModelOperations reconciliation filter 3 tests passed.
|
||||||
|
- Visual/AT/Playwright evidence remains required before completion; this Slice is `IN_PROGRESS`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# V13-FE-020 — T10 Version Governance screen template
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: v60 screen recipe governance requirements and current `VersionGovernancePage.vue` shared screen port.
|
||||||
|
- Assumption: version comparison, evidence matrix, approval, and rollback are evidence-bearing presentation slots; activation remains human-approved.
|
||||||
|
- Unknown: approved model/policy API, gate-pack schema, same-dataset/cost definition, rollback contract, and permission mapping.
|
||||||
|
- Decision Required: Quant/Risk/QA approval before wiring activation or rollback commands.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Preserves version metadata and governance slots.
|
||||||
|
- Suppresses comparison/evidence/approval/rollback/footer content in blocked or terminal states.
|
||||||
|
- Keeps retry under the shared boundary; no automatic promotion, rollback, threshold mutation, or order path is added.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted test: 1 file / 2 tests passed.
|
||||||
|
- Typecheck and full FE regression evidence must be refreshed after this Slice; visual/AT/Playwright and G4-A evidence remain outstanding.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-021 — Form contract audit
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `frontend/src/shared/crud/StandardCrudFormPage.vue`, `frontend/src/shared/ui/screen-types/v2/EditFormPage.vue`, `FormPageLayout.vue`, and `useOptimisticCommand.ts`.
|
||||||
|
- Assumption: form rendering owns state/presentation; schema parsing and command idempotency belong at the feature submit boundary.
|
||||||
|
- Unknown: approved standard Zod form adapter, 422 ProblemDetails field mapping, and per-form If-Match contract.
|
||||||
|
- Decision Required: FE/BE/QA approval before introducing a shared form validation abstraction.
|
||||||
|
|
||||||
|
## Audit result
|
||||||
|
|
||||||
|
- Existing form screens provide dirty/readonly state precedence, shared retry, submit event forwarding, and evidence version display.
|
||||||
|
- Added `validateFormSubmission()` as an explicit feature submit-boundary helper; the generic form shell remains presentation-only.
|
||||||
|
- The helper returns typed data or a stable summary plus field/form-level errors.
|
||||||
|
- Added `mapProblemToFormFailure()` for server ProblemDetails `errors` without mutating the response.
|
||||||
|
- Duplicate-submit protection and `Idempotency-Key`/`If-Match` forwarding exist in `useOptimisticCommand`, not in the generic form shell.
|
||||||
|
- No new library or speculative abstraction was introduced because the approved form contract is missing.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `frontend/src/shared/ui/screen-types/tests/EditFormPage.spec.ts`: 1 file / 2 tests passed in the FE regression run.
|
||||||
|
- `frontend/src/shared/crud/tests/useOptimisticCommand.spec.ts`: 1 file / 2 tests passed.
|
||||||
|
- `frontend/src/shared/crud/tests/formValidation.spec.ts`: 1 file / 4 tests passed.
|
||||||
|
|
||||||
|
## Outstanding
|
||||||
|
|
||||||
|
Feature-specific integration, 422 transport tests, and approved form workflow evidence remain outstanding. This WBS item remains `IN_PROGRESS`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-022 — URL query codec slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-URL
|
||||||
|
- **API/UI/Test:** UI-URL-01 / T-FE-URL-01
|
||||||
|
- **Source:** v60 deep-link/canonical route boundary; current `frontend/src/shared/crud/queryCodec.ts`.
|
||||||
|
- **Assumption:** Each screen that consumes the codec can provide an approved field/operator allowlist from its resource contract.
|
||||||
|
- **Unknown:** No active production screen currently wires this codec to router state.
|
||||||
|
- **Decision Required:** FE/QA must approve per-resource filter/sort allowlists before enabling URL synchronization in a screen.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Added optional allowlists for sort fields, filter fields, and filter operators.
|
||||||
|
- When supplied, unapproved URL values are discarded and the approved fallback remains authoritative.
|
||||||
|
- Preserved existing behavior for callers that have not yet supplied a screen-specific allowlist.
|
||||||
|
- Kept URL state as a serializable query contract; no Pinia/server-state duplication was introduced.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/crud/tests/queryCodec.spec.ts
|
||||||
|
PASS: 1 file / 3 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Slice remains IN_PROGRESS until a production screen supplies an approved allowlist and deep-link browser evidence is collected.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# V13-FE-023 — KBX v59 grid status boundary adoption
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-023
|
||||||
|
- **Requirement/API/UI/Test:** REQ-FE-GRID / Cross / UI-GRID-01 / T-FE-GRID-01
|
||||||
|
- **Source:** `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/packages/kbx-ui/src/grid/status.ts` and v59 Grid Status Dictionary guidance.
|
||||||
|
- **Adopted:** pure `raw canonical value → label/semantic/unknown` resolver, label-based filtering/export helpers, and optional `statusMap` on the shared grid-column contract.
|
||||||
|
- **Not adopted:** OMS status catalog, backend status mutation, vendor grid APIs, filter/CSV behavior, or client-side data ownership.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** KBX resolver contract and current `UiGridColumn`/`DataGridShell` adapter boundary.
|
||||||
|
- **Assumption:** status maps are feature-owned declarations; shared UI owns only the resolver contract.
|
||||||
|
- **Unknown:** approved status dictionaries for each K-ArtSell feature and whether the current vendor adapters consume formatter/status metadata.
|
||||||
|
- **Decision Required:** FE/Domain Owner must approve each feature's status vocabulary and adapter rendering/filter/export mapping before production wiring.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted Vitest: 1 file / 4 tests passed.
|
||||||
|
- `pnpm typecheck`: passed (`vue-tsc --noEmit`).
|
||||||
|
- Grid boundary adoption: `ModelsList` now uses `KsDataGrid`; KBX registry columns are converted by `toUiGridColumns` and selected rows use the explicit `Model.modelId` contract.
|
||||||
|
- Targeted adapter Vitest: 1 file / 3 tests passed.
|
||||||
|
- Full frontend regression: 57 files / 150 tests passed, `pnpm typecheck` passed, and `pnpm build` passed. The known >500 kB chunk warning remains; no performance gate pass is claimed.
|
||||||
|
- Evidence: `evidence/V13-FE-023/models-grid-port_20260813.log`.
|
||||||
|
- `ShadowRunList` now uses the same provider-neutral grid boundary; row navigation is explicitly backed by `ShadowRun.runId`.
|
||||||
|
- Full frontend regression after this change: 58 files / 153 tests passed, typecheck passed, build passed. The known >500 kB chunk warning remains.
|
||||||
|
- Evidence: `evidence/V13-FE-023/shadow-run-grid-port_20260813.log`.
|
||||||
|
- Dead-code cleanup: removed unused `useRoute/route` bindings from both migrated list screens; retained the adapter export for compatibility because external consumers were not verified.
|
||||||
|
- Cleanup evidence: `evidence/V13-FE-023/grid-boundary-cleanup_20260813.log`.
|
||||||
|
|
||||||
|
## Remaining acceptance evidence
|
||||||
|
|
||||||
|
Status cell rendering, label-based filtering/export, visual/AT/browser evidence, server query mapping, and feature-level canonical parity remain outstanding.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-023 — Server-side grid contract slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-GRID
|
||||||
|
- **API/UI/Test:** UI-GRID-01 / T-FE-GRID-01
|
||||||
|
- **Source:** v60 server-side grid boundary; current `DataGridShell`, `KsDataGrid`, `KsPaginator`, and `CrudPageResult` contracts.
|
||||||
|
- **Assumption:** Page metadata is authoritative input from the query/read model; the UI emits a request intent and does not fetch or mutate server state.
|
||||||
|
- **Unknown:** Sort/filter event wiring and column-state persistence are not connected to an active production screen through this component.
|
||||||
|
- **Decision Required:** FE/BE must approve a per-resource query contract before adding server sort/filter event mapping.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Added optional `page`, `pageSize`, and `total` metadata to `DataGridShell`.
|
||||||
|
- Rendered the shared paginator only when all page metadata is explicitly supplied.
|
||||||
|
- Forwarded page changes as an event; the component does not own fetching, caching, or server state.
|
||||||
|
- Preserved existing client-row callers without pagination metadata.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/ui/tests/DataGridShell.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
No server API, sort/filter event, column-state persistence, visual, AT, or browser E2E evidence is claimed.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# V13-FE-024 — Permission route metadata slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-AUTHZ
|
||||||
|
- **API/UI/Test:** UI-AUTH-01 / T-FE-AUTH-01
|
||||||
|
- **Source:** v60 fail-closed permission boundary; current `frontend/src/features/{models,shadow-run}/registry.ts` declarations.
|
||||||
|
- **Assumption:** `model.read` is the only permission identifier currently evidenced by the active feature registries.
|
||||||
|
- **Unknown:** Authenticated permission hydration and the complete route-to-permission catalog are not yet connected to the active router.
|
||||||
|
- **Decision Required:** Security/FE owners must approve the auth permission source and route visibility behavior before enabling a global navigation guard.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Added `permissions: ['model.read']` to the four active ModelOps routes whose feature registries already declare that permission.
|
||||||
|
- Added a pure `canAccessRoute` policy that fails closed for declared permissions and does not perform authentication or network access.
|
||||||
|
- Kept API authorization as the final authority; this Slice does not claim server security or hide routes globally.
|
||||||
|
- Did not invent permissions for financial, operations, internal, or portfolio routes.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/auth/tests/routeAccess.spec.ts
|
||||||
|
PASS: 1 file / 3 tests (2026-08-12)
|
||||||
|
|
||||||
|
The third test verifies all four active ModelOps route permissions match their feature registries.
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
Status remains IN_PROGRESS until the permission source and global route policy are approved.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# V13-FE-028 — Reconciliation API contract boundary
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: current `PortfolioReconciliation/Endpoints.cs` response DTOs and v60 runtime-validation/API boundary guidance.
|
||||||
|
- Assumption: FE treats the current holdings and mismatch responses as read contracts and validates them at the HTTP boundary.
|
||||||
|
- Unknown: approved route permissions, pagination/filter semantics, response version/as-of metadata, and correction command contract.
|
||||||
|
- Decision Required: approve the production API/permission contract before adding route registration, TanStack Query wiring, or correction mutations.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Added Zod schemas and typed API adapters for `GET /reconciliation/holdings` and `GET /reconciliation/mismatches`.
|
||||||
|
- API adapters parse response payloads at runtime; no response is copied into Pinia.
|
||||||
|
- Added TanStack Query keys/hooks with explicit mismatch date-window cache identity and bounded retry.
|
||||||
|
- No route, query cache, correction mutation, or permission claim was added while backend authority remains unresolved.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `pnpm test -- src/features/reconciliation/tests/schema.spec.ts`: 1 file / 3 tests passed.
|
||||||
|
- `pnpm test -- src/features/reconciliation/tests/queries.spec.ts`: 1 file / 2 tests passed.
|
||||||
|
- `pnpm typecheck`: passed.
|
||||||
|
- `git diff --check`: passed.
|
||||||
|
|
||||||
|
## Outstanding
|
||||||
|
|
||||||
|
TanStack Query integration, route permission metadata, server pagination/version/as-of contract, Playwright/AT evidence, and correction/maker-checker API remain outstanding. This Slice is `IN_PROGRESS`.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# V13-FE-033 — KBX ProblemDetails adoption Slice note
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-033
|
||||||
|
- **Requirement/API/Test:** REQ-FE-ERROR / Cross / T-FE-ERROR-01
|
||||||
|
- **Goal:** KBX v60 problem discriminator and recovery metadata를 현재 ASP.NET ProblemDetails + Zod 경계에 제한적으로 차용한다.
|
||||||
|
- **Status:** IN_PROGRESS
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/contracts/problems/kbx.problem.schema.json`, `packages/kbx-ui`의 오류 계약, 현재 `frontend/src/shared/api/problem.ts`와 `client.ts`.
|
||||||
|
- **Assumption:** 기존 `status` HTTP 필드는 유지한다. KBX `type`은 도메인 오류 분류를 보강하는 discriminator로만 사용한다.
|
||||||
|
- **Unknown:** BE가 실제로 `correlationId`, `retryable`, `validationErrors`, `actions`를 어떤 JSON naming policy로 발행하는지와 모든 endpoint의 응답 일관성.
|
||||||
|
- **Decision Required:** BE/Architect가 공통 ProblemDetails payload의 필드명과 401/403/409/422/429/503별 표준 메시지·재시도 정책을 승인해야 한다.
|
||||||
|
|
||||||
|
## Adopted
|
||||||
|
|
||||||
|
- Discriminator: `validation`, `business-rule`, `conflict`, `permission`, `not-found`, `integration`, `system`.
|
||||||
|
- Recovery metadata: `correlationId`, `code`, `retryable`, `currentVersion`, `actions`, structured validation errors.
|
||||||
|
- Pure FE interaction mapping: `UNAUTHORIZED`, `FORBIDDEN`, `CONFLICT`, `VALIDATION`, `RETRYABLE`, `ERROR`.
|
||||||
|
|
||||||
|
## Explicitly not adopted
|
||||||
|
|
||||||
|
- OMS lifecycle/shipment status values, OMS routes, database migrations, generated clients, PrimeVue/AG Grid imports, automatic retry execution, or KIS/order submission behavior.
|
||||||
|
- No server-side error contract was invented; current implementation accepts the proposed fields but does not claim BE parity.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `pnpm exec vitest run src/shared/api/tests/problem.spec.ts`: 1 file, 9 tests passed.
|
||||||
|
- `pnpm typecheck`: passed (`vue-tsc --noEmit`).
|
||||||
|
|
||||||
|
## Remaining acceptance evidence
|
||||||
|
|
||||||
|
- BE contract fixture or live endpoint evidence for all six interaction classes.
|
||||||
|
- API Architect approval of field naming, redaction, correlation propagation, and retry ownership.
|
||||||
|
- Browser/component evidence that each standard state renders the approved action without duplicate submission.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# V13-FE-033 — KBX ProblemDetails BE/OpenAPI adoption
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **Source:** KBX v60 `docs/api-contract-governance-v14.md` and `contracts/problems/kbx.problem.schema.json`.
|
||||||
|
- **Adopted:** OpenAPI operation metadata documenting the supported ProblemDetails discriminator family and correlation header.
|
||||||
|
- **Not adopted:** generated API catalogs, route/permission invention, automatic retry, endpoint behavior changes, database changes, or KIS/order paths.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** existing `ProblemDetailsOperationFilter`, ASP.NET Core `AddProblemDetails()`, and `CorrelationIdMiddleware`.
|
||||||
|
- **Assumption:** `X-Correlation-Id` is the existing host response/request correlation contract.
|
||||||
|
- **Unknown:** endpoint-specific retryability and business error codes are not uniformly declared by current handlers.
|
||||||
|
- **Decision Required:** API Architect must approve the extension names and whether a generated OpenAPI contract will become the release baseline.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Host build/test verification is required after the OpenAPI package compatibility check.
|
||||||
|
- No endpoint or runtime error behavior was changed by this adoption.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# V13-FE-034 — Idempotency retry contract
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: `frontend/src/shared/commands/idempotency.ts`, `frontend/src/shared/crud/useOptimisticCommand.ts`, and their characterization tests.
|
||||||
|
- Assumption: one `createRequest()` represents one user intent; every retry reuses that immutable request envelope.
|
||||||
|
- Unknown: each production command's server-side deduplication store, response replay contract, retention, and endpoint coverage.
|
||||||
|
- Decision Required: BE/QA approval of per-command idempotency persistence and duplicate-side-effect evidence.
|
||||||
|
|
||||||
|
## Existing contract verified
|
||||||
|
|
||||||
|
- UUID key is generated once per intent.
|
||||||
|
- `run()` sends the same `Idempotency-Key` on repeated execution.
|
||||||
|
- Optional `If-Match` is forwarded for optimistic concurrency.
|
||||||
|
- 409/412 sets the conflict state and pending state is cleared.
|
||||||
|
- Concurrent execution is rejected while a command is pending.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- `pnpm test -- src/shared/crud/tests/useOptimisticCommand.spec.ts`: 1 file / 2 tests passed in the FE regression run.
|
||||||
|
- Full FE regression: 49 files / 126 tests passed; typecheck passed.
|
||||||
|
|
||||||
|
## Outstanding
|
||||||
|
|
||||||
|
This proves the client boundary only. Server-side deduplication, replayed response equivalence, retention, and per-endpoint integration evidence remain outstanding. This Slice is `IN_PROGRESS`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# V13-FE-035 — Freshness boundary slice note
|
||||||
|
|
||||||
|
- **Requirement:** REQ-FE-FRESH
|
||||||
|
- **API/UI/Test:** UI-ALL / T-FE-FRESH-01
|
||||||
|
- **Source:** v60 freshness/version guidance; current `frontend/src/shared/status/DataFreshnessBadge.vue` and centralized financial formatter.
|
||||||
|
- **Assumption:** The caller owns the approved current instant and passes it as `now`; the component is presentation-only.
|
||||||
|
- **Unknown:** No active production screen currently consumes this badge with a server-provided `published_at`/revision contract.
|
||||||
|
- **Decision Required:** FE/Data/QA must approve the per-resource freshness window and the authoritative server timestamp before production adoption.
|
||||||
|
|
||||||
|
## Applied boundary
|
||||||
|
|
||||||
|
- Removed direct machine-clock access from `DataFreshnessBadge`.
|
||||||
|
- Added required `now` input so identical `asOf + now + threshold` inputs produce identical state.
|
||||||
|
- Reused centralized `formatAsOf` for deterministic KST display formatting.
|
||||||
|
- Characterized fresh/stale boundary behavior at and after the configured threshold.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
```text
|
||||||
|
pnpm test -- src/shared/status/tests/DataFreshnessBadge.spec.ts
|
||||||
|
PASS: 1 file / 2 tests (2026-08-12)
|
||||||
|
|
||||||
|
pnpm typecheck
|
||||||
|
PASS (2026-08-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a contract slice only; no production freshness policy, API integration, revision, or visual evidence is claimed.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# V13-FE-035 — KBX freshness indicator adoption
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **Source:** KBX v60 `KbxFreshnessIndicator.vue` and freshness contract principles.
|
||||||
|
- **Adopted:** explicit caller-supplied clock, optional source/revision metadata, stale label, and opt-in refresh command with accessible naming.
|
||||||
|
- **Not adopted:** system-clock reads, invented freshness windows, automatic refresh loops, server `published_at`/revision persistence, or provider APIs.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** current `DataFreshnessBadge.vue`, centralized `formatAsOf`, and KBX freshness indicator.
|
||||||
|
- **Assumption:** `staleAfterMinutes` remains an approved caller input; the shared component does not choose a domain threshold.
|
||||||
|
- **Unknown:** production mapping from PIT `published_at`/revision and approved source display names.
|
||||||
|
- **Decision Required:** Data/UX/QA must approve freshness windows, source redaction, revision semantics, and refresh ownership.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted Vitest: 3 tests passed.
|
||||||
|
- `pnpm typecheck`: required after this change.
|
||||||
|
|
||||||
|
Visual, browser, PIT integration, and production evidence remain outstanding.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# V13-FE-036 — T12 Work Queue screen template
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: v60 T12 queue contract and existing `WorkQueuePage.vue` shared screen port.
|
||||||
|
- Assumption: queue and exception summary are server-owned operational data; state boundary controls presentation only.
|
||||||
|
- Unknown: queue-depth source, exception-count definition, permissions, and production JobRun API.
|
||||||
|
- Decision Required: Ops/SRE approval of queue metrics, ownership, retry semantics, and runbook.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Propagates evidence version.
|
||||||
|
- Suppresses queue/exception content in blocked or terminal states.
|
||||||
|
- Preserves shared retry and quick-action/work-summary slots.
|
||||||
|
- No blind retry, schedule, mutation, or automatic order/KIS path added.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted test: 1 file / 2 tests passed.
|
||||||
|
- Full FE regression, visual/AT/Playwright, queue source, and operational approval remain outstanding.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# V13-FE-037 — T11 Fast Entry Grid screen template
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- Source: v60 T11 screen contract and current `FastEntryGridPage.vue` shared screen port.
|
||||||
|
- Assumption: grid and validation summary are presentation slots; cell validation, paste audit, and idempotent commit are feature/API responsibilities.
|
||||||
|
- Unknown: approved bulk command schema, maximum paste size, cell-level error contract, audit payload, and permission mapping.
|
||||||
|
- Decision Required: FE/BE/QA approval of bulk commit, idempotency, paste audit, and partial-result semantics.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Preserves version metadata, grid, validation summary, total, and actions slots.
|
||||||
|
- Suppresses editable grid content in blocked/terminal states and retains shared retry.
|
||||||
|
- No silent bulk overwrite, unbounded paste, or mutation API was introduced.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Targeted test: 1 file / 2 tests passed.
|
||||||
|
- Cell-level validation, paste audit, idempotency API, visual/AT/Playwright, and approval evidence remain outstanding.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# v60 Reference Integration Index
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Canonical reference: `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening`.
|
||||||
|
|
||||||
|
This index records what was borrowed into the current domain, what remains intentionally deferred, and the evidence that supports each disposition. The reference package is not the current domain source of truth.
|
||||||
|
|
||||||
|
## Adopted patterns
|
||||||
|
|
||||||
|
| Area | Adopted element | Current artifact | Evidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| FE UI | Provider-neutral UI adapter and shared screen/state boundaries | `frontend/src/shared/ui/adapter`; `frontend/src/shared/ui/screen-types` | FE regression `53 files / 135 tests`, typecheck passed |
|
||||||
|
| FE validation | Runtime request/form validation and ProblemDetails mapping | `frontend/src/shared/crud/formValidation.ts`; `frontend/src/shared/api` | Form and full FE tests |
|
||||||
|
| FE vendor boundary | Feature code cannot import PrimeVue/AG Grid directly | `frontend/src/shared/ui/adapter/tests/vendorBoundary.spec.ts` | Vendor boundary test `1/1` passed |
|
||||||
|
| API generation | Runtime Swashbuckle generation with deterministic schema IDs | `src/KArtSell.Host/Program.cs` | Host Release build; real artifact generation |
|
||||||
|
| API errors | ProblemDetails response family documented without inventing `422` | `src/KArtSell.Host/OpenApi/ProblemDetailsOperationFilter.cs` | Generated responses `200/400/401/403/404/409/500` |
|
||||||
|
| Reliability | JobRun repository-to-baseline column drift check | `tests/KArtSell.ArchitectureTests/RepositoryRulesTests.cs` | Architecture tests `17/17` passed |
|
||||||
|
| Safety | KIS/Trade endpoints remain unregistered while hard-off | `TradeEndpoints.cs`; architecture guard | KIS hard-off evidence and architecture test |
|
||||||
|
| CI governance | Approved baseline only; candidate upload, no auto-promotion | `.gitea/workflows/openapi-gate.yml` | PyYAML parse passed; fail-closed baseline path |
|
||||||
|
|
||||||
|
## Intentionally deferred
|
||||||
|
|
||||||
|
- v60 API operation catalog as a current baseline: explicit audit found live IDs `31`, reference IDs `66`, intersection `0`.
|
||||||
|
- `docs/api/openapi.json` baseline: requires API Architect approval.
|
||||||
|
- Reconciliation route authorization and correction mutation: approved permission/maker-checker contract absent.
|
||||||
|
- JobRun fresh/upgrade/re-run/failure DB rehearsal: PostgreSQL was unavailable; no completion claim.
|
||||||
|
- FE global router guard: approved auth hydration and unauthorized screen contract absent.
|
||||||
|
- KIS order/status/cancel/settlement activation: separately approved release required; capability remains OFF.
|
||||||
|
|
||||||
|
## Governance invariants
|
||||||
|
|
||||||
|
- Reference code is borrowed only through current repository ports and contracts.
|
||||||
|
- No guessed route, permission, threshold, migration, or production capability is introduced.
|
||||||
|
- Evidence records actual commands and results; unavailable external dependencies remain unresolved.
|
||||||
|
- WBS status is not promoted to `COMPLETED` without its acceptance evidence.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
## Safety invariants
|
## Safety invariants
|
||||||
|
|
||||||
1. Allowed provider values are `primevue` and `native`; any other value fails closed at startup.
|
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/`.
|
2. Feature source remains vendor-free. Existing application-owned components under `frontend/src/shared/ui/components/` may use the selected vendor directly to preserve full component functionality. New vendor-neutral components may use `frontend/src/shared/ui/adapter/`; the direct-component ownership harness prevents accidental adapter regression.
|
||||||
3. Changing a provider means building and deploying a new artifact. Do not mutate the active application's global provider.
|
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.
|
4. Rollback restores the last approved artifact and its recorded provider value. It does not alter data, decisions, evidence, or audit records.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# ADR-API-BASELINE-001 — Current Host OpenAPI baseline scope
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted for candidate-baseline work; final release baseline remains evidence-gated.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-08-13 (user approval recorded in session)
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Use the current Host's generated 31-operation OpenAPI artifact as the candidate baseline scope for K-ArtSell Aegis. Treat the KBX v60 reference's 66 operations as selective design references only. Do not import reference routes, permissions, DTOs, database tables, or workflows without a separate approved WBS Slice.
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- Exact operation ID intersection between the current Host and KBX reference is zero.
|
||||||
|
- The current Host artifact is generated from registered runtime endpoints and is therefore the only available source for current API surface evidence.
|
||||||
|
- Automatic reference-to-product generation would create route, authorization, DTO, and data-contract drift.
|
||||||
|
- Candidate and approved baseline are distinct lifecycle states; approval requires preserved artifact, hash, parity output, and API Architect sign-off.
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
|
||||||
|
- No automatic order/KIS submission or model promotion path is introduced.
|
||||||
|
- No client-supplied evidence is trusted as production PIT context.
|
||||||
|
- No API operation is added solely because it exists in the KBX reference.
|
||||||
|
- OpenAPI generation runs with Hangfire disabled for deterministic lifecycle behavior.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Candidate: `src/KArtSell.Host/artifacts/openapi/current_20260813_auto-off.json`
|
||||||
|
- Generation log: `evidence/AEG-X-008/openapi-generation_20260813_auto-off.log`
|
||||||
|
- Prior parity: 31 live operations, 66 KBX reference operations, 0 shared IDs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- API baseline work can proceed against an actual Host surface.
|
||||||
|
- KBX adoption must continue as bounded FE/BE/component/template slices.
|
||||||
|
- Release completion remains blocked until the approved baseline is stored at the designated release artifact location and the gate is executed successfully.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# ADR-FE-CONTRACT-001 — FE API 계약과 Zod 검증 경계
|
||||||
|
|
||||||
|
- **WBS:** V13-FE-009
|
||||||
|
- **Requirement:** REQ-FE-OPENAPI
|
||||||
|
- **API/UI/Test:** UI-FOUND-09 / T-FE-CONTRACT-01
|
||||||
|
- **Status:** ACCEPTED FOR CURRENT IMPLEMENTATION BOUNDARY
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
v60 참조 구현은 `generated API client → shared contract package → feature query` 경계를 사용한다. 현재 K-ArtSell FE는 `axios → feature API → Zod schema → TanStack Query` 경계를 사용하고 있다. v60의 OMS 계약·생성 클라이언트·DTO를 그대로 복사하면 현재 금융 도메인의 API 의미와 일치하지 않는다.
|
||||||
|
|
||||||
|
## Source / Assumption / Unknown / Decision Required
|
||||||
|
|
||||||
|
- **Source:** `frontend/src/shared/api/client.ts`, `frontend/src/features/*/api.ts`, `frontend/src/features/*/schema.ts`, `frontend/src/shared/commands/idempotency.ts`, `contracts/ui/crud-resource.v2.json`, WBS `V13-FE-009`.
|
||||||
|
- **Assumption:** 서버 OpenAPI 또는 승인된 JSON Schema가 FE API response의 authoritative source이며, feature-local Zod는 runtime boundary validation을 담당한다.
|
||||||
|
- **Unknown:** 현재 모든 internal endpoint에 대해 versioned OpenAPI artifact가 저장소에 연결되어 있는지는 확인되지 않았다.
|
||||||
|
- **Decision Required:** OpenAPI artifact가 승인·보존되기 전에는 generated client 도입, DTO 자동 생성, v60 `@kbx/contracts` 의존성 도입을 금지한다.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **현재 경계 유지:** `shared/api/client.ts`는 transport와 ProblemDetails 변환만 담당한다. 업무 정책·query key·도메인 mapping을 넣지 않는다.
|
||||||
|
2. **Runtime validation:** 각 feature의 response/request는 feature-owned Zod schema로 `parse`한다. TypeScript interface만으로 외부 응답을 신뢰하지 않는다.
|
||||||
|
3. **Server state ownership:** API 응답은 TanStack Query가 소유한다. Pinia에는 API response를 복제하지 않는다.
|
||||||
|
4. **Command retry:** 동일 재시도는 동일 `Idempotency-Key`를 재사용한다. 새 시도는 명시적 새 command로만 생성한다.
|
||||||
|
5. **Generated code gate:** 생성 클라이언트는 승인된 OpenAPI/JSON Schema artifact, generator version, input SHA, output diff, contract test가 모두 존재할 때 별도 Slice에서 도입한다.
|
||||||
|
6. **v60 차용 범위:** v60의 request routing·contract validation 아이디어만 차용한다. OMS endpoint, OMS status, KBX package, KBX permission host/router/store는 현재 FE에 이식하지 않는다.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- 현재 feature API의 명시적 Zod 경계와 existing tests를 보존한다.
|
||||||
|
- generated DTO 중복은 즉시 제거하지 않고, authoritative contract가 확인된 뒤 migration 대상으로 기록한다.
|
||||||
|
- OpenAPI artifact가 없는 endpoint는 자동생성 대상이 아니라 `DECISION_REQUIRED`로 남는다.
|
||||||
|
- 이 ADR만으로 API/DB schema 변경이나 새 endpoint를 승인하지 않는다.
|
||||||
|
|
||||||
|
## Verification Evidence
|
||||||
|
|
||||||
|
- `frontend`: `pnpm typecheck` PASS
|
||||||
|
- `frontend`: `pnpm test` PASS (34 files, 75 tests)
|
||||||
|
- `frontend`: `pnpm typecheck` and production build PASS via `dotnet build src/KArtSell.Host/KArtSell.Host.csproj --no-restore` on 2026-08-12
|
||||||
|
- 범위: FE contract boundary decision only. Build, E2E, migration, production runtime evidence는 주장하지 않는다.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# KBX Design Philosophy — Reference Index
|
# KBX Design Philosophy — Reference Index
|
||||||
|
|
||||||
이 디렉터리의 4개 문서는 K-ArtSell Aegis 프론트엔드가 채택하는 **디자인 철학 소스**다. `docs/Design/kbx-foundation-v36/`은 이 문서를 구현한 참조 코드(도메인은 OMS/WMS/ERP로 다르지만 UX 계약은 동일)이며, 이식 대상이 아니라 구현 참고용이다.
|
이 디렉터리의 4개 문서는 K-ArtSell Aegis 프론트엔드가 채택하는 **디자인 철학 소스**다. `docs/Design/kbx-foundation-v60-status-canonical-contract-hardening/`은 이 문서를 구현한 최신 참조 코드(도메인은 OMS/WMS/ERP로 다르지만 UX 계약은 동일)이며, 이식 대상이 아니라 구현 참고용이다.
|
||||||
|
|
||||||
## 문서 역할
|
## 문서 역할
|
||||||
|
|
||||||
|
|||||||
@@ -1,355 +0,0 @@
|
|||||||
# KBX Foundation v50 — FE Operational Truth · Retryability · Home Overflow · Theme Hardening
|
|
||||||
|
|
||||||
## v50 핵심
|
|
||||||
|
|
||||||
- T01~T09 주요 Template에 `errorRetryable` 계약을 전파해 영구 오류에서 무의미한 `[다시 조회]` 제거
|
|
||||||
- `KbxTemplateStateBoundary 1.3.0`: transient error와 non-retryable error를 UI 계약으로 분리
|
|
||||||
- `WMS-WORK-001 1.2.0`: `WMS_WORK_QUERY_NOT_CONNECTED`를 `retryable:false`로 수정, API 미연결에 무한 Retry 착시 제거
|
|
||||||
- Home Attention: 기본 8개 유지, 총 건수 >24이면 `전체`가 아니라 `우선 24개 보기`로 정확히 표현
|
|
||||||
- 24개 이후 남은 Attention은 `작업 센터 전체 / 알림 센터 전체`로 직접 연결
|
|
||||||
- `KbxHomePage 1.10.0`, `KbxQueuePage 1.9.0`, 기타 T01~T09 Template 공개 Retry 계약 Minor Upgrade
|
|
||||||
- Reference HTML/JavaScript도 28개 fixture로 8→24 bounded disclosure와 Center navigation 재현
|
|
||||||
- KBX Business Light/Dark + Forced Colors semantic theme 유지, Theme는 Retry/Permission/Domain truth를 결정하지 않음
|
|
||||||
- Browser Reference QA **16/16 PASS**, 전체 `validate:kbx` PASS, Design Debt **116 <= 131**
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v50.md`
|
|
||||||
검증: `docs/validation-report-v50.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v49 — FE Workbench · WMS Truthful Boundary · Theme Hardening
|
|
||||||
|
|
||||||
## v49 핵심
|
|
||||||
|
|
||||||
- Home `확인 필요`가 상위 8개만 숨긴 채 끝나지 않도록 `8개 ↔ 전체(최대 24개)` 접근성 Disclosure 추가
|
|
||||||
- Home 우선업무 출처를 미저장/실패/중요알림/진행/알림으로 요약해 AX 판단량 감소
|
|
||||||
- `KbxQueuePage 1.8.0`: T06 Empty/Error/Retry copy와 Empty recovery Action 계약 추가
|
|
||||||
- `WMS-WORK-001 1.1.0`: Demo에서는 실제 필터 가능한 Queue, 일반 Runtime에서는 미연결 API를 `0건`으로 가장하지 않고 Integration Problem으로 fail-explicit
|
|
||||||
- Receiving/Putaway/Counting `1.1.0`: client-only 성공 상태를 Production에서 차단하고 Demo Mode에서만 FE scanner state machine 검증
|
|
||||||
- `KbxWmsMobilePage 1.7.0`: Production execution 차단 시 빈 sticky action footer를 숨기는 `actionsVisible` 계약
|
|
||||||
- WMS 네트워크 표시를 `useKbxNetworkState()`로 통일하고 hardcoded online 제거
|
|
||||||
- Reference Home도 동일한 8→전체 Priority disclosure와 KBX Business Light/Dark semantic theme 적용
|
|
||||||
- Browser Reference QA **10/10 PASS**: Attention disclosure, keyboard collapse, Dark theme, 1280×720 overflow 0, runtime error 0
|
|
||||||
- WMS에서 Server validation / Idempotency / Audit 이전의 성공표시는 금지한다는 경계를 FE Gate로 고정
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v49.md`
|
|
||||||
검증: `docs/validation-report-v49.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v48 — FE Template Completion · Real Search/File · Theme-safe Recovery Hardening
|
|
||||||
|
|
||||||
## v48 핵심
|
|
||||||
|
|
||||||
- 실제 Vue T04/T06/T07/T08에 `KbxProblemFeedback` 기반 Actionable Problem Recovery 확장
|
|
||||||
- T04~T08 Template 계층의 `problem / retry / dismiss` 계약 통일
|
|
||||||
- `KbxProblemFeedback 1.1.0`: 상위 Validation Problem의 bounded summary 추가, Field/Cell Validation 역할은 유지
|
|
||||||
- Reference T05/T06/T07: Toast/표시 위주의 조회를 실제 검색 → Loading → 결과/Empty → 복구 상태 전이로 교체
|
|
||||||
- Chromium QA에서 발견한 Search Panel Mouse no-op 결함 수정: T05/T06/T07 `[조회]` click과 Enter/F3 동일 실행
|
|
||||||
- Reference T08: 샘플 파일명 주입 제거, 실제 Browser File input/Drop + `.xlsx/.xls` + 20MB guard
|
|
||||||
- T08 6단계(`파일→매핑→검증→미리보기→반영→결과`)와 CSS 6-column 정합화
|
|
||||||
- KBX Business Light/Dark semantic theme, Home/Workspace, Permission/Domain server-authority 경계 유지
|
|
||||||
- Browser Reference QA **30/30 PASS**: T05~T08 completion loops, Dark theme, 1280px overflow 0, runtime error 0
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v48.md`
|
|
||||||
검증: `docs/validation-report-v48.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v47 — Actionable Problem Recovery · Theme-safe FE Hardening
|
|
||||||
|
|
||||||
## v47 핵심
|
|
||||||
|
|
||||||
- 실제 T01/T02/T03에서 Command 실패를 조회 Error와 분리: `business-rule / permission / not-found / integration / system` 인라인 Problem Surface
|
|
||||||
- `KbxProblemFeedback 1.0.0`: 문제 유형 텍스트, 원인, 참조번호, retryable 재시도, 닫기
|
|
||||||
- Server `problem.actions`는 기본 미노출 + 명시적 allow-list만 렌더링하여 임의 Action ID → FE Command 실행 차단
|
|
||||||
- `OMS-ORD-001` Bulk 출고, `OMS-ORD-002` 저장/확정, `ERP-MST-ITEM-001` 목록 로드/저장/사용중지 실패 시 현재 화면·선택·입력 Context 유지
|
|
||||||
- `KbxListPage 1.8.0`, `KbxMasterPage 1.7.0`, `KbxTransactionPage 1.7.0`
|
|
||||||
- Reference HTML/JavaScript: 예외 주문 출고지시 → `업무 규칙` 인라인 오류 → 문제 주문만 선택 복구
|
|
||||||
- Browser Reference QA **12/12 PASS**: Dark Theme, 문제표시/복구/닫기, 1280px overflow 0, runtime error 0
|
|
||||||
- 전체 Problem 계약에서도 Validation은 Field/Cell, Conflict는 최신값 비교, Command Problem은 인라인 복구로 역할 분리
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v47.md`
|
|
||||||
검증: `docs/validation-report-v47.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v46 — Safe Workspace Resume · T09 Recovery · Theme-preserving FE Hardening
|
|
||||||
|
|
||||||
## v46 핵심
|
|
||||||
|
|
||||||
- 열린 Workspace의 **clean 업무 위치만 sessionStorage**에 복원, Query/Hash/Dirty Form·Grid 값은 저장 금지
|
|
||||||
- 복원 전 Permission + Screen Registry + Safe Route 재검증, `resumed` 상태는 실제 업무 진입 시 제거
|
|
||||||
- Home `바로 시작`에서 `복원` 업무를 구분하고 미저장 편집은 안전상 복원 제외 사실만 표시
|
|
||||||
- Home에서 active tab이 없어도 첫 Workspace Tab을 keyboard roving entry로 제공
|
|
||||||
- Navigation Preference의 최초 localStorage read까지 fail-soft로 통일
|
|
||||||
- `KbxWorkspaceTabs 1.3.0`, `KbxHomePage 1.8.0`, `KbxWmsMobilePage 1.6.0`
|
|
||||||
- T09 WMS Picking: TanStack Query Loading/Error/Ready → 공통 Mobile State Boundary, 오류 `[작업 다시 불러오기]` 실제 refetch 연결
|
|
||||||
- Reference HTML/CSS/JavaScript Browser QA **20/20 PASS**: clean resume, dirty exclusion, storage-blocked Shell, Dark Theme, 1280px overflow
|
|
||||||
- Theme/Permission/Domain 경계는 유지: Theme/Resume는 FE 표현·생산성 상태이며 업무 truth가 아님
|
|
||||||
- 전체 `validate:kbx` PASS: 153 tokens / 20 screens / 84 components / 69 catalog entries / 306 TS·Vue units / Design Debt **116 <= 131**
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v46.md`
|
|
||||||
검증: `docs/validation-report-v46.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v45 — Safe Preference · Home Context · Theme Hardening
|
|
||||||
|
|
||||||
## v45 핵심
|
|
||||||
|
|
||||||
- 실제 Vue `OMS-ORD-001`에 `기본조건 저장 / 마지막 조회조건 기억`을 연결하되 자유검색어는 localStorage 저장에서 제외
|
|
||||||
- Screen Preference key를 `Tenant/User scope + ScreenId + ScreenVersion`으로 격리하고 size/type 검증 + fail-soft 적용
|
|
||||||
- Home의 실패/진행 작업 클릭 시 Runtime Center 전체가 아니라 해당 Operation/Notification으로 Focus
|
|
||||||
- `KbxSearchPanel 1.4.0`, `KbxOperationCenter 1.1.0`, `KbxNotificationCenter 1.1.0`
|
|
||||||
- HTML/JavaScript Reference Lab에도 동일한 안전한 조회조건 기억과 Light/Dark Theme parity 적용
|
|
||||||
- Browser QA 14/14: 저장/복원, 자유검색어 비저장, Dark Theme, 1280 Home overflow, runtime error 0
|
|
||||||
- Domain Rule·Permission·Concurrency·Audit은 여전히 Server authority이며 Preference는 표현/생산성 상태에 한정
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v45.md`
|
|
||||||
검증: `docs/validation-report-v45.md`
|
|
||||||
|
|
||||||
|
|
||||||
## v44 핵심
|
|
||||||
|
|
||||||
- 실제 App artifact version `v44-fe-theme-state`로 정합화
|
|
||||||
- Header 작업/알림 trigger ↔ runtime panel `aria-expanded/aria-controls` 연결
|
|
||||||
- runtime panel: 명시 제목/닫기, Esc 닫기, open 시 panel focus, close 시 trigger focus restore
|
|
||||||
- route 전환/메뉴검색/Utility Rail 진입 시 non-modal runtime panel을 정리해 surface stacking 방지
|
|
||||||
- Windows Forced Colors에서 Light/Dark palette 대신 OS system color로 fail-safe
|
|
||||||
- Forced Colors에서도 현재 화면/선택/Focus를 색상 하나가 아니라 outline/텍스트 상태로 유지
|
|
||||||
- T01 Empty → `[조회조건 초기화]` → 즉시 재조회 공통 Recovery contract
|
|
||||||
- FE Reference Lab 작업/알림을 실제 Vue와 동일한 non-modal System Panel로 정렬
|
|
||||||
- FE Reference Lab에도 동일 high-contrast bridge 적용
|
|
||||||
- Browser matrix **28/28 PASS**: T01~T09 Light/Dark, T01 Empty recovery, 1280px, Forced Colors, runtime error 0
|
|
||||||
- v44 전용 static QA gate를 전체 `validate:kbx`에 편입
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v44.md`
|
|
||||||
검증: `docs/validation-report-v44.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v43 — Semantic Theme · Home Navigation Hierarchy · Light/Dark FE Hardening
|
|
||||||
|
|
||||||
이번 iteration은 v42의 first-class Vue runtime 위에 **KBX Business Light / Dark를 실제 업무 Semantic Theme으로 통합**한다. Theme를 장식용 팔레트로 만들지 않고 `surface / text / border / primary / success / warning / danger / focus / AI` 의미 계약을 Application Shell, Home, PrimeVue, AG Grid, 공통 Component, FE Reference Lab까지 공유한다.
|
|
||||||
|
|
||||||
## v43 핵심
|
|
||||||
|
|
||||||
- Design Token source 1.9.0: `KBX Business Light / Dark` semantic override 계약
|
|
||||||
- Theme runtime API + `light|dark` allow-list/fail-safe preference persistence
|
|
||||||
- Global Header에 명시적 Light/Dark 전환 + 접근성 label + narrow-header reflow
|
|
||||||
- PrimeVue dark selector와 KBX theme root 연결
|
|
||||||
- AG Grid Quartz foreground/background/accent/border/header/hover/selection을 KBX semantic token으로 연결
|
|
||||||
- Home: `확인 필요 → 바로 시작 → 모듈 업무`의 시각 우선순위를 warning/danger/info semantic으로 강화하되 Dashboard card 증식 금지
|
|
||||||
- Side Navigation 현재 화면 primary inset marker
|
|
||||||
- Network/Dialog/Toast/Reference surface 하드코딩 감소
|
|
||||||
- FE Reference Lab Light/Dark parity + 실제 Dark inheritance 결함 발견/수정
|
|
||||||
- Theme Browser QA **16/16 PASS**: T01~T09 Dark render, T09 WMS mobile mode, 1280px overflow 0, runtime error 0
|
|
||||||
- 전체 `validate:kbx` PASS / 305 TS·Vue script units / Design Debt **116 (ratchet 131 대비 감소)**
|
|
||||||
|
|
||||||
### v43에서 완료로 계산하지 않는 항목
|
|
||||||
|
|
||||||
첨부 표준은 Semantic Color/Density/Accessibility를 요구하지만 Light/Dark 자체를 명시적으로 강제하지 않는다. Dark는 사용자 요청에 따른 구현 확장이다. 또한 현재 실행환경은 package registry 접근이 차단되어 **실제 Vite + PrimeVue + AG Grid live-runtime Theme E2E를 통과했다고 주장하지 않는다.** Browser 증적은 실제 FE Reference HTML/CSS/JS asset 기준이며 production CSP는 별도 정적 Gate로 검증한다. Server-side full-query XLSX 역시 기존 잔여 P1이다.
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v43.md`
|
|
||||||
검증: `docs/validation-report-v43.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v42 — First-class Vue Runtime · Secure Demo Transport · Home Context · Excel Menu · Zoom Hardening
|
|
||||||
|
|
||||||
이번 iteration은 v41의 Golden Screen parity를 넘어 **실제 FE 제품을 독립적으로 기동·빌드·검증할 수 있는 Runtime 경계**를 우선 완성한다. BE 결합을 더 늘리지 않고 `apps/web`을 Vite 8 application으로 명시하고, Demo Mode도 별도 Mock UI가 아니라 실제 Axios/kbxApi 경계의 transport adapter로 구성했다.
|
|
||||||
|
|
||||||
## v42 핵심
|
|
||||||
|
|
||||||
- `apps/web` first-class Vite 8 package / `index.html` / `main.ts` / `App.vue` / typecheck-build scripts
|
|
||||||
- Vue + Pinia + Router + TanStack Query + PrimeVue + `installKbx()` 실제 application bootstrap
|
|
||||||
- `--mode demo`에서만 fixture HTTP adapter 사용, 일반 runtime은 real transport + permission fail-closed
|
|
||||||
- Demo unknown operation은 성공 위장이 아니라 `DEMO_OPERATION_UNSUPPORTED` 명시 실패
|
|
||||||
- Home/Notification의 OMS 예외 Context를 `useOrderSearch()`가 allow-list로 hydrate하고 자동 조회
|
|
||||||
- `KbxCommandBar` ↔ `KbxExcelMenu` 실제 통합, Excel 5개 표준 진입점 + keyboard/focus contract
|
|
||||||
- 일부 row만 로드된 경우 local CSV를 전체 Export로 오인하지 않도록 다운로드 차단
|
|
||||||
- 56rem/40rem Shell pressure hardening: 200% Zoom에서 Workspace 우선 보존
|
|
||||||
- v42 runtime-readiness Gate를 전체 `validate:kbx`에 편입
|
|
||||||
- 304 TS/Vue syntax + 전체 Governance PASS / Design Debt **122 (ratchet 131 대비 감소)**
|
|
||||||
|
|
||||||
### v42에서 완료로 계산하지 않는 항목
|
|
||||||
|
|
||||||
현재 실행환경은 npm registry 접근이 차단되어 pnpm 의존성 설치가 불가능하다. 따라서 **실제 Vite build/server와 Vue/PrimeVue/AG Grid Playwright를 통과했다고 주장하지 않는다.** 또한 T01의 전체 검색조건 XLSX/Background Export API는 아직 미연결이며, 부분 row CSV는 전체 Export로 오인될 수 있어 의도적으로 차단한다.
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v42.md`
|
|
||||||
검증: `docs/validation-report-v42.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v41 — Actual Vue Golden Screen Parity · Controlled Grid Selection · Drawer/Dirty/Focus Hardening
|
|
||||||
|
|
||||||
이번 iteration은 v40의 FE Reference Lab을 더 확장하지 않고, **실제 Vue/PrimeVue/AG Grid Golden Screen이 동일한 업무 UX 계약을 지키는지**를 우선 검수한다. 대상은 `OMS-ORD-001`, `OMS-ORD-002`, `ERP-MST-ITEM-001`, `KbxDataGrid`, `KbxLookup`, `KbxUnsavedChangesDialog`, Application Shell navigation이다.
|
|
||||||
|
|
||||||
## v41 핵심
|
|
||||||
|
|
||||||
- OMS-ORD-001 주문번호 Link의 no-op 제거: 실제 Order Detail Drawer 연결
|
|
||||||
- 행 Double Click도 Drawer 우선으로 통일하여 List 조회 Context 보존
|
|
||||||
- Drawer async request sequence guard로 늦게 도착한 응답의 stale overwrite 방지
|
|
||||||
- KbxDataGrid: AG Grid 33+ multiRow selection contract, checkbox/header checkbox, 외부 Selection State ↔ 내부 Grid selection 동기화
|
|
||||||
- Grid error navigation이 Header/Form 오류를 Cell 오류로 잘못 세지 않도록 grid-scoped error 분리
|
|
||||||
- KbxDataGrid에 `focusCell`, `focusNextEditableCell`, `selectRowByKey`, `clearSelection` 제어 API 추가
|
|
||||||
- KbxLookup: 직접 코드 Enter 성공도 다음 Control로 Focus 이동, 선택/취소 Focus 정책 분리, 접근성 label 고유화
|
|
||||||
- ERP-MST-ITEM-001: Dirty 상태에서 다른 Master 선택/New 전환 시 3-way Guard로 편집값 덮어쓰기 방지
|
|
||||||
- OMS-ORD-002: F2 품목 선택 후 다음 Editable Cell Focus, New/Copy 내부 전환에도 Dirty Guard 적용
|
|
||||||
- KbxUnsavedChangesDialog: Esc/Tab/Shift+Tab Focus Trap + 닫힌 후 Focus Restore
|
|
||||||
- OMS 주문관리의 구현되지 않은 `보류` no-op Command 제거
|
|
||||||
- Side Navigation 즐겨찾기 접근성 이름과 현재 화면 상태 보강
|
|
||||||
- Component version: KbxDataGrid 1.5.0 / KbxLookup 1.4.0 / UnsavedChangesDialog 1.1.0 / SideNavigation 1.3.1
|
|
||||||
- 전체 `validate:kbx` PASS / Design Debt **131 유지**
|
|
||||||
|
|
||||||
### v41에서 완료로 계산하지 않는 항목
|
|
||||||
|
|
||||||
현재 OMS 주문조회 `엑셀` 동작은 AG Grid CSV export 수준이다. KBX 표준의 Excel 메뉴 전체 계약(현재 조회결과 Excel, 양식, 업로드, 붙여넣기, 최근 업로드)은 아직 완료가 아니므로 **잔여 P1**로 유지한다. 또한 이 산출물에는 `node_modules`가 없어 실제 Vue runtime E2E/visual regression을 실행하지 못했다. 이번 v41 증적은 TypeScript/Vue syntax transpile + 전체 Foundation governance/static parity gate이며, 실제 앱 런타임 E2E는 다음 Gate다.
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v41.md`
|
|
||||||
검증: `docs/validation-report-v41.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v40 — FE Completion Loop · Validation-safe Shortcut · Master/Transaction/Queue/Reconcile Hardening
|
|
||||||
|
|
||||||
이번 iteration은 BE 구조나 새 Metadata Framework를 늘리지 않고, 실제 HTML/CSS/JavaScript FE에서 **T01~T09의 최소 업무 완료 루프가 닫히는지**를 기준으로 고도화한다. v39의 Navigation/Lookup/Drawer/Job/WMS 기반 위에서 T01 실제 검색, T02 Master CRUD, T03 Header+Detail 저장/확정, T06 Queue Action, T07 Reconcile Resolution을 실제 상태 전이로 연결했다.
|
|
||||||
|
|
||||||
## v40 핵심
|
|
||||||
|
|
||||||
- T01 검색조건(판매채널/상태/키워드/예외) ↔ 실제 Grid 결과 결합
|
|
||||||
- T02 Master 신규/수정/복사/검증/F2 Lookup/저장/상태/Dirty 전환
|
|
||||||
- T03 Header+Detail 입력/계산/Validation/F8 저장/작성→확정
|
|
||||||
- T06 선택 기반 작업자 배정/Wave 생성 실제 Row 갱신
|
|
||||||
- T07 선택 기반 재처리 후 Expected/Actual/Difference 및 mismatch 목록 갱신
|
|
||||||
- Global F8이 실제 화면 Save Handler를 호출하도록 수정 — Validation 우회 제거
|
|
||||||
- Hash/Refresh/Close까지 Dirty 보호 확대
|
|
||||||
- Browser QA **49/49 PASS**, Runtime Error 0
|
|
||||||
- 1280×720 document overflow 0, WMS 390×844 Full View
|
|
||||||
- Design Debt ratchet **131 유지**
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v40.md`
|
|
||||||
검증: `docs/validation-report-v40.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v39 — FE Product Parity · Lookup · Dirty Navigation · Job/WMS Hardening
|
|
||||||
|
|
||||||
이번 iteration은 BE 구조를 늘리지 않고 실제 HTML/CSS/JavaScript와 핵심 Vue Component의 사용성을 고도화한다. v38의 상태전이 기반 위에 **Context-aware Home, 화면 이동 Dirty Guard, 실제 F2 Lookup, Utility Drawer, T08 Background Job UX, T09 Mobile Full View/중복 Scan Guard**를 추가했다.
|
|
||||||
|
|
||||||
## v39 핵심
|
|
||||||
|
|
||||||
- Home 예외 → 실제 대상 필터/Context 적용 후 이동
|
|
||||||
- Dirty 상태에서 Tab Close뿐 아니라 Side Nav/Home/다른 Tab 이동도 3-way Guard
|
|
||||||
- T02/T03 F2 Lookup: Search/Arrow/Enter/Esc/Focus Next
|
|
||||||
- 실제 `KbxLookup.vue`: 선택 후 다음 Control, 취소 시 Trigger Focus 복원
|
|
||||||
- Help/AI/Suggestion/Job/Notification 실제 Drawer + inert/Focus Trap
|
|
||||||
- T08 6단계 + Background Progress + 화면 이탈/복귀
|
|
||||||
- T09 중복 Barcode Guard + Network Failure No-Commit UX
|
|
||||||
- 767px 이하 WMS Mobile Full View
|
|
||||||
- 1280×720 및 720px zoom-pressure document overflow 없음
|
|
||||||
- Browser QA 31/31 PASS · Runtime Error 0
|
|
||||||
- Design Debt ratchet 131 유지
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v39.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# KBX Foundation v38 — FE QA Hardening · Interaction-complete Reference Lab
|
|
||||||
|
|
||||||
이 패키지는 기존 v36 Foundation 거버넌스를 유지하면서 v37 FE Reference Lab을 **실제 상태 전이·복구·키보드·보안 실패 격리** 기준으로 고도화한 v38 작업본이다. 이번 iteration은 BE 구조를 확장하지 않고 실제 HTML/CSS/JavaScript FE 완성도에 집중한다.
|
|
||||||
|
|
||||||
## v38 핵심
|
|
||||||
|
|
||||||
- Home: 예외 우선 → 이어서 처리 → 모듈별 업무 → Ctrl+K 검색
|
|
||||||
- T01: Quick Filter/전체선택/Bulk/Drawer/Data State/선택 기반 Command disable
|
|
||||||
- T02: Master→Detail 실제 전환 + Dirty 보호
|
|
||||||
- T03: Header/Detail edit + Enter 흐름 + Dirty/Save
|
|
||||||
- T04: Fast Entry input + TSV paste + Add/Duplicate/Fill Down + error gate
|
|
||||||
- T05: 품목→Location→재고이력 동시 문맥 갱신
|
|
||||||
- T06/T07: Queue/불일치 필터 실제 적용
|
|
||||||
- T08: 파일 gate + 이전/다음 + Mapping/Validation/Commit/Result
|
|
||||||
- T09: keyboard-wedge Scan + wrong barcode recovery + quantity/exception dialog
|
|
||||||
- CSP + dynamic HTML escape + localStorage fail-soft + prompt/eval/document.write 금지
|
|
||||||
- Headless Chromium interaction evidence 및 1440×900/390px 화면 증적
|
|
||||||
- Design Debt ratchet **131 유지**
|
|
||||||
|
|
||||||
## v38 검증
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node scripts/validate-fe-reference.mjs
|
|
||||||
node scripts/validate-kbx.mjs
|
|
||||||
```
|
|
||||||
|
|
||||||
상세: `docs/frontend/KBX-FE-QA-Hardening-v38.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 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`
|
|
||||||
|
|
||||||
## FE Reference Lab (v37)
|
|
||||||
|
|
||||||
Backend 없이 실제 브라우저에서 Application Shell, 홈 네비게이션과 T01~T09를 검증하려면 `apps/web/fe-reference/`를 사용한다.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/web/fe-reference
|
|
||||||
python3 -m http.server 4173
|
|
||||||
```
|
|
||||||
|
|
||||||
FE 정적 계약 검증:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm validate:fe-reference
|
|
||||||
```
|
|
||||||
|
|
||||||
상세 설계 기록: `docs/frontend/KBX-FE-Completion-v37.md`
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { isKbxProblem, type KbxProblem } from '@kbx/contracts'
|
|
||||||
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 problem=ref<KbxProblem|null>(null)
|
|
||||||
let retryAction:(()=>Promise<void>)|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() {
|
|
||||||
problem.value=null; retryAction=null
|
|
||||||
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 runProblemAware(action:()=>Promise<void>){
|
|
||||||
problem.value=null; retryAction=null
|
|
||||||
try{await action()}catch(error){if(isKbxProblem(error)){problem.value=error;retryAction=()=>runProblemAware(action);return}throw error}
|
|
||||||
}
|
|
||||||
async function executeCommand(id: string) {
|
|
||||||
const ids = selectedRows.value.map(x => x.id)
|
|
||||||
if (id === 'search') return search()
|
|
||||||
if (id === 'claim' && ids.length) return runProblemAware(async()=>{await operationsApi.claim(ids);selectedRows.value=[];await query.refetch()})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function executeAction(action: KbxWorkItemAction) {
|
|
||||||
if (!detail.value) return
|
|
||||||
const item=detail.value
|
|
||||||
if(action.kind==='navigate'){window.dispatchEvent(new CustomEvent('kbx:navigate-source',{detail:item}));detail.value=null;return}
|
|
||||||
await runProblemAware(async()=>{
|
|
||||||
if (action.kind === 'claim') await operationsApi.claim([item.id])
|
|
||||||
if (action.kind === 'resolve') { 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(item.id)
|
|
||||||
detail.value = null
|
|
||||||
await query.refetch()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
async function retryProblem(){const retry=retryAction;problem.value=null;retryAction=null;if(retry)await retry()}
|
|
||||||
function dismissProblem(){problem.value=null;retryAction=null}
|
|
||||||
</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)"
|
|
||||||
:problem="problem"
|
|
||||||
@command="executeCommand"
|
|
||||||
@retry-problem="retryProblem"
|
|
||||||
@dismiss-problem="dismissProblem"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { isKbxProblem, type KbxProblem } from '@kbx/contracts'
|
|
||||||
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 problem=ref<KbxProblem|null>(null)
|
|
||||||
let retryAction:(()=>Promise<void>)|null=null
|
|
||||||
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() {
|
|
||||||
problem.value=null; retryAction=null
|
|
||||||
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 runProblemAware(action:()=>Promise<void>){problem.value=null;retryAction=null;try{await action()}catch(error){if(isKbxProblem(error)){problem.value=error;retryAction=()=>runProblemAware(action);return}throw error}}
|
|
||||||
async function command(id: string) {
|
|
||||||
if (id === 'search') return search()
|
|
||||||
if (id === 'createException' && selectedRows.value.length) {
|
|
||||||
const ids=selectedRows.value.map(x=>x.id)
|
|
||||||
return runProblemAware(async()=>{await reconcileApi.createExceptions(ids);selectedRows.value=[];await query.refetch()})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function retryProblem(){const retry=retryAction;problem.value=null;retryAction=null;if(retry)await retry()}
|
|
||||||
function dismissProblem(){problem.value=null;retryAction=null}
|
|
||||||
</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)" :problem="problem" comparison-title="OMS ↔ WMS 대사" comparison-description="Expected · Actual · Difference · Reason · Resolution 순서로 불일치 근거를 확인합니다." :comparison-count="query.data.value?.summary.totalCount ?? 0" @command="command" @retry-problem="retryProblem" @dismiss-problem="dismissProblem">
|
|
||||||
<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>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
<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}종`}]" header-title="이동 정보" header-description="출발/도착 창고와 이동일을 먼저 확인합니다." detail-title="이동 품목" :detail-count="lines.length" detail-description="가용재고와 이동수량을 비교하며 입력합니다." @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>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, reactive, ref } from 'vue'
|
|
||||||
import { KbxDataGrid, KbxDateField, KbxFormGrid, KbxFormSection, KbxLookup, KbxInput, KbxTransactionPage, KbxWorkflowBar } from '@kbx/ui'
|
|
||||||
import { purchaseColumns, purchaseScreen, purchaseWorkflow, type PurchaseLine } from './purchase.definition'
|
|
||||||
const status=ref('DRAFT'); const lines=ref<PurchaseLine[]>([{clientId:crypto.randomUUID(),itemId:null,itemCode:'',itemName:'',quantity:1,unitPrice:0,amount:0,dueDate:''}])
|
|
||||||
const header=reactive({ purchaseDate:new Date().toISOString().slice(0,10), supplierId:null as string|null, warehouseId:null as string|null, buyer:'', remark:'' })
|
|
||||||
const total=computed(()=>lines.value.reduce((s,x)=>s+x.quantity*x.unitPrice,0))
|
|
||||||
function command(id:string){ if(id==='new'){ status.value='DRAFT'; lines.value=[] } if(id==='confirm' && status.value==='DRAFT') status.value='CONFIRMED' }
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<KbxTransactionPage :screen="purchaseScreen" :status="status" :summary-items="[{key:'amount',label:'총 구매금액',value:`₩${total.toLocaleString('ko-KR')}`,emphasis:true}]" header-title="구매 정보" header-description="거래처·입고창고·담당자 기준을 먼저 확정합니다." detail-title="구매 품목" :detail-count="lines.length" detail-description="수량·단가·납기일을 한 Grid에서 연속 입력합니다." @command="command">
|
|
||||||
<template #header>
|
|
||||||
<KbxWorkflowBar :workflow="purchaseWorkflow" :current="status" @transition="command" />
|
|
||||||
<KbxFormSection title="구매정보"><KbxFormGrid><KbxDateField v-model="header.purchaseDate" label="구매일" required/><KbxLookup v-model="header.supplierId" entity="customer" label="거래처" required/><KbxLookup v-model="header.warehouseId" entity="warehouse" label="입고창고" required/><KbxInput v-model="header.buyer" label="담당자"/></KbxFormGrid></KbxFormSection>
|
|
||||||
</template>
|
|
||||||
<template #detail><KbxDataGrid :rows="lines" :columns="purchaseColumns" row-key="clientId" editable clipboard /></template>
|
|
||||||
</KbxTransactionPage>
|
|
||||||
</template>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { reactive, ref } from 'vue'
|
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
import { KbxDataGrid, KbxListPage, KbxSearchPanel, KbxWorkflowBar } from '@kbx/ui'
|
|
||||||
import { claimColumns, claimScreen, claimSearchFields, claimWorkflow, type ClaimRow } from './claims.definition'
|
|
||||||
import { claimsApi } from './claimsApi'
|
|
||||||
const search = reactive<Record<string,unknown>>({ status:'REQUESTED' })
|
|
||||||
const selected = ref<ClaimRow[]>([])
|
|
||||||
const q = useQuery({ queryKey:['oms','claims',search], queryFn:()=>claimsApi.search(search), enabled:false })
|
|
||||||
async function run(){ selected.value=[]; await q.refetch() }
|
|
||||||
async function command(id:string){ if(id==='search') return run(); if(['approve','hold'].includes(id)) for(const row of selected.value) await claimsApi.transition(row.id,id); await run() }
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<KbxListPage :screen="claimScreen" :content-state="q.error.value?'error':q.isFetching.value&&!q.data.value?'loading':!q.data.value?'idle':q.data.value.totalCount===0?'empty':'ready'" :refreshing="q.isFetching.value&&Boolean(q.data.value)" @command="command">
|
|
||||||
<template #search><KbxSearchPanel v-model="search" :fields="claimSearchFields" @search="run" /></template>
|
|
||||||
<template #content>
|
|
||||||
<KbxWorkflowBar v-if="selected.length === 1" :workflow="claimWorkflow" :current="selected[0].status" @transition="command" />
|
|
||||||
<KbxDataGrid v-model:selection="selected" :rows="q.data.value?.items ?? []" :columns="claimColumns" row-key="id" selection="multiple" :loading="q.isFetching.value" personalization exportable />
|
|
||||||
</template>
|
|
||||||
<template #summary>클레임 {{ q.data.value?.totalCount ?? 0 }}건 · 선택 {{ selected.length }}건</template>
|
|
||||||
</KbxListPage>
|
|
||||||
</template>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { defineKbxScreen, type KbxGridColumn, type KbxRecordStatePolicy, type KbxWorkflowDefinition } from '@kbx/ui'
|
|
||||||
|
|
||||||
export interface OrderLineForm { clientId:string; itemId:string|null; itemCode:string; itemName:string; availableQty:number|null; orderQty:number; unitPrice:number; amount:number; remark:string }
|
|
||||||
|
|
||||||
export const orderWorkflow:KbxWorkflowDefinition={id:'oms.order.lifecycle',version:'1.0.0',states:[
|
|
||||||
{value:'신규',label:'신규',semantic:'draft'},{value:'작성',label:'작성',semantic:'draft'},{value:'확정',label:'확정',semantic:'pending'},{value:'할당',label:'할당',semantic:'processing'},{value:'피킹',label:'피킹',semantic:'processing'},{value:'검수',label:'검수',semantic:'processing'},{value:'출고완료',label:'출고완료',semantic:'completed',terminal:true}],
|
|
||||||
transitions:[{id:'confirm',from:['작성'],to:'확정',label:'주문확정',permission:'oms.order.confirm',confirm:true}]}
|
|
||||||
export const orderStatePolicies:KbxRecordStatePolicy[]=[{status:'신규',editability:'editable'},{status:'작성',editability:'editable'},{status:'확정',editability:'readonly',message:'확정된 주문은 직접 수정할 수 없습니다.'},{status:'할당',editability:'readonly'},{status:'피킹',editability:'readonly'},{status:'검수',editability:'readonly'},{status:'출고완료',editability:'readonly'}]
|
|
||||||
|
|
||||||
export const orderRegisterScreen = defineKbxScreen({
|
|
||||||
id:'OMS-ORD-002',version:'1.3.0',module:'OMS',type:'transaction', templateCode:'T03',title:'주문등록',helpKey:'OMS-ORD-002',permissions:['oms.order.read','erp.item.read'],
|
|
||||||
commands:[
|
|
||||||
{id:'new',label:'신규',group:'edit'},
|
|
||||||
{id:'save',label:'저장',group:'edit',variant:'primary',shortcut:'F8',permissionByStatus:{'신규':'oms.order.create','작성':'oms.order.write'},allowedStatuses:['신규','작성'],requiresDirty:true},
|
|
||||||
{id:'confirm',label:'주문확정',group:'workflow',permission:'oms.order.confirm',allowedStatuses:['작성'],requiresClean:true,disabledReason:'변경사항을 저장한 뒤 주문을 확정하세요.',confirm:{title:'주문을 확정하시겠습니까?',detail:'확정 후 주문의 일반 입력 필드는 직접 수정할 수 없습니다.',level:'high',confirmLabel:'주문확정'}},
|
|
||||||
{id:'copy',label:'주문복사',group:'edit'},
|
|
||||||
{id:'excel',label:'엑셀',group:'output'},
|
|
||||||
],telemetry:{enabled:true},
|
|
||||||
})
|
|
||||||
export const orderLineColumns:KbxGridColumn<OrderLineForm>[]=[
|
|
||||||
{field:'itemCode',header:'품목코드',type:'lookup',width:130,editable:true,pinned:'left',lookup:{entity:'item'}},{field:'itemName',header:'품목명',width:220},{field:'availableQty',header:'가용재고',type:'quantity',width:100},{field:'orderQty',header:'수량',type:'quantity',width:95,editable:true},{field:'unitPrice',header:'단가',type:'money',width:120,editable:true},{field:'amount',header:'금액',type:'money',width:130},{field:'remark',header:'비고',width:220,editable:true},
|
|
||||||
]
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import {
|
|
||||||
KbxBarcodeCapture,
|
|
||||||
KbxWmsActionButton,
|
|
||||||
KbxWmsMobilePage,
|
|
||||||
} from '@kbx/ui'
|
|
||||||
import PickingExceptionSheet from './PickingExceptionSheet.vue'
|
|
||||||
import PickingQuantitySheet from './PickingQuantitySheet.vue'
|
|
||||||
import { useWmsPicking } from './useWmsPicking'
|
|
||||||
import { pickingScreen } from './picking.definition'
|
|
||||||
|
|
||||||
const props = defineProps<{ taskId: string }>()
|
|
||||||
const vm = useWmsPicking(props.taskId)
|
|
||||||
|
|
||||||
const progress = computed(() => vm.task.value
|
|
||||||
? `${vm.task.value.completedLines} / ${vm.task.value.totalLines}`
|
|
||||||
: undefined)
|
|
||||||
|
|
||||||
const current = computed(() => vm.task.value?.currentLine ?? null)
|
|
||||||
const stageTitle = computed(() => {
|
|
||||||
switch (vm.task.value?.stage) {
|
|
||||||
case 'ready': return '작업을 시작하세요.'
|
|
||||||
case 'await-location': return '위치를 스캔하세요.'
|
|
||||||
case 'await-item': return '상품을 스캔하세요.'
|
|
||||||
case 'processing': return '처리 중입니다.'
|
|
||||||
case 'completed': return '피킹을 완료했습니다.'
|
|
||||||
case 'blocked': return '관리자 확인이 필요합니다.'
|
|
||||||
default: return '작업을 불러오는 중입니다.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<KbxWmsMobilePage
|
|
||||||
:screen="pickingScreen"
|
|
||||||
:progress="progress"
|
|
||||||
:online="vm.online.value"
|
|
||||||
:pending-commands="vm.pendingCommands.value"
|
|
||||||
:syncing="vm.syncing.value"
|
|
||||||
:content-state="vm.contentState.value"
|
|
||||||
retry-label="작업 다시 불러오기"
|
|
||||||
@retry="vm.reload"
|
|
||||||
>
|
|
||||||
<p v-if="vm.message.value" class="message" role="status">{{ vm.message.value }}</p>
|
|
||||||
|
|
||||||
<section v-if="vm.task.value" class="instruction">
|
|
||||||
<p class="eyebrow">{{ stageTitle }}</p>
|
|
||||||
|
|
||||||
<template v-if="current">
|
|
||||||
<div class="location">
|
|
||||||
<span>LOCATION</span>
|
|
||||||
<strong>{{ current.locationCode }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="item">
|
|
||||||
<span>상품</span>
|
|
||||||
<strong>{{ current.itemName }}</strong>
|
|
||||||
<small>{{ current.itemCode }}<template v-if="current.itemOption"> · {{ current.itemOption }}</template></small>
|
|
||||||
<small>바코드 {{ current.barcode }}</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="qty">
|
|
||||||
<div><span>필요</span><strong>{{ current.requiredQty }}</strong></div>
|
|
||||||
<div><span>피킹</span><strong>{{ current.pickedQty }}</strong></div>
|
|
||||||
<div><span>남음</span><strong>{{ current.remainingQty }}</strong></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<KbxWmsActionButton
|
|
||||||
v-if="current && vm.task.value.stage === 'await-item' && current.pickedQty > 0 && current.remainingQty > 0"
|
|
||||||
class="quantity-action"
|
|
||||||
label="수량 직접 입력"
|
|
||||||
variant="secondary"
|
|
||||||
@click="vm.openQuantity"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-else-if="vm.task.value.stage === 'ready'" class="ready">
|
|
||||||
<strong>{{ vm.task.value.taskNo }}</strong>
|
|
||||||
<span>총 {{ vm.task.value.totalLines }}개 라인 · {{ vm.task.value.totalQty }}개</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<KbxBarcodeCapture
|
|
||||||
v-if="vm.task.value && !['ready','completed','blocked'].includes(vm.task.value.stage)"
|
|
||||||
:enabled="vm.scanEnabled.value"
|
|
||||||
:label="vm.scanLabel.value"
|
|
||||||
@scan="vm.handleScan"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template #actions>
|
|
||||||
<KbxWmsActionButton
|
|
||||||
v-if="vm.task.value?.stage === 'ready'"
|
|
||||||
label="작업 시작"
|
|
||||||
:busy="vm.starting.value"
|
|
||||||
:disabled="!vm.online.value"
|
|
||||||
@click="vm.start"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<KbxWmsActionButton
|
|
||||||
v-else-if="vm.task.value && ['await-location','await-item'].includes(vm.task.value.stage)"
|
|
||||||
label="문제 신고"
|
|
||||||
variant="secondary"
|
|
||||||
:disabled="!vm.online.value || vm.pendingCommands.value > 0"
|
|
||||||
@click="vm.openException"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<KbxWmsActionButton
|
|
||||||
v-else-if="vm.task.value?.stage === 'completed'"
|
|
||||||
label="작업 목록으로"
|
|
||||||
@click="$router.push('/wms/picking')"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</KbxWmsMobilePage>
|
|
||||||
|
|
||||||
<PickingQuantitySheet
|
|
||||||
v-if="vm.quantityOpen.value && current"
|
|
||||||
:current="current.pickedQty"
|
|
||||||
:required="current.requiredQty"
|
|
||||||
:busy="vm.settingQuantity.value"
|
|
||||||
@close="vm.closeQuantity"
|
|
||||||
@submit="vm.setQuantity"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PickingExceptionSheet
|
|
||||||
v-if="vm.exceptionOpen.value"
|
|
||||||
:busy="vm.reportingException.value"
|
|
||||||
@close="vm.closeException"
|
|
||||||
@submit="(type, memo) => vm.reportException(type, memo)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.message { margin:0 0 12px; padding:10px 12px; background:var(--kbx-color-surface-subtle); border-radius:6px; font-size:14px; }
|
|
||||||
.instruction { text-align:center; }
|
|
||||||
.eyebrow { margin:0 0 12px; font-size:16px; font-weight:650; }
|
|
||||||
.location { padding:16px; background:var(--kbx-color-surface-muted); border:1px solid var(--kbx-color-border); border-radius:8px; }
|
|
||||||
.location span, .item span { display:block; color:var(--kbx-color-text-muted); font-size:12px; font-weight:600; }
|
|
||||||
.location strong { display:block; margin-top:3px; font-size:32px; letter-spacing:.03em; }
|
|
||||||
.item { padding:20px 4px 8px; }
|
|
||||||
.item strong { display:block; margin-top:4px; font-size:22px; }
|
|
||||||
.item small { display:block; margin-top:4px; color:var(--kbx-color-text-muted); font-size:13px; }
|
|
||||||
.qty { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-top:12px; }
|
|
||||||
.qty div { border:1px solid var(--kbx-color-border); border-radius:8px; padding:12px 4px; }
|
|
||||||
.qty span { display:block; font-size:13px; color:var(--kbx-color-text-muted); }
|
|
||||||
.qty strong { display:block; margin-top:2px; font-size:28px; }
|
|
||||||
.quantity-action { margin-top:var(--kbx-space-3); }
|
|
||||||
.ready { min-height:260px; display:flex; flex-direction:column; justify-content:center; gap:8px; }
|
|
||||||
.ready strong { font-size:26px; }
|
|
||||||
.ready span { color:var(--kbx-color-text-muted); }
|
|
||||||
</style>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import type { KbxWmsScanCommand } from '@kbx/contracts'
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'kbx:wms:safe-retry:v1'
|
|
||||||
|
|
||||||
export interface QueuedPickingScan {
|
|
||||||
command: KbxWmsScanCommand
|
|
||||||
queuedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readPickingRetryQueue(): QueuedPickingScan[] {
|
|
||||||
try {
|
|
||||||
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]') as QueuedPickingScan[]
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queuePickingScan(command: KbxWmsScanCommand) {
|
|
||||||
// Safety-first queue: only unacknowledged commands are retained for replay.
|
|
||||||
// UI stops accepting the next authoritative scan until the server confirms this one.
|
|
||||||
const queue = readPickingRetryQueue()
|
|
||||||
if (!queue.some(x => x.command.idempotencyKey === command.idempotencyKey)) {
|
|
||||||
queue.push({ command, queuedAt: new Date().toISOString() })
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(queue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removePickingScan(idempotencyKey: string) {
|
|
||||||
const next = readPickingRetryQueue().filter(x => x.command.idempotencyKey !== idempotencyKey)
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(next))
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, reactive, ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { isKbxProblem } from '@kbx/contracts'
|
|
||||||
import { KbxDataGrid, KbxQueuePage, KbxSearchPanel } from '@kbx/ui'
|
|
||||||
import { routeForWmsTask, searchWmsWork } from './workApi'
|
|
||||||
import { wmsWorkColumns, wmsWorkScreen, wmsWorkSearchFields, type WmsWorkRow } from './work.definition'
|
|
||||||
const router=useRouter();const search=reactive({taskType:'',status:'READY',owner:'mine',keyword:''});const rows=ref<WmsWorkRow[]>([]);const selection=ref<WmsWorkRow[]>([]);const loading=ref(false);const searched=ref(false);const error=ref<unknown>(null);const activeExceptionKey=ref<string|null>(null)
|
|
||||||
const queryProblem=computed(()=>isKbxProblem(error.value)?error.value:null)
|
|
||||||
const contentState=computed(()=>error.value?'error':loading.value&&!searched.value?'loading':!searched.value?'idle':rows.value.length===0?'empty':'ready')
|
|
||||||
async function executeSearch(){loading.value=true;error.value=null;selection.value=[];try{rows.value=await searchWmsWork(search);searched.value=true}catch(cause){error.value=cause;searched.value=true}finally{loading.value=false}}
|
|
||||||
function executeCommand(id:string){if(id==='search')return executeSearch();if(id==='start'&&selection.value[0])return router.push(routeForWmsTask(selection.value[0]))}
|
|
||||||
const summaryItems=computed(()=>[
|
|
||||||
{key:'total',label:'현재 결과',value:rows.value.length},
|
|
||||||
{key:'ready',label:'대기',value:rows.value.filter(x=>x.status==='READY').length},
|
|
||||||
{key:'blocked',label:'예외',value:rows.value.filter(x=>x.status==='BLOCKED').length,emphasis:true},
|
|
||||||
])
|
|
||||||
const exceptionCounters=computed(()=>[{key:'blocked',label:'확인 필요한 작업',count:rows.value.filter(x=>x.status==='BLOCKED').length,severity:'warning' as const}])
|
|
||||||
async function filterException(key:string|null){activeExceptionKey.value=key;search.status=key==='blocked'?'BLOCKED':'READY';await executeSearch()}
|
|
||||||
async function resetSearch(){search.taskType='';search.status='READY';search.owner='mine';search.keyword='';activeExceptionKey.value=null;await executeSearch()}
|
|
||||||
</script>
|
|
||||||
<template><KbxQueuePage :screen="wmsWorkScreen" :selection-count="selection.length" :content-state="contentState" :refreshing="loading&&searched&&!error" :summary-items="summaryItems" :exception-counters="exceptionCounters" :active-exception-key="activeExceptionKey" queue-title="현재 작업 Queue" queue-description="작업유형·작업자·상태를 기준으로 지금 처리할 업무만 모아서 봅니다." :queue-count="rows.length" breadcrumb="WMS > 작업관리" empty-title="조건에 맞는 물류 작업이 없습니다." empty-detail="작업유형·상태·작업자 조건을 변경하거나 기본조건으로 다시 조회하세요." empty-action-label="기본조건으로 조회" :error-title="queryProblem?.title || '물류 작업을 조회하지 못했습니다.'" :error-detail="queryProblem?.detail || '네트워크 상태를 확인한 후 다시 조회하세요.'" :error-retryable="queryProblem?.retryable !== false" retry-label="다시 조회" @command="executeCommand" @exception-filter="filterException" @empty-action="resetSearch"><template #search><KbxSearchPanel v-model="search" :fields="wmsWorkSearchFields" @search="executeSearch"/></template><template #content><KbxDataGrid :rows="rows" :columns="wmsWorkColumns" row-key="taskId" selection="single" :loading="loading" @selection-changed="selection=$event" @row-double-clicked="row=>router.push(routeForWmsTask(row))"/></template></KbxQueuePage></template>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"foundationIteration": 50,
|
|
||||||
"contractVersion": "1.31.0",
|
|
||||||
"baseline": "v21",
|
|
||||||
"declaredChangeLevel": "major",
|
|
||||||
"releaseMode": "fe-operational-truth-home-retry-hardening",
|
|
||||||
"notes": "v50 continues FE-first hardening without adding BE coupling or new template families. All major KBX templates now expose an explicit errorRetryable contract so permanent permission/integration/configuration failures do not render meaningless retry actions. WMS-WORK-001 marks the production API-not-connected problem non-retryable and T06 binds the problem retryability to its state boundary. Home Attention keeps the 8-item default density, truthfully labels the 24-item expansion as a bounded priority view when more items exist, and exposes direct Work Center / Notification Center routes for remaining items instead of implying that all items are on screen. Reference HTML/JavaScript mirrors the bounded disclosure and center navigation under KBX Business Light/Dark. Domain truth, permissions, idempotency and authoritative WMS completion remain server-owned."
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
const props=withDefaults(defineProps<{open:boolean;title:string;size?:'sm'|'md'|'lg';closeable?:boolean}>(),{size:'md',closeable:true})
|
|
||||||
const emit=defineEmits<{ 'update:open':[boolean] }>()
|
|
||||||
const widths={sm:'420px',md:'600px',lg:'840px'} as const
|
|
||||||
</script>
|
|
||||||
<template><Dialog :visible="open" modal :header="title" :closable="closeable" :style="{width:widths[size]}" @update:visible="emit('update:open',$event)"><slot/><template #footer><slot name="footer"/></template></Dialog></template>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Drawer from 'primevue/drawer'
|
|
||||||
withDefaults(defineProps<{open:boolean;title:string;position?:'left'|'right';width?:string}>(),{position:'right',width:'560px'})
|
|
||||||
const emit=defineEmits<{ 'update:open':[boolean] }>()
|
|
||||||
</script>
|
|
||||||
<template><Drawer :visible="open" :header="title" :position="position" :style="{width}" @update:visible="emit('update:open',$event)"><slot/><template #footer><slot name="footer"/></template></Drawer></template>
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import type {
|
|
||||||
KbxImportDefinition,
|
|
||||||
KbxImportMapping,
|
|
||||||
KbxImportSession,
|
|
||||||
KbxImportProgressEvent,
|
|
||||||
} from '@kbx/contracts'
|
|
||||||
import KbxButton from './KbxButton.vue'
|
|
||||||
import KbxConfirm from './KbxConfirm.vue'
|
|
||||||
import KbxJobProgress from './KbxJobProgress.vue'
|
|
||||||
import KbxProgressSteps from './KbxProgressSteps.vue'
|
|
||||||
import { validateKbxImportFileCandidate, validateKbxImportMappings } from '../excel/importGuard'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
definition: KbxImportDefinition
|
|
||||||
session: KbxImportSession | null
|
|
||||||
busy?: boolean
|
|
||||||
progress?: KbxImportProgressEvent | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
upload: [File]
|
|
||||||
saveMapping: [KbxImportMapping[]]
|
|
||||||
saveNamedMapping: [string, KbxImportMapping[]]
|
|
||||||
validate: []
|
|
||||||
commit: []
|
|
||||||
downloadTemplate: []
|
|
||||||
downloadErrors: []
|
|
||||||
cancel: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null)
|
|
||||||
const localMapping = ref<KbxImportMapping[] | null>(null)
|
|
||||||
const dragActive = ref(false)
|
|
||||||
const mappingName = ref('')
|
|
||||||
const clientIssue = ref('')
|
|
||||||
const commitConfirmOpen = ref(false)
|
|
||||||
const importSteps=[{key:'file',label:'파일'},{key:'mapping',label:'매핑'},{key:'validation',label:'검증'},{key:'commit',label:'반영'}]
|
|
||||||
const currentStepKey=computed(()=>step.value===1?'file':step.value===2?'mapping':step.value===3?'validation':'commit')
|
|
||||||
|
|
||||||
const step = computed(() => {
|
|
||||||
const status = props.session?.status
|
|
||||||
if (!status || status === 'created') return 1
|
|
||||||
if (status === 'uploaded' || status === 'mapping-required') return 2
|
|
||||||
if (status === 'validating' || status === 'validated') return 3
|
|
||||||
return 4
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => props.session?.id, () => {
|
|
||||||
localMapping.value = null
|
|
||||||
mappingName.value = ''
|
|
||||||
clientIssue.value = ''
|
|
||||||
commitConfirmOpen.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
const mappings = computed({
|
|
||||||
get: () => localMapping.value ?? (props.session?.mapping ?? []),
|
|
||||||
set: value => { localMapping.value = value },
|
|
||||||
})
|
|
||||||
|
|
||||||
const importableFields = computed(() => props.definition.fields.filter(field => field.importable !== false))
|
|
||||||
const mappingIssues = computed(() => validateKbxImportMappings(props.definition,mappings.value))
|
|
||||||
const canValidate = computed(() => mappings.value.length > 0 && mappingIssues.value.length === 0 && !props.busy)
|
|
||||||
const canCommit = computed(() => (props.session?.validRows ?? 0) > 0 && props.session?.status === 'validated' && !props.busy)
|
|
||||||
const isFailed = computed(() => props.session?.status === 'failed')
|
|
||||||
const isCancelled = computed(() => props.session?.status === 'cancelled')
|
|
||||||
const isPartial = computed(() => props.session?.status === 'partially-completed')
|
|
||||||
|
|
||||||
function chooseFile() { clientIssue.value=''; fileInput.value?.click() }
|
|
||||||
function onFiles(files: FileList | null) {
|
|
||||||
const file = files?.item(0)
|
|
||||||
if (!file) return
|
|
||||||
clientIssue.value=''
|
|
||||||
const issue=validateKbxImportFileCandidate(props.definition,file)
|
|
||||||
if(issue){clientIssue.value=issue;return}
|
|
||||||
emit('upload', file)
|
|
||||||
}
|
|
||||||
function onDrop(event: DragEvent) {
|
|
||||||
dragActive.value = false
|
|
||||||
onFiles(event.dataTransfer?.files ?? null)
|
|
||||||
}
|
|
||||||
function setTarget(index: number, targetField: string) {
|
|
||||||
const next = mappings.value.map((item, itemIndex) => itemIndex === index
|
|
||||||
? { ...item, targetField: targetField || null, source: 'manual' as const, confidence: undefined, reason:undefined }
|
|
||||||
: item)
|
|
||||||
mappings.value = next
|
|
||||||
emit('saveMapping', next)
|
|
||||||
}
|
|
||||||
function requestCommit(){if(canCommit.value)commitConfirmOpen.value=true}
|
|
||||||
function confirmCommit(){commitConfirmOpen.value=false;emit('commit')}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="kbx-import" data-kbx-component="excel-import" :aria-busy="busy || undefined">
|
|
||||||
<header class="kbx-import__header">
|
|
||||||
<div>
|
|
||||||
<h2>{{ definition.title }}</h2>
|
|
||||||
<p>파일 → 매핑 → 검증 → 반영 순서로 처리합니다. 원본 행 번호와 오류 이력은 유지됩니다.</p>
|
|
||||||
</div>
|
|
||||||
<KbxButton label="업로드 양식 다운로드" variant="secondary" @click="emit('downloadTemplate')" />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<KbxProgressSteps data-kbx-surface="progress-steps" :steps="importSteps" :current="currentStepKey" />
|
|
||||||
|
|
||||||
<div v-if="step === 1" class="kbx-import__drop" data-kbx-surface="file"
|
|
||||||
:class="{ 'is-dragging': dragActive }"
|
|
||||||
@dragover.prevent="dragActive = true"
|
|
||||||
@dragleave.prevent="dragActive = false"
|
|
||||||
@drop.prevent="onDrop">
|
|
||||||
<input ref="fileInput" hidden type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @change="onFiles(($event.target as HTMLInputElement).files)">
|
|
||||||
<strong>Excel 파일을 선택하세요.</strong>
|
|
||||||
<span>.xlsx · 최대 {{ Math.round((definition.maxFileSizeBytes ?? 10485760) / 1048576) }}MB</span>
|
|
||||||
<KbxButton label="파일 선택" variant="primary" :loading="busy" @click="chooseFile" />
|
|
||||||
<small>Drag & Drop은 보조 기능이며 파일 선택 버튼은 항상 제공됩니다.</small>
|
|
||||||
<p v-if="clientIssue" class="kbx-import__client-error" role="alert">{{clientIssue}}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section v-else-if="step === 2" class="kbx-import__mapping" data-kbx-surface="mapping">
|
|
||||||
<div class="mapping-head"><span>Excel 열</span><span>시스템 필드</span><span>매핑 근거</span></div>
|
|
||||||
<div v-for="(mapping, index) in mappings" :key="mapping.sourceColumn" class="mapping-row">
|
|
||||||
<strong>{{ mapping.sourceColumn }}</strong>
|
|
||||||
<select :value="mapping.targetField ?? ''" :aria-invalid="mapping.targetField ? mappings.filter(item=>item.targetField===mapping.targetField).length>1 : undefined" @change="setTarget(index, ($event.target as HTMLSelectElement).value)">
|
|
||||||
<option value="">매핑하지 않음</option>
|
|
||||||
<option v-for="field in importableFields" :key="field.key" :value="field.key">
|
|
||||||
{{ field.label }}{{ field.required ? ' *' : '' }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<span class="mapping-reason">
|
|
||||||
{{ mapping.source === 'ai' ? `AI 추천 ${Math.round((mapping.confidence ?? 0) * 100)}%` : mapping.reason ?? mapping.source }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="mappingIssues.length" class="mapping-issues" role="alert">
|
|
||||||
<strong>매핑을 확인하세요.</strong>
|
|
||||||
<ul><li v-for="issue in mappingIssues" :key="`${issue.code}:${issue.fieldKey}`">{{issue.message}}</li></ul>
|
|
||||||
</div>
|
|
||||||
<div class="mapping-save">
|
|
||||||
<label>다음에도 사용할 매핑 이름 <input v-model="mappingName" maxlength="80" placeholder="예: 쿠팡 주문양식"></label>
|
|
||||||
<KbxButton label="매핑 저장" variant="secondary" :disabled="!mappingName.trim() || Boolean(mappingIssues.length)" @click="emit('saveNamedMapping', mappingName.trim(), mappings)" />
|
|
||||||
</div>
|
|
||||||
<div class="kbx-import__actions">
|
|
||||||
<KbxButton label="취소" variant="secondary" @click="emit('cancel')" />
|
|
||||||
<KbxButton label="검증 시작" variant="primary" :disabled="!canValidate" :loading="busy" @click="emit('validate')" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="step === 3" class="kbx-import__validation" data-kbx-surface="validation">
|
|
||||||
<KbxJobProgress v-if="session?.status === 'validating'" :progress="progress ?? null" />
|
|
||||||
<template v-else>
|
|
||||||
<div class="summary" aria-label="검증 결과">
|
|
||||||
<div><span>전체</span><strong>{{ session?.totalRows.toLocaleString() }}</strong></div>
|
|
||||||
<div><span>정상</span><strong>{{ session?.validRows.toLocaleString() }}</strong></div>
|
|
||||||
<div><span>오류</span><strong>{{ session?.invalidRows.toLocaleString() }}</strong></div>
|
|
||||||
<div><span>경고</span><strong>{{ session?.warningRows.toLocaleString() }}</strong></div>
|
|
||||||
</div>
|
|
||||||
<div v-if="session?.errors?.length" class="errors">
|
|
||||||
<div class="errors__head"><span>행</span><span>필드</span><span>사유</span></div>
|
|
||||||
<div v-for="error in session.errors.slice(0, 100)" :key="`${error.rowNumber}-${error.field}-${error.code}`" class="errors__row" :data-severity="error.severity">
|
|
||||||
<span>{{ error.rowNumber }}</span><span>{{ error.sourceColumn ?? error.field ?? '-' }}</span><span>{{ error.message }}</span>
|
|
||||||
</div>
|
|
||||||
<small v-if="session.errors.length > 100">화면에는 처음 100건만 표시합니다. 전체 오류는 오류파일로 확인하세요.</small>
|
|
||||||
</div>
|
|
||||||
<div class="kbx-import__actions">
|
|
||||||
<KbxButton v-if="session?.invalidRows" label="오류파일 다운로드" variant="secondary" @click="emit('downloadErrors')" />
|
|
||||||
<KbxButton label="정상 데이터 반영" variant="primary" :disabled="!canCommit" @click="requestCommit" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else class="kbx-import__commit" data-kbx-surface="result" :data-result="session?.status">
|
|
||||||
<KbxJobProgress v-if="session?.status === 'committing'" :progress="progress ?? null" />
|
|
||||||
<div v-else-if="isFailed" class="kbx-import__terminal is-error" role="alert">
|
|
||||||
<strong>{{session?.failure?.title ?? '반영 작업을 완료하지 못했습니다.'}}</strong>
|
|
||||||
<p>{{session?.failure?.detail ?? '원본 업로드와 작업 이력은 유지됩니다. 원인을 확인한 뒤 다시 진행하세요.'}}</p>
|
|
||||||
<small v-if="session?.failure?.code">오류코드 {{session.failure.code}}</small>
|
|
||||||
<div class="kbx-import__actions"><KbxButton label="새 파일로 다시 시작" variant="secondary" @click="emit('cancel')" /></div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="isCancelled" class="kbx-import__terminal">
|
|
||||||
<strong>반영 작업이 취소되었습니다.</strong><p>새 파일을 선택해 다시 시작할 수 있습니다.</p>
|
|
||||||
<div class="kbx-import__actions"><KbxButton label="새 파일 선택" variant="secondary" @click="emit('cancel')" /></div>
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
|
||||||
<div class="summary summary--result" :class="{'is-partial':isPartial}">
|
|
||||||
<div><span>신규</span><strong>{{ session?.createdRows.toLocaleString() }}</strong></div>
|
|
||||||
<div><span>수정</span><strong>{{ session?.updatedRows.toLocaleString() }}</strong></div>
|
|
||||||
<div><span>오류</span><strong>{{ session?.invalidRows.toLocaleString() }}</strong></div>
|
|
||||||
</div>
|
|
||||||
<p v-if="isPartial" class="kbx-import__partial">정상 데이터는 반영되었고 오류 데이터는 제외되었습니다. 오류파일로 실패 건만 다시 처리할 수 있습니다.</p>
|
|
||||||
<div v-if="isPartial && session?.invalidRows" class="kbx-import__actions"><KbxButton label="오류파일 다운로드" variant="secondary" @click="emit('downloadErrors')" /></div>
|
|
||||||
</template>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<KbxConfirm
|
|
||||||
:open="commitConfirmOpen"
|
|
||||||
title="정상 데이터를 반영하시겠습니까?"
|
|
||||||
:detail="`${(session?.validRows ?? 0).toLocaleString()}건을 반영합니다. 오류 ${(session?.invalidRows ?? 0).toLocaleString()}건은 제외됩니다.`"
|
|
||||||
level="high"
|
|
||||||
:confirm-label="`${(session?.validRows ?? 0).toLocaleString()}건 반영`"
|
|
||||||
@update:open="commitConfirmOpen=$event"
|
|
||||||
@confirm="confirmCommit"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.kbx-import{display:grid;gap:var(--kbx-space-4);min-width:0}.kbx-import__header{display:flex;justify-content:space-between;gap:var(--kbx-space-4);align-items:flex-start}.kbx-import__header h2{margin:0 0 var(--kbx-space-1);font-size:var(--kbx-font-xl)}.kbx-import__header p{margin:0;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}.kbx-import__drop{min-height:var(--kbx-import-drop-min-height);border:var(--kbx-border-width) dashed var(--kbx-color-border-strong);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--kbx-space-2);background:var(--kbx-color-surface-muted)}.kbx-import__drop.is-dragging{outline:calc(var(--kbx-border-width) * 2) solid var(--kbx-color-focus);outline-offset:calc(var(--kbx-border-width) * -2)}.kbx-import__drop>span,.kbx-import__drop small{color:var(--kbx-color-text-muted)}.kbx-import__client-error{margin:0;color:var(--kbx-color-danger);font-size:var(--kbx-font-sm)}.kbx-import__mapping,.kbx-import__validation,.kbx-import__commit{border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface);padding:var(--kbx-space-3)}.mapping-head,.mapping-row{display:grid;grid-template-columns:minmax(var(--kbx-import-source-column-min-width),1fr) minmax(var(--kbx-import-target-column-min-width),1.3fr) minmax(var(--kbx-import-reason-column-min-width),.8fr);gap:var(--kbx-space-3);min-height:var(--kbx-control-md);align-items:center;border-bottom:var(--kbx-border-width) solid var(--kbx-color-border)}.mapping-head{font-size:var(--kbx-font-xs);font-weight:600;color:var(--kbx-color-text-muted)}.mapping-row select{height:var(--kbx-control-height);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);background:var(--kbx-color-surface)}.mapping-row select[aria-invalid="true"]{border-color:var(--kbx-color-danger);background:var(--kbx-color-danger-surface)}.mapping-reason{font-size:var(--kbx-font-xs);color:var(--kbx-color-text-muted)}.mapping-issues{margin-top:var(--kbx-space-3);padding:var(--kbx-space-3);border:var(--kbx-border-width) solid var(--kbx-color-danger-border);background:var(--kbx-color-danger-surface);font-size:var(--kbx-font-sm)}.mapping-issues ul{margin:var(--kbx-space-2) 0 0;padding-left:var(--kbx-space-5)}.mapping-save{display:flex;align-items:center;justify-content:flex-end;gap:var(--kbx-space-2);padding-top:var(--kbx-space-3)}.mapping-save label{display:flex;align-items:center;gap:var(--kbx-space-2);font-size:var(--kbx-font-sm)}.mapping-save input{width:var(--kbx-import-mapping-name-width);height:var(--kbx-control-height);border:var(--kbx-border-width) solid var(--kbx-color-border-strong);border-radius:var(--kbx-radius-sm);padding:0 var(--kbx-space-2)}.summary{display:grid;grid-template-columns:repeat(4,minmax(var(--kbx-summary-item-min-width),1fr));border:var(--kbx-border-width) solid var(--kbx-color-border);margin-bottom:var(--kbx-space-3)}.summary>div{padding:var(--kbx-space-3);display:flex;justify-content:space-between;border-right:var(--kbx-border-width) solid var(--kbx-color-border)}.summary>div:last-child{border-right:0}.summary strong{font-size:var(--kbx-font-xl)}.summary--result{grid-template-columns:repeat(3,minmax(var(--kbx-summary-item-min-width),1fr))}.summary--result.is-partial{border-color:var(--kbx-color-warning-border)}.errors{border:var(--kbx-border-width) solid var(--kbx-color-border)}.errors__head,.errors__row{display:grid;grid-template-columns:var(--kbx-import-error-row-width) var(--kbx-import-error-field-width) 1fr;gap:var(--kbx-space-2);min-height:var(--kbx-control-height);align-items:center;padding:0 var(--kbx-space-2);border-bottom:var(--kbx-border-width) solid var(--kbx-color-border);font-size:var(--kbx-font-sm)}.errors__head{font-weight:600;background:var(--kbx-color-surface-muted)}.errors__row[data-severity="warning"]{background:var(--kbx-color-warning-surface)}.errors__row[data-severity="error"]{background:var(--kbx-color-danger-surface)}.errors__row:last-of-type{border-bottom:0}.errors small{display:block;padding:var(--kbx-space-2);color:var(--kbx-color-text-muted)}.kbx-import__terminal{display:grid;gap:var(--kbx-space-2);padding:var(--kbx-space-4);background:var(--kbx-color-surface-muted);border:var(--kbx-border-width) solid var(--kbx-color-border)}.kbx-import__terminal.is-error{background:var(--kbx-color-danger-surface);border-color:var(--kbx-color-danger-border)}.kbx-import__terminal p,.kbx-import__partial{margin:0;color:var(--kbx-color-text-muted);font-size:var(--kbx-font-sm)}.kbx-import__actions{display:flex;justify-content:flex-end;gap:var(--kbx-space-2);margin-top:var(--kbx-space-3)}
|
|
||||||
</style>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { KbxStatusSemantic } from '@kbx/contracts'
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
label: string
|
|
||||||
semantic: KbxStatusSemantic
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="kbx-status" :data-semantic="semantic">
|
|
||||||
<span class="kbx-status__dot" aria-hidden="true" />
|
|
||||||
{{ label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.kbx-status { display:inline-flex; align-items:center; gap:6px; min-height:24px; padding:0 8px; border:1px solid var(--kbx-color-border); border-radius:999px; background:var(--kbx-color-surface); font-size:12px; font-weight:600; white-space:nowrap; }
|
|
||||||
.kbx-status__dot { width:6px; height:6px; border-radius:50%; background:var(--kbx-status-color, var(--kbx-color-text-muted)); }
|
|
||||||
.kbx-status[data-semantic="completed"] { --kbx-status-color:var(--kbx-color-success); }
|
|
||||||
.kbx-status[data-semantic="processing"], .kbx-status[data-semantic="pending"] { --kbx-status-color:var(--kbx-color-primary); }
|
|
||||||
.kbx-status[data-semantic="warning"], .kbx-status[data-semantic="hold"] { --kbx-status-color:var(--kbx-color-warning); }
|
|
||||||
.kbx-status[data-semantic="error"] { --kbx-status-color:var(--kbx-color-danger); }
|
|
||||||
.kbx-status[data-semantic="cancelled"], .kbx-status[data-semantic="disabled"] { --kbx-status-color:var(--kbx-color-text-muted); color:var(--kbx-color-text-muted); }
|
|
||||||
</style>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { KbxAsyncState, KbxProblem, KbxScreenDefinition, KbxSummaryItem, KbxTemplateContext, KbxWorkQueueCounter } from '@kbx/contracts'
|
|
||||||
import KbxQueuePage from './KbxQueuePage.vue'
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
|
||||||
screen:KbxScreenDefinition
|
|
||||||
selectionCount?:number
|
|
||||||
can?:(permission:string)=>boolean
|
|
||||||
breadcrumb?:string
|
|
||||||
context?:KbxTemplateContext|null
|
|
||||||
contentState?:KbxAsyncState
|
|
||||||
refreshing?:boolean
|
|
||||||
errorRetryable?:boolean
|
|
||||||
summaryItems?:KbxSummaryItem[]
|
|
||||||
exceptionCounters?:KbxWorkQueueCounter[]
|
|
||||||
activeExceptionKey?:string|null
|
|
||||||
problem?:KbxProblem|null
|
|
||||||
problemActionAllowlist?:readonly string[]
|
|
||||||
}>(), { context:null, contentState:'ready', refreshing:false, errorRetryable:true, summaryItems:()=>[], exceptionCounters:()=>[], activeExceptionKey:null, problem:null, problemActionAllowlist:()=>[] })
|
|
||||||
const emit=defineEmits<{ command:[string]; exceptionFilter:[string|null]; problemAction:[string]; retryProblem:[]; dismissProblem:[] }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<KbxQueuePage :screen="screen" :selection-count="selectionCount" :can="can" :breadcrumb="breadcrumb" :context="context" :content-state="contentState" :refreshing="refreshing" :error-retryable="errorRetryable" :summary-items="summaryItems" :exception-counters="exceptionCounters" :active-exception-key="activeExceptionKey" :problem="problem" :problem-action-allowlist="problemActionAllowlist" @command="emit('command',$event)" @exception-filter="emit('exceptionFilter',$event)" @problem-action="emit('problemAction',$event)" @retry-problem="emit('retryProblem')" @dismiss-problem="emit('dismissProblem')">
|
|
||||||
<template v-if="$slots.utility" #utility><slot name="utility" /></template>
|
|
||||||
<template v-if="$slots.notice" #notice><slot name="notice" /></template>
|
|
||||||
<template v-if="$slots.search" #search><slot name="search" /></template>
|
|
||||||
<template v-if="$slots['queue-summary'] || $slots.summary" #summary><slot name="queue-summary"><slot name="summary" /></slot></template>
|
|
||||||
<template v-if="$slots.exceptions" #exceptions><slot name="exceptions" /></template>
|
|
||||||
<template v-if="$slots.contextual" #contextual><slot name="contextual" /></template>
|
|
||||||
<template #content><slot name="content" /></template>
|
|
||||||
<template v-if="$slots.footer" #footer><slot name="footer" /></template>
|
|
||||||
<template v-if="$slots.detail" #detail><slot name="detail" /></template>
|
|
||||||
</KbxQueuePage>
|
|
||||||
</template>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, inject, ref } from 'vue'
|
|
||||||
import type { KbxWorkflowDefinition, KbxWorkflowTransitionDefinition } from '@kbx/contracts'
|
|
||||||
import KbxButton from './KbxButton.vue'
|
|
||||||
import KbxConfirm from './KbxConfirm.vue'
|
|
||||||
import KbxStatus from './KbxStatus.vue'
|
|
||||||
import { KbxPermissionHostKey } from '../permission/host'
|
|
||||||
const props=defineProps<{workflow:KbxWorkflowDefinition;current:string;can?:(permission:string)=>boolean}>();const emit=defineEmits<{transition:[string]}>();const pending=ref<KbxWorkflowTransitionDefinition|null>(null);const permissionHost=inject(KbxPermissionHostKey,null)
|
|
||||||
function canPermission(permission:string){return props.can?props.can(permission):(permissionHost?.has(permission)??true)}
|
|
||||||
const currentState=computed(()=>props.workflow.states.find(x=>x.value===props.current));const actions=computed(()=>props.workflow.transitions.filter(x=>x.from.includes(props.current)&&(!x.permission||canPermission(x.permission))))
|
|
||||||
function invoke(action:KbxWorkflowTransitionDefinition){if(action.confirm){pending.value=action;return}emit('transition',action.id)}function confirm(){if(!pending.value)return;const id=pending.value.id;pending.value=null;emit('transition',id)}
|
|
||||||
</script>
|
|
||||||
<template><section class="kbx-workflow-bar" aria-label="업무 상태"><div class="kbx-workflow-bar__state"><span class="label">현재 상태</span><KbxStatus v-if="currentState" :label="currentState.label" :semantic="currentState.semantic"/><strong v-else>{{current}}</strong></div><div class="kbx-workflow-bar__flow" aria-label="상태 흐름"><template v-for="(state,i) in workflow.states" :key="state.value"><span :class="['step',{current:state.value===current}]">{{state.label}}</span><span v-if="i<workflow.states.length-1" class="arrow">→</span></template></div><div class="kbx-workflow-bar__actions"><KbxButton v-for="action in actions" :key="action.id" :label="action.label" @click="invoke(action)"/></div><KbxConfirm v-if="pending" :open="Boolean(pending)" :title="`${pending.label}하시겠습니까?`" :detail="`${currentState?.label??current} 상태에서 ${workflow.states.find(x=>x.value===pending?.to)?.label??pending.to} 상태로 변경합니다.`" level="high" :confirm-label="pending.label" @update:open="value=>{if(!value)pending=null}" @confirm="confirm"/></section></template>
|
|
||||||
<style scoped>.kbx-workflow-bar{display:grid;grid-template-columns:auto 1fr auto;gap:var(--kbx-space-3);align-items:center;min-height:var(--kbx-command-bar-height);padding:var(--kbx-space-2);border:var(--kbx-border-width) solid var(--kbx-color-border);background:var(--kbx-color-surface-muted);border-radius:var(--kbx-radius-sm);font-size:var(--kbx-font-sm)}.kbx-workflow-bar__state{display:flex;align-items:center;gap:var(--kbx-space-2);white-space:nowrap}.label,.kbx-workflow-bar__flow{color:var(--kbx-color-text-muted)}.kbx-workflow-bar__flow{min-width:0;overflow:auto;display:flex;align-items:center;gap:var(--kbx-space-1);white-space:nowrap}.step.current{color:var(--kbx-color-text);font-weight:600}.arrow{color:var(--kbx-color-border-strong)}.kbx-workflow-bar__actions{display:flex;gap:var(--kbx-space-2)}@media(max-width:68.75rem){.kbx-workflow-bar{grid-template-columns:1fr auto}.kbx-workflow-bar__flow{grid-column:1/-1;grid-row:2}}</style>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
|
||||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
|
||||||
|
|
||||||
const queueRecovery=requireKbxScenario('scenario.common.operations.queue-recovery')
|
|
||||||
|
|
||||||
test.describe('COMMON-OPS-001 업무 예외 센터', () => {
|
|
||||||
test(scenarioTitle(queueRecovery.id), async ({ page }) => {
|
|
||||||
await page.goto('/operations/exceptions')
|
|
||||||
await expect(page.locator('[data-screen-id="COMMON-OPS-001"]')).toBeVisible()
|
|
||||||
await expect(page.locator('[data-kbx-surface="exception-summary"]')).toBeVisible()
|
|
||||||
await expect(page.locator('[data-kbx-surface="queue/content"]')).toBeVisible()
|
|
||||||
await expect(page.getByText(/정상 건은 표시하지 않습니다/)).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
|
||||||
import { requireKbxScenario, scenarioTitle } from './helpers/kbxScenario'
|
|
||||||
|
|
||||||
const mismatchRecovery=requireKbxScenario('scenario.common.reconcile.mismatch-recovery')
|
|
||||||
|
|
||||||
test.describe('COMMON-REC-001 업무 데이터 대사', () => {
|
|
||||||
test(scenarioTitle(mismatchRecovery.id), async ({ page }) => {
|
|
||||||
await page.goto('/operations/reconcile')
|
|
||||||
await expect(page.locator('[data-screen-id="COMMON-REC-001"]')).toBeVisible()
|
|
||||||
await expect(page.locator('[data-kbx-surface="criteria/search"]')).toBeVisible()
|
|
||||||
await expect(page.locator('[data-kbx-surface="comparison-grid"]')).toBeVisible()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
import { test, expect } from '@playwright/test'
|
|
||||||
test('OMS-CLM-001 keeps claim processing inside familiar list/workflow grammar', async ({page}) => {
|
|
||||||
await page.goto('/oms/claims'); await expect(page.locator('[data-screen-id="OMS-CLM-001"]')).toBeVisible(); await page.keyboard.press('F3');
|
|
||||||
})
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test'
|
|
||||||
|
|
||||||
test.describe('OMS-ORD-003 주문 Excel Import Golden Flow', () => {
|
|
||||||
test('파일 → 매핑 → 검증 → 정상건 반영', async ({ page }) => {
|
|
||||||
await page.goto('/oms/orders/import')
|
|
||||||
await expect(page.getByRole('heading', { name: '주문 Excel 업로드' })).toBeVisible()
|
|
||||||
|
|
||||||
// Host repository should supply tests/fixtures/orders-valid-and-invalid.xlsx.
|
|
||||||
await page.locator('input[type=file]').setInputFiles('tests/fixtures/orders-valid-and-invalid.xlsx')
|
|
||||||
await expect(page.getByText('2 매핑')).toBeVisible()
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: '검증 시작' }).click()
|
|
||||||
await expect(page.getByText('검증 결과')).toBeVisible({ timeout: 30_000 })
|
|
||||||
await expect(page.getByText('오류')).toBeVisible()
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: '정상 데이터 반영' }).click()
|
|
||||||
await expect(page.getByText('신규')).toBeVisible({ timeout: 30_000 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -22,17 +22,19 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Enable Corepack
|
- name: Enable Corepack
|
||||||
run: corepack enable
|
run: corepack enable
|
||||||
- name: Install and test when lockfile exists
|
- name: Install and execute FE runtime evidence when lockfile exists
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if [ -f pnpm-lock.yaml ]; then
|
if [ -f pnpm-lock.yaml ]; then
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm -r --if-present run typecheck
|
pnpm typecheck:web
|
||||||
pnpm -r --if-present run test
|
pnpm test:unit
|
||||||
pnpm -r --if-present run build
|
pnpm build:web:demo
|
||||||
echo "KBX canonical scenario contract validated statically; Playwright host run belongs to product repository."
|
pnpm exec playwright install --with-deps chromium
|
||||||
|
pnpm test:e2e
|
||||||
else
|
else
|
||||||
echo "Starter has no pnpm-lock.yaml yet; dependency build gate is intentionally deferred."
|
node scripts/report-fe-runtime-readiness.mjs
|
||||||
|
echo "pnpm-lock.yaml is absent; reproducible browser-runtime evidence is blocked and is not reported as PASS."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
@@ -16,6 +16,22 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Validate KBX contracts and configuration governance
|
- name: Validate KBX contracts and configuration governance
|
||||||
run: node scripts/validate-kbx.mjs
|
run: node scripts/validate-kbx.mjs
|
||||||
|
- name: Frontend runtime evidence gate
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
corepack enable
|
||||||
|
if [ -f pnpm-lock.yaml ]; then
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
pnpm typecheck:web
|
||||||
|
pnpm test:unit
|
||||||
|
pnpm build:web:demo
|
||||||
|
pnpm exec playwright install --with-deps chromium
|
||||||
|
pnpm test:e2e
|
||||||
|
else
|
||||||
|
node scripts/report-fe-runtime-readiness.mjs
|
||||||
|
echo "Frontend runtime evidence is BLOCKED because the immutable candidate has no pnpm-lock.yaml."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- name: Verify generated configuration/deployment artifacts
|
- 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/
|
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
|
- name: Build/test when host repository is available
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# KBX Foundation v60 — Status Canonical Contract Hardening
|
||||||
|
|
||||||
|
## 핵심
|
||||||
|
|
||||||
|
v60은 상태 Chip을 더 만드는 릴리스가 아니다. v59에서 만든 Status Dictionary를 **Backend/API canonical value → FE Catalog → Grid/Filter/Drawer/Workflow → Demo/Static Reference**까지 하나의 계약으로 닫는다.
|
||||||
|
|
||||||
|
- OMS 주문 Aggregate API는 `DRAFT / CONFIRMED / ALLOCATED / PICKING / CHECKED / SHIPPED` canonical code를 반환한다.
|
||||||
|
- 한국어 `작성 / 확정 / 출고완료`는 API 상태값이 아니라 FE Status Catalog의 display label이다.
|
||||||
|
- 신규 주문의 저장 전 상태는 FE-local `NEW`, 첫 저장 성공 이후 서버 상태는 `DRAFT`다.
|
||||||
|
- 주문 Search Select와 URL/저장조건 검증은 Grid와 같은 shipment Status Catalog를 사용한다.
|
||||||
|
- Demo Order Projection은 `shipmentStatus` 하나만 Source of Truth로 사용한다.
|
||||||
|
- Static HTML/JavaScript Reference도 canonical raw + familiar display를 동일하게 재현한다.
|
||||||
|
- 과거 한국어 display value로 저장된 Static Reference 검색조건은 canonical value로 안전하게 migration한다.
|
||||||
|
|
||||||
|
## QA
|
||||||
|
|
||||||
|
`node scripts/validate-kbx.mjs`와 `node scripts/validate-status-canonical-v60.mjs`로 계약을 검증한다. Browser Runtime은 lockfile/node_modules/registry 접근이 확보되기 전까지 별도 Runtime readiness에서 PASS로 간주하지 않는다.
|
||||||
@@ -1,7 +1,29 @@
|
|||||||
# KBX FE Reference Lab v50
|
# KBX FE Reference Lab v55
|
||||||
|
|
||||||
|
v55는 T01~T09 Reference 중 T08/T09의 운영 Truth를 강화한다.
|
||||||
|
|
||||||
|
- T08: 6단계 `파일/매핑/검증/미리보기/반영/결과`, 검증 오류 첫/다음 Focus 이동
|
||||||
|
- T09: Scan buffer와 최근 서버 확인 Scan을 분리하고 Offline에서는 authoritative mutation을 차단
|
||||||
|
- 기존 Home/T02/T04/T05 v54 work-surface regression을 유지
|
||||||
|
- Light/Dark/forced-colors와 strict CSP 경계를 유지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# KBX FE Reference Lab v53
|
||||||
|
|
||||||
Backend/API 없이 브라우저에서 즉시 실행하는 KBX 업무 UI 원형이다. 목적은 별도 Demo UI를 만드는 것이 아니라 **T01~T09의 실제 HTML/CSS/JavaScript 상호작용, Theme State, Keyboard, Recovery, WMS Mobile**을 빠르게 재현하는 것이다.
|
Backend/API 없이 브라우저에서 즉시 실행하는 KBX 업무 UI 원형이다. 목적은 별도 Demo UI를 만드는 것이 아니라 **T01~T09의 실제 HTML/CSS/JavaScript 상호작용, Theme State, Keyboard, Recovery, WMS Mobile**을 빠르게 재현하는 것이다.
|
||||||
|
|
||||||
|
|
||||||
|
## v53 Golden Screen Interaction QA
|
||||||
|
|
||||||
|
- Home module rail에 화면/확인필요/즐겨찾기/열린업무 Context를 함께 표시
|
||||||
|
- ArrowLeft/ArrowRight/Home/End로 module rail을 mouse 없이 탐색
|
||||||
|
- module 선택 후 Work Explorer로 scroll + focus landing
|
||||||
|
- Vue Home과 동일한 module attention pressure를 JavaScript Reference에서도 계산
|
||||||
|
- `:focus-visible`과 forced-colors active cue 유지
|
||||||
|
- Reference는 Domain Commit을 흉내 내지 않으며 Server/Domain truth 경계를 유지
|
||||||
|
- 정적 계약 증적: `scripts/validate-fe-golden-interaction-v53.mjs`
|
||||||
|
|
||||||
## 실행
|
## 실행
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-src 'none'; worker-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-src 'none'; worker-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" />
|
||||||
<meta name="referrer" content="no-referrer" />
|
<meta name="referrer" content="no-referrer" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<title>KBX FE Reference Lab v52 · Operational Navigation · Screen Anatomy · T01~T09</title>
|
<!-- FE Reference Lab v53 compatibility baseline retained for regression gates. -->
|
||||||
|
<!-- FE Reference Lab v54 compatibility baseline retained for regression gates. -->
|
||||||
|
<!-- FE Reference Lab v55 compatibility baseline retained for regression gates. -->
|
||||||
|
<title>KBX FE Reference Lab v60 · Canonical Status Contract · Runtime Evidence · Grid/Filter/Workflow Truth · T01~T09</title>
|
||||||
<link rel="stylesheet" href="./kbx-fe.css" />
|
<link rel="stylesheet" href="./kbx-fe.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -103,7 +103,7 @@ button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-vis
|
|||||||
|
|
||||||
/* v44 non-modal runtime centers: preserve workspace context while inspecting jobs/notifications. */
|
/* v44 non-modal runtime centers: preserve workspace context while inspecting jobs/notifications. */
|
||||||
.system-panel{position:fixed;z-index:82;right:8px;top:60px;width:min(420px,calc(100vw - 24px));max-height:70vh;overflow:auto;border:1px solid var(--border);border-radius:6px;background:var(--surface);box-shadow:var(--shadow)}
|
.system-panel{position:fixed;z-index:82;right:8px;top:60px;width:min(420px,calc(100vw - 24px));max-height:70vh;overflow:auto;border:1px solid var(--border);border-radius:6px;background:var(--surface);box-shadow:var(--shadow)}
|
||||||
.system-panel[hidden]{display:none}.system-panel>header{min-height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 8px;border-bottom:1px solid var(--border)}.system-panel>header button{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:var(--text)}.system-panel>header button:hover{background:var(--surface-hover)}.system-panel-body{padding:8px}
|
.system-panel[hidden]{display:none}.system-panel>header{min-height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 8px;border-bottom:1px solid var(--border)}.system-panel>header button{width:28px;height:28px;border:0;border-radius:4px;background:transparent;color:var(--text)}.system-panel>header button:hover{background:var(--muted)}.system-panel-body{padding:8px}
|
||||||
|
|
||||||
/* v45 safe screen preference: keep repetitive filters without persisting free-text business data. */
|
/* v45 safe screen preference: keep repetitive filters without persisting free-text business data. */
|
||||||
.search-preference{min-height:34px;display:grid;grid-template-columns:18px auto;align-items:center;column-gap:6px;margin-left:auto;padding:0 4px;color:var(--text-muted);font-size:12px}.search-preference input{width:16px;height:16px;margin:0}.search-preference span{color:var(--text);font-size:13px}.search-preference small{grid-column:2;max-width:320px;color:var(--text-muted)}
|
.search-preference{min-height:34px;display:grid;grid-template-columns:18px auto;align-items:center;column-gap:6px;margin-left:auto;padding:0 4px;color:var(--text-muted);font-size:12px}.search-preference input{width:16px;height:16px;margin:0}.search-preference span{color:var(--text);font-size:13px}.search-preference small{grid-column:2;max-width:320px;color:var(--text-muted)}
|
||||||
@@ -144,8 +144,43 @@ button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-vis
|
|||||||
.home-module-rail button:last-child{border-right:0}.home-module-rail button:hover{background:var(--muted)}
|
.home-module-rail button:last-child{border-right:0}.home-module-rail button:hover{background:var(--muted)}
|
||||||
.home-module-rail button b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem}.home-module-rail button small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums}
|
.home-module-rail button b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem}.home-module-rail button small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;color:var(--text-muted);font-variant-numeric:tabular-nums}
|
||||||
.home-module-rail button[aria-pressed="true"]{box-shadow:inset 0 -.125rem 0 var(--module-accent);background:var(--module-accent-surface);color:var(--module-accent)}
|
.home-module-rail button[aria-pressed="true"]{box-shadow:inset 0 -.125rem 0 var(--module-accent);background:var(--module-accent-surface);color:var(--module-accent)}
|
||||||
|
.home-module-rail button:focus-visible{outline:.125rem solid var(--module-accent);outline-offset:-.125rem;position:relative;z-index:1}
|
||||||
.home-module-rail button[data-module="OMS"],.home-module-rail button[data-module="ERP"],.home-module-rail button[data-module="WMS"],.home-module-rail button[data-module="COMMON"]{border-top:.125rem solid var(--module-accent)}
|
.home-module-rail button[data-module="OMS"],.home-module-rail button[data-module="ERP"],.home-module-rail button[data-module="WMS"],.home-module-rail button[data-module="COMMON"]{border-top:.125rem solid var(--module-accent)}
|
||||||
#homeExplorer:focus-visible{outline:.125rem solid var(--module-accent);outline-offset:var(--space-1)}
|
#homeExplorer:focus-visible{outline:.125rem solid var(--module-accent);outline-offset:var(--space-1)}
|
||||||
@media(max-width:64rem){.home-module-rail{grid-template-columns:repeat(2,minmax(0,1fr))}.home-module-label{grid-column:1 / -1;min-height:var(--control-h);border-right:0;border-bottom:1px solid var(--border)}.home-module-rail button{border-bottom:1px solid var(--border)}.home-module-rail button:nth-last-child(-n+2){border-bottom:0}}
|
@media(max-width:64rem){.home-module-rail{grid-template-columns:repeat(2,minmax(0,1fr))}.home-module-label{grid-column:1 / -1;min-height:var(--control-h);border-right:0;border-bottom:1px solid var(--border)}.home-module-rail button{border-bottom:1px solid var(--border)}.home-module-rail button:nth-last-child(-n+2){border-bottom:0}}
|
||||||
@media(max-width:40rem){.home-module-rail{grid-template-columns:1fr}.home-module-rail button{border-right:0}.home-module-rail button:nth-last-child(2){border-bottom:1px solid var(--border)}}
|
@media(max-width:40rem){.home-module-rail{grid-template-columns:1fr}.home-module-rail button{border-right:0}.home-module-rail button:nth-last-child(2){border-bottom:1px solid var(--border)}}
|
||||||
@media(forced-colors:active){.home-module-rail button[aria-pressed="true"]{outline:1px solid Highlight;outline-offset:-1px;box-shadow:none}}
|
@media(forced-colors:active){.home-module-rail button[aria-pressed="true"]{outline:1px solid Highlight;outline-offset:-1px;box-shadow:none}}
|
||||||
|
|
||||||
|
/* v54 — operational triage lanes + record wayfinding + denser work surfaces */
|
||||||
|
.attention-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--space-2);padding:var(--space-2)}
|
||||||
|
.attention-lane{min-width:0;border:1px solid var(--border);border-top:.125rem solid var(--border-strong);border-radius:var(--radius);background:var(--surface);overflow:hidden}
|
||||||
|
.attention-lane[data-tone="danger"]{border-top-color:var(--danger)}.attention-lane[data-tone="warning"]{border-top-color:var(--warning)}.attention-lane[data-tone="info"]{border-top-color:var(--primary)}
|
||||||
|
.attention-lane>header{min-height:var(--control-h);display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-1) var(--space-2);border-bottom:1px solid var(--border);background:var(--muted)}
|
||||||
|
.attention-lane>header>div{display:grid;min-width:0}.attention-lane>header strong{font-size:.8125rem}.attention-lane>header small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-muted);font-size:.75rem}.attention-lane>header>b{font-size:1rem}
|
||||||
|
.attention-lane-items{display:flex;flex-direction:column}.attention-lane .attention-item{min-height:var(--control-h);grid-template-columns:auto minmax(0,1fr) auto .875rem;border:0;border-bottom:1px solid var(--border);border-radius:0}.attention-lane .attention-item:last-child{border-bottom:0}.attention-lane .attention-item.critical{box-shadow:inset .125rem 0 0 var(--danger)}
|
||||||
|
.attention-lane-more{width:100%;min-height:1.75rem;border:0;border-top:1px solid var(--border);background:var(--muted);color:var(--text-muted);font-size:.75rem}.attention-lane-more:hover{background:var(--muted);color:var(--primary)}
|
||||||
|
.record-navigator{display:flex;align-items:center;gap:var(--space-1);margin-left:auto}.record-position{padding-right:var(--space-1);font-size:.75rem;color:var(--text-muted);white-space:nowrap}.record-position strong{color:var(--text)}.record-navigator button{min-height:1.75rem;padding:0 var(--space-2);border:1px solid var(--border);border-radius:var(--radius);background:var(--surface);color:var(--text);font-size:.75rem}.record-navigator button:hover:not(:disabled){border-color:var(--primary);background:var(--muted)}.record-navigator button:disabled{opacity:.45}.record-navigator button:focus-visible,.attention-lane-more:focus-visible{outline:.125rem solid var(--primary);outline-offset:-.125rem}
|
||||||
|
.detail-pane>.pane-header{gap:var(--space-3)}.detail-pane>.pane-header>div{display:grid;min-width:0}.detail-pane>.pane-header>div span{color:var(--text-muted);font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
.fast-entry-surface{min-height:0;flex:1;border:1px solid var(--border);border-top:.125rem solid var(--module-accent);background:var(--surface);overflow:auto}.fast-entry-surface>.pane-header>div{display:grid;min-width:0}.fast-entry-surface>.pane-header>div span{font-size:.75rem;color:var(--text-muted)}
|
||||||
|
@media(max-width:68.75rem){.attention-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||||
|
@media(max-width:40rem){.attention-grid{grid-template-columns:1fr}.record-navigator .record-position{display:none}}
|
||||||
|
@media(forced-colors:active){.attention-lane[data-tone],.fast-entry-surface{border-top-color:CanvasText}.record-navigator button:focus-visible,.attention-lane-more:focus-visible{outline-color:Highlight}}
|
||||||
|
|
||||||
|
/* v55 operational recovery surfaces */
|
||||||
|
.import-error-nav{display:flex;align-items:center;justify-content:flex-end;gap:.5rem;padding:.5rem .625rem;border:.0625rem solid var(--border);border-bottom:0;background:var(--muted);font-size:.75rem}.import-error-nav>span{margin-right:auto}.grid tbody tr[data-import-error-row][aria-current="true"]{outline:.125rem solid var(--primary);outline-offset:-.125rem}.scan-truth{display:grid;gap:.375rem;padding:.5rem .625rem;border:.0625rem solid var(--border);border-left:.25rem solid var(--success);background:var(--muted);font-size:.75rem}.scan-truth.blocked{border-left-color:var(--danger);background:var(--red-50)}.scan-truth>div{display:grid;grid-template-columns:auto minmax(0,1fr);gap:.5rem}.scan-truth span{color:var(--text-muted)}.scan-truth b{text-align:right;overflow-wrap:anywhere}
|
||||||
|
@media(forced-colors:active){.import-error-nav,.scan-truth{border-color:CanvasText}.grid tbody tr[data-import-error-row][aria-current="true"]{outline:.125rem solid Highlight}}
|
||||||
|
|
||||||
|
/* v56 interaction closure: import stage transitions must not drop keyboard focus. */
|
||||||
|
.import-stage-focus:focus-visible{outline:.125rem solid var(--primary);outline-offset:.125rem}
|
||||||
|
@media(forced-colors:active){.import-stage-focus:focus-visible{outline-color:Highlight}}
|
||||||
|
|
||||||
|
/* v59 status dictionary parity: status is text + semantic surface, never color-only. */
|
||||||
|
.business-status{--status-dot:var(--text-muted);--status-surface:var(--surface);--status-border:var(--border);display:inline-flex;align-items:center;gap:6px;min-height:24px;padding:0 8px;border:1px solid var(--status-border);border-radius:999px;background:var(--status-surface);font-size:12px;font-weight:600;white-space:nowrap}
|
||||||
|
.business-status__dot{width:6px;height:6px;border-radius:50%;background:var(--status-dot)}
|
||||||
|
.business-status[data-semantic="ready"],.business-status[data-semantic="pending"],.business-status[data-semantic="processing"]{--status-dot:var(--primary);--status-surface:var(--blue-50);--status-border:var(--blue-100)}
|
||||||
|
.business-status[data-semantic="completed"]{--status-dot:var(--success);--status-surface:var(--green-50);--status-border:var(--green-200)}
|
||||||
|
.business-status[data-semantic="warning"],.business-status[data-semantic="hold"]{--status-dot:var(--warning);--status-surface:var(--amber-50);--status-border:var(--amber-200)}
|
||||||
|
.business-status[data-semantic="error"]{--status-dot:var(--danger);--status-surface:var(--red-50);--status-border:var(--red-200)}
|
||||||
|
.business-status[data-semantic="disabled"]{color:var(--text-muted);background:var(--muted)}
|
||||||
|
.business-status[data-unknown="true"]{border-style:dashed}
|
||||||
|
@media(forced-colors:active){.business-status{forced-color-adjust:auto;border-color:CanvasText;background:Canvas}.business-status__dot{background:CanvasText}.business-status[data-unknown="true"]{border-color:Highlight}}
|
||||||
@@ -15,14 +15,14 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const orders = [
|
const orders = [
|
||||||
{no:'ORD-260811-1024',channel:'쿠팡',time:'16:42',buyer:'김민수',item:'런닝화 외 1',qty:3,amount:128000,stock:'정상',status:'출고대기',exception:''},
|
{no:'ORD-260811-1024',channel:'쿠팡',time:'16:42',buyer:'김민수',item:'런닝화 외 1',qty:3,amount:128000,stock:'정상',status:'READY',exception:''},
|
||||||
{no:'ORD-260811-1023',channel:'스마트스토어',time:'16:39',buyer:'이지은',item:'셔츠 BLACK',qty:1,amount:48000,stock:'⚠ 부족',status:'보류',exception:'재고부족'},
|
{no:'ORD-260811-1023',channel:'스마트스토어',time:'16:39',buyer:'이지은',item:'셔츠 BLACK',qty:1,amount:48000,stock:'⚠ 부족',status:'HOLD',exception:'재고부족'},
|
||||||
{no:'ORD-260811-1022',channel:'자사몰',time:'16:31',buyer:'박서준',item:'운동화 270',qty:2,amount:196000,stock:'정상',status:'할당완료',exception:''},
|
{no:'ORD-260811-1022',channel:'자사몰',time:'16:31',buyer:'박서준',item:'운동화 270',qty:2,amount:196000,stock:'정상',status:'CONFIRMED',exception:''},
|
||||||
{no:'ORD-260811-1021',channel:'쿠팡',time:'16:28',buyer:'최유진',item:'양말 3PACK',qty:4,amount:36000,stock:'정상',status:'출고대기',exception:''},
|
{no:'ORD-260811-1021',channel:'쿠팡',time:'16:28',buyer:'최유진',item:'양말 3PACK',qty:4,amount:36000,stock:'정상',status:'READY',exception:''},
|
||||||
{no:'ORD-260811-1018',channel:'11번가',time:'16:12',buyer:'정하늘',item:'후드티 GRAY',qty:1,amount:69000,stock:'정상',status:'오류',exception:'송장실패'},
|
{no:'ORD-260811-1018',channel:'11번가',time:'16:12',buyer:'정하늘',item:'후드티 GRAY',qty:1,amount:69000,stock:'정상',status:'ERROR',exception:'송장실패'},
|
||||||
{no:'ORD-260811-1014',channel:'쿠팡',time:'15:57',buyer:'한도윤',item:'운동화 BLACK',qty:2,amount:178000,stock:'정상',status:'출고대기',exception:''},
|
{no:'ORD-260811-1014',channel:'쿠팡',time:'15:57',buyer:'한도윤',item:'운동화 BLACK',qty:2,amount:178000,stock:'정상',status:'READY',exception:''},
|
||||||
{no:'ORD-260811-1007',channel:'자사몰',time:'15:43',buyer:'오서연',item:'스니커즈 WHITE',qty:1,amount:82000,stock:'정상',status:'신규',exception:''},
|
{no:'ORD-260811-1007',channel:'자사몰',time:'15:43',buyer:'오서연',item:'스니커즈 WHITE',qty:1,amount:82000,stock:'정상',status:'NEW',exception:''},
|
||||||
{no:'ORD-260811-1002',channel:'스마트스토어',time:'15:31',buyer:'윤지호',item:'양말 WHITE',qty:5,amount:45000,stock:'정상',status:'출고대기',exception:'주소확인'},
|
{no:'ORD-260811-1002',channel:'스마트스토어',time:'15:31',buyer:'윤지호',item:'양말 WHITE',qty:5,amount:45000,stock:'정상',status:'READY',exception:'주소확인'},
|
||||||
];
|
];
|
||||||
|
|
||||||
const masterItems = [
|
const masterItems = [
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
selectedOrders:new Set(), orderQuickFilter:'all', orderAdvanced:false, orderDataState:'ready', commandProblem:null, searchIndex:0,
|
selectedOrders:new Set(), orderQuickFilter:'all', orderAdvanced:false, orderDataState:'ready', commandProblem:null, searchIndex:0,
|
||||||
orderSearch:{channel:'all',status:'all',keyword:'',exception:'all'}, orderRememberSearch:false, orderSavedDefaults:null,
|
orderSearch:{channel:'all',status:'all',keyword:'',exception:'all'}, orderRememberSearch:false, orderSavedDefaults:null,
|
||||||
selectedMasterId:'ABC001', masterQuery:'', masterMode:'edit', masterDraft:null, masterErrors:{}, pendingMasterId:null,
|
selectedMasterId:'ABC001', masterQuery:'', masterMode:'edit', masterDraft:null, masterErrors:{}, pendingMasterId:null,
|
||||||
transactionStatus:'작성', transactionErrors:[], transactionHeader:{orderDate:'2026-08-11',channel:'직접주문',customerCode:'10001',customerName:'대한상사',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'홍길동',phone:'010-1234-5678',postal:'06236',address:'서울특별시 강남구 테헤란로'},
|
transactionStatus:'DRAFT', transactionErrors:[], transactionHeader:{orderDate:'2026-08-11',channel:'직접주문',customerCode:'10001',customerName:'대한상사',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'홍길동',phone:'010-1234-5678',postal:'06236',address:'서울특별시 강남구 테헤란로'},
|
||||||
transactionLines:[{code:'ABC001',name:'운동화',spec:'BLACK / 270',available:83,qty:2,price:98000,remark:''},{code:'XYZ003',name:'양말',spec:'WHITE',available:721,qty:3,price:9000,remark:''}],
|
transactionLines:[{code:'ABC001',name:'운동화',spec:'BLACK / 270',available:83,qty:2,price:98000,remark:''},{code:'XYZ003',name:'양말',spec:'WHITE',available:721,qty:3,price:9000,remark:''}],
|
||||||
inventoryItemId:'ABC001', inventorySearch:{warehouse:'all',keyword:''}, inventoryDataState:'ready',
|
inventoryItemId:'ABC001', inventorySearch:{warehouse:'all',keyword:''}, inventoryDataState:'ready',
|
||||||
queueFilter:'all', queueSearch:{worker:'',keyword:''}, queueDataState:'ready', selectedQueueRows:new Set(), queueActionResult:'',
|
queueFilter:'all', queueSearch:{worker:'',keyword:''}, queueDataState:'ready', selectedQueueRows:new Set(), queueActionResult:'',
|
||||||
mismatchOnly:true, reconcileSearch:{date:'2026-08-11',keyword:''}, reconcileDataState:'ready', selectedReconcileRows:new Set(), reconcileActionResult:'',
|
mismatchOnly:true, reconcileSearch:{date:'2026-08-11',keyword:''}, reconcileDataState:'ready', selectedReconcileRows:new Set(), reconcileActionResult:'',
|
||||||
importStep:1, importFileName:'', importFileSize:0, importFileType:'', importJobStatus:'idle', importProgress:0, importProcessed:0,
|
importStep:1, importFocusStep:0, importFileName:'', importFileSize:0, importFileType:'', importJobStatus:'idle', importProgress:0, importProcessed:0, importErrorIndex:0,
|
||||||
pickingQty:4, pickingRequired:6, scanBuffer:'', scanError:'', networkStatus:'online', pendingSync:0, lastScanValue:'', lastScanAt:0,
|
pickingQty:4, pickingRequired:6, scanBuffer:'', scanError:'', networkStatus:'online', pendingSync:0, lastScanValue:'', lastScanAt:0, lastConfirmedScanValue:'', lastConfirmedScanAt:0,
|
||||||
pendingCloseId:null, pendingNavigationId:null, pendingNavigationOptions:null, pendingUnsavedMode:null, workspaceDirtyDiscarded:0,
|
pendingCloseId:null, pendingNavigationId:null, pendingNavigationOptions:null, pendingUnsavedMode:null, workspaceDirtyDiscarded:0,
|
||||||
lookupEntity:'', lookupField:'', lookupIndex:0,
|
lookupEntity:'', lookupField:'', lookupIndex:0,
|
||||||
fastRows:[
|
fastRows:[
|
||||||
@@ -81,6 +81,33 @@
|
|||||||
const $ = (selector, root=document) => root.querySelector(selector);
|
const $ = (selector, root=document) => root.querySelector(selector);
|
||||||
const $$ = (selector, root=document) => [...root.querySelectorAll(selector)];
|
const $$ = (selector, root=document) => [...root.querySelectorAll(selector)];
|
||||||
const esc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
const esc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||||||
|
const orderStatusDefinitions={
|
||||||
|
NEW:{label:'신규',semantic:'pending'},DRAFT:{label:'작성',semantic:'draft'},CONFIRMED:{label:'확정',semantic:'pending'},ALLOCATED:{label:'할당',semantic:'processing'},PICKING:{label:'피킹',semantic:'processing'},CHECKED:{label:'검수',semantic:'processing'},SHIPPED:{label:'출고완료',semantic:'completed'},HOLD:{label:'보류',semantic:'hold'},ERROR:{label:'오류',semantic:'error'},READY:{label:'출고대기',semantic:'ready'}
|
||||||
|
};
|
||||||
|
const shipmentStatusDefinitions={...orderStatusDefinitions,CONFIRMED:{label:'출고지시',semantic:'processing'}};
|
||||||
|
const legacyStatusDefinitions={
|
||||||
|
'정상':{label:'정상',semantic:'ready'},'사용':{label:'사용',semantic:'completed'},'할당완료':{label:'할당완료',semantic:'completed'},'출고완료':{label:'출고완료',semantic:'completed'},'완료':{label:'완료',semantic:'completed'},
|
||||||
|
'신규':{label:'신규',semantic:'pending'},'출고대기':{label:'출고대기',semantic:'ready'},'미처리':{label:'미처리',semantic:'pending'},'확인필요':{label:'확인필요',semantic:'warning'},'진행':{label:'진행',semantic:'processing'},
|
||||||
|
'보류':{label:'보류',semantic:'hold'},'부족':{label:'부족',semantic:'warning'},'재고부족':{label:'재고부족',semantic:'warning'},'지연':{label:'지연',semantic:'error'},'오류':{label:'오류',semantic:'error'},'사용중지':{label:'사용중지',semantic:'disabled'}
|
||||||
|
};
|
||||||
|
function resolveStatus(value,context='generic'){
|
||||||
|
const raw=String(value??'').replace('⚠ ','');
|
||||||
|
const dictionary=context==='shipment'?shipmentStatusDefinitions:context==='lifecycle'?orderStatusDefinitions:null;
|
||||||
|
const definition=dictionary?.[raw]||orderStatusDefinitions[raw]||legacyStatusDefinitions[raw];
|
||||||
|
return definition?{...definition,raw,unknown:false}:{label:`정의되지 않음 · ${raw||'빈 값'}`,semantic:'warning',raw,unknown:true};
|
||||||
|
}
|
||||||
|
function statusSemantic(value,context='generic'){return resolveStatus(value,context).semantic;}
|
||||||
|
function statusLabel(value,context='generic'){return resolveStatus(value,context).label;}
|
||||||
|
function statusChip(value,context='generic'){
|
||||||
|
const resolved=resolveStatus(value,context);
|
||||||
|
return `<span class="business-status" data-semantic="${resolved.semantic}" ${resolved.unknown?'data-unknown="true"':''}><span class="business-status__dot" aria-hidden="true"></span>${esc(resolved.label)}</span>`;
|
||||||
|
}
|
||||||
|
function canonicalOrderStatus(value){
|
||||||
|
const raw=String(value??'all');
|
||||||
|
const legacy={신규:'NEW',출고대기:'READY',할당완료:'CONFIRMED',보류:'HOLD',오류:'ERROR',출고완료:'SHIPPED'};
|
||||||
|
const normalized=legacy[raw]||raw;
|
||||||
|
return ['all','NEW','READY','CONFIRMED','HOLD','ERROR','SHIPPED'].includes(normalized)?normalized:'all';
|
||||||
|
}
|
||||||
const knownIds = new Set(screens.map(x=>x.id));
|
const knownIds = new Set(screens.map(x=>x.id));
|
||||||
const app=$('#app'), workspace=$('#workspace'), sideNav=$('#sideNav'), tabs=$('#workspaceTabs');
|
const app=$('#app'), workspace=$('#workspace'), sideNav=$('#sideNav'), tabs=$('#workspaceTabs');
|
||||||
const searchDialog=$('#menuSearchDialog'), searchInput=$('#menuSearchInput'), searchResults=$('#menuSearchResults');
|
const searchDialog=$('#menuSearchDialog'), searchInput=$('#menuSearchInput'), searchResults=$('#menuSearchResults');
|
||||||
@@ -136,7 +163,7 @@
|
|||||||
if(typeof value.orderRememberSearch==='boolean')state.orderRememberSearch=value.orderRememberSearch;
|
if(typeof value.orderRememberSearch==='boolean')state.orderRememberSearch=value.orderRememberSearch;
|
||||||
if(value.orderSavedDefaults&&typeof value.orderSavedDefaults==='object'){
|
if(value.orderSavedDefaults&&typeof value.orderSavedDefaults==='object'){
|
||||||
const d=value.orderSavedDefaults;
|
const d=value.orderSavedDefaults;
|
||||||
const safe={channel:['all','쿠팡','스마트스토어','자사몰','11번가'].includes(d.channel)?d.channel:'all',status:['all','신규','출고대기','할당완료','보류','오류'].includes(d.status)?d.status:'all',exception:['all','재고부족','주소확인','송장실패'].includes(d.exception)?d.exception:'all'};
|
const safe={channel:['all','쿠팡','스마트스토어','자사몰','11번가'].includes(d.channel)?d.channel:'all',status:canonicalOrderStatus(d.status),exception:['all','재고부족','주소확인','송장실패'].includes(d.exception)?d.exception:'all'};
|
||||||
state.orderSavedDefaults=safe;state.orderSearch={...state.orderSearch,...safe,keyword:''};
|
state.orderSavedDefaults=safe;state.orderSearch={...state.orderSearch,...safe,keyword:''};
|
||||||
}
|
}
|
||||||
}catch{}
|
}catch{}
|
||||||
@@ -329,30 +356,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function attention(source,title,detail,id,critical=false,intent=''){return `<button type="button" class="attention-item ${critical?'critical':''}" data-screen="${esc(id)}" ${intent?`data-intent="${esc(intent)}"`:''}><span class="attention-source">${esc(source)}</span><span class="attention-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">열기</span><span aria-hidden="true">›</span></button>`;}
|
function attention(source,title,detail,id,critical=false,intent=''){return `<button type="button" class="attention-item ${critical?'critical':''}" data-screen="${esc(id)}" ${intent?`data-intent="${esc(intent)}"`:''}><span class="attention-source">${esc(source)}</span><span class="attention-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">열기</span><span aria-hidden="true">›</span></button>`;}
|
||||||
|
function attentionTriageGroups(items,expanded){
|
||||||
|
const perGroup=expanded?6:2;
|
||||||
|
const groups=[
|
||||||
|
{key:'urgent',label:'긴급·예외',description:'실패와 중요 알림부터 처리',tone:'danger',match:item=>item.critical},
|
||||||
|
{key:'draft',label:'미저장',description:'중단한 편집을 이어서 처리',tone:'warning',match:item=>item.source==='미저장'},
|
||||||
|
{key:'running',label:'진행 작업',description:'대량·연계 작업 상태 확인',tone:'info',match:item=>item.source==='진행 작업'},
|
||||||
|
{key:'notice',label:'알림',description:'업무 후속 확인 항목',tone:'default',match:item=>item.source==='알림'},
|
||||||
|
];
|
||||||
|
return groups.map(group=>{const all=items.filter(group.match);return {...group,count:all.length,items:all.slice(0,perGroup)}}).filter(group=>group.count);
|
||||||
|
}
|
||||||
|
function attentionLane(group){return `<section class="attention-lane" data-tone="${esc(group.tone)}"><header><div><strong>${esc(group.label)}</strong><small>${esc(group.description)}</small></div><b>${group.count}</b></header><div class="attention-lane-items">${group.items.map(item=>attention(item.source,item.title,item.detail,item.id,item.critical,item.intent)).join('')}</div>${group.count>group.items.length?`<button type="button" class="attention-lane-more" data-attention-lane="${esc(group.key)}">나머지 ${group.count-group.items.length}개 보기</button>`:''}</section>`;}
|
||||||
function continueItem(source,title,detail,id,type){return `<button type="button" class="continue-item" data-screen="${esc(id)}" data-source="${esc(type)}"><span class="continue-source">${esc(source)}</span><span class="continue-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">${type==='dirty'?'이어서':'열기'}</span><span aria-hidden="true">›</span></button>`;}
|
function continueItem(source,title,detail,id,type){return `<button type="button" class="continue-item" data-screen="${esc(id)}" data-source="${esc(type)}"><span class="continue-source">${esc(source)}</span><span class="continue-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">${type==='dirty'?'이어서':'열기'}</span><span aria-hidden="true">›</span></button>`;}
|
||||||
function homeMenuRow(item){return `<div class="home-menu-row" data-screen="${esc(item.id)}"><button class="open-screen" type="button"><span>${esc(item.title)}</span><small>${esc(item.template)}</small></button><button class="fav ${isFav(item.id)?'on':''}" type="button" aria-label="${isFav(item.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(item.id)}">${isFav(item.id)?'★':'☆'}</button></div>`;}
|
function homeMenuRow(item){return `<div class="home-menu-row" data-screen="${esc(item.id)}"><button class="open-screen" type="button"><span>${esc(item.title)}</span><small>${esc(item.template)}</small></button><button class="fav ${isFav(item.id)?'on':''}" type="button" aria-label="${isFav(item.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(item.id)}">${isFav(item.id)?'★':'☆'}</button></div>`;}
|
||||||
function renderHome(){
|
function renderHome(){
|
||||||
const allScreens=screens.filter(x=>x.id!=='HOME');const visible=allScreens.filter(x=>state.module==='ALL'||x.module===state.module);const grouped=groupScreens(visible);
|
const allScreens=screens.filter(x=>x.id!=='HOME');const visible=allScreens.filter(x=>state.module==='ALL'||x.module===state.module);const grouped=groupScreens(visible);
|
||||||
const moduleKeys=['OMS','ERP','WMS','COMMON'].filter(module=>allScreens.some(item=>item.module===module));
|
const moduleKeys=['OMS','ERP','WMS','COMMON'].filter(module=>allScreens.some(item=>item.module===module));
|
||||||
const moduleRail=`<nav class="home-module-rail" aria-label="업무 영역 바로가기"><span class="home-module-label">업무영역</span><button type="button" data-home-module="ALL" aria-pressed="${state.module==='ALL'}"><b>전체</b><small>${allScreens.length}개 화면</small></button>${moduleKeys.map(module=>{const items=allScreens.filter(item=>item.module===module);const favoriteCount=items.filter(item=>isFav(item.id)).length;const openCount=state.tabs.filter(tab=>screen(tab.id).module===module).length;return `<button type="button" data-home-module="${esc(module)}" data-module="${esc(module)}" aria-pressed="${state.module===module}"><b>${esc(labelModule(module))}</b><small>${items.length}개 · ★ ${favoriteCount} · 열림 ${openCount}</small></button>`;}).join('')}</nav>`;
|
const allAttention=homeAttentionItems();
|
||||||
|
const moduleAttentionCount=module=>allAttention.filter(item=>screen(item.id).module===module).length;
|
||||||
|
const moduleRail=`<nav class="home-module-rail" aria-label="업무 영역 바로가기"><span class="home-module-label">업무영역</span><button type="button" data-home-module="ALL" aria-pressed="${state.module==='ALL'}"><b>전체</b><small>${allScreens.length}개 화면 · 확인 ${allAttention.length}</small></button>${moduleKeys.map(module=>{const items=allScreens.filter(item=>item.module===module);const favoriteCount=items.filter(item=>isFav(item.id)).length;const openCount=state.tabs.filter(tab=>screen(tab.id).module===module).length;return `<button type="button" data-home-module="${esc(module)}" data-module="${esc(module)}" aria-pressed="${state.module===module}"><b>${esc(labelModule(module))}</b><small>${items.length}개 · 확인 ${moduleAttentionCount(module)} · ★ ${favoriteCount} · 열림 ${openCount}</small></button>`;}).join('')}</nav>`;
|
||||||
const dirtyTab=state.tabs.find(t=>t.dirty);const resumedTab=state.tabs.find(t=>t.resumed&&!t.dirty);const regularOpen=state.tabs.find(t=>!t.dirty&&!t.resumed);
|
const dirtyTab=state.tabs.find(t=>t.dirty);const resumedTab=state.tabs.find(t=>t.resumed&&!t.dirty);const regularOpen=state.tabs.find(t=>!t.dirty&&!t.resumed);
|
||||||
const allAttention=homeAttentionItems();const attentionCap=24;const visibleAttention=state.homeAttentionExpanded?allAttention.slice(0,attentionCap):allAttention.slice(0,8);const attentionOverflow=Math.max(allAttention.length-visibleAttention.length,0);const attentionToggleLabel=state.homeAttentionExpanded?'상위 8개만 보기':allAttention.length<=attentionCap?`전체 ${allAttention.length}개 보기`:`우선 ${attentionCap}개 보기`;const attentionCenters=state.homeAttentionExpanded&&attentionOverflow?`<div class="attention-centers" aria-label="나머지 확인 항목"><button id="homeJobsAll" class="section-toggle" type="button">작업 센터 전체</button><button id="homeNoticesAll" class="section-toggle" type="button">알림 센터 전체</button></div>`:'';
|
const attentionCap=24;const legacyCollapsedAttention=allAttention.slice(0,8);const triageGroups=attentionTriageGroups(allAttention,state.homeAttentionExpanded);const visibleAttention=triageGroups.flatMap(group=>group.items).slice(0,attentionCap);const attentionOverflow=Math.max(allAttention.length-visibleAttention.length,0);const attentionToggleLabel=state.homeAttentionExpanded?'상위 8개만 보기':allAttention.length<=attentionCap?`전체 ${allAttention.length}개 보기`:`우선 ${attentionCap}개 보기`;const attentionCenters=state.homeAttentionExpanded&&attentionOverflow?`<div class="attention-centers" aria-label="나머지 확인 항목"><button id="homeJobsAll" class="section-toggle" type="button">작업 센터 전체</button><button id="homeNoticesAll" class="section-toggle" type="button">알림 센터 전체</button></div>`:'';
|
||||||
workspace.innerHTML=`<section class="home-page"><header class="home-heading"><div><p>KBX 업무 시작</p><h1>홈</h1><small>검색하고, 이어서 처리하고, 예외만 확인하는 한국형 업무 시작점${state.workspaceDirtyDiscarded?` · 이전 미저장 ${state.workspaceDirtyDiscarded}개는 안전상 복원 제외`:''}</small></div><button class="home-search" id="homeSearch" type="button" aria-keyshortcuts="Control+K"><span>업무 화면 검색</span><kbd>Ctrl K</kbd></button></header>
|
workspace.innerHTML=`<section class="home-page"><header class="home-heading"><div><p>KBX 업무 시작</p><h1>홈</h1><small>검색하고, 이어서 처리하고, 예외만 확인하는 한국형 업무 시작점${state.workspaceDirtyDiscarded?` · 이전 미저장 ${state.workspaceDirtyDiscarded}개는 안전상 복원 제외`:''}</small></div><button class="home-search" id="homeSearch" type="button" aria-keyshortcuts="Control+K"><span>업무 화면 검색</span><kbd>Ctrl K</kbd></button></header>
|
||||||
<nav class="home-pulse" aria-label="업무 시작 상태"><button id="homePulseScreens" type="button"><span>사용 가능 화면</span><b>${allScreens.length}</b><small>검색</small></button><button id="homePulseDirty" type="button" ${dirtyTab?'':'disabled'}><span>미저장 업무</span><b>${state.tabs.filter(tab=>tab.dirty).length}</b><small>${dirtyTab?'이어서':'없음'}</small></button><button id="homePulseJobs" type="button"><span>진행 작업</span><b>2</b><small>작업 센터</small></button><button id="homePulseNotices" type="button"><span>새 알림</span><b>4</b><small>알림 센터</small></button></nav>
|
<nav class="home-pulse" aria-label="업무 시작 상태"><button id="homePulseScreens" type="button"><span>사용 가능 화면</span><b>${allScreens.length}</b><small>검색</small></button><button id="homePulseDirty" type="button" ${dirtyTab?'':'disabled'}><span>미저장 업무</span><b>${state.tabs.filter(tab=>tab.dirty).length}</b><small>${dirtyTab?'이어서':'없음'}</small></button><button id="homePulseJobs" type="button"><span>진행 작업</span><b>2</b><small>작업 센터</small></button><button id="homePulseNotices" type="button"><span>새 알림</span><b>4</b><small>알림 센터</small></button></nav>
|
||||||
${moduleRail}
|
${moduleRail}
|
||||||
<section class="attention-zone"><div class="section-head"><div><h2>확인 필요</h2><small>정상 8,224건은 자동 처리 · 사용자는 예외와 실패만 확인</small></div><div class="section-actions"><span class="section-count">전체 ${allAttention.length}개 · ${visibleAttention.length}개 표시</span><button id="homeAttentionToggle" class="section-toggle" type="button" aria-controls="homeAttentionGrid" aria-expanded="${state.homeAttentionExpanded}">${attentionToggleLabel}</button>${attentionCenters}</div></div><div id="homeAttentionGrid" class="attention-grid">${visibleAttention.map(item=>attention(item.source,item.title,item.detail,item.id,item.critical,item.intent)).join('')}</div></section>
|
<section class="attention-zone"><div class="section-head"><div><h2>확인 필요</h2><small>정상 8,224건은 자동 처리 · 사용자는 예외와 실패만 확인</small></div><div class="section-actions"><span class="section-count">전체 ${allAttention.length}개 · ${visibleAttention.length}개 표시</span><button id="homeAttentionToggle" class="section-toggle" type="button" aria-controls="homeAttentionGrid" aria-expanded="${state.homeAttentionExpanded}">${attentionToggleLabel}</button>${attentionCenters}</div></div><div id="homeAttentionGrid" class="attention-grid">${triageGroups.map(attentionLane).join('')}</div></section>
|
||||||
<section class="home-section"><div class="section-head"><div><h2>바로 시작</h2><small>미저장 → 열린 업무 → 즐겨찾기 → 최근 순으로 중복 없이 연결</small></div><span class="section-count">업무 문맥 유지</span></div><div class="continue-list">${continueItem('미저장',dirtyTab?.title||'주문등록',dirtyTab?`${screen(dirtyTab.id).module} · 저장하지 않은 변경`:'OMS · 주문 · ORD 임시작성',dirtyTab?.id||'OMS-ORD-002','dirty')}${resumedTab?continueItem('복원',resumedTab.title,`${screen(resumedTab.id).module} · 이전 세션의 업무 위치`,resumedTab.id,'open'):continueItem('열림',regularOpen?.title||'품목관리',regularOpen?`${screen(regularOpen.id).module} · 열린 업무`:'ERP · 기준정보',regularOpen?.id||'ERP-MST-ITEM-001','open')}${continueItem('즐겨찾기','재고현황','ERP · 재고','ERP-INV-001','favorite')}${continueItem('최근','오늘 물류작업','WMS · 작업관리','WMS-WORK-001','recent')}</div></section>
|
<section class="home-section"><div class="section-head"><div><h2>바로 시작</h2><small>미저장 → 열린 업무 → 즐겨찾기 → 최근 순으로 중복 없이 연결</small></div><span class="section-count">업무 문맥 유지</span></div><div class="continue-list">${continueItem('미저장',dirtyTab?.title||'주문등록',dirtyTab?`${screen(dirtyTab.id).module} · 저장하지 않은 변경`:'OMS · 주문 · ORD 임시작성',dirtyTab?.id||'OMS-ORD-002','dirty')}${resumedTab?continueItem('복원',resumedTab.title,`${screen(resumedTab.id).module} · 이전 세션의 업무 위치`,resumedTab.id,'open'):continueItem('열림',regularOpen?.title||'품목관리',regularOpen?`${screen(regularOpen.id).module} · 열린 업무`:'ERP · 기준정보',regularOpen?.id||'ERP-MST-ITEM-001','open')}${continueItem('즐겨찾기','재고현황','ERP · 재고','ERP-INV-001','favorite')}${continueItem('최근','오늘 물류작업','WMS · 작업관리','WMS-WORK-001','recent')}</div></section>
|
||||||
<section id="homeExplorer" class="home-section" tabindex="-1"><div class="section-head"><div><h2>${state.module==='ALL'?'전체 업무':`${esc(labelModule(state.module))} 업무`}</h2><small>신규 화면은 빈 Canvas가 아니라 T01~T09 원형에서 시작 · 검증된 업무 문법을 재사용</small></div><div class="section-actions">${state.module!=='ALL'?'<button id="homeShowAll" class="section-toggle" type="button">전체 업무 보기</button>':''}<span class="section-count">${visible.length}개 화면</span></div></div><div class="work-explorer">${[...grouped.entries()].map(([module,groups])=>`<section class="module-column" data-module="${esc(module)}"><div class="module-title"><strong>${esc(labelModule(module))}</strong><span>${module==='OMS'?'주문·채널':module==='ERP'?'기준정보·구매·재고':module==='WMS'?'물류 작업':'공통 운영'}</span><small>${[...groups.values()].flat().length}개</small></div>${[...groups.entries()].map(([group,items])=>`<div class="group-block"><h3>${esc(group)}</h3>${items.map(homeMenuRow).join('')}</div>`).join('')}</section>`).join('')}</div></section>
|
<section id="homeExplorer" class="home-section" tabindex="-1"><div class="section-head"><div><h2>${state.module==='ALL'?'전체 업무':`${esc(labelModule(state.module))} 업무`}</h2><small>신규 화면은 빈 Canvas가 아니라 T01~T09 원형에서 시작 · 검증된 업무 문법을 재사용</small></div><div class="section-actions">${state.module!=='ALL'?'<button id="homeShowAll" class="section-toggle" type="button">전체 업무 보기</button>':''}<span class="section-count">${visible.length}개 화면</span></div></div><div class="work-explorer">${[...grouped.entries()].map(([module,groups])=>`<section class="module-column" data-module="${esc(module)}"><div class="module-title"><strong>${esc(labelModule(module))}</strong><span>${module==='OMS'?'주문·채널':module==='ERP'?'기준정보·구매·재고':module==='WMS'?'물류 작업':'공통 운영'}</span><small>${[...groups.values()].flat().length}개</small></div>${[...groups.entries()].map(([group,items])=>`<div class="group-block"><h3>${esc(group)}</h3>${items.map(homeMenuRow).join('')}</div>`).join('')}</section>`).join('')}</div></section>
|
||||||
</section>`;
|
</section>`;
|
||||||
$('#homeSearch').onclick=openSearch;$('#homePulseScreens').onclick=openSearch;const pulseDirty=$('#homePulseDirty');if(pulseDirty&&!pulseDirty.disabled)pulseDirty.onclick=()=>openScreen(dirtyTab.id);$('#homePulseJobs').onclick=()=>showSystemPanel('jobs');$('#homePulseNotices').onclick=()=>showSystemPanel('notices');$$('[data-home-module]',workspace).forEach(button=>button.onclick=()=>{state.module=button.dataset.homeModule;$('#moduleSelect').value=state.module;renderNavigation();renderHome();setTimeout(()=>$('#homeExplorer')?.scrollIntoView({block:'start',behavior:'smooth'}),0);});const showAll=$('#homeShowAll');if(showAll)showAll.onclick=()=>{state.module='ALL';$('#moduleSelect').value='ALL';renderNavigation();renderHome();setTimeout(()=>$('#homeExplorer')?.focus({preventScroll:true}),0);};const attentionToggle=$('#homeAttentionToggle');if(attentionToggle)attentionToggle.onclick=()=>{state.homeAttentionExpanded=!state.homeAttentionExpanded;renderHome();};const jobsAll=$('#homeJobsAll');if(jobsAll)jobsAll.onclick=()=>showSystemPanel('jobs');const noticesAll=$('#homeNoticesAll');if(noticesAll)noticesAll.onclick=()=>showSystemPanel('notices');$$('.attention-item',workspace).forEach(b=>b.onclick=()=>b.dataset.intent?applyIntent(b.dataset.intent):openScreen(b.dataset.screen));$$('.continue-item',workspace).forEach(b=>b.onclick=()=>openScreen(b.dataset.screen));$$('.home-menu-row',workspace).forEach(row=>{$('.open-screen',row).onclick=()=>openScreen(row.dataset.screen);$('.fav',row).onclick=()=>toggleFavorite(row.dataset.screen);});
|
$('#homeSearch').onclick=openSearch;$('#homePulseScreens').onclick=openSearch;const pulseDirty=$('#homePulseDirty');if(pulseDirty&&!pulseDirty.disabled)pulseDirty.onclick=()=>openScreen(dirtyTab.id);$('#homePulseJobs').onclick=()=>showSystemPanel('jobs');$('#homePulseNotices').onclick=()=>showSystemPanel('notices');const railButtons=$$('[data-home-module]',workspace);railButtons.forEach((button,index)=>{button.onclick=()=>{state.module=button.dataset.homeModule;$('#moduleSelect').value=state.module;renderNavigation();renderHome();setTimeout(()=>{$('#homeExplorer')?.scrollIntoView({block:'start',behavior:'smooth'});$('#homeExplorer')?.focus({preventScroll:true});},0);};button.onkeydown=e=>{let next=index;if(e.key==='ArrowRight')next=(index+1)%railButtons.length;else if(e.key==='ArrowLeft')next=(index-1+railButtons.length)%railButtons.length;else if(e.key==='Home')next=0;else if(e.key==='End')next=railButtons.length-1;else return;e.preventDefault();railButtons[next]?.focus();};});const showAll=$('#homeShowAll');if(showAll)showAll.onclick=()=>{state.module='ALL';$('#moduleSelect').value='ALL';renderNavigation();renderHome();setTimeout(()=>$('#homeExplorer')?.focus({preventScroll:true}),0);};const attentionToggle=$('#homeAttentionToggle');if(attentionToggle)attentionToggle.onclick=()=>{state.homeAttentionExpanded=!state.homeAttentionExpanded;renderHome();};const jobsAll=$('#homeJobsAll');if(jobsAll)jobsAll.onclick=()=>showSystemPanel('jobs');const noticesAll=$('#homeNoticesAll');if(noticesAll)noticesAll.onclick=()=>showSystemPanel('notices');$$('.attention-item',workspace).forEach(b=>b.onclick=()=>b.dataset.intent?applyIntent(b.dataset.intent):openScreen(b.dataset.screen));$$('.attention-lane-more',workspace).forEach(button=>button.onclick=()=>{if(button.dataset.attentionLane==='running')showSystemPanel('jobs');else if(button.dataset.attentionLane==='notice')showSystemPanel('notices');else{state.homeAttentionExpanded=true;renderHome();}});$$('.continue-item',workspace).forEach(b=>b.onclick=()=>openScreen(b.dataset.screen));$$('.home-menu-row',workspace).forEach(row=>{$('.open-screen',row).onclick=()=>openScreen(row.dataset.screen);$('.fav',row).onclick=()=>toggleFavorite(row.dataset.screen);});
|
||||||
}
|
}
|
||||||
|
|
||||||
function filteredOrders(){
|
function filteredOrders(){
|
||||||
let visible=orders;
|
let visible=orders;
|
||||||
if(state.orderQuickFilter==='new')visible=visible.filter(x=>x.status==='신규');
|
if(state.orderQuickFilter==='new')visible=visible.filter(x=>x.status==='NEW');
|
||||||
else if(state.orderQuickFilter==='ready')visible=visible.filter(x=>x.status==='출고대기');
|
else if(state.orderQuickFilter==='ready')visible=visible.filter(x=>x.status==='READY');
|
||||||
else if(state.orderQuickFilter==='stock')visible=visible.filter(x=>x.stock.includes('부족'));
|
else if(state.orderQuickFilter==='stock')visible=visible.filter(x=>x.stock.includes('부족'));
|
||||||
else if(state.orderQuickFilter==='error')visible=visible.filter(x=>x.exception||x.status==='오류');
|
else if(state.orderQuickFilter==='error')visible=visible.filter(x=>x.exception||x.status==='ERROR');
|
||||||
if(state.orderSearch.channel!=='all')visible=visible.filter(x=>x.channel===state.orderSearch.channel);
|
if(state.orderSearch.channel!=='all')visible=visible.filter(x=>x.channel===state.orderSearch.channel);
|
||||||
if(state.orderSearch.status!=='all')visible=visible.filter(x=>x.status===state.orderSearch.status);
|
if(state.orderSearch.status!=='all')visible=visible.filter(x=>x.status===state.orderSearch.status);
|
||||||
if(state.orderSearch.exception!=='all')visible=visible.filter(x=>x.exception===state.orderSearch.exception || (state.orderSearch.exception==='재고부족'&&x.stock.includes('부족')));
|
if(state.orderSearch.exception!=='all')visible=visible.filter(x=>x.exception===state.orderSearch.exception || (state.orderSearch.exception==='재고부족'&&x.stock.includes('부족')));
|
||||||
@@ -371,12 +411,12 @@
|
|||||||
function clearProblem(){state.commandProblem=null;}
|
function clearProblem(){state.commandProblem=null;}
|
||||||
|
|
||||||
function activeOrderSearchCount(){return [state.orderSearch.channel!=='all',state.orderSearch.status!=='all',Boolean(state.orderSearch.keyword.trim()),state.orderSearch.exception!=='all'].filter(Boolean).length;}
|
function activeOrderSearchCount(){return [state.orderSearch.channel!=='all',state.orderSearch.status!=='all',Boolean(state.orderSearch.keyword.trim()),state.orderSearch.exception!=='all'].filter(Boolean).length;}
|
||||||
function safeOrderDefaults(){return {channel:['all','쿠팡','스마트스토어','자사몰','11번가'].includes(state.orderSearch.channel)?state.orderSearch.channel:'all',status:['all','신규','출고대기','할당완료','보류','오류'].includes(state.orderSearch.status)?state.orderSearch.status:'all',exception:['all','재고부족','주소확인','송장실패'].includes(state.orderSearch.exception)?state.orderSearch.exception:'all'};}
|
function safeOrderDefaults(){return {channel:['all','쿠팡','스마트스토어','자사몰','11번가'].includes(state.orderSearch.channel)?state.orderSearch.channel:'all',status:canonicalOrderStatus(state.orderSearch.status),exception:['all','재고부족','주소확인','송장실패'].includes(state.orderSearch.exception)?state.orderSearch.exception:'all'};}
|
||||||
function saveOrderDefaults(message){state.orderSavedDefaults=safeOrderDefaults();persist();toast(message||'현재 조회조건을 기본조건으로 저장했습니다. 자유검색어는 저장하지 않습니다.');}
|
function saveOrderDefaults(message){state.orderSavedDefaults=safeOrderDefaults();persist();toast(message||'현재 조회조건을 기본조건으로 저장했습니다. 자유검색어는 저장하지 않습니다.');}
|
||||||
function orderSearchPanel(){
|
function orderSearchPanel(){
|
||||||
const advanced=state.orderAdvanced?`<div class="search-advanced"><label class="field-inline"><span>택배사</span><select class="control" disabled title="Reference Lab: 택배사 데이터셋 미포함"><option>전체</option></select></label><label class="field-inline"><span>창고</span><select class="control" disabled title="Reference Lab: 창고 검색 데이터셋 미포함"><option>전체</option></select></label><label class="field-inline"><span>오류유형</span><select id="orderException" class="control">${searchOption('all','전체',state.orderSearch.exception)}${searchOption('재고부족','재고부족',state.orderSearch.exception)}${searchOption('주소확인','주소확인',state.orderSearch.exception)}${searchOption('송장실패','송장실패',state.orderSearch.exception)}</select></label><label class="search-preference"><input id="rememberOrderSearch" type="checkbox" ${state.orderRememberSearch?'checked':''}><span>마지막 조회조건 기억</span><small>주문자·주문번호·상품 자유검색어는 저장하지 않음</small></label></div>`:'';
|
const advanced=state.orderAdvanced?`<div class="search-advanced"><label class="field-inline"><span>택배사</span><select class="control" disabled title="Reference Lab: 택배사 데이터셋 미포함"><option>전체</option></select></label><label class="field-inline"><span>창고</span><select class="control" disabled title="Reference Lab: 창고 검색 데이터셋 미포함"><option>전체</option></select></label><label class="field-inline"><span>오류유형</span><select id="orderException" class="control">${searchOption('all','전체',state.orderSearch.exception)}${searchOption('재고부족','재고부족',state.orderSearch.exception)}${searchOption('주소확인','주소확인',state.orderSearch.exception)}${searchOption('송장실패','송장실패',state.orderSearch.exception)}</select></label><label class="search-preference"><input id="rememberOrderSearch" type="checkbox" ${state.orderRememberSearch?'checked':''}><span>마지막 조회조건 기억</span><small>주문자·주문번호·상품 자유검색어는 저장하지 않음</small></label></div>`:'';
|
||||||
const activeCount=activeOrderSearchCount();
|
const activeCount=activeOrderSearchCount();
|
||||||
return `<form class="search-panel ${state.orderAdvanced?'expanded':''}" id="orderSearchForm"><div class="search-fields"><label class="field-inline"><span>기간</span><input id="orderPeriod" class="control medium" value="2026-08-11 ~ 2026-08-11" readonly aria-label="주문기간"></label><label class="field-inline"><span>판매채널</span><select id="orderChannel" class="control">${searchOption('all','전체',state.orderSearch.channel)}${searchOption('쿠팡','쿠팡',state.orderSearch.channel)}${searchOption('스마트스토어','스마트스토어',state.orderSearch.channel)}${searchOption('자사몰','자사몰',state.orderSearch.channel)}${searchOption('11번가','11번가',state.orderSearch.channel)}</select></label><label class="field-inline"><span>상태</span><select id="orderStatus" class="control">${searchOption('all','전체',state.orderSearch.status)}${searchOption('신규','신규',state.orderSearch.status)}${searchOption('출고대기','출고대기',state.orderSearch.status)}${searchOption('할당완료','할당완료',state.orderSearch.status)}${searchOption('보류','보류',state.orderSearch.status)}${searchOption('오류','오류',state.orderSearch.status)}</select></label><label class="field-inline"><span>통합검색</span><input id="orderKeyword" class="control wide" value="${esc(state.orderSearch.keyword)}" placeholder="주문번호/주문자/상품" maxlength="80"></label></div><div class="search-actions">${btn('조회','primary','searchExecute')}${btn(state.orderAdvanced?`상세조건 ${activeCount?`(${activeCount})`:''} 닫기`:`상세조건${activeCount?` ${activeCount}`:''}`,'','toggleAdvanced')}${btn('초기화','','searchReset')}${btn('기본조건 저장','','saveOrderDefaults')}</div>${advanced}</form>`;
|
return `<form class="search-panel ${state.orderAdvanced?'expanded':''}" id="orderSearchForm"><div class="search-fields"><label class="field-inline"><span>기간</span><input id="orderPeriod" class="control medium" value="2026-08-11 ~ 2026-08-11" readonly aria-label="주문기간"></label><label class="field-inline"><span>판매채널</span><select id="orderChannel" class="control">${searchOption('all','전체',state.orderSearch.channel)}${searchOption('쿠팡','쿠팡',state.orderSearch.channel)}${searchOption('스마트스토어','스마트스토어',state.orderSearch.channel)}${searchOption('자사몰','자사몰',state.orderSearch.channel)}${searchOption('11번가','11번가',state.orderSearch.channel)}</select></label><label class="field-inline"><span>상태</span><select id="orderStatus" class="control">${searchOption('all','전체',state.orderSearch.status)}${searchOption('NEW','신규',state.orderSearch.status)}${searchOption('READY','출고대기',state.orderSearch.status)}${searchOption('CONFIRMED','출고지시',state.orderSearch.status)}${searchOption('SHIPPED','출고완료',state.orderSearch.status)}${searchOption('HOLD','보류',state.orderSearch.status)}${searchOption('ERROR','오류',state.orderSearch.status)}</select></label><label class="field-inline"><span>통합검색</span><input id="orderKeyword" class="control wide" value="${esc(state.orderSearch.keyword)}" placeholder="주문번호/주문자/상품" maxlength="80"></label></div><div class="search-actions">${btn('조회','primary','searchExecute')}${btn(state.orderAdvanced?`상세조건 ${activeCount?`(${activeCount})`:''} 닫기`:`상세조건${activeCount?` ${activeCount}`:''}`,'','toggleAdvanced')}${btn('초기화','','searchReset')}${btn('기본조건 저장','','saveOrderDefaults')}</div>${advanced}</form>`;
|
||||||
}
|
}
|
||||||
function captureOrderSearch(){
|
function captureOrderSearch(){
|
||||||
state.orderSearch.channel=$('#orderChannel')?.value||'all';
|
state.orderSearch.channel=$('#orderChannel')?.value||'all';
|
||||||
@@ -386,7 +426,7 @@
|
|||||||
const remember=$('#rememberOrderSearch');if(remember)state.orderRememberSearch=remember.checked;
|
const remember=$('#rememberOrderSearch');if(remember)state.orderRememberSearch=remember.checked;
|
||||||
}
|
}
|
||||||
function orderRows(){
|
function orderRows(){
|
||||||
return filteredOrders().map((order,index)=>`<tr class="order-data-row ${state.selectedOrders.has(order.no)?'selected-row':''}" data-order-row="${esc(order.no)}" tabindex="${index===0?'0':'-1'}"><td class="center"><input type="checkbox" aria-label="${esc(order.no)} 선택" data-order="${esc(order.no)}" ${state.selectedOrders.has(order.no)?'checked':''}></td><td><a href="#" class="link order-link" data-order="${esc(order.no)}">${esc(order.no)}</a></td><td>${esc(order.channel)}</td><td class="center">${esc(order.time)}</td><td>${esc(order.buyer)}</td><td>${esc(order.item)}</td><td class="num">${order.qty.toLocaleString('ko-KR')}</td><td class="num">₩${order.amount.toLocaleString('ko-KR')}</td><td class="center ${order.stock.includes('부족')?'warn':''}">${esc(order.stock)}</td><td class="center ${order.status==='오류'?'error':''}">${esc(order.status)}</td><td class="${order.exception?'error':''}">${esc(order.exception||'-')}</td></tr>`).join('');
|
return filteredOrders().map((order,index)=>`<tr class="order-data-row ${state.selectedOrders.has(order.no)?'selected-row':''}" data-order-row="${esc(order.no)}" tabindex="${index===0?'0':'-1'}"><td class="center"><input type="checkbox" aria-label="${esc(order.no)} 선택" data-order="${esc(order.no)}" ${state.selectedOrders.has(order.no)?'checked':''}></td><td><a href="#" class="link order-link" data-order="${esc(order.no)}">${esc(order.no)}</a></td><td>${esc(order.channel)}</td><td class="center">${esc(order.time)}</td><td>${esc(order.buyer)}</td><td>${esc(order.item)}</td><td class="num">${order.qty.toLocaleString('ko-KR')}</td><td class="num">₩${order.amount.toLocaleString('ko-KR')}</td><td class="center">${statusChip(order.stock)}</td><td class="center">${statusChip(order.status,'shipment')}</td><td class="${order.exception?'error':''}">${esc(order.exception||'-')}</td></tr>`).join('');
|
||||||
}
|
}
|
||||||
function renderOrderData(){
|
function renderOrderData(){
|
||||||
if(state.orderDataState==='loading')return dataState('loading','조회 중...','기존 조회조건을 유지한 채 데이터를 불러오고 있습니다.');
|
if(state.orderDataState==='loading')return dataState('loading','조회 중...','기존 조회조건을 유지한 채 데이터를 불러오고 있습니다.');
|
||||||
@@ -428,7 +468,7 @@
|
|||||||
}
|
}
|
||||||
function showOrderDetail(orderNo){
|
function showOrderDetail(orderNo){
|
||||||
const order=orders.find(x=>x.no===orderNo);if(!order)return;drawerRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;
|
const order=orders.find(x=>x.no===orderNo);if(!order)return;drawerRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;
|
||||||
openDrawerContent(`주문 ${order.no}`,`<div class="detail-status"><span class="status-chip">${esc(order.status)}</span>${order.exception?`<span class="status-chip danger">${esc(order.exception)}</span>`:''}</div><div class="detail-list"><div class="detail-row"><span>판매채널</span><strong>${esc(order.channel)}</strong></div><div class="detail-row"><span>주문자</span><strong>${esc(order.buyer)} / 010-****-1234</strong></div><div class="detail-row"><span>대표상품</span><strong>${esc(order.item)}</strong></div><div class="detail-row"><span>주문금액</span><strong>₩${order.amount.toLocaleString('ko-KR')}</strong></div><div class="detail-row"><span>예외</span><strong>${esc(order.exception||'없음')}</strong></div></div><h3>처리이력</h3><div class="detail-list"><div class="detail-row"><span>16:42</span><strong>주문수집</strong></div><div class="detail-row"><span>16:43</span><strong>결제확인</strong></div><div class="detail-row"><span>16:45</span><strong>재고할당</strong></div></div><div class="drawer-actions">${btn('전체 상세')}${btn('출고지시','primary','drawerShip',Boolean(order.exception))}</div>`);
|
openDrawerContent(`주문 ${order.no}`,`<div class="detail-status">${statusChip(order.status,'shipment')}${order.exception?`<span class="status-chip danger">${esc(order.exception)}</span>`:''}</div><div class="detail-list"><div class="detail-row"><span>판매채널</span><strong>${esc(order.channel)}</strong></div><div class="detail-row"><span>주문자</span><strong>${esc(order.buyer)} / 010-****-1234</strong></div><div class="detail-row"><span>대표상품</span><strong>${esc(order.item)}</strong></div><div class="detail-row"><span>주문금액</span><strong>₩${order.amount.toLocaleString('ko-KR')}</strong></div><div class="detail-row"><span>예외</span><strong>${esc(order.exception||'없음')}</strong></div></div><h3>처리이력</h3><div class="detail-list"><div class="detail-row"><span>16:42</span><strong>주문수집</strong></div><div class="detail-row"><span>16:43</span><strong>결제확인</strong></div><div class="detail-row"><span>16:45</span><strong>재고할당</strong></div></div><div class="drawer-actions">${btn('전체 상세')}${btn('출고지시','primary','drawerShip',Boolean(order.exception))}</div>`);
|
||||||
}
|
}
|
||||||
function closeDrawer(){drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true');drawerBackdrop.hidden=true;setBackgroundInert(false);const restore=drawerRestoreFocus;drawerRestoreFocus=null;restore?.focus();}
|
function closeDrawer(){drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true');drawerBackdrop.hidden=true;setBackgroundInert(false);const restore=drawerRestoreFocus;drawerRestoreFocus=null;restore?.focus();}
|
||||||
|
|
||||||
@@ -460,9 +500,10 @@
|
|||||||
if(!state.masterDraft)loadMasterDraft(state.selectedMasterId);
|
if(!state.masterDraft)loadMasterDraft(state.selectedMasterId);
|
||||||
const draft=state.masterDraft;
|
const draft=state.masterDraft;
|
||||||
const q=state.masterQuery.trim().toLowerCase();const visible=masterItems.filter(x=>!q||`${x.id} ${x.name} ${x.group}`.toLowerCase().includes(q));
|
const q=state.masterQuery.trim().toLowerCase();const visible=masterItems.filter(x=>!q||`${x.id} ${x.name} ${x.group}`.toLowerCase().includes(q));
|
||||||
const rows=visible.map(x=>`<tr class="${x.id===state.selectedMasterId?'selected-row':''}"><td><button type="button" class="row-link master-select" data-item="${esc(x.id)}">${esc(x.id)}</button></td><td>${esc(x.name)}</td><td>${esc(x.group)}</td><td class="center">${esc(x.status)}</td></tr>`).join('');
|
const rows=visible.map(x=>`<tr class="${x.id===state.selectedMasterId?'selected-row':''}"><td><button type="button" class="row-link master-select" data-item="${esc(x.id)}">${esc(x.id)}</button></td><td>${esc(x.name)}</td><td>${esc(x.group)}</td><td class="center">${statusChip(x.status)}</td></tr>`).join('');
|
||||||
const modeLabel=state.masterMode==='new'?'신규 품목':'품목 상세';
|
const modeLabel=state.masterMode==='new'?'신규 품목':'품목 상세';
|
||||||
const body=`${masterValidationSummary()}<div class="master-layout"><aside class="master-pane"><form class="search-panel" id="masterSearchForm"><div class="search-fields"><label class="field-inline"><span>검색</span><input id="masterQueryInput" class="control medium" value="${esc(state.masterQuery)}" placeholder="품목코드/품목명"></label></div><div class="search-actions">${btn('조회','primary','masterSearch')}</div></form><div class="context-bar"><strong>품목 목록</strong><span>표시 ${visible.length}건</span><span>${state.masterMode==='new'?'신규 입력 중':`선택 ${esc(state.selectedMasterId)}`}</span></div>${grid(['품목코드','품목명','그룹','상태'],rows,{caption:'품목 목록'})}</aside><main class="detail-pane"><div class="pane-header"><strong>${modeLabel}</strong><span>${state.masterMode==='new'?'고유 식별자와 Barcode는 새로 입력':'목록을 바꾸기 전에 미저장 상태를 확인'}</span></div><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('품목코드',draft.id,{required:true,name:'code',readonly:state.masterMode!=='new'})}${formField('품목명',draft.name,{required:true,name:'name'})}${formField('품목그룹',draft.group,{name:'group'})}${formField('규격',draft.spec,{name:'spec'})}${formField('단위',draft.unit,{name:'unit'})}</div></section><section class="form-section"><h2>물류정보</h2><div class="form-grid">${lookupField('기본창고',draft.warehouseCode,draft.warehouse,{field:'warehouse'})}${formField('바코드',draft.barcode,{name:'barcode'})}${checkboxField('LOT 관리',draft.lot,{field:'lot'})}${checkboxField('유통기한 관리',draft.expiry,{field:'expiry'})}</div></section><div class="summary-bar"><span>상태 <strong>${esc(draft.status)}</strong></span><span>Mode <strong>${esc(state.masterMode)}</strong></span><span>Version 17</span><button id="auditButton" type="button" class="text-action">변경이력</button></div></main></div>`;
|
const currentIndex=visible.findIndex(item=>item.id===state.selectedMasterId);const dirty=Boolean(state.tabs.find(tab=>tab.id==='ERP-MST-ITEM-001')?.dirty);const navigator=state.masterMode==='new'?'<span class="record-position">신규 입력</span>':`<nav class="record-navigator" aria-label="품목 이전·다음 이동"><span class="record-position">품목 <strong>${currentIndex>=0?currentIndex+1:0} / ${visible.length}</strong></span><button id="masterPrev" type="button" ${dirty||currentIndex<=0?'disabled':''}>‹ 이전</button><button id="masterNext" type="button" ${dirty||currentIndex<0||currentIndex>=visible.length-1?'disabled':''}>다음 ›</button></nav>`;
|
||||||
|
const body=`${masterValidationSummary()}<div class="master-layout"><aside class="master-pane"><form class="search-panel" id="masterSearchForm"><div class="search-fields"><label class="field-inline"><span>검색</span><input id="masterQueryInput" class="control medium" value="${esc(state.masterQuery)}" placeholder="품목코드/품목명"></label></div><div class="search-actions">${btn('조회','primary','masterSearch')}</div></form><div class="context-bar"><strong>품목 목록</strong><span>표시 ${visible.length}건</span><span>${state.masterMode==='new'?'신규 입력 중':`선택 ${esc(state.selectedMasterId)}`}</span></div>${grid(['품목코드','품목명','그룹','상태'],rows,{caption:'품목 목록'})}</aside><main class="detail-pane"><div class="pane-header"><div><strong>${modeLabel}</strong><span>${state.masterMode==='new'?'고유 식별자와 Barcode는 새로 입력':'목록을 바꾸기 전에 미저장 상태를 확인'}</span></div>${navigator}</div><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('품목코드',draft.id,{required:true,name:'code',readonly:state.masterMode!=='new'})}${formField('품목명',draft.name,{required:true,name:'name'})}${formField('품목그룹',draft.group,{name:'group'})}${formField('규격',draft.spec,{name:'spec'})}${formField('단위',draft.unit,{name:'unit'})}</div></section><section class="form-section"><h2>물류정보</h2><div class="form-grid">${lookupField('기본창고',draft.warehouseCode,draft.warehouse,{field:'warehouse'})}${formField('바코드',draft.barcode,{name:'barcode'})}${checkboxField('LOT 관리',draft.lot,{field:'lot'})}${checkboxField('유통기한 관리',draft.expiry,{field:'expiry'})}</div></section><div class="summary-bar"><span>상태 ${statusChip(draft.status)}</span><span>Mode <strong>${esc(state.masterMode)}</strong></span><span>Version 17</span><button id="auditButton" type="button" class="text-action">변경이력</button></div></main></div>`;
|
||||||
const commands=`<div class="command-group">${btn('조회 F3','','masterSearchTop')}${btn('신규','','masterNew')}${btn('저장 F8','primary','masterSave')}${btn('복사','','masterCopy')}</div><span class="command-separator"></span><div class="command-group">${btn(draft.status==='사용'?'사용중지':'사용재개','danger','masterSuspend')}${btn('엑셀 ▼')}</div>`;
|
const commands=`<div class="command-group">${btn('조회 F3','','masterSearchTop')}${btn('신규','','masterNew')}${btn('저장 F8','primary','masterSave')}${btn('복사','','masterCopy')}</div><span class="command-separator"></span><div class="command-group">${btn(draft.status==='사용'?'사용중지':'사용재개','danger','masterSuspend')}${btn('엑셀 ▼')}</div>`;
|
||||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT02();
|
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT02();
|
||||||
}
|
}
|
||||||
@@ -475,6 +516,7 @@
|
|||||||
$('#masterSearchForm').onsubmit=e=>{e.preventDefault();runSearch();};$('#masterSearch').onclick=runSearch;$('#masterSearchTop').onclick=runSearch;
|
$('#masterSearchForm').onsubmit=e=>{e.preventDefault();runSearch();};$('#masterSearch').onclick=runSearch;$('#masterSearchTop').onclick=runSearch;
|
||||||
$('#masterSave').onclick=saveMaster;$('#masterNew').onclick=()=>requestMasterTarget('__NEW__');$('#masterCopy').onclick=()=>requestMasterTarget('__COPY__');
|
$('#masterSave').onclick=saveMaster;$('#masterNew').onclick=()=>requestMasterTarget('__NEW__');$('#masterCopy').onclick=()=>requestMasterTarget('__COPY__');
|
||||||
$('#masterSuspend').onclick=()=>{state.masterDraft.status=state.masterDraft.status==='사용'?'중지':'사용';markDirty('ERP-MST-ITEM-001',true);renderT02(screen('ERP-MST-ITEM-001'));};
|
$('#masterSuspend').onclick=()=>{state.masterDraft.status=state.masterDraft.status==='사용'?'중지':'사용';markDirty('ERP-MST-ITEM-001',true);renderT02(screen('ERP-MST-ITEM-001'));};
|
||||||
|
const visible=masterItems.filter(x=>!state.masterQuery.trim()||`${x.id} ${x.name} ${x.group}`.toLowerCase().includes(state.masterQuery.trim().toLowerCase()));const index=visible.findIndex(item=>item.id===state.selectedMasterId);const prev=$('#masterPrev'),next=$('#masterNext');if(prev)prev.onclick=()=>visible[index-1]&&requestMasterTarget(visible[index-1].id);if(next)next.onclick=()=>visible[index+1]&&requestMasterTarget(visible[index+1].id);
|
||||||
$('#auditButton').onclick=()=>showUtilityPanel(screen('ERP-MST-ITEM-001'),'help');
|
$('#auditButton').onclick=()=>showUtilityPanel(screen('ERP-MST-ITEM-001'),'help');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,12 +533,12 @@
|
|||||||
state.transactionErrors=errors;return errors.length===0;
|
state.transactionErrors=errors;return errors.length===0;
|
||||||
}
|
}
|
||||||
function transactionValidationSummary(){return state.transactionErrors.length?`<div class="validation-summary" role="alert"><strong>저장할 수 없습니다. ${state.transactionErrors.length}개 항목을 확인하세요.</strong>${state.transactionErrors.map((error,index)=>`<button type="button" data-transaction-error="${index}">${esc(error.message)}</button>`).join('')}</div>`:'';}
|
function transactionValidationSummary(){return state.transactionErrors.length?`<div class="validation-summary" role="alert"><strong>저장할 수 없습니다. ${state.transactionErrors.length}개 항목을 확인하세요.</strong>${state.transactionErrors.map((error,index)=>`<button type="button" data-transaction-error="${index}">${esc(error.message)}</button>`).join('')}</div>`:'';}
|
||||||
function resetTransaction(){state.transactionStatus='작성';state.transactionErrors=[];state.transactionHeader={orderDate:'2026-08-11',channel:'직접주문',customerCode:'',customerName:'',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'',phone:'',postal:'',address:''};state.transactionLines=[{code:'',name:'',spec:'',available:0,qty:1,price:0,remark:''}];markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));setTimeout(()=>$('[data-field="customerCode"]',workspace)?.focus(),0);}
|
function resetTransaction(){state.transactionStatus='NEW';state.transactionErrors=[];state.transactionHeader={orderDate:'2026-08-11',channel:'직접주문',customerCode:'',customerName:'',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'',phone:'',postal:'',address:''};state.transactionLines=[{code:'',name:'',spec:'',available:0,qty:1,price:0,remark:''}];markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));setTimeout(()=>$('[data-field="customerCode"]',workspace)?.focus(),0);}
|
||||||
function renderT03(s){
|
function renderT03(s){
|
||||||
const h=state.transactionHeader;const totals=transactionTotals();
|
const h=state.transactionHeader;const totals=transactionTotals();
|
||||||
const lineRows=state.transactionLines.map((line,rowIndex)=>`<tr><td><input class="cell-editor" data-line-row="${rowIndex}" data-line-field="code" value="${esc(line.code)}" aria-label="${rowIndex+1}행 품목코드"></td><td>${esc(line.name||fastName(line.code))}</td><td>${esc(line.spec)}</td><td class="num">${Number(line.available||0).toLocaleString('ko-KR')}</td><td class="num"><input class="cell-editor num" data-line-row="${rowIndex}" data-line-field="qty" inputmode="decimal" value="${esc(line.qty)}" aria-label="${rowIndex+1}행 수량"></td><td class="num"><input class="cell-editor num" data-line-row="${rowIndex}" data-line-field="price" inputmode="decimal" value="${Number(line.price||0).toLocaleString('ko-KR')}" aria-label="${rowIndex+1}행 단가"></td><td class="num">${(Number(line.qty||0)*Number(line.price||0)).toLocaleString('ko-KR')}</td><td><input class="cell-editor" data-line-row="${rowIndex}" data-line-field="remark" value="${esc(line.remark||'')}" aria-label="${rowIndex+1}행 비고"></td></tr>`).join('');
|
const lineRows=state.transactionLines.map((line,rowIndex)=>`<tr><td><input class="cell-editor" data-line-row="${rowIndex}" data-line-field="code" value="${esc(line.code)}" aria-label="${rowIndex+1}행 품목코드"></td><td>${esc(line.name||fastName(line.code))}</td><td>${esc(line.spec)}</td><td class="num">${Number(line.available||0).toLocaleString('ko-KR')}</td><td class="num"><input class="cell-editor num" data-line-row="${rowIndex}" data-line-field="qty" inputmode="decimal" value="${esc(line.qty)}" aria-label="${rowIndex+1}행 수량"></td><td class="num"><input class="cell-editor num" data-line-row="${rowIndex}" data-line-field="price" inputmode="decimal" value="${Number(line.price||0).toLocaleString('ko-KR')}" aria-label="${rowIndex+1}행 단가"></td><td class="num">${(Number(line.qty||0)*Number(line.price||0)).toLocaleString('ko-KR')}</td><td><input class="cell-editor" data-line-row="${rowIndex}" data-line-field="remark" value="${esc(line.remark||'')}" aria-label="${rowIndex+1}행 비고"></td></tr>`).join('');
|
||||||
const body=`${transactionValidationSummary()}<div class="transaction-layout"><section class="workflow-strip">${['작성','확정','할당','피킹','검수','출고'].map(status=>`<span class="${state.transactionStatus===status?'active':''}">${status}</span>`).join('')}<strong>현재: ${esc(state.transactionStatus)} · ${state.transactionStatus==='작성'?'다음: 저장/확정':'확정 이후 주요 값은 서버 업무 규칙으로 제한'}</strong></section><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('주문일',h.orderDate,{required:true,name:'orderDate'})}${formField('판매채널',h.channel,{name:'channel'})}${lookupField('거래처',h.customerCode,h.customerName,{field:'customer'})}${formField('담당자',h.owner,{name:'owner'})}${lookupField('출고창고',h.warehouseCode,h.warehouseName,{field:'warehouse'})}</div></section><section class="form-section"><h2>배송정보</h2><div class="form-grid">${formField('수취인',h.receiver,{required:true,name:'receiver'})}${formField('연락처',h.phone,{name:'phone'})}${formField('우편번호',h.postal,{name:'postal'})}${formField('주소',h.address,{required:true,name:'address'})}</div></section><section class="detail-grid-section"><div class="pane-header"><strong>상품</strong><span>코드·수량·단가 직접 편집 · Enter 다음 셀</span><button id="transactionAddLine" type="button" class="text-action">+ 행추가</button></div>${grid(['품목코드','품목명','규격','가용재고','수량','단가','금액','비고'],lineRows,{caption:'주문 품목 상세'})}</section><div class="summary-bar"><span>품목 <strong id="transactionItemCount">${totals.items}종</strong></span><span>총수량 <strong id="transactionQty">${totals.qty.toLocaleString('ko-KR')}</strong></span><span>주문금액 <strong id="transactionAmount">₩${totals.amount.toLocaleString('ko-KR')}</strong></span><span>상태 <strong>${esc(state.transactionStatus)}</strong></span></div></div>`;
|
const body=`${transactionValidationSummary()}<div class="transaction-layout"><section class="workflow-strip">${['NEW','DRAFT','CONFIRMED','ALLOCATED','PICKING','CHECKED','SHIPPED'].map(status=>`<span class="${state.transactionStatus===status?'active':''}">${esc(statusLabel(status,'lifecycle'))}</span>`).join('')}<strong>현재: ${esc(statusLabel(state.transactionStatus,'lifecycle'))} · ${state.transactionStatus==='NEW'?'다음: 저장':state.transactionStatus==='DRAFT'?'다음: 확정':'확정 이후 주요 값은 서버 업무 규칙으로 제한'}</strong></section><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('주문일',h.orderDate,{required:true,name:'orderDate'})}${formField('판매채널',h.channel,{name:'channel'})}${lookupField('거래처',h.customerCode,h.customerName,{field:'customer'})}${formField('담당자',h.owner,{name:'owner'})}${lookupField('출고창고',h.warehouseCode,h.warehouseName,{field:'warehouse'})}</div></section><section class="form-section"><h2>배송정보</h2><div class="form-grid">${formField('수취인',h.receiver,{required:true,name:'receiver'})}${formField('연락처',h.phone,{name:'phone'})}${formField('우편번호',h.postal,{name:'postal'})}${formField('주소',h.address,{required:true,name:'address'})}</div></section><section class="detail-grid-section"><div class="pane-header"><strong>상품</strong><span>코드·수량·단가 직접 편집 · Enter 다음 셀</span><button id="transactionAddLine" type="button" class="text-action">+ 행추가</button></div>${grid(['품목코드','품목명','규격','가용재고','수량','단가','금액','비고'],lineRows,{caption:'주문 품목 상세'})}</section><div class="summary-bar"><span>품목 <strong id="transactionItemCount">${totals.items}종</strong></span><span>총수량 <strong id="transactionQty">${totals.qty.toLocaleString('ko-KR')}</strong></span><span>주문금액 <strong id="transactionAmount">₩${totals.amount.toLocaleString('ko-KR')}</strong></span><span>상태 <strong>${esc(statusLabel(state.transactionStatus,'lifecycle'))}</strong></span></div></div>`;
|
||||||
const commands=`<div class="command-group">${btn('신규','','transactionNew')}${btn('저장 F8','primary','saveTransaction')}${btn('주문복사','','transactionCopy')}</div><span class="command-separator"></span><div class="command-group">${btn('확정','','confirmTransaction',state.transactionStatus!=='작성')}${btn('엑셀 ▼')}</div>`;
|
const commands=`<div class="command-group">${btn('신규','','transactionNew')}${btn('저장 F8','primary','saveTransaction')}${btn('주문복사','','transactionCopy')}</div><span class="command-separator"></span><div class="command-group">${btn('확정','','confirmTransaction',state.transactionStatus!=='DRAFT')}${btn('엑셀 ▼')}</div>`;
|
||||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT03();
|
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT03();
|
||||||
}
|
}
|
||||||
function bindT03(){
|
function bindT03(){
|
||||||
@@ -505,10 +547,10 @@
|
|||||||
const editors=$$('.cell-editor',workspace);editors.forEach((editor,index)=>{editor.oninput=()=>{const line=state.transactionLines[Number(editor.dataset.lineRow)];const field=editor.dataset.lineField;let value=editor.value;if(['qty','price'].includes(field))value=Number(String(value).replaceAll(',',''))||0;line[field]=value;if(field==='code')line.name=fastName(String(value).trim());state.transactionErrors=[];markDirty('OMS-ORD-002',true);const totals=transactionTotals();$('#transactionItemCount').textContent=`${totals.items}종`;$('#transactionQty').textContent=totals.qty.toLocaleString('ko-KR');$('#transactionAmount').textContent=`₩${totals.amount.toLocaleString('ko-KR')}`;};editor.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();editors[Math.min(index+1,editors.length-1)]?.focus();}};});
|
const editors=$$('.cell-editor',workspace);editors.forEach((editor,index)=>{editor.oninput=()=>{const line=state.transactionLines[Number(editor.dataset.lineRow)];const field=editor.dataset.lineField;let value=editor.value;if(['qty','price'].includes(field))value=Number(String(value).replaceAll(',',''))||0;line[field]=value;if(field==='code')line.name=fastName(String(value).trim());state.transactionErrors=[];markDirty('OMS-ORD-002',true);const totals=transactionTotals();$('#transactionItemCount').textContent=`${totals.items}종`;$('#transactionQty').textContent=totals.qty.toLocaleString('ko-KR');$('#transactionAmount').textContent=`₩${totals.amount.toLocaleString('ko-KR')}`;};editor.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();editors[Math.min(index+1,editors.length-1)]?.focus();}};});
|
||||||
$$('[data-transaction-error]',workspace).forEach(button=>button.onclick=()=>{const error=state.transactionErrors[Number(button.dataset.transactionError)];if(!error)return;if(Number.isInteger(error.row))$(`[data-line-row="${error.row}"][data-line-field="${CSS.escape(error.field)}"]`,workspace)?.focus();else if(error.field==='customerCode')$('.lookup[data-lookup-field="customer"] .lookup-trigger',workspace)?.focus();else if(error.field==='warehouseCode')$('.lookup[data-lookup-field="warehouse"] .lookup-trigger',workspace)?.focus();else $(`[data-field="${CSS.escape(error.field)}"]`,workspace)?.focus();});
|
$$('[data-transaction-error]',workspace).forEach(button=>button.onclick=()=>{const error=state.transactionErrors[Number(button.dataset.transactionError)];if(!error)return;if(Number.isInteger(error.row))$(`[data-line-row="${error.row}"][data-line-field="${CSS.escape(error.field)}"]`,workspace)?.focus();else if(error.field==='customerCode')$('.lookup[data-lookup-field="customer"] .lookup-trigger',workspace)?.focus();else if(error.field==='warehouseCode')$('.lookup[data-lookup-field="warehouse"] .lookup-trigger',workspace)?.focus();else $(`[data-field="${CSS.escape(error.field)}"]`,workspace)?.focus();});
|
||||||
$('#transactionAddLine').onclick=()=>{state.transactionLines.push({code:'',name:'',spec:'',available:0,qty:1,price:0,remark:''});markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));setTimeout(()=>$$('.cell-editor',workspace).at(-4)?.focus(),0);};
|
$('#transactionAddLine').onclick=()=>{state.transactionLines.push({code:'',name:'',spec:'',available:0,qty:1,price:0,remark:''});markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));setTimeout(()=>$$('.cell-editor',workspace).at(-4)?.focus(),0);};
|
||||||
$('#saveTransaction').onclick=()=>{if(!validateTransaction()){renderT03(screen('OMS-ORD-002'));return;}markDirty('OMS-ORD-002',false);toast('주문을 저장했습니다. 서버 최종 검증을 통과한 것으로 가정한 FE Reference 상태입니다.');};
|
$('#saveTransaction').onclick=()=>{if(!validateTransaction()){renderT03(screen('OMS-ORD-002'));return;}state.transactionStatus='DRAFT';markDirty('OMS-ORD-002',false);renderT03(screen('OMS-ORD-002'));toast('주문을 저장했습니다. 서버 응답 상태 DRAFT를 사용자 Label 작성으로 표시합니다.');};
|
||||||
$('#confirmTransaction').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('저장하지 않은 변경이 있습니다. 먼저 저장하세요.','error');return;}if(!validateTransaction()){renderT03(screen('OMS-ORD-002'));return;}state.transactionStatus='확정';renderT03(screen('OMS-ORD-002'));toast('주문 상태를 확정으로 전환했습니다. 실제 제품에서는 Domain이 최종 검증합니다.');};
|
$('#confirmTransaction').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('저장하지 않은 변경이 있습니다. 먼저 저장하세요.','error');return;}if(!validateTransaction()){renderT03(screen('OMS-ORD-002'));return;}state.transactionStatus='CONFIRMED';renderT03(screen('OMS-ORD-002'));toast('서버 응답 상태 CONFIRMED를 사용자 Label 확정으로 표시합니다.');};
|
||||||
$('#transactionNew').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('현재 주문의 미저장 변경을 먼저 저장하거나 버리세요.','error');return;}resetTransaction();};
|
$('#transactionNew').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('현재 주문의 미저장 변경을 먼저 저장하거나 버리세요.','error');return;}resetTransaction();};
|
||||||
$('#transactionCopy').onclick=()=>{if(state.tabs.find(x=>x.id==='OMS-ORD-002')?.dirty){toast('미저장 변경을 먼저 처리하세요.','error');return;}state.transactionStatus='작성';state.transactionErrors=[];state.transactionHeader={...state.transactionHeader,receiver:'',phone:'',postal:'',address:''};state.transactionLines=state.transactionLines.map(line=>({...line}));markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));toast('주문을 복사했습니다. 수취/배송정보를 새 주문에 맞게 입력하세요.');};
|
$('#transactionCopy').onclick=()=>{if(state.tabs.find(x=>x.id==='OMS-ORD-002')?.dirty){toast('미저장 변경을 먼저 처리하세요.','error');return;}state.transactionStatus='NEW';state.transactionErrors=[];state.transactionHeader={...state.transactionHeader,receiver:'',phone:'',postal:'',address:''};state.transactionLines=state.transactionLines.map(line=>({...line}));markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));toast('주문을 복사했습니다. 수취/배송정보를 새 주문에 맞게 입력하세요.');};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fastName(code){return ({ABC001:'운동화',ABC002:'러닝화',ABC003:'양말',ABC004:'셔츠'})[code]||'';}
|
function fastName(code){return ({ABC001:'운동화',ABC002:'러닝화',ABC003:'양말',ABC004:'셔츠'})[code]||'';}
|
||||||
@@ -516,7 +558,7 @@
|
|||||||
function renderT04(s){
|
function renderT04(s){
|
||||||
const rows=state.fastRows.map((row,index)=>`<tr><td class="center">${index+1}</td><td><input class="fast-cell" data-row="${index}" data-field="code" value="${esc(row.code)}" aria-label="${index+1}행 품목코드"></td><td>${esc(row.name||fastName(row.code))}</td><td class="num"><input class="fast-cell num ${row.code&&(!row.price||Number(String(row.price).replaceAll(',',''))<=0)?'invalid':''}" data-row="${index}" data-field="price" value="${esc(row.price)}" aria-label="${index+1}행 단가"></td><td><input class="fast-cell" data-row="${index}" data-field="date" value="${esc(row.date)}" aria-label="${index+1}행 적용일"></td><td><input class="fast-cell" data-row="${index}" data-field="remark" value="${esc(row.remark)}" aria-label="${index+1}행 비고"></td></tr>`).join('');
|
const rows=state.fastRows.map((row,index)=>`<tr><td class="center">${index+1}</td><td><input class="fast-cell" data-row="${index}" data-field="code" value="${esc(row.code)}" aria-label="${index+1}행 품목코드"></td><td>${esc(row.name||fastName(row.code))}</td><td class="num"><input class="fast-cell num ${row.code&&(!row.price||Number(String(row.price).replaceAll(',',''))<=0)?'invalid':''}" data-row="${index}" data-field="price" value="${esc(row.price)}" aria-label="${index+1}행 단가"></td><td><input class="fast-cell" data-row="${index}" data-field="date" value="${esc(row.date)}" aria-label="${index+1}행 적용일"></td><td><input class="fast-cell" data-row="${index}" data-field="remark" value="${esc(row.remark)}" aria-label="${index+1}행 비고"></td></tr>`).join('');
|
||||||
const inputCount=state.fastRows.filter(x=>x.code).length;const errorCount=fastErrorCount();
|
const inputCount=state.fastRows.filter(x=>x.code).length;const errorCount=fastErrorCount();
|
||||||
const body=`<div class="context-bar"><strong>Fast Entry</strong><span>Enter = 다음 입력 셀</span><span>Ctrl+V = 다중 Cell 붙여넣기</span><span>F2 = 품목 Lookup</span><span class="${errorCount?'error':''}">오류 ${errorCount}개</span></div>${grid(['#','품목코드','품목명','단가','적용일','비고'],rows,{caption:'품목단가 빠른 입력'})}<div class="summary-bar"><span>입력 ${inputCount}행</span><span>오류 <strong>${errorCount}</strong></span><span>빈 행 ${state.fastRows.length-inputCount}</span></div>`;
|
const body=`<div class="context-bar"><strong>Fast Entry</strong><span>Enter = 다음 입력 셀</span><span>Ctrl+V = 다중 Cell 붙여넣기</span><span>F2 = 품목 Lookup</span><span class="${errorCount?'error':''}">오류 ${errorCount}개</span></div><section class="fast-entry-surface"><div class="pane-header"><div><strong>품목 단가 입력</strong><span>품목·단가를 연속 입력하고 오류를 해소한 뒤 F8로 저장</span></div><span>입력 ${inputCount}건 · 오류 ${errorCount}건</span></div>${grid(['#','품목코드','품목명','단가','적용일','비고'],rows,{caption:'품목단가 빠른 입력'})}</section><div class="summary-bar"><span>입력 ${inputCount}행</span><span>오류 <strong>${errorCount}</strong></span><span>빈 행 ${state.fastRows.length-inputCount}</span></div>`;
|
||||||
const commands=`<div class="command-group">${btn('저장 F8','primary','fastSave',errorCount>0)}${btn('행추가','','fastAdd')}${btn('행복제','','fastDuplicate')}${btn('Fill Down','','fastFill')}</div><span class="command-separator"></span><div class="command-group">${btn('엑셀 붙여넣기')}${btn('엑셀 ▼')}</div>`;
|
const commands=`<div class="command-group">${btn('저장 F8','primary','fastSave',errorCount>0)}${btn('행추가','','fastAdd')}${btn('행복제','','fastDuplicate')}${btn('Fill Down','','fastFill')}</div><span class="command-separator"></span><div class="command-group">${btn('엑셀 붙여넣기')}${btn('엑셀 ▼')}</div>`;
|
||||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT04();
|
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT04();
|
||||||
}
|
}
|
||||||
@@ -579,7 +621,7 @@
|
|||||||
function renderT06(s){
|
function renderT06(s){
|
||||||
const visible=queueVisibleRows();
|
const visible=queueVisibleRows();
|
||||||
const selectedCount=state.selectedQueueRows.size;const allSelected=visible.length>0&&visible.every(row=>state.selectedQueueRows.has(row.no));
|
const selectedCount=state.selectedQueueRows.size;const allSelected=visible.length>0&&visible.every(row=>state.selectedQueueRows.has(row.no));
|
||||||
const rows=visible.map(row=>`<tr class="${state.selectedQueueRows.has(row.no)?'selected-row':''}"><td class="center"><input type="checkbox" data-queue-row="${esc(row.no)}" aria-label="${esc(row.no)} 선택" ${state.selectedQueueRows.has(row.no)?'checked':''}></td><td>${esc(row.no)}</td><td>${esc(row.type)}</td><td>${esc(row.wave)}</td><td>${esc(row.worker)}</td><td class="num">${esc(row.progress)}</td><td>${esc(row.sla)}</td><td class="center ${row.status==='지연'?'error':''}">${esc(row.status)}</td></tr>`).join('');
|
const rows=visible.map(row=>`<tr class="${state.selectedQueueRows.has(row.no)?'selected-row':''}"><td class="center"><input type="checkbox" data-queue-row="${esc(row.no)}" aria-label="${esc(row.no)} 선택" ${state.selectedQueueRows.has(row.no)?'checked':''}></td><td>${esc(row.no)}</td><td>${esc(row.type)}</td><td>${esc(row.wave)}</td><td>${esc(row.worker)}</td><td class="num">${esc(row.progress)}</td><td>${esc(row.sla)}</td><td class="center">${statusChip(row.status)}</td></tr>`).join('');
|
||||||
const table=grid(['선택','작업번호','작업유형','Wave','작업자','진행률','SLA','상태'],rows,{caption:'물류 작업 대기열'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="queueSelectAll" type="checkbox" aria-label="현재 표시 작업 전체 선택" ${allSelected?'checked':''}></th>`);
|
const table=grid(['선택','작업번호','작업유형','Wave','작업자','진행률','SLA','상태'],rows,{caption:'물류 작업 대기열'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="queueSelectAll" type="checkbox" aria-label="현재 표시 작업 전체 선택" ${allSelected?'checked':''}></th>`);
|
||||||
const summary=[['입고','입고대기','218'],['적치','적치대기','43'],['피킹','피킹대기','927'],['검수','검수대기','122'],['exception','예외','12']];
|
const summary=[['입고','입고대기','218'],['적치','적치대기','43'],['피킹','피킹대기','927'],['검수','검수대기','122'],['exception','예외','12']];
|
||||||
const result=state.queueActionResult?`<div class="action-result" role="status"><strong>최근 처리</strong><span>${esc(state.queueActionResult)}</span><button id="queueResultClear" type="button" class="text-action">닫기</button></div>`:'';
|
const result=state.queueActionResult?`<div class="action-result" role="status"><strong>최근 처리</strong><span>${esc(state.queueActionResult)}</span><button id="queueResultClear" type="button" class="text-action">닫기</button></div>`:'';
|
||||||
@@ -608,7 +650,7 @@
|
|||||||
}
|
}
|
||||||
function renderT07(s){
|
function renderT07(s){
|
||||||
const visible=reconcileVisibleRows();const selectedCount=state.selectedReconcileRows.size;const allSelected=visible.length>0&&visible.every(row=>state.selectedReconcileRows.has(row.no));
|
const visible=reconcileVisibleRows();const selectedCount=state.selectedReconcileRows.size;const allSelected=visible.length>0&&visible.every(row=>state.selectedReconcileRows.has(row.no));
|
||||||
const rows=visible.map(row=>`<tr class="${state.selectedReconcileRows.has(row.no)?'selected-row':''}"><td class="center"><input type="checkbox" data-reconcile-row="${esc(row.no)}" aria-label="${esc(row.no)} 선택" ${state.selectedReconcileRows.has(row.no)?'checked':''}></td><td>${esc(row.no)}</td><td class="num">${row.expected}</td><td class="num">${row.actual}</td><td class="num ${row.diff!==0?'error':''}">${row.diff}</td><td>${esc(row.reason)}</td><td>${esc(row.status)}</td></tr>`).join('');
|
const rows=visible.map(row=>`<tr class="${state.selectedReconcileRows.has(row.no)?'selected-row':''}"><td class="center"><input type="checkbox" data-reconcile-row="${esc(row.no)}" aria-label="${esc(row.no)} 선택" ${state.selectedReconcileRows.has(row.no)?'checked':''}></td><td>${esc(row.no)}</td><td class="num">${row.expected}</td><td class="num">${row.actual}</td><td class="num ${row.diff!==0?'error':''}">${row.diff}</td><td>${esc(row.reason)}</td><td>${statusChip(row.status)}</td></tr>`).join('');
|
||||||
const table=grid(['선택','주문번호','OMS','WMS','차이','원인','처리상태'],rows,{caption:'OMS WMS 재고 출고 대사'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="reconcileSelectAll" type="checkbox" aria-label="현재 표시 대사 전체 선택" ${allSelected?'checked':''}></th>`);
|
const table=grid(['선택','주문번호','OMS','WMS','차이','원인','처리상태'],rows,{caption:'OMS WMS 재고 출고 대사'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="reconcileSelectAll" type="checkbox" aria-label="현재 표시 대사 전체 선택" ${allSelected?'checked':''}></th>`);
|
||||||
const result=state.reconcileActionResult?`<div class="action-result" role="status"><strong>최근 처리</strong><span>${esc(state.reconcileActionResult)}</span><button id="reconcileResultClear" type="button" class="text-action">닫기</button></div>`:'';
|
const result=state.reconcileActionResult?`<div class="action-result" role="status"><strong>최근 처리</strong><span>${esc(state.reconcileActionResult)}</span><button id="reconcileResultClear" type="button" class="text-action">닫기</button></div>`:'';
|
||||||
const search=`<form class="search-panel" id="reconcileSearch"><div class="search-fields"><label class="field-inline"><span>기준일</span><input id="reconcileDate" class="control" type="date" value="${esc(state.reconcileSearch.date)}"></label><label class="field-inline"><span>통합검색</span><input id="reconcileKeyword" class="control wide" value="${esc(state.reconcileSearch.keyword)}" placeholder="주문번호/원인/상태"></label><label class="field-inline checkbox-inline"><input id="mismatchOnly" type="checkbox" ${state.mismatchOnly?'checked':''}><span>불일치만 표시</span></label></div><div class="search-actions">${btn('조회','primary','reconcileSearchInline')}</div></form>`;
|
const search=`<form class="search-panel" id="reconcileSearch"><div class="search-fields"><label class="field-inline"><span>기준일</span><input id="reconcileDate" class="control" type="date" value="${esc(state.reconcileSearch.date)}"></label><label class="field-inline"><span>통합검색</span><input id="reconcileKeyword" class="control wide" value="${esc(state.reconcileSearch.keyword)}" placeholder="주문번호/원인/상태"></label><label class="field-inline checkbox-inline"><input id="mismatchOnly" type="checkbox" ${state.mismatchOnly?'checked':''}><span>불일치만 표시</span></label></div><div class="search-actions">${btn('조회','primary','reconcileSearchInline')}</div></form>`;
|
||||||
@@ -622,7 +664,7 @@
|
|||||||
const reset=$('#reconcileReset');if(reset)reset.onclick=()=>{state.mismatchOnly=false;state.reconcileSearch={date:'2026-08-11',keyword:''};state.selectedReconcileRows.clear();state.reconcileDataState='ready';renderT07(screen('COMMON-REC-001'));};
|
const reset=$('#reconcileReset');if(reset)reset.onclick=()=>{state.mismatchOnly=false;state.reconcileSearch={date:'2026-08-11',keyword:''};state.selectedReconcileRows.clear();state.reconcileDataState='ready';renderT07(screen('COMMON-REC-001'));};
|
||||||
$$('[data-reconcile-row]',workspace).forEach(box=>box.onchange=()=>{box.checked?state.selectedReconcileRows.add(box.dataset.reconcileRow):state.selectedReconcileRows.delete(box.dataset.reconcileRow);renderT07(screen('COMMON-REC-001'));});
|
$$('[data-reconcile-row]',workspace).forEach(box=>box.onchange=()=>{box.checked?state.selectedReconcileRows.add(box.dataset.reconcileRow):state.selectedReconcileRows.delete(box.dataset.reconcileRow);renderT07(screen('COMMON-REC-001'));});
|
||||||
const all=$('#reconcileSelectAll');if(all)all.onchange=()=>{reconcileVisibleRows().forEach(row=>all.checked?state.selectedReconcileRows.add(row.no):state.selectedReconcileRows.delete(row.no));renderT07(screen('COMMON-REC-001'));};
|
const all=$('#reconcileSelectAll');if(all)all.onchange=()=>{reconcileVisibleRows().forEach(row=>all.checked?state.selectedReconcileRows.add(row.no):state.selectedReconcileRows.delete(row.no));renderT07(screen('COMMON-REC-001'));};
|
||||||
const rerun=()=>{if(!state.selectedReconcileRows.size)return;const count=state.selectedReconcileRows.size;for(const row of reconcileRows){if(state.selectedReconcileRows.has(row.no)){row.actual=row.expected;row.diff=0;row.reason='재처리 완료';row.status='해결';}}state.reconcileActionResult=`${count}건을 재처리하여 현재 Reference 데이터 기준 차이를 0으로 맞췄습니다.`;state.selectedReconcileRows.clear();renderT07(screen('COMMON-REC-001'));};
|
const rerun=()=>{if(!state.selectedReconcileRows.size)return;const count=state.selectedReconcileRows.size;state.reconcileActionResult=`${count}건의 재처리 요청을 등록했습니다. 원천 OMS/WMS 값은 이 화면에서 직접 덮어쓰지 않습니다.`;state.selectedReconcileRows.clear();renderT07(screen('COMMON-REC-001'));};
|
||||||
for(const id of ['reconcileRun','reconcileRunTop']){const button=$('#'+id);if(button)button.onclick=rerun;}const reason=$('#reconcileReason');if(reason)reason.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건의 원인 Projection을 다시 조회했습니다.`;renderT07(screen('COMMON-REC-001'));};const note=$('#reconcileReasonNote');if(note)note.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건에 처리사유가 필요합니다. 실제 제품에서는 사유 입력 Drawer를 사용합니다.`;renderT07(screen('COMMON-REC-001'));};const clear=$('#reconcileResultClear');if(clear)clear.onclick=()=>{state.reconcileActionResult='';renderT07(screen('COMMON-REC-001'));};
|
for(const id of ['reconcileRun','reconcileRunTop']){const button=$('#'+id);if(button)button.onclick=rerun;}const reason=$('#reconcileReason');if(reason)reason.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건의 원인 Projection을 다시 조회했습니다.`;renderT07(screen('COMMON-REC-001'));};const note=$('#reconcileReasonNote');if(note)note.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건에 처리사유가 필요합니다. 실제 제품에서는 사유 입력 Drawer를 사용합니다.`;renderT07(screen('COMMON-REC-001'));};const clear=$('#reconcileResultClear');if(clear)clear.onclick=()=>{state.reconcileActionResult='';renderT07(screen('COMMON-REC-001'));};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,14 +677,14 @@
|
|||||||
const step=state.importStep;const steps=['파일','매핑','검증','미리보기','반영','결과'];
|
const step=state.importStep;const steps=['파일','매핑','검증','미리보기','반영','결과'];
|
||||||
let content='';
|
let content='';
|
||||||
if(step===1)content=`<input id="importFileInput" class="sr-only" type="file" accept=".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"><div class="drop-zone" id="dropZone" tabindex="0" role="button" aria-controls="importFileInput"><div><strong>${state.importFileName?esc(state.importFileName):'Excel 파일을 선택하세요'}</strong><p>${state.importFileName?`${state.importFileSize?`${(state.importFileSize/1024/1024).toFixed(2)}MB · `:''}파일 선택 완료 · 다음 단계에서 필드 매핑을 확인합니다.`:'파일 선택은 필수 접근 경로이며 Drag & Drop은 편의 기능입니다. .xlsx/.xls · 최대 20MB'}</p>${btn(state.importFileName?'파일 다시 선택':'파일 선택','primary','fileSelect')} ${btn('업로드 양식 다운로드')}</div></div>`;
|
if(step===1)content=`<input id="importFileInput" class="sr-only" type="file" accept=".xlsx,.xls,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"><div class="drop-zone" id="dropZone" tabindex="0" role="button" aria-controls="importFileInput"><div><strong>${state.importFileName?esc(state.importFileName):'Excel 파일을 선택하세요'}</strong><p>${state.importFileName?`${state.importFileSize?`${(state.importFileSize/1024/1024).toFixed(2)}MB · `:''}파일 선택 완료 · 다음 단계에서 필드 매핑을 확인합니다.`:'파일 선택은 필수 접근 경로이며 Drag & Drop은 편의 기능입니다. .xlsx/.xls · 최대 20MB'}</p>${btn(state.importFileName?'파일 다시 선택':'파일 선택','primary','fileSelect')} ${btn('업로드 양식 다운로드')}</div></div>`;
|
||||||
if(step===2)content=`<div class="mapping-table"><div class="mapping-head"><strong>Excel Column</strong><strong>System Field</strong><strong>매핑 근거</strong></div>${[['주문번호','주문번호','정확 일치'],['상품코드','품목코드','Alias 일치'],['수량','주문수량','Alias 일치'],['구매자','주문자명','Alias 일치'],['업체코드','거래처코드','사용자 확인 필요']].map(row=>`<div class="mapping-row"><span>${esc(row[0])}</span><span>→ ${esc(row[1])}</span><span>${esc(row[2])}</span></div>`).join('')}</div>`;
|
if(step===2)content=`<section class="import-stage-focus" data-import-stage-heading tabindex="-1" aria-label="필드 매핑"><h2>필드 매핑</h2><div class="mapping-table"><div class="mapping-head"><strong>Excel Column</strong><strong>System Field</strong><strong>매핑 근거</strong></div>${[['주문번호','주문번호','정확 일치'],['상품코드','품목코드','Alias 일치'],['수량','주문수량','Alias 일치'],['구매자','주문자명','Alias 일치'],['업체코드','거래처코드','사용자 확인 필요']].map(row=>`<div class="mapping-row"><span>${esc(row[0])}</span><span>→ ${esc(row[1])}</span><span>${esc(row[2])}</span></div>`).join('')}</div></section>`;
|
||||||
if(step===3)content=`<div class="validation-cards"><div class="validation-card"><span>전체</span><b>18,230</b></div><div class="validation-card"><span>정상</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div>${grid(['행','필드','값','오류'],`<tr><td>8</td><td>품목코드</td><td>ABC99</td><td class="error">존재하지 않는 품목</td></tr><tr><td>12</td><td>수량</td><td>-1</td><td class="error">수량은 0보다 커야 합니다.</td></tr>`,{caption:'Excel 검증 오류'})}`;
|
if(step===3){const validationErrors=[['8','품목코드','ABC99','존재하지 않는 품목'],['12','수량','-1','수량은 0보다 커야 합니다.']];content=`<section class="import-stage-focus" data-import-stage-heading tabindex="-1" aria-label="검증 결과"><h2>검증 결과</h2><div class="validation-cards"><div class="validation-card"><span>전체</span><b>18,230</b></div><div class="validation-card"><span>정상</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div><div class="import-error-nav" role="group" aria-label="검증 오류 탐색"><span>샘플 오류 <b>${state.importErrorIndex+1} / ${validationErrors.length}</b></span>${btn('첫 오류','','importFirstError')}${btn('다음 오류','','importNextError')}${btn('오류 336건 다운로드')}</div>${grid(['행','필드','값','오류'],validationErrors.map((row,index)=>`<tr tabindex="-1" data-import-error-row="${index}" aria-current="${index===state.importErrorIndex?'true':'false'}"><td>${row[0]}</td><td>${esc(row[1])}</td><td>${esc(row[2])}</td><td class="error">${esc(row[3])}</td></tr>`).join(''),{caption:'Excel 검증 오류'})}</section>`;}
|
||||||
if(step===4)content=`<section class="commit-panel"><div class="pane-header"><strong>반영 미리보기</strong><span>오류 336건 제외</span></div><div class="commit-body"><p><strong>17,894건</strong>을 반영할 수 있습니다.</p><dl><div><dt>신규</dt><dd>17,201</dd></div><div><dt>수정</dt><dd>693</dd></div><div><dt>제외 오류</dt><dd>336</dd></div></dl><p class="warning-copy">실제 제품은 Staging 데이터를 Domain 검증한 뒤 Commit합니다.</p></div></section>`;
|
if(step===4)content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>반영 미리보기</strong><span>오류 336건 제외</span></div><div class="commit-body"><p><strong>17,894건</strong>을 반영할 수 있습니다.</p><dl><div><dt>신규</dt><dd>17,201</dd></div><div><dt>수정</dt><dd>693</dd></div><div><dt>제외 오류</dt><dd>336</dd></div></dl><p class="warning-copy">실제 제품은 Staging 데이터를 Domain 검증한 뒤 Commit합니다.</p></div></section>`;
|
||||||
if(step===5){const running=state.importJobStatus==='running';const completed=state.importJobStatus==='completed';content=`<section class="commit-panel"><div class="pane-header"><strong>Background 반영 작업</strong><span>${completed?'완료':running?'처리 중':'대기'}</span></div><div class="commit-body"><div class="job-progress"><div class="job-progress-track"><span style="width:${state.importProgress}%"></span></div><strong>${state.importProgress}%</strong><p>${state.importProcessed.toLocaleString('ko-KR')} / 17,894 처리</p></div><p>다른 화면으로 이동해도 작업은 계속됩니다. 홈의 진행 작업에서 다시 들어올 수 있습니다.</p>${!running&&!completed?btn('17,894건 반영','primary','importCommit'):''}${completed?btn('결과 보기','primary','importResult'):''}</div></section>`;}
|
if(step===5){const running=state.importJobStatus==='running';const completed=state.importJobStatus==='completed';content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>Background 반영 작업</strong><span>${completed?'완료':running?'처리 중':'대기'}</span></div><div class="commit-body"><div class="job-progress"><div class="job-progress-track"><span style="width:${state.importProgress}%"></span></div><strong>${state.importProgress}%</strong><p>${state.importProcessed.toLocaleString('ko-KR')} / 17,894 처리</p></div><p>다른 화면으로 이동해도 작업은 계속됩니다. 홈의 진행 작업에서 다시 들어올 수 있습니다.</p>${!running&&!completed?btn('17,894건 반영','primary','importCommit'):''}${completed?btn('결과 보기','primary','importResult'):''}</div></section>`;}
|
||||||
if(step===6)content=`<section class="commit-panel"><div class="pane-header"><strong>반영 결과</strong><span>완료</span></div><div class="commit-body"><div class="validation-cards"><div class="validation-card"><span>성공</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div><p>오류 데이터는 Domain DB에 반영하지 않았습니다.</p><div class="inline-actions">${btn('오류 336건 다운로드')}${btn('새 업로드','primary','importReset')}</div></div></section>`;
|
if(step===6)content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>반영 결과</strong><span>완료</span></div><div class="commit-body"><div class="validation-cards"><div class="validation-card"><span>성공</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div><p>오류 데이터는 Domain DB에 반영하지 않았습니다.</p><div class="inline-actions">${btn('오류 336건 다운로드')}${btn('새 업로드','primary','importReset')}</div></div></section>`;
|
||||||
const nav=`<div class="import-steps">${steps.map((label,index)=>`<div class="import-step ${index+1===step?'active':''} ${index+1<step?'done':''}"><b>${index+1}</b><span>${label}</span></div>`).join('')}</div>`;
|
const nav=`<div class="import-steps">${steps.map((label,index)=>`<div class="import-step ${index+1===step?'active':''} ${index+1<step?'done':''}"><b>${index+1}</b><span>${label}</span></div>`).join('')}</div>`;
|
||||||
const buttons=step<5?`<div class="wizard-actions">${step>1?btn('이전','','importBack'):''}${step<4?btn('다음','primary','importNext',step===1&&!state.importFileName):step===4?btn('반영 단계','primary','importNext'):''}</div>`:'';
|
const buttons=step<5?`<div class="wizard-actions">${step>1?btn('이전','','importBack'):''}${step<4?btn('다음','primary','importNext',step===1&&!state.importFileName):step===4?btn('반영 단계','primary','importNext'):''}</div>`:'';
|
||||||
workspace.innerHTML=pageShell(s,`${nav}${content}${buttons}`,`<div class="command-group">${btn('양식 다운로드')}</div><span class="command-separator"></span><div class="command-group">${btn('최근 업로드 결과')}</div>`);bindPageTools(s);bindT08();
|
workspace.innerHTML=pageShell(s,`${nav}${content}${buttons}`,`<div class="command-group">${btn('양식 다운로드')}</div><span class="command-separator"></span><div class="command-group">${btn('최근 업로드 결과')}</div>`);bindPageTools(s);bindT08();if(state.importFocusStep!==step){state.importFocusStep=step;setTimeout(()=>{const target=step===1?$('#dropZone',workspace):$('[data-import-stage-heading]',workspace);target?.focus({preventScroll:true});},0);}
|
||||||
}
|
}
|
||||||
function startImportJob(){
|
function startImportJob(){
|
||||||
if(importTimer)clearInterval(importTimer);state.importStep=5;state.importJobStatus='running';state.importProgress=0;state.importProcessed=0;renderT08(screen('OMS-ORD-003'));
|
if(importTimer)clearInterval(importTimer);state.importStep=5;state.importJobStatus='running';state.importProgress=0;state.importProcessed=0;renderT08(screen('OMS-ORD-003'));
|
||||||
@@ -654,14 +696,18 @@
|
|||||||
const next=$('#importNext');if(next)next.onclick=()=>{if(state.importStep===1&&!state.importFileName)return;state.importStep=Math.min(5,state.importStep+1);renderT08(screen('OMS-ORD-003'));};
|
const next=$('#importNext');if(next)next.onclick=()=>{if(state.importStep===1&&!state.importFileName)return;state.importStep=Math.min(5,state.importStep+1);renderT08(screen('OMS-ORD-003'));};
|
||||||
const back=$('#importBack');if(back)back.onclick=()=>{state.importStep=Math.max(1,state.importStep-1);renderT08(screen('OMS-ORD-003'));};
|
const back=$('#importBack');if(back)back.onclick=()=>{state.importStep=Math.max(1,state.importStep-1);renderT08(screen('OMS-ORD-003'));};
|
||||||
const commit=$('#importCommit');if(commit)commit.onclick=startImportJob;const result=$('#importResult');if(result)result.onclick=()=>{state.importStep=6;renderT08(screen('OMS-ORD-003'));};
|
const commit=$('#importCommit');if(commit)commit.onclick=startImportJob;const result=$('#importResult');if(result)result.onclick=()=>{state.importStep=6;renderT08(screen('OMS-ORD-003'));};
|
||||||
const reset=$('#importReset');if(reset)reset.onclick=()=>{state.importStep=1;state.importFileName='';state.importFileSize=0;state.importFileType='';state.importJobStatus='idle';state.importProgress=0;state.importProcessed=0;renderT08(screen('OMS-ORD-003'));};
|
const firstError=$('#importFirstError');if(firstError)firstError.onclick=()=>{state.importErrorIndex=0;renderT08(screen('OMS-ORD-003'));setTimeout(()=>$('[data-import-error-row="0"]',workspace)?.focus(),0);};
|
||||||
|
const nextError=$('#importNextError');if(nextError)nextError.onclick=()=>{state.importErrorIndex=(state.importErrorIndex+1)%2;renderT08(screen('OMS-ORD-003'));setTimeout(()=>$(`[data-import-error-row="${state.importErrorIndex}"]`,workspace)?.focus(),0);};
|
||||||
|
const reset=$('#importReset');if(reset)reset.onclick=()=>{state.importStep=1;state.importFileName='';state.importFileSize=0;state.importFileType='';state.importJobStatus='idle';state.importProgress=0;state.importProcessed=0;state.importErrorIndex=0;state.importFocusStep=0;renderT08(screen('OMS-ORD-003'));};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderT09(s){
|
function renderT09(s){
|
||||||
const remain=Math.max(0,state.pickingRequired-state.pickingQty);const completed=remain===0;
|
const remain=Math.max(0,state.pickingRequired-state.pickingQty);const completed=remain===0;
|
||||||
const network=state.networkStatus==='online'?['●','온라인 · 동기화 완료','online']:state.networkStatus==='unstable'?['▲','연결 불안정 · 서버 확인 필요','unstable']:['○','오프라인 · 작업 반영 중지','offline'];
|
const network=state.networkStatus==='online'?['●','온라인 · 동기화 완료','online']:state.networkStatus==='unstable'?['▲','연결 불안정 · 서버 확인 필요','unstable']:['○','오프라인 · 작업 반영 중지','offline'];
|
||||||
|
const scanTruth=state.networkStatus!=='online'?'입력 차단 · 온라인 확인 필요':completed?'상품 완료':'스캔 가능';
|
||||||
|
const confirmed=state.lastConfirmedScanValue?`${esc(state.lastConfirmedScanValue)} · ${new Date(state.lastConfirmedScanAt).toLocaleTimeString('ko-KR')}`:'아직 확인된 스캔 없음';
|
||||||
const errorTitle=state.scanError?.startsWith('NETWORK:')?'네트워크를 확인하세요.':'다른 상품입니다.';const errorCopy=(state.scanError||'').replace(/^NETWORK:/,'');
|
const errorTitle=state.scanError?.startsWith('NETWORK:')?'네트워크를 확인하세요.':'다른 상품입니다.';const errorCopy=(state.scanError||'').replace(/^NETWORK:/,'');
|
||||||
workspace.innerHTML=`<div class="mobile-stage"><main class="wms-phone" data-screen-id="${esc(s.id)}"><header class="wms-header"><div><h1>출고 피킹</h1><strong>23 / 138</strong></div><span class="network ${network[2]}">${network[0]} ${network[1]}</span></header><section class="wms-content"><span class="wms-label">LOCATION</span><div class="scan-location">A-03-02</div><p>위치 확인 완료 · 상품을 스캔하세요.</p><div id="scanBox" class="scan-box ${state.scanError?'scan-error':''}" tabindex="0" role="textbox" aria-label="바코드 스캔 입력" aria-describedby="scanHint"><strong>${state.scanBuffer?esc(state.scanBuffer):'SCAN READY'}</strong><small id="scanHint">스캐너 입력 후 Enter · 기대 바코드 8801234567890</small></div><section class="wms-item"><strong>ABC 운동화</strong><p>BLACK / 270</p></section><div class="qty-grid"><div><span>필요</span><b>${state.pickingRequired}</b></div><div><span>피킹</span><b>${state.pickingQty}</b></div><div><span>남음</span><b>${remain}</b></div></div>${completed?'<div class="success-panel"><strong>상품 완료</strong><span>다음 품목으로 자동 진행할 수 있습니다.</span></div>':''}${state.scanError?`<div class="error-panel" role="alert"><strong>${esc(errorTitle)}</strong><span>${esc(errorCopy)}</span></div>`:''}</section><footer class="wms-actions">${btn('+1 피킹','primary','pickOne',completed||state.networkStatus!=='online')}${btn('수량입력','','pickQty',state.networkStatus!=='online')}${btn('재고부족 신고','danger full','stockIssue')}</footer></main></div>`;
|
workspace.innerHTML=`<div class="mobile-stage"><main class="wms-phone" data-screen-id="${esc(s.id)}"><header class="wms-header"><div><h1>출고 피킹</h1><strong>23 / 138</strong></div><span class="network ${network[2]}">${network[0]} ${network[1]}</span></header><section class="wms-content"><span class="wms-label">LOCATION</span><div class="scan-location">A-03-02</div><p>위치 확인 완료 · 상품을 스캔하세요.</p><div class="scan-truth ${state.networkStatus==='online'?'ready':'blocked'}" role="status"><div><span>스캔 상태</span><b>${esc(scanTruth)}</b></div><div><span>최근 서버 확인</span><b>${confirmed}</b></div></div><div id="scanBox" class="scan-box ${state.scanError?'scan-error':''}" tabindex="0" role="textbox" aria-label="바코드 스캔 입력" aria-describedby="scanHint"><strong>${state.scanBuffer?esc(state.scanBuffer):'SCAN READY'}</strong><small id="scanHint">스캐너 입력 후 Enter · 기대 바코드 8801234567890</small></div><section class="wms-item"><strong>ABC 운동화</strong><p>BLACK / 270</p></section><div class="qty-grid"><div><span>필요</span><b>${state.pickingRequired}</b></div><div><span>피킹</span><b>${state.pickingQty}</b></div><div><span>남음</span><b>${remain}</b></div></div>${completed?'<div class="success-panel"><strong>상품 완료</strong><span>다음 품목으로 자동 진행할 수 있습니다.</span></div>':''}${state.scanError?`<div class="error-panel" role="alert"><strong>${esc(errorTitle)}</strong><span>${esc(errorCopy)}</span></div>`:''}</section><footer class="wms-actions">${btn('+1 피킹','primary','pickOne',completed||state.networkStatus!=='online')}${btn('수량입력','','pickQty',state.networkStatus!=='online')}${btn('재고부족 신고','danger full','stockIssue')}</footer></main></div>`;
|
||||||
bindT09();
|
bindT09();
|
||||||
}
|
}
|
||||||
function applyScan(value){
|
function applyScan(value){
|
||||||
@@ -670,7 +716,7 @@
|
|||||||
state.lastScanValue=normalized;state.lastScanAt=now;
|
state.lastScanValue=normalized;state.lastScanAt=now;
|
||||||
if(state.networkStatus!=='online'){state.scanError=`NETWORK:${state.networkStatus==='unstable'?'연결이 불안정하여 서버 검증을 완료하지 못했습니다. 수량은 반영하지 않았습니다.':'오프라인에서는 서버 검증이 필요한 피킹 수량을 반영하지 않습니다.'}`;renderT09(screen('WMS-PICK-001'));setTimeout(()=>$('#scanBox')?.focus(),0);return;}
|
if(state.networkStatus!=='online'){state.scanError=`NETWORK:${state.networkStatus==='unstable'?'연결이 불안정하여 서버 검증을 완료하지 못했습니다. 수량은 반영하지 않았습니다.':'오프라인에서는 서버 검증이 필요한 피킹 수량을 반영하지 않습니다.'}`;renderT09(screen('WMS-PICK-001'));setTimeout(()=>$('#scanBox')?.focus(),0);return;}
|
||||||
if(normalized!=='8801234567890'){state.scanError=`스캔 ${normalized} · 필요 ABC001 / 8801234567890`;renderT09(screen('WMS-PICK-001'));setTimeout(()=>$('#scanBox')?.focus(),0);return;}
|
if(normalized!=='8801234567890'){state.scanError=`스캔 ${normalized} · 필요 ABC001 / 8801234567890`;renderT09(screen('WMS-PICK-001'));setTimeout(()=>$('#scanBox')?.focus(),0);return;}
|
||||||
state.scanError='';if(state.pickingQty<state.pickingRequired)state.pickingQty++;renderT09(screen('WMS-PICK-001'));toast(state.pickingQty===state.pickingRequired?'상품 피킹 완료.':'정상 상품을 1개 피킹했습니다.');setTimeout(()=>$('#scanBox')?.focus(),0);
|
state.scanError='';state.lastConfirmedScanValue=normalized;state.lastConfirmedScanAt=Date.now();if(state.pickingQty<state.pickingRequired)state.pickingQty++;renderT09(screen('WMS-PICK-001'));toast(state.pickingQty===state.pickingRequired?'상품 피킹 완료.':'정상 상품을 1개 피킹했습니다.');setTimeout(()=>$('#scanBox')?.focus(),0);
|
||||||
}
|
}
|
||||||
function bindT09(){
|
function bindT09(){
|
||||||
const scan=$('#scanBox');if(scan){scan.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();applyScan(state.scanBuffer);return;}if(e.key==='Backspace'){state.scanBuffer=state.scanBuffer.slice(0,-1);}else if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){state.scanBuffer+=e.key;}const strong=$('strong',scan);if(strong)strong.textContent=state.scanBuffer||'SCAN READY';};}
|
const scan=$('#scanBox');if(scan){scan.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();applyScan(state.scanBuffer);return;}if(e.key==='Backspace'){state.scanBuffer=state.scanBuffer.slice(0,-1);}else if(e.key.length===1&&!e.ctrlKey&&!e.metaKey&&!e.altKey){state.scanBuffer+=e.key;}const strong=$('strong',scan);if(strong)strong.textContent=state.scanBuffer||'SCAN READY';};}
|
||||||
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |