[{"data":1,"prerenderedAt":808},["ShallowReactive",2],{"content-/mf-extension-bug-fixes-and-ui-polish":3,"all-pages-for-dir":806,"og-image-/mf-extension-bug-fixes-and-ui-polish":807},{"id":4,"title":5,"body":6,"category":786,"description":787,"extension":788,"meta":789,"navigation":790,"ogImage":791,"path":792,"project_name":793,"published":794,"publishedAt":795,"seo":796,"stem":797,"tags":798,"todo":804,"unpublished":794,"updatedAt":791,"__hash__":805},"pages/2026-03/2026-03-25/mf-extension-bug-fixes-and-ui-polish.md","会計サービス Chrome拡張：連携明細の重複表示バグ修正・列幅自動調整・UI統一",{"type":7,"value":8,"toc":759},"minimark",[9,14,18,21,25,29,32,35,46,49,59,62,66,69,79,154,157,166,169,171,175,179,186,192,196,199,396,403,405,409,412,427,430,548,553,555,559,562,578,580,584,587,665,668,670,674,678,681,685,688,690,693,696,699,702,720,723,735,737,740,743,746,755],[10,11,13],"h1",{"id":12},"会計サービス-chrome拡張連携明細の重複表示バグ修正とui磨き込み","会計サービス Chrome拡張：連携明細の重複表示バグ修正とUI磨き込み",[15,16,17],"p",{},"朝、連携明細の事業者リストが3つずつ重複して並んでいるのを見て手が止まった。原因を追いかけてasync/awaitのインターリーブ問題にたどり着き、排他制御を入れるところから一日が始まった。その後、Sheets APIのautoResizeが沈黙する問題をねじ伏せ、UIのスタイルを統一し、推移表のBS/PLを1シートに結合するところまで進めた。",[19,20],"hr",{},[22,23,24],"h2",{"id":24},"連携明細の3重表示バグ",[26,27,28],"h3",{"id":28},"症状",[15,30,31],{},"連携明細タブを開くと、同じ事業者が3回ずつDOMに追加されている。3事業者なら9行表示される。",[26,33,34],{"id":34},"原因特定のプロセス",[15,36,37,38,42,43,45],{},"最初はDOM操作の二重呼び出しを疑ったが、",[39,40,41],"code",{},"populateEntityServiceList()"," の呼び出し元を検索すると、3つのURL入力フィールドのauto-saveイベントが同時に発火していた。ページロード時にchrome.storageから値を復元する処理が3つのinputそれぞれのchangeイベントを発火させ、それぞれが ",[39,44,41],{}," を呼ぶ。",[15,47,48],{},"問題はこの関数がasyncだったこと。3つの呼び出しが同時に走り、for loop内のawaitでインターリーブが発生する。各呼び出しが独立にDOMへappendするため、結果が3倍になる。",[50,51,56],"pre",{"className":52,"code":54,"language":55},[53],"language-text","呼出A: fetch事業者1 → append → fetch事業者2 → append → ...\n呼出B: fetch事業者1 → append → fetch事業者2 → append → ...\n呼出C: fetch事業者1 → append → fetch事業者2 → append → ...\n","text",[39,57,54],{"__ignoreMap":58},"",[15,60,61],{},"console.logにタイムスタンプと呼び出し元IDを仕込んで、3つのPromiseが並走しているのを目視で確認した瞬間に原因が確定した。",[26,63,65],{"id":64},"修正-排他制御と純粋関数の分離","修正: 排他制御と純粋関数の分離",[15,67,68],{},"2段階で修正した。",[15,70,71],{},[72,73,74,75,78],"strong",{},"1. ",[39,76,77],{},"makeAsyncQueue"," による排他制御",[50,80,84],{"className":81,"code":82,"language":83,"meta":58,"style":58},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// 同一関数の並列実行を直列化するキュー\nconst enqueue = makeAsyncQueue();\nconst populateEntityServiceList = () => enqueue(async () => { ... });\n","javascript",[39,85,86,95,117],{"__ignoreMap":58},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,94],{"class":93},"sxvE3","// 同一関数の並列実行を直列化するキュー\n",[87,96,98,102,106,110,114],{"class":89,"line":97},2,[87,99,101],{"class":100},"stQ0i","const",[87,103,105],{"class":104},"s4oTP"," enqueue",[87,107,109],{"class":108},"shFtX"," =",[87,111,113],{"class":112},"senZ8"," makeAsyncQueue",[87,115,116],{"class":108},"();\n",[87,118,120,122,125,127,130,133,135,138,141,143,145,148,151],{"class":89,"line":119},3,[87,121,101],{"class":100},[87,123,124],{"class":112}," populateEntityServiceList",[87,126,109],{"class":108},[87,128,129],{"class":108}," ()",[87,131,132],{"class":108}," =>",[87,134,105],{"class":112},[87,136,137],{"class":108},"(",[87,139,140],{"class":100},"async",[87,142,129],{"class":108},[87,144,132],{"class":108},[87,146,147],{"class":108}," {",[87,149,150],{"class":108}," ...",[87,152,153],{"class":108}," });\n",[15,155,156],{},"先行する呼び出しが完了するまで次の呼び出しをブロックする。これで3重実行が消える。",[15,158,159],{},[72,160,161,162,165],{},"2. ",[39,163,164],{},"buildEntityServiceListItems"," 純粋関数への分離",[15,167,168],{},"DOM操作とデータ構築を分離した。純粋関数がHTML要素の配列を返し、呼び出し側が一度だけDOMをクリアしてappendする。DOMクリア→append が1箇所に集約されるため、仮にキューなしで2回呼ばれても重複しない（冪等になる）。",[19,170],{},[22,172,174],{"id":173},"google-sheets列幅自動調整","Google Sheets列幅自動調整",[26,176,178],{"id":177},"autoresizedimensionsが動かない","autoResizeDimensionsが動かない",[15,180,181,182,185],{},"エクスポート後のスプレッドシートで列幅がデフォルトのまま狭く、データが見切れる。Sheets APIの ",[39,183,184],{},"autoResizeDimensions"," リクエストを送っているのに変化がない。",[15,187,188,189,191],{},"APIリファレンスとStack Overflowを掘ると、",[39,190,184],{}," はデータ量が多いシートで正しく動作しない既知のバグだった。Googleのissue trackerにも2019年から報告が上がっていて、2026年現在も未修正。",[26,193,195],{"id":194},"回避策-calccolumnwidths純粋関数","回避策: calcColumnWidths純粋関数",[15,197,198],{},"API任せを諦めて、自前で列幅を計算する方針に切り替えた。",[50,200,202],{"className":81,"code":201,"language":83,"meta":58,"style":58},"// 各列のデータを走査し、最大文字数からピクセル幅を算出\nconst calcColumnWidths = (rows, options) =>\n  rows[0].map((_, colIdx) => {\n    const maxLen = Math.max(...rows.map(row => String(row[colIdx] ?? '').length));\n    return Math.max(maxLen * options.charWidth + options.padding, options.minWidth);\n  });\n",[39,203,204,209,236,272,340,390],{"__ignoreMap":58},[87,205,206],{"class":89,"line":90},[87,207,208],{"class":93},"// 各列のデータを走査し、最大文字数からピクセル幅を算出\n",[87,210,211,213,216,218,221,224,227,230,233],{"class":89,"line":97},[87,212,101],{"class":100},[87,214,215],{"class":112}," calcColumnWidths",[87,217,109],{"class":108},[87,219,220],{"class":108}," (",[87,222,223],{"class":104},"rows",[87,225,226],{"class":108},",",[87,228,229],{"class":104}," options",[87,231,232],{"class":108},")",[87,234,235],{"class":108}," =>\n",[87,237,238,241,244,248,251,254,257,260,262,265,267,269],{"class":89,"line":119},[87,239,240],{"class":104},"  rows",[87,242,243],{"class":108},"[",[87,245,247],{"class":246},"sM54T","0",[87,249,250],{"class":108},"].",[87,252,253],{"class":112},"map",[87,255,256],{"class":108},"((",[87,258,259],{"class":104},"_",[87,261,226],{"class":108},[87,263,264],{"class":104}," colIdx",[87,266,232],{"class":108},[87,268,132],{"class":108},[87,270,271],{"class":108}," {\n",[87,273,275,278,281,283,286,289,292,295,297,299,301,303,306,308,311,313,315,317,320,323,326,330,333,337],{"class":89,"line":274},4,[87,276,277],{"class":100},"    const",[87,279,280],{"class":104}," maxLen",[87,282,109],{"class":108},[87,284,285],{"class":104}," Math",[87,287,288],{"class":108},".",[87,290,291],{"class":112},"max",[87,293,294],{"class":108},"(...",[87,296,223],{"class":104},[87,298,288],{"class":108},[87,300,253],{"class":112},[87,302,137],{"class":108},[87,304,305],{"class":104},"row",[87,307,132],{"class":108},[87,309,310],{"class":112}," String",[87,312,137],{"class":108},[87,314,305],{"class":104},[87,316,243],{"class":108},[87,318,319],{"class":104},"colIdx",[87,321,322],{"class":108},"]",[87,324,325],{"class":100}," ??",[87,327,329],{"class":328},"sMJiu"," ''",[87,331,332],{"class":108},").",[87,334,336],{"class":335},"sz8Xr","length",[87,338,339],{"class":108},"));\n",[87,341,343,347,349,351,353,355,358,361,363,365,368,371,373,375,378,380,382,384,387],{"class":89,"line":342},5,[87,344,346],{"class":345},"sHkkW","    return",[87,348,285],{"class":104},[87,350,288],{"class":108},[87,352,291],{"class":112},[87,354,137],{"class":108},[87,356,357],{"class":104},"maxLen",[87,359,360],{"class":100}," *",[87,362,229],{"class":104},[87,364,288],{"class":108},[87,366,367],{"class":104},"charWidth",[87,369,370],{"class":100}," +",[87,372,229],{"class":104},[87,374,288],{"class":108},[87,376,377],{"class":104},"padding",[87,379,226],{"class":108},[87,381,229],{"class":104},[87,383,288],{"class":108},[87,385,386],{"class":104},"minWidth",[87,388,389],{"class":108},");\n",[87,391,393],{"class":89,"line":392},6,[87,394,395],{"class":108},"  });\n",[15,397,398,399,402],{},"計算結果を ",[39,400,401],{},"updateDimensionProperties"," で明示的に設定する。autoResizeに頼らない分、フォントサイズやpadding値を自分で決められる利点もある。",[19,404],{},[22,406,408],{"id":407},"chrometabscreate失敗時のガード","chrome.tabs.create失敗時のガード",[26,410,28],{"id":411},"症状-1",[15,413,414,415,418,419,422,423,426],{},"特定条件下でchrome.tabs.createが ",[39,416,417],{},"undefined"," を返し、直後の ",[39,420,421],{},"tab.id"," 参照でクラッシュ。",[39,424,425],{},"sendResponse"," が呼ばれないまま関数が終了し、content.jsのメッセージリスナーが永久にハングする。",[26,428,429],{"id":429},"修正",[50,431,433],{"className":81,"code":432,"language":83,"meta":58,"style":58},"const tab = await chrome.tabs.create({ url, active: false });\nif (!tab?.id) {\n  sendResponse({ success: false, error: 'Tab creation failed' });\n  return;\n}\n",[39,434,435,479,502,535,543],{"__ignoreMap":58},[87,436,437,439,442,444,447,450,452,455,457,460,463,466,468,471,474,477],{"class":89,"line":90},[87,438,101],{"class":100},[87,440,441],{"class":104}," tab",[87,443,109],{"class":108},[87,445,446],{"class":345}," await",[87,448,449],{"class":104}," chrome",[87,451,288],{"class":108},[87,453,454],{"class":104},"tabs",[87,456,288],{"class":108},[87,458,459],{"class":112},"create",[87,461,462],{"class":108},"({",[87,464,465],{"class":104}," url",[87,467,226],{"class":108},[87,469,470],{"class":335}," active",[87,472,473],{"class":108},":",[87,475,476],{"class":345}," false",[87,478,153],{"class":108},[87,480,481,484,486,489,492,495,498,500],{"class":89,"line":97},[87,482,483],{"class":345},"if",[87,485,220],{"class":108},[87,487,488],{"class":100},"!",[87,490,491],{"class":104},"tab",[87,493,494],{"class":108},"?.",[87,496,497],{"class":104},"id",[87,499,232],{"class":108},[87,501,271],{"class":108},[87,503,504,507,509,512,514,516,518,521,523,526,530,533],{"class":89,"line":119},[87,505,506],{"class":112},"  sendResponse",[87,508,462],{"class":108},[87,510,511],{"class":335}," success",[87,513,473],{"class":108},[87,515,476],{"class":345},[87,517,226],{"class":108},[87,519,520],{"class":335}," error",[87,522,473],{"class":108},[87,524,525],{"class":328}," '",[87,527,529],{"class":528},"sdGka","Tab creation failed",[87,531,532],{"class":328},"'",[87,534,153],{"class":108},[87,536,537,540],{"class":89,"line":274},[87,538,539],{"class":345},"  return",[87,541,542],{"class":108},";\n",[87,544,545],{"class":89,"line":342},[87,546,547],{"class":108},"}\n",[15,549,550,552],{},[39,551,425],{}," を確実に呼ぶガードを追加。content.js側はタイムアウトを持っていないため、このガードがないと一度の失敗でタブ全体がフリーズする。",[19,554],{},[22,556,558],{"id":557},"連携明細uiの改善","連携明細UIの改善",[15,560,561],{},"チェックボックスの視覚的ノイズを減らした。",[563,564,565,572],"ul",{},[566,567,568,571],"li",{},[72,569,570],{},"チェックボックスを非表示にしてタイトルラベル化",": 事業者名のチェックボックスは実質「全選択トグル」なので、チェックボックスを消してクリッカブルなタイトルラベルに変更。クリックで配下の全サービスが選択/解除される",[566,573,574,577],{},[72,575,576],{},"全選択と個別選択の視覚的区別",": 全選択はボールドの事業者名、個別選択は通常ウェイトのサービス名。インデントで親子関係を表現",[19,579],{},[22,581,583],{"id":582},"スタイル統一-全帳表全年度全選択ボタン","スタイル統一: 全帳表・全年度・全選択ボタン",[15,585,586],{},"散らばっていた「全選択」系のUI要素のスタイルを統一した。",[50,588,592],{"className":589,"code":590,"language":591,"meta":58,"style":58},"language-css shiki shiki-themes vitesse-light vitesse-light","/* 全帳表・全年度・全選択ボタン共通 */\n.select-all-btn {\n  border: none;\n  background: transparent;\n  color: #888;\n  cursor: pointer;\n}\n","css",[39,593,594,599,608,621,633,648,660],{"__ignoreMap":58},[87,595,596],{"class":89,"line":90},[87,597,598],{"class":93},"/* 全帳表・全年度・全選択ボタン共通 */\n",[87,600,601,603,606],{"class":89,"line":97},[87,602,288],{"class":108},[87,604,605],{"class":104},"select-all-btn",[87,607,271],{"class":108},[87,609,610,613,615,619],{"class":89,"line":119},[87,611,612],{"class":335},"  border",[87,614,473],{"class":108},[87,616,618],{"class":617},"snbK4"," none",[87,620,542],{"class":108},[87,622,623,626,628,631],{"class":89,"line":274},[87,624,625],{"class":335},"  background",[87,627,473],{"class":108},[87,629,630],{"class":617}," transparent",[87,632,542],{"class":108},[87,634,635,638,640,643,646],{"class":89,"line":342},[87,636,637],{"class":335},"  color",[87,639,473],{"class":108},[87,641,642],{"class":108}," #",[87,644,645],{"class":617},"888",[87,647,542],{"class":108},[87,649,650,653,655,658],{"class":89,"line":392},[87,651,652],{"class":335},"  cursor",[87,654,473],{"class":108},[87,656,657],{"class":617}," pointer",[87,659,542],{"class":108},[87,661,663],{"class":89,"line":662},7,[87,664,547],{"class":108},[15,666,667],{},"ボーダーなし、背景透明、グレー文字。主張しすぎない補助UIとして統一。以前はボタンごとにボーダーの有無やフォントサイズが異なっていた。",[19,669],{},[22,671,673],{"id":672},"推移表のbspl統合","推移表のBS/PL統合",[26,675,677],{"id":676},"before","Before",[15,679,680],{},"BS（貸借対照表）とPL（損益計算書）がそれぞれ別シートに出力されていた。比較するたびにシートを切り替える必要がある。",[26,682,684],{"id":683},"after","After",[15,686,687],{},"1シートに縦結合。BSを上半分、PLを下半分に配置し、間に空行を1行挟む。ヘッダー行はBSとPLそれぞれに付与。",[19,689],{},[22,691,692],{"id":692},"年度ヘッダーのスクレイピング問題",[26,694,28],{"id":695},"症状-2",[15,697,698],{},"推移表のスクレイピングで年度セルが空文字列になる。データ行は正常に取得できるのに、ヘッダーだけ取れない。",[26,700,701],{"id":701},"原因",[15,703,704,705,708,709,712,713,716,717,719],{},"DOMを確認すると、年度セルは ",[39,706,707],{},"\u003Ctd>"," ではなく ",[39,710,711],{},"\u003Cth>"," で記述されていた。スクレイピングコードが ",[39,714,715],{},"querySelectorAll('td')"," で行内のセルを取得していたため、",[39,718,711],{}," がスキップされていた。",[26,721,429],{"id":722},"修正-1",[15,724,725,728,729,731,732,734],{},[39,726,727],{},"querySelectorAll('td, th')"," に変更。テーブルのスクレイピングでは ",[39,730,707],{}," だけ取るのはよくある落とし穴で、この会計サービスに限らず ",[39,733,711],{}," がデータ行に混在するパターンは珍しくない。",[19,736],{},[22,738,739],{"id":739},"振り返り",[15,741,742],{},"3重表示バグの原因を追いかけるうちに、「async関数を複数箇所から同時に呼ぶと何が起きるか」を手を動かして確認できた。ログにタイムスタンプを仕込んで3本のPromiseが並走する様子を眺めたとき、排他制御の必要性が腹落ちした。makeAsyncQueueと純粋関数分離の組み合わせは、同種の問題に再利用できるパターンとして手元に残った。",[15,744,745],{},"autoResizeDimensionsの件は、「APIが正しく動くはず」という前提で30分ほど設定パラメータを変えて試行錯誤した後、issue trackerを開いて既知バグだと知った。先にissue trackerを確認すべきだった。自前計算に切り替えた結果、列幅のコントロールが効くようになったのは怪我の功名。",[15,747,748,749,751,752,754],{},"年度ヘッダーの ",[39,750,711],{}," vs ",[39,753,707],{}," は、DevToolsでDOM構造を直接見た瞬間に解決した。推測でコードを直す前にDOMを見る、というのを徹底すればもっと早く片付いた。",[756,757,758],"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 .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}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);}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}",{"title":58,"searchDepth":97,"depth":97,"links":760},[761,766,770,774,775,776,780,785],{"id":24,"depth":97,"text":24,"children":762},[763,764,765],{"id":28,"depth":119,"text":28},{"id":34,"depth":119,"text":34},{"id":64,"depth":119,"text":65},{"id":173,"depth":97,"text":174,"children":767},[768,769],{"id":177,"depth":119,"text":178},{"id":194,"depth":119,"text":195},{"id":407,"depth":97,"text":408,"children":771},[772,773],{"id":411,"depth":119,"text":28},{"id":429,"depth":119,"text":429},{"id":557,"depth":97,"text":558},{"id":582,"depth":97,"text":583},{"id":672,"depth":97,"text":673,"children":777},[778,779],{"id":676,"depth":119,"text":677},{"id":683,"depth":119,"text":684},{"id":692,"depth":97,"text":692,"children":781},[782,783,784],{"id":695,"depth":119,"text":28},{"id":701,"depth":119,"text":701},{"id":722,"depth":119,"text":429},{"id":739,"depth":97,"text":739},"dev","クラウド会計サービス連携Chrome拡張で連携明細が3重表示されるバグを排他制御で修正、Google Sheets APIのautoResize既知バグを回避して列幅を手動計算、UIスタイル統一と推移表のBS/PL統合まで","md",{},true,null,"/mf-extension-bug-fixes-and-ui-polish","misc-dev",false,"2026-03-25T00:00:00.000Z",{"title":5,"description":787},"2026-03/2026-03-25/mf-extension-bug-fixes-and-ui-polish",[799,800,801,802,803],"Chrome拡張機能","クラウド会計","排他制御","Google Sheets API","UI改善","memo","wNqR3tjXJAjT3ZswAuEdyT69pSfgOnxXt_Jsp7KkAwA",[],"https://log.eurekapu.com/og/blog/mf-extension-bug-fixes-and-ui-polish.png?v=2026-03-25T00%3A00%3A00.000Z&title=%E4%BC%9A%E8%A8%88%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%20Chrome%E6%8B%A1%E5%BC%B5%EF%BC%9A%E9%80%A3%E6%90%BA%E6%98%8E%E7%B4%B0%E3%81%AE%E9%87%8D%E8%A4%87%E8%A1%A8%E7%A4%BA%E3%83%90%E3%82%B0%E4%BF%AE%E6%AD%A3%E3%83%BB%E5%88%97%E5%B9%85%E8%87%AA%E5%8B%95%E8%AA%BF%E6%95%B4%E3%83%BBUI%E7%B5%B1%E4%B8%80&author=Kei%20Komatsu&sig=c73c5142c5b56bff",1782528820714]