import React, { useEffect, useMemo, useState } from “react”;
// ——————————
// 業種別テンプレート
// ——————————
const INDUSTRY_TEMPLATES = {
飲食店: [
{ id: “logo_visibility”, label: “ロゴ・店舗名の視認性”, max: 10 },
{ id: “menu_info”, label: “メニュー情報のわかりやすさ”, max: 10 },
{ id: “food_photo”, label: “写真の美味しさ表現”, max: 10 },
{ id: “color_psychology”, label: “色使い(食欲を刺激する色)”, max: 10 },
{ id: “clean_feel”, label: “清潔感の表現”, max: 10 },
{ id: “promo_emphasis”, label: “キャンペーンや価格の強調度”, max: 10 },
{ id: “target_tone”, label: “ターゲット層に合ったトーン”, max: 10 },
],
“美容院・サロン”: [
{ id: “brand_image”, label: “ブランドイメージの表現(高級感/親しみ)”, max: 10 },
{ id: “color_coord”, label: “カラーコーディネート(落ち着き・洗練)”, max: 10 },
{ id: “before_after”, label: “ビフォーアフターの見せ方”, max: 10 },
{ id: “menu_struct”, label: “サービスメニューの整理性”, max: 10 },
{ id: “type_elegance”, label: “フォントの柔らかさ・上品さ”, max: 10 },
{ id: “booking_flow”, label: “予約導線のわかりやすさ”, max: 10 },
],
不動産: [
{ id: “property_photos”, label: “物件写真の見やすさ”, max: 10 },
{ id: “info_hierarchy”, label: “価格・間取り・立地の整理”, max: 10 },
{ id: “trust_color”, label: “配色(信頼感)”, max: 10 },
{ id: “heading_read”, label: “見出しと本文の視認性”, max: 10 },
{ id: “cta_contact”, label: “CTA(問い合わせ/内覧予約)の目立ち度”, max: 10 },
{ id: “map_clarity”, label: “地図・位置情報のわかりやすさ”, max: 10 },
{ id: “brand_trust”, label: “ブランドの信頼感表現”, max: 10 },
],
“医療・クリニック”: [
{ id: “clean_sense”, label: “清潔感の表現”, max: 10 },
{ id: “safe_color”, label: “色(安心感の青・白基調)”, max: 10 },
{ id: “staff_intro”, label: “医師/スタッフ紹介の見せ方”, max: 10 },
{ id: “hours_access”, label: “診療時間・アクセスのわかりやすさ”, max: 10 },
{ id: “expertise”, label: “専門性の表現”, max: 10 },
{ id: “patient_view”, label: “患者視点での情報整理”, max: 10 },
{ id: “cta_reserve”, label: “予約・問い合わせ導線”, max: 10 },
],
教育: [
{ id: “fun_learning”, label: “学びの楽しさ・成果の表現”, max: 10 },
{ id: “readability”, label: “読みやすさ(保護者/子供)”, max: 10 },
{ id: “color_trust_bright”, label: “色(明るさ・信頼性)”, max: 10 },
{ id: “course_compare”, label: “コース/料金の比較のしやすさ”, max: 10 },
{ id: “achievement”, label: “実績や合格事例の強調”, max: 10 },
{ id: “cta_trial”, label: “CTA(体験申込など)の導線”, max: 10 },
],
“IT・BtoB”: [
{ id: “brand_unity”, label: “ロゴ・ブランドカラーの統一感”, max: 10 },
{ id: “tech_trust”, label: “技術力・信頼感のビジュアル”, max: 10 },
{ id: “case_studies”, label: “導入事例の見やすさ”, max: 10 },
{ id: “service_clarity”, label: “サービス説明のわかりやすさ”, max: 10 },
{ id: “cta_docs”, label: “CTA(資料請求/問合せ)の強調”, max: 10 },
{ id: “responsive”, label: “レスポンシブ対応の適切さ”, max: 10 },
{ id: “diagram_read”, label: “図解・複雑情報の可読性”, max: 10 },
],
“建築・リフォーム”: [
{ id: “works_photo”, label: “施工事例写真の魅力”, max: 10 },
{ id: “before_after_comp”, label: “ビフォーアフター比較”, max: 10 },
{ id: “warm_trust_color”, label: “色(温かみ・信頼感)”, max: 10 },
{ id: “service_struct”, label: “サービス内容の整理性”, max: 10 },
{ id: “voice_showcase”, label: “お客様の声の見せ方”, max: 10 },
{ id: “cta_quote”, label: “CTA(見積依頼/相談予約)”, max: 10 },
{ id: “whitespace_space”, label: “余白による空間感”, max: 10 },
],
“アパレル・ファッション”: [
{ id: “product_photo”, label: “商品写真の魅力(質感・色)”, max: 10 },
{ id: “brand_world”, label: “配色(ブランド世界観)”, max: 10 },
{ id: “type_unity”, label: “タイポグラフィの統一性”, max: 10 },
{ id: “model_visual”, label: “モデル着用の見せ方”, max: 10 },
{ id: “promo_price”, label: “キャンペーン/価格訴求の強調”, max: 10 },
{ id: “purchase_flow”, label: “購入導線のわかりやすさ”, max: 10 },
{ id: “brand_story”, label: “ブランドストーリーの表現”, max: 10 },
],
“観光・宿泊”: [
{ id: “scenery_photo”, label: “写真(風景・施設)の魅力”, max: 10 },
{ id: “info_struct”, label: “料金・アクセス・予約の整理”, max: 10 },
{ id: “relax_color”, label: “配色(非日常/リラックス感)”, max: 10 },
{ id: “plan_event”, label: “プラン/イベントの強調”, max: 10 },
{ id: “multi_language”, label: “多言語の視認性”, max: 10 },
{ id: “cta_booking”, label: “CTA(予約/問い合わせ)”, max: 10 },
{ id: “map_access”, label: “地図・アクセスのわかりやすさ”, max: 10 },
],
製造業: [
{ id: “tech_visual”, label: “技術力の視覚表現(写真/図解)”, max: 10 },
{ id: “reliable_color”, label: “配色(信頼・堅牢感)”, max: 10 },
{ id: “product_struct”, label: “製品情報の整理性”, max: 10 },
{ id: “numbers_show”, label: “実績・数字データの見せ方”, max: 10 },
{ id: “font_legible”, label: “フォントの視認性(BtoB向け)”, max: 10 },
{ id: “cta_quote_b2b”, label: “CTA(資料請求・見積依頼)”, max: 10 },
{ id: “quality_safe”, label: “安全性・品質保証の強調”, max: 10 },
],
};
// ——————————
// デフォルト評価項目(共通・各10点満点)
// ——————————
const DEFAULT_ITEMS = [
{ id: “layout_info”, label: “レイアウト – 情報整理”, max: 10 },
{ id: “layout_prac”, label: “レイアウト – 近接・整列・反復・対比”, max: 10 },
{ id: “color_balance”, label: “色 – 配色バランス”, max: 10 },
{ id: “color_access”, label: “色 – 視認性・心理効果”, max: 10 },
{ id: “type_read”, label: “フォント – 読みやすさ・統一感”, max: 10 },
{ id: “type_hierarchy”, label: “フォント – 見出し/本文/強調のバランス”, max: 10 },
{ id: “white_read”, label: “余白 – 読みやすさ”, max: 10 },
{ id: “white_dense”, label: “余白 – 詰め込み感”, max: 10 },
{ id: “fit_target”, label: “全体印象 – ターゲット適合性”, max: 10 },
{ id: “fit_brand”, label: “全体印象 – ブランド/目的適合性”, max: 10 },
];
const STORAGE_KEY = “designChecklistV1”;
function useChecklistState() {
const [items, setItems] = useState(() => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed;
}
} catch (e) {}
return DEFAULT_ITEMS.map((it) => ({ …it, score: “”, comment: “” }));
});
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}, [items]);
return [items, setItems];
}
function clamp(n, min, max) {
const num = Number(n);
if (Number.isNaN(num)) return “”;
return Math.min(Math.max(num, min), max);
}
function toIntOrBlank(v) {
if (v === “” || v === null || v === undefined) return “”;
const n = Number(v);
return Number.isFinite(n) ? Math.round(n) : “”;
}
function ScoreRow({ item, onChange, onRemove }) {
const usableMax = Number(item.max) || 10;
const scoreValue = item.score === “” ? “” : clamp(item.score, 0, usableMax);
return (
onChange({ …item, score: toIntOrBlank(e.target.value) })
}
className=”w-full”
/>
onChange({ …item, score: toIntOrBlank(e.target.value) })
}
className=”w-16 rounded-lg border border-gray-300 px-2 py-1 text-right”
/>
);
}
// ——————————
// 共有: CSV 文字列生成(テストでも使用)
// ——————————
function buildCSV(items) {
const header = [“評価項目”, “配点”, “得点”, “コメント”];
const totalMax = items.reduce((a, b) => a + (Number(b.max) || 0), 0);
const totalScore = items.reduce((a, b) => a + (Number(b.score) || 0), 0);
const pct = totalMax > 0 ? Math.round((totalScore / totalMax) * 100) : 0;
const rows = items.map((it) => [
it.label,
it.max,
it.score || 0,
(it.comment || “”).replace(/\n/g, ” “), // ← 改行をスペースに置換
]);
const csv = [header, …rows, [“総計”, totalMax, totalScore, `${pct}%`]]
.map((r) => r.map((c) => `”${String(c).replace(/”/g, ‘””‘)}”`).join(“,”))
.join(“\n”); // ← 行区切りはLF
return { csv, totals: { totalMax, totalScore, pct } };
}
export default function ChecklistApp() {
const [items, setItems] = useChecklistState();
const [newLabel, setNewLabel] = useState(“”);
const [newMax, setNewMax] = useState(10);
const [imageURL, setImageURL] = useState(“”);
const [savedFlag, setSavedFlag] = useState(false);
const [industry, setIndustry] = useState(“”);
const [testResults, setTestResults] = useState(null);
useEffect(() => {
setSavedFlag(true);
const t = setTimeout(() => setSavedFlag(false), 1200);
return () => clearTimeout(t);
}, [items]);
const totals = useMemo(() => {
const totalMax = items.reduce((a, b) => a + (Number(b.max) || 0), 0);
const totalScore = items.reduce((a, b) => a + (Number(b.score) || 0), 0);
const pct = totalMax > 0 ? Math.round((totalScore / totalMax) * 100) : 0;
return { totalMax, totalScore, pct };
}, [items]);
const applyIndustryTemplate = () => {
if (!industry || !INDUSTRY_TEMPLATES[industry]) return;
if (!confirm(`${industry}のテンプレートで項目を置き換えます。よろしいですか?`)) return;
const tmpl = INDUSTRY_TEMPLATES[industry].map((it) => ({ …it, score: “”, comment: “” }));
setItems(tmpl);
window.scrollTo({ top: 0, behavior: “smooth” });
};
const updateItem = (updated) => {
setItems((prev) => prev.map((it) => (it.id === updated.id ? updated : it)));
};
const removeItem = (target) => {
if (!confirm(`「${target.label}」を削除しますか?`)) return;
setItems((prev) => prev.filter((it) => it.id !== target.id));
};
const addItem = () => {
const label = newLabel.trim();
const max = Number(newMax) || 10;
if (!label) return;
const id = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
setItems((prev) => […prev, { id, label, max, score: “”, comment: “” }]);
setNewLabel(“”);
setNewMax(10);
};
const resetScores = () => {
if (!confirm(“得点とコメントを空にします。よろしいですか?”)) return;
setItems((prev) => prev.map((it) => ({ …it, score: “”, comment: “” })));
};
const loadExample = () => {
const example = [8, 7, 9, 8, 7, 6, 8, 7, 9, 8];
setItems((prev) =>
prev.map((it, idx) => ({
…it,
score: example[idx] ?? 7,
comment:
idx === 0
? “メイン情報は明確。補足はもう少し圧縮可能”
: idx === 1
? “整列は概ね良いが余白が不均一”
: idx === 2
? “3色ルール遵守で統一感あり”
: idx === 3
? “青基調で信頼感。CTAのコントラストを少し強めたい”
: idx === 4
? “本文は読みやすい。見出しのウェイトを+1すると良い”
: idx === 5
? “本文と見出しの差が小さめ”
: idx === 6
? “適度な余白で読みやすい”
: idx === 7
? “一部要素が詰まって見える”
: idx === 8
? “ターゲット適合性は高い”
: “目的合致。CTAの視認性を強化するとさらに良い”,
}))
);
};
const exportCSV = () => {
const { csv } = buildCSV(items);
const blob = new Blob([“\uFEFF” + csv], { type: “text/csv;charset=utf-8;” });
const url = URL.createObjectURL(blob);
const a = document.createElement(“a”);
a.href = url;
a.download = `design_checklist_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
};
const exportJSON = () => {
const blob = new Blob([JSON.stringify({ items, totals }, null, 2)], {
type: “application/json”,
});
const url = URL.createObjectURL(blob);
const a = document.createElement(“a”);
a.href = url;
a.download = `design_checklist_${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
};
const onUploadImage = (e) => {
const file = e.target.files?.[0];
if (!file) return;
const url = URL.createObjectURL(file);
setImageURL((old) => {
if (old) URL.revokeObjectURL(old);
return url;
});
};
// ——————————
// 簡易テスト(自己診断)
// ——————————
const runTests = () => {
const results = [];
// テストデータ
const sample = [
{ id: “t1”, label: “L1”, max: 10, score: 5, comment: “hello\nworld” },
{ id: “t2”, label: “L2”, max: 5, score: 3, comment: ‘He said “ok”‘ },
];
const { csv, totals: t } = buildCSV(sample);
// 1) 改行がスペースに置換されている
results.push({
name: “改行→スペース”,
pass: csv.includes(‘”hello world”‘),
detail: ‘期待: “hello world” を含む’,
});
// 2) ダブルクォートのエスケープ
results.push({
name: “ダブルクォートのエスケープ”,
pass: csv.includes(‘”He said “”ok”””‘),
detail: ‘期待: “He said “”ok””” を含む’,
});
// 3) 改行コードはLFのみ
results.push({
name: “改行コード”,
pass: !/\r/.test(csv),
detail: “期待: CR(\r) を含まない”,
});
// 4) 総計の計算
const expectedLast = ‘”総計”,”15″,”8″,”53%”‘;
results.push({
name: “総計の計算”,
pass: csv.trim().endsWith(expectedLast),
detail: `期待: 最終行が ${expectedLast}`,
});
setTestResults(results);
};
return (
デザイン改善チェックリスト
画像を見ながら、各観点を10点満点で採点しコメントを残せます。入力内容は自動保存されます。
{totals.totalScore} / {totals.totalMax}({totals.pct}%)
{/* アクションバー */}
{/* 業種テンプレ選択 */}
{/* 自己診断 */}
{/* 画像プレビュー */}
{imageURL && (
)}
{/* 追加フォーム */}
評価項目の追加
className=”md:col-span-7 rounded-xl border border-gray-300 px-3 py-2″
/>
setNewMax(clamp(e.target.value, 1, 50))}
className=”w-24 rounded-xl border border-gray-300 px-3 py-2″
/>
点
{/* 評価リスト */}
))}
{/* テスト結果表示 */}
{Array.isArray(testResults) && (
自己診断の結果
-
{testResults.map((r, i) => (
-
{r.pass ? “✔” : “✖”} {r.name}
— {r.detail}
))}
)}
{/* フッターのメモ */}
{/* 印刷用スタイルの微調整 */}
);
}




