20260607134939

ガントバー移動挙動の調査報告書

Ganttビューにおけるガントバーの描画および移動操作(ドラッグ&ドロップ)の仕組みを詳細に調査しました。その結果、移動時に内部で発生する一連の処理プロセスと、挙動が安定しない原因となっている具体的な不具合が明らかになりました。以下に詳細を報告します。


1. ガントバー移動時の処理プロセス

ユーザーがガントバー(予定バーまたは実績バー)をドラッグして移動させるとき、システム内では以下の順序で処理が実行されます。

sequenceDiagram
    autonumber
    actor User as ユーザー
    participant Row as GanttTimelineRow (UI)
    participant Store as Zustand Store (tasks / dragState)
    participant DragHook as useGanttDrag (Hook)
    participant Utils as taskStoreUtils

    User->>Row: ガントバー上でマウスダウン (MouseDown)
    Row->>Store: setDragState() でドラッグ開始状態を保存<br/>(始点座標、ドラッグ開始日・終了日などをセット)
    Note over Row,Store: ドラッグ中の行のみが再描画され、<br/>Zustandの dragState 基準でバー位置を即時反映

    loop マウス移動中 (MouseMove)
        User->>DragHook: マウス移動
        DragHook->>Store: マウス移動量(deltaX)を日付の差分(deltaDays)に変換し、<br/>setDragState() でドラッグ現在値をリアルタイム更新
    end

    User->>Row: マウスアップ (MouseUp)
    DragHook->>Store: updates を生成して state.updateTask() をコール
    Store->>Utils: applyDateCalculations() による日程自動補正<br/>(営業日カレンダー考慮)
    Store->>Utils: propagateDependencyDates() による依存タスクの日程自動シフト
    Store->>Utils: recalculateParentDatesRecursive() による親タスク日程の再帰ロールアップ
    Store->>Store: 最終的なタスク情報を保存、dragState を null にリセット
    Store->>Row: 変更後のタスクデータでガントバーと依存線を完全再描画

各ステップの詳細な処理内容

  1. ドラッグ開始 (onMouseDown / UI)

    • ユーザーがガントバーをクリックすると、イベントバブリングを防いだ上でドラッグが開始されます。
    • Zustandストア内の dragStatetaskId、ドラッグモード(move / resize-left / resize-right)、クリック位置の日付(initialStartDate / initialEndDate 等)が保存されます。
    • このドラッグ状態は、高頻度なUI更新時のパフォーマンス確保のため、ドラッグ対象の1行のみを部分レンダリングする設計になっています。
  2. ドラッグ中 (onMouseMove / useGanttDrag)

    • ウィンドウ全体のマウス移動イベントを監視し、移動ピクセル数(deltaX)を1日あたりのピクセル幅(timelineMetrics.pixelsPerDay)で割ることで、移動日数(deltaDays)を算出します。
    • 移動日数に基づき、ドラッグ中のプレビュー用日付(currentStartDate / currentEndDate)を算出して dragState をリアルタイムに更新します。
  3. ドラッグ確定 (onMouseUp / useGanttDrag)

    • マウスが離されたタイミングで、確定された日付から稼働日数(Duration)を再計算し、state.updateTask() を呼び出します。
    • 呼び出し後、dragStatenull にリセットされ、ドラッグプレビュー表示が終了します。
  4. 日付計算および依存関係・親子の伝播 (updateTask / Zustand Store & Utils)

    • ベースライン固定 (baselineLocked) が ON の場合、予定列 (planStartDate等) への変更は破棄され、実績列 (startDate等) のみが更新されます。
    • 営業日計算: applyDateCalculations により、営業日カレンダー (calendar) を加味した終了日や期間の自動計算が行われます。
    • 依存関係の自動追従 (propagateDependencyDates): 移動したタスクを先行タスクとする後続タスクが存在する場合、それらのタスクの開始日・終了日もカレンダーに沿って自動的に後ろ倒しされます。
    • 親タスクの日程ロールアップ (recalculateParentDatesRecursive): 移動したタスクが子タスクである場合、親タスクの開始日・終了日を「全子タスクの最小開始日〜最大終了日」に自動で再帰計算して上位へ伝播させます。

2. 検出された不具合と設計上の課題

調査の結果、ガントバー描画や移動時の挙動が「安定しない」と感じられる原因となっている3つの不具合・設計上の問題点を特定しました。

不具合 ①:予定バーのドラッグ開始時にバー幅が1日分に縮小するバグ (UIバグ)

  • 発生箇所: GanttTimelineRow.tsx
  • 原因: 予定バーをドラッグ移動(mode: 'move')するためにマウスダウンした際、ドラッグ状態の初期化で currentEndDate に誤って予定終了日ではなく予定開始日をセットしています。
    // GanttTimelineRow.tsx L149 付近
    currentStartDate: new Date(planStartStr),
    currentEndDate: new Date(planStartStr), // <-- 本来は planEndStr にすべきタイポ
    
  • 影響: ユーザーが予定バーをクリックした瞬間に、ドラッグプレビュー用のバー幅が画面上で**「1日分」に一瞬で縮んでしまい**、マウスを少し動かすと元の長さに戻るという、非常に不自然なチラつきやガタつきが発生します。

不具合 ②:ベースライン固定OFF(計画フェーズ)時の予定と実績の同期漏れ (ロジックバグ)

  • 発生箇所: useTaskStore.tsupdateTask() および taskStoreUtils.tspropagateDependencyDates()
  • 仕様(HANDOFF.mdより):

    「ベースライン固定が OFF のとき:予定と実績は相互に自動同期(連動)するため、最初の計画フェーズでの二重入力の手間を防止。」

  • 実際のコード:
    • updateTask には、baselineLocked === false のときに予定(Plan)と実績(Actual)の値を同期するコードが存在しません。
    • そのため、予定列を編集・ドラッグしても実績列は null のままとなり、逆に実績列を編集しても予定列は更新されません。
    • さらに、依存関係による日程追従処理 (propagateDependencyDates) が予定列(Plan)のみを更新対象としており、実績列(Actual)を完全に無視しています。このため、ベースライン固定がOFFであっても、後続タスクは予定バーのみが移動し、実績データとの乖離が発生します。
  • 影響: 計画作成中に予定を動かしても実績データが同期されず、ベースライン固定を有効にした時点で実績データが空または乖離した状態になってしまいます。

不具合 ③:WBSカラムとガントバーにおける日付フォールバックロジックの乖離 (表示バグ)

  • 発生箇所: TaskPlanDateCell.tsxGanttTimelineRow.tsx
  • 現状の処理:
    • WBSテーブルのカラム( TaskPlanDateCell.tsx )では、予定日付が未設定の場合、実績日付を代替表示(フォールバック)するロジックになっています。
      const planStart = task?.planStartDate || task?.startDate || '';
    • しかし、ガント描画側( GanttTimelineRow.tsx )では、予定データ (task.planStartDate) 自体が明示的に存在しない限り、予定バーの描画をスキップします。
  • 影響: WBSテーブル上にはフォールバックされた日付が表示されているにもかかわらず、ガントチャート上にはバーが描画されないという、データ表示の不一致が発生します。

3. 推奨される修正アプローチ

これらの問題を解消し、ガントバーの描画および移動挙動を完全に安定させるためには、以下の修正を行うことを推奨します。

  1. 予定バーのドラッグ初期化のタイポ修正

    • GanttTimelineRow.tsx の L149 を new Date(planEndStr) に修正し、クリック時のバー縮小バグを即時解消します。
  2. ベースラインOFF時の予定・実績の双方向同期の実装

    • ストアの updateTask および applyDateCalculations において、baselineLockedfalse の場合に「予定 ➔ 実績」および「実績 ➔ 予定」の変更を相互にコピーするようにします。
    • propagateDependencyDates 内で、baselineLockedfalse の場合は実績の日程も予定と同期してシフトするように処理を拡張します。
  3. ガント描画のフォールバックロジックの統一

    • GanttTimelineRow.tsx でも、WBSセルと同様に planStartDate がない場合に startDate を使用するフォールバックを適用し、表示の不一致を解消します。