/* ═══════════════════════════════════════════════════════════════════════════
   ★ 템플릿 빌더 v2 — 기능 구현 중심
   1) 블록 드래그 이동 + 리사이즈
   2) 속성 ↔ 캔버스 양방향 반영
   3) 팔레트 → 캔버스 드래그앤드롭
   4) 등록/수정 팝업 편집 가능
   5) 범용: 특정 도메인 종속 없음
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── 테이블 메타데이터 ─── */
const AB_TABLE_FIELDS = {
  'order_management_test': [
    {col:'order_no',label:'수주번호',type:'numbering',on:true,required:true,pk:true},
    {col:'order_date',label:'수주일',type:'date',on:true,required:true,searchable:true},
    {col:'customer',label:'거래처',type:'entity',on:true,required:true,searchable:true,ref:{table:'customer_mng',display:'customer_name'}},
    {col:'item_name',label:'품목명',type:'text',on:true,searchable:true},
    {col:'quantity',label:'수량',type:'number',on:true},
    {col:'unit_price',label:'단가',type:'number',on:true},
    {col:'amount',label:'금액',type:'number',on:true,computed:'quantity * unit_price'},
    {col:'delivery_date',label:'납기일',type:'date',on:true},
    {col:'status',label:'상태',type:'category',on:true,searchable:true,options:['임시저장','확정','완료','취소']},
    {col:'remarks',label:'비고',type:'textarea',on:false},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
    {col:'writer',label:'작성자',type:'text',on:false,system:true},
    {col:'created_date',label:'등록일',type:'date',on:false,system:true},
  ],
  'equipment_mng': [
    {col:'equipment_code',label:'설비코드',type:'numbering',on:true,required:true,pk:true},
    {col:'equipment_name',label:'설비명',type:'text',on:true,required:true,searchable:true},
    {col:'equipment_type',label:'설비유형',type:'category',on:true,searchable:true,options:['프레스','CNC','사출','조립','검사','기타']},
    {col:'manufacturer',label:'제조사',type:'text',on:true,searchable:true},
    {col:'model_name',label:'모델명',type:'text',on:true},
    {col:'installation_location',label:'설치장소',type:'text',on:true},
    {col:'operation_status',label:'가동상태',type:'category',on:true,options:['가동중','대기','수리중','폐기']},
    {col:'introduction_date',label:'도입일자',type:'date',on:false},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
  'customer_mng': [
    {col:'customer_code',label:'거래처코드',type:'numbering',on:true,required:true,pk:true},
    {col:'customer_name',label:'거래처명',type:'text',on:true,required:true,searchable:true},
    {col:'biz_number',label:'사업자번호',type:'text',on:true,searchable:true},
    {col:'ceo_name',label:'대표자',type:'text',on:true},
    {col:'biz_type',label:'업태',type:'text',on:true},
    {col:'biz_category',label:'업종',type:'text',on:true},
    {col:'address',label:'주소',type:'text',on:true},
    {col:'tel',label:'전화번호',type:'text',on:true},
    {col:'fax',label:'팩스',type:'text',on:false},
    {col:'email',label:'이메일',type:'text',on:false},
    {col:'manager_name',label:'담당자',type:'text',on:true},
    {col:'status',label:'거래상태',type:'category',on:true,options:['거래중','거래중단','폐업']},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
  'item_info': [
    {col:'item_code',label:'품목코드',type:'numbering',on:true,required:true,pk:true},
    {col:'item_name',label:'품목명',type:'text',on:true,required:true,searchable:true},
    {col:'item_type',label:'품목유형',type:'category',on:true,searchable:true,options:['원자재','반제품','완제품','부자재']},
    {col:'unit',label:'단위',type:'category',on:true,options:['EA','KG','M','BOX','SET']},
    {col:'spec',label:'규격',type:'text',on:true},
    {col:'safety_stock',label:'안전재고',type:'number',on:false},
    {col:'unit_price',label:'단가',type:'number',on:false},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
  'purchase_order_mng': [
    {col:'po_no',label:'발주번호',type:'numbering',on:true,required:true,pk:true},
    {col:'po_date',label:'발주일',type:'date',on:true,required:true,searchable:true},
    {col:'supplier_name',label:'공급업체',type:'entity',on:true,required:true,searchable:true},
    {col:'item_name',label:'품목',type:'text',on:true},
    {col:'order_qty',label:'발주수량',type:'number',on:true},
    {col:'received_qty',label:'입고수량',type:'number',on:true},
    {col:'pending_qty',label:'미입고',type:'number',on:true,computed:'order_qty - received_qty'},
    {col:'delivery_date',label:'납기일',type:'date',on:true},
    {col:'status',label:'상태',type:'category',on:true,searchable:true,options:['진행중','입고완료','취소']},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
  'user_info': [
    {col:'user_id',label:'사원번호',type:'numbering',on:true,required:true,pk:true},
    {col:'user_name',label:'사원명',type:'text',on:true,required:true,searchable:true},
    {col:'dept_name',label:'부서',type:'entity',on:true,searchable:true},
    {col:'position',label:'직급',type:'category',on:true,options:['사원','주임','대리','과장','차장','부장','이사']},
    {col:'status',label:'상태',type:'category',on:true,searchable:true,options:['재직','휴직','퇴직']},
    {col:'join_date',label:'입사일',type:'date',on:true},
    {col:'phone',label:'전화번호',type:'text',on:false},
    {col:'email',label:'이메일',type:'text',on:false},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
  'inbound_mng': [
    {col:'receive_no',label:'입고번호',type:'numbering',on:true,required:true,pk:true},
    {col:'receive_date',label:'입고일',type:'date',on:true,required:true,searchable:true},
    {col:'receive_type',label:'입고유형',type:'category',on:true,searchable:true,options:['구매입고','외주입고','생산입고','기타']},
    {col:'warehouse',label:'창고',type:'category',on:true,options:['자재창고','제품창고','반품창고']},
    {col:'supplier',label:'공급업체',type:'entity',on:true},
    {col:'item_name',label:'품목',type:'text',on:true},
    {col:'quantity',label:'수량',type:'number',on:true},
    {col:'inspector',label:'검수자',type:'entity',on:true},
    {col:'status',label:'상태',type:'category',on:true,options:['대기','검수중','완료','반품']},
    {col:'company_code',label:'회사코드',type:'text',on:false,system:true},
  ],
};

/* ─── DataPort 기본 포트 정의 (컴포넌트 규격 v1.0) ─── */
const AB_DEFAULT_PORTS = {
  table: {
    outputs: [
      {name:'selectedRow', type:'row', label:'선택 행'},
      {name:'selectedRows', type:'rows', label:'선택 행(복수)'}
    ],
    inputs: [
      {name:'searchParams', type:'params', label:'검색 조건'},
      {name:'refreshTrigger', type:'value', label:'새로고침'}
    ]
  },
  form: {
    outputs: [
      {name:'formData', type:'row', label:'폼 데이터'},
      {name:'savedRow', type:'row', label:'저장된 행'}
    ],
    inputs: [
      {name:'loadRow', type:'row', label:'행 로드'}
    ]
  },
  search: {
    outputs: [
      {name:'searchParams', type:'params', label:'검색 조건'}
    ],
    inputs: []
  },
  'button': {
    outputs: [
      {name:'clicked', type:'value', label:'클릭'}
    ],
    inputs: []
  },
  'btn-bar': {
    outputs: [
      {name:'clicked', type:'value', label:'클릭'}
    ],
    inputs: []
  },
  pagi: {
    outputs: [
      {name:'pageChange', type:'params', label:'페이지 변경'}
    ],
    inputs: [
      {name:'totalCount', type:'value', label:'총 건수'}
    ]
  },
  tabs: { outputs:[], inputs:[] },
  'text-display': { outputs:[], inputs:[] },
};

/* ─── FieldConfig 타입 목록 (규격 v1.0) ─── */
const AB_FIELD_TYPES = ['text','number','date','datetime','select','entity','checkbox','textarea','file','code','numbering','category'];

/* ─── 상태 ─── */
let _abTable=null, _abPreset='basic', _abFields=[], _abBlocks=[], _abSelectedBlock=null, _abBlockId=0;
let _abView='list';
let _abViewBlocks={list:[],create:[],edit:[]};
let _abConnections=[]; /* [{id, from:{componentId,port}, to:{componentId,port}}] */
let _abExpandedField=null; /* 현재 펼쳐진 필드 상세 (col) */

/* 드래그/리사이즈 상태 */
let _drag=null; /* {id, startX, startY, origX, origY, mode:'move'|'resize', origW, origH} */

function getTableFields(t){
  if(AB_TABLE_FIELDS[t]) return JSON.parse(JSON.stringify(AB_TABLE_FIELDS[t]));
  const m=AB_TABLES.find(x=>x.name===t);
  const c=m?m.cols:5, f=[];
  f.push({col:'id',label:'ID',type:'numbering',on:true,required:true,pk:true});
  for(let i=1;i<c-2;i++) f.push({col:'field_'+i,label:'필드'+i,type:'text',on:true});
  f.push({col:'status',label:'상태',type:'category',on:true,options:['활성','비활성']});
  f.push({col:'company_code',label:'회사코드',type:'text',on:false,system:true});
  return f;
}

/* ═══ 테이블/프리셋 변경 ═══ */
function abOnTableChange(v){
  if(!v) return;
  _abTable=v; _abFields=getTableFields(v);
  const t=AB_TABLES.find(x=>x.name===v);
  toast((t?t.label:v)+' — '+_abFields.length+'개 필드 로드','info');
  /* 기존 블록들 갱신 (테이블 바뀌면 프리뷰도 바뀌어야) */
  if(_abBlocks.length) abRefreshBlockContent();
}

/* ═══ 뷰 탭 전환 ═══ */
function abSwitchView(view){
  _abViewBlocks[_abView]=[..._abBlocks];
  _abView=view;
  document.querySelectorAll('.ab-view-tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));
  const hints={list:'목록 + 검색 + 그리드',create:'등록 팝업 편집',edit:'수정 팝업 편집 (등록 상속)'};
  const h=document.getElementById('ab-view-hint'); if(h) h.textContent=hints[view]||'';

  const overlay=document.getElementById('ab-popup-overlay');
  if(view==='list'){
    overlay.classList.remove('show');
    _abBlocks=[]; abRenderBlocks(_abViewBlocks.list);
  } else {
    if(!_abViewBlocks[view].length && _abTable) abAutoPopup(view);
    overlay.classList.add('show');
    _abBlocks=[]; abRenderPopupBlocks(_abViewBlocks[view], view);
  }
  abClearSel();
}

/* ═══ 생성 ═══ */
function abGenerate(){
  if(!_abTable){toast('테이블을 먼저 선택하세요','warn');return;}
  document.querySelectorAll('.ab-tb-btn[data-preset]').forEach(b=>{if(b.classList.contains('active'))_abPreset=b.dataset.preset;});
  const t=AB_TABLES.find(x=>x.name===_abTable), label=t?t.label:_abTable;
  const on=_abFields.filter(f=>f.on), sr=_abFields.filter(f=>f.searchable);
  abClearCanvas();
  if(_abPreset==='basic') abGenBasic(label,on,sr);
  else if(_abPreset==='split') abGenSplit(label,on,sr);
  else if(_abPreset==='tabs') abGenTabs(label,on,sr);
  _abViewBlocks.list=[..._abBlocks];
  _abViewBlocks.create=[]; _abViewBlocks.edit=[];
  abAutoPopup('create'); abAutoPopup('edit');
  abAutoConnections();
  abUpdateStatus();
  toast('⚡ '+label+' ('+_abPreset+') 생성 — 3뷰','success');
}

function abGenBasic(L,F,S){
  const cv=document.getElementById('ab-canvas');
  _addBlk(cv,'text-display','제목',16,12,300,36,pvTitle(L));
  _addBlk(cv,'btn-bar','버튼 바',500,12,370,36,pvBtnBar(L),true);
  if(S.length) _addBlk(cv,'search','검색 필터',16,58,854,42,pvSearch(S));
  const y=S.length?110:58;
  _addBlk(cv,'table','데이터 테이블 — '+_abTable,16,y,854,380,pvTable(F));
  _addBlk(cv,'pagi','페이지네이션',16,y+390,854,24,pvPagi());
}
function abGenSplit(L,F,S){
  const cv=document.getElementById('ab-canvas');
  _addBlk(cv,'text-display','제목',16,12,250,36,pvTitle(L));
  _addBlk(cv,'btn-bar','버튼 바',270,12,200,36,pvBtnBar(L),true);
  if(S.length) _addBlk(cv,'search','검색 필터',16,58,480,42,pvSearch(S));
  const y=S.length?110:58;
  _addBlk(cv,'table','목록 — '+_abTable,16,y,480,400,pvTable(F.slice(0,5)));
  _addBlk(cv,'form','상세 폼 — '+_abTable,510,58,360,452,pvForm(L+' 상세',F));
}
function abGenTabs(L,F,S){
  const cv=document.getElementById('ab-canvas');
  _addBlk(cv,'text-display','제목',16,12,300,36,pvTitle(L));
  _addBlk(cv,'tabs','탭',16,56,854,32,pvTabsHtml());
  if(S.length) _addBlk(cv,'search','검색 필터',16,96,854,42,pvSearch(S));
  const y=S.length?148:96;
  _addBlk(cv,'table','데이터 테이블 — '+_abTable,16,y,854,360,pvTable(F));
}

/* 팝업 자동생성 (등록/수정) */
function abAutoPopup(vt){
  const t=AB_TABLES.find(x=>x.name===_abTable), label=t?t.label:_abTable;
  const F=_abFields.filter(f=>f.on&&!f.system);
  const title=vt==='create'?label+' 등록':label+' 수정';
  const blocks=[];
  /* 팝업 안에서도 개별 블록으로 배치 — 편집 가능 */
  let yy=12;
  blocks.push({id:'pop-title-'+vt,type:'text-display',label:'팝업 제목',x:16,y:yy,w:440,h:32,
    content:pvTitle(title)});
  yy+=40;
  blocks.push({id:'pop-form-'+vt,type:'form',label:'입력 폼',x:16,y:yy,w:440,h:Math.max(200,F.length*38+40),
    content:pvForm(title,F)});
  yy+=Math.max(200,F.length*38+40)+10;
  blocks.push({id:'pop-btns-'+vt,type:'btn-bar',label:'버튼',x:16,y:yy,w:440,h:36,
    content:'<div style="display:flex;gap:.2rem;justify-content:flex-end;padding:.35rem .5rem;">'+
      '<button style="padding:.2rem .5rem;border-radius:4px;background:transparent;border:1px solid var(--border);color:var(--text-muted);font-size:.46rem;">취소</button>'+
      '<button style="padding:.2rem .5rem;border-radius:4px;background:var(--primary);border:none;color:#fff;font-size:.46rem;font-weight:700;">💾 저장</button></div>'});
  _abViewBlocks[vt]=blocks;
}

/* ═══ 블록 추가 (공통) ═══ */
function _addBlk(cv,type,label,x,y,w,h,content,isAct){
  const id='ab-blk-'+(++_abBlockId);
  const b={id,type,label,table:_abTable,x,y,w,h,content:content||''};
  _abBlocks.push(b);
  _renderOneBlock(cv,b,isAct);
  document.getElementById('ab-empty').style.display='none';
  return b;
}

function _renderOneBlock(cv,b,isAct){
  const el=document.createElement('div');
  el.className='ab-block'; el.id=b.id; el.dataset.type=b.type;
  _applyPos(el,b);
  el.innerHTML='<div class="ab-block-label">'+b.label+'</div>'+
    '<div class="ab-block-content" '+(isAct?'style="pointer-events:auto;"':'')+'>'+b.content+'</div>'+
    '<div class="ab-resize-handle"></div>';
  el.addEventListener('mousedown',function(e){
    if(e.target.classList.contains('ab-resize-handle')){
      _startResize(e,b.id); e.preventDefault(); e.stopPropagation();
    } else if(!e.target.closest('.ab-action-btn')){
      abSelectBlock(b.id);
      _startDrag(e,b.id);
      e.stopPropagation();
    }
  });
  cv.appendChild(el);
}

function _applyPos(el,b){
  el.style.left=b.x+'px'; el.style.top=b.y+'px'; el.style.width=b.w+'px'; el.style.height=b.h+'px';
}

/* ═══ 블록 드래그 이동 ═══ */
function _startDrag(e,id){
  const b=_abBlocks.find(x=>x.id===id); if(!b) return;
  _drag={id,startX:e.clientX,startY:e.clientY,origX:b.x,origY:b.y,mode:'move',origW:b.w,origH:b.h};
  document.body.style.cursor='grabbing';
  document.body.style.userSelect='none';
}
function _startResize(e,id){
  const b=_abBlocks.find(x=>x.id===id); if(!b) return;
  _drag={id,startX:e.clientX,startY:e.clientY,origX:b.x,origY:b.y,mode:'resize',origW:b.w,origH:b.h};
  document.body.style.cursor='nwse-resize';
  document.body.style.userSelect='none';
}
/* 자유 이동 (Shift 누르면 8px 스냅) */
document.addEventListener('mousemove',function(e){
  if(!_drag) return;
  e.preventDefault();
  const dx=e.clientX-_drag.startX, dy=e.clientY-_drag.startY;
  const b=_abBlocks.find(x=>x.id===_drag.id); if(!b) return;
  const el=document.getElementById(_drag.id); if(!el) return;

  if(_drag.mode==='move'){
    let nx=_drag.origX+dx, ny=_drag.origY+dy;
    if(e.shiftKey){ nx=Math.round(nx/8)*8; ny=Math.round(ny/8)*8; }
    b.x=Math.max(0,nx); b.y=Math.max(0,ny);
  } else {
    let nw=_drag.origW+dx, nh=_drag.origH+dy;
    if(e.shiftKey){ nw=Math.round(nw/8)*8; nh=Math.round(nh/8)*8; }
    b.w=Math.max(40,nw); b.h=Math.max(20,nh);
  }
  _applyPos(el,b);
  /* 속성 패널 위치/크기 실시간 반영 */
  if(_abSelectedBlock&&_abSelectedBlock.id===b.id){
    const xi=document.querySelector('.ab-p-smv[onchange*="\'x\'"]');
    const yi=document.querySelector('.ab-p-smv[onchange*="\'y\'"]');
    const wi=document.querySelector('.ab-p-smv[onchange*="\'w\'"]');
    const hi=document.querySelector('.ab-p-smv[onchange*="\'h\'"]');
    if(xi) xi.value=Math.round(b.x);
    if(yi) yi.value=Math.round(b.y);
    if(wi) wi.value=Math.round(b.w);
    if(hi) hi.value=Math.round(b.h);
  }
});
document.addEventListener('mouseup',function(){
  if(_drag){
    /* 놓을 때 가까운 정수로 정리 */
    const b=_abBlocks.find(x=>x.id===_drag.id);
    if(b){ b.x=Math.round(b.x); b.y=Math.round(b.y); b.w=Math.round(b.w); b.h=Math.round(b.h);
      const el=document.getElementById(b.id); if(el) _applyPos(el,b); }
    _drag=null;
    document.body.style.cursor='';
    document.body.style.userSelect='';
    abUpdateStatus();
  }
});

/* ═══ 블록 선택/해제 ═══ */
function abSelectBlock(id){
  document.querySelectorAll('.ab-block.selected').forEach(b=>b.classList.remove('selected'));
  const el=document.getElementById(id); if(!el) return;
  el.classList.add('selected');
  _abSelectedBlock=_abBlocks.find(b=>b.id===id);
  abUpdateProps();
}
function abClearSel(){
  document.querySelectorAll('.ab-block.selected').forEach(b=>b.classList.remove('selected'));
  _abSelectedBlock=null;
  const p=document.getElementById('ab-props-content');
  if(p) p.innerHTML='<div style="padding:.6rem;text-align:center;color:var(--text-muted);font-size:.5rem;">캔버스에서 컴포넌트를<br>선택하세요</div>';
}
/* 빈 캔버스 클릭 */
document.addEventListener('click',function(e){
  if(e.target.closest('.ab-block')||e.target.closest('.ab-popup-frame')) return;
  if(e.target.closest('#ab-canvas-wrap')||e.target.closest('#ab-canvas')) abClearSel();
});

/* 키보드 단축키 */
document.addEventListener('keydown',function(e){
  if(!_abSelectedBlock) return;
  if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT'||e.target.tagName==='TEXTAREA') return;
  const b=_abSelectedBlock;
  const el=document.getElementById(b.id);
  const step=e.shiftKey?10:1;
  switch(e.key){
    case 'Delete': case 'Backspace': abDeleteBlock(); e.preventDefault(); break;
    case 'ArrowLeft':  b.x=Math.max(0,b.x-step); _applyPos(el,b); abUpdateProps(); e.preventDefault(); break;
    case 'ArrowRight': b.x+=step; _applyPos(el,b); abUpdateProps(); e.preventDefault(); break;
    case 'ArrowUp':    b.y=Math.max(0,b.y-step); _applyPos(el,b); abUpdateProps(); e.preventDefault(); break;
    case 'ArrowDown':  b.y+=step; _applyPos(el,b); abUpdateProps(); e.preventDefault(); break;
  }
});

/* ═══ 속성 패널 (타입별 분기 + 양방향) ═══ */
function abUpdateProps(){
  const P=document.getElementById('ab-props-content');
  if(!P||!_abSelectedBlock) return;
  const b=_abSelectedBlock, F=_abFields.length?_abFields:[];
  let h='';

  h+='<div class="ab-p-sec">컴포넌트 정보</div>';
  h+=pRow('종류',tLabel(b.type),true);
  h+='<div class="ab-p-row"><div class="ab-p-label">이름</div><input class="ab-p-val" value="'+esc(b.label)+'" onchange="abSetProp(\'label\',this.value)"></div>';

  h+='<div class="ab-p-sec">위치 · 크기</div>';
  h+='<div class="ab-p-grid">';
  h+=pSm('X',b.x,'x')+pSm('Y',b.y,'y')+pSm('W',b.w,'w')+pSm('H',b.h,'h');
  h+='</div>';

  /* 타입별 */
  if(b.type==='table') h+=propsTable(b,F);
  else if(b.type==='search') h+=propsSearch(b,F);
  else if(b.type==='form') h+=propsForm(b,F);
  else if(b.type==='button') h+=propsButton(b);
  else if(b.type==='btn-bar') h+=propsBtnBar(b);
  else if(b.type==='text-display') h+=propsText(b);
  else if(b.type==='tabs') h+=propsTabs(b);
  else{ h+='<div class="ab-p-sec">설정</div><div class="ab-p-hint">속성 설정 영역</div>'; }

  /* 삭제 버튼 */
  h+='<div style="padding:.4rem .35rem;"><button style="width:100%;padding:.25rem;border-radius:4px;border:1px solid var(--danger,#f87171);background:transparent;color:var(--danger,#f87171);font-size:.46rem;font-weight:600;cursor:pointer;" onclick="abDeleteBlock()">🗑 블록 삭제</button></div>';

  P.innerHTML=h;
}

/* 속성 변경 → 캔버스 반영 */
function abSetProp(key,val){
  if(!_abSelectedBlock) return;
  const b=_abSelectedBlock;
  if(key==='label'){
    b.label=val;
    const el=document.getElementById(b.id);
    if(el) el.querySelector('.ab-block-label').textContent=val;
  }
}
function abSetPos(key,val){
  if(!_abSelectedBlock) return;
  const b=_abSelectedBlock, n=parseInt(val)||0;
  b[key]=Math.max(0,n);
  const el=document.getElementById(b.id);
  if(el) _applyPos(el,b);
}

/* 블록 삭제 */
function abDeleteBlock(){
  if(!_abSelectedBlock) return;
  const el=document.getElementById(_abSelectedBlock.id);
  if(el) el.remove();
  _abBlocks=_abBlocks.filter(b=>b.id!==_abSelectedBlock.id);
  abClearSel(); abUpdateStatus();
  if(!_abBlocks.length) document.getElementById('ab-empty').style.display='';
}

/* 필드 on/off 토글 + 캔버스 재생성 */
function abToggleField(col,el){
  el.classList.toggle('on');
  const f=_abFields.find(x=>x.col===col);
  if(f) f.on=el.classList.contains('on');
  /* 테이블/폼 블록 미리보기 재생성 */
  abRefreshBlockContent();
}
function abRefreshBlockContent(){
  if(!_abTable) return;
  const F=_abFields.filter(f=>f.on);
  const S=_abFields.filter(f=>f.searchable);
  const t=AB_TABLES.find(x=>x.name===_abTable), L=t?t.label:_abTable;
  /* 같은 테이블의 모든 블록 갱신 */
  _abBlocks.forEach(b=>{
    let newContent=null;
    if(b.type==='table') newContent=pvTable(F.slice(0,8));
    else if(b.type==='form') newContent=pvForm(L+' 입력',F.filter(f=>!f.system));
    else if(b.type==='search') newContent=S.length?pvSearch(S):pvSearch([{label:'검색 조건 없음'}]);
    if(newContent){
      b.content=newContent;
      const el=document.getElementById(b.id);
      if(el){ const c=el.querySelector('.ab-block-content'); if(c) c.innerHTML=newContent; }
    }
  });
}

/* ─── 데이터 테이블 속성 ─── */
function propsTable(b,F){
  let h='<button class="ab-p-tbl-settings-btn" onclick="abTblModalOpen()">⚙ 테이블 설정</button>';
  h+='<div class="ab-p-sec">데이터</div>';
  h+=pSel('연결 테이블',b.table||_abTable,AB_TABLES.map(t=>t.name));
  const sortCols=F.filter(f=>f.on).map(f=>f.col);
  h+=pSel('기본 정렬',sortCols[0]||'',sortCols);
  h+=pSel('정렬 순서','내림차순',['내림차순','오름차순']);
  h+=pTog('자동 불러오기',true);

  h+='<div class="ab-p-sec">표시할 컬럼</div>';
  h+='<div class="ab-p-hint">체크: 보이기 · 클릭: 상세 설정</div>';
  h+=fieldListFC(F);

  h+='<div class="ab-p-sec">테이블 동작</div>';
  h+=pTog('행 클릭 선택',true);
  h+=pSel('선택 방식','다중 선택',['단일 선택','다중 선택']);
  h+=pTog('표에서 바로 수정',false);
  h+=pTog('페이지 나누기',true);

  h+='<div class="ab-p-sec">데이터 흐름</div>';
  h+='<div class="ab-p-hint">이 테이블이 다른 컴포넌트와 주고받는 데이터</div>';
  h+=propsDataPorts(b,'outputs');
  h+=propsDataPorts(b,'inputs');
  h+=propsConnections(b);
  return h;
}
function propsSearch(b,F){
  let h='<div class="ab-p-sec">검색 조건</div>';
  h+='<div class="ab-p-hint">체크: 검색에 포함 · 클릭: 상세 설정</div>';
  h+='<div class="ab-p-fields">';
  F.forEach(f=>{
    h+='<div class="ab-p-field" onclick="abExpandField(\''+f.col+'\',event)" style="cursor:pointer;">';
    h+='<div class="ab-p-fcheck '+(f.searchable?'on':'')+'" onclick="abToggleSearchable(\''+f.col+'\',this);event.stopPropagation();">✓</div>';
    h+='<span class="ab-p-fname">'+f.label+'</span><span class="ab-p-ftype">'+f.type+'</span></div>';
    if(_abExpandedField===f.col) h+=fieldDetailPanel(f);
  });
  h+='</div>';
  h+='<div class="ab-p-sec">검색 옵션</div>';
  h+=pTog('날짜는 범위로 검색',true);
  h+=pTog('초기화 버튼 보이기',true);

  h+='<div class="ab-p-sec">데이터 흐름</div>';
  h+='<div class="ab-p-hint">검색 결과를 어디로 보낼지</div>';
  h+=propsDataPorts(b,'outputs');
  h+=propsConnections(b);
  return h;
}
function propsForm(b,F){
  let h='<div class="ab-p-sec">입력 폼 설정</div>';
  h+=pSel('한 줄에 몇 칸','2칸',['1칸','2칸','3칸']);
  h+=pTog('구역 나누기',true);

  h+='<div class="ab-p-sec">입력 항목</div>';
  h+='<div class="ab-p-hint">체크: 폼에 표시 · 클릭: 상세 설정</div>';
  h+=fieldListFC(F.filter(f=>!f.system));

  h+='<div class="ab-p-sec">저장 설정</div>';
  h+=pSel('저장 방식','새로 만들기 또는 수정',['새로 만들기','수정','새로 만들기 또는 수정']);

  h+='<div class="ab-p-sec">데이터 흐름</div>';
  h+='<div class="ab-p-hint">어디서 데이터를 받고, 저장 후 어디로 보낼지</div>';
  h+=propsDataPorts(b,'inputs');
  h+=propsDataPorts(b,'outputs');
  h+=propsConnections(b);
  return h;
}
function propsButton(b){
  let h='<div class="ab-p-sec">버튼 설정</div>';
  h+=pRow('버튼 텍스트','저장');
  h+='<div class="ab-p-row"><div class="ab-p-label">눌렀을 때</div><select class="ab-p-val">';
  [['save','저장'],['delete','삭제'],['modal','팝업 열기'],['approval','결재 요청'],
   ['navigate','화면 이동'],['excel_download','엑셀 다운로드'],['custom','직접 설정']].forEach(([v,l])=>{
    h+='<option value="'+v+'">'+l+'</option>';
  });
  h+='</select></div>';
  h+='<div class="ab-p-row"><div class="ab-p-label">버튼 스타일</div><select class="ab-p-val">';
  [['primary','강조 (파란색)'],['default','기본 (테두리)'],['destructive','위험 (빨간색)'],['ghost','투명']].forEach(([v,l])=>{
    h+='<option value="'+v+'">'+l+'</option>';
  });
  h+='</select></div>';
  h+=pTog('누르기 전 확인 메시지',false);
  h+=pRow('확인 메시지','','','정말 삭제하시겠습니까?');

  h+='<div class="ab-p-sec">데이터 흐름</div>';
  h+='<div class="ab-p-hint">버튼과 연결된 컴포넌트</div>';
  h+=propsDataPorts(b,'outputs');
  h+=propsConnections(b);
  return h;
}

function propsBtnBar(b){
  let h='<div class="ab-p-sec">버튼 목록</div>';
  h+='<div class="ab-p-hint">각 버튼이 눌렸을 때 무엇을 할지 설정</div>';
  const btnDefs=[
    ['등록','팝업 열기','modal'],
    ['삭제','선택 항목 삭제','delete'],
    ['엑셀','엑셀 다운로드','excel_download']
  ];
  btnDefs.forEach(([name,desc,action])=>{
    h+='<div style="display:flex;align-items:center;gap:.2rem;padding:.15rem .35rem;border-bottom:1px dashed var(--border);">';
    h+='<span style="font-size:.48rem;font-weight:600;flex:1;color:var(--text);">'+name+'</span>';
    h+='<select class="ab-p-val" style="width:auto;font-size:.42rem;padding:.1rem;">';
    [['modal','팝업 열기'],['save','저장'],['delete','삭제'],['approval','결재 요청'],
     ['navigate','화면 이동'],['excel_download','엑셀 다운로드'],['custom','직접 설정']].forEach(([val,lbl])=>{
      h+='<option value="'+val+'"'+(val===action?' selected':'')+'>'+lbl+'</option>';
    });
    h+='</select></div>';
  });
  h+='<button style="margin:.3rem .35rem;padding:.18rem;border-radius:4px;border:1px dashed var(--border);background:transparent;color:var(--text-muted);font-size:.44rem;cursor:pointer;width:calc(100% - .7rem);">+ 버튼 추가</button>';
  return h;
}
function propsText(b){
  let h='<div class="ab-p-sec">제목 설정</div>';
  const t=AB_TABLES.find(x=>x.name===_abTable);
  h+=pRow('표시 내용',t?t.label:'제목');
  h+=pSel('글자 크기','보통',[['작게','.5rem'],['보통','.75rem'],['크게','1rem'],['아주 크게','1.2rem']].map(x=>x[0]));
  h+=pSel('글자 굵기','굵게',['보통','약간 굵게','굵게','아주 굵게']);
  return h;
}
function propsTabs(b){
  let h='<div class="ab-p-sec">탭 설정</div>';
  h+='<div class="ab-p-hint">탭 이름을 쉼표(,)로 구분해서 입력하세요</div>';
  h+=pRow('탭 이름','전체 목록, 상태별, 통계');
  return h;
}

/* 필드 목록 (on/off 체크 + 드래그 순서) */
function fieldList(F){
  let h='<div class="ab-p-fields">';
  F.forEach(f=>{
    h+='<div class="ab-p-field">';
    h+='<div class="ab-p-fcheck '+(f.on?'on':'')+'" onclick="abToggleField(\''+f.col+'\',this)">✓</div>';
    h+='<span class="ab-p-fname">'+f.label+'<span class="ab-p-fcol">'+f.col+'</span></span>';
    h+='<span class="ab-p-ftype">'+f.type+'</span><span class="ab-p-fdrag">⋮</span></div>';
  });
  return h+'</div>';
}

function abToggleSearchable(col,el){
  el.classList.toggle('on');
  const f=_abFields.find(x=>x.col===col);
  if(f) f.searchable=el.classList.contains('on');
  abRefreshBlockContent();
}

/* ═══ 데이터 흐름 속성 섹션 ═══ */
function propsDataPorts(b, direction){
  const ports = AB_DEFAULT_PORTS[b.type];
  if(!ports) return '';
  const list = direction==='outputs' ? ports.outputs : ports.inputs;
  if(!list || !list.length) return '';
  const dirLabel = direction==='outputs' ? '내보내는 데이터' : '받는 데이터';
  const dirIcon = direction==='outputs' ? '→' : '←';
  let h='<div style="padding:.1rem .35rem;font-size:.4rem;font-weight:600;color:var(--text-muted);margin-top:.2rem;">'+dirLabel+'</div>';
  list.forEach(p=>{
    h+='<div class="ab-p-conn">';
    h+='<span class="ab-p-conn-dir">'+dirIcon+'</span>';
    h+='<span class="ab-p-conn-target">'+p.label+'</span>';
    h+='<span class="ab-p-conn-trigger">'+p.name+'</span>';
    h+='<span class="ab-p-ftype" style="margin-left:auto;">'+p.type+'</span>';
    h+='</div>';
  });
  return h;
}

/* ═══ 컴포넌트 간 연결 설정 UI ═══ */
function propsConnections(b){
  const ports = AB_DEFAULT_PORTS[b.type];
  if(!ports) return '';
  const hasOutputs = ports.outputs && ports.outputs.length;
  const hasInputs = ports.inputs && ports.inputs.length;
  if(!hasOutputs && !hasInputs) return '';

  let h='<div style="padding:.1rem .35rem;font-size:.4rem;font-weight:600;color:var(--text-muted);margin-top:.2rem;">연결된 컴포넌트</div>';
  /* 현재 블록과 관련된 연결 표시 */
  const myConns = _abConnections.filter(c=>c.from.componentId===b.id || c.to.componentId===b.id);
  if(myConns.length){
    myConns.forEach(c=>{
      const isFrom = c.from.componentId===b.id;
      const otherBlk = _abBlocks.find(x=>x.id===(isFrom?c.to.componentId:c.from.componentId));
      const otherLabel = otherBlk ? otherBlk.label : '?';
      h+='<div class="ab-p-conn">';
      if(isFrom){
        h+='<span class="ab-p-conn-dir" style="color:var(--cyan,#22d3ee);">→</span>';
        h+='<span class="ab-p-conn-target" style="font-size:.42rem;">'+c.from.port+' → '+otherLabel+'.'+c.to.port+'</span>';
      } else {
        h+='<span class="ab-p-conn-dir" style="color:#f472b6;">←</span>';
        h+='<span class="ab-p-conn-target" style="font-size:.42rem;">'+otherLabel+'.'+c.from.port+' → '+c.to.port+'</span>';
      }
      h+='<span class="ab-p-conn-del" onclick="abRemoveConn(\''+c.id+'\')">✕</span>';
      h+='</div>';
    });
  } else {
    h+='<div class="ab-p-hint">연결 없음</div>';
  }
  /* 새 연결 추가 버튼 */
  if(hasOutputs){
    h+='<div class="ab-p-conn-add" onclick="abShowNewConn(\''+b.id+'\')">+ 연결 추가</div>';
  }
  return h;
}

/* 새 연결 추가 인터페이스 */
let _abNewConnState = null; /* {fromId, showing} */
function abShowNewConn(fromId){
  _abNewConnState = {fromId};
  const b = _abBlocks.find(x=>x.id===fromId);
  if(!b) return;
  const ports = AB_DEFAULT_PORTS[b.type];
  if(!ports || !ports.outputs.length) return;
  /* 연결 가능한 대상 (input 포트가 있는 다른 블록) */
  const targets = _abBlocks.filter(x=>x.id!==fromId && AB_DEFAULT_PORTS[x.type] && AB_DEFAULT_PORTS[x.type].inputs && AB_DEFAULT_PORTS[x.type].inputs.length);
  if(!targets.length){toast('연결 가능한 대상 컴포넌트가 없습니다','warn');return;}

  let h='<div class="ab-p-new-conn">';
  h+='<div class="ab-p-hint" style="padding:0;margin-bottom:.1rem;">보낼 데이터</div>';
  h+='<select class="ab-p-val" id="ab-conn-out-port" style="margin-bottom:.2rem;">';
  ports.outputs.forEach(p=>{ h+='<option value="'+p.name+'">'+p.label+'</option>'; });
  h+='</select>';
  h+='<div class="ab-p-hint" style="padding:0;margin-bottom:.1rem;">받을 컴포넌트</div>';
  h+='<select class="ab-p-val" id="ab-conn-target" style="margin-bottom:.2rem;" onchange="abUpdateTargetPorts()">';
  targets.forEach(t=>{ h+='<option value="'+t.id+'">'+t.label+'</option>'; });
  h+='</select>';
  h+='<div class="ab-p-hint" style="padding:0;margin-bottom:.1rem;">받을 데이터</div>';
  h+='<select class="ab-p-val" id="ab-conn-in-port">';
  const firstTarget = targets[0];
  const inPorts = AB_DEFAULT_PORTS[firstTarget.type].inputs;
  inPorts.forEach(p=>{ h+='<option value="'+p.name+'">'+p.label+'</option>'; });
  h+='</select>';
  h+='<div style="display:flex;gap:.2rem;margin-top:.25rem;">';
  h+='<button class="ab-p-val" style="flex:1;cursor:pointer;text-align:center;font-weight:600;background:var(--primary);color:#fff;border-color:var(--primary);" onclick="abAddConn()">연결</button>';
  h+='<button class="ab-p-val" style="width:40px;cursor:pointer;text-align:center;" onclick="abCancelNewConn()">취소</button>';
  h+='</div></div>';

  /* 기존 연결 추가 버튼을 교체 */
  const P=document.getElementById('ab-props-content');
  const addBtn = P.querySelector('.ab-p-conn-add');
  if(addBtn){
    addBtn.outerHTML = h;
  }
}

function abUpdateTargetPorts(){
  const targetSel = document.getElementById('ab-conn-target');
  const inPortSel = document.getElementById('ab-conn-in-port');
  if(!targetSel || !inPortSel) return;
  const targetBlk = _abBlocks.find(x=>x.id===targetSel.value);
  if(!targetBlk) return;
  const inPorts = AB_DEFAULT_PORTS[targetBlk.type]?.inputs || [];
  inPortSel.innerHTML='';
  inPorts.forEach(p=>{
    const o=document.createElement('option');
    o.value=p.name; o.textContent=p.label+' ('+p.name+' : '+p.type+')';
    inPortSel.appendChild(o);
  });
}

function abAddConn(){
  if(!_abNewConnState) return;
  const outPort = document.getElementById('ab-conn-out-port')?.value;
  const targetId = document.getElementById('ab-conn-target')?.value;
  const inPort = document.getElementById('ab-conn-in-port')?.value;
  if(!outPort || !targetId || !inPort){ toast('포트를 모두 선택하세요','warn'); return; }
  /* 중복 확인 */
  const dup = _abConnections.find(c=>c.from.componentId===_abNewConnState.fromId && c.from.port===outPort && c.to.componentId===targetId && c.to.port===inPort);
  if(dup){ toast('이미 동일한 연결이 존재합니다','warn'); return; }
  _abConnections.push({
    id:'conn-'+(Date.now()),
    from:{componentId:_abNewConnState.fromId, port:outPort},
    to:{componentId:targetId, port:inPort}
  });
  _abNewConnState=null;
  const targetBlk = _abBlocks.find(x=>x.id===targetId);
  toast('연결 추가: '+outPort+' → '+(targetBlk?targetBlk.label:'?')+'.'+inPort,'success');
  abUpdateProps();
  abUpdateStatus();
}

function abCancelNewConn(){
  _abNewConnState=null;
  abUpdateProps();
}

function abRemoveConn(connId){
  _abConnections = _abConnections.filter(c=>c.id!==connId);
  abUpdateProps();
  abUpdateStatus();
  toast('연결 제거','info');
}

/* 자동 연결 생성 (프리셋 생성 시) */
function abAutoConnections(){
  _abConnections=[];
  const searchBlk = _abBlocks.find(b=>b.type==='search');
  const tableBlk = _abBlocks.find(b=>b.type==='table');
  const formBlk = _abBlocks.find(b=>b.type==='form');
  /* search → table */
  if(searchBlk && tableBlk){
    _abConnections.push({
      id:'conn-auto-1',
      from:{componentId:searchBlk.id, port:'searchParams'},
      to:{componentId:tableBlk.id, port:'searchParams'}
    });
  }
  /* table → form (split 프리셋) */
  if(tableBlk && formBlk){
    _abConnections.push({
      id:'conn-auto-2',
      from:{componentId:tableBlk.id, port:'selectedRow'},
      to:{componentId:formBlk.id, port:'loadRow'}
    });
  }
}

/* ═══ FieldConfig 상세 필드 목록 (클릭으로 펼침) ═══ */
function fieldListFC(F){
  let h='<div class="ab-p-fields" style="max-height:280px;">';
  F.forEach(f=>{
    const expanded = _abExpandedField===f.col;
    h+='<div class="ab-p-field'+(expanded?' ab-p-field-expanded':'')+'" onclick="abExpandField(\''+f.col+'\',event)" style="cursor:pointer;flex-wrap:wrap;">';
    h+='<div class="ab-p-fcheck '+(f.on?'on':'')+'" onclick="abToggleField(\''+f.col+'\',this);event.stopPropagation();">✓</div>';
    h+='<span class="ab-p-fname">'+f.label+'<span class="ab-p-fcol">'+f.col+'</span></span>';
    h+='<span class="ab-p-ftype">'+f.type+'</span>';
    /* FieldConfig 배지 표시 */
    let badges='';
    if(f.pk) badges+='<span class="ab-fc-badge pk">PK</span>';
    if(f.required) badges+='<span class="ab-fc-badge req">필수</span>';
    if(f.searchable) badges+='<span class="ab-fc-badge sch">검색</span>';
    if(f.system) badges+='<span class="ab-fc-badge sys">SYS</span>';
    if(f.computed) badges+='<span class="ab-fc-badge cmp">계산</span>';
    if(badges) h+='<div style="display:flex;gap:2px;margin-left:auto;">'+badges+'</div>';
    h+='<span class="ab-p-fdrag">⋮</span>';
    h+='</div>';
    /* 펼쳐진 상태면 FieldConfig 상세 패널 표시 */
    if(expanded) h+=fieldDetailPanel(f);
  });
  return h+'</div>';
}

/* 필드 클릭 → 상세 펼침/접힘 */
function abExpandField(col, evt){
  if(evt && evt.target.closest('.ab-p-fcheck')) return;
  if(evt) evt.stopPropagation();
  _abExpandedField = (_abExpandedField===col) ? null : col;
  abUpdateProps();
}

/* 필드 상세 편집 패널 */
function fieldDetailPanel(f){
  let h='<div class="ab-fc-detail" onclick="event.stopPropagation();">';
  h+='<div class="ab-fc-row"><span class="ab-fc-label">DB 컬럼명</span><input class="ab-fc-val" value="'+esc(f.col)+'" readonly></div>';
  h+='<div class="ab-fc-row"><span class="ab-fc-label">표시 이름</span><input class="ab-fc-val" value="'+esc(f.label)+'" onchange="abSetFieldProp(\''+f.col+'\',\'label\',this.value)"></div>';
  h+='<div class="ab-fc-row"><span class="ab-fc-label">입력 방식</span><select class="ab-fc-val" onchange="abSetFieldProp(\''+f.col+'\',\'type\',this.value)">';
  AB_FIELD_TYPES.forEach(t=>{ h+='<option'+(t===f.type?' selected':'')+'>'+t+'</option>'; });
  h+='</select></div>';
  h+='<div class="ab-fc-toggles">';
  h+=fcToggle('보이기', f.on!==false, f.col, 'on');
  h+=fcToggle('필수', !!f.required, f.col, 'required');
  h+=fcToggle('수정 가능', f.editable!==false, f.col, 'editable');
  h+=fcToggle('검색 가능', !!f.searchable, f.col, 'searchable');
  h+=fcToggle('정렬 가능', f.sortable!==false, f.col, 'sortable');
  h+='</div>';
  h+='<div class="ab-fc-toggles">';
  h+=fcToggle('기본키', !!f.pk, f.col, 'pk');
  h+=fcToggle('시스템 필드', !!f.system, f.col, 'system');
  h+='</div>';
  /* 선택지 (드롭다운용) */
  h+='<div class="ab-fc-row"><span class="ab-fc-label">선택지</span><input class="ab-fc-val" value="'+esc((f.options||[]).join(', '))+'" placeholder="쉼표로 구분 (예: 진행중, 완료, 취소)" onchange="abSetFieldOptions(\''+f.col+'\',this.value)"></div>';
  /* 참조 테이블 (엔티티용) */
  h+='<div class="ab-fc-row"><span class="ab-fc-label">참조 테이블</span><input class="ab-fc-val" value="'+esc(f.ref?.table||'')+'" placeholder="예: customer_mng" onchange="abSetFieldRef(\''+f.col+'\',\'table\',this.value)"></div>';
  h+='<div class="ab-fc-row"><span class="ab-fc-label">표시할 컬럼</span><input class="ab-fc-val" value="'+esc(f.ref?.display||f.ref?.displayColumn||'')+'" placeholder="예: customer_name" onchange="abSetFieldRef(\''+f.col+'\',\'display\',this.value)"></div>';
  /* 표시 형식 */
  h+='<div class="ab-fc-row"><span class="ab-fc-label">표시 형식</span><input class="ab-fc-val" value="'+esc(f.format||'')+'" placeholder="숫자: #,##0 / 날짜: YYYY-MM-DD" onchange="abSetFieldProp(\''+f.col+'\',\'format\',this.value)"></div>';
  /* 자동 계산 */
  h+='<div class="ab-fc-row"><span class="ab-fc-label">자동 계산식</span><input class="ab-fc-val" value="'+esc(f.computed||'')+'" placeholder="예: 수량 * 단가" onchange="abSetFieldProp(\''+f.col+'\',\'computed\',this.value)"></div>';
  /* 컬럼 너비 / 정렬 */
  h+='<div class="ab-fc-row"><span class="ab-fc-label">컬럼 너비</span><input class="ab-fc-val" type="number" value="'+(f.width||'')+'" placeholder="자동" onchange="abSetFieldProp(\''+f.col+'\',\'width\',parseInt(this.value)||undefined)"></div>';
  h+='<div class="ab-fc-row"><span class="ab-fc-label">텍스트 정렬</span><select class="ab-fc-val" onchange="abSetFieldProp(\''+f.col+'\',\'align\',this.value)">';
  [['left','왼쪽'],['center','가운데'],['right','오른쪽']].forEach(([v,l])=>{ h+='<option value="'+v+'"'+(v===(f.align||'left')?' selected':'')+'>'+l+'</option>'; });
  h+='</select></div>';
  h+='</div>';
  return h;
}

function fcToggle(label, on, col, prop){
  return '<div class="ab-fc-tog" onclick="abToggleFieldProp(\''+col+'\',\''+prop+'\',this)">'
    +'<div class="ab-p-toggle '+(on?'on':'')+'"></div>'
    +'<span class="ab-fc-tog-label">'+label+'</span></div>';
}

/* FieldConfig 속성 변경 핸들러 */
function abSetFieldProp(col, key, val){
  const f=_abFields.find(x=>x.col===col);
  if(!f) return;
  f[key]=val;
  abRefreshBlockContent();
}

function abSetFieldOptions(col, val){
  const f=_abFields.find(x=>x.col===col);
  if(!f) return;
  f.options = val ? val.split(',').map(s=>s.trim()).filter(s=>s) : undefined;
  abRefreshBlockContent();
}

function abSetFieldRef(col, key, val){
  const f=_abFields.find(x=>x.col===col);
  if(!f) return;
  if(!f.ref) f.ref={};
  f.ref[key]=val||undefined;
}

function abToggleFieldProp(col, prop, el){
  const f=_abFields.find(x=>x.col===col);
  if(!f) return;
  if(prop==='on') f.on=!f.on;
  else f[prop]=!f[prop];
  /* 토글 UI 업데이트 */
  const toggle = el.querySelector('.ab-p-toggle');
  if(toggle) toggle.classList.toggle('on');
  abRefreshBlockContent();
}

/* ═══ 속성 유틸 ═══ */
function pRow(l,v,ro){ return '<div class="ab-p-row"><div class="ab-p-label">'+l+'</div><input class="ab-p-val" value="'+(v||'')+'" '+(ro?'readonly':'')+' ></div>'; }
function pSel(l,v,opts){ let h='<div class="ab-p-row"><div class="ab-p-label">'+l+'</div><select class="ab-p-val">'; opts.forEach(o=>{h+='<option'+(o===v?' selected':'')+'>'+o+'</option>';}); return h+'</select></div>'; }
function pTog(l,on){ return '<div class="ab-p-row" style="flex-direction:row;align-items:center;justify-content:space-between;"><div class="ab-p-label">'+l+'</div><div class="ab-p-toggle '+(on?'on':'')+'" onclick="this.classList.toggle(\'on\')"></div></div>'; }
function pSm(l,v,k){ return '<div class="ab-p-sm"><span class="ab-p-sml">'+l+'</span><input class="ab-p-smv" value="'+v+'" onchange="abSetPos(\''+k+'\',this.value)"></div>'; }
function tLabel(t){ return {'table':'데이터 테이블','search':'검색 필터','form':'입력 폼','text-display':'텍스트/제목','button':'버튼','btn-bar':'버튼 바','tabs':'탭','pagi':'페이지네이션'}[t]||t; }
function esc(s){ return String(s).replace(/"/g,'&quot;'); }

/* ═══ 캔버스 관리 ═══ */
function abClearCanvas(){
  const cv=document.getElementById('ab-canvas');
  cv.querySelectorAll('.ab-block').forEach(b=>b.remove());
  document.getElementById('ab-empty').style.display='';
  document.getElementById('ab-popup-overlay').classList.remove('show');
  _abBlocks=[]; _abSelectedBlock=null; _abBlockId=0;
  _abConnections=[]; _abExpandedField=null;
  _abViewBlocks={list:[],create:[],edit:[]};
  _abView='list';
  document.querySelectorAll('.ab-view-tab').forEach(t=>t.classList.toggle('active',t.dataset.view==='list'));
  abClearSel(); abUpdateStatus();
}

function abRenderBlocks(blocks){
  const cv=document.getElementById('ab-canvas');
  cv.querySelectorAll('.ab-block').forEach(b=>b.remove());
  _abBlocks=[];
  if(!blocks.length){ document.getElementById('ab-empty').style.display=''; return; }
  document.getElementById('ab-empty').style.display='none';
  blocks.forEach(b=>{ _abBlocks.push(b); _renderOneBlock(cv,b); });
}

/* 팝업 뷰 — 블록을 popup-frame 안에 렌더 (편집 가능!) */
function abRenderPopupBlocks(blocks,vt){
  const frame=document.getElementById('ab-popup-frame');
  frame.innerHTML='';
  frame.style.position='relative'; frame.style.minHeight='400px';
  const label=vt==='create'?'등록 팝업':'수정 팝업';
  frame.innerHTML='<div class="ab-popup-label">'+label+'</div>';
  if(!blocks.length) return;
  _abBlocks=[];
  blocks.forEach(b=>{ _abBlocks.push(b); _renderOneBlock(frame,b); });
}

function abUpdateStatus(){
  const el=document.getElementById('ab-status-l');
  if(!el) return;
  let text='컴포넌트: '+_abBlocks.length+'개';
  if(_abTable) text+=' | '+_abTable;
  if(_abConnections.length){
    text+=' | DataPort: '+_abConnections.length+'개 연결';
    /* 간략 연결 표시 */
    const summary = _abConnections.map(c=>{
      const fb=_abBlocks.find(x=>x.id===c.from.componentId);
      const tb=_abBlocks.find(x=>x.id===c.to.componentId);
      return (fb?fb.label:'?')+'.'+c.from.port+'→'+(tb?tb.label:'?')+'.'+c.to.port;
    }).join(', ');
    text+=' ('+summary+')';
  }
  el.textContent=text;
}

/* ═══ 프리뷰 HTML (범용) ═══ */
function pvTitle(L){ return '<div style="font-size:.75rem;font-weight:800;padding:.5rem;color:var(--text);">'+L+'</div>'; }
function pvBtnBar(L){
  return '<div style="display:flex;gap:.2rem;justify-content:flex-end;padding:.35rem .5rem;">'+
    '<button class="ab-action-btn" style="padding:.2rem .5rem;border-radius:4px;background:var(--primary);color:#fff;border:none;font-size:.46rem;font-weight:700;cursor:pointer;" onclick="abSwitchView(\'create\')">+ '+L+' 등록</button>'+
    '<button class="ab-action-btn" style="padding:.2rem .5rem;border-radius:4px;background:transparent;color:var(--text-muted);border:1px solid var(--border);font-size:.46rem;cursor:pointer;">엑셀</button></div>';
}
function pvSearch(S){
  return '<div style="display:flex;gap:.3rem;padding:.4rem .5rem;align-items:center;">'+
    '<div style="flex:1;padding:.22rem .4rem;border-radius:4px;border:1px solid var(--border);background:var(--surface);font-size:.48rem;color:var(--text-muted);">'+S.map(f=>f.label).join(' / ')+' 검색...</div>'+
    '<button style="padding:.22rem .5rem;border-radius:4px;background:var(--primary);color:#fff;border:none;font-size:.46rem;font-weight:600;">🔍 검색</button></div>';
}
function pvTable(F){
  let h='<div style="padding:.4rem;overflow:hidden;"><table style="width:100%;border-collapse:collapse;font-size:.48rem;"><thead><tr>';
  F.forEach(f=>h+='<th style="text-align:left;padding:.22rem .35rem;font-weight:700;color:var(--text-muted);border-bottom:1px solid var(--border);font-size:.42rem;">'+f.label+'</th>');
  h+='</tr></thead><tbody>';
  for(let i=0;i<5;i++){
    h+='<tr>';
    F.forEach(f=>{
      let v='';
      if(f.type==='numbering') v='AUTO-'+(1000+i);
      else if(f.type==='date') v='2026-04-0'+(i+1);
      else if(f.type==='number') v=(Math.floor(Math.random()*900)+100).toLocaleString();
      else if(f.type==='category'&&f.options) v=f.options[i%f.options.length];
      else v=f.label+String.fromCharCode(65+i);
      h+='<td style="padding:.22rem .35rem;border-bottom:1px dashed var(--border);color:var(--text-muted);">'+v+'</td>';
    });
    h+='</tr>';
  }
  return h+'</tbody></table></div>';
}
function pvPagi(){ return '<div style="display:flex;justify-content:space-between;padding:.3rem .5rem;font-size:.42rem;color:var(--text-muted);">표시: 20 | 총 0건<span>◀ 1/1 ▶</span></div>'; }
function pvTabsHtml(){ return '<div style="display:flex;border-bottom:2px solid var(--border);"><div style="padding:.3rem .7rem;font-size:.52rem;font-weight:700;border-bottom:2px solid var(--primary);color:var(--text);margin-bottom:-2px;">전체 목록</div><div style="padding:.3rem .7rem;font-size:.52rem;color:var(--text-muted);">상태별</div><div style="padding:.3rem .7rem;font-size:.52rem;color:var(--text-muted);">통계</div></div>'; }
function pvForm(title,F){
  let h='<div style="padding:.5rem;overflow-y:auto;"><div style="font-size:.6rem;font-weight:700;color:var(--text);margin-bottom:.4rem;">'+title+'</div>';
  F.filter(f=>!f.system).forEach(f=>{
    let inp='';
    const s='padding:.2rem .35rem;border-radius:4px;border:1px solid var(--border);background:var(--surface);font-size:.48rem;color:var(--text-muted);';
    if(f.type==='textarea') inp='<div style="'+s+'height:40px;"></div>';
    else if(f.type==='category'&&f.options) inp='<div style="'+s+'">'+f.options[0]+' ▼</div>';
    else if(f.type==='entity') inp='<div style="'+s+'">🔍 검색...</div>';
    else if(f.type==='date') inp='<div style="'+s+'">📅 날짜 선택</div>';
    else if(f.type==='numbering') inp='<div style="'+s+'opacity:.6;">자동 생성</div>';
    else inp='<div style="'+s+'"></div>';
    h+='<div style="display:flex;flex-direction:column;gap:.1rem;margin-bottom:.3rem;"><div style="font-size:.42rem;font-weight:700;color:var(--text-muted);">'+f.label+(f.required?'<span style="color:var(--danger,#f87171);margin-left:2px;">*</span>':'')+'</div>'+inp+'</div>';
  });
  h+='</div>';
  return h;
}

/* ═══ 팔레트 → 캔버스 드래그앤드롭 ═══ */

/* 팔레트 아이템별 정확한 타입 + 크기 + 프리뷰 매핑 */
/* 팔레트 컴포넌트 매핑 — FieldConfig를 소비하는 컴포넌트만 */
/* 레이아웃 = 프리셋이 처리 (내부 모델에 존재, 팔레트에서 숨김) */
/* 필드 = FieldConfig.type (컴포넌트 아님) */
/* 개별 액션 = ActionType (버튼 바 속성) */
const PAL_COMP_MAP = {
  /* 데이터 — FieldConfig[]를 소비하는 핵심 컴포넌트 */
  '데이터 테이블':{type:'table',   w:600, h:280, preview: _pvTableDrop},
  '검색 필터':    {type:'search',  w:600, h:42,  preview: _pvSearchDrop},
  '입력 폼':      {type:'form',    w:400, h:250, preview: _pvFormDrop},
  /* 액션 — 개별 버튼 (자유 배치, ActionType 설정) */
  '버튼':         {type:'button',  w:120, h:36,  preview: _pvButtonDrop},
  /* 표시 */
  '통계 카드':    {type:'text-display', w:500, h:80, preview: _pvStatsCard},
  '차트':         {type:'text-display', w:400, h:200, preview: _pvChart},
  '텍스트/제목':  {type:'text-display', w:300, h:36, preview: _pvTitleDrop},
  '구분선':       {type:'text-display', w:600, h:8,  preview: _pvDivider},
};

/* ─── 각 타입별 프리뷰 함수 ─── */
function _pvGrid(){
  const s='border:1px dashed var(--border);border-radius:3px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:.42rem;';
  return '<div style="display:grid;grid-template-columns:repeat(12,1fr);gap:2px;padding:6px;height:100%;">'+
    Array(12).fill(0).map((_,i)=>'<div style="'+s+'background:var(--surface);opacity:.5;">'+(i+1)+'</div>').join('')+
    '</div>';
}
function _pvSplitPanel(){
  const s='border:1px dashed var(--border);border-radius:4px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:.46rem;';
  return '<div style="display:flex;gap:4px;padding:6px;height:100%;">'+
    '<div style="flex:1;'+s+'background:var(--surface);">좌측 패널</div>'+
    '<div style="width:3px;background:var(--border);border-radius:2px;cursor:col-resize;"></div>'+
    '<div style="flex:1;'+s+'background:var(--surface);">우측 패널</div></div>';
}
function _pvTabsDrop(){
  return '<div style="display:flex;border-bottom:2px solid var(--border);padding:0 4px;">'+
    '<div style="padding:5px 12px;font-size:.48rem;font-weight:700;border-bottom:2px solid var(--primary);color:var(--text);margin-bottom:-2px;">탭 1</div>'+
    '<div style="padding:5px 12px;font-size:.48rem;color:var(--text-muted);">탭 2</div>'+
    '<div style="padding:5px 12px;font-size:.48rem;color:var(--text-muted);">탭 3</div></div>';
}
function _pvCard(){
  return '<div style="padding:8px;height:100%;border:1px solid var(--border);border-radius:6px;background:var(--surface);">'+
    '<div style="font-size:.5rem;font-weight:700;color:var(--text);margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid var(--border);">섹션 제목</div>'+
    '<div style="color:var(--text-muted);font-size:.44rem;">컴포넌트를 이 안에 배치하세요</div></div>';
}
function _pvModal(){
  return '<div style="padding:8px;height:100%;display:flex;flex-direction:column;">'+
    '<div style="display:flex;justify-content:space-between;align-items:center;padding-bottom:6px;border-bottom:1px solid var(--border);margin-bottom:6px;">'+
      '<span style="font-size:.55rem;font-weight:700;color:var(--text);">모달 제목</span>'+
      '<span style="color:var(--text-muted);font-size:.6rem;cursor:pointer;">✕</span></div>'+
    '<div style="flex:1;border:1px dashed var(--border);border-radius:4px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:.44rem;">모달 내용 영역</div>'+
    '<div style="display:flex;justify-content:flex-end;gap:4px;margin-top:6px;">'+
      '<div style="padding:3px 10px;border-radius:4px;border:1px solid var(--border);font-size:.42rem;color:var(--text-muted);">취소</div>'+
      '<div style="padding:3px 10px;border-radius:4px;background:var(--primary);color:#fff;font-size:.42rem;">확인</div></div></div>';
}
function _pvTableDrop(){
  if(!_abFields.length){
    return '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:.5rem;">'+
      '먼저 테이블을 선택하세요</div>';
  }
  return pvTable(_abFields.filter(f=>f.on).slice(0,8));
}
function _pvSearchDrop(){
  if(!_abFields.length){
    return '<div style="display:flex;gap:4px;padding:6px;align-items:center;">'+
      '<div style="flex:1;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--surface);font-size:.46rem;color:var(--text-muted);">테이블을 선택하면 검색 조건이 표시됩니다</div>'+
      '<div style="padding:4px 10px;border-radius:4px;background:var(--primary);color:#fff;font-size:.44rem;font-weight:600;">🔍 검색</div></div>';
  }
  const S=_abFields.filter(f=>f.searchable);
  return S.length?pvSearch(S):pvSearch([{label:'검색 조건 없음'}]);
}
function _pvFormDrop(){
  if(!_abFields.length){
    return '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:.5rem;padding:12px;text-align:center;">'+
      '먼저 테이블을 선택하세요<br><span style="font-size:.42rem;opacity:.6;">필드가 자동으로 표시됩니다</span></div>';
  }
  const F=_abFields.filter(f=>f.on&&!f.system);
  const t=AB_TABLES.find(x=>x.name===_abTable);
  return pvForm(t?t.label+' 입력':'입력 폼',F);
}
function _pvMasterDetail(){
  return '<div style="display:flex;flex-direction:column;gap:4px;padding:6px;height:100%;">'+
    '<div style="flex:0 0 40%;border:1px dashed var(--border);border-radius:4px;padding:6px;display:flex;flex-direction:column;">'+
      '<div style="font-size:.46rem;font-weight:700;color:var(--text-muted);margin-bottom:4px;">마스터 (상위)</div>'+
      '<div style="flex:1;background:var(--surface);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:.4rem;color:var(--text-muted);">폼 필드 영역</div></div>'+
    '<div style="flex:1;border:1px dashed var(--border);border-radius:4px;padding:6px;display:flex;flex-direction:column;">'+
      '<div style="font-size:.46rem;font-weight:700;color:var(--text-muted);margin-bottom:4px;">디테일 (하위 목록)</div>'+
      '<div style="flex:1;background:var(--surface);border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:.4rem;color:var(--text-muted);">테이블 영역</div></div></div>';
}
function _pvButtonDrop(){
  return '<div style="display:flex;align-items:center;justify-content:center;height:100%;width:100%;'+
    'border-radius:4px;background:var(--primary);color:#fff;font-size:.5rem;font-weight:700;cursor:pointer;">저장</div>';
}

function _pvBtnBarDrop(){
  const t=_abTable&&AB_TABLES.find(x=>x.name===_abTable);
  const label=t?t.label:'항목';
  return '<div style="display:flex;gap:4px;justify-content:flex-end;padding:4px 8px;height:100%;align-items:center;">'+
    '<div style="padding:3px 10px;border-radius:4px;background:var(--primary);color:#fff;font-size:.44rem;font-weight:700;">+ '+label+' 등록</div>'+
    '<div style="padding:3px 10px;border-radius:4px;border:1px solid var(--border);color:var(--text-muted);font-size:.44rem;">삭제</div>'+
    '<div style="padding:3px 10px;border-radius:4px;border:1px solid var(--border);color:var(--text-muted);font-size:.44rem;">엑셀</div></div>';
}

function _pvFieldInput(placeholder, ftype){
  const colors={text:'var(--success,#4ade80)',number:'var(--success,#4ade80)',date:'var(--success,#4ade80)',
    select:'var(--success,#4ade80)',entity:'var(--success,#4ade80)',checkbox:'var(--success,#4ade80)',
    file:'var(--success,#4ade80)',textarea:'var(--success,#4ade80)'};
  return function(){
    return '<div style="padding:4px 8px;height:100%;display:flex;flex-direction:column;justify-content:center;">'+
      '<div style="font-size:.4rem;font-weight:700;color:var(--text-muted);margin-bottom:2px;">필드 라벨</div>'+
      '<div style="padding:3px 6px;border-radius:3px;border:1px solid var(--border);background:var(--surface);font-size:.44rem;color:var(--text-muted);">'+placeholder+'</div></div>';
  };
}
function _pvBtn(text, variant){
  return function(){
    const bg=variant==='primary'?'var(--primary)':variant==='danger'?'var(--danger,#f87171)':'var(--surface)';
    const color=variant==='default'?'var(--text-muted)':'#fff';
    const border=variant==='default'?'1px solid var(--border)':'none';
    return '<div style="display:flex;align-items:center;justify-content:center;height:100%;">'+
      '<div style="padding:4px 12px;border-radius:4px;background:'+bg+';color:'+color+';border:'+border+';font-size:.46rem;font-weight:600;">'+text+'</div></div>';
  };
}
function _pvStatsCard(){
  return '<div style="display:flex;gap:6px;padding:6px;height:100%;">'+
    ['전체','활성','대기','완료'].map((l,i)=>{
      const colors=['var(--primary)','var(--success,#4ade80)','var(--warning,#fb923c)','var(--text-muted)'];
      return '<div style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--surface);">'+
        '<div style="font-size:.38rem;color:var(--text-muted);">'+l+'</div>'+
        '<div style="font-size:.7rem;font-weight:800;color:'+colors[i]+';">'+(Math.floor(Math.random()*50)+10)+'</div></div>';
    }).join('')+'</div>';
}
function _pvChart(){
  const bars=[60,80,45,90,70,55,85];
  return '<div style="padding:8px;height:100%;display:flex;flex-direction:column;">'+
    '<div style="font-size:.46rem;font-weight:700;color:var(--text);margin-bottom:6px;">차트</div>'+
    '<div style="flex:1;display:flex;align-items:flex-end;gap:4px;padding:4px;">'+
    bars.map(h=>'<div style="flex:1;background:var(--primary);border-radius:2px 2px 0 0;height:'+h+'%;opacity:.6;"></div>').join('')+
    '</div></div>';
}
function _pvTitleDrop(){
  return pvTitle(_abTable&&AB_TABLES.find(x=>x.name===_abTable)?AB_TABLES.find(x=>x.name===_abTable).label:'제목');
}
function _pvDivider(){
  return '<div style="display:flex;align-items:center;height:100%;padding:0 8px;">'+
    '<div style="flex:1;height:1px;background:var(--border);"></div></div>';
}

document.addEventListener('DOMContentLoaded',function(){
  document.querySelectorAll('.ab-pal-item[draggable="true"]').forEach(item=>{
    item.addEventListener('dragstart',function(e){
      const label=item.dataset.comp||item.textContent.trim();
      const cat=item.dataset.cat||'data';
      e.dataTransfer.setData('text/plain',JSON.stringify({cat,label}));
      e.dataTransfer.effectAllowed='copy';
    });
  });
  const wrap=document.getElementById('ab-canvas-wrap');
  if(!wrap) return;
  wrap.addEventListener('dragover',function(e){ e.preventDefault(); e.dataTransfer.dropEffect='copy'; });
  wrap.addEventListener('drop',function(e){
    e.preventDefault();
    try{
      const d=JSON.parse(e.dataTransfer.getData('text/plain'));
      const cv=document.getElementById('ab-canvas');
      const r=cv.getBoundingClientRect();
      const scrollX=cv.parentElement.scrollLeft||0;
      const scrollY=cv.parentElement.scrollTop||0;
      const x=Math.round((Math.max(0,e.clientX-r.left+scrollX-20))/20)*20;
      const y=Math.round((Math.max(0,e.clientY-r.top+scrollY-10))/20)*20;

      const mapping=PAL_COMP_MAP[d.label];
      if(!mapping){ toast('알 수 없는 컴포넌트','warn'); return; }

      const preview=typeof mapping.preview==='function'?mapping.preview():(mapping.preview||'');
      document.getElementById('ab-empty').style.display='none';
      _addBlk(cv,mapping.type,d.label,x,y,mapping.w,mapping.h,preview);
      abUpdateStatus();
      toast(d.label+' 배치','info');
    }catch(ex){ console.error(ex); }
  });
});

/* ═══════════════════════════════════════════════════════════════════════════
   ★ 테이블 설정 모달 — VEX 스타일
   3탭: 컬럼 설정 / 필터 설정 / 그룹 설정
   ═══════════════════════════════════════════════════════════════════════════ */

let _abTblModalTab = 'columns'; /* columns | filters | groups */
let _abTblModalSnapshot = null; /* 원본 스냅샷 (취소 시 복원) */
let _abTblModalDefaultFilters = []; /* 기본 데이터 필터 조건 */
let _abTblModalGroupFields = [];   /* 그룹화 선택된 컬럼 col[] */
let _abTblModalSubtotal = false;   /* 소계 합산 여부 */

/* ─── 모달 열기 ─── */
function abTblModalOpen(){
  if(!_abFields.length){toast('테이블을 먼저 선택하세요','warn');return;}
  /* 필드 상태 스냅샷 (취소 시 복원) */
  _abTblModalSnapshot = JSON.parse(JSON.stringify(_abFields));
  _abTblModalTab = 'columns';
  /* 탭 초기화 */
  document.querySelectorAll('.ab-tbl-modal-tab').forEach(t=>t.classList.toggle('active',t.dataset.tab==='columns'));
  /* 모달 표시 */
  document.getElementById('ab-tbl-modal-backdrop').classList.add('show');
  document.getElementById('ab-tbl-modal').classList.add('show');
  abTblModalRender();
}

/* ─── 모달 닫기 ─── */
function abTblModalClose(){
  document.getElementById('ab-tbl-modal-backdrop').classList.remove('show');
  document.getElementById('ab-tbl-modal').classList.remove('show');
  /* 취소면 스냅샷 복원 */
  if(_abTblModalSnapshot){
    _abFields = _abTblModalSnapshot;
    _abTblModalSnapshot = null;
  }
}

/* ─── 모달 저장 ─── */
function abTblModalSave(){
  _abTblModalSnapshot = null; /* 스냅샷 무효화 (저장이니까 복원 안 함) */
  document.getElementById('ab-tbl-modal-backdrop').classList.remove('show');
  document.getElementById('ab-tbl-modal').classList.remove('show');
  /* 캔버스 프리뷰 갱신 */
  abRefreshBlockContent();
  /* 속성 패널 갱신 */
  if(_abSelectedBlock) abUpdateProps();
  toast('테이블 설정이 저장되었습니다','success');
}

/* ─── 탭 전환 ─── */
function abTblModalSwitchTab(tab,el){
  _abTblModalTab = tab;
  document.querySelectorAll('.ab-tbl-modal-tab').forEach(t=>t.classList.toggle('active',t.dataset.tab===tab));
  abTblModalRender();
}

/* ─── 본문 렌더 ─── */
function abTblModalRender(){
  const body = document.getElementById('ab-tbl-modal-body');
  if(!body) return;
  if(_abTblModalTab==='columns') body.innerHTML = abTblModalColumnsHtml();
  else if(_abTblModalTab==='filters') body.innerHTML = abTblModalFiltersHtml();
  else if(_abTblModalTab==='groups') body.innerHTML = abTblModalGroupsHtml();
  /* 컬럼 탭 드래그 초기화 */
  if(_abTblModalTab==='columns') abTblModalInitDrag();
}

/* ═══ 탭 1: 컬럼 설정 ═══ */
function abTblModalColumnsHtml(){
  const onCount = _abFields.filter(f=>f.on).length;
  const total = _abFields.length;
  const hiddenCount = total - onCount;
  let h = '';
  /* 카운터 */
  h += '<div class="ab-tbl-counter">';
  h += '<span class="ab-tbl-counter-num">'+onCount+'/'+total+'</span>개 컬럼 표시 중';
  if(hiddenCount>0) h += ' · <span style="color:var(--text-muted);">숨김: '+hiddenCount+'개 컬럼</span>';
  h += '</div>';
  /* 컬럼 행들 */
  _abFields.forEach((f,i)=>{
    h += '<div class="ab-tbl-col-row" data-idx="'+i+'" draggable="true">';
    h += '<span class="ab-tbl-drag-handle">⋮</span>';
    h += '<div class="ab-tbl-toggle '+(f.on?'on':'off')+'" onclick="abTblModalToggleCol('+i+',this)"></div>';
    h += '<span class="ab-tbl-col-label">'+esc(f.label)+'</span>';
    h += '<span class="ab-tbl-col-db">'+esc(f.col)+'</span>';
    h += '<div class="ab-tbl-col-width">';
    h += '<select onchange="abTblModalSetWidth('+i+',this.value)">';
    h += '<option value="auto"'+((!f.width||f.width==='auto')?' selected':'')+'>\uC790\uB3D9</option>';
    h += '<option value="custom"'+((f.width&&f.width!=='auto')?' selected':'')+'>\uC9C1\uC811\uC785\uB825</option>';
    h += '</select>';
    if(f.width && f.width!=='auto'){
      h += '<input type="number" value="'+(typeof f.width==='number'?f.width:100)+'" onchange="abTblModalSetWidthVal('+i+',this.value)" style="width:50px;">';
    }
    h += '</div>';
    h += '</div>';
  });
  return h;
}

function abTblModalToggleCol(idx, el){
  _abFields[idx].on = !_abFields[idx].on;
  el.classList.toggle('on');
  el.classList.toggle('off');
  /* 카운터 업데이트 */
  abTblModalRender();
  /* 실시간 프리뷰 갱신 */
  abRefreshBlockContent();
}

function abTblModalSetWidth(idx, val){
  if(val==='auto'){
    _abFields[idx].width = undefined;
  } else {
    _abFields[idx].width = _abFields[idx].width || 100;
  }
  abTblModalRender();
}

function abTblModalSetWidthVal(idx, val){
  _abFields[idx].width = parseInt(val) || undefined;
}

/* 드래그 순서 변경 */
function abTblModalInitDrag(){
  const rows = document.querySelectorAll('.ab-tbl-col-row[draggable]');
  let dragIdx = null;
  rows.forEach(row=>{
    row.addEventListener('dragstart',function(e){
      dragIdx = parseInt(row.dataset.idx);
      row.classList.add('dragging');
      e.dataTransfer.effectAllowed='move';
      e.dataTransfer.setData('text/plain',dragIdx);
    });
    row.addEventListener('dragend',function(){
      row.classList.remove('dragging');
      document.querySelectorAll('.ab-tbl-col-row.drag-over').forEach(r=>r.classList.remove('drag-over'));
    });
    row.addEventListener('dragover',function(e){
      e.preventDefault();
      e.dataTransfer.dropEffect='move';
      document.querySelectorAll('.ab-tbl-col-row.drag-over').forEach(r=>r.classList.remove('drag-over'));
      row.classList.add('drag-over');
    });
    row.addEventListener('dragleave',function(){
      row.classList.remove('drag-over');
    });
    row.addEventListener('drop',function(e){
      e.preventDefault();
      row.classList.remove('drag-over');
      const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
      const toIdx = parseInt(row.dataset.idx);
      if(fromIdx===toIdx || isNaN(fromIdx) || isNaN(toIdx)) return;
      /* 배열 재정렬 */
      const item = _abFields.splice(fromIdx,1)[0];
      _abFields.splice(toIdx,0,item);
      abTblModalRender();
      abRefreshBlockContent();
    });
  });
}

/* ═══ 탭 2: 필터 설정 ═══ */
function abTblModalFiltersHtml(){
  const activeCount = _abFields.filter(f=>f.searchable).length;
  const total = _abFields.length;
  let h = '';
  /* 카운터 */
  h += '<div class="ab-tbl-counter">';
  h += '<span class="ab-tbl-counter-num">'+activeCount+'/'+total+'</span>개 활성';
  h += '</div>';
  /* 필드 행 */
  _abFields.forEach((f,i)=>{
    const filterType = f._filterType || _guessFilterType(f);
    h += '<div class="ab-tbl-col-row">';
    h += '<div class="ab-tbl-checkbox '+(f.searchable?'on':'')+'" onclick="abTblModalToggleFilter('+i+',this)">'+'\u2713'+'</div>';
    h += '<span class="ab-tbl-col-label">'+esc(f.label)+'</span>';
    h += '<select class="ab-tbl-filter-type" onchange="abTblModalSetFilterType('+i+',this.value)">';
    ['텍스트','숫자','날짜','셀렉트'].forEach(ft=>{
      h += '<option'+(ft===filterType?' selected':'')+'>'+ft+'</option>';
    });
    h += '</select>';
    h += '</div>';
  });
  /* 기본 데이터 필터 섹션 */
  h += '<div class="ab-tbl-default-filter">';
  h += '<div class="ab-tbl-df-title">기본 데이터 필터</div>';
  h += '<div style="font-size:.4rem;color:var(--text-muted);margin-bottom:.3rem;">항상 적용되는 필터 조건을 설정합니다</div>';
  _abTblModalDefaultFilters.forEach((df,di)=>{
    h += '<div class="ab-tbl-df-row">';
    h += '<select onchange="abTblModalSetDefaultFilter('+di+',\'col\',this.value)">';
    h += '<option value="">컬럼 선택</option>';
    _abFields.forEach(f=>{
      h += '<option value="'+esc(f.col)+'"'+(df.col===f.col?' selected':'')+'>'+esc(f.label)+'</option>';
    });
    h += '</select>';
    h += '<input placeholder="필터 값" value="'+esc(df.value||'')+'" onchange="abTblModalSetDefaultFilter('+di+',\'value\',this.value)">';
    h += '<button style="flex-shrink:0;width:24px;height:24px;border-radius:4px;border:1px solid var(--border);background:transparent;color:var(--text-muted);cursor:pointer;font-size:.5rem;" onclick="abTblModalRemoveDefaultFilter('+di+')">✕</button>';
    h += '</div>';
  });
  h += '<button class="ab-tbl-df-add" onclick="abTblModalAddDefaultFilter()">+ 필터 조건 추가</button>';
  h += '</div>';
  return h;
}

function _guessFilterType(f){
  if(f.type==='number') return '숫자';
  if(f.type==='date'||f.type==='datetime') return '날짜';
  if(f.type==='category'||f.type==='select'||f.type==='entity') return '셀렉트';
  return '텍스트';
}

function abTblModalToggleFilter(idx, el){
  _abFields[idx].searchable = !_abFields[idx].searchable;
  el.classList.toggle('on');
  /* 카운터 업데이트 */
  abTblModalRender();
  /* 실시간: 검색 블록 갱신 */
  abRefreshBlockContent();
}

function abTblModalSetFilterType(idx, val){
  _abFields[idx]._filterType = val;
}

function abTblModalAddDefaultFilter(){
  _abTblModalDefaultFilters.push({col:'',value:''});
  abTblModalRender();
}

function abTblModalRemoveDefaultFilter(idx){
  _abTblModalDefaultFilters.splice(idx,1);
  abTblModalRender();
}

function abTblModalSetDefaultFilter(idx, key, val){
  _abTblModalDefaultFilters[idx][key] = val;
}

/* ═══ 탭 3: 그룹 설정 ═══ */
function abTblModalGroupsHtml(){
  const activeCount = _abTblModalGroupFields.length;
  const total = _abFields.length;
  let h = '';
  /* 카운터 */
  h += '<div class="ab-tbl-counter">';
  h += '<span class="ab-tbl-counter-num">'+activeCount+'/'+total+'</span>개 그룹화';
  h += '</div>';
  /* 필드 행 */
  _abFields.forEach((f,i)=>{
    const checked = _abTblModalGroupFields.includes(f.col);
    h += '<div class="ab-tbl-col-row">';
    h += '<div class="ab-tbl-checkbox '+(checked?'on':'')+'" onclick="abTblModalToggleGroup(\''+esc(f.col)+'\',this)">'+'\u2713'+'</div>';
    h += '<span class="ab-tbl-col-label">'+esc(f.label)+'</span>';
    h += '<span class="ab-tbl-col-db">'+esc(f.col)+'</span>';
    h += '</div>';
  });
  /* 소계 합산 토글 */
  h += '<div class="ab-tbl-subtotal-row">';
  h += '<span class="ab-tbl-subtotal-label">소계 합산</span>';
  h += '<div class="ab-tbl-toggle '+(_abTblModalSubtotal?'on':'off')+'" onclick="abTblModalToggleSubtotal(this)"></div>';
  h += '</div>';
  return h;
}

function abTblModalToggleGroup(col, el){
  const idx = _abTblModalGroupFields.indexOf(col);
  if(idx>=0){
    _abTblModalGroupFields.splice(idx,1);
    el.classList.remove('on');
  } else {
    _abTblModalGroupFields.push(col);
    el.classList.add('on');
  }
  /* 카운터 갱신 */
  abTblModalRender();
}

function abTblModalToggleSubtotal(el){
  _abTblModalSubtotal = !_abTblModalSubtotal;
  el.classList.toggle('on');
  el.classList.toggle('off');
}
