📖 OPEN_GRID 개발 가이드
코딩을 처음 배우는 분도 따라할 수 있도록 하나씩 쉽게 설명합니다.
단계별로 읽으면 어느새 그리드 전문가가 되어 있을 거예요!
OPEN_GRID란 무엇인가요?
OPEN_GRID가 어떤 도구인지, 뭘 할 수 있는지 먼저 알아봅시다.
그리드(Grid)가 뭐예요?
그리드란 행(가로줄)과 열(세로줄)로 이루어진 표입니다. 마이크로소프트 엑셀이나 구글 스프레드시트처럼 생긴 것을 상상하면 딱 맞아요.
웹 페이지에서 데이터를 표 형태로 보여줄 때 HTML <table>을 쓰기도 하지만,
데이터가 수만 개가 넘거나, 정렬·필터·수정 같은 기능이 필요하면 한계가 생깁니다.
그럴 때 OPEN_GRID 같은 전문 그리드 컴포넌트를 사용합니다.
정렬, 필터, 수정, 내보내기까지 다 됩니다!
어떤 기능이 있어요?
무료인가요?
네! OPEN_GRID는 MIT 라이선스 오픈소스입니다. 개인 프로젝트, 회사 프로젝트 모두 무료로 자유롭게 사용할 수 있습니다.
준비하기 (설치)
OPEN_GRID를 내 프로젝트에 넣는 방법은 두 가지입니다. 상황에 맞게 골라보세요.
방법 A — CDN 방식 (가장 빠른 방법)
HTML 파일에 <script>와 <link> 태그만 추가하면 끝!
별도 설치 없이 인터넷에서 바로 불러옵니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<!-- 1. OPEN_GRID 스타일 (반드시 포함!) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/open-grid/dist/style.css">
</head>
<body>
<!-- 2. 그리드를 표시할 영역 -->
<div id="myGrid" style="height: 400px;"></div>
<!-- 3. OPEN_GRID 스크립트 -->
<script type="module">
import { OpenGrid } from 'https://cdn.jsdelivr.net/npm/open-grid/dist/open-grid.js';
const grid = new OpenGrid('#myGrid', {
columns: [
{ field: 'name', header: '이름', width: 120 },
{ field: 'score', header: '점수', width: 100 },
]
});
grid.setData([
{ name: '홍길동', score: 95 },
{ name: '김영희', score: 87 },
]);
</script>
</body>
</html>
방법 B — npm 방식 (Vite/Webpack 프로젝트)
Node.js와 패키지 매니저(npm)가 설치된 환경에서 사용합니다. Vue, React, Angular 등 프레임워크 프로젝트에서 주로 이 방법을 씁니다.
① 패키지 설치
npm install open-grid
② JavaScript/TypeScript 파일에서 import
import { OpenGrid } from 'open-grid';
import 'open-grid/dist/style.css';
③ Vue 3에서 사용하기
<!-- Vue 3 (.vue 파일) -->
<template>
<OpenGridVue :columns="columns" :data="rows" />
</template>
<script setup>
import { OpenGridVue } from 'open-grid/vue';
import 'open-grid/dist/style.css';
const columns = [
{ field: 'name', header: '이름', width: 120 },
];
const rows = [{ name: '홍길동' }];
</script>
④ React 18에서 사용하기
// React (.tsx 파일)
import { OpenGridReact } from 'open-grid/react';
import 'open-grid/dist/style.css';
function App() {
const columns = [{ field: 'name', header: '이름', width: 120 }];
const rows = [{ name: '홍길동' }];
return <OpenGridReact columns={columns} data={rows} style={{ height: 400 }} />;
}
첫 번째 그리드 만들기
딱 5분이면 완성됩니다. 하나씩 따라해보세요.
전체 코드 (복사해서 바로 실행)
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>나의 첫 그리드</title>
<!-- ① 스타일 불러오기 -->
<link rel="stylesheet" href="/src/styles/base.css">
<link rel="stylesheet" href="/src/styles/themes.css">
<style>
body { padding: 20px; font-family: sans-serif; }
</style>
</head>
<body>
<h2>우리 반 성적표</h2>
<!-- ② 그리드가 들어갈 빈 상자 -->
<div id="myGrid" style="height: 300px;"></div>
<script type="module">
// ③ OpenGrid 가져오기
import { OpenGrid } from '/src/index.ts';
// ④ 컬럼(열) 정의 — 어떤 열을 만들지 알려줍니다
const columns = [
{ field: 'num', header: '번호', width: 70, align: 'center' },
{ field: 'name', header: '이름', width: 100 },
{ field: 'korean', header: '국어', width: 80, type: 'number', align: 'right' },
{ field: 'math', header: '수학', width: 80, type: 'number', align: 'right' },
{ field: 'grade', header: '등급', width: 70, align: 'center' },
];
// ⑤ 그리드 만들기
const grid = new OpenGrid('#myGrid', { columns });
// ⑥ 데이터 넣기
grid.setData([
{ num: 1, name: '홍길동', korean: 95, math: 87, grade: 'A' },
{ num: 2, name: '김영희', korean: 78, math: 92, grade: 'B' },
{ num: 3, name: '이민준', korean: 85, math: 76, grade: 'B' },
{ num: 4, name: '박서연', korean: 91, math: 95, grade: 'A' },
{ num: 5, name: '최지훈', korean: 67, math: 71, grade: 'C' },
]);
</script>
</body>
</html>
코드 하나씩 설명
-
① 스타일 불러오기 그리드가 예쁘게 보이려면 CSS 파일이 필요합니다.
base.css는 기본 스타일,themes.css는 테마 색상입니다. -
② 빈 상자 만들기
<div id="myGrid">가 그리드가 들어갈 자리입니다. height를 꼭 지정해야 합니다. 높이가 없으면 그리드가 보이지 않아요! -
③ OpenGrid 가져오기
import는 "이 도구를 사용하겠다"고 선언하는 것입니다. -
④ columns 정의 배열(
[]) 안에 각 열의 정보를 객체({})로 씁니다.field는 데이터 키 이름,header는 화면에 보이는 제목입니다. -
⑤ 그리드 만들기
new OpenGrid('#myGrid', { columns })—#myGrid는 "저 div에 그리드를 그려줘"라는 의미입니다. -
⑥ 데이터 넣기
setData([...])에 배열 형태로 데이터를 넘겨줍니다. 각 행이 하나의 객체{ field명: 값, ... }입니다.
컬럼(열) 설정하기
그리드에서 가장 중요한 설정입니다. 열을 어떻게 구성하느냐에 따라 완전히 달라집니다.
기본 필수 옵션
| 옵션명 | 타입 | 설명 | 예시 |
|---|---|---|---|
field 필수 | string | 데이터의 키 이름 (영어로 작성) | 'name' |
header 필수 | string | 화면에 보이는 열 제목 | '이름' |
width 선택 | number | 열 너비 (픽셀). 없으면 자동 | 120 |
const columns = [
{ field: 'id', header: 'ID', width: 60 }, // 너비 60px
{ field: 'name', header: '이름', width: 140 }, // 너비 140px
{ field: 'memo', header: '메모' }, // 너비 자동
];
데이터 타입 설정 — type
데이터의 종류를 알려주면 정렬·표시가 더 정확해집니다.
| type 값 | 설명 | 예시 데이터 |
|---|---|---|
'string' | 문자 (기본값) | '홍길동' |
'number' | 숫자. 오른쪽 정렬, 숫자 기준 정렬 | 1500000 |
'date' | 날짜. 날짜 기준 정렬 | '2024-03-15' |
'boolean' | 참/거짓. 체크박스로 표시 | true |
const columns = [
{ field: 'price', header: '가격', type: 'number', width: 120 },
{ field: 'regDt', header: '등록일', type: 'date', width: 110 },
{ field: 'inUse', header: '사용중', type: 'boolean', width: 80 },
];
숫자 포맷 — format
숫자에 천 단위 구분자나 소수점을 붙일 때 씁니다.
| format 값 | 1500000이 이렇게 보임 |
|---|---|
'#,##0' | 1,500,000 |
'#,##0.00' | 1,500,000.00 |
'0.0' | 1500000.0 |
{ field: 'salary', header: '급여', type: 'number', format: '#,##0', width: 130 }
정렬 — align
{ field: 'id', header: 'ID', align: 'center' }, // 가운데
{ field: 'price', header: '가격', align: 'right' }, // 오른쪽
{ field: 'name', header: '이름', align: 'left' }, // 왼쪽 (기본)
정렬/필터 허용 — sortable / filterable
{ field: 'name', header: '이름', sortable: true, filterable: true }
// 헤더 클릭으로 정렬, 필터 아이콘 표시
// 그리드 전체에 적용하려면 옵션에서 설정
const grid = new OpenGrid('#myGrid', {
columns,
sortable: true, // 모든 열 정렬 가능
filterable: true, // 모든 열 필터 가능
});
편집 가능 여부 — editable / editor
const columns = [
{ field: 'name', header: '이름', editable: true }, // 텍스트 편집
{ field: 'dept', header: '부서', editable: true,
editor: { type: 'select', options: ['개발팀','영업팀','인사팀'] } }, // 드롭다운
{ field: 'salary', header: '급여', editable: true,
editor: { type: 'number', min: 0, max: 99999999 } }, // 숫자 입력
{ field: 'joinDt', header: '입사일', editable: true,
editor: { type: 'date' } }, // 날짜 선택
];
editable: true를 추가해야 해요.화면 표시 방식 — renderer
데이터를 그냥 텍스트로 보여주는 게 아니라 아이콘, 이미지, 버튼 등으로 꾸밀 수 있습니다.
| renderer 값 | 설명 |
|---|---|
'checkbox' | 체크박스로 표시 (boolean 데이터) |
'switch' | 토글 스위치로 표시 (boolean 데이터) |
'image' | 이미지 URL을 <img>로 표시 |
'button' | 버튼으로 표시 |
'link' | 하이퍼링크로 표시 |
'badge' | 배지(뱃지) 스타일로 표시 |
'progress' | 진행 바로 표시 |
'rating' | 별점으로 표시 |
'sparkline' | 미니 차트로 표시 |
const columns = [
{ field: 'photo', header: '사진', renderer: 'image', width: 60 },
{ field: 'active', header: '활성', renderer: 'switch', width: 80 },
{ field: 'progress', header: '진행률', renderer: 'progress', width: 120 },
{ field: 'rating', header: '평점', renderer: 'rating', width: 100 },
{
field: 'action', header: '관리', width: 80,
renderer: { type: 'button', label: '상세보기' }
},
];
숨기기 / 고정 — hidden / frozen
{ field: 'id', header: 'ID', hidden: true }, // 화면에 안 보임 (데이터는 있음)
{ field: 'name', header: '이름', frozen: true }, // 스크롤해도 이 열은 고정
동적 셀 스타일 — cellStyle
값에 따라 글자색이나 배경색을 바꾸고 싶을 때 씁니다.
{
field: 'score', header: '점수', type: 'number',
cellStyle: (value) => {
if (value >= 90) return { color: '#2e7d32', fontWeight: '700' }; // 90점 이상 → 초록 굵게
if (value < 60) return { color: '#c62828' }; // 60점 미만 → 빨간색
return {};
}
}
그룹 헤더 (열 묶기) — children
여러 열을 하나의 제목으로 묶을 때 씁니다.
const columns = [
{ field: 'name', header: '이름', width: 100 },
{
header: '점수', // 그룹 헤더 제목
children: [ // 하위 열들
{ field: 'korean', header: '국어', width: 80 },
{ field: 'math', header: '수학', width: 80 },
{ field: 'eng', header: '영어', width: 80 },
]
}
];
데이터 넣고 꺼내기
그리드에 데이터를 넣거나 꺼내는 방법을 배웁니다.
데이터 넣기
const data = [
{ name: '홍길동', age: 30, city: '서울' },
{ name: '김영희', age: 25, city: '부산' },
];
// ✅ 데이터 전체 교체 (가장 많이 쓰는 방법)
grid.setData(data);
// ✅ 끝에 데이터 추가 (기존 데이터 유지)
grid.pushData([
{ name: '이민준', age: 28, city: '인천' }
]);
// ✅ 앞에 데이터 추가
grid.prefixData([
{ name: '박서연', age: 22, city: '대전' }
]);
// ✅ 전체 삭제
grid.clearData();
데이터 꺼내기
// 현재 화면에 보이는 데이터 (필터/정렬 적용 후)
const current = grid.getData();
console.log(current); // [{ name: '홍길동', ... }, ...]
// 원본 데이터 (필터/정렬 전 상태)
const original = grid.getSourceRows();
// 특정 행의 데이터 가져오기 (0번째 = 첫 번째 행)
const firstRow = grid.getRowAt(0);
console.log(firstRow.name); // '홍길동'
// 특정 셀 값 읽기
const name = grid.readCell(0, 'name'); // 0번 행의 name 값
console.log(name); // '홍길동'
실전 예제 — 서버에서 데이터 불러오기
const grid = new OpenGrid('#grid', { columns });
// 서버 API에서 데이터를 받아와서 그리드에 넣기
async function loadData() {
const response = await fetch('/api/users');
const data = await response.json();
grid.setData(data);
}
loadData();
행 추가 / 수정 / 삭제
그리드의 특정 행을 코드로 제어하는 방법입니다.
행 추가하기
// 맨 뒤에 행 추가
grid.pushRow({ name: '신규직원', dept: '개발팀', salary: 3500000 });
// 맨 앞에 행 추가
grid.unshiftRow({ name: '신규직원', dept: '개발팀', salary: 3500000 });
// 특정 위치에 삽입
grid.insertRow({ name: '신규직원' }, 2); // 2번 위치에 삽입
grid.insertRow({ name: '신규직원' }, 'first'); // 맨 앞
grid.insertRow({ name: '신규직원' }, 'last'); // 맨 뒤
grid.insertRow({ name: '신규직원' }, 'before'); // 현재 선택된 행 앞
grid.insertRow({ name: '신규직원' }, 'after'); // 현재 선택된 행 뒤
행 삭제하기
// 0번 행 삭제
grid.deleteRow(0);
// 여러 행 동시 삭제 (배열로 전달)
grid.deleteRow([0, 2, 4]);
// 선택된 행 삭제 (체크박스 사용 시)
const checked = grid.getChecked();
const indexes = checked.map(c => c.rowIndex);
grid.deleteRow(indexes);
특정 셀 값 변경하기
// 0번 행의 'salary' 값을 5000000으로 변경
grid.writeCell(0, 'salary', 5000000);
// 값 읽기
const salary = grid.readCell(0, 'salary');
console.log(salary); // 5000000
// 표시용 텍스트 읽기 (format 적용 후)
const display = grid.getDisplayValue(0, 'salary');
console.log(display); // '5,000,000'
실전 예제 — 버튼으로 행 추가/삭제
<button id="btnAdd">행 추가</button>
<button id="btnDel">선택 행 삭제</button>
<div id="myGrid" style="height:400px"></div>
<script type="module">
import { OpenGrid } from '/src/index.ts';
const grid = new OpenGrid('#myGrid', {
columns: [
{ field: 'name', header: '이름', width: 120, editable: true },
{ field: 'dept', header: '부서', width: 120, editable: true },
],
editable: true,
checkColumn: true, // 체크박스 열 표시
});
document.getElementById('btnAdd').onclick = () => {
grid.pushRow({ name: '신규', dept: '' });
};
document.getElementById('btnDel').onclick = () => {
const checked = grid.getChecked();
if (checked.length === 0) { alert('삭제할 행을 체크해주세요.'); return; }
grid.deleteRow(checked.map(c => c.rowIndex));
};
</script>
이벤트 처리
사용자가 그리드를 클릭하거나 편집할 때 내 코드가 반응하도록 하는 방법입니다. "이벤트"란 그리드에서 발생하는 사건(클릭, 편집 완료 등)을 말합니다.
방법 1 — 그리드 옵션에서 직접 설정 (추천)
const grid = new OpenGrid('#myGrid', {
columns,
// 그리드가 완전히 준비됐을 때
onReady: (grid) => {
console.log('그리드 준비 완료!');
grid.setData(myData);
},
// 셀을 클릭했을 때
onCellClick: (e) => {
console.log('클릭한 행 번호:', e.rowIndex);
console.log('클릭한 열 이름:', e.field);
console.log('그 셀의 값:', e.value);
console.log('그 행의 전체 데이터:', e.row);
},
// 셀을 더블클릭했을 때
onCellDblClick: (e) => {
console.log('더블클릭!', e.value);
},
// 행 클릭
onRowClick: (e) => {
console.log('행 클릭:', e.row);
},
// 편집 완료했을 때
onEditEnd: (e) => {
console.log('수정 전 값:', e.oldValue);
console.log('수정 후 값:', e.newValue);
console.log('수정한 열:', e.field);
},
// 선택이 바뀌었을 때
onSelectionChange: (e) => {
console.log('선택된 행들:', e.rows);
},
});
방법 2 — on() / off() 로 나중에 등록
const grid = new OpenGrid('#myGrid', { columns });
// 이벤트 등록
grid.on('cellClick', (e) => {
console.log('셀 클릭:', e.value);
});
// 한 번만 실행 후 자동 해제
grid.once('cellClick', (e) => {
alert('첫 번째 클릭!');
});
// 이벤트 해제
const handler = (e) => { console.log(e); };
grid.on('editEnd', handler);
grid.off('editEnd', handler); // 해제
이벤트 목록
| 이벤트명 | 언제 발생하나요? | 주요 정보 |
|---|---|---|
onReady | 그리드 초기화 완료 시 | grid 인스턴스 |
onCellClick | 셀 클릭 시 | rowIndex, field, value, row |
onCellDblClick | 셀 더블클릭 시 | rowIndex, field, value |
onRowClick | 행 클릭 시 | rowIndex, row |
onEditStart | 셀 편집 시작 시 | rowIndex, field, value |
onEditEnd | 편집 완료 시 | oldValue, newValue, field |
onEditBefore | 편집 저장 직전 (false 반환 시 취소) | oldValue, newValue |
onSelectionChange | 행 선택 변경 시 | rows, rowIndexes |
onSortChange | 정렬 변경 시 | field, dir |
onFilterChange | 필터 변경 시 | field, filterItems |
onScroll | 스크롤 시 | scrollTop, isAtBottom |
onDataChange | 데이터 변경 시 | 변경된 전체 데이터 |
실전 예제 — 셀 클릭 시 상세 패널 표시
const grid = new OpenGrid('#myGrid', {
columns,
onRowClick: (e) => {
const row = e.row;
// 오른쪽 패널에 선택된 행의 정보 표시
document.getElementById('detail-name').textContent = row.name;
document.getElementById('detail-dept').textContent = row.dept;
document.getElementById('detail-panel').style.display = 'block';
},
});
정렬과 필터
데이터를 원하는 순서로 정렬하거나 특정 조건으로 걸러내는 방법입니다.
UI로 정렬하기 (헤더 클릭)
sortable: true 옵션을 주면 헤더를 클릭해서 오름차순/내림차순으로 정렬됩니다.
multiSort: true를 추가하면 여러 열을 동시에 정렬할 수 있어요 (Shift + 클릭).
const grid = new OpenGrid('#myGrid', {
columns,
sortable: true, // 헤더 클릭 정렬 활성화
multiSort: true, // 여러 열 동시 정렬
});
코드로 정렬하기
// 이름 열을 오름차순(가나다) 정렬
grid.orderBy('name', 'asc');
// 급여 열을 내림차순(높은 것부터) 정렬
grid.orderBy('salary', 'desc');
// 여러 열 동시 정렬 (부서 오름차순, 그 안에서 급여 내림차순)
grid.orderBy([
{ field: 'dept', dir: 'asc' },
{ field: 'salary', dir: 'desc' },
]);
// 정렬 해제 (원래 순서로)
grid.resetOrder();
UI로 필터하기
filterable: true 옵션을 주면 헤더에 필터 아이콘이 나타납니다.
클릭하면 조건을 선택하는 드롭다운이 열립니다.
const grid = new OpenGrid('#myGrid', {
columns,
filterable: true, // 필터 아이콘 표시
});
코드로 필터하기
// 부서가 '개발팀'인 행만 보여주기
grid.setFilter('dept', [{ operator: '=', value: '개발팀' }]);
// 급여가 4000000 이상인 행만 보여주기
grid.setFilter('salary', [{ operator: '>=', value: 4000000 }]);
// 이름에 '김'이 포함된 행만 보여주기
grid.setFilter('name', [{ operator: 'contains', value: '김' }]);
// 여러 조건 동시 (OR 조건: 개발팀 또는 영업팀)
grid.setFilter('dept', [
{ operator: '=', value: '개발팀' },
{ operator: '=', value: '영업팀' },
]);
// 특정 열 필터 해제
grid.resetFilter('dept');
// 전체 필터 해제
grid.resetFilter();
| operator | 의미 | 사용 예 |
|---|---|---|
'=' | 같다 | 값이 정확히 일치 |
'!=' | 같지 않다 | 해당 값 제외 |
'>' | 보다 크다 | 숫자/날짜 비교 |
'>=' | 이상 | 숫자/날짜 비교 |
'<' | 보다 작다 | 숫자/날짜 비교 |
'<=' | 이하 | 숫자/날짜 비교 |
'contains' | 포함한다 | 검색어가 포함된 것 |
'startsWith' | 로 시작한다 | 특정 글자로 시작 |
'endsWith' | 로 끝난다 | 특정 글자로 끝남 |
행 선택
사용자가 행을 클릭해서 선택하고, 선택된 데이터를 활용하는 방법입니다.
선택 모드 설정 — selection
new OpenGrid('#myGrid', {
columns,
selection: 'row', // 행 단위 선택 (기본값)
// selection: 'single', // 단일 행만 선택
// selection: 'multiple', // 여러 행 선택 (Ctrl/Shift 클릭)
// selection: 'cells', // 셀 단위 선택
});
코드로 선택/해제하기
// 2번 행 선택 (0부터 시작)
grid.activate(2);
// 현재 선택된 행들 가져오기
const selected = grid.getSelections();
console.log(selected); // [{ name: '이민준', ... }]
// 현재 활성 행 번호
const rowIndex = grid.getActiveRow();
// 선택 해제
grid.deselect();
체크박스 열 — checkColumn
맨 앞에 체크박스 열을 추가해서 여러 행을 체크하고 처리할 수 있습니다.
const grid = new OpenGrid('#myGrid', {
columns,
checkColumn: true, // 체크박스 열 표시
});
// 체크된 행 가져오기
const checked = grid.getChecked();
// [{ row: { name: '홍길동', ... }, rowIndex: 0 }, ...]
// 체크된 행의 원본 데이터만 추출
const checkedData = grid.getAllChecked();
// 전체 체크 해제
grid.uncheckAll();
// 특정 값으로 체크 (id가 '001', '003'인 행 체크)
grid.checkByValue('id', ['001', '003']);
편집 기능
셀을 직접 수정하고, 변경 내역을 추적하고, 실행취소까지 하는 방법입니다.
편집 활성화
const grid = new OpenGrid('#myGrid', {
columns: [
{ field: 'name', header: '이름', editable: true },
{ field: 'dept', header: '부서', editable: true },
{ field: 'salary', header: '급여', editable: true, type: 'number' },
{ field: 'id', header: 'ID' }, // editable 없으면 수정 불가
],
editable: true, // 전체 편집 ON
editMode: 'dblclick', // 더블클릭으로 편집 (기본: 'click')
history: true, // 실행취소/다시실행 활성화
});
실행취소 / 다시실행
// Ctrl+Z 와 같은 효과
grid.undo();
// Ctrl+Y 와 같은 효과
grid.redo();
// 변경 기록 초기화
grid.clearHistory();
변경 내역 추적
어떤 행이 추가/수정/삭제됐는지 확인할 수 있습니다. 서버에 저장할 때 유용합니다.
// 수정된 행들 (원래 있던 행 중 값이 바뀐 것)
const changed = grid.getChangedRows();
// 새로 추가된 행들
const added = grid.getAddedRows();
// 삭제된 행들
const removed = grid.getRemovedRows();
// 어떤 열이 바뀌었는지도 확인 가능
const changedCols = grid.getChangedColumns();
// [{ row: {...}, fields: ['name', 'salary'] }, ...]
// 저장 버튼 예시
document.getElementById('btnSave').onclick = async () => {
const payload = {
updated: grid.getChangedRows(),
created: grid.getAddedRows(),
deleted: grid.getRemovedRows(),
};
await fetch('/api/save', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' }
});
alert('저장 완료!');
};
편집 검증 — onEditBefore
false를 반환하면 편집이 취소됩니다.
const grid = new OpenGrid('#myGrid', {
columns,
editable: true,
onEditBefore: (e) => {
// 급여가 0 이하면 편집 거부
if (e.field === 'salary' && Number(e.newValue) <= 0) {
alert('급여는 0보다 커야 합니다.');
return false; // ← 이걸 반환하면 저장 안 됨
}
return true;
},
});
고정 컬럼 / 행
열이 많아서 스크롤해도 중요한 열(이름, ID 등)이 항상 보이게 고정하는 방법입니다.
열 고정하기
// 방법 1: 그리드 옵션으로 — 왼쪽 2개 열 고정
const grid = new OpenGrid('#myGrid', {
columns,
frozenColumns: 2, // 처음 2개 열 고정
});
// 방법 2: 컬럼 정의에서 — frozen: true
const columns = [
{ field: 'no', header: 'No.', width: 60, frozen: true }, // 고정
{ field: 'name', header: '이름', width: 120, frozen: true }, // 고정
{ field: 'dept', header: '부서', width: 120 }, // 스크롤
// ...
];
// 방법 3: 코드로 나중에 고정
grid.freeze(2); // 왼쪽 2개 열 고정
grid.freeze(0); // 고정 해제
행 고정하기
// 위 2개 행 고정 (옵션)
const grid = new OpenGrid('#myGrid', {
columns,
frozenRows: 2,
});
// 코드로
grid.freezeRows(1); // 위 1개 행 고정
그룹핑
같은 값끼리 묶어서 계층 형태로 보여주는 기능입니다. 부서별, 지역별 정리에 유용합니다.
그룹핑 설정
// 부서별 그룹핑
const grid = new OpenGrid('#myGrid', {
columns,
groupBy: ['dept'], // 'dept' 열 기준으로 그룹핑
});
// 부서 > 팀 순으로 2단계 그룹핑
const grid2 = new OpenGrid('#myGrid2', {
columns,
groupBy: ['dept', 'team'],
});
코드로 그룹핑 변경
// 지역별로 재그룹핑
grid.groupBy(['region']);
// 그룹 해제 (일반 표로)
grid.clearGroup();
소계 표시 — summary
그룹 행에 컬럼별 소계가 정렬되어 표시됩니다. OGDecimal 정밀 계산으로 소수점 오류가 없습니다.
const grid = new OpenGrid('#myGrid', {
columns,
groupBy: ['dept'],
summary: {
fields: ['salary', 'bonus', 'rate'], // 소계를 구할 컬럼
ops: 'SUM', // 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'COUNT'
format: '#,##0', // 천단위 콤마 정수 포맷
},
onReady: g => g.setData(data),
});
+N 추가된 행
MN 수정된 행
DN 삭제 표시된 행
런타임 그룹핑 변경 + 소계 동시 적용
// ① summary 옵션 먼저 적용 (setOptions는 즉시 반영 X → groupBy 호출 시 반영됨)
grid.setOptions({
summary: {
fields: ['salary', 'bonus'],
ops: 'SUM',
format: '#,##0',
},
});
// ② groupBy 호출 → 소계 포함 그룹 재구성
grid.groupBy(['dept']);
grid.expandAll(); // 모든 그룹 펼치기
그룹 펼치기 / 접기
grid.expandAll(); // 전체 펼치기
grid.collapseAll(); // 전체 접기
grid.clearGroup(); // 그룹 해제 (일반 테이블)
트리 구조
부모-자식 관계(조직도, 카테고리, 메뉴 구조 등)를 계층 트리로 표시하는 방법입니다.
데이터 구조 이해하기
트리를 만들려면 데이터에 자신의 ID와 부모 ID가 있어야 합니다.
const treeData = [
{ id: '1', parentId: null, name: '전체', type: '루트' },
{ id: '1-1', parentId: '1', name: '개발팀', type: '부서' },
{ id: '1-2', parentId: '1', name: '영업팀', type: '부서' },
{ id: '1-1-1', parentId: '1-1', name: '홍길동', type: '직원' },
{ id: '1-1-2', parentId: '1-1', name: '김영희', type: '직원' },
{ id: '1-2-1', parentId: '1-2', name: '이민준', type: '직원' },
];
트리 그리드 만들기
const grid = new OpenGrid('#myGrid', {
columns: [
{ field: 'name', header: '이름/부서', width: 200 },
{ field: 'type', header: '구분', width: 100 },
],
treeMode: 'flat', // 'flat' = id/parentId 방식
treeId: 'id', // 자신의 ID 필드명
treeParentId: 'parentId', // 부모 ID 필드명
expandOnLoad: true, // 처음부터 모두 펼침
});
grid.setData(treeData);
펼치기 / 접기
// 전체 펼치기
grid.expandAll();
// 전체 접기
grid.collapseAll();
// 특정 노드만 펼치기
grid.expandNodes('1-1', true); // '1-1' 노드 열기
grid.expandNodes('1-1', false); // '1-1' 노드 닫기
// 트리에 새 자식 행 추가
grid.addTreeRow(
{ id: '1-1-3', name: '신규직원', type: '직원' },
'1-1' // 부모 ID
);
셀 병합
같은 값이 연속으로 있을 때 셀을 합쳐서 보기 좋게 만드는 기능입니다. 엑셀의 '셀 병합'과 같습니다.
자동 병합 (autoMerge) — rowSpan
지정한 컬럼에서 연속으로 같은 값이 나오면 위아래로 셀을 합칩니다.
const data = [
{ dept: '개발팀', team: '프론트', name: '홍길동', salary: 52000000 },
{ dept: '개발팀', team: '프론트', name: '김영희', salary: 48000000 },
{ dept: '개발팀', team: '백엔드', name: '이민준', salary: 55000000 },
{ dept: '영업팀', team: '국내', name: '박서연', salary: 45000000 },
{ dept: '영업팀', team: '국내', name: '최지훈', salary: 43000000 },
];
const grid = new OpenGrid('#myGrid', {
columns: [
{ field: 'dept', header: '부서', width: 130 },
{ field: 'team', header: '팀', width: 130 },
{ field: 'name', header: '이름', width: 120 },
{ field: 'salary', header: '급여', width: 110, type: 'number', align: 'right' },
],
});
grid.setData(data);
// 부서·팀 기준으로 같은 값인 셀을 자동 rowSpan 병합
grid.autoMerge(['dept', 'team']);
수동 병합 (mergeCells) — rowSpan / colSpan
정확한 위치를 직접 지정해서 병합합니다. 행 방향(rowSpan)과 열 방향(colSpan) 모두 지원합니다.
// 0번 행, 0번 컬럼을 3행 병합 (rowSpan: 3)
grid.mergeCells([
{ row: 0, col: 0, rowSpan: 3 }, // 세로 3행 병합
{ row: 0, col: 2, colSpan: 2 }, // 가로 2열 병합
{ row: 3, col: 0, rowSpan: 2, colSpan: 2 }, // 2×2 병합
]);
병합 해제
grid.clearMerge();
병합 API 요약
| 메서드 | 설명 |
|---|---|
autoMerge(fields[]) | 지정 컬럼에서 연속 같은 값을 자동 rowSpan |
mergeCells(cells[]) | 정확한 row/col/rowSpan/colSpan 수동 지정 |
clearMerge() | 모든 병합 해제 |
내보내기 & 인쇄
그리드 데이터를 파일로 저장하거나 인쇄하는 방법입니다. 정말 쉽습니다!
Excel로 내보내기
// 기본 (파일명: grid_export.xlsx)
grid.exportExcel();
// 파일명 지정
grid.exportExcel('직원목록_2024.xlsx');
// 상세 옵션
grid.exportExcel({
filename: '직원목록',
sheetName: '직원',
includeHeader: true, // 헤더 포함 (기본: true)
exceptFields: ['id'], // 이 열은 제외
});
CSV / JSON으로 내보내기
// CSV (엑셀에서도 열림)
grid.exportCsv('데이터.csv');
// JSON 파일로 저장
grid.exportJson('데이터.json');
인쇄하기
// 기본 인쇄
grid.print();
// 인쇄 옵션
grid.print({
title: '직원 목록 보고서', // 인쇄물 제목
excludeFields: ['id'], // 인쇄 제외 열
showFooter: true, // 푸터 포함 여부
});
실전 예제 — 버튼으로 내보내기
<button onclick="grid.exportExcel('직원목록')">Excel 내보내기</button>
<button onclick="grid.exportCsv('직원목록')">CSV 내보내기</button>
<button onclick="grid.print({ title: '직원 목록' })">인쇄</button>
테마 꾸미기
12가지 내장 테마로 그리드 색상을 바꾸거나, 내가 직접 색을 정할 수 있습니다.
내장 테마 12가지
// 초기 설정 시
const grid = new OpenGrid('#myGrid', {
columns,
theme: 'dark', // 다크 테마로 시작
});
// 나중에 바꾸기
grid.setTheme('ocean');
grid.setTheme('forest');
내가 직접 색상 정하기 — CSS 변수
// 그리드 색상을 직접 커스텀
const grid = new OpenGrid('#myGrid', {
columns,
cssVars: {
'--og-header-bg': '#2d3748', // 헤더 배경색
'--og-header-color': '#e2e8f0', // 헤더 글자색
'--og-row-bg': '#ffffff', // 행 배경색
'--og-row-alt-bg': '#f7fafc', // 짝수 행 배경색
'--og-accent': '#6366f1', // 강조색 (선택/포커스)
},
});
// 하나씩 변경
grid.setThemeVar('--og-accent', '#f59e0b');
API 전체 레퍼런스
OPEN_GRID의 모든 메서드를 한눈에 볼 수 있습니다. Ctrl+F로 검색해서 찾아보세요!
데이터 관련
| 메서드 | 설명 |
|---|---|
setData(data[]) | 데이터 전체 교체 |
getData() | 현재 표시 데이터 반환 (필터/정렬 적용 후) |
getSourceRows() | 원본 데이터 반환 |
pushData(data[]) | 데이터 끝에 추가 |
prefixData(data[]) | 데이터 앞에 추가 |
clearData() | 전체 삭제 |
행(Row) 관련
| 메서드 | 설명 |
|---|---|
insertRow(item, position?) | 특정 위치에 행 삽입 |
pushRow(items) | 맨 뒤에 행 추가 |
unshiftRow(items) | 맨 앞에 행 추가 |
deleteRow(rowIndex) | 행 삭제 (배열로 여러 개 가능) |
getRowAt(rowIndex) | 특정 행 객체 반환 |
readCell(rowIndex, field) | 셀 값 읽기 |
writeCell(rowIndex, field, value) | 셀 값 변경 |
getDisplayValue(rowIndex, field) | 포맷 적용된 표시값 반환 |
변경 추적
| 메서드 | 설명 |
|---|---|
getChangedRows() | 수정된 행 반환 |
getAddedRows() | 추가된 행 반환 |
getRemovedRows() | 삭제된 행 반환 |
getChangedColumns() | 어떤 열이 변경됐는지 반환 |
undo() | 실행취소 |
redo() | 다시실행 |
clearHistory() | 변경 기록 초기화 |
컬럼 관련
| 메서드 | 설명 |
|---|---|
applyColumns(columns) | 컬럼 정의 교체 |
getColumnDefs() | 현재 컬럼 정의 반환 |
insertColumn(colDef, position?) | 열 추가 |
deleteColumn(field) | 열 삭제 |
hideColumn(field) | 열 숨기기 |
showColumn(field) | 열 보이기 |
getColValues(field) | 특정 열의 모든 값 반환 |
getUniqueValues(field) | 특정 열의 중복 제거 값 반환 |
선택 / 체크
| 메서드 | 설명 |
|---|---|
activate(rowIndex) | 특정 행 선택 |
getSelections() | 선택된 행 반환 |
getActiveRow() | 활성 행 번호 반환 |
deselect() | 선택 해제 |
getChecked() | 체크된 행 반환 [{row, rowIndex}] |
getAllChecked() | 체크된 행 데이터만 반환 |
checkByValue(field, values[]) | 값으로 체크 |
uncheckAll() | 전체 체크 해제 |
정렬 / 필터
| 메서드 | 설명 |
|---|---|
orderBy(field, dir?) | 단일 열 정렬 |
orderBy(sortList[]) | 다중 열 정렬 |
resetOrder() | 정렬 해제 |
setFilter(field, filterItems[]) | 필터 적용 |
resetFilter(field?) | 필터 해제 (field 없으면 전체) |
getFilterState() | 현재 필터 상태 반환 |
고정 / 그룹 / 트리 / 병합
| 메서드 | 설명 |
|---|---|
freeze(n) | 왼쪽 n개 열 고정 |
freezeRows(n) | 위 n개 행 고정 |
groupBy(fields[]) | 그룹핑 |
clearGroup() | 그룹 해제 |
expandAll() | 트리/그룹 전체 펼치기 |
collapseAll() | 트리/그룹 전체 접기 |
addTreeRow(item, parentId) | 트리 자식 행 추가 |
autoMerge(fields[]) | 자동 셀 병합 |
clearMerge() | 병합 해제 |
내보내기 / 인쇄 / UI
| 메서드 | 설명 |
|---|---|
exportExcel(options?) | Excel 파일 다운로드 |
exportCsv(options?) | CSV 파일 다운로드 |
exportJson(options?) | JSON 파일 다운로드 |
print(options?) | 인쇄 대화상자 열기 |
jumpToRow(rowIndex) | 특정 행으로 스크롤 |
jumpToCol(field) | 특정 열로 스크롤 |
setTheme(theme) | 테마 변경 |
setThemeVar(varName, value) | CSS 변수 하나 변경 |
resize(w?, h?) | 그리드 크기 재계산 |
destroy() | 그리드 제거 및 메모리 정리 |
이벤트 관련
| 메서드 | 설명 |
|---|---|
on(event, handler) | 이벤트 리스너 등록 |
once(event, handler) | 한 번만 실행되는 리스너 등록 |
off(event, handler?) | 이벤트 리스너 해제 |
emit(event, data?) | 이벤트 발생시키기 |
GridOptions 전체 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
columns 필수 | ColumnDef[] | - | 열 정의 배열 |
height | number|string | auto | 그리드 높이 |
rowHeight | number | 32 | 행 높이(px) |
editable | boolean | false | 편집 활성화 |
editMode | string | 'click' | 편집 트리거 방식 |
history | boolean | false | undo/redo 활성화 |
selection | string | 'row' | 선택 모드 |
sortable | boolean | false | 전체 열 정렬 허용 |
multiSort | boolean | false | 다중 정렬 허용 |
filterable | boolean | false | 전체 열 필터 허용 |
frozenColumns | number | 0 | 고정 열 개수 |
frozenRows | number | 0 | 고정 행 개수 |
rowNumber | boolean | false | 행 번호 열 표시 |
checkColumn | boolean | false | 체크박스 열 표시 |
draggable | boolean | false | 행 드래그&드롭 활성화 |
mergeCells | boolean | false | 셀 병합 기능 활성화 |
groupBy | string[] | - | 그룹핑 열 배열 |
treeMode | string | - | 'auto' 또는 'flat' |
treeId | string | 'id' | 트리 ID 필드명 |
treeParentId | string | 'parentId' | 트리 부모ID 필드명 |
expandOnLoad | boolean | false | 로드 시 전체 펼침 |
pagination | boolean | false | 페이징 활성화 |
pageSize | number | 50 | 페이지당 행 수 |
theme | string | 'default' | 테마 이름 |
ariaLabel | string | - | 접근성 레이블 |
contextMenu | boolean | ContextMenuItem[] | false | 우클릭 메뉴 활성화 (true=기본메뉴, 배열=커스텀) |
worksheets | WorksheetDef[] | - | 초기 워크시트 탭 목록 |
신기능 메서드
| 메서드 | 설명 |
|---|---|
setOptions(opts) | 런타임 옵션 갱신 (contextMenu 변경 시 자동 재생성) |
openContextMenu(e, items?) | 컨텍스트 메뉴 직접 열기 |
closeContextMenu() | 컨텍스트 메뉴 닫기 |
addWorksheet(name, cols?, data?) | 워크시트 탭 추가 |
removeWorksheet(name) | 워크시트 탭 제거 |
switchWorksheet(name) | 워크시트 탭 전환 |
renameWorksheet(old, new) | 워크시트 이름 변경 |
getWorksheet(name) | 워크시트 상태(컬럼+데이터) 반환 |
getWorksheetNames() | 전체 시트 이름 배열 반환 |
exportSheetsExcel(filename?) | 모든 워크시트를 다중 시트 xlsx로 내보내기 |
우클릭 컨텍스트 메뉴
셀 위에서 마우스 오른쪽 버튼을 클릭하면 나타나는 팝업 메뉴입니다. 정렬, 찾기, 내보내기 등 기본 메뉴와 내가 만든 커스텀 메뉴를 함께 쓸 수 있어요.
기본 메뉴 켜기
그리드 옵션에 contextMenu: true 한 줄만 추가하면 기본 메뉴가 활성화됩니다.
기본 메뉴에는 오름/내림차순 정렬, 찾기, Excel 저장, CSV 저장, 인쇄가 들어 있어요.
const grid = new OpenGrid('#myGrid', {
columns: [...],
contextMenu: true, // ← 이 한 줄로 끝!
height: 400,
});
커스텀 메뉴 항목 추가
contextMenu에 배열을 넣으면 내 메뉴만 표시됩니다.
기본 메뉴와 내 메뉴를 같이 쓰고 싶으면 setOptions()를 이용하세요 (아래 참조).
const grid = new OpenGrid('#myGrid', {
columns: [...],
contextMenu: [
{ label: '행 복사', icon: 'bi bi-files', action: (e) => copyRow(e.row) },
{ label: '즐겨찾기', icon: 'bi bi-star', action: (e) => bookmark(e.row) },
{ type: 'divider' },
{ label: '상세보기', icon: 'bi bi-zoom-in', action: (e) => detail(e.rowIndex) },
{ label: '비활성 항목', icon: '🚫', disabled: true },
],
height: 400,
});
| ContextMenuItem 필드 | 타입 | 설명 |
|---|---|---|
label | string | 메뉴에 표시되는 텍스트 |
icon | string | 이모지('⭐'), 특수문자('↑'), Bootstrap Icons 클래스('bi bi-star') 모두 가능 |
action | function | string | 클릭 시 실행. 함수면 (e) => {} 형태, 문자열이면 내장 액션 ID |
disabled | boolean | true 시 회색으로 비활성화 |
type | 'divider' | 구분선 (label/action 불필요) |
<head>에<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">를 추가하세요. 이모지나 특수문자는 별도 설치 없이 바로 됩니다.
런타임에 메뉴 바꾸기 — setOptions()
그리드를 만든 뒤에도 setOptions()로 컨텍스트 메뉴를 동적으로 변경할 수 있습니다.
기존 메뉴 인스턴스를 자동으로 정리하고 새로 만들어줘요.
// 기본 메뉴 켜기
grid.setOptions({ contextMenu: true });
// 커스텀 메뉴로 교체
grid.setOptions({
contextMenu: [
{ label: '행 삭제', icon: 'bi bi-trash', action: (e) => grid.deleteRow(e.rowIndex) },
],
});
// 메뉴 끄기
grid.setOptions({ contextMenu: false });
프로그래밍 방식으로 열기 / 닫기
버튼 클릭이나 키보드 이벤트 등 원하는 시점에 직접 메뉴를 열 수도 있습니다.
// 마우스 이벤트 위치에 메뉴 열기
document.getElementById('btn-open').addEventListener('click', (e) => {
grid.openContextMenu(e);
});
// 추가 아이템을 넣어서 열기
grid.openContextMenu(mouseEvent, [
{ label: '특별 기능', icon: '⚡', action: () => doSpecial() },
]);
// 메뉴 닫기
grid.closeContextMenu();
다중 워크시트
엑셀의 시트 탭처럼 하나의 그리드 컨테이너 안에 여러 데이터셋을 탭으로 관리할 수 있습니다. 시트마다 컬럼 구성도 다르게 설정할 수 있어요.
초기 워크시트 설정
옵션에 worksheets 배열을 넣으면 시작부터 여러 탭이 만들어집니다.
첫 번째 시트가 자동으로 활성화됩니다.
const salesCols = [
{ field: 'product', header: '상품명', width: 150 },
{ field: 'amount', header: '판매액', width: 120, type: 'number' },
];
const inventoryCols = [
{ field: 'product', header: '상품명', width: 150 },
{ field: 'stock', header: '재고량', width: 100, type: 'number' },
{ field: 'location',header: '보관위치', width: 120 },
];
const grid = new OpenGrid('#myGrid', {
columns: salesCols, // 기본 컬럼 (시트 컬럼 없을 때 대체)
worksheets: [
{ name: 'Sheet1', columns: salesCols, data: salesData },
{ name: '재고현황', columns: inventoryCols, data: inventoryData },
],
height: 450,
});
런타임에 시트 추가 — addWorksheet()
그리드를 먼저 만들고, 나중에 시트를 동적으로 추가할 수도 있습니다.
첫 번째 addWorksheet() 호출 시 자동으로 탭 UI가 생성됩니다.
const grid = new OpenGrid('#myGrid', {
columns: salesCols,
height: 450,
});
// 버튼 클릭 등 원하는 시점에 시트 추가
document.getElementById('btn-add').addEventListener('click', () => {
grid.addWorksheet('Sheet1', salesCols, salesData);
grid.addWorksheet('재고현황', inventoryCols, inventoryData);
grid.addWorksheet('요약', summaryCols, []); // 빈 시트도 OK
});
시트 전환 / 이름 변경 / 삭제
// 특정 시트로 전환
grid.switchWorksheet('재고현황');
// 시트 이름 변경
grid.renameWorksheet('Sheet1', '판매현황');
// 시트 삭제
grid.removeWorksheet('요약');
// 전체 시트 이름 목록
const names = grid.getWorksheetNames(); // ['판매현황', '재고현황']
// 특정 시트 데이터 읽기
const state = grid.getWorksheet('판매현황');
console.log(state.data); // 해당 시트 데이터 배열
console.log(state.columns); // 해당 시트 컬럼 정의
모든 시트를 Excel 파일로 내보내기
exportSheetsExcel()을 사용하면 모든 워크시트가 하나의 xlsx 파일 안에 여러 시트로 저장됩니다.
엑셀에서 시트 탭을 그대로 볼 수 있어요.
// 전체 시트 → workbook.xlsx (각 시트가 엑셀 탭으로)
grid.exportSheetsExcel();
// 파일 이름 지정
grid.exportSheetsExcel('월간_보고서');
grid.exportExcel()을 사용하세요.
Excel CSS 테마 내보내기
그리드에 적용된 테마 색상을 Excel 파일 스타일로 그대로 옮겨줍니다. 다크 테마로 보던 표를 내보내면 Excel 파일도 다크 스타일로 저장돼요!
테마 스타일 그대로 내보내기
기본 exportExcel()은 이미 styleMode: 'theme'로 동작합니다.
그리드의 CSS 변수(--og-header-bg, --og-row-bg 등)를
자동으로 읽어 Excel 셀 스타일에 적용해요.
추가 설정 없이 그냥 호출하면 됩니다.
// 현재 그리드 테마를 그대로 Excel 파일로 저장
grid.exportExcel();
// 파일 이름 지정
grid.exportExcel('보고서_2026Q2.xlsx');
// 옵션 객체로 세부 조정
grid.exportExcel({
filename: '판매현황.xlsx',
sheetName: '판매데이터', // Excel 시트 탭 이름
includeHeader: true, // 헤더 행 포함 여부 (기본 true)
styleMode: 'theme', // 'theme' | 'none'
exceptFields: ['_internal'], // 제외할 열 필드명 배열
});
| 옵션 | 값 | 설명 |
|---|---|---|
styleMode: 'theme' | 기본값 | 현재 그리드 테마(CSS 변수)를 Excel 스타일로 자동 변환. 헤더 색상, 행 색상, 테두리 모두 그대로 반영. |
styleMode: 'none' | - | 스타일 없이 데이터만 저장. 파일 크기가 작아지고 Excel 기본 테마가 유지됨. |
어떤 CSS 변수가 Excel에 반영되나요?
| CSS 변수 | Excel 적용 위치 |
|---|---|
--og-header-bg | 헤더 행 배경색 |
--og-header-color | 헤더 행 글자색 |
--og-row-bg | 일반 행 배경색 |
--og-row-alt-bg | 홀짝 줄무늬 배경색 |
--og-row-color | 데이터 행 글자색 |
--og-border-color | 셀 테두리 색상 |
--og-font-size | 폰트 크기 |
grid.setTheme('dark')로 테마를 바꾼 다음 내보내면
Excel 파일도 다크 배경으로 저장됩니다.
테마 12종 모두 Excel CSS 내보내기와 연동됩니다.
의존 패키지
Excel CSS 내보내기는 내부적으로 xlsx-js-style 패키지를 사용합니다.
exportExcel()을 처음 호출할 때 자동으로 불러오므로(dynamic import) 평소에는 번들 크기에 영향을 주지 않습니다.
# npm 프로젝트라면 직접 설치도 가능 (CDN 사용 시 불필요)
npm install xlsx-js-style
정밀 소수점 계산 (OGDecimal)
JavaScript의 기본 소수점 계산은 오차가 생깁니다. OGDecimal은 BigInt를 사용해 수백 자리까지 오차 없이 계산하는 도구예요. 금융, 음원 수익 배분, 과학 계산처럼 정밀도가 중요할 때 사용합니다.
왜 OGDecimal이 필요한가요?
JavaScript는 모든 숫자를 IEEE 754 방식으로 저장하는데, 이 때문에 소수점 계산에서 오차가 생깁니다.
// JavaScript 기본 계산의 문제점
console.log(0.1 + 0.2); // 0.30000000000000004 ← 오차!
console.log(1000000.005 * 3); // 3000000.0149999996 ← 오차!
// OGDecimal로 계산하면
import { OGDecimal } from 'open-grid/OGDecimal';
const a = OGDecimal.from('0.1');
const b = OGDecimal.from('0.2');
console.log(a.add(b).toString()); // '0.3' ← 정확!
console.log(a.add(b).toFixed(2)); // '0.30'
OGDecimal.from(0.1)은 이미 오차가 있는 0.1을 그대로 받습니다.
정밀한 값이 필요할 때는 항상 문자열로 넣으세요: OGDecimal.from('0.1')
기본 사용법
import { OGDecimal } from 'open-grid/OGDecimal';
// 생성
const price = OGDecimal.from('1234.567');
const quantity = OGDecimal.from('3');
const discount = OGDecimal.from('0.05'); // 5%
// 사칙 연산 — 모두 새 OGDecimal 반환 (불변)
const subtotal = price.mul(quantity); // 3703.701
const discAmt = subtotal.mul(discount); // 185.18505
const total = subtotal.sub(discAmt); // 3518.51595
// 나눗셈 — precision으로 소수점 자리 수 지정 (기본 20자리)
const unitPrice = total.div(quantity, 10); // 1172.8386500...
// 출력
console.log(total.toString()); // '3518.51595'
console.log(total.toFixed(2)); // '3518.52' (반올림)
console.log(total.toNumber()); // 3518.51595 (number 타입, 표시 전용)
| 메서드 | 설명 |
|---|---|
OGDecimal.from(value) | 문자열·숫자·bigint → OGDecimal 생성. 문자열 권장. |
OGDecimal.zero() | 0 생성 |
.add(other) | 덧셈 |
.sub(other) | 뺄셈 |
.mul(other) | 곱셈 |
.div(other, precision?) | 나눗셈. precision = 결과 소수점 자리수 (기본 20) |
.mod(other) | 나머지 |
.neg() | 부호 반전 (음수→양수, 양수→음수) |
.abs() | 절댓값 |
.toFixed(dp) | 소수점 dp자리까지 반올림 후 문자열 반환 |
.toString() | 후행 0 제거 후 최소 표현 문자열 반환 |
.toNumber() | number로 변환 (표시 전용, 정밀도 손실 주의) |
비교 연산
const a = OGDecimal.from('10.5');
const b = OGDecimal.from('10.50');
a.eq(b) // true — 값이 같다 (0.50 = 0.5)
a.gt('10') // true — 10.5 > 10
a.lt('11') // true — 10.5 < 11
a.gte(b) // true
a.lte(b) // true
a.isZero() // false
a.isNeg() // false
a.isPos() // true
집계 — sum / avg / min / max
배열에 담긴 값들을 오차 없이 합산하거나 평균을 구할 수 있습니다.
const revenues = ['1234.56', '789.01', '456.78', '2345.67'];
// 정확한 합계
const total = OGDecimal.sum(revenues);
console.log(total.toFixed(2)); // '4826.02'
// 정확한 평균 (나눗셈 소수점 10자리)
const avg = OGDecimal.avg(revenues, 10);
console.log(avg.toFixed(4)); // '1206.5050'
// 최솟값 / 최댓값
const min = OGDecimal.min(revenues); // '456.78'
const max = OGDecimal.max(revenues); // '2345.67'
그리드와 함께 쓰기
OGDecimal로 계산한 값을 그리드에 표시할 때는 toFixed()나 toString()으로
문자열로 변환해서 넣거나, type: 'number' 컬럼에 toNumber()로 넣을 수 있습니다.
// 수익 배분 계산 후 그리드에 표시
const royaltyRate = OGDecimal.from('0.0000035'); // 0.00035%
const rows = streamData.map(item => {
const streams = OGDecimal.from(item.streams.toString());
const royalty = streams.mul(royaltyRate);
return {
...item,
royalty: royalty.toFixed(8), // 문자열로 그리드에
};
});
const grid = new OpenGrid('#royaltyGrid', {
columns: [
{ field: 'track', header: '곡명', width: 200 },
{ field: 'streams', header: '스트리밍', width: 120, type: 'number' },
{ field: 'royalty', header: '수익(원)', width: 160 },
],
height: 400,
});
grid.setData(rows);
데이터 마스킹
개인정보(주민번호·전화번호·이메일·카드번호 등)를 *로 가려서 표시하는 기능입니다. 컬럼 단위·셀 단위로 마스킹을 켜거나 끌 수 있습니다.
기본 설정 — mask 옵션
const grid = new OpenGrid('#container', {
columns: [
{ field: 'name', header: '이름', width: 100, mask: { type: 'name' } },
{ field: 'phone', header: '전화번호', width: 140, mask: { type: 'mobile' } },
{ field: 'ssn', header: '주민번호', width: 160, mask: { type: 'ssn' } },
{ field: 'email', header: '이메일', width: 200, mask: { type: 'email' } },
{ field: 'credit', header: '카드번호', width: 180, mask: { type: 'credit' } },
{ field: 'account', header: '계좌번호', width: 180, mask: { type: 'account' } },
],
});
지원 마스킹 타입 10종
| 타입 | 예시 입력 | 마스킹 결과 |
|---|---|---|
ssn | 900101-1234567 | 900101-******* |
phone | 02-1234-5678 | 02-****-5678 |
mobile | 010-1234-5678 | 010-****-5678 |
email | user@example.com | u***@example.com |
credit | 1234-5678-9012-3456 | 1234-****-****-3456 |
account | 110-123-456789 | 110-***-456789 |
password | any value | •••••••• |
name | 홍길동 | 홍*동 |
ip | 192.168.1.100 | 192.***.***100 |
partial | custom | 앞/뒤 n자 보존, 중간 마스킹 |
컬럼 단위 마스킹 토글 API
// 특정 컬럼 마스킹 해제 (원본 표시)
grid.setMaskEnabled('phone', false);
// 다시 마스킹 적용
grid.setMaskEnabled('phone', true);
// 현재 마스킹 상태 조회
const isOn = grid.getMaskEnabled('phone'); // true | false
내보내기 시 마스킹 적용 (maskOnExport)
// Excel 내보내기 — 마스킹된 값으로 출력
grid.exportExcel({ filename: 'masked-data', maskOnExport: true });
// CSV 내보내기 — 원본 값으로 출력 (기본)
grid.exportCsv({ filename: 'raw-data' });
// CSV 내보내기 — 마스킹된 값으로 출력
grid.exportCsv({ filename: 'masked', maskOnExport: true });
부분 마스킹 (partial)
// 앞 3자·뒤 4자 보존, 중간을 * 로 가림
{ field: 'card', mask: { type: 'partial', keep: { front: 4, back: 4 } } }
조직도 (OrgChart)
OrgChart는 계층 데이터를 카드+SVG 연결선으로 시각화하는 독립 컴포넌트입니다. 펼침/접힘, 카드 선택, 테마 연동을 지원합니다.
기본 사용법
import { OrgChart } from 'open-grid';
const chart = new OrgChart('#org-container', {
nodes: [
{ id: 'ceo', parentId: null, name: '홍길동', title: 'CEO' },
{ id: 'cto', parentId: 'ceo', name: '김철수', title: 'CTO' },
{ id: 'cfo', parentId: 'ceo', name: '이영희', title: 'CFO' },
{ id: 'fe', parentId: 'cto', name: '박민준', title: '프론트엔드팀장' },
{ id: 'be', parentId: 'cto', name: '최수진', title: '백엔드팀장' },
],
onNodeClick: (node) => console.log('선택:', node.name),
});
OrgChart 옵션
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
nodes | OrgNode[] | [] | 노드 데이터 배열 |
cardWidth | number | 160 | 카드 너비(px) |
cardHeight | number | 72 | 카드 높이(px) |
hGap | number | 24 | 형제 노드 가로 간격 |
vGap | number | 48 | 레벨 간 세로 간격 |
theme | string | 'default' | 초기 테마 |
onNodeClick | function | — | 카드 클릭 콜백 |
OrgChart API
// 데이터 교체
chart.setData(newNodes);
// 모두 펼치기 / 모두 접기
chart.expandAll();
chart.collapseAll();
// 특정 노드 선택
chart.select('ceo');
// 테마 변경 (그리드와 동기화 가능)
chart.setTheme('dark');
// 현재 선택된 노드 조회
const node = chart.getSelectedNode();
그리드와 테마 동기화
document.querySelectorAll('.theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.dataset.theme;
grid.setTheme(theme);
chart.setTheme(theme); // 동일 테마 동시 적용
});
});
OrgNode 구조
interface OrgNode {
id: string; // 고유 ID
parentId: string | null; // 부모 ID (루트는 null)
name: string; // 표시 이름
title?: string; // 직함/부서
avatar?: string; // 아바타 이니셜 또는 이미지 URL
dept?: string; // 추가 정보 필드
}
페이지네이션
대용량 데이터를 페이지 단위로 나눠 표시합니다. 그리드 하단에 페이지 이동 버튼이 자동으로 렌더링됩니다.
기본 설정
const grid = new OpenGrid('#container', {
columns: [...],
pagination: true, // 페이지네이션 활성
pageSize: 20, // 페이지당 행 수 (기본: 20)
onReady: g => g.setData(largeData),
});
페이지네이션 API
const page = grid.getCurrentPage(); // 현재 페이지 (1부터)
const total = grid.getTotalPages(); // 전체 페이지 수
grid.goToPage(3); // 3페이지로 이동
grid.nextPage(); // 다음 페이지
grid.prevPage(); // 이전 페이지
grid.firstPage(); // 첫 페이지
grid.lastPage(); // 마지막 페이지
grid.setPageSize(50); // 페이지 크기 변경
정렬·필터와 조합
const grid = new OpenGrid('#container', {
columns: [...],
pagination: true,
pageSize: 30,
sortable: true,
filterable: true,
});
exportExcel()/exportCsv()는 현재 페이지가 아닌 전체 데이터를 내보냅니다.서버사이드 페이지네이션
const grid = new OpenGrid('#container', {
columns: [...],
pagination: false, // 클라이언트 페이지네이션 비활성
});
async function loadPage(page, size) {
const res = await fetch(`/api/data?page=${page}&size=${size}`);
const { data, total } = await res.json();
grid.setData(data);
// 커스텀 페이지 UI 직접 업데이트
}
React / Vue 예제
// React
<OpenGridReact columns={cols} pagination pageSize={25} onReady={g => g.setData(data)} />
<!-- Vue 3 -->
<OpenGrid :columns="cols" :pagination="true" :page-size="25" :on-ready="g => g.setData(data)" />
키보드 단축키
OPEN_GRID는 WCAG 2.2 키보드 접근성 기준을 준수합니다. 마우스 없이도 모든 기능을 사용할 수 있습니다.
셀 네비게이션
| 키 | 동작 |
|---|---|
| ↑ ↓ ← → | 셀 이동 |
| Tab | 오른쪽 셀로 이동 |
| Shift + Tab | 왼쪽 셀로 이동 |
| Home | 현재 행의 첫 번째 셀 |
| End | 현재 행의 마지막 셀 |
| PageUp | 10행 위로 이동 |
| PageDown | 10행 아래로 이동 |
행 선택
| 키 | 동작 | 조건 |
|---|---|---|
| Space | 현재 행 선택/해제 | selection: 'multiple' |
| Shift + ↑/↓ | 범위 선택 확장 | selection: 'multiple' |
| Ctrl + A | 전체 선택 | selection: 'multiple' |
| Space | 체크박스 토글 | checkColumn: true |
편집
| 키 | 동작 |
|---|---|
| Enter / F2 | 셀 편집 시작 (editMode: 'none'일 때) |
| Enter (편집 중) | 편집 완료 + 아래 셀로 이동 |
| Tab (편집 중) | 편집 완료 + 오른쪽 셀로 이동 |
| Escape | 편집 취소 (원래 값 복원) |
| Ctrl + Z | 실행 취소 (Undo) |
| Ctrl + Y | 다시 실행 (Redo) |
트리 그리드
| 키 | 동작 |
|---|---|
| Enter / Space | 폴더 아이콘 — 펼침/접힘 토글 |
| → | 접힌 노드 펼치기 |
| ← | 펼친 노드 접기 |
행 드래그 키보드 대안
| 키 | 동작 | 조건 |
|---|---|---|
| Ctrl + ↑ | 현재 행을 위로 이동 | draggable: true |
| Ctrl + ↓ | 현재 행을 아래로 이동 | draggable: true |
aria-live 리전을 통해 스크린리더에 알림이 전달됩니다 (WCAG 2.5.7).헤더 정렬
| 키 | 동작 |
|---|---|
| Enter / Space (헤더 포커스) | 해당 컬럼 정렬 토글 |
접근성 레이블 설정
const grid = new OpenGrid('#container', {
columns: [...],
ariaLabel: '직원 명단 그리드', // role="grid"의 aria-label
});
| WCAG 기준 | 내용 |
|---|---|
| 2.1.1 Keyboard | 모든 기능 키보드로 사용 가능 |
| 2.1.2 No Keyboard Trap | 포커스가 그리드 안에 갇히지 않음 |
| 2.4.7 Focus Visible | 포커스 아웃라인 3px 표시 |
| 2.5.7 Dragging Movements | DnD의 키보드 대안 제공 |
| 1.4.3 Contrast | 모든 상태 색상 4.5:1 이상 대비율 |
변경 추적 (Change Tracking)
그리드에서 사용자가 추가·수정·삭제한 데이터를 서버로 저장하기 전에 취득하는 API입니다. 각 행의 상태(추가·수정·삭제)를 식별할 수 있고, 수정된 행은 어떤 컬럼이 어떻게 바뀌었는지까지 확인할 수 있습니다.
한 번에 전체 변경 내역 가져오기 — getChanges()
const { added, edited, removed } = grid.getChanges();
// 결과는 JSON 객체로 바로 사용 가능
console.log(JSON.stringify({ added, edited, removed }, null, 2));
// edited 행에는 _changedFields(변경된 필드 목록)가 자동 포함됩니다
// → { name: '홍길동', salary: 45000000, _changedFields: ['salary'] }
개별 상태별 취득
// 수정된 행만 (하위 호환: getChangedRows()와 동일)
const edited = grid.getEditedRows();
// 추가된 행만
const added = grid.getAddedRows();
// 삭제 표시된 행만 (화면에서는 숨겨짐)
const removed = grid.getRemovedRows();
수정된 컬럼 상세 diff — getChangedColumns()
어떤 행의 어떤 컬럼이 어떤 값에서 어떤 값으로 바뀌었는지 상세하게 반환합니다.
// 반환값: { row, fields, diff }[] 형태의 JSON 배열
const result = grid.getChangedColumns().map(({ row, fields, diff }) => ({
name: row.name,
fields, // ['salary', 'dept']
diff, // [{ field:'salary', oldValue:40000000, newValue:45000000 }, ...]
}));
console.log(JSON.stringify(result, null, 2));
// [
// {
// "name": "홍길동",
// "fields": ["salary", "dept"],
// "diff": [
// { "field": "salary", "oldValue": 40000000, "newValue": 45000000 },
// { "field": "dept", "oldValue": "개발팀", "newValue": "마케팅팀" }
// ]
// }
// ]
수정 전 원본 값 — getOriginalRow(rowIndex)
// 0번 행의 수정 전 원본 데이터
const original = grid.getOriginalRow(0);
if (original) {
console.log('원본 이름:', original.name);
console.log('원본 연봉:', original.salary);
} else {
// 추가된 행이거나 변경이 없는 경우
console.log('원본 없음 (신규 추가 행)');
}
행 상태 표시 — stateColumn
const grid = new OpenGrid('#container', {
columns: [...],
stateColumn: true, // 맨 왼쪽에 상태 아이콘 열 표시
// + : 추가된 행 (초록)
// M : 수정된 행 (노랑)
// D : 삭제된 행 (빨강, 취소선)
});
서버 저장 패턴 예제
async function saveChanges() {
const { added, edited, removed } = grid.getChanges();
if (!added.length && !edited.length && !removed.length) {
alert('변경된 데이터가 없습니다.');
return;
}
// 내부 필드(_ogRowId, _changedFields) 제거 헬퍼
const clean = ({ _ogRowId, _changedFields, ...rest }) => rest;
const payload = {
added: added.map(clean),
edited: edited.map(r => ({ id: r.id, ...clean(r), _changedFields: r._changedFields })),
removed: removed.map(r => r.id),
};
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
변경 추적 API 요약
| 메서드 | 반환 타입 | 설명 |
|---|---|---|
getChanges() | { added, edited, removed } | 모든 변경사항을 한 번에 반환 |
getEditedRows() | T[] | 수정된 행만 반환 |
getAddedRows() | T[] | 추가된 행만 반환 |
getRemovedRows() | T[] | 삭제 표시된 행 반환 |
getChangedColumns() | { row, fields, diff }[] | 수정 행별 컬럼 단위 diff |
getOriginalRow(rowIndex) | T | undefined | 수정 전 원본 행 데이터 |
getRowState(rowIndex) | 'added'|'edited'|'removed'|'none' | 특정 행의 현재 상태 |
합계 / 소계 (Footer & Subtotal)
그리드 하단(또는 상단)에 SUM·AVG·MIN·MAX·COUNT 집계 행을 표시합니다. OGDecimal을 사용하므로 소수점 누적 오류가 없습니다.
기본 합계 푸터 — setFooter()
const grid = new OpenGrid('#container', {
columns: [...],
footer: [
{ label: '합계', colspan: 2, align: 'left' }, // 앞 2컬럼 병합 레이블
{ field: 'salary', op: 'SUM', format: '#,##0' }, // 52,000,000
{ field: 'bonus', op: 'SUM', format: '#,##0' },
{ field: 'rate', op: 'AVG', format: '#,##0.000' }, // 0.100
{ field: 'score', op: 'MAX', format: '#,##0.0' }, // 95.0
],
footerPosition: 'bottom', // 'top' | 'bottom' (기본: 'bottom')
onReady: g => g.setData(data),
});
FooterDef 옵션
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
field | string | — | 집계 대상 컬럼의 field |
op | 'SUM'|'AVG'|'MIN'|'MAX'|'COUNT' | — | 집계 연산 |
label | string | — | 레이블 텍스트 (op 없이 label만 설정 가능) |
format | string | — | 숫자 포맷 (아래 표 참조) |
align | 'left'|'center'|'right' | right | 셀 정렬 |
colspan | number | 1 | 오른쪽으로 병합할 컬럼 수 |
format 문자열 규칙
| format | 예시 결과 | 설명 |
|---|---|---|
'#,##0' | 52,000,000 | 천단위 콤마, 정수 |
'#,##0.00' | 52,000,000.00 | 천단위 콤마, 소수 2자리 |
'0' | 52000000 | 정수 (콤마 없음) |
'0.00' | 52000000.00 | 소수 2자리 (콤마 없음) |
'2' | 52000000.00 | 숫자만 — 소수 2자리 (하위 호환) |
런타임 푸터 변경 + colspan 레이블 예제
// colspan으로 여러 컬럼을 하나의 레이블 셀로 병합
grid.setFooter([
{ label: '합계', colspan: 2, align: 'left' }, // 이름+부서 2열 병합
{ field: 'salary', op: 'SUM', format: '#,##0', align: 'right' },
{ field: 'bonus', op: 'SUM', format: '#,##0', align: 'right' },
{ field: 'score', op: 'AVG', format: '#,##0.0', align: 'right' },
]);
// op 없이 label만 → 텍스트 전용 셀
grid.setFooter([
{ label: '기준: 전체', align: 'center' }, // 1셀 텍스트 표시
{ field: 'salary', op: 'SUM', format: '#,##0' },
]);
// 푸터 제거
grid.setFooter([]);
집계 값 조회 API
// 특정 필드의 집계 결과 조회
const total = grid.getFooterValue('salary');
console.log('연봉 합계:', total); // 502000000
// 전체 집계 데이터 조회
const all = grid.getFooterData();
// → [{ _field: 'salary', _value: 502000000, _formatted: '502000000' }, ...]
소수점 정밀 계산 (OGDecimal)
JavaScript의 부동소수점 문제로 0.1 × 10 = 1.0000000000000002 같은 오류가 발생합니다. OPEN_GRID 푸터는 OGDecimal로 계산하므로 정확히 1.00을 반환합니다.
// rate 컬럼에 0.1이 10개 있을 때
// ❌ JavaScript float: 0.1+0.1+0.1+... = 1.0000000000000002
// ✅ OGDecimal SUM: = 1.00 (정확)
grid.setFooter([
{ field: 'rate', op: 'SUM', format: '2' }
]);
grid.getFooterValue('rate'); // → 1.00 (정확)
그룹 소계 (각 그룹 행에 합계 표시)
그룹 행이 컬럼 단위로 정렬되어 소계값이 해당 컬럼 위치에 표시됩니다. OGDecimal 정밀 계산 사용.
const grid = new OpenGrid('#container', {
columns: [...],
groupBy: ['dept'], // 부서별 그룹핑
summary: {
fields: ['salary', 'bonus', 'rate', 'score'],
ops: 'SUM', // SUM | AVG | MIN | MAX | COUNT
format: '#,##0.##', // 소계 행의 숫자 포맷
},
onReady: g => g.setData(data),
});
+N 추가된 행 수 | MN 수정된 행 수 | DN 삭제 표시된 행 수
그룹 소계 + 전체 합계 푸터 동시 사용
// ① summary 옵션 먼저 적용 (setOptions는 바로 반영 X)
grid.setOptions({
summary: {
fields: ['salary', 'bonus', 'score'],
ops: 'SUM',
format: '#,##0',
},
});
// ② groupBy 호출 → 소계 포함 그룹 재구성
grid.groupBy(['dept']);
grid.expandAll(); // 모든 그룹 펼치기
// ③ 전체 합계 푸터 (소계와 별개로 하단에 표시)
grid.setFooter([
{ label: '전체합계', colspan: 2, align: 'left' },
{ field: 'salary', op: 'SUM', format: '#,##0' },
{ field: 'bonus', op: 'SUM', format: '#,##0' },
{ field: 'score', op: 'AVG', format: '#,##0.0' },
]);
summary → 각 그룹 행(소계) | footer → 하단 전체 합계둘 다 OGDecimal 정밀 계산을 사용하므로 소수점 오류가 없습니다.
데이터 수정 후 자동 재계산
// 셀 수정 시 푸터 집계값이 자동으로 재계산됩니다
grid.writeCell(0, 'salary', 60000000);
// → 푸터의 salary SUM이 즉시 갱신됨
// 행 추가 / 삭제 시에도 자동 갱신
grid.insertRow({ name: '신규직원', salary: 55000000 }); // 맨 아래 추가
grid.deleteRow(2); // 2번 행 삭제 표시
컬럼 드래그 리오더 (Column Reorder)
헤더 셀을 마우스로 드래그해서 컬럼 순서를 자유롭게 변경합니다. 드래그 중에는 파란 테두리로 드롭 위치를 안내합니다.
기본 설정
const grid = new OpenGrid('#container', {
columns: [
{ field: 'no', header: 'No', width: 60 },
{ field: 'name', header: '이름', width: 120 },
{ field: 'dept', header: '부서', width: 100 },
{ field: 'salary', header: '연봉', width: 110, type: 'number', align: 'right' },
],
columnReorder: true, // ← 헤더 드래그 리오더 활성화
sortable: true,
onReady: g => g.setData(data),
});
리오더 이벤트 — onColumnReorder
const grid = new OpenGrid('#container', {
columns: [...],
columnReorder: true,
onColumnReorder: (e) => {
console.log('컬럼 이동:', e.fromIndex, '→', e.toIndex);
console.log('이동된 컬럼:', e.field);
// e.fromIndex : 드래그 시작 컬럼 인덱스 (0부터)
// e.toIndex : 드롭 위치 컬럼 인덱스
// e.field : 이동된 컬럼의 field 이름
},
});
컬럼 순서 초기화
// 리오더 후 원래 순서로 복원
const originalColumns = [
{ field: 'no', header: 'No', width: 60 },
{ field: 'name', header: '이름', width: 120 },
{ field: 'dept', header: '부서', width: 100 },
];
grid.applyColumns([...originalColumns]);
옵션 요약
| 옵션/이벤트 | 타입 | 설명 |
|---|---|---|
columnReorder | boolean | 헤더 드래그 리오더 활성화 (기본: false) |
onColumnReorder | function | 리오더 완료 시 콜백 ({ fromIndex, toIndex, field }) |
applyColumns(cols[]) | method | 런타임 컬럼 배열 교체 (순서 초기화 등) |
opacity: 0.5)으로 표시되고,
드롭 대상 헤더에는 파란 왼쪽 테두리로 삽입 위치를 안내합니다.
XML 데이터 연동 (XmlConverter)
XML ↔ 그리드 데이터를 양방향으로 변환합니다. Element / Attribute 방식을 자동 감지하고, SAP BAPI XML 응답도 파싱할 수 있습니다. React 18 · Vue 3 · jQuery · AngularJS · Vanilla JS 모두 지원.
기본 파싱 — Element 방식
import { XmlConverter } from 'open-grid';
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<documents>
<document>
<docNo>DOC-001</docNo>
<hkont>523000</hkont>
<wrbtr_d>200000</wrbtr_d>
<sgtxt>5월 팀 회식</sgtxt>
</document>
</documents>`;
// rowTag 자동 감지 — 루트의 첫 번째 자식 tagName 사용
const rows = XmlConverter.parse(xml);
// → [{ docNo:'DOC-001', hkont:'523000', wrbtr_d:'200000', sgtxt:'5월 팀 회식' }]
// number 컬럼은 직접 변환 필요 (XML은 모든 값이 문자열)
const typed = rows.map(r => ({ ...r, wrbtr_d: Number(r.wrbtr_d) || 0 }));
grid.setData(typed);
type:'number' 컬럼은 Number(r.wrbtr_d)||0으로 변환하세요.그리드 → XML 직렬화
// Element 방식 (기본)
const xmlElem = XmlConverter.stringify(grid.getData(), {
rootTag: 'documents', rowTag: 'document',
mode: 'element', // 'element' | 'attribute'
declaration: true, // XML 선언부 포함
excludeFields: ['_ogRowId', '_changedFields'], // 내부 필드 제외
});
// Attribute 방식
const xmlAttr = XmlConverter.stringify(grid.getData(), {
rootTag: 'documents', rowTag: 'document',
mode: 'attribute',
excludeFields: ['_ogRowId'],
});
SAP BAPI XML 응답 파싱
const bapiXml = `<?xml version="1.0"?>
<Response>
<DOCUMENTHEADER><BUKRS>1000</BUKRS><BELNR>1900042</BELNR></DOCUMENTHEADER>
<ACCOUNTGL>
<ITEM><BUZEI>001</BUZEI><HKONT>110100</HKONT><WRBTR>1100000</WRBTR></ITEM>
</ACCOUNTGL>
<RETURN><TYPE>S</TYPE><MESSAGE>전표 1900042 생성 완료</MESSAGE></RETURN>
</Response>`;
const { header, items, returns } = XmlConverter.parseSap(bapiXml);
console.log('전표번호:', header.BELNR); // "1900042"
console.log('처리결과:', returns[0].TYPE); // "S"
grid.setData(items); // 라인 아이템을 그리드에 바인딩
React 18 통합 예시
import { XmlConverter } from 'open-grid';
// 그리드 초기화 시 샘플 자동 로드
const xmlGridRef = useRef(null);
useEffect(() => {
if (xmlGridRef.current) return;
xmlGridRef.current = new OpenGridCore('#grid-xml', {
columns: xmlCols,
onReady: g => {
const rows = XmlConverter.parse(sampleXml).map(r => ({
...r, wrbtr_d: Number(r.wrbtr_d)||0,
}));
g.setData(rows);
},
});
}, [activeSection]);
옵션 요약
| 메서드 | 설명 |
|---|---|
XmlConverter.parse(xml, opts?) | XML → 데이터 배열. rowTag 자동 감지. |
XmlConverter.stringify(data, opts) | 데이터 배열 → XML. mode:'element'|'attribute' |
XmlConverter.parseSap(xml) | SAP BAPI XML → { header, items, returns } |
XmlConverter.stringifySap(payload) | 단건 BAPI 페이로드 → SAP XML |
XmlConverter.stringifySapBatch({ documents }) | 다건 페이로드 → 배치 SAP XML |
트리거 처리 (addTrigger)
그리드 작업(삽입·삭제·편집·정렬 등) 전후에 커스텀 로직을 실행합니다.
before:{op}에서 ctx.cancel()을 호출하면 작업을 중단할 수 있습니다.
before 트리거 — 작업 차단
// ① 이름 없는 행 삽입 차단
grid.addTrigger('before:insertRow', ctx => {
const item = ctx.args[0]; // 삽입될 행 데이터
if (!item?.name?.trim()) {
ctx.cancel(); // 여기서 return해도 cancel됨
alert('이름을 입력해야 합니다.');
}
});
// ② 특정 컬럼 수정 차단
grid.addTrigger('before:writeCell', ctx => {
const [rowIndex, field] = ctx.args;
if (field === 'id') ctx.cancel(); // id 컬럼은 읽기 전용
});
// ③ 조건부 삭제 차단
grid.addTrigger('before:deleteRow', ctx => {
const rows = ctx.extra?.rows || []; // 삭제 대상 행 배열
if (rows.some(r => r.isLocked)) {
ctx.cancel();
alert('잠긴 행은 삭제할 수 없습니다.');
}
});
ctx.cancel()이 호출되면 해당 before 트리거 이후 핸들러와 complete 이벤트 모두 실행되지 않습니다.after 트리거 — 작업 결과 수신
// after 트리거 — ctx.result에 결과가 담김
grid.addTrigger('after:insertRow', ctx => {
console.log('삽입됨:', ctx.result.item.name);
console.log('총 행수:', ctx.result.rowCount);
});
grid.addTrigger('after:deleteRow', ctx => {
console.log('삭제:', ctx.result.deleted, '건, 잔여:', ctx.result.rowCount);
});
grid.addTrigger('after:writeCell', ctx => {
const { field, oldValue, newValue } = ctx.result;
console.log(`변경: ${field} "${oldValue}" → "${newValue}"`);
});
grid.addTrigger('after:orderBy', ctx => {
const sort = ctx.result.sortList[0];
console.log(`정렬: ${sort.field} ${sort.dir}`);
});
complete 트리거 — 공통 완료 핸들러
// complete — 모든 after 이벤트 완료 후 공통 발화
// (before에서 cancel 시에는 발화되지 않음)
grid.addTrigger('complete', ctx => {
const duration = Date.now() - ctx.timestamp;
auditLog({
op: ctx.operation, // 'insertRow' | 'deleteRow' | 'writeCell' 등
args: ctx.args,
result: ctx.result,
duration: duration + 'ms',
});
});
트리거 제거
// 특정 핸들러 제거 (핸들러 참조 필요)
const handler = ctx => { /* ... */ };
grid.addTrigger('before:insertRow', handler);
grid.removeTrigger('before:insertRow', handler);
// 특정 이벤트 전체 초기화
grid.clearTriggers('before:insertRow');
// 모든 트리거 초기화
grid.clearTriggers();
지원 이벤트 목록
| 이벤트 | ctx.args | ctx.result (after) |
|---|---|---|
before/after:insertRow | [item, insertIndex?] | { item, rowCount } |
before/after:deleteRow | [rowIndex] | { deleted, rowCount } |
before/after:writeCell | [rowIndex, field, value] | { rowIndex, field, oldValue, newValue } |
before/after:setData | [dataArray] | dataArray.length |
before/after:orderBy | [field, dir] | { sortList } |
complete | — | same as after result |
SAP 전표처리 (SAP FI Document Posting)
OPEN_GRID를 SAP ERP와 연동하여 매출전표·지급전표·분개전표 등 6가지 전표 유형을 그리드에서 직접 입력하고 BAPI JSON 페이로드를 생성합니다. React 18 / Vanilla JS 데모에서 바로 체험할 수 있습니다.
지원 전표 유형 (6종)
| 전표 유형 | 거래 유형 | T-Code | BAPI |
|---|---|---|---|
| 매출전표 | DR (고객 채권) | FB70 | BAPI_ACC_DOCUMENT_POST |
| 지급전표 | KZ (벤더 지급) | F-53 | BAPI_ACC_DOCUMENT_POST |
| 상계전표 | AB (채권·채무 상계) | F-32 | BAPI_ACC_DOCUMENT_POST |
| 분개전표 | SA (GL 분개) | FB50 | BAPI_ACC_GL_POSTING_POST |
| 선지급전표 | KA (특수 GL) | F-48 | BAPI_ACC_DOCUMENT_POST |
| 부가세전표 | KR (매입 세금계산서) | FB60 | BAPI_ACC_DOCUMENT_POST |
핵심 아키텍처 — sap-core.js
모든 SAP 기능은 examples/sap-core.js에 집중되어 있습니다.
React, Vue, Vanilla 등 어느 프레임워크든 sapCoreInit(OpenGrid) 한 번이면
6가지 전표를 모두 활성화할 수 있습니다.
// React 18
import { sapCoreInit } from '../sap-core.js';
import { OpenGrid as OpenGridCore } from 'open-grid';
// 컴포넌트 마운트 후 1회 초기화
sapCoreInit(OpenGridCore);
// 섹션 활성화 시 그리드 생성
window.initSapSection('sap_sales'); // HTML 구조 생성
window.initSapGrid('sap_sales'); // OpenGrid 초기화
SAP_COL_LIB — 컬럼 원자 사전
모든 SAP 필드를 하나의 사전에 등록합니다.
sapField 메타가 BAPI 페이로드 자동 매핑에 사용됩니다.
const SAP_COL_LIB = {
bschl: { field:'bschl', header:'전기키', width:115, type:'select',
options:[{label:'01 고객차변',value:'01'}, ...],
sapField:'BSCHL' },
wrbtr: { field:'wrbtr', header:'금액', width:130, type:'number',
format:'#,##0', editable:true, sapField:'WRBTR' },
mwskz: { field:'mwskz', header:'세금코드', width:115,
type:'select', sapField:'MWSKZ' },
// ... 40개 필드
};
SAP_DOC_CFG — 전표 유형별 컬럼 조합
const SAP_DOC_CFG = {
sap_sales: {
id:'sap_sales', label:'매출전표', icon:'bi-receipt',
docType:'DR', tcode:'FB70', bapi:'BAPI_ACC_DOCUMENT_POST',
baseCols: ['docNo','buzei','bschl','kunnr','hkont','wrbtr_d','wrbtr_c','mwskz','mwsts','sgtxt'],
extCols: ['txbhw','kostl','prctr','aufnr'], // 체크박스로 on/off
requiredCols:['bschl','hkont'], // B: 필수 필드 유효성 검사
footerFields:['wrbtr_d','wrbtr_c'],
sampleRows: [ ... ],
},
// sap_payment, sap_clearing, sap_journal, sap_advance, sap_vat
};
B. 필수 필드 유효성 검사
requiredCols에 정의된 필드가 비어 있으면 SAP JSON 생성 시 경고를 표시합니다.
페이로드 생성은 계속 진행되므로, 검토 후 수정할 수 있습니다.
// 내부 동작 (sapGenPayload 내)
const errors = sapGetValidationErrors('sap_sales');
// → ['DOC-001 3행: [전기키, GL계정] 미입력', ...]
if (errors.length > 0) {
resultEl.textContent = `⚠ 필수 필드 미입력 (${errors.length}건):\n${errors.join('\n')}\n\n${payload}`;
}
C. 전표 복사
행이 선택된 전표(docNo)의 모든 행을 새 전표 번호로 복제합니다. 동일한 거래 구조를 반복 입력할 때 편리합니다.
// 툴바 '📋 전표 복사' 버튼이 호출하는 함수
window.sapCopyDocument('sap_sales');
// 선택 행의 docNo(예: DOC-002) → 동일 구조의 DOC-004 생성
// → ✅ 전표 복사 완료: DOC-002 → DOC-004 (3행)
SAP BAPI 페이로드 구조
// 📤 SAP JSON 생성 버튼이 만드는 다건 페이로드
{
"totalDocuments": 3,
"totalLines": 9,
"documents": [
{
"BAPI_FUNCTION": "BAPI_ACC_DOCUMENT_POST",
"DOCUMENTHEADER": {
"BUKRS": "1000",
"BLDAT": "2026-05-31",
"BUDAT": "2026-05-31",
"WAERS": "KRW",
"XBLNR": "DOC-001"
},
"ACCOUNTGL": [
{ "BUZEI": "001", "BSCHL": "01", "KUNNR": "C0001", "HKONT": "110100", "WRBTR": 1100000 },
{ "BUZEI": "002", "BSCHL": "50", "HKONT": "400100", "WRBTR": 1000000, "MWSKZ": "A1" },
{ "BUZEI": "003", "BSCHL": "50", "HKONT": "251100", "WRBTR": 100000 }
],
"_meta": { "docNo": "DOC-001", "docType": "DR", "balanced": true }
}
]
}
확장 컬럼 — 원가센터 · 외화 등
체크박스로 extCols를 on/off합니다. 외화 컬럼(waers_fc, wrbtr_fc, kursf, dmbtr)
활성화 시 💱 환율 계산기가 자동으로 활성화됩니다.
// 확장 컬럼 토글 — 체크박스 onchange 시 자동 호출
window.toggleSapExtCol('sap_sales', 'kostl', true); // 원가센터 추가
window.toggleSapExtCol('sap_sales', 'waers_fc', true); // 외화 통화 추가
// → grid.applyColumns(getSapCols('sap_sales')) 자동 실행
① SAP_DOC_CFG에 항목 추가 → ② HTML에
<section id="sec-sap_custom"> 추가 →
③ 사이드바 버튼 추가. sap-core.js가 나머지를 자동 처리합니다.
React 18 통합 예시
// examples/react/App.tsx
// @ts-ignore
import { sapCoreInit } from '../sap-core.js';
const SAP_IDS = ['sap_sales','sap_payment','sap_clearing',
'sap_journal','sap_advance','sap_vat'] as const;
// useEffect — activeSection이 SAP 섹션일 때 초기화
useEffect(() => {
if (activeSection.startsWith('sap_')) {
setTimeout(() => {
if (!sapInitRef.current) {
sapCoreInit(OpenGridCore);
sapInitRef.current = true;
}
window.initSapSection(activeSection);
window.initSapGrid(activeSection);
}, 50);
}
}, [activeSection]);
// JSX — 각 SAP 섹션은 빈 div 컨테이너만 렌더링
{SAP_IDS.map(docId => (
<section key={docId} className="demo-section"
style={activeSection===docId ? undefined : {display:'none'}}>
<div id={`sec-${docId}`}></div>
</section>
))}