Files
kjh2064 3f293d8aa8
deploy / deploy (push) Successful in 1m52s
deploy / notify (push) Successful in 1s
V13-FE-006: consolidate approved UI and contract hardening
2026-08-13 02:41:00 +09:00

800 lines
135 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
'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:'READY',exception:''},
{no:'ORD-260811-1023',channel:'스마트스토어',time:'16:39',buyer:'이지은',item:'셔츠 BLACK',qty:1,amount:48000,stock:'⚠ 부족',status:'HOLD',exception:'재고부족'},
{no:'ORD-260811-1022',channel:'자사몰',time:'16:31',buyer:'박서준',item:'운동화 270',qty:2,amount:196000,stock:'정상',status:'CONFIRMED',exception:''},
{no:'ORD-260811-1021',channel:'쿠팡',time:'16:28',buyer:'최유진',item:'양말 3PACK',qty:4,amount:36000,stock:'정상',status:'READY',exception:''},
{no:'ORD-260811-1018',channel:'11번가',time:'16:12',buyer:'정하늘',item:'후드티 GRAY',qty:1,amount:69000,stock:'정상',status:'ERROR',exception:'송장실패'},
{no:'ORD-260811-1014',channel:'쿠팡',time:'15:57',buyer:'한도윤',item:'운동화 BLACK',qty:2,amount:178000,stock:'정상',status:'READY',exception:''},
{no:'ORD-260811-1007',channel:'자사몰',time:'15:43',buyer:'오서연',item:'스니커즈 WHITE',qty:1,amount:82000,stock:'정상',status:'NEW',exception:''},
{no:'ORD-260811-1002',channel:'스마트스토어',time:'15:31',buyer:'윤지호',item:'양말 WHITE',qty:5,amount:45000,stock:'정상',status:'READY',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:'DRAFT', transactionErrors:[], transactionHeader:{orderDate:'2026-08-11',channel:'직접주문',customerCode:'10001',customerName:'대한상사',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'홍길동',phone:'010-1234-5678',postal:'06236',address:'서울특별시 강남구 테헤란로'},
transactionLines:[{code:'ABC001',name:'운동화',spec:'BLACK / 270',available:83,qty:2,price:98000,remark:''},{code:'XYZ003',name:'양말',spec:'WHITE',available:721,qty:3,price:9000,remark:''}],
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, importFocusStep:0, importFileName:'', importFileSize:0, importFileType:'', importJobStatus:'idle', importProgress:0, importProcessed:0, importErrorIndex:0,
pickingQty:4, pickingRequired:6, scanBuffer:'', scanError:'', networkStatus:'online', pendingSync:0, lastScanValue:'', lastScanAt:0, lastConfirmedScanValue:'', lastConfirmedScanAt: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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
const orderStatusDefinitions={
NEW:{label:'신규',semantic:'pending'},DRAFT:{label:'작성',semantic:'draft'},CONFIRMED:{label:'확정',semantic:'pending'},ALLOCATED:{label:'할당',semantic:'processing'},PICKING:{label:'피킹',semantic:'processing'},CHECKED:{label:'검수',semantic:'processing'},SHIPPED:{label:'출고완료',semantic:'completed'},HOLD:{label:'보류',semantic:'hold'},ERROR:{label:'오류',semantic:'error'},READY:{label:'출고대기',semantic:'ready'}
};
const shipmentStatusDefinitions={...orderStatusDefinitions,CONFIRMED:{label:'출고지시',semantic:'processing'}};
const legacyStatusDefinitions={
'정상':{label:'정상',semantic:'ready'},'사용':{label:'사용',semantic:'completed'},'할당완료':{label:'할당완료',semantic:'completed'},'출고완료':{label:'출고완료',semantic:'completed'},'완료':{label:'완료',semantic:'completed'},
'신규':{label:'신규',semantic:'pending'},'출고대기':{label:'출고대기',semantic:'ready'},'미처리':{label:'미처리',semantic:'pending'},'확인필요':{label:'확인필요',semantic:'warning'},'진행':{label:'진행',semantic:'processing'},
'보류':{label:'보류',semantic:'hold'},'부족':{label:'부족',semantic:'warning'},'재고부족':{label:'재고부족',semantic:'warning'},'지연':{label:'지연',semantic:'error'},'오류':{label:'오류',semantic:'error'},'사용중지':{label:'사용중지',semantic:'disabled'}
};
function resolveStatus(value,context='generic'){
const raw=String(value??'').replace('⚠ ','');
const dictionary=context==='shipment'?shipmentStatusDefinitions:context==='lifecycle'?orderStatusDefinitions:null;
const definition=dictionary?.[raw]||orderStatusDefinitions[raw]||legacyStatusDefinitions[raw];
return definition?{...definition,raw,unknown:false}:{label:`정의되지 않음 · ${raw||'빈 값'}`,semantic:'warning',raw,unknown:true};
}
function statusSemantic(value,context='generic'){return resolveStatus(value,context).semantic;}
function statusLabel(value,context='generic'){return resolveStatus(value,context).label;}
function statusChip(value,context='generic'){
const resolved=resolveStatus(value,context);
return `<span class="business-status" data-semantic="${resolved.semantic}" ${resolved.unknown?'data-unknown="true"':''}><span class="business-status__dot" aria-hidden="true"></span>${esc(resolved.label)}</span>`;
}
function canonicalOrderStatus(value){
const raw=String(value??'all');
const legacy={신규:'NEW',출고대기:'READY',할당완료:'CONFIRMED',보류:'HOLD',오류:'ERROR',출고완료:'SHIPPED'};
const normalized=legacy[raw]||raw;
return ['all','NEW','READY','CONFIRMED','HOLD','ERROR','SHIPPED'].includes(normalized)?normalized:'all';
}
const knownIds = new Set(screens.map(x=>x.id));
const 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:canonicalOrderStatus(d.status),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&&current?.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)} &gt; ${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 attentionTriageGroups(items,expanded){
const perGroup=expanded?6:2;
const groups=[
{key:'urgent',label:'긴급·예외',description:'실패와 중요 알림부터 처리',tone:'danger',match:item=>item.critical},
{key:'draft',label:'미저장',description:'중단한 편집을 이어서 처리',tone:'warning',match:item=>item.source==='미저장'},
{key:'running',label:'진행 작업',description:'대량·연계 작업 상태 확인',tone:'info',match:item=>item.source==='진행 작업'},
{key:'notice',label:'알림',description:'업무 후속 확인 항목',tone:'default',match:item=>item.source==='알림'},
];
return groups.map(group=>{const all=items.filter(group.match);return {...group,count:all.length,items:all.slice(0,perGroup)}}).filter(group=>group.count);
}
function attentionLane(group){return `<section class="attention-lane" data-tone="${esc(group.tone)}"><header><div><strong>${esc(group.label)}</strong><small>${esc(group.description)}</small></div><b>${group.count}</b></header><div class="attention-lane-items">${group.items.map(item=>attention(item.source,item.title,item.detail,item.id,item.critical,item.intent)).join('')}</div>${group.count>group.items.length?`<button type="button" class="attention-lane-more" data-attention-lane="${esc(group.key)}">나머지 ${group.count-group.items.length}개 보기</button>`:''}</section>`;}
function continueItem(source,title,detail,id,type){return `<button type="button" class="continue-item" data-screen="${esc(id)}" data-source="${esc(type)}"><span class="continue-source">${esc(source)}</span><span class="continue-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">${type==='dirty'?'이어서':'열기'}</span><span aria-hidden="true"></span></button>`;}
function 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 allAttention=homeAttentionItems();
const moduleAttentionCount=module=>allAttention.filter(item=>screen(item.id).module===module).length;
const moduleRail=`<nav class="home-module-rail" aria-label="업무 영역 바로가기"><span class="home-module-label">업무영역</span><button type="button" data-home-module="ALL" aria-pressed="${state.module==='ALL'}"><b>전체</b><small>${allScreens.length}개 화면 · 확인 ${allAttention.length}</small></button>${moduleKeys.map(module=>{const items=allScreens.filter(item=>item.module===module);const favoriteCount=items.filter(item=>isFav(item.id)).length;const openCount=state.tabs.filter(tab=>screen(tab.id).module===module).length;return `<button type="button" data-home-module="${esc(module)}" data-module="${esc(module)}" aria-pressed="${state.module===module}"><b>${esc(labelModule(module))}</b><small>${items.length}개 · 확인 ${moduleAttentionCount(module)} · ★ ${favoriteCount} · 열림 ${openCount}</small></button>`;}).join('')}</nav>`;
const dirtyTab=state.tabs.find(t=>t.dirty);const resumedTab=state.tabs.find(t=>t.resumed&&!t.dirty);const regularOpen=state.tabs.find(t=>!t.dirty&&!t.resumed);
const attentionCap=24;const legacyCollapsedAttention=allAttention.slice(0,8);const triageGroups=attentionTriageGroups(allAttention,state.homeAttentionExpanded);const visibleAttention=triageGroups.flatMap(group=>group.items).slice(0,attentionCap);const attentionOverflow=Math.max(allAttention.length-visibleAttention.length,0);const attentionToggleLabel=state.homeAttentionExpanded?'상위 8개만 보기':allAttention.length<=attentionCap?`전체 ${allAttention.length}개 보기`:`우선 ${attentionCap}개 보기`;const attentionCenters=state.homeAttentionExpanded&&attentionOverflow?`<div class="attention-centers" aria-label="나머지 확인 항목"><button id="homeJobsAll" class="section-toggle" type="button">작업 센터 전체</button><button id="homeNoticesAll" class="section-toggle" type="button">알림 센터 전체</button></div>`:'';
workspace.innerHTML=`<section class="home-page"><header class="home-heading"><div><p>KBX 업무 시작</p><h1>홈</h1><small>검색하고, 이어서 처리하고, 예외만 확인하는 한국형 업무 시작점${state.workspaceDirtyDiscarded?` · 이전 미저장 ${state.workspaceDirtyDiscarded}개는 안전상 복원 제외`:''}</small></div><button class="home-search" id="homeSearch" type="button" aria-keyshortcuts="Control+K"><span>업무 화면 검색</span><kbd>Ctrl K</kbd></button></header>
<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">${triageGroups.map(attentionLane).join('')}</div></section>
<section class="home-section"><div class="section-head"><div><h2>바로 시작</h2><small>미저장 → 열린 업무 → 즐겨찾기 → 최근 순으로 중복 없이 연결</small></div><span class="section-count">업무 문맥 유지</span></div><div class="continue-list">${continueItem('미저장',dirtyTab?.title||'주문등록',dirtyTab?`${screen(dirtyTab.id).module} · 저장하지 않은 변경`:'OMS · 주문 · ORD 임시작성',dirtyTab?.id||'OMS-ORD-002','dirty')}${resumedTab?continueItem('복원',resumedTab.title,`${screen(resumedTab.id).module} · 이전 세션의 업무 위치`,resumedTab.id,'open'):continueItem('열림',regularOpen?.title||'품목관리',regularOpen?`${screen(regularOpen.id).module} · 열린 업무`:'ERP · 기준정보',regularOpen?.id||'ERP-MST-ITEM-001','open')}${continueItem('즐겨찾기','재고현황','ERP · 재고','ERP-INV-001','favorite')}${continueItem('최근','오늘 물류작업','WMS · 작업관리','WMS-WORK-001','recent')}</div></section>
<section 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');const railButtons=$$('[data-home-module]',workspace);railButtons.forEach((button,index)=>{button.onclick=()=>{state.module=button.dataset.homeModule;$('#moduleSelect').value=state.module;renderNavigation();renderHome();setTimeout(()=>{$('#homeExplorer')?.scrollIntoView({block:'start',behavior:'smooth'});$('#homeExplorer')?.focus({preventScroll:true});},0);};button.onkeydown=e=>{let next=index;if(e.key==='ArrowRight')next=(index+1)%railButtons.length;else if(e.key==='ArrowLeft')next=(index-1+railButtons.length)%railButtons.length;else if(e.key==='Home')next=0;else if(e.key==='End')next=railButtons.length-1;else return;e.preventDefault();railButtons[next]?.focus();};});const showAll=$('#homeShowAll');if(showAll)showAll.onclick=()=>{state.module='ALL';$('#moduleSelect').value='ALL';renderNavigation();renderHome();setTimeout(()=>$('#homeExplorer')?.focus({preventScroll:true}),0);};const attentionToggle=$('#homeAttentionToggle');if(attentionToggle)attentionToggle.onclick=()=>{state.homeAttentionExpanded=!state.homeAttentionExpanded;renderHome();};const jobsAll=$('#homeJobsAll');if(jobsAll)jobsAll.onclick=()=>showSystemPanel('jobs');const noticesAll=$('#homeNoticesAll');if(noticesAll)noticesAll.onclick=()=>showSystemPanel('notices');$$('.attention-item',workspace).forEach(b=>b.onclick=()=>b.dataset.intent?applyIntent(b.dataset.intent):openScreen(b.dataset.screen));$$('.attention-lane-more',workspace).forEach(button=>button.onclick=()=>{if(button.dataset.attentionLane==='running')showSystemPanel('jobs');else if(button.dataset.attentionLane==='notice')showSystemPanel('notices');else{state.homeAttentionExpanded=true;renderHome();}});$$('.continue-item',workspace).forEach(b=>b.onclick=()=>openScreen(b.dataset.screen));$$('.home-menu-row',workspace).forEach(row=>{$('.open-screen',row).onclick=()=>openScreen(row.dataset.screen);$('.fav',row).onclick=()=>toggleFavorite(row.dataset.screen);});
}
function filteredOrders(){
let visible=orders;
if(state.orderQuickFilter==='new')visible=visible.filter(x=>x.status==='NEW');
else if(state.orderQuickFilter==='ready')visible=visible.filter(x=>x.status==='READY');
else if(state.orderQuickFilter==='stock')visible=visible.filter(x=>x.stock.includes('부족'));
else if(state.orderQuickFilter==='error')visible=visible.filter(x=>x.exception||x.status==='ERROR');
if(state.orderSearch.channel!=='all')visible=visible.filter(x=>x.channel===state.orderSearch.channel);
if(state.orderSearch.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:canonicalOrderStatus(state.orderSearch.status),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('NEW','신규',state.orderSearch.status)}${searchOption('READY','출고대기',state.orderSearch.status)}${searchOption('CONFIRMED','출고지시',state.orderSearch.status)}${searchOption('SHIPPED','출고완료',state.orderSearch.status)}${searchOption('HOLD','보류',state.orderSearch.status)}${searchOption('ERROR','오류',state.orderSearch.status)}</select></label><label class="field-inline"><span>통합검색</span><input id="orderKeyword" class="control wide" value="${esc(state.orderSearch.keyword)}" placeholder="주문번호/주문자/상품" maxlength="80"></label></div><div class="search-actions">${btn('조회','primary','searchExecute')}${btn(state.orderAdvanced?`상세조건 ${activeCount?`(${activeCount})`:''} 닫기`:`상세조건${activeCount?` ${activeCount}`:''}`,'','toggleAdvanced')}${btn('초기화','','searchReset')}${btn('기본조건 저장','','saveOrderDefaults')}</div>${advanced}</form>`;
}
function captureOrderSearch(){
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">${statusChip(order.stock)}</td><td class="center">${statusChip(order.status,'shipment')}</td><td class="${order.exception?'error':''}">${esc(order.exception||'-')}</td></tr>`).join('');
}
function renderOrderData(){
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">&lt;input&gt;</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">${statusChip(order.status,'shipment')}${order.exception?`<span class="status-chip danger">${esc(order.exception)}</span>`:''}</div><div class="detail-list"><div class="detail-row"><span>판매채널</span><strong>${esc(order.channel)}</strong></div><div class="detail-row"><span>주문자</span><strong>${esc(order.buyer)} / 010-****-1234</strong></div><div class="detail-row"><span>대표상품</span><strong>${esc(order.item)}</strong></div><div class="detail-row"><span>주문금액</span><strong>₩${order.amount.toLocaleString('ko-KR')}</strong></div><div class="detail-row"><span>예외</span><strong>${esc(order.exception||'없음')}</strong></div></div><h3>처리이력</h3><div class="detail-list"><div class="detail-row"><span>16:42</span><strong>주문수집</strong></div><div class="detail-row"><span>16:43</span><strong>결제확인</strong></div><div class="detail-row"><span>16:45</span><strong>재고할당</strong></div></div><div class="drawer-actions">${btn('전체 상세')}${btn('출고지시','primary','drawerShip',Boolean(order.exception))}</div>`);
}
function closeDrawer(){drawer.classList.remove('open');drawer.setAttribute('aria-hidden','true');drawerBackdrop.hidden=true;setBackgroundInert(false);const restore=drawerRestoreFocus;drawerRestoreFocus=null;restore?.focus();}
function 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">${statusChip(x.status)}</td></tr>`).join('');
const modeLabel=state.masterMode==='new'?'신규 품목':'품목 상세';
const currentIndex=visible.findIndex(item=>item.id===state.selectedMasterId);const dirty=Boolean(state.tabs.find(tab=>tab.id==='ERP-MST-ITEM-001')?.dirty);const navigator=state.masterMode==='new'?'<span class="record-position">신규 입력</span>':`<nav class="record-navigator" aria-label="품목 이전·다음 이동"><span class="record-position">품목 <strong>${currentIndex>=0?currentIndex+1:0} / ${visible.length}</strong></span><button id="masterPrev" type="button" ${dirty||currentIndex<=0?'disabled':''}> 이전</button><button id="masterNext" type="button" ${dirty||currentIndex<0||currentIndex>=visible.length-1?'disabled':''}>다음 </button></nav>`;
const body=`${masterValidationSummary()}<div class="master-layout"><aside class="master-pane"><form class="search-panel" id="masterSearchForm"><div class="search-fields"><label class="field-inline"><span>검색</span><input id="masterQueryInput" class="control medium" value="${esc(state.masterQuery)}" placeholder="품목코드/품목명"></label></div><div class="search-actions">${btn('조회','primary','masterSearch')}</div></form><div class="context-bar"><strong>품목 목록</strong><span>표시 ${visible.length}건</span><span>${state.masterMode==='new'?'신규 입력 중':`선택 ${esc(state.selectedMasterId)}`}</span></div>${grid(['품목코드','품목명','그룹','상태'],rows,{caption:'품목 목록'})}</aside><main class="detail-pane"><div class="pane-header"><div><strong>${modeLabel}</strong><span>${state.masterMode==='new'?'고유 식별자와 Barcode는 새로 입력':'목록을 바꾸기 전에 미저장 상태를 확인'}</span></div>${navigator}</div><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('품목코드',draft.id,{required:true,name:'code',readonly:state.masterMode!=='new'})}${formField('품목명',draft.name,{required:true,name:'name'})}${formField('품목그룹',draft.group,{name:'group'})}${formField('규격',draft.spec,{name:'spec'})}${formField('단위',draft.unit,{name:'unit'})}</div></section><section class="form-section"><h2>물류정보</h2><div class="form-grid">${lookupField('기본창고',draft.warehouseCode,draft.warehouse,{field:'warehouse'})}${formField('바코드',draft.barcode,{name:'barcode'})}${checkboxField('LOT 관리',draft.lot,{field:'lot'})}${checkboxField('유통기한 관리',draft.expiry,{field:'expiry'})}</div></section><div class="summary-bar"><span>상태 ${statusChip(draft.status)}</span><span>Mode <strong>${esc(state.masterMode)}</strong></span><span>Version 17</span><button id="auditButton" type="button" class="text-action">변경이력</button></div></main></div>`;
const commands=`<div class="command-group">${btn('조회 F3','','masterSearchTop')}${btn('신규','','masterNew')}${btn('저장 F8','primary','masterSave')}${btn('복사','','masterCopy')}</div><span class="command-separator"></span><div class="command-group">${btn(draft.status==='사용'?'사용중지':'사용재개','danger','masterSuspend')}${btn('엑셀 ▼')}</div>`;
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'));};
const visible=masterItems.filter(x=>!state.masterQuery.trim()||`${x.id} ${x.name} ${x.group}`.toLowerCase().includes(state.masterQuery.trim().toLowerCase()));const index=visible.findIndex(item=>item.id===state.selectedMasterId);const prev=$('#masterPrev'),next=$('#masterNext');if(prev)prev.onclick=()=>visible[index-1]&&requestMasterTarget(visible[index-1].id);if(next)next.onclick=()=>visible[index+1]&&requestMasterTarget(visible[index+1].id);
$('#auditButton').onclick=()=>showUtilityPanel(screen('ERP-MST-ITEM-001'),'help');
}
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='NEW';state.transactionErrors=[];state.transactionHeader={orderDate:'2026-08-11',channel:'직접주문',customerCode:'',customerName:'',owner:'홍길동',warehouseCode:'WH01',warehouseName:'서울물류센터',receiver:'',phone:'',postal:'',address:''};state.transactionLines=[{code:'',name:'',spec:'',available:0,qty:1,price:0,remark:''}];markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));setTimeout(()=>$('[data-field="customerCode"]',workspace)?.focus(),0);}
function renderT03(s){
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">${['NEW','DRAFT','CONFIRMED','ALLOCATED','PICKING','CHECKED','SHIPPED'].map(status=>`<span class="${state.transactionStatus===status?'active':''}">${esc(statusLabel(status,'lifecycle'))}</span>`).join('')}<strong>현재: ${esc(statusLabel(state.transactionStatus,'lifecycle'))} · ${state.transactionStatus==='NEW'?'다음: 저장':state.transactionStatus==='DRAFT'?'다음: 확정':'확정 이후 주요 값은 서버 업무 규칙으로 제한'}</strong></section><section class="form-section"><h2>기본정보</h2><div class="form-grid">${formField('주문일',h.orderDate,{required:true,name:'orderDate'})}${formField('판매채널',h.channel,{name:'channel'})}${lookupField('거래처',h.customerCode,h.customerName,{field:'customer'})}${formField('담당자',h.owner,{name:'owner'})}${lookupField('출고창고',h.warehouseCode,h.warehouseName,{field:'warehouse'})}</div></section><section class="form-section"><h2>배송정보</h2><div class="form-grid">${formField('수취인',h.receiver,{required:true,name:'receiver'})}${formField('연락처',h.phone,{name:'phone'})}${formField('우편번호',h.postal,{name:'postal'})}${formField('주소',h.address,{required:true,name:'address'})}</div></section><section class="detail-grid-section"><div class="pane-header"><strong>상품</strong><span>코드·수량·단가 직접 편집 · Enter 다음 셀</span><button id="transactionAddLine" type="button" class="text-action">+ 행추가</button></div>${grid(['품목코드','품목명','규격','가용재고','수량','단가','금액','비고'],lineRows,{caption:'주문 품목 상세'})}</section><div class="summary-bar"><span>품목 <strong id="transactionItemCount">${totals.items}종</strong></span><span>총수량 <strong id="transactionQty">${totals.qty.toLocaleString('ko-KR')}</strong></span><span>주문금액 <strong id="transactionAmount">₩${totals.amount.toLocaleString('ko-KR')}</strong></span><span>상태 <strong>${esc(statusLabel(state.transactionStatus,'lifecycle'))}</strong></span></div></div>`;
const commands=`<div class="command-group">${btn('신규','','transactionNew')}${btn('저장 F8','primary','saveTransaction')}${btn('주문복사','','transactionCopy')}</div><span class="command-separator"></span><div class="command-group">${btn('확정','','confirmTransaction',state.transactionStatus!=='DRAFT')}${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;}state.transactionStatus='DRAFT';markDirty('OMS-ORD-002',false);renderT03(screen('OMS-ORD-002'));toast('주문을 저장했습니다. 서버 응답 상태 DRAFT를 사용자 Label 작성으로 표시합니다.');};
$('#confirmTransaction').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('저장하지 않은 변경이 있습니다. 먼저 저장하세요.','error');return;}if(!validateTransaction()){renderT03(screen('OMS-ORD-002'));return;}state.transactionStatus='CONFIRMED';renderT03(screen('OMS-ORD-002'));toast('서버 응답 상태 CONFIRMED를 사용자 Label 확정으로 표시합니다.');};
$('#transactionNew').onclick=()=>{const tab=state.tabs.find(x=>x.id==='OMS-ORD-002');if(tab?.dirty){toast('현재 주문의 미저장 변경을 먼저 저장하거나 버리세요.','error');return;}resetTransaction();};
$('#transactionCopy').onclick=()=>{if(state.tabs.find(x=>x.id==='OMS-ORD-002')?.dirty){toast('미저장 변경을 먼저 처리하세요.','error');return;}state.transactionStatus='NEW';state.transactionErrors=[];state.transactionHeader={...state.transactionHeader,receiver:'',phone:'',postal:'',address:''};state.transactionLines=state.transactionLines.map(line=>({...line}));markDirty('OMS-ORD-002',true);renderT03(screen('OMS-ORD-002'));toast('주문을 복사했습니다. 수취/배송정보를 새 주문에 맞게 입력하세요.');};
}
function fastName(code){return ({ABC001:'운동화',ABC002:'러닝화',ABC003:'양말',ABC004:'셔츠'})[code]||'';}
function 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><section class="fast-entry-surface"><div class="pane-header"><div><strong>품목 단가 입력</strong><span>품목·단가를 연속 입력하고 오류를 해소한 뒤 F8로 저장</span></div><span>입력 ${inputCount}건 · 오류 ${errorCount}건</span></div>${grid(['#','품목코드','품목명','단가','적용일','비고'],rows,{caption:'품목단가 빠른 입력'})}</section><div class="summary-bar"><span>입력 ${inputCount}행</span><span>오류 <strong>${errorCount}</strong></span><span>빈 행 ${state.fastRows.length-inputCount}</span></div>`;
const commands=`<div class="command-group">${btn('저장 F8','primary','fastSave',errorCount>0)}${btn('행추가','','fastAdd')}${btn('행복제','','fastDuplicate')}${btn('Fill Down','','fastFill')}</div><span class="command-separator"></span><div class="command-group">${btn('엑셀 붙여넣기')}${btn('엑셀 ▼')}</div>`;
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">${statusChip(row.status)}</td></tr>`).join('');
const table=grid(['선택','작업번호','작업유형','Wave','작업자','진행률','SLA','상태'],rows,{caption:'물류 작업 대기열'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="queueSelectAll" type="checkbox" aria-label="현재 표시 작업 전체 선택" ${allSelected?'checked':''}></th>`);
const 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>${statusChip(row.status)}</td></tr>`).join('');
const table=grid(['선택','주문번호','OMS','WMS','차이','원인','처리상태'],rows,{caption:'OMS WMS 재고 출고 대사'}).replace('<th scope="col">선택</th>',`<th scope="col" class="center"><input id="reconcileSelectAll" type="checkbox" aria-label="현재 표시 대사 전체 선택" ${allSelected?'checked':''}></th>`);
const 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;state.reconcileActionResult=`${count}건의 재처리 요청을 등록했습니다. 원천 OMS/WMS 값은 이 화면에서 직접 덮어쓰지 않습니다.`;state.selectedReconcileRows.clear();renderT07(screen('COMMON-REC-001'));};
for(const id of ['reconcileRun','reconcileRunTop']){const button=$('#'+id);if(button)button.onclick=rerun;}const reason=$('#reconcileReason');if(reason)reason.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건의 원인 Projection을 다시 조회했습니다.`;renderT07(screen('COMMON-REC-001'));};const note=$('#reconcileReasonNote');if(note)note.onclick=()=>{state.reconcileActionResult=`${state.selectedReconcileRows.size}건에 처리사유가 필요합니다. 실제 제품에서는 사유 입력 Drawer를 사용합니다.`;renderT07(screen('COMMON-REC-001'));};const clear=$('#reconcileResultClear');if(clear)clear.onclick=()=>{state.reconcileActionResult='';renderT07(screen('COMMON-REC-001'));};
}
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=`<section class="import-stage-focus" data-import-stage-heading tabindex="-1" aria-label="필드 매핑"><h2>필드 매핑</h2><div class="mapping-table"><div class="mapping-head"><strong>Excel Column</strong><strong>System Field</strong><strong>매핑 근거</strong></div>${[['주문번호','주문번호','정확 일치'],['상품코드','품목코드','Alias 일치'],['수량','주문수량','Alias 일치'],['구매자','주문자명','Alias 일치'],['업체코드','거래처코드','사용자 확인 필요']].map(row=>`<div class="mapping-row"><span>${esc(row[0])}</span><span>→ ${esc(row[1])}</span><span>${esc(row[2])}</span></div>`).join('')}</div></section>`;
if(step===3){const validationErrors=[['8','품목코드','ABC99','존재하지 않는 품목'],['12','수량','-1','수량은 0보다 커야 합니다.']];content=`<section class="import-stage-focus" data-import-stage-heading tabindex="-1" aria-label="검증 결과"><h2>검증 결과</h2><div class="validation-cards"><div class="validation-card"><span>전체</span><b>18,230</b></div><div class="validation-card"><span>정상</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div><div class="import-error-nav" role="group" aria-label="검증 오류 탐색"><span>샘플 오류 <b>${state.importErrorIndex+1} / ${validationErrors.length}</b></span>${btn('첫 오류','','importFirstError')}${btn('다음 오류','','importNextError')}${btn('오류 336건 다운로드')}</div>${grid(['행','필드','값','오류'],validationErrors.map((row,index)=>`<tr tabindex="-1" data-import-error-row="${index}" aria-current="${index===state.importErrorIndex?'true':'false'}"><td>${row[0]}</td><td>${esc(row[1])}</td><td>${esc(row[2])}</td><td class="error">${esc(row[3])}</td></tr>`).join(''),{caption:'Excel 검증 오류'})}</section>`;}
if(step===4)content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>반영 미리보기</strong><span>오류 336건 제외</span></div><div class="commit-body"><p><strong>17,894건</strong>을 반영할 수 있습니다.</p><dl><div><dt>신규</dt><dd>17,201</dd></div><div><dt>수정</dt><dd>693</dd></div><div><dt>제외 오류</dt><dd>336</dd></div></dl><p class="warning-copy">실제 제품은 Staging 데이터를 Domain 검증한 뒤 Commit합니다.</p></div></section>`;
if(step===5){const running=state.importJobStatus==='running';const completed=state.importJobStatus==='completed';content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>Background 반영 작업</strong><span>${completed?'완료':running?'처리 중':'대기'}</span></div><div class="commit-body"><div class="job-progress"><div class="job-progress-track"><span style="width:${state.importProgress}%"></span></div><strong>${state.importProgress}%</strong><p>${state.importProcessed.toLocaleString('ko-KR')} / 17,894 처리</p></div><p>다른 화면으로 이동해도 작업은 계속됩니다. 홈의 진행 작업에서 다시 들어올 수 있습니다.</p>${!running&&!completed?btn('17,894건 반영','primary','importCommit'):''}${completed?btn('결과 보기','primary','importResult'):''}</div></section>`;}
if(step===6)content=`<section class="commit-panel import-stage-focus" data-import-stage-heading tabindex="-1"><div class="pane-header"><strong>반영 결과</strong><span>완료</span></div><div class="commit-body"><div class="validation-cards"><div class="validation-card"><span>성공</span><b>17,894</b></div><div class="validation-card error"><span>오류</span><b>336</b></div></div><p>오류 데이터는 Domain DB에 반영하지 않았습니다.</p><div class="inline-actions">${btn('오류 336건 다운로드')}${btn('새 업로드','primary','importReset')}</div></div></section>`;
const nav=`<div class="import-steps">${steps.map((label,index)=>`<div class="import-step ${index+1===step?'active':''} ${index+1<step?'done':''}"><b>${index+1}</b><span>${label}</span></div>`).join('')}</div>`;
const 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();if(state.importFocusStep!==step){state.importFocusStep=step;setTimeout(()=>{const target=step===1?$('#dropZone',workspace):$('[data-import-stage-heading]',workspace);target?.focus({preventScroll:true});},0);}
}
function startImportJob(){
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 firstError=$('#importFirstError');if(firstError)firstError.onclick=()=>{state.importErrorIndex=0;renderT08(screen('OMS-ORD-003'));setTimeout(()=>$('[data-import-error-row="0"]',workspace)?.focus(),0);};
const nextError=$('#importNextError');if(nextError)nextError.onclick=()=>{state.importErrorIndex=(state.importErrorIndex+1)%2;renderT08(screen('OMS-ORD-003'));setTimeout(()=>$(`[data-import-error-row="${state.importErrorIndex}"]`,workspace)?.focus(),0);};
const reset=$('#importReset');if(reset)reset.onclick=()=>{state.importStep=1;state.importFileName='';state.importFileSize=0;state.importFileType='';state.importJobStatus='idle';state.importProgress=0;state.importProcessed=0;state.importErrorIndex=0;state.importFocusStep=0;renderT08(screen('OMS-ORD-003'));};
}
function renderT09(s){
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 scanTruth=state.networkStatus!=='online'?'입력 차단 · 온라인 확인 필요':completed?'상품 완료':'스캔 가능';
const confirmed=state.lastConfirmedScanValue?`${esc(state.lastConfirmedScanValue)} · ${new Date(state.lastConfirmedScanAt).toLocaleTimeString('ko-KR')}`:'아직 확인된 스캔 없음';
const errorTitle=state.scanError?.startsWith('NETWORK:')?'네트워크를 확인하세요.':'다른 상품입니다.';const errorCopy=(state.scanError||'').replace(/^NETWORK:/,'');
workspace.innerHTML=`<div class="mobile-stage"><main class="wms-phone" data-screen-id="${esc(s.id)}"><header class="wms-header"><div><h1>출고 피킹</h1><strong>23 / 138</strong></div><span class="network ${network[2]}">${network[0]} ${network[1]}</span></header><section class="wms-content"><span class="wms-label">LOCATION</span><div class="scan-location">A-03-02</div><p>위치 확인 완료 · 상품을 스캔하세요.</p><div class="scan-truth ${state.networkStatus==='online'?'ready':'blocked'}" role="status"><div><span>스캔 상태</span><b>${esc(scanTruth)}</b></div><div><span>최근 서버 확인</span><b>${confirmed}</b></div></div><div id="scanBox" class="scan-box ${state.scanError?'scan-error':''}" tabindex="0" role="textbox" aria-label="바코드 스캔 입력" aria-describedby="scanHint"><strong>${state.scanBuffer?esc(state.scanBuffer):'SCAN READY'}</strong><small id="scanHint">스캐너 입력 후 Enter · 기대 바코드 8801234567890</small></div><section class="wms-item"><strong>ABC 운동화</strong><p>BLACK / 270</p></section><div class="qty-grid"><div><span>필요</span><b>${state.pickingRequired}</b></div><div><span>피킹</span><b>${state.pickingQty}</b></div><div><span>남음</span><b>${remain}</b></div></div>${completed?'<div class="success-panel"><strong>상품 완료</strong><span>다음 품목으로 자동 진행할 수 있습니다.</span></div>':''}${state.scanError?`<div class="error-panel" role="alert"><strong>${esc(errorTitle)}</strong><span>${esc(errorCopy)}</span></div>`:''}</section><footer class="wms-actions">${btn('+1 피킹','primary','pickOne',completed||state.networkStatus!=='online')}${btn('수량입력','','pickQty',state.networkStatus!=='online')}${btn('재고부족 신고','danger full','stockIssue')}</footer></main></div>`;
bindT09();
}
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='';state.lastConfirmedScanValue=normalized;state.lastConfirmedScanAt=Date.now();if(state.pickingQty<state.pickingRequired)state.pickingQty++;renderT09(screen('WMS-PICK-001'));toast(state.pickingQty===state.pickingRequired?'상품 피킹 완료.':'정상 상품을 1개 피킹했습니다.');setTimeout(()=>$('#scanBox')?.focus(),0);
}
function bindT09(){
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();
})();