APIを叩くツール（個人）

# 全体構成
- HTML: 画面レイアウト（ヘッダ、プルダウン、パラメータ編集部、レスポンス表示部）。
- CSS: プルダウンの横並び、開閉可能なセクション、レスポンス表示の整形。
- JavaScript: プルダウンの動的生成、CSV/JS読み込み、APIリクエスト送信、レスポンス処理。
- Excel（VBA）: 管理シートからプルダウン内容やパラメータをCSVまたはJSファイルに出力。
- 運用:管理シートに環境、機能、API、パラメータを定義。VBAでデータをdata.jsまたはdata.csvに出力。HTMLがdata.jsを読み込み、画面を動的に生成。ユーザーがプルダウンで選択し、パラメータを編集後、GET/POSTでAPIを呼び出し。

# VBA
```vb
Sub ExportToJs()
    Dim ws As Worksheet
    Dim filePath As String
    Dim row As Long, col As Long
    Dim paramId As Long
    Dim currentPaths As Object ' Collection
    Dim arrayIndices As Object ' Dictionary
    Dim output As String
    
    filePath = ThisWorkbook.Path & "\data.js"
    
    ' 出力バッファを構築
    output = "const data = {" & vbCrLf
    
    ' 環境
    Set ws = ThisWorkbook.Sheets("Environment")
    output = output & "  environments: [" & vbCrLf
    For row = 2 To ws.Cells(ws.Rows.Count, 1).End(xlUp).row
        output = output & "    { id: """ & ws.Cells(row, 1).Value & """, name: """ & ws.Cells(row, 2).Value & """, baseURL: """ & ws.Cells(row, 3).Value & """ }," & vbCrLf
    Next row
    output = output & "  ]," & vbCrLf
    
    ' 機能
    Set ws = ThisWorkbook.Sheets("Function")
    output = output & "  functions: [" & vbCrLf
    For row = 2 To ws.Cells(ws.Rows.Count, 1).End(xlUp).row
        output = output & "    { id: """ & ws.Cells(row, 1).Value & """, name: """ & ws.Cells(row, 2).Value & """ }," & vbCrLf
    Next row
    output = output & "  ]," & vbCrLf
    
    ' API
    output = output & "  apis: [" & vbCrLf
    For Each ws In ThisWorkbook.Sheets
        If ws.Name <> "Environment" And ws.Name <> "Function" Then
            Dim apiId As String: apiId = ws.Range("D1").Value
            Dim apiName As String: apiName = ws.Range("D2").Value
            Dim functionId As String: functionId = Left(apiId, 8) ' 例: KLLLAA00
            output = output & "    { functionId: """ & functionId & """, id: """ & apiId & """, name: """ & apiName & """ }," & vbCrLf
        End If
    Next ws
    output = output & "  ]," & vbCrLf
    
    ' パラメータ
    output = output & "  parameters: [" & vbCrLf
    For Each ws In ThisWorkbook.Sheets
        If ws.Name <> "Environment" And ws.Name <> "Function" Then
            apiId = ws.Range("D1").Value
            apiName = ws.Range("D2").Value
            functionId = Left(apiId, 8) ' 例: KLLLAA00
            
            ' パラメータ（BD列以降）
            Dim paramCount As Long: paramCount = ws.Cells(4, 56).End(xlToRight).Column - 56 + 1 ' BD=56
            For col = 56 To 55 + paramCount
                paramId = paramId + 1
                Dim paramIdStr As String: paramIdStr = Format(paramId, "000")
                Dim paramName As String: paramName = ws.Cells(4, col).Value
                If paramName = "" Then paramName = "パラメータ" & paramIdStr
                
                ' ネスト/配列構造を解析
                Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, 3).End(xlUp).row
                Set currentPaths = CreateObject("Scripting.Dictionary") ' 各要素はfullPathとitemType
                Set arrayIndices = CreateObject("Scripting.Dictionary") ' 配列名 -> 次のインデックス
                
                For row = 4 To lastRow
                    ' 値がある最初の列（C=3～L=12）を特定
                    Dim level As Long: level = 3
                    While level <= 12 And ws.Cells(row, level).Value = ""
                        level = level + 1
                    Wend
                    If level > 12 Then GoTo NextRow
                    
                    Dim depth As Long: depth = level - 3
                    Dim key As String: key = ws.Cells(row, level).Value
                    Dim itemType As String: itemType = ws.Cells(row, level + 23).Value ' AA列
                    
                    ' currentPathsをdepthに合わせて調整
                    While currentPaths.Count > depth
                        currentPaths.Remove currentPaths.Keys()(currentPaths.Count - 1)
                    Wend
                    
                    ' fullPathを構築
                    Dim fullPath As String
                    If currentPaths.Count = 0 Then
                        fullPath = key
                        If itemType = "配列" Then fullPath = fullPath & "[0]"
                    Else
                        Dim parent As Variant: parent = currentPaths.Item(currentPaths.Keys()(currentPaths.Count - 1))
                        Dim parentPath As String: parentPath = parent(0)
                        Dim parentType As String: parentType = parent(1)
                        
                        If parentType = "配列" Then
                            Dim arrayName As String: arrayName = Split(parentPath, "[")(0)
                            If Not arrayIndices.Exists(arrayName) Then arrayIndices(arrayName) = 0
                            Dim index As Long: index = arrayIndices(arrayName)
                            fullPath = parentPath & "[" & index & "]"
                            If itemType = "配列" Then
                                fullPath = fullPath & "." & key & "[0]"
                            Else
                                fullPath = fullPath & "." & key
                            End If
                            arrayIndices(arrayName) = index + 1
                        Else
                            fullPath = parentPath & "." & key
                            If itemType = "配列" Then fullPath = fullPath & "[0]"
                        End If
                    End If
                    
                    ' currentPathsに追加（構造マーカー）
                    If itemType = "オブジェクト" Or itemType = "配列" Then
                        currentPaths(depth) = Array(fullPath, itemType)
                    End If
                    
                    ' プロパティ行の場合、パラメータを生成
                    If itemType = "" Then
                        Dim logicalName As String: logicalName = ws.Cells(row, level + 11).Value ' N列以降
                        Dim value As String: value = ws.Cells(row, col).Value
                        If value <> "" Then
                            output = output & "    { apiId: """ & apiId & """, id: """ & paramIdStr & """, name: """ & paramName & """, logicalName: """ & logicalName & """, physicalName: """ & fullPath & """, value: """ & Replace(value, """", "\""") & """ }," & vbCrLf
                        End If
                    End If
                    
NextRow:
                Next row
            Next col
        End If
    Next ws
    output = output & "  ]" & vbCrLf
    output = output & "};"
    
    ' Shift_JISでファイル出力
    With CreateObject("ADODB.Stream")
        .Type = 2 ' テキスト
        .Charset = "Shift_JIS"
        .Open
        .WriteText output
        .SaveToFile filePath, 2 ' 上書き
        .Close
    End With
    
    MsgBox "Exported to " & filePath
End Sub
```

# HTML
index.html
```html
<!DOCTYPE html>
<html>
<head>
    <title>Web API Test Tool</title>
    <meta charset="Shift_JIS">
    <link rel="stylesheet" href="styles.css">
    <script src="data.js"></script>
    <script src="script.js"></script>
</head>
<body>
    <!-- ヘッダ部分 -->
    <div class="header">
        <div class="dropdown">
            <label>環境</label>
            <select id="environment" onchange="updateURL()">
                <option value="">選択してください</option>
            </select>
        </div>
        <div class="dropdown">
            <label>機能</label>
            <select id="function" onchange="updateApis()">
                <option value="">選択してください</option>
            </select>
        </div>
        <div class="dropdown">
            <label>API</label>
            <select id="api" onchange="updateParameters()">
                <option value="">選択してください</option>
            </select>
        </div>
        <div class="dropdown">
            <label>パラメータ</label>
            <select id="parameter" onchange="displayParameterDetails()">
                <option value="">選択してください</option>
            </select>
        </div>
    </div>

    <!-- リクエスト送信先URL -->
    <div class="url-header">
        <label>リクエスト送信先URL: </label>
        <input type="text" id="requestUrl" oninput="updateCustomURL()">
    </div>

    <!-- リクエストヘッダ -->
    <div class="section">
        <h2 onclick="toggleSection('headers')">リクエストヘッダ <span id="headers-toggle">[+]</span></h2>
        <div id="headers" class="collapsible">
            <textarea id="requestHeaders" placeholder='{"Content-Type": "application/json"}'></textarea>
        </div>
    </div>

    <!-- メソッド -->
    <div class="method">
        <label>メソッド: </label>
        <select id="method">
            <option value="GET">GET</option>
            <option value="POST">POST</option>
        </select>
        <button onclick="sendRequest()">送信</button>
    </div>

    <!-- パラメータ表示部 -->
    <div class="section">
        <h2 onclick="toggleSection('parameters')">パラメータ <span id="parameters-toggle">[+]</span></h2>
        <div id="parameters" class="collapsible">
            <table>
                <thead>
                    <tr>
                        <th>項目論理名</th>
                        <th>項目物理名</th>
                        <th>設定値</th>
                    </tr>
                </thead>
                <tbody id="parameterDetails"></tbody>
            </table>
        </div>
    </div>

    <!-- レスポンス表示部 -->
    <div class="section">
        <h2 onclick="toggleSection('response')">レスポンス <span id="response-toggle">[+]</span></h2>
        <div id="response" class="collapsible">
            <pre id="responseContent"></pre>
        </div>
    </div>
</body>
</html>
```

# CSS
styles.html
```css
body {
    font-family: Arial, sans-serif;
    margin: 20px;
}
.header {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}
.dropdown {
    display: flex;
    align-items: center;
    gap: 5px;
}
.dropdown label {
    font-weight: bold;
}
.url-header, .method {
    margin: 10px 0;
}
.url-header input {
    width: 50%;
    padding: 5px;
}
.section h2 {
    cursor: pointer;
    background: #f0f0f0;
    padding: 10px;
    margin: 10px 0;
}
.collapsible {
    display: none;
    padding: 10px;
}
.collapsible.open {
    display: block;
}
#headers textarea {
    width: 100%;
    height: 50px; /* 狭くする */
    resize: vertical;
}
table {
    width: 100%;
    border-collapse: collapse;
}
th, td {
    border: 1px solid #ccc;
    padding: 8px;
    text-align: left;
}
th {
    background: #f0f0f0;
}
input[type="text"] {
    width: 100%;
    box-sizing: border-box;
}
```

# JS
script.js
```js
// カスタムURLを保持
let customURL = '';

// プルダウン初期化
window.onload = () => {
    initDropdowns();
    updateURL();
};

// プルダウン初期化
function initDropdowns() {
    const envSelect = document.getElementById('environment');
    const funcSelect = document.getElementById('function');
    
    // 環境（最初の項目をデフォルト選択）
    data.environments.forEach((env, index) => {
        const option = document.createElement('option');
        option.value = env.id;
        option.textContent = env.name;
        envSelect.appendChild(option);
        if (index === 0) {
            envSelect.value = env.id; // デフォルト選択
        }
    });

    // 機能
    data.functions.forEach(func => {
        const option = document.createElement('option');
        option.value = func.id;
        option.textContent = `${func.id}_${func.name}`;
        funcSelect.appendChild(option);
    });

    updateApis(); // APIプルダウンを初期化
}

// APIプルダウン更新
function updateApis() {
    const funcSelect = document.getElementById('function');
    const apiSelect = document.getElementById('api');
    apiSelect.innerHTML = '<option value="">選択してください</option>';

    const funcId = funcSelect.value;
    if (funcId) {
        data.apis
            .filter(api => api.functionId === funcId)
            .forEach(api => {
                const option = document.createElement('option');
                option.value = api.id;
                option.textContent = `${api.id}_${api.name}`;
                apiSelect.appendChild(option);
            });
    }
    updateParameters();
    updateURL();
}

// パラメータプルダウン更新
function updateParameters() {
    const apiSelect = document.getElementById('api');
    const paramSelect = document.getElementById('parameter');
    paramSelect.innerHTML = '<option value="">選択してください</option>';

    const apiId = apiSelect.value;
    if (apiId) {
        const paramGroups = {};
        data.parameters
            .filter(param => param.apiId === apiId)
            .forEach(param => {
                if (!paramGroups[param.id]) {
                    paramGroups[param.id] = param.name;
                }
            });

        Object.entries(paramGroups).forEach(([id, name]) => {
            const option = document.createElement('option');
            option.value = id;
            option.textContent = `${id}_${name}`;
            paramSelect.appendChild(option);
        });
    }
    displayParameterDetails();
}

// パラメータ詳細表示
function displayParameterDetails() {
    const apiSelect = document.getElementById('api');
    const paramSelect = document.getElementById('parameter');
    const paramDetails = document.getElementById('parameterDetails');
    paramDetails.innerHTML = '';

    const apiId = apiSelect.value;
    const paramId = paramSelect.value;
    if (apiId && paramId) {
        data.parameters
            .filter(param => param.apiId === apiId && param.id === paramId)
            .forEach(param => {
                if (param.value !== "") {
                    const row = document.createElement('tr');
                    row.innerHTML = `
                        <td>${param.logicalName}</td>
                        <td>${param.physicalName}</td>
                        <td><input type="text" value="${param.value}" onchange="updateParameterValue('${param.physicalName}', this.value)"></td>
                    `;
                    paramDetails.appendChild(row);
                }
            });
    }
}

// パラメータ値更新
let editedParameters = {};
function updateParameterValue(physicalName, value) {
    editedParameters[physicalName] = value;
}

// カスタムURL更新
function updateCustomURL() {
    customURL = document.getElementById('requestUrl').value;
}

// リクエストURL更新
function updateURL() {
    const envSelect = document.getElementById('environment');
    const apiSelect = document.getElementById('api');
    const requestUrl = document.getElementById('requestUrl');
    
    const envId = envSelect.value;
    const apiId = apiSelect.value;
    if (envId && apiId) {
        const env = data.environments.find(e => e.id === envId);
        const defaultUrl = `${env.baseURL}${apiId}`;
        requestUrl.value = customURL || defaultUrl; // カスタムURLがなければデフォルト
        customURL = ''; // リセット
    } else {
        requestUrl.value = '';
    }
}

// セクションの開閉
function toggleSection(sectionId) {
    const section = document.getElementById(sectionId);
    const toggle = document.getElementById(`${sectionId}-toggle`);
    section.classList.toggle('open');
    toggle.textContent = section.classList.contains('open') ? '[-]' : '[+]';
}

// リクエスト送信
function sendRequest() {
    const envSelect = document.getElementById('environment');
    const apiSelect = document.getElementById('api');
    const methodSelect = document.getElementById('method');
    const headersInput = document.getElementById('requestHeaders');
    const requestUrl = document.getElementById('requestUrl');
    const responseContent = document.getElementById('responseContent');

    const envId = envSelect.value;
    const apiId = apiSelect.value;
    const method = methodSelect.value;

    if (!envId || !apiId) {
        responseContent.textContent = '環境とAPIを選択してください';
        return;
    }

    const url = requestUrl.value; // 編集されたURLを使用
    let headers = {};
    try {
        headers = JSON.parse(headersInput.value || '{}');
    } catch (e) {
        responseContent.textContent = '無効なヘッダ形式';
        return;
    }

    // JSONボディ生成
    const paramSelect = document.getElementById('parameter');
    const paramId = paramSelect.value;
    const body = {};
    if (paramId) {
        data.parameters
            .filter(param => param.apiId === apiId && param.id === paramId)
            .forEach(param => {
                const value = editedParameters[param.physicalName] || param.value;
                if (value !== "") {
                    let parsedValue = value;
                    if (value.startsWith('[') && value.endsWith(']')) {
                        try {
                            parsedValue = JSON.parse(value);
                        } catch (e) {
                            console.warn(`Invalid JSON array: ${value}`);
                        }
                    }
                    setNestedValue(body, param.physicalName, parsedValue);
                }
            });
    }

    // リクエスト送信
    fetch(url, {
        method: method,
        headers: headers,
        body: method === 'POST' ? JSON.stringify({ [apiId]: body }) : undefined
    })
    .then(response => {
        responseContent.textContent = `Status: ${response.status}\n`;
        return response.text();
    })
    .then(text => {
        try {
            const json = JSON.parse(text);
            responseContent.textContent += JSON.stringify(json, null, 2);
        } catch {
            responseContent.textContent += text;
        }
        toggleSection('response');
    })
    .catch(error => {
        responseContent.textContent = `エラー: ${error.message}`;
        toggleSection('response');
    });
}

// ネストされた値の設定
function setNestedValue(obj, key, value) {
    const parts = key.split('.');
    let current = obj;

    for (let i = 0; i < parts.length; i++) {
        const part = parts[i];
        const arrayMatch = part.match(/(\w+)\[(\d+)\]/);

        if (arrayMatch) {
            const arrayName = arrayMatch[1];
            const index = parseInt(arrayMatch[2]);

            current[arrayName] = current[arrayName] || [];
            while (current[arrayName].length <= index) {
                current[arrayName].push(null);
            }

            if (i === parts.length - 1) {
                current[arrayName][index] = value;
            } else {
                current[arrayName][index] = current[arrayName][index] || {};
                current = current[arrayName][index];
            }
        } else {
            if (i === parts.length - 1) {
                current[part] = value;
            } else {
                current[part] = current[part] || {};
                current = current[part];
            }
        }
    }
}
```