MyMemoDays

https://github.com/uukino/mymemo/

GitHub

React

JavaScript

Node.js

VSCode

みんなでメモ共有

UUKiNO

わしじゃよ

chebuo

mori_yama

推しアイデア

サイトのどこでもメモをはっ付けられる

作った背景

記事が古くて使えない!そんな時はみんなに共有してあげよう

推し技術

みんなにメモを共有 みんなのメモを検索

プロジェクト詳細

たくさん!かくさん!Mymemoとは

技術記事を読んで実践したとき、その記事が使えなかったことはありませか? Mymemoはそんな時にメモを共有してみんなに知らせるだけでなく、他のユーザーのメモを参照することにより、早期に参考にしていた技術記事に見切りをつけることができます 知識のアウトプットにもなることを目指して開発したプロダクトです。

アーキテクチャ図

image

技術スタック

レイヤー技術
フロントエンドReact 19.2.0 + Vite 7.3.1
DnD@dnd-kit/core 6.3.1, @dnd-kit/sortable 10.0.0
バックエンドExpress 5.2.1 (Node.js, ESM)
DBSupabase (PostgreSQL)
デプロイRender.com
配布形式Chrome Extension (Manifest V3)

完成度 (B)

実装済み機能:

  • URL紐づきメモの作成・編集・削除
  • ページ上への付箋表示(ドラッグ移動・リサイズ)
  • お絵描きキャンバスメモ
  • ドラッグ&ドロップによる並び替え
  • タグ管理
  • ローカル全文検索(テキスト・タグ)
  • リモートへのメモ共有
  • リモートメモ一覧・キーワード検索
  • マイページ(ユーザー登録・ログイン・メモ統計)
  • メモのカラー変更

機能説明

・メモ保存機能 書いたメモはGoogle Cloud Storageで保存されているため、高速で読み込み可能

メモの保存はバックエンド(Express)経由で Supabase に書き込みます。画像データは GCS にアップロードし、そのURLをDBに格納します。

// Express (backend) app.post('/api/memos', async (req, res) => { const { userId, url, content, tags, color } = req.body; const { data, error } = await supabase .from('memos') .insert({ user_id: userId, page_url: url, content, tags, color }); if (error) return res.status(500).json({ error }); res.json(data); });

・メモ貼付機能 書いたメモをサイトのどこにでも張ることが可能。貼付したメモはサイズの変更や削除、非表示、内容の編集を直接できます。

Chrome の content script から React コンポーネントをページに注入し、@dnd-kit でドラッグ移動を実装しています。

// content_script.tsx(ページへの注入) import { useDraggable } from '@dnd-kit/core'; function StickyNote({ memo }: { memo: Memo }) { const { attributes, listeners, setNodeRef, transform } = useDraggable({ id: memo.id, }); const style = { transform: `translate(${transform?.x ?? memo.x}px, ${transform?.y ?? memo.y}px)`, backgroundColor: memo.color, }; return ( <div ref={setNodeRef} style={style} {...listeners} {...attributes} className="sticky-note"> <p>{memo.content}</p> </div> ); }
// manifest.json (Manifest V3) { "manifest_version": 3, "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content_script.js"] }] }

・メモ共有機能 書いたメモをほかの人が見られるように共有することができます。

is_public フラグを true に更新するだけで公開状態に切り替わります。

// 共有ボタン押下時 async function shareMemo(memoId) { const { error } = await supabase .from('memos') .update({ is_public: true }) .eq('id', memoId); }

・タグ&メモ検索機能 メモにはタグを追加することが可能。検索機能ではタグや本文、またはその両方からメモの検索が可能。ほかの人が書いたメモも検索可能であり、メモからサイトへアクセスすることができます。

ローカル検索はメモリ上でフィルタリング、リモート検索は Supabase の ilike と配列演算子を使用します。

// ローカル全文検索 function searchLocal(memos: Memo[], query: string, tags: string[]) { return memos.filter(m => { const matchText = m.content.toLowerCase().includes(query.toLowerCase()); const matchTag = tags.length === 0 || tags.every(t => m.tags.includes(t)); return matchText && matchTag; }); } // リモート検索(公開メモ) async function searchRemote(query: string, tag: string) { const { data } = await supabase .from('memos') .select('*') .eq('is_public', true) .ilike('content', `%${query}%`) .contains('tags', tag ? [tag] : []); return data; }

・いいね機能 メモにはいいねをすることができ、有益な情報にいいねを付けて後から見返すことが可能です。

いいねはユニーク制約付きで管理し、重複いいねを防止しています。

// いいね追加 async function likeMemo(memoId: string, userId: string) { await supabase .from('likes') .upsert({ memo_id: memoId, user_id: userId }, { onConflict: 'memo_id,user_id' }); } // いいね解除 async function unlikeMemo(memoId: string, userId: string) { await supabase .from('likes') .delete() .match({ memo_id: memoId, user_id: userId }); }

お絵かき機能ではキャンバスを設置して好きなものを書くことができます

Canvas API でマウスの軌跡を描画し、toDataURL() で画像として保存します。

function DrawingCanvas() { const canvasRef = useRef<HTMLCanvasElement>(null); const drawing = useRef(false); const startDraw = (e: React.MouseEvent) => { drawing.current = true; const ctx = canvasRef.current!.getContext('2d')!; ctx.beginPath(); ctx.moveTo(e.nativeEvent.offsetX, e.nativeEvent.offsetY); }; const draw = (e: React.MouseEvent) => { if (!drawing.current) return; const ctx = canvasRef.current!.getContext('2d')!; ctx.lineTo(e.nativeEvent.offsetX, e.nativeEvent.offsetY); ctx.stroke(); }; const save = () => { const dataUrl = canvasRef.current!.toDataURL('image/png'); // dataUrl を GCS にアップロードして保存 }; return ( <canvas ref={canvasRef} onMouseDown={startDraw} onMouseMove={draw} onMouseUp={() => (drawing.current = false)} /> ); }

・マイページ機能 このページでは自身が保存したメモとそのURLを閲覧することができます

ログインユーザーのメモ一覧と統計(総数・公開数・いいね数)を取得して表示します。

// マイページデータ取得 async function fetchMyPage(userId: string) { const { data: memos } = await supabase .from('memos') .select('*, likes(count)') .eq('user_id', userId) .order('created_at', { ascending: false }); const stats = { total: memos?.length ?? 0, public: memos?.filter(m => m.is_public).length ?? 0, totalLikes: memos?.reduce((sum, m) => sum + (m.likes[0]?.count ?? 0), 0) ?? 0, }; return { memos, stats }; }

推しポイント

chromeの拡張機能で使えるためブラウザのタブを増やすことなくサイトの評価を見ることができる!

マイページにて過去自分が保存したメモと紐づけされたURLを確認できるため、開発中にメモを保存しておくことで後から有用だったサイトを共有しようとしたときにマイページを見るだけで当時参考にしていたサイトが分かる。

つまりchromeのブックマーク機能+メモでサイトを探すのが楽だしあとから見直したときに楽ができる

大変だったところ

事前開発をしている段階でメインで開発を進めている人が風邪で4日ダウンしてしまった

バックエンドデプロイする用のRenderが一生動かなくて二日ぐらいバックの開発が止まっていた

(原因は参照するgithubのプロダクトが全く同じ名前のものが二つあり、参照を間違っていた)

そのため毎日22時に進捗報告を行っているが進捗が止まっている情報が出るだけの日があってつらかった

メイン画面

image

オンライン画面

image

検索画面

image

マイページ

image

お絵かき

image

UUKiNO

@uukino