Claude-skill-registry hojokin-navi
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/hojokin-navi" ~/.claude/skills/majiayu000-claude-skill-registry-hojokin-navi && rm -rf "$T"
manifest:
skills/data/hojokin-navi/SKILL.mdsource content
補助金ナビ開発スキル
JグランツAPI
エンドポイント
BASE_URL = https://api.jgrants-portal.go.jp/exp/v1/public
| エンドポイント | 用途 |
|---|---|
| 一覧取得(検索) |
| 詳細取得 |
一覧取得
// 募集中の補助金を取得 const request = { acceptance: 1, // 1=募集中, 0=全て keyword: "IT", // オプション area: "東京都", // オプション }; const url = `${BASE_URL}/subsidies?request=${encodeURIComponent(JSON.stringify(request))}`; const res = await fetch(url); const { result, metadata } = await res.json(); // result: JGrantsSubsidy[] // metadata.resultset.count: 総件数
詳細取得
const res = await fetch(`${BASE_URL}/subsidies/id/${jgrantsId}`); const { result } = await res.json(); const subsidy = result[0]; // 単一オブジェクト
レスポンス型定義
type JGrantsSubsidy = { id: string; // "a0WJ200000CDWaWMAX" name: string; // "S-00007689" title: string; subsidy_catch_phrase?: string; detail?: string; // ⚠️ HTML含む target_area_search?: string; target_area_detail?: string; industry?: string; // ⚠️ "/"区切り use_purpose?: string; subsidy_max_limit?: number; subsidy_rate?: string; target_number_of_employees?: string; acceptance_start_datetime?: string; acceptance_end_datetime?: string; front_subsidy_detail_page_url?: string; application_guidelines?: Array<{ // ⚠️ 大容量 name: string; data: string; // base64 }>; };
データ変換
業種パース
// "製造業/建設業/情報通信業" → ["製造業", "建設業", "情報通信業"] function parseIndustry(industry?: string): string[] { if (!industry) return []; return industry.split('/').map(s => s.trim()).filter(Boolean); }
HTMLサニタイズ
import DOMPurify from 'isomorphic-dompurify'; function sanitizeHtml(html?: string): string { if (!html) return ''; return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'], ALLOWED_ATTR: ['href', 'target'], }); }
DB保存用変換
function toDbSubsidy(api: JGrantsSubsidy) { return { jgrants_id: api.id, name: api.name, title: api.title, catch_phrase: api.subsidy_catch_phrase, description: sanitizeHtml(api.detail), target_area: api.target_area_search ? [api.target_area_search] : [], target_area_detail: api.target_area_detail, industry: parseIndustry(api.industry), use_purpose: api.use_purpose, max_amount: api.subsidy_max_limit, subsidy_rate: api.subsidy_rate, start_date: api.acceptance_start_datetime, end_date: api.acceptance_end_datetime, front_url: api.front_subsidy_detail_page_url, is_active: true, updated_at: new Date().toISOString(), }; }
Supabase操作
Upsert(作成 or 更新)
const { error } = await supabase .from('subsidies') .upsert(toDbSubsidy(apiData), { onConflict: 'jgrants_id', });
検索クエリ
// 基本検索 let query = supabase .from('subsidies') .select('*') .eq('is_active', true) .order('end_date', { ascending: true }); // キーワード検索(ILIKE) if (keyword) { query = query.or(`title.ilike.%${keyword}%,description.ilike.%${keyword}%`); } // 地域フィルター if (area) { query = query.contains('target_area', [area]); } // 業種フィルター(JSONB配列) if (industry) { query = query.contains('industry', [industry]); } // 金額フィルター if (maxAmount) { query = query.lte('max_amount', maxAmount); } // ページネーション const { data, count } = await query .range(offset, offset + limit - 1) .select('*', { count: 'exact' });
Slack通知
Webhook送信
async function sendSlackNotification(message: string, blocks?: any[]) { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: message, blocks, }), }); }
新着補助金通知テンプレート
const blocks = [ { type: 'header', text: { type: 'plain_text', text: '🆕 新着補助金' }, }, { type: 'section', text: { type: 'mrkdwn', text: `*${subsidy.title}*\n${subsidy.catch_phrase || ''}`, }, }, { type: 'section', fields: [ { type: 'mrkdwn', text: `*上限額:* ${formatCurrency(subsidy.max_amount)}` }, { type: 'mrkdwn', text: `*締切:* ${formatDate(subsidy.end_date)}` }, ], }, { type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: '詳細を見る' }, url: `${APP_URL}/subsidies/${subsidy.id}`, }], }, ];
バッチ処理
データ同期フロー
1. JグランツAPI一覧取得(acceptance=1) 2. 各補助金の詳細取得(並列、rate limit考慮) 3. データ変換(業種パース、HTMLサニタイズ) 4. Supabase Upsert 5. 新規追加分をSlack通知 6. 募集終了分のis_active更新
Vercel Cron設定
// vercel.json { "crons": [{ "path": "/api/cron/sync-subsidies", "schedule": "0 3 * * 1" // 毎週月曜3時 }] }
ユーティリティ
金額フォーマット
function formatCurrency(amount?: number): string { if (!amount) return '金額未定'; if (amount >= 100000000) return `${(amount / 100000000).toFixed(1)}億円`; if (amount >= 10000) return `${(amount / 10000).toFixed(0)}万円`; return `${amount.toLocaleString()}円`; }
残り日数計算
function getDaysRemaining(endDate?: string): number | null { if (!endDate) return null; const end = new Date(endDate); const now = new Date(); const diff = end.getTime() - now.getTime(); return Math.ceil(diff / (1000 * 60 * 60 * 24)); }
締切ステータス
function getDeadlineStatus(days: number | null): 'urgent' | 'soon' | 'normal' | 'closed' { if (days === null) return 'normal'; if (days < 0) return 'closed'; if (days <= 7) return 'urgent'; if (days <= 30) return 'soon'; return 'normal'; }
注意事項
- APIレート: 公式制限なしだが、並列リクエストは5件程度に抑える
- PDF:
は保存しない(数MB/件)application_guidelines - 文字コード: APIはUTF-8、特殊文字エスケープ済み
- 日付: ISO 8601形式、タイムゾーンはJST想定
- NULL処理: オプショナルフィールドは全てnull許容