Add environment variable example and update .gitignore
- Created a new .env.example file to provide a template for environment variables, including database connection details, JWT settings, encryption keys, and external API keys. - Updated .gitignore to include additional test output directories and archive files, ensuring that unnecessary files are not tracked by Git. - Removed outdated approval test reports and scripts that are no longer needed, streamlining the project structure. These changes improve the clarity of environment configuration and maintain a cleaner repository.
This commit is contained in:
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
모든 ResizableDialogContent에 modalId와 userId를 추가하는 스크립트
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def process_file(file_path):
|
||||
"""파일을 처리하여 modalId와 userId를 추가"""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
modified = False
|
||||
|
||||
# 파일명에서 modalId 생성 (예: UserFormModal.tsx -> user-form-modal)
|
||||
file_name = Path(file_path).stem
|
||||
modal_id = re.sub(r'(?<!^)(?=[A-Z])', '-', file_name).lower()
|
||||
|
||||
# useAuth import 확인
|
||||
has_use_auth = 'useAuth' in content
|
||||
|
||||
# useAuth import 추가 (없으면)
|
||||
if not has_use_auth and 'ResizableDialogContent' in content:
|
||||
# import 섹션 찾기
|
||||
import_match = re.search(r'(import.*from.*;\n)', content)
|
||||
if import_match:
|
||||
last_import_pos = content.rfind('import')
|
||||
next_newline = content.find('\n', last_import_pos)
|
||||
if next_newline != -1:
|
||||
content = (
|
||||
content[:next_newline + 1] +
|
||||
'import { useAuth } from "@/hooks/useAuth";\n' +
|
||||
content[next_newline + 1:]
|
||||
)
|
||||
modified = True
|
||||
|
||||
# 함수 컴포넌트 내부에 useAuth 추가
|
||||
# 패턴: export default function ComponentName() { 또는 const ComponentName = () => {
|
||||
if 'ResizableDialogContent' in content and 'const { user } = useAuth();' not in content:
|
||||
# 함수 시작 부분 찾기
|
||||
patterns = [
|
||||
r'(export default function \w+\([^)]*\)\s*\{)',
|
||||
r'(export function \w+\([^)]*\)\s*\{)',
|
||||
r'(const \w+ = \([^)]*\)\s*=>\s*\{)',
|
||||
r'(function \w+\([^)]*\)\s*\{)',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, content)
|
||||
if match:
|
||||
insert_pos = match.end()
|
||||
# 이미 useAuth가 있는지 확인
|
||||
next_100_chars = content[insert_pos:insert_pos + 200]
|
||||
if 'useAuth' not in next_100_chars:
|
||||
content = (
|
||||
content[:insert_pos] +
|
||||
'\n const { user } = useAuth();' +
|
||||
content[insert_pos:]
|
||||
)
|
||||
modified = True
|
||||
break
|
||||
|
||||
# ResizableDialogContent에 modalId와 userId 추가
|
||||
# 패턴: <ResizableDialogContent ... > (modalId가 없는 경우)
|
||||
pattern = r'<ResizableDialogContent\s+([^>]*?)(?<!modalId=")>'
|
||||
|
||||
def add_props(match):
|
||||
nonlocal modified
|
||||
props = match.group(1).strip()
|
||||
|
||||
# 이미 modalId가 있는지 확인
|
||||
if 'modalId=' in props:
|
||||
return match.group(0)
|
||||
|
||||
# props가 있으면 끝에 추가, 없으면 새로 추가
|
||||
if props:
|
||||
if not props.endswith(' '):
|
||||
props += ' '
|
||||
new_props = f'{props}modalId="{modal_id}" userId={{user?.userId}}'
|
||||
else:
|
||||
new_props = f'modalId="{modal_id}" userId={{user?.userId}}'
|
||||
|
||||
modified = True
|
||||
return f'<ResizableDialogContent {new_props}>'
|
||||
|
||||
content = re.sub(pattern, add_props, content)
|
||||
|
||||
# 변경사항이 있으면 파일 저장
|
||||
if modified and content != original_content:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""메인 함수"""
|
||||
frontend_dir = Path('frontend/components')
|
||||
|
||||
if not frontend_dir.exists():
|
||||
print(f"❌ 디렉토리를 찾을 수 없습니다: {frontend_dir}")
|
||||
return
|
||||
|
||||
# 모든 .tsx 파일 찾기
|
||||
tsx_files = list(frontend_dir.rglob('*.tsx'))
|
||||
|
||||
modified_files = []
|
||||
|
||||
for file_path in tsx_files:
|
||||
# ResizableDialogContent가 있는 파일만 처리
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
if 'ResizableDialogContent' not in content:
|
||||
continue
|
||||
|
||||
if process_file(file_path):
|
||||
modified_files.append(file_path)
|
||||
print(f"✅ {file_path}")
|
||||
|
||||
print(f"\n🎉 총 {len(modified_files)}개 파일 수정 완료!")
|
||||
|
||||
if modified_files:
|
||||
print("\n수정된 파일 목록:")
|
||||
for f in modified_files:
|
||||
print(f" - {f}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* 회사 기본정보 화면 - 컴포넌트 렌더링 비율 정밀 분석
|
||||
*
|
||||
* 사용법: 브라우저에서 회사 기본정보 화면을 연 상태에서
|
||||
* F12 → Console 탭 → 이 스크립트 전체를 붙여넣고 Enter
|
||||
*/
|
||||
|
||||
(function analyzeLayout() {
|
||||
const results = { part1: null, part2: null };
|
||||
|
||||
// ========== Part 1: DesktopCanvasRenderer 구조 확인 ==========
|
||||
const runtime = document.querySelector('[data-screen-runtime="true"]');
|
||||
if (!runtime) {
|
||||
console.warn('⚠️ [data-screen-runtime="true"] 요소를 찾을 수 없습니다.');
|
||||
console.log('대안: ScreenModal 기반 렌더링이거나 다른 구조일 수 있습니다.');
|
||||
results.part1 = { error: 'Runtime not found' };
|
||||
} else {
|
||||
const rect = runtime.getBoundingClientRect();
|
||||
const inner = runtime.firstElementChild;
|
||||
|
||||
results.part1 = {
|
||||
runtimeContainer: { width: rect.width, height: rect.height },
|
||||
innerDiv: null,
|
||||
components: [],
|
||||
};
|
||||
|
||||
if (inner) {
|
||||
const style = inner.style;
|
||||
results.part1.innerDiv = {
|
||||
width: style.width,
|
||||
height: style.height,
|
||||
transform: style.transform,
|
||||
transformOrigin: style.transformOrigin,
|
||||
position: style.position,
|
||||
};
|
||||
|
||||
const comps = inner.querySelectorAll('[data-component-id]');
|
||||
comps.forEach((comp) => {
|
||||
const s = comp.style;
|
||||
const r = comp.getBoundingClientRect();
|
||||
results.part1.components.push({
|
||||
type: comp.getAttribute('data-component-type'),
|
||||
id: comp.getAttribute('data-component-id'),
|
||||
stylePos: `(${s.left}, ${s.top})`,
|
||||
styleSize: `${s.width} x ${s.height}`,
|
||||
renderedSize: `${Math.round(r.width)} x ${Math.round(r.height)}`,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// ResponsiveGridRenderer (flex 기반) 구조일 수 있음 - 행 단위로 확인
|
||||
const rows = runtime.querySelectorAll(':scope > div');
|
||||
results.part1.rows = [];
|
||||
rows.forEach((row, i) => {
|
||||
const children = row.children;
|
||||
const rowData = { rowIndex: i, childCount: children.length, children: [] };
|
||||
Array.from(children).forEach((child, j) => {
|
||||
const cs = window.getComputedStyle(child);
|
||||
const r = child.getBoundingClientRect();
|
||||
rowData.children.push({
|
||||
type: child.getAttribute('data-component-type') || 'unknown',
|
||||
width: Math.round(r.width),
|
||||
height: Math.round(r.height),
|
||||
flexGrow: cs.flexGrow,
|
||||
flexBasis: cs.flexBasis,
|
||||
});
|
||||
});
|
||||
results.part1.rows.push(rowData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Part 2: wrapper vs child 크기 확인 ==========
|
||||
const comps = document.querySelectorAll('[data-component-id]');
|
||||
results.part2 = [];
|
||||
comps.forEach((comp) => {
|
||||
const type = comp.getAttribute('data-component-type');
|
||||
const child = comp.firstElementChild;
|
||||
if (child) {
|
||||
const childRect = child.getBoundingClientRect();
|
||||
const compRect = comp.getBoundingClientRect();
|
||||
results.part2.push({
|
||||
type,
|
||||
wrapper: `${Math.round(compRect.width)}x${Math.round(compRect.height)}`,
|
||||
child: `${Math.round(childRect.width)}x${Math.round(childRect.height)}`,
|
||||
overflow: childRect.width > compRect.width ? 'YES' : 'no',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ========== 결과 출력 ==========
|
||||
console.log('========== Part 1: Runtime 구조 ==========');
|
||||
console.log(JSON.stringify(results.part1, null, 2));
|
||||
|
||||
console.log('\n========== Part 2: Wrapper vs Child ==========');
|
||||
results.part2.forEach((r) => {
|
||||
console.log(`${r.type}: wrapper=${r.wrapper}, child=${r.child}, overflow=${r.overflow}`);
|
||||
});
|
||||
|
||||
// scale 값 추출 (transform에서)
|
||||
if (results.part1?.innerDiv?.transform) {
|
||||
const m = results.part1.innerDiv.transform.match(/scale\(([^)]+)\)/);
|
||||
if (m) console.log('\n📐 Scale 값:', m[1]);
|
||||
}
|
||||
|
||||
return results;
|
||||
})();
|
||||
@@ -1,170 +0,0 @@
|
||||
/**
|
||||
* 프로덕션에서 "관리자 메뉴로 전환" 버튼 가시성 테스트
|
||||
* 두 계정 (topseal_admin, rsw1206)으로 로그인하여 버튼 표시 여부 확인
|
||||
*
|
||||
* 실행: node scripts/browser-test-admin-switch-button.js
|
||||
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-admin-switch-button.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
const fs = require("fs");
|
||||
|
||||
const BASE_URL = "https://v1.vexplor.com";
|
||||
const SCREENSHOT_DIR = "test-screenshots/admin-switch-test";
|
||||
|
||||
const ACCOUNTS = [
|
||||
{ userId: "topseal_admin", password: "qlalfqjsgh11", name: "topseal_admin" },
|
||||
{ userId: "rsw1206", password: "qlalfqjsgh11", name: "rsw1206" },
|
||||
];
|
||||
|
||||
async function runTest() {
|
||||
const results = { topseal_admin: {}, rsw1206: {} };
|
||||
const browser = await chromium.launch({
|
||||
headless: process.env.HEADLESS !== "0",
|
||||
});
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 900 },
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const screenshot = async (name) => {
|
||||
const path = `${SCREENSHOT_DIR}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
console.log(` [스크린샷] ${path}`);
|
||||
return path;
|
||||
};
|
||||
|
||||
for (let i = 0; i < ACCOUNTS.length; i++) {
|
||||
const acc = ACCOUNTS[i];
|
||||
console.log(`\n========== ${acc.name} 테스트 (${i + 1}/${ACCOUNTS.length}) ==========\n`);
|
||||
|
||||
// 로그인 페이지로 이동
|
||||
await page.goto(`${BASE_URL}/login`, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 20000,
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 로그인
|
||||
await page.fill("#userId", acc.userId);
|
||||
await page.fill("#password", acc.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 로그인 성공 시 대시보드 또는 메인으로 리다이렉트될 것임
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("/login") && !currentUrl.includes("error")) {
|
||||
// 아직 로그인 페이지에 있다면 조금 더 대기
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
const afterLoginUrl = page.url();
|
||||
const screenshotPath = await screenshot(`01_${acc.name}_after_login`);
|
||||
|
||||
// "관리자 메뉴로 전환" 버튼 찾기
|
||||
const buttonSelectors = [
|
||||
'button:has-text("관리자 메뉴로 전환")',
|
||||
'[class*="button"]:has-text("관리자 메뉴로 전환")',
|
||||
'button >> text=관리자 메뉴로 전환',
|
||||
];
|
||||
|
||||
let buttonVisible = false;
|
||||
for (const sel of buttonSelectors) {
|
||||
try {
|
||||
const btn = page.locator(sel).first();
|
||||
const count = await btn.count();
|
||||
if (count > 0) {
|
||||
const isVisible = await btn.isVisible();
|
||||
if (isVisible) {
|
||||
buttonVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 추가: 페이지 내 텍스트로 버튼 존재 여부 확인
|
||||
if (!buttonVisible) {
|
||||
const pageText = await page.textContent("body");
|
||||
buttonVisible = pageText && pageText.includes("관리자 메뉴로 전환");
|
||||
}
|
||||
|
||||
results[acc.name] = {
|
||||
buttonVisible,
|
||||
screenshotPath,
|
||||
afterLoginUrl,
|
||||
};
|
||||
|
||||
console.log(` 버튼 가시성: ${buttonVisible ? "표시됨" : "표시 안 됨"}`);
|
||||
console.log(` URL: ${afterLoginUrl}`);
|
||||
|
||||
// 로그아웃 (다음 계정 테스트 전)
|
||||
if (i < ACCOUNTS.length - 1) {
|
||||
console.log("\n 로그아웃 중...");
|
||||
try {
|
||||
// 프로필 드롭다운 클릭 (좌측 하단)
|
||||
const profileBtn = page.locator(
|
||||
'button:has-text("로그아웃"), [class*="dropdown"]:has-text("로그아웃"), [data-radix-collection-item]:has-text("로그아웃")'
|
||||
);
|
||||
const profileTrigger = page.locator(
|
||||
'button[class*="flex w-full"][class*="gap-3"]'
|
||||
).first();
|
||||
if (await profileTrigger.count() > 0) {
|
||||
await profileTrigger.click();
|
||||
await page.waitForTimeout(500);
|
||||
const logoutItem = page.locator('text=로그아웃').first();
|
||||
if (await logoutItem.count() > 0) {
|
||||
await logoutItem.click();
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
// 또는 직접 로그아웃 URL
|
||||
if (page.url().includes("/login") === false) {
|
||||
await page.goto(`${BASE_URL}/api/auth/logout`, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 5000,
|
||||
}).catch(() => {});
|
||||
await page.goto(`${BASE_URL}/login`, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(" 로그아웃 대체: 로그인 페이지로 직접 이동");
|
||||
await page.goto(`${BASE_URL}/login`, {
|
||||
waitUntil: "networkidle",
|
||||
timeout: 10000,
|
||||
});
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n========== 최종 결과 ==========\n");
|
||||
console.log("topseal_admin: 관리자 메뉴로 전환 버튼 =", results.topseal_admin.buttonVisible ? "표시됨" : "표시 안 됨");
|
||||
console.log("rsw1206: 관리자 메뉴로 전환 버튼 =", results.rsw1206.buttonVisible ? "표시됨" : "표시 안 됨");
|
||||
console.log("\n스크린샷:", SCREENSHOT_DIR);
|
||||
|
||||
return results;
|
||||
} catch (err) {
|
||||
console.error("테스트 오류:", err);
|
||||
throw err;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
runTest()
|
||||
.then((r) => {
|
||||
console.log("\n테스트 완료.");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* 거래처관리 화면 CRUD 브라우저 테스트
|
||||
* 실행: node scripts/browser-test-customer-crud.js
|
||||
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-customer-crud.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = "test-screenshots";
|
||||
|
||||
async function runTest() {
|
||||
const results = { success: [], failed: [], screenshots: [] };
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// 스크린샷 디렉토리
|
||||
const fs = require("fs");
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const screenshot = async (name) => {
|
||||
const path = `${SCREENSHOT_DIR}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
results.screenshots.push(path);
|
||||
console.log(` [스크린샷] ${path}`);
|
||||
};
|
||||
|
||||
console.log("\n=== 1단계: 로그인 ===\n");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.fill('#userId', 'topseal_admin');
|
||||
await page.fill('#password', 'qlalfqjsgh11');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
await screenshot("01_after_login");
|
||||
results.success.push("로그인 완료");
|
||||
|
||||
console.log("\n=== 2단계: 거래처관리 화면 이동 ===\n");
|
||||
await page.goto(`${BASE_URL}/screens/227`, { waitUntil: "domcontentloaded", timeout: 20000 });
|
||||
// 테이블 또는 메인 콘텐츠 로딩 대기 (API 호출 후 React 렌더링)
|
||||
try {
|
||||
await page.waitForSelector('table, tbody, [role="row"], .rt-tbody', { timeout: 25000 });
|
||||
results.success.push("테이블 로드 감지");
|
||||
} catch (e) {
|
||||
console.log(" [경고] 테이블 대기 타임아웃, 계속 진행");
|
||||
}
|
||||
await page.waitForTimeout(3000);
|
||||
await screenshot("02_screen_227");
|
||||
results.success.push("화면 227 로드");
|
||||
|
||||
console.log("\n=== 3단계: 거래처 선택 (READ 테스트) ===\n");
|
||||
// 좌측 테이블 행 선택 - 다양한 레이아웃 대응
|
||||
const rowSelectors = [
|
||||
'table tbody tr.cursor-pointer',
|
||||
'tbody tr.hover\\:bg-accent',
|
||||
'table tbody tr:has(td)',
|
||||
'tbody tr',
|
||||
];
|
||||
let rows = [];
|
||||
for (const sel of rowSelectors) {
|
||||
rows = await page.$$(sel);
|
||||
if (rows.length > 0) break;
|
||||
}
|
||||
if (rows.length > 0) {
|
||||
await rows[0].click();
|
||||
results.success.push("거래처 행 클릭");
|
||||
} else {
|
||||
results.failed.push("거래처 테이블 행을 찾을 수 없음");
|
||||
// 디버그: 페이지 구조 저장
|
||||
const bodyHtml = await page.evaluate(() => {
|
||||
const tables = document.querySelectorAll('table, tbody, [role="grid"], [role="table"]');
|
||||
return `Tables found: ${tables.length}\n` + document.body.innerHTML.slice(0, 8000);
|
||||
});
|
||||
require("fs").writeFileSync(`${SCREENSHOT_DIR}/debug_body.html`, bodyHtml);
|
||||
console.log(" [디버그] body HTML 일부 저장: debug_body.html");
|
||||
}
|
||||
await page.waitForTimeout(3000);
|
||||
await screenshot("03_after_customer_select");
|
||||
|
||||
// SelectedItemsDetailInput 영역 확인
|
||||
const detailArea = await page.$('[data-component="selected-items-detail-input"], [class*="selected-items"], .selected-items-detail');
|
||||
if (detailArea) {
|
||||
results.success.push("SelectedItemsDetailInput 컴포넌트 렌더링 확인");
|
||||
} else {
|
||||
// 품목/입력 관련 영역이 있는지
|
||||
const hasInputArea = await page.$('input[placeholder*="품번"], input[placeholder*="품목"], [class*="detail"]');
|
||||
results.success.push(hasInputArea ? "입력 영역 확인됨" : "SelectedItemsDetailInput 영역 미확인");
|
||||
}
|
||||
|
||||
console.log("\n=== 4단계: 품목 추가 (CREATE 테스트) ===\n");
|
||||
const addBtnLoc = page.locator('button').filter({ hasText: /추가|품목/ }).first();
|
||||
const addBtnExists = await addBtnLoc.count() > 0;
|
||||
if (addBtnExists) {
|
||||
await addBtnLoc.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await screenshot("04_after_add_click");
|
||||
|
||||
// 모달/팝업에서 품목 선택
|
||||
const modalItem = await page.$('[role="dialog"] tr, [role="listbox"] [role="option"], .modal tbody tr');
|
||||
if (modalItem) {
|
||||
await modalItem.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// 필수 필드 입력
|
||||
const itemCodeInput = await page.$('input[name*="품번"], input[placeholder*="품번"], input[id*="item"]');
|
||||
if (itemCodeInput) {
|
||||
await itemCodeInput.fill("TEST_BROWSER");
|
||||
}
|
||||
await screenshot("04_before_save");
|
||||
|
||||
const saveBtnLoc = page.locator('button').filter({ hasText: /저장/ }).first();
|
||||
if (await saveBtnLoc.count() > 0) {
|
||||
await saveBtnLoc.click();
|
||||
await page.waitForTimeout(3000);
|
||||
await screenshot("05_after_save");
|
||||
results.success.push("저장 버튼 클릭");
|
||||
|
||||
const toast = await page.$('[data-sonner-toast], .toast, [role="alert"]');
|
||||
if (toast) {
|
||||
const toastText = await toast.textContent();
|
||||
results.success.push(`토스트 메시지: ${toastText?.slice(0, 50)}`);
|
||||
}
|
||||
} else {
|
||||
results.failed.push("저장 버튼을 찾을 수 없음");
|
||||
}
|
||||
} else {
|
||||
results.failed.push("품목 추가/추가 버튼을 찾을 수 없음");
|
||||
await screenshot("04_no_add_button");
|
||||
}
|
||||
|
||||
console.log("\n=== 5단계: 최종 결과 ===\n");
|
||||
await screenshot("06_final_state");
|
||||
|
||||
// 콘솔 에러 수집
|
||||
const consoleErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
const type = msg.type();
|
||||
if (type === "error") {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
results.failed.push(`예외: ${err.message}`);
|
||||
try {
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
|
||||
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
|
||||
} catch (_) {}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// 결과 출력
|
||||
console.log("\n========== 테스트 결과 ==========\n");
|
||||
console.log("성공:", results.success);
|
||||
console.log("실패:", results.failed);
|
||||
console.log("스크린샷:", results.screenshots);
|
||||
return results;
|
||||
}
|
||||
|
||||
runTest().then((r) => {
|
||||
process.exit(r.failed.length > 0 ? 1 : 0);
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,157 +0,0 @@
|
||||
/**
|
||||
* 거래처관리 메뉴 경유 브라우저 테스트
|
||||
* 영업관리 > 거래처관리 메뉴 클릭 후 상세 화면 진입
|
||||
* 실행: node scripts/browser-test-customer-via-menu.js
|
||||
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-customer-via-menu.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = "test-screenshots";
|
||||
const CREDENTIALS = { userId: "topseal_admin", password: "qlalfqjsgh11" };
|
||||
|
||||
async function runTest() {
|
||||
const results = { success: [], failed: [], screenshots: [] };
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const fs = require("fs");
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const screenshot = async (name) => {
|
||||
const path = `${SCREENSHOT_DIR}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
results.screenshots.push(path);
|
||||
console.log(` [스크린샷] ${path}`);
|
||||
};
|
||||
|
||||
try {
|
||||
// 로그인 (이미 로그인된 상태면 자동 리다이렉트됨)
|
||||
console.log("\n=== 로그인 확인 ===\n");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("/login") && !(await page.$('input#userId'))) {
|
||||
// 로그인 폼이 있으면 로그인
|
||||
await page.fill("#userId", CREDENTIALS.userId);
|
||||
await page.fill("#password", CREDENTIALS.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
} else if (currentUrl.includes("/login")) {
|
||||
await page.fill("#userId", CREDENTIALS.userId);
|
||||
await page.fill("#password", CREDENTIALS.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
results.success.push("로그인/세션 확인");
|
||||
|
||||
// 단계 1: 영업관리 메뉴 클릭
|
||||
console.log("\n=== 단계 1: 영업관리 메뉴 클릭 ===\n");
|
||||
const salesMenu = page.locator('nav, aside').getByText('영업관리', { exact: true }).first();
|
||||
if (await salesMenu.count() > 0) {
|
||||
await salesMenu.click();
|
||||
await page.waitForTimeout(2000);
|
||||
results.success.push("영업관리 메뉴 클릭");
|
||||
} else {
|
||||
const salesAlt = page.getByRole('button', { name: /영업관리/ }).or(page.getByText('영업관리').first());
|
||||
if (await salesAlt.count() > 0) {
|
||||
await salesAlt.first().click();
|
||||
await page.waitForTimeout(2000);
|
||||
results.success.push("영업관리 메뉴 클릭 (대안)");
|
||||
} else {
|
||||
results.failed.push("영업관리 메뉴를 찾을 수 없음");
|
||||
}
|
||||
}
|
||||
await screenshot("01_after_sales_menu");
|
||||
|
||||
// 단계 2: 거래처관리 서브메뉴 클릭
|
||||
console.log("\n=== 단계 2: 거래처관리 서브메뉴 클릭 ===\n");
|
||||
const customerMenu = page.getByText("거래처관리", { exact: true }).first();
|
||||
if (await customerMenu.count() > 0) {
|
||||
await customerMenu.click();
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push("거래처관리 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("거래처관리 메뉴를 찾을 수 없음");
|
||||
}
|
||||
await screenshot("02_after_customer_menu");
|
||||
|
||||
// 단계 3: 거래처 목록 확인 및 행 클릭
|
||||
console.log("\n=== 단계 3: 거래처 목록 확인 ===\n");
|
||||
const rows = await page.$$('tbody tr, table tr, [role="row"]');
|
||||
const clickableRows = rows.length > 0 ? rows : [];
|
||||
if (clickableRows.length > 0) {
|
||||
await clickableRows[0].click();
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push(`거래처 행 클릭 (${clickableRows.length}개 행 중 첫 번째)`);
|
||||
} else {
|
||||
results.failed.push("거래처 테이블 행을 찾을 수 없음");
|
||||
}
|
||||
await screenshot("03_after_row_click");
|
||||
|
||||
// 단계 4: 편집/수정 버튼 또는 더블클릭 (분할 패널이면 행 선택만으로 우측에 상세 표시될 수 있음)
|
||||
console.log("\n=== 단계 4: 상세 화면 진입 시도 ===\n");
|
||||
const editBtn = page.locator('button').filter({ hasText: /편집|수정|상세/ }).first();
|
||||
let editEnabled = false;
|
||||
try {
|
||||
if (await editBtn.count() > 0) {
|
||||
editEnabled = !(await editBtn.isDisabled());
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
if (editEnabled) {
|
||||
await editBtn.click();
|
||||
results.success.push("편집/수정 버튼 클릭");
|
||||
} else {
|
||||
const row = await page.$('tbody tr, table tr');
|
||||
if (row) {
|
||||
await row.dblclick();
|
||||
results.success.push("행 더블클릭 시도");
|
||||
} else if (await editBtn.count() > 0) {
|
||||
results.success.push("수정 버튼 비활성화 - 분할 패널 우측 상세 확인");
|
||||
} else {
|
||||
results.failed.push("편집 버튼/행을 찾을 수 없음");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
results.success.push("상세 진입 스킵 - 우측 패널에 상세 표시 여부 확인");
|
||||
}
|
||||
await page.waitForTimeout(5000);
|
||||
await screenshot("04_after_detail_enter");
|
||||
|
||||
// 단계 5: 품목 관련 영역 확인
|
||||
console.log("\n=== 단계 5: 품목 관련 영역 확인 ===\n");
|
||||
const hasItemSection = await page.getByText(/품목|납품품목|거래처 품번|거래처 품명/).first().count() > 0;
|
||||
const hasDetailInput = await page.$('input[placeholder*="품번"], input[name*="품번"], [class*="selected-items"]');
|
||||
if (hasItemSection || hasDetailInput) {
|
||||
results.success.push("품목 관련 UI 확인됨");
|
||||
} else {
|
||||
results.failed.push("품목 관련 영역 미확인");
|
||||
}
|
||||
await screenshot("05_item_section");
|
||||
|
||||
console.log("\n========== 테스트 결과 ==========\n");
|
||||
console.log("성공:", results.success);
|
||||
console.log("실패:", results.failed);
|
||||
console.log("스크린샷:", results.screenshots);
|
||||
|
||||
} catch (err) {
|
||||
results.failed.push(`예외: ${err.message}`);
|
||||
try {
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
|
||||
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
|
||||
} catch (_) {}
|
||||
console.error(err);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
runTest()
|
||||
.then((r) => process.exit(r.failed.length > 0 ? 1 : 0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,196 +0,0 @@
|
||||
/**
|
||||
* 구매관리 - 공급업체관리 / 구매품목정보 CRUD 브라우저 테스트
|
||||
* 실행: node scripts/browser-test-purchase-supplier.js
|
||||
* 브라우저 표시: HEADLESS=0 node scripts/browser-test-purchase-supplier.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = "test-screenshots";
|
||||
const CREDENTIALS = { userId: "topseal_admin", password: "qlalfqjsgh11" };
|
||||
|
||||
async function runTest() {
|
||||
const results = { success: [], failed: [], screenshots: [] };
|
||||
const browser = await chromium.launch({ headless: process.env.HEADLESS !== "0" });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const fs = require("fs");
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const screenshot = async (name) => {
|
||||
const path = `${SCREENSHOT_DIR}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
results.screenshots.push(path);
|
||||
console.log(` [스크린샷] ${path}`);
|
||||
return path;
|
||||
};
|
||||
|
||||
const clickMenu = async (text) => {
|
||||
const loc = page.getByText(text, { exact: true }).first();
|
||||
if ((await loc.count()) > 0) {
|
||||
await loc.click();
|
||||
return true;
|
||||
}
|
||||
const alt = page.getByRole("link", { name: text }).or(page.locator(`a:has-text("${text}")`)).first();
|
||||
if ((await alt.count()) > 0) {
|
||||
await alt.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const clickRow = async () => {
|
||||
const rows = await page.$$('tbody tr, table tr, [role="row"]');
|
||||
for (const r of rows) {
|
||||
const t = await r.textContent();
|
||||
if (t && !t.includes("데이터가 없습니다") && !t.includes("로딩")) {
|
||||
await r.click();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (rows.length > 0) {
|
||||
await rows[0].click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const clickButton = async (regex) => {
|
||||
const btn = page.locator("button").filter({ hasText: regex }).first();
|
||||
try {
|
||||
if ((await btn.count()) > 0 && !(await btn.isDisabled())) {
|
||||
await btn.click();
|
||||
return true;
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
try {
|
||||
console.log("\n=== 로그인 확인 ===\n");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 15000 });
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", CREDENTIALS.userId);
|
||||
await page.fill("#password", CREDENTIALS.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
results.success.push("세션 확인");
|
||||
|
||||
// ========== 테스트 1: 공급업체관리 ==========
|
||||
console.log("\n=== 테스트 1: 공급업체관리 ===\n");
|
||||
|
||||
console.log("단계 1: 구매관리 메뉴 열기");
|
||||
if (await clickMenu("구매관리")) {
|
||||
await page.waitForTimeout(3000);
|
||||
results.success.push("구매관리 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("구매관리 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p1_01_purchase_menu");
|
||||
|
||||
console.log("단계 2: 공급업체관리 서브메뉴 클릭");
|
||||
if (await clickMenu("공급업체관리")) {
|
||||
await page.waitForTimeout(8000);
|
||||
results.success.push("공급업체관리 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("공급업체관리 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p1_02_supplier_screen");
|
||||
|
||||
console.log("단계 3: 공급업체 선택");
|
||||
if (await clickRow()) {
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push("공급업체 행 클릭");
|
||||
} else {
|
||||
results.failed.push("공급업체 테이블 행 미발견");
|
||||
}
|
||||
await screenshot("p1_03_after_supplier_select");
|
||||
|
||||
console.log("단계 4: 납품품목 탭/영역 확인");
|
||||
const itemTab = page.getByText(/납품품목|품목/).first();
|
||||
if ((await itemTab.count()) > 0) {
|
||||
await itemTab.click();
|
||||
await page.waitForTimeout(3000);
|
||||
results.success.push("납품품목/품목 탭 클릭");
|
||||
} else {
|
||||
results.failed.push("납품품목 탭 미발견");
|
||||
}
|
||||
await screenshot("p1_04_item_tab");
|
||||
|
||||
console.log("단계 5: 품목 추가 시도");
|
||||
const addBtn = page.locator("button").filter({ hasText: /추가|\+ 추가/ }).first();
|
||||
let addBtnEnabled = false;
|
||||
try {
|
||||
addBtnEnabled = (await addBtn.count()) > 0 && !(await addBtn.isDisabled());
|
||||
} catch (_) {}
|
||||
if (addBtnEnabled) {
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(2000);
|
||||
const modal = await page.$('[role="dialog"], .modal, [class*="modal"]');
|
||||
if (modal) {
|
||||
const modalRow = await page.$('[role="dialog"] tbody tr, .modal tbody tr');
|
||||
if (modalRow) {
|
||||
await modalRow.click();
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
results.success.push("추가 버튼 클릭 및 품목 선택 시도");
|
||||
} else {
|
||||
results.failed.push("추가 버튼 미발견 또는 비활성화");
|
||||
}
|
||||
await screenshot("p1_05_add_item");
|
||||
|
||||
// ========== 테스트 2: 구매품목정보 ==========
|
||||
console.log("\n=== 테스트 2: 구매품목정보 ===\n");
|
||||
|
||||
console.log("단계 6: 구매품목정보 메뉴 클릭");
|
||||
if (await clickMenu("구매품목정보")) {
|
||||
await page.waitForTimeout(8000);
|
||||
results.success.push("구매품목정보 메뉴 클릭");
|
||||
} else {
|
||||
results.failed.push("구매품목정보 메뉴 미발견");
|
||||
}
|
||||
await screenshot("p2_01_item_screen");
|
||||
|
||||
console.log("단계 7: 품목 선택 및 공급업체 확인");
|
||||
if (await clickRow()) {
|
||||
await page.waitForTimeout(5000);
|
||||
results.success.push("구매품목 행 클릭");
|
||||
} else {
|
||||
results.failed.push("구매품목 테이블 행 미발견");
|
||||
}
|
||||
await screenshot("p2_02_after_item_select");
|
||||
|
||||
// SelectedItemsDetailInput 컴포넌트 확인
|
||||
const hasDetailInput = await page.$('input[placeholder*="품번"], [class*="selected-items"], input[name*="품번"]');
|
||||
results.success.push(hasDetailInput ? "SelectedItemsDetailInput 렌더링 확인" : "SelectedItemsDetailInput 미확인");
|
||||
await screenshot("p2_03_final");
|
||||
|
||||
console.log("\n========== 테스트 결과 ==========\n");
|
||||
console.log("성공:", results.success);
|
||||
console.log("실패:", results.failed);
|
||||
console.log("스크린샷:", results.screenshots);
|
||||
|
||||
} catch (err) {
|
||||
results.failed.push(`예외: ${err.message}`);
|
||||
try {
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/error.png`, fullPage: true });
|
||||
results.screenshots.push(`${SCREENSHOT_DIR}/error.png`);
|
||||
} catch (_) {}
|
||||
console.error(err);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
runTest()
|
||||
.then((r) => process.exit(r.failed.length > 0 ? 1 : 0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -95,7 +95,7 @@ echo ============================================
|
||||
echo [완료] 모든 서비스가 시작되었습니다!
|
||||
echo ============================================
|
||||
echo.
|
||||
echo [DATABASE] PostgreSQL: http://39.117.244.52:11132
|
||||
echo [DATABASE] PostgreSQL: http://211.115.91.141:11134
|
||||
echo [BACKEND] Node.js API: http://localhost:8080/api
|
||||
echo [FRONTEND] Next.js: http://localhost:9771
|
||||
echo.
|
||||
|
||||
@@ -150,7 +150,7 @@ Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host "[완료] 모든 서비스가 시작되었습니다!" -ForegroundColor Green
|
||||
Write-Host "============================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "[DATABASE] PostgreSQL: http://39.117.244.52:11132" -ForegroundColor White
|
||||
Write-Host "[DATABASE] PostgreSQL: http://211.115.91.141:11134" -ForegroundColor White
|
||||
Write-Host "[BACKEND] Node.js API: http://localhost:8080/api" -ForegroundColor White
|
||||
Write-Host "[FRONTEND] Next.js: http://localhost:9771" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
@@ -92,7 +92,7 @@ echo "============================================"
|
||||
echo "🎉 모든 서비스가 시작되었습니다!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "[DATABASE] PostgreSQL: http://39.117.244.52:11132"
|
||||
echo "[DATABASE] PostgreSQL: http://211.115.91.141:11134"
|
||||
echo "[BACKEND] Node.js API: http://localhost:8080/api"
|
||||
echo "[FRONTEND] Next.js: http://localhost:9771"
|
||||
echo ""
|
||||
|
||||
@@ -29,7 +29,7 @@ echo "============================================"
|
||||
echo "백엔드 서비스가 시작되었습니다!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "[DATABASE] PostgreSQL: http://39.117.244.52:11132"
|
||||
echo "[DATABASE] PostgreSQL: http://211.115.91.141:11134"
|
||||
echo "[BACKEND] Node.js API: http://localhost:8080/api"
|
||||
echo ""
|
||||
echo "상태 확인: docker-compose -f docker/dev/docker-compose.backend.mac.yml ps"
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* 메뉴 복사 자동화 스크립트
|
||||
*
|
||||
* 실행: npx ts-node scripts/menu-copy-automation.ts
|
||||
* 또는: npx playwright test scripts/menu-copy-automation.ts (playwright test 모드)
|
||||
*
|
||||
* 요구사항: playwright 설치 (npm install playwright)
|
||||
*/
|
||||
|
||||
import { chromium, type Browser, type Page } from "playwright";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../screenshots-menu-copy");
|
||||
|
||||
// 스크린샷 저장
|
||||
async function takeScreenshot(page: Page, stepName: string): Promise<string> {
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
const filename = `${Date.now()}_${stepName}.png`;
|
||||
const filepath = path.join(SCREENSHOT_DIR, filename);
|
||||
await page.screenshot({ path: filepath, fullPage: true });
|
||||
console.log(`[스크린샷] ${stepName} -> ${filepath}`);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let browser: Browser | null = null;
|
||||
const screenshots: { step: string; path: string }[] = [];
|
||||
|
||||
try {
|
||||
console.log("=== 메뉴 복사 자동화 시작 ===\n");
|
||||
|
||||
browser = await chromium.launch({ headless: false });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 900 },
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
// 1. 로그인
|
||||
console.log("1. 로그인 페이지 이동...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle" });
|
||||
await takeScreenshot(page, "01_login_page").then((p) =>
|
||||
screenshots.push({ step: "로그인 페이지", path: p })
|
||||
);
|
||||
|
||||
await page.fill('#userId', "admin");
|
||||
await page.fill('#password', "1234");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await takeScreenshot(page, "02_after_login").then((p) =>
|
||||
screenshots.push({ step: "로그인 후", path: p })
|
||||
);
|
||||
|
||||
// 로그인 실패 시 wace 계정 시도 (admin이 DB에 없을 수 있음)
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("/login")) {
|
||||
console.log("admin 로그인 실패, wace 계정으로 재시도...");
|
||||
await page.fill('#userId', "wace");
|
||||
await page.fill('#password', "1234");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// 2. 메뉴 관리 페이지로 이동
|
||||
console.log("2. 메뉴 관리 페이지 이동...");
|
||||
await page.goto(`${BASE_URL}/admin/menu`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(2000);
|
||||
await takeScreenshot(page, "03_menu_page").then((p) =>
|
||||
screenshots.push({ step: "메뉴 관리 페이지", path: p })
|
||||
);
|
||||
|
||||
// 3. 회사 선택 - 탑씰 (COMPANY_7)
|
||||
console.log("3. 회사 선택: 탑씰 (COMPANY_7)...");
|
||||
const companyDropdown = page.locator('.company-dropdown button, button:has(svg)').first();
|
||||
await companyDropdown.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const topsealOption = page.getByText("탑씰", { exact: false }).first();
|
||||
await topsealOption.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await takeScreenshot(page, "04_company_selected").then((p) =>
|
||||
screenshots.push({ step: "탑씰 선택 후", path: p })
|
||||
);
|
||||
|
||||
// 4. "사용자" 메뉴 찾기 및 복사 버튼 클릭
|
||||
console.log("4. 사용자 메뉴 찾기 및 복사 버튼 클릭...");
|
||||
const userMenuRow = page.locator('tr').filter({ hasText: "사용자" }).first();
|
||||
await userMenuRow.waitFor({ timeout: 10000 });
|
||||
const copyButton = userMenuRow.getByRole("button", { name: "복사" });
|
||||
await copyButton.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await takeScreenshot(page, "05_copy_dialog_open").then((p) =>
|
||||
screenshots.push({ step: "복사 다이얼로그", path: p })
|
||||
);
|
||||
|
||||
// 5. 대상 회사 선택: 두바이 강정 단단 (COMPANY_18)
|
||||
console.log("5. 대상 회사 선택: 두바이 강정 단단 (COMPANY_18)...");
|
||||
const targetCompanyTrigger = page.locator('[id="company"]').or(page.getByRole("combobox")).first();
|
||||
await targetCompanyTrigger.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const dubaiOption = page.getByText("두바이 강정 단단", { exact: false }).first();
|
||||
await dubaiOption.click();
|
||||
await page.waitForTimeout(500);
|
||||
await takeScreenshot(page, "06_target_company_selected").then((p) =>
|
||||
screenshots.push({ step: "대상 회사 선택 후", path: p })
|
||||
);
|
||||
|
||||
// 6. 복사 시작 버튼 클릭
|
||||
console.log("6. 복사 시작...");
|
||||
const copyStartButton = page.getByRole("button", { name: /복사 시작|확인/ }).first();
|
||||
await copyStartButton.click();
|
||||
|
||||
// 7. 복사 완료 대기 (최대 5분)
|
||||
console.log("7. 복사 완료 대기 (최대 5분)...");
|
||||
try {
|
||||
await page.waitForSelector('text=완료, text=성공, [role="status"]', { timeout: 300000 });
|
||||
await page.waitForTimeout(3000);
|
||||
} catch {
|
||||
console.log("타임아웃 또는 완료 메시지 대기 중...");
|
||||
}
|
||||
await takeScreenshot(page, "07_copy_result").then((p) =>
|
||||
screenshots.push({ step: "복사 결과", path: p })
|
||||
);
|
||||
|
||||
// 결과 확인
|
||||
const resultText = await page.locator("body").textContent();
|
||||
if (resultText?.includes("완료") || resultText?.includes("성공")) {
|
||||
console.log("\n=== 메뉴 복사 성공 ===");
|
||||
} else if (resultText?.includes("오류") || resultText?.includes("실패") || resultText?.includes("error")) {
|
||||
console.log("\n=== 에러 발생 가능 - 스크린샷 확인 필요 ===");
|
||||
}
|
||||
|
||||
console.log("\n=== 스크린샷 목록 ===");
|
||||
screenshots.forEach((s) => console.log(` - ${s.step}: ${s.path}`));
|
||||
} catch (error) {
|
||||
console.error("오류 발생:", error);
|
||||
if (browser) {
|
||||
const pages = (browser as any).contexts?.()?.[0]?.pages?.() || [];
|
||||
for (const p of pages) {
|
||||
try {
|
||||
await takeScreenshot(p, "error_state").then((path) =>
|
||||
screenshots.push({ step: "에러 상태", path })
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -91,7 +91,7 @@ echo "🎉 모든 서비스가 시작되었습니다!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "📊 서비스 접속 정보:"
|
||||
echo " [DATABASE] PostgreSQL: http://39.117.244.52:11132"
|
||||
echo " [DATABASE] PostgreSQL: http://211.115.91.141:11134"
|
||||
echo " [BACKEND] API: https://api.vexplor.com"
|
||||
echo " [FRONTEND] Web: https://v1.vexplor.com"
|
||||
echo " [BACKEND LOCAL] http://localhost:3001/api"
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, '../frontend/lib/utils/buttonActions.ts');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 디버깅 console.log 제거 (전체 줄)
|
||||
// console.log로 시작하는 줄만 제거 (이모지 포함)
|
||||
const patterns = [
|
||||
// 디버깅 로그 (이모지 포함)
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔍[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📦[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📋[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔗[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔄[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🎯[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]✅[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]⏭️[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📊[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🏗️[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📝[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]💾[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔐[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔑[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔒[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🧹[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🗑️[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📂[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📤[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📥[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔎[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🆕[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📌[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔥[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]⚡[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🎉[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🚀[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]📡[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🌐[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]👤[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🚫[^]*?\);\s*$/gm,
|
||||
/^\s*console\.log\s*\([^)]*["'`]🔧[^]*?\);\s*$/gm,
|
||||
];
|
||||
|
||||
let totalRemoved = 0;
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
const matches = content.match(pattern);
|
||||
if (matches) {
|
||||
totalRemoved += matches.length;
|
||||
content = content.replace(pattern, '');
|
||||
}
|
||||
});
|
||||
|
||||
// 연속된 빈 줄 제거 (3개 이상의 빈 줄을 2개로)
|
||||
content = content.replace(/\n\n\n+/g, '\n\n');
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log(`Removed ${totalRemoved} console.log statements`);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/**
|
||||
* 결재 템플릿 관리 및 결재함 E2E 테스트
|
||||
* 실행: node scripts/run-e2e-test.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = ".agent-pipeline/browser-tests";
|
||||
|
||||
async function runTest() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const results = [];
|
||||
let allPassed = true;
|
||||
|
||||
function pass(name) {
|
||||
results.push({ name, status: "PASS" });
|
||||
console.log(`PASS: ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, reason) {
|
||||
results.push({ name, status: "FAIL", reason });
|
||||
console.log(`FAIL: ${name} - ${reason}`);
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 로그인
|
||||
await page.goto(`${BASE_URL}/login`);
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.getByPlaceholder("사용자 ID를 입력하세요").fill("wace");
|
||||
await page.getByPlaceholder("비밀번호를 입력하세요").fill("qlalfqjsgh11");
|
||||
|
||||
await Promise.all([
|
||||
page.waitForURL(url => !url.toString().includes("/login"), { timeout: 30000 }),
|
||||
page.getByRole("button", { name: "로그인" }).click(),
|
||||
]);
|
||||
await page.waitForLoadState("networkidle");
|
||||
pass("로그인");
|
||||
|
||||
// =========================================================
|
||||
// 결재 템플릿 관리 페이지
|
||||
// =========================================================
|
||||
await page.goto(`${BASE_URL}/admin/approvalTemplate`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 1. "결재 템플릿 관리" 제목 확인
|
||||
try {
|
||||
await page.locator("h1").filter({ hasText: "결재 템플릿 관리" }).waitFor({ timeout: 10000 });
|
||||
pass("결재 템플릿 관리 - 제목 확인");
|
||||
} catch (e) {
|
||||
fail("결재 템플릿 관리 - 제목 확인", e.message);
|
||||
}
|
||||
|
||||
// 2. "신규 등록" 버튼 확인
|
||||
try {
|
||||
await page.getByRole("button", { name: "신규 등록" }).waitFor({ timeout: 10000 });
|
||||
pass("결재 템플릿 관리 - 신규 등록 버튼 확인");
|
||||
} catch (e) {
|
||||
fail("결재 템플릿 관리 - 신규 등록 버튼 확인", e.message);
|
||||
}
|
||||
|
||||
// 3. 검색 입력란 확인
|
||||
try {
|
||||
await page.getByPlaceholder("템플릿명 또는 설명 검색...").waitFor({ timeout: 10000 });
|
||||
pass("결재 템플릿 관리 - 검색 입력란 확인");
|
||||
} catch (e) {
|
||||
// placeholder가 다를 수 있으므로 input으로 재시도
|
||||
try {
|
||||
await page.locator("input[type='text']").first().waitFor({ timeout: 5000 });
|
||||
pass("결재 템플릿 관리 - 검색 입력란 확인 (input fallback)");
|
||||
} catch (e2) {
|
||||
fail("결재 템플릿 관리 - 검색 입력란 확인", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. JS 에러 오버레이 확인
|
||||
const hasError1 = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (!hasError1) {
|
||||
pass("결재 템플릿 관리 - JS 에러 없음");
|
||||
} else {
|
||||
fail("결재 템플릿 관리 - JS 에러 없음", "에러 오버레이 감지됨");
|
||||
}
|
||||
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/result-template.png`, fullPage: true });
|
||||
|
||||
// =========================================================
|
||||
// 결재함 페이지
|
||||
// =========================================================
|
||||
await page.goto(`${BASE_URL}/admin/approvalBox`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 1. 탭 확인
|
||||
try {
|
||||
await page.getByRole("tab", { name: /수신함/ }).waitFor({ timeout: 10000 });
|
||||
pass("결재함 - 수신함 탭 확인");
|
||||
} catch (e) {
|
||||
fail("결재함 - 수신함 탭 확인", e.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await page.getByRole("tab", { name: /상신함/ }).waitFor({ timeout: 10000 });
|
||||
pass("결재함 - 상신함 탭 확인");
|
||||
} catch (e) {
|
||||
fail("결재함 - 상신함 탭 확인", e.message);
|
||||
}
|
||||
|
||||
// 2. JS 에러 오버레이 확인
|
||||
const hasError2 = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (!hasError2) {
|
||||
pass("결재함 - JS 에러 없음");
|
||||
} else {
|
||||
fail("결재함 - JS 에러 없음", "에러 오버레이 감지됨");
|
||||
}
|
||||
|
||||
await page.screenshot({ path: `${SCREENSHOT_DIR}/result-box.png`, fullPage: true });
|
||||
|
||||
} catch (e) {
|
||||
fail("테스트 실행", e.message);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log("\n=== 테스트 결과 ===");
|
||||
results.forEach(r => {
|
||||
const status = r.status === "PASS" ? "✓" : "✗";
|
||||
console.log(`${status} ${r.name}${r.reason ? `: ${r.reason}` : ""}`);
|
||||
});
|
||||
|
||||
if (allPassed) {
|
||||
console.log("\nBROWSER_TEST_RESULT: PASS");
|
||||
process.exit(0);
|
||||
} else {
|
||||
const failed = results.filter(r => r.status === "FAIL").map(r => r.name).join(", ");
|
||||
console.log(`\nBROWSER_TEST_RESULT: FAIL - ${failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch(e => {
|
||||
console.error("치명적 오류:", e);
|
||||
console.log("BROWSER_TEST_RESULT: FAIL - 치명적 오류: " + e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* 옵션설정 (Option Settings) 페이지 반응형 동작 테스트
|
||||
* - V2CategoryManagerComponent + ResponsiveGridRenderer
|
||||
* - 화면: http://localhost:9771/screens/1421
|
||||
*
|
||||
* 실행: npx tsx scripts/test-option-settings-responsive.ts
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const API_URL = "http://localhost:8080/api";
|
||||
const PAGE_URL = `${BASE_URL}/screens/1421`;
|
||||
const CREDENTIALS = [
|
||||
{ userId: "SUPER", password: "1234" },
|
||||
{ userId: "wace", password: "qlalfqjsgh11" },
|
||||
];
|
||||
|
||||
const OUTPUT_DIR = path.join(__dirname, "../test-output/option-settings-responsive");
|
||||
|
||||
async function loginViaApi(): Promise<string> {
|
||||
for (const cred of CREDENTIALS) {
|
||||
const res = await fetch(`${API_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId: cred.userId, password: cred.password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && data.data?.token) {
|
||||
console.log(` Using credentials: ${cred.userId}`);
|
||||
return data.data.token;
|
||||
}
|
||||
}
|
||||
throw new Error("Login failed with all credentials");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const token = await loginViaApi();
|
||||
console.log("1. Logged in via API");
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const consoleErrors: string[] = [];
|
||||
const reactHookErrors: string[] = [];
|
||||
|
||||
page.on("console", (msg) => {
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
if (type === "error") {
|
||||
consoleErrors.push(text);
|
||||
if (text.includes("order of Hooks") || text.includes("React has detected")) {
|
||||
reactHookErrors.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
console.log("2. Loading login page to inject token...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.evaluate((t: string) => {
|
||||
localStorage.setItem("authToken", t);
|
||||
document.cookie = `authToken=${t}; path=/; max-age=86400; SameSite=Lax`;
|
||||
}, token);
|
||||
|
||||
console.log("3. Navigating to Option Settings page (screens/1421)...");
|
||||
await page.goto(PAGE_URL, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const report: string[] = [];
|
||||
|
||||
const widths = [
|
||||
{ w: 1400, name: "1-desktop-1400px" },
|
||||
{ w: 1100, name: "2-tablet-1100px" },
|
||||
{ w: 900, name: "3-tablet-900px" },
|
||||
{ w: 600, name: "4-mobile-600px" },
|
||||
{ w: 1400, name: "5-desktop-1400px-restored" },
|
||||
];
|
||||
|
||||
for (const { w, name } of widths) {
|
||||
console.log(`\nResizing to ${w}px...`);
|
||||
await page.setViewportSize({ width: w, height: w === 600 ? 812 : 900 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const filePath = path.join(OUTPUT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: filePath, fullPage: false });
|
||||
console.log(` Saved: ${filePath}`);
|
||||
|
||||
const hasCategoryColumn = (await page.locator('text=카테고리 컬럼').count()) > 0;
|
||||
const hasUseStatus = (await page.locator('text=사용여부').count()) > 0;
|
||||
const hasVerticalStack = (await page.locator('button:has-text("목록")').count()) > 0;
|
||||
const hasLeftRightSplit = (await page.locator('[class*="cursor-col-resize"]').count()) > 0;
|
||||
|
||||
report.push(`[${w}px] Category column: ${hasCategoryColumn}, Use status: ${hasUseStatus}, Vertical: ${hasVerticalStack}, Split: ${hasLeftRightSplit}`);
|
||||
}
|
||||
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log("OPTION SETTINGS RESPONSIVE TEST REPORT");
|
||||
console.log("=".repeat(60));
|
||||
report.forEach((r) => console.log(r));
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log("\n--- Console Errors ---");
|
||||
consoleErrors.slice(0, 10).forEach((e) => console.log(" ", e));
|
||||
}
|
||||
if (reactHookErrors.length > 0) {
|
||||
console.log("\n--- React Hook Errors (order of Hooks) ---");
|
||||
reactHookErrors.forEach((e) => console.log(" ", e));
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR, "report.txt"),
|
||||
[
|
||||
"OPTION SETTINGS RESPONSIVE TEST REPORT",
|
||||
"=".repeat(50),
|
||||
...report,
|
||||
"",
|
||||
"Console errors: " + consoleErrors.length,
|
||||
"React Hook errors: " + reactHookErrors.length,
|
||||
...reactHookErrors.map((e) => " " + e),
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
console.log("\nScreenshots saved to:", OUTPUT_DIR);
|
||||
} catch (e) {
|
||||
console.error("Error:", e);
|
||||
throw e;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* ResponsiveSplitPanel 반응형 동작 테스트
|
||||
* - Desktop (>= 1280px): 좌우 분할 + 리사이저
|
||||
* - Tablet (768-1279px): 좌측 패널 자동 접힘, 아이콘 버튼만 표시
|
||||
* - Mobile (< 768px): 세로 스택 + 접기/펼치기 헤더
|
||||
*
|
||||
* 실행: npx tsx scripts/test-responsive-split-panel.ts
|
||||
*/
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const API_URL = "http://localhost:8080/api";
|
||||
const TABLE_MNG_PATH = "/admin/systemMng/tableMngList";
|
||||
const USER_ID = "wace";
|
||||
const PASSWORD = "qlalfqjsgh11";
|
||||
|
||||
const OUTPUT_DIR = path.join(__dirname, "../test-output/responsive-split-panel");
|
||||
|
||||
async function loginViaApi(): Promise<string> {
|
||||
const res = await fetch(`${API_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ userId: USER_ID, password: PASSWORD }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success || !data.data?.token) throw new Error("Login failed: " + (data.message || "no token"));
|
||||
return data.data.token;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const token = await loginViaApi();
|
||||
console.log("1. Logged in via API");
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// 공개 페이지에서 토큰 주입 후 테이블 페이지로 이동
|
||||
console.log("2. Loading login page to inject token...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.evaluate((t: string) => {
|
||||
localStorage.setItem("authToken", t);
|
||||
document.cookie = `authToken=${t}; path=/; max-age=86400; SameSite=Lax`;
|
||||
}, token);
|
||||
|
||||
console.log("3. Navigating to table management page...");
|
||||
await page.goto(`${BASE_URL}${TABLE_MNG_PATH}`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const report: string[] = [];
|
||||
|
||||
// --- 3. Desktop 1400px (default wide) ---
|
||||
console.log("\n3. Desktop 1400px - taking screenshot...");
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await page.waitForTimeout(1000);
|
||||
const desktopPath = path.join(OUTPUT_DIR, "1-desktop-1400px.png");
|
||||
await page.screenshot({ path: desktopPath, fullPage: false });
|
||||
console.log(` Saved: ${desktopPath}`);
|
||||
|
||||
const at1400 = {
|
||||
resizer: (await page.locator('[class*="cursor-col-resize"]').count()) > 0,
|
||||
leftPanelVisible: (await page.locator('text=테이블 검색').count()) > 0 || (await page.locator('input[placeholder*="검색"]').count()) > 0,
|
||||
iconButtonOnly: (await page.locator('button[title*="열기"]').count()) > 0 && !(await page.locator('input[placeholder*="검색"]').isVisible()),
|
||||
};
|
||||
report.push(`[1400px] Resizer: ${at1400.resizer}, Left panel visible: ${at1400.leftPanelVisible}, Icon-only: ${at1400.iconButtonOnly}`);
|
||||
|
||||
// --- 4. Tablet 1000px ---
|
||||
console.log("\n4. Tablet 1000px - resizing and taking screenshot...");
|
||||
await page.setViewportSize({ width: 1000, height: 900 });
|
||||
await page.waitForTimeout(1500);
|
||||
const tabletPath = path.join(OUTPUT_DIR, "2-tablet-1000px.png");
|
||||
await page.screenshot({ path: tabletPath, fullPage: false });
|
||||
console.log(` Saved: ${tabletPath}`);
|
||||
|
||||
const at1000 = {
|
||||
resizer: (await page.locator('[class*="cursor-col-resize"]').count()) > 0,
|
||||
leftPanelVisible: (await page.locator('input[placeholder*="검색"]').isVisible()),
|
||||
iconButtonOnly: (await page.locator('button[title*="열기"]').count()) > 0,
|
||||
verticalStack: (await page.locator('button:has-text("테이블 목록")').count()) > 0,
|
||||
};
|
||||
report.push(`[1000px] Resizer: ${at1000.resizer}, Left panel visible: ${at1000.leftPanelVisible}, Icon-only: ${at1000.iconButtonOnly}, Vertical stack: ${at1000.verticalStack}`);
|
||||
|
||||
// --- 5. Mobile 600px ---
|
||||
console.log("\n5. Mobile 600px - resizing and taking screenshot...");
|
||||
await page.setViewportSize({ width: 600, height: 812 });
|
||||
await page.waitForTimeout(1500);
|
||||
const mobilePath = path.join(OUTPUT_DIR, "3-mobile-600px.png");
|
||||
await page.screenshot({ path: mobilePath, fullPage: false });
|
||||
console.log(` Saved: ${mobilePath}`);
|
||||
|
||||
const at600 = {
|
||||
collapsibleHeader: (await page.locator('button:has-text("테이블 목록")').count()) > 0,
|
||||
verticalStack: (await page.locator('button:has-text("테이블 목록")').count()) > 0,
|
||||
leftPanelVisible: (await page.locator('input[placeholder*="검색"]').isVisible()),
|
||||
};
|
||||
report.push(`[600px] Collapsible header: ${at600.collapsibleHeader}, Vertical stack: ${at600.verticalStack}, Left panel visible: ${at600.leftPanelVisible}`);
|
||||
|
||||
// --- 6. Back to Desktop 1400px ---
|
||||
console.log("\n6. Back to Desktop 1400px - resizing and taking screenshot...");
|
||||
await page.setViewportSize({ width: 1400, height: 900 });
|
||||
await page.waitForTimeout(1500);
|
||||
const desktopAgainPath = path.join(OUTPUT_DIR, "4-desktop-1400px-again.png");
|
||||
await page.screenshot({ path: desktopAgainPath, fullPage: false });
|
||||
console.log(` Saved: ${desktopAgainPath}`);
|
||||
|
||||
const at1400Again = {
|
||||
resizer: (await page.locator('[class*="cursor-col-resize"]').count()) > 0,
|
||||
leftPanelVisible: (await page.locator('input[placeholder*="검색"]').isVisible()),
|
||||
};
|
||||
report.push(`[1400px again] Resizer: ${at1400Again.resizer}, Left panel visible: ${at1400Again.leftPanelVisible}`);
|
||||
|
||||
// --- Report ---
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log("RESPONSIVE LAYOUT TEST REPORT");
|
||||
console.log("=".repeat(60));
|
||||
report.forEach((r) => console.log(r));
|
||||
console.log("\nScreenshots saved to:", OUTPUT_DIR);
|
||||
} catch (e) {
|
||||
console.error("Error:", e);
|
||||
throw e;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user