0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

掃除癖のない私が、達成感で掃除を続けるための Web アプリを作った

0
Last updated at Posted at 2026-10-27

一人暮らしの「掃除が続かない」を助ける Web アプリ「Cleeky」を、設計書から作って公開しました

まず、使ってみてください(登録1分・無料)

👉 https://cleeky.inori0218adt.workers.dev

部屋ごとに掃除を登録すると、「今日やること」だけを並べてくれる、一人暮らし向けの掃除習慣アプリです。掃除癖のない私が、「終えたときの達成感」で、面倒な掃除を楽しくしたくて作りました。

  • 登録は メールアドレス or Google アカウントで1分
  • 最初はタップするだけ:部屋を選ぶ → おすすめの掃除を選ぶ → 完了(約1分)
  • 掃除が終わったら丸を押すだけ。続けた日数も数えてくれます
  • パソコンでもスマホでも同じデータ(アプリのインストールは不要。ホーム画面に追加すれば、アプリのようにも開けます)
  • 基本の機能はずっと無料です(任意の有料プラン「Cleeky Plus」もあります)

📷 (ここに、今日の画面のスクリーンショットを入れる。パソコン幅とスマホ幅の2枚があると伝わりやすいです)

使ってもらえたら、とても嬉しいです。 「ここが分かりにくかった」「こうだったら続けられそう」というご意見は、この記事のコメントでいただけると助かります。初めて触る人が迷わないように、アプリの中に「はじめてガイド」も用意しました(あとで詳しく書きます)。


この記事に書くこと

「作ったものの紹介」だけでなく、どう設計したかも、できるだけ具体的に書きます。

章 内容
1 どんなアプリか(できること・画面)
2 なぜ作ったか・技術構成
3 設計書の進め方(設計 → 見本 → 実装 → 確認)
4 インフラ設計(構成図・環境・秘密の値・CI/CD・決済と通知の流れ)
5 データベース設計(ER 図・RLS・関数・日付の扱い・型の自動生成)
6 アプリの設計(フォルダ構成・層の依存ルール・テスト)
7 作ってハマったこと・工夫

1. どんなアプリか

3ステップで使えます

  1. 部屋を選ぶ:キッチン・浴室・リビングなど、使っている部屋を選びます
  2. 掃除を選ぶ:部屋ごとの「おすすめ」から選ぶだけ。頻度と時間はおすすめのままでOK
  3. 今日やることをこなす:毎日、今日の掃除が並びます。終わったら丸を押すだけ

「掃除の習慣が無い人」が、入力をほとんどせずに始められることを、いちばん大事にしました。

続けるための仕掛け

  • 連続記録:続けた日数と自己ベスト。途切れそうな日は「おやすみ券」で守れます
  • お祝い:今日の掃除を全部終えた日、節目の日に、お祝いが出ます
  • ごほうびシール:掃除の回数・続けた日数に応じて集まります
  • 今日はしない/明日にする:できない日は、責められずに外せます
  • 毎朝のお知らせ(Web Push):今日の掃除を、決めた時刻に知らせます
  • はじめてガイド:初めての人に、4つの基本操作(丸を押す・行を押す・「…」・タスクを足す)を、操作しながら覚えてもらいます

有料プラン「Cleeky Plus」(任意)

月額 300円 / 年額 1,500円(最初の1か月は無料)。使える時間に合わせて今日の掃除を自動で決める「自動の毎日プラン」、部屋ごとの汚れ具合を見る「きれい度メーター」、旅行や体調不良の日に連続記録を守る「おやすみモード」、月ごとの「振り返り」、季節の掃除の提案、があります。無料の機能だけでも、掃除の習慣づくりには十分なように作っています。


2. なぜ作ったか・技術構成

なぜ作ったか

正直に言うと、私はもともと掃除癖がありません。掃除は面倒で、つい後回しにしてしまいます。

そこで思いついたのが、「アプリで達成感を味わえるようにすれば、面倒な作業が楽しくなり、家をきれいにできるのではないか」ということでした。掃除のやり方を教えてくれるアプリではなく、掃除を「やった!」と気持ちよく終えられる仕組みを作りたかったのです。

そのために、Cleeky には、達成感を味わうための仕掛けを入れています。

気持ち 仕掛け
「今日は何をすればいい?」で迷わない 今日やることだけを並べる(頻度から自動で決まる)
終えた手応えがほしい 丸を押すだけで完了。押した瞬間に、丸が弾む
続いていることを感じたい 連続記録(続けた日数と自己ベスト)
頑張りを認めてほしい お祝い(全部終えた日・節目の日)と、ごほうびシール
できない日に挫折したくない 今日はしない/明日にする、おやすみ券

「掃除をする」という面倒な作業の、終わったときの気持ちよさを、小さく何度も味わえるようにして、習慣にしていく。それが、このアプリの出発点です。

Web アプリにした理由

誰でも、アプリのインストールなしで、すぐに試せるように、Web アプリにしました。パソコンの広い画面でも、スマホのブラウザでも使えて、同じデータが見られます。ホーム画面に追加すれば、アプリのようにも開けます。

そのため、ログインとサーバーを前提にしました。

項目 方針
データの置き場所 サーバー(Supabase の PostgreSQL)
ログイン あり(全員。メールアドレス、または Google アカウント)
有料プラン Stripe
公開 Cloudflare(Web サイト)

なぜ全員ログインにしたか:有料プランの「買った」という記録を、ブラウザの中に置くと、利用者に書き換えられてしまいます。「誰が買ったか」を見分けるにはログインが必要で、ログインが無いと、ブラウザを変えたり履歴を消したりしたときに、データも消えてしまいます。保存の仕組みを1つ(サーバー)にして、パソコンとスマホで同じデータを見られるようにしました。

技術構成

区分 使っているもの
画面 React 19 / TypeScript / Vite / React Router / styled-components
ログイン・データ Supabase(Auth・PostgreSQL・Edge Functions。東京リージョン)
決済 Stripe(Checkout・カスタマーポータル・Webhook)
公開 Cloudflare Workers(静的な資産)。GitHub の main に push すると自動で公開
自動の確認 GitHub Actions(型チェック・lint・テスト・ビルド)
テスト Vitest(計算・ユースケース)/ PGlite(データベースの決まり・RLS)

3. 設計書の進め方

このアプリは、**「作る前に、設計書に書く」**を決まりにして進めました。個人開発だと、頭の中だけで進めて、あとから「なぜこうしたんだっけ」となりがちなので、決めたことを必ず文書に残しています。

設計書の置き場所

plan/
  PROJECT_PLAN.md     目的・画面・仕様・決定事項・進捗(これが「正」)
docs/
  architecture.md     フォルダ構成と、層の依存ルール
  database.md         表・RLS・データベースの関数
  infrastructure.md   構成図・ER 図・シーケンス図(Mermaid)
  testing.md          テストの方針と書き方
  workflow.md         作業の進め方(ブランチ・確認・コミット)
  deploy.md           公開の手順(Cloudflare・Supabase)
  stripe.md           決済の準備
  notifications.md    通知の準備
design/mockups/       画面の見本(HTML)

進め方(1つの機能の例)

  1. PROJECT_PLAN.md に仕様を書く。決まっていない点は、推測で決めず、選択肢を出して選ぶ
  2. 決まったことは「決定事項」の表に書く(あとから理由を辿れるように)
  3. **画面の見本(HTML)**を先に作る(Figma の利用上限に当たったので、HTML で。パソコン幅とスマホ幅の両方)
  4. domain(計算)→ 保存 → 画面の順に、小さく作る。各段階でテストを書く
  5. ブラウザ(パソコン幅とスマホ幅)で確かめて、型チェック・lint・テストを通してからコミット

たとえば、前述の「はじめてガイド」は、こう進めました。

  • 「初めて使う人が戸惑わないか」を調べて、気づいた課題を書き出す
  • 「形」の選択肢を4つ(ツアー/チェックリストのカード/その場のヒント/使い方ページ)並べ、良い点・気になる点を比べて、カード+使い方ページに決める
  • 「あとで設定する」を選んだ人(タスクが0件)は、最初のタスクを登録したときに始める、と仕様に書く
  • 見本(HTML)→ domain とテスト → 保存 → 画面、の順に作る

設計書に「選んだ理由」まで残してあるので、半年後に自分が読み返しても迷いません。


4. インフラ設計

4.1 システム構成図

ポイントは3つです。

  • 画面のファイルは Cloudflare から配るだけ。データの読み書きは、ブラウザから Supabase へ直接行います(自前のサーバーは持ちません)
  • ブラウザに入る鍵は publishable key だけ。守りは データベースの RLS(他の人の行は見えない・触れない)で行います
  • 決済・通知・アカウント削除は、ブラウザに置けない鍵(Stripe の秘密鍵・Supabase の secret key)が要るので、Edge Functions で行います

4.2 環境

開発 本番
画面 自分のパソコン(npm run dev) Cloudflare
Supabase 同じプロジェクト(東京)を共用 同じ
Stripe テストモードで開発 本番モード
データベースのテスト PGlite(パソコンの中だけで動く PostgreSQL) —

個人開発で規模が小さいので、Supabase のプロジェクトは開発と本番で共用しています。そのため、マイグレーションを db push した時点で、公開中のサイトにも効きます。「表・関数を足すときは、画面より先に db push」「消す・名前を変えるときは、公開中の画面が使っていないことを確かめる」を、手順書(deploy.md)に書いています。利用者が増えたら、本番用に分ける予定です。

4.3 秘密の値の置き場所

値 置き場所 画面のコードに入れてよいか
Supabase の URL・publishable key .env(VITE_ で始まる名前) よい(誰でも見られる前提)
Supabase の secret key Edge Functions の設定だけ だめ
Stripe の秘密鍵・Webhook の署名の鍵 Edge Functions の設定だけ だめ
通知の秘密鍵(VAPID) Edge Functions の設定だけ だめ

.env は Git に入れません(見本は .env.example)。秘密の値は、コード・文書・チャット(AI との会話も含む)に書かない決まりにして、各サービスの管理画面にだけ入れています。

4.4 CI/CD

  • CI(GitHub Actions):npm ci → npm run check(型チェック・lint・テスト)→ npm run build。Supabase には触らない(画面のビルドに必要な環境変数は、ダミーの値)
  • CD(Cloudflare Workers Builds):main への push で npm run build → npx wrangler deploy。ビルドが失敗したら、前の公開がそのまま残るので、サイトは壊れません
  • ブランチを push すると、本番とは別の「プレビュー」に出ます(wrangler.jsonc に "previews": {} が必要でした。7章で書きます)

4.5 処理の流れ(シーケンス図)

今日の画面を開いて、丸を押す。予定日の計算は**ブラウザ(domain)**で行い、データベースには「計算した結果」を渡します。

Cleeky Plus を買う。Stripe の Webhook を、署名を確かめてから受け取ります。

毎朝のお知らせ。データベースの時刻で動く仕組み(pg_cron)から、1時間ごとに Edge Function を呼びます。


5. データベース設計

5.1 ER 図

表 役割
rooms 部屋(論理削除。同じ人の、削除していない部屋どうしで名前の重複を禁止)
tasks 掃除タスク(頻度は daily / weekdays / everyNDays)
task_occurrences 予定と実施の記録(同じタスク・同じ日は1件だけ)
app_settings 設定(user_id + key の「名前と値」。進み具合などの JSON もここ)
push_subscriptions お知らせを受け取る端末(ブラウザごとに1行)
stripe_customers / subscriptions 決済(画面からは読むだけ。書くのは Edge Function だけ)

アカウントを消すと(on delete cascade)、その人の行はすべて消えます。

5.2 守り方(RLS)の設計

方針は4つです。

  1. すべての表で RLS を有効にする(RLS の無い表は作らない)
  2. まず全部の権限を取り上げ、必要な操作だけを authenticated に渡す。anon には何も渡さない
  3. 部屋・タスクは消さない(論理削除)ので、delete の権限を渡さない。決済の表は select だけ
  4. RLS と外部キーで二重に守る
-- まず全部の権限を取り上げ、ログインしている人だけに、必要なものを渡す
revoke all on table public.rooms, public.tasks, public.task_occurrences,
  public.app_settings, public.stripe_customers, public.subscriptions
  from anon, authenticated;

grant select, insert, update on table public.rooms to authenticated;   -- delete は渡さない
grant select on table public.subscriptions to authenticated;           -- Plus は読むだけ

alter table public.rooms enable row level security;

create policy "rooms: 自分の部屋を読む" on public.rooms
  for select to authenticated using ((select auth.uid()) = user_id);
create policy "rooms: 自分の部屋を足す" on public.rooms
  for insert to authenticated with check ((select auth.uid()) = user_id);

auth.uid() は (select auth.uid()) で包みます。行ごとに呼ばれず、1回だけ求められるので、速くなります(Supabase の公式ドキュメントの書き方)。

5.3 「他の人のデータを指せない」複合外部キー

RLS だけに頼らず、テーブルの定義でも、他の人のデータを指せないようにしています。

create table public.rooms (
  id      bigint generated always as identity primary key,
  user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
  name    text not null check (char_length(name) between 1 and 30),
  -- タスクから「同じ持ち主の部屋」だけを参照するための組み合わせ
  unique (id, user_id)
);

create table public.tasks (
  -- ...省略...
  room_id bigint not null,
  -- 部屋は「同じ持ち主の部屋」だけ(他の人の部屋の id を指定しても登録できない)
  foreign key (room_id, user_id) references public.rooms (id, user_id)
);

create table public.task_occurrences (
  -- ...省略...
  unique (task_id, scheduled_date),  -- パソコンとスマホで同時に予定を作っても二重にならない
  check ((status = 'completed') = (performed_date is not null)),  -- 完了なら実施日あり
  foreign key (task_id, user_id) references public.tasks (id, user_id)
);
  • user_id は default auth.uid()。画面から送らなくても、ログインしている人になります
  • 「同じタスク・同じ日は1件」を unique で守ると、複数の端末から同時に予定を作っても、二重になりません
  • 「完了なら実施日あり、未完了なら実施日なし」を check で守り、あり得ない状態を作れなくしています

5.4 複数の書き込みは「データベースの関数」で1回にまとめる

画面から何度もやり取りして、途中で失敗すると、中途半端な状態が残ります。そこで、複数の書き込みが必要な操作は、**PostgreSQL の関数(RPC)**にまとめました。関数の中は1つのトランザクションなので、全部成功するか、全部取り消されるかです。

関数 内容
complete_onboarding 選んだ部屋・タスクの登録、完了の印、今日までの予定の作成
generate_occurrences 「前回作った日から今日まで」の予定をまとめて保存
add_task / edit_task タスクの登録・編集と、今日の予定の判定し直し
stop_task / delete_room 停止・削除と、今日以降の未完了の予定の削除
record_done_now / postpone_occurrence 「今やった」「明日にする」を1回で
list_history 履歴(予定・タスク・部屋をつないで読む)

関数は、引数で user_id を受け取らず、auth.uid() を使います(なりすましを防ぐため)。

5.5 日付の扱い

  • 「今日」はブラウザ(利用者の端末)の日付で決め、'YYYY-MM-DD' の文字でデータベースに渡します(サーバーの時刻に依存しない)
  • 予定日の計算は TypeScript(domain)で行い、データベースには「計算した結果」を渡します。計算を TypeScript と SQL の2か所に書かないためです

5.6 supabase gen types で型を自動生成

表の型は、手で書かずに生成します。

npx supabase gen types typescript --linked --schema public > src/shared/infrastructure/supabase/database.types.ts
export const supabase = createClient<Database>(url, publishableKey);

生成した型には弱点があります。補い方も決めています。

  • 文字の列(status など)は string になる → 値を確かめて、決まった値の型にする関数(想定外の値なら例外)を通す
  • データベースの関数の引数・戻り値は null を表せない → 必要な所だけ、理由のコメントを付けて型を補う

6. アプリの設計

6.1 フォルダ構成(機能単位 × 層 × Atomic Design)

src/
  app/                  ルーティングと、アプリの組み立て
  features/             機能ごとのまとまり(today・rooms・tasks・history・guide ...)
    <機能名>/
      ui/               画面と、その機能だけの部品・フック
      application/      ユースケース:1つの操作の手順
      domain/           型・入力ルール・計算(React にも Supabase にも依存しない純粋な TypeScript)
      infrastructure/   データベースの読み書き
  shared/               2つ以上の機能で使うもの(ui の atoms/molecules/organisms など)
supabase/
  migrations/           表の作成・変更の SQL(公開済みのものは書き換えず、足していく)
  functions/            Edge Functions

6.2 層の依存ルール

  • Supabase は infrastructure だけで使う(画面から直接データベースを触らない)
  • domain は React もライブラリも使わない。だから単体テストが書きやすい
  • 決まりは ESLint の no-restricted-imports で自動的に確かめます。破ると lint が落ちます

6.3 domain の例:「今日は予定日か」

頻度は3種類(毎日/曜日指定/N日ごと)。'YYYY-MM-DD' の文字は、そのまま大小を比べられます。

export function isDueOn(rule: ScheduleRule, date: string): boolean {
  if (date < rule.scheduleAnchorDate) return false; // 起点日より前は予定日にしない
  const { frequency } = rule;
  switch (frequency.type) {
    case 'daily':
      return true;
    case 'weekdays':
      return includesWeekday(frequency.days, weekdayOf(date));
    case 'everyNDays':
      // 起点日を1回目として、N日おきに予定日になる
      return daysBetween(rule.scheduleAnchorDate, date) % frequency.days === 0;
  }
}

日付の計算は UTC で行います(サマータイムのある地域で、1日が23時間・25時間になって、日数がずれるのを避けるため)。

6.4 テストの方針

対象 テスト
domain(計算・入力ルール) 必ず書く。境目(0件・上限ちょうど・上限+1)、月末・年末・うるう年
application(ユースケース) 必ず書く。infrastructure を代役に差し替え、手順と結果を確かめる
データベース(表・RLS・関数) 必ず書く。PGlite に migration の SQL を流して、他の人のデータが見えない・書き換えられないことを確かめる
ui(画面) 今は書かない。ブラウザ(パソコン幅とスマホ幅)で確かめる

全体で 約600件(2026年10月27日時点で 594件)。RLS のテストは「守りを外すと失敗する」ことを一度確かめています。そうしないと、テストが何も守っていなくても、気づけないからです。


7. 作ってハマったこと・工夫

7.1 Google で検索しても出てこない(SPA の SEO)

公開してから、「Cleeky」で検索しても出てきませんでした。調べると、index.html に本文が無く、JavaScript が動いてから画面が描かれる作りでした。Google は JavaScript も読めますが、読む順番が後回しになり、登録が遅れやすいです。

そこで、ビルドの最後に、トップの画面を HTML の文字に書き出して dist/index.html に入れるようにしました(プリレンダー)。

  • 新しいパッケージは入れず、react-dom/server と styled-components の ServerStyleSheet で書き出す
  • vite build のあとに、Node のスクリプトが、画面を「サーバー側の形」に Vite で一度作り直して呼ぶ
  • 書き出したトップは #root の隣(#prerender)に置き、アプリが最初の中身を描いたら取り除く(#root を書き換えると、「文章 → 白 → 文章」とちらつくため)
  • /login など / 以外では、アプリを読む前に、小さなインラインスクリプトで取り除く
  • canonical は足さない。どのアドレスにも同じ index.html が返るので、/login なども「トップと同じ」と読まれる心配があるため

7.2 wrangler preview が「previews ブロックが無い」で失敗

プルリクエストのブランチをプレビュー用に公開するとき、Cloudflare のビルドが wrangler preview で失敗しました。

Your Wrangler configuration is missing a `previews` block to run this command.

wrangler.jsonc に、空の "previews": {} を足すと直りました(本番の wrangler deploy には影響しません)。

7.3 styled.div is not a function(SSR で styled-components が読めない)

プリレンダーのスクリプトで、styled-components を Node がそのまま読むと、**styled.div が使えない形(CommonJS)**で入ってきて落ちました。Vite の SSR ビルドの ssr.noExternal: ['styled-components'] で、一緒にまとめて作ると動きます。

7.4 保存を待たずに画面を進める(楽観的な更新)と、失敗の扱い

丸を押したら、保存を待たずに「完了」の見た目にして、保存に失敗したら元に戻して知らせる、としています。保存・削除などの「失敗したら押せる状態に戻す」流れが、画面ごとに14か所に散らばっていたので、1つのフック(useAction)にまとめました。例外を握りつぶさないよう、原因は console.error に残します。

7.5 as で型を上書きしない

Supabase の戻り値を as で型に合わせていた箇所が24か所ありました。型の自動生成に切り替えて、**文字の列は値を確かめる関数(想定外なら例外)**を通すようにしたので、「データがおかしいときに黙って壊れる」ことが減りました。

7.6 初めて使う人のための「はじめてガイド」

「ログイン後に、初めて使う人が戸惑わないか」を調べると、アプリの中の案内が何もありませんでした。説明を読ませるより、実際に操作して覚えてもらうように、今日の画面の上に4ステップのカードを出し、操作したら自動でチェックが付く形にしました。いま押してほしい所には、静かな輪を付けます(強制のツアーにはしません。「視差効果を減らす」の設定なら、動きは止めます)。

ガイドの状態は、新しい表を作らず、既存の app_settings(名前と値)に JSON で保存しました。表が増えないので、db push も要りません。

7.7 そのほか

  • OneDrive の中のフォルダだと、開発用サーバーがファイルの変更に気づかないことがあり、画面が古いままに見えて、迷いました(サーバーを起動し直すと直ります)
  • 画面幅の判定:「パソコン幅(900px 以上)か」は、見た目の違いだけなら CSS の media で、入力欄の並べ方そのものが違う画面だけ、フックで切り替えました

まとめ・これから

  • 設計書を先に書くと、決めた理由が残り、迷いが減りました。選択肢を出して選ぶ形にすると、「なんとなく」で決めずに済みます
  • RLS・複合外部キー・check 制約・データベースの関数の組み合わせで、データベースの側でも、あり得ない状態を作れないようにしました
  • 層の依存ルールを ESLint で自動的に確かめるようにすると、コードが育っても、構造が崩れにくいです

掃除癖のない自分のために作ったアプリが、同じように「掃除が続かない」人にも役立てば、とても嬉しいです。これからは、使ってくれた人の声をもとに、初回の流れやガイドを直していきたいです。

👉 https://cleeky.inori0218adt.workers.dev

登録は1分、基本の機能はずっと無料です。「今日の掃除、全部おわり!」のお祝いが出る日を、ぜひ作ってみてください。気づいたことは、コメントでお知らせいただけると、とても助かります。


この記事について

この記事の設計書とコードは、生成 AI(Claude Code)と一緒に作りました。設計の判断・選択肢の決定は、私が行い、内容は私が読んで確認しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?