実現したいこと
ファイルアップロード(画像、動画、PDF)が可能なログイン無しのQ&A掲示板を作成したいです。
発生している問題・分からないこと
現在質問画面と質問一覧表示画面を作り終えて回答画面を作成中です。
質問画面と質問一覧表示画面のテーブル構造は下記になります。
回答画面には追加でカラムに質問と回答を紐付けするための回答IDとその回答に対する返信のIDの2つを追加する予定なのですが、質問&回答の両用テーブルで使用するか回答専用テーブルを用意するどちらのほうが良いと思われますでしょうか?
両用にする場合はQ&A掲示板に投稿されたものが質問か回答かを識別するカラムを追加して、質問を「1」、回答を「2」のように意味付けて識別する形で考えております。
どちらもメリット、デメリットがあると思うのでそちらについてもお聞きしたいです。
※質問画面と質問一覧表示画面のテーブル構造
ID (質問番号)・・・・無限スクロールで表示しているため
TS (投稿日時)・・・・公開期間終了後に質問を cron で削除するため
question (質問内容)・・・・質問投稿文
title (質問タイトル)・・・・質問タイトル
namae (質問者名)・・・・任意なのでない場合は匿名で表示
stamp (リアクションスタンプ)・・・・用意された画像で気持ちを伝える
unique_id (質問UUID)・・・・アップロードされたファイル名の改名に使用
ip (IPアドレス)・・・・セキュリティ保護のために保存
attach1 (アップロードされたファイル)・・・・1、2、3で分けているのは1つ目の画像を一覧表示&1つ目の動画を横スクロールで表示するため
attach2 (IPアドレス)・・・・略
attach3 (IPアドレス)・・・・略
usericon (アイコン画像)・・・・アップロードされた画像を表示、アップロードがない場合は代替え画像を表示する
該当のソースコード
PHP
1<?php 2/* 3Template Name: input 4固定ページ: 入力画面 5*/ 6get_header(); 7?> 8<?php 9if (!$noindexaccess) { 10exit('不正アクセス'); 11} 12if (empty($_SESSION['token'])) {// 悪意のある攻撃者があらかじめ作成したコードが実行されてしまうのを防ぐ 13$_SESSION['token'] = bin2hex(random_bytes(16)); 14} 15$attach = []; 16if (!empty($_SESSION['attach'])) { 17foreach ($_SESSION['attach']['data'] as $i => $data) { 18if (!empty($data)) { 19$base64 = base64_encode($data); 20} 21$type = $_SESSION['attach']['type'][$i]; 22switch ($type) { 23case 'image/jpeg': 24case 'image/png': 25$attach[] = '<img style="height: 100px;" src="data:'.$type.';base64,'.$base64.'">'; 26break; 27case 'video/mp4': 28$attach[] = '<video style="height: 100px;" controls src="data:'.$type.';base64,'.$base64.'">'; 29break; 30case 'application/pdf': 31$attach[] = '<iframe style="height: 100px;" src="data:'.$type.';base64,'.$base64.'"></iframe>'; 32break; 33default: 34$attach[] = ''; 35break; 36} 37} 38} 39$stamp_checked = []; 40$stamp_checked[$stamp] = 'checked'; 41$upload_dir = wp_upload_dir(); 42$camera_url = $upload_dir['baseurl'].'/camera.png'; 43?> 44<h2>入力画面</h2> 45<?php 46foreach ($errors as $error) { 47echo "<p>{$error}</p>"; 48} 49?> 50<div class="board_form_partial" id="js_board_form_partial"> 51<form method="post" enctype="multipart/form-data"> 52<input type="hidden" name="token" value="<?php echo $_SESSION['token']; ?>"> 53<div class="image-partial"> 54<h2>動画・画像をアップロード(Upload video・image)<span class="required">※ファイルサイズ15MB以内、JPG/GIF/PNG/MP4</span></h2> 55<div class="image-selector-button"> 56<label> 57<div class="image-camera-icon"> 58<img src="<?php echo $camera_url; ?>" class="changeImg" style="height: 100px;"> 59</div> 60<input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .pdf, .mp4" style="display: none;"> 61</label> 62<input type="hidden" class="attachdel" name="attachdel[]"> 63<div class="viewer"><?php echo $attach[0]; ?></div> 64<button type="button" class="attachclear">clear</button> 65</div> 66<div class="image-selector-button"> 67<label> 68<div class="image-camera-icon"> 69<img src="<?php echo $camera_url; ?>" class="changeImg" style="height: 100px;"> 70</div> 71<input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .pdf, .mp4" style="display: none;"> 72</label> 73<input type="hidden" class="attachdel" name="attachdel[]"> 74<div class="viewer"><?php echo $attach[1]; ?></div> 75<button type="button" class="attachclear">clear</button> 76</div> 77 78<div class="image-selector-button"> 79<label> 80<div class="image-camera-icon"> 81<img src="<?php echo $camera_url; ?>" class="changeImg" style="height: 100px;"> 82</div> 83<input type="file" class="attach" name="attach[]" accept=".png, .jpg, .jpeg, .pdf, .mp4" style="display: none;"> 84</label> 85<input type="hidden" class="attachdel" name="attachdel[]"> 86<div class="viewer"><?php echo $attach[2]; ?></div> 87<button type="button" class="attachclear">clear</button> 88</div> 89</div> 90<style> 91.hideItems { 92display: none; 93} 94</style> 95<div class="title-partial parts"> <!-- title-partial + parts --> 96<h2>名前(name)<span class="required">※必須</span></h2> 97<div class=parts> 98<input class=input type="text" name="namae" id="name" data-length="32" placeholder="未入力の場合は、匿名で表示されます" value="<?php echo $namae; ?>"> 99<div></div> 100</div> 101</div> 102 103<div class="body-partial parts"><!-- body-partial + parts --> 104<h2>コメント(comment)<span class="required">※必須</span></h2> 105<div class=parts> 106<textarea class=input name="message" id="message" data-length="40" placeholder="荒らし行為や誹謗中傷や著作権の侵害はご遠慮ください"><?php echo $message; ?></textarea> 107<div></div> 108</div> 109</div> 110 111<div class="stamp-partial"> 112<h2>スタンプを選ぶ(必須)</h2> 113<input type="radio" name="stamp" value="1" id="stamp_1" <?php echo $stamp_checked['1']; ?>><label for="stamp_1"></label> 114<input type="radio" name="stamp" value="2" id="stamp_2" <?php echo $stamp_checked['2']; ?>><label for="stamp_2"></label> 115<input type="radio" name="stamp" value="3" id="stamp_3" <?php echo $stamp_checked['3']; ?>><label for="stamp_3"></label> 116<input type="radio" name="stamp" value="4" id="stamp_4" <?php echo $stamp_checked['4']; ?>><label for="stamp_4"></label> 117<input type="radio" name="stamp" value="5" id="stamp_5" <?php echo $stamp_checked['5']; ?>><label for="stamp_5"></label> 118<input type="radio" name="stamp" value="6" id="stamp_6" <?php echo $stamp_checked['6']; ?>><label for="stamp_6"></label> 119<input type="radio" name="stamp" value="7" id="stamp_7" <?php echo $stamp_checked['7']; ?>><label for="stamp_7"></label> 120<input type="radio" name="stamp" value="8" id="stamp_8" <?php echo $stamp_checked['8']; ?>><label for="stamp_8"></label> 121</div> 122<div class="post-button"><!-- ボタンを押せなくする --> 123<button type="submit" id="submit_button" name="mode" value="confirm">表示画面へ進む</button> 124</div> 125<!-- type、name、id、valueの順番 --> 126</form> 127 128<script> 129function validation_submit(f) { 130const submit = document.getElementById("submit_button"); 131/* 判定は逆なので、逆に渡す */ 132submit.disabled = f ? false : true; 133}; 134function validation_text(parts) { 135/* このpartsグループの、inputを抽出 */ 136let text = parts.getElementsByClassName('input')[0]; 137/* 最小チェック */ 138if (text.value.length == 0) { 139return false; 140} 141/* 最大チェック */ 142if (text.value.length >= text.dataset.length) { 143return false; 144} 145return true; 146}; 147/* バリデーション条件判断部分 */ 148function validation() { 149let parts = document.getElementsByClassName('parts'); 150let submit = true; 151for (let i = 0; i < parts.length; i++) { 152if (validation_text(parts[i]) != true) { 153submit = false; 154} 155} 156validation_submit(submit); 157}; 158/* 例えばのチェック */ 159function init() { 160/* カメラ画像をファイルアップロード時に非表示にする */ 161/* 省略 */ 162/* カメラ画像をファイルアップロード時に非表示にする */ 163const attach = document.querySelectorAll('.attach'); 164const del = document.querySelectorAll('.attachdel'); 165const clear = document.querySelectorAll('.attachclear'); 166const viewer = document.querySelectorAll('.viewer'); 167const changeImg = document.querySelectorAll('.changeImg'); // 入力されたら消す画像 168for (let i = 0; i < attach.length; i++) { 169attach[i].addEventListener('change', () => { 170if (attach[i].files[0].size > 15 * 1024 * 1024) { 171alert('ファイルサイズが 15MBバイトを超えています'); 172return; 173} 174del[i].value = ""; 175viewer[i].innerHTML = ""; 176if (attach[i].files.length !== 0) { 177const reader = new FileReader(); 178reader.onload = () => { 179var child = null; 180if (reader.result.indexOf("data:image/jpeg;base64,") === 0 || 181reader.result.indexOf("data:image/png;base64,") === 0) { 182child = document.createElement("img"); 183} else if (reader.result.indexOf("data:video/mp4;base64,") === 0) { 184child = document.createElement("video"); 185child.setAttribute("controls", null); 186} else if (reader.result.indexOf("data:application/pdf;base64,") === 0) { 187child = document.createElement("iframe"); 188} else { 189alert("対象外のファイルです"); 190alert(reader.result); 191attach[i].value = ""; 192} 193if (child !== null) { 194child.style.height = "350px"; 195child.style.width = "528px"; 196child.src = reader.result; 197viewer[i].appendChild(child); 198changeImg[i].classList.add('hideItems'); // もともとの画像を消す 199} 200}; 201reader.readAsDataURL(attach[i].files[0]); 202} 203}); 204clear[i].addEventListener('click', () => { 205attach[i].value = ""; 206del[i].value = "1"; 207viewer[i].innerHTML = ""; 208changeImg[i].classList.remove('hideItems'); 209}); 210} 211/* 文字数表示 */ 212document.addEventListener('input', e => { 213if (!['name', 'message'].includes(e.target.id)) return; 214const 215t = e.target, 216m = t.nextElementSibling, 217n = t.value.length - (t.dataset.length | 0), 218c = document.createElement('span'); 219c.append(Math.abs(n)); 220m.style.color = n > 0 ? 'red' : 'black'; 221m.replaceChildren(n > 0 ? '' : '残り', c, 222`文字${n > 0 ? '超過してい' : '入力でき'}ます。`); 223/* 毎回判定によるボタン制御 */ 224validation(); 225}); 226/* 初回判定のボタン制御 */ 227validation(); 228}; 229document.addEventListener("DOMContentLoaded", init); 230</script>
試したこと・調べたこと
- teratailやGoogle等で検索した
- ソースコードを自分なりに変更した
- 知人に聞いた
- その他
上記の詳細・結果
検索してみたのですが Q&A掲示板についての記事がなく掲示板のコメント返信機能に必要な部分のみ参考にしております。
追記
データ通信量によるかと思いますが回答専用テーブルを用意して質問専用テーブルと分けたほうが良いのではないかと考えております。
7~10日で質問を cron で削除する前提であるものの同時刻に複数の掲示板が動作した場合負荷がかかり遅延に繋がるのではないかと思います。
質問&回答の両用テーブルの場合共通のコードなどはまとめることが可能だと思うのですがそれ以外のメリットがあまり無さそうです。
補足
※参考サイト
https://qiita.com/ryouya3948/items/6928c89607cf4eaa72a0
回答8件
あなたの回答
tips
プレビュー