Files
HeuJung cd189190c2 fix(core,extensions): 창 크기 변경 후 저장 시 편집기 본문이 사라지던 문제
저장소 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건이 지나가던 문제
2026-09-01 18:08:40 +09:00

70 lines
3.0 KiB
TypeScript

/**
* 상태 병합 헬퍼
*
* 이중 저장소(React `localDynamicState` = 저장소 A, `globalState._local` = 저장소 B)를
* 동기화하는 두 쓰기 경로가 공유하는 병합 프리미티브를 담는다.
*
* 왜 `helpers/` 인가:
* `addMissingLeafKeys` 는 `G7CoreGlobals.ts` 의 모듈 프라이빗 함수였다. 그런데
* `G7CoreGlobals.ts` 가 `ActionDispatcher` 를 import 하므로 `ActionDispatcher` 에서
* 역방향 값 import 를 하면 런타임 순환이 된다. `helpers/` 는 엔진 모듈을 import 하지
* 않으므로 양쪽이 안전하게 공유할 수 있다.
*
* @since engine-v1.63.3
* @packageDocumentation
*/
/**
* base 객체에 없는 leaf 키만 extra에서 추가합니다.
*
* deepMerge와 달리 base에 이미 존재하는 값(배열 포함)은 절대 덮어쓰지 않습니다.
* extra에만 존재하는 키는 재귀적으로 추가됩니다.
*
* 용도:
* - `setLocal`(G7CoreGlobals)에서 dynamicLocal(actionContext.state)의 setState 전용 키를
* globalLocal에 안전하게 추가할 때. dynamicLocal의 stale 배열(init_actions 기본값)이
* globalLocal의 정상 API 데이터를 덮어쓰는 것을 방지합니다.
* - `handleSetState`(ActionDispatcher) COMPONENT path 에서 저장소 B 를 base 로 삼을 때
* 저장소 A 전용 키(`loadingActions`, `apiError`, `*_result` 등)를 보충할 때.
*
* @param base 우선하는 기준 객체 (충돌 시 이 값이 이긴다)
* @param extra base 에 없는 키만 가져올 보충 객체
* @return base 를 얕게 복사한 뒤 누락 키를 채운 새 객체 (base/extra 모두 변이하지 않음)
*
* @since engine-v1.41.0 (G7CoreGlobals 내부 함수로 최초 도입)
* @since engine-v1.63.3 helpers/StateMerge 로 이동해 ActionDispatcher 와 공유
*
* @example
* ```ts
* const base = { form: { category_ids: [381, 384], name: 'A' } };
* const extra = { form: { category_ids: [], options: [] }, selectedProducts: [1] };
* addMissingLeafKeys(base, extra);
* // → { form: { category_ids: [381, 384], name: 'A', options: [] }, selectedProducts: [1] }
* // base의 category_ids는 보존, extra의 selectedProducts와 options는 추가
* ```
*/
export function addMissingLeafKeys(
base: Record<string, any>,
extra: Record<string, any>
): Record<string, any> {
const result = { ...base };
for (const key of Object.keys(extra)) {
if (!(key in result)) {
// base에 없는 키: extra 값 그대로 추가
result[key] = extra[key];
} else if (
result[key] !== null &&
typeof result[key] === 'object' &&
!Array.isArray(result[key]) &&
extra[key] !== null &&
typeof extra[key] === 'object' &&
!Array.isArray(extra[key])
) {
// 양쪽 모두 plain object: 재귀적으로 처리
result[key] = addMissingLeafKeys(result[key], extra[key]);
}
// base에 이미 존재하는 leaf 값(배열, 문자열, 숫자 등): 건너뜀 (base 값 보존)
}
return result;
}