From 7c1b19e0f58189c116e0d4ea14cfea66a8758d98 Mon Sep 17 00:00:00 2001 From: JK Date: Wed, 25 Feb 2026 21:59:15 +0900 Subject: [PATCH] =?UTF-8?q?confluence-mdx:=20reverse=5Fsync=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=95=AD=EB=AA=A9=20=EC=88=98=20=EB=B6=88?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EC=8B=9C=20=EC=A0=84=EC=B2=B4=20inner=20X?= =?UTF-8?q?HTML=20=EC=9E=AC=EC=83=9D=EC=84=B1=20(#850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - `build_list_item_patches`에서 `len(old_items) != len(new_items)`일 때 빈 패치를 반환하여 리스트 항목 삭제/추가가 XHTML에 반영되지 않는 버그를 수정합니다. - parent mapping이 존재하는 경우, 전체 리스트를 `new_inner_xhtml`로 재생성하도록 변경합니다. ### Background release-notes/10.3.0-10.3.4.mdx에서 중복된 리스트 항목을 삭제했으나, reverse_sync verify 시 XHTML에서 해당 항목이 제거되지 않아 verify가 실패했습니다. ## Related tickets & links - split/ko-proofread-20260221-release-notes 브랜치의 verify 실패 건 ## Added/updated tests? - [x] Yes Co-Authored-By: Claude Opus 4.6 --- .../bin/reverse_sync/patch_builder.py | 13 +++++ .../tests/test_reverse_sync_patch_builder.py | 56 +++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/confluence-mdx/bin/reverse_sync/patch_builder.py b/confluence-mdx/bin/reverse_sync/patch_builder.py index 3e3310d47..3eac4693e 100644 --- a/confluence-mdx/bin/reverse_sync/patch_builder.py +++ b/confluence-mdx/bin/reverse_sync/patch_builder.py @@ -525,6 +525,19 @@ def build_list_item_patches( old_items = split_list_items(change.old_block.content) new_items = split_list_items(change.new_block.content) if len(old_items) != len(new_items): + # 항목 수가 다르면 (삭제/추가) 전체 리스트 inner XHTML 재생성 + parent = None + if mdx_to_sidecar is not None and xpath_to_mapping is not None: + parent = find_mapping_by_sidecar( + change.index, mdx_to_sidecar, xpath_to_mapping) + if parent is not None: + new_inner = mdx_block_to_inner_xhtml( + change.new_block.content, change.new_block.type) + return [{ + 'xhtml_xpath': parent.xhtml_xpath, + 'old_plain_text': parent.xhtml_plain_text, + 'new_inner_xhtml': new_inner, + }] return [] # sidecar에서 parent mapping 획득 diff --git a/confluence-mdx/tests/test_reverse_sync_patch_builder.py b/confluence-mdx/tests/test_reverse_sync_patch_builder.py index ab16c4e49..b5cd9850b 100644 --- a/confluence-mdx/tests/test_reverse_sync_patch_builder.py +++ b/confluence-mdx/tests/test_reverse_sync_patch_builder.py @@ -313,7 +313,7 @@ def test_path1_sidecar_match_child_resolved(self): assert 'updated child' in patches[0]['new_plain_text'] # Path 2: sidecar 매칭 → children 있음 → child 해석 실패 - # → 텍스트 불일치 → list 분리 (반환 빈 = item 수 불일치) + # → 텍스트 불일치 → list 분리 (item 수 불일치 → inner XHTML 재생성) def test_path2_sidecar_match_child_fail_list_split(self): parent = _make_mapping('p1', 'totally different parent', xpath='ul[1]', type_='list', children=['c1']) @@ -321,7 +321,7 @@ def test_path2_sidecar_match_child_fail_list_split(self): mappings = [parent, child] xpath_to_mapping = {m.xhtml_xpath: m for m in mappings} - # list type with different item count → returns [] + # list type with different item count → new_inner_xhtml 재생성 change = _make_change( 0, '- item one\n- item two', '- item one\n- item two\n- item three', type_='list') @@ -331,8 +331,10 @@ def test_path2_sidecar_match_child_fail_list_split(self): [change], [change.old_block], [change.new_block], mappings, mdx_to_sidecar, xpath_to_mapping) - # item count mismatch → build_list_item_patches returns [] - assert patches == [] + # item count mismatch → parent mapping으로 전체 리스트 inner XHTML 재생성 + assert len(patches) == 1 + assert 'new_inner_xhtml' in patches[0] + assert patches[0]['xhtml_xpath'] == 'ul[1]' # Path 3: sidecar 매칭 → children 있음 → child 해석 실패 # → parent를 containing block으로 사용 @@ -625,13 +627,57 @@ def test_patches_changed_item_with_child(self): assert len(patches) == 1 assert 'new item' in patches[0]['new_plain_text'] - def test_item_count_mismatch_returns_empty(self): + def test_item_count_mismatch_without_parent_returns_empty(self): + """parent mapping이 없으면 item count 불일치 시 빈 패치를 반환한다.""" change = _make_change( 0, '- item one\n- item two', '- item one', type_='list') patches = build_list_item_patches(change, [], set(), {}, {}) assert patches == [] + def test_item_count_mismatch_with_parent_generates_inner_xhtml(self): + """리스트 항목 수가 달라지면(삭제/추가) new_inner_xhtml 패치를 생성해야 한다. + + 실제 사례: 10.3.0-10.3.4.mdx — 중복된 리스트 항목 삭제 + Original MDX: "* [DAC] DRM 연동\\n* [DAC] 기능 제공\\n* [DAC] DRM 연동\\n* [DAC] Default Privilege" + Improved MDX: "* [DAC] DRM 연동\\n* [DAC] 기능 제공\\n* [DAC] Default Privilege" + 현상: 중복 항목 삭제 시 build_list_item_patches가 빈 패치를 반환하여 + XHTML에서 중복이 제거되지 않음 + """ + parent = _make_mapping( + 'list-56', + '[DAC] DRM 연동 [DAC] 기능 제공 [DAC] DRM 연동 [DAC] Default Privilege', + xpath='ul[5]', + type_='list', + children=[], + ) + mappings = [parent] + xpath_to_mapping = {m.xhtml_xpath: m for m in mappings} + id_to_mapping = {m.block_id: m for m in mappings} + + change = _make_change( + 0, + '* [DAC] DRM 연동\n* [DAC] 기능 제공\n* [DAC] DRM 연동\n* [DAC] Default Privilege\n', + '* [DAC] DRM 연동\n* [DAC] 기능 제공\n* [DAC] Default Privilege\n', + type_='list', + ) + mdx_to_sidecar = {0: _make_sidecar('ul[5]', [0])} + + patches = build_list_item_patches( + change, mappings, set(), + mdx_to_sidecar, xpath_to_mapping, id_to_mapping) + + assert len(patches) == 1, ( + 'item count 불일치 시 parent mapping이 있으면 ' + 'new_inner_xhtml 패치를 생성해야 함' + ) + assert 'new_inner_xhtml' in patches[0] + # 중복 항목이 제거된 결과여야 함 + inner = patches[0]['new_inner_xhtml'] + assert inner.count('DRM 연동') == 1, ( + f'중복 항목이 제거되어야 함: {inner!r}' + ) + def test_list_item_inline_code_added_generates_inner_xhtml(self): """리스트 항목에서 backtick 추가 시 new_inner_xhtml 패치를 생성한다.""" child = _make_mapping('c1', 'use kubectl command', xpath='ul[1]/li[1]/p[1]')