자사핀 발행 / 핀확인 완료
This commit is contained in:
parent
466cb89307
commit
23136fc1da
@ -4,14 +4,65 @@ namespace App\Http\Controllers\Web\Mypage;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Mypage\UsageService;
|
||||
use App\Services\Payments\OwnPinIssueService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class UsageController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UsageService $service,
|
||||
private readonly OwnPinIssueService $ownPinIssueService,
|
||||
) {}
|
||||
|
||||
public function issuePinInstant(Request $request, int $attemptId)
|
||||
{
|
||||
if ((bool)session('_sess._login_') !== true) {
|
||||
return redirect()->route('web.auth.login');
|
||||
}
|
||||
|
||||
$memNo = (int)session('_sess._mno', 0);
|
||||
if ($memNo <= 0) abort(403);
|
||||
|
||||
$out = $this->ownPinIssueService->issuePinInstant($attemptId, $memNo);
|
||||
|
||||
if (!($out['ok'] ?? false)) {
|
||||
return redirect()->back()->with('error', (string)($out['message'] ?? '발행 실패'));
|
||||
}
|
||||
|
||||
return redirect()->route('web.mypage.usage.show', ['attemptId' => $attemptId])
|
||||
->with('success', (string)($out['message'] ?? '발행 완료'));
|
||||
}
|
||||
|
||||
public function revealPins(Request $request, int $attemptId)
|
||||
{
|
||||
if ((bool)session('_sess._login_') !== true) {
|
||||
return redirect()->route('web.auth.login');
|
||||
}
|
||||
|
||||
$memNo = (int)session('_sess._mno', 0);
|
||||
if ($memNo <= 0) abort(403);
|
||||
|
||||
$data = $request->validate([
|
||||
'pin2' => ['required', 'string', 'min:4', 'max:20'],
|
||||
]);
|
||||
|
||||
$out = $this->service->revealPins(
|
||||
$attemptId,
|
||||
$memNo,
|
||||
(string)$data['pin2']
|
||||
);
|
||||
|
||||
if (!($out['ok'] ?? false)) {
|
||||
return redirect()
|
||||
->route('web.mypage.usage.show', ['attemptId' => $attemptId])
|
||||
->with('error', (string)($out['message'] ?? '핀번호 확인에 실패했습니다.'));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('web.mypage.usage.show', ['attemptId' => $attemptId, 'revealed' => 1])
|
||||
->with('success', '핀번호 확인이 완료되었습니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /mypage/usage
|
||||
* - 리스트 + 검색 + 페이징
|
||||
|
||||
@ -37,13 +37,14 @@ final class UsageRepository
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function decodeJsonArray($v): array
|
||||
private function decodeJsonArray($json): array
|
||||
{
|
||||
if ($v === null) return [];
|
||||
if (is_array($v)) return $v;
|
||||
$s = trim((string)$v);
|
||||
if ($s === '') return [];
|
||||
$decoded = json_decode($s, true);
|
||||
if (is_array($json)) return $json;
|
||||
|
||||
$json = trim((string)$json);
|
||||
if ($json === '') return [];
|
||||
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
@ -119,17 +120,23 @@ final class UsageRepository
|
||||
$from = trim((string)($filters['from'] ?? ''));
|
||||
$to = trim((string)($filters['to'] ?? ''));
|
||||
|
||||
// order_items 집계 서브쿼리 (group by를 메인 쿼리에서 피해서 paginate 안정)
|
||||
// 주문아이템 집계
|
||||
$oiAgg = DB::table('gc_pin_order_items')
|
||||
->selectRaw('order_id, SUM(qty) as total_qty, MIN(item_name) as first_item_name')
|
||||
->groupBy('order_id');
|
||||
|
||||
// 발행 집계
|
||||
$issueAgg = DB::table('gc_pin_issues')
|
||||
->selectRaw('order_id, COUNT(*) as issued_count')
|
||||
->groupBy('order_id');
|
||||
|
||||
$qb = DB::table('gc_payment_attempts as a')
|
||||
->leftJoin('gc_pin_order as o', 'o.id', '=', 'a.order_id')
|
||||
->leftJoinSub($oiAgg, 'oi', 'oi.order_id', '=', 'o.id')
|
||||
->leftJoinSub($issueAgg, 'gi', 'gi.order_id', '=', 'o.id')
|
||||
->where('a.mem_no', $memNo)
|
||||
|
||||
// ✅ 중요: OR 조건 전체를 반드시 하나의 where 그룹으로 묶어야 함
|
||||
// 리스트 기본 노출 대상
|
||||
->where(function ($s) {
|
||||
// 1) 취소완료
|
||||
$s->where(function ($x) {
|
||||
@ -152,6 +159,7 @@ final class UsageRepository
|
||||
|
||||
->select([
|
||||
'a.id as attempt_id',
|
||||
'o.id as order_id',
|
||||
'o.oid as order_oid',
|
||||
DB::raw("COALESCE(o.products_name, oi.first_item_name) as product_name"),
|
||||
'oi.first_item_name as item_name',
|
||||
@ -162,11 +170,12 @@ final class UsageRepository
|
||||
'o.stat_pay as order_stat_pay',
|
||||
'a.cancel_status as attempt_cancel_status',
|
||||
'o.cancel_status as order_cancel_status',
|
||||
DB::raw("COALESCE(gi.issued_count, 0) as issued_count"),
|
||||
'a.created_at as created_at',
|
||||
])
|
||||
->orderByDesc('a.id');
|
||||
|
||||
// ✅ q 검색: 거래번호(o.oid / a.oid) 정확히 일치
|
||||
// 주문번호 검색
|
||||
if ($q !== '') {
|
||||
$qb->where(function ($w) use ($q) {
|
||||
$w->where('o.oid', $q)
|
||||
@ -179,31 +188,63 @@ final class UsageRepository
|
||||
$qb->where('a.pay_method', $method);
|
||||
}
|
||||
|
||||
// ✅ 상태 필터도 화면 의미에 맞게 묶어서 처리
|
||||
// (현재 화면 기준: paid / issued / cancel를 의미 상태로 쓰는 경우)
|
||||
/**
|
||||
* 상태 필터는 화면 의미 기준
|
||||
*
|
||||
* pay : 입금/결제 진행 상태
|
||||
* issue_wait : 결제완료 + 발행대기
|
||||
* issue_done : 결제완료 + 발행완료
|
||||
* cancelled : 결제취소
|
||||
* failed : 결제실패
|
||||
*
|
||||
* 하위호환:
|
||||
* paid : issue_wait + issue_done
|
||||
* issued : pay
|
||||
* cancel : cancelled
|
||||
* canceled : cancelled
|
||||
*/
|
||||
if ($status !== '') {
|
||||
if ($status === 'paid') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'paid')
|
||||
->orWhere('o.stat_pay', 'p');
|
||||
});
|
||||
} elseif ($status === 'issued') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'issued')
|
||||
->orWhere('o.stat_pay', 'w');
|
||||
});
|
||||
} elseif (in_array($status, ['cancel', 'cancelled', 'canceled'], true)) {
|
||||
if (in_array($status, ['cancel', 'cancelled', 'canceled'], true)) {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.cancel_status', 'success')
|
||||
->orWhere('o.cancel_status', 'success');
|
||||
});
|
||||
} elseif ($status === 'issue_done') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'paid')
|
||||
->orWhere('o.stat_pay', 'p');
|
||||
});
|
||||
$qb->whereRaw('COALESCE(gi.issued_count, 0) > 0');
|
||||
} elseif ($status === 'issue_wait') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'paid')
|
||||
->orWhere('o.stat_pay', 'p');
|
||||
});
|
||||
$qb->whereRaw('COALESCE(gi.issued_count, 0) = 0');
|
||||
} elseif ($status === 'pay' || $status === 'issued') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'issued')
|
||||
->orWhere('o.stat_pay', 'w')
|
||||
->orWhere('a.status', 'ready')
|
||||
->orWhere('a.status', 'redirected');
|
||||
});
|
||||
} elseif ($status === 'paid') {
|
||||
// 하위호환: 결제완료 전체(발행대기+발행완료)
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'paid')
|
||||
->orWhere('o.stat_pay', 'p');
|
||||
});
|
||||
} elseif ($status === 'failed') {
|
||||
$qb->where(function ($x) {
|
||||
$x->where('a.status', 'failed')
|
||||
->orWhere('o.stat_pay', 'f');
|
||||
});
|
||||
} else {
|
||||
// 기타 상태는 attempts 기준으로 그대로
|
||||
$qb->where('a.status', $status);
|
||||
}
|
||||
}
|
||||
|
||||
// 날짜 필터 (date 기준)
|
||||
// 날짜 필터
|
||||
if ($from !== '') {
|
||||
$qb->whereDate('a.created_at', '>=', $from);
|
||||
}
|
||||
@ -268,4 +309,81 @@ final class UsageRepository
|
||||
|
||||
return array_map(fn($r) => (array)$r, $rows->all());
|
||||
}
|
||||
|
||||
public function getIssuesForOrder(int $orderId): array
|
||||
{
|
||||
$rows = DB::table('gc_pin_issues')
|
||||
->where('order_id', $orderId)
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
|
||||
return array_map(function ($r) {
|
||||
$arr = (array)$r;
|
||||
$arr['pins_json_decoded'] = $this->decodeJsonArray($arr['pins_json'] ?? null);
|
||||
$arr['issue_logs_json_decoded'] = $this->decodeJsonArray($arr['issue_logs_json'] ?? null);
|
||||
return $arr;
|
||||
}, $rows->all());
|
||||
}
|
||||
|
||||
public function getIssuesForOrderForUpdate(int $orderId): array
|
||||
{
|
||||
$rows = DB::table('gc_pin_issues')
|
||||
->where('order_id', $orderId)
|
||||
->orderBy('id', 'asc')
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
return array_map(fn ($r) => (array)$r, $rows->all());
|
||||
}
|
||||
|
||||
/**
|
||||
* 핀번호 확인 / SMS / 출금완료 등 강한 잠금
|
||||
*/
|
||||
public function hasLockedOrOpenedIssues(int $orderId): bool
|
||||
{
|
||||
return DB::table('gc_pin_issues')
|
||||
->where('order_id', $orderId)
|
||||
->where(function ($q) {
|
||||
$q->whereNotNull('opened_at')
|
||||
->orWhereNotNull('sms_sent_at')
|
||||
->orWhereNotNull('payout_done_at')
|
||||
->orWhere('cancel_status', 'LOCKED');
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
|
||||
public function hasAnyIssuedIssues(int $orderId): bool
|
||||
{
|
||||
return DB::table('gc_pin_issues')
|
||||
->where('order_id', $orderId)
|
||||
->where(function ($q) {
|
||||
$q->whereIn('issue_status', ['PROCESSING', 'ISSUED'])
|
||||
->orWhere('pin_count', '>', 0)
|
||||
->orWhereNotNull('issued_at');
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function markIssueOpened(int $issueId, string $openedAt, array $logs, ?string $reason = null): void
|
||||
{
|
||||
DB::table('gc_pin_issues')
|
||||
->where('id', $issueId)
|
||||
->update([
|
||||
'opened_at' => $openedAt,
|
||||
'cancel_status' => 'LOCKED',
|
||||
'cancel_locked_reason' => $reason ?? '핀번호 확인 완료',
|
||||
'issue_logs_json' => json_encode($logs, JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => $openedAt,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getSkuForOrderItem(int $productId, int $skuId): ?object
|
||||
{
|
||||
return DB::table('gc_product_skus')
|
||||
->where('product_id', $productId)
|
||||
->where('id', $skuId)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Repositories/Payments/GcPinIssueRepository.php
Normal file
43
app/Repositories/Payments/GcPinIssueRepository.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Payments;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class GcPinIssueRepository
|
||||
{
|
||||
public function findByOrderItemId(int $orderItemId): ?object
|
||||
{
|
||||
return DB::table('gc_pin_issues')
|
||||
->where('order_item_id', $orderItemId)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function findByOrderId(int $orderId): array
|
||||
{
|
||||
$rows = DB::table('gc_pin_issues')
|
||||
->where('order_id', $orderId)
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
|
||||
return array_map(fn ($r) => (array)$r, $rows->all());
|
||||
}
|
||||
|
||||
public function insert(array $data): int
|
||||
{
|
||||
return (int) DB::table('gc_pin_issues')->insertGetId($data);
|
||||
}
|
||||
|
||||
public function markOpened(int $id, string $now, array $logs, ?string $reason = null): void
|
||||
{
|
||||
DB::table('gc_pin_issues')
|
||||
->where('id', $id)
|
||||
->update([
|
||||
'opened_at' => $now,
|
||||
'cancel_status' => 'LOCKED',
|
||||
'cancel_locked_reason' => $reason ?? '핀 오픈 완료',
|
||||
'issue_logs_json' => json_encode($logs, JSON_UNESCAPED_UNICODE),
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
app/Repositories/Payments/GcPinsRepository.php
Normal file
36
app/Repositories/Payments/GcPinsRepository.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repositories\Payments;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class GcPinsRepository
|
||||
{
|
||||
public function lockAvailablePins(int $productId, int $skuId, int $qty): array
|
||||
{
|
||||
$rows = DB::table('gc_pins')
|
||||
->where('product_id', $productId)
|
||||
->where('sku_id', $skuId)
|
||||
->where('status', 'AVAILABLE')
|
||||
->orderBy('id', 'asc')
|
||||
->lockForUpdate()
|
||||
->limit($qty)
|
||||
->get();
|
||||
|
||||
return array_map(fn ($r) => (array)$r, $rows->all());
|
||||
}
|
||||
|
||||
public function updateStatusByIds(array $ids, string $status, array $extra = []): void
|
||||
{
|
||||
if (empty($ids)) return;
|
||||
|
||||
$payload = array_merge([
|
||||
'status' => $status,
|
||||
'updated_at' => now(),
|
||||
], $extra);
|
||||
|
||||
DB::table('gc_pins')
|
||||
->whereIn('id', $ids)
|
||||
->update($payload);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,9 @@ namespace App\Services\Mypage;
|
||||
|
||||
use App\Repositories\Mypage\UsageRepository;
|
||||
use App\Repositories\Payments\GcPinOrderRepository;
|
||||
use App\Repositories\Member\MemberAuthRepository;
|
||||
use App\Services\Payments\PaymentCancelService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class UsageService
|
||||
{
|
||||
@ -12,6 +14,7 @@ final class UsageService
|
||||
private readonly UsageRepository $repo,
|
||||
private readonly GcPinOrderRepository $orders,
|
||||
private readonly PaymentCancelService $cancelSvc,
|
||||
private readonly MemberAuthRepository $memberAuthRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -19,7 +22,7 @@ final class UsageService
|
||||
*/
|
||||
public function buildListPageData(int $sessionMemNo, array $filters): array
|
||||
{
|
||||
$rows = $this->repo->paginateAttemptsWithOrder($sessionMemNo, $filters, 5);
|
||||
$rows = $this->repo->paginateAttemptsWithOrder($sessionMemNo, $filters, 15);
|
||||
|
||||
return [
|
||||
'pageTitle' => '구매내역',
|
||||
@ -43,81 +46,215 @@ final class UsageService
|
||||
$order = (array)($data['order'] ?? []);
|
||||
$orderId = (int)($order['id'] ?? 0);
|
||||
|
||||
$pins = $this->repo->getPinsForOrder($orderId);
|
||||
$issues = $this->repo->getIssuesForOrder($orderId);
|
||||
$cancelLogs = $this->repo->getCancelLogsForAttempt($attemptId, 20);
|
||||
|
||||
$retData = $order['ret_data'] ?? null;
|
||||
$retArr = is_array($retData) ? $retData : [];
|
||||
$pinsOpened = !empty($retArr['pin_opened_at']);
|
||||
$pins = [];
|
||||
foreach ($issues as $issue) {
|
||||
$issue = (array)$issue;
|
||||
|
||||
$orderItemId = (int)($issue['order_item_id'] ?? 0);
|
||||
$pinsJson = $issue['pins_json_decoded'] ?? null;
|
||||
|
||||
if (!is_array($pinsJson)) {
|
||||
$pinsJson = $this->decodeJsonArray($issue['pins_json'] ?? null);
|
||||
}
|
||||
|
||||
foreach ($pinsJson as $pin) {
|
||||
$pin = (array)$pin;
|
||||
|
||||
$pins[] = [
|
||||
'id' => (int)($pin['gc_pin_id'] ?? 0),
|
||||
'issue_id' => (int)($issue['id'] ?? 0),
|
||||
'order_item_id' => $orderItemId,
|
||||
'status' => (string)($issue['issue_status'] ?? ''),
|
||||
'pin_mask' => (string)($pin['pin_mask'] ?? ''),
|
||||
'pin' => $this->decryptIssuedPin(
|
||||
(string)($pin['pin_enc'] ?? ''),
|
||||
(int)($order['mem_no'] ?? 0),
|
||||
(string)($order['oid'] ?? ''),
|
||||
$orderItemId,
|
||||
(int)($pin['seq'] ?? 0)
|
||||
),
|
||||
'face_value' => (int)($pin['face_value'] ?? 0),
|
||||
'issued_at' => (string)($pin['issued_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 이번 요청에서만 실핀 표시
|
||||
$pinsRevealed = (
|
||||
(int)request()->query('revealed', 0) === 1
|
||||
|| (int)session('pin_revealed_attempt_id', 0) === $attemptId
|
||||
);
|
||||
|
||||
// 결제완료 조건
|
||||
$attempt = (array)($data['attempt'] ?? []);
|
||||
$isPaid = (($order['stat_pay'] ?? '') === 'p') || (($attempt['status'] ?? '') === 'paid');
|
||||
|
||||
// cancel_status 기반 버튼 제어
|
||||
$aCancel = (string)($attempt['cancel_status'] ?? 'none');
|
||||
$canCancel = $isPaid && !$pinsOpened && in_array($aCancel, ['none','failed'], true);
|
||||
// 핀 발행 완료되면 회원 취소 불가
|
||||
$hasIssuedIssues = !empty($issues);
|
||||
|
||||
$aCancel = (string)($attempt['cancel_status'] ?? 'none');
|
||||
$canCancel = $isPaid
|
||||
&& !$hasIssuedIssues
|
||||
&& in_array($aCancel, ['none', 'failed'], true);
|
||||
|
||||
$data['issues'] = $issues;
|
||||
$data['pins'] = $pins;
|
||||
$data['pinsOpened'] = $pinsOpened;
|
||||
$data['pinsRevealed'] = $pinsRevealed;
|
||||
$data['hasIssuedIssues'] = $hasIssuedIssues;
|
||||
$data['canCancel'] = $canCancel;
|
||||
$data['cancelLogs'] = $cancelLogs;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function decodeJsonArray($json): array
|
||||
{
|
||||
if (is_array($json)) {
|
||||
return $json;
|
||||
}
|
||||
|
||||
$json = trim((string)$json);
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
private function decryptIssuedPin(string $enc, int $memNo, string $oid, int $orderItemId, int $seq): string
|
||||
{
|
||||
if (trim($enc) === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$plain = \Illuminate\Support\Facades\Crypt::decryptString($enc);
|
||||
|
||||
$suffix = '|M:' . $memNo . '|O:' . $oid . '|I:' . $orderItemId . '|S:' . $seq;
|
||||
|
||||
if (str_ends_with($plain, $suffix)) {
|
||||
return substr($plain, 0, -strlen($suffix));
|
||||
}
|
||||
|
||||
return $plain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 핀 오픈(확인): ret_data에 pin_opened_at 기록
|
||||
*/
|
||||
public function openPins(int $attemptId, int $sessionMemNo): array
|
||||
{
|
||||
return \Illuminate\Support\Facades\DB::transaction(function () use ($attemptId, $sessionMemNo) {
|
||||
$row = $this->repo->findAttemptWithOrder($attemptId);
|
||||
if (!$row) return ['ok'=>false, 'message'=>'결제내역을 찾을 수 없습니다.'];
|
||||
if (!$row) {
|
||||
return ['ok' => false, 'message' => '결제내역을 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$attemptMem = (int)($row->attempt_mem_no ?? 0);
|
||||
$orderMem = (int)($row->order_mem_no ?? 0);
|
||||
|
||||
if ($attemptMem !== $sessionMemNo || ($orderMem > 0 && $orderMem !== $sessionMemNo)) {
|
||||
return ['ok'=>false, 'message'=>'권한이 없습니다.'];
|
||||
return ['ok' => false, 'message' => '권한이 없습니다.'];
|
||||
}
|
||||
|
||||
$attemptStatus = (string)($row->attempt_status ?? '');
|
||||
$orderStatPay = (string)($row->order_stat_pay ?? '');
|
||||
|
||||
if (!(($orderStatPay === 'p') || ($attemptStatus === 'paid'))) {
|
||||
return ['ok'=>false, 'message'=>'결제완료 상태에서만 핀 확인이 가능합니다.'];
|
||||
return ['ok' => false, 'message' => '결제완료 상태에서만 핀 확인이 가능합니다.'];
|
||||
}
|
||||
|
||||
$oid = (string)($row->order_oid ?? '');
|
||||
if ($oid === '') return ['ok'=>false, 'message'=>'주문정보가 올바르지 않습니다.'];
|
||||
|
||||
$order = $this->orders->findByOidForUpdate($oid);
|
||||
if (!$order) return ['ok'=>false, 'message'=>'주문을 찾을 수 없습니다.'];
|
||||
|
||||
$ret = (array)($order->ret_data ?? []);
|
||||
if (!empty($ret['pin_opened_at'])) {
|
||||
return ['ok'=>true];
|
||||
$orderId = (int)($row->order_id ?? 0);
|
||||
if ($orderId <= 0) {
|
||||
return ['ok' => false, 'message' => '주문정보가 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
$ret['pin_opened_at'] = now()->toDateTimeString();
|
||||
$order->ret_data = $ret;
|
||||
$order->save();
|
||||
|
||||
return ['ok'=>true];
|
||||
// 발행건 잠금 조회
|
||||
$issues = $this->repo->getIssuesForOrderForUpdate($orderId);
|
||||
if (empty($issues)) {
|
||||
return ['ok' => false, 'message' => '발행된 핀이 없습니다.'];
|
||||
}
|
||||
|
||||
// 실제 발행된 건만 대상
|
||||
$issuedIssues = array_values(array_filter($issues, function ($issue) {
|
||||
$issue = (array)$issue;
|
||||
|
||||
$status = (string)($issue['issue_status'] ?? '');
|
||||
$pinCnt = (int)($issue['pin_count'] ?? 0);
|
||||
|
||||
return in_array($status, ['ISSUED', 'PROCESSING'], true) || $pinCnt > 0;
|
||||
}));
|
||||
|
||||
if (empty($issuedIssues)) {
|
||||
return ['ok' => false, 'message' => '확인 가능한 핀이 없습니다.'];
|
||||
}
|
||||
|
||||
// 이미 모두 오픈된 상태면 성공 처리
|
||||
$allOpened = collect($issuedIssues)->every(function ($issue) {
|
||||
$issue = (array)$issue;
|
||||
return !empty($issue['opened_at']);
|
||||
});
|
||||
|
||||
if ($allOpened) {
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
$now = now()->format('Y-m-d H:i:s');
|
||||
|
||||
foreach ($issuedIssues as $issue) {
|
||||
$issue = (array)$issue;
|
||||
|
||||
if (!empty($issue['opened_at'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$logs = $this->decodeJsonArray($issue['issue_logs_json'] ?? null);
|
||||
$logs[] = [
|
||||
'at' => $now,
|
||||
'type' => 'OPEN',
|
||||
'code' => 'PIN_OPENED',
|
||||
'msg' => '회원 웹페이지에서 핀 오픈',
|
||||
];
|
||||
|
||||
$this->repo->markIssueOpened(
|
||||
(int)$issue['id'],
|
||||
$now,
|
||||
$logs,
|
||||
'핀 오픈 완료'
|
||||
);
|
||||
}
|
||||
|
||||
return ['ok' => true];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 결제완료 후 취소(핀 오픈 전만)
|
||||
*/
|
||||
public function cancelPaidAttempt(int $attemptId, int $sessionMemNo, string $reason): array
|
||||
{
|
||||
$data = $this->buildDetailPageData($attemptId, $sessionMemNo);
|
||||
$order = (array)($data['order'] ?? []);
|
||||
$orderId = (int)($order['id'] ?? 0);
|
||||
|
||||
$pinsOpened = (bool)($data['pinsOpened'] ?? false);
|
||||
// 핀 발행이 완료되면 회원 취소 금지
|
||||
if ($orderId > 0 && $this->repo->hasAnyIssuedIssues($orderId)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => '핀 발행이 완료된 주문은 회원이 직접 취소할 수 없습니다. 관리자에게 문의해 주세요.',
|
||||
];
|
||||
}
|
||||
|
||||
return $this->cancelSvc->cancelByAttempt(
|
||||
$attemptId,
|
||||
['type' => 'user', 'mem_no' => $sessionMemNo, 'id' => $sessionMemNo],
|
||||
$reason,
|
||||
$pinsOpened
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@ -445,4 +582,47 @@ final class UsageService
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
public function revealPins(int $attemptId, int $sessionMemNo, string $pin2): array
|
||||
{
|
||||
$row = $this->repo->findAttemptWithOrder($attemptId);
|
||||
if (!$row) {
|
||||
return ['ok' => false, 'message' => '결제내역을 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$attemptMem = (int)($row->attempt_mem_no ?? 0);
|
||||
$orderMem = (int)($row->order_mem_no ?? 0);
|
||||
|
||||
if ($attemptMem !== $sessionMemNo || ($orderMem > 0 && $orderMem !== $sessionMemNo)) {
|
||||
return ['ok' => false, 'message' => '권한이 없습니다.'];
|
||||
}
|
||||
|
||||
$attemptStatus = (string)($row->attempt_status ?? '');
|
||||
$orderStatPay = (string)($row->order_stat_pay ?? '');
|
||||
|
||||
if (!(($orderStatPay === 'p') || ($attemptStatus === 'paid'))) {
|
||||
return ['ok' => false, 'message' => '결제완료 상태에서만 핀번호 확인이 가능합니다.'];
|
||||
}
|
||||
|
||||
$pin2Ok = $this->memberAuthRepo->verifyPin2($sessionMemNo, $pin2);
|
||||
if (!$pin2Ok) {
|
||||
return ['ok' => false, 'message' => '2차 비밀번호가 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
$orderId = (int)($row->order_id ?? 0);
|
||||
if ($orderId <= 0) {
|
||||
return ['ok' => false, 'message' => '주문정보가 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
$issues = $this->repo->getIssuesForOrder($orderId);
|
||||
if (empty($issues)) {
|
||||
return ['ok' => false, 'message' => '발행된 핀이 없습니다.'];
|
||||
}
|
||||
|
||||
// 상태값 변경 없이 이번 요청에서만 실핀 표시
|
||||
session()->flash('pin_revealed_attempt_id', $attemptId);
|
||||
|
||||
return ['ok' => true];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
437
app/Services/Payments/OwnPinIssueService.php
Normal file
437
app/Services/Payments/OwnPinIssueService.php
Normal file
@ -0,0 +1,437 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use App\Repositories\Mypage\UsageRepository;
|
||||
use App\Repositories\Payments\GcPinIssueRepository;
|
||||
use App\Repositories\Payments\GcPinsRepository;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class OwnPinIssueService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UsageRepository $usageRepo,
|
||||
private readonly GcPinIssueRepository $issueRepo,
|
||||
private readonly GcPinsRepository $pinsRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 웹페이지 직접발행 실행
|
||||
* - 주문에는 OWN_PIN 또는 API_LINK 중 한 종류만 존재한다고 가정
|
||||
* - OWN_PIN : 실제 자사핀 발행
|
||||
* - API_LINK : 현재는 분기만 처리하고 미구현 메시지 반환
|
||||
*/
|
||||
public function issuePinInstant(int $attemptId, int $sessionMemNo): array
|
||||
{
|
||||
try {
|
||||
return DB::transaction(function () use ($attemptId, $sessionMemNo) {
|
||||
$row = $this->usageRepo->findAttemptWithOrder($attemptId);
|
||||
if (!$row) {
|
||||
return ['ok' => false, 'message' => '결제내역을 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$attemptMem = (int)($row->attempt_mem_no ?? 0);
|
||||
$orderMem = (int)($row->order_mem_no ?? 0);
|
||||
|
||||
if ($attemptMem !== $sessionMemNo || ($orderMem > 0 && $orderMem !== $sessionMemNo)) {
|
||||
return ['ok' => false, 'message' => '권한이 없습니다.'];
|
||||
}
|
||||
|
||||
$orderId = (int)($row->order_id ?? 0);
|
||||
if ($orderId <= 0) {
|
||||
return ['ok' => false, 'message' => '주문 정보가 없습니다.'];
|
||||
}
|
||||
|
||||
$isPaid = (($row->order_stat_pay ?? '') === 'p') || (($row->attempt_status ?? '') === 'paid');
|
||||
if (!$isPaid) {
|
||||
return ['ok' => false, 'message' => '결제 완료 주문만 발행할 수 있습니다.'];
|
||||
}
|
||||
|
||||
$items = $this->usageRepo->getOrderItems($orderId);
|
||||
if ($items->isEmpty()) {
|
||||
return ['ok' => false, 'message' => '주문 아이템이 없습니다.'];
|
||||
}
|
||||
|
||||
$now = now()->format('Y-m-d H:i:s');
|
||||
$created = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$orderItemId = (int)($item->id ?? 0);
|
||||
if ($orderItemId <= 0) {
|
||||
return ['ok' => false, 'message' => '주문 아이템 정보가 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
// 이미 발행된 주문아이템이면 중복 방지
|
||||
$exists = $this->issueRepo->findByOrderItemId($orderItemId);
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$meta = $this->decodeJson((string)($item->meta ?? ''));
|
||||
$productId = (int)($meta['product_id'] ?? 0);
|
||||
|
||||
// 정책상 item_code = gc_product_skus.id
|
||||
$skuId = (int)($item->item_code ?? ($meta['sku_id'] ?? 0));
|
||||
|
||||
if ($productId <= 0 || $skuId <= 0) {
|
||||
return ['ok' => false, 'message' => '상품 정보(product_id/sku_id)가 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
$qty = (int)($item->qty ?? 0);
|
||||
if ($qty <= 0) {
|
||||
return ['ok' => false, 'message' => '주문 수량이 올바르지 않습니다.'];
|
||||
}
|
||||
|
||||
// 상품의 회원 발행 방식 허용 여부 확인
|
||||
$issueOptions = $this->usageRepo->getProductsIssueOptions([$productId]);
|
||||
$methods = $issueOptions[$productId]['pin_check_methods'] ?? [];
|
||||
if (!in_array('PIN_INSTANT', $methods, true)) {
|
||||
return ['ok' => false, 'message' => '웹페이지 직접발행이 허용되지 않은 상품입니다.'];
|
||||
}
|
||||
|
||||
// SKU 조회 -> sales_method 분기
|
||||
$sku = $this->usageRepo->getSkuForOrderItem($productId, $skuId);
|
||||
if (!$sku) {
|
||||
return ['ok' => false, 'message' => 'SKU 정보를 찾을 수 없습니다.'];
|
||||
}
|
||||
|
||||
$salesMethod = (string)($sku->sales_method ?? 'OWN_PIN');
|
||||
$apiProviderId = (int)($sku->api_provider_id ?? 0);
|
||||
$apiProductCode = trim((string)($sku->api_product_code ?? ''));
|
||||
|
||||
if ($salesMethod === 'OWN_PIN') {
|
||||
$this->issueOwnPinItem(
|
||||
row: $row,
|
||||
item: (array)$item,
|
||||
attemptId: $attemptId,
|
||||
orderId: $orderId,
|
||||
orderItemId: $orderItemId,
|
||||
productId: $productId,
|
||||
skuId: $skuId,
|
||||
qty: $qty,
|
||||
now: $now
|
||||
);
|
||||
|
||||
$created++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($salesMethod === 'API_LINK') {
|
||||
$this->issueApiLinkPlaceholder(
|
||||
row: $row,
|
||||
item: (array)$item,
|
||||
attemptId: $attemptId,
|
||||
orderId: $orderId,
|
||||
orderItemId: $orderItemId,
|
||||
productId: $productId,
|
||||
skuId: $skuId,
|
||||
qty: $qty,
|
||||
apiProviderId: $apiProviderId,
|
||||
apiProductCode: $apiProductCode,
|
||||
now: $now
|
||||
);
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => '연동발행 상품입니다. 연동발행 구현 후 처리됩니다.',
|
||||
];
|
||||
}
|
||||
|
||||
return ['ok' => false, 'message' => '알 수 없는 발행 방식입니다.'];
|
||||
}
|
||||
|
||||
if ($created <= 0) {
|
||||
return ['ok' => true, 'message' => '이미 발행된 주문입니다.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'message' => '자사핀 발행이 완료되었습니다.'];
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage() !== '' ? $e->getMessage() : '핀 발행 중 오류가 발생했습니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OWN_PIN 실제 발행
|
||||
*/
|
||||
private function issueOwnPinItem(
|
||||
object|array $row,
|
||||
array $item,
|
||||
int $attemptId,
|
||||
int $orderId,
|
||||
int $orderItemId,
|
||||
int $productId,
|
||||
int $skuId,
|
||||
int $qty,
|
||||
string $now,
|
||||
): void {
|
||||
$row = (object)$row;
|
||||
|
||||
$pins = $this->pinsRepo->lockAvailablePins($productId, $skuId, $qty);
|
||||
if (count($pins) < $qty) {
|
||||
throw new \Exception('죄송합니다 재고가 부족합니다. 관리자에게 문의해 주시면 바로 처리해 드리겠습니다.');
|
||||
}
|
||||
|
||||
$pinIds = array_map(fn ($p) => (int)$p['id'], $pins);
|
||||
|
||||
// 1차 선점
|
||||
$this->pinsRepo->updateStatusByIds($pinIds, 'HOLD', [
|
||||
'order_id' => $orderId,
|
||||
]);
|
||||
|
||||
$memNo = (int)($row->order_mem_no ?? 0);
|
||||
$oid = (string)($row->order_oid ?? '');
|
||||
|
||||
$pinsJson = [];
|
||||
foreach ($pins as $idx => $pin) {
|
||||
$rawPin = $this->decryptSourcePin((string)($pin['pin_code'] ?? ''));
|
||||
|
||||
$memberEnc = $this->encryptForIssue(
|
||||
$rawPin,
|
||||
$memNo,
|
||||
$oid,
|
||||
$orderItemId,
|
||||
$idx + 1
|
||||
);
|
||||
|
||||
$pinsJson[] = [
|
||||
'seq' => $idx + 1,
|
||||
'gc_pin_id' => (int)($pin['id'] ?? 0),
|
||||
'pin_enc' => $memberEnc,
|
||||
'pin_mask' => $this->maskPin($rawPin),
|
||||
'pin_hash' => hash('sha256', $rawPin),
|
||||
'face_value' => (int)($pin['face_value'] ?? 0),
|
||||
'issued_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
$logs = [[
|
||||
'at' => $now,
|
||||
'type' => 'ISSUE',
|
||||
'code' => 'OWN_PIN_ISSUED',
|
||||
'msg' => '자사핀 발행 완료',
|
||||
'count' => count($pinsJson),
|
||||
]];
|
||||
|
||||
$this->issueRepo->insert([
|
||||
'issue_no' => $this->makeIssueNo(),
|
||||
'order_id' => $orderId,
|
||||
'order_item_id' => $orderItemId,
|
||||
'oid' => $oid,
|
||||
'mem_no' => $memNo,
|
||||
|
||||
'product_id' => $productId,
|
||||
'sku_id' => $skuId,
|
||||
'item_name' => (string)($item['item_name'] ?? ''),
|
||||
'item_code' => (string)($item['item_code'] ?? ''),
|
||||
'qty' => $qty,
|
||||
|
||||
'unit_price' => (int)($item['unit_price'] ?? 0),
|
||||
'unit_pay_price' => (int)($item['unit_pay_price'] ?? 0),
|
||||
'line_subtotal' => (int)($item['line_subtotal'] ?? 0),
|
||||
'line_fee' => (int)($item['line_fee'] ?? 0),
|
||||
'line_total' => (int)($item['line_total'] ?? 0),
|
||||
|
||||
'supply_type' => 'OWN_PIN',
|
||||
'provider_code' => 'OWN',
|
||||
'provider_id' => null,
|
||||
'provider_product_code' => null,
|
||||
|
||||
'member_delivery_type' => 'PIN_INSTANT',
|
||||
'issue_status' => 'ISSUED',
|
||||
|
||||
'pin_count' => count($pinsJson),
|
||||
'pins_json' => json_encode($pinsJson, JSON_UNESCAPED_UNICODE),
|
||||
'pin_key_ver' => 1,
|
||||
|
||||
'issued_at' => $now,
|
||||
'opened_at' => null,
|
||||
|
||||
'sms_phone_enc' => null,
|
||||
'sms_phone_hash' => null,
|
||||
'sms_sent_at' => null,
|
||||
'sms_result_code' => null,
|
||||
'sms_result_msg' => null,
|
||||
|
||||
'payout_amount' => 0,
|
||||
'payout_fee_amount' => 0,
|
||||
'payout_status' => 'NONE',
|
||||
'payout_done_at' => null,
|
||||
|
||||
'cancel_status' => 'AVAILABLE',
|
||||
'cancel_locked_reason' => null,
|
||||
'cancelled_at' => null,
|
||||
|
||||
'provider_payload_json' => null,
|
||||
'issue_logs_json' => json_encode($logs, JSON_UNESCAPED_UNICODE),
|
||||
'meta' => json_encode([
|
||||
'source' => 'OWN_PIN',
|
||||
'attempt_id' => $attemptId,
|
||||
'sales_method' => 'OWN_PIN',
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
// 최종 판매 처리
|
||||
$this->pinsRepo->updateStatusByIds($pinIds, 'SOLD', [
|
||||
'sold_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* API_LINK 분기만 먼저 저장
|
||||
* 실제 연동 통신은 다음 단계에서 구현
|
||||
*/
|
||||
private function issueApiLinkPlaceholder(
|
||||
object|array $row,
|
||||
array $item,
|
||||
int $attemptId,
|
||||
int $orderId,
|
||||
int $orderItemId,
|
||||
int $productId,
|
||||
int $skuId,
|
||||
int $qty,
|
||||
int $apiProviderId,
|
||||
string $apiProductCode,
|
||||
string $now,
|
||||
): void {
|
||||
$row = (object)$row;
|
||||
|
||||
$logs = [[
|
||||
'at' => $now,
|
||||
'type' => 'ISSUE',
|
||||
'code' => 'API_LINK_PENDING',
|
||||
'msg' => '연동발행 대상 상품 - 실제 연동 구현 전',
|
||||
'api_provider_id' => $apiProviderId,
|
||||
'api_product_code' => $apiProductCode,
|
||||
]];
|
||||
|
||||
$this->issueRepo->insert([
|
||||
'issue_no' => $this->makeIssueNo(),
|
||||
'order_id' => $orderId,
|
||||
'order_item_id' => $orderItemId,
|
||||
'oid' => (string)($row->order_oid ?? ''),
|
||||
'mem_no' => (int)($row->order_mem_no ?? 0),
|
||||
|
||||
'product_id' => $productId,
|
||||
'sku_id' => $skuId,
|
||||
'item_name' => (string)($item['item_name'] ?? ''),
|
||||
'item_code' => (string)($item['item_code'] ?? ''),
|
||||
'qty' => $qty,
|
||||
|
||||
'unit_price' => (int)($item['unit_price'] ?? 0),
|
||||
'unit_pay_price' => (int)($item['unit_pay_price'] ?? 0),
|
||||
'line_subtotal' => (int)($item['line_subtotal'] ?? 0),
|
||||
'line_fee' => (int)($item['line_fee'] ?? 0),
|
||||
'line_total' => (int)($item['line_total'] ?? 0),
|
||||
|
||||
'supply_type' => 'API_LINK',
|
||||
'provider_code' => 'API_LINK',
|
||||
'provider_id' => $apiProviderId > 0 ? $apiProviderId : null,
|
||||
'provider_product_code' => $apiProductCode !== '' ? $apiProductCode : null,
|
||||
|
||||
'member_delivery_type' => 'PIN_INSTANT',
|
||||
'issue_status' => 'PROCESSING',
|
||||
|
||||
'pin_count' => 0,
|
||||
'pins_json' => null,
|
||||
'pin_key_ver' => 1,
|
||||
|
||||
'issued_at' => null,
|
||||
'opened_at' => null,
|
||||
|
||||
'sms_phone_enc' => null,
|
||||
'sms_phone_hash' => null,
|
||||
'sms_sent_at' => null,
|
||||
'sms_result_code' => null,
|
||||
'sms_result_msg' => null,
|
||||
|
||||
'payout_amount' => 0,
|
||||
'payout_fee_amount' => 0,
|
||||
'payout_status' => 'NONE',
|
||||
'payout_done_at' => null,
|
||||
|
||||
'cancel_status' => 'AVAILABLE',
|
||||
'cancel_locked_reason' => null,
|
||||
'cancelled_at' => null,
|
||||
|
||||
'provider_payload_json' => json_encode([
|
||||
'api_provider_id' => $apiProviderId,
|
||||
'api_product_code' => $apiProductCode,
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
'issue_logs_json' => json_encode($logs, JSON_UNESCAPED_UNICODE),
|
||||
'meta' => json_encode([
|
||||
'source' => 'API_LINK',
|
||||
'attempt_id' => $attemptId,
|
||||
'sales_method' => 'API_LINK',
|
||||
], JSON_UNESCAPED_UNICODE),
|
||||
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private function decodeJson(string $json): array
|
||||
{
|
||||
$json = trim($json);
|
||||
if ($json === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$arr = json_decode($json, true);
|
||||
return is_array($arr) ? $arr : [];
|
||||
}
|
||||
|
||||
private function decryptSourcePin(string $enc): string
|
||||
{
|
||||
$enc = trim($enc);
|
||||
if ($enc === '') {
|
||||
throw new \RuntimeException('원본 핀 데이터가 비어 있습니다.');
|
||||
}
|
||||
|
||||
return Crypt::decryptString($enc);
|
||||
}
|
||||
|
||||
private function maskPin(string $raw): string
|
||||
{
|
||||
$v = preg_replace('/\s+/', '', $raw);
|
||||
$len = mb_strlen($v);
|
||||
|
||||
if ($len <= 4) {
|
||||
return str_repeat('*', $len);
|
||||
}
|
||||
|
||||
if ($len <= 8) {
|
||||
return mb_substr($v, 0, 2)
|
||||
. str_repeat('*', max(1, $len - 4))
|
||||
. mb_substr($v, -2);
|
||||
}
|
||||
|
||||
return mb_substr($v, 0, 4)
|
||||
. str_repeat('*', max(4, $len - 6))
|
||||
. mb_substr($v, -2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 발행용 암호화
|
||||
* 현재는 Crypt 기반으로 suffix를 붙이는 방식
|
||||
* 추후 HKDF 기반 파생키 구조로 교체 가능
|
||||
*/
|
||||
private function encryptForIssue(string $rawPin, int $memNo, string $oid, int $orderItemId, int $seq): string
|
||||
{
|
||||
return Crypt::encryptString(
|
||||
$rawPin . '|M:' . $memNo . '|O:' . $oid . '|I:' . $orderItemId . '|S:' . $seq
|
||||
);
|
||||
}
|
||||
|
||||
private function makeIssueNo(): string
|
||||
{
|
||||
return 'GI' . now()->format('YmdHis') . strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
|
||||
}
|
||||
}
|
||||
@ -107,7 +107,7 @@ return [
|
||||
[
|
||||
'category' => 'code',
|
||||
'q' => '상품 코드는 어디에서 확인하나요?',
|
||||
'a' => "마이페이지 > 이용내역에서 확인 가능합니다.\n단, 일부 상품은 휴대폰으로 즉시 전송되어 이용내역에서 확인이 어려울 수 있습니다.\n미수신/확인 불가 시 1:1 문의로 접수해 주세요.",
|
||||
'a' => "마이페이지 > 구매내역에서 확인 가능합니다.\n단, 일부 상품은 휴대폰으로 즉시 전송되어 구매내역에서 확인이 어려울 수 있습니다.\n미수신/확인 불가 시 1:1 문의로 접수해 주세요.",
|
||||
],
|
||||
[
|
||||
'category' => 'code',
|
||||
|
||||
@ -86,7 +86,7 @@ return [
|
||||
],
|
||||
'mypage_tabs' => [
|
||||
['label' => '나의정보', 'route' => 'web.mypage.info.index', 'key' => 'info'],
|
||||
['label' => '이용내역', 'route' => 'web.mypage.usage.index', 'key' => 'usage'],
|
||||
['label' => '구매내역', 'route' => 'web.mypage.usage.index', 'key' => 'usage'],
|
||||
['label' => '교환내역', 'route' => 'web.mypage.exchange.index', 'key' => 'exchange'],
|
||||
['label' => '1:1문의내역', 'route' => 'web.mypage.qna.index', 'key' => 'qna'],
|
||||
],
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
<ul style="color: #fda4af; margin-top: 8px; margin-bottom: 0; padding-left: 20px; font-size: 13px;">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeachZ
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
<div class="border-t profile-sep"></div>
|
||||
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.mypage.info.index') }}">내 정보</a>
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.mypage.usage.index') }}">이용내역</a>
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.mypage.usage.index') }}">구매내역</a>
|
||||
<a class="block px-4 py-2 hover:bg-gray-50" href="{{ route('web.cs.qna.index') }}">1:1 문의</a>
|
||||
|
||||
<div class="border-t profile-sep"></div>
|
||||
|
||||
@ -115,7 +115,7 @@
|
||||
|
||||
<div class="guide-note">
|
||||
<b>확인</b>
|
||||
결제 완료 후 안내가 진행되며, 필요한 경우 마이페이지 이용내역에서 확인할 수 있어요.
|
||||
결제 완료 후 안내가 진행되며, 필요한 경우 마이페이지 구매내역에서 확인할 수 있어요.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
||||
@ -19,26 +19,34 @@
|
||||
};
|
||||
|
||||
// 상태는 "결제완료/결제취소" 중심 + 화면 깨짐 방지 최소 처리
|
||||
$statusLabel = function ($r) {
|
||||
$payStatusLabel = function ($r) {
|
||||
$aCancel = (string)($r->attempt_cancel_status ?? 'none');
|
||||
$oCancel = (string)($r->order_cancel_status ?? 'none');
|
||||
|
||||
// 결제 후 취소 인식: status/stat_pay 유지 + cancel_status=success
|
||||
if ($aCancel === 'success' || $oCancel === 'success') return '결제취소';
|
||||
|
||||
$aStatus = (string)($r->attempt_status ?? '');
|
||||
$oPay = (string)($r->order_stat_pay ?? '');
|
||||
|
||||
if ($aStatus === 'paid' || $oPay === 'p') return '결제완료';
|
||||
|
||||
// 아래는 운영/테스트 중 섞여도 UI가 깨지지 않게 최소 표시
|
||||
if ($aStatus === 'failed' || $oPay === 'f') return '결제실패';
|
||||
if ($aStatus === 'issued' || $oPay === 'w') return '입금대기';
|
||||
|
||||
return '진행중';
|
||||
};
|
||||
|
||||
$statusClass = function ($label) {
|
||||
$issueStatusLabel = function ($r) use ($payStatusLabel) {
|
||||
$pay = $payStatusLabel($r);
|
||||
|
||||
// 결제취소면 발행 상태는 의미 없음
|
||||
if ($pay === '결제취소') return '-';
|
||||
|
||||
$issuedCount = (int)($r->issued_count ?? 0);
|
||||
|
||||
return $issuedCount > 0 ? '발행완료' : '발행대기';
|
||||
};
|
||||
|
||||
$payStatusClass = function ($label) {
|
||||
return match ($label) {
|
||||
'결제취소' => 'pill--danger',
|
||||
'결제완료' => 'pill--ok',
|
||||
@ -48,6 +56,14 @@
|
||||
};
|
||||
};
|
||||
|
||||
$issueStatusClass = function ($label) {
|
||||
return match ($label) {
|
||||
'발행완료' => 'pill--ok',
|
||||
'발행대기' => 'pill--wait',
|
||||
default => 'pill--muted',
|
||||
};
|
||||
};
|
||||
|
||||
$formatDate = function ($v) {
|
||||
$s = (string)$v;
|
||||
if ($s === '') return '-';
|
||||
@ -120,8 +136,10 @@
|
||||
$qty = (int)($r->total_qty ?? 0);
|
||||
$money = (int)($r->pay_money ?? 0);
|
||||
$method = (string)($r->pay_method ?? '');
|
||||
$st = $statusLabel($r);
|
||||
$stCls = $statusClass($st);
|
||||
$paySt = $payStatusLabel($r);
|
||||
$payStCls = $payStatusClass($paySt);
|
||||
$issueSt = $issueStatusLabel($r);
|
||||
$issueStCls = $issueStatusClass($issueSt);
|
||||
$dt = $formatDate($r->created_at ?? '');
|
||||
$href = route('web.mypage.usage.show', array_merge(['attemptId' => $r->attempt_id], $listQuery));
|
||||
@endphp
|
||||
@ -129,7 +147,10 @@
|
||||
<a class="mcard" href="{{ $href }}">
|
||||
<div class="mcard__top">
|
||||
<div class="mcard__no">No. {{ $no }}</div>
|
||||
<span class="pill {{ $stCls }}">{{ $st }}</span>
|
||||
<div class="mcard__badges">
|
||||
<span class="pill {{ $payStCls }}">{{ $paySt }}</span>
|
||||
<span class="pill {{ $issueStCls }}">{{ $issueSt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcard__title">
|
||||
@ -170,6 +191,7 @@
|
||||
<th style="width:110px;">결제수단</th>
|
||||
<th style="width:80px;">수량</th>
|
||||
<th style="width:130px;">금액</th>
|
||||
<th style="width:120px;">결제</th>
|
||||
<th style="width:120px;">상태</th>
|
||||
<th style="width:160px;">일시</th>
|
||||
</tr>
|
||||
@ -186,8 +208,10 @@
|
||||
$qty = (int)($r->total_qty ?? 0);
|
||||
$money = (int)($r->pay_money ?? 0);
|
||||
$method = (string)($r->pay_method ?? '');
|
||||
$st = $statusLabel($r);
|
||||
$stCls = $statusClass($st);
|
||||
$paySt = $payStatusLabel($r);
|
||||
$payStCls = $payStatusClass($paySt);
|
||||
$issueSt = $issueStatusLabel($r);
|
||||
$issueStCls = $issueStatusClass($issueSt);
|
||||
$dt = $formatDate($r->created_at ?? '');
|
||||
$href = route('web.mypage.usage.show', array_merge(['attemptId' => $r->attempt_id], $listQuery));
|
||||
@endphp
|
||||
@ -197,11 +221,12 @@
|
||||
<td>{{ $methodLabel($method) }}</td>
|
||||
<td>{{ $qty }}</td>
|
||||
<td class="money">{{ number_format($money) }} 원</td>
|
||||
<td><span class="pill {{ $stCls }}">{{ $st }}</span></td>
|
||||
<td><span class="pill {{ $payStCls }}">{{ $paySt }}</span></td>
|
||||
<td><span class="pill {{ $issueStCls }}">{{ $issueSt }}</span></td>
|
||||
<td>{{ $dt }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="empty">구매내역이 없습니다.</td></tr>
|
||||
<tr><td colspan="8" class="empty">구매내역이 없습니다.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -305,5 +330,6 @@
|
||||
.list-mobile{display:none;}
|
||||
.list-desktop{display:block;}
|
||||
}
|
||||
.mcard__badges{display:flex; gap:6px; flex-wrap:wrap; justify-content:flex-end;}
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@ -6,13 +6,14 @@
|
||||
$order = $order ?? [];
|
||||
$items = $items ?? [];
|
||||
$pins = $pins ?? [];
|
||||
$pinsOpened = (bool)($pinsOpened ?? false);
|
||||
$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) {
|
||||
@ -24,12 +25,10 @@
|
||||
};
|
||||
};
|
||||
|
||||
// 리스트와 동일한 상태 라벨
|
||||
$statusLabel = function () use ($attempt, $order) {
|
||||
$aCancel = (string)($attempt['cancel_status'] ?? 'none');
|
||||
$oCancel = (string)($order['cancel_status'] ?? 'none');
|
||||
|
||||
// 결제 후 취소는 cancel_status=success로만 인식
|
||||
if ($aCancel === 'success' || $oCancel === 'success') return '결제취소';
|
||||
|
||||
$aStatus = (string)($attempt['status'] ?? '');
|
||||
@ -37,14 +36,12 @@
|
||||
|
||||
if ($aStatus === 'paid' || $oPay === 'p') return '결제완료';
|
||||
if ($aStatus === 'issued' || $oPay === 'w') return '입금대기';
|
||||
|
||||
// 화면 깨짐 방지용(원하면 숨겨도 됨)
|
||||
if ($aStatus === 'failed' || $oPay === 'f') return '결제실패';
|
||||
return '진행중';
|
||||
};
|
||||
|
||||
$st = $statusLabel();
|
||||
$isCancelledAfterPaid = ($st === '결제취소'); // 취소 완료면 전표만 남김
|
||||
$isCancelledAfterPaid = ($st === '결제취소');
|
||||
|
||||
$statusClass = function ($label) {
|
||||
return match ($label) {
|
||||
@ -56,7 +53,6 @@
|
||||
};
|
||||
};
|
||||
|
||||
// 전표용 값
|
||||
$attemptId = (int)($attempt['id'] ?? 0);
|
||||
$oid = (string)($order['oid'] ?? '');
|
||||
$method = (string)($order['pay_method'] ?? ($attempt['pay_method'] ?? ''));
|
||||
@ -67,32 +63,22 @@
|
||||
$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;
|
||||
|
||||
// 핀발행 완료 여부 (우선 pins 존재 기준)
|
||||
// 추후 서버에서 bool($pinsIssuedCompleted) 내려주면 그 값 우선 사용 권장
|
||||
$isPinIssuedCompleted = (bool)($pinsIssuedCompleted ?? !empty($pins));
|
||||
|
||||
// 오른쪽 영역 배너 모드 조건
|
||||
$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'];
|
||||
@ -112,7 +98,6 @@
|
||||
if (isset($issueMap[$k])) $issueMissingLabels[] = $issueMap[$k];
|
||||
}
|
||||
|
||||
// 1개만 가능하면 그 옵션을 기본으로 열어둠
|
||||
$issueOpenKey = (count($issueMethods) === 1) ? ($issueMethods[0] ?? null) : null;
|
||||
@endphp
|
||||
|
||||
@ -128,9 +113,7 @@
|
||||
<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>
|
||||
@ -186,16 +169,8 @@
|
||||
<span class="v">{{ number_format($payMoney) }}원</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!empty($order['cancel_last_msg']))
|
||||
<div class="notice-box notice-box--err">
|
||||
<b>취소 처리 결과</b><br>
|
||||
{{ $order['cancel_last_msg'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 우측: 핀발행 인터랙션 또는 안내 배너 --}}
|
||||
<aside class="right-panel {{ $useRightBannerMode ? 'right-panel--banner right-panel--mobile-hide' : '' }}">
|
||||
@if($useRightBannerMode)
|
||||
<div class="banner-stack">
|
||||
@ -240,11 +215,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-banner info-banner--warn">
|
||||
<div class="info-banner__title">취소 제한 안내</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
|
||||
@ -262,7 +244,6 @@
|
||||
</div>
|
||||
|
||||
<div class="issue-picker" id="issuePicker">
|
||||
{{-- 옵션 1 --}}
|
||||
@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>
|
||||
@ -276,15 +257,17 @@
|
||||
<p class="issue-option__detail-text">
|
||||
핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요.
|
||||
</p>
|
||||
<button id="btnIssueView" type="button" class="issue-run issue-run--dark">
|
||||
<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
|
||||
|
||||
{{-- 옵션 2 --}}
|
||||
@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>
|
||||
@ -297,7 +280,6 @@
|
||||
<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 발송 실행
|
||||
@ -307,7 +289,6 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 옵션 3 --}}
|
||||
@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>
|
||||
@ -321,7 +302,6 @@
|
||||
<p class="issue-option__detail-text">
|
||||
구매하신 상품권을 판매합니다. 계좌번호가 등록되어 있어야 하며,
|
||||
매입 처리 후 회원님 계좌로 입금됩니다.
|
||||
|
||||
</p>
|
||||
<button id="btnIssueSell" type="button" class="issue-run issue-run--green">
|
||||
구매상품권 판매 실행
|
||||
@ -343,77 +323,154 @@
|
||||
</div>
|
||||
|
||||
@if(!$isCancelledAfterPaid)
|
||||
|
||||
{{-- 핀 목록 --}}
|
||||
@if($showPinsNow)
|
||||
<div class="usage-card">
|
||||
<div class="section-head">
|
||||
<h3 class="card-title">핀 목록</h3>
|
||||
<div class="sub muted">
|
||||
핀 발행이 완료되면 이 영역에서 핀 정보를 확인할 수 있습니다. (현재는 UI 확인을 위해 표시 중)
|
||||
@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
|
||||
<ul class="pins">
|
||||
@foreach($pins as $p)
|
||||
<div class="gift-list">
|
||||
@foreach($pins as $idx => $p)
|
||||
@php
|
||||
$id = (int)($p['id'] ?? 0);
|
||||
$status = (string)($p['status'] ?? '');
|
||||
$raw = (string)($p['pin'] ?? $p['pin_code'] ?? $p['pin_no'] ?? '');
|
||||
$masked = (string)($p['pin_masked'] ?? $p['pin_mask'] ?? '');
|
||||
if ($masked === '' && $raw !== '') {
|
||||
$digits = preg_replace('/\s+/', '', $raw);
|
||||
$masked = (mb_strlen($digits) >= 8)
|
||||
? (mb_substr($digits,0,4).str_repeat('*',4).mb_substr($digits,-2))
|
||||
: '****';
|
||||
}
|
||||
// 오픈 전에는 마스킹 우선
|
||||
$display = $pinsOpened ? ($raw ?: $masked) : ($masked ?: '****');
|
||||
$raw = (string)($p['pin'] ?? '');
|
||||
$masked = (string)($p['pin_mask'] ?? '****');
|
||||
$display = $pinsRevealed ? ($raw ?: $masked) : $masked;
|
||||
$amount = number_format((int)($p['face_value'] ?? 0));
|
||||
@endphp
|
||||
<li class="pin-row">
|
||||
<span class="pill">#{{ $id }}</span>
|
||||
@if($status !== '') <span class="pill pill--muted">{{ $status }}</span> @endif
|
||||
<span class="mono pin-code">{{ $display }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 취소 버튼은 맨 아래, 작게 --}}
|
||||
<div class="usage-card cancel-box">
|
||||
<h3 class="card-title">결제 취소</h3>
|
||||
<div class="muted">
|
||||
핀을 확인/발행한 이후에는 취소가 제한될 수 있습니다.<br>
|
||||
결제 후 취소는 처리 시간이 소요될 수 있으며, 취소 결과는 본 페이지에 반영됩니다.
|
||||
<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)
|
||||
<form method="post" action="{{ route('web.mypage.usage.cancel', ['attemptId' => $attemptId]) }}" class="cancel-form">
|
||||
<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
|
||||
<input type="hidden" name="q" value="{{ request('q', '') }}">
|
||||
<input type="hidden" name="method" value="{{ request('method', '') }}">
|
||||
<input type="hidden" name="status" value="{{ request('status', '') }}">
|
||||
<input type="hidden" name="from" value="{{ request('from', '') }}">
|
||||
<input type="hidden" name="to" value="{{ request('to', '') }}">
|
||||
<input type="hidden" name="page" value="{{ request('page', '') }}">
|
||||
<input class="inp" name="reason" placeholder="취소 사유(선택)">
|
||||
|
||||
<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>
|
||||
@else
|
||||
<div class="notice-box">
|
||||
현재 상태에서는 결제 취소가 불가능합니다.
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@endif
|
||||
<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>
|
||||
@ -425,7 +482,7 @@
|
||||
.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; background:#fff;
|
||||
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;}
|
||||
@ -449,7 +506,6 @@
|
||||
.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); }
|
||||
|
||||
/* ===== Hero grid (전표 + 오른쪽 영역) ===== */
|
||||
.detail-hero-grid{
|
||||
display:grid;
|
||||
grid-template-columns:1fr;
|
||||
@ -457,8 +513,8 @@
|
||||
}
|
||||
@media (min-width: 960px){
|
||||
.detail-hero-grid{
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); /* 5:5 */
|
||||
align-items: stretch; /* 좌우 높이 자연스럽게 맞춤 */
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
.detail-hero-grid > * {
|
||||
min-width: 0;
|
||||
@ -479,7 +535,6 @@
|
||||
.right-panel--mobile-hide{display:none;}
|
||||
}
|
||||
|
||||
/* ===== Receipt (카드 영수증 느낌) ===== */
|
||||
.receipt-card{
|
||||
border:1px solid rgba(0,0,0,.08);
|
||||
border-radius:18px;
|
||||
@ -538,7 +593,6 @@
|
||||
font-size:15px; font-weight:900;
|
||||
}
|
||||
|
||||
/* ===== Right panel: banner mode ===== */
|
||||
.banner-stack{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
@ -573,7 +627,6 @@
|
||||
border-color:rgba(255,190,0,.20);
|
||||
}
|
||||
|
||||
/* ===== Cancel 상태용 세로 광고 배너 ===== */
|
||||
.promo-vertical-banner{
|
||||
border-radius:20px;
|
||||
position:relative;
|
||||
@ -664,7 +717,7 @@
|
||||
}
|
||||
|
||||
.promo-vertical-banner__footer{
|
||||
margin-top:auto; /* 아래 정렬 */
|
||||
margin-top:auto;
|
||||
border-top:1px dashed rgba(0,0,0,.10);
|
||||
padding-top:10px;
|
||||
}
|
||||
@ -680,7 +733,6 @@
|
||||
color:rgba(0,0,0,.55);
|
||||
}
|
||||
|
||||
/* ===== Right panel: issue picker (색감 + 입체감 강화) ===== */
|
||||
.issue-panel{
|
||||
border-radius:20px;
|
||||
padding:14px;
|
||||
@ -702,9 +754,6 @@
|
||||
.issue-panel__title{
|
||||
margin:0; font-size:17px; font-weight:900;
|
||||
}
|
||||
.issue-panel__desc{
|
||||
font-size:13px; color:rgba(0,0,0,.62); line-height:1.4;
|
||||
}
|
||||
|
||||
.issue-picker{
|
||||
display:flex; flex-direction:column; gap:10px;
|
||||
@ -753,7 +802,6 @@
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
|
||||
/* 카드별 컬러 포인트 */
|
||||
.issue-option[data-issue-card="view"]::before{
|
||||
background:linear-gradient(180deg, rgba(70,70,70,.85), rgba(20,20,20,.85));
|
||||
}
|
||||
@ -858,68 +906,254 @@
|
||||
border-color:rgba(0,160,60,.18);
|
||||
}
|
||||
|
||||
/* ===== Standard card / Pins / Cancel ===== */
|
||||
.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;}
|
||||
|
||||
.pins{margin:0; padding-left:0; list-style:none; display:flex; flex-direction:column; gap:8px;}
|
||||
.pin-row{display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
|
||||
.pin-code{font-weight:900; letter-spacing:0.3px;}
|
||||
|
||||
.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: '핀 확인' }
|
||||
"핀 확인(오픈) 후에도 핀번호는 기본 마스킹 상태로 유지됩니다.\n\n진행할까요?",
|
||||
{ type: 'confirm', title: '핀 오픈' }
|
||||
);
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
const form = e.currentTarget.closest('form');
|
||||
if (!form) return;
|
||||
|
||||
// requestSubmit이 있으면 native validation도 같이 탄다
|
||||
if (form.requestSubmit) form.requestSubmit();
|
||||
else form.submit();
|
||||
}
|
||||
|
||||
// ---- 결제 취소(confirm) ----
|
||||
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결제를 취소할까요?",
|
||||
"선택한 사유로 결제를 취소할까요?\n\n취소 사유: " + reason,
|
||||
{ type: 'confirm', title: '결제 취소' }
|
||||
);
|
||||
|
||||
if (!ok) return;
|
||||
|
||||
const form = e.currentTarget.closest('form');
|
||||
if (!form) return;
|
||||
|
||||
if (form.requestSubmit) form.requestSubmit();
|
||||
else form.submit();
|
||||
}
|
||||
|
||||
// ---- 준비중(3버튼 alert) ----
|
||||
async function onIssueViewSoon() {
|
||||
await showMsg(
|
||||
"준비중입니다.\n\n핀번호를 개인 암호화하여 발행합니다. 핀번호 유출에 주의하세요.",
|
||||
{ type: 'alert', title: '안내' }
|
||||
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() {
|
||||
@ -936,17 +1170,37 @@
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 바인딩 (최소 1회) ----
|
||||
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 btn1 = document.getElementById('btnIssueView');
|
||||
if (btn1) btn1.addEventListener('click', onIssueViewSoon);
|
||||
const btnPinInstant = document.getElementById('btnIssuePinInstant');
|
||||
if (btnPinInstant) btnPinInstant.addEventListener('click', onIssuePinInstant);
|
||||
|
||||
const btn2 = document.getElementById('btnIssueSms');
|
||||
if (btn2) btn2.addEventListener('click', onIssueSmsSoon);
|
||||
@ -954,7 +1208,11 @@
|
||||
const btn3 = document.getElementById('btnIssueSell');
|
||||
if (btn3) btn3.addEventListener('click', onIssueSellSoon);
|
||||
|
||||
// ---- 핀 발행 선택형 배너 (한 번에 1개만 확장) ----
|
||||
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]');
|
||||
@ -962,11 +1220,7 @@
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
<div class="m-usercard2__actions">
|
||||
@if($isLogin)
|
||||
<a class="m-btn2" href="{{ route('web.mypage.info.index') }}">내 정보</a>
|
||||
<a class="m-btn2 m-btn2--ghost" href="{{ route('web.mypage.usage.index') }}">이용내역</a>
|
||||
<a class="m-btn2 m-btn2--ghost" href="{{ route('web.mypage.usage.index') }}">구매내역</a>
|
||||
|
||||
<form action="{{ route('web.auth.logout') }}" method="post" class="m-usercard2__logout">
|
||||
@csrf
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
</a>
|
||||
|
||||
<a class="cs-quick__card" href="{{ route('web.mypage.usage.index') }}">
|
||||
<div class="cs-quick__title">이용내역</div>
|
||||
<div class="cs-quick__title">구매내역</div>
|
||||
<div class="cs-quick__desc">구매/결제/발송 진행 확인</div>
|
||||
</a>
|
||||
|
||||
|
||||
@ -66,8 +66,10 @@ Route::prefix('mypage')->name('web.mypage.')
|
||||
|
||||
Route::get('usage', [UsageController::class, 'index'])->name('usage.index');
|
||||
Route::get('usage/{attemptId}', [UsageController::class, 'show'])->whereNumber('attemptId')->name('usage.show');
|
||||
Route::post('usage/{attemptId}/issue/pin-instant', [UsageController::class, 'issuePinInstant'])->whereNumber('attemptId')->name('usage.issue.pin_instant');
|
||||
Route::post('usage/{attemptId}/open', [UsageController::class, 'openPins'])->whereNumber('attemptId')->name('usage.open');
|
||||
Route::post('usage/{attemptId}/cancel', [UsageController::class, 'cancel'])->whereNumber('attemptId')->name('usage.cancel');
|
||||
Route::post('usage/{attemptId}/reveal', [UsageController::class, 'revealPins'])->whereNumber('attemptId')->name('usage.reveal');
|
||||
|
||||
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||
Route::get('qna', [MypageQnaController::class, 'index'])->name('qna.index');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user