저장소 B(_global._local)를 동기화하는 두 지점이 저장소 A 계열 스냅샷을 그대로
넘겨 왔다. setGlobalState 는 _local 을 얕게 병합하므로 그 쓰기는 patch 가 아니라
통째 교체이며, A 가 아직 받지 못한 값 — CKEditor 가 setLocal({render:false,
selfManaged:true}) 로 B 에만 쓴 본문 — 이 예외도 로그도 없이 사라진다.
handleSetState COMPONENT 분기의 B 쓰기와 sequence 반환값을 함께 live B 기반으로
바꾼다. 요청 body 는 B 가 아니라 그 반환값에서 오므로, B 쓰기만 고치면 저장은
여전히 422 가 난다. 같은 형태의 setParentLocal 도 함께 정정했다.
트리거는 브레이크포인트 교차가 아니라 19px 폭 변경이다 — 모바일 URL바 숨김·
가상키보드·화면 회전이 모두 해당한다. 작성 화면은 422 로, 수정 화면은 성공
토스트와 함께 조용한 데이터 손실로 나타났다.
함께 담은 것:
- ckeditor5: 미리보기 입력이 설정으로 저장되던 문제, 본문만 고치면 저장 버튼이
비활성으로 남던 문제
- board: 일괄 적용 확인 창의 항목 목록이 통째로 렌더되지 않던 문제(표현식
평가기가 콜백 안 멤버 대입을 거부), 「금지어」 라벨 누락
- 확장 문서의 Playwright 명령이 코어 config 를 잡아 "No tests found" 로 조용히
0건이 지나가던 문제
75 lines
4.2 KiB
YAML
75 lines
4.2 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
|
|
- 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
|
|
|
|
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
|