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

This commit is contained in:
hyeonsu 2025-09-04 14:59:08 +09:00
commit 30b56b1acf
18 changed files with 2019 additions and 246 deletions

View File

@ -8,7 +8,17 @@ datasource db {
url = env("DATABASE_URL")
}
// 테이블 타입관리 관련 모델은 이미 정의되어 있음 (line 11, 717)
model dynamic_form_data {
id Int @id @default(autoincrement())
screen_id Int
table_name String @db.VarChar(100)
form_data Json
created_at DateTime? @default(now()) @db.Timestamp(6)
updated_at DateTime? @default(now()) @updatedAt @db.Timestamp(6)
created_by String @db.VarChar(50)
updated_by String @db.VarChar(50)
company_code String @db.VarChar(20)
}
model admin_supply_mng {
objid Decimal @id @default(0) @db.Decimal
@ -74,15 +84,12 @@ model approval {
@@ignore
}
model approval_kind {
target_type String @db.VarChar
target_type String @id @db.VarChar
target_name String? @db.VarChar
regdate DateTime? @db.Timestamp(6)
status String? @db.VarChar
@@id([target_type])
@@index([status])
}
@ -135,8 +142,6 @@ model arrival_plan {
@@index([part_objid])
}
model as_mng {
objid Int @id
as_no String? @db.VarChar
@ -247,20 +252,13 @@ model attach_file_info {
@@ignore
}
model authority_master {
objid Decimal @id @default(0) @db.Decimal
auth_name String? @default("NULL::character varying") @db.VarChar(256)
auth_code String? @default("NULL::character varying") @db.VarChar(64)
writer String? @default("NULL::character varying") @db.VarChar(32)
regdate DateTime? @db.Timestamp(6)
status String? @default("NULL::character varying") @db.VarChar(32)
// 관계 설정
objid Decimal @id @default(0) @db.Decimal
auth_name String? @default("NULL::character varying") @db.VarChar(256)
auth_code String? @default("NULL::character varying") @db.VarChar(64)
writer String? @default("NULL::character varying") @db.VarChar(32)
regdate DateTime? @db.Timestamp(6)
status String? @default("NULL::character varying") @db.VarChar(32)
sub_users authority_sub_user[]
}
@ -277,15 +275,12 @@ model authority_master_history {
}
model authority_sub_user {
objid Decimal @id @default(0) @db.Decimal
master_objid Decimal? @db.Decimal
user_id String? @default("NULL::character varying") @db.VarChar(64)
writer String? @default("NULL::character varying") @db.VarChar(64)
regdate DateTime? @db.Timestamp(6)
// 관계 설정
objid Decimal @id @default(0) @db.Decimal
master_objid Decimal? @db.Decimal
user_id String? @default("NULL::character varying") @db.VarChar(64)
writer String? @default("NULL::character varying") @db.VarChar(64)
regdate DateTime? @db.Timestamp(6)
authority_master authority_master? @relation(fields: [master_objid], references: [objid])
user user_info? @relation(fields: [user_id], references: [user_id])
@@index([master_objid])
@@index([user_id])
@ -343,13 +338,6 @@ model bom_part_qty {
@@index([parent_objid])
}
model car_distribute_member {
objid Decimal @db.Decimal
car_objid Decimal @db.Decimal
@ -439,6 +427,7 @@ model column_labels {
column_name String? @db.VarChar(100)
column_label String? @db.VarChar(200)
web_type String? @db.VarChar(50)
input_type String? @default("direct") @db.VarChar(20) // direct, auto
detail_settings String?
description String?
display_order Int? @default(0)
@ -530,9 +519,10 @@ model company_mng {
regdate DateTime? @db.Timestamp(6)
status String? @db.VarChar(32)
// 관계
// 관계 정
menus menu_info[]
}
model contract_mgmt {
objid String @id @db.VarChar
category_cd String? @db.VarChar
@ -599,8 +589,6 @@ model contract_mgmt_option {
@@index([option_objid])
}
model counselingmgmt {
objid String @id @db.VarChar
reg_date String? @db.VarChar
@ -625,8 +613,6 @@ model counselingmgmt {
parent_seq String? @db.VarChar
}
model customer_service_mgmt {
objid String @id @db.VarChar
service_no String? @db.VarChar
@ -1181,8 +1167,6 @@ model inventory_mgmt_in {
@@index([parent_objid])
}
model inventory_mgmt_out {
objid String @id @db.VarChar
parent_objid String? @db.VarChar
@ -1262,6 +1246,7 @@ model invoice_mgmt {
discount_percentage String? @db.VarChar
inv_discount_price String? @db.VarChar
}
model invoice_mgmt_part {
objid String @id @db.VarChar
invoice_objid String? @db.VarChar
@ -1506,7 +1491,7 @@ model menu_info {
lang_key String? @db.VarChar(100)
lang_key_desc String? @db.VarChar(100)
// 관계 설정 (나중에 활용 가능)
// 관계 정의
company company_mng? @relation(fields: [company_code], references: [company_code])
@@index([parent_obj_id])
@ -1537,8 +1522,6 @@ model mold_dev_request_info {
status String? @db.VarChar(50)
}
model multi_lang_key_master {
key_id Int @id @default(autoincrement())
company_code String @default("*") @db.VarChar(20)
@ -1801,7 +1784,6 @@ model order_spec_mng_history {
@@ignore
}
model part_bom_qty {
bom_report_objid Decimal @db.Decimal
objid Decimal @id @db.Decimal
@ -1837,9 +1819,6 @@ model part_bom_report {
@@index([unit_code, contract_objid], map: "part_bom_report_unit_code_idx")
}
model part_distribution_list {
part_objid Decimal @db.Decimal
product_mgmt_objid String? @db.VarChar(100)
@ -2021,9 +2000,6 @@ model part_mng_history {
@@ignore
}
model planning_issue {
objid String @id @db.VarChar
issue_no String @db.VarChar
@ -2221,10 +2197,6 @@ model pms_wbs_task {
@@ignore
}
model pms_wbs_task_confirm {
objid Decimal? @db.Decimal
target_objid Decimal? @db.Decimal
@ -2349,6 +2321,7 @@ model product_group_mng {
@@ignore
}
model product_kind_spec {
objid String @db.VarChar
objid_parent String @db.VarChar
@ -2798,13 +2771,6 @@ model purchase_order_master {
@@index([multi_master_objid])
}
model purchase_order_master_241216 {
objid String? @db.VarChar
purchase_order_no String? @db.VarChar
@ -2920,13 +2886,6 @@ model purchase_order_part {
@@index([purchase_order_master_objid])
}
model ratecal_mgmt {
ratecal_mgmt_objid Decimal @default(0) @db.Decimal
position String? @db.VarChar(100)
@ -2944,8 +2903,6 @@ model ratecal_mgmt {
@@ignore
}
model receive_history {
objid String @id @db.VarChar
part_objid String @db.VarChar
@ -3071,7 +3028,6 @@ model route {
@@ignore
}
model sales_bom_part_qty {
sales_bom_objid String @db.VarChar
objid String @id @db.VarChar
@ -3106,7 +3062,6 @@ model sales_bom_part_qty {
sales_part_code String? @db.VarChar
}
model sales_bom_report {
objid String @id @default("") @db.VarChar
parent_objid String? @unique(map: "sales_bom_report_parent_objid_idx") @db.VarChar
@ -3152,7 +3107,6 @@ model sales_bom_report_part {
@@index([parent_objid])
}
model sales_bom_report_part_241218 {
objid String? @db.VarChar
parent_objid String? @db.VarChar
@ -3193,8 +3147,6 @@ model sales_long_delivery {
price String? @default("0") @db.VarChar
}
model sales_long_delivery_input {
objid String @id(map: "sales_long_delivery_plan_pkey") @db.VarChar
parent_objid String? @db.VarChar
@ -3205,8 +3157,6 @@ model sales_long_delivery_input {
admin_editor String? @db.VarChar
}
model sales_long_delivery_predict {
objid String @id @db.VarChar
parent_objid String? @db.VarChar
@ -3249,7 +3199,6 @@ model sales_request_master {
remark String? @db.VarChar
}
model sales_request_part {
objid String @id @db.VarChar
sales_bom_qty_objid String? @db.VarChar
@ -3309,7 +3258,7 @@ model setup_wbs_task {
}
model setup_wbs_task_standard {
objid String? @unique(map: "setup_wbs_task_standard_objid_key") @db.VarChar
objid String? @unique @db.VarChar
contract_objid String? @db.VarChar
parent_objid String? @db.VarChar
task_category String? @db.VarChar
@ -3808,6 +3757,7 @@ model swpc120a_tbl {
@@id([imitemid, suvndcd], map: "pk_swpc120a_tbl")
}
model swpc130a_tbl {
imitemid String @db.VarChar(15)
suvndcd String @db.VarChar(5)
@ -4566,6 +4516,7 @@ model swsb500a_tbl {
edit_date DateTime? @db.Timestamp(6)
edit_emp String? @db.VarChar(30)
}
model swsb510a_tbl {
frw_req_no String @id(map: "pk_swsb510a_tbl") @db.VarChar(10)
req_date String? @db.VarChar(8)
@ -4874,15 +4825,6 @@ model table_labels {
column_labels column_labels[]
}
model template_mng {
objid Int @id
template_code String? @db.VarChar
@ -4893,12 +4835,6 @@ model template_mng {
template_code_detail String? @db.VarChar
}
model time_sheet {
objid Decimal @default(0) @db.Decimal
project_mgmt_objid Decimal? @db.Decimal
@ -4971,7 +4907,6 @@ model user_info {
user_type String? @db.VarChar(1024)
user_type_name String? @db.VarChar(1024)
regdate DateTime? @db.Timestamp(6)
data_type String? @db.VarChar(64)
status String? @db.VarChar(32)
end_date DateTime? @db.Timestamp(6)
fax_no String? @db.VarChar
@ -4980,9 +4915,7 @@ model user_info {
photo Bytes?
locale String? @db.VarChar
company_code String? @db.VarChar(50)
// 관계 설정
authorities authority_sub_user[]
data_type String? @db.VarChar(64)
}
model user_info_history {
@ -5051,44 +4984,38 @@ model zz_230410_user_info {
@@ignore
}
// 화면관리 시스템 Prisma 스키마
// 기존 schema.prisma에 추가할 모델들
model screen_definitions {
screen_id Int @id @default(autoincrement())
screen_name String @db.VarChar(100)
screen_code String @unique @db.VarChar(50)
table_name String @db.VarChar(100)
company_code String @db.VarChar(50)
description String? @db.Text
is_active String @default("Y") @db.Char(1)
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)
// 관계
layouts screen_layouts[]
screen_id Int @id @default(autoincrement())
screen_name String @db.VarChar(100)
screen_code String @unique @db.VarChar(50)
table_name String @db.VarChar(100)
company_code String @db.VarChar(50)
description String?
is_active String @default("Y") @db.Char(1)
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)
layouts screen_layouts[]
menu_assignments screen_menu_assignments[]
@@index([company_code])
}
model screen_layouts {
layout_id Int @id @default(autoincrement())
layout_id Int @id @default(autoincrement())
screen_id Int
component_type String @db.VarChar(50)
component_id String @unique @db.VarChar(100)
parent_id String? @db.VarChar(100)
component_type String @db.VarChar(50)
component_id String @unique @db.VarChar(100)
parent_id String? @db.VarChar(100)
position_x Int
position_y Int
width Int
height Int
properties Json?
display_order Int @default(0)
created_date DateTime @default(now()) @db.Timestamp(6)
// 관계
display_order Int @default(0)
created_date DateTime @default(now()) @db.Timestamp(6)
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
widgets screen_widgets[]
@ -5096,99 +5023,82 @@ model screen_layouts {
}
model screen_widgets {
widget_id Int @id @default(autoincrement())
layout_id Int
table_name String @db.VarChar(100)
column_name String @db.VarChar(100)
widget_type String @db.VarChar(50)
label String? @db.VarChar(200)
placeholder String? @db.VarChar(200)
is_required Boolean @default(false)
is_readonly Boolean @default(false)
validation_rules Json?
display_properties Json?
created_date DateTime @default(now()) @db.Timestamp(6)
// 관계
layout screen_layouts @relation(fields: [layout_id], references: [layout_id], onDelete: Cascade)
widget_id Int @id @default(autoincrement())
layout_id Int
table_name String @db.VarChar(100)
column_name String @db.VarChar(100)
widget_type String @db.VarChar(50)
label String? @db.VarChar(200)
placeholder String? @db.VarChar(200)
is_required Boolean @default(false)
is_readonly Boolean @default(false)
validation_rules Json?
display_properties Json?
created_date DateTime @default(now()) @db.Timestamp(6)
layout screen_layouts @relation(fields: [layout_id], references: [layout_id], onDelete: Cascade)
@@index([layout_id])
}
model screen_templates {
template_id Int @id @default(autoincrement())
template_name String @db.VarChar(100)
template_type String @db.VarChar(50)
company_code String @db.VarChar(50)
description String? @db.Text
layout_data Json?
is_public Boolean @default(false)
created_by String? @db.VarChar(50)
created_date DateTime @default(now()) @db.Timestamp(6)
template_id Int @id @default(autoincrement())
template_name String @db.VarChar(100)
template_type String @db.VarChar(50)
company_code String @db.VarChar(50)
description String?
layout_data Json?
is_public Boolean @default(false)
created_by String? @db.VarChar(50)
created_date DateTime @default(now()) @db.Timestamp(6)
@@index([company_code])
}
model screen_menu_assignments {
assignment_id Int @id @default(autoincrement())
screen_id Int
menu_objid Decimal @db.Decimal
company_code String @db.VarChar(50)
display_order Int @default(0)
is_active String @default("Y") @db.Char(1)
created_date DateTime @default(now()) @db.Timestamp(6)
created_by String? @db.VarChar(50)
// 관계
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
assignment_id Int @id @default(autoincrement())
screen_id Int
menu_objid Decimal @db.Decimal
company_code String @db.VarChar(50)
display_order Int @default(0)
is_active String @default("Y") @db.Char(1)
created_date DateTime @default(now()) @db.Timestamp(6)
created_by String? @db.VarChar(50)
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
@@unique([screen_id, menu_objid, company_code])
@@index([company_code])
}
// =====================================================
// 공통코드 관리 시스템 모델
// =====================================================
/// 공통코드 카테고리 테이블
model code_category {
category_code String @id @db.VarChar(50)
category_name String @db.VarChar(100)
category_name_eng String? @db.VarChar(100)
description String? @db.Text
sort_order Int @default(0)
is_active String @default("Y") @db.Char(1)
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)
// 관계 - 코드 상세 정보
codes code_info[]
@@index([is_active])
@@index([sort_order])
category_code String @id @db.VarChar(50)
category_name String @db.VarChar(100)
category_name_eng String? @db.VarChar(100)
description String?
sort_order Int? @default(0)
is_active String? @default("Y") @db.Char(1)
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)
codes code_info[]
}
/// 공통코드 상세 정보 테이블
model code_info {
code_category String @db.VarChar(50)
code_value String @db.VarChar(50)
code_name String @db.VarChar(100)
code_name_eng String? @db.VarChar(100)
description String? @db.Text
sort_order Int @default(0)
is_active String @default("Y") @db.Char(1)
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)
code_category String @db.VarChar(50)
code_value String @db.VarChar(50)
code_name String @db.VarChar(100)
code_name_eng String? @db.VarChar(100)
description String?
sort_order Int? @default(0)
is_active String? @default("Y") @db.Char(1)
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)
category code_category @relation(fields: [code_category], references: [category_code], onDelete: Cascade, map: "fk_code_info_category")
// 관계 - 코드 카테고리
category code_category @relation(fields: [code_category], references: [category_code], onDelete: Cascade, onUpdate: Cascade)
@@id([code_category, code_value])
@@index([code_category])
@@index([is_active])
@@index([code_category, sort_order])
@@id([code_category, code_value], map: "pk_code_info")
@@index([code_category, sort_order], map: "idx_code_info_sort")
}

View File

@ -15,6 +15,7 @@ import multilangRoutes from "./routes/multilangRoutes";
import tableManagementRoutes from "./routes/tableManagementRoutes";
import screenManagementRoutes from "./routes/screenManagementRoutes";
import commonCodeRoutes from "./routes/commonCodeRoutes";
import dynamicFormRoutes from "./routes/dynamicFormRoutes";
// import userRoutes from './routes/userRoutes';
// import menuRoutes from './routes/menuRoutes';
@ -67,6 +68,7 @@ app.use("/api/multilang", multilangRoutes);
app.use("/api/table-management", tableManagementRoutes);
app.use("/api/screen-management", screenManagementRoutes);
app.use("/api/common-codes", commonCodeRoutes);
app.use("/api/dynamic-form", dynamicFormRoutes);
// app.use('/api/users', userRoutes);
// app.use('/api/menus', menuRoutes);

View File

@ -0,0 +1,310 @@
import { Response } from "express";
import { dynamicFormService } from "../services/dynamicFormService";
import { AuthenticatedRequest } from "../types/auth";
// 폼 데이터 저장
export const saveFormData = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { companyCode, userId } = req.user as any;
const { screenId, tableName, data } = req.body;
console.log("💾 폼 데이터 저장 요청:", {
userId,
companyCode,
screenId,
tableName,
data,
});
// 필수 필드 검증
if (!screenId || !tableName || !data) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (screenId, tableName, data)",
});
}
// 메타데이터 추가 (사용자가 입력한 경우에만 company_code 추가)
const formDataWithMeta = {
...data,
created_by: userId,
updated_by: userId,
screen_id: screenId,
};
// company_code는 사용자가 명시적으로 입력한 경우에만 추가
if (data.company_code !== undefined) {
formDataWithMeta.company_code = data.company_code;
} else if (companyCode && companyCode !== "*") {
// 기본 company_code가 '*'가 아닌 경우에만 추가
formDataWithMeta.company_code = companyCode;
}
const result = await dynamicFormService.saveFormData(
screenId,
tableName,
formDataWithMeta
);
console.log("✅ 폼 데이터 저장 성공:", result);
res.json({
success: true,
data: result,
message: "데이터가 성공적으로 저장되었습니다.",
});
} catch (error: any) {
console.error("❌ 폼 데이터 저장 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 저장에 실패했습니다.",
});
}
};
// 폼 데이터 업데이트
export const updateFormData = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { id } = req.params;
const { companyCode, userId } = req.user as any;
const { tableName, data } = req.body;
console.log("🔄 폼 데이터 업데이트 요청:", {
id,
userId,
companyCode,
tableName,
data,
});
if (!tableName || !data) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (tableName, data)",
});
}
// 메타데이터 추가
const formDataWithMeta = {
...data,
updated_by: userId,
updated_at: new Date(),
};
const result = await dynamicFormService.updateFormData(
parseInt(id),
tableName,
formDataWithMeta
);
console.log("✅ 폼 데이터 업데이트 성공:", result);
res.json({
success: true,
data: result,
message: "데이터가 성공적으로 업데이트되었습니다.",
});
} catch (error: any) {
console.error("❌ 폼 데이터 업데이트 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 업데이트에 실패했습니다.",
});
}
};
// 폼 데이터 삭제
export const deleteFormData = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { id } = req.params;
const { companyCode } = req.user as any;
const { tableName } = req.body;
console.log("🗑️ 폼 데이터 삭제 요청:", { id, companyCode, tableName });
if (!tableName) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (tableName)",
});
}
await dynamicFormService.deleteFormData(parseInt(id), tableName);
console.log("✅ 폼 데이터 삭제 성공");
res.json({
success: true,
message: "데이터가 성공적으로 삭제되었습니다.",
});
} catch (error: any) {
console.error("❌ 폼 데이터 삭제 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 삭제에 실패했습니다.",
});
}
};
// 단일 폼 데이터 조회
export const getFormData = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { id } = req.params;
const { companyCode } = req.user as any;
console.log("📄 폼 데이터 단건 조회 요청:", { id, companyCode });
const data = await dynamicFormService.getFormData(parseInt(id));
if (!data) {
return res.status(404).json({
success: false,
message: "데이터를 찾을 수 없습니다.",
});
}
console.log("✅ 폼 데이터 단건 조회 성공");
res.json({
success: true,
data: data,
});
} catch (error: any) {
console.error("❌ 폼 데이터 단건 조회 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 조회에 실패했습니다.",
});
}
};
// 화면별 폼 데이터 목록 조회
export const getFormDataList = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { screenId } = req.params;
const { companyCode } = req.user as any;
const {
page = 1,
size = 10,
search = "",
sortBy = "created_at",
sortOrder = "desc",
} = req.query;
console.log("📋 폼 데이터 목록 조회 요청:", {
screenId,
companyCode,
page,
size,
search,
sortBy,
sortOrder,
});
const result = await dynamicFormService.getFormDataList(
parseInt(screenId as string),
{
page: parseInt(page as string),
size: parseInt(size as string),
search: search as string,
sortBy: sortBy as string,
sortOrder: sortOrder as "asc" | "desc",
}
);
console.log("✅ 폼 데이터 목록 조회 성공");
res.json({
success: true,
data: result,
});
} catch (error: any) {
console.error("❌ 폼 데이터 목록 조회 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 조회에 실패했습니다.",
});
}
};
// 폼 데이터 검증
export const validateFormData = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { tableName, data } = req.body;
console.log("✅ 폼 데이터 검증 요청:", { tableName, data });
if (!tableName || !data) {
return res.status(400).json({
success: false,
message: "필수 필드가 누락되었습니다. (tableName, data)",
});
}
const validationResult = await dynamicFormService.validateFormData(
tableName,
data
);
console.log("✅ 폼 데이터 검증 성공:", validationResult);
res.json({
success: true,
data: validationResult,
});
} catch (error: any) {
console.error("❌ 폼 데이터 검증 실패:", error);
res.status(500).json({
success: false,
message: error.message || "데이터 검증에 실패했습니다.",
});
}
};
// 테이블 컬럼 정보 조회 (검증용)
export const getTableColumns = async (
req: AuthenticatedRequest,
res: Response
): Promise<Response | void> => {
try {
const { tableName } = req.params;
console.log("📊 테이블 컬럼 정보 조회 요청:", { tableName });
const columns = await dynamicFormService.getTableColumns(tableName);
console.log("✅ 테이블 컬럼 정보 조회 성공");
res.json({
success: true,
data: {
tableName,
columns,
},
});
} catch (error: any) {
console.error("❌ 테이블 컬럼 정보 조회 실패:", error);
res.status(500).json({
success: false,
message: error.message || "테이블 정보 조회에 실패했습니다.",
});
}
};

View File

@ -386,7 +386,7 @@ export async function updateColumnWebType(
): Promise<void> {
try {
const { tableName, columnName } = req.params;
const { webType, detailSettings } = req.body;
const { webType, detailSettings, inputType } = req.body;
logger.info(
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
@ -410,7 +410,8 @@ export async function updateColumnWebType(
tableName,
columnName,
webType,
detailSettings
detailSettings,
inputType
);
logger.info(

View File

@ -0,0 +1,33 @@
import express from "express";
import { authenticateToken } from "../middleware/authMiddleware";
import {
saveFormData,
updateFormData,
deleteFormData,
getFormData,
getFormDataList,
validateFormData,
getTableColumns,
} from "../controllers/dynamicFormController";
const router = express.Router();
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
// 폼 데이터 CRUD
router.post("/save", saveFormData);
router.put("/:id", updateFormData);
router.delete("/:id", deleteFormData);
router.get("/:id", getFormData);
// 화면별 폼 데이터 목록 조회
router.get("/screen/:screenId", getFormDataList);
// 폼 데이터 검증
router.post("/validate", validateFormData);
// 테이블 컬럼 정보 조회 (검증용)
router.get("/table/:tableName/columns", getTableColumns);
export default router;

View File

@ -0,0 +1,573 @@
import prisma from "../config/database";
import { Prisma } from "@prisma/client";
export interface FormDataResult {
id: number;
screenId: number;
tableName: string;
data: Record<string, any>;
createdAt: Date | null;
updatedAt: Date | null;
createdBy: string;
updatedBy: string;
}
export interface PaginatedFormData {
content: FormDataResult[];
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}
export interface ValidationError {
field: string;
message: string;
code: string;
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
}
export interface TableColumn {
columnName: string;
dataType: string;
nullable: boolean;
primaryKey: boolean;
maxLength?: number;
defaultValue?: any;
}
export class DynamicFormService {
/**
* ( )
*/
private async getTableColumnNames(tableName: string): Promise<string[]> {
try {
const result = (await prisma.$queryRawUnsafe(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = '${tableName}'
AND table_schema = 'public'
`)) as any[];
return result.map((row) => row.column_name);
} catch (error) {
console.error(`❌ 테이블 ${tableName} 컬럼 정보 조회 실패:`, error);
return [];
}
}
/**
* Primary Key
*/
private async getTablePrimaryKeys(tableName: string): Promise<string[]> {
try {
const result = (await prisma.$queryRawUnsafe(`
SELECT kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = '${tableName}'
AND tc.constraint_type = 'PRIMARY KEY'
AND tc.table_schema = 'public'
`)) as any[];
return result.map((row) => row.column_name);
} catch (error) {
console.error(`❌ 테이블 ${tableName} Primary Key 조회 실패:`, error);
return [];
}
}
/**
* ( )
*/
async saveFormData(
screenId: number,
tableName: string,
data: Record<string, any>
): Promise<FormDataResult> {
try {
console.log("💾 서비스: 실제 테이블에 폼 데이터 저장 시작:", {
screenId,
tableName,
data,
});
// 테이블의 실제 컬럼 정보와 Primary Key 조회
const tableColumns = await this.getTableColumnNames(tableName);
const primaryKeys = await this.getTablePrimaryKeys(tableName);
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys);
// 메타데이터 제거 (실제 테이블 컬럼이 아님)
const { created_by, updated_by, company_code, screen_id, ...actualData } =
data;
// 기본 데이터 준비
const dataToInsert: any = { ...actualData };
// 테이블에 존재하는 공통 필드들만 추가
if (tableColumns.includes("created_at")) {
dataToInsert.created_at = new Date();
}
if (tableColumns.includes("updated_at")) {
dataToInsert.updated_at = new Date();
}
if (tableColumns.includes("regdate") && !dataToInsert.regdate) {
dataToInsert.regdate = new Date();
}
// 생성자/수정자 정보가 있고 해당 컬럼이 존재한다면 추가
if (created_by && tableColumns.includes("created_by")) {
dataToInsert.created_by = created_by;
}
if (updated_by && tableColumns.includes("updated_by")) {
dataToInsert.updated_by = updated_by;
}
if (company_code && tableColumns.includes("company_code")) {
dataToInsert.company_code = company_code;
}
// 존재하지 않는 컬럼 제거
Object.keys(dataToInsert).forEach((key) => {
if (!tableColumns.includes(key)) {
console.log(
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨`
);
delete dataToInsert[key];
}
});
console.log("🎯 실제 테이블에 삽입할 데이터:", {
tableName,
dataToInsert,
});
// 동적 SQL을 사용하여 실제 테이블에 UPSERT
const columns = Object.keys(dataToInsert);
const values: any[] = Object.values(dataToInsert);
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
let upsertQuery: string;
if (primaryKeys.length > 0) {
// Primary Key가 있는 경우 UPSERT 사용
const conflictColumns = primaryKeys.join(", ");
const updateSet = columns
.filter((col) => !primaryKeys.includes(col)) // Primary Key는 UPDATE에서 제외
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
if (updateSet) {
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${conflictColumns})
DO UPDATE SET ${updateSet}
RETURNING *
`;
} else {
// 업데이트할 컬럼이 없는 경우 (Primary Key만 있는 테이블)
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${conflictColumns})
DO NOTHING
RETURNING *
`;
}
} else {
// Primary Key가 없는 경우 일반 INSERT
upsertQuery = `
INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
RETURNING *
`;
}
console.log("📝 실행할 UPSERT SQL:", upsertQuery);
console.log("📊 SQL 파라미터:", values);
const result = await prisma.$queryRawUnsafe(upsertQuery, ...values);
console.log("✅ 서비스: 실제 테이블 저장 성공:", result);
// 결과를 표준 형식으로 변환
const insertedRecord = Array.isArray(result) ? result[0] : result;
return {
id: insertedRecord.id || insertedRecord.objid || 0,
screenId: screenId,
tableName: tableName,
data: insertedRecord as Record<string, any>,
createdAt: insertedRecord.created_at || new Date(),
updatedAt: insertedRecord.updated_at || new Date(),
createdBy: insertedRecord.created_by || created_by || "system",
updatedBy: insertedRecord.updated_by || updated_by || "system",
};
} catch (error) {
console.error("❌ 서비스: 실제 테이블 저장 실패:", error);
throw new Error(`실제 테이블 저장 실패: ${error}`);
}
}
/**
* ( )
*/
async updateFormData(
id: number,
tableName: string,
data: Record<string, any>
): Promise<FormDataResult> {
try {
console.log("🔄 서비스: 실제 테이블에서 폼 데이터 업데이트 시작:", {
id,
tableName,
data,
});
// 테이블의 실제 컬럼 정보 조회
const tableColumns = await this.getTableColumnNames(tableName);
console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns);
// 메타데이터 제거
const { created_by, updated_by, company_code, screen_id, ...actualData } =
data;
// 기본 데이터 준비
const dataToUpdate: any = { ...actualData };
// 테이블에 존재하는 업데이트 관련 필드들만 추가
if (tableColumns.includes("updated_at")) {
dataToUpdate.updated_at = new Date();
}
if (tableColumns.includes("regdate") && !dataToUpdate.regdate) {
dataToUpdate.regdate = new Date();
}
// 수정자 정보가 있고 해당 컬럼이 존재한다면 추가
if (updated_by && tableColumns.includes("updated_by")) {
dataToUpdate.updated_by = updated_by;
}
// 존재하지 않는 컬럼 제거
Object.keys(dataToUpdate).forEach((key) => {
if (!tableColumns.includes(key)) {
console.log(
`⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨`
);
delete dataToUpdate[key];
}
});
console.log("🎯 실제 테이블에서 업데이트할 데이터:", {
tableName,
id,
dataToUpdate,
});
// 동적 UPDATE SQL 생성
const setClause = Object.keys(dataToUpdate)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const values: any[] = Object.values(dataToUpdate);
values.push(id); // WHERE 조건용 ID 추가
// ID 또는 objid로 찾기 시도
const updateQuery = `
UPDATE ${tableName}
SET ${setClause}
WHERE (id = $${values.length} OR objid = $${values.length})
RETURNING *
`;
console.log("📝 실행할 UPDATE SQL:", updateQuery);
console.log("📊 SQL 파라미터:", values);
const result = await prisma.$queryRawUnsafe(updateQuery, ...values);
console.log("✅ 서비스: 실제 테이블 업데이트 성공:", result);
const updatedRecord = Array.isArray(result) ? result[0] : result;
return {
id: updatedRecord.id || updatedRecord.objid || id,
screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정
tableName: tableName,
data: updatedRecord as Record<string, any>,
createdAt: updatedRecord.created_at || new Date(),
updatedAt: updatedRecord.updated_at || new Date(),
createdBy: updatedRecord.created_by || "system",
updatedBy: updatedRecord.updated_by || updated_by || "system",
};
} catch (error) {
console.error("❌ 서비스: 실제 테이블 업데이트 실패:", error);
throw new Error(`실제 테이블 업데이트 실패: ${error}`);
}
}
/**
* ( )
*/
async deleteFormData(id: number, tableName: string): Promise<void> {
try {
console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", {
id,
tableName,
});
// 동적 DELETE SQL 생성
const deleteQuery = `
DELETE FROM ${tableName}
WHERE (id = $1 OR objid = $1)
RETURNING *
`;
console.log("📝 실행할 DELETE SQL:", deleteQuery);
console.log("📊 SQL 파라미터:", [id]);
const result = await prisma.$queryRawUnsafe(deleteQuery, id);
console.log("✅ 서비스: 실제 테이블 삭제 성공:", result);
} catch (error) {
console.error("❌ 서비스: 실제 테이블 삭제 실패:", error);
throw new Error(`실제 테이블 삭제 실패: ${error}`);
}
}
/**
*
*/
async getFormData(id: number): Promise<FormDataResult | null> {
try {
console.log("📄 서비스: 폼 데이터 단건 조회 시작:", { id });
const result = await prisma.dynamic_form_data.findUnique({
where: { id },
});
if (!result) {
console.log("❌ 서비스: 폼 데이터를 찾을 수 없음");
return null;
}
console.log("✅ 서비스: 폼 데이터 단건 조회 성공");
return {
id: result.id,
screenId: result.screen_id,
tableName: result.table_name,
data: result.form_data as Record<string, any>,
createdAt: result.created_at,
updatedAt: result.updated_at,
createdBy: result.created_by,
updatedBy: result.updated_by,
};
} catch (error) {
console.error("❌ 서비스: 폼 데이터 단건 조회 실패:", error);
throw new Error(`폼 데이터 조회 실패: ${error}`);
}
}
/**
* ()
*/
async getFormDataList(
screenId: number,
params: {
page: number;
size: number;
search?: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
}
): Promise<PaginatedFormData> {
try {
console.log("📋 서비스: 폼 데이터 목록 조회 시작:", { screenId, params });
const {
page,
size,
search,
sortBy = "created_at",
sortOrder = "desc",
} = params;
const skip = (page - 1) * size;
// 검색 조건 구성
const where: Prisma.dynamic_form_dataWhereInput = {
screen_id: screenId,
};
// 검색어가 있는 경우 form_data 필드에서 검색
if (search) {
where.OR = [
{
form_data: {
path: [],
string_contains: search,
},
},
{
table_name: {
contains: search,
mode: "insensitive",
},
},
];
}
// 정렬 조건 구성
const orderBy: Prisma.dynamic_form_dataOrderByWithRelationInput = {};
if (sortBy === "created_at" || sortBy === "updated_at") {
orderBy[sortBy] = sortOrder;
} else {
orderBy.created_at = "desc"; // 기본값
}
// 데이터 조회
const [results, totalCount] = await Promise.all([
prisma.dynamic_form_data.findMany({
where,
orderBy,
skip,
take: size,
}),
prisma.dynamic_form_data.count({ where }),
]);
const formDataResults: FormDataResult[] = results.map((result) => ({
id: result.id,
screenId: result.screen_id,
tableName: result.table_name,
data: result.form_data as Record<string, any>,
createdAt: result.created_at,
updatedAt: result.updated_at,
createdBy: result.created_by,
updatedBy: result.updated_by,
}));
const totalPages = Math.ceil(totalCount / size);
console.log("✅ 서비스: 폼 데이터 목록 조회 성공:", {
totalCount,
totalPages,
currentPage: page,
});
return {
content: formDataResults,
totalElements: totalCount,
totalPages,
currentPage: page,
size,
};
} catch (error) {
console.error("❌ 서비스: 폼 데이터 목록 조회 실패:", error);
throw new Error(`폼 데이터 목록 조회 실패: ${error}`);
}
}
/**
*
*/
async validateFormData(
tableName: string,
data: Record<string, any>
): Promise<ValidationResult> {
try {
console.log("✅ 서비스: 폼 데이터 검증 시작:", { tableName, data });
const errors: ValidationError[] = [];
// 기본 검증 로직 (실제로는 테이블 스키마를 확인해야 함)
Object.entries(data).forEach(([key, value]) => {
// 예시: 빈 값 검증
if (value === null || value === undefined || value === "") {
// 특정 필드가 required인지 확인하는 로직이 필요
// 지금은 간단히 모든 필드를 선택사항으로 처리
}
// 예시: 데이터 타입 검증
// 실제로는 테이블 스키마의 컬럼 타입과 비교해야 함
});
const result: ValidationResult = {
valid: errors.length === 0,
errors,
};
console.log("✅ 서비스: 폼 데이터 검증 완료:", result);
return result;
} catch (error) {
console.error("❌ 서비스: 폼 데이터 검증 실패:", error);
throw new Error(`폼 데이터 검증 실패: ${error}`);
}
}
/**
* (PostgreSQL )
*/
async getTableColumns(tableName: string): Promise<TableColumn[]> {
try {
console.log("📊 서비스: 테이블 컬럼 정보 조회 시작:", { tableName });
// PostgreSQL의 information_schema를 사용하여 컬럼 정보 조회
const columns = await prisma.$queryRaw<any[]>`
SELECT
column_name,
data_type,
is_nullable,
column_default,
character_maximum_length
FROM information_schema.columns
WHERE table_name = ${tableName}
AND table_schema = 'public'
ORDER BY ordinal_position
`;
// Primary key 정보 조회
const primaryKeys = await prisma.$queryRaw<any[]>`
SELECT
kcu.column_name
FROM
information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE
tc.constraint_type = 'PRIMARY KEY'
AND tc.table_name = ${tableName}
AND tc.table_schema = 'public'
`;
const primaryKeyColumns = new Set(
primaryKeys.map((pk) => pk.column_name)
);
const result: TableColumn[] = columns.map((col) => ({
columnName: col.column_name,
dataType: col.data_type,
nullable: col.is_nullable === "YES",
primaryKey: primaryKeyColumns.has(col.column_name),
maxLength: col.character_maximum_length,
defaultValue: col.column_default,
}));
console.log("✅ 서비스: 테이블 컬럼 정보 조회 성공:", result);
return result;
} catch (error) {
console.error("❌ 서비스: 테이블 컬럼 정보 조회 실패:", error);
throw new Error(`테이블 컬럼 정보 조회 실패: ${error}`);
}
}
}
// 싱글톤 인스턴스 생성 및 export
export const dynamicFormService = new DynamicFormService();

View File

@ -69,6 +69,7 @@ export class TableManagementService {
COALESCE(cl.column_label, c.column_name) as "displayName",
c.data_type as "dbType",
COALESCE(cl.web_type, 'text') as "webType",
COALESCE(cl.input_type, 'direct') as "inputType",
COALESCE(cl.detail_settings, '') as "detailSettings",
COALESCE(cl.description, '') as "description",
c.is_nullable as "isNullable",
@ -368,7 +369,8 @@ export class TableManagementService {
tableName: string,
columnName: string,
webType: string,
detailSettings?: Record<string, any>
detailSettings?: Record<string, any>,
inputType?: string
): Promise<void> {
try {
logger.info(
@ -394,30 +396,42 @@ export class TableManagementService {
if (existingColumn) {
// 기존 컬럼 라벨 업데이트
const updateData: any = {
web_type: webType,
detail_settings: JSON.stringify(finalDetailSettings),
updated_date: new Date(),
};
if (inputType) {
updateData.input_type = inputType;
}
await prisma.column_labels.update({
where: {
id: existingColumn.id,
},
data: {
web_type: webType,
detail_settings: JSON.stringify(finalDetailSettings),
updated_date: new Date(),
},
data: updateData,
});
logger.info(
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
);
} else {
// 새로운 컬럼 라벨 생성
const createData: any = {
table_name: tableName,
column_name: columnName,
web_type: webType,
detail_settings: JSON.stringify(finalDetailSettings),
created_date: new Date(),
updated_date: new Date(),
};
if (inputType) {
createData.input_type = inputType;
}
await prisma.column_labels.create({
data: {
table_name: tableName,
column_name: columnName,
web_type: webType,
detail_settings: JSON.stringify(finalDetailSettings),
created_date: new Date(),
updated_date: new Date(),
},
data: createData,
});
logger.info(
`컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`

View File

@ -224,6 +224,7 @@ export interface ColumnInfo {
columnLabel?: string;
dataType: string;
webType?: WebType;
inputType?: "direct" | "auto"; // 입력 타입
isNullable: string;
columnDefault?: string;
characterMaximumLength?: number;

View File

@ -12,6 +12,7 @@ export interface ColumnTypeInfo {
displayName: string;
dbType: string;
webType: string;
inputType?: "direct" | "auto";
detailSettings: string;
description: string;
isNullable: string;

View File

@ -206,6 +206,10 @@ export default function ScreenViewPage() {
}));
}}
hideLabel={true} // 라벨 숨김 플래그 전달
screenInfo={{
id: screenId,
tableName: screen?.tableName,
}}
/>
</div>
</div>

View File

@ -28,6 +28,8 @@ import {
ButtonTypeConfig,
} from "@/types/screen";
import { InteractiveDataTable } from "./InteractiveDataTable";
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
import { useParams } from "next/navigation";
interface InteractiveScreenViewerProps {
component: ComponentData;
@ -35,6 +37,10 @@ interface InteractiveScreenViewerProps {
formData?: Record<string, any>;
onFormDataChange?: (fieldName: string, value: any) => void;
hideLabel?: boolean;
screenInfo?: {
id: number;
tableName?: string;
};
}
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
@ -43,6 +49,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
formData: externalFormData,
onFormDataChange,
hideLabel = false,
screenInfo,
}) => {
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
@ -683,28 +690,300 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
const widget = comp as WidgetComponent;
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
const handleButtonClick = () => {
if (config?.actionType === "popup" && config.popupTitle) {
alert(`${config.popupTitle}\n\n${config.popupContent || "팝업 내용이 없습니다."}`);
} else if (config?.actionType === "navigate" && config.navigateUrl) {
const handleButtonClick = async () => {
const actionType = config?.actionType || "save";
try {
switch (actionType) {
case "save":
await handleSaveAction();
break;
case "cancel":
handleCancelAction();
break;
case "delete":
await handleDeleteAction();
break;
case "edit":
handleEditAction();
break;
case "add":
handleAddAction();
break;
case "search":
handleSearchAction();
break;
case "reset":
handleResetAction();
break;
case "submit":
await handleSubmitAction();
break;
case "close":
handleCloseAction();
break;
case "popup":
handlePopupAction();
break;
case "navigate":
handleNavigateAction();
break;
case "custom":
await handleCustomAction();
break;
default:
console.log(`알 수 없는 액션 타입: ${actionType}`);
}
} catch (error) {
console.error(`버튼 액션 실행 오류 (${actionType}):`, error);
alert(`작업 중 오류가 발생했습니다: ${error.message}`);
}
};
// 저장 액션
const handleSaveAction = async () => {
if (!formData || Object.keys(formData).length === 0) {
alert("저장할 데이터가 없습니다.");
return;
}
// 필수 항목 검증
const requiredFields = allComponents.filter(c => c.required && (c.columnName || c.id));
const missingFields = requiredFields.filter(field => {
const fieldName = field.columnName || field.id;
const value = formData[fieldName];
return !value || value.toString().trim() === "";
});
if (missingFields.length > 0) {
const fieldNames = missingFields.map(f => f.label || f.columnName || f.id).join(", ");
alert(`다음 필수 항목을 입력해주세요: ${fieldNames}`);
return;
}
if (!screenInfo?.id) {
alert("화면 정보가 없어 저장할 수 없습니다.");
return;
}
try {
// 컬럼명 기반으로 데이터 매핑
const mappedData: Record<string, any> = {};
// 컴포넌트에서 컬럼명이 있는 것들만 매핑
allComponents.forEach(comp => {
if (comp.columnName) {
const fieldName = comp.columnName;
const componentId = comp.id;
// formData에서 해당 값 찾기 (컬럼명 우선, 없으면 컴포넌트 ID)
const value = formData[fieldName] || formData[componentId];
if (value !== undefined && value !== "") {
mappedData[fieldName] = value;
}
}
});
console.log("💾 저장할 데이터 매핑:", {
원본데이터: formData,
매핑된데이터: mappedData,
화면정보: screenInfo,
});
// 테이블명 결정 (화면 정보에서 가져오거나 첫 번째 컴포넌트의 테이블명 사용)
const tableName = screenInfo.tableName ||
allComponents.find(c => c.columnName)?.tableName ||
"dynamic_form_data"; // 기본값
const saveData: DynamicFormData = {
screenId: screenInfo.id,
tableName: tableName,
data: mappedData,
};
console.log("🚀 API 저장 요청:", saveData);
const result = await dynamicFormApi.saveFormData(saveData);
if (result.success) {
alert("저장되었습니다.");
console.log("✅ 저장 성공:", result.data);
// 저장 후 데이터 초기화 (선택사항)
if (onFormDataChange) {
Object.keys(formData).forEach(key => {
onFormDataChange(key, "");
});
}
} else {
throw new Error(result.message || "저장에 실패했습니다.");
}
} catch (error: any) {
console.error("❌ 저장 실패:", error);
alert(`저장 중 오류가 발생했습니다: ${error.message}`);
}
};
// 취소 액션
const handleCancelAction = () => {
if (confirm("변경사항을 취소하시겠습니까?")) {
// 폼 초기화 또는 이전 페이지로 이동
if (onFormDataChange) {
// 모든 폼 데이터 초기화
Object.keys(formData).forEach(key => {
onFormDataChange(key, "");
});
}
console.log("❌ 작업이 취소되었습니다.");
}
};
// 삭제 액션
const handleDeleteAction = async () => {
const confirmMessage = config?.confirmMessage || "정말로 삭제하시겠습니까?";
if (!confirm(confirmMessage)) {
return;
}
// 삭제할 레코드 ID가 필요 (폼 데이터에서 id 필드 찾기)
const recordId = formData["id"] || formData["ID"] || formData["objid"];
if (!recordId) {
alert("삭제할 데이터를 찾을 수 없습니다. (ID가 없음)");
return;
}
// 테이블명 결정
const tableName = screenInfo?.tableName ||
allComponents.find(c => c.columnName)?.tableName ||
"unknown_table";
if (!tableName || tableName === "unknown_table") {
alert("테이블 정보가 없어 삭제할 수 없습니다.");
return;
}
try {
console.log("🗑️ 삭제 실행:", { recordId, tableName, formData });
const result = await dynamicFormApi.deleteFormDataFromTable(recordId, tableName);
if (result.success) {
alert("삭제되었습니다.");
console.log("✅ 삭제 성공");
// 삭제 후 폼 초기화
if (onFormDataChange) {
Object.keys(formData).forEach(key => {
onFormDataChange(key, "");
});
}
} else {
throw new Error(result.message || "삭제에 실패했습니다.");
}
} catch (error: any) {
console.error("❌ 삭제 실패:", error);
alert(`삭제 중 오류가 발생했습니다: ${error.message}`);
}
};
// 편집 액션
const handleEditAction = () => {
console.log("✏️ 편집 모드 활성화");
// 읽기 전용 모드를 편집 모드로 전환
alert("편집 모드로 전환되었습니다.");
};
// 추가 액션
const handleAddAction = () => {
console.log(" 새 항목 추가");
// 새 항목 추가 로직
alert("새 항목을 추가할 수 있습니다.");
};
// 검색 액션
const handleSearchAction = () => {
console.log("🔍 검색 실행:", formData);
// 검색 로직
const searchTerms = Object.values(formData).filter(v => v && v.toString().trim());
if (searchTerms.length === 0) {
alert("검색할 내용을 입력해주세요.");
} else {
alert(`검색 실행: ${searchTerms.join(", ")}`);
}
};
// 초기화 액션
const handleResetAction = () => {
if (confirm("모든 입력을 초기화하시겠습니까?")) {
if (onFormDataChange) {
Object.keys(formData).forEach(key => {
onFormDataChange(key, "");
});
}
console.log("🔄 폼 초기화 완료");
alert("입력이 초기화되었습니다.");
}
};
// 제출 액션
const handleSubmitAction = async () => {
console.log("📤 폼 제출:", formData);
// 제출 로직
alert("제출되었습니다.");
};
// 닫기 액션
const handleCloseAction = () => {
console.log("❌ 창 닫기");
// 창 닫기 또는 모달 닫기
if (window.opener) {
window.close();
} else {
history.back();
}
};
// 팝업 액션
const handlePopupAction = () => {
if (config?.popupTitle && config?.popupContent) {
// 커스텀 모달 대신 기본 alert 사용 (향후 모달 컴포넌트로 교체 가능)
alert(`${config.popupTitle}\n\n${config.popupContent}`);
} else {
alert("팝업을 표시합니다.");
}
};
// 네비게이션 액션
const handleNavigateAction = () => {
if (config?.navigateUrl) {
if (config.navigateTarget === "_blank") {
window.open(config.navigateUrl, "_blank");
} else {
window.location.href = config.navigateUrl;
}
} else if (config?.actionType === "custom" && config.customAction) {
} else {
console.log("🔗 네비게이션 URL이 설정되지 않았습니다.");
}
};
// 커스텀 액션
const handleCustomAction = async () => {
if (config?.customAction) {
try {
// 간단한 JavaScript 실행 (보안상 제한적)
eval(config.customAction);
// 보안상 제한적인 eval 사용
const result = eval(config.customAction);
if (result instanceof Promise) {
await result;
}
console.log("⚡ 커스텀 액션 실행 완료");
} catch (error) {
console.error("커스텀 액션 실행 오류:", error);
}
} else if (config?.actionType === "delete" && config.confirmMessage) {
if (confirm(config.confirmMessage)) {
console.log("삭제 확인됨");
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
}
} else {
console.log(`버튼 클릭: ${config?.actionType || "기본"} 액션`);
console.log("⚡ 커스텀 액션이 설정되지 않았습니다.");
}
};

View File

@ -85,8 +85,64 @@ const renderWidget = (component: ComponentData) => {
const widget = component as WidgetComponent;
const config = widget.webTypeConfig as TextTypeConfig | undefined;
// 입력 타입에 따른 처리
const isAutoInput = widget.inputType === "auto";
// 자동 값 생성 함수
const getAutoValue = (autoValueType: string) => {
switch (autoValueType) {
case "current_datetime":
return new Date().toLocaleString("ko-KR");
case "current_date":
return new Date().toLocaleDateString("ko-KR");
case "current_time":
return new Date().toLocaleTimeString("ko-KR");
case "current_user":
return "현재사용자";
case "uuid":
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
case "sequence":
return "SEQ_001";
case "user_defined":
return "사용자정의값";
default:
return "자동생성값";
}
};
// 자동 값 플레이스홀더 생성 함수
const getAutoPlaceholder = (autoValueType: string) => {
switch (autoValueType) {
case "current_datetime":
return "현재 날짜시간";
case "current_date":
return "현재 날짜";
case "current_time":
return "현재 시간";
case "current_user":
return "현재 사용자";
case "uuid":
return "UUID";
case "sequence":
return "시퀀스";
case "user_defined":
return "사용자 정의";
default:
return "자동 생성됨";
}
};
// 플레이스홀더 처리
const finalPlaceholder = config?.placeholder || placeholder || "텍스트를 입력하세요";
const finalPlaceholder = isAutoInput
? getAutoPlaceholder(widget.autoValueType || "current_datetime")
: config?.placeholder || placeholder || "텍스트를 입력하세요";
// 자동 값 처리
const autoValue = isAutoInput ? getAutoValue(widget.autoValueType || "current_datetime") : "";
const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text";
@ -139,12 +195,14 @@ const renderWidget = (component: ComponentData) => {
const inputProps = {
...commonProps,
placeholder: finalPlaceholder,
value: isAutoInput ? autoValue : undefined, // 자동입력인 경우 자동 값 표시
minLength: config?.minLength,
maxLength: config?.maxLength,
pattern: getPatternByFormat(config?.format || "none"),
onInput: handleInputChange,
onChange: () => {}, // 읽기 전용으로 처리
readOnly: true,
readOnly: readonly || isAutoInput, // 자동입력인 경우 읽기 전용
className: `w-full h-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`,
};
// multiline이면 Textarea로 렌더링
@ -160,19 +218,69 @@ const renderWidget = (component: ComponentData) => {
const widget = component as WidgetComponent;
const config = widget.webTypeConfig as NumberTypeConfig | undefined;
// 입력 타입에 따른 처리
const isAutoInput = widget.inputType === "auto";
// 자동 값 생성 함수 (숫자용)
const getAutoNumberValue = (autoValueType: string) => {
switch (autoValueType) {
case "current_datetime":
return Date.now().toString();
case "current_date":
return new Date().getDate().toString();
case "current_time":
return new Date().getHours().toString();
case "sequence":
return "1001";
case "uuid":
return Math.floor(Math.random() * 1000000).toString();
case "user_defined":
return "999";
default:
return "0";
}
};
// 자동 값 플레이스홀더 생성 함수 (숫자용)
const getAutoNumberPlaceholder = (autoValueType: string) => {
switch (autoValueType) {
case "current_datetime":
return "타임스탬프";
case "current_date":
return "현재 일";
case "current_time":
return "현재 시";
case "sequence":
return "시퀀스";
case "uuid":
return "랜덤 숫자";
case "user_defined":
return "사용자 정의";
default:
return "자동 생성";
}
};
// 자동 값 처리
const autoValue = isAutoInput ? getAutoNumberValue(widget.autoValueType || "sequence") : "";
// 디버깅: 현재 설정값 확인
console.log("🔢 숫자 위젯 렌더링:", {
componentId: widget.id,
widgetType: widget.widgetType,
config,
placeholder: widget.placeholder,
inputType: widget.inputType,
isAutoInput,
});
// 단계값 결정: webTypeConfig > 기본값 (소수는 0.01, 정수는 1)
const step = config?.step || (widgetType === "decimal" ? 0.01 : 1);
// 플레이스홀더 처리
const finalPlaceholder = config?.placeholder || placeholder || "숫자를 입력하세요";
const finalPlaceholder = isAutoInput
? getAutoNumberPlaceholder(widget.autoValueType || "sequence")
: config?.placeholder || placeholder || "숫자를 입력하세요";
// 형식에 따른 표시값 처리
const formatValue = (value: string) => {
@ -215,9 +323,10 @@ const renderWidget = (component: ComponentData) => {
max={config?.max}
{...commonProps}
placeholder={finalPlaceholder}
className={`${config?.prefix ? "rounded-l-none" : ""} ${config?.suffix ? "rounded-r-none" : ""} ${borderClass}`}
value={isAutoInput ? autoValue : undefined} // 자동입력인 경우 자동 값 표시
className={`${config?.prefix ? "rounded-l-none" : ""} ${config?.suffix ? "rounded-r-none" : ""} ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
onChange={() => {}} // 읽기 전용으로 처리
readOnly
readOnly={readonly || isAutoInput}
/>
{config.suffix && (
<span className="rounded-r border border-l-0 bg-gray-50 px-2 py-2 text-sm text-gray-600">
@ -236,8 +345,10 @@ const renderWidget = (component: ComponentData) => {
max={config?.max}
{...commonProps}
placeholder={finalPlaceholder}
value={isAutoInput ? autoValue : undefined} // 자동입력인 경우 자동 값 표시
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
onChange={() => {}} // 읽기 전용으로 처리
readOnly
readOnly={readonly || isAutoInput}
/>
);
}
@ -247,6 +358,46 @@ const renderWidget = (component: ComponentData) => {
const widget = component as WidgetComponent;
const config = widget.webTypeConfig as DateTypeConfig | undefined;
// 입력 타입에 따른 처리
const isAutoInput = widget.inputType === "auto";
// 자동 값 생성 함수 (날짜용)
const getAutoDateValue = (autoValueType: string, inputType: string) => {
const now = new Date();
switch (autoValueType) {
case "current_datetime":
return inputType === "datetime-local"
? now.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm
: now.toISOString().slice(0, 10); // YYYY-MM-DD
case "current_date":
return now.toISOString().slice(0, 10); // YYYY-MM-DD
case "current_time":
return inputType === "datetime-local"
? now.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm
: now.toTimeString().slice(0, 5); // HH:mm
case "user_defined":
return inputType === "datetime-local" ? "2024-01-01T09:00" : "2024-01-01";
default:
return inputType === "datetime-local" ? now.toISOString().slice(0, 16) : now.toISOString().slice(0, 10);
}
};
// 자동 값 플레이스홀더 생성 함수 (날짜용)
const getAutoDatePlaceholder = (autoValueType: string) => {
switch (autoValueType) {
case "current_datetime":
return "현재 날짜시간";
case "current_date":
return "현재 날짜";
case "current_time":
return "현재 시간";
case "user_defined":
return "사용자 정의";
default:
return "자동 생성";
}
};
// 웹타입 설정에 따른 input type 결정
let inputType = "date";
if (config?.showTime || config?.format?.includes("HH:mm")) {
@ -273,8 +424,13 @@ const renderWidget = (component: ComponentData) => {
}
}
// 자동 값 처리
const autoValue = isAutoInput ? getAutoDateValue(widget.autoValueType || "current_date", inputType) : "";
// 플레이스홀더 우선순위: webTypeConfig > placeholder > 기본값
const finalPlaceholder = config?.placeholder || placeholder || "날짜를 선택하세요";
const finalPlaceholder = isAutoInput
? getAutoDatePlaceholder(widget.autoValueType || "current_date")
: config?.placeholder || placeholder || "날짜를 선택하세요";
// 디버깅: 현재 설정값 확인
console.log("📅 날짜 위젯 렌더링:", {
@ -331,9 +487,10 @@ const renderWidget = (component: ComponentData) => {
placeholder={finalPlaceholder}
min={config?.minDate}
max={config?.maxDate}
value={processedDefaultValue}
value={isAutoInput ? autoValue : processedDefaultValue}
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
onChange={() => {}} // 읽기 전용으로 처리
readOnly
readOnly={readonly || isAutoInput}
/>
);
}

View File

@ -173,6 +173,32 @@ export default function TableTypeSelector({
}
};
// 입력 타입 변경
const handleInputTypeChange = async (columnName: string, inputType: "direct" | "auto") => {
try {
// 현재 컬럼 정보 가져오기
const currentColumn = columns.find((col) => col.columnName === columnName);
if (!currentColumn) return;
// 웹 타입과 함께 입력 타입 업데이트
await tableTypeApi.setColumnWebType(
selectedTable,
columnName,
currentColumn.webType || "text",
undefined, // detailSettings
inputType,
);
// 로컬 상태 업데이트
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
console.log(`컬럼 ${columnName}의 입력 타입을 ${inputType}로 변경했습니다.`);
} catch (error) {
console.error("입력 타입 변경 실패:", error);
alert("입력 타입 설정에 실패했습니다. 다시 시도해주세요.");
}
};
const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
return (
@ -233,6 +259,7 @@ export default function TableTypeSelector({
<TableHead></TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead> </TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
@ -267,6 +294,20 @@ export default function TableTypeSelector({
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Select
value={column.inputType || "direct"}
onValueChange={(value) => handleInputTypeChange(column.columnName, value as "direct" | "auto")}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="direct"></SelectItem>
<SelectItem value="auto"></SelectItem>
</SelectContent>
</Select>
</TableCell>
<TableCell>
<Badge variant={column.isNullable === "NO" ? "default" : "secondary"}>
{column.isNullable === "NO" ? "필수" : "선택"}

View File

@ -48,23 +48,42 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
// 로컬 상태 관리
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>({
actionType: "custom",
variant: "default",
size: "sm",
...config,
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>(() => {
const defaultConfig = {
actionType: "custom" as ButtonActionType,
variant: "default" as ButtonVariant,
size: "sm" as ButtonSize,
};
return {
...defaultConfig,
...config, // 저장된 값이 기본값을 덮어씀
};
});
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
// 기본값 설정 (실제 값이 있으면 덮어쓰지 않음)
const defaultConfig = {
actionType: "custom" as ButtonActionType,
variant: "default" as ButtonVariant,
size: "sm" as ButtonSize,
};
// 실제 저장된 값이 우선순위를 가지도록 설정
setLocalConfig({
actionType: "custom",
variant: "default",
size: "sm",
...newConfig,
...defaultConfig,
...newConfig, // 저장된 값이 기본값을 덮어씀
});
}, [component.webTypeConfig]);
console.log("🔄 ButtonConfigPanel 로컬 상태 동기화:", {
componentId: component.id,
savedConfig: newConfig,
finalConfig: { ...defaultConfig, ...newConfig },
});
}, [component.webTypeConfig, component.id]);
// 설정 업데이트 함수
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
@ -194,7 +213,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
break;
}
updateConfig(updates);
// 로컬 상태 업데이트 후 webTypeConfig도 함께 업데이트
const newConfig = { ...localConfig, ...updates };
setLocalConfig(newConfig);
// webTypeConfig를 마지막에 다시 업데이트하여 확실히 저장되도록 함
setTimeout(() => {
onUpdateComponent({
webTypeConfig: newConfig,
});
console.log("🎯 ButtonActionType webTypeConfig 최종 업데이트:", {
actionType,
newConfig,
componentId: component.id,
});
}, 0);
};
const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType);

View File

@ -2,6 +2,7 @@
import React from "react";
import { Settings } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
ComponentData,
WidgetComponent,
@ -223,6 +224,87 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({ select
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">{widget.widgetType}</span>
</div>
<div className="mt-1 text-xs text-gray-500">: {widget.columnName}</div>
{/* 입력 타입 설정 */}
<div className="mt-3 space-y-2">
<label className="text-sm font-medium text-gray-700"> </label>
<Select
value={widget.inputType || "direct"}
onValueChange={(value: "direct" | "auto") => {
onUpdateProperty(widget.id, "inputType", value);
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="direct"></SelectItem>
<SelectItem value="auto"></SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
{widget.inputType === "auto"
? "시스템에서 자동으로 값을 생성합니다 (읽기 전용)"
: "사용자가 직접 값을 입력할 수 있습니다"}
</p>
{/* 자동 값 타입 설정 (자동입력일 때만 표시) */}
{widget.inputType === "auto" && (
<div className="mt-3 space-y-2">
<label className="text-sm font-medium text-gray-700"> </label>
<Select
value={widget.autoValueType || "current_datetime"}
onValueChange={(
value:
| "current_datetime"
| "current_date"
| "current_time"
| "current_user"
| "uuid"
| "sequence"
| "user_defined",
) => {
onUpdateProperty(widget.id, "autoValueType", value);
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="current_datetime"> </SelectItem>
<SelectItem value="current_date"> </SelectItem>
<SelectItem value="current_time"> </SelectItem>
<SelectItem value="current_user"> </SelectItem>
<SelectItem value="uuid">UUID</SelectItem>
<SelectItem value="sequence">퀀</SelectItem>
<SelectItem value="user_defined"> </SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
{(() => {
switch (widget.autoValueType || "current_datetime") {
case "current_datetime":
return "현재 날짜와 시간을 자동으로 입력합니다";
case "current_date":
return "현재 날짜를 자동으로 입력합니다";
case "current_time":
return "현재 시간을 자동으로 입력합니다";
case "current_user":
return "현재 로그인한 사용자 정보를 입력합니다";
case "uuid":
return "고유한 UUID를 생성합니다";
case "sequence":
return "순차적인 번호를 생성합니다";
case "user_defined":
return "사용자가 정의한 규칙에 따라 값을 생성합니다";
default:
return "";
}
})()}
</p>
</div>
)}
</div>
</div>
{/* 상세 설정 영역 */}

View File

@ -0,0 +1,319 @@
import { apiClient, ApiResponse } from "./client";
// 동적 폼 데이터 타입
export interface DynamicFormData {
screenId: number;
tableName: string;
data: Record<string, any>;
}
// 폼 데이터 저장 응답 타입
export interface SaveFormDataResponse {
id: number;
success: boolean;
message: string;
data?: Record<string, any>;
}
// 폼 데이터 조회 응답 타입
export interface FormDataResponse {
id: number;
screenId: number;
tableName: string;
data: Record<string, any>;
createdAt: string;
updatedAt: string;
}
// 동적 폼 API 클래스
export class DynamicFormApi {
/**
*
* @param formData
* @returns
*/
static async saveFormData(formData: DynamicFormData): Promise<ApiResponse<SaveFormDataResponse>> {
try {
console.log("💾 폼 데이터 저장 요청:", formData);
const response = await apiClient.post("/dynamic-form/save", formData);
console.log("✅ 폼 데이터 저장 성공:", response.data);
return {
success: true,
data: response.data,
message: "데이터가 성공적으로 저장되었습니다.",
};
} catch (error: any) {
console.error("❌ 폼 데이터 저장 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 저장 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param id ID
* @param formData
* @returns
*/
static async updateFormData(
id: number,
formData: Partial<DynamicFormData>,
): Promise<ApiResponse<SaveFormDataResponse>> {
try {
console.log("🔄 폼 데이터 업데이트 요청:", { id, formData });
const response = await apiClient.put(`/dynamic-form/${id}`, formData);
console.log("✅ 폼 데이터 업데이트 성공:", response.data);
return {
success: true,
data: response.data,
message: "데이터가 성공적으로 업데이트되었습니다.",
};
} catch (error: any) {
console.error("❌ 폼 데이터 업데이트 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 업데이트 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param id ID
* @returns
*/
static async deleteFormData(id: number): Promise<ApiResponse<void>> {
try {
console.log("🗑️ 폼 데이터 삭제 요청:", id);
await apiClient.delete(`/dynamic-form/${id}`);
console.log("✅ 폼 데이터 삭제 성공");
return {
success: true,
message: "데이터가 성공적으로 삭제되었습니다.",
};
} catch (error: any) {
console.error("❌ 폼 데이터 삭제 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 삭제 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param id ID
* @param tableName
* @returns
*/
static async deleteFormDataFromTable(id: number, tableName: string): Promise<ApiResponse<void>> {
try {
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName });
await apiClient.delete(`/dynamic-form/${id}`, {
data: { tableName },
});
console.log("✅ 실제 테이블에서 폼 데이터 삭제 성공");
return {
success: true,
message: "데이터가 성공적으로 삭제되었습니다.",
};
} catch (error: any) {
console.error("❌ 실제 테이블에서 폼 데이터 삭제 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 삭제 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param screenId ID
* @param params
* @returns
*/
static async getFormDataList(
screenId: number,
params?: {
page?: number;
size?: number;
search?: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
},
): Promise<
ApiResponse<{
content: FormDataResponse[];
totalElements: number;
totalPages: number;
currentPage: number;
size: number;
}>
> {
try {
console.log("📋 폼 데이터 목록 조회 요청:", { screenId, params });
const response = await apiClient.get(`/dynamic-form/screen/${screenId}`, { params });
console.log("✅ 폼 데이터 목록 조회 성공:", response.data);
return {
success: true,
data: response.data,
};
} catch (error: any) {
console.error("❌ 폼 데이터 목록 조회 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 조회 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param id ID
* @returns
*/
static async getFormData(id: number): Promise<ApiResponse<FormDataResponse>> {
try {
console.log("📄 폼 데이터 단건 조회 요청:", id);
const response = await apiClient.get(`/dynamic-form/${id}`);
console.log("✅ 폼 데이터 단건 조회 성공:", response.data);
return {
success: true,
data: response.data,
};
} catch (error: any) {
console.error("❌ 폼 데이터 단건 조회 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 조회 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
* ( )
* @param tableName
* @returns
*/
static async getTableColumns(tableName: string): Promise<
ApiResponse<{
tableName: string;
columns: Array<{
columnName: string;
dataType: string;
nullable: boolean;
primaryKey: boolean;
maxLength?: number;
defaultValue?: any;
}>;
}>
> {
try {
console.log("📊 테이블 컬럼 정보 조회 요청:", tableName);
const response = await apiClient.get(`/dynamic-form/table/${tableName}/columns`);
console.log("✅ 테이블 컬럼 정보 조회 성공:", response.data);
return {
success: true,
data: response.data,
};
} catch (error: any) {
console.error("❌ 테이블 컬럼 정보 조회 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "테이블 정보 조회 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
/**
*
* @param tableName
* @param data
* @returns
*/
static async validateFormData(
tableName: string,
data: Record<string, any>,
): Promise<
ApiResponse<{
valid: boolean;
errors: Array<{
field: string;
message: string;
code: string;
}>;
}>
> {
try {
console.log("✅ 폼 데이터 검증 요청:", { tableName, data });
const response = await apiClient.post(`/dynamic-form/validate`, {
tableName,
data,
});
console.log("✅ 폼 데이터 검증 성공:", response.data);
return {
success: true,
data: response.data,
};
} catch (error: any) {
console.error("❌ 폼 데이터 검증 실패:", error);
const errorMessage = error.response?.data?.message || error.message || "데이터 검증 중 오류가 발생했습니다.";
return {
success: false,
message: errorMessage,
errorCode: error.response?.data?.errorCode,
};
}
}
}
// 편의를 위한 기본 export
export const dynamicFormApi = DynamicFormApi;

View File

@ -152,10 +152,12 @@ export const tableTypeApi = {
columnName: string,
webType: string,
detailSettings?: Record<string, any>,
inputType?: "direct" | "auto",
): Promise<void> => {
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
webType,
detailSettings,
inputType,
});
},

View File

@ -156,6 +156,15 @@ export interface BaseComponent {
tableName?: string; // 테이블명 추가
label?: string; // 라벨 추가
gridColumns?: number; // 그리드에서 차지할 컬럼 수 (1-12)
inputType?: "direct" | "auto"; // 입력 타입 (직접입력/자동입력)
autoValueType?:
| "current_datetime"
| "current_date"
| "current_time"
| "current_user"
| "uuid"
| "sequence"
| "user_defined"; // 자동 값 타입
}
// 컨테이너 컴포넌트
@ -460,6 +469,7 @@ export interface ColumnInfo {
dataType: string;
webType?: WebType;
widgetType?: WebType; // 프론트엔드에서 사용하는 필드 (webType과 동일)
inputType?: "direct" | "auto"; // 입력 타입
isNullable: string;
required?: boolean; // isNullable에서 변환된 필드
columnDefault?: string;