2026-03-06 15:48:44 +09:00

1232 lines
51 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('web.layouts.subpage')
@php
$mypageActive = $mypageActive ?? 'usage';
$attempt = $attempt ?? [];
$order = $order ?? [];
$items = $items ?? [];
$pins = $pins ?? [];
$pinsOpened = (bool)($pinsOpened ?? false); // 기존 핀 오픈 상태
$pinsRevealed = (bool)($pinsRevealed ?? false); // 이번 요청에서 실핀 표시 여부
$pinsRevealLocked = (bool)($pinsRevealLocked ?? false); // 실핀 확인 이력(취소 잠금)
$canCancel = (bool)($canCancel ?? false);
$backToListQuery = request()->only(['q', 'method', 'status', 'from', 'to', 'page']);
$backToListQuery = array_filter($backToListQuery, fn($v) => $v !== null && $v !== '');
$methodLabel = function ($m) {
$m = (string)$m;
return match ($m) {
'card' => '카드',
'phone' => '휴대폰',
'wire' => '계좌이체',
'vact' => '가상계좌',
default => $m ?: '-',
};
};
$statusLabel = function () use ($attempt, $order) {
$aCancel = (string)($attempt['cancel_status'] ?? 'none');
$oCancel = (string)($order['cancel_status'] ?? 'none');
if ($aCancel === 'success' || $oCancel === 'success') return '결제취소';
$aStatus = (string)($attempt['status'] ?? '');
$oPay = (string)($order['stat_pay'] ?? '');
if ($aStatus === 'paid' || $oPay === 'p') return '결제완료';
if ($aStatus === 'issued' || $oPay === 'w') return '입금대기';
if ($aStatus === 'failed' || $oPay === 'f') return '결제실패';
return '진행중';
};
$st = $statusLabel();
$isCancelledAfterPaid = ($st === '결제취소');
$statusClass = function ($label) {
return match ($label) {
'결제취소' => 'pill--danger',
'결제완료' => 'pill--ok',
'입금대기' => 'pill--wait',
'결제실패' => 'pill--danger',
default => 'pill--muted',
};
};
$attemptId = (int)($attempt['id'] ?? 0);
$oid = (string)($order['oid'] ?? '');
$method = (string)($order['pay_method'] ?? ($attempt['pay_method'] ?? ''));
$methodKor = $methodLabel($method);
$amounts = (array)($order['amounts'] ?? []);
$subtotal = (int)($amounts['subtotal'] ?? 0);
$fee = (int)($amounts['fee'] ?? 0);
$payMoney = (int)($amounts['pay_money'] ?? 0);
$productName = (string)($productname ?? '');
if ($productName === '') $productName = '-';
$itemName = (string)($items[0]['name'] ?? '');
if ($itemName === '') $itemName = '-';
$totalQty = 0;
foreach ($items as $it) $totalQty += (int)($it['qty'] ?? 0);
$createdAt = (string)($order['created_at'] ?? ($attempt['created_at'] ?? ''));
$dateStr = $createdAt ? \Carbon\Carbon::parse($createdAt)->format('Y-m-d H:i') : '-';
$showPinsNow = true;
$isPinIssuedCompleted = (bool)($pinsIssuedCompleted ?? !empty($pins));
$useRightBannerMode = $isCancelledAfterPaid || $isPinIssuedCompleted;
$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];
}
$issueOpenKey = (count($issueMethods) === 1) ? ($issueMethods[0] ?? null) : null;
@endphp
@section('title', '구매내역 상세')
@section('subcontent')
<div class="mypage-usage">
<div class="topbar">
@if(session('success')) <div class="flash ok">{{ session('success') }}</div> @endif
@if(session('error')) <div class="flash err">{{ session('error') }}</div> @endif
<div class="sp"></div>
<a class="btn btn--back" href="{{ route('web.mypage.usage.index', $backToListQuery) }}"> 목록</a>
</div>
<div class="detail-hero-grid">
<div class="receipt-card receipt-card--paper">
<div class="receipt-head">
<div>
<div class="receipt-title">결제 영수증</div>
<div class="receipt-sub">
{{ $dateStr }} · {{ $methodKor }}
</div>
</div>
<span class="pill {{ $statusClass($st) }}">{{ $st }}</span>
</div>
<div class="receipt-divider"></div>
<div class="receipt-main">
<div class="receipt-product">
{{ $productName }}
<span class="receipt-item">[ {{ $itemName }} ]</span>
</div>
<div class="receipt-grid">
<div class="row">
<span class="k">결제번호</span>
<span class="v">#{{ $attemptId ?: '-' }}</span>
</div>
<div class="row">
<span class="k">주문번호</span>
<span class="v mono">{{ $oid ?: '-' }}</span>
</div>
<div class="row">
<span class="k">결제수단</span>
<span class="v">{{ $methodKor }}</span>
</div>
<div class="row">
<span class="k">수량</span>
<span class="v">{{ $totalQty }}</span>
</div>
</div>
</div>
<div class="receipt-divider receipt-divider--dashed"></div>
<div class="receipt-amount">
<div class="amt-row">
<span class="k">상품금액</span>
<span class="v">{{ number_format($subtotal) }}</span>
</div>
<div class="amt-row">
<span class="k">고객수수료</span>
<span class="v">{{ number_format($fee) }}</span>
</div>
<div class="amt-row total">
<span class="k">결제금액</span>
<span class="v">{{ number_format($payMoney) }}</span>
</div>
</div>
</div>
<aside class="right-panel {{ $useRightBannerMode ? 'right-panel--banner right-panel--mobile-hide' : '' }}">
@if($useRightBannerMode)
<div class="banner-stack">
@if($isCancelledAfterPaid)
<div class="promo-vertical-banner promo-vertical-banner--cancel">
<div class="promo-vertical-banner__inner">
<div class="promo-vertical-banner__badge">PROMO</div>
<div class="promo-vertical-banner__eyebrow">PIN FOR YOU</div>
<div class="promo-vertical-banner__title">
다음 구매는<br>
빠르고 간편하게
</div>
<div class="promo-vertical-banner__desc">
자주 찾는 상품권을 빠르게 확인하고,<br>
구매 내역/상태를 번에 관리해보세요.
</div>
<div class="promo-vertical-banner__chips">
<span class="chip">빠른 재구매</span>
<span class="chip">구매내역 관리</span>
<span class="chip">안전한 결제</span>
</div>
<div class="promo-vertical-banner__footer">
<div class="promo-vertical-banner__footer-title">추천 안내</div>
<div class="promo-vertical-banner__footer-desc">
진행 중인 이벤트 혜택은 메인/상품 페이지에서 확인할 있습니다.
</div>
</div>
</div>
</div>
@endif
@if($isPinIssuedCompleted && !$isCancelledAfterPaid)
<div class="info-banner info-banner--ok">
<div class="info-banner__title"> 발행이 완료되었습니다</div>
<div class="info-banner__desc">
발행이 완료된 주문은 우측 발행 선택 영역 대신 안내 배너를 표시합니다.
목록 영역에서 발행된 정보를 확인해 주세요.
</div>
</div>
<div class="info-banner {{ $pinsRevealLocked ? 'info-banner--danger' : 'info-banner--warn' }}">
<div class="info-banner__title">
{{ $pinsRevealLocked ? '핀번호 확인 완료' : '핀번호 확인 안내' }}
</div>
<div class="info-banner__desc">
@if($pinsRevealLocked)
핀번호를 확인한 주문은 회원이 직접 취소할 없습니다.
취소가 필요한 경우 관리자에게 문의해 주세요.
@else
오픈 후에도 기본은 마스킹 상태로 유지됩니다.
“핀번호 확인” 버튼에서 2 비밀번호 인증 전체 핀번호를 확인할 있습니다.
@endif
</div>
</div>
@endif
</div>
@else
<div class="issue-panel">
<div class="issue-panel__head">
<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 class="issue-picker" id="issuePicker">
@if(isset($issueAllowed['PIN_INSTANT']))
<div class="issue-option {{ $issueOpenKey==='PIN_INSTANT' ? 'is-active' : '' }}" data-issue-card="view">
<button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__kicker">즉시 확인</div>
<div class="issue-option__title">핀번호 바로 확인</div>
<div class="issue-option__subtitle">안전하게 핀번호를 직접 확인합니다.</div>
<span class="issue-option__chev" aria-hidden="true"></span>
</button>
<div class="issue-option__detail">
<div class="issue-option__detail-inner">
<p class="issue-option__detail-text">
핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요.
</p>
<form method="post" action="{{ route('web.mypage.usage.issue.pin_instant', ['attemptId' => $attemptId]) }}">
@csrf
<button id="btnIssuePinInstant" type="submit" class="issue-run issue-run--dark">
핀번호 바로 확인 실행
</button>
</form>
</div>
</div>
</div>
@endif
@if(isset($issueAllowed['SMS']))
<div class="issue-option {{ $issueOpenKey==='SMS' ? 'is-active' : '' }}" data-issue-card="sms">
<button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__kicker">문자 발송</div>
<div class="issue-option__title">SMS 발송</div>
<div class="issue-option__subtitle">문자로 핀번호를 전송합니다.</div>
<span class="issue-option__chev" aria-hidden="true"></span>
</button>
<div class="issue-option__detail">
<div class="issue-option__detail-inner">
<p class="issue-option__detail-text">
SMS 발송 핀번호는 저장되지 않습니다. 문자 수신 즉시 확인하세요.
</p>
<button id="btnIssueSms" type="button" class="issue-run issue-run--sky">
SMS 발송 실행
</button>
</div>
</div>
</div>
@endif
@if(isset($issueAllowed['BUYBACK']))
<div class="issue-option {{ $issueOpenKey==='BUYBACK' ? 'is-active' : '' }}" data-issue-card="sell">
<button type="button" class="issue-option__toggle" data-issue-toggle>
<div class="issue-option__kicker">재판매</div>
<div class="issue-option__title">구매상품권 판매</div>
<div class="issue-option__subtitle">구매하신 상품권을 판매 처리합니다.</div>
<span class="issue-option__chev" aria-hidden="true"></span>
</button>
<div class="issue-option__detail">
<div class="issue-option__detail-inner">
<p class="issue-option__detail-text">
구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며,
매입 처리 회원님 계좌로 입금됩니다.
</p>
<button id="btnIssueSell" type="button" class="issue-run issue-run--green">
구매상품권 판매 실행
</button>
</div>
</div>
</div>
@endif
@if(empty($issueAllowed))
<div class="muted" style="padding:12px 6px;">
현재 상품은 발행 방식이 설정되지 않았습니다. 고객센터로 문의해 주세요.
</div>
@endif
</div>
</div>
@endif
</aside>
</div>
@if(!$isCancelledAfterPaid)
@if($showPinsNow && $hasIssuedIssues)
<div class="gift-zone">
<div class="gift-zone__head">
<div>
<h3 class="gift-zone__title">핀번호</h3>
<div class="gift-zone__sub">
기본은 마스킹 상태로 표시됩니다.
핀번호 확인 버튼에서 2 비밀번호를 입력하면 이번 화면에서만 전체 핀번호를 확인할 있습니다.
</div>
</div>
@if($pinsRevealed)
<span class="gift-badge gift-badge--danger">핀번호 확인됨</span>
@else
<span class="gift-badge">마스킹 표시중</span>
@endif
</div>
@if(empty($pins))
<div class="usage-card">
<p class="muted">표시할 핀이 없습니다.</p>
</div>
@else
<div class="gift-list">
@foreach($pins as $idx => $p)
@php
$raw = (string)($p['pin'] ?? '');
$masked = (string)($p['pin_mask'] ?? '****');
$display = $pinsRevealed ? ($raw ?: $masked) : $masked;
$amount = number_format((int)($p['face_value'] ?? 0));
@endphp
<article class="gift-card">
<div class="gift-card__top">
<div class="gift-card__brand">MOBILE GIFT</div>
<div class="gift-card__chip">No. {{ $idx + 1 }}</div>
</div>
<div class="gift-card__body">
<div class="gift-card__name">{{ $productName }}</div>
<div class="gift-card__amount">{{ $amount }}</div>
<div class="gift-card__pinbox">
<div class="gift-card__pinlabel">PIN NUMBER</div>
<div
class="gift-card__pincode mono"
data-pin-value="{{ $pinsRevealed ? ($raw ?: $masked) : '' }}"
>
{{ $display }}
</div>
</div>
<div class="gift-card__notice">
핀번호는 본인만 확인해 주세요.<br>
발행이 완료된 주문은 회원이 직접 취소할 없습니다.
</div>
</div>
</article>
@endforeach
</div>
@if(!$pinsRevealed)
<div class="gift-actions">
<button type="button" class="issue-run issue-run--dark gift-open-btn" onclick="openPinRevealModal()">
핀번호 확인
</button>
</div>
@else
<div class="gift-actions">
<button type="button" class="issue-run issue-run--dark gift-open-btn" id="btnCopyPins">
핀번호 복사
</button>
</div>
@endif
@endif
</div>
@endif
@if($canCancel)
<div class="usage-card cancel-box">
<h3 class="card-title">결제 취소</h3>
<div class="muted">
취소 사유를 선택한 결제를 취소할 있습니다.
</div>
<form method="post"
action="{{ route('web.mypage.usage.cancel', ['attemptId' => $attemptId]) }}"
class="cancel-form cancel-form--inline"
id="cancelForm">
@csrf
<select class="inp sel" name="reason" id="cancelReason" required>
<option value="">취소 사유를 선택해 주세요</option>
<option value="단순 변심">단순 변심</option>
<option value="상품을 잘못 선택함">상품을 잘못 선택함</option>
<option value="구매 수량을 잘못 선택함">구매 수량을 잘못 선택함</option>
<option value="결제 수단을 잘못 선택함">결제 수단을 잘못 선택함</option>
<option value="중복 결제 시도">중복 결제 시도</option>
<option value="다른 상품으로 다시 구매 예정">다른 상품으로 다시 구매 예정</option>
<option value="가격을 다시 확인 후 구매 예정">가격을 다시 확인 구매 예정</option>
<option value="회원 정보 확인 후 다시 진행 예정">회원 정보 확인 다시 진행 예정</option>
<option value="프로모션/혜택 적용 후 재구매 예정">프로모션/혜택 적용 재구매 예정</option>
<option value="기타">기타</option>
</select>
<button id="btnCancel" class="btn btn--danger btn--sm" type="submit">
결제 취소
</button>
</form>
</div>
@endif
@endif
</div>
<div class="pin-auth-modal" id="pinRevealModal" hidden>
<div class="pin-auth-modal__backdrop" onclick="closePinRevealModal()"></div>
<div class="pin-auth-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="pinRevealTitle">
<div class="pin-auth-modal__head">
<h3 id="pinRevealTitle">핀번호 확인</h3>
<button type="button" class="pin-auth-modal__close" onclick="closePinRevealModal()">×</button>
</div>
<div class="pin-auth-modal__body">
<p class="pin-auth-modal__desc">
전체 핀번호 확인을 위해 2 비밀번호를 입력해 주세요.
</p>
<form id="pinRevealForm" method="post" action="{{ route('web.mypage.usage.reveal', ['attemptId' => $attemptId]) }}">
@csrf
<input
class="pin-auth-modal__input"
type="password"
name="pin2"
inputmode="numeric"
pattern="\d{4}"
maxlength="4"
autocomplete="off"
placeholder="2차 비밀번호 4자리"
>
<div class="pin-auth-modal__actions">
<button type="button" class="issue-run" onclick="closePinRevealModal()">닫기</button>
<button type="submit" class="issue-run issue-run--dark">핀번호 확인</button>
</div>
</form>
</div>
</div>
</div>
<style>
.mypage-usage { display:flex; flex-direction:column; gap:14px; }
.topbar{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
.topbar .sp{flex:1;}
.btn{
display:inline-flex;align-items:center;justify-content:center;
padding:10px 12px;border-radius:12px;border:1px solid rgba(0,0,0,.14);
cursor:pointer;text-decoration:none;font-size:13px; white-space:nowrap;
}
.btn--sm{padding:8px 10px;border-radius:10px;font-size:12px;}
.btn--danger{border-color: rgba(220,0,0,.35); color:rgb(180,0,0); font-weight:800;}
.flash{padding:8px 10px;border-radius:10px;font-size:13px;}
.flash.ok{background:rgba(0,160,60,.08);}
.flash.err{background:rgba(220,0,0,.08);}
.mono{font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;}
.muted { color:rgba(0,0,0,.55); }
.pill{
display:inline-flex; padding:4px 10px; border-radius:999px; font-size:12px;
border:1px solid rgba(0,0,0,.12); white-space:nowrap;
}
.pill--ok{background:rgba(0,160,60,.08); border-color:rgba(0,160,60,.18);}
.pill--danger{background:rgba(220,0,0,.08); border-color:rgba(220,0,0,.18); color:rgb(180,0,0);}
.pill--wait{background:rgba(255,190,0,.12); border-color:rgba(255,190,0,.25);}
.pill--muted{opacity:.75;}
.notice-box { padding:12px; border-radius:12px; background:rgba(0,0,0,.04); margin-top:10px; }
.notice-box--err { background:rgba(220,0,0,.06); }
.detail-hero-grid{
display:grid;
grid-template-columns:1fr;
gap:14px;
}
@media (min-width: 960px){
.detail-hero-grid{
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: stretch;
}
.detail-hero-grid > * {
min-width: 0;
}
}
.right-panel{
display:block;
height:100%;
}
.right-panel .issue-panel,
.right-panel .banner-stack,
.right-panel .promo-vertical-banner{
height:100%;
}
@media (max-width: 959px){
.right-panel--mobile-hide{display:none;}
}
.receipt-card{
border:1px solid rgba(0,0,0,.08);
border-radius:18px;
padding:16px;
background:#fff;
}
.receipt-card--paper{
box-shadow: 0 10px 24px rgba(0,0,0,.04);
}
.receipt-head{
display:flex; justify-content:space-between; align-items:flex-start; gap:10px;
}
.receipt-title{font-size:18px; font-weight:900; line-height:1.1;}
.receipt-sub{margin-top:4px; color:rgba(0,0,0,.55); font-size:12px;}
.receipt-divider{
margin:12px 0;
border-top:1px solid rgba(0,0,0,.08);
}
.receipt-divider--dashed{
border-top-style:dashed;
border-top-color:rgba(0,0,0,.16);
}
.receipt-main{display:flex; flex-direction:column; gap:10px;}
.receipt-product{
font-size:15px; font-weight:900; line-height:1.35;
}
.receipt-item{
font-size:13px; font-weight:700; color:rgba(0,0,0,.6);
}
.receipt-grid{
display:grid; grid-template-columns:1fr; gap:8px;
}
.receipt-grid .row{
display:flex; justify-content:space-between; gap:10px; font-size:13px;
}
.receipt-grid .k{color:rgba(0,0,0,.55);}
.receipt-grid .v{font-weight:800; text-align:right;}
.receipt-amount{
display:flex; flex-direction:column; gap:8px;
padding:12px;
border-radius:14px;
background:rgba(0,0,0,.02);
border:1px solid rgba(0,0,0,.05);
}
.amt-row{display:flex; justify-content:space-between; gap:10px; font-size:13px;}
.amt-row .k{color:rgba(0,0,0,.6);}
.amt-row .v{font-weight:800;}
.amt-row.total{
margin-top:2px;
padding-top:8px;
border-top:1px dashed rgba(0,0,0,.14);
font-size:15px; font-weight:900;
}
.banner-stack{
display:flex;
flex-direction:column;
gap:10px;
}
.info-banner{
border-radius:18px;
padding:14px;
border:1px solid rgba(0,0,0,.08);
background:#fff;
box-shadow: 0 8px 20px rgba(0,0,0,.03);
}
.info-banner__title{
font-size:14px; font-weight:900;
}
.info-banner__desc{
margin-top:6px;
font-size:13px; line-height:1.45;
color:rgba(0,0,0,.62);
}
.info-banner--danger{
background:linear-gradient(180deg, rgba(220,0,0,.05), rgba(220,0,0,.02));
border-color:rgba(220,0,0,.14);
}
.info-banner--ok{
background:linear-gradient(180deg, rgba(0,160,60,.07), rgba(0,160,60,.03));
border-color:rgba(0,160,60,.16);
}
.info-banner--warn{
background:linear-gradient(180deg, rgba(255,190,0,.10), rgba(255,190,0,.04));
border-color:rgba(255,190,0,.20);
}
.promo-vertical-banner{
border-radius:20px;
position:relative;
overflow:hidden;
border:1px solid rgba(0,0,0,.08);
background:
radial-gradient(120% 80% at 0% 0%, rgba(255,120,120,.16), transparent 55%),
radial-gradient(90% 70% at 100% 100%, rgba(255,190,0,.16), transparent 55%),
linear-gradient(180deg, #fff 0%, #fbfbfd 100%);
box-shadow:
0 20px 35px rgba(0,0,0,.06),
0 8px 16px rgba(0,0,0,.03),
inset 0 1px 0 rgba(255,255,255,.9);
min-height:100%;
}
.promo-vertical-banner--cancel::before{
content:'';
position:absolute;
inset:0;
background:
linear-gradient(135deg, rgba(220,0,0,.04) 0%, transparent 38%),
linear-gradient(315deg, rgba(255,190,0,.05) 0%, transparent 42%);
pointer-events:none;
}
.promo-vertical-banner__inner{
position:relative;
z-index:1;
height:100%;
display:flex;
flex-direction:column;
padding:16px;
gap:10px;
}
.promo-vertical-banner__badge{
align-self:flex-start;
font-size:11px;
font-weight:900;
letter-spacing:.06em;
color:#7a1b1b;
background:rgba(220,0,0,.08);
border:1px solid rgba(220,0,0,.16);
border-radius:999px;
padding:4px 8px;
}
.promo-vertical-banner__eyebrow{
font-size:12px;
font-weight:800;
color:rgba(0,0,0,.45);
letter-spacing:.03em;
}
.promo-vertical-banner__title{
font-size:22px;
line-height:1.15;
font-weight:900;
letter-spacing:-0.02em;
color:#1d1d1f;
margin-top:2px;
}
.promo-vertical-banner__desc{
font-size:13px;
line-height:1.5;
color:rgba(0,0,0,.62);
}
.promo-vertical-banner__chips{
display:flex;
flex-wrap:wrap;
gap:8px;
margin-top:2px;
}
.promo-vertical-banner__chips .chip{
display:inline-flex;
align-items:center;
justify-content:center;
padding:6px 10px;
border-radius:999px;
font-size:12px;
font-weight:700;
background:rgba(255,255,255,.9);
border:1px solid rgba(0,0,0,.08);
box-shadow: 0 3px 6px rgba(0,0,0,.03);
}
.promo-vertical-banner__footer{
margin-top:auto;
border-top:1px dashed rgba(0,0,0,.10);
padding-top:10px;
}
.promo-vertical-banner__footer-title{
font-size:12px;
font-weight:900;
color:rgba(0,0,0,.75);
}
.promo-vertical-banner__footer-desc{
margin-top:4px;
font-size:12px;
line-height:1.45;
color:rgba(0,0,0,.55);
}
.issue-panel{
border-radius:20px;
padding:14px;
background:
radial-gradient(1200px 320px at -10% -20%, rgba(0,130,255,.10), transparent 45%),
radial-gradient(1000px 280px at 110% 120%, rgba(0,160,60,.08), transparent 45%),
linear-gradient(180deg, rgba(255,255,255,.98), rgba(250,251,253,.98));
border:1px solid rgba(0,0,0,.08);
box-shadow:
0 20px 35px rgba(0,0,0,.06),
0 6px 14px rgba(0,0,0,.03),
inset 0 1px 0 rgba(255,255,255,.8);
}
.issue-panel__head{
display:flex; flex-direction:column; gap:6px;
margin-bottom:12px;
}
.issue-panel__title{
margin:0; font-size:17px; font-weight:900;
}
.issue-picker{
display:flex; flex-direction:column; gap:10px;
}
.issue-option{
border:1px solid rgba(0,0,0,.08);
border-radius:16px;
background:linear-gradient(180deg, rgba(255,255,255,1), rgba(248,249,251,1));
overflow:hidden;
transition:
border-color .25s ease,
box-shadow .25s ease,
transform .18s ease,
background .25s ease;
box-shadow:
0 6px 12px rgba(0,0,0,.02),
inset 0 1px 0 rgba(255,255,255,.85);
position:relative;
}
.issue-option::before{
content:'';
position:absolute;
left:0; top:0; bottom:0;
width:4px;
background:rgba(0,0,0,.08);
transition:opacity .25s ease, background .25s ease;
opacity:.7;
}
.issue-option:hover{
border-color:rgba(0,0,0,.14);
transform:translateY(-1px);
box-shadow:
0 12px 18px rgba(0,0,0,.04),
inset 0 1px 0 rgba(255,255,255,.9);
}
.issue-option.is-active{
border-color:rgba(0,0,0,.18);
box-shadow:
0 18px 24px rgba(0,0,0,.06),
0 8px 14px rgba(0,0,0,.03),
inset 0 1px 0 rgba(255,255,255,.95);
transform:translateY(-1px);
}
.issue-option[data-issue-card="view"]::before{
background:linear-gradient(180deg, rgba(70,70,70,.85), rgba(20,20,20,.85));
}
.issue-option[data-issue-card="sms"]::before{
background:linear-gradient(180deg, rgba(0,130,255,.95), rgba(0,90,220,.85));
}
.issue-option[data-issue-card="sell"]::before{
background:linear-gradient(180deg, rgba(0,170,95,.95), rgba(0,130,70,.85));
}
.issue-option[data-issue-card="view"].is-active{
background:linear-gradient(180deg, rgba(0,0,0,.025), rgba(255,255,255,1));
}
.issue-option[data-issue-card="sms"].is-active{
background:linear-gradient(180deg, rgba(0,130,255,.06), rgba(255,255,255,1));
}
.issue-option[data-issue-card="sell"].is-active{
background:linear-gradient(180deg, rgba(0,160,60,.07), rgba(255,255,255,1));
}
.issue-option__toggle{
width:100%;
border:0;
background:transparent;
text-align:left;
cursor:pointer;
padding:12px 14px 12px 16px;
display:grid;
grid-template-columns:1fr auto;
gap:8px;
}
.issue-option__kicker{
grid-column:1 / 2;
font-size:11px; font-weight:800;
color:rgba(0,0,0,.45);
text-transform:uppercase;
letter-spacing:.03em;
}
.issue-option__title{
grid-column:1 / 2;
font-size:15px; font-weight:900; line-height:1.1;
}
.issue-option__subtitle{
grid-column:1 / 2;
font-size:12px; color:rgba(0,0,0,.58); line-height:1.35;
}
.issue-option__chev{
grid-column:2 / 3;
grid-row:1 / span 3;
align-self:center;
font-size:18px;
color:rgba(0,0,0,.45);
transition:transform .25s ease;
}
.issue-option.is-active .issue-option__chev{
transform:rotate(180deg);
}
.issue-option__detail{
max-height:0;
opacity:0;
overflow:hidden;
transition:max-height .32s ease, opacity .22s ease;
}
.issue-option.is-active .issue-option__detail{
max-height:220px;
opacity:1;
}
.issue-option__detail-inner{
padding:0 14px 14px 16px;
border-top:1px dashed rgba(0,0,0,.08);
}
.issue-option__detail-text{
margin:10px 0 12px;
font-size:13px; line-height:1.45;
color:rgba(0,0,0,.65);
}
.issue-run{
display:inline-flex; align-items:center; justify-content:center;
border:1px solid rgba(0,0,0,.12);
border-radius:12px;
background:#fff;
padding:10px 12px;
font-size:13px;
font-weight:800;
cursor:pointer;
box-shadow:
0 6px 12px rgba(0,0,0,.04),
inset 0 1px 0 rgba(255,255,255,.9);
}
.issue-run--dark{
background:linear-gradient(180deg, rgba(0,0,0,.06), rgba(0,0,0,.03));
}
.issue-run--sky{
background:linear-gradient(180deg, rgba(0,130,255,.10), rgba(0,130,255,.05));
border-color:rgba(0,130,255,.20);
}
.issue-run--green{
background:linear-gradient(180deg, rgba(0,160,60,.12), rgba(0,160,60,.06));
border-color:rgba(0,160,60,.18);
}
.usage-card{border:1px solid rgba(0,0,0,.08); border-radius:16px; padding:16px; background:#fff;}
.section-head{display:flex; flex-direction:column; gap:6px; margin-bottom:10px;}
.card-title{font-size:16px; margin:0;}
.sub{font-size:13px;}
.cancel-box{padding:14px;}
.cancel-form{margin-top:10px; display:flex; flex-direction:column; gap:8px; align-items:flex-start;}
.inp{
padding:10px 10px; border-radius:12px; border:1px solid rgba(0,0,0,.14);
background:#fff; font-size:13px; width:100%; max-width:420px;
}
.gift-zone{display:flex;flex-direction:column;gap:14px;}
.gift-zone__head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;}
.gift-zone__title{margin:0;font-size:18px;font-weight:900;}
.gift-zone__sub{margin-top:6px;font-size:13px;color:rgba(0,0,0,.58);line-height:1.5;}
.gift-badge{
display:inline-flex;align-items:center;justify-content:center;
min-height:34px;padding:0 12px;border-radius:999px;
border:1px solid rgba(0,0,0,.08);background:#fff;font-size:12px;font-weight:800;
}
.gift-badge--danger{
color:#b42318;background:rgba(255,90,90,.08);border-color:rgba(180,35,24,.16);
}
.gift-badge--ok{
color:#067647;background:rgba(18,183,106,.10);border-color:rgba(6,118,71,.16);
}
.gift-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px;}
.gift-card{
position:relative;overflow:hidden;
border-radius:24px;
padding:18px;
background:
radial-gradient(circle at top right, rgba(255,255,255,.82), rgba(255,255,255,.08) 35%),
linear-gradient(135deg, #0f172a 0%, #1e293b 44%, #334155 100%);
color:#fff;
box-shadow:0 18px 38px rgba(15,23,42,.16);
}
.gift-card::after{
content:"";
position:absolute;right:-28px;bottom:-28px;width:120px;height:120px;border-radius:50%;
background:rgba(255,255,255,.08);
}
.gift-card__top{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:18px;}
.gift-card__brand{font-size:12px;font-weight:900;letter-spacing:.18em;opacity:.92;}
.gift-card__chip{
padding:6px 10px;border-radius:999px;
background:rgba(255,255,255,.14);font-size:11px;font-weight:800;
border:1px solid rgba(255,255,255,.18);
}
.gift-card__body{position:relative;z-index:1;display:flex;flex-direction:column;gap:12px;}
.gift-card__name{font-size:17px;font-weight:900;line-height:1.35;}
.gift-card__amount{font-size:28px;font-weight:900;letter-spacing:-.02em;}
.gift-card__pinbox{
border-radius:18px;padding:14px;
background:rgba(255,255,255,.1);
border:1px solid rgba(255,255,255,.18);
backdrop-filter: blur(8px);
}
.gift-card__pinlabel{font-size:11px;letter-spacing:.14em;opacity:.8;margin-bottom:6px;}
.gift-card__pincode{font-size:18px;font-weight:900;letter-spacing:.08em;word-break:break-all;}
.gift-card__notice{font-size:12px;line-height:1.55;color:rgba(255,255,255,.82);}
.gift-actions{display:flex;justify-content:center;margin-top:6px;}
.gift-open-btn{min-width:260px;}
.pin-auth-modal[hidden]{display:none !important;}
.pin-auth-modal{position:fixed;inset:0;z-index:1000;}
.pin-auth-modal__backdrop{position:absolute;inset:0;background:rgba(15,23,42,.52);}
.pin-auth-modal__dialog{
position:relative;
width:min(92vw, 420px);
margin:8vh auto 0;
background:#fff;border-radius:24px;
box-shadow:0 24px 60px rgba(15,23,42,.22);
overflow:hidden;
}
.pin-auth-modal__head{
display:flex;justify-content:space-between;align-items:center;
padding:18px 18px 0 18px;
}
.pin-auth-modal__head h3{margin:0;font-size:18px;}
.pin-auth-modal__close{
border:0;background:transparent;font-size:28px;line-height:1;cursor:pointer;color:#111827;
}
.pin-auth-modal__body{padding:16px 18px 18px;}
.pin-auth-modal__desc{margin:0 0 14px;font-size:13px;line-height:1.6;color:rgba(0,0,0,.65);}
.pin-auth-modal__input{
width:100%;height:48px;border-radius:14px;border:1px solid rgba(0,0,0,.14);
padding:0 14px;font-size:15px;background:#fff;
}
.pin-auth-modal__actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px;flex-wrap:wrap;}
.cancel-form--inline{
margin-top:10px;
display:flex;
flex-direction:row;
align-items:center;
gap:8px;
flex-wrap:nowrap;
}
.cancel-form--inline .sel{
flex:1 1 auto;
min-width:0;
max-width:none;
height:40px;
}
.cancel-form--inline .btn{
flex:0 0 auto;
height:40px;
}
@media (max-width: 640px){
.cancel-form--inline{
flex-direction:column;
align-items:stretch;
}
.cancel-form--inline .btn{
width:100%;
}
}
</style>
<script>
async function copyAllPins() {
const nodes = Array.from(document.querySelectorAll('[data-pin-value]'));
const pins = nodes
.map(el => (el.getAttribute('data-pin-value') || '').trim())
.filter(v => v !== '');
if (!pins.length) {
await showMsg(
"복사할 핀번호가 없습니다.",
{ type: 'alert', title: '안내' }
);
return;
}
const text = pins.join('\n');
try {
await navigator.clipboard.writeText(text);
await showMsg(
"전체 핀번호가 복사되었습니다.",
{ type: 'alert', title: '복사 완료' }
);
} catch (e) {
// clipboard API 실패 시 fallback
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
ta.style.top = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
let ok = false;
try {
ok = document.execCommand('copy');
} catch (_) {
ok = false;
}
document.body.removeChild(ta);
if (ok) {
await showMsg(
"전체 핀번호가 복사되었습니다.",
{ type: 'alert', title: '복사 완료' }
);
} else {
await showMsg(
"복사에 실패했습니다. 브라우저 권한을 확인해 주세요.",
{ type: 'alert', title: '복사 실패' }
);
}
}
}
async function onOpenPinsOnce(e) {
e.preventDefault();
const btn = e.currentTarget;
const form = btn ? btn.closest('form') : null;
const ok = await showMsg(
"핀 확인(오픈) 후에도 핀번호는 기본 마스킹 상태로 유지됩니다.\n\n진행할까요?",
{ type: 'confirm', title: '핀 오픈' }
);
if (!ok) return;
if (!form) return;
if (form.requestSubmit) form.requestSubmit();
else form.submit();
}
async function onCancelOnce(e) {
e.preventDefault();
const btn = e.currentTarget;
const form = btn ? btn.closest('form') : null;
if (!form) return;
const reasonEl = form.querySelector('select[name="reason"]');
const reason = (reasonEl?.value || '').trim();
if (!reason) {
await showMsg(
"취소 사유를 먼저 선택해 주세요.",
{ type: 'alert', title: '취소 사유 선택' }
);
reasonEl?.focus();
return;
}
const ok = await showMsg(
"선택한 사유로 결제를 취소할까요?\n\n취소 사유: " + reason,
{ type: 'confirm', title: '결제 취소' }
);
if (!ok) return;
if (form.requestSubmit) form.requestSubmit();
else form.submit();
}
async function onIssuePinInstant(e) {
e.preventDefault();
const btn = e.currentTarget;
const form = btn ? btn.closest('form') : null;
const ok = await showMsg(
"핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요.",
{ type: 'confirm', title: '핀발행' }
);
if (!ok || !form) return;
if (form.requestSubmit) form.requestSubmit();
else form.submit();
}
async function onIssueSmsSoon() {
await showMsg(
"준비중입니다.\n\nSMS 발송 시 핀번호는 저장되지 않습니다. 문자 수신 후 즉시 확인하세요.",
{ type: 'alert', title: '안내' }
);
}
async function onIssueSellSoon() {
await showMsg(
"준비중입니다.\n\n구매하신 상품권을 판매합니다.\n계좌번호가 등록되어 있어야 합니다.\n매입 처리는 약간의 시간이 걸릴 수 있으며, 완료 후 회원님 계좌로 입금됩니다.",
{ type: 'alert', title: '안내' }
);
}
function openPinRevealModal() {
const modal = document.getElementById('pinRevealModal');
if (!modal) return;
modal.hidden = false;
const input = modal.querySelector('input[name="pin2"]');
setTimeout(() => input && input.focus(), 30);
}
function closePinRevealModal() {
const modal = document.getElementById('pinRevealModal');
if (!modal) return;
modal.hidden = true;
const input = modal.querySelector('input[name="pin2"]');
if (input) input.value = '';
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closePinRevealModal();
});
document.addEventListener('DOMContentLoaded', () => {
const btnOpen = document.getElementById('btnOpenPins');
if (btnOpen) btnOpen.addEventListener('click', onOpenPinsOnce);
const btnCancel = document.getElementById('btnCancel');
if (btnCancel) btnCancel.addEventListener('click', onCancelOnce);
const btnPinInstant = document.getElementById('btnIssuePinInstant');
if (btnPinInstant) btnPinInstant.addEventListener('click', onIssuePinInstant);
const btn2 = document.getElementById('btnIssueSms');
if (btn2) btn2.addEventListener('click', onIssueSmsSoon);
const btn3 = document.getElementById('btnIssueSell');
if (btn3) btn3.addEventListener('click', onIssueSellSoon);
const btnCopyPins = document.getElementById('btnCopyPins');
if (btnCopyPins) {
btnCopyPins.addEventListener('click', copyAllPins);
}
const issueCards = Array.from(document.querySelectorAll('[data-issue-card]'));
issueCards.forEach((card) => {
const toggle = card.querySelector('[data-issue-toggle]');
if (!toggle) return;
toggle.addEventListener('click', () => {
const isActive = card.classList.contains('is-active');
issueCards.forEach(c => c.classList.remove('is-active'));
if (!isActive) {
card.classList.add('is-active');
}
});
});
});
</script>
@endsection