Skip to content

fix: 요소 안정 ID 도입 리뷰 확정 결함 수정 - #124

Open
lee-sihun wants to merge 19 commits into
feature/stable-element-idfrom
fix/stable-element-id-review
Open

fix: 요소 안정 ID 도입 리뷰 확정 결함 수정#124
lee-sihun wants to merge 19 commits into
feature/stable-element-idfrom
fix/stable-element-id-review

Conversation

@lee-sihun

Copy link
Copy Markdown
Member

#123 리뷰에서 확정된 결함들을 고칩니다. feature/stable-element-id에서 분기했고 base도 그 브랜치입니다 — #123을 머지하기 전에 이 PR을 먼저 머지하면 됩니다.

배경

#123의 코드 리뷰에서 후보 55건을 수집해 32건을 적대적으로 검증했고, 24건이 확정됐습니다. 이 PR은 그중 실사용에 영향이 있는 HIGH 5건 + MEDIUM 4건을 고칩니다. 각 수정에는 회귀 테스트를 붙였고, 여러 건은 수정을 되돌려 테스트가 실제로 실패하는지 확인했습니다.

백엔드 (Rust)

v1 플러그인 호환 계약 복원pluginWriteGateway가 약속한 "id 없는 레거시 입력 장기 수용"이 실제로는 깨져 있었습니다.

  • id 없는 위치 단독 패치가 전부 거부되던 문제: 값이 바뀐 요소마다 새 UUID가 발급돼 id 순서가 흔들리고, 새로 추가된 paired 검사가 통상적인 플러그인 위치 업데이트를 PAIRED_UPDATE_REQUIRED(비재시도)로 막았습니다. keys를 동반하지 않는 패치는 validate_paired_update가 형태 불변을 보장하므로, 그 경로에 한해 index를 신원으로 삼아 제자리 id를 먼저 확정합니다.
  • 슬롯 불변인데 값만 바뀌면 ID가 재발급되던 문제: 슬롯 승계 웨이브가 모두 값 일치를 요구했습니다. 값을 무시하고 같은 모드·같은 index의 슬롯 일치만 보는 웨이브를 추가하고, 우연한 값 일치보다 앞에 둡니다.
  • 읽은 객체를 복사해 요소를 복제하면 하드 거부되던 문제: id가 플러그인 읽기에 노출되면서 관용적인 복사-덧붙이기 패턴이 DUPLICATE_ELEMENT_ID를 받았습니다. v1에서는 소유가 어긋나거나 중복된 id를 거부 대신 비워 승계·신규 발급 경로로 넘깁니다(형식 오류는 호출부 버그 신호이므로 거부 유지). 타 컬렉션 id 이식도 함께 막힙니다.
  • ID가 모드 간에 무음으로 이동하던 문제: 값 매칭이 전 모드를 사전순으로 훑었습니다. 같은 모드 우선으로 정렬합니다.

store 오염 경로 차단stat/graph/knob_positions_update 레거시 커맨드가 id를 전혀 검증하지 않아 빈 id가 canonical store에 들어갈 수 있었고, 그 뒤로는 네이티브 컬렉션을 건드리는 모든 편집이 MISSING_ELEMENT_ID로 거부됐습니다(오염 컬렉션과 무관한 편집까지). 레거시 트랜잭션 경계에서 선언된 컬렉션에 한해 백필합니다.

프리셋 관용 복원 — 프리셋 로드가 검증 직전 전 요소를 rekey해서 한도 초과·라벨 길이·그림자 등 그랜드파더링 조회가 전부 실패했고, 관용 상태를 가진 사용자는 자기 프리셋 백업을 다시 불러올 수 없었습니다. 관용 신원 기준을 GrandfatherKeying으로 명시해 프리셋 트랜잭션에서만 자리 기준으로 판정합니다(일반 편집 경로는 ID 기준 유지 — 새 요소가 남의 관용을 물려받지 않습니다). 탭 프리셋이 key_positions만 무조건 rekey하던 것도 기록 여부로 게이트하고, 미기록 컬렉션은 빈 항목만 백필합니다.

프론트엔드

  • 붙여넣기 중 Delete가 원본을 지우던 문제: 선택 갱신이 커밋 라운드트립 뒤로 밀려 사본이 보이는데도 선택은 원본을 가리켰습니다. eager 적용 직후 동기 구간으로 되돌립니다.
  • 분리 패널에서 레이어 드롭이 오동작하던 문제: 새 buildLiveLayerModel이 창 인지 selector 대신 raw store를 읽어, 분리 패널에서는 플러그인 행이 드롭 기준 모델에서 통째로 빠졌습니다(플러그인 행 드래그는 무음 no-op, 네이티브 드롭은 z 재부여 오류). 결정론적으로 재현되던 회귀입니다.
  • 백엔드 ID 재발급 시 선택이 사라지던 문제: id 조회 실패 시 즉시 제거했습니다. 그 자리 요소가 id만 빼고 동일하면 새 id로 재채택하고, 내용이 다르면 기존처럼 선택을 풉니다.
  • 붙여넣기가 복사본의 상하 순서를 뒤집던 문제: 블록이 타입별 payload 순서로 조립되는데 배열 위치가 곧 최종 z였습니다. 원본 z 내림차순으로 정렬합니다.
  • 드롭이 엉뚱한 그룹에 편입되던 문제: 아이템 드롭이 index만 live 모델로 재해석하고 그룹은 캡처값을 그대로 썼습니다. 재유도하되, 접힌 그룹 헤더 드롭은 그룹 편입이 명시적 의도이므로 제외합니다.
  • 레이어 드롭이 반쪽만 저장되던 문제: 영속되는 플러그인 z 쓰기가 fail-closed 게이트보다 먼저 실행됐습니다. 게이트 뒤로 옮기고, 러너가 실제로 receipt를 되돌린 경우에만 CAS로 복원합니다(편입 후 transient 실패는 재시도가 소유하므로 건드리지 않습니다).

검증

cargo test 497건, cargo clippy 경고 0, cargo fmt 클린, tsc --noEmit 0건, vitest 1222건, lint 신규 경고 0건. 플러그인 API 문서는 변경된 id 의미론에 맞춰 en/ko 양쪽을 갱신했습니다.

이번 PR에서 제외한 것

혼합 선택의 "맨 앞으로/맨 뒤로" 순서(MEDIUM): 네이티브와 플러그인을 함께 선택해 z를 옮기면 선택 순서와 무관하게 네이티브가 항상 플러그인 위에 놓입니다. 최소 수정은 기존의 결정론을 없애면서 선택 순서 보장도 주지 못해 어느 쪽도 아닌 상태가 됩니다. 제대로 고치려면 applyZOrderByIds에 명시적 순서 인자를 추가하고 플러그인·네이티브를 같은 z 밴드로 묶어야 하는데, 공유 런타임과 호출부 8곳에 영향이 가므로 별도 PR이 적절합니다.

리뷰에서 함께 나온 저심각도 항목(중복 id keeper 선정 규칙의 문서 정밀도, buildLayerItems의 tabId 필터 부재 등 기존 결함)도 후속으로 남깁니다.

🤖 Generated with Claude Code

lee-sihun and others added 19 commits August 13, 2026 15:58
id 없는 v1 패치에서 값 매칭만으로 ID를 승계하면 두 가지 문제가 생긴다.
같은 길이 positions 단독 패치에서 값이 바뀐 요소가 새 ID를 받아 id 순서가
흔들리고(paired 검사가 통상적인 플러그인 위치 업데이트를 거부), 값 매칭이
전 모드를 훑어 한 모드의 ID가 다른 모드 요소로 옮겨간다.

승계 순서를 같은 모드 index(길이 동일) → 같은 모드 값 → 전역 값 폴백으로
정렬한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
paired v1 패치의 승계 웨이브가 모두 값 일치를 요구해, 키 슬롯은 그대로인데
위치 값만 바뀐 요소가 ID를 상속받지 못하고 새로 발급받았다. 이동이 신원
회전으로 번져 선택·바인딩이 끊긴다.

값을 무시하고 같은 모드·같은 index의 슬롯 일치만 보는 웨이브를 값 폴백
앞에 추가한다. index를 고정해 중복 슬롯끼리 교차 승계하지 않는다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
id가 플러그인 읽기에 노출되면서, 읽어온 위치 객체를 복사해 요소를 늘리는
관용 패턴이 DUPLICATE_ELEMENT_ID 비재시도 거부를 받게 됐다. 타 컬렉션의
id를 실은 패치도 전역 유일성 검사만 통과하면 신원이 이식됐다.

v1 경로에서 소유가 어긋나거나 패치 안에서 중복된 id는 비워서 승계·신규 발급
경로로 넘긴다. 중복은 canonical 자리를 원본으로 보고 사본만 비운다. 형식이
잘못된 id는 호출부 버그 신호이므로 거부를 유지한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stat/graph/knob_positions_update 같은 레거시 커맨드는 호출부가 넘긴 레코드를
검증 없이 대입해, id가 빈 요소가 canonical store에 들어가 영속·전파됐다.
그 뒤로는 네이티브 컬렉션을 건드리는 모든 v2 커밋이 MISSING_ELEMENT_ID로
거부되어 오염 컬렉션과 무관한 편집까지 막혔다.

레거시 트랜잭션 경계에서 선언된 컬렉션에 한해 빈·중복·형식 오류 id를
채운다. 제자리 id 치환이라 keys[i]↔keyPositions[i] 결합은 유지된다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
rekey_tab_preset_elements가 stat/graph/knob은 기록 여부로 게이트하면서
key_positions만 무조건 true를 넘겼다. keys만 담긴 탭 프리셋에서 merge가
기존 위치를 값 그대로 되삽입해도 모든 키의 신원이 회전해, 선택·진행 중인
picker 완료·크로스 윈도우 선택이 한꺼번에 끊겼다.

네 컬렉션 모두 기록 여부로 게이트하고, 프리셋이 위치를 주지 않은 컬렉션은
정렬이 덧붙인 빈 항목만 백필한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
한도 초과 관용이 (mode,index)에서 안정 ID 조회로 바뀌었는데, 프리셋 로드는
검증 직전 모든 요소의 id를 재발급한다. 값이 그대로여도 관용 상대를 찾지
못해, 그랜드파더된 값을 가진 store에서 자기 프리셋 백업을 다시 불러오면
전체 임포트가 비재시도 실패했다. 프리셋 스냅샷 복원(undo)도 같은 기전이다.

관용 신원 기준을 GrandfatherKeying으로 명시하고 프리셋 트랜잭션에서만
(모드, index) 짝짓기를 쓴다. 일반 편집 경로는 ID 기준을 그대로 유지해
새 요소가 남의 관용을 물려받지 못한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
buildLiveLayerModel이 렌더 목록과 달리 창 인지 selector 대신 raw elements를
읽었다. 분리 속성 패널 창은 panelElements만 채우므로, 모든 드롭 커밋의 기준
모델에서 plugin 행이 통째로 빠졌다. plugin 행 드래그는 무음 no-op이 되고
native 드롭은 plugin이 빠진 목록으로 z를 재부여해 기존 plugin과 겹쳤다.

렌더 목록과 같은 selectPropertyPanelPluginElements를 쓴다. Grid와
rotateTargetPluginSessions는 패널 창에 도달하지 않는 경로임을 주석으로 남긴다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
id 조회가 실패하면 곧바로 선택을 제거해, 요소는 그대로인데 신원만 재발급된
경우(탭 프리셋 rekey, v1 어댑터 재발급)에도 사용자의 선택이 사라졌다.
base의 조기 반환이 없어져 모든 문서 적용마다 이 판정이 돌기에 더 자주 노출됐다.

해당 타입에 구조 경계가 없으면(길이 불변) 같은 자리의 새 id로 재채택한다.
길이가 바뀐 경우는 기존 경계 판정을 그대로 따른다. 합성 id 판정도 문자열
조립 대신 공유 isSyntheticElementId를 쓴다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
선택 이동이 커밋 라운드트립 뒤 성공 게이트로 밀려나, 사본이 이미 화면에
보이는데도 선택은 원본을 가리켰다. 그 사이 Delete를 누르면 사본이 아니라
원본이 지워지고, 커밋이 편입 후 실패하면 선택이 영영 옮겨가지 않았다.

eager 적용 직후 동기 구간에서 동결 id로 선택을 옮긴다. 실패로 eager가
롤백되면 다음 문서 적용의 선택 재조정이 정리한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
붙여넣기 블록이 keys→stats→graphs→knobs→plugins의 타입별 payload 순서로
조립되는데 배열 위치가 곧 최종 z라, 원본에서 stat이 key 위에 있어도
붙여넣으면 key가 위로 올라갔다. 마퀴·범위 선택은 배열 인덱스 순으로 담기
때문에 같은 타입 안에서도 뒤집혔다.

블록 내부를 동결 zIndex 내림차순으로 정렬하고 동률은 payload 순서를 유지한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
아이템 드래그 드롭이 index는 live 모델로 재해석하면서 targetGroupId만
mousemove 캡처값을 그대로 넘겼다. 앵커 행이 그 사이 그룹을 떠나도 가드는
그룹 생존만 보므로, 드롭한 요소가 사용자가 넣은 적 없는 그룹에 편입되고
드롭 위치보다 위로 튀어 올랐다.

그룹 드래그 경로와 같이 resolveItemDropTarget으로 재유도한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
commitDropIntents가 영속되는 plugin z authority 쓰기를 eager 게이트보다
먼저 무조건 실행했다. 동시 변경으로 게이트가 닫히면 native 의도는 전부
드롭되는데 plugin만 새 깊이로 옮겨진 반쪽 순서가 저장됐다.

plugin-only 드롭은 그대로 즉시 쓰고, native 혼합은 게이트를 통과한 뒤에
쓴다. 편입 후 실패·스킵에서는 드래그 전 z로 되돌린다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
중복 id가 거부에서 사본 재발급으로 바뀐 것을 en/ko 양쪽에 기술한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
index 승계를 모든 컬렉션에 적용하면 형태 제약이 없는 stat/graph/knob에서
같은 길이의 삭제+추가가 새 요소에 삭제된 요소의 신원을 넘긴다. 신원 기준을
ID로 옮긴 목적(새 요소가 남의 관용을 물려받지 않는 것)과 정면으로 어긋난다.
index 승계를 keys 미동반 keyPositions 패치로 한정한다 - 그 경로만
validate_paired_update가 형태 불변을 보장한다.

또한 슬롯 전용 웨이브를 값 재바인딩 웨이브보다 앞에 둔다. 슬롯 고정이 우연한
값 일치보다 강한 신원 단서이고, 그래야 두 키의 좌표 맞바꿈이 keys 동반 여부와
무관하게 같은 결과를 낸다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
리뷰 지적 반영.

targetGroupId 재유도가 접힌 그룹 헤더 하단 드롭까지 덮어써, 그룹 편입이라는
명시적 사용자 의도가 사라졌다(접힌 그룹은 자식 행을 렌더하지 않으므로
재유도는 항상 undefined를 낸다). 헤더 앵커가 있는 경로는 재유도에서 제외한다 -
헤더 생존은 앵커 해석이 이미 확인한다.

plugin z 복원도 두 가지를 고친다. 복원 전 현재 값을 확인해 우리가 쓴 값일
때만 되돌리고(native 속성 receipt와 같은 CAS 규칙), 러너가 실제로 receipt를
되돌린 경우에만 복원한다. 편입 후 transient 실패는 pendingLocal 재시도가
소유하므로 되돌리면 native만 새 순서로 남는 반쪽 상태가 된다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
길이 불변만으로 재채택하면 삭제+삽입이 한 문서에 정산된 경우처럼 자리의
요소가 교체된 상황에서도 선택이 무관한 요소에 재결합된다. 붙여넣기가
실패해 죽은 선택이 남은 뒤 같은 자리에 새 요소가 생기면 같은 요소가 선택에
중복으로 들어가 이동 폭이 배가 되기도 한다.

그 자리 요소가 id만 빼고 동일할 때만 재채택하고, 다르면 기존처럼 선택을
푼다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
한도 초과 관용만 자리 기준으로 바꿔서, 키 라벨 길이 관용과 그림자·노브
수치 같은 구조 위반 관용은 여전히 id 조회에 남아 있었다. 프리셋 로드가
id를 재발급하므로 그 관용들은 전부 소실되고, 그림자나 과길이 라벨을 가진
store는 여전히 자기 프리셋 백업을 되돌릴 수 없었다.

라벨 조회를 같은 헬퍼 규칙으로 통일하고, 구조 위반은 후보 id를 같은 자리의
현재 id로 치환해 한 번 더 대조한다. 일반 편집 경로는 치환표가 비어 있어
동작이 그대로다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
탭 프리셋이 위치를 준 컬렉션만 재발급하도록 바뀐 것과, 중복 id가 거부 대신
사본 재발급으로 처리되는 것을 Keys 페이지 en/ko에 반영한다.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eun-yeon

Copy link
Copy Markdown
Member

살려줘

@lee-sihun

Copy link
Copy Markdown
Member Author

할 수 있어

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants