Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into commonCodeMng

This commit is contained in:
hyeonsu 2025-09-02 13:09:07 +09:00
commit 523ebd020a
13 changed files with 1090 additions and 559 deletions

View File

@ -108,7 +108,7 @@ export const deleteScreen = async (
}
};
// 테이블 목록 조회
// 테이블 목록 조회 (모든 테이블)
export const getTables = async (req: AuthenticatedRequest, res: Response) => {
try {
const { companyCode } = req.user as any;
@ -122,6 +122,46 @@ export const getTables = async (req: AuthenticatedRequest, res: Response) => {
}
};
// 특정 테이블 정보 조회 (최적화된 단일 테이블 조회)
export const getTableInfo = async (
req: AuthenticatedRequest,
res: Response
): Promise<void> => {
try {
const { tableName } = req.params;
const { companyCode } = req.user as any;
if (!tableName) {
res.status(400).json({
success: false,
message: "테이블명이 필요합니다.",
});
return;
}
console.log(`=== 테이블 정보 조회 API 호출: ${tableName} ===`);
const tableInfo = await screenManagementService.getTableInfo(
tableName,
companyCode
);
if (!tableInfo) {
res.status(404).json({
success: false,
message: `테이블 '${tableName}'을 찾을 수 없습니다.`,
});
return;
}
res.json({ success: true, data: tableInfo });
} catch (error) {
console.error("테이블 정보 조회 실패:", error);
res
.status(500)
.json({ success: false, message: "테이블 정보 조회에 실패했습니다." });
}
};
// 테이블 컬럼 정보 조회
export const getTableColumns = async (
req: AuthenticatedRequest,

View File

@ -7,6 +7,7 @@ import {
updateScreen,
deleteScreen,
getTables,
getTableInfo,
getTableColumns,
saveLayout,
getLayout,
@ -33,6 +34,7 @@ router.get("/generate-screen-code/:companyCode", generateScreenCode);
// 테이블 관리
router.get("/tables", getTables);
router.get("/tables/:tableName", getTableInfo); // 특정 테이블 정보 조회 (최적화)
router.get("/tables/:tableName/columns", getTableColumns);
// 레이아웃 관리

View File

@ -205,7 +205,7 @@ export class ScreenManagementService {
// ========================================
/**
*
* ( )
*/
async getTables(companyCode: string): Promise<TableInfo[]> {
try {
@ -242,6 +242,54 @@ export class ScreenManagementService {
}
}
/**
* ( )
*/
async getTableInfo(
tableName: string,
companyCode: string
): Promise<TableInfo | null> {
try {
console.log(`=== 단일 테이블 조회 시작: ${tableName} ===`);
// 테이블 존재 여부 확인
const tableExists = await prisma.$queryRaw<Array<{ table_name: string }>>`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
AND table_name = ${tableName}
`;
if (tableExists.length === 0) {
console.log(`테이블 ${tableName}이 존재하지 않습니다.`);
return null;
}
// 해당 테이블의 컬럼 정보 조회
const columns = await this.getTableColumns(tableName, companyCode);
if (columns.length === 0) {
console.log(`테이블 ${tableName}의 컬럼 정보가 없습니다.`);
return null;
}
const tableInfo: TableInfo = {
tableName: tableName,
tableLabel: this.getTableLabel(tableName),
columns: columns,
};
console.log(
`단일 테이블 조회 완료: ${tableName}, 컬럼 ${columns.length}`
);
return tableInfo;
} catch (error) {
console.error(`테이블 ${tableName} 조회 실패:`, error);
throw new Error(`테이블 ${tableName} 정보를 조회할 수 없습니다.`);
}
}
/**
*
*/

View File

@ -1,58 +0,0 @@
"use client";
import { ArrowLeft, HelpCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface PageHeaderProps {
title: string;
subtitle?: string;
showBackButton?: boolean;
onBack?: () => void;
showHelpButton?: boolean;
onHelp?: () => void;
children?: React.ReactNode;
className?: string;
}
export function PageHeader({
title,
subtitle,
showBackButton = false,
onBack,
showHelpButton = false,
onHelp,
children,
className,
}: PageHeaderProps) {
return (
<div className={cn("flex items-center justify-between border-b border-slate-200 pb-6", className)}>
<div className="flex items-center space-x-4">
{showBackButton && (
<Button variant="ghost" size="sm" onClick={onBack} className="h-8 w-8 p-0">
<ArrowLeft className="h-4 w-4" />
</Button>
)}
<div>
<h1 className="text-2xl font-bold text-slate-900">{title}</h1>
{subtitle && <p className="mt-1 text-sm text-slate-600">{subtitle}</p>}
</div>
</div>
<div className="flex items-center space-x-2">
{children}
{showHelpButton && (
<Button variant="outline" size="sm" onClick={onHelp} className="h-8 w-8 p-0">
<HelpCircle className="h-4 w-4" />
</Button>
)}
</div>
</div>
);
}
// 페이지 헤더 액션 버튼 컴포넌트
export function PageHeaderActions({ children, className }: { children: React.ReactNode; className?: string }) {
return <div className={cn("flex items-center space-x-2", className)}>{children}</div>;
}

View File

@ -24,8 +24,6 @@ import { menuScreenApi } from "@/lib/api/screen";
import { toast } from "sonner";
import { MainHeader } from "./MainHeader";
import { ProfileModal } from "./ProfileModal";
import { PageHeader } from "./PageHeader";
import { getPageInfo } from "@/constants/pageInfo";
// useAuth의 UserInfo 타입을 확장
interface ExtendedUserInfo {
@ -408,10 +406,7 @@ export function AppLayout({ children }: AppLayoutProps) {
</aside>
{/* 가운데 컨텐츠 영역 */}
<main className="bg-background flex-1 p-6">
<PageHeader title={getPageInfo(pathname).title} description={getPageInfo(pathname).description} />
{children}
</main>
<main className="bg-background flex-1 p-6">{children}</main>
</div>
{/* 프로필 수정 모달 */}

View File

@ -1,21 +0,0 @@
interface PageHeaderProps {
title: string;
description?: string;
}
/**
*
* , ,
*/
export function PageHeader({ title, description }: PageHeaderProps) {
return (
<div className="mb-6">
<div className="flex items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">{title}</h1>
{description && <p className="text-muted-foreground">{description}</p>}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,181 @@
"use client";
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Grid, Settings, RotateCcw } from "lucide-react";
interface GridSettings {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
}
interface GridControlsProps {
gridSettings: GridSettings;
onGridSettingsChange: (settings: GridSettings) => void;
className?: string;
}
export default function GridControls({ gridSettings, onGridSettingsChange, className }: GridControlsProps) {
const [localSettings, setLocalSettings] = useState<GridSettings>(gridSettings);
const handleSettingChange = (key: keyof GridSettings, value: number | boolean) => {
const newSettings = { ...localSettings, [key]: value };
setLocalSettings(newSettings);
onGridSettingsChange(newSettings);
};
const resetToDefault = () => {
const defaultSettings: GridSettings = {
columns: 12,
gap: 16,
padding: 16,
snapToGrid: true,
};
setLocalSettings(defaultSettings);
onGridSettingsChange(defaultSettings);
};
return (
<Card className={className}>
<CardHeader className="pb-3">
<CardTitle className="flex items-center space-x-2 text-sm">
<Grid className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* 격자 열 개수 */}
<div className="space-y-2">
<Label htmlFor="columns" className="text-xs font-medium">
</Label>
<div className="flex items-center space-x-2">
<Slider
id="columns"
min={1}
max={24}
step={1}
value={[localSettings.columns]}
onValueChange={(value) => handleSettingChange("columns", value[0])}
className="flex-1"
/>
<Input
type="number"
min={1}
max={24}
value={localSettings.columns}
onChange={(e) => handleSettingChange("columns", parseInt(e.target.value) || 12)}
className="w-16 text-xs"
/>
</div>
<div className="text-xs text-gray-500">: {localSettings.columns}</div>
</div>
{/* 격자 간격 */}
<div className="space-y-2">
<Label htmlFor="gap" className="text-xs font-medium">
(px)
</Label>
<div className="flex items-center space-x-2">
<Slider
id="gap"
min={0}
max={32}
step={2}
value={[localSettings.gap]}
onValueChange={(value) => handleSettingChange("gap", value[0])}
className="flex-1"
/>
<Input
type="number"
min={0}
max={32}
value={localSettings.gap}
onChange={(e) => handleSettingChange("gap", parseInt(e.target.value) || 16)}
className="w-16 text-xs"
/>
</div>
</div>
{/* 여백 */}
<div className="space-y-2">
<Label htmlFor="padding" className="text-xs font-medium">
(px)
</Label>
<div className="flex items-center space-x-2">
<Slider
id="padding"
min={0}
max={48}
step={4}
value={[localSettings.padding]}
onValueChange={(value) => handleSettingChange("padding", value[0])}
className="flex-1"
/>
<Input
type="number"
min={0}
max={48}
value={localSettings.padding}
onChange={(e) => handleSettingChange("padding", parseInt(e.target.value) || 16)}
className="w-16 text-xs"
/>
</div>
</div>
{/* 격자 스냅 */}
<div className="flex items-center justify-between">
<Label htmlFor="snapToGrid" className="text-xs font-medium">
</Label>
<Button
variant={localSettings.snapToGrid ? "default" : "outline"}
size="sm"
onClick={() => handleSettingChange("snapToGrid", !localSettings.snapToGrid)}
className="h-6 px-2 text-xs"
>
{localSettings.snapToGrid ? "ON" : "OFF"}
</Button>
</div>
{/* 격자 변경 안내 */}
<div className="rounded-md bg-blue-50 p-2 text-xs text-blue-700">
<div className="font-medium">💡 </div>
<div className="mt-1 text-blue-600">
.
</div>
</div>
{/* 격자 미리보기 */}
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<div className="rounded border bg-gray-50 p-2">
<div
className="grid h-16 gap-px"
style={{
gridTemplateColumns: `repeat(${localSettings.columns}, 1fr)`,
gap: `${Math.max(1, localSettings.gap / 4)}px`,
}}
>
{Array.from({ length: localSettings.columns }).map((_, i) => (
<div key={i} className="rounded-sm bg-blue-200" />
))}
</div>
</div>
</div>
{/* 초기화 버튼 */}
<Button variant="outline" size="sm" onClick={resetToDefault} className="w-full text-xs">
<RotateCcw className="mr-1 h-3 w-3" />
</Button>
</CardContent>
</Card>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
interface SliderProps extends React.InputHTMLAttributes<HTMLInputElement> {
value?: number[];
onValueChange?: (value: number[]) => void;
min?: number;
max?: number;
step?: number;
className?: string;
}
const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
({ className, value = [0], onValueChange, min = 0, max = 100, step = 1, ...props }, ref) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = parseFloat(e.target.value);
onValueChange?.([newValue]);
};
return (
<div className={cn("relative flex w-full touch-none items-center select-none", className)}>
<input
ref={ref}
type="range"
min={min}
max={max}
step={step}
value={value[0]}
onChange={handleChange}
className={cn(
"relative h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 outline-none",
"focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
// WebKit 스타일
"[&::-webkit-slider-track]:h-2 [&::-webkit-slider-track]:rounded-lg [&::-webkit-slider-track]:bg-gray-200",
"[&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:bg-blue-600 [&::-webkit-slider-thumb]:shadow-sm",
// Firefox 스타일
"[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded-lg [&::-moz-range-track]:border-0 [&::-moz-range-track]:bg-gray-200",
"[&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-blue-600 [&::-moz-range-thumb]:shadow-sm",
)}
{...props}
/>
</div>
);
},
);
Slider.displayName = "Slider";
export { Slider };

View File

@ -26,7 +26,7 @@ export const FORM_VALIDATION = {
} as const;
export const UI_CONFIG = {
COMPANY_NAME: "PLM 솔루션",
COMPANY_NAME: "WACE 솔루션",
COPYRIGHT: "© 2025 WACE PLM Solution. All rights reserved.",
POWERED_BY: "Powered by Spring Boot + Next.js",
} as const;

View File

@ -1,64 +0,0 @@
/**
*
*/
export interface PageInfo {
title: string;
description?: string;
}
export const PAGE_INFO: Record<string, PageInfo> = {
// 메인 대시보드
"/main": {
title: "대시보드",
description: "PLM 시스템의 주요 현황을 확인하세요",
},
// 관리자 페이지들
"/admin": {
title: "관리자 대시보드",
description: "시스템 관리 및 모니터링",
},
"/admin/company": {
title: "회사 관리",
description: "회사 정보를 등록하고 관리합니다",
},
"/admin/userMng": {
title: "사용자 관리",
description: "시스템 사용자를 관리합니다",
},
"/admin/menu": {
title: "메뉴 관리",
description: "시스템 메뉴를 관리합니다",
},
"/admin/i18n": {
title: "다국어 관리",
description: "다국어 번역을 관리합니다",
},
"/admin/tableMng": {
title: "테이블 타입 관리",
description: "데이터베이스 테이블과 컬럼 타입을 관리합니다",
},
// 기타 페이지들
"/multilang": {
title: "다국어 설정",
description: "언어 설정을 변경합니다",
},
"/dashboard": {
title: "대시보드",
description: "PLM 시스템 현황",
},
// 기본값 (매핑되지 않은 페이지)
default: {
title: "WACE 솔루션",
description: "제품 수명 주기 관리 시스템",
},
};
/**
*
*/
export function getPageInfo(pathname: string): PageInfo {
return PAGE_INFO[pathname] || PAGE_INFO["default"];
}

View File

@ -0,0 +1,166 @@
import { Position, Size } from "@/types/screen";
export interface GridSettings {
columns: number;
gap: number;
padding: number;
snapToGrid: boolean;
}
export interface GridInfo {
columnWidth: number;
totalWidth: number;
totalHeight: number;
}
/**
*
*/
export function calculateGridInfo(
containerWidth: number,
containerHeight: number,
gridSettings: GridSettings,
): GridInfo {
const { columns, gap, padding } = gridSettings;
// 사용 가능한 너비 계산 (패딩 제외)
const availableWidth = containerWidth - padding * 2;
// 격자 간격을 고려한 컬럼 너비 계산
const totalGaps = (columns - 1) * gap;
const columnWidth = (availableWidth - totalGaps) / columns;
return {
columnWidth: Math.max(columnWidth, 50), // 최소 50px
totalWidth: containerWidth,
totalHeight: containerHeight,
};
}
/**
*
*/
export function snapToGrid(position: Position, gridInfo: GridInfo, gridSettings: GridSettings): Position {
if (!gridSettings.snapToGrid) {
return position;
}
const { columnWidth } = gridInfo;
const { gap, padding } = gridSettings;
// 격자 기준으로 위치 계산
const gridX = Math.round((position.x - padding) / (columnWidth + gap));
const gridY = Math.round((position.y - padding) / 20); // 20px 단위로 세로 스냅
// 실제 픽셀 위치로 변환
const snappedX = Math.max(padding, padding + gridX * (columnWidth + gap));
const snappedY = Math.max(padding, padding + gridY * 20);
return {
x: snappedX,
y: snappedY,
z: position.z,
};
}
/**
*
*/
export function snapSizeToGrid(size: Size, gridInfo: GridInfo, gridSettings: GridSettings): Size {
if (!gridSettings.snapToGrid) {
return size;
}
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
// 격자 단위로 너비 계산
const gridColumns = Math.max(1, Math.round(size.width / (columnWidth + gap)));
const snappedWidth = gridColumns * columnWidth + (gridColumns - 1) * gap;
// 높이는 20px 단위로 스냅
const snappedHeight = Math.max(40, Math.round(size.height / 20) * 20);
return {
width: Math.max(columnWidth, snappedWidth),
height: snappedHeight,
};
}
/**
*
*/
export function calculateWidthFromColumns(columns: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
return columns * columnWidth + (columns - 1) * gap;
}
/**
*
*/
export function calculateColumnsFromWidth(width: number, gridInfo: GridInfo, gridSettings: GridSettings): number {
const { columnWidth } = gridInfo;
const { gap } = gridSettings;
return Math.max(1, Math.round((width + gap) / (columnWidth + gap)));
}
/**
*
*/
export function generateGridLines(
containerWidth: number,
containerHeight: number,
gridSettings: GridSettings,
): {
verticalLines: number[];
horizontalLines: number[];
} {
const { columns, gap, padding } = gridSettings;
const gridInfo = calculateGridInfo(containerWidth, containerHeight, gridSettings);
const { columnWidth } = gridInfo;
// 세로 격자선 (컬럼 경계)
const verticalLines: number[] = [];
for (let i = 0; i <= columns; i++) {
const x = padding + i * (columnWidth + gap) - gap / 2;
if (x >= padding && x <= containerWidth - padding) {
verticalLines.push(x);
}
}
// 가로 격자선 (20px 단위)
const horizontalLines: number[] = [];
for (let y = padding; y < containerHeight; y += 20) {
horizontalLines.push(y);
}
return {
verticalLines,
horizontalLines,
};
}
/**
*
*/
export function isOnGridBoundary(
position: Position,
size: Size,
gridInfo: GridInfo,
gridSettings: GridSettings,
tolerance: number = 5,
): boolean {
const snappedPos = snapToGrid(position, gridInfo, gridSettings);
const snappedSize = snapSizeToGrid(size, gridInfo, gridSettings);
const positionMatch =
Math.abs(position.x - snappedPos.x) <= tolerance && Math.abs(position.y - snappedPos.y) <= tolerance;
const sizeMatch =
Math.abs(size.width - snappedSize.width) <= tolerance && Math.abs(size.height - snappedSize.height) <= tolerance;
return positionMatch && sizeMatch;
}

View File

@ -198,6 +198,7 @@ export interface GridSettings {
columns: number; // 기본값: 12
gap: number; // 기본값: 16px
padding: number; // 기본값: 16px
snapToGrid?: boolean; // 격자에 맞춤 여부 (기본값: true)
}
// 유효성 검증 규칙