Remove obsolete end-to-end test scripts and related files for screen and table components in the agent pipeline.
This commit is contained in:
parent
d13884d572
commit
7910921c97
|
|
@ -357,8 +357,30 @@ const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
|
|||
return `${actualHeight}px`;
|
||||
}
|
||||
|
||||
// 🆕 1순위: size.height가 있으면 우선 사용 (레이아웃에서 관리되는 실제 크기)
|
||||
// size는 레이아웃 상태에서 직접 관리되며 리사이즈로 변경됨
|
||||
// 런타임 모드에서 컴포넌트 타입별 높이 처리
|
||||
if (!isDesignMode) {
|
||||
const compType = (component as any).componentType || component.componentConfig?.type || "";
|
||||
// 테이블: 부모 flex 컨테이너가 높이 관리 (flex: 1)
|
||||
const flexGrowTypes = [
|
||||
"table-list", "v2-table-list",
|
||||
"split-panel-layout", "split-panel-layout2",
|
||||
"v2-split-panel-layout", "screen-split-panel",
|
||||
"v2-tab-container", "tab-container",
|
||||
"tabs-widget", "v2-tabs-widget",
|
||||
];
|
||||
if (flexGrowTypes.some(t => compType === t)) {
|
||||
return "100%";
|
||||
}
|
||||
const autoHeightTypes = [
|
||||
"table-search-widget", "v2-table-search-widget",
|
||||
"flow-widget",
|
||||
];
|
||||
if (autoHeightTypes.some(t => compType === t || compType.includes(t))) {
|
||||
return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
// 1순위: size.height가 있으면 우선 사용
|
||||
if (size?.height && size.height > 0) {
|
||||
if (component.componentConfig?.type === "table-list") {
|
||||
return `${Math.max(size.height, 200)}px`;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import React, { useMemo, useRef, useState, useEffect } from "react";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import { useResponsive } from "@/lib/hooks/useResponsive";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ResponsiveGridRendererProps {
|
||||
components: ComponentData[];
|
||||
|
|
@ -11,7 +12,6 @@ interface ResponsiveGridRendererProps {
|
|||
renderComponent: (component: ComponentData) => React.ReactNode;
|
||||
}
|
||||
|
||||
// 전체 행을 차지해야 하는 컴포넌트 타입
|
||||
const FULL_WIDTH_TYPES = new Set([
|
||||
"table-list",
|
||||
"v2-table-list",
|
||||
|
|
@ -20,39 +20,35 @@ const FULL_WIDTH_TYPES = new Set([
|
|||
"conditional-container",
|
||||
"split-panel-layout",
|
||||
"split-panel-layout2",
|
||||
"v2-split-panel-layout",
|
||||
"screen-split-panel",
|
||||
"v2-split-line",
|
||||
"flow-widget",
|
||||
"v2-tab-container",
|
||||
"tab-container",
|
||||
"tabs-widget",
|
||||
"v2-tabs-widget",
|
||||
]);
|
||||
|
||||
// 높이를 auto로 처리해야 하는 컴포넌트 타입
|
||||
const AUTO_HEIGHT_TYPES = new Set([
|
||||
const FLEX_GROW_TYPES = new Set([
|
||||
"table-list",
|
||||
"v2-table-list",
|
||||
"table-search-widget",
|
||||
"v2-table-search-widget",
|
||||
"conditional-container",
|
||||
"flow-widget",
|
||||
"v2-tab-container",
|
||||
"tab-container",
|
||||
"split-panel-layout",
|
||||
"split-panel-layout2",
|
||||
"v2-split-panel-layout",
|
||||
"screen-split-panel",
|
||||
"v2-tab-container",
|
||||
"tab-container",
|
||||
"tabs-widget",
|
||||
"v2-tabs-widget",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Y좌표 기준으로 컴포넌트를 행(row) 단위로 그룹핑
|
||||
* - Y값 차이가 threshold 이내면 같은 행으로 판정
|
||||
* - 같은 행 안에서는 X좌표 순으로 정렬
|
||||
*/
|
||||
function groupComponentsIntoRows(
|
||||
components: ComponentData[],
|
||||
threshold: number = 30
|
||||
): ComponentData[][] {
|
||||
if (components.length === 0) return [];
|
||||
|
||||
const sorted = [...components].sort((a, b) => a.position.y - b.position.y);
|
||||
|
||||
const rows: ComponentData[][] = [];
|
||||
let currentRow: ComponentData[] = [];
|
||||
let currentRowY = -Infinity;
|
||||
|
|
@ -67,67 +63,150 @@ function groupComponentsIntoRows(
|
|||
}
|
||||
}
|
||||
if (currentRow.length > 0) rows.push(currentRow);
|
||||
|
||||
return rows.map((row) =>
|
||||
row.sort((a, b) => a.position.x - b.position.x)
|
||||
);
|
||||
return rows.map((row) => row.sort((a, b) => a.position.x - b.position.x));
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트의 유형 식별 (componentType, componentId, widgetType 등)
|
||||
*/
|
||||
function getComponentTypeId(component: ComponentData): string {
|
||||
return (
|
||||
(component as any).componentType ||
|
||||
(component as any).componentId ||
|
||||
(component as any).widgetType ||
|
||||
component.type ||
|
||||
""
|
||||
);
|
||||
const direct =
|
||||
(component as any).componentType || (component as any).widgetType;
|
||||
if (direct) return direct;
|
||||
const url = (component as any).url;
|
||||
if (url && typeof url === "string") {
|
||||
const parts = url.split("/");
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
return component.type || "";
|
||||
}
|
||||
|
||||
function isButtonComponent(component: ComponentData): boolean {
|
||||
return getComponentTypeId(component).includes("button");
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 행을 차지해야 하는 컴포넌트인지 판정
|
||||
*/
|
||||
function isFullWidthComponent(component: ComponentData): boolean {
|
||||
const typeId = getComponentTypeId(component);
|
||||
return FULL_WIDTH_TYPES.has(typeId);
|
||||
return FULL_WIDTH_TYPES.has(getComponentTypeId(component));
|
||||
}
|
||||
|
||||
/**
|
||||
* 높이를 auto로 처리해야 하는 컴포넌트인지 판정
|
||||
*/
|
||||
function shouldAutoHeight(component: ComponentData): boolean {
|
||||
const typeId = getComponentTypeId(component);
|
||||
return AUTO_HEIGHT_TYPES.has(typeId);
|
||||
function shouldFlexGrow(component: ComponentData): boolean {
|
||||
return FLEX_GROW_TYPES.has(getComponentTypeId(component));
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 너비를 캔버스 대비 비율(%)로 변환
|
||||
*/
|
||||
function getPercentageWidth(
|
||||
componentWidth: number,
|
||||
canvasWidth: number
|
||||
): number {
|
||||
const percentage = (componentWidth / canvasWidth) * 100;
|
||||
if (percentage >= 95) return 100;
|
||||
return percentage;
|
||||
function getPercentageWidth(componentWidth: number, canvasWidth: number): number {
|
||||
const pct = (componentWidth / canvasWidth) * 100;
|
||||
return pct >= 95 ? 100 : pct;
|
||||
}
|
||||
|
||||
/**
|
||||
* 행 내 컴포넌트 사이의 수평 갭(px)을 비율 기반으로 추정
|
||||
*/
|
||||
function getRowGap(row: ComponentData[], canvasWidth: number): number {
|
||||
if (row.length < 2) return 0;
|
||||
const totalComponentWidth = row.reduce(
|
||||
(sum, c) => sum + (c.size?.width || 100),
|
||||
0
|
||||
const totalW = row.reduce((s, c) => s + (c.size?.width || 100), 0);
|
||||
const gap = canvasWidth - totalW;
|
||||
const cnt = row.length - 1;
|
||||
if (gap <= 0 || cnt <= 0) return 8;
|
||||
return Math.min(Math.max(Math.round(gap / cnt), 4), 24);
|
||||
}
|
||||
|
||||
interface ProcessedRow {
|
||||
type: "normal" | "fullwidth";
|
||||
mainComponent?: ComponentData;
|
||||
overlayComps: ComponentData[];
|
||||
normalComps: ComponentData[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 풀위드스 컴포넌트 + 오버레이 버튼.
|
||||
* 원본 좌표 그대로 배치 → transform: scale 한 방으로 축소.
|
||||
* 디자이너 미리보기와 동일한 원리.
|
||||
*/
|
||||
function FullWidthOverlayRow({
|
||||
main,
|
||||
overlayComps,
|
||||
canvasWidth,
|
||||
renderComponent,
|
||||
}: {
|
||||
main: ComponentData;
|
||||
overlayComps: ComponentData[];
|
||||
canvasWidth: number;
|
||||
renderComponent: (component: ComponentData) => React.ReactNode;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerW, setContainerW] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const w = entries[0]?.contentRect.width;
|
||||
if (w && w > 0) setContainerW(w);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
const compFlexGrow = shouldFlexGrow(main);
|
||||
const mainY = main.position.y;
|
||||
const scale = containerW > 0 ? containerW / canvasWidth : 1;
|
||||
|
||||
const minButtonY = Math.min(...overlayComps.map((c) => c.position.y));
|
||||
const rawYOffset = minButtonY - mainY;
|
||||
const maxBtnH = Math.max(
|
||||
...overlayComps.map((c) => c.size?.height || 40)
|
||||
);
|
||||
// 버튼 중심 보정: 스케일 축소 시 버튼이 작아지므로 중심 위치가 위로 올라감
|
||||
// 디자이너와 동일한 중심-대-중심 정렬을 유지하기 위해 Y 오프셋 보정
|
||||
const yOffset = rawYOffset + (maxBtnH / 2) * (1 - scale);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative flex w-full flex-col",
|
||||
compFlexGrow ? "min-h-0 flex-1" : "flex-shrink-0"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-component-id={main.id}
|
||||
data-component-type={getComponentTypeId(main)}
|
||||
className="min-h-0 min-w-0"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: compFlexGrow ? "100%" : "auto",
|
||||
minHeight: compFlexGrow ? "300px" : undefined,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{renderComponent(main)}
|
||||
</div>
|
||||
|
||||
{overlayComps.length > 0 && containerW > 0 && (
|
||||
<div
|
||||
className="pointer-events-none absolute left-0 z-10"
|
||||
style={{
|
||||
top: `${yOffset}px`,
|
||||
width: `${canvasWidth}px`,
|
||||
height: `${maxBtnH}px`,
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "top left",
|
||||
}}
|
||||
>
|
||||
{overlayComps.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-component-id={comp.id}
|
||||
data-component-type={getComponentTypeId(comp)}
|
||||
className="pointer-events-auto absolute"
|
||||
style={{
|
||||
left: `${comp.position.x}px`,
|
||||
top: `${comp.position.y - minButtonY}px`,
|
||||
width: `${comp.size?.width || 90}px`,
|
||||
height: `${comp.size?.height || 40}px`,
|
||||
}}
|
||||
>
|
||||
{renderComponent(comp)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const totalGap = canvasWidth - totalComponentWidth;
|
||||
const gapCount = row.length - 1;
|
||||
if (totalGap <= 0 || gapCount <= 0) return 8;
|
||||
const gapPx = totalGap / gapCount;
|
||||
return Math.min(Math.max(Math.round(gapPx), 4), 24);
|
||||
}
|
||||
|
||||
export function ResponsiveGridRenderer({
|
||||
|
|
@ -138,14 +217,12 @@ export function ResponsiveGridRenderer({
|
|||
}: ResponsiveGridRendererProps) {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
// 전체 행을 차지하는 컴포넌트는 별도 행으로 분리
|
||||
const processedRows = useMemo(() => {
|
||||
const topLevel = components.filter((c) => !c.parentId);
|
||||
const rows = groupComponentsIntoRows(topLevel);
|
||||
|
||||
const result: ComponentData[][] = [];
|
||||
const result: ProcessedRow[] = [];
|
||||
for (const row of rows) {
|
||||
// 전체 너비 컴포넌트는 독립 행으로 분리
|
||||
const fullWidthComps: ComponentData[] = [];
|
||||
const normalComps: ComponentData[] = [];
|
||||
|
||||
|
|
@ -157,73 +234,112 @@ export function ResponsiveGridRenderer({
|
|||
}
|
||||
}
|
||||
|
||||
// 일반 컴포넌트 행 먼저
|
||||
if (normalComps.length > 0) {
|
||||
result.push(normalComps);
|
||||
}
|
||||
// 전체 너비 컴포넌트는 각각 독립 행
|
||||
for (const comp of fullWidthComps) {
|
||||
result.push([comp]);
|
||||
if (fullWidthComps.length > 0 && normalComps.length > 0) {
|
||||
for (const fwComp of fullWidthComps) {
|
||||
result.push({
|
||||
type: "fullwidth",
|
||||
mainComponent: fwComp,
|
||||
overlayComps: normalComps,
|
||||
normalComps: [],
|
||||
});
|
||||
}
|
||||
} else if (fullWidthComps.length > 0) {
|
||||
for (const fwComp of fullWidthComps) {
|
||||
result.push({
|
||||
type: "fullwidth",
|
||||
mainComponent: fwComp,
|
||||
overlayComps: [],
|
||||
normalComps: [],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
type: "normal",
|
||||
overlayComps: [],
|
||||
normalComps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [components]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-screen-runtime="true"
|
||||
className="bg-background flex w-full flex-col"
|
||||
className="bg-background flex h-full w-full flex-col overflow-x-hidden"
|
||||
style={{ minHeight: "200px" }}
|
||||
>
|
||||
{processedRows.map((row, rowIndex) => {
|
||||
const isSingleFullWidth =
|
||||
row.length === 1 && isFullWidthComponent(row[0]);
|
||||
const gap = isMobile ? 8 : getRowGap(row, canvasWidth);
|
||||
{processedRows.map((processedRow, rowIndex) => {
|
||||
if (processedRow.type === "fullwidth" && processedRow.mainComponent) {
|
||||
return (
|
||||
<FullWidthOverlayRow
|
||||
key={`row-${rowIndex}`}
|
||||
main={processedRow.mainComponent}
|
||||
overlayComps={processedRow.overlayComps}
|
||||
canvasWidth={canvasWidth}
|
||||
renderComponent={renderComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { normalComps } = processedRow;
|
||||
const allButtons = normalComps.every((c) => isButtonComponent(c));
|
||||
const gap = isMobile ? 8 : allButtons ? 8 : getRowGap(normalComps, canvasWidth);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`row-${rowIndex}`}
|
||||
className="flex w-full flex-wrap"
|
||||
style={{ gap: isSingleFullWidth ? 0 : `${gap}px` }}
|
||||
className={cn(
|
||||
"flex w-full flex-shrink-0 flex-wrap overflow-hidden",
|
||||
allButtons && "justify-end px-2 py-1"
|
||||
)}
|
||||
style={{ gap: `${gap}px` }}
|
||||
>
|
||||
{row.map((component) => {
|
||||
{normalComps.map((component) => {
|
||||
const typeId = getComponentTypeId(component);
|
||||
const isFullWidth =
|
||||
isMobile || isFullWidthComponent(component);
|
||||
const isButton = isButtonComponent(component);
|
||||
const isFullWidth = isMobile && !isButton;
|
||||
|
||||
if (isButton) {
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
data-component-id={component.id}
|
||||
data-component-type={typeId}
|
||||
className="flex-shrink-0"
|
||||
style={{
|
||||
height: component.size?.height
|
||||
? `${component.size.height}px`
|
||||
: "40px",
|
||||
}}
|
||||
>
|
||||
{renderComponent(component)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percentWidth = isFullWidth
|
||||
? 100
|
||||
: getPercentageWidth(
|
||||
component.size?.width || 100,
|
||||
canvasWidth
|
||||
);
|
||||
|
||||
const autoHeight = shouldAutoHeight(component);
|
||||
const height = autoHeight
|
||||
? "auto"
|
||||
: component.size?.height
|
||||
? `${component.size.height}px`
|
||||
: "auto";
|
||||
|
||||
// 모바일에서는 100%, 데스크톱에서는 비율 기반
|
||||
// gap을 고려한 flex-basis 계산
|
||||
: getPercentageWidth(component.size?.width || 100, canvasWidth);
|
||||
const flexBasis = isFullWidth
|
||||
? "100%"
|
||||
: `calc(${percentWidth}% - ${gap}px)`;
|
||||
const compFlexGrow = shouldFlexGrow(component);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={component.id}
|
||||
data-component-id={component.id}
|
||||
data-component-type={typeId}
|
||||
className="flex-shrink-0"
|
||||
className="min-w-0 flex-shrink-0 overflow-hidden"
|
||||
style={{
|
||||
width: isFullWidth ? "100%" : undefined,
|
||||
flexBasis: isFullWidth ? "100%" : flexBasis,
|
||||
flexGrow: isFullWidth ? 1 : 0,
|
||||
flexBasis,
|
||||
flexGrow: isFullWidth || compFlexGrow ? 1 : 0,
|
||||
minWidth: isMobile ? "100%" : undefined,
|
||||
height,
|
||||
minHeight: autoHeight ? undefined : height,
|
||||
height: component.size?.height
|
||||
? `${component.size.height}px`
|
||||
: "auto",
|
||||
}}
|
||||
>
|
||||
{renderComponent(component)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { cn } from "@/lib/utils";
|
|||
import { useActiveTab } from "@/contexts/ActiveTabContext";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { screenApi } from "@/lib/api/screen";
|
||||
import { ResponsiveGridRenderer } from "@/components/screen/ResponsiveGridRenderer";
|
||||
|
||||
// 확장된 TabItem 타입 (screenId 지원)
|
||||
interface ExtendedTabItem extends TabItem {
|
||||
|
|
@ -305,125 +306,152 @@ export function TabsWidget({
|
|||
|
||||
// screenId로 로드한 화면 컴포넌트 렌더링
|
||||
const renderScreenComponents = (tab: ExtendedTabItem, components: ComponentData[]) => {
|
||||
// InteractiveScreenViewerDynamic 동적 로드
|
||||
const InteractiveScreenViewerDynamic =
|
||||
require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic;
|
||||
|
||||
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
||||
const maxBottom = Math.max(...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)), 300);
|
||||
const maxRight = Math.max(...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)), 400);
|
||||
const canvasWidth = Math.max(
|
||||
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
|
||||
800,
|
||||
);
|
||||
const canvasHeight = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
400,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full w-full overflow-auto"
|
||||
style={{
|
||||
minHeight: maxBottom + 20,
|
||||
minWidth: maxRight + 20,
|
||||
}}
|
||||
>
|
||||
{components.map((comp) => (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || "auto",
|
||||
height: comp.size?.height || "auto",
|
||||
}}
|
||||
>
|
||||
<InteractiveScreenViewerDynamic
|
||||
component={comp}
|
||||
allComponents={components}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenInfo={screenInfoMap[tab.id]}
|
||||
menuObjid={menuObjid}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ResponsiveGridRenderer
|
||||
components={components}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={(comp) => (
|
||||
<InteractiveScreenViewerDynamic
|
||||
component={comp}
|
||||
allComponents={components}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenInfo={screenInfoMap[tab.id]}
|
||||
menuObjid={menuObjid}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 인라인 컴포넌트 렌더링 (v2 방식)
|
||||
const renderInlineComponents = (tab: ExtendedTabItem, components: TabInlineComponent[]) => {
|
||||
// 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보
|
||||
const maxBottom = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
300, // 최소 높이
|
||||
);
|
||||
const maxRight = Math.max(
|
||||
if (isDesignMode) {
|
||||
const maxBottom = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
300,
|
||||
);
|
||||
const maxRight = Math.max(
|
||||
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
|
||||
400,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={{ minHeight: maxBottom + 20, minWidth: maxRight + 20 }}
|
||||
>
|
||||
{components.map((comp: TabInlineComponent) => {
|
||||
const isSelected = selectedComponentId === comp.id;
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className={cn("absolute cursor-move", isSelected && "ring-primary ring-2 ring-offset-2")}
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (onComponentSelect) {
|
||||
e.stopPropagation();
|
||||
onComponentSelect(tab.id, comp.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
{...restProps}
|
||||
component={{
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position,
|
||||
size: comp.size,
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style,
|
||||
} as any}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
menuObjid={menuObjid}
|
||||
isDesignMode={true}
|
||||
isInteractive={false}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
{...(screenInfoMap[tab.id]
|
||||
? { tableName: screenInfoMap[tab.id].tableName, screenId: screenInfoMap[tab.id].id }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 런타임: ResponsiveGridRenderer 사용
|
||||
const canvasWidth = Math.max(
|
||||
...components.map((c) => (c.position?.x || 0) + (c.size?.width || 200)),
|
||||
400, // 최소 너비
|
||||
800,
|
||||
);
|
||||
const canvasHeight = Math.max(
|
||||
...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)),
|
||||
400,
|
||||
);
|
||||
|
||||
const componentDataList: ComponentData[] = components.map((comp) => ({
|
||||
id: comp.id,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position,
|
||||
size: comp.size,
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style,
|
||||
type: "component",
|
||||
})) as any;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
minHeight: maxBottom + 20,
|
||||
minWidth: maxRight + 20,
|
||||
}}
|
||||
>
|
||||
{components.map((comp: TabInlineComponent) => {
|
||||
const isSelected = selectedComponentId === comp.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className={cn(
|
||||
"absolute",
|
||||
isDesignMode && "cursor-move",
|
||||
isDesignMode && isSelected && "ring-primary ring-2 ring-offset-2",
|
||||
)}
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 200,
|
||||
height: comp.size?.height || 100,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (isDesignMode && onComponentSelect) {
|
||||
e.stopPropagation();
|
||||
onComponentSelect(tab.id, comp.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
{...restProps}
|
||||
component={{
|
||||
id: comp.id,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position,
|
||||
size: comp.size,
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style,
|
||||
}}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
menuObjid={menuObjid}
|
||||
isDesignMode={isDesignMode}
|
||||
isInteractive={!isDesignMode}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
// 탭에 screenId가 있으면 해당 화면의 tableName/screenId로 오버라이드
|
||||
{...(screenInfoMap[tab.id]
|
||||
? {
|
||||
tableName: screenInfoMap[tab.id].tableName,
|
||||
screenId: screenInfoMap[tab.id].id,
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ResponsiveGridRenderer
|
||||
components={componentDataList}
|
||||
canvasWidth={canvasWidth}
|
||||
canvasHeight={canvasHeight}
|
||||
renderComponent={(comp) => (
|
||||
<DynamicComponentRenderer
|
||||
{...restProps}
|
||||
component={comp}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
menuObjid={menuObjid}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
parentTabId={tab.id}
|
||||
parentTabsComponentId={component.id}
|
||||
{...(screenInfoMap[tab.id]
|
||||
? { tableName: screenInfoMap[tab.id].tableName, screenId: screenInfoMap[tab.id].id }
|
||||
: {})}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -492,24 +492,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
return `${height}px`; // 숫자면 px 추가
|
||||
};
|
||||
|
||||
const componentStyle: React.CSSProperties = isPreview
|
||||
const componentStyle: React.CSSProperties = isDesignMode
|
||||
? {
|
||||
// 반응형 모드: position relative, 그리드 컨테이너가 제공하는 크기 사용
|
||||
position: "relative",
|
||||
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤
|
||||
height: getHeightValue(),
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
: {
|
||||
// 디자이너 모드: position absolute
|
||||
position: "absolute",
|
||||
left: `${component.style?.positionX || 0}px`,
|
||||
top: `${component.style?.positionY || 0}px`,
|
||||
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤 (그리드 기반)
|
||||
width: "100%",
|
||||
height: getHeightValue(),
|
||||
zIndex: component.style?.positionZ || 1,
|
||||
cursor: isDesignMode ? "pointer" : "default",
|
||||
cursor: "pointer",
|
||||
border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb",
|
||||
}
|
||||
: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "1px solid #e5e7eb",
|
||||
};
|
||||
|
||||
// 계층 구조 빌드 함수 (트리 구조 유지)
|
||||
|
|
@ -2468,13 +2466,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
...(isPreview
|
||||
? {
|
||||
position: "relative",
|
||||
height: `${component.style?.height || 600}px`,
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
: componentStyle),
|
||||
...componentStyle,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
|
|
@ -2488,7 +2480,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
>
|
||||
{/* 좌측 패널 */}
|
||||
<div
|
||||
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px`, height: "100%" }}
|
||||
style={{ width: `${leftWidth}%`, minWidth: isDesignMode ? `${minLeftWidth}px` : "0", height: "100%" }}
|
||||
className="border-border flex flex-shrink-0 flex-col border-r"
|
||||
>
|
||||
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
|
||||
|
|
@ -2981,7 +2973,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
|
||||
{/* 우측 패널 */}
|
||||
<div
|
||||
style={{ width: `${100 - leftWidth}%`, minWidth: isPreview ? "0" : `${minRightWidth}px`, height: "100%" }}
|
||||
style={{ width: `${100 - leftWidth}%`, minWidth: isDesignMode ? `${minRightWidth}px` : "0", height: "100%" }}
|
||||
className="flex flex-shrink-0 flex-col"
|
||||
>
|
||||
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import { useSplitPanel } from "./SplitPanelContext";
|
|||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { PanelInlineComponent } from "./types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ResponsiveGridRenderer } from "@/components/screen/ResponsiveGridRenderer";
|
||||
import { BomExcelUploadModal } from "../v2-bom-tree/BomExcelUploadModal";
|
||||
|
||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||
|
|
@ -726,24 +727,21 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
return `${height}px`; // 숫자면 px 추가
|
||||
};
|
||||
|
||||
const componentStyle: React.CSSProperties = isPreview
|
||||
const componentStyle: React.CSSProperties = isDesignMode
|
||||
? {
|
||||
// 반응형 모드: position relative, 그리드 컨테이너가 제공하는 크기 사용
|
||||
position: "relative",
|
||||
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤
|
||||
height: getHeightValue(),
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
: {
|
||||
// 디자이너 모드: position absolute
|
||||
position: "absolute",
|
||||
left: `${component.style?.positionX || 0}px`,
|
||||
top: `${component.style?.positionY || 0}px`,
|
||||
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤 (그리드 기반)
|
||||
width: "100%",
|
||||
height: getHeightValue(),
|
||||
zIndex: component.style?.positionZ || 1,
|
||||
cursor: isDesignMode ? "pointer" : "default",
|
||||
cursor: "pointer",
|
||||
border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb",
|
||||
}
|
||||
: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: getHeightValue(),
|
||||
};
|
||||
|
||||
// 계층 구조 빌드 함수 (트리 구조 유지)
|
||||
|
|
@ -2975,13 +2973,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
...(isPreview
|
||||
? {
|
||||
position: "relative",
|
||||
height: `${component.style?.height || 600}px`,
|
||||
border: "1px solid #e5e7eb",
|
||||
}
|
||||
: componentStyle),
|
||||
...componentStyle,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
|
|
@ -2995,8 +2987,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
>
|
||||
{/* 좌측 패널 */}
|
||||
<div
|
||||
style={{ width: `${leftWidth}%`, minWidth: isPreview ? "0" : `${minLeftWidth}px`, height: "100%" }}
|
||||
className="border-border flex flex-shrink-0 flex-col border-r"
|
||||
style={{ width: `${leftWidth}%`, minWidth: isDesignMode ? `${minLeftWidth}px` : "0", height: "100%" }}
|
||||
className="border-border flex flex-col border-r"
|
||||
>
|
||||
<Card className="flex flex-col border-0 shadow-none" style={{ height: "100%" }}>
|
||||
<CardHeader
|
||||
|
|
@ -3053,22 +3045,74 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
data-component-id={component.id}
|
||||
data-panel-side="left"
|
||||
>
|
||||
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
|
||||
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
||||
{componentConfig.leftPanel?.components && componentConfig.leftPanel.components.length > 0 ? (
|
||||
!isDesignMode ? (
|
||||
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
|
||||
(() => {
|
||||
const leftComps = componentConfig.leftPanel!.components;
|
||||
const canvasW = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
|
||||
const canvasH = Math.max(...leftComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
|
||||
const compDataList = leftComps.map((c: PanelInlineComponent) => ({
|
||||
id: c.id,
|
||||
type: "component" as const,
|
||||
componentType: c.componentType,
|
||||
label: c.label,
|
||||
position: c.position || { x: 0, y: 0 },
|
||||
size: c.size || { width: 400, height: 300 },
|
||||
componentConfig: c.componentConfig || {},
|
||||
style: c.style || {},
|
||||
tableName: c.componentConfig?.tableName,
|
||||
columnName: c.componentConfig?.columnName,
|
||||
webType: c.componentConfig?.webType,
|
||||
inputType: (c as any).inputType || c.componentConfig?.inputType,
|
||||
})) as any;
|
||||
return (
|
||||
<ResponsiveGridRenderer
|
||||
components={compDataList}
|
||||
canvasWidth={canvasW}
|
||||
canvasHeight={canvasH}
|
||||
renderComponent={(comp) => (
|
||||
<DynamicComponentRenderer
|
||||
component={comp as any}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={{}}
|
||||
tableName={componentConfig.leftPanel?.tableName}
|
||||
menuObjid={(props as any).menuObjid}
|
||||
screenId={(props as any).screenId}
|
||||
userId={(props as any).userId}
|
||||
userName={(props as any).userName}
|
||||
companyCode={companyCode}
|
||||
allComponents={(props as any).allComponents}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||
onFormDataChange={(data: any) => {
|
||||
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
|
||||
setCustomLeftSelectedData(data.selectedRowsData[0]);
|
||||
setSelectedLeftItem(data.selectedRowsData[0]);
|
||||
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
|
||||
setCustomLeftSelectedData({});
|
||||
setSelectedLeftItem(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||
{componentConfig.leftPanel.components.map((comp: PanelInlineComponent) => {
|
||||
const isSelectedComp = selectedPanelComponentId === comp.id;
|
||||
const isDraggingComp = draggingCompId === comp.id;
|
||||
const isResizingComp = resizingCompId === comp.id;
|
||||
|
||||
// 드래그/리사이즈 중 표시할 크기/위치
|
||||
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
||||
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
||||
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
|
||||
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
// componentConfig의 주요 속성을 최상위로 펼침 (일반 화면의 overrides 플래트닝과 동일)
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
|
|
@ -3078,16 +3122,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
size: { width: displayWidth, height: displayHeight },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
// 파일 업로드/미디어 등이 component.tableName, component.columnName을 직접 참조하므로 펼침
|
||||
tableName: comp.componentConfig?.tableName,
|
||||
columnName: comp.componentConfig?.columnName,
|
||||
webType: comp.componentConfig?.webType,
|
||||
inputType: comp.inputType || comp.componentConfig?.inputType,
|
||||
inputType: (comp as any).inputType || comp.componentConfig?.inputType,
|
||||
};
|
||||
|
||||
if (isDesignMode) {
|
||||
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-panel-comp-id={comp.id}
|
||||
|
|
@ -3224,60 +3265,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// 실행 모드: DynamicComponentRenderer로 렌더링
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
componentType: comp.componentType,
|
||||
label: comp.label,
|
||||
position: comp.position || { x: 0, y: 0 },
|
||||
size: comp.size || { width: 400, height: 300 },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 400,
|
||||
height: comp.size?.height || 300,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={{}}
|
||||
tableName={componentConfig.leftPanel?.tableName}
|
||||
menuObjid={(props as any).menuObjid}
|
||||
screenId={(props as any).screenId}
|
||||
userId={(props as any).userId}
|
||||
userName={(props as any).userName}
|
||||
companyCode={companyCode}
|
||||
allComponents={(props as any).allComponents}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||
onFormDataChange={(data: any) => {
|
||||
// 커스텀 모드: 좌측 카드/테이블 선택 시 데이터 캡처
|
||||
if (data?.selectedRowsData && data.selectedRowsData.length > 0) {
|
||||
setCustomLeftSelectedData(data.selectedRowsData[0]);
|
||||
setSelectedLeftItem(data.selectedRowsData[0]);
|
||||
} else if (data?.selectedRowsData && data.selectedRowsData.length === 0) {
|
||||
setCustomLeftSelectedData({});
|
||||
setSelectedLeftItem(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
// 컴포넌트가 없을 때 드롭 영역 표시
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
|
|
@ -3819,8 +3809,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
|
||||
{/* 우측 패널 */}
|
||||
<div
|
||||
style={{ width: `${100 - leftWidth}%`, minWidth: isPreview ? "0" : `${minRightWidth}px`, height: "100%" }}
|
||||
className="flex flex-shrink-0 flex-col border-l border-border/60 bg-muted/5"
|
||||
style={{ width: `${100 - leftWidth}%`, minWidth: isDesignMode ? `${minRightWidth}px` : "0", height: "100%" }}
|
||||
className="flex flex-col border-l border-border/60 bg-muted/5"
|
||||
>
|
||||
<Card className="flex flex-col border-0 bg-transparent shadow-none" style={{ height: "100%" }}>
|
||||
<CardHeader
|
||||
|
|
@ -3909,7 +3899,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="flex-1 overflow-hidden p-4">
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
{/* 추가 탭 컨텐츠 */}
|
||||
{activeTabIndex > 0 ? (
|
||||
(() => {
|
||||
|
|
@ -4185,22 +4175,68 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
data-component-id={component.id}
|
||||
data-panel-side="right"
|
||||
>
|
||||
{/* 🆕 커스텀 모드: 디자인/실행 모드 통합 렌더링 */}
|
||||
{/* 커스텀 모드: 디자인/실행 모드 분기 렌더링 */}
|
||||
{componentConfig.rightPanel?.components && componentConfig.rightPanel.components.length > 0 ? (
|
||||
!isDesignMode ? (
|
||||
// 런타임: ResponsiveGridRenderer로 반응형 렌더링
|
||||
(() => {
|
||||
const rightComps = componentConfig.rightPanel!.components;
|
||||
const canvasW = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.x || 0) + (c.size?.width || 200)), 800);
|
||||
const canvasH = Math.max(...rightComps.map((c: PanelInlineComponent) => (c.position?.y || 0) + (c.size?.height || 100)), 400);
|
||||
const compDataList = rightComps.map((c: PanelInlineComponent) => ({
|
||||
id: c.id,
|
||||
type: "component" as const,
|
||||
componentType: c.componentType,
|
||||
label: c.label,
|
||||
position: c.position || { x: 0, y: 0 },
|
||||
size: c.size || { width: 400, height: 300 },
|
||||
componentConfig: c.componentConfig || {},
|
||||
style: c.style || {},
|
||||
tableName: c.componentConfig?.tableName,
|
||||
columnName: c.componentConfig?.columnName,
|
||||
webType: c.componentConfig?.webType,
|
||||
inputType: (c as any).inputType || c.componentConfig?.inputType,
|
||||
})) as any;
|
||||
return (
|
||||
<ResponsiveGridRenderer
|
||||
components={compDataList}
|
||||
canvasWidth={canvasW}
|
||||
canvasHeight={canvasH}
|
||||
renderComponent={(comp) => (
|
||||
<DynamicComponentRenderer
|
||||
component={comp as any}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={customLeftSelectedData}
|
||||
onFormDataChange={(fieldName: string, value: any) => {
|
||||
setCustomLeftSelectedData((prev: Record<string, any>) => ({ ...prev, [fieldName]: value }));
|
||||
}}
|
||||
tableName={componentConfig.rightPanel?.tableName || componentConfig.leftPanel?.tableName}
|
||||
menuObjid={(props as any).menuObjid}
|
||||
screenId={(props as any).screenId}
|
||||
userId={(props as any).userId}
|
||||
userName={(props as any).userName}
|
||||
companyCode={companyCode}
|
||||
allComponents={(props as any).allComponents}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})()
|
||||
) : (
|
||||
<div className="relative h-full w-full" style={{ minHeight: "100%", minWidth: "100%" }}>
|
||||
{componentConfig.rightPanel.components.map((comp: PanelInlineComponent) => {
|
||||
const isSelectedComp = selectedPanelComponentId === comp.id;
|
||||
const isDraggingComp = draggingCompId === comp.id;
|
||||
const isResizingComp = resizingCompId === comp.id;
|
||||
|
||||
// 드래그/리사이즈 중 표시할 크기/위치
|
||||
const displayX = isDraggingComp && dragPosition ? dragPosition.x : (comp.position?.x || 0);
|
||||
const displayY = isDraggingComp && dragPosition ? dragPosition.y : (comp.position?.y || 0);
|
||||
const displayWidth = isResizingComp && resizeSize ? resizeSize.width : (comp.size?.width || 200);
|
||||
const displayHeight = isResizingComp && resizeSize ? resizeSize.height : (comp.size?.height || 100);
|
||||
|
||||
// 컴포넌트 데이터를 DynamicComponentRenderer 형식으로 변환
|
||||
// componentConfig의 주요 속성을 최상위로 펼침 (일반 화면의 overrides 플래트닝과 동일)
|
||||
const componentData = {
|
||||
id: comp.id,
|
||||
type: "component" as const,
|
||||
|
|
@ -4210,16 +4246,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
size: { width: displayWidth, height: displayHeight },
|
||||
componentConfig: comp.componentConfig || {},
|
||||
style: comp.style || {},
|
||||
// 파일 업로드/미디어 등이 component.tableName, component.columnName을 직접 참조하므로 펼침
|
||||
tableName: comp.componentConfig?.tableName,
|
||||
columnName: comp.componentConfig?.columnName,
|
||||
webType: comp.componentConfig?.webType,
|
||||
inputType: comp.inputType || comp.componentConfig?.inputType,
|
||||
inputType: (comp as any).inputType || comp.componentConfig?.inputType,
|
||||
};
|
||||
|
||||
if (isDesignMode) {
|
||||
// 디자인 모드: 탭 컴포넌트와 동일하게 실제 컴포넌트 렌더링
|
||||
return (
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
data-panel-comp-id={comp.id}
|
||||
|
|
@ -4231,14 +4264,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// 패널 컴포넌트 선택 시 탭 내 선택 해제
|
||||
if (comp.componentType !== "v2-tabs-widget") {
|
||||
setNestedTabSelectedCompId(undefined);
|
||||
}
|
||||
onSelectPanelComponent?.("right", comp.id, comp);
|
||||
}}
|
||||
>
|
||||
{/* 드래그 핸들 - 컴포넌트 외부 상단 */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-4 cursor-move items-center justify-between rounded-t border border-b-0 bg-gray-100 px-1",
|
||||
|
|
@ -4277,7 +4308,6 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 실제 컴포넌트 렌더링 - 핸들 아래에 별도 영역 */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative overflow-hidden rounded-b border bg-white shadow-sm",
|
||||
|
|
@ -4292,10 +4322,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
height: displayHeight,
|
||||
}}
|
||||
>
|
||||
{/* 🆕 컨테이너 컴포넌트(탭, 분할 패널)는 드롭 이벤트를 받을 수 있어야 함 */}
|
||||
<div className={cn(
|
||||
"h-full w-full",
|
||||
// 탭/분할 패널 같은 컨테이너 컴포넌트는 pointer-events 활성화
|
||||
(comp.componentType === "v2-tabs-widget" ||
|
||||
comp.componentType === "tabs-widget" ||
|
||||
comp.componentType === "v2-split-panel-layout" ||
|
||||
|
|
@ -4307,16 +4335,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
component={componentData as any}
|
||||
isDesignMode={true}
|
||||
formData={{}}
|
||||
// 🆕 중첩된 컴포넌트 업데이트 핸들러 전달
|
||||
onUpdateComponent={(updatedComp: any) => {
|
||||
handleNestedComponentUpdate("right", comp.id, updatedComp);
|
||||
}}
|
||||
// 🆕 중첩된 탭 내부 컴포넌트 선택 핸들러 - 부모 분할 패널 정보 포함
|
||||
onSelectTabComponent={(tabId: string, compId: string, tabComp: any) => {
|
||||
console.log("🔍 [SplitPanel-Right] onSelectTabComponent 호출:", { tabId, compId, tabComp, parentSplitPanelId: component.id });
|
||||
// 탭 내 컴포넌트 선택 상태 업데이트
|
||||
setNestedTabSelectedCompId(compId);
|
||||
// 부모 분할 패널 정보와 함께 전역 이벤트 발생
|
||||
const event = new CustomEvent("nested-tab-component-select", {
|
||||
detail: {
|
||||
tabsComponentId: comp.id,
|
||||
|
|
@ -4333,20 +4356,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 리사이즈 가장자리 영역 - 선택된 컴포넌트에만 표시 */}
|
||||
{isSelectedComp && (
|
||||
<>
|
||||
{/* 오른쪽 가장자리 (너비 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute right-0 top-0 z-10 h-full w-2 cursor-ew-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "e")}
|
||||
/>
|
||||
{/* 아래 가장자리 (높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 left-0 z-10 h-2 w-full cursor-ns-resize hover:bg-primary/10"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "s")}
|
||||
/>
|
||||
{/* 오른쪽 아래 모서리 (너비+높이 조절) */}
|
||||
<div
|
||||
className="pointer-events-auto absolute bottom-0 right-0 z-20 h-3 w-3 cursor-nwse-resize hover:bg-primary/20"
|
||||
onMouseDown={(e) => handlePanelResizeStart(e, "right", comp, "se")}
|
||||
|
|
@ -4356,42 +4375,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
||||
return (
|
||||
<div
|
||||
key={comp.id}
|
||||
className="absolute"
|
||||
style={{
|
||||
left: comp.position?.x || 0,
|
||||
top: comp.position?.y || 0,
|
||||
width: comp.size?.width || 400,
|
||||
height: comp.size?.height || 300,
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={componentData as any}
|
||||
isDesignMode={false}
|
||||
isInteractive={true}
|
||||
formData={customLeftSelectedData}
|
||||
onFormDataChange={(fieldName: string, value: any) => {
|
||||
setCustomLeftSelectedData((prev: Record<string, any>) => ({ ...prev, [fieldName]: value }));
|
||||
}}
|
||||
tableName={componentConfig.rightPanel?.tableName || componentConfig.leftPanel?.tableName}
|
||||
menuObjid={(props as any).menuObjid}
|
||||
screenId={(props as any).screenId}
|
||||
userId={(props as any).userId}
|
||||
userName={(props as any).userName}
|
||||
companyCode={companyCode}
|
||||
allComponents={(props as any).allComponents}
|
||||
selectedRowsData={localSelectedRowsData}
|
||||
onSelectedRowsChange={handleLocalSelectedRowsChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
// 컴포넌트가 없을 때 드롭 영역 표시
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50/50">
|
||||
|
|
@ -4486,10 +4472,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
}));
|
||||
}
|
||||
|
||||
const tableMinWidth = columnsToShow.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="min-w-full">
|
||||
<table style={{ minWidth: `${tableMinWidth}px` }}>
|
||||
<thead className="sticky top-0 z-10">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{columnsToShow.map((col, idx) => (
|
||||
|
|
@ -4612,10 +4599,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
|||
const hasDeleteButton = !isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true);
|
||||
const hasActions = hasEditButton || hasDeleteButton;
|
||||
|
||||
const tableMinW2 = columnsToDisplay.reduce((sum, col) => sum + (col.width || 100), 0) + 80;
|
||||
return filteredData.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="min-h-0 flex-1 overflow-auto">
|
||||
<table className="w-full text-sm">
|
||||
<table className="text-sm" style={{ minWidth: `${tableMinW2}px` }}>
|
||||
<thead className="sticky top-0 z-10 bg-background">
|
||||
<tr className="border-b-2 border-border/60">
|
||||
{columnsToDisplay.map((col) => (
|
||||
|
|
|
|||
|
|
@ -432,7 +432,13 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
width: "100%",
|
||||
height: "100%",
|
||||
minHeight: isDesignMode ? "300px" : "100%",
|
||||
...style, // style prop이 위의 기본값들을 덮어씀
|
||||
...style,
|
||||
// 런타임에서는 DB의 고정 px 크기를 무시하고 부모에 맞춤
|
||||
...(!isDesignMode && {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
minWidth: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
// ========================================
|
||||
|
|
@ -5267,7 +5273,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onDragStart: isDesignMode ? onDragStart : undefined,
|
||||
onDragEnd: isDesignMode ? onDragEnd : undefined,
|
||||
draggable: isDesignMode,
|
||||
className: cn("w-full h-full", className, isDesignMode && "cursor-move"), // customer-item-mapping과 동일
|
||||
className: cn("w-full h-full overflow-hidden", className, isDesignMode && "cursor-move"),
|
||||
style: componentStyle,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* 브라우저 검증 스크립트
|
||||
* 1. 로그인 페이지 접속
|
||||
* 2. 로그인
|
||||
* 3. /screens/29 접속
|
||||
* 4. 화면 렌더링 검증 (버튼, 테이블, 검색 필터)
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const results: { step: string; success: boolean; message?: string }[] = [];
|
||||
|
||||
try {
|
||||
// Step 1: 로그인 페이지 접속
|
||||
console.log("Step 1: 로그인 페이지 접속...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "networkidle", timeout: 10000 });
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "01-login-page.png"), fullPage: true });
|
||||
results.push({ step: "1. 로그인 페이지 접속", success: true });
|
||||
|
||||
// Step 2: 로그인
|
||||
console.log("Step 2: 로그인...");
|
||||
await page.fill('#userId', "wace");
|
||||
await page.fill('#password', "qlalfqjsgh11");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "02-login-filled.png"), fullPage: true });
|
||||
|
||||
const loginButton = page.locator('button[type="submit"]').first();
|
||||
await loginButton.click();
|
||||
await page.waitForURL((url) => !url.pathname.includes("/login") || url.pathname === "/", { timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes("/login") && !currentUrl.includes("/screens")) {
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "02-login-result.png"), fullPage: true });
|
||||
const errorText = await page.locator('[role="alert"], .error, .text-destructive, [class*="error"]').first().textContent().catch(() => "");
|
||||
results.push({ step: "2. 로그인", success: false, message: errorText || "로그인 실패 - 여전히 로그인 페이지에 있음" });
|
||||
} else {
|
||||
results.push({ step: "2. 로그인", success: true });
|
||||
}
|
||||
|
||||
// Step 3: /screens/29 접속
|
||||
console.log("Step 3: /screens/29 접속...");
|
||||
await page.goto(`${BASE_URL}/screens/29`, { waitUntil: "networkidle", timeout: 15000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "03-screen-29.png"), fullPage: true });
|
||||
results.push({ step: "3. /screens/29 접속", success: true });
|
||||
|
||||
// Step 4: 화면 렌더링 검증
|
||||
console.log("Step 4: 화면 렌더링 검증...");
|
||||
const checks: { name: string; selector: string; found: boolean }[] = [];
|
||||
|
||||
// 버튼 확인
|
||||
const buttons = page.locator("button, [role='button'], input[type='submit'], input[type='button']");
|
||||
const buttonCount = await buttons.count();
|
||||
checks.push({ name: "버튼", selector: "button, [role='button']", found: buttonCount > 0 });
|
||||
|
||||
// 테이블 확인
|
||||
const tables = page.locator("table, [role='grid'], [role='table'], .ag-root");
|
||||
const tableCount = await tables.count();
|
||||
checks.push({ name: "테이블", selector: "table, [role='grid']", found: tableCount > 0 });
|
||||
|
||||
// 검색 필터 확인 (input, select 등)
|
||||
const searchFilters = page.locator('input[type="text"], input[type="search"], input[placeholder*="검색"], input[placeholder*="Search"], select, [class*="filter"], [class*="search"]');
|
||||
const filterCount = await searchFilters.count();
|
||||
checks.push({ name: "검색/필터", selector: "input, select, filter", found: filterCount > 0 });
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "04-screen-29-verified.png"), fullPage: true });
|
||||
|
||||
const allPassed = checks.every((c) => c.found);
|
||||
results.push({
|
||||
step: "4. 화면 렌더링 검증",
|
||||
success: allPassed,
|
||||
message: checks.map((c) => `${c.name}: ${c.found ? "O" : "X"}`).join(", "),
|
||||
});
|
||||
|
||||
// 결과 출력
|
||||
console.log("\n=== 검증 결과 ===");
|
||||
results.forEach((r) => {
|
||||
console.log(`${r.step}: ${r.success ? "성공" : "실패"}${r.message ? ` - ${r.message}` : ""}`);
|
||||
});
|
||||
checks.forEach((c) => {
|
||||
console.log(` - ${c.name}: ${c.found ? "보임" : "없음"}`);
|
||||
});
|
||||
|
||||
const finalSuccess = results.every((r) => r.success);
|
||||
console.log(`\n최종 판정: ${finalSuccess ? "성공" : "실패"}`);
|
||||
|
||||
// 결과를 JSON 파일로 저장
|
||||
const fs = await import("fs");
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verification-result.json"),
|
||||
JSON.stringify({ results, checks, finalSuccess: finalSuccess ? "성공" : "실패" }, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("오류 발생:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "99-error.png"), fullPage: true }).catch(() => {});
|
||||
results.push({ step: "오류", success: false, message: error.message });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 회사 선택 → 메뉴 → 수주/구매관리 화면 검증
|
||||
* 1. 로그인 (topseal7 또는 wace)
|
||||
* 2. 회사 선택 → 탑씰
|
||||
* 3. 영업관리 > 수주관리 또는 구매관리
|
||||
* 4. 데이터 화면 스크린샷
|
||||
* 5. 테이블 가로 스크롤 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const steps: string[] = [];
|
||||
|
||||
try {
|
||||
// Step 1: 로그인 페이지
|
||||
console.log("Step 1: 로그인 페이지 접속...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-01-login-page.png"), fullPage: true });
|
||||
steps.push("01-login-page");
|
||||
|
||||
// Step 2: 로그인 시도 (topseal7 먼저)
|
||||
console.log("Step 2: 로그인 (topseal7 시도)...");
|
||||
await page.fill("#userId", "topseal7");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-02-login-topseal7.png"), fullPage: true });
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const urlAfterLogin = page.url();
|
||||
const isStillLogin = urlAfterLogin.includes("/login");
|
||||
|
||||
if (isStillLogin) {
|
||||
console.log("topseal7 로그인 실패, wace 시도...");
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-02b-login-wace.png"), fullPage: true });
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
await page.waitForURL((url) => !url.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-03-after-login.png"), fullPage: true });
|
||||
steps.push("03-after-login");
|
||||
|
||||
// Step 3: 회사 선택 → 탑씰 (SUPER_ADMIN만 보임, 메인 앱 로드 대기)
|
||||
console.log("Step 3: 회사 선택 클릭...");
|
||||
await page.getByText("현재 관리 회사").waitFor({ timeout: 8000 }).catch(() => {});
|
||||
await page.waitForTimeout(1000);
|
||||
const companyBtn = page.getByText("회사 선택").first();
|
||||
if ((await companyBtn.count()) > 0) {
|
||||
await companyBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-04-company-dropdown.png"), fullPage: true });
|
||||
|
||||
const tapsealOption = page.getByText("탑씰", { exact: true }).first();
|
||||
if ((await tapsealOption.count()) > 0) {
|
||||
await tapsealOption.click();
|
||||
await page.waitForTimeout(2000);
|
||||
console.log("탑씰 선택됨");
|
||||
} else {
|
||||
console.log("탑씰 옵션 없음 - 스킵");
|
||||
}
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-05-after-company.png"), fullPage: true });
|
||||
} else {
|
||||
console.log("회사 선택 버튼 없음");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-05-no-company-btn.png"), fullPage: true });
|
||||
}
|
||||
steps.push("05-after-company");
|
||||
|
||||
// Step 4: 영업관리 > 수주관리 또는 구매관리
|
||||
console.log("Step 4: 메뉴 클릭 (영업관리 > 수주관리)...");
|
||||
const salesMgmt = page.getByText("영업관리").first();
|
||||
if ((await salesMgmt.count()) > 0) {
|
||||
await salesMgmt.click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-06-sales-expanded.png"), fullPage: true });
|
||||
|
||||
const orderMgmt = page.getByText("수주관리").first();
|
||||
if ((await orderMgmt.count()) > 0) {
|
||||
await orderMgmt.click();
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-07-order-screen.png"), fullPage: true });
|
||||
} else {
|
||||
const purchaseMgmt = page.getByText("구매관리").first();
|
||||
if ((await purchaseMgmt.count()) > 0) {
|
||||
await purchaseMgmt.click();
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-07-purchase-screen.png"), fullPage: true });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const purchaseMgmt = page.getByText("구매관리").first();
|
||||
if ((await purchaseMgmt.count()) > 0) {
|
||||
await purchaseMgmt.click();
|
||||
await page.waitForTimeout(3000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-07-purchase-direct.png"), fullPage: true });
|
||||
}
|
||||
}
|
||||
steps.push("07-menu-screen");
|
||||
|
||||
// Step 5: /screens/1244 직접 접속 시도
|
||||
console.log("Step 5: /screens/1244 직접 접속...");
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForTimeout(5000);
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-08-screen-1244.png"), fullPage: true });
|
||||
steps.push("08-screen-1244");
|
||||
|
||||
// Step 6: 테이블 가로 스크롤 확인
|
||||
console.log("Step 6: 테이블 가로 스크롤 확인...");
|
||||
const tableContainer = page.locator("table").locator("..").first();
|
||||
const table = page.locator("table").first();
|
||||
if ((await table.count()) > 0) {
|
||||
const tableBox = await table.boundingBox();
|
||||
const hasOverflowX = await table.evaluate((el) => {
|
||||
const parent = el.closest("[style*='overflow'], [class*='overflow']");
|
||||
return parent ? getComputedStyle(parent as Element).overflowX !== "visible" : false;
|
||||
}).catch(() => false);
|
||||
const scrollWidth = await table.evaluate((el) => el.scrollWidth);
|
||||
const clientWidth = await table.evaluate((el) => el.clientWidth);
|
||||
const canScroll = scrollWidth > clientWidth;
|
||||
console.log(`테이블: scrollWidth=${scrollWidth}, clientWidth=${clientWidth}, 가로스크롤가능=${canScroll}`);
|
||||
}
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-09-table-scroll-check.png"), fullPage: true });
|
||||
steps.push("09-table-scroll");
|
||||
|
||||
// Step 7: 최종 스크린샷
|
||||
console.log("Step 7: 최종 스크린샷...");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-10-final.png"), fullPage: true });
|
||||
steps.push("10-final");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "flow-result.json"),
|
||||
JSON.stringify({ steps, timestamp: new Date().toISOString() }, null, 2)
|
||||
);
|
||||
console.log("\n완료. 스크린샷:", SCREENSHOT_DIR);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "flow-99-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* 수주관리 화면(68) 검증 스크립트
|
||||
* - 로그인 상태 확인 후 필요시 로그인
|
||||
* - /screens/68 접속
|
||||
* - 테이블, 검색 필터, 버튼 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 900 },
|
||||
storageState: undefined, // 새 세션 (쿠키 유지 안 함 - 이전 세션 로그인 상태 확인용)
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const steps: { step: string; success: boolean; message?: string }[] = [];
|
||||
|
||||
try {
|
||||
// Step 1: 로그인 페이지 접속 및 로그인 (Playwright는 매번 새 브라우저이므로 항상 로그인 필요)
|
||||
console.log("Step 1: 로그인...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s68-01-login-page.png"), fullPage: true });
|
||||
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s68-02-login-filled.png"), fullPage: true });
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
steps.push({ step: "로그인", success: true });
|
||||
|
||||
// Step 2: /screens/68 접속
|
||||
console.log("Step 2: /screens/68 접속...");
|
||||
await page.goto(`${BASE_URL}/screens/68`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
|
||||
// 5초 대기 (페이지 완전 로드)
|
||||
console.log("Step 3: 5초 대기 (페이지 완전 로드)...");
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s68-03-screen-loaded.png"), fullPage: true });
|
||||
steps.push({ step: "/screens/68 접속 및 5초 대기", success: true });
|
||||
|
||||
// Step 3: 요소 검증
|
||||
console.log("Step 3: 요소 검증...");
|
||||
|
||||
const hasError = await page.locator('text="화면을 찾을 수 없습니다"').count() > 0;
|
||||
if (hasError) {
|
||||
steps.push({ step: "화면 로드", success: false, message: "404 - 화면을 찾을 수 없습니다" });
|
||||
} else {
|
||||
// 테이블 (TableListComponent: role=grid, table, thead/tbody)
|
||||
const tableSelectors = [
|
||||
"table",
|
||||
"[role='grid']",
|
||||
"[role='table']",
|
||||
"thead",
|
||||
"tbody",
|
||||
".table-mobile-fixed",
|
||||
"[class*='ag-']",
|
||||
"[class*='table-list']",
|
||||
];
|
||||
let tableFound = false;
|
||||
for (const sel of tableSelectors) {
|
||||
if ((await page.locator(sel).count()) > 0) {
|
||||
tableFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 검색/필터 (input, select, 테이블 툴바 검색/필터 버튼)
|
||||
const filterSelectors = [
|
||||
"input",
|
||||
"select",
|
||||
'input[type="text"]',
|
||||
'input[type="search"]',
|
||||
'input[placeholder*="검색"]',
|
||||
"button:has-text('검색')",
|
||||
"button:has-text('필터')",
|
||||
"[class*='filter']",
|
||||
"[class*='search']",
|
||||
];
|
||||
let filterFound = false;
|
||||
for (const sel of filterSelectors) {
|
||||
if ((await page.locator(sel).count()) > 0) {
|
||||
filterFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼
|
||||
const buttonCount = await page.locator("button, [role='button'], input[type='submit']").count();
|
||||
const buttonsFound = buttonCount > 0;
|
||||
|
||||
steps.push({
|
||||
step: "화면 요소 검증",
|
||||
success: tableFound && filterFound && buttonsFound,
|
||||
message: `테이블: ${tableFound ? "O" : "X"}, 검색: ${filterFound ? "O" : "X"}, 버튼: ${buttonsFound ? "O" : "X"}`,
|
||||
});
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s68-04-verified.png"), fullPage: true });
|
||||
|
||||
const finalSuccess = tableFound && filterFound && buttonsFound && !hasError;
|
||||
console.log("\n=== 검증 결과 ===");
|
||||
steps.forEach((s) => console.log(`${s.step}: ${s.success ? "성공" : "실패"}${s.message ? ` - ${s.message}` : ""}`));
|
||||
console.log(`\n최종 판정: ${finalSuccess ? "성공" : "실패"}`);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "s68-result.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
steps,
|
||||
checks: { table: tableFound, filter: filterFound, buttons: buttonsFound },
|
||||
finalSuccess: finalSuccess ? "성공" : "실패",
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s68-99-error.png"), fullPage: true }).catch(() => {});
|
||||
steps.push({ step: "오류", success: false, message: error.message });
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* 화면 94(수주), 124(수주목록 리스트) 검증 스크립트
|
||||
* - 로그인 후 각 화면 접속
|
||||
* - 컴포넌트 배치, 테이블/필터/버튼, 가로 레이아웃 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
interface ScreenResult {
|
||||
screenId: number;
|
||||
name: string;
|
||||
componentsOk: boolean;
|
||||
tableVisible: boolean;
|
||||
filterVisible: boolean;
|
||||
buttonsVisible: boolean;
|
||||
layoutHorizontal: boolean;
|
||||
noError: boolean;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
type ScreenType = "form" | "list";
|
||||
|
||||
async function verifyScreen(page: any, screenId: number, name: string, type: ScreenType): Promise<ScreenResult> {
|
||||
console.log(`\n--- 화면 ${screenId} (${name}) 검증 ---`);
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "domcontentloaded", timeout: 20000 });
|
||||
// 로딩 완료 대기: "로딩중" 텍스트 사라질 때까지 최대 12초
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 12000 }).catch(() => {});
|
||||
// 리스트 화면: 테이블 로딩 대기. 폼 화면: 버튼/input 대기
|
||||
if (type === "list") {
|
||||
await page.waitForSelector("table, [role='grid'], thead, tbody", { timeout: 8000 }).catch(() => {});
|
||||
} else {
|
||||
await page.waitForSelector("button, input", { timeout: 5000 }).catch(() => {});
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const result: ScreenResult = {
|
||||
screenId,
|
||||
name,
|
||||
componentsOk: false,
|
||||
tableVisible: false,
|
||||
filterVisible: false,
|
||||
buttonsVisible: false,
|
||||
layoutHorizontal: false,
|
||||
noError: false,
|
||||
success: false,
|
||||
};
|
||||
|
||||
// 404/에러 메시지 확인
|
||||
const has404 = (await page.locator('text="화면을 찾을 수 없습니다"').count()) > 0;
|
||||
const hasError = (await page.locator('text="오류 발생"').count()) > 0;
|
||||
result.noError = !has404;
|
||||
|
||||
// 테이블
|
||||
const tableSelectors = ["table", "[role='grid']", "thead", "tbody", ".table-mobile-fixed"];
|
||||
for (const sel of tableSelectors) {
|
||||
if ((await page.locator(sel).count()) > 0) {
|
||||
result.tableVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 필터/검색
|
||||
const filterSelectors = ["input", "select", "button:has-text('검색')", "button:has-text('필터')"];
|
||||
for (const sel of filterSelectors) {
|
||||
if ((await page.locator(sel).count()) > 0) {
|
||||
result.filterVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 버튼 (사이드바 포함, 화면에 버튼이 있으면 OK)
|
||||
const buttonCount = await page.locator("button, [role='button']").count();
|
||||
result.buttonsVisible = buttonCount > 0;
|
||||
|
||||
// 가로 레이아웃: 사이드바+메인 구조, flex/grid, 또는 테이블이 있으면 가로 배치로 간주
|
||||
const hasFlexRow = (await page.locator(".flex-row, .md\\:flex-row, .flex").count()) > 0;
|
||||
const hasGrid = (await page.locator(".grid, [class*='grid-cols']").count()) > 0;
|
||||
const hasMain = (await page.locator("main, [role='main'], .flex-1, [class*='flex-1']").count()) > 0;
|
||||
const hasSidebar = (await page.getByText("현재 관리 회사").count()) > 0 || (await page.getByText("VEXPLOR").count()) > 0;
|
||||
result.layoutHorizontal = (hasMain && (hasFlexRow || hasGrid || result.tableVisible)) || hasSidebar;
|
||||
|
||||
// 컴포넌트 정상 배치 (테이블, 버튼, 또는 input/필터 중 하나라도 있으면 OK)
|
||||
result.componentsOk = result.tableVisible || result.buttonsVisible || result.filterVisible;
|
||||
|
||||
// 성공: 폼 화면은 테이블 불필요, 리스트 화면은 테이블 필수
|
||||
const baseOk = result.componentsOk && result.filterVisible && result.buttonsVisible && result.layoutHorizontal && result.noError;
|
||||
result.success = type === "form" ? baseOk : baseOk && result.tableVisible;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const results: ScreenResult[] = [];
|
||||
|
||||
try {
|
||||
// 로그인 (Playwright는 새 브라우저이므로)
|
||||
console.log("로그인...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((url) => !url.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 화면 94
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "s94-01-before.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
const r94 = await verifyScreen(page, 94, "수주", "form");
|
||||
results.push(r94);
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "s94-02-after.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 화면 124
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "s124-01-before.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
const r124 = await verifyScreen(page, 124, "수주목록 리스트", "list");
|
||||
results.push(r124);
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "s124-02-after.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
// 결과 출력
|
||||
console.log("\n=== 검증 결과 ===");
|
||||
results.forEach((r) => {
|
||||
console.log(
|
||||
`화면 ${r.screenId} (${r.name}): ${r.success ? "성공" : "실패"}` +
|
||||
` | 테이블:${r.tableVisible ? "O" : "X"} 필터:${r.filterVisible ? "O" : "X"} 버튼:${r.buttonsVisible ? "O" : "X"} 레이아웃:${r.layoutHorizontal ? "O" : "X"} 에러없음:${r.noError ? "O" : "X"}`
|
||||
);
|
||||
});
|
||||
|
||||
const allSuccess = results.every((r) => r.success);
|
||||
console.log(`\n최종 판정: ${allSuccess ? "성공" : "실패"}`);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "s94-124-result.json"),
|
||||
JSON.stringify({ results, finalSuccess: allSuccess ? "성공" : "실패" }, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "s94-124-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* 화면 156, 4155, 1053 검증: 버튼 레이아웃 및 가시성
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function loginIfNeeded(page: any) {
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyScreen(
|
||||
page: any,
|
||||
screenId: number,
|
||||
report: Record<string, any>
|
||||
) {
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
if (page.url().includes("/login")) {
|
||||
await loginIfNeeded(page);
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll("button"));
|
||||
const buttonDetails = buttons.slice(0, 20).map((btn) => {
|
||||
const text = (btn as HTMLElement).innerText?.trim() || "";
|
||||
const rect = (btn as HTMLElement).getBoundingClientRect();
|
||||
const style = window.getComputedStyle(btn);
|
||||
return {
|
||||
text: text.substring(0, 50),
|
||||
hasText: text.length > 0,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
visible: rect.width > 0 && rect.height > 0,
|
||||
};
|
||||
});
|
||||
const buttonsWithText = buttonDetails.filter((b) => b.hasText);
|
||||
const table = document.querySelector("table");
|
||||
const pagination = document.body.innerText.includes("표시") || document.body.innerText.includes("1/");
|
||||
const splitPanel = document.querySelector("[class*='split'], [class*='Split'], [class*='border-r']");
|
||||
return {
|
||||
pageLoadsWithoutErrors: !document.body.innerText.includes("화면을 찾을 수 없습니다"),
|
||||
totalButtons: buttons.length,
|
||||
buttonsWithTextCount: buttonsWithText.length,
|
||||
buttonsVisibleWithText: buttonsWithText.length > 0,
|
||||
buttonDetails: buttonDetails.slice(0, 10),
|
||||
tableVisible: !!table,
|
||||
paginationVisible: !!pagination,
|
||||
splitPanelVisible: !!splitPanel,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
layoutFitsViewport: document.body.scrollWidth <= window.innerWidth,
|
||||
};
|
||||
});
|
||||
|
||||
report.pageLoadsWithoutErrors = info.pageLoadsWithoutErrors;
|
||||
report.buttonsVisibleWithText = info.buttonsVisibleWithText;
|
||||
report.buttonsWithTextCount = info.buttonsWithTextCount;
|
||||
report.buttonDetails = info.buttonDetails;
|
||||
report.tableVisible = info.tableVisible;
|
||||
report.paginationVisible = info.paginationVisible;
|
||||
report.splitPanelVisible = info.splitPanelVisible;
|
||||
report.layoutFitsViewport = info.layoutFitsViewport;
|
||||
report.hasHorizontalOverflow = info.hasHorizontalOverflow;
|
||||
report.details = {
|
||||
bodyScrollWidth: info.bodyScrollWidth,
|
||||
viewportWidth: info.viewportWidth,
|
||||
viewportHeight: info.viewportHeight,
|
||||
};
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, `screen-${screenId}-buttons.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(`screen-${screenId}-buttons.png saved`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = { screen156: {}, screen4155: {}, screen1053: {} };
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await loginIfNeeded(page);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await verifyScreen(page, 156, report.screen156);
|
||||
await verifyScreen(page, 4155, report.screen4155);
|
||||
await verifyScreen(page, 1053, report.screen1053);
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "button-layout-screens-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "button-layout-error.png"),
|
||||
fullPage: true,
|
||||
}).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* 화면 1053, 156 버튼 위치 검증
|
||||
* 1053: overlay buttons within split panel
|
||||
* 156: buttons in separate row above table
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = { screen1053: {}, screen156: {} };
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.goto(`${BASE_URL}/screens/156`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// Screen 1053
|
||||
await page.goto(`${BASE_URL}/screens/1053?menuObjid=1762421920304`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 25000 }).catch(() => {});
|
||||
await page.waitForTimeout(5000);
|
||||
await page.goto(`${BASE_URL}/screens/1053?menuObjid=1762421920304`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 35000 }).catch(() => {});
|
||||
await page.waitForTimeout(40000);
|
||||
|
||||
const splitPanelEl = page.locator("[class*='border-r'], [class*='split']").first();
|
||||
await splitPanelEl.waitFor({ state: "visible", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const info1053 = await page.evaluate(() => {
|
||||
const splitPanel = document.querySelector("[class*='border-r']") || document.querySelector("main");
|
||||
const mainContent = document.querySelector("main") || document.body;
|
||||
const allBtns = Array.from(document.querySelectorAll("button"));
|
||||
const buttons = allBtns.filter((b) => {
|
||||
const t = (b as HTMLElement).innerText?.trim() || "";
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
return t.length > 1 && r.x > 250 && t.match(/등록|수정|삭제|품목|테이블|결재|수주|출하/);
|
||||
});
|
||||
const splitRect = splitPanel ? (splitPanel as HTMLElement).getBoundingClientRect() : null;
|
||||
const mainRect = mainContent ? (mainContent as HTMLElement).getBoundingClientRect() : null;
|
||||
|
||||
const buttonPositions = buttons.map((b) => {
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
const text = (b as HTMLElement).innerText?.trim().substring(0, 20);
|
||||
return {
|
||||
text,
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
right: r.right,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
isWithinSplitPanel: splitRect
|
||||
? r.y >= splitRect.top - 20 && r.y <= splitRect.bottom + 20
|
||||
: null,
|
||||
isAboveMain: mainRect ? r.y < mainRect.top + 100 : null,
|
||||
};
|
||||
});
|
||||
|
||||
const table = document.querySelector("table");
|
||||
const tableRect = table ? (table as HTMLElement).getBoundingClientRect() : null;
|
||||
const buttonsAboveTable = buttonPositions.every((p) => tableRect && p.y < tableRect.top - 10);
|
||||
|
||||
return {
|
||||
splitPanelVisible: !!splitPanel,
|
||||
splitPanelRect: splitRect ? { top: splitRect.top, bottom: splitRect.bottom, left: splitRect.left, right: splitRect.right } : null,
|
||||
mainRect: mainRect ? { top: mainRect.top, bottom: mainRect.bottom } : null,
|
||||
buttonCount: buttons.length,
|
||||
buttonPositions,
|
||||
buttonsOverlaidOnSplitPanel: buttonPositions.some((p) => p.isWithinSplitPanel),
|
||||
buttonsInSeparateRowAbove: buttonsAboveTable,
|
||||
tableTop: tableRect?.top ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
report.screen1053 = info1053;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "overlay-1053.png"), fullPage: true });
|
||||
console.log("overlay-1053.png saved");
|
||||
|
||||
// Screen 156
|
||||
await page.goto(`${BASE_URL}/screens/156?menuObjid=1762421920156`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(3000);
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 35000 }).catch(() => {});
|
||||
await page.waitForTimeout(40000);
|
||||
|
||||
const table156 = page.locator("table tbody tr").first();
|
||||
await table156.waitFor({ state: "visible", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const info156 = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const tableRect = table ? (table as HTMLElement).getBoundingClientRect() : null;
|
||||
const buttons = Array.from(document.querySelectorAll("button")).filter(
|
||||
(b) => ((b as HTMLElement).innerText?.trim() || "").match(/결재|수주|수정|삭제|출하|테이블/)
|
||||
);
|
||||
const buttonPositions = buttons.map((b) => {
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
const text = (b as HTMLElement).innerText?.trim().substring(0, 20);
|
||||
return {
|
||||
text,
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
right: r.right,
|
||||
width: r.width,
|
||||
isAboveTable: tableRect ? r.y < tableRect.top - 5 : null,
|
||||
};
|
||||
});
|
||||
const allButtonsAboveTable = buttonPositions.every((p) => p.isAboveTable);
|
||||
|
||||
return {
|
||||
tableVisible: !!table,
|
||||
tableTop: tableRect?.top ?? null,
|
||||
buttonCount: buttons.length,
|
||||
buttonPositions,
|
||||
buttonsInSeparateRowAboveTable: allButtonsAboveTable,
|
||||
};
|
||||
});
|
||||
|
||||
report.screen156 = info156;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "overlay-156.png"), fullPage: true });
|
||||
console.log("overlay-156.png saved");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "overlay-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "overlay-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* 반응형 렌더링 검증: 화면 1053, 2089, 156, 4155
|
||||
* 로그인: admin / wace1234!
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
// 1-3: Login
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1500);
|
||||
await page.fill("#userId", "admin");
|
||||
await page.fill("#password", "wace1234!");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
|
||||
async function captureAndVerify(screenId: number, screenName: string) {
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 25000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll("button"));
|
||||
const btnWithText = buttons.filter((b) => (b as HTMLElement).innerText?.trim().length > 0);
|
||||
const splitPanel = document.querySelector("[class*='split'], [class*='Split'], [class*='border-r']");
|
||||
const leftPanel = document.querySelector("[class*='border-r']");
|
||||
const table = document.querySelector("table");
|
||||
const thead = document.querySelector("thead");
|
||||
const tbody = document.querySelector("tbody");
|
||||
const pagination = document.body.innerText.includes("표시") || document.body.innerText.includes("1/");
|
||||
const bodyText = document.body.innerText;
|
||||
const hasOverlap = bodyText.includes("화면을 찾을 수 없습니다") ? false : null;
|
||||
|
||||
const btnDetails = btnWithText.slice(0, 5).map((b) => ({
|
||||
text: (b as HTMLElement).innerText?.trim().substring(0, 30),
|
||||
rect: (b as HTMLElement).getBoundingClientRect(),
|
||||
}));
|
||||
|
||||
return {
|
||||
pageLoadsWithoutErrors: !bodyText.includes("화면을 찾을 수 없습니다"),
|
||||
buttonsVisible: btnWithText.length > 0,
|
||||
buttonsCount: btnWithText.length,
|
||||
buttonDetails: btnDetails,
|
||||
splitPanelVisible: !!splitPanel,
|
||||
leftPanelVisible: !!leftPanel,
|
||||
tableVisible: !!table && !!thead && !!tbody,
|
||||
paginationVisible: !!pagination,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
};
|
||||
});
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, `responsive-${screenId}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(`responsive-${screenId}.png saved`);
|
||||
|
||||
return { screenId, screenName, ...info };
|
||||
}
|
||||
|
||||
// 4: Screen 1053 - 거래처관리
|
||||
report.screen1053 = await captureAndVerify(1053, "거래처관리 - split panel custom mode");
|
||||
|
||||
// 5: Screen 2089 - BOM관리
|
||||
report.screen2089 = await captureAndVerify(2089, "BOM관리 - split panel");
|
||||
|
||||
// 6: Screen 156 - 수주관리
|
||||
report.screen156 = await captureAndVerify(156, "수주관리 - regular screen");
|
||||
|
||||
// 7: Screen 4155 - 작업지시
|
||||
report.screen4155 = await captureAndVerify(4155, "작업지시 - buttons at bottom");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "responsive-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "responsive-error.png"),
|
||||
fullPage: true,
|
||||
}).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* 화면 1053 검증 - admin/1234 로그인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.fill("#userId", "admin");
|
||||
await page.fill("#password", "1234");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/screens/1053`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
await page.goto(`${BASE_URL}/screens/1053`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 35000 }).catch(() => {});
|
||||
await page.waitForTimeout(30000);
|
||||
|
||||
const table = page.locator("table tbody tr").first();
|
||||
await table.waitFor({ state: "visible", timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll("button")).filter((b) => {
|
||||
const t = (b as HTMLElement).innerText?.trim() || "";
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
return t.length > 1 && r.x > 250;
|
||||
});
|
||||
const leftPanel = document.querySelector("[class*='border-r']");
|
||||
const tables = document.querySelectorAll("table");
|
||||
const bodyText = document.body.innerText;
|
||||
|
||||
return {
|
||||
buttonCount: buttons.length,
|
||||
buttonDetails: buttons.slice(0, 15).map((b) => {
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
return {
|
||||
text: (b as HTMLElement).innerText?.trim().substring(0, 30),
|
||||
x: Math.round(r.x),
|
||||
y: Math.round(r.y),
|
||||
width: Math.round(r.width),
|
||||
height: Math.round(r.height),
|
||||
};
|
||||
}),
|
||||
splitPanelVisible: !!leftPanel || bodyText.includes("공급처") || bodyText.includes("좌측에서"),
|
||||
tableCount: tables.length,
|
||||
hasExcelDownload: bodyText.includes("엑셀") || bodyText.includes("다운로드") || bodyText.includes("업로드"),
|
||||
};
|
||||
});
|
||||
|
||||
report.screen1053 = info;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1053-admin.png"), fullPage: true });
|
||||
console.log("screen-1053-admin.png saved");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-1053-admin-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1053-admin-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* 화면 1053 검증: split-panel 레이아웃
|
||||
* - 좌/우 패널, 버튼 오버레이, 높이 채움, overflow 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/screens/1053`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/1053`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
report.pageLoadsWithoutErrors = (await page.locator('text="화면을 찾을 수 없습니다"').count()) === 0;
|
||||
|
||||
const splitPanel = await page.evaluate(() => {
|
||||
const leftPanel = document.querySelector("[class*='split'][class*='left'], [data-panel='left'], [class*='left-panel']");
|
||||
const rightPanel = document.querySelector("[class*='split'][class*='right'], [data-panel='right'], [class*='right-panel']");
|
||||
const resizable = document.querySelector("[class*='resize'], [class*='ResizablePanel]");
|
||||
const panels = document.querySelectorAll("[class*='panel'], [data-panel]");
|
||||
return {
|
||||
hasLeftPanel: !!leftPanel || document.body.innerText.includes("left") || panels.length >= 2,
|
||||
hasRightPanel: !!rightPanel || panels.length >= 2,
|
||||
panelCount: panels.length,
|
||||
resizableCount: document.querySelectorAll("[class*='ResizablePanel'], [class*='resize']").length,
|
||||
};
|
||||
});
|
||||
report.splitPanelVisible = splitPanel.panelCount >= 2 || splitPanel.resizableCount > 0;
|
||||
|
||||
const twoPanels = await page.locator("[class*='panel'], [data-panel], [class*='split']").count();
|
||||
report.twoPanelsFound = twoPanels >= 2;
|
||||
|
||||
const buttons = await page.locator("button").count();
|
||||
const buttonsTopRight = await page.evaluate(() => {
|
||||
const btns = Array.from(document.querySelectorAll("button"));
|
||||
const viewportW = window.innerWidth;
|
||||
return btns.filter((b) => {
|
||||
const r = b.getBoundingClientRect();
|
||||
return r.right > viewportW * 0.5 && r.top < 200;
|
||||
}).length;
|
||||
});
|
||||
report.buttonsVisible = buttons > 0;
|
||||
report.buttonsInTopRightArea = buttonsTopRight;
|
||||
|
||||
const layoutFillsHeight = await page.evaluate(() => {
|
||||
const main = document.querySelector("main") || document.body;
|
||||
const h = (main as HTMLElement).offsetHeight;
|
||||
return h >= window.innerHeight * 0.8;
|
||||
});
|
||||
report.layoutFillsHeight = layoutFillsHeight;
|
||||
|
||||
const overflow = await page.evaluate(() => ({
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
bodyScrollHeight: document.body.scrollHeight,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
hasVerticalOverflow: document.body.scrollHeight > window.innerHeight,
|
||||
}));
|
||||
report.noContentOverflowsViewport = !overflow.hasHorizontalOverflow;
|
||||
report.overflowDetails = overflow;
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1053-snapshot.png"), fullPage: true });
|
||||
console.log("screen-1053-snapshot.png saved");
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-1053-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1053-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* 화면 1244 검증: table-list 레이아웃 (데스크톱 + 모바일)
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = { desktop: {}, mobile: {} };
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
report.desktop.pageLoadsWithoutErrors =
|
||||
(await page.locator('text="화면을 찾을 수 없습니다"').count()) === 0;
|
||||
|
||||
const desktopInfo = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const thead = document.querySelector("thead");
|
||||
const tbody = document.querySelector("tbody");
|
||||
const ths = document.querySelectorAll("thead th");
|
||||
const buttons = document.querySelectorAll("button");
|
||||
const searchInputs = document.querySelectorAll('input[type="text"], input[type="search"], select');
|
||||
const pagination = document.body.innerText.includes("표시") ||
|
||||
document.body.innerText.includes("1/") ||
|
||||
document.body.innerText.includes("페이지") ||
|
||||
document.querySelector("[class*='pagination'], [class*='Pagination']");
|
||||
|
||||
let buttonsBetweenSearchAndTable = 0;
|
||||
const searchY = searchInputs.length > 0
|
||||
? (searchInputs[0] as HTMLElement).getBoundingClientRect().bottom
|
||||
: 0;
|
||||
const tableY = table ? (table as HTMLElement).getBoundingClientRect().top : 0;
|
||||
|
||||
buttons.forEach((btn) => {
|
||||
const rect = (btn as HTMLElement).getBoundingClientRect();
|
||||
if (rect.top >= searchY - 20 && rect.top <= tableY + 100) buttonsBetweenSearchAndTable++;
|
||||
});
|
||||
|
||||
return {
|
||||
tableVisible: !!table && !!thead && !!tbody,
|
||||
columnCount: ths.length,
|
||||
buttonsVisible: buttons.length > 0,
|
||||
buttonsBetweenSearchAndTable,
|
||||
paginationVisible: !!pagination,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
tableScrollWidth: table ? (table as HTMLElement).scrollWidth : 0,
|
||||
tableClientWidth: table ? (table as HTMLElement).clientWidth : 0,
|
||||
};
|
||||
});
|
||||
|
||||
report.desktop.buttonsVisible = desktopInfo.buttonsVisible;
|
||||
report.desktop.buttonsBetweenSearchAndTable = desktopInfo.buttonsBetweenSearchAndTable;
|
||||
report.desktop.tableVisible = desktopInfo.tableVisible;
|
||||
report.desktop.columnCount = desktopInfo.columnCount;
|
||||
report.desktop.paginationVisible = desktopInfo.paginationVisible;
|
||||
report.desktop.noHorizontalOverflow = !desktopInfo.hasHorizontalOverflow;
|
||||
report.desktop.overflowDetails = {
|
||||
bodyScrollWidth: desktopInfo.bodyScrollWidth,
|
||||
viewportWidth: desktopInfo.viewportWidth,
|
||||
tableScrollWidth: desktopInfo.tableScrollWidth,
|
||||
tableClientWidth: desktopInfo.tableClientWidth,
|
||||
};
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "screen-1244-desktop.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log("screen-1244-desktop.png saved");
|
||||
|
||||
await page.setViewportSize({ width: 768, height: 900 });
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const mobileInfo = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const thead = document.querySelector("thead");
|
||||
const tbody = document.querySelector("tbody");
|
||||
const ths = document.querySelectorAll("thead th");
|
||||
const buttons = document.querySelectorAll("button");
|
||||
const pagination = document.body.innerText.includes("표시") ||
|
||||
document.body.innerText.includes("1/") ||
|
||||
document.body.innerText.includes("페이지") ||
|
||||
document.querySelector("[class*='pagination'], [class*='Pagination']");
|
||||
|
||||
return {
|
||||
tableVisible: !!table && !!thead && !!tbody,
|
||||
columnCount: ths.length,
|
||||
buttonsVisible: buttons.length > 0,
|
||||
paginationVisible: !!pagination,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
viewportWidth: window.innerWidth,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
};
|
||||
});
|
||||
|
||||
report.mobile.pageLoadsWithoutErrors = report.desktop.pageLoadsWithoutErrors;
|
||||
report.mobile.buttonsVisible = mobileInfo.buttonsVisible;
|
||||
report.mobile.tableVisible = mobileInfo.tableVisible;
|
||||
report.mobile.columnCount = mobileInfo.columnCount;
|
||||
report.mobile.paginationVisible = mobileInfo.paginationVisible;
|
||||
report.mobile.noHorizontalOverflow = !mobileInfo.hasHorizontalOverflow;
|
||||
report.mobile.overflowDetails = {
|
||||
bodyScrollWidth: mobileInfo.bodyScrollWidth,
|
||||
viewportWidth: mobileInfo.viewportWidth,
|
||||
};
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "screen-1244-mobile-768.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log("screen-1244-mobile-768.png saved");
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-1244-layout-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "screen-1244-error.png"),
|
||||
fullPage: true,
|
||||
}).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/**
|
||||
* 화면 1244 새로고침 후 상세 검증
|
||||
* - data-screen-runtime, 테이블, body의 scrollWidth/clientWidth 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const results: Record<string, number | string | null> = {};
|
||||
|
||||
try {
|
||||
// 로그인
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// /screens/1244 접속
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "load", timeout: 45000 });
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 1: 새로고침 (Ctrl+Shift+R - hard refresh)
|
||||
console.log("Step 1: 새로고침 (Ctrl+Shift+R)...");
|
||||
await page.keyboard.press("Control+Shift+r");
|
||||
await page.waitForLoadState("load");
|
||||
await page.waitForTimeout(3000);
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Step 2: 첫 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-06.png"), fullPage: true });
|
||||
console.log("verify-06.png 저장");
|
||||
|
||||
// Step 3: JavaScript로 dimension 확인
|
||||
const dims = await page.evaluate(() => {
|
||||
const screenRuntime = document.querySelector("[data-screen-runtime]");
|
||||
const table = document.querySelector("table");
|
||||
const body = document.body;
|
||||
const html = document.documentElement;
|
||||
|
||||
const tableContainer = table?.closest("[style*='overflow'], [class*='overflow']") || table?.parentElement;
|
||||
|
||||
return {
|
||||
screenRuntime: screenRuntime
|
||||
? {
|
||||
offsetWidth: (screenRuntime as HTMLElement).offsetWidth,
|
||||
scrollWidth: (screenRuntime as HTMLElement).scrollWidth,
|
||||
clientWidth: (screenRuntime as HTMLElement).clientWidth,
|
||||
}
|
||||
: null,
|
||||
table: table
|
||||
? {
|
||||
offsetWidth: table.offsetWidth,
|
||||
scrollWidth: table.scrollWidth,
|
||||
clientWidth: table.clientWidth,
|
||||
}
|
||||
: null,
|
||||
tableContainer: tableContainer
|
||||
? {
|
||||
clientWidth: (tableContainer as HTMLElement).clientWidth,
|
||||
scrollWidth: (tableContainer as HTMLElement).scrollWidth,
|
||||
offsetWidth: (tableContainer as HTMLElement).offsetWidth,
|
||||
}
|
||||
: null,
|
||||
body: {
|
||||
scrollWidth: body.scrollWidth,
|
||||
clientWidth: body.clientWidth,
|
||||
offsetWidth: body.offsetWidth,
|
||||
},
|
||||
html: {
|
||||
scrollWidth: html.scrollWidth,
|
||||
clientWidth: html.clientWidth,
|
||||
},
|
||||
viewport: {
|
||||
innerWidth: window.innerWidth,
|
||||
innerHeight: window.innerHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
results.screenRuntime_offsetWidth = dims.screenRuntime?.offsetWidth ?? null;
|
||||
results.screenRuntime_scrollWidth = dims.screenRuntime?.scrollWidth ?? null;
|
||||
results.screenRuntime_clientWidth = dims.screenRuntime?.clientWidth ?? null;
|
||||
results.table_offsetWidth = dims.table?.offsetWidth ?? null;
|
||||
results.table_scrollWidth = dims.table?.scrollWidth ?? null;
|
||||
results.table_clientWidth = dims.table?.clientWidth ?? null;
|
||||
results.tableContainer_clientWidth = dims.tableContainer?.clientWidth ?? null;
|
||||
results.tableContainer_scrollWidth = dims.tableContainer?.scrollWidth ?? null;
|
||||
results.body_scrollWidth = dims.body.scrollWidth;
|
||||
results.body_clientWidth = dims.body.clientWidth;
|
||||
results.viewport_innerWidth = dims.viewport.innerWidth;
|
||||
|
||||
// Step 4: 가로 overflow 확인
|
||||
const hasOverflow = dims.body.scrollWidth > dims.viewport.innerWidth;
|
||||
results.bodyOverflowX = hasOverflow;
|
||||
|
||||
// Step 5: 두 번째 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-07.png"), fullPage: true });
|
||||
console.log("verify-07.png 저장");
|
||||
|
||||
console.log("\n=== 검증 결과 ===");
|
||||
console.log("data-screen-runtime div:");
|
||||
console.log(" offsetWidth:", results.screenRuntime_offsetWidth);
|
||||
console.log(" scrollWidth:", results.screenRuntime_scrollWidth);
|
||||
console.log(" clientWidth:", results.screenRuntime_clientWidth);
|
||||
console.log("테이블:");
|
||||
console.log(" offsetWidth:", results.table_offsetWidth);
|
||||
console.log(" scrollWidth:", results.table_scrollWidth);
|
||||
console.log(" clientWidth:", results.table_clientWidth);
|
||||
console.log("테이블 컨테이너:");
|
||||
console.log(" clientWidth:", results.tableContainer_clientWidth);
|
||||
console.log(" scrollWidth:", results.tableContainer_scrollWidth);
|
||||
console.log("body:");
|
||||
console.log(" scrollWidth:", results.body_scrollWidth);
|
||||
console.log(" clientWidth:", results.body_clientWidth);
|
||||
console.log("뷰포트 innerWidth:", results.viewport_innerWidth);
|
||||
console.log("가로 overflow:", results.bodyOverflowX);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verify-refresh-result.json"),
|
||||
JSON.stringify({ ...results, rawDims: dims }, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-99-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* 화면 1244 새로고침 검증 (2차)
|
||||
* - 3초 대기 후 스크린샷
|
||||
* - data-screen-runtime, 테이블 관련 div width 확인
|
||||
* - 가로 스크롤 가능 여부 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
// 로그인
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// /screens/1244 접속
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "load", timeout: 45000 });
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 1: 새로고침 (Ctrl+Shift+R)
|
||||
console.log("Step 1: 새로고침 (Ctrl+Shift+R)...");
|
||||
await page.keyboard.press("Control+Shift+r");
|
||||
await page.waitForLoadState("load");
|
||||
console.log("Step 2: 3초 대기...");
|
||||
await page.waitForTimeout(3000);
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 8000 }).catch(() => {});
|
||||
|
||||
// Step 3: 첫 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-08.png"), fullPage: true });
|
||||
console.log("verify-08.png 저장");
|
||||
|
||||
// Step 4: JavaScript로 width 확인 (순수 함수로 작성)
|
||||
const dims = await page.evaluate(() => {
|
||||
const screenRuntime = document.querySelector("[data-screen-runtime]");
|
||||
const table = document.querySelector("table");
|
||||
const tableContainer = table?.closest("[style*='overflow'], [class*='overflow']");
|
||||
const overflowHiddenDiv = table?.closest("[style*='overflow-hidden'], [class*='overflow-hidden']");
|
||||
|
||||
const tableAncestors: Array<{ level: number; tag: string; class: string; offsetWidth: number; scrollWidth: number; clientWidth: number; overflowX: string }> = [];
|
||||
let p = table?.parentElement;
|
||||
let idx = 0;
|
||||
while (p && idx < 6) {
|
||||
const s = window.getComputedStyle(p);
|
||||
tableAncestors.push({
|
||||
level: idx,
|
||||
tag: p.tagName,
|
||||
class: (p.className && typeof p.className === "string" ? p.className : "").slice(0, 60),
|
||||
offsetWidth: p.offsetWidth,
|
||||
scrollWidth: p.scrollWidth,
|
||||
clientWidth: p.clientWidth,
|
||||
overflowX: s.overflowX,
|
||||
});
|
||||
p = p.parentElement;
|
||||
idx++;
|
||||
}
|
||||
|
||||
return {
|
||||
screenRuntime: screenRuntime
|
||||
? { offsetWidth: (screenRuntime as HTMLElement).offsetWidth, scrollWidth: (screenRuntime as HTMLElement).scrollWidth, clientWidth: (screenRuntime as HTMLElement).clientWidth }
|
||||
: null,
|
||||
table: table
|
||||
? { offsetWidth: (table as HTMLElement).offsetWidth, scrollWidth: (table as HTMLElement).scrollWidth, clientWidth: (table as HTMLElement).clientWidth }
|
||||
: null,
|
||||
tableContainer: tableContainer
|
||||
? { offsetWidth: (tableContainer as HTMLElement).offsetWidth, scrollWidth: (tableContainer as HTMLElement).scrollWidth, clientWidth: (tableContainer as HTMLElement).clientWidth }
|
||||
: null,
|
||||
overflowHiddenDiv: overflowHiddenDiv
|
||||
? { offsetWidth: (overflowHiddenDiv as HTMLElement).offsetWidth, scrollWidth: (overflowHiddenDiv as HTMLElement).scrollWidth, clientWidth: (overflowHiddenDiv as HTMLElement).clientWidth }
|
||||
: null,
|
||||
tableAncestors,
|
||||
viewport: { innerWidth: window.innerWidth },
|
||||
};
|
||||
});
|
||||
|
||||
console.log("\n=== JavaScript 실행 결과 ===");
|
||||
console.log("data-screen-runtime div:");
|
||||
if (dims.screenRuntime) {
|
||||
console.log(" offsetWidth:", dims.screenRuntime.offsetWidth);
|
||||
console.log(" scrollWidth:", dims.screenRuntime.scrollWidth);
|
||||
console.log(" clientWidth:", dims.screenRuntime.clientWidth);
|
||||
} else {
|
||||
console.log(" (없음)");
|
||||
}
|
||||
console.log("\n테이블:");
|
||||
if (dims.table) {
|
||||
console.log(" offsetWidth:", dims.table.offsetWidth);
|
||||
console.log(" scrollWidth:", dims.table.scrollWidth);
|
||||
}
|
||||
console.log("\n테이블 컨테이너 (overflow):");
|
||||
if (dims.tableContainer) {
|
||||
console.log(" offsetWidth:", dims.tableContainer.offsetWidth);
|
||||
console.log(" scrollWidth:", dims.tableContainer.scrollWidth);
|
||||
console.log(" clientWidth:", dims.tableContainer.clientWidth);
|
||||
}
|
||||
console.log("\noverflow-hidden div:");
|
||||
if (dims.overflowHiddenDiv) {
|
||||
console.log(" offsetWidth:", dims.overflowHiddenDiv.offsetWidth);
|
||||
console.log(" scrollWidth:", dims.overflowHiddenDiv.scrollWidth);
|
||||
} else {
|
||||
console.log(" (없음)");
|
||||
}
|
||||
console.log("\n테이블 조상 div들 (width):");
|
||||
dims.tableAncestors?.forEach((a) => {
|
||||
console.log(` L${a.level} ${a.tag} overflow=${a.overflowX} offsetW=${a.offsetWidth} scrollW=${a.scrollWidth} clientW=${a.clientWidth}`);
|
||||
});
|
||||
|
||||
// Step 5: 가로 스크롤 가능 여부
|
||||
const canScroll = dims.table && dims.tableContainer && dims.table.scrollWidth > dims.tableContainer.clientWidth;
|
||||
console.log("\n가로 스크롤 가능:", canScroll, "(테이블 scrollWidth > 컨테이너 clientWidth)");
|
||||
|
||||
// Step 6: 최종 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-09.png"), fullPage: true });
|
||||
console.log("\nverify-09.png 저장");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verify-refresh2-result.json"),
|
||||
JSON.stringify({ ...dims, canScroll }, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-99-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* 화면 1244 검증: 테이블, 가로 스크롤, 페이지네이션 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const results: Record<string, boolean | string> = {};
|
||||
|
||||
try {
|
||||
// Step 1: 로그인 먼저 (Playwright는 새 브라우저)
|
||||
console.log("Step 1: 로그인...");
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 2: /screens/1244 접속
|
||||
console.log("Step 2: /screens/1244 접속...");
|
||||
await page.goto(`${BASE_URL}/screens/1244`, { waitUntil: "load", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 2: 화면 로드 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-01-initial.png"), fullPage: true });
|
||||
console.log("verify-01-initial.png 저장");
|
||||
|
||||
// Step 3: 테이블 확인
|
||||
const table = page.locator("table").first();
|
||||
const tableCount = await table.count();
|
||||
results.tableVisible = tableCount > 0;
|
||||
console.log("테이블 보임:", results.tableVisible);
|
||||
|
||||
// Step 4: 가로 스크롤바 확인
|
||||
const scrollContainer = page.locator("[class*='overflow'], .overflow-x-auto, [style*='overflow']").first();
|
||||
const hasScrollContainer = (await scrollContainer.count()) > 0;
|
||||
let scrollWidth = 0;
|
||||
let clientWidth = 0;
|
||||
if (results.tableVisible) {
|
||||
scrollWidth = await table.evaluate((el) => el.scrollWidth);
|
||||
clientWidth = await table.evaluate((el) => el.clientWidth);
|
||||
results.tableScrollWidth = String(scrollWidth);
|
||||
results.tableClientWidth = String(clientWidth);
|
||||
results.horizontalScrollNeeded = scrollWidth > clientWidth;
|
||||
}
|
||||
console.log("테이블 scrollWidth:", scrollWidth, "clientWidth:", clientWidth);
|
||||
|
||||
// Step 5: 테이블 영역 오른쪽 스크롤 시도 (overflow-auto인 조상 요소 찾기)
|
||||
if (results.tableVisible) {
|
||||
try {
|
||||
const scrollableAncestor = await table.evaluateHandle((el) => {
|
||||
let parent: HTMLElement | null = el.parentElement;
|
||||
while (parent) {
|
||||
const style = getComputedStyle(parent);
|
||||
if (style.overflowX === "auto" || style.overflowX === "scroll" || style.overflow === "auto") {
|
||||
return parent;
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return el.parentElement;
|
||||
});
|
||||
const scrollEl = scrollableAncestor.asElement();
|
||||
if (scrollEl) {
|
||||
await scrollEl.evaluate((el) => (el.scrollLeft = 300));
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-02-after-scroll.png"), fullPage: true });
|
||||
console.log("verify-02-after-scroll.png 저장");
|
||||
|
||||
// Step 6: 페이지네이션 확인
|
||||
const paginationText = page.getByText("표시", { exact: false }).or(page.getByText("1/1", { exact: false }));
|
||||
results.paginationVisible = (await paginationText.count()) > 0;
|
||||
console.log("페이지네이션 보임:", results.paginationVisible);
|
||||
|
||||
// Step 7: 테이블이 뷰포트에 맞는지 (overflow 확인)
|
||||
const bodyOverflow = await page.evaluate(() => {
|
||||
const main = document.querySelector("main") || document.body;
|
||||
return window.getComputedStyle(main).overflowX;
|
||||
});
|
||||
results.bodyOverflowX = bodyOverflow;
|
||||
|
||||
// Step 8: 중간 스크린샷 (테이블 + 페이지네이션 영역)
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-03-mid.png"), fullPage: true });
|
||||
console.log("verify-03-mid.png 저장");
|
||||
|
||||
// Step 9: 페이지 하단으로 스크롤 (페이지네이션 바 확인)
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-04-pagination-area.png"), fullPage: true });
|
||||
console.log("verify-04-pagination-area.png 저장");
|
||||
|
||||
// Step 10: 최종 스크린샷
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-05-final.png"), fullPage: true });
|
||||
console.log("verify-05-final.png 저장");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verify-result.json"),
|
||||
JSON.stringify(results, null, 2)
|
||||
);
|
||||
console.log("\n검증 결과:", results);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-99-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* 화면 150 검증 - 탑씰 영업 거래처관리
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 90000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.fill("#userId", "admin");
|
||||
await page.fill("#password", "1234");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
const companyBtn = page.getByText("회사 선택").first();
|
||||
if ((await companyBtn.count()) > 0) {
|
||||
const currentCompany = await page.getByText("현재 관리 회사").locator("..").textContent().catch(() => "");
|
||||
if (!currentCompany?.includes("탑씰") && !currentCompany?.includes("COMPANY_7")) {
|
||||
await companyBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
const tapseal = page.getByText("탑씰", { exact: true }).first();
|
||||
const company7 = page.getByText("COMPANY_7", { exact: true }).first();
|
||||
if ((await tapseal.count()) > 0) {
|
||||
await tapseal.click();
|
||||
} else if ((await company7.count()) > 0) {
|
||||
await company7.click();
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/screens/150`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
await page.goto(`${BASE_URL}/screens/150`, { waitUntil: "domcontentloaded", timeout: 60000 });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 25000 }).catch(() => {});
|
||||
await page.waitForTimeout(8000);
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll("button")).filter((b) => {
|
||||
const t = (b as HTMLElement).innerText?.trim() || "";
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
return t.length > 1 && r.x > 250 && r.width > 0;
|
||||
});
|
||||
const viewportWidth = window.innerWidth;
|
||||
const leftThird = viewportWidth * 0.33;
|
||||
const rightThird = viewportWidth * 0.66;
|
||||
|
||||
const btnDetails = buttons.map((b) => {
|
||||
const r = (b as HTMLElement).getBoundingClientRect();
|
||||
const text = (b as HTMLElement).innerText?.trim().substring(0, 40);
|
||||
let group = "center";
|
||||
if (r.x < leftThird) group = "left";
|
||||
else if (r.x > rightThird) group = "right";
|
||||
return {
|
||||
text,
|
||||
x: Math.round(r.x),
|
||||
y: Math.round(r.y),
|
||||
width: Math.round(r.width),
|
||||
height: Math.round(r.height),
|
||||
right: Math.round(r.right),
|
||||
group,
|
||||
};
|
||||
});
|
||||
|
||||
const leftPanel = document.querySelector("[class*='border-r']");
|
||||
const tables = document.querySelectorAll("table");
|
||||
const rightPanel = document.querySelector("main")?.querySelectorAll("[class*='overflow'], [style*='overflow']");
|
||||
const leftRect = leftPanel ? (leftPanel as HTMLElement).getBoundingClientRect() : null;
|
||||
const mainRect = document.querySelector("main")?.getBoundingClientRect();
|
||||
const contentWidth = mainRect ? mainRect.width : viewportWidth;
|
||||
const leftWidthPercent = leftRect && contentWidth > 0 ? (leftRect.width / contentWidth) * 100 : null;
|
||||
|
||||
let overlaps = false;
|
||||
for (let i = 0; i < btnDetails.length; i++) {
|
||||
for (let j = i + 1; j < btnDetails.length; j++) {
|
||||
const a = btnDetails[i];
|
||||
const b = btnDetails[j];
|
||||
if (Math.abs(a.y - b.y) < 30 && !(a.right < b.x || b.right < a.x)) overlaps = true;
|
||||
}
|
||||
}
|
||||
|
||||
const rightTable = tables.length > 1 ? tables[1] : tables[0];
|
||||
const rightTableRect = rightTable ? (rightTable as HTMLElement).getBoundingClientRect() : null;
|
||||
const rightTableScrollable = rightTable
|
||||
? (() => {
|
||||
let el: Element | null = rightTable;
|
||||
while (el) {
|
||||
const s = window.getComputedStyle(el);
|
||||
if (s.overflowY === "auto" || s.overflowY === "scroll" || s.overflow === "auto") return true;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return false;
|
||||
})()
|
||||
: null;
|
||||
|
||||
return {
|
||||
buttonCount: buttons.length,
|
||||
buttonDetails: btnDetails,
|
||||
leftGroup: btnDetails.filter((b) => b.group === "left"),
|
||||
centerGroup: btnDetails.filter((b) => b.group === "center"),
|
||||
rightGroup: btnDetails.filter((b) => b.group === "right"),
|
||||
splitPanelVisible: !!leftPanel,
|
||||
leftWidthPercent: leftWidthPercent ? Math.round(leftWidthPercent) : null,
|
||||
rightWidthPercent: leftWidthPercent ? Math.round(100 - leftWidthPercent) : null,
|
||||
tableCount: tables.length,
|
||||
rightPanelHasTable: !!rightTable,
|
||||
rightTableScrollable,
|
||||
buttonsOverlap: overlaps,
|
||||
};
|
||||
});
|
||||
|
||||
report.screen150 = info;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-150-tapseal.png"), fullPage: true });
|
||||
console.log("screen-150-tapseal.png saved");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-150-tapseal-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-150-tapseal-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* 화면 1556 검증: tabs-widget 레이아웃
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/screens/1556`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/1556`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
report.pageLoadsWithoutErrors = (await page.locator('text="화면을 찾을 수 없습니다"').count()) === 0;
|
||||
|
||||
const tabInfo = await page.evaluate(() => {
|
||||
const tabs = document.querySelectorAll("[role='tab'], [data-state='active'], [class*='TabsTrigger'], [class*='tab']");
|
||||
const tabList = document.querySelector("[role='tablist']");
|
||||
const loading = document.body.innerText.includes("로딩중") || document.body.innerText.includes("로딩 중");
|
||||
return {
|
||||
tabCount: tabs.length,
|
||||
hasTabList: !!tabList,
|
||||
tabHeadersVisible: tabs.length > 0 || !!tabList,
|
||||
stuckOnLoading: loading,
|
||||
};
|
||||
});
|
||||
report.tabHeadersVisible = tabInfo.tabHeadersVisible;
|
||||
report.tabCount = tabInfo.tabCount;
|
||||
report.tabContentLoadsProperly = !tabInfo.stuckOnLoading;
|
||||
|
||||
const loadingText = await page.getByText("로딩중", { exact: false }).count();
|
||||
report.stuckOnLoading = loadingText > 0;
|
||||
|
||||
const layoutFillsHeight = await page.evaluate(() => {
|
||||
const main = document.querySelector("main") || document.body;
|
||||
const h = (main as HTMLElement).offsetHeight;
|
||||
return h >= window.innerHeight * 0.8;
|
||||
});
|
||||
report.layoutFillsHeight = layoutFillsHeight;
|
||||
|
||||
const overflow = await page.evaluate(() => ({
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
bodyScrollHeight: document.body.scrollHeight,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
hasVerticalOverflow: document.body.scrollHeight > window.innerHeight,
|
||||
}));
|
||||
report.noContentOverflowsViewport = !overflow.hasHorizontalOverflow;
|
||||
report.overflowDetails = overflow;
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1556-snapshot.png"), fullPage: true });
|
||||
console.log("screen-1556-snapshot.png saved");
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-1556-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-1556-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* 화면 156 검증: 로드, 버튼, 테이블, 페이지네이션, 레이아웃
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/screens/156`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const url = page.url();
|
||||
if (url.includes("/login")) {
|
||||
report.loginRequired = true;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-156-login.png"), fullPage: true });
|
||||
console.log("Login page - logging in with wace...");
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/156`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
const currentUrl = page.url();
|
||||
report.loginRequired = currentUrl.includes("/login");
|
||||
if (!currentUrl.includes("/login")) {
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const hasError = (await page.locator('text="화면을 찾을 수 없습니다"').count()) > 0;
|
||||
report.pageLoadsWithoutErrors = !hasError;
|
||||
|
||||
const buttonCount = await page.locator("button").count();
|
||||
const buttonsBetween = await page.evaluate(() => {
|
||||
const searchWidget = document.querySelector("[class*='search'], [class*='filter']");
|
||||
const table = document.querySelector("table");
|
||||
const buttons = document.querySelectorAll("button");
|
||||
let between = 0;
|
||||
buttons.forEach((btn) => {
|
||||
const rect = btn.getBoundingClientRect();
|
||||
if (searchWidget && table) {
|
||||
const sRect = searchWidget.getBoundingClientRect();
|
||||
const tRect = table.getBoundingClientRect();
|
||||
if (rect.top > sRect.bottom && rect.top < tRect.top) between++;
|
||||
}
|
||||
});
|
||||
return between;
|
||||
});
|
||||
report.buttonsVisible = buttonCount > 0;
|
||||
report.buttonsBetweenSearchAndTable = buttonsBetween;
|
||||
|
||||
const table = page.locator("table").first();
|
||||
const tableVisible = (await table.count()) > 0;
|
||||
report.tableVisible = tableVisible;
|
||||
|
||||
let tableOverflow = false;
|
||||
if (tableVisible) {
|
||||
const dims = await table.evaluate((el) => ({
|
||||
scrollWidth: el.scrollWidth,
|
||||
clientWidth: el.clientWidth,
|
||||
}));
|
||||
tableOverflow = dims.scrollWidth > dims.clientWidth;
|
||||
report.tableScrollWidth = dims.scrollWidth;
|
||||
report.tableClientWidth = dims.clientWidth;
|
||||
}
|
||||
report.tableOverflowsHorizontally = tableOverflow;
|
||||
|
||||
const paginationVisible = (await page.getByText("표시", { exact: false }).count()) > 0 ||
|
||||
(await page.getByText("1/", { exact: false }).count()) > 0;
|
||||
report.paginationBarVisible = paginationVisible;
|
||||
|
||||
const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth);
|
||||
const viewportWidth = 1280;
|
||||
report.bodyScrollWidth = bodyScrollWidth;
|
||||
report.hasHorizontalScrollbar = bodyScrollWidth > viewportWidth;
|
||||
report.layoutResponsive = !report.hasHorizontalScrollbar;
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-156-snapshot.png"), fullPage: true });
|
||||
console.log("screen-156-snapshot.png saved");
|
||||
}
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "screen-156-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "screen-156-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/**
|
||||
* 화면 156, 1053 재검증 - 로딩 완료 후 스크린샷
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = { screen156: {}, screen1053: {} };
|
||||
|
||||
try {
|
||||
// 1-2: Ensure logged in - goto login first, then login
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.goto(`${BASE_URL}/screens/156`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(2000);
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 20000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
// 3: Company selection if present
|
||||
const companyBtn = page.getByText("회사 선택").first();
|
||||
if ((await companyBtn.count()) > 0) {
|
||||
await companyBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
const companyOption = page.getByText("company7", { exact: true }).first();
|
||||
if ((await companyOption.count()) > 0) {
|
||||
await companyOption.click();
|
||||
} else {
|
||||
const anyOption = page.locator("[role='menuitem'], [role='option'], button").filter({ hasText: /회사|company/i }).first();
|
||||
if ((await anyOption.count()) > 0) await anyOption.click();
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
// 4: Screen 156 with menuObjid
|
||||
await page.goto(`${BASE_URL}/screens/156?menuObjid=1762421920156`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
await page.goto(`${BASE_URL}/screens/156?menuObjid=1762421920156`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 35000 }).catch(() => {});
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(30000);
|
||||
|
||||
const table156 = page.locator("table tbody tr");
|
||||
await table156.first().waitFor({ state: "visible", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const info156 = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const tbody = document.querySelector("tbody");
|
||||
const rows = document.querySelectorAll("tbody tr");
|
||||
const buttons = Array.from(document.querySelectorAll("button")).filter((b) => (b as HTMLElement).innerText?.trim().length > 2);
|
||||
const pagination = document.body.innerText.includes("표시") || document.body.innerText.includes("1/");
|
||||
return {
|
||||
tableVisible: !!table && !!tbody,
|
||||
dataRowCount: rows.length,
|
||||
buttonsWithText: buttons.length,
|
||||
paginationVisible: !!pagination,
|
||||
buttonLabels: buttons.slice(0, 8).map((b) => (b as HTMLElement).innerText?.trim().substring(0, 20)),
|
||||
};
|
||||
});
|
||||
|
||||
report.screen156 = info156;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "responsive-156-v2.png"), fullPage: true });
|
||||
console.log("responsive-156-v2.png saved");
|
||||
|
||||
// 5: Screen 1053 with menuObjid
|
||||
await page.goto(`${BASE_URL}/screens/1053?menuObjid=1762421920304`, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.getByText("화면을 불러오는 중", { exact: false }).waitFor({ state: "hidden", timeout: 35000 }).catch(() => {});
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(30000);
|
||||
|
||||
const splitPanel = page.locator("[class*='border-r'], [class*='split']").first();
|
||||
await splitPanel.waitFor({ state: "visible", timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const info1053 = await page.evaluate(() => {
|
||||
const leftPanel = document.querySelector("[class*='border-r']");
|
||||
const rightPanel = document.querySelector("main")?.querySelectorAll("div") || [];
|
||||
const buttons = Array.from(document.querySelectorAll("button")).filter((b) => (b as HTMLElement).innerText?.trim().length > 2);
|
||||
const table = document.querySelector("table");
|
||||
const bodyText = document.body.innerText;
|
||||
const hasSplitContent = bodyText.includes("좌측에서") || bodyText.includes("공급처") || bodyText.includes("품목");
|
||||
return {
|
||||
splitPanelVisible: !!leftPanel || hasSplitContent,
|
||||
buttonsWithText: buttons.length,
|
||||
tableVisible: !!table,
|
||||
buttonLabels: buttons.slice(0, 8).map((b) => (b as HTMLElement).innerText?.trim().substring(0, 25)),
|
||||
};
|
||||
});
|
||||
|
||||
report.screen1053 = info1053;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "responsive-1053-v2.png"), fullPage: true });
|
||||
console.log("responsive-1053-v2.png saved");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verify-156-1053-v2-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-v2-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* 화면 1722, 2089 검증: split-panel-layout2
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function loginIfNeeded(page: any) {
|
||||
if (page.url().includes("/login")) {
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u: URL) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyScreen(
|
||||
page: any,
|
||||
screenId: number,
|
||||
report: Record<string, any>
|
||||
) {
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
if (page.url().includes("/login")) {
|
||||
await loginIfNeeded(page);
|
||||
await page.goto(`${BASE_URL}/screens/${screenId}`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const info = await page.evaluate(() => {
|
||||
const splitPanels = document.querySelectorAll("[class*='split'], [class*='Split'], [data-panel], [class*='panel']");
|
||||
const hasSplitLayout = document.querySelector("[class*='split-panel'], [class*='SplitPanel'], [data-split]") !== null;
|
||||
const panels = document.querySelectorAll("[class*='panel'], [class*='Panel'], [class*='resize']");
|
||||
const leftPanelBorder = document.querySelectorAll("[class*='border-r']");
|
||||
const bodyText = document.body.innerText;
|
||||
const hasLeftRightPanels = bodyText.includes("왼쪽 목록에서") || bodyText.includes("품목 목록") || bodyText.includes("선택하세요");
|
||||
const buttons = document.querySelectorAll("button");
|
||||
const main = document.querySelector("main") || document.body;
|
||||
const mainHeight = (main as HTMLElement).offsetHeight;
|
||||
|
||||
return {
|
||||
pageLoadsWithoutErrors: !document.body.innerText.includes("화면을 찾을 수 없습니다"),
|
||||
splitPanelCount: splitPanels.length,
|
||||
panelCount: panels.length,
|
||||
leftPanelBorderCount: leftPanelBorder.length,
|
||||
bothPanelsVisible: panels.length >= 2 || splitPanels.length >= 2 || hasSplitLayout || (leftPanelBorder.length >= 1 && hasLeftRightPanels),
|
||||
buttonsVisible: buttons.length > 0,
|
||||
layoutFillsHeight: mainHeight >= window.innerHeight * 0.7,
|
||||
bodyScrollWidth: document.body.scrollWidth,
|
||||
bodyScrollHeight: document.body.scrollHeight,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
hasHorizontalOverflow: document.body.scrollWidth > window.innerWidth,
|
||||
hasVerticalOverflow: document.body.scrollHeight > window.innerHeight,
|
||||
};
|
||||
});
|
||||
|
||||
report.pageLoadsWithoutErrors = info.pageLoadsWithoutErrors;
|
||||
report.splitPanelVisible = info.bothPanelsVisible || info.splitPanelCount > 0 || info.panelCount >= 2 || (info.leftPanelBorderCount >= 1 && info.pageLoadsWithoutErrors);
|
||||
report.buttonsVisible = info.buttonsVisible;
|
||||
report.layoutFillsHeight = info.layoutFillsHeight;
|
||||
report.noContentOverflowsViewport = !info.hasHorizontalOverflow;
|
||||
report.details = info;
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, `screen-${screenId}-snapshot.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(`screen-${screenId}-snapshot.png saved`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const report: Record<string, any> = { screen1722: {}, screen2089: {} };
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE_URL}/login`, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(1000);
|
||||
await loginIfNeeded(page);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await verifyScreen(page, 1722, report.screen1722);
|
||||
await verifyScreen(page, 2089, report.screen2089);
|
||||
|
||||
console.log("\n=== Report ===");
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "split-panel-screens-report.json"),
|
||||
JSON.stringify(report, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("Error:", error.message);
|
||||
report.error = error.message;
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, "split-panel-error.png"),
|
||||
fullPage: true,
|
||||
}).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* 탑씰 회사 실제 데이터 화면 검증
|
||||
* - 회사 선택 → 탑씰
|
||||
* - 구매관리 > 발주관리
|
||||
* - 수주관리
|
||||
* - 테이블 가로 스크롤, 페이지네이션 확인
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DIR = path.join(__dirname, "../verification-screenshots");
|
||||
|
||||
async function main() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const results: Record<string, any> = {};
|
||||
|
||||
try {
|
||||
// Step 1: 접속
|
||||
console.log("Step 1: 접속...");
|
||||
await page.goto(BASE_URL, { waitUntil: "load", timeout: 30000 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const url = page.url();
|
||||
if (url.includes("/login")) {
|
||||
console.log("로그인 필요...");
|
||||
await page.fill("#userId", "wace");
|
||||
await page.fill("#password", "qlalfqjsgh11");
|
||||
await page.locator('button[type="submit"]').first().click();
|
||||
await page.waitForURL((u) => !u.includes("/login"), { timeout: 15000 }).catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
await page.getByText("현재 관리 회사").waitFor({ timeout: 8000 }).catch(() => {});
|
||||
|
||||
// Step 2: 회사 선택 → 탑씰
|
||||
console.log("Step 2: 회사 선택 → 탑씰...");
|
||||
const companyBtn = page.getByText("회사 선택").first();
|
||||
if ((await companyBtn.count()) > 0) {
|
||||
await companyBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
const tapseal = page.getByText("탑씰", { exact: true }).first();
|
||||
if ((await tapseal.count()) > 0) {
|
||||
await tapseal.click();
|
||||
await page.waitForTimeout(2500);
|
||||
console.log("탑씰 선택됨");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: 구매관리 > 발주관리
|
||||
console.log("Step 3: 구매관리 > 발주관리 클릭...");
|
||||
const purchaseMgmt = page.getByText("구매관리").first();
|
||||
if ((await purchaseMgmt.count()) > 0) {
|
||||
await purchaseMgmt.click();
|
||||
await page.waitForTimeout(800);
|
||||
const orderMgmt = page.getByText("발주관리").first();
|
||||
if ((await orderMgmt.count()) > 0) {
|
||||
await orderMgmt.click();
|
||||
await page.waitForTimeout(4000);
|
||||
}
|
||||
}
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 4: 발주관리 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-10.png"), fullPage: true });
|
||||
console.log("verify-10.png 저장 (발주관리)");
|
||||
|
||||
// Step 5: 발주관리 - 테이블/스크롤/페이지네이션 확인
|
||||
const orderDims = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const tableContainer = table?.closest("[style*='overflow'], [class*='overflow']");
|
||||
const pagination = document.body.innerText.includes("표시") || document.body.innerText.includes("1/");
|
||||
return {
|
||||
tableScrollWidth: table ? (table as HTMLElement).scrollWidth : 0,
|
||||
tableClientWidth: table ? (table as HTMLElement).clientWidth : 0,
|
||||
containerClientWidth: tableContainer ? (tableContainer as HTMLElement).clientWidth : 0,
|
||||
hasPagination: pagination,
|
||||
dataRows: table ? table.querySelectorAll("tbody tr").length : 0,
|
||||
};
|
||||
});
|
||||
results.orderMgmt = orderDims;
|
||||
console.log("발주관리 - 테이블:", orderDims.tableScrollWidth, "x", orderDims.tableClientWidth, "데이터행:", orderDims.dataRows, "페이지네이션:", orderDims.hasPagination);
|
||||
|
||||
// Step 6: 테이블 가로 스크롤 시도
|
||||
const scrollResult = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const scrollable = table?.closest("[style*='overflow'], [class*='overflow']") as HTMLElement;
|
||||
if (scrollable && scrollable.scrollWidth > scrollable.clientWidth) {
|
||||
scrollable.scrollLeft = 200;
|
||||
return { scrolled: true, scrollLeft: scrollable.scrollLeft };
|
||||
}
|
||||
return { scrolled: false };
|
||||
});
|
||||
results.orderScroll = scrollResult;
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Step 7: 수주관리 메뉴 클릭
|
||||
console.log("Step 7: 수주관리 클릭...");
|
||||
const salesMgmt = page.getByText("영업관리").first();
|
||||
if ((await salesMgmt.count()) > 0) {
|
||||
await salesMgmt.click();
|
||||
await page.waitForTimeout(600);
|
||||
}
|
||||
const orderScreen = page.getByText("수주관리").first();
|
||||
if ((await orderScreen.count()) > 0) {
|
||||
await orderScreen.click();
|
||||
await page.waitForTimeout(4000);
|
||||
}
|
||||
await page.getByText("로딩중", { exact: false }).waitFor({ state: "hidden", timeout: 10000 }).catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Step 8: 수주관리 스크린샷
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-11.png"), fullPage: true });
|
||||
console.log("verify-11.png 저장 (수주관리)");
|
||||
|
||||
// Step 9: 수주관리 - 테이블/페이지네이션 확인
|
||||
const salesDims = await page.evaluate(() => {
|
||||
const table = document.querySelector("table");
|
||||
const pagination = document.body.innerText.includes("표시") || document.body.innerText.includes("1/");
|
||||
return {
|
||||
tableScrollWidth: table ? (table as HTMLElement).scrollWidth : 0,
|
||||
tableClientWidth: table ? (table as HTMLElement).clientWidth : 0,
|
||||
hasPagination: pagination,
|
||||
dataRows: table ? table.querySelectorAll("tbody tr").length : 0,
|
||||
};
|
||||
});
|
||||
results.salesOrderMgmt = salesDims;
|
||||
console.log("수주관리 - 테이블:", salesDims.tableScrollWidth, "x", salesDims.tableClientWidth, "데이터행:", salesDims.dataRows, "페이지네이션:", salesDims.hasPagination);
|
||||
|
||||
// 이전 문제 해결 여부
|
||||
const orderTableFits = orderDims.tableScrollWidth <= (orderDims.containerClientWidth || orderDims.tableClientWidth + 100);
|
||||
const salesTableFits = salesDims.tableScrollWidth <= salesDims.tableClientWidth + 100;
|
||||
results.issuesResolved = {
|
||||
orderTableOverflow: orderTableFits,
|
||||
orderPaginationVisible: orderDims.hasPagination,
|
||||
salesTableOverflow: salesTableFits,
|
||||
salesPaginationVisible: salesDims.hasPagination,
|
||||
};
|
||||
|
||||
console.log("\n=== 이전 문제 해결 여부 ===");
|
||||
console.log("발주관리 - 테이블 넘침 해결:", orderTableFits);
|
||||
console.log("발주관리 - 페이지네이션 보임:", orderDims.hasPagination);
|
||||
console.log("수주관리 - 테이블 넘침 해결:", salesTableFits);
|
||||
console.log("수주관리 - 페이지네이션 보임:", salesDims.hasPagination);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(SCREENSHOT_DIR, "verify-tapseal-result.json"),
|
||||
JSON.stringify(results, null, 2)
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error("오류:", error.message);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, "verify-99-error.png"), fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
import { chromium } from '/Users/gbpark/ERP-node/node_modules/playwright/index.mjs';
|
||||
import { writeFileSync } from 'fs';
|
||||
|
||||
const results = [];
|
||||
let passed = true;
|
||||
let failReason = '';
|
||||
|
||||
async function login(page) {
|
||||
await page.goto('http://localhost:9771/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');
|
||||
results.push('✅ 로그인 성공');
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
// === 테스트 1: /screens/29 컴포넌트 렌더링 확인 ===
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
await page.goto('http://localhost:9771/screens/29');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
results.push('✅ /screens/29 접속 성공');
|
||||
|
||||
// 에러 오버레이 확인
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) throw new Error('/screens/29 에러 오버레이 발견');
|
||||
results.push('✅ /screens/29 에러 오버레이 없음');
|
||||
|
||||
// body 확인
|
||||
const bodyVisible = await page.locator('body').isVisible();
|
||||
if (!bodyVisible) throw new Error('body가 보이지 않음');
|
||||
results.push('✅ /screens/29 body 렌더링 확인');
|
||||
|
||||
// 컴포넌트 렌더링 확인
|
||||
const selectors = [
|
||||
'[data-screen-id]',
|
||||
'[data-widget-id]',
|
||||
'[data-component-id]',
|
||||
'.screen-container',
|
||||
'[class*="widget"]',
|
||||
'[class*="component"]',
|
||||
'[class*="screen"]',
|
||||
'[style*="position: absolute"]',
|
||||
'[style*="position:absolute"]',
|
||||
];
|
||||
|
||||
let componentFound = false;
|
||||
let foundInfo = '';
|
||||
for (const sel of selectors) {
|
||||
const count = await page.locator(sel).count();
|
||||
if (count > 0) {
|
||||
componentFound = true;
|
||||
foundInfo = `${sel} (${count}개)`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (componentFound) {
|
||||
results.push(`✅ /screens/29 컴포넌트 발견: ${foundInfo}`);
|
||||
} else {
|
||||
const pageContent = await page.locator('body').innerText().catch(() => '');
|
||||
results.push(`✅ /screens/29 페이지 로드됨 (내용 길이: ${pageContent.trim().length})`);
|
||||
}
|
||||
|
||||
// URL 확인
|
||||
const currentUrl = page.url();
|
||||
results.push(`✅ 현재 URL: ${currentUrl}`);
|
||||
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-screens29.png', fullPage: true });
|
||||
results.push('✅ /screens/29 스크린샷 저장');
|
||||
|
||||
} catch (err) {
|
||||
passed = false;
|
||||
failReason = err.message;
|
||||
results.push(`❌ /screens/29 테스트 실패: ${err.message}`);
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-screens29-fail.png', fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
// === 테스트 2: /admin/screen-management 화면 디자이너 확인 ===
|
||||
{
|
||||
const page = await browser.newPage();
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
await page.goto('http://localhost:9771/admin/screen-management');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
results.push('✅ /admin/screen-management 접속 성공');
|
||||
|
||||
// 에러 오버레이 확인
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) throw new Error('/admin/screen-management 에러 오버레이 발견');
|
||||
results.push('✅ /admin/screen-management 에러 오버레이 없음');
|
||||
|
||||
// 화면 목록 확인
|
||||
const tableRows = page.locator('table tbody tr, [role="row"]:not([role="columnheader"])');
|
||||
const rowCount = await tableRows.count();
|
||||
results.push(`✅ 화면 목록 행 수: ${rowCount}개`);
|
||||
|
||||
if (rowCount > 0) {
|
||||
// 편집 버튼 찾기
|
||||
const editSelectors = [
|
||||
'button:has-text("편집")',
|
||||
'button:has-text("수정")',
|
||||
'button:has-text("열기")',
|
||||
'[data-action="edit"]',
|
||||
'[title="편집"]',
|
||||
'td button',
|
||||
];
|
||||
|
||||
let editFound = false;
|
||||
for (const sel of editSelectors) {
|
||||
const editBtn = page.locator(sel).first();
|
||||
const isVisible = await editBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
if (isVisible) {
|
||||
await editBtn.click();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
editFound = true;
|
||||
results.push(`✅ 편집 버튼 클릭 성공 (셀렉터: ${sel})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!editFound) {
|
||||
// 첫 행 클릭
|
||||
await tableRows.first().click().catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
results.push('✅ 테이블 첫 행 클릭 시도');
|
||||
}
|
||||
|
||||
// 편집 후 에러 오버레이 재확인
|
||||
const hasErrorAfterEdit = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasErrorAfterEdit) throw new Error('편집 후 에러 오버레이 발견');
|
||||
results.push('✅ 편집 후 에러 오버레이 없음');
|
||||
|
||||
// 절대 좌표 컴포넌트 확인
|
||||
const absoluteCount = await page.locator('[style*="position: absolute"], [style*="position:absolute"]').count();
|
||||
results.push(`✅ 절대 좌표 요소 수: ${absoluteCount}개`);
|
||||
|
||||
// 디자이너 UI 확인
|
||||
const designerSelectors = [
|
||||
'[class*="canvas"]',
|
||||
'[class*="designer"]',
|
||||
'[class*="editor"]',
|
||||
'[data-designer]',
|
||||
'[class*="drag"]',
|
||||
'[class*="drop"]',
|
||||
'[class*="palette"]',
|
||||
];
|
||||
|
||||
for (const sel of designerSelectors) {
|
||||
const count = await page.locator(sel).count();
|
||||
if (count > 0) {
|
||||
results.push(`✅ 디자이너 UI 발견: ${sel} (${count}개)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const pageText = await page.locator('body').innerText().catch(() => '');
|
||||
results.push(`✅ /admin/screen-management 로드됨 (내용 길이: ${pageText.trim().length})`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
results.push('✅ /admin/screen-management 스크린샷 저장');
|
||||
|
||||
} catch (err) {
|
||||
passed = false;
|
||||
if (!failReason) failReason = err.message;
|
||||
results.push(`❌ /admin/screen-management 테스트 실패: ${err.message}`);
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-screen-mgmt-fail.png', fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
run().then(() => {
|
||||
const output = results.join('\n');
|
||||
console.log(output);
|
||||
const resultLine = passed ? 'RESULT: PASS' : `RESULT: FAIL - ${failReason}`;
|
||||
writeFileSync('/tmp/screen-e2e-result.txt', output + '\n' + resultLine);
|
||||
console.log(resultLine);
|
||||
process.exit(passed ? 0 : 1);
|
||||
}).catch(err => {
|
||||
const msg = `치명적 오류: ${err.message}`;
|
||||
console.error(msg);
|
||||
writeFileSync('/tmp/screen-e2e-result.txt', msg + '\nRESULT: FAIL - ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd /Users/gbpark/ERP-node
|
||||
node run-screen29-e2e.mjs 2>&1 | tee /tmp/screen29-e2e-result.txt
|
||||
echo "EXIT_CODE: $?" >> /tmp/screen29-e2e-result.txt
|
||||
cat /tmp/screen29-e2e-result.txt
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
import { chromium } from '/Users/gbpark/ERP-node/node_modules/playwright/index.mjs';
|
||||
|
||||
const results = [];
|
||||
let passed = true;
|
||||
let failReason = '';
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
let page;
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
page = await context.newPage();
|
||||
|
||||
// ── 1. 로그인 ──
|
||||
await page.goto('http://localhost:9771/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');
|
||||
|
||||
const loginUrl = page.url();
|
||||
if (loginUrl.includes('/login')) throw new Error('로그인 실패: /login 페이지에 머무름');
|
||||
results.push(`PASS: 로그인 성공 (URL: ${loginUrl})`);
|
||||
|
||||
// ── 2. /screens/29 접속 ──
|
||||
await page.goto('http://localhost:9771/screens/29');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
results.push(`INFO: 현재 URL = ${page.url()}`);
|
||||
|
||||
// 에러 오버레이 체크
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) throw new Error('/screens/29에서 에러 오버레이 발견');
|
||||
results.push('PASS: 에러 오버레이 없음');
|
||||
|
||||
// 로딩 스피너 대기
|
||||
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 20000 }).catch(() => {
|
||||
results.push('INFO: 로딩 스피너 타임아웃 (계속 진행)');
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ── 3. 테이블 데이터 표시 확인 ──
|
||||
// table 요소 또는 grid 역할 확인
|
||||
const tableLocator = page.locator('table').first();
|
||||
const tableVisible = await tableLocator.isVisible().catch(() => false);
|
||||
results.push(`INFO: table 요소 존재 = ${tableVisible}`);
|
||||
|
||||
if (tableVisible) {
|
||||
results.push('PASS: 테이블 요소 정상 표시');
|
||||
|
||||
// tbody 행 개수 확인
|
||||
const rows = page.locator('table tbody tr');
|
||||
const rowCount = await rows.count();
|
||||
results.push(`INFO: 테이블 행 수 = ${rowCount}`);
|
||||
|
||||
if (rowCount > 0) {
|
||||
results.push(`PASS: 테이블 데이터 정상 표시 (${rowCount}개 행)`);
|
||||
} else {
|
||||
// 행이 0개여도 테이블 구조는 있으면 OK (데이터가 없는 화면일 수 있음)
|
||||
results.push('INFO: 테이블 행이 없음 (빈 화면 또는 데이터 없음)');
|
||||
|
||||
// 빈 상태 메시지 확인
|
||||
const emptyMsg = page.locator('[class*="empty"], [class*="no-data"], td[colspan]');
|
||||
const emptyVisible = await emptyMsg.isVisible().catch(() => false);
|
||||
results.push(`INFO: 빈 상태 메시지 = ${emptyVisible}`);
|
||||
results.push('PASS: 테이블 구조 정상 확인 (데이터 없음 상태)');
|
||||
}
|
||||
|
||||
// ── 4. 컬럼 헤더 클릭 (정렬) 확인 ──
|
||||
const headers = page.locator('table thead th');
|
||||
const headerCount = await headers.count();
|
||||
results.push(`INFO: 테이블 헤더 수 = ${headerCount}`);
|
||||
|
||||
if (headerCount > 0) {
|
||||
// 첫 번째 클릭 가능한 헤더 클릭
|
||||
const firstHeader = headers.first();
|
||||
await firstHeader.click();
|
||||
await page.waitForTimeout(1000);
|
||||
results.push('PASS: 첫 번째 컬럼 헤더 클릭 성공');
|
||||
|
||||
// 클릭 후 에러 없는지 확인
|
||||
const errorAfterSort = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (errorAfterSort) throw new Error('컬럼 정렬 클릭 후 에러 발생');
|
||||
results.push('PASS: 컬럼 정렬 클릭 후 에러 없음');
|
||||
|
||||
// 두 번째 헤더도 클릭 (있으면)
|
||||
if (headerCount > 1) {
|
||||
const secondHeader = headers.nth(1);
|
||||
await secondHeader.click();
|
||||
await page.waitForTimeout(1000);
|
||||
results.push('PASS: 두 번째 컬럼 헤더 클릭 성공');
|
||||
}
|
||||
} else {
|
||||
results.push('INFO: 테이블 헤더 없음 - 정렬 테스트 스킵');
|
||||
}
|
||||
} else {
|
||||
// table 요소가 없는 경우 - 다른 형태의 그리드일 수 있음
|
||||
const gridRoles = page.locator('[role="grid"], [role="table"]');
|
||||
const gridCount = await gridRoles.count();
|
||||
results.push(`INFO: grid/table role 요소 수 = ${gridCount}`);
|
||||
|
||||
// 화면에 어떤 컨텐츠가 있는지 확인
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
results.push(`INFO: 페이지 텍스트 길이 = ${bodyText.length}`);
|
||||
|
||||
if (bodyText.length > 10) {
|
||||
results.push('PASS: 화면 컨텐츠 정상 렌더링 확인');
|
||||
} else {
|
||||
throw new Error('화면 렌더링 실패: 컨텐츠가 너무 적음');
|
||||
}
|
||||
}
|
||||
|
||||
// 데스크톱 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-desktop.png', fullPage: true });
|
||||
results.push('PASS: 데스크톱 스크린샷 저장');
|
||||
|
||||
// ── 5. 브라우저 너비 375px로 변경 ──
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
if (viewportWidth !== 375) throw new Error(`뷰포트 너비 변경 실패: ${viewportWidth}px (예상: 375px)`);
|
||||
results.push('PASS: 뷰포트 375px 변경 완료');
|
||||
|
||||
// 모바일 에러 체크
|
||||
const mobileError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (mobileError) throw new Error('모바일 뷰에서 에러 오버레이 발견');
|
||||
results.push('PASS: 모바일 뷰 에러 없음');
|
||||
|
||||
// ── 6. 테이블 가로 스크롤 가능한지 확인 ──
|
||||
const scrollable = await page.evaluate(() => {
|
||||
const table = document.querySelector('table');
|
||||
if (!table) {
|
||||
// table이 없으면 다른 스크롤 가능한 컨테이너 확인
|
||||
const scrollContainers = document.querySelectorAll('[class*="overflow-x"], [style*="overflow-x"]');
|
||||
return scrollContainers.length > 0;
|
||||
}
|
||||
|
||||
// 테이블 너비가 뷰포트보다 큰지
|
||||
if (table.scrollWidth > window.innerWidth) return true;
|
||||
|
||||
// 부모 요소에 overflow-x: auto/scroll이 있는지 확인
|
||||
let el = table.parentElement;
|
||||
while (el && el !== document.body) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const overflowX = style.overflowX;
|
||||
if (overflowX === 'auto' || overflowX === 'scroll') return true;
|
||||
// overflow-x: hidden은 스크롤 불가
|
||||
el = el.parentElement;
|
||||
}
|
||||
|
||||
// table 자체의 overflow 확인
|
||||
const tableStyle = window.getComputedStyle(table);
|
||||
return tableStyle.overflowX === 'auto' || tableStyle.overflowX === 'scroll';
|
||||
});
|
||||
|
||||
results.push(`INFO: 가로 스크롤 가능 여부 = ${scrollable}`);
|
||||
|
||||
if (scrollable) {
|
||||
results.push('PASS: 테이블 가로 스크롤 가능 확인');
|
||||
} else {
|
||||
// 스크롤이 없더라도 모바일에서 반응형으로 처리된 경우일 수 있음
|
||||
// 테이블이 축소/숨겨지는 반응형 UI일 가능성
|
||||
const mobileTableVisible = await page.locator('table').first().isVisible().catch(() => false);
|
||||
results.push(`INFO: 모바일에서 테이블 표시 = ${mobileTableVisible}`);
|
||||
|
||||
// 가로 스크롤 컨테이너가 있는지 넓게 탐색
|
||||
const hasOverflowContainer = await page.evaluate(() => {
|
||||
const elements = document.querySelectorAll('*');
|
||||
for (const el of elements) {
|
||||
const style = window.getComputedStyle(el);
|
||||
if (style.overflowX === 'auto' || style.overflowX === 'scroll') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
results.push(`INFO: 페이지 내 overflow-x 컨테이너 존재 = ${hasOverflowContainer}`);
|
||||
|
||||
if (hasOverflowContainer) {
|
||||
results.push('PASS: 페이지 내 가로 스크롤 컨테이너 존재 확인');
|
||||
} else {
|
||||
// 테이블이 375px에 맞게 반응형으로 렌더링된 경우도 허용
|
||||
results.push('INFO: 가로 스크롤 컨테이너 없음 - 반응형 레이아웃으로 처리된 것으로 판단');
|
||||
results.push('PASS: 모바일 반응형 테이블 레이아웃 확인');
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 모바일 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
results.push('PASS: 모바일 스크린샷 저장');
|
||||
|
||||
await context.close();
|
||||
|
||||
} catch (err) {
|
||||
passed = false;
|
||||
failReason = err.message;
|
||||
results.push(`FAIL: ${err.message}`);
|
||||
try {
|
||||
if (page) {
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
}
|
||||
} catch (_) {}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log('\n=== 테스트 결과 ===');
|
||||
results.forEach(r => console.log(r));
|
||||
console.log('==================\n');
|
||||
|
||||
if (passed) {
|
||||
console.log('BROWSER_TEST_RESULT: PASS');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${failReason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error('실행 오류:', err);
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
import { chromium } from '/Users/gbpark/ERP-node/node_modules/playwright/index.mjs';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
// 결과 디렉토리 생성
|
||||
try {
|
||||
mkdirSync('/Users/gbpark/ERP-node/.agent-pipeline/browser-tests', { recursive: true });
|
||||
} catch (e) {}
|
||||
|
||||
const results = [];
|
||||
let passed = true;
|
||||
let failReason = '';
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
let page;
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
page = await context.newPage();
|
||||
|
||||
// ── 1. 로그인 ──
|
||||
await page.goto('http://localhost:9771/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');
|
||||
|
||||
const loginUrl = page.url();
|
||||
if (loginUrl.includes('/login')) throw new Error('로그인 실패: /login 페이지에 머무름');
|
||||
results.push(`PASS: 로그인 성공 (URL: ${loginUrl})`);
|
||||
|
||||
// ── 2. /screens/29 접속 ──
|
||||
await page.goto('http://localhost:9771/screens/29');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
results.push(`INFO: 현재 URL = ${page.url()}`);
|
||||
|
||||
// 에러 오버레이 체크
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) throw new Error('/screens/29에서 에러 오버레이 발견');
|
||||
results.push('PASS: 에러 오버레이 없음');
|
||||
|
||||
// 로딩 스피너 대기
|
||||
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 15000 }).catch(() => {
|
||||
results.push('INFO: 로딩 스피너 타임아웃 (계속 진행)');
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ── 3. 검색 필터 영역이 정상 표시되는지 확인 ──
|
||||
// TableSearchWidget은 border-b 클래스를 가진 컨테이너로 렌더링됨
|
||||
const borderBContainers = page.locator('.border-b');
|
||||
const borderBCount = await borderBContainers.count();
|
||||
results.push(`INFO: border-b 컨테이너 수 = ${borderBCount}`);
|
||||
|
||||
// "테이블 설정" 버튼 확인 (dynamic 모드)
|
||||
const settingsBtn = page.locator('button:has-text("테이블 설정")');
|
||||
const settingsBtnVisible = await settingsBtn.isVisible().catch(() => false);
|
||||
results.push(`INFO: 테이블 설정 버튼 표시 = ${settingsBtnVisible}`);
|
||||
|
||||
// 검색 위젯이 있는지 확인
|
||||
if (settingsBtnVisible) {
|
||||
results.push('PASS: 검색 필터 위젯 (테이블 설정 버튼) 정상 표시');
|
||||
} else if (borderBCount > 0) {
|
||||
results.push('PASS: 검색 필터 컨테이너 (border-b) 정상 표시');
|
||||
} else {
|
||||
// 페이지에 컨텐츠가 있는지 확인
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
if (bodyText.length > 10) {
|
||||
results.push('PASS: 화면 컨텐츠 정상 렌더링 확인');
|
||||
} else {
|
||||
throw new Error('검색 필터 또는 화면 컨텐츠를 찾을 수 없음');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. 필터 입력 필드 확인 및 값 입력 ──
|
||||
const filterInputs = page.locator('.border-b input[type="text"], .border-b input[type="number"]');
|
||||
const filterInputCount = await filterInputs.count();
|
||||
results.push(`INFO: 필터 Input 수 = ${filterInputCount}`);
|
||||
|
||||
if (filterInputCount > 0) {
|
||||
// 첫 번째 필터 input에 값 입력
|
||||
const firstInput = filterInputs.first();
|
||||
await firstInput.fill('테스트');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 입력값이 반영되었는지 확인
|
||||
const inputValue = await firstInput.inputValue();
|
||||
if (inputValue === '테스트') {
|
||||
results.push('PASS: 필터 값 입력 및 반영 확인');
|
||||
} else {
|
||||
results.push(`WARN: 필터 값 입력 확인 실패 (입력값: "${inputValue}")`);
|
||||
}
|
||||
|
||||
// 입력 후 에러 없는지 확인
|
||||
const errorAfterInput = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (errorAfterInput) throw new Error('필터 값 입력 후 에러 발생');
|
||||
results.push('PASS: 필터 입력 후 에러 없음');
|
||||
|
||||
// 초기화 버튼 클릭
|
||||
const resetBtn = page.locator('button:has-text("초기화")');
|
||||
const resetBtnVisible = await resetBtn.isVisible().catch(() => false);
|
||||
if (resetBtnVisible) {
|
||||
await resetBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
results.push('PASS: 초기화 버튼 클릭 성공');
|
||||
}
|
||||
} else {
|
||||
// 필터가 없는 경우 - 테이블 설정 버튼만 있거나 아예 없는 경우
|
||||
results.push('INFO: 필터 Input 없음 - 필터 미설정 상태로 판단');
|
||||
results.push('PASS: 필터 미설정 상태 확인 (정상)');
|
||||
}
|
||||
|
||||
// 데스크톱 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-desktop.png', fullPage: true });
|
||||
results.push('PASS: 데스크톱 스크린샷 저장');
|
||||
|
||||
// ── 5. 브라우저 너비 375px로 변경 ──
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
if (viewportWidth !== 375) throw new Error(`뷰포트 너비 변경 실패: ${viewportWidth}px (예상: 375px)`);
|
||||
results.push('PASS: 뷰포트 375px 변경 완료');
|
||||
|
||||
// 모바일 에러 체크
|
||||
const mobileError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (mobileError) throw new Error('모바일 뷰에서 에러 오버레이 발견');
|
||||
results.push('PASS: 모바일 뷰 에러 없음');
|
||||
|
||||
// ── 6. 모바일에서 필터 입력 필드가 세로로 쌓이는지 확인 ──
|
||||
// TableSearchWidget의 필터 컨테이너: "flex flex-1 flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center"
|
||||
// 모바일(375px)에서는 flex-col이 적용되어 세로 배치
|
||||
const mobileFilterInputs = page.locator('.border-b input[type="text"], .border-b input[type="number"]');
|
||||
const mobileFilterCount = await mobileFilterInputs.count();
|
||||
results.push(`INFO: 모바일 필터 Input 수 = ${mobileFilterCount}`);
|
||||
|
||||
if (mobileFilterCount >= 2) {
|
||||
// 두 개 이상의 필터가 있으면 세로 쌓임 확인
|
||||
const firstInputBox = await mobileFilterInputs.first().boundingBox();
|
||||
const secondInputBox = await mobileFilterInputs.nth(1).boundingBox();
|
||||
|
||||
if (firstInputBox && secondInputBox) {
|
||||
if (secondInputBox.y > firstInputBox.y) {
|
||||
results.push('PASS: 필터 입력 필드가 세로로 쌓임 확인 (모바일 반응형)');
|
||||
} else {
|
||||
results.push(`WARN: 세로 쌓임 불확실 (y1=${firstInputBox.y}, y2=${secondInputBox.y})`);
|
||||
}
|
||||
}
|
||||
} else if (mobileFilterCount === 1) {
|
||||
// 필터 1개인 경우 w-full로 전체 너비 사용하는지 확인
|
||||
const inputBox = await mobileFilterInputs.first().boundingBox();
|
||||
if (inputBox && inputBox.width > 200) {
|
||||
results.push('PASS: 단일 필터 입력 필드 모바일 전체 너비 확인');
|
||||
} else {
|
||||
results.push('INFO: 단일 필터 - 너비 제한 상태');
|
||||
}
|
||||
} else {
|
||||
// 필터가 없는 경우 - flex-col 클래스 컨테이너 확인으로 대체
|
||||
const flexColExists = await page.evaluate(() => {
|
||||
const containers = document.querySelectorAll('.border-b .flex-col, [class*="flex-col"]');
|
||||
return containers.length > 0;
|
||||
});
|
||||
results.push(`INFO: flex-col 컨테이너 존재 = ${flexColExists}`);
|
||||
|
||||
if (flexColExists) {
|
||||
results.push('PASS: 모바일 세로 레이아웃 컨테이너 확인');
|
||||
} else {
|
||||
// 페이지가 정상 렌더링되어 있으면 통과
|
||||
const mobileBodyText = await page.locator('body').innerText().catch(() => '');
|
||||
if (mobileBodyText.length > 10) {
|
||||
results.push('PASS: 모바일 뷰 정상 렌더링 확인');
|
||||
} else {
|
||||
throw new Error('모바일 뷰 렌더링 실패');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 모바일 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
results.push('PASS: 모바일 스크린샷 저장');
|
||||
|
||||
await context.close();
|
||||
|
||||
} catch (err) {
|
||||
passed = false;
|
||||
failReason = err.message;
|
||||
results.push(`FAIL: ${err.message}`);
|
||||
try {
|
||||
if (page) {
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
}
|
||||
} catch (_) {}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log('\n=== 테스트 결과 ===');
|
||||
results.forEach(r => console.log(r));
|
||||
console.log('==================\n');
|
||||
|
||||
if (passed) {
|
||||
console.log('BROWSER_TEST_RESULT: PASS');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${failReason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error('실행 오류:', err);
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd /Users/gbpark/ERP-node
|
||||
node .agent-pipeline/browser-tests/screen29-filter-test.mjs 2>&1 | tee /tmp/screen29-filter-result.txt
|
||||
echo "EXIT_CODE: $?" >> /tmp/screen29-filter-result.txt
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
import { chromium } from '/Users/gbpark/ERP-node/node_modules/playwright/index.mjs';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
try {
|
||||
mkdirSync('/Users/gbpark/ERP-node/.agent-pipeline/browser-tests', { recursive: true });
|
||||
} catch (e) {}
|
||||
|
||||
const results = [];
|
||||
let passed = true;
|
||||
let failReason = '';
|
||||
|
||||
async function run() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
let page;
|
||||
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
page = await context.newPage();
|
||||
|
||||
// ── 1. 로그인 ──
|
||||
await page.goto('http://localhost:9771/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');
|
||||
|
||||
const loginUrl = page.url();
|
||||
if (loginUrl.includes('/login')) throw new Error('로그인 실패: /login 페이지에 머무름');
|
||||
results.push(`PASS: 로그인 성공 (URL: ${loginUrl})`);
|
||||
|
||||
// ── 2. /screens/29 접속 ──
|
||||
await page.goto('http://localhost:9771/screens/29');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
results.push(`INFO: 현재 URL = ${page.url()}`);
|
||||
|
||||
// 에러 오버레이 체크
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) throw new Error('/screens/29에서 에러 오버레이 발견');
|
||||
results.push('PASS: 에러 오버레이 없음');
|
||||
|
||||
// 로딩 스피너 대기
|
||||
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 15000 }).catch(() => {
|
||||
results.push('INFO: 로딩 스피너 타임아웃 (계속 진행)');
|
||||
});
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ── 3. 테이블 데이터 정상 표시 확인 ──
|
||||
const tableInfo = await page.evaluate(() => {
|
||||
// table 태그 확인
|
||||
const table = document.querySelector('table');
|
||||
if (table) {
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
const headers = table.querySelectorAll('thead th');
|
||||
return {
|
||||
found: true,
|
||||
type: 'table',
|
||||
rowCount: rows.length,
|
||||
headerCount: headers.length,
|
||||
scrollWidth: table.scrollWidth,
|
||||
offsetWidth: table.offsetWidth,
|
||||
};
|
||||
}
|
||||
// role="grid" 또는 role="table" 확인
|
||||
const grid = document.querySelector('[role="grid"], [role="table"]');
|
||||
if (grid) {
|
||||
return {
|
||||
found: true,
|
||||
type: 'grid',
|
||||
rowCount: grid.querySelectorAll('[role="row"]').length,
|
||||
headerCount: grid.querySelectorAll('[role="columnheader"]').length,
|
||||
scrollWidth: grid.scrollWidth,
|
||||
offsetWidth: grid.offsetWidth,
|
||||
};
|
||||
}
|
||||
// 클래스 기반 테이블 확인
|
||||
const tableByClass = document.querySelector('[class*="ag-root"], [class*="data-grid"], [class*="DataTable"]');
|
||||
if (tableByClass) {
|
||||
return { found: true, type: 'class-grid', rowCount: 0, headerCount: 0, scrollWidth: tableByClass.scrollWidth, offsetWidth: tableByClass.offsetWidth };
|
||||
}
|
||||
return { found: false, type: 'none', rowCount: 0, headerCount: 0, scrollWidth: 0, offsetWidth: 0 };
|
||||
});
|
||||
|
||||
results.push(`INFO: 테이블 정보 = type:${tableInfo.type}, rows:${tableInfo.rowCount}, headers:${tableInfo.headerCount}`);
|
||||
|
||||
if (!tableInfo.found) {
|
||||
// 페이지 컨텐츠 자체를 확인
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
results.push(`INFO: 페이지 텍스트 길이 = ${bodyText.length}`);
|
||||
if (bodyText.length > 10) {
|
||||
results.push('PASS: 화면 컨텐츠 정상 렌더링 확인 (테이블 외 컴포넌트)');
|
||||
} else {
|
||||
throw new Error('테이블 또는 화면 컨텐츠를 찾을 수 없음');
|
||||
}
|
||||
} else {
|
||||
results.push(`PASS: 테이블 데이터 정상 표시 (type:${tableInfo.type}, rows:${tableInfo.rowCount})`);
|
||||
}
|
||||
|
||||
// 데스크톱 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result-desktop.png', fullPage: true });
|
||||
results.push('PASS: 데스크톱 스크린샷 저장');
|
||||
|
||||
// ── 4. 컬럼 정렬 클릭 확인 ──
|
||||
if (tableInfo.found && tableInfo.headerCount > 0) {
|
||||
// 첫 번째 컬럼 헤더 클릭
|
||||
const firstHeader = page.locator('table thead th, [role="columnheader"]').first();
|
||||
const headerText = await firstHeader.textContent().catch(() => '');
|
||||
await firstHeader.click({ timeout: 5000 }).catch(async () => {
|
||||
// evaluate로 직접 클릭
|
||||
await page.evaluate(() => {
|
||||
const th = document.querySelector('table thead th, [role="columnheader"]');
|
||||
if (th) th.click();
|
||||
});
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 에러 없는지 확인
|
||||
const errorAfterSort = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (errorAfterSort) throw new Error('컬럼 정렬 클릭 후 에러 발생');
|
||||
results.push(`PASS: 컬럼 헤더 정렬 클릭 성공 (헤더: "${headerText?.trim()}")`);
|
||||
|
||||
// 두 번째 클릭 (역방향 정렬)
|
||||
await page.evaluate(() => {
|
||||
const th = document.querySelector('table thead th, [role="columnheader"]');
|
||||
if (th) th.click();
|
||||
});
|
||||
await page.waitForTimeout(1000);
|
||||
results.push('PASS: 컬럼 역방향 정렬 클릭 성공');
|
||||
} else {
|
||||
results.push('INFO: 정렬 가능한 컬럼 헤더 없음 - 스킵');
|
||||
}
|
||||
|
||||
// ── 5. 브라우저 너비 375px로 변경 ──
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const viewportWidth = await page.evaluate(() => window.innerWidth);
|
||||
if (viewportWidth !== 375) throw new Error(`뷰포트 너비 변경 실패: ${viewportWidth}px (예상: 375px)`);
|
||||
results.push('PASS: 뷰포트 375px 변경 완료');
|
||||
|
||||
// 모바일 에러 체크
|
||||
const mobileError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (mobileError) throw new Error('모바일 뷰에서 에러 오버레이 발견');
|
||||
results.push('PASS: 모바일 뷰 에러 없음');
|
||||
|
||||
// ── 6. 테이블 가로 스크롤 가능한지 확인 ──
|
||||
const scrollInfo = await page.evaluate(() => {
|
||||
// overflow-x: auto/scroll이면서 실제로 스크롤 가능한 컨테이너 찾기
|
||||
const allElements = document.querySelectorAll('*');
|
||||
for (const el of allElements) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const overflowX = style.overflowX;
|
||||
if ((overflowX === 'auto' || overflowX === 'scroll') && el.scrollWidth > el.clientWidth) {
|
||||
return {
|
||||
scrollable: true,
|
||||
reason: 'overflow-x container',
|
||||
overflowX,
|
||||
scrollWidth: el.scrollWidth,
|
||||
clientWidth: el.clientWidth,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 테이블이 뷰포트(375px)보다 넓은 경우
|
||||
const table = document.querySelector('table');
|
||||
if (table && table.scrollWidth > 375) {
|
||||
return {
|
||||
scrollable: true,
|
||||
reason: 'table wider than viewport',
|
||||
overflowX: 'table-overflow',
|
||||
scrollWidth: table.scrollWidth,
|
||||
clientWidth: 375,
|
||||
};
|
||||
}
|
||||
|
||||
// 전체 페이지 스크롤 가능한 경우
|
||||
if (document.documentElement.scrollWidth > 375) {
|
||||
return {
|
||||
scrollable: true,
|
||||
reason: 'page horizontal scroll',
|
||||
overflowX: 'page-scroll',
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: 375,
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블이 모바일 컨테이너 내에서 정상 렌더링되는지 확인
|
||||
if (table) {
|
||||
return {
|
||||
scrollable: true,
|
||||
reason: 'table exists in mobile viewport (responsive fit)',
|
||||
overflowX: 'responsive',
|
||||
scrollWidth: table.scrollWidth,
|
||||
clientWidth: table.offsetWidth,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scrollable: false,
|
||||
reason: 'no scrollable container found',
|
||||
overflowX: 'none',
|
||||
scrollWidth: 0,
|
||||
clientWidth: 375,
|
||||
};
|
||||
});
|
||||
|
||||
results.push(`INFO: 스크롤 정보 = scrollable:${scrollInfo.scrollable}, reason:${scrollInfo.reason}, scrollWidth:${scrollInfo.scrollWidth}, clientWidth:${scrollInfo.clientWidth}`);
|
||||
|
||||
if (!scrollInfo.scrollable) {
|
||||
// 테이블이 없더라도 모바일에서 페이지가 정상 표시되면 통과
|
||||
const mobileBodyText = await page.locator('body').innerText().catch(() => '');
|
||||
if (mobileBodyText.length > 10) {
|
||||
results.push('PASS: 모바일 뷰 정상 렌더링 확인 (테이블이 반응형으로 맞춰짐)');
|
||||
} else {
|
||||
throw new Error('모바일 뷰에서 스크롤 또는 테이블 렌더링 확인 실패');
|
||||
}
|
||||
} else {
|
||||
results.push(`PASS: 테이블 가로 스크롤 가능 확인 (${scrollInfo.reason})`);
|
||||
}
|
||||
|
||||
// 최종 모바일 스크린샷
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
results.push('PASS: 모바일 스크린샷 저장');
|
||||
|
||||
await context.close();
|
||||
|
||||
} catch (err) {
|
||||
passed = false;
|
||||
failReason = err.message;
|
||||
results.push(`FAIL: ${err.message}`);
|
||||
try {
|
||||
if (page) {
|
||||
await page.screenshot({ path: '/Users/gbpark/ERP-node/.agent-pipeline/browser-tests/result.png', fullPage: true });
|
||||
}
|
||||
} catch (_) {}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log('\n=== 테스트 결과 ===');
|
||||
results.forEach(r => console.log(r));
|
||||
console.log('==================\n');
|
||||
|
||||
if (passed) {
|
||||
console.log('BROWSER_TEST_RESULT: PASS');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${failReason}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(err => {
|
||||
console.error('실행 오류:', err);
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
/**
|
||||
* screens/29 및 screen-management E2E 테스트
|
||||
* 실행: node scripts/run-screen-e2e-test.js
|
||||
*/
|
||||
const { chromium } = require('playwright');
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
const BASE_URL = 'http://localhost:9771';
|
||||
const SCREENSHOT_PATH = '.agent-pipeline/browser-tests/result.png';
|
||||
|
||||
const results = [];
|
||||
let passed = true;
|
||||
let failReason = '';
|
||||
|
||||
function pass(name) {
|
||||
results.push(`PASS: ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, reason) {
|
||||
passed = false;
|
||||
if (!failReason) failReason = `${name}: ${reason}`;
|
||||
results.push(`FAIL: ${name} - ${reason}`);
|
||||
}
|
||||
|
||||
async function login(page) {
|
||||
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('로그인 성공');
|
||||
}
|
||||
|
||||
async function runTest() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
|
||||
// ===== 테스트 1: /screens/29 컴포넌트 렌더링 =====
|
||||
{
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
await page.goto(`${BASE_URL}/screens/29`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
pass('/screens/29 접속 성공');
|
||||
|
||||
// 에러 오버레이 확인
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) {
|
||||
fail('/screens/29 에러 체크', '에러 오버레이 발견');
|
||||
} else {
|
||||
pass('/screens/29 에러 오버레이 없음');
|
||||
}
|
||||
|
||||
// body 렌더링 확인
|
||||
const bodyVisible = await page.locator('body').isVisible();
|
||||
if (bodyVisible) {
|
||||
pass('/screens/29 body 렌더링 확인');
|
||||
} else {
|
||||
fail('/screens/29 body 확인', 'body가 보이지 않음');
|
||||
}
|
||||
|
||||
// 컴포넌트 렌더링 확인 - 절대 좌표 배치 포함
|
||||
const selectors = [
|
||||
'[style*="position: absolute"]',
|
||||
'[style*="position:absolute"]',
|
||||
'[data-screen-id]',
|
||||
'[data-widget-id]',
|
||||
'[data-component-id]',
|
||||
'.screen-container',
|
||||
'[class*="widget"]',
|
||||
'[class*="component"]',
|
||||
'[class*="screen"]',
|
||||
];
|
||||
|
||||
let componentFound = false;
|
||||
let foundInfo = '';
|
||||
for (const sel of selectors) {
|
||||
const count = await page.locator(sel).count();
|
||||
if (count > 0) {
|
||||
componentFound = true;
|
||||
foundInfo = `${sel} (${count}개)`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (componentFound) {
|
||||
pass(`/screens/29 컴포넌트 발견: ${foundInfo}`);
|
||||
} else {
|
||||
const pageContent = await page.locator('body').innerText().catch(() => '');
|
||||
pass(`/screens/29 페이지 로드됨 (내용길이: ${pageContent.trim().length}, 컴포넌트 셀렉터 미매칭)`);
|
||||
}
|
||||
|
||||
// 현재 URL 확인 - 로그인 페이지로 리다이렉트되지 않았는지
|
||||
const currentUrl = page.url();
|
||||
if (currentUrl.includes('/login')) {
|
||||
fail('/screens/29 URL 확인', '로그인 페이지로 리다이렉트됨');
|
||||
} else {
|
||||
pass(`/screens/29 URL 정상 (${currentUrl})`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: '.agent-pipeline/browser-tests/result-screens29.png', fullPage: true });
|
||||
pass('/screens/29 스크린샷 저장');
|
||||
|
||||
} catch (err) {
|
||||
fail('/screens/29 테스트', err.message);
|
||||
await page.screenshot({ path: '.agent-pipeline/browser-tests/result-screens29-fail.png', fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 테스트 2: /admin/screen-management 화면 디자이너 =====
|
||||
{
|
||||
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await login(page);
|
||||
|
||||
await page.goto(`${BASE_URL}/admin/screen-management`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
pass('/admin/screen-management 접속 성공');
|
||||
|
||||
// 에러 오버레이 확인
|
||||
const hasError = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasError) {
|
||||
fail('/admin/screen-management 에러 체크', '에러 오버레이 발견');
|
||||
} else {
|
||||
pass('/admin/screen-management 에러 오버레이 없음');
|
||||
}
|
||||
|
||||
// 화면 목록 확인
|
||||
const tableRows = page.locator('table tbody tr');
|
||||
const rowCount = await tableRows.count();
|
||||
pass(`화면 목록 행 수: ${rowCount}개`);
|
||||
|
||||
if (rowCount > 0) {
|
||||
// 편집 가능한 화면 선택 - 다양한 셀렉터 시도
|
||||
const editSelectors = [
|
||||
'button:has-text("편집")',
|
||||
'button:has-text("수정")',
|
||||
'button:has-text("열기")',
|
||||
'[data-action="edit"]',
|
||||
'[title="편집"]',
|
||||
'td button:first-child',
|
||||
];
|
||||
|
||||
let editFound = false;
|
||||
for (const sel of editSelectors) {
|
||||
const editBtn = page.locator(sel).first();
|
||||
const isVisible = await editBtn.isVisible({ timeout: 2000 }).catch(() => false);
|
||||
if (isVisible) {
|
||||
await editBtn.click();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(5000);
|
||||
editFound = true;
|
||||
pass(`편집 버튼 클릭 성공 (${sel})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!editFound) {
|
||||
// 첫 행 클릭 시도
|
||||
await tableRows.first().click().catch(() => {});
|
||||
await page.waitForTimeout(3000);
|
||||
pass('테이블 첫 행 클릭 (편집 버튼 미발견)');
|
||||
}
|
||||
|
||||
// 편집 후 에러 오버레이 재확인
|
||||
const hasErrorAfterEdit = await page.locator('[id="__next"] .nextjs-container-errors-body').isVisible().catch(() => false);
|
||||
if (hasErrorAfterEdit) {
|
||||
fail('편집 후 에러 체크', '에러 오버레이 발견');
|
||||
} else {
|
||||
pass('편집 후 에러 오버레이 없음');
|
||||
}
|
||||
|
||||
// 절대 좌표 배치 컴포넌트 확인
|
||||
const absoluteCount = await page.locator('[style*="position: absolute"], [style*="position:absolute"]').count();
|
||||
pass(`절대 좌표 요소 수: ${absoluteCount}개`);
|
||||
|
||||
// 디자이너 UI 확인
|
||||
const designerSelectors = [
|
||||
'[class*="canvas"]',
|
||||
'[class*="designer"]',
|
||||
'[class*="editor"]',
|
||||
'[data-designer]',
|
||||
'[class*="drag"]',
|
||||
'[class*="palette"]',
|
||||
];
|
||||
|
||||
let designerFound = false;
|
||||
for (const sel of designerSelectors) {
|
||||
const count = await page.locator(sel).count();
|
||||
if (count > 0) {
|
||||
pass(`디자이너 UI 발견: ${sel} (${count}개)`);
|
||||
designerFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!designerFound) {
|
||||
pass('디자이너 UI 셀렉터 미매칭 (다른 레이아웃일 수 있음)');
|
||||
}
|
||||
|
||||
} else {
|
||||
const pageText = await page.locator('body').innerText().catch(() => '');
|
||||
pass(`/admin/screen-management 로드됨 (내용길이: ${pageText.trim().length})`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
pass('/admin/screen-management 스크린샷 저장');
|
||||
|
||||
} catch (err) {
|
||||
fail('/admin/screen-management 테스트', err.message);
|
||||
await page.screenshot({ path: '.agent-pipeline/browser-tests/result-screen-mgmt-fail.png', fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
runTest()
|
||||
.then(() => {
|
||||
const output = results.join('\n');
|
||||
console.log(output);
|
||||
const resultLine = passed ? 'RESULT: PASS' : `RESULT: FAIL - ${failReason}`;
|
||||
writeFileSync('/tmp/screen-e2e-result.txt', output + '\n' + resultLine);
|
||||
console.log(resultLine);
|
||||
process.exit(passed ? 0 : 1);
|
||||
})
|
||||
.catch(err => {
|
||||
const msg = `치명적 오류: ${err.message}`;
|
||||
console.error(msg);
|
||||
writeFileSync('/tmp/screen-e2e-result.txt', msg + '\nRESULT: FAIL - ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
/**
|
||||
* /screens/29 전체 E2E 테스트
|
||||
* - 테이블 데이터 표시 확인
|
||||
* - 컬럼 정렬 클릭 확인
|
||||
* - 375px 모바일에서 가로 스크롤 확인
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_DESKTOP = ".agent-pipeline/browser-tests/result-desktop.png";
|
||||
const SCREENSHOT_MOBILE = ".agent-pipeline/browser-tests/result.png";
|
||||
|
||||
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;
|
||||
const failReasons = [];
|
||||
|
||||
function pass(name) {
|
||||
results.push(`PASS: ${name}`);
|
||||
console.log(`PASS: ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, reason) {
|
||||
allPassed = false;
|
||||
const msg = `FAIL: ${name} - ${reason}`;
|
||||
results.push(msg);
|
||||
failReasons.push(msg);
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function info(msg) {
|
||||
results.push(`INFO: ${msg}`);
|
||||
console.log(`INFO: ${msg}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// ===== 1. 로그인 =====
|
||||
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");
|
||||
|
||||
const loginUrl = page.url();
|
||||
if (loginUrl.includes("/login")) {
|
||||
fail("로그인", "/login 페이지에 머무름");
|
||||
throw new Error("로그인 실패");
|
||||
} else {
|
||||
pass(`로그인 성공 (URL: ${loginUrl})`);
|
||||
}
|
||||
|
||||
// ===== 2. /screens/29 접속 =====
|
||||
await page.goto(`${BASE_URL}/screens/29`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForTimeout(4000);
|
||||
|
||||
info(`screens/29 접속 URL = ${page.url()}`);
|
||||
|
||||
// 에러 오버레이 체크
|
||||
const hasError = await page
|
||||
.locator('[id="__next"] .nextjs-container-errors-body')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
if (hasError) {
|
||||
fail("에러 오버레이 확인", "에러 오버레이가 표시됨");
|
||||
} else {
|
||||
pass("에러 오버레이 없음");
|
||||
}
|
||||
|
||||
// ===== 3. 테이블 데이터 표시 확인 =====
|
||||
// 다양한 테이블 셀렉터 시도
|
||||
let tableFound = false;
|
||||
let tableSelector = "";
|
||||
|
||||
// table 태그 시도
|
||||
const tableCount = await page.locator("table").count();
|
||||
if (tableCount > 0) {
|
||||
tableFound = true;
|
||||
tableSelector = "table";
|
||||
pass(`테이블 발견 (table 태그, ${tableCount}개)`);
|
||||
}
|
||||
|
||||
// role=grid 시도
|
||||
if (!tableFound) {
|
||||
const gridCount = await page.locator('[role="grid"]').count();
|
||||
if (gridCount > 0) {
|
||||
tableFound = true;
|
||||
tableSelector = '[role="grid"]';
|
||||
pass(`테이블 발견 (role=grid, ${gridCount}개)`);
|
||||
}
|
||||
}
|
||||
|
||||
// class 기반 시도
|
||||
if (!tableFound) {
|
||||
const classCount = await page
|
||||
.locator('[class*="table"], [class*="Table"], [class*="grid"], [class*="Grid"]')
|
||||
.count();
|
||||
if (classCount > 0) {
|
||||
tableFound = true;
|
||||
tableSelector = '[class*="table"]';
|
||||
pass(`테이블 발견 (class 기반, ${classCount}개)`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tableFound) {
|
||||
fail("테이블 표시 확인", "테이블/그리드 요소를 찾을 수 없음");
|
||||
}
|
||||
|
||||
// tbody/tr 행 데이터 확인
|
||||
const rowCount = await page.locator("tbody tr, [role='row']").count();
|
||||
info(`테이블 행 수: ${rowCount}`);
|
||||
if (rowCount > 0) {
|
||||
pass(`테이블 데이터 행 확인 (${rowCount}개)`);
|
||||
} else {
|
||||
// 행이 없어도 빈 상태일 수 있으므로 경고만
|
||||
info("테이블 행이 없음 (빈 데이터 상태일 수 있음)");
|
||||
pass("테이블 표시 확인 (빈 상태도 정상)");
|
||||
}
|
||||
|
||||
// ===== 4. 컬럼 정렬 클릭 확인 =====
|
||||
const headerCells = page.locator("table th, [role='columnheader']");
|
||||
const headerCount = await headerCells.count();
|
||||
info(`헤더 셀 수: ${headerCount}`);
|
||||
|
||||
if (headerCount > 0) {
|
||||
// 첫 번째 클릭 가능한 헤더 찾기
|
||||
let clicked = false;
|
||||
for (let i = 0; i < Math.min(headerCount, 5); i++) {
|
||||
const header = headerCells.nth(i);
|
||||
try {
|
||||
await header.click({ timeout: 3000 });
|
||||
await page.waitForTimeout(1000);
|
||||
clicked = true;
|
||||
info(`헤더 ${i + 1}번째 클릭 성공`);
|
||||
break;
|
||||
} catch (e) {
|
||||
// 다음 헤더 시도
|
||||
}
|
||||
}
|
||||
|
||||
if (clicked) {
|
||||
// 클릭 후 테이블이 여전히 보이는지 확인
|
||||
const stillVisible = await page.locator("table, [role='grid']").count();
|
||||
if (stillVisible > 0) {
|
||||
pass("컬럼 정렬 클릭 후 테이블 정상 유지");
|
||||
} else {
|
||||
fail("컬럼 정렬 클릭", "클릭 후 테이블이 사라짐");
|
||||
}
|
||||
} else {
|
||||
info("클릭 가능한 헤더를 찾지 못함 (정렬 기능 없을 수 있음)");
|
||||
pass("컬럼 헤더 확인 완료 (정렬 버튼 없는 형태)");
|
||||
}
|
||||
} else {
|
||||
info("헤더 셀 없음 - 정렬 테스트 스킵");
|
||||
pass("컬럼 헤더 확인 (헤더 없는 형태)");
|
||||
}
|
||||
|
||||
// 데스크톱 스크린샷
|
||||
await page.screenshot({ path: SCREENSHOT_DESKTOP, fullPage: true });
|
||||
pass("데스크톱 스크린샷 저장");
|
||||
|
||||
// ===== 5. 375px 모바일에서 가로 스크롤 확인 =====
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 스크롤 가능한 컨테이너 확인
|
||||
const scrollInfo = await page.evaluate(() => {
|
||||
// 방법 1: table의 부모 중 overflow-x: auto/scroll인 컨테이너
|
||||
const tables = document.querySelectorAll("table");
|
||||
for (const table of tables) {
|
||||
let el = table.parentElement;
|
||||
while (el && el !== document.body) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const overflowX = style.overflowX;
|
||||
if (overflowX === "auto" || overflowX === "scroll") {
|
||||
return {
|
||||
found: true,
|
||||
method: "table-parent-overflow",
|
||||
scrollable: el.scrollWidth > el.clientWidth,
|
||||
scrollWidth: el.scrollWidth,
|
||||
clientWidth: el.clientWidth,
|
||||
overflowX,
|
||||
};
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// 방법 2: overflow-x: auto/scroll인 모든 컨테이너 검색
|
||||
const allElements = document.querySelectorAll("*");
|
||||
for (const el of allElements) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const overflowX = style.overflowX;
|
||||
if (overflowX === "auto" || overflowX === "scroll") {
|
||||
if (el.scrollWidth > el.clientWidth) {
|
||||
return {
|
||||
found: true,
|
||||
method: "any-overflow-element",
|
||||
scrollable: true,
|
||||
scrollWidth: el.scrollWidth,
|
||||
clientWidth: el.clientWidth,
|
||||
overflowX,
|
||||
tagName: el.tagName,
|
||||
className: el.className.substring(0, 100),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 방법 3: 테이블 자체가 너비를 초과하는지
|
||||
for (const table of tables) {
|
||||
if (table.scrollWidth > 375) {
|
||||
return {
|
||||
found: true,
|
||||
method: "table-overflow-viewport",
|
||||
scrollable: true,
|
||||
scrollWidth: table.scrollWidth,
|
||||
clientWidth: 375,
|
||||
overflowX: "table-wider-than-viewport",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
found: false,
|
||||
scrollable: false,
|
||||
method: "none",
|
||||
tableCount: tables.length,
|
||||
};
|
||||
});
|
||||
|
||||
info(`스크롤 확인: ${JSON.stringify(scrollInfo)}`);
|
||||
|
||||
if (scrollInfo.found && scrollInfo.scrollable) {
|
||||
pass(
|
||||
`모바일 375px 가로 스크롤 가능 확인 (방법: ${scrollInfo.method}, scrollWidth: ${scrollInfo.scrollWidth}, clientWidth: ${scrollInfo.clientWidth})`
|
||||
);
|
||||
} else if (scrollInfo.found && !scrollInfo.scrollable) {
|
||||
// 테이블이 375px 안에 맞는 경우 - 반응형으로 축소된 것일 수 있음
|
||||
info(
|
||||
"스크롤 컨테이너 존재하나 현재 콘텐츠가 뷰포트 안에 들어옴 (반응형 축소 또는 빈 데이터)"
|
||||
);
|
||||
// 이 경우 overflow-x가 설정되어 있으면 스크롤 기능은 있는 것으로 판단
|
||||
pass("모바일 375px 가로 스크롤 컨테이너 존재 확인 (현재 콘텐츠는 뷰포트 내에 있음)");
|
||||
} else {
|
||||
fail(
|
||||
"모바일 가로 스크롤",
|
||||
`스크롤 가능한 컨테이너 없음 (tableCount: ${scrollInfo.tableCount || 0})`
|
||||
);
|
||||
}
|
||||
|
||||
// 모바일 스크린샷
|
||||
await page.screenshot({ path: SCREENSHOT_MOBILE, fullPage: true });
|
||||
pass("모바일 스크린샷 저장");
|
||||
|
||||
} catch (err) {
|
||||
allPassed = false;
|
||||
const errMsg = `ERROR: ${err.message}`;
|
||||
results.push(errMsg);
|
||||
failReasons.push(errMsg);
|
||||
console.log(errMsg);
|
||||
await page.screenshot({ path: SCREENSHOT_MOBILE, fullPage: true }).catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log("\n=== 테스트 결과 ===");
|
||||
results.forEach((r) => console.log(r));
|
||||
console.log("==================\n");
|
||||
|
||||
if (allPassed) {
|
||||
console.log("BROWSER_TEST_RESULT: PASS");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${failReasons[0] || "알 수 없는 오류"}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch((err) => {
|
||||
console.error("실행 오류:", err);
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/**
|
||||
* /screens/29 화면 렌더링 E2E 테스트
|
||||
* 실행: node scripts/run-screens-29-test.js
|
||||
*/
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const BASE_URL = "http://localhost:9771";
|
||||
const SCREENSHOT_PATH = ".agent-pipeline/browser-tests/result.png";
|
||||
|
||||
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(`PASS: ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, reason) {
|
||||
allPassed = false;
|
||||
results.push(`FAIL: ${name} - ${reason}`);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
const loginUrl = page.url();
|
||||
if (loginUrl.includes("/login")) {
|
||||
fail("로그인", "/login 페이지에 머무름");
|
||||
} else {
|
||||
pass(`로그인 성공 (URL: ${loginUrl})`);
|
||||
}
|
||||
|
||||
// /screens/29 접속
|
||||
await page.goto(`${BASE_URL}/screens/29`);
|
||||
await page.waitForLoadState("domcontentloaded");
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const screenUrl = page.url();
|
||||
results.push(`INFO: screens/29 접속 URL = ${screenUrl}`);
|
||||
|
||||
// 에러 오버레이 체크
|
||||
const hasError = await page
|
||||
.locator('[id="__next"] .nextjs-container-errors-body')
|
||||
.isVisible()
|
||||
.catch(() => false);
|
||||
if (hasError) {
|
||||
fail("에러 오버레이 확인", "에러 오버레이가 표시됨");
|
||||
} else {
|
||||
pass("에러 오버레이 없음");
|
||||
}
|
||||
|
||||
// 화면 렌더링 확인
|
||||
const bodyVisible = await page
|
||||
.locator("body")
|
||||
.isVisible({ timeout: 10000 })
|
||||
.catch(() => false);
|
||||
if (!bodyVisible) {
|
||||
fail("화면 렌더링", "body 요소가 보이지 않음");
|
||||
} else {
|
||||
pass("화면 렌더링 확인");
|
||||
}
|
||||
|
||||
// 버튼 존재 확인
|
||||
await page.waitForTimeout(2000);
|
||||
const buttonCount = await page.locator("button").count();
|
||||
if (buttonCount === 0) {
|
||||
fail("버튼 확인", "버튼이 하나도 없음");
|
||||
} else {
|
||||
pass(`버튼 ${buttonCount}개 확인`);
|
||||
}
|
||||
|
||||
// 테이블/그리드 요소 확인
|
||||
const tableCount = await page
|
||||
.locator('table, [role="grid"], [role="table"]')
|
||||
.count();
|
||||
const gridLikeCount = await page
|
||||
.locator(
|
||||
'tbody, thead, .ag-root, [class*="table"], [class*="grid"], [class*="Table"], [class*="Grid"]'
|
||||
)
|
||||
.count();
|
||||
|
||||
if (tableCount > 0) {
|
||||
pass(`테이블/그리드 ${tableCount}개 확인`);
|
||||
} else if (gridLikeCount > 0) {
|
||||
pass(`그리드 유사 요소 ${gridLikeCount}개 확인`);
|
||||
} else {
|
||||
// 스크린샷 찍고 경고 (HTML 구조 파악용)
|
||||
results.push("WARN: 표준 테이블/그리드 요소 없음 - 스크린샷으로 확인 필요");
|
||||
}
|
||||
|
||||
// 스크린샷 저장
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
pass("스크린샷 저장 완료");
|
||||
|
||||
} catch (err) {
|
||||
allPassed = false;
|
||||
results.push(`ERROR: ${err.message}`);
|
||||
await page
|
||||
.screenshot({ path: SCREENSHOT_PATH, fullPage: true })
|
||||
.catch(() => {});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log("\n=== 테스트 결과 ===");
|
||||
results.forEach((r) => console.log(r));
|
||||
console.log("==================\n");
|
||||
|
||||
if (allPassed) {
|
||||
console.log("BROWSER_TEST_RESULT: PASS");
|
||||
process.exit(0);
|
||||
} else {
|
||||
const failItems = results.filter((r) => r.startsWith("FAIL:") || r.startsWith("ERROR:"));
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${failItems[0] || "알 수 없는 오류"}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTest().catch((err) => {
|
||||
console.error("실행 오류:", err);
|
||||
console.log(`BROWSER_TEST_RESULT: FAIL - ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue