[{"data":1,"prerenderedAt":818},["ShallowReactive",2],{"content-/chrome-extension-journal-management":3,"all-pages-for-dir":816,"og-image-/chrome-extension-journal-management":817},{"id":4,"title":5,"body":6,"category":796,"description":797,"extension":798,"meta":799,"navigation":800,"ogImage":801,"path":802,"project_name":803,"published":804,"publishedAt":805,"seo":806,"stem":807,"tags":808,"todo":814,"unpublished":804,"updatedAt":801,"__hash__":815},"pages/2026-04/2026-03-31/chrome-extension-journal-management.md","Chrome拡張 仕訳管理タブの新設 - 仕訳削除・対象外復帰・カスタムモーダル置換・Playwright PoC",{"type":7,"value":8,"toc":773},"minimark",[9,14,35,38,42,47,50,79,82,85,91,98,100,104,113,122,133,139,149,159,321,323,326,329,355,358,360,366,372,376,386,396,398,402,406,424,434,589,592,595,597,600,739,741,744,769],[10,11,13],"h1",{"id":12},"chrome拡張-仕訳管理タブの新設と仕訳削除対象外復帰機能","Chrome拡張 仕訳管理タブの新設と仕訳削除・対象外復帰機能",[15,16,17,18,22,23,26,27,30,31,34],"p",{},"朝から会計ソフトA連携Chrome拡張の仕訳管理タブを一から組み上げ、夕方にはPlaywright PoCまで走らせていた。",[19,20,21],"code",{},"journalizing_suggestions"," APIが ",[19,24,25],{},"status=ignored"," を無視する仕様に30分取られ、",[19,28,29],{},"trans_list","のHTMLをfetchしたらtbodyが空で手が止まり、",[19,32,33],{},"confirm()","を18箇所潰してEscapeキーバグを踏み抜いた。最終的にChrome DevTools MCP経由で全テスト項目がパスし、未登録明細211件のスプレッドシート書き出しまで確認が通った。",[36,37],"hr",{},[39,40,41],"h2",{"id":41},"仕訳管理タブの設計と実装",[43,44,46],"h3",{"id":45},"_4セクション構成","4セクション構成",[15,48,49],{},"新設した仕訳管理タブは4つのセクションで構成した。",[51,52,53,61,67,73],"ol",{},[54,55,56,60],"li",{},[57,58,59],"strong",{},"仕訳帳エクスポート"," - 仕訳帳データをスプレッドシートに書き出し",[54,62,63,66],{},[57,64,65],{},"仕訳一括削除"," - 期間指定で仕訳を一括削除",[54,68,69,72],{},[57,70,71],{},"対象外明細エクスポート"," - 対象外に設定された明細の一覧を書き出し",[54,74,75,78],{},[57,76,77],{},"対象外一括復帰"," - 対象外明細を未登録状態に戻す",[15,80,81],{},"3階層のインデントでセクションを視覚的に分離し、各セクションにタイトルを付けた。タブ→セクション→操作ボタンの階層が目で追えるようになった。",[43,83,84],{"id":84},"口座共通ルールとページネーション",[15,86,87,90],{},[19,88,89],{},"sub_account_id=\"0\""," を指定すると口座共通の自動仕訳ルールが返ることを確認した。APIドキュメントには記載がなく、Chrome DevToolsでリクエストを観察して突き止めた。",[15,92,93,94,97],{},"50件超の明細がある場合のページネーションも検証。APIはページ番号ベースで、",[19,95,96],{},"page","パラメータを1ずつ増やして空配列が返るまで取得する方式で実装した。",[36,99],{},[39,101,103],{"id":102},"api調査で踏んだ罠","API調査で踏んだ罠",[43,105,107,109,110,112],{"id":106},"journalizing_suggestions-の-statusignored-は無視される",[19,108,21],{}," の ",[19,111,25],{}," は無視される",[15,114,115,116,118,119,121],{},"対象外に設定した明細を一覧取得しようとして、",[19,117,21],{}," APIに ",[19,120,25],{}," パラメータを渡した。レスポンスが返ってきたが、中身を見ると未入力の明細しか含まれていなかった。",[15,123,124,125,128,129,132],{},"Chrome DevToolsのNetworkタブでリクエストを再確認し、パラメータを変えて何度か試した。結論として、このAPIは未入力(",[19,126,127],{},"status=default",")の明細しか返さず、",[19,130,131],{},"status","パラメータ自体を無視している。会計ソフトAのフロントエンドも、対象外明細の取得にはこのAPIを使っていなかった。",[43,134,136,138],{"id":135},"trans_list-のfetchが空-iframe方式に転換",[19,137,29],{}," のfetchが空 → iframe方式に転換",[15,140,141,142,144,145,148],{},"対象外明細のIDを取得するために ",[19,143,29],{}," ページをfetchで取得した。HTMLは返ってくるが、",[19,146,147],{},"\u003Ctbody>"," が空だった。ページのソースを見るとテーブルのデータ部分はAjaxで後から読み込まれる構造だった。",[15,150,151,152,154,155,158],{},"fetchでは静的HTMLしか取得できないので、iframe方式に切り替えた。非表示のiframeに ",[19,153,29],{}," を読み込み、Ajax完了を待ってからiframe内のDOMを走査して明細IDを取得する。読み込み完了の検知は ",[19,156,157],{},"MutationObserver"," でtbodyに子要素が追加されるのを監視した。",[160,161,166],"pre",{"className":162,"code":163,"language":164,"meta":165,"style":165},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// iframeのtbodyに行が追加されるのを待つ\nconst observer = new MutationObserver((mutations, obs) => {\n  const rows = iframe.contentDocument.querySelectorAll('tbody tr');\n  if (rows.length > 0) { obs.disconnect(); resolve(rows); }\n});\n","javascript","",[19,167,168,177,220,261,315],{"__ignoreMap":165},[169,170,173],"span",{"class":171,"line":172},"line",1,[169,174,176],{"class":175},"sxvE3","// iframeのtbodyに行が追加されるのを待つ\n",[169,178,180,184,188,192,195,199,202,205,208,211,214,217],{"class":171,"line":179},2,[169,181,183],{"class":182},"stQ0i","const",[169,185,187],{"class":186},"s4oTP"," observer",[169,189,191],{"class":190},"shFtX"," =",[169,193,194],{"class":182}," new",[169,196,198],{"class":197},"senZ8"," MutationObserver",[169,200,201],{"class":190},"((",[169,203,204],{"class":186},"mutations",[169,206,207],{"class":190},",",[169,209,210],{"class":186}," obs",[169,212,213],{"class":190},")",[169,215,216],{"class":190}," =>",[169,218,219],{"class":190}," {\n",[169,221,223,226,229,231,234,237,240,242,245,248,252,256,258],{"class":171,"line":222},3,[169,224,225],{"class":182},"  const",[169,227,228],{"class":186}," rows",[169,230,191],{"class":190},[169,232,233],{"class":186}," iframe",[169,235,236],{"class":190},".",[169,238,239],{"class":186},"contentDocument",[169,241,236],{"class":190},[169,243,244],{"class":197},"querySelectorAll",[169,246,247],{"class":190},"(",[169,249,251],{"class":250},"sMJiu","'",[169,253,255],{"class":254},"sdGka","tbody tr",[169,257,251],{"class":250},[169,259,260],{"class":190},");\n",[169,262,264,268,271,274,276,280,283,287,289,292,294,296,299,302,305,307,309,312],{"class":171,"line":263},4,[169,265,267],{"class":266},"sHkkW","  if",[169,269,270],{"class":190}," (",[169,272,273],{"class":186},"rows",[169,275,236],{"class":190},[169,277,279],{"class":278},"sz8Xr","length",[169,281,282],{"class":190}," >",[169,284,286],{"class":285},"sM54T"," 0",[169,288,213],{"class":190},[169,290,291],{"class":190}," {",[169,293,210],{"class":186},[169,295,236],{"class":190},[169,297,298],{"class":197},"disconnect",[169,300,301],{"class":190},"();",[169,303,304],{"class":197}," resolve",[169,306,247],{"class":190},[169,308,273],{"class":186},[169,310,311],{"class":190},");",[169,313,314],{"class":190}," }\n",[169,316,318],{"class":171,"line":317},5,[169,319,320],{"class":190},"});\n",[36,322],{},[39,324,325],{"id":325},"フルサイクル検証",[15,327,328],{},"仕訳登録→削除→対象外設定→復帰のフルサイクルをChrome DevTools MCP経由で検証した。",[51,330,331,337,343,349],{},[54,332,333,336],{},[57,334,335],{},"仕訳登録",": 未登録明細から仕訳を作成",[54,338,339,342],{},[57,340,341],{},"仕訳削除",": 作成した仕訳を一括削除 → 明細が未登録に戻ることを確認",[54,344,345,348],{},[57,346,347],{},"対象外設定",": 明細を対象外に設定",[54,350,351,354],{},[57,352,353],{},"対象外復帰",": 対象外明細を未登録状態に復帰 → 再度仕訳登録できることを確認",[15,356,357],{},"複合ルール（1つの明細から複数の仕訳行が生成されるルール）の個別削除も検証し、関連する仕訳行が連動して削除されることを確認した。",[36,359],{},[39,361,363,365],{"id":362},"confirm-18箇所のカスタムモーダル置換",[19,364,33],{}," 18箇所のカスタムモーダル置換",[15,367,368,369,371],{},"Chrome拡張のUIでは ",[19,370,33],{}," を使っていたが、ブラウザのネイティブダイアログは「このページのスクリプトがダイアログを表示しています」の警告が出る場合があり、UXが悪い。18箇所すべてをカスタムモーダルに置き換えた。",[43,373,375],{"id":374},"escapeキーバグ","Escapeキーバグ",[15,377,378,379,381,382,385],{},"置換後のテストで、Escapeキーを押すとモーダルが閉じるがPromiseがresolveもrejectもされない状態になる問題が見つかった。",[19,380,33],{}," はEscapeで ",[19,383,384],{},"false"," を返すが、カスタムモーダルではEscapeのイベントハンドリングが抜けていた。",[15,387,388,391,392,395],{},[19,389,390],{},"keydown"," イベントリスナーでEscapeキーをキャッチし、キャンセルボタンと同じ処理（",[19,393,394],{},"resolve(false)","）を実行するように修正した。モーダルの表示時にリスナーを登録し、閉じるときに解除する。",[36,397],{},[39,399,401],{"id":400},"playwright-会計ソフトa-api連携poc","Playwright 会計ソフトA API連携PoC",[43,403,405],{"id":404},"httponlyセッションcookie-の壁","HttpOnlyセッションCookie の壁",[15,407,408,409,412,413,415,416,419,420,423],{},"Playwrightから会計ソフトAのAPIを直接叩こうとして、セッションCookieが ",[19,410,411],{},"HttpOnly"," 属性を持っていることに気づいた。",[19,414,411],{}," のCookieはJavaScriptの ",[19,417,418],{},"document.cookie"," からは読めず、",[19,421,422],{},"page.context().cookies()"," でPlaywrightから取得してfetchのヘッダーに付けようとしたが、API側のCSRF検証で弾かれた。",[15,425,426,427,430,431,433],{},"最終的に ",[19,428,429],{},"page.evaluate()"," 内でfetchを実行する方式に落ち着いた。",[19,432,429],{}," はブラウザコンテキストで実行されるため、Cookieは自動的に付与され、CSRFトークンもDOMから取得できる。",[160,435,437],{"className":162,"code":436,"language":164,"meta":165,"style":165},"// page.evaluate() 内ならCookieもCSRFトークンも使える\nconst data = await page.evaluate(async () => {\n  const token = document.querySelector('meta[name=\"csrf-token\"]').content;\n  const res = await fetch('/api/...', {\n    headers: { 'X-CSRF-Token': token }\n  });\n  return res.json();\n});\n",[19,438,439,444,476,511,538,562,568,584],{"__ignoreMap":165},[169,440,441],{"class":171,"line":172},[169,442,443],{"class":175},"// page.evaluate() 内ならCookieもCSRFトークンも使える\n",[169,445,446,448,451,453,456,459,461,464,466,469,472,474],{"class":171,"line":179},[169,447,183],{"class":182},[169,449,450],{"class":186}," data",[169,452,191],{"class":190},[169,454,455],{"class":266}," await",[169,457,458],{"class":186}," page",[169,460,236],{"class":190},[169,462,463],{"class":197},"evaluate",[169,465,247],{"class":190},[169,467,468],{"class":182},"async",[169,470,471],{"class":190}," ()",[169,473,216],{"class":190},[169,475,219],{"class":190},[169,477,478,480,483,485,488,490,493,495,497,500,502,505,508],{"class":171,"line":222},[169,479,225],{"class":182},[169,481,482],{"class":186}," token",[169,484,191],{"class":190},[169,486,487],{"class":186}," document",[169,489,236],{"class":190},[169,491,492],{"class":197},"querySelector",[169,494,247],{"class":190},[169,496,251],{"class":250},[169,498,499],{"class":254},"meta[name=\"csrf-token\"]",[169,501,251],{"class":250},[169,503,504],{"class":190},").",[169,506,507],{"class":186},"content",[169,509,510],{"class":190},";\n",[169,512,513,515,518,520,522,525,527,529,532,534,536],{"class":171,"line":263},[169,514,225],{"class":182},[169,516,517],{"class":186}," res",[169,519,191],{"class":190},[169,521,455],{"class":266},[169,523,524],{"class":197}," fetch",[169,526,247],{"class":190},[169,528,251],{"class":250},[169,530,531],{"class":254},"/api/...",[169,533,251],{"class":250},[169,535,207],{"class":190},[169,537,219],{"class":190},[169,539,540,543,546,548,551,554,556,558,560],{"class":171,"line":317},[169,541,542],{"class":278},"    headers",[169,544,545],{"class":190},":",[169,547,291],{"class":190},[169,549,550],{"class":250}," '",[169,552,553],{"class":254},"X-CSRF-Token",[169,555,251],{"class":250},[169,557,545],{"class":190},[169,559,482],{"class":186},[169,561,314],{"class":190},[169,563,565],{"class":171,"line":564},6,[169,566,567],{"class":190},"  });\n",[169,569,571,574,576,578,581],{"class":171,"line":570},7,[169,572,573],{"class":266},"  return",[169,575,517],{"class":186},[169,577,236],{"class":190},[169,579,580],{"class":197},"json",[169,582,583],{"class":190},"();\n",[169,585,587],{"class":171,"line":586},8,[169,588,320],{"class":190},[43,590,591],{"id":591},"未登録明細211件のエクスポート",[15,593,594],{},"Chrome DevTools MCP経由で未登録明細のエクスポートを実行し、211件が新規スプレッドシートに書き出された。APIのページネーション、スプレッドシートの書式設定、行数の整合性をすべて確認した。",[36,596],{},[39,598,599],{"id":599},"試行錯誤テーブル",[601,602,603,625],"table",{},[604,605,606],"thead",{},[607,608,609,613,616,619,622],"tr",{},[610,611,612],"th",{},"#",[610,614,615],{},"テーマ",[610,617,618],{},"試したこと",[610,620,621],{},"結果",[610,623,624],{},"気づき",[626,627,628,650,667,683,700,720],"tbody",{},[607,629,630,634,637,644,647],{},[631,632,633],"td",{},"1",[631,635,636],{},"対象外明細取得",[631,638,639,641,642],{},[19,640,21],{}," に ",[19,643,25],{},[631,645,646],{},"未入力のみ返り、パラメータ無視",[631,648,649],{},"APIドキュメントを鵜呑みにせず実際のレスポンスを見る",[607,651,652,655,658,661,664],{},[631,653,654],{},"2",[631,656,657],{},"trans_list取得",[631,659,660],{},"fetchでHTML取得",[631,662,663],{},"tbodyが空（Ajax読み込み）",[631,665,666],{},"SSRかSPAかでスクレイピング方法が変わる",[607,668,669,672,674,677,680],{},[631,670,671],{},"3",[631,673,657],{},[631,675,676],{},"iframe + MutationObserver",[631,678,679],{},"成功。Ajax完了後のDOMを取得できた",[631,681,682],{},"fetchがダメならiframeに切り替える判断を早くする",[607,684,685,688,691,694,697],{},[631,686,687],{},"4",[631,689,690],{},"confirm置換",[631,692,693],{},"18箇所を一括でカスタムモーダルに",[631,695,696],{},"Escapeキーでresolveされないバグ発生",[631,698,699],{},"ネイティブAPIの暗黙の挙動を再実装し忘れがち",[607,701,702,705,708,711,714],{},[631,703,704],{},"5",[631,706,707],{},"Playwright API",[631,709,710],{},"fetchヘッダーにCookie手動設定",[631,712,713],{},"CSRF検証で弾かれた",[631,715,716,717,719],{},"HttpOnly + CSRFの組み合わせは ",[19,718,429],{}," 一択",[607,721,722,725,728,733,736],{},[631,723,724],{},"6",[631,726,727],{},"口座共通ルール",[631,729,730,732],{},[19,731,89],{}," で取得",[631,734,735],{},"共通ルールが返ってきた",[631,737,738],{},"ドキュメントにない仕様はDevToolsで観察して見つける",[36,740],{},[39,742,743],{"id":743},"学び",[745,746,747,752,755,760,766],"ul",{},[54,748,749,751],{},[19,750,21],{}," APIは未入力明細専用だった。APIの名前やパラメータだけ見て推測せず、レスポンスを実際に確認してから実装に入るべきだった。30分の回り道になった",[54,753,754],{},"fetchで取れないHTMLはiframe + MutationObserverで取る。Ajax依存ページのスクレイピングはfetchを試す前にページのNetwork通信を見て判断した方が速い",[54,756,757,759],{},[19,758,33],{}," → カスタムモーダルの置換では、Escapeキー・タブキー・オーバーレイクリックなどネイティブダイアログの暗黙の挙動を全て洗い出してから実装に入るべきだった",[54,761,762,763,765],{},"PlaywrightでHttpOnlyセッションが必要なAPIを叩くなら、",[19,764,429],{}," 内でfetchを実行する。Node.js側からリクエストを組み立てようとするとCookie/CSRFの壁に当たる",[54,767,768],{},"Chrome DevTools MCP経由の実機テストは、コードを書いて即座にブラウザで確認できるのでフィードバックループが速い",[770,771,772],"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 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 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 .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}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":165,"searchDepth":179,"depth":179,"links":774},[775,779,785,786,790,794,795],{"id":41,"depth":179,"text":41,"children":776},[777,778],{"id":45,"depth":222,"text":46},{"id":84,"depth":222,"text":84},{"id":102,"depth":179,"text":103,"children":780},[781,783],{"id":106,"depth":222,"text":782},"journalizing_suggestions の status=ignored は無視される",{"id":135,"depth":222,"text":784},"trans_list のfetchが空 → iframe方式に転換",{"id":325,"depth":179,"text":325},{"id":362,"depth":179,"text":787,"children":788},"confirm() 18箇所のカスタムモーダル置換",[789],{"id":374,"depth":222,"text":375},{"id":400,"depth":179,"text":401,"children":791},[792,793],{"id":404,"depth":222,"text":405},{"id":591,"depth":222,"text":591},{"id":599,"depth":179,"text":599},{"id":743,"depth":179,"text":743},"dev","会計ソフトA連携Chrome拡張に仕訳管理タブを新設。仕訳一括削除・対象外明細復帰のフルサイクル実装、confirm()18箇所のカスタムモーダル置換、Playwright API連携PoCまでを一日で完了","md",{},true,null,"/chrome-extension-journal-management","tax-assistant",false,"2026-03-31T00:00:00.000Z",{"title":5,"description":797},"2026-04/2026-03-31/chrome-extension-journal-management",[809,810,811,812,813],"Chrome拡張機能","クラウド会計","仕訳管理","Playwright","UI改善","memo","jfbzIh5zT1-2H5dkTJ_Q4j6PoYnBD7hEmrqKcUsqKlg",[],"https://log.eurekapu.com/og/blog/chrome-extension-journal-management.png?v=2026-03-31T00%3A00%3A00.000Z&title=Chrome%E6%8B%A1%E5%BC%B5%20%E4%BB%95%E8%A8%B3%E7%AE%A1%E7%90%86%E3%82%BF%E3%83%96%E3%81%AE%E6%96%B0%E8%A8%AD%20-%20%E4%BB%95%E8%A8%B3%E5%89%8A%E9%99%A4%E3%83%BB%E5%AF%BE%E8%B1%A1%E5%A4%96%E5%BE%A9%E5%B8%B0%E3%83%BB%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB%E7%BD%AE%E6%8F%9B%E3%83%BBPlaywright%20PoC&author=Kei%20Komatsu&sig=83c1fe649c15bfbb",1782528822674]