modules_academic_h-cmb.js

import { generateUUIDv4 } from "../../utils/crypto/generate-uuidv4";
import { sha256 } from "../../utils/crypto/sha256";
import { getBindCellIndexWithMultiColumns } from "../../utils/grid/get-bind-cell-index-with-multi-columns";
import {
    removeRowsByCondition,
    filterRowsByCondition,
} from "../../utils/dataset/remove-rows-by-condition";
import { forEachDataset } from "../../utils/dataset/for-each-dataset";
import { getMenuId } from "../../adapters/ezworks/com-api/get-menu-id";
import { mkArg } from "../../utils/common/mk-arg";
import { YySmr } from "./yy-smr-wrapper";
import { LinkedComboManager, insertModeRow } from "../../utils/combo/linked-combo";

/**
 * @typedef hCmbOption
 * @property {nexacro.Div | nexacro.Div[] | undefined} div
 * 학사콤보를 적용할 Div 객체 혹은 그
 * 배열입니다.
 * @property {nexacro.Grid | nexacro.Grid[]} [grid]
 * 학사콤보를 적용할 Grid 객체
 * 혹은 그 배열입니다.
 * @property {HCmbOrgMode | HCmbOrgMode[] | HCmbComboMode | HCmbComboMode[]}
 * [orgMode]
 * 학사콤보의 콤보 모드. 문자열로 설정시 모든 모드가 동일하게 적용됩니다. (예: "a")
 * @property {HCmbOrgMode | HCmbOrgMode[] | HCmbComboMode | HCmbComboMode[]}
 * [orgGridMode]
 * 그리드에서 사용하는 조직 모드. 문자열로 설정시 모든 모드가 동일하게 적용됩니다. (예: "s")
 * @property {FilterRowsByCondtionFunc} [orgFilter]
 * 조직 콤보 필터를 위한 함수
 * @property {HCmbComboMode} [smrMode]
 * 학사콤보의 학기 콤보 모드
 * @property {HCmbComboMode} [smrGridMode]
 * 그리드에서 사용하는 학사콤보의 학기 콤보 모드
 * @property {FilterRowsByCondtionFunc} [smrFilter]
 * 학기 콤보 필터를 위한 함수
 * @property {FilterRowsByCondtionFunc} [smrGridFilter]
 * 그리드에서 사용하는 학기 콤보 필터를 위한 함수
 * @property {string | string[]} [smrAliasId]
 * 학기 콤보의 표시 타입, 전역 설정에서 hCmbSmrAlias 설정을 통해 참조합니다.
 * @property {string | string[]} [smrTypeId]
 * 학기 콤보의 타입, 전역 설정에서 hCmbSmrType 설정을 통해 참조합니다.
 * @property {string | string[]} [smrGridTypeId]
 * 학기 콤보의 타입, 전역 설정에서 hCmbSmrType 설정을 통해 참조합니다.
 * @property {HCmbTargetComponentName} [targtCompNm]
 * 학사콤보를 적용할 컴포넌트를 판별하는 정규식을 재정의
 * 합니다.
 * @property {boolean} [useMngtDpmj=false]
 * **@deprecated** 이 함수는 추가로 사용하지 마세요. orgDetUse으로 대체되었습니다.
 * 조직 데이터를 관리학과 기준으로 조회합니다.
 * @property {string} [fcltGrscDstnct]
 * 학부대학원구분 (공통코드: 1715) (생략 시 메뉴관리의 데이터를 사용)
 * @property {string} [orgDetUse]
 * 조직세부사용 (공통코드: 2002) (생략 시 메뉴관리의 데이터를 사용)
 * @property {boolean} [useDataAuth]
 * 데이터권한여부 (생략 시 메뉴관리의 데이터를 사용)
 * @property {number} [smrOffset]
 * 학기 설정 값을 기준 연도 학기에서 더하거나 뺄 수
 */

/**
 * @typedef HCmbComboComponentSet
 * @property {nexacro.Combo} cmb_fcltDvcd
 * @property {nexacro.Combo} cmb_univCd
 * @property {nexacro.Combo} cmb_dpmjCd
 * @property {nexacro.Combo} cmb_majrCd
 * @property {nexacro.Combo} cmb_detMajrCd
 * @access private
 */

/**
 * 콤보의 최상위 빈 값에 사용되는 명칭을 구분하는 플래그 입니다.
 *
 * 'a': 전체
 *
 * 's': 학과
 *
 * 'n': 추가 안함
 *
 * @typedef {'a' | 's' | 'n'} HCmbComboMode
 */

/**
 * 그리드의 각 조직별 콤보 mode 값
 *
 * @typedef HCmbOrgMode
 * @property {HCmbComboMode} [fclt]     대학/대학원 모드
 * @property {HCmbComboMode} [univ]     대학 모드
 * @property {HCmbComboMode} [dpmj]     학과 모드
 * @property {HCmbComboMode} [majr]     전공 모드
 * @property {HCmbComboMode} [detMajr]  전공 세부 모드
 */

/** 학사콤보 클래스 */
export class HCmb {
    static LOG_KEY = "HCmb";

    static EXIST_YY = 1;
    static EXIST_SMR = 2;
    static EXIST_FCLT = 4;
    static EXIST_UNIV = 8;
    static EXIST_DPMJ = 16;
    static EXIST_MAJR = 32;
    static EXIST_DET_MAJR = 64;
    static EXIST_YY_SMR = HCmb.EXIST_YY | HCmb.EXIST_SMR;
    static EXIST_ORG =
        HCmb.EXIST_FCLT |
        HCmb.EXIST_UNIV |
        HCmb.EXIST_DPMJ |
        HCmb.EXIST_MAJR |
        HCmb.EXIST_DET_MAJR;

    constructor(form) {
        this.fm = form;
        this.UNIQUE_KEY = sha256(generateUUIDv4()).toString();
        this.cmbLockKeys = new Set();
        this.oldValue = ""; // 그리드 grdOldValue 저장용

        const globalConfig = nexacro.EzFoundation?.hCmbTargetComponentName;

        // 대상 컴포넌트 판별 정규식
        this.targetCompNm = {
            yyCompNm: globalConfig?.yyCompNm || /^spn_(?:grdu)?[yY]y$/,
            smrCompNm: globalConfig?.smrCompNm || /^cmb_(?:grdu)?[sS]mr$/,
            fcltCompNm: globalConfig?.fcltCompNm || /^cmb_fcltDvcd$/,
            univCompNm: globalConfig?.univCompNm || /^cmb_univCd$/,
            dpmjCompNm: globalConfig?.dpmjCompNm || /^cmb_dpmjCd$/,
            majrCompNm: globalConfig?.majrCompNm || /^cmb_majrCd$/,
            detMajrCompNm: globalConfig?.detMajrCompNm || /^cmb_detMajrCd$/,
            smrColNm: globalConfig?.smrColNm || ["SMR", "GRDU_SMR"],
            fcltColNm: globalConfig?.fcltColNm || "FCLT_DVCD",
            univColNm: globalConfig?.univColNm || "UNIV_CD",
            dpmjColNm: globalConfig?.dpmjColNm || "DPMJ_CD",
            majrColNm: globalConfig?.majrColNm || "MAJR_CD",
            detMajrColNm: globalConfig?.detMajrColNm || "DET_MAJR_CD",
        };
    }

    get taskDtlCd() {
        const openMenuDs = nexacro.getApplication().gds_openMenu;
        return openMenuDs.getColumn(openMenuDs.rowposition, "TASK_DTL_CD");
    }

    /**
     * 학사콤보 초기화
     *
     * @param {hCmbOption} option
     * @returns {Promise<never> | Promise<void>}
     */
    async init(option) {
        if (!option) return Promise.reject(Error("인자가 부족합니다."));

        this.parseOption(option);

        this.discoverTargetComponent();

        await this.searchData();
        this.initYySmrValue();
        await this.makeInitDs();
        await this.initComp();
    }

    //
    /**
     * 받은 이름에 고유 키를 붙여서 반환합니다.
     *
     * @param {string} name  명칭
     * @returns {string} 고유 키가 붙은 명칭
     * @access private
     */
    unique(name) {
        return name + "_" + this.UNIQUE_KEY;
    }

    /**
     * Option을 해석합니다.
     *
     * @param {hCmbOption} option
     * @access private
     */
    parseOption(option) {
        // arrDiv
        this.arrDiv = option.div || [];
        if (this.arrDiv && !(this.arrDiv instanceof Array)) {
            this.arrDiv = [this.arrDiv];
        }

        this.arrGrid = option.grid || [];
        if (this.arrGrid && !(this.arrGrid instanceof Array)) {
            this.arrGrid = [this.arrGrid];
        }

        // 조직 콤보 모드
        let orgMode = option.orgMode || [];
        if (orgMode && !(orgMode instanceof Array)) {
            orgMode = [orgMode];
        }
        /**
         * @type {HCmbOrgMode[]}
         */
        this.orgMode = orgMode.map((mode) => {
            if (typeof mode === "string") {
                const modeObj = {
                    fclt: mode,
                    univ: mode,
                    dpmj: mode,
                    majr: mode,
                    detMajr: mode,
                };
                return modeObj;
            } else if (typeof mode === "object") {
                return mode;
            } else {
                return {};
            }
        });

        // 그리드 조직 콤보 모드
        let orgGridMode = option.orgGridMode || [];
        if (orgGridMode && !(orgGridMode instanceof Array)) {
            orgGridMode = [orgGridMode];
        }

        /**
         * @type {HCmbOrgMode[]}
         */
        this.orgGridMode = orgGridMode.map((mode) => {
            if (typeof mode === "string") {
                return {
                    fclt: mode,
                    univ: mode,
                    dpmj: mode,
                    majr: mode,
                    detMajr: mode,
                };
            } else if (typeof mode === "object") {
                return mode;
            } else {
                return {};
            }
        });

        this.orgFilter = option.orgFilter || null;

        // 학기 모드
        this.smrMode = option.smrMode || [];
        if (this.smrMode && !(this.smrMode instanceof Array)) {
            this.smrMode = [this.smrMode];
        }

        // 그리드 학기 모드
        this.smrGridMode = option.smrGridMode || [];
        if (this.smrGridMode && !(this.smrGridMode instanceof Array)) {
            this.smrGridMode = [this.smrGridMode];
        }

        // 학기 별칭 아이디
        this.smrAliasId = option.smrAliasId || [];
        if (this.smrAliasId && !(this.smrAliasId instanceof Array)) {
            this.smrAliasId = [this.smrAliasId];
        }

        // 학기 타입 아이디
        this.smrTypeId = option.smrTypeId || [];
        if (this.smrTypeId && !(this.smrTypeId instanceof Array)) {
            this.smrTypeId = [this.smrTypeId];
        }

        // 그리드 학기 타입 아이디
        this.smrGridTypeId = option.smrGridTypeId || [];
        if (this.smrGridTypeId && !(this.smrGridTypeId instanceof Array)) {
            this.smrGridTypeId = [this.smrGridTypeId];
        }

        // 학기 필터
        this.smrFilter = option.smrFilter || [];
        if (this.smrFilter && !(this.smrFilter instanceof Array)) {
            this.smrFilter = [this.smrFilter];
        }

        // 그리드 학기 필터
        this.smrGridFilter = option.smrGridFilter || [];
        if (this.smrGridFilter && !(this.smrGridFilter instanceof Array)) {
            this.smrGridFilter = [this.smrGridFilter];
        }

        // 관리학과 사용
        this.useMngtDpmj = option.useMngtDpmj || false;

        // 학부/대학원 구분
        this.fcltGrscDstnct = option.fcltGrscDstnct;

        // 조직 세부 사용
        this.orgDetUse = option.orgDetUse;

        // 데이터권한사용
        this.useDataAuth = option.useDataAuth;

        /**
         * 컴포넌트 명칭 재정의
         *
         * @type {HCmbTargetComponentName}
         * @access private
         */
        this.targetCompNm = Object.assign(
            this.targetCompNm,
            option.targtCompNm,
        );

        // 학기 오프셋
        this.smrOffset = option.smrOffset;
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isYyComponent(comp) {
        return this.targetCompNm.yyCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isSmrComponent(comp) {
        return this.targetCompNm.smrCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isFcltComponent(comp) {
        return this.targetCompNm.fcltCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isUnivComponent(comp) {
        return this.targetCompNm.univCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isDpmjComponent(comp) {
        return this.targetCompNm.dpmjCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isMajrComponent(comp) {
        return this.targetCompNm.majrCompNm.test(comp?.name);
    }

    /**
     * @param {nexacro.Component} comp  검사 대상 컴포넌트
     * @returns {boolean}
     * @access private
     */
    isDetMajrComponent(comp) {
        return this.targetCompNm.detMajrCompNm.test(comp?.name);
    }

    /**
     * 학사 콤보의 대상 컴포넌트를 찾아 멤버 변수에 저장한다.
     *
     * @access private
     */
    discoverTargetComponent() {
        // 일반 컴포넌트
        const comp = this.arrDiv?.reduce((acc, div) => {
            // 컴포넌트를 반복하여 대상 컴포넌트를 찾는다.
            return (
                acc |
                Array.from(div.form.all).reduce((acc, comp) => {
                    // 대상 컴포넌트 검사
                    if (this.isYyComponent(comp)) {
                        acc |= HCmb.EXIST_YY;
                    } else if (this.isSmrComponent(comp)) {
                        acc |= HCmb.EXIST_SMR;
                    } else if (this.isFcltComponent(comp)) {
                        acc |= HCmb.EXIST_FCLT;
                    } else if (this.isUnivComponent(comp)) {
                        acc |= HCmb.EXIST_UNIV;
                    } else if (this.isDpmjComponent(comp)) {
                        acc |= HCmb.EXIST_DPMJ;
                    } else if (this.isMajrComponent(comp)) {
                        acc |= HCmb.EXIST_MAJR;
                    } else if (this.isDetMajrComponent(comp)) {
                        acc |= HCmb.EXIST_DET_MAJR;
                    }

                    return acc;
                }, 0)
            );
        }, 0);

        // 그리드 컬럼
        const grid = this.arrGrid?.reduce((acc, grid) => {
            [
                [this.targetCompNm.smrColNm, HCmb.EXIST_SMR],
                [this.targetCompNm.fcltColNm, HCmb.EXIST_FCLT],
                [this.targetCompNm.univColNm, HCmb.EXIST_UNIV],
                [this.targetCompNm.dpmjColNm, HCmb.EXIST_DPMJ],
                [this.targetCompNm.majrColNm, HCmb.EXIST_MAJR],
                [this.targetCompNm.detMajrColNm, HCmb.EXIST_DET_MAJR],
            ].forEach(([colNm, flag]) => {
                const colIndex = getBindCellIndexWithMultiColumns(
                    "body",
                    grid,
                    colNm,
                );
                if (colIndex !== -1) {
                    acc |= flag;
                }
            });

            return acc;
        }, 0);

        this.existComp = comp | grid;
    }

    /**
     * 데이터를 검색합니다.
     *
     * @returns {Promise}
     * @access private
     */
    searchData() {
        let DATA_AUTH_XCPT_YN = null;

        switch (this.useDataAuth) {
            case true:
                DATA_AUTH_XCPT_YN = "0";
                break;
            case false:
                DATA_AUTH_XCPT_YN = "1";
                break;
        }

        let FCLT_DVCD = this.fcltGrscDstnct ?? null;
        let ORG_DET_USE_DVCD = this.orgDetUse ?? null;

        const args = mkArg({
            MENU_ID: getMenuId(this.fm),
            TASK_DTL_CD: this.taskDtlCd,
            DATA_AUTH_XCPT_YN,
            FCLT_DVCD,
            ORG_DET_USE_DVCD,
        });

        this._ds_yy = new Dataset("_ds_yy");
        this._ds_smr = new Dataset("_ds_smr");
        this._ds_org = new Dataset("_ds_org");
        this.fm.addChild(this.unique("_ds_yy"), this._ds_yy);
        this.fm.addChild(this.unique("_ds_smr"), this._ds_smr);
        this.fm.addChild(this.unique("_ds_org"), this._ds_org);

        const dataList = [
            {
                sqlId: nexacro.EzFoundation.hCmbYySqlId || "hcmb.s01",
                outDs: this.unique("_ds_yy"),
            },
            {
                sqlId: nexacro.EzFoundation.hCmbSmrSqlId || "hcmb.s02",
                outDs: this.unique("_ds_smr"),
            },
            {
                sqlId: this.useMngtDpmj
                    ? nexacro.EzFoundation.hCmbMngtDpmjSqlId || "@hcmb.s04"
                    : nexacro.EzFoundation.hCmbOrgSqlId || "hcmb.03",
                outDs: this.unique("_ds_org"),
            },
        ];

        return this.fm.tx.search({
            action: "basic",
            svcId: this.unique("getYy"),
            sqlId: dataList.map((v) => v.sqlId).join(" "),
            outDs: dataList.map((v) => v.outDs).join(" "),
            param: args,
        });
    }

    /**
     * 대학, 학과, 전공 데이터셋을 만든다.
     *
     * @access private
     */
    makeInitDs() {
        // 세부 전공 없을 시 행 삭제
        if ((HCmb.EXIST_DET_MAJR & this.existComp) === 0) {
            removeRowsByCondition(
                this._ds_org,
                (ctx) => {
                    return (ctx.rowData["DET_MAJR_CD"] ?? "") !== "";
                },
                { mutate: true },
            );
        }

        this._ds_fclt = filterRowsByCondition(this._ds_org, this.orgFilter, {
            mutate: false,
        });
        this._ds_univ = filterRowsByCondition(this._ds_org, this.orgFilter, {
            mutate: false,
        });
        this._ds_dpmj = filterRowsByCondition(this._ds_org, this.orgFilter, {
            mutate: false,
        });
        this._ds_majr = filterRowsByCondition(this._ds_org, this.orgFilter, {
            mutate: false,
        });
        this._ds_detMajr = filterRowsByCondition(this._ds_org, this.orgFilter, {
            mutate: false,
        });

        this.fm.addChild(this.unique("_ds_fcltDvcd"), this._ds_fclt);
        this.fm.addChild(this.unique("_ds_univ"), this._ds_univ);
        this.fm.addChild(this.unique("_ds_dpmj"), this._ds_dpmj);
        this.fm.addChild(this.unique("_ds_majr"), this._ds_majr);
        this.fm.addChild(this.unique("_ds_detMajr"), this._ds_detMajr);

        // 데이터셋 그룹바이
        this.groupByOrgDs(this._ds_fclt, "FCLT_DVCD", "ORD");
        this.groupByOrgDs(this._ds_univ, "UNIV_CD", "ORD");
        this.groupByOrgDs(this._ds_dpmj, "DPMJ_CD", "ORD");
        this.groupByOrgDs(this._ds_majr, "MAJR_CD", "ORD");
        this.groupByOrgDs(this._ds_detMajr, "DET_MAJR_CD", "ORD");

        // ALL_PATH 변경
        this.hotfixAllPath(this._ds_dpmj, 2);
    }

    /**
     * ALL_PATH 컬럼이 맞지 않는 것을 임시로 핫 픽스함.
     *
     * @access private
     */
    hotfixAllPath(ds, maxSep) {
        const sep = " > ";

        forEachDataset(ds, (ctx) => {
            const allPath = ctx.rowData["ALL_PATH"];

            if (allPath) {
                const pathList = allPath.split(sep);
                const newPath = pathList.slice(0, maxSep).join(sep);
                ctx.rowData["ALL_PATH"] = newPath;
                ctx.dataset.setColumn(ctx.row, "ALL_PATH", newPath);
            }
        });
    }

    /**
     * 데이터셋 그룹바이
     *
     * @param {nexacro.Dataset} sDs
     * @param {string} colNm
     * @param {string} sortColNm
     * @param {string[]} [childColIdList]
     * @access private
     */
    groupByOrgDs(sDs, colNm, sortColNm, childColIdList) {
        sDs.enableevent = false;
        var initVal = null;
        sDs.keystring = `S:${colNm}-ORD`;

        removeRowsByCondition(
            sDs,
            (ctx) => {
                // 하위 데이터가 undefined가 아닌 경우 행 삭제
                if (
                    childColIdList &&
                    childColIdList.some(
                        (colId) => ctx.rowData[colId] !== undefined,
                    )
                ) {
                    return true;
                }

                if (!ctx.rowData[colNm]) {
                    return true;
                } else if (initVal !== ctx.rowData[colNm]) {
                    initVal = ctx.rowData[colNm];
                    return false;
                } else {
                    return true;
                }
            },
            { mutate: true },
        );

        sDs.keystring = `S:${sortColNm}`;
        sDs.enableevent = true;
        sDs.applyChange();
    }

    /**
     * 연도, 학기 초기화
     *
     * @access private
     */
    initYySmrValue() {
        // 연도 초기화
        const originYy = this._ds_yy.getColumn(0, "YY");

        // 학기 초기화
        const originSmr = this._ds_yy.getColumn(0, "SMR");

        // 연도 학기 초기화
        let yySmr = new YySmr(originYy, originSmr);

        if (this.smrOffset !== undefined) {
            yySmr = yySmr.add(this.smrOffset);
        } else {
            nexacro.EzFoundation.hCmbSmrOffsetRule.forEach((rule) => {
                if (rule.condition({ form: this.fm })) {
                    yySmr = yySmr.add(rule.offset);
                }
            });
        }

        this.yy = yySmr.yy;
        this.smr = yySmr.smr;
    }

    /**
     * 연도 컴포넌트 초기화
     *
     * @param {nexacro.Spin} comp
     * @access private
     */
    initYySpin(comp) {
        comp.value = this.yy;
    }

    /**
     * 학기 별칭을 분석하여 가져옵니다.
     *
     * @param {nexacro.Component} comp
     * 넥사크로 컴포넌트
     * @param {string} [id]
     * 키 지정
     * @param {string} [gridBindColId]
     * @returns {HCmbSmrAliasItem[]}
     * 별칭 목록
     * @access private
     */
    selectSmrAlias({ comp, id, gridBindColId }) {
        let result = [];

        if (!nexacro.EzFoundation?.hCmbSmrAlias) {
            return [];
        }

        if (id) {
            // ID 기반 탐색
            for (let alias of nexacro.EzFoundation.hCmbSmrAlias) {
                if (id === alias.id) {
                    result = alias.aliases;
                    break;
                }
            }
        } else {
            // 조건 기반 탐색
            for (let alias of nexacro.EzFoundation.hCmbSmrAlias) {
                const isCombo = comp instanceof nexacro.Combo;
                const isGrid = comp instanceof nexacro.Grid;

                if (
                    alias.condition({
                        form: this.fm,
                        component: comp,
                        isCombo,
                        isGrid,
                        gridBindColId,
                    })
                ) {
                    result = alias.aliases;
                    break;
                }
            }
        }

        return result;
    }

    /**
     * 학기 타입을 분석하여 가져옵니다.
     *
     * @param {nexacro.Component} comp
     * 넥사크로 컴포넌트
     * @param {string} [id]
     * 키 지정
     * @param {string} [gridBindColId]
     * @returns {HCmbSmrTypeConfig}
     * 별칭 목록
     * @access private
     */
    selectSmrType({ comp, id, gridBindColId }) {
        let result = {};

        if (!nexacro.EzFoundation?.hCmbSmrType) {
            return {};
        }

        if (id) {
            // ID 기반 탐색
            for (let type of nexacro.EzFoundation.hCmbSmrType) {
                if (id === type.id) {
                    result = type;
                    break;
                }
            }
        } else {
            // 조건 기반 탐색
            for (let type of nexacro.EzFoundation.hCmbSmrType) {
                const isCombo = comp instanceof nexacro.Combo;
                const isGrid = comp instanceof nexacro.Grid;

                if (
                    type.condition({
                        form: this.fm,
                        component: comp,
                        isCombo,
                        isGrid,
                        gridBindColId,
                    })
                ) {
                    result = type;
                    break;
                }
            }
        }

        return result;
    }

    /**
     * 학기 컴포넌트 초기화
     *
     * @param {nexacro.Combo} comp
     * @param {number}        divIdx
     * @access private
     */
    initSmrCombo(comp, divIdx) {
        const smrFilter = this.smrFilter[divIdx];
        const smrMode = this.smrMode[divIdx] || "n";

        const smrType = this.selectSmrType({
            comp,
            id: this.smrTypeId[divIdx],
        });

        // 학기 타입 기본 필터
        this._ds_smr_c = filterRowsByCondition(this._ds_smr, smrType.filter, {
            mutate: false,
        });

        // 학기 추가 필터
        filterRowsByCondition(this._ds_smr_c, smrFilter, {
            mutate: true,
        });

        // 컴포넌트 추가
        this.fm.addChild(
            this.unique("_ds_smr_c") + divIdx + "_" + comp.id,
            this._ds_smr_c,
        );

        // 학기 콤보 모드 추가
        insertModeRow(this._ds_smr_c, ["SMR_NM"], smrMode, {
            insertNewRow: false,
        });

        // 별칭 처리
        const smrAlias = this.selectSmrAlias({
            comp,
            id: this.smrAliasId[divIdx],
        });

        smrAlias.forEach((item) => {
            const foundRow = this._ds_smr_c.findRow("SMR_CD", item.smrCd);
            if (foundRow === -1) {
                return;
            }
            this._ds_smr_c.setColumn(foundRow, "SMR_NM", item.smrNm);
        });

        comp.innerdataset = this._ds_smr_c;
        comp.codecolumn = "SMR_CD";
        comp.datacolumn = "SMR_NM";

        // 기준학기
        const baseSmr = this.smr;

        // 기본 세팅 값 처리
        if (this._ds_smr_c.findRow("SMR_CD", baseSmr) !== -1) {
            // 기준 학기 데이터를 찾을 경우 설정
            comp.value = baseSmr;
        } else if (
            typeof smrType.failValue === "string" &&
            this._ds_smr_c.findRow("SMR_CD", smrType.failValue) !== -1
        ) {
            // 문자열로 지정된 경우
            comp.value = smrType.failValue;
        } else if (typeof smrType.failValue === "function") {
            const val = this._ds_smr_c.findRow("SMR_CD", smrType.failValue());

            if (val !== -1) {
                comp.value = value;
            } else {
                comp.index = 0;
            }
        } else {
            comp.index = 0;
        }
    }

    /**
     * 그리드의 학기 콤보를 초기화 한다.
     *
     * @param {nexacro.Grid} grid
     * @param {number}       gridIdx
     * @access private
     */
    initGridSmrCombo(grid, gridIdx) {
        const smrFilter = this.smrGridFilter[gridIdx] || "";
        const smrMode = this.smrGridMode[gridIdx] || "s";
        const bindDsNm = this.unique("_ds_smr_g") + gridIdx + "_" + grid.id;

        const dsIdx = getBindCellIndexWithMultiColumns(
            "body",
            grid,
            this.targetCompNm.smrColNm,
        );

        if (dsIdx === -1) {
            return;
        }

        // 타입 필터링
        const smrType = this.selectSmrType({
            comp: grid,
            id: this.smrGridTypeId[gridIdx],
            gridBindColId: grid
                .getCellProperty("body", dsIdx, "text")
                .replace("bind:", ""),
        });

        // 학기 타입 기본 필터
        this._ds_smr_g = filterRowsByCondition(this._ds_smr, smrType.filter, {
            mutate: false,
        });

        // 학기 추가 필터
        filterRowsByCondition(this._ds_smr_g, smrFilter, {
            mutate: true,
        });

        this.fm.addChild(bindDsNm, this._ds_smr_g);

        // 학기 콤보 모드 추가
        insertModeRow(this._ds_smr_g, ["SMR_NM"], smrMode, {
            insertNewRow: false,
        });

        // 별칭 처리
        const smrAlias = this.selectSmrAlias({
            comp: grid,
            id: this.smrAliasId[gridIdx],
            gridBindColId: grid
                .getCellProperty("body", dsIdx, "text")
                .replace("bind:", ""),
        });

        smrAlias.forEach((item) => {
            const foundRow = this._ds_smr_g.findRow("SMR_CD", item.smrCd);
            if (foundRow === -1) {
                return;
            }
            this._ds_smr_g.setColumn(foundRow, "SMR_NM", item.smrNm);
        });

        if (dsIdx !== -1) {
            grid.setCellProperty("body", dsIdx, "combodataset", bindDsNm);
            grid.setCellProperty("body", dsIdx, "combocodecol", "SMR_CD");
            grid.setCellProperty("body", dsIdx, "combodatacol", "SMR_NM");
        }
    }

    /**
     * 컴포넌트 초기화
     *
     * @access private
     */
    initComp() {
        this.arrDiv.map((div, i) => {
            /**
             * @type {HCmbComboComponentSet}
             */
            const cmbComp = {};

            Array.from(div.form.all).map((v) => {
                if (this.isYyComponent(v)) {
                    this.initYySpin(v);
                } else if (this.isSmrComponent(v)) {
                    this.initSmrCombo(v, i);
                } else if (this.isUnivComponent(v)) {
                    cmbComp.cmb_univCd = v;
                    this.overrideOrgCompProperty(v);
                } else if (this.isDpmjComponent(v)) {
                    cmbComp.cmb_dpmjCd = v;
                    this.overrideOrgCompProperty(v);
                } else if (this.isMajrComponent(v)) {
                    cmbComp.cmb_majrCd = v;
                    this.overrideOrgCompProperty(v);
                } else if (this.isDetMajrComponent(v)) {
                    cmbComp.cmb_detMajrCd = v;
                    this.overrideOrgCompProperty(v);
                } else if (this.isFcltComponent(v)) {
                    cmbComp.cmb_fcltDvcd = v;
                }
            }, this);

            this.makeCmbDpmj(cmbComp, this.orgMode[i], i, {
                showAbl:
                    /div_WFSA_bg/.test(div.cssclass) ||
                    div.name === "div_search",
            });
        }, this);

        // 그리드
        this.arrGrid.map((grid, i) => {
            this.initGridSmrCombo(grid, i);
            this.makeGrdDpmj(grid, i);
        });
    }

    attachAblText(ds, dataCol, useYnCol) {
        forEachDataset(ds, (ctx) => {
            if (ctx.rowData[useYnCol] === "0") {
                ds.setColumn(ctx.row, dataCol, `${ctx.rowData[dataCol]}(폐지)`);
            }
        });
    }

    /**
     * @typedef {Object} hCmbMakeCmbOption
     * @property {boolean} [showAbl=false]  폐지 표시 여부
     * @access private
     */

    /**
     * 대학/조직/학과 콤보 생성
     *
     * @param {HCmbComboComponentSet} cmbComp
     * @param {HCmbOrgMode} mode
     * @param idx
     * @param {hCmbMakeCmbOption} [option]
     * @returns {void}
     * @access private
     */
    makeCmbDpmj(cmbComp, mode = {}, idx, { showAbl = false } = {}) {
        const {
            fclt: fcltMode = "a",
            univ: univMode = "a",
            dpmj: dpmjMode = "a",
            majr: majrMode = "a",
            detMajr: detMajrMode = "a",
        } = mode;

        // 대학원/대학 구분
        if (cmbComp.cmb_fcltDvcd) {
            const ds_fclt = $ds.clone(this._ds_fclt);
            this.fm.addChild(this.unique("_ds_fclt_c_") + idx, ds_fclt);
            cmbComp.cmb_fcltDvcd.innerdataset = ds_fclt;
            cmbComp.cmb_fcltDvcd.codecolumn = "FCLT_DVCD";
            cmbComp.cmb_fcltDvcd.datacolumn = "FCLT_DVNM";
        }

        // 대학
        if (cmbComp.cmb_univCd) {
            const ds_univ = $ds.clone(this._ds_univ);
            showAbl && this.attachAblText(ds_univ, "UNIV_NM", "UNIV_USE_YN");
            this.fm.addChild(this.unique("_ds_univ_c_") + idx, ds_univ);
            cmbComp.cmb_univCd.innerdataset = ds_univ;
            cmbComp.cmb_univCd.codecolumn = "UNIV_CD";
            cmbComp.cmb_univCd.datacolumn = "UNIV_NM";
        }

        // 학과
        if (cmbComp.cmb_dpmjCd) {
            const ds_dpmj = $ds.clone(this._ds_dpmj);
            showAbl && this.attachAblText(ds_dpmj, "DPMJ_NM", "DPMJ_USE_YN");
            this.fm.addChild(this.unique("_ds_dpmj_c_") + idx, ds_dpmj);
            cmbComp.cmb_dpmjCd.innerdataset = ds_dpmj;
            cmbComp.cmb_dpmjCd.codecolumn = "DPMJ_CD";
            cmbComp.cmb_dpmjCd.datacolumn = "DPMJ_NM";
        }

        // 전공
        if (cmbComp.cmb_majrCd) {
            const ds_majr = $ds.clone(this._ds_majr);
            showAbl && this.attachAblText(ds_majr, "MAJR_NM", "MAJR_USE_YN");
            this.fm.addChild(this.unique("_ds_majr_c_") + idx, ds_majr);
            cmbComp.cmb_majrCd.innerdataset = ds_majr;
            cmbComp.cmb_majrCd.codecolumn = "MAJR_CD";
            cmbComp.cmb_majrCd.datacolumn = "MAJR_NM";
        }

        // 세부 전공
        if (cmbComp.cmb_detMajrCd) {
            const ds_detMajr = $ds.clone(this._ds_detMajr);
            showAbl &&
                this.attachAblText(
                    ds_detMajr,
                    "DET_MAJR_NM",
                    "DET_MAJR_USE_YN",
                );
            this.fm.addChild(this.unique("_ds_detMajr_c_") + idx, ds_detMajr);
            cmbComp.cmb_detMajrCd.innerdataset = ds_detMajr;
            cmbComp.cmb_detMajrCd.codecolumn = "DET_MAJR_CD";
            cmbComp.cmb_detMajrCd.datacolumn = "DET_MAJR_NM";
        }

        // LinkedComboManager 인스턴스 생성 및 노드 등록
        const comboManager = new LinkedComboManager();

        const possibleNodes = [
            {
                id: "fclt",
                component: cmbComp.cmb_fcltDvcd,
                codeField: "FCLT_DVCD",
                textField: "FCLT_DVNM",
                parents: [],
                mode: fcltMode,
            },
            {
                id: "univ",
                component: cmbComp.cmb_univCd,
                codeField: "UNIV_CD",
                textField: "UNIV_NM",
                parents: ["fclt"],
                mode: univMode,
            },
            {
                id: "dpmj",
                component: cmbComp.cmb_dpmjCd,
                codeField: "DPMJ_CD",
                textField: "DPMJ_NM",
                parents: ["univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: dpmjMode,
            },
            {
                id: "majr",
                component: cmbComp.cmb_majrCd,
                codeField: "MAJR_CD",
                textField: "MAJR_NM",
                parents: ["dpmj", "univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: majrMode,
            },
            {
                id: "detMajr",
                component: cmbComp.cmb_detMajrCd,
                codeField: "DET_MAJR_CD",
                textField: "DET_MAJR_NM",
                parents: ["dpmj", "univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: detMajrMode,
            },
        ];

        const parentCodeFields = {
            fclt: null,
            univ: "FCLT_DVCD",
            dpmj: { univ: "UNIV_CD", fclt: "FCLT_DVCD" },
            majr: { dpmj: "DPMJ_CD", univ: "UNIV_CD", fclt: "FCLT_DVCD" },
            detMajr: { dpmj: "DPMJ_CD", univ: "UNIV_CD", fclt: "FCLT_DVCD" },
        };

        const activeNodes = possibleNodes.filter((node) => node.component);
        const activeIds = activeNodes.map((node) => node.id);

        activeNodes.forEach((node) => {
            const parentId =
                node.parents.find((p) => activeIds.includes(p)) || null;

            comboManager.addNode({
                id: node.id,
                parentId: parentId,
                component: node.component,
                codeField: node.codeField,
                textField: node.textField,
                parentCodeField: parentCodeFields[node.id],
                allPathField: node.allPathField,
                mode: node.mode,
            });
        });

        comboManager.attachEvents();

        // addNode에서 mode row 삽입 후 첫 번째 항목 선택
        if (cmbComp.cmb_fcltDvcd) cmbComp.cmb_fcltDvcd.index = 0;
        if (cmbComp.cmb_univCd) cmbComp.cmb_univCd.index = 0;
        if (cmbComp.cmb_dpmjCd) cmbComp.cmb_dpmjCd.index = 0;
        if (cmbComp.cmb_majrCd) cmbComp.cmb_majrCd.index = 0;
        if (cmbComp.cmb_detMajrCd) cmbComp.cmb_detMajrCd.index = 0;

        if (!this.comboManagers) {
            this.comboManagers = [];
        }
        this.comboManagers.push(comboManager);
    }

    /**
     * 학사 콤보에 조직명과 ALL_PATH의 데이터를 변경합니다.
     *
     * @param {nexacro.Dataset} ds          변경할 데이터셋
     * @param {string}          colNm       변경할 컬럼명
     * @param {string}          changColNm  변경대상의 컬럼명
     * @access private
     */
    hCmbChangColVal(ds, colNm, changColNm) {
        //그리드쪽에 바인딩 컬럼명을 변경하는순간 검색어가 날라가는 현상이 있어 그리드는 건들지 않으면서 안에 데이터만 변경함d
        ds.enableevent = false;
        for (var i = 0; i < ds.rowcount; i++) {
            let cn = ds.getColumn(i, colNm);
            let ccn = ds.getColumn(i, changColNm);
            ds.setColumn(i, colNm, ccn);
            ds.setColumn(i, changColNm, cn);
        }
        ds.enableevent = true;
    }

    /**
     * 그리드 학과 콤보 생성
     *
     * @param {nexacro.Grid} grid
     * @param {number}       idx
     * @access private
     */
    makeGrdDpmj(grid, idx) {
        const comboConfig = [
            {
                // 대학
                ds: this._ds_fclt,
                dsNm: "_ds_fclt",
                mode: this.orgGridMode[idx]?.fclt || "s",
                comboColNm: this.targetCompNm.fcltColNm,
                comboColCode: "FCLT_DVCD",
                comboColData: "FCLT_DVNM",
            },
            {
                // 대학
                ds: this._ds_univ,
                dsNm: "_ds_univ",
                mode: this.orgGridMode[idx]?.univ || "s",
                comboColNm: this.targetCompNm.univColNm,
                comboColCode: "UNIV_CD",
                comboColData: "UNIV_NM",
            },
            {
                // 학부
                ds: this._ds_dpmj,
                dsNm: "_ds_dpmj",
                mode: this.orgGridMode[idx]?.dpmj || "n",
                comboColNm: this.targetCompNm.dpmjColNm,
                comboColCode: "DPMJ_CD",
                comboColData: "DPMJ_NM",
            },
            {
                // 전공
                ds: this._ds_majr,
                dsNm: "_ds_majr",
                mode: this.orgGridMode[idx]?.majr || "n",
                comboColNm: this.targetCompNm.majrColNm,
                comboColCode: "MAJR_CD",
                comboColData: "MAJR_NM",
            },
            {
                // 세부 전공
                ds: this._ds_detMajr,
                dsNm: "_ds_detMajr",
                mode: this.orgGridMode[idx]?.detMajr || "n",
                comboColNm: this.targetCompNm.detMajrColNm,
                comboColCode: "DET_MAJR_CD",
                comboColData: "DET_MAJR_NM",
            },
        ];

        comboConfig.forEach((c) => {
            if (!c.ds) {
                return;
            }

            const ds = $ds.clone(c.ds);
            this.fm.addChild(this.unique(c.dsNm) + idx, ds);

            const dsIdx = grid.getBindCellIndex("body", c.comboColNm);

            if (dsIdx !== -1) {
                grid.setCellProperty(
                    "body",
                    dsIdx,
                    "combodataset",
                    this.unique(c.dsNm) + idx,
                );
                grid.setCellProperty(
                    "body",
                    dsIdx,
                    "combocodecol",
                    c.comboColCode,
                );
                grid.setCellProperty(
                    "body",
                    dsIdx,
                    "combodatacol",
                    c.comboColData,
                );
            }
        });

        // LinkedComboManager 인스턴스 생성 및 노드 등록
        const comboManager = new LinkedComboManager();

        const possibleNodes = [
            {
                id: "fclt",
                colNm: this.targetCompNm.fcltColNm,
                codeField: "FCLT_DVCD",
                textField: "FCLT_DVNM",
                parents: [],
                mode: this.orgGridMode[idx]?.fclt || "s",
            },
            {
                id: "univ",
                colNm: this.targetCompNm.univColNm,
                codeField: "UNIV_CD",
                textField: "UNIV_NM",
                parents: ["fclt"],
                mode: this.orgGridMode[idx]?.univ || "s",
            },
            {
                id: "dpmj",
                colNm: this.targetCompNm.dpmjColNm,
                codeField: "DPMJ_CD",
                textField: "DPMJ_NM",
                parents: ["univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: this.orgGridMode[idx]?.dpmj || "n",
            },
            {
                id: "majr",
                colNm: this.targetCompNm.majrColNm,
                codeField: "MAJR_CD",
                textField: "MAJR_NM",
                parents: ["dpmj", "univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: this.orgGridMode[idx]?.majr || "n",
            },
            {
                id: "detMajr",
                colNm: this.targetCompNm.detMajrColNm,
                codeField: "DET_MAJR_CD",
                textField: "DET_MAJR_NM",
                parents: ["dpmj", "univ", "fclt"],
                allPathField: "ALL_PATH",
                mode: this.orgGridMode[idx]?.detMajr || "n",
            },
        ];

        const parentCodeFields = {
            fclt: null,
            univ: "FCLT_DVCD",
            dpmj: { univ: "UNIV_CD", fclt: "FCLT_DVCD" },
            majr: { dpmj: "DPMJ_CD", univ: "UNIV_CD", fclt: "FCLT_DVCD" },
            detMajr: { dpmj: "DPMJ_CD", univ: "UNIV_CD", fclt: "FCLT_DVCD" },
        };

        const activeNodes = possibleNodes.filter(
            (node) => grid.getBindCellIndex("body", node.colNm) !== -1,
        );
        const activeIds = activeNodes.map((node) => node.id);

        activeNodes.forEach((node) => {
            const parentId =
                node.parents.find((p) => activeIds.includes(p)) || null;

            comboManager.addNode({
                id: node.id,
                parentId: parentId,
                grid: grid,
                colNm: node.colNm,
                codeField: node.codeField,
                textField: node.textField,
                parentCodeField: parentCodeFields[node.id],
                allPathField: node.allPathField,
                mode: node.mode,
                onDisplayChange: (n, type) => {
                    if (!n.allPathField) return;
                    const ds = n.adapter.getInnerDataset();
                    if (!ds) return;

                    if (ds.__isSwapped === undefined) {
                        ds.__isSwapped = false;
                    }

                    const shouldBeSwapped = type === "allPath";
                    if (ds.__isSwapped !== shouldBeSwapped) {
                        this.hCmbChangColVal(ds, n.textField, n.allPathField);
                        ds.__isSwapped = shouldBeSwapped;
                    }
                },
            });
        });

        comboManager.attachGridEvents(grid);
        if (!this.comboManagers) {
            this.comboManagers = [];
        }
        this.comboManagers.push(comboManager);
    }

    // 컴포넌트 프로퍼티 덮어쓰기
    overrideOrgCompProperty(comp) {
        comp.type = "filterlike";
    }
}

/**
 * 학사 콤보 객체를 리턴합니다. 상세한 사용법은 학사 콤보({@link HCmb#init})를 참조하세요.
 *
 * 인수는 학사 콤보 옵션({@link hCmbOption})을 참고하세요
 *
 * @function getHCmb
 * @param {nexacro.Form} form  현재 폼 (this)
 * @returns {HCmb}
 * @memberof $f
 * @example
 * // 기본 사용법
 * $f.getHCmb(this).init({
 *     div: [this.div_search],
 *     grid: [this.grd_main],
 * });
 *
 * // 특정 조직 필터링
 * $f.getHCmb(this).init({
 *     div: this.div_search,
 *     orgFilter: (ctx) => ctx.rowData["UNIV_CD"] === "03130000",
 * });
 *
 * // 콤보 모드 설정
 * $f.getHCmb(this).init({
 *     div: this.div_search,
 *     orgMode: {
 *         // 조직 콤보 모드
 *         fclt: "a", // 대학/대학원: '전체' 표시
 *         univ: "s", // 대학: '선택' 표시
 *         dpmj: "n", // 학과: 빈값 없음
 *         majr: "n", // 전공: 빈값 없음
 *         detMajr: "n", // 세부전공: 빈값 없음
 *     },
 *     smrMode: "a", // 학기 콤보: '전체' 표시
 * });
 *
 * // 문자열로 모든 모드 일괄 설정
 * $f.getHCmb(this).init({
 *     div: this.div_search,
 *     orgMode: "a", // 모든 조직 콤보에 '전체' 표시
 *     smrMode: "a", // 학기 콤보: '전체' 표시
 * });
 *
 * // 그리드 콤보 모드 설정
 * $f.getHCmb(this).init({
 *     grid: this.grd_main,
 *     orgGridMode: {
 *         // 그리드 조직 콤보 모드
 *         fclt: "s", // 대학/대학원: '선택' 표시
 *         univ: "s", // 대학: '선택' 표시
 *         dpmj: "n", // 학과: 빈값 없음
 *     },
 *     smrGridMode: "s", // 그리드 학기 콤보: '선택' 표시
 * });
 *
 * // 바인딩 컴포넌트 대상 변경
 * $f.getHCmb(this).init({
 *     div: this.div_search,
 *     targtCompNm: {
 *         // 컴포넌트 명 재정의
 *         yyCompNm: /^spn_aplyYy$/,
 *         smrCompNm: /^cmb_aplySmr$/,
 *         fcltCompNm: /^cmb_fcltDvcd$/,
 *         univCompNm: /^cmb_testUnivCd$/,
 *         dpmjCompNm: /^cmb_hakgwaCd$/,
 *         majrCompNm: /^cmb_majrCd$/,
 *         detMajrCompNm: /^cmb_detMajrCd$/,
 *
 *         // 그리드 컬럼 명 재정의
 *         smrColNm: ["SMR", "GRDU_SMR"],
 *         fcltGridColNm: "FCLT_DVCD",
 *         univColNm: "CUSTOM_1",
 *         dpmjColNm: "CUSTOM_2",
 *         majrColNm: "CUSTOM_3",
 *         detMajrColNm: "detMajrColNm",
 *     },
 * });
 *
 * // 관리학과 기준으로 데이터 조회
 * $f.getHCmb(this).init({
 *     div: this.div_search,
 *     useMngtDpmj: true, // 관리학과 기준으로 조직 데이터 조회
 * });
 *
 */
export function getHCmb(form) {
    if (!(form instanceof nexacro.Form)) {
        return null;
    }

    return new HCmb(form);
}