# OneGrid 사용 안내 (AI용) 이 폴더는 **OneGrid 배포본**입니다. 병원 HIS(React + Spring Boot + PostgreSQL)에서 쓰는 고속 데이터 그리드이며, 다른 프로젝트에 **include(복사·참조)해서 그대로** 씁니다. 코드를 생성하기 전에 이 문서를 끝까지 읽고, 옵션·API 세부는 `docs/REFERENCE.md`(전체 옵션 표)와 `esm/*.d.ts`(타입 정의)를 확인하세요. 문서에 없는 옵션 이름을 지어내지 마세요. ## 1. 무엇을 include 할지 고르기 | 상황 | 쓸 파일 | 불러오는 법 | |---|---|---| | 빌드 도구 없는 HTML·JSP·Thymeleaf·레거시 화면 | `browser/onegrid.min.js` (CSS 포함, 의존성 없음) | `` → 전역 `onegrid` | | ES 모듈(Vite·webpack·순수 ` ``` 더 큰 예: `examples/plain-html/index.html`(편집·콤보·필터·합계·변경분 보기), `examples/plain-html/spring-fetch.html`(서버 조회·저장). ## 4. React ```tsx import { useEffect, useMemo, useRef } from 'react' import { Dataset, OneGrid, GridColumn, fetchColumnar, saveChanges } from './onegrid/esm/index.js' import { OneGridView, useDataset, useDatasetStatus } from './onegrid/esm/react.js' export function Orders({ ptno }: { ptno: string }) { const ds = useDataset(() => new Dataset([{ id: 'ordCd' }, { id: 'ordNm' }, { id: 'dose', type: 'number' }])) const status = useDatasetStatus(ds) // { rows, inserted, updated, deleted, dirty } const grid = useRef(null) const columns = useMemo(() => [ { id: 'ordCd', header: '처방코드', editor: 'combo', items: [{ value: 'D0001', label: '타이레놀정' }], comboShow: 'value', required: true }, { id: 'ordNm', header: '처방명', editor: false }, { id: 'dose', header: '1회량', editor: 'number', summary: 'sum' }, ], []) useEffect(() => { fetchColumnar(ds, `/api/patients/${ptno}/orders`) }, [ds, ptno]) const save = async () => { if (grid.current!.validate().length) return const res = await saveChanges(ds, `/api/patients/${ptno}/orders`) // 성공하면 자동 commit if (!res.ok) grid.current!.showServerErrors(res.errors) } return <> } ``` - `columns`·`items` 는 `useMemo` 로 고정하세요(매 렌더 새 배열이면 다시 설정됩니다). - Vite 에서 이 폴더를 심볼릭 링크로 쓰면 `resolve: { dedupe: ['react', 'react-dom'] }`. - 쿼리 정의 화면(서버 `screen_def`)은 `` 한 줄 — `docs/REFERENCE.md` "디자인 유형 + 쿼리 정의 화면". ## 5. Spring Boot 의존성 (Maven): ```xml kr.co.onebiohealthonegrid-spring-core1.0.0 system${project.basedir}/libs/onegrid-spring-core-1.0.0.jar ``` (사내 Nexus 가 있으면 `mvn install:install-file -Dfile=libs/onegrid-spring-core-1.0.0.jar -DgroupId=kr.co.onebiohealth -DartifactId=onegrid-spring-core -Dversion=1.0.0 -Dpackaging=jar` 로 올려 일반 의존성으로 쓰는 편이 낫습니다.) 컴포넌트 스캔: 앱 클래스에 `@SpringBootApplication(scanBasePackages = {"내.패키지", "kr.co.onebiohealth.onegrid.core"})` — `FastGzipFilter`(JSON gzip 수준 1), `GridTransactionAdvice`(저장 오류 400/409), `RowEventHub`(실시간 SSE)가 등록됩니다. `FastGzipFilter` 를 쓰면 `server.compression.enabled=false`. ```java @RestController @RequestMapping("/api/patients/{ptno}/orders") public class OrderController { public record OrderRow(Long ordSeq, String ordCd, String ordNm, Double dose, Integer ver, Map _orig) {} private final JdbcTemplate jdbc; public OrderController(JdbcTemplate jdbc) { this.jdbc = jdbc; } @GetMapping public GridData list(@PathVariable String ptno) { // 조회: 컬럼형 return jdbc.query("SELECT ord_seq, ord_cd, ord_nm, dose, ver FROM patient_order WHERE ptno = ? ORDER BY ord_seq", GridData::fromResultSet, ptno); } @PostMapping @Transactional public ResponseEntity save(@PathVariable String ptno, @RequestBody GridChanges ch) { // 저장: 행 상태별 List errors = new ArrayList<>(); for (int i = 0; i < ch.getInserted().size(); i++) if (ch.getInserted().get(i).dose() == null) errors.add(new GridError("inserted", i, "dose", "1회량은 필수입니다")); if (!errors.isEmpty()) return ResponseEntity.badRequest().body(Map.of("errors", errors)); // → 그리드가 해당 셀에 표시 for (OrderRow d : ch.getDeleted()) jdbc.update("DELETE FROM patient_order WHERE ord_seq = ?", d.ordSeq()); for (OrderRow r : ch.getInserted()) jdbc.update("INSERT INTO patient_order (ptno, ord_cd, ord_nm, dose) VALUES (?, ?, ?, ?)", ptno, r.ordCd(), r.ordNm(), r.dose()); for (OrderRow u : ch.getUpdated()) { // ver 낙관적 잠금 → 0건이면 409 int n = jdbc.update("UPDATE patient_order SET ord_cd = ?, ord_nm = ?, dose = ?, ver = ver + 1 WHERE ord_seq = ? AND ver = ?", u.ordCd(), u.ordNm(), u.dose(), u.ordSeq(), u.ver()); if (n == 0) return ResponseEntity.status(409).body(Map.of("conflicts", List.of(Map.of("section", "updated", "index", ch.getUpdated().indexOf(u))))); } return ResponseEntity.ok(Map.of("data", list(ptno))); } } ``` - MyBatis: `GridDataResultHandler` (resultType `java.util.LinkedHashMap`, `mybatis.configuration.call-setters-on-nulls=true`). - 대량(20만 행 이상)·서버 정렬/필터: `GridQuery` + 화면 옵션 `rowModel: 'server'` — `docs/REFERENCE.md` "서버 행 모델 화면 추가". - 여러 Dataset 한 트랜잭션: 클라이언트 `saveTransaction`, 서버 `GridTransaction`. - 예제 전체: `examples/spring/`. ## 6. 자주 쓰는 옵션·API 요약 | 하고 싶은 것 | 쓰는 것 | |---|---| | 편집 켜기 / 행 상태 표시 | `editable: true`, `showRowState: true`, `showIndicator: true` | | 컬럼 편집기 | 컬럼 `editor: 'text'·'number'·'date'·'combo'·'check'·'textarea'·'time'·'spin'·'button'·'check3'·'radio'` 또는 `false`(읽기 전용) | | 콤보 | `items: [{ value, label }]`, `comboShow: 'value'·'label'·'both'`, 다중 컬럼 룩업 `listColumns: [{ field, header, width }]` | | 필수·검증 | `required: true`, `validate: (v, row, ds) => '오류' | null`, 비동기 `validateAsync`, 행 사이 `uniqueBy`·`maxSum`·`validateRows` | | 행별 편집 규칙 | `editRule: (row, ds) => ({ readOnly, required, editor, items })` | | 필터·검색 | `showFilterRow`, `setFilter(col, '>=140')`, `setQuickSearch(text)`, Ctrl+F 찾기 패널 | | 정렬 | 머리글 클릭(Shift 다중), `setSort([{ col, dir }])`, 콤보 명칭으로 `sortByDisplayText: true` | | 합계·그룹 | 컬럼 `summary: 'sum'` / `summaries: […]`, `setGroupBy(['dept'])`, `groupFooter` | | 고정·병합 | 컬럼 `pinned: true·'right'`, `merge: true`, `headerGroups` | | 새 행 입력 | `addRow(values)`, `appendRowOnTab`, 새 항목 행 `newItemRow: true` | | 삭제 | `deleteRows()` (확인 창: `deletingConfirmation: true`), 되돌리기 `revertRows`/`ds.revertAll()` | | 키 기준 갱신 | `ds.keyCol = 'ordSeq'`(복합: `['ptno','ordSeq']`), `ds.applyTransaction`, `ds.mergeColumnar` | | 이벤트 | `onCellChange`, `onCellClick`, `onRowFocus`, `onCellFocus`, `onSelectionChange`, `grid.on('beforeRowFocus', …)` | | 출력 | `exportXlsx`, `exportCsv`, `exportPdf`, `printGrid`, 서버 대용량 `exportServerXlsx(url)` | | 사용자 화면 설정 저장 | `layoutKey` + `layoutStore: springLayoutStore('/api/layouts', userId)`, 필터 포함 `saveFilters: true` | | 개인정보 | 컬럼 `privacy: 'name'·'rrn'·'phone'`, 해제 감사 `onAudit` | | 다국어 | `locale: 'ko'·'en'` | ## 7. 하지 말 것 - 그리드 안에서 직접 `fetch` 해서 DOM 을 만들지 말고, 항상 Dataset 에 넣으세요. - `ds.getChanges()` 를 그리드 없이 바로 부르기 전에 `grid.validate()` 로 새 항목 행·검증을 먼저 정리하세요. - 역할별 컬럼 숨김(`access`)은 화면 제어일 뿐입니다. 민감 컬럼은 서버에서 `ColumnAccess` 로 응답에서 빼세요. - `eval`/`new Function` 을 쓰는 사용자 코드(수식 문자열 실행 등)를 넣지 마세요. 수식은 컬럼 `expr`(자체 파서)을 씁니다. - 100만 행을 한 번에 받는 화면은 피하고 서버 행 모델을 쓰세요(클라이언트 모드는 20만 행 안쪽 권장). ## 8. 성능 참고 (측정값) | 상황 | 결과 | |---|---| | 1,000,000행 × 11컬럼 (헤드리스) | 바인딩 후 첫 화면 수십 ms, 스크롤 60fps | | 200컬럼 × 100,000행, PostgreSQL → Spring → 그리드 (Win10 Edge) | 전체 5.2초 (서버 1.7 · 수신 2.5 · 파싱 0.48 · 바인딩 0.52), 스크롤 60fps | | 같은 200컬럼 화면, AG Grid 33 대비 | 대각 점프 64fps vs 14fps, 정렬 46ms vs 709ms, 필터 35ms vs 454ms | ## 9. 폴더 안내 | 경로 | 내용 | |---|---| | `browser/` | `onegrid.min.js`(전역 `onegrid`), `onegrid.js`(읽기용), `onegrid.esm.js`(한 파일 ESM), 소스맵 | | `esm/` | 모듈별 ES 모듈 + 타입 정의(`*.d.ts`) — 옵션 이름은 `grid.d.ts` 의 `GridOptions`·`GridColumn` 이 정답 | | `spring/` | 서버 코어 jar·소스 jar, `pom-dependency.xml` | | `examples/` | `plain-html/`, `react/`, `spring/` | | `docs/REFERENCE.md` | 전체 기능·옵션·API 표, 키보드, 측정 (원본 README) | | `CHANGELOG.md`, `VERSION` | 변경 이력·빌드 정보 |