Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions app/DoctrineMigrations/Version20260722010000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/*
* This file is part of EC-CUBE
*
* Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved.
*
* http://www.ec-cube.co.jp/
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;

/**
* 営業時間(構造化データ schema.org OpeningHoursSpecification)用の dtb_opening_hours を作成する.
*/
final class Version20260722010000 extends AbstractMigration
{
public const NAME = 'dtb_opening_hours';

public function up(Schema $schema): void
{
if ($schema->hasTable(self::NAME)) {
return;
}

$table = $schema->createTable(self::NAME);
$table->addColumn('id', Types::INTEGER, ['autoincrement' => true, 'unsigned' => true]);
$table->addColumn('base_info_id', Types::INTEGER, ['unsigned' => true, 'notnull' => false]);
$table->addColumn('day_of_week', Types::SIMPLE_ARRAY, ['notnull' => false]);
$table->addColumn('opens', Types::TIME_MUTABLE, ['notnull' => false]);
$table->addColumn('closes', Types::TIME_MUTABLE, ['notnull' => false]);
$table->addColumn('sort_no', Types::INTEGER, ['default' => 0]);
$table->addColumn('discriminator_type', Types::STRING, ['length' => 255]);
$table->setPrimaryKey(['id']);
$table->addIndex(['base_info_id'], 'dtb_opening_hours_base_info_id_idx');
$table->addForeignKeyConstraint('dtb_base_info', ['base_info_id'], ['id'], [], 'fk_opening_hours_base_info');
}

public function down(Schema $schema): void
{
if (!$schema->hasTable(self::NAME)) {
return;
}

$schema->dropTable(self::NAME);
}
}
98 changes: 98 additions & 0 deletions e2e/tests/admin-basicinfo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1325,4 +1325,102 @@ test.describe('Admin Basic Info (EA07)', () => {
await page.waitForLoadState('load');
await expect(page.locator('#page_admin_setting_shop_calendar .alert-success')).toContainText('保存しました');
});

test('basicinfo_構造化データ_営業時間とSNS等URL - EA0701-UC01-T18', async ({ page }) => {
test.setTimeout(120_000);

const sameAsUrl = 'https://example.com/official-sns';

// --- 入力(構造化データのスカラー項目 + 営業時間1行) ---
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
await ensureAdminLoggedIn(page);
if (!page.url().includes('/setting/shop')) {
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
}
await expect(page.locator('.c-pageTitle')).toContainText('基本設定');

// --- クリーンな基準状態にする(過去実行の残存行・値を消して保存) ---
const resetDeletes = page.locator('#opening-hours-group .delete-opening-hour');
while (await resetDeletes.count() > 0) {
await resetDeletes.first().click();
}
await page.locator('#shop_master_same_as').fill('');
await page.locator('#shop_master_founding_date').fill('');
await page.locator('#shop_master_number_of_employees').fill('');
await page.locator('#shop_master_copyright_year').fill('');
await page.locator('#shop_master_site_image').fill('');
await page.locator('button.ladda-button[type="submit"]').click();
await page.waitForLoadState('load');
await expect(page.locator('.alert-success')).toContainText('保存しました', { timeout: 30_000 });

// --- 入力(構造化データのスカラー項目 + 営業時間1行) ---
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
await expect(page.locator('#opening-hours-group .opening-hours-item')).toHaveCount(0);

await page.locator('#shop_master_same_as').fill(sameAsUrl);
await page.locator('#shop_master_founding_date').fill('2000-04-01');
await page.locator('#shop_master_number_of_employees').fill('42');
await page.locator('#shop_master_copyright_year').fill('2020');
await page.locator('#shop_master_site_image').fill('https://example.com/site.png');

// 営業時間の行を追加(月曜 09:00-18:00)
await page.locator('#add-opening-hour-button').click();
await expect(page.locator('#opening-hours-group .opening-hours-item')).toHaveCount(1);
await page.locator('#shop_master_OpeningHours_0_day_of_week_0').check(); // Monday
await page.locator('#shop_master_OpeningHours_0_opens').fill('09:00');
await page.locator('#shop_master_OpeningHours_0_closes').fill('18:00');

await page.locator('button.ladda-button[type="submit"]').click();
await page.waitForLoadState('load');
await expect(page.locator('.alert-success')).toContainText('保存しました', { timeout: 30_000 });

// --- 保存往復の検証(DB反映→再表示) ---
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
await expect(page.locator('#shop_master_same_as')).toHaveValue(sameAsUrl);
await expect(page.locator('#shop_master_number_of_employees')).toHaveValue('42');
await expect(page.locator('#shop_master_copyright_year')).toHaveValue('2020');
await expect(page.locator('#opening-hours-group .opening-hours-item')).toHaveCount(1);
await expect(page.locator('#shop_master_OpeningHours_0_day_of_week_0')).toBeChecked();

// --- フロントの JSON-LD 反映を検証 ---
await page.goto('/');
await page.waitForLoadState('load');
const content = await page.content();
expect(content).toContain('"openingHoursSpecification"');
expect(content).toContain('"sameAs"');
expect(content).toContain(sameAsUrl);

// --- 重複バリデーション:同一曜日で重なる行を追加すると保存が弾かれ、行にエラーが出る ---
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
await page.locator('#add-opening-hour-button').click();
await expect(page.locator('#opening-hours-group .opening-hours-item')).toHaveCount(2);
await page.locator('#shop_master_OpeningHours_1_day_of_week_0').check(); // Monday(既存行と重複)
await page.locator('#shop_master_OpeningHours_1_opens').fill('12:00');
await page.locator('#shop_master_OpeningHours_1_closes').fill('20:00');
await page.locator('button.ladda-button[type="submit"]').click();
await page.waitForLoadState('load');
await expect(page.locator('.alert-success')).toHaveCount(0);
await expect(page.locator('#opening-hours-group')).toContainText('同じ曜日で営業時間が重複しています');

// --- クリーンアップ:営業時間の行とスカラー項目を消して保存 ---
await page.goto(`/${adminRoute}/setting/shop`);
await page.waitForLoadState('load');
const deleteButtons = page.locator('#opening-hours-group .delete-opening-hour');
while (await deleteButtons.count() > 0) {
await deleteButtons.first().click();
}
await page.locator('#shop_master_same_as').fill('');
await page.locator('#shop_master_founding_date').fill('');
await page.locator('#shop_master_number_of_employees').fill('');
await page.locator('#shop_master_copyright_year').fill('');
await page.locator('#shop_master_site_image').fill('');
await page.locator('button.ladda-button[type="submit"]').click();
await page.waitForLoadState('load');
await expect(page.locator('.alert-success')).toContainText('保存しました', { timeout: 30_000 });
});
});
150 changes: 150 additions & 0 deletions src/Eccube/Entity/BaseInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@

namespace Eccube\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Eccube\Entity\Master\Country;
use Eccube\Entity\Master\Pref;
use Eccube\Repository\BaseInfoRepository;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Table(name: 'dtb_base_info')]
#[ORM\InheritanceType('SINGLE_TABLE')]
Expand Down Expand Up @@ -170,6 +173,34 @@ class BaseInfo extends AbstractEntity
#[ORM\Column(name: 'ucp_catalog_requires_auth', type: Types::BOOLEAN, options: ['default' => false])]
private bool $ucp_catalog_requires_auth = false;

#[ORM\Column(name: 'same_as', type: Types::TEXT, nullable: true)]
private ?string $same_as = null;

#[ORM\Column(name: 'founding_date', type: Types::DATE_MUTABLE, nullable: true)]
private ?\DateTime $founding_date = null;

#[ORM\Column(name: 'number_of_employees', type: Types::INTEGER, nullable: true)]
private ?int $number_of_employees = null;

#[ORM\Column(name: 'copyright_year', type: Types::INTEGER, nullable: true)]
private ?int $copyright_year = null;

#[ORM\Column(name: 'site_image', type: Types::STRING, length: 255, nullable: true)]
private ?string $site_image = null;

/**
* @var Collection<int, OpeningHours>
*/
#[ORM\OneToMany(targetEntity: OpeningHours::class, mappedBy: 'BaseInfo', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OrderBy(['sort_no' => 'ASC'])]
#[Assert\Valid]
private Collection $OpeningHours;

public function __construct()
{
$this->OpeningHours = new ArrayCollection();
}

/**
* Get id.
*
Expand Down Expand Up @@ -937,4 +968,123 @@ public function isUcpCatalogRequiresAuth(): bool
{
return $this->ucp_catalog_requires_auth;
}

/**
* Get sameAs.
*
* 構造化データ(Organization.sameAs)に出力する SNS 等の公式 URL を改行区切りで保持する.
*/
public function getSameAs(): ?string
{
return $this->same_as;
}

/**
* Set sameAs.
*/
public function setSameAs(?string $sameAs): BaseInfo
{
$this->same_as = $sameAs;

return $this;
}

/**
* Get foundingDate.
*/
public function getFoundingDate(): ?\DateTime
{
return $this->founding_date;
}

/**
* Set foundingDate.
*/
public function setFoundingDate(?\DateTime $foundingDate): BaseInfo
{
$this->founding_date = $foundingDate;

return $this;
}

/**
* Get numberOfEmployees.
*/
public function getNumberOfEmployees(): ?int
{
return $this->number_of_employees;
}

/**
* Set numberOfEmployees.
*/
public function setNumberOfEmployees(?int $numberOfEmployees): BaseInfo
{
$this->number_of_employees = $numberOfEmployees;

return $this;
}

/**
* Get copyrightYear.
*/
public function getCopyrightYear(): ?int
{
return $this->copyright_year;
}

/**
* Set copyrightYear.
*/
public function setCopyrightYear(?int $copyrightYear): BaseInfo
{
$this->copyright_year = $copyrightYear;

return $this;
}

/**
* Get siteImage.
*/
public function getSiteImage(): ?string
{
return $this->site_image;
}

/**
* Set siteImage.
*/
public function setSiteImage(?string $siteImage): BaseInfo
{
$this->site_image = $siteImage;

return $this;
}

/**
* Get openingHours.
*
* @return Collection<int, OpeningHours>
*/
public function getOpeningHours(): Collection
{
return $this->OpeningHours;
}

public function addOpeningHour(OpeningHours $openingHour): BaseInfo
{
if (!$this->OpeningHours->contains($openingHour)) {
$this->OpeningHours[] = $openingHour;
$openingHour->setBaseInfo($this);
}

return $this;
}

public function removeOpeningHour(OpeningHours $openingHour): BaseInfo
{
$this->OpeningHours->removeElement($openingHour);

return $this;
}
}
Loading
Loading