이용내역 리스트 / 뷰

This commit is contained in:
sungro815 2026-03-04 08:37:08 +09:00
parent 9825350372
commit 466cb89307
9 changed files with 506 additions and 91 deletions

View File

@ -39,15 +39,31 @@ final class AdminProductController
private function validateProduct(Request $request): array 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([ return $request->validate([
'category_id' => ['required', 'integer'], 'category_id' => ['required', 'integer'],
'name' => ['required', 'string', 'max:150'], 'name' => ['required', 'string', 'max:150'],
'thumbnail_media_id' => ['nullable', 'integer'], 'thumbnail_media_id' => ['nullable', 'integer'],
'status' => ['required', 'in:ACTIVE,HIDDEN,SOLD_OUT'], 'status' => ['required', 'in:ACTIVE,HIDDEN,SOLD_OUT'],
'product_type' => ['required', 'in:ONLINE,DELIVERY'], 'product_type' => ['required', 'in:ONLINE,DELIVERY'],
'is_buyback_allowed' => ['required', 'in:0,1'],
'payment_methods' => ['required', 'array', 'min:1'], 'payment_methods' => ['required', 'array', 'min:1'],
'payment_methods.*' => ['integer'], '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'], 'is_always_on_sale' => ['required', 'in:0,1'],
'sales_start_at' => ['nullable', 'date'], 'sales_start_at' => ['nullable', 'date'],
'sales_end_at' => ['nullable', 'date', 'after_or_equal:sales_start_at'], 'sales_end_at' => ['nullable', 'date', 'after_or_equal:sales_start_at'],
@ -74,6 +90,7 @@ final class AdminProductController
], [ ], [
'payment_methods.required' => '최소 1개 이상의 결제 수단을 선택해주세요.', 'payment_methods.required' => '최소 1개 이상의 결제 수단을 선택해주세요.',
'pin_check_methods.required' => '핀번호 확인 방법을 최소 1개 이상 선택해주세요.',
'skus.required' => '최소 1개 이상의 권종을 등록해야 합니다.', 'skus.required' => '최소 1개 이상의 권종을 등록해야 합니다.',
'skus.*.api_provider_id.required_if' => 'API 연동 시 연동사를 선택해야 합니다.', 'skus.*.api_provider_id.required_if' => 'API 연동 시 연동사를 선택해야 합니다.',
'skus.*.api_product_code.required_if' => 'API 연동 시 상품 코드를 선택해야 합니다.', 'skus.*.api_product_code.required_if' => 'API 연동 시 상품 코드를 선택해야 합니다.',

View File

@ -7,6 +7,46 @@ use Illuminate\Support\Facades\DB;
final class UsageRepository 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 public function findAttemptWithOrder(int $attemptId): ?object
{ {
return DB::table('gc_payment_attempts as a') return DB::table('gc_payment_attempts as a')
@ -41,6 +81,7 @@ final class UsageRepository
'o.mem_no as order_mem_no', 'o.mem_no as order_mem_no',
'o.stat_pay as order_stat_pay', 'o.stat_pay as order_stat_pay',
'o.products_name as order_product_name', 'o.products_name as order_product_name',
'o.products_id as order_product_id',
'o.provider as order_provider', 'o.provider as order_provider',
'o.pay_method as order_pay_method', 'o.pay_method as order_pay_method',
'o.pg_tid as order_pg_tid', 'o.pg_tid as order_pg_tid',

View File

@ -22,6 +22,7 @@ final class AdminProductService
return [ return [
'products' => $this->repo->paginateProducts($filters), 'products' => $this->repo->paginateProducts($filters),
'categories' => $this->categoryService->getCategoryTree(), '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']), 'name' => trim($input['name']),
'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null, 'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null,
'purchase_type' => $input['purchase_type'] ?? 'MULTI_SKU', '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), 'min_buy_qty' => (int)($input['min_buy_qty'] ?? 1),
'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0), 'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0),
'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0), 'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0),
@ -119,6 +121,9 @@ final class AdminProductService
// JSON으로 저장된 결제수단 배열로 복원 // JSON으로 저장된 결제수단 배열로 복원
$product->allowed_payments = json_decode($product->allowed_payments ?? '[]', true); $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; $thumbnail = null;
if ($product->thumbnail_media_id) { if ($product->thumbnail_media_id) {
@ -147,6 +152,7 @@ final class AdminProductService
'name' => trim($input['name']), 'name' => trim($input['name']),
'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null, 'thumbnail_media_id' => !empty($input['thumbnail_media_id']) ? (int)$input['thumbnail_media_id'] : null,
'purchase_type' => $input['purchase_type'] ?? 'MULTI_SKU', '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), 'min_buy_qty' => (int)($input['min_buy_qty'] ?? 1),
'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0), 'max_buy_qty' => (int)($input['max_buy_qty'] ?? 0),
'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0), 'max_buy_amount' => (int)($input['max_buy_amount'] ?? 0),

View File

@ -148,6 +148,11 @@ final class UsageService
$items = $this->repo->getOrderItems($orderId); $items = $this->repo->getOrderItems($orderId);
// ✅ 핀 발행 선택(핀확인방법): 상품 설정(gc_products.pin_check_methods) 기반
// - 주문에 여러 상품이 섞였을 수 있으므로 "공통으로 가능한 방식"(교집합)만 허용
$orderProductId = (int)($row->order_product_id ?? 0);
$issue = $this->resolveIssueOptions($orderProductId, $items);
$requiredQty = 0; $requiredQty = 0;
foreach ($items as $it) $requiredQty += (int)($it->qty ?? 0); foreach ($items as $it) $requiredQty += (int)($it->qty ?? 0);
@ -170,6 +175,10 @@ final class UsageService
'items' => $this->itemsViewModel($items), 'items' => $this->itemsViewModel($items),
'productname' => $row->order_product_name, 'productname' => $row->order_product_name,
// 핀 발행 선택 UI 제어용
'issueMethods' => $issue['methods'],
'issueMissing' => $issue['missing'],
'requiredQty' => $requiredQty, 'requiredQty' => $requiredQty,
'assignedPinsCount' => $assignedPinsCount, 'assignedPinsCount' => $assignedPinsCount,
'pinsSummary' => $pinsSummary, 'pinsSummary' => $pinsSummary,
@ -282,6 +291,106 @@ final class UsageService
return $out; 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 private function extractVactInfo(object $row): array
{ {
$candidates = [ $candidates = [

View File

@ -51,8 +51,8 @@
['label' => '카테고리 관리', 'route' => 'admin.categories.index', 'roles' => ['super_admin','product']], ['label' => '카테고리 관리', 'route' => 'admin.categories.index', 'roles' => ['super_admin','product']],
['label' => '결제/매입/출금 수수료 관리', 'route' => 'admin.fees.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' => 'API 연동판매 코드 관리', 'route' => 'admin.sale-codes.index', 'roles' => ['super_admin','product']],
['label' => '상품 이미지 라이브러리 관리', 'route' => 'admin.media.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.products.index', 'roles' => ['super_admin','product']],
], ],
], ],
[ [

View File

@ -53,7 +53,6 @@
<form action="{{ route('admin.products.store') }}" method="POST"> <form action="{{ route('admin.products.store') }}" method="POST">
@csrf @csrf
<div class="a-card" style="padding: 24px; margin-bottom: 24px; border-top: 3px solid #3b82f6;"> <div class="a-card" style="padding: 24px; margin-bottom: 24px; border-top: 3px solid #3b82f6;">
<div class="section-title">📦 1. 상품 기본 정보</div> <div class="section-title">📦 1. 상품 기본 정보</div>
<div style="display: flex; gap: 24px;"> <div style="display: flex; gap: 24px;">
@ -94,30 +93,99 @@
</select> </select>
</div> </div>
<div class="a-field"> <div class="a-field">
<label class="a-label">상품 유형 / 매입 허용</label> <label class="a-label">상품 유형</label>
<div style="display:flex; gap:10px;"> <div style="display:flex; gap:10px;">
<select name="product_type" class="a-input" style="flex:1;"> <select name="product_type" class="a-input" style="flex:1;">
<option value="ONLINE" {{ old('product_type') == 'ONLINE' ? 'selected' : '' }}>온라인 발송</option> <option value="ONLINE" {{ old('product_type') == 'ONLINE' ? 'selected' : '' }}>온라인 발송</option>
<option value="DELIVERY" {{ old('product_type') == 'DELIVERY' ? 'selected' : '' }}>실물 배송</option> <option value="DELIVERY" {{ old('product_type') == 'DELIVERY' ? 'selected' : '' }}>실물 배송</option>
</select> </select>
<select name="is_buyback_allowed" class="a-input" style="flex:1;">
<option value="0" {{ old('is_buyback_allowed') == '0' ? 'selected' : '' }}>매입 불가</option>
<option value="1" {{ old('is_buyback_allowed') == '1' ? 'selected' : '' }}>매입 허용</option>
</select>
</div> </div>
</div> </div>
@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
<div class="a-field" style="grid-column: span 2;"> <div class="a-field" style="grid-column: span 2;">
<label class="a-label">결제 수단 허용 (다중 선택)</label> <label class="a-label">결제 수단 허용</label>
<div class="check-group">
@foreach($payments as $pm) <div style="display:flex; gap:18px; flex-wrap:wrap; align-items:flex-start;">
<label class="check-item"> <div style="min-width:260px;">
<input type="checkbox" name="payment_methods[]" value="{{ $pm->id }}" class="payment-checkbox" data-name="{{ $pm->name }}" <div class="a-muted" style="font-size:12px; margin-bottom:6px;">카드 결제 (1 선택)</div>
{{ in_array($pm->id, old('payment_methods', [])) || empty(old('payment_methods')) ? 'checked' : '' }}> <select name="payment_card_method" class="a-input" style="width:260px;">
<span>{{ $pm->name }}</span> <option value="">-- 카드 결제 선택 안함 --</option>
</label> @foreach($cardPayments as $pm)
@endforeach <option value="{{ $pm->id }}" {{ (string)$selectedCardId === (string)$pm->id ? 'selected' : '' }}>
{{ $pm->name }}{{ $pm->code ? ' ('.$pm->code.')' : '' }}
</option>
@endforeach
</select>
</div>
<div style="flex:1; min-width:260px;">
<div class="a-muted" style="font-size:12px; margin-bottom:6px;">기타 결제수단 (복수 선택)</div>
<div class="check-group">
@foreach($otherPayments as $pm)
<label class="check-item">
<input type="checkbox" name="payment_methods[]" value="{{ $pm->id }}" class="payment-checkbox" data-name="{{ $pm->name }}"
{{ in_array((int)$pm->id, $displayPayments) || $defaultAllOthersChecked ? 'checked' : '' }}>
<span>{{ $pm->name }}{{ $pm->code ? ' ('.$pm->code.')' : '' }}</span>
</label>
@endforeach
</div>
</div>
</div> </div>
</div> </div>
@php
$savedPins = old('pin_check_methods');
if (!is_array($savedPins) || empty($savedPins)) $savedPins = ['PIN_INSTANT'];
@endphp
<div class="a-field" style="grid-column: span 2;">
<label class="a-label">핀번호 확인 방법 (복수 선택)</label>
<div class="check-group">
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="PIN_INSTANT" {{ in_array('PIN_INSTANT', $savedPins, true) ? 'checked' : '' }}>
<span>핀즉시확인</span>
</label>
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="SMS" {{ in_array('SMS', $savedPins, true) ? 'checked' : '' }}>
<span>SMS 발송</span>
</label>
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="BUYBACK" {{ in_array('BUYBACK', $savedPins, true) ? 'checked' : '' }}>
<span>재판매(매입)</span>
</label>
</div>
</div>
<div class="a-field" style="grid-column: span 2;"> <div class="a-field" style="grid-column: span 2;">
<label class="a-label">판매 기간 설정</label> <label class="a-label">판매 기간 설정</label>
<div style="display:flex; gap:10px; align-items:center;"> <div style="display:flex; gap:10px; align-items:center;">

View File

@ -39,7 +39,7 @@
<ul style="color: #fda4af; margin-top: 8px; margin-bottom: 0; padding-left: 20px; font-size: 13px;"> <ul style="color: #fda4af; margin-top: 8px; margin-bottom: 0; padding-left: 20px; font-size: 13px;">
@foreach ($errors->all() as $error) @foreach ($errors->all() as $error)
<li>{{ $error }}</li> <li>{{ $error }}</li>
@endforeach @endforeachZ
</ul> </ul>
</div> </div>
@endif @endif
@ -51,6 +51,7 @@
<form action="{{ route('admin.products.update', $product->id) }}" method="POST"> <form action="{{ route('admin.products.update', $product->id) }}" method="POST">
@csrf @csrf
@method('PUT') @method('PUT')
<div class="a-card" style="padding: 24px; margin-bottom: 24px; border-top: 3px solid #3b82f6;"> <div class="a-card" style="padding: 24px; margin-bottom: 24px; border-top: 3px solid #3b82f6;">
<div class="section-title">📦 1. 상품 기본 정보</div> <div class="section-title">📦 1. 상품 기본 정보</div>
<div style="display: flex; gap: 24px;"> <div style="display: flex; gap: 24px;">
@ -91,35 +92,95 @@
</select> </select>
</div> </div>
<div class="a-field"> <div class="a-field">
<label class="a-label">상품 유형 / 매입 허용</label> <label class="a-label">상품 유형</label>
<div style="display:flex; gap:10px;"> <div style="display:flex; gap:10px;">
<select name="product_type" class="a-input" style="flex:1;"> <select name="product_type" class="a-input" style="flex:1;">
<option value="ONLINE" {{ old('product_type', $product->product_type) == 'ONLINE' ? 'selected' : '' }}>온라인 발송</option> <option value="ONLINE" {{ old('product_type', $product->product_type) == 'ONLINE' ? 'selected' : '' }}>온라인 발송</option>
<option value="DELIVERY" {{ old('product_type', $product->product_type) == 'DELIVERY' ? 'selected' : '' }}>실물 배송</option> <option value="DELIVERY" {{ old('product_type', $product->product_type) == 'DELIVERY' ? 'selected' : '' }}>실물 배송</option>
</select> </select>
<select name="is_buyback_allowed" class="a-input" style="flex:1;">
<option value="0" {{ old('is_buyback_allowed', $product->is_buyback_allowed) == '0' ? 'selected' : '' }}>매입 불가</option>
<option value="1" {{ old('is_buyback_allowed', $product->is_buyback_allowed) == '1' ? 'selected' : '' }}>매입 허용</option>
</select>
</div> </div>
</div> </div>
@php @php
// JSON 결제 수단 배열화 $savedPaymentsRaw = old('payment_methods', $product->allowed_payments ?? []);
$savedPayments = 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 @endphp
<div class="a-field" style="grid-column: span 2;"> <div class="a-field" style="grid-column: span 2;">
<label class="a-label">결제 수단 허용 (다중 선택)</label> <label class="a-label">결제 수단 허용</label>
<div class="check-group">
@foreach($payments as $pm) <div style="display:flex; gap:18px; flex-wrap:wrap; align-items:flex-start;">
<label class="check-item"> <div style="min-width:260px;">
<input type="checkbox" name="payment_methods[]" value="{{ $pm->id }}" class="payment-checkbox" data-name="{{ $pm->name }}" <div class="a-muted" style="font-size:12px; margin-bottom:6px;">카드 결제 (1 선택)</div>
{{ in_array($pm->id, $savedPayments) ? 'checked' : '' }}> <select name="payment_card_method" class="a-input" style="width:260px;">
<span>{{ $pm->name }}</span> <option value="">-- 카드 결제 선택 안함 --</option>
</label> @foreach($cardPayments as $pm)
@endforeach <option value="{{ $pm->id }}" {{ (string)$selectedCardId === (string)$pm->id ? 'selected' : '' }}>
{{ $pm->name }}{{ $pm->code ? ' ('.$pm->code.')' : '' }}
</option>
@endforeach
</select>
</div>
<div style="flex:1; min-width:260px;">
<div class="a-muted" style="font-size:12px; margin-bottom:6px;">기타 결제수단 (복수 선택)</div>
<div class="check-group">
@foreach($otherPayments as $pm)
<label class="check-item">
<input type="checkbox" name="payment_methods[]" value="{{ $pm->id }}" class="payment-checkbox" data-name="{{ $pm->name }}"
{{ in_array((int)$pm->id, $displayPayments) || $defaultAllOthersChecked ? 'checked' : '' }}>
<span>{{ $pm->name }}{{ $pm->code ? ' ('.$pm->code.')' : '' }}</span>
</label>
@endforeach
</div>
</div>
</div> </div>
</div> </div>
<div class="a-field" style="grid-column: span 2;">
<label class="a-label">핀번호 확인 방법 (복수 선택)</label>
<div class="check-group">
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="PIN_INSTANT" {{ in_array('PIN_INSTANT', $savedPins, true) ? 'checked' : '' }}>
<span>핀즉시확인</span>
</label>
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="SMS" {{ in_array('SMS', $savedPins, true) ? 'checked' : '' }}>
<span>SMS 발송</span>
</label>
<label class="check-item">
<input type="checkbox" name="pin_check_methods[]" value="BUYBACK" {{ in_array('BUYBACK', $savedPins, true) ? 'checked' : '' }}>
<span>재판매(이게 선택이 되면 해당 상품권은 핀발행 없이 바로 매입/출금 됩니다.)</span>
</label>
</div>
</div>
<div class="a-field" style="grid-column: span 2;"> <div class="a-field" style="grid-column: span 2;">
<label class="a-label">판매 기간 설정</label> <label class="a-label">판매 기간 설정</label>
<div style="display:flex; gap:10px; align-items:center;"> <div style="display:flex; gap:10px; align-items:center;">

View File

@ -20,6 +20,20 @@
.p-name { font-weight: bold; font-size: 14px; margin-bottom: 4px; display: block; color: #fff; text-decoration: none; } .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; } .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);}
</style> </style>
@endpush @endpush
@ -75,7 +89,9 @@
<th style="width: 70px;">이미지</th> <th style="width: 70px;">이미지</th>
<th style="width: 250px;">카테고리 / 상품명</th> <th style="width: 250px;">카테고리 / 상품명</th>
<th>권종 상세 (API / 재고)</th> <th>권종 상세 (API / 재고)</th>
<th style="text-align: center; width: 140px;">판매 기간 / 매입</th> <th style="text-align: center; width: 100px;">판매기간</th>
<th style="text-align:center; width: 100px;">핀확인방법</th>
<th style="text-align:center; width: 100px;">결제수단</th>
<th style="width: 80px; text-align: center;">상태</th> <th style="width: 80px; text-align: center;">상태</th>
<th style="width: 80px; text-align: center;">관리</th> <th style="width: 80px; text-align: center;">관리</th>
</tr> </tr>
@ -129,12 +145,68 @@
@else @else
<div style="color: #f59e0b; margin-bottom: 4px;">{{ \Carbon\Carbon::parse($p->sales_end_at)->format('Y-m-d H시 마감') }}</div> <div style="color: #f59e0b; margin-bottom: 4px;">{{ \Carbon\Carbon::parse($p->sales_end_at)->format('Y-m-d H시 마감') }}</div>
@endif @endif
</td>
@if($p->is_buyback_allowed) <td style="text-align: center; font-size: 12px;">
<span class="pill pill--outline pill--info" style="font-size: 10px;">매입가능</span> @php
@else $pin = $p->pin_check_methods ?? null;
<span class="pill pill--outline pill--muted" style="font-size: 10px;">매입불가</span> if (is_string($pin)) $pin = json_decode($pin, true) ?: [];
@endif 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
<div class="badges">
@forelse($pinBadges as $b)
<span class="badge {{ $b['cls'] }}">{{ $b['label'] }}</span>
@empty
<span class="badge badge--muted">-</span>
@endforelse
</div>
</td>
<td style="text-align:center; font-size:12px;">
@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
<div class="badges">
@forelse($payLabels as $lb)
<span class="badge">{{ $lb }}</span>
@empty
<span class="badge badge--muted">-</span>
@endforelse
</div>
</td> </td>
<td style="text-align: center;"> <td style="text-align: center;">
@if($p->status === 'ACTIVE') <span class="pill pill--ok">판매중</span> @if($p->status === 'ACTIVE') <span class="pill pill--ok">판매중</span>
@ -146,7 +218,7 @@
</td> </td>
</tr> </tr>
@empty @empty
<tr><td colspan="7" style="text-align: center; padding: 40px;">등록된 상품이 없습니다.</td></tr> <tr><td colspan="9" style="text-align: center; padding: 40px;">등록된 상품이 없습니다.</td></tr>
@endforelse @endforelse
</tbody> </tbody>
</table> </table>

View File

@ -91,6 +91,29 @@
// 오른쪽 영역 배너 모드 조건 // 오른쪽 영역 배너 모드 조건
$useRightBannerMode = $isCancelledAfterPaid || $isPinIssuedCompleted; $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 @endphp
@section('title', '구매내역 상세') @section('title', '구매내역 상세')
@ -230,71 +253,89 @@
<div class="issue-panel"> <div class="issue-panel">
<div class="issue-panel__head"> <div class="issue-panel__head">
<h3 class="issue-panel__title"> 발행 선택</h3> <h3 class="issue-panel__title"> 발행 선택</h3>
@if(!empty($issueMissingLabels))
<div class="muted" style="font-size:12px;margin-top:6px;">
상품은 {{ implode(', ', $issueMissingLabels) }} 기능을 지원하지 않습니다.
</div>
@endif
</div> </div>
<div class="issue-picker" id="issuePicker"> <div class="issue-picker" id="issuePicker">
{{-- 옵션 1 --}} {{-- 옵션 1 --}}
<div class="issue-option" data-issue-card="view"> @if(isset($issueAllowed['PIN_INSTANT']))
<button type="button" class="issue-option__toggle" data-issue-toggle> <div class="issue-option {{ $issueOpenKey==='PIN_INSTANT' ? 'is-active' : '' }}" data-issue-card="view">
<div class="issue-option__kicker">즉시 확인</div> <button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__title">핀번호 바로 확인</div> <div class="issue-option__kicker">즉시 확인</div>
<div class="issue-option__subtitle">안전하게 핀번호를 직접 확인합니다.</div> <div class="issue-option__title">핀번호 바로 확인</div>
<span class="issue-option__chev" aria-hidden="true"></span> <div class="issue-option__subtitle">안전하게 핀번호를 직접 확인합니다.</div>
</button> <span class="issue-option__chev" aria-hidden="true"></span>
<div class="issue-option__detail"> </button>
<div class="issue-option__detail-inner"> <div class="issue-option__detail">
<p class="issue-option__detail-text"> <div class="issue-option__detail-inner">
핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요. <p class="issue-option__detail-text">
</p> 핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요.
<button id="btnIssueView" type="button" class="issue-run issue-run--dark"> </p>
핀번호 바로 확인 실행 <button id="btnIssueView" type="button" class="issue-run issue-run--dark">
</button> 핀번호 바로 확인 실행
</button>
</div>
</div> </div>
</div> </div>
</div> @endif
{{-- 옵션 2 --}} {{-- 옵션 2 --}}
<div class="issue-option" data-issue-card="sms"> @if(isset($issueAllowed['SMS']))
<button type="button" class="issue-option__toggle" data-issue-toggle> <div class="issue-option {{ $issueOpenKey==='SMS' ? 'is-active' : '' }}" data-issue-card="sms">
<div class="issue-option__kicker">문자 발송</div> <button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__title">SMS 발송</div> <div class="issue-option__kicker">문자 발송</div>
<div class="issue-option__subtitle">문자로 핀번호를 전송합니다.</div> <div class="issue-option__title">SMS 발송</div>
<span class="issue-option__chev" aria-hidden="true"></span> <div class="issue-option__subtitle">문자로 핀번호를 전송합니다.</div>
</button> <span class="issue-option__chev" aria-hidden="true"></span>
<div class="issue-option__detail"> </button>
<div class="issue-option__detail-inner"> <div class="issue-option__detail">
<p class="issue-option__detail-text"> <div class="issue-option__detail-inner">
SMS 발송 핀번호는 저장되지 않습니다. 문자 수신 즉시 확인하세요. <p class="issue-option__detail-text">
SMS 발송 핀번호는 저장되지 않습니다. 문자 수신 즉시 확인하세요.
</p> </p>
<button id="btnIssueSms" type="button" class="issue-run issue-run--sky"> <button id="btnIssueSms" type="button" class="issue-run issue-run--sky">
SMS 발송 실행 SMS 발송 실행
</button> </button>
</div>
</div> </div>
</div> </div>
</div> @endif
{{-- 옵션 3 --}} {{-- 옵션 3 --}}
<div class="issue-option" data-issue-card="sell"> @if(isset($issueAllowed['BUYBACK']))
<button type="button" class="issue-option__toggle" data-issue-toggle> <div class="issue-option {{ $issueOpenKey==='BUYBACK' ? 'is-active' : '' }}" data-issue-card="sell">
<div class="issue-option__kicker">재판매</div> <button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__title">구매상품권 판매</div> <div class="issue-option__kicker">재판매</div>
<div class="issue-option__subtitle">구매하신 상품권을 판매 처리합니다.</div> <div class="issue-option__title">구매상품권 판매</div>
<span class="issue-option__chev" aria-hidden="true"></span> <div class="issue-option__subtitle">구매하신 상품권을 판매 처리합니다.</div>
</button> <span class="issue-option__chev" aria-hidden="true"></span>
<div class="issue-option__detail"> </button>
<div class="issue-option__detail-inner"> <div class="issue-option__detail">
<p class="issue-option__detail-text"> <div class="issue-option__detail-inner">
구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며, <p class="issue-option__detail-text">
매입 처리 회원님 계좌로 입금됩니다. 구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며,
매입 처리 회원님 계좌로 입금됩니다.
</p> </p>
<button id="btnIssueSell" type="button" class="issue-run issue-run--green"> <button id="btnIssueSell" type="button" class="issue-run issue-run--green">
구매상품권 판매 실행 구매상품권 판매 실행
</button> </button>
</div>
</div> </div>
</div> </div>
</div> @endif
@if(empty($issueAllowed))
<div class="muted" style="padding:12px 6px;">
현재 상품은 발행 방식이 설정되지 않았습니다. 고객센터로 문의해 주세요.
</div>
@endif
</div> </div>
</div> </div>
@endif @endif