Files
Gnuboard7/tests/scenarios/editor-resize-save-body-integrity.yaml
T
HeuJung 151a7ae945 fix(extensions,core): 저장 대상이 아닌 편집기가 폼을 변경됨으로 만들지 않게
미리보기 편집기는 설정을 시험해 보는 자리인데, 본문 동기화가 hasChanges 를
렌더를 일으키는 setLocal 로 올리면서 시험 입력만으로 [저장] 이 켜졌다.
어느 편집기가 저장 대상인지는 레이아웃만 알므로 핸들러가 필드명을 알아보게
하지 않고 trackChanges 선언으로 받는다 — 하드코딩하면 다른 확장이 같은
미리보기 패턴을 쓸 때 그대로 재발한다.

같은 결함이 형제 경로에도 있었다. 평문 폴백은 hasChanges 를 본문 배치
(render:false + selfManaged:true)에 실어, 편집기 자산을 못 불러온 환경에서
본문만 고치면 저장 버튼이 잠긴 채로 남았다 — 폐쇄망에서는 그 폴백이 정상
경로다. 폴백은 렌더 시점에도 같은 함수를 부르므로 화면을 여는 것만으로
변경됨이 되지 않도록, 사용자 입력에서 온 호출만 플래그를 올린다.

components.json 생성기는 종결 개행을 PHP_EOL 로 써서 Windows 빌드마다
그 파일을 유령 변경으로 남겼다. 줄 내용이 같아 diff 가 비어 보이므로
무엇이 바뀐 것인지 알 수 없는 변경만 쌓인다.
2026-09-01 21:53:47 +09:00

111 lines
6.7 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
# 편집기 확보 실패 시의 평문 폴백도 같은 조건(저장소 B 전용 쓰기)을 만든다 — 그 축을
# 잠그는 케이스가 이 spec 안에 있으므로 여기에 함께 건다. 자산 폴백 자체의 계약은
# self-hosted-runtime-assets.yaml 이 소유한다.
- plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/editor-fallback.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
- fallback_body_survives_resize_on_save
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
- id: plain_fallback_after_resize
description: |
편집기 확보에 실패해 평문 입력창으로 내려간 경우도 본문을 `setLocal({ render:false,
selfManaged:true })` 로 저장소 B 에만 쓴다 — 이 결함의 성립 조건이 그대로 성립한다.
폴백은 디바운스를 쓰지 않아 입력 즉시 B 에 실리므로, 편집기 축의 결정화 4단계 중
①(디바운스 발화 대기)만 빠지고 ②폭 변경 → ③pending 클리어 확정 → ④저장은 동일하다.
이 축이 별도로 필요한 이유: 폐쇄망·차단 환경에서는 폴백이 **정상 경로**다. 편집기 축만
잠그면 그 환경의 운영자에게는 결함이 남은 채로 남는다.
effects: [fallback_body_survives_resize_on_save]