@vmscloud/moz-ui-grid-vue 0.2.23 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -130,7 +130,7 @@ export interface MozGridVueProps extends MozGridVueCoreProps {
130
130
  isCustomState?: boolean;
131
131
  headerMsg?: string;
132
132
  contentMsg?: string;
133
- /** 안내 그림 (기본 true). 'auto' 면 안내 영역 높이가 모자랄 때 그림만 숨기고 문구는 남김(리사이즈 시 재판정) */
133
+ /** 안내 그림 (기본 true). true·'auto' 는 헤더 아래 안내 영역 높이가 모자라면 그림만 숨기고 문구는 남김(리사이즈 시 재판정), false 는 문구만 */
134
134
  useImg?: boolean | 'auto';
135
135
  /** 빈 상태 이미지 강제 지정 (생략 시 totalRowCount/filteredRowCount로 자동 결정) */
136
136
  selectImg?: 'Custom' | 'Result' | 'Data' | 'Search';
@@ -27,6 +27,11 @@ export interface DetailSlotProps {
27
27
  /** 그룹 슬롯 props */
28
28
  export interface GroupSlotProps {
29
29
  groupKey: string;
30
+ /**
31
+ * 그룹 컬럼의 표시 이름(header → label → 컬럼 id, 헤더를 비운 컬럼이면 `''`).
32
+ * 템플릿은 이름을 자동으로 그리지 않는다 — 필요하면 `{{ columnLabel }}: {{ groupValue }}` 처럼 직접 쓴다.
33
+ */
34
+ columnLabel: string;
30
35
  groupValue: unknown;
31
36
  level: number;
32
37
  count: number;
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("../../chunks/index-BLDAzbGv.cjs"),r=require("pinia"),a=a=>{const{tableID:t,flexGrid:o,primaryKeyColumns:s,usePreclear:l=!1,setUploadMessage:n,additionalColumns:i,targetStorage:u,refreshKey:d,propExclusionMessage:p=""}=a,c=e.useExcelStore(),{excelInfo:m}=r.storeToRefs(c),g=new Set(["cfg_demand"]);m.value.targetTable=t,m.value.refreshKey=d??t,m.value.columnMap=c.createColumnMapForImport(o,s),m.value.useDataPreclear=l,m.value.targetStorage=u;const y=i||{};m.value.additionalColumns=y,m.value.additionalClearConditions=y;g.has(t)?(null==n||n(p),m.value.disableMasterProperty=!0):(null==n||n(""),m.value.disableMasterProperty=!1),c.show(l)};exports.createSmartEditorImporter=e=>{const{tableID:r,flexGrid:t,primaryKeyColumns:o,setUploadMessage:s,additionalColumns:l,targetStorage:n,refreshKey:i,propExclusionMessage:u}=e;return e=>{const d=t();if(!d)return;const p=l?l():void 0;a({tableID:r,flexGrid:d,primaryKeyColumns:o(),usePreclear:e,setUploadMessage:s,additionalColumns:p,targetStorage:n,refreshKey:i,propExclusionMessage:u})}},exports.handleSmartEditorImportExcel=a;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("../../chunks/index-niC5bYpl.cjs"),r=require("pinia"),a=a=>{const{tableID:t,flexGrid:o,primaryKeyColumns:s,usePreclear:l=!1,setUploadMessage:n,additionalColumns:i,targetStorage:u,refreshKey:d,propExclusionMessage:p=""}=a,c=e.useExcelStore(),{excelInfo:m}=r.storeToRefs(c),g=new Set(["cfg_demand"]);m.value.targetTable=t,m.value.refreshKey=d??t,m.value.columnMap=c.createColumnMapForImport(o,s),m.value.useDataPreclear=l,m.value.targetStorage=u;const y=i||{};m.value.additionalColumns=y,m.value.additionalClearConditions=y;g.has(t)?(null==n||n(p),m.value.disableMasterProperty=!0):(null==n||n(""),m.value.disableMasterProperty=!1),c.show(l)};exports.createSmartEditorImporter=e=>{const{tableID:r,flexGrid:t,primaryKeyColumns:o,setUploadMessage:s,additionalColumns:l,targetStorage:n,refreshKey:i,propExclusionMessage:u}=e;return e=>{const d=t();if(!d)return;const p=l?l():void 0;a({tableID:r,flexGrid:d,primaryKeyColumns:o(),usePreclear:e,setUploadMessage:s,additionalColumns:p,targetStorage:n,refreshKey:i,propExclusionMessage:u})}},exports.handleSmartEditorImportExcel=a;
@@ -1,4 +1,4 @@
1
- import { u as useExcelStore } from "../../chunks/index-DY0dbAHJ.js";
1
+ import { u as useExcelStore } from "../../chunks/index-CZM3pAmI.js";
2
2
  import { storeToRefs } from "pinia";
3
3
  const handleSmartEditorImportExcel = (params) => {
4
4
  const {
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}}),require("pinia"),require("vue"),require("./ExcelGuard.cjs");const e=require("./ExcelTimeoutManager.cjs");require("../../chunks/index-BLDAzbGv.cjs"),exports.default=e.ExcelQueueManager,exports.queue=e.queue,exports.useExcelQueueStore=e.useExcelQueueStore;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}}),require("pinia"),require("vue"),require("./ExcelGuard.cjs");const e=require("./ExcelTimeoutManager.cjs");require("../../chunks/index-niC5bYpl.cjs"),exports.default=e.ExcelQueueManager,exports.queue=e.queue,exports.useExcelQueueStore=e.useExcelQueueStore;
@@ -2,7 +2,7 @@ import "pinia";
2
2
  import "vue";
3
3
  import "./ExcelGuard.js";
4
4
  import { E, q, a } from "./ExcelTimeoutManager.js";
5
- import "../../chunks/index-DY0dbAHJ.js";
5
+ import "../../chunks/index-CZM3pAmI.js";
6
6
  export {
7
7
  E as default,
8
8
  q as queue,
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),require("../../chunks/notification.contract-oFkBaE2U.cjs"),require("../../chunks/index-BLDAzbGv.cjs"),require("dayjs"),require("./ExcelGuard.cjs");const e=require("./ExcelTimeoutManager.cjs");require("pinia"),require("vue"),exports.cancelExcelTask=e.cancelExcelTask,exports.startDownloadTask=e.startDownloadTask,exports.useExcelTaskStore=e.useExcelTaskStore;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),require("../../chunks/notification.contract-oFkBaE2U.cjs"),require("../../chunks/index-niC5bYpl.cjs"),require("dayjs"),require("./ExcelGuard.cjs");const e=require("./ExcelTimeoutManager.cjs");require("pinia"),require("vue"),exports.cancelExcelTask=e.cancelExcelTask,exports.startDownloadTask=e.startDownloadTask,exports.useExcelTaskStore=e.useExcelTaskStore;
@@ -1,5 +1,5 @@
1
1
  import "../../chunks/notification.contract-BTOch6ed.js";
2
- import "../../chunks/index-DY0dbAHJ.js";
2
+ import "../../chunks/index-CZM3pAmI.js";
3
3
  import "dayjs";
4
4
  import "./ExcelGuard.js";
5
5
  import { c, s, u } from "./ExcelTimeoutManager.js";
@@ -1 +1 @@
1
- "use strict";var e=Object.defineProperty,t=(t,n,r)=>((t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r)(t,"symbol"!=typeof n?n+"":n,r);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("pinia"),r=require("vue"),o=require("./ExcelGuard.cjs"),i=require("../../chunks/notification.contract-oFkBaE2U.cjs"),a=require("../../chunks/index-BLDAzbGv.cjs"),s=require("dayjs"),l=e=>e?btoa(encodeURIComponent(e)):"",u=Symbol.for("MozProjectInfo");function c(){const e=r.inject(u);if(!e)throw new Error("[ProjectInfo] store not provided. make sure provideProjectInfo() is called in root component (e.g., App.vue).");return e}const d=n.defineStore("excelTimeout",()=>{const e=c();return{tenantID:r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),userID:r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""}),timeoutMessage:r.computed(()=>a.getGridVueLocale().requestTimedOut)}}),p=class e{constructor(){t(this,"timers",new Map),t(this,"timeoutMs",3e5)}static getInstance(){return e.instance||(e.instance=new e),e.instance}clear(e){const t=this.timers.get(e);t&&clearTimeout(t),this.timers.delete(e)}start(e,t=this.timeoutMs){if(!e)return;this.clear(e);const n=i.useGridNotificationStore(),r=a.useExcelStore(),s=d(),l=window.setTimeout(()=>{var t,i;if(!n)return;const a=n.eventMessages.find(t=>{var n;return(null==(n=t.prop_value)?void 0:n.session_id)===e});a&&a.prop_value&&(a.prop_value.status="error",a.prop_value.error_message=a.prop_value.error_message||s.timeoutMessage),r.excelFileMap.delete(e);const l=o.getUserLockKey(s.tenantID,s.userID);o.unlock(l),null==(t=x.removeDownloadByClientId)||t.call(x,e),null==(i=x.tryStartNext)||i.call(x),o.removeSession(s.tenantID,s.userID),this.clear(e)},t);this.timers.set(e,l)}};t(p,"instance");let v=p;const m=v.getInstance(),_=n.defineStore("excelTask",()=>{const e=c(),t=r.computed(()=>e.currentProjectID),n=r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),u=r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantName)||""}),d=r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""}),p=r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.name)||""}),v=r.computed(()=>{var t;return(null==(t=e.currentProject)?void 0:t.projectNM)||""});return{startDownloadTask:async e=>{const r=a.useExcelStore(),c=i.useGridNotificationStore();if(!c){const e=o.getUserLockKey(n.value,d.value);return o.unlock(e),null}const _={...e||{}};_.project_id=_.project_id||t.value,_.api_key&&(_.data_path=`/api/aps/backend/${t.value}/${_.api_key}`,delete _.api_key),_.column_source&&(_.column_map=r.createColumnMapForExport(_.column_source),delete _.column_source),_.field_source&&(_.column_map=r.createFieldMapForExport(_.field_source),delete _.field_source),_.client_id||(_.client_id=a.generateGUID());const f=o.getUserLockKey(n.value,d.value),h=localStorage.getItem(f);if(h)try{const e=JSON.parse(h);if((null==e?void 0:e.sessionId)&&e.sessionId!==_.client_id)return null}catch{o.lock(f,_.client_id)}else o.lock(f,_.client_id);o.setSession(n.value,d.value,_.client_id,_.file_name||""),_.file_format||(_.file_format="excel"),r.excelFileMap.set(_.client_id,_.file_name),m.start(_.client_id);const g={noti_type:"progress",header_key:"text-file_download",noti_datetime:s().format("YYYY-MM-DD HH:mm:ss"),noti_sender_id:"",prop_value:{session_id:_.client_id,file_name:`${_.file_name||""}_${s().format("YYYYMMDD")}`,status:"in_progress",progress_per:0,operation_type:"download",remain_time:-1}};c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===_.client_id})||c.eventMessages.unshift(g);try{await r.requestGenerateExcel(_,{headers:{"tenant-id":l(n.value),"tenant-name":l(u.value),"project-name":l(v.value),"user-name":l(p.value)}}).then(e=>{const t=e.data.data.file_name,i=_.client_id;r.excelFileMap.set(i,t),o.updateSessionFileName(n.value,d.value,i,t);const a=c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===i});a&&a.prop_value&&(a.prop_value.file_name=t)}).catch(e=>{r.excelFileMap.delete(_.client_id);const t=c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===_.client_id});t&&t.prop_value&&(t.prop_value.status="error"),m.clear(_.client_id),o.unlock(f)})}catch(y){r.excelFileMap.delete(_.client_id),m.clear(_.client_id),o.unlock(f)}return{sessionId:_.client_id}},cancelExcelTask:async(e,t,r)=>{var a,s;if("upload"===t)return;const l=i.useGridNotificationStore();l&&l.markSessionCanceled(e),r&&await r(e);const u=o.getUserLockKey(n.value,d.value);o.unlock(u),null==(a=x.removeDownloadByClientId)||a.call(x,e),o.removeSession(n.value,d.value),null==(s=x.tryStartNext)||s.call(x)}}}),f=async e=>_().startDownloadTask(e),h=n.defineStore("excelQueue",()=>{const e=c();return{tenantID:r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),userID:r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""})}}),g=class e{constructor(){t(this,"inMemoryUploadQueue",[]),t(this,"inMemoryDownloadQueue",[]),t(this,"initialized",!1)}static getInstance(){return e.instance||(e.instance=new e),e.instance}initSubscriptions(){this.initialized||(this.initialized=!0,o.initExcelGuard())}loadQueue(){return this.inMemoryDownloadQueue.slice()}saveQueue(e){this.inMemoryDownloadQueue=e}enqueueDownload(e){this.initSubscriptions();const t=this.loadQueue(),n={id:a.generateGUID(),type:"download",payload:{...e},meta:{fileName:null==e?void 0:e.file_name,createdAt:Date.now()}};return t.push(n),this.saveQueue(t),this.tryStartNext(),this.getTotalQueueLength()}getQueueLength(){return this.loadQueue().length}getUploadQueueLength(){return this.inMemoryUploadQueue.length}getTotalQueueLength(){return this.getQueueLength()+this.getUploadQueueLength()}enqueueUpload(e,t){this.initSubscriptions();const n={id:a.generateGUID(),type:"upload",start:e,meta:{fileName:t,createdAt:Date.now()}};return this.inMemoryUploadQueue.push(n),this.tryStartNext(),this.getTotalQueueLength()}removeDownloadByClientId(e){if(!e)return;const t=this.loadQueue(),n=t.findIndex(t=>{var n;return"download"===t.type&&(null==(n=t.payload)?void 0:n.client_id)===e});-1!==n&&(t.splice(n,1),this.saveQueue(t))}async tryStartNext(){this.initSubscriptions();const e=h(),t=o.getUserLockKey(e.tenantID,e.userID);if(o.isLocked(t))return;const n=this.loadQueue(),r=n.length>0?n[0]:null,i=this.inMemoryUploadQueue.length>0?this.inMemoryUploadQueue[0]:null;if(!r&&!i)return;if((()=>{var e,t;if(r&&i){return((null==(e=r.meta)?void 0:e.createdAt)??0)<=((null==(t=i.meta)?void 0:t.createdAt)??0)}return!!r})()&&"download"===(null==r?void 0:r.type)){const e={...r.payload};e.client_id||(e.client_id=a.generateGUID()),o.lock(t,e.client_id),n.shift(),this.saveQueue(n);try{await f(e)}catch(s){o.unlock(t)}return}if(i){const e=this.inMemoryUploadQueue.shift();try{await Promise.resolve(e.start())}catch(s){}return}}};t(g,"instance");let y=g;const x=y.getInstance();exports.ExcelQueueManager=y,exports.cancelExcelTask=async(e,t,n)=>_().cancelExcelTask(e,t,n),exports.default=v,exports.excelTimeoutManager=m,exports.queue=x,exports.startDownloadTask=f,exports.useExcelQueueStore=h,exports.useExcelTaskStore=_,exports.useExcelTimeoutStore=d;
1
+ "use strict";var e=Object.defineProperty,t=(t,n,r)=>((t,n,r)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[n]=r)(t,"symbol"!=typeof n?n+"":n,r);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const n=require("pinia"),r=require("vue"),o=require("./ExcelGuard.cjs"),i=require("../../chunks/notification.contract-oFkBaE2U.cjs"),a=require("../../chunks/index-niC5bYpl.cjs"),s=require("dayjs"),l=e=>e?btoa(encodeURIComponent(e)):"",u=Symbol.for("MozProjectInfo");function c(){const e=r.inject(u);if(!e)throw new Error("[ProjectInfo] store not provided. make sure provideProjectInfo() is called in root component (e.g., App.vue).");return e}const d=n.defineStore("excelTimeout",()=>{const e=c();return{tenantID:r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),userID:r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""}),timeoutMessage:r.computed(()=>a.getGridVueLocale().requestTimedOut)}}),p=class e{constructor(){t(this,"timers",new Map),t(this,"timeoutMs",3e5)}static getInstance(){return e.instance||(e.instance=new e),e.instance}clear(e){const t=this.timers.get(e);t&&clearTimeout(t),this.timers.delete(e)}start(e,t=this.timeoutMs){if(!e)return;this.clear(e);const n=i.useGridNotificationStore(),r=a.useExcelStore(),s=d(),l=window.setTimeout(()=>{var t,i;if(!n)return;const a=n.eventMessages.find(t=>{var n;return(null==(n=t.prop_value)?void 0:n.session_id)===e});a&&a.prop_value&&(a.prop_value.status="error",a.prop_value.error_message=a.prop_value.error_message||s.timeoutMessage),r.excelFileMap.delete(e);const l=o.getUserLockKey(s.tenantID,s.userID);o.unlock(l),null==(t=x.removeDownloadByClientId)||t.call(x,e),null==(i=x.tryStartNext)||i.call(x),o.removeSession(s.tenantID,s.userID),this.clear(e)},t);this.timers.set(e,l)}};t(p,"instance");let v=p;const m=v.getInstance(),_=n.defineStore("excelTask",()=>{const e=c(),t=r.computed(()=>e.currentProjectID),n=r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),u=r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantName)||""}),d=r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""}),p=r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.name)||""}),v=r.computed(()=>{var t;return(null==(t=e.currentProject)?void 0:t.projectNM)||""});return{startDownloadTask:async e=>{const r=a.useExcelStore(),c=i.useGridNotificationStore();if(!c){const e=o.getUserLockKey(n.value,d.value);return o.unlock(e),null}const _={...e||{}};_.project_id=_.project_id||t.value,_.api_key&&(_.data_path=`/api/aps/backend/${t.value}/${_.api_key}`,delete _.api_key),_.column_source&&(_.column_map=r.createColumnMapForExport(_.column_source),delete _.column_source),_.field_source&&(_.column_map=r.createFieldMapForExport(_.field_source),delete _.field_source),_.client_id||(_.client_id=a.generateGUID());const f=o.getUserLockKey(n.value,d.value),h=localStorage.getItem(f);if(h)try{const e=JSON.parse(h);if((null==e?void 0:e.sessionId)&&e.sessionId!==_.client_id)return null}catch{o.lock(f,_.client_id)}else o.lock(f,_.client_id);o.setSession(n.value,d.value,_.client_id,_.file_name||""),_.file_format||(_.file_format="excel"),r.excelFileMap.set(_.client_id,_.file_name),m.start(_.client_id);const g={noti_type:"progress",header_key:"text-file_download",noti_datetime:s().format("YYYY-MM-DD HH:mm:ss"),noti_sender_id:"",prop_value:{session_id:_.client_id,file_name:`${_.file_name||""}_${s().format("YYYYMMDD")}`,status:"in_progress",progress_per:0,operation_type:"download",remain_time:-1}};c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===_.client_id})||c.eventMessages.unshift(g);try{await r.requestGenerateExcel(_,{headers:{"tenant-id":l(n.value),"tenant-name":l(u.value),"project-name":l(v.value),"user-name":l(p.value)}}).then(e=>{const t=e.data.data.file_name,i=_.client_id;r.excelFileMap.set(i,t),o.updateSessionFileName(n.value,d.value,i,t);const a=c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===i});a&&a.prop_value&&(a.prop_value.file_name=t)}).catch(e=>{r.excelFileMap.delete(_.client_id);const t=c.eventMessages.find(e=>{var t;return(null==(t=e.prop_value)?void 0:t.session_id)===_.client_id});t&&t.prop_value&&(t.prop_value.status="error"),m.clear(_.client_id),o.unlock(f)})}catch(y){r.excelFileMap.delete(_.client_id),m.clear(_.client_id),o.unlock(f)}return{sessionId:_.client_id}},cancelExcelTask:async(e,t,r)=>{var a,s;if("upload"===t)return;const l=i.useGridNotificationStore();l&&l.markSessionCanceled(e),r&&await r(e);const u=o.getUserLockKey(n.value,d.value);o.unlock(u),null==(a=x.removeDownloadByClientId)||a.call(x,e),o.removeSession(n.value,d.value),null==(s=x.tryStartNext)||s.call(x)}}}),f=async e=>_().startDownloadTask(e),h=n.defineStore("excelQueue",()=>{const e=c();return{tenantID:r.computed(()=>{var t;return(null==(t=e.tenantInfo)?void 0:t.tenantID)||""}),userID:r.computed(()=>{var t;return(null==(t=e.userInfo)?void 0:t.id)||""})}}),g=class e{constructor(){t(this,"inMemoryUploadQueue",[]),t(this,"inMemoryDownloadQueue",[]),t(this,"initialized",!1)}static getInstance(){return e.instance||(e.instance=new e),e.instance}initSubscriptions(){this.initialized||(this.initialized=!0,o.initExcelGuard())}loadQueue(){return this.inMemoryDownloadQueue.slice()}saveQueue(e){this.inMemoryDownloadQueue=e}enqueueDownload(e){this.initSubscriptions();const t=this.loadQueue(),n={id:a.generateGUID(),type:"download",payload:{...e},meta:{fileName:null==e?void 0:e.file_name,createdAt:Date.now()}};return t.push(n),this.saveQueue(t),this.tryStartNext(),this.getTotalQueueLength()}getQueueLength(){return this.loadQueue().length}getUploadQueueLength(){return this.inMemoryUploadQueue.length}getTotalQueueLength(){return this.getQueueLength()+this.getUploadQueueLength()}enqueueUpload(e,t){this.initSubscriptions();const n={id:a.generateGUID(),type:"upload",start:e,meta:{fileName:t,createdAt:Date.now()}};return this.inMemoryUploadQueue.push(n),this.tryStartNext(),this.getTotalQueueLength()}removeDownloadByClientId(e){if(!e)return;const t=this.loadQueue(),n=t.findIndex(t=>{var n;return"download"===t.type&&(null==(n=t.payload)?void 0:n.client_id)===e});-1!==n&&(t.splice(n,1),this.saveQueue(t))}async tryStartNext(){this.initSubscriptions();const e=h(),t=o.getUserLockKey(e.tenantID,e.userID);if(o.isLocked(t))return;const n=this.loadQueue(),r=n.length>0?n[0]:null,i=this.inMemoryUploadQueue.length>0?this.inMemoryUploadQueue[0]:null;if(!r&&!i)return;if((()=>{var e,t;if(r&&i){return((null==(e=r.meta)?void 0:e.createdAt)??0)<=((null==(t=i.meta)?void 0:t.createdAt)??0)}return!!r})()&&"download"===(null==r?void 0:r.type)){const e={...r.payload};e.client_id||(e.client_id=a.generateGUID()),o.lock(t,e.client_id),n.shift(),this.saveQueue(n);try{await f(e)}catch(s){o.unlock(t)}return}if(i){const e=this.inMemoryUploadQueue.shift();try{await Promise.resolve(e.start())}catch(s){}return}}};t(g,"instance");let y=g;const x=y.getInstance();exports.ExcelQueueManager=y,exports.cancelExcelTask=async(e,t,n)=>_().cancelExcelTask(e,t,n),exports.default=v,exports.excelTimeoutManager=m,exports.queue=x,exports.startDownloadTask=f,exports.useExcelQueueStore=h,exports.useExcelTaskStore=_,exports.useExcelTimeoutStore=d;
@@ -5,7 +5,7 @@ import { defineStore } from "pinia";
5
5
  import { inject, computed } from "vue";
6
6
  import { getUserLockKey, unlock, removeSession, lock, setSession, updateSessionFileName, isLocked, initExcelGuard } from "./ExcelGuard.js";
7
7
  import { u as useGridNotificationStore } from "../../chunks/notification.contract-BTOch6ed.js";
8
- import { g as getGridVueLocale, u as useExcelStore, f as generateGUID } from "../../chunks/index-DY0dbAHJ.js";
8
+ import { g as getGridVueLocale, u as useExcelStore, f as generateGUID } from "../../chunks/index-CZM3pAmI.js";
9
9
  import dayjs from "dayjs";
10
10
  const encodeUnescapeBtoa = (text) => {
11
11
  if (!text) return "";
@@ -243,7 +243,7 @@ await grid.waitForReady();
243
243
  | 4 | (빈 칸) | (빈 칸) | 님 |
244
244
  | 5, 6 | 홍길동 | A | 홍길동님 |
245
245
 
246
- - 묶기: 같은 설정에 `rowGroupConfig: { columns: ['owner'] }` 를 더하면 1·2행은 객체 참조가 달라도 id 가 같아 그룹 하나(`owner:u2`), 3행(동명이인)은 따로(`owner:u3`)입니다. 배너 라벨은 둘 다 `김철수` 입니다.
246
+ - 묶기: 같은 설정에 `rowGroupConfig: { columns: ['owner'] }` 를 더하면 1·2행은 객체 참조가 달라도 id 가 같아 그룹 하나(`owner:u2`), 3행(동명이인)은 따로(`owner:u3`)입니다. 배너 라벨은 둘 다 `담당자: 김철수` 입니다(`rowGroupConfig.showColumnLabel: false` 면 `김철수`).
247
247
 
248
248
  ```typescript
249
249
  await grid.rowGroup.collapse('owner:u2'); // 1·2행만 접힌다 — 3행은 그대로 보인다
@@ -636,6 +636,7 @@ await grid.rowGroup.setConfig({
636
636
  columns: ['region', 'category'],
637
637
  aggregates: { amount: 'sum', quantity: 'avg' },
638
638
  defaultCollapsed: false,
639
+ // showColumnLabel: false, // 배너를 `서울 (12개 항목)` 처럼 값만 — 기본(true)은 `지역: 서울 (12개 항목)`
639
640
  });
640
641
 
641
642
  // ── 간편 그룹화 ──
@@ -910,14 +911,16 @@ await grid.sort([{ id: 'region', direction: 'asc', sequence: 1 }]);
910
911
  // 값(집계) 기준 정렬 — by 로 집계 값 기준 정렬
911
912
  await grid.sort([{ id: 'region', direction: 'desc', sequence: 1, by: 'sales' }]);
912
913
 
913
- // 차원(행/열 필드) 필터 → core 가 자동으로 pre(소스 축소 후 재집계)로 분류
914
- await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1 }]);
915
- // 값(measure) 필터 → 자동으로 post(피벗 값 기준 행 필터). excludeUnmatchedColumns 로 빈 컬럼까지 제거
914
+ // 피벗 필터 기본 = post(피벗 결과 기준). 행 필드 필터 → 행을 줄인다. excludeUnmatchedColumns 로 빈 컬럼까지 제거
915
+ await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1, excludeUnmatchedColumns: true }]);
916
+ // 값(measure) 필터 → 피벗 값 기준 행 필터. excludeUnmatchedColumns 로 매칭 셀 없는 컬럼 제거
916
917
  await grid.filter([
917
918
  { id: 'sales', operator: 'gte', filterValue: 1000, sequence: 1, excludeUnmatchedColumns: true },
918
919
  ]);
919
- // 명시 mode 로 분류 override (예: dimension 필터를 소스 축소 없이 post 로)
920
- await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1, mode: 'post' }]);
920
+ // 열 필드 필터 → 열을 줄인다. excludeUnmatchedRows 로 남은 열에 데이터가 없는 행까지 제거
921
+ await grid.filter([{ id: 'quarter', operator: 'eq', filterValue: 'Q1', sequence: 1, excludeUnmatchedRows: true }]);
922
+ // 명시 mode 로 override (예: 원본 행을 거른 뒤 집계)
923
+ await grid.filter([{ id: 'region', operator: 'eq', filterValue: '서울', sequence: 1, mode: 'pre' }]);
921
924
 
922
925
  // ── 열 노드 네비게이션 (Row 메서드와 대칭) ──
923
926
  await grid.pivot.toggleColumn('quarter:Q1');
@@ -1272,6 +1272,7 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
1272
1272
  > `{ id, data }`로 추가한 데이터 고정 행(본문 데이터에 없음)은 `enableReferCache` 없이도 클릭·더블클릭·우클릭(`contextmenu` 의 `rowData` 포함)이 동작합니다. 셀 편집은 그 고정 행 데이터(`row.getData()`)에 저장되고 화면에 표시되며 `cell:editCommitted`도 발행됩니다. 화면 전용 행이므로 변경 추적(`changes.getAll()`)에는 들어가지 않지만 편집은 Undo/Redo 됩니다(`enableReferCache:true` 에서 본문에 같은 id 행이 있으면 기존대로 본문 행이 편집됩니다). 고정 영역 셀을 선택하고 붙여넣기(Ctrl+V)해도 같은 규칙이며, 여러 셀은 Undo 한 번에 되돌아갑니다. 붙여넣기는 그 고정 영역의 행 순서로만 채우고 본문 행으로 넘치지 않습니다(본문 행을 고정한 행은 본문 데이터로 저장되어 변경 추적에 들어갑니다).
1273
1273
  > 화면 전용 고정 행(`addPinnedTop/Bottom`, 본문에 없는 행의 `pinTop*`/`pinBottom*` — `variant: 'custom'` 포함)의 **고정과 해제(`unpin`/`unpinById`/`clearPinned`)도 Undo/Redo** 됩니다. Undo 는 같은 `Row` 인스턴스(편집 값 포함)를 같은 영역·같은 순서로 되살리고, `clearPinned`로 여러 행을 해제했으면 Undo 한 번에 모두 복원됩니다. 본문 행의 고정·해제(`setProps({ pinned })`, 본문 행을 옮기는 `pin*`)와 `summaryRow` 총합계 행은 화면 상태라 기록하지 않습니다(컬럼 고정·정렬과 같은 기준). Undo/Redo 로 고정 상태가 바뀌면 `row:pinned`/`row:unpinned`가 `source: 'undo' | 'redo'`와 함께 다시 발행됩니다.
1274
1274
  > 고정 행(종류 무관 — 화면 전용·본문 행 고정 모두)의 키 컬럼(`keyFields`)은 편집할 수 없습니다.
1275
+ > `addPinnedTop`/`addPinnedBottom`(및 화면 전용 `pin*`)은 행을 즉시 `getPinned()`에 넣고, 화면은 **다음 microtask 에 한 번** 그립니다 — 같은 틱에 여러 행을 추가해도 고정 영역은 한 번만 다시 그려집니다. 고정 집계 행의 집계값(함수형 `func` 포함)은 데이터·필터가 바뀔 때(`refresh`·`refreshPinned`·필터·셀 수정 등)만 다시 계산하고, 그 사이의 다시 그리기(병합 계산·열 폭 변경·가로 스크롤)는 앞 결과를 씁니다. `row.setAggregates()` 로 집계 설정을 바꾸면 다음 그리기에서 새로 계산됩니다.
1275
1276
 
1276
1277
  #### 고정 관리
1277
1278
 
@@ -1331,7 +1332,7 @@ rowSelection(선택된 행들)·cellSelection(선택된 셀 range)은 서로 다
1331
1332
 
1332
1333
  **Satellite 키 자동 동기화** — `update` 가 내부적으로 동시에 mutate 하는 짝:
1333
1334
  - `pinned` ↔ `frozen` (서로 alias)
1334
- - `header` ↔ `label` (header 입력 시 label 도 함께 세팅)
1335
+ - `header` ↔ `label` (header 입력 시 label 도 함께 세팅). `label`은 deprecated 입니다 — `header`를 쓰세요
1335
1336
  - `dataType` → `type`
1336
1337
 
1337
1338
  **`options.updateSnapshot: true`** — 변경분을 초기 스냅샷에도 미러합니다. 이후 `reset(colId)` 호출 시 이번 변경을 **포함한** 새 baseline 으로 복귀. 사용자 UI 의 임시 오버레이가 아닌 "**새 영구 baseline 으로 승격**" 의도일 때 사용합니다.
@@ -1361,7 +1362,7 @@ grid.columns.update('amount', { header: '판매금액' }, { updateSnapshot: true
1361
1362
  **충돌 키 정책** — state(런타임)가 승:
1362
1363
  - `width` / `pinned` / `visible` / `autoFit` → 현재값 (state)
1363
1364
  - `hidden` → `!visible` 로 파생
1364
- - `header` → `def.header ?? def.label ?? def.id` 로 항상 비어있지 않음
1365
+ - `header` → `def.header ?? def.label ?? def.id` 로 항상 비어있지 않음. 표시 이름은 이 `header`를 읽으세요(`label`은 deprecated)
1365
1366
 
1366
1367
  #### 저장/복원 (직렬화)
1367
1368
 
@@ -1629,7 +1630,7 @@ await grid.changes.commit({ allowInvalid: true });
1629
1630
  - 결과: `grid.selection.getSelectedGroups()` → `[{ groupId, groupKeys, rowCount, rowIds }]`, `selection:changed`의 `selectedGroups`. 서버 그룹(SSRM)에서는 `rowIds` 가 없습니다. `rowIds`는 그룹의 리프 멤버 전체(접혀 있어도, 중첩 그룹이면 하위 리프 전부)이며 표기는 `getGroupRows`와 같습니다(단일 키 원시값).
1630
1631
  - 행 선택(`rowSelection`) 결과는 바뀌지 않습니다 — 그룹 멤버를 행 선택에 넣으려면 소비처가 `rowIds`를 사용합니다.
1631
1632
  - 배너에서 고른 그룹은 배너와 그룹행 셀을 행 선택 색으로 칠합니다(`.ps-group-banner-selected`, `.ps-group-row-selected`). 범위가 지나간 그룹은 결과에만 기록되고 화면 표시는 없습니다(사각형은 배너에서 분할).
1632
- - 배너 커서: selectable이면 배너 본문·그룹행은 데이터 셀과 같은 커서이고 ▾ 버튼만 pointer입니다. 옵션(`selectable`/`defaultCollapsed`/`defaultExpandLevel`)은 그룹 컬럼을 모두 뺐다가(`removeColumn`/`groupBy([])`/`setConfig({ columns: [] })`, `getConfig()`는 `null`) 다시 넣어도 유지되며, 생략 시 이어받고 명시값(`false` 포함)이 이깁니다. `clear()`/`setConfig(null)`은 옵션까지 지웁니다.
1633
+ - 배너 커서: selectable이면 배너 본문·그룹행은 데이터 셀과 같은 커서이고 ▾ 버튼만 pointer입니다. 옵션(`selectable`/`showColumnLabel`/`defaultCollapsed`/`defaultExpandLevel`)은 그룹 컬럼을 모두 뺐다가(`removeColumn`/`groupBy([])`/`setConfig({ columns: [] })`, `getConfig()`는 `null`) 다시 넣어도 유지되며, 생략 시 이어받고 명시값(`false` 포함)이 이깁니다. `clear()`/`setConfig(null)`은 옵션까지 지웁니다.
1633
1634
  - 다른 셀/행 클릭·새 범위, `clearSelection`/`clearCellSelection`, `rowGroup.setConfig`(재그룹), `loadData` 시 그룹 선택이 비워집니다.
1634
1635
  - Ctrl+A·프로그래매틱 범위 API(`selectCellRangeBy*`)는 그룹을 고르지 않습니다.
1635
1636
  - 키보드(셀 범위 모드·행 선택 모드 공통, 펼침/접힘·다단 그룹 모두 같음):
@@ -1655,6 +1656,31 @@ await grid.loadData(rows);
1655
1656
  grid.selection.selectGroupsByViewIndices([0]); // 또는 selectGroupsByIds(['region:R1'])
1656
1657
  ```
1657
1658
 
1659
+ #### 그룹 배너의 컬럼 이름 (`showColumnLabel`)
1660
+
1661
+ `rowGroupConfig.showColumnLabel`(기본 `true`)이면 기본 그룹 배너에서 그룹 값 앞에 **그룹 컬럼의 헤더 이름**을 붙입니다. `false`면 예전처럼 값만 보입니다.
1662
+
1663
+ | 설정 | 배너 글자 (부서로 묶고 `aggregates: [{ column: 'salary', func: 'sum' }]`) |
1664
+ |------|------|
1665
+ | `showColumnLabel` 생략 / `true` | `▾ 부서: 개발팀 (12개 항목) 연봉 (Sum): 67,254` |
1666
+ | `showColumnLabel: false` | `▾ 개발팀 (12개 항목) 연봉 (Sum): 67,254` |
1667
+
1668
+ - 이름은 그 컬럼의 헤더(`fields[].header`)이고, 헤더가 없으면 컬럼 id 입니다. 예: `{ id: 'dept', header: '부서' }` → `부서: `, `{ id: 'dept' }` → `dept: `. 헤더를 일부러 비운 컬럼(`header: ''`)은 이름 없이 값만 보입니다(`: 개발팀`처럼 콜론만 남지 않음).
1669
+ - 다단계 그룹은 레벨마다 자기 그룹 컬럼 이름을 씁니다. 예: `columns: ['dept', 'status']` → 1단 `부서: 개발팀 (12개 항목)`, 2단 `재직 상태: 재직 (9개 항목)`.
1670
+ - DOM: `<span class="ps-group-label"><span class="ps-group-column-label">부서: </span><strong>개발팀</strong> (12개 항목)</span>`. 이름 부분의 모양만 바꾸려면 `.ps-group-column-label`에 스타일을 줍니다(예: `.ps-group-column-label { color: var(--ps-text-secondary); font-weight: normal; }`).
1671
+ - 내보내기(XLSX·CSV)의 그룹행 글자도 화면과 같습니다: `true` → `부서: 개발팀 (12)`, `false` → `개발팀 (12)`.
1672
+ - GroupTemplate(Vue `<GroupTemplate>`, React `groupTemplate`)으로 배너를 바꾼 경우에는 코어가 이름을 그리지 않습니다 — 템플릿이 배너 내용을 대신 그립니다(내보내기는 위 기본 배너 글자 규칙을 따릅니다). 템플릿에서 이름을 보이려면 슬롯 props `columnLabel`을 직접 씁니다: `<GroupTemplate #default="{ columnLabel, groupValue }"><span v-if="columnLabel" class="ps-group-column-label">{{ columnLabel }}: </span>{{ groupValue }}</GroupTemplate>` (`ps-group-column-label` 클래스를 쓰면 기본 배너와 같은 모양).
1673
+ - 보존 규칙은 `selectable`과 같습니다: 그룹 컬럼을 모두 뺐다가 다시 넣어도 유지되고, 생략하면 이어받으며 명시값(`false` 포함)이 이깁니다. `clear()`/`setConfig(null)`은 옵션까지 지웁니다.
1674
+
1675
+ > **동작 변화** — 기본값이 `true`이므로 이 버전으로 올리면 기존 화면의 배너에도 컬럼 이름이 붙습니다(`개발팀 (12개 항목)` → `부서: 개발팀 (12개 항목)`). 예전 모양을 유지하려면 `showColumnLabel: false`를 주세요.
1676
+
1677
+ ```ts
1678
+ // 예전처럼 값만 보이기
1679
+ await grid.rowGroup.setConfig({ columns: ['dept'], showColumnLabel: false });
1680
+ // 이후 groupBy/addColumn 은 false 를 이어받습니다
1681
+ await grid.rowGroup.addColumn('status'); // → 1단 `개발팀 (12개 항목)`, 2단 `재직 (9개 항목)`
1682
+ ```
1683
+
1658
1684
  ---
1659
1685
 
1660
1686
  #### 그룹 집계 표시 자리와 함수형 집계
@@ -2314,7 +2340,7 @@ await grid.io.saveCsvToFile({ includePinnedRows: true });
2314
2340
 
2315
2341
  | 필드 | 타입 | 기본값 | 설명 |
2316
2342
  |------|------|--------|------|
2317
- | `groupRowMode` | `'include' \| 'visible' \| 'dataOnly'` | `'include'` | 플랫 행 그룹화(`grid.rowGroup.setConfig`) 활성 시 export 동작. `'include'`: 접힘 상태 무시하고 모든 그룹 행 + data를 펼친 상태로 export. `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). `'dataOnly'`: 그룹 행 제외하고 data 행만. 그룹 행·순서·개수·집계는 화면과 같다(행 필터·정렬, 그룹 정렬·그룹 결과 필터 반영). 내보내기는 화면 접힘 상태를 바꾸지 않는다. 그룹 행 집계도 화면 자리 그대로 나간다: 그룹 행에서 자기 칸을 갖는 컬럼(고정 컬럼, center 양 끝의 연속 `unmergeOnGroupRow`)의 컬럼 `aggregate` 값은 그 컬럼 칸에(숫자 셀, 서식·mask 는 본문 칸과 같다), 배너 글자(`rowGroupConfig.aggregates` — 자기 칸 컬럼 항목 포함)는 그룹 라벨 칸에 화면 배너와 같은 글자로 ` · ` 를 붙여 나간다(예: `A (3) · Qty (Sum): 6,000 · Amt (Max): 7`, `scope`·숨김 컬럼 규칙도 화면과 같다). 자기 칸이 없는 컬럼의 컬럼 `aggregate`(함수형 포함)는 화면처럼 나가지 않는다. GroupTemplate 으로 배너를 바꾼 화면도 파일은 기본 배너 글자를 쓴다 |
2343
+ | `groupRowMode` | `'include' \| 'visible' \| 'dataOnly'` | `'include'` | 플랫 행 그룹화(`grid.rowGroup.setConfig`) 활성 시 export 동작. `'include'`: 접힘 상태 무시하고 모든 그룹 행 + data를 펼친 상태로 export. `'visible'`: 현재 접힘 상태 그대로 (collapsed 자식 제외). `'dataOnly'`: 그룹 행 제외하고 data 행만. 그룹 행·순서·개수·집계는 화면과 같다(행 필터·정렬, 그룹 정렬·그룹 결과 필터 반영). 내보내기는 화면 접힘 상태를 바꾸지 않는다. 그룹 행 집계도 화면 자리 그대로 나간다: 그룹 행에서 자기 칸을 갖는 컬럼(고정 컬럼, center 양 끝의 연속 `unmergeOnGroupRow`)의 컬럼 `aggregate` 값은 그 컬럼 칸에(숫자 셀, 서식·mask 는 본문 칸과 같다), 배너 글자(`rowGroupConfig.aggregates` — 자기 칸 컬럼 항목 포함)는 그룹 라벨 칸에 화면 배너와 같은 글자로 ` · ` 를 붙여 나간다(예: `Cat: A (3) · Qty (Sum): 6,000 · Amt (Max): 7` — `Cat:`은 그룹 컬럼 헤더 이름이며 `rowGroupConfig.showColumnLabel: false`면 `A (3) · …`, `scope`·숨김 컬럼 규칙도 화면과 같다). 자기 칸이 없는 컬럼의 컬럼 `aggregate`(함수형 포함)는 화면처럼 나가지 않는다. GroupTemplate 으로 배너를 바꾼 화면도 파일은 기본 배너 글자를 쓴다 |
2318
2344
  | `groupOutline` | `boolean` | `true` | Excel 네이티브 outline (+/- 버튼) 생성. 그룹 행을 summary row로, 데이터 행에 `outlineLevel`을 부여. `groupRowMode: 'dataOnly'`이면 무시. 중첩 그룹(`columns: ['dept','role']`)은 depth별 outlineLevel 자동 계산 |
2319
2345
 
2320
2346
  ##### 트리 그리드 export 옵션
@@ -2707,6 +2733,7 @@ const b = store.select(s => s.canRedo);
2707
2733
  | 필드 | 타입 | 설명 |
2708
2734
  |------|------|------|
2709
2735
  | `activity` | `GridActivity` | 현재 실행 중인 비동기 작업 (중첩 시 가장 바깥쪽만 노출) |
2736
+ | `activities` | `readonly GridActivity[]` | 동시에 진행 중인 비동기 작업 전부(시작 순, 같은 작업은 한 번, 없으면 `[]`). 겹쳐 시작한 작업(예: `refresh` 도중의 `setPivotConfig`)도 보인다 — "이 작업이 하나라도 도는가"는 `s.activities.includes('setPivotConfig')` |
2710
2737
  | `isSearching` | `boolean` | 검색 진행 중 여부 (background 작업, `activity`와 독립) |
2711
2738
 
2712
2739
  `GridActivity` 전체 값:
@@ -3268,10 +3295,11 @@ interface RowGroupConfig {
3268
3295
  defaultCollapsed?: boolean;
3269
3296
  defaultExpandLevel?: number;
3270
3297
  selectable?: boolean;
3298
+ showColumnLabel?: boolean; // 기본 true — 배너 값 앞에 그룹 컬럼 헤더 이름(`부서: 개발팀`)
3271
3299
  }
3272
3300
  ```
3273
3301
 
3274
- `defaultCollapsed: true`는 `defaultExpandLevel`보다 우선합니다. `defaultExpandLevel`은 0이면 최상위 그룹행만, 1이면 그 아래 한 단계까지 펼치며 생략하면 모두 펼칩니다. `selectable`은 기본 `false`입니다.
3302
+ `defaultCollapsed: true`는 `defaultExpandLevel`보다 우선합니다. `defaultExpandLevel`은 0이면 최상위 그룹행만, 1이면 그 아래 한 단계까지 펼치며 생략하면 모두 펼칩니다. `selectable`은 기본 `false`입니다. `showColumnLabel`은 기본 `true`이며 기본 배너가 `부서: 개발팀 (12개 항목)`처럼 그룹 컬럼 이름을 붙입니다. `false`면 `개발팀 (12개 항목)`처럼 값만 보입니다(→ [그룹 배너의 컬럼 이름](#그룹-배너의-컬럼-이름-showcolumnlabel)).
3275
3303
 
3276
3304
  재로드(`loadData`) 때 기본 접힘 규칙은 **처음 보는 그룹**에 적용하고, 이미 본 그룹은 지금 상태(사용자가 펴거나 접은 것)를 유지합니다(서버 그룹과 같은 규칙). 예: `defaultCollapsed: true`로 빈 데이터에서 시작해 조회하면 도착한 그룹이 모두 접힌 채 나오고, `HR`을 편 뒤 재조회로 `Ops`가 새로 생기면 `HR`은 펼친 채, `Ops`는 접힌 채입니다. 그룹 열이 바뀌거나 `defaultCollapsed`/`defaultExpandLevel`을 바꾸면 모든 그룹에 다시 적용합니다. Vue/React 래퍼의 `data`는 참조로 비교하므로 같은 배열을 다시 넣으면 재로드(`data:loaded`)가 일어나지 않습니다 — 조회마다 새 배열을 넘기세요.
3277
3305
 
@@ -3751,7 +3779,9 @@ type RowData = Row;
3751
3779
  ```typescript
3752
3780
  interface ColumnDef {
3753
3781
  id: string;
3754
- label?: string; // 헤더 표시명 (FieldDef.header와 동일 역할)
3782
+ header?: string; // 헤더 표시명 (FieldDef.header가 그대로 들어감)
3783
+ /** @deprecated header 를 쓰세요 — 아직 삭제되지 않음 */
3784
+ label?: string; // FieldDef.header 값이 header 와 같이 들어감
3755
3785
  type?: 'string' | 'number' | 'boolean' | 'date' | 'array' | 'object';
3756
3786
  width?: number;
3757
3787
  minWidth?: number;
@@ -3772,6 +3802,10 @@ interface ColumnDef {
3772
3802
 
3773
3803
  > 사용자 코드에선 보통 `FieldDef`만 작성합니다. `ColumnDef`는 내부 정규화 타입이며, **`grid.columns.*` get류 API 의 반환은 `ColumnInfo`** (ColumnDef + ColumnState 합본) 입니다.
3774
3804
 
3805
+ > **표시 이름 `header` / `label`** — 플랫 `fields[].header: '부서'`는 `ColumnDef`의 `header`와 `label` 둘 다에 `'부서'`로 들어갑니다. 그리드는 헤더·그룹 배너(`부서: 개발팀`)·내보내기 등에서 **`header` → `label` → `id`** 순으로 표시 이름을 고릅니다. `ColumnDef.label`과 `ColumnInfo.label`(`grid.columns.getById()` 등의 반환)은 deprecated 입니다(아직 삭제되지 않음) — 이름을 읽을 때는 `header`를 쓰세요.
3806
+ > - `FieldDef`(`fields[]`)는 원래 `header`라 바꿀 것이 없습니다.
3807
+ > - `ColumnDef`를 직접 만들면서 `label`과 `header`를 다르게 준 경우만 화면 이름이 `header` 쪽으로 바뀝니다. 예: `{ id: 'dept', label: '부서', header: 'Dept' }` → 헤더·배너 모두 `Dept`. `label`만 준 경우(`{ id: 'dept', label: '부서' }`)는 그대로 `부서`입니다.
3808
+
3775
3809
  #### ColumnState
3776
3810
 
3777
3811
  ```typescript
@@ -3801,6 +3835,7 @@ interface ColumnInfo {
3801
3835
 
3802
3836
  // ── 데이터 타입 / 라벨 / 헤더 ──
3803
3837
  type?: ColumnType;
3838
+ /** @deprecated header 를 쓰세요 (아직 삭제되지 않음) */
3804
3839
  label?: string;
3805
3840
  /** def.header ?? def.label ?? def.id 로 합성 — 항상 비어있지 않음 */
3806
3841
  header?: string;
@@ -3873,7 +3908,8 @@ interface ColumnInfo {
3873
3908
  **충돌 키 정책**: state(런타임)가 승.
3874
3909
  - `width` / `pinned` / `visible` / `autoFit` → 현재값 (state)
3875
3910
  - `hidden` → `!visible` 로 파생
3876
- - `header` → `def.header ?? def.label ?? def.id` 로 fallback 합성 (항상 truthy)
3911
+ - `header` → `def.header ?? def.label ?? def.id` 로 fallback 합성 (항상 truthy). 그리드가 헤더·그룹 배너·내보내기 등에 쓰는 표시 이름도 같은 순서(header → label → id)입니다
3912
+ - `label` → deprecated. 같은 값이 `header`에도 있으니 `header`를 읽으세요(아직 삭제되지 않음)
3877
3913
  - `frozen` → ColumnDef 의 alias 그대로 노출 (정식 사용은 `pinned`)
3878
3914
 
3879
3915
  #### CellPosition
@@ -3939,14 +3975,20 @@ interface FilterStateBase {
3939
3975
  mask?: MaskConfig;
3940
3976
  /**
3941
3977
  * (피벗 전용) 필터 적용 단계 override. 플랫 그리드에서는 무시.
3942
- * 생략(기본) = core 자동 분류: 행/열 필드(차원) 필터 → `'pre'`(소스 축소 후 재집계 = 소계/총합까지 반영),
3943
- * 값(measure) 필터 → `'post'`(피벗된 값 기준 행 필터 + 살아남은 source 로 소계/총합 재집계).
3978
+ * 생략(기본) = `'post'`: 피벗 결과 기준으로 거른다 — 행 축(행 필드·값) 필터는 행을, 열 필드 필터는 열을
3979
+ * 줄이고 살아남은 행·열로 소계/총합을 재집계한다. `'pre'` = 원본 행을 거른 뒤 집계(소스 축소 강제).
3944
3980
  */
3945
3981
  mode?: 'pre' | 'post';
3946
3982
  /**
3947
- * (피벗 값 필터 전용) 조건을 만족하는 셀이 하나도 없는 리프 컬럼을 결과에서 제외 (기본 `false`).
3983
+ * (피벗, 행을 줄이는 필터) 남은 행에 셀이 없는 리프 컬럼을 결과에서 제외 (기본 `false`). 짝: `excludeUnmatchedRows`.
3984
+ * 값 필터는 조건을 만족하는 셀, 행 필드 필터는 원본 데이터가 있는 셀이 기준이다.
3948
3985
  */
3949
3986
  excludeUnmatchedColumns?: boolean;
3987
+ /**
3988
+ * (피벗, 열 필드 필터) 남은 열 어디에도 원본 데이터가 없는 행(과 아래가 모두 빠진 소계 행)을 결과에서 제외
3989
+ * (기본 `false`). 원본 값 0 은 데이터로 본다. 짝: `excludeUnmatchedColumns`.
3990
+ */
3991
+ excludeUnmatchedRows?: boolean;
3950
3992
  }
3951
3993
 
3952
3994
  /** 값 하나를 비교 — 스칼라 filterValue */
@@ -5236,6 +5278,7 @@ interface GroupRowContext {
5236
5278
  viewIndex: number;
5237
5279
  groupId: string; // 토글용 ID (= grid.rowGroup.getGroupRows 인자)
5238
5280
  column: string;
5281
+ columnLabel: string; // 그룹 컬럼 표시 이름(header → label → id, 헤더를 비우면 ''). showColumnLabel 과 무관하게 항상 들어 있다
5239
5282
  value: CellValue;
5240
5283
  level: number;
5241
5284
  itemCount: number;
@@ -5316,7 +5359,7 @@ interface FormatHeaderContext {
5316
5359
  interface HeaderCellInfo {
5317
5360
  /** 컬럼 ID — leaf 셀(Flat/rowHeader/center-leaf)에서만 의미. Pivot 그룹 셀은 빈 문자열 */
5318
5361
  columnId: string;
5319
- /** 표시되는 헤더 텍스트 (label ?? header ?? id 합성 — 항상 비어있지 않음) */
5362
+ /** 표시되는 헤더 텍스트 (header ?? label ?? id 합성 — 항상 비어있지 않음) */
5320
5363
  header: string;
5321
5364
  /** 헤더 셀 DOM (인터랙티브 요소 포함, 정렬 아이콘 등 자식 노드 있음) */
5322
5365
  element: HTMLElement;
@@ -436,14 +436,14 @@ customMenu: [{ id: 'wip-detail', label: '상세 보기', hidden: ({ area, cellVa
436
436
  | `isCustomState` | `boolean?` | 커스텀 빈 뷰 강제(`custom_empty` 이미지) |
437
437
  | `headerMsg` | `string?` | 헤더 텍스트 오버라이드(미지정 시 뷰 타입별 i18n 기본) |
438
438
  | `contentMsg` | `string?` | 콘텐츠 텍스트 오버라이드(`unsafeHTML`로 렌더, 미지정 시 i18n 기본) |
439
- | `useImg` | `boolean \| 'auto'` | 빈 상태 이미지 표시(기본 `true`). `false`는 문구만. `'auto'`(옵트인)는 안내 영역 높이가 그림(최소 110px)+문구(약 60px)에 모자라면 그림만 숨기고 문구는 남김. 그리드 높이가 바뀌면 다시 판정 |
439
+ | `useImg` | `boolean \| 'auto'` | 빈 상태 이미지 표시(기본 `true`). `true`·`'auto'`는 안내 영역(컬럼 헤더 아래 빈 영역) 높이가 그림(최소 110px)+문구(약 60px)에 모자라면 그림만 숨기고 문구는 남김. 그리드·헤더 높이가 바뀌면 다시 판정. `false`는 문구만. (`'auto'`는 `true`와 같으며 호환용으로 유지) |
440
440
  | `selectImg` | `EmptyStateImg?` | 명시적 이미지 선택. 행 수가 0일 때 자동 뷰 타입을 오버라이드 |
441
441
  | `noDataHeaderMsg` | `string?` | 전체 소스 0행 전용 헤더. 필터 결과만 0행인 경우에는 미적용 |
442
442
  | `noDataContentMsg` | `string?` | 전체 소스 0행 전용 콘텐츠(HTML). 필터 결과만 0행인 경우에는 미적용 |
443
443
 
444
444
  둘 중 하나라도 지정하면 전체 0행은 읽기 전용 여부와 무관하게 `data_empty` 뷰를 사용합니다. `selectImg`/`isCustomState`가 이 자동 선택보다 우선하며, 미지정 시 기존 동작을 유지합니다. `contentMsg`/`noDataContentMsg`는 `unsafeHTML` 렌더 경로이므로 **신뢰할 수 있는 HTML만** 전달하세요. 외부 입력을 그대로 전달하지 마세요.
445
445
 
446
- 예) 높이 150px 작은 그리드: `emptyState: { enabled: false }` → 0건이면 헤더만 있는 빈 표 / `emptyState: { useImg: 'auto' }` → 문구만 보이고 그림은 숨김(600px로 커지면 그림 다시 표시).
446
+ 예) 높이 150px 작은 그리드: `emptyState: { enabled: false }` → 0건이면 헤더만 있는 빈 표 / 기본값(`useImg` 미지정) → 문구만 보이고 그림은 숨김(600px로 커지면 그림 다시 표시). 안내는 컬럼 헤더 아래 영역에만 그려져 헤더를 덮지 않음.
447
447
 
448
448
  `EmptyStateImg = 'Custom' | 'Result' | 'Data' | 'Search'`
449
449
 
@@ -963,7 +963,7 @@ watch(coreConfig, (cfg) => {
963
963
  | `HeaderTemplate` | `{ field: string (필수) }` | `HeaderSlotProps = { columnId: string; header: string }` |
964
964
  | `CellEditor` | `{ field: string (필수) }` | `EditorSlotProps = { value: unknown; rowData: Record<string,unknown>; columnId: string; onCommit: (newValue: unknown) => void; onCancel: () => void }` |
965
965
  | `DetailTemplate` | `{ minHeight?: number=100; maxHeight?: number; singleExpand?: boolean=false; keepDetailRows?: boolean=true }` | `DetailSlotProps = { parentData: Record<string,unknown>; dataIndex: number }` |
966
- | `GroupTemplate` | (없음) | `GroupSlotProps = { groupKey: string; groupValue: unknown; level: number; count: number; isExpanded: boolean }` |
966
+ | `GroupTemplate` | (없음) | `GroupSlotProps = { groupKey: string; columnLabel: string; groupValue: unknown; level: number; count: number; isExpanded: boolean }` — `columnLabel` = 그룹 컬럼 표시 이름(header → label → id, 헤더를 비우면 `''`) |
967
967
 
968
968
  **Editor popup 경계:** `CellEditor`의 `EditorSlotProps`에는 `registerOwnedSurface`가 전달되지 않습니다. body/Teleport popup이 자동으로 editor 소유 영역에 등록되거나 자동 제거된다고 가정하지 마세요. 소유권 등록이 필요한 경우 [코어 custom editor 계약](reference.md#셀-에디터--editoroption--customeditorparams)에 따라 `CustomEditorParams.registerOwnedSurface?: (surface: HTMLElement) => () => void`를 실제 전달받는 core custom renderer 경로를 사용합니다. 아래는 그 경로의 수명주기 일부이며 CellEditor 슬롯 예제가 아닙니다.
969
969
 
@@ -980,7 +980,7 @@ const cleanupPopup = () => {
980
980
  새 draft validation 이벤트/구독은 이 API에 포함되지 않습니다.
981
981
 
982
982
  - `DetailTemplate`: 마운트 시 grid와 default 슬롯이 존재하면 `grid.masterDetail.setConfig`로 props 매핑(`keepDetailRows` → 코어 `keepMasterDetailRows`), 디테일 행마다 `detail:${dataIndex}` 키로 슬롯 렌더. 언마운트 시 `renderer.unmountByPrefix('detail:')`. 아래 예제는 공개 구성 형태이며 최초 mount 순서·grid 교체 시 재등록은 실행 검증 대상입니다. GroupTemplate의 준비 대기/재등록 계약을 DetailTemplate에도 동일하게 보장하지 않습니다.
983
- - `GroupTemplate`: `_groupTemplate` 이름(래퍼 예약 키)의 `formatRow` 핸들러를 등록한다. 코어는 이 핸들러에만 그룹행 대신 **보이는 그룹 배너 안의 호스트**(`span.ps-group-label.ps-group-template-host`)를 `ctx.element`로 넘기므로, 슬롯이 배너의 라벨·건수·집계 자리를 대체한다(펼침/접힘 토글 버튼은 코어가 유지, 배너 본문 클릭은 그룹 토글 — `rowGroupConfig.selectable: true`면 본문 클릭은 그룹 선택이고 토글은 ▾ 버튼만). 슬롯 요소의 이벤트(툴팁 등)는 슬롯이 그대로 받는다. 배너가 생기지 않는 컬럼 구성(가운데 컬럼이 모두 `unmergeOnGroupRow`)에서는 예전처럼 그룹행 요소에 마운트된다.
983
+ - `GroupTemplate`: `_groupTemplate` 이름(래퍼 예약 키)의 `formatRow` 핸들러를 등록한다. 코어는 이 핸들러에만 그룹행 대신 **보이는 그룹 배너 안의 호스트**(`span.ps-group-label.ps-group-template-host`)를 `ctx.element`로 넘기므로, 슬롯이 배너의 라벨·건수·집계 자리를 대체한다(펼침/접힘 토글 버튼은 코어가 유지, 배너 본문 클릭은 그룹 토글 — `rowGroupConfig.selectable: true`면 본문 클릭은 그룹 선택이고 토글은 ▾ 버튼만). 슬롯 요소의 이벤트(툴팁 등)는 슬롯이 그대로 받는다. 슬롯이 배너 내용을 대신 그리므로 `rowGroupConfig.showColumnLabel`(기본 배너의 `부서: ` 컬럼 이름)은 화면에 영향이 없다 — 이름이 필요하면 슬롯 props `columnLabel`로 직접 그린다(`<span v-if="columnLabel" class="ps-group-column-label">{{ columnLabel }}: </span>`, 클래스는 기본 배너와 같은 모양). 배너가 생기지 않는 컬럼 구성(가운데 컬럼이 모두 `unmergeOnGroupRow`)에서는 예전처럼 그룹행 요소에 마운트된다.
984
984
  - 호스트별로 인스턴스 1개(`renderer.mountInstance`)를 유지하고, 접기/펼치기·건수 변화는 props 갱신으로 반영한다. 배너가 스크롤/그룹 해제/데이터 갱신으로 사라지면 코어가 호스트에 보내는 내부 이벤트로 인스턴스를 정리한다. 그리드 준비 전에 마운트돼도 그리드 인스턴스가 준비되면 등록되고, 언마운트 시 `formatRow.removeHandler('_groupTemplate')` 후 남은 인스턴스를 정리한다.
985
985
 
986
986
  ```vue
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmscloud/moz-ui-grid-vue",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "MozGrid - Vue 3 wrapping library for @vmscloud/moz-ui-grid-core",
5
5
  "license": "ISC",
6
6
  "type": "module",