View.cshtml 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. @page "{id:int}"
  2. @model Admin.Pages.Payments.Toss.Confirm.ViewModel
  3. @using Domain.Entities.Payments.ValueObject
  4. @using SharedKernel.Extensions
  5. @{
  6. ViewData["Title"] = "토스 결제 상세";
  7. string StatusName(string? s) => s switch
  8. {
  9. "DONE" => "결제 완료",
  10. "WAITING_FOR_DEPOSIT" => "입금 대기",
  11. "CANCELED" => "취소",
  12. "PARTIAL_CANCELED" => "부분 취소",
  13. "ABORTED" => "승인 실패",
  14. "EXPIRED" => "만료",
  15. null => "-",
  16. _ => s
  17. };
  18. }
  19. <div class="container">
  20. <h3>@ViewData["Title"]</h3>
  21. <hr />
  22. <partial name="_StatusMessage" />
  23. @if (Model.Data is null)
  24. {
  25. <div class="alert alert-danger">결제 내역을 불러올 수 없습니다.</div>
  26. }
  27. else
  28. {
  29. var confirm = Model.Data.Confirm;
  30. var cancel = Model.Data.Cancel;
  31. var isVAccount = confirm.Status == "WAITING_FOR_DEPOSIT" || !string.IsNullOrEmpty(confirm.VirtualAccountNumber);
  32. <h5 class="mb-3">토스 결제 요청</h5>
  33. <div class="row mb-2">
  34. <label class="col-sm-2 col-form-label">PK</label>
  35. <div class="col-sm-10">
  36. <div class="form-control-plaintext">@confirm.ID</div>
  37. </div>
  38. </div>
  39. <div class="row mb-2">
  40. <label class="col-sm-2 col-form-label">회원</label>
  41. <div class="col-sm-10">
  42. <div class="form-control-plaintext">[@confirm.MemberID] @(confirm.MemberName ?? "-") / @confirm.MemberEmail</div>
  43. </div>
  44. </div>
  45. <div class="row mb-2">
  46. <label class="col-sm-2 col-form-label">결제 키</label>
  47. <div class="col-sm-10">
  48. <div class="form-control-plaintext">@confirm.PaymentKey</div>
  49. </div>
  50. </div>
  51. <div class="row mb-2">
  52. <label class="col-sm-2 col-form-label">주문번호</label>
  53. <div class="col-sm-10">
  54. <div class="form-control-plaintext">@confirm.OrderID</div>
  55. </div>
  56. </div>
  57. <div class="row mb-2">
  58. <label class="col-sm-2 col-form-label">요청 금액</label>
  59. <div class="col-sm-10">
  60. <div class="form-control-plaintext">@confirm.Amount.ToString("N0") 원</div>
  61. </div>
  62. </div>
  63. <div class="row mb-2">
  64. <label class="col-sm-2 col-form-label">가맹점 구분</label>
  65. <div class="col-sm-10">
  66. <div class="form-control-plaintext">@confirm.MerchantID</div>
  67. </div>
  68. </div>
  69. <div class="row mb-2">
  70. <label class="col-sm-2 col-form-label">상품명</label>
  71. <div class="col-sm-10">
  72. <div class="form-control-plaintext">@(string.IsNullOrEmpty(confirm.OrderName) ? "-" : confirm.OrderName)</div>
  73. </div>
  74. </div>
  75. <hr />
  76. <h5 class="mb-3">토스 결제 응답</h5>
  77. @if (string.IsNullOrEmpty(confirm.ResponseCode))
  78. {
  79. <div class="alert alert-warning">토스 승인 요청 후 최종 응답이 기록되지 않았습니다. (통신 불명 — 자동 대사 대상)</div>
  80. }
  81. else
  82. {
  83. @if (confirm.ResponseCode != "SUCCESS")
  84. {
  85. <div class="row mb-2">
  86. <label class="col-sm-2 col-form-label">에러 코드</label>
  87. <div class="col-sm-10">
  88. <div class="form-control-plaintext text-danger">[@confirm.ResponseCode] @confirm.ResponseMessage</div>
  89. </div>
  90. </div>
  91. }
  92. <div class="row mb-2">
  93. <label class="col-sm-2 col-form-label">결제 상태</label>
  94. <div class="col-sm-10">
  95. <div class="form-control-plaintext">@StatusName(confirm.Status) (@(confirm.Status ?? "-"))</div>
  96. </div>
  97. </div>
  98. <div class="row mb-2">
  99. <label class="col-sm-2 col-form-label">결제 수단</label>
  100. <div class="col-sm-10">
  101. <div class="form-control-plaintext">@(confirm.Method ?? "-")</div>
  102. </div>
  103. </div>
  104. <div class="row mb-2">
  105. <label class="col-sm-2 col-form-label">최종 승인 금액</label>
  106. <div class="col-sm-10">
  107. <div class="form-control-plaintext">@(confirm.TotalAmount.HasValue ? confirm.TotalAmount.Value.ToString("N0") + " 원" : "-")</div>
  108. </div>
  109. </div>
  110. <div class="row mb-2">
  111. <label class="col-sm-2 col-form-label">승인 일시</label>
  112. <div class="col-sm-10">
  113. <div class="form-control-plaintext">@(confirm.ApprovedAt is not null ? confirm.ApprovedAt.GetDateAt() : "-")</div>
  114. </div>
  115. </div>
  116. <div class="row mb-2">
  117. <label class="col-sm-2 col-form-label">영수증</label>
  118. <div class="col-sm-10">
  119. <div class="form-control-plaintext">
  120. @if (string.IsNullOrEmpty(confirm.ReceiptUrl))
  121. {
  122. <text>-</text>
  123. }
  124. else
  125. {
  126. <a href="@confirm.ReceiptUrl" target="_blank" rel="noopener">영수증 보기</a>
  127. }
  128. </div>
  129. </div>
  130. </div>
  131. @if (isVAccount)
  132. {
  133. <div class="row mb-2">
  134. <label class="col-sm-2 col-form-label">가상계좌 번호</label>
  135. <div class="col-sm-10">
  136. <div class="form-control-plaintext">@(confirm.VirtualAccountNumber ?? "-")</div>
  137. </div>
  138. </div>
  139. <div class="row mb-2">
  140. <label class="col-sm-2 col-form-label">입금 은행</label>
  141. <div class="col-sm-10">
  142. <div class="form-control-plaintext">@(confirm.VirtualAccountBank ?? "-")</div>
  143. </div>
  144. </div>
  145. <div class="row mb-2">
  146. <label class="col-sm-2 col-form-label">예금주</label>
  147. <div class="col-sm-10">
  148. <div class="form-control-plaintext">@(confirm.VirtualAccountHolder ?? "-")</div>
  149. </div>
  150. </div>
  151. <div class="row mb-2">
  152. <label class="col-sm-2 col-form-label">입금 마감</label>
  153. <div class="col-sm-10">
  154. <div class="form-control-plaintext">@(confirm.VirtualAccountExpireAt is not null ? confirm.VirtualAccountExpireAt.GetDateAt() : "-")</div>
  155. </div>
  156. </div>
  157. }
  158. @if (confirm.UpdatedAt is not null)
  159. {
  160. <div class="row mb-2">
  161. <label class="col-sm-2 col-form-label">수정 일시</label>
  162. <div class="col-sm-10">
  163. <div class="form-control-plaintext">@confirm.UpdatedAt.GetDateAt()</div>
  164. </div>
  165. </div>
  166. }
  167. <div class="row mb-2">
  168. <label class="col-sm-2 col-form-label">등록 일시</label>
  169. <div class="col-sm-10">
  170. <div class="form-control-plaintext">@confirm.CreatedAt.GetDateAt()</div>
  171. </div>
  172. </div>
  173. }
  174. <hr />
  175. <h5 class="mb-3">토스 결제 취소</h5>
  176. @if (cancel is not null)
  177. {
  178. <div class="alert alert-success" role="alert">
  179. 이미 토스 결제 취소가 완료되었습니다. (@cancel.CancelAmount.ToString("N0") 원 — 요청자: @(cancel.CancelRequester ?? "-"), 사유: @(cancel.CancelReason ?? "-"))
  180. </div>
  181. }
  182. else if (confirm.OrderStatus == PaymentStatus.Paid)
  183. {
  184. var isForcedCancel = confirm.ResponseCode != "SUCCESS";
  185. if (isForcedCancel)
  186. {
  187. <div class="alert alert-danger" role="alert">
  188. <strong>주의 — 토스 응답 코드가 [@(string.IsNullOrEmpty(confirm.ResponseCode) ? "응답 미수신" : confirm.ResponseCode)] 인 강제 환불입니다.</strong><br />
  189. PaymentOrder.Status 는 <code>Paid</code> 이지만 TossConfirm 응답이 비정상입니다. 토스 상점관리자에서 실제 거래 상태를 사전 확인 후 진행하세요.
  190. </div>
  191. }
  192. <div class="alert alert-warning" role="alert">
  193. 토스 결제 승인 후 전액 취소가 가능합니다. (취소 금액: <u>@confirm.Amount.ToString("N0") 원</u>)<br />
  194. 취소 요청자는 주체를 구분하기 위해 사용합니다. 취소 사유는 토스 취소 API 의 cancelReason 으로 전송됩니다.
  195. </div>
  196. <form id="fAdminWrite" method="post" asp-page-handler="Cancel" accept-charset="utf-8" autocomplete="off" data-forced="@(isForcedCancel ? "1" : "0")">
  197. @Html.AntiForgeryToken()
  198. <div class="row mb-2">
  199. <label asp-for="Input.CancelRequester" class="col-sm-2 col-form-label">취소 요청자</label>
  200. <div class="col-sm-10">
  201. <input asp-for="Input.CancelRequester" class="form-control" maxlength="20" placeholder="20자 이내" required />
  202. <span asp-validation-for="Input.CancelRequester" class="text-danger"></span>
  203. </div>
  204. </div>
  205. <div class="row mb-2">
  206. <label asp-for="Input.CancelReason" class="col-sm-2 col-form-label">취소 사유</label>
  207. <div class="col-sm-10">
  208. <textarea asp-for="Input.CancelReason" class="form-control" placeholder="최대 200자" rows="3" maxlength="200" required></textarea>
  209. <span asp-validation-for="Input.CancelReason" class="text-danger"></span>
  210. </div>
  211. </div>
  212. <div class="d-grid gap-2 text-center d-md-block">
  213. <button type="submit" class="btn btn-danger">결제 취소 (전액 @confirm.Amount.ToString("N0") 원)</button>
  214. </div>
  215. </form>
  216. }
  217. else if (confirm.OrderStatus == PaymentStatus.Cancelled)
  218. {
  219. <div class="alert alert-info" role="alert">
  220. 이미 취소된 주문입니다. (PaymentOrder.Status = Cancelled)
  221. </div>
  222. }
  223. else if (confirm.OrderStatus == PaymentStatus.Failed)
  224. {
  225. <div class="alert alert-secondary" role="alert">
  226. 결제가 실패한 주문입니다. 환불할 금액이 없습니다. (PaymentOrder.Status = Failed)
  227. </div>
  228. }
  229. else if (confirm.OrderStatus is PaymentStatus.Pending or PaymentStatus.WaitingDeposit or PaymentStatus.NeedsReconciliation)
  230. {
  231. <div class="alert alert-warning" role="alert">
  232. 결제가 아직 완료되지 않았습니다. (PaymentOrder.Status = @confirm.OrderStatus) — 입금 대기 / 대사 중인 거래는 취소할 수 없습니다.
  233. </div>
  234. }
  235. else
  236. {
  237. <div class="alert alert-warning" role="alert">
  238. 승인되지 않은 결제는 취소할 수 없습니다.
  239. </div>
  240. }
  241. <hr />
  242. <div class="d-grid gap-2 text-center d-md-block">
  243. <a href="/Payments/Toss/Confirm@(Request.QueryString)" class="btn btn-sm btn-secondary">뒤로가기</a>
  244. </div>
  245. <br />
  246. <br />
  247. <br />
  248. }
  249. </div>
  250. @section Scripts {
  251. <script>
  252. $(document).on("submit", "#fAdminWrite", function (e) {
  253. e.preventDefault();
  254. var forced = $(this).data("forced") === 1 || $(this).data("forced") === "1";
  255. var message = forced
  256. ? "[강제 환불] 토스 응답 코드가 비정상이지만 PaymentOrder.Status 는 Paid 입니다.\n토스 상점관리자에서 실제 거래 상태를 확인하셨습니까?\n전액 결제 취소를 진행합니다 — 토스 API 를 실제로 호출합니다."
  257. : "전액 결제 취소를 진행하시겠습니까?\n토스 API를 실제로 호출합니다.";
  258. if (!confirm(message)) {
  259. return false;
  260. }
  261. this.submit();
  262. });
  263. </script>
  264. }