複雑な歯科電子レセプトを可視化するWebビューアを作ってみた

こちらの記事は「MEDLEY Summer Tech Blog Relay」の18日目の記事です🎉

今週はFY25新卒エンジニアの4人が連続して記事をお届けします!

サムネイル:複雑な歯科電子レセプトを可視化するWebビューアを作ってみた

はじめに

株式会社メドレーに 2025 年 4 月に新卒入社した、平林です。 医療プラットフォームの歯科診療所事業部でソフトウェアエンジニアをしており、クラウド歯科業務支援システム「Dentis」を開発しています。

皆さんは病院や歯科医院で診察を受けた後、窓口で料金を支払っていると思います。ご存じの方も多いかもしれませんが、ここで払う料金は医療機関の収入の一部に過ぎません。

残りの診療報酬は患者が加入している保険組合(保険者)から支払われることになっており、医療機関は毎月患者に行った診療をまとめた書類(診療報酬請求書、通称レセプト)を作成して支払基金(国保の場合は国民健康保険団体連合会)に請求します。 以下は、社会保険に加入している患者の診療報酬請求の流れです。

日本の医療保険制度における、患者、医療機関、保険者、支払基金の関係を図解したイラスト。中心には患者(被保険者)がおり、医療費の一部を医療機関へ、保険料を保険者へ支払う。医療機関は診療報酬を支払基金に請求し、支払基金は医療機関へ支払い、保険者へも請求を行う。複雑な医療費の流れが示されている。

このレセプトをコンピューターを使って自動で作成してくれるのが、レセプトコンピューター(レセコン)です。

今回は、このレセプトについて学習するために、レセプトビューアを作ってみました。

UKE Readerのスクリーンショット。画面中央にUKEがグリッド形式で表示されている。画面上部に選択項目の説明、サイドには選択項目のプロパティが表示されている。

ソースコードは以下で公開しています。

電子レセプトについて

レセプトには、紙レセプトと電子レセプトの2種類があります。元々は紙レセプトのみでしたが、現在はそれをデータ化した電子レセプトが主流となっています。

電子レセプトはCSVライクなカンマ区切りのフォーマットとなっており、UKEという拡張子で保存されます。

以下は、一般的な電子レセプトの例です。※データは架空の情報です

UK,2,13,3,1234567,,メドレークリニック,202201,0117,00
IR,2,13,3,1234567,,202201,03-1234-5678,0117
RE,4,3318,202112,歯科 太郎,1,19400808,,,20210609,1,,,29,,123456/8,,,,,,,,,,シカタロウ,
HO,39141320,,23572449,2,1279,,,,,,,,
JD,1,,,,,,,,1,,,,,,,,,,,,,,1,,,,,,,,,
MF,00,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
HS,,,101700101500101400101300101200101100102100102200102300102400102500102600102700104800104500104400104300104200104100103100103200103300103400103500103700,5234009,,,,,,,,
SS,12,1,301001610,,,CA062,,CA045,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,57,2,,,,,,,,1,,,,,,,,,,,,,,1,,,,,,,,,
(...以下省略)
GO,116,155332,99

一見よくわからないデータですが、社会保険診療報酬支払基金のWebページには「オンライン又は光ディスク等による 請求に係る記録条件仕様(歯科用)」(以下、仕様書)が公開されているので、それを見ながら読み進めていきます。

UKEから抜き出した行。UK,2,13,3,1234567,,メドレークリニック,202201,0117,00

1列目には「UK」と書いてあります。これはレコード識別情報です。

レコード識別情報の一覧は、以下のとおりです。

レコードの種類データの一例
UK受付情報レコード審査支払機関・医療機関コード・都道府県など
IR医療機関情報レコード医療機関コード・連絡先・請求年月など
REレセプト共通レコード患者(被保険者)の名前・性別など
HO保険者レコード保険者番号・合計点数など
KO公費レコード負担者番号公費負担者番号など
SN資格確認レコード負担者種別や受給者番号など
JD受診日等レコード日別の受診等区分など
MF窓口負担額レコード高額療養費の扱いなど
HS傷病名部位レコード傷病名コード・部位の歯式コードなど
SS歯科診療行為レコード診療識別・歯科診療行為コードなど
SI診療行為レコード医科診療行為コードなど
IY医薬品レコード診療で使用した医薬品・院内で処方した医薬品など
TO特定器材レコード特定器材コードなど
COコメントレコード診療識別・コメントコードなど
SJ症状詳記レコード症状詳記(診療内容が明確でない場合などに記録する)など
GO診療報酬請求書レコード総件数など

この値によってこの行がどんな情報を含んでいるのか判別することができます。

今回はUK(受付情報レコード)だけ見てみましょう。仕様書と照らし合わせると、以下の内容であることが分かります。

説明備考
1審査支払機関1: 社会保険診療報酬支払基金、2: 国民健康保険団体連合会
13都道府県コード総務省が定める都道府県コード(URL) 16: 富山県
3点数表種別点数表種別 3: 歯科 他にも、1: 医科, 4: 調剤など
1234567医療機関コード医療機関コード
(空欄)予備エリア
メドレークリニック地方厚生(支)局長に届け出た名称20文字を超える場合は、決まっている省略名称になる
202201請求年月レセプトが請求される年月。例: 2022年1月
0117施設基準届出コード2文字区切りで解釈される。01:補管(クラウン・ブリッジ維持管理料)17:歯科初診料
00マルチボリューム識別情報フロッピーディスクなど容量の少ない媒体を使用する際、レセプト情報が複数のファイルに分割されている場合に、そのファイルの順番を示す

このような形で各行を読んでいくと、電子レセプトが示している内容が分かります。

レセプトの読み方がなんとなく分かったので、次は実際にこれを読み込むプログラムを作ります。

CSVを読み込んでパースする

電子レセプトはShift-JISで記録されているので、文字コードを指定して読み込みます。

const loadFile = (file) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const text = e.target?.result;
      if (typeof text !== "string") {
        reject(new Error("Failed to load the file"));
        return;
      }
      resolve(text);
    };
    reader.onerror = () => {
      reject(new Error("Failed to read the file"));
    };
    reader.readAsText(file, "shift-jis");
  });

読み込んだあとは、UKEデータをパースします。

CSVのパースは考慮することが多くエンジニア泣かせで有名ですが、UKEの仕様は非常にシンプルなので簡単にパースできます。

(以下は、一部抜粋です)

符号名称図形記号用途
コンマ,項目の区切りを表現する
引用符"使用しない
改行コード\nレコードの区切りを表現する
const csv = await loadFile(file);
const receipts = csv
  .trim()
  .split("\n")
  .map((line) => line.split(","));

項目の説明を表示する

行ごとのレコードと列番号を渡すと説明が返ってくるロジックを作ります。

説明情報は、社会保険診療報酬支払基金のWebページに公開されている「電子レセプトの作成手引き」を参考にします。

「電子レセプトの作成手引き」はPDFで公開されているデータですが、 markitdown を使ってマークダウンに変換し作業スペースに置くことで、CursorやClaude Codeといった開発支援ツールを活用して効率的に実装を進めることができました。

export type RecordType = {
  index: number;
  data: string[];
};
export const explain = (record: RecordType) => {
  switch (record.identification) {
    case "UK":
      return explainUK(record);
    case "IR":
      return explainIR(record);
    // ...省略
    default:
      return null;
  }
};

値によって説明が変わる内容もあるので、動的に説明文を作れるようにしました。

const explainUK: (((record: RecordType) => string) | string)[] = [
  "この行が受付情報レコードであることを示します",
  (record) =>
    `審査支払機関を示します。値「${record.data[record.index]}」は「${getShiharaiKikan(record.data[record.index])}」です`,
  // ...省略
];

マスターファイルから情報を取得する

UKEの中にはそのまま解釈できる内容だけでなく、診療行為コードや医薬品コードのような別のデータ(マスターファイル)を参照しないと内容がわからないものがあります。

マスターファイルや仕様は、厚生労働省がWebで公開してくれています。

マスターファイルを表計算ソフトで開いたときのスクリーンショット。

マスターファイルは電子レセプトと仕様の違うCSVで、以下のような仕様になっています。

  1. 項目間の区切り文字は「,」(カンマ)とする。
  2. 各項目の値は、モード(「数字」、「英数」及び「漢字」)に関わらず、引用符「”」(ダブルクォーテーション)を前後に付す。
  3. 最大バイトは引用符「“」を除いたバイト数であり、小数部がある値は、小数点及び小数以降の数字を含む。
  4. 0バイトの文字列(Null)の場合は、引用符「“」を続けて記録する。

値をダブルクォーテーションで囲むとのことなので、値の中のカンマを許容する仕様である可能性があります。愚直に1文字ずつ処理します。

// const csv = 生のCSVデータ;
const lines = csv.split("\n");
const records: Record<string, string>[] = [];
const shobyomeiMasterHeaders = ["変更区分", "マスター種別", "傷病名コード", /** 以下省略... */];
for (const line of lines) {
  const record: Record<string, string> = {};
  let charIndex = 0;
  let columnIndex = 0;
  let inQuotes = false;
  let buffer = "";
  while (charIndex < line.length) {
    const char = line[charIndex];
    if (char === '"') {
      inQuotes = !inQuotes;
    } else if (char === ',' && !inQuotes) {
      // カンマ区切り、かつ引用符の外
      record[shobyomeiMasterHeaders[columnIndex]] = buffer;
      columnIndex++;
      buffer = "";
    } else {
      // それ以外はバッファに追加
      buffer += char;
    }
    charIndex++;
  }
  if (buffer.length > 0) {
    record[shobyomeiMasterHeaders[columnIndex]] = buffer;
  }
  // データベースへの保存
  await saveToDatabase("shobyomei", record.傷病名コード, [record]);
}

マスターファイルは全部で20MB程度あり、毎回処理していたら時間がかかってしまうので、indexedDBに展開しました。

ブラウザのデベロッパーツールで、IndexedDBのshobyomeiMasterというオブジェクトストアが選択され、その中のデータが表示されている。

これで、診療行為コードや医薬品コードを基にマスターの内容を参照できるようになりました。

ここで一つ気をつけなければいけないのは、医薬品コードの情報には有効期限があるということです。

該当するレコードの診療日が、変更年月日〜廃止年月日の間であることを確認する必要があります。例えば2015年のレセプトから情報を得たいのであれば、2015年時点のマスターファイルを参照する必要があります。

そのため、時系列に合った複数バージョンのマスターを保持できる設計にするために、キーに対してデータは配列で持つようにしました。

表としてレンダリングする

UKEのパーサーと情報の取得処理が書けたので、表としてレンダリングします。

type CellPosition = { x: number; y: number };
const UKERenderer = ({ uke }: { uke: string[][] }) => {
  const [activeCell, setActiveCell] = useState<CellPosition | null>(null);
  const handleClickCell = (x: number, y: number) => setActiveCell({ x, y });
  const isActiveCell = (x: number, y: number) =>
    activeCell && activeCell.x === x && activeCell.y === y;
  return uke.map((row, y) => (
    <Row key={`row-${y}`}>
      {row.map((value, x) => (
        <Cell
          key={`cell-${x}`}
          onClick={() => handleClickCell(x, y)}
          active={isActiveCell(x, y)}
          description={explain({ index: x, data: row })}
        >
          {value}
        </Cell>
      ))}
    </Row>
  ));
};

実際に開発したもの

実際に開発したものは、今まで解説したコードをベースに機能や処理を付け加えています。

UKE Readerを操作する様子が収録されているGIFアニメーション。

開発してみて

以前は、テキストエディターを使って目視でマスターファイルの内容を照らし合わせており確認作業が大変でしたが、今回作った電子レセプトビューアによって、正しくレセプトが出力されているかの確認が行いやすくなりました。

また、自分自身も電子レセプトの仕様を学んだことで、業務で行っていることが少しずつ分かるようになってきました!

おわりに

今回は、歯科の電子レセプトを理解するために、電子レセプトビューアを開発しました。

私たちソフトウェアエンジニアは、普段医療に関するシステムの話をあまり目にすることは少ないですが、意外と仕様やデータが一般にも公開されており、手軽に触れてみることができます。

この記事を読んで、医療ITに興味を持ってくださる方がいれば幸いです!


次は、新卒エンジニア4人目の池田さんです!お楽しみに!!