fix(m/orders/new): 검색조건 동작 / 마감일 범위 검색 / 마감 글자 줄바꿈 개선
- API: items/list 의 OR 조건(상시 OR 판매기간)을 외곽 괄호로 묶어 키워드 등 다른 AND 필터가 무력화되던 버그 수정 - API: saleEndDateFrom/saleEndDateTo 추가 (마감일 범위 검색) - 페이지: 면세/과세/전체 셀렉트 제거, 마감일 date range 입력 추가 - 페이지: 카드의 빨간 '마감' 글자를 text-[11px]/whitespace-nowrap 으로 축소해 모바일에서 한 줄 유지 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,8 @@ function ItemsBrowse() {
|
||||
const [onBehalfName, setOnBehalfName] = useState("");
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [taxFilter, setTaxFilter] = useState<"" | "Y" | "N">("");
|
||||
const [endFrom, setEndFrom] = useState("");
|
||||
const [endTo, setEndTo] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cart, setCart] = useState<CartLine[]>([]);
|
||||
const [extras, setExtras] = useState<ExtraLine[]>([]);
|
||||
@@ -112,15 +113,16 @@ function ItemsBrowse() {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
keyword,
|
||||
isTaxFree: taxFilter || undefined,
|
||||
keyword: keyword.trim() || undefined,
|
||||
saleEndDateFrom: endFrom || undefined,
|
||||
saleEndDateTo: endTo || undefined,
|
||||
forSale: true,
|
||||
}),
|
||||
});
|
||||
const j = await res.json();
|
||||
setItems(j.RESULTLIST ?? []);
|
||||
setLoading(false);
|
||||
}, [keyword, taxFilter]);
|
||||
}, [keyword, endFrom, endTo]);
|
||||
|
||||
// 검색조건 변경 시 즉시 자동 조회 (디바운스 250ms)
|
||||
useEffect(() => {
|
||||
@@ -578,8 +580,8 @@ function ItemsBrowse() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<div className="relative flex-1 min-w-[160px]">
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
value={keyword}
|
||||
@@ -589,27 +591,50 @@ function ItemsBrowse() {
|
||||
className="w-full h-10 pl-9 pr-3 rounded-lg border border-slate-200 text-sm focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/15 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<select value={taxFilter} onChange={(e) => setTaxFilter(e.target.value as "" | "Y" | "N")} className="h-10 px-2 sm:px-3 rounded-lg border border-slate-200 text-sm">
|
||||
<option value="">전체</option>
|
||||
<option value="Y">면세</option>
|
||||
<option value="N">과세</option>
|
||||
</select>
|
||||
{/* 보기 모드 토글 */}
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-0.5 ml-auto">
|
||||
<button
|
||||
onClick={() => setViewMode("card")}
|
||||
title="상세보기"
|
||||
className={`h-9 px-2.5 rounded-md text-xs font-bold inline-flex items-center gap-1 ${viewMode === "card" ? "bg-white text-emerald-700 shadow" : "text-slate-500"}`}
|
||||
>
|
||||
<LayoutGrid size={14} /> 상세보기
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("list")}
|
||||
title="간단히보기"
|
||||
className={`h-9 px-2.5 rounded-md text-xs font-bold inline-flex items-center gap-1 ${viewMode === "list" ? "bg-white text-emerald-700 shadow" : "text-slate-500"}`}
|
||||
>
|
||||
<ListIcon size={14} /> 간단히보기
|
||||
</button>
|
||||
<div className="flex gap-2 items-center flex-wrap">
|
||||
<div className="flex items-center gap-1 text-xs text-slate-600">
|
||||
<span className="font-semibold whitespace-nowrap">마감일</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endFrom}
|
||||
onChange={(e) => setEndFrom(e.target.value)}
|
||||
className="h-9 px-2 rounded-md border border-slate-200 text-xs"
|
||||
/>
|
||||
<span className="text-slate-400">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endTo}
|
||||
onChange={(e) => setEndTo(e.target.value)}
|
||||
className="h-9 px-2 rounded-md border border-slate-200 text-xs"
|
||||
/>
|
||||
{(endFrom || endTo) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEndFrom(""); setEndTo(""); }}
|
||||
className="h-9 w-7 inline-flex items-center justify-center text-slate-400 hover:text-rose-500"
|
||||
title="마감일 검색 초기화"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* 보기 모드 토글 */}
|
||||
<div className="flex items-center bg-slate-100 rounded-lg p-0.5 ml-auto">
|
||||
<button
|
||||
onClick={() => setViewMode("card")}
|
||||
title="상세보기"
|
||||
className={`h-9 px-2.5 rounded-md text-xs font-bold inline-flex items-center gap-1 ${viewMode === "card" ? "bg-white text-emerald-700 shadow" : "text-slate-500"}`}
|
||||
>
|
||||
<LayoutGrid size={14} /> 상세보기
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("list")}
|
||||
title="간단히보기"
|
||||
className={`h-9 px-2.5 rounded-md text-xs font-bold inline-flex items-center gap-1 ${viewMode === "list" ? "bg-white text-emerald-700 shadow" : "text-slate-500"}`}
|
||||
>
|
||||
<ListIcon size={14} /> 간단히보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -672,7 +697,7 @@ function ItemsBrowse() {
|
||||
</div>
|
||||
</div>
|
||||
{it.SALE_END_DATE && (
|
||||
<div className={`text-sm sm:text-base mb-1 tabular-nums font-extrabold leading-tight ${closed ? "text-slate-400" : "text-rose-600"}`}>
|
||||
<div className={`text-[11px] sm:text-xs mb-1 tabular-nums font-bold leading-tight whitespace-nowrap truncate ${closed ? "text-slate-400" : "text-rose-600"}`}>
|
||||
⏰ {it.SALE_END_DATE} 마감{closed ? " (종료)" : ""}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -44,13 +44,15 @@ export async function POST(req: NextRequest) {
|
||||
await ensureColumns();
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const { keyword, isTaxFree, makerObjid, status, onlyAvailable, forSale } = body as {
|
||||
const { keyword, isTaxFree, makerObjid, status, onlyAvailable, forSale, saleEndDateFrom, saleEndDateTo } = body as {
|
||||
keyword?: string;
|
||||
isTaxFree?: "Y" | "N";
|
||||
makerObjid?: string;
|
||||
status?: string;
|
||||
onlyAvailable?: boolean;
|
||||
forSale?: boolean;
|
||||
saleEndDateFrom?: string;
|
||||
saleEndDateTo?: string;
|
||||
};
|
||||
|
||||
// admin 판정 — role/isAdmin/userType='A' 중 하나라도 해당하면 관리자
|
||||
@@ -123,25 +125,37 @@ export async function POST(req: NextRequest) {
|
||||
// 출고요청 노출 규칙(새 정책):
|
||||
// 상시(is_always_sale='Y') 이거나, 시작/종료일이 최소 하나라도 설정되고 현재 기간 안.
|
||||
// 날짜도 없고 상시도 아니면 = 미노출 (이전엔 "상시" 취급이었음).
|
||||
// 외곽 괄호 필수: OR 가 AND 보다 우선순위 낮아서 빠지면 키워드/세금 등 다른 필터가 무력화됨.
|
||||
conditions.push(
|
||||
`(
|
||||
COALESCE(I.is_always_sale,'N') = 'Y'
|
||||
)
|
||||
OR (
|
||||
(I.sale_start_date IS NOT NULL OR I.sale_end_date IS NOT NULL)
|
||||
AND ((NOW() AT TIME ZONE 'Asia/Seoul') >= I.sale_start_date OR I.sale_start_date IS NULL)
|
||||
AND (
|
||||
I.sale_end_date IS NULL
|
||||
OR (NOW() AT TIME ZONE 'Asia/Seoul') <= CASE
|
||||
WHEN I.sale_end_date = date_trunc('day', I.sale_end_date)
|
||||
THEN I.sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
|
||||
ELSE I.sale_end_date
|
||||
END
|
||||
OR (
|
||||
(I.sale_start_date IS NOT NULL OR I.sale_end_date IS NOT NULL)
|
||||
AND ((NOW() AT TIME ZONE 'Asia/Seoul') >= I.sale_start_date OR I.sale_start_date IS NULL)
|
||||
AND (
|
||||
I.sale_end_date IS NULL
|
||||
OR (NOW() AT TIME ZONE 'Asia/Seoul') <= CASE
|
||||
WHEN I.sale_end_date = date_trunc('day', I.sale_end_date)
|
||||
THEN I.sale_end_date + INTERVAL '1 day' - INTERVAL '1 second'
|
||||
ELSE I.sale_end_date
|
||||
END
|
||||
)
|
||||
)
|
||||
)`
|
||||
);
|
||||
}
|
||||
|
||||
// 마감일(SALE_END_DATE) 기준 날짜 범위 검색 (출고요청 화면용)
|
||||
// 입력은 'YYYY-MM-DD'. 종료일은 그 날 23:59:59 까지 포함.
|
||||
if (saleEndDateFrom) {
|
||||
conditions.push(`I.sale_end_date >= $${i++}::date`);
|
||||
params.push(saleEndDateFrom);
|
||||
}
|
||||
if (saleEndDateTo) {
|
||||
conditions.push(`I.sale_end_date < ($${i++}::date + INTERVAL '1 day')`);
|
||||
params.push(saleEndDateTo);
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
I.objid AS "OBJID",
|
||||
|
||||
Reference in New Issue
Block a user