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:
kjs
2026-04-01 12:12:15 +09:00
parent 250a83b581
commit ccb0c8df4c
112 changed files with 1165 additions and 11644 deletions

View File

@@ -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()

View File

@@ -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;
})();

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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.

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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"

View File

@@ -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);

View File

@@ -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"

View File

@@ -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`);

View File

@@ -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);
});

View File

@@ -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();

View File

@@ -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();