fix(orders): 일반 사용자 권한 매칭 (objid + userId) + 출고요청 리스트 가로스크롤 제거
Deploy momo-erp / deploy (push) Successful in 2m56s

권한 fix — order.customer_objid 와 매칭 시:
- 기존: user.objid 만 비교 → FITO 사용자(objid 없음)는 항상 fail → "권한 없음"
- 변경: user.objid ?? user.userId 와 customer_objid 매칭, 또는 user.userId 와 직접 매칭
- 적용: items/update, lines/save, cancel, statement, items/remark (5개 API)

출고요청 리스트(m/orders/new) UI:
- table 의 min-w-[640px] 제거 + table-fixed 적용
- 수량 컬럼 폭 180px → 112px, "담기" 버튼 텍스트 제거 (+ 아이콘만)
- 폰트 11~12px 로 축소
- 모바일 화면 한 줄에 들어오도록

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-13 11:49:26 +09:00
parent a8049f57a6
commit 7151a401d4
6 changed files with 40 additions and 32 deletions
+26 -27
View File
@@ -618,15 +618,15 @@ function ListView({ items, cart, unlimitedQty, onAdd, onPlus, onMinus, onSetQty,
}) {
return (
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm min-w-[640px]">
<thead className="bg-slate-50 text-slate-600 text-xs">
<div className="overflow-x-hidden">
<table className="w-full text-[12px] table-fixed">
<thead className="bg-slate-50 text-slate-600 text-[10px]">
<tr>
<th className="text-left px-3 py-2.5"></th>
<th className="text-center px-3 py-2.5 w-14"></th>
<th className="text-right px-3 py-2.5 w-20"></th>
<th className="text-right px-3 py-2.5 w-16"></th>
<th className="text-center px-3 py-2.5 w-[180px]"></th>
<th className="text-left px-2 py-2"></th>
<th className="text-center px-1 py-2 w-10"></th>
<th className="text-right px-1 py-2 w-[68px]"></th>
<th className="text-right px-1 py-2 w-12"></th>
<th className="text-center px-1 py-2 w-[112px]"></th>
</tr>
</thead>
<tbody>
@@ -639,26 +639,25 @@ function ListView({ items, cart, unlimitedQty, onAdd, onPlus, onMinus, onSetQty,
const soldOut = stock === 0;
return (
<tr key={it.OBJID} className={`border-t border-slate-100 ${inCart > 0 ? "bg-emerald-50/40" : "hover:bg-slate-50"}`}>
<td className="px-3 py-2">
<div className="font-semibold">
<td className="px-2 py-2 overflow-hidden">
<div className="font-semibold truncate text-[12px]">
{it.ITEM_NAME}
{it.REQUIRES_DELIVERY === "Y" && <span className="ml-1 px-1 py-0.5 rounded bg-orange-100 text-orange-700 text-[9px] font-bold"></span>}
{inCart > 0 && <span className="ml-1 px-1.5 py-0.5 rounded bg-emerald-600 text-white text-[10px] font-bold"> {inCart}</span>}
</div>
<div className="text-[10px] text-slate-400">{it.MAKER_NAME || "-"}</div>
{inCart > 0 && <div className="text-[10px] text-emerald-700 font-bold"> {inCart}</div>}
</td>
<td className="px-3 py-2 text-center">
<span className={`text-[10px] px-1.5 py-0.5 rounded font-bold ${it.IS_TAX_FREE === "Y" ? "bg-violet-100 text-violet-700" : "bg-rose-100 text-rose-700"}`}>
<td className="px-1 py-2 text-center">
<span className={`text-[9px] px-1 py-0.5 rounded font-bold ${it.IS_TAX_FREE === "Y" ? "bg-violet-100 text-violet-700" : "bg-rose-100 text-rose-700"}`}>
{it.IS_TAX_FREE === "Y" ? "면세" : "과세"}
</span>
</td>
<td className="px-3 py-2 text-right tabular-nums font-bold">{Number(it.UNIT_PRICE).toLocaleString("ko-KR")}</td>
<td className={`px-3 py-2 text-right tabular-nums ${stock <= 0 ? "text-rose-500 font-bold" : "text-slate-700"}`}>{Number(stock).toLocaleString("ko-KR")}</td>
<td className="px-3 py-2">
<td className="px-1 py-2 text-right tabular-nums font-bold text-[11px]">{Number(it.UNIT_PRICE).toLocaleString("ko-KR")}</td>
<td className={`px-1 py-2 text-right tabular-nums text-[11px] ${stock <= 0 ? "text-rose-500 font-bold" : "text-slate-700"}`}>{Number(stock).toLocaleString("ko-KR")}</td>
<td className="px-1 py-2">
{soldOut ? (
<div className="text-center text-xs text-slate-400"></div>
<div className="text-center text-[10px] text-slate-400"></div>
) : inCart === 0 ? (
<div className="flex gap-1 justify-end">
<div className="flex gap-0.5 justify-end">
<input
type="number"
min={1}
@@ -673,25 +672,25 @@ function ListView({ items, cart, unlimitedQty, onAdd, onPlus, onMinus, onSetQty,
}
}}
id={`lqty-${it.OBJID}`}
className="w-14 h-8 px-2 rounded border border-slate-200 text-center text-sm tabular-nums"
className="w-12 h-7 px-1 rounded border border-slate-200 text-center text-[11px] tabular-nums"
/>
<button onClick={() => {
const el = document.getElementById(`lqty-${it.OBJID}`) as HTMLInputElement | null;
const v = el ? Number(el.value) || 1 : 1;
onAdd(it, Math.max(1, Math.min(limit, v)));
if (el) el.value = "1";
}} className="h-8 px-3 rounded bg-emerald-700 text-white text-xs font-bold hover:bg-emerald-800 inline-flex items-center gap-0.5">
<Plus size={12} />
}} className="h-7 w-9 rounded bg-emerald-700 text-white font-bold hover:bg-emerald-800 inline-flex items-center justify-center" title="담기">
<Plus size={14} />
</button>
</div>
) : (
<div className="flex items-center gap-1 justify-end">
<button onClick={() => onMinus(it.OBJID)} className="w-8 h-8 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center"><Minus size={12} /></button>
<div className="flex items-center gap-0.5 justify-end">
<button onClick={() => onMinus(it.OBJID)} className="w-7 h-7 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center"><Minus size={11} /></button>
<input type="number" min={1} max={limit} value={inCart}
onChange={(e) => onSetQty(it.OBJID, Number(e.target.value))}
className="w-12 h-8 text-center font-bold text-sm tabular-nums border border-emerald-200 rounded" />
<button onClick={() => onPlus(it.OBJID)} className="w-8 h-8 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center"><Plus size={12} /></button>
<button onClick={() => onRemove(it.OBJID)} title="빼기" className="w-8 h-8 rounded bg-white border border-rose-200 hover:bg-rose-100 text-rose-500 flex items-center justify-center"><X size={12} /></button>
className="w-10 h-7 text-center font-bold text-[11px] tabular-nums border border-emerald-200 rounded" />
<button onClick={() => onPlus(it.OBJID)} className="w-7 h-7 rounded bg-white border border-emerald-200 hover:bg-emerald-100 flex items-center justify-center"><Plus size={11} /></button>
<button onClick={() => onRemove(it.OBJID)} title="빼기" className="w-7 h-7 rounded bg-white border border-rose-200 hover:bg-rose-100 text-rose-500 flex items-center justify-center"><X size={11} /></button>
</div>
)}
</td>
+3 -1
View File
@@ -12,7 +12,9 @@ export async function POST(req: NextRequest) {
[objid]
);
if (!order) return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 });
if (r.user.role === "USER" && order.customer_objid !== r.user.objid) {
// 본인 발주 매칭 (user.objid 또는 user.userId 와 customer_objid 일치)
const userOwn = r.user.objid ?? r.user.userId;
if (r.user.role === "USER" && order.customer_objid !== userOwn && order.customer_objid !== r.user.userId) {
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
}
if (order.status !== "REQUESTED") {
+2 -1
View File
@@ -28,7 +28,8 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, message: "라인을 찾을 수 없습니다." }, { status: 404 });
}
const row = own.rows[0];
if (!isAdmin && row.customer_objid !== r.user.objid) {
const userOwn = r.user.objid ?? r.user.userId;
if (!isAdmin && row.customer_objid !== userOwn && row.customer_objid !== r.user.userId) {
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
}
if (row.status !== "REQUESTED") {
+4 -1
View File
@@ -35,7 +35,10 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 });
}
const order = orderRes.rows[0];
if (!isAdmin && order.customer_objid !== r.user.objid) {
// 본인 발주 매칭: customer_objid 가 user.objid 또는 user.userId 와 일치하면 OK
// (momo 가입자는 user.objid, FITO 사용자는 user.userId 가 customer_objid 로 들어감)
const userOwn = r.user.objid ?? r.user.userId;
if (!isAdmin && order.customer_objid !== userOwn && order.customer_objid !== r.user.userId) {
await client.query("ROLLBACK");
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
}
+3 -1
View File
@@ -40,7 +40,9 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ success: false, message: "발주를 찾을 수 없습니다." }, { status: 404 });
}
const order = orderRes.rows[0];
if (!isAdmin && order.customer_objid !== r.user.objid) {
// 본인 발주 매칭: customer_objid 가 user.objid 또는 user.userId 와 일치
const userOwn = r.user.objid ?? r.user.userId;
if (!isAdmin && order.customer_objid !== userOwn && order.customer_objid !== r.user.userId) {
await client.query("ROLLBACK");
return NextResponse.json({ success: false, message: "권한이 없습니다." }, { status: 403 });
}
+2 -1
View File
@@ -26,7 +26,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
[id]
);
if (!order) return NextResponse.json({ success: false, message: "찾을 수 없습니다." }, { status: 404 });
if (r.user.role === "USER" && order.customer_objid !== r.user.objid) {
const userOwn = r.user.objid ?? r.user.userId;
if (r.user.role === "USER" && order.customer_objid !== userOwn && order.customer_objid !== r.user.userId) {
return NextResponse.json({ success: false, message: "권한 없음" }, { status: 403 });
}