SCALE — Build Lab
開発パターン · TYPESCRIPT LIBRARY

採用Slack通知パターン

CATEGORY開発パターン TYPETypeScript Library EFFORT90〜240分 DIFFICULTY
PRIMARY CODE
ts · lib/recruit-slack.ts
// Slack notification module for SCALE Recruit
// Uses Slack Bot Token via webhook-style POST

const SLACK_WEBHOOK_URL = process.env.NEXT_PUBLIC_SLACK_WEBHOOK_URL || ''
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || ''
const SLACK_CHANNEL = process.env.SLACK_CHANNEL_RECRUIT || '#recruit-alert'

interface SlackMessage {
  text: string
  blocks?: SlackBlock[]
}

interface SlackBlock {
  type: string
  text?: { type: string; text: string; emoji?: boolean }
  elements?: { type: string; text: string }[]
  fields?: { type: string; text: string }[]
}

async function sendSlack(message: SlackMessage): Promise<boolean> {
  // Try webhook first
  if (SLACK_WEBHOOK_URL) {
    try {
      const res = await fetch(SLACK_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(message),
      })
      return res.ok
    } catch {
      return false
    }
  }

  // Try Bot Token
  if (SLACK_BOT_TOKEN) {
    try {
      const res = await fetch('https://slack.com/api/chat.postMessage', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
        },
        body: JSON.stringify({
          channel: SLACK_CHANNEL,
          text: message.text,
          blocks: message.blocks,
        }),
      })
      return res.ok
    } catch {
      return false
    }
  }

  console.log('[Slack Mock]', message.text)
  return true
}

// ============================================
// Notification Templates
// ============================================

export async function notifyNewApplication(name: string, source: string, score: number | null) {
  const scoreText = score !== null ? `AIスコア: ${score}点` : 'スコア計算中'
  const action = score !== null && score >= 80 ? ' → 即面談候補' :
                 score !== null && score >= 60 ? ' → 書類通過' :
                 score !== null && score >= 40 ? ' → 要レビュー' : ''

  return sendSlack({
    text: `📥 新規応募: ${name}さん(${source}経由)${scoreText}${action}`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `📥 *新規応募*\n*${name}* さん` },
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*媒体:* ${source}` },
          { type: 'mrkdwn', text: `*${scoreText}*${action}` },
        ],
      },
    ],
  })
}

export async function notifyScreeningComplete(total: number, autoInterview: number, review: number) {
  return sendSlack({
    text: `🤖 本日の応募${total}件をスクリーニング完了。即面談候補${autoInterview}名、要レビュー${review}名`,
  })
}

export async function notifyInterviewScheduled(candidateName: string, date: string, interviewer: string) {
  return sendSlack({
    text: `📅 面談確定: ${candidateName}さん ${date} 担当: ${interviewer}`,
  })
}

export async function notifyInterviewReminder(candidateName: string, time: string, meetingUrl: string) {
  return sendSlack({
    text: `⏰ 本日の面談: ${time} ${candidateName}さん`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `⏰ *本日の面談リマインド*\n*${time}* — ${candidateName}さん\n<${meetingUrl}|Zoomに参加>` },
      },
    ],
  })
}

export async function notifyOfferSent(candidateName: string) {
  return sendSlack({
    text: `🎉 ${candidateName}さんにオファーメール送信完了`,
  })
}

export async function notifyContractSigned(candidateName: string) {
  return sendSlack({
    text: `✅ ${candidateName}さん 契約締結完了。オンボーディング開始`,
  })
}

export async function notifyOnboardingDelay(candidateName: string, step: string, daysPastDue: number) {
  return sendSlack({
    text: `⚠️ ${candidateName}さんの${step}が未完了(期限超過${daysPastDue}日)`,
  })
}

export async function notifyWorkStart(candidateName: string, projectName: string) {
  return sendSlack({
    text: `🚀 ${candidateName}さん 本日より稼働開始(${projectName}配属)`,
  })
}

export async function notifyRetentionRisk(candidateName: string, reason: string) {
  return sendSlack({
    text: `🔴 ${candidateName}さん: ${reason}。フォロー推奨`,
  })
}

export async function notifyWeeklyReport(stats: {
  applications: number
  interviews: number
  offers: number
  hires: number
  starts: number
}) {
  return sendSlack({
    text: `📊 今週: 応募${stats.applications}件、面談${stats.interviews}件、合格${stats.offers}名、稼働開始${stats.starts}名`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `📊 *週次採用レポート*` },
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*応募:* ${stats.applications}件` },
          { type: 'mrkdwn', text: `*面談:* ${stats.interviews}件` },
          { type: 'mrkdwn', text: `*合格:* ${stats.offers}名` },
          { type: 'mrkdwn', text: `*稼働開始:* ${stats.starts}名` },
        ],
      },
    ],
  })
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 採用管理
  • 商談ステータス通知
  • タスク完了通知

採用Slack通知パターン

:LiTarget: 用途

応募者の選考ステータス変更時に Slack へ自動通知するパターン。Bot Token + Block Kit。

:LiSparkle: 特徴

  • ステータス変更フック
  • Block Kit テンプレ
  • 通知先チャンネル可変
  • メンション制御

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: lib/recruit-slack.ts の中身そのもの。コピペ即可。

// Slack notification module for SCALE Recruit
// Uses Slack Bot Token via webhook-style POST

const SLACK_WEBHOOK_URL = process.env.NEXT_PUBLIC_SLACK_WEBHOOK_URL || ''
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || ''
const SLACK_CHANNEL = process.env.SLACK_CHANNEL_RECRUIT || '#recruit-alert'

interface SlackMessage {
  text: string
  blocks?: SlackBlock[]
}

interface SlackBlock {
  type: string
  text?: { type: string; text: string; emoji?: boolean }
  elements?: { type: string; text: string }[]
  fields?: { type: string; text: string }[]
}

async function sendSlack(message: SlackMessage): Promise<boolean> {
  // Try webhook first
  if (SLACK_WEBHOOK_URL) {
    try {
      const res = await fetch(SLACK_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(message),
      })
      return res.ok
    } catch {
      return false
    }
  }

  // Try Bot Token
  if (SLACK_BOT_TOKEN) {
    try {
      const res = await fetch('https://slack.com/api/chat.postMessage', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
        },
        body: JSON.stringify({
          channel: SLACK_CHANNEL,
          text: message.text,
          blocks: message.blocks,
        }),
      })
      return res.ok
    } catch {
      return false
    }
  }

  console.log('[Slack Mock]', message.text)
  return true
}

// ============================================
// Notification Templates
// ============================================

export async function notifyNewApplication(name: string, source: string, score: number | null) {
  const scoreText = score !== null ? `AIスコア: ${score}点` : 'スコア計算中'
  const action = score !== null && score >= 80 ? ' → 即面談候補' :
                 score !== null && score >= 60 ? ' → 書類通過' :
                 score !== null && score >= 40 ? ' → 要レビュー' : ''

  return sendSlack({
    text: `📥 新規応募: ${name}さん(${source}経由)${scoreText}${action}`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `📥 *新規応募*\n*${name}* さん` },
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*媒体:* ${source}` },
          { type: 'mrkdwn', text: `*${scoreText}*${action}` },
        ],
      },
    ],
  })
}

export async function notifyScreeningComplete(total: number, autoInterview: number, review: number) {
  return sendSlack({
    text: `🤖 本日の応募${total}件をスクリーニング完了。即面談候補${autoInterview}名、要レビュー${review}名`,
  })
}

export async function notifyInterviewScheduled(candidateName: string, date: string, interviewer: string) {
  return sendSlack({
    text: `📅 面談確定: ${candidateName}さん ${date} 担当: ${interviewer}`,
  })
}

export async function notifyInterviewReminder(candidateName: string, time: string, meetingUrl: string) {
  return sendSlack({
    text: `⏰ 本日の面談: ${time} ${candidateName}さん`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `⏰ *本日の面談リマインド*\n*${time}* — ${candidateName}さん\n<${meetingUrl}|Zoomに参加>` },
      },
    ],
  })
}

export async function notifyOfferSent(candidateName: string) {
  return sendSlack({
    text: `🎉 ${candidateName}さんにオファーメール送信完了`,
  })
}

export async function notifyContractSigned(candidateName: string) {
  return sendSlack({
    text: `✅ ${candidateName}さん 契約締結完了。オンボーディング開始`,
  })
}

export async function notifyOnboardingDelay(candidateName: string, step: string, daysPastDue: number) {
  return sendSlack({
    text: `⚠️ ${candidateName}さんの${step}が未完了(期限超過${daysPastDue}日)`,
  })
}

export async function notifyWorkStart(candidateName: string, projectName: string) {
  return sendSlack({
    text: `🚀 ${candidateName}さん 本日より稼働開始(${projectName}配属)`,
  })
}

export async function notifyRetentionRisk(candidateName: string, reason: string) {
  return sendSlack({
    text: `🔴 ${candidateName}さん: ${reason}。フォロー推奨`,
  })
}

export async function notifyWeeklyReport(stats: {
  applications: number
  interviews: number
  offers: number
  hires: number
  starts: number
}) {
  return sendSlack({
    text: `📊 今週: 応募${stats.applications}件、面談${stats.interviews}件、合格${stats.offers}名、稼働開始${stats.starts}名`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `📊 *週次採用レポート*` },
      },
      {
        type: 'section',
        fields: [
          { type: 'mrkdwn', text: `*応募:* ${stats.applications}件` },
          { type: 'mrkdwn', text: `*面談:* ${stats.interviews}件` },
          { type: 'mrkdwn', text: `*合格:* ${stats.offers}名` },
          { type: 'mrkdwn', text: `*稼働開始:* ${stats.starts}名` },
        ],
      },
    ],
  })
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/recruit-slack.ts

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加