423 lines
20 KiB
PHP
423 lines
20 KiB
PHP
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
|
<?php
|
|
$CURRENT_SECTION = 'member'; // 상단 1뎁스 "회사" 섹션에 노출
|
|
include '../inc/header.php';
|
|
?>
|
|
<div class="container" style="max-width:1280px; margin-top:24px;">
|
|
|
|
<!-- 상단 툴바 -->
|
|
<div class="sam-toolbar shadow-sm mb-3">
|
|
<div class="sam-title">옵션 그룹 관리</div>
|
|
<div class="sam-actions">
|
|
<input type="text" class="form-control form-control-sm" id="kw" placeholder="그룹키/이름 검색">
|
|
<button class="btn btn-sm btn-outline-secondary" id="btnFind">검색</button>
|
|
<button class="btn btn-sm btn-primary" id="btnAddGroup">+ 그룹 추가</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<!-- 그룹 목록 -->
|
|
<div class="col-12 col-lg-5">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<strong>그룹 목록</strong>
|
|
<small class="text-muted" id="grpCount"></small>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sam table-hover m-0" id="grpTable">
|
|
<thead><tr>
|
|
<th style="width:90px;">ID</th>
|
|
<th>그룹키</th>
|
|
<th>이름</th>
|
|
<th style="width:120px;">관리</th>
|
|
</tr></thead>
|
|
<tbody><!-- JS 렌더 --></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 값(항목) 목록 -->
|
|
<div class="col-12 col-lg-7">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>항목 값</strong>
|
|
<span class="text-muted ms-2" id="selGroupLabel">선택된 그룹 없음</span>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-secondary" id="btnSortValueUp" title="선택 항목 위로">▲</button>
|
|
<button class="btn btn-sm btn-outline-secondary" id="btnSortValueDown" title="선택 항목 아래로">▼</button>
|
|
<button class="btn btn-sm btn-primary" id="btnAddValue">+ 항목 추가</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sam table-hover m-0" id="valTable">
|
|
<thead><tr>
|
|
<th style="width:90px;">ID</th>
|
|
<th style="width:160px;">value_key</th>
|
|
<th>라벨</th>
|
|
<th style="width:90px;">정렬</th>
|
|
<th style="width:90px;">사용</th>
|
|
<th style="width:120px;">관리</th>
|
|
</tr></thead>
|
|
<tbody><!-- JS 렌더 --></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 그룹 추가/수정 모달 -->
|
|
<div class="modal fade" id="groupModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-md modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="groupModalTitle">그룹 추가</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<form id="groupForm" autocomplete="off">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="g_id">
|
|
<div class="mb-2">
|
|
<label class="form-label">그룹키 <span class="text-danger">*</span></label>
|
|
<input type="text" name="group_key" id="g_key" class="form-control" maxlength="64" required placeholder="예: position, job_title">
|
|
<div class="form-text">영문/숫자/언더스코어 권장, 테넌트 내 유니크</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">이름 <span class="text-danger">*</span></label>
|
|
<input type="text" name="name" id="g_name" class="form-control" maxlength="100" required placeholder="화면 표기용 이름">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">설명</label>
|
|
<input type="text" name="description" id="g_desc" class="form-control" maxlength="255">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" type="button">취소</button>
|
|
<button class="btn btn-primary" type="submit">저장</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 값 추가/수정 모달 -->
|
|
<div class="modal fade" id="valueModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-md modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="valueModalTitle">항목 추가</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<form id="valueForm" autocomplete="off">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="v_id">
|
|
<input type="hidden" name="group_id" id="v_group_id">
|
|
<div class="mb-2">
|
|
<label class="form-label">value_key <span class="text-danger">*</span></label>
|
|
<input type="text" name="value_key" id="v_key" class="form-control" maxlength="64" required placeholder="예: manager, engineer">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">라벨 <span class="text-danger">*</span></label>
|
|
<input type="text" name="value_label" id="v_label" class="form-control" maxlength="100" required placeholder="예: 과장, 대리">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">정렬</label>
|
|
<input type="number" name="sort_order" id="v_sort" class="form-control" value="0">
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="v_active" checked>
|
|
<label class="form-check-label" for="v_active">사용</label>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" type="button">취소</button>
|
|
<button class="btn btn-primary" type="submit">저장</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* 선택된 행 강조 */
|
|
#grpTable tbody tr.active,
|
|
#valTable tbody tr.active { background:#eef3ff; }
|
|
</style>
|
|
|
|
<script>
|
|
$(function(){
|
|
// ===== 샘플 데이터 (실서비스에선 Ajax로 교체) =====
|
|
// 그룹
|
|
let GROUPS = [
|
|
{id:101, tenant_id:1, group_key:'position', name:'직급', description:'회사 직급'},
|
|
{id:102, tenant_id:1, group_key:'job_title', name:'직책', description:'직책/보임직'},
|
|
{id:103, tenant_id:1, group_key:'employment_type', name:'고용형태', description:'정규/계약/인턴'},
|
|
{id:104, tenant_id:1, group_key:'work_location', name:'근무지', description:'본사/공장/현장'},
|
|
{id:105, tenant_id:1, group_key:'work_type', name:'근무형태', description:'주간/야간/교대'},
|
|
{id:106, tenant_id:1, group_key:'gender', name:'성별', description:''},
|
|
{id:107, tenant_id:1, group_key:'bank_name', name:'은행', description:'급여계좌 은행'}
|
|
];
|
|
// 값
|
|
let VALUES = [
|
|
{id:1, group_id:101, value_key:'staff', value_label:'사원', sort_order:10, is_active:1},
|
|
{id:2, group_id:101, value_key:'assistant', value_label:'대리', sort_order:20, is_active:1},
|
|
{id:3, group_id:102, value_key:'team_lead', value_label:'팀장', sort_order:10, is_active:1},
|
|
{id:4, group_id:103, value_key:'regular', value_label:'정규직', sort_order:10, is_active:1},
|
|
{id:5, group_id:103, value_key:'contract', value_label:'계약직', sort_order:20, is_active:1},
|
|
{id:6, group_id:104, value_key:'hq', value_label:'본사', sort_order:10, is_active:1},
|
|
{id:7, group_id:105, value_key:'day', value_label:'주간', sort_order:10, is_active:1},
|
|
{id:8, group_id:106, value_key:'male', value_label:'남', sort_order:10, is_active:1},
|
|
{id:9, group_id:106, value_key:'female', value_label:'여', sort_order:20, is_active:1},
|
|
{id:10, group_id:107, value_key:'kb', value_label:'국민은행', sort_order:10, is_active:1},
|
|
{id:11, group_id:107, value_key:'shinhan', value_label:'신한은행', sort_order:20, is_active:1}
|
|
];
|
|
|
|
// 상태
|
|
let selectedGroupId = null;
|
|
let valSelectedId = null;
|
|
|
|
// ===== 유틸 =====
|
|
const $grpBody = $('#grpTable tbody');
|
|
const $valBody = $('#valTable tbody');
|
|
|
|
function esc(s){ return String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"', "'":''' }[m])); }
|
|
function groupById(id){ return GROUPS.find(g=>g.id==id); }
|
|
function valuesOf(gid){ return VALUES.filter(v=>v.group_id==gid).sort((a,b)=>a.sort_order-b.sort_order || a.id-b.id); }
|
|
|
|
// ===== 렌더 =====
|
|
function renderGroups(list){
|
|
const html = list.map(g=>`
|
|
<tr data-id="${g.id}" class="${g.id==selectedGroupId?'active':''}">
|
|
<td>${g.id}</td>
|
|
<td class="text-start">${esc(g.group_key)}</td>
|
|
<td class="text-start">${esc(g.name)}</td>
|
|
<td>
|
|
<div class="d-flex justify-content-center gap-1">
|
|
<button class="btn btn-sm btn-outline-secondary btn-edit-group">수정</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-del-group">삭제</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
$grpBody.html(html);
|
|
$('#grpCount').text(`총 ${list.length}건`);
|
|
}
|
|
function renderValues(){
|
|
if(!selectedGroupId){
|
|
$('#selGroupLabel').text('선택된 그룹 없음');
|
|
$valBody.html(`<tr><td colspan="6" class="text-muted">좌측에서 그룹을 선택하세요.</td></tr>`);
|
|
return;
|
|
}
|
|
const g = groupById(selectedGroupId);
|
|
$('#selGroupLabel').text(`(${g.group_key}) ${g.name}`);
|
|
const rows = valuesOf(selectedGroupId);
|
|
if(rows.length===0){
|
|
$valBody.html(`<tr><td colspan="6" class="text-muted">항목이 없습니다. [항목 추가]를 눌러 등록하세요.</td></tr>`);
|
|
return;
|
|
}
|
|
const html = rows.map(v=>`
|
|
<tr data-id="${v.id}" class="${v.id==valSelectedId?'active':''}">
|
|
<td>${v.id}</td>
|
|
<td class="text-start"><code>${esc(v.value_key)}</code></td>
|
|
<td class="text-start">${esc(v.value_label)}</td>
|
|
<td>${v.sort_order}</td>
|
|
<td>${v.is_active?'<span class="badge bg-success-subtle text-success">Y</span>':'<span class="badge bg-secondary-subtle text-muted">N</span>'}</td>
|
|
<td>
|
|
<div class="d-flex justify-content-center gap-1">
|
|
<button class="btn btn-sm btn-outline-secondary btn-edit-val">수정</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-del-val">삭제</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
$valBody.html(html);
|
|
}
|
|
|
|
// 최초 렌더
|
|
renderGroups(GROUPS);
|
|
renderValues();
|
|
|
|
// ===== 검색 =====
|
|
$('#btnFind').on('click', ()=>{
|
|
const q = $('#kw').val().trim().toLowerCase();
|
|
const list = !q ? GROUPS : GROUPS.filter(g =>
|
|
[g.group_key, g.name].some(x=> String(x).toLowerCase().includes(q))
|
|
);
|
|
renderGroups(list);
|
|
});
|
|
|
|
// ===== 그룹 선택 =====
|
|
$(document).on('click', '#grpTable tbody tr', function(e){
|
|
if($(e.target).closest('button').length) return; // 버튼 클릭은 제외
|
|
selectedGroupId = +$(this).data('id');
|
|
valSelectedId = null;
|
|
renderGroups(GROUPS);
|
|
renderValues();
|
|
});
|
|
|
|
// ===== 그룹 추가/수정/삭제 =====
|
|
$('#btnAddGroup').on('click', ()=>{
|
|
$('#groupModalTitle').text('그룹 추가');
|
|
$('#g_id').val('');
|
|
$('#g_key').val('');
|
|
$('#g_name').val('');
|
|
$('#g_desc').val('');
|
|
new bootstrap.Modal('#groupModal').show();
|
|
});
|
|
|
|
$(document).on('click', '.btn-edit-group', function(){
|
|
const id = +$(this).closest('tr').data('id');
|
|
const g = groupById(id); if(!g) return;
|
|
$('#groupModalTitle').text('그룹 수정');
|
|
$('#g_id').val(g.id);
|
|
$('#g_key').val(g.group_key);
|
|
$('#g_name').val(g.name);
|
|
$('#g_desc').val(g.description||'');
|
|
new bootstrap.Modal('#groupModal').show();
|
|
});
|
|
|
|
$('#groupForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const id = $('#g_id').val();
|
|
const payload = {
|
|
id: id? +id : null,
|
|
group_key: $('#g_key').val().trim(),
|
|
name: $('#g_name').val().trim(),
|
|
description: $('#g_desc').val().trim()
|
|
};
|
|
if(payload.group_key.length<2 || payload.name.length<1){ alert('그룹키/이름을 확인하세요.'); return; }
|
|
|
|
if(payload.id){
|
|
const g = groupById(payload.id);
|
|
if(!g) return;
|
|
g.group_key = payload.group_key;
|
|
g.name = payload.name;
|
|
g.description = payload.description;
|
|
}else{
|
|
const newId = (Math.max(0, ...GROUPS.map(x=>x.id)) + 1);
|
|
GROUPS.push({id:newId, tenant_id:1, ...payload});
|
|
}
|
|
bootstrap.Modal.getInstance(document.getElementById('groupModal')).hide();
|
|
renderGroups(GROUPS);
|
|
renderValues();
|
|
|
|
// 실제 API 예)
|
|
// $.post('/tenant/api/settings/group_save.php', payload).done(()=>location.reload());
|
|
});
|
|
|
|
$(document).on('click', '.btn-del-group', function(){
|
|
const id = +$(this).closest('tr').data('id');
|
|
const g = groupById(id); if(!g) return;
|
|
if(!confirm(`그룹(${g.name}) 및 모든 항목을 삭제할까요?`)) return;
|
|
// 값 삭제
|
|
VALUES = VALUES.filter(v=>v.group_id!=id);
|
|
// 그룹 삭제
|
|
GROUPS = GROUPS.filter(x=>x.id!=id);
|
|
if(selectedGroupId==id) { selectedGroupId=null; valSelectedId=null; }
|
|
renderGroups(GROUPS);
|
|
renderValues();
|
|
|
|
// 실제 API 예)
|
|
// location.href = '/tenant/api/settings/group_delete.php?id='+id;
|
|
});
|
|
|
|
// ===== 값(항목) 추가/수정/삭제 =====
|
|
$('#btnAddValue').on('click', ()=>{
|
|
if(!selectedGroupId){ alert('좌측에서 그룹을 선택하세요.'); return; }
|
|
$('#valueModalTitle').text('항목 추가');
|
|
$('#v_id').val('');
|
|
$('#v_group_id').val(selectedGroupId);
|
|
$('#v_key').val('');
|
|
$('#v_label').val('');
|
|
$('#v_sort').val( (valuesOf(selectedGroupId).slice(-1)[0]?.sort_order || 0) + 10 );
|
|
$('#v_active').prop('checked', true);
|
|
new bootstrap.Modal('#valueModal').show();
|
|
});
|
|
|
|
$(document).on('click', '.btn-edit-val', function(){
|
|
const id = +$(this).closest('tr').data('id');
|
|
const v = VALUES.find(x=>x.id==id); if(!v) return;
|
|
$('#valueModalTitle').text('항목 수정');
|
|
$('#v_id').val(v.id);
|
|
$('#v_group_id').val(v.group_id);
|
|
$('#v_key').val(v.value_key);
|
|
$('#v_label').val(v.value_label);
|
|
$('#v_sort').val(v.sort_order);
|
|
$('#v_active').prop('checked', !!v.is_active);
|
|
new bootstrap.Modal('#valueModal').show();
|
|
});
|
|
|
|
$('#valueForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const id = $('#v_id').val();
|
|
const payload = {
|
|
id: id? +id : null,
|
|
group_id: +$('#v_group_id').val(),
|
|
value_key: $('#v_key').val().trim(),
|
|
value_label: $('#v_label').val().trim(),
|
|
sort_order: parseInt($('#v_sort').val(),10) || 0,
|
|
is_active: $('#v_active').is(':checked') ? 1 : 0
|
|
};
|
|
if(!payload.group_id){ alert('그룹을 선택하세요.'); return; }
|
|
if(payload.value_key.length<1 || payload.value_label.length<1){ alert('value_key/라벨을 확인하세요.'); return; }
|
|
|
|
if(payload.id){
|
|
const v = VALUES.find(x=>x.id==payload.id);
|
|
if(!v) return;
|
|
Object.assign(v, payload);
|
|
}else{
|
|
const newId = (Math.max(0, ...VALUES.map(x=>x.id)) + 1);
|
|
VALUES.push({id:newId, ...payload});
|
|
}
|
|
bootstrap.Modal.getInstance(document.getElementById('valueModal')).hide();
|
|
renderValues();
|
|
|
|
// 실제 API 예)
|
|
// $.post('/tenant/api/settings/value_save.php', payload).done(()=>renderValues());
|
|
});
|
|
|
|
$(document).on('click', '.btn-del-val', function(){
|
|
const id = +$(this).closest('tr').data('id');
|
|
const v = VALUES.find(x=>x.id==id); if(!v) return;
|
|
if(!confirm(`항목(${v.value_label})을 삭제할까요?`)) return;
|
|
VALUES = VALUES.filter(x=>x.id!=id);
|
|
if(valSelectedId==id) valSelectedId=null;
|
|
renderValues();
|
|
|
|
// 실제 API 예)
|
|
// location.href = '/tenant/api/settings/value_delete.php?id='+id;
|
|
});
|
|
|
|
// 값 행 선택 (정렬 이동 대상 선택용)
|
|
$(document).on('click', '#valTable tbody tr', function(e){
|
|
if($(e.target).closest('button').length) return;
|
|
valSelectedId = +$(this).data('id');
|
|
renderValues();
|
|
});
|
|
|
|
// 정렬 이동 (클라이언트 사이드 재배치)
|
|
function moveSelected(delta){
|
|
if(!selectedGroupId || !valSelectedId) return;
|
|
const list = valuesOf(selectedGroupId);
|
|
const idx = list.findIndex(v=>v.id==valSelectedId);
|
|
if(idx<0) return;
|
|
const newIdx = idx + delta;
|
|
if(newIdx<0 || newIdx>=list.length) return;
|
|
// swap sort_order
|
|
const a = list[idx], b = list[newIdx];
|
|
const tmp = a.sort_order; a.sort_order = b.sort_order; b.sort_order = tmp;
|
|
renderValues();
|
|
|
|
// 실제 API 예) 서버에서 sort_order 리시퀀싱
|
|
// $.post('/tenant/api/settings/value_reorder.php', {group_id:selectedGroupId, id:valSelectedId, dir:(delta<0?'up':'down')})
|
|
}
|
|
$('#btnSortValueUp').on('click', ()=>moveSelected(-1));
|
|
$('#btnSortValueDown').on('click', ()=>moveSelected(1));
|
|
|
|
});
|
|
</script>
|
|
<?php include '../inc/footer.php'; ?>
|