| name | iaud-schedulegrid-guide |
| description | i-AUD ScheduleGrid(Gantt Chart / 스케줄 그리드) 개발 가이드. AddIn 컴포넌트로 동작하는 간트 차트 컨트롤의 초기화, 데이터 바인딩, 시간축 설정(Major/Minor Unit), 행렬 방향(TransposeType), 셀 이벤트(OnDrawCell, OnEndDrag, OnContextMenuOpenning), 셀 조작(Split/Merge/Delete/Lock), DataGrid 양방향 동기화, 엑셀/PDF 내보내기 등을 안내합니다. "ScheduleGrid", "Gantt Chart", "간트 차트", "스케줄 그리드", "ScheduleCell", "TransposeType", "시간축", "Major Unit", "Minor Unit", "셀 분할", "셀 병합", "셀 잠금", "FocusCell", "OnDrawCell", "OnEndDrag", "ContextMenu", "SplitCell", "MergeCell" 등을 물어볼 때 사용하세요. |
i-AUD ScheduleGrid (Gantt Chart) 개발 가이드
1. 개요
ScheduleGrid는 시간축 기반으로 작업/일정 셀을 표시하는 Canvas 기반 간트 차트 컴포넌트입니다. AddIn 컴포넌트 형태로 보고서에 배치되며, ClassName은 BIMatrix.ScheduleGrid.ScheduleGrid입니다.
[AddIn(Component="ScheduleGrid")]
└── ScheduleGrid (main)
├── Model (ScheduleModel — 데이터/옵션)
│ ├── OptionData (시간축, 필드 매핑, 기간)
│ ├── OptionView (셀 크기, 행 높이)
│ ├── OptionStyle (BoxStyle 키, 색상)
│ └── Selection (선택된 셀들)
├── View (ScheduleView — 렌더링/상호작용)
└── ContextMenu (우클릭 메뉴)
타입 정의 위치
| 구분 | 경로 |
|---|
| ScheduleGrid | types/aud/ext/schedule/ScheduleGrid.ts |
| ScheduleModel | types/aud/ext/schedule/ScheduleModel.ts |
| ScheduleView | types/aud/ext/schedule/ScheduleView.ts |
| ScheduleCell | types/aud/ext/schedule/ScheduleCell.ts |
| ScheduleRow | types/aud/ext/schedule/ScheduleRow.ts |
| OptionData/View/Style | types/aud/ext/schedule/OptionData.ts 등 |
| TimeUnit | types/aud/ext/schedule/TimeUnit.ts |
| LineSchedule | types/aud/ext/schedule/LineSchedule.ts |
| enum (TimeUnit, SortType, TransposeType 등) | types/aud/enums/schedule/ |
2. MTSD ComponentElement.Model 구조
ScheduleGrid는 AddIn Element로 배치됩니다. ComponentElement.Model 하위에 Fields / OptionData / OptionView / OptionStyle 4개 파트로 구성됩니다.
2.1 Fields — 열 헤더 영역에 표시할 필드
"Fields": [
{ "Name": "PLANT_NAME", "Caption": "공정이름", "Width": 150, "Height": 120,
"Sortable": true, "Filterable": false, "ShowSubTotalRow": false },
{ "Name": "LINE_NAME", "Caption": "라인이름", "Width": 120, "Height": 120, ... },
{ "Name": "DATE_FROM_TEXT", ...
...
- 모든 DataSource 컬럼을 Fields에 등록해야
cell.DataRow.GetValue("XXX")로 접근할 수 있습니다.
Width는 RowHeader 영역에서 해당 필드의 가로 너비. Height는 컬럼 헤더 세로 높이.
Sortable: 헤더 클릭 정렬 여부. Filterable: 필터 아이콘 표시 여부.
2.2 OptionData — 필드 매핑과 시간축 설정
"OptionData": {
"HeaderFieldNames": "PLANT_NAME,LINE_NAME",
"FromFieldName": "DATE_FROM_TEXT",
"ToFieldName": "DATE_TO_TEXT",
"ColorFieldName": "COLOR",
"LabelFieldName": "DURATION",
"ToolTipFieldName":"TOOLTIP",
"DateTimeFormat": "yyyy-MM-dd HH:mm:ss",
"MoveAbleKeyFieldName": "PLANT_NAME",
"GroupFieldName": "GROUP_NAME",
"DisplayFromDate": "20170101000000",
...
enTimeUnit 값: 0:Second 1:Minute 2:Hour 3:Day 4:Month 5:Year 6:Quarter 7:Week 8:DayNight 9:HalfHour 10:Text
2.3 OptionView — 크기/여백
"OptionView": {
"ColumnHeaderHeight": 23,
"RowHeight": 30,
"CellWidth": 5,
"LabelMargin": 5,
"BoxMargin": 2,
"BoxLabelMargin": 10,
"LineHeight": 5
}
2.4 OptionStyle — BoxStyle 키 + 색상
"OptionStyle": {
"ColumnHeaderBoxStyleKey": "BX8FFB5C2489BC4D0AAB7E393442ABB119",
"RowHeaderBoxStyleKey": "BXDD80F1DA2A524BDABB6E663CB6DD95D6",
"GuideAreaBoxStyleKey": "BXA56CA4F06973411B8F71600E18D5617C",
"SubTotalHeaderBoxStyleKey": "BX1C85207526164A5F89B469BB2B68484D",
"SubTotalDataBoxStyleKey": "BXDE549298837943D79F81D950BF074E02",
"GrandTotalHeaderBoxStyleKeys": ["BX5E6066E92DC4468C96C33F3695775AD7"],
"GrandTotalDataBoxStyleKeys": [],
"LineBrush": { "Color": { "R": 170, "G": 170, "B": 172, "A"
BoxStyle 키가 서버에 없으면 기본 색상이 사용됩니다 (최근 패치로 null 체크됨).
3. 기본 사용법
3.1 인스턴스 획득 (OnLoadComplete에서)
let grid = null;
const OnLoadComplete = function(sender, args) {
try {
let ganttChartAddIn = Matrix.getObject("GanttChart");
grid = ganttChartAddIn.getScriptClass("BIMatrix.ScheduleGrid.ScheduleGrid");
grid.Model.OptionData.CellMoveMode = 0;
grid.View.OnContextMenuOpenning = function() { ... };
} catch (ex) {
Matrix.Alert(ex.message);
}
};
반드시 getScriptClass("BIMatrix.ScheduleGrid.ScheduleGrid")로 꺼내 사용합니다. AddIn 객체를 직접 사용하면 ScheduleGrid API에 접근할 수 없습니다.
3.2 데이터 바인딩 — 3 steps 패턴
const inquiry = function() {
Matrix.Execute("Data1", function(p) {
if (p.Success == false) { Matrix.Alert(p.Message); return; }
let ds = p.DataSet;
grid.SetDataSet(ds);
grid.Calculate(false);
grid.Update();
});
};
핵심: 데이터/옵션을 바꾼 뒤에는 반드시 Calculate(false) → Update() 순서로 호출합니다. Update()만 호출하면 시간축/셀 위치가 재계산되지 않습니다.
4. 행렬 방향 (TransposeType)
grid.Model.OptionData.TransposeType = 1;
grid.Transpose();
grid.Calculate(false);
grid.Update();
| TransposeType | 설명 | 예시 용도 |
|---|
| 0 (Right, 기본) | 시간축이 가로, 행이 세로로 쌓임 | 장기간 프로젝트 일정 (분기별/월별) |
| 1 (Bottom) | 시간축이 세로, 열이 가로로 쌓임 | 일일 설비 가동 스케줄 (시간별) |
5. ContextMenu 커스터마이징
grid.View.OnContextMenuOpenning에 함수를 할당하면 우클릭 시 호출됩니다. grid.ContextMenu.Clear()로 초기화 후 AddMenu("라벨", 콜백)으로 항목을 추가합니다.
grid.View.OnContextMenuOpenning = function() {
grid.ContextMenu.Clear();
if (grid.Model.Selection.Cells.length > 0) {
grid.ContextMenu.AddMenu("Summary", function() {
if (grid.View.MenuCell) {
let dataRow = grid.View.MenuCell.DataRow;
Matrix.getObject("lblPlanCode").Text = dataRow.GetValue("PLAN_CODE");
formPopup = Matrix.ShowWindow("Form2", 0, 0, 300, 400, true, true, "Summary", true, null, null, null);
}
});
grid.ContextMenu.AddMenu("Lock Selected Cells", function() {
( i = ; i < grid....; i++) {
cell = grid...[i];
cell..(, );
cell..(, );
}
grid.();
});
grid..(, () {
( i = ; i < grid....; i++) {
cell = grid...[i];
cell..(, );
cell..(, cell..());
}
grid.();
});
}
(grid.... > ) {
grid..(, () { ... });
}
};
grid.View.MenuCell: 우클릭 대상 단일 셀
grid.Model.Selection.Cells: 현재 선택된 셀 배열 (Shift/Ctrl 다중 선택)
- 메뉴를 숨기려면
AddMenu를 호출하지 않으면 됩니다 (조건부 분기).
6. 셀 조작 API
6.1 셀 분할 (SplitCell) — 수량 기반 분할 예시
let total = grid.View.MenuCell.DataRow.GetValue("DURATION");
let firstCellDuration = Number(Matrix.getObject("txtSplitCount").Value);
let secondCellDuration = total - firstCellDuration;
let firstCellWidth = Math.round(
(grid.View.MenuCell.Width / cellWidth.Text) * (firstCellDuration / total)
);
let cells = grid.Model.SplitCell(grid.View.MenuCell, firstCellWidth);
if (cells.length > 0) {
cells[0].DataRow.SetValue("DURATION", firstCellDuration);
cells[1].DataRow.SetValue("DURATION", secondCellDuration);
}
grid.Update();
6.2 셀 병합 (MergeCell)
grid.Model.MergeCell(grid.Model.Selection.Cells);
grid.Update();
6.3 셀 삭제 (DeleteCell)
grid.Model.Selection.Cells.forEach(function(cell) {
grid.Model.DeleteCell(cell);
});
grid.Update();
6.4 행(row)/셀 추가 — DataSet에 직접 AppendRow
const OnButtonClick = function(sender, args) {
if (args.Id == "btnAdd") {
let dataTable = grid.Model.DataSet.GetTable(0);
for (let i = 0; i < 2; i++) {
let rIdx = dataTable.AppendRow();
let row = dataTable.GetRow(rIdx);
row.RowState = "N";
let uniqueKey = Matrix.getUniqueKey("P");
row.SetValue("PLAN_CODE", uniqueKey);
row.SetValue("PLANT_NAME", "구로");
row.SetValue("LINE_NAME", "W100");
row.SetValue("COLOR", "yellow");
row.SetValue("DURATION", "test 추가");
}
grid.SetDataSet(grid.Model.DataSet);
grid.Calculate(false);
grid.Update();
grid..(, uniqueKey);
}
};
6.5 FocusCell — 특정 값 필드로 스크롤/선택 이동
grid.View.FocusCell("PLAN_CODE", "P0D15BCB2C9F547FCBEB9C3E6225A3CBA");
7. 이벤트 처리
7.1 OnDrawCell — 셀 커스텀 렌더링
셀이 그려질 때마다 호출됩니다. args.Context(ContextWrapper = HTML5 Canvas 래퍼)로 직접 그릴 수 있습니다.
const OnDrawCell = function(sender, args) {
let fromText = args.Cell.DataRow.GetValue("DATE_FROM_TEXT").replaceAll("-", "");
let fMonth = fromText.substr(4, 2);
let fDay = "/" + fromText.substr(6, 2);
args.Context.Font = "10px '맑은 고딕'";
args.Context.FillStyle = "red";
args.Context.FillText(fMonth, 10, 10);
args.Context.FillText(fDay, 10, 20);
args.Context.TextAlign = "right";
args.Context.FillText("종료월", args.Cell.Width - 10, 10);
if (args.Cell.DataRow. == || args... == ) {
args.. = ;
args.. = ;
args.. = ;
args..(, , );
}
};
args.Cell.Width, args.Cell.Height는 현재 셀의 픽셀 크기. 좌표는 셀 내부 로컬 좌표(0,0 = 좌상단).
7.2 OnDrawSubtotalCell — 소계/총계 셀 커스텀 계산
args.Cell.ParentNode.RowType: 0:Normal 1:SubTotal 2:GrandTotal
const OnDrawSubtotalCell = function(sender, args) {
if (args.Cell.ParentNode.RowType == 1) {
let sum = calculateTotalCell(args.Cell, 1, "DURATION");
args.Cell.DataRow.SetValue("DURATION", sum);
} else if (args.Cell.ParentNode.RowType == 2) {
if (args.index == 0) {
let sum = calculateTotalCell(args.Cell, 2, "DURATION");
args.Cell.DataRow.SetValue("DURATION", sum);
}
}
};
소계 구간(cell.FromDate ~ cell.ToDate)과 실제 데이터 셀의 기간이 걸쳐 있는 경우, 겹치는 부분만 비율 계산해야 정확한 집계가 됩니다 (2번째 샘플 calculateTotalCell 참조).
7.3 OnStartDrag — Drag 시작 취소
const OnStartDrag = function(sender, args) {
if (args.DragType == 0) {
let blocked = args.SelectedCells.some(function(cell) {
return cell.DataRow.GetValue("COLOR") != "BACK_YELLOW";
});
if (blocked) {
Matrix.Warning("주황 셀만 리사이징 가능", "경고");
args.Cancel = true;
}
}
};
7.4 OnEndDrag — 이동/리사이징 종료 시 검증 (겹침 검사 예시)
const OnEndDrag = function(sender, args) {
if (!CHK_USE_OVERLAP.IsChecked && args.TargetRow) {
if (args.TargetRow.Cells.length > 0) {
let targetStartDt = args.TargetStartDate.getTime();
let targetEndDt = args.TargetEndDate.getTime();
let isOverlap = false;
for (let i = 0; i < args.TargetRow.Cells.length; ++i) {
let tc = args.TargetRow.Cells[i];
if (args.SelectedCells.indexOf(tc) > -1) continue;
let cellStart = tc.FromDate.getTime();
let cellEnd = tc.ToDate.getTime();
if (cellEnd == targetEndDt
|| (targetStartDt < cellStart && cellStart < targetEndDt)
|| cellStart == targetStartDt
|| (targetStartDt < cellEnd && cellEnd < targetEndDt)) {
isOverlap = true;
break;
}
}
if (isOverlap) {
.(, );
args. = ;
}
}
}
};
7.5 OnBindEndDrag — Drawing 완료 후 (연동 그리드 갱신용)
const OnBindEndDrag = function(sender, args) {
mainGrid.Update();
};
7.6 OnGanttChartCellDoubleClick — 더블클릭
const OnGanttChartCellDoubleClick = function(sender, args) {
if (args.Id == "GanttChart") {
let planCode = args.Cell.DataRow.GetValue("PLAN_CODE");
let cnt = mainGrid.GetRowCount();
for (let i = 0; i < cnt; i++) {
let row = mainGrid.GetRow(i);
if (planCode == row.GetValue("PLAN_CODE")) {
mainGrid.ScrollTop = row.Bound.Top - mainGrid.ColumnHeaderHeight;
mainGrid.SelectRow(i);
break;
}
}
}
};
8. DataGrid 양방향 동기화 패턴
ScheduleGrid는 단독 사용뿐 아니라 DataGrid와 동일한 DataSet을 공유하는 구성이 흔합니다.
let ds = p.DataSet;
grid.SetDataSet(ds, true);
mainGrid.SetDataSet(ds);
grid.Model.OptionData.SetDisplayPeriod(displayFromTo.Value, displayFromTo.Value2);
grid.Model.OptionData.SetAvaliablePeriod(movablePeriod.Value, movablePeriod.Value2);
grid.Calculate(false);
grid.Update();
const OnValidate = function(sender, args) {
if (args.Id == "MainGrid") {
grid.SetDataSet(mainGrid.DataSet);
grid.Calculate(false);
grid.Update();
}
};
const OnCellDoubleClick = function(sender, args) {
if (args.Id == "MainGrid") {
let planCode = args.Cell.Row.GetValue("PLAN_CODE");
grid.View.FocusCell("PLAN_CODE", planCode);
}
};
grid.Model.SplitCell(grid.View.MenuCell, firstCellWidth);
grid.Update();
mainGrid.SetDataSet(grid.Model.DataSet);
9. 기간/라인 스케줄 스크립트 API
9.1 기간 설정 (조회 버튼에서 동적 변경)
grid.Model.OptionData.SetDisplayPeriod("20170101000000", "20170531000000");
grid.Model.OptionData.SetAvaliablePeriod("20170103000000", "20170531000000");
grid.Calculate(false);
grid.Update();
9.2 라인 정지 일정 — 휴무일/주말 블록
let ls = grid.Model.OptionData.LineSchedule;
ls.StopTimeClear();
ls.AddStopSpecialDay(2023, 11, 1);
ls.AddStopTimeWeekly(0);
ls.AddStopTimeDaily(0, 8);
ls.AddStopTimeMonthly(15);
ls.AddStopTimeYearly(1, 1);
ls.AddStopTimeWeeklyBetween(0, 10, 18);
이후 grid.Calculate(false); grid.Update()를 호출해야 반영됩니다.
10. CRUD 저장
else if (args.Id == "btnSave") {
if (grid.IsModified()) {
Matrix.RunScript("MainGrid", "SaveGantt", function(p) {
if (p.Success == false) { Matrix.Alert(p.Message); return; }
Matrix.iMessage("저장되었습니다.");
inquiry();
});
} else {
Matrix.Information("수정된 데이타가 없습니다.");
}
}
grid.IsModified(): DataSet의 RowState가 N/U/D 인 행이 하나라도 있으면 true
- 서버 스크립트 첫 인자(gridNames)에 DataGrid 이름을 넘기면 해당 그리드의 변경 데이터(
req.getDataSet)가 함께 전달됩니다.
11. 엑셀 / PDF 내보내기
11.1 PDF
grid.ExportPDF("pdfTest.pdf", 10, 10);
grid.ExportPDF("pdfTest.pdf", 10, 10, true);
11.2 엑셀 — ExcelExportServiceCall
차트가 이미지로, 하단 DataGrid는 표 데이터로 내보냅니다.
const exportExcel = function() {
let nowText = Matrix.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss");
let WORKBOOK = {
"FontName": "맑은 고딕",
"FontSize": 11,
WorkSheets: [
{
"Name": "Sheet1",
"Ranges": [
{ "Range":"A1", "ColSpan":1, "Value": LANGUAGE_EXPORT_REPORT_TITLE + " " + Matrix.GetReportInfo().NAME },
{ "Range":"A2", "ColSpan":1, "Value": LANGUAGE_EXPORT_USER_NAME + " " + Matrix.GetUserInfo().UserName },
{ "Range":"A3", "ColSpan":1, "Value": LANGUAGE_EXPORT_CREATE_DATE + " " + nowText }
],
"Controls": [ { "Name": "GanttChart", "Range": "A6" } ]
},
{
"Name": ,
: [ { : , : } ]
}
]
};
.(, , () {
(!e.) { ( + e.); ; }
row = e..().();
folderName = row.();
fileName = row.();
newFileName = .(). + + nowText + ;
.(folderName + , fileName, newFileName, );
});
};
OnExcelExportStart를 정의하면 i-PORTAL의 기본 엑셀 다운로드 버튼에서도 이 함수가 호출됩니다.
const OnExcelExportStart = function(sender, args) {
exportExcel();
};
12. 실전 케이스 스니펫
12.1 세로축 간트 (TransposeType=1) — 일일 설비 스케줄
grid.Model.OptionData.TransposeType = 1;
grid.Model.OptionData.CellMoveMode = 2;
grid.View.OnContextMenuOpenning = function() {
grid.ContextMenu.Clear();
grid.ContextMenu.AddMenu("디자인", showDesignPopup);
};
12.2 가로축 간트 (기본) — 장기 프로젝트 + 연동 DataGrid
let ganttChartAddIn = Matrix.getObject("GanttChart");
grid = ganttChartAddIn.getScriptClass("BIMatrix.ScheduleGrid.ScheduleGrid");
mainGrid = Matrix.getObject("MainGrid");
grid.Model.OptionData.CellMoveMode = 0;
12.3 Lock/Unlock 토글
function toggleLock(cell, lockYN) {
if (lockYN === "Y") {
cell.DataRow.SetValue("LOCK_YN", "Y");
cell.DataRow.SetValue("COLOR", "gray");
} else {
cell.DataRow.SetValue("LOCK_YN", "N");
cell.DataRow.SetValue("COLOR", cell.DataRow.GetValue("ORIGIN_COLOR"));
}
}
보통 SQL에서 ORIGIN_COLOR 컬럼을 별도로 만들어 잠금 해제 시 복원할 원본 색을 저장합니다. 동일한 패턴으로 ORIGIN_DATE_FROM_TEXT, ORIGIN_DATE_TO_TEXT를 두면 드래그 검증/되돌리기에 사용할 수 있습니다.
13. 자주 하는 실수
| 증상 | 원인 | 해결 |
|---|
| 데이터가 안 보임 | Calculate() 또는 Update() 누락 | SetDataSet → Calculate(false) → Update() 순서 준수 |
| 셀 색이 안 바뀜 | cell.DataRow.SetValue("COLOR", ...) 후 Update() 안 함 | grid.Update() 호출 |
FocusCell이 안 움직임 | Calculate 이전에 호출 | 데이터 바인딩 후 Update 완료 시점에 호출 |
| 겹침 검사 Cancel이 무시됨 | args.Cancel = true만 하고 Warning 먼저 띄움 | Cancel 설정 → 이후 MessageBox는 가능(순서 무관하지만 Cancel은 할당 필수) |
| BoxStyle이 적용 안 됨 | 서버에 해당 BoxStyle Name 없음 | get_boxstyle_list로 존재 확인, 없으면 save_boxstyle로 등록 |
| 행렬 바꿔도 화면 그대로 | Transpose() 후 Calculate 미호출 | Transpose(); Calculate(false); Update(); |
| 기간 변경이 반영 안 됨 | SetDisplayPeriod 후 Calculate 미호출 | 설정 후 반드시 Calculate + Update |
cell.DataRow.GetValue("XX")가 undefined | Fields에 해당 필드가 등록 안 됨 | MTSD ComponentElement.Model.Fields에 컬럼 추가 |
14. 관련 스킬
/iaud-client-script: 일반 클라이언트 스크립트 패턴
/iaud-grid-guide: 연동 DataGrid/TreeGrid 개발
/iaud-boxstyle-guide: ColumnHeaderBoxStyleKey 등 스타일 키 관리
/iaud-mtsd-create: MTSD 보고서 UI 생성 (build_mtsd)