800 lines
135 KiB
JavaScript
800 lines
135 KiB
JavaScript
(() => {
|
||
'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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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&¤t?.dirty){
|
||
state.pendingNavigationId=target.id;
|
||
state.pendingNavigationOptions={focus,updateHash};
|
||
state.pendingUnsavedMode='navigate';
|
||
$('#unsavedMessage').textContent=`${current.title}에 저장하지 않은 변경사항이 있습니다. 이동하기 전에 저장하거나 변경을 버리세요.`;
|
||
if(!unsavedDialog.open)unsavedDialog.showModal();
|
||
return false;
|
||
}
|
||
state.activeId=target.id; addRecent(target.id);
|
||
const existingTab=state.tabs.find(t=>t.id===target.id);if(existingTab)existingTab.resumed=false;
|
||
if(target.id!=='HOME'&&!existingTab)state.tabs.push({id:target.id,title:target.title,pinned:false,dirty:false,resumed:false});
|
||
persistWorkspaceSession();if(updateHash)setHash(target); renderAll(); if(focus)workspace.focus({preventScroll:true});
|
||
return true;
|
||
}
|
||
function closeTabNow(id){
|
||
const index=state.tabs.findIndex(t=>t.id===id); if(index<0)return;
|
||
const wasActive=state.activeId===id; state.tabs.splice(index,1);persistWorkspaceSession();
|
||
if(wasActive){ const fallback=state.tabs[index-1] || state.tabs[index] || null; openScreen(fallback?.id || 'HOME'); }
|
||
else renderTabs();
|
||
}
|
||
function requestCloseTab(id){
|
||
const tab=state.tabs.find(t=>t.id===id); if(!tab)return;
|
||
if(!tab.dirty){closeTabNow(id);return;}
|
||
state.pendingCloseId=id; state.pendingUnsavedMode='close';
|
||
$('#unsavedMessage').textContent=`${tab.title}에 저장하지 않은 변경사항이 있습니다.`;
|
||
if(!unsavedDialog.open)unsavedDialog.showModal();
|
||
}
|
||
|
||
function groupScreens(list=screens.filter(x=>x.id!=='HOME')){
|
||
const map=new Map();
|
||
list.forEach(item=>{ if(!map.has(item.module))map.set(item.module,new Map());const groups=map.get(item.module);if(!groups.has(item.group))groups.set(item.group,[]);groups.get(item.group).push(item); });
|
||
return map;
|
||
}
|
||
function isFav(id){return state.favorites.has(id)}
|
||
function toggleFavorite(id){ if(!knownIds.has(id)||id==='HOME')return; isFav(id)?state.favorites.delete(id):state.favorites.add(id);persist();renderNavigation();if(state.activeId==='HOME')renderHome(); }
|
||
|
||
function navRow(item,favorite=true,icon=''){
|
||
const collapsedLabel=icon||iconFor(item);
|
||
return `<div class="nav-row" data-screen="${esc(item.id)}"><button class="nav-entry ${state.activeId===item.id?'active':''}" type="button" title="${esc(item.title)}" ${state.activeId===item.id?'aria-current="page"':''}>${state.collapsed?esc(collapsedLabel):esc(item.title)}</button>${favorite?`<button class="star ${isFav(item.id)?'on':''}" type="button" aria-label="${isFav(item.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(item.id)}">${isFav(item.id)?'★':'☆'}</button>`:''}</div>`;
|
||
}
|
||
function bindNavRow(row){ const id=row.dataset.screen; $('.nav-entry',row).onclick=()=>openScreen(id);const star=$('.star',row);if(star)star.onclick=e=>{e.stopPropagation();toggleFavorite(id)}; }
|
||
function renderNavigation(){
|
||
app.dataset.density=state.density;app.dataset.theme=state.theme;app.dataset.nav=state.collapsed?'collapsed':'expanded';app.dataset.module=state.module==='ALL'?(screen(state.activeId).module||'COMMON'):state.module;document.documentElement.style.colorScheme=state.theme;sideNav.classList.toggle('collapsed',state.collapsed);
|
||
$('#collapseButton').textContent=state.collapsed?'›':'‹';$('#collapseButton').setAttribute('aria-label',state.collapsed?'메뉴 펼치기':'메뉴 접기');$('#collapseButton').setAttribute('aria-expanded',String(!state.collapsed));
|
||
$('#densityButton').textContent=state.density==='compact'?'Compact':'Comfortable';
|
||
const themeButton=$('#themeButton');if(themeButton){themeButton.innerHTML=state.theme==='dark'?'<span aria-hidden="true">☾</span> <span>Dark</span>':'<span aria-hidden="true">☀</span> <span>Light</span>';themeButton.setAttribute('aria-label',state.theme==='dark'?'밝은 테마로 변경':'어두운 테마로 변경');}
|
||
const modules=['OMS','ERP','WMS','COMMON'];
|
||
$('#moduleSwitcher').innerHTML=modules.map(m=>`<button type="button" class="${state.module===m?'active':''}" data-module="${m}" aria-pressed="${state.module===m}" title="${esc(labelModule(m))}">${state.collapsed?(m==='COMMON'?'공':m[0]):esc(labelModule(m))}</button>`).join('');
|
||
$$('#moduleSwitcher button').forEach(button=>button.onclick=()=>{state.module=button.dataset.module;$('#moduleSelect').value=state.module;renderNavigation();if(state.activeId==='HOME')renderHome();});
|
||
$('#moduleSelect').value=state.module;
|
||
const favoriteItems=screens.filter(x=>isFav(x.id)&&x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));
|
||
$('#favoriteNav').innerHTML=favoriteItems.length?`<h2>즐겨찾기</h2>${favoriteItems.slice(0,6).map(item=>navRow(item)).join('')}`:'';
|
||
const recentItems=state.recents.map(screen).filter(x=>x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));
|
||
$('#recentNav').innerHTML=recentItems.length?`<h2>최근메뉴</h2>${recentItems.slice(0,4).map(item=>navRow(item,false,'↺')).join('')}`:'';
|
||
const visible=screens.filter(x=>x.id!=='HOME'&&(state.module==='ALL'||x.module===state.module));const grouped=groupScreens(visible);
|
||
$('#menuNav').innerHTML=[...grouped.entries()].map(([module,groups])=>`<section class="menu-group"><h2>${esc(labelModule(module))} 업무</h2>${[...groups.entries()].map(([group,items])=>`<div class="menu-subgroup"><h2>${esc(group)}</h2>${items.map(item=>navRow(item)).join('')}</div>`).join('')}</section>`).join('');
|
||
$$('.nav-row',sideNav).forEach(bindNavRow);$('#sideHomeButton').classList.toggle('active',state.activeId==='HOME');$('#sideHomeButton').setAttribute('aria-current',state.activeId==='HOME'?'page':'false');
|
||
}
|
||
|
||
function renderTabs(){
|
||
if(!state.tabs.length){tabs.innerHTML='<div class="empty-tabs">열린 업무가 없습니다.</div>';return;}
|
||
const focusableId=state.tabs.some(t=>t.id===state.activeId)?state.activeId:state.tabs[0]?.id;
|
||
tabs.innerHTML=state.tabs.map(t=>`<div class="tab-shell ${state.activeId===t.id?'active':''}" data-tab="${esc(t.id)}"><button class="tab-main" role="tab" aria-selected="${state.activeId===t.id}" tabindex="${focusableId===t.id?'0':'-1'}" type="button">${t.resumed&&!t.dirty?'<span class="tab-resumed" title="이전 세션에서 복원된 업무 위치" aria-label="이전 세션에서 복원됨">↺</span>':''}<span class="tab-title">${esc(t.title)}</span>${t.dirty?'<span class="tab-dirty" title="저장하지 않은 변경" aria-label="저장하지 않은 변경">●</span>':''}</button><button class="tab-pin" type="button" aria-label="${t.pinned?'고정 해제':'고정'}" aria-pressed="${t.pinned}">${t.pinned?'◆':'◇'}</button><button class="tab-close" type="button" aria-label="${esc(t.title)} 닫기">×</button></div>`).join('');
|
||
const shells=$$('.tab-shell',tabs);
|
||
shells.forEach((shell,index)=>{
|
||
const id=shell.dataset.tab;const main=$('.tab-main',shell);
|
||
main.onclick=()=>openScreen(id);$('.tab-close',shell).onclick=e=>{e.stopPropagation();requestCloseTab(id)};
|
||
$('.tab-pin',shell).onclick=e=>{e.stopPropagation();const tab=state.tabs.find(x=>x.id===id);if(tab){tab.pinned=!tab.pinned;persistWorkspaceSession();renderTabs();}};
|
||
main.onkeydown=e=>{let next=index;if(e.key==='ArrowRight')next=(index+1)%shells.length;else if(e.key==='ArrowLeft')next=(index-1+shells.length)%shells.length;else if(e.key==='Home')next=0;else if(e.key==='End')next=shells.length-1;else return;e.preventDefault();const target=shells[next]?.dataset.tab;if(target){openScreen(target,{focus:false});$('.tab-main',shells[next])?.focus();}};
|
||
});
|
||
}
|
||
|
||
const btn=(label,cls='',id='',disabled=false)=>`<button ${id?`id="${esc(id)}"`:''} type="button" class="btn ${cls}" ${disabled?'disabled aria-disabled="true"':''}>${esc(label)}</button>`;
|
||
function defaultCommands(primary='저장 F8',prefix='default'){
|
||
return `<div class="command-group">${btn('조회 F3','',''+prefix+'Search')}${btn('신규','',''+prefix+'New')}${btn(primary,'primary',''+prefix+'Save')}</div><span class="command-separator"></span><div class="command-group">${btn('엑셀 ▼')}${btn('더보기 ⋯')}</div>`;
|
||
}
|
||
function pageShell(s,body,commands=''){
|
||
return `<section class="screen" data-screen-id="${esc(s.id)}" data-template="${esc(s.template)}" data-module="${esc(s.module)}"><header class="page-header"><div class="page-identity"><div class="breadcrumb">${esc(s.module)} > ${esc(s.group)}</div><div class="title-row"><h1>${esc(s.title)}</h1><span class="status-chip">${esc(s.template)}</span></div></div><div class="page-tools"><button id="pageFavorite" type="button" aria-label="${isFav(s.id)?'즐겨찾기 해제':'즐겨찾기 추가'}" aria-pressed="${isFav(s.id)}">${isFav(s.id)?'★':'☆'}</button><button id="pageHelp" type="button">도움말</button><button id="pageAi" type="button">AI</button><button id="pageSuggestion" type="button">제안</button></div></header>${commands?`<div class="command-bar">${commands}</div>`:''}<div class="screen-body">${body}</div></section>`;
|
||
}
|
||
function setBackgroundInert(value){for(const el of [$('.global-header'),$('.shell-body')]){if(!el)continue;value?el.setAttribute('inert',''):el.removeAttribute('inert');}}
|
||
function openDrawerContent(title,html){
|
||
drawerRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;drawerTitle.textContent=title;drawerBody.innerHTML=html;
|
||
drawer.classList.add('open');drawer.setAttribute('aria-hidden','false');drawerBackdrop.hidden=false;setBackgroundInert(true);setTimeout(()=>$('#drawerClose').focus(),0);
|
||
}
|
||
function applyIntent(intent){
|
||
if(intent==='order-stock'){state.orderQuickFilter='stock';state.selectedOrders.clear();state.orderDataState='ready';return openScreen('OMS-ORD-001');}
|
||
if(intent==='order-errors'){state.orderQuickFilter='error';state.selectedOrders.clear();state.orderDataState='ready';return openScreen('OMS-ORD-001');}
|
||
if(intent==='reconcile-mismatch'){state.mismatchOnly=true;return openScreen('COMMON-REC-001');}
|
||
if(intent==='import-progress'){state.importStep=state.importJobStatus==='completed'?6:Math.max(1,state.importStep);return openScreen('OMS-ORD-003');}
|
||
}
|
||
function showUtilityPanel(s,kind){
|
||
if(kind==='help'){openDrawerContent(`${s.title} 도움말`,`<section class="utility-panel"><p class="utility-kicker">화면 도움말 · ${esc(s.id)}</p><h2>${esc(s.title)}</h2><p>이 화면은 ${esc(s.group)} 업무를 표준 ${esc(s.template)} 문법으로 처리합니다.</p><h3>기본 사용법</h3><ol><li>조회 조건 또는 업무 대상을 확인합니다.</li><li>필요한 행이나 값을 선택합니다.</li><li>Command Bar의 업무 동사를 실행합니다.</li></ol><h3>단축키</h3><dl class="shortcut-list"><div><dt>F2</dt><dd>Lookup</dd></div><div><dt>F3</dt><dd>조회</dd></div><div><dt>F8</dt><dd>저장</dd></div><div><dt>Esc</dt><dd>현재 Context 닫기</dd></div></dl><div class="utility-note">화면의 Disabled 상태는 UX 표현이며 최종 권한과 업무 규칙은 서버에서 재검증해야 합니다.</div></section>`);return;}
|
||
if(kind==='ai'){openDrawerContent(`${s.title} AI 도우미`,`<section class="utility-panel"><p class="utility-kicker">현재 화면 · ${esc(s.id)}</p><h2>확인할 항목</h2><div class="ai-attention"><button type="button" data-ai-intent="order-stock"><span>재고부족</span><b>7건</b><small>주문관리에서 보기</small></button><button type="button" data-ai-intent="order-errors"><span>처리오류</span><b>16건</b><small>오류 주문만 보기</small></button><button type="button" data-ai-intent="reconcile-mismatch"><span>대사 불일치</span><b>43건</b><small>불일치만 보기</small></button></div><h3>빠른 질문</h3><div class="utility-actions"><button class="btn" type="button" data-ai-intent="order-stock">왜 출고가 안 된 주문이 있어?</button><button class="btn" type="button" data-ai-intent="reconcile-mismatch">불일치만 보여줘</button></div><div class="utility-note">AI는 설명·제안만 수행합니다. 실제 변경은 구조화된 Proposal과 사용자 확인 후 Domain Command로 실행해야 합니다.</div></section>`);$$('[data-ai-intent]',drawerBody).forEach(button=>button.onclick=()=>{const intent=button.dataset.aiIntent;closeDrawer();setTimeout(()=>applyIntent(intent),0);});return;}
|
||
openDrawerContent(`${s.title} 제안`,`<form id="suggestionForm" class="utility-panel"><p class="utility-kicker">사용자 제안 · ${esc(s.id)}</p><h2>이 화면에 의견 보내기</h2><fieldset class="suggestion-types"><legend>유형</legend><label><input type="radio" name="type" value="불편" checked> 불편</label><label><input type="radio" name="type" value="오류"> 오류</label><label><input type="radio" name="type" value="개선 제안"> 개선 제안</label></fieldset><label class="suggestion-copy"><span>내용</span><textarea id="suggestionText" required maxlength="500" rows="7" placeholder="업무 중 불편했던 상황과 기대하는 동작을 적어주세요."></textarea></label><label class="suggestion-context"><input type="checkbox" checked> 현재 화면 정보 포함 <small>ScreenId, Route, Version, 필터 구조만 포함 · 업무 민감데이터 제외</small></label><div class="drawer-actions"><button class="btn primary" type="submit">보내기</button></div></form>`);$('#suggestionForm').onsubmit=e=>{e.preventDefault();const value=$('#suggestionText').value.trim();if(!value){$('#suggestionText').focus();return;}closeDrawer();toast('화면 의견을 접수했습니다. 업무 민감데이터는 포함하지 않았습니다.');};setTimeout(()=>$('#suggestionText')?.focus(),0);
|
||
}
|
||
function closeSystemPanel(restoreFocus=true){
|
||
if(systemPanel.hidden)return;systemPanel.hidden=true;$('#jobsButton').setAttribute('aria-expanded','false');$('#noticeButton').setAttribute('aria-expanded','false');
|
||
const restore=systemPanelRestoreFocus;systemPanelRestoreFocus=null;if(restoreFocus)restore?.focus();
|
||
}
|
||
function showSystemPanel(kind){
|
||
const trigger=kind==='jobs'?$('#jobsButton'):$('#noticeButton');
|
||
if(!systemPanel.hidden&&trigger.getAttribute('aria-expanded')==='true'){closeSystemPanel();return;}
|
||
closeDrawer();systemPanelRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;systemPanelTitle.textContent=kind==='jobs'?'작업 센터':'알림 센터';
|
||
$('#jobsButton').setAttribute('aria-expanded',String(kind==='jobs'));$('#noticeButton').setAttribute('aria-expanded',String(kind!=='jobs'));
|
||
if(kind==='jobs'){const progress=state.importJobStatus==='running'?`${state.importProgress}%`:'완료';systemPanelBody.innerHTML=`<section class="utility-panel"><p class="utility-kicker">백그라운드 작업</p><h2>진행 중 / 최근 작업</h2><button class="system-row" type="button" data-system-intent="import-progress"><span><b>Excel 주문 Import</b><small>${esc(progress)} · 정상 ${state.importProcessed.toLocaleString('ko-KR')}건</small></span><span>열기 ›</span></button><div class="system-row static"><span><b>송장 재처리</b><small>대기 · 자동 재시도</small></span><span>대기</span></div></section>`;}else{systemPanelBody.innerHTML=`<section class="utility-panel"><p class="utility-kicker">업무 알림</p><h2>확인 필요 4건</h2><button class="system-row critical" type="button" data-system-intent="order-errors"><span><b>송장 발급 실패 3건</b><small>원인 확인 후 재처리</small></span><span>열기 ›</span></button><button class="system-row" type="button" data-system-intent="order-stock"><span><b>재고부족 주문 7건</b><small>예외 주문만 확인</small></span><span>열기 ›</span></button></section>`;}
|
||
systemPanel.hidden=false;$$('[data-system-intent]',systemPanelBody).forEach(button=>button.onclick=()=>{const intent=button.dataset.systemIntent;closeSystemPanel(false);setTimeout(()=>applyIntent(intent),0);});setTimeout(()=>systemPanel.focus(),0);
|
||
}
|
||
function bindPageTools(s){
|
||
const fav=$('#pageFavorite');if(fav)fav.onclick=()=>toggleFavorite(s.id);
|
||
const help=$('#pageHelp');if(help)help.onclick=()=>showUtilityPanel(s,'help');
|
||
const ai=$('#pageAi');if(ai)ai.onclick=()=>showUtilityPanel(s,'ai');
|
||
const suggestion=$('#pageSuggestion');if(suggestion)suggestion.onclick=()=>showUtilityPanel(s,'suggestion');
|
||
}
|
||
function grid(cols,rows,{caption='업무 데이터',className=''}={}){
|
||
return `<div class="grid-wrap ${className}"><table class="data-grid"><caption class="sr-only">${esc(caption)}</caption><thead><tr>${cols.map(c=>`<th scope="col">${esc(c)}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table></div>`;
|
||
}
|
||
function formField(label,value,{required=false,name='',readonly=false}={}){
|
||
return `<div class="form-field"><label class="${required?'required':''}">${esc(label)}</label><input ${name?`data-field="${esc(name)}"`:''} value="${esc(value)}" ${readonly?'readonly':''}></div>`;
|
||
}
|
||
function lookupField(label,code,name,{field='',entity=field}={}){
|
||
return `<div class="form-field"><label>${esc(label)}</label><div class="lookup" data-lookup="${esc(entity)}" data-lookup-field="${esc(field)}"><input ${field?`data-field="${esc(field)}Code"`:''} value="${esc(code)}" autocomplete="off"><input ${field?`data-field="${esc(field)}Name"`:''} value="${esc(name)}" readonly><button type="button" class="lookup-trigger" title="F2 조회" aria-label="${esc(label)} 조회" aria-keyshortcuts="F2">검색</button></div></div>`;
|
||
}
|
||
function checkboxField(label,checked,{field=''}={}){return `<div class="form-field"><label>${esc(label)}</label><label class="checkbox-control"><input ${field?`data-field="${esc(field)}"`:''} type="checkbox" ${checked?'checked':''}> 사용</label></div>`;}
|
||
|
||
function lookupItems(){
|
||
const list=lookupData[state.lookupEntity]||[];const q=(lookupSearchInput.value||'').trim().toLowerCase();
|
||
return list.filter(item=>!q||`${item.code} ${item.name} ${item.secondary}`.toLowerCase().includes(q));
|
||
}
|
||
function renderLookupResults(){
|
||
const list=lookupItems();if(state.lookupIndex>=list.length)state.lookupIndex=Math.max(0,list.length-1);
|
||
lookupResults.innerHTML=list.length?list.map((item,index)=>`<button type="button" class="lookup-result ${index===state.lookupIndex?'active':''}" data-index="${index}" role="option" aria-selected="${index===state.lookupIndex}"><span class="code">${esc(item.code)}</span><span class="copy"><b>${esc(item.name)}</b><small>${esc(item.secondary)}</small></span></button>`).join(''):'<div class="search-empty">조회된 항목이 없습니다.</div>';
|
||
$('#lookupStatus').textContent=`${list.length}건 · ↑↓ 이동 · Enter 선택 · Esc 닫기`;
|
||
$$('.lookup-result',lookupResults).forEach((button,index)=>{button.onmousemove=()=>{state.lookupIndex=index;renderLookupResults();};button.onclick=()=>{state.lookupIndex=index;selectLookup();};});
|
||
$('#lookupSelectButton').disabled=!list.length;
|
||
}
|
||
function openLookup(root){
|
||
if(!root)return;state.lookupEntity=root.dataset.lookup||'';state.lookupField=root.dataset.lookupField||'';state.lookupIndex=0;lookupRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;
|
||
const label=root.closest('.form-field')?.querySelector('label')?.textContent?.trim()||'업무';$('#lookupTitle').textContent=`${label} 검색`;$('#lookupSubtitle').textContent='F2 · 코드/명칭 조회';lookupSearchInput.value='';renderLookupResults();
|
||
if(!lookupDialog.open)lookupDialog.showModal();setTimeout(()=>lookupSearchInput.focus(),0);
|
||
}
|
||
function focusAfterLookup(root){
|
||
const focusables=$$('input:not([disabled]),select:not([disabled]),button:not([disabled]),textarea:not([disabled])',workspace);
|
||
const current=root?.querySelector('button.lookup-trigger');const index=focusables.indexOf(current);(focusables[index+1]||lookupRestoreFocus)?.focus();
|
||
}
|
||
function applyLookupToActive(field,item){
|
||
if(state.activeId==='ERP-MST-ITEM-001'&&field==='warehouse'&&state.masterDraft){state.masterDraft.warehouseCode=item.code;state.masterDraft.warehouse=item.name;}
|
||
if(state.activeId==='OMS-ORD-002'){
|
||
if(field==='customer'){state.transactionHeader.customerCode=item.code;state.transactionHeader.customerName=item.name;}
|
||
if(field==='warehouse'){state.transactionHeader.warehouseCode=item.code;state.transactionHeader.warehouseName=item.name;}
|
||
}
|
||
}
|
||
function selectLookup(){
|
||
const list=lookupItems();const item=list[state.lookupIndex];if(!item)return;const root=$(`.lookup[data-lookup-field="${CSS.escape(state.lookupField)}"]`,workspace);
|
||
if(root){const code=$(`[data-field="${CSS.escape(state.lookupField)}Code"]`,root);const name=$(`[data-field="${CSS.escape(state.lookupField)}Name"]`,root);if(code)code.value=item.code;if(name)name.value=item.name;applyLookupToActive(state.lookupField,item);markDirty(state.activeId,true);}
|
||
lookupDialog.close();announce(`${item.code} ${item.name}을 선택했습니다.`);toast(`${item.name}을 선택했습니다.`);setTimeout(()=>focusAfterLookup(root),0);
|
||
}
|
||
function bindLookups(){
|
||
$$('.lookup',workspace).forEach(root=>{const trigger=$('.lookup-trigger',root);if(trigger)trigger.onclick=()=>openLookup(root);$$('input',root).forEach(input=>input.onkeydown=e=>{if(e.key==='F2'){e.preventDefault();openLookup(root);}});});
|
||
}
|
||
function dataState(kind,title,detail,retryId=''){
|
||
const action=retryId?btn('다시 조회','primary',retryId):'';
|
||
return `<section class="data-state ${esc(kind)}" role="status"><strong>${esc(title)}</strong><span>${esc(detail)}</span>${action}</section>`;
|
||
}
|
||
|
||
|
||
function homeAttentionItems(){
|
||
return [
|
||
{source:'실패 작업',title:'송장 발급 실패 3건',detail:'오류 주문 필터로 바로 이동',id:'OMS-ORD-001',critical:true,intent:'order-errors'},
|
||
{source:'예외',title:'재고부족 주문 7건',detail:'재고부족 필터를 적용해 이동',id:'OMS-ORD-001',critical:true,intent:'order-stock'},
|
||
{source:'중요 알림',title:'대사 불일치 43건',detail:'불일치만 표시해 이동',id:'COMMON-REC-001',critical:false,intent:'reconcile-mismatch'},
|
||
{source:'진행 작업',title:state.importJobStatus==='running'?`Excel 주문 반영 ${state.importProgress}%`:state.importJobStatus==='completed'?'Excel 주문 반영 완료':'Excel 주문 업로드 대기',detail:'작업상태 보기',id:'OMS-ORD-003',critical:false,intent:'import-progress'},
|
||
{source:'미저장',title:'주문등록 수정 중',detail:'저장하지 않은 변경사항 이어서 편집',id:'OMS-ORD-002',critical:false,intent:''},
|
||
{source:'알림',title:'주소 확인 필요 4건',detail:'배송지 오류 주문만 확인',id:'OMS-ORD-001',critical:false,intent:'order-errors'},
|
||
{source:'알림',title:'SKU 매칭 실패 2건',detail:'상품 매핑 오류 주문만 확인',id:'OMS-ORD-001',critical:false,intent:'order-errors'},
|
||
{source:'진행 작업',title:'피킹 작업 1건 지연',detail:'현재 물류작업 Queue에서 확인',id:'WMS-WORK-001',critical:false,intent:''},
|
||
{source:'알림',title:'품목 바코드 확인 1건',detail:'품목 기준정보에서 확인',id:'ERP-MST-ITEM-001',critical:false,intent:''},
|
||
{source:'진행 작업',title:'재고 실사 작업 확인',detail:'물류 작업 Queue에서 확인',id:'WMS-WORK-001',critical:false,intent:''},
|
||
...Array.from({length:18},(_,index)=>({source:index%3===0?'진행 작업':'알림',title:`추가 확인 업무 ${index+1}`,detail:index%3===0?'작업 센터에서 전체 진행상태 확인':'알림 센터에서 전체 알림 확인',id:index%3===0?'WMS-WORK-001':'OMS-ORD-001',critical:false,intent:''})),
|
||
];
|
||
}
|
||
|
||
function attention(source,title,detail,id,critical=false,intent=''){return `<button type="button" class="attention-item ${critical?'critical':''}" data-screen="${esc(id)}" ${intent?`data-intent="${esc(intent)}"`:''}><span class="attention-source">${esc(source)}</span><span class="attention-copy"><b>${esc(title)}</b><small>${esc(detail)}</small></span><span class="attention-action">열기</span><span aria-hidden="true">›</span></button>`;}
|
||
function 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"><input></th>',`<th scope="col" class="center"><input id="selectVisibleOrders" type="checkbox" aria-label="현재 표시된 주문 전체 선택" ${allSelected?'checked':''}></th>`);
|
||
return table;
|
||
}
|
||
function renderT01(s){
|
||
const selection=state.selectedOrders.size;
|
||
const quick=[['all','전체','8,241'],['new','신규','2,138'],['ready','출고대기','927'],['stock','재고부족','7'],['error','처리오류','16']];
|
||
const body=`${problemSurface(state.commandProblem)}${orderSearchPanel()}<div class="quick-filter" role="group" aria-label="주문 빠른 필터">${quick.map(([key,label,count])=>`<button type="button" data-order-filter="${key}" class="${state.orderQuickFilter===key?'active':''} ${['stock','error'].includes(key)?'danger':''}" aria-pressed="${state.orderQuickFilter===key}"><small>${esc(label)}</small><b>${esc(count)}</b></button>`).join('')}</div><div class="context-bar"><strong>오늘 주문</strong><span>조회 8,241건</span><span>표시 ${filteredOrders().length}건</span><span>선택 <b id="selectionContext">${selection}</b>건</span><span>오류 16건</span></div><div id="bulkBar" class="bulk-bar" ${selection?'':'hidden'}><strong><span id="bulkCount">${selection}</span>건 선택</strong>${btn('출고지시','primary','bulkShip')}${btn('보류')}${btn('담당자 변경')}${btn('택배사 변경')}</div><div id="orderDataRegion">${renderOrderData()}</div><div class="summary-bar"><span>전체 8,241건</span><span>선택 <strong id="selectionSummary">${selection}</strong>건</span><span>상품수량 12,824</span><span>금액 <strong>₩182,420,300</strong></span></div>`;
|
||
const commands=`<div class="command-group">${btn('조회 F3','','commandSearch')}${btn('신규','','commandNew')}${btn('주문복사')}</div><span class="command-separator"></span><div class="command-group">${btn('출고지시','primary','commandShip',selection===0)}${btn('보류','','commandHold',selection===0)}</div><span class="command-separator"></span><div class="command-group">${btn('인쇄')}${btn('엑셀 ▼')}</div>`;
|
||
workspace.innerHTML=pageShell(s,body,commands);bindPageTools(s);bindT01();
|
||
}
|
||
function executeOrderSearch(){ clearProblem();captureOrderSearch();if(state.orderRememberSearch){state.orderSavedDefaults=safeOrderDefaults();persist();}state.selectedOrders.clear();state.orderDataState='loading';renderT01(screen('OMS-ORD-001'));setTimeout(()=>{state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));toast(`현재 조건으로 ${filteredOrders().length}건을 조회했습니다.${state.orderRememberSearch?' 안전한 조회조건을 기억했습니다.':''}`);},260); }
|
||
function bindT01(){
|
||
$('#orderSearchForm').onsubmit=e=>{e.preventDefault();executeOrderSearch()};$('#searchExecute').onclick=executeOrderSearch;$('#commandSearch').onclick=executeOrderSearch;
|
||
$('#toggleAdvanced').onclick=()=>{captureOrderSearch();state.orderAdvanced=!state.orderAdvanced;renderT01(screen('OMS-ORD-001'));};
|
||
$('#searchReset').onclick=()=>{clearProblem();state.orderQuickFilter='all';state.orderAdvanced=false;state.orderSearch={channel:'all',status:'all',keyword:'',exception:'all'};state.selectedOrders.clear();state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));toast('조회조건을 초기화했습니다.');};
|
||
const saveDefaults=$('#saveOrderDefaults');if(saveDefaults)saveDefaults.onclick=()=>{captureOrderSearch();saveOrderDefaults();renderT01(screen('OMS-ORD-001'));};
|
||
const remember=$('#rememberOrderSearch');if(remember)remember.onchange=()=>{captureOrderSearch();state.orderRememberSearch=remember.checked;if(remember.checked)state.orderSavedDefaults=safeOrderDefaults();persist();toast(remember.checked?'마지막 조회조건 기억을 켰습니다. 자유검색어는 저장하지 않습니다.':'마지막 조회조건 기억을 껐습니다.');renderT01(screen('OMS-ORD-001'));};
|
||
const retry=$('#orderRetry');if(retry)retry.onclick=executeOrderSearch;
|
||
$$('[data-order-filter]',workspace).forEach(button=>button.onclick=()=>{clearProblem();state.orderQuickFilter=button.dataset.orderFilter;state.selectedOrders.clear();state.orderDataState='ready';renderT01(screen('OMS-ORD-001'));announce(`${button.textContent.trim()} 필터를 적용했습니다.`);});
|
||
$$('input[data-order]',workspace).forEach(cb=>cb.onchange=()=>{cb.checked?state.selectedOrders.add(cb.dataset.order):state.selectedOrders.delete(cb.dataset.order);renderT01(screen('OMS-ORD-001'));});
|
||
const all=$('#selectVisibleOrders');if(all)all.onchange=()=>{filteredOrders().forEach(order=>all.checked?state.selectedOrders.add(order.no):state.selectedOrders.delete(order.no));renderT01(screen('OMS-ORD-001'));};
|
||
$$('.order-link',workspace).forEach(link=>link.onclick=e=>{e.preventDefault();showOrderDetail(link.dataset.order);});
|
||
const orderRows=$$('.order-data-row',workspace);orderRows.forEach((row,index)=>row.onkeydown=e=>{
|
||
if(['INPUT','A','BUTTON'].includes(e.target?.tagName))return;
|
||
if(e.key==='ArrowDown'||e.key==='ArrowUp'){e.preventDefault();const next=Math.max(0,Math.min(orderRows.length-1,index+(e.key==='ArrowDown'?1:-1)));orderRows.forEach((x,i)=>x.tabIndex=i===next?0:-1);orderRows[next]?.focus();}
|
||
else if(e.key===' '){e.preventDefault();const no=row.dataset.orderRow;no&&(state.selectedOrders.has(no)?state.selectedOrders.delete(no):state.selectedOrders.add(no));renderT01(screen('OMS-ORD-001'));setTimeout(()=>$$('.order-data-row',workspace)[index]?.focus(),0);}
|
||
else if(e.key==='Enter'){e.preventDefault();showOrderDetail(row.dataset.orderRow);}
|
||
});
|
||
for(const id of ['bulkShip','commandShip']){const b=$('#'+id);if(b)b.onclick=()=>{if(!state.selectedOrders.size)return;const selected=orders.filter(order=>state.selectedOrders.has(order.no));const blocked=selected.filter(order=>Boolean(order.exception));if(blocked.length){state.commandProblem={type:'business-rule',title:'출고할 수 없는 주문이 포함되어 있습니다.',detail:`${blocked.length}건은 재고·주소·송장 예외를 먼저 해결해야 합니다. 정상 주문은 이 Reference Lab에서 임의 처리하지 않습니다.`,actions:[{id:'show-blocked',label:`문제 ${blocked.length}건만 선택`}],retryable:false};renderT01(screen('OMS-ORD-001'));return;}toast(`${state.selectedOrders.size}건 출고지시를 요청했습니다.`);};}const dismiss=$('#problemDismiss');if(dismiss)dismiss.onclick=()=>{clearProblem();renderT01(screen('OMS-ORD-001'));};$$('[data-problem-action="show-blocked"]',workspace).forEach(button=>button.onclick=()=>{const blocked=orders.filter(order=>state.selectedOrders.has(order.no)&&order.exception);state.selectedOrders=new Set(blocked.map(order=>order.no));state.orderQuickFilter='all';state.orderSearch.exception='all';clearProblem();renderT01(screen('OMS-ORD-001'));announce(`문제 주문 ${blocked.length}건만 선택했습니다.`);});
|
||
const newButton=$('#commandNew');if(newButton)newButton.onclick=()=>openScreen('OMS-ORD-002');
|
||
}
|
||
function showOrderDetail(orderNo){
|
||
const order=orders.find(x=>x.no===orderNo);if(!order)return;drawerRestoreFocus=document.activeElement instanceof HTMLElement?document.activeElement:null;
|
||
openDrawerContent(`주문 ${order.no}`,`<div class="detail-status">${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();
|
||
})();
|