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));
}