저장소 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건이 지나가던 문제
70 lines
3.0 KiB
TypeScript
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;
|
|
}
|