| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- @page "{id:int}"
- @model Admin.Pages.Store.Product.LimitConfigModel
- @using Domain.Entities.Store.ValueObject
- @{
- ViewData["Title"] = "상품 판매 정책";
- }
- <div class="container">
- <h3>@ViewData["Title"]</h3>
- <hr />
- <partial name="_StatusMessage" />
- <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off">
- <input type="hidden" asp-for="Input.ProductID" />
- <div class="row mb-3">
- <label class="col-sm-2 col-form-label">상품</label>
- <div class="col-sm-10 d-flex align-items-center gap-2">
- @if (!string.IsNullOrEmpty(Model.ProductThumbnail))
- {
- <img src="@Model.ProductThumbnail" alt="@Model.ProductName" style="width:128px;height:128px;object-fit:cover;border:1px solid #eee;border-radius:4px;" /><br/>
- }
- <p>[@Model.ProductID] @Model.ProductName</p>
- </div>
- </div>
- <hr />
- <div class="row mb-3">
- <label asp-for="Input.UseQuantityLimit" class="col-sm-2 col-form-label">수량 제한</label>
- <div class="col-sm-10">
- <div class="form-check">
- <input class="form-check-input" type="checkbox" asp-for="Input.UseQuantityLimit" id="useQuantityLimit" />
- <label class="form-check-label" for="useQuantityLimit">사용합니다.</label>
- </div>
- <div class="form-text text-muted">
- 회원당 누적 구매 수량을 제한합니다.
- </div>
- </div>
- </div>
- <div class="row mb-3">
- <label asp-for="Input.MaxQuantity" class="col-sm-2 col-form-label">최대 구매 수량</label>
- <div class="col-sm-10">
- <div class="input-group w-auto d-inline-flex">
- <input type="number" min="0" max="9999" step="1" maxlength="4" asp-for="Input.MaxQuantity" class="form-control w-auto" id="maxQuantity" />
- <label for="maxQuantity" class="input-group-text">개</label>
- </div>
- <div class="form-text text-muted">
- 예) 0 = 제한 없음, 1 = 1개만, 9999 = 최대 9999개까지
- </div>
- <span asp-validation-for="Input.MaxQuantity" class="text-danger"></span>
- </div>
- </div>
- <hr />
- <div class="row mb-3">
- <label asp-for="Input.UsePeriodLimit" class="col-sm-2 col-form-label">기간 제한</label>
- <div class="col-sm-10">
- <div class="form-check">
- <input class="form-check-input" type="checkbox" asp-for="Input.UsePeriodLimit" id="usePeriodLimit" />
- <label class="form-check-label" for="usePeriodLimit">사용합니다.</label>
- </div>
- <div class="form-text text-muted">
- 위 누적 수량을 일정 기간 윈도우로 제한합니다. 미사용 시 전체 기간 누적.
- </div>
- </div>
- </div>
- <div class="row mb-3">
- <label class="col-sm-2">기간 유형</label>
- <div class="col-sm-10">
- @{
- var pt = Model.Input.PeriodType;
- }
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="radio" name="Input.PeriodType" id="periodType_0" value="@((int)LimitPeriodType.None)" @(pt == LimitPeriodType.None ? "checked" : null) />
- <label class="form-check-label" for="periodType_0">사용 안함</label>
- </div>
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="radio" name="Input.PeriodType" id="periodType_1" value="@((int)LimitPeriodType.Day)" @(pt == LimitPeriodType.Day ? "checked" : null) />
- <label class="form-check-label" for="periodType_1">1일</label>
- </div>
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="radio" name="Input.PeriodType" id="periodType_2" value="@((int)LimitPeriodType.Week)" @(pt == LimitPeriodType.Week ? "checked" : null) />
- <label class="form-check-label" for="periodType_2">1주</label>
- </div>
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="radio" name="Input.PeriodType" id="periodType_3" value="@((int)LimitPeriodType.Month)" @(pt == LimitPeriodType.Month ? "checked" : null) />
- <label class="form-check-label" for="periodType_3">30일</label>
- </div>
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="radio" name="Input.PeriodType" id="periodType_4" value="@((int)LimitPeriodType.Custom)" @(pt == LimitPeriodType.Custom ? "checked" : null) />
- <label class="form-check-label" for="periodType_4">시간 지정</label>
- </div>
- <div id="periodCustomBox" class="mt-2" hidden>
- <div class="row g-2 align-items-end">
- <div class="col-md-5">
- <label asp-for="Input.PeriodStartAt" class="form-label">시작 시각</label>
- <input asp-for="Input.PeriodStartAt" class="form-control" type="datetime-local" step="1" id="periodStartAt" />
- <span asp-validation-for="Input.PeriodStartAt" class="text-danger"></span>
- </div>
- <div class="col-md-5">
- <label asp-for="Input.PeriodEndAt" class="form-label">종료 시각</label>
- <input asp-for="Input.PeriodEndAt" class="form-control" type="datetime-local" step="1" id="periodEndAt" />
- <span asp-validation-for="Input.PeriodEndAt" class="text-danger"></span>
- </div>
- </div>
- <div class="form-text">
- 예) 시작 2026-05-01 00:00:00 ~ 종료 2026-05-31 23:59:59 (KST 입력, UTC 저장)
- </div>
- <div id="periodRangeError" class="text-danger small mt-1" hidden>종료 시각은 시작 시각보다 이후여야 합니다.</div>
- </div>
- <div id="windowPreviewBox" class="alert alert-info mt-3 mb-0 py-2 px-3 small" role="status">
- <div class="fw-bold mb-1">
- <i class="fa-solid fa-clock"></i>미리보기 <small class="text-muted ms-1">(현재 KST 기준)</small>
- </div>
- <div id="windowPreviewText" class="font-monospace">-</div>
- </div>
- </div>
- </div>
- <hr />
- <div class="d-grid gap-2 text-center d-md-block">
- <button type="submit" class="btn btn-success" id="btnSubmit">저장</button>
- <a href="/Store/Product/Edit/@Model.ProductID@(Model.QueryString)" class="btn btn-secondary">상품 수정으로</a>
- <a href="/Store/Product" class="btn btn-light">상품 목록</a>
- </div>
- <br />
- </form>
- </div>
- @section Scripts {
- <partial name="_ValidationScriptsPartial" />
- <script>
- (function () {
- var previewText = document.getElementById('windowPreviewText');
- var startInput = document.getElementById('periodStartAt');
- var endInput = document.getElementById('periodEndAt');
- var usePeriodLimit = document.getElementById('usePeriodLimit');
- var rangeError = document.getElementById('periodRangeError');
- var btnSubmit = document.getElementById('btnSubmit');
- var form = document.getElementById('fAdminWrite');
- function pad(n) { return String(n).padStart(2, '0'); }
- function formatKst(date) {
- return date.getFullYear() + '-' +
- pad(date.getMonth() + 1) + '-' +
- pad(date.getDate()) + ' ' +
- pad(date.getHours()) + ':' +
- pad(date.getMinutes()) + ':' +
- pad(date.getSeconds());
- }
- function parseLocal(value) {
- if (!value) {
- return null;
- }
- // datetime-local 의 value 는 "YYYY-MM-DDTHH:mm" 또는 "YYYY-MM-DDTHH:mm:ss" — 로컬 시각으로 해석.
- var d = new Date(value);
- return isNaN(d.getTime()) ? null : d;
- }
- function formatDurationLabel(ms) {
- if (ms <= 0) {
- return '0초';
- }
- var totalSec = Math.floor(ms / 1000);
- var days = Math.floor(totalSec / 86400);
- var hours = Math.floor((totalSec % 86400) / 3600);
- var minutes = Math.floor((totalSec % 3600) / 60);
- var seconds = totalSec % 60;
- var parts = [];
- if (days > 0) { parts.push(days + '일'); }
- if (hours > 0) { parts.push(hours + '시간'); }
- if (minutes > 0) { parts.push(minutes + '분'); }
- if (seconds > 0) { parts.push(seconds + '초'); }
- return parts.length > 0 ? parts.join(' ') : '0초';
- }
- function togglePeriodCustomBox() {
- var val = document.querySelector('input[name="Input.PeriodType"]:checked');
- if (val && val.value === '4') {
- document.getElementById('periodCustomBox').removeAttribute('hidden');
- } else {
- document.getElementById('periodCustomBox').setAttribute('hidden', 'true');
- }
- }
- // PeriodType 라디오 → UsePeriodLimit 체크박스 자동 sync
- function syncFromPeriodType() {
- var checked = document.querySelector('input[name="Input.PeriodType"]:checked');
- if (!checked || !usePeriodLimit) {
- return;
- }
- if (checked.value === '0') {
- usePeriodLimit.checked = false;
- } else {
- usePeriodLimit.checked = true;
- }
- }
- // UsePeriodLimit 체크박스 → PeriodType 라디오 자동 sync
- function syncFromUsePeriodLimit() {
- if (!usePeriodLimit) {
- return;
- }
- var checked = document.querySelector('input[name="Input.PeriodType"]:checked');
- var currentValue = checked ? checked.value : '0';
- if (!usePeriodLimit.checked) {
- var noneRadio = document.getElementById('periodType_0');
- if (noneRadio) {
- noneRadio.checked = true;
- }
- } else if (currentValue === '0') {
- var dayRadio = document.getElementById('periodType_1');
- if (dayRadio) {
- dayRadio.checked = true;
- }
- }
- }
- function validateCustomRange() {
- var checked = document.querySelector('input[name="Input.PeriodType"]:checked');
- var isCustom = checked && checked.value === '4';
- var useLimit = usePeriodLimit && usePeriodLimit.checked;
- // Custom + 기간 제한 ON 일 때만 검증
- if (!isCustom || !useLimit) {
- rangeError.setAttribute('hidden', 'true');
- if (btnSubmit) { btnSubmit.disabled = false; }
- return true;
- }
- var s = parseLocal(startInput && startInput.value);
- var e = parseLocal(endInput && endInput.value);
- if (s && e && e.getTime() <= s.getTime()) {
- rangeError.removeAttribute('hidden');
- if (btnSubmit) { btnSubmit.disabled = true; }
- return false;
- }
- rangeError.setAttribute('hidden', 'true');
- if (btnSubmit) { btnSubmit.disabled = false; }
- return true;
- }
- function updateWindowPreview() {
- var checked = document.querySelector('input[name="Input.PeriodType"]:checked');
- var typeVal = checked ? checked.value : '0';
- // 기간 제한 자체가 꺼져 있으면 안내
- if (usePeriodLimit && !usePeriodLimit.checked) {
- previewText.innerHTML = '<span class="text-secondary">기간 제한 미사용 — 전체 누적(all-time) 기준으로 계산됩니다.</span>';
- return;
- }
- var hours = 0;
- switch (typeVal) {
- case '0': // None
- previewText.innerHTML = '<span class="text-secondary">기간 유형 "사용 안함" — 전체 누적(all-time) 기준으로 계산됩니다.</span>';
- return;
- case '1': hours = 24; break;
- case '2': hours = 168; break;
- case '3': hours = 720; break;
- case '4': {
- var s = parseLocal(startInput && startInput.value);
- var e = parseLocal(endInput && endInput.value);
- if (!s || !e) {
- previewText.innerHTML = '<span class="text-danger">시작/종료 시각을 모두 입력하세요.</span>';
- return;
- }
- if (e.getTime() <= s.getTime()) {
- previewText.innerHTML = '<span class="text-danger">종료 시각은 시작 시각보다 이후여야 합니다.</span>';
- return;
- }
- var durMs = e.getTime() - s.getTime();
- previewText.innerHTML =
- '<strong>' + formatKst(s) + '</strong>' +
- ' ~ <strong>' + formatKst(e) + '</strong>' +
- ' <span class="text-muted ms-1">(' + formatDurationLabel(durMs) + ' 윈도우)</span>';
- return;
- }
- default:
- previewText.textContent = '-';
- return;
- }
- // rolling 유형 (Day/Week/Month)
- var now = new Date();
- var end = new Date(now.getTime() + hours * 60 * 60 * 1000);
- previewText.innerHTML =
- '<strong>' + formatKst(now) + '</strong>' +
- ' ~ <strong>' + formatKst(end) + '</strong>' +
- ' <span class="text-muted ms-1">(' + hours + '시간 윈도우)</span>';
- }
- function refresh() {
- togglePeriodCustomBox();
- validateCustomRange();
- updateWindowPreview();
- }
- // 초기 반영
- refresh();
- // 라디오 변경 시 — 체크박스 자동 sync 후 refresh
- document.querySelectorAll('input[name="Input.PeriodType"]').forEach(function (el) {
- el.addEventListener('change', function () {
- syncFromPeriodType();
- refresh();
- });
- });
- // datetime 입력 변경 시 반영
- if (startInput) {
- startInput.addEventListener('input', refresh);
- startInput.addEventListener('change', refresh);
- }
- if (endInput) {
- endInput.addEventListener('input', refresh);
- endInput.addEventListener('change', refresh);
- }
- // UsePeriodLimit 체크 변경 시 — 라디오 자동 sync 후 refresh
- if (usePeriodLimit) {
- usePeriodLimit.addEventListener('change', function () {
- syncFromUsePeriodLimit();
- refresh();
- });
- }
- // 제출 직전 최종 검증 + sync 안전망
- if (form) {
- form.addEventListener('submit', function (ev) {
- // PeriodType != None 이면 UsePeriodLimit 강제 ON (서버 안전망과 동일 동작)
- syncFromPeriodType();
- if (!validateCustomRange()) {
- ev.preventDefault();
- alert('종료 시각은 시작 시각보다 이후여야 합니다.');
- return false;
- }
- });
- }
- })();
- </script>
- }
|