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 (

評価項目
{item.label}
配点:{usableMax} 点

得点(0〜{usableMax})

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}%)

{savedFlag ? “保存しました” : “”} 

{/* アクションバー */}

{/* 業種テンプレ選択 */}

業種テンプレ





{/* 自己診断 */}

{/* 画像プレビュー */}
{imageURL && (

参考画像プレビュー
参考画像

)}

{/* 追加フォーム */}

評価項目の追加

setNewLabel(e.target.value)}
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″
/>

{/* 評価リスト */}

{items.map((it) => (

))}

{/* テスト結果表示 */}
{Array.isArray(testResults) && (

自己診断の結果

    {testResults.map((r, i) => (

  • {r.pass ? “✔” : “✖”} {r.name}
    — {r.detail}
  • ))}

)}

{/* フッターのメモ */}

入力内容はブラウザのローカルに自動保存されます。別の端末・ブラウザでは共有されません。

{/* 印刷用スタイルの微調整 */}

);
}