開発概要
目的
- 現在、アプリ内のメニューでGoogleドキュメントへのリンクとして実装されている「マニュアル表示」を、アプリ内で直接PDFとして閲覧できるUIに改善する
- トグル(ExpansionTileなど)を開いた際に、その中でPDFが表示される仕組みを構築する
- ユーザーがアプリを離れることなく、マニュアルを確認できるようにする
- バックエンドでGoogleドキュメントのURLをPDF形式に変換し、フロントエンドでPDF表示できるようにする
開発期間
考えられる開発内容
Phase 1: バックエンド実装(API側)
GoogleドキュメントURL変換処理の実装
Taskエンティティの拡張
TaskUseCaseの更新
動作確認
Phase 2: フロントエンド実装(Mobile側)
PDF表示ライブラリの導入
manual_list_page.dartのUI変更
ExpansionTileへの変更
PDF表示Widgetの実装
ローディングとエラーハンドリング
フォールバック機能の実装
レイアウト調整
Phase 3: 動作確認
バックエンドのテスト
フロントエンドのテスト
備考
- 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を使用)
参考リンク
バックエンド関連
フロントエンド関連
開発の流れ
- PMにIssue(タスク)をもらう
- 開発をする(↓の「リンク」の『開発のやり方』を見よう!)
- チェックボックスを押していこう
- ヤバい状況になったらIssueの右側にあるStatusを「Help」にしてPMにSlackで連絡しよう
- チェックボックスが全部押せたらプルリクを作ろう
- レビューを待とう
- 修正点があれば修正しよう。なければPMがマージします!お疲れ様!
SeeFTのタスク管理のルール
- タスクは全てGit-Hub Projectで管理する
- 全てのタスクに期日を決める
- 毎週タスクの進捗を確認する(MTに出られない人はSlackで報告)
- 毎週忙しさ(消化できるタスク量)を共有する
- Helpは余裕のある人がいれば巻き取る。いなければ期日を変更する
リンク
開発概要
目的
開発期間
考えられる開発内容
Phase 1: バックエンド実装(API側)
GoogleドキュメントURL変換処理の実装
api/lib/utils/google_docs_converter.goを作成(新規ファイル)ConvertToPdfUrl(url string) stringメソッドを実装https://docs.google.com/document/d/{DOC_ID}/edit→https://docs.google.com/document/d/{DOC_ID}/export?format=pdfhttps://docs.google.com/spreadsheets/d/{SHEET_ID}/edit#gid={GID}→https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=pdf&gid={GID}Taskエンティティの拡張
api/lib/entity/task.goを更新Task構造体にPdfUrlフィールドを追加(JSONタグ:pdfUrl)UrlフィールドはGoogleドキュメントのURLとして保持(後方互換性のため)TaskUseCaseの更新
api/lib/usecase/task_usecase.goを更新GetTasksメソッドで、各タスクのUrlをConvertToPdfUrlで変換し、PdfUrlフィールドに設定GetTaskByIDメソッドでも同様の処理を追加GetTasksByShiftメソッドでも同様の処理を追加GetTasksByUserIDメソッドでも同様の処理を追加Urlフィールドはそのまま保持(既存の機能への影響を避けるため)動作確認
/tasksエンドポイントのレスポンスにpdfUrlフィールドが含まれているか確認urlフィールドがそのまま返却されているか確認(後方互換性)Phase 2: フロントエンド実装(Mobile側)
PDF表示ライブラリの導入
mobile/pubspec.yamlにpdfx: ^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メソッドを修正ListTileをExpansionTileに変更Container、padding、ListView.builder)を維持height: 40)を削除し、ExpansionTileの自然な高さに任せるPDF表示Widgetの実装
_ManualItemStateクラスを作成(StatefulWidgetとして分離、または既存のStateクラス内で状態管理)isLoading,hasError,pdfUrl)children内にPDF表示Widgetを実装PdfViewPinchまたはPdfViewを使用PdfControllerPinchでPDFドキュメントを制御pdfUrlフィールドを使用(manuals[index]["pdfUrl"])pdfUrlが空の場合はurlフィールドを使用(フォールバック)ローディングとエラーハンドリング
PdfViewPinchのloadingBuilderでCircularProgressIndicatorを表示PdfViewPinchのerrorBuilderでエラーメッセージを表示フォールバック機能の実装
ElevatedButtonまたはTextButtonを使用url_launcherのlaunchUrlで外部ブラウザを開く_launchManualUrlメソッドと同様の実装urlフィールド(Googleドキュメントの元のURL)を使用レイアウト調整
children内のレイアウトを調整SizedBoxでPDF表示エリアの高さを制限(例:height: 400)Phase 3: 動作確認
バックエンドのテスト
/tasksエンドポイントのレスポンス確認pdfUrlフィールドが正しく含まれているかurlフィールドがそのまま返却されているかフロントエンドのテスト
web/index.htmlにPDF.jsが正しく追加されているかpdfUrlが正しく使用されているか備考
pdfxパッケージを採用(オープンソース、Web対応、商用ライセンス不要)urlフィールドはそのまま保持し、新しくpdfUrlフィールドを追加することで、既存の機能への影響を最小限に抑えます/editを/export?format=pdfに変換/edit#gid={GID}を/export?format=pdf&gid={GID}に変換web/index.htmlにPDF.jsのスクリプトが追加されていることを確認してくださいshift_card.dartの_buildManualSectionは現時点では変更しません。必要に応じて後で同様の改善を検討できます参考
対象ファイル
バックエンド(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変換処理の実装例
バックエンド: TaskUseCaseでの使用例
フロントエンド: PDF表示の実装例
フロントエンド: ExpansionTileの実装例
データフロー
参考リンク
バックエンド関連
フロントエンド関連
開発の流れ
SeeFTのタスク管理のルール
リンク