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:
chpark
2026-05-31 22:05:06 +09:00
parent 7bcfe9ce34
commit b7bc6b2bbf
2 changed files with 79 additions and 40 deletions
+53 -28
View File
@@ -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>
)}
+26 -12
View File
@@ -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",