<?php
/*
==============================================
ファイルアップロード機能(確認画面まで)
==============================================
*/
// Controller部分(Contact.php)
class Contact extends CI_Controller {
public function __construct() {
parent::__construct();
$this->load->database();
$this->load->helper(['form', 'url', 'file']);
$this->load->library(['form_validation', 'upload', 'session', 'smarty']);
}
/**
* メイン処理
*/
public function index() {
$data = [
'errors' => [],
'form_data' => [],
'success' => false
];
if ($this->input->method() === 'post') {
$action = $this->input->post('action');
if ($action === 'confirm') {
$data = $this->_process_confirm();
if (empty($data['errors'])) {
$this->_show_confirm($data);
return;
}
}
}
$this->_show_form($data);
}
/**
* 確認処理(ファイル名表示対応)
*/
private function _process_confirm() {
$data = [
'errors' => [],
'form_data' => [],
'uploaded_files' => [],
'file_info' => []
];
// バリデーション設定
$this->form_validation->set_rules('name', '名前', 'required|trim|max_length[100]');
$this->form_validation->set_rules('email', 'メールアドレス', 'required|valid_email|max_length[255]');
$this->form_validation->set_rules('message', 'メッセージ', 'required|trim|max_length[1000]');
// バリデーション実行
if (!$this->form_validation->run()) {
$data['errors'] = $this->form_validation->error_array();
}
// フォームデータ取得
$data['form_data'] = [
'name' => trim($this->input->post('name')),
'email' => trim($this->input->post('email')),
'message' => trim($this->input->post('message'))
];
// ファイルアップロード処理(一時保存)
$upload_result = $this->_handle_file_upload_temp();
if (!empty($upload_result['errors'])) {
$data['errors'] = array_merge($data['errors'], $upload_result['errors']);
} else {
$data['uploaded_files'] = $upload_result['files'];
$data['file_info'] = $upload_result['file_info'];
// セッションに保存(確認画面→送信で使用)
$this->session->set_userdata('temp_uploaded_files', $upload_result['files']);
$this->session->set_userdata('temp_file_info', $upload_result['file_info']);
}
return $data;
}
/**
* ファイルアップロード処理(一時保存)
* 投稿者名+input名でファイル名を生成
*/
private function _handle_file_upload_temp() {
$result = [
'files' => [],
'file_info' => [],
'errors' => []
];
// 投稿者名取得(ファイル名用)
$user_name = trim($this->input->post('name'));
$safe_user_name = $this->_sanitize_filename($user_name);
// 複数のファイル入力フィールドに対応
$file_inputs = [
'application_form' => '申請書',
'resume' => '履歴書',
'portfolio' => 'ポートフォリオ',
'certificate' => '証明書',
'other_document' => 'その他資料'
];
// 単一ファイルの場合(従来互換)
if (isset($_FILES['upload_file']) && !empty($_FILES['upload_file']['name'])) {
$file_inputs['upload_file'] = '添付ファイル';
}
foreach ($file_inputs as $input_name => $display_name) {
if (!isset($_FILES[$input_name]) || empty($_FILES[$input_name]['name'])) {
continue; // ファイルが選択されていない場合はスキップ
}
// 一時保存用フォルダ
$temp_path = './uploads/temp/';
if (!is_dir($temp_path)) {
mkdir($temp_path, 0755, true);
}
// ファイル情報を保存
$original_name = $_FILES[$input_name]['name'];
$file_size = $_FILES[$input_name]['size'];
$file_type = $_FILES[$input_name]['type'];
$extension = pathinfo($original_name, PATHINFO_EXTENSION);
// 投稿者名+input名でファイル名生成
// 例:田中太郎_申請書_20241209143022.xlsx
$timestamp = date('YmdHis');
$safe_filename = "{$safe_user_name}_{$display_name}_{$timestamp}." . strtolower($extension);
// アップロード設定(エクセルファイル対応)
$config = [
'upload_path' => $temp_path,
'allowed_types' => 'jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt|zip|csv',
'max_size' => 10240, // 10MB(エクセルファイルは大きくなりがち)
'max_width' => 3000,
'max_height' => 3000,
'file_name' => $safe_filename, // カスタムファイル名
'remove_spaces' => TRUE
];
$this->upload->initialize($config);
if ($this->upload->do_upload($input_name)) {
$upload_data = $this->upload->data();
// アップロードされたファイル情報を保存
$result['files'][] = $upload_data['file_name'];
$result['file_info'][] = [
'original_name' => $original_name,
'saved_name' => $upload_data['file_name'],
'input_name' => $input_name,
'input_display_name' => $display_name,
'file_size' => $file_size,
'file_type' => $file_type,
'extension' => $extension,
'upload_time' => date('Y-m-d H:i:s'),
'user_name' => $user_name
];
} else {
$result['errors'][$input_name] = strip_tags($this->upload->display_errors());
}
}
return $result;
}
/**
* ファイル名用の文字列サニタイズ
*/
private function _sanitize_filename($name) {
// 日本語をローマ字に変換(簡易版)
$name = trim($name);
// 特殊文字を除去
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
// スペースをアンダースコアに変換
$name = preg_replace('/\s+/', '_', $name);
// 長すぎる場合は切り詰め
if (mb_strlen($name) > 20) {
$name = mb_substr($name, 0, 20);
}
// 空の場合はデフォルト名
if (empty($name)) {
$name = 'user_' . date('mdHis');
}
return $name;
}
/**
* フォーム表示
*/
private function _show_form($data) {
$this->smarty->assign('errors', $data['errors']);
$this->smarty->assign('form_data', $data['form_data']);
$this->smarty->display('contact/form.tpl');
}
/**
* 確認画面表示(ファイル名表示対応)
*/
private function _show_confirm($data) {
$this->smarty->assign('form_data', $data['form_data']);
$this->smarty->assign('uploaded_files', $data['uploaded_files']);
$this->smarty->assign('file_info', $data['file_info']);
$this->smarty->display('contact/confirm.tpl');
}
}
?>
<!--
==============================================
フォームテンプレート: contact/form.tpl
==============================================
-->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>お問い合わせフォーム</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; }
.form-group { margin-bottom: 20px; }
.error { color: red; font-size: 14px; margin-top: 5px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
input[type="text"], input[type="email"], textarea {
width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px;
}
textarea { resize: vertical; min-height: 100px; }
.file-input-group {
border: 1px solid #ddd; border-radius: 4px; padding: 15px; margin-bottom: 15px; background: #fafafa;
}
.file-input-title {
font-weight: bold; color: #495057; margin-bottom: 10px;
border-bottom: 1px solid #dee2e6; padding-bottom: 5px;
}
.file-info-text { font-size: 12px; color: #666; margin-top: 5px; line-height: 1.4; }
.file-preview {
background: #f5f5f5; padding: 10px; margin-top: 10px; border-radius: 4px; display: none;
}
.selected-file {
margin: 5px 0; padding: 10px; background: white; border: 1px solid #ddd;
border-radius: 3px; border-left: 4px solid #007bff;
}
.excel-icon::before { content: "📊 "; }
.pdf-icon::before { content: "📄 "; }
.doc-icon::before { content: "📝 "; }
.image-icon::before { content: "🖼️ "; }
.btn {
background: #007bff; color: white; padding: 12px 30px; border: none;
border-radius: 4px; font-size: 16px; cursor: pointer;
}
.btn:hover { background: #0056b3; }
</style>
</head>
<body>
<div class="container">
<h2>お問い合わせフォーム</h2>
<form method="post" enctype="multipart/form-data" action="{site_url('contact')}">
<!-- 名前 -->
<div class="form-group">
<label for="name">お名前 *</label>
<input type="text" id="name" name="name" value="{$form_data.name|default:''|escape}" placeholder="山田太郎">
<div class="file-info-text">※ファイル名に使用されます</div>
{if isset($errors.name)}
<div class="error">{$errors.name}</div>
{/if}
</div>
<!-- メールアドレス -->
<div class="form-group">
<label for="email">メールアドレス *</label>
<input type="email" id="email" name="email" value="{$form_data.email|default:''|escape}" placeholder="example@example.com">
{if isset($errors.email)}
<div class="error">{$errors.email}</div>
{/if}
</div>
<!-- メッセージ -->
<div class="form-group">
<label for="message">お問い合わせ内容 *</label>
<textarea id="message" name="message" rows="6" placeholder="お問い合わせ内容をご記入ください">{$form_data.message|default:''|escape}</textarea>
{if isset($errors.message)}
<div class="error">{$errors.message}</div>
{/if}
</div>
<!-- ファイルアップロード -->
<div class="form-group">
<h3>ファイル添付(任意)</h3>
<!-- 申請書 -->
<div class="file-input-group">
<div class="file-input-title excel-icon">申請書(Excelファイル推奨)</div>
<input type="file" id="application_form" name="application_form"
accept=".xlsx,.xls,.pdf,.doc,.docx"
onchange="showSelectedFile(this, 'application_form_preview')">
<div class="file-info-text">保存名例: 山田太郎_申請書_20241209143022.xlsx</div>
<div id="application_form_preview" class="file-preview"></div>
{if isset($errors.application_form)}
<div class="error">{$errors.application_form}</div>
{/if}
</div>
<!-- 履歴書 -->
<div class="file-input-group">
<div class="file-input-title doc-icon">履歴書</div>
<input type="file" id="resume" name="resume"
accept=".pdf,.doc,.docx,.xlsx,.xls"
onchange="showSelectedFile(this, 'resume_preview')">
<div class="file-info-text">保存名例: 山田太郎_履歴書_20241209143022.pdf</div>
<div id="resume_preview" class="file-preview"></div>
{if isset($errors.resume)}
<div class="error">{$errors.resume}</div>
{/if}
</div>
<!-- ポートフォリオ -->
<div class="file-input-group">
<div class="file-input-title image-icon">ポートフォリオ</div>
<input type="file" id="portfolio" name="portfolio"
accept=".pdf,.jpg,.jpeg,.png,.zip,.xlsx,.xls"
onchange="showSelectedFile(this, 'portfolio_preview')">
<div class="file-info-text">保存名例: 山田太郎_ポートフォリオ_20241209143022.pdf</div>
<div id="portfolio_preview" class="file-preview"></div>
{if isset($errors.portfolio)}
<div class="error">{$errors.portfolio}</div>
{/if}
</div>
<!-- 証明書 -->
<div class="file-input-group">
<div class="file-input-title pdf-icon">証明書・資格証</div>
<input type="file" id="certificate" name="certificate"
accept=".pdf,.jpg,.jpeg,.png"
onchange="showSelectedFile(this, 'certificate_preview')">
<div class="file-info-text">保存名例: 山田太郎_証明書_20241209143022.pdf</div>
<div id="certificate_preview" class="file-preview"></div>
{if isset($errors.certificate)}
<div class="error">{$errors.certificate}</div>
{/if}
</div>
<!-- その他資料 -->
<div class="file-input-group">
<div class="file-input-title">その他資料</div>
<input type="file" id="other_document" name="other_document"
accept=".pdf,.doc,.docx,.xlsx,.xls,.jpg,.jpeg,.png,.zip,.txt"
onchange="showSelectedFile(this, 'other_document_preview')">
<div class="file-info-text">保存名例: 山田太郎_その他資料_20241209143022.zip</div>
<div id="other_document_preview" class="file-preview"></div>
{if isset($errors.other_document)}
<div class="error">{$errors.other_document}</div>
{/if}
</div>
<!-- アップロード仕様説明 -->
<div style="background: #e7f3ff; padding: 15px; border-radius: 4px; margin-top: 15px;">
<h4 style="margin-top: 0;">📋 ファイルアップロード仕様</h4>
<ul style="margin: 0; padding-left: 20px;">
<li><strong>最大ファイルサイズ:</strong> 10MB</li>
<li><strong>対応形式:</strong> Excel(.xlsx/.xls), PDF, Word(.doc/.docx), 画像(JPG/PNG), ZIP, CSV, TXT</li>
<li><strong>ファイル名:</strong> 自動的に「お名前_種類_日時」の形式で保存されます</li>
<li><strong>注意:</strong> Excelファイルは互換性のため.xlsx形式を推奨します</li>
</ul>
</div>
</div>
<!-- 送信ボタン -->
<div class="form-group">
<input type="hidden" name="action" value="confirm">
<button type="submit" class="btn">確認画面へ</button>
</div>
</form>
</div>
<script>
// 選択されたファイル名を表示する関数
function showSelectedFile(input, previewId) {
const preview = document.getElementById(previewId);
if (input.files && input.files.length > 0) {
const file = input.files[0];
const userName = document.getElementById('name').value || 'ユーザー名未入力';
const inputDisplayName = getInputDisplayName(input.name);
// 予想されるファイル名を生成
const extension = file.name.split('.').pop();
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
const predictedName = `${userName}_${inputDisplayName}_${timestamp}.${extension}`;
// ファイル情報を表示
preview.innerHTML = `
<div class="selected-file">
<strong>📎 選択されたファイル:</strong> ${file.name}<br>
<strong>💾 保存予定名:</strong> ${predictedName}<br>
<strong>📏 サイズ:</strong> ${formatFileSize(file.size)}<br>
<strong>📋 種類:</strong> ${file.type || '不明'} (${getFileIcon(extension)} ${extension.toUpperCase()})
</div>
`;
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
}
// input名から表示名を取得
function getInputDisplayName(inputName) {
const names = {
'application_form': '申請書',
'resume': '履歴書',
'portfolio': 'ポートフォリオ',
'certificate': '証明書',
'other_document': 'その他資料'
};
return names[inputName] || '添付ファイル';
}
// ファイル拡張子からアイコンを取得
function getFileIcon(extension) {
const ext = extension.toLowerCase();
if (['xlsx', 'xls', 'csv'].includes(ext)) return '📊';
if (['pdf'].includes(ext)) return '📄';
if (['doc', 'docx'].includes(ext)) return '📝';
if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) return '🖼️';
if (['zip', 'rar'].includes(ext)) return '📦';
return '📄';
}
// ファイルサイズを読みやすい形式に変換
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 名前が変更されたときにファイル名プレビューを更新
document.getElementById('name').addEventListener('input', function() {
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
if (input.files && input.files.length > 0) {
const previewId = input.id + '_preview';
showSelectedFile(input, previewId);
}
});
});
</script>
</body>
</html>
<!--
==============================================
確認画面テンプレート: contact/confirm.tpl
==============================================
-->
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>入力内容確認</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; }
.confirm-box { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
.confirm-item { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #dee2e6; }
.confirm-item:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
.confirm-label { font-weight: bold; margin-bottom: 8px; color: #495057; font-size: 14px; }
.confirm-value { padding: 10px; background: white; border-radius: 4px; border: 1px solid #ced4da; min-height: 40px; line-height: 1.5; }
.file-list { margin-top: 10px; }
.file-item {
background: #e7f3ff; padding: 10px; margin: 5px 0; border-radius: 4px;
border-left: 4px solid #007bff;
}
.btn { padding: 12px 30px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; text-decoration: none; display: inline-block; margin: 5px; }
.btn-primary { background: #007bff; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn:hover { opacity: 0.8; }
</style>
</head>
<body>
<div class="container">
<h2>入力内容確認</h2>
<div class="confirm-box">
<!-- 名前 -->
<div class="confirm-item">
<div class="confirm-label">お名前</div>
<div class="confirm-value">{$form_data.name|escape}</div>
</div>
<!-- メールアドレス -->
<div class="confirm-item">
<div class="confirm-label">メールアドレス</div>
<div class="confirm-value">{$form_data.email|escape}</div>
</div>
<!-- メッセージ -->
<div class="confirm-item">
<div class="confirm-label">お問い合わせ内容</div>
<div class="confirm-value">{$form_data.message|nl2br|escape}</div>
</div>
<!-- ファイル情報 -->
<div class="confirm-item">
<div class="confirm-label">添付ファイル</div>
<div class="confirm-value">
{if $file_info && count($file_info) > 0}
<div class="file-list">
{foreach $file_info as $file}
<div class="file-item">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="file-icon">
{if $file.extension == 'xlsx' || $file.extension == 'xls'}📊
{elseif $file.extension == 'pdf'}📄
{elseif $file.extension == 'doc' || $file.extension == 'docx'}📝
{elseif $file.extension == 'jpg' || $file.extension == 'jpeg' || $file.extension == 'png'}🖼️
{else}📄{/if}
</span>
<div style="flex: 1;">
<strong>{$file.input_display_name}</strong><br>
<div style="font-size: 14px; color: #666;">
元ファイル名: {$file.original_name|escape}<br>
保存ファイル名: {$file.saved_name|escape}
</div>
</div>
<div style="text-align: right; font-size: 12px; color: #999;">
{$file.file_size|number_format} bytes<br>
{$file.upload_time}
</div>
</div>
</div>
{/foreach}
<div style="margin-top: 15px; padding: 10px; background: #e7f3ff; border-radius: 4px;">
<strong>📦 ZIPファイル保存先:</strong><br>
<code>uploads/{$smarty.now|date_format:'Y/m/d'}/{$form_data.name|escape}様_{$smarty.now|date_format:'YmdHis'}_xxxxxx/</code>
</div>
</div>
{else}
<div style="color: #6c757d; font-style: italic;">ファイルの添付はありません</div>
{/if}
</div>
</div>
</div>
<!-- 送信フォーム -->
<form method="post" action="{site_url('contact')}">
<!-- 入力データを隠しフィールドで保持 -->
<input type="hidden" name="name" value="{$form_data.name|escape}">
<input type="hidden" name="email" value="{$form_data.email|escape}">
<input type="hidden" name="message" value="{$form_data.message|escape}">
<input type="hidden" name="action" value="submit">
<div style="text-align: center;">
<button type="submit" class="btn btn-primary">送信する</button>
<button type="button" class="btn btn-secondary" onclick="history.back()">戻って修正</button>
</div>
</form>
</div>
</body>
</html>
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme