@bimatrix-aud-platform/aud_mcp_server 1.1.37 → 1.1.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,723 @@
1
+ import { generateId } from "../utils/uuid.js";
2
+ import { parseDocking } from "../utils/docking.js";
3
+ import { parseHexColor } from "../utils/color.js";
4
+ import { generateGridColumns } from "./grid-column.js";
5
+ // ============================================================
6
+ // 2. Schema-Compliant Style Helpers
7
+ // ============================================================
8
+ /** Transparent background (MTSD schema requires Color + flat RGBA) */
9
+ function emptyBg() {
10
+ return {
11
+ Color: { R: 0, G: 0, B: 0, A: 0 },
12
+ ColorR: 0, ColorG: 0, ColorB: 0, ColorA: 0,
13
+ };
14
+ }
15
+ /** Colored background from hex string */
16
+ function fullBg(hex) {
17
+ const c = parseHexColor(hex);
18
+ return {
19
+ Color: { R: c.R, G: c.G, B: c.B, A: 1 },
20
+ ColorR: c.R, ColorG: c.G, ColorB: c.B, ColorA: 1,
21
+ };
22
+ }
23
+ /** Transparent border */
24
+ function emptyBorder() {
25
+ return {
26
+ Color: { R: 0, G: 0, B: 0, A: 0 },
27
+ ColorR: 0, ColorG: 0, ColorB: 0, ColorA: 0,
28
+ CornerRadius: "0,0,0,0",
29
+ LineType: "none",
30
+ Thickness: "0,0,0,0",
31
+ };
32
+ }
33
+ /** Full border with options */
34
+ function fullBorder(opts = {}) {
35
+ const c = opts.color ? parseHexColor(opts.color) : { R: 224, G: 224, B: 224, A: 255 };
36
+ return {
37
+ Color: { R: c.R, G: c.G, B: c.B, A: 1 },
38
+ ColorR: c.R, ColorG: c.G, ColorB: c.B, ColorA: 1,
39
+ CornerRadius: opts.radius || "0,0,0,0",
40
+ LineType: opts.lineType || "solid",
41
+ Thickness: opts.thickness || "1,1,1,1",
42
+ };
43
+ }
44
+ /** Full font with options */
45
+ function fullFont(opts = {}) {
46
+ const c = opts.color ? parseHexColor(opts.color) : { R: 0, G: 0, B: 0, A: 255 };
47
+ return {
48
+ Color: { R: c.R, G: c.G, B: c.B, A: 1 },
49
+ Size: opts.size ?? 12,
50
+ Family: opts.family ?? "inherit",
51
+ Bold: opts.bold ?? false,
52
+ Italic: opts.italic ?? false,
53
+ UnderLine: false,
54
+ HorizontalAlignment: opts.align ?? "left",
55
+ VerticalAlignment: opts.valign ?? "middle",
56
+ };
57
+ }
58
+ /** Skin style (Type=0, no custom colors applied) */
59
+ function skinStyle() {
60
+ return {
61
+ Type: 0,
62
+ BoxStyle: "",
63
+ Background: emptyBg(),
64
+ Border: emptyBorder(),
65
+ Font: fullFont(),
66
+ };
67
+ }
68
+ /** Custom style (Type=2, colors applied) */
69
+ function customStyle(opts = {}) {
70
+ return {
71
+ Type: 2,
72
+ BoxStyle: "",
73
+ Background: opts.bg ? fullBg(opts.bg) : emptyBg(),
74
+ Border: opts.border ? fullBorder(opts.border) : emptyBorder(),
75
+ Font: fullFont(opts.font),
76
+ };
77
+ }
78
+ // ============================================================
79
+ // 3. Position & Docking Helper
80
+ // ============================================================
81
+ function pos(left, top, width, height, dock, margin) {
82
+ const docking = dock ? parseDocking(dock) : parseDocking("none");
83
+ if (margin)
84
+ docking.Margin = margin;
85
+ return {
86
+ Left: left, Top: top, Width: width, Height: height,
87
+ ZIndex: 1, TabIndex: 0, Docking: docking,
88
+ };
89
+ }
90
+ // ============================================================
91
+ // 4. Element Factory Functions
92
+ // ============================================================
93
+ function mkLabel(id, name, text, position, fontOpts) {
94
+ return {
95
+ Type: "Label",
96
+ Id: id || generateId("Label"),
97
+ Name: name,
98
+ Position: position,
99
+ Style: fontOpts ? customStyle({ font: fontOpts }) : skinStyle(),
100
+ LanguageCode: "",
101
+ Text: text,
102
+ Cursor: "default",
103
+ Formula: "",
104
+ UseTextOverflow: false,
105
+ UseAutoLineBreak: false,
106
+ LineSpacing: 0,
107
+ HasLineSpacing: false,
108
+ MxBinding: "",
109
+ MxBindingUseStyle: false,
110
+ };
111
+ }
112
+ function mkButton(id, name, text, position, styleOpts) {
113
+ return {
114
+ Type: "Button",
115
+ Id: id || generateId("Button"),
116
+ Name: name,
117
+ Position: position,
118
+ Style: styleOpts || skinStyle(),
119
+ LanguageCode: "",
120
+ Value: text,
121
+ Cursor: "pointer",
122
+ HasNewRadius: false,
123
+ };
124
+ }
125
+ function mkGroup(id, name, position, children, styleOpts) {
126
+ // Set InGroup on all children
127
+ const groupId = id || generateId("Group");
128
+ for (const child of children) {
129
+ child.InGroup = groupId;
130
+ }
131
+ return {
132
+ Type: "Group",
133
+ Id: groupId,
134
+ Name: name,
135
+ Position: position,
136
+ Style: styleOpts || skinStyle(),
137
+ ChildElements: children,
138
+ };
139
+ }
140
+ function mkCalendar(id, name, position) {
141
+ return {
142
+ Type: "Calendar",
143
+ Id: id || generateId("Calendar"),
144
+ Name: name,
145
+ Position: position,
146
+ Style: skinStyle(),
147
+ LanguageCode: "",
148
+ Value: "",
149
+ Text: "",
150
+ InitType: 0,
151
+ IsReadOnly: false,
152
+ CalendarType: 0,
153
+ UseButton: true,
154
+ AutoRefresh: false,
155
+ AfterRefresh: "",
156
+ DataSource: "",
157
+ MxBinding: "",
158
+ MxBindingUseStyle: false,
159
+ };
160
+ }
161
+ function mkDataGrid(id, name, position, columns, dsName) {
162
+ return {
163
+ Type: "DataGrid",
164
+ Id: id || generateId("DataGrid"),
165
+ Name: name,
166
+ Position: position,
167
+ Style: skinStyle(),
168
+ DataSource: dsName,
169
+ CellMargin: "2",
170
+ Columns: columns,
171
+ UsePPTExport: false,
172
+ AutoRefresh: false,
173
+ DoRefresh: false,
174
+ DoExport: false,
175
+ ColumnHeaderHeight: 30,
176
+ RowHeight: 26,
177
+ ShowHeader: true,
178
+ SelectRule: 0,
179
+ };
180
+ }
181
+ function mkChart(id, name, position, dsName) {
182
+ return {
183
+ Type: "Chart",
184
+ Id: id || generateId("Chart"),
185
+ Name: name,
186
+ Position: position,
187
+ Style: skinStyle(),
188
+ DataSource: dsName,
189
+ AutoRefresh: false,
190
+ DoRefresh: false,
191
+ DoExport: false,
192
+ PlotOptions: {},
193
+ };
194
+ }
195
+ function mkTextBox(id, name, position) {
196
+ return {
197
+ Type: "TextBox",
198
+ Id: id || generateId("TextBox"),
199
+ Name: name,
200
+ Position: position,
201
+ Style: skinStyle(),
202
+ LanguageCode: "",
203
+ Value: "",
204
+ Text: "",
205
+ IsReadOnly: false,
206
+ Formula: "",
207
+ MaxLength: 0,
208
+ MxBinding: "",
209
+ MxBindingUseStyle: false,
210
+ };
211
+ }
212
+ function mkComboBox(id, name, position) {
213
+ return {
214
+ Type: "ComboBox",
215
+ Id: id || generateId("ComboBox"),
216
+ Name: name,
217
+ Position: position,
218
+ Style: skinStyle(),
219
+ DataSource: "",
220
+ Value: "",
221
+ Text: "",
222
+ InitType: 0,
223
+ RefreshType: 0,
224
+ IsReadOnly: false,
225
+ SortType: 0,
226
+ AutoRefresh: false,
227
+ AfterRefresh: "",
228
+ UseAllItems: false,
229
+ UseAllItemsText: "",
230
+ DisplayType: 0,
231
+ DataSourceInfo: { LabelField: "", ValueField: "" },
232
+ InitValue: "",
233
+ };
234
+ }
235
+ // ============================================================
236
+ // 5. DataSource Factory
237
+ // ============================================================
238
+ function extractParamsFromSQL(sql) {
239
+ const paramSet = new Set();
240
+ const regex = /[@%]?:([A-Za-z_][A-Za-z0-9_]*)/g;
241
+ let match;
242
+ while ((match = regex.exec(sql)) !== null) {
243
+ paramSet.add(match[1]);
244
+ }
245
+ return Array.from(paramSet);
246
+ }
247
+ function mkDataSource(name, connection, sql, columns) {
248
+ const paramNames = extractParamsFromSQL(sql);
249
+ return {
250
+ Id: generateId("DS"),
251
+ Name: name,
252
+ ConnectionCode: connection,
253
+ DSType: 0,
254
+ UseMeta: false,
255
+ UseCache: false,
256
+ Encrypted: false,
257
+ SQL: sql,
258
+ Params: paramNames.map(n => ({
259
+ Name: n.startsWith(":") ? n : `:${n}`,
260
+ ParamType: "String",
261
+ })),
262
+ Columns: (columns || []).map(c => ({
263
+ Name: c.name,
264
+ Type: c.type || "string",
265
+ })),
266
+ };
267
+ }
268
+ // ============================================================
269
+ // 6. Script Generator
270
+ // ============================================================
271
+ function generateSearchListScript(input) {
272
+ const gridName = "GRD_MAIN";
273
+ return `import { Matrix } from "@AUD_CLIENT/control/Matrix";
274
+ import { Button } from "@AUD_CLIENT/control/Button";
275
+ import { Calendar } from "@AUD_CLIENT/control/Calendar";
276
+ import { DataGrid } from "@AUD_CLIENT/control/DataGrid";
277
+
278
+ let Matrix: Matrix;
279
+
280
+ Matrix.OnDocumentLoadComplete = function (sender, args) {
281
+ let btnSearch = Matrix.getObject("BTN_SEARCH") as Button;
282
+ btnSearch.OnClick = btnSearchOnClick;
283
+ };
284
+
285
+ const btnSearchOnClick = function (sender, args) {
286
+ let calFrom = Matrix.getObject("CAL_FROM") as Calendar;
287
+ let calTo = Matrix.getObject("CAL_TO") as Calendar;
288
+
289
+ Matrix.SetVariable("VS_YMD_FROM", calFrom.Value);
290
+ Matrix.SetVariable("VS_YMD_TO", calTo.Value);
291
+
292
+ Matrix.doRefresh(["${gridName}"]);
293
+ };
294
+ `;
295
+ }
296
+ function generateCrudScript(input) {
297
+ const gridName = "GRD_MAIN";
298
+ return `import { Matrix } from "@AUD_CLIENT/control/Matrix";
299
+ import { Button } from "@AUD_CLIENT/control/Button";
300
+ import { Calendar } from "@AUD_CLIENT/control/Calendar";
301
+ import { DataGrid } from "@AUD_CLIENT/control/DataGrid";
302
+
303
+ let Matrix: Matrix;
304
+
305
+ Matrix.OnDocumentLoadComplete = function (sender, args) {
306
+ let btnSearch = Matrix.getObject("BTN_SEARCH") as Button;
307
+ let btnSave = Matrix.getObject("BTN_SAVE") as Button;
308
+ let btnAddRow = Matrix.getObject("BTN_ADD_ROW") as Button;
309
+ let btnDelRow = Matrix.getObject("BTN_DEL_ROW") as Button;
310
+
311
+ btnSearch.OnClick = btnSearchOnClick;
312
+ btnSave.OnClick = btnSaveOnClick;
313
+ btnAddRow.OnClick = btnAddRowOnClick;
314
+ btnDelRow.OnClick = btnDelRowOnClick;
315
+ };
316
+
317
+ const btnSearchOnClick = function (sender, args) {
318
+ let calFrom = Matrix.getObject("CAL_FROM") as Calendar;
319
+ let calTo = Matrix.getObject("CAL_TO") as Calendar;
320
+
321
+ Matrix.SetVariable("VS_YMD_FROM", calFrom.Value);
322
+ Matrix.SetVariable("VS_YMD_TO", calTo.Value);
323
+
324
+ Matrix.doRefresh(["${gridName}"]);
325
+ };
326
+
327
+ const btnSaveOnClick = function (sender, args) {
328
+ let grd = Matrix.getObject("${gridName}") as DataGrid;
329
+ Matrix.RunScriptEx(["${gridName}"], "SVC_SAVE", {}, function (p) {
330
+ if (p.Success) {
331
+ Matrix.Alert("저장되었습니다.");
332
+ Matrix.doRefresh(["${gridName}"]);
333
+ }
334
+ });
335
+ };
336
+
337
+ const btnAddRowOnClick = function (sender, args) {
338
+ let grd = Matrix.getObject("${gridName}") as DataGrid;
339
+ grd.addRow();
340
+ };
341
+
342
+ const btnDelRowOnClick = function (sender, args) {
343
+ let grd = Matrix.getObject("${gridName}") as DataGrid;
344
+ let row = grd.getSelectedRowIndex();
345
+ if (row >= 0) {
346
+ grd.deleteRow(row);
347
+ }
348
+ };
349
+ `;
350
+ }
351
+ function generateMasterDetailScript(input) {
352
+ return `import { Matrix } from "@AUD_CLIENT/control/Matrix";
353
+ import { Button } from "@AUD_CLIENT/control/Button";
354
+ import { Calendar } from "@AUD_CLIENT/control/Calendar";
355
+ import { DataGrid } from "@AUD_CLIENT/control/DataGrid";
356
+
357
+ let Matrix: Matrix;
358
+
359
+ Matrix.OnDocumentLoadComplete = function (sender, args) {
360
+ let btnSearch = Matrix.getObject("BTN_SEARCH") as Button;
361
+ btnSearch.OnClick = btnSearchOnClick;
362
+
363
+ let grdMaster = Matrix.getObject("GRD_MASTER") as DataGrid;
364
+ grdMaster.OnSelectionChanged = grdMasterOnSelectionChanged;
365
+ };
366
+
367
+ const btnSearchOnClick = function (sender, args) {
368
+ let calFrom = Matrix.getObject("CAL_FROM") as Calendar;
369
+ let calTo = Matrix.getObject("CAL_TO") as Calendar;
370
+
371
+ Matrix.SetVariable("VS_YMD_FROM", calFrom.Value);
372
+ Matrix.SetVariable("VS_YMD_TO", calTo.Value);
373
+
374
+ Matrix.doRefresh(["GRD_MASTER"]);
375
+ };
376
+
377
+ const grdMasterOnSelectionChanged = function (sender, args) {
378
+ let grd = Matrix.getObject("GRD_MASTER") as DataGrid;
379
+ let row = grd.getSelectedRowIndex();
380
+ if (row < 0) return;
381
+
382
+ let dt = grd.getDataTable();
383
+ // TODO: Set detail key variable and refresh detail grid
384
+ // Matrix.SetVariable("VS_MASTER_KEY", dt.getRowValue(row, "KEY_COLUMN"));
385
+ // Matrix.doRefresh(["GRD_DETAIL"]);
386
+ };
387
+ `;
388
+ }
389
+ function generateDashboardScript(input) {
390
+ const kpiLabels = (input.kpiCards || []).map(k => k.valueLabel);
391
+ const kpiLines = kpiLabels.map(lbl => ` let ${lbl.replace("LBL_", "lbl")} = Matrix.getObject("${lbl}") as Label;`).join("\n");
392
+ return `import { Matrix } from "@AUD_CLIENT/control/Matrix";
393
+ import { Button } from "@AUD_CLIENT/control/Button";
394
+ import { Label } from "@AUD_CLIENT/control/Label";
395
+ import { Calendar } from "@AUD_CLIENT/control/Calendar";
396
+ import { DataGrid } from "@AUD_CLIENT/control/DataGrid";
397
+ import { Chart } from "@AUD_CLIENT/control/Chart";
398
+
399
+ let Matrix: Matrix;
400
+
401
+ Matrix.OnDocumentLoadComplete = function (sender, args) {
402
+ let btnSearch = Matrix.getObject("BTN_SEARCH") as Button;
403
+ btnSearch.OnClick = btnSearchOnClick;
404
+ };
405
+
406
+ Matrix.OnDataBindEnd = function (sender, args) {
407
+ updateKPI();
408
+ };
409
+
410
+ const btnSearchOnClick = function (sender, args) {
411
+ let calFrom = Matrix.getObject("CAL_FROM") as Calendar;
412
+ let calTo = Matrix.getObject("CAL_TO") as Calendar;
413
+
414
+ Matrix.SetVariable("VS_YMD_FROM", calFrom.Value);
415
+ Matrix.SetVariable("VS_YMD_TO", calTo.Value);
416
+
417
+ Matrix.doRefresh(["CHART_MAIN", "GRD_MAIN"]);
418
+ };
419
+
420
+ /** KPI 카드 값 업데이트 */
421
+ const updateKPI = function () {
422
+ let grd = Matrix.getObject("GRD_MAIN") as DataGrid;
423
+ let dt = grd.getDataTable();
424
+
425
+ if (dt == null || dt.GetRowCount() === 0) return;
426
+
427
+ // TODO: KPI 계산 로직을 업무에 맞게 수정하세요
428
+ ${kpiLines}
429
+ };
430
+
431
+ /** 숫자 콤마 포맷 */
432
+ const formatNumber = function (num: number): string {
433
+ return num.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ",");
434
+ };
435
+ `;
436
+ }
437
+ // ============================================================
438
+ // 7. Template Layout Builders
439
+ // ============================================================
440
+ function buildSearchListLayout(input) {
441
+ const warnings = [];
442
+ const elements = [];
443
+ // IDs
444
+ const grpHeaderId = generateId("Group");
445
+ const grpSearchId = generateId("Group");
446
+ // Header
447
+ const lblTitle = mkLabel(generateId("Label"), "LBL_TITLE", input.title || input.reportName, pos(15, 10, 300, 30), { size: 18, bold: true, color: "#333333" });
448
+ const btnSearch = mkButton(generateId("Button"), "BTN_SEARCH", "조회", pos(0, 10, 80, 30, "right", "0,0,15,0"));
449
+ btnSearch.Position.Docking.HoldSize = true;
450
+ const grpHeader = mkGroup(grpHeaderId, "GRP_HEADER", pos(0, 0, 1200, 50, "left+right"), [lblTitle, btnSearch], customStyle({
451
+ bg: "#F8F9FA",
452
+ border: { thickness: "0,0,1,0", color: "#E0E0E0" },
453
+ }));
454
+ elements.push(grpHeader);
455
+ // Search Conditions
456
+ const lblFrom = mkLabel(generateId("Label"), "LBL_FROM", "조회기간", pos(15, 10, 60, 25), { size: 12, color: "#555555" });
457
+ const calFrom = mkCalendar(generateId("Calendar"), "CAL_FROM", pos(80, 8, 130, 28));
458
+ const lblWave = mkLabel(generateId("Label"), "LBL_WAVE", "~", pos(215, 10, 15, 25), { size: 12, align: "center" });
459
+ const calTo = mkCalendar(generateId("Calendar"), "CAL_TO", pos(235, 8, 130, 28));
460
+ const grpSearch = mkGroup(grpSearchId, "GRP_SEARCH", pos(0, 50, 1200, 45, "left+right"), [lblFrom, calFrom, lblWave, calTo], customStyle({ border: { thickness: "0,0,1,0", color: "#E0E0E0" } }));
461
+ elements.push(grpSearch);
462
+ // Grid
463
+ const gridCols = input.gridColumns
464
+ ? generateGridColumns({ columns: input.gridColumns }).columns
465
+ : [];
466
+ const dsName = input.dataSources[0]?.name || "DS_GRID";
467
+ const grdMain = mkDataGrid(generateId("DataGrid"), "GRD_MAIN", pos(0, 95, 1200, 500, "left+right+bottom"), gridCols, dsName);
468
+ elements.push(grdMain);
469
+ return { elements, warnings };
470
+ }
471
+ function buildCrudLayout(input) {
472
+ const warnings = [];
473
+ const elements = [];
474
+ const grpHeaderId = generateId("Group");
475
+ const grpSearchId = generateId("Group");
476
+ // Header with multiple buttons
477
+ const lblTitle = mkLabel(generateId("Label"), "LBL_TITLE", input.title || input.reportName, pos(15, 10, 300, 30), { size: 18, bold: true, color: "#333333" });
478
+ const btnSearch = mkButton(generateId("Button"), "BTN_SEARCH", "조회", pos(0, 10, 80, 30, "right", "0,0,275,0"));
479
+ btnSearch.Position.Docking.HoldSize = true;
480
+ const btnSave = mkButton(generateId("Button"), "BTN_SAVE", "저장", pos(0, 10, 80, 30, "right", "0,0,190,0"));
481
+ btnSave.Position.Docking.HoldSize = true;
482
+ const btnAddRow = mkButton(generateId("Button"), "BTN_ADD_ROW", "행추가", pos(0, 10, 80, 30, "right", "0,0,105,0"));
483
+ btnAddRow.Position.Docking.HoldSize = true;
484
+ const btnDelRow = mkButton(generateId("Button"), "BTN_DEL_ROW", "행삭제", pos(0, 10, 80, 30, "right", "0,0,15,0"));
485
+ btnDelRow.Position.Docking.HoldSize = true;
486
+ const grpHeader = mkGroup(grpHeaderId, "GRP_HEADER", pos(0, 0, 1200, 50, "left+right"), [lblTitle, btnSearch, btnSave, btnAddRow, btnDelRow], customStyle({
487
+ bg: "#F8F9FA",
488
+ border: { thickness: "0,0,1,0", color: "#E0E0E0" },
489
+ }));
490
+ elements.push(grpHeader);
491
+ // Search
492
+ const lblFrom = mkLabel(generateId("Label"), "LBL_FROM", "조회기간", pos(15, 10, 60, 25), { size: 12, color: "#555555" });
493
+ const calFrom = mkCalendar(generateId("Calendar"), "CAL_FROM", pos(80, 8, 130, 28));
494
+ const lblWave = mkLabel(generateId("Label"), "LBL_WAVE", "~", pos(215, 10, 15, 25), { size: 12, align: "center" });
495
+ const calTo = mkCalendar(generateId("Calendar"), "CAL_TO", pos(235, 8, 130, 28));
496
+ const grpSearch = mkGroup(grpSearchId, "GRP_SEARCH", pos(0, 50, 1200, 45, "left+right"), [lblFrom, calFrom, lblWave, calTo], customStyle({ border: { thickness: "0,0,1,0", color: "#E0E0E0" } }));
497
+ elements.push(grpSearch);
498
+ // Grid (editable)
499
+ const gridCols = input.gridColumns
500
+ ? generateGridColumns({ columns: input.gridColumns.map(c => ({ ...c, editable: true })) }).columns
501
+ : [];
502
+ const dsName = input.dataSources[0]?.name || "DS_GRID";
503
+ const grdMain = mkDataGrid(generateId("DataGrid"), "GRD_MAIN", pos(0, 95, 1200, 500, "left+right+bottom"), gridCols, dsName);
504
+ elements.push(grdMain);
505
+ return { elements, warnings };
506
+ }
507
+ function buildMasterDetailLayout(input) {
508
+ const warnings = [];
509
+ const elements = [];
510
+ const grpHeaderId = generateId("Group");
511
+ const grpSearchId = generateId("Group");
512
+ // Header
513
+ const lblTitle = mkLabel(generateId("Label"), "LBL_TITLE", input.title || input.reportName, pos(15, 10, 300, 30), { size: 18, bold: true, color: "#333333" });
514
+ const btnSearch = mkButton(generateId("Button"), "BTN_SEARCH", "조회", pos(0, 10, 80, 30, "right", "0,0,15,0"));
515
+ btnSearch.Position.Docking.HoldSize = true;
516
+ const grpHeader = mkGroup(grpHeaderId, "GRP_HEADER", pos(0, 0, 1200, 50, "left+right"), [lblTitle, btnSearch], customStyle({
517
+ bg: "#F8F9FA",
518
+ border: { thickness: "0,0,1,0", color: "#E0E0E0" },
519
+ }));
520
+ elements.push(grpHeader);
521
+ // Search
522
+ const lblFrom = mkLabel(generateId("Label"), "LBL_FROM", "조회기간", pos(15, 10, 60, 25), { size: 12, color: "#555555" });
523
+ const calFrom = mkCalendar(generateId("Calendar"), "CAL_FROM", pos(80, 8, 130, 28));
524
+ const lblWave = mkLabel(generateId("Label"), "LBL_WAVE", "~", pos(215, 10, 15, 25), { size: 12, align: "center" });
525
+ const calTo = mkCalendar(generateId("Calendar"), "CAL_TO", pos(235, 8, 130, 28));
526
+ const grpSearch = mkGroup(grpSearchId, "GRP_SEARCH", pos(0, 50, 1200, 45, "left+right"), [lblFrom, calFrom, lblWave, calTo], customStyle({ border: { thickness: "0,0,1,0", color: "#E0E0E0" } }));
527
+ elements.push(grpSearch);
528
+ // Master grid (left side, ~60% width)
529
+ const gridCols = input.gridColumns
530
+ ? generateGridColumns({ columns: input.gridColumns }).columns
531
+ : [];
532
+ const masterDsName = input.dataSources[0]?.name || "DS_MASTER";
533
+ const grdMaster = mkDataGrid(generateId("DataGrid"), "GRD_MASTER", pos(0, 95, 700, 500, "left+bottom"), gridCols, masterDsName);
534
+ elements.push(grdMaster);
535
+ // Detail panel (right side)
536
+ const grpDetailId = generateId("Group");
537
+ const lblDetailTitle = mkLabel(generateId("Label"), "LBL_DETAIL_TITLE", "상세 정보", pos(15, 10, 200, 25), { size: 14, bold: true, color: "#333333" });
538
+ const grpDetail = mkGroup(grpDetailId, "GRP_DETAIL", pos(710, 95, 490, 500, "right+bottom", "0,0,0,0"), [lblDetailTitle], customStyle({
539
+ border: { thickness: "1,0,0,0", color: "#E0E0E0" },
540
+ }));
541
+ grpDetail.Position.Docking.HoldSize = true;
542
+ elements.push(grpDetail);
543
+ warnings.push("master-detail: 상세 패널(GRP_DETAIL)에 업무에 맞는 컨트롤을 추가해주세요.");
544
+ return { elements, warnings };
545
+ }
546
+ function buildDashboardLayout(input) {
547
+ const warnings = [];
548
+ const elements = [];
549
+ const grpHeaderId = generateId("Group");
550
+ const grpSearchId = generateId("Group");
551
+ const grpKpiId = generateId("Group");
552
+ // Header
553
+ const lblTitle = mkLabel(generateId("Label"), "LBL_TITLE", input.title || input.reportName, pos(15, 10, 300, 30), { size: 18, bold: true, color: "#333333" });
554
+ const btnSearch = mkButton(generateId("Button"), "BTN_SEARCH", "조회", pos(0, 10, 80, 30, "right", "0,0,15,0"));
555
+ btnSearch.Position.Docking.HoldSize = true;
556
+ const grpHeader = mkGroup(grpHeaderId, "GRP_HEADER", pos(0, 0, 1200, 50, "left+right"), [lblTitle, btnSearch], customStyle({
557
+ bg: "#F8F9FA",
558
+ border: { thickness: "0,0,1,0", color: "#E0E0E0" },
559
+ }));
560
+ elements.push(grpHeader);
561
+ // Search
562
+ const lblFrom = mkLabel(generateId("Label"), "LBL_FROM", "조회기간", pos(15, 10, 60, 25), { size: 12, color: "#555555" });
563
+ const calFrom = mkCalendar(generateId("Calendar"), "CAL_FROM", pos(80, 8, 130, 28));
564
+ const lblWave = mkLabel(generateId("Label"), "LBL_WAVE", "~", pos(215, 10, 15, 25), { size: 12, align: "center" });
565
+ const calTo = mkCalendar(generateId("Calendar"), "CAL_TO", pos(235, 8, 130, 28));
566
+ const grpSearch = mkGroup(grpSearchId, "GRP_SEARCH", pos(0, 50, 1200, 45, "left+right"), [lblFrom, calFrom, lblWave, calTo], customStyle({ border: { thickness: "0,0,1,0", color: "#E0E0E0" } }));
567
+ elements.push(grpSearch);
568
+ // KPI Cards
569
+ const defaultKpiCards = [
570
+ { title: "총매출", valueLabel: "LBL_KPI_VAL1", color: "#4A90D9" },
571
+ { title: "주문건수", valueLabel: "LBL_KPI_VAL2", color: "#50C878" },
572
+ { title: "고객수", valueLabel: "LBL_KPI_VAL3", color: "#FFB347" },
573
+ { title: "달성률", valueLabel: "LBL_KPI_VAL4", color: "#FF6B6B" },
574
+ ];
575
+ const kpiCards = input.kpiCards && input.kpiCards.length > 0
576
+ ? input.kpiCards
577
+ : defaultKpiCards;
578
+ const kpiChildren = [];
579
+ const cardWidth = 270;
580
+ const cardGap = 20;
581
+ kpiCards.forEach((card, i) => {
582
+ const cardX = 15 + i * (cardWidth + cardGap);
583
+ const cardColor = card.color || ["#4A90D9", "#50C878", "#FFB347", "#FF6B6B"][i % 4];
584
+ const cardId = generateId("Group");
585
+ const lblCardTitle = mkLabel(generateId("Label"), `LBL_KPI_TTL${i + 1}`, card.title, pos(15, 8, 240, 22), { size: 12, color: "#666666" });
586
+ const lblCardValue = mkLabel(generateId("Label"), card.valueLabel || `LBL_KPI_VAL${i + 1}`, "-", pos(15, 35, 240, 40), { size: 24, bold: true, color: cardColor });
587
+ const kpiCard = mkGroup(cardId, `GRP_KPI_CARD${i + 1}`, pos(cardX, 10, cardWidth, 90), [lblCardTitle, lblCardValue], customStyle({
588
+ bg: "#FFFFFF",
589
+ border: { thickness: "1,1,1,1", color: "#E0E0E0", radius: "8,8,8,8" },
590
+ }));
591
+ kpiChildren.push(kpiCard);
592
+ });
593
+ const grpKpi = mkGroup(grpKpiId, "GRP_KPI", pos(0, 95, 1200, 120, "left+right"), kpiChildren);
594
+ elements.push(grpKpi);
595
+ // Chart (left)
596
+ const chartDsName = input.dataSources.find(d => d.name.toUpperCase().includes("CHART"))?.name
597
+ || input.dataSources[0]?.name || "DS_CHART";
598
+ const chartMain = mkChart(generateId("Chart"), "CHART_MAIN", pos(5, 220, 595, 380, "left+bottom"), chartDsName);
599
+ elements.push(chartMain);
600
+ // Grid (right)
601
+ const gridCols = input.gridColumns
602
+ ? generateGridColumns({ columns: input.gridColumns }).columns
603
+ : [];
604
+ const gridDsName = input.dataSources.find(d => d.name.toUpperCase().includes("GRID"))?.name
605
+ || input.dataSources[input.dataSources.length > 1 ? 1 : 0]?.name || "DS_GRID";
606
+ const grdMain = mkDataGrid(generateId("DataGrid"), "GRD_MAIN", pos(605, 220, 590, 380, "left+right+bottom", "600,0,5,5"), gridCols, gridDsName);
607
+ elements.push(grdMain);
608
+ return { elements, warnings };
609
+ }
610
+ // ============================================================
611
+ // 8. Main Entry
612
+ // ============================================================
613
+ export function generateReportTemplate(input) {
614
+ const warnings = [];
615
+ const reportCode = generateId("REP");
616
+ const formId = generateId("Form");
617
+ const title = input.title || input.reportName;
618
+ const moduleCode = input.moduleCode || "SD";
619
+ // --- DataSources ---
620
+ const dataSources = input.dataSources.map(ds => mkDataSource(ds.name, input.connection, ds.sql, ds.columns));
621
+ // --- Layout ---
622
+ let layoutResult;
623
+ switch (input.templateType) {
624
+ case "search-list":
625
+ layoutResult = buildSearchListLayout(input);
626
+ break;
627
+ case "crud":
628
+ layoutResult = buildCrudLayout(input);
629
+ break;
630
+ case "master-detail":
631
+ layoutResult = buildMasterDetailLayout(input);
632
+ break;
633
+ case "dashboard":
634
+ layoutResult = buildDashboardLayout(input);
635
+ break;
636
+ default:
637
+ layoutResult = buildSearchListLayout(input);
638
+ warnings.push(`알 수 없는 템플릿 타입 '${input.templateType}'. search-list로 대체합니다.`);
639
+ }
640
+ warnings.push(...layoutResult.warnings);
641
+ // --- Script ---
642
+ let script;
643
+ switch (input.templateType) {
644
+ case "search-list":
645
+ script = generateSearchListScript(input);
646
+ break;
647
+ case "crud":
648
+ script = generateCrudScript(input);
649
+ break;
650
+ case "master-detail":
651
+ script = generateMasterDetailScript(input);
652
+ break;
653
+ case "dashboard":
654
+ script = generateDashboardScript(input);
655
+ break;
656
+ default:
657
+ script = generateSearchListScript(input);
658
+ }
659
+ // --- MTSD ---
660
+ const mtsd = {
661
+ ReportInfo: {
662
+ ReportCode: reportCode,
663
+ FolderCode: "",
664
+ SavePath: `/${reportCode}.mtsd`,
665
+ ReportName: input.reportName,
666
+ Writer: "",
667
+ WriteDate: "",
668
+ Editor: "",
669
+ EditDate: "",
670
+ TabPosition: 0,
671
+ UsePersonalConditions: false,
672
+ DocumentVersion: "3.0.0.0",
673
+ RefreshType: 0,
674
+ },
675
+ DataSources: { Datas: dataSources },
676
+ ScriptText: "",
677
+ ServerScriptText: [],
678
+ Forms: [
679
+ {
680
+ Id: formId,
681
+ Name: "Form1",
682
+ Activated: true,
683
+ Visible: true,
684
+ LanguageCode: "",
685
+ Style: {
686
+ Type: 0,
687
+ BoxStyle: "",
688
+ Background: emptyBg(),
689
+ Border: emptyBorder(),
690
+ Font: fullFont(),
691
+ },
692
+ Elements: layoutResult.elements,
693
+ },
694
+ ],
695
+ MetaDataSources: {
696
+ TemplateMeta: { TemplateName: "" },
697
+ MetaDataSources: [],
698
+ },
699
+ EXECUTION_PLANS: [],
700
+ Variables: [],
701
+ Modules: [],
702
+ ResponsiveLayout: [],
703
+ Langs: [],
704
+ WorkFlowModules: [],
705
+ WorkFlowInfo: "",
706
+ };
707
+ // --- SQL files ---
708
+ const sqlFiles = input.dataSources.map(ds => ({
709
+ name: ds.name,
710
+ sql: ds.sql,
711
+ }));
712
+ return {
713
+ mtsd,
714
+ script,
715
+ dataSources: sqlFiles,
716
+ reportInfo: {
717
+ ReportCode: reportCode,
718
+ ReportName: input.reportName,
719
+ ModuleCode: moduleCode,
720
+ },
721
+ warnings,
722
+ };
723
+ }