看板（チケット）を多層で管理するToDo（Web）アプリ「無限看板」を作りました

![](https://storage.googleapis.com/zenn-user-upload/a8d3bb37025a-20241004.jpg)
![](https://storage.googleapis.com/zenn-user-upload/3ccf812fcf94-20241008.png)

# 無限看板の概要
何はともあれVercelにデプロイしてます。
https://mugen-kanban.nexsjp.com/

「無限看板」はざっくりいうとToDoアプリです。ただし、以下の点で業務管理、特にシステム開発に特化したToDoアプリになっていると思います。

- 全体の業務数に対する完了数や予測作業時間に対する実際の作業時間がわかる（逆に常に増減する日常用途には向かない）
- 作業者の作業（ToDo）に対する履歴（開始・終了時刻、作業時間）が記録される
- 作業に対して作業者とその確認者を設定できる
- 作業の中に作業を作る形で詳細化可能で、その階層数に上限が無い

# アーキテクチャ
- 形式：WEBアプリケーション
- 対象：基本的にPC、特にWindows？（MAC持ってないので検証不可）
- フレームワーク：Nuxt3（Vue3）
- UI：Vuetify
- ユーザ管理等：Firebase Authentication
- DB：MySQL（そろそろ会社潰れるかも）
- デプロイ（WEBサーバー）：Vercel
- デプロイ（DB）：GCP SQL（MySQL）
- 制作補助（DB設計・自動生成）：N-Dev（https://n-dev.nexsjp.com/)
- ほぼ使ってない：Vitest、Histoire

# コンセプト・作成理由等
さて、私はずっと個人開発をしてきたので、ずっとほぼアジャイルと言えばアジャイル、しかし全然アジャイルなんか知らないという状態で、チケットとか看板とか言われてもよくわからないのですが、
この度、他会社様との共同での開発や、医療系のシステム開発に関わることになり、業務及び品質の管理をそれなりにしなければならない段になりました。その中で、テストである「Vitest」やUIカタログツール「Histoire」（Storybookを経由後）を試しました。で、複数人での業務管理はどうしようかなあ、と考え、そういえば過去に独自のToDoアプリを計画していたっけな、ということでそれを実現してみました。
コンセプトは、アジャイルとかスクラムとか全然知らないけどそういうので使えそうな感じ、です。
多分類似アプリにJetBrainsの「YouTrack」やAtlassianの「JIRA」とかがあると思いますが、使ったことないのでよく知りません。この記事を書き始めてから少しだけ「JIRA」触ったのですが、おもっきしスクラム用ですね。ちんぷんかんぷん。こういうのって機能多すぎて教えてもらわないと使えなくないすか。
取り合えず、無限に詳細化できるという点は差別化できてるんじゃないかなあと思います。需要は知りません。後はシンプル、イズべスト。

参考：
YourTrack関連
・https://zenn.dev/howtelevision/articles/72cc834438e959
・https://ohina.work/post/youtrail/
JIRA関連
・https://qiita.com/iguchikoma/items/e712a29ad0750c7719ac
・https://n-v-l.co/blog/jira-softwawe-how-to-use

因みに、作成期間は10日程度です。当初はたかがToDoリスト、N-Devもあるし3日で作れるやろ、と思ったのですが、色々こだわったのもありますが、兎に角D&Dでタスクを移動させるのがめちゃめちゃに難しく（後の「D&D機能実装」の項で詳しく書きます）、想定よりもかなり時間がかかってしまいました。これのみに時間を割いたわけではありませんが、10日と言えば2週間、つまり半月、会社としては20万円分くらいは働いてくれないと、、、世知辛い。300円の記事を1000人の方が買ってくだされば解決ですね()

# 無限看板
無限看板の細かい機能や使い方を書いていきます。

## 機能
以下のような機能があります。

- プロジェクト作成
- メンバー管理（権限）
- タスク（看板）作成・更新
- 進捗表示
- D&D操作
- 子（詳細）要素作成
- 担当者割り当て
- 作業時間計測
- 作業履歴表示

## 今後実装したい機能

- 作業者が自ら作業に応募できるようにする
- 操作履歴
- 時間計測は単一化して保存、composables化
- メール等通知
- タスク・プロジェクト複製・プロジェクトリセット
- 全体並び替え
- 作業時間等再計算（通信量の削減）
- 権限でTodoメンバー追加対象を制限、権限の判定を２重化
- ユーザ更新
- ユーザ退会
- スマホ画面対応（最低限は対応している、ハズ）

## 使い方
使い方をできるだけ画像を入れて説明します。
まずはページにアクセス。
https://mugen-kanban.nexsjp.com/
![](https://storage.googleapis.com/zenn-user-upload/2b9fd8361186-20241004.png)

### ログイン
ログイン方法はメールログインとGoogleログインを用意してます。開発者らしくGitHubログインも用意したかった、というかさして難しくないのですが、想定より長引いたので実装してません。要望があれば。
メールログインの方は認証メールがFirebaseから届きますので認証してからご利用ください。

![](https://storage.googleapis.com/zenn-user-upload/80af1c2afecd-20241004.png)

### プロジェクト作成
ログインしたらプロジェクト一覧ページに遷移しますので、最初のプロジェクトを作成してください。
作成者には自動的に「オーナー」権限が割り当てられます。
作成したら作成されたプロジェクトを選択し、プロジェクト内に進んで下さい。

![](https://storage.googleapis.com/zenn-user-upload/9b28dafcdf94-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/71de10bf2c55-20241004.png)

## メンバー追加
大きな画面では左側にタブが出ます。タブは「メンバー」「TODO」「作業履歴」の3つが表示されると思います。メンバーを追加したい場合はメンバータブを選択します。

メンバーの追加は右上「＋」マークのボタンから、ユーザ名、メールアドレス、権限を選択するだけです。ユーザが存在しない場合は自動的に追加され、指定したメールアドレスに自動生成されたパスワードの情報が送信されます。ユーザはログイン後パスワードの変更...をしてほしいのですが**パスワード変更画面をうっかりつけ忘れました。**必要でしたらコメント欄で要望してください。既に存在するユーザの場合は作成はされず追加のみになります。
また、指定したユーザ名は共通のユーザ名とプロジェクト内でのユーザ名に設定され、プロジェクト内でのユーザ名は個別に変更できます。

![](https://storage.googleapis.com/zenn-user-upload/18c83a7818e8-20241004.png)

権限は以下の通りです。

- オーナー：作成者、一名のみ
- マネージャー：基本的に全ての操作が可能、ただしオーナーの変更等はできない
- 作業者：割り当てられたタスクを実行する
- 閲覧者：データの閲覧のみ、クライアントへの共有など

## タスク（ToDo）管理
TODOタブではタスクの管理ができます。

### タスク作成
まずは「Top」に最初のタスクを追加します。追加は右上の「＋」ボタンか、タスク欄の「追加」ボタンから行います。2つのボタンの機能は同じです。
作成するタスクには以下の項目があります。

- タスク名
- 重要度（優先度）
- 予測作業時間
- 作業期限（締め切り）
- 詳細
- タグ

タスク名のみが必須です。まず「TEST」というタスクを作成してみました。
タスクを作成するとタスク欄の上部に作業時間と作業件数が表示されます。

![](https://storage.googleapis.com/zenn-user-upload/655b565fa95c-20241004.png)

### タスク更新
タスク右上のメニューボタンからタスクの編集や削除ができます。
![](https://storage.googleapis.com/zenn-user-upload/9d6a3f2f6366-20241004.png)

### 子（詳細）タスク追加
タスク右上のメニューボタン、又はタスクを選択してタスク内に入り、その画面でタスクを追加することで、子タスクを作成できます。
子タスクが作成されると、親タスクの完了状態や作業時間は子タスクを基にして再計算されます。例えば、子タスクが全て「完了」していれば親タスクの状態も「完了」になります。ただし、作業期限はタスクに設定したものが適用されます。
また、子タスクの下に更に孫タスクを追加するように、制限なく詳細化できます。

![](https://storage.googleapis.com/zenn-user-upload/6cf447aee7a8-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/249acd524976-20241004.png)

### タスク移動・削除
プロジェクトのマネージャー以上の権限を持つユーザは、タスクの移動や削除ができます。
タスクの移動は同階層の親タスクの入れ替えだけでなく、子タスクの並び替え、親から子、子から親への移動、孫から親への移動、またゴミ箱に移動することで削除が可能です。

![](https://storage.googleapis.com/zenn-user-upload/8750ed2a7873-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/4e19968b410f-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/a967337bc299-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/b2788eafd40a-20241004.png)

### メンバー割り当て
各タスクには作業者と確認者（マネージャー）を割り当てることができます。この権限はプロジェクトの権限とは異なりますので注意してください。また、プロジェクトの閲覧者はタスクの実行メンバーに割り当てることはできません。
なお、タスクを実行できるユーザは以下の通りです。

- プロジェクトのオーナー、マネージャー
- タスクに割り当てられた作業者、マネージャー

### タスク実行
タスクが実行できるユーザには、各タスクにタイマーが表示されます。タイマーを開始すると時間の計測が開始され、終了時に停止されることで、自動的に作業終了時刻や作業時間が記録されます。
記録された履歴は「作業履歴」から確認できます。

![](https://storage.googleapis.com/zenn-user-upload/83e33cd3bf28-20241004.png)

### 作業履歴
「作業履歴」タブから作業履歴を確認できます。
履歴は作業者、対象タスク、作業時間、作業終了時刻が表示されます。作業は秒単位で記録されます。履歴を削除することで対象の作業時間は全体の作業時間から差し引かれます。

![](https://storage.googleapis.com/zenn-user-upload/7398842c146c-20241004.png)

### 完了報告
作業者は作業が完了すると作業の状態を「完了」にすることができます。マネージャーは作業の「完了」を受けて確認し、「確認済」に変更します。確認済となったタスクは、上部の「作業件数」に追加されます。

![](https://storage.googleapis.com/zenn-user-upload/da0df975b7aa-20241004.png)
![](https://storage.googleapis.com/zenn-user-upload/823a6f038f8e-20241004.png)

# 開発
開発に関すること

## 設計・構成について
いつもならタスク群を配列にしてJSONで管理するところなのですが、その方が圧倒的に楽なのですが、通知やタスク数を数えるためにはタスクはバラバラに保存したほうがよかろう、とタスクテーブルにタスクを保存し、親タスクのIDを持つことで親子関係を構築しています。しかし、この設計のせいで並び替えや編集をするたびにDBから再取得をしており、今動かしている感じ特に気にならないものの、少し動作速度等心配ではあります。工夫のしようはあると思うのですが、D&Dの処理が複雑になっている為に少し大変そうです。また、タスクの移動機能から、どうしても毎回合計作業時間の再計算などは発生します。
この再計算は元々はVueのComputedで実装して自動でさせていたのですが、リアクティブな状態とそうでない状態が混ざって解消が難しい複雑なバグが発生してしまったので、自動は諦めました。そのためにも毎回取得から計算させた方が正直楽です...
因みに、APIで親子関係を構築し、クライアントでクラスに入れる過程で、コンストラクタで各合計値を計算させています。

## N-Dev
https://n-dev.nexsjp.com/
弊社制のWebアプリ作成支援Webアプリです。DBの設計を支援し、設計したDBを基にSQLの発行、コードの自動生成をしてくれる便利な奴です。
ただ、如何せん対象環境が「Nuxt3 × MySQL × Vuetify × Firebase」という単体はともかく組み合わせるとだいぶマイナーな構成のプロジェクトを生成するので、あまり使われてないです。

いくつか画面を載せてみます。

DB設計
![](https://storage.googleapis.com/zenn-user-upload/04bb2ab20178-20241004.png)

todoテーブル
![](https://storage.googleapis.com/zenn-user-upload/8f59398598ae-20241004.png)

自動生成画面
![](https://storage.googleapis.com/zenn-user-upload/5bfde1da2daa-20241004.png)

全体画面
![](https://storage.googleapis.com/zenn-user-upload/fb8dec3bbcf8-20241004.png)

## D&D（DnD）機能実装
さて、わりと今回の本題です。今回もっとも苦労したところになります。
先ずもって、D&Dと一口に言っても、私は少なくとも3つの種類があると思います。3つとは「ファイル操作」「ソート」「自由移動」です。今回は「ソート」の用途になります。生のDnD APIは恐らく「ファイル操作」の為に作成されてると思います。つまり、ファイルをWEBページにDnDして読み込む、といった操作です。ですから対象の要素（エレメント）をドラッグして移動させるような用途には向いていない仕様がだいぶあります。
D&Dのライブラリは少なくありませんが、種類を絞れば実はそんなに多くありません。Vueのようなフレームワークとの相性を考えればさらに少なくなります。

### 要件
まず、今回DnDで実現したかった要件を以下に書き出してみようと思います。

1. 要素のリアルタイム入れ替え：タスクをドラッグして入れ替える際、ドロップした後に見た目が変更されるのではなく、リアルタイムに入れ替わっているような見た目で表示したい
2. 要素の親子間移動：親要素どうし、子要素どうしだけではなく、親と子の相互の並び替えを実現したい
3. ゴミ箱機能（孫親移動）：タスクをドラッグしてゴミ箱に入れると削除されるようにしたい
4. 対象の表示：ゴミ箱ならゴミ箱に重なっている際にゴミ箱の色が変わる、など、移動先が視覚的にわかるようにしたい

実はこれらの要件はかなり厳しいです。
先ず、1のリアルタイム入れ替えですが、これを自前で実装するのはほぼ無理です。この時点で何らかのライブラリに頼ることになります。そして、3のゴミ箱機能、これは実は1と矛盾する機能です。普通に1を実装すると、ゴミ箱に運んだ際にタスクとゴミ箱の位置が入れ替わるという挙動になります。タスクどうしは入れ替え、ゴミ箱には移動、という場合分けが必要です。これは孫から親への移動でも同様です。
また、実は1と2も矛盾する要素があります。表示上、親要素の中に子要素を表示していますが、普通に実装すると子要素にたどり着く前に親要素の入れ替えが発生してしまいます。また、（言葉での説明はかなり難しいですが）親要素が子要素に移動することで親要素が画面から削除され、削除された分親要素の位置が詰まることで場所の変更が生じ、移動対象が別のタスクにずれる、といったことも起こります。

### ライブラリの選定
D&Dの実装ですが、基本的にライブラリを使用すべきと思います（理由は後ほど）。見つけたライブラリを書き出します。

- [vue.draggable.next](https://github.com/SortableJS/vue.draggable.next)：Vue3で使用するなら最も有名なD&Dでソートする為のライブラリ
- [Vue.Draggable](https://github.com/SortableJS/Vue.Draggable?tab=readme-ov-file)：上のnextの全身、Vue2で使うなら
- [Sortable](https://github.com/SortableJS/Sortable)：上記Draggableの中身・本体。JS用でAngularやReact用もある
- [VueDraggablePlus](https://vue-draggable-plus.pages.dev/en/)：今回使用したライブラリ。上記nextが長期間放置されている為に作成され、nextから一部改善されている
- [Vue3 DnD](https://www.vue3-dnd.com/en/)：使用していないので詳細はわからないが、使いやすそうに見える。次回があれば試したい。様々な用途に使用可能。
- [生のDnD API](https://developer.mozilla.org/ja/docs/Web/API/HTML_Drag_and_Drop_API)：基本的にはファイル用
- VueのDnD：恐らく生のAPIをラップ?したもの
- [VueUse](https://vueuse.org/core/useDraggable/)：VueUseに含まれるD&D、基本的に自由移動型、ソートではない
- [draggable等](https://shopify.github.io/draggable/examples/)：Shopifyが開発したらしい様々な機能を持つD&Dライブラリ。ただし見た目含めて結構独特なのと、説明がわかりにくい、記事が少ない為使いこなすのは難しそう
- [interact.js](https://interactjs.io/)：自由移動型で高機能なライブラリ
- [Draggabilly](https://draggabilly.desandro.com/)：自由移動型、jQueryかバニラで使用する。更新が3年前で少し古い。
- [Packery](https://packery.metafizzy.co/)：D&Dで画面レイアウトの変更ができるなんかすごいライブラリ、パズルをやっている気分になる

### 詳細の実装、問題点等

・ゴミ箱に挿入する際の問題
今回使用したplusにはグループ機能とクローン機能があります。グループ機能は、基本的に同グループ間でしか移動できないようにするもので、クローン機能は別グループに移動する際に移動ではなく複製する機能です。今回はタスクとゴミ箱を別のグループで作成し、タスクからゴミ箱へのクローンのみ許可しました。また、ゴミ箱の中身は透明にし、見えないようにすることでクローンではなく削除に見えるようにしました。クローンなので元の位置にタスクが残ってしまいますが、APIでDB上で削除した後再取得するので、結局元の位置からも消えるという力技です。

・親から子に移動する際の問題
こちらもクローンに設定することで、元の位置にタスクが残り、親の位置にずれが発生しないようにしています。こちらも元のタスクが残ってしまう現象が発生しますが、移動前と移動後のタスクを検索し、親も位置も変わっていないものを削除対象と判断して削除することにより対応しています。
また、子要素と入れ替える前に親要素との入れ替えが発生する現象については、plusでは交換が発生する閾値を設定できるため、親の閾値を高め、子要素の閾値を緩和することで対応しました。

・対象の表示
plusには私が見た限りドラッグ中の要素がドロップゾーンの上にあるかを判断する機能はありません。そこで、生のAPIを同時に使用して、ドラッグしながら重なっている場合はドロップゾーンの色を変えるように実装しました。

・生のAPIの問題
D&D APIでは、例えばドラッグ開始時に「dragstart」イベント、ドロップゾーンに入った際に「dragenter」ゾーンから出た場合に「dragleave」イベントが発生します。この時の問題は、対象の要素が入れ子になっているとき、動かすたびに、範囲から出入りしていなくてもenterとleaveが呼ばれ続けるという現象が起こります。D&Dについては親要素に設定したイベントが子要素にも波及し、設定した範囲の判定がうまく取れません。また、この波及は基本的に止めることができません。この問題は根深く、以下のように解決のための記事がいくつかありますが、本質的な解決には至っておりません。正直、本当の修正はD&D API自体が修正される必要があると思います。

参考：
https://qiita.com/keiliving/items/5e8b26e6567efbc15765
https://qiita.com/sounisi5011/items/dc4878d3e8b38101cf0b

# 終わりに
まあ頑張って作りましたのでよろしければ使ってください。
ご要望もお待ちしております（実装するとは言ってない）。