Files
Gnuboard7/tests/scenarios/editor-resize-save-body-integrity.yaml
T
HeuJung de584ea93f fix(core): 자동바인딩 키입력이 편집기 본문을 서버 원본으로 되돌리던 문제
자동바인딩은 키입력마다 __g7PendingLocalState 에 저장소 A 기반 전체 스냅샷을
대입해 왔다. 그 base(parentFormContext.state)는 extendedDataContext useMemo 의
결과라, selfManaged 플러그인이 저장소 B 에만 쓴 뒤 memo 가 재계산되지 않은
구간에서는 편집 이전 스냅샷으로 고정된다. 이어지는 setLocal 이
currentSnapshot = pendingState || baseLocal 로 그것을 채택하면 저장소 B 가
통째 교체되어 편집분이 사라진다.

pending 은 getLocal 이 읽는 "화면과 같은 전체 스냅샷" 이므로, 렌더러가
_local 을 만드는 순서(dataContext._local → dynamicState → __g7ForcedLocalFields)를
그대로 따르게 합성한다. 방금 입력한 경로는 마지막에 다시 얹어 오버레이의 직전
값이 입력을 되돌리지 못하게 했다.

저장소 A 경로와 setLocal 의 base 우선순위는 건드리지 않았다. 전자는 2026-04-22 에
로그인 폼 email 손실로 철회된 자리이고, 후자는 _localInit 이 초기 데이터를
pending 에만 싣는 구간을 깨뜨린다.

engine-v1.63.3이 저장 클릭 경로를 고쳤다면 이번 수정은 그 앞의 키입력
경로를 고친다. 성립 조건에는 memo deps 와 무관한 리렌더가 선행해야 하며,
리사이즈 없는 대조군은 수정 전에도 정상이다(브라우저 실측).

트러블슈팅 246 사례 전수 판정: 영향 있음 0 · 개선 3 · 불변 10.
2026-09-01 19:57:24 +09:00

95 lines
5.5 KiB
YAML

feature: 폭 변경 후 저장 시 편집기 본문 보존 (공개 이슈 #130 / engine-v1.63.3)
description: |
엔진은 폼 상태를 React localDynamicState(저장소 A)와 globalState._local(저장소 B)에 이중
저장한다. setGlobalState 는 _local 을 얕게 병합하므로 `{ _local: X }` 는 B 를 통째 교체한다.
CKEditor 는 setLocal({ render:false, selfManaged:true }) 로 B 에만 본문을 쓴다 — 그 호출은
React 렌더를 한 번도 일으키지 않아 extendedDataContext useMemo 가 재계산되지 않고
context.state 가 입력 이전 스냅샷에 머문다. 여기에 브레이크포인트를 넘지 않는 폭 변경(19px)
이 겹치면 의존성 배열 없는 useLayoutEffect 가 __g7PendingLocalState 를 null 로 지우고,
저장 시 setState 의 base 가 stale A 로 떨어져 B 와 sequence 반환값이 함께 오염된다.
수정은 두 지점을 함께 고친다:
- B 동기화 base 를 live B(getGlobalState()._local) + 변경 키로
- 같은 분기의 반환값도 같은 canonical 결과로 (요청 body 는 B 가 아니라 이 반환값에서 온다)
두 경로 모두 addMissingLeafKeys 로 저장소 A 전용 키(loadingActions 등)를 보충한다.
증상이 조용하다는 점이 이 기능의 핵심이다 — 작성은 422, 수정은 성공 토스트와 함께 편집분
소실이며 콘솔 에러는 0건이다. 그래서 "저장됐다" 가 아니라 요청 body 의 content 를 본다.
test_files:
- resources/js/core/template-engine/__tests__/troubleshooting-state-setstate.test.ts
- resources/js/core/template-engine/__tests__/DynamicRenderer.resizePendingClear.test.tsx
- resources/js/core/__tests__/G7CoreGlobals.selfManagedPromotion.test.ts
- resources/js/core/template-engine/__tests__/DynamicRenderer.autoBindingPendingBase.test.tsx
- plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/admin/editor-resize-save-body-integrity.spec.ts
axes:
save_flow: [create, edit]
resize_kind: [same_breakpoint, crossed_breakpoint, none]
effects:
- request_body_content_matches_editor_getdata
- create_save_returns_2xx_not_422
- edit_save_persists_typed_addition
- canonical_local_keeps_editor_content_after_resize
- setstate_local_preserves_canonical_keys_when_pending_is_null
- save_without_resize_unaffected
- resize_save_emits_no_console_errors
- success_toast_only_when_content_actually_saved
- resize_clears_pending_local_state
- selfmanaged_setlocal_writes_canonical_without_render
- autobinding_keystroke_preserves_editor_content
- pending_snapshot_carries_forced_overlay
- typed_path_wins_over_forced_previous_value
sub_flows:
- id: control_no_resize
description: |
리사이즈 없이 저장하면 수정 전에도 정상이다 — 재현 조건이 폭 변경임을 고정한다.
이 축이 수정 전에 실패하면 결함 모델이 틀린 것이므로, 자동바인딩 → pending → setLocal
두 번째 소실 경로(사례 40 "인접 벡터")를 먼저 조사해야 한다.
effects: [save_without_resize_unaffected]
- id: silent_failure_surface
description: |
콘솔에도 토스트에도 드러나지 않던 결함이라 조용함 자체를 증거로 고정한다.
수정 화면은 성공 토스트가 뜨므로 화면 피드백으로는 판정할 수 없다 — DB 재조회로 본다.
effects: [resize_save_emits_no_console_errors, success_toast_only_when_content_actually_saved]
- id: canonical_store_integrity
description: |
pending 이 null 인 상태에서 setState(local) 을 태워도 저장소 B 의 본문이 남는지를
직접 본다. 요청 body 축과 별개로 B 쓰기 축을 독립 고정한다.
effects:
- canonical_local_keeps_editor_content_after_resize
- setstate_local_preserves_canonical_keys_when_pending_is_null
- id: trigger_halves
description: |
결함은 두 조각이 겹쳐야 성립한다. 각 조각을 단위 계층에서 따로 잠근다 —
① 폭 변경 리렌더가 __g7PendingLocalState 를 비운다(실물 렌더)
② selfManaged setLocal 이 렌더 없이 저장소 B 에만 쓴다(엔진 실물)
단위 계층에서는 소실 자체를 재현할 수 없으므로(TemplateApp 재구현 필요) 조각만 고정하고,
합류 지점은 사례 40 회귀 테스트와 E2E 가 담당한다.
effects:
- resize_clears_pending_local_state
- selfmanaged_setlocal_writes_canonical_without_render
- id: autobinding_after_resize
description: |
engine-v1.63.4 (사례 41) — 같은 뿌리의 **다른 방아쇠**. 폭 변경으로 pending 이 비워진 뒤
제목 등 자동바인딩 입력에 키를 치면, performStateUpdate 가 stale 한 저장소 A 스냅샷을
pending 에 싣고 이어지는 setLocal 이 그것을 base 로 채택해 저장소 B 를 통째 교체한다.
**저장 클릭 전에** 이미 편집분이 사라지므로 engine-v1.63.3 의 수정은 이 경로를 지나가지 않는다.
pending 은 getLocal() 이 읽는 "화면과 같은 전체 스냅샷" 이어야 하므로, 렌더러가
extendedDataContext._local 을 만드는 순서(dataContext._local → dynamicState →
__g7ForcedLocalFields)를 그대로 따르게 합성한다. 방금 입력한 경로는 마지막에 다시 얹어
오버레이의 직전 값이 입력을 되돌리지 못하게 한다(사례 11 계열 역방향 결함 방지).
effects:
- autobinding_keystroke_preserves_editor_content
- pending_snapshot_carries_forced_overlay
- typed_path_wins_over_forced_previous_value