Compare commits
2 Commits
feb26fa32a
...
78d4d7de23
| Author | SHA1 | Date |
|---|---|---|
|
|
78d4d7de23 | |
|
|
d7c41fc35d |
|
|
@ -8,7 +8,17 @@ datasource db {
|
||||||
url = env("DATABASE_URL")
|
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 {
|
model admin_supply_mng {
|
||||||
objid Decimal @id @default(0) @db.Decimal
|
objid Decimal @id @default(0) @db.Decimal
|
||||||
|
|
@ -74,15 +84,12 @@ model approval {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model approval_kind {
|
model approval_kind {
|
||||||
target_type String @db.VarChar
|
target_type String @id @db.VarChar
|
||||||
target_name String? @db.VarChar
|
target_name String? @db.VarChar
|
||||||
regdate DateTime? @db.Timestamp(6)
|
regdate DateTime? @db.Timestamp(6)
|
||||||
status String? @db.VarChar
|
status String? @db.VarChar
|
||||||
|
|
||||||
@@id([target_type])
|
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,8 +142,6 @@ model arrival_plan {
|
||||||
@@index([part_objid])
|
@@index([part_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model as_mng {
|
model as_mng {
|
||||||
objid Int @id
|
objid Int @id
|
||||||
as_no String? @db.VarChar
|
as_no String? @db.VarChar
|
||||||
|
|
@ -247,20 +252,13 @@ model attach_file_info {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model authority_master {
|
model authority_master {
|
||||||
objid Decimal @id @default(0) @db.Decimal
|
objid Decimal @id @default(0) @db.Decimal
|
||||||
auth_name String? @default("NULL::character varying") @db.VarChar(256)
|
auth_name String? @default("NULL::character varying") @db.VarChar(256)
|
||||||
auth_code String? @default("NULL::character varying") @db.VarChar(64)
|
auth_code String? @default("NULL::character varying") @db.VarChar(64)
|
||||||
writer String? @default("NULL::character varying") @db.VarChar(32)
|
writer String? @default("NULL::character varying") @db.VarChar(32)
|
||||||
regdate DateTime? @db.Timestamp(6)
|
regdate DateTime? @db.Timestamp(6)
|
||||||
status String? @default("NULL::character varying") @db.VarChar(32)
|
status String? @default("NULL::character varying") @db.VarChar(32)
|
||||||
|
|
||||||
// 관계 설정
|
|
||||||
sub_users authority_sub_user[]
|
sub_users authority_sub_user[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,15 +275,12 @@ model authority_master_history {
|
||||||
}
|
}
|
||||||
|
|
||||||
model authority_sub_user {
|
model authority_sub_user {
|
||||||
objid Decimal @id @default(0) @db.Decimal
|
objid Decimal @id @default(0) @db.Decimal
|
||||||
master_objid Decimal? @db.Decimal
|
master_objid Decimal? @db.Decimal
|
||||||
user_id String? @default("NULL::character varying") @db.VarChar(64)
|
user_id String? @default("NULL::character varying") @db.VarChar(64)
|
||||||
writer String? @default("NULL::character varying") @db.VarChar(64)
|
writer String? @default("NULL::character varying") @db.VarChar(64)
|
||||||
regdate DateTime? @db.Timestamp(6)
|
regdate DateTime? @db.Timestamp(6)
|
||||||
|
|
||||||
// 관계 설정
|
|
||||||
authority_master authority_master? @relation(fields: [master_objid], references: [objid])
|
authority_master authority_master? @relation(fields: [master_objid], references: [objid])
|
||||||
user user_info? @relation(fields: [user_id], references: [user_id])
|
|
||||||
|
|
||||||
@@index([master_objid])
|
@@index([master_objid])
|
||||||
@@index([user_id])
|
@@index([user_id])
|
||||||
|
|
@ -343,13 +338,6 @@ model bom_part_qty {
|
||||||
@@index([parent_objid])
|
@@index([parent_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model car_distribute_member {
|
model car_distribute_member {
|
||||||
objid Decimal @db.Decimal
|
objid Decimal @db.Decimal
|
||||||
car_objid Decimal @db.Decimal
|
car_objid Decimal @db.Decimal
|
||||||
|
|
@ -439,6 +427,7 @@ model column_labels {
|
||||||
column_name String? @db.VarChar(100)
|
column_name String? @db.VarChar(100)
|
||||||
column_label String? @db.VarChar(200)
|
column_label String? @db.VarChar(200)
|
||||||
web_type String? @db.VarChar(50)
|
web_type String? @db.VarChar(50)
|
||||||
|
input_type String? @default("direct") @db.VarChar(20) // direct, auto
|
||||||
detail_settings String?
|
detail_settings String?
|
||||||
description String?
|
description String?
|
||||||
display_order Int? @default(0)
|
display_order Int? @default(0)
|
||||||
|
|
@ -530,9 +519,10 @@ model company_mng {
|
||||||
regdate DateTime? @db.Timestamp(6)
|
regdate DateTime? @db.Timestamp(6)
|
||||||
status String? @db.VarChar(32)
|
status String? @db.VarChar(32)
|
||||||
|
|
||||||
// 관계 설정
|
// 관계 정의
|
||||||
menus menu_info[]
|
menus menu_info[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model contract_mgmt {
|
model contract_mgmt {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
category_cd String? @db.VarChar
|
category_cd String? @db.VarChar
|
||||||
|
|
@ -599,8 +589,6 @@ model contract_mgmt_option {
|
||||||
@@index([option_objid])
|
@@index([option_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model counselingmgmt {
|
model counselingmgmt {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
reg_date String? @db.VarChar
|
reg_date String? @db.VarChar
|
||||||
|
|
@ -625,8 +613,6 @@ model counselingmgmt {
|
||||||
parent_seq String? @db.VarChar
|
parent_seq String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model customer_service_mgmt {
|
model customer_service_mgmt {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
service_no String? @db.VarChar
|
service_no String? @db.VarChar
|
||||||
|
|
@ -1181,8 +1167,6 @@ model inventory_mgmt_in {
|
||||||
@@index([parent_objid])
|
@@index([parent_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model inventory_mgmt_out {
|
model inventory_mgmt_out {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
parent_objid String? @db.VarChar
|
parent_objid String? @db.VarChar
|
||||||
|
|
@ -1262,6 +1246,7 @@ model invoice_mgmt {
|
||||||
discount_percentage String? @db.VarChar
|
discount_percentage String? @db.VarChar
|
||||||
inv_discount_price String? @db.VarChar
|
inv_discount_price String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
model invoice_mgmt_part {
|
model invoice_mgmt_part {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
invoice_objid String? @db.VarChar
|
invoice_objid String? @db.VarChar
|
||||||
|
|
@ -1506,7 +1491,7 @@ model menu_info {
|
||||||
lang_key String? @db.VarChar(100)
|
lang_key String? @db.VarChar(100)
|
||||||
lang_key_desc String? @db.VarChar(100)
|
lang_key_desc String? @db.VarChar(100)
|
||||||
|
|
||||||
// 관계 설정 (나중에 활용 가능)
|
// 관계 정의
|
||||||
company company_mng? @relation(fields: [company_code], references: [company_code])
|
company company_mng? @relation(fields: [company_code], references: [company_code])
|
||||||
|
|
||||||
@@index([parent_obj_id])
|
@@index([parent_obj_id])
|
||||||
|
|
@ -1537,8 +1522,6 @@ model mold_dev_request_info {
|
||||||
status String? @db.VarChar(50)
|
status String? @db.VarChar(50)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model multi_lang_key_master {
|
model multi_lang_key_master {
|
||||||
key_id Int @id @default(autoincrement())
|
key_id Int @id @default(autoincrement())
|
||||||
company_code String @default("*") @db.VarChar(20)
|
company_code String @default("*") @db.VarChar(20)
|
||||||
|
|
@ -1801,7 +1784,6 @@ model order_spec_mng_history {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model part_bom_qty {
|
model part_bom_qty {
|
||||||
bom_report_objid Decimal @db.Decimal
|
bom_report_objid Decimal @db.Decimal
|
||||||
objid Decimal @id @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")
|
@@index([unit_code, contract_objid], map: "part_bom_report_unit_code_idx")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model part_distribution_list {
|
model part_distribution_list {
|
||||||
part_objid Decimal @db.Decimal
|
part_objid Decimal @db.Decimal
|
||||||
product_mgmt_objid String? @db.VarChar(100)
|
product_mgmt_objid String? @db.VarChar(100)
|
||||||
|
|
@ -2021,9 +2000,6 @@ model part_mng_history {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model planning_issue {
|
model planning_issue {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
issue_no String @db.VarChar
|
issue_no String @db.VarChar
|
||||||
|
|
@ -2221,10 +2197,6 @@ model pms_wbs_task {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model pms_wbs_task_confirm {
|
model pms_wbs_task_confirm {
|
||||||
objid Decimal? @db.Decimal
|
objid Decimal? @db.Decimal
|
||||||
target_objid Decimal? @db.Decimal
|
target_objid Decimal? @db.Decimal
|
||||||
|
|
@ -2349,6 +2321,7 @@ model product_group_mng {
|
||||||
|
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
model product_kind_spec {
|
model product_kind_spec {
|
||||||
objid String @db.VarChar
|
objid String @db.VarChar
|
||||||
objid_parent String @db.VarChar
|
objid_parent String @db.VarChar
|
||||||
|
|
@ -2798,13 +2771,6 @@ model purchase_order_master {
|
||||||
@@index([multi_master_objid])
|
@@index([multi_master_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model purchase_order_master_241216 {
|
model purchase_order_master_241216 {
|
||||||
objid String? @db.VarChar
|
objid String? @db.VarChar
|
||||||
purchase_order_no String? @db.VarChar
|
purchase_order_no String? @db.VarChar
|
||||||
|
|
@ -2920,13 +2886,6 @@ model purchase_order_part {
|
||||||
@@index([purchase_order_master_objid])
|
@@index([purchase_order_master_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model ratecal_mgmt {
|
model ratecal_mgmt {
|
||||||
ratecal_mgmt_objid Decimal @default(0) @db.Decimal
|
ratecal_mgmt_objid Decimal @default(0) @db.Decimal
|
||||||
position String? @db.VarChar(100)
|
position String? @db.VarChar(100)
|
||||||
|
|
@ -2944,8 +2903,6 @@ model ratecal_mgmt {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model receive_history {
|
model receive_history {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
part_objid String @db.VarChar
|
part_objid String @db.VarChar
|
||||||
|
|
@ -3071,7 +3028,6 @@ model route {
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model sales_bom_part_qty {
|
model sales_bom_part_qty {
|
||||||
sales_bom_objid String @db.VarChar
|
sales_bom_objid String @db.VarChar
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
|
|
@ -3106,7 +3062,6 @@ model sales_bom_part_qty {
|
||||||
sales_part_code String? @db.VarChar
|
sales_part_code String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model sales_bom_report {
|
model sales_bom_report {
|
||||||
objid String @id @default("") @db.VarChar
|
objid String @id @default("") @db.VarChar
|
||||||
parent_objid String? @unique(map: "sales_bom_report_parent_objid_idx") @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])
|
@@index([parent_objid])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model sales_bom_report_part_241218 {
|
model sales_bom_report_part_241218 {
|
||||||
objid String? @db.VarChar
|
objid String? @db.VarChar
|
||||||
parent_objid String? @db.VarChar
|
parent_objid String? @db.VarChar
|
||||||
|
|
@ -3193,8 +3147,6 @@ model sales_long_delivery {
|
||||||
price String? @default("0") @db.VarChar
|
price String? @default("0") @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model sales_long_delivery_input {
|
model sales_long_delivery_input {
|
||||||
objid String @id(map: "sales_long_delivery_plan_pkey") @db.VarChar
|
objid String @id(map: "sales_long_delivery_plan_pkey") @db.VarChar
|
||||||
parent_objid String? @db.VarChar
|
parent_objid String? @db.VarChar
|
||||||
|
|
@ -3205,8 +3157,6 @@ model sales_long_delivery_input {
|
||||||
admin_editor String? @db.VarChar
|
admin_editor String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model sales_long_delivery_predict {
|
model sales_long_delivery_predict {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
parent_objid String? @db.VarChar
|
parent_objid String? @db.VarChar
|
||||||
|
|
@ -3249,7 +3199,6 @@ model sales_request_master {
|
||||||
remark String? @db.VarChar
|
remark String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
model sales_request_part {
|
model sales_request_part {
|
||||||
objid String @id @db.VarChar
|
objid String @id @db.VarChar
|
||||||
sales_bom_qty_objid String? @db.VarChar
|
sales_bom_qty_objid String? @db.VarChar
|
||||||
|
|
@ -3309,7 +3258,7 @@ model setup_wbs_task {
|
||||||
}
|
}
|
||||||
|
|
||||||
model setup_wbs_task_standard {
|
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
|
contract_objid String? @db.VarChar
|
||||||
parent_objid String? @db.VarChar
|
parent_objid String? @db.VarChar
|
||||||
task_category String? @db.VarChar
|
task_category String? @db.VarChar
|
||||||
|
|
@ -3808,6 +3757,7 @@ model swpc120a_tbl {
|
||||||
|
|
||||||
@@id([imitemid, suvndcd], map: "pk_swpc120a_tbl")
|
@@id([imitemid, suvndcd], map: "pk_swpc120a_tbl")
|
||||||
}
|
}
|
||||||
|
|
||||||
model swpc130a_tbl {
|
model swpc130a_tbl {
|
||||||
imitemid String @db.VarChar(15)
|
imitemid String @db.VarChar(15)
|
||||||
suvndcd String @db.VarChar(5)
|
suvndcd String @db.VarChar(5)
|
||||||
|
|
@ -4566,6 +4516,7 @@ model swsb500a_tbl {
|
||||||
edit_date DateTime? @db.Timestamp(6)
|
edit_date DateTime? @db.Timestamp(6)
|
||||||
edit_emp String? @db.VarChar(30)
|
edit_emp String? @db.VarChar(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
model swsb510a_tbl {
|
model swsb510a_tbl {
|
||||||
frw_req_no String @id(map: "pk_swsb510a_tbl") @db.VarChar(10)
|
frw_req_no String @id(map: "pk_swsb510a_tbl") @db.VarChar(10)
|
||||||
req_date String? @db.VarChar(8)
|
req_date String? @db.VarChar(8)
|
||||||
|
|
@ -4874,15 +4825,6 @@ model table_labels {
|
||||||
column_labels column_labels[]
|
column_labels column_labels[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model template_mng {
|
model template_mng {
|
||||||
objid Int @id
|
objid Int @id
|
||||||
template_code String? @db.VarChar
|
template_code String? @db.VarChar
|
||||||
|
|
@ -4893,12 +4835,6 @@ model template_mng {
|
||||||
template_code_detail String? @db.VarChar
|
template_code_detail String? @db.VarChar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model time_sheet {
|
model time_sheet {
|
||||||
objid Decimal @default(0) @db.Decimal
|
objid Decimal @default(0) @db.Decimal
|
||||||
project_mgmt_objid Decimal? @db.Decimal
|
project_mgmt_objid Decimal? @db.Decimal
|
||||||
|
|
@ -4971,7 +4907,6 @@ model user_info {
|
||||||
user_type String? @db.VarChar(1024)
|
user_type String? @db.VarChar(1024)
|
||||||
user_type_name String? @db.VarChar(1024)
|
user_type_name String? @db.VarChar(1024)
|
||||||
regdate DateTime? @db.Timestamp(6)
|
regdate DateTime? @db.Timestamp(6)
|
||||||
data_type String? @db.VarChar(64)
|
|
||||||
status String? @db.VarChar(32)
|
status String? @db.VarChar(32)
|
||||||
end_date DateTime? @db.Timestamp(6)
|
end_date DateTime? @db.Timestamp(6)
|
||||||
fax_no String? @db.VarChar
|
fax_no String? @db.VarChar
|
||||||
|
|
@ -4980,9 +4915,7 @@ model user_info {
|
||||||
photo Bytes?
|
photo Bytes?
|
||||||
locale String? @db.VarChar
|
locale String? @db.VarChar
|
||||||
company_code String? @db.VarChar(50)
|
company_code String? @db.VarChar(50)
|
||||||
|
data_type String? @db.VarChar(64)
|
||||||
// 관계 설정
|
|
||||||
authorities authority_sub_user[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model user_info_history {
|
model user_info_history {
|
||||||
|
|
@ -5051,44 +4984,38 @@ model zz_230410_user_info {
|
||||||
|
|
||||||
@@ignore
|
@@ignore
|
||||||
}
|
}
|
||||||
// 화면관리 시스템 Prisma 스키마
|
|
||||||
// 기존 schema.prisma에 추가할 모델들
|
|
||||||
|
|
||||||
model screen_definitions {
|
model screen_definitions {
|
||||||
screen_id Int @id @default(autoincrement())
|
screen_id Int @id @default(autoincrement())
|
||||||
screen_name String @db.VarChar(100)
|
screen_name String @db.VarChar(100)
|
||||||
screen_code String @unique @db.VarChar(50)
|
screen_code String @unique @db.VarChar(50)
|
||||||
table_name String @db.VarChar(100)
|
table_name String @db.VarChar(100)
|
||||||
company_code String @db.VarChar(50)
|
company_code String @db.VarChar(50)
|
||||||
description String? @db.Text
|
description String?
|
||||||
is_active String @default("Y") @db.Char(1)
|
is_active String @default("Y") @db.Char(1)
|
||||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
created_by String? @db.VarChar(50)
|
created_by String? @db.VarChar(50)
|
||||||
updated_date DateTime @default(now()) @db.Timestamp(6)
|
updated_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
updated_by String? @db.VarChar(50)
|
updated_by String? @db.VarChar(50)
|
||||||
|
layouts screen_layouts[]
|
||||||
// 관계
|
|
||||||
layouts screen_layouts[]
|
|
||||||
menu_assignments screen_menu_assignments[]
|
menu_assignments screen_menu_assignments[]
|
||||||
|
|
||||||
@@index([company_code])
|
@@index([company_code])
|
||||||
}
|
}
|
||||||
|
|
||||||
model screen_layouts {
|
model screen_layouts {
|
||||||
layout_id Int @id @default(autoincrement())
|
layout_id Int @id @default(autoincrement())
|
||||||
screen_id Int
|
screen_id Int
|
||||||
component_type String @db.VarChar(50)
|
component_type String @db.VarChar(50)
|
||||||
component_id String @unique @db.VarChar(100)
|
component_id String @unique @db.VarChar(100)
|
||||||
parent_id String? @db.VarChar(100)
|
parent_id String? @db.VarChar(100)
|
||||||
position_x Int
|
position_x Int
|
||||||
position_y Int
|
position_y Int
|
||||||
width Int
|
width Int
|
||||||
height Int
|
height Int
|
||||||
properties Json?
|
properties Json?
|
||||||
display_order Int @default(0)
|
display_order Int @default(0)
|
||||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
|
|
||||||
// 관계
|
|
||||||
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
|
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
|
||||||
widgets screen_widgets[]
|
widgets screen_widgets[]
|
||||||
|
|
||||||
|
|
@ -5096,99 +5023,82 @@ model screen_layouts {
|
||||||
}
|
}
|
||||||
|
|
||||||
model screen_widgets {
|
model screen_widgets {
|
||||||
widget_id Int @id @default(autoincrement())
|
widget_id Int @id @default(autoincrement())
|
||||||
layout_id Int
|
layout_id Int
|
||||||
table_name String @db.VarChar(100)
|
table_name String @db.VarChar(100)
|
||||||
column_name String @db.VarChar(100)
|
column_name String @db.VarChar(100)
|
||||||
widget_type String @db.VarChar(50)
|
widget_type String @db.VarChar(50)
|
||||||
label String? @db.VarChar(200)
|
label String? @db.VarChar(200)
|
||||||
placeholder String? @db.VarChar(200)
|
placeholder String? @db.VarChar(200)
|
||||||
is_required Boolean @default(false)
|
is_required Boolean @default(false)
|
||||||
is_readonly Boolean @default(false)
|
is_readonly Boolean @default(false)
|
||||||
validation_rules Json?
|
validation_rules Json?
|
||||||
display_properties Json?
|
display_properties Json?
|
||||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
|
layout screen_layouts @relation(fields: [layout_id], references: [layout_id], onDelete: Cascade)
|
||||||
// 관계
|
|
||||||
layout screen_layouts @relation(fields: [layout_id], references: [layout_id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([layout_id])
|
@@index([layout_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model screen_templates {
|
model screen_templates {
|
||||||
template_id Int @id @default(autoincrement())
|
template_id Int @id @default(autoincrement())
|
||||||
template_name String @db.VarChar(100)
|
template_name String @db.VarChar(100)
|
||||||
template_type String @db.VarChar(50)
|
template_type String @db.VarChar(50)
|
||||||
company_code String @db.VarChar(50)
|
company_code String @db.VarChar(50)
|
||||||
description String? @db.Text
|
description String?
|
||||||
layout_data Json?
|
layout_data Json?
|
||||||
is_public Boolean @default(false)
|
is_public Boolean @default(false)
|
||||||
created_by String? @db.VarChar(50)
|
created_by String? @db.VarChar(50)
|
||||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
|
|
||||||
@@index([company_code])
|
@@index([company_code])
|
||||||
}
|
}
|
||||||
|
|
||||||
model screen_menu_assignments {
|
model screen_menu_assignments {
|
||||||
assignment_id Int @id @default(autoincrement())
|
assignment_id Int @id @default(autoincrement())
|
||||||
screen_id Int
|
screen_id Int
|
||||||
menu_objid Decimal @db.Decimal
|
menu_objid Decimal @db.Decimal
|
||||||
company_code String @db.VarChar(50)
|
company_code String @db.VarChar(50)
|
||||||
display_order Int @default(0)
|
display_order Int @default(0)
|
||||||
is_active String @default("Y") @db.Char(1)
|
is_active String @default("Y") @db.Char(1)
|
||||||
created_date DateTime @default(now()) @db.Timestamp(6)
|
created_date DateTime @default(now()) @db.Timestamp(6)
|
||||||
created_by String? @db.VarChar(50)
|
created_by String? @db.VarChar(50)
|
||||||
|
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
|
||||||
// 관계
|
|
||||||
screen screen_definitions @relation(fields: [screen_id], references: [screen_id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([screen_id, menu_objid, company_code])
|
@@unique([screen_id, menu_objid, company_code])
|
||||||
@@index([company_code])
|
@@index([company_code])
|
||||||
}
|
}
|
||||||
|
|
||||||
// =====================================================
|
|
||||||
// 공통코드 관리 시스템 모델
|
|
||||||
// =====================================================
|
|
||||||
|
|
||||||
/// 공통코드 카테고리 테이블
|
/// 공통코드 카테고리 테이블
|
||||||
model code_category {
|
model code_category {
|
||||||
category_code String @id @db.VarChar(50)
|
category_code String @id @db.VarChar(50)
|
||||||
category_name String @db.VarChar(100)
|
category_name String @db.VarChar(100)
|
||||||
category_name_eng String? @db.VarChar(100)
|
category_name_eng String? @db.VarChar(100)
|
||||||
description String? @db.Text
|
description String?
|
||||||
sort_order Int @default(0)
|
sort_order Int? @default(0)
|
||||||
is_active String @default("Y") @db.Char(1)
|
is_active String? @default("Y") @db.Char(1)
|
||||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||||
created_by String? @db.VarChar(50)
|
created_by String? @db.VarChar(50)
|
||||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||||
updated_by String? @db.VarChar(50)
|
updated_by String? @db.VarChar(50)
|
||||||
|
codes code_info[]
|
||||||
// 관계 - 코드 상세 정보
|
|
||||||
codes code_info[]
|
|
||||||
|
|
||||||
@@index([is_active])
|
|
||||||
@@index([sort_order])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 공통코드 상세 정보 테이블
|
/// 공통코드 상세 정보 테이블
|
||||||
model code_info {
|
model code_info {
|
||||||
code_category String @db.VarChar(50)
|
code_category String @db.VarChar(50)
|
||||||
code_value String @db.VarChar(50)
|
code_value String @db.VarChar(50)
|
||||||
code_name String @db.VarChar(100)
|
code_name String @db.VarChar(100)
|
||||||
code_name_eng String? @db.VarChar(100)
|
code_name_eng String? @db.VarChar(100)
|
||||||
description String? @db.Text
|
description String?
|
||||||
sort_order Int @default(0)
|
sort_order Int? @default(0)
|
||||||
is_active String @default("Y") @db.Char(1)
|
is_active String? @default("Y") @db.Char(1)
|
||||||
created_date DateTime? @default(now()) @db.Timestamp(6)
|
created_date DateTime? @default(now()) @db.Timestamp(6)
|
||||||
created_by String? @db.VarChar(50)
|
created_by String? @db.VarChar(50)
|
||||||
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
updated_date DateTime? @default(now()) @db.Timestamp(6)
|
||||||
updated_by String? @db.VarChar(50)
|
updated_by String? @db.VarChar(50)
|
||||||
|
category code_category @relation(fields: [code_category], references: [category_code], onDelete: Cascade, map: "fk_code_info_category")
|
||||||
|
|
||||||
// 관계 - 코드 카테고리
|
@@id([code_category, code_value], map: "pk_code_info")
|
||||||
category code_category @relation(fields: [code_category], references: [category_code], onDelete: Cascade, onUpdate: Cascade)
|
@@index([code_category, sort_order], map: "idx_code_info_sort")
|
||||||
|
|
||||||
@@id([code_category, code_value])
|
|
||||||
@@index([code_category])
|
|
||||||
@@index([is_active])
|
|
||||||
@@index([code_category, sort_order])
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import multilangRoutes from "./routes/multilangRoutes";
|
||||||
import tableManagementRoutes from "./routes/tableManagementRoutes";
|
import tableManagementRoutes from "./routes/tableManagementRoutes";
|
||||||
import screenManagementRoutes from "./routes/screenManagementRoutes";
|
import screenManagementRoutes from "./routes/screenManagementRoutes";
|
||||||
import commonCodeRoutes from "./routes/commonCodeRoutes";
|
import commonCodeRoutes from "./routes/commonCodeRoutes";
|
||||||
|
import dynamicFormRoutes from "./routes/dynamicFormRoutes";
|
||||||
// import userRoutes from './routes/userRoutes';
|
// import userRoutes from './routes/userRoutes';
|
||||||
// import menuRoutes from './routes/menuRoutes';
|
// import menuRoutes from './routes/menuRoutes';
|
||||||
|
|
||||||
|
|
@ -67,6 +68,7 @@ app.use("/api/multilang", multilangRoutes);
|
||||||
app.use("/api/table-management", tableManagementRoutes);
|
app.use("/api/table-management", tableManagementRoutes);
|
||||||
app.use("/api/screen-management", screenManagementRoutes);
|
app.use("/api/screen-management", screenManagementRoutes);
|
||||||
app.use("/api/common-codes", commonCodeRoutes);
|
app.use("/api/common-codes", commonCodeRoutes);
|
||||||
|
app.use("/api/dynamic-form", dynamicFormRoutes);
|
||||||
// app.use('/api/users', userRoutes);
|
// app.use('/api/users', userRoutes);
|
||||||
// app.use('/api/menus', menuRoutes);
|
// app.use('/api/menus', menuRoutes);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 || "테이블 정보 조회에 실패했습니다.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -386,7 +386,7 @@ export async function updateColumnWebType(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { tableName, columnName } = req.params;
|
const { tableName, columnName } = req.params;
|
||||||
const { webType, detailSettings } = req.body;
|
const { webType, detailSettings, inputType } = req.body;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
|
`=== 컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType} ===`
|
||||||
|
|
@ -410,7 +410,8 @@ export async function updateColumnWebType(
|
||||||
tableName,
|
tableName,
|
||||||
columnName,
|
columnName,
|
||||||
webType,
|
webType,
|
||||||
detailSettings
|
detailSettings,
|
||||||
|
inputType
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -69,6 +69,7 @@ export class TableManagementService {
|
||||||
COALESCE(cl.column_label, c.column_name) as "displayName",
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
||||||
c.data_type as "dbType",
|
c.data_type as "dbType",
|
||||||
COALESCE(cl.web_type, 'text') as "webType",
|
COALESCE(cl.web_type, 'text') as "webType",
|
||||||
|
COALESCE(cl.input_type, 'direct') as "inputType",
|
||||||
COALESCE(cl.detail_settings, '') as "detailSettings",
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
||||||
COALESCE(cl.description, '') as "description",
|
COALESCE(cl.description, '') as "description",
|
||||||
c.is_nullable as "isNullable",
|
c.is_nullable as "isNullable",
|
||||||
|
|
@ -368,7 +369,8 @@ export class TableManagementService {
|
||||||
tableName: string,
|
tableName: string,
|
||||||
columnName: string,
|
columnName: string,
|
||||||
webType: string,
|
webType: string,
|
||||||
detailSettings?: Record<string, any>
|
detailSettings?: Record<string, any>,
|
||||||
|
inputType?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -394,30 +396,42 @@ export class TableManagementService {
|
||||||
|
|
||||||
if (existingColumn) {
|
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({
|
await prisma.column_labels.update({
|
||||||
where: {
|
where: {
|
||||||
id: existingColumn.id,
|
id: existingColumn.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: updateData,
|
||||||
web_type: webType,
|
|
||||||
detail_settings: JSON.stringify(finalDetailSettings),
|
|
||||||
updated_date: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
|
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
|
||||||
);
|
);
|
||||||
} else {
|
} 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({
|
await prisma.column_labels.create({
|
||||||
data: {
|
data: createData,
|
||||||
table_name: tableName,
|
|
||||||
column_name: columnName,
|
|
||||||
web_type: webType,
|
|
||||||
detail_settings: JSON.stringify(finalDetailSettings),
|
|
||||||
created_date: new Date(),
|
|
||||||
updated_date: new Date(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
`컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
`컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ export interface ColumnInfo {
|
||||||
columnLabel?: string;
|
columnLabel?: string;
|
||||||
dataType: string;
|
dataType: string;
|
||||||
webType?: WebType;
|
webType?: WebType;
|
||||||
|
inputType?: "direct" | "auto"; // 입력 타입
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
columnDefault?: string;
|
columnDefault?: string;
|
||||||
characterMaximumLength?: number;
|
characterMaximumLength?: number;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface ColumnTypeInfo {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
dbType: string;
|
dbType: string;
|
||||||
webType: string;
|
webType: string;
|
||||||
|
inputType?: "direct" | "auto";
|
||||||
detailSettings: string;
|
detailSettings: string;
|
||||||
description: string;
|
description: string;
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,10 @@ export default function ScreenViewPage() {
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
hideLabel={true} // 라벨 숨김 플래그 전달
|
hideLabel={true} // 라벨 숨김 플래그 전달
|
||||||
|
screenInfo={{
|
||||||
|
id: screenId,
|
||||||
|
tableName: screen?.tableName,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import {
|
||||||
ButtonTypeConfig,
|
ButtonTypeConfig,
|
||||||
} from "@/types/screen";
|
} from "@/types/screen";
|
||||||
import { InteractiveDataTable } from "./InteractiveDataTable";
|
import { InteractiveDataTable } from "./InteractiveDataTable";
|
||||||
|
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
interface InteractiveScreenViewerProps {
|
interface InteractiveScreenViewerProps {
|
||||||
component: ComponentData;
|
component: ComponentData;
|
||||||
|
|
@ -35,6 +37,10 @@ interface InteractiveScreenViewerProps {
|
||||||
formData?: Record<string, any>;
|
formData?: Record<string, any>;
|
||||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
|
screenInfo?: {
|
||||||
|
id: number;
|
||||||
|
tableName?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
|
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
|
||||||
|
|
@ -43,6 +49,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
formData: externalFormData,
|
formData: externalFormData,
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
hideLabel = false,
|
hideLabel = false,
|
||||||
|
screenInfo,
|
||||||
}) => {
|
}) => {
|
||||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||||
|
|
@ -683,28 +690,300 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
const widget = comp as WidgetComponent;
|
const widget = comp as WidgetComponent;
|
||||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = async () => {
|
||||||
if (config?.actionType === "popup" && config.popupTitle) {
|
const actionType = config?.actionType || "save";
|
||||||
alert(`${config.popupTitle}\n\n${config.popupContent || "팝업 내용이 없습니다."}`);
|
|
||||||
} else if (config?.actionType === "navigate" && config.navigateUrl) {
|
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") {
|
if (config.navigateTarget === "_blank") {
|
||||||
window.open(config.navigateUrl, "_blank");
|
window.open(config.navigateUrl, "_blank");
|
||||||
} else {
|
} else {
|
||||||
window.location.href = config.navigateUrl;
|
window.location.href = config.navigateUrl;
|
||||||
}
|
}
|
||||||
} else if (config?.actionType === "custom" && config.customAction) {
|
} else {
|
||||||
|
console.log("🔗 네비게이션 URL이 설정되지 않았습니다.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 커스텀 액션
|
||||||
|
const handleCustomAction = async () => {
|
||||||
|
if (config?.customAction) {
|
||||||
try {
|
try {
|
||||||
// 간단한 JavaScript 실행 (보안상 제한적)
|
// 보안상 제한적인 eval 사용
|
||||||
eval(config.customAction);
|
const result = eval(config.customAction);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
await result;
|
||||||
|
}
|
||||||
|
console.log("⚡ 커스텀 액션 실행 완료");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("커스텀 액션 실행 오류:", error);
|
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
|
||||||
}
|
|
||||||
} else if (config?.actionType === "delete" && config.confirmMessage) {
|
|
||||||
if (confirm(config.confirmMessage)) {
|
|
||||||
console.log("삭제 확인됨");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`버튼 클릭: ${config?.actionType || "기본"} 액션`);
|
console.log("⚡ 커스텀 액션이 설정되지 않았습니다.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,64 @@ const renderWidget = (component: ComponentData) => {
|
||||||
const widget = component as WidgetComponent;
|
const widget = component as WidgetComponent;
|
||||||
const config = widget.webTypeConfig as TextTypeConfig | undefined;
|
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";
|
const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text";
|
||||||
|
|
||||||
|
|
@ -139,12 +195,14 @@ const renderWidget = (component: ComponentData) => {
|
||||||
const inputProps = {
|
const inputProps = {
|
||||||
...commonProps,
|
...commonProps,
|
||||||
placeholder: finalPlaceholder,
|
placeholder: finalPlaceholder,
|
||||||
|
value: isAutoInput ? autoValue : undefined, // 자동입력인 경우 자동 값 표시
|
||||||
minLength: config?.minLength,
|
minLength: config?.minLength,
|
||||||
maxLength: config?.maxLength,
|
maxLength: config?.maxLength,
|
||||||
pattern: getPatternByFormat(config?.format || "none"),
|
pattern: getPatternByFormat(config?.format || "none"),
|
||||||
onInput: handleInputChange,
|
onInput: handleInputChange,
|
||||||
onChange: () => {}, // 읽기 전용으로 처리
|
onChange: () => {}, // 읽기 전용으로 처리
|
||||||
readOnly: true,
|
readOnly: readonly || isAutoInput, // 자동입력인 경우 읽기 전용
|
||||||
|
className: `w-full h-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// multiline이면 Textarea로 렌더링
|
// multiline이면 Textarea로 렌더링
|
||||||
|
|
@ -160,19 +218,69 @@ const renderWidget = (component: ComponentData) => {
|
||||||
const widget = component as WidgetComponent;
|
const widget = component as WidgetComponent;
|
||||||
const config = widget.webTypeConfig as NumberTypeConfig | undefined;
|
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("🔢 숫자 위젯 렌더링:", {
|
console.log("🔢 숫자 위젯 렌더링:", {
|
||||||
componentId: widget.id,
|
componentId: widget.id,
|
||||||
widgetType: widget.widgetType,
|
widgetType: widget.widgetType,
|
||||||
config,
|
config,
|
||||||
placeholder: widget.placeholder,
|
placeholder: widget.placeholder,
|
||||||
|
inputType: widget.inputType,
|
||||||
|
isAutoInput,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 단계값 결정: webTypeConfig > 기본값 (소수는 0.01, 정수는 1)
|
// 단계값 결정: webTypeConfig > 기본값 (소수는 0.01, 정수는 1)
|
||||||
const step = config?.step || (widgetType === "decimal" ? 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) => {
|
const formatValue = (value: string) => {
|
||||||
|
|
@ -215,9 +323,10 @@ const renderWidget = (component: ComponentData) => {
|
||||||
max={config?.max}
|
max={config?.max}
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
placeholder={finalPlaceholder}
|
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={() => {}} // 읽기 전용으로 처리
|
onChange={() => {}} // 읽기 전용으로 처리
|
||||||
readOnly
|
readOnly={readonly || isAutoInput}
|
||||||
/>
|
/>
|
||||||
{config.suffix && (
|
{config.suffix && (
|
||||||
<span className="rounded-r border border-l-0 bg-gray-50 px-2 py-2 text-sm text-gray-600">
|
<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}
|
max={config?.max}
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
placeholder={finalPlaceholder}
|
placeholder={finalPlaceholder}
|
||||||
|
value={isAutoInput ? autoValue : undefined} // 자동입력인 경우 자동 값 표시
|
||||||
|
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
|
||||||
onChange={() => {}} // 읽기 전용으로 처리
|
onChange={() => {}} // 읽기 전용으로 처리
|
||||||
readOnly
|
readOnly={readonly || isAutoInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +358,46 @@ const renderWidget = (component: ComponentData) => {
|
||||||
const widget = component as WidgetComponent;
|
const widget = component as WidgetComponent;
|
||||||
const config = widget.webTypeConfig as DateTypeConfig | undefined;
|
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 결정
|
// 웹타입 설정에 따른 input type 결정
|
||||||
let inputType = "date";
|
let inputType = "date";
|
||||||
if (config?.showTime || config?.format?.includes("HH:mm")) {
|
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 > 기본값
|
// 플레이스홀더 우선순위: webTypeConfig > placeholder > 기본값
|
||||||
const finalPlaceholder = config?.placeholder || placeholder || "날짜를 선택하세요";
|
const finalPlaceholder = isAutoInput
|
||||||
|
? getAutoDatePlaceholder(widget.autoValueType || "current_date")
|
||||||
|
: config?.placeholder || placeholder || "날짜를 선택하세요";
|
||||||
|
|
||||||
// 디버깅: 현재 설정값 확인
|
// 디버깅: 현재 설정값 확인
|
||||||
console.log("📅 날짜 위젯 렌더링:", {
|
console.log("📅 날짜 위젯 렌더링:", {
|
||||||
|
|
@ -331,9 +487,10 @@ const renderWidget = (component: ComponentData) => {
|
||||||
placeholder={finalPlaceholder}
|
placeholder={finalPlaceholder}
|
||||||
min={config?.minDate}
|
min={config?.minDate}
|
||||||
max={config?.maxDate}
|
max={config?.maxDate}
|
||||||
value={processedDefaultValue}
|
value={isAutoInput ? autoValue : processedDefaultValue}
|
||||||
|
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
|
||||||
onChange={() => {}} // 읽기 전용으로 처리
|
onChange={() => {}} // 읽기 전용으로 처리
|
||||||
readOnly
|
readOnly={readonly || isAutoInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -233,6 +259,7 @@ export default function TableTypeSelector({
|
||||||
<TableHead>라벨</TableHead>
|
<TableHead>라벨</TableHead>
|
||||||
<TableHead>데이터 타입</TableHead>
|
<TableHead>데이터 타입</TableHead>
|
||||||
<TableHead>웹 타입</TableHead>
|
<TableHead>웹 타입</TableHead>
|
||||||
|
<TableHead>입력 타입</TableHead>
|
||||||
<TableHead>필수</TableHead>
|
<TableHead>필수</TableHead>
|
||||||
<TableHead>표시</TableHead>
|
<TableHead>표시</TableHead>
|
||||||
<TableHead>액션</TableHead>
|
<TableHead>액션</TableHead>
|
||||||
|
|
@ -267,6 +294,20 @@ export default function TableTypeSelector({
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</TableCell>
|
</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>
|
<TableCell>
|
||||||
<Badge variant={column.isNullable === "NO" ? "default" : "secondary"}>
|
<Badge variant={column.isNullable === "NO" ? "default" : "secondary"}>
|
||||||
{column.isNullable === "NO" ? "필수" : "선택"}
|
{column.isNullable === "NO" ? "필수" : "선택"}
|
||||||
|
|
|
||||||
|
|
@ -48,23 +48,42 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
||||||
|
|
||||||
// 로컬 상태 관리
|
// 로컬 상태 관리
|
||||||
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>({
|
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>(() => {
|
||||||
actionType: "custom",
|
const defaultConfig = {
|
||||||
variant: "default",
|
actionType: "custom" as ButtonActionType,
|
||||||
size: "sm",
|
variant: "default" as ButtonVariant,
|
||||||
...config,
|
size: "sm" as ButtonSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultConfig,
|
||||||
|
...config, // 저장된 값이 기본값을 덮어씀
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
|
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
|
||||||
|
|
||||||
|
// 기본값 설정 (실제 값이 있으면 덮어쓰지 않음)
|
||||||
|
const defaultConfig = {
|
||||||
|
actionType: "custom" as ButtonActionType,
|
||||||
|
variant: "default" as ButtonVariant,
|
||||||
|
size: "sm" as ButtonSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 실제 저장된 값이 우선순위를 가지도록 설정
|
||||||
setLocalConfig({
|
setLocalConfig({
|
||||||
actionType: "custom",
|
...defaultConfig,
|
||||||
variant: "default",
|
...newConfig, // 저장된 값이 기본값을 덮어씀
|
||||||
size: "sm",
|
|
||||||
...newConfig,
|
|
||||||
});
|
});
|
||||||
}, [component.webTypeConfig]);
|
|
||||||
|
console.log("🔄 ButtonConfigPanel 로컬 상태 동기화:", {
|
||||||
|
componentId: component.id,
|
||||||
|
savedConfig: newConfig,
|
||||||
|
finalConfig: { ...defaultConfig, ...newConfig },
|
||||||
|
});
|
||||||
|
}, [component.webTypeConfig, component.id]);
|
||||||
|
|
||||||
// 설정 업데이트 함수
|
// 설정 업데이트 함수
|
||||||
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
|
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
|
||||||
|
|
@ -194,7 +213,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||||
break;
|
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);
|
const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Settings } from "lucide-react";
|
import { Settings } from "lucide-react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import {
|
import {
|
||||||
ComponentData,
|
ComponentData,
|
||||||
WidgetComponent,
|
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>
|
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">{widget.widgetType}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">컬럼: {widget.columnName}</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>
|
</div>
|
||||||
|
|
||||||
{/* 상세 설정 영역 */}
|
{/* 상세 설정 영역 */}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -152,10 +152,12 @@ export const tableTypeApi = {
|
||||||
columnName: string,
|
columnName: string,
|
||||||
webType: string,
|
webType: string,
|
||||||
detailSettings?: Record<string, any>,
|
detailSettings?: Record<string, any>,
|
||||||
|
inputType?: "direct" | "auto",
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
|
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
|
||||||
webType,
|
webType,
|
||||||
detailSettings,
|
detailSettings,
|
||||||
|
inputType,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,15 @@ export interface BaseComponent {
|
||||||
tableName?: string; // 테이블명 추가
|
tableName?: string; // 테이블명 추가
|
||||||
label?: string; // 라벨 추가
|
label?: string; // 라벨 추가
|
||||||
gridColumns?: number; // 그리드에서 차지할 컬럼 수 (1-12)
|
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;
|
dataType: string;
|
||||||
webType?: WebType;
|
webType?: WebType;
|
||||||
widgetType?: WebType; // 프론트엔드에서 사용하는 필드 (webType과 동일)
|
widgetType?: WebType; // 프론트엔드에서 사용하는 필드 (webType과 동일)
|
||||||
|
inputType?: "direct" | "auto"; // 입력 타입
|
||||||
isNullable: string;
|
isNullable: string;
|
||||||
required?: boolean; // isNullable에서 변환된 필드
|
required?: boolean; // isNullable에서 변환된 필드
|
||||||
columnDefault?: string;
|
columnDefault?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue