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