layoutService = app(LayoutService::class); } /** * 부모만 meta.seo가 있을 때 부모의 seo가 사용됩니다. */ public function test_parent_only_seo_is_used(): void { $parentSeo = [ 'enabled' => true, 'priority' => 'high', 'og' => ['type' => 'website'], ]; $parent = $this->buildLayout(seo: $parentSeo); $child = $this->buildLayout(); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertTrue($result['meta']['seo']['enabled']); $this->assertSame('high', $result['meta']['seo']['priority']); $this->assertSame('website', $result['meta']['seo']['og']['type']); } /** * 자식만 meta.seo가 있을 때 자식의 seo가 사용됩니다. */ public function test_child_only_seo_is_used(): void { $childSeo = [ 'enabled' => true, 'priority' => 'low', 'og' => ['title' => '자식 페이지'], ]; $parent = $this->buildLayout(); $child = $this->buildLayout(seo: $childSeo); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertTrue($result['meta']['seo']['enabled']); $this->assertSame('low', $result['meta']['seo']['priority']); $this->assertSame('자식 페이지', $result['meta']['seo']['og']['title']); } /** * 부모와 자식 모두 meta.seo가 있을 때 자식의 priority가 우선됩니다. */ public function test_child_overrides_priority_parent_rest_preserved(): void { $parentSeo = [ 'enabled' => true, 'priority' => 'high', 'title_template' => '{{title}} - 사이트명', 'description' => '기본 설명', ]; $childSeo = [ 'priority' => 'low', ]; $parent = $this->buildLayout(seo: $parentSeo); $child = $this->buildLayout(seo: $childSeo); $result = $this->layoutService->mergeLayouts($parent, $child); // 자식이 오버라이드한 값 $this->assertSame('low', $result['meta']['seo']['priority']); // 부모에서 보존된 값 $this->assertTrue($result['meta']['seo']['enabled']); $this->assertSame('{{title}} - 사이트명', $result['meta']['seo']['title_template']); $this->assertSame('기본 설명', $result['meta']['seo']['description']); } /** * 자식이 og를 추가하면 부모의 기본값과 함께 병합됩니다. */ public function test_child_adds_og_merged_with_parent_defaults(): void { $parentSeo = [ 'enabled' => true, 'priority' => 'medium', ]; $childSeo = [ 'og' => [ 'title' => '상품 상세', 'type' => 'product', ], ]; $parent = $this->buildLayout(seo: $parentSeo); $child = $this->buildLayout(seo: $childSeo); $result = $this->layoutService->mergeLayouts($parent, $child); // 부모 기본값 보존 $this->assertTrue($result['meta']['seo']['enabled']); $this->assertSame('medium', $result['meta']['seo']['priority']); // 자식이 추가한 og $this->assertSame('상품 상세', $result['meta']['seo']['og']['title']); $this->assertSame('product', $result['meta']['seo']['og']['type']); } /** * 자식이 enabled=false로 설정하면 최종 결과에서 enabled=false가 됩니다. */ public function test_child_sets_enabled_false(): void { $parentSeo = [ 'enabled' => true, 'priority' => 'high', ]; $childSeo = [ 'enabled' => false, ]; $parent = $this->buildLayout(seo: $parentSeo); $child = $this->buildLayout(seo: $childSeo); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertFalse($result['meta']['seo']['enabled']); // 부모 나머지 보존 $this->assertSame('high', $result['meta']['seo']['priority']); } /** * 부모 seo.og.type + 자식 seo.og.title → og가 deep merge됩니다. */ public function test_og_deep_merge_parent_type_child_title(): void { $parentSeo = [ 'enabled' => true, 'og' => [ 'type' => 'website', 'site_name' => 'G7 Store', ], ]; $childSeo = [ 'og' => [ 'title' => '카테고리 페이지', ], ]; $parent = $this->buildLayout(seo: $parentSeo); $child = $this->buildLayout(seo: $childSeo); $result = $this->layoutService->mergeLayouts($parent, $child); // 부모 og 보존 $this->assertSame('website', $result['meta']['seo']['og']['type']); $this->assertSame('G7 Store', $result['meta']['seo']['og']['site_name']); // 자식 og 추가 $this->assertSame('카테고리 페이지', $result['meta']['seo']['og']['title']); } /** * seo 이외의 meta 키(title, description)는 기존 얕은 병합이 유지됩니다. */ public function test_non_seo_meta_keys_use_shallow_merge(): void { $parent = [ 'meta' => [ 'title' => 'Parent Title', 'description' => 'Parent Description', 'seo' => ['enabled' => true, 'priority' => 'high'], ], 'data_sources' => [], 'components' => [], ]; $child = [ 'meta' => [ 'title' => 'Child Title', 'seo' => ['priority' => 'low'], ], 'data_sources' => [], 'components' => [], ]; $result = $this->layoutService->mergeLayouts($parent, $child); // 얕은 병합: 자식 title 우선 $this->assertSame('Child Title', $result['meta']['title']); // 얕은 병합: 자식에 없는 description 보존 $this->assertSame('Parent Description', $result['meta']['description']); // seo는 deep merge $this->assertSame('low', $result['meta']['seo']['priority']); $this->assertTrue($result['meta']['seo']['enabled']); } /** * 3단계 상속(mergeLayouts 2회 호출)에서 모든 레벨의 seo가 누적됩니다. */ public function test_three_level_inheritance_accumulates_seo(): void { // Level 1: _base (최상위 부모) $base = [ 'meta' => [ 'seo' => [ 'enabled' => true, 'title_template' => '{{title}} - G7', 'og' => [ 'type' => 'website', 'site_name' => 'G7', ], ], ], 'data_sources' => [], 'components' => [ ['component' => 'Div', 'slot' => 'content'], ], ]; // Level 2: shop 기본 (중간 레벨) $shopBase = [ 'meta' => [ 'seo' => [ 'priority' => 'medium', 'og' => [ 'type' => 'product.group', ], 'structured_data' => [ 'type' => 'ItemList', ], ], ], 'data_sources' => [], 'slots' => [ 'content' => [ 'component' => 'Div', 'slot' => 'shop_content', ], ], ]; // Level 3: 상품 상세 (최하위) $shopShow = [ 'meta' => [ 'seo' => [ 'priority' => 'high', 'og' => [ 'title' => '{{product.name}}', 'image' => '{{product.image}}', ], 'data_sources' => ['product'], ], ], 'data_sources' => [], 'slots' => [ 'shop_content' => [ 'component' => 'Div', 'props' => ['text' => '상품 상세'], ], ], ]; // 3단계 병합: _base + shop_base → 중간 결과 + shop_show $merged1 = $this->layoutService->mergeLayouts($base, $shopBase); $result = $this->layoutService->mergeLayouts($merged1, $shopShow); $seo = $result['meta']['seo']; // _base에서 상속 $this->assertTrue($seo['enabled']); $this->assertSame('{{title}} - G7', $seo['title_template']); // shop_base에서 상속 (deep merge) $this->assertSame('ItemList', $seo['structured_data']['type']); // shop_show에서 오버라이드 $this->assertSame('high', $seo['priority']); // data_sources는 합집합 병합 (Level 3의 'product'만 존재 — Level 1,2는 미정의) $this->assertSame(['product'], $seo['data_sources']); // og deep merge: _base의 site_name + shop_base의 type 오버라이드 + shop_show의 title, image $this->assertSame('G7', $seo['og']['site_name']); $this->assertSame('product.group', $seo['og']['type']); $this->assertSame('{{product.name}}', $seo['og']['title']); $this->assertSame('{{product.image}}', $seo['og']['image']); } // ─── data_sources 합집합 병합 테스트 ────────────────────────────── /** * 부모와 자식의 data_sources가 합집합으로 병합됩니다. */ public function test_data_sources_union_merge_parent_and_child(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'data_sources' => ['stats', 'menu'], ]); $child = $this->buildLayout(seo: [ 'data_sources' => ['product'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame(['stats', 'menu', 'product'], $result['meta']['seo']['data_sources']); } /** * 부모와 자식의 data_sources 중복이 제거됩니다. */ public function test_data_sources_union_dedup(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'data_sources' => ['products', 'categories'], ]); $child = $this->buildLayout(seo: [ 'data_sources' => ['products', 'reviews'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame(['products', 'categories', 'reviews'], $result['meta']['seo']['data_sources']); } /** * 자식만 data_sources를 정의하면 그대로 사용됩니다. */ public function test_data_sources_child_only(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, ]); $child = $this->buildLayout(seo: [ 'data_sources' => ['product'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame(['product'], $result['meta']['seo']['data_sources']); } /** * 부모만 data_sources를 정의하면 자식이 상속합니다. */ public function test_data_sources_parent_only(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'data_sources' => ['stats'], ]); $child = $this->buildLayout(seo: [ 'priority' => 0.8, ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame(['stats'], $result['meta']['seo']['data_sources']); } /** * 양쪽 모두 data_sources 미정의 시 키가 없습니다. */ public function test_data_sources_both_empty(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, ]); $child = $this->buildLayout(seo: [ 'priority' => 0.5, ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertArrayNotHasKey('data_sources', $result['meta']['seo']); } // ─── vars deep merge 테스트 ────────────────────────────────────── /** * 부모와 자식의 vars가 deep merge되어 양쪽 모두 보존됩니다. */ public function test_vars_deep_merge_parent_child(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'vars' => ['site_name' => '$core_settings:general.site_name'], ]); $child = $this->buildLayout(seo: [ 'vars' => ['product_name' => '{{product.data.name}}'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $vars = $result['meta']['seo']['vars']; $this->assertSame('$core_settings:general.site_name', $vars['site_name']); $this->assertSame('{{product.data.name}}', $vars['product_name']); } /** * 자식의 vars가 부모의 동일 키를 오버라이드합니다. */ public function test_vars_child_overrides_parent_key(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'vars' => ['name' => 'Parent Name'], ]); $child = $this->buildLayout(seo: [ 'vars' => ['name' => 'Child Name'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame('Child Name', $result['meta']['seo']['vars']['name']); } // ─── base enabled 상속 테스트 ──────────────────────────────────── /** * 부모 enabled=false + 자식 enabled=true → 자식 우선으로 활성화됩니다. */ public function test_base_seo_disabled_child_enables(): void { $parent = $this->buildLayout(seo: [ 'enabled' => false, 'og' => ['type' => 'website'], ]); $child = $this->buildLayout(seo: [ 'enabled' => true, ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertTrue($result['meta']['seo']['enabled']); $this->assertSame('website', $result['meta']['seo']['og']['type']); } /** * 부모의 og.type을 자식이 오버라이드합니다. */ public function test_base_og_type_inherited_child_overrides(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'og' => ['type' => 'website'], ]); $child = $this->buildLayout(seo: [ 'og' => ['type' => 'product'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame('product', $result['meta']['seo']['og']['type']); } /** * 부모의 og.type이 보존되면서 자식이 og.title을 추가합니다. */ public function test_base_og_type_inherited_child_adds_title(): void { $parent = $this->buildLayout(seo: [ 'enabled' => true, 'og' => ['type' => 'website'], ]); $child = $this->buildLayout(seo: [ 'og' => ['title' => '상품 페이지'], ]); $result = $this->layoutService->mergeLayouts($parent, $child); $this->assertSame('website', $result['meta']['seo']['og']['type']); $this->assertSame('상품 페이지', $result['meta']['seo']['og']['title']); } // ─── 3단계 상속 data_sources 누적 테스트 ───────────────────────── /** * 3단계 상속에서 각 레벨의 data_sources가 합집합으로 누적됩니다. */ public function test_three_level_data_sources_accumulate(): void { $base = [ 'meta' => [ 'seo' => [ 'enabled' => true, 'data_sources' => ['stats'], ], ], 'data_sources' => [], 'components' => [ ['component' => 'Div', 'slot' => 'content'], ], ]; $mid = [ 'meta' => [ 'seo' => [ 'data_sources' => ['categories'], ], ], 'data_sources' => [], 'slots' => [ 'content' => [ 'component' => 'Div', 'slot' => 'inner', ], ], ]; $leaf = [ 'meta' => [ 'seo' => [ 'data_sources' => ['product', 'stats'], ], ], 'data_sources' => [], 'slots' => [ 'inner' => [ 'component' => 'Div', 'props' => ['text' => 'leaf'], ], ], ]; $merged1 = $this->layoutService->mergeLayouts($base, $mid); $result = $this->layoutService->mergeLayouts($merged1, $leaf); // stats(base) + categories(mid) + product(leaf) — stats 중복 제거 $this->assertSame(['stats', 'categories', 'product'], $result['meta']['seo']['data_sources']); $this->assertTrue($result['meta']['seo']['enabled']); } /** * 테스트용 레이아웃 배열을 빌드합니다. * * @param array|null $seo SEO 설정 배열 (null이면 seo 키 없음) * @return array 레이아웃 배열 */ private function buildLayout(?array $seo = null): array { $meta = []; if ($seo !== null) { $meta['seo'] = $seo; } return [ 'meta' => $meta, 'data_sources' => [], 'components' => [], ]; } }