| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- @page "{id:int}"
- @model Admin.Pages.Store.Coupon.CodesModel
- @using Domain.Entities.Store.ValueObject
- @{
- ViewData["Title"] = "쿠폰 코드 풀";
- }
- <div class="container-fluid">
- <h3>@ViewData["Title"]</h3>
- <hr />
- <partial name="_StatusMessage" />
- <div class="card mb-3">
- <div class="card-body">
- <dl class="row mb-0">
- <dt class="col-sm-2">쿠폰명</dt>
- <dd class="col-sm-10"><strong>@Model.CouponName</strong></dd>
- <dt class="col-sm-2">게임명</dt>
- <dd class="col-sm-10">@Model.GameName</dd>
- <dt class="col-sm-2">상품명</dt>
- <dd class="col-sm-10 mb-0">@Model.ProductName</dd>
- </dl>
- </div>
- </div>
- <div class="row mb-3 g-2">
- <div class="col">
- <div class="card text-center">
- <div class="card-body">
- <h6 class="text-muted mb-1">사용 가능</h6>
- <h3 class="@(Model.AvailableCount < 10 ? "text-danger" : "text-success")">@Model.AvailableCount</h3>
- </div>
- </div>
- </div>
- <div class="col">
- <div class="card text-center">
- <div class="card-body">
- <h6 class="text-muted mb-1">결제 진행 중</h6>
- <h3 class="text-warning">@Model.ReservedCount</h3>
- </div>
- </div>
- </div>
- <div class="col">
- <div class="card text-center">
- <div class="card-body">
- <h6 class="text-muted mb-1">발급됨</h6>
- <h3 class="text-primary">@Model.IssuedCount</h3>
- </div>
- </div>
- </div>
- <div class="col">
- <div class="card text-center">
- <div class="card-body">
- <h6 class="text-muted mb-1">사용됨</h6>
- <h3 class="text-info">@Model.UsedCount</h3>
- </div>
- </div>
- </div>
- <div class="col">
- <div class="card text-center">
- <div class="card-body">
- <h6 class="text-muted mb-1">만료</h6>
- <h3 class="text-danger">@Model.ExpiredCount</h3>
- </div>
- </div>
- </div>
- </div>
- <div class="card mb-3">
- <div class="card-header d-flex justify-content-between align-items-center">
- <strong>CSV Import / Export</strong>
- <div class="btn-group btn-group-sm">
- <a class="btn btn-outline-secondary" asp-page-handler="Template">
- <i class="bi bi-file-earmark-text"></i> 양식 다운로드
- </a>
- <a class="btn btn-outline-success" asp-page-handler="Export" asp-route-id="@Model.ID">
- <i class="bi bi-download"></i> 전체 다운로드
- </a>
- </div>
- </div>
- <div class="card-body">
- <form method="post" enctype="multipart/form-data" asp-page-handler="Upload" accept-charset="utf-8">
- @Html.AntiForgeryToken()
- <div class="row g-2 align-items-center">
- <div class="col">
- <input asp-for="Upload.CsvFile" type="file" class="form-control" accept=".csv,.txt,text/csv,text/plain,application/csv,application/vnd.ms-excel" />
- </div>
- <div class="col-auto">
- <button type="submit" class="btn btn-primary">Upload</button>
- </div>
- </div>
- <span asp-validation-for="Upload.CsvFile" class="text-danger"></span>
- </form>
- <p class="small text-muted mt-3 mb-1">
- CSV 양식 (UTF-8, 헤더 1행 + 데이터 N행, 최대 10MB):
- </p>
- <p class="small text-muted mb-1">
- <code>ID, Code, Status, IssuedToMemberID, IssuedToMemberName, IssuedToMemberEmail, IssuedAt, UsedAt, CreatedAt</code>
- </p>
- <ul class="small text-muted mb-0 pb-0">
- <li><strong>Status</strong>: <code>0</code> = 미사용(신규 입고) / <code>1</code> = 사용됨</li>
- <li>Status=1 인 경우 <code>IssuedToMemberID</code>, <code>IssuedAt</code>, <code>UsedAt</code> 필수. 회원 ID 는 DB에 실존해야 함 (없으면 행 skip)</li>
- <li><code>ID</code>, <code>IssuedToMemberName</code>/<code>Email</code>, <code>CreatedAt</code> 는 참고용 — Import 시 무시</li>
- <li>일시 형식: <code>yyyy-MM-dd HH:mm:ss</code> (KST)</li>
- <li>이미 등록된 코드는 자동 skip</li>
- </ul>
- </div>
- </div>
- <div class="row g-2 mb-2">
- <div class="col-auto">
- <select name="Query.Status" id="statusFilter" class="form-select w-auto" form="fAdminSearch">
- <option value="" selected="@(!Model.Query.Status.HasValue)">전체 상태 (@(Model.AvailableCount + Model.ReservedCount + Model.IssuedCount + Model.UsedCount + Model.ExpiredCount))</option>
- <option value="@((int)CouponCodeStatus.Available)" selected="@(Model.Query.Status == CouponCodeStatus.Available)">사용 가능 (@Model.AvailableCount)</option>
- <option value="@((int)CouponCodeStatus.Reserved)" selected="@(Model.Query.Status == CouponCodeStatus.Reserved)">결제 진행 중 (@Model.ReservedCount)</option>
- <option value="@((int)CouponCodeStatus.Issued)" selected="@(Model.Query.Status == CouponCodeStatus.Issued)">발급됨 (@Model.IssuedCount)</option>
- <option value="@((int)CouponCodeStatus.Used)" selected="@(Model.Query.Status == CouponCodeStatus.Used)">사용됨 (@Model.UsedCount)</option>
- <option value="@((int)CouponCodeStatus.Expired)" selected="@(Model.Query.Status == CouponCodeStatus.Expired)">만료 (@Model.ExpiredCount)</option>
- </select>
- </div>
- <div class="col-auto">
- <select name="Query.PerPage" id="perPage" class="form-select w-auto" form="fAdminSearch">
- <option value="20" selected="@(Model.Query.PerPage == 20)">20</option>
- <option value="50" selected="@(Model.Query.PerPage == 50)">50</option>
- <option value="100" selected="@(Model.Query.PerPage == 100)">100</option>
- <option value="200" selected="@(Model.Query.PerPage == 200)">200</option>
- </select>
- </div>
- <div class="col align-self-center">
- <span class="text-muted">결과: <strong>@Model.Total</strong>개</span>
- </div>
- <div class="col-auto">
- <button type="button" id="btnRestoreCodes" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#restoreCodesModal" disabled>선택 복구</button>
- <button type="button" id="btnExpireCodes" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#expireCodesModal" disabled>선택 만료</button>
- <button type="button" id="btnDeleteCodes" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteCodesModal" disabled>선택 삭제</button>
- <a class="btn btn-secondary" asp-page="/Store/Coupon/Index">목록</a>
- </div>
- </div>
- <form id="fDeleteCodes" method="post" asp-page-handler="DeleteCodes" asp-route-id="@Model.ID" accept-charset="utf-8">
- @Html.AntiForgeryToken()
- </form>
- <form id="fExpireCodes" method="post" asp-page-handler="ExpireCodes" asp-route-id="@Model.ID" accept-charset="utf-8">
- @Html.AntiForgeryToken()
- </form>
- <form id="fRestoreCodes" method="post" asp-page-handler="RestoreCodes" asp-route-id="@Model.ID" accept-charset="utf-8">
- @Html.AntiForgeryToken()
- </form>
- <div class="table-responsive">
- <table class="table table-striped table-bordered table-hover">
- <thead>
- <tr>
- <th style="width:3%">
- <input type="checkbox" id="checkedAll" class="form-check-input" value="1" title="현재 페이지의 선택 가능한 코드 전체 선택 (사용 가능 / 발급됨 / 만료)" />
- </th>
- <th style="width:4%">번호</th>
- <th>코드</th>
- <th style="width:8%">상태</th>
- <th style="width:7%">회원ID</th>
- <th style="width:10%">회원 별명</th>
- <th>이메일</th>
- <th style="width:12%">발급일</th>
- <th style="width:12%">사용일</th>
- <th style="width:10%">등록일</th>
- </tr>
- </thead>
- <tbody>
- @if (Model.List == null || Model.List.Count <= 0)
- {
- <tr><td colspan="10">등록된 코드가 없습니다. 위에서 CSV Import 해주세요.</td></tr>
- }
- else
- {
- @foreach (var row in Model.List)
- {
- var rowClass = row.StatusText switch
- {
- "사용 가능" => "table-success",
- "결제 진행 중" => "table-warning",
- "발급됨" => "table-info",
- "사용됨" => "table-secondary",
- "만료" => "table-danger",
- _ => ""
- };
- // 체크박스 활성 조건: 액션(삭제/만료/복귀) 대상이 될 수 있는 상태만. Reserved/Used 는 제외.
- var selectable = row.StatusText is "사용 가능" or "발급됨" or "만료";
- var statusKey = row.StatusText switch
- {
- "사용 가능" => "available",
- "발급됨" => "issued",
- "만료" => "expired",
- _ => ""
- };
- <tr class="@rowClass">
- <td class="justify-self-center">
- @if (selectable)
- {
- <input type="checkbox" name="codeIDs" class="form-check-input list-code-check" value="@row.ID" data-status="@statusKey" />
- }
- else
- {
- <input type="checkbox" class="form-check-input" disabled title="결제 진행 중·사용됨 상태는 선택할 수 없습니다" />
- }
- </td>
- <td>@row.Num</td>
- <td><code>@row.Code</code></td>
- <td>@row.StatusText</td>
- <td>@(row.IssuedToMemberID?.ToString() ?? "-")</td>
- <td>@(row.IssuedToMemberName ?? "-")</td>
- <td>@(row.IssuedToMemberEmail ?? "-")</td>
- <td>@(row.IssuedAt ?? "-")</td>
- <td>@(row.UsedAt ?? "-")</td>
- <td>@row.CreatedAt</td>
- </tr>
- }
- }
- </tbody>
- </table>
- <partial name="_Pagination" model="@Model.Pagination" />
- </div>
- </div>
- <form id="fAdminSearch" method="get" accept-charset="utf-8">
- <input type="hidden" name="Query.PageNum" value="@Model.Query.PageNum" />
- </form>
- <div class="modal fade" id="deleteCodesModal" tabindex="-1">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">쿠폰 코드 삭제</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
- </div>
- <div class="modal-body">
- <p>선택한 <strong id="deleteCodesCount">0</strong>개 코드를 삭제합니다.</p>
- <p class="text-muted small mb-0">사용 가능(Available) 상태의 코드만 삭제됩니다. 결제 진행 중·발급됨·사용됨·만료 상태는 정합성 보호를 위해 삭제할 수 없습니다.</p>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
- <button type="button" class="btn btn-danger" id="btnDeleteCodesConfirm">삭제</button>
- </div>
- </div>
- </div>
- </div>
- <div class="modal fade" id="expireCodesModal" tabindex="-1">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">쿠폰 코드 만료 처리</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
- </div>
- <div class="modal-body">
- <p>선택한 <strong id="expireCodesCount">0</strong>개 코드를 <strong>만료</strong> 상태로 변경합니다.</p>
- <p class="text-muted small mb-0">
- 사용 가능(Available) / 발급됨(Issued) 상태만 만료 처리됩니다. 발급됨 코드를 만료하면 해당 회원의 보관함에서 즉시 만료 표시되며 사용할 수 없게 됩니다.
- 필요 시 [선택 복귀] 로 되돌릴 수 있습니다.
- </p>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
- <button type="button" class="btn btn-warning" id="btnExpireCodesConfirm">만료 처리</button>
- </div>
- </div>
- </div>
- </div>
- <div class="modal fade" id="restoreCodesModal" tabindex="-1">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">쿠폰 코드 복귀</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
- </div>
- <div class="modal-body">
- <p>선택한 <strong id="restoreCodesCount">0</strong>개 만료 코드를 복귀합니다.</p>
- <p class="text-muted small mb-0">
- 만료(Expired) 상태 코드만 복귀할 수 있습니다. 회원에게 발급되었던 코드는 발급됨(Issued) 으로, 그 외는 사용 가능(Available) 으로 복귀하며 해당 회원의 보관함에서 다시 사용 가능해집니다.
- </p>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
- <button type="button" class="btn btn-success" id="btnRestoreCodesConfirm">복귀</button>
- </div>
- </div>
- </div>
- </div>
- @section Scripts {
- <script>
- let f = document.getElementById("fAdminSearch");
- $(document).on("change", "#statusFilter, #perPage", function() { f.submit(); });
- // 체크박스 / 액션 버튼 동기화
- const $checks = () => document.querySelectorAll(".list-code-check");
- const btnDelete = document.getElementById("btnDeleteCodes");
- const btnExpire = document.getElementById("btnExpireCodes");
- const btnRestore = document.getElementById("btnRestoreCodes");
- const checkedAll = document.getElementById("checkedAll");
- function updateActionButtons() {
- const checked = document.querySelectorAll(".list-code-check:checked");
- const count = checked.length;
- const statuses = Array.from(checked).map(c => c.getAttribute("data-status"));
- const onlyAvailable = count > 0 && statuses.every(s => s === "available");
- const expirable = count > 0 && statuses.every(s => s === "available" || s === "issued");
- const onlyExpired = count > 0 && statuses.every(s => s === "expired");
- btnDelete.disabled = !onlyAvailable;
- btnExpire.disabled = !expirable;
- btnRestore.disabled = !onlyExpired;
- const setCount = (id) => {
- const el = document.getElementById(id);
- if (el) el.textContent = count;
- };
- setCount("deleteCodesCount");
- setCount("expireCodesCount");
- setCount("restoreCodesCount");
- }
- $(document).on("change", ".list-code-check", updateActionButtons);
- $(document).on("change", "#checkedAll", function() {
- $checks().forEach(c => c.checked = checkedAll.checked);
- updateActionButtons();
- });
- function submitForm(formID) {
- const form = document.getElementById(formID);
- // 기존 동적 hidden input 정리
- form.querySelectorAll('input[name="codeIDs"]').forEach(el => el.remove());
- // 체크된 코드 ID 들을 hidden input 으로 폼에 주입
- document.querySelectorAll(".list-code-check:checked").forEach(c => {
- const h = document.createElement("input");
- h.type = "hidden";
- h.name = "codeIDs";
- h.value = c.value;
- form.appendChild(h);
- });
- form.submit();
- }
- $(document).on("click", "#btnDeleteCodesConfirm", function() { submitForm("fDeleteCodes"); });
- $(document).on("click", "#btnExpireCodesConfirm", function() { submitForm("fExpireCodes"); });
- $(document).on("click", "#btnRestoreCodesConfirm", function() { submitForm("fRestoreCodes"); });
- </script>
- }
|