| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- @page "{id:int}"
- @model Admin.Pages.Payments.Toss.Confirm.ViewModel
- @using Domain.Entities.Payments.ValueObject
- @using SharedKernel.Extensions
- @{
- ViewData["Title"] = "토스 결제 상세";
- string StatusName(string? s) => s switch
- {
- "DONE" => "결제 완료",
- "WAITING_FOR_DEPOSIT" => "입금 대기",
- "CANCELED" => "취소",
- "PARTIAL_CANCELED" => "부분 취소",
- "ABORTED" => "승인 실패",
- "EXPIRED" => "만료",
- null => "-",
- _ => s
- };
- }
- <div class="container">
- <h3>@ViewData["Title"]</h3>
- <hr />
- <partial name="_StatusMessage" />
- @if (Model.Data is null)
- {
- <div class="alert alert-danger">결제 내역을 불러올 수 없습니다.</div>
- }
- else
- {
- var confirm = Model.Data.Confirm;
- var cancel = Model.Data.Cancel;
- var isVAccount = confirm.Status == "WAITING_FOR_DEPOSIT" || !string.IsNullOrEmpty(confirm.VirtualAccountNumber);
- <h5 class="mb-3">토스 결제 요청</h5>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">PK</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.ID</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">회원</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">[@confirm.MemberID] @(confirm.MemberName ?? "-") / @confirm.MemberEmail</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">결제 키</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.PaymentKey</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">주문번호</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.OrderID</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">요청 금액</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.Amount.ToString("N0") 원</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">가맹점 구분</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.MerchantID</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">상품명</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(string.IsNullOrEmpty(confirm.OrderName) ? "-" : confirm.OrderName)</div>
- </div>
- </div>
- <hr />
- <h5 class="mb-3">토스 결제 응답</h5>
- @if (string.IsNullOrEmpty(confirm.ResponseCode))
- {
- <div class="alert alert-warning">토스 승인 요청 후 최종 응답이 기록되지 않았습니다. (통신 불명 — 자동 대사 대상)</div>
- }
- else
- {
- @if (confirm.ResponseCode != "SUCCESS")
- {
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">에러 코드</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext text-danger">[@confirm.ResponseCode] @confirm.ResponseMessage</div>
- </div>
- </div>
- }
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">결제 상태</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@StatusName(confirm.Status) (@(confirm.Status ?? "-"))</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">결제 수단</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.Method ?? "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">최종 승인 금액</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.TotalAmount.HasValue ? confirm.TotalAmount.Value.ToString("N0") + " 원" : "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">승인 일시</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.ApprovedAt is not null ? confirm.ApprovedAt.GetDateAt() : "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">영수증</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">
- @if (string.IsNullOrEmpty(confirm.ReceiptUrl))
- {
- <text>-</text>
- }
- else
- {
- <a href="@confirm.ReceiptUrl" target="_blank" rel="noopener">영수증 보기</a>
- }
- </div>
- </div>
- </div>
- @if (isVAccount)
- {
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">가상계좌 번호</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.VirtualAccountNumber ?? "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">입금 은행</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.VirtualAccountBank ?? "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">예금주</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.VirtualAccountHolder ?? "-")</div>
- </div>
- </div>
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">입금 마감</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@(confirm.VirtualAccountExpireAt is not null ? confirm.VirtualAccountExpireAt.GetDateAt() : "-")</div>
- </div>
- </div>
- }
- @if (confirm.UpdatedAt is not null)
- {
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">수정 일시</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.UpdatedAt.GetDateAt()</div>
- </div>
- </div>
- }
- <div class="row mb-2">
- <label class="col-sm-2 col-form-label">등록 일시</label>
- <div class="col-sm-10">
- <div class="form-control-plaintext">@confirm.CreatedAt.GetDateAt()</div>
- </div>
- </div>
- }
- <hr />
- <h5 class="mb-3">토스 결제 취소</h5>
- @if (cancel is not null)
- {
- <div class="alert alert-success" role="alert">
- 이미 토스 결제 취소가 완료되었습니다. (@cancel.CancelAmount.ToString("N0") 원 — 요청자: @(cancel.CancelRequester ?? "-"), 사유: @(cancel.CancelReason ?? "-"))
- </div>
- }
- else if (confirm.OrderStatus == PaymentStatus.Paid)
- {
- var isForcedCancel = confirm.ResponseCode != "SUCCESS";
- if (isForcedCancel)
- {
- <div class="alert alert-danger" role="alert">
- <strong>주의 — 토스 응답 코드가 [@(string.IsNullOrEmpty(confirm.ResponseCode) ? "응답 미수신" : confirm.ResponseCode)] 인 강제 환불입니다.</strong><br />
- PaymentOrder.Status 는 <code>Paid</code> 이지만 TossConfirm 응답이 비정상입니다. 토스 상점관리자에서 실제 거래 상태를 사전 확인 후 진행하세요.
- </div>
- }
- <div class="alert alert-warning" role="alert">
- 토스 결제 승인 후 전액 취소가 가능합니다. (취소 금액: <u>@confirm.Amount.ToString("N0") 원</u>)<br />
- 취소 요청자는 주체를 구분하기 위해 사용합니다. 취소 사유는 토스 취소 API 의 cancelReason 으로 전송됩니다.
- </div>
- <form id="fAdminWrite" method="post" asp-page-handler="Cancel" accept-charset="utf-8" autocomplete="off" data-forced="@(isForcedCancel ? "1" : "0")">
- @Html.AntiForgeryToken()
- <div class="row mb-2">
- <label asp-for="Input.CancelRequester" class="col-sm-2 col-form-label">취소 요청자</label>
- <div class="col-sm-10">
- <input asp-for="Input.CancelRequester" class="form-control" maxlength="20" placeholder="20자 이내" required />
- <span asp-validation-for="Input.CancelRequester" class="text-danger"></span>
- </div>
- </div>
- <div class="row mb-2">
- <label asp-for="Input.CancelReason" class="col-sm-2 col-form-label">취소 사유</label>
- <div class="col-sm-10">
- <textarea asp-for="Input.CancelReason" class="form-control" placeholder="최대 200자" rows="3" maxlength="200" required></textarea>
- <span asp-validation-for="Input.CancelReason" class="text-danger"></span>
- </div>
- </div>
- <div class="d-grid gap-2 text-center d-md-block">
- <button type="submit" class="btn btn-danger">결제 취소 (전액 @confirm.Amount.ToString("N0") 원)</button>
- </div>
- </form>
- }
- else if (confirm.OrderStatus == PaymentStatus.Cancelled)
- {
- <div class="alert alert-info" role="alert">
- 이미 취소된 주문입니다. (PaymentOrder.Status = Cancelled)
- </div>
- }
- else if (confirm.OrderStatus == PaymentStatus.Failed)
- {
- <div class="alert alert-secondary" role="alert">
- 결제가 실패한 주문입니다. 환불할 금액이 없습니다. (PaymentOrder.Status = Failed)
- </div>
- }
- else if (confirm.OrderStatus is PaymentStatus.Pending or PaymentStatus.WaitingDeposit or PaymentStatus.NeedsReconciliation)
- {
- <div class="alert alert-warning" role="alert">
- 결제가 아직 완료되지 않았습니다. (PaymentOrder.Status = @confirm.OrderStatus) — 입금 대기 / 대사 중인 거래는 취소할 수 없습니다.
- </div>
- }
- else
- {
- <div class="alert alert-warning" role="alert">
- 승인되지 않은 결제는 취소할 수 없습니다.
- </div>
- }
- <hr />
- <div class="d-grid gap-2 text-center d-md-block">
- <a href="/Payments/Toss/Confirm@(Request.QueryString)" class="btn btn-sm btn-secondary">뒤로가기</a>
- </div>
- <br />
- <br />
- <br />
- }
- </div>
- @section Scripts {
- <script>
- $(document).on("submit", "#fAdminWrite", function (e) {
- e.preventDefault();
- var forced = $(this).data("forced") === 1 || $(this).data("forced") === "1";
- var message = forced
- ? "[강제 환불] 토스 응답 코드가 비정상이지만 PaymentOrder.Status 는 Paid 입니다.\n토스 상점관리자에서 실제 거래 상태를 확인하셨습니까?\n전액 결제 취소를 진행합니다 — 토스 API 를 실제로 호출합니다."
- : "전액 결제 취소를 진행하시겠습니까?\n토스 API를 실제로 호출합니다.";
- if (!confirm(message)) {
- return false;
- }
- this.submit();
- });
- </script>
- }
|