Files
sam-api/public/tenant/settings/member_fields.php

321 lines
16 KiB
PHP

<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
<?php
// 섹션은 임시로 'member' 사용 (상단 2뎁스에 노출만 필요)
$CURRENT_SECTION = 'member';
include '../inc/header.php';
// ====== 샘플 데이터 (실구현은 DB 조인) ======
// setting_field_defs
$FIELD_DEFS = [
['field_key'=>'name','label'=>'이름','data_type'=>'string','input_type'=>'text','option_source'=>null],
['field_key'=>'email','label'=>'이메일','data_type'=>'string','input_type'=>'text','option_source'=>null],
['field_key'=>'phone','label'=>'전화번호','data_type'=>'string','input_type'=>'text','option_source'=>null],
['field_key'=>'department','label'=>'부서','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'position','label'=>'직급','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'job_title','label'=>'직책','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'manager_id','label'=>'직속 상급자','data_type'=>'bigint','input_type'=>'select_user','option_source'=>null],
['field_key'=>'hire_date','label'=>'입사일','data_type'=>'date','input_type'=>'date','option_source'=>null],
['field_key'=>'employment_type','label'=>'고용형태','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'work_location','label'=>'근무지','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'work_type','label'=>'근무형태','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'gender','label'=>'성별','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'bank_name','label'=>'은행','data_type'=>'string','input_type'=>'select','option_source'=>'tenant_list'],
['field_key'=>'bank_account','label'=>'계좌번호','data_type'=>'string','input_type'=>'text','option_source'=>null],
['field_key'=>'employee_no','label'=>'사번','data_type'=>'string','input_type'=>'text','option_source'=>null],
['field_key'=>'profile_photo','label'=>'프로필 사진','data_type'=>'file','input_type'=>'file','option_source'=>null],
];
// tenant_option_groups (id, name) — select 항목에서 선택할 그룹
$OPTION_GROUPS = [
['id'=>101,'group_key'=>'department','name'=>'부서 그룹'],
['id'=>102,'group_key'=>'position','name'=>'직급 그룹'],
['id'=>103,'group_key'=>'job_title','name'=>'직책 그룹'],
['id'=>104,'group_key'=>'employment_type','name'=>'고용형태 그룹'],
['id'=>105,'group_key'=>'work_location','name'=>'근무지 그룹'],
['id'=>106,'group_key'=>'work_type','name'=>'근무형태 그룹'],
['id'=>107,'group_key'=>'gender','name'=>'성별 그룹'],
['id'=>108,'group_key'=>'bank_name','name'=>'은행 그룹'],
];
// tenant_field_settings (샘플: enabled/required/sort/order/group)
// key = field_key
$SETTINGS = [
'name' => ['enabled'=>1,'required'=>1,'sort_order'=>10,'option_group_id'=>null],
'email' => ['enabled'=>1,'required'=>1,'sort_order'=>20,'option_group_id'=>null],
'phone' => ['enabled'=>1,'required'=>0,'sort_order'=>30,'option_group_id'=>null],
'department' => ['enabled'=>1,'required'=>1,'sort_order'=>40,'option_group_id'=>101],
'position' => ['enabled'=>1,'required'=>0,'sort_order'=>50,'option_group_id'=>102],
'job_title' => ['enabled'=>0,'required'=>0,'sort_order'=>60,'option_group_id'=>103],
'manager_id' => ['enabled'=>0,'required'=>0,'sort_order'=>70,'option_group_id'=>null],
'hire_date' => ['enabled'=>1,'required'=>0,'sort_order'=>80,'option_group_id'=>null],
'employment_type' => ['enabled'=>1,'required'=>0,'sort_order'=>90,'option_group_id'=>104],
'work_location' => ['enabled'=>1,'required'=>0,'sort_order'=>100,'option_group_id'=>105],
'work_type' => ['enabled'=>0,'required'=>0,'sort_order'=>110,'option_group_id'=>106],
'gender' => ['enabled'=>0,'required'=>0,'sort_order'=>120,'option_group_id'=>107],
'bank_name' => ['enabled'=>0,'required'=>0,'sort_order'=>130,'option_group_id'=>108],
'bank_account' => ['enabled'=>0,'required'=>0,'sort_order'=>140,'option_group_id'=>null],
'employee_no' => ['enabled'=>1,'required'=>0,'sort_order'=>150,'option_group_id'=>null],
'profile_photo' => ['enabled'=>0,'required'=>0,'sort_order'=>160,'option_group_id'=>null],
];
// helper: 옵션그룹 셀렉트 HTML
function render_group_select($field_key,$sel,$groups,$def){
if (!($def['input_type']==='select' && $def['option_source']==='tenant_list')) return '<span class="text-muted">—</span>';
$html = '<select class="form-select form-select-sm opt-group" data-field="'.$field_key.'" style="min-width:160px;">';
$html .= '<option value="">(그룹 선택)</option>';
foreach($groups as $g){
$s = ($sel==$g['id'])?'selected':'';
$html .= '<option value="'.$g['id'].'" '.$s.'>'.htmlspecialchars($g['name']).'</option>';
}
$html .= '</select>';
return $html;
}
// 기존 render_group_select() 제거하고 ↓로 교체
function group_name_by_id($id, $groups)
{
foreach ($groups as $g) {
if ((string)$g['id'] === (string)$id) return $g['name'];
}
return null;
}
function render_group_label($field_key, $sel_id, $groups, $def)
{
// select + tenant_list 타입만 노출, 그 외는 대시
if (!($def['input_type'] === 'select' && $def['option_source'] === 'tenant_list')) {
return '<span class="text-muted">—</span>';
}
if (!$sel_id) return '<span class="badge bg-light text-muted">미지정</span>';
$name = htmlspecialchars(group_name_by_id($sel_id, $groups) ?: '알 수 없음');
// 필요하면 관리 링크 표시
return '<span class="badge bg-secondary-subtle text-dark">' . $name . '</span>'
. ' <a href="/tenant/settings/option_groups.php?selected=' . $sel_id . '" class="btn btn-sm btn-outline-secondary ms-1">관리</a>';
}
?>
<div class="container" style="max-width:1280px; margin-top:18px;">
<!-- 상단 툴바 -->
<div class="card mb-3">
<div class="sam-toolbar">
<div class="sam-title">회원가입 필드 설정</div>
<div class="sam-actions">
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnAllOn">모두 사용</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnAllOff">모두 미사용</button>
<button type="button" class="btn btn-primary btn-sm" id="btnSaveAll">일괄 저장</button>
</div>
</div>
<div class="p-3">
<div class="small text-muted mb-1">미리보기 · 회원가입 화면 노출 순서</div>
<div id="preview" class="d-flex flex-wrap gap-2"></div>
</div>
</div>
<!-- 설정 테이블 -->
<div class="card p-3">
<div class="table-responsive">
<table class="table table-sam align-middle" id="fieldsTable">
<thead>
<tr>
<th style="width:150px;">순서</th>
<th class="text-start">필드</th>
<th style="width:120px;">입력유형</th>
<th style="width:180px;">옵션 그룹</th>
<th style="width:100px;">필수</th>
<th style="width:100px;">사용</th>
<th style="width:140px;">관리</th>
</tr>
</thead>
<tbody>
<?php
// sort_order 순으로 정렬된 것처럼 표시
usort($FIELD_DEFS, function($a,$b) use($SETTINGS){
$sa = $SETTINGS[$a['field_key']]['sort_order'] ?? 9999;
$sb = $SETTINGS[$b['field_key']]['sort_order'] ?? 9999;
return $sa <=> $sb;
});
foreach($FIELD_DEFS as $def):
$key = $def['field_key'];
$set = $SETTINGS[$key] ?? ['enabled'=>0,'required'=>0,'sort_order'=>9999,'option_group_id'=>null];
$enabled = (int)$set['enabled'];
$required = (int)$set['required'];
$order = (int)$set['sort_order'];
$groupId = $set['option_group_id'];
?>
<tr data-field="<?= htmlspecialchars($key) ?>">
<td>
<div class="input-group input-group-sm">
<button class="btn btn-outline-secondary btn-order" data-dir="-10" type="button">▲</button>
<input type="number" class="form-control form-control-sm sort-order text-center" value="<?= $order ?>">
<button class="btn btn-outline-secondary btn-order" data-dir="10" type="button">▼</button>
</div>
</td>
<td class="text-start">
<div class="fw-semibold"><?= htmlspecialchars($def['label']) ?></div>
<div class="text-muted small"><?= htmlspecialchars($key) ?> · <?= htmlspecialchars($def['data_type']) ?></div>
</td>
<td><span class="badge rounded-pill bg-light text-dark"><?= htmlspecialchars($def['input_type']) ?></span></td>
<td><?= render_group_label($key,$groupId,$OPTION_GROUPS,$def) ?></td>
<td>
<div class="form-check form-switch d-flex justify-content-center m-0">
<input class="form-check-input toggle-req" type="checkbox" <?= $required?'checked':'' ?> <?= $enabled? '' : 'disabled' ?>>
</div>
</td>
<td>
<div class="form-check form-switch d-flex justify-content-center m-0">
<input class="form-check-input toggle-on" type="checkbox" <?= $enabled?'checked':'' ?>>
</div>
</td>
<td>
<button class="btn btn-sm btn-outline-primary btn-apply" type="button">저장</button>
<button class="btn btn-sm btn-outline-secondary btn-reset" type="button">되돌리기</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<style>
#preview .pill{
display:inline-flex; align-items:center; gap:6px;
background:#eef3ff; color:#2c4a85; border:1px solid #d9e4ff;
padding:.25rem .5rem; border-radius:999px; font-size:.85rem;
}
#preview .pill .req{ color:#c1121f; font-weight:600; }
#fieldsTable td .input-group-sm .form-control { max-width:70px; }
#fieldsTable .btn-order { width:34px; }
</style>
<script>
$(function(){
// —— 메모리 상태(실제 구현은 서버 조인 결과로 초기화)
const defs = <?= json_encode($FIELD_DEFS, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?>;
const initSettings = <?= json_encode($SETTINGS, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?>;
let state = JSON.parse(JSON.stringify(initSettings)); // deep copy
// 프리뷰 갱신
function renderPreview(){
const list = Object.keys(state)
.map(k => ({k, ...state[k], label: findDef(k)?.label || k}))
.filter(x => x.enabled)
.sort((a,b)=> a.sort_order - b.sort_order);
const html = list.map(x => `
<span class="pill" data-k="${x.k}">
${escapeHtml(x.label)}
${x.required ? '<span class="req">*</span>' : ''}
<small class="text-muted ms-1">(${x.sort_order})</small>
</span>
`).join('');
$('#preview').html(html || '<span class="text-muted">사용 중인 필드가 없습니다.</span>');
}
function findDef(key){ return defs.find(d => d.field_key===key); }
function escapeHtml(s){ return String(s??'').replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
// 테이블 → state 동기화(행 하나)
function pullRow($tr){
const key = $tr.data('field');
const enabled = $tr.find('.toggle-on').prop('checked') ? 1:0;
const required = $tr.find('.toggle-req').prop('checked') ? 1:0;
const sort_order = parseInt($tr.find('.sort-order').val(),10) || 0;
const groupSel = $tr.find('.opt-group');
const option_group_id = groupSel.length ? (groupSel.val()? parseInt(groupSel.val(),10): null) : null;
state[key] = { enabled, required, sort_order, option_group_id };
}
// state → 테이블(행 하나) 재적용
function pushRow($tr){
const key = $tr.data('field');
const s = state[key] || {};
$tr.find('.sort-order').val(s.sort_order ?? 0);
$tr.find('.toggle-on').prop('checked', !!s.enabled);
$tr.find('.toggle-req').prop('checked', !!s.required).prop('disabled', !s.enabled);
const groupSel = $tr.find('.opt-group');
if (groupSel.length){
groupSel.val(s.option_group_id ? String(s.option_group_id) : '');
}
}
// 개별 저장 (모의)
$(document).on('click', '.btn-apply', function(){
const $tr = $(this).closest('tr');
pullRow($tr);
renderPreview();
// 실제:
// $.post('/tenant/api/settings/member_field_save.php', { field_key:..., ...state[field_key] })
alert('저장(모의): '+$tr.data('field'));
});
// 되돌리기
$(document).on('click', '.btn-reset', function(){
const key = $(this).closest('tr').data('field');
state[key] = JSON.parse(JSON.stringify(initSettings[key] || {enabled:0,required:0,sort_order:9999,option_group_id:null}));
pushRow($(this).closest('tr'));
renderPreview();
});
// 사용 스위치
$(document).on('change', '.toggle-on', function(){
const $tr = $(this).closest('tr');
const on = $(this).prop('checked');
$tr.find('.toggle-req').prop('disabled', !on);
pullRow($tr); renderPreview();
});
// 필수 스위치
$(document).on('change', '.toggle-req', function(){
const $tr = $(this).closest('tr');
pullRow($tr); renderPreview();
});
// 옵션그룹 변경
$(document).on('change', '.opt-group', function(){
pullRow($(this).closest('tr')); renderPreview();
});
// 순서 숫자 직접 수정
$(document).on('input', '.sort-order', function(){
pullRow($(this).closest('tr')); renderPreview();
});
// 순서 버튼 ▲▼
$(document).on('click', '.btn-order', function(){
const $tr = $(this).closest('tr');
const dir = parseInt($(this).data('dir'),10);
const $inp = $tr.find('.sort-order');
const cur = parseInt($inp.val(),10) || 0;
$inp.val(cur + dir).trigger('input');
});
// 모두 사용/미사용
$('#btnAllOn').on('click', function(){
$('#fieldsTable tbody tr').each(function(){
$(this).find('.toggle-on').prop('checked', true).trigger('change');
});
});
$('#btnAllOff').on('click', function(){
$('#fieldsTable tbody tr').each(function(){
$(this).find('.toggle-on').prop('checked', false).trigger('change');
});
});
// 일괄 저장(모의)
$('#btnSaveAll').on('click', function(){
// 실제: $.post('/tenant/api/settings/member_fields_bulk_save.php', { payload: JSON.stringify(state) })
console.log('[BULK SAVE]', state);
alert('일괄 저장(모의). 콘솔을 확인하세요.');
});
// 초기 렌더
renderPreview();
});
</script>
<?php include '../inc/footer.php'; ?>