- 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)}
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -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,13 +43,16 @@ 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 (
|
||||
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
|
||||
@@ -51,6 +60,8 @@ export async function POST(req: NextRequest) {
|
||||
THEN sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
|
||||
ELSE sale_end_date
|
||||
END
|
||||
)
|
||||
)
|
||||
)`,
|
||||
objids
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user