app/Customize/Controller/Admin/Product/SpreadsheetImportController.php line 195

Open in your IDE?
  1. <?php
  2. namespace Customize\Controller\Admin\Product;
  3. use Customize\Service\ScraperService;
  4. use Eccube\Controller\AbstractController;
  5. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  6. use Symfony\Component\HttpFoundation\JsonResponse;
  7. use Symfony\Component\HttpFoundation\Request;
  8. use Symfony\Component\Routing\Annotation\Route;
  9. /**
  10.  * Admin controller for launching scraper processes and monitoring progress.
  11.  */
  12. class SpreadsheetImportController extends AbstractController
  13. {
  14.     /** @var ScraperService */
  15.     private $scraperService;
  16.     /** @var array */
  17.     private static $categoryLabels = [
  18.         'cp' => 'カーポート',
  19.         'cy' => 'サイクルポート',
  20.         'cg' => 'カーゲート',
  21.         'sh' => 'ガレージシャッター',
  22.         'wd' => 'ウッドデッキ',
  23.         'rs' => '立水栓・ガーデンシンク',
  24.         'gf' => 'ガーデンファニチャー',
  25.         'tf' => '人工芝・芝生',
  26.         'fe' => 'フェンス・柵・塀',
  27.         'ts' => '手すり',
  28.         'mp' => '門扉',
  29.         'fu' => 'ポスト・門柱・宅配ボックス',
  30.         'tr' => 'テラス屋根',
  31.         'tg' => 'テラス囲い・サンルーム',
  32.         'br' => 'バルコニー屋根',
  33.         'bl' => 'バルコニー・ベランダ',
  34.         'aw' => 'オーニング・日よけ',
  35.         'pg' => 'パーゴラ',
  36.         'mo' => '物置・収納・屋外倉庫',
  37.         'sy' => 'ストックヤード',
  38.         'gs' => 'ゴミステーション',
  39.         'wh' => '倉庫・ガレージ',
  40.         'um' => '二重窓(内窓)',
  41.         'ws' => '窓シャッター',
  42.         'mk' => '面格子・窓格子',
  43.         'fj' => '風除室・玄関フード',
  44.         'rd' => '玄関ドア',
  45.         'st' => '石材',
  46.         'sl' => '照明',
  47.         'dy' => 'DIY',
  48.     ];
  49.     public function __construct(ScraperService $scraperService)
  50.     {
  51.         $this->scraperService $scraperService;
  52.     }
  53.     /**
  54.      * Main page with buttons for product and price scraping.
  55.      *
  56.      * @Route("/%eccube_admin_route%/product/scraper", name="admin_product_scraper", methods={"GET"})
  57.      * @Template("@admin/Product/scraper.twig")
  58.      */
  59.     public function index()
  60.     {
  61.         return [
  62.             'categories'       => self::$categoryLabels,
  63.             'runningJobs'      => $this->scraperService->getRunningJobs(),
  64.             'spreadsheetLinks' => $this->scraperService->getSpreadsheetLinks(),
  65.         ];
  66.     }
  67.     /**
  68.      * Start product scraper (basic data).
  69.      *
  70.      * @Route("/%eccube_admin_route%/product/scraper/start_product", name="admin_product_scraper_start_product", methods={"POST"})
  71.      */
  72.     public function startProduct(Request $request): JsonResponse
  73.     {
  74.         if (!$this->isTokenValid()) {
  75.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  76.         }
  77.         if ($this->scraperService->isProductLocked()) {
  78.             return $this->json(['error' => '基本データ取込は既に実行中です'], 409);
  79.         }
  80.         // Parse comma-separated categories (empty = all)
  81.         $catStr trim((string) $request->request->get('categories'''));
  82.         $categories = [];
  83.         if ($catStr !== '') {
  84.             foreach (explode(','$catStr) as $c) {
  85.                 $c trim($c);
  86.                 if ($c !== '' && preg_match('/^[a-z0-9]+$/'$c)) {
  87.                     $categories[] = $c;
  88.                 }
  89.             }
  90.         }
  91.         $result $this->scraperService->startProductScraper($categories);
  92.         if ($result) {
  93.             $label = empty($categories) ? '全カテゴリ' implode(','$categories);
  94.             return $this->json([
  95.                 'status'  => 'started',
  96.                 'message' => "基本データ取込を開始しました(対象: {$label})",
  97.             ]);
  98.         }
  99.         return $this->json(['error' => '開始できませんでした'], 500);
  100.     }
  101.     /**
  102.      * Start the color scraper.
  103.      *
  104.      * @Route("/%eccube_admin_route%/product/scraper/start_color", name="admin_product_scraper_start_color", methods={"POST"})
  105.      */
  106.     public function startColor(Request $request): JsonResponse
  107.     {
  108.         if (!$this->isTokenValid()) {
  109.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  110.         }
  111.         if ($this->scraperService->isColorLocked()) {
  112.             return $this->json(['error' => 'カラー画像取込は既に実行中です'], 409);
  113.         }
  114.         $mode $request->request->get('mode''scrape');
  115.         if (!in_array($mode, ['scrape''reapply'], true)) {
  116.             $mode 'scrape';
  117.         }
  118.         $categories = [];
  119.         if ($mode === 'scrape') {
  120.             $catStr trim((string) $request->request->get('categories'''));
  121.             if ($catStr !== '') {
  122.                 foreach (explode(','$catStr) as $c) {
  123.                     $c trim($c);
  124.                     if ($c !== '' && preg_match('/^[a-z0-9]+$/'$c)) {
  125.                         $categories[] = $c;
  126.                     }
  127.                 }
  128.             }
  129.         }
  130.         $result $this->scraperService->startColorScraper($categories$mode);
  131.         if ($result) {
  132.             $modeLabel $mode === 'reapply' '再処理' '取込';
  133.             $catLabel  = empty($categories) ? '全カテゴリ' implode(','$categories);
  134.             $msg $mode === 'reapply'
  135.                 "カラー画像{$modeLabel}を開始しました"
  136.                 "カラー画像{$modeLabel}を開始しました(対象: {$catLabel})";
  137.             return $this->json(['status' => 'started''message' => $msg]);
  138.         }
  139.         return $this->json(['error' => '開始できませんでした'], 500);
  140.     }
  141.     /**
  142.      * Start price scraper for a specific category.
  143.      *
  144.      * @Route("/%eccube_admin_route%/product/scraper/start_price", name="admin_product_scraper_start_price", methods={"POST"})
  145.      */
  146.     public function startPrice(Request $request): JsonResponse
  147.     {
  148.         if (!$this->isTokenValid()) {
  149.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  150.         }
  151.         $category $request->request->get('category''');
  152.         if (!$category || !isset(self::$categoryLabels[$category])) {
  153.             return $this->json(['error' => '無効なカテゴリです'], 400);
  154.         }
  155.         if ($this->scraperService->isPriceLocked($category)) {
  156.             $label self::$categoryLabels[$category];
  157.             return $this->json(['error' => "{$label} の価格マトリクス取込は既に実行中です"], 409);
  158.         }
  159.         $maker $request->request->get('maker');
  160.         $result $this->scraperService->startPriceScraper($category$maker ?: null);
  161.         if ($result) {
  162.             $label self::$categoryLabels[$category];
  163.             return $this->json(['status' => 'started''message' => "{$label} の価格マトリクス取込を開始しました"]);
  164.         }
  165.         return $this->json(['error' => '開始できませんでした'], 500);
  166.     }
  167.     /**
  168.      * Get current status of all scrapers (AJAX polling endpoint).
  169.      *
  170.      * @Route("/%eccube_admin_route%/product/scraper/status", name="admin_product_scraper_status", methods={"GET"})
  171.      */
  172.     public function status(): JsonResponse
  173.     {
  174.         $product $this->scraperService->getProductProgress();
  175.         $productLocked $this->scraperService->isProductLocked();
  176.         // Collect price statuses for all categories
  177.         $priceStatuses = [];
  178.         foreach (self::$categoryLabels as $cat => $label) {
  179.             $locked $this->scraperService->isPriceLocked($cat);
  180.             $progress $this->scraperService->getPriceProgress($cat);
  181.             if ($locked || $progress) {
  182.                 $priceStatuses[$cat] = [
  183.                     'locked'   => $locked,
  184.                     'progress' => $progress,
  185.                 ];
  186.             }
  187.         }
  188.         return $this->json([
  189.             'product' => [
  190.                 'locked'   => $productLocked,
  191.                 'progress' => $product,
  192.             ],
  193.             'color' => [
  194.                 'locked' => $this->scraperService->isColorLocked(),
  195.             ],
  196.             'price' => $priceStatuses,
  197.         ]);
  198.     }
  199.     /**
  200.      * Start DB import from ProductALL sheet (Symfony console command).
  201.      *
  202.      * @Route("/%eccube_admin_route%/product/scraper/db_import_product", name="admin_product_scraper_db_import_product", methods={"POST"})
  203.      */
  204.     public function dbImportProduct(Request $request): JsonResponse
  205.     {
  206.         if (!$this->isTokenValid()) {
  207.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  208.         }
  209.         [$ok$message] = $this->scraperService->startDbImportProduct();
  210.         if ($ok) {
  211.             return $this->json(['status' => 'started''message' => $message]);
  212.         }
  213.         return $this->json(['error' => $message], 409);
  214.     }
  215.     /**
  216.      * Start DB import from PP_{category} sheet for a specific category.
  217.      *
  218.      * @Route("/%eccube_admin_route%/product/scraper/db_import_price", name="admin_product_scraper_db_import_price", methods={"POST"})
  219.      */
  220.     public function dbImportPrice(Request $request): JsonResponse
  221.     {
  222.         if (!$this->isTokenValid()) {
  223.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  224.         }
  225.         $category $request->request->get('category''');
  226.         if (!$category) {
  227.             return $this->json(['error' => 'カテゴリを指定してください'], 400);
  228.         }
  229.         [$ok$message] = $this->scraperService->startDbImportPrice($category);
  230.         if ($ok) {
  231.             return $this->json(['status' => 'started''message' => $message]);
  232.         }
  233.         return $this->json(['error' => $message], 409);
  234.     }
  235.     /**
  236.      * Get execution log for a scraper (for debugging).
  237.      *
  238.      * @Route("/%eccube_admin_route%/product/scraper/log", name="admin_product_scraper_log", methods={"GET"})
  239.      */
  240.     public function log(Request $request): JsonResponse
  241.     {
  242.         $type $request->query->get('type''product');
  243.         $category $request->query->get('category''');
  244.         try {
  245.             $log null;
  246.             if ($type === 'product') {
  247.                 $log $this->scraperService->getProductLog(200);
  248.             } elseif ($type === 'price' && $category) {
  249.                 $log $this->scraperService->getPriceLog($category200);
  250.             } elseif ($type === 'color') {
  251.                 $log $this->scraperService->getColorLog(200);
  252.             } elseif ($type === 'db_import_product') {
  253.                 $log $this->scraperService->getDbImportProductLog(200);
  254.             } elseif ($type === 'db_import_price' && $category) {
  255.                 $log $this->scraperService->getDbImportPriceLog($category200);
  256.             } else {
  257.                 return $this->json(['error' => 'invalid type/category'], 400);
  258.             }
  259.             // Include debug info only for 'product' request (reduces load for repeated polls)
  260.             $debug = ($type === 'product') ? $this->scraperService->getDebugInfo() : null;
  261.             return $this->json([
  262.                 'log'   => $log,
  263.                 'debug' => $debug,
  264.             ]);
  265.         } catch (\Throwable $e) {
  266.             return $this->json([
  267.                 'log'   => '(exception: ' $e->getMessage() . ')',
  268.                 'debug' => null,
  269.             ], 200);
  270.         }
  271.     }
  272.     /**
  273.      * Cancel a running scraper.
  274.      *
  275.      * @Route("/%eccube_admin_route%/product/scraper/cancel", name="admin_product_scraper_cancel", methods={"POST"})
  276.      */
  277.     public function cancel(Request $request): JsonResponse
  278.     {
  279.         if (!$this->isTokenValid()) {
  280.             return $this->json(['error' => 'Invalid CSRF token'], 403);
  281.         }
  282.         $type $request->request->get('type''');
  283.         $category $request->request->get('category''');
  284.         if ($type === 'product') {
  285.             $this->scraperService->cancelProduct();
  286.             return $this->json(['status' => 'cancelled''message' => '基本データ取込をキャンセルしました']);
  287.         }
  288.         if ($type === 'color') {
  289.             $this->scraperService->cancelColor();
  290.             return $this->json(['status' => 'cancelled''message' => 'カラー画像取込をキャンセルしました']);
  291.         }
  292.         if ($type === 'price' && $category) {
  293.             $this->scraperService->cancelPrice($category);
  294.             $label self::$categoryLabels[$category] ?? $category;
  295.             return $this->json(['status' => 'cancelled''message' => "{$label} の価格マトリクス取込をキャンセルしました"]);
  296.         }
  297.         return $this->json(['error' => '無効なリクエスト'], 400);
  298.     }
  299. }