[{"data":1,"prerenderedAt":556},["ShallowReactive",2],{"content-/mf-extension-export-import-enhancements":3,"all-pages-for-dir":554,"og-image-/mf-extension-export-import-enhancements":555},{"id":4,"title":5,"body":6,"category":534,"description":535,"extension":536,"meta":537,"navigation":538,"ogImage":539,"path":540,"project_name":541,"published":542,"publishedAt":543,"seo":544,"stem":545,"tags":546,"todo":552,"unpublished":542,"updatedAt":539,"__hash__":553},"pages/2026-03/2026-03-26/mf-extension-export-import-enhancements.md","Chrome拡張クラウド会計連携 - エクスポート整合性検証と一括インポート機能の段階的構築",{"type":7,"value":8,"toc":516},"minimark",[9,14,18,21,25,30,33,63,346,350,353,355,358,361,367,369,372,376,379,382,408,411,414,416,420,424,427,431,434,438,446,449,478,482,489,491,495,498,501,503,506,509,512],[10,11,13],"h1",{"id":12},"chrome拡張クラウド会計連携-エクスポート整合性検証と一括インポート機能","Chrome拡張クラウド会計連携 - エクスポート整合性検証と一括インポート機能",[15,16,17],"p",{},"エクスポートデータに検証関数を3つ追加してテストを162から200に増やした後、一括インポート機能を一から組み上げ、UIを2回作り直した。朝はバリデーションのテストを黙々と書き、昼にインポートの骨格を通し、夕方からUIの設計方針を変えて「使い捨てURL方式」に着地するまでの記録。",[19,20],"hr",{},[22,23,24],"h2",{"id":24},"エクスポートデータの整合性検証",[26,27,29],"h3",{"id":28},"_3つの検証関数を-libjs-に追加","3つの検証関数を lib.js に追加",[15,31,32],{},"エクスポート処理は動いていたが、壊れたデータがそのままスプレッドシートに流れ込むリスクがあった。行単位→ブロック単位→全体の3段階でバリデーションを入れた。",[34,35,36,47,55],"ul",{},[37,38,39,46],"li",{},[40,41,42],"strong",{},[43,44,45],"code",{},"validateJournalRow",": 仕訳1行の必須フィールド存在チェックと型チェック。借方・貸方金額が数値であること、日付フォーマットが正しいことを検証",[37,48,49,54],{},[40,50,51],{},[43,52,53],{},"validateJournalBlock",": 仕訳ブロック（1取引を構成する複数行）の貸借一致を検証。借方合計と貸方合計が一致しなければエラー",[37,56,57,62],{},[40,58,59],{},[43,60,61],{},"validateExportData",": エクスポート対象の全データを走査し、上記2つを順に呼び出す。エラーがあれば行番号と理由を配列で返す",[64,65,70],"pre",{"className":66,"code":67,"language":68,"meta":69,"style":69},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// 貸借一致の検証（核心部分）\nconst validateJournalBlock = (rows) => {\n  const debitSum = rows.reduce((s, r) => s + (r.debit || 0), 0);\n  const creditSum = rows.reduce((s, r) => s + (r.credit || 0), 0);\n  return Math.abs(debitSum - creditSum) \u003C 0.01\n    ? { valid: true }\n    : { valid: false, reason: `貸借不一致: 借方${debitSum} / 貸方${creditSum}` };\n};\n","javascript","",[43,71,72,81,112,179,230,264,286,340],{"__ignoreMap":69},[73,74,77],"span",{"class":75,"line":76},"line",1,[73,78,80],{"class":79},"sxvE3","// 貸借一致の検証（核心部分）\n",[73,82,84,88,92,96,99,103,106,109],{"class":75,"line":83},2,[73,85,87],{"class":86},"stQ0i","const",[73,89,91],{"class":90},"senZ8"," validateJournalBlock",[73,93,95],{"class":94},"shFtX"," =",[73,97,98],{"class":94}," (",[73,100,102],{"class":101},"s4oTP","rows",[73,104,105],{"class":94},")",[73,107,108],{"class":94}," =>",[73,110,111],{"class":94}," {\n",[73,113,115,118,121,123,126,129,132,135,138,141,144,146,148,151,154,156,159,161,164,167,171,174,176],{"class":75,"line":114},3,[73,116,117],{"class":86},"  const",[73,119,120],{"class":101}," debitSum",[73,122,95],{"class":94},[73,124,125],{"class":101}," rows",[73,127,128],{"class":94},".",[73,130,131],{"class":90},"reduce",[73,133,134],{"class":94},"((",[73,136,137],{"class":101},"s",[73,139,140],{"class":94},",",[73,142,143],{"class":101}," r",[73,145,105],{"class":94},[73,147,108],{"class":94},[73,149,150],{"class":101}," s",[73,152,153],{"class":86}," +",[73,155,98],{"class":94},[73,157,158],{"class":101},"r",[73,160,128],{"class":94},[73,162,163],{"class":101},"debit",[73,165,166],{"class":86}," ||",[73,168,170],{"class":169},"sM54T"," 0",[73,172,173],{"class":94},"),",[73,175,170],{"class":169},[73,177,178],{"class":94},");\n",[73,180,182,184,187,189,191,193,195,197,199,201,203,205,207,209,211,213,215,217,220,222,224,226,228],{"class":75,"line":181},4,[73,183,117],{"class":86},[73,185,186],{"class":101}," creditSum",[73,188,95],{"class":94},[73,190,125],{"class":101},[73,192,128],{"class":94},[73,194,131],{"class":90},[73,196,134],{"class":94},[73,198,137],{"class":101},[73,200,140],{"class":94},[73,202,143],{"class":101},[73,204,105],{"class":94},[73,206,108],{"class":94},[73,208,150],{"class":101},[73,210,153],{"class":86},[73,212,98],{"class":94},[73,214,158],{"class":101},[73,216,128],{"class":94},[73,218,219],{"class":101},"credit",[73,221,166],{"class":86},[73,223,170],{"class":169},[73,225,173],{"class":94},[73,227,170],{"class":169},[73,229,178],{"class":94},[73,231,233,237,240,242,245,248,251,254,256,258,261],{"class":75,"line":232},5,[73,234,236],{"class":235},"sHkkW","  return",[73,238,239],{"class":101}," Math",[73,241,128],{"class":94},[73,243,244],{"class":90},"abs",[73,246,247],{"class":94},"(",[73,249,250],{"class":101},"debitSum",[73,252,253],{"class":86}," -",[73,255,186],{"class":101},[73,257,105],{"class":94},[73,259,260],{"class":94}," \u003C",[73,262,263],{"class":169}," 0.01\n",[73,265,267,270,273,277,280,283],{"class":75,"line":266},6,[73,268,269],{"class":86},"    ?",[73,271,272],{"class":94}," {",[73,274,276],{"class":275},"sz8Xr"," valid",[73,278,279],{"class":94},":",[73,281,282],{"class":235}," true",[73,284,285],{"class":94}," }\n",[73,287,289,292,294,296,298,301,303,306,308,312,316,319,321,324,327,329,332,334,337],{"class":75,"line":288},7,[73,290,291],{"class":86},"    :",[73,293,272],{"class":94},[73,295,276],{"class":275},[73,297,279],{"class":94},[73,299,300],{"class":235}," false",[73,302,140],{"class":94},[73,304,305],{"class":275}," reason",[73,307,279],{"class":94},[73,309,311],{"class":310},"sMJiu"," `",[73,313,315],{"class":314},"sdGka","貸借不一致: 借方",[73,317,318],{"class":235},"${",[73,320,250],{"class":314},[73,322,323],{"class":235},"}",[73,325,326],{"class":314}," / 貸方",[73,328,318],{"class":235},[73,330,331],{"class":314},"creditSum",[73,333,323],{"class":235},[73,335,336],{"class":310},"`",[73,338,339],{"class":94}," };\n",[73,341,343],{"class":75,"line":342},8,[73,344,345],{"class":94},"};\n",[26,347,349],{"id":348},"テスト-162-200","テスト 162 → 200",[15,351,352],{},"検証関数のテストを38件追加。正常系（全フィールド揃い、貸借一致）だけでなく、金額が文字列で入ってくるケース、日付が空のケース、貸借が1円ずれるケースなど、前日までに踏んだ型の罠をそのままテストケースに変換した。200件全パス。",[19,354],{},[22,356,357],{"id":357},"事業者データ一括取得の進捗表示改善",[15,359,360],{},"複数事業者のデータを順繰りに取得する処理で、進捗表示が「処理中...」としか出ていなかった。5社あるうちの3社目で止まっているのか、1社目で詰まっているのか分からない。",[15,362,363,366],{},[43,364,365],{},"N/M"," 形式に変更して、「3/5 社目を処理中」のように表示するようにした。content.jsのDOM更新箇所は1行の変更で済んだが、lib.js側でコールバックを受け取れるようにシグネチャを修正する必要があった。",[19,368],{},[22,370,371],{"id":371},"一括インポート機能の初期実装",[26,373,375],{"id":374},"libjs-に新関数を追加","lib.js に新関数を追加",[15,377,378],{},"エクスポートの逆方向。スプレッドシートから仕訳データを読み取り、会計サービスのインポート用CSVフォーマットに変換してアップロードする流れを組んだ。",[15,380,381],{},"主要な関数:",[34,383,384,392,400],{},[37,385,386,391],{},[40,387,388],{},[43,389,390],{},"fetchSheetData",": Sheets APIで指定範囲のデータを取得",[37,393,394,399],{},[40,395,396],{},[43,397,398],{},"transformToMfCsv",": スプレッドシートの列構造を会計サービスのインポートCSVの列順に変換。勘定科目コードのマッピングもここで処理",[37,401,402,407],{},[40,403,404],{},[43,405,406],{},"uploadImportCsv",": 会計サービスのインポートエンドポイントにCSVをPOST",[15,409,410],{},"content.jsにはインポートパネルのUIを追加。この時点ではシンプルなテキストボックス1つとボタン1つの構成だった。",[15,412,413],{},"テストは既存200件に追加分を入れて全パス。",[19,415],{},[22,417,419],{"id":418},"インポートパネルuiの大幅リデザイン","インポートパネルUIの大幅リデザイン",[26,421,423],{"id":422},"最初のuiの問題","最初のUIの問題",[15,425,426],{},"シンプルに作った最初のUIは、スプレッドシートURLを設定テーブルに永続保存する方式だった。しかし実際に使ってみると問題が見えた。インポートは月に1回程度の作業で、URLも毎回変わることがある。永続保存する意味が薄い上に、古いURLが残っていると誤ったデータを取り込むリスクがある。",[26,428,430],{"id":429},"使い捨てurl方式への転換","「使い捨てURL方式」への転換",[15,432,433],{},"URLを永続化せず、インポートパネルを開くたびに空のURL入力欄を表示する方式に切り替えた。パネルを閉じればURLは消える。",[26,435,437],{"id":436},"新しいuiの構成","新しいUIの構成",[64,439,444],{"className":440,"code":442,"language":443},[441],"language-text","インポートパネル\n┌──────────────────────────────────────┐\n│ 年度選択:  ○ 2023  ○ 2024  ● 2025  │\n├──────────────────────────────────────┤\n│ 事業者A  [スプレッドシートURL入力欄]  │\n│ 事業者B  [スプレッドシートURL入力欄]  │\n│ 事業者C  [スプレッドシートURL入力欄]  │\n├──────────────────────────────────────┤\n│ [一括インポート実行]                  │\n│ 進捗: 2/3 社目を処理中...            │\n└──────────────────────────────────────┘\n","text",[43,445,442],{"__ignoreMap":69},[15,447,448],{},"設計のポイント:",[34,450,451,457,463,469],{},[37,452,453,456],{},[40,454,455],{},"全事業者を一覧表示",": エクスポート側と同じく、登録済み事業者を全て並べる。インポートしない事業者はURL欄を空のままにすればスキップされる",[37,458,459,462],{},[40,460,461],{},"年度はラジオボタンで単一選択",": インポートは1年度ずつ確認しながら進めたい。複数年度を一気にインポートすると、途中でエラーが出たときの切り分けが面倒になる",[37,464,465,468],{},[40,466,467],{},"事業者ごとのURL入力欄",": インポート元のスプレッドシートは事業者ごとに異なる。URLが入っている事業者だけを対象に順繰り処理する",[37,470,471,474,475,477],{},[40,472,473],{},"進捗表示",": エクスポート側と同じ ",[43,476,365],{}," 形式",[26,479,481],{"id":480},"仕訳インポートcsvサンプルフォーマットへのリンク","仕訳インポートCSVサンプルフォーマットへのリンク",[15,483,484,485,488],{},"インポート時に会計サービスが期待するCSVフォーマットを確認できるよう、サンプルフォーマットのダウンロードリンクを追加した。会計サービスの内部URL ",[43,486,487],{},"/imports/mf_journals/sample_format"," にCTI（事業者コンテキスト）パラメータを付けてリンクする。事業者ごとにCTIが異なるため、各事業者の行にリンクを配置した。",[19,490],{},[22,492,494],{"id":493},"設定テーブルのui改善議論","設定テーブルのUI改善議論",[15,496,497],{},"エクスポート・インポートの両方でスプレッドシートURLを扱うようになり、設定テーブルが横に長くなってきた。特にスプレッドシート名称（「2025年度_仕訳帳_事業者A」のような長い文字列）がテーブルを圧迫する。",[15,499,500],{},"名称をツールチップに逃がしてURLだけ表示する案、名称を15文字で切って末尾に「...」を付ける案などを検討した。この日は方針を決めるところまでで、実装には入っていない。",[19,502],{},[22,504,505],{"id":505},"振り返り",[15,507,508],{},"一日の流れを並べると、「壊れたデータを検知する仕組みを作る → 逆方向の機能を組む → UIを実際に触って設計を変える」という段階を踏んでいた。検証関数を先に作ったのは正解で、インポート機能の開発中にバリデーションの考え方をそのまま転用できた。",[15,510,511],{},"UIを2回作り直すことになったのは無駄に見えるが、最初のシンプル版を実際に触ってみなければ「URLを永続化する必要がない」という判断には至らなかった。手を動かしてプロトタイプを触り、違和感に気づいてから設計を変える方が、机上で完璧なUIを考え続けるより速く正解にたどり着く。",[513,514,515],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":69,"searchDepth":83,"depth":83,"links":517},[518,522,523,526,532,533],{"id":24,"depth":83,"text":24,"children":519},[520,521],{"id":28,"depth":114,"text":29},{"id":348,"depth":114,"text":349},{"id":357,"depth":83,"text":357},{"id":371,"depth":83,"text":371,"children":524},[525],{"id":374,"depth":114,"text":375},{"id":418,"depth":83,"text":419,"children":527},[528,529,530,531],{"id":422,"depth":114,"text":423},{"id":429,"depth":114,"text":430},{"id":436,"depth":114,"text":437},{"id":480,"depth":114,"text":481},{"id":493,"depth":83,"text":494},{"id":505,"depth":83,"text":505},"dev","エクスポートデータの整合性検証関数3つを実装し162→200テストに拡充、一括インポート機能の初期実装からUIリデザイン（使い捨てURL方式・事業者一覧・年度ラジオボタン）までの段階的改善記録","md",{},true,null,"/mf-extension-export-import-enhancements","misc-dev",false,"2026-03-26T00:00:00.000Z",{"title":5,"description":535},"2026-03/2026-03-26/mf-extension-export-import-enhancements",[547,548,549,550,551],"Chrome拡張機能","クラウド会計","データ検証","インポート機能","UIデザイン","memo","YQuSWOW4lo2tbezlqUlU4Gheb-sVO8_SBV8WFd7eEjU",[],"https://log.eurekapu.com/og/blog/mf-extension-export-import-enhancements.png?v=2026-03-26T00%3A00%3A00.000Z&title=Chrome%E6%8B%A1%E5%BC%B5%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E4%BC%9A%E8%A8%88%E9%80%A3%E6%90%BA%20-%20%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88%E6%95%B4%E5%90%88%E6%80%A7%E6%A4%9C%E8%A8%BC%E3%81%A8%E4%B8%80%E6%8B%AC%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88%E6%A9%9F%E8%83%BD%E3%81%AE%E6%AE%B5%E9%9A%8E%E7%9A%84%E6%A7%8B%E7%AF%89&author=Kei%20Komatsu&sig=58c6635b3ace0579",1782528821160]