Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into dataflowMng
This commit is contained in:
@@ -5020,6 +5020,10 @@ model screen_layouts {
|
||||
height Int
|
||||
properties Json?
|
||||
display_order Int @default(0)
|
||||
layout_type String? @db.VarChar(50)
|
||||
layout_config Json?
|
||||
zones_config Json?
|
||||
zone_id String? @db.VarChar(100)
|
||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
|
||||
widgets screen_widgets[]
|
||||
@@ -5255,6 +5259,33 @@ model component_standards {
|
||||
@@index([category], map: "idx_component_standards_category")
|
||||
@@index([company_code], map: "idx_component_standards_company")
|
||||
}
|
||||
|
||||
// 레이아웃 표준 관리 테이블
|
||||
model layout_standards {
|
||||
layout_code String @id @db.VarChar(50)
|
||||
layout_name String @db.VarChar(100)
|
||||
layout_name_eng String? @db.VarChar(100)
|
||||
description String? @db.Text
|
||||
layout_type String @db.VarChar(50)
|
||||
category String @db.VarChar(50)
|
||||
icon_name String? @db.VarChar(50)
|
||||
default_size Json? // { width: number, height: number }
|
||||
layout_config Json // 레이아웃 설정 (그리드, 플렉스박스 등)
|
||||
zones_config Json // 존 설정 (영역 정의)
|
||||
preview_image String? @db.VarChar(255)
|
||||
sort_order Int? @default(0)
|
||||
is_active String? @default("Y") @db.Char(1)
|
||||
is_public String? @default("Y") @db.Char(1)
|
||||
company_code String @db.VarChar(50)
|
||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
created_by String? @db.VarChar(50)
|
||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||
updated_by String? @db.VarChar(50)
|
||||
|
||||
@@index([layout_type], map: "idx_layout_standards_type")
|
||||
@@index([category], map: "idx_layout_standards_category")
|
||||
@@index([company_code], map: "idx_layout_standards_company")
|
||||
}
|
||||
model table_relationships {
|
||||
relationship_id Int @id @default(autoincrement())
|
||||
diagram_id Int // 관계도 그룹 식별자
|
||||
|
||||
105
backend-node/scripts/add-missing-columns.js
Normal file
105
backend-node/scripts/add-missing-columns.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function addMissingColumns() {
|
||||
try {
|
||||
console.log("🔄 누락된 컬럼들을 screen_layouts 테이블에 추가 중...");
|
||||
|
||||
// layout_type 컬럼 추가
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
ALTER TABLE screen_layouts
|
||||
ADD COLUMN IF NOT EXISTS layout_type VARCHAR(50);
|
||||
`;
|
||||
console.log("✅ layout_type 컬럼 추가 완료");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"ℹ️ layout_type 컬럼이 이미 존재하거나 추가 중 오류:",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
// layout_config 컬럼 추가
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
ALTER TABLE screen_layouts
|
||||
ADD COLUMN IF NOT EXISTS layout_config JSONB;
|
||||
`;
|
||||
console.log("✅ layout_config 컬럼 추가 완료");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"ℹ️ layout_config 컬럼이 이미 존재하거나 추가 중 오류:",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
// zones_config 컬럼 추가
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
ALTER TABLE screen_layouts
|
||||
ADD COLUMN IF NOT EXISTS zones_config JSONB;
|
||||
`;
|
||||
console.log("✅ zones_config 컬럼 추가 완료");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"ℹ️ zones_config 컬럼이 이미 존재하거나 추가 중 오류:",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
// zone_id 컬럼 추가
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
ALTER TABLE screen_layouts
|
||||
ADD COLUMN IF NOT EXISTS zone_id VARCHAR(100);
|
||||
`;
|
||||
console.log("✅ zone_id 컬럼 추가 완료");
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"ℹ️ zone_id 컬럼이 이미 존재하거나 추가 중 오류:",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
|
||||
// 인덱스 생성 (성능 향상)
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
CREATE INDEX IF NOT EXISTS idx_screen_layouts_layout_type
|
||||
ON screen_layouts(layout_type);
|
||||
`;
|
||||
console.log("✅ layout_type 인덱스 생성 완료");
|
||||
} catch (error) {
|
||||
console.log("ℹ️ layout_type 인덱스 생성 중 오류:", error.message);
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$executeRaw`
|
||||
CREATE INDEX IF NOT EXISTS idx_screen_layouts_zone_id
|
||||
ON screen_layouts(zone_id);
|
||||
`;
|
||||
console.log("✅ zone_id 인덱스 생성 완료");
|
||||
} catch (error) {
|
||||
console.log("ℹ️ zone_id 인덱스 생성 중 오류:", error.message);
|
||||
}
|
||||
|
||||
// 최종 테이블 구조 확인
|
||||
const columns = await prisma.$queryRaw`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'screen_layouts'
|
||||
ORDER BY ordinal_position
|
||||
`;
|
||||
|
||||
console.log("\n📋 screen_layouts 테이블 최종 구조:");
|
||||
console.table(columns);
|
||||
|
||||
console.log("\n🎉 모든 누락된 컬럼 추가 작업이 완료되었습니다!");
|
||||
} catch (error) {
|
||||
console.error("❌ 컬럼 추가 중 오류 발생:", error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
addMissingColumns();
|
||||
309
backend-node/scripts/init-layout-standards.js
Normal file
309
backend-node/scripts/init-layout-standards.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 레이아웃 표준 데이터 초기화 스크립트
|
||||
* 기본 레이아웃들을 layout_standards 테이블에 삽입합니다.
|
||||
*/
|
||||
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 기본 레이아웃 데이터
|
||||
const PREDEFINED_LAYOUTS = [
|
||||
{
|
||||
layout_code: "GRID_2X2_001",
|
||||
layout_name: "2x2 그리드",
|
||||
layout_name_eng: "2x2 Grid",
|
||||
description: "2행 2열의 균등한 그리드 레이아웃입니다.",
|
||||
layout_type: "grid",
|
||||
category: "basic",
|
||||
icon_name: "grid",
|
||||
default_size: { width: 800, height: 600 },
|
||||
layout_config: {
|
||||
grid: { rows: 2, columns: 2, gap: 16 },
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "zone1",
|
||||
name: "상단 좌측",
|
||||
position: { row: 0, column: 0 },
|
||||
size: { width: "50%", height: "50%" },
|
||||
},
|
||||
{
|
||||
id: "zone2",
|
||||
name: "상단 우측",
|
||||
position: { row: 0, column: 1 },
|
||||
size: { width: "50%", height: "50%" },
|
||||
},
|
||||
{
|
||||
id: "zone3",
|
||||
name: "하단 좌측",
|
||||
position: { row: 1, column: 0 },
|
||||
size: { width: "50%", height: "50%" },
|
||||
},
|
||||
{
|
||||
id: "zone4",
|
||||
name: "하단 우측",
|
||||
position: { row: 1, column: 1 },
|
||||
size: { width: "50%", height: "50%" },
|
||||
},
|
||||
],
|
||||
sort_order: 1,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
{
|
||||
layout_code: "FORM_TWO_COLUMN_001",
|
||||
layout_name: "2단 폼 레이아웃",
|
||||
layout_name_eng: "Two Column Form",
|
||||
description: "좌우 2단으로 구성된 폼 레이아웃입니다.",
|
||||
layout_type: "grid",
|
||||
category: "form",
|
||||
icon_name: "columns",
|
||||
default_size: { width: 800, height: 400 },
|
||||
layout_config: {
|
||||
grid: { rows: 1, columns: 2, gap: 24 },
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "left",
|
||||
name: "좌측 입력 영역",
|
||||
position: { row: 0, column: 0 },
|
||||
size: { width: "50%", height: "100%" },
|
||||
},
|
||||
{
|
||||
id: "right",
|
||||
name: "우측 입력 영역",
|
||||
position: { row: 0, column: 1 },
|
||||
size: { width: "50%", height: "100%" },
|
||||
},
|
||||
],
|
||||
sort_order: 2,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
{
|
||||
layout_code: "FLEXBOX_ROW_001",
|
||||
layout_name: "가로 플렉스박스",
|
||||
layout_name_eng: "Horizontal Flexbox",
|
||||
description: "가로 방향으로 배치되는 플렉스박스 레이아웃입니다.",
|
||||
layout_type: "flexbox",
|
||||
category: "basic",
|
||||
icon_name: "flex",
|
||||
default_size: { width: 800, height: 300 },
|
||||
layout_config: {
|
||||
flexbox: {
|
||||
direction: "row",
|
||||
justify: "flex-start",
|
||||
align: "stretch",
|
||||
wrap: "nowrap",
|
||||
gap: 16,
|
||||
},
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "left",
|
||||
name: "좌측 영역",
|
||||
position: {},
|
||||
size: { width: "50%", height: "100%" },
|
||||
},
|
||||
{
|
||||
id: "right",
|
||||
name: "우측 영역",
|
||||
position: {},
|
||||
size: { width: "50%", height: "100%" },
|
||||
},
|
||||
],
|
||||
sort_order: 3,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
{
|
||||
layout_code: "SPLIT_HORIZONTAL_001",
|
||||
layout_name: "수평 분할",
|
||||
layout_name_eng: "Horizontal Split",
|
||||
description: "크기 조절이 가능한 수평 분할 레이아웃입니다.",
|
||||
layout_type: "split",
|
||||
category: "basic",
|
||||
icon_name: "separator-horizontal",
|
||||
default_size: { width: 800, height: 400 },
|
||||
layout_config: {
|
||||
split: {
|
||||
direction: "horizontal",
|
||||
ratio: [50, 50],
|
||||
minSize: [200, 200],
|
||||
resizable: true,
|
||||
splitterSize: 4,
|
||||
},
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "left",
|
||||
name: "좌측 패널",
|
||||
position: {},
|
||||
size: { width: "50%", height: "100%" },
|
||||
isResizable: true,
|
||||
},
|
||||
{
|
||||
id: "right",
|
||||
name: "우측 패널",
|
||||
position: {},
|
||||
size: { width: "50%", height: "100%" },
|
||||
isResizable: true,
|
||||
},
|
||||
],
|
||||
sort_order: 4,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
{
|
||||
layout_code: "TABS_HORIZONTAL_001",
|
||||
layout_name: "수평 탭",
|
||||
layout_name_eng: "Horizontal Tabs",
|
||||
description: "상단에 탭이 있는 탭 레이아웃입니다.",
|
||||
layout_type: "tabs",
|
||||
category: "navigation",
|
||||
icon_name: "tabs",
|
||||
default_size: { width: 800, height: 500 },
|
||||
layout_config: {
|
||||
tabs: {
|
||||
position: "top",
|
||||
variant: "default",
|
||||
size: "md",
|
||||
defaultTab: "tab1",
|
||||
closable: false,
|
||||
},
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "tab1",
|
||||
name: "첫 번째 탭",
|
||||
position: {},
|
||||
size: { width: "100%", height: "100%" },
|
||||
},
|
||||
{
|
||||
id: "tab2",
|
||||
name: "두 번째 탭",
|
||||
position: {},
|
||||
size: { width: "100%", height: "100%" },
|
||||
},
|
||||
{
|
||||
id: "tab3",
|
||||
name: "세 번째 탭",
|
||||
position: {},
|
||||
size: { width: "100%", height: "100%" },
|
||||
},
|
||||
],
|
||||
sort_order: 5,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
{
|
||||
layout_code: "TABLE_WITH_FILTERS_001",
|
||||
layout_name: "필터가 있는 테이블",
|
||||
layout_name_eng: "Table with Filters",
|
||||
description: "상단에 필터가 있고 하단에 테이블이 있는 레이아웃입니다.",
|
||||
layout_type: "flexbox",
|
||||
category: "table",
|
||||
icon_name: "table",
|
||||
default_size: { width: 1000, height: 600 },
|
||||
layout_config: {
|
||||
flexbox: {
|
||||
direction: "column",
|
||||
justify: "flex-start",
|
||||
align: "stretch",
|
||||
wrap: "nowrap",
|
||||
gap: 16,
|
||||
},
|
||||
},
|
||||
zones_config: [
|
||||
{
|
||||
id: "filters",
|
||||
name: "검색 필터",
|
||||
position: {},
|
||||
size: { width: "100%", height: "auto" },
|
||||
},
|
||||
{
|
||||
id: "table",
|
||||
name: "데이터 테이블",
|
||||
position: {},
|
||||
size: { width: "100%", height: "1fr" },
|
||||
},
|
||||
],
|
||||
sort_order: 6,
|
||||
is_active: "Y",
|
||||
is_public: "Y",
|
||||
company_code: "DEFAULT",
|
||||
},
|
||||
];
|
||||
|
||||
async function initializeLayoutStandards() {
|
||||
try {
|
||||
console.log("🏗️ 레이아웃 표준 데이터 초기화 시작...");
|
||||
|
||||
// 기존 데이터 확인
|
||||
const existingLayouts = await prisma.layout_standards.count();
|
||||
if (existingLayouts > 0) {
|
||||
console.log(`⚠️ 이미 ${existingLayouts}개의 레이아웃이 존재합니다.`);
|
||||
console.log(
|
||||
"기존 데이터를 삭제하고 새로 생성하시겠습니까? (기본값: 건너뛰기)"
|
||||
);
|
||||
|
||||
// 기존 데이터가 있으면 건너뛰기 (안전을 위해)
|
||||
console.log("💡 기존 데이터를 유지하고 건너뜁니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 데이터 삽입
|
||||
let insertedCount = 0;
|
||||
|
||||
for (const layoutData of PREDEFINED_LAYOUTS) {
|
||||
try {
|
||||
await prisma.layout_standards.create({
|
||||
data: {
|
||||
...layoutData,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
created_by: "SYSTEM",
|
||||
updated_by: "SYSTEM",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ ${layoutData.layout_name} 생성 완료`);
|
||||
insertedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ ${layoutData.layout_name} 생성 실패:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🎉 레이아웃 표준 데이터 초기화 완료! (${insertedCount}/${PREDEFINED_LAYOUTS.length})`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("❌ 레이아웃 표준 데이터 초기화 실패:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 스크립트 실행
|
||||
if (require.main === module) {
|
||||
initializeLayoutStandards()
|
||||
.then(() => {
|
||||
console.log("✨ 스크립트 실행 완료");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("💥 스크립트 실행 실패:", error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initializeLayoutStandards };
|
||||
|
||||
46
backend-node/scripts/list-components.js
Normal file
46
backend-node/scripts/list-components.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function getComponents() {
|
||||
try {
|
||||
const components = await prisma.component_standards.findMany({
|
||||
where: { is_active: "Y" },
|
||||
select: {
|
||||
component_code: true,
|
||||
component_name: true,
|
||||
category: true,
|
||||
component_config: true,
|
||||
},
|
||||
orderBy: [{ category: "asc" }, { sort_order: "asc" }],
|
||||
});
|
||||
|
||||
console.log("📋 데이터베이스 컴포넌트 목록:");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
const grouped = components.reduce((acc, comp) => {
|
||||
if (!acc[comp.category]) {
|
||||
acc[comp.category] = [];
|
||||
}
|
||||
acc[comp.category].push(comp);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.entries(grouped).forEach(([category, comps]) => {
|
||||
console.log(`\n🏷️ ${category.toUpperCase()} 카테고리:`);
|
||||
comps.forEach((comp) => {
|
||||
const type = comp.component_config?.type || "unknown";
|
||||
console.log(
|
||||
` - ${comp.component_code}: ${comp.component_name} (type: ${type})`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`\n총 ${components.length}개 컴포넌트 발견`);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
getComponents();
|
||||
@@ -26,6 +26,8 @@ import buttonActionStandardRoutes from "./routes/buttonActionStandardRoutes";
|
||||
import screenStandardRoutes from "./routes/screenStandardRoutes";
|
||||
import templateStandardRoutes from "./routes/templateStandardRoutes";
|
||||
import componentStandardRoutes from "./routes/componentStandardRoutes";
|
||||
import layoutRoutes from "./routes/layoutRoutes";
|
||||
import dataRoutes from "./routes/dataRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
@@ -114,7 +116,9 @@ app.use("/api/admin/web-types", webTypeStandardRoutes);
|
||||
app.use("/api/admin/button-actions", buttonActionStandardRoutes);
|
||||
app.use("/api/admin/template-standards", templateStandardRoutes);
|
||||
app.use("/api/admin/component-standards", componentStandardRoutes);
|
||||
app.use("/api/layouts", layoutRoutes);
|
||||
app.use("/api/screen", screenStandardRoutes);
|
||||
app.use("/api/data", dataRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
||||
@@ -204,7 +204,15 @@ class ComponentStandardController {
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("컴포넌트 수정 실패:", error);
|
||||
const { component_code } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.error("컴포넌트 수정 실패 [상세]:", {
|
||||
component_code,
|
||||
updateData,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "컴포넌트 수정에 실패했습니다.",
|
||||
@@ -382,6 +390,52 @@ class ComponentStandardController {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 코드 중복 체크
|
||||
*/
|
||||
async checkDuplicate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { component_code } = req.params;
|
||||
|
||||
if (!component_code) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "컴포넌트 코드가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicate = await componentStandardService.checkDuplicate(
|
||||
component_code,
|
||||
req.user?.companyCode
|
||||
);
|
||||
|
||||
console.log(
|
||||
`🔍 중복 체크 결과: component_code=${component_code}, company_code=${req.user?.companyCode}, isDuplicate=${isDuplicate}`
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: { isDuplicate, component_code },
|
||||
message: isDuplicate
|
||||
? "이미 사용 중인 컴포넌트 코드입니다."
|
||||
: "사용 가능한 컴포넌트 코드입니다.",
|
||||
});
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("컴포넌트 코드 중복 체크 실패:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "컴포넌트 코드 중복 체크에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ComponentStandardController();
|
||||
|
||||
276
backend-node/src/controllers/layoutController.ts
Normal file
276
backend-node/src/controllers/layoutController.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { Request, Response } from "express";
|
||||
import { layoutService } from "../services/layoutService";
|
||||
import {
|
||||
CreateLayoutRequest,
|
||||
UpdateLayoutRequest,
|
||||
GetLayoutsRequest,
|
||||
DuplicateLayoutRequest,
|
||||
} from "../types/layout";
|
||||
|
||||
export class LayoutController {
|
||||
/**
|
||||
* 레이아웃 목록 조회
|
||||
*/
|
||||
async getLayouts(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const {
|
||||
page = 1,
|
||||
size = 20,
|
||||
category,
|
||||
layoutType,
|
||||
searchTerm,
|
||||
includePublic = true,
|
||||
} = req.query as any;
|
||||
|
||||
const params = {
|
||||
page: parseInt(page, 10),
|
||||
size: parseInt(size, 10),
|
||||
category,
|
||||
layoutType,
|
||||
searchTerm,
|
||||
companyCode: user.companyCode,
|
||||
includePublic: includePublic === "true",
|
||||
};
|
||||
|
||||
const result = await layoutService.getLayouts(params);
|
||||
|
||||
const response = {
|
||||
...result,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
totalPages: Math.ceil(result.total / params.size),
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: response,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 목록 조회 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 목록 조회에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 상세 조회
|
||||
*/
|
||||
async getLayoutById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const { id: layoutCode } = req.params;
|
||||
|
||||
const layout = await layoutService.getLayoutById(
|
||||
layoutCode,
|
||||
user.companyCode
|
||||
);
|
||||
|
||||
if (!layout) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "레이아웃을 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: layout,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 상세 조회 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 상세 조회에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 생성
|
||||
*/
|
||||
async createLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const layoutRequest: CreateLayoutRequest = req.body;
|
||||
|
||||
// 요청 데이터 검증
|
||||
if (
|
||||
!layoutRequest.layoutName ||
|
||||
!layoutRequest.layoutType ||
|
||||
!layoutRequest.category
|
||||
) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"필수 필드가 누락되었습니다. (layoutName, layoutType, category)",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layoutRequest.layoutConfig || !layoutRequest.zonesConfig) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "레이아웃 설정과 존 설정은 필수입니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const layout = await layoutService.createLayout(
|
||||
layoutRequest,
|
||||
user.companyCode,
|
||||
user.userId
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: layout,
|
||||
message: "레이아웃이 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 생성 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 생성에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 수정
|
||||
*/
|
||||
async updateLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const { id: layoutCode } = req.params;
|
||||
const updateRequest: Partial<CreateLayoutRequest> = req.body;
|
||||
|
||||
const updatedLayout = await layoutService.updateLayout(
|
||||
{ ...updateRequest, layoutCode },
|
||||
user.companyCode,
|
||||
user.userId
|
||||
);
|
||||
|
||||
if (!updatedLayout) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "레이아웃을 찾을 수 없거나 수정 권한이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedLayout,
|
||||
message: "레이아웃이 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 수정 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 수정에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 삭제
|
||||
*/
|
||||
async deleteLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const { id: layoutCode } = req.params;
|
||||
|
||||
await layoutService.deleteLayout(
|
||||
layoutCode,
|
||||
user.companyCode,
|
||||
user.userId
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "레이아웃이 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 삭제 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 삭제에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 복제
|
||||
*/
|
||||
async duplicateLayout(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
const { id: layoutCode } = req.params;
|
||||
const { newName }: DuplicateLayoutRequest = req.body;
|
||||
|
||||
if (!newName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "새 레이아웃 이름이 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const duplicatedLayout = await layoutService.duplicateLayout(
|
||||
layoutCode,
|
||||
newName,
|
||||
user.companyCode,
|
||||
user.userId
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: duplicatedLayout,
|
||||
message: "레이아웃이 성공적으로 복제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("레이아웃 복제 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "레이아웃 복제에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 레이아웃 개수 조회
|
||||
*/
|
||||
async getLayoutCountsByCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { user } = req as any;
|
||||
|
||||
const counts = await layoutService.getLayoutCountsByCategory(
|
||||
user.companyCode
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: counts,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("카테고리별 레이아웃 개수 조회 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "카테고리별 레이아웃 개수 조회에 실패했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const layoutController = new LayoutController();
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import { Request, Response } from "express";
|
||||
import { templateStandardService } from "../services/templateStandardService";
|
||||
import { handleError } from "../utils/errorHandler";
|
||||
import { checkMissingFields } from "../utils/validation";
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
companyCode: string;
|
||||
company_code?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 표준 관리 컨트롤러
|
||||
@@ -11,26 +17,26 @@ export class TemplateStandardController {
|
||||
/**
|
||||
* 템플릿 목록 조회
|
||||
*/
|
||||
async getTemplates(req: AuthenticatedRequest, res: Response) {
|
||||
async getTemplates(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const {
|
||||
active = "Y",
|
||||
category,
|
||||
search,
|
||||
companyCode,
|
||||
company_code,
|
||||
is_public = "Y",
|
||||
page = "1",
|
||||
limit = "50",
|
||||
} = req.query;
|
||||
|
||||
const user = req.user;
|
||||
const userCompanyCode = user?.companyCode || "DEFAULT";
|
||||
const userCompanyCode = user?.company_code || "DEFAULT";
|
||||
|
||||
const result = await templateStandardService.getTemplates({
|
||||
active: active as string,
|
||||
category: category as string,
|
||||
search: search as string,
|
||||
company_code: (companyCode as string) || userCompanyCode,
|
||||
company_code: (company_code as string) || userCompanyCode,
|
||||
is_public: is_public as string,
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
@@ -47,23 +53,24 @@ export class TemplateStandardController {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(
|
||||
res,
|
||||
error,
|
||||
"템플릿 목록 조회 중 오류가 발생했습니다."
|
||||
);
|
||||
console.error("템플릿 목록 조회 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 목록 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 상세 조회
|
||||
*/
|
||||
async getTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async getTemplate(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { templateCode } = req.params;
|
||||
|
||||
if (!templateCode) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "템플릿 코드가 필요합니다.",
|
||||
});
|
||||
@@ -72,7 +79,7 @@ export class TemplateStandardController {
|
||||
const template = await templateStandardService.getTemplate(templateCode);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "템플릿을 찾을 수 없습니다.",
|
||||
});
|
||||
@@ -83,40 +90,46 @@ export class TemplateStandardController {
|
||||
data: template,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 조회 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 조회 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 생성
|
||||
*/
|
||||
async createTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async createTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const user = req.user;
|
||||
const templateData = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = [
|
||||
"template_code",
|
||||
"template_name",
|
||||
"category",
|
||||
"layout_config",
|
||||
];
|
||||
const missingFields = checkMissingFields(templateData, requiredFields);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return res.status(400).json({
|
||||
if (
|
||||
!templateData.template_code ||
|
||||
!templateData.template_name ||
|
||||
!templateData.category ||
|
||||
!templateData.layout_config
|
||||
) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `필수 필드가 누락되었습니다: ${missingFields.join(", ")}`,
|
||||
message:
|
||||
"필수 필드가 누락되었습니다. (template_code, template_name, category, layout_config)",
|
||||
});
|
||||
}
|
||||
|
||||
// 회사 코드와 생성자 정보 추가
|
||||
const templateWithMeta = {
|
||||
...templateData,
|
||||
company_code: user?.companyCode || "DEFAULT",
|
||||
created_by: user?.userId || "system",
|
||||
updated_by: user?.userId || "system",
|
||||
company_code: user?.company_code || "DEFAULT",
|
||||
created_by: user?.user_id || "system",
|
||||
updated_by: user?.user_id || "system",
|
||||
};
|
||||
|
||||
const newTemplate =
|
||||
@@ -128,21 +141,29 @@ export class TemplateStandardController {
|
||||
message: "템플릿이 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 생성 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 생성 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 생성 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 수정
|
||||
*/
|
||||
async updateTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async updateTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { templateCode } = req.params;
|
||||
const templateData = req.body;
|
||||
const user = req.user;
|
||||
|
||||
if (!templateCode) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "템플릿 코드가 필요합니다.",
|
||||
});
|
||||
@@ -151,7 +172,7 @@ export class TemplateStandardController {
|
||||
// 수정자 정보 추가
|
||||
const templateWithMeta = {
|
||||
...templateData,
|
||||
updated_by: user?.userId || "system",
|
||||
updated_by: user?.user_id || "system",
|
||||
};
|
||||
|
||||
const updatedTemplate = await templateStandardService.updateTemplate(
|
||||
@@ -160,7 +181,7 @@ export class TemplateStandardController {
|
||||
);
|
||||
|
||||
if (!updatedTemplate) {
|
||||
return res.status(404).json({
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "템플릿을 찾을 수 없습니다.",
|
||||
});
|
||||
@@ -172,19 +193,27 @@ export class TemplateStandardController {
|
||||
message: "템플릿이 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 수정 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 수정 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 수정 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 삭제
|
||||
*/
|
||||
async deleteTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async deleteTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { templateCode } = req.params;
|
||||
|
||||
if (!templateCode) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "템플릿 코드가 필요합니다.",
|
||||
});
|
||||
@@ -194,7 +223,7 @@ export class TemplateStandardController {
|
||||
await templateStandardService.deleteTemplate(templateCode);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "템플릿을 찾을 수 없습니다.",
|
||||
});
|
||||
@@ -205,19 +234,27 @@ export class TemplateStandardController {
|
||||
message: "템플릿이 성공적으로 삭제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 삭제 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 삭제 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 삭제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 정렬 순서 일괄 업데이트
|
||||
*/
|
||||
async updateSortOrder(req: AuthenticatedRequest, res: Response) {
|
||||
async updateSortOrder(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { templates } = req.body;
|
||||
|
||||
if (!Array.isArray(templates)) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "templates는 배열이어야 합니다.",
|
||||
});
|
||||
@@ -230,25 +267,29 @@ export class TemplateStandardController {
|
||||
message: "템플릿 정렬 순서가 성공적으로 업데이트되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(
|
||||
res,
|
||||
error,
|
||||
"템플릿 정렬 순서 업데이트 중 오류가 발생했습니다."
|
||||
);
|
||||
console.error("템플릿 정렬 순서 업데이트 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 정렬 순서 업데이트 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 복제
|
||||
*/
|
||||
async duplicateTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async duplicateTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { templateCode } = req.params;
|
||||
const { new_template_code, new_template_name } = req.body;
|
||||
const user = req.user;
|
||||
|
||||
if (!templateCode || !new_template_code || !new_template_name) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "필수 필드가 누락되었습니다.",
|
||||
});
|
||||
@@ -259,8 +300,8 @@ export class TemplateStandardController {
|
||||
originalCode: templateCode,
|
||||
newCode: new_template_code,
|
||||
newName: new_template_name,
|
||||
company_code: user?.companyCode || "DEFAULT",
|
||||
created_by: user?.userId || "system",
|
||||
company_code: user?.company_code || "DEFAULT",
|
||||
created_by: user?.user_id || "system",
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
@@ -269,17 +310,22 @@ export class TemplateStandardController {
|
||||
message: "템플릿이 성공적으로 복제되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 복제 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 복제 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 복제 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 카테고리 목록 조회
|
||||
*/
|
||||
async getCategories(req: AuthenticatedRequest, res: Response) {
|
||||
async getCategories(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = req.user;
|
||||
const companyCode = user?.companyCode || "DEFAULT";
|
||||
const companyCode = user?.company_code || "DEFAULT";
|
||||
|
||||
const categories =
|
||||
await templateStandardService.getCategories(companyCode);
|
||||
@@ -289,24 +335,28 @@ export class TemplateStandardController {
|
||||
data: categories,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(
|
||||
res,
|
||||
error,
|
||||
"템플릿 카테고리 조회 중 오류가 발생했습니다."
|
||||
);
|
||||
console.error("템플릿 카테고리 조회 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 카테고리 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 가져오기 (JSON 파일에서)
|
||||
*/
|
||||
async importTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async importTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const user = req.user;
|
||||
const templateData = req.body;
|
||||
|
||||
if (!templateData.layout_config) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "유효한 템플릿 데이터가 아닙니다.",
|
||||
});
|
||||
@@ -315,9 +365,9 @@ export class TemplateStandardController {
|
||||
// 회사 코드와 생성자 정보 추가
|
||||
const templateWithMeta = {
|
||||
...templateData,
|
||||
company_code: user?.companyCode || "DEFAULT",
|
||||
created_by: user?.userId || "system",
|
||||
updated_by: user?.userId || "system",
|
||||
company_code: user?.company_code || "DEFAULT",
|
||||
created_by: user?.user_id || "system",
|
||||
updated_by: user?.user_id || "system",
|
||||
};
|
||||
|
||||
const importedTemplate =
|
||||
@@ -329,31 +379,41 @@ export class TemplateStandardController {
|
||||
message: "템플릿이 성공적으로 가져왔습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 가져오기 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 가져오기 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 가져오기 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿 내보내기 (JSON 형태로)
|
||||
*/
|
||||
async exportTemplate(req: AuthenticatedRequest, res: Response) {
|
||||
async exportTemplate(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { templateCode } = req.params;
|
||||
|
||||
if (!templateCode) {
|
||||
return res.status(400).json({
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "템플릿 코드가 필요합니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const template = await templateStandardService.getTemplate(templateCode);
|
||||
|
||||
if (!template) {
|
||||
return res.status(404).json({
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "템플릿을 찾을 수 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 내보내기용 데이터 (메타데이터 제외)
|
||||
@@ -373,7 +433,12 @@ export class TemplateStandardController {
|
||||
data: exportData,
|
||||
});
|
||||
} catch (error) {
|
||||
return handleError(res, error, "템플릿 내보내기 중 오류가 발생했습니다.");
|
||||
console.error("템플릿 내보내기 중 오류:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "템플릿 내보내기 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "알 수 없는 오류",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ router.get(
|
||||
componentStandardController.getStatistics.bind(componentStandardController)
|
||||
);
|
||||
|
||||
// 컴포넌트 코드 중복 체크
|
||||
router.get(
|
||||
"/check-duplicate/:component_code",
|
||||
componentStandardController.checkDuplicate.bind(componentStandardController)
|
||||
);
|
||||
|
||||
// 컴포넌트 상세 조회
|
||||
router.get(
|
||||
"/:component_code",
|
||||
|
||||
130
backend-node/src/routes/dataRoutes.ts
Normal file
130
backend-node/src/routes/dataRoutes.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import express from "express";
|
||||
import { dataService } from "../services/dataService";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* 동적 테이블 데이터 조회 API
|
||||
* GET /api/data/{tableName}
|
||||
*/
|
||||
router.get(
|
||||
"/:tableName",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const { limit = "10", offset = "0", orderBy, ...filters } = req.query;
|
||||
|
||||
// 입력값 검증
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 필요합니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
// SQL 인젝션 방지를 위한 테이블명 검증
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 테이블명입니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📊 데이터 조회 요청: ${tableName}`, {
|
||||
limit: parseInt(limit as string),
|
||||
offset: parseInt(offset as string),
|
||||
orderBy: orderBy as string,
|
||||
filters,
|
||||
user: req.user?.userId,
|
||||
});
|
||||
|
||||
// 데이터 조회
|
||||
const result = await dataService.getTableData({
|
||||
tableName,
|
||||
limit: parseInt(limit as string),
|
||||
offset: parseInt(offset as string),
|
||||
orderBy: orderBy as string,
|
||||
filters: filters as Record<string, string>,
|
||||
userCompany: req.user?.companyCode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 데이터 조회 성공: ${tableName}, ${result.data?.length || 0}개 항목`
|
||||
);
|
||||
|
||||
return res.json(result.data);
|
||||
} catch (error) {
|
||||
console.error("데이터 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회 API
|
||||
* GET /api/data/{tableName}/columns
|
||||
*/
|
||||
router.get(
|
||||
"/:tableName/columns",
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
|
||||
// 입력값 검증
|
||||
if (!tableName || typeof tableName !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "테이블명이 필요합니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
// SQL 인젝션 방지를 위한 테이블명 검증
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "유효하지 않은 테이블명입니다.",
|
||||
error: "INVALID_TABLE_NAME",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`📋 컬럼 정보 조회: ${tableName}`);
|
||||
|
||||
// 컬럼 정보 조회
|
||||
const result = await dataService.getTableColumns(tableName);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 컬럼 정보 조회 성공: ${tableName}, ${result.data?.length || 0}개 컬럼`
|
||||
);
|
||||
|
||||
return res.json(result);
|
||||
} catch (error) {
|
||||
console.error("컬럼 정보 조회 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
73
backend-node/src/routes/layoutRoutes.ts
Normal file
73
backend-node/src/routes/layoutRoutes.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Router } from "express";
|
||||
import { layoutController } from "../controllers/layoutController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 모든 레이아웃 라우트에 인증 미들웨어 적용
|
||||
router.use(authenticateToken);
|
||||
|
||||
/**
|
||||
* @route GET /api/layouts
|
||||
* @desc 레이아웃 목록 조회
|
||||
* @access Private
|
||||
* @params page, size, category, layoutType, searchTerm, includePublic
|
||||
*/
|
||||
router.get("/", layoutController.getLayouts.bind(layoutController));
|
||||
|
||||
/**
|
||||
* @route GET /api/layouts/counts-by-category
|
||||
* @desc 카테고리별 레이아웃 개수 조회
|
||||
* @access Private
|
||||
*/
|
||||
router.get(
|
||||
"/counts-by-category",
|
||||
layoutController.getLayoutCountsByCategory.bind(layoutController)
|
||||
);
|
||||
|
||||
/**
|
||||
* @route GET /api/layouts/:id
|
||||
* @desc 레이아웃 상세 조회
|
||||
* @access Private
|
||||
* @params id (layoutCode)
|
||||
*/
|
||||
router.get("/:id", layoutController.getLayoutById.bind(layoutController));
|
||||
|
||||
/**
|
||||
* @route POST /api/layouts
|
||||
* @desc 레이아웃 생성
|
||||
* @access Private
|
||||
* @body CreateLayoutRequest
|
||||
*/
|
||||
router.post("/", layoutController.createLayout.bind(layoutController));
|
||||
|
||||
/**
|
||||
* @route PUT /api/layouts/:id
|
||||
* @desc 레이아웃 수정
|
||||
* @access Private
|
||||
* @params id (layoutCode)
|
||||
* @body Partial<CreateLayoutRequest>
|
||||
*/
|
||||
router.put("/:id", layoutController.updateLayout.bind(layoutController));
|
||||
|
||||
/**
|
||||
* @route DELETE /api/layouts/:id
|
||||
* @desc 레이아웃 삭제
|
||||
* @access Private
|
||||
* @params id (layoutCode)
|
||||
*/
|
||||
router.delete("/:id", layoutController.deleteLayout.bind(layoutController));
|
||||
|
||||
/**
|
||||
* @route POST /api/layouts/:id/duplicate
|
||||
* @desc 레이아웃 복제
|
||||
* @access Private
|
||||
* @params id (layoutCode)
|
||||
* @body { newName: string }
|
||||
*/
|
||||
router.post(
|
||||
"/:id/duplicate",
|
||||
layoutController.duplicateLayout.bind(layoutController)
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -131,9 +131,16 @@ class ComponentStandardService {
|
||||
);
|
||||
}
|
||||
|
||||
// 'active' 필드를 'is_active'로 변환
|
||||
const createData = { ...data };
|
||||
if ("active" in createData) {
|
||||
createData.is_active = (createData as any).active;
|
||||
delete (createData as any).active;
|
||||
}
|
||||
|
||||
const component = await prisma.component_standards.create({
|
||||
data: {
|
||||
...data,
|
||||
...createData,
|
||||
created_date: new Date(),
|
||||
updated_date: new Date(),
|
||||
},
|
||||
@@ -151,10 +158,17 @@ class ComponentStandardService {
|
||||
) {
|
||||
const existing = await this.getComponent(component_code);
|
||||
|
||||
// 'active' 필드를 'is_active'로 변환
|
||||
const updateData = { ...data };
|
||||
if ("active" in updateData) {
|
||||
updateData.is_active = (updateData as any).active;
|
||||
delete (updateData as any).active;
|
||||
}
|
||||
|
||||
const component = await prisma.component_standards.update({
|
||||
where: { component_code },
|
||||
data: {
|
||||
...data,
|
||||
...updateData,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
@@ -216,21 +230,19 @@ class ComponentStandardService {
|
||||
data: {
|
||||
component_code: new_code,
|
||||
component_name: new_name,
|
||||
component_name_eng: source.component_name_eng,
|
||||
description: source.description,
|
||||
category: source.category,
|
||||
icon_name: source.icon_name,
|
||||
default_size: source.default_size as any,
|
||||
component_config: source.component_config as any,
|
||||
preview_image: source.preview_image,
|
||||
sort_order: source.sort_order,
|
||||
is_active: source.is_active,
|
||||
is_public: source.is_public,
|
||||
company_code: source.company_code,
|
||||
component_name_eng: source?.component_name_eng,
|
||||
description: source?.description,
|
||||
category: source?.category,
|
||||
icon_name: source?.icon_name,
|
||||
default_size: source?.default_size as any,
|
||||
component_config: source?.component_config as any,
|
||||
preview_image: source?.preview_image,
|
||||
sort_order: source?.sort_order,
|
||||
is_active: source?.is_active,
|
||||
is_public: source?.is_public,
|
||||
company_code: source?.company_code || "DEFAULT",
|
||||
created_date: new Date(),
|
||||
created_by: source.created_by,
|
||||
updated_date: new Date(),
|
||||
updated_by: source.updated_by,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -297,6 +309,27 @@ class ComponentStandardService {
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 컴포넌트 코드 중복 체크
|
||||
*/
|
||||
async checkDuplicate(
|
||||
component_code: string,
|
||||
company_code?: string
|
||||
): Promise<boolean> {
|
||||
const whereClause: any = { component_code };
|
||||
|
||||
// 회사 코드가 있고 "*"가 아닌 경우에만 조건 추가
|
||||
if (company_code && company_code !== "*") {
|
||||
whereClause.company_code = company_code;
|
||||
}
|
||||
|
||||
const existingComponent = await prisma.component_standards.findFirst({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
return !!existingComponent;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ComponentStandardService();
|
||||
|
||||
328
backend-node/src/services/dataService.ts
Normal file
328
backend-node/src/services/dataService.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
interface GetTableDataParams {
|
||||
tableName: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: string;
|
||||
filters?: Record<string, string>;
|
||||
userCompany?: string;
|
||||
}
|
||||
|
||||
interface ServiceResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 안전한 테이블명 목록 (화이트리스트)
|
||||
* SQL 인젝션 방지를 위해 허용된 테이블만 접근 가능
|
||||
*/
|
||||
const ALLOWED_TABLES = [
|
||||
"company_mng",
|
||||
"user_info",
|
||||
"dept_info",
|
||||
"code_info",
|
||||
"code_category",
|
||||
"menu_info",
|
||||
"approval",
|
||||
"approval_kind",
|
||||
"board",
|
||||
"comm_code",
|
||||
"product_mng",
|
||||
"part_mng",
|
||||
"material_mng",
|
||||
"order_mng_master",
|
||||
"inventory_mng",
|
||||
"contract_mgmt",
|
||||
"project_mgmt",
|
||||
"screen_definitions",
|
||||
"screen_layouts",
|
||||
"layout_standards",
|
||||
"component_standards",
|
||||
"web_type_standards",
|
||||
"button_action_standards",
|
||||
"template_standards",
|
||||
"grid_standards",
|
||||
"style_templates",
|
||||
"multi_lang_key_master",
|
||||
"multi_lang_text",
|
||||
"language_master",
|
||||
"table_labels",
|
||||
"column_labels",
|
||||
"dynamic_form_data",
|
||||
];
|
||||
|
||||
/**
|
||||
* 회사별 필터링이 필요한 테이블 목록
|
||||
*/
|
||||
const COMPANY_FILTERED_TABLES = [
|
||||
"company_mng",
|
||||
"user_info",
|
||||
"dept_info",
|
||||
"approval",
|
||||
"board",
|
||||
"product_mng",
|
||||
"part_mng",
|
||||
"material_mng",
|
||||
"order_mng_master",
|
||||
"inventory_mng",
|
||||
"contract_mgmt",
|
||||
"project_mgmt",
|
||||
];
|
||||
|
||||
class DataService {
|
||||
/**
|
||||
* 테이블 데이터 조회
|
||||
*/
|
||||
async getTableData(
|
||||
params: GetTableDataParams
|
||||
): Promise<ServiceResponse<any[]>> {
|
||||
const {
|
||||
tableName,
|
||||
limit = 10,
|
||||
offset = 0,
|
||||
orderBy,
|
||||
filters = {},
|
||||
userCompany,
|
||||
} = params;
|
||||
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블 존재 여부 확인
|
||||
const tableExists = await this.checkTableExists(tableName);
|
||||
if (!tableExists) {
|
||||
return {
|
||||
success: false,
|
||||
message: `테이블을 찾을 수 없습니다: ${tableName}`,
|
||||
error: "TABLE_NOT_FOUND",
|
||||
};
|
||||
}
|
||||
|
||||
// 동적 SQL 쿼리 생성
|
||||
let query = `SELECT * FROM "${tableName}"`;
|
||||
const queryParams: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// WHERE 조건 생성
|
||||
const whereConditions: string[] = [];
|
||||
|
||||
// 회사별 필터링 추가
|
||||
if (COMPANY_FILTERED_TABLES.includes(tableName) && userCompany) {
|
||||
// 슈퍼관리자(*)가 아닌 경우에만 회사 필터 적용
|
||||
if (userCompany !== "*") {
|
||||
whereConditions.push(`company_code = $${paramIndex}`);
|
||||
queryParams.push(userCompany);
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정의 필터 추가
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (
|
||||
value &&
|
||||
key !== "limit" &&
|
||||
key !== "offset" &&
|
||||
key !== "orderBy" &&
|
||||
key !== "userLang"
|
||||
) {
|
||||
// 컬럼명 검증 (SQL 인젝션 방지)
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
||||
continue; // 유효하지 않은 컬럼명은 무시
|
||||
}
|
||||
|
||||
whereConditions.push(`"${key}" ILIKE $${paramIndex}`);
|
||||
queryParams.push(`%${value}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// WHERE 절 추가
|
||||
if (whereConditions.length > 0) {
|
||||
query += ` WHERE ${whereConditions.join(" AND ")}`;
|
||||
}
|
||||
|
||||
// ORDER BY 절 추가
|
||||
if (orderBy) {
|
||||
// ORDER BY 검증 (SQL 인젝션 방지)
|
||||
const orderParts = orderBy.split(" ");
|
||||
const columnName = orderParts[0];
|
||||
const direction = orderParts[1]?.toUpperCase();
|
||||
|
||||
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
|
||||
const validDirection = direction === "DESC" ? "DESC" : "ASC";
|
||||
query += ` ORDER BY "${columnName}" ${validDirection}`;
|
||||
}
|
||||
} else {
|
||||
// 기본 정렬: 최신순 (가능한 컬럼 시도)
|
||||
const dateColumns = [
|
||||
"created_date",
|
||||
"regdate",
|
||||
"reg_date",
|
||||
"updated_date",
|
||||
"upd_date",
|
||||
];
|
||||
const tableColumns = await this.getTableColumnsSimple(tableName);
|
||||
const availableDateColumn = dateColumns.find((col) =>
|
||||
tableColumns.some((tableCol) => tableCol.column_name === col)
|
||||
);
|
||||
|
||||
if (availableDateColumn) {
|
||||
query += ` ORDER BY "${availableDateColumn}" DESC`;
|
||||
}
|
||||
}
|
||||
|
||||
// LIMIT과 OFFSET 추가
|
||||
query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
queryParams.push(limit, offset);
|
||||
|
||||
console.log("🔍 실행할 쿼리:", query);
|
||||
console.log("📊 쿼리 파라미터:", queryParams);
|
||||
|
||||
// 쿼리 실행
|
||||
const result = await prisma.$queryRawUnsafe(query, ...queryParams);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result as any[],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`데이터 조회 오류 (${tableName}):`, error);
|
||||
return {
|
||||
success: false,
|
||||
message: "데이터 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회
|
||||
*/
|
||||
async getTableColumns(tableName: string): Promise<ServiceResponse<any[]>> {
|
||||
try {
|
||||
// 테이블명 화이트리스트 검증
|
||||
if (!ALLOWED_TABLES.includes(tableName)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `접근이 허용되지 않은 테이블입니다: ${tableName}`,
|
||||
error: "TABLE_NOT_ALLOWED",
|
||||
};
|
||||
}
|
||||
|
||||
const columns = await this.getTableColumnsSimple(tableName);
|
||||
|
||||
// 컬럼 라벨 정보 추가
|
||||
const columnsWithLabels = await Promise.all(
|
||||
columns.map(async (column) => {
|
||||
const label = await this.getColumnLabel(
|
||||
tableName,
|
||||
column.column_name
|
||||
);
|
||||
return {
|
||||
columnName: column.column_name,
|
||||
columnLabel: label || column.column_name,
|
||||
dataType: column.data_type,
|
||||
isNullable: column.is_nullable === "YES",
|
||||
defaultValue: column.column_default,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: columnsWithLabels,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`컬럼 정보 조회 오류 (${tableName}):`, error);
|
||||
return {
|
||||
success: false,
|
||||
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 존재 여부 확인
|
||||
*/
|
||||
private async checkTableExists(tableName: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = $1
|
||||
);
|
||||
`,
|
||||
tableName
|
||||
);
|
||||
|
||||
return (result as any)[0]?.exists || false;
|
||||
} catch (error) {
|
||||
console.error("테이블 존재 확인 오류:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회 (간단 버전)
|
||||
*/
|
||||
private async getTableColumnsSimple(tableName: string): Promise<any[]> {
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = $1
|
||||
AND table_schema = 'public'
|
||||
ORDER BY ordinal_position;
|
||||
`,
|
||||
tableName
|
||||
);
|
||||
|
||||
return result as any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 라벨 조회
|
||||
*/
|
||||
private async getColumnLabel(
|
||||
tableName: string,
|
||||
columnName: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
// column_labels 테이블에서 라벨 조회
|
||||
const result = await prisma.$queryRawUnsafe(
|
||||
`
|
||||
SELECT label_ko
|
||||
FROM column_labels
|
||||
WHERE table_name = $1 AND column_name = $2
|
||||
LIMIT 1;
|
||||
`,
|
||||
tableName,
|
||||
columnName
|
||||
);
|
||||
|
||||
const labelResult = result as any[];
|
||||
return labelResult[0]?.label_ko || null;
|
||||
} catch (error) {
|
||||
// column_labels 테이블이 없거나 오류가 발생하면 null 반환
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dataService = new DataService();
|
||||
425
backend-node/src/services/layoutService.ts
Normal file
425
backend-node/src/services/layoutService.ts
Normal file
@@ -0,0 +1,425 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
CreateLayoutRequest,
|
||||
UpdateLayoutRequest,
|
||||
LayoutStandard,
|
||||
LayoutType,
|
||||
LayoutCategory,
|
||||
} from "../types/layout";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// JSON 데이터를 안전하게 파싱하는 헬퍼 함수
|
||||
function safeJSONParse(data: any): any {
|
||||
if (data === null || data === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이미 객체인 경우 그대로 반환
|
||||
if (typeof data === "object") {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 문자열인 경우 파싱 시도
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
console.error("JSON 파싱 오류:", error, "Data:", data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// JSON 데이터를 안전하게 문자열화하는 헬퍼 함수
|
||||
function safeJSONStringify(data: any): string | null {
|
||||
if (data === null || data === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 이미 문자열인 경우 그대로 반환
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
}
|
||||
|
||||
// 객체인 경우 문자열로 변환
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
} catch (error) {
|
||||
console.error("JSON 문자열화 오류:", error, "Data:", data);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class LayoutService {
|
||||
/**
|
||||
* 레이아웃 목록 조회
|
||||
*/
|
||||
async getLayouts(params: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
category?: string;
|
||||
layoutType?: string;
|
||||
searchTerm?: string;
|
||||
companyCode: string;
|
||||
includePublic?: boolean;
|
||||
}): Promise<{ data: LayoutStandard[]; total: number }> {
|
||||
const {
|
||||
page = 1,
|
||||
size = 20,
|
||||
category,
|
||||
layoutType,
|
||||
searchTerm,
|
||||
companyCode,
|
||||
includePublic = true,
|
||||
} = params;
|
||||
|
||||
const skip = (page - 1) * size;
|
||||
|
||||
// 검색 조건 구성
|
||||
const where: any = {
|
||||
is_active: "Y",
|
||||
OR: [
|
||||
{ company_code: companyCode },
|
||||
...(includePublic ? [{ is_public: "Y" }] : []),
|
||||
],
|
||||
};
|
||||
|
||||
if (category) {
|
||||
where.category = category;
|
||||
}
|
||||
|
||||
if (layoutType) {
|
||||
where.layout_type = layoutType;
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
where.OR = [
|
||||
...where.OR,
|
||||
{ layout_name: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ layout_name_eng: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ description: { contains: searchTerm, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.layout_standards.findMany({
|
||||
where,
|
||||
skip,
|
||||
take: size,
|
||||
orderBy: [{ sort_order: "asc" }, { created_date: "desc" }],
|
||||
}),
|
||||
prisma.layout_standards.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
data: data.map(
|
||||
(layout) =>
|
||||
({
|
||||
layoutCode: layout.layout_code,
|
||||
layoutName: layout.layout_name,
|
||||
layoutNameEng: layout.layout_name_eng,
|
||||
description: layout.description,
|
||||
layoutType: layout.layout_type as LayoutType,
|
||||
category: layout.category as LayoutCategory,
|
||||
iconName: layout.icon_name,
|
||||
defaultSize: safeJSONParse(layout.default_size),
|
||||
layoutConfig: safeJSONParse(layout.layout_config),
|
||||
zonesConfig: safeJSONParse(layout.zones_config),
|
||||
previewImage: layout.preview_image,
|
||||
sortOrder: layout.sort_order,
|
||||
isActive: layout.is_active,
|
||||
isPublic: layout.is_public,
|
||||
companyCode: layout.company_code,
|
||||
createdDate: layout.created_date,
|
||||
createdBy: layout.created_by,
|
||||
updatedDate: layout.updated_date,
|
||||
updatedBy: layout.updated_by,
|
||||
}) as LayoutStandard
|
||||
),
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 상세 조회
|
||||
*/
|
||||
async getLayoutById(
|
||||
layoutCode: string,
|
||||
companyCode: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
const layout = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!layout) return null;
|
||||
|
||||
return {
|
||||
layoutCode: layout.layout_code,
|
||||
layoutName: layout.layout_name,
|
||||
layoutNameEng: layout.layout_name_eng,
|
||||
description: layout.description,
|
||||
layoutType: layout.layout_type as LayoutType,
|
||||
category: layout.category as LayoutCategory,
|
||||
iconName: layout.icon_name,
|
||||
defaultSize: safeJSONParse(layout.default_size),
|
||||
layoutConfig: safeJSONParse(layout.layout_config),
|
||||
zonesConfig: safeJSONParse(layout.zones_config),
|
||||
previewImage: layout.preview_image,
|
||||
sortOrder: layout.sort_order,
|
||||
isActive: layout.is_active,
|
||||
isPublic: layout.is_public,
|
||||
companyCode: layout.company_code,
|
||||
createdDate: layout.created_date,
|
||||
createdBy: layout.created_by,
|
||||
updatedDate: layout.updated_date,
|
||||
updatedBy: layout.updated_by,
|
||||
} as LayoutStandard;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 생성
|
||||
*/
|
||||
async createLayout(
|
||||
request: CreateLayoutRequest,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<LayoutStandard> {
|
||||
// 레이아웃 코드 생성 (자동)
|
||||
const layoutCode = await this.generateLayoutCode(
|
||||
request.layoutType,
|
||||
companyCode
|
||||
);
|
||||
|
||||
const layout = await prisma.layout_standards.create({
|
||||
data: {
|
||||
layout_code: layoutCode,
|
||||
layout_name: request.layoutName,
|
||||
layout_name_eng: request.layoutNameEng,
|
||||
description: request.description,
|
||||
layout_type: request.layoutType,
|
||||
category: request.category,
|
||||
icon_name: request.iconName,
|
||||
default_size: safeJSONStringify(request.defaultSize) as any,
|
||||
layout_config: safeJSONStringify(request.layoutConfig) as any,
|
||||
zones_config: safeJSONStringify(request.zonesConfig) as any,
|
||||
is_public: request.isPublic ? "Y" : "N",
|
||||
company_code: companyCode,
|
||||
created_by: userId,
|
||||
updated_by: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return this.mapToLayoutStandard(layout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 수정
|
||||
*/
|
||||
async updateLayout(
|
||||
request: UpdateLayoutRequest,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<LayoutStandard | null> {
|
||||
// 수정 권한 확인
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: request.layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 수정 권한이 없습니다.");
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
};
|
||||
|
||||
// 수정할 필드만 업데이트
|
||||
if (request.layoutName !== undefined)
|
||||
updateData.layout_name = request.layoutName;
|
||||
if (request.layoutNameEng !== undefined)
|
||||
updateData.layout_name_eng = request.layoutNameEng;
|
||||
if (request.description !== undefined)
|
||||
updateData.description = request.description;
|
||||
if (request.layoutType !== undefined)
|
||||
updateData.layout_type = request.layoutType;
|
||||
if (request.category !== undefined) updateData.category = request.category;
|
||||
if (request.iconName !== undefined) updateData.icon_name = request.iconName;
|
||||
if (request.defaultSize !== undefined)
|
||||
updateData.default_size = safeJSONStringify(request.defaultSize) as any;
|
||||
if (request.layoutConfig !== undefined)
|
||||
updateData.layout_config = safeJSONStringify(request.layoutConfig) as any;
|
||||
if (request.zonesConfig !== undefined)
|
||||
updateData.zones_config = safeJSONStringify(request.zonesConfig) as any;
|
||||
if (request.isPublic !== undefined)
|
||||
updateData.is_public = request.isPublic ? "Y" : "N";
|
||||
|
||||
const updated = await prisma.layout_standards.update({
|
||||
where: { layout_code: request.layoutCode },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return this.mapToLayoutStandard(updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 삭제 (소프트 삭제)
|
||||
*/
|
||||
async deleteLayout(
|
||||
layoutCode: string,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<boolean> {
|
||||
const existing = await prisma.layout_standards.findFirst({
|
||||
where: {
|
||||
layout_code: layoutCode,
|
||||
company_code: companyCode,
|
||||
is_active: "Y",
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("레이아웃을 찾을 수 없거나 삭제 권한이 없습니다.");
|
||||
}
|
||||
|
||||
await prisma.layout_standards.update({
|
||||
where: { layout_code: layoutCode },
|
||||
data: {
|
||||
is_active: "N",
|
||||
updated_by: userId,
|
||||
updated_date: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 복제
|
||||
*/
|
||||
async duplicateLayout(
|
||||
layoutCode: string,
|
||||
newName: string,
|
||||
companyCode: string,
|
||||
userId: string
|
||||
): Promise<LayoutStandard> {
|
||||
const original = await this.getLayoutById(layoutCode, companyCode);
|
||||
if (!original) {
|
||||
throw new Error("복제할 레이아웃을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
const duplicateRequest: CreateLayoutRequest = {
|
||||
layoutName: newName,
|
||||
layoutNameEng: original.layoutNameEng
|
||||
? `${original.layoutNameEng} Copy`
|
||||
: undefined,
|
||||
description: original.description,
|
||||
layoutType: original.layoutType,
|
||||
category: original.category,
|
||||
iconName: original.iconName,
|
||||
defaultSize: original.defaultSize,
|
||||
layoutConfig: original.layoutConfig,
|
||||
zonesConfig: original.zonesConfig,
|
||||
isPublic: false, // 복제본은 비공개로 시작
|
||||
};
|
||||
|
||||
return this.createLayout(duplicateRequest, companyCode, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카테고리별 레이아웃 개수 조회
|
||||
*/
|
||||
async getLayoutCountsByCategory(
|
||||
companyCode: string
|
||||
): Promise<Record<string, number>> {
|
||||
const counts = await prisma.layout_standards.groupBy({
|
||||
by: ["category"],
|
||||
_count: {
|
||||
layout_code: true,
|
||||
},
|
||||
where: {
|
||||
is_active: "Y",
|
||||
OR: [{ company_code: companyCode }, { is_public: "Y" }],
|
||||
},
|
||||
});
|
||||
|
||||
return counts.reduce(
|
||||
(acc: Record<string, number>, item: any) => {
|
||||
acc[item.category] = item._count.layout_code;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이아웃 코드 자동 생성
|
||||
*/
|
||||
private async generateLayoutCode(
|
||||
layoutType: string,
|
||||
companyCode: string
|
||||
): Promise<string> {
|
||||
const prefix = `${layoutType.toUpperCase()}_${companyCode}`;
|
||||
const existingCodes = await prisma.layout_standards.findMany({
|
||||
where: {
|
||||
layout_code: {
|
||||
startsWith: prefix,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
layout_code: true,
|
||||
},
|
||||
});
|
||||
|
||||
const maxNumber = existingCodes.reduce((max: number, item: any) => {
|
||||
const match = item.layout_code.match(/_(\d+)$/);
|
||||
if (match) {
|
||||
const number = parseInt(match[1], 10);
|
||||
return Math.max(max, number);
|
||||
}
|
||||
return max;
|
||||
}, 0);
|
||||
|
||||
return `${prefix}_${String(maxNumber + 1).padStart(3, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터베이스 모델을 LayoutStandard 타입으로 변환
|
||||
*/
|
||||
private mapToLayoutStandard(layout: any): LayoutStandard {
|
||||
return {
|
||||
layoutCode: layout.layout_code,
|
||||
layoutName: layout.layout_name,
|
||||
layoutNameEng: layout.layout_name_eng,
|
||||
description: layout.description,
|
||||
layoutType: layout.layout_type,
|
||||
category: layout.category,
|
||||
iconName: layout.icon_name,
|
||||
defaultSize: layout.default_size,
|
||||
layoutConfig: layout.layout_config,
|
||||
zonesConfig: layout.zones_config,
|
||||
previewImage: layout.preview_image,
|
||||
sortOrder: layout.sort_order,
|
||||
isActive: layout.is_active,
|
||||
isPublic: layout.is_public,
|
||||
companyCode: layout.company_code,
|
||||
createdDate: layout.created_date,
|
||||
createdBy: layout.created_by,
|
||||
updatedDate: layout.updated_date,
|
||||
updatedBy: layout.updated_by,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const layoutService = new LayoutService();
|
||||
198
backend-node/src/types/layout.ts
Normal file
198
backend-node/src/types/layout.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
// 레이아웃 관련 타입 정의
|
||||
|
||||
// 레이아웃 타입
|
||||
export type LayoutType =
|
||||
| "grid"
|
||||
| "flexbox"
|
||||
| "split"
|
||||
| "card"
|
||||
| "tabs"
|
||||
| "accordion"
|
||||
| "sidebar"
|
||||
| "header-footer"
|
||||
| "three-column"
|
||||
| "dashboard"
|
||||
| "form"
|
||||
| "table"
|
||||
| "custom";
|
||||
|
||||
// 레이아웃 카테고리
|
||||
export type LayoutCategory =
|
||||
| "basic"
|
||||
| "form"
|
||||
| "table"
|
||||
| "dashboard"
|
||||
| "navigation"
|
||||
| "content"
|
||||
| "business";
|
||||
|
||||
// 레이아웃 존 정의
|
||||
export interface LayoutZone {
|
||||
id: string;
|
||||
name: string;
|
||||
position: {
|
||||
row?: number;
|
||||
column?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
};
|
||||
size: {
|
||||
width: number | string;
|
||||
height: number | string;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
};
|
||||
style?: Record<string, any>;
|
||||
allowedComponents?: string[];
|
||||
isResizable?: boolean;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
// 레이아웃 설정
|
||||
export interface LayoutConfig {
|
||||
grid?: {
|
||||
rows: number;
|
||||
columns: number;
|
||||
gap: number;
|
||||
rowGap?: number;
|
||||
columnGap?: number;
|
||||
autoRows?: string;
|
||||
autoColumns?: string;
|
||||
};
|
||||
|
||||
flexbox?: {
|
||||
direction: "row" | "column" | "row-reverse" | "column-reverse";
|
||||
justify:
|
||||
| "flex-start"
|
||||
| "flex-end"
|
||||
| "center"
|
||||
| "space-between"
|
||||
| "space-around"
|
||||
| "space-evenly";
|
||||
align: "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
|
||||
wrap: "nowrap" | "wrap" | "wrap-reverse";
|
||||
gap: number;
|
||||
};
|
||||
|
||||
split?: {
|
||||
direction: "horizontal" | "vertical";
|
||||
ratio: number[];
|
||||
minSize: number[];
|
||||
resizable: boolean;
|
||||
splitterSize: number;
|
||||
};
|
||||
|
||||
tabs?: {
|
||||
position: "top" | "bottom" | "left" | "right";
|
||||
variant: "default" | "pills" | "underline";
|
||||
size: "sm" | "md" | "lg";
|
||||
defaultTab: string;
|
||||
closable: boolean;
|
||||
};
|
||||
|
||||
accordion?: {
|
||||
multiple: boolean;
|
||||
defaultExpanded: string[];
|
||||
collapsible: boolean;
|
||||
};
|
||||
|
||||
sidebar?: {
|
||||
position: "left" | "right";
|
||||
width: number | string;
|
||||
collapsible: boolean;
|
||||
collapsed: boolean;
|
||||
overlay: boolean;
|
||||
};
|
||||
|
||||
headerFooter?: {
|
||||
headerHeight: number | string;
|
||||
footerHeight: number | string;
|
||||
stickyHeader: boolean;
|
||||
stickyFooter: boolean;
|
||||
};
|
||||
|
||||
dashboard?: {
|
||||
columns: number;
|
||||
rowHeight: number;
|
||||
margin: [number, number];
|
||||
padding: [number, number];
|
||||
isDraggable: boolean;
|
||||
isResizable: boolean;
|
||||
};
|
||||
|
||||
custom?: {
|
||||
cssProperties: Record<string, string>;
|
||||
className: string;
|
||||
template: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 레이아웃 표준 정의
|
||||
export interface LayoutStandard {
|
||||
layoutCode: string;
|
||||
layoutName: string;
|
||||
layoutNameEng?: string;
|
||||
description?: string;
|
||||
layoutType: LayoutType;
|
||||
category: LayoutCategory;
|
||||
iconName?: string;
|
||||
defaultSize?: { width: number; height: number };
|
||||
layoutConfig: LayoutConfig;
|
||||
zonesConfig: LayoutZone[];
|
||||
previewImage?: string;
|
||||
sortOrder?: number;
|
||||
isActive?: string;
|
||||
isPublic?: string;
|
||||
companyCode: string;
|
||||
createdDate?: Date;
|
||||
createdBy?: string;
|
||||
updatedDate?: Date;
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
// 레이아웃 생성 요청
|
||||
export interface CreateLayoutRequest {
|
||||
layoutName: string;
|
||||
layoutNameEng?: string;
|
||||
description?: string;
|
||||
layoutType: LayoutType;
|
||||
category: LayoutCategory;
|
||||
iconName?: string;
|
||||
defaultSize?: { width: number; height: number };
|
||||
layoutConfig: LayoutConfig;
|
||||
zonesConfig: LayoutZone[];
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
// 레이아웃 수정 요청
|
||||
export interface UpdateLayoutRequest extends Partial<CreateLayoutRequest> {
|
||||
layoutCode: string;
|
||||
}
|
||||
|
||||
// 레이아웃 목록 조회 요청
|
||||
export interface GetLayoutsRequest {
|
||||
page?: number;
|
||||
size?: number;
|
||||
category?: LayoutCategory;
|
||||
layoutType?: LayoutType;
|
||||
searchTerm?: string;
|
||||
includePublic?: boolean;
|
||||
}
|
||||
|
||||
// 레이아웃 목록 응답
|
||||
export interface GetLayoutsResponse {
|
||||
data: LayoutStandard[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// 레이아웃 복제 요청
|
||||
export interface DuplicateLayoutRequest {
|
||||
layoutCode: string;
|
||||
newName: string;
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Response } from "express";
|
||||
import { logger } from "./logger";
|
||||
|
||||
/**
|
||||
* 에러 처리 유틸리티
|
||||
*/
|
||||
export const handleError = (
|
||||
res: Response,
|
||||
error: any,
|
||||
message: string = "서버 오류가 발생했습니다."
|
||||
) => {
|
||||
logger.error(`Error: ${message}`, error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "SERVER_ERROR",
|
||||
details: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 잘못된 요청 에러 처리
|
||||
*/
|
||||
export const handleBadRequest = (
|
||||
res: Response,
|
||||
message: string = "잘못된 요청입니다."
|
||||
) => {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "BAD_REQUEST",
|
||||
details: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 찾을 수 없음 에러 처리
|
||||
*/
|
||||
export const handleNotFound = (
|
||||
res: Response,
|
||||
message: string = "요청한 리소스를 찾을 수 없습니다."
|
||||
) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "NOT_FOUND",
|
||||
details: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 권한 없음 에러 처리
|
||||
*/
|
||||
export const handleUnauthorized = (
|
||||
res: Response,
|
||||
message: string = "권한이 없습니다."
|
||||
) => {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: "UNAUTHORIZED",
|
||||
details: message,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 유효성 검증 유틸리티
|
||||
*/
|
||||
|
||||
/**
|
||||
* 필수 값 검증
|
||||
*/
|
||||
export const validateRequired = (value: any, fieldName: string): void => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
throw new Error(`${fieldName}은(는) 필수 입력값입니다.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 여러 필수 값 검증
|
||||
*/
|
||||
export const validateRequiredFields = (
|
||||
data: Record<string, any>,
|
||||
requiredFields: string[]
|
||||
): void => {
|
||||
for (const field of requiredFields) {
|
||||
validateRequired(data[field], field);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 문자열 길이 검증
|
||||
*/
|
||||
export const validateStringLength = (
|
||||
value: string,
|
||||
fieldName: string,
|
||||
minLength?: number,
|
||||
maxLength?: number
|
||||
): void => {
|
||||
if (minLength !== undefined && value.length < minLength) {
|
||||
throw new Error(
|
||||
`${fieldName}은(는) 최소 ${minLength}자 이상이어야 합니다.`
|
||||
);
|
||||
}
|
||||
|
||||
if (maxLength !== undefined && value.length > maxLength) {
|
||||
throw new Error(`${fieldName}은(는) 최대 ${maxLength}자 이하여야 합니다.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 이메일 형식 검증
|
||||
*/
|
||||
export const validateEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* 숫자 범위 검증
|
||||
*/
|
||||
export const validateNumberRange = (
|
||||
value: number,
|
||||
fieldName: string,
|
||||
min?: number,
|
||||
max?: number
|
||||
): void => {
|
||||
if (min !== undefined && value < min) {
|
||||
throw new Error(`${fieldName}은(는) ${min} 이상이어야 합니다.`);
|
||||
}
|
||||
|
||||
if (max !== undefined && value > max) {
|
||||
throw new Error(`${fieldName}은(는) ${max} 이하여야 합니다.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 배열이 비어있지 않은지 검증
|
||||
*/
|
||||
export const validateNonEmptyArray = (
|
||||
array: any[],
|
||||
fieldName: string
|
||||
): void => {
|
||||
if (!Array.isArray(array) || array.length === 0) {
|
||||
throw new Error(`${fieldName}은(는) 비어있을 수 없습니다.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 필수 필드 검증 후 누락된 필드 목록 반환
|
||||
*/
|
||||
export const checkMissingFields = (
|
||||
data: Record<string, any>,
|
||||
requiredFields: string[]
|
||||
): string[] => {
|
||||
const missingFields: string[] = [];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
const value = data[field];
|
||||
if (value === null || value === undefined || value === "") {
|
||||
missingFields.push(field);
|
||||
}
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
};
|
||||
4
backend-node/uploads/company_COMPANY_2/README.txt
Normal file
4
backend-node/uploads/company_COMPANY_2/README.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
회사 코드: COMPANY_2
|
||||
생성일: 2025-09-11T02:07:40.033Z
|
||||
폴더 구조: YYYY/MM/DD/파일명
|
||||
관리자: 시스템 자동 생성
|
||||
4
backend-node/uploads/company_COMPANY_3/README.txt
Normal file
4
backend-node/uploads/company_COMPANY_3/README.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
회사 코드: COMPANY_3
|
||||
생성일: 2025-09-11T02:08:06.303Z
|
||||
폴더 구조: YYYY/MM/DD/파일명
|
||||
관리자: 시스템 자동 생성
|
||||
Reference in New Issue
Block a user