[{"data":1,"prerenderedAt":647},["ShallowReactive",2],{"content-/mf-journal-rule-tab-refactoring":3,"all-pages-for-dir":645,"og-image-/mf-journal-rule-tab-refactoring":646},{"id":4,"title":5,"body":6,"category":626,"description":627,"extension":628,"meta":629,"navigation":203,"ogImage":630,"path":631,"project_name":632,"published":633,"publishedAt":634,"seo":635,"stem":636,"tags":637,"todo":643,"unpublished":633,"updatedAt":630,"__hash__":644},"pages/2026-03/2026-03-29/mf-journal-rule-tab-refactoring.md","Chrome拡張 自動仕訳ルールの独立タブ化とルールID照合バグ修正 - NFKC正規化で29件未特定を解消",{"type":7,"value":8,"toc":610},"minimark",[9,14,18,21,26,29,32,67,75,77,81,85,88,91,94,97,125,127,131,134,138,145,298,301,305,316,418,421,425,428,431,445,448,451,454,456,459,462,465,550,560,562,565,591,593,596,603,606],[10,11,13],"h1",{"id":12},"chrome拡張-自動仕訳ルールの独立タブ化とルールid照合バグ修正","Chrome拡張 自動仕訳ルールの独立タブ化とルールID照合バグ修正",[15,16,17],"p",{},"自動仕訳ルールの管理UIがエクスポート/インポートの中に埋もれていて、毎回タブを切り替えてスクロールしていた。「設定/エクスポート/インポート/自動仕訳ルール/ログ」の5タブに再編し、ルールを独立タブに引き出した。その過程で、ルールID照合が29件分失敗しているバグを発見し、原因を3つ潰して全件解消した一日の記録。",[19,20],"hr",{},[22,23,25],"h2",{"id":24},"タブ構成の再編-3タブから5タブへ","タブ構成の再編: 3タブから5タブへ",[15,27,28],{},"もともと「設定/エクスポート/ログ」の3タブ構成で、エクスポートタブの中にインポートと自動仕訳ルールの機能がすべて詰まっていた。事業者が増えるにつれスクロール量が膨らみ、目的の操作にたどり着くまでに手が止まる。",[15,30,31],{},"新しいタブ構成:",[33,34,35,43,49,55,61],"ol",{},[36,37,38,42],"li",{},[39,40,41],"strong",{},"設定"," -- 認証情報とグローバル設定",[36,44,45,48],{},[39,46,47],{},"エクスポート"," -- データ取得（プル系操作）",[36,50,51,54],{},[39,52,53],{},"インポート"," -- データ書き込み（プッシュ系操作）",[36,56,57,60],{},[39,58,59],{},"自動仕訳ルール"," -- ルールのドライラン・同期",[36,62,63,66],{},[39,64,65],{},"ログ"," -- 実行履歴",[15,68,69,70,74],{},"タブ位置は ",[71,72,73],"code",{},"localStorage"," に保存し、拡張を開き直しても前回のタブが復元される。",[19,76],{},[22,78,80],{"id":79},"uiデザインの試行錯誤","UIデザインの試行錯誤",[82,83,84],"h3",{"id":84},"レイアウト遍歴",[15,86,87],{},"最初は3行の縦並びレイアウトを試した。エクスポートとインポートが同じ見た目で並ぶと、どちらのボタンを押しているのか目が迷う。",[15,89,90],{},"次に2カラム5行のグリッドレイアウトに変えた。情報密度は上がったが、事業者ごとのカードが横に広がりすぎてスクロールが横方向にも発生した。",[15,92,93],{},"最終的に落ち着いたのは、設定画面風のカードスタイル。枠線と角丸で領域を区切り、エクスポート領域とインポート領域を背景色で視覚的に分離した。",[82,95,96],{"id":96},"ボタンと入力欄の細かい調整",[98,99,100,111,122],"ul",{},[36,101,102,103,106,107,110],{},"エクスポートボタンに ",[71,104,105],{},"↓","、同期実行ボタンに ",[71,108,109],{},"↑"," の矢印アイコンを追加。プル/プッシュの方向が一目で伝わる",[36,112,113,114,117,118,121],{},"URL入力欄のフォントを ",[71,115,116],{},"9px Consolas"," に変更。スプレッドシートURLの構造（",[71,119,120],{},"/d/{id}/edit","）がそのまま読める",[36,123,124],{},"ヘルプモーダルの記述を修正 -- 会計サービスの仕様上「ルール変更」APIは存在せず、削除+追加で対応する旨を明記",[19,126],{},[22,128,130],{"id":129},"ルールid照合-29件未特定バグの解消","ルールID照合: 29件未特定バグの解消",[15,132,133],{},"自動仕訳ルールの同期処理では、CSVからエクスポートしたルール一覧とAPIから取得したルール一覧を突合し、各ルールにIDを紐付ける。このID照合で29件が「未特定」のまま残っていた。",[82,135,137],{"id":136},"原因1-同一マッチキーの複数ルール","原因1: 同一マッチキーの複数ルール",[15,139,140,141,144],{},"ルールの照合キーは「勘定科目+摘要+取引先」の組み合わせで構成していた。ところが同じ組み合わせで複数のルールが存在するケースがあり、",[71,142,143],{},"Map"," にセットすると後勝ちで上書きされていた。",[146,147,152],"pre",{"className":148,"code":149,"language":150,"meta":151,"style":151},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// Before: 上書きで消える\nruleIdMap.set(matchKey, rule.id);\n\n// After: 1つのキーに複数IDを保持\nif (!ruleIdMap.has(matchKey)) {\n  ruleIdMap.set(matchKey, []);\n}\nruleIdMap.get(matchKey).push(rule.id);\n","javascript","",[71,153,154,163,198,205,211,242,261,267],{"__ignoreMap":151},[155,156,159],"span",{"class":157,"line":158},"line",1,[155,160,162],{"class":161},"sxvE3","// Before: 上書きで消える\n",[155,164,166,170,174,178,181,184,187,190,192,195],{"class":157,"line":165},2,[155,167,169],{"class":168},"s4oTP","ruleIdMap",[155,171,173],{"class":172},"shFtX",".",[155,175,177],{"class":176},"senZ8","set",[155,179,180],{"class":172},"(",[155,182,183],{"class":168},"matchKey",[155,185,186],{"class":172},",",[155,188,189],{"class":168}," rule",[155,191,173],{"class":172},[155,193,194],{"class":168},"id",[155,196,197],{"class":172},");\n",[155,199,201],{"class":157,"line":200},3,[155,202,204],{"emptyLinePlaceholder":203},true,"\n",[155,206,208],{"class":157,"line":207},4,[155,209,210],{"class":161},"// After: 1つのキーに複数IDを保持\n",[155,212,214,218,221,225,227,229,232,234,236,239],{"class":157,"line":213},5,[155,215,217],{"class":216},"sHkkW","if",[155,219,220],{"class":172}," (",[155,222,224],{"class":223},"stQ0i","!",[155,226,169],{"class":168},[155,228,173],{"class":172},[155,230,231],{"class":176},"has",[155,233,180],{"class":172},[155,235,183],{"class":168},[155,237,238],{"class":172},"))",[155,240,241],{"class":172}," {\n",[155,243,245,248,250,252,254,256,258],{"class":157,"line":244},6,[155,246,247],{"class":168},"  ruleIdMap",[155,249,173],{"class":172},[155,251,177],{"class":176},[155,253,180],{"class":172},[155,255,183],{"class":168},[155,257,186],{"class":172},[155,259,260],{"class":172}," []);\n",[155,262,264],{"class":157,"line":263},7,[155,265,266],{"class":172},"}\n",[155,268,270,272,274,277,279,281,284,287,289,292,294,296],{"class":157,"line":269},8,[155,271,169],{"class":168},[155,273,173],{"class":172},[155,275,276],{"class":176},"get",[155,278,180],{"class":172},[155,280,183],{"class":168},[155,282,283],{"class":172},").",[155,285,286],{"class":176},"push",[155,288,180],{"class":172},[155,290,291],{"class":168},"rule",[155,293,173],{"class":172},[155,295,194],{"class":168},[155,297,197],{"class":172},[15,299,300],{},"マルチマップに変えて、消費済みIDを除外しながらマッチングする形にした。",[82,302,304],{"id":303},"原因2-全角半角の文字コード差異","原因2: 全角/半角の文字コード差異",[15,306,307,308,311,312,315],{},"CSVに含まれるカタカナや記号が全角で、APIレスポンスでは半角になっているケースがあった。",[71,309,310],{},"\"ｶ\""," と ",[71,313,314],{},"\"カ\""," が別文字として扱われ、キーが一致しない。",[146,317,319],{"className":148,"code":318,"language":150,"meta":151,"style":151},"// NFKC正規化で全角/半角を統一\nconst normalize = (s) => s.normalize('NFKC');\nconst matchKey = normalize(`${account}|${summary}|${partner}`);\n",[71,320,321,326,370],{"__ignoreMap":151},[155,322,323],{"class":157,"line":158},[155,324,325],{"class":161},"// NFKC正規化で全角/半角を統一\n",[155,327,328,331,334,337,339,342,345,348,351,353,356,358,362,366,368],{"class":157,"line":165},[155,329,330],{"class":223},"const",[155,332,333],{"class":176}," normalize",[155,335,336],{"class":172}," =",[155,338,220],{"class":172},[155,340,341],{"class":168},"s",[155,343,344],{"class":172},")",[155,346,347],{"class":172}," =>",[155,349,350],{"class":168}," s",[155,352,173],{"class":172},[155,354,355],{"class":176},"normalize",[155,357,180],{"class":172},[155,359,361],{"class":360},"sMJiu","'",[155,363,365],{"class":364},"sdGka","NFKC",[155,367,361],{"class":360},[155,369,197],{"class":172},[155,371,372,374,377,379,381,383,386,389,392,395,398,400,403,405,407,409,412,414,416],{"class":157,"line":200},[155,373,330],{"class":223},[155,375,376],{"class":168}," matchKey",[155,378,336],{"class":172},[155,380,333],{"class":176},[155,382,180],{"class":172},[155,384,385],{"class":360},"`",[155,387,388],{"class":216},"${",[155,390,391],{"class":364},"account",[155,393,394],{"class":216},"}",[155,396,397],{"class":364},"|",[155,399,388],{"class":216},[155,401,402],{"class":364},"summary",[155,404,394],{"class":216},[155,406,397],{"class":364},[155,408,388],{"class":216},[155,410,411],{"class":364},"partner",[155,413,394],{"class":216},[155,415,385],{"class":360},[155,417,197],{"class":172},[15,419,420],{},"NFKC正規化を照合キー生成時に適用することで、文字コードの揺れを吸収した。",[82,422,424],{"id":423},"原因3-csvの商品名途中切れ","原因3: CSVの商品名途中切れ",[15,426,427],{},"CSVの摘要欄に入る商品名が長い場合、途中で切れていた。会計サービス側で自動登録されたルールは商品名がフルで入るため、完全一致では引っかからない。",[15,429,430],{},"2パスアルゴリズムで対処した:",[33,432,433,439],{},[36,434,435,438],{},[39,436,437],{},"1パス目",": NFKC正規化済みキーで完全一致",[36,440,441,444],{},[39,442,443],{},"2パス目",": 1パス目で未特定のルールに対し、摘要の先頭15文字での前方一致",[15,446,447],{},"当初は3パス目のフォールバック（さらに緩い条件）も実装したが、NFKC正規化の導入で2パス目までに全件がマッチするようになったため、3パス目は削除した。",[82,449,450],{"id":450},"結果",[15,452,453],{},"29件未特定 → 0件。照合ログを出力して、全ルールにIDが紐付いていることを確認した。",[19,455],{},[22,457,458],{"id":458},"グローバルボタンロック",[15,460,461],{},"処理中に別の事業者のボタンを押すと、会計サービスのセッション単位でCTI（事業者コンテキスト）が切り替わり、実行中の処理と競合する。",[15,463,464],{},"全事業者のボタンをグローバルにdisableするロック機構を入れた。処理開始時にロックを取得し、完了（成功/失敗問わず）時に解放する。",[146,466,468],{"className":148,"code":467,"language":150,"meta":151,"style":151},"function setGlobalLock(locked) {\n  document.querySelectorAll('[data-lockable]')\n    .forEach(btn => { btn.disabled = locked; });\n}\n",[71,469,470,487,509,546],{"__ignoreMap":151},[155,471,472,475,478,480,483,485],{"class":157,"line":158},[155,473,474],{"class":223},"function",[155,476,477],{"class":176}," setGlobalLock",[155,479,180],{"class":172},[155,481,482],{"class":168},"locked",[155,484,344],{"class":172},[155,486,241],{"class":172},[155,488,489,492,494,497,499,501,504,506],{"class":157,"line":165},[155,490,491],{"class":168},"  document",[155,493,173],{"class":172},[155,495,496],{"class":176},"querySelectorAll",[155,498,180],{"class":172},[155,500,361],{"class":360},[155,502,503],{"class":364},"[data-lockable]",[155,505,361],{"class":360},[155,507,508],{"class":172},")\n",[155,510,511,514,517,519,522,524,527,530,532,535,537,540,543],{"class":157,"line":200},[155,512,513],{"class":172},"    .",[155,515,516],{"class":176},"forEach",[155,518,180],{"class":172},[155,520,521],{"class":168},"btn",[155,523,347],{"class":172},[155,525,526],{"class":172}," {",[155,528,529],{"class":168}," btn",[155,531,173],{"class":172},[155,533,534],{"class":168},"disabled",[155,536,336],{"class":172},[155,538,539],{"class":168}," locked",[155,541,542],{"class":172},";",[155,544,545],{"class":172}," });\n",[155,547,548],{"class":157,"line":207},[155,549,266],{"class":172},[15,551,552,555,556,559],{},[71,553,554],{},"CLAUDE.md"," にも「新しいボタンを追加する際は ",[71,557,558],{},"data-lockable"," 属性を付与すること」の注意書きを追記した。",[19,561],{},[22,563,564],{"id":564},"その他の修正",[98,566,567,573,579,585],{},[36,568,569,572],{},[39,570,571],{},"設定画面の整理",": 自動仕訳ルールURL欄を設定タブから削除。ルール管理は専用タブに一本化し、情報の重複を排除",[36,574,575,578],{},[39,576,577],{},"URL空欄時の挙動修正",": エクスポートURL欄を空にして保存すると、既存のスプレッドシートURLが消えた上に新規SSが自動作成されるバグを修正。空欄保存時はURL消去のみ行い、自動作成は走らせない",[36,580,581,584],{},[39,582,583],{},"ドライラン/同期ボタンの制御",": ルール管理URLが空欄のとき、ドライランと同期ボタンをdisableに。押しても何も起きないボタンが活性化しているのは混乱の元",[36,586,587,590],{},[39,588,589],{},"プレースホルダー改善",": URL入力欄に「未設定時はスプレッドシートを自動作成します」の説明を追加",[19,592],{},[22,594,595],{"id":595},"振り返り",[15,597,598,599,602],{},"29件の未特定バグは、ログに照合キーのペアを並べて出力したことで原因の切り分けが進んだ。目で見比べて「同じに見えるのに不一致」となった瞬間に文字コードを疑い、",[71,600,601],{},"charCodeAt"," でバイト列を確認して全角/半角の差異を突き止めた。NFKC正規化という一手で根本から片付いたのは気持ちがよかった。",[15,604,605],{},"タブの再編は、コードの移動量こそ多かったが判断は単純だった。一方でID照合は、コード変更量は少ないのに原因特定に時間を食った。「動かない」より「ほぼ動くが一部だけ合わない」問題のほうが、調査に手間がかかることを改めて実感した。",[607,608,609],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}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 .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}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 .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}",{"title":151,"searchDepth":165,"depth":165,"links":611},[612,613,617,623,624,625],{"id":24,"depth":165,"text":25},{"id":79,"depth":165,"text":80,"children":614},[615,616],{"id":84,"depth":200,"text":84},{"id":96,"depth":200,"text":96},{"id":129,"depth":165,"text":130,"children":618},[619,620,621,622],{"id":136,"depth":200,"text":137},{"id":303,"depth":200,"text":304},{"id":423,"depth":200,"text":424},{"id":450,"depth":200,"text":450},{"id":458,"depth":165,"text":458},{"id":564,"depth":165,"text":564},{"id":595,"depth":165,"text":595},"dev","会計サービス連携Chrome拡張のタブ構成を5タブに再編し、自動仕訳ルールを独立タブに分離。ルールID照合で29件未特定だったバグを、マルチマップ化・NFKC正規化・前方一致の2パスアルゴリズムで全件解消するまでの記録","md",{},null,"/mf-journal-rule-tab-refactoring","misc-dev",false,"2026-03-29T00:00:00.000Z",{"title":5,"description":627},"2026-03/2026-03-29/mf-journal-rule-tab-refactoring",[638,639,59,640,641,642],"Chrome拡張機能","クラウド会計","NFKC正規化","UI設計","バグ修正","memo","tuRXgzkx0qQXBwDrCGKKtz_Bp8uI5zhzOYagLdy6F0c",[],"https://log.eurekapu.com/og/blog/mf-journal-rule-tab-refactoring.png?v=2026-03-29T00%3A00%3A00.000Z&title=Chrome%E6%8B%A1%E5%BC%B5%20%E8%87%AA%E5%8B%95%E4%BB%95%E8%A8%B3%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E7%8B%AC%E7%AB%8B%E3%82%BF%E3%83%96%E5%8C%96%E3%81%A8%E3%83%AB%E3%83%BC%E3%83%ABID%E7%85%A7%E5%90%88%E3%83%90%E3%82%B0%E4%BF%AE%E6%AD%A3%20-%20NFKC%E6%AD%A3%E8%A6%8F%E5%8C%96%E3%81%A729%E4%BB%B6%E6%9C%AA%E7%89%B9%E5%AE%9A%E3%82%92%E8%A7%A3%E6%B6%88&author=Kei%20Komatsu&sig=bf7f6fa8a488e933",1782528821970]