feat(items): 일괄 상시 판매 전환 버튼 + 목록 상시/미노출 배지 강조
Deploy momo-erp / deploy (push) Successful in 1m56s

- bulk-sale-range API: alwaysSale 모드 추가 — 선택 품목들을 is_always_sale='Y'
  로 설정하면서 날짜는 모두 NULL 로 초기화.
- 품목 관리 일괄 패널: [상시 판매로 설정] 버튼 추가. 안내 문구도 갱신
  (상시=항상 노출/날짜 초기화, 해제=미노출/날짜 초기화).
- 목록 판매기간 컬럼: 상시(초록 배지)/날짜범위/미노출(빨강 배지) 3종 명확 표시.
This commit is contained in:
chpark
2026-05-29 01:10:26 +09:00
parent 8f26ed496d
commit 0ee120f628
2 changed files with 53 additions and 27 deletions
+29 -14
View File
@@ -335,12 +335,14 @@ export default function AdminItemsPage() {
{fmt(it.STOCK_QTY)}
</span>
</td>
<td className="px-3 py-2 text-center text-[11px] tabular-nums text-slate-600 whitespace-nowrap">
{it.SALE_START_DATE || it.SALE_END_DATE
? <>{it.SALE_START_DATE ?? "—"} <span className="text-slate-300">~</span> {it.SALE_END_DATE ?? "—"}</>
: it.IS_ALWAYS_SALE === "Y"
? <span className="text-emerald-700 font-bold"></span>
: <span className="text-rose-500"></span>}
<td className="px-3 py-2 text-center text-[11px] tabular-nums whitespace-nowrap">
{it.IS_ALWAYS_SALE === "Y" ? (
<span className="inline-block px-2 py-0.5 rounded bg-emerald-100 text-emerald-700 font-bold"></span>
) : (it.SALE_START_DATE || it.SALE_END_DATE) ? (
<span className="text-slate-600">{it.SALE_START_DATE ?? "—"} <span className="text-slate-300">~</span> {it.SALE_END_DATE ?? "—"}</span>
) : (
<span className="inline-block px-2 py-0.5 rounded bg-rose-100 text-rose-600 font-bold"></span>
)}
</td>
<td className="px-3 py-2 text-center">
{it.IS_HIDDEN === "Y" && (
@@ -693,12 +695,12 @@ function BulkSaleRangeBar({
const [to, setTo] = useState("");
const [busy, setBusy] = useState(false);
const apply = async (clear: boolean) => {
const apply = async (mode: "apply" | "clear" | "always") => {
if (selectedIds.size === 0) {
Swal.fire({ icon: "warning", title: "품목을 선택하세요" });
return;
}
if (!clear && !from && !to) {
if (mode === "apply" && !from && !to) {
Swal.fire({ icon: "warning", title: "시작일 또는 종료일을 입력하세요" });
return;
}
@@ -711,14 +713,18 @@ function BulkSaleRangeBar({
objids: Array.from(selectedIds),
saleStartDate: from || null,
saleEndDate: to || null,
clear,
clear: mode === "clear",
alwaysSale: mode === "always",
}),
});
const j = await res.json();
if (j.success) {
const title = mode === "clear" ? "미노출 처리됨"
: mode === "always" ? "상시 판매로 설정됨"
: "판매기간 일괄 적용 완료";
Swal.fire({
icon: "success",
title: clear ? "판매기간 해제됨" : "판매기간 일괄 적용 완료",
title,
text: `${j.count}개 품목 적용`,
timer: 1500,
showConfirmButton: false,
@@ -754,11 +760,11 @@ function BulkSaleRangeBar({
className="h-9 px-2 rounded-md border border-emerald-300 text-sm bg-white"
/>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
disabled={busy}
onClick={() => apply(false)}
onClick={() => apply("apply")}
className="h-9 px-3 rounded-md bg-emerald-700 text-white text-sm font-bold hover:bg-emerald-800 disabled:opacity-50"
>
{selectedIds.size}
@@ -766,14 +772,23 @@ function BulkSaleRangeBar({
<button
type="button"
disabled={busy}
onClick={() => apply(true)}
onClick={() => apply("always")}
className="h-9 px-3 rounded-md bg-emerald-600 text-white text-sm font-bold hover:bg-emerald-700 disabled:opacity-50"
title="선택한 품목을 상시 판매로 설정 (날짜는 모두 초기화)"
>
</button>
<button
type="button"
disabled={busy}
onClick={() => apply("clear")}
className="h-9 px-3 rounded-md bg-white border border-emerald-300 text-emerald-700 text-sm font-semibold hover:bg-emerald-100 disabled:opacity-50"
>
</button>
</div>
<div className="text-[11px] text-emerald-700/80 ml-auto">
( = )
· <b></b>= ( ) · <b></b>=( )
</div>
</div>
);
+24 -13
View File
@@ -8,26 +8,32 @@ export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const { objids, saleStartDate, saleEndDate, clear } = await req.json() as {
objids: string[]; saleStartDate?: string; saleEndDate?: string; clear?: boolean;
const { objids, saleStartDate, saleEndDate, clear, alwaysSale } = await req.json() as {
objids: string[]; saleStartDate?: string; saleEndDate?: string; clear?: boolean; alwaysSale?: boolean;
};
if (!Array.isArray(objids) || objids.length === 0) {
return NextResponse.json({ success: false, message: "대상 품목을 선택하세요." }, { status: 400 });
}
const userId = g.user.objid || g.user.userId;
const start = clear ? null : (saleStartDate || null);
const end = clear ? null : (saleEndDate || null);
// 모드:
// alwaysSale=true → 상시 판매로 전환: is_always_sale='Y', 날짜 모두 초기화
// clear=true → 미노출로 전환: is_always_sale='N', 날짜 모두 초기화
// 기본 → 날짜 적용: is_always_sale='N', 날짜 입력 그대로
const flag = alwaysSale ? "Y" : "N";
const start = (alwaysSale || clear) ? null : (saleStartDate || null);
const end = (alwaysSale || clear) ? null : (saleEndDate || null);
const placeholders = objids.map((_, i) => `$${i + 1}`).join(",");
const res = await pool.query(
`UPDATE momo_items
SET sale_start_date = $${objids.length + 1}::timestamp,
sale_end_date = $${objids.length + 2}::timestamp,
is_always_sale = $${objids.length + 4},
update_date = NOW(),
update_id = $${objids.length + 3}
WHERE objid IN (${placeholders})`,
[...objids, start, end, userId]
[...objids, start, end, userId, flag]
);
// 일괄 적용한 판매 일정이 유효(오늘/미래)한 품목이 있으면 일반 사용자에게 알림.
@@ -37,20 +43,25 @@ export async function POST(req: NextRequest) {
const sell = await pool.query<{ item_name: string; start_txt: string | null; orderable_now: boolean }>(
`SELECT item_name,
TO_CHAR(sale_start_date,'YYYY-MM-DD HH24:MI') AS start_txt,
(sale_start_date IS NULL OR (NOW() AT TIME ZONE 'Asia/Seoul') >= sale_start_date) AS orderable_now
(COALESCE(is_always_sale,'N')='Y' OR sale_start_date IS NULL OR (NOW() AT TIME ZONE 'Asia/Seoul') >= sale_start_date) AS orderable_now
FROM momo_items
WHERE objid IN (${placeholders})
AND COALESCE(is_del,'N') != 'Y'
AND UPPER(COALESCE(status,'')) = 'ACTIVE'
AND COALESCE(is_hidden,'N') != 'Y'
AND (sale_start_date IS NOT NULL OR sale_end_date IS NOT NULL)
AND (
sale_end_date IS NULL
OR (NOW() AT TIME ZONE 'Asia/Seoul') <= CASE
WHEN sale_end_date = date_trunc('day', sale_end_date)
THEN sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
ELSE sale_end_date
END
COALESCE(is_always_sale,'N') = 'Y'
OR (
(sale_start_date IS NOT NULL OR sale_end_date IS NOT NULL)
AND (
sale_end_date IS NULL
OR (NOW() AT TIME ZONE 'Asia/Seoul') <= CASE
WHEN sale_end_date = date_trunc('day', sale_end_date)
THEN sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
ELSE sale_end_date
END
)
)
)`,
objids
);