# 技術スキルリファレンス

このドキュメントは、メール送付システムで使用している技術パターンとベストプラクティスをまとめたものです。  
他のプロジェクトでも参考にしてください。

---

## 目次

1. [Microsoft Graph API メール操作](#1-microsoft-graph-api-メール操作)
2. [MSAL.js 認証フロー](#2-msaljs-認証フロー)
3. [Firebase/Firestore 統合](#3-firebasefirestore-統合)
4. [メール一括送信システム設計](#4-メール一括送信システム設計)
5. [CSV処理・文字コード対応](#5-csv処理文字コード対応)
6. [Dify API 連携](#6-dify-api-連携)
7. [ローカルプロキシサーバー](#7-ローカルプロキシサーバー)
8. [UI/UXパターン](#8-uiuxパターン)

---

## 1. Microsoft Graph API メール操作

Microsoft 365のメール機能をAPI経由で操作するパターンです。

### 必要なスコープ

| スコープ | 用途 |
|---------|------|
| `Mail.Send` | メール送信 |
| `Mail.Read` | 自分のメールボックス読み取り |
| `Mail.Read.Shared` | 共有メールボックス読み取り |

### メール送信

```javascript
const response = await fetch('https://graph.microsoft.com/v1.0/me/sendMail', {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        message: {
            subject: '件名',
            body: {
                contentType: 'HTML',  // または 'Text'
                content: '<p>本文</p>'
            },
            toRecipients: [
                { emailAddress: { address: 'recipient@example.com' } }
            ]
        }
    })
});
```

### メール取得

```javascript
// 受信トレイ
const inbox = await fetch(
    `https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?$top=100&$filter=receivedDateTime ge ${fromDate}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
);

// 送信済み
const sent = await fetch(
    `https://graph.microsoft.com/v1.0/me/mailFolders/sentitems/messages?$top=100`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
);
```

### 添付ファイル

```javascript
message.attachments = [
    {
        '@odata.type': '#microsoft.graph.fileAttachment',
        name: 'document.pdf',
        contentType: 'application/pdf',
        contentBytes: base64EncodedContent  // Base64エンコード済み
    }
];
```

### エラーハンドリング

| ステータス | 対応 |
|-----------|------|
| 401 | トークン期限切れ → 再認証 |
| 429 | レート制限 → 待機後リトライ |
| 503 | サービス一時停止 → リトライ |

---

## 2. MSAL.js 認証フロー

Microsoft Authentication Library (MSAL) を使用した認証パターンです。

### 初期化

```javascript
const msalConfig = {
    auth: {
        clientId: 'YOUR_CLIENT_ID',
        authority: 'https://login.microsoftonline.com/common',
        redirectUri: window.location.origin
    },
    cache: {
        cacheLocation: 'localStorage'  // セッション間で保持
    }
};

const msalInstance = new msal.PublicClientApplication(msalConfig);
```

### ログイン方式

#### ポップアップ方式（推奨）

```javascript
try {
    const response = await msalInstance.loginPopup({
        scopes: ['User.Read', 'Mail.Send', 'Mail.Read']
    });
    console.log('ログイン成功:', response.account.username);
} catch (error) {
    console.error('ログイン失敗:', error);
}
```

#### リダイレクト方式

```javascript
// ログイン開始
msalInstance.loginRedirect({ scopes: ['User.Read'] });

// リダイレクト後の処理
msalInstance.handleRedirectPromise().then(response => {
    if (response) {
        console.log('ログイン成功');
    }
});
```

### トークン取得（サイレント優先）

```javascript
async function getAccessToken(scopes) {
    const account = msalInstance.getAllAccounts()[0];
    
    try {
        // まずサイレント取得を試行
        const response = await msalInstance.acquireTokenSilent({
            scopes: scopes,
            account: account
        });
        return response.accessToken;
    } catch (error) {
        // 失敗時はインタラクティブ
        const response = await msalInstance.acquireTokenPopup({
            scopes: scopes
        });
        return response.accessToken;
    }
}
```

### 注意事項

- COOP (Cross-Origin-Opener-Policy) 警告が出る場合がありますが、機能には影響しません
- ポップアップブロッカーに注意が必要です

---

## 3. Firebase/Firestore 統合

Firebaseのリアルタイムデータベース Firestore を使用するパターンです。

### 初期化

```javascript
// Firebase設定
const firebaseConfig = {
    apiKey: 'YOUR_API_KEY',
    authDomain: 'project-id.firebaseapp.com',
    projectId: 'project-id'
};

firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
```

### 基本操作

#### 追加

```javascript
// 自動ID
const docRef = await db.collection('items').add({
    name: 'Item Name',
    createdAt: firebase.firestore.FieldValue.serverTimestamp()
});
console.log('追加されたID:', docRef.id);

// 指定ID
await db.collection('items').doc('custom-id').set({
    name: 'Item Name'
});
```

#### 取得

```javascript
// 全件取得
const snapshot = await db.collection('items').get();
snapshot.forEach(doc => {
    console.log(doc.id, '=>', doc.data());
});

// 条件付き取得
const activeItems = await db.collection('items')
    .where('status', '==', 'active')
    .limit(10)
    .get();
```

#### 更新

```javascript
await db.collection('items').doc('id').update({
    name: 'Updated Name',
    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
});
```

#### 削除

```javascript
await db.collection('items').doc('id').delete();
```

### バッチ処理（500件まで）

```javascript
const batch = db.batch();

items.forEach(item => {
    const ref = db.collection('items').doc();
    batch.set(ref, item);
});

await batch.commit();
console.log('バッチ書き込み完了');
```

### セキュリティルール（firestore.rules）

```javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 認証済みユーザーのみアクセス可能
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
    
    // パブリックアクセス（開発用）
    match /publicCollection/{doc} {
      allow read, write: if true;
    }
  }
}
```

---

## 4. メール一括送信システム設計

大量のメールを安全に送信するためのシステム設計パターンです。

### アーキテクチャ

```
[CSV読み込み] → [プレビュー] → [停止リストチェック] → [送信] → [履歴保存]
                                    ↓
                              [Firestore停止リスト]
```

### 停止リスト管理

```javascript
// Setで管理（高速な検索）
const stopList = new Set();

// CSVから読み込み
csvEmails.forEach(email => stopList.add(email.toLowerCase()));

// Firestoreから追加読み込み
const snapshot = await db.collection('stopList').get();
snapshot.forEach(doc => stopList.add(doc.data().email.toLowerCase()));

// チェック
function isBlocked(email) {
    return stopList.has(email.toLowerCase());
}
```

### プレースホルダー置換

```javascript
function replacePlaceholders(template, rowData, headers) {
    let result = template;
    headers.forEach((header, index) => {
        const placeholder = new RegExp(`\\{${header}\\}`, 'g');
        result = result.replace(placeholder, rowData[index] || '');
    });
    return result;
}

// 使用例
// テンプレート: "こんにちは、{name}様"
// データ: ["田中太郎"]
// ヘッダー: ["name"]
// 結果: "こんにちは、田中太郎様"
```

### バウンス・配信停止検出

```javascript
const bounceKeywords = [
    'Undeliverable', 'Mail Delivery Failed', 'Delivery Status Notification',
    '配信不能', '送信エラー', 'アドレスが見つかりません'
];

const unsubKeywords = [
    '配信停止', '購読解除', 'unsubscribe', '登録解除'
];

function detectBounce(message) {
    const content = (message.subject + ' ' + message.body?.content).toLowerCase();
    return bounceKeywords.some(kw => content.includes(kw.toLowerCase()));
}
```

### レート制限対策

```javascript
async function sendWithDelay(recipients, delayMs = 500) {
    for (const recipient of recipients) {
        await sendEmail(recipient);
        await new Promise(resolve => setTimeout(resolve, delayMs));
    }
}
```

---

## 5. CSV処理・文字コード対応

日本語CSVファイルを正しく処理するパターンです。

### 文字コード自動判定

```javascript
function detectEncoding(arrayBuffer) {
    const bytes = new Uint8Array(arrayBuffer);
    
    // UTF-8 BOM
    if (bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) {
        return 'utf-8';
    }
    
    // UTF-16 LE BOM
    if (bytes[0] === 0xFF && bytes[1] === 0xFE) {
        return 'utf-16le';
    }
    
    // デフォルト: Shift-JIS（日本語環境）
    return 'shift-jis';
}

// 使用
const buffer = await file.arrayBuffer();
const encoding = detectEncoding(buffer);
const text = new TextDecoder(encoding).decode(buffer);
```

### CSVパース（Papa Parse使用）

```javascript
// ライブラリ読み込み
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.2/papaparse.min.js"></script>

// パース
const result = Papa.parse(csvText, {
    header: true,           // 1行目をヘッダーとして使用
    skipEmptyLines: true,   // 空行をスキップ
    dynamicTyping: false    // 型変換しない（文字列のまま）
});

const headers = result.meta.fields;  // ヘッダー配列
const data = result.data;            // データ配列（オブジェクト形式）
```

### CSVエクスポート（Excel対応）

```javascript
function exportCSV(data, filename) {
    const csv = Papa.unparse(data);
    
    // BOM付きUTF-8でExcel互換
    const bom = '\uFEFF';
    const blob = new Blob([bom + csv], { type: 'text/csv;charset=utf-8' });
    
    // ダウンロード
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    a.click();
    URL.revokeObjectURL(url);
}
```

---

## 6. Dify API 連携

Dify（AI開発プラットフォーム）を使用したテキスト生成パターンです。

### 基本呼び出し

```javascript
const response = await fetch('https://chatbot.aimeet.jp/v1/completion-messages', {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        inputs: {
            query: 'ユーザーの入力',
            purpose: '目的',
            tone: 'very_polite',      // very_polite / standard / casual
            language: 'ja',           // ja / en / zh
            extra_requests: '追加要望',
            input_format: 'html'      // html / text
        },
        response_mode: 'streaming',
        user: 'user-identifier'
    })
});
```

### ストリーミング処理

```javascript
async function consumeStream(response) {
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let result = '';
    
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        
        const chunk = decoder.decode(value, { stream: true });
        
        // SSE形式をパース
        const lines = chunk.split('\n');
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                try {
                    const json = JSON.parse(line.slice(6));
                    if (json.event === 'message' && json.answer) {
                        result += json.answer;
                        updateUI(result);  // リアルタイム表示更新
                    }
                } catch (e) {}
            }
        }
    }
    
    return result;
}
```

### API Key管理（チーム共有）

```javascript
// 保存（Firestore）
await db.collection('settings').doc('difyApiKey').set({
    apiKey: key,
    updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
    updatedBy: userEmail
});

// 読み込み
const doc = await db.collection('settings').doc('difyApiKey').get();
if (doc.exists) {
    return doc.data().apiKey;
}
```

---

## 7. ローカルプロキシサーバー

CORS制限を回避するためのNode.jsプロキシサーバーです。

### 基本実装

```javascript
const http = require('http');
const PORT = 8787;

const server = http.createServer(async (req, res) => {
    // CORS設定
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    
    // プリフライトリクエスト
    if (req.method === 'OPTIONS') {
        res.statusCode = 204;
        res.end();
        return;
    }
    
    // POSTリクエストのみ処理
    if (req.method === 'POST' && req.url === '/difyProxy') {
        const body = await readBody(req);
        const externalResponse = await fetch('https://external-api.com/endpoint', {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${body.apiKey}` },
            body: JSON.stringify(body)
        });
        
        // ストリーミング転送
        const { Readable } = require('stream');
        const stream = Readable.fromWeb(externalResponse.body);
        stream.pipe(res);
    }
});

server.listen(PORT, () => {
    console.log(`Proxy running at http://localhost:${PORT}`);
});
```

### 起動方法

```bash
node local_proxy.js
# → http://localhost:8787/difyProxy で利用可能
```

---

## 8. UI/UXパターン

モダンなWebアプリのUI実装パターンです。

### モーダルダイアログ

```html
<div class="modal-overlay" id="myModal" style="display: none;">
    <div class="modal-content">
        <div class="modal-header">
            <h2>タイトル</h2>
            <button onclick="closeModal()">×</button>
        </div>
        <div class="modal-body">
            コンテンツ
        </div>
    </div>
</div>
```

```css
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal-content {
    background: white;
    border-radius: 12px;
    max-width: 800px;
    max-height: 90vh;
    overflow-y: auto;
}
```

```javascript
// ESCキーで閉じる
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal();
});

// オーバーレイクリックで閉じる
document.getElementById('myModal').addEventListener('click', (e) => {
    if (e.target === e.currentTarget) closeModal();
});
```

### プログレスログ表示

```javascript
function addLog(message, type = 'info') {
    const logContainer = document.getElementById('logContainer');
    const colors = {
        success: '#10b981',
        error: '#ef4444',
        warning: '#f59e0b',
        info: '#6b7280'
    };
    
    logContainer.innerHTML += `
        <div style="color: ${colors[type]}; padding: 4px 0;">
            ${new Date().toLocaleTimeString()} - ${message}
        </div>
    `;
    
    // 自動スクロール
    logContainer.scrollTop = logContainer.scrollHeight;
}
```

### ステップウィザード

```javascript
function showStep(stepNumber) {
    // 全ステップを非表示
    document.querySelectorAll('.step-content').forEach(el => {
        el.style.display = 'none';
    });
    
    // 指定ステップを表示
    document.getElementById(`step${stepNumber}`).style.display = 'block';
    
    // ステップインジケーター更新
    document.querySelectorAll('.step-indicator').forEach((el, index) => {
        el.classList.toggle('active', index + 1 === stepNumber);
        el.classList.toggle('completed', index + 1 < stepNumber);
    });
}
```

---

## ライセンス

© 2026 Techsor Inc. All rights reserved.

このドキュメントは社内利用を目的としています。
