utils_dataset_init-dataset-row-tracer.js
/**
* @typedef {Object} RowTracerConfig
* @property {string} searchFn - 폼에 등록된 조회 함수의 이름. 초기화 시점에 행 추적
* 래퍼로 교체됩니다.
* 교체된 함수는 기존과 동일하게 호출할 수 있습니다.
* @property {nexacro.Grid} grid - 조회 결과가 바인딩된 그리드 컴포넌트 (binddataset
* 기반)
* @property {string[]} keys - 행 식별에 사용할 컬럼명 목록. 인덱스 0부터 순서대로
* 시도하며 hit 되면 중단합니다.
* 쉼표로 구분하여 해당 단계의 복합 키를 구성할 수 있습니다.
* 예: ["SEQ", "SNO, NAME"] → 1순위: SEQ
* 단독 / fallback: SNO + NAME 복합키
* @property {RowTracerConfig[]} [children] - 자식 조회 설정 목록. 부모 조회 중 자식 데이터셋의
* 이벤트를 차단합니다.
*/
/**
* Keys 배열을 파싱하여 우선순위별 컬럼명 그룹 배열로 반환합니다.
*
* @param {string[]} keys
* @returns {string[][]} 우선순위별 컬럼명 그룹 배열
* @example
* parseKeys(["SEQ", "SNO, NAME"]);
* // → [["SEQ"], ["SNO", "NAME"]]
*
*/
function parseKeys(keys) {
return (keys || [])
.map((k) =>
k
.split(",")
.map((s) => s.trim())
.filter(Boolean),
)
.filter((group) => group.length > 0);
}
/** 조회 함수의 앞뒤에 행 추적 로직을 삽입하는 클래스입니다. */
export class RowTracer {
/**
* @param {nexacro.Form} form
* @param {RowTracerConfig} config
* @param {RowTracer[]} children
*/
constructor(form, config, children) {
this._form = form;
this._originalFn = form[config.searchFn];
this.grid = config.grid;
this.keys = parseKeys(config.keys);
this.children = children;
this._searching = false;
// 폼의 조회 함수를 행 추적 래퍼로 교체
form[config.searchFn] = (...args) => this.search(...args);
}
/**
* 그리드에 바인딩된 데이터셋을 반환합니다.
*
* @returns {nexacro.Dataset | null}
* @access private
*/
_getDataset() {
return this.grid?.getBindDataset?.() ?? null;
}
/**
* 현재 행의 키 컬럼 값을 캡처합니다.
* 모든 우선순위 그룹의 컬럼 값을 한 번에 저장합니다.
*
* @returns {Object | null} 키-값 맵. 유효한 행이 없으면 null.
* @access private
*/
_captureState() {
const ds = this._getDataset();
if (!ds || ds.rowposition < 0) return null;
const state = {};
this.keys.forEach((keyGroup) => {
keyGroup.forEach((key) => {
state[key] = ds.getColumn(ds.rowposition, key);
});
});
return state;
}
/**
* 캡처된 키 값으로 행을 탐색합니다 (hit).
* keys 배열의 인덱스 0부터 순서대로 시도하며, 일치하는 행이 발견되면 즉시 반환합니다.
*
* @param {Object | null} state 키-값 맵
* @returns {number} 행 인덱스. 미발견 시 -1.
* @access private
*/
_hitRow(state) {
if (!state || this.keys.length === 0) return -1;
const ds = this._getDataset();
if (!ds) return -1;
for (const keyGroup of this.keys) {
const expr = keyGroup
.map((key) => `${key}=='${state[key]}'`)
.join("&&");
const row = ds.findRowExpr(expr);
if (row !== -1) return row;
}
return -1;
}
/**
* 자식 데이터셋의 이벤트를 활성화하거나 비활성화합니다.
*
* @param {boolean} enabled
* @access private
*/
_setChildrenEventEnabled(enabled) {
this.children.forEach((child) => {
const ds = child._getDataset();
if (ds) ds.enableevent = enabled;
});
}
/**
* 행 추적과 함께 조회를 실행합니다. 폼의 조회 함수 교체 후 기존 호출 방식으로 자동 실행됩니다.
*
* 실행 순서:
* 1. 현재 행 키 값 캡처
* 2. 자식 데이터셋 이벤트 차단
* 3. 원본 조회 함수 실행
* 4. 키 값으로 행 탐색 (hit)
* 5. 자식 데이터셋 이벤트 활성화
* 6. 찾은 행으로 이동 (onrowposchanged 발생 → 자식 조회 연동)
*
* @param {...*} args 원본 조회 함수에 전달할 인수
* @returns {Promise<void>}
*/
async search(...args) {
if (this._searching) return;
this._searching = true;
const ds = this._getDataset();
const state = this._captureState();
// 자기 자신 + 자식 데이터셋 이벤트 차단:
// 트랜잭션이 내부적으로 rowposition을 설정할 때 onrowposchanged가 조기 발생하는 것을 막음
if (ds) ds.enableevent = false;
this._setChildrenEventEnabled(false);
try {
await this._originalFn.call(this._form, ...args);
} finally {
const foundRow = this._hitRow(state);
this._setChildrenEventEnabled(true);
if (ds) {
// 동일한 행으로 복귀 시에도 onrowposchanged가 발생하도록 -1 선행 설정
ds.rowposition = -1;
if (ds) ds.enableevent = true;
if (foundRow !== -1) {
ds.rowposition = foundRow;
} else if (ds.rowcount > 0) {
ds.rowposition = 0;
}
}
this._searching = false;
}
}
}
/**
* @param {nexacro.Form} form
* @param {RowTracerConfig} config
* @returns {RowTracer}
*/
function buildTracer(form, config) {
const children = (config.children || []).map((child) =>
buildTracer(form, child),
);
return new RowTracer(form, config, children);
}
/**
* 폼에 등록된 조회 함수를 행 추적 래퍼로 교체합니다.
*
* 초기화 후에는 기존 방식(`this.fn_searchMain()`)으로 호출해도 행 추적 로직이 자동으로 적용됩니다.
* 조회 전 현재 행의 키 값을 기억하고, 조회 완료 후 동일한 행을 찾아 이동합니다.
* 부모 조회 중에는 자식 데이터셋의 이벤트를 차단하여 불필요한 연쇄 조회를 방지합니다.
*
* @function initRowTracer
* @param {nexacro.Form} form 현재 폼 (this)
* @param {RowTracerConfig[]} configs 추적기 설정 목록
* @returns {RowTracer[]}
* @memberof $f
* @example
* // onload에서 1회 초기화 — 이후 fn_searchMain 호출 시 행 추적이 자동 적용됨
* $f.initRowTracer(this, [
* {
* searchFn: "fn_searchMain",
* grid: this.grd_main,
* keys: ["SEQ", "SNO, NAME"],
* children: [
* {
* searchFn: "fn_searchSub",
* grid: this.grd_sub,
* keys: ["SEQ"],
* },
* ],
* },
* ]);
*
* // 기존 호출 방식 그대로 사용
* this.fn_searchMain();
*
*/
export function initDatasetRowTracer(form, configs) {
return configs.map((config) => buildTracer(form, config));
}