diff --git a/app/Http/Controllers/Admin/Product/AdminProductController.php b/app/Http/Controllers/Admin/Product/AdminProductController.php index c4d2caf..06ad52e 100644 --- a/app/Http/Controllers/Admin/Product/AdminProductController.php +++ b/app/Http/Controllers/Admin/Product/AdminProductController.php @@ -39,15 +39,31 @@ final class AdminProductController private function validateProduct(Request $request): array { + $methods = $request->input('payment_methods', []); + if (!is_array($methods)) $methods = []; + + $card = $request->input('payment_card_method'); + if ($card !== null && $card !== '') { + $methods[] = (int)$card; + } + + $request->merge([ + 'payment_methods' => array_values(array_unique(array_map('intval', $methods))), + 'pin_check_methods' => array_values(array_unique((array)$request->input('pin_check_methods', []))), + ]); + return $request->validate([ 'category_id' => ['required', 'integer'], 'name' => ['required', 'string', 'max:150'], 'thumbnail_media_id' => ['nullable', 'integer'], 'status' => ['required', 'in:ACTIVE,HIDDEN,SOLD_OUT'], 'product_type' => ['required', 'in:ONLINE,DELIVERY'], - 'is_buyback_allowed' => ['required', 'in:0,1'], 'payment_methods' => ['required', 'array', 'min:1'], 'payment_methods.*' => ['integer'], + + 'pin_check_methods' => ['required', 'array', 'min:1'], + 'pin_check_methods.*' => ['string', 'in:PIN_INSTANT,SMS,BUYBACK'], + 'is_always_on_sale' => ['required', 'in:0,1'], 'sales_start_at' => ['nullable', 'date'], 'sales_end_at' => ['nullable', 'date', 'after_or_equal:sales_start_at'], @@ -74,6 +90,7 @@ final class AdminProductController ], [ 'payment_methods.required' => '최소 1개 이상의 결제 수단을 선택해주세요.', + 'pin_check_methods.required' => '핀번호 확인 방법을 최소 1개 이상 선택해주세요.', 'skus.required' => '최소 1개 이상의 권종을 등록해야 합니다.', 'skus.*.api_provider_id.required_if' => 'API 연동 시 연동사를 선택해야 합니다.', 'skus.*.api_product_code.required_if' => 'API 연동 시 상품 코드를 선택해야 합니다.', diff --git a/app/Repositories/Mypage/UsageRepository.php b/app/Repositories/Mypage/UsageRepository.php index 3759e5a..abad402 100644 --- a/app/Repositories/Mypage/UsageRepository.php +++ b/app/Repositories/Mypage/UsageRepository.php @@ -7,6 +7,46 @@ use Illuminate\Support\Facades\DB; final class UsageRepository { + + /** + * 상품별 핀 발행(확인) 방법 조회 + * - gc_products.pin_check_methods: JSON 배열 + * - gc_products.is_buyback_allowed: BUYBACK 안전장치 + */ + public function getProductsIssueOptions(array $productIds): array + { + $ids = array_values(array_unique(array_filter(array_map('intval', $productIds), fn($n) => $n > 0))); + if (empty($ids)) return []; + + $rows = DB::table('gc_products') + ->select(['id', 'pin_check_methods', 'is_buyback_allowed']) + ->whereIn('id', $ids) + ->get(); + + $out = []; + foreach ($rows as $r) { + $raw = $r->pin_check_methods ?? null; + $hasPinCheckMethods = !($raw === null || trim((string)$raw) === ''); + + $out[(int)$r->id] = [ + 'pin_check_methods' => $this->decodeJsonArray($raw), + 'has_pin_check_methods' => $hasPinCheckMethods, + 'is_buyback_allowed' => (bool)($r->is_buyback_allowed ?? false), + ]; + } + return $out; + } + + private function decodeJsonArray($v): array + { + if ($v === null) return []; + if (is_array($v)) return $v; + $s = trim((string)$v); + if ($s === '') return []; + $decoded = json_decode($s, true); + return is_array($decoded) ? $decoded : []; + } + public function findAttemptWithOrder(int $attemptId): ?object { return DB::table('gc_payment_attempts as a') @@ -41,6 +81,7 @@ final class UsageRepository 'o.mem_no as order_mem_no', 'o.stat_pay as order_stat_pay', 'o.products_name as order_product_name', + 'o.products_id as order_product_id', 'o.provider as order_provider', 'o.pay_method as order_pay_method', 'o.pg_tid as order_pg_tid', diff --git a/app/Services/Admin/Product/AdminProductService.php b/app/Services/Admin/Product/AdminProductService.php index 03624ad..302d09a 100644 --- a/app/Services/Admin/Product/AdminProductService.php +++ b/app/Services/Admin/Product/AdminProductService.php @@ -22,6 +22,7 @@ final class AdminProductService return [ 'products' => $this->repo->paginateProducts($filters), 'categories' => $this->categoryService->getCategoryTree(), + 'payments' => DB::table('gc_payment_methods')->where('is_active', 1)->orderBy('sort_order')->get()->toArray(), ]; } @@ -45,6 +46,7 @@ final class AdminProductService 'name' => trim($input['name']), 'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null, 'purchase_type' => $input['purchase_type'] ?? 'MULTI_SKU', + 'pin_check_methods' => json_encode($input['pin_check_methods'] ?? [], JSON_UNESCAPED_UNICODE), 'min_buy_qty' => (int)($input['min_buy_qty'] ?? 1), 'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0), 'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0), @@ -119,6 +121,9 @@ final class AdminProductService // JSON으로 저장된 결제수단 배열로 복원 $product->allowed_payments = json_decode($product->allowed_payments ?? '[]', true); + $product->pin_check_methods = json_decode($product->pin_check_methods ?? '[]', true); + if (!is_array($product->pin_check_methods)) $product->pin_check_methods = []; + // 썸네일 정보 가져오기 $thumbnail = null; if ($product->thumbnail_media_id) { @@ -147,6 +152,7 @@ final class AdminProductService 'name' => trim($input['name']), 'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null, 'purchase_type' => $input['purchase_type'] ?? 'MULTI_SKU', + 'pin_check_methods' => json_encode($input['pin_check_methods'] ?? [], JSON_UNESCAPED_UNICODE), 'min_buy_qty' => (int)($input['min_buy_qty'] ?? 1), 'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0), 'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0), diff --git a/app/Services/Mypage/UsageService.php b/app/Services/Mypage/UsageService.php index 4f703c4..46d7114 100644 --- a/app/Services/Mypage/UsageService.php +++ b/app/Services/Mypage/UsageService.php @@ -148,6 +148,11 @@ final class UsageService $items = $this->repo->getOrderItems($orderId); + // ✅ 핀 발행 선택(핀확인방법): 상품 설정(gc_products.pin_check_methods) 기반 + // - 주문에 여러 상품이 섞였을 수 있으므로 "공통으로 가능한 방식"(교집합)만 허용 + $orderProductId = (int)($row->order_product_id ?? 0); + $issue = $this->resolveIssueOptions($orderProductId, $items); + $requiredQty = 0; foreach ($items as $it) $requiredQty += (int)($it->qty ?? 0); @@ -170,6 +175,10 @@ final class UsageService 'items' => $this->itemsViewModel($items), 'productname' => $row->order_product_name, + // 핀 발행 선택 UI 제어용 + 'issueMethods' => $issue['methods'], + 'issueMissing' => $issue['missing'], + 'requiredQty' => $requiredQty, 'assignedPinsCount' => $assignedPinsCount, 'pinsSummary' => $pinsSummary, @@ -282,6 +291,106 @@ final class UsageService return $out; } + /** + * 주문 아이템에서 상품ID 후보를 추출하고, 상품 설정(pin_check_methods)에 따라 + * 사용자 페이지의 "핀 발행 선택" 옵션을 계산한다. + * + * - 여러 상품이 한 주문에 섞일 수 있으므로 교집합만 허용 + * - 설정이 비어있거나 조회 실패 시: 기존 UI 깨짐 방지를 위해 3개 모두 허용 + */ + private function resolveIssueOptions(int $orderProductId, $items): array + { + $all = ['PIN_INSTANT', 'SMS', 'BUYBACK']; + + // ✅ 주문이 단일 상품 구조면 order.product_id를 우선 사용 + $productIds = []; + if ($orderProductId > 0) { + $productIds = [$orderProductId]; + } else { + // (구조가 바뀌거나 다상품 주문일 수 있는 경우) 아이템에서 추출 + $productIds = $this->extractProductIdsFromOrderItems($items); + } + + $opts = $this->repo->getProductsIssueOptions($productIds); + if (empty($opts)) { + // 조회 실패/미설정 시 UI 깨짐 방지: 3개 모두 허용 + return ['methods' => $all, 'missing' => [], 'product_ids' => $productIds]; + } + + $intersection = null; + foreach ($opts as $pid => $opt) { + $m = $this->normalizeIssueMethods($opt); + + $intersection = ($intersection === null) + ? $m + : array_values(array_intersect($intersection, $m)); + } + + if (empty($intersection)) $intersection = ['PIN_INSTANT']; + + // 표시 순서 고정 + $ordered = array_values(array_filter($all, fn($k) => in_array($k, $intersection, true))); + $missing = array_values(array_diff($all, $ordered)); + + return ['methods' => $ordered, 'missing' => $missing, 'product_ids' => array_map('intval', array_keys($opts))]; + } + + /** + * 상품 1개의 옵션을 "표시용 methods"로 정규화 + * + * - pin_check_methods가 명시적으로 설정된 경우: 그 값을 그대로 신뢰 (BUYBACK 포함/제외도 여기서 결정) + * - pin_check_methods가 아직 비어있는(레거시) 경우: 기본은 PIN_INSTANT+SMS, BUYBACK은 is_buyback_allowed로만 노출 + */ + private function normalizeIssueMethods(array $opt): array + { + $explicit = !empty($opt['has_pin_check_methods']); + + $m = $opt['pin_check_methods'] ?? []; + if (!is_array($m)) $m = []; + $m = array_values(array_unique(array_filter(array_map('strval', $m)))); + + if ($explicit) { + // 명시 설정인데 비어있으면 안전하게 즉시확인만 + if (empty($m)) return ['PIN_INSTANT']; + return $m; + } + + // 레거시: 기본 2개 + 매입은 기존 플래그로만 + $out = ['PIN_INSTANT', 'SMS']; + if (!empty($opt['is_buyback_allowed'])) $out[] = 'BUYBACK'; + return $out; + } + + private function extractProductIdsFromOrderItems($items): array + { + $ids = []; + + foreach ($items as $it) { + $pid = null; + + // 1) 컬럼이 존재하는 경우 (gc_pin_order_items.product_id 등) + if (is_object($it) && property_exists($it, 'product_id') && is_numeric($it->product_id)) { + $pid = (int)$it->product_id; + } + + // 2) meta(JSON) 안에 들어있는 경우 + if (!$pid) { + $meta = $this->jsonDecodeOrArray($it->meta ?? null); + foreach (['product_id', 'gc_product_id', 'productId', 'pid', 'product'] as $k) { + if (isset($meta[$k]) && is_numeric($meta[$k])) { + $pid = (int)$meta[$k]; + break; + } + } + } + + if ($pid && $pid > 0) $ids[] = $pid; + } + + $ids = array_values(array_unique(array_filter($ids, fn($n) => $n > 0))); + return $ids; + } + private function extractVactInfo(object $row): array { $candidates = [ diff --git a/resources/views/admin/partials/sidebar.blade.php b/resources/views/admin/partials/sidebar.blade.php index c49646a..ed33325 100644 --- a/resources/views/admin/partials/sidebar.blade.php +++ b/resources/views/admin/partials/sidebar.blade.php @@ -51,8 +51,8 @@ ['label' => '카테고리 관리', 'route' => 'admin.categories.index', 'roles' => ['super_admin','product']], ['label' => '결제/매입/출금 수수료 관리', 'route' => 'admin.fees.index', 'roles' => ['super_admin','product']], ['label' => 'API 연동판매 코드 관리', 'route' => 'admin.sale-codes.index', 'roles' => ['super_admin','product']], - ['label' => '상품 이미지 라이브러리 관리', 'route' => 'admin.media.index', 'roles' => ['super_admin','product']], - ['label' => '판매 상품등록', 'route' => 'admin.products.index', 'roles' => ['super_admin','product']], + ['label' => '이미지 라이브러리 관리', 'route' => 'admin.media.index', 'roles' => ['super_admin','product']], + ['label' => '판매상품관리', 'route' => 'admin.products.index', 'roles' => ['super_admin','product']], ], ], [ diff --git a/resources/views/admin/product/products/create.blade.php b/resources/views/admin/product/products/create.blade.php index c2b4bb2..ee9b0b6 100644 --- a/resources/views/admin/product/products/create.blade.php +++ b/resources/views/admin/product/products/create.blade.php @@ -53,7 +53,6 @@