feat(momo v0.6): 거래명세표 행 순서 + 메뉴 재배치 + 로그인 랜딩 변경
Deploy momo-erp / deploy (push) Successful in 51s

[거래명세표 행 순서]
- 택배(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:
chpark
2026-05-07 16:43:57 +09:00
parent e65ea43429
commit b0808b1d0a
6 changed files with 71 additions and 13 deletions
+33
View File
@@ -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;
+2 -1
View File
@@ -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 });
}
+7 -4
View File
@@ -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 ? "택배" : "용차"}
+11 -4
View File
@@ -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) });
}
}
+7 -1
View File
@@ -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]
);
+11 -3
View File
@@ -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),