[거래명세표 행 순서] - 택배(DELIVERY)/용차(CHARTER) 라인이 품목(ITEM) 위로 표시되도록 정렬 - /api/m/orders/detail, /api/m/orders/statement/[id]: ORDER BY CASE kind 추가 - /m/admin/orders 화면 + xlsx 출력: 표시 순서 기준으로 SEQ 재부여 (DB seq 와 무관) [메뉴 014] - 마스터 관리 (9000200) → 마지막 (seq 900) - 대시보드 (9000001) → 통계 그룹(9000500) 자식으로 이동, parent 변경 - 빈 [DASHBOARD] 대메뉴(1837127121) 비활성화 - 최종 순서: 거래처 주문 → 매입/입고 → 출고/정산 → 통계(대시보드 포함) → 마스터 관리 [로그인 랜딩] - 기존: 모든 사용자 /m/dashboard - 변경: 역할별 분기 · ADMIN/관리자 → /m/admin/orders (발주서 관리·출고처리) · USER/거래처 → /m/orders/new (출고 요청) - 회원가입 직후도 /m/orders/new 로 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
-- 014_menu_reorder.sql
|
||||
-- v0.6 (2026-05-07)
|
||||
-- 메뉴 순서 재배치 + 대시보드 위치 이동
|
||||
--
|
||||
-- 변경 후 순서:
|
||||
-- 600 거래처 주문 (9000100)
|
||||
-- 700 매입/입고 (9000300)
|
||||
-- 750 출고/정산 (9000400)
|
||||
-- 800 통계 (9000500) ← 대시보드 자식으로 포함
|
||||
-- 900 마스터 관리 (9000200) ← 마지막
|
||||
--
|
||||
-- 대시보드(9000001) 는 [DASHBOARD] 대메뉴(1837127121) → 통계(9000500) 자식으로 이동
|
||||
-- [DASHBOARD] 대메뉴 자체는 비활성화
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. 대메뉴 seq 재조정 — 마스터 관리를 맨 뒤로
|
||||
UPDATE menu_info SET seq = 900 WHERE objid = 9000200; -- 마스터 관리
|
||||
-- 거래처 주문(600), 매입/입고(700), 출고/정산(750), 통계(800)는 그대로
|
||||
|
||||
-- 2. 대시보드(9000001) 를 통계(9000500) 의 첫 자식으로 이동
|
||||
UPDATE menu_info
|
||||
SET parent_obj_id = 9000500,
|
||||
seq = 5,
|
||||
menu_name_kor = '대시보드'
|
||||
WHERE objid = 9000001;
|
||||
|
||||
-- 3. 빈 [DASHBOARD] 대메뉴(1837127121) 비활성화
|
||||
UPDATE menu_info
|
||||
SET status = 'inactive'
|
||||
WHERE objid = 1837127121;
|
||||
|
||||
COMMIT;
|
||||
@@ -61,7 +61,8 @@ export default function SignupPage() {
|
||||
text: "이제 발주를 시작하실 수 있습니다.",
|
||||
confirmButtonColor: "#0f766e",
|
||||
});
|
||||
router.push("/m/dashboard");
|
||||
// 거래처 가입 직후 — 출고 요청 화면으로
|
||||
router.push("/m/orders/new");
|
||||
} else {
|
||||
Swal.fire({ icon: "error", title: "가입 실패", text: data.message });
|
||||
}
|
||||
|
||||
@@ -441,7 +441,8 @@ function StatementPreview({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tabular-nums">
|
||||
{items.map((it) => {
|
||||
{items.map((it, idx) => {
|
||||
const displaySeq = idx + 1;
|
||||
const isExtra = it.KIND === "DELIVERY" || it.KIND === "CHARTER";
|
||||
const lack = !isExtra && Number(it.STOCK_QTY) < Number(it.QTY);
|
||||
const kindBadge = it.KIND === "DELIVERY" ? "택배" : it.KIND === "CHARTER" ? "용차" : null;
|
||||
@@ -452,6 +453,7 @@ function StatementPreview({
|
||||
<ExtraRow
|
||||
key={it.OBJID}
|
||||
line={it}
|
||||
displaySeq={displaySeq}
|
||||
onSave={(updated) => upsertExtra({ objid: it.OBJID, kind: it.KIND as "DELIVERY" | "CHARTER", ...updated })}
|
||||
onDelete={() => deleteExtra(it.OBJID)}
|
||||
/>
|
||||
@@ -460,7 +462,7 @@ function StatementPreview({
|
||||
|
||||
return (
|
||||
<tr key={it.OBJID || it.SEQ} className={kindBg}>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-center">{it.SEQ}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-center">{displaySeq}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1">
|
||||
{kindBadge && <span className={`mr-1 text-[9px] font-bold px-1 py-0.5 rounded ${it.KIND === "DELIVERY" ? "bg-orange-200 text-orange-800" : "bg-sky-200 text-sky-800"}`}>{kindBadge}</span>}
|
||||
{it.ITEM_NAME}
|
||||
@@ -524,8 +526,9 @@ function StatementPreview({
|
||||
);
|
||||
}
|
||||
|
||||
function ExtraRow({ line, onSave, onDelete }: {
|
||||
function ExtraRow({ line, displaySeq, onSave, onDelete }: {
|
||||
line: DetailLine;
|
||||
displaySeq: number;
|
||||
onSave: (data: { label: string; unitPrice: number; qty: number }) => void;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
@@ -542,7 +545,7 @@ function ExtraRow({ line, onSave, onDelete }: {
|
||||
|
||||
return (
|
||||
<tr className={isDelivery ? "bg-orange-50" : "bg-sky-50"}>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-center">{line.SEQ}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1 text-center">{displaySeq}</td>
|
||||
<td className="border border-slate-300 px-1.5 py-1">
|
||||
<span className={`mr-1 text-[9px] font-bold px-1 py-0.5 rounded ${isDelivery ? "bg-orange-200 text-orange-800" : "bg-sky-200 text-sky-800"}`}>
|
||||
{isDelivery ? "택배" : "용차"}
|
||||
|
||||
@@ -18,12 +18,20 @@ export async function POST(request: NextRequest) {
|
||||
// 이메일 형태이면 MOMO 사용자 우선 시도, 그 외에는 FITO 우선 시도
|
||||
const looksLikeEmail = /@/.test(userId);
|
||||
|
||||
// 역할별 진입 페이지:
|
||||
// ADMIN(모모유통 임직원/시스템 관리자) → /m/admin/orders (발주서 관리·출고처리)
|
||||
// USER (거래처) → /m/orders/new (출고 요청)
|
||||
const landingFor = (u: User): string =>
|
||||
(u.isAdmin || u.role === "ADMIN" || u.userType === "A")
|
||||
? "/m/admin/orders"
|
||||
: "/m/orders/new";
|
||||
|
||||
if (looksLikeEmail) {
|
||||
const momo = await verifyMomoCredentials(userId, password);
|
||||
if (momo.success && momo.user) {
|
||||
const sessionUser: User = momoToSessionUser(momo.user);
|
||||
await createSession(sessionUser);
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: "/m/dashboard" });
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: landingFor(sessionUser) });
|
||||
}
|
||||
// MOMO 실패 시 FITO 폴백 시도 (관리자 마이그레이션 케이스)
|
||||
}
|
||||
@@ -31,8 +39,7 @@ export async function POST(request: NextRequest) {
|
||||
const fito = await verifyCredentials(userId, password);
|
||||
if (fito.success && fito.user) {
|
||||
await createSession(fito.user);
|
||||
// 모모유통 도메인은 모두 모모 대시보드로 (FITO admin 도 /m/dashboard 로 진입)
|
||||
return NextResponse.json({ success: true, user: fito.user, redirectTo: "/m/dashboard" });
|
||||
return NextResponse.json({ success: true, user: fito.user, redirectTo: landingFor(fito.user) });
|
||||
}
|
||||
|
||||
// FITO 도 실패하면 MOMO를 한 번 더 시도 (이메일 형태가 아니지만 MOMO 계정인 경우)
|
||||
@@ -41,7 +48,7 @@ export async function POST(request: NextRequest) {
|
||||
if (momo.success && momo.user) {
|
||||
const sessionUser: User = momoToSessionUser(momo.user);
|
||||
await createSession(sessionUser);
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: "/m/dashboard" });
|
||||
return NextResponse.json({ success: true, user: sessionUser, redirectTo: landingFor(sessionUser) });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,13 @@ export async function POST(req: NextRequest) {
|
||||
FROM momo_order_items OI
|
||||
LEFT JOIN momo_items I ON OI.item_objid = I.objid
|
||||
WHERE OI.order_objid = $1
|
||||
ORDER BY OI.seq ASC`,
|
||||
ORDER BY
|
||||
CASE COALESCE(OI.kind,'ITEM')
|
||||
WHEN 'DELIVERY' THEN 0
|
||||
WHEN 'CHARTER' THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
OI.seq ASC`,
|
||||
[objid]
|
||||
);
|
||||
|
||||
|
||||
@@ -26,8 +26,16 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
|
||||
const items = await queryRows<Record<string, unknown>>(
|
||||
`SELECT seq, item_name_snap, unit_price, qty, is_tax_free, supply_amount, vat_amount, total_amount,
|
||||
COALESCE(kind, 'ITEM') AS kind, extra_label,
|
||||
(SELECT unit FROM momo_items WHERE objid = OI.item_objid) AS unit
|
||||
FROM momo_order_items OI WHERE order_objid = $1 ORDER BY seq`,
|
||||
FROM momo_order_items OI WHERE order_objid = $1
|
||||
ORDER BY
|
||||
CASE COALESCE(kind,'ITEM')
|
||||
WHEN 'DELIVERY' THEN 0
|
||||
WHEN 'CHARTER' THEN 1
|
||||
ELSE 2
|
||||
END,
|
||||
seq ASC`,
|
||||
[id]
|
||||
);
|
||||
|
||||
@@ -46,8 +54,8 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
|
||||
phone: process.env.MOMO_PHONE ?? "010-6624-5315",
|
||||
email: process.env.SMTP_FROM ?? "momo8443@daum.net",
|
||||
},
|
||||
items: items.map((it) => ({
|
||||
seq: Number(it.seq),
|
||||
items: items.map((it, idx) => ({
|
||||
seq: idx + 1,
|
||||
itemName: String(it.item_name_snap),
|
||||
unit: String(it.unit ?? "EA"),
|
||||
qty: Number(it.qty),
|
||||
|
||||
Reference in New Issue
Block a user