メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Notion のタスクのメモはどこに書く?コメント機能よりも「ページ下部」がオススメな理由と実践方法

こんにちは。メドピア内 Slack チャンネル 「#club_notion」 部長の佐藤太一(@teach_kaiju)です。

今回の「ClinPeerアプリ開発の裏側連載記事」では Notion を用いたタスク管理におけるメモの取り方を紹介します。

tech.medpeer.co.jp

目次

はじめに

Notion はその自由度の高さから、様々な情報を集約できる万能ツールとして扱うことができます。特にタスク管理においては、詳細情報だけでなく、関連するメモやアイデア、Slack でのやりとりのリンクなどを一緒に残しておきたい場面も多いのではないでしょうか。

そんなとき、「タスクに関するメモ、どこに書いていますか?」

よく使われるのは Notion の画面上部のコメントだと思います。

コメント(画面上部)

手軽に書ける反面、他タスク管理ツールとの挙動の違いで戸惑った経験はありませんか?

今回は ClinPeer チームの開発タスクで実際に行われている、メモはコメント機能ではなく「タスクページの下部」に書くという文化を紹介したいと思います!

コメント機能のよくある課題点

手軽に使えるコメント機能ですが、タスクのメモを残す場所としては、いくつかの課題があります。 情報は2025年5月時点のものです。

課題1: コメントのリンクを取得することができない

コメントのスレッドのリンクは取得できますが、2つめ以降のコメントはリンクが取得できません。

コメントへのリンクがない

課題2: Enter で送信

誤って書き途中のまま送信してしまいがち。
(Enter は改行という挙動に切り替えられるようになってほしい)

課題3: 場所が画面上部で折り畳まれるデザイン

画面上部のコメントは数が多いと折り畳まれます。中身を確認するために開く必要があり、これが手間です。

コメントの折りたたみ

ページ下部へのメモの仕方

ページ下部にメモをとることで、上記の課題を解決できる他、ブロックを用いた見やすいデザインを作ることができる等のメリットも得られます。

その具体的な方法がこちらです。

ページ下部にメモ

  1. 「Memo」という見出しを作る
  2. 日付を書く
  3. 誰が書いたのかを示すために自身の絵文字のアイコンを出す(省略することはよくあります)
  4. 内容を記述

主なメモの内容

  • タスクの進捗
  • 関連するやりとりへのリンク
  • 思ったこと・気づき
  • 課題
  • 簡易的な議事録

特にタスクに関連するやりとりへのリンク(Slack等)はメモっておくと後から見返す時に非常に助かります。

タスクに関するMTGを行う場合は、最近出た AI ミーティングノートをタスクページに作るのも良さそうです。

Q & A

Q. 他のメンバーへの通知はどうする ?
A. 画面上部のコメント使います。内容が多い時は Memo のリンクをコメントに貼ります。ページ内メンションは使っていません。

Q. ページがどんどん長くなって見づらくならないか?
A. 画像を大量に貼ったりすると長くなりますが、トグルで折り畳めば気になりません。

まとめ

今回はタスクのメモをページ下部にとる具体的なやり方を紹介しました。

タスクにメモをとっておくと、タスクページそのものがドキュメントの役割を果たしたりすることが可能になります。情報を追いやすくなるためとてもオススメです。

また、本手法はチームでのタスク管理だけではなく、個人でのタスク管理でも使えます。
ぜひ試して見てください!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

SwiftUIにおけるEnvironmentの活用法

こんにちは!メドピアにてモバイルアプリエンジニアをしている王です。 今回の「ClinPeerアプリ開発の裏側連載記事」では、SwiftUIのEnvironmentについてお話しできればと思います。 tech.medpeer.co.jp

SwiftUIのEnvironmentは、ビュー間でデータを共有するための強力な依存性注入(DI)の仕組みです。多くのSwiftUIプロジェクトで活用されており、ビュー間のデータ伝達を簡素化し、アプリ設計の柔軟性を高めます。ClinPeerでは、ほぼすべての画面をSwiftUIで構築しており、Environmentの活用について考察しています。

SwiftUIにおけるUI専用の依存性注入

依存性注入(Dependency Injection、DI)は、モダンなソフトウェア開発において重要な技術であり、以下の利点があります。

  • 疎結合:コンポーネント間の依存関係を最小限に抑えることで、モジュールの再利用性と保守性が向上します。
  • 保守性:依存関係が明示的になることで、コードの理解と修正が容易になります。
  • テスト容易性:依存関係を差し替えることで、ユニットテストが容易になります。

SwiftUIのEnvironmentは、ビューがロードされた後にのみ値を取得できるという特徴があります。この設計は、SwiftUIが宣言型UIフレームワークであることを反映しており、ビューの構築と更新がデータや環境と一貫性を保つようになっています。

値型以外の活用

Environmentは、単なる値型の注入にとどまらず、関数、ファクトリーメソッド、プロトコル制約など、さまざまな形式の依存性を注入できます。実際、SwiftUIの標準Environment値には、editModedismissmanagedObjectContextなど、非値型の例も含まれています。

開発者は、Environmentの活用を値型に限定せず、SwiftUIの特性を活かして、より柔軟で疎結合なコンポーネントを構築することが重要です。

Observationフレームワークの活用

EnvironmentObjectを使用する際、依存性の注入を忘れるとアプリがクラッシュするリスクがあります。一方、Environmentはデフォルト値を要求するため、より安全で信頼性の高い設計が可能です。iOS 17以降、Observationフレームワークの導入により、可観測オブジェクトの注入がさらに容易になりました。

extension EnvironmentValues {
    @Entry var article: Article = .init()
}

@Observable
class Article {
    // プロパティやメソッドを定義
}

struct ContentView: View {
    @Environment(\.article) var article
    var body: some View {
        // ビューの構築
    }
}

この方法では、クラッシュのリスクを回避し、同じ型の可観測インスタンスを複数使い分ける柔軟性も得られます。

extension EnvironmentValues {
    @Entry var article: Article = .init()
    @Entry var article1: Article = .init()
    @Entry var article2: Article = .init()
}

Environmentの最適化

Environmentを使用してアプリの状態を管理する際、ビューの更新効率がユーザー体験に影響を与えることがあります。以下の最適化戦略を採用することで、不要なビューの再描画を抑えることができます。

精密な注入

複数のサブ状態を含む複合値型に対して、特定のプロパティのみを注入することで、不要な更新を回避できます。ビューが実際に必要とする部分の状態だけを購読することで、より効率的なリアクティブUIを構築できます。

struct UserState {
    var height = 175 // 単位: cm
    var weight = 75  // 単位: kg
}

extension EnvironmentValues {
    @Entry var userState = UserState()
}

struct HeightView: View {
    // heightのみが更新対象
    @Environment(\.userState.height) var height
    var body: some View {
        Text("身長: \(height) cm")
    }
}

struct WeightView: View {
    // weightのみが更新対象
    @Environment(\.userState.weight) var weight
    var body: some View {
        Text("体重: \(weight) kg")
    }
}

struct BMIView: View {
    @Environment(\.userState) var userState
    var body: some View {
        let heightInMeters = Double(userState.height) / 100.0
        let bmi = Double(userState.weight) / (heightInMeters * heightInMeters)
        return Text(String(format: "BMI: %.2f", bmi))
    }
}

struct RootView: View {
    @State var userState = UserState()
    var body: some View {
        List {
            Button("身長を変更") {
                userState.height = Int.random(in: 130...220)
            }
            Button("体重を変更") {
                userState.weight = Int.random(in: 35...120)
            }
            HeightView()
            WeightView()
            BMIView()
        }
        .environment(\.userState, userState)
    }
}

条件付きの更新

transformEnvironmentを使用すると、特定の条件を満たす場合にのみ環境値を更新できます。これにより、更新頻度を減らし、アプリの応答性と滑らかさを向上させることができます。

struct RootView: View {
    @State var userState = UserState()
    @State var height = 175
    var body: some View {
        List {
            Button("身長を変更") {
                height = Int.random(in: 130...220)
            }
            HeightView()
        }
        .transformEnvironment(\.userState) { state in
            guard height > 150 else {
                print("無視: \(height)")
                return
            }
            state.height = height // height > 150 の場合のみ更新
        }
    }
}

Environmentとサードパーティ製DIフレームワークの併用

SwiftUIのEnvironmentは優れた機能を提供しますが、ビューのライフサイクルに厳密に制限されます。ビジネスロジックをViewModel層に分離したり、ユニットテストを行う場合、サードパーティ製のDIフレームワークの使用が有効です。ClinPeerでは、FactoryというSDKを使用しています。

@Observable
class UserState {
    var height: Int = 175
    var weight: Int = 75
}

extension Container {
    var userState: Factory<UserState> {
        Factory(self) { UserState() }
            .scope(.shared)
    }
}

// 使用例
@Injected(\.userState) private var userState

ハイブリッドアーキテクチャの利点

Environmentとサードパーティ製DIツールを併用することで、以下の利点が得られます。

  • 柔軟なアーキテクチャ:ビュー層ではSwiftUIのEnvironmentを使用し、ビジネスロジック層ではFactoryなどのDIツールを活用できます。
  • テストの容易性:ビジネスロジックをUIから完全に切り離すことで、ユニットテストやモック化が簡単になります。
  • 保守性の向上:特定のUIフレームワークへの依存を最小限に抑え、コードベースの長期的な安定性と再利用性を高めます。

まとめ

SwiftUIのEnvironmentは、依存性注入とビューのライフサイクルを一体化した設計であり、視覚的なUIコンポーネントの境界を明確にしながら、ビュー階層におけるデータの効率的かつ制御可能な伝達を実現します。

また、精密な注入や選択的な変更によって、不要なビュー更新を避けることでアプリ全体の効率を高めることができます。 さらに、SwiftUIのエコシステムにおいては、柔軟にサードパーティ製のDIフレームワークを取り入れることで、より複雑なユースケースにも対応可能です。

Environmentの設計に対する深い理解は、拡張性が高く品質の高いSwiftUIアプリを構築するための確かな基盤となります。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

環境ごとの設定管理が可能な ClinPeer のフィーチャーフラグの紹介

こんにちは。サーバーサイドエンジニアの佐藤太一(@teach_kaiju)です。

今回の「ClinPeerアプリ開発の裏側連載記事」ではサーバーサイドにおける、フィーチャーフラグの実装方法を紹介します。

tech.medpeer.co.jp

目次

フィーチャーフラグとは

フィーチャーフラグ(機能フラグ、別名フィーチャートグル)は、機能のオン・オフを制御する仕組みです。
ClinPeer ではフィーチャーフラグを用いることで、開発中の機能の細かいリリースや、社内IPのみ機能を有効にするなどの柔軟な制御を実現しています。

フィーチャーフラグに関する詳細は以下の記事に書かれています。こちらもぜひご覧ください。

tech.medpeer.co.jp

機能の有効化 Feature#enabled?

ClinPeer では Flipper gem をラップした Feature クラスを用いています。

基本的な使い方

特定の機能(例:allow_access_to_new_sugoi_feature)が有効かどうかを調べるには、以下のように記述します。

if Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?

特定の条件で有効化

enabled? メソッドには、オプションで引数を渡すことができます。この引数を使うことで、「特定のユーザーだけに機能を有効にする」「特定のIPアドレスからアクセスされた場合のみ機能を有効にする」といった、より細かい制御が可能になります。

例えば、特定のIPアドレスからのアクセスに対してのみ機能を有効にしたい場合は、そのIPアドレス文字列を渡します。

if Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?(request.remote_ip)

許可するIPアドレスは、Flipper UI で設定します。

flipper_ui_allow_ip

フラグの運用

フィーチャーフラグの定義や操作は、主に config/features.yml ファイルと Flipper UI を通じて行います。

features.yml

フィーチャーフラグの設定は、config/features.yml ファイルで一元管理されます。このファイルには、各フラグの識別子 (kind)、説明 (description)、そして環境ごとの設定を記述します。

# config/features.yml の例
- kind: allow_access_to_new_sugoi_feature
  description: 新しいスゴイ機能へのアクセスを許可する
  development: flipper
  test: true
  staging: flipper
  production: flipper

各項目の意味は以下の通りです。

  • kind: フラグの一意な識別子です。コード中では Feature::KIND_NAME のようにして参照できます。
  • description: フラグの説明です。この内容は Flipper UI のダッシュボードにも表示されます。
  • 環境名 (development, test, staging, production など):
    • flipper: Flipper UI でフラグの有効/無効を制御する場合に指定します。(デフォルト: 無効)
    • true: その環境では常にフラグを有効にします。
    • false: その環境では常にフラグを無効にします。

ClinPeer ではデプロイ時にseed を実行し、その中で features.yml の内容をもとに差分を更新します。

# app/models/feature.rb の抜粋

class Feature < ActiveYaml::Base
  include ActiveHash::Enum

  # 略
  
  set_root_path Rails.root.join("config")

  enum_accessor :kind

  scope :flipper_controllable, -> { where(Rails.env => FLIPPER_VALUE) }

  FLIPPER_VALUE = "flipper"
  private_constant :FLIPPER_VALUE

  # 略
end
# seed の処理

features = Feature.flipper_controllable.pluck(:kind) # ymlからFlipper制御対象のkindを取得
current_features = Flipper.features.map(&:name) # 現在Flipperに登録されている機能名を取得

# ymlにあってFlipperにないものを追加
(features - current_features).each { |f| Flipper.add(f) }
# Flipperにあってymlにないものを削除 (ymlから削除されたフラグ)
(current_features - features).each { |f| Flipper.remove(f) }

フラグの新規追加

config/features.yml に新しいフラグの定義を追加します。 デプロイ時に seed で features.yml の内容をもとに差分を更新します。

フラグの有効・無効の切り替え

config/features.ymlflipper と設定されているフラグの有効/無効は、Flipper UI (社内用管理画面) 上で操作します。

flipper_ui_on_off

フラグの削除

不要になったフィーチャーフラグを削除する際は、以下の手順で行います。

  1. フラグの参照箇所をコード上から削除
  2. 上記対応をリリース
  3. config/features.yml から該当フラグの定義を削除

ポイント
「フラグ参照箇所の削除」と「ymlからのフラグ定義の削除」を同一のリリースに含めないようにしています。

  • Flipper は、存在しないフラグを参照した場合、無効 (false) として扱われます。
  • ymlの変更(フラグ定義の削除)を反映するデプロイタスクは、アプリケーションコードの反映よりも先に実行される場合があります。

もし同一リリースに含めてしまうと、ymlからフラグが削除された後、まだ古いコードがそのフラグを参照しているわずかな時間帯に、意図せず機能が無効化されてしまう可能性があります。

条件付き有効化の実装

Flipper には対象をflipper_idで識別し、一致した場合のみ機能を有効化するという機能があります。 具体的には以下の2つを比較し、一致した場合機能を有効化します。

箇所
enabled? の第二引数 puts some_obj.flipper_id # 127.0.0.1
Flipper.enabled?("allow_access_to_new_sugoi_feature", some_obj)
Flipper UI で設定した actor
flipper_ui_allow_ip

(actor は flipper_id という識別子を持ったオブジェクト。この識別子を比較することで actor が同一であるかどうかを判断しています。そして、actor が同一であれば機能を有効化します。)

https://www.flippercloud.io/docs/features/actors

ClinPeerでは StringFlipperActor クラスを導入することで、任意の文字列を直接Actorの識別子として扱えるように拡張しています。

# app/models/feature.rb の抜粋

class Feature < ActiveYaml::Base

# 略

  class StringFlipperActor
    attr_reader :value

    def initialize(value)
      @value = value
    end

    alias flipper_id value
  end

# 略

  def enabled?(obj = nil)
    case value
    when FLIPPER_VALUE
      obj = StringFlipperActor.new(obj) if obj.is_a?(String)
      Flipper.enabled?(kind, obj)
    else
      !!value
    end
  end

  private

  def value
    public_send(Rails.env)
  end
end

これにより、Feature#enabled? メソッドに文字列を渡すと、その文字列がそのまま flipper_id として扱われます。

Feature::ALLOW_ACCESS_TO_NEW_SUGOI_FEATURE.enabled?("127.0.0.1") # "127.0.0.1" がflipper_idとなる

この仕組みを利用することで、IPアドレスや特定の識別文字列など、モデルオブジェクトが存在しないようなケースでも柔軟にActorベースのフラグ制御を行うことができます。

生成AIを活用したフラグ削除

フィーチャーフラグは、機能のリリースサイクルを柔軟にする強力なツールですが、役目を終えたフラグは適切に削除していく必要があります。フラグが増えすぎると、コードの複雑性が増し、管理コストも増大するためです。

従来、フラグの参照箇所の削除は以下の手順で行っていました。

  1. コードベース全体から、削除対象フラグの参照箇所を検索する。
  2. 特定された参照箇所を一つ一つ手動で修正・削除する。

このプロセスは、特に view 等の分岐が複雑な場合、時間と手間がかかり、見落としのリスクも伴いました。

そこで、現在はフラグの参照箇所の削除に生成AIを使用しています。プロンプトの例を以下に示します。

Feature::{フラグ名}は常に{true or false}なフラグとなりました。
上記を参照しているすべての条件分岐を削除してください。
features.ymlから対象のフラグの削除はしないでください。
features.ymlの該当のフラグに「TODO: 参照箇所削除済み、削除予定」というコメントを追加してください。
参考:
 - kind: allow_access_to_new_sugoi_feature # TODO: 参照箇所削除済み、削除予定
 
その後 features.ymlをコミットしてください
コミットメッセージ: Git履歴を残すために削除予定のコメント追加

AIを活用すると、手動と比較して、以下のようなメリットが見込めます。

  • 参照箇所の自動特定: AIがコードを解析し、削除対象のフィーチャーフラグが使用されている箇所を迅速に特定します。
  • 修正コードの提案: 特定された箇所に対して、AIが適切な修正案(フラグ参照の削除や、条件分岐の恒久化など)を提案してくれる場合があります。
  • 作業時間の短縮とミスの削減: 手作業による検索や修正と比較して、作業時間を大幅に短縮し、ヒューマンエラーによる見落としや修正ミスを減らすことができます。

最終的なコードの確認とテストは開発者自身が行う必要がありますが、AIツールを補助として利用することで、フィーチャーフラグのライフサイクル管理をよりスムーズかつ安全に行えるようになると考えています。

おわりに

本記事では、ClinPeerにおけるフィーチャーフラグの実装と運用方法について紹介しました。 Flipperという強力な基盤ライブラリを利用しつつ、Feature というActiveHashモデルでラップすることにより、アプリケーション固有の事情や、より使いやすいインターフェースを開発チームに提供しています。

このように、フィーチャーフラグシステムを適切に抽象化(ラップ)することには、多くのメリットがあります。

  • 管理の容易化: フラグの定義を一元化 (features.yml) し、環境ごとの挙動を明確にすることで、管理コストを低減します。
  • 利用の簡便化: Feature::KIND_NAME.enabled? のような直感的で統一されたインターフェースを提供することで、開発者が迷うことなくフラグを利用できます。
  • 将来的な拡張性: 例えば、将来的に別のフィーチャーフラグ管理システムに移行する場合でも、Feature クラス内部の実装を変更するだけで済み、アプリケーションコードへの影響を最小限に抑えることができます。
  • 独自のロジックの追加: StringFlipperActor のように、特定のニーズに合わせた独自のロジックを組み込みやすくなります。

フィーチャーフラグは、アジャイルな開発、安全な機能リリース、そしてA/Bテストなど、現代的なソフトウェア開発において非常に有効なプラクティスです。 ClinPeerでは、このような仕組みを活用し、ユーザーにより良い価値を迅速に届けられるよう、日々改善を続けています。

この記事が、フィーチャーフラグの導入や運用を検討されている方の一助となれば幸いです。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

GitHub Copilot を味方につける:AI に渡すコンテキスト整備の工夫

こんにちは。事業本部開発部 MISP グループのフロントエンドエンジニアの小林和弘(@kzhrk0430)です。

メドピアでは「AI ファーストカンパニー」を目指すことを全社で掲げています。実際に社内では、AI ツールを活用して業務を効率化する動きが活発に行われています。たとえば、Gemini を使って Google Meet の文字起こしや議事メモを作成したり、Notion AI で要件定義とテストケースの整合性を確認したり、スライド作成を AI に任せたりと、日々の業務に AI を積極的に取り入れています。

今回は、開発環境をより快適にするために GitHub Copilot(VS Code 拡張)を活用した取り組みをご紹介します。

AI に渡すコンテキストを整備する

Copilot は多様な機能を提供していますが、それぞれ異なる種類のコンテキストを参照している印象があります。そのため、意図通りに動作させるには、各機能ごとに適切なコンテキストを整備することが重要です。

今回は、以下の 3 点にフォーカスして取り組みました:

  • VS Code におけるコード生成
  • GitHub におけるコードレビュー支援
  • VS Code におけるコミットメッセージ自動生成

VS Code におけるコード生成

まず、コード生成機能に関しては、.github/copilot-instructions.md というファイルをプロジェクトに追加しました。このファイルには、プロジェクト特有の文脈や設計方針、命名規則などを記載しています。

作成時には、まず Copilot に初稿を書かせ、その内容を人間がレビュー・修正してブラッシュアップするという流れを取りました。なお、社内では AI ツールの Cline も利用しているため Cline 用にコンテキストを渡す.clinerules ファイルには .github/copilot-instructions.md を参照させるよう設定し、Cline 経由でも文脈が共有されるようにしています。

.clinerules の中身はこの用になっています。

# やくばとシステム開発ガイドライン

## 注意事項

このファイルは参照用として保持されていますが、最新かつ詳細な開発ガイドラインは `.github/copilot-instructions.md` に移動しました。
開発作業を行う際は、`.github/copilot-instructions.md` を参照してください。

GitHub Copilot をはじめとする開発ツールは、`.github/copilot-instructions.md` を参照してコード提案やガイドラインの適用を行います。

## リンク

開発ガイドラインの詳細は [.github/copilot-instructions.md](.github/copilot-instructions.md) を参照してください。

当初は .clinerules.github/copilot-instructions.md と同様に Cline に生成させていたのですが、コンテキストが二重管理になっていたため Copilot Agent に 2 つのファイルを統合させて .clinerules の中身を書き換えています。

GitHub におけるコードレビュー支援

GitHub の Pull Request テンプレートにも Copilot 活用の工夫を加えました。具体的には、Copilot のレビューがわかりやすくなるように、PR テンプレート内にレビューのコンテキストとなる情報を明記するようにしています。

今はまだレビューを日本語で書かせて、レビューコメントの表示がバグる HTML タグのコメントのルールを書いているだけですが、今後コンテキストを増やしてレビュー支援の質を高めて、より有用なフィードバックが得られるようにしたいと考えています。

<details>
<summary>このブロックは Copilot レビューのためのコンテキストです。Copilot は下記の命令を守ってください。</summary>

- レビューコメントは日本語で行う
- レビューコメントの HTML タグはマークダウンの Code spans (`) でラップする

</details>

GitHub 側でコンテキストを設定する機能は用意しているようですが、Copilot Enterprise プランでのみ利用でき、現在は一部のユーザーしか利用できない状態なので、今回の PR の説明文にコンテキストを注入する方法は一時的なハックになっています。

VS Code におけるコミットメッセージ自動生成

Copilot Chat 拡張機能のひとつに、コミットメッセージを自動生成してくれる機能があります。これに対しても、プロジェクトの文脈を反映させる設定を行いました。

具体的には、以下のように VS Code の settings.json.github/copilot-instructions.md を指定しています:

{
  "github.copilot.chat.commitMessageGeneration.instructions": [
    {
      "file": ".github/copilot-instructions.md"
    }
  ]
}

.github/copilot-instructions.md にはコミットメッセージにおけるルールを下記のように記載しています。

## 提案すべきコミットメッセージ

- コミットメッセージは日本語で書く
- git log で参照した過去のコミットメッセージを参考にする
- Conventional Commit を基本とする
- 1 行目のコミットの下には空白の行間を設ける
- 複数行の詳細なコミットメッセージを書く
  - 詳細なコミットメッセージは `## 背景``## 修正内容` などのマークダウンの見出しをつける

この設定により、コミットメッセージの生成時にもプロジェクト固有の背景が反映されるようになり、精度の高い出力が得られるようになりました。

Copilot で生成したコミットメッセージの一例

簡単なコミットメッセージであれば Copilot でコミットメッセージをジェネレートさせてさっとレビューするだけでコミットを作成しています。

VS Code における Copilot 設定の展望

VS Code の Copilot の instructions の設定は Experimental で提供されていますが、下記の 5 つの機能にコンテキスト設定ができるようになっています。

  • Review Selection: Instructions
  • Code Generation: Instructions
  • Commit Message Generation: Instructions
  • Pull Request Description Generation: Instructions
  • Test Generation: Instructions

VS Code の Copilot の設定画面のキャプチャー

GitHub 側も、.github/copilot-instructions.md にすべてのユースケースの情報を統合することが難しいと判断したのか、将来的には用途ごとにマークダウンファイルを分けることを検討しているのかもしれません。

AI に渡すコンテキスト整備の重要性

最近公開された Devin Wiki は、AI のコード理解能力を強く印象づけるものでした。

メドピアでも Devin AI を導入しており、社内で Devin Wiki の生成結果を確認する機会がありました。その中で、医療機関向けおよび薬局向けの Nuxt アプリを管理しているモノレポ構成のリポジトリに対して、Devin が自動的に、医療機関・薬局・患者間の処方せんの流れを示すフローチャートを生成していたのを目にし、大きな驚きがありました。

ただし、すべての機能について完璧に Wiki 化されているわけではなく、ハルシネーション(誤生成)が起きそうな領域については、あえてページを生成しないようにしているケースも見受けられました。

このように、AI ツールの進化によって、今後さらに多くの業務が AI によって支援・代替されるようになると感じています。これはエンジニアの仕事を奪うということではなく、むしろ課題発見力や判断力といった本質的な能力に集中できる環境が整っていく、というポジティブな変化だと考えています。

この点については、VPoE の保立さんも以下のインタビュー記事で言及しています: style.medpeer.co.jp

現時点では、AI がどこからどのようにコンテキストを取得しているのかを意識し、AI が誤解しないようなデータセット(明確な変数名や整理されたコード構造など)を整備することが、エンジニアに求められていると強く感じます。

おまけ:AI 活用と執筆の裏側

「AI に渡すコンテキストを整備する」セクションは、まず箇条書きで要点を整理し、それを ChatGPT に文章化してもらったうえで、内容を加筆・修正して仕上げました。

その他のセクションは、最初に自分で文章を書き、その後 ChatGPT にレビューを依頼して改善点を洗い出しました。

また、OGP 画像も記事をレビューさせたついでに ChatGPT に出力させています。

このように、試せるところから積極的に AI を活用し、自分なりに現在地を確認していくことが、AI 時代を前向きに生きる上で大切な姿勢だと考えています。

今後も、実務に即した形での AI 活用について、実験と発信を続けていきたいと思います。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

#RubyKaigi 2025 セッションレポート

皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。

RubyKaigi 2025に参加されていた皆さん、お疲れ様でした。

今回、内藤がRubyKaigi 本編に登壇しました。 発表内容の詳細は以下の記事にまとめておりますのでよろしければご覧ください。

naitoh.hatenablog.com

セッションレポート

RubyKaigi のセッションの中で特に印象に残ったセッションをご紹介します。 タイムテーブルは下記から確認ください。

rubykaigi.org

Make Parsers Compatible Using Automata Learning

rubykaigi.org

オートマトン理論は理解していなかったのですが、「オートマトンと正規表現は相互に変換できる」とのことなので、正規表現が数学的にオートマトンで表現できるのは美しく非常に良いですね!

RubyKaigi に来るたびに、自分の知らないコンピュータサイエンスの知識を思い知らされます。

聞きたいセッションがあれば、タイムテーブルに書かれている説明を予習しておくとセッションをもっと楽しめるんですよね。 続きものであれば、同じ人の昨年以前のセッション動画を見ておくのもお勧めです。

Goodbye fat gem 2025

rubykaigi.org

様々な gem をメンテナンスされている須藤さんの公演で、fat gem*1 の辛みを面白おかしく共感を呼ぶ形でお話されるトークでした。 自分のトークもこのように、会場の反応を楽しみながらできれば良いのですが、なかなかハードルが高いです。

公演の内容は、fat gem はユーザー視点だと利用するだけなら楽で良いけど、これって実は開発者側に多大なコストがかかっているので、持続可能性の意味で厳しいんですよね。 例えば nokogiri gem の場合、Ruby 3.4 がリリースされたその日に、対応する11プラットフォーム(内部的にサポート対象の Ruby のバージョンは4つ)が用意されています。ユーザーとしては非常にありがたいのですが、開発者目線で見ると nokogiri は頑張りすぎだと思います。 自分の gem でこんな事求められたら無理ですと断るレベル。

なので、須藤さんの提案は、

  1. C拡張 gem でもユーザー自身にビルドしてもらいましょう
  2. ビルド環境を用意するのが手間(よくビルドエラーになる)なので、ビルド環境を自動で準備できれば良さそう
  3. Windows 環境には RubyInstaller2 という先駆者がいる
    • Devkit というビルド環境がセット
    • パッケージマネージャもセット
    • 依存パッケージを自動インストール
    • 依存パッケージが存在せずインストールに失敗するということはない
  4. インストール時に自動で外部依存もインストールする rubygems-requirements-system gem を用意

このように、ユーザー自身でビルドする世界になれば持続可能性が高まり、みんなハッピーになるだろうということです。

ユーザーの環境で毎回ビルドのコストがかかる点と、ユーザーの環境に依存パッケージをインストールする必要がある点がデメリットですが、前者は許容できるコストで、後者はローカル環境を汚染したくない場合、Docker 上で実施するのが良いのではないでしょうか。(この gem がもし主流になれば、Dockerfile に依存パッケージ名を記載する手間がなくなる可能性もあるかもしれません。)

RuboCop: Modularity and AST Insights

rubykaigi.org

精力的に開発が続いている RuboCop のモジュール性のお話です。 これまで RuboCop は公式のプラグイン API を提供していなかったため、inject と呼ばれるモンキーパッチを利用する形で実現されており、それがデファクトスタンダードだったそうです。末恐ろしい状況ですね。

体系的なプラグインシステムが提供されるとユーザーとしても安心して使えるし、開発者としても貢献しやすくなりますよね!

また、RuboCop のバックエンドパーサーとして機能してきた Parser gem の代わりに、今後は Prism が採用されるとのことで、Ruby エコシステムの世代交代が進んでいますね。 Ruby の最新の機能が実用段階に来ているということで、どんどん最新版を使っていきましょう!

おわりに

3日間にわたる RubyKaigi 2025が終了しました。 非常に魅力的な公演が目白押しでしたが、3トラックのため、視聴できるセッションが限られていた点と、自身の発表の裏番組だった ZJIT を聞けなかったのが残念です。

次回のRubyKaigiは 2026年4月22日から4月24日、場所は北海道函館市です。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

*1:事前にビルド(コンパイル)された C拡張バイナリを同梱した gem。新しい Ruby が出るとその Ruby のバージョンにあわせた対応バイナリが必要。

ScrollableTabRowでスクロール状態を監視する

はじめに

こんにちは!メドピアにてモバイルアプリエンジニアをしている佐藤です。
今回の「ClinPeerアプリ開発の裏側連載記事」では、ScrollableTabRowでのスクロール状態の監視方法について解説していきたいと思います。 tech.medpeer.co.jp

背景

ClinPeerアプリでは、ユーザーが関心のあるカテゴリーをタブとして動的に追加できるようにしており、追加したカテゴリーを上部のタブで表示しています。その際、右側にまだ表示しきれていないタブがあることをユーザーに伝えるため、右端にスクロール可能な場合はアイコンを表示しています。

左:スクロールできるときは赤枠内のアイコンを表示
右:スクロールできない場合はアイコンを非表示

XMLレイアウトのTabLayout + ViewPager2であればaddOnScrollChangedListenerで監視出来るので、Jetpack Composeでも rememberScrollState などを使えば簡単に実装できると思っていましたが、実際には少しハマりどころがありました。
そのため、今回はその解決方法を共有します。

結論

PrimaryScrollableTabRow を使うことで解決できます!
androidx.compose.material3:material3-*:1.2.0-alpha09」のリリースでスクロール状態が公開されるようになりましたので、本バージョン以降のPrimaryScrollableTabRowを用いれば簡単に監視が出来るようになります。

PrimaryScrollableTabRow は執筆時点ではまだ試験運用版で @OptIn(ExperimentalMaterial3Api::class) のアノテーション付与が必要です。
将来的に仕様が変更される可能性もあるため、バージョンアップ時の挙動に注意しましょう。

PrimaryScrollableTabRowについて

ScrollableTabRowとPrimaryScrollableTabRowの定義を比較してみます。(定義のandroidx.compose.material3:material3のバージョンは1.3.1のものです)

@Composable
fun ScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    containerColor: Color = TabRowDefaults.primaryContainerColor,
    contentColor: Color = TabRowDefaults.primaryContentColor,
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding,
    indicator: @Composable (tabPositions: List<TabPosition>) -> Unit =
        @Composable { tabPositions ->
            TabRowDefaults.SecondaryIndicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable () -> Unit = @Composable { HorizontalDivider() },
    tabs: @Composable () -> Unit
)
@ExperimentalMaterial3Api
@Composable
fun PrimaryScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    scrollState: ScrollState = rememberScrollState(),
    containerColor: Color = TabRowDefaults.primaryContainerColor,
    contentColor: Color = TabRowDefaults.primaryContentColor,
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding,
    indicator: @Composable TabIndicatorScope.() -> Unit =
        @Composable {
            TabRowDefaults.PrimaryIndicator(
                Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = true),
                width = Dp.Unspecified,
            )
        },
    divider: @Composable () -> Unit = @Composable { HorizontalDivider() },
    tabs: @Composable () -> Unit
)

ScrollableTabRowではscrollStateを指定することは出来ませんが、PrimaryScrollableTabRowではscrollStateのパラメーターが追加されているので、scrollStateにrememberScrollStateを指定してスクロール状態を監視出来るようにします。

解説

これらの内容を踏まえた上で実装例を書きます。
まず、スクロール状態の監視が行えないScrollableTabRowのコードが下記の通りです。

Scaffold(
    modifier = Modifier.fillMaxSize(),
    topBar = {
        CenterAlignedTopAppBar(
            title = {
                Text(
                    "ScrollableTabRowExample",
                    fontSize = 18.sp,
                    textAlign = TextAlign.Center
                )
            }
        )
    },
    content = { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
             val items = listOf(
                "Tab 1",
                "Tab 2",
                "Tab 3",
                "Tab 4",
                "Tab 5",
                "Tab 6",
                "Tab 7",
                "Tab 8",
                "Tab 9",
                "Tab 10"
            )
            val pagerState =
                rememberPagerState(pageCount = { items.size })
            val scope = rememberCoroutineScope()

            ScrollableTabRow(
                selectedTabIndex = pagerState.currentPage,
                edgePadding = 0.dp
            ) {
                items.forEachIndexed { index, tab ->
                    Tab(
                        selected = pagerState.currentPage == index,
                        onClick = {
                            scope.launch {
                                pagerState.animateScrollToPage(index)
                            }
                        },
                        modifier = Modifier.height(48.dp)
                    ) {
                        Text(tab)
                    }
                }
            }
            HorizontalPager(
                state = pagerState
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "page: ${items[it]}"
                    )
                }
            }
        }
    }
)

スクロール状態の監視を行なっていない画面

右側にスクロールが出来る状態である場合にアイコンを表示するといったことをしたい場合、以下の通り変更します。

Scaffold(
    modifier = Modifier.fillMaxSize(),
    topBar = {
        CenterAlignedTopAppBar(
            title = {
                Text(
                    "PrimaryScrollableTabRowExample",
                    fontSize = 18.sp,
                    textAlign = TextAlign.Center
                )
            }
        )
    },
    content = { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
             val items = listOf(
                "Tab 1",
                "Tab 2",
                "Tab 3",
                "Tab 4",
                "Tab 5",
                "Tab 6",
                "Tab 7",
                "Tab 8",
                "Tab 9",
                "Tab 10"
            )
            val pagerState =
                rememberPagerState(pageCount = { items.size })
            val scope = rememberCoroutineScope()
            val scrollState = rememberScrollState()

            // スクロール可能なアイコンの表示状態を管理するフラグを追加する
            var showArrow by remember { mutableStateOf(false) }
            LaunchedEffect(scrollState.maxValue, scrollState.value) {
                // 右側にスクロール可能な状態の場合はshowArrowをtrueにする
                showArrow = scrollState.value < scrollState.maxValue
            }

            Box {
                PrimaryScrollableTabRow(
                    selectedTabIndex = pagerState.currentPage,
                    edgePadding = 0.dp,
                    scrollState = scrollState
                ) {
                    items.forEachIndexed { index, tab ->
                        Tab(
                            selected = pagerState.currentPage == index,
                            onClick = {
                                scope.launch {
                                    pagerState.animateScrollToPage(index)
                                }
                            },
                            modifier = Modifier.height(48.dp)
                        ) {
                            Text(tab)
                        }
                    }
                }
                if (showArrow) {
                    // 右側にスクロール可能な状態の場合は右端にアイコンを表示する
                    Box(
                        modifier = Modifier
                            .width(44.dp)
                            .height(48.dp)
                            .background(
                                Brush.linearGradient(
                                    colors = listOf(
                                        Color.White.copy(0f),
                                        Color.White.copy(1f)
                                    )
                                )
                            )
                            .align(Alignment.CenterEnd)
                    ) {
                        Image(
                            imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
                            contentDescription = null,
                            modifier = Modifier.align(Alignment.Center)
                        )
                    }
                }
            }
            HorizontalPager(
                state = pagerState
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        "page: ${items[it]}"
                    )
                }
            }
        }
    }
)

スクロール状態を監視して右側にスクロール出来る時はアイコンを表示

最後に

PrimaryScrollableTabRowに関する記事がほとんどなかった為、この場を借りて紹介させていただきました。
PrimaryScrollableTabRowを使うことで、スクロール状態を監視してユーザー体験を向上させるような細かなUI調整も可能になります。
本記事が同じような課題で悩んでいる方や今後同じような機能を実装する方の助けになれば幸いです!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp

RubyKaigi 2025 に@naitoh が登壇します #rubykaigi

皆様こんにちは、メドピアのサーバーサイドエンジニアの内藤(@naitoh)です。

この度、2025/04/16(水)-18(金)の3日間で開催される「RubyKaigi 2025」に登壇させていただくこととなりました! タイトルは「Improvement of REXML and speed up using StringScanner」となります。

rubykaigi.org

スケジュールは、 Day2 11:50 〜 12:20 / Pearls Room を予定しています。 ぜひセッションにお越しください。

登壇内容について

セッションでは以下の内容をお伝えします。

REXML is a standard XML library (Bundled Gem) for Ruby implemented in Pure Ruby. It is up to 40% faster between rexml 3.2.6 gem attached to Ruby 3.3.0 and rexml 3.4.0 gem attached to Ruby 3.4.0. Through our REXML speedup efforts using StringScanner, I will explain why using StringScanner is faster and how it can be implemented to make it faster.

昨年の RubyKaigi 2024 のLT でお話した話

の続きで、 REXML のXMLパース処理を StringScanner を使ってREXML 3.2.6 (Ruby 3.3.0 添付のバージョン) からREXML 3.4.0 (Ruby 3.4.0 添付のバージョン) の間で約4割速くしたので、StringScanner を使うと何故パース処理が速くなるのか、どのような点に気をつけてパース処理を書けば速くなるのかなど、高速化のポイントを皆様に紹介します。

おわりに

それでは皆様、当日お会いできることを楽しみにしております!


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp