Skip to content

マニュアルをGoogleドキュメントからPDF表示変換 #248

@taminororo

Description

@taminororo

開発概要

目的

  • 現在、アプリ内のメニューでGoogleドキュメントへのリンクとして実装されている「マニュアル表示」を、アプリ内で直接PDFとして閲覧できるUIに改善する
  • トグル(ExpansionTileなど)を開いた際に、その中でPDFが表示される仕組みを構築する
  • ユーザーがアプリを離れることなく、マニュアルを確認できるようにする
  • バックエンドでGoogleドキュメントのURLをPDF形式に変換し、フロントエンドでPDF表示できるようにする

開発期間

  • 開始日:
  • 締切日:

考えられる開発内容

Phase 1: バックエンド実装(API側)

GoogleドキュメントURL変換処理の実装

  • api/lib/utils/google_docs_converter.go を作成(新規ファイル)
    • GoogleドキュメントのURLをPDF形式に変換する関数を実装
    • ConvertToPdfUrl(url string) string メソッドを実装
    • 対応パターン:
      • Google Docs: https://docs.google.com/document/d/{DOC_ID}/edithttps://docs.google.com/document/d/{DOC_ID}/export?format=pdf
      • Google Sheets: https://docs.google.com/spreadsheets/d/{SHEET_ID}/edit#gid={GID}https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=pdf&gid={GID}
    • URLがGoogleドキュメントでない場合(既にPDF URLなど)はそのまま返す

Taskエンティティの拡張

  • api/lib/entity/task.go を更新
    • Task 構造体に PdfUrl フィールドを追加(JSONタグ: pdfUrl
    • 既存の Url フィールドはGoogleドキュメントのURLとして保持(後方互換性のため)

TaskUseCaseの更新

  • api/lib/usecase/task_usecase.go を更新
    • GetTasks メソッドで、各タスクの UrlConvertToPdfUrl で変換し、PdfUrl フィールドに設定
    • GetTaskByID メソッドでも同様の処理を追加
    • GetTasksByShift メソッドでも同様の処理を追加
    • GetTasksByUserID メソッドでも同様の処理を追加
    • 既存の Url フィールドはそのまま保持(既存の機能への影響を避けるため)

動作確認

  • /tasks エンドポイントのレスポンスに pdfUrl フィールドが含まれているか確認
  • GoogleドキュメントのURLが正しくPDF形式に変換されているか確認
  • 既存の url フィールドがそのまま返却されているか確認(後方互換性)

Phase 2: フロントエンド実装(Mobile側)

PDF表示ライブラリの導入

  • mobile/pubspec.yamlpdfx: ^1.3.0 を追加
  • flutter pub get を実行してパッケージをインストール
  • flutter pub run pdfx:install_web を実行してWeb対応のセットアップ(PDF.jsをweb/index.htmlに追加)

manual_list_page.dartのUI変更

ExpansionTileへの変更

  • mobile/lib/pages/manual_list_page.dart_manualItem メソッドを修正
    • ListTileExpansionTile に変更
    • タイトルを「業務マニュアル」またはマニュアルのタスク名に設定
    • 既存のレイアウト構造(ContainerpaddingListView.builder)を維持
    • 高さの制約(height: 40)を削除し、ExpansionTileの自然な高さに任せる

PDF表示Widgetの実装

  • _ManualItemState クラスを作成(StatefulWidgetとして分離、または既存のStateクラス内で状態管理)
    • PDF読み込み状態を管理(isLoading, hasError, pdfUrl
  • ExpansionTileの children 内にPDF表示Widgetを実装
    • PdfViewPinch または PdfView を使用
    • PdfControllerPinch でPDFドキュメントを制御
    • APIから取得した pdfUrl フィールドを使用(manuals[index]["pdfUrl"]
    • pdfUrl が空の場合は url フィールドを使用(フォールバック)

ローディングとエラーハンドリング

  • PDF読み込み中のローディングインジケーターを実装
    • PdfViewPinchloadingBuilderCircularProgressIndicator を表示
  • PDF読み込み失敗時のエラーハンドリングを実装
    • PdfViewPincherrorBuilder でエラーメッセージを表示
    • CORS制限やネットワークエラーに対応

フォールバック機能の実装

  • 「ブラウザで開く」ボタンを実装
    • ElevatedButton または TextButton を使用
    • url_launcherlaunchUrl で外部ブラウザを開く
    • 既存の _launchManualUrl メソッドと同様の実装
    • PDF表示エリアの下に配置
    • エラー時も表示して代替手段を提供
    • url フィールド(Googleドキュメントの元のURL)を使用

レイアウト調整

  • ExpansionTileの children 内のレイアウトを調整
    • SizedBox でPDF表示エリアの高さを制限(例: height: 400
    • PDF表示エリアの上下に適切なパディングを設定
    • 「ブラウザで開く」ボタンとPDF表示エリアの間隔を調整

Phase 3: 動作確認

バックエンドのテスト

  • /tasks エンドポイントのレスポンス確認
    • pdfUrl フィールドが正しく含まれているか
    • GoogleドキュメントのURLが正しくPDF形式に変換されているか
    • 既存の url フィールドがそのまま返却されているか

フロントエンドのテスト

  • モバイル(iOS/Android)での動作確認
    • PDFの表示が正常に動作するか
    • ローディングインジケーターが表示されるか
    • エラーハンドリングが正常に動作するか
    • 「ブラウザで開く」ボタンが正常に動作するか
  • Webでの動作確認
    • web/index.html にPDF.jsが正しく追加されているか
    • PDFの表示が正常に動作するか
    • CORS制限がないか確認
  • 既存レイアウトの確認
    • メニュー画面のレイアウトが崩れていないか
    • 他の機能に影響がないか
  • 統合テスト
    • バックエンドとフロントエンドが正しく連携しているか
    • APIから取得した pdfUrl が正しく使用されているか

備考

  • PDFライブラリの選定: pdfxパッケージを採用(オープンソース、Web対応、商用ライセンス不要)
  • バックエンドとフロントエンドの連携: バックエンドでGoogleドキュメントのURLをPDF形式に変換し、フロントエンドでPDF表示する方式を採用
  • 後方互換性: 既存の url フィールドはそのまま保持し、新しく pdfUrl フィールドを追加することで、既存の機能への影響を最小限に抑えます
  • GoogleドキュメントのURL変換:
    • Google Docs: /edit/export?format=pdf に変換
    • Google Sheets: /edit#gid={GID}/export?format=pdf&gid={GID} に変換
    • 既にPDF URLの場合はそのまま返す
  • CORS制限: GoogleドキュメントのPDFエクスポートURLはCORS制限がある可能性があります。エラーハンドリングで対応し、「ブラウザで開く」ボタンで代替手段を提供します
  • Webビルド: Webビルドではweb/index.htmlにPDF.jsのスクリプトが追加されていることを確認してください
  • パフォーマンス: 大きなPDFファイルの読み込みには時間がかかる可能性があります。ローディングインジケーターでユーザーに状態を伝えます
  • 既存機能への影響:
    • shift_card.dart_buildManualSectionは現時点では変更しません。必要に応じて後で同様の改善を検討できます
    • 既存のAPIクライアント(Admin側など)への影響を確認し、必要に応じて対応します

参考

対象ファイル

バックエンド(API)

  • api/lib/utils/google_docs_converter.go - 新規作成(GoogleドキュメントURL変換処理)
  • api/lib/entity/task.go - PdfUrlフィールドの追加
  • api/lib/usecase/task_usecase.go - URL変換処理の追加
  • api/lib/internals/controller/task_controller.go - 変更なし(既存のエンドポイントを使用)

フロントエンド(Mobile)

  • mobile/lib/pages/manual_list_page.dart - メインの変更対象
  • mobile/pubspec.yaml - PDF表示ライブラリの追加
  • mobile/lib/widgets/shift_card.dart - 参考(ExpansionTileの実装例)

実装の詳細

バックエンド: GoogleドキュメントURL変換処理の実装例

package utils

import (
    "regexp"
    "strings"
)

// ConvertToPdfUrl GoogleドキュメントのURLをPDF形式に変換
func ConvertToPdfUrl(url string) string {
    if url == "" {
        return ""
    }
    
    // Google Docs のパターン
    // https://docs.google.com/document/d/{DOC_ID}/edit
    docPattern := regexp.MustCompile(`https://docs\.google\.com/document/d/([a-zA-Z0-9_-]+)/.*`)
    if matches := docPattern.FindStringSubmatch(url); len(matches) > 1 {
        docID := matches[1]
        return "https://docs.google.com/document/d/" + docID + "/export?format=pdf"
    }
    
    // Google Sheets のパターン
    // https://docs.google.com/spreadsheets/d/{SHEET_ID}/edit#gid={GID}
    sheetPattern := regexp.MustCompile(`https://docs\.google\.com/spreadsheets/d/([a-zA-Z0-9_-]+)/.*gid=([0-9]+)`)
    if matches := sheetPattern.FindStringSubmatch(url); len(matches) > 2 {
        sheetID := matches[1]
        gid := matches[2]
        return "https://docs.google.com/spreadsheets/d/" + sheetID + "/export?format=pdf&gid=" + gid
    }
    
    // 既にPDF URLまたはその他のURLの場合はそのまま返す
    return url
}

バックエンド: TaskUseCaseでの使用例

func (b *taskUseCase) GetTasks(c context.Context) ([]entity.Task, error) {
    // ... 既存の処理 ...
    
    for rows.Next() {
        err := rows.Scan(
            &task.ID,
            &task.Task,
            &task.PlaceID,
            &task.Url,
            // ... 他のフィールド ...
        )
        if err != nil {
            return nil, errors.Wrapf(err, "cannot connect SQL")
        }
        
        // PDF URLを生成
        task.PdfUrl = utils.ConvertToPdfUrl(task.Url)
        
        tasks = append(tasks, task)
    }
    
    return tasks, nil
}

フロントエンド: PDF表示の実装例

PdfViewPinch(
  controller: PdfControllerPinch(
    document: PdfDocument.openUrl(manuals[index]["pdfUrl"] ?? manuals[index]["url"]),
  ),
  builders: PdfViewPinchBuilders<DefaultBuilderOptions>(
    loadingBuilder: (context, progress) => Center(
      child: CircularProgressIndicator(),
    ),
    errorBuilder: (context, error) => Center(
      child: Text('PDF読み込みエラー: ${error.toString()}'),
    ),
  ),
)

フロントエンド: ExpansionTileの実装例

ExpansionTile(
  title: Text(manuals[index]["task"].toString()),
  children: [
    SizedBox(
      height: 400,
      child: PdfViewPinch(...),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: ElevatedButton(
        onPressed: () => _launchManualUrl(manuals[index]["url"]),
        child: Text('ブラウザで開く'),
      ),
    ),
  ],
)

データフロー

マニュアル一覧ページ表示(Mobile)
  ↓
APIリクエスト: GET /tasks
  ↓
TaskUseCase.GetTasks() 実行
  ↓
データベースからタスク取得
  ↓
各タスクのUrlをConvertToPdfUrl()で変換
  ↓
PdfUrlフィールドを設定してレスポンス返却
  ↓
Mobile側でgetAllManual()がレスポンスを受信
  ↓
各マニュアルをExpansionTileで表示
  ↓
ユーザーがトグルを開く
  ↓
pdfUrlを使用してPDFを読み込み(PdfViewPinch)
  ↓
ローディング表示 → PDF表示
  ↓
(エラー時)エラーメッセージ + 「ブラウザで開く」ボタン(元のurlを使用)

参考リンク

バックエンド関連

フロントエンド関連

開発の流れ

  1. PMにIssue(タスク)をもらう
  2. 開発をする(↓の「リンク」の『開発のやり方』を見よう!)
  3. チェックボックスを押していこう
  4. ヤバい状況になったらIssueの右側にあるStatusを「Help」にしてPMにSlackで連絡しよう
  5. チェックボックスが全部押せたらプルリクを作ろう
  6. レビューを待とう
  7. 修正点があれば修正しよう。なければPMがマージします!お疲れ様!

SeeFTのタスク管理のルール

  1. タスクは全てGit-Hub Projectで管理する
  2. 全てのタスクに期日を決める
  3. 毎週タスクの進捗を確認する(MTに出られない人はSlackで報告)
  4. 毎週忙しさ(消化できるタスク量)を共有する
  5. Helpは余裕のある人がいれば巻き取る。いなければ期日を変更する

リンク

Metadata

Metadata

Labels

Size-M開発時間の目安は10時間✨Backendバックエンドのタスク. 主にGo, TypeScriptを使用✨Frontend-MobileMobileのフロントのタスク. 主にDart/Flutterを使用優先度1Better・なるべくここまでは実装したい枠

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions