chore: remove kbx-foundation-v36 reference (superseded by v4 implementation)
Removed entire kbx-foundation-v36 directory as it's been replaced by the new KBX Foundation v4 patterns implemented in this session: - Registry-driven screen definitions - Density-aware UI adapter components - Feature module templates (ShadowRun, Models) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@@ -0,0 +1,113 @@
|
||||
# KBX FE Reference Lab v50
|
||||
|
||||
Backend/API 없이 브라우저에서 즉시 실행하는 KBX 업무 UI 원형이다. 목적은 별도 Demo UI를 만드는 것이 아니라 **T01~T09의 실제 HTML/CSS/JavaScript 상호작용, Theme State, Keyboard, Recovery, WMS Mobile**을 빠르게 재현하는 것이다.
|
||||
|
||||
## 실행
|
||||
|
||||
```bash
|
||||
cd apps/web/fe-reference
|
||||
python3 -m http.server 4173
|
||||
```
|
||||
|
||||
운영 Domain을 흉내 내는 제품이 아니다. Permission, Domain Validation, DB Commit, Audit, Concurrency, Idempotency는 Server 최종 책임이다.
|
||||
|
||||
## v50 Operational Truth / Home Overflow QA
|
||||
|
||||
- Home Attention 기본 8개, 최대 priority surface 24개
|
||||
- 총 건수 >24이면 `전체 보기`라고 과장하지 않고 `우선 24개 보기`
|
||||
- 남은 항목은 `작업 센터 전체 / 알림 센터 전체`로 이동
|
||||
- Work Center open → Esc close → Home trigger Focus restore
|
||||
- KBX Business Light/Dark에서 동일한 Label/Action/Focus 유지
|
||||
- 실제 Template 코드의 non-retryable error는 `[다시 시도]`를 숨기는 공통 계약을 사용
|
||||
- 증적: `docs/evidence/v50-browser-operational-truth.json` (16/16)
|
||||
- Preview: `previews/v50-home-overflow-dark-1440x900.png`
|
||||
|
||||
|
||||
## v46 검증 범위
|
||||
|
||||
- KBX Business Light / Dark + Forced Colors 의미 체계 유지
|
||||
- clean Workspace 위치만 `sessionStorage`에 복원
|
||||
- Query/Hash/Dirty Form·Grid 값은 session snapshot에서 제외
|
||||
- 복원 tab `↺` + Home `복원` 표시, 실제 진입 시 resumed marker 제거
|
||||
- Home/no-active-tab 상태에서도 첫 Workspace Tab keyboard entry 보장
|
||||
- Storage API 차단 시 Preference/Resume 없이 fail-soft 기동
|
||||
- T09 WMS common state boundary의 Error → `[작업 다시 불러오기]` recovery loop
|
||||
- Browser Reference QA 20/20: clean resume, dirty exclusion, Dark Theme, 1280px overflow, runtime error 0
|
||||
|
||||
## v44 검증 범위
|
||||
|
||||
- KBX Business Light / Dark semantic theme
|
||||
- Windows Forced Colors system-color fallback
|
||||
- Home: 확인 필요 → 바로 시작 → 모듈별 업무
|
||||
- Header: 작업/알림 **비모달 System Panel**, Esc, Focus Restore
|
||||
- Help/AI/Suggestion/Detail: Drawer contract 유지
|
||||
- T01: 검색 → Loading/Empty/Error/Ready → 선택/Bulk/Drawer → Empty 복구
|
||||
- T02: Master CRUD + Dirty Guard + Lookup
|
||||
- T03: Header/Detail + Validation-safe F8
|
||||
- T04: Fast Entry/Paste/Fill Down
|
||||
- T05: Master→Location→재고이력
|
||||
- T06: Actionable Queue
|
||||
- T07: Reconcile/Resolution
|
||||
- T08: Excel Import + Background Job
|
||||
- T09: 390×844 WMS Mobile + Scanner/Network/Duplicate Guard
|
||||
|
||||
## Theme 원칙
|
||||
|
||||
Light/Dark는 표현 모드이며 업무 상태나 Permission의 Source of Truth가 아니다. Forced Colors가 활성화되면 OS `Canvas`, `CanvasText`, `Highlight`, `LinkText`를 우선한다. Theme 변경으로 Layout/Density/Keyboard 문법을 변경하지 않는다.
|
||||
|
||||
## QA 증적
|
||||
|
||||
- `previews/v44-home-light-1440x900.png`
|
||||
- `previews/v44-home-dark-1440x900.png`
|
||||
- `previews/v44-runtime-panel-dark-1440x900.png`
|
||||
- `previews/v44-t01-dark-1440x900.png`
|
||||
- `previews/v44-t01-forced-colors-1440x900.png`
|
||||
- `previews/v44-t09-dark-390x844.png`
|
||||
- `docs/evidence/v44-theme-state-browser.json` — 14/14
|
||||
- `docs/evidence/v44-browser-matrix.json` — 28/28
|
||||
|
||||
실행환경의 localhost/file navigation 제약 때문에 Browser QA는 동일 HTML/CSS/JS asset을 Playwright inline harness로 실행했다. 실제 Vue/Vite/PrimeVue/AG Grid runtime E2E와 혼동하지 않는다.
|
||||
|
||||
## 검증
|
||||
|
||||
```bash
|
||||
node scripts/validate-fe-reference.mjs
|
||||
node scripts/validate-fe-state-navigation-v44.mjs
|
||||
node scripts/validate-kbx.mjs
|
||||
```
|
||||
|
||||
|
||||
## v45 Safe Preference QA
|
||||
|
||||
- T01 `기본조건 저장`, `마지막 조회조건 기억` 실제 JavaScript 동작
|
||||
- 판매채널/상태/오류유형만 저장하고 자유검색어는 저장하지 않음
|
||||
- 저장값 allow-list 복원, 잘못된 값은 기본값으로 복구
|
||||
- Light/Dark Theme와 개인화 UI 동시 검증
|
||||
- 증적: `docs/evidence/v45-browser-preference-theme.json` (14/14)
|
||||
|
||||
## v46 Safe Workspace Resume QA
|
||||
|
||||
- clean Workspace tab만 `sessionStorage` 복원
|
||||
- Query/Hash, Dirty Form/Grid 값은 복원 snapshot에서 제외
|
||||
- Home `바로 시작`에 `복원` 상태 표시
|
||||
- active tab이 없는 Home에서도 첫 Workspace tab `tabindex=0`
|
||||
- Storage API 차단 시 Home/Shell fail-soft
|
||||
- KBX Business Light/Dark와 resume marker semantic parity
|
||||
- 증적: `docs/evidence/v46-browser-resume-theme.json` (20/20)
|
||||
- Preview: `previews/v46-home-resume-dark-1440x900.png`
|
||||
|
||||
|
||||
## v47 Actionable Problem
|
||||
|
||||
T01에서 예외 주문을 선택해 출고지시하면 업무규칙 Problem을 인라인으로 표시하고 `문제 N건만 선택`으로 복구한다. 단순 성공 Toast로 거짓 완료시키지 않는다. Light/Dark는 동일 semantic state를 사용한다.
|
||||
|
||||
## v48 Template Completion QA
|
||||
|
||||
- T05 재고현황: 실제 창고/품목 검색, Loading, Empty, 조건 초기화, Master→Location→History
|
||||
- T06 작업 Queue: 실제 작업자/통합검색/빠른필터, Selection, Empty Recovery
|
||||
- T07 Reconcile: 실제 검색/불일치 필터, Selection, Reference 재처리 상태변경
|
||||
- T08 Excel Import: 실제 Browser File input/Drop, `.xlsx/.xls`/20MB guard, 6-step visual flow
|
||||
- Search Panel의 조회 버튼은 Enter/F3뿐 아니라 Mouse click도 동일 실행 경로를 사용한다.
|
||||
- KBX Business Light/Dark semantic theme는 동일 업무 상태와 조작 계약을 유지한다.
|
||||
- Browser 증적: `docs/evidence/v48-browser-template-completion.json` (30/30)
|
||||
- Preview: `previews/v48-t05-inventory-dark-1440x900.png`, `previews/v48-t08-file-dark-1440x900.png`
|
||||
@@ -0,0 +1,123 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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="color-scheme" content="light dark" />
|
||||
<title>KBX FE Reference Lab v52 · Operational Navigation · Screen Anatomy · T01~T09</title>
|
||||
<link rel="stylesheet" href="./kbx-fe.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="kbx-app" data-density="compact" data-nav="expanded" data-module="COMMON">
|
||||
<a class="skip-link" href="#workspace">본문 바로가기</a>
|
||||
|
||||
<header class="global-header">
|
||||
<div class="brand-area">
|
||||
<button id="homeButton" class="brand" type="button" aria-label="KBX 홈으로 이동">
|
||||
<strong>KBX</strong><span>업무 플랫폼</span>
|
||||
</button>
|
||||
<label class="module-select"><span class="sr-only">업무 모듈</span>
|
||||
<select id="moduleSelect"><option value="ALL">전체 모듈</option><option value="OMS">OMS</option><option value="ERP">ERP</option><option value="WMS">WMS</option><option value="COMMON">공통</option></select>
|
||||
</label>
|
||||
</div>
|
||||
<button id="globalSearchButton" class="global-search" type="button" aria-keyshortcuts="Control+K">
|
||||
<span>메뉴명 · 화면코드 · 업무명 검색</span><kbd>Ctrl K</kbd>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button id="jobsButton" type="button" data-panel="jobs" aria-controls="systemPanel" aria-expanded="false">작업 <b>2</b></button>
|
||||
<button id="noticeButton" type="button" data-panel="notice" aria-controls="systemPanel" aria-expanded="false">알림 <b>4</b></button>
|
||||
<button id="densityButton" type="button" title="화면 밀도 전환">Compact</button>
|
||||
<button id="themeButton" type="button" title="KBX Business 테마 전환" aria-label="어두운 테마로 변경"><span aria-hidden="true">☀</span> <span>Light</span></button>
|
||||
<button type="button">사용자</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="shell-body">
|
||||
<aside id="sideNav" class="side-nav" aria-label="업무 메뉴">
|
||||
<div class="side-nav-top">
|
||||
<button id="sideHomeButton" class="nav-home active" type="button"><span aria-hidden="true">⌂</span><span class="nav-label">홈</span></button>
|
||||
<button id="collapseButton" class="collapse-button" type="button" aria-label="메뉴 접기" aria-expanded="true">‹</button>
|
||||
</div>
|
||||
<button id="sideSearchButton" class="side-search" type="button" aria-keyshortcuts="Control+K"><span class="nav-label">메뉴·화면 검색</span><kbd class="nav-label">Ctrl K</kbd><span class="collapsed-only" aria-hidden="true">⌕</span></button>
|
||||
<nav id="moduleSwitcher" class="module-switcher" aria-label="업무 모듈"></nav>
|
||||
<div id="favoriteNav" class="personal-nav"></div>
|
||||
<div id="recentNav" class="personal-nav"></div>
|
||||
<div id="menuNav" class="menu-nav"></div>
|
||||
</aside>
|
||||
|
||||
<main class="workspace-shell">
|
||||
<nav id="workspaceTabs" class="workspace-tabs" role="tablist" aria-label="열린 업무"></nav>
|
||||
<div id="workspace" class="workspace" tabindex="-1"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<dialog id="menuSearchDialog" class="menu-search-dialog" aria-labelledby="menuSearchTitle">
|
||||
<div class="search-dialog-shell">
|
||||
<header><strong id="menuSearchTitle">업무 화면 검색</strong><button id="searchCloseButton" type="button" aria-label="닫기">×</button></header>
|
||||
<label class="search-input-wrap"><span class="sr-only">검색어</span><input id="menuSearchInput" autocomplete="off" placeholder="예: 주문, 재고, OMS-ORD-001" role="combobox" aria-controls="menuSearchResults" /></label>
|
||||
<div id="menuSearchResults" class="menu-search-results" role="listbox" aria-label="검색 결과"></div>
|
||||
<footer><span>↑↓ 이동 · Enter 열기 · Esc 닫기</span><span>화면코드 검색 지원</span></footer>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="unsavedDialog" class="confirm-dialog" aria-labelledby="unsavedTitle">
|
||||
<form method="dialog" class="confirm-shell">
|
||||
<header><strong id="unsavedTitle">저장하지 않은 변경사항이 있습니다.</strong></header>
|
||||
<p id="unsavedMessage">변경사항을 저장하거나 버린 뒤 화면을 닫을 수 있습니다.</p>
|
||||
<footer>
|
||||
<button id="unsavedStay" value="cancel" class="btn" type="submit">계속 편집</button>
|
||||
<button id="unsavedDiscard" value="discard" class="btn danger" type="button">변경 버리기</button>
|
||||
<button id="unsavedSave" value="save" class="btn primary" type="button">저장 후 닫기</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
<dialog id="lookupDialog" class="lookup-dialog" aria-labelledby="lookupTitle">
|
||||
<div class="lookup-dialog-shell">
|
||||
<header><div><strong id="lookupTitle">업무 조회</strong><small id="lookupSubtitle">F2 조회</small></div><button id="lookupCloseButton" type="button" aria-label="닫기">×</button></header>
|
||||
<form id="lookupSearchForm" class="lookup-search">
|
||||
<label><span class="sr-only">검색어</span><input id="lookupSearchInput" autocomplete="off" maxlength="80" placeholder="코드 또는 명칭" /></label>
|
||||
<button class="btn" type="submit">조회</button>
|
||||
</form>
|
||||
<div id="lookupResults" class="lookup-results" role="listbox" aria-label="조회 결과"></div>
|
||||
<footer><span id="lookupStatus">↑↓ 이동 · Enter 선택 · Esc 닫기</span><button id="lookupSelectButton" class="btn primary" type="button">선택</button></footer>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="quantityDialog" class="confirm-dialog" aria-labelledby="quantityTitle">
|
||||
<form method="dialog" class="confirm-shell" id="quantityForm">
|
||||
<header><strong id="quantityTitle">피킹 수량 입력</strong></header>
|
||||
<label class="dialog-field"><span>수량</span><input id="quantityInput" type="number" inputmode="numeric" min="0" step="1" /></label>
|
||||
<p class="dialog-help" id="quantityHelp">필요수량을 초과할 수 없습니다.</p>
|
||||
<footer><button class="btn" value="cancel" type="submit">취소</button><button id="quantityApply" class="btn primary" type="button">적용</button></footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="exceptionDialog" class="confirm-dialog" aria-labelledby="exceptionTitle">
|
||||
<form method="dialog" class="confirm-shell" id="exceptionForm">
|
||||
<header><strong id="exceptionTitle">재고부족 신고</strong></header>
|
||||
<fieldset class="exception-options"><legend>사유</legend><label><input type="radio" name="exceptionReason" value="실물 없음" checked /> 실물 없음</label><label><input type="radio" name="exceptionReason" value="수량 부족" /> 수량 부족</label><label><input type="radio" name="exceptionReason" value="위치 오류" /> 위치 오류</label><label><input type="radio" name="exceptionReason" value="상품 이상" /> 상품 이상</label></fieldset>
|
||||
<label class="dialog-field"><span>메모</span><textarea id="exceptionMemo" rows="3" maxlength="200" placeholder="선택 입력"></textarea></label>
|
||||
<footer><button class="btn" value="cancel" type="submit">취소</button><button id="exceptionApply" class="btn primary" type="button">등록</button></footer>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<aside id="systemPanel" class="system-panel" hidden tabindex="-1" role="region" aria-labelledby="systemPanelTitle">
|
||||
<header><strong id="systemPanelTitle">업무 센터</strong><button id="systemPanelClose" type="button" aria-label="패널 닫기">×</button></header>
|
||||
<div id="systemPanelBody" class="system-panel-body"></div>
|
||||
</aside>
|
||||
|
||||
<aside id="drawer" class="drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" tabindex="-1">
|
||||
<header><strong id="drawerTitle">상세</strong><button id="drawerClose" type="button" aria-label="닫기">×</button></header>
|
||||
<div id="drawerBody" class="drawer-body"></div>
|
||||
</aside>
|
||||
<div id="drawerBackdrop" class="drawer-backdrop" hidden></div>
|
||||
<div id="toastRegion" class="toast-region" aria-live="polite" aria-atomic="true"></div>
|
||||
<div id="liveRegion" class="sr-only" aria-live="polite" aria-atomic="true"></div>
|
||||
</div>
|
||||
<script src="./kbx-fe.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,151 @@
|
||||
:root{
|
||||
--blue-50:#eff6ff;--blue-100:#dbeafe;--blue-600:#2563eb;--blue-700:#1d4ed8;
|
||||
--gray-0:#fff;--gray-25:#fcfcfd;--gray-50:#f8fafc;--gray-100:#f1f5f9;--gray-200:#e2e8f0;--gray-300:#cbd5e1;--gray-400:#94a3b8;--gray-500:#64748b;--gray-600:#475569;--gray-700:#334155;--gray-800:#1e293b;--gray-900:#0f172a;
|
||||
--red-50:#fff1f2;--red-200:#fecdd3;--red-600:#e11d48;--amber-50:#fffbeb;--amber-200:#fde68a;--amber-700:#b45309;--green-50:#f0fdf4;--green-200:#bbf7d0;--green-700:#15803d;
|
||||
--surface:var(--gray-0);--muted:var(--gray-50);--border:var(--gray-200);--border-strong:var(--gray-300);--text:var(--gray-900);--text-muted:var(--gray-500);--primary:var(--blue-600);--danger:var(--red-600);--warning:var(--amber-700);--success:var(--green-700);
|
||||
--space-1:4px;--space-2:8px;--space-3:12px;--space-4:16px;--space-5:20px;--space-6:24px;--radius:4px;--shadow:0 12px 34px rgba(15,23,42,.14);
|
||||
--header-h:56px;--tabs-h:40px;--nav-w:220px;--nav-collapsed:56px;--control-h:34px;--row-h:34px;--font:14px;--grid-font:13px;
|
||||
}
|
||||
.kbx-app[data-theme="dark"]{
|
||||
color-scheme:dark;
|
||||
--surface:#111827;--muted:#0b1220;--border:#334155;--border-strong:#64748b;--text:#e5e7eb;--text-muted:#94a3b8;--primary:#60a5fa;--danger:#fb7185;--warning:#fbbf24;--success:#34d399;
|
||||
--blue-50:#102a43;--blue-100:#153b5c;--gray-0:#111827;--gray-25:#111827;--gray-50:#0b1220;--gray-100:#172033;--gray-200:#334155;--gray-300:#475569;--gray-400:#64748b;--gray-500:#94a3b8;--gray-600:#cbd5e1;--gray-700:#e2e8f0;--gray-800:#e5e7eb;--gray-900:#f8fafc;
|
||||
--red-50:#3b1118;--red-200:#9f1239;--red-600:#fb7185;--amber-50:#33270b;--amber-200:#854d0e;--amber-700:#fbbf24;--green-50:#062e24;--green-200:#166534;--green-700:#34d399;--shadow:0 18px 48px rgba(0,0,0,.42);
|
||||
}
|
||||
*{box-sizing:border-box}html,body,#app{height:100%;margin:0}body{font-family:Pretendard,"Apple SD Gothic Neo","Noto Sans KR",Arial,sans-serif;font-size:var(--font);color:var(--text);background:var(--muted)}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.skip-link{position:fixed;top:-60px;left:8px;z-index:999;padding:8px 12px;background:var(--text);color:#fff;border-radius:4px}.skip-link:focus{top:8px}
|
||||
.kbx-app{height:100%;display:flex;flex-direction:column;color:var(--text);background:var(--muted)}.kbx-app[data-density="comfortable"]{--control-h:38px;--row-h:40px;--font:14px;--grid-font:14px}
|
||||
.global-header{height:var(--header-h);display:grid;grid-template-columns:var(--nav-w) minmax(320px,620px) 1fr;align-items:center;gap:16px;padding:0 12px;border-bottom:1px solid var(--border);background:var(--surface);z-index:40}.brand-area{display:flex;align-items:center;gap:8px;min-width:0}.brand{height:36px;border:0;background:transparent;display:flex;align-items:baseline;gap:8px;padding:0 8px;border-radius:4px}.brand:hover{background:var(--muted)}.brand strong{font-size:17px}.brand span{font-size:12px;color:var(--text-muted)}.module-select{padding-left:8px;border-left:1px solid var(--border)}.module-select select{height:32px;width:88px;border:1px solid var(--border);border-radius:4px;background:var(--surface);padding:0 8px}.global-search{height:36px;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--border-strong);border-radius:4px;background:var(--muted);color:var(--text-muted);padding:0 12px;text-align:left}.global-search:hover{border-color:var(--gray-400);background:var(--surface)}.global-search kbd,.side-search kbd{font-size:11px;border:1px solid var(--border);border-radius:3px;padding:2px 5px;background:var(--surface)}.header-actions{display:flex;justify-content:flex-end;gap:4px}.header-actions button{height:32px;border:0;background:transparent;border-radius:4px;padding:0 9px}.header-actions button:hover{background:var(--muted)}.header-actions b{color:var(--primary);margin-left:3px}
|
||||
.shell-body{flex:1;min-height:0;display:flex}.side-nav{width:var(--nav-w);min-width:var(--nav-w);padding:8px;border-right:1px solid var(--border);background:var(--surface);overflow:auto;transition:width .16s ease,min-width .16s ease}.side-nav.collapsed{width:var(--nav-collapsed);min-width:var(--nav-collapsed);overflow:hidden}.side-nav-top{position:sticky;top:-8px;z-index:3;display:flex;gap:4px;padding:8px 0 8px;background:var(--surface)}.nav-home{flex:1;height:32px;display:flex;align-items:center;gap:8px;padding:0 8px;border:0;border-radius:4px;background:transparent;text-align:left}.nav-home:hover,.nav-home.active{background:var(--blue-50);color:var(--primary)}.collapse-button{width:28px;height:28px;border:1px solid var(--border);border-radius:4px;background:var(--surface)}.side-search{width:100%;height:34px;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--border);border-radius:4px;background:var(--muted);color:var(--text-muted);padding:0 8px}.module-switcher{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;margin-top:8px}.module-switcher button{height:28px;border:1px solid var(--border);border-radius:4px;background:var(--surface);font-size:12px}.module-switcher button.active{border-color:var(--primary);color:var(--primary);background:var(--blue-50);font-weight:600}.personal-nav,.menu-nav{margin-top:12px}.personal-nav h2,.menu-group h2{margin:0 8px 4px;font-size:12px;font-weight:600;color:var(--text-muted)}.nav-row{display:flex;align-items:center;border-radius:4px}.nav-row:hover{background:var(--muted)}.nav-row .nav-entry{min-width:0;flex:1;height:32px;border:0;background:transparent;text-align:left;padding:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nav-row .nav-entry.active{color:var(--primary);font-weight:600}.nav-row .star{width:28px;height:28px;border:0;background:transparent;color:var(--gray-400);border-radius:4px}.nav-row .star.on{color:var(--primary)}.collapsed .nav-label,.collapsed .personal-nav,.collapsed .menu-nav h2,.collapsed .nav-row .star{display:none}.collapsed .side-nav-top{flex-direction:column}.collapsed .nav-home{width:38px;flex:none;justify-content:center;padding:0}.collapsed .collapse-button{width:38px}.collapsed .side-search{width:38px;justify-content:center;padding:0}.collapsed .module-switcher{grid-template-columns:1fr}.collapsed .module-switcher button{width:38px;height:34px}.collapsed .menu-nav{display:block}.collapsed .menu-group{margin:0}.collapsed .nav-row .nav-entry{width:38px;flex:none;text-align:center;padding:0;font-size:0}.collapsed .nav-row .nav-entry::first-letter{font-size:12px}.collapsed-only{display:none}.collapsed .collapsed-only{display:inline}
|
||||
.workspace-shell{flex:1;min-width:0;display:flex;flex-direction:column}.workspace-tabs{height:var(--tabs-h);display:flex;align-items:flex-end;gap:1px;padding:0 8px;border-bottom:1px solid var(--border);background:var(--muted);overflow-x:auto}.tab-shell{height:36px;display:flex;align-items:center;max-width:220px;border:1px solid transparent;border-bottom:0}.tab-shell.active{border-color:var(--border);background:var(--surface)}.tab-main{min-width:0;height:100%;display:flex;align-items:center;gap:6px;border:0;background:transparent;padding:0 8px;color:var(--text-muted)}.tab-shell.active .tab-main{color:var(--text);font-weight:500}.tab-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tab-close,.tab-pin{width:26px;height:100%;border:0;background:transparent;color:var(--text-muted)}.tab-close:hover,.tab-pin:hover{background:var(--muted)}.tab-dirty{color:var(--warning);font-size:10px}.workspace-tabs .empty-tabs{margin:auto;color:var(--text-muted);font-size:12px;padding-bottom:9px}.workspace{flex:1;min-height:0;overflow:auto;background:var(--surface)}
|
||||
.home-page{max-width:1180px;margin:0 auto;padding:20px;display:flex;flex-direction:column;gap:16px}.home-heading{display:grid;grid-template-columns:1fr minmax(320px,520px);align-items:center;gap:24px;min-height:76px;border-bottom:1px solid var(--border)}.home-heading p,.home-heading small{margin:0;color:var(--text-muted);font-size:12px}.home-heading h1{margin:1px 0 3px;font-size:20px}.home-search{height:40px;display:flex;align-items:center;justify-content:space-between;border:1px solid var(--border-strong);background:var(--surface);border-radius:4px;padding:0 12px;color:var(--text-muted)}.attention-zone{border:1px solid var(--amber-200);background:var(--amber-50)}.section-head{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 12px;border-bottom:1px solid var(--border);background:var(--muted)}.attention-zone .section-head{background:transparent;border-color:var(--amber-200)}.section-head h2{font-size:16px;margin:0}.section-head small{color:var(--text-muted)}.attention-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px;padding:8px}.attention-item{min-height:44px;display:grid;grid-template-columns:72px minmax(0,1fr) auto 14px;align-items:center;gap:8px;border:1px solid var(--border);border-radius:4px;background:var(--surface);padding:4px 8px;text-align:left}.attention-item.critical{border-color:var(--red-200);background:var(--red-50)}.attention-source{font-size:12px;font-weight:600;color:var(--text-muted)}.attention-item.critical .attention-source{color:var(--danger)}.attention-copy{min-width:0;display:grid}.attention-copy b,.attention-copy small{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.attention-copy b{font-weight:500}.attention-copy small{font-size:12px;color:var(--text-muted)}.attention-action{font-size:12px;color:var(--text-muted)}.home-section{border:1px solid var(--border);background:var(--surface)}.continue-list{display:flex;flex-direction:column}.continue-item{min-height:42px;display:grid;grid-template-columns:72px minmax(0,1fr) auto 14px;align-items:center;gap:8px;border:0;border-bottom:1px solid var(--border);background:var(--surface);padding:0 12px;text-align:left}.continue-item:last-child{border-bottom:0}.continue-item:hover{background:var(--muted)}.continue-source{font-size:12px;color:var(--text-muted);font-weight:600}.continue-item[data-source="dirty"] .continue-source{color:var(--warning)}.continue-item[data-source="favorite"] .continue-source{color:var(--primary)}.continue-copy{display:grid;min-width:0}.continue-copy small{color:var(--text-muted);font-size:12px}.work-explorer{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.module-column{border-right:1px solid var(--border);border-bottom:1px solid var(--border)}.module-column:nth-child(2n){border-right:0}.module-title{height:44px;display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--border)}.module-title strong{font-size:16px}.module-title span,.module-title small{font-size:12px;color:var(--text-muted)}.module-title small{margin-left:auto}.group-block{padding:8px}.group-block h3{font-size:12px;color:var(--text-muted);margin:0 4px 4px}.home-menu-row{display:flex;align-items:center;min-height:36px;border-radius:4px}.home-menu-row:hover{background:var(--muted)}.home-menu-row .open-screen{flex:1;min-width:0;height:36px;border:0;background:transparent;text-align:left;padding:0 8px;display:flex;align-items:center;justify-content:space-between}.home-menu-row .open-screen small{font-size:12px;color:var(--text-muted)}.home-menu-row .fav{width:28px;height:28px;border:0;background:transparent;color:var(--gray-400)}.home-menu-row .fav.on{color:var(--primary)}
|
||||
.screen{height:100%;min-height:0;display:flex;flex-direction:column;background:var(--surface)}.page-header{min-height:48px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:4px 16px;border-bottom:1px solid var(--border)}.page-identity{min-width:0}.breadcrumb{font-size:12px;color:var(--text-muted);margin-bottom:2px}.title-row{display:flex;align-items:center;gap:8px}.title-row h1{font-size:20px;margin:0;font-weight:600}.status-chip,.dirty-chip{font-size:12px;border:1px solid var(--border);border-radius:4px;padding:2px 6px;background:var(--muted)}.dirty-chip{color:var(--warning);border-color:var(--amber-200);background:var(--amber-50)}.page-tools{display:flex;gap:4px}.page-tools button{height:28px;border:1px solid var(--border);border-radius:4px;background:var(--surface);padding:0 8px;font-size:12px}.command-bar{min-height:44px;display:flex;align-items:center;gap:8px;padding:0 16px;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:6}.command-group{display:flex;gap:6px}.command-separator{height:22px;width:1px;background:var(--border)}.btn{height:var(--control-h);border:1px solid var(--border-strong);border-radius:4px;background:var(--surface);padding:0 12px}.btn:hover{background:var(--muted)}.btn.primary{border-color:var(--primary);background:var(--primary);color:#fff}.btn.danger{border-color:var(--red-200);color:var(--danger);background:var(--red-50)}.btn:disabled{opacity:.5;cursor:not-allowed}.screen-body{flex:1;min-height:0;display:flex;flex-direction:column;gap:8px;padding:8px 16px 12px}.search-panel{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px 12px;padding:8px;border:1px solid var(--border);background:var(--muted)}.search-fields{display:flex;align-items:center;gap:8px 16px;flex-wrap:wrap}.field-inline{display:flex;align-items:center;gap:6px}.field-inline>span{font-size:13px;color:var(--gray-700);white-space:nowrap}.control{height:var(--control-h);border:1px solid var(--border-strong);border-radius:4px;background:var(--surface);padding:0 8px;min-width:110px}.control.medium{min-width:180px}.control.wide{min-width:240px}.search-actions{display:flex;gap:4px}.quick-filter{display:flex;align-items:stretch;border:1px solid var(--border);background:var(--surface)}.quick-filter button{min-width:96px;min-height:44px;border:0;border-right:1px solid var(--border);background:var(--surface);text-align:left;padding:5px 10px}.quick-filter button:last-child{border-right:0}.quick-filter button.active{box-shadow:inset 0 -2px var(--primary);background:var(--blue-50)}.quick-filter b{display:block;font-size:16px}.quick-filter small{color:var(--text-muted)}.quick-filter .danger b{color:var(--danger)}.context-bar{min-height:34px;display:flex;align-items:center;gap:12px;padding:0 10px;border:1px solid var(--border);background:var(--muted);font-size:12px}.context-bar strong{font-size:13px}.bulk-bar[hidden]{display:none!important}.bulk-bar{min-height:42px;display:flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--blue-100);background:var(--blue-50)}.bulk-bar strong{margin-right:6px}.grid-wrap{flex:1;min-height:220px;border:1px solid var(--border);overflow:auto;background:var(--surface)}.data-grid{width:100%;border-collapse:separate;border-spacing:0;font-size:var(--grid-font)}.data-grid th,.data-grid td{height:var(--row-h);padding:0 8px;border-right:1px solid var(--border);border-bottom:1px solid var(--border);white-space:nowrap}.data-grid th{height:36px;position:sticky;top:0;z-index:2;background:var(--muted);font-weight:600;text-align:left}.data-grid th:last-child,.data-grid td:last-child{border-right:0}.data-grid tr:hover td{background:var(--blue-50)}.data-grid .num{text-align:right}.data-grid .center{text-align:center}.data-grid .link{color:var(--primary);text-decoration:none}.data-grid .warn{color:var(--warning);font-weight:600}.data-grid .error{color:var(--danger);font-weight:600}.data-grid td[contenteditable="true"]{background:var(--amber-50);outline:0}.data-grid td[contenteditable="true"]:focus{box-shadow:inset 0 0 0 2px var(--primary)}.summary-bar{min-height:34px;display:flex;align-items:center;gap:18px;padding:0 10px;border:1px solid var(--border);border-top:0;color:var(--text-muted);font-size:12px}.summary-bar strong{color:var(--text)}
|
||||
.form-section{border:1px solid var(--border);background:var(--surface)}.form-section h2{height:38px;display:flex;align-items:center;margin:0;padding:0 12px;border-bottom:1px solid var(--border);background:var(--muted);font-size:15px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 20px;padding:12px}.form-field{display:grid;grid-template-columns:96px minmax(0,1fr);align-items:center;gap:8px;min-height:34px}.form-field label{font-size:13px;color:var(--gray-700)}.form-field .required::after{content:" *";color:var(--danger)}.lookup{display:grid;grid-template-columns:100px minmax(120px,1fr) 58px;gap:4px}.lookup input,.form-field input,.form-field select{height:var(--control-h);border:1px solid var(--border-strong);border-radius:4px;background:var(--surface);padding:0 8px;min-width:0}.lookup button{height:var(--control-h);border:1px solid var(--border-strong);border-radius:4px;background:var(--surface)}.transaction-layout{display:flex;flex-direction:column;gap:8px;min-height:0;flex:1}.master-layout{display:grid;grid-template-columns:minmax(280px,32%) minmax(0,1fr);gap:8px;min-height:0;flex:1}.master-pane,.detail-pane{border:1px solid var(--border);min-height:0;overflow:auto}.master-pane .search-panel{border:0;border-bottom:1px solid var(--border)}.detail-pane{padding:12px;display:flex;flex-direction:column;gap:10px}.split-layout{display:grid;grid-template-columns:minmax(260px,40%) minmax(0,1fr);gap:8px;min-height:0;flex:1}.pane{border:1px solid var(--border);min-height:0;overflow:auto}.pane-header{height:38px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;border-bottom:1px solid var(--border);background:var(--muted)}.queue-summary{display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--border)}.queue-summary button{min-height:58px;border:0;border-right:1px solid var(--border);background:var(--surface);text-align:left;padding:7px 10px}.queue-summary button:last-child{border-right:0}.queue-summary b{display:block;font-size:18px}.queue-summary .danger b{color:var(--danger)}.reconcile-summary{display:flex;gap:20px;padding:8px 10px;border:1px solid var(--border);background:var(--muted);font-size:13px}.reconcile-summary .mismatch strong{color:var(--danger)}.import-steps{display:grid;grid-template-columns:repeat(6,1fr);border:1px solid var(--border)}.import-step{min-height:42px;display:flex;align-items:center;justify-content:center;gap:6px;border-right:1px solid var(--border);background:var(--muted);font-size:13px}.import-step:last-child{border-right:0}.import-step.active{background:var(--blue-50);color:var(--primary);font-weight:600}.drop-zone{min-height:230px;border:1px dashed var(--border-strong);display:grid;place-items:center;text-align:center;background:var(--gray-25)}.drop-zone strong{display:block;font-size:16px;margin-bottom:8px}.validation-cards{display:flex;gap:8px}.validation-card{min-width:120px;border:1px solid var(--border);padding:8px 10px}.validation-card b{display:block;font-size:18px}.validation-card.error b{color:var(--danger)}
|
||||
.mobile-stage{background:var(--gray-100);padding:20px;min-height:100%;display:flex;justify-content:center;align-items:flex-start}.wms-phone{width:390px;min-height:720px;background:var(--surface);border:1px solid var(--border-strong);box-shadow:0 8px 24px rgba(15,23,42,.1);display:flex;flex-direction:column}.wms-header{min-height:64px;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--border)}.wms-header h1{font-size:20px;margin:0}.network{font-size:12px;color:var(--success)}.wms-content{flex:1;padding:16px;display:flex;flex-direction:column;gap:14px}.scan-location{font-size:36px;font-weight:700;letter-spacing:.02em}.scan-box{height:110px;border:2px solid var(--border-strong);display:grid;place-items:center;font-weight:700;background:var(--muted)}.qty-grid{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--border)}.qty-grid div{text-align:center;padding:10px;border-right:1px solid var(--border)}.qty-grid div:last-child{border-right:0}.qty-grid b{display:block;font-size:24px}.wms-actions{position:sticky;bottom:0;display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:12px 16px;border-top:1px solid var(--border);background:var(--surface)}.wms-actions .btn{height:54px;font-size:16px;font-weight:600}.wms-actions .full{grid-column:1/-1}.error-panel{border:1px solid var(--red-200);background:var(--red-50);padding:12px}.error-panel strong{display:block;color:var(--danger);margin-bottom:4px}
|
||||
.menu-search-dialog{width:min(680px,calc(100vw - 32px));padding:0;border:1px solid var(--border-strong);border-radius:6px;box-shadow:var(--shadow)}.menu-search-dialog::backdrop{background:rgba(15,23,42,.35)}.search-dialog-shell header{height:46px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--border)}.search-dialog-shell header button{width:30px;height:30px;border:0;background:transparent;font-size:20px}.search-input-wrap{display:block;padding:10px;border-bottom:1px solid var(--border)}.search-input-wrap input{width:100%;height:40px;border:1px solid var(--border-strong);border-radius:4px;padding:0 12px}.menu-search-results{max-height:420px;overflow:auto}.search-result{width:100%;min-height:48px;display:grid;grid-template-columns:82px minmax(0,1fr) auto;align-items:center;gap:10px;border:0;border-bottom:1px solid var(--border);background:var(--surface);padding:5px 12px;text-align:left}.search-result.active,.search-result:hover{background:var(--blue-50)}.search-result .code{font-size:11px;color:var(--text-muted)}.search-result .copy{display:grid}.search-result .copy small{color:var(--text-muted);font-size:12px}.search-result .module{font-size:12px;color:var(--text-muted)}.search-dialog-shell footer{height:34px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:var(--muted);color:var(--text-muted);font-size:11px}
|
||||
.drawer{position:fixed;z-index:90;top:56px;right:0;bottom:0;width:min(560px,92vw);background:var(--surface);border-left:1px solid var(--border);box-shadow:-10px 0 24px rgba(15,23,42,.1);transform:translateX(100%);transition:transform .18s ease;display:flex;flex-direction:column}.drawer.open{transform:translateX(0)}.drawer header{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;border-bottom:1px solid var(--border)}.drawer header button{width:30px;height:30px;border:0;background:transparent;font-size:20px}.drawer-body{flex:1;overflow:auto;padding:14px}.detail-list{display:grid;gap:0;border:1px solid var(--border)}.detail-row{display:grid;grid-template-columns:110px 1fr;min-height:36px;border-bottom:1px solid var(--border);align-items:center}.detail-row:last-child{border-bottom:0}.detail-row span{color:var(--text-muted);padding:0 10px}.detail-row strong{font-weight:500}.toast-region{position:fixed;right:16px;bottom:16px;z-index:120;display:grid;gap:8px}.toast{min-width:260px;max-width:420px;padding:10px 12px;border:1px solid var(--border-strong);background:var(--surface);box-shadow:var(--shadow);border-radius:4px}.toast.success{border-color:var(--green-200);background:var(--green-50)}.toast.error{border-color:var(--red-200);background:var(--red-50)}
|
||||
button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[contenteditable="true"]:focus-visible{outline:2px solid var(--primary);outline-offset:1px}
|
||||
@media(max-width:1100px){.global-header{grid-template-columns:var(--nav-w) minmax(260px,1fr) auto}.header-actions button:nth-child(-n+2){display:none}.work-explorer{grid-template-columns:1fr}.module-column{border-right:0}.attention-grid{grid-template-columns:1fr}.queue-summary{grid-template-columns:repeat(3,1fr)}}
|
||||
@media(max-width:820px){.global-header{grid-template-columns:auto 1fr auto}.module-select,.brand span,.global-search kbd,.header-actions button:not(#densityButton):not(#themeButton){display:none}.side-nav{width:56px;min-width:56px}.side-nav .nav-label,.side-nav .personal-nav,.side-nav .menu-nav h2,.side-nav .nav-row .star{display:none}.side-nav .module-switcher{grid-template-columns:1fr}.home-heading{grid-template-columns:1fr}.home-page{padding:12px}.form-grid{grid-template-columns:1fr}.master-layout,.split-layout{grid-template-columns:1fr}.master-pane{max-height:280px}}
|
||||
|
||||
/* v38 FE interaction hardening */
|
||||
.section-head>div{display:grid;gap:2px}.section-head>.section-actions{display:flex;grid-auto-flow:column;align-items:center;gap:8px}.section-count{font-size:12px;color:var(--text-muted);white-space:nowrap}.section-toggle{min-height:28px;padding:0 8px;border:1px solid var(--amber-200);border-radius:4px;background:var(--surface);color:var(--text);font-size:12px;font-weight:600}.section-toggle:hover{background:var(--muted)}.attention-centers{display:flex;align-items:center;gap:4px}.attention-centers .section-toggle{border-color:var(--border)}.page-tools button[aria-pressed="true"]{color:var(--primary);border-color:var(--blue-100);background:var(--blue-50)}
|
||||
.search-panel.expanded{align-items:start}.search-advanced{grid-column:1/-1;display:flex;align-items:center;gap:8px 16px;padding-top:8px;border-top:1px solid var(--border)}.checkbox-inline{min-height:var(--control-h)}
|
||||
.data-state{min-height:220px;display:grid;place-items:center;align-content:center;gap:7px;border:1px solid var(--border);background:var(--gray-25);text-align:center;padding:24px}.data-state strong{font-size:16px}.data-state span{max-width:560px;color:var(--text-muted);font-size:13px}.data-state.error{border-color:var(--red-200);background:var(--red-50)}.data-state.error strong{color:var(--danger)}.data-state.loading strong::before{content:"";display:inline-block;width:12px;height:12px;margin-right:8px;border:2px solid var(--gray-300);border-top-color:var(--primary);border-radius:50%;vertical-align:-2px;animation:kbx-spin .7s linear infinite}@keyframes kbx-spin{to{transform:rotate(360deg)}}
|
||||
.row-link,.text-action{border:0;background:transparent;color:var(--primary);padding:0;text-align:left}.row-link:hover,.text-action:hover{text-decoration:underline}.selected-row td{background:var(--blue-50)!important}.checkbox-control{display:flex;align-items:center;gap:6px;font-size:13px}.detail-status{display:flex;gap:6px;margin-bottom:10px}.status-chip.danger{border-color:var(--red-200);background:var(--red-50);color:var(--danger)}
|
||||
.workflow-strip{min-height:38px;display:flex;align-items:center;gap:0;border:1px solid var(--border);background:var(--muted);font-size:12px}.workflow-strip span{padding:0 10px;border-right:1px solid var(--border);color:var(--text-muted)}.workflow-strip span.active{color:var(--primary);font-weight:600}.workflow-strip strong{margin-left:auto;padding:0 10px;font-weight:500}.detail-grid-section{display:flex;min-height:230px;flex:1;flex-direction:column;border:1px solid var(--border)}.detail-grid-section .grid-wrap{border:0}.cell-editor,.fast-cell{width:100%;height:calc(var(--row-h) - 6px);border:1px solid transparent;background:transparent;padding:0 4px}.cell-editor:hover,.fast-cell:hover{border-color:var(--border-strong);background:var(--surface)}.cell-editor:focus,.fast-cell:focus{border-color:var(--primary);background:var(--surface);outline:1px solid var(--primary)}.cell-editor.num,.fast-cell.num{text-align:right}.fast-cell.invalid{border-color:var(--red-200);background:var(--red-50)}
|
||||
.history-pane{min-height:190px}.queue-summary button.active{box-shadow:inset 0 -2px var(--primary);background:var(--blue-50)}.queue-summary button.danger.active{box-shadow:inset 0 -2px var(--danger);background:var(--red-50)}.context-bar .text-action{margin-left:auto}
|
||||
.inline-actions,.wizard-actions{display:flex;align-items:center;gap:6px}.inline-actions{padding:8px;border:1px solid var(--border);border-top:0}.wizard-actions{justify-content:flex-end;padding-top:8px}.import-step.done{background:var(--green-50);color:var(--success)}.drop-zone.dragging{border-color:var(--primary);background:var(--blue-50)}.commit-panel{max-width:760px;width:100%;margin:8px auto 0}.commit-body{padding:16px}.commit-body dl{display:flex;gap:8px;margin:12px 0}.commit-body dl div{min-width:130px;border:1px solid var(--border);padding:8px}.commit-body dt{font-size:12px;color:var(--text-muted)}.commit-body dd{margin:2px 0 0;font-size:18px;font-weight:600}.warning-copy{padding:8px;border:1px solid var(--amber-200);background:var(--amber-50);color:var(--warning)}
|
||||
.wms-label{font-size:12px;color:var(--text-muted);font-weight:600}.wms-item p{margin:4px 0 0;color:var(--text-muted)}.scan-box{cursor:text;text-align:center}.scan-box strong{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis}.scan-box small{display:block;margin-top:6px;color:var(--text-muted);font-weight:400}.scan-box.scan-error{border-color:var(--danger);background:var(--red-50)}.success-panel{display:grid;gap:4px;padding:10px 12px;border:1px solid var(--green-200);background:var(--green-50)}.success-panel strong{color:var(--success)}
|
||||
.confirm-dialog{width:min(480px,calc(100vw - 32px));padding:0;border:1px solid var(--border-strong);border-radius:6px;box-shadow:var(--shadow)}.confirm-dialog::backdrop{background:rgba(15,23,42,.35)}.confirm-shell{margin:0}.confirm-shell>header{min-height:48px;display:flex;align-items:center;padding:0 14px;border-bottom:1px solid var(--border)}.confirm-shell>p,.dialog-field,.exception-options{margin:0;padding:14px}.confirm-shell>footer{display:flex;justify-content:flex-end;gap:6px;padding:10px 14px;border-top:1px solid var(--border);background:var(--muted)}.dialog-field{display:grid;gap:6px}.dialog-field>span,.dialog-help{font-size:13px;color:var(--text-muted)}.dialog-field input,.dialog-field textarea{width:100%;border:1px solid var(--border-strong);border-radius:4px;padding:8px}.exception-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;border:0}.exception-options legend{padding:0 0 8px;font-size:13px;color:var(--text-muted)}.exception-options label{display:flex;align-items:center;gap:6px}.dialog-help{padding:0 14px 14px!important}
|
||||
.drawer-backdrop{position:fixed;z-index:85;inset:56px 0 0;background:rgba(15,23,42,.18)}.drawer-actions{position:sticky;bottom:0;display:flex;justify-content:flex-end;gap:6px;margin:16px -14px -14px;padding:10px 14px;border-top:1px solid var(--border);background:var(--surface)}.search-empty{padding:20px;color:var(--text-muted);text-align:center}
|
||||
|
||||
@media(max-width:820px){.search-advanced{flex-direction:column;align-items:stretch}.workflow-strip{overflow:auto}.workflow-strip strong{display:none}.exception-options{grid-template-columns:1fr}}
|
||||
|
||||
/* v39 FE product-parity hardening */
|
||||
.lookup-dialog{width:min(720px,calc(100vw - 32px));padding:0;border:1px solid var(--border-strong);border-radius:6px;box-shadow:var(--shadow)}
|
||||
.lookup-dialog::backdrop{background:rgba(15,23,42,.35)}
|
||||
.lookup-dialog-shell{display:grid;grid-template-rows:auto auto auto auto;background:var(--surface)}
|
||||
.lookup-dialog-shell>header{min-height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--border)}
|
||||
.lookup-dialog-shell>header>div{display:grid;gap:2px}.lookup-dialog-shell>header small{font-size:12px;color:var(--text-muted)}
|
||||
.lookup-dialog-shell>header button{width:32px;height:32px;border:0;background:transparent;font-size:20px}
|
||||
.lookup-search{display:grid;grid-template-columns:1fr auto;gap:6px;padding:10px;border-bottom:1px solid var(--border);background:var(--muted)}
|
||||
.lookup-search input{width:100%;height:var(--control-h);border:1px solid var(--border-strong);border-radius:4px;padding:0 10px;background:var(--surface)}
|
||||
.lookup-results{min-height:180px;max-height:52vh;overflow:auto;background:var(--surface)}.lookup-result{width:100%;min-height:42px;display:grid;grid-template-columns:110px minmax(0,1fr);align-items:center;gap:10px;padding:4px 12px;border:0;border-bottom:1px solid var(--border);background:var(--surface);text-align:left}
|
||||
.lookup-result:hover,.lookup-result.active{background:var(--blue-50)}.lookup-result .code{font-variant-numeric:tabular-nums;color:var(--text-muted)}.lookup-result .copy{display:grid;min-width:0}.lookup-result .copy b{font-weight:500}.lookup-result .copy small{font-size:12px;color:var(--text-muted)}
|
||||
.lookup-dialog-shell>footer{min-height:44px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:6px 10px;border-top:1px solid var(--border);background:var(--muted);font-size:12px;color:var(--text-muted)}
|
||||
.lookup input[readonly]{background:var(--muted);color:var(--text-muted)}.lookup-trigger{min-width:58px}
|
||||
.order-data-row:focus{outline:2px solid var(--primary);outline-offset:-2px}.order-data-row:focus td{background:var(--blue-50)}
|
||||
.utility-panel{display:grid;gap:12px}.utility-panel h2,.utility-panel h3,.utility-panel p{margin:0}.utility-panel h2{font-size:18px}.utility-panel h3{font-size:14px;margin-top:4px}.utility-panel ol{margin:0;padding-left:22px;display:grid;gap:6px}.utility-kicker{font-size:12px;color:var(--text-muted)}
|
||||
.shortcut-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;margin:0;border:1px solid var(--border);background:var(--border)}.shortcut-list div{display:grid;grid-template-columns:52px 1fr;min-height:34px;align-items:center;background:var(--surface)}.shortcut-list dt{padding:0 8px;font-weight:600}.shortcut-list dd{margin:0;color:var(--text-muted)}
|
||||
.utility-note{padding:10px 12px;border:1px solid var(--border);background:var(--muted);font-size:12px;color:var(--text-muted);line-height:1.5}.utility-actions{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.ai-attention{display:grid;gap:6px}.ai-attention button{min-height:48px;display:grid;grid-template-columns:90px 52px minmax(0,1fr);align-items:center;gap:8px;border:1px solid var(--border);background:var(--surface);padding:5px 10px;text-align:left}.ai-attention button:hover{background:var(--muted)}.ai-attention b{font-size:16px}.ai-attention small{color:var(--text-muted)}
|
||||
.suggestion-types{display:flex;gap:12px;padding:10px;border:1px solid var(--border)}.suggestion-types legend{padding:0 4px;color:var(--text-muted);font-size:12px}.suggestion-types label{display:flex;align-items:center;gap:5px}.suggestion-copy{display:grid;gap:6px}.suggestion-copy span{font-size:13px;color:var(--text-muted)}.suggestion-copy textarea{width:100%;resize:vertical;border:1px solid var(--border-strong);border-radius:4px;padding:8px}.suggestion-context{display:grid;grid-template-columns:auto 1fr;align-items:start;gap:2px 7px}.suggestion-context small{grid-column:2;color:var(--text-muted)}
|
||||
.system-row{width:100%;min-height:52px;display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--border);background:var(--surface);padding:6px 10px;text-align:left}.system-row+ .system-row{border-top:0}.system-row span:first-child{display:grid;gap:2px}.system-row small{font-size:12px;color:var(--text-muted)}.system-row.critical{border-color:var(--red-200);background:var(--red-50)}.system-row.static{cursor:default}
|
||||
.import-steps.six{grid-template-columns:repeat(6,1fr)}.import-review{display:flex;flex-wrap:wrap;gap:8px 14px;padding:10px;border:1px solid var(--border);background:var(--muted);font-size:12px}.job-progress-head{display:flex;align-items:end;justify-content:space-between;gap:12px}.job-progress-head>div{display:grid;gap:3px}.job-progress-head small{color:var(--text-muted)}.job-progress-head>b{font-size:22px}.progress-track{height:12px;overflow:hidden;border:1px solid var(--border-strong);background:var(--gray-100)}.progress-track span{display:block;height:100%;background:var(--primary);transition:width .16s linear}.result-actions{display:flex;gap:6px;justify-content:flex-end;margin-top:12px}
|
||||
.network.unstable{color:var(--warning)}.network.offline{color:var(--danger)}
|
||||
|
||||
@media(max-width:1280px){
|
||||
.global-header{grid-template-columns:var(--nav-w) minmax(280px,1fr) auto;gap:8px}.header-actions button{padding:0 6px}.page-header{position:sticky;top:0;z-index:11}.command-bar{overflow-x:auto;white-space:nowrap}.screen-body{min-width:0}.search-panel{align-items:flex-start}.search-fields{flex-wrap:wrap}.home-page{max-width:none}.work-explorer{grid-template-columns:1fr 1fr}.grid-wrap{max-width:100%;overflow:auto}
|
||||
}
|
||||
@media(max-width:900px){
|
||||
.page-header{min-height:52px;height:auto;align-items:flex-start;gap:8px;padding-top:6px;padding-bottom:6px}.page-tools{flex-wrap:wrap;justify-content:flex-end}.command-bar{position:sticky;top:52px;z-index:10}.quick-filter{overflow-x:auto}.quick-filter button{min-width:96px}.context-bar,.summary-bar{overflow-x:auto;white-space:nowrap}.import-steps.six{overflow-x:auto;grid-template-columns:repeat(6,minmax(92px,1fr))}.validation-cards{overflow-x:auto}.lookup-dialog-shell>footer{align-items:flex-start;flex-direction:column}
|
||||
}
|
||||
@media(max-width:620px){
|
||||
.attention-item,.continue-item{grid-template-columns:64px minmax(0,1fr) 14px}.attention-action{display:none}.suggestion-types{flex-direction:column}.shortcut-list{grid-template-columns:1fr}.ai-attention button{grid-template-columns:78px 44px minmax(0,1fr)}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}
|
||||
.drawer[aria-hidden="true"]{visibility:hidden;pointer-events:none}
|
||||
@media(max-width:767px){
|
||||
.kbx-app[data-mode="wms-mobile"] .global-header,.kbx-app[data-mode="wms-mobile"] .side-nav,.kbx-app[data-mode="wms-mobile"] .workspace-tabs{display:none}
|
||||
.kbx-app[data-mode="wms-mobile"] .shell-body,.kbx-app[data-mode="wms-mobile"] .workspace-shell,.kbx-app[data-mode="wms-mobile"] .workspace{height:100%;min-height:100%;width:100%}
|
||||
.kbx-app[data-mode="wms-mobile"] .mobile-stage{min-height:100%;padding:0;background:var(--surface)}
|
||||
.kbx-app[data-mode="wms-mobile"] .wms-phone{width:100%;min-height:100%;border:0;box-shadow:none}
|
||||
.kbx-app[data-mode="wms-mobile"] .wms-content{min-height:0}
|
||||
}
|
||||
|
||||
/* v40 FE completion-loop hardening */
|
||||
.validation-summary{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:8px 10px;border:1px solid var(--red-200);background:var(--red-50);color:var(--danger)}
|
||||
.validation-summary strong{margin-right:4px}.validation-summary button{min-height:28px;border:1px solid var(--red-200);border-radius:4px;background:var(--surface);color:var(--danger);padding:0 8px;text-align:left}.validation-summary button:hover,.validation-summary button:focus-visible{background:var(--red-50)}
|
||||
.action-result{min-height:38px;display:flex;align-items:center;gap:10px;padding:6px 10px;border:1px solid var(--green-200);background:var(--green-50);color:var(--green-700)}.action-result span{min-width:0;flex:1}.action-result .text-action{margin-left:auto}
|
||||
.master-pane .context-bar{border-top:0}.detail-pane>.pane-header{border-bottom:1px solid var(--border)}
|
||||
.data-grid input[type="checkbox"]{width:16px;height:16px;vertical-align:middle}.detail-grid-section .pane-header .text-action{margin-left:auto}
|
||||
@media(max-width:900px){.validation-summary{align-items:flex-start;flex-direction:column}.action-result{align-items:flex-start;flex-wrap:wrap}.action-result .text-action{margin-left:0}}
|
||||
|
||||
/* v43 KBX Business Theme: hierarchy first, decoration second */
|
||||
.brand strong{color:var(--primary)}.nav-row .nav-entry.active{box-shadow:inset 4px 0 0 var(--primary);padding-left:10px}.attention-zone{border-color:var(--amber-200)}.attention-item{border-left:4px solid var(--amber-200)}.attention-item.critical{border-left-color:var(--danger)}.continue-source{justify-self:start;border:1px solid var(--border);border-radius:999px;background:var(--muted);padding:3px 8px}.continue-item[data-source="dirty"] .continue-source{background:var(--amber-50);border-color:var(--amber-200)}.continue-item[data-source="favorite"] .continue-source{background:var(--blue-50);border-color:var(--blue-100)}
|
||||
.kbx-app[data-theme="dark"] .global-search:hover,.kbx-app[data-theme="dark"] .home-search:hover{background:var(--blue-50)}
|
||||
|
||||
/* v44 OS high-contrast bridge: OS colors override Light/Dark without changing workflow geometry. */
|
||||
@media (forced-colors: active){
|
||||
:root,.kbx-app[data-theme="dark"],.kbx-app[data-theme="light"]{--surface:Canvas;--surface-muted:Canvas;--surface-subtle:Canvas;--surface-hover:Highlight;--text:CanvasText;--muted:CanvasText;--border:ButtonBorder;--border-strong:CanvasText;--primary:LinkText;--success:CanvasText;--warning:CanvasText;--danger:CanvasText;--info-surface:Canvas;--warning-surface:Canvas;--danger-surface:Canvas}
|
||||
button,input,select,textarea,a{forced-color-adjust:auto}.active,[aria-current="page"],[aria-pressed="true"],[aria-selected="true"]{outline:1px solid Highlight;outline-offset:-2px}
|
||||
}
|
||||
|
||||
/* 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[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}
|
||||
|
||||
/* 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)}
|
||||
@media(max-width:900px){.search-preference{margin-left:0}}
|
||||
|
||||
.tab-resumed{font-size:var(--font-sm);font-weight:700;color:var(--primary);line-height:1}
|
||||
|
||||
|
||||
.problem-feedback{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);padding:var(--space-3);border:1px solid var(--info-border);border-left:4px solid var(--primary);background:var(--info-surface)}
|
||||
.problem-feedback.warning{border-color:var(--warning-border);border-left-color:var(--warning);background:var(--warning-surface)}
|
||||
.problem-feedback.danger{border-color:var(--danger-border);border-left-color:var(--danger);background:var(--danger-surface)}
|
||||
.problem-copy{display:grid;gap:4px;min-width:0}.problem-heading{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.problem-heading span{font-size:12px;font-weight:600;color:var(--text-muted)}.problem-copy p,.problem-copy small{margin:0;color:var(--text-muted);font-size:13px;line-height:1.5}.problem-actions{display:flex;align-items:center;justify-content:flex-end;gap:4px;flex-wrap:wrap}
|
||||
@media(max-width:48rem){.problem-feedback{flex-direction:column}.problem-actions{width:100%;justify-content:flex-start}}
|
||||
|
||||
|
||||
/* v51 FE presentation workbench: module identity is chrome-only; business status keeps semantic colors. */
|
||||
:root{--workspace:#f5f7fa;--shell:#fff;--nav-surface:#fbfcfe;--section-heading:#f7f9fc;--module-accent:#175cd3;--module-accent-surface:#eff8ff;--module-accent-border:#b2ddff;--shell-shadow:0 1px 2px rgba(16,24,40,.05)}
|
||||
.kbx-app[data-theme="dark"]{--workspace:#0a1020;--shell:#111827;--nav-surface:#0d1524;--section-heading:#151f31;--module-accent:#84adff;--module-accent-surface:#102a43;--module-accent-border:#285b8f;--shell-shadow:0 1px 2px rgba(0,0,0,.42)}
|
||||
[data-module="OMS"]{--module-accent:#175cd3;--module-accent-surface:#eff8ff;--module-accent-border:#b2ddff}
|
||||
[data-module="ERP"]{--module-accent:#6938ef;--module-accent-surface:#f4f3ff;--module-accent-border:#d9d6fe}
|
||||
[data-module="WMS"]{--module-accent:#087e8b;--module-accent-surface:#ecfdff;--module-accent-border:#a5f0fc}
|
||||
[data-module="COMMON"]{--module-accent:#475467;--module-accent-surface:#f2f4f7;--module-accent-border:#d0d5dd}
|
||||
.kbx-app[data-theme="dark"] [data-module="OMS"],.kbx-app[data-theme="dark"][data-module="OMS"]{--module-accent:#84adff;--module-accent-surface:#102a43;--module-accent-border:#285b8f}
|
||||
.kbx-app[data-theme="dark"] [data-module="ERP"],.kbx-app[data-theme="dark"][data-module="ERP"]{--module-accent:#b9a2ff;--module-accent-surface:#241b45;--module-accent-border:#6d5bd0}
|
||||
.kbx-app[data-theme="dark"] [data-module="WMS"],.kbx-app[data-theme="dark"][data-module="WMS"]{--module-accent:#63c5da;--module-accent-surface:#0d2f36;--module-accent-border:#1f6672}
|
||||
.kbx-app[data-theme="dark"] [data-module="COMMON"],.kbx-app[data-theme="dark"][data-module="COMMON"]{--module-accent:#b3b8c4;--module-accent-surface:#172033;--module-accent-border:#475467}
|
||||
.kbx-app{background:var(--workspace)}.global-header{background:var(--shell);box-shadow:var(--shell-shadow)}.brand strong,.header-actions b{color:var(--module-accent)}.global-search:hover{border-color:var(--module-accent);background:var(--module-accent-surface)}.side-nav{background:var(--nav-surface)}.side-nav-top{background:var(--nav-surface)}.nav-home:hover,.nav-home.active,.module-switcher button.active{background:var(--module-accent-surface);color:var(--module-accent);border-color:var(--module-accent-border)}.nav-row .nav-entry.active{color:var(--module-accent);box-shadow:inset 4px 0 0 var(--module-accent)}.nav-row .star.on{color:var(--module-accent)}.workspace-shell,.workspace{background:var(--workspace)}.tab-shell.active{box-shadow:inset 0 2px 0 var(--module-accent)}
|
||||
.screen{background:var(--workspace)}.page-header{background:var(--shell);box-shadow:var(--shell-shadow);border-left:4px solid var(--module-accent)}.status-chip{border-color:var(--module-accent-border);background:var(--module-accent-surface);color:var(--module-accent)}.command-bar{background:var(--shell);box-shadow:var(--shell-shadow)}.screen-body{background:var(--workspace)}
|
||||
.home-page{max-width:1280px}.home-pulse{min-height:40px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid var(--border);background:var(--surface)}.home-pulse button{min-height:40px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:0 8px;padding:4px 12px;border:0;border-right:1px solid var(--border);background:transparent;text-align:left}.home-pulse button:last-child{border-right:0}.home-pulse button:hover:not(:disabled){background:var(--muted)}.home-pulse button:disabled{cursor:default}.home-pulse span{font-size:12px;color:var(--text-muted)}.home-pulse b{grid-row:1 / span 2;grid-column:2;font-size:16px;font-variant-numeric:tabular-nums}.home-pulse small{font-size:12px;color:var(--module-accent)}.module-column .module-title{box-shadow:inset 4px 0 0 var(--module-accent);background:var(--section-heading)}.module-column .module-title strong{color:var(--module-accent)}
|
||||
.screen[data-template="T01"] .search-panel{box-shadow:inset 4px 0 0 var(--module-accent)}.screen[data-template="T02"] .master-pane,.screen[data-template="T02"] .detail-pane,.screen[data-template="T03"] .form-section,.screen[data-template="T03"] .grid-wrap,.screen[data-template="T04"] .grid-wrap,.screen[data-template="T05"] .pane,.screen[data-template="T06"] .grid-wrap,.screen[data-template="T07"] .grid-wrap,.screen[data-template="T08"] .import-stage{box-shadow:var(--shell-shadow)}.screen[data-template="T02"] .detail-pane,.screen[data-template="T03"] .form-section,.screen[data-template="T04"] .fast-guide,.screen[data-template="T05"] .master-pane,.screen[data-template="T06"] .queue-summary,.screen[data-template="T07"] .reconcile-summary,.screen[data-template="T08"] .import-steps{border-top:2px solid var(--module-accent)}.screen[data-template="T06"] .queue-summary{background:var(--surface)}.screen[data-template="T07"] .reconcile-summary{background:var(--surface)}.screen[data-template="T08"] .import-steps{background:var(--section-heading)}
|
||||
@media(max-width:820px){.home-pulse{grid-template-columns:repeat(2,minmax(0,1fr))}.home-pulse button:nth-child(2){border-right:0}.home-pulse button:nth-child(-n+2){border-bottom:1px solid var(--border)}}
|
||||
@media(forced-colors:active){:root,.kbx-app[data-theme="dark"],.kbx-app[data-theme="light"]{--workspace:Canvas;--shell:Canvas;--nav-surface:Canvas;--section-heading:Canvas;--module-accent:Highlight;--module-accent-surface:Canvas;--module-accent-border:Highlight;--shell-shadow:none}}
|
||||
|
||||
/* v52 operational navigation: Home is a launcher, resume point, exception ingress, and full work explorer. */
|
||||
.home-module-rail{display:grid;grid-template-columns:auto repeat(5,minmax(0,1fr));align-items:stretch;border:1px solid var(--border);background:var(--surface)}
|
||||
.home-module-label{display:flex;align-items:center;padding:0 var(--space-3);border-right:1px solid var(--border);background:var(--section-heading);font-size:.75rem;font-weight:600;color:var(--text-muted)}
|
||||
.home-module-rail button{min-width:0;min-height:var(--control-h);display:grid;grid-template-columns:minmax(0,1fr);align-content:center;gap:var(--space-1);padding:var(--space-2) var(--space-3);border:0;border-right:1px solid var(--border);background:transparent;text-align:left}
|
||||
.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[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[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)}
|
||||
@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(forced-colors:active){.home-module-rail button[aria-pressed="true"]{outline:1px solid Highlight;outline-offset:-1px;box-shadow:none}}
|
||||
@@ -0,0 +1,753 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const screens = [
|
||||
{ id:'HOME', title:'홈', module:'COMMON', group:'홈', type:'home', template:'HOME', path:'#home', keywords:['시작','메뉴'] },
|
||||
{ id:'OMS-ORD-001', title:'주문관리', module:'OMS', group:'주문', type:'list', template:'T01', path:'#orders', keywords:['주문조회','출고대기','오류'] },
|
||||
{ id:'ERP-MST-ITEM-001', title:'품목관리', module:'ERP', group:'기준정보', type:'master', template:'T02', path:'#items', keywords:['품목','상품','SKU'] },
|
||||
{ id:'OMS-ORD-002', title:'주문등록', module:'OMS', group:'주문', type:'transaction', template:'T03', path:'#order-new', keywords:['신규주문','등록'] },
|
||||
{ id:'ERP-PRICE-001', title:'품목단가 일괄입력', module:'ERP', group:'기준정보', type:'fast-entry', template:'T04', path:'#prices', keywords:['단가','Fast Entry','붙여넣기'] },
|
||||
{ id:'ERP-INV-001', title:'재고현황', module:'ERP', group:'재고', type:'master-detail', template:'T05', path:'#inventory', keywords:['현재고','가용재고','로케이션'] },
|
||||
{ id:'WMS-WORK-001', title:'오늘 물류작업', module:'WMS', group:'작업관리', type:'queue', template:'T06', path:'#wms-work', keywords:['피킹','입고','예외','Queue'] },
|
||||
{ id:'COMMON-REC-001', title:'재고·출고 대사', module:'COMMON', group:'운영', type:'reconcile', template:'T07', path:'#reconcile', keywords:['대사','불일치','정합성'] },
|
||||
{ id:'OMS-ORD-003', title:'주문 Excel 업로드', module:'OMS', group:'주문', type:'import', template:'T08', path:'#import', keywords:['엑셀','업로드','매핑'] },
|
||||
{ id:'WMS-PICK-001', title:'출고 피킹', module:'WMS', group:'현장업무', type:'wms-mobile', template:'T09', path:'#picking', keywords:['PDA','스캔','바코드'] },
|
||||
];
|
||||
|
||||
const orders = [
|
||||
{no:'ORD-260811-1024',channel:'쿠팡',time:'16:42',buyer:'김민수',item:'런닝화 외 1',qty:3,amount:128000,stock:'정상',status:'출고대기',exception:''},
|
||||
{no:'ORD-260811-1023',channel:'스마트스토어',time:'16:39',buyer:'이지은',item:'셔츠 BLACK',qty:1,amount:48000,stock:'⚠ 부족',status:'보류',exception:'재고부족'},
|
||||
{no:'ORD-260811-1022',channel:'자사몰',time:'16:31',buyer:'박서준',item:'운동화 270',qty:2,amount:196000,stock:'정상',status:'할당완료',exception:''},
|
||||
{no:'ORD-260811-1021',channel:'쿠팡',time:'16:28',buyer:'최유진',item:'양말 3PACK',qty:4,amount:36000,stock:'정상',status:'출고대기',exception:''},
|
||||
{no:'ORD-260811-1018',channel:'11번가',time:'16:12',buyer:'정하늘',item:'후드티 GRAY',qty:1,amount:69000,stock:'정상',status:'오류',exception:'송장실패'},
|
||||
{no:'ORD-260811-1014',channel:'쿠팡',time:'15:57',buyer:'한도윤',item:'운동화 BLACK',qty:2,amount:178000,stock:'정상',status:'출고대기',exception:''},
|
||||
{no:'ORD-260811-1007',channel:'자사몰',time:'15:43',buyer:'오서연',item:'스니커즈 WHITE',qty:1,amount:82000,stock:'정상',status:'신규',exception:''},
|
||||
{no:'ORD-260811-1002',channel:'스마트스토어',time:'15:31',buyer:'윤지호',item:'양말 WHITE',qty:5,amount:45000,stock:'정상',status:'출고대기',exception:'주소확인'},
|
||||
];
|
||||
|
||||
const masterItems = [
|
||||
{id:'ABC001',name:'남성 운동화',group:'신발',spec:'BLACK / 270',unit:'EA',warehouse:'서울물류센터',warehouseCode:'WH01',barcode:'8801234567890',lot:true,expiry:false,status:'사용'},
|
||||
{id:'ABC002',name:'러닝화',group:'신발',spec:'WHITE / 265',unit:'EA',warehouse:'서울물류센터',warehouseCode:'WH01',barcode:'8801234567891',lot:false,expiry:false,status:'사용'},
|
||||
{id:'ABC003',name:'양말',group:'의류',spec:'WHITE / FREE',unit:'PACK',warehouse:'부산물류센터',warehouseCode:'WH02',barcode:'8801234567892',lot:false,expiry:false,status:'사용'},
|
||||
{id:'ABC004',name:'셔츠',group:'의류',spec:'BLACK / L',unit:'EA',warehouse:'서울물류센터',warehouseCode:'WH01',barcode:'8801234567893',lot:false,expiry:false,status:'사용'},
|
||||
{id:'ABC005',name:'후드티',group:'의류',spec:'GRAY / XL',unit:'EA',warehouse:'인천물류센터',warehouseCode:'WH03',barcode:'8801234567894',lot:false,expiry:false,status:'사용'},
|
||||
];
|
||||
|
||||
const inventoryItems = [
|
||||
{id:'ABC001',name:'운동화',stock:1280,locations:[['서울센터','A-01-01',300,100,20],['서울센터','A-01-02',530,0,0],['부산센터','B-03-01',450,30,10]]},
|
||||
{id:'ABC002',name:'양말',stock:721,locations:[['서울센터','A-02-01',421,50,0],['부산센터','B-02-07',300,0,10]]},
|
||||
{id:'ABC003',name:'셔츠',stock:312,locations:[['서울센터','A-05-03',212,20,5],['인천센터','C-01-01',100,0,0]]},
|
||||
{id:'ABC004',name:'후드티',stock:187,locations:[['서울센터','A-06-02',87,10,0],['부산센터','B-06-01',100,0,0]]},
|
||||
];
|
||||
|
||||
const queueRows = [
|
||||
{no:'WK-260811-184',type:'피킹',wave:'W260811-12',worker:'김작업',progress:'72%',sla:'17:10',status:'진행',exception:false},
|
||||
{no:'WK-260811-182',type:'검수',wave:'W260811-11',worker:'박물류',progress:'91%',sla:'16:55',status:'지연',exception:true},
|
||||
{no:'WK-260811-176',type:'입고',wave:'-',worker:'이현장',progress:'38%',sla:'17:30',status:'진행',exception:false},
|
||||
{no:'WK-260811-171',type:'피킹',wave:'W260811-10',worker:'최창고',progress:'100%',sla:'16:30',status:'완료',exception:false},
|
||||
];
|
||||
|
||||
const reconcileRows = [
|
||||
{no:'ORD-001',expected:10,actual:10,diff:0,reason:'-',status:'정상'},
|
||||
{no:'ORD-002',expected:10,actual:9,diff:-1,reason:'피킹부족',status:'확인필요'},
|
||||
{no:'ORD-003',expected:5,actual:0,diff:-5,reason:'연계실패',status:'미처리'},
|
||||
{no:'ORD-004',expected:12,actual:12,diff:0,reason:'-',status:'정상'},
|
||||
];
|
||||
|
||||
const state = {
|
||||
activeId:'HOME', module:'ALL', collapsed:false, density:'compact', theme:'light', homeAttentionExpanded:false,
|
||||
favorites:new Set(['OMS-ORD-001','ERP-INV-001','WMS-WORK-001']), recents:['OMS-ORD-001','ERP-MST-ITEM-001','ERP-INV-001'], tabs:[],
|
||||
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,
|
||||
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:'서울특별시 강남구 테헤란로'},
|
||||
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',
|
||||
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:'',
|
||||
importStep:1, importFileName:'', importFileSize:0, importFileType:'', importJobStatus:'idle', importProgress:0, importProcessed:0,
|
||||
pickingQty:4, pickingRequired:6, scanBuffer:'', scanError:'', networkStatus:'online', pendingSync:0, lastScanValue:'', lastScanAt:0,
|
||||
pendingCloseId:null, pendingNavigationId:null, pendingNavigationOptions:null, pendingUnsavedMode:null, workspaceDirtyDiscarded:0,
|
||||
lookupEntity:'', lookupField:'', lookupIndex:0,
|
||||
fastRows:[
|
||||
{code:'ABC001',name:'운동화',price:'98,000',date:'2026-08-11',remark:''},
|
||||
{code:'ABC002',name:'러닝화',price:'89,000',date:'2026-08-11',remark:''},
|
||||
{code:'ABC003',name:'양말',price:'9,000',date:'2026-08-11',remark:''},
|
||||
{code:'ABC004',name:'셔츠',price:'48,000',date:'2026-08-11',remark:''},
|
||||
...Array.from({length:8},()=>({code:'',name:'',price:'',date:'',remark:''})),
|
||||
],
|
||||
};
|
||||
|
||||
const $ = (selector, root=document) => root.querySelector(selector);
|
||||
const $$ = (selector, root=document) => [...root.querySelectorAll(selector)];
|
||||
const esc = value => String(value ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||||
const knownIds = new Set(screens.map(x=>x.id));
|
||||
const app=$('#app'), workspace=$('#workspace'), sideNav=$('#sideNav'), tabs=$('#workspaceTabs');
|
||||
const searchDialog=$('#menuSearchDialog'), searchInput=$('#menuSearchInput'), searchResults=$('#menuSearchResults');
|
||||
const unsavedDialog=$('#unsavedDialog'), lookupDialog=$('#lookupDialog'), lookupSearchInput=$('#lookupSearchInput'), lookupResults=$('#lookupResults'), quantityDialog=$('#quantityDialog'), exceptionDialog=$('#exceptionDialog');
|
||||
const drawer=$('#drawer'), drawerTitle=$('#drawerTitle'), drawerBody=$('#drawerBody'), drawerBackdrop=$('#drawerBackdrop');
|
||||
const systemPanel=$('#systemPanel'),systemPanelTitle=$('#systemPanelTitle'),systemPanelBody=$('#systemPanelBody');
|
||||
let drawerRestoreFocus=null,systemPanelRestoreFocus=null;
|
||||
let lookupRestoreFocus=null;
|
||||
let importTimer=null;
|
||||
const lookupData={
|
||||
customer:[
|
||||
{id:'c-10001',code:'10001',name:'대한상사',secondary:'서울 · 정상'},
|
||||
{id:'c-10002',code:'10002',name:'대한유통',secondary:'경기 · 정상'},
|
||||
{id:'c-10003',code:'10003',name:'대한물류',secondary:'인천 · 정상'},
|
||||
{id:'c-20001',code:'20001',name:'한빛상사',secondary:'부산 · 정상'},
|
||||
],
|
||||
warehouse:[
|
||||
{id:'w-WH01',code:'WH01',name:'서울물류센터',secondary:'상온 · 사용'},
|
||||
{id:'w-WH02',code:'WH02',name:'부산물류센터',secondary:'상온 · 사용'},
|
||||
{id:'w-WH03',code:'WH03',name:'인천물류센터',secondary:'상온 · 사용'},
|
||||
],
|
||||
};
|
||||
|
||||
function screen(id){ return screens.find(x=>x.id===id) || screens[0]; }
|
||||
function labelModule(module){ return module==='COMMON'?'공통':module; }
|
||||
function iconFor(item){ return ({T01:'조',T02:'기',T03:'등',T04:'입',T05:'재',T06:'작',T07:'대',T08:'엑',T09:'피'})[item.template] || '·'; }
|
||||
function announce(message){ $('#liveRegion').textContent=message; }
|
||||
function toast(message,tone='success'){
|
||||
const el=document.createElement('div'); el.className=`toast ${tone}`; el.textContent=message; $('#toastRegion').append(el); announce(message); setTimeout(()=>el.remove(),2600);
|
||||
}
|
||||
function persist(){
|
||||
try{ localStorage.setItem('kbx-fe-reference-state',JSON.stringify({favorites:[...state.favorites],density:state.density,collapsed:state.collapsed,theme:state.theme,orderRememberSearch:state.orderRememberSearch,orderSavedDefaults:state.orderSavedDefaults})); }catch{}
|
||||
}
|
||||
function persistWorkspaceSession(){
|
||||
const payload={tabs:state.tabs.filter(tab=>!tab.dirty).slice(0,10).map(tab=>({id:tab.id,pinned:Boolean(tab.pinned)})),dirtyDiscardedCount:Math.min(10,state.tabs.filter(tab=>tab.dirty).length)};
|
||||
try{sessionStorage.setItem('kbx-fe-reference-workspace-v1',JSON.stringify(payload));}catch{}
|
||||
}
|
||||
function restoreWorkspaceSession(){
|
||||
let raw='';try{raw=sessionStorage.getItem('kbx-fe-reference-workspace-v1')||'';}catch{return;}
|
||||
if(!raw||raw.length>16000)return;
|
||||
try{const value=JSON.parse(raw);if(!Array.isArray(value.tabs))return;const seen=new Set();state.tabs=value.tabs.slice(0,10).flatMap(item=>{const id=typeof item?.id==='string'&&knownIds.has(item.id)&&item.id!=='HOME'?item.id:'';if(!id||seen.has(id))return [];seen.add(id);return [{id,title:screen(id).title,pinned:Boolean(item.pinned),dirty:false,resumed:true}];});state.workspaceDirtyDiscarded=Math.min(10,Math.max(0,Number(value.dirtyDiscardedCount)||0));}catch{}
|
||||
}
|
||||
function restore(){
|
||||
let raw='';
|
||||
try{ raw=localStorage.getItem('kbx-fe-reference-state')||''; }catch{return;}
|
||||
if(!raw)return;
|
||||
try{
|
||||
const value=JSON.parse(raw);
|
||||
if(Array.isArray(value.favorites))state.favorites=new Set(value.favorites.filter(id=>knownIds.has(id)&&id!=='HOME'));
|
||||
if(['compact','comfortable'].includes(value.density))state.density=value.density;
|
||||
if(typeof value.collapsed==='boolean')state.collapsed=value.collapsed;
|
||||
if(['light','dark'].includes(value.theme))state.theme=value.theme;
|
||||
if(typeof value.orderRememberSearch==='boolean')state.orderRememberSearch=value.orderRememberSearch;
|
||||
if(value.orderSavedDefaults&&typeof value.orderSavedDefaults==='object'){
|
||||
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'};
|
||||
state.orderSavedDefaults=safe;state.orderSearch={...state.orderSearch,...safe,keyword:''};
|
||||
}
|
||||
}catch{}
|
||||
}
|
||||
function markDirty(id,dirty=true){ const tab=state.tabs.find(x=>x.id===id); if(tab){tab.dirty=dirty;tab.resumed=false;persistWorkspaceSession();renderTabs();} }
|
||||
function addRecent(id){ if(id==='HOME')return; state.recents=[id,...state.recents.filter(x=>x!==id)].slice(0,6); }
|
||||
function setHash(s){ if(location.hash!==s.path)location.hash=s.path; }
|
||||
function openScreen(id,{focus=true,updateHash=true,force=false}={}){
|
||||
const target=screen(id);
|
||||
const current=state.tabs.find(t=>t.id===state.activeId);
|
||||
if(!force&&target.id!==state.activeId&¤t?.dirty){
|
||||
state.pendingNavigationId=target.id;
|
||||
state.pendingNavigationOptions={focus,updateHash};
|
||||
state.pendingUnsavedMode='navigate';
|
||||
$('#unsavedMessage').textContent=`${current.title}에 저장하지 않은 변경사항이 있습니다. 이동하기 전에 저장하거나 변경을 버리세요.`;
|
||||
if(!unsavedDialog.open)unsavedDialog.showModal();
|
||||
return false;
|
||||
}
|
||||
state.activeId=target.id; addRecent(target.id);
|
||||
const existingTab=state.tabs.find(t=>t.id===target.id);if(existingTab)existingTab.resumed=false;
|
||||
if(target.id!=='HOME'&&!existingTab)state.tabs.push({id:target.id,title:target.title,pinned:false,dirty:false,resumed:false});
|
||||
persistWorkspaceSession();if(updateHash)setHash(target); renderAll(); if(focus)workspace.focus({preventScroll:true});
|
||||
return true;
|
||||
}
|
||||
function closeTabNow(id){
|
||||
const index=state.tabs.findIndex(t=>t.id===id); if(index<0)return;
|
||||
const wasActive=state.activeId===id; state.tabs.splice(index,1);persistWorkspaceSession();
|
||||
if(wasActive){ const fallback=state.tabs[index-1] || state.tabs[index] || null; openScreen(fallback?.id || 'HOME'); }
|
||||
else renderTabs();
|
||||
}
|
||||
function requestCloseTab(id){
|
||||
const tab=state.tabs.find(t=>t.id===id); if(!tab)return;
|
||||
if(!tab.dirty){closeTabNow(id);return;}
|
||||
state.pendingCloseId=id; state.pendingUnsavedMode='close';
|
||||
$('#unsavedMessage').textContent=`${tab.title}에 저장하지 않은 변경사항이 있습니다.`;
|
||||
if(!unsavedDialog.open)unsavedDialog.showModal();
|
||||
}
|
||||
|
||||
function groupScreens(list=screens.filter(x=>x.id!=='HOME')){
|
||||
const map=new Map();
|
||||
list.forEach(item=>{ if(!map.has(item.module))map.set(item.module,new Map());const groups=map.get(item.module);if(!groups.has(item.group))groups.set(item.group,[]);groups.get(item.group).push(item); });
|
||||
return map;
|
||||
}
|
||||
function isFav(id){return state.favorites.has(id)}
|
||||
function toggleFavorite(id){ if(!knownIds.has(id)||id==='HOME')return; isFav(id)?state.favorites.delete(id):state.favorites.add(id);persist();renderNavigation();if(state.activeId==='HOME')renderHome(); }
|
||||
|
||||
function navRow(item,favorite=true,icon=''){
|
||||
const collapsedLabel=icon||iconFor(item);
|
||||
return `<div class="nav-row" data-screen="${esc(item.id)}"><button class="nav-entry ${state.activeId===item.id?'active':''}" type="button" title="${esc(item.title)}" ${state.activeId===item.id?'aria-current="page"':''}>${state.collapsed?esc(collapsedLabel):esc(item.title)}</button>${favorite?`<button class="star ${isFav(item.id)?'on':''}" type="button" aria-label="${isFav(item.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(item.id)}">${isFav(item.id)?'★':'☆'}</button>`:''}</div>`;
|
||||
}
|
||||
function bindNavRow(row){ const id=row.dataset.screen; $('.nav-entry',row).onclick=()=>openScreen(id);const star=$('.star',row);if(star)star.onclick=e=>{e.stopPropagation();toggleFavorite(id)}; }
|
||||
function renderNavigation(){
|
||||
app.dataset.density=state.density;app.dataset.theme=state.theme;app.dataset.nav=state.collapsed?'collapsed':'expanded';app.dataset.module=state.module==='ALL'?(screen(state.activeId).module||'COMMON'):state.module;document.documentElement.style.colorScheme=state.theme;sideNav.classList.toggle('collapsed',state.collapsed);
|
||||
$('#collapseButton').textContent=state.collapsed?'›':'‹';$('#collapseButton').setAttribute('aria-label',state.collapsed?'메뉴 펼치기':'메뉴 접기');$('#collapseButton').setAttribute('aria-expanded',String(!state.collapsed));
|
||||
$('#densityButton').textContent=state.density==='compact'?'Compact':'Comfortable';
|
||||
const themeButton=$('#themeButton');if(themeButton){themeButton.innerHTML=state.theme==='dark'?'<span aria-hidden="true">☾</span> <span>Dark</span>':'<span aria-hidden="true">☀</span> <span>Light</span>';themeButton.setAttribute('aria-label',state.theme==='dark'?'밝은 테마로 변경':'어두운 테마로 변경');}
|
||||
const modules=['OMS','ERP','WMS','COMMON'];
|
||||
$('#moduleSwitcher').innerHTML=modules.map(m=>`<button type="button" class="${state.module===m?'active':''}" data-module="${m}" aria-pressed="${state.module===m}" title="${esc(labelModule(m))}">${state.collapsed?(m==='COMMON'?'공':m[0]):esc(labelModule(m))}</button>`).join('');
|
||||
$$('#moduleSwitcher button').forEach(button=>button.onclick=()=>{state.module=button.dataset.module;$('#moduleSelect').value=state.module;renderNavigation();if(state.activeId==='HOME')renderHome();});
|
||||
$('#moduleSelect').value=state.module;
|
||||
const favoriteItems=screens.filter(x=>isFav(x.id)&&x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));
|
||||
$('#favoriteNav').innerHTML=favoriteItems.length?`<h2>즐겨찾기</h2>${favoriteItems.slice(0,6).map(item=>navRow(item)).join('')}`:'';
|
||||
const recentItems=state.recents.map(screen).filter(x=>x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));
|
||||
$('#recentNav').innerHTML=recentItems.length?`<h2>최근메뉴</h2>${recentItems.slice(0,4).map(item=>navRow(item,false,'↺')).join('')}`:'';
|
||||
const visible=screens.filter(x=>x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));const grouped=groupScreens(visible);
|
||||
$('#menuNav').innerHTML=[...grouped.entries()].map(([module,groups])=>`<section class="menu-group"><h2>${esc(labelModule(module))} 업무</h2>${[...groups.entries()].map(([group,items])=>`<div class="menu-subgroup"><h2>${esc(group)}</h2>${items.map(item=>navRow(item)).join('')}</div>`).join('')}</section>`).join('');
|
||||
$$('.nav-row',sideNav).forEach(bindNavRow);$('#sideHomeButton').classList.toggle('active',state.activeId==='HOME');$('#sideHomeButton').setAttribute('aria-current',state.activeId==='HOME'?'page':'false');
|
||||
}
|
||||
|
||||
function renderTabs(){
|
||||
if(!state.tabs.length){tabs.innerHTML='<div class="empty-tabs">열린 업무가 없습니다.</div>';return;}
|
||||
const focusableId=state.tabs.some(t=>t.id===state.activeId)?state.activeId:state.tabs[0]?.id;
|
||||
tabs.innerHTML=state.tabs.map(t=>`<div class="tab-shell ${state.activeId===t.id?'active':''}" data-tab="${esc(t.id)}"><button class="tab-main" role="tab" aria-selected="${state.activeId===t.id}" tabindex="${focusableId===t.id?'0':'-1'}" type="button">${t.resumed&&!t.dirty?'<span class="tab-resumed" title="이전 세션에서 복원된 업무 위치" aria-label="이전 세션에서 복원됨">↺</span>':''}<span class="tab-title">${esc(t.title)}</span>${t.dirty?'<span class="tab-dirty" title="저장하지 않은 변경" aria-label="저장하지 않은 변경">●</span>':''}</button><button class="tab-pin" type="button" aria-label="${t.pinned?'고정 해제':'고정'}" aria-pressed="${t.pinned}">${t.pinned?'◆':'◇'}</button><button class="tab-close" type="button" aria-label="${esc(t.title)} 닫기">×</button></div>`).join('');
|
||||
const shells=$$('.tab-shell',tabs);
|
||||
shells.forEach((shell,index)=>{
|
||||
const id=shell.dataset.tab;const main=$('.tab-main',shell);
|
||||
main.onclick=()=>openScreen(id);$('.tab-close',shell).onclick=e=>{e.stopPropagation();requestCloseTab(id)};
|
||||
$('.tab-pin',shell).onclick=e=>{e.stopPropagation();const tab=state.tabs.find(x=>x.id===id);if(tab){tab.pinned=!tab.pinned;persistWorkspaceSession();renderTabs();}};
|
||||
main.onkeydown=e=>{let next=index;if(e.key==='ArrowRight')next=(index+1)%shells.length;else if(e.key==='ArrowLeft')next=(index-1+shells.length)%shells.length;else if(e.key==='Home')next=0;else if(e.key==='End')next=shells.length-1;else return;e.preventDefault();const target=shells[next]?.dataset.tab;if(target){openScreen(target,{focus:false});$('.tab-main',shells[next])?.focus();}};
|
||||
});
|
||||
}
|
||||
|
||||
const btn=(label,cls='',id='',disabled=false)=>`<button ${id?`id="${esc(id)}"`:''} type="button" class="btn ${cls}" ${disabled?'disabled aria-disabled="true"':''}>${esc(label)}</button>`;
|
||||
function defaultCommands(primary='저장 F8',prefix='default'){
|
||||
return `<div class="command-group">${btn('조회 F3','',''+prefix+'Search')}${btn('신규','',''+prefix+'New')}${btn(primary,'primary',''+prefix+'Save')}</div><span class="command-separator"></span><div class="command-group">${btn('엑셀 ▼')}${btn('더보기 ⋯')}</div>`;
|
||||
}
|
||||
function pageShell(s,body,commands=''){
|
||||
return `<section class="screen" data-screen-id="${esc(s.id)}" data-template="${esc(s.template)}" data-module="${esc(s.module)}"><header class="page-header"><div class="page-identity"><div class="breadcrumb">${esc(s.module)} > ${esc(s.group)}</div><div class="title-row"><h1>${esc(s.title)}</h1><span class="status-chip">${esc(s.template)}</span></div></div><div class="page-tools"><button id="pageFavorite" type="button" aria-label="${isFav(s.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(s.id)}">${isFav(s.id)?'★':'☆'}</button><button id="pageHelp" type="button">도움말</button><button id="pageAi" type="button">AI</button><button id="pageSuggestion" type="button">제안</button></div></header>${commands?`<div class="command-bar">${commands}</div>`:''}<div class="screen-body">${body}</div></section>`;
|
||||
}
|
||||
function setBackgroundInert(value){for(const el of [$('.global-header'),$('.shell-body')]){if(!el)continue;value?el.setAttribute('inert',''):el.removeAttribute('inert');}}
|
||||
function openDrawerContent(title,html){
|
||||
drawerRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;drawerTitle.textContent=title;drawerBody.innerHTML=html;
|
||||
drawer.classList.add('open');drawer.setAttribute('aria-hidden','false');drawerBackdrop.hidden=false;setBackgroundInert(true);setTimeout(()=>$('#drawerClose').focus(),0);
|
||||
}
|
||||
function applyIntent(intent){
|
||||
if(intent==='order-stock'){state.orderQuickFilter='stock';state.selectedOrders.clear();state.orderDataState='ready';return openScreen('OMS-ORD-001');}
|
||||
if(intent==='order-errors'){state.orderQuickFilter='error';state.selectedOrders.clear();state.orderDataState='ready';return openScreen('OMS-ORD-001');}
|
||||
if(intent==='reconcile-mismatch'){state.mismatchOnly=true;return openScreen('COMMON-REC-001');}
|
||||
if(intent==='import-progress'){state.importStep=state.importJobStatus==='completed'?6:Math.max(1,state.importStep);return openScreen('OMS-ORD-003');}
|
||||
}
|
||||
function showUtilityPanel(s,kind){
|
||||
if(kind==='help'){openDrawerContent(`${s.title} 도움말`,`<section class="utility-panel"><p class="utility-kicker">화면 도움말 · ${esc(s.id)}</p><h2>${esc(s.title)}</h2><p>이 화면은 ${esc(s.group)} 업무를 표준 ${esc(s.template)} 문법으로 처리합니다.</p><h3>기본 사용법</h3><ol><li>조회 조건 또는 업무 대상을 확인합니다.</li><li>필요한 행이나 값을 선택합니다.</li><li>Command Bar의 업무 동사를 실행합니다.</li></ol><h3>단축키</h3><dl class="shortcut-list"><div><dt>F2</dt><dd>Lookup</dd></div><div><dt>F3</dt><dd>조회</dd></div><div><dt>F8</dt><dd>저장</dd></div><div><dt>Esc</dt><dd>현재 Context 닫기</dd></div></dl><div class="utility-note">화면의 Disabled 상태는 UX 표현이며 최종 권한과 업무 규칙은 서버에서 재검증해야 합니다.</div></section>`);return;}
|
||||
if(kind==='ai'){openDrawerContent(`${s.title} AI 도우미`,`<section class="utility-panel"><p class="utility-kicker">현재 화면 · ${esc(s.id)}</p><h2>확인할 항목</h2><div class="ai-attention"><button type="button" data-ai-intent="order-stock"><span>재고부족</span><b>7건</b><small>주문관리에서 보기</small></button><button type="button" data-ai-intent="order-errors"><span>처리오류</span><b>16건</b><small>오류 주문만 보기</small></button><button type="button" data-ai-intent="reconcile-mismatch"><span>대사 불일치</span><b>43건</b><small>불일치만 보기</small></button></div><h3>빠른 질문</h3><div class="utility-actions"><button class="btn" type="button" data-ai-intent="order-stock">왜 출고가 안 된 주문이 있어?</button><button class="btn" type="button" data-ai-intent="reconcile-mismatch">불일치만 보여줘</button></div><div class="utility-note">AI는 설명·제안만 수행합니다. 실제 변경은 구조화된 Proposal과 사용자 확인 후 Domain Command로 실행해야 합니다.</div></section>`);$$('[data-ai-intent]',drawerBody).forEach(button=>button.onclick=()=>{const intent=button.dataset.aiIntent;closeDrawer();setTimeout(()=>applyIntent(intent),0);});return;}
|
||||
openDrawerContent(`${s.title} 제안`,`<form id="suggestionForm" class="utility-panel"><p class="utility-kicker">사용자 제안 · ${esc(s.id)}</p><h2>이 화면에 의견 보내기</h2><fieldset class="suggestion-types"><legend>유형</legend><label><input type="radio" name="type" value="불편" checked> 불편</label><label><input type="radio" name="type" value="오류"> 오류</label><label><input type="radio" name="type" value="개선 제안"> 개선 제안</label></fieldset><label class="suggestion-copy"><span>내용</span><textarea id="suggestionText" required maxlength="500" rows="7" placeholder="업무 중 불편했던 상황과 기대하는 동작을 적어주세요."></textarea></label><label class="suggestion-context"><input type="checkbox" checked> 현재 화면 정보 포함 <small>ScreenId, Route, Version, 필터 구조만 포함 · 업무 민감데이터 제외</small></label><div class="drawer-actions"><button class="btn primary" type="submit">보내기</button></div></form>`);$('#suggestionForm').onsubmit=e=>{e.preventDefault();const value=$('#suggestionText').value.trim();if(!value){$('#suggestionText').focus();return;}closeDrawer();toast('화면 의견을 접수했습니다. 업무 민감데이터는 포함하지 않았습니다.');};setTimeout(()=>$('#suggestionText')?.focus(),0);
|
||||
}
|
||||
function closeSystemPanel(restoreFocus=true){
|
||||
if(systemPanel.hidden)return;systemPanel.hidden=true;$('#jobsButton').setAttribute('aria-expanded','false');$('#noticeButton').setAttribute('aria-expanded','false');
|
||||
const restore=systemPanelRestoreFocus;systemPanelRestoreFocus=null;if(restoreFocus)restore?.focus();
|
||||
}
|
||||
function showSystemPanel(kind){
|
||||
const trigger=kind==='jobs'?$('#jobsButton'):$('#noticeButton');
|
||||
if(!systemPanel.hidden&&trigger.getAttribute('aria-expanded')==='true'){closeSystemPanel();return;}
|
||||
closeDrawer();systemPanelRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;systemPanelTitle.textContent=kind==='jobs'?'작업 센터':'알림 센터';
|
||||
$('#jobsButton').setAttribute('aria-expanded',String(kind==='jobs'));$('#noticeButton').setAttribute('aria-expanded',String(kind!=='jobs'));
|
||||
if(kind==='jobs'){const progress=state.importJobStatus==='running'?`${state.importProgress}%`:'완료';systemPanelBody.innerHTML=`<section class="utility-panel"><p class="utility-kicker">백그라운드 작업</p><h2>진행 중 / 최근 작업</h2><button class="system-row" type="button" data-system-intent="import-progress"><span><b>Excel 주문 Import</b><small>${esc(progress)} · 정상 ${state.importProcessed.toLocaleString('ko-KR')}건</small></span><span>열기 ›</span></button><div class="system-row static"><span><b>송장 재처리</b><small>대기 · 자동 재시도</small></span><span>대기</span></div></section>`;}else{systemPanelBody.innerHTML=`<section class="utility-panel"><p class="utility-kicker">업무 알림</p><h2>확인 필요 4건</h2><button class="system-row critical" type="button" data-system-intent="order-errors"><span><b>송장 발급 실패 3건</b><small>원인 확인 후 재처리</small></span><span>열기 ›</span></button><button class="system-row" type="button" data-system-intent="order-stock"><span><b>재고부족 주문 7건</b><small>예외 주문만 확인</small></span><span>열기 ›</span></button></section>`;}
|
||||
systemPanel.hidden=false;$$('[data-system-intent]',systemPanelBody).forEach(button=>button.onclick=()=>{const intent=button.dataset.systemIntent;closeSystemPanel(false);setTimeout(()=>applyIntent(intent),0);});setTimeout(()=>systemPanel.focus(),0);
|
||||
}
|
||||
function bindPageTools(s){
|
||||
const fav=$('#pageFavorite');if(fav)fav.onclick=()=>toggleFavorite(s.id);
|
||||
const help=$('#pageHelp');if(help)help.onclick=()=>showUtilityPanel(s,'help');
|
||||
const ai=$('#pageAi');if(ai)ai.onclick=()=>showUtilityPanel(s,'ai');
|
||||
const suggestion=$('#pageSuggestion');if(suggestion)suggestion.onclick=()=>showUtilityPanel(s,'suggestion');
|
||||
}
|
||||
function grid(cols,rows,{caption='업무 데이터',className=''}={}){
|
||||
return `<div class="grid-wrap ${className}"><table class="data-grid"><caption class="sr-only">${esc(caption)}</caption><thead><tr>${cols.map(c=>`<th scope="col">${esc(c)}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
||||
}
|
||||
function formField(label,value,{required=false,name='',readonly=false}={}){
|
||||
return `<div class="form-field"><label class="${required?'required':''}">${esc(label)}</label><input ${name?`data-field="${esc(name)}"`:''} value="${esc(value)}" ${readonly?'readonly':''}></div>`;
|
||||
}
|
||||
function lookupField(label,code,name,{field='',entity=field}={}){
|
||||
return `<div class="form-field"><label>${esc(label)}</label><div class="lookup" data-lookup="${esc(entity)}" data-lookup-field="${esc(field)}"><input ${field?`data-field="${esc(field)}Code"`:''} value="${esc(code)}" autocomplete="off"><input ${field?`data-field="${esc(field)}Name"`:''} value="${esc(name)}" readonly><button type="button" class="lookup-trigger" title="F2 조회" aria-label="${esc(label)} 조회" aria-keyshortcuts="F2">검색</button></div></div>`;
|
||||
}
|
||||
function checkboxField(label,checked,{field=''}={}){return `<div class="form-field"><label>${esc(label)}</label><label class="checkbox-control"><input ${field?`data-field="${esc(field)}"`:''} type="checkbox" ${checked?'checked':''}> 사용</label></div>`;}
|
||||
|
||||
function lookupItems(){
|
||||
const list=lookupData[state.lookupEntity]||[];const q=(lookupSearchInput.value||'').trim().toLowerCase();
|
||||
return list.filter(item=>!q||`${item.code} ${item.name} ${item.secondary}`.toLowerCase().includes(q));
|
||||
}
|
||||
function renderLookupResults(){
|
||||
const list=lookupItems();if(state.lookupIndex>=list.length)state.lookupIndex=Math.max(0,list.length-1);
|
||||
lookupResults.innerHTML=list.length?list.map((item,index)=>`<button type="button" class="lookup-result ${index===state.lookupIndex?'active':''}" data-index="${index}" role="option" aria-selected="${index===state.lookupIndex}"><span class="code">${esc(item.code)}</span><span class="copy"><b>${esc(item.name)}</b><small>${esc(item.secondary)}</small></span></button>`).join(''):'<div class="search-empty">조회된 항목이 없습니다.</div>';
|
||||
$('#lookupStatus').textContent=`${list.length}건 · ↑↓ 이동 · Enter 선택 · Esc 닫기`;
|
||||
$$('.lookup-result',lookupResults).forEach((button,index)=>{button.onmousemove=()=>{state.lookupIndex=index;renderLookupResults();};button.onclick=()=>{state.lookupIndex=index;selectLookup();};});
|
||||
$('#lookupSelectButton').disabled=!list.length;
|
||||
}
|
||||
function openLookup(root){
|
||||
if(!root)return;state.lookupEntity=root.dataset.lookup||'';state.lookupField=root.dataset.lookupField||'';state.lookupIndex=0;lookupRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;
|
||||
const label=root.closest('.form-field')?.querySelector('label')?.textContent?.trim()||'업무';$('#lookupTitle').textContent=`${label} 검색`;$('#lookupSubtitle').textContent='F2 · 코드/명칭 조회';lookupSearchInput.value='';renderLookupResults();
|
||||
if(!lookupDialog.open)lookupDialog.showModal();setTimeout(()=>lookupSearchInput.focus(),0);
|
||||
}
|
||||
function focusAfterLookup(root){
|
||||
const focusables=$$('input:not([disabled]),select:not([disabled]),button:not([disabled]),textarea:not([disabled])',workspace);
|
||||
const current=root?.querySelector('button.lookup-trigger');const index=focusables.indexOf(current);(focusables[index+1]||lookupRestoreFocus)?.focus();
|
||||
}
|
||||
function applyLookupToActive(field,item){
|
||||
if(state.activeId==='ERP-MST-ITEM-001'&&field==='warehouse'&&state.masterDraft){state.masterDraft.warehouseCode=item.code;state.masterDraft.warehouse=item.name;}
|
||||
if(state.activeId==='OMS-ORD-002'){
|
||||
if(field==='customer'){state.transactionHeader.customerCode=item.code;state.transactionHeader.customerName=item.name;}
|
||||
if(field==='warehouse'){state.transactionHeader.warehouseCode=item.code;state.transactionHeader.warehouseName=item.name;}
|
||||
}
|
||||
}
|
||||
function selectLookup(){
|
||||
const list=lookupItems();const item=list[state.lookupIndex];if(!item)return;const root=$(`.lookup[data-lookup-field="${CSS.escape(state.lookupField)}"]`,workspace);
|
||||
if(root){const code=$(`[data-field="${CSS.escape(state.lookupField)}Code"]`,root);const name=$(`[data-field="${CSS.escape(state.lookupField)}Name"]`,root);if(code)code.value=item.code;if(name)name.value=item.name;applyLookupToActive(state.lookupField,item);markDirty(state.activeId,true);}
|
||||
lookupDialog.close();announce(`${item.code} ${item.name}을 선택했습니다.`);toast(`${item.name}을 선택했습니다.`);setTimeout(()=>focusAfterLookup(root),0);
|
||||
}
|
||||
function bindLookups(){
|
||||
$$('.lookup',workspace).forEach(root=>{const trigger=$('.lookup-trigger',root);if(trigger)trigger.onclick=()=>openLookup(root);$$('input',root).forEach(input=>input.onkeydown=e=>{if(e.key==='F2'){e.preventDefault();openLookup(root);}});});
|
||||
}
|
||||
function dataState(kind,title,detail,retryId=''){
|
||||
const action=retryId?btn('다시 조회','primary',retryId):'';
|
||||
return `<section class="data-state ${esc(kind)}" role="status"><strong>${esc(title)}</strong><span>${esc(detail)}</span>${action}</section>`;
|
||||
}
|
||||
|
||||
|
||||
function homeAttentionItems(){
|
||||
return [
|
||||
{source:'실패 작업',title:'송장 발급 실패 3건',detail:'오류 주문 필터로 바로 이동',id:'OMS-ORD-001',critical:true,intent:'order-errors'},
|
||||
{source:'예외',title:'재고부족 주문 7건',detail:'재고부족 필터를 적용해 이동',id:'OMS-ORD-001',critical:true,intent:'order-stock'},
|
||||
{source:'중요 알림',title:'대사 불일치 43건',detail:'불일치만 표시해 이동',id:'COMMON-REC-001',critical:false,intent:'reconcile-mismatch'},
|
||||
{source:'진행 작업',title:state.importJobStatus==='running'?`Excel 주문 반영 ${state.importProgress}%`:state.importJobStatus==='completed'?'Excel 주문 반영 완료':'Excel 주문 업로드 대기',detail:'작업상태 보기',id:'OMS-ORD-003',critical:false,intent:'import-progress'},
|
||||
{source:'미저장',title:'주문등록 수정 중',detail:'저장하지 않은 변경사항 이어서 편집',id:'OMS-ORD-002',critical:false,intent:''},
|
||||
{source:'알림',title:'주소 확인 필요 4건',detail:'배송지 오류 주문만 확인',id:'OMS-ORD-001',critical:false,intent:'order-errors'},
|
||||
{source:'알림',title:'SKU 매칭 실패 2건',detail:'상품 매핑 오류 주문만 확인',id:'OMS-ORD-001',critical:false,intent:'order-errors'},
|
||||
{source:'진행 작업',title:'피킹 작업 1건 지연',detail:'현재 물류작업 Queue에서 확인',id:'WMS-WORK-001',critical:false,intent:''},
|
||||
{source:'알림',title:'품목 바코드 확인 1건',detail:'품목 기준정보에서 확인',id:'ERP-MST-ITEM-001',critical:false,intent:''},
|
||||
{source:'진행 작업',title:'재고 실사 작업 확인',detail:'물류 작업 Queue에서 확인',id:'WMS-WORK-001',critical:false,intent:''},
|
||||
...Array.from({length:18},(_,index)=>({source:index%3===0?'진행 작업':'알림',title:`추가 확인 업무 ${index+1}`,detail:index%3===0?'작업 센터에서 전체 진행상태 확인':'알림 센터에서 전체 알림 확인',id:index%3===0?'WMS-WORK-001':'OMS-ORD-001',critical:false,intent:''})),
|
||||
];
|
||||
}
|
||||
|
||||
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 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 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 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 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>`:'';
|
||||
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>
|
||||
${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="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>`;
|
||||
$('#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);});
|
||||
}
|
||||
|
||||
function filteredOrders(){
|
||||
let visible=orders;
|
||||
if(state.orderQuickFilter==='new')visible=visible.filter(x=>x.status==='신규');
|
||||
else if(state.orderQuickFilter==='ready')visible=visible.filter(x=>x.status==='출고대기');
|
||||
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==='오류');
|
||||
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.exception!=='all')visible=visible.filter(x=>x.exception===state.orderSearch.exception || (state.orderSearch.exception==='재고부족'&&x.stock.includes('부족')));
|
||||
const keyword=state.orderSearch.keyword.trim().toLowerCase();
|
||||
if(keyword)visible=visible.filter(x=>`${x.no} ${x.buyer} ${x.item} ${x.channel} ${x.exception}`.toLowerCase().includes(keyword));
|
||||
return visible;
|
||||
}
|
||||
function searchOption(value,label,current){return `<option value="${esc(value)}" ${current===value?'selected':''}>${esc(label)}</option>`;}
|
||||
function problemSurface(problem){
|
||||
if(!problem)return '';
|
||||
const kind={ 'business-rule':'업무 규칙', permission:'권한', 'not-found':'데이터', integration:'외부 연계', system:'시스템' }[problem.type]||'업무 오류';
|
||||
const tone=['permission','system'].includes(problem.type)?'danger':problem.type==='integration'?(problem.retryable?'warning':'danger'):'warning';
|
||||
const actions=(problem.actions||[]).map(action=>`<button type="button" class="btn secondary" data-problem-action="${esc(action.id)}">${esc(action.label)}</button>`).join('');
|
||||
return `<section class="problem-feedback ${tone}" role="alert" data-problem-type="${esc(problem.type)}"><div class="problem-copy"><div class="problem-heading"><span>${esc(kind)}</span><strong>${esc(problem.title)}</strong></div>${problem.detail?`<p>${esc(problem.detail)}</p>`:''}${problem.correlationId?`<small>참조번호 ${esc(problem.correlationId)}</small>`:''}</div><div class="problem-actions">${actions}${problem.retryable?'<button id="problemRetry" type="button" class="btn secondary">다시 시도</button>':''}<button id="problemDismiss" type="button" class="btn ghost">닫기</button></div></section>`;
|
||||
}
|
||||
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 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 saveOrderDefaults(message){state.orderSavedDefaults=safeOrderDefaults();persist();toast(message||'현재 조회조건을 기본조건으로 저장했습니다. 자유검색어는 저장하지 않습니다.');}
|
||||
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 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>`;
|
||||
}
|
||||
function captureOrderSearch(){
|
||||
state.orderSearch.channel=$('#orderChannel')?.value||'all';
|
||||
state.orderSearch.status=$('#orderStatus')?.value||'all';
|
||||
state.orderSearch.keyword=$('#orderKeyword')?.value||'';
|
||||
state.orderSearch.exception=$('#orderException')?.value||state.orderSearch.exception||'all';
|
||||
const remember=$('#rememberOrderSearch');if(remember)state.orderRememberSearch=remember.checked;
|
||||
}
|
||||
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('');
|
||||
}
|
||||
function renderOrderData(){
|
||||
if(state.orderDataState==='loading')return dataState('loading','조회 중...','기존 조회조건을 유지한 채 데이터를 불러오고 있습니다.');
|
||||
if(state.orderDataState==='empty')return dataState('empty','조회된 주문이 없습니다.','조회조건을 변경하거나 초기화해 보세요.','orderRetry');
|
||||
if(state.orderDataState==='error')return dataState('error','주문을 조회하지 못했습니다.','네트워크 연결을 확인한 후 다시 시도하세요. 입력한 조회조건은 보존됩니다.','orderRetry');
|
||||
const visible=filteredOrders();
|
||||
if(!visible.length)return dataState('empty','현재 필터에 해당하는 주문이 없습니다.','다른 빠른 필터를 선택해 보세요.');
|
||||
const allSelected=visible.every(x=>state.selectedOrders.has(x.no));
|
||||
const table=grid([`<input>`,'주문번호','채널','주문시각','주문자','대표상품','수량','주문금액','재고','출고상태','오류'],orderRows(),{caption:'주문 조회 결과'}).replace('<th scope="col"><input></th>',`<th scope="col" class="center"><input id="selectVisibleOrders" type="checkbox" aria-label="현재 표시된 주문 전체 선택" ${allSelected?'checked':''}></th>`);
|
||||
return table;
|
||||
}
|
||||
function renderT01(s){
|
||||
const selection=state.selectedOrders.size;
|
||||
const quick=[['all','전체','8,241'],['new','신규','2,138'],['ready','출고대기','927'],['stock','재고부족','7'],['error','처리오류','16']];
|
||||
const body=`${problemSurface(state.commandProblem)}${orderSearchPanel()}<div class="quick-filter" role="group" aria-label="주문 빠른 필터">${quick.map(([key,label,count])=>`<button type="button" data-order-filter="${key}" class="${state.orderQuickFilter===key?'active':''} ${['stock','error'].includes(key)?'danger':''}" aria-pressed="${state.orderQuickFilter===key}"><small>${esc(label)}</small><b>${esc(count)}</b></button>`).join('')}</div><div class="context-bar"><strong>오늘 주문</strong><span>조회 8,241건</span><span>표시 ${filteredOrders().length}건</span><span>선택 <b id="selectionContext">${selection}</b>건</span><span>오류 16건</span></div><div id="bulkBar" class="bulk-bar" ${selection?'':'hidden'}><strong><span id="bulkCount">${selection}</span>건 선택</strong>${btn('출고지시','primary','bulkShip')}${btn('보류')}${btn('담당자 변경')}${btn('택배사 변경')}</div><div id="orderDataRegion">${renderOrderData()}</div><div class="summary-bar"><span>전체 8,241건</span><span>선택 <strong id="selectionSummary">${selection}</strong>건</span><span>상품수량 12,824</span><span>금액 <strong>₩182,420,300</strong></span></div>`;
|
||||
const commands=`<div class="command-group">${btn('조회 F3','','commandSearch')}${btn('신규','','commandNew')}${btn('주문복사')}</div><span class="command-separator"></span><div class="command-group">${btn('출고지시','primary','commandShip',selection===0)}${btn('보류','','commandHold',selection===0)}</div><span class="command-separator"></span><div class="command-group">${btn('인쇄')}${btn('엑셀 ▼')}</div>`;
|
||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT01();
|
||||
}
|
||||
function executeOrderSearch(){ clearProblem();captureOrderSearch();if(state.orderRememberSearch){state.orderSavedDefaults=safeOrderDefaults();persist();}state.selectedOrders.clear();state.orderDataState='loading';renderT01(screen('OMS-ORD-001'));setTimeout(()=>{state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));toast(`현재 조건으로 ${filteredOrders().length}건을 조회했습니다.${state.orderRememberSearch?' 안전한 조회조건을 기억했습니다.':''}`);},260); }
|
||||
function bindT01(){
|
||||
$('#orderSearchForm').onsubmit=e=>{e.preventDefault();executeOrderSearch()};$('#searchExecute').onclick=executeOrderSearch;$('#commandSearch').onclick=executeOrderSearch;
|
||||
$('#toggleAdvanced').onclick=()=>{captureOrderSearch();state.orderAdvanced=!state.orderAdvanced;renderT01(screen('OMS-ORD-001'));};
|
||||
$('#searchReset').onclick=()=>{clearProblem();state.orderQuickFilter='all';state.orderAdvanced=false;state.orderSearch={channel:'all',status:'all',keyword:'',exception:'all'};state.selectedOrders.clear();state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));toast('조회조건을 초기화했습니다.');};
|
||||
const saveDefaults=$('#saveOrderDefaults');if(saveDefaults)saveDefaults.onclick=()=>{captureOrderSearch();saveOrderDefaults();renderT01(screen('OMS-ORD-001'));};
|
||||
const remember=$('#rememberOrderSearch');if(remember)remember.onchange=()=>{captureOrderSearch();state.orderRememberSearch=remember.checked;if(remember.checked)state.orderSavedDefaults=safeOrderDefaults();persist();toast(remember.checked?'마지막 조회조건 기억을 켰습니다. 자유검색어는 저장하지 않습니다.':'마지막 조회조건 기억을 껐습니다.');renderT01(screen('OMS-ORD-001'));};
|
||||
const retry=$('#orderRetry');if(retry)retry.onclick=executeOrderSearch;
|
||||
$$('[data-order-filter]',workspace).forEach(button=>button.onclick=()=>{clearProblem();state.orderQuickFilter=button.dataset.orderFilter;state.selectedOrders.clear();state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));announce(`${button.textContent.trim()} 필터를 적용했습니다.`);});
|
||||
$$('input[data-order]',workspace).forEach(cb=>cb.onchange=()=>{cb.checked?state.selectedOrders.add(cb.dataset.order):state.selectedOrders.delete(cb.dataset.order);renderT01(screen('OMS-ORD-001'));});
|
||||
const all=$('#selectVisibleOrders');if(all)all.onchange=()=>{filteredOrders().forEach(order=>all.checked?state.selectedOrders.add(order.no):state.selectedOrders.delete(order.no));renderT01(screen('OMS-ORD-001'));};
|
||||
$$('.order-link',workspace).forEach(link=>link.onclick=e=>{e.preventDefault();showOrderDetail(link.dataset.order);});
|
||||
const orderRows=$$('.order-data-row',workspace);orderRows.forEach((row,index)=>row.onkeydown=e=>{
|
||||
if(['INPUT','A','BUTTON'].includes(e.target?.tagName))return;
|
||||
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const next=Math.max(0,Math.min(orderRows.length-1,index+(e.key==='ArrowDown'?1:-1)));orderRows.forEach((x,i)=>x.tabIndex=i===next?0:-1);orderRows[next]?.focus();}
|
||||
else if(e.key===' '){e.preventDefault();const no=row.dataset.orderRow;no&&(state.selectedOrders.has(no)?state.selectedOrders.delete(no):state.selectedOrders.add(no));renderT01(screen('OMS-ORD-001'));setTimeout(()=>$$('.order-data-row',workspace)[index]?.focus(),0);}
|
||||
else if(e.key==='Enter'){e.preventDefault();showOrderDetail(row.dataset.orderRow);}
|
||||
});
|
||||
for(const id of ['bulkShip','commandShip']){const b=$('#'+id);if(b)b.onclick=()=>{if(!state.selectedOrders.size)return;const selected=orders.filter(order=>state.selectedOrders.has(order.no));const blocked=selected.filter(order=>Boolean(order.exception));if(blocked.length){state.commandProblem={type:'business-rule',title:'출고할 수 없는 주문이 포함되어 있습니다.',detail:`${blocked.length}건은 재고·주소·송장 예외를 먼저 해결해야 합니다. 정상 주문은 이 Reference Lab에서 임의 처리하지 않습니다.`,actions:[{id:'show-blocked',label:`문제 ${blocked.length}건만 선택`}],retryable:false};renderT01(screen('OMS-ORD-001'));return;}toast(`${state.selectedOrders.size}건 출고지시를 요청했습니다.`);};}const dismiss=$('#problemDismiss');if(dismiss)dismiss.onclick=()=>{clearProblem();renderT01(screen('OMS-ORD-001'));};$$('[data-problem-action="show-blocked"]',workspace).forEach(button=>button.onclick=()=>{const blocked=orders.filter(order=>state.selectedOrders.has(order.no)&&order.exception);state.selectedOrders=new Set(blocked.map(order=>order.no));state.orderQuickFilter='all';state.orderSearch.exception='all';clearProblem();renderT01(screen('OMS-ORD-001'));announce(`문제 주문 ${blocked.length}건만 선택했습니다.`);});
|
||||
const newButton=$('#commandNew');if(newButton)newButton.onclick=()=>openScreen('OMS-ORD-002');
|
||||
}
|
||||
function showOrderDetail(orderNo){
|
||||
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>`);
|
||||
}
|
||||
function closeDrawer(){drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true');drawerBackdrop.hidden=true;setBackgroundInert(false);const restore=drawerRestoreFocus;drawerRestoreFocus=null;restore?.focus();}
|
||||
|
||||
function masterDraftFrom(item){return item?{...item}:{id:'',name:'',group:'',spec:'',unit:'EA',warehouse:'',warehouseCode:'',barcode:'',lot:false,expiry:false,status:'사용'};}
|
||||
function loadMasterDraft(id){const item=masterItems.find(x=>x.id===id);state.selectedMasterId=item?.id||'';state.masterMode='edit';state.masterDraft=masterDraftFrom(item||masterItems[0]);state.masterErrors={};}
|
||||
function validateMasterDraft(){
|
||||
const draft=state.masterDraft||masterDraftFrom();const errors={};
|
||||
if(!draft.id.trim())errors.id='품목코드를 입력하세요.';
|
||||
if(!draft.name.trim())errors.name='품목명을 입력하세요.';
|
||||
if(!draft.warehouseCode.trim())errors.warehouse='기본창고를 선택하세요.';
|
||||
if(state.masterMode==='new'&&masterItems.some(item=>item.id===draft.id.trim()))errors.id='이미 사용 중인 품목코드입니다.';
|
||||
state.masterErrors=errors;return Object.keys(errors).length===0;
|
||||
}
|
||||
function masterValidationSummary(){const entries=Object.entries(state.masterErrors);return entries.length?`<div class="validation-summary" role="alert"><strong>저장할 수 없습니다. ${entries.length}개 항목을 확인하세요.</strong>${entries.map(([field,message])=>`<button type="button" data-master-error="${esc(field)}">${esc(message)}</button>`).join('')}</div>`:'';}
|
||||
function performMasterTarget(target){
|
||||
if(target==='__NEW__'){state.selectedMasterId='';state.masterMode='new';state.masterDraft=masterDraftFrom();state.masterErrors={};renderT02(screen('ERP-MST-ITEM-001'));setTimeout(()=>$('[data-field="code"]',workspace)?.focus(),0);return;}
|
||||
if(target==='__COPY__'){const source=state.masterDraft||masterItems.find(x=>x.id===state.selectedMasterId)||masterItems[0];state.selectedMasterId='';state.masterMode='new';state.masterDraft={...masterDraftFrom(source),id:'',barcode:'',status:'사용'};state.masterErrors={};renderT02(screen('ERP-MST-ITEM-001'));setTimeout(()=>$('[data-field="code"]',workspace)?.focus(),0);return;}
|
||||
loadMasterDraft(target);renderT02(screen('ERP-MST-ITEM-001'));
|
||||
}
|
||||
function requestMasterTarget(target){const tab=state.tabs.find(x=>x.id==='ERP-MST-ITEM-001');if(tab?.dirty){state.pendingMasterId=target;state.pendingUnsavedMode='master-select';$('#unsavedMessage').textContent='현재 품목에 저장하지 않은 변경사항이 있습니다. 저장하거나 변경을 버린 뒤 다른 품목으로 전환하세요.';if(!unsavedDialog.open)unsavedDialog.showModal();return;}performMasterTarget(target);}
|
||||
function saveMaster(){
|
||||
if(!validateMasterDraft()){renderT02(screen('ERP-MST-ITEM-001'));setTimeout(()=>$('[data-master-error]',workspace)?.focus(),0);return false;}
|
||||
const draft={...state.masterDraft,id:state.masterDraft.id.trim(),name:state.masterDraft.name.trim(),warehouseCode:state.masterDraft.warehouseCode.trim()};
|
||||
if(state.masterMode==='new'){masterItems.push(draft);state.selectedMasterId=draft.id;state.masterMode='edit';}
|
||||
else{const current=masterItems.find(x=>x.id===state.selectedMasterId);if(current)Object.assign(current,draft);}
|
||||
state.masterDraft={...draft};state.masterErrors={};markDirty('ERP-MST-ITEM-001',false);renderT02(screen('ERP-MST-ITEM-001'));toast('품목을 저장했습니다.');return true;
|
||||
}
|
||||
function renderT02(s){
|
||||
if(!state.masterDraft)loadMasterDraft(state.selectedMasterId);
|
||||
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 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 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 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();
|
||||
}
|
||||
function bindT02(){
|
||||
bindLookups();
|
||||
$$('.master-select',workspace).forEach(button=>button.onclick=()=>requestMasterTarget(button.dataset.item));
|
||||
$$('[data-field]',workspace).forEach(input=>{if(input.readOnly)return;const event=input.type==='checkbox'?'change':'input';input.addEventListener(event,()=>{if(!state.masterDraft)return;const key=input.dataset.field;if(key==='warehouseName')return;if(key==='code')state.masterDraft.id=input.value;else if(key==='warehouseCode'){state.masterDraft.warehouseCode=input.value;state.masterDraft.warehouse='';}else state.masterDraft[key]=input.type==='checkbox'?input.checked:input.value;state.masterErrors={};markDirty('ERP-MST-ITEM-001',true);});});
|
||||
$$('[data-master-error]',workspace).forEach(button=>button.onclick=()=>{const field=button.dataset.masterError;const target=field==='warehouse'?$('.lookup-trigger',workspace):$(`[data-field="${CSS.escape(field==='id'?'code':field)}"]`,workspace);target?.focus();});
|
||||
const runSearch=()=>{state.masterQuery=$('#masterQueryInput')?.value||'';renderT02(screen('ERP-MST-ITEM-001'));};
|
||||
$('#masterSearchForm').onsubmit=e=>{e.preventDefault();runSearch();};$('#masterSearch').onclick=runSearch;$('#masterSearchTop').onclick=runSearch;
|
||||
$('#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'));};
|
||||
$('#auditButton').onclick=()=>showUtilityPanel(screen('ERP-MST-ITEM-001'),'help');
|
||||
}
|
||||
|
||||
function transactionTotals(){const valid=state.transactionLines.filter(line=>line.code);return {items:valid.length,qty:valid.reduce((sum,line)=>sum+Number(line.qty||0),0),amount:valid.reduce((sum,line)=>sum+Number(line.qty||0)*Number(line.price||0),0)};}
|
||||
function validateTransaction(){
|
||||
const h=state.transactionHeader;const errors=[];
|
||||
if(!h.orderDate.trim())errors.push({field:'orderDate',message:'주문일을 입력하세요.'});
|
||||
if(!h.customerCode.trim())errors.push({field:'customerCode',message:'거래처를 선택하세요.'});
|
||||
if(!h.warehouseCode.trim())errors.push({field:'warehouseCode',message:'출고창고를 선택하세요.'});
|
||||
if(!h.receiver.trim())errors.push({field:'receiver',message:'수취인을 입력하세요.'});
|
||||
if(!h.address.trim())errors.push({field:'address',message:'주소를 입력하세요.'});
|
||||
state.transactionLines.forEach((line,index)=>{if(!line.code)return;if(Number(line.qty)<=0)errors.push({row:index,field:'qty',message:`${index+1}행 수량은 0보다 커야 합니다.`});if(Number(line.price)<0)errors.push({row:index,field:'price',message:`${index+1}행 단가는 0 이상이어야 합니다.`});});
|
||||
if(!state.transactionLines.some(line=>line.code))errors.push({field:'lines',message:'품목을 1개 이상 입력하세요.'});
|
||||
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 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 renderT03(s){
|
||||
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 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 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>`;
|
||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT03();
|
||||
}
|
||||
function bindT03(){
|
||||
bindLookups();
|
||||
$$('[data-field]',workspace).forEach(input=>{if(input.readOnly)return;input.addEventListener('input',()=>{const key=input.dataset.field;if(key==='customerCode'){state.transactionHeader.customerCode=input.value;state.transactionHeader.customerName='';}else if(key==='warehouseCode'){state.transactionHeader.warehouseCode=input.value;state.transactionHeader.warehouseName='';}else if(key in state.transactionHeader)state.transactionHeader[key]=input.value;state.transactionErrors=[];markDirty('OMS-ORD-002',true);});});
|
||||
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();});
|
||||
$('#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 상태입니다.');};
|
||||
$('#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이 최종 검증합니다.');};
|
||||
$('#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('주문을 복사했습니다. 수취/배송정보를 새 주문에 맞게 입력하세요.');};
|
||||
}
|
||||
|
||||
function fastName(code){return ({ABC001:'운동화',ABC002:'러닝화',ABC003:'양말',ABC004:'셔츠'})[code]||'';}
|
||||
function fastErrorCount(){return state.fastRows.reduce((sum,row)=>sum+(row.code&&(!row.price||Number(String(row.price).replaceAll(',',''))<=0)?1:0),0);}
|
||||
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 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 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();
|
||||
}
|
||||
function bindT04(){
|
||||
const cells=$$('.fast-cell',workspace);
|
||||
cells.forEach((cell,index)=>{
|
||||
cell.oninput=()=>{const row=state.fastRows[Number(cell.dataset.row)];row[cell.dataset.field]=cell.value;if(cell.dataset.field==='code')row.name=fastName(cell.value.trim());markDirty('ERP-PRICE-001',true);};
|
||||
cell.onkeydown=e=>{if(e.key==='Enter'){e.preventDefault();cells[Math.min(index+1,cells.length-1)]?.focus();}};
|
||||
cell.onpaste=e=>{const text=e.clipboardData?.getData('text/plain');if(!text||(!text.includes('\t')&&!text.includes('\n')))return;e.preventDefault();const matrix=text.replace(/\r/g,'').split('\n').filter(Boolean).map(line=>line.split('\t'));const startRow=Number(cell.dataset.row);const fieldOrder=['code','price','date','remark'];const startField=Math.max(0,fieldOrder.indexOf(cell.dataset.field));matrix.forEach((values,rowOffset)=>values.forEach((value,colOffset)=>{const target=state.fastRows[startRow+rowOffset];const field=fieldOrder[startField+colOffset];if(target&&field){target[field]=value.trim();if(field==='code')target.name=fastName(target[field]);}}));markDirty('ERP-PRICE-001',true);renderT04(screen('ERP-PRICE-001'));toast(`${matrix.length}행 붙여넣기를 적용했습니다.`);};
|
||||
});
|
||||
$('#fastAdd').onclick=()=>{state.fastRows.push({code:'',name:'',price:'',date:'',remark:''});markDirty('ERP-PRICE-001',true);renderT04(screen('ERP-PRICE-001'));};
|
||||
$('#fastDuplicate').onclick=()=>{const last=[...state.fastRows].reverse().find(x=>x.code);if(last){state.fastRows.push({...last,code:'',name:''});markDirty('ERP-PRICE-001',true);renderT04(screen('ERP-PRICE-001'));}};
|
||||
$('#fastFill').onclick=()=>{const source=state.fastRows.find(x=>x.date)?.date||'2026-08-11';state.fastRows.forEach(row=>{if(row.code&&!row.date)row.date=source;});markDirty('ERP-PRICE-001',true);renderT04(screen('ERP-PRICE-001'));toast('적용일을 Fill Down 했습니다.');};
|
||||
$('#fastSave').onclick=()=>{markDirty('ERP-PRICE-001',false);toast('오류 없는 변경 행을 저장했습니다.');};
|
||||
}
|
||||
|
||||
function inventoryVisibleItems(){
|
||||
const keyword=state.inventorySearch.keyword.trim().toLocaleLowerCase('ko-KR');
|
||||
return inventoryItems.filter(item=>{
|
||||
const matchesKeyword=!keyword||`${item.id} ${item.name}`.toLocaleLowerCase('ko-KR').includes(keyword);
|
||||
const matchesWarehouse=state.inventorySearch.warehouse==='all'||item.locations.some(location=>location[0]===state.inventorySearch.warehouse);
|
||||
return matchesKeyword&&matchesWarehouse;
|
||||
});
|
||||
}
|
||||
function executeInventorySearch(){
|
||||
const warehouse=$('#inventoryWarehouse')?.value??state.inventorySearch.warehouse;const keyword=$('#inventoryKeyword')?.value??state.inventorySearch.keyword;
|
||||
state.inventorySearch={warehouse,keyword:keyword.trim()};state.inventoryDataState='loading';renderT05(screen('ERP-INV-001'));
|
||||
setTimeout(()=>{state.inventoryDataState='ready';const visible=inventoryVisibleItems();if(!visible.some(item=>item.id===state.inventoryItemId))state.inventoryItemId=visible[0]?.id||'';renderT05(screen('ERP-INV-001'));announce(`재고현황 ${visible.length}개 품목을 조회했습니다.`);},220);
|
||||
}
|
||||
function renderT05(s){
|
||||
const visibleItems=inventoryVisibleItems();let item=visibleItems.find(x=>x.id===state.inventoryItemId)||visibleItems[0]||null;if(item&&state.inventoryItemId!==item.id)state.inventoryItemId=item.id;
|
||||
const search=`<form class="search-panel" id="inventorySearch"><div class="search-fields"><label class="field-inline"><span>창고</span><select id="inventoryWarehouse" class="control"><option value="all" ${state.inventorySearch.warehouse==='all'?'selected':''}>전체</option><option ${state.inventorySearch.warehouse==='서울센터'?'selected':''}>서울센터</option><option ${state.inventorySearch.warehouse==='부산센터'?'selected':''}>부산센터</option><option ${state.inventorySearch.warehouse==='인천센터'?'selected':''}>인천센터</option></select></label><label class="field-inline"><span>품목</span><input id="inventoryKeyword" class="control wide" value="${esc(state.inventorySearch.keyword)}" placeholder="품목코드/품목명"></label></div><div class="search-actions">${btn('조회','primary','inventorySearchInline')}</div></form>`;
|
||||
let content='';
|
||||
if(state.inventoryDataState==='loading')content=dataState('loading','재고 조회 중...','창고와 품목 조건을 유지한 채 재고 Projection을 조회하고 있습니다.');
|
||||
else if(!item)content=dataState('empty','조회된 재고 품목이 없습니다.','조건을 초기화하고 다시 조회해 보세요.','inventoryReset');
|
||||
else{
|
||||
const leftRows=visibleItems.map(x=>`<tr class="${x.id===item.id?'selected-row':''}"><td><button class="row-link inventory-select" data-item="${esc(x.id)}" type="button">${esc(x.id)}</button></td><td>${esc(x.name)}</td><td class="num">${x.stock.toLocaleString('ko-KR')}</td></tr>`).join('');
|
||||
const rightRows=item.locations.filter(row=>state.inventorySearch.warehouse==='all'||row[0]===state.inventorySearch.warehouse).map(row=>`<tr><td>${esc(row[0])}</td><td>${esc(row[1])}</td><td class="num">${Number(row[2]).toLocaleString('ko-KR')}</td><td class="num">${Number(row[3]).toLocaleString('ko-KR')}</td><td class="num">${Number(row[4]).toLocaleString('ko-KR')}</td></tr>`).join('');
|
||||
const history=[['16:31','출고할당','','20',item.stock-20,'ORD-260811-1022'],['14:02','입고','100','',item.stock,'RCV-260811-0081'],['11:24','출고','','12',item.stock-100,'ORD-260811-0931']].map(row=>`<tr>${row.map((value,index)=>`<td class="${index>=2&&index<=4?'num':''}">${esc(value)}</td>`).join('')}</tr>`).join('');
|
||||
content=`<div class="context-bar"><strong>현재 조회</strong><span>품목 <b>${visibleItems.length}</b>개</span><span>창고 ${esc(state.inventorySearch.warehouse==='all'?'전체':state.inventorySearch.warehouse)}</span><button id="inventoryReset" type="button" class="text-action">조건 초기화</button></div><div class="split-layout"><section class="pane"><div class="pane-header"><strong>품목</strong><span>현재고</span></div>${grid(['품목코드','품목명','현재고'],leftRows,{caption:'재고 품목 목록'})}</section><section class="pane"><div class="pane-header"><strong>창고 / Location</strong><span>${esc(item.id)} ${esc(item.name)} · ${item.stock.toLocaleString('ko-KR')}개</span></div>${grid(['창고','Location','현재고','할당','보류'],rightRows||`<tr><td colspan="5" class="center">선택한 창고의 재고 위치가 없습니다.</td></tr>`,{caption:'품목별 창고 로케이션 재고'})}</section></div><section class="pane history-pane"><div class="pane-header"><strong>재고이력</strong><span>현재고 근거 추적 · ${esc(item.id)}</span></div>${grid(['일시','업무','입고','출고','잔량','참조번호'],history,{caption:'재고 이력'})}</section>`;
|
||||
}
|
||||
workspace.innerHTML=pageShell(s,`${search}${content}`,`<div class="command-group">${btn('조회 F3','','inventorySearchButton')}</div><span class="command-separator"></span><div class="command-group">${btn('재고이동')}${btn('엑셀 ▼')}</div>`);bindPageTools(s);
|
||||
$$('.inventory-select',workspace).forEach(button=>button.onclick=()=>{state.inventoryItemId=button.dataset.item;renderT05(s);announce(`${button.dataset.item} 재고 상세를 표시했습니다.`);});
|
||||
$('#inventorySearch').onsubmit=e=>{e.preventDefault();executeInventorySearch();};$('#inventorySearchButton').onclick=executeInventorySearch;const inventoryInline=$('#inventorySearchInline');if(inventoryInline)inventoryInline.onclick=executeInventorySearch;
|
||||
const reset=$('#inventoryReset');if(reset)reset.onclick=()=>{state.inventorySearch={warehouse:'all',keyword:''};state.inventoryItemId=inventoryItems[0].id;state.inventoryDataState='ready';renderT05(s);announce('재고 조회조건을 초기화했습니다.');};
|
||||
}
|
||||
|
||||
function queueVisibleRows(){
|
||||
const keyword=state.queueSearch.keyword.trim().toLocaleLowerCase('ko-KR');const worker=state.queueSearch.worker.trim().toLocaleLowerCase('ko-KR');
|
||||
return queueRows.filter(row=>{
|
||||
const quick=state.queueFilter==='all'||state.queueFilter==='exception'&&row.exception||state.queueFilter===row.type;
|
||||
const workerMatch=!worker||row.worker.toLocaleLowerCase('ko-KR').includes(worker);
|
||||
const keywordMatch=!keyword||`${row.no} ${row.type} ${row.wave} ${row.status}`.toLocaleLowerCase('ko-KR').includes(keyword);
|
||||
return quick&&workerMatch&&keywordMatch;
|
||||
});
|
||||
}
|
||||
function executeQueueSearch(){
|
||||
state.queueSearch={worker:($('#queueWorker')?.value??state.queueSearch.worker).trim(),keyword:($('#queueKeyword')?.value??state.queueSearch.keyword).trim()};state.selectedQueueRows.clear();state.queueActionResult='';state.queueDataState='loading';renderT06(screen('WMS-WORK-001'));setTimeout(()=>{state.queueDataState='ready';renderT06(screen('WMS-WORK-001'));announce(`작업 Queue ${queueVisibleRows().length}건을 조회했습니다.`);},220);
|
||||
}
|
||||
function renderT06(s){
|
||||
const visible=queueVisibleRows();
|
||||
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 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 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 search=`<form class="search-panel" id="queueSearchForm"><div class="search-fields"><label class="field-inline"><span>작업자</span><input id="queueWorker" class="control medium" value="${esc(state.queueSearch.worker)}" placeholder="작업자명"></label><label class="field-inline"><span>통합검색</span><input id="queueKeyword" class="control wide" value="${esc(state.queueSearch.keyword)}" placeholder="작업번호/Wave/상태"></label></div><div class="search-actions">${btn('조회','primary','queueSearchInline')}</div></form>`;
|
||||
let queueContent='';if(state.queueDataState==='loading')queueContent=dataState('loading','작업 Queue 조회 중...','현재 필터와 검색조건을 유지하고 최신 작업 상태를 조회합니다.');else if(!visible.length)queueContent=dataState('empty','처리할 작업이 없습니다.','검색조건 또는 예외 필터를 초기화해 보세요.','queueClearSearch');else queueContent=`<div class="context-bar"><strong>지금 처리할 업무</strong><span>표시 ${visible.length}건</span><span>선택 <b>${selectedCount}</b>건</span><span>지연 ${visible.filter(row=>row.status==='지연').length}건</span><button id="queueReset" type="button" class="text-action">빠른필터 해제</button></div>${selectedCount?`<div class="bulk-bar"><strong>${selectedCount}건 선택</strong>${btn('작업자 배정','primary','queueAssign')}${btn('Wave 생성','','queueWave')}</div>`:''}${result}${table}`;
|
||||
const body=`${search}<div class="queue-summary" role="group" aria-label="작업 유형 빠른 필터">${summary.map(([key,label,count])=>`<button type="button" data-queue-filter="${esc(key)}" class="${state.queueFilter===key?'active':''} ${key==='exception'?'danger':''}" aria-pressed="${state.queueFilter===key}"><span>${esc(label)}</span><b>${esc(count)}</b></button>`).join('')}</div>${queueContent}`;
|
||||
const commands=`<div class="command-group">${btn('조회 F3','','queueSearch')}</div><span class="command-separator"></span><div class="command-group">${btn('작업자 배정','primary','queueAssignTop',selectedCount===0)}${btn('Wave 생성','','queueWaveTop',selectedCount===0)}${btn('예외만 보기','','queueException')}</div>`;
|
||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT06();
|
||||
}
|
||||
function bindT06(){
|
||||
$$('[data-queue-filter]',workspace).forEach(button=>button.onclick=()=>{state.queueFilter=button.dataset.queueFilter;state.selectedQueueRows.clear();state.queueActionResult='';state.queueDataState='ready';renderT06(screen('WMS-WORK-001'));});
|
||||
const reset=$('#queueReset');if(reset)reset.onclick=()=>{state.queueFilter='all';state.selectedQueueRows.clear();renderT06(screen('WMS-WORK-001'));};$('#queueException').onclick=()=>{state.queueFilter='exception';state.selectedQueueRows.clear();state.queueDataState='ready';renderT06(screen('WMS-WORK-001'));};$('#queueSearch').onclick=executeQueueSearch;const inlineSearch=$('#queueSearchInline');if(inlineSearch)inlineSearch.onclick=executeQueueSearch;const form=$('#queueSearchForm');if(form)form.onsubmit=e=>{e.preventDefault();executeQueueSearch();};
|
||||
const clearSearch=$('#queueClearSearch');if(clearSearch)clearSearch.onclick=()=>{state.queueFilter='all';state.queueSearch={worker:'',keyword:''};state.queueDataState='ready';state.selectedQueueRows.clear();renderT06(screen('WMS-WORK-001'));};
|
||||
$$('[data-queue-row]',workspace).forEach(box=>box.onchange=()=>{box.checked?state.selectedQueueRows.add(box.dataset.queueRow):state.selectedQueueRows.delete(box.dataset.queueRow);renderT06(screen('WMS-WORK-001'));});
|
||||
const selectAll=$('#queueSelectAll');if(selectAll)selectAll.onchange=()=>{queueVisibleRows().forEach(row=>selectAll.checked?state.selectedQueueRows.add(row.no):state.selectedQueueRows.delete(row.no));renderT06(screen('WMS-WORK-001'));};
|
||||
const assign=()=>{if(!state.selectedQueueRows.size)return;for(const row of queueRows){if(state.selectedQueueRows.has(row.no))row.worker='홍길동';}state.queueActionResult=`${state.selectedQueueRows.size}건을 홍길동 작업자에게 배정했습니다.`;state.selectedQueueRows.clear();renderT06(screen('WMS-WORK-001'));};
|
||||
const wave=()=>{if(!state.selectedQueueRows.size)return;const waveNo='W260811-NEW';let changed=0;for(const row of queueRows){if(state.selectedQueueRows.has(row.no)&&row.wave==='-'){row.wave=waveNo;changed++;}}state.queueActionResult=changed?`${changed}건에 Wave ${waveNo}를 생성했습니다.`:'선택 작업에는 새 Wave를 생성할 대상이 없습니다.';state.selectedQueueRows.clear();renderT06(screen('WMS-WORK-001'));};
|
||||
for(const id of ['queueAssign','queueAssignTop']){const button=$('#'+id);if(button)button.onclick=assign;}for(const id of ['queueWave','queueWaveTop']){const button=$('#'+id);if(button)button.onclick=wave;}const clear=$('#queueResultClear');if(clear)clear.onclick=()=>{state.queueActionResult='';renderT06(screen('WMS-WORK-001'));};
|
||||
}
|
||||
|
||||
function reconcileVisibleRows(){
|
||||
const keyword=state.reconcileSearch.keyword.trim().toLocaleLowerCase('ko-KR');return reconcileRows.filter(row=>(!state.mismatchOnly||row.diff!==0)&&(!keyword||`${row.no} ${row.reason} ${row.status}`.toLocaleLowerCase('ko-KR').includes(keyword)));
|
||||
}
|
||||
function executeReconcileSearch(){
|
||||
state.reconcileSearch={date:($('#reconcileDate')?.value??state.reconcileSearch.date).trim(),keyword:($('#reconcileKeyword')?.value??state.reconcileSearch.keyword).trim()};state.selectedReconcileRows.clear();state.reconcileActionResult='';state.reconcileDataState='loading';renderT07(screen('COMMON-REC-001'));setTimeout(()=>{state.reconcileDataState='ready';renderT07(screen('COMMON-REC-001'));announce(`대사 결과 ${reconcileVisibleRows().length}건을 표시했습니다.`);},220);
|
||||
}
|
||||
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 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 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 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>`;
|
||||
let resultContent='';if(state.reconcileDataState==='loading')resultContent=dataState('loading','대사 결과 조회 중...','Expected/Actual/Difference Projection을 다시 조회하고 있습니다.');else if(!visible.length)resultContent=dataState('empty','조건에 맞는 대사 건이 없습니다.','불일치 필터 또는 검색어를 초기화해 보세요.','reconcileReset');else resultContent=`${selectedCount?`<div class="bulk-bar"><strong>${selectedCount}건 선택</strong>${btn('원인 재조회','','reconcileReason')}${btn('재처리','primary','reconcileRun')}${btn('처리사유 등록','','reconcileReasonNote')}</div>`:''}${result}${table}`;
|
||||
const body=`${search}<div class="reconcile-summary"><span>전체 <strong>12,482</strong></span><span>정상 <strong>12,439</strong></span><span class="mismatch">불일치 <strong>43</strong></span><span>표시 <strong>${visible.length}</strong></span><span>선택 <strong>${selectedCount}</strong></span></div>${resultContent}`;
|
||||
const commands=`<div class="command-group">${btn('조회 F3','','reconcileSearchButton')}</div><span class="command-separator"></span><div class="command-group">${btn('재처리','primary','reconcileRunTop',selectedCount===0)}${btn('엑셀 ▼')}</div>`;
|
||||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT07();
|
||||
}
|
||||
function bindT07(){
|
||||
const mismatch=$('#mismatchOnly');if(mismatch)mismatch.onchange=e=>{state.mismatchOnly=e.target.checked;state.selectedReconcileRows.clear();state.reconcileDataState='ready';renderT07(screen('COMMON-REC-001'));};const form=$('#reconcileSearch');if(form)form.onsubmit=e=>{e.preventDefault();executeReconcileSearch();};const reconcileInline=$('#reconcileSearchInline');if(reconcileInline)reconcileInline.onclick=executeReconcileSearch;$('#reconcileSearchButton').onclick=executeReconcileSearch;
|
||||
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'));});
|
||||
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'));};
|
||||
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'));};
|
||||
}
|
||||
|
||||
function applyImportFile(file){
|
||||
if(!file)return false;const name=String(file.name||'').trim();const size=Number(file.size||0);const validExt=/\.xlsx?$/i.test(name);const max=20*1024*1024;
|
||||
if(!validExt){toast('Excel .xlsx 또는 .xls 파일을 선택하세요.','error');return false}if(size>max){toast('파일 크기는 20MB 이하여야 합니다.','error');return false}
|
||||
state.importFileName=name;state.importFileSize=size;state.importFileType=file.type||'';state.importJobStatus='idle';state.importProgress=0;state.importProcessed=0;renderT08(screen('OMS-ORD-003'));announce(`${name} 파일을 선택했습니다.`);return true;
|
||||
}
|
||||
function renderT08(s){
|
||||
const step=state.importStep;const steps=['파일','매핑','검증','미리보기','반영','결과'];
|
||||
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===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===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===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===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===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>`;
|
||||
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>`:'';
|
||||
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();
|
||||
}
|
||||
function startImportJob(){
|
||||
if(importTimer)clearInterval(importTimer);state.importStep=5;state.importJobStatus='running';state.importProgress=0;state.importProcessed=0;renderT08(screen('OMS-ORD-003'));
|
||||
importTimer=setInterval(()=>{const increment=state.importProgress<60?11:state.importProgress<90?7:3;state.importProgress=Math.min(100,state.importProgress+increment);state.importProcessed=Math.floor(17894*state.importProgress/100);if(state.activeId==='OMS-ORD-003'&&state.importStep===5)renderT08(screen('OMS-ORD-003'));if(state.importProgress>=100){clearInterval(importTimer);importTimer=null;state.importJobStatus='completed';state.importProcessed=17894;if(state.activeId==='OMS-ORD-003'&&state.importStep===5)renderT08(screen('OMS-ORD-003'));toast('Excel 주문 Import가 완료되었습니다.');}},180);
|
||||
}
|
||||
function bindT08(){
|
||||
const trigger=$('#fileSelect');const input=$('#importFileInput');if(trigger&&input)trigger.onclick=()=>input.click();if(input)input.onchange=()=>{const file=input.files?.[0];if(file)applyImportFile(file);};
|
||||
const drop=$('#dropZone');if(drop){drop.ondragover=e=>{e.preventDefault();drop.classList.add('dragging')};drop.ondragleave=()=>drop.classList.remove('dragging');drop.ondrop=e=>{e.preventDefault();drop.classList.remove('dragging');applyImportFile(e.dataTransfer?.files?.[0]);};drop.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();input?.click();}};}
|
||||
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 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'));};
|
||||
}
|
||||
|
||||
function renderT09(s){
|
||||
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 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>`;
|
||||
bindT09();
|
||||
}
|
||||
function applyScan(value){
|
||||
const normalized=value.trim();state.scanBuffer='';if(!normalized)return;const now=Date.now();
|
||||
if(normalized===state.lastScanValue&&now-state.lastScanAt<700){toast('중복 스캔을 감지해 무시했습니다. 수량은 변경되지 않았습니다.','error');setTimeout(()=>$('#scanBox')?.focus(),0);return;}
|
||||
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(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);
|
||||
}
|
||||
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';};}
|
||||
$('#pickOne').onclick=()=>{if(state.pickingQty<state.pickingRequired)state.pickingQty++;state.scanError='';renderT09(screen('WMS-PICK-001'));if(state.pickingQty===state.pickingRequired)toast('상품 피킹 완료. 다음 상품으로 진행합니다.');};
|
||||
$('#pickQty').onclick=()=>{$('#quantityInput').value=String(state.pickingQty);$('#quantityInput').max=String(state.pickingRequired);quantityDialog.showModal();setTimeout(()=>$('#quantityInput').focus(),0);};
|
||||
$('#stockIssue').onclick=()=>{exceptionDialog.showModal();setTimeout(()=>$('input[name="exceptionReason"]',exceptionDialog)?.focus(),0);};
|
||||
}
|
||||
|
||||
function renderActive(){ const s=screen(state.activeId);app.dataset.mode=s.template==='T09'?'wms-mobile':'desktop';if(s.id==='HOME')return renderHome();({T01:renderT01,T02:renderT02,T03:renderT03,T04:renderT04,T05:renderT05,T06:renderT06,T07:renderT07,T08:renderT08,T09:renderT09}[s.template]||renderHome)(s); }
|
||||
function renderAll(){renderNavigation();renderTabs();renderActive();}
|
||||
|
||||
function openSearch(){closeSystemPanel(false);state.searchIndex=0;searchInput.value='';searchInput.setAttribute('aria-expanded','true');renderSearchResults();if(!searchDialog.open)searchDialog.showModal();setTimeout(()=>searchInput.focus(),0);}
|
||||
function searchMatches(){const q=searchInput.value.trim().toLowerCase();return screens.filter(x=>x.id!=='HOME').filter(x=>!q||[x.id,x.title,x.module,x.group,...x.keywords].join(' ').toLowerCase().includes(q));}
|
||||
function renderSearchResults(){
|
||||
const list=searchMatches();if(state.searchIndex>=list.length)state.searchIndex=Math.max(0,list.length-1);searchInput.setAttribute('aria-activedescendant',list[state.searchIndex]?`menu-result-${list[state.searchIndex].id}`:'');
|
||||
searchResults.innerHTML=list.length?list.map((item,index)=>`<button id="menu-result-${esc(item.id)}" type="button" class="search-result ${index===state.searchIndex?'active':''}" data-screen="${esc(item.id)}" role="option" aria-selected="${index===state.searchIndex}"><span class="code">${esc(item.id)}</span><span class="copy"><b>${esc(item.title)}</b><small>${esc(item.group)} · ${esc(item.template)}</small></span><span class="module">${esc(labelModule(item.module))}</span></button>`).join(''):'<div class="search-empty">검색 결과가 없습니다.</div>';
|
||||
$$('.search-result',searchResults).forEach((button,index)=>{button.onmousemove=()=>{state.searchIndex=index;$$('.search-result',searchResults).forEach((x,i)=>x.classList.toggle('active',i===index));};button.onclick=()=>{searchDialog.close();openScreen(button.dataset.screen);};});
|
||||
}
|
||||
|
||||
searchInput.oninput=()=>{state.searchIndex=0;renderSearchResults();};
|
||||
searchInput.onkeydown=e=>{const list=searchMatches();if(e.key==='ArrowDown'){e.preventDefault();state.searchIndex=Math.min(list.length-1,state.searchIndex+1);renderSearchResults();$('.search-result.active',searchResults)?.scrollIntoView({block:'nearest'});}else if(e.key==='ArrowUp'){e.preventDefault();state.searchIndex=Math.max(0,state.searchIndex-1);renderSearchResults();$('.search-result.active',searchResults)?.scrollIntoView({block:'nearest'});}else if(e.key==='Enter'&&list[state.searchIndex]){e.preventDefault();searchDialog.close();openScreen(list[state.searchIndex].id);}};
|
||||
|
||||
$('#homeButton').onclick=()=>openScreen('HOME');$('#sideHomeButton').onclick=()=>openScreen('HOME');$('#globalSearchButton').onclick=openSearch;$('#sideSearchButton').onclick=openSearch;$('#searchCloseButton').onclick=()=>searchDialog.close();
|
||||
searchDialog.addEventListener('close',()=>searchInput.setAttribute('aria-expanded','false'));
|
||||
$('#lookupCloseButton').onclick=()=>lookupDialog.close();$('#lookupSearchForm').onsubmit=e=>{e.preventDefault();state.lookupIndex=0;renderLookupResults();};lookupSearchInput.oninput=()=>{state.lookupIndex=0;renderLookupResults();};
|
||||
lookupSearchInput.onkeydown=e=>{const list=lookupItems();if(e.key==='ArrowDown'){e.preventDefault();state.lookupIndex=Math.min(Math.max(0,list.length-1),state.lookupIndex+1);renderLookupResults();$('.lookup-result.active',lookupResults)?.scrollIntoView({block:'nearest'});}else if(e.key==='ArrowUp'){e.preventDefault();state.lookupIndex=Math.max(0,state.lookupIndex-1);renderLookupResults();$('.lookup-result.active',lookupResults)?.scrollIntoView({block:'nearest'});}else if(e.key==='Enter'&&list[state.lookupIndex]){e.preventDefault();selectLookup();}};$('#lookupSelectButton').onclick=selectLookup;lookupDialog.addEventListener('close',()=>{if(document.activeElement===document.body)lookupRestoreFocus?.focus();});
|
||||
$('#collapseButton').onclick=()=>{state.collapsed=!state.collapsed;persist();renderNavigation();};
|
||||
$('#densityButton').onclick=()=>{state.density=state.density==='compact'?'comfortable':'compact';persist();renderNavigation();toast(`화면 밀도를 ${state.density==='compact'?'Compact':'Comfortable'}로 변경했습니다.`);};
|
||||
$('#themeButton').onclick=()=>{state.theme=state.theme==='dark'?'light':'dark';persist();renderNavigation();toast(`KBX Business ${state.theme==='dark'?'Dark':'Light'} 테마를 적용했습니다.`);};
|
||||
$('#moduleSelect').onchange=e=>{state.module=e.target.value;renderNavigation();if(state.activeId==='HOME')renderHome();};
|
||||
$('#drawerClose').onclick=closeDrawer;drawerBackdrop.onclick=closeDrawer;$('#systemPanelClose').onclick=()=>closeSystemPanel();$$('[data-panel]').forEach(button=>button.onclick=()=>showSystemPanel(button.dataset.panel));
|
||||
drawer.onkeydown=e=>{if(e.key==='Escape'){e.preventDefault();closeDrawer();return;}if(e.key!=='Tab')return;const focusables=$$('button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),a[href]',drawer);if(!focusables.length)return;const first=focusables[0],last=focusables[focusables.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}};
|
||||
|
||||
function clearUnsavedPending(){state.pendingCloseId=null;state.pendingNavigationId=null;state.pendingNavigationOptions=null;state.pendingMasterId=null;state.pendingUnsavedMode=null;}
|
||||
function continuePendingNavigation(message){
|
||||
const target=state.pendingNavigationId;const options=state.pendingNavigationOptions||{};clearUnsavedPending();unsavedDialog.close();
|
||||
if(target){openScreen(target,{...options,force:true});toast(message);}
|
||||
}
|
||||
function trySaveDirtyScreen(id){
|
||||
if(id==='ERP-MST-ITEM-001'){if(!validateMasterDraft())return false;return saveMaster();}
|
||||
if(id==='OMS-ORD-002'){if(!validateTransaction())return false;markDirty(id,false);toast('주문을 저장했습니다.');return true;}
|
||||
if(id==='ERP-PRICE-001'){if(fastErrorCount()>0)return false;markDirty(id,false);toast('오류 없는 변경 행을 저장했습니다.');return true;}
|
||||
markDirty(id,false);return true;
|
||||
}
|
||||
$('#unsavedStay').onclick=()=>{clearUnsavedPending();};
|
||||
$('#unsavedDiscard').onclick=()=>{
|
||||
if(state.pendingUnsavedMode==='master-select'){const target=state.pendingMasterId;markDirty('ERP-MST-ITEM-001',false);clearUnsavedPending();unsavedDialog.close();if(target)performMasterTarget(target);toast('저장하지 않은 품목 변경을 버리고 전환했습니다.');return;}
|
||||
if(state.pendingUnsavedMode==='navigate'){markDirty(state.activeId,false);continuePendingNavigation('저장하지 않은 변경을 버리고 이동했습니다.');return;}
|
||||
const id=state.pendingCloseId;clearUnsavedPending();unsavedDialog.close();if(id){markDirty(id,false);closeTabNow(id);toast('저장하지 않은 변경을 버리고 화면을 닫았습니다.');}
|
||||
};
|
||||
$('#unsavedSave').onclick=()=>{
|
||||
if(state.pendingUnsavedMode==='master-select'){const target=state.pendingMasterId;if(!validateMasterDraft()){clearUnsavedPending();unsavedDialog.close();renderT02(screen('ERP-MST-ITEM-001'));setTimeout(()=>$('[data-master-error]',workspace)?.focus(),0);return;}saveMaster();clearUnsavedPending();unsavedDialog.close();if(target)performMasterTarget(target);return;}
|
||||
if(state.pendingUnsavedMode==='navigate'){const activeId=state.activeId;if(!trySaveDirtyScreen(activeId)){const mode=state.pendingUnsavedMode;clearUnsavedPending();unsavedDialog.close();if(activeId==='ERP-MST-ITEM-001')renderT02(screen(activeId));if(activeId==='OMS-ORD-002')renderT03(screen(activeId));if(activeId==='ERP-PRICE-001')renderT04(screen(activeId));toast('입력 오류가 있어 저장 후 이동할 수 없습니다.','error');return;}continuePendingNavigation('변경사항을 저장하고 이동했습니다.');return;}
|
||||
const id=state.pendingCloseId;if(id&&!trySaveDirtyScreen(id)){clearUnsavedPending();unsavedDialog.close();toast('입력 오류가 있어 저장 후 닫을 수 없습니다.','error');return;}clearUnsavedPending();unsavedDialog.close();if(id){closeTabNow(id);toast('변경사항을 저장하고 화면을 닫았습니다.');}
|
||||
};
|
||||
$('#quantityApply').onclick=()=>{const value=Number($('#quantityInput').value);if(!Number.isInteger(value)||value<0||value>state.pickingRequired){$('#quantityHelp').textContent=`0 ~ ${state.pickingRequired} 사이 정수를 입력하세요.`;$('#quantityInput').focus();return;}state.pickingQty=value;state.scanError='';quantityDialog.close();renderT09(screen('WMS-PICK-001'));toast('피킹 수량을 변경했습니다.');};
|
||||
$('#exceptionApply').onclick=()=>{const reason=$('input[name="exceptionReason"]:checked',exceptionDialog)?.value||'기타';exceptionDialog.close();toast(`재고부족 사유 '${reason}'를 등록했습니다.`,'error');};
|
||||
|
||||
document.addEventListener('keydown',e=>{
|
||||
if(e.defaultPrevented)return;
|
||||
const modalOpen=[searchDialog,lookupDialog,unsavedDialog,quantityDialog,exceptionDialog].some(dialog=>dialog.open);
|
||||
if(modalOpen)return;
|
||||
if(e.key==='Escape'&&!systemPanel.hidden){e.preventDefault();closeSystemPanel();return;}
|
||||
if(e.key==='Escape'&&drawer.classList.contains('open')){e.preventDefault();closeDrawer();return;}
|
||||
if(drawer.classList.contains('open'))return;
|
||||
if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='k'){e.preventDefault();openSearch();return;}
|
||||
if(e.key==='F2'&&['T02','T03'].includes(screen(state.activeId).template)){const root=document.activeElement?.closest?.('.lookup')||$('.lookup',workspace);if(root){e.preventDefault();openLookup(root);return;}}
|
||||
if(e.key==='F3'&&state.activeId!=='HOME'){e.preventDefault();const searchButton=state.activeId==='OMS-ORD-001'?$('#commandSearch'):state.activeId==='ERP-MST-ITEM-001'?$('#masterSearchTop'):state.activeId==='ERP-INV-001'?$('#inventorySearchButton'):state.activeId==='WMS-WORK-001'?$('#queueSearch'):state.activeId==='COMMON-REC-001'?$('#reconcileSearchButton'):null;if(searchButton)searchButton.click();else toast(`${screen(state.activeId).title} 조회를 실행했습니다.`);}
|
||||
if(e.key==='F8'&&['T02','T03','T04'].includes(screen(state.activeId).template)){e.preventDefault();const saveButton=state.activeId==='ERP-MST-ITEM-001'?$('#masterSave'):state.activeId==='OMS-ORD-002'?$('#saveTransaction'):state.activeId==='ERP-PRICE-001'?$('#fastSave'):null;if(saveButton&&!saveButton.disabled)saveButton.click();}
|
||||
});
|
||||
window.addEventListener('hashchange',()=>{const found=screens.find(x=>x.path===location.hash);if(!found||found.id===state.activeId)return;const opened=openScreen(found.id,{updateHash:false});if(!opened){state.pendingNavigationOptions={...(state.pendingNavigationOptions||{}),updateHash:true};history.replaceState(null,'',screen(state.activeId).path);}});
|
||||
window.addEventListener('beforeunload',event=>{if(state.tabs.some(tab=>tab.dirty)){event.preventDefault();event.returnValue='';}});
|
||||
|
||||
unsavedDialog.addEventListener('close',()=>{if(state.pendingUnsavedMode)clearUnsavedPending();});
|
||||
restore();restoreWorkspaceSession();
|
||||
const params=new URLSearchParams(location.search);const demoState=params.get('state');if(['ready','loading','empty','error'].includes(demoState))state.orderDataState=demoState;const network=params.get('network');if(['online','unstable','offline'].includes(network))state.networkStatus=network;const theme=params.get('theme');if(['light','dark'].includes(theme))state.theme=theme;
|
||||
const byHash=screens.find(x=>x.path===location.hash);if(byHash)state.activeId=byHash.id;
|
||||
if(state.activeId!=='HOME'&&!state.tabs.some(t=>t.id===state.activeId))state.tabs.push({id:state.activeId,title:screen(state.activeId).title,pinned:false,dirty:false,resumed:false});persistWorkspaceSession();
|
||||
renderAll();
|
||||
})();
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"viewport": "1440x900",
|
||||
"method": "Headless Chromium interaction harness using the same index/css/js with resources inlined because loopback/file navigation is blocked by the execution sandbox.",
|
||||
"runtimeErrors": 0,
|
||||
"checks": {
|
||||
"homeRendered": true,
|
||||
"ctrlKSearchOpenedT01": true,
|
||||
"t01QuickFilterChangedRows": true,
|
||||
"t01SelectionEnabledBulkAction": true,
|
||||
"drawerEscapeRecovery": true,
|
||||
"dirtyTabCloseGuard": true,
|
||||
"t05MasterDetailChanged": true,
|
||||
"t06ExceptionFilter": true,
|
||||
"t07MismatchToggle": true,
|
||||
"t08FileGateAndStep2": true,
|
||||
"t09WrongBarcodeError": true,
|
||||
"t09CorrectBarcodeIncrement": true,
|
||||
"t09QuantityDialog": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"version": "v39",
|
||||
"passed": 31,
|
||||
"total": 31,
|
||||
"results": [
|
||||
{
|
||||
"name": "home stock intent opens T01",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "home stock intent applies filter",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "stock filter narrows visible rows",
|
||||
"ok": true,
|
||||
"detail": "1"
|
||||
},
|
||||
{
|
||||
"name": "T01 Space selects focused row",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T01 Enter opens detail drawer",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "drawer makes background inert",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "drawer Esc restores background",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "dirty navigation opens guard",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "dirty navigation keeps T03 until decision",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "discard continues pending navigation",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 F2 opens lookup dialog",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "lookup Enter selects warehouse",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "lookup selection advances focus",
|
||||
"ok": true,
|
||||
"detail": "barcode"
|
||||
},
|
||||
{
|
||||
"name": "help opens actual drawer content",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "suggestion form submits and closes",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 opens after save",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 reaches preview step",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 background job progresses",
|
||||
"ok": true,
|
||||
"detail": "22"
|
||||
},
|
||||
{
|
||||
"name": "T08 can leave while job runs",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 return restores progress",
|
||||
"ok": true,
|
||||
"detail": "(22, 55)"
|
||||
},
|
||||
{
|
||||
"name": "T08 job completes",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "WMS valid scan increments once",
|
||||
"ok": true,
|
||||
"detail": "5"
|
||||
},
|
||||
{
|
||||
"name": "WMS duplicate scan does not increment",
|
||||
"ok": true,
|
||||
"detail": "(5, 5)"
|
||||
},
|
||||
{
|
||||
"name": "runtime console/page errors",
|
||||
"ok": true,
|
||||
"detail": "[]"
|
||||
},
|
||||
{
|
||||
"name": "1280x720 no document horizontal overflow",
|
||||
"ok": true,
|
||||
"detail": "[1280, 1280]"
|
||||
},
|
||||
{
|
||||
"name": "1280x720 grid remains in local scroll container",
|
||||
"ok": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1280 runtime errors",
|
||||
"ok": true,
|
||||
"detail": "[]"
|
||||
},
|
||||
{
|
||||
"name": "narrow/zoom-pressure shell avoids document overflow",
|
||||
"ok": true,
|
||||
"detail": "[720, 720]"
|
||||
},
|
||||
{
|
||||
"name": "narrow shell collapses side nav",
|
||||
"ok": true,
|
||||
"detail": "56px"
|
||||
},
|
||||
{
|
||||
"name": "narrow runtime errors",
|
||||
"ok": true,
|
||||
"detail": "[]"
|
||||
},
|
||||
{
|
||||
"name": "WMS 390x844 uses full mobile viewport",
|
||||
"ok": true,
|
||||
"detail": "mode=wms-mobile, header=none, width=390px, scroll=[390, 390], errors=[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"version": "v40",
|
||||
"passed": 49,
|
||||
"total": 49,
|
||||
"results": [
|
||||
{
|
||||
"name": "home renders",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "home actionable attention 4",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T01 channel+keyword filters actual rows",
|
||||
"pass": true,
|
||||
"detail": "rows=1"
|
||||
},
|
||||
{
|
||||
"name": "T01 query values preserved after loading render",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T01 selection enables ship",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T01 reset clears actual criteria",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 F8 uses validation not dirty bypass",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 lookup dialog opens",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 lookup selected warehouse",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 new master save adds list row",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 save clears dirty indicator",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 master selection dirty guard",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T02 discard then select target",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T03 F8 blocks invalid line",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T03 valid save clears dirty",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T03 explicit state transition",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T03 confirmed disables confirm",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T06 selection enables assign",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T06 assignment changes row state",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T06 action result visible",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T07 reprocess resolves selected mismatch",
|
||||
"pass": true,
|
||||
"detail": "2->1"
|
||||
},
|
||||
{
|
||||
"name": "T07 result feedback persists",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "browser hash navigation dirty guard opens",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "browser hash rolled back while pending",
|
||||
"pass": true,
|
||||
"detail": "#items"
|
||||
},
|
||||
{
|
||||
"name": "discard completes pending route",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 background job running",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 can leave job",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T09 mobile hides desktop shell",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T09 validated scan increments",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "runtime console/page errors zero",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "Ctrl+K opens menu search",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "menu search Enter opens target",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "side nav collapse state",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "density toggle applies comfortable",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "utility drawer opens",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "drawer makes shell inert",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "drawer Esc restores shell",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T04 invalid price disables save",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T04 corrected price restores save",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T04 save clears dirty before navigation",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T05 master detail context changes",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 job progresses while away",
|
||||
"pass": true,
|
||||
"detail": "11->33"
|
||||
},
|
||||
{
|
||||
"name": "T08 job completes asynchronously",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T08 result step reachable",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T09 duplicate scan does not double increment",
|
||||
"pass": true,
|
||||
"detail": "5->5"
|
||||
},
|
||||
{
|
||||
"name": "T09 wrong barcode recovery visible",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "T09 390 full viewport width",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
},
|
||||
{
|
||||
"name": "1280x720 document horizontal overflow none",
|
||||
"pass": true,
|
||||
"detail": "0"
|
||||
},
|
||||
{
|
||||
"name": "supplement runtime errors zero",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
}
|
||||
],
|
||||
"runtimeErrors": [],
|
||||
"note": "Production DOM/CSS/JS tested in inline-asset browser harness because container browser navigation is administratively blocked."
|
||||
}
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"version": "v43",
|
||||
"scope": "inline-asset browser QA for FE Reference Lab; production CSP remains statically validated",
|
||||
"passed": 16,
|
||||
"total": 16,
|
||||
"checks": [
|
||||
{
|
||||
"name": "initial-light",
|
||||
"pass": true,
|
||||
"detail": "light"
|
||||
},
|
||||
{
|
||||
"name": "toggle-dark",
|
||||
"pass": true,
|
||||
"detail": "dark"
|
||||
},
|
||||
{
|
||||
"name": "dark-text-recomputed",
|
||||
"pass": true,
|
||||
"detail": "rgb(229, 231, 235)"
|
||||
},
|
||||
{
|
||||
"name": "theme-aria",
|
||||
"pass": true,
|
||||
"detail": "밝은 테마로 변경"
|
||||
},
|
||||
{
|
||||
"name": "T01-dark-render",
|
||||
"pass": true,
|
||||
"detail": "OMS-ORD-001:T01"
|
||||
},
|
||||
{
|
||||
"name": "T02-dark-render",
|
||||
"pass": true,
|
||||
"detail": "ERP-MST-ITEM-001:T02"
|
||||
},
|
||||
{
|
||||
"name": "T03-dark-render",
|
||||
"pass": true,
|
||||
"detail": "OMS-ORD-002:T03"
|
||||
},
|
||||
{
|
||||
"name": "T04-dark-render",
|
||||
"pass": true,
|
||||
"detail": "ERP-PRICE-001:T04"
|
||||
},
|
||||
{
|
||||
"name": "T05-dark-render",
|
||||
"pass": true,
|
||||
"detail": "ERP-INV-001:T05"
|
||||
},
|
||||
{
|
||||
"name": "T06-dark-render",
|
||||
"pass": true,
|
||||
"detail": "WMS-WORK-001:T06"
|
||||
},
|
||||
{
|
||||
"name": "T07-dark-render",
|
||||
"pass": true,
|
||||
"detail": "COMMON-REC-001:T07"
|
||||
},
|
||||
{
|
||||
"name": "T08-dark-render",
|
||||
"pass": true,
|
||||
"detail": "OMS-ORD-003:T08"
|
||||
},
|
||||
{
|
||||
"name": "T09-dark-render",
|
||||
"pass": true,
|
||||
"detail": "WMS-PICK-001:wms-mobile:wms-mobile"
|
||||
},
|
||||
{
|
||||
"name": "1280-no-document-overflow",
|
||||
"pass": true,
|
||||
"detail": "False"
|
||||
},
|
||||
{
|
||||
"name": "toggle-light-back",
|
||||
"pass": true,
|
||||
"detail": "light"
|
||||
},
|
||||
{
|
||||
"name": "runtime-errors-zero",
|
||||
"pass": true,
|
||||
"detail": ""
|
||||
}
|
||||
],
|
||||
"pageErrors": []
|
||||
}
|
||||
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 182 KiB |