modules_schedule_check-scdul.js

/**
 * @typedef CheckScdulOptions
 * @property {string | string[]} [scdulCd]
 * 체크할 일정 코드 (기본으로 메뉴의 것을 사용)<br>
 * 여러 일정을 인수로 넘길 시 다음과 같이 처리합니다.<br>
 * 일정 중 단 하나라도 일치하는 것이 있으면 통과<br>
 * 모두 불일치 일 시 다음 우선순위로 일정 메시지와 정보를 반환합니다.<br>
 * 1. 현재 일자보다 시작일자가 늦는 일정<br>
 * 2. 현재 일자와 시작일자가 가까운 일정
 * @property {string} [sno]
 * 학번<br>
 * 해당 인수의 학번을 바탕으로 일정의 대학 코드를 찾습니다.
 * 학생의 경우 세션의 ID를 사용합니다.
 * @property {string} [univCd]
 * 대학 코드<br>
 * 해당 인수를 생략 시 업무 코드를 기반 등의 조건을 거쳐 찾습니다.
 * @property {boolean} [enableDefaultAction=true]
 * 기본 액션 실행 여부<br>
 * 해당 값을 false로 설정 시 일정 결과에 따른 기본 작업을 수행하지 않습니다.
 * @property {nexacro.Component[]|function(boolean)} [targetComponent]
 * 일정의 대상이 되는 컴포넌트 혹은 boolean 타입의 매개변수를 받는 함수<br>
 * nexacro.Component 객체의 경우 set_enable을 실행하여 컴포넌트 활성/비활성화를 진행합니다.<br>
 * function 객체의 경우 일정 여부를 받는 매개변수를 받는 함수입니다.
 * @property {boolean} [targetComponentOverride=true]
 * 일정의 대상이 되는 컴포넌트의 기본을 유지하고자 하는 경우 false.
 * @property {boolean} [isCloseOutOfScdul=false]
 * 일정 범위가 아닐 시 화면을 닫을 지 여부<br>
 * 이 기능은 enableDefaultAction 값이 true여야 동작합니다.<br>
 * 즉시 종료하므로 리턴받은 Promise에서 후처리 동작이 불가합니다.
 */

/**
 * @typedef CheckSchdReturn
 * @property {boolean} scdulYn       일정 체크 결과
 * @property {string}  scdulMsg      일정 메시지
 * @property {boolean} chkSuccessYn  일정 검사 수행의 성공 여부. 인수 부족, 일정 해당 없는 메뉴의 경우
 *                                   true로 반환
 * @property {string}  scdulCd       일정 검사 후 해당되는 일정 코드
 * @property {string}  beginDtm      일정 시작 일시
 * @property {string}  endDtm        일정 종료 일시
 */

/**
 * @param {string} scdulCd  일정 코드
 * @returns {Promise}
 * @access private
 */
const _fetchCheckScdul = async function (formObj, scdulCd, options) {
    const args = {
        SCDUL_CD: scdulCd,
        SNO: options.sno || "",
        UNIV_CD: options.univCd || "",
    };
    const datasetName = "_ds_schdChk_" + scdulCd;
    const dataset = new Dataset();

    if (!formObj._ds_schdChk) {
        formObj.addChild(datasetName, dataset);
    }

    try {
        await formObj.tx.search({
            action: "basic",
            svcId: "f_chkScdul_" + scdulCd,
            sqlId: "@f_chkScdul.s01",
            param: args,
            outDs: datasetName,
            cursorNm: "OUT_CURSOR",
        });

        const scdulYn = dataset.getColumn(0, "SCDUL_YN") === "1";
        const scdulMsg = dataset.getColumn(0, "SCDUL_MSG");

        return {
            scdulYn: scdulYn,
            scdulMsg: scdulMsg,
            scdulCd: scdulCd,
            beginDtm: dataset.getColumn(0, "BEGIN_DTM"),
            endDtm: dataset.getColumn(0, "END_DTM"),
        };
    } finally {
        formObj.removeChild(datasetName);
    }
};

/**
 * @typedef ParsedScdulMsg
 * @property {string} [scdulNm]   일정명
 * @property {string} [beginDtm]  시작 일자
 * @property {string} [endDtm]    종료 일자
 * @access private
 */

/**
 * @param {string} scdulMsg  일정 메시지
 * @returns {ParsedScdulMsg} 파싱된 데이터
 * @access private
 */
const _parseScdulMsg = function (scdulMsg) {
    if (!scdulMsg) {
        return {};
    }

    const matched = scdulMsg.match(
        /^(.+?) : ([0-9]+)년 ([0-9]+)월 ([0-9]+)일 \(([0-9]+):([0-9]+)\) 부터 ([0-9]+)년 ([0-9]+)월 ([0-9]+)일 \(([0-9]+):([0-9]+)\) 까지 입니다/,
    );

    if (!matched) {
        return {};
    }

    return {
        scdulNm: matched[1],
        beginDtm:
            matched[2] + matched[3] + matched[4] + matched[5] + matched[6],
        endDtm:
            matched[7] + matched[8] + matched[9] + matched[10] + matched[11],
    };
};

/**
 * 화면을 닫는 함수
 *
 * @access private
 */
const _closeCurrentMenu = function () {
    const menuId = $comp.getWorkInfoDataset(this).getColumn(0, "MENU_ID");

    nexacro.$workManager.close(menuId);
};

/**
 * 일정을 검사하여 관련한 처리를 수행합니다.
 *
 * 이 함수는 this를 form으로 간주합니다. 다른 스코프에서 실행 시 call을 통해서 this를 form으로 바인딩하여야 합니다.
 *
 * @function checkScdul
 * @param {nexacro.Form}      formObj       넥사크로 폼 오브젝트
 * @param {CheckScdulOptions} [options={}]  일정 검사 옵션
 * @returns {Promise<CheckSchdResultCode>} 일정 체크의 결과
 * @memberof $f
 * @example
 * // 기본 처리
 * // 메뉴에 등록된 일정을 가지고 검사를 수행하며, 각 일정에 대하여 다음과 같이 동작합니다.
 * //
 * // 일정 범위 내:
 * // 메인 타이틀바의 모든 버튼을 활성화 합니다.
 * //
 * // 일정 범위 밖:
 * // 메인 타이틀바의 조회, 초기화 버튼을 제외한 버튼을 모두 비활성화 합니다.
 * $f.fn_checkScdul(this);
 *
 * // 모든 인수
 * $f.checkScdul(this, {
 *     scdulCd: "ABC1234", // 일정 코드 (기본 값: 메뉴의 일정 코드)
 *     sno: this.div_search.form.sch_sno.value, // 학번
 *     enableDefaultAction: true, // 기본 컴포넌트 활성/비활성, 화면 종료 등의 액션을 사용합니다. false시 아무런 작업도 하지 않습니다.
 *     targetComponent: [
 *         this.getMainTitle().getButton("SEARCH"), // 대상 컴포넌트를 지정하면 set_enable 함수로 동작합니다.
 *         (b) => formObj.getMainTitle().set_useSaveButton(b), // 일정 여부를 인수로 갖는 함수입니다. 일정 여부에 따라 다른 옵션을 지정하는데 사용합니다.
 *     ], // 대상 컴포넌트 및 함수
 *     targetComponentOverride: false, // true로 설정 시 targetComponent를 기본 대상 컴포넌트로 덮어쓰며, false로 설정 시 병합합니다.
 *     isCloseOutOfScdul: false, // true로 설정 시 일정 검사 후 화면을 닫습니다.
 * });
 *
 * // 리턴값 활용 예제
 * // 1. 일정 범위 내인 경우만 처리
 * const result = await $f.checkScdul(this);
 * if (result.scdulYn) {
 *     // 일정 범위 내 처리
 *     this.saveData();
 * }
 *
 * // 2. 시작/종료 일시 활용
 * const result = await $f.checkScdul(this);
 * if (result.scdulYn) {
 *     this.div_info.form.sta_period.set_text(
 *         `${result.beginDtm} ~ ${result.endDtm}`,
 *     );
 * }
 *
 * // 3. 메시지 직접 처리
 * const result = await $f.checkScdul(this, {
 *     enableDefaultAction: false,
 * });
 * if (!result.scdulYn) {
 *     await this.pop.warn(result.scdulMsg);
 * }
 *
 */
export async function checkScdul(formObj, options) {
    options = options || {};

    // 인수로 들어온 옵션이 없을 경우
    if (!options.scdulCd) {
        options.scdulCd = [
            $comp.getWorkInfoDataset(formObj).getColumn(0, "SCDUL_CD"),
        ];
    }

    if (options.enableDefaultAction === undefined) {
        options.enableDefaultAction = true;
    }

    if (options.targetComponentOverride === undefined) {
        options.targetComponentOverride = true;
    }

    if (!(options.scdulCd instanceof Array)) {
        options.scdulCd = [options.scdulCd];
    }

    if (options.isCloseOutOfScdul === undefined) {
        options.isCloseOutOfScdul = false;
    }

    const defaultTargetComponent = [
        formObj?.getMainTitle?.()?.getButton?.("SAVE"),
        formObj?.getMainTitle?.()?.getButton?.("ADD"),
        formObj?.getMainTitle?.()?.getButton?.("REMOVE"),
        formObj?.getMainTitle?.()?.getButton?.("PRINT"),
        formObj?.getMainTitle?.()?.getButton?.("EXCEL"),
    ].filter((e) => e);

    if (!options.targetComponent) {
        options.targetComponent = [...defaultTargetComponent];
    } else if (options.targetComponent && !options.targetComponentOverride) {
        options.targetComponent = [
            ...options.targetComponent,
            ...defaultTargetComponent,
        ];
    }

    // 해당 화면에 일정 코드 부여가 안된 경우
    if (options.scdulCd.length <= 0) {
        return Promise.resolve({
            scdulYn: true,
            scdulMsg: "",
            chkSuccessYn: false,
        });
    }

    const proms = options.scdulCd.map((scdulCd) => {
        return _fetchCheckScdul(formObj, scdulCd, options);
    });

    const promsList = await Promise.allSettled(proms);

    let scdulYn = false;
    let scdulMsg = "";
    let scdulCd = "";
    let beginDtm = "";
    let endDtm = "";

    const dataList = promsList.map((v) => {
        const { scdulCd, scdulMsg, scdulYn, beginDtm, endDtm } = v.value;
        return {
            scdulYn,
            scdulMsg,
            beginDtm,
            endDtm,
            scdulCd,
        };
    });

    //수강신청같이 여러일정이 동시에 걸쳐있는경우 오늘날짜가 일정안에 포함되면 1순위 이미지났으면 2순위 아예 안걸려있으면 3순위로 정렬후 0번째 가져감
    // 이미 일정안에 포함되면 해당스케쥴은 열린거로 봐야하기때문
    const today = $util.todayTime.substring(0, 12); // 현재 시간 (YYYYMMDDHHMI)

    dataList.sort((a, b) => {
        // 1. 진행 중인 일정인지 확인 (SCDUL_YN이 1이고 현재 기간 내)
        const aIsRunning =
            a.scdulYn && today >= a.beginDtm && today <= a.endDtm ? 0 : 1;
        const bIsRunning =
            b.scdulYn && today >= b.beginDtm && today <= b.endDtm ? 0 : 1;

        // 2. 진행 중인 게 있다면 최우선 (0이 1보다 앞으로)
        if (aIsRunning !== bIsRunning) return aIsRunning - bIsRunning;

        // 3. 둘 다 진행 중이거나 둘 다 아닌 경우, SCDUL_YN이 1인 것을 우선
        const aYn = a.scdulYn ? 0 : 1;
        const bYn = b.scdulYn ? 0 : 1;
        if (aYn !== bYn) return aYn - bYn;

        // 4. 그 외에는 시작일자가 가까운 순으로 정렬
        return b.beginDtm - a.beginDtm;
    });

    let schduledDataList = dataList.filter((e) => e.scdulYn);

    if (schduledDataList.length > 0) {
        // 일정 통과
        scdulYn = true;
        scdulCd = schduledDataList[0].scdulCd;
        beginDtm = schduledDataList[0]?.beginDtm;
        endDtm = schduledDataList[0]?.endDtm;
    } else {
        const today = $util.todayTime.substring(0, 12);
        const displayScdul = dataList
            .filter((a) => a.beginDtm)
            .sort(
                (a, b) =>
                    Math.abs(today - a.beginDtm) +
                    (today - a.beginDtm > 0 ? 1000000000 : 0) -
                    (Math.abs(today - b.beginDtm) +
                        (today - b.beginDtm > 0 ? 1000000000 : 0)),
            )[0];

        scdulYn = false;
        scdulMsg = displayScdul && displayScdul.scdulMsg;
        scdulCd = displayScdul && displayScdul.scdulCd;
        beginDtm = displayScdul?.beginDtm;
        endDtm = displayScdul?.endDtm;
    }

    // 후처리
    if (options.enableDefaultAction) {
        if (!scdulYn) {
            await formObj.pop.alert(scdulMsg || "일정이 존재하지 않습니다.");
            options.isCloseOutOfScdul && _closeCurrentMenu.call(formObj);
        }

        // 대상 컴포넌트 활성화, 비활성화
        options.targetComponent.forEach((comp) => {
            if (typeof comp === "function") {
                comp && comp(scdulYn);
            } else {
                comp && comp.set_enable && comp.set_enable(scdulYn);
            }
        });
    }

    // 결과 반환
    return {
        scdulYn: scdulYn,
        scdulMsg: scdulMsg,
        chkSuccessYn: true,
        scdulCd: scdulCd,
        beginDtm: beginDtm,
        endDtm: endDtm,
    };
}