採用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: 注意事項
- 依存パッケージを忘れず追加