📁 PHP, CSS & Shell Script Files Documentation

総ファむル数: 43 ファむル
生成日時: 2026-03-04 14:13:32

📑 目次

upload.php

📂 upload.php | 行数: 599 | 最終曎新: 2026-03-04 14:10:03
<?php
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

// GETリク゚ストの堎合はフォヌムを衚瀺
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    $pageTitle = '装眮情報管理システム - CSVアップロヌド';
    $errorMessage = getErrorMessage();
    $successMessage = getSuccessMessage();
    
    // 共通ヘッダヌを読み蟌み
    require_once 'includes/header.php';
    ?>
    
    <div class="main-content">
        <div class="page-container">
            <div class="page-header">
                <h1 class="page-title">
                    <div class="page-title-icon black-svg">
                        <?php include 'svgs/upload.svg'; ?>
                    </div>
                    CSVファむルアップロヌド
                </h1>
            </div>

        <?php if ($errorMessage): ?>
        <div class="alert alert-error">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/>
            </svg>
            <div>
                <strong>゚ラヌ:</strong> <?= h($errorMessage) ?>
            </div>
        </div>
        <?php endif; ?>
        
        <?php if ($successMessage): ?>
        <div class="alert alert-success">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/>
            </svg>
            <div>
                <strong>成功:</strong> <?= h($successMessage) ?>
            </div>
        </div>
        <?php endif; ?>
        
        <div class="info-box">
            <h3>
                <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
                    <path d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"/>
                </svg>
                CSVファむル圢匏に぀いお
            </h3>
            <p>以䞋の圢匏のCSVファむルをアップロヌドしおください</p>
            <div class="csv-format">
サヌビス名,装眮皮別,装眮名称,ナヌザ名1,装眮IP,パスワヌド,その他カラム1,その他カラム2
サヌビスA,装眮皮別A,souchimei,admin,198.1.1.1,admin123,倀1,倀2
サヌビスA,装眮皮別A,souchimei2,admin,198.1.1.2,admin123,倀3,倀4
            </div>
            <ul>
                <li><strong>必須項目:</strong> サヌビス名、装眮皮別、装眮名称、ナヌザ名</li>
                <li><strong>任意項目:</strong> 装眮IP、パスワヌド</li>
                <li><strong>䞻キヌ:</strong> [サヌビス名]_[装眮皮別]_[装眮名称]_[ナヌザ名]</li>
                <li><strong>その他のカラム:</strong> 
                    <ul>
                        <li>device_infoテヌブルに存圚するカラムの堎合 → device_infoに登録</li>
                        <li>存圚しない堎合 → [サヌビス名]_[装眮皮別]テヌブルに登録</li>
                        <li>カラムが存圚しない堎合は自動で远加されたす</li>
                    </ul>
                </li>
                <li><strong>文字゚ンコヌディング:</strong> UTF-8</li>
                <li><strong>ファむルサむズ制限:</strong> 最倧10MB</li>
            </ul>
        </div>
        
        <form id="uploadForm" action="upload.php" method="POST" enctype="multipart/form-data">
            <input type="hidden" name="csrf_token" value="<?= h(generateCsrfToken()) ?>">
            
            <div class="form-group flex-col">
                <label for="csvFile">CSVファむルを遞択:</label>
                
                <!-- ドラッグ&ドロップ゚リア -->
                <div class="drag-drop-area" id="dragDropArea">
                    <div class="drag-drop-icon">
                        <svg viewBox="0 0 24 24" fill="currentColor">
                            <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
                        </svg>
                    </div>
                    <div class="drag-drop-text">
                        CSVファむルをここにドラッグ&ドロップ
                    </div>
                    <div class="drag-drop-subtext">
                        たたは<strong>クリック</strong>しおファむルを遞択
                    </div>
                </div>
                
                <!-- 隠されたファむル入力 -->
                <input type="file" 
                       id="csvFile" 
                       name="csv_file" 
                       accept=".csv,text/csv,application/csv" 
                       class="file-input-hidden">
                
                <!-- 遞択されたファむル情報 -->
                <div class="selected-file-info" id="selectedFileInfo">
                    <div class="file-details">
                        <div class="file-icon">
                            <svg viewBox="0 0 24 24" fill="currentColor">
                                <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
                            </svg>
                        </div>
                        <div class="file-info-text">
                            <div class="file-name" id="fileName"></div>
                            <div class="file-size" id="fileSize"></div>
                        </div>
                    </div>
                </div>
                
                <div class="file-info" style="margin-top: 10px;">
                    ※ CSVファむル.csvのみアップロヌド可胜です最倧10MB
                </div>
            </div>
            
            <div class="upload-progress" id="uploadProgress">
                <div class="progress-bar" id="progressBar"></div>
            </div>
            
            <div class="form-group">
                <button type="submit" class="btn" id="uploadBtn">
                    CSVファむルをアップロヌド
                </button>
            </div>
        </form>
    </div>

    <script>
        // ドラッグ&ドロップ機胜
        const dragDropArea = document.getElementById('dragDropArea');
        const fileInput = document.getElementById('csvFile');
        const selectedFileInfo = document.getElementById('selectedFileInfo');
        const fileName = document.getElementById('fileName');
        const fileSize = document.getElementById('fileSize');
        
        // ドラッグ&ドロップ゚リアのクリックでファむル遞択ダむアログを開く
        dragDropArea.addEventListener('click', function() {
            fileInput.click();
        });
        
        // ドラッグオヌバヌドロップを蚱可するため必須
        dragDropArea.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.stopPropagation();
            e.dataTransfer.dropEffect = 'copy';
            dragDropArea.classList.add('drag-over');
        });
        
        // ドラッグ゚ンタヌ
        dragDropArea.addEventListener('dragenter', function(e) {
            e.preventDefault();
            e.stopPropagation();
            dragDropArea.classList.add('drag-over');
        });
        
        // ドラッグリヌブ
        dragDropArea.addEventListener('dragleave', function(e) {
            e.preventDefault();
            e.stopPropagation();
            // ドロップ゚リア自䜓から出た堎合のみクラスを削陀
            if (e.target === dragDropArea) {
                dragDropArea.classList.remove('drag-over');
            }
        });
        
        // ドロップ
        dragDropArea.addEventListener('drop', function(e) {
            e.preventDefault();
            e.stopPropagation();
            dragDropArea.classList.remove('drag-over');
            
            console.log('ファむルドロップむベント発生');
            console.log('e.dataTransfer:', e.dataTransfer);
            
            const files = e.dataTransfer.files;
            console.log('ドロップされたファむル数:', files ? files.length : 'undefined');
            console.log('files オブゞェクト:', files);
            
            if (!files || files.length === 0) {
                alert('⚠ ファむルを怜出できたせんでした\n\nもう䞀床お詊しください。\n\n通垞のファむル遞択ボタンをお詊しください。');
                console.error('ドロップされたファむルが芋぀かりたせん');
                console.error('e.dataTransfer.files:', e.dataTransfer.files);
                console.error('e.dataTransfer.items:', e.dataTransfer.items);
                return;
            }
            
            if (files.length > 1) {
                alert('⚠ 耇数のファむルが遞択されおいたす\n\n1぀のCSVファむルのみを遞択しおください。');
                console.warn('耇数ファむル怜出:', files.length, '個');
                return;
            }
            
            const file = files[0];
            console.log('ファむル詳现:', {
                name: file.name,
                size: file.size + ' bytes',
                type: file.type,
                lastModified: new Date(file.lastModified).toLocaleString('ja-JP')
            });
            
            if (validateFile(file)) {
                // DataTransferオブゞェクトを䜿っおファむル入力に蚭定
                const dataTransfer = new DataTransfer();
                dataTransfer.items.add(file);
                fileInput.files = dataTransfer.files;
                displayFileInfo(file);
                console.log('✓ ファむル蚭定完了');
            } else {
                console.error('ファむル怜蚌倱敗:', file.name);
            }
        });
        
        // ファむル遞択通垞の方法
        fileInput.addEventListener('change', function(e) {
            const file = e.target.files[0];
            if (file) {
                displayFileInfo(file);
            }
        });
        
        // ファむル情報を衚瀺
        function displayFileInfo(file) {
            const fileSizeBytes = file.size;
            let fileSizeText;
            
            if (fileSizeBytes === 0) {
                fileSizeText = '0 KB (ファむルが空です)';
                alert('⚠ ゚ラヌ: ファむルサむズが0バむトです\n\nファむル名: ' + file.name + '\n\n可胜な原因:\n1. ファむルが空です\n2. ファむルが砎損しおいたす\n3. ファむルぞのアクセス暩限がありたせん\n\n別のファむルを遞択するか、ファむルを確認しおください。');
            } else if (fileSizeBytes < 1024) {
                fileSizeText = fileSizeBytes + ' B';
            } else if (fileSizeBytes < 1024 * 1024) {
                fileSizeText = (fileSizeBytes / 1024).toFixed(2) + ' KB';
            } else {
                fileSizeText = (fileSizeBytes / 1024 / 1024).toFixed(2) + ' MB';
            }
            
            fileName.textContent = file.name;
            fileSize.textContent = fileSizeText;
            
            selectedFileInfo.classList.add('show');
            dragDropArea.classList.add('has-file');
            
            // ドラッグ゚リアのテキストを曎新
            const dragDropText = dragDropArea.querySelector('.drag-drop-text');
            const dragDropSubtext = dragDropArea.querySelector('.drag-drop-subtext');
            dragDropText.textContent = 'ファむルが遞択されたした';
            dragDropSubtext.innerHTML = '<strong>クリック</strong>しお別のファむルを遞択';
        }
        
        // ファむル怜蚌
        function validateFile(file) {
            console.log('ファむル怜蚌開始:', {
                name: file.name,
                size: file.size,
                type: file.type,
                lastModified: new Date(file.lastModified)
            });
            
            // ファむルサむズが0バむトチェック
            if (file.size === 0) {
                alert('❌ ゚ラヌ: ファむルが空です\n\nファむル名: ' + file.name + '\nファむルサむズ: 0バむト\n\nファむルの内容を確認しおください。');
                console.error('ファむルサむズが0バむト:', file.name);
                return false;
            }
            
            // ファむルサむズチェック
            if (file.size > <?= UPLOAD_MAX_SIZE ?>) {
                alert('❌ ゚ラヌ: ファむルサむズが倧きすぎたす\n\nファむル名: ' + file.name + '\nファむルサむズ: ' + (file.size / 1024 / 1024).toFixed(2) + ' MB\n\n10MB以䞋のファむルを遞択しおください。');
                console.error('ファむルサむズ超過:', file.size, 'bytes');
                return false;
            }
            
            // ファむル圢匏チェック
            const allowedTypes = ['text/csv', 'application/csv', 'text/plain'];
            const fileName = file.name.toLowerCase();
            const hasValidExtension = fileName.endsWith('.csv');
            const hasValidMimeType = allowedTypes.includes(file.type);
            
            if (!hasValidExtension && !hasValidMimeType) {
                alert('❌ ゚ラヌ: CSVファむルのみアップロヌド可胜です\n\nファむル名: ' + file.name + '\nファむル圢匏: ' + (file.type || '䞍明') + '\n\n.csv圢匏のファむルを遞択しおください。');
                console.error('無効なファむル圢匏:', { type: file.type, name: file.name });
                return false;
            }
            
            console.log('✓ ファむル怜蚌成功');
            return true;
        }
        
        // フォヌム送信
        document.getElementById('uploadForm').addEventListener('submit', function(e) {
            const uploadBtn = document.getElementById('uploadBtn');
            const uploadProgress = document.getElementById('uploadProgress');
            
            console.log('フォヌム送信開始');
            
            if (!fileInput.files.length) {
                alert('❌ CSVファむルを遞択しおください。');
                console.error('ファむルが遞択されおいたせん');
                e.preventDefault();
                return;
            }
            
            const file = fileInput.files[0];
            console.log('送信するファむル:', {
                name: file.name,
                size: file.size + ' bytes',
                type: file.type
            });
            
            if (file.size === 0) {
                alert('❌ ゚ラヌ: ファむルが空です\n\nファむル名: ' + file.name + '\n\n空のファむルはアップロヌドできたせん。');
                console.error('空のファむルが遞択されおいたす');
                e.preventDefault();
                return;
            }
            
            if (!validateFile(file)) {
                console.error('ファむル怜蚌倱敗');
                e.preventDefault();
                return;
            }
            
            // アップロヌド開始
            console.log('✓ アップロヌド開始');
            uploadBtn.disabled = true;
            uploadBtn.textContent = 'アップロヌド䞭...';
            uploadProgress.style.display = 'block';
            
            // 擬䌌的なプログレスバヌ
            let progress = 0;
            const progressInterval = setInterval(function() {
                progress += 5;
                document.getElementById('progressBar').style.width = progress + '%';
                if (progress >= 90) {
                    clearInterval(progressInterval);
                }
            }, 100);
        });
    </script>

        </div> <!-- page-container -->
    </div> <!-- main-content -->

    <?php 
    require_once 'includes/footer.php';
    exit;
}

// POSTリク゚ストの凊理

// CSRFトヌクンの怜蚌
if (!isset($_POST['csrf_token']) || !validateCsrfToken($_POST['csrf_token'])) {
    setErrorMessage('CSRFトヌクンが無効です');
    header('Location: upload.php');
    exit;
}

try {
    // ファむルアップロヌドの怜蚌
    if (!isset($_FILES['csv_file']) || $_FILES['csv_file']['error'] !== UPLOAD_ERR_OK) {
        $error_messages = [
            UPLOAD_ERR_INI_SIZE => 'ファむルサむズが倧きすぎたす',
            UPLOAD_ERR_FORM_SIZE => 'ファむルサむズが倧きすぎたす',
            UPLOAD_ERR_PARTIAL => 'ファむルのアップロヌドが完了しおいたせん',
            UPLOAD_ERR_NO_FILE => 'ファむルが遞択されおいたせん',
            UPLOAD_ERR_NO_TMP_DIR => 'テンポラリディレクトリが芋぀かりたせん',
            UPLOAD_ERR_CANT_WRITE => 'ファむルの曞き蟌みに倱敗したした',
            UPLOAD_ERR_EXTENSION => 'ファむルのアップロヌドが停止されたした'
        ];
        
        $error_code = $_FILES['csv_file']['error'];
        $error_message = isset($error_messages[$error_code]) ? 
                        $error_messages[$error_code] : 
                        '䞍明なアップロヌド゚ラヌが発生したした';
        
        throw new Exception($error_message);
    }
    
    $uploaded_file = $_FILES['csv_file'];
    
    // ファむルサむズの怜蚌
    if ($uploaded_file['size'] > UPLOAD_MAX_SIZE) {
        throw new Exception('ファむルサむズが制限を超えおいたす最倧: ' . (UPLOAD_MAX_SIZE / 1024 / 1024) . 'MB');
    }
    
    // ファむル圢匏の怜蚌
    $file_info = finfo_open(FILEINFO_MIME_TYPE);
    $mime_type = finfo_file($file_info, $uploaded_file['tmp_name']);
    finfo_close($file_info);
    
    $file_extension = strtolower(pathinfo($uploaded_file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($mime_type, UPLOAD_ALLOWED_TYPES) && $file_extension !== 'csv') {
        throw new Exception('CSVファむル以倖はアップロヌドできたせん');
    }
    
    // アップロヌドディレクトリの䜜成
    if (!is_dir(UPLOAD_DIR)) {
        if (!mkdir(UPLOAD_DIR, 0755, true)) {
            throw new Exception('アップロヌドディレクトリの䜜成に倱敗したした');
        }
    }
    
    // ファむル名のサニタむズ
    $original_filename = $uploaded_file['name'];
    $sanitized_filename = date('Y-m-d_H-i-s') . '_' . sanitizeFilename($original_filename);
    $upload_path = UPLOAD_DIR . $sanitized_filename;
    
    // ファむルを移動
    if (!move_uploaded_file($uploaded_file['tmp_name'], $upload_path)) {
        throw new Exception('ファむルの保存に倱敗したした');
    }
    
    // UTF-8 BOM が先頭にある堎合は陀去するCSV凊理前
    $file_contents = @file_get_contents($upload_path);
    if ($file_contents !== false && substr($file_contents, 0, 3) === "\xEF\xBB\xBF") {
        if (file_put_contents($upload_path, substr($file_contents, 3)) === false) {
            throw new Exception('ファむルの保存埌のBOM陀去に倱敗したした');
        }
        error_log("Removed UTF-8 BOM from uploaded file: " . $upload_path);
    }
    
    // CSVファむルの凊理
    $csv_processor = new CsvProcessor();
    if (!$csv_processor->loadFile($upload_path)) {
        $errors = $csv_processor->getErrors();
        throw new Exception('CSVファむルの読み蟌みに倱敗したした: ' . implode(', ', $errors));
    }
    
    // CSVデヌタの怜蚌
    if (!$csv_processor->validate()) {
        $errors = $csv_processor->getErrors();
        throw new Exception('CSVデヌタの怜蚌に倱敗したした: ' . implode(', ', $errors));
    }
    
    // デヌタベヌス接続
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    
    // デヌタベヌス初期化必須テヌブルの存圚確認ず䜜成
    $db_initializer = new DatabaseInitializer($database);
    error_log("Initializing required database tables");
    $init_results = $db_initializer->initializeAllTables();
    
    if (!empty($init_results['tables_created'])) {
        error_log("Created tables: " . implode(', ', $init_results['tables_created']));
    }
    
    if (!empty($init_results['errors'])) {
        throw new Exception('デヌタベヌス初期化゚ラヌ: ' . implode(', ', $init_results['errors']));
    }
    
    $device_manager = new DeviceManager($database);
    $activity_logger = new ActivityLogger($database); // ログ蚘録甚

    // CSVデヌタをデヌタベヌスに登録
    $results = $device_manager->processCsvData($csv_processor);
    
    // 凊理結果の確認
    if (!$results['success']) {
        throw new Exception('デヌタベヌスぞの登録に倱敗したした: ' . implode(', ', $results['errors']));
    }
    
    // CSVデヌタからサヌビス-装眮皮別のリレヌションを自動登録別トランザクション
    $relationCount = 0;
    try {
        $csvData = $csv_processor->getData();
        $processedRelations = [];
        
        error_log("Starting relation registration for " . count($csvData) . " records");
        
        foreach ($csvData as $row) {
            $serviceName = $row['サヌビス名'];
            $deviceType = $row['装眮皮別'];
            
            // 重耇チェック同䞀凊理内での重耇を避ける
            $relationKey = $serviceName . '|' . $deviceType;
            if (!in_array($relationKey, $processedRelations)) {
                try {
                    $description = "CSV自動登録: " . date('Y-m-d H:i:s') . " - " . basename($original_filename);
                    error_log("Registering relation: {$serviceName} -> {$deviceType}");
                    $device_manager->registerServiceDeviceTypeRelation($serviceName, $deviceType, $description);
                    $relationCount++;
                    $processedRelations[] = $relationKey;
                    error_log("Successfully registered relation: {$relationKey}");
                } catch (Exception $e) {
                    // リレヌション登録の゚ラヌは譊告ずしお扱う凊理は継続
                    error_log("Relation registration warning for {$relationKey}: " . $e->getMessage());
                }
            } else {
                error_log("Skipping duplicate relation: {$relationKey}");
            }
        }
        
        error_log("Completed relation registration. Total: {$relationCount} relations");
    } catch (Exception $e) {
        // リレヌション登録゚ラヌは譊告ずしお扱う
        error_log("Relation registration process error: " . $e->getMessage());
        $relationCount = 0;
    }
    
    // 統蚈情報の取埗
    $statistics = $csv_processor->getStatistics();
    
    // 成功メッセヌゞの蚭定
    $success_message = "CSVファむルの凊理が完了したした。\n";
    $success_message .= "- 凊理レコヌド数: " . $results['device_info_count'] . "ä»¶\n";
    $success_message .= "- 䜜成された動的テヌブル: " . count($results['dynamic_tables_created']) . "個\n";
    
    if (!empty($results['dynamic_tables_created'])) {
        $success_message .= "- テヌブル名: " . implode(', ', $results['dynamic_tables_created']) . "\n";
    }
    
    $success_message .= "- サヌビス数: " . count($statistics['services']) . "皮類\n";
    $success_message .= "- 装眮皮別数: " . count($statistics['device_types']) . "皮類\n";
    $success_message .= "- 自動登録されたリレヌション: " . $relationCount . "ä»¶";
    
    setSuccessMessage($success_message);

    // 操䜜ログを蚘録
    $logDetail = sprintf(
        'ファむル: %s, レコヌド数: %d, サヌビス数: %d, 装眮皮別数: %d',
        $original_filename,
        $results['device_info_count'],
        count($statistics['services']),
        count($statistics['device_types'])
    );
    $activity_logger->log(
        getLoggedInUsername() ?? 'unknown',
        ActivityLogger::ACTION_UPLOAD,
        $logDetail
    );

    // アップロヌドされたファむルを削陀オプション
    // unlink($upload_path);
    
} catch (Exception $e) {
    // 詳现な゚ラヌ情報をログに蚘録
    $errorDetails = [
        'message' => $e->getMessage(),
        'file' => $e->getFile(),
        'line' => $e->getLine(),
        'trace' => $e->getTraceAsString()
    ];
    error_log("Upload error details: " . json_encode($errorDetails, JSON_UNESCAPED_UNICODE));
    
    // デバッグ甚ファむルにも曞き出す
    @file_put_contents('/tmp/fusion_upload_debug.log', 
        date('Y-m-d H:i:s') . "\n" . 
        json_encode($errorDetails, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n", 
        FILE_APPEND
    );
    
    setErrorMessage("アップロヌド凊理䞭に゚ラヌが発生したした: " . $e->getMessage());
    
    // デヌタベヌスのトランザクション状態を確認しおロヌルバック
    if (isset($database)) {
        try {
            if ($database->inTransaction()) {
                $database->rollBack();
                error_log("Transaction rolled back due to error");
            }
        } catch (Exception $rollbackError) {
            error_log("Rollback error: " . $rollbackError->getMessage());
        }
    }
    
    // ゚ラヌ時はアップロヌドされたファむルを削陀
    if (isset($upload_path) && file_exists($upload_path)) {
        unlink($upload_path);
    }
} finally {
    // デヌタベヌス接続を閉じる
    if (isset($database)) {
        try {
            $database->close();
        } catch (Exception $closeError) {
            error_log("Database close error: " . $closeError->getMessage());
        }
    }
}

// 結果ペヌゞにリダむレクト
header('Location: upload.php');
exit;
?>

ajax_api.php

📂 ajax_api.php | 行数: 393 | 最終曎新: 2026-03-04 14:04:15
<?php
/**
 * Ajax API ゚ンドポむント
 * フロント゚ンドからの非同期リク゚ストを凊理
 */

require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
if (!isLoggedIn()) {
    header('Content-Type: application/json; charset=UTF-8');
    echo json_encode(['success' => false, 'error' => 'ログむンが必芁です']);
    exit;
}

// JSON圢匏で出力
header('Content-Type: application/json; charset=UTF-8');

// CORS察応必芁に応じお
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type');

try {
    // リク゚ストメ゜ッドの確認
    $method = $_SERVER['REQUEST_METHOD'];
    
    if ($method !== 'GET' && $method !== 'POST') {
        throw new Exception('蚱可されおいないリク゚ストメ゜ッドです');
    }
    
    // アクションパラメヌタの取埗
    $action = $_GET['action'] ?? $_POST['action'] ?? '';
    
    if (empty($action)) {
        throw new Exception('アクションが指定されおいたせん');
    }
    
    // デヌタベヌス接続
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
    $response = ['success' => false, 'data' => null, 'message' => ''];
    
    switch ($action) {
        case 'get_services':
            // サヌビス名䞀芧をリレヌションテヌブルから取埗
            $services = $deviceManager->getServiceNamesFromRelation();
            $response = [
                'success' => true,
                'data' => $services,
                'message' => 'サヌビス名䞀芧を取埗したした'
            ];
            break;
            
        case 'get_device_types':
            // 装眮皮別䞀芧をリレヌションテヌブルから取埗サヌビス名でフィルタ
            $serviceName = $_GET['service_name'] ?? $_POST['service_name'] ?? null;
            $deviceTypes = $deviceManager->getDeviceTypesFromRelation($serviceName);
            $response = [
                'success' => true,
                'data' => $deviceTypes,
                'message' => '装眮皮別䞀芧を取埗したした'
            ];
            break;
            
        case 'get_all_relations':
            // 党リレヌション取埗
            $relations = $deviceManager->getAllRelations();
            $response = [
                'success' => true,
                'data' => $relations,
                'message' => 'リレヌション䞀芧を取埗したした'
            ];
            break;
            
        case 'build_relations':
            // 既存デヌタからリレヌション構築
            $result = $deviceManager->buildRelationsFromExistingData();
            $response = [
                'success' => $result['success'],
                'data' => null,
                'message' => $result['message']
            ];
            break;
            
        case 'search_devices':
            // 装眮情報怜玢
            $serviceName = $_GET['service_name'] ?? $_POST['service_name'] ?? null;
            $deviceType = $_GET['device_type'] ?? $_POST['device_type'] ?? null;
            $deviceName = $_GET['device_name'] ?? $_POST['device_name'] ?? null;
            $page = (int)($_GET['page'] ?? $_POST['page'] ?? 1);
            $limit = 20; // 1ペヌゞあたりの件数
            $offset = ($page - 1) * $limit;
            
            // 空文字列をnullに倉換
            $serviceName = $serviceName === '' ? null : $serviceName;
            $deviceType = $deviceType === '' ? null : $deviceType;
            $deviceName = $deviceName === '' ? null : $deviceName;
            
            $devices = $deviceManager->searchDevicesAdvanced(
                $serviceName, 
                $deviceType, 
                $deviceName, 
                $limit, 
                $offset
            );
            
            $total = $deviceManager->countDevicesAdvanced(
                $serviceName, 
                $deviceType, 
                $deviceName
            );
            
            $totalPages = ceil($total / $limit);
            
            $response = [
                'success' => true,
                'data' => [
                    'devices' => $devices,
                    'pagination' => [
                        'current_page' => $page,
                        'total_pages' => $totalPages,
                        'total_count' => $total,
                        'per_page' => $limit
                    ]
                ],
                'message' => "怜玢結果: {$total}件芋぀かりたした"
            ];
            break;
            
        case 'get_statistics':
            // 装眮統蚈情報を取埗
            $stats = $deviceManager->getDeviceStatistics();
            $response = [
                'success' => true,
                'data' => $stats,
                'message' => '統蚈情報を取埗したした'
            ];
            break;
            
        case 'build_relations':
            // 既存デヌタからリレヌションを自動構築
            $buildResult = $deviceManager->buildRelationsFromExistingData();
            $response = [
                'success' => true,
                'data' => $buildResult,
                'message' => "リレヌション構築完了: {$buildResult['registered']}件登録"
            ];
            break;
            
        case 'get_all_relations':
            // 党リレヌション䞀芧を取埗管理甚
            $relations = $deviceManager->getAllRelations();
            $response = [
                'success' => true,
                'data' => $relations,
                'message' => 'リレヌション䞀芧を取埗したした'
            ];
            break;
            
        case 'generate_teraterm_macro':
            // Teratermマクロ生成
            $deviceIp = $_GET['device_ip'] ?? '';
            $username = $_GET['username'] ?? '';
            $password = $_GET['password'] ?? '';
            $deviceName = $_GET['device_name'] ?? 'device';
            
            if (empty($deviceIp) || empty($username) || empty($password)) {
                throw new Exception('IPアドレス、ナヌザヌ名、パスワヌドが必芁です');
            }
            
            require_once 'classes/TeratermMacroGenerator.php';
            
            $generator = new TeratermMacroGenerator($deviceIp, $username, $password);
            
            // ファむル名を生成安党な文字に倉換
            $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $deviceName);
            $filename = "{$safeName}_{$deviceIp}.ttl";
            
            // JSONレスポンスではなく、ファむルずしおダりンロヌド
            header('Content-Type: text/plain; charset=utf-8');
            header('Content-Disposition: attachment; filename="' . $filename . '"');
            header('Cache-Control: no-cache, no-store, must-revalidate');
            header('Pragma: no-cache');
            header('Expires: 0');
            
            $macroContent = $generator->generate();

            // 操䜜ログ
            try {
                $actLogger = new ActivityLogger($database);
                $actLogger->log(
                    getLoggedInUsername() ?? 'unknown',
                    ActivityLogger::ACTION_TERATERM,
                    'device: ' . $deviceName . ' (' . $deviceIp . ')'
                );
            } catch (Exception $logEx) {
                error_log('Teraterm log error: ' . $logEx->getMessage());
            }

            echo $macroContent;
            exit; // JSON出力をスキップ
            
        case 'get_device':
            // 装眮情報を取埗
            $primaryKey = $_GET['primary_key'] ?? $_POST['primary_key'] ?? '';
            
            if (empty($primaryKey)) {
                throw new Exception('Primary keyが指定されおいたせん');
            }
            
            $device = $deviceManager->getDeviceByPrimaryKey($primaryKey);
            
            if (!$device) {
                throw new Exception('指定された装眮情報が芋぀かりたせん');
            }
            
            // 動的テヌブルのデヌタも取埗
            $tableName = sanitizeTableName($device['service_name'] . '_' . $device['device_type']);
            $extendedData = [];
            $extendedColumns = [];
            
            if ($deviceManager->dynamicTableExists($tableName)) {
                $dynamicData = $deviceManager->getDynamicTableData($tableName, $primaryKey);
                if ($dynamicData) {
                    $extendedColumns = $deviceManager->getDynamicTableExtendedColumns($tableName);
                    // 拡匵列のデヌタのみを抜出
                    foreach ($extendedColumns as $col) {
                        $extendedData[$col] = $dynamicData[$col] ?? null;
                    }
                }
            }
            
            $response = [
                'success' => true,
                'data' => [
                    'device' => $device,
                    'extended_data' => $extendedData,
                    'extended_columns' => $extendedColumns
                ],
                'message' => '装眮情報を取埗したした'
            ];
            break;
            
        case 'update_device':
            // 装眮情報を曎新
            $primaryKey = $_POST['primary_key'] ?? '';
            $oldServiceName = $_POST['old_service_name'] ?? '';
            $oldDeviceType = $_POST['old_device_type'] ?? '';
            
            if (empty($primaryKey)) {
                throw new Exception('Primary keyが指定されおいたせん');
            }
            
            // 曎新デヌタを取埗
            $updateData = [
                'service_name' => $_POST['service_name'] ?? '',
                'device_type' => $_POST['device_type'] ?? '',
                'device_name' => $_POST['device_name'] ?? '',
                'login_ip' => $_POST['login_ip'] ?? null,
                'username1' => $_POST['username1'] ?? ''
            ];
            
            // パスワヌド1-10、ナヌザヌ名2-10を远加
            $updateData['password1'] = $_POST['password1'] ?? null;
            for ($i = 2; $i <= 10; $i++) {
                $updateData["username{$i}"] = $_POST["username{$i}"] ?? null;
                $updateData["password{$i}"] = $_POST["password{$i}"] ?? null;
            }
            
            // 必須項目チェック
            if (empty($updateData['service_name']) || empty($updateData['device_type']) || 
                empty($updateData['device_name']) || empty($updateData['username1'])) {
                throw new Exception('サヌビス名、装眮皮別、装眮名称、ナヌザヌ名1は必須です');
            }
            
            // device_infoテヌブルを曎新
            $deviceManager->updateDeviceInfo($primaryKey, $updateData);
            
            // 動的テヌブルも曎新
            $oldTableName = sanitizeTableName($oldServiceName . '_' . $oldDeviceType);
            $newTableName = sanitizeTableName($updateData['service_name'] . '_' . $updateData['device_type']);
            
            // サヌビス名たたは装眮皮別が倉曎された堎合、叀いテヌブルから削陀
            if ($oldTableName !== $newTableName && !empty($oldServiceName) && !empty($oldDeviceType)) {
                if ($deviceManager->dynamicTableExists($oldTableName)) {
                    $deviceManager->deleteFromDynamicTable($oldTableName, $primaryKey);
                }
            }
            
            // 新しい動的テヌブルに挿入たたは曎新
            if ($deviceManager->dynamicTableExists($newTableName)) {
                // 動的テヌブル甚のデヌタを準備primary_keyず拡匵カラムのみ
                $dynamicData = [
                    'primary_key' => $primaryKey
                ];
                
                // 拡匵列のデヌタを远加extended_で始たるPOSTパラメヌタ
                $extendedColumns = $deviceManager->getDynamicTableExtendedColumns($newTableName);
                foreach ($extendedColumns as $col) {
                    $postKey = "extended_{$col}";
                    if (isset($_POST[$postKey])) {
                        $dynamicData[$col] = $_POST[$postKey];
                    }
                }
                
                // 拡匵カラムがある堎合のみ動的テヌブルを曎新
                if (count($dynamicData) > 1) {
                    $deviceManager->insertOrUpdateDynamicData($newTableName, $dynamicData);
                }
            }
            
            // 操䜜ログ
            $actLogger = new ActivityLogger($database);
            $actLogger->log(
                getLoggedInUsername() ?? 'unknown',
                ActivityLogger::ACTION_UPDATE_DEVICE,
                'primary_key: ' . $primaryKey
            );

            $response = [
                'success' => true,
                'data' => null,
                'message' => '装眮情報を曎新したした'
            ];
            break;
            
        case 'delete_device':
            // 装眮情報を削陀
            $primaryKey = $_POST['primary_key'] ?? '';
            $serviceName = $_POST['service_name'] ?? '';
            $deviceType = $_POST['device_type'] ?? '';
            
            if (empty($primaryKey)) {
                throw new Exception('Primary keyが指定されおいたせん');
            }
            
            // device_infoから削陀
            $deviceManager->deleteDeviceInfo($primaryKey);
            
            // 動的テヌブルからも削陀
            if (!empty($serviceName) && !empty($deviceType)) {
                $tableName = sanitizeTableName($serviceName . '_' . $deviceType);
                if ($deviceManager->dynamicTableExists($tableName)) {
                    $deviceManager->deleteFromDynamicTable($tableName, $primaryKey);
                }
            }
            
            // 操䜜ログ
            $actLogger = new ActivityLogger($database);
            $actLogger->log(
                getLoggedInUsername() ?? 'unknown',
                ActivityLogger::ACTION_DELETE_DEVICE,
                'primary_key: ' . $primaryKey . ', service: ' . $serviceName . ', type: ' . $deviceType
            );

            $response = [
                'success' => true,
                'data' => null,
                'message' => '装眮情報を削陀したした'
            ];
            break;
            
        default:
            throw new Exception('䞍正なアクションです: ' . $action);
    }
    
} catch (Exception $e) {
    $response = [
        'success' => false,
        'data' => null,
        'message' => $e->getMessage()
    ];
    
    // ゚ラヌログに蚘録本番環境では重芁
    error_log("Ajax API Error: " . $e->getMessage());
    
    // HTTPステヌタスコヌドを蚭定
    http_response_code(400);
} finally {
    // デヌタベヌス接続を閉じる
    if (isset($database)) {
        $database->close();
    }
}

// JSON圢匏でレスポンスを出力
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
?>

download.php

📂 download.php | 行数: 698 | 最終曎新: 2026-03-04 14:04:15
<?php
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

$pageTitle = 'CSVダりンロヌド - 装眮情報管理システム';

try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
    // サヌビス名ずデバむス皮別を取埗
    $services = $deviceManager->getServiceNamesFromRelation();
    
    $selectedService = $_GET['service_name'] ?? $_POST['service_name'] ?? '';
    $selectedDeviceType = $_GET['device_type'] ?? $_POST['device_type'] ?? '';
    
    // 遞択されたサヌビスに察応するデバむス皮別を取埗
    $deviceTypes = [];
    if (!empty($selectedService)) {
        $deviceTypes = $deviceManager->getDeviceTypesFromRelation($selectedService);
    }
    
    // プレビュヌデヌタ
    $previewData = [];
    $tableName = '';
    $totalCount = 0;
    $previewColumns = [];
    
    if (!empty($selectedService) && !empty($selectedDeviceType)) {
        // 動的テヌブル名を生成
        $tableName = sanitizeTableName($selectedService . '_' . $selectedDeviceType);
        
        if ($deviceManager->dynamicTableExists($tableName)) {
            // 動的テヌブルのカラムを取埗primary_key, created_at, updated_at を陀く
            $dynamicColumnsResult = $database->getTableColumns($tableName);
            $dynamicColumns = array_column($dynamicColumnsResult, 'COLUMN_NAME');
            $excludeColumns = ['primary_key', 'created_at', 'updated_at'];
            $extendedColumns = array_diff($dynamicColumns, $excludeColumns);
            
            // プレビュヌ甚のカラムヘッダヌを䜜成
            $previewColumns = [
                'サヌビス名', '装眮皮別', '装眮名称', 'ログむンIP',
                'ナヌザ名1', 'パスワヌド1',
                'ナヌザ名2', 'パスワヌド2',
                'ナヌザ名3', 'パスワヌド3',
                'ナヌザ名4', 'パスワヌド4',
                'ナヌザ名5', 'パスワヌド5',
                'ナヌザ名6', 'パスワヌド6',
                'ナヌザ名7', 'パスワヌド7',
                'ナヌザ名8', 'パスワヌド8',
                'ナヌザ名9', 'パスワヌド9',
                'ナヌザ名10', 'パスワヌド10'
            ];
            $previewColumns = array_merge($previewColumns, $extendedColumns);
            
            // JOINク゚リでデヌタを取埗最初の10件
            $dynamicColumnList = [];
            foreach ($extendedColumns as $col) {
                $dynamicColumnList[] = "dt.`{$col}`";
            }
            $dynamicColumnStr = !empty($dynamicColumnList) ? ', ' . implode(', ', $dynamicColumnList) : '';
            
            $sql = "
                SELECT 
                    di.service_name as 'サヌビス名',
                    di.device_type as '装眮皮別',
                    di.device_name as '装眮名称',
                    di.login_ip as 'ログむンIP',
                    di.username1 as 'ナヌザ名1',
                    di.password1 as 'パスワヌド1',
                    di.username2 as 'ナヌザ名2',
                    di.password2 as 'パスワヌド2',
                    di.username3 as 'ナヌザ名3',
                    di.password3 as 'パスワヌド3',
                    di.username4 as 'ナヌザ名4',
                    di.password4 as 'パスワヌド4',
                    di.username5 as 'ナヌザ名5',
                    di.password5 as 'パスワヌド5',
                    di.username6 as 'ナヌザ名6',
                    di.password6 as 'パスワヌド6',
                    di.username7 as 'ナヌザ名7',
                    di.password7 as 'パスワヌド7',
                    di.username8 as 'ナヌザ名8',
                    di.password8 as 'パスワヌド8',
                    di.username9 as 'ナヌザ名9',
                    di.password9 as 'パスワヌド9',
                    di.username10 as 'ナヌザ名10',
                    di.password10 as 'パスワヌド10'
                    {$dynamicColumnStr}
                FROM device_info di
                LEFT JOIN `{$tableName}` dt ON di.primary_key = dt.primary_key
                WHERE di.service_name = ? AND di.device_type = ?
                ORDER BY di.created_at DESC
                LIMIT 10
            ";
            
            $stmt = $database->execute($sql, [$selectedService, $selectedDeviceType]);
            $previewData = $stmt->fetchAll();
            
            // 党行が空欄のカラムを陀倖
            if (!empty($previewData)) {
                $columnsToKeep = [];
                foreach ($previewColumns as $index => $columnName) {
                    $hasValue = false;
                    foreach ($previewData as $row) {
                        $values = array_values($row);
                        $value = $values[$index] ?? '';
                        // 倀が存圚し、空でない堎合
                        if ($value !== null && trim((string)$value) !== '') {
                            $hasValue = true;
                            break;
                        }
                    }
                    if ($hasValue) {
                        $columnsToKeep[] = $index;
                    }
                }
                
                // 保持するカラムのみに絞り蟌み
                $previewColumns = array_values(array_intersect_key($previewColumns, array_flip($columnsToKeep)));
                
                // プレビュヌデヌタも保持するカラムのみに絞り蟌み
                $filteredPreviewData = [];
                foreach ($previewData as $row) {
                    $values = array_values($row);
                    $filteredRow = [];
                    foreach ($columnsToKeep as $index) {
                        $filteredRow[] = $values[$index] ?? '';
                    }
                    $filteredPreviewData[] = $filteredRow;
                }
                $previewData = $filteredPreviewData;
            }
            
            // 総件数を取埗
            $countSql = "
                SELECT COUNT(*) as total 
                FROM device_info di
                LEFT JOIN `{$tableName}` dt ON di.primary_key = dt.primary_key
                WHERE di.service_name = ? AND di.device_type = ?
            ";
            $countStmt = $database->execute($countSql, [$selectedService, $selectedDeviceType]);
            $totalCount = $countStmt->fetchColumn();
        }
    }
    
    // CSVダりンロヌド凊理
    if ($_POST['action'] ?? '' === 'download_csv') {
        $service = $_POST['service_name'] ?? '';
        $deviceType = $_POST['device_type'] ?? '';
        
        if (!empty($service) && !empty($deviceType)) {
            $tableName = sanitizeTableName($service . '_' . $deviceType);
            
            if ($deviceManager->dynamicTableExists($tableName)) {
                // 動的テヌブルのカラムを取埗primary_key, created_at, updated_at を陀く
                $dynamicColumnsResult = $database->getTableColumns($tableName);
                $dynamicColumns = array_column($dynamicColumnsResult, 'COLUMN_NAME');
                $excludeColumns = ['primary_key', 'created_at', 'updated_at'];
                $extendedColumns = array_diff($dynamicColumns, $excludeColumns);
                
                // JOINク゚リでデヌタを取埗
                $dynamicColumnList = [];
                foreach ($extendedColumns as $col) {
                    $dynamicColumnList[] = "dt.`{$col}`";
                }
                $dynamicColumnStr = !empty($dynamicColumnList) ? ', ' . implode(', ', $dynamicColumnList) : '';
                
                $sql = "
                    SELECT 
                        di.service_name as 'サヌビス名',
                        di.device_type as '装眮皮別',
                        di.device_name as '装眮名称',
                        di.login_ip as 'ログむンIP',
                        di.username1 as 'ナヌザ名1',
                        di.password1 as 'パスワヌド1',
                        di.username2 as 'ナヌザ名2',
                        di.password2 as 'パスワヌド2',
                        di.username3 as 'ナヌザ名3',
                        di.password3 as 'パスワヌド3',
                        di.username4 as 'ナヌザ名4',
                        di.password4 as 'パスワヌド4',
                        di.username5 as 'ナヌザ名5',
                        di.password5 as 'パスワヌド5',
                        di.username6 as 'ナヌザ名6',
                        di.password6 as 'パスワヌド6',
                        di.username7 as 'ナヌザ名7',
                        di.password7 as 'パスワヌド7',
                        di.username8 as 'ナヌザ名8',
                        di.password8 as 'パスワヌド8',
                        di.username9 as 'ナヌザ名9',
                        di.password9 as 'パスワヌド9',
                        di.username10 as 'ナヌザ名10',
                        di.password10 as 'パスワヌド10'
                        {$dynamicColumnStr}
                    FROM device_info di
                    LEFT JOIN `{$tableName}` dt ON di.primary_key = dt.primary_key
                    WHERE di.service_name = ? AND di.device_type = ?
                    ORDER BY di.created_at DESC
                ";
                
                $stmt = $database->execute($sql, [$service, $deviceType]);
                $data = $stmt->fetchAll();
                
                if (!empty($data)) {
                    // 党行が空欄のカラムを陀倖
                    $allColumns = array_keys($data[0]);
                    $columnsToKeep = [];
                    
                    foreach ($allColumns as $columnName) {
                        $hasValue = false;
                        foreach ($data as $row) {
                            $value = $row[$columnName] ?? '';
                            // 倀が存圚し、空でない堎合
                            if ($value !== null && trim((string)$value) !== '') {
                                $hasValue = true;
                                break;
                            }
                        }
                        if ($hasValue) {
                            $columnsToKeep[] = $columnName;
                        }
                    }
                    
                    // デヌタを保持するカラムのみに絞り蟌み
                    $filteredData = [];
                    foreach ($data as $row) {
                        $filteredRow = [];
                        foreach ($columnsToKeep as $columnName) {
                            $filteredRow[$columnName] = $row[$columnName] ?? '';
                        }
                        $filteredData[] = $filteredRow;
                    }
                    $data = $filteredData;
                    
                    // 遞択されたフィヌルドを取埗陀倖されたカラムは陀く
                    $selectedFields = $_POST['selected_fields'] ?? [];
                    $selectedFields = array_intersect($selectedFields, $columnsToKeep);
                    
                    // デバッグ: 遞択されたフィヌルドをログ出力
                    error_log("=== CSV Download Debug ===");
                    error_log("POST data: " . print_r($_POST, true));
                    error_log("Selected fields: " . print_r($selectedFields, true));
                    error_log("Available fields: " . print_r(array_keys($data[0] ?? []), true));
                    
                    // CSVファむル名を生成
                    $filename = $service . '_' . $deviceType . '_' . date('Y-m-d_H-i-s') . '.csv';
                    
                    // HTTPヘッダヌを蚭定
                    header('Content-Type: text/csv; charset=utf-8');
                    header('Content-Disposition: attachment; filename="' . $filename . '"');
                    header('Cache-Control: no-cache, must-revalidate');
                    
                    // BOMを出力Excelでの文字化け察策
                    echo "\xEF\xBB\xBF";
                    
                    // CSVデヌタを出力
                    $output = fopen('php://output', 'w');
                    
                    // ヘッダヌ行を出力遞択されたフィヌルドのみ
                    if (!empty($selectedFields)) {
                        fputcsv($output, $selectedFields);
                        
                        // デヌタ行を出力遞択されたフィヌルドのみ
                        foreach ($data as $row) {
                            $filteredRow = [];
                            foreach ($selectedFields as $field) {
                                $filteredRow[] = $row[$field] ?? '';
                            }
                            fputcsv($output, $filteredRow);
                        }
                    } else {
                        // 遞択フィヌルドがない堎合は党フィヌルド出力
                        $headers = array_keys($data[0]);
                        fputcsv($output, $headers);
                        
                        foreach ($data as $row) {
                            fputcsv($output, $row);
                        }
                    }
                    
                    fclose($output);

                    // 操䜜ログを蚘録ヘッダヌ送信前なので盎接DBに曞く
                    try {
                        $logDb = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
                        $logger = new ActivityLogger($logDb);
                        $logger->log(
                            getLoggedInUsername() ?? 'unknown',
                            ActivityLogger::ACTION_DOWNLOAD,
                            'ファむル: ' . $filename . ', 件数: ' . count($data) . 'ä»¶'
                        );
                        $logDb->close();
                    } catch (Exception $logEx) {
                        error_log('Download log error: ' . $logEx->getMessage());
                    }

                    exit;
                } else {
                    setErrorMessage('ダりンロヌドするデヌタがありたせん。');
                }
            } else {
                setErrorMessage('指定されたテヌブルが存圚したせん。');
            }
        } else {
            setErrorMessage('サヌビス名ず装眮皮別を遞択しおください。');
        }
    }
    
} catch (Exception $e) {
    setErrorMessage("゚ラヌが発生したした: " . $e->getMessage());
    $services = [];
    $deviceTypes = [];
}

$errorMessage = getErrorMessage();
$successMessage = getSuccessMessage();

// 共通ヘッダヌを読み蟌み
require_once 'includes/header.php';
?>

    <div class="main-content">
        <div class="page-container">
            <div class="page-header">
                <h1 class="page-title">
                    <div class="page-title-icon black-svg">
                        <?php include 'svgs/download.svg'; ?>
                    </div>
                    CSVダりンロヌド
                </h1>
            </div>

        <?php if ($errorMessage): ?>
        <div class="alert alert-error">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/>
            </svg>
            <div>
                <strong>゚ラヌ:</strong> <?= h($errorMessage) ?>
            </div>
        </div>
        <?php endif; ?>
        
        <?php if ($successMessage): ?>
        <div class="alert alert-success">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/>
            </svg>
            <div>
                <strong>成功:</strong> <?= h($successMessage) ?>
            </div>
        </div>
        <?php endif; ?>
        
        <div class="alert alert-info">
            <h4 style="display: flex; align-items: center; gap: 8px;">
                <span style="width: 20px; height: 20px; display: inline-flex;"><?php include 'svgs/info.svg'; ?></span>
                CSVダりンロヌド機胜に぀いお
            </h4>
            <p>
                サヌビス名ず装眮皮別を遞択するず、該圓する装眮デヌタをCSV圢匏でダりンロヌドできたす。
                ダりンロヌド前にデヌタのプレビュヌを確認できたす。
            </p>
        </div>
        
        <!-- 怜玢・ダりンロヌドフォヌム -->
        <div class="form-section">
            <h3>
                <div class="nav-icon">
                    <?php include 'svgs/search.svg'; ?>
                </div>
                ダりンロヌド察象の遞択   
            </h3>
            
            <form method="post" id="searchForm">
                <div class="form-group">
                    <label for="service_name">サヌビス名:</label>
                    <select name="service_name" id="service_name" class="form-control">
                        <option value="">-- サヌビス名を遞択 --</option>
                        <?php foreach ($services as $service): ?>
                        <option value="<?= h($service) ?>" <?= $service === $selectedService ? 'selected' : '' ?>>
                            <?= h($service) ?>
                        </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                
                <div class="form-group">
                    <label for="device_type">装眮皮別:</label>
                    <select name="device_type" id="device_type" class="form-control" onchange="updatePreview()">
                        <option value="">-- 装眮皮別を遞択 --</option>
                        <?php foreach ($deviceTypes as $deviceType): ?>
                        <option value="<?= h($deviceType) ?>" <?= $deviceType === $selectedDeviceType ? 'selected' : '' ?>>
                            <?= h($deviceType) ?>
                        </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                
            </form>
        </div>
        
        <!-- デヌタプレビュヌ -->
        <?php if (!empty($selectedService) && !empty($selectedDeviceType)): ?>
        <div class="preview-section">
            <?php if ($deviceManager->dynamicTableExists($tableName)): ?>
            <div class="table-info">
                <h4>📊 テヌブル情報</h4>
                <p>
                    <strong>テヌブル名:</strong> <?= h($tableName) ?><br>
                    <strong>総デヌタ数:</strong> <?= number_format($totalCount) ?>ä»¶<br>
                    <strong>プレビュヌ:</strong> 最初の<?= count($previewData) ?>件を衚瀺
                </p>
            </div>
            
            <?php if (!empty($previewData)): ?>
            <div style="margin-bottom: 15px;">
                <p style="font-weight: bold; color: #333; margin-bottom: 5px;">
    
                        <span class="svg-wrapper" style="width: 20px; height: 20px; display: inline-flex;"><?php include 'svgs/info.svg'; ?></span>
                        䞍芁なカラムはチェックを倖しおください
                </p>
                <p style="color: #666; font-size: 0.95em;">※出力されるCSVは行列逆になりたす</p>
            </div>
            <div style="overflow-x: auto;">
                <table id="previewTable" class="preview-table transposed">
                    <?php
                    // デヌタを逆転させお衚瀺
                    if (!empty($previewData)) {
                        $transposedData = [];
                        
                        // 各カラムのデヌタを集める
                        foreach ($previewColumns as $colIndex => $columnName) {
                            $transposedData[$columnName] = [];
                            foreach ($previewData as $row) {
                                $values = array_values($row);
                                $transposedData[$columnName][] = $values[$colIndex] ?? '';
                            }
                        }
                        
                        // 逆転衚瀺
                        echo '<thead><tr>';
                        echo '<th class="checkbox-header"><input type="checkbox" id="select_all" checked onchange="toggleAllCheckboxes(this.checked)"></th>';
                        echo '<th class="row-header">項目</th>';
                        for ($i = 0; $i < count($previewData); $i++) {
                            echo '<th>デヌタ' . ($i + 1) . '</th>';
                        }
                        echo '</tr></thead>';
                        
                        echo '<tbody>';
                        foreach ($transposedData as $fieldName => $fieldValues) {
                            $fieldId = 'field_' . md5($fieldName); // ナニヌクなID生成
                            echo '<tr class="clickable-row" data-checkbox-id="' . $fieldId . '" style="cursor: pointer;">';
                            echo '<td class="checkbox-cell" onclick="event.stopPropagation();"><input type="checkbox" id="' . $fieldId . '" name="selected_fields[]" value="' . h($fieldName) . '" checked></td>';
                            echo '<th class="row-header">' . h($fieldName) . '</th>';
                            foreach ($fieldValues as $value) {
                                echo '<td title="' . h($value) . '">' . h($value) . '</td>';
                            }
                            echo '</tr>';
                        }
                        echo '</tbody>';
                    }
                    ?>
                </table>
            </div>
            
            <!-- ダりンロヌドボタン -->
            <div class="download-section">
                <h4>                
                    CSVダりンロヌド
                </h4>
                <p id="download-info">å…š<?= number_format($totalCount) ?>件のデヌタをCSVファむルずしおダりンロヌドしたす。</p>
                
                <form method="post" id="downloadForm">
                    <input type="hidden" name="action" value="download_csv">
                    <input type="hidden" name="service_name" value="<?= h($selectedService) ?>">
                    <input type="hidden" name="device_type" value="<?= h($selectedDeviceType) ?>">
                    
                    <!-- 遞択されたフィヌルドをここに動的に远加 -->
                    
                    <button type="submit" class="btn btn-success">
                        <span style="width: 16px; height: 16px; display: inline-flex; vertical-align: middle; margin-right: 4px;"><?php include 'svgs/download.svg'; ?></span>
                        CSV ダりンロヌド
                    </button>
                </form>
            </div>
            
            <?php else: ?>
            <div class="empty-state">
                <h3>デヌタがありたせん</h3>
                <p>遞択されたサヌビス・装眮皮別に察応するデヌタが芋぀かりたせん。</p>
            </div>
            <?php endif; ?>
            
            <?php else: ?>
            <div class="empty-state">
                <h3>テヌブルが存圚したせん</h3>
                <p>遞択されたサヌビス・装眮皮別に察応するテヌブルがただ䜜成されおいたせん。<br>
                先にCSVファむルをアップロヌドしおデヌタを登録しおください。</p>
            </div>
            <?php endif; ?>
        </div>
        <?php endif; ?>
        
        <?php if (empty($selectedService) || empty($selectedDeviceType)): ?>
        <div class="empty-state">
            <h3 style="display: flex; align-items: center; gap: 8px; justify-content: center;">
                <span style="width: 24px; height: 24px; display: inline-flex;"><?php include 'svgs/info.svg'; ?></span>
                䜿い方
            </h3>
            <ol style="text-align: left; display: inline-block;">
                <li>サヌビス名を遞択しおください</li>
                <li>装眮皮別を遞択しおください</li>
                <li>「プレビュヌ衚瀺」ボタンでデヌタを確認</li>
                <li>「CSVダりンロヌド」ボタンでファむルを取埗</li>
            </ol>
        </div>
        <?php endif; ?>

    <script>
        // 共通fetch関数セッションCookie送信のため credentials: 'same-origin' を付䞎
        async function apiFetch(url, options) {
            return fetch(url, Object.assign({ credentials: 'same-origin' }, options));
        }

        // 装眮皮別の曎新
        var updateDeviceTypes = async function() {
            const serviceName = document.getElementById('service_name').value;
            const deviceTypeSelect = document.getElementById('device_type');
            
            // 珟圚の遞択倀を保存
            const currentValue = deviceTypeSelect.value;
            
            // 装眮皮別をリセット
            deviceTypeSelect.innerHTML = '<option value="">-- 装眮皮別を遞択 --</option>';
            
            if (serviceName) {
                try {
                    const response = await apiFetch(`ajax_api.php?action=get_device_types&service_name=${encodeURIComponent(serviceName)}`);
                    const result = await response.json();
                    
                    if (result.success) {
                        result.data.forEach(deviceType => {
                            const option = document.createElement('option');
                            option.value = deviceType;
                            option.textContent = deviceType;
                            // 以前の遞択倀ず䞀臎する堎合は遞択状態にする
                            if (deviceType === currentValue) {
                                option.selected = true;
                            }
                            deviceTypeSelect.appendChild(option);
                        });
                    }
                } catch (error) {
                    console.error('装眮皮別取埗゚ラヌ:', error);
                }
            }
        }

        // プレビュヌの曎新
        function updatePreview() {
            const serviceName = document.getElementById('service_name').value;
            const deviceType = document.getElementById('device_type').value;
            
            if (serviceName && deviceType) {
                const params = new URLSearchParams();
                params.append('service_name', serviceName);
                params.append('device_type', deviceType);
                
                window.location.href = '?' + params.toString();
            }
        }
        
        // ペヌゞ読み蟌み時に装眮皮別を曎新遞択状態を保持
        document.addEventListener('DOMContentLoaded', async function() {
            // inline onchange の代わりに addEventListener で登録党ブラりザ察応
            document.getElementById('service_name').addEventListener('change', updateDeviceTypes);

            const serviceName = document.getElementById('service_name').value;
            if (serviceName) {
                await updateDeviceTypes();
            }
        });
        
        // チェックボックス関連機胜
        function toggleAllCheckboxes(checked) {
            const checkboxes = document.querySelectorAll('input[name="selected_fields[]"]');
            checkboxes.forEach(cb => cb.checked = checked);
            updateDownloadButton();
        }
        
        function updateDownloadButton() {
            const selectedFields = getSelectedFields();
            const downloadBtn = document.querySelector('#downloadForm button');
            if (downloadBtn) {
                const iconHtml = downloadBtn.querySelector('span')?.outerHTML || '';
                const baseText = 'CSV ダりンロヌド';
                const countText = selectedFields.length > 0 ? ` (${selectedFields.length}項目)` : ' (項目未遞択)';
                downloadBtn.innerHTML = iconHtml + baseText + countText;
            }
        }
        
        function getSelectedFields() {
            const checkboxes = document.querySelectorAll('input[name="selected_fields[]"]:checked');
            return Array.from(checkboxes).map(cb => cb.value);
        }
        
        function updateSelectedFields() {
            // チェックボックス倉曎時にダりンロヌドボタンを曎新
            updateDownloadButton();
        }
        
        // CSVダりンロヌド時のフィヌルド遞択を考慮
        document.addEventListener('DOMContentLoaded', function() {
            const downloadForm = document.getElementById('downloadForm');
            console.log('downloadForm found:', downloadForm);
            
            if (downloadForm) {
                downloadForm.addEventListener('submit', function(e) {
                    const selectedFields = getSelectedFields();
                    console.log('Submit event - Selected fields:', selectedFields);
                    
                    if (selectedFields.length === 0) {
                        alert('少なくずも1぀の項目を遞択しおください。');
                        e.preventDefault();
                        return;
                    }
                    
                    // 既存のhiddenフィヌルドをクリア
                    const existingHidden = downloadForm.querySelectorAll('input[type="hidden"][name="selected_fields[]"]');
                    console.log('Existing hidden fields to remove:', existingHidden.length);
                    existingHidden.forEach(input => input.remove());
                    
                    // 遞択されたフィヌルドをフォヌムに远加
                    selectedFields.forEach(field => {
                        const input = document.createElement('input');
                        input.type = 'hidden';
                        input.name = 'selected_fields[]';
                        input.value = field;
                        downloadForm.appendChild(input);
                        console.log('Added hidden field:', field);
                    });
                    
                    // 最終的なフォヌムデヌタを確認
                    const formData = new FormData(downloadForm);
                    console.log('Final form data:');
                    for (let pair of formData.entries()) {
                        console.log(pair[0] + ': ' + pair[1]);
                    }
                });
            }
            
            // チェックボックスにむベントリスナヌを远加
            const checkboxes = document.querySelectorAll('input[name="selected_fields[]"]');
            checkboxes.forEach(checkbox => {
                checkbox.addEventListener('change', updateSelectedFields);
            });
            
            // 「すべお遞択/解陀」チェックボックスにもむベントリスナヌ
            const selectAllCheckbox = document.getElementById('select_all');
            if (selectAllCheckbox) {
                selectAllCheckbox.addEventListener('change', function() {
                    toggleAllCheckboxes(this.checked);
                    updateDownloadButton(); // ボタンも曎新
                });
            }
            
            // 初期衚瀺時にダりンロヌドボタンを曎新
            updateDownloadButton();
            
            // 行クリックでチェックボックスの状態を倉曎
            const clickableRows = document.querySelectorAll('.clickable-row');
            clickableRows.forEach(row => {
                row.addEventListener('click', function(e) {
                    // チェックボックスセル以倖をクリックした堎合
                    if (!e.target.closest('.checkbox-cell')) {
                        const checkboxId = this.getAttribute('data-checkbox-id');
                        const checkbox = document.getElementById(checkboxId);
                        if (checkbox) {
                            checkbox.checked = !checkbox.checked;
                            updateDownloadButton();
                        }
                    }
                });
            });
        });
    </script>

        </div> <!-- page-container -->
    </div> <!-- main-content -->

<?php require_once 'includes/footer.php'; ?>

create_audit_logs.php

📂 admin\create_audit_logs.php | 行数: 42 | 最終曎新: 2026-03-04 14:03:48
<?php
/**
 * audit_logs テヌブル䜜成マむグレヌション
 * 実行: docker compose exec web php admin/create_audit_logs.php
 */
require_once __DIR__ . '/../config.php';

echo "=== audit_logs テヌブル マむグレヌション ===\n\n";

try {
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
    $conn = $database->connect();

    $sql = "
        CREATE TABLE IF NOT EXISTS audit_logs (
            id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
            username    VARCHAR(100)  NOT NULL                      COMMENT '操䜜ナヌザヌ名',
            action      VARCHAR(50)   NOT NULL                      COMMENT 'アクション皮別',
            detail      TEXT                                        COMMENT '補足情報',
            ip_address  VARCHAR(45)                                 COMMENT 'クラむアントIP',
            created_at  TIMESTAMP     DEFAULT CURRENT_TIMESTAMP     COMMENT '操䜜日時',
            INDEX idx_audit_username (username),
            INDEX idx_audit_action   (action),
            INDEX idx_audit_created  (created_at)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
          COMMENT='ナヌザヌ操䜜ログ'
    ";

    $conn->exec($sql);
    echo "✓ audit_logs テヌブルを䜜成したした既存の堎合はスキップ\n";

    // 確認
    $check = $conn->query("SELECT COUNT(*) FROM audit_logs")->fetchColumn();
    echo "✓ 珟圚のログ件数: {$check} ä»¶\n";

    echo "\n=== マむグレヌション完了 ===\n";

} catch (Exception $e) {
    echo "❌ ゚ラヌ: " . $e->getMessage() . "\n";
    exit(1);
}

ActivityLogger.php

📂 classes\ActivityLogger.php | 行数: 86 | 最終曎新: 2026-03-04 14:03:24
<?php
/**
 * ナヌザヌ操䜜ログ蚘録クラス
 * upload / download / manage (update_device, delete_device, teraterm) の操䜜を
 * audit_logs テヌブルに蚘録する。
 */
class ActivityLogger {

    private $db;

    /** ログのアクション定数 */
    const ACTION_UPLOAD         = 'upload';
    const ACTION_DOWNLOAD       = 'download';
    const ACTION_UPDATE_DEVICE  = 'update_device';
    const ACTION_DELETE_DEVICE  = 'delete_device';
    const ACTION_TERATERM       = 'teraterm_download';

    public function __construct(Database $database) {
        $this->db = $database;
    }

    /**
     * 操䜜ログを蚘録する
     *
     * @param string      $username  操䜜ナヌザヌ名
     * @param string      $action    アクション皮別ACTION_* 定数を掚奚
     * @param string|null $detail    補足情報ファむル名・察象装眮 primary_key など
     * @param string|null $ip        クラむアントIPアドレス省略時は自動取埗
     * @return void
     */
    public function log(string $username, string $action, ?string $detail = null, ?string $ip = null): void {
        try {
            $resolvedIp = $ip ?? $this->getClientIp();

            $this->db->execute(
                "INSERT INTO audit_logs (username, action, detail, ip_address, created_at)
                 VALUES (:username, :action, :detail, :ip, NOW())",
                [
                    ':username' => $username,
                    ':action'   => $action,
                    ':detail'   => $detail,
                    ':ip'       => $resolvedIp,
                ]
            );
        } catch (Exception $e) {
            // ログ倱敗はアプリの凊理を止めない゚ラヌログのみ
            error_log("ActivityLogger::log error: " . $e->getMessage());
        }
    }

    /**
     * クラむアントIPを取埗リバヌスプロキシも考慮
     */
    private function getClientIp(): string {
        $headers = [
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_REAL_IP',
            'HTTP_CLIENT_IP',
            'REMOTE_ADDR',
        ];
        foreach ($headers as $h) {
            if (!empty($_SERVER[$h])) {
                $ip = trim(explode(',', $_SERVER[$h])[0]);
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }
        return 'unknown';
    }

    /**
     * ログ䞀芧を取埗管理画面甚
     *
     * @param int $limit
     * @param int $offset
     * @return array
     */
    public function getLogs(int $limit = 100, int $offset = 0): array {
        return $this->db->query(
            "SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
            [':limit' => $limit, ':offset' => $offset]
        );
    }
}

config.php

📂 config.php | 行数: 137 | 最終曎新: 2026-03-04 14:00:16
<?php
/**
 * アプリケヌション蚭定ファむル
 * Docker環境甚蚭定docker-compose.yml の環境倉数に察応
 */

// デヌタベヌス蚭定環境倉数察応
define('DB_HOST',    $_ENV['DB_HOST']    ?? 'mysql');
define('DB_NAME',    $_ENV['DB_NAME']    ?? 'device_management');
define('DB_USER',    $_ENV['DB_USER']    ?? 'root');
define('DB_PASS',    $_ENV['DB_PASS']    ?? 'rootpassword');
define('DB_CHARSET', 'utf8mb4');
define('DB_TYPE',    $_ENV['DB_TYPE']    ?? 'mysql');
define('DB_PORT',    $_ENV['DB_PORT']    ?? null);

// アップロヌド蚭定
define('UPLOAD_MAX_SIZE',     10 * 1024 * 1024); // 10MB
define('UPLOAD_ALLOWED_TYPES', ['text/csv', 'application/csv', 'text/plain']);
define('UPLOAD_DIR',           __DIR__ . '/uploads/');

// ゚ラヌレポヌト蚭定本番環境では 0 に倉曎
ini_set('display_errors', 1);
error_reporting(E_ALL);

// ゚ラヌログ蚭定
ini_set('log_errors',  1);
ini_set('error_log',   __DIR__ . '/logs/php_error.log');

// タむムゟヌン蚭定
date_default_timezone_set('Asia/Tokyo');

// セッション蚭定
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// クラスファむルの自動読み蟌み
spl_autoload_register(function ($class_name) {
    $file = __DIR__ . '/classes/' . $class_name . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

// -------------------------------------------------------
// ナヌティリティ関数
// -------------------------------------------------------

/** テヌブル名のサニタむズ */
function sanitizeTableName($name) {
    return preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
}

/** HTML出力甚゚スケヌプ */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

/** ゚ラヌメッセヌゞをセッションに保存 */
function setErrorMessage($message) {
    $_SESSION['error_message'] = $message;
}

/** ゚ラヌメッセヌゞを取埗しお削陀 */
function getErrorMessage() {
    if (isset($_SESSION['error_message'])) {
        $message = $_SESSION['error_message'];
        unset($_SESSION['error_message']);
        return $message;
    }
    return null;
}

/** 成功メッセヌゞをセッションに保存 */
function setSuccessMessage($message) {
    $_SESSION['success_message'] = $message;
}

/** 成功メッセヌゞを取埗しお削陀 */
function getSuccessMessage() {
    if (isset($_SESSION['success_message'])) {
        $message = $_SESSION['success_message'];
        unset($_SESSION['success_message']);
        return $message;
    }
    return null;
}

/** ファむル名のサニタむズ */
function sanitizeFilename($filename) {
    $filename = basename($filename);
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
    return $filename;
}

/** CSVヘッダヌのサニタむズBOM・特殊文字陀去 */
function sanitizeColumnName($name) {
    $name = str_replace("\xEF\xBB\xBF", '', $name); // BOM削陀
    $name = trim($name);
    $name = preg_replace('/[^\x20-\x7E]/', '', $name); // 制埡文字を削陀
    $name = preg_replace('/[^a-zA-Z0-9_\x80-\xFF]/', '_', $name);
    return $name;
}

/** 拡匵カラムのバリデヌション */
function validateExtendedColumn($columnName) {
    $reservedWords = [
        'SELECT','INSERT','UPDATE','DELETE','DROP','CREATE','ALTER',
        'TABLE','DATABASE','INDEX','PRIMARY','KEY','FOREIGN','REFERENCES',
        'WHERE','FROM','JOIN','UNION','GROUP','ORDER','HAVING','LIMIT'
    ];
    if (in_array(strtoupper($columnName), $reservedWords)) return false;

    $fixedColumns = [
        'primary_key','service_name','device_type','device_name',
        'device_ip','username','password','created_at','updated_at'
    ];
    if (in_array($columnName, $fixedColumns)) return false;
    if (strlen($columnName) > 64)             return false;
    if (preg_match('/^\d/', $columnName))      return false;

    return true;
}

/** CSRFトヌクン生成 */
function generateCsrfToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/** CSRFトヌクン怜蚌 */
function validateCsrfToken($token) {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

Database.php

📂 classes\Database.php | 行数: 206 | 最終曎新: 2026-03-04 13:59:10
<?php
/**
 * Database接続管理クラス
 */
class Database {
    private $connection;
    private $host;
    private $dbname;
    private $username;
    private $password;
    private $charset;
    private $dbType;
    private $port;
    private $inTransaction = false;
    
    public function __construct($host, $dbname, $username, $password, $charset = 'utf8mb4', $dbType = 'mysql', $port = null) {
        $this->host = $host;
        $this->dbname = $dbname;
        $this->username = $username;
        $this->password = $password;
        $this->charset = $charset;
        $this->dbType = $dbType;
        $this->port = $port ?: ($dbType === 'pgsql' ? 5432 : 3306);
    }
    
    /**
     * デヌタベヌスタむプを取埗
     * @return string
     */
    public function getDbType() {
        return $this->dbType;
    }
    
    /**
     * デヌタベヌスに接続
     * @return PDO
     * @throws Exception
     */
    public function connect() {
        if ($this->connection === null) {
            try {
                // DSN生成PostgreSQLずMySQL察応
                if ($this->dbType === 'pgsql') {
                    $dsn = "pgsql:host={$this->host};port={$this->port};dbname={$this->dbname}";
                } else {
                    $dsn = "mysql:host={$this->host};port={$this->port};dbname={$this->dbname};charset={$this->charset}";
                }
                
                $options = [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ];
                
                $this->connection = new PDO($dsn, $this->username, $this->password, $options);
                
                // PostgreSQLの堎合はUTF-8゚ンコヌディングを蚭定
                if ($this->dbType === 'pgsql') {
                    $this->connection->exec("SET NAMES 'UTF8'");
                }
            } catch (PDOException $e) {
                throw new Exception("デヌタベヌス接続゚ラヌ: " . $e->getMessage());
            }
        }
        
        return $this->connection;
    }
    
    /**
     * 接続を閉じる
     */
    public function close() {
        if ($this->connection !== null) {
            // アクティブなトランザクションがあればロヌルバック
            try {
                if ($this->connection->inTransaction()) {
                    error_log("Warning: Active transaction found during connection close, rolling back");
                    $this->connection->rollBack();
                    $this->inTransaction = false;
                }
            } catch (Exception $e) {
                error_log("Error during transaction cleanup: " . $e->getMessage());
            }
            
            $this->connection = null;
        }
    }
    
    /**
     * トランザクション開始
     */
    public function beginTransaction() {
        $connection = $this->connect();
        if (!$connection->inTransaction()) {
            $result = $connection->beginTransaction();
            if ($result) {
                $this->inTransaction = true;
            }
            return $result;
        }
        return true; // 既にトランザクション䞭
    }
    
    /**
     * コミット
     */
    public function commit() {
        $connection = $this->connect();
        if ($connection->inTransaction()) {
            $result = $connection->commit();
            $this->inTransaction = false;
            return $result;
        }
        return true; // アクティブなトランザクションがない
    }
    
    /**
     * ロヌルバック
     */
    public function rollBack() {
        $connection = $this->connect();
        if ($connection->inTransaction()) {
            $result = $connection->rollBack();
            $this->inTransaction = false;
            return $result;
        }
        return true; // アクティブなトランザクションがない
    }
    
    /**
     * トランザクション状態を確認
     */
    public function inTransaction() {
        return $this->connect()->inTransaction();
    }
    
    /**
     * プリペアドステヌトメントの実行
     * @param string $query
     * @param array $params
     * @return PDOStatement
     */
    public function execute($query, $params = []) {
        try {
            $stmt = $this->connect()->prepare($query);
            $stmt->execute($params);
            return $stmt;
        } catch (PDOException $e) {
            throw new Exception("ク゚リ実行゚ラヌ: " . $e->getMessage() . " SQL: " . $query);
        }
    }
    
    /**
     * SQLク゚リを実行しお結果を配列で返す
     * @param string $query
     * @param array $params
     * @return array
     */
    public function query($query, $params = []) {
        try {
            $stmt = $this->connect()->prepare($query);
            $stmt->execute($params);
            return $stmt->fetchAll();
        } catch (PDOException $e) {
            throw new Exception("ク゚リ実行゚ラヌ: " . $e->getMessage() . " SQL: " . $query);
        }
    }

    /**
     * テヌブルの存圚確認
     * @param string $tableName
     * @return bool
     */
    public function tableExists($tableName) {
        try {
            $stmt = $this->execute(
                "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ?",
                [$this->dbname, $tableName]
            );
            return $stmt->fetchColumn() > 0;
        } catch (Exception $e) {
            throw new Exception("テヌブル存圚確認゚ラヌ: " . $e->getMessage());
        }
    }
    
    /**
     * テヌブルのカラム情報を取埗
     * @param string $tableName
     * @return array
     */
    public function getTableColumns($tableName) {
        try {
            $stmt = $this->execute(
                "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT 
                 FROM information_schema.columns 
                 WHERE table_schema = ? AND table_name = ? 
                 ORDER BY ORDINAL_POSITION",
                [$this->dbname, $tableName]
            );
            return $stmt->fetchAll();
        } catch (Exception $e) {
            throw new Exception("カラム情報取埗゚ラヌ: " . $e->getMessage());
        }
    }
}
?>

test_ajax_direct.php

📂 test_ajax_direct.php | 行数: 379 | 最終曎新: 2026-03-04 13:58:02
<?php
/**
 * Ajax API 盎接テスト - ブラりザで開いお動䜜確認
 */
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ajax API テスト</title>
    <style>
        body { font-family: sans-serif; margin: 20px; background: #f5f5f5; }
        .test-section { background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
        .btn { padding: 10px 20px; margin: 5px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .btn:hover { background: #2980b9; }
        .result { background: #f9f9f9; padding: 15px; margin: 10px 0; border-left: 4px solid #3498db; }
        .error { border-left-color: #e74c3c; background: #fadbd8; }
        .success { border-left-color: #27ae60; background: #d5f4e6; }
        pre { white-space: pre-wrap; word-wrap: break-word; }
        .loading { color: #3498db; font-style: italic; }
    </style>
</head>
<body>
    <h1>🧪 Ajax API 盎接テスト</h1>

    <div class="test-section">
        <h2>1⃣ サヌビス名取埗テスト</h2>
        <button class="btn" onclick="testGetServices()">サヌビス名取埗</button>
        <div id="result1" class="result" style="display:none;"></div>
    </div>

    <div class="test-section">
        <h2>2⃣ 装眮皮別取埗テスト</h2>
        <p>たずサヌビス名を取埗しおから実行しおください</p>
        <input type="text" id="serviceNameInput" placeholder="サヌビス名を入力" style="padding: 8px; width: 300px;">
        <button class="btn" onclick="testGetDeviceTypes()">装眮皮別取埗</button>
        <div id="result2" class="result" style="display:none;"></div>
    </div>

    <div class="test-section">
        <h2>3⃣ 怜玢API テスト党件</h2>
        <button class="btn" onclick="testSearchAll()">党件怜玢</button>
        <div id="result3" class="result" style="display:none;"></div>
    </div>

    <div class="test-section">
        <h2>4⃣ 怜玢API テスト条件指定</h2>
        <p>サヌビス名: <input type="text" id="searchServiceName" placeholder="サヌビス名" style="padding: 8px; width: 200px;"></p>
        <p>装眮皮別: <input type="text" id="searchDeviceType" placeholder="装眮皮別" style="padding: 8px; width: 200px;"></p>
        <p>装眮名称: <input type="text" id="searchDeviceName" placeholder="装眮名称郚分䞀臎" style="padding: 8px; width: 200px;"></p>
        <button class="btn" onclick="testSearchWithConditions()">条件怜玢</button>
        <div id="result4" class="result" style="display:none;"></div>
    </div>

    <div class="test-section">
        <h2>📊 統合テスト</h2>
        <button class="btn" onclick="runAllTests()">党テスト実行</button>
        <div id="result5" class="result" style="display:none;"></div>
    </div>

    <div class="test-section">
        <h2>📚 関連リンク</h2>
        <ul>
            <li><a href="search.php" target="_blank">怜玢ペヌゞ本番</a></li>
            <li><a href="manage.php" target="_blank">管理ペヌゞ本番</a></li>
            <li><a href="debug_search.php">デバッグツヌル</a></li>
            <li><a href="index.php">トップペヌゞ</a></li>
        </ul>
    </div>

    <script>
        // ナヌティリティ関数
        function showResult(elementId, content, type = 'info') {
            const element = document.getElementById(elementId);
            element.style.display = 'block';
            element.className = 'result ' + type;
            element.innerHTML = content;
        }

        function showLoading(elementId) {
            showResult(elementId, '<span class="loading">⏳ 凊理䞭...</span>', 'info');
        }

        // 1. サヌビス名取埗テスト
        async function testGetServices() {
            showLoading('result1');
            
            try {
                console.log('🔍 Testing: ajax_api.php?action=get_services');
                
                const response = await fetch('ajax_api.php?action=get_services');
                const text = await response.text();
                
                console.log('Response status:', response.status);
                console.log('Response text:', text);
                
                let result;
                try {
                    result = JSON.parse(text);
                } catch (e) {
                    throw new Error('JSON parse error: ' + e.message + '\n\nResponse: ' + text);
                }
                
                let html = '<h3>レスポンス:</h3>';
                html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
                
                if (result.success) {
                    html += '<h3>取埗されたサヌビス名:</h3>';
                    html += '<ul>';
                    if (result.data && result.data.length > 0) {
                        result.data.forEach(service => {
                            html += '<li>' + escapeHtml(service) + '</li>';
                        });
                        
                        // 最初のサヌビス名を自動入力
                        document.getElementById('serviceNameInput').value = result.data[0];
                        document.getElementById('searchServiceName').value = result.data[0];
                    } else {
                        html += '<li style="color: orange;">デヌタがありたせん</li>';
                    }
                    html += '</ul>';
                    
                    showResult('result1', html, 'success');
                } else {
                    html += '<p style="color: red;">゚ラヌ: ' + escapeHtml(result.message) + '</p>';
                    showResult('result1', html, 'error');
                }
            } catch (error) {
                console.error('Error:', error);
                const html = '<h3>゚ラヌ発生:</h3><pre>' + escapeHtml(error.message) + '</pre>';
                showResult('result1', html, 'error');
            }
        }

        // 2. 装眮皮別取埗テスト
        async function testGetDeviceTypes() {
            const serviceName = document.getElementById('serviceNameInput').value;
            
            if (!serviceName) {
                alert('サヌビス名を入力しおください');
                return;
            }
            
            showLoading('result2');
            
            try {
                const url = 'ajax_api.php?action=get_device_types&service_name=' + encodeURIComponent(serviceName);
                console.log('🔍 Testing:', url);
                
                const response = await fetch(url);
                const text = await response.text();
                
                console.log('Response status:', response.status);
                console.log('Response text:', text);
                
                let result;
                try {
                    result = JSON.parse(text);
                } catch (e) {
                    throw new Error('JSON parse error: ' + e.message + '\n\nResponse: ' + text);
                }
                
                let html = '<h3>レスポンス:</h3>';
                html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
                
                if (result.success) {
                    html += '<h3>取埗された装眮皮別:</h3>';
                    html += '<ul>';
                    if (result.data && result.data.length > 0) {
                        result.data.forEach(type => {
                            html += '<li>' + escapeHtml(type) + '</li>';
                        });
                        
                        // 最初の装眮皮別を自動入力
                        document.getElementById('searchDeviceType').value = result.data[0];
                    } else {
                        html += '<li style="color: orange;">デヌタがありたせん</li>';
                    }
                    html += '</ul>';
                    
                    showResult('result2', html, 'success');
                } else {
                    html += '<p style="color: red;">゚ラヌ: ' + escapeHtml(result.message) + '</p>';
                    showResult('result2', html, 'error');
                }
            } catch (error) {
                console.error('Error:', error);
                const html = '<h3>゚ラヌ発生:</h3><pre>' + escapeHtml(error.message) + '</pre>';
                showResult('result2', html, 'error');
            }
        }

        // 3. 党件怜玢テスト
        async function testSearchAll() {
            showLoading('result3');
            
            try {
                const url = 'ajax_api.php?action=search_devices&page=1';
                console.log('🔍 Testing:', url);
                
                const response = await fetch(url);
                const text = await response.text();
                
                console.log('Response status:', response.status);
                console.log('Response text:', text);
                
                let result;
                try {
                    result = JSON.parse(text);
                } catch (e) {
                    throw new Error('JSON parse error: ' + e.message + '\n\nResponse: ' + text);
                }
                
                let html = '<h3>レスポンス:</h3>';
                html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
                
                if (result.success && result.data) {
                    const count = result.data.pagination.total_count;
                    const devices = result.data.devices;
                    
                    html += '<h3>怜玢結果: ' + count + ' ä»¶</h3>';
                    
                    if (devices && devices.length > 0) {
                        html += '<table border="1" cellpadding="5" style="margin-top: 10px;">';
                        html += '<tr><th>サヌビス名</th><th>装眮皮別</th><th>装眮名称</th><th>ログむンIP</th><th>ナヌザ名1</th></tr>';
                        devices.forEach(device => {
                            html += '<tr>';
                            html += '<td>' + escapeHtml(device.service_name) + '</td>';
                            html += '<td>' + escapeHtml(device.device_type) + '</td>';
                            html += '<td>' + escapeHtml(device.device_name) + '</td>';
                            html += '<td>' + escapeHtml(device.login_ip || '-') + '</td>';
                            html += '<td>' + escapeHtml(device.username1) + '</td>';
                            html += '</tr>';
                        });
                        html += '</table>';
                    }
                    
                    showResult('result3', html, 'success');
                } else {
                    html += '<p style="color: red;">゚ラヌ: ' + escapeHtml(result.message || '䞍明な゚ラヌ') + '</p>';
                    showResult('result3', html, 'error');
                }
            } catch (error) {
                console.error('Error:', error);
                const html = '<h3>゚ラヌ発生:</h3><pre>' + escapeHtml(error.message) + '</pre>';
                showResult('result3', html, 'error');
            }
        }

        // 4. 条件怜玢テスト
        async function testSearchWithConditions() {
            showLoading('result4');
            
            try {
                const serviceName = document.getElementById('searchServiceName').value;
                const deviceType = document.getElementById('searchDeviceType').value;
                const deviceName = document.getElementById('searchDeviceName').value;
                
                const params = new URLSearchParams();
                params.append('action', 'search_devices');
                params.append('page', '1');
                if (serviceName) params.append('service_name', serviceName);
                if (deviceType) params.append('device_type', deviceType);
                if (deviceName) params.append('device_name', deviceName);
                
                const url = 'ajax_api.php?' + params.toString();
                console.log('🔍 Testing:', url);
                
                const response = await fetch(url);
                const text = await response.text();
                
                console.log('Response status:', response.status);
                console.log('Response text:', text);
                
                let result;
                try {
                    result = JSON.parse(text);
                } catch (e) {
                    throw new Error('JSON parse error: ' + e.message + '\n\nResponse: ' + text);
                }
                
                let html = '<h3>怜玢条件:</h3>';
                html += '<ul>';
                html += '<li>サヌビス名: ' + (serviceName || '指定なし') + '</li>';
                html += '<li>装眮皮別: ' + (deviceType || '指定なし') + '</li>';
                html += '<li>装眮名称: ' + (deviceName || '指定なし') + '</li>';
                html += '</ul>';
                
                html += '<h3>レスポンス:</h3>';
                html += '<pre>' + JSON.stringify(result, null, 2) + '</pre>';
                
                if (result.success && result.data) {
                    const count = result.data.pagination.total_count;
                    html += '<h3>怜玢結果: ' + count + ' ä»¶</h3>';
                    showResult('result4', html, 'success');
                } else {
                    html += '<p style="color: red;">゚ラヌ: ' + escapeHtml(result.message || '䞍明な゚ラヌ') + '</p>';
                    showResult('result4', html, 'error');
                }
            } catch (error) {
                console.error('Error:', error);
                const html = '<h3>゚ラヌ発生:</h3><pre>' + escapeHtml(error.message) + '</pre>';
                showResult('result4', html, 'error');
            }
        }

        // 党テスト実行
        async function runAllTests() {
            showResult('result5', '<h3>党テストを順次実行䞭...</h3>', 'info');
            
            let allResults = '<h3>統合テスト結果</h3>';
            let allSuccess = true;
            
            // テスト1
            try {
                const response = await fetch('ajax_api.php?action=get_services');
                const result = await response.json();
                if (result.success && result.data && result.data.length > 0) {
                    allResults += '<p>✓ サヌビス名取埗: <span style="color: green;">成功</span> (' + result.data.length + 'ä»¶)</p>';
                } else {
                    allResults += '<p>✗ サヌビス名取埗: <span style="color: red;">倱敗</span></p>';
                    allSuccess = false;
                }
            } catch (e) {
                allResults += '<p>✗ サヌビス名取埗: <span style="color: red;">゚ラヌ</span> - ' + e.message + '</p>';
                allSuccess = false;
            }
            
            // テスト2
            try {
                const response = await fetch('ajax_api.php?action=search_devices&page=1');
                const result = await response.json();
                if (result.success && result.data) {
                    allResults += '<p>✓ 党件怜玢: <span style="color: green;">成功</span> (' + result.data.pagination.total_count + 'ä»¶)</p>';
                } else {
                    allResults += '<p>✗ 党件怜玢: <span style="color: red;">倱敗</span></p>';
                    allSuccess = false;
                }
            } catch (e) {
                allResults += '<p>✗ 党件怜玢: <span style="color: red;">゚ラヌ</span> - ' + e.message + '</p>';
                allSuccess = false;
            }
            
            if (allSuccess) {
                allResults += '<h2 style="color: green;">✓ すべおのテストが成功したした</h2>';
                allResults += '<p><strong>怜玢機胜は正垞に動䜜しおいたす。</strong></p>';
                allResults += '<p>それでも怜玢ペヌゞで動䜜しない堎合、以䞋を確認しおください</p>';
                allResults += '<ul>';
                allResults += '<li>ブラりザのキャッシュをクリアCtrl+Shift+Delete</li>';
                allResults += '<li>ブラりザの開発者ツヌルF12でJavaScript゚ラヌを確認</li>';
                allResults += '<li>怜玢ペヌゞsearch.phpのJavaScriptが正しく読み蟌たれおいるか確認</li>';
                allResults += '</ul>';
                showResult('result5', allResults, 'success');
            } else {
                allResults += '<h2 style="color: red;">✗ 䞀郚のテストが倱敗したした</h2>';
                showResult('result5', allResults, 'error');
            }
        }

        // HTML゚スケヌプ
        function escapeHtml(text) {
            if (text === null || text === undefined) return '';
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }
    </script>
</body>
</html>

debug_search.php

📂 debug_search.php | 行数: 313 | 最終曎新: 2026-03-04 13:58:02
<?php
/**
 * 怜玢機胜デバッグツヌル
 */
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>怜玢デバッグツヌル</title>
    <style>
        body { font-family: sans-serif; margin: 20px; background: #f5f5f5; }
        .section { background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
        .success { color: green; }
        .error { color: red; }
        .warning { color: orange; }
        table { width: 100%; border-collapse: collapse; margin: 10px 0; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background: #3498db; color: white; }
        tr:nth-child(even) { background: #f9f9f9; }
        .code { background: #f4f4f4; padding: 10px; border-left: 3px solid #3498db; margin: 10px 0; overflow-x: auto; }
        pre { margin: 0; }
        .count { font-size: 24px; font-weight: bold; color: #3498db; }
    </style>
</head>
<body>
    <h1>🔍 怜玢機胜デバッグツヌル</h1>

<?php
try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
    // ========== 1. デヌタベヌス接続確認 ==========
    echo "<div class='section'>";
    echo "<h2>1⃣ デヌタベヌス接続</h2>";
    echo "<p class='success'>✓ 接続成功</p>";
    echo "<p>デヌタベヌスタむプ: <strong>{$dbType}</strong></p>";
    echo "</div>";
    
    // ========== 2. テヌブル存圚確認 ==========
    echo "<div class='section'>";
    echo "<h2>2⃣ テヌブル存圚確認</h2>";
    
    $tables = ['device_info', 'service_device_type_relations'];
    foreach ($tables as $table) {
        $exists = $database->tableExists($table);
        $status = $exists ? "<span class='success'>✓ 存圚</span>" : "<span class='error'>✗ 䞍存圚</span>";
        echo "<p>{$table}: {$status}</p>";
    }
    echo "</div>";
    
    // ========== 3. デヌタ件数確認 ==========
    echo "<div class='section'>";
    echo "<h2>3⃣ デヌタ件数確認</h2>";
    
    if ($database->tableExists('device_info')) {
        $stmt = $database->execute("SELECT COUNT(*) as count FROM device_info");
        $result = $stmt->fetch();
        $count = $result['count'];
        
        echo "<p>device_info テヌブルのレコヌド数: <span class='count'>{$count}</span> ä»¶</p>";
        
        if ($count == 0) {
            echo "<p class='warning'>⚠ デヌタが登録されおいたせん。CSVをアップロヌドしおください。</p>";
        }
    } else {
        echo "<p class='error'>✗ device_info テヌブルが存圚したせん</p>";
    }
    
    if ($database->tableExists('service_device_type_relations')) {
        $stmt = $database->execute("SELECT COUNT(*) as count FROM service_device_type_relations WHERE is_active = 1");
        $result = $stmt->fetch();
        $relationCount = $result['count'];
        
        echo "<p>service_device_type_relations の有効なレコヌド数: <span class='count'>{$relationCount}</span> ä»¶</p>";
        
        if ($relationCount == 0) {
            echo "<p class='warning'>⚠ リレヌションデヌタが登録されおいたせん。</p>";
        }
    }
    echo "</div>";
    
    // ========== 4. サンプルデヌタ衚瀺 ==========
    if (isset($count) && $count > 0) {
        echo "<div class='section'>";
        echo "<h2>4⃣ サンプルデヌタ最新5件</h2>";
        
        $stmt = $database->execute("SELECT * FROM device_info ORDER BY created_at DESC LIMIT 5");
        $samples = $stmt->fetchAll();
        
        if (!empty($samples)) {
            echo "<table>";
            echo "<tr>";
            echo "<th>サヌビス名</th>";
            echo "<th>装眮皮別</th>";
            echo "<th>装眮名称</th>";
            echo "<th>ログむンIP</th>";
            echo "<th>ナヌザ名1</th>";
            echo "<th>登録日時</th>";
            echo "</tr>";
            
            foreach ($samples as $row) {
                echo "<tr>";
                echo "<td>" . htmlspecialchars($row['service_name']) . "</td>";
                echo "<td>" . htmlspecialchars($row['device_type']) . "</td>";
                echo "<td>" . htmlspecialchars($row['device_name']) . "</td>";
                echo "<td>" . htmlspecialchars($row['login_ip'] ?? '-') . "</td>";
                echo "<td>" . htmlspecialchars($row['username1']) . "</td>";
                echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
                echo "</tr>";
            }
            
            echo "</table>";
        }
        echo "</div>";
    }
    
    // ========== 5. サヌビス名䞀芧 ==========
    echo "<div class='section'>";
    echo "<h2>5⃣ サヌビス名䞀芧リレヌション</h2>";
    
    try {
        $services = $deviceManager->getServiceNamesFromRelation();
        
        echo "<p>取埗されたサヌビス名数: <span class='count'>" . count($services) . "</span> ä»¶</p>";
        
        if (!empty($services)) {
            echo "<ul>";
            foreach ($services as $service) {
                echo "<li>" . htmlspecialchars($service) . "</li>";
            }
            echo "</ul>";
        } else {
            echo "<p class='warning'>⚠ サヌビス名が取埗できたせん</p>";
            
            // device_infoから盎接取埗を詊す
            if ($database->tableExists('device_info')) {
                $stmt = $database->execute("SELECT DISTINCT service_name FROM device_info ORDER BY service_name");
                $directServices = $stmt->fetchAll(PDO::FETCH_COLUMN);
                
                if (!empty($directServices)) {
                    echo "<p class='warning'>⚠ device_infoには以䞋のサヌビス名がありたす</p>";
                    echo "<ul>";
                    foreach ($directServices as $service) {
                        echo "<li>" . htmlspecialchars($service) . "</li>";
                    }
                    echo "</ul>";
                    echo "<p><strong>原因:</strong> service_device_type_relations テヌブルにデヌタが登録されおいたせん。</p>";
                    echo "<p><a href='rebuild_relations.php' style='background:#3498db;color:white;padding:10px 20px;text-decoration:none;border-radius:4px;'>リレヌションを再構築</a></p>";
                }
            }
        }
    } catch (Exception $e) {
        echo "<p class='error'>✗ ゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>";
    }
    echo "</div>";
    
    // ========== 6. 装眮皮別䞀芧 ==========
    if (!empty($services)) {
        echo "<div class='section'>";
        echo "<h2>6⃣ 装眮皮別䞀芧最初のサヌビス</h2>";
        
        try {
            $firstService = $services[0];
            $deviceTypes = $deviceManager->getDeviceTypesFromRelation($firstService);
            
            echo "<p>サヌビス名: <strong>" . htmlspecialchars($firstService) . "</strong></p>";
            echo "<p>装眮皮別数: <span class='count'>" . count($deviceTypes) . "</span> ä»¶</p>";
            
            if (!empty($deviceTypes)) {
                echo "<ul>";
                foreach ($deviceTypes as $type) {
                    echo "<li>" . htmlspecialchars($type) . "</li>";
                }
                echo "</ul>";
            } else {
                echo "<p class='warning'>⚠ 装眮皮別が取埗できたせん</p>";
            }
        } catch (Exception $e) {
            echo "<p class='error'>✗ ゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>";
        }
        echo "</div>";
    }
    
    // ========== 7. 怜玢APIテスト ==========
    echo "<div class='section'>";
    echo "<h2>7⃣ 怜玢APIテスト</h2>";
    
    // テスト1: 党件怜玢
    try {
        echo "<h3>テスト1: 党件怜玢条件なし</h3>";
        $devices = $deviceManager->searchDevicesAdvanced(null, null, null, 10, 0);
        $total = $deviceManager->countDevicesAdvanced(null, null, null);
        
        echo "<p>怜玢結果: <span class='count'>{$total}</span> 件衚瀺: " . count($devices) . " 件</p>";
        
        if ($total > 0 && count($devices) > 0) {
            echo "<p class='success'>✓ 党件怜玢は正垞に動䜜しおいたす</p>";
        } elseif ($total > 0 && count($devices) == 0) {
            echo "<p class='error'>✗ デヌタは存圚するが取埗できたせんク゚リに問題がある可胜性</p>";
        } else {
            echo "<p class='warning'>⚠ デヌタが登録されおいたせん</p>";
        }
    } catch (Exception $e) {
        echo "<p class='error'>✗ ゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>";
        echo "<div class='code'><pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre></div>";
    }
    
    // テスト2: サヌビス名で絞り蟌み
    if (!empty($services)) {
        try {
            echo "<h3>テスト2: サヌビス名で絞り蟌み" . htmlspecialchars($services[0]) . "</h3>";
            $devices = $deviceManager->searchDevicesAdvanced($services[0], null, null, 10, 0);
            $total = $deviceManager->countDevicesAdvanced($services[0], null, null);
            
            echo "<p>怜玢結果: <span class='count'>{$total}</span> ä»¶</p>";
            
            if ($total > 0) {
                echo "<p class='success'>✓ サヌビス名での絞り蟌みは正垞に動䜜しおいたす</p>";
            } else {
                echo "<p class='warning'>⚠ 該圓デヌタなし</p>";
            }
        } catch (Exception $e) {
            echo "<p class='error'>✗ ゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>";
        }
    }
    
    echo "</div>";
    
    // ========== 8. Ajax APIテスト ==========
    echo "<div class='section'>";
    echo "<h2>8⃣ Ajax APIテスト</h2>";
    
    echo "<p>以䞋のリンクで盎接APIをテストできたす</p>";
    echo "<ul>";
    echo "<li><a href='ajax_api.php?action=get_services' target='_blank'>サヌビス名取埗API</a></li>";
    if (!empty($services)) {
        echo "<li><a href='ajax_api.php?action=get_device_types&service_name=" . urlencode($services[0]) . "' target='_blank'>装眮皮別取埗API" . htmlspecialchars($services[0]) . "</a></li>";
        echo "<li><a href='ajax_api.php?action=search_devices&service_name=" . urlencode($services[0]) . "&page=1' target='_blank'>怜玢API" . htmlspecialchars($services[0]) . "</a></li>";
    }
    echo "<li><a href='ajax_api.php?action=search_devices&page=1' target='_blank'>怜玢API党件</a></li>";
    echo "</ul>";
    echo "</div>";
    
    // ========== 蚺断結果 ==========
    echo "<div class='section' style='background: #e8f4f8;'>";
    echo "<h2>🎯 蚺断結果</h2>";
    
    $issues = [];
    
    if (!isset($count) || $count == 0) {
        $issues[] = "device_infoテヌブルにデヌタが登録されおいたせん → CSVをアップロヌドしおください";
    }
    
    if (!isset($relationCount) || $relationCount == 0) {
        $issues[] = "リレヌションデヌタが登録されおいたせん → <a href='rebuild_relations.php'>リレヌション再構築</a>を実行しおください";
    }
    
    if (empty($services) && isset($count) && $count > 0) {
        $issues[] = "デヌタは存圚するがリレヌションが未登録です → <a href='rebuild_relations.php'>リレヌション再構築</a>を実行しおください";
    }
    
    if (empty($issues)) {
        echo "<p class='success' style='font-size: 18px;'>✓ 問題は怜出されたせんでした。</p>";
        echo "<p>それでも怜玢できない堎合は、ブラりザの開発者ツヌルF12でコン゜ヌル゚ラヌを確認しおください。</p>";
    } else {
        echo "<p style='font-size: 18px; color: #e74c3c;'>⚠ 以䞋の問題が芋぀かりたした</p>";
        echo "<ol>";
        foreach ($issues as $issue) {
            echo "<li>{$issue}</li>";
        }
        echo "</ol>";
    }
    
    echo "</div>";
    
    $database->close();
    
} catch (Exception $e) {
    echo "<div class='section'>";
    echo "<h2 class='error'>❌ ゚ラヌ</h2>";
    echo "<p class='error'>" . htmlspecialchars($e->getMessage()) . "</p>";
    echo "<div class='code'><pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre></div>";
    echo "</div>";
}
?>

<div class="section">
    <h2>📚 関連リンク</h2>
    <ul>
        <li><a href="index.php">トップペヌゞ</a></li>
        <li><a href="search.php">怜玢ペヌゞ</a></li>
        <li><a href="manage.php">管理ペヌゞ</a></li>
        <li><a href="check_db_schema.php">スキヌマ確認</a></li>
        <li><a href="rebuild_relations.php">リレヌション再構築</a></li>
    </ul>
</div>

</body>
</html>

CsvProcessor.php

📂 classes\CsvProcessor.php | 行数: 376 | 最終曎新: 2026-03-04 13:58:02
<?php
/**
 * CSVファむル凊理クラス
 */
class CsvProcessor {
    private $filePath;
    private $headers;
    private $data;
    private $errors;
    private $delimiter;
    
    public function __construct($filePath = null) {
        $this->filePath = $filePath;
        $this->headers = [];
        $this->data = [];
        $this->errors = [];
        $this->delimiter = ',';
    }

    /**
     * CSVの区切り文字を自動怜出するカンマ or タブ
     * @param string $content ファむル内容
     * @return string 怜出された区切り文字
     */
    private function detectDelimiter($content) {
        $firstLine = strtok($content, "\n");
        $tabCount   = substr_count($firstLine, "\t");
        $commaCount = substr_count($firstLine, ',');
        return ($tabCount > $commaCount) ? "\t" : ',';
    }

    /**
     * 䜿甚䞭の区切り文字を返す
     * @return string
     */
    public function getDelimiter() {
        return $this->delimiter;
    }
    
    /**
     * CSVファむルを読み蟌む
     * @param string $filePath
     * @return bool
     */
    public function loadFile($filePath) {
        $this->filePath = $filePath;
        $this->headers = [];
        $this->data = [];
        $this->errors = [];
        
        if (!file_exists($filePath)) {
            $this->errors[] = "ファむルが存圚したせん: " . $filePath;
            return false;
        }
        
        if (!is_readable($filePath)) {
            $this->errors[] = "ファむルを読み蟌めたせん: " . $filePath;
            return false;
        }
        
        // ファむルの゚ンコヌディングを怜出・倉換
        $content = file_get_contents($filePath);
        $encoding = mb_detect_encoding($content, ['UTF-8', 'SJIS', 'EUC-JP', 'ASCII'], true);
        
        if ($encoding !== 'UTF-8') {
            $content = mb_convert_encoding($content, 'UTF-8', $encoding);
            file_put_contents($filePath, $content);
        }

        // 区切り文字を自動怜出カンマ or タブ
        $this->delimiter = $this->detectDelimiter($content);

        // CSVファむルを開く
        $handle = fopen($filePath, 'r');
        if ($handle === false) {
            $this->errors[] = "CSVファむルを開けたせん";
            return false;
        }
        
        $rowNumber = 0;
        while (($row = fgetcsv($handle, 0, $this->delimiter)) !== false) {
            $rowNumber++;
            
            if ($rowNumber === 1) {
                // ヘッダヌ行
                $this->headers = array_map('trim', $row);
                
                // 必須カラムの存圚確認4぀のみ
                $requiredColumns = ['サヌビス名', '装眮皮別', '装眮名称', 'ナヌザ名1'];
                foreach ($requiredColumns as $required) {
                    if (!in_array($required, $this->headers)) {
                        $this->errors[] = "必須カラムが䞍足しおいたす: " . $required;
                    }
                }
                
                if (!empty($this->errors)) {
                    fclose($handle);
                    return false;
                }
            } else {
                // デヌタ行
                if (count($row) !== count($this->headers)) {
                    $this->errors[] = "行{$rowNumber}: カラム数が䞀臎したせん期埅: " . count($this->headers) . ", 実際: " . count($row) . "";
                    continue;
                }
                
                $rowData = [];
                for ($i = 0; $i < count($this->headers); $i++) {
                    $rowData[$this->headers[$i]] = isset($row[$i]) ? trim($row[$i]) : '';
                }
                
                // 必須項目の怜蚌
                if (empty($rowData['サヌビス名'])) {
                    $this->errors[] = "行{$rowNumber}: サヌビス名が空です";
                    continue;
                }
                if (empty($rowData['装眮皮別'])) {
                    $this->errors[] = "行{$rowNumber}: 装眮皮別が空です";
                    continue;
                }
                if (empty($rowData['装眮名称'])) {
                    $this->errors[] = "行{$rowNumber}: 装眮名称が空です";
                    continue;
                }
                if (empty($rowData['ナヌザ名1'])) {
                    $this->errors[] = "行{$rowNumber}: ナヌザ名1が空です";
                    continue;
                }
                
                $this->data[] = $rowData;
            }
        }
        
        fclose($handle);
        
        if (empty($this->data)) {
            $this->errors[] = "有効なデヌタが芋぀かりたせん";
            return false;
        }
        
        return empty($this->errors);
    }
    
    /**
     * ヘッダヌ情報を取埗
     * @return array
     */
    public function getHeaders() {
        return $this->headers;
    }
    
    /**
     * デヌタを取埗
     * @return array
     */
    public function getData() {
        return $this->data;
    }
    
    /**
     * ゚ラヌ情報を取埗
     * @return array
     */
    public function getErrors() {
        return $this->errors;
    }
    
    /**
     * device_infoテヌブルのカラム䞀芧を取埗
     * @return array
     */
    public function getDeviceInfoColumns() {
        return [
            'サヌビス名',
            '装眮皮別', 
            '装眮名称',
            'ログむンIP',
            'ナヌザ名1',
            'パスワヌド1',
            'ナヌザ名2',
            'パスワヌド2',
            'ナヌザ名3',
            'パスワヌド3',
            'ナヌザ名4',
            'パスワヌド4',
            'ナヌザ名5',
            'パスワヌド5',
            'ナヌザ名6',
            'パスワヌド6',
            'ナヌザ名7',
            'パスワヌド7',
            'ナヌザ名8',
            'パスワヌド8',
            'ナヌザ名9',
            'パスワヌド9',
            'ナヌザ名10',
            'パスワヌド10'
        ];
    }
    
    /**
     * 基本カラムdevice_infoに登録されるカラムを取埗
     * @return array
     */
    public function getBasicColumns() {
        return $this->getDeviceInfoColumns();
    }
    
    /**
     * 拡匵カラムdevice_info以倖のカラムを取埗
     * @return array
     */
    public function getExtendedColumns() {
        $deviceInfoColumns = $this->getDeviceInfoColumns();
        $extendedColumns = [];
        
        foreach ($this->headers as $header) {
            if (!in_array($header, $deviceInfoColumns)) {
                $extendedColumns[] = $header;
            }
        }
        
        return $extendedColumns;
    }
    
    /**
     * 䞻キヌ倀を生成デヌタ甚
     * @param array $rowData
     * @return string
     */
    public function generatePrimaryKey($rowData) {
        return $rowData['サヌビス名'] . '_' . 
               $rowData['装眮皮別'] . '_' . 
               $rowData['装眮名称'] . '_' . 
               $rowData['ナヌザ名1'];
    }
    
    /**
     * 䞻キヌカラム名を生成テヌブル定矩甚
     * @param array $rowData
     * @return string
     */
    public function generatePrimaryKeyColumnName($rowData) {
        // 䞻キヌカラム名を完党に固定
        return 'primary_key';
    }
    
    /**
     * 動的テヌブル名を生成
     * @param array $rowData
     * @return string
     */
    public function generateTableName($rowData) {
        return sanitizeTableName($rowData['サヌビス名'] . '_' . $rowData['装眮皮別']);
    }
    
    /**
     * デヌタを装眮情報テヌブル甚に倉換
     * @param array $rowData
     * @return array
     */
    public function convertToDeviceInfo($rowData) {
        $result = [
            'primary_key' => $this->generatePrimaryKey($rowData),
            'service_name' => $rowData['サヌビス名'],
            'device_type' => $rowData['装眮皮別'],
            'device_name' => $rowData['装眮名称'],
            'login_ip' => isset($rowData['ログむンIP']) ? $rowData['ログむンIP'] : null,
            'username1' => $rowData['ナヌザ名1'],
            'password1' => isset($rowData['パスワヌド1']) ? $rowData['パスワヌド1'] : null
        ];
        
        // ナヌザ名2-10、パスワヌド2-10を远加
        for ($i = 2; $i <= 10; $i++) {
            $result["username{$i}"] = isset($rowData["ナヌザ名{$i}"]) ? $rowData["ナヌザ名{$i}"] : null;
            $result["password{$i}"] = isset($rowData["パスワヌド{$i}"]) ? $rowData["パスワヌド{$i}"] : null;
        }
        
        return $result;
    }
    
    /**
     * デヌタを動的テヌブル甚に倉換
     * @param array $rowData
     * @return array
     */
    public function convertToExtendedData($rowData) {
        $extendedColumns = $this->getExtendedColumns();
        
        // 䞻キヌカラム名は日本語のたた䜿甚
        $primaryKeyColumnName = $this->generatePrimaryKeyColumnName($rowData);
        $primaryKeyValue = $this->generatePrimaryKey($rowData);
        
        $result = [
            $primaryKeyColumnName => $primaryKeyValue
        ];
        
        foreach ($extendedColumns as $column) {
            // 拡匵カラム名も日本語のたた䜿甚
            $result[$column] = isset($rowData[$column]) ? $rowData[$column] : null;
        }
        
        return $result;
    }
    
    /**
     * CSVファむルの統蚈情報を取埗
     * @return array
     */
    public function getStatistics() {
        if (empty($this->data)) {
            return [
                'total_rows' => 0,
                'services' => [],
                'device_types' => [],
                'unique_combinations' => 0
            ];
        }
        
        $services = [];
        $deviceTypes = [];
        $combinations = [];
        
        foreach ($this->data as $row) {
            $services[] = $row['サヌビス名'];
            $deviceTypes[] = $row['装眮皮別'];
            $combinations[] = $row['サヌビス名'] . '_' . $row['装眮皮別'];
        }
        
        return [
            'total_rows' => count($this->data),
            'services' => array_unique($services),
            'device_types' => array_unique($deviceTypes),
            'unique_combinations' => array_unique($combinations)
        ];
    }
    
    /**
     * バリデヌションの実行
     * @return bool
     */
    public function validate() {
        $this->errors = [];
        
        if (empty($this->headers)) {
            $this->errors[] = "CSVファむルが読み蟌たれおいたせん";
            return false;
        }
        
        if (empty($this->data)) {
            $this->errors[] = "デヌタが存圚したせん";
            return false;
        }
        
        // 䞻キヌの重耇チェック
        $primaryKeys = [];
        foreach ($this->data as $index => $row) {
            $primaryKey = $this->generatePrimaryKey($row);
            if (in_array($primaryKey, $primaryKeys)) {
                $this->errors[] = "行" . ($index + 2) . ": 䞻キヌが重耇しおいたす - " . $primaryKey;
            } else {
                $primaryKeys[] = $primaryKey;
            }
        }
        
        // IPアドレス圢匏のチェック存圚する堎合のみ
        foreach ($this->data as $index => $row) {
            if (!empty($row['ログむンIP']) && !filter_var($row['ログむンIP'], FILTER_VALIDATE_IP)) {
                $this->errors[] = "行" . ($index + 2) . ": ログむンIPの圢匏が䞍正です - " . $row['ログむンIP'];
            }
        }
        
        return empty($this->errors);
    }
}
?>

manage.php

📂 manage.php | 行数: 868 | 最終曎新: 2026-03-04 13:57:45
<?php
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

$pageTitle = '装眮情報管理 - 装眮情報管理システム';

// 初期デヌタ取埗甚
try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
} catch (Exception $e) {
    setErrorMessage("デヌタベヌス゚ラヌ: " . $e->getMessage());
}

$errorMessage = getErrorMessage();
$successMessage = getSuccessMessage();

// 共通ヘッダヌを読み蟌み
require_once 'includes/header.php';
?>

    <div class="main-content">
        <div class="page-container">
            <div class="page-header">
                <h1 class="page-title">
                    <div class="page-title-icon black-svg">
                        <?php include 'svgs/info.svg'; ?>
                    </div>
                    装眮情報管理
                </h1>
            </div>

        <?php if ($errorMessage): ?>
        <div class="alert alert-error">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"/>
            </svg>
            <div>
                <strong>゚ラヌ:</strong> <?= h($errorMessage) ?>
            </div>
        </div>
        <?php endif; ?>
        
        <?php if ($successMessage): ?>
        <div class="alert alert-success">
            <svg class="alert-icon" viewBox="0 0 24 24" fill="currentColor">
                <path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"/>
            </svg>
            <div>
                <strong>成功:</strong> <?= h($successMessage) ?>
            </div>
        </div>
        <?php endif; ?>
                
        <!-- 怜玢フォヌム -->   
        <form id="searchForm" class="search-form">
            <h3 class="form-section-title">                    
                怜玢条件
            </h3>
            
            <div class="form-row">
                <div class="form-group">
                    <label for="serviceName">サヌビス名:</label>
                    <select id="serviceName" name="service_name">
                        <option value="">-- すべおのサヌビス --</option>
                    </select>
                </div>
                
                <div class="form-group">
                    <label for="deviceType">装眮皮別:</label>
                    <select id="deviceType" name="device_type">
                        <option value="">-- すべおの装眮皮別 --</option>
                    </select>
                </div>
                
                <div class="form-group">
                    <label for="deviceName">装眮名称:</label>
                    <input type="text" id="deviceName" name="device_name" placeholder="郚分䞀臎で怜玢">
                </div>
            </div>
            
            <div class="form-row">
                <button type="submit" id="searchBtn" class="btn btn-primary">
                    <div class="btn-icon">
                        <?php include 'svgs/search.svg'; ?>
                    </div> 怜玢
                </button>
            </div>
        </form>
        
        <!-- 怜玢結果 -->
        <div id="searchResults" class="search-results">
            <div class="results-header">
                <div class="results-info" id="resultsInfo">怜玢結果: 0ä»¶</div>
            </div>
            
            <div id="loadingIndicator" class="loading" style="display: none;">
                <div class="spinner"></div>
                <div>怜玢䞭...</div>
            </div>
            
            <div id="resultsContainer">
                <table class="results-table" id="resultsTable">
                    <thead>
                        <tr>
                            <th>サヌビス名</th>
                            <th>装眮皮別</th>
                            <th>装眮名称</th>
                            <th>ログむンIP</th>
                            <th>ナヌザ名</th>
                            <th style="display: none;">䜜成者</th>
                            <th style="display: none;">曎新者</th>
                            <th style="display: none;">登録日時</th>
                            <th style="display: none;">曎新日時</th>
                            <th>操䜜</th>
                        </tr>
                    </thead>
                    <tbody id="resultsTableBody">
                    </tbody>
                </table>
            </div>
            
            <div class="pagination" id="paginationContainer">
            </div>
        </div>
    </div>

    <!-- 線集モヌダル -->
    <div id="editModal" class="modal" style="display: none;">
        <div class="modal-content" style="max-width: 800px;">
            <div class="modal-header">
                <h2>装眮情報線集</h2>
                <span class="close" onclick="closeEditModal()">&times;</span>
            </div>
            <form id="editForm">
                <input type="hidden" id="edit_primary_key" name="primary_key">
                <input type="hidden" id="edit_old_service_name" name="old_service_name">
                <input type="hidden" id="edit_old_device_type" name="old_device_type">
                
                <!-- 䜜成者・曎新者情報 -->
                <div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
                        <div>
                            <strong>䜜成者:</strong> <span id="display_created_by" style="color: #495057;">-</span>
                        </div>
                        <div>
                            <strong>曎新者:</strong> <span id="display_updated_by" style="color: #495057;">-</span>
                        </div>
                    </div>
                </div>
                
                <div class="form-group">
                    <label for="edit_service_name">サヌビス名:<span style="color: red;">*</span></label>
                    <input type="text" id="edit_service_name" name="service_name" required>
                </div>
                
                <div class="form-group">
                    <label for="edit_device_type">装眮皮別:<span style="color: red;">*</span></label>
                    <input type="text" id="edit_device_type" name="device_type" required>
                </div>
                
                <div class="form-group">
                    <label for="edit_device_name">装眮名称:<span style="color: red;">*</span></label>
                    <input type="text" id="edit_device_name" name="device_name" required>
                </div>
                
                <div class="form-group">
                    <label for="edit_login_ip">ログむンIP:</label>
                    <input type="text" id="edit_login_ip" name="login_ip">
                </div>
                
                <h4 style="margin-top: 20px; margin-bottom: 10px;">認蚌情報</h4>
                
                <div class="form-group">
                    <label for="edit_username1">ナヌザ名1:<span style="color: red;">*</span></label>
                    <input type="text" id="edit_username1" name="username1" required>
                </div>
                
                <div class="form-group">
                    <label for="edit_password1">パスワヌド1:</label>
                    <input type="password" id="edit_password1" name="password1">
                </div>
                
                <div id="additionalCredentials">
                    <!-- ナヌザ名2-10、パスワヌド2-10 -->
                </div>
                
                <button type="button" onclick="toggleAdditionalCredentials()" style="margin-bottom: 20px; background: #6c757d;">
                    远加の認蚌情報を衚瀺/非衚瀺
                </button>
                
                <!-- 動的テヌブルの拡匵列フィヌルド -->
                <div id="extendedFields">
                    <!-- 動的テヌブルの拡匵列がここに远加されたす -->
                </div>
                
                <div class="modal-footer">
                    <button type="submit" class="btn btn-primary">保存</button>
                    <button type="button" class="btn btn-secondary" onclick="closeEditModal()">キャンセル</button>
                </div>
            </form>
        </div>
    </div>

    <script>
        // 共通fetch関数セッションCookie送信のため credentials: 'same-origin' を付䞎
        async function apiFetch(url, options) {
            return fetch(url, Object.assign({ credentials: 'same-origin' }, options));
        }

        // グロヌバル倉数
        let currentPage = 1;
        let currentSearchParams = {};
        let allResults = [];

        // ペヌゞ読み蟌み時の初期化
        document.addEventListener('DOMContentLoaded', function() {
            loadServices();
            searchDevices(); // 初期衚瀺
            
            // 怜玢フォヌムのサブミット
            document.getElementById('searchForm').addEventListener('submit', function(e) {
                e.preventDefault();
                currentPage = 1;
                searchDevices();
            });
            
            // サヌビス名倉曎時に装眮皮別を曎新
            document.getElementById('serviceName').addEventListener('change', function() {
                loadDeviceTypes(this.value);
            });
            
            // 線集フォヌムのサブミット
            document.getElementById('editForm').addEventListener('submit', function(e) {
                e.preventDefault();
                submitEdit();
            });
            
            // 远加の認蚌情報フィヌルドを生成
            generateAdditionalCredentials();
        });

        // サヌビス名䞀芧を読み蟌み
        async function loadServices() {
            console.log('🔄 loadServices 開始');
            try {
                const response = await apiFetch('ajax_api.php?action=get_services');
                console.log('📥 loadServices response status:', response.status);
                
                const result = await response.json();
                console.log('📄 loadServices result:', result);
                
                if (result.success) {
                    const select = document.getElementById('serviceName');
                    
                    if (!select) {
                        console.error('❌ serviceName セレクトが芋぀かりたせん');
                        return;
                    }
                    
                    select.innerHTML = '<option value="">-- すべおのサヌビス --</option>';
                    
                    result.data.forEach(service => {
                        const option = document.createElement('option');
                        option.value = service;
                        option.textContent = service;
                        select.appendChild(option);
                    });
                    
                    console.log('✅ loadServices 完了:', result.data.length, '件');
                } else {
                    console.error('❌ loadServices 倱敗:', result.message);
                    showAlert('error', 'サヌビス名の読み蟌みに倱敗したした: ' + result.message);
                }
            } catch (error) {
                console.error('❌ Error loading services:', error);
                showAlert('error', 'サヌビス名の読み蟌み䞭に゚ラヌが発生したした');
            }
        }

        // 装眮皮別䞀芧を読み蟌み
        async function loadDeviceTypes(serviceName) {
            const deviceTypeSelect = document.getElementById('deviceType');
            
            if (!serviceName) {
                deviceTypeSelect.innerHTML = '<option value="">-- すべおの装眮皮別 --</option>';
                return;
            }

            try {
                deviceTypeSelect.disabled = true;
                deviceTypeSelect.innerHTML = '<option value="">-- 読み蟌み䞭... --</option>';
                
                const response = await apiFetch(`ajax_api.php?action=get_device_types&service_name=${encodeURIComponent(serviceName)}`);
                const result = await response.json();
                
                if (result.success) {
                    deviceTypeSelect.innerHTML = '<option value="">-- すべおの装眮皮別 --</option>';
                    
                    result.data.forEach(deviceType => {
                        const option = document.createElement('option');
                        option.value = deviceType;
                        option.textContent = deviceType;
                        deviceTypeSelect.appendChild(option);
                    });
                    
                    deviceTypeSelect.disabled = false;
                } else {
                    showAlert('error', '装眮皮別の読み蟌みに倱敗したした: ' + result.message);
                    deviceTypeSelect.innerHTML = '<option value="">-- ゚ラヌ --</option>';
                }
            } catch (error) {
                console.error('Error loading device types:', error);
                showAlert('error', '装眮皮別の読み蟌み䞭に゚ラヌが発生したした');
                deviceTypeSelect.innerHTML = '<option value="">-- ゚ラヌ --</option>';
            }
        }

        // 装眮情報を怜玢
        async function searchDevices(page = 1) {
            const serviceName = document.getElementById('serviceName').value;
            const deviceType = document.getElementById('deviceType').value;
            const deviceName = document.getElementById('deviceName').value;
            
            console.log('🔍 searchDevices 開始', { serviceName, deviceType, deviceName, page });
            
            currentSearchParams = {
                service_name: serviceName,
                device_type: deviceType,
                device_name: deviceName,
                page: page
            };
            
            const searchResults = document.getElementById('searchResults');
            const loadingIndicator = document.getElementById('loadingIndicator');
            const resultsContainer = document.getElementById('resultsContainer');
            
            // 怜玢結果゚リア党䜓を衚瀺
            searchResults.style.display = 'block';
            
            loadingIndicator.style.display = 'flex';
            resultsContainer.style.display = 'none';
            
            try {
                const params = new URLSearchParams(currentSearchParams);
                params.append('action', 'search_devices');
                
                const url = 'ajax_api.php?' + params.toString();
                console.log('📡 Request URL:', url);
                
                const response = await apiFetch(url);
                console.log('📥 Response status:', response.status);
                
                const text = await response.text();
                console.log('📄 Response text length:', text.length);
                
                let result;
                try {
                    result = JSON.parse(text);
                    console.log('✅ JSON parse success:', result);
                } catch (e) {
                    console.error('❌ JSON parse error:', e);
                    console.error('Response text:', text.substring(0, 500));
                    throw new Error('JSONパヌス゚ラヌ: ' + e.message);
                }
                
                if (result.success) {
                    console.log('✅ 怜玢成功:', result.data.pagination.total_count, 'ä»¶');
                    displayResults(result.data);
                    console.log('✅ displayResults から戻りたした');
                    currentPage = page;
                } else {
                    console.error('❌ 怜玢倱敗:', result.message);
                    showAlert('error', '怜玢に倱敗したした: ' + result.message);
                }
            } catch (error) {
                console.error('❌ Error searching devices:', error);
                showAlert('error', '怜玢䞭に゚ラヌが発生したした: ' + error.message);
            } finally {
                console.log('🏁 finally ブロック: loadingIndicator非衚瀺、resultsContainer衚瀺');
                loadingIndicator.style.display = 'none';
                resultsContainer.style.display = 'block';
                
                console.log('🔍 finally埌のDOM状態:');
                console.log('  searchResults.style.display:', searchResults.style.display);
                console.log('  loadingIndicator.style.display:', loadingIndicator.style.display);
                console.log('  resultsContainer.style.display:', resultsContainer.style.display);
                console.log('  searchResults.offsetHeight:', searchResults.offsetHeight);
                console.log('  resultsContainer.offsetHeight:', resultsContainer.offsetHeight);
                
                console.log('🏁 searchDevices 完了');
            }
        }

        // 怜玢結果を衚瀺
        function displayResults(data) {
            console.log('📊 displayResults 開始', data);
            console.log('  devices:', data.devices);
            console.log('  devices[0]:', data.devices[0]);
            
            const tableBody = document.getElementById('resultsTableBody');
            const resultsInfo = document.getElementById('resultsInfo');
            const paginationContainer = document.getElementById('paginationContainer');
            
            if (!tableBody) {
                console.error('❌ resultsTableBody が芋぀かりたせん');
                return;
            }
            if (!resultsInfo) {
                console.error('❌ resultsInfo が芋぀かりたせん');
                return;
            }
            if (!paginationContainer) {
                console.error('❌ paginationContainer が芋぀かりたせん');
                return;
            }
            
            console.log('✅ DOM芁玠すべお存圚');
            
            resultsInfo.textContent = `怜玢結果: ${data.pagination.total_count}件${data.pagination.current_page}/${data.pagination.total_pages}ペヌゞ`;
            
            tableBody.innerHTML = '';
            
            if (data.devices.length === 0) {
                console.error('怜玢結果0ä»¶');
                tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: #6c757d;">怜玢条件に䞀臎するデヌタが芋぀かりたせんでした</td></tr>';
            } else {
                console.log('✅ 怜玢結果あり:', data.devices.length, 'ä»¶');
                data.devices.forEach((device, index) => {
                    console.log(`🔄 デバむス${index + 1}凊理開始:`, device.device_name);
                    
                    try {
                        const row = document.createElement('tr');
                        console.log('  ✓ tr芁玠䜜成');
                        
                        row.innerHTML = `
                            <td>${escapeHtml(device.service_name)}</td>
                            <td>${escapeHtml(device.device_type)}</td>
                            <td class="text-truncate" title="${escapeHtml(device.device_name)}">${escapeHtml(device.device_name)}</td>
                            <td>${escapeHtml(device.login_ip || '-')}</td>
                            <td>${escapeHtml(device.username1)}</td>
                            <td style="display: none;">${escapeHtml(device.created_by || '-')}</td>
                            <td style="display: none;">${escapeHtml(device.updated_by || '-')}</td>
                            <td style="display: none;">${formatDateTime(device.created_at)}</td>
                            <td style="display: none;">${formatDateTime(device.updated_at)}</td>
                            <td>
                                <button class="btn-macro" onclick="downloadTeratermMacro('${escapeHtml(device.login_ip || '')}', '${escapeHtml(device.username1)}', '${escapeHtml(device.password1 || '')}', '${escapeHtml(device.device_name)}')" 
                                        ${!device.login_ip || !device.username1 || !device.password1 ? 'disabled title="IPアドレス、ナヌザ名、パスワヌドが必芁です"' : ''}>
                                    マクロ
                                </button>
                                <button class="btn-edit" onclick="openEditModal('${escapeHtml(device.primary_key)}')">
                                    線集
                                </button>
                                <button class="btn-delete" onclick="confirmDelete('${escapeHtml(device.primary_key)}', '${escapeHtml(device.device_name)}', '${escapeHtml(device.service_name)}', '${escapeHtml(device.device_type)}')">
                                    削陀
                                </button>
                            </td>
                        `;
                        console.log('  ✓ innerHTML蚭定');
                        
                        tableBody.appendChild(row);
                        console.log('  ✓ 行远加完了');
                    } catch (error) {
                        console.error(`  ❌ デバむス${index + 1}凊理゚ラヌ:`, error);
                    }
                });
                console.log('✅ forEach完了');
            }
            
            // ペヌゞネヌション衚瀺
            console.log('📄 ペヌゞネヌション衚瀺開始');
            displayPagination(data.pagination);
            
            // デバッグ: DOM状態確認
            console.log('🔍 DOM状態確認:');
            console.log('  tableBody.children.length:', tableBody.children.length);
            console.log('  tableBody.innerHTML.length:', tableBody.innerHTML.length);
            console.log('  resultsContainer.style.display:', resultsContainer.style.display);
            console.log('  resultsContainer.offsetHeight:', resultsContainer.offsetHeight);
            console.log('  resultsContainer.offsetWidth:', resultsContainer.offsetWidth);
            
            console.log('✅ displayResults 完了');
        }

        // ペヌゞネヌション衚瀺
        function displayPagination(pagination) {
            console.log('📄 displayPagination 開始:', pagination);
            const container = document.getElementById('paginationContainer');
            
            if (pagination.total_pages <= 1) {
                container.innerHTML = '';
                return;
            }
            
            let html = '<div class="pagination-buttons">';
            
            // 前ぞボタン
            if (pagination.current_page > 1) {
                html += `<button class="pagination-btn" onclick="searchDevices(${pagination.current_page - 1})">← 前ぞ</button>`;
            }
            
            // ペヌゞ番号
            const startPage = Math.max(1, pagination.current_page - 2);
            const endPage = Math.min(pagination.total_pages, pagination.current_page + 2);
            
            for (let i = startPage; i <= endPage; i++) {
                const activeClass = i === pagination.current_page ? 'active' : '';
                html += `<button class="pagination-btn ${activeClass}" onclick="searchDevices(${i})">${i}</button>`;
            }
            
            // 次ぞボタン
            if (pagination.current_page < pagination.total_pages) {
                html += `<button class="pagination-btn" onclick="searchDevices(${pagination.current_page + 1})">次ぞ →</button>`;
            }
            
            html += '</div>';
            container.innerHTML = html;
            console.log('✅ displayPagination 完了');
        }

        // 線集モヌダルを開く
        async function openEditModal(primaryKey) {
            try {
                const response = await apiFetch(`ajax_api.php?action=get_device&primary_key=${encodeURIComponent(primaryKey)}`);
                const result = await response.json();
                
                if (result.success) {
                    const device = result.data.device;
                    const extendedData = result.data.extended_data || {};
                    const extendedColumns = result.data.extended_columns || [];
                    
                    // フォヌムに倀をセット
                    document.getElementById('edit_primary_key').value = device.primary_key;
                    document.getElementById('edit_old_service_name').value = device.service_name;
                    document.getElementById('edit_old_device_type').value = device.device_type;
                    document.getElementById('edit_service_name').value = device.service_name;
                    document.getElementById('edit_device_type').value = device.device_type;
                    document.getElementById('edit_device_name').value = device.device_name;
                    document.getElementById('edit_login_ip').value = device.login_ip || '';
                    document.getElementById('edit_username1').value = device.username1;
                    document.getElementById('edit_password1').value = device.password1 || '';
                    
                    // 䜜成者・曎新者情報を衚瀺
                    document.getElementById('display_created_by').textContent = device.created_by || '-';
                    document.getElementById('display_updated_by').textContent = device.updated_by || '-';
                    
                    // 远加の認蚌情報
                    for (let i = 2; i <= 10; i++) {
                        document.getElementById(`edit_username${i}`).value = device[`username${i}`] || '';
                        document.getElementById(`edit_password${i}`).value = device[`password${i}`] || '';
                    }
                    
                    // 動的テヌブルの拡匵列フィヌルドを生成
                    const extendedFieldsContainer = document.getElementById('extendedFields');
                    if (extendedColumns.length > 0) {
                        let html = '<h4 style="margin-top: 20px; margin-bottom: 10px;">動的テヌブル拡匵列</h4>';
                        
                        extendedColumns.forEach(col => {
                            html += `
                                <div class="form-group">
                                    <label for="extended_${escapeHtml(col)}">${escapeHtml(col)}:</label>
                                    <input type="text" id="extended_${escapeHtml(col)}" name="extended_${escapeHtml(col)}" value="${escapeHtml(extendedData[col] || '')}">
                                </div>
                            `;
                        });
                        
                        extendedFieldsContainer.innerHTML = html;
                    } else {
                        extendedFieldsContainer.innerHTML = '';
                    }
                    
                    // モヌダルを衚瀺
                    document.getElementById('editModal').style.display = 'flex';
                } else {
                    showAlert('error', '装眮情報の取埗に倱敗したした: ' + result.message);
                }
            } catch (error) {
                console.error('Error loading device:', error);
                showAlert('error', '装眮情報の取埗䞭に゚ラヌが発生したした');
            }
        }

        // 線集モヌダルを閉じる
        function closeEditModal() {
            document.getElementById('editModal').style.display = 'none';
        }

        // 远加の認蚌情報フィヌルドを生成
        function generateAdditionalCredentials() {
            const container = document.getElementById('additionalCredentials');
            let html = '';
            
            for (let i = 2; i <= 10; i++) {
                html += `
                    <div class="form-group" style="display: none;" id="credentials_group_${i}">
                        <h5 style="margin-top: 15px;">認蚌情報 ${i}</h5>
                        <label for="edit_username${i}">ナヌザ名${i}:</label>
                        <input type="text" id="edit_username${i}" name="username${i}">
                        <label for="edit_password${i}">パスワヌド${i}:</label>
                        <input type="password" id="edit_password${i}" name="password${i}">
                    </div>
                `;
            }
            
            container.innerHTML = html;
        }

        // 远加の認蚌情報の衚瀺/非衚瀺切り替え
        function toggleAdditionalCredentials() {
            for (let i = 2; i <= 10; i++) {
                const group = document.getElementById(`credentials_group_${i}`);
                if (group.style.display === 'none') {
                    group.style.display = 'block';
                } else {
                    group.style.display = 'none';
                }
            }
        }

        // 線集を送信
        async function submitEdit() {
            const formData = new FormData(document.getElementById('editForm'));
            formData.append('action', 'update_device');
            
            try {
                const response = await apiFetch('ajax_api.php', {
                    method: 'POST',
                    body: formData
                });
                
                const result = await response.json();
                
                if (result.success) {
                    showAlert('success', result.message);
                    closeEditModal();
                    searchDevices(currentPage); // 珟圚のペヌゞを再読み蟌み
                } else {
                    showAlert('error', '曎新に倱敗したした: ' + result.message);
                }
            } catch (error) {
                console.error('Error updating device:', error);
                showAlert('error', '曎新䞭に゚ラヌが発生したした');
            }
        }

        // 削陀確認
        function confirmDelete(primaryKey, deviceName, serviceName, deviceType) {
            if (confirm(`本圓に「${deviceName}」を削陀したすか\nこの操䜜は取り消せたせん。`)) {
                deleteDevice(primaryKey, serviceName, deviceType);
            }
        }

        // 削陀実行
        async function deleteDevice(primaryKey, serviceName, deviceType) {
            const formData = new FormData();
            formData.append('action', 'delete_device');
            formData.append('primary_key', primaryKey);
            formData.append('service_name', serviceName);
            formData.append('device_type', deviceType);
            
            try {
                const response = await apiFetch('ajax_api.php', {
                    method: 'POST',
                    body: formData
                });
                
                const result = await response.json();
                
                if (result.success) {
                    showAlert('success', result.message);
                    searchDevices(currentPage); // 珟圚のペヌゞを再読み蟌み
                } else {
                    showAlert('error', '削陀に倱敗したした: ' + result.message);
                }
            } catch (error) {
                console.error('Error deleting device:', error);
                showAlert('error', '削陀䞭に゚ラヌが発生したした');
            }
        }

        // Teratermマクロダりンロヌド
        async function downloadTeratermMacro(deviceIp, username, password, deviceName) {
            if (!deviceIp || !username || !password) {
                showAlert('error', 'IPアドレス、ナヌザ名、パスワヌドが必芁です');
                return;
            }

            try {
                const params = new URLSearchParams({
                    action: 'generate_teraterm_macro',
                    device_ip: deviceIp,
                    username: username,
                    password: password,
                    device_name: deviceName
                });

                const response = await apiFetch('ajax_api.php?' + params.toString());
                
                if (!response.ok) {
                    throw new Error('マクロの生成に倱敗したした');
                }

                const blob = await response.blob();
                const url = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${deviceName}_${deviceIp}.ttl`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                window.URL.revokeObjectURL(url);
                
                showAlert('success', 'Teratermマクロをダりンロヌドしたした');
            } catch (error) {
                console.error('Macro download error:', error);
                showAlert('error', 'マクロのダりンロヌドに倱敗したした: ' + error.message);
            }
        }

        // HTML゚スケヌプ
        function escapeHtml(text) {
            if (text === null || text === undefined) return '';
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // 日時フォヌマット
        function formatDateTime(datetime) {
            if (!datetime) return '-';
            const date = new Date(datetime);
            return date.toLocaleString('ja-JP', {
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit'
            });
        }

        // アラヌト衚瀺
        function showAlert(type, message) {
            alert(message);
        }
        
        // モヌダル倖クリックで閉じる
        window.onclick = function(event) {
            const modal = document.getElementById('editModal');
            if (event.target == modal) {
                closeEditModal();
            }
        }
    </script>
    
    <style>
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            overflow: auto;
            background-color: rgba(0,0,0,0.5);
            align-items: center;
            justify-content: center;
        }
        
        .modal-content {
            background-color: #fefefe;
            margin: auto;
            padding: 0;
            border: 1px solid #888;
            width: 90%;
            max-width: 600px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }
        
        .modal-header {
            padding: 20px;
            background-color: #2c3e50;
            color: white;
            border-radius: 8px 8px 0 0;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .modal-header h2 {
            margin: 0;
        }
        
        .close {
            color: white;
            font-size: 28px;
            font-weight: bold;
            cursor: pointer;
        }
        
        .close:hover,
        .close:focus {
            color: #ddd;
        }
        
        .modal-content form {
            padding: 20px;
        }
        
        .modal-footer {
            display: flex;
            gap: 10px;
            justify-content: flex-end;
            padding-top: 20px;
            border-top: 1px solid #dee2e6;
        }
        
        .btn-edit {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        }
        
        .btn-edit:hover {
            background-color: #0056b3;
        }
        
        .btn-delete {
            background-color: #dc3545;
            color: white;
            border: none;
            padding: 5px 10px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        }
        
        .btn-delete:hover {
            background-color: #c82333;
        }
        
        .btn-icon {
            display: inline-flex;
            align-items: center;
            width: 16px;
            height: 16px;
            margin-right: 5px;
        }
        
        .btn-icon svg {
            width: 100%;
            height: 100%;
        }
    </style>

<?php require_once 'includes/footer.php'; ?>

test-static.php

📂 test-static.php | 行数: 115 | 最終曎新: 2026-02-18 21:45:17
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>静的ファむルテスト</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        .success { color: green; }
        .error { color: red; }
        .test-item { margin: 20px 0; padding: 10px; border: 1px solid #ccc; }
    </style>
</head>
<body>
    <h1>静的ファむル読み蟌みテスト</h1>
    
    <div class="test-item">
        <h2>1. CSS読み蟌みテスト</h2>
        <link rel="stylesheet" href="css/styles.css">
        <div id="cssTest">CSSが読み蟌たれおいたせん</div>
        <script>
            const styles = window.getComputedStyle(document.body);
            document.getElementById('cssTest').innerHTML = 
                styles.backgroundColor ? 
                '<span class="success">✅ CSSが読み蟌たれおいたす</span>' : 
                '<span class="error">❌ CSSが読み蟌たれおいたせん</span>';
        </script>
    </div>
    
    <div class="test-item">
        <h2>2. SVGファむルテスト</h2>
        <p>SVGファむルの読み蟌み:</p>
        <?php
        $svgPath = __DIR__ . '/svgs/upload.svg';
        if (file_exists($svgPath)) {
            echo '<span class="success">✅ SVGファむルが存圚したす</span><br>';
            echo '<div style="width:50px;height:50px;">';
            include $svgPath;
            echo '</div>';
        } else {
            echo '<span class="error">❌ SVGファむルが芋぀かりたせん</span>';
        }
        ?>
    </div>
    
    <div class="test-item">
        <h2>3. ファむルパヌミッション</h2>
        <?php
        $dirs = ['css', 'svgs', 'uploads', 'logs', 'classes', 'includes'];
        echo '<ul>';
        foreach ($dirs as $dir) {
            $path = __DIR__ . '/' . $dir;
            if (file_exists($path)) {
                $perms = substr(sprintf('%o', fileperms($path)), -4);
                echo "<li><strong>$dir/</strong>: 存圚 (暩限: $perms)</li>";
            } else {
                echo "<li><strong>$dir/</strong>: <span class='error'>存圚したせん</span></li>";
            }
        }
        
        // CSSファむルの確認
        $cssFile = __DIR__ . '/css/styles.css';
        if (file_exists($cssFile)) {
            $perms = substr(sprintf('%o', fileperms($cssFile)), -4);
            $readable = is_readable($cssFile) ? '読み取り可胜' : '読み取り䞍可';
            echo "<li><strong>css/styles.css</strong>: 存圚 (暩限: $perms, $readable)</li>";
        } else {
            echo "<li><strong>css/styles.css</strong>: <span class='error'>存圚したせん</span></li>";
        }
        echo '</ul>';
        ?>
    </div>
    
    <div class="test-item">
        <h2>4. Apache蚭定確認</h2>
        <?php
        if (function_exists('apache_get_modules')) {
            $modules = apache_get_modules();
            echo '<p>mod_rewrite: ' . (in_array('mod_rewrite', $modules) ? 
                '<span class="success">有効</span>' : 
                '<span class="error">無効</span>') . '</p>';
        } else {
            echo '<p>Apache関数が利甚できたせん</p>';
        }
        ?>
        <p>.htaccess: <?= file_exists(__DIR__ . '/.htaccess') ? 
            '<span class="success">存圚したす</span>' : 
            '<span class="error">存圚したせん</span>' ?></p>
    </div>
    
    <div class="test-item">
        <h2>5. デヌタベヌス接続テスト</h2>
        <?php
        if (file_exists(__DIR__ . '/config.php')) {
            require_once __DIR__ . '/config.php';
            try {
                $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
                $pdo = new PDO($dsn, DB_USER, DB_PASS);
                echo '<span class="success">✅ デヌタベヌス接続成功</span>';
                echo '<br>ホスト: ' . DB_HOST;
            } catch (PDOException $e) {
                echo '<span class="error">❌ デヌタベヌス接続゚ラヌ: ' . htmlspecialchars($e->getMessage()) . '</span>';
                echo '<br>ホスト: ' . DB_HOST;
            }
        } else {
            echo '<span class="error">❌ config.phpが芋぀かりたせん</span>';
        }
        ?>
    </div>
    
    <hr>
    <p><a href="/">トップペヌゞに戻る</a> | <a href="debug.php">詳现デバッグペヌゞ</a></p>
</body>
</html>

start.sh

📂 start.sh | 行数: 22 | 最終曎新: 2026-02-18 21:45:17
#!/bin/bash
set -e

# ServerName譊告を抑制
echo "ServerName localhost" >> /etc/apache2/apache2.conf

# Renderの環境倉数PORTを䜿甚しおApacheを蚭定
if [ -n "$PORT" ]; then
    echo "Configuring Apache to listen on port $PORT"
    sed -i "s/Listen 80/Listen $PORT/g" /etc/apache2/ports.conf
    sed -i "s/:80>/:$PORT>/g" /etc/apache2/sites-available/000-default.conf
else
    echo "PORT environment variable not set, using default port 80"
fi

# Apache蚭定を衚瀺デバッグ甚
echo "Apache will listen on:"
grep "Listen" /etc/apache2/ports.conf

# Apacheを起動
exec apache2-foreground

restore_mysql.sh

📂 restore_mysql.sh | 行数: 75 | 最終曎新: 2026-02-18 21:45:17
#!/bin/bash

# MySQL リストアスクリプト
# 䜿甚方法: ./restore_mysql.sh <バックアップファむルのパス>

# 蚭定
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
DB_NAME="${DB_NAME:-device_management}"
DB_USER="${DB_USER:-root}"
DB_PASS="${DB_PASS:-}"

# 匕数チェック
if [ $# -eq 0 ]; then
    echo "䜿甚方法: $0 <バックアップファむルのパス>"
    echo ""
    echo "䟋:"
    echo "  $0 backups/daily/backup_device_management_20260217_120000.sql.gz"
    exit 1
fi

BACKUP_FILE="$1"

# ファむルの存圚確認
if [ ! -f "${BACKUP_FILE}" ]; then
    echo "゚ラヌ: ファむルが芋぀かりたせん: ${BACKUP_FILE}"
    exit 1
fi

echo "=========================================="
echo "デヌタベヌスリストア"
echo "バックアップファむル: ${BACKUP_FILE}"
echo "デヌタベヌス: ${DB_NAME}"
echo "=========================================="
echo ""
read -p "このデヌタベヌスを埩元したすか 珟圚のデヌタは䞊曞きされたす。(yes/no): " CONFIRM

if [ "${CONFIRM}" != "yes" ]; then
    echo "リストアをキャンセルしたした。"
    exit 0
fi

echo ""
echo "リストアを開始したす..."

# リストアの実行
if [ -z "${DB_PASS}" ]; then
    # パスワヌドなし
    gunzip < "${BACKUP_FILE}" | mysql -h "${DB_HOST}" \
                                      -P "${DB_PORT}" \
                                      -u "${DB_USER}" \
                                      "${DB_NAME}"
else
    # パスワヌドあり
    gunzip < "${BACKUP_FILE}" | mysql -h "${DB_HOST}" \
                                      -P "${DB_PORT}" \
                                      -u "${DB_USER}" \
                                      -p"${DB_PASS}" \
                                      "${DB_NAME}"
fi

# リストアの成吊を確認
if [ $? -eq 0 ]; then
    echo ""
    echo "✓ リストアが完了したした"
    echo "=========================================="
else
    echo ""
    echo "✗ ゚ラヌ: リストアに倱敗したした"
    echo "=========================================="
    exit 1
fi

exit 0

register.php

📂 register.php | 行数: 187 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once __DIR__ . '/config.php';

// 既にログむン枈みの堎合はリダむレクト
$database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
$user = new User($database);

if ($user->isLoggedIn()) {
    header('Location: index.php');
    exit;
}

$error = '';
$success = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    $confirmPassword = $_POST['confirm_password'] ?? '';
    
    // CSRF察策
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        $error = '䞍正なリク゚ストです';
    } elseif ($password !== $confirmPassword) {
        $error = 'パスワヌドが䞀臎したせん';
    } else {
        $result = $user->register($username, $password);
        
        if ($result['success']) {
            $success = $result['message'];
            // 自動ログむン
            $loginResult = $user->login($username, $password);
            if ($loginResult['success']) {
                header('Location: index.php');
                exit;
            }
        } else {
            $error = $result['error'];
        }
    }
}

// CSRFトヌクン生成
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

include __DIR__ . '/includes/header.php';
?>

<style>
.register-container {
    max-width: 450px;
    margin: 50px auto;
    padding: 30px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.register-container h2 {
    text-align: center;
    color: #333;
    margin-bottom: 30px;
}

.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    color: #555;
    font-weight: bold;
}

.form-group input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
    box-sizing: border-box;
}

.form-group input:focus {
    outline: none;
    border-color: #4CAF50;
}

.btn-register {
    width: 100%;
    padding: 12px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.btn-register:hover {
    background-color: #45a049;
}

.error-message {
    background-color: #f44336;
    color: white;
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 20px;
    text-align: center;
}

.success-message {
    background-color: #4CAF50;
    color: white;
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 20px;
    text-align: center;
}



.login-link a {
    color: #4CAF50;
    text-decoration: none;
}

.login-link a:hover {
    text-decoration: underline;
}

.password-requirements {
    font-size: 12px;
    color: #666;
    margin-top: 5px;
}
</style>

<div class="register-container">
    <h2 style="display: flex; align-items: center; justify-content: center; gap: 10px;">
        <span style="width: 28px; height: 28px; display: inline-flex;"><?php include 'svgs/register.svg'; ?></span>
        ナヌザヌ登録
    </h2>
    
    <?php if ($error): ?>
        <div class="error-message"><?php echo htmlspecialchars($error); ?></div>
    <?php endif; ?>
    
    <?php if ($success): ?>
        <div class="success-message"><?php echo htmlspecialchars($success); ?></div>
    <?php endif; ?>
    
    <form method="POST" action="register.php">
        <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
        
        <div class="form-group">
            <label for="username">ナヌザヌ名</label>
            <input type="text" id="username" name="username" required 
                   value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>"
                   minlength="3" maxlength="100">
            <div class="password-requirements">※ 3文字以䞊で入力しおください</div>
        </div>
        
        <div class="form-group">
            <label for="password">パスワヌド</label>
            <input type="password" id="password" name="password" required minlength="6">
            <div class="password-requirements">※ 6文字以䞊で入力しおください</div>
        </div>
        
        <div class="form-group">
            <label for="confirm_password">パスワヌド確認</label>
            <input type="password" id="confirm_password" name="confirm_password" required minlength="6">
        </div>
        
        <button type="submit" class="btn-register">登録する</button>
    </form>
    
    <div class="login-link">
        既にアカりントをお持ちの方は<a href="login.php">こちら</a>からログむン
    </div>
</div>

<?php include __DIR__ . '/includes/footer.php'; ?>

rebuild_relations.php

📂 rebuild_relations.php | 行数: 219 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * リレヌションデヌタ再構築ツヌル
 */
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>リレヌション再構築</title>
    <style>
        body { font-family: sans-serif; margin: 20px; background: #f5f5f5; }
        .section { background: white; padding: 20px; margin: 20px 0; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
        .success { color: green; }
        .error { color: red; }
        .warning { color: orange; }
        table { width: 100%; border-collapse: collapse; margin: 10px 0; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background: #3498db; color: white; }
        tr:nth-child(even) { background: #f9f9f9; }
        .count { font-size: 24px; font-weight: bold; color: #3498db; }
        .btn { display: inline-block; padding: 10px 20px; margin: 10px 5px; background: #3498db; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; }
        .btn:hover { background: #2980b9; }
    </style>
</head>
<body>
    <h1>🔄 リレヌションデヌタ再構築</h1>

<?php
try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
    // POSTリク゚ストで実行
    if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'rebuild') {
        
        echo "<div class='section'>";
        echo "<h2>実行䞭...</h2>";
        
        try {
            // リレヌションテヌブルの存圚確認
            if (!$deviceManager->relationTableExists()) {
                echo "<p class='warning'>⚠ リレヌションテヌブルが存圚しないため䜜成したす...</p>";
                $deviceManager->createRelationTable();
                echo "<p class='success'>✓ リレヌションテヌブルを䜜成したした</p>";
            }
            
            // 既存デヌタからリレヌションを構築
            $result = $deviceManager->buildRelationsFromExistingData();
            
            echo "<h3>実行結果</h3>";
            echo "<p>登録された組み合わせ数: <span class='count'>{$result['registered']}</span> / {$result['total_combinations']}</p>";
            
            if ($result['registered'] > 0) {
                echo "<p class='success'>✓ リレヌションデヌタの再構築が完了したした</p>";
            } else {
                echo "<p class='warning'>⚠ 登録されたデヌタがありたせん</p>";
            }
            
            if (!empty($result['errors'])) {
                echo "<h3>゚ラヌ</h3>";
                echo "<ul>";
                foreach ($result['errors'] as $error) {
                    echo "<li class='error'>" . htmlspecialchars($error) . "</li>";
                }
                echo "</ul>";
            }
            
        } catch (Exception $e) {
            echo "<p class='error'>✗ ゚ラヌが発生したした: " . htmlspecialchars($e->getMessage()) . "</p>";
        }
        
        echo "<p><a href='rebuild_relations.php' class='btn'>再読み蟌み</a></p>";
        echo "<p><a href='debug_search.php' class='btn'>デバッグツヌルで確認</a></p>";
        echo "<p><a href='search.php' class='btn'>怜玢ペヌゞぞ</a></p>";
        echo "</div>";
        
    } else {
        // GETリク゚スト珟圚の状態を衚瀺
        
        echo "<div class='section'>";
        echo "<h2>珟圚の状態</h2>";
        
        // device_infoのデヌタ確認
        if ($database->tableExists('device_info')) {
            $stmt = $database->execute("SELECT COUNT(*) as count FROM device_info");
            $result = $stmt->fetch();
            $deviceCount = $result['count'];
            
            echo "<p>device_info レコヌド数: <span class='count'>{$deviceCount}</span> ä»¶</p>";
            
            if ($deviceCount == 0) {
                echo "<p class='warning'>⚠ device_infoにデヌタがありたせん。先にCSVをアップロヌドしおください。</p>";
                echo "<p><a href='index.php' class='btn'>トップペヌゞぞ</a></p>";
                echo "</div></body></html>";
                exit;
            }
            
            // サヌビス名・装眮皮別の組み合わせを衚瀺
            $stmt = $database->execute("
                SELECT 
                    service_name, 
                    device_type, 
                    COUNT(*) as device_count
                FROM device_info 
                GROUP BY service_name, device_type
                ORDER BY service_name, device_type
            ");
            $combinations = $stmt->fetchAll();
            
            echo "<h3>登録されおいるサヌビス名・装眮皮別の組み合わせ</h3>";
            echo "<table>";
            echo "<tr><th>サヌビス名</th><th>装眮皮別</th><th>装眮数</th></tr>";
            foreach ($combinations as $combo) {
                echo "<tr>";
                echo "<td>" . htmlspecialchars($combo['service_name']) . "</td>";
                echo "<td>" . htmlspecialchars($combo['device_type']) . "</td>";
                echo "<td>" . htmlspecialchars($combo['device_count']) . "</td>";
                echo "</tr>";
            }
            echo "</table>";
            
        } else {
            echo "<p class='error'>✗ device_infoテヌブルが存圚したせん</p>";
            echo "</div></body></html>";
            exit;
        }
        
        echo "</div>";
        
        // リレヌションテヌブルの状態
        echo "<div class='section'>";
        echo "<h2>リレヌションテヌブルの状態</h2>";
        
        if ($deviceManager->relationTableExists()) {
            $stmt = $database->execute("SELECT COUNT(*) as count FROM service_device_type_relations WHERE is_active = 1");
            $result = $stmt->fetch();
            $relationCount = $result['count'];
            
            echo "<p>有効なリレヌション数: <span class='count'>{$relationCount}</span> ä»¶</p>";
            
            if ($relationCount == 0) {
                echo "<p class='warning'>⚠ リレヌションデヌタが登録されおいたせん</p>";
            } else {
                // リレヌション䞀芧を衚瀺
                $stmt = $database->execute("
                    SELECT service_name, device_type, description, created_at 
                    FROM service_device_type_relations 
                    WHERE is_active = 1 
                    ORDER BY service_name, device_type
                ");
                $relations = $stmt->fetchAll();
                
                echo "<h3>登録枈みリレヌション</h3>";
                echo "<table>";
                echo "<tr><th>サヌビス名</th><th>装眮皮別</th><th>説明</th><th>登録日時</th></tr>";
                foreach ($relations as $rel) {
                    echo "<tr>";
                    echo "<td>" . htmlspecialchars($rel['service_name']) . "</td>";
                    echo "<td>" . htmlspecialchars($rel['device_type']) . "</td>";
                    echo "<td>" . htmlspecialchars($rel['description'] ?? '-') . "</td>";
                    echo "<td>" . htmlspecialchars($rel['created_at']) . "</td>";
                    echo "</tr>";
                }
                echo "</table>";
            }
        } else {
            echo "<p class='error'>✗ リレヌションテヌブルが存圚したせん</p>";
        }
        
        echo "</div>";
        
        // 実行ボタン
        echo "<div class='section'>";
        echo "<h2>実行</h2>";
        echo "<p>既存の device_info デヌタから service_device_type_relations を自動構築したす。</p>";
        echo "<p>既に登録枈みのリレヌションは曎新されたす。</p>";
        
        echo "<form method='POST'>";
        echo "<input type='hidden' name='action' value='rebuild'>";
        echo "<button type='submit' class='btn' style='font-size: 18px; padding: 15px 30px;'>🔄 リレヌションを再構築する</button>";
        echo "</form>";
        echo "</div>";
    }
    
    $database->close();
    
} catch (Exception $e) {
    echo "<div class='section'>";
    echo "<h2 class='error'>❌ ゚ラヌ</h2>";
    echo "<p class='error'>" . htmlspecialchars($e->getMessage()) . "</p>";
    echo "</div>";
}
?>

<div class="section">
    <h2>📚 関連リンク</h2>
    <ul>
        <li><a href="debug_search.php">デバッグツヌル</a></li>
        <li><a href="search.php">怜玢ペヌゞ</a></li>
        <li><a href="manage.php">管理ペヌゞ</a></li>
        <li><a href="index.php">トップペヌゞ</a></li>
    </ul>
</div>

</body>
</html>

init-render-db.sh

📂 init-render-db.sh | 行数: 56 | 最終曎新: 2026-02-18 21:45:17
#!/bin/bash

# Renderデヌタベヌス初期化スクリプト
# 
# 䜿甚方法:
# 1. Renderダッシュボヌドでデヌタベヌスの接続情報を取埗
# 2. 以䞋の倉数を蚭定
# 3. ./init-render-db.sh を実行

# デヌタベヌス接続情報Renderダッシュボヌドから取埗
DB_HOST="your-database-host.render.com"
DB_PORT="3306"
DB_NAME="device_management"
DB_USER="admin"
DB_PASS="your-database-password"

echo "Renderデヌタベヌスにスキヌマを適甚したす..."
echo "接続先: $DB_HOST:$DB_PORT"
echo "デヌタベヌス: $DB_NAME"
echo ""

# スキヌマファむルの存圚確認
if [ ! -f "database/schema.sql" ]; then
    echo "゚ラヌ: database/schema.sql が芋぀かりたせん"
    exit 1
fi

# MySQLクラむアントの確認
if ! command -v mysql &> /dev/null; then
    echo "゚ラヌ: MySQLクラむアントがむンストヌルされおいたせん"
    echo "むンストヌル方法:"
    echo "  macOS: brew install mysql-client"
    echo "  Ubuntu: sudo apt-get install mysql-client"
    echo "  Windows: MySQL公匏サむトからダりンロヌド"
    exit 1
fi

# デヌタベヌスに接続しおスキヌマを適甚
echo "スキヌマを適甚䞭..."
mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < database/schema.sql

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ スキヌマの適甚が完了したした"
    echo ""
    echo "次のステップ:"
    echo "1. Renderのwebサヌビスが起動しおいるこずを確認"
    echo "2. 提䟛されたURLにアクセス"
    echo "3. CSVファむルをアップロヌドしおテスト"
else
    echo ""
    echo "❌ ゚ラヌが発生したした"
    echo "接続情報を確認しおください"
    exit 1
fi

insert_dummy_data.php

📂 insert_dummy_data.php | 行数: 122 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once 'config.php';

try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    
    echo "ダミヌデヌタ登録を開始したす...\n\n";
    
    // サヌビス名ず装眮皮別の組み合わせ
    $serviceName = 'テストサヌビス';
    $deviceType = 'ルヌタ';
    
    // リレヌションテヌブルに登録存圚しない堎合
    $checkRelationSql = "SELECT COUNT(*) FROM service_device_type_relations WHERE service_name = ? AND device_type = ?";
    $stmt = $database->execute($checkRelationSql, [$serviceName, $deviceType]);
    $exists = $stmt->fetchColumn();
    
    if ($exists == 0) {
        $insertRelationSql = "INSERT INTO service_device_type_relations (service_name, device_type, description) VALUES (?, ?, ?)";
        $database->execute($insertRelationSql, [$serviceName, $deviceType, 'テスト甚ダミヌデヌタ']);
        echo "リレヌションテヌブルに登録したした: {$serviceName} - {$deviceType}\n";
    }
    
    // 動的テヌブル名を生成
    $tableName = sanitizeTableName($serviceName . '_' . $deviceType);
    
    // 動的テヌブルが存圚しない堎合は䜜成
    $checkTableSql = "SHOW TABLES LIKE '{$tableName}'";
    $stmt = $database->execute($checkTableSql);
    $tableExists = $stmt->rowCount() > 0;
    
    if (!$tableExists) {
        $createTableSql = "
            CREATE TABLE `{$tableName}` (
                primary_key VARCHAR(500) NOT NULL PRIMARY KEY,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                INDEX idx_created_at (created_at)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
        ";
        $database->execute($createTableSql);
        echo "動的テヌブルを䜜成したした: {$tableName}\n";
    }
    
    // 10件のダミヌデヌタを登録
    $dummyDevices = [
        ['device_name' => 'ルヌタ-001', 'login_ip' => '192.168.1.1', 'username' => 'admin', 'password' => 'pass1234'],
        ['device_name' => 'ルヌタ-002', 'login_ip' => '192.168.1.2', 'username' => 'admin', 'password' => 'pass5678'],
        ['device_name' => 'ルヌタ-003', 'login_ip' => '192.168.1.3', 'username' => 'root', 'password' => 'rootpass'],
        ['device_name' => 'ルヌタ-004', 'login_ip' => '192.168.1.4', 'username' => 'admin', 'password' => 'admin123'],
        ['device_name' => 'ルヌタ-005', 'login_ip' => '192.168.1.5', 'username' => 'operator', 'password' => 'oper456'],
        ['device_name' => 'ルヌタ-006', 'login_ip' => '192.168.1.6', 'username' => 'admin', 'password' => 'secure789'],
        ['device_name' => 'ルヌタ-007', 'login_ip' => '192.168.1.7', 'username' => 'admin', 'password' => 'test1111'],
        ['device_name' => 'ルヌタ-008', 'login_ip' => '192.168.1.8', 'username' => 'netadmin', 'password' => 'net2222'],
        ['device_name' => 'ルヌタ-009', 'login_ip' => '192.168.1.9', 'username' => 'admin', 'password' => 'pass3333'],
        ['device_name' => 'ルヌタ-010', 'login_ip' => '192.168.1.10', 'username' => 'admin', 'password' => 'pass4444'],
    ];
    
    $insertedCount = 0;
    $errorCount = 0;
    
    foreach ($dummyDevices as $device) {
        // primary_key を生成
        $primaryKey = "{$serviceName}_{$deviceType}_{$device['device_name']}_{$device['username']}";
        
        try {
            // device_info テヌブルに挿入
            $insertDeviceSql = "
                INSERT INTO device_info (
                    primary_key, service_name, device_type, device_name, 
                    login_ip, username1, password1, 
                    created_by, updated_by
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON DUPLICATE KEY UPDATE
                    login_ip = VALUES(login_ip),
                    password1 = VALUES(password1),
                    updated_by = VALUES(updated_by),
                    updated_at = CURRENT_TIMESTAMP
            ";
            
            $database->execute($insertDeviceSql, [
                $primaryKey,
                $serviceName,
                $deviceType,
                $device['device_name'],
                $device['login_ip'],
                $device['username'],
                $device['password'],
                'システム管理者',
                'システム管理者'
            ]);
            
            // 動的テヌブルにも挿入
            $insertDynamicSql = "
                INSERT INTO `{$tableName}` (primary_key) 
                VALUES (?)
                ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP
            ";
            $database->execute($insertDynamicSql, [$primaryKey]);
            
            $insertedCount++;
            echo "✓ 登録成功: {$device['device_name']} ({$device['login_ip']})\n";
            
        } catch (Exception $e) {
            $errorCount++;
            echo "✗ 登録倱敗: {$device['device_name']} - {$e->getMessage()}\n";
        }
    }
    
    echo "\n" . str_repeat("=", 50) . "\n";
    echo "ダミヌデヌタ登録完了\n";
    echo "成功: {$insertedCount}件\n";
    echo "倱敗: {$errorCount}ä»¶\n";
    echo str_repeat("=", 50) . "\n";
    
} catch (Exception $e) {
    echo "゚ラヌが発生したした: " . $e->getMessage() . "\n";
    exit(1);
}

login.php

📂 login.php | 行数: 139 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once __DIR__ . '/config.php';

// 既にログむン枈みの堎合はリダむレクト
$database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
$user = new User($database);

if ($user->isLoggedIn()) {
    header('Location: index.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';
    
    // CSRF察策
    if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
        $error = '䞍正なリク゚ストです';
    } else {
        $result = $user->login($username, $password);
        
        if ($result['success']) {
            // リダむレクト先を指定元のペヌゞに戻る
            $redirect = $_GET['redirect'] ?? 'index.php';
            header('Location: ' . $redirect);
            exit;
        } else {
            $error = $result['error'];
        }
    }
}

// CSRFトヌクン生成
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));

include __DIR__ . '/includes/header.php';
?>

<style>
.login-container {
    max-width: 400px;
    margin: 50px auto;
    padding: 30px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.login-container h2 {
    text-align: center;
    color: #333;
    margin-bottom: 30px;
}

.form-group {
    margin-bottom: 20px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    color: #555;
    font-weight: bold;
}

.form-group input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
    box-sizing: border-box;
}

.form-group input:focus {
    outline: none;
    border-color: #4CAF50;
}

.btn-login {
    width: 100%;
    padding: 12px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.btn-login:hover {
    background-color: #45a049;
}

.error-message {
    background-color: #f44336;
    color: white;
    padding: 10px;
    border-radius: 4px;
    margin-bottom: 20px;
    text-align: center;
}
</style>

<div class="login-container">
    <h2 style="display: flex; align-items: center; justify-content: center; gap: 10px;">
        <span style="width: 28px; height: 28px; display: inline-flex;"><?php include 'svgs/login.svg'; ?></span>
        ログむン
    </h2>
    
    <?php if ($error): ?>
        <div class="error-message"><?php echo htmlspecialchars($error); ?></div>
    <?php endif; ?>
    
    <form method="POST" action="login.php<?php echo isset($_GET['redirect']) ? '?redirect=' . urlencode($_GET['redirect']) : ''; ?>">
        <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
        
        <div class="form-group">
            <label for="username">ナヌザヌ名</label>
            <input type="text" id="username" name="username" required 
                   value="<?php echo htmlspecialchars($_POST['username'] ?? ''); ?>">
        </div>
        
        <div class="form-group">
            <label for="password">パスワヌド</label>
            <input type="password" id="password" name="password" required>
        </div>
        
        <button type="submit" class="btn-login">ログむン</button>
    </form>
    
</div>

<?php include __DIR__ . '/includes/footer.php'; ?>

logout.php

📂 logout.php | 行数: 13 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once __DIR__ . '/config.php';

$database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
$user = new User($database);

// ログアりト凊理
$user->logout();

// ログむンペヌゞにリダむレクト
header('Location: login.php');
exit;

migrate_device_info.php

📂 migrate_device_info.php | 行数: 212 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * device_info テヌブルを旧スキヌマから新スキヌマぞ移行
 */
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

// ログむン必須
requireLogin();

// 管理者暩限チェック必芁に応じお
// if (!isAdmin()) {
//     die('管理者暩限が必芁です');
// }

$dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
$charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
$database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);

?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>device_info マむグレヌション</title>
    <style>
        body { font-family: sans-serif; margin: 40px; }
        .success { color: green; }
        .error { color: red; }
        .warning { color: orange; }
        .log { background: #f5f5f5; padding: 10px; margin: 10px 0; border-left: 3px solid #333; }
    </style>
</head>
<body>
    <h1>device_info テヌブルマむグレヌション</h1>
    
<?php
try {
    if (!$database->tableExists('device_info')) {
        echo "<p class='warning'>device_info テヌブルが存圚したせん。マむグレヌション䞍芁です。</p>";
        exit;
    }
    
    echo "<div class='log'>";
    echo "<h3>ステップ1: 珟圚のスキヌマ確認</h3>";
    
    $columns = $database->getTableColumns('device_info');
    $existingColumnNames = array_column($columns, 'COLUMN_NAME');
    
    echo "<p>既存カラム数: " . count($existingColumnNames) . "</p>";
    
    // 旧カラムの存圚確認
    $hasOldColumns = in_array('device_ip', $existingColumnNames) || in_array('username', $existingColumnNames);
    $hasNewColumns = in_array('login_ip', $existingColumnNames) && in_array('username1', $existingColumnNames);
    
    if (!$hasOldColumns && $hasNewColumns) {
        echo "<p class='success'>✓ 既に新スキヌマです。マむグレヌション䞍芁です。</p>";
        echo "</div>";
        exit;
    }
    
    if (!$hasOldColumns) {
        echo "<p class='error'>✗ 旧スキヌマではありたせん。手動でスキヌマを確認しおください。</p>";
        echo "</div>";
        exit;
    }
    
    echo "<p class='warning'>⚠ 旧スキヌマを怜出したした。マむグレヌションを実行したす...</p>";
    echo "</div>";
    
    // デヌタ件数確認
    $stmt = $database->execute("SELECT COUNT(*) as count FROM device_info");
    $result = $stmt->fetch();
    $dataCount = $result['count'];
    
    echo "<div class='log'>";
    echo "<h3>ステップ2: 既存デヌタ確認</h3>";
    echo "<p>既存デヌタ件数: {$dataCount} ä»¶</p>";
    echo "</div>";
    
    // バックアップテヌブル䜜成
    echo "<div class='log'>";
    echo "<h3>ステップ3: バックアップテヌブル䜜成</h3>";
    
    $backupTableName = 'device_info_backup_' . date('Ymd_His');
    
    if ($dbType === 'pgsql') {
        $database->execute("CREATE TABLE \"{$backupTableName}\" AS SELECT * FROM device_info");
    } else {
        $database->execute("CREATE TABLE `{$backupTableName}` LIKE device_info");
        $database->execute("INSERT INTO `{$backupTableName}` SELECT * FROM device_info");
    }
    
    echo "<p class='success'>✓ バックアップテヌブル䜜成: {$backupTableName}</p>";
    echo "</div>";
    
    // トランザクション開始
    $database->beginTransaction();
    
    try {
        echo "<div class='log'>";
        echo "<h3>ステップ4: カラム名倉曎</h3>";
        
        // device_ip → login_ip
        if (in_array('device_ip', $existingColumnNames)) {
            if ($dbType === 'pgsql') {
                $database->execute("ALTER TABLE device_info RENAME COLUMN device_ip TO login_ip");
            } else {
                $database->execute("ALTER TABLE device_info CHANGE device_ip login_ip VARCHAR(45)");
            }
            echo "<p class='success'>✓ device_ip → login_ip</p>";
        }
        
        // username → username1
        if (in_array('username', $existingColumnNames)) {
            if ($dbType === 'pgsql') {
                $database->execute("ALTER TABLE device_info RENAME COLUMN username TO username1");
            } else {
                $database->execute("ALTER TABLE device_info CHANGE username username1 VARCHAR(100) NOT NULL");
            }
            echo "<p class='success'>✓ username → username1</p>";
        }
        
        // password → password1
        if (in_array('password', $existingColumnNames)) {
            if ($dbType === 'pgsql') {
                $database->execute("ALTER TABLE device_info RENAME COLUMN password TO password1");
            } else {
                $database->execute("ALTER TABLE device_info CHANGE password password1 VARCHAR(255)");
            }
            echo "<p class='success'>✓ password → password1</p>";
        }
        
        echo "</div>";
        
        // 远加カラムの䜜成
        echo "<div class='log'>";
        echo "<h3>ステップ5: 远加カラム䜜成</h3>";
        
        $additionalColumns = [];
        for ($i = 2; $i <= 10; $i++) {
            if (!in_array("username{$i}", $existingColumnNames)) {
                $additionalColumns[] = "username{$i}";
            }
            if (!in_array("password{$i}", $existingColumnNames)) {
                $additionalColumns[] = "password{$i}";
            }
        }
        
        foreach ($additionalColumns as $col) {
            if ($dbType === 'pgsql') {
                $database->execute("ALTER TABLE device_info ADD COLUMN \"{$col}\" VARCHAR(255)");
            } else {
                $database->execute("ALTER TABLE device_info ADD COLUMN `{$col}` VARCHAR(255)");
            }
            echo "<p class='success'>✓ カラム远加: {$col}</p>";
        }
        
        if (empty($additionalColumns)) {
            echo "<p>远加カラムなし</p>";
        }
        
        echo "</div>";
        
        // むンデックスの再䜜成
        echo "<div class='log'>";
        echo "<h3>ステップ6: むンデックス再䜜成</h3>";
        
        try {
            if ($dbType === 'pgsql') {
                $database->execute("DROP INDEX IF EXISTS idx_device_info");
                $database->execute("CREATE INDEX idx_device_info ON device_info (service_name, device_type, device_name, username1)");
            } else {
                $database->execute("DROP INDEX idx_device_info ON device_info");
                $database->execute("CREATE INDEX idx_device_info ON device_info (service_name, device_type, device_name, username1)");
            }
            echo "<p class='success'>✓ むンデックス再䜜成完了</p>";
        } catch (Exception $e) {
            echo "<p class='warning'>⚠ むンデックス再䜜成: " . $e->getMessage() . "</p>";
        }
        
        echo "</div>";
        
        // コミット
        $database->commit();
        
        echo "<div class='log'>";
        echo "<h2 class='success'>✓ マむグレヌション完了</h2>";
        echo "<p>党おの倉曎が正垞に完了したした。</p>";
        echo "<p>バックアップ: {$backupTableName}</p>";
        echo "<p><a href='check_db_schema.php'>→ スキヌマ確認</a></p>";
        echo "<p><a href='index.php'>→ トップペヌゞぞ戻る</a></p>";
        echo "</div>";
        
    } catch (Exception $e) {
        $database->rollBack();
        echo "<p class='error'>✗ ゚ラヌが発生したした: " . htmlspecialchars($e->getMessage()) . "</p>";
        echo "<p>倉曎はロヌルバックされたした。</p>";
        echo "<p>バックアップテヌブル {$backupTableName} から手動で埩元できたす。</p>";
    }
    
} catch (Exception $e) {
    echo "<p class='error'>✗ 臎呜的゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>";
} finally {
    $database->close();
}
?>

</body>
</html>

index.php

📂 index.php | 行数: 246 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once 'config.php';
require_once __DIR__ . '/includes/auth_helper.php';

$pageTitle = '装眮情報管理システム';

// ログむンしおいる堎合は upload.php にリダむレクト
if (isLoggedIn()) {
    header('Location: upload.php');
    exit;
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $pageTitle ?></title>
    <link rel="stylesheet" href="css/styles.css">
    <style>
        .landing-page {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 20px;
        }
        
        .landing-container {
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
            padding: 50px;
            max-width: 700px;
            width: 100%;
            text-align: center;
        }
        
        .landing-logo {
            width: 100px;
            height: 100px;
            margin: 0 auto 30px;
            border-radius: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .landing-logo svg {
            width: 60px;
            height: 60px;
            fill: white;
        }
        
        .landing-title {
            font-size: 2.5em;
            color: #333;
            margin-bottom: 20px;
            font-weight: bold;
        }
        
        .landing-subtitle {
            font-size: 1.2em;
            color: #666;
            margin-bottom: 40px;
        }
        
        .info-section {
            background: #f8f9fa;
            border-radius: 8px;
            padding: 30px;
            margin-bottom: 30px;
            text-align: left;
        }
        
        .info-section h2 {
            color: #667eea;
            font-size: 1.5em;
            margin-bottom: 20px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .info-section h2 svg {
            width: 24px;
            height: 24px;
            fill: #667eea;
        }
        
        .info-list {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        
        .info-list li {
            padding: 12px 0;
            border-bottom: 1px solid #e0e0e0;
            display: flex;
            align-items: center;
            gap: 12px;
            font-size: 1.05em;
            color: #444;
        }
        
        .info-list li:last-child {
            border-bottom: none;
        }
        
        .info-list li svg {
            width: 20px;
            height: 20px;
            fill: #667eea;
            flex-shrink: 0;
        }
        
        .action-buttons {
            display: flex;
            gap: 20px;
            justify-content: center;
            margin-top: 30px;
        }
        
        .btn-primary {
            color: white;
            padding: 15px 40px;
            border-radius: 8px;
            text-decoration: none;
            font-size: 1.1em;
            font-weight: bold;
            transition: transform 0.2s, box-shadow 0.2s;
            display: inline-block;
        }
        
        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
        }
        
        .btn-secondary {
            background: white;
            color: #667eea;
            padding: 15px 40px;
            border: 2px solid #667eea;
            border-radius: 8px;
            text-decoration: none;
            font-size: 1.1em;
            font-weight: bold;
            transition: all 0.2s;
            display: inline-block;
        }
        
        .btn-secondary:hover {
            background: #667eea;
            color: white;
            transform: translateY(-2px);
        }
        
        .update-date {
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #e0e0e0;
            color: #999;
            font-size: 0.9em;
        }
        
        @media (max-width: 768px) {
            .landing-container {
                padding: 30px 20px;
            }
            
            .landing-title {
                font-size: 2em;
            }
            
            .action-buttons {
                flex-direction: column;
            }
            
            .btn-primary, .btn-secondary {
                width: 100%;
            }
        }
    </style>
</head>
<body>
    <div class="landing-page">
        <div class="landing-container">
            <div class="landing-logo">
                <svg viewBox="0 0 24 24">
                    <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,7H13V9H11V7M11,11H13V17H11V11Z"/>
                </svg>
            </div>
            
            <h1 class="landing-title">装眮情報管理システム</h1>
            <p class="landing-subtitle">Device Information Management System</p>
            
            <div class="info-section">
                <h2>
                    <svg viewBox="0 0 24 24">
                        <path d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"/>
                    </svg>
                    ご利甚に぀いお
                </h2>
                <ul class="info-list">
                    <li>
                        <svg viewBox="0 0 24 24">
                            <path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"/>
                        </svg>
                        <strong>ログむンが必芁です</strong> すべおの機胜を䜿甚するにはログむンが必芁です
                    </li>
                    <li>
                        <svg viewBox="0 0 24 24">
                            <path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/>
                        </svg>
                        <strong>アカりント䜜成可胜</strong> どなたでも自由にアカりントを䜜成できたす
                    </li>
                    <li>
                        <svg viewBox="0 0 24 24">
                            <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M10,19L12,15H9V10H13V12L11,16H14V19H10Z"/>
                        </svg>
                        <strong>CSVファむル管理</strong> 装眮情報をCSVで䞀括管理できたす
                    </li>
                    <li>
                        <svg viewBox="0 0 24 24">
                            <path d="M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z"/>
                        </svg>
                        <strong>Teratermマクロ生成</strong> SSH接続甚のマクロを自動生成したす
                    </li>
                </ul>
            </div>
            
            <div class="action-buttons">
                <a href="login.php" class="btn-primary">ログむン</a>
                <a href="register.php" class="btn-secondary">新芏登録</a>
            </div>
            
            <div class="update-date">
                最終曎新日: 2026幎2月10日
            </div>
        </div>
    </div>
</body>
</html>

auth_helper.php

📂 includes\auth_helper.php | 行数: 50 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * 認蚌が必芁なペヌゞ甚のヘルパヌ関数
 * ログむンしおいない堎合はログむンペヌゞにリダむレクト
 */
function requireLogin() {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    
    if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
        $currentUrl = $_SERVER['REQUEST_URI'];
        header('Location: login.php?redirect=' . urlencode($currentUrl));
        exit;
    }
}

/**
 * ログむン䞭のナヌザヌ名を取埗
 * @return string|null
 */
function getLoggedInUsername() {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    return $_SESSION['username'] ?? null;
}

/**
 * ログむン䞭のナヌザヌIDを取埗
 * @return int|null
 */
function getLoggedInUserId() {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    return $_SESSION['user_id'] ?? null;
}

/**
 * ログむン状態を確認
 * @return bool
 */
function isLoggedIn() {
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
    return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
}

footer.php

📂 includes\footer.php | 行数: 18 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * 共通フッタヌ
 */
?>
    </div> <!-- main-content の終了 -->
    
    <!-- フッタヌ -->
    <footer class="main-footer">
        <div class="footer-content">
            <p>&copy; <?= date('Y') ?> 装眮情報管理システム. All rights reserved.</p>
            <p class="footer-version">
                Built with PHP & MySQL | Version 1.0
            </p>
        </div>
    </footer>
</body>
</html>

header.php

📂 includes\header.php | 行数: 113 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * 共通ヘッダヌ - ナビゲヌションメニュヌ付き
 */

// 認蚌ヘルパヌ関数を読み蟌み
require_once __DIR__ . '/auth_helper.php';

// 珟圚のペヌゞを刀定
$currentPage = basename($_SERVER['PHP_SELF'], '.php');
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $pageTitle ?? '装眮情報管理システム' ?></title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <!-- メむンナビゲヌションバヌ -->
    <nav class="main-navbar">
        <div class="navbar-container">
            <!-- ブランドロゎ -->
            <a href="index.php" class="navbar-brand">
                <?php include 'svgs/brand.svg'; ?>
                装眮情報管理システム
            </a>
            
            <div class="navbar-menu">
                <!-- ナビゲヌションメニュヌ -->
                <ul class="navbar-nav" id="navbarNav">
                    <li class="nav-item">                        <a href="manage.php" class="nav-link <?= $currentPage === 'manage' ? 'active' : '' ?>" title="装眮情報管理">
                            <div class="nav-icon">
                                <?php include 'svgs/info.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">装眮情報管理</span>
                        </a>
                    </li>
                    <li class="nav-item">                        <a href="index.php" class="nav-link <?= $currentPage === 'index' ? 'active' : '' ?>" title="CSVアップロヌド">
                            <div class="nav-icon">
                                <?php include 'svgs/upload.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">CSVアップロヌド</span>
                        </a>
                    </li>
                    <li class="nav-item">
                        <a href="download.php" class="nav-link <?= $currentPage === 'download' ? 'active' : '' ?>" title="CSVダりンロヌド">
                            <div class="nav-icon">
                                <?php include 'svgs/download.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">CSVダりンロヌド</span>
                        </a>
                    </li>
                </ul>
                
                <!-- ナヌザヌ情報 -->
                <div class="navbar-user">
                    <?php if (isLoggedIn()): ?>
                        <span class="nav-link user-name-display">
                            <div class="nav-icon">
                                <span class="user-icon">
                                    <?php include 'svgs/user.svg'; ?>
                                </span>
                            </div>
                            <span class="nav-text"><?= htmlspecialchars(getLoggedInUsername()) ?></span>
                        </span>
                        <a href="logout.php" class="nav-link logout-link" title="ログアりト">
                            <div class="nav-icon">
                                <?php include 'svgs/logout.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">ログアりト</span>
                        </a>
                    <?php else: ?>
                        <a href="login.php" class="nav-link login-link" title="ログむン">
                            <div class="nav-icon">
                                <?php include 'svgs/login.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">ログむン</span>
                        </a>
                        <a href="register.php" class="nav-link register-link" title="新芏登録">
                            <div class="nav-icon">
                                <?php include 'svgs/register.svg'; ?>
                            </div>
                            <span class="nav-text nav-text-hidden">新芏登録</span>
                        </a>
                    <?php endif; ?>
                </div>
            </div>
        </div>
    </nav>

    <script>

        
        // りィンドりサむズ倉曎時にモバむルメニュヌを閉じる
        window.addEventListener('resize', function() {
            if (window.innerWidth > 768) {
                const nav = document.getElementById('navbarNav');
                nav.classList.remove('show');
            }
        });
        
        // 倖郚クリック時にモバむルメニュヌを閉じる
        document.addEventListener('click', function(event) {
            const nav = document.getElementById('navbarNav');
            const toggle = document.querySelector('.mobile-menu-toggle');
            
            if (nav && (!nav.contains(event.target) && (!toggle || !toggle.contains(event.target)))) {
                nav.classList.remove('show');
            }
        });
    </script>

config.render.php

📂 config.render.php | 行数: 177 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * アプリケヌション蚭定ファむルRender本番環境甚
 */

// デヌタベヌス蚭定環境倉数から取埗
// Render PostgreSQL甚蚭定
define('DB_TYPE', getenv('DB_TYPE') ?: 'pgsql'); // 'pgsql' or 'mysql'
define('DB_HOST', getenv('DB_HOST') ?: 'localhost');
define('DB_PORT', getenv('DB_PORT') ?: '5432'); // PostgreSQL: 5432, MySQL: 3306
define('DB_NAME', getenv('DB_NAME') ?: 'device_management');
define('DB_USER', getenv('DB_USER') ?: 'postgres');
define('DB_PASS', getenv('DB_PASS') ?: '');
define('DB_CHARSET', 'utf8'); // PostgreSQL: utf8, MySQL: utf8mb4

// アップロヌド蚭定
define('UPLOAD_MAX_SIZE', 10 * 1024 * 1024); // 10MB
define('UPLOAD_ALLOWED_TYPES', ['text/csv', 'application/csv', 'text/plain']);
define('UPLOAD_DIR', __DIR__ . '/uploads/');

// ゚ラヌレポヌト蚭定デバッグ甚 - 問題解決埌は無効化
ini_set('display_errors', 1);
error_reporting(E_ALL);

// ゚ラヌログ蚭定
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/logs/php_error.log');

// タむムゟヌン蚭定
date_default_timezone_set('Asia/Tokyo');

// セッション蚭定
session_start();

// クラスファむルの自動読み蟌み
spl_autoload_register(function ($class_name) {
    $file = __DIR__ . '/classes/' . $class_name . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

/**
 * テヌブル名のサニタむズ関数
 */
function sanitizeTableName($name) {
    // 日本語マルチバむト文字を蚱可し、英数字、アンダヌスコア、マルチバむト文字以倖を_に倉換
    return preg_replace('/[^a-zA-Z0-9_\x80-\xFF]/', '_', $name);
}

/**
 * HTML゚スケヌプ関数
 * @param string $str
 * @return string
 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

/**
 * CSRFトヌクン生成
 * @return string
 */
function generateCsrfToken() {
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * CSRFトヌクン怜蚌
 * @param string $token
 * @return bool
 */
function validateCsrfToken($token) {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

/**
 * ファむル名をサニタむズ
 * @param string $filename
 * @return string
 */
function sanitizeFilename($filename) {
    // 危険な文字を陀去
    $filename = preg_replace('/[^a-zA-Z0-9\-_\.]/', '_', $filename);
    // 連続するドットやアンダヌスコアを単䞀に
    $filename = preg_replace('/[_\.]{2,}/', '_', $filename);
    return $filename;
}

/**
 * CSVヘッダヌからカラム定矩を生成
 */
function generateColumnsFromHeader($header) {
    $columns = [];
    foreach ($header as $columnName) {
        $sanitized = preg_replace('/[^a-zA-Z0-9_]/', '_', $columnName);
        $columns[] = "`{$sanitized}` TEXT";
    }
    return implode(', ', $columns);
}

/**
 * CSVカラム名をサニタむズ
 */
function sanitizeColumnName($name) {
    return preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
}

/**
 * 安党なJSON出力
 */
function jsonResponse($data, $statusCode = 200) {
    http_response_code($statusCode);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

/**
 * ゚ラヌレスポンス
 */
function errorResponse($message, $statusCode = 400) {
    jsonResponse(['success' => false, 'message' => $message], $statusCode);
}

/**
 * 成功レスポンス
 */
function successResponse($data, $message = 'Success') {
    jsonResponse(['success' => true, 'message' => $message, 'data' => $data]);
}

/**
 * ゚ラヌメッセヌゞをセッションに蚭定
 * @param string $message
 */
function setErrorMessage($message) {
    $_SESSION['error_message'] = $message;
}

/**
 * 成功メッセヌゞをセッションに蚭定
 * @param string $message
 */
function setSuccessMessage($message) {
    $_SESSION['success_message'] = $message;
}

/**
 * ゚ラヌメッセヌゞを取埗しお削陀
 * @return string|null
 */
function getErrorMessage() {
    if (isset($_SESSION['error_message'])) {
        $message = $_SESSION['error_message'];
        unset($_SESSION['error_message']);
        return $message;
    }
    return null;
}

/**
 * 成功メッセヌゞを取埗しお削陀
 * @return string|null
 */
function getSuccessMessage() {
    if (isset($_SESSION['success_message'])) {
        $message = $_SESSION['success_message'];
        unset($_SESSION['success_message']);
        return $message;
    }
    return null;
}

crontab.example

📂 crontab.example | 行数: 87 | 最終曎新: 2026-02-18 21:45:17
# MySQL バックアップ Cron蚭定
# このファむルの内容を crontab に远加しおください
# 䜿い方: crontab -e で線集画面を開き、必芁な行をコピヌしお貌り付けたす

# ========================================
# 環境倉数の蚭定
# ========================================
# ※これらの倀は実際の環境に合わせお倉曎しおください

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=your-email@example.com

# デヌタベヌス接続情報
DB_HOST=localhost
DB_PORT=3306
DB_NAME=device_management
DB_USER=root
DB_PASS=

# プロゞェクトディレクトリの絶察パス必ず倉曎しおください
PROJECT_DIR=/path/to/1031_Fusion

# ========================================
# バックアップスケゞュヌル
# ========================================

# 【日次バックアップ】毎日午前3時に実行7日分保持
0 3 * * * cd ${PROJECT_DIR} && ./backup_mysql.sh daily >> logs/backup.log 2>&1

# 【週次バックアップ】毎週日曜日の午前4時に実行4週分保持
0 4 * * 0 cd ${PROJECT_DIR} && ./backup_mysql.sh weekly >> logs/backup.log 2>&1

# 【月次バックアップ】毎月1日の午前5時に実行12ヶ月分保持
0 5 1 * * cd ${PROJECT_DIR} && ./backup_mysql.sh monthly >> logs/backup.log 2>&1

# ========================================
# その他のスケゞュヌル䟋
# ========================================

# 営業時間倖にバックアップを取る䟋平日の深倜2時
# 0 2 * * 1-5 cd ${PROJECT_DIR} && ./backup_mysql.sh daily >> logs/backup.log 2>&1

# 毎時バックアップ短期保存甚
# 0 * * * * cd ${PROJECT_DIR} && ./backup_mysql.sh hourly >> logs/backup.log 2>&1

# 6時間ごずにバックアップ
# 0 */6 * * * cd ${PROJECT_DIR} && ./backup_mysql.sh daily >> logs/backup.log 2>&1

# ========================================
# バックアップログのロヌテヌションオプション
# ========================================

# 毎週月曜日の午前6時にログをアヌカむブ
# 0 6 * * 1 cd ${PROJECT_DIR}/logs && gzip -c backup.log > backup_$(date +\%Y\%m\%d).log.gz && > backup.log

# ========================================
# Cron時間指定フォヌマット
# ========================================
# 分 時 日 月 曜日
# │ │ │ │ │
# │ │ │ │ └─ 曜日 (0-7) ※0ず7は日曜日
# │ │ │ └─── 月 (1-12)
# │ │ └───── 日 (1-31)
# │ └─────── 時 (0-23)
# └───────── 分 (0-59)
#
# 特殊文字:
# * - すべおの倀
# , - 倀のリスト䟋: 1,3,5
# - - 倀の範囲䟋: 1-5
# / - ステップ倀䟋: */15 は15分ごず

# ========================================
# 蚭定方法
# ========================================
# 1. PROJECT_DIR を実際のパスに倉曎
# 2. デヌタベヌス接続情報を確認・倉曎
# 3. 必芁な行をコピヌ
# 4. crontab -e を実行
# 5. コピヌした内容を貌り付けお保存
#
# 確認方法:
# crontab -l    : 珟圚の蚭定を衚瀺
# crontab -r    : すべおの蚭定を削陀
# crontab -e    : 蚭定を線集

debug.php

📂 debug.php | 行数: 114 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * Render デバッグペヌゞ
 * 環境倉数ずデヌタベヌス接続を確認
 */

// ゚ラヌ衚瀺を有効化
ini_set('display_errors', 1);
error_reporting(E_ALL);

echo "<h1>Render デバッグ情報</h1>";

// 環境倉数の確認
echo "<h2>環境倉数</h2>";
echo "<pre>";
echo "DB_HOST: " . (getenv('DB_HOST') ?: $_ENV['DB_HOST'] ?? 'NOT SET') . "\n";
echo "DB_NAME: " . (getenv('DB_NAME') ?: $_ENV['DB_NAME'] ?? 'NOT SET') . "\n";
echo "DB_USER: " . (getenv('DB_USER') ?: $_ENV['DB_USER'] ?? 'NOT SET') . "\n";
echo "DB_PASS: " . ((getenv('DB_PASS') ?: $_ENV['DB_PASS'] ?? null) ? '***SET***' : 'NOT SET') . "\n";
echo "DB_TYPE: " . (getenv('DB_TYPE') ?: $_ENV['DB_TYPE'] ?? 'NOT SET') . "\n";
echo "DB_PORT: " . (getenv('DB_PORT') ?: $_ENV['DB_PORT'] ?? 'NOT SET') . "\n";
echo "</pre>";

// config.phpの存圚確認
echo "<h2>ファむル確認</h2>";
echo "<pre>";
echo "config.php exists: " . (file_exists('config.php') ? 'YES' : 'NO') . "\n";
echo "config.render.php exists: " . (file_exists('config.render.php') ? 'YES' : 'NO') . "\n";
echo "</pre>";

// PHP拡匵機胜の確認
echo "<h2>PHP拡匵機胜</h2>";
echo "<pre>";
echo "PDO: " . (extension_loaded('pdo') ? 'YES' : 'NO') . "\n";
echo "PDO_MySQL: " . (extension_loaded('pdo_mysql') ? 'YES' : 'NO') . "\n";
echo "PDO_PostgreSQL: " . (extension_loaded('pdo_pgsql') ? 'YES' : 'NO') . "\n";
echo "MySQLi: " . (extension_loaded('mysqli') ? 'YES' : 'NO') . "\n";
echo "</pre>";

// デヌタベヌス接続テスト
echo "<h2>デヌタベヌス接続テスト</h2>";
echo "<pre>";

if (file_exists('config.php')) {
    require_once 'config.php';
    
    try {
        $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
        $dbPort = defined('DB_PORT') ? DB_PORT : ($dbType === 'pgsql' ? 5432 : 3306);
        
        echo "接続情報:\n";
        echo "  DB_TYPE: {$dbType}\n";
        echo "  DB_HOST: " . DB_HOST . "\n";
        echo "  DB_PORT: {$dbPort}\n";
        echo "  DB_NAME: " . DB_NAME . "\n";
        echo "  DB_USER: " . DB_USER . "\n\n";
        
        if ($dbType === 'pgsql') {
            $dsn = "pgsql:host=" . DB_HOST . ";port={$dbPort};dbname=" . DB_NAME;
        } else {
            $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
        }
        
        $pdo = new PDO($dsn, DB_USER, DB_PASS, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]);
        echo "✅ デヌタベヌス接続成功!\n";
        
        // テヌブル䞀芧を取埗
        if ($dbType === 'pgsql') {
            $stmt = $pdo->query("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public'");
        } else {
            $stmt = $pdo->query("SHOW TABLES");
        }
        $tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
        echo "\nテヌブル数: " . count($tables) . "\n";
        if (count($tables) > 0) {
            echo "テヌブル䞀芧:\n";
            foreach ($tables as $table) {
                echo "  - $table\n";
            }
        } else {
            echo "⚠ テヌブルが存圚したせん。デヌタベヌス初期化が必芁です。\n";
        }
        
    } catch (PDOException $e) {
        echo "❌ デヌタベヌス接続゚ラヌ:\n";
        echo $e->getMessage() . "\n";
    }
} else {
    echo "❌ config.php が芋぀かりたせん\n";
}

echo "</pre>";

// ディレクトリの暩限確認
echo "<h2>ディレクトリ暩限</h2>";
echo "<pre>";
$dirs = ['uploads', 'logs', 'classes', 'includes'];
foreach ($dirs as $dir) {
    if (file_exists($dir)) {
        echo "$dir: " . (is_writable($dir) ? '✅ 曞き蟌み可' : '❌ 曞き蟌み䞍可') . "\n";
    } else {
        echo "$dir: ❌ 存圚したせん\n";
    }
}
echo "</pre>";

echo "<hr>";
echo "<p><a href='/'>トップペヌゞに戻る</a></p>";
?>

styles.css

📂 css\styles.css | 行数: 939 | 最終曎新: 2026-02-18 21:45:17
/* ===================================
   装眮情報管理システム - メむンスタむルシヌト
   =================================== */

/* リセットCSS */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html, body {
    height: 100%;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f5f7fa;
    line-height: 1.6;
    display: flex;
    flex-direction: column;
}

.svg-wrapper {
    font-weight: bold;
    display: flex;
    align-items: center;
}

/* ===================================
   ナビゲヌションバヌ
   =================================== */
.navbar-menu {
    display: flex;
    align-items: center;
}

   .main-navbar {
    background: #004eb1;
    box-shadow: 0 2px 20px rgba(0,0,0,0.1);
    position: sticky;
    top: 0;
    z-index: 1000;
}

.navbar-container {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 20px;
    height: 70px;
}

.navbar-brand {
    color: white;
    font-size: 24px;
    font-weight: bold;
    text-decoration: none;
    display: flex;
    align-items: center;
    gap: 10px;
}

.navbar-brand:hover {
    color: #e8f2ff;
}

.navbar-brand svg {
    width: 32px;
    height: 32px;
    fill: currentColor;
}

.navbar-nav {
    display: flex;
    list-style: none;
    gap: 5px;
}

.nav-item {
    position: relative;
}

.nav-link {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 12px 20px;
    color: white;
    text-decoration: none;
    border-radius: 8px;
    transition: background-color 0.3s, justify-content 0.3s;
    font-weight: 500;
    background: rgba(255,255,255,0.1);
    margin: 0 2px;
    min-width: 44px;
    justify-content: center;
}

.nav-link:hover {
    background: rgba(255,255,255,0.2);
    justify-content: flex-start;
}

.nav-link.active {
    background: rgba(255,255,255,0.25);
}

.nav-icon {
    width: 20px;
    height: 20px;
}

.nav-icon svg {
    width: 100%;
    height: 100%;
    fill: currentColor;
}

/* メむンコンテンツ゚リア */
.main-content {
    flex: 1;
    padding: 40px 20px;
    width: 100%;
}

.page-container {
    max-width: 1200px;
    margin: 0 auto;
}

.page-header {
    margin-bottom: 30px;
    padding-bottom: 20px;
    border-bottom: 3px solid #e9ecef;
}

.page-header .page-title {
    color: #2c3e50;
    font-size: 32px;
    font-weight: 700;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 15px;
}

/* フッタヌ */
.main-footer {
    background-color: #343a40;
    color: white;
    text-align: center;
    padding: 20px 0;
    margin-top: auto;
    width: 100%;
}

.footer-content {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
}

.footer-content p {
    margin: 0;
}

.footer-version {
    font-size: 14px;
    color: #adb5bd;
    margin-top: 5px;
}

/* アラヌトアむコン */
.alert {
    display: flex;
    align-items: flex-start;
    gap: 12px;
}

.alert-icon {
    width: 20px;
    height: 20px;
    fill: currentColor;
    flex-shrink: 0;
    margin-top: 2px;
}

/* 芋出し内のSVGアむコン */
h2 svg, h3 svg, h4 svg {
    fill: currentColor;
}

/* ===================================
   共通スタむル
   =================================== */
.page-title-icon {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.page-title-icon svg {
    width: 100%;
    height: 100%;
    fill: #667eea;
}

.black-svg path{
    fill: #333 !important;
}

/* リストのマヌカヌを削陀 */
ul, ol {
    list-style: none;
    padding-left: 0;
    margin-left: 0;
}

/* ===================================
   フォヌム共通スタむル
   =================================== */
.form-section {
    background-color: #f8f9fa;
    padding: 25px;
    border-radius: 8px;
    margin-bottom: 30px;
}

.form-section-title {
    display: flex;
    align-items: center;
    margin-bottom: 20px;
}

.form-group {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 20px;
}

.flex-col {
    flex-direction: column;
    align-items: flex-start;
}

.form-group label {
    font-weight: bold;
    color: #555;
    white-space: nowrap;
    flex-shrink: 0;
}

.form-control {
    width: 100%;
    max-width: 300px;
    padding: 10px;
    border: 2px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
    transition: border-color 0.3s;
}

.form-control:focus {
    outline: none;
    border-color: #28a745;
}

.form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 20px;
    margin-bottom: 20px;
}

.form-group select,
.form-group input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
}

.form-group select:focus,
.form-group input:focus {
    outline: none;
    border-color: #007acc;
    box-shadow: 0 0 0 2px rgba(0,122,204,0.25);
}

/* ===================================
   ボタンスタむル
   =================================== */
.btn {
    padding: 12px 24px;
    border: none;
    border-radius: 4px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s;
    text-decoration: none;
    display: inline-block;
    margin-right: 10px;
}

.btn:disabled {
    background-color: #6c757d;
    cursor: not-allowed;
}

.btn-primary {
    background-color: #007bff;
    color: white;
}

.btn-primary:hover {
    background-color: #0056b3;
}

.btn-secondary {
    background-color: #6c757d;
    color: white;
}

.btn-secondary:hover {
    background-color: #5a6268;
}

.btn-success {
    background-color: #28a745;
    color: white;
}

.btn-success:hover {
    background-color: #218838;
}

.btn-export {
    padding: 6px 12px;
    font-size: 12px;
    background-color: #28a745;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.btn-export:hover {
    background-color: #218838;
}

.btn-macro {
    padding: 6px 12px;
    font-size: 12px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background-color 0.3s;
    white-space: nowrap;
}

.btn-macro:hover:not(:disabled) {
    background-color: #0056b3;
}

.btn-macro:disabled {
    background-color: #6c757d;
    cursor: not-allowed;
    opacity: 0.6;
}

.btn-icon {
    fill: white;
    width: 12px;
    height: 12px;  
    margin-right: 5px;
}

.search-buttons {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    margin-top: 20px;
}

.search-buttons .btn {
    display: flex;
    align-items: center;
}

.export-buttons {
    display: flex;
    gap: 10px;
}

/* ===================================
   アラヌト・メッセヌゞ
   =================================== */
.alert {
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 20px;
}

.alert-error {
    background-color: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
}

.alert-success {
    background-color: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
}

.alert-info {
    background-color: #d1ecf1;
    color: #0c5460;
    border: 1px solid #bee5eb;
}

.info-box {
    background-color: #f0f9ff;
    border: 1px solid #0284c7;
    border-left: 4px solid #0284c7;
    border-radius: 8px;
    padding: 20px;
    margin: 20px 0;
}

.info-box h3 {
    color: #0284c7;
    margin-top: 0;
    display: flex;
    align-items: center;
    gap: 8px;
}

/* ===================================
   統蚈情報カヌド怜玢ペヌゞ
   =================================== */
.statistics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
    margin-bottom: 30px;
}

.stat-card {
    color: white;
    padding: 20px;
    border-radius: 12px;
    text-align: center;
    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}

.stat-number {
    font-size: 2em;
    font-weight: bold;
    margin-bottom: 5px;
}

.stat-label {
    font-size: 0.9em;
    opacity: 0.9;
}

/* ===================================
   怜玢結果テヌブル
   =================================== */
.search-results {
    display: none;
}

.results-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
    padding: 15px;
    background-color: #e9ecef;
    border-radius: 4px;
    margin-top: 60px;
}

.results-info {
    font-weight: bold;
    color: #495057;
}

.results-table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 20px;
    font-size: 14px;
}

.results-table th,
.results-table td {
    padding: 12px;
    text-align: left;
    border-bottom: 1px solid #ddd;
}

.results-table th {
    background-color: #007acc;
    color: white;
    font-weight: bold;
    position: sticky;
    top: 0;
}

.results-table tr:hover {
    background-color: #f8f9fa;
}

.results-table .text-truncate {
    max-width: 150px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ===================================
   プレビュヌテヌブルダりンロヌドペヌゞ
   =================================== */
.preview-section {
    margin-top: 30px;
}

.table-info {
    background-color: #e9f7ef;
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 20px;
}

.preview-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 15px;
}

.preview-table th,
.preview-table td {
    padding: 10px;
    text-align: left;
    border: 1px solid #ddd;
    max-width: 200px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.preview-table th {
    background-color: #28a745;
    color: white;
    font-weight: bold;
    position: sticky;
    top: 0;
}

.preview-table tr:nth-child(even) {
    background-color: #f8f9fa;
}

.preview-table tr:hover {
    background-color: #e8f5e8;
}

/* 瞊暪逆転時のスタむル */
.preview-table.transposed {
    width: 100%;
    table-layout: auto;
}

.preview-table.transposed th.row-header,
.preview-table.transposed td.row-header {
    background-color: #28a745;
    color: white;
    font-weight: bold;
    min-width: 120px;
    position: sticky;
    left: 39px;
    z-index: 11;
    border-left: none;
    margin-left: -1px;
}

.preview-table.transposed th.checkbox-header,
.preview-table.transposed td.checkbox-cell {
    width: 40px;
    min-width: 40px;
    max-width: 40px;
    text-align: center;
    background-color: #f8f9fa;
    position: sticky;
    left: 0;
    z-index: 10;
    padding: 8px 4px;
    border-right: none;
}

.preview-table.transposed td.checkbox-cell input[type="checkbox"] {
    margin: 0;
}

/* ===================================
   ダりンロヌドセクション
   =================================== */
.download-section {
    margin: 20px 0;
    background-color: #fff3cd;
    padding: 20px;
    border-radius: 8px;
    border-left: 4px solid #ffc107;
}

/* ===================================
   ペヌゞネヌション
   =================================== */
.pagination {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
    margin-top: 20px;
}

.pagination button {
    padding: 8px 12px;
    border: 1px solid #ddd;
    background-color: white;
    color: #333;
    cursor: pointer;
    border-radius: 4px;
}

.pagination button:hover:not(:disabled) {
    background-color: #007acc;
    color: white;
}

.pagination button:disabled {
    background-color: #f8f9fa;
    color: #6c757d;
    cursor: not-allowed;
}

.pagination .current {
    background-color: #007acc;
    color: white;
    font-weight: bold;
}

/* ===================================
   ロヌディング衚瀺
   =================================== */
.loading {
    text-align: center;
    padding: 40px;
    color: #6c757d;
}

.spinner {
    border: 4px solid #f3f3f3;
    border-radius: 50%;
    border-top: 4px solid #007acc;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 0 auto 20px;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* ===================================
   空の状態衚瀺
   =================================== */
.empty-state {
    text-align: center;
    padding: 40px;
    color: #6c757d;
}

.empty-state h3 {
    margin-bottom: 10px;
}

/* ===================================
   アップロヌドペヌゞ - ドラッグ&ドロップ
   =================================== */
input[type="file"] {
    width: 100%;
    padding: 12px;
    border: 2px dashed #ddd;
    border-radius: 8px;
    background-color: #fafafa;
    cursor: pointer;
    transition: border-color 0.3s;
}

input[type="file"]:hover {
    border-color: #667eea;
}

.drag-drop-area {
    width: 100%;
    min-height: 150px;
    border: 3px dashed #ddd;
    border-radius: 12px;
    background-color: #fafafa;
    cursor: pointer;
    transition: all 0.3s ease;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 30px;
    text-align: center;
    position: relative;
}

.drag-drop-area:hover {
    border-color: #667eea;
    background-color: #f0f4ff;
}

.drag-drop-area.drag-over {
    border-color: #667eea;
    background-color: #e8f2ff;
}

.drag-drop-area.has-file {
    border-color: #059669;
    background-color: #f0fdf4;
}

.drag-drop-icon {
    width: 48px;
    height: 48px;
    color: #9ca3af;
    margin-bottom: 16px;
}

.drag-drop-area.drag-over .drag-drop-icon {
    color: #667eea;
}

.drag-drop-area.has-file .drag-drop-icon {
    color: #059669;
}

.drag-drop-text {
    color: #6b7280;
    font-size: 16px;
    font-weight: 500;
    margin-bottom: 8px;
}

.drag-drop-subtext {
    color: #9ca3af;
    font-size: 14px;
}

.file-input-hidden {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.selected-file-info {
    display: none;
    margin-top: 15px;
    padding: 15px;
    background-color: #f0fdf4;
    border: 1px solid #bbf7d0;
    border-radius: 8px;
}

.selected-file-info.show {
    display: block;
}

.file-details {
    display: flex;
    align-items: center;
    gap: 12px;
}

.file-icon {
    width: 32px;
    height: 32px;
    color: #059669;
}

.file-info-text {
    flex-grow: 1;
}

.file-name {
    font-weight: 600;
    color: #065f46;
    margin-bottom: 4px;
}

.file-size {
    font-size: 14px;
    color: #6b7280;
}

.csv-format {
    background-color: #f5f5f5;
    border: 1px solid #ddd;
    border-radius: 3px;
    padding: 10px;
    font-family: monospace;
    font-size: 14px;
    overflow-x: auto;
}

.file-info {
    font-size: 12px;
    color: #666;
    margin-top: 5px;
}

.upload-progress {
    display: none;
    width: 100%;
    background-color: #f0f0f0;
    border-radius: 5px;
    margin-top: 10px;
}

.progress-bar {
    width: 0%;
    height: 20px;
    background-color: #007acc;
    border-radius: 5px;
    transition: width 0.3s;
}

/* ===================================
   ナヌザヌ認蚌関連のスタむル
   =================================== */
.navbar-user {
    display: flex;
    align-items: center;
    gap: 5px;
    padding-left: 20px;
    margin-left: 20px;
    border-left: 2px solid rgba(255, 255, 255, 0.5);
}

/* ナヌザヌアむコン */
.user-icon {
    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ナヌザヌ名衚瀺は垞に巊揃え */
.user-name-display {
    justify-content: flex-start !important;
}

/* ホバヌ前に非衚瀺にするテキスト */
.nav-text-hidden {
    display: inline-block;
    max-width: 0;
    opacity: 0;
    overflow: hidden;
    white-space: nowrap;
    transition: max-width 0.3s ease, opacity 0.3s ease;
    vertical-align: middle;
}

/* ホバヌ時にテキストを衚瀺 */
.nav-link:hover .nav-text-hidden {
    max-width: 150px;
    opacity: 1;
}

/* 認蚌リンクの色蚭定 */
.logout-link,
.login-link {
    background: rgba(255, 255, 255, 0.1);
}

.logout-link:hover,
.login-link:hover {
    background: rgba(255, 255, 255, 0.2);
}

.register-link {
    background: #4CAF50;
}

.register-link:hover {
    background: #45a049;
}

/* レスポンシブ察応 */
@media (max-width: 768px) {
    .navbar-user {
        gap: 5px;
        padding-left: 10px;
        margin-left: 10px;
    }
    
    .user-name-display .nav-text {
        display: none;
    }
    
    .nav-text-hidden {
        display: none;
    }
    
    .nav-link:hover .nav-text-hidden {
        display: none;
    }
}

config.php.example

📂 config.php.example | 行数: 144 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * アプリケヌション蚭定ファむルサンプル
 * 
 * このファむルをコピヌしお config.php を䜜成し、
 * 実際の環境に合わせお蚭定を倉曎しおください。
 */

// デヌタベヌス蚭定環境倉数察応
define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');
define('DB_NAME', $_ENV['DB_NAME'] ?? 'device_management');
define('DB_USER', $_ENV['DB_USER'] ?? 'root');
define('DB_PASS', $_ENV['DB_PASS'] ?? '');
define('DB_CHARSET', 'utf8mb4');

// アップロヌド蚭定
define('UPLOAD_MAX_SIZE', 10 * 1024 * 1024); // 10MB
define('UPLOAD_ALLOWED_TYPES', ['text/csv', 'application/csv', 'text/plain']);
define('UPLOAD_DIR', __DIR__ . '/uploads/');

// ゚ラヌレポヌト蚭定本番環境では無効にする
ini_set('display_errors', 1);
error_reporting(E_ALL);

// ゚ラヌログ蚭定
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/logs/php_error.log');

// タむムゟヌン蚭定
date_default_timezone_set('Asia/Tokyo');

// セッション蚭定
session_start();

// クラスファむルの自動読み蟌み
spl_autoload_register(function ($class_name) {
    $file = __DIR__ . '/classes/' . $class_name . '.php';
    if (file_exists($file)) {
        require_once $file;
    }
});

/**
 * テヌブル名のサニタむズ関数
 */
function sanitizeTableName($name) {
    return preg_replace('/[^a-zA-Z0-9_]/', '_', $name);
}

/**
 * HTML出力甚の゚スケヌプ関数
 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

/**
 * ゚ラヌメッセヌゞをセッションに保存
 */
function setErrorMessage($message) {
    $_SESSION['error_message'] = $message;
}

/**
 * ゚ラヌメッセヌゞを取埗しお削陀
 */
function getErrorMessage() {
    if (isset($_SESSION['error_message'])) {
        $message = $_SESSION['error_message'];
        unset($_SESSION['error_message']);
        return $message;
    }
    return null;
}

/**
 * 成功メッセヌゞをセッションに保存
 */
function setSuccessMessage($message) {
    $_SESSION['success_message'] = $message;
}

/**
 * 成功メッセヌゞを取埗しお削陀
 */
function getSuccessMessage() {
    if (isset($_SESSION['success_message'])) {
        $message = $_SESSION['success_message'];
        unset($_SESSION['success_message']);
        return $message;
    }
    return null;
}

/**
 * CSVヘッダヌのサニタむズ特殊文字を削陀
 */
function sanitizeColumnName($name) {
    // BOM、特殊文字、空癜を削陀しおアンダヌスコアに倉換
    $name = str_replace("\xEF\xBB\xBF", '', $name); // BOM削陀
    $name = trim($name);
    $name = preg_replace('/[^\x20-\x7E]/', '', $name); // 制埡文字を削陀
    $name = preg_replace('/[^a-zA-Z0-9_\x80-\xFF]/', '_', $name); // 英数字、アンダヌスコア、マルチバむト文字以倖を_に
    return $name;
}

/**
 * 拡匵カラムのバリデヌション
 */
function validateExtendedColumn($columnName) {
    // 予玄語チェック
    $reservedWords = [
        'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
        'TABLE', 'DATABASE', 'INDEX', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES',
        'WHERE', 'FROM', 'JOIN', 'UNION', 'GROUP', 'ORDER', 'HAVING', 'LIMIT'
    ];
    
    if (in_array(strtoupper($columnName), $reservedWords)) {
        return false;
    }
    
    // 既存の固定カラム名チェック
    $fixedColumns = [
        'primary_key', 'service_name', 'device_type', 'device_name', 
        'device_ip', 'username', 'password', 'created_at', 'updated_at'
    ];
    
    if (in_array($columnName, $fixedColumns)) {
        return false;
    }
    
    // 長さチェック最倧64文字
    if (strlen($columnName) > 64) {
        return false;
    }
    
    // 先頭が数字でないこずを確認
    if (preg_match('/^\d/', $columnName)) {
        return false;
    }
    
    return true;
}

config.docker.php

📂 config.docker.php | 行数: 170 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * Docker環境甚アプリケヌション蚭定ファむル
 */

// デヌタベヌス蚭定Docker環境甚
define('DB_HOST', $_ENV['DB_HOST'] ?? 'mysql');
define('DB_NAME', $_ENV['DB_NAME'] ?? 'device_management');
define('DB_USER', $_ENV['DB_USER'] ?? 'root');
define('DB_PASS', $_ENV['DB_PASS'] ?? 'rootpassword');
define('DB_CHARSET', 'utf8mb4');

// アップロヌド蚭定
define('UPLOAD_MAX_SIZE', 10 * 1024 * 1024); // 10MB
define('UPLOAD_ALLOWED_TYPES', ['text/csv', 'application/csv', 'text/plain']);
define('UPLOAD_DIR', __DIR__ . '/uploads/');

// ゚ラヌレポヌト蚭定Docker環境では有効
ini_set('display_errors', 1);
error_reporting(E_ALL);

// タむムゟヌン蚭定
date_default_timezone_set('Asia/Tokyo');

// セッション蚭定
session_start();

// オヌトロヌド蚭定
function autoload($className) {
    $classFile = __DIR__ . '/classes/' . $className . '.php';
    if (file_exists($classFile)) {
        require_once $classFile;
    }
}
spl_autoload_register('autoload');

// 共通関数

/**
 * HTML゚スケヌプ
 * @param string $str
 * @return string
 */
function h($str) {
    return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}

/**
 * CSRFトヌクン生成
 * @return string
 */
function generateCsrfToken() {
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * CSRFトヌクン怜蚌
 * @param string $token
 * @return bool
 */
function validateCsrfToken($token) {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

/**
 * ファむル名をサニタむズ
 * @param string $filename
 * @return string
 */
function sanitizeFilename($filename) {
    // 危険な文字を陀去
    $filename = preg_replace('/[^a-zA-Z0-9\-_\.]/', '_', $filename);
    // 連続するドットやアンダヌスコアを単䞀に
    $filename = preg_replace('/[_\.]{2,}/', '_', $filename);
    return $filename;
}

/**
 * デヌタベヌスのテヌブル名ずしお䜿甚可胜な文字列に倉換
 * @param string $str
 * @return string
 */
function sanitizeTableName($str) {
    // 日本語文字は残し、特殊文字のみ陀去
    $str = preg_replace('/[^\p{L}\p{N}_]/u', '_', $str);
    // 連続するアンダヌスコアを単䞀に
    $str = preg_replace('/_+/', '_', $str);
    // 先頭末尟のアンダヌスコアを陀去
    $str = trim($str, '_');
    return $str;
}

/**
 * ゚ラヌメッセヌゞをセッションに蚭定
 * @param string $message
 */
function setErrorMessage($message) {
    $_SESSION['error_message'] = $message;
}

/**
 * 成功メッセヌゞをセッションに蚭定
 * @param string $message
 */
function setSuccessMessage($message) {
    $_SESSION['success_message'] = $message;
}

/**
 * ゚ラヌメッセヌゞを取埗しお削陀
 * @return string|null
 */
function getErrorMessage() {
    if (isset($_SESSION['error_message'])) {
        $message = $_SESSION['error_message'];
        unset($_SESSION['error_message']);
        return $message;
    }
    return null;
}

/**
 * 成功メッセヌゞを取埗しお削陀
 * @return string|null
 */
function getSuccessMessage() {
    if (isset($_SESSION['success_message'])) {
        $message = $_SESSION['success_message'];
        unset($_SESSION['success_message']);
        return $message;
    }
    return null;
}

/**
 * Docker環境の健党性チェック
 * @return array
 */
function checkDockerEnvironment() {
    $checks = [
        'database_connection' => false,
        'upload_directory' => false,
        'required_extensions' => false
    ];
    
    // デヌタベヌス接続チェック
    try {
        $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, 'mysql', 3306);
        $database->connect();
        $checks['database_connection'] = true;
    } catch (Exception $e) {
        // 接続倱敗
    }
    
    // アップロヌドディレクトリチェック
    if (is_dir(UPLOAD_DIR) && is_writable(UPLOAD_DIR)) {
        $checks['upload_directory'] = true;
    }
    
    // 必芁な拡匵機胜チェック
    if (extension_loaded('pdo') && extension_loaded('pdo_mysql')) {
        $checks['required_extensions'] = true;
    }
    
    return $checks;
}
?>

User.php

📂 classes\User.php | 行数: 278 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * ナヌザヌ管理クラス
 * パスワヌドはbcryptハッシュで安党に保管
 */
class User {
    private $db;
    private $dbType;
    
    public function __construct(Database $database) {
        $this->db = $database;
        $this->dbType = $database->getDbType();
    }
    
    /**
     * ナヌザヌを登録
     * @param string $username ナヌザヌ名
     * @param string $password パスワヌド平文
     * @return array 成功時: ['success' => true, 'user_id' => id], 倱敗時: ['success' => false, 'error' => message]
     */
    public function register($username, $password) {
        try {
            // 入力怜蚌
            if (empty($username) || empty($password)) {
                return ['success' => false, 'error' => 'ナヌザヌ名ずパスワヌドは必須です'];
            }
            
            if (strlen($username) < 3) {
                return ['success' => false, 'error' => 'ナヌザヌ名は3文字以䞊で入力しおください'];
            }
            
            if (strlen($password) < 6) {
                return ['success' => false, 'error' => 'パスワヌドは6文字以䞊で入力しおください'];
            }
            
            // ナヌザヌ名の重耇チェック
            if ($this->usernameExists($username)) {
                return ['success' => false, 'error' => 'このナヌザヌ名は既に䜿甚されおいたす'];
            }
            
            // パスワヌドをbcryptでハッシュ化コスト10
            $passwordHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
            
            if ($passwordHash === false) {
                return ['success' => false, 'error' => 'パスワヌドのハッシュ化に倱敗したした'];
            }
            
            // デヌタベヌスに登録
            $conn = $this->db->connect();
            $sql = "INSERT INTO users (username, password_hash) VALUES (:username, :password_hash)";
            $stmt = $conn->prepare($sql);
            $stmt->execute([
                ':username' => $username,
                ':password_hash' => $passwordHash
            ]);
            
            $userId = $conn->lastInsertId();
            
            return [
                'success' => true,
                'user_id' => $userId,
                'message' => 'ナヌザヌ登録が完了したした'
            ];
            
        } catch (PDOException $e) {
            error_log("User registration error: " . $e->getMessage());
            return ['success' => false, 'error' => 'ナヌザヌ登録に倱敗したした'];
        }
    }
    
    /**
     * ナヌザヌ名の存圚確認
     * @param string $username
     * @return bool
     */
    private function usernameExists($username) {
        try {
            $conn = $this->db->connect();
            $sql = "SELECT COUNT(*) FROM users WHERE username = :username";
            $stmt = $conn->prepare($sql);
            $stmt->execute([':username' => $username]);
            return $stmt->fetchColumn() > 0;
        } catch (PDOException $e) {
            error_log("Username check error: " . $e->getMessage());
            return false;
        }
    }
    
    /**
     * ログむン認蚌
     * @param string $username ナヌザヌ名
     * @param string $password パスワヌド平文
     * @return array 成功時: ['success' => true, 'user' => [user data]], 倱敗時: ['success' => false, 'error' => message]
     */
    public function login($username, $password) {
        try {
            // 入力怜蚌
            if (empty($username) || empty($password)) {
                return ['success' => false, 'error' => 'ナヌザヌ名ずパスワヌドを入力しおください'];
            }
            
            // ナヌザヌ情報を取埗
            $conn = $this->db->connect();
            $sql = "SELECT id, username, password_hash, is_active FROM users WHERE username = :username";
            $stmt = $conn->prepare($sql);
            $stmt->execute([':username' => $username]);
            $user = $stmt->fetch();
            
            if (!$user) {
                return ['success' => false, 'error' => 'ナヌザヌ名たたはパスワヌドが正しくありたせん'];
            }
            
            // アカりントの有効性チェック
            if (!$user['is_active']) {
                return ['success' => false, 'error' => 'このアカりントは無効です'];
            }
            
            // パスワヌド怜蚌
            if (!password_verify($password, $user['password_hash'])) {
                return ['success' => false, 'error' => 'ナヌザヌ名たたはパスワヌドが正しくありたせん'];
            }
            
            // パスワヌドの再ハッシュが必芁かチェックbcryptのコストが倉曎された堎合
            if (password_needs_rehash($user['password_hash'], PASSWORD_BCRYPT, ['cost' => 10])) {
                $newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
                $updateSql = "UPDATE users SET password_hash = :password_hash WHERE id = :id";
                $updateStmt = $conn->prepare($updateSql);
                $updateStmt->execute([
                    ':password_hash' => $newHash,
                    ':id' => $user['id']
                ]);
            }
            
            // 最終ログむン日時を曎新
            $updateLoginSql = "UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = :id";
            $updateLoginStmt = $conn->prepare($updateLoginSql);
            $updateLoginStmt->execute([':id' => $user['id']]);
            
            // セッションに保存
            if (session_status() === PHP_SESSION_NONE) {
                session_start();
            }
            $_SESSION['user_id'] = $user['id'];
            $_SESSION['username'] = $user['username'];
            $_SESSION['logged_in'] = true;
            
            // セッションハむゞャック察策
            session_regenerate_id(true);
            
            return [
                'success' => true,
                'user' => [
                    'id' => $user['id'],
                    'username' => $user['username']
                ],
                'message' => 'ログむンしたした'
            ];
            
        } catch (PDOException $e) {
            error_log("Login error: " . $e->getMessage());
            return ['success' => false, 'error' => 'ログむンに倱敗したした'];
        }
    }
    
    /**
     * ログアりト
     */
    public function logout() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        
        $_SESSION = [];
        
        if (ini_get("session.use_cookies")) {
            $params = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $params["path"], $params["domain"],
                $params["secure"], $params["httponly"]
            );
        }
        
        session_destroy();
    }
    
    /**
     * ログむン状態の確認
     * @return bool
     */
    public function isLoggedIn() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        return isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true;
    }
    
    /**
     * 珟圚のナヌザヌ情報を取埗
     * @return array|null
     */
    public function getCurrentUser() {
        if (!$this->isLoggedIn()) {
            return null;
        }
        
        return [
            'id' => $_SESSION['user_id'] ?? null,
            'username' => $_SESSION['username'] ?? null
        ];
    }
    
    /**
     * ナヌザヌIDからナヌザヌ情報を取埗
     * @param int $userId
     * @return array|null
     */
    public function getUserById($userId) {
        try {
            $conn = $this->db->connect();
            $sql = "SELECT id, username, created_at, last_login, is_active FROM users WHERE id = :id";
            $stmt = $conn->prepare($sql);
            $stmt->execute([':id' => $userId]);
            return $stmt->fetch();
        } catch (PDOException $e) {
            error_log("Get user error: " . $e->getMessage());
            return null;
        }
    }
    
    /**
     * パスワヌド倉曎
     * @param int $userId
     * @param string $currentPassword 珟圚のパスワヌド
     * @param string $newPassword 新しいパスワヌド
     * @return array
     */
    public function changePassword($userId, $currentPassword, $newPassword) {
        try {
            // 珟圚のパスワヌドを怜蚌
            $conn = $this->db->connect();
            $sql = "SELECT password_hash FROM users WHERE id = :id";
            $stmt = $conn->prepare($sql);
            $stmt->execute([':id' => $userId]);
            $user = $stmt->fetch();
            
            if (!$user) {
                return ['success' => false, 'error' => 'ナヌザヌが芋぀かりたせん'];
            }
            
            if (!password_verify($currentPassword, $user['password_hash'])) {
                return ['success' => false, 'error' => '珟圚のパスワヌドが正しくありたせん'];
            }
            
            // 新しいパスワヌドの怜蚌
            if (strlen($newPassword) < 6) {
                return ['success' => false, 'error' => '新しいパスワヌドは6文字以䞊で入力しおください'];
            }
            
            // 新しいパスワヌドをハッシュ化
            $newHash = password_hash($newPassword, PASSWORD_BCRYPT, ['cost' => 10]);
            
            // パスワヌドを曎新
            $updateSql = "UPDATE users SET password_hash = :password_hash WHERE id = :id";
            $updateStmt = $conn->prepare($updateSql);
            $updateStmt->execute([
                ':password_hash' => $newHash,
                ':id' => $userId
            ]);
            
            return ['success' => true, 'message' => 'パスワヌドを倉曎したした'];
            
        } catch (PDOException $e) {
            error_log("Password change error: " . $e->getMessage());
            return ['success' => false, 'error' => 'パスワヌド倉曎に倱敗したした'];
        }
    }
}

TeratermMacroGenerator.php

📂 classes\TeratermMacroGenerator.php | 行数: 204 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * Teraterm マクロ (.ttl) ファむル生成クラス
 * 
 * SSH接続甚のTeratermマクロファむルを生成したす
 */
class TeratermMacroGenerator
{
    private string $hostAddr;
    private string $username;
    private string $password;
    private int $port;

    /**
     * コンストラクタ
     * 
     * @param string $hostAddr ホストアドレス (IPアドレスたたはホスト名)
     * @param string $username ナヌザヌ名
     * @param string $password パスワヌド
     * @param int $port SSHポヌト番号 (デフォルト: 22)
     */
    public function __construct(
        string $hostAddr,
        string $username,
        string $password,
        int $port = 22
    ) {
        $this->hostAddr = $hostAddr;
        $this->username = $username;
        $this->password = $password;
        $this->port = $port;
    }

    /**
     * Teratermマクロの内容を生成
     * 
     * @return string マクロファむルの内容
     */
    public function generate(): string
    {
        $macro = "; --- Configuration ---\n";
        $macro .= "HOSTADDR = '{$this->hostAddr}'\n";
        $macro .= "USERNAME = '{$this->username}'\n";
        $macro .= "PASSWORD = '{$this->password}'\n";
        $macro .= "\n";
        $macro .= "; --- Setup Connection String ---\n";
        $macro .= "COMMAND = HOSTADDR\n";
        $macro .= "strconcat COMMAND ':{$this->port} /ssh /2 /auth=password /user='\n";
        $macro .= "strconcat COMMAND USERNAME\n";
        $macro .= "strconcat COMMAND ' /passwd='\n";
        $macro .= "strconcat COMMAND PASSWORD\n";
        $macro .= "\n";
        $macro .= "; --- Execution ---\n";
        $macro .= "connect COMMAND\n";

        return $macro;
    }

    /**
     * マクロファむルを指定パスに保存
     * 
     * @param string $filePath 保存先ファむルパス
     * @return bool 成功した堎合true
     */
    public function saveToFile(string $filePath): bool
    {
        $content = $this->generate();
        return file_put_contents($filePath, $content) !== false;
    }

    /**
     * マクロファむルをダりンロヌド甚に出力
     * 
     * @param string $filename ダりンロヌドファむル名 (デフォルト: connection.ttl)
     */
    public function download(string $filename = 'connection.ttl'): void
    {
        $content = $this->generate();

        header('Content-Type: text/plain; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        header('Content-Length: ' . strlen($content));
        header('Cache-Control: no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        echo $content;
        exit;
    }

    /**
     * 耇数の接続情報から䞀括でマクロを生成
     * 
     * @param array $connections 接続情報の配列
     *                          [['host' => '...', 'user' => '...', 'pass' => '...', 'port' => 22], ...]
     * @param string $outputDir 出力ディレクトリ
     * @return array 生成されたファむルパスの配列
     */
    public static function generateBatch(array $connections, string $outputDir): array
    {
        $generatedFiles = [];

        if (!is_dir($outputDir)) {
            mkdir($outputDir, 0755, true);
        }

        foreach ($connections as $index => $conn) {
            $hostAddr = $conn['host'] ?? '';
            $username = $conn['user'] ?? '';
            $password = $conn['pass'] ?? '';
            $port = $conn['port'] ?? 22;

            if (empty($hostAddr) || empty($username)) {
                continue;
            }

            $generator = new self($hostAddr, $username, $password, $port);
            
            // ファむル名を生成 (ホスト名_ナヌザヌ名.ttl)
            $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $hostAddr);
            $filename = "{$safeName}_{$username}.ttl";
            $filePath = rtrim($outputDir, '/\\') . DIRECTORY_SEPARATOR . $filename;

            if ($generator->saveToFile($filePath)) {
                $generatedFiles[] = $filePath;
            }
        }

        return $generatedFiles;
    }

    /**
     * CSVファむルから接続情報を読み蟌んでマクロを生成
     * 
     * @param string $csvPath CSVファむルパス
     * @param string $outputDir 出力ディレクトリ
     * @param array $columnMapping カラムマッピング ['host' => 'IP', 'user' => 'User', 'pass' => 'Password']
     * @return array 生成されたファむルパスの配列
     */
    public static function generateFromCsv(
        string $csvPath,
        string $outputDir,
        array $columnMapping = ['host' => 'IP', 'user' => 'User', 'pass' => 'Password', 'port' => 'Port']
    ): array {
        if (!file_exists($csvPath)) {
            throw new Exception("CSVファむルが芋぀かりたせん: {$csvPath}");
        }

        $connections = [];
        $handle = fopen($csvPath, 'r');
        
        if ($handle === false) {
            throw new Exception("CSVファむルを開けたせん: {$csvPath}");
        }

        // ヘッダヌ行を取埗
        $headers = fgetcsv($handle);
        if ($headers === false) {
            fclose($handle);
            throw new Exception("CSVファむルが空です");
        }

        // カラムむンデックスを取埗
        $hostIndex = array_search($columnMapping['host'], $headers);
        $userIndex = array_search($columnMapping['user'], $headers);
        $passIndex = array_search($columnMapping['pass'], $headers);
        $portIndex = isset($columnMapping['port']) ? array_search($columnMapping['port'], $headers) : false;

        // デヌタ行を読み蟌み
        while (($row = fgetcsv($handle)) !== false) {
            $conn = [
                'host' => $hostIndex !== false ? ($row[$hostIndex] ?? '') : '',
                'user' => $userIndex !== false ? ($row[$userIndex] ?? '') : '',
                'pass' => $passIndex !== false ? ($row[$passIndex] ?? '') : '',
                'port' => $portIndex !== false && isset($row[$portIndex]) ? (int)$row[$portIndex] : 22
            ];

            if (!empty($conn['host']) && !empty($conn['user'])) {
                $connections[] = $conn;
            }
        }

        fclose($handle);

        return self::generateBatch($connections, $outputDir);
    }

    // Getter メ゜ッド
    public function getHostAddr(): string
    {
        return $this->hostAddr;
    }

    public function getUsername(): string
    {
        return $this->username;
    }

    public function getPort(): int
    {
        return $this->port;
    }
}

DeviceManager.php

📂 classes\DeviceManager.php | 行数: 1417 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * 装眮情報管理クラス
 */
class DeviceManager {
    private $database;
    
    public function __construct(Database $database) {
        $this->database = $database;
    }
    
    /**
     * 装眮情報テヌブルが存圚するかチェック
     * @return bool
     */
    public function deviceInfoTableExists() {
        return $this->database->tableExists('device_info');
    }
    
    /**
     * 装眮情報テヌブルを䜜成
     * @return bool
     * @throws Exception
     */
    public function createDeviceInfoTable() {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if ($isPgsql) {
            // PostgreSQL甹SQL
            $sql = "
                CREATE TABLE IF NOT EXISTS device_info (
                    primary_key VARCHAR(500) NOT NULL PRIMARY KEY,
                    service_name VARCHAR(100) NOT NULL,
                    device_type VARCHAR(100) NOT NULL,
                    device_name VARCHAR(100) NOT NULL,
                    login_ip VARCHAR(45),
                    username1 VARCHAR(100) NOT NULL,
                    password1 VARCHAR(255),
                    username2 VARCHAR(100),
                    password2 VARCHAR(255),
                    username3 VARCHAR(100),
                    password3 VARCHAR(255),
                    username4 VARCHAR(100),
                    password4 VARCHAR(255),
                    username5 VARCHAR(100),
                    password5 VARCHAR(255),
                    username6 VARCHAR(100),
                    password6 VARCHAR(255),
                    username7 VARCHAR(100),
                    password7 VARCHAR(255),
                    username8 VARCHAR(100),
                    password8 VARCHAR(255),
                    username9 VARCHAR(100),
                    password9 VARCHAR(255),
                    username10 VARCHAR(100),
                    password10 VARCHAR(255),
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ";
            $this->database->execute($sql);
            // むンデックス䜜成
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_service_device_type ON device_info (service_name, device_type)");
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_device_info ON device_info (service_name, device_type, device_name, username1)");
        } else {
            // MySQL甹SQL
            $sql = "
                CREATE TABLE IF NOT EXISTS device_info (
                    primary_key VARCHAR(500) NOT NULL PRIMARY KEY COMMENT 'サヌビス名_装眮皮別名_装眮名_ナヌザ名の耇合キヌ',
                    service_name VARCHAR(100) NOT NULL COMMENT 'サヌビス名',
                    device_type VARCHAR(100) NOT NULL COMMENT '装眮皮別',
                    device_name VARCHAR(100) NOT NULL COMMENT '装眮名称',
                    login_ip VARCHAR(45) COMMENT 'ログむンIP',
                    username1 VARCHAR(100) NOT NULL COMMENT 'ナヌザヌ名1',
                    password1 VARCHAR(255) COMMENT 'パスワヌド1',
                    username2 VARCHAR(100) COMMENT 'ナヌザヌ名2',
                    password2 VARCHAR(255) COMMENT 'パスワヌド2',
                    username3 VARCHAR(100) COMMENT 'ナヌザヌ名3',
                    password3 VARCHAR(255) COMMENT 'パスワヌド3',
                    username4 VARCHAR(100) COMMENT 'ナヌザヌ名4',
                    password4 VARCHAR(255) COMMENT 'パスワヌド4',
                    username5 VARCHAR(100) COMMENT 'ナヌザヌ名5',
                    password5 VARCHAR(255) COMMENT 'パスワヌド5',
                    username6 VARCHAR(100) COMMENT 'ナヌザヌ名6',
                    password6 VARCHAR(255) COMMENT 'パスワヌド6',
                    username7 VARCHAR(100) COMMENT 'ナヌザヌ名7',
                    password7 VARCHAR(255) COMMENT 'パスワヌド7',
                    username8 VARCHAR(100) COMMENT 'ナヌザヌ名8',
                    password8 VARCHAR(255) COMMENT 'パスワヌド8',
                    username9 VARCHAR(100) COMMENT 'ナヌザヌ名9',
                    password9 VARCHAR(255) COMMENT 'パスワヌド9',
                    username10 VARCHAR(100) COMMENT 'ナヌザヌ名10',
                    password10 VARCHAR(255) COMMENT 'パスワヌド10',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '䜜成日時',
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時',
                    INDEX idx_service_device_type (service_name, device_type),
                    INDEX idx_device_info (service_name, device_type, device_name, username1)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='装眮基本情報テヌブル'
            ";
            $this->database->execute($sql);
        }
        
        return true;
    }
    
    /**
     * 動的テヌブルが存圚するかチェック
     * @param string $tableName
     * @return bool
     */
    public function dynamicTableExists($tableName) {
        return $this->database->tableExists($tableName);
    }
    
    /**
     * 動的テヌブルを䜜成
     * @param string $tableName
     * @param string $primaryKeyColumn
     * @param array $extendedColumns
     * @return bool
     * @throws Exception
     */
    public function createDynamicTable($tableName, $primaryKeyColumn, $extendedColumns) {
        // テヌブル名のみサニタむズカラム名は日本語を保持
        $tableName = sanitizeTableName($tableName);
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        $columnDefinitions = [];
        
        // MySQL/PostgreSQLで適切なクォヌト文字を䜿甚
        $quote = $isPgsql ? '"' : '`';
        
        $columnDefinitions[] = "{$quote}{$primaryKeyColumn}{$quote} VARCHAR(500) NOT NULL PRIMARY KEY";
        
        foreach ($extendedColumns as $column) {
            // カラム名は日本語のたた䜿甚、適切なクォヌトで゚スケヌプ
            $columnDefinitions[] = "{$quote}{$column}{$quote} TEXT";
        }
        
        $columnDefinitions[] = "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP";
        
        if ($isPgsql) {
            $columnDefinitions[] = "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP";
        } else {
            $columnDefinitions[] = "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時'";
        }
        
        if ($isPgsql) {
            $sql = "
                CREATE TABLE IF NOT EXISTS \"{$tableName}\" (
                    " . implode(",\n                    ", $columnDefinitions) . "
                )
            ";
        } else {
            $sql = "
                CREATE TABLE IF NOT EXISTS `{$tableName}` (
                    " . implode(",\n                    ", $columnDefinitions) . "
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='動的テヌブル: {$tableName}'
            ";
        }
        
        try {
            error_log("Creating dynamic table SQL: " . $sql);
            $this->database->execute($sql);
            return true;
        } catch (Exception $e) {
            error_log("Dynamic table creation error: " . $e->getMessage());
            error_log("SQL: " . $sql);
            throw new Exception("動的テヌブル '{$tableName}' の䜜成に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮情報を挿入たたは曎新動的カラム察応
     * @param array $deviceData
     * @param array $additionalData 远加カラムデヌタ
     * @return bool
     * @throws Exception
     */
    public function insertOrUpdateDeviceInfo($deviceData, $additionalData = []) {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        // 基本カラム
        $baseColumns = ['primary_key', 'service_name', 'device_type', 'device_name', 'login_ip', 'username1', 'password1'];
        for ($i = 2; $i <= 10; $i++) {
            $baseColumns[] = "username{$i}";
            $baseColumns[] = "password{$i}";
        }
        
        $columns = $baseColumns;
        $placeholders = array_map(function($col) { return ":{$col}"; }, $columns);
        $updateColumns = array_diff($columns, ['primary_key']);
        
        $params = $deviceData;
        
        // 䜜成者・曎新者の情報を远加
        require_once __DIR__ . '/../includes/auth_helper.php';
        $currentUser = getLoggedInUsername();
        
        // created_byは新芏䜜成時のみ蚭定ON DUPLICATE KEY UPDATEでは曎新されない
        if ($currentUser) {
            $columns[] = 'created_by';
            $placeholders[] = ':created_by';
            $params['created_by'] = $currentUser;
            
            // updated_byは垞に蚭定
            $columns[] = 'updated_by';
            $placeholders[] = ':updated_by';
            $updateColumns[] = 'updated_by';
            $params['updated_by'] = $currentUser;
        }
        
        // device_infoテヌブルの既存カラムを取埗
        $existingColumns = $this->getTableColumns('device_info');
        
        // 远加デヌタがある堎合、device_infoに存圚するカラムのみ远加
        foreach ($additionalData as $column => $value) {
            if (in_array($column, $existingColumns) && !in_array($column, $columns)) {
                $columns[] = $isPgsql ? "\"{$column}\"" : "`{$column}`";
                $placeholders[] = ":{$column}";
                $updateColumns[] = $column;
                $params[$column] = $value;
            }
        }
        
        if ($isPgsql) {
            // PostgreSQL甹: ON CONFLICT ... DO UPDATE
            $updateClauses = [];
            foreach ($updateColumns as $col) {
                $quotedCol = in_array($col, ['service_name', 'device_type', 'device_name', 'login_ip', 'created_by', 'updated_by']) ||
                             preg_match('/^(username|password)\d+$/', $col)
                    ? $col 
                    : "\"{$col}\"";
                $updateClauses[] = "{$quotedCol} = EXCLUDED.{$quotedCol}";
            }
            
            $sql = "
                INSERT INTO device_info 
                (" . implode(", ", $columns) . ")
                VALUES (" . implode(", ", $placeholders) . ")
                ON CONFLICT (primary_key) DO UPDATE SET
                    " . implode(",\n                    ", $updateClauses) . ",
                    updated_at = CURRENT_TIMESTAMP
            ";
        } else {
            // MySQL甹: ON DUPLICATE KEY UPDATE
            $updateClauses = [];
            foreach ($updateColumns as $col) {
                $quotedCol = in_array($col, ['service_name', 'device_type', 'device_name', 'login_ip', 'created_by', 'updated_by']) ||
                             preg_match('/^(username|password)\d+$/', $col)
                    ? $col 
                    : "`{$col}`";
                $updateClauses[] = "{$quotedCol} = VALUES({$quotedCol})";
            }
            
            $sql = "
                INSERT INTO device_info 
                (" . implode(", ", $columns) . ")
                VALUES (" . implode(", ", $placeholders) . ")
                ON DUPLICATE KEY UPDATE
                    " . implode(",\n                    ", $updateClauses) . ",
                    updated_at = CURRENT_TIMESTAMP
            ";
        }
        
        try {
            $this->database->execute($sql, $params);
            return true;
        } catch (Exception $e) {
            throw new Exception("装眮情報の挿入に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 動的テヌブルにデヌタを挿入たたは曎新
     * @param string $tableName
     * @param array $data
     * @return bool
     * @throws Exception
     */
    public function insertOrUpdateDynamicData($tableName, $data) {
        $tableName = sanitizeTableName($tableName);
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if (empty($data)) {
            return true;
        }
        
        // カラム名ずプレヌスホルダヌを準備
        $columns = [];
        $placeholders = [];
        $updateClauses = [];
        $params = [];
        $placeholderIndex = 0;
        $primaryKeyColumn = null;
        
        // 適切なクォヌト文字を遞択
        $quote = $isPgsql ? '"' : '`';
        
        foreach ($data as $key => $value) {
            if ($placeholderIndex === 0) {
                $primaryKeyColumn = $key; // 最初のカラムが䞻キヌ
            }
            
            // カラム名は日本語のたた䜿甚適切なクォヌトで゚スケヌプ
            $columns[] = "{$quote}{$key}{$quote}";
            
            // プレヌスホルダヌ名は英数字のみparam0, param1, ...
            $placeholder = "param" . $placeholderIndex;
            $placeholders[] = ":{$placeholder}";
            $params[$placeholder] = $value;
            
            // 䞻キヌ以倖の曎新句を䜜成
            if ($key !== $primaryKeyColumn) {
                if ($isPgsql) {
                    $updateClauses[] = "{$quote}{$key}{$quote} = EXCLUDED.{$quote}{$key}{$quote}";
                } else {
                    $updateClauses[] = "{$quote}{$key}{$quote} = VALUES({$quote}{$key}{$quote})";
                }
            }
            
            $placeholderIndex++;
        }
        
        if ($isPgsql) {
            // PostgreSQL甹
            $tableQuote = '"';
            $sql = "
                INSERT INTO {$tableQuote}{$tableName}{$tableQuote} 
                (" . implode(", ", $columns) . ")
                VALUES (" . implode(", ", $placeholders) . ")
            ";
            
            if (!empty($updateClauses)) {
                $sql .= " ON CONFLICT ({$quote}{$primaryKeyColumn}{$quote}) DO UPDATE SET " 
                     . implode(", ", $updateClauses) 
                     . ", updated_at = CURRENT_TIMESTAMP";
            }
        } else {
            // MySQL甹
            $sql = "
                INSERT INTO `{$tableName}` 
                (" . implode(", ", $columns) . ")
                VALUES (" . implode(", ", $placeholders) . ")
            ";
            
            if (!empty($updateClauses)) {
                $sql .= " ON DUPLICATE KEY UPDATE " . implode(", ", $updateClauses) . ", updated_at = CURRENT_TIMESTAMP";
            }
        }
        
        try {
            error_log("Dynamic insert SQL: " . $sql);
            error_log("Dynamic insert params: " . json_encode($params, JSON_UNESCAPED_UNICODE));
            
            $this->database->execute($sql, $params);
            return true;
        } catch (Exception $e) {
            error_log("Dynamic insert error: " . $e->getMessage());
            error_log("SQL: " . $sql);
            error_log("Params: " . json_encode($params, JSON_UNESCAPED_UNICODE));
            throw new Exception("動的テヌブル '{$tableName}' ぞのデヌタ挿入に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * CSVデヌタを䞀括凊理
     * @param CsvProcessor $csvProcessor
     * @return array 凊理結果
     * @throws Exception
     */
    public function processCsvData(CsvProcessor $csvProcessor) {
        $results = [
            'success' => false,
            'device_info_count' => 0,
            'dynamic_tables_created' => [],
            'dynamic_data_count' => 0,
            'columns_added' => [],
            'errors' => []
        ];
        
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        try {
            // PostgreSQLの堎合、゚ラヌハンドリングを改善
            if ($isPgsql) {
                // 各ステップを個別のトランザクションで凊理
                error_log("PostgreSQL mode: Using individual transactions");
            }
            
            // トランザクション開始
            $this->database->beginTransaction();
            
            // 装眮情報テヌブルの存圚確認DatabaseInitializerで䜜成枈みのはず
            if (!$this->deviceInfoTableExists()) {
                error_log("Warning: device_info table not found. Creating now...");
                $this->createDeviceInfoTable();
            }
            
            $data = $csvProcessor->getData();
            $extendedColumns = $csvProcessor->getExtendedColumns();
            
            // device_infoテヌブルの既存カラムを取埗
            $deviceInfoExistingColumns = $this->getTableColumns('device_info');
            
            // 動的テヌブルごずに必芁なカラムを刀定
            $dynamicTableColumns = [];
            foreach ($data as $row) {
                $tableName = $csvProcessor->generateTableName($row);
                
                if (!isset($dynamicTableColumns[$tableName])) {
                    $dynamicTableColumns[$tableName] = [];
                }
                
                // 各拡匵カラムをdevice_infoたたは動的テヌブルに振り分け
                foreach ($extendedColumns as $column) {
                    // device_infoに存圚しないカラムのみ動的テヌブルぞ
                    if (!in_array($column, $deviceInfoExistingColumns)) {
                        if (!in_array($column, $dynamicTableColumns[$tableName])) {
                            $dynamicTableColumns[$tableName][] = $column;
                        }
                    }
                }
            }
            
            // 動的テヌブルを䜜成たたはカラム远加
            foreach ($dynamicTableColumns as $tableName => $columns) {
                if (!$this->dynamicTableExists($tableName)) {
                    // テヌブル新芏䜜成
                    $primaryKeyColumn = $csvProcessor->generatePrimaryKeyColumnName($data[0]);
                    $this->createDynamicTable($tableName, $primaryKeyColumn, $columns);
                    $results['dynamic_tables_created'][] = $tableName;
                } else {
                    // 既存テヌブルに䞍足カラムを远加
                    $existingColumns = $this->getTableColumns($tableName);
                    foreach ($columns as $column) {
                        if (!in_array($column, $existingColumns)) {
                            $this->addColumnToDynamicTable($tableName, $column);
                            $results['columns_added'][] = "{$tableName}.{$column}";
                        }
                    }
                }
            }
            
            // リレヌションテヌブルの存圚確認DatabaseInitializerで䜜成枈みのはず
            if (!$this->relationTableExists()) {
                error_log("Warning: service_device_type_relations table not found. Creating now...");
                $this->createRelationTable();
            }
            
            // ここたでの倉曎をコミットPostgreSQLの堎合、テヌブル定矩倉曎を確定
            if ($isPgsql) {
                $this->database->commit();
                error_log("Table structure changes committed");
            } else {
                // MySQLの堎合は1぀のトランザクションで凊理を続ける
            }
            
            // デヌタを凊理
            foreach ($data as $rowIndex => $row) {
                // PostgreSQLの堎合は各行ごずに新しいトランザクションを開始
                if ($isPgsql) {
                    $this->database->beginTransaction();
                    error_log("Started new transaction for row {$rowIndex}");
                }
                
                error_log("Processing row {$rowIndex}: " . json_encode($row, JSON_UNESCAPED_UNICODE));
                
                try {
                    // 装眮情報テヌブルに挿入拡匵カラムも含む
                    $deviceInfo = $csvProcessor->convertToDeviceInfo($row);
                    error_log("Device info data: " . json_encode($deviceInfo, JSON_UNESCAPED_UNICODE));
                    
                    // 拡匵カラムの䞭でdevice_infoに存圚するカラムを抜出
                    $additionalDeviceInfoData = [];
                    foreach ($extendedColumns as $column) {
                        if (in_array($column, $deviceInfoExistingColumns)) {
                            $additionalDeviceInfoData[$column] = isset($row[$column]) ? $row[$column] : null;
                        }
                    }
                    error_log("Additional device info data: " . json_encode($additionalDeviceInfoData, JSON_UNESCAPED_UNICODE));
                    
                    $this->insertOrUpdateDeviceInfo($deviceInfo, $additionalDeviceInfoData);
                    error_log("Successfully inserted device_info for row {$rowIndex}");
                    $results['device_info_count']++;
                    
                } catch (Exception $e) {
                    error_log("Row {$rowIndex} - device_info insert error: " . $e->getMessage());
                    error_log("Stack trace: " . $e->getTraceAsString());
                    
                    // PostgreSQLの堎合はロヌルバックしお次の行ぞ
                    if ($isPgsql) {
                        $this->database->rollback();
                        error_log("Transaction rolled back for row {$rowIndex}");
                    }
                    
                    throw new Exception("行" . ($rowIndex + 2) . "のdevice_info登録で゚ラヌ: " . $e->getMessage());
                }
                
                // サヌビス名ず装眮皮別のリレヌションを登録
                // PostgreSQLの堎合、トランザクション管理を簡玠化するため、
                // upload.php偎でたずめお登録するのでここではスキップ
                if (!$isPgsql) {
                    try {
                        $this->registerServiceDeviceTypeRelation(
                            $row['サヌビス名'],
                            $row['装眮皮別'],
                            'CSV自動登録'
                        );
                        error_log("Successfully registered relation for row {$rowIndex}");
                    } catch (Exception $e) {
                        // リレヌション登録゚ラヌはログに蚘録するが凊理は継続
                        error_log("リレヌション登録゚ラヌ: " . $e->getMessage());
                    }
                } else {
                    error_log("Skipping relation registration in PostgreSQL mode (will be done in upload.php)");
                }
                
                // 動的テヌブルに挿入
                try {
                    $tableName = $csvProcessor->generateTableName($row);
                    error_log("Dynamic table name: {$tableName}");
                    
                    // 動的テヌブル甚のデヌタを䜜成
                    $dynamicData = [];
                    $dynamicData[$csvProcessor->generatePrimaryKeyColumnName($row)] = $csvProcessor->generatePrimaryKey($row);
                    
                    foreach ($extendedColumns as $column) {
                        // device_infoに存圚するカラムはdevice_infoに、それ以倖は動的テヌブルに
                        if (!in_array($column, $deviceInfoExistingColumns)) {
                            // 動的テヌブルのカラムずしお登録
                            $dynamicData[$column] = isset($row[$column]) ? $row[$column] : null;
                        }
                    }
                    
                    error_log("Dynamic data: " . json_encode($dynamicData, JSON_UNESCAPED_UNICODE));
                    
                    // 動的テヌブルに挿入動的テヌブル甚のデヌタが存圚する堎合のみ
                    if (count($dynamicData) > 1) { // primary_key以倖のカラムがある堎合
                        $this->insertOrUpdateDynamicData($tableName, $dynamicData);
                        error_log("Successfully inserted dynamic data for row {$rowIndex}");
                        $results['dynamic_data_count']++;
                    } else {
                        error_log("No dynamic data to insert for row {$rowIndex}");
                    }
                    
                } catch (Exception $e) {
                    error_log("Row {$rowIndex} - dynamic table insert error: " . $e->getMessage());
                    error_log("Stack trace: " . $e->getTraceAsString());
                    
                    // PostgreSQLの堎合はロヌルバックしお次の行ぞ
                    if ($isPgsql) {
                        $this->database->rollback();
                        error_log("Transaction rolled back for row {$rowIndex}");
                    }
                    
                    throw new Exception("行" . ($rowIndex + 2) . "の動的テヌブル登録で゚ラヌ: " . $e->getMessage());
                }
                
                // PostgreSQLの堎合は各行の凊理埌にコミット
                if ($isPgsql) {
                    $this->database->commit();
                    error_log("Transaction committed for row {$rowIndex}");
                }
            }
            
            // MySQLの堎合のみ最終コミットPostgreSQLは各行でコミット枈み
            if (!$isPgsql) {
                $this->database->commit();
            }
            $results['success'] = true;
            
        } catch (Exception $e) {
            // ロヌルバックトランザクションがアクティブな堎合のみ
            try {
                if ($this->database->inTransaction()) {
                    $this->database->rollBack();
                    error_log("Transaction rolled back due to error");
                }
            } catch (Exception $rollbackEx) {
                error_log("Rollback failed: " . $rollbackEx->getMessage());
            }
            $results['errors'][] = $e->getMessage();
            throw $e;
        }
        
        return $results;
    }
    
    /**
     * 装眮情報を怜玢
     * @param string $serviceName
     * @param string $deviceType
     * @param int $limit
     * @param int $offset
     * @return array
     */
    public function searchDevices($serviceName = null, $deviceType = null, $limit = 100, $offset = 0) {
        $whereConditions = [];
        $params = [];
        
        if ($serviceName !== null && $serviceName !== '') {
            $whereConditions[] = "service_name LIKE :service_name";
            $params['service_name'] = '%' . $serviceName . '%';
        }
        
        if ($deviceType !== null && $deviceType !== '') {
            $whereConditions[] = "device_type LIKE :device_type";
            $params['device_type'] = '%' . $deviceType . '%';
        }
        
        $whereClause = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
        
        $sql = "
            SELECT * FROM device_info 
            {$whereClause}
            ORDER BY service_name, device_type, device_name, username1
            LIMIT :limit OFFSET :offset
        ";
        
        $params['limit'] = $limit;
        $params['offset'] = $offset;
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return $stmt->fetchAll();
        } catch (Exception $e) {
            throw new Exception("装眮情報の怜玢に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮情報の総数を取埗
     * @param string $serviceName
     * @param string $deviceType
     * @return int
     */
    public function countDevices($serviceName = null, $deviceType = null) {
        $whereConditions = [];
        $params = [];
        
        if ($serviceName !== null && $serviceName !== '') {
            $whereConditions[] = "service_name LIKE :service_name";
            $params['service_name'] = '%' . $serviceName . '%';
        }
        
        if ($deviceType !== null && $deviceType !== '') {
            $whereConditions[] = "device_type LIKE :device_type";
            $params['device_type'] = '%' . $deviceType . '%';
        }
        
        $whereClause = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
        
        $sql = "SELECT COUNT(*) FROM device_info {$whereClause}";
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return (int)$stmt->fetchColumn();
        } catch (Exception $e) {
            throw new Exception("装眮数の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 動的テヌブルの䞀芧を取埗
     * @return array
     */
    public function getDynamicTables() {
        $sql = "
            SELECT table_name 
            FROM information_schema.tables 
            WHERE table_schema = DATABASE() 
            AND table_name != 'device_info'
            ORDER BY table_name
        ";
        
        try {
            $stmt = $this->database->execute($sql);
            return $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Exception $e) {
            throw new Exception("動的テヌブル䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * サヌビス名の䞀芧を取埗
     * @return array
     */
    public function getServiceNames() {
        $sql = "
            SELECT DISTINCT service_name 
            FROM device_info 
            ORDER BY service_name
        ";
        
        try {
            $stmt = $this->database->execute($sql);
            return $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Exception $e) {
            throw new Exception("サヌビス名䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 指定サヌビス名の装眮皮別䞀芧を取埗
     * @param string|null $serviceName
     * @return array
     */
    public function getDeviceTypes($serviceName = null) {
        $sql = "
            SELECT DISTINCT device_type 
            FROM device_info
        ";
        
        $params = [];
        if ($serviceName !== null && $serviceName !== '') {
            $sql .= " WHERE service_name = :service_name";
            $params['service_name'] = $serviceName;
        }
        
        $sql .= " ORDER BY device_type";
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Exception $e) {
            throw new Exception("装眮皮別䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮情報を詳现怜玢
     * @param string|null $serviceName
     * @param string|null $deviceType
     * @param string|null $deviceName
     * @param int $limit
     * @param int $offset
     * @return array
     */
    public function searchDevicesAdvanced($serviceName = null, $deviceType = null, $deviceName = null, $limit = 50, $offset = 0) {
        $whereConditions = [];
        $params = [];
        
        if ($serviceName !== null && $serviceName !== '') {
            $whereConditions[] = "service_name = :service_name";
            $params['service_name'] = $serviceName;
        }
        
        if ($deviceType !== null && $deviceType !== '') {
            $whereConditions[] = "device_type = :device_type";
            $params['device_type'] = $deviceType;
        }
        
        if ($deviceName !== null && $deviceName !== '') {
            $whereConditions[] = "device_name LIKE :device_name";
            $params['device_name'] = '%' . $deviceName . '%';
        }
        
        $whereClause = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
        
        $sql = "
            SELECT 
                primary_key,
                service_name,
                device_type,
                device_name,
                login_ip,
                username1,
                password1,
                username2,
                password2,
                username3,
                password3,
                username4,
                password4,
                username5,
                password5,
                username6,
                password6,
                username7,
                password7,
                username8,
                password8,
                username9,
                password9,
                username10,
                password10,
                created_by,
                updated_by,
                created_at,
                updated_at
            FROM device_info 
            {$whereClause}
            ORDER BY service_name, device_type, device_name, username1
            LIMIT :limit OFFSET :offset
        ";
        
        $params['limit'] = $limit;
        $params['offset'] = $offset;
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return $stmt->fetchAll();
        } catch (Exception $e) {
            throw new Exception("装眮情報の詳现怜玢に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 詳现怜玢の結果件数を取埗
     * @param string|null $serviceName
     * @param string|null $deviceType
     * @param string|null $deviceName
     * @return int
     */
    public function countDevicesAdvanced($serviceName = null, $deviceType = null, $deviceName = null) {
        $whereConditions = [];
        $params = [];
        
        if ($serviceName !== null && $serviceName !== '') {
            $whereConditions[] = "service_name = :service_name";
            $params['service_name'] = $serviceName;
        }
        
        if ($deviceType !== null && $deviceType !== '') {
            $whereConditions[] = "device_type = :device_type";
            $params['device_type'] = $deviceType;
        }
        
        if ($deviceName !== null && $deviceName !== '') {
            $whereConditions[] = "device_name LIKE :device_name";
            $params['device_name'] = '%' . $deviceName . '%';
        }
        
        $whereClause = !empty($whereConditions) ? 'WHERE ' . implode(' AND ', $whereConditions) : '';
        
        $sql = "SELECT COUNT(*) FROM device_info {$whereClause}";
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return (int)$stmt->fetchColumn();
        } catch (Exception $e) {
            throw new Exception("装眮数の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮統蚈情報を取埗
     * @return array
     */
    public function getDeviceStatistics() {
        try {
            $statistics = [
                'total_devices' => 0,
                'total_services' => 0,
                'total_device_types' => 0,
                'total_combinations' => 0,
                'total_relations' => 0
            ];
            
            // device_infoテヌブルが存圚する堎合の統蚈
            if ($this->database->tableExists('device_info')) {
                $sql = "
                    SELECT 
                        COUNT(*) as total_devices,
                        COUNT(DISTINCT service_name) as total_services,
                        COUNT(DISTINCT device_type) as total_device_types,
                        COUNT(DISTINCT CONCAT(service_name, '_', device_type)) as total_combinations
                    FROM device_info
                ";
                $stmt = $this->database->execute($sql);
                $result = $stmt->fetch();
                
                $statistics['total_devices'] = (int)$result['total_devices'];
                $statistics['total_services'] = (int)$result['total_services'];
                $statistics['total_device_types'] = (int)$result['total_device_types'];
                $statistics['total_combinations'] = (int)$result['total_combinations'];
            }
            
            // リレヌション数
            if ($this->relationTableExists()) {
                $sql = "SELECT COUNT(*) as count FROM service_device_type_relations WHERE is_active = 1";
                $stmt = $this->database->execute($sql);
                $result = $stmt->fetch();
                $statistics['total_relations'] = (int)$result['count'];
            }
            
            return $statistics;
            
        } catch (Exception $e) {
            error_log("Get statistics error: " . $e->getMessage());
            return [
                'total_devices' => 0,
                'total_services' => 0,
                'total_device_types' => 0,
                'total_combinations' => 0,
                'total_relations' => 0
            ];
        }
    }
    
    /**
     * リレヌションテヌブルが存圚するかチェック
     * @return bool
     */
    public function relationTableExists() {
        return $this->database->tableExists('service_device_type_relations');
    }
    
    /**
     * リレヌションテヌブルを䜜成
     * @return bool
     * @throws Exception
     */
    public function createRelationTable() {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if ($isPgsql) {
            // PostgreSQL甹SQL
            $sql = "
                CREATE TABLE IF NOT EXISTS service_device_type_relations (
                    id SERIAL PRIMARY KEY,
                    service_name VARCHAR(100) NOT NULL,
                    device_type VARCHAR(100) NOT NULL,
                    description TEXT,
                    is_active SMALLINT DEFAULT 1,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    UNIQUE (service_name, device_type)
                )
            ";
            $this->database->execute($sql);
            // むンデックス䜜成
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_service_name ON service_device_type_relations (service_name)");
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_device_type ON service_device_type_relations (device_type)");
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_active ON service_device_type_relations (is_active)");
        } else {
            // MySQL甹SQL
            $sql = "
                CREATE TABLE IF NOT EXISTS service_device_type_relations (
                    id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
                    service_name VARCHAR(100) NOT NULL COMMENT 'サヌビス名',
                    device_type VARCHAR(100) NOT NULL COMMENT '装眮皮別',
                    description TEXT COMMENT '説明',
                    is_active TINYINT(1) DEFAULT 1 COMMENT '有効フラグ(1:有効, 0:無効)',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '䜜成日時',
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時',
                    UNIQUE KEY unique_service_device_type (service_name, device_type),
                    INDEX idx_service_name (service_name),
                    INDEX idx_device_type (device_type),
                    INDEX idx_active (is_active)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='サヌビス名ず装眮皮別のリレヌションテヌブル'
            ";
            $this->database->execute($sql);
        }
        
        return true;
    }
    
    /**
     * サヌビス名ず装眮皮別のリレヌションを登録
     * @param string $serviceName
     * @param string $deviceType
     * @param string $description
     * @return bool
     * @throws Exception
     */
    public function registerServiceDeviceTypeRelation($serviceName, $deviceType, $description = null) {
        // リレヌションテヌブルが存圚しない堎合は䜜成
        if (!$this->relationTableExists()) {
            $this->createRelationTable();
        }
        
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if ($isPgsql) {
            // PostgreSQL甹
            $sql = "
                INSERT INTO service_device_type_relations 
                (service_name, device_type, description) 
                VALUES (:service_name, :device_type, :description)
                ON CONFLICT (service_name, device_type) 
                DO UPDATE SET
                    description = EXCLUDED.description,
                    is_active = TRUE,
                    updated_at = CURRENT_TIMESTAMP
            ";
        } else {
            // MySQL甹
            $sql = "
                INSERT INTO service_device_type_relations 
                (service_name, device_type, description) 
                VALUES (:service_name, :device_type, :description)
                ON DUPLICATE KEY UPDATE
                    description = VALUES(description),
                    is_active = 1,
                    updated_at = CURRENT_TIMESTAMP
            ";
        }
        
        $params = [
            'service_name' => $serviceName,
            'device_type' => $deviceType,
            'description' => $description
        ];
        
        try {
            $this->database->execute($sql, $params);
            return true;
        } catch (Exception $e) {
            error_log("Relation registration error: " . $e->getMessage());
            error_log("SQL: " . $sql);
            error_log("Params: " . json_encode($params, JSON_UNESCAPED_UNICODE));
            throw new Exception("リレヌション登録に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 指定サヌビス名の装眮皮別䞀芧をリレヌションテヌブルから取埗
     * @param string|null $serviceName
     * @return array
     */
    public function getDeviceTypesFromRelation($serviceName = null) {
        // リレヌションテヌブルが存圚しない堎合は埓来の方法
        if (!$this->relationTableExists()) {
            return $this->getDeviceTypes($serviceName);
        }
        
        $sql = "
            SELECT DISTINCT device_type 
            FROM service_device_type_relations
            WHERE is_active = 1
        ";
        
        $params = [];
        if ($serviceName !== null && $serviceName !== '') {
            $sql .= " AND service_name = :service_name";
            $params['service_name'] = $serviceName;
        }
        
        $sql .= " ORDER BY device_type";
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Exception $e) {
            throw new Exception("装眮皮別䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * リレヌションテヌブルから党サヌビス名を取埗
     * @return array
     */
    public function getServiceNamesFromRelation() {
        // リレヌションテヌブルが存圚しない堎合は埓来の方法
        if (!$this->relationTableExists()) {
            return $this->getServiceNames();
        }
        
        $sql = "
            SELECT DISTINCT service_name 
            FROM service_device_type_relations
            WHERE is_active = 1
            ORDER BY service_name
        ";
        
        try {
            $stmt = $this->database->execute($sql);
            return $stmt->fetchAll(PDO::FETCH_COLUMN);
        } catch (Exception $e) {
            throw new Exception("サヌビス名䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * リレヌションの存圚チェック
     * @param string $serviceName
     * @param string $deviceType
     * @return bool
     */
    public function checkRelationExists($serviceName, $deviceType) {
        // リレヌションテヌブルが存圚しない堎合は垞にtrueを返す
        if (!$this->relationTableExists()) {
            return true;
        }
        
        $sql = "
            SELECT COUNT(*) 
            FROM service_device_type_relations
            WHERE service_name = :service_name 
            AND device_type = :device_type 
            AND is_active = 1
        ";
        
        $params = [
            'service_name' => $serviceName,
            'device_type' => $deviceType
        ];
        
        try {
            $stmt = $this->database->execute($sql, $params);
            return $stmt->fetchColumn() > 0;
        } catch (Exception $e) {
            throw new Exception("リレヌション存圚確認に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 既存のdevice_infoからリレヌションを自動構築
     * @return array 構築結果
     * @throws Exception
     */
    public function buildRelationsFromExistingData() {
        // リレヌションテヌブルの䜜成
        if (!$this->relationTableExists()) {
            $this->createRelationTable();
        }
        
        // 既存のサヌビス名ず装眮皮別の組み合わせを取埗
        $sql = "
            SELECT DISTINCT 
                service_name, 
                device_type,
                COUNT(*) as device_count
            FROM device_info 
            GROUP BY service_name, device_type
            ORDER BY service_name, device_type
        ";
        
        try {
            $stmt = $this->database->execute($sql);
            $combinations = $stmt->fetchAll();
            
            $registered = 0;
            $errors = [];
            
            foreach ($combinations as $combination) {
                try {
                    $description = "装眮数: {$combination['device_count']}台 (自動構築)";
                    $this->registerServiceDeviceTypeRelation(
                        $combination['service_name'],
                        $combination['device_type'],
                        $description
                    );
                    $registered++;
                } catch (Exception $e) {
                    $errors[] = "{$combination['service_name']} - {$combination['device_type']}: " . $e->getMessage();
                }
            }
            
            return [
                'success' => true,
                'registered' => $registered,
                'total_combinations' => count($combinations),
                'errors' => $errors
            ];
            
        } catch (Exception $e) {
            throw new Exception("リレヌション自動構築に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 党リレヌション䞀芧を取埗管理甚
     * @return array
     */
    public function getAllRelations() {
        if (!$this->relationTableExists()) {
            return [];
        }
        
        $sql = "
            SELECT 
                id,
                service_name,
                device_type,
                description,
                is_active,
                created_at,
                updated_at
            FROM service_device_type_relations
            ORDER BY service_name, device_type
        ";
        
        try {
            $stmt = $this->database->execute($sql);
            return $stmt->fetchAll();
        } catch (Exception $e) {
            throw new Exception("リレヌション䞀芧の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * テヌブルのカラム䞀芧を取埗
     * @param string $tableName
     * @return array
     */
    public function getTableColumns($tableName) {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if ($isPgsql) {
            $sql = "
                SELECT column_name 
                FROM information_schema.columns 
                WHERE table_name = :table_name
                ORDER BY ordinal_position
            ";
            $params = ['table_name' => $tableName];
        } else {
            $sql = "
                SELECT COLUMN_NAME as column_name
                FROM information_schema.COLUMNS
                WHERE TABLE_SCHEMA = DATABASE()
                AND TABLE_NAME = :table_name
                ORDER BY ORDINAL_POSITION
            ";
            $params = ['table_name' => $tableName];
        }
        
        try {
            $stmt = $this->database->execute($sql, $params);
            $columns = [];
            while ($row = $stmt->fetch()) {
                $columns[] = $row['column_name'];
            }
            return $columns;
        } catch (Exception $e) {
            error_log("Failed to get columns for table {$tableName}: " . $e->getMessage());
            return [];
        }
    }
    
    /**
     * 動的テヌブルにカラムを远加
     * @param string $tableName
     * @param string $columnName
     * @return bool
     * @throws Exception
     */
    public function addColumnToDynamicTable($tableName, $columnName) {
        $tableName = sanitizeTableName($tableName);
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        if ($isPgsql) {
            $sql = "ALTER TABLE \"{$tableName}\" ADD COLUMN \"{$columnName}\" TEXT";
        } else {
            $sql = "ALTER TABLE `{$tableName}` ADD COLUMN `{$columnName}` TEXT";
        }
        
        try {
            error_log("Adding column to dynamic table: {$sql}");
            $this->database->execute($sql);
            return true;
        } catch (Exception $e) {
            error_log("Failed to add column {$columnName} to table {$tableName}: " . $e->getMessage());
            throw new Exception("カラム '{$columnName}' をテヌブル '{$tableName}' に远加できたせんでした: " . $e->getMessage());
        }
    }
    
    /**
     * Primary Keyで装眮情報を取埗
     * @param string $primaryKey
     * @return array|null
     */
    public function getDeviceByPrimaryKey($primaryKey) {
        $sql = "SELECT * FROM device_info WHERE primary_key = :primary_key";
        
        try {
            $stmt = $this->database->execute($sql, ['primary_key' => $primaryKey]);
            $result = $stmt->fetch();
            return $result ?: null;
        } catch (Exception $e) {
            throw new Exception("装眮情報の取埗に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮情報を曎新
     * @param string $primaryKey
     * @param array $deviceData
     * @return bool
     */
    public function updateDeviceInfo($primaryKey, $deviceData) {
        $updateFields = [];
        $params = ['primary_key' => $primaryKey];
        
        // 曎新可胜なフィヌルド
        $allowedFields = ['service_name', 'device_type', 'device_name', 'login_ip'];
        for ($i = 1; $i <= 10; $i++) {
            $allowedFields[] = "username{$i}";
            $allowedFields[] = "password{$i}";
        }
        
        foreach ($deviceData as $field => $value) {
            if (in_array($field, $allowedFields)) {
                $updateFields[] = "{$field} = :{$field}";
                $params[$field] = $value;
            }
        }
        
        if (empty($updateFields)) {
            throw new Exception("曎新するフィヌルドがありたせん");
        }
        
        // 曎新者の情報を远加
        require_once __DIR__ . '/../includes/auth_helper.php';
        $currentUser = getLoggedInUsername();
        if ($currentUser) {
            $updateFields[] = "updated_by = :updated_by";
            $params['updated_by'] = $currentUser;
        }
        
        $sql = "UPDATE device_info SET " . implode(', ', $updateFields) . ", updated_at = CURRENT_TIMESTAMP WHERE primary_key = :primary_key";
        
        try {
            $this->database->execute($sql, $params);
            return true;
        } catch (Exception $e) {
            throw new Exception("装眮情報の曎新に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 装眮情報を削陀
     * @param string $primaryKey
     * @return bool
     */
    public function deleteDeviceInfo($primaryKey) {
        try {
            // device_infoから削陀
            $sql = "DELETE FROM device_info WHERE primary_key = :primary_key";
            $this->database->execute($sql, ['primary_key' => $primaryKey]);
            
            return true;
        } catch (Exception $e) {
            throw new Exception("装眮情報の削陀に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 動的テヌブルからも関連デヌタを削陀
     * @param string $tableName
     * @param string $primaryKey
     * @return bool
     */
    public function deleteFromDynamicTable($tableName, $primaryKey) {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        try {
            if ($isPgsql) {
                $sql = "DELETE FROM \"{$tableName}\" WHERE primary_key = :primary_key";
            } else {
                $sql = "DELETE FROM `{$tableName}` WHERE primary_key = :primary_key";
            }
            
            $this->database->execute($sql, ['primary_key' => $primaryKey]);
            return true;
        } catch (Exception $e) {
            throw new Exception("動的テヌブルからの削陀に倱敗したした: " . $e->getMessage());
        }
    }
    
    /**
     * 動的テヌブルからデヌタを取埗
     * @param string $tableName
     * @param string $primaryKey
     * @return array|null
     */
    public function getDynamicTableData($tableName, $primaryKey) {
        $isPgsql = $this->database->getDbType() === 'pgsql';
        
        try {
            if ($isPgsql) {
                $sql = "SELECT * FROM \"{$tableName}\" WHERE primary_key = :primary_key";
            } else {
                $sql = "SELECT * FROM `{$tableName}` WHERE primary_key = :primary_key";
            }
            
            $stmt = $this->database->execute($sql, ['primary_key' => $primaryKey]);
            return $stmt->fetch();
        } catch (Exception $e) {
            error_log("動的テヌブル '{$tableName}' からのデヌタ取埗に倱敗: " . $e->getMessage());
            return null;
        }
    }
    
    /**
     * 動的テヌブルの拡匵列名を取埗基本列を陀く
     * @param string $tableName
     * @return array
     */
    public function getDynamicTableExtendedColumns($tableName) {
        $allColumns = $this->getTableColumns($tableName);
        
        // 基本列device_infoず共通の列を陀倖
        $baseColumns = [
            'primary_key', 'device_name', 'login_ip',
            'username1', 'password1', 'username2', 'password2',
            'username3', 'password3', 'username4', 'password4',
            'username5', 'password5', 'username6', 'password6',
            'username7', 'password7', 'username8', 'password8',
            'username9', 'password9', 'username10', 'password10',
            'created_at', 'updated_at'
        ];
        
        $extendedColumns = [];
        foreach ($allColumns as $column) {
            if (!in_array($column, $baseColumns)) {
                $extendedColumns[] = $column;
            }
        }
        
        return $extendedColumns;
    }
}
?>

DatabaseInitializer.php

📂 classes\DatabaseInitializer.php | 行数: 295 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * デヌタベヌス初期化クラス
 * 必芁なテヌブルの存圚確認ず自動䜜成を行う
 */
class DatabaseInitializer {
    private $database;
    private $isPgsql;
    
    public function __construct(Database $database) {
        $this->database = $database;
        $this->isPgsql = $database->getDbType() === 'pgsql';
    }
    
    /**
     * すべおの必須テヌブルを初期化
     * @return array 初期化結果
     * @throws Exception
     */
    public function initializeAllTables() {
        $results = [
            'success' => false,
            'tables_created' => [],
            'errors' => []
        ];
        
        try {
            error_log("Database initialization started");
            
            // device_infoテヌブルの初期化
            if ($this->initializeDeviceInfoTable()) {
                $results['tables_created'][] = 'device_info';
                error_log("device_info table initialized");
            }
            
            // service_device_type_relationsテヌブルの初期化
            if ($this->initializeRelationTable()) {
                $results['tables_created'][] = 'service_device_type_relations';
                error_log("service_device_type_relations table initialized");
            }
            
            // created_by、updated_byカラムのマむグレヌション
            $this->migrateCreatedUpdatedByColumns();
            
            $results['success'] = true;
            error_log("Database initialization completed successfully");
            
        } catch (Exception $e) {
            $results['errors'][] = $e->getMessage();
            error_log("Database initialization error: " . $e->getMessage());
            throw $e;
        }
        
        return $results;
    }
    
    /**
     * device_infoテヌブルの初期化
     * @return bool テヌブルを新芏䜜成した堎合true
     * @throws Exception
     */
    private function initializeDeviceInfoTable() {
        if ($this->database->tableExists('device_info')) {
            error_log("device_info table already exists");
            return false;
        }
        
        error_log("Creating device_info table");
        
        if ($this->isPgsql) {
            // PostgreSQL甹
            $sql = "
                CREATE TABLE IF NOT EXISTS device_info (
                    primary_key VARCHAR(500) NOT NULL PRIMARY KEY,
                    service_name VARCHAR(100) NOT NULL,
                    device_type VARCHAR(100) NOT NULL,
                    device_name VARCHAR(100) NOT NULL,
                    login_ip VARCHAR(45),
                    username1 VARCHAR(100) NOT NULL,
                    password1 VARCHAR(255),
                    username2 VARCHAR(100),
                    password2 VARCHAR(255),
                    username3 VARCHAR(100),
                    password3 VARCHAR(255),
                    username4 VARCHAR(100),
                    password4 VARCHAR(255),
                    username5 VARCHAR(100),
                    password5 VARCHAR(255),
                    username6 VARCHAR(100),
                    password6 VARCHAR(255),
                    username7 VARCHAR(100),
                    password7 VARCHAR(255),
                    username8 VARCHAR(100),
                    password8 VARCHAR(255),
                    username9 VARCHAR(100),
                    password9 VARCHAR(255),
                    username10 VARCHAR(100),
                    password10 VARCHAR(255),
                    created_by VARCHAR(100),
                    updated_by VARCHAR(100),
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ";
            $this->database->execute($sql);
            
            // むンデックス䜜成
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_service_device_type ON device_info (service_name, device_type)");
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_device_info ON device_info (service_name, device_type, device_name, username1)");
            
        } else {
            // MySQL甹
            $sql = "
                CREATE TABLE IF NOT EXISTS device_info (
                    primary_key VARCHAR(500) NOT NULL PRIMARY KEY COMMENT 'サヌビス名_装眮皮別名_装眮名_ナヌザ名の耇合キヌ',
                    service_name VARCHAR(100) NOT NULL COMMENT 'サヌビス名',
                    device_type VARCHAR(100) NOT NULL COMMENT '装眮皮別',
                    device_name VARCHAR(100) NOT NULL COMMENT '装眮名称',
                    login_ip VARCHAR(45) COMMENT 'ログむンIP',
                    username1 VARCHAR(100) NOT NULL COMMENT 'ナヌザヌ名1',
                    password1 VARCHAR(255) COMMENT 'パスワヌド1',
                    username2 VARCHAR(100) COMMENT 'ナヌザヌ名2',
                    password2 VARCHAR(255) COMMENT 'パスワヌド2',
                    username3 VARCHAR(100) COMMENT 'ナヌザヌ名3',
                    password3 VARCHAR(255) COMMENT 'パスワヌド3',
                    username4 VARCHAR(100) COMMENT 'ナヌザヌ名4',
                    password4 VARCHAR(255) COMMENT 'パスワヌド4',
                    username5 VARCHAR(100) COMMENT 'ナヌザヌ名5',
                    password5 VARCHAR(255) COMMENT 'パスワヌド5',
                    username6 VARCHAR(100) COMMENT 'ナヌザヌ名6',
                    password6 VARCHAR(255) COMMENT 'パスワヌド6',
                    username7 VARCHAR(100) COMMENT 'ナヌザヌ名7',
                    password7 VARCHAR(255) COMMENT 'パスワヌド7',
                    username8 VARCHAR(100) COMMENT 'ナヌザヌ名8',
                    password8 VARCHAR(255) COMMENT 'パスワヌド8',
                    username9 VARCHAR(100) COMMENT 'ナヌザヌ名9',
                    password9 VARCHAR(255) COMMENT 'パスワヌド9',
                    username10 VARCHAR(100) COMMENT 'ナヌザヌ名10',
                    password10 VARCHAR(255) COMMENT 'パスワヌド10',
                    created_by VARCHAR(100) COMMENT '䜜成者',
                    updated_by VARCHAR(100) COMMENT '曎新者',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '䜜成日時',
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時',
                    INDEX idx_service_device_type (service_name, device_type),
                    INDEX idx_device_info (service_name, device_type, device_name, username1)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='装眮基本情報テヌブル'
            ";
            $this->database->execute($sql);
        }
        
        error_log("device_info table created successfully");
        return true;
    }
    
    /**
     * service_device_type_relationsテヌブルの初期化
     * @return bool テヌブルを新芏䜜成した堎合true
     * @throws Exception
     */
    private function initializeRelationTable() {
        if ($this->database->tableExists('service_device_type_relations')) {
            error_log("service_device_type_relations table already exists");
            return false;
        }
        
        error_log("Creating service_device_type_relations table");
        
        if ($this->isPgsql) {
            // PostgreSQL甹
            $sql = "
                CREATE TABLE IF NOT EXISTS service_device_type_relations (
                    id SERIAL PRIMARY KEY,
                    service_name VARCHAR(100) NOT NULL,
                    device_type VARCHAR(100) NOT NULL,
                    description TEXT,
                    is_active BOOLEAN DEFAULT TRUE,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    UNIQUE (service_name, device_type)
                )
            ";
            $this->database->execute($sql);
            
            // むンデックス䜜成
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_service_name ON service_device_type_relations (service_name)");
            $this->database->execute("CREATE INDEX IF NOT EXISTS idx_device_type ON service_device_type_relations (device_type)");
            
        } else {
            // MySQL甹
            $sql = "
                CREATE TABLE IF NOT EXISTS service_device_type_relations (
                    id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'リレヌションID',
                    service_name VARCHAR(100) NOT NULL COMMENT 'サヌビス名',
                    device_type VARCHAR(100) NOT NULL COMMENT '装眮皮別',
                    description TEXT COMMENT '説明',
                    is_active BOOLEAN DEFAULT TRUE COMMENT 'アクティブフラグ',
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '䜜成日時',
                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時',
                    UNIQUE KEY unique_service_device (service_name, device_type),
                    INDEX idx_service_name (service_name),
                    INDEX idx_device_type (device_type)
                ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='サヌビス-装眮皮別リレヌションテヌブル'
            ";
            $this->database->execute($sql);
        }
        
        error_log("service_device_type_relations table created successfully");
        return true;
    }
    
    /**
     * テヌブルの存圚確認
     * @param string $tableName
     * @return bool
     */
    public function tableExists($tableName) {
        return $this->database->tableExists($tableName);
    }
    
    /**
     * 必須テヌブルがすべお存圚するか確認
     * @return array 各テヌブルの存圚状況
     */
    public function checkRequiredTables() {
        return [
            'device_info' => $this->database->tableExists('device_info'),
            'service_device_type_relations' => $this->database->tableExists('service_device_type_relations')
        ];
    }
    
    /**
     * device_infoテヌブルにcreated_by、updated_byカラムを远加するマむグレヌション
     * @return void
     */
    private function migrateCreatedUpdatedByColumns() {
        try {
            // device_infoテヌブルが存圚しない堎合はスキップ
            if (!$this->database->tableExists('device_info')) {
                return;
            }
            
            // 既存のカラムを確認
            if ($this->isPgsql) {
                $checkSql = "
                    SELECT column_name 
                    FROM information_schema.columns 
                    WHERE table_name = 'device_info' 
                    AND column_name IN ('created_by', 'updated_by')
                ";
            } else {
                $dbName = $this->database->query("SELECT DATABASE()")[0]['DATABASE()'];
                $checkSql = "
                    SELECT COLUMN_NAME 
                    FROM INFORMATION_SCHEMA.COLUMNS 
                    WHERE TABLE_NAME = 'device_info' 
                    AND TABLE_SCHEMA = '{$dbName}'
                    AND COLUMN_NAME IN ('created_by', 'updated_by')
                ";
            }
            
            $existingColumns = $this->database->query($checkSql);
            $existingColumnNames = array_column($existingColumns, $this->isPgsql ? 'column_name' : 'COLUMN_NAME');
            
            // created_byカラムを远加
            if (!in_array('created_by', $existingColumnNames)) {
                error_log("Adding created_by column to device_info table");
                if ($this->isPgsql) {
                    $sql = "ALTER TABLE device_info ADD COLUMN created_by VARCHAR(100)";
                } else {
                    $sql = "ALTER TABLE device_info ADD COLUMN created_by VARCHAR(100) COMMENT '䜜成者' AFTER password10";
                }
                $this->database->execute($sql);
                error_log("created_by column added successfully");
            }
            
            // updated_byカラムを远加
            if (!in_array('updated_by', $existingColumnNames)) {
                error_log("Adding updated_by column to device_info table");
                if ($this->isPgsql) {
                    $sql = "ALTER TABLE device_info ADD COLUMN updated_by VARCHAR(100)";
                } else {
                    $sql = "ALTER TABLE device_info ADD COLUMN updated_by VARCHAR(100) COMMENT '曎新者' AFTER created_by";
                }
                $this->database->execute($sql);
                error_log("updated_by column added successfully");
            }
            
        } catch (Exception $e) {
            error_log("Migration error for created_by/updated_by columns: " . $e->getMessage());
            // マむグレヌション゚ラヌは譊告ずしお扱い、凊理を継続
        }
    }
}
?>

backup_mysql.sh

📂 backup_mysql.sh | 行数: 103 | 最終曎新: 2026-02-18 21:45:17
#!/bin/bash

# MySQL バックアップスクリプト
# 䜿甚方法: ./backup_mysql.sh [daily|weekly|monthly]

# 蚭定
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKUP_ROOT="${SCRIPT_DIR}/backups"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
DB_NAME="${DB_NAME:-device_management}"
DB_USER="${DB_USER:-root}"
DB_PASS="${DB_PASS:-}"

# バックアップタむプ匕数から取埗、デフォルトはdaily
BACKUP_TYPE="${1:-daily}"

# バックアップディレクトリ
BACKUP_DIR="${BACKUP_ROOT}/${BACKUP_TYPE}"
mkdir -p "${BACKUP_DIR}"

# タむムスタンプ
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="${BACKUP_DIR}/backup_${DB_NAME}_${TIMESTAMP}.sql.gz"

# ログファむル
LOG_DIR="${SCRIPT_DIR}/logs"
mkdir -p "${LOG_DIR}"
LOG_FILE="${LOG_DIR}/backup.log"

# ログ関数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}"
}

log "=========================================="
log "バックアップ開始: ${BACKUP_TYPE}"
log "デヌタベヌス: ${DB_NAME}"

# mysqldumpの実行
if [ -z "${DB_PASS}" ]; then
    # パスワヌドなし
    mysqldump -h "${DB_HOST}" \
              -P "${DB_PORT}" \
              -u "${DB_USER}" \
              --single-transaction \
              --routines \
              --triggers \
              --events \
              "${DB_NAME}" | gzip > "${BACKUP_FILE}"
else
    # パスワヌドあり
    mysqldump -h "${DB_HOST}" \
              -P "${DB_PORT}" \
              -u "${DB_USER}" \
              -p"${DB_PASS}" \
              --single-transaction \
              --routines \
              --triggers \
              --events \
              "${DB_NAME}" | gzip > "${BACKUP_FILE}"
fi

# バックアップの成吊を確認
if [ $? -eq 0 ] && [ -f "${BACKUP_FILE}" ]; then
    BACKUP_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
    log "バックアップ成功: ${BACKUP_FILE} (${BACKUP_SIZE})"
else
    log "゚ラヌ: バックアップに倱敗したした"
    exit 1
fi

# 叀いバックアップファむルの削陀
case "${BACKUP_TYPE}" in
    daily)
        # 7日以䞊前のファむルを削陀
        KEEP_DAYS=7
        log "日次バックアップ: ${KEEP_DAYS}日より叀いファむルを削陀"
        find "${BACKUP_DIR}" -name "backup_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete
        ;;
    weekly)
        # 4週間28日以䞊前のファむルを削陀
        KEEP_DAYS=28
        log "週次バックアップ: ${KEEP_DAYS}日より叀いファむルを削陀"
        find "${BACKUP_DIR}" -name "backup_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete
        ;;
    monthly)
        # 12ヶ月365日以䞊前のファむルを削陀
        KEEP_DAYS=365
        log "月次バックアップ: ${KEEP_DAYS}日より叀いファむルを削陀"
        find "${BACKUP_DIR}" -name "backup_*.sql.gz" -type f -mtime +${KEEP_DAYS} -delete
        ;;
esac

# 残っおいるバックアップファむルの数を衚瀺
BACKUP_COUNT=$(ls -1 "${BACKUP_DIR}"/backup_*.sql.gz 2>/dev/null | wc -l)
log "保存されおいるバックアップファむル数: ${BACKUP_COUNT}"

log "バックアップ完了"
log "=========================================="

exit 0

check_db_schema.php

📂 check_db_schema.php | 行数: 88 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * デヌタベヌススキヌマ確認ツヌル
 */
require_once 'config.php';

try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    
    echo "<h2>device_info テヌブルのカラム確認</h2>\n";
    
    if (!$database->tableExists('device_info')) {
        echo "<p style='color: orange;'>device_info テヌブルが存圚したせん。</p>\n";
        echo "<p>CSVアップロヌドを実行するず自動的に䜜成されたす。</p>\n";
    } else {
        $columns = $database->getTableColumns('device_info');
        
        echo "<h3>珟圚のカラム䞀芧</h3>\n";
        echo "<ul>\n";
        foreach ($columns as $col) {
            echo "<li>" . htmlspecialchars($col['COLUMN_NAME']) . " - " . htmlspecialchars($col['DATA_TYPE']) . "</li>\n";
        }
        echo "</ul>\n";
        
        // 必須カラムのチェック
        $requiredColumns = ['primary_key', 'service_name', 'device_type', 'device_name', 'login_ip', 'username1', 'password1'];
        $existingColumnNames = array_column($columns, 'COLUMN_NAME');
        
        echo "<h3>必須カラムのチェック</h3>\n";
        $allOk = true;
        foreach ($requiredColumns as $required) {
            $exists = in_array($required, $existingColumnNames);
            $color = $exists ? 'green' : 'red';
            $status = $exists ? '✓ 存圚' : '✗ 䞍足';
            echo "<p style='color: {$color};'>{$required}: {$status}</p>\n";
            if (!$exists) {
                $allOk = false;
            }
        }
        
        // 旧カラム削陀すべきのチェック
        $oldColumns = ['device_ip', 'username', 'password'];
        echo "<h3>旧スキヌマカラムのチェック</h3>\n";
        $hasOldColumns = false;
        foreach ($oldColumns as $old) {
            if (in_array($old, $existingColumnNames)) {
                echo "<p style='color: orange;'>⚠ 旧カラムが残っおいたす: {$old}</p>\n";
                $hasOldColumns = true;
            }
        }
        
        if (!$hasOldColumns) {
            echo "<p style='color: green;'>✓ 旧カラムは存圚したせん</p>\n";
        }
        
        // 総合刀定
        echo "<hr>\n";
        if ($allOk && !$hasOldColumns) {
            echo "<h2 style='color: green;'>✓ スキヌマは正垞です</h2>\n";
            
            // デヌタ件数確認
            $stmt = $database->execute("SELECT COUNT(*) as count FROM device_info");
            $result = $stmt->fetch();
            echo "<p>登録デヌタ件数: " . $result['count'] . " ä»¶</p>\n";
            
        } elseif ($hasOldColumns) {
            echo "<h2 style='color: orange;'>⚠ 旧スキヌマのテヌブルが存圚したす</h2>\n";
            echo "<p><strong>察応方法</strong></p>\n";
            echo "<ol>\n";
            echo "<li>既存デヌタが䞍芁な堎合: テヌブルを削陀しお再䜜成</li>\n";
            echo "<li>既存デヌタを保持する堎合: マむグレヌションスクリプトを実行</li>\n";
            echo "</ol>\n";
            echo "<p><a href='migrate_device_info.php'>→ 自動マむグレヌションを実行</a></p>\n";
        } else {
            echo "<h2 style='color: red;'>✗ 必須カラムが䞍足しおいたす</h2>\n";
            echo "<p>テヌブルを削陀しお再䜜成しおください。</p>\n";
        }
    }
    
    $database->close();
    
} catch (Exception $e) {
    echo "<p style='color: red;'>゚ラヌ: " . htmlspecialchars($e->getMessage()) . "</p>\n";
}
?>

update_existing_user_fields.php

📂 admin\update_existing_user_fields.php | 行数: 46 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * 既存デヌタのcreated_by、updated_byフィヌルドにデフォルト倀を蚭定するマむグレヌション
 */

require_once __DIR__ . '/../config.php';

try {
    // デヌタベヌス接続
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    
    echo "デヌタベヌスに接続したした。\n\n";
    
    // created_byがNULLのレコヌドを確認
    $checkSql = "SELECT COUNT(*) as count FROM device_info WHERE created_by IS NULL OR updated_by IS NULL";
    $stmt = $database->execute($checkSql);
    $result = $stmt->fetch();
    $nullCount = $result['count'];
    
    echo "created_byたたはupdated_byがNULLのレコヌド数: {$nullCount}\n\n";
    
    if ($nullCount > 0) {
        echo "既存デヌタにデフォルト倀を蚭定䞭...\n";
        
        // created_byがNULLの堎合は'system'を蚭定
        $updateCreatedBySql = "UPDATE device_info SET created_by = 'system' WHERE created_by IS NULL";
        $database->execute($updateCreatedBySql);
        echo "✓ created_byにデフォルト倀を蚭定したした。\n";
        
        // updated_byがNULLの堎合は'system'を蚭定
        $updateUpdatedBySql = "UPDATE device_info SET updated_by = 'system' WHERE updated_by IS NULL";
        $database->execute($updateUpdatedBySql);
        echo "✓ updated_byにデフォルト倀を蚭定したした。\n\n";
        
        echo "マむグレヌション完了\n";
    } else {
        echo "すべおのレコヌドに倀が蚭定されおいたす。凊理䞍芁です。\n";
    }
    
} catch (Exception $e) {
    echo "゚ラヌ: " . $e->getMessage() . "\n";
    exit(1);
}

migrate_device_info_schema.php

📂 admin\migrate_device_info_schema.php | 行数: 144 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * device_infoテヌブルのスキヌマ倉曎マむグレヌション
 * 旧: username, password, device_ip
 * 新: username1-10, password1-10, login_ip
 */

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../classes/Database.php';

try {
    echo "デヌタベヌス接続䞭...\n";
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
    $database->connect();
    
    echo "既存のdevice_infoテヌブルを確認䞭...\n";
    $tableExists = $database->tableExists('device_info');
    
    if (!$tableExists) {
        echo "device_infoテヌブルが存圚したせん。新芏䜜成したす。\n";
        require_once __DIR__ . '/../classes/DeviceManager.php';
        $deviceManager = new DeviceManager($database);
        $deviceManager->createDeviceInfoTable();
        echo "✓ device_infoテヌブルを䜜成したした。\n";
        exit(0);
    }
    
    echo "既存デヌタを確認䞭...\n";
    $stmt = $database->execute("SELECT COUNT(*) as cnt FROM device_info");
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
    $count = $result[0]['cnt'];
    echo "既存レコヌド数: {$count}\n";
    
    if ($count > 0) {
        echo "\n譊告: 既存デヌタが存圚したす。\n";
        echo "このマむグレヌションでは、既存デヌタを保持したたたスキヌマを倉曎したす。\n";
        echo "- username → username1\n";
        echo "- password → password1\n";
        echo "- device_ip → login_ip\n";
        echo "- username2-10, password2-10 を远加NULL\n\n";
        
        // バックアップテヌブル䜜成
        echo "バックアップテヌブル䜜成䞭...\n";
        $timestamp = date('Ymd_His');
        $backupTable = "device_info_backup_{$timestamp}";
        $database->execute("CREATE TABLE {$backupTable} AS SELECT * FROM device_info");
        echo "✓ バックアップテヌブル䜜成完了: {$backupTable}\n";
    }
    
    echo "\nスキヌマ倉曎開始...\n";
    
    // 1. 新しいカラムを远加
    echo "1. 新しいカラムを远加䞭...\n";
    $alterQueries = [
        "ALTER TABLE device_info ADD COLUMN login_ip VARCHAR(45) AFTER device_name",
        "ALTER TABLE device_info ADD COLUMN username1 VARCHAR(100) AFTER login_ip",
        "ALTER TABLE device_info ADD COLUMN password1 VARCHAR(255) AFTER username1"
    ];
    
    // username2-10, password2-10 を远加
    for ($i = 2; $i <= 10; $i++) {
        $alterQueries[] = "ALTER TABLE device_info ADD COLUMN username{$i} VARCHAR(100) AFTER password" . ($i - 1);
        $alterQueries[] = "ALTER TABLE device_info ADD COLUMN password{$i} VARCHAR(255) AFTER username{$i}";
    }
    
    foreach ($alterQueries as $query) {
        try {
            $database->execute($query);
        } catch (Exception $e) {
            // カラムが既に存圚する堎合はスキップ
            if (strpos($e->getMessage(), 'Duplicate column name') === false) {
                throw $e;
            }
        }
    }
    echo "✓ 新しいカラムを远加したした。\n";
    
    if ($count > 0) {
        // 2. デヌタを移行
        echo "2. 既存デヌタを新カラムに移行䞭...\n";
        $database->execute("UPDATE device_info SET username1 = username WHERE username1 IS NULL");
        $database->execute("UPDATE device_info SET password1 = password WHERE password1 IS NULL");
        $database->execute("UPDATE device_info SET login_ip = device_ip WHERE login_ip IS NULL");
        echo "✓ デヌタ移行完了。\n";
    }
    
    // 3. 叀いカラムを削陀
    echo "3. 叀いカラムを削陀䞭...\n";
    try {
        $database->execute("ALTER TABLE device_info DROP COLUMN username");
    } catch (Exception $e) {
        echo "  - username カラムは既に削陀されおいたす。\n";
    }
    try {
        $database->execute("ALTER TABLE device_info DROP COLUMN password");
    } catch (Exception $e) {
        echo "  - password カラムは既に削陀されおいたす。\n";
    }
    try {
        $database->execute("ALTER TABLE device_info DROP COLUMN device_ip");
    } catch (Exception $e) {
        echo "  - device_ip カラムは既に削陀されおいたす。\n";
    }
    echo "✓ 叀いカラムを削陀したした。\n";
    
    // 4. username1 を NOT NULL に倉曎
    if ($count > 0) {
        echo "4. username1 を NOT NULL に倉曎䞭...\n";
        $database->execute("ALTER TABLE device_info MODIFY username1 VARCHAR(100) NOT NULL COMMENT 'ナヌザヌ名1'");
        echo "✓ username1 を NOT NULL に倉曎したした。\n";
    }
    
    // 5. むンデックスを曎新
    echo "5. むンデックスを曎新䞭...\n";
    try {
        $database->execute("DROP INDEX idx_device_info ON device_info");
    } catch (Exception $e) {
        echo "  - idx_device_info は既に削陀されおいたす。\n";
    }
    $database->execute("CREATE INDEX idx_device_info ON device_info (service_name, device_type, device_name, username1)");
    echo "✓ むンデックスを曎新したした。\n";
    
    echo "\n✅ マむグレヌション完了\n";
    
    if ($count > 0) {
        echo "\nバックアップテヌブル: {$backupTable}\n";
        echo "問題がなければ、以䞋のコマンドでバックアップを削陀できたす\n";
        echo "DROP TABLE {$backupTable};\n";
    }
    
    // 曎新埌のスキヌマを衚瀺
    echo "\n珟圚のdevice_infoテヌブル構造:\n";
    $stmt = $database->execute("SHOW COLUMNS FROM device_info");
    $columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
    foreach ($columns as $col) {
        echo "  - {$col['Field']}: {$col['Type']} " . ($col['Null'] === 'NO' ? 'NOT NULL' : 'NULL') . "\n";
    }
    
} catch (Exception $e) {
    echo "\n❌ ゚ラヌ: " . $e->getMessage() . "\n";
    exit(1);
}
?>

init_users_table.php

📂 admin\init_users_table.php | 行数: 37 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * デヌタベヌス初期化スクリプトナヌザヌテヌブル远加
 */

require_once __DIR__ . '/../config.php';

try {
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET, DB_TYPE, DB_PORT);
    $conn = $database->connect();
    
    echo "デヌタベヌス接続成功\n";
    
    // ナヌザヌテヌブル䜜成
    $sql = "CREATE TABLE IF NOT EXISTS users (
        id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'ナヌザヌID',
        username VARCHAR(100) NOT NULL UNIQUE COMMENT 'ナヌザヌ名',
        password_hash VARCHAR(255) NOT NULL COMMENT 'パスワヌドハッシュbcrypt',
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '䜜成日時',
        updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '曎新日時',
        last_login TIMESTAMP NULL COMMENT '最終ログむン日時',
        is_active TINYINT(1) DEFAULT 1 COMMENT '有効フラグ(1:有効, 0:無効)',
        INDEX idx_username (username),
        INDEX idx_active (is_active)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='ナヌザヌ認蚌テヌブル'";
    
    $conn->exec($sql);
    echo "✓ usersテヌブルを䜜成したした\n";
    
    echo "\n初期化完了\n";
    echo "登録ペヌゞにアクセスしおナヌザヌを䜜成しおください: register.php\n";
    
} catch (Exception $e) {
    echo "゚ラヌ: " . $e->getMessage() . "\n";
    exit(1);
}

init_database.php

📂 admin\init_database.php | 行数: 107 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * デヌタベヌス初期化スクリプト
 * アプリケヌションの初回セットアップ時に実行
 */

require_once __DIR__ . '/../config.php';

try {
    echo "=== 装眮情報管理システム - デヌタベヌス初期化 ===\n\n";
    
    // デヌタベヌス接続テスト
    echo "1. デヌタベヌス接続テスト...\n";
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $pdo = $database->connect();
    echo "   ✓ 接続成功\n\n";
    
    // デヌタベヌス初期化必須テヌブルの䜜成
    echo "2. 必須テヌブル䜜成...\n";
    $dbInitializer = new DatabaseInitializer($database);
    $initResults = $dbInitializer->initializeAllTables();
    
    if (!empty($initResults['tables_created'])) {
        foreach ($initResults['tables_created'] as $tableName) {
            echo "   ✓ {$tableName} テヌブルを䜜成したした\n";
        }
    } else {
        echo "   - すべおの必須テヌブルは既に存圚したす\n";
    }
    
    if (!empty($initResults['errors'])) {
        foreach ($initResults['errors'] as $error) {
            echo "   ⚠ ゚ラヌ: {$error}\n";
        }
    }
    
    // テヌブル存圚確認
    $tableStatus = $dbInitializer->checkRequiredTables();
    echo "\n   テヌブル状態:\n";
    foreach ($tableStatus as $tableName => $exists) {
        $status = $exists ? "✓ 存圚" : "✗ 䞍圚";
        echo "   - {$tableName}: {$status}\n";
    }
    
    // テヌブル情報の衚瀺
    echo "\n3. テヌブル構造確認...\n";
    $columns = $database->getTableColumns('device_info');
    echo "   device_info テヌブルのカラム:\n";
    foreach ($columns as $column) {
        echo "   - {$column['COLUMN_NAME']} ({$column['DATA_TYPE']})\n";
    }
    
    // アップロヌドディレクトリの確認
    echo "\n4. アップロヌドディレクトリ確認...\n";
    if (!is_dir(UPLOAD_DIR)) {
        mkdir(UPLOAD_DIR, 0755, true);
        echo "   ✓ アップロヌドディレクトリを䜜成したした: " . UPLOAD_DIR . "\n";
    } else {
        echo "   - アップロヌドディレクトリは既に存圚したす: " . UPLOAD_DIR . "\n";
    }
    
    // 暩限確認
    if (is_writable(UPLOAD_DIR)) {
        echo "   ✓ アップロヌドディレクトリは曞き蟌み可胜です\n";
    } else {
        echo "   ⚠ アップロヌドディレクトリに曞き蟌み暩限がありたせん\n";
    }
    
    // サンプルCSVファむルの存圚確認
    echo "\n5. サンプルファむル確認...\n";
    $sampleCsvPath = __DIR__ . '/sample.csv';
    if (file_exists($sampleCsvPath)) {
        echo "   ✓ サンプルCSVファむルが存圚したす: sample.csv\n";
        
        // サンプルファむルの内容確認
        $csvProcessor = new CsvProcessor();
        if ($csvProcessor->loadFile($sampleCsvPath)) {
            $stats = $csvProcessor->getStatistics();
            echo "   - デヌタ行数: {$stats['total_rows']}\n";
            echo "   - サヌビス: " . implode(', ', $stats['services']) . "\n";
            echo "   - 装眮皮別: " . implode(', ', $stats['device_types']) . "\n";
        }
    } else {
        echo "   ⚠ sample.csv が芋぀かりたせん\n";
    }
    
    // 蚭定倀の確認
    echo "\n6. アプリケヌション蚭定確認...\n";
    echo "   - デヌタベヌスホスト: " . DB_HOST . "\n";
    echo "   - デヌタベヌス名: " . DB_NAME . "\n";
    echo "   - 最倧アップロヌドサむズ: " . (UPLOAD_MAX_SIZE / 1024 / 1024) . "MB\n";
    echo "   - 蚱可ファむル圢匏: " . implode(', ', UPLOAD_ALLOWED_TYPES) . "\n";
    
    echo "\n=== 初期化完了 ===\n";
    echo "ブラりザで index.php にアクセスしおアプリケヌションを開始しおください。\n";
    
} catch (Exception $e) {
    echo "❌ ゚ラヌが発生したした: " . $e->getMessage() . "\n";
    echo "\n蚭定を確認しおください:\n";
    echo "1. MySQLサヌバヌが起動しおいるこずを確認\n";
    echo "2. config.php のデヌタベヌス接続情報を確認\n";
    echo "3. デヌタベヌス '{DB_NAME}' が存圚するこずを確認\n";
    echo "4. ナヌザヌ '{DB_USER}' に適切な暩限があるこずを確認\n";
}
?>

admin_tables.php

📂 admin\admin_tables.php | 行数: 249 | 最終曎新: 2026-02-18 21:45:17
<?php
require_once '../config.php';

// 管理者甚テヌブル管理ペヌゞ
ini_set('display_errors', 1);
error_reporting(E_ALL);

$message = '';
$error = '';

try {
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    $deviceManager = new DeviceManager($database);
    
    // テヌブル削陀凊理
    if ($_POST['action'] ?? '' === 'delete_dynamic_tables') {
        $tablesDeleted = 0;
        
        // 党テヌブルを取埗
        $sql = "SHOW TABLES";
        $stmt = $database->execute($sql);
        $tables = $stmt->fetchAll(PDO::FETCH_NUM);
        
        foreach ($tables as $table) {
            $tableName = $table[0];
            // システムテヌブル以倖を削陀
            if (!in_array($tableName, ['device_info', 'service_device_type_relations'])) {
                try {
                    $deleteSql = "DROP TABLE `{$tableName}`";
                    $database->execute($deleteSql);
                    $tablesDeleted++;
                } catch (Exception $e) {
                    error_log("テヌブル削陀゚ラヌ ({$tableName}): " . $e->getMessage());
                }
            }
        }
        
        $message = "{$tablesDeleted}個の動的テヌブルを削陀したした。";
    }
    
    // 党テヌブルを取埗しお衚瀺
    $sql = "SHOW TABLES";
    $stmt = $database->execute($sql);
    $tables = $stmt->fetchAll(PDO::FETCH_NUM);
    
} catch (Exception $e) {
    $error = "゚ラヌ: " . $e->getMessage();
    $tables = [];
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>テヌブル管理 - 装眮情報管理システム</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background-color: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 30px;
            border-bottom: 3px solid #dc3545;
            padding-bottom: 15px;
        }
        .header h1 {
            color: #333;
            margin: 0;
        }
        .nav-buttons {
            display: flex;
            gap: 10px;
        }
        .nav-buttons a {
            padding: 8px 16px;
            background-color: #6c757d;
            color: white;
            text-decoration: none;
            border-radius: 4px;
            font-size: 14px;
            transition: background-color 0.3s;
        }
        .nav-buttons a:hover {
            background-color: #5a6268;
        }
        .alert {
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .alert-error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .alert-success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .alert-warning {
            background-color: #fff3cd;
            color: #856404;
            border: 1px solid #ffeaa7;
        }
        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 4px;
            font-size: 14px;
            cursor: pointer;
            transition: background-color 0.3s;
            text-decoration: none;
            display: inline-block;
            margin-right: 10px;
        }
        .btn-danger {
            background-color: #dc3545;
            color: white;
        }
        .btn-danger:hover {
            background-color: #c82333;
        }
        .table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 20px;
        }
        .table th,
        .table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        .table th {
            background-color: #f8f9fa;
            font-weight: bold;
        }
        .table tr:hover {
            background-color: #f8f9fa;
        }
        .system-table {
            color: #28a745;
            font-weight: bold;
        }
        .dynamic-table {
            color: #dc3545;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>⚙ テヌブル管理</h1>
            <div class="nav-buttons">
                <a href="upload.php">📀 CSVアップロヌド</a>
                <a href="manage.php">⚙ 装眮情報管理</a>
                <a href="relations.php">🔗 リレヌション管理</a>
            </div>
        </div>
        
        <?php if ($error): ?>
        <div class="alert alert-error">
            <strong>゚ラヌ:</strong> <?= htmlspecialchars($error) ?>
        </div>
        <?php endif; ?>
        
        <?php if ($message): ?>
        <div class="alert alert-success">
            <strong>成功:</strong> <?= htmlspecialchars($message) ?>
        </div>
        <?php endif; ?>
        
        <div class="alert alert-warning">
            <h4>⚠ 泚意</h4>
            <p>
                このペヌゞは開発・テスト甚です。動的テヌブルを削陀するず、CSVでアップロヌドしたデヌタが倱われたす。
                本番環境では䜿甚しないでください。
            </p>
        </div>
        
        <h3>珟圚のテヌブル䞀芧</h3>
        
        <table class="table">
            <thead>
                <tr>
                    <th>テヌブル名</th>
                    <th>タむプ</th>
                    <th>説明</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($tables as $table): ?>
                <?php 
                    $tableName = $table[0];
                    $isSystemTable = in_array($tableName, ['device_info', 'service_device_type_relations']);
                ?>
                <tr>
                    <td class="<?= $isSystemTable ? 'system-table' : 'dynamic-table' ?>">
                        <?= htmlspecialchars($tableName) ?>
                    </td>
                    <td>
                        <?= $isSystemTable ? 'システムテヌブル' : '動的テヌブル' ?>
                    </td>
                    <td>
                        <?php if ($tableName === 'device_info'): ?>
                            装眮情報の基本デヌタ
                        <?php elseif ($tableName === 'service_device_type_relations'): ?>
                            サヌビス・装眮皮別リレヌション
                        <?php else: ?>
                            CSVアップロヌドで䜜成された拡匵デヌタテヌブル
                        <?php endif; ?>
                    </td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        
        <h3>管理操䜜</h3>
        
        <form method="post" style="margin-top: 20px;" onsubmit="return confirm('党おの動的テヌブルを削陀したすかこの操䜜は取り消せたせん。');">
            <input type="hidden" name="action" value="delete_dynamic_tables">
            <button type="submit" class="btn btn-danger">
                🗑 党おの動的テヌブルを削陀
            </button>
        </form>
        
        <p style="margin-top: 20px; color: #6c757d; font-size: 14px;">
            動的テヌブルを削陀するず、次回のCSVアップロヌド時に新しい構造でテヌブルが再䜜成されたす。
        </p>
    </div>
</body>
</html>

add_created_updated_by.php

📂 admin\add_created_updated_by.php | 行数: 74 | 最終曎新: 2026-02-18 21:45:17
<?php
/**
 * device_infoテヌブルにcreated_by、updated_byカラムを远加するマむグレヌション
 */

require_once __DIR__ . '/../config.php';

try {
    // デヌタベヌス接続
    $dbType = defined('DB_TYPE') ? DB_TYPE : 'mysql';
    $charset = ($dbType === 'pgsql') ? 'utf8' : DB_CHARSET;
    $database = new Database(DB_HOST, DB_NAME, DB_USER, DB_PASS, $charset, $dbType, defined('DB_PORT') ? DB_PORT : null);
    
    echo "デヌタベヌスに接続したした。\n\n";
    
    // 既存のカラムを確認
    if ($dbType === 'pgsql') {
        $checkSql = "
            SELECT column_name 
            FROM information_schema.columns 
            WHERE table_name = 'device_info' 
            AND column_name IN ('created_by', 'updated_by')
        ";
    } else {
        $checkSql = "
            SELECT COLUMN_NAME 
            FROM INFORMATION_SCHEMA.COLUMNS 
            WHERE TABLE_NAME = 'device_info' 
            AND TABLE_SCHEMA = '" . DB_NAME . "'
            AND COLUMN_NAME IN ('created_by', 'updated_by')
        ";
    }
    
    $stmt = $database->execute($checkSql);
    $existingColumns = $stmt->fetchAll();
    $existingColumnNames = array_column($existingColumns, $dbType === 'pgsql' ? 'column_name' : 'COLUMN_NAME');
    
    echo "既存のカラム: " . implode(', ', $existingColumnNames) . "\n\n";
    
    // created_byカラムを远加
    if (!in_array('created_by', $existingColumnNames)) {
        echo "created_byカラムを远加䞭...\n";
        if ($dbType === 'pgsql') {
            $sql = "ALTER TABLE device_info ADD COLUMN created_by VARCHAR(100)";
        } else {
            $sql = "ALTER TABLE device_info ADD COLUMN created_by VARCHAR(100) COMMENT '䜜成者' AFTER password10";
        }
        $database->execute($sql);
        echo "✓ created_byカラムを远加したした。\n\n";
    } else {
        echo "created_byカラムは既に存圚したす。\n\n";
    }
    
    // updated_byカラムを远加
    if (!in_array('updated_by', $existingColumnNames)) {
        echo "updated_byカラムを远加䞭...\n";
        if ($dbType === 'pgsql') {
            $sql = "ALTER TABLE device_info ADD COLUMN updated_by VARCHAR(100)";
        } else {
            $sql = "ALTER TABLE device_info ADD COLUMN updated_by VARCHAR(100) COMMENT '曎新者' AFTER created_by";
        }
        $database->execute($sql);
        echo "✓ updated_byカラムを远加したした。\n\n";
    } else {
        echo "updated_byカラムは既に存圚したす。\n\n";
    }
    
    echo "マむグレヌション完了\n";
    
} catch (Exception $e) {
    echo "゚ラヌ: " . $e->getMessage() . "\n";
    exit(1);
}