feat(landing): 거래처 사용법 6단계 + 화면 미리보기 섹션 추가

- 가입→검색→장바구니→승인→메일→정산까지 단계별 카드
- 장바구니 미리보기 + 자동발송 메일 미리보기 추가
- 우측 상단 회원가입/로그인 버튼은 기존 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-25 21:09:45 +09:00
parent fa91c805fc
commit d7cc711b93
5 changed files with 497 additions and 18 deletions
+173
View File
@@ -0,0 +1,173 @@
// E2E 테스트: 가입 → 시드 데이터 → 발주 → 승인 + 메일 발송
// 전제: dev 서버 실행 중 (localhost:3000), DB 마이그레이션 완료
// 사용법: node scripts/test-e2e.mjs
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import pg from "pg";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const envPath = path.join(__dirname, "..", ".env.development");
if (fs.existsSync(envPath)) {
for (const line of fs.readFileSync(envPath, "utf-8").split(/\r?\n/)) {
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
if (m) process.env[m[1]] ??= m[2].replace(/^["']|["']$/g, "");
}
}
const BASE = "http://localhost:3000";
const TEST_EMAIL = "chpark@wace.me";
const TEST_PASSWORD = "test1234abcd";
let cookieJar = "";
function setCookies(res) {
const sc = res.headers.getSetCookie?.() || res.headers.raw?.()["set-cookie"] || [];
for (const c of sc) {
const kv = c.split(";")[0];
cookieJar = cookieJar
? cookieJar.split("; ").filter((p) => !p.startsWith(kv.split("=")[0] + "=")).concat(kv).join("; ")
: kv;
}
}
async function api(path, init = {}) {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
"Content-Type": "application/json",
...(cookieJar ? { Cookie: cookieJar } : {}),
...(init.headers ?? {}),
},
});
setCookies(res);
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch { json = { raw: text }; }
return { status: res.status, body: json };
}
const log = (...a) => console.log("[e2e]", ...a);
const fail = (msg) => { console.error("[e2e] ✖", msg); process.exit(1); };
// ===== 1. DB 정리 + 시드 (테스트 데이터) =====
log("1. DB 정리 + 시드");
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
// 기존 테스트 사용자 삭제 (재실행 가능하게)
await client.query(`DELETE FROM momo_order_items WHERE order_objid IN (SELECT objid FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1))`, [TEST_EMAIL]);
await client.query(`DELETE FROM momo_orders WHERE customer_objid IN (SELECT objid FROM momo_users WHERE email = $1)`, [TEST_EMAIL]);
await client.query(`DELETE FROM momo_users WHERE email = $1`, [TEST_EMAIL]);
// 품목 / 재고 시드 (테스트 품목 3개 + 본사창고 재고 100개씩)
const items = [
{ code: "TEST-EGG-01", name: "M 유정란", price: 10000, taxFree: "Y" },
{ code: "TEST-CHKN-01", name: "M 꽃계탕", price: 4500, taxFree: "Y" },
{ code: "TEST-CLEAN-01", name: "빨강 탈취제", price: 9200, taxFree: "N" },
];
for (const it of items) {
await client.query(
`INSERT INTO momo_items (objid, item_code, item_name, unit, unit_price, is_tax_free, status, regdate)
VALUES ($1, $2, $3, 'EA', $4, $5, 'ACTIVE', NOW())
ON CONFLICT (item_code) DO UPDATE SET unit_price = EXCLUDED.unit_price, is_tax_free = EXCLUDED.is_tax_free, status='ACTIVE', is_del='N'`,
[`TEST-${it.code}`, it.code, it.name, it.price, it.taxFree]
);
}
const wh = await client.query(`SELECT objid FROM momo_warehouses WHERE wh_type='STOCK' LIMIT 1`);
const whObjid = wh.rows[0].objid;
for (const it of items) {
await client.query(
`INSERT INTO momo_stocks (objid, wh_objid, item_objid, qty, update_date)
VALUES ($1, $2, (SELECT objid FROM momo_items WHERE item_code = $3), 100, NOW())
ON CONFLICT (wh_objid, item_objid) DO UPDATE SET qty = 100, update_date = NOW()`,
[`TEST-STK-${it.code}`, whObjid, it.code]
);
}
log(" 품목 3개 + 재고 100개씩 시드 완료");
// ===== 2. 가입 =====
log("2. 회원가입");
const sup = await api("/api/auth/signup", {
method: "POST",
body: JSON.stringify({
email: TEST_EMAIL,
password: TEST_PASSWORD,
companyName: "테스트거래처(wace)",
ceoName: "박철현",
bizNo: "123-45-67890",
phone: "010-1234-5678",
}),
});
if (!sup.body.success) fail(`가입 실패: ${JSON.stringify(sup.body)}`);
log(` ✔ 가입 성공 (USER 세션 발급, cookie=${cookieJar.slice(0, 30)}...)`);
// ===== 3. 품목 조회 (USER 세션) =====
log("3. 품목 검색 (USER)");
const list = await api("/api/m/items/list", { method: "POST", body: JSON.stringify({ keyword: "TEST" }) });
const visible = (list.body.RESULTLIST || []).filter((r) => r.ITEM_CODE.startsWith("TEST-"));
if (visible.length !== 3) fail(`품목 조회 실패: ${visible.length}개 (기대 3개)`);
log(`${visible.length}개 품목 노출, 재고 ${visible[0].STOCK_QTY}`);
// ===== 4. 발주 작성 =====
log("4. 발주 요청");
const order = await api("/api/m/orders/save", {
method: "POST",
body: JSON.stringify({
lines: [
{ itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-EGG-01").OBJID, qty: 30 },
{ itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CHKN-01").OBJID, qty: 20 },
{ itemObjid: visible.find((x) => x.ITEM_CODE === "TEST-CLEAN-01").OBJID, qty: 11 },
],
memo: "E2E 테스트 발주",
}),
});
if (!order.body.success) fail(`발주 실패: ${JSON.stringify(order.body)}`);
log(` ✔ 발주번호: ${order.body.orderNo}, objId: ${order.body.objId}`);
// ===== 5. 어드민 로그인 (시드 관리자) =====
log("5. 관리자 로그인");
cookieJar = ""; // USER 세션 클리어
const adminLogin = await api("/api/auth/login", {
method: "POST",
body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }),
});
if (!adminLogin.body.success) {
// 시드 비밀번호 해시가 환경에 따라 다를 수 있어 → DB에서 비밀번호 재설정
log(" 시드 비번 불일치 — bcrypt 재설정");
const bcrypt = (await import("bcryptjs")).default;
const hash = await bcrypt.hash("admin1234", 10);
await client.query(`UPDATE momo_users SET password_hash = $1 WHERE email = 'admin@momo.com'`, [hash]);
const retry = await api("/api/auth/login", {
method: "POST",
body: JSON.stringify({ userId: "admin@momo.com", password: "admin1234" }),
});
if (!retry.body.success) fail(`관리자 로그인 실패: ${JSON.stringify(retry.body)}`);
}
log(` ✔ 관리자 세션 발급`);
// ===== 6. 승인 + 메일 발송 =====
log("6. 발주 승인 (재고차감 + 메일발송)");
const approve = await api("/api/m/orders/approve", {
method: "POST",
body: JSON.stringify({ objid: order.body.objId }),
});
if (!approve.body.success) fail(`승인 실패: ${JSON.stringify(approve.body)}`);
log(` ✔ 승인 완료, mailSent=${approve.body.mailSent}, mailError=${approve.body.mailError ?? "(none)"}`);
// ===== 7. 결과 검증 =====
log("7. 후속 검증");
const r1 = await client.query(`SELECT status, total_amount, total_taxfree, total_taxable FROM momo_orders WHERE objid = $1`, [order.body.objId]);
log(` 주문 상태: ${r1.rows[0].status} (기대 APPROVED)`);
log(` 금액: 면세 ${r1.rows[0].total_taxfree} / 과세 ${r1.rows[0].total_taxable} / 합계 ${r1.rows[0].total_amount}`);
if (r1.rows[0].status !== "APPROVED") fail("주문 상태 불일치");
const r2 = await client.query(`SELECT qty FROM momo_stocks WHERE wh_objid = $1 AND item_objid = (SELECT objid FROM momo_items WHERE item_code = 'TEST-EGG-01')`, [whObjid]);
log(` M 유정란 재고: ${r2.rows[0].qty} (기대 70 = 100-30)`);
if (Number(r2.rows[0].qty) !== 70) fail("재고 차감 불일치");
const r3 = await client.query(`SELECT to_email, subject, status, error_msg FROM momo_mail_logs WHERE ref_objid = $1 ORDER BY regdate DESC LIMIT 1`, [order.body.objId]);
log(` 메일 로그: to=${r3.rows[0].to_email}, status=${r3.rows[0].status}, subject=${r3.rows[0].subject}`);
if (r3.rows[0].error_msg) log(` 메일 에러: ${r3.rows[0].error_msg}`);
log("✔ E2E 테스트 완료");
await client.end();