﻿# GeeGrid v3 공개 문법 명세

언어 선택: [한국어](/GEEGRID_SYNTAX.ko.md) | [English](/GEEGRID_SYNTAX.en.md)

이 명세의 기본 화면은 `gee_search` 요청을 현재 소스 트리의 `/home/git/html/searchtool` 프로젝트로 해석한 것이다. `search.html`, `common-app.js`, `search-page.js`, `db-query-result.js`와 해당 프로젝트의 `html/js/geegrid_v3` 실제 소스에서 확인한 공개 사용법만 정리한다. 내부 렌더링 구현은 제외한다.

## 로드

```html
<link rel="stylesheet" href="/js/geegrid_v3/geegrid.css">
<link rel="stylesheet" href="/js/geegrid_v3/geegrid-template.css">
<script src="/js/geegrid_v3/gee.common.js"></script>
<script src="/js/geegrid_v3/gee.util.js"></script>
<script src="/js/geegrid_v3/gee.grid.js"></script>
<script src="/js/geegrid_v3/gee.object.js"></script>
<script src="/js/geegrid_v3/gee.search.js"></script>
```

```js
var grid = new GeeGrid('userGrid');
var search = new GeeSearch('searchTool');
var form = new GeeObject('settingsForm');
```

루트 ID가 DOM에 있는 뒤 생성자를 호출한다. `GeeGrid`는 `<geegrid>`·`<column>`, `GeeSearch`는 `<item>`, `GeeObject`는 입력 요소를 읽는다. 단, `searchtool` 표준 화면은 `GeeSearch`를 생성하지 않고 네이티브 `textarea`·`input`·`button`을 직접 읽는다.

## 컴포넌트

| 요소 | 역할 | API |
| --- | --- | --- |
| `<geegrid>` + `<column>` | 표·컬럼·정렬·페이징 | `new GeeGrid(id)` |
| `<item>` | 검색·입력 항목 | `new GeeSearch(id)` 또는 `new GeeObject(id)` |
| `<gg-layout>` | 선언형 배치 | `GeeObject.loadLayouts()` |
| `<gg-frame>` | 도킹·분할 | `GeeObject.layout` |
| `<gg-layer>` | 독립 창·팝업 | `GeeGrid.layer`·`GeeObject.layout` |
| `<gg-tabs>` / `<gg-tab>` | 탭 | `GeeObject.tabs(id)` |
| `<gg-tree>` | 트리 | `GeeObject.tree(id, data)` |
| `<gg-progress>` | 진행률 | 요소의 `start`, `setValue`, `end`, `error` |

`<cond>`는 현재 기준 v3 소스에서 확인되지 않았으므로 사용하지 않는다. 조건 표시가 필요하면 데이터를 JavaScript로 가공한다.

## Grid

```html
<geegrid id="userGrid" width="100%" height="420px" numbering="true"
         pageable="true" rows="20" handler="load_users" sort_handler="sort_users">
  <column id="id" type="number" width="80px" align="right">ID</column>
  <column id="displayName" dataid="name" type="html" width="180px"
          sortable="true" orderBy="name" renderer="render_name"
          onclick="open_user">이름</column>
</geegrid>
```

루트 속성: `id`, `width`, `height`, `pageable`, `rows`, `page`, `numbering`, `number_width`, `check_column`, `handler`, `sort_handler`, `fixCol`, `leftWidth`, `rightWidth`, `rowHeight`, `show_totalcount`, `dynamic_scroll`, `dynamic_load`, `dynamicthreshold`, `dynamichandler`, `status`, `debug`, `formName`, `key`, `value`.

컬럼 공통 속성: `id`, `dataid`, `type`, `title` 또는 본문, `width`, `align`, `class`, `style`, `resize`, `sortable`, `orderBy`/`orderby`, `merge`, `mergekey`, `renderer`, `onclick`, `ondblclick`, `onchange`, `data`, `jsondata`, `keytext`, `keyvalue`.

컬럼 타입: `html`, `input`, `textarea`, `select`, `selectbox`, `dropdown`, `radio`, `checkbox`, `calendar`, `number`, `range`, `button`, `code`, `key`, `numberformat`, `hidden`, `numbering`, `check_column`, `define`.

추가 속성: `textEvent`, `textHandler`, `editMask`, `textColor`, `textCursor`, `readonly`, `disabled`, `cols`, `rows`, `flow`, `drag`, `text`, `value`.

```js
var statusCodes = [['READY', '준비'], ['DONE', '완료']];
grid.setJsonData([{ id: 1, name: '홍길동', status: 'READY' }]);
grid.setObjectData([{ id: 1, name: '홍길동' }]);
grid.setArrayData(['id', 'name'], [[1, '홍길동']]);
grid.setTotalCount(120);
grid.setCurrentPage(1);
grid.setViewLength(20);
grid.makePaging();
var rows = grid.getData();
var value = grid.getValue(0, 'name');
grid.setValue(0, 'name', '김철수');
```

객체 행은 키를 사용하고 배열 행은 `fieldNames`를 먼저 전달한다. `dataid`는 컬럼 ID와 행 키가 다를 때 사용한다. `data`는 `[value, label]` 배열이며 JSON 배열은 `keytext`, `keyvalue`로 매핑한다.

`dynamic_scroll`은 메모리의 전체 데이터 렌더링 최적화이고, `dynamic_load`는 API의 다음 페이지 추가 적재다. 두 속성을 함께 사용하지 않는다.

## 검색·입력

### gee_search 표준: 네이티브 입력 + GeeGrid

`search.html`에서 확인된 검색 입력은 다음과 같다.

```html
<textarea id="keywordsText" rows="4" placeholder="한 줄에 하나씩 입력..."></textarea>
<input id="caseSensitiveChk" type="checkbox">
<input id="wholeWordChk" type="checkbox">
<input id="quoteModeChk" type="checkbox">
<input id="excludeCommentChk" type="checkbox">
<button type="button" id="scanToggleBtn">검색</button>
<gg-progress id="scanProgress" auto="20" i18n="search.progress">진행상태</gg-progress>
```

```js
var scanResultGrid = new GeeGrid('scanResultGrid');
document.getElementById('scanToggleBtn').addEventListener('click', function () {
  var options = {
    keywords: document.getElementById('keywordsText').value,
    caseSensitive: document.getElementById('caseSensitiveChk').checked,
    wholeWord: document.getElementById('wholeWordChk').checked,
    quoteMode: document.getElementById('quoteModeChk').checked,
    excludeComment: document.getElementById('excludeCommentChk').checked
  };
  fetch('/api/scan/start', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(options)
  }).then(function (r) { return r.json(); }).then(function (result) {
    var rows = Array.isArray(result.rows) ? result.rows : [];
    scanResultGrid.setTotalCount(Number(result.count || rows.length));
    scanResultGrid.setCurrentPage(1);
    scanResultGrid.setViewLength(rows.length || 1);
    scanResultGrid.setObjectData(rows);
    scanResultGrid.makePaging();
  });
});
```

`searchtool`의 결과 컬럼은 `file`, `extension`, `matchedKeywordsText`, `managementSelected`, `managementStatusHtml`, `managementCommentHtml`, `lineHtml`, `source`이며 `managementSelected`는 `checkbox` 컬럼과 `onchange="search_result_management_selection_change"`를 사용한다.

### GeeSearch item 방식

```html
<div id="searchTool" width="100%" titleWidth="100px" itemWidth="240px">
  <item id="keyword" type="text" title="검색어" placeholder="이름" maxlength="50"></item>
  <item id="status" type="select" title="상태" data="statusCodes"></item>
  <item id="period" type="period" title="기간" fromValue="2026-01-01" toValue="2026-12-31"></item>
</div>
```

`GeeSearch` 타입: `hidden`, `number`, `blank`, `text`, `input`, `select`, `combo`, `checkbox`, `check`, `radio`, `dropdown`, `period`, `calendar`, `popup`, `button`, `html`, `loop`.

item 속성: `id`, `type`, `title`, `value`, `width`, `itemWidth`, `titleWidth`, `align`, `data`, `jsondata`, `keytext`, `keyvalue`, `disabled`, `readonly`, `required`, `placeholder`, `maxlength`, `className`, `event`, `handler`, `flow`, `disableValue`, `disabledValue`, `fromValue`, `toValue`.

```js
var search = new GeeSearch('searchTool');
var encoded = search.getFormData();
var keyword = search.getValue('keyword');
search.setValue('keyword', '홍');
search.reset();
```

`jsondata`가 있으면 JSON 배열을 우선한다. 일반 입력 폼은 `new GeeObject(id)`와 `getFormData()`를 사용할 수 있다.

## 이벤트·콜백

속성에는 실행식이 아니라 등록 이름을 둔다.

```html
<geegrid id="userGrid" handler="load_users" sort_handler="sort_users">
  <column id="name" renderer="render_name" onclick="open_user" onchange="name_changed">이름</column>
</geegrid>
<button type="button" click="refresh_users" confirm="정말 조회할까요?">조회</button>
```

```js
GeeGrid.on('refresh_users', function () { loadUsers(); });
GeeGrid.on('render_name', function (payload) {
  payload.cell.textContent = String(payload.value || '');
});
GeeGrid.on('open_user', function (payload) {
  console.log(payload.gridId, payload.rowIndex, payload.row);
});
GeeGrid.on('name_changed', function (payload) {
  console.log(payload.columnId, payload.value, payload.element);
});
```

payload에서 소스가 확인하는 필드는 콜백에 따라 `gridId`, `rowIndex`, `columnIndex`, `columnId`, `value`, `options`, `dataType`, `row`, `data`, `cell`, `element`, `event`다. `GeeGrid.on`, `off`, `emit`, `invoke`, `source`는 전역 API이며 인스턴스에도 같은 이름이 있다.

```html
<column id="memo" type="input" textEvent="input" textHandler="memo_input">메모</column>
```

```js
GeeGrid.on('memo_input', function (payload) {
  payload.row.memo = payload.value;
});
```

탭 이벤트는 `geegrid-tabs-change`, `geegrid-tabs-add`, `geegrid-tabs-close`, `geegrid-tabs-rename`, 트리 이벤트는 `gee-tree-select`, `gee-tree-toggle`다.

## 템플릿 바인딩

```html
<div id="cards">
  <loop><article><b>{{name}}</b><span>{{status}}</span><i>{{LOOP_SEQ}}</i></article></loop>
</div>
```

```js
GeeObject.render('cards', [
  { name: '홍길동', status: 'READY' },
  { name: '김철수', status: 'DONE' }
], '데이터가 없습니다.', true);
```

시그니처는 `GeeObject.render(domid, data, defaultMessage, initFlag)`다. `initFlag=true`는 초기화 후 출력하고 `false`는 뒤에 추가한다. 확인된 치환은 `{{field}}`, `{{LOOP_SEQ}}`, `GEE_GLOBAL_VARS`다.

## 레이아웃·레이어

```html
<gg-layout type="page" direction="column" rows="auto 1fr auto" gap="12px">
  <gg-layout type="header" direction="row" justify="end" gap="8px"><h1>사용자</h1></gg-layout>
  <gg-layout type="body" direction="row" columns="1fr 280px" gap="12px">
    <main><geegrid id="userGrid"></geegrid></main><aside>필터</aside>
  </gg-layout>
  <gg-layout type="footer" direction="row" justify="end"><button>닫기</button></gg-layout>
</gg-layout>
<button type="button" layer="userLayer">상세</button>
<gg-layer id="userLayer" layer-title="사용자" width="640px" height="480px" close="true" resize="true">
  <header>사용자</header><main>상세 내용</main>
  <footer><button type="button" click="save_user">저장</button><button type="button" click="geegrid_layer_close">닫기</button></footer>
</gg-layer>
```

`gg-layout` 속성은 `type`, `direction`, `columns`, `rows`, `gap`, `justify`, `align`, `wrap`, `span`, `width`, `height`다. `gg-layer`에서 확인된 속성은 `layer-title`, 크기·위치 속성, `drag`, `resize`, `modal`, `snap`, `collapse`, `close`, `minimize`, `fullscreen`, `fold`, `include`, `definition`, `anchor`, `anchor-target`다.

```js
GeeGrid.on('save_user', function (payload) {
  GeeObject.layout.closeLayer(payload.button.closest('gg-layer').id);
});
GeeObject.layout.openWindowLayer('userLayer');
GeeObject.layout.closeWindowLayer('userLayer');
```

동적 공개 API는 `GeeGrid.layer.open({ key, title, ... })`, `getElement({ key })`, `getId(key)`, `ready({ key })`다. 공통 버튼 이름은 `geegrid_layer_close`, `geegrid_layer_minimize`, `geegrid_layer_fullscreen`이다.

## API·쿼리 연계

```js
function loadUsers() {
  var formParams = new URLSearchParams(search.getFormData());
  var pageParams = grid.makePageParams().replace(/^&/, '');
  fetch('/api/users?' + formParams.toString() + '&' + pageParams)
    .then(function (response) { return response.json(); })
    .then(function (ret) {
      grid.setTotalCount(Number(ret.count || 0));
      grid.setCurrentPage(Number(ret.page || 1));
      grid.setViewLength(Number(ret.rows || 20));
      grid.setJsonData(Array.isArray(ret.records) ? ret.records : []);
      grid.makePaging();
    });
}
```

`makePageParams()`는 `page`, `pageSize`, `startIDX`, `endIDX`, `orderColumn`, `orderMethod`를 반환한다. 권장 응답은 다음과 같다.

```json
{"result":"success","count":120,"page":1,"rows":20,"records":[]}
```

기존 API 래퍼 형식:

```js
function API_USER_LIST(param, callback) {
  this.url = '/users';
  this.params = ['keyword', 'page', 'pageSize', 'orderColumn', 'orderMethod'];
  this.param = param;
  this.callback = callback;
  return CALL_JSON(this);
}
```

`params` 이름에 `*`가 붙으면 기존 `CALL_JSON`이 필수값으로 검사한다. 서버는 정렬 컬럼을 허용 목록에 매핑하고 `asc`·`desc`만 처리한다. 서버 SQL·인증 구현은 이 문서 범위가 아니다.

## 확인되지 않은 내용

`searchtool`과 다른 프로젝트의 v3 파일은 완전히 같지 않다. 실제 브라우저 렌더링, 공개 배포 경로, 실제 API 응답은 실행 검증하지 않았다. Runtime·Builder 내부 구현, 배포·복구 알고리즘, 라이선스·보안 로직, 인증정보는 이 명세에 포함하지 않는다.
