SpeedMIS v7 소개
SpeedMIS는 코딩 없이 업무 시스템(MIS)을 구축하는 노코드 웹 빌더입니다. 2005년부터 21년간 발전해온 기업용 플랫폼으로, 메뉴와 필드를 정의하면 목록·조회·수정·삭제 화면이 자동 생성됩니다.
v7 기술 스택
| 구분 | 기술 |
|---|---|
| 백엔드 | PHP 8.3 + Slim 4 |
| 프론트엔드 | React 18 + Vite |
| 데이터베이스 | MariaDB 10.11 / MySQL 8 |
| 스타일 | Tailwind CSS + CSS 변수 디자인 시스템 |
| 코드 에디터 | Monaco Editor (VS Code 엔진) |
핵심 특징
- 노코드 CRUD — 메뉴+필드 등록만으로 완전한 프로그램 자동 생성
- 서버 훅 시스템 — 16개 이벤트 훅으로 비즈니스 로직 확장 (PHP)
- 실시간 목록편집 — 그리드에서 직접 값 수정, Enter로 셀 이동
- 인쇄양식 빌더 — HTML 템플릿 + 자식 프로그램 루프
- 마스터-디테일 — 부모-자식 프로그램 탭으로 연결
- 모바일 자동 대응 — 토스/Samsung One UI 감성 카드형 UI
- 다크모드 — 라이트/다크 원클릭 전환
- 공통 로직 —
_common.php+_common_udef.php
화면 구성 가이드
SpeedMIS v7의 데스크톱 화면은 크게 탑바(Topbar), 사이드바(Sidebar), 메인 영역(Main)으로 구성됩니다. 아래 다이어그램에서 각 영역의 이름과 역할을 확인하세요.
전체 레이아웃
| No | 거래처명 | 대표자 | 전화번호 | 지역 |
|---|---|---|---|---|
| 1 | 삼성전자 | 이재용 | 02-1234-5678 | 서울 |
| 2 | LG전자 | 구광모 | 02-9876-5432 | 서울 |
| 3 | 현대모비스 | 정의선 | 031-123-4567 | 경기 |
| 4 | SK하이닉스 | 박정호 | 031-789-0123 | 경기 |
| 5 | 포스코 | 최정우 | 054-220-0114 | 경북 |
영역별 명칭과 역할
| 영역 | 이름 | 설명 |
|---|---|---|
| ■ 탑바 | Topbar | 로고, 열린 프로그램 탭, 사용자 이름, 설정(⚙) 버튼. 높이 48px 고정 |
| ■ 사이드바 | Sidebar | 1레벨(카테고리)·2레벨(프로그램) 메뉴 트리. 펼침 240px, 접힘 56px |
| ■ 툴바 | Toolbar | 필터 드롭다운, 검색, 엑셀/신규 버튼, 전체 건수 표시 |
| ■ 그리드 | DataGrid | 목록 테이블. 컬럼 헤더 클릭 정렬, 행 클릭으로 패널 열기, 인라인 편집 지원 |
| ■ 패널 | Panel (DataForm) | 선택한 행의 상세·수정 폼. 탭으로 자식 프로그램 전환, 4단계 크기 조절(1~4) |
| ■ 페이저 | Pagination | 페이지 네비게이션. 기본 50건/페이지, pageSize 파라미터로 조절 |
탑바(Topbar) 상세
| 요소 | 설명 |
|---|---|
| 로고 | 사이트 타이틀 (.env의 SITE_TITLE). 클릭 시 홈 |
| 프로그램 탭 | 열린 프로그램 목록. 클릭으로 전환, ✕로 닫기. 다중 탭 지원 |
| 사용자 이름 | 로그인 사용자. 클릭 시 설정 패널 열림 |
| 설정 ⚙ | 다크/라이트 모드, PC/모바일 전환, 개발자 모드, 보기 설정 통합 패널 |
사이드바(Sidebar) 상세
- 1레벨 (카테고리) — 펼침/접힘 가능한 메뉴 그룹. ▾/▸ 토글
- 2레벨 (프로그램) — 실제 프로그램 링크. 클릭 시 탭으로 열림
- 접힘 모드 — 아이콘만 표시 (56px), 마우스 오버 시 메뉴명 팝업
- 즐겨찾기 ★ — 자주 쓰는 메뉴를 상단에 고정
그리드(DataGrid) 상세
| No | 거래처명 ▲ | 대표자 | 전화번호 | 상태 |
|---|---|---|---|---|
| 1 | 삼성전자 | 이재용 | 02-1234-5678 | 거래중 |
| 2 | LG전자 | 구광모 | 02-9876-5432 | 거래중 |
| 3 | 현대모비스 | 정의선 | 031-123-4567 | 보류 |
| 요소 | 설명 |
|---|---|
| 필터(Toolbar) | 필드 타입이 selectbox인 컬럼은 자동으로 필터 드롭다운 생성 |
| 컬럼 헤더 | 클릭으로 정렬 토글 (▲오름/▼내림). Shift+클릭으로 복수 정렬 |
| 데이터 행 | 클릭 시 우측 패널에 상세 표시. 선택 행은 파란 배경으로 강조 |
| 뱃지 | selectbox 값은 자동으로 컬러 뱃지로 렌더링 |
| 페이저 | 하단 페이지 네비게이션. 기본 50건, pageSize로 변경 가능 |
패널(Panel / DataForm) 상세
| 요소 | 설명 |
|---|---|
| 탭 헤더 | 첫 번째 탭 = 현재 프로그램 폼, 나머지 = 자식 프로그램 (마스터-디테일) |
| 크기 조절 (1~4) | 1=최소(폼만), 2=기본(그리드+패널), 3=넓은 패널, 4=전체화면 |
| 폼 필드 | 왼쪽 라벨 + 오른쪽 값. 수정 모드에서 입력 가능한 필드가 활성화 |
| 섹션 타이틀 | 필드 제목에 콜론(:)이 포함되면 전체 너비 구분선으로 표시 |
| ⋯ 더보기 | 인쇄, 이력보기, 공유 등 확장 메뉴 |
| 저장/삭제 버튼 | 저장 시 스피너 표시 → 성공/실패 토스트 알림 |
모바일 화면 구성
모바일 모드에서는 토스(Toss) 앱 감성의 카드형 UI로 자동 전환됩니다.
전화: 02-1234-5678
전화: 02-9876-5432
| 영역 | 설명 |
|---|---|
| 상단바 | 햄버거(☰) 메뉴, 프로그램명, 설정. 사이드바 드로어로 열림 |
| 검색 | 상단 검색바. 필터 드롭다운은 아래 시트로 표시 |
| 카드 리스트 | 데스크톱 그리드 대신 카드형 레이아웃. 주요 필드만 표시 |
| 하단 탭바 | 2레벨 메뉴를 하단 탭으로 표시. 3레벨은 목록으로 전환 |
설정(Settings) 패널
탑바 우측 ⚙ 버튼을 클릭하면 통합 설정 패널이 열립니다.
설치 가이드
SpeedMIS v7 은 3개의 DBMS 배포판으로 제공됩니다. 각각 GitHub Public 레포에서 받아 install.php 한 번 호출로 설치 끝.
1) 배포판 선택
| DBMS | GitHub 레포 | 무료 데모 | PHP 확장 |
|---|---|---|---|
| MariaDB / MySQL | speedmis_v7_mariadb | v7ma.speedmis.com | pdo_mysql |
| Microsoft SQL Server | speedmis_v7_mssql | v7ms.speedmis.com | pdo_sqlsrv |
| PostgreSQL | speedmis_v7_postgresql | v7po.speedmis.com | pdo_pgsql |
2) 공통 요구사항
- PHP 8.3 이상 (
mbstring,json,openssl,fileinfo+ 위 표의 PDO 확장) - DB 서버: MariaDB 10.4+ / MySQL 8.0+ / MSSQL 2019+ / PostgreSQL 14+ 중 택1
- 웹서버: Nginx, Apache, IIS 어느 것이든 (PHP-FPM 또는 mod_php)
3) 설치 (3단계)
STEP 1. 원하는 배포판을 다운로드/클론 (예: MariaDB 판)
git clone https://github.com/speedmis/speedmis_v7_mariadb.git
# 또는 GitHub 의 "Code → Download ZIP" 으로 받아 압축 해제
STEP 2. 웹서버 root 를 해당 폴더로 지정. nginx 예시:
server {
listen 80;
server_name yourdomain.com;
root /var/www/speedmis_v7_mariadb;
index index.php install.php;
location / { try_files $uri $uri/ /index.php?$args; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
STEP 3. 브라우저에서 https://yourdomain.com/install.php 호출 → DB 호스트/포트/사용자/비밀번호 입력 → 연결 & 자동 설치 버튼 한 번.
- DB 가 없으면 자동 생성 (MariaDB 는
utf8mb4_unicode_ci, MSSQL 은 default collation, PG 는UTF8) - 스키마 + 마스킹된 샘플데이터(116 테이블) 가 자동 적재
- 접속 호스트에서
SITE_ID자동 생성 (예:yourdomain.com→yourdom) .env자동 작성 — JWT 키 자동 생성
4) 로그인
설치 직후 접속:
아이디: admin (또는 gadmin)
비밀번호: 4321 ← .env 의 MASTER_PASSWORD (만능비번)
.env 의 MASTER_PASSWORD=4321 을 비우거나 강력한 값으로 변경하고, 보안을 위해 install.php 를 삭제하세요.
5) 자주 묻는 질문
Q. 동봉 DB 의 비밀번호는 뭔가요?
A. Public 레포이므로 모든 비밀번호는 마스킹(passwd_decrypt = NULL)되어 있습니다. MASTER_PASSWORD=4321 만능비번으로 처음 로그인 후, mis_users 메뉴에서 본인 비밀번호 설정하세요.
Q. 외부 IP / 도메인 없는 환경에서도 설치 가능?
A. 가능. install.php 가 GitHub Public 레포에서 DB 번들을 file_get_contents 로 직접 받습니다. 인터넷만 되면 OK. 인터넷도 안 되는 폐쇄망이면 GitHub raw URL 의 db/*.sql.gz 을 미리 받아 db/ 안에 두면 로컬 우선 사용.
Q. composer install / npm build 가 필요한가요?
A. 일반적으로는 불필요. 모든 배포판은 vendor/ 와 public/build/ 가 이미 포함되어 있습니다 (다운로드 즉시 실행 가능). 개발자가 직접 코드를 수정할 때만 필요합니다.
Q. 다른 DBMS 로 옮길 수 있나요?
A. 가능. 같은 코드 베이스에서 .env 의 DB_DRIVER 값만 다릅니다 (mysql / sqlsrv / pgsql). 데이터 마이그레이션은 별도 작업이지만, 메뉴/필드 정의(mis_menus / mis_menu_fields) 는 호환됩니다.
빠른 시작 — 5분 만에 프로그램 만들기
1단계: 메뉴 등록
웹소스관리(프로그램 314)에서 새 메뉴를 등록합니다.
| 항목 | 값 | 설명 |
|---|---|---|
| 메뉴명 | 고객관리 | 좌측 메뉴에 표시 |
| 기준테이블 | my_customers | DB 테이블명 |
| 메뉴타입 | 01 | 일반 프로그램 |
2단계: 필드 정의
웹소스관리 디테일(프로그램 267)에서 필드를 추가합니다.
| 번호 | 항목명 | 필드명 | 표시폭 |
|---|---|---|---|
| 1 | idx | idx | 0 (숨김) |
| 2 | 고객명 | name | 20 |
| 3 | 전화번호 | phone | 15 |
| 4 | 이메일 | 25 | |
| 5 | 메모 | remark | 30 |
3단계: 즉시 사용
메뉴를 클릭하면 목록, 등록, 수정, 삭제 화면이 자동으로 생성됩니다. 별도 배포가 필요 없습니다.
서버 훅 시스템
훅은 programs/{real_pid}.php 파일에 정의합니다. DataHandler가 각 이벤트 시점에 자동으로 호출합니다.
훅 실행 순서
common_훅() → user_훅() → 개별_훅()
_common.php → SpeedMIS 기본 공통
_common_udef.php → 고객사 전용 공통
{real_pid}.php → 프로그램별 개별
전체 훅 목록
| 훅 | 시점 | 파라미터 |
|---|---|---|
pageLoad() | 프로그램 로드 1회 | 없음 |
before_query($menu, $fields, $params) | 쿼리 빌드 전 | 메뉴, 필드, 파라미터 |
list_query(&$select, &$count) | 목록 쿼리 생성 후 | SELECT/COUNT SQL (수정 가능) |
list_json_init() | 목록 로딩 전 | 없음 |
list_json_load(&$data) | 목록 각 행 | 행 데이터 (수정 가능) |
view_query(&$viewSql) | 단건 조회 쿼리 후 | SELECT SQL (수정 가능) |
view_load(&$row) | 단건 조회 후 | 레코드 (수정 가능) |
save_updateReady(&$saveList) | 저장 전 검증 | POST 원본 데이터 |
save_updateBefore(&$updateList) | UPDATE 직전 | DB 컬럼 데이터 |
save_updateQueryBefore(&$sql, &$bindings) | UPDATE SQL 직전 | SQL + 바인딩 |
save_updateAfter($idx, &$afterScript) | UPDATE 완료 후 | 레코드 idx |
save_writeBefore(&$updateList) | INSERT 직전 | DB 컬럼 데이터 |
save_writeQueryBefore(&$sql, &$bindings) | INSERT SQL 직전 | SQL + 바인딩 |
save_writeAfter($newIdx, &$afterScript) | INSERT 완료 후 | 새 레코드 idx |
save_deleteBefore($idx, &$cancelDelete) | 삭제 전 검증 | idx, 취소 플래그 |
save_deleteAfter($idx, &$afterScript) | 삭제 완료 후 | idx |
addLogic_treat(&$result) | 커스텀 API | 결과 배열 |
예제: 저장 전 확인
function save_updateReady(&$saveList) {
global $isListEdit, $idx;
if ((int)$saveList['amount'] > 1000000) {
$GLOBALS['_client_confirm'] = '100만원 이상입니다. 저장할까요?';
}
}
예제: 저장 후 연관 데이터 처리
function save_updateAfter($idx, &$afterScript) {
global $__pdo, $misSessionUserId;
execSql("INSERT INTO change_log (ref_idx, user_id, wdate)
VALUES ({$idx}, '{$misSessionUserId}', NOW())");
$GLOBALS['_client_toast'] = '저장 완료 + 로그 기록됨';
}
전역변수 레퍼런스
훅 함수 내에서 global 선언 후 사용할 수 있는 변수입니다.
세션/요청 정보
| 변수 | 타입 | 설명 |
|---|---|---|
$actionFlag | string | 현재 액션 (list/view/modify/write/delete) |
$gubun | int | 메뉴 idx |
$idx | int | 레코드 idx |
$real_pid | string | 프로그램 ID (speedmis000036 형태) |
$menu_name | string | 프로그램명 |
$parent_idx | int | 마스터-디테일 상위 idx |
$misSessionUserId | string | 로그인 사용자 ID |
$misSessionIsAdmin | string | 관리자 여부 ('Y' 또는 '') |
$misSessionPositionCode | string | 직급 코드 |
$isFirstLoad | bool | 프로그램 최초 로딩 여부 |
$isListEdit | bool | 목록편집(인라인) 저장 여부 |
$listEditField | array | 목록편집 시 변경된 필드명 배열 |
$customAction | string | 사용자 정의 버튼 action 값 |
$__pdo | PDO | DB 인스턴스 |
클라이언트 제어 ($GLOBALS[...])
| 키 | 값 | 효과 |
|---|---|---|
_client_alert | string | alert() 팝업 |
_client_toast | string | 토스트 알림 |
_client_confirm | string | 저장 전 확인 (Yes→저장, No→취소) |
_client_openTab | array | 새 탭 열기 {gubun, label, idx, openFull} |
_client_redirect | array | 현재 탭 교체 {gubun, label} |
_client_css | string | CSS 주입 |
_client_buttonText | array | 버튼 텍스트 변경 {write, reset} |
_client_buttons | array | 리스트 헤더 커스텀 버튼 [{label, action}] |
_client_formButtons | array | 우측 폼 상단 버튼 [{label, realPid|gubun, idx, openFull}] — view_load에서 설정 |
_client_fields | array | 필드 속성 오버라이드 |
_client_viewPref | string | 조회설정 (list/auto) |
_onlyList | bool | 리스트전용 모드 |
클라이언트 제어
버튼 텍스트 변경
function pageLoad() {
$GLOBALS['_client_buttonText'] = [
'write' => '접수하기', // +등록 → 접수하기
'reset' => '전체보기', // 초기화 → 전체보기
];
}
사용자 정의 버튼
function pageLoad() {
$GLOBALS['_client_buttons'] = [
['label' => '일괄적용', 'action' => 'apply'],
['label' => '마감처리', 'action' => 'close'],
];
}
function list_json_init() {
global $customAction;
if ($customAction === 'apply') {
execSql("UPDATE my_table SET status='적용' WHERE status='대기'");
$GLOBALS['_client_toast'] = '일괄 적용 완료!';
}
}
CSS 주입
function pageLoad() {
$GLOBALS['_client_css'] = '
#mis-btn-write { display: none; }
#mis-btn-reset { background: #3182F6; color: #fff; }
';
}
// 주요 CSS ID:
// #mis-program, #mis-header, #mis-title
// #mis-btn-write, #mis-btn-reset, #mis-btn-custom-0
그리드 셀 버튼 (openTabBtn)
function list_json_load(&$data) {
$data['__html']['gname'] = $data['gname']
. openTabBtn('상세', [
'real_pid' => 'speedmis000314',
'idx' => $data['idx'],
]);
}
// 버튼 스타일: btn-open(파랑), btn-danger(빨강), btn-success(초록), btn-sm(작게)
우측 폼 상단 버튼 (_client_formButtons)
view_load 훅에서 설정. 리스트의 data-opentab과 동일한 전역 위임을 사용해 클릭 시 탭이 열립니다.
function view_load(&$row) {
$rp = $row['real_pid'] ?? '';
if ($rp) {
$GLOBALS['_client_formButtons'] = [
['label' => '연결', 'realPid' => $rp, 'openFull' => true],
['label' => '히스토리', 'gubun' => 42, 'idx' => $row['idx']],
];
}
}
필드 속성 커스텀
$GLOBALS['_client_fields']로 런타임에 필드 속성을 동적 변경합니다.
function pageLoad() {
global $misSessionUserId;
$GLOBALS['_client_fields'] = [
'remark' => [
'col_title' => '비고', // 항목명 변경
'col_width' => 30, // 표시폭
'max_length' => 200, // 입력글수
'required' => 'Y', // 필수입력
'items' => '옵션1,옵션2', // 셀렉트박스
'grid_list_edit'=> 'Y', // 목록편집
],
];
}
변경 가능한 속성
| 키 | 설명 |
|---|---|
col_title | 항목명 (그리드 헤더 + 폼 레이블) |
col_width | 그리드 표시폭 |
max_length | 입력 최대 글수 |
default_value | 등록 시 기본값 |
required | 필수입력 (Y/N) |
schema_type | 데이터타입 |
items | 셀렉트박스 목록 |
form_group | 폼 탭그룹명 |
grid_list_edit | 목록편집 (Y/N) |
grid_ctl_name | 객체명 (check/dropdownlist 등) |
grid_is_handle | 필터 (s=셀렉트, t=텍스트) |
목록 HTML 렌더링 (__html)
그리드 셀에 HTML을 렌더링합니다. 원본 데이터는 보존됩니다.
function list_json_load(&$data) {
// 링크
$data['__html']['site'] = '<a href="'.$data['url'].'" target="_blank">'.$data['site'].'</a>';
// 상태 뱃지
$st = $data['status'];
$color = $st === '완료' ? '#22c55e' : '#ef4444';
$data['__html']['status'] = '<span class="badge" style="background:'.$color.';color:#fff">'.$st.'</span>';
// 조건부 버튼
$data['__html']['action'] = openTabBtn('처리', [
'real_pid' => 'speedmis000100',
'idx' => $data['idx'],
'class' => 'btn-danger btn-sm',
]);
}
인쇄양식 템플릿
설정
mis_menus.is_use_print = 1체크programs/{real_pid}_print.html파일 작성
문법
| 문법 | 설명 |
|---|---|
{{alias_name}} | 메인 레코드 필드값 |
{{#each childAlias}}...{{/each}} | 자식 프로그램 루프 |
{{@index}} | 루프 인덱스 (1부터) |
{{@total}} | 자식 전체 건수 |
예제
<h2>{{gname}} 그룹 상세</h2>
<table>
<tr><th>그룹명</th><td>{{gname}}</td></tr>
<tr><th>메모</th><td>{{remark}}</td></tr>
</table>
<h3>멤버 목록</h3>
<table>
{{#each zmembeohwagin}}
<tr>
<td>{{@index}}</td>
<td>{{user_id}}</td>
</tr>
{{/each}}
</table>
SQL 실행 헬퍼
// 단일 쿼리 (바인딩)
$result = execSql("INSERT INTO t (name) VALUES (?)", ['홍길동']);
// 멀티 쿼리 (세미콜론 구분)
$result = execSql("
UPDATE a SET status='done' WHERE idx=1;
INSERT INTO b (ref) VALUES (1)
");
// 결과
// $result['resultCode'] — 'success' 또는 'fail'
// $result['resultMessage'] — 에러 메시지
// $result['lastInsertId'] — 마지막 INSERT idx
// $result['rowCount'] — 영향받은 행 수
공통 / 사용자 로직
파일 구조
| 파일 | 접두어 | 업데이트 시 | 용도 |
|---|---|---|---|
_common.php | common_ | 덮어씀 | SpeedMIS 기본 |
_common_udef.php | user_ | 보존 | 고객사 전용 |
{real_pid}.php | (없음) | 보존 | 프로그램별 |
실행 순서
common_save_updateAfter() → user_save_updateAfter() → save_updateAfter()
일반 헬퍼 함수
// _common_udef.php
function getUserName($userId) {
global $__pdo;
return $__pdo->query("SELECT user_name FROM mis_users
WHERE user_id='{$userId}'")->fetchColumn() ?: '';
}
// 개별 프로그램에서 바로 사용
function save_updateAfter($idx, &$afterScript) {
$name = getUserName($GLOBALS['misSessionUserId']);
$GLOBALS['_client_toast'] = "{$name}님이 수정";
}
API 레퍼런스
모든 API는 /api.php?act= 방식입니다.
| act | Method | 설명 |
|---|---|---|
list | GET | 목록 조회 |
view | GET | 단건 조회 |
save | POST | 등록/수정 (CSRF 필수) |
delete | POST | 삭제 (CSRF 필수) |
menu | GET | 메뉴 트리 |
menuItem | GET | 메뉴 단건 |
login | POST | 로그인 |
logout | POST | 로그아웃 |
me | GET | 현재 사용자 |
treat | POST | 커스텀 API 훅 |
briefInsert | POST | 간편추가 |
fileUpload | POST | 파일 업로드 |
fileDown | GET | 파일 다운로드 |
필드 관리
프로그램 267번(웹소스관리 디테일)에서 필드를 관리합니다.
col_width 규칙
| 값 | 의미 |
|---|---|
| 양수 (예: 20) | 그리드에 표시 (폭 = 값 × 8px) |
| 0 | 그리드 숨김, 폼에만 표시 |
| -1 | 숨김 PK (첫 번째 visible 필드가 링크) |
| -2 | 완전 숨김 |
grid_ctl_name 객체명
| 값 | 설명 |
|---|---|
| text | 텍스트 입력 |
| check | 체크박스 |
| dropdownlist | 드롭다운 (prime_key 연동) |
| attach | 파일 첨부 |
| child | 자식 프로그램 탭 |
| datepicker | 날짜 선택 |
max_length (입력글수) 규칙
| 값 형식 | 일반 필드 동작 | 첨부/이미지 (attach/image) |
|---|---|---|
양수 (예: 50) | 등록·수정 모두 입력 가능 (최대 N자) | 업로드 크기 제한 N MB |
음수 (예: -14) | 등록 시만 abs(N)자 입력 가능 / 수정 시 읽기전용 | 해당 없음 |
0 | 등록·수정 모두 읽기전용 | 해당 없음 |
| 빈값 | 기본 200자, 등록·수정 모두 가능 | 기본 제한 없음 |
끝이 ! (예: 500!) | 행 단위 readonly(rowReadOnly) 무시 — 그 필드만 편집 허용 | multi-attach (여러 파일 첨부 가능) |
예시: 트리 데이터의 부모 노드도 비고만 수정 허용
parts_cate_a 트리에서 자식 있는 부모는 read_only_cond 로 readonly 처리되어 일반 수정/삭제 차단. 그러나 비고(remark) 필드의 max_length 를 500! 로 두면:
- 조회 화면에 [부분수정] 버튼 +
🔒 일부 필드만 수정 가능안내 - 수정 모드 진입 시 비고만 편집, 다른 필드는 readonly
- 저장 → 비고만 UPDATE, 그 외 변경은 BE 가 무시
권한 관리
권한 코드 (auth_code)
| 코드 | 의미 |
|---|---|
| 01 또는 빈값 | 전체 공개 |
| 02 | 멤버만 접근 (mis_menu_auth 기반) |
그룹 권한
mis_groups + mis_group_members 테이블로 관리. 프로그램별 new_gidx 필드로 그룹 지정.
프로그램 모드
| g01 | 모드 | 동작 |
|---|---|---|
| (빈값) | 일반 | 목록 + 등록/수정/삭제 |
| simple_list | 심플 목록 | 목록 + 수정만 (등록/삭제 없음) |
| only_one_list | 단건 | 리스트 없이 최근 1건만 수정 |
| gantt | 간트차트 | 그리드 대신 간트차트 전체화면 |
서버로직으로 모드 제어
function pageLoad() {
$GLOBALS['_onlyList'] = true; // 리스트전용
$GLOBALS['_client_viewPref'] = 'list'; // 내용 자동열림 비활성
}
설정
topbar 우측 ⚙ 아이콘에서 접근합니다.
| 설정 | 저장 위치 | 설명 |
|---|---|---|
| PC / 모바일 | localStorage | 레이아웃 전환 |
| 라이트 / 다크 | localStorage + DB | 테마 |
| 실사용 / 개발자 | localStorage | SQL 디버그 도구 |
| 조회설정 | localStorage | 목록만 / 자동열림 / 개별 |
그리드 사용법
기본 조작
- 행 클릭 → 우측에 상세 내용 표시
- Shift+클릭 → 새 탭으로 열기
- Ctrl+클릭 → 새 창으로 열기
- 조회모드 👁 → 클릭 시 조회
- 수정모드 ✏ → 클릭 시 수정폼 열기
패널 크기
우측 4 3 2 1 버튼으로 조절:
- 4 — 전체화면 (그리드 숨김)
- 3 — 75%
- 2 — 50%
- 1 — 25%
폼 사용법
- 조회 모드 → 데이터 확인만 가능
- 수정 버튼 → 수정 모드 전환
- 저장 버튼 → 데이터 저장
- 삭제 버튼 → 확인 후 삭제
- 그룹 탭 → 필드가 많으면 탭으로 분리
- 인쇄 🖨 → 인쇄양식 있으면 표시
필터 & 검색
- 툴바 필터 — 상단의 셀렉트박스/텍스트 입력으로 필터링
- 컬럼 헤더 필터 — 그리드 헤더 아래 행에서 직접 입력
- 최근순 — 최근 등록순 정렬 토글
- 초기화 — 모든 필터 + 정렬 초기화 (마스터-디테일의 자식 프로그램에도 지원)
- 컬럼 정렬 — 헤더 클릭 (Ctrl+클릭: 다중 정렬)
- 간편추가 — 1/2/3/5/10/50줄 옵션 (
mis_menus.brief_insert_sql설정 필요)
부분합 / 총합 (aggregate)
URL 파라미터 &aggregate= 로 목록에 부분합·총합 행을 자동 삽입합니다.
모드 4종
| 모드 | 동작 |
|---|---|
aggregate=auto | 실데이터 + 부분합/총합 (숫자 필드=합계, 그 외=건수) |
aggregate=sum | 실데이터 + 부분합/총합 (숫자 필드만 합계, 나머지는 공란) |
aggregate=simple.auto | 실데이터 숨김, 집계 행만 표시. 행 클릭 → 실데이터 팝업 |
aggregate=simple.sum | 실데이터 숨김, 숫자 합계만. 행 클릭 → 실데이터 팝업 |
규칙
orderby의 alias 조합을 그룹 키로 사용 → 그룹이 바뀔 때 소계 행 삽입recently=Y(최근순)이면 aggregate 무시- 합계:
schema_type이number로 시작하는 필드. 굵은 글씨로 표시 - 건수: 그 외 필드. 필드별로 값이
null/빈 문자열인 행은 제외하고 카운트. 이탤릭체로 표시 - 우측 패널 자동 열기 비활성
psize/pageSize미지정 시 기본 1000- 엑셀 다운로드에도 집계 행 포함 (No 칸=
소계/합계). 숫자 필드는 Excel의 숫자 셀(bold), 건수 셀은 이탤릭으로 출력
예시
/?gubun=36&orderby=dept,position&aggregate=auto
/?gubun=36&orderby=dept&aggregate=simple.sum
dept, position으로 정렬되어 있으면 두 값 조합이 바뀔 때마다 소계 행이 삽입되고, 마지막에 총합 행이 추가됩니다.
목록편집
grid_list_edit=Y인 필드는 그리드에서 직접 편집 가능합니다.
조작법
| 동작 | 결과 |
|---|---|
| 더블클릭 또는 선택 후 클릭 | 편집 모드 진입 |
| Enter | 저장 → 아래 셀 이동 |
| Shift+Enter | 저장 → 위 셀 이동 |
| Escape | 취소 |
| 체크박스 클릭 | 확인 후 즉시 토글 |
모바일
설정 → 뷰 모드 → 📱 모바일로 전환하거나, URL에 ?mode=mobile 추가.
- 햄버거 메뉴 → 1레벨 메뉴
- 하단 탭바 → 2레벨 메뉴
- 카드형 리스트 → 터치 최적화
- 카드 클릭 → 풀스크린 폼
- 자동 감지 — 모바일 기기는 자동 모바일 모드
단축키
| 키 | 동작 |
|---|---|
Ctrl+C | 선택 영역 복사 (탭 구분) |
Ctrl+A | 전체 선택 |
Enter | 목록편집: 저장 + 아래로 |
Shift+Enter | 목록편집: 저장 + 위로 |
Escape | 목록편집: 취소 |
자주 묻는 질문 (FAQ)
운영 중 자주 마주치는 상황과 해결책 모음.
Q. 행을 삭제하려는데 "읽기전용 행"이라고 막혀요
해당 메뉴의 read_only_cond (g04) 가 1 을 반환한 것입니다. 트리 구조에서 부모 노드는 자식이 모두 삭제된 후에만 삭제 가능 — 잎(leaf)부터 차례로 지우세요.
일부 필드만은 그래도 수정하고 싶다면 그 필드의 max_length 끝에 ! 를 붙이면 됩니다. (예: 500!) 해당 필드만 편집 가능한 [부분수정] 버튼이 활성화됩니다.
Q. 6068 통합인쇄대기열에 추가했는데 안 보여요
인쇄 대기열 추가 = `print_request_time = NOW()` UPDATE. 뷰가 다른 DB 의 테이블을 참조하면 동기화 안 됨. v7 마이그레이션 후엔 v7 테이블을 가리키도록 뷰 재생성 필요.
관리자(gadmin)면 ⚙ 설정 → 🗑 캐시 비우기 후 재시도.
Q. RealCid Unknown column 에러
v6 PascalCase 컬럼명이 mis_menu_fields.items SQL 등 저장된 SQL 조각에 남아있는 경우. 코어가 자동으로 v7 snake_case 로 치환하지만, 일부 자유서식(items 의 select)은 못 잡을 수 있습니다. → 해당 SQL 을 직접 v7 컬럼명으로 수정하거나, 코어가 처리할 수 있는 표준 식으로 바꾸세요.
Q. 기본 폼에서 작성자명만 보이고 사번이 안 보여요
v7 의 Qn 패턴(table_XXXQnYYY) 은 편집모드에서 selectbox 하나로 통합 표시하므로 value 필드를 숨깁니다. 조회(view) 모드에서는 함께 표시되도록 자동 처리됩니다 (이름 + 사번 동시 노출).
Q. virtual_field 행이 리스트에 안 나타나요
virtual_field 도 일반 필드와 동일한 col_width 룰을 따릅니다 — col_width=0 은 리스트 숨김 / 폼만 표시. 리스트에 보이려면 col_width 를 양수로 설정하세요. 그러면 grid_ctl_name 에 따라 컨트롤이 렌더되고, 사용자로직 (list_json_load / view_load) 에서 값을 채워 넣고, 저장 시엔 BE 의 filterData 가 db_table≠'table_m' 항목을 자동 skip 합니다.
Q. 저장 직후 "real_pid='631'" 같은 이상한 쿼리
숨김 PK (col_width=-1) 메뉴에서 발생하던 이슈. 코어가 저장 응답에 visible-key 값(예: 'speedmis000631')을 돌려주도록 수정되어 해결. 캐시 비우기 후 재시도하세요.
Q. 캐시 비우기 버튼이 어디 있나요?
topbar ⚙ 설정 → 🗑 캐시 비우기. gadmin 또는 개발자 그룹(group_idx=83) 멤버만 노출.
Q. 운영 모드 / 개발자 모드 전환은?
설정 패널의 운영 모드 토글. 개발자 모드 켜면 SQL 디버그 출력, 라벨 tooltip 에 컬럼/alias 정보 표시됨. gadmin 또는 개발자 그룹 만 토글 가능. 비멤버도 콘솔에서 misDevOn() / misDevOff() 로 임시 사용 가능.
필드 설계 팁
1. 그룹화 — col_title 의 콤마 형식
같은 그룹 필드의 col_title 을 "그룹명,컬럼명" 형식으로 두면 자동으로 묶임:
분류,제조사모델분류
,제조사모델분류코드 ← 콤마 + 빈 그룹명 = 직전 그룹 계속
,변경할 제조사모델분류
,부품분류명
부품분류코드 ← 콤마 없음 = 그룹 종료
- 리스트(그리드): 2단 헤더 — 그룹 띠가 위에 colspan 으로 표시
- 폼(뷰): 곡선 테두리(fieldset+legend) 박스로 묶임. 그룹 시작/끝 자동 줄바꿈
2. : 콜론으로 섹션 제목
col_title 에 : 가 포함되면 폼에서 데이터 영역 없는 섹션 헤더로 렌더 (전체 폭 가로띠).
3. max_length 활용
| 값 | 의미 |
|---|---|
| 양수 | 일반 (입력 한도) |
| 음수 | 등록만 가능, 수정 불가 (예: 자동 발번 필드) |
| 0 | 등록·수정 모두 readonly (조회 전용) |
| 끝이 ! | row readonly 무시 + 부분수정 허용 (첨부는 multi 의미) |
4. virtual_field 사용
DB 컬럼 없이 폼/리스트에 가상 필드 노출:
db_table = 'virtual_field'
db_field = 'price' -- 가상 컬럼명 (alias_name 의 Qn 뒷부분)
grid_ctl_name = 'text' -- 컨트롤
max_length = '13' -- 입력 한도
col_title = '실판매가'
BE 는 '' AS alias 로 빈 값 반환. 사용자로직에서 list_json_load / view_load 훅으로 값 채워 넣고, save_writeBefore 등에서 $_POST 로 입력값 받아 별도 처리. 저장 시엔 코어가 자동 무시.
5. read_only_cond (g04) — 행 단위 잠금
mis_menus.read_only_cond 에 SQL 식을 넣으면 1=잠금 / 0=정상. 행 단위 readonly 처리되어 수정·삭제 차단.
-- 트리 잎 노드만 삭제 허용 (parts_cate_a)
EXISTS(SELECT 1 FROM parts_cate_a c WHERE c.upidx = table_m.idx AND c.useflag = '1')
-- gadmin 외엔 종료된 데이터 잠금
case when datediff(IFNULL(table_m.종료일,'2100-01-01'), NOW()) < 0
and '$misSessionUserId' <> 'gadmin' then 1 else 0 end
6. ip / hit / wdate 자동 컬럼
테이블에 다음 컬럼이 있으면 코어가 자동 처리 (사용자로직 불필요):
| 컬럼 | 동작 |
|---|---|
wdater / wdate | INSERT 시 작성자 / 작성일시 |
lastupdater / lastupdate | UPDATE 시 수정자 / 수정일시 |
ip 또는 ip_address | INSERT 시 접속자 IP (프록시 헤더 우선) |
hit | 조회 시 +1, 같은 브라우저 세션에서 같은 레코드는 1회만 카운트 |
7. col_width 음수 활용
| 값 | 리스트 | 폼 |
|---|---|---|
| 양수 (10) | 표시 (10*8px) | 표시 |
| 0 | 숨김 | 표시 |
| -1 | 숨김 (PK 일 경우 visible 필드가 링크) | 숨김 |
| -2 | 완전 숨김 | 숨김 |
성능 / 캐시 팁
캐시 계층
| 레벨 | 저장 | 키 | 무효화 시점 |
|---|---|---|---|
| 스키마 캐시 | APCu / 파일 | fields, menus | mis_menus / mis_menu_fields 변경 시 자동 |
| 데이터 캐시 | APCu / 파일 | {real_pid}_{user}_{md5(filter+orderby+page)} | 해당 real_pid 저장/삭제 시 자동 |
| 클라이언트 | TanStack Query | queryKey | invalidate 호출 시 |
캐시 비우기
topbar ⚙ 설정 → 🗑 캐시 비우기 (gadmin / 개발자 그룹 멤버).
또는 그리드 패널 더보기 (...) 메뉴 → 캐시 비우기 (gadmin 만).
같은 테이블을 쓰는 형제 메뉴 자동 무효화
예: 6062 (parts_cate_a) 에서 저장 → 6038 (같은 테이블 쓰는 다른 메뉴)도 자동 invalidate.
뷰(view) 를 통해 간접 참조하는 경우도 INFORMATION_SCHEMA.VIEW_TABLE_USAGE 로 추적해 함께 비웁니다.
SSR initialData
첫 페이지 로드 시 PHP 가 첫 데이터를 window.__INITIAL_DATA__ 에 주입 → 별도 API 호출 없이 즉시 표시.
대용량 리스트 — psize 999999
URL 파라미터 psize=999999 또는 pageSize=999999 로 페이징을 숨길 수 있지만 데이터가 많으면 브라우저 메모리 부담. 5만건 이하 권장.
aggregate 모드 활용
대용량 조회를 즉시 집계행만 보고 싶을 때 aggregate=simple.auto / simple.sum 사용. 실데이터는 집계 행 클릭 시 팝업.
v6 → v7 마이그레이션 컨벤션
이름 규칙
- 테이블:
PascalCase→mis_접두어 +snake_case(예: MisMenuList → mis_menus) - 컬럼: 동일 (RealPid → real_pid)
- 예외: g5_*, mis_parts_*, v(v)_mis_parts_* 외부 테이블/뷰의 컬럼은 변경 금지 (useflag/HIT 만 rename 가능)
이름은 그대로, snake_case 변환만
v6 의 의미있는 컬럼명은 그대로 유지. PascalCase → snake_case 변환만 적용. 이름 자체를 교체하지 마세요 (예: lastupdate → updated_at 같은 변경 금지).
v7 코어가 자동 변환하는 영역
- 저장된 SQL 조각의 컬럼명 (read_only_cond, base_filter, items, group_compute, prime_key)
- 식별자 alias 변환 (
table_RealPid↔table_real_pidcollapsed 매칭) - FROM/JOIN 절의 v6 테이블명 → v7 테이블명
훅 함수 이름
| v6 | v7 |
|---|---|
| list_query() | list_query(&$selectQuery, &$countQuery) |
| save_updateAfter() | save_updateAfter($idx, &$afterScript) |
| save_writeAfter() | save_writeAfter($newIdx, &$afterScript) |
| list_json_load() — global $data | list_json_load(&$data) — 파라미터 참조 |
전역 변수 케이스
v7 은 $misSessionUserId 같은 camelCase 표준이지만, v6 호환 위해 $MisSession_UserID 도 같은 값으로 자동 alias.
alias_name_old 매핑
v6 로직 이식 시 변수명이 다른 경우 mis_menu_fields.alias_name_old 컬럼으로 v7 alias_name 을 찾아 치환할 수 있습니다.
프로젝트 구조
clude_speedmis_v7/
├── index.php # 프론트 컨트롤러
├── api.php # JSON API (?act= 방식)
├── .env # 환경변수
├── config/
│ └── constants.php # 상수 + execSql + openTabBtn
├── core/src/
│ ├── DataHandler.php # CRUD 핵심 + 훅 시스템
│ ├── QueryBuilder.php # allFilter → SQL
│ ├── MenuRouter.php # 메뉴 라우터
│ ├── AuthMiddleware.php # JWT 인증
│ ├── PrintRenderer.php # 인쇄양식 렌더러
│ └── MisCache.php # APCu/파일 캐시
├── programs/
│ ├── _common.php # SpeedMIS 공통
│ ├── _common_udef.php # 고객사 공통
│ ├── speedmis000036.php # 개별 프로그램
│ └── speedmis000036_print.html # 인쇄양식
├── src/ # React 소스
│ ├── components/
│ │ ├── Layout.jsx
│ │ ├── MainContent.jsx
│ │ ├── DataGrid.jsx
│ │ ├── DataForm.jsx
│ │ ├── GanttChart.jsx
│ │ └── mobile/
│ └── api.js
└── public/build/ # 빌드 결과