fix(momo): user_info/supply_mng 통합 + JOIN 타입 캐스팅 + ADMIN 가드 보강
Deploy momo-erp / deploy (push) Successful in 48s

- momo-auth: user_info(AES) 기반으로 전환 (momo_users 폐기)
- /api/auth/signup: user_info INSERT (user_type=C 거래처)
- /api/m/vendors: supply_mng 재사용 (charge_user_name/supply_tel_no/reg_no 등 매핑)
- /api/m/{orders,dashboard,statistics,inbounds,procurements}:
  momo_users → user_info LEFT JOIN, momo_vendors/makers → supply_mng (objid::text 캐스팅)
- /api/m/{users,makers} 라우트 삭제 (admin-panel 사용)
- requireMomoAdmin: FITO isAdmin / userType=A / admin@momo.com 모두 통과
- DB: momo_users/roles/menus/role_menus/makers/vendors/attachments DROP (스펙 v0.2)
This commit is contained in:
chpark
2026-04-26 00:41:02 +09:00
parent 3f97e4eac6
commit 95129cf606
17 changed files with 110 additions and 165 deletions
+2 -2
View File
@@ -56,10 +56,10 @@ export async function GET() {
);
const pending = await queryRows(
`SELECT O.objid AS "OBJID", O.order_no AS "ORDER_NO",
U.company_name AS "COMPANY_NAME",
U.user_name AS "COMPANY_NAME",
TO_CHAR(O.order_date,'YYYY-MM-DD') AS "ORDER_DATE",
O.total_amount AS "TOTAL_AMOUNT"
FROM momo_orders O JOIN momo_users U ON O.customer_objid = U.objid
FROM momo_orders O LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE O.status = 'REQUESTED' AND COALESCE(O.is_del,'N') != 'Y'
ORDER BY O.regdate ASC LIMIT 5`
);
+2 -2
View File
@@ -15,13 +15,13 @@ export async function POST(req: NextRequest) {
const rows = await queryRows(
`SELECT I.objid AS "OBJID", I.inbound_no AS "INBOUND_NO",
TO_CHAR(I.inbound_date,'YYYY-MM-DD') AS "INBOUND_DATE",
V.vendor_name AS "VENDOR_NAME", W.wh_name AS "WH_NAME",
V.supply_name AS "VENDOR_NAME", W.wh_name AS "WH_NAME",
I.status AS "STATUS", I.total_amount AS "TOTAL_AMOUNT",
(SELECT COALESCE(SUM(qty_normal),0) FROM momo_inbound_items WHERE inbound_objid = I.objid) AS "QTY_NORMAL",
(SELECT COALESCE(SUM(qty_defect),0) FROM momo_inbound_items WHERE inbound_objid = I.objid) AS "QTY_DEFECT",
P.proc_no AS "PROC_NO"
FROM momo_inbounds I
LEFT JOIN momo_vendors V ON I.vendor_objid = V.objid
LEFT JOIN supply_mng V ON I.vendor_objid = V.objid::text
LEFT JOIN momo_warehouses W ON I.wh_objid = W.objid
LEFT JOIN momo_procurements P ON I.proc_objid = P.objid
WHERE ${conds.join(" AND ")}
-13
View File
@@ -1,13 +0,0 @@
import { NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { requireMomoUser } from "@/lib/momo-guard";
export async function POST() {
const r = await requireMomoUser();
if (r instanceof NextResponse) return r;
const rows = await queryRows(
`SELECT objid AS "OBJID", maker_name AS "MAKER_NAME", contact AS "CONTACT", phone AS "PHONE"
FROM momo_makers WHERE COALESCE(is_del,'N') != 'Y' ORDER BY maker_name ASC`
);
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
}
-27
View File
@@ -1,27 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { execute } from "@/lib/db";
import { createObjectId } from "@/lib/utils";
import { requireMomoAdmin } from "@/lib/momo-guard";
export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const { objid, actionType, makerName, contact, phone } = await req.json();
if (!makerName) return NextResponse.json({ success: false, message: "제조사명은 필수" }, { status: 400 });
if (actionType === "regist") {
const id = createObjectId();
await execute(
`INSERT INTO momo_makers (objid, maker_name, contact, phone, regdate)
VALUES ($1,$2,$3,$4,NOW())`,
[id, makerName, contact ?? null, phone ?? null]
);
return NextResponse.json({ success: true, objId: id });
}
await execute(
`UPDATE momo_makers SET maker_name=$2, contact=$3, phone=$4 WHERE objid=$1`,
[objid, makerName, contact ?? null, phone ?? null]
);
return NextResponse.json({ success: true });
}
+2 -2
View File
@@ -98,11 +98,11 @@ export async function POST(req: NextRequest) {
try {
const order = await queryOne<Record<string, unknown>>(
`SELECT O.objid, O.order_no, TO_CHAR(O.order_date,'YYYY-MM-DD') AS order_date,
U.company_name, U.email, U.ceo_name, U.biz_no, U.phone,
U.user_name, U.email, NULL, NULL, U.cell_phone,
O.total_supply, O.total_vat, O.total_amount,
O.total_taxfree, O.total_taxable
FROM momo_orders O
JOIN momo_users U ON O.customer_objid = U.objid
LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE O.objid = $1`,
[objid]
);
+3 -3
View File
@@ -14,8 +14,8 @@ export async function POST(req: NextRequest) {
O.objid AS "OBJID", O.order_no AS "ORDER_NO",
TO_CHAR(O.order_date,'YYYY-MM-DD') AS "ORDER_DATE",
O.customer_objid AS "CUSTOMER_OBJID",
U.company_name AS "COMPANY_NAME", U.email AS "EMAIL",
U.ceo_name AS "CEO_NAME", U.biz_no AS "BIZ_NO", U.phone AS "PHONE",
U.user_name AS "COMPANY_NAME", U.email AS "EMAIL",
NULL AS "CEO_NAME", NULL AS "BIZ_NO", U.cell_phone AS "PHONE",
O.status AS "STATUS", O.memo AS "MEMO",
O.total_supply AS "TOTAL_SUPPLY", O.total_vat AS "TOTAL_VAT",
O.total_amount AS "TOTAL_AMOUNT",
@@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
O.paid_amount AS "PAID_AMOUNT",
TO_CHAR(O.approve_date,'YYYY-MM-DD HH24:MI') AS "APPROVE_DATE"
FROM momo_orders O
JOIN momo_users U ON O.customer_objid = U.objid
LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE O.objid = $1 AND COALESCE(O.is_del,'N') != 'Y'`,
[objid]
);
+2 -2
View File
@@ -42,7 +42,7 @@ export async function POST(req: NextRequest) {
O.order_no AS "ORDER_NO",
TO_CHAR(O.order_date, 'YYYY-MM-DD') AS "ORDER_DATE",
O.customer_objid AS "CUSTOMER_OBJID",
U.company_name AS "COMPANY_NAME",
U.user_name AS "COMPANY_NAME",
U.email AS "EMAIL",
O.status AS "STATUS",
O.total_supply AS "TOTAL_SUPPLY",
@@ -56,7 +56,7 @@ export async function POST(req: NextRequest) {
TO_CHAR(O.approve_date, 'YYYY-MM-DD HH24:MI') AS "APPROVE_DATE",
O.memo AS "MEMO"
FROM momo_orders O
JOIN momo_users U ON O.customer_objid = U.objid
LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE ${conditions.join(" AND ")}
ORDER BY O.order_date DESC, O.regdate DESC
LIMIT 500`,
+2 -2
View File
@@ -12,10 +12,10 @@ export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string
const order = await queryOne<Record<string, unknown>>(
`SELECT O.objid, O.order_no, TO_CHAR(O.order_date,'YYYY-MM-DD') AS order_date,
O.customer_objid,
U.company_name, U.ceo_name, U.biz_no, U.phone,
U.user_name, NULL, NULL, U.cell_phone,
O.total_supply, O.total_vat, O.total_amount,
O.total_taxfree, O.total_taxable
FROM momo_orders O JOIN momo_users U ON O.customer_objid = U.objid
FROM momo_orders O LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE O.objid = $1`,
[id]
);
+2 -2
View File
@@ -10,9 +10,9 @@ export async function POST(req: NextRequest) {
`SELECT P.objid AS "OBJID", P.proc_no AS "PROC_NO",
TO_CHAR(P.proc_date,'YYYY-MM-DD') AS "PROC_DATE",
P.status AS "STATUS", P.total_amount AS "TOTAL_AMOUNT", P.memo AS "MEMO",
V.objid AS "VENDOR_OBJID", V.vendor_name AS "VENDOR_NAME"
V.objid AS "VENDOR_OBJID", V.supply_name AS "VENDOR_NAME"
FROM momo_procurements P
LEFT JOIN momo_vendors V ON P.vendor_objid = V.objid
LEFT JOIN supply_mng V ON P.vendor_objid = V.objid::text
WHERE P.objid = $1`,
[objid]
);
+2 -2
View File
@@ -17,11 +17,11 @@ export async function POST(req: NextRequest) {
const rows = await queryRows(
`SELECT P.objid AS "OBJID", P.proc_no AS "PROC_NO",
TO_CHAR(P.proc_date,'YYYY-MM-DD') AS "PROC_DATE",
P.vendor_objid AS "VENDOR_OBJID", V.vendor_name AS "VENDOR_NAME",
P.vendor_objid AS "VENDOR_OBJID", V.supply_name AS "VENDOR_NAME",
P.status AS "STATUS", P.total_amount AS "TOTAL_AMOUNT", P.memo AS "MEMO",
(SELECT COUNT(*) FROM momo_procurement_items WHERE proc_objid = P.objid) AS "LINE_CNT"
FROM momo_procurements P
LEFT JOIN momo_vendors V ON P.vendor_objid = V.objid
LEFT JOIN supply_mng V ON P.vendor_objid = V.objid::text
WHERE ${conds.join(" AND ")}
ORDER BY P.proc_date DESC, P.regdate DESC LIMIT 500`,
params
+3 -3
View File
@@ -12,17 +12,17 @@ export async function POST(req: NextRequest) {
const rows = await queryRows(
`SELECT
U.company_name AS "COMPANY_NAME",
U.user_name AS "COMPANY_NAME",
COALESCE(SUM(O.total_taxfree), 0) AS "TAX_FREE",
COALESCE(SUM(O.total_taxable), 0) AS "TAXABLE",
COALESCE(SUM(O.total_amount), 0) AS "TOTAL"
FROM momo_orders O
JOIN momo_users U ON O.customer_objid = U.objid
LEFT JOIN user_info U ON U.user_id = O.customer_objid
WHERE EXTRACT(YEAR FROM O.order_date) = $1
AND EXTRACT(MONTH FROM O.order_date) = $2
AND O.status IN ('APPROVED', 'SHIPPED', 'INVOICED', 'PAID')
AND COALESCE(O.is_del,'N') != 'Y'
GROUP BY U.company_name
GROUP BY U.user_name
ORDER BY "TOTAL" DESC`,
[y, m]
);
-18
View File
@@ -1,18 +0,0 @@
import { NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { requireMomoAdmin } from "@/lib/momo-guard";
export async function POST() {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const rows = await queryRows(
`SELECT objid AS "OBJID", email AS "EMAIL", company_name AS "COMPANY_NAME",
ceo_name AS "CEO_NAME", phone AS "PHONE", biz_no AS "BIZ_NO",
role AS "ROLE", status AS "STATUS",
TO_CHAR(regdate, 'YYYY-MM-DD') AS "REGDATE"
FROM momo_users WHERE COALESCE(is_del,'N') != 'Y'
ORDER BY regdate DESC`
);
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
}
-22
View File
@@ -1,22 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { execute } from "@/lib/db";
import { requireMomoAdmin } from "@/lib/momo-guard";
export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const { objid, role, status } = await req.json();
if (!objid) return NextResponse.json({ success: false, message: "objid 누락" }, { status: 400 });
const sets: string[] = [];
const params: unknown[] = [objid];
let i = 2;
if (role) { sets.push(`role = $${i++}`); params.push(role); }
if (status) { sets.push(`status = $${i++}`); params.push(status); }
if (sets.length === 0) return NextResponse.json({ success: false, message: "변경 사항 없음" });
sets.push(`update_date = NOW()`);
await execute(`UPDATE momo_users SET ${sets.join(", ")} WHERE objid = $1`, params);
return NextResponse.json({ success: true });
}
+11 -5
View File
@@ -1,3 +1,4 @@
// 매입처 목록 — supply_mng 재사용 (charger_type 같은 컬럼 없으므로 모든 supply_mng 행 노출)
import { NextResponse } from "next/server";
import { queryRows } from "@/lib/db";
import { requireMomoUser } from "@/lib/momo-guard";
@@ -6,11 +7,16 @@ export async function POST() {
const r = await requireMomoUser();
if (r instanceof NextResponse) return r;
const rows = await queryRows(
`SELECT objid AS "OBJID", vendor_name AS "VENDOR_NAME",
contact AS "CONTACT", phone AS "PHONE",
biz_no AS "BIZ_NO", email AS "EMAIL", address AS "ADDRESS"
FROM momo_vendors WHERE COALESCE(is_del,'N') != 'Y'
ORDER BY vendor_name ASC`
`SELECT objid AS "OBJID",
supply_name AS "VENDOR_NAME",
charge_user_name AS "CONTACT",
supply_tel_no AS "PHONE",
reg_no AS "BIZ_NO",
email AS "EMAIL",
supply_address AS "ADDRESS"
FROM supply_mng
WHERE COALESCE(status,'active') = 'active'
ORDER BY supply_name ASC`
);
return NextResponse.json({ RESULTLIST: rows, TOTAL_CNT: rows.length });
}
+8 -4
View File
@@ -1,3 +1,4 @@
// 매입처 등록/수정 — supply_mng 재사용
import { NextRequest, NextResponse } from "next/server";
import { execute } from "@/lib/db";
import { createObjectId } from "@/lib/utils";
@@ -6,20 +7,23 @@ import { requireMomoAdmin } from "@/lib/momo-guard";
export async function POST(req: NextRequest) {
const g = await requireMomoAdmin();
if (g instanceof NextResponse) return g;
const userId = g.user.userId;
const { objid, actionType, vendorName, contact, phone, bizNo, email, address } = await req.json();
if (!vendorName) return NextResponse.json({ success: false, message: "매입처명 필수" }, { status: 400 });
if (actionType === "regist") {
const id = createObjectId();
const code = "VND-" + id.slice(-6);
await execute(
`INSERT INTO momo_vendors (objid, vendor_name, contact, phone, biz_no, email, address, regdate)
VALUES ($1,$2,$3,$4,$5,$6,$7,NOW())`,
[id, vendorName, contact ?? null, phone ?? null, bizNo ?? null, email ?? null, address ?? null]
`INSERT INTO supply_mng (objid, supply_code, supply_name, charge_user_name, supply_tel_no, reg_no, email, supply_address, status, reg_id, reg_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', $9, NOW())`,
[id, code, vendorName, contact ?? null, phone ?? null, bizNo ?? null, email ?? null, address ?? null, userId]
);
return NextResponse.json({ success: true, objId: id });
}
await execute(
`UPDATE momo_vendors SET vendor_name=$2, contact=$3, phone=$4, biz_no=$5, email=$6, address=$7 WHERE objid=$1`,
`UPDATE supply_mng SET supply_name=$2, charge_user_name=$3, supply_tel_no=$4, reg_no=$5, email=$6, supply_address=$7 WHERE objid=$1`,
[objid, vendorName, contact ?? null, phone ?? null, bizNo ?? null, email ?? null, address ?? null]
);
return NextResponse.json({ success: true });
+60 -54
View File
@@ -1,23 +1,20 @@
// 모모유통 사용자 인증 (bcrypt + momo_users)
// 기존 FITO user_info(AES) 와 별도로 동작
import bcrypt from "bcryptjs";
// 모모 사용자 인증 — user_info 테이블 사용 (momo_users 폐기 v0.2)
// 기존 FITO AES 암호화와 동일하게 user_password 사용 — verifyCredentials 와 호환
import { queryOne, execute } from "./db";
import { createObjectId } from "./utils";
import { encrypt } from "./encrypt";
export interface MomoUser {
objid: string;
objid: string; // = user_id
email: string;
companyName: string;
companyName: string; // = user_name
ceoName: string;
bizNo: string;
phone: string;
role: "USER" | "ADMIN";
status: "ACTIVE" | "LOCKED" | "LEFT";
// 기존 User 호환 필드 (메뉴/세션이 사용)
userId: string; // = email
userName: string; // = companyName
isAdmin: boolean; // role === 'ADMIN'
status: string;
userId: string;
userName: string;
isAdmin: boolean;
}
export interface SignupInput {
@@ -30,53 +27,64 @@ export interface SignupInput {
}
function rowToUser(r: Record<string, unknown>): MomoUser {
const role = (r.ROLE as string) || "USER";
const email = (r.EMAIL as string) || "";
const companyName = (r.COMPANY_NAME as string) || "";
const userType = String(r.USER_TYPE || "").toUpperCase();
const role: "USER" | "ADMIN" = userType === "A" ? "ADMIN" : "USER";
const userId = (r.USER_ID as string) || "";
const email = (r.EMAIL as string) || userId;
const companyName = (r.USER_NAME as string) || "";
return {
objid: (r.OBJID as string) || "",
objid: userId,
email,
companyName,
ceoName: (r.CEO_NAME as string) || "",
bizNo: (r.BIZ_NO as string) || "",
phone: (r.PHONE as string) || "",
role: role as "USER" | "ADMIN",
status: ((r.STATUS as string) || "ACTIVE") as MomoUser["status"],
userId: email,
ceoName: (r.USER_NAME_ENG as string) || "",
bizNo: "",
phone: (r.CELL_PHONE as string) || (r.TEL as string) || "",
role,
status: (r.STATUS as string) || "active",
userId,
userName: companyName,
isAdmin: role === "ADMIN",
};
}
export async function findMomoUserByEmail(email: string): Promise<{ user: MomoUser; passwordHash: string } | null> {
export async function findMomoUserByEmail(email: string): Promise<MomoUser | null> {
const row = await queryOne<Record<string, unknown>>(
`SELECT objid AS "OBJID", email AS "EMAIL", password_hash AS "PASSWORD_HASH",
company_name AS "COMPANY_NAME", ceo_name AS "CEO_NAME",
biz_no AS "BIZ_NO", phone AS "PHONE",
role AS "ROLE", status AS "STATUS"
FROM momo_users
WHERE LOWER(email) = LOWER($1) AND COALESCE(is_del, 'N') != 'Y'`,
`SELECT user_id AS "USER_ID", user_name AS "USER_NAME",
user_name_eng AS "USER_NAME_ENG",
email AS "EMAIL", cell_phone AS "CELL_PHONE", tel AS "TEL",
user_type AS "USER_TYPE", status AS "STATUS"
FROM user_info
WHERE LOWER(user_id) = LOWER($1) OR LOWER(email) = LOWER($1)
LIMIT 1`,
[email]
);
if (!row) return null;
return {
user: rowToUser(row),
passwordHash: (row.PASSWORD_HASH as string) || "",
};
return rowToUser(row);
}
export async function verifyMomoCredentials(
email: string,
password: string
): Promise<{ success: boolean; user?: MomoUser; error?: string }> {
const found = await findMomoUserByEmail(email);
if (!found) return { success: false, error: "사용자가 존재하지 않습니다." };
if (found.user.status !== "ACTIVE") {
return { success: false, error: "계정이 비활성화 상태입니다. 관리자에게 문의하세요." };
const row = await queryOne<{ user_password: string; status: string }>(
`SELECT user_password, status FROM user_info
WHERE LOWER(user_id) = LOWER($1) OR LOWER(email) = LOWER($1) LIMIT 1`,
[email]
);
if (!row) return { success: false, error: "사용자가 존재하지 않습니다." };
if (row.status && row.status !== "active") {
return { success: false, error: "계정이 비활성화 상태입니다." };
}
const ok = await bcrypt.compare(password, found.passwordHash);
if (!ok) return { success: false, error: "비밀번호가 일치하지 않습니다." };
return { success: true, user: found.user };
const stored = row.user_password ?? "";
if (!stored) {
return { success: false, error: "비밀번호 미설정 — 관리자에게 비밀번호 초기화 요청하세요." };
}
const enc = encrypt(password);
if (stored !== enc) {
return { success: false, error: "비밀번호가 일치하지 않습니다." };
}
const user = await findMomoUserByEmail(email);
return user ? { success: true, user } : { success: false, error: "사용자 조회 실패" };
}
export async function signupMomoUser(input: SignupInput): Promise<{ success: boolean; user?: MomoUser; error?: string }> {
@@ -84,25 +92,23 @@ export async function signupMomoUser(input: SignupInput): Promise<{ success: boo
if (!email || !input.password || !input.companyName) {
return { success: false, error: "이메일/비밀번호/업체명은 필수입니다." };
}
if (input.password.length < 8) {
return { success: false, error: "비밀번호는 8자 이상이어야 합니다." };
if (input.password.length < 4) {
return { success: false, error: "비밀번호는 4자 이상이어야 합니다." };
}
const dup = await queryOne(`SELECT 1 FROM momo_users WHERE LOWER(email) = LOWER($1)`, [email]);
const dup = await queryOne(
`SELECT 1 FROM user_info WHERE LOWER(user_id) = LOWER($1) OR LOWER(email) = LOWER($1)`,
[email]
);
if (dup) return { success: false, error: "이미 가입된 이메일입니다." };
const objid = createObjectId();
const hash = await bcrypt.hash(input.password, 10);
const enc = encrypt(input.password);
await execute(
`INSERT INTO momo_users (objid, email, password_hash, company_name, ceo_name, biz_no, phone, role, status, regdate, regid)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'USER', 'ACTIVE', NOW(), $1)`,
[objid, email, hash, input.companyName.trim(),
input.ceoName?.trim() ?? null,
input.bizNo?.trim() ?? null,
input.phone?.trim() ?? null]
`INSERT INTO user_info (user_id, user_password, user_name, email, cell_phone, user_type, user_type_name, status, regdate)
VALUES ($1, $2, $3, $1, $4, 'C', '거래처', 'active', NOW())`,
[email, enc, input.companyName.trim(), input.phone?.trim() ?? ""]
);
const found = await findMomoUserByEmail(email);
return { success: true, user: found?.user };
const user = await findMomoUserByEmail(email);
return user ? { success: true, user } : { success: false, error: "가입 후 조회 실패" };
}
+11 -2
View File
@@ -13,8 +13,17 @@ export async function requireMomoUser(): Promise<{ user: User } | NextResponse>
export async function requireMomoAdmin(): Promise<{ user: User } | NextResponse> {
const r = await requireMomoUser();
if (r instanceof NextResponse) return r;
// ADMIN 판정: MOMO role==='ADMIN' OR FITO isAdmin===true (plm_admin 등)
const isAdmin = r.user.role === "ADMIN" || r.user.isAdmin === true;
// ADMIN 판정:
// - MOMO role==='ADMIN'
// - FITO isAdmin===true (plm_admin)
// - userType==='MOMO' or userType==='A' (user_info.user_type='A' 케이스)
// - admin@momo.com 이메일
const u = r.user;
const isAdmin =
u.role === "ADMIN" ||
u.isAdmin === true ||
u.userType === "A" ||
u.userId?.toLowerCase() === "admin@momo.com";
if (!isAdmin) {
return NextResponse.json({ success: false, message: "관리자 권한이 필요합니다." }, { status: 403 });
}