Greasy Fork

Boss Batch Push [Boss直聘批量投简历]

boss直聘批量简历投递

当前为 2023-06-30 提交的版本,查看 最新版本

// ==UserScript==
// @name         Boss Batch Push [Boss直聘批量投简历]
// @description  boss直聘批量简历投递
// @namespace    maple
// @version      1.1.2
// @author       maple,Ocyss
// @license      Apache License 2.0
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_addValueChangeListener
// @match        https://www.zhipin.com/*
// ==/UserScript==

const docTextArr = [
  "!加油,相信自己😶‍🌫️",
  "1.批量投递:点击批量投递开始批量投简历,请先通过上方Boss的筛选功能筛选大致的范围,然后通过脚本的筛选进一步确认投递目标。",
  "2.重置开关:如果你需要自己浏览工作详情页面,请点击该按钮关闭自动投递。如果不关闭,打开工作详情页,会自动投递并关闭页面。",
  "3.保存配置:保持下方脚本筛选项,用于后续直接使用当前配置。",
  "4.过滤不活跃Boss:打开后会自动过滤掉最近未活跃的Boss发布的工作。以免浪费每天的100次机会。",
  "😏",
  "脚本筛选项介绍:",
  "公司名包含:投递工作的公司名一定包含在当前集合中,模糊匹配,多个使用逗号分割。这个一般不用,如果使用了也就代表只投这些公司的岗位。例子:【阿里,华为】",
  "排除公司名:投递工作的公司名一定不在当前集合中,也就是排除当前集合中的公司,模糊匹配,多个使用逗号分割。例子:【xxx外包】",
  "排除工作内容:会自动检测上文(不是,不,无需等关键字),下文(系统,工具),例子:【外包,上门,销售,驾照】,如果写着是'不是外包''销售系统'那也不会被排除",
  "Job名包含:投递工作的名称一定包含在当前集合中,模糊匹配,多个使用逗号分割。例如:【软件,Java,后端,服务端,开发,后台】",
  "薪资范围:投递工作的薪资范围一定在当前区间中,一定是区间,使用-连接范围。例如:【12-20】",
  "公司规模范围:投递工作的公司人员范围一定在当前区间中,一定是区间,使用-连接范围。例如:【500-20000000】",
  "👻",
];
const aboutLink = [
  [
    [
      "GreasyFork",
      "https://greasyfork.org/zh-CN/scripts/468125-boss-batch-push-boss%E7%9B%B4%E8%81%98%E6%89%B9%E9%87%8F%E6%8A%95%E7%AE%80%E5%8E%86",
    ],
    ["GitHub", "https://github.com/yangfeng20/boss_batch_push"],
    ["Gitee", "https://gitee.com/yangfeng20/boss_batch_push"],
  ],
  [
    ["作者:yangfeng20", "https://github.com/yangfeng20"],
    ["二改:Ocyss_04", "https://github.com/Ocyss"],
  ],
];
let companyArr = []; //公司名
let companyExclude = []; //排除公司名
let descriptionExclude = []; //排除工作内容
let jobNameArr = []; //job名
let salaryRange = ""; //薪资范围
let companyScale = ""; //公司规模范围

/**
 * 投递多少个,每页默认有30个job,筛选过后不确定
 * @type {number}
 */
const pushPageCount = 100;

/**
 * 当前页,勿动!
 * @type {number}
 */
let currentPage = 0;
let iframeEl, toolEl;
let loadConfig, saveConfig;
let runT = false;
/**
 * 本地存储key
 */
const ACTIVE_READY = "activeReady";
const ACTIVE_ENABLE = "activeEnable";
const LOCAL_CONFIG = "config";
const PUSH_COUNT = "pushCount";
const PUSH_LOCK = "lock";
const PUSH_LIMIT = "limit";
const BATCH_ENABLE = "enable";
const RUN_DATE = "rundate"; // 上一次运行时间,不一样就清空COUNT

// 开始批量投递
const batchHandler = (el) => {
  if (!runT) {
    runT = true;
    el.style.backgroundColor = "#67c23a";
    el.innerText = "停止投递";
    const runbatch = () => {
      if (!runT) {
        return;
      }
      // 每次投递加载最新的配置
      loadConfig();
      console.log("开始批量投递,当前页数:", ++currentPage);
      GM_setValue(BATCH_ENABLE, true);

      async function clickJobList(jobList, delay) {
        // 过滤只留下立即沟通的job
        jobList = filterJob(jobList);
        await activeWait();
        console.log("过滤后的job数量", jobList.length, "默认30");

        for (let i = 0; i < jobList.length && runT; i++) {
          const job = jobList[i];
          let innerText = job.querySelector(".job-title").innerText;
          const jobTitle = innerText.replace("\n", " ");

          while (true) {
            if (!GM_getValue(PUSH_LOCK, false)) {
              console.log("解锁---" + jobTitle);
              break;
            }
            console.log("等待---" + jobTitle);
            // 每300毫秒检查一次状态
            await sleep(300);
          }

          if (GM_getValue(PUSH_LIMIT, false)) {
            console.log("今日沟通已达boss限制");
            window.alert(
              "今天已经不能在沟通了,愿你早日找到心满意足的工作,不要灰心,我一直与你同在~"
            );
            break;
          }

          // 当前table页是活跃的,也是另外一遍点击立即沟通之后,以及关闭页面
          await new Promise((resolve) => setTimeout(resolve, delay)); // 等待 delay 秒
          GM_setValue(PUSH_LOCK, true);
          console.log("加锁---" + jobTitle);
          // job.click();
          iframeEl.src = job.querySelector(".job-card-left").href;
        }

        if (
          !runT ||
          currentPage >= pushPageCount ||
          GM_getValue(PUSH_LIMIT, false)
        ) {
          console.log("一共", pushPageCount, "页");
          console.log("共投递", GM_getValue(PUSH_COUNT, 0), "份");
          console.log("投递完毕");
          clear();
          return;
        }

        const nextButton = document.querySelector(".ui-icon-arrow-right");
        // 没有下一页
        if (nextButton.parentElement.className === "disabled") {
          let temp =
            "共投递" +
            GM_getValue(PUSH_COUNT, 0) +
            "份,没有更多符合条件的工作";
          window.alert(temp);
          console.log(temp);
          batchHandler(el);
          clear();
          return;
        }

        console.log("下一页,开始等待8秒钟");
        nextButton.click();
        setTimeout(() => runbatch(), 8000);
      }

      // 每隔5秒执行一次点击操作
      clickJobList(document.querySelectorAll(".job-card-wrapper"), 5000);
    };
    runbatch();
  } else {
    runT = false;
    el.style.backgroundColor = "#409eff";
    el.innerText = "批量投递";
    GM_setValue(BATCH_ENABLE, true);
  }
};

// Job列表事件处理
const jobListHandler = () => {
  // 重置逻辑状态,可能由于执行过程的中断导致状态错乱
  resetStatus();

  // 批量投递按钮
  const batchButton = document.createElement("button");
  batchButton.innerText = "批量投递";
  batchButton.addEventListener("click", () => {
    batchHandler(batchButton);
  });

  // 重置开关按钮
  const resetButton = document.createElement("button");
  resetButton.innerText = "重置开关";
  resetButton.addEventListener("click", () => {
    GM_setValue(BATCH_ENABLE, false);
    console.log("重置脚本开关成功");
    window.alert("重置脚本开关成功");
  });

  // 保存配置按钮
  const saveButton = document.createElement("button");
  saveButton.innerText = "保存配置";
  saveButton.addEventListener("click", () => {
    saveConfig();
    window.alert("保存配置成功");
  });

  // 过滤不活跃boss按钮
  const switchButton = document.createElement("button");

  const addStyle = (button) => {
    button.style.cssText =
      "display: inline-block; border-radius: 5px; background-color: rgb(64, 158, 255); color: rgb(255, 255, 255); text-decoration: none; padding: 10px;cursor: pointer";
  };
  addStyle(batchButton);
  addStyle(resetButton);
  addStyle(saveButton);
  addStyle(switchButton);

  let switchState = false;
  const setSwitchButtonState = (isOpen) => {
    switchState = isOpen;
    if (isOpen) {
      switchButton.innerText = "过滤不活跃Boss:已开启";
      switchButton.style.backgroundColor = "#67c23a";
      GM_setValue(ACTIVE_ENABLE, true);
    } else {
      switchButton.innerText = "过滤不活跃Boss:已关闭";
      switchButton.style.backgroundColor = "#f56c6c";
      GM_setValue(ACTIVE_ENABLE, false);
    }
  };
  setSwitchButtonState(GM_getValue(ACTIVE_ENABLE, true));
  iframeEl = document.createElement("iframe");
  // 添加事件监听,执行回调函数
  switchButton.addEventListener("click", () => {
    setSwitchButtonState(!switchState);
  });

  const ButtonEl = document.createElement("div");
  ButtonEl.style.display = "flex";
  ButtonEl.style.justifyContent = "space-evenly";
  ButtonEl.appendChild(batchButton);
  ButtonEl.appendChild(resetButton);
  ButtonEl.appendChild(saveButton);
  ButtonEl.appendChild(switchButton);
  // 等待页面元素渲染,然后加载配置并渲染页面
  const tempT = setInterval(() => {
    const container = document.querySelector(".job-list-wrapper");
    if (container == undefined) {
      return;
    }
    toolEl = document.createElement("div");
    toolEl.id = "boos-tool";
    toolEl.style.cssText =
      "padding: 10px;display: flex;flex-direction: column;min-height: 50vh;justify-content: space-between;";
    toolEl.appendChild(docEl());
    toolEl.appendChild(ButtonEl);
    toolEl.appendChild(iframeEl);
    toolEl.appendChild(configEl());
    container.insertBefore(toolEl, container.firstElementChild);
    // console.log(docTextArr.join("\n"));
    clearInterval(tempT);
  }, 1000);
};

// 详情页面处理
function jobDetailHandler() {
  if (!GM_getValue(BATCH_ENABLE, false)) {
    console.log("未开启脚本开关");
    return;
  }

  /**
   * 招聘boss是否活跃
   */
  const isBossActive = () => {
    const activeEle = document.querySelector(".boss-active-time");
    if (!activeEle) {
      return true;
    }
    const activeText = activeEle.innerText;
    return !(activeText.includes("月") || activeText.includes("年"));
  };

  // 关闭页面并重置对应状态
  const closeTab = (ms) => {
    // console.log("关闭页面");
    setTimeout(() => {
      // 沟通限制对话框
      const limitDialog = document.querySelector(
        ".greet-pop .dialog-container"
      );
      if (limitDialog) {
        if (limitDialog.innerText.includes("人数已达上限")) {
          GM_setValue(PUSH_LIMIT, true);
        } else {
          // 更新投递次数,可能存在性能问题
          GM_setValue(PUSH_COUNT, GM_getValue(PUSH_COUNT, 0) + 1);
        }
      }
      GM_setValue(PUSH_LOCK, false);
      // window.close();
    }, ms);
  };

  // boss是否活跃,过滤不活跃boss
  if (!isBossActive()) {
    console.log("过滤不活跃boss");
    // closeTab(0);
    return;
  }

  // 立即沟通或者继续沟通按钮
  const handlerButton = document.querySelector(".btn-startchat");
  if (handlerButton.innerText.includes("立即沟通")) {
    // 如果是沟通按钮则点击
    // console.log("点击立即沟通");
    handlerButton.click();
  }

  closeTab(1000);
}

// 岗位匹配过滤
function filterJob(job_list) {
  const result = [];
  let requestCount = 0;
  // 过滤器
  const matchJob = (job) => {
    // 公司名
    const companyName = job.querySelector(".company-name").innerText;
    // 工作名
    const jobName = job.querySelector(".job-name").innerText;
    // 薪资范围
    const salary = job.querySelector(".salary").innerText;
    // 公司规模范围
    const companyScale_ =
      job.querySelector(".company-tag-list").lastChild.innerText;

    // 模糊匹配
    function fuzzyMatch(arr, input, emptyStatus) {
      if (arr.length === 0) {
        // 为空时直接返回指定的空状态
        return emptyStatus;
      }
      input = input.toLowerCase();
      let emptyEle = false;
      // 遍历数组中的每个元素
      for (let i = 0; i < arr.length; i++) {
        // 如果当前元素包含指定值,则返回 true
        let arrEleStr = arr[i].toLowerCase();
        if (arrEleStr.length === 0) {
          emptyEle = true;
          continue;
        }
        if (arrEleStr.includes(input) || input.includes(arrEleStr)) {
          return true;
        }
      }

      // 所有元素均为空元素【返回空状态】
      if (emptyEle) {
        return emptyStatus;
      }

      // 如果没有找到匹配的元素,则返回 false
      return false;
    }
    // 范围匹配
    function rangeMatch(rangeStr, input, by = 1) {
      if (!rangeStr) {
        return true;
      }
      // 匹配定义范围的正则表达式
      let reg = /^(\d+)(?:-(\d+))?$/;
      let match = rangeStr.match(reg);

      if (match) {
        let start = parseInt(match[1]) * by;
        let end = parseInt(match[2] || match[1]) * by;

        // 如果输入只有一个数字的情况
        if (/^\d+$/.test(input)) {
          let number = parseInt(input);
          return number >= start && number <= end;
        }

        // 如果输入有两个数字的情况
        let inputReg = /^(\d+)(?:-(\d+))?/;
        let inputMatch = input.match(inputReg);
        if (inputMatch) {
          let inputStart = parseInt(inputMatch[1]);
          let inputEnd = parseInt(inputMatch[2] || inputMatch[1]);
          return (
            (inputStart >= start && inputStart <= end) ||
            (inputEnd >= start && inputEnd <= end)
          );
        }
      }

      // 其他情况均视为不匹配
      return false;
    }

    const companyNameCondition = fuzzyMatch(companyArr, companyName, true);
    const companyNameExclude = fuzzyMatch(companyExclude, companyName, false);
    const jobNameCondition = fuzzyMatch(jobNameArr, jobName, true);
    const salaryRangeCondition =
      rangeMatch(salaryRange, salary) || rangeMatch(salaryRange, salary, 30); //时薪也算进去,不100%准确
    const companyScaleCondition = rangeMatch(companyScale, companyScale_);

    return (
      companyNameCondition &&
      !companyNameExclude &&
      jobNameCondition &&
      salaryRangeCondition &&
      companyScaleCondition
    );
  };

  for (let i = 0; i < job_list.length; i++) {
    let job = job_list[i];
    let innerText = job.querySelector(".job-title").innerText;
    const jobTitle = innerText.replace("\n", " ");

    // 匹配符合条件的Job
    if (!matchJob(job)) {
      console.log("%c 跳过不匹配的job:" + jobTitle, "color:#e88080;");
      continue;
    }

    const jobStatusStr = job.querySelector(".start-chat-btn").innerText;
    if (!jobStatusStr.includes("立即沟通")) {
      console.log("%c 跳过沟通过的Job:" + jobTitle, "color:#FF9733;");
      continue;
    }

    // 当没开启活跃度检查和工作内容筛选不进行网络请求
    if (!GM_getValue(ACTIVE_ENABLE, false) && descriptionExclude.length == 0) {
      // 未打开boss活跃度开关
      result.push(job);
      continue;
    }

    // 活跃度检查【如果是活跃才添加到result中】
    requestCount++;
    const params = job.querySelector(".job-card-left").href.split("?")[1];
    axios
      .get("https://www.zhipin.com/wapi/zpgeek/job/card.json?" + params, {
        timeout: 2000,
      })
      .then((resp) => {
        const activeText = resp.data.zpData.jobCard.activeTimeDesc;
        if (
          GM_getValue(ACTIVE_ENABLE, false) &&
          (activeText.includes("月") || activeText.includes("年"))
        ) {
          console.log("%c 过滤不活跃的Job:" + jobTitle, "color:#F8FD5A;");
          return;
        }
        const content = resp.data.zpData.jobCard.postDescription;
        for (let i = 0; i < descriptionExclude.length; i++) {
            if (!descriptionExclude[i]) {
              continue
            }
          let re = new RegExp(
            "(?<!(不|无).{0,5})" +
              descriptionExclude[i] +
              "(?!系统|软件|工具|服务)"
          );
          if (re.test(content)) {
            console.log(
              "%c 过滤不符合的工作内容-" +
                descriptionExclude[i] +
                ":" +
                jobTitle,
              "color:#f2c97d;"
            );
            return;
          }
        }
        console.log("%c 添加符合bossJob:" + jobTitle, "color:#63e2b7;");
        result.push(job);
      })
      .catch((e) => {
        console.log("网络筛选失败,原因:");
        console.log(e);
      })
      .finally(() => {
        requestCount--;
        if (requestCount === 0) {
          GM_setValue(ACTIVE_READY, true);
        }
      });
  }
  return result;
}

// 活跃度检查
async function activeWait() {
  // 未开启活跃度检查
  if (!GM_getValue(ACTIVE_ENABLE, false)) {
    return new Promise((resolve) => resolve());
  }
  return new Promise((resolve) => {
    const timer = setInterval(() => {
      if (
        GM_getValue(ACTIVE_ENABLE, false) &&
        GM_getValue(ACTIVE_READY, false)
      ) {
        clearInterval(timer);
        resolve();
      }
      console.log(
        "等待检查Job活跃度阻塞中---------",
        GM_getValue(ACTIVE_ENABLE, false),
        GM_getValue(ACTIVE_READY, false)
      );
    }, 1000);
  });
}

// 重置
function resetStatus() {
  const d = new Date();
  GM_setValue(PUSH_LIMIT, false);
  if (GM_getValue(RUN_DATE, -1) != d.toDateString()) {
    window.caches;
    GM_setValue(PUSH_COUNT, 0);
    GM_setValue(PUSH_LIMIT, false);
    GM_setValue(RUN_DATE, d.toDateString());
    console.log(
      "%c Hi,今天又是新的一天咯,元气满满找工作~也愿这是你我最后一次相遇🥳",
      "color:red;font-size:36px;"
    );
  }
}

// 清理
function clear() {
  runT = false;
  GM_setValue(PUSH_LOCK, false);
  GM_setValue(PUSH_LIMIT, false);
  GM_setValue(BATCH_ENABLE, false);
}

// 等待
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// 文档元素生成
function docEl() {
  const Div = document.createElement("div");
  const docDiv = document.createElement("div");
  const title = document.createElement("h2");
  Div.style.cssText = "overflow:hidden;height:32px;";
  title.textContent = `Boos直聘投递助手(${GM_getValue(PUSH_COUNT, 0)}次)`;
  title.style.cursor = "pointer";
  // 折叠功能(低能版)
  title.addEventListener("click", () => {
    if (Div.style.height == "32px") {
      Div.style.height = "420px";
    } else {
      Div.style.height = "32px";
    }
  });
  Div.appendChild(title);
  docDiv.style.cssText = "backgroundColor:#f2f2f2;padding:5px;width:100%;";
  for (let i = 0; i < docTextArr.length; i++) {
    const textTag = document.createElement("p");
    textTag.style.color = "rgb(127,124,l124);";
    textTag.innerHTML = docTextArr[i];
    docDiv.appendChild(textTag);
  }
  // 关于

  aboutLink.forEach((link) => {
    const about = document.createElement("p");
    link.forEach((item) => {
      const a = document.createElement("a");
      a.href = item[1];
      a.innerText = item[0];
      a.target = "_blank";
      a.style.margin = "0 20px 0 0";
      about.appendChild(a);
    });
    docDiv.appendChild(about);
  });

  // 增加观察者,实时修改(性能?不管~)
  GM_addValueChangeListener(
    PUSH_COUNT,
    function (name, old_value, new_value, remote) {
      title.textContent = `Boos直聘投递助手(${new_value}次)`;
    }
  );
  Div.appendChild(docDiv);
  return Div;
}
// 配置元素生成
function configEl() {
  // 加载持久化的配置,并加载到内存
  const config = JSON.parse(GM_getValue(LOCAL_CONFIG, "{}"));
  companyArr = companyArr.concat(config.companyArr);
  companyExclude = companyExclude.concat(config.companyExclude);
  descriptionExclude = descriptionExclude.concat(config.descriptionExclude);
  jobNameArr = jobNameArr.concat(config.jobNameArr);
  salaryRange = config.salaryRange ? config.salaryRange : salaryRange;
  companyScale = config.companyScale ? config.companyScale : companyScale;

  function renderConfigText() {
    /**
     * 渲染配置输入框
     * 将用户配置渲染到页面
     * 同时将钩子函数赋值!!!
     */
    const bossInput = document.createElement("div");
    bossInput.id = "boss-input";

    const companyLabel1 = document.createElement("label");
    companyLabel1.textContent = "公司名包含";
    const companyArr_ = document.createElement("input");
    companyArr_.type = "text";
    companyArr_.id = "companyArr";
    companyLabel1.appendChild(companyArr_);
    bossInput.appendChild(companyLabel1);
    companyArr_.value = deWeight(companyArr).join(",");

    const companyLabel2 = document.createElement("label");
    companyLabel2.textContent = "公司名排除";
    const companyExclude_ = document.createElement("input");
    companyExclude_.type = "text";
    companyExclude_.id = "companyExclude";
    companyLabel2.appendChild(companyExclude_);
    bossInput.appendChild(companyLabel2);
    companyExclude_.value = deWeight(companyExclude).join(",");

    const descriptionLabel = document.createElement("label");
    descriptionLabel.textContent = "工作内容排除";
    const descriptionExclude_ = document.createElement("input");
    descriptionExclude_.type = "text";
    descriptionExclude_.id = "descriptionExclude";
    descriptionLabel.appendChild(descriptionExclude_);
    bossInput.appendChild(descriptionLabel);
    descriptionExclude_.value = deWeight(descriptionExclude).join(",");

    const jobNameLabel = document.createElement("label");
    jobNameLabel.textContent = "Job名包含";
    const jobNameArr_ = document.createElement("input");
    jobNameArr_.type = "text";
    jobNameArr_.id = "jobNameArr";
    jobNameLabel.appendChild(jobNameArr_);
    bossInput.appendChild(jobNameLabel);
    jobNameArr_.value = deWeight(jobNameArr).join(",");

    const salaryLabel = document.createElement("label");
    salaryLabel.textContent = "薪资范围";
    const salaryRange_ = document.createElement("input");
    salaryRange_.type = "text";
    salaryRange_.id = "salaryRange";
    salaryLabel.appendChild(salaryRange_);
    bossInput.appendChild(salaryLabel);
    salaryRange_.value = salaryRange;

    const companyScaleLabel = document.createElement("label");
    companyScaleLabel.textContent = "公司规模范围";
    const companyScale_ = document.createElement("input");
    companyScale_.type = "text";
    companyScale_.id = "companyScale";
    companyScaleLabel.appendChild(companyScale_);
    bossInput.appendChild(companyScaleLabel);
    companyScale_.value = companyScale;

    // 美化样式
    bossInput.style.cssText =
      "padding: 20px; border: 1px solid rgb(204, 204, 204); background: rgb(240, 240, 240); border-radius: 10px; width: 100%;";

    const labels = bossInput.querySelectorAll("label");
    labels.forEach((label) => {
      label.style.cssText =
        "display: inline-block; width: 20%; font-weight: bold;";
    });

    const inputs = bossInput.querySelectorAll("input[type='text']");
    inputs.forEach((input) => {
      input.style.cssText =
        "margin-left: 10px; width: 70%; padding: 5px; border-radius: 5px; border: 1px solid rgb(204, 204, 204); box-sizing: border-box;";
    });

    loadConfig = () => {
      companyArr = companyArr_.value.split(",");
      companyExclude = companyExclude_.value.split(",");
      descriptionExclude = descriptionExclude_.value.split(",");
      jobNameArr = jobNameArr_.value.split(",");
      salaryRange = salaryRange_.value;
      companyScale = companyScale_.value = companyScale;
    };
    saveConfig = () => {
      const config = {
        companyArr: companyArr_.value.split(","),
        companyExclude: companyExclude_.value.split(","),
        descriptionExclude: descriptionExclude_.value.split(","),
        jobNameArr: jobNameArr_.value.split(","),
        salaryRange: salaryRange_.value,
        companyScale: companyScale_.value,
      };
      // 持久化配置
      GM_setValue(LOCAL_CONFIG, JSON.stringify(config));
    };
    return bossInput;
  }

  function deWeight(arr) {
    let uniqueArr = [];
    for (let i = 0; i < arr.length; i++) {
      if (uniqueArr.indexOf(arr[i]) === -1) {
        uniqueArr.push(arr[i]);
      }
    }
    return uniqueArr;
  }
  // 将配置渲染到页面
  return renderConfigText();
}

(function () {
  const list_url = "web/geek/job";
  const detail_url = "job_detail";
  if (document.URL.includes(list_url)) {
    window.addEventListener("load", jobListHandler);
  } else if (document.URL.includes(detail_url)) {
    window.addEventListener("load", jobDetailHandler);
  }
})();