348 lines
16 KiB
PHP
348 lines
16 KiB
PHP
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
|
<?php
|
|
$CURRENT_SECTION = 'tenant';
|
|
include '../inc/header.php';
|
|
?>
|
|
<div class="container" style="max-width:1280px; margin-top:40px;">
|
|
<div class="card shadow p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h4 class="mb-0">회원(유저) 목록</h4>
|
|
<button class="btn btn-primary" id="btnOpenAdd">+ 회원 등록</button>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle text-center" id="userTable">
|
|
<thead class="table-light">
|
|
<tr id="theadRow">
|
|
<!-- JS가 동적 컬럼(옵션) 포함하여 렌더 -->
|
|
</tr>
|
|
</thead>
|
|
<tbody id="userTbody"><!-- JS 렌더 --></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 등록 모달 -->
|
|
<div class="modal fade" id="userAddModal" 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">회원 등록</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<form id="userAddForm" autocomplete="off" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="mb-2">
|
|
<label class="form-label">회원 아이디 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" name="user_id" maxlength="100" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">이름 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" name="name" maxlength="100" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">이메일 <span class="text-danger">*</span></label>
|
|
<input type="email" class="form-control" name="email" maxlength="255" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">전화번호</label>
|
|
<input type="text" class="form-control" name="phone" maxlength="30">
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col">
|
|
<label class="form-label">비밀번호 <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" name="password" maxlength="30" required>
|
|
</div>
|
|
<div class="col">
|
|
<label class="form-label">비밀번호 확인 <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" name="password2" maxlength="30" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">프로필 사진</label>
|
|
<input type="file" class="form-control" name="profile_photo" accept="image/*">
|
|
</div>
|
|
|
|
<!-- 옵션 필드(샘플: 사번/계좌번호) -->
|
|
<div id="addOptionFields"><!-- JS 렌더 --></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">취소</button>
|
|
<button class="btn btn-primary" type="submit">등록</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 수정 모달 -->
|
|
<div class="modal fade" id="userEditModal" 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">회원 정보 수정</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<form id="userEditForm" autocomplete="off" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="id" id="edit_id">
|
|
<div class="mb-2">
|
|
<label class="form-label">회원 아이디</label>
|
|
<input type="text" class="form-control" id="edit_user_id" readonly>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">이름 <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" name="name" id="edit_name" maxlength="100" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">이메일 <span class="text-danger">*</span></label>
|
|
<input type="email" class="form-control" name="email" id="edit_email" maxlength="255" required>
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">전화번호</label>
|
|
<input type="text" class="form-control" name="phone" id="edit_phone" maxlength="30">
|
|
</div>
|
|
<div class="mb-2">
|
|
<label class="form-label">프로필 사진</label>
|
|
<div id="edit_photo_preview" class="mb-2"></div>
|
|
<input type="file" class="form-control" name="profile_photo" accept="image/*">
|
|
</div>
|
|
<div class="row g-2">
|
|
<div class="col">
|
|
<label class="form-label">비밀번호 변경</label>
|
|
<input type="password" class="form-control" name="password" maxlength="30" placeholder="변경 시 입력">
|
|
</div>
|
|
<div class="col">
|
|
<label class="form-label">비밀번호 확인</label>
|
|
<input type="password" class="form-control" name="password2" maxlength="30" placeholder="변경 시 입력">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 옵션 필드(동적) -->
|
|
<div id="editOptionFields" class="mt-2"><!-- JS 렌더 --></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">취소</button>
|
|
<button class="btn btn-primary" type="submit">저장</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 삭제 확인 모달 -->
|
|
<div class="modal fade" id="userDeleteModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header"><h5 class="modal-title">삭제 확인</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="small">해당 회원을 삭제할까요?</div>
|
|
<input type="hidden" id="del_user_id">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">취소</button>
|
|
<button class="btn btn-danger" type="button" id="btnConfirmDelete">삭제</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
$(function(){
|
|
// ------- 샘플: 이 테넌트에서 허용된 옵션 -------
|
|
const allowedOptions = ['사번','계좌번호'];
|
|
|
|
// ------- 샘플 유저 데이터 -------
|
|
let SEQ = 2;
|
|
const users = [
|
|
{ id:1, user_id:'kevin', name:'권혁성', email:'kevin@sample.com', phone:'010-1111-2222',
|
|
options: {'사번':'A001','계좌번호':'111-2222-3333'}, created_at:'2024-07-01', profile_photo_path:'' },
|
|
{ id:2, user_id:'sally', name:'김슬기', email:'sally@sample.com', phone:'010-3333-4444',
|
|
options: {'사번':'A002','계좌번호':'222-3333-4444'}, created_at:'2024-07-10', profile_photo_path:'' },
|
|
];
|
|
|
|
// ------- 유틸 -------
|
|
function escapeHtml(s){ return String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }
|
|
|
|
// ------- 테이블 헤더 렌더(옵션 포함 동적) -------
|
|
function renderThead(){
|
|
const fixed = ['#','회원ID','이름','이메일','전화번호'];
|
|
const tail = ['가입일','관리'];
|
|
const ths = [
|
|
...fixed.map(t=>`<th>${t}</th>`),
|
|
...allowedOptions.map(o=>`<th>${escapeHtml(o)}</th>`),
|
|
...tail.map(t=>`<th>${t}</th>`)
|
|
].join('');
|
|
$('#theadRow').html(ths);
|
|
}
|
|
|
|
// ------- 리스트 렌더 -------
|
|
function renderTbody(){
|
|
const rows = users.map(u=>{
|
|
const opts = allowedOptions.map(o=>{
|
|
const v = (u.options && u.options[o]) ? u.options[o] : '-';
|
|
return `<td>${escapeHtml(v)}</td>`;
|
|
}).join('');
|
|
return `
|
|
<tr data-id="${u.id}">
|
|
<td>${u.id}</td>
|
|
<td>${escapeHtml(u.user_id)}</td>
|
|
<td>${escapeHtml(u.name)}</td>
|
|
<td>${escapeHtml(u.email)}</td>
|
|
<td>${escapeHtml(u.phone||'')}</td>
|
|
${opts}
|
|
<td>${u.created_at||''}</td>
|
|
<td>
|
|
<div class="d-flex justify-content-center gap-1">
|
|
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${u.id}">수정</button>
|
|
<button class="btn btn-sm btn-outline-danger btn-del" data-id="${u.id}">삭제</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
$('#userTbody').html(rows);
|
|
}
|
|
|
|
// ------- 옵션 입력 필드 렌더 -------
|
|
function renderOptionInputs($wrap, values={}){
|
|
const html = allowedOptions.map(o=>{
|
|
const key = o; // 표시 라벨과 키 동일(샘플)
|
|
const val = values[key] ?? '';
|
|
return `
|
|
<div class="mb-2">
|
|
<label class="form-label">${escapeHtml(o)}</label>
|
|
<input type="text" class="form-control" name="opt__${encodeURIComponent(key)}" value="${escapeHtml(val)}">
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
$wrap.html(html);
|
|
}
|
|
|
|
// 초기 렌더
|
|
renderThead();
|
|
renderTbody();
|
|
|
|
// ------- 등록 모달 열기 -------
|
|
$('#btnOpenAdd').on('click', function(){
|
|
$('#userAddForm')[0].reset();
|
|
renderOptionInputs($('#addOptionFields'), {});
|
|
new bootstrap.Modal('#userAddModal').show();
|
|
});
|
|
|
|
// ------- 등록 처리(프로토타입: 클라이언트 메모리) -------
|
|
$('#userAddForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const get = n => $(this).find(`[name="${n}"]`).val();
|
|
const uid = get('user_id').trim();
|
|
const name = get('name').trim();
|
|
const email = get('email').trim();
|
|
const phone = get('phone').trim();
|
|
const pw1 = get('password'), pw2 = get('password2');
|
|
|
|
if(uid.length<2){ alert('회원 아이디를 2글자 이상 입력하세요.'); return; }
|
|
if(name.length<2){ alert('이름을 2글자 이상 입력하세요.'); return; }
|
|
if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)){ alert('이메일을 올바르게 입력하세요.'); return; }
|
|
if(pw1.length<4){ alert('비밀번호는 4글자 이상이어야 합니다.'); return; }
|
|
if(pw1!==pw2){ alert('비밀번호가 일치하지 않습니다.'); return; }
|
|
|
|
// 옵션 수집
|
|
const opts = {};
|
|
allowedOptions.forEach(o=>{
|
|
const v = $(this).find(`[name="opt__${encodeURIComponent(o)}"]`).val().trim();
|
|
if(v) opts[o]=v;
|
|
});
|
|
|
|
const id = ++SEQ;
|
|
users.push({
|
|
id, user_id:uid, name, email, phone,
|
|
options: opts, created_at: new Date().toISOString().slice(0,10),
|
|
profile_photo_path:''
|
|
});
|
|
renderTbody();
|
|
bootstrap.Modal.getInstance(document.getElementById('userAddModal')).hide();
|
|
|
|
// 실제 연동 예:
|
|
// const fd = new FormData(this);
|
|
// allowedOptions.forEach(o=> fd.append('options['+o+']', $(this).find(`[name="opt__${encodeURIComponent(o)}"]`).val()));
|
|
// $.ajax({url:'/tenant/tenant/user_add_process.php', method:'POST', data:fd, processData:false, contentType:false})
|
|
// .done(()=>location.reload());
|
|
});
|
|
|
|
// ------- 수정 모달 열기 -------
|
|
$(document).on('click', '.btn-edit', function(){
|
|
const id = +$(this).data('id');
|
|
const u = users.find(x=>x.id===id);
|
|
if(!u) return;
|
|
|
|
$('#edit_id').val(u.id);
|
|
$('#edit_user_id').val(u.user_id);
|
|
$('#edit_name').val(u.name);
|
|
$('#edit_email').val(u.email);
|
|
$('#edit_phone').val(u.phone||'');
|
|
$('#edit_photo_preview').html(u.profile_photo_path ? `<img src="${escapeHtml(u.profile_photo_path)}" alt="프로필" style="height:40px;">` : '');
|
|
|
|
renderOptionInputs($('#editOptionFields'), u.options||{});
|
|
new bootstrap.Modal('#userEditModal').show();
|
|
});
|
|
|
|
// ------- 수정 처리(프로토타입) -------
|
|
$('#userEditForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const id = +$('#edit_id').val();
|
|
const name = $('#edit_name').val().trim();
|
|
const email = $('#edit_email').val().trim();
|
|
const phone = $('#edit_phone').val().trim();
|
|
const pw1 = $(this).find('[name="password"]').val();
|
|
const pw2 = $(this).find('[name="password2"]').val();
|
|
|
|
if(name.length<2){ alert('이름은 2글자 이상 입력하세요.'); return; }
|
|
if(!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)){ alert('이메일을 올바르게 입력하세요.'); return; }
|
|
if(pw1 || pw2){
|
|
if(pw1.length<4){ alert('비밀번호는 4글자 이상이어야 합니다.'); return; }
|
|
if(pw1!==pw2){ alert('비밀번호가 일치하지 않습니다.'); return; }
|
|
}
|
|
|
|
const u = users.find(x=>x.id===id);
|
|
if(!u) return;
|
|
u.name = name; u.email = email; u.phone = phone;
|
|
|
|
// 옵션 업데이트
|
|
const opts = {};
|
|
allowedOptions.forEach(o=>{
|
|
const v = $('#editOptionFields').find(`[name="opt__${encodeURIComponent(o)}"]`).val().trim();
|
|
if(v) opts[o]=v;
|
|
});
|
|
u.options = opts;
|
|
|
|
renderTbody();
|
|
bootstrap.Modal.getInstance(document.getElementById('userEditModal')).hide();
|
|
|
|
// 실제 연동 예:
|
|
// const fd = new FormData(this);
|
|
// allowedOptions.forEach(o=> fd.append('options['+o+']', $('#editOptionFields').find(`[name="opt__${encodeURIComponent(o)}"]`).val()));
|
|
// $.ajax({url:'/tenant/tenant/user_edit_process.php', method:'POST', data:fd, processData:false, contentType:false})
|
|
// .done(()=>location.reload());
|
|
});
|
|
|
|
// ------- 삭제 -------
|
|
$(document).on('click', '.btn-del', function(){
|
|
$('#del_user_id').val(+$(this).data('id'));
|
|
new bootstrap.Modal('#userDeleteModal').show();
|
|
});
|
|
$('#btnConfirmDelete').on('click', function(){
|
|
const id = +$('#del_user_id').val();
|
|
const i = users.findIndex(x=>x.id===id);
|
|
if(i>-1) users.splice(i,1);
|
|
renderTbody();
|
|
bootstrap.Modal.getInstance(document.getElementById('userDeleteModal')).hide();
|
|
|
|
// 실제:
|
|
// location.href = '/tenant/tenant/user_delete.php?id='+id;
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<?php include '../inc/footer.php'; ?>
|