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 @@
@csrf -
📦 1. 상품 기본 정보
@@ -94,30 +93,99 @@
- +
-
+ @php + $cardPayments = []; + $otherPayments = []; + foreach ($payments as $pm) { + $code = (string)($pm->code ?? ''); + if ($code !== '' && str_starts_with($code, 'CREDIT_CARD')) $cardPayments[] = $pm; + else $otherPayments[] = $pm; + } + + $savedPaymentsRaw = old('payment_methods', []); + if (!is_array($savedPaymentsRaw)) $savedPaymentsRaw = []; + $savedPayments = array_map('intval', $savedPaymentsRaw); + + $cardIds = array_map(fn($pm) => (int)$pm->id, $cardPayments); + + $selectedCardId = old('payment_card_method'); + if (($selectedCardId === null || $selectedCardId === '') && !empty($savedPayments)) { + $found = array_values(array_intersect($savedPayments, $cardIds)); + $selectedCardId = $found[0] ?? ''; + } + // 신규 등록(결제수단 아무것도 선택 안한 상태)일 땐 카드도 기본 1개 선택 (기존 '전체 체크' 흐름 유지) + if (($selectedCardId === null || $selectedCardId === '') && empty($savedPayments) && !empty($cardPayments)) { + $selectedCardId = (string)$cardPayments[0]->id; + } + + + // 카드 결제는 체크박스에서 제외하기 위해 제거 + $displayPayments = array_values(array_diff($savedPayments, $cardIds)); + + // 신규 등록 기본값: 기존처럼 "기타 결제수단"은 전체 체크 + $defaultAllOthersChecked = empty($savedPayments); + @endphp
- -
- @foreach($payments as $pm) - - @endforeach + + +
+
+
카드 결제 (1개 선택)
+ +
+ +
+
기타 결제수단 (복수 선택)
+
+ @foreach($otherPayments as $pm) + + @endforeach +
+
+ + @php + $savedPins = old('pin_check_methods'); + if (!is_array($savedPins) || empty($savedPins)) $savedPins = ['PIN_INSTANT']; + @endphp +
+ +
+ + + +
+
+
diff --git a/resources/views/admin/product/products/edit.blade.php b/resources/views/admin/product/products/edit.blade.php index e77563a..d7295bc 100644 --- a/resources/views/admin/product/products/edit.blade.php +++ b/resources/views/admin/product/products/edit.blade.php @@ -39,7 +39,7 @@
    @foreach ($errors->all() as $error)
  • {{ $error }}
  • - @endforeach + @endforeachZ
@endif @@ -51,6 +51,7 @@ @csrf @method('PUT') +
📦 1. 상품 기본 정보
@@ -91,35 +92,95 @@
- +
-
@php - // JSON 결제 수단 배열화 - $savedPayments = old('payment_methods', $product->allowed_payments ?? []); + $savedPaymentsRaw = old('payment_methods', $product->allowed_payments ?? []); + if (!is_array($savedPaymentsRaw)) $savedPaymentsRaw = []; + $savedPayments = array_map('intval', $savedPaymentsRaw); + + $cardPayments = []; + $otherPayments = []; + foreach ($payments as $pm) { + $code = (string)($pm->code ?? ''); + if ($code !== '' && str_starts_with($code, 'CREDIT_CARD')) $cardPayments[] = $pm; + else $otherPayments[] = $pm; + } + + $cardIds = array_map(fn($pm) => (int)$pm->id, $cardPayments); + + $selectedCardId = old('payment_card_method'); + if ($selectedCardId === null || $selectedCardId === '') { + $found = array_values(array_intersect($savedPayments, $cardIds)); + $selectedCardId = $found[0] ?? ''; + } + + // 카드 결제는 체크박스에서 제외하기 위해 제거 + $displayPayments = array_values(array_diff($savedPayments, $cardIds)); + + // 혹시 기존 데이터가 비어있다면(구상품), 기존 신규등록처럼 "기타 결제수단" 전체 체크 + $defaultAllOthersChecked = empty($savedPayments); + + $savedPins = old('pin_check_methods', $product->pin_check_methods ?? []); + if (!is_array($savedPins) || empty($savedPins)) $savedPins = ['PIN_INSTANT']; @endphp +
- -
- @foreach($payments as $pm) - - @endforeach + + +
+
+
카드 결제 (1개 선택)
+ +
+ +
+
기타 결제수단 (복수 선택)
+
+ @foreach($otherPayments as $pm) + + @endforeach +
+
+ +
+ +
+ + + +
+
+
diff --git a/resources/views/admin/product/products/index.blade.php b/resources/views/admin/product/products/index.blade.php index 41e9183..cdcf932 100644 --- a/resources/views/admin/product/products/index.blade.php +++ b/resources/views/admin/product/products/index.blade.php @@ -20,6 +20,20 @@ .p-name { font-weight: bold; font-size: 14px; margin-bottom: 4px; display: block; color: #fff; text-decoration: none; } .p-name:hover { text-decoration: underline; color: #60a5fa; } + + .badges{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;} + .badge{ + display:inline-flex;align-items:center; + padding:2px 8px;border-radius:999px; + font-size:11px;line-height:1.6; + border:1px solid rgba(0,0,0,.12); + background:rgba(0,0,0,.04); + white-space:nowrap; + } + .badge--muted{opacity:.65;} + .badge--ok{background:rgba(0,160,60,.08);border-color:rgba(0,160,60,.25);} + .badge--info{background:rgba(0,120,220,.08);border-color:rgba(0,120,220,.25);} + .badge--warn{background:rgba(220,140,0,.10);border-color:rgba(220,140,0,.25);} @endpush @@ -75,7 +89,9 @@ 이미지 카테고리 / 상품명 권종 상세 (API / 재고) - 판매 기간 / 매입 + 판매기간 + 핀확인방법 + 결제수단 상태 관리 @@ -129,12 +145,68 @@ @else
{{ \Carbon\Carbon::parse($p->sales_end_at)->format('Y-m-d H시 마감') }}
@endif + - @if($p->is_buyback_allowed) - 매입가능 - @else - 매입불가 - @endif + + @php + $pin = $p->pin_check_methods ?? null; + if (is_string($pin)) $pin = json_decode($pin, true) ?: []; + if (!is_array($pin)) $pin = []; + + $pinMap = [ + 'PIN_INSTANT' => ['label' => '핀즉시확인', 'cls' => 'badge--ok'], + 'SMS' => ['label' => 'SMS 발송', 'cls' => 'badge--info'], + 'BUYBACK' => ['label' => '재판매(매입)','cls' => 'badge--warn'], + ]; + + $pinBadges = []; + foreach ($pin as $k) { + if (isset($pinMap[$k])) $pinBadges[] = $pinMap[$k]; + } + @endphp + +
+ @forelse($pinBadges as $b) + {{ $b['label'] }} + @empty + - + @endforelse +
+ + + @php + $allowed = $p->allowed_payments ?? null; + if (is_string($allowed)) $allowed = json_decode($allowed, true) ?: []; + if (!is_array($allowed)) $allowed = []; + + // $payments 가 뷰에 넘어온다는 전제 (create/edit처럼) + $payMap = []; + if (isset($payments) && is_iterable($payments)) { + foreach ($payments as $pm) { + $id = (int)($pm->id ?? 0); + if ($id <= 0) continue; + + $name = trim((string)($pm->name ?? '')); // 있으면 name 우선 + $code = trim((string)($pm->code ?? '')); // 없으면 code + $payMap[$id] = $name !== '' ? $name : ($code !== '' ? $code : ('#'.$id)); + } + } + + $payLabels = []; + foreach ($allowed as $pid) { + $pid = (int)$pid; + if ($pid <= 0) continue; + $payLabels[] = $payMap[$pid] ?? ('#'.$pid); // fallback: #id + } + @endphp + +
+ @forelse($payLabels as $lb) + {{ $lb }} + @empty + - + @endforelse +
@if($p->status === 'ACTIVE') 판매중 @@ -146,7 +218,7 @@ @empty - 등록된 상품이 없습니다. + 등록된 상품이 없습니다. @endforelse diff --git a/resources/views/web/mypage/usage/show.blade.php b/resources/views/web/mypage/usage/show.blade.php index 898b31e..a1c35fb 100644 --- a/resources/views/web/mypage/usage/show.blade.php +++ b/resources/views/web/mypage/usage/show.blade.php @@ -91,6 +91,29 @@ // 오른쪽 영역 배너 모드 조건 $useRightBannerMode = $isCancelledAfterPaid || $isPinIssuedCompleted; + + // 핀 발행(확인) 방식: gc_products.pin_check_methods 기반 (서비스에서 내려줌) + $issueMethods = $issueMethods ?? ['PIN_INSTANT','SMS','BUYBACK']; + if (is_string($issueMethods)) $issueMethods = json_decode($issueMethods, true) ?: []; + if (!is_array($issueMethods) || empty($issueMethods)) $issueMethods = ['PIN_INSTANT','SMS','BUYBACK']; + $issueAllowed = array_fill_keys($issueMethods, true); + + $issueMissing = $issueMissing ?? []; + if (is_string($issueMissing)) $issueMissing = json_decode($issueMissing, true) ?: []; + if (!is_array($issueMissing)) $issueMissing = []; + + $issueMap = [ + 'PIN_INSTANT' => '핀번호 바로 확인', + 'SMS' => 'SMS 발송', + 'BUYBACK' => '구매상품권 판매', + ]; + $issueMissingLabels = []; + foreach ($issueMissing as $k) { + if (isset($issueMap[$k])) $issueMissingLabels[] = $issueMap[$k]; + } + + // 1개만 가능하면 그 옵션을 기본으로 열어둠 + $issueOpenKey = (count($issueMethods) === 1) ? ($issueMethods[0] ?? null) : null; @endphp @section('title', '구매내역 상세') @@ -230,71 +253,89 @@

핀 발행 선택

+ + @if(!empty($issueMissingLabels)) +
+ 이 상품은 {{ implode(', ', $issueMissingLabels) }} 기능을 지원하지 않습니다. +
+ @endif
{{-- 옵션 1 --}} -
- -
-
-

- 핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요. -

- + @if(isset($issueAllowed['PIN_INSTANT'])) +
+ +
+
+

+ 핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요. +

+ +
-
+ @endif {{-- 옵션 2 --}} -
- -
-
-

- SMS 발송 시 핀번호는 저장되지 않습니다. 문자 수신 후 즉시 확인하세요. + @if(isset($issueAllowed['SMS'])) +

+ +
+
+

+ SMS 발송 시 핀번호는 저장되지 않습니다. 문자 수신 후 즉시 확인하세요. -

- +

+ +
-
+ @endif {{-- 옵션 3 --}} -
- -
-
-

- 구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며, - 매입 처리 후 회원님 계좌로 입금됩니다. + @if(isset($issueAllowed['BUYBACK'])) +

+ +
+
+

+ 구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며, + 매입 처리 후 회원님 계좌로 입금됩니다. -

- +

+ +
-
+ @endif + + @if(empty($issueAllowed)) +
+ 현재 이 상품은 핀 발행 방식이 설정되지 않았습니다. 고객센터로 문의해 주세요. +
+ @endif
@endif