<?php
namespace Customize\Controller\Admin\Product;
use Customize\Service\ScraperService;
use Eccube\Controller\AbstractController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* Admin controller for launching scraper processes and monitoring progress.
*/
class SpreadsheetImportController extends AbstractController
{
/** @var ScraperService */
private $scraperService;
/** @var array */
private static $categoryLabels = [
'cp' => 'カーポート',
'cy' => 'サイクルポート',
'cg' => 'カーゲート',
'sh' => 'ガレージシャッター',
'wd' => 'ウッドデッキ',
'rs' => '立水栓・ガーデンシンク',
'gf' => 'ガーデンファニチャー',
'tf' => '人工芝・芝生',
'fe' => 'フェンス・柵・塀',
'ts' => '手すり',
'mp' => '門扉',
'fu' => 'ポスト・門柱・宅配ボックス',
'tr' => 'テラス屋根',
'tg' => 'テラス囲い・サンルーム',
'br' => 'バルコニー屋根',
'bl' => 'バルコニー・ベランダ',
'aw' => 'オーニング・日よけ',
'pg' => 'パーゴラ',
'mo' => '物置・収納・屋外倉庫',
'sy' => 'ストックヤード',
'gs' => 'ゴミステーション',
'wh' => '倉庫・ガレージ',
'um' => '二重窓(内窓)',
'ws' => '窓シャッター',
'mk' => '面格子・窓格子',
'fj' => '風除室・玄関フード',
'rd' => '玄関ドア',
'st' => '石材',
'sl' => '照明',
'dy' => 'DIY',
];
public function __construct(ScraperService $scraperService)
{
$this->scraperService = $scraperService;
}
/**
* Main page with buttons for product and price scraping.
*
* @Route("/%eccube_admin_route%/product/scraper", name="admin_product_scraper", methods={"GET"})
* @Template("@admin/Product/scraper.twig")
*/
public function index()
{
return [
'categories' => self::$categoryLabels,
'runningJobs' => $this->scraperService->getRunningJobs(),
'spreadsheetLinks' => $this->scraperService->getSpreadsheetLinks(),
];
}
/**
* Start product scraper (basic data).
*
* @Route("/%eccube_admin_route%/product/scraper/start_product", name="admin_product_scraper_start_product", methods={"POST"})
*/
public function startProduct(Request $request): JsonResponse
{
if (!$this->isTokenValid()) {
return $this->json(['error' => 'Invalid CSRF token'], 403);
}
if ($this->scraperService->isProductLocked()) {
return $this->json(['error' => '基本データ取込は既に実行中です'], 409);
}
// Parse comma-separated categories (empty = all)
$catStr = trim((string) $request->request->get('categories', ''));
$categories = [];
if ($catStr !== '') {
foreach (explode(',', $catStr) as $c) {
$c = trim($c);
if ($c !== '' && preg_match('/^[a-z0-9]+$/', $c)) {
$categories[] = $c;
}
}
}
$result = $this->scraperService->startProductScraper($categories);
if ($result) {
$label = empty($categories) ? '全カテゴリ' : implode(',', $categories);
return $this->json([
'status' => 'started',
'message' => "基本データ取込を開始しました(対象: {$label})",
]);
}
return $this->json(['error' => '開始できませんでした'], 500);
}
/**
* Start price scraper for a specific category.
*
* @Route("/%eccube_admin_route%/product/scraper/start_price", name="admin_product_scraper_start_price", methods={"POST"})
*/
public function startPrice(Request $request): JsonResponse
{
if (!$this->isTokenValid()) {
return $this->json(['error' => 'Invalid CSRF token'], 403);
}
$category = $request->request->get('category', '');
if (!$category || !isset(self::$categoryLabels[$category])) {
return $this->json(['error' => '無効なカテゴリです'], 400);
}
if ($this->scraperService->isPriceLocked($category)) {
$label = self::$categoryLabels[$category];
return $this->json(['error' => "{$label} の価格マトリクス取込は既に実行中です"], 409);
}
$maker = $request->request->get('maker');
$result = $this->scraperService->startPriceScraper($category, $maker ?: null);
if ($result) {
$label = self::$categoryLabels[$category];
return $this->json(['status' => 'started', 'message' => "{$label} の価格マトリクス取込を開始しました"]);
}
return $this->json(['error' => '開始できませんでした'], 500);
}
/**
* Get current status of all scrapers (AJAX polling endpoint).
*
* @Route("/%eccube_admin_route%/product/scraper/status", name="admin_product_scraper_status", methods={"GET"})
*/
public function status(): JsonResponse
{
$product = $this->scraperService->getProductProgress();
$productLocked = $this->scraperService->isProductLocked();
// Collect price statuses for all categories
$priceStatuses = [];
foreach (self::$categoryLabels as $cat => $label) {
$locked = $this->scraperService->isPriceLocked($cat);
$progress = $this->scraperService->getPriceProgress($cat);
if ($locked || $progress) {
$priceStatuses[$cat] = [
'locked' => $locked,
'progress' => $progress,
];
}
}
return $this->json([
'product' => [
'locked' => $productLocked,
'progress' => $product,
],
'price' => $priceStatuses,
]);
}
/**
* Start DB import from ProductALL sheet (Symfony console command).
*
* @Route("/%eccube_admin_route%/product/scraper/db_import_product", name="admin_product_scraper_db_import_product", methods={"POST"})
*/
public function dbImportProduct(Request $request): JsonResponse
{
if (!$this->isTokenValid()) {
return $this->json(['error' => 'Invalid CSRF token'], 403);
}
[$ok, $message] = $this->scraperService->startDbImportProduct();
if ($ok) {
return $this->json(['status' => 'started', 'message' => $message]);
}
return $this->json(['error' => $message], 409);
}
/**
* Start DB import from PP_{category} sheet for a specific category.
*
* @Route("/%eccube_admin_route%/product/scraper/db_import_price", name="admin_product_scraper_db_import_price", methods={"POST"})
*/
public function dbImportPrice(Request $request): JsonResponse
{
if (!$this->isTokenValid()) {
return $this->json(['error' => 'Invalid CSRF token'], 403);
}
$category = $request->request->get('category', '');
if (!$category) {
return $this->json(['error' => 'カテゴリを指定してください'], 400);
}
[$ok, $message] = $this->scraperService->startDbImportPrice($category);
if ($ok) {
return $this->json(['status' => 'started', 'message' => $message]);
}
return $this->json(['error' => $message], 409);
}
/**
* Get execution log for a scraper (for debugging).
*
* @Route("/%eccube_admin_route%/product/scraper/log", name="admin_product_scraper_log", methods={"GET"})
*/
public function log(Request $request): JsonResponse
{
$type = $request->query->get('type', 'product');
$category = $request->query->get('category', '');
try {
$log = null;
if ($type === 'product') {
$log = $this->scraperService->getProductLog(200);
} elseif ($type === 'price' && $category) {
$log = $this->scraperService->getPriceLog($category, 200);
} elseif ($type === 'db_import_product') {
$log = $this->scraperService->getDbImportProductLog(200);
} elseif ($type === 'db_import_price' && $category) {
$log = $this->scraperService->getDbImportPriceLog($category, 200);
} else {
return $this->json(['error' => 'invalid type/category'], 400);
}
// Include debug info only for 'product' request (reduces load for repeated polls)
$debug = ($type === 'product') ? $this->scraperService->getDebugInfo() : null;
return $this->json([
'log' => $log,
'debug' => $debug,
]);
} catch (\Throwable $e) {
return $this->json([
'log' => '(exception: ' . $e->getMessage() . ')',
'debug' => null,
], 200);
}
}
/**
* Cancel a running scraper.
*
* @Route("/%eccube_admin_route%/product/scraper/cancel", name="admin_product_scraper_cancel", methods={"POST"})
*/
public function cancel(Request $request): JsonResponse
{
if (!$this->isTokenValid()) {
return $this->json(['error' => 'Invalid CSRF token'], 403);
}
$type = $request->request->get('type', '');
$category = $request->request->get('category', '');
if ($type === 'product') {
$this->scraperService->cancelProduct();
return $this->json(['status' => 'cancelled', 'message' => '基本データ取込をキャンセルしました']);
}
if ($type === 'price' && $category) {
$this->scraperService->cancelPrice($category);
$label = self::$categoryLabels[$category] ?? $category;
return $this->json(['status' => 'cancelled', 'message' => "{$label} の価格マトリクス取込をキャンセルしました"]);
}
return $this->json(['error' => '無効なリクエスト'], 400);
}
}