C:/Users/kawag/work/ 配下に構築された全36プロジェクトの横断分析。医療・介護DXを中心に、臨床評価・業務効率化・学会運営・CRMまで幅広い領域をカバーする。
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| drawing-cognitive-test | Next.js + Firebase | 図形描画による認知機能テスト | Firebase |
| dementia-risk-suite | React + Vite + Cloudflare D1 | 認知症リスク評価(Lancet 2024準拠) | Cloudflare |
| dementia-risk-check | Next.js + Vercel | 認知症リスク判定(簡易版) | Vercel |
| rokomo-check-app | React 19 + Vite + Supabase | ロコモティブシンドローム評価 | — |
| suita-stroke-risk | Next.js + Vercel | 脳卒中リスク評価 | Vercel |
| updrs-tracker | Next.js + Vercel | UPDRS追跡(パーキンソン病) | Vercel |
| visitcare | Next.js 14 + Supabase | 訪問リハビリケア記録・管理 | Vercel |
| motion-analyzer | Next.js + Vercel | 運動分析・動作解析 | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| endai-system | Next.js 16 + Supabase | 学会演題管理プラットフォーム | Vercel |
| editorial-manager | Next.js 15 + Supabase + Puppeteer | 論文投稿→査読→抄録集PDF生成 | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| leave-management-web | Next.js 14 + Firebase | 有給休暇申請・承認管理 | Vercel |
| leave-management-supabase | Next.js 14 + Supabase | 同上(Supabase版) | Vercel |
| nurse-expense-app | Next.js 14 + Firebase + Gemini | 訪問看護経費精算PWA | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| line-crm | Next.js 15 + Supabase + LINE API | LINE公式CRM | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| productivity-health-survey | React 19 + Vite | 従業員健康・エンゲージメント調査 | Vercel |
| guideline-quiz | Next.js 14 + SQLite | ガイドライン理解度テスト | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| VoiceClip-Online | React 19 + Vite + faster-whisper | 音声文字起こし | Vercel |
| VoiceClip | Python(ローカル) | オフライン音声文字起こし | — |
| docu-scan | Next.js 14 + Claude Vision + GAS | 書類スキャナーPWA | Vercel |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| rehab-monitoring-system | Flutter + Firebase | リハビリ活動モニタリング | Firebase |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| kinki-rehab-watcher | Cloudflare Workers | 近畿厚生局改定情報監視→Discord通知 | Cloudflare |
| service-health-monitor | Cloudflare Workers | サービスヘルスモニタリング | Cloudflare |
| プロジェクト | スタック | 用途 |
|---|---|---|
| design-system | Next.js + Tailwind v4 | 医療DX向けUIコンポーネント集 |
| dashboard-design-system | Next.js | ダッシュボード用デザインシステム |
| プロジェクト | スタック | 用途 | デプロイ先 |
|---|---|---|---|
| hirakata-pt-hp | HTML + CSS + TS | 企業HP | Cloudflare Pages |
| フレームワーク | 件数 | 用途 |
|---|---|---|
| Next.js | 20+ | メインフレームワーク |
| React + Vite | 5 | 軽量SPA |
| Flutter | 1 | モバイル+Web |
| Cloudflare Workers | 2 | サーバーレス |
| Python | 2 | ローカルツール |
| サービス | 件数 | 特徴 |
|---|---|---|
| Supabase | 8+ | PostgreSQL + RLS + Auth |
| Firebase | 6+ | Firestore + Auth + Hosting |
| Cloudflare D1 | 1 | エッジDB |
| SQLite | 1 | ローカルDB |
| LocalStorage | 2 | サーバーレス |
| プラットフォーム | 件数 | 用途 |
|---|---|---|
| Vercel | 20+ | Next.jsデプロイ |
| Firebase Hosting | 3 | Firebase統合プロジェクト |
| Cloudflare Pages/Workers | 4 | 静的サイト・Workers |
| AI | プロジェクト | 用途 |
|---|---|---|
| Claude API (Sonnet) | endai-system | 演題フォーマット検査・カテゴリ提案 |
| Claude Vision | docu-scan | 書類OCR・文字抽出 |
| Gemini Vision | nurse-expense-app | 領収書OCR |
| faster-whisper | VoiceClip-Online | 音声文字起こし |
フレイルとリハマネ加算の関係は、「対象者像(誰に)」と「提供の仕組み(どう届け・算定するか)」の関係である。
| 側面 | フレイル | リハマネ加算 |
|---|---|---|
| 役割 | 対象者の状態像 | サービス提供・算定の仕組み |
| 評価 | KCL、Friedの5項目 | FIM、Barthel Index(LIFE提出) |
| 介入 | 改善可能な段階 | PDCAサイクルによる計画的介入 |
| 期間 | 短期集中〜維持期 | 月次算定(リハ会議3か月ごと) |
KCL等でフレイルと判定された高齢者が、介護予防・訪問リハビリの対象となる。
リハマネ加算(B)では、多職種によるリハビリテーション会議でフレイル状態に応じた計画を策定する。
LIFEへのデータ提出(FIM、Barthel Index等)がフレイル改善の定量的エビデンスとなる。
本Wikiが対象とする事業体は、医療法人と株式会社の2法人で構成される。臨床(訪問リハビリ)・研究(フレイル改善解析)・DX(SaaS/BPO)を三位一体で展開し、高齢者医療・介護領域のDXを推進する。
Firebaseには「ブラウザ用の道具」と「サーバー用の道具」がある。この2つの違いを理解すると、Firebaseプロジェクトのコードが読めるようになる。
生徒(ブラウザ)が学生証(Client SDK)を使って図書室(Firestore)に入る。学生証には「何年何組のだれか」が書いてあるので、図書室のルール(Security Rules)が「この生徒は何の本を借りていいか」を判定する。
校長先生(サーバー)は鍵束(Admin SDK)で図書室の裏口から入れる。ルール(Security Rules)を完全に無視できる。全部の本を見れるし、生徒の借り出し記録も変更できる。
// nurse-expense-app: src/lib/firebase.ts
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Firebase の設定(環境変数から取得)
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
// ...
};
// 同じアプリが何度も作られないようにチェック
// (Reactは再レンダリングで何度もこのコードを実行するから)
if (!firebaseConfig.apiKey) return null; // ビルド時は環境変数がない
const app = getApps().length > 0 ? getApps()[0] : initializeApp(firebaseConfig);
export const auth = getAuth(app); // ログイン/ログアウト用
export const db = getFirestore(app); // データベース読み書き用
// bento_order_web: src/contexts/AuthContext.tsx の仕組み
// 「ログイン状態が変わったら教えて」と登録する
const unsubscribe = onAuthStateChanged(auth, async (fbUser) => {
// fbUser が null → ログアウト状態
// fbUser がある → ログイン状態
if (fbUser) {
// Firestoreから追加情報を取得(名前、権限など)
const userData = await getUserData(fbUser.uid);
setUser(userData);
} else {
setUser(null);
}
});
// コンポーネントが消える時に監視を解除(メモリリーク防止)
return () => unsubscribe();
なぜuseEffectの中で使うか?: onAuthStateChangedは「ずっと見張り続ける」仕組み。コンポーネントが表示された時に監視を開始し、消える時に解除する。これがReactのuseEffectの役割。
// bento_order_web: src/services/firestore.ts
// 「メニューが変わったら教えて」と登録する
export function subscribeToMenuItems(callback) {
const q = query(collection(db, "menu_items"), orderBy("createdAt", "desc"));
// onSnapshot = データが変わるたびに自動で呼ばれる
return onSnapshot(q, (snapshot) => {
const items = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
callback(items); // 新しいデータでUIを更新
});
}
普通のデータ取得(getDocs)との違い:
getDocs = 1回だけ取得して終わり(写真を撮る)onSnapshot = 変更があるたびに自動更新(ライブカメラ)bento_order_webでは注文がリアルタイムで更新されるので、onSnapshotを使っている。
// visitcare: src/app/actions/auth.ts(Server Actions内)
"use server";
// Admin SDK はサーバーでしか動かない
import { getFirestore } from "firebase-admin/firestore";
export async function updatePatientRecord(patientId, data) {
const db = getFirestore();
// トランザクション = 「途中で失敗したら全部なかったことにする」仕組み
await db.runTransaction(async (transaction) => {
const patientRef = db.doc(patients/${patientId});
const patient = await transaction.get(patientRef);
if (!patient.exists) {
throw new Error("患者が見つかりません");
}
// 更新(トランザクション内なので、途中で他の人が同じデータを
// 変更しようとしたら、やり直しになる)
transaction.update(patientRef, {
...data,
updatedAt: new Date(),
});
});
}
Client SDKを使う時は、Security Rulesがデータを守る。
// bento_order_web: firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 注文データ: ログインユーザーだけ読み書きできる
match /orders/{orderId} {
allow read: if request.auth != null;
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.userId;
// ↑ 自分のユーザーIDが入った注文だけ作れる(なりすまし防止)
}
// メニュー: 誰でも読める、管理者だけ書ける
match /menu_items/{itemId} {
allow read: if true;
allow write: if request.auth != null
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
}
}
読み方:
request.auth != null → ログインしているrequest.auth.uid → ログインしている人のIDallow read: if 条件 → 条件がTRUEならデータを読めるallow write: if 条件 → 条件がTRUEならデータを書ける質問: そのコードはどこで動く?
→ ブラウザ(React コンポーネントの中)
→ Client SDK ✅
→ Security Rulesがデータを守る
→ サーバー(Server Actions, API Routes)
→ Admin SDK ✅
→ Security Rulesをバイパスできる(= 自分でチェックする責任がある)
| 特徴 | Client SDK | Admin SDK |
|---|---|---|
| 動く場所 | ブラウザ | サーバー |
| Security Rules | 効く | 効かない |
| ログイン情報 | 自動で付く | 手動で管理 |
| リアルタイム | onSnapshot使える | 使えない |
| 初期化 | 環境変数(NEXT_PUBLIC_) | サービスアカウントJSON |
| 特徴 | Firebase | Supabase |
|---|---|---|
| ブラウザ側の保護 | Security Rules | RLS (Row Level Security) |
| サーバー側のバイパス | Admin SDK | Service Role Key |
| リアルタイム | onSnapshot(強力) | Realtime(あるが弱め) |
| SQL使える? | ❌ NoSQL | ✅ PostgreSQL |
Middlewareは、ユーザーがページを開く「前」に動くコード。「この人はこのページを見ていいか?」を入口でチェックする門番のような存在。
ビル(Webアプリ)に入ろうとする人を、入口(Middleware)で警備員がチェックする。
来訪者が入口に来る
→ 警備員「IDカードを見せてください」
→ IDカードなし → 「ログインページに行ってください」(リダイレクト)
→ IDカードあり → 「何階に行きますか?」
→ 3階(管理者エリア)→ 「管理者IDですか?」
→ YES → 通す
→ NO → 「ダッシュボードに戻ってください」(リダイレクト)
→ 1階(一般エリア)→ 通す
認証は「二重ロック」で守る。Middlewareだけに頼ると危険。
第1層: Middleware(入口の門番)
→ ページに到達する前にチェック
→ 画面遷移をブロック・リダイレクト
→ 「見せない」ことで守る
第2層: Server Actions / API Routes内のチェック(金庫の鍵)
→ データを操作する直前にチェック
→ 万が一Middlewareをすり抜けても、データは守られる
→ 「触らせない」ことで守る
なぜ二重にするか?: Middlewareは「ページを見せるかどうか」しか制御できない。ブラウザの開発者ツールから直接APIを叩かれたら、Middlewareは通らない。
// endai-system: middleware.ts の考え方
// 「どのページに何のロール(役割)が必要か」を定義
const ADMIN_PATHS = ["/admin"]; // 管理者だけ
const REVIEWER_PATHS = ["/review"]; // 査読者以上
const AUTH_PATHS = ["/dashboard"]; // ログインしていればOK
// ロール(役割)の強さの順番
const ROLE_HIERARCHY = {
admin: 4, // 管理者(一番強い)
organizer: 3, // 運営者
reviewer: 2, // 査読者
author: 1, // 著者(一番弱い)
};
// 実際のチェック処理
export async function middleware(request) {
// 1. ログインしているか確認
const user = await getUser(request);
if (!user) {
// ログインしていない → ログインページへ
return NextResponse.redirect("/login");
}
// 2. そのページに必要なロールを持っているか確認
if (isAdminPage && userRoleLevel < ROLE_HIERARCHY.organizer) {
// 管理者ページに一般ユーザーが来た → ダッシュボードへ
return NextResponse.redirect("/dashboard");
}
// 3. OK → そのまま通す
return NextResponse.next();
}
// endai-system: src/lib/auth.ts
"use server";
// ロールの強さの順番(Middlewareと同じ定義を持つ)
const ROLE_HIERARCHY = {
admin: 4, organizer: 3, reviewer: 2, author: 1
};
// 「この操作には最低○○のロールが必要」をチェックする関数
export async function requireRole(minimumRole: UserRole) {
// 1. Supabaseからログインユーザー情報を取得
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error("認証エラー: ログインが必要です");
}
// 2. そのユーザーのロール(役割)をDBから取得
const { data: profile } = await supabase
.from("profiles").select("role").eq("id", user.id).single();
const userRole = profile?.role || "author";
// 3. ロールの強さを比較
if (ROLE_HIERARCHY[userRole] < ROLE_HIERARCHY[minimumRole]) {
throw new Error("権限エラー: この操作には権限がありません");
}
return { user, role: userRole };
}
使い方:
// Server Action内で使う
"use server";
export async function deleteAbstract(id: string) {
// 管理者以上でないと削除できない
await requireRole("organizer");
// ... 削除処理
}
bento_order_webはVite+Reactなので、Middlewareがない。代わりにReact Contextでブラウザ側で管理する。
// bento_order_web: src/contexts/AuthContext.tsx の考え方
// 「ログイン状態が変わったら教えてね」とFirebaseに頼む
useEffect(() => {
const unsubscribe = onAuthChange(async (fbUser) => {
if (fbUser) {
// ログインした → ユーザー情報をDBから取得
const userData = await getUserData(fbUser.uid);
setUser(userData); // アプリ全体に共有
} else {
// ログアウトした
setUser(null);
}
setLoading(false);
});
return unsubscribe; // クリーンアップ
}, []);
⚠️ 要確認:ブラウザ側のチェックだけでは不十分。Firestore Security Rulesが第2層の役割を果たしている。
| プロジェクト | 第1層(門番) | 第2層(金庫) |
|---|---|---|
| endai-system | middleware.ts(ロール階層) | requireRole()(Server Actions内) |
| line-crm | なし(管理者専用アプリ) | Supabase RLS |
| visitcare | middleware.ts | Server Actions + Firestore Rules |
| nurse-expense-app | なし(Firebaseのみ) | Firestore Security Rules |
| bento_order_web | なし(React Context判定) | Firestore Security Rules |
質問1: ロール(役割)が複数ある?
→ YES → middleware.ts でページレベルの制御が必要
→ NO → middleware不要(Firebase Rulesで十分)
質問2: Next.jsを使っている?
→ YES → Server Actions内でも requireRole() / requireAuth() でチェック
→ NO → Firestore Security Rules / Supabase RLS に頼る
質問3: 外部サービスからのアクセスがある?
→ YES → API Routes内で署名検証(line-crmパターン)
→ NO → 通常の認証フローでOK
Server ActionsとAPI Routesは、どちらも「サーバー側で動くコード」だが、役割が違う。この2つの使い分けが、Next.jsプロジェクトの設計の基本になる。
教室(ブラウザ)にいる生徒(ユーザー)が、先生(サーバー)に直接手を挙げて質問する。先生はその場で答えを返す。教室の中だけの会話。
ボタンを押す → サーバーが処理する → 結果が画面に反映される
学校の受付窓口(URL)があって、誰でもそこに来れば対応してもらえる。生徒だけでなく、郵便屋さん(外部サービス)や他の学校(別のアプリ)も使える。
HTTPリクエスト → サーバーが処理する → JSONを返す
endai-systemでは40以上のServer Actionsがある(src/lib/actions/)。
// ファイルの先頭に "use server" と書くだけで、サーバーで動くコードになる
"use server";
// 演題を保存するServer Action
export async function saveAbstract(data: FormData) {
// 1. ログイン確認(サーバー側で安全にチェック)
const { user } = await requireAuth();
// 2. データを検証(おかしなデータを弾く)
const parsed = abstractSchema.safeParse(data);
if (!parsed.success) {
return { error: "入力内容に問題があります" };
}
// 3. データベースに保存
const supabase = await createClient();
await supabase.from("abstracts").insert({ ...parsed.data, author_id: user.id });
return { success: "保存しました" };
}
ポイント: ブラウザから関数を直接呼ぶように見えるが、実際にはNext.jsが裏でHTTPリクエストに変換している。
line-crmではServer Actionsを使わず、API Routesだけで動いている。
// app/api/webhook/line/route.ts
export async function POST(request: Request) {
// 1. LINEから送られてきたデータを受け取る
const body = await request.text();
const signature = request.headers.get("x-line-signature")!;
// 2. 本当にLINEから来たか確認(署名検証)
const isValid = await verifySignature(body, signature);
if (!isValid) {
return new Response("不正なリクエスト", { status: 401 });
}
// 3. メッセージを処理
const events = JSON.parse(body).events;
// ... イベント処理
return new Response("OK");
}
ポイント: LINEのサーバーが呼び出す「窓口」なので、API Routesでないと対応できない。
質問: そのコードを呼び出すのは誰?
→ 自分のアプリのブラウザ画面だけ
→ Server Actions を使う ✅
→ 外部サービス(LINE, Stripe, GAS等)も呼び出す
→ API Routes を使う ✅
→ ファイルダウンロード(CSV, PDF等)を返したい
→ API Routes を使う ✅
| プロジェクト | Server Actions | API Routes | 理由 |
|---|---|---|---|
| endai-system | 40+ | 少数 | フォーム送信が中心。外部連携(Stripe webhook等)だけAPI |
| line-crm | 0 | 多数 | LINEサーバーからの呼び出しが中心 |
| visitcare | あり | なし | Firestoreに直接接続。外部連携なし |
| nurse-expense-app | なし | あり | CSV出力(ファイルダウンロード)が必要 |
| bento_order_web | — | — | Vite+Reactなので両方なし(Firestore直接接続) |
Server Actionsの方がコードが短くて安全。フォーム処理なら Server Actions 一択。
外部サービスは「URL」にリクエストを送るので、Server Actionsでは受け取れない。
ファイルの先頭に "use server" がないと、ブラウザで動いてしまう(秘密の情報が漏れる危険)。
// ❌ ダメ: re-exportは禁止
"use server";
export { foo } from "./other";
// ✅ OK: import してから export
"use server";
import { foo } from "./other";
export { foo };
Supabaseのデータベースには「誰がどのデータを見れるか」を制御する仕組みがある。これがRLS(Row Level Security)。そしてRLSを「無視」できる特別な鍵がService Role Key。
学校のロッカーを想像しよう。生徒Aは自分のロッカーだけ開けられる。生徒Bも自分のだけ。たとえハッカーが「全員のロッカーを開けろ」と命令しても、自分の鍵では自分のしか開かない。
-- 「自分のデータだけ読める」というルール
CREATE POLICY "自分のデータだけ読める"
ON abstracts FOR SELECT
USING (author_id = auth.uid());
-- auth.uid() = 今ログインしている人のID
-- ハッカーが全件取得しようとしても、自分のデータしか返ってこない
校長先生は全員のロッカーを開けられる。でもこの鍵は校長室(サーバー)にしかない。生徒(ブラウザ)には絶対渡さない。
// endai-system: src/lib/supabase/server.ts
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
SUPABASE_URL,
SUPABASE_ANON_KEY, // ← 一般の鍵(RLSが効く)
{ cookies: { getAll() { return cookieStore.getAll(); }, ... } }
);
}
何が起きているか:
// line-crm: lib/supabase/server.ts
export function createServiceClient() {
return createServerClient(
SUPABASE_URL,
SUPABASE_SERVICE_ROLE_KEY!, // ← マスターキー(RLSを無視)
{ cookies: { getAll: () => [], setAll: () => {} } } // cookieは不要
);
}
なぜcookieが空なのか: マスターキーで接続するので「誰がログインしているか」は関係ない。全データにアクセスできる。
質問: そのデータ操作は「誰」がやっている?
→ ログイン中のユーザー本人
→ 普通のクライアント(Anon Key)✅
→ ユーザーではなく「システム」がやっている
→ Service Roleクライアント ✅
1. 患者AがLINEで「明日休みます」とメッセージを送る
- LINEのサーバーがline-crmのWebhookを呼ぶ
- line-crmがメッセージをデータベースに保存したい
問題: この時「ログインしているユーザー」はいない!
LINEのサーバーが呼んでいるだけ。
普通のクライアントだとRLSで弾かれてしまう。
解決: Service Role Key(マスターキー)でRLSをバイパスする。
| プロジェクト | Anon Key | Service Role | 理由 |
|---|---|---|---|
| endai-system | ✅ | ❌ | 全操作がログインユーザー経由 |
| line-crm | ✅ | ✅ | Webhook(LINEサーバー経由)でDB書き込みが必要 |
| visitcare | ✅ | ❌ | 全操作がログインユーザー経由 |
// ❌ 絶対ダメ: NEXT_PUBLIC_ で始まる環境変数はブラウザに見える
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=xxx
// ✅ OK: NEXT_PUBLIC_ なし → サーバーだけで使える
SUPABASE_SERVICE_ROLE_KEY=xxx
NEXT_PUBLIC_ がつくと、ブラウザのJavaScriptに含まれてしまう。マスターキーが全世界に公開される。
OK な場所:
- API Routes(app/api/の中)
- Webhookハンドラ
NG な場所:
- Server Components(必要ない)
- Client Components(論外)
- Server Actions(ログインユーザーがいるはず)
// endai-system のパターン(推奨)
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
if (!SUPABASE_URL) {
throw new Error("NEXT_PUBLIC_SUPABASE_URL が未設定です");
}
// → 設定忘れをデプロイ前に気づける
-- endai-systemの例: 演題テーブルのRLS
-- 「自分が著者の演題だけ読める」
CREATE POLICY "著者は自分の演題を読める"
ON abstracts FOR SELECT
USING (author_id = auth.uid());
-- 「管理者は全部読める」
CREATE POLICY "管理者は全演題を読める"
ON abstracts FOR SELECT
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role IN ('admin', 'organizer')
)
);
読み方: USING (条件) の条件がTRUEのデータだけ見える/操作できる。
テストは「コードが正しく動いているか自動で確認する仕組み」。手動で毎回ボタンを押して確認する代わりに、コンピュータが代わりにチェックしてくれる。
車を組み立てる前に、エンジン、タイヤ、ブレーキなどの部品を1つずつ検品する。部品(関数)が単体で正しく動くか確認する。
// 「消費税を計算する関数」のテスト
test("1000円の10%は100円", () => {
expect(calcTax(1000, 0.1)).toBe(100);
});
test("0円なら税金も0円", () => {
expect(calcTax(0, 0.1)).toBe(0);
});
組み立てた車を実際に道路で走らせる。エンジンかけて、ハンドル切って、ブレーキ踏んで。ユーザーと同じ操作を自動で再現する。
// 「ログインしてダッシュボードが表示される」テスト
test("ログインできる", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "test@example.com");
await page.fill('[name="password"]', "password123");
await page.click('button[type="submit"]');
// ダッシュボードに遷移したか確認
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toContainText("ダッシュボード");
});
泥棒(ハッカー)のフリをして、家(アプリ)に侵入できないか試す。
# endai-system: npm run security-check
# → APIキーがコードに直書きされていないか
# → SQLインジェクション(悪意のあるSQL)が入り込めないか
# → ログイン情報が漏れる場所がないか
/\
/ \ E2Eテスト(少数・遅い・高コスト)
/ \ → 全体が動くか確認
/------\
/ \ Integrationテスト(中程度)
/ \ → 複数の部品が組み合わさって動くか
/------------\
/ \ Unitテスト(多数・速い・低コスト)
/________________\ → 部品が1つずつ正しいか
下(Unit)ほどたくさん書く。上(E2E)は重要な操作だけ。
endai-system/
├── e2e/ # E2Eテスト(Playwright)
│ ├── auth.spec.ts # ログイン・ログアウト
│ ├── abstract.spec.ts # 演題の登録・編集・削除
│ └── ... (30+ ファイル)
├── src/lib/**/*.spec.ts # Unitテスト(Vitest)
│ ├── auth.spec.ts # 認証関数のテスト
│ └── validations/*.spec.ts # バリデーションのテスト
└── scripts/
└── security-review.sh # セキュリティチェック一括実行
実行コマンド:
# Unitテスト(速い・頻繁に実行)
npx vitest run
# E2Eテスト(遅い・デプロイ前に実行)
npx playwright test
# セキュリティチェック(デプロイ前に実行)
npm run security-check
bento_order_web/
└── e2e/
├── tests/ # テストファイル
└── pages/ # Page Object Model
Page Object Model(POM)とは: テストコードを「ページごとのクラス」にまとめるパターン。UIが変わった時に修正箇所が1箇所で済む。
// Page Object(ページの操作をまとめたクラス)
class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.fill('[name="email"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
}
// テスト(Page Objectを使うと読みやすい)
test("ログインできる", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login("test@example.com", "pass123");
// ...
});
CSP(Content Security Policy)= ブラウザに「どこからのスクリプト/データを許可するか」を伝えるルール。ビルド(tsc/vite build)では検出できないため、ブラウザで実行して初めて分かる。
// Playwrightでコンソールエラーを検出する
test("CSPエラーがないこと", async ({ page }) => {
const errors = [];
// コンソールのエラーを全部記録
page.on("console", msg => {
if (msg.type() === "error") errors.push(msg.text());
});
page.on("pageerror", err => errors.push(err.message));
// ページを開く
await page.goto("http://localhost:3000");
// エラーが0件であることを確認
expect(errors).toHaveLength(0);
});
過去の事故例:
| 事故 | 原因 | ビルドで検出? |
|---|---|---|
| fetchが全部失敗 | CSPのconnect-srcに通信先を書き忘れ | ❌ |
| ボタンが全部無反応 | CSPのscript-srcに'unsafe-inline'がない | ❌ |
| ビルド成功なのに本番で白画面 | CSPがブラウザ実行時にしか効かない | ❌ |
→ 全部Playwrightでブラウザ実行しないと見つからない
質問1: 何をテストしたい?
→ 関数の計算結果・バリデーション
→ Unitテスト(Vitest)✅
→ ユーザーの操作(ログイン→注文→確認)
→ E2Eテスト(Playwright)✅
→ セキュリティ(APIキー漏れ、SQLインジェクション)
→ セキュリティチェック(Semgrep + ESLint)✅
質問2: いつテストを実行する?
→ コードを書いた直後(頻繁)
→ Unitテスト ✅(数秒で終わる)
→ コミット/プッシュ前
→ Unitテスト + E2Eテスト ✅
→ デプロイ前
→ 全部 + CSPチェック + セキュリティチェック ✅
| プロジェクト | Unit | E2E | Security | CSPチェック |
|---|---|---|---|---|
| endai-system | ✅ Vitest | ✅ Playwright (30+) | ✅ Semgrep | ✅ |
| bento_order_web | ❌ | ✅ Playwright | ❌ | ✅ |
| line-crm | ❌ | ❌ | ❌ | ❌ |
| visitcare | ❌ | ❌ | ❌ | ❌ |
| nurse-expense-app | ❌ | ❌ | ❌ | ❌ |
⚠️ 要確認:line-crm, visitcare, nurse-expense-appにはテストがない。特にline-crmはWebhookの署名検証テストが重要。
フレイル (Frailty) は、加齢に伴い心身の活力(運動機能、認知機能等)が低下し、要介護状態に至る前段階の状態を指す。適切な介入により改善・進行抑制が可能であることが特徴。
以下の5項目中3項目以上該当でフレイル、1-2項目でプレフレイル:
| 指標 | 種類 | 用途 |
|---|---|---|
| KCL (基本チェックリスト) | 質問紙 | 介護予防スクリーニング |
| Friedの5項目 | 身体測定+質問 | 研究でのフレイル判定 |
| BREQ-3 | 質問紙 | 運動動機づけ評価 |
| 握力・歩行速度 | 身体測定 | サルコペニア/フレイル判定 |
| 2ステップテスト | 身体測定 | 歩行機能評価 |
フレイル関連研究で使用される主な手法:
リハビリテーションマネジメント加算(リハマネ加算)は、介護保険サービスにおいてリハビリテーションの質を担保するための加算制度である。多職種連携による計画策定と定期的な見直しを要件とする。
⚠️ 要確認:令和8年度改定で加算体系が変更される可能性あり。最新の告示を確認すること。
外部サービス(LINE、Google Apps Script、Claude API等)とWebアプリを繋ぐパターン。大きく分けて「こちらから呼ぶ」と「向こうから呼ばれる」の2種類がある。
自分から相手に電話をかけて、答えを聞く。
docu-scan → Claude Vision APIに画像を送る → 文字認識結果を受け取る
相手から突然電話がかかってくる。受話器を取る人(API Route)が必要。
患者がLINEで「明日休みます」→ LINEサーバー → line-crmのWebhookに通知
// endai-system: src/lib/claude-client.ts の考え方
// 1. 患者名などの個人情報を取り除く(匿名化)
const { anonymizedText } = anonymize(abstractBody, speakerNames);
// 2. Claude APIに送る
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY!, // サーバーだけが持つ鍵
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-6-20250514",
messages: [{ role: "user", content: anonymizedText }],
}),
});
// 3. 結果を受け取る
const result = await response.json();
重要なポイント:
NEXT_PUBLIC_ をつけない)// nurse-expense-app の仕組み
// ブラウザ → GAS(Google Apps Script)→ Gemini Vision API
// 1. ブラウザから撮影画像をGASに送る
const response = await fetch(process.env.NEXT_PUBLIC_GAS_OCR_URL!, {
method: "POST",
body: JSON.stringify({ image: base64Image }),
});
// 2. GAS側(gas/ocr.gs)でGeminiを呼ぶ
// なぜGAS経由? → Gemini APIキーをブラウザに持たせたくないから
// GASは無料でサーバーの役割を果たしてくれる
// 3. GASが結果を返す
const ocrResult = await response.json();
// { amount: 1500, date: "2026-04-01", store: "コンビニA" }
なぜGAS経由にするか?:
直接呼ぶ場合(❌ 危険):
ブラウザ → Gemini API(APIキーがブラウザに見える!)
GAS経由(✅ 安全):
ブラウザ → GAS(APIキーはGAS内に隠れている)→ Gemini API
患者がLINEでメッセージを送る
→ LINEのサーバーが line-crm の /api/webhook/line に POST する
→ line-crm が受け取って処理する
// line-crm: lib/line/webhook.ts
export async function verifySignature(body: string, signature: string) {
const secret = process.env.LINE_CHANNEL_SECRET!;
// 「body(メッセージ内容)」と「secret(秘密の鍵)」を
// 混ぜ合わせて暗号化したものが、LINEが送ってきた signature と一致するか確認
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw", encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const signatureBuffer = await crypto.subtle.sign(
"HMAC", key, encoder.encode(body)
);
const computed = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
return computed === signature; // 一致すればLINE本物
}
なぜ署名検証が必要か?: 悪意のある人が「LINEのふりをして」偽のメッセージを送ってくる可能性がある。署名検証で「本当にLINEから来たのか」を確認する。
署名検証のたとえ: 手紙に蝋印(ろういん)が押してあるようなもの。蝋印のデザイン(secret)を知っているのはLINEと自分だけ。偽物は同じ蝋印を作れない。
// line-crm: app/api/webhook/line/route.ts
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("x-line-signature")!;
// 署名検証
if (!await verifySignature(body, signature)) {
return new Response("不正", { status: 401 });
}
// イベントを処理
const { events } = JSON.parse(body);
for (const event of events) {
if (event.type === "message" && event.message?.type === "text") {
// テキストメッセージを受信した
await handleTextMessage(event);
}
if (event.type === "follow") {
// 友だち追加された
await handleFollow(event);
}
}
return new Response("OK");
}
docu-scan の流れ:
- スマホで書類を撮影
- Claude Vision APIで文字を読み取る
- 読み取ったデータをGASに送る
- GASがスプレッドシートに書き込む
nurse-expense-app の流れ:
- スマホで領収書を撮影
- GASに画像を送る
- GAS内でGemini Visionが文字を読み取る
- 結果をブラウザに返す
質問1: 自分のアプリから外部サービスを呼ぶ? それとも外部サービスから呼ばれる?
→ 自分から呼ぶ
→ APIキーが必要?
→ YES → サーバー側(Server Actions / API Routes)から呼ぶ
→ NO → ブラウザから直接呼んでもOK
→ 外部から呼ばれる(Webhook)
→ API Routes で受け口を作る
→ 署名検証を必ず入れる
→ Service Role Key で DB書き込み(ログインユーザーがいないから)
質問2: 無料でサーバー処理したい?
→ GAS WebApp を中継サーバーとして使う(nurse-expense-appパターン)
| プロジェクト | 外部サービス | パターン | 役割 |
|---|---|---|---|
| endai-system | Claude API | こちらから呼ぶ | 演題チェック・カテゴリ提案 |
| line-crm | LINE Messaging API | 双方向 | メッセージ送受信 |
| docu-scan | Claude Vision | こちらから呼ぶ | 書類OCR |
| nurse-expense-app | Gemini Vision (via GAS) | こちらから呼ぶ | 領収書OCR |
| bento_order_web | GAS WebApp | こちらから呼ぶ | スプレッドシート連携 |
| kinki-rehab-watcher | 近畿厚生局Web | こちらから呼ぶ | 更新情報監視→Discord通知 |
Reactアプリで「データをどこに持つか」を決めるのが状態管理。小さいアプリならuseStateで十分だが、アプリが大きくなると「どのコンポーネントからでもデータにアクセスしたい」問題が出てくる。
自分の机に置いてある筆箱。自分だけが使う。隣の席の人には見えない。
// ボタンを押した回数を数える(このコンポーネント内だけ)
const [count, setCount] = useState(0);
教室に貼ってある掲示板。教室にいる全員が見れる。でも隣の教室には見えない。
// AuthContextの例: 「今ログインしている人は誰か」を教室全体に共有
<AuthProvider> {/* ← 掲示板を教室に設置 */}
<Header /> {/* ← ログインユーザー名を表示できる */}
<Sidebar /> {/* ← ログイン状態に応じてメニューを変えられる */}
<MainContent /> {/* ← ログインユーザーのデータを取得できる */}
</AuthProvider>
放送室からアナウンスすると、全教室に聞こえる。教室(コンポーネント)をまたいで情報を共有できる。しかもContextよりセットアップが簡単。
// visitcareの例: 患者の選択状態をアプリ全体で共有
const usePatientStore = create((set) => ({
selectedPatient: null,
setPatient: (patient) => set({ selectedPatient: patient }),
}));
// どのコンポーネントからでも使える
function PatientHeader() {
const patient = usePatientStore(state => state.selectedPatient);
return <h1>{patient?.name}</h1>;
}
// フォームの入力値、モーダルの開閉、ローディング状態など
const [isOpen, setIsOpen] = useState(false);
const [formData, setFormData] = useState({ name: "", email: "" });
使う場面: そのコンポーネントだけで使うデータ
// bento_order_web: src/contexts/AuthContext.tsx
// ログインユーザー情報 = めったに変わらない → Context向き
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
// ... Firebase Auth 監視
return (
<AuthContext.Provider value={{ user, loading }}>
{children}
</AuthContext.Provider>
);
}
// 使う側
function ProfilePage() {
const { user } = useAuth(); // どこからでもユーザー情報にアクセス
return <p>{user.name}さん、こんにちは</p>;
}
使う場面: 認証情報、テーマ(ダークモード)、言語設定など
注意点: Contextの値が変わると、その中の全コンポーネントが再描画される。頻繁に変わるデータには向かない。
// visitcareのパターン
import { create } from "zustand";
// store(データの置き場)を定義
const usePatientStore = create((set) => ({
// データ
selectedPatient: null,
records: [],
// データを変更する関数
setPatient: (patient) => set({ selectedPatient: patient }),
addRecord: (record) => set((state) => ({
records: [...state.records, record] // 既存の配列に追加(immutable)
})),
}));
Contextより優れている点:
// endai-systemのパターン
// Server Component = サーバーで動く → データベースに直接アクセスできる
// app/dashboard/page.tsx(Server Component)
export default async function DashboardPage() {
// サーバーで直接データを取得(ブラウザには取得コードが送られない)
const supabase = await createClient();
const { data: abstracts } = await supabase.from("abstracts").select("*");
// 取得したデータをClient Componentに渡す
return <AbstractList abstracts={abstracts} />;
}
// components/AbstractList.tsx(Client Component)
"use client";
export function AbstractList({ abstracts }) {
// ブラウザで動くコード(フィルター、ソートなどのUI操作)
const [filter, setFilter] = useState("");
const filtered = abstracts.filter(a => a.title.includes(filter));
return <>{filtered.map(a => <div key={a.id}>{a.title}</div>)}</>;
}
使う場面: ページの初期データ読み込み。Next.js App Routerなら最初の選択肢。
そのデータは誰が使う?
→ 1つのコンポーネントだけ
→ useState ✅
→ 複数のコンポーネントで共有したい
→ Next.js App Router を使っている?
→ YES → まずServer Componentで取得してpropsで渡す ✅
→ それでも足りない場合(ユーザー操作で変わるデータ)
→ 頻繁に変わる → Zustand ✅
→ めったに変わらない → Context ✅
→ NO(Vite + React)
→ 認証情報 → Context ✅
→ それ以外 → Zustand ✅(アプリが大きいなら)
→ useState + props ✅(アプリが小さいなら)
| プロジェクト | useState | Context | Zustand | Server Components |
|---|---|---|---|---|
| endai-system | ✅ | — | — | ✅(メイン) |
| line-crm | ✅ | — | — | ✅(メイン) |
| visitcare | ✅ | — | ✅ | ✅ |
| nurse-expense-app | ✅ | ✅(Auth) | — | — |
| bento_order_web | ✅ | ✅(Auth) | — | —(Vite) |
VoiceClip-Onlineは、ブラウザベースの音声文字起こしアプリ。Web Speech APIとfaster-whisperのデュアルエンジンで、医療・リハビリ現場での記録を効率化する。
| エンジン | 精度 | 速度 | オフライン |
|---|---|---|---|
| faster-whisper | 高 | 中 | 要サーバー |
| Web Speech API | 中 | 高 | 不可 |
VoiceClip(Pythonローカル版)は医療データを外部送信しない完全オフライン仕様C:/Users/kawag/work/VoiceClip-Online/C:/Users/kawag/work/VoiceClip/bento_order_webは、介護・リハビリ施設向けの弁当注文Webシステム。スタッフがスマホから弁当を注文し、管理者が集計・発注を行う。
firestore.rules(エミュレータテスト後のみ変更可)C:/Users/kawag/work/bento_order_web/dementia-risk-suiteは、Lancet 2024論文に基づく認知症リスク評価スイート。14のリスク因子評価と5領域の認知機能テストを統合したWebアプリ。
| 機能 | 説明 |
|---|---|
| 14リスク因子評価 | Lancet 2024基準のリスク因子チェック |
| 5領域認知機能テスト | 注意・記憶・言語・視空間・実行機能 |
| プロファイル管理 | 最大3人まで管理可能 |
| オフライン対応 | ネットワーク不要で動作 |
C:/Users/kawag/work/dementia-risk-suite/(Cloudflare版: dementia-cf-suite/)docu-scanは、書類スキャナーPWA。カメラで書類を撮影し、Claude Vision AIが丸囲み・チェック文字を抽出、GAS経由でGoogleスプレッドシートに自動転記する。
C:/Users/kawag/work/docu-scan/drawing-cognitive-testは、図形描画による認知機能テストアプリ。患者がタブレット/スマホ上で図形を描画し、その正確性から認知機能を評価する。
firestore.rules(生体情報保護ルール、エミュレータテスト後のみ変更可)C:/Users/kawag/work/drawing-cognitive-test/endai-systemは、学会演題管理プラットフォーム。学術大会の演題登録・査読・プログラム編成・参加登録・決済までを一元管理する最大規模のプロジェクト。
| 機能 | 説明 |
|---|---|
| 学会管理 | 学会の作成・設定・運営 |
| 演題登録 | 著者による演題の投稿・編集 |
| 査読 | 査読者のアサイン・5軸スコアリング |
| メール自動通知 | ステータス変更時の自動送信 |
| 抄録集PDF生成 | 全演題の抄録集を自動生成 |
| 参加登録・決済 | Stripe連携による決済 |
| プログラム編成 | セッション・タイムテーブル管理 |
| 広告管理 | スポンサー広告の配置管理 |
| AI演題チェック | フォーマット検査・カテゴリ提案・自動返信案 |
| コンテンツアクセス分析 | 閲覧データの分析ダッシュボード |
supabase/migrations/(37件、新規追加のみ、既存編集禁止)C:/Users/kawag/work/endai-system/line-crmは、LINE公式アカウントを活用したCRMシステム。L-step/Utageの代替として自社開発。ステップ配信・受診リマインド・分析ダッシュボードを備える。
| 機能 | 説明 |
|---|---|
| ステップ配信 | シナリオベースの自動メッセージ配信 |
| 休み連絡 | 患者からの休み連絡受付 |
| 受診リマインド | 定期受診のリマインド自動送信 |
| 分析ダッシュボード | 配信・反応の分析 |
⚠️ 要確認:デプロイ前にAuth + RLS設定が必要(MEMORY.mdに記載あり)
C:/Users/kawag/work/line-crm/nurse-expense-appは、訪問看護ステーション向けの経費精算PWA。スマホで領収書を撮影し、AIがOCRで読み取り、申請→承認→CSV出力まで完結する。
| 機能 | 説明 |
|---|---|
| 領収書撮影 | スマホカメラで領収書を撮影 |
| AI-OCR | Gemini Visionで金額・日付・店舗名を自動抽出 |
| 経費申請 | カテゴリ選択・コメント付きで申請 |
| 管理者承認 | 承認/差戻しワークフロー |
| CSV出力 | 承認済み経費のCSVエクスポート |
C:/Users/kawag/work/nurse-expense-app/rehab-monitoring-systemは、Flutter製のリハビリテーション活動モニタリングアプリ。患者の歩数・活動量をスマホのヘルスデータから取得し、セラピストと共有する。
| 機能 | 説明 |
|---|---|
| 歩数記録 | ヘルスデータから自動取得 |
| 目標設定 | セラピストが個別に目標歩数を設定 |
| 活動量トレンド | 日次・週次のグラフ表示 |
| セラピスト共有 | 患者データをセラピストが閲覧 |
'wasm-unsafe-eval' + connect-src にgstatic/fonts必須C:/flutter/bin/flutter.bat build web --releasefirebase deploy --only hostingC:/Users/kawag/work/rehab-monitoring-system/visitcareは、訪問リハビリテーションのケア記録・管理システム。セラピストが訪問先で記録を入力し、管理者が一覧・分析を行う。
supabase/migrations/(新規追加のみ、既存編集禁止).env.local(読取・コミット禁止)C:/Users/kawag/work/visitcare/川口脳神経外科リハビリクリニックは、大阪府枚方市・寝屋川市を中心に訪問リハビリテーションを提供する医療法人である。
枚方市BPOは、枚方市から受託するビジネス・プロセス・アウトソーシング事業である。リハビリテーション評価データの分析を中心に、自治体の介護予防施策を支援する。