fix(engine): 반복 렌더 경로의 리터럴 단일 바인딩 판정 누락 해소
판정 통일(engine-v1.55.0)에서 리터럴 해석을 BindingShape 한 곳으로 모았으나
renderItemChildren 만 `hasPipes → isComplexExpression → 경로 탐색` 3분기를 직접
갈라 리터럴을 몰랐다. 그 결과 같은 `{{true}}` 가 조건 자리에서는 true,
목록 셀·카드 등 반복 렌더 prop 자리에서는 undefined 로 갈렸다 —
통일이 없애려던 비대칭이 이 지점에만 남아 있었다.
형태 판정을 resolveSingleBindingValue 에 위임해 다른 경로와 같은 규칙을 쓴다.
같은 지점이 빈 바인딩(`{{}}`)도 몰라 경로 탐색으로 보내던 것을 함께 봉인했다.
브라우저 실측(배포 번들의 G7Core.renderItemChildren 직접 호출)으로 발견했고,
저장소 레이아웃의 리터럴 단일 바인딩 60건은 전부 if/condition 자리라
화면 표시에는 변화가 없다.
SEO 패리티: 봇 측 ExpressionEvaluator 는 이미 리터럴을 해석하고 있었으므로
이 수정은 React 경로를 봇 경로에 맞춘 것이다 (파일 헤더에 면제 사유 기재).
This commit is contained in:
@@ -118,6 +118,7 @@
|
||||
- 계산식에 날짜·숫자 서식을 함께 걸면 원본 값이 바뀌어도 처음 계산된 값이 계속 표시되던 문제를 수정했습니다.
|
||||
- 목록 칸이나 합계 행 안에서 번역 문구가 문장 중간에 들어간 경우 번역되지 않고 원본 키가 그대로 보이던 문제, 그리고 일부 표시 값에 `{{ }}` 기호가 섞여 나오던 문제를 수정했습니다.
|
||||
- 목록 칸이나 합계 행에서 '번역하지 않고 원문 그대로 표시'로 지정한 값에 보이지 않는 특수문자가 함께 출력되던 문제를 수정했습니다. 화면에서는 빈칸이나 깨진 글자로 보였고, 값을 복사해 붙여넣을 때 따라붙었습니다.
|
||||
- 목록 표·카드의 셀에서 `true`/`false` 같은 값을 직접 쓴 자리가 비어 보이던 문제를 수정했습니다. 같은 값을 표시 조건에 쓰면 정상 동작해서, 같은 작성이 놓인 자리에 따라 갈렸습니다.
|
||||
- 목록 화면에서 글이나 항목을 열어 보고 돌아올 때 보고 있던 페이지·검색어·필터가 사라지던 문제를 화면 엔진 차원에서 함께 수정했습니다. '현재 주소의 조건을 유지한다'고 지정해도 덧붙일 값이 없으면 유지가 아예 동작하지 않아 조건이 통째로 사라지던 동작을 바로잡았습니다. (#75 @jiwonpapa 님께서 제보해주셨습니다.)
|
||||
|
||||
#### 확장·데이터베이스 안정성
|
||||
|
||||
+15
-15
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
||||
>
|
||||
> 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)
|
||||
|
||||
## [engine-v1.56.4] - 2026-08-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- 반복 렌더 경로(`renderItemChildren`)가 리터럴 단일 바인딩(`{{true}}` / `{{false}}` / `{{null}}` / `{{undefined}}`)을 경로로 탐색해 값이 `undefined` 가 됐다. 판정 통일(engine-v1.55.0)에서 리터럴을 `BindingShape` 한 곳으로 모았지만 이 경로만 `hasPipes → isComplexExpression → 경로 탐색` 3분기를 직접 갈라 리터럴을 몰랐고, 그 결과 같은 `{{true}}` 가 조건 자리에서는 `true`, 목록 셀·카드 등 반복 렌더 prop 자리에서는 `undefined` 로 갈렸다 — 통일이 없애려던 비대칭이 이 지점에만 남아 있었다. 형태 판정을 `resolveSingleBindingValue` 에 위임해 다른 경로와 같은 규칙을 쓴다.
|
||||
- 저장소 레이아웃의 리터럴 단일 바인딩 60건은 전부 `if`/`condition` 자리라 화면 표시에는 변화가 없다. 브라우저 실측으로 발견했고(배포 번들의 `G7Core.renderItemChildren` 직접 호출), 같은 지점이 빈 바인딩(`{{}}`)도 몰라 경로 탐색으로 보내던 것을 함께 봉인했다.
|
||||
|
||||
## [engine-v1.56.3] - 2026-08-02
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// audit:allow seo-renderer-parity-sync 이번 변경은 노드 키·컨텍스트 처리 추가가 아니라
|
||||
// 리터럴 판정을 BindingShape 로 위임한 것이다. 봇(SEO) 측 ExpressionEvaluator 는
|
||||
// 이미 'true'/'false'/'null' 을 리터럴로 해석하고 있었으므로(app/Seo/ExpressionEvaluator.php),
|
||||
// 이 수정은 React 경로를 봇 경로에 맞춘 것이며 패리티를 좁힌다. 봇 측 대응 변경 없음.
|
||||
/**
|
||||
* 렌더링 관련 헬퍼 함수 모듈
|
||||
*
|
||||
@@ -762,34 +766,34 @@ export function renderItemChildren(
|
||||
effectivePath = singleBindingPath.slice(RAW_PREFIX.length);
|
||||
}
|
||||
|
||||
let result;
|
||||
if (hasPipes(effectivePath)) {
|
||||
// 파이프 표현식은 evaluatePipeExpression 으로 평가한다.
|
||||
// isComplexExpression 은 `|` 를 연산자로 보아 evaluateExpression 으로 보내는데,
|
||||
// 그 경로에서는 `value | pipe` 가 JS 비트 OR 로 평가되어
|
||||
// 인자 있는 파이프는 예외(값 소실), 인자 없는 파이프는 조용한 오답이 된다.
|
||||
// DynamicRenderer 의 text 처리와 동일한 분기 순서를 유지한다.
|
||||
// raw: 접두사는 아래 공통 분기(wrapRawDeep)가 담당하므로 여기서는 제거된
|
||||
// effectivePath 만 넘긴다 — 원본 value 를 넘기면 raw 마커가 이중으로 감싸진다.
|
||||
// @since engine-v1.54.3
|
||||
// 반복 렌더의 props 값도 원본 타입이 필요하다 (문자열 서식 미적용).
|
||||
// @since engine-v1.54.9
|
||||
try {
|
||||
result = bindingEngine.evaluatePipeExpression(effectivePath, context, { skipCache: true });
|
||||
} catch (error) {
|
||||
logger.warn('renderItemChildren: 파이프 표현식 평가 실패:', error);
|
||||
return undefined;
|
||||
}
|
||||
} else if (isComplexExpression(effectivePath)) {
|
||||
try {
|
||||
result = bindingEngine.evaluateExpression(effectivePath, context);
|
||||
} catch (error) {
|
||||
// 형태 판정은 `BindingShape` 단일 출처에 맡긴다. 종전에는 이 지점만
|
||||
// `hasPipes → isComplexExpression → 경로 탐색` 3분기를 직접 갈라, 판정 통일
|
||||
// (engine-v1.55.0)이 모아 둔 **리터럴**(`true`/`false`/`null`/`undefined`)을
|
||||
// 이 경로만 몰랐다. 그래서 `{{true}}` 가 조건 자리에서는 `true`, 반복 렌더 prop
|
||||
// 자리에서는 경로 탐색으로 새어 `undefined` 가 됐다 — 통일이 없애려던 바로 그
|
||||
// 비대칭이 여기에만 남아 있었다.
|
||||
//
|
||||
// 실행 정책은 종전과 같다: 파이프는 원본 타입 보존(engine-v1.54.9), 평가 실패는
|
||||
// 경고 후 `undefined`(값 소실이 화면 전체를 깨뜨리지 않도록).
|
||||
// raw: 접두사는 아래 공통 분기(wrapRawDeep)가 담당하므로 제거된 effectivePath 만
|
||||
// 넘긴다 — 원본 value 를 넘기면 raw 마커가 이중으로 감싸진다. @since engine-v1.54.3
|
||||
// @since engine-v1.56.4
|
||||
let evaluationFailed = false;
|
||||
let result = resolveSingleBindingValue(effectivePath, context, bindingEngine, {
|
||||
skipCache: true,
|
||||
onError: (error) => {
|
||||
logger.warn('renderItemChildren: 표현식 평가 실패:', error);
|
||||
evaluationFailed = true;
|
||||
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
// skipCache: true - 반복 렌더링에서 같은 경로가 다른 값을 가져야 함
|
||||
result = bindingEngine.resolve(effectivePath, context, { skipCache: true });
|
||||
},
|
||||
onEmpty: () => {
|
||||
logger.warn('renderItemChildren: 빈 바인딩 `{{}}` 은 해석하지 않습니다.');
|
||||
},
|
||||
});
|
||||
|
||||
if (evaluationFailed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 표현식 평가 결과가 $t: 접두사 문자열이면 번역 처리
|
||||
|
||||
+34
@@ -307,4 +307,38 @@ describe('renderItemChildren — 반복 렌더 경로 기능 격차', () => {
|
||||
expect(JSON.parse(screen.getByTestId('probe').getAttribute('data-json')!)).toBe(42);
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
/**
|
||||
* 리터럴 단일 바인딩 (engine-v1.56.4)
|
||||
*
|
||||
* 판정 통일(engine-v1.55.0)에서 리터럴을 `BindingShape` 한 곳으로 모았지만, 이 경로만
|
||||
* `hasPipes → isComplexExpression → 경로 탐색` 3분기를 직접 갈라 리터럴을 몰랐다.
|
||||
* 그래서 `{{true}}` 가 조건 자리에서는 `true`, 반복 렌더 prop 자리에서는 `undefined` 로
|
||||
* 갈렸다 — 통일이 없애려던 바로 그 비대칭이 이 경로에만 남아 있었다.
|
||||
*/
|
||||
it.each([
|
||||
['{{true}}', true],
|
||||
['{{false}}', false],
|
||||
['{{null}}', null],
|
||||
])('리터럴 %s 이 prop 자리에서 경로 탐색으로 새지 않는다', (expr, expected) => {
|
||||
const view = renderItems(
|
||||
[{ id: 'p', type: 'basic', name: 'Probe', props: { value: expr } }],
|
||||
{ row: { id: 1 } },
|
||||
);
|
||||
|
||||
expect(JSON.parse(screen.getByTestId('probe').getAttribute('data-json')!)).toBe(expected);
|
||||
view.unmount();
|
||||
});
|
||||
|
||||
it('리터럴과 같은 이름의 컨텍스트 경로보다 리터럴이 우선한다', () => {
|
||||
// `true` 라는 이름의 컨텍스트 키가 있어도 `{{true}}` 는 리터럴이다 —
|
||||
// 조건 평가 경로와 같은 규칙.
|
||||
const view = renderItems(
|
||||
[{ id: 'p', type: 'basic', name: 'Probe', props: { value: '{{true}}' } }],
|
||||
{ true: '경로값', row: { id: 1 } },
|
||||
);
|
||||
|
||||
expect(JSON.parse(screen.getByTestId('probe').getAttribute('data-json')!)).toBe(true);
|
||||
view.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,6 +110,39 @@ test.describe('렌더 경로 간 바인딩 해석 대칭', () => {
|
||||
expect(String(value)).not.toContain('{{');
|
||||
});
|
||||
|
||||
// @scenario binding_literal_symmetry
|
||||
// @effects same_expression_same_value_across_paths
|
||||
test('리터럴이 반복 렌더와 조건 판정에서 같은 값을 본다', async ({ page }) => {
|
||||
const result = await page.evaluate(() => {
|
||||
const g7 = (window as any).G7Core;
|
||||
const Span = function Span() { return null; };
|
||||
const context = { row: { id: 1 } };
|
||||
const viaRepeat = (expr: string) =>
|
||||
(g7.renderItemChildren(
|
||||
[{ type: 'basic', name: 'Span', props: { title: expr } }],
|
||||
context,
|
||||
{ Span },
|
||||
'e2e-literal',
|
||||
) as any[])[0]?.props?.title;
|
||||
|
||||
return {
|
||||
repeatTrue: viaRepeat('{{true}}'),
|
||||
repeatFalse: viaRepeat('{{false}}'),
|
||||
repeatNull: viaRepeat('{{null}}'),
|
||||
condTrue: g7.evaluateCondition('{{true}}', context),
|
||||
condFalse: g7.evaluateCondition('{{false}}', context),
|
||||
};
|
||||
});
|
||||
|
||||
// 수정 전: 반복 렌더 경로만 리터럴을 몰라 경로 탐색으로 새어 전부 undefined 였다.
|
||||
// 조건 경로는 리터럴을 알았으므로 같은 작성이 자리에 따라 갈렸다.
|
||||
expect(result.repeatTrue).toBe(true);
|
||||
expect(result.repeatFalse).toBe(false);
|
||||
expect(result.repeatNull).toBeNull();
|
||||
expect(result.repeatTrue).toBe(result.condTrue);
|
||||
expect(result.repeatFalse).toBe(result.condFalse);
|
||||
});
|
||||
|
||||
// @scenario binding_empty_expression
|
||||
// @effects same_expression_same_value_across_paths
|
||||
test('빈 바인딩이 컨텍스트 객체 전체를 노출하지 않는다', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user