feat : Front Page [Ver 0.1]
This commit is contained in:
10
public/html/inc/footer.php
Normal file
10
public/html/inc/footer.php
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
</div>
|
||||
<footer class="text-center" style="padding:20px 0; border-top:1px solid #eee; margin-top:40px;">
|
||||
<!-- 푸터 영역(카피라이터/회사정보 자리) -->
|
||||
<small>© 2025 SAM(Smart Automation Management). All rights reserved.</small>
|
||||
</footer>
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
34
public/html/inc/header.php
Normal file
34
public/html/inc/header.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>작업 지시 생성</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
||||
<style>
|
||||
body { padding-bottom: 60px; }
|
||||
.page-header h2 { margin-top: 0; }
|
||||
.table > thead > tr > th, .table > tbody > tr > td { vertical-align: middle; }
|
||||
.form-inline .form-group { margin-right: 8px; }
|
||||
.help-note { color:#777; font-size:12px; }
|
||||
.table-fixed thead tr th { background:#f9f9f9; }
|
||||
.panel-heading .btn { margin-left:5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="?page=home">SAM </a>
|
||||
<a class="navbar-brand" href="?page=processes">생산관리</a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="<?=($page=='processes')?'active':''?>"><a href="?page=processes">공정</a></li>
|
||||
<li class="<?=($page=='tasks')?'active':''?>"><a href="?page=tasks">작업</a></li>
|
||||
<li class="<?=($page=='process_settings')?'active':''?>"><a href="?page=process_settings">작업지시 설정</a></li>
|
||||
<li class="<?=($page=='job_order_form')?'active':''?>"><a href="?page=job_order_form">작업지시 생성</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container">
|
||||
2
public/html/pages/home.html
Normal file
2
public/html/pages/home.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<h1>Welcome to my Pure PHP Website!</h1>
|
||||
<p>This is the main content of the page.</p>
|
||||
24
public/tenant/api/login_process.php
Normal file
24
public/tenant/api/login_process.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// POST 파라미터 받기
|
||||
$userid = isset($_POST['userid']) ? trim($_POST['userid']) : '';
|
||||
$password = isset($_POST['password']) ? $_POST['password'] : '';
|
||||
|
||||
// 데모이므로 비밀번호 검증은 생략
|
||||
if (!$userid) {
|
||||
echo json_encode(['success' => false, 'message' => '아이디를 입력하세요.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($userid === 'tenant') {
|
||||
$_SESSION['user_id'] = 'tenant';
|
||||
} else {
|
||||
$_SESSION['user_id'] = 'user';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
?>
|
||||
27
public/tenant/api/register_process.php
Normal file
27
public/tenant/api/register_process.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// POST 데이터 받기
|
||||
$userid = isset($_POST['userid']) ? trim($_POST['userid']) : '';
|
||||
$username = isset($_POST['username']) ? trim($_POST['username']) : '';
|
||||
$password = isset($_POST['password']) ? $_POST['password'] : '';
|
||||
$password2 = isset($_POST['password2']) ? $_POST['password2'] : '';
|
||||
|
||||
// 기본 밸리데이션
|
||||
if (!$userid || !$username || !$password || !$password2) {
|
||||
echo "<script>alert('모든 항목을 입력하세요.'); history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
if ($password !== $password2) {
|
||||
echo "<script>alert('비밀번호가 일치하지 않습니다.'); history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 실제로는 DB에 유저 등록해야 하지만, 샘플이므로 생략
|
||||
|
||||
// 세션 생성 (user로)
|
||||
$_SESSION['user_id'] = 'user';
|
||||
|
||||
// 가입 성공 → 대시보드 이동
|
||||
header("Location: /tenant/member/dashboard.php");
|
||||
exit;
|
||||
59
public/tenant/approval/instances.php
Normal file
59
public/tenant/approval/instances.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php $CURRENT_SECTION='approval'; include '../inc/header.php'; ?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex gap-2 align-items-center">
|
||||
<strong>결재 현황</strong>
|
||||
<select class="form-select form-select-sm" style="width:200px;">
|
||||
<option>LEAVE</option><option>IQC_REPORT</option>
|
||||
</select>
|
||||
<input class="form-control form-control-sm" style="width:220px;" placeholder="제목/작성자/번호">
|
||||
<button class="btn btn-sm btn-outline-secondary">검색</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover align-middle text-center">
|
||||
<thead class="table-light"><tr><th>#</th><th>대상</th><th>참조키</th><th>상태</th><th>작성자</th><th>생성일</th><th style="width:120px;">보기</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>101</td><td>LEAVE</td><td>5678</td><td><span class="badge bg-warning text-dark">진행중</span></td><td>kevin</td><td>2025-08-01</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#viewModal">상세</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 모달: 진행중 결재선 + 의견 -->
|
||||
<div class="modal fade" id="viewModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg"><div class="modal-content">
|
||||
<div class="modal-header"><h5 class="modal-title">결재 상세</h5><button class="btn-close" data-bs-dismiss="modal"></button></div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div class="small text-muted">결재선</div>
|
||||
<ul class="approval-tree">
|
||||
<li><div class="node"><span class="badge bg-dark">루트=문서 포함 승인자</span></div>
|
||||
<ul>
|
||||
<li><div class="node"><span class="badge bg-secondary">개발팀</span> — <span class="badge bg-success">승인</span></div></li>
|
||||
<li><div class="node"><span class="badge bg-info text-dark">일반관리자</span> — <span class="badge bg-warning text-dark">대기</span></div></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">의견</label>
|
||||
<textarea class="form-control" rows="3"></textarea>
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
<button class="btn btn-success">승인</button>
|
||||
<button class="btn btn-danger">반려</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.approval-tree { list-style:none; padding-left:1rem; }
|
||||
.approval-tree .node { padding:.25rem .5rem; border:1px solid #e5e8ef; border-radius:.5rem; display:inline-block; background:#f8fafc; }
|
||||
.approval-tree ul { list-style:none; margin-left:1.25rem; padding-left:1rem; border-left:2px dashed #dde3f3; }
|
||||
.approval-tree li { margin:.5rem 0; }
|
||||
</style>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
42
public/tenant/approval/objects.php
Normal file
42
public/tenant/approval/objects.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php $CURRENT_SECTION='approval'; include '../inc/header.php'; ?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>결재 대상 관리</strong>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#objModal">추가</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover align-middle text-center">
|
||||
<thead class="table-light"><tr><th>코드</th><th>이름</th><th>연결 메뉴</th><th style="width:140px;">관리</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>LEAVE</td><td>연차 신청</td><td>/tenant/hr/leave_apply.php</td>
|
||||
<td><a class="btn btn-sm btn-outline-secondary" href="/tenant/approval/rules.php?object=LEAVE">규칙</a>
|
||||
<button class="btn btn-sm btn-outline-danger">삭제</button></td></tr>
|
||||
<tr><td>IQC_REPORT</td><td>수입성적서</td><td>/tenant/quality/iqc_report.php</td>
|
||||
<td><a class="btn btn-sm btn-outline-secondary" href="/tenant/approval/rules.php?object=IQC_REPORT">규칙</a>
|
||||
<button class="btn btn-sm btn-outline-danger">삭제</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 대상 추가 모달 -->
|
||||
<div class="modal fade" id="objModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog"><div class="modal-content">
|
||||
<div class="modal-header"><h5 class="modal-title">결재 대상 추가</h5><button class="btn-close" data-bs-dismiss="modal"></button></div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-2"><label class="form-label">코드</label><input class="form-control" id="objCode"></div>
|
||||
<div class="mb-2"><label class="form-label">이름</label><input class="form-control" id="objName"></div>
|
||||
<div class="mb-2"><label class="form-label">연결 메뉴 경로</label><input class="form-control" id="objPath" placeholder="/tenant/..."></div>
|
||||
</div>
|
||||
<div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">닫기</button><button class="btn btn-primary" id="saveObj">저장</button></div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#saveObj').on('click', function(){ alert('저장(샘플) /api/approval/object_save.php'); $('#objModal').modal('hide'); });
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
32
public/tenant/approval/pool.php
Normal file
32
public/tenant/approval/pool.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php $CURRENT_SECTION='approval'; include '../inc/header.php'; ?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>결재권자 풀 관리</strong>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" id="type" style="width:140px;">
|
||||
<option value="user">개인</option><option value="department">부서</option><option value="role">역할</option>
|
||||
</select>
|
||||
<input class="form-control form-control-sm" id="keyword" placeholder="이름/코드">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="find">검색</button>
|
||||
<button class="btn btn-sm btn-primary" id="add">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light"><tr><th>유형</th><th>대상</th><th style="width:120px;">관리</th></tr></thead>
|
||||
<tbody id="poolRows">
|
||||
<tr><td>부서</td><td>개발팀</td><td><button class="btn btn-sm btn-outline-danger">삭제</button></td></tr>
|
||||
<tr><td>역할</td><td>일반관리자</td><td><button class="btn btn-sm btn-outline-danger">삭제</button></td></tr>
|
||||
<tr><td>개인</td><td>kevin</td><td><button class="btn btn-sm btn-outline-danger">삭제</button></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#add').on('click', ()=>alert('추가(샘플) /api/approval/pool_add.php'));
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
145
public/tenant/approval/rules.php
Normal file
145
public/tenant/approval/rules.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php $CURRENT_SECTION='approval'; include '../inc/header.php';
|
||||
$object = $_GET['object'] ?? 'LEAVE';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>결재 규칙 (<?=$object?>)</strong>
|
||||
<button class="btn btn-sm btn-primary" id="btnAddRule">추가</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group" id="ruleList">
|
||||
<li class="list-group-item active" data-id="1">연차 기본 규칙</li>
|
||||
<li class="list-group-item" data-id="2">연차 간소화 규칙</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">결재선(트리)</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info small">
|
||||
문서 생성 시 <b>포함된 승인자</b>가 자동으로 <b>1차 결재자(루트)</b>가 됩니다.
|
||||
아래 트리는 <u>루트 이후 상위 결재자</u>를 parent_id로 연결합니다.
|
||||
</div>
|
||||
|
||||
<div id="treeArea" class="border rounded p-3">
|
||||
<!-- 샘플 트리 표현 (간단한 UL/LI) -->
|
||||
<ul class="approval-tree">
|
||||
<li>
|
||||
<div class="node">
|
||||
<span class="badge bg-dark">루트=문서 포함 승인자(동적)</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="node">
|
||||
<span class="badge bg-secondary">개발팀</span>
|
||||
<span class="text-muted ms-2">required: 1</span>
|
||||
<button class="btn btn-sm btn-outline-danger ms-2 btnDelNode">삭제</button>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<div class="node">
|
||||
<span class="badge bg-info text-dark">일반관리자</span>
|
||||
<span class="text-muted ms-2">required: 1</span>
|
||||
<button class="btn btn-sm btn-outline-danger ms-2 btnDelNode">삭제</button>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="node">
|
||||
<span class="badge bg-dark">kevin</span>
|
||||
<span class="text-muted ms-2">required: 1</span>
|
||||
<button class="btn btn-sm btn-outline-danger ms-2 btnDelNode">삭제</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<select class="form-select" id="poolSelect" style="max-width:220px;">
|
||||
<option value="d1" data-type="department">개발팀</option>
|
||||
<option value="r2" data-type="role">일반관리자</option>
|
||||
<option value="u9" data-type="user">kevin</option>
|
||||
</select>
|
||||
<input type="number" class="form-control" id="requiredCnt" value="1" style="max-width:120px;" placeholder="required">
|
||||
<button class="btn btn-outline-primary" id="btnAddChild">선택 노드의 상위로 추가</button>
|
||||
<button class="btn btn-primary" id="btnSave">저장</button>
|
||||
</div>
|
||||
|
||||
<div class="small text-muted mt-2">* “상위 추가”는 현재 포커스 노드의 parent로 삽입하는 의미(=다음 결재자).</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.approval-tree { list-style:none; padding-left:1rem; }
|
||||
.approval-tree .node { padding:.25rem .5rem; border:1px solid #e5e8ef; border-radius:.5rem; display:inline-block; background:#f8fafc; }
|
||||
.approval-tree ul { list-style:none; margin-left:1.25rem; padding-left:1rem; border-left:2px dashed #dde3f3; }
|
||||
.approval-tree li { margin:.5rem 0; }
|
||||
.node.focus { outline:2px solid #5b8bff; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
let focused = null;
|
||||
|
||||
// 노드 포커스
|
||||
$(document).on('click', '.node', function(){
|
||||
$('.node').removeClass('focus');
|
||||
$(this).addClass('focus');
|
||||
focused = this;
|
||||
});
|
||||
|
||||
// 상위(부모) 노드 추가 (시각적 데모)
|
||||
$('#btnAddChild').on('click', function(){
|
||||
if (!focused) return alert('추가할 기준 노드를 선택하세요.');
|
||||
const label = $('#poolSelect option:selected').text();
|
||||
const req = $('#requiredCnt').val()||1;
|
||||
|
||||
// focused의 부모 <ul>을 찾고 그 위에 새 노드를 끼워 넣는 느낌 (데모)
|
||||
const $li = $(focused).closest('li');
|
||||
const $parentUl = $li.parent();
|
||||
|
||||
const html = `
|
||||
<li>
|
||||
<div class="node"><span class="badge bg-secondary">${label}</span>
|
||||
<span class="text-muted ms-2">required: ${req}</span>
|
||||
<button class="btn btn-sm btn-outline-danger ms-2 btnDelNode">삭제</button>
|
||||
</div>
|
||||
</li>`;
|
||||
$parentUl.prepend(html);
|
||||
});
|
||||
|
||||
$(document).on('click', '.btnDelNode', function(e){
|
||||
e.stopPropagation();
|
||||
$(this).closest('li').remove();
|
||||
});
|
||||
|
||||
$('#btnSave').on('click', function(){
|
||||
alert('저장(샘플) /api/approval/rule_save.php');
|
||||
});
|
||||
|
||||
$('#btnAddRule').on('click', function(){
|
||||
alert('규칙 추가(샘플) /api/approval/rule_add.php?object=<?=$object?>');
|
||||
});
|
||||
|
||||
// 규칙 선택
|
||||
$(document).on('click', '#ruleList .list-group-item', function(){
|
||||
$('#ruleList .list-group-item').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
// 선택 규칙의 트리 로드(샘플)
|
||||
alert('규칙 로드(샘플) /api/approval/rule_get.php?id='+$(this).data('id'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
91
public/tenant/assets/js/permission_menu.js
Normal file
91
public/tenant/assets/js/permission_menu.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// /tenant/assets/js/permission_menu.js
|
||||
// 공통: 메뉴 트리 데이터 관리 + 행 빌더 + 전파/요약 유틸
|
||||
|
||||
window.PermissionMenu = (function () {
|
||||
let MENU_DATA = [];
|
||||
let TENANT_USE = {}; // code:boolean (메뉴관리에서 활성화된 것만 권한 대상)
|
||||
let byId = {}, byCode = {}, childrenMap = {};
|
||||
|
||||
// ---------- 내부 유틸 ----------
|
||||
const esc = s => String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
|
||||
function rebuildMaps() {
|
||||
const active = MENU_DATA.filter(n => !!TENANT_USE[n.code]);
|
||||
byId = {}; byCode = {}; childrenMap = {};
|
||||
active.forEach(n => { byId[n.id] = n; byCode[n.code] = n; });
|
||||
active.forEach(n => {
|
||||
const k = n.parent_id ?? 0;
|
||||
(childrenMap[k] ??= []).push(n);
|
||||
});
|
||||
// 정렬
|
||||
Object.keys(childrenMap).forEach(k => childrenMap[k].sort((a,b)=>a.title.localeCompare(b.title,'ko')));
|
||||
}
|
||||
|
||||
function badgeSource(src) {
|
||||
if (src === 'workflow') return '<span class="badge bg-warning text-dark ms-1">workflow</span>';
|
||||
if (src === 'board') return '<span class="badge bg-success ms-1">board</span>';
|
||||
return '<span class="badge bg-secondary ms-1">system</span>';
|
||||
}
|
||||
|
||||
function indentOf(depth) {
|
||||
return ' '.repeat(depth*4) + (depth ? '└ ' : '');
|
||||
}
|
||||
|
||||
// ---------- 외부 API ----------
|
||||
/**
|
||||
* 데이터 세팅 (페이지 최초 1회)
|
||||
* @param {Array} menuData - [{id,parent_id,title,code,path,source}]
|
||||
* @param {Object} tenantUse - { code: true/false }
|
||||
*/
|
||||
function setData(menuData, tenantUse) {
|
||||
MENU_DATA = Array.isArray(menuData) ? menuData : [];
|
||||
TENANT_USE = tenantUse || {};
|
||||
rebuildMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* 트리 순회하며 행을 만든다.
|
||||
* @param {Function} rowBuilder - (node, depth) => string (추가 컬럼 HTML)
|
||||
* @returns {string} HTML rows
|
||||
*
|
||||
* 기본 1열(메뉴명/배지/URL)은 공통이 그려주고,
|
||||
* 나머지 열들은 rowBuilder가 반환한 HTML을 붙인다.
|
||||
*/
|
||||
function buildRows(rowBuilder) {
|
||||
function walk(parentId = null, depth = 0, acc = []) {
|
||||
const list = childrenMap[parentId ?? 0] || [];
|
||||
for (const n of list) {
|
||||
const titleCell = `
|
||||
<td>
|
||||
<span class="indent">${indentOf(depth)}</span>
|
||||
<span class="menu-name">${esc(n.title)}</span>
|
||||
${badgeSource(n.source || 'system')}
|
||||
${n.path ? `<span class="menu-url ms-2 text-muted">${esc(n.path)}</span>` : ''}
|
||||
</td>`;
|
||||
const rest = rowBuilder ? rowBuilder(n, depth) : '';
|
||||
acc.push(`<tr data-id="${n.id}" data-code="${esc(n.code)}" data-parent="${n.parent_id ?? ''}">${titleCell}${rest}</tr>`);
|
||||
walk(n.id, depth + 1, acc);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
return walk(null, 0, []).join('');
|
||||
}
|
||||
|
||||
// 전파/요약에 필요한 헬퍼들을 노출 (페이지별 권한 UI에서 사용)
|
||||
function childrenOf(code) {
|
||||
const node = byCode[code];
|
||||
return node ? (childrenMap[node.id] || []) : [];
|
||||
}
|
||||
function parentOf(code) {
|
||||
const node = byCode[code];
|
||||
return (node && node.parent_id) ? byId[node.parent_id] : null;
|
||||
}
|
||||
function activeCodes() {
|
||||
return Object.keys(byCode);
|
||||
}
|
||||
function maps() { return { byId, byCode, childrenMap }; }
|
||||
|
||||
return { setData, buildRows, childrenOf, parentOf, activeCodes, maps };
|
||||
})();
|
||||
295
public/tenant/category/category_list.php
Normal file
295
public/tenant/category/category_list.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<!-- 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>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary" id="btnToggleAll">전체 접기</button>
|
||||
<button class="btn btn-primary" id="btnOpenAddRoot">+ 카테고리 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle text-center" id="catTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:80px;">#</th>
|
||||
<th class="text-start">카테고리명</th>
|
||||
<th>설명</th>
|
||||
<th style="width:220px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="catTbody"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<div class="modal fade" id="catAddModal" 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="catAddForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 카테고리</label>
|
||||
<select class="form-select" id="add_parent_select"><!-- 옵션 렌더 --></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">카테고리명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="desc" maxlength="100">
|
||||
</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="catEditModal" 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="catEditForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit_id">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 카테고리</label>
|
||||
<select class="form-select" id="edit_parent_select"><!-- 옵션 렌더 --></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">카테고리명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="edit_name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" id="edit_desc" maxlength="100">
|
||||
</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="catDeleteModal" 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_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>
|
||||
|
||||
<style>
|
||||
/* 부서 트리와 동일 톤 */
|
||||
#catTbody tr[data-level] td.name-cell{
|
||||
--indent: calc(var(--level,0)*18px);
|
||||
padding-left: calc(var(--indent) + .25rem) !important;
|
||||
}
|
||||
.caret{
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
width:1.15rem; height:1.15rem; border-radius:4px; border:1px solid #cbd3e1;
|
||||
background:#f6f8fc; cursor:pointer; margin-right:.35rem; font-size:.8rem; user-select:none;
|
||||
}
|
||||
.caret[aria-expanded="false"]::after{ content:"+"; }
|
||||
.caret[aria-expanded="true"]::after{ content:"–"; }
|
||||
.badge-node{ background:#e9f0fb; color:#2c4a85; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// ===== 샘플 데이터 (id, parent_id, name, desc) =====
|
||||
let SEQ = 6;
|
||||
const nodes = [
|
||||
{id:1, parent_id:0, name:'제품군', desc:'회사 제품 카테고리'},
|
||||
{id:2, parent_id:1, name:'방화제품', desc:'방화/내화'},
|
||||
{id:3, parent_id:2, name:'방화셔터', desc:'셔터 라인'},
|
||||
{id:4, parent_id:1, name:'일반제품', desc:'상용 제품'},
|
||||
{id:5, parent_id:0, name:'서비스', desc:'서비스 카테고리'},
|
||||
{id:6, parent_id:5, name:'유지보수', desc:'유지보수/점검'},
|
||||
];
|
||||
const expanded = new Set([1,5]); // 기본 펼침
|
||||
|
||||
const childrenOf = pid => nodes.filter(n=>n.parent_id===pid);
|
||||
const hasChildren = id => nodes.some(n=>n.parent_id===id);
|
||||
const findNode = id => nodes.find(n=>n.id==id);
|
||||
const esc = s => String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m]));
|
||||
|
||||
function flatten(pid=0, level=0){
|
||||
const list = childrenOf(pid).sort((a,b)=>a.name.localeCompare(b.name,'ko'));
|
||||
let out=[]; for(const n of list){
|
||||
out.push({...n, level});
|
||||
if(expanded.has(n.id)) out.push(...flatten(n.id, level+1));
|
||||
} return out;
|
||||
}
|
||||
|
||||
function render(){
|
||||
const rows = flatten(0,0).map(r=>{
|
||||
const caret = hasChildren(r.id)
|
||||
? `<span class="caret" data-id="${r.id}" aria-expanded="${expanded.has(r.id)}"></span>`
|
||||
: `<span style="display:inline-block;width:1.15rem;margin-right:.35rem;"></span>`;
|
||||
return `
|
||||
<tr data-id="${r.id}" data-level="${r.level}" style="--level:${r.level}">
|
||||
<td>${r.id}</td>
|
||||
<td class="text-start name-cell">
|
||||
${caret}
|
||||
<span class="badge badge-node me-2">Lv.${r.level}</span>
|
||||
<strong>${esc(r.name)}</strong>
|
||||
</td>
|
||||
<td>${esc(r.desc||'')}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button class="btn btn-sm btn-outline-primary btn-add" data-id="${r.id}">하위등록</button>
|
||||
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${r.id}">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btn-del" data-id="${r.id}">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
$('#catTbody').html(rows);
|
||||
}
|
||||
|
||||
function buildOptions(selected=0, excludeId=null){
|
||||
function walk(pid=0, level=0){
|
||||
return childrenOf(pid).sort((a,b)=>a.name.localeCompare(b.name,'ko'))
|
||||
.flatMap(n=>{
|
||||
if(excludeId && n.id===excludeId) return [];
|
||||
return [
|
||||
`<option value="${n.id}" ${selected==n.id?'selected':''}>${esc('— '.repeat(level)+n.name)}</option>`,
|
||||
...walk(n.id, level+1)
|
||||
];
|
||||
});
|
||||
}
|
||||
return [`<option value="0" ${selected==0?'selected':''}>(최상위)</option>`, ...walk()].join('');
|
||||
}
|
||||
|
||||
function removeBranch(id){
|
||||
for(const k of childrenOf(id)) removeBranch(k.id);
|
||||
const i = nodes.findIndex(n=>n.id==id);
|
||||
if(i>-1) nodes.splice(i,1);
|
||||
expanded.delete(id);
|
||||
}
|
||||
|
||||
// 펼침/접기
|
||||
$(document).on('click', '.caret', function(){
|
||||
const id = +$(this).data('id');
|
||||
if(expanded.has(id)) expanded.delete(id); else expanded.add(id);
|
||||
render();
|
||||
});
|
||||
|
||||
// 전체 접기/펼치기
|
||||
$('#btnToggleAll').on('click', function(){
|
||||
const allIds = nodes.map(n=>n.id);
|
||||
const allOpen = allIds.every(id => expanded.has(id) || !hasChildren(id));
|
||||
if(allOpen){ expanded.clear(); $(this).text('전체 펼치기'); }
|
||||
else{ expanded.clear(); allIds.forEach(id=>{ if(hasChildren(id)) expanded.add(id); }); $(this).text('전체 접기'); }
|
||||
render();
|
||||
});
|
||||
|
||||
// 루트 등록
|
||||
$('#btnOpenAddRoot').on('click', function(){
|
||||
$('#catAddForm')[0].reset();
|
||||
$('#add_parent_select').html(buildOptions(0));
|
||||
new bootstrap.Modal('#catAddModal').show();
|
||||
});
|
||||
|
||||
// 하위등록
|
||||
$(document).on('click', '.btn-add', function(){
|
||||
const pid = +$(this).data('id');
|
||||
$('#catAddForm')[0].reset();
|
||||
$('#add_parent_select').html(buildOptions(pid));
|
||||
expanded.add(pid);
|
||||
new bootstrap.Modal('#catAddModal').show();
|
||||
});
|
||||
|
||||
// 등록 처리(프로토타입)
|
||||
$('#catAddForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const pid = +$('#add_parent_select').val();
|
||||
const name = $(this).find('[name="name"]').val().trim();
|
||||
const desc = $(this).find('[name="desc"]').val().trim();
|
||||
if(name.length<2){ alert('카테고리명을 2글자 이상 입력하세요.'); return; }
|
||||
const id = ++SEQ;
|
||||
nodes.push({id, parent_id:pid, name, desc});
|
||||
expanded.add(pid);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('catAddModal')).hide();
|
||||
// 실제: $.post('/tenant/category/category_add_process.php', {parent_id:pid,name,desc})
|
||||
});
|
||||
|
||||
// 수정 열기
|
||||
$(document).on('click', '.btn-edit', function(){
|
||||
const id = +$(this).data('id');
|
||||
const n = findNode(id); if(!n) return;
|
||||
$('#edit_id').val(n.id);
|
||||
$('#edit_name').val(n.name);
|
||||
$('#edit_desc').val(n.desc||'');
|
||||
$('#edit_parent_select').html(buildOptions(n.parent_id, n.id));
|
||||
new bootstrap.Modal('#catEditModal').show();
|
||||
});
|
||||
|
||||
// 수정 처리
|
||||
$('#catEditForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const id = +$('#edit_id').val();
|
||||
const pid = +$('#edit_parent_select').val();
|
||||
const name = $('#edit_name').val().trim();
|
||||
const desc = $('#edit_desc').val().trim();
|
||||
if(name.length<2){ alert('카테고리명을 2글자 이상 입력하세요.'); return; }
|
||||
const n = findNode(id); if(!n) return;
|
||||
n.name = name; n.desc = desc; n.parent_id = pid;
|
||||
expanded.add(pid);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('catEditModal')).hide();
|
||||
// 실제: $.post('/tenant/category/category_edit_process.php', {id, parent_id:pid, name, desc})
|
||||
});
|
||||
|
||||
// 삭제
|
||||
$(document).on('click', '.btn-del', function(){
|
||||
$('#del_id').val(+$(this).data('id'));
|
||||
new bootstrap.Modal('#catDeleteModal').show();
|
||||
});
|
||||
$('#btnConfirmDelete').on('click', function(){
|
||||
const id = +$('#del_id').val();
|
||||
removeBranch(id);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('catDeleteModal')).hide();
|
||||
// 실제: location.href='/tenant/category/category_delete.php?id='+id;
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
274
public/tenant/category/subcategory_list.php
Normal file
274
public/tenant/category/subcategory_list.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
$CURRENT_SECTION = 'category';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 서버 렌더 기준 선택된 카테고리 (없으면 1)
|
||||
$category_id = isset($_GET['category_id']) ? intval($_GET['category_id']) : 1;
|
||||
?>
|
||||
<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">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<h4 class="mb-0">분류(서브카테고리)</h4>
|
||||
<!-- 카테고리 선택 -->
|
||||
<select id="selCategory" class="form-select form-select-sm" style="width:220px;">
|
||||
<!-- JS에서 옵션 렌더 -->
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="btnOpenAdd">+ 분류 등록</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle text-center" id="subTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:80px;">#</th>
|
||||
<th>분류명</th>
|
||||
<th>설명</th>
|
||||
<th style="width:150px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subTbody"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<div class="modal fade" id="subAddModal" 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="subAddForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 카테고리</label>
|
||||
<select class="form-select" id="add_parent_cat"></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">분류명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="desc" maxlength="100">
|
||||
</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="subEditModal" 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="subEditForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit_id">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 카테고리</label>
|
||||
<select class="form-select" id="edit_parent_cat"></select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">분류명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="edit_name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" id="edit_desc" maxlength="100">
|
||||
</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="subDeleteModal" 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_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(){
|
||||
// ===== 샘플 데이터 =====
|
||||
// 카테고리 목록 (id, name)
|
||||
const categories = [
|
||||
{id:1, name:'제품군'},
|
||||
{id:2, name:'서비스'}
|
||||
];
|
||||
// 분류 목록 (카테고리별 배열)
|
||||
let SEQ = 300;
|
||||
const subsByCat = {
|
||||
1: [
|
||||
{id:101, name:'노트북', desc:'휴대용 컴퓨터'},
|
||||
{id:102, name:'데스크탑', desc:'사무/게임용 컴퓨터'},
|
||||
],
|
||||
2: [
|
||||
{id:201, name:'호스팅', desc:'웹 호스팅 서비스'},
|
||||
{id:202, name:'유지보수',desc:'운영/AS 지원'},
|
||||
]
|
||||
};
|
||||
|
||||
// 초기 선택(서버에서 전달된 category_id)
|
||||
let currentCat = <?php echo json_encode($category_id); ?>;
|
||||
if(!categories.some(c=>c.id===currentCat)) currentCat = categories[0]?.id || 1;
|
||||
|
||||
// ----- 렌더러 -----
|
||||
function renderCatOptions(){
|
||||
$('#selCategory, #add_parent_cat, #edit_parent_cat').each(function(){
|
||||
const selId = this.id==='selCategory' ? currentCat : currentCat;
|
||||
const html = categories.map(c =>
|
||||
`<option value="${c.id}" ${c.id===selId?'selected':''}>${escapeHtml(c.name)}</option>`
|
||||
).join('');
|
||||
$(this).html(html);
|
||||
});
|
||||
}
|
||||
|
||||
function renderList(){
|
||||
const list = subsByCat[currentCat] || [];
|
||||
const rows = list.map(s=>`
|
||||
<tr data-id="${s.id}">
|
||||
<td>${s.id}</td>
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td>${escapeHtml(s.desc||'')}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${s.id}">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btn-del" data-id="${s.id}">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#subTbody').html(rows);
|
||||
}
|
||||
|
||||
const escapeHtml = s => String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m]));
|
||||
|
||||
// ----- 이벤트 -----
|
||||
// 카테고리 변경
|
||||
$(document).on('change', '#selCategory', function(){
|
||||
currentCat = +$(this).val();
|
||||
renderCatOptions(); // 상단/모달의 셀렉트 동기화
|
||||
renderList();
|
||||
// 실제: location.href = '?category_id='+currentCat;
|
||||
});
|
||||
|
||||
// 등록 열기
|
||||
$('#btnOpenAdd').on('click', function(){
|
||||
$('#subAddForm')[0].reset();
|
||||
renderCatOptions();
|
||||
new bootstrap.Modal('#subAddModal').show();
|
||||
});
|
||||
|
||||
// 등록 처리
|
||||
$('#subAddForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const pid = +$('#add_parent_cat').val();
|
||||
const name = $(this).find('[name="name"]').val().trim();
|
||||
const desc = $(this).find('[name="desc"]').val().trim();
|
||||
if(name.length<2){ alert('분류명을 2글자 이상 입력하세요.'); return; }
|
||||
|
||||
const id = ++SEQ;
|
||||
subsByCat[pid] = subsByCat[pid] || [];
|
||||
subsByCat[pid].push({id, name, desc});
|
||||
|
||||
currentCat = pid;
|
||||
renderCatOptions();
|
||||
renderList();
|
||||
bootstrap.Modal.getInstance(document.getElementById('subAddModal')).hide();
|
||||
|
||||
// 실제: $.post('/tenant/category/subcategory_add_process.php', {category_id:pid, subcategory_name:name, subcategory_desc:desc})
|
||||
});
|
||||
|
||||
// 수정 열기
|
||||
$(document).on('click', '.btn-edit', function(){
|
||||
const id = +$(this).data('id');
|
||||
const list = subsByCat[currentCat] || [];
|
||||
const item = list.find(x=>x.id===id); if(!item) return;
|
||||
$('#edit_id').val(id);
|
||||
$('#edit_name').val(item.name);
|
||||
$('#edit_desc').val(item.desc||'');
|
||||
renderCatOptions();
|
||||
new bootstrap.Modal('#subEditModal').show();
|
||||
});
|
||||
|
||||
// 수정 처리
|
||||
$('#subEditForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const id = +$('#edit_id').val();
|
||||
const pid = +$('#edit_parent_cat').val();
|
||||
const name = $('#edit_name').val().trim();
|
||||
const desc = $('#edit_desc').val().trim();
|
||||
if(name.length<2){ alert('분류명을 2글자 이상 입력하세요.'); return; }
|
||||
|
||||
// 기존 위치에서 제거 후 새 카테고리에 삽입(카테고리 이동 허용)
|
||||
for(const k of Object.keys(subsByCat)){
|
||||
const idx = (subsByCat[k]||[]).findIndex(x=>x.id===id);
|
||||
if(idx>-1){ subsByCat[k].splice(idx,1); break; }
|
||||
}
|
||||
subsByCat[pid] = subsByCat[pid] || [];
|
||||
subsByCat[pid].push({id, name, desc});
|
||||
|
||||
currentCat = pid;
|
||||
renderCatOptions();
|
||||
renderList();
|
||||
bootstrap.Modal.getInstance(document.getElementById('subEditModal')).hide();
|
||||
|
||||
// 실제: $.post('/tenant/category/subcategory_edit_process.php', {category_id:pid, subcategory_id:id, subcategory_name:name, subcategory_desc:desc})
|
||||
});
|
||||
|
||||
// 삭제 열기/확정
|
||||
$(document).on('click', '.btn-del', function(){
|
||||
$('#del_id').val(+$(this).data('id'));
|
||||
new bootstrap.Modal('#subDeleteModal').show();
|
||||
});
|
||||
$('#btnConfirmDelete').on('click', function(){
|
||||
const id = +$('#del_id').val();
|
||||
const list = subsByCat[currentCat] || [];
|
||||
const idx = list.findIndex(x=>x.id===id);
|
||||
if(idx>-1) list.splice(idx,1);
|
||||
renderList();
|
||||
bootstrap.Modal.getInstance(document.getElementById('subDeleteModal')).hide();
|
||||
|
||||
// 실제: location.href = '/tenant/category/subcategory_delete.php?category_id='+currentCat+'&id='+id;
|
||||
});
|
||||
|
||||
// 초기 렌더
|
||||
renderCatOptions();
|
||||
renderList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
3
public/tenant/inc/config.php
Normal file
3
public/tenant/inc/config.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
@session_start();
|
||||
?>
|
||||
39
public/tenant/inc/footer.php
Normal file
39
public/tenant/inc/footer.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<br><br><br>
|
||||
<footer>
|
||||
© 2025 CodeBridge. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<script src="https://code.highcharts.com/maps/highmaps.js"></script>
|
||||
<script src="https://code.highcharts.com/mapdata/custom/world.js"></script>
|
||||
<script src="https://code.highcharts.com/modules/funnel.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 부트스트랩5 서브메뉴 토글
|
||||
document.addEventListener("DOMContentLoaded", function(){
|
||||
document.querySelectorAll('.dropdown-submenu .dropdown-toggle').forEach(function(element){
|
||||
element.addEventListener('click', function(e){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 다른 열린 서브메뉴 닫기
|
||||
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(subMenu){
|
||||
if(subMenu !== this.nextElementSibling){
|
||||
subMenu.classList.remove('show');
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// 현재만 토글
|
||||
this.nextElementSibling.classList.toggle('show');
|
||||
});
|
||||
});
|
||||
|
||||
// 바깥 클릭 시 닫힘
|
||||
document.body.addEventListener('click', function(){
|
||||
document.querySelectorAll('.dropdown-submenu .dropdown-menu.show').forEach(function(subMenu){
|
||||
subMenu.classList.remove('show');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
81
public/tenant/inc/header.php
Normal file
81
public/tenant/inc/header.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php include '../inc/config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>SAM::테넌트페이지</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background-color: #fefefe; /* #f8f9fa */
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
padding-bottom: 80px; /* 푸터 높이만큼 */
|
||||
}
|
||||
header, footer {
|
||||
background-color: #2c4a85;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(44,74,133,0.07);
|
||||
border: 1.5px solid #e3eaf3;
|
||||
}
|
||||
nav.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
nav a {
|
||||
color: white;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* 푸터 하단 고정 */
|
||||
footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* 2단 메뉴(서브드롭다운) 오른쪽에 뜨게 */
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
}
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
margin-top: -1px;
|
||||
margin-left: 0;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
<!-- FullCalendar CSS/JS (jQuery, Bootstrap 5용) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.css" rel="stylesheet" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/index.global.min.js"></script>
|
||||
<!-- FullCalendar 한국어 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.11/locales-all.global.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>SAM (Smart Automation Management)</h1>
|
||||
<nav class="container">
|
||||
<?php include '../inc/navi.php'; ?>
|
||||
</nav>
|
||||
</header>
|
||||
231
public/tenant/inc/navi.php
Normal file
231
public/tenant/inc/navi.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
// 섹션-서브메뉴 정의 (링크는 네가 쓰는 경로에 맞춰두었어)
|
||||
$NAV = [
|
||||
'item' => [
|
||||
'title'=>'품목',
|
||||
'items'=>[
|
||||
['라벨'=>'품목관리','href'=>'/tenant/material/list.php'],
|
||||
['라벨'=>'BOM 단품(기초 바람시)','href'=>'/tenant/product//bending.php'],
|
||||
['라벨'=>'BOM 결합 상품','href'=>'/tenant/product/bom_combined.php'],
|
||||
['라벨'=>'제품 단가','href'=>'/tenant/product/product_price.php'],
|
||||
['라벨'=>'코드 및 로트 관리','href'=>'/tenant/product/code_lot.php'],
|
||||
],
|
||||
],
|
||||
'order' => [
|
||||
'title'=>'수주',
|
||||
'items'=>[
|
||||
['라벨'=>'견적 관리','href'=>'/tenant/order/manage.php'],
|
||||
['라벨'=>'수주 관리','href'=>'/tenant/order/manage.php'],
|
||||
['라벨'=>'수주 등록/수정','href'=>'/tenant/order/edit.php'],
|
||||
['라벨'=>'상태 관리','href'=>'/tenant/order/status.php'],
|
||||
],
|
||||
],
|
||||
'process' => [
|
||||
'title'=>'생산',
|
||||
'items'=>[
|
||||
['라벨'=>'공정관리','href'=>'/tenant/process/processes.php'],
|
||||
['라벨'=>'작업관리','href'=>'/tenant/process/tasks.php'],
|
||||
['라벨'=>'작업지시 설정','href'=>'/tenant/process/process_settings.php'],
|
||||
['라벨'=>'스크린 작업','href'=>'/tenant/production/screen_work.php'],
|
||||
['라벨'=>'슬랫 작업','href'=>'/tenant/production/screen_work.php'],
|
||||
['라벨'=>'절곡 작업','href'=>'/tenant/production/screen_work.php'],
|
||||
],
|
||||
],
|
||||
'shipment' => [
|
||||
'title'=>'출고',
|
||||
'items'=>[
|
||||
['라벨'=>'출고 현황 조회','href'=>'/tenant/shipment/status.php'],
|
||||
['라벨'=>'월간 출고 일정','href'=>'/tenant/shipment/monthly_schedule.php'],
|
||||
],
|
||||
],
|
||||
'quality' => [
|
||||
'title'=>'품질',
|
||||
'items'=>[
|
||||
['라벨'=>'제품 검사 요청','href'=>'/tenant/quality/request.php'],
|
||||
['라벨'=>'제품 검사 일정','href'=>'/tenant/quality/schedule.php'],
|
||||
['라벨'=>'품질 인정 실적 대장','href'=>'/tenant/quality/record.php'],
|
||||
['라벨'=>'인정서류 자료실','href'=>'/tenant/quality/docs.php'],
|
||||
],
|
||||
],
|
||||
'inventory' => [
|
||||
'title'=>'재고',
|
||||
'items'=>[
|
||||
['라벨'=>'재고 조회','href'=>'/tenant/inventory/stock.php'],
|
||||
['라벨'=>'수입 검사 대장','href'=>'/tenant/material/inspection_list.php'],
|
||||
['라벨'=>'입고 현황','href'=>'/tenant/material/inventory_status.php'],
|
||||
['라벨'=>'부적합품 관리','href'=>'/tenant/inventory/defective.php'],
|
||||
],
|
||||
],
|
||||
'subscription' => [
|
||||
'title'=>'구독',
|
||||
'items'=>[
|
||||
['라벨'=>'구독 하기','href'=>'/tenant/tenant/subscribe.php'],
|
||||
['라벨'=>'구독 관리','href'=>'/tenant/subscription/list.php'],
|
||||
['라벨'=>'회사 조회','href'=>'/tenant/subscription/tenant_list.php'],
|
||||
['라벨'=>'상품별 회사 리스트','href'=>'/tenant/subscription/tenant_product_list.php'],
|
||||
],
|
||||
],
|
||||
'member' => [
|
||||
'title' => '회원',
|
||||
'items' => [
|
||||
['라벨'=>'가입','href'=>'/tenant/member/register.php'],
|
||||
['라벨'=>'회사 선택','href'=>'/tenant/member/tenant_select.php'],
|
||||
['라벨'=>'내정보 수정','href'=>'/tenant/member/profile_edit.php'],
|
||||
['라벨'=>'탈퇴','href'=>'/tenant/member/withdraw.php'],
|
||||
],
|
||||
],
|
||||
'permission' => [
|
||||
'title'=>'메뉴/권한',
|
||||
'items'=>[
|
||||
['라벨'=>'메뉴 관리','href'=>'/tenant/permission/tenant_menu.php'],
|
||||
['라벨'=>'부서 권한 설정','href'=>'/tenant/permission/department.php'],
|
||||
['라벨'=>'역할 권한 설정','href'=>'/tenant/permission/role.php'],
|
||||
['라벨'=>'유저 권한 설정','href'=>'/tenant/permission/user.php'],
|
||||
['라벨'=>'권한 분석','href'=>'/tenant/permission/analyze.php'],
|
||||
],
|
||||
],
|
||||
'approval' => [
|
||||
'title'=>'결재(승인)',
|
||||
'items'=>[
|
||||
['라벨'=>'결재 대상 관리','href'=>'/tenant/approval/objects.php'],
|
||||
['라벨'=>'결재 규칙/결재선 관리','href'=>'/tenant/approval/rules.php'],
|
||||
['라벨'=>'결재권자 관리','href'=>'/tenant/approval/pool.php'],
|
||||
['라벨'=>'결재 현황 조회','href'=>'/tenant/approval/instances.php'],
|
||||
],
|
||||
],
|
||||
'tenant' => [
|
||||
'title'=>'회사',
|
||||
'items'=>[
|
||||
['라벨'=>'회사 가입','href'=>'/tenant/tenant/join.php'],
|
||||
['라벨'=>'수정','href'=>'/tenant/tenant/edit.php'],
|
||||
['라벨'=>'탈퇴','href'=>'/tenant/tenant/withdraw.php'],
|
||||
['라벨'=>'카테고리 관리','href'=>'/tenant/category/category_list.php'],
|
||||
['라벨'=>'분류 관리','href'=>'/tenant/category/subcategory_list.php'],
|
||||
['라벨'=>'부서 관리','href'=>'/tenant/tenant/department_list.php'],
|
||||
['라벨'=>'역할 관리','href'=>'/tenant/tenant/role_list.php'],
|
||||
['라벨'=>'유저 관리','href'=>'/tenant/tenant/user_list.php'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// ② 1뎁스가 없는 화면에서 노출할 "기본 2뎁스"
|
||||
$NAV_ROOT = [
|
||||
'title' => '기본',
|
||||
'items' => [
|
||||
['라벨'=>'인트로','href'=>'/tenant/member/intro.php'],
|
||||
['라벨'=>'대시보드','href'=>'/tenant/member/dashboard.php'],
|
||||
],
|
||||
];
|
||||
|
||||
// ③ 현재 경로/섹션 파악
|
||||
$currentPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
|
||||
$hasSection = isset($CURRENT_SECTION, $NAV[$CURRENT_SECTION]);
|
||||
|
||||
// ④ JS/초기 렌더에 사용할 "초기 섹션" (없으면 root 사용)
|
||||
$INITIAL_SECTION = $hasSection ? $CURRENT_SECTION : 'root';
|
||||
|
||||
// ⑤ JS로 넘길 NAV(+root)
|
||||
$NAV_JS = $NAV;
|
||||
$NAV_JS['root'] = $NAV_ROOT;
|
||||
?>
|
||||
|
||||
|
||||
<div id="navArea"><!-- 마우스가 이 영역(1뎁스+2뎁스)을 떠나면 복귀 -->
|
||||
|
||||
<!-- 1뎁스 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color:#2c4a85;">
|
||||
<div class="container" style="max-width:1280px;">
|
||||
<a class="navbar-brand" href="/tenant/">(주)경동기업</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="mainNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<?php foreach ($NAV as $key => $conf): // root는 1뎁스에 출력하지 않음 ?>
|
||||
<li class="nav-item">
|
||||
<a href="#"
|
||||
class="nav-link top-link <?= ($hasSection && $CURRENT_SECTION === $key) ? 'active' : '' ?>"
|
||||
data-section="<?= $key ?>">
|
||||
<?= htmlspecialchars($conf['title']) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<!-- 우측 로그인/로그아웃 영역은 그대로 -->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 2뎁스 -->
|
||||
<div class="subnav-wrapper" style="background:#e9f0fb;border-bottom:1px solid #d5deef;">
|
||||
<div class="container" style="max-width:1280px;">
|
||||
<ul id="subnav" class="subnav list-unstyled d-flex flex-wrap gap-3 m-0 py-2">
|
||||
<?php
|
||||
$INIT_ITEMS = ($INITIAL_SECTION === 'root') ? $NAV_ROOT['items'] : $NAV[$INITIAL_SECTION]['items'];
|
||||
foreach ($INIT_ITEMS as $item):
|
||||
$activeClass = ($currentPath === $item['href']) ? 'active' : '';
|
||||
?>
|
||||
<li><a class="subnav-link <?= $activeClass ?>" href="<?= $item['href'] ?>"><?= htmlspecialchars($item['라벨']) ?></a></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /#navArea -->
|
||||
|
||||
<style>
|
||||
.navbar .nav-link.active{background:rgba(255,255,255,.15);border-radius:.375rem}
|
||||
.navbar .top-link{cursor:default}
|
||||
.navbar .top-link:hover{background:rgba(255,255,255,.10);border-radius:.375rem}
|
||||
.subnav-link{display:inline-block;padding:.35rem .6rem;border-radius:.35rem;color:#2c3e67;text-decoration:none}
|
||||
.subnav-link:hover{background:#dbe7ff;text-decoration:none}
|
||||
.subnav-link.active{font-weight:700;text-decoration:underline}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
const NAV = <?= json_encode($NAV_JS, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) ?>;
|
||||
const INITIAL_SECTION = '<?= $INITIAL_SECTION ?>'; // 'root' 또는 실제 섹션 키
|
||||
const $subnav = $('#subnav');
|
||||
|
||||
const esc = s => String(s)
|
||||
.replace(/&/g,'&').replace(/</g,'<')
|
||||
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||
|
||||
// 현재 섹션 렌더
|
||||
function renderSection(sectionKey){
|
||||
if (!NAV[sectionKey]) return;
|
||||
// 1뎁스 active 표시 (root일 땐 모두 해제)
|
||||
$('.top-link').removeClass('active');
|
||||
if (sectionKey !== 'root') {
|
||||
$(`.top-link[data-section="${sectionKey}"]`).addClass('active');
|
||||
}
|
||||
// 2뎁스 갱신
|
||||
const path = location.pathname.replace(/\/+$/,'');
|
||||
const items = NAV[sectionKey].items || [];
|
||||
const html = items.map(it=>{
|
||||
const href = String(it.href||'');
|
||||
const isActive = (href.replace(/\/+$/,'') === path);
|
||||
return `<li><a class="subnav-link ${isActive?'active':''}" href="${esc(href)}">${esc(it['라벨'])}</a></li>`;
|
||||
}).join('');
|
||||
$subnav.html(html);
|
||||
}
|
||||
|
||||
// 1뎁스: 마우스오버/포커스 → 2뎁스만 교체 (이동 없음)
|
||||
$(document).on('mouseenter', '.top-link', function(){
|
||||
renderSection($(this).data('section'));
|
||||
});
|
||||
$(document).on('focusin', '.top-link', function(){
|
||||
renderSection($(this).data('section'));
|
||||
});
|
||||
|
||||
// 네비 영역(1뎁스+2뎁스)을 벗어나면 원래 섹션으로 복귀
|
||||
$('#navArea').on('mouseleave', function(){
|
||||
renderSection(INITIAL_SECTION);
|
||||
});
|
||||
|
||||
// 필요 시 최초 렌더 보정 (서버에서 이미 렌더했지만 JS도 상태 기억)
|
||||
renderSection(INITIAL_SECTION);
|
||||
});
|
||||
</script>
|
||||
13
public/tenant/index.php
Normal file
13
public/tenant/index.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
// 로그인 안 한 경우 로그인 페이지로
|
||||
header("Location: ./member/intro.php");
|
||||
exit;
|
||||
} else {
|
||||
// 로그인 한 경우 대시보드로
|
||||
header("Location: ./member/dashboard.php");
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
8
public/tenant/inventory/defective.php
Normal file
8
public/tenant/inventory/defective.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='inventory';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/inventory/stock.php
Normal file
8
public/tenant/inventory/stock.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='inventory';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/material/inspection_list.php
Normal file
8
public/tenant/material/inspection_list.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='inventory';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/material/inventory_status.php
Normal file
8
public/tenant/material/inventory_status.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='inventory';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
507
public/tenant/material/list.php
Normal file
507
public/tenant/material/list.php
Normal file
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
// 자재관리 > 리스트
|
||||
$CURRENT_SECTION='item';;
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
|
||||
<!-- 상단 탭 -->
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><a class="nav-link active" href="/tenant/material/list.php">자재 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/product/model_list.php">모델 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/product/bom_editor.php">BOM 관리</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- 툴바 -->
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="text-muted">분류</label>
|
||||
<select class="form-select form-select-sm" id="filterCategory" style="width:160px;">
|
||||
<option value="">전체</option>
|
||||
<option value="자재">자재</option>
|
||||
<option value="부품">부품</option>
|
||||
<option value="소재">소재</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="max-width:340px;">
|
||||
<input type="text" class="form-control form-control-sm" id="keyword" placeholder="품목명/제조사/코드 검색">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="btnSearch">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="text-muted">표시</label>
|
||||
<select id="pageSize" class="form-select form-select-sm" style="width:80px;">
|
||||
<option>10</option><option selected>20</option><option>30</option><option>50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto small text-muted" id="totalInfo">총 0건</div>
|
||||
<button class="btn btn-primary btn-sm" id="btnOpenCreate">신규등록</button>
|
||||
</div>
|
||||
|
||||
<!-- 리스트 -->
|
||||
<div class="table-responsive border rounded">
|
||||
<table class="table table-hover table-striped align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:70px;">NO</th>
|
||||
<th style="width:120px;">분류</th>
|
||||
<th>품목명</th>
|
||||
<th style="width:90px;">단위</th>
|
||||
<th style="width:140px;">제조사</th>
|
||||
<th style="width:120px;">이미지</th>
|
||||
<th style="width:140px;" class="text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="listBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 페이징 -->
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
<nav id="pagerNav" aria-label="Page navigation"></nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록/수정 모달 -->
|
||||
<div class="modal fade" id="materialModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<form class="modal-content needs-validation" id="materialForm" novalidate>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="materialModalTitle">자재 등록</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||||
</div>
|
||||
|
||||
<!-- === 모달 본문 (동적 규격/기타 + 품목명 자동생성) === -->
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="matId">
|
||||
|
||||
<!-- 기본정보 -->
|
||||
<details open class="mb-3">
|
||||
<summary class="fw-semibold">기본정보 입력</summary>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">분류 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="matCategory" required>
|
||||
<option value="">선택</option>
|
||||
<option>자재</option>
|
||||
<option>부품</option>
|
||||
<option>소재</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">분류를 선택해 주세요.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">자재명(베이스) <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="matBaseName" placeholder="예: 절곡판" required>
|
||||
<div class="invalid-feedback">자재명을 입력해 주세요.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">단위</label>
|
||||
<input type="text" class="form-control" id="matUnit" placeholder="EA, mm, kg …">
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">품목명(자동생성)</label>
|
||||
<input type="text" class="form-control" id="matName" placeholder="자재명 + 규격값/단위" readonly>
|
||||
<div class="form-text">규격을 추가하면 자동으로 갱신됩니다. (예) 절곡판 1.5t 20mm</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 규격정보 -->
|
||||
<details open class="mb-3">
|
||||
<summary class="fw-semibold d-flex align-items-center">
|
||||
규격정보 입력
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="btnAddSpec">+ 추가</button>
|
||||
</summary>
|
||||
<div id="specRows" class="mt-2"></div>
|
||||
</details>
|
||||
|
||||
<!-- 기타정보 -->
|
||||
<details class="mb-3">
|
||||
<summary class="fw-semibold d-flex align-items-center">
|
||||
기타정보 입력
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="btnAddExtra">+ 추가</button>
|
||||
</summary>
|
||||
<div id="extraRows" class="mt-2"></div>
|
||||
</details>
|
||||
|
||||
<!-- 부가 입력 -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">제조사</label>
|
||||
<input type="text" class="form-control" id="matMaker">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">품목코드</label>
|
||||
<input type="text" class="form-control" id="matCode">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">안전재고</label>
|
||||
<input type="number" class="form-control" id="matSafety" min="0" step="1" value="0">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">이미지 업로드</label>
|
||||
<input class="form-control" type="file" id="matImages" accept="image/*" multiple>
|
||||
<div class="form-text">여러 장 업로드 가능 (프로토타입: 미리보기만)</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">비고</label>
|
||||
<textarea class="form-control" id="matMemo" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex gap-2 flex-wrap" id="previewWrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모달 푸터 -->
|
||||
<div class="modal-footer">
|
||||
<div class="me-auto small text-muted" id="modalHint"></div>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSubmit">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 이미지 라이트박스 -->
|
||||
<div class="modal fade" id="imgModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content bg-dark">
|
||||
<div class="modal-body p-0">
|
||||
<img id="imgModalImg" src="" alt="preview" class="w-100" style="display:block;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 토스트 -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:1080">
|
||||
<div id="snack" class="toast align-items-center text-bg-primary border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="snackMsg">Saved</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.thumb{width:56px;height:40px;object-fit:cover;border-radius:4px;cursor:zoom-in;border:1px solid rgba(0,0,0,.06)}
|
||||
.table > :not(caption) > * > * { vertical-align: middle; }
|
||||
details > summary { cursor: pointer; }
|
||||
</style>
|
||||
|
||||
<!-- ===== 페이지 스크립트 (jQuery 불필요) ===== -->
|
||||
<script>
|
||||
(function(){
|
||||
// ---------- 샘플 데이터 ----------
|
||||
function buildSampleData(count=173){
|
||||
const cats=['자재','부품','소재'], makers=['KG스틸','대한','경동기업','한빛','에이스','대림'], units=['EA','M','KG','매','BOX'];
|
||||
return Array.from({length:count}, (_,i)=>({
|
||||
id: i+1,
|
||||
category: cats[(i+1)%cats.length],
|
||||
name: `${cats[(i+1)%cats.length]} 샘플 품목 #${i+1} (${['A','B','C','D'][i%4]})`,
|
||||
base_name: '',
|
||||
unit: units[i%units.length],
|
||||
maker: makers[i%makers.length],
|
||||
code: `MAT-${String(i+1).padStart(5,'0')}`,
|
||||
safety: (i%7===0)?50:0,
|
||||
images: (i%5===0)?[`https://picsum.photos/seed/m${i+1}/120/80`]:[],
|
||||
specs:[], extras:[]
|
||||
}));
|
||||
}
|
||||
let MATERIALS = buildSampleData();
|
||||
|
||||
// ---------- 상태 ----------
|
||||
let PAGE = 1, SIZE = 20;
|
||||
|
||||
// ---------- 헬퍼 ----------
|
||||
const $ = sel => document.querySelector(sel);
|
||||
const $$ = sel => Array.from(document.querySelectorAll(sel));
|
||||
|
||||
function getFiltered(){
|
||||
const cat = ($('#filterCategory')?.value || '').trim();
|
||||
const kw = ($('#keyword')?.value || '').trim().toLowerCase();
|
||||
return MATERIALS.filter(m=>{
|
||||
const okCat = !cat || m.category===cat;
|
||||
const hay = (m.name+' '+(m.maker||'')+' '+(m.code||'')).toLowerCase();
|
||||
const okKw = !kw || hay.includes(kw);
|
||||
return okCat && okKw;
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- 렌더 ----------
|
||||
function renderList(){
|
||||
const data = getFiltered();
|
||||
const total = data.length;
|
||||
const pages = Math.max(1, Math.ceil(total / SIZE));
|
||||
if (PAGE>pages) PAGE=pages;
|
||||
|
||||
const start=(PAGE-1)*SIZE, end=start+SIZE;
|
||||
const pageData = data.slice(start,end);
|
||||
|
||||
const body = $('#listBody');
|
||||
body.innerHTML = pageData.map((m,i)=>`
|
||||
<tr data-id="${m.id}">
|
||||
<td>${start+i+1}</td>
|
||||
<td>${m.category||''}</td>
|
||||
<td>${m.name||''}</td>
|
||||
<td>${m.unit||''}</td>
|
||||
<td>${m.maker||''}</td>
|
||||
<td>${
|
||||
(m.images||[]).slice(0,2).map(src=>`<img src="${src}" class="thumb me-1" data-src="${src}">`).join('') +
|
||||
((m.images&&m.images.length>2)? `<span class="text-muted small">+${m.images.length-2}</span>`:'')
|
||||
}</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-outline-primary me-1 btnEdit">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btnDel">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || `<tr><td colspan="7" class="text-center text-muted py-4">데이터가 없습니다.</td></tr>`;
|
||||
|
||||
// 총건수
|
||||
$('#totalInfo').textContent = `총 ${total.toLocaleString()}건 · ${total? (start+1):0}-${Math.min(end,total)} 표시`;
|
||||
|
||||
renderPager(pages);
|
||||
}
|
||||
|
||||
function renderPager(pages){
|
||||
const nav = $('#pagerNav');
|
||||
const win=7;
|
||||
let sp=Math.max(1, PAGE-Math.floor(win/2));
|
||||
let ep=Math.min(pages, sp+win-1);
|
||||
sp=Math.max(1, ep-win+1);
|
||||
|
||||
let html = `<ul class="pagination pagination-sm m-0">`;
|
||||
html += `<li class="page-item ${PAGE===1?'disabled':''}">
|
||||
<a class="page-link" href="#" data-go="first" aria-label="First">«</a></li>`;
|
||||
html += `<li class="page-item ${PAGE===1?'disabled':''}">
|
||||
<a class="page-link" href="#" data-go="${PAGE-1}" aria-label="Prev">‹</a></li>`;
|
||||
for(let p=sp;p<=ep;p++){
|
||||
html += `<li class="page-item ${p===PAGE?'active':''}">
|
||||
<a class="page-link" href="#" data-go="${p}">${p}</a></li>`;
|
||||
}
|
||||
html += `<li class="page-item ${PAGE===pages?'disabled':''}">
|
||||
<a class="page-link" href="#" data-go="${PAGE+1}" aria-label="Next">›</a></li>`;
|
||||
html += `<li class="page-item ${PAGE===pages?'disabled':''}">
|
||||
<a class="page-link" href="#" data-go="last" aria-label="Last">»</a></li>`;
|
||||
html += `</ul>`;
|
||||
nav.innerHTML = html;
|
||||
}
|
||||
|
||||
// ---------- 규격/기타 동적행 + 품목명 자동생성 ----------
|
||||
const UNIT_OPTIONS = ['EA','mm','cm','m','kg','t','L'];
|
||||
function specRowTpl(data={label:'규격', value:'', unit:''}) {
|
||||
const uid='spec_'+Math.random().toString(36).slice(2,8);
|
||||
const opts=UNIT_OPTIONS.map(u=>`<option ${data.unit===u?'selected':''}>${u}</option>`).join('');
|
||||
return `
|
||||
<div class="row g-2 align-items-center border rounded p-2 mb-2" data-type="spec" id="${uid}">
|
||||
<div class="col-auto"><button type="button" class="btn btn-sm btn-outline-secondary btnDelRow">-</button></div>
|
||||
<div class="col-md-2"><label class="form-label m-0 small">규격</label>
|
||||
<input type="text" class="form-control form-control-sm spec-label" value="${data.label||''}" placeholder="예: 두께"></div>
|
||||
<div class="col-md-4"><label class="form-label m-0 small">값</label>
|
||||
<input type="text" class="form-control form-control-sm spec-value" value="${data.value||''}" placeholder="예: 1.5"></div>
|
||||
<div class="col-md-2"><label class="form-label m-0 small">단위</label>
|
||||
<select class="form-select form-select-sm spec-unit"><option value="">선택</option>${opts}</select></div>
|
||||
</div>`;
|
||||
}
|
||||
function extraRowTpl(data={key:'', value:''}) {
|
||||
const uid='extra_'+Math.random().toString(36).slice(2,8);
|
||||
return `
|
||||
<div class="row g-2 align-items-center border rounded p-2 mb-2" data-type="extra" id="${uid}">
|
||||
<div class="col-auto"><button type="button" class="btn btn-sm btn-outline-secondary btnDelRow">-</button></div>
|
||||
<div class="col-md-3"><label class="form-label m-0 small">항목</label>
|
||||
<input type="text" class="form-control form-control-sm extra-key" value="${data.key||''}" placeholder="예: 색상"></div>
|
||||
<div class="col-md-6"><label class="form-label m-0 small">값</label>
|
||||
<input type="text" class="form-control form-control-sm extra-value" value="${data.value||''}" placeholder="예: 블루"></div>
|
||||
</div>`;
|
||||
}
|
||||
function addSpecRow(data){ $('#specRows').insertAdjacentHTML('beforeend', specRowTpl(data)); wireSpecEvents(); composeName(); }
|
||||
function addExtraRow(data){ $('#extraRows').insertAdjacentHTML('beforeend', extraRowTpl(data)); }
|
||||
function wireSpecEvents(){
|
||||
$$('#specRows .spec-value, #specRows .spec-unit, #matBaseName').forEach(el=>{
|
||||
el.oninput = composeName; el.onchange = composeName;
|
||||
});
|
||||
}
|
||||
function composeName(){
|
||||
const base = ($('#matBaseName')?.value||'').trim();
|
||||
const parts=[];
|
||||
$$('#specRows [data-type="spec"]').forEach(row=>{
|
||||
const v=row.querySelector('.spec-value')?.value.trim();
|
||||
const u=row.querySelector('.spec-unit')?.value.trim();
|
||||
if (v) parts.push(u?`${v}${u}`:v);
|
||||
});
|
||||
$('#matName').value = [base, ...parts].filter(Boolean).join(' ');
|
||||
}
|
||||
document.addEventListener('click', e=>{
|
||||
if (e.target.id==='btnAddSpec') addSpecRow({});
|
||||
if (e.target.id==='btnAddExtra') addExtraRow({});
|
||||
if (e.target.classList.contains('btnDelRow')){
|
||||
e.preventDefault();
|
||||
const row=e.target.closest('.row'); row?.parentNode?.removeChild(row);
|
||||
composeName();
|
||||
}
|
||||
});
|
||||
|
||||
function resetDynamicRows(specs=[], extras=[]){
|
||||
$('#specRows').innerHTML=''; $('#extraRows').innerHTML='';
|
||||
if (!specs.length) specs=[{}];
|
||||
specs.forEach(s=> addSpecRow(s));
|
||||
extras.forEach(x=> addExtraRow(x));
|
||||
composeName();
|
||||
}
|
||||
|
||||
// ---------- 모달/토스트 ----------
|
||||
let materialModal, imgModal, snackToast;
|
||||
function ensureBootstrapInstances(){
|
||||
// 부트스트랩 JS가 있다면 인스턴스 생성
|
||||
if (window.bootstrap){
|
||||
materialModal = materialModal || new bootstrap.Modal('#materialModal');
|
||||
imgModal = imgModal || new bootstrap.Modal('#imgModal');
|
||||
snackToast = snackToast || new bootstrap.Toast('#snack');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 이벤트 바인딩 ----------
|
||||
// 신규등록
|
||||
document.getElementById('btnOpenCreate').addEventListener('click', ()=>{
|
||||
ensureBootstrapInstances();
|
||||
$('#materialModalTitle').textContent='자재 등록';
|
||||
$('#btnSubmit').textContent='저장';
|
||||
$('#materialForm').reset();
|
||||
$('#matId').value='';
|
||||
$('#previewWrap').innerHTML='';
|
||||
$('#modalHint').textContent='※ 필수 입력: 분류, 자재명(베이스)';
|
||||
resetDynamicRows();
|
||||
materialModal ? materialModal.show() : document.getElementById('materialModal').classList.add('show');
|
||||
});
|
||||
|
||||
// 이미지 미리보기
|
||||
document.getElementById('matImages').addEventListener('change', function(){
|
||||
const wrap=$('#previewWrap'); wrap.innerHTML='';
|
||||
Array.from(this.files).forEach(f=>{
|
||||
const url=URL.createObjectURL(f);
|
||||
wrap.insertAdjacentHTML('beforeend', `<img src="${url}" class="thumb">`);
|
||||
});
|
||||
});
|
||||
|
||||
// 저장(등록/수정)
|
||||
document.getElementById('materialForm').addEventListener('submit', function(e){
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
this.classList.add('was-validated');
|
||||
if (!this.checkValidity()) return;
|
||||
|
||||
const specs = $$('#specRows [data-type="spec"]').map(row=>({
|
||||
label: row.querySelector('.spec-label')?.value.trim() || '규격',
|
||||
value: row.querySelector('.spec-value')?.value.trim() || '',
|
||||
unit : row.querySelector('.spec-unit')?.value.trim() || ''
|
||||
}));
|
||||
const extras = $$('#extraRows [data-type="extra"]').map(row=>({
|
||||
key: row.querySelector('.extra-key')?.value.trim() || '',
|
||||
value: row.querySelector('.extra-value')?.value.trim() || ''
|
||||
}));
|
||||
|
||||
const dto = {
|
||||
id: $('#matId').value ? parseInt($('#matId').value,10) : null,
|
||||
category: $('#matCategory').value,
|
||||
base_name: $('#matBaseName').value.trim(),
|
||||
name: $('#matName').value.trim(),
|
||||
unit: $('#matUnit').value.trim(),
|
||||
maker: $('#matMaker').value.trim(),
|
||||
code: $('#matCode').value.trim(),
|
||||
safety: parseInt($('#matSafety').value,10) || 0,
|
||||
memo: $('#matMemo').value.trim(),
|
||||
images: [],
|
||||
specs, extras
|
||||
};
|
||||
const files = $('#matImages').files; for (const f of files) dto.images.push(URL.createObjectURL(f));
|
||||
|
||||
if (dto.id){
|
||||
const idx = MATERIALS.findIndex(m=>m.id===dto.id);
|
||||
if (idx>=0) MATERIALS[idx] = {...MATERIALS[idx], ...dto};
|
||||
if (snackToast){ $('#snackMsg').textContent='수정되었습니다.'; snackToast.show(); }
|
||||
}else{
|
||||
dto.id = (MATERIALS.reduce((mx,m)=>Math.max(mx,m.id),0)||0)+1;
|
||||
MATERIALS.unshift(dto);
|
||||
if (snackToast){ $('#snackMsg').textContent='등록되었습니다.'; snackToast.show(); }
|
||||
}
|
||||
materialModal?.hide();
|
||||
PAGE=1; renderList();
|
||||
});
|
||||
|
||||
// 수정/삭제
|
||||
document.addEventListener('click', e=>{
|
||||
if (e.target.classList.contains('btnEdit')){
|
||||
ensureBootstrapInstances();
|
||||
const id = parseInt(e.target.closest('tr').dataset.id,10);
|
||||
const m = MATERIALS.find(x=>x.id===id); if(!m) return;
|
||||
|
||||
$('#materialModalTitle').textContent='자재 수정';
|
||||
$('#btnSubmit').textContent='수정';
|
||||
|
||||
$('#matId').value=m.id;
|
||||
$('#matCategory').value=m.category||'';
|
||||
$('#matBaseName').value=m.base_name || (m.name||'').split(' ')[0] || '';
|
||||
$('#matUnit').value=m.unit||'';
|
||||
$('#matName').value=m.name||'';
|
||||
$('#matMaker').value=m.maker||'';
|
||||
$('#matCode').value=m.code||'';
|
||||
$('#matSafety').value=m.safety||0;
|
||||
$('#matMemo').value=m.memo||'';
|
||||
$('#matImages').value='';
|
||||
const wrap=$('#previewWrap'); wrap.innerHTML='';
|
||||
(m.images||[]).forEach(src=> wrap.insertAdjacentHTML('beforeend', `<img src="${src}" class="thumb">`));
|
||||
|
||||
resetDynamicRows(m.specs||[], m.extras||[]);
|
||||
materialModal ? materialModal.show() : document.getElementById('materialModal').classList.add('show');
|
||||
}
|
||||
if (e.target.classList.contains('btnDel')){
|
||||
const id = parseInt(e.target.closest('tr').dataset.id,10);
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
MATERIALS = MATERIALS.filter(m=>m.id!==id);
|
||||
if (snackToast){ $('#snackMsg').textContent='삭제되었습니다.'; snackToast.show(); }
|
||||
renderList();
|
||||
}
|
||||
if (e.target.classList.contains('thumb')){
|
||||
ensureBootstrapInstances();
|
||||
const src = e.target.dataset.src || e.target.getAttribute('src');
|
||||
$('#imgModalImg').setAttribute('src', src);
|
||||
imgModal ? imgModal.show() : document.getElementById('imgModal').classList.add('show');
|
||||
}
|
||||
});
|
||||
|
||||
// 검색/필터/페이지크기
|
||||
document.getElementById('btnSearch').addEventListener('click', ()=>{ PAGE=1; renderList(); });
|
||||
document.getElementById('keyword').addEventListener('keydown', e=>{ if(e.key==='Enter'){ PAGE=1; renderList(); }});
|
||||
document.getElementById('filterCategory').addEventListener('change', ()=>{ PAGE=1; renderList(); });
|
||||
document.getElementById('pageSize').addEventListener('change', function(){ SIZE=parseInt(this.value,10)||20; PAGE=1; renderList(); });
|
||||
|
||||
// 페이징 클릭
|
||||
document.addEventListener('click', (e)=>{
|
||||
const a = e.target.closest('#pagerNav a'); if (!a) return;
|
||||
e.preventDefault();
|
||||
const pages = Math.max(1, Math.ceil(getFiltered().length / SIZE));
|
||||
const go = a.dataset.go;
|
||||
if (go==='first') PAGE=1;
|
||||
else if (go==='last') PAGE=pages;
|
||||
else PAGE = Math.min(Math.max(parseInt(go,10)||1,1), pages);
|
||||
renderList();
|
||||
});
|
||||
|
||||
// 최초 렌더
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
try{ ensureBootstrapInstances(); }catch(_){}
|
||||
const ps=document.getElementById('pageSize'); if(ps) SIZE=parseInt(ps.value,10)||20;
|
||||
renderList();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS (모달 동작 보장용; 이미 로드되어 있어도 괜찮습니다) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
279
public/tenant/member/dashboard.php
Normal file
279
public/tenant/member/dashboard.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php include '../inc/header.php'; ?>
|
||||
<style>
|
||||
.card {
|
||||
min-height: 300px; /* 필요에 따라 220~320 조절 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="row g-3 align-items-stretch">
|
||||
<!-- 1줄 -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Revenue BY MONTH</h6>
|
||||
<div id="revenueByMonthChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-6">
|
||||
<div class="card p-3">
|
||||
<h6>Revenue BY REGION</h6>
|
||||
<div id="revenueByRegionChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2줄 -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Debt to Equity</h6>
|
||||
<div id="debtEquityChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Working Capital</h6>
|
||||
<div id="workingCapitalChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Return on Equity</h6>
|
||||
<div id="returnOnEquityChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3 text-center">
|
||||
<h6>Average Purchase Value YTD</h6>
|
||||
<div style="font-size: 1.5rem; font-weight: bold;">$8,301</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3줄 -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3 text-center">
|
||||
<h6>Leads (Last 30 Days)</h6>
|
||||
<div style="font-size: 1.5rem; font-weight: bold; color: red;">768</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Top Customers</h6>
|
||||
<table class="table table-sm mb-0">
|
||||
<thead>
|
||||
<tr><th>Client</th><th>Revenue</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Alfreds </td><td>$330,221</td></tr>
|
||||
<tr><td>Berglunds </td><td>$299,112</td></tr>
|
||||
<tr><td>Centro </td><td>$200,349</td></tr>
|
||||
<tr><td>Ernst </td><td>$199,312</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Sales by Product Category</h6>
|
||||
<div id="salesPieChart" style="height: 220px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4줄 -->
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Budget</h6>
|
||||
<div id="budgetRadarChart" style="height: 260px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<h6>Website Conversion Funnel</h6>
|
||||
<div id="conversionFunnelChart" style="height: 260px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mb-4">
|
||||
<div class="card p-3" style="min-height:540px;">
|
||||
<h6>일정 관리 (Calendar)</h6>
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
// 1. Revenue By Month (Column)
|
||||
Highcharts.chart('revenueByMonthChart', {
|
||||
chart: { type: 'column' },
|
||||
title: { text: null },
|
||||
xAxis: { categories: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] },
|
||||
yAxis: { title: { text: 'Revenue ($M)' } },
|
||||
series: [
|
||||
{ name: 'Last Year', data: [10,12,14,16,18,20,21,19,17,14,12,8], color:'#2c4a85' },
|
||||
{ name: 'Current Year', data: [11,13,15,18,20,22,23,20,18,16,13,10], color:'#517fc4' }
|
||||
]
|
||||
});
|
||||
|
||||
// 2. Revenue By Region (Map)
|
||||
Highcharts.mapChart('revenueByRegionChart', {
|
||||
chart: { map: 'custom/world' },
|
||||
title: { text: null },
|
||||
series: [{
|
||||
data: [
|
||||
['kr', 45], ['us', 28], ['de', 15], ['jp', 12], ['cn', 20], ['fr', 14], ['gb', 13]
|
||||
],
|
||||
name: 'Revenue',
|
||||
states: { hover: { color: '#BADA55' } },
|
||||
dataLabels: { enabled: true, format: '{point.name}' }
|
||||
}]
|
||||
});
|
||||
|
||||
// 3. Debt to Equity (Column)
|
||||
Highcharts.chart('debtEquityChart', {
|
||||
chart: { type: 'column' },
|
||||
title: { text: null },
|
||||
xAxis: { categories: ['Y-4','Y-3','Y-2','Y-1','Y0'] },
|
||||
yAxis: { title: { text: 'Debt to Equity Ratio' } },
|
||||
series: [
|
||||
{ name: 'Debt', data: [20,30,40,35,25], color:'#2c4a85' },
|
||||
{ name: 'Equity', data: [40,45,50,55,60], color:'#517fc4' },
|
||||
{ name: 'Debt-to-Equity', data: [50,60,70,65,55], color:'#7a99d9' }
|
||||
]
|
||||
});
|
||||
|
||||
// 4. Working Capital (Column)
|
||||
Highcharts.chart('workingCapitalChart', {
|
||||
chart: { type: 'column' },
|
||||
title: { text: null },
|
||||
xAxis: { categories: ['Mar','Apr','May','Jun','Jul','Aug','Sep'] },
|
||||
yAxis: { title: { text: 'Working Capital' } },
|
||||
series: [
|
||||
{ name: 'Cash', data: [10,9,11,12,10,8,9], color:'#2c4a85' },
|
||||
{ name: 'Investments', data: [7,8,6,7,6,7,8], color:'#517fc4' },
|
||||
{ name: 'A/R', data: [4,3,4,5,4,5,6], color:'#7a99d9' }
|
||||
]
|
||||
});
|
||||
|
||||
// 5. Return on Equity (Line)
|
||||
Highcharts.chart('returnOnEquityChart', {
|
||||
chart: { type: 'line' },
|
||||
title: { text: null },
|
||||
xAxis: { categories: ['Y-4','Y-3','Y-2','Y-1','Y0'] },
|
||||
yAxis: { title: { text: 'Return on Equity (%)' } },
|
||||
series: [
|
||||
{ name: 'ROE', data: [10,12,13,15,14], color:'#2c4a85' }
|
||||
]
|
||||
});
|
||||
|
||||
// 6. Sales Pie Chart
|
||||
Highcharts.chart('salesPieChart', {
|
||||
chart: { type: 'pie' },
|
||||
title: { text: null },
|
||||
series: [{
|
||||
name: 'Sales',
|
||||
colorByPoint: true,
|
||||
data: [
|
||||
{ name: 'Scientific Equipment', y: 45 },
|
||||
{ name: 'IT Services', y: 15 },
|
||||
{ name: 'Financial Services', y: 10 },
|
||||
{ name: 'Consumer Electronics', y: 10 },
|
||||
{ name: 'Workwear', y: 10 },
|
||||
{ name: 'Cosmetics', y: 10 }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
// 7. Budget Radar (Polar Area)
|
||||
Highcharts.chart('budgetRadarChart', {
|
||||
chart: { polar: true, type: 'area' },
|
||||
title: { text: null },
|
||||
pane: { size: '80%' },
|
||||
xAxis: {
|
||||
categories: ['Admin','Marketing','Develop','Customer','IT'],
|
||||
tickmarkPlacement: 'on',
|
||||
lineWidth: 0
|
||||
},
|
||||
yAxis: {
|
||||
gridLineInterpolation: 'polygon',
|
||||
lineWidth: 0,
|
||||
min: 0
|
||||
},
|
||||
series: [
|
||||
{ name: 'Allocated Budget', data: [43000,19000,60000,35000,17000], color:'#2c4a85' },
|
||||
{ name: 'Actual Spending', data: [50000,39000,42000,31000,26000], color:'#517fc4' }
|
||||
]
|
||||
});
|
||||
|
||||
// 8. Website Conversion Funnel
|
||||
Highcharts.chart('conversionFunnelChart', {
|
||||
chart: { type: 'funnel' },
|
||||
title: { text: null },
|
||||
plotOptions: {
|
||||
series: {
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
format: '<b>{point.name}</b> ({point.y:,.0f})',
|
||||
color: 'black',
|
||||
softConnector: true
|
||||
},
|
||||
center: ['40%', '50%'],
|
||||
neckWidth: '30%',
|
||||
neckHeight: '25%'
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
name: 'Website Conversion',
|
||||
data: [
|
||||
['Visit', 313000],
|
||||
['Product Page', 230000],
|
||||
['Add to Cart', 146000],
|
||||
['Checkout', 110000],
|
||||
['Sale', 63000]
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
height: 500,
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'ko',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||
},
|
||||
selectable: true,
|
||||
editable: true,
|
||||
events: [
|
||||
// 샘플 일정
|
||||
{ title: '회의', start: '2026-08-14', end: '2026-08-15' },
|
||||
{ title: '업무 마감', start: '2026-08-17' }
|
||||
],
|
||||
select: function(info) {
|
||||
var title = prompt('새 일정을 입력하세요:');
|
||||
if (title) {
|
||||
calendar.addEvent({
|
||||
title: title,
|
||||
start: info.startStr,
|
||||
end: info.endStr
|
||||
});
|
||||
}
|
||||
calendar.unselect();
|
||||
},
|
||||
eventClick: function(info) {
|
||||
if (confirm("'" + info.event.title + "' 일정을 삭제할까요?")) {
|
||||
info.event.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
47
public/tenant/member/intro.php
Normal file
47
public/tenant/member/intro.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:800px; margin-top:60px; margin-bottom:60px;">
|
||||
<div class="text-center mb-5">
|
||||
<img src="https://img.icons8.com/color/96/000000/robot-2.png" alt="SAM 로고" style="width:80px;">
|
||||
<h1 class="fw-bold mt-3 mb-2" style="color:#2c4a85;">SAM</h1>
|
||||
<h5 class="mb-3" style="color:#517fc4;">Smart Automation Management</h5>
|
||||
<p class="lead" style="color:#333;">
|
||||
쉽고, 빠르고, 똑똑하게<br>
|
||||
<b>자동화된 업무관리와 협업</b>을 경험하세요.
|
||||
</p>
|
||||
</div>
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<img src="https://img.icons8.com/color/48/000000/checked-checkbox.png" alt="">
|
||||
<h6 class="fw-bold mt-2 mb-2">프로세스 자동화</h6>
|
||||
<p class="small text-muted">견적·수주·생산·회계까지<br>모든 업무를 손쉽게 자동화</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<img src="https://img.icons8.com/color/48/000000/group-task.png" alt="">
|
||||
<h6 class="fw-bold mt-2 mb-2">스마트 협업</h6>
|
||||
<p class="small text-muted">실시간 공유/관리로<br>팀워크·생산성 향상</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<img src="https://img.icons8.com/color/48/000000/security-checked.png" alt="">
|
||||
<h6 class="fw-bold mt-2 mb-2">안전한 데이터</h6>
|
||||
<p class="small text-muted">기업별(테넌트) 완벽 분리와<br>보안 우선 설계</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/tenant/member/login.php" class="btn btn-primary btn-lg px-5 fw-bold shadow-sm" style="background:#2c4a85;">로그인/회원가입</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
66
public/tenant/member/login.php
Normal file
66
public/tenant/member/login.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'member';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="height: 70vh;">
|
||||
<div class="card shadow p-4" style="width: 350px;"> <form id="loginForm" style="width: 300px;" method="post" action="/api/login_process.php">
|
||||
<h3 class="text-center mb-4">로그인</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="userid" class="form-label">아이디</label>
|
||||
<input type="text" class="form-control" id="userid" name="userid" placeholder="아이디를 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">비밀번호</label>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="비밀번호를 입력하세요" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">로그인</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#loginForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 간단 밸리데이션 예제
|
||||
const userid = $('#userid').val().trim();
|
||||
const password = $('#password').val().trim();
|
||||
|
||||
if (!userid) {
|
||||
alert('아이디를 입력하세요.');
|
||||
$('#userid').focus();
|
||||
return;
|
||||
}
|
||||
if (!password) {
|
||||
alert('비밀번호를 입력하세요.');
|
||||
$('#password').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// AJAX 로그인 요청 (예시)
|
||||
$.ajax({
|
||||
url: '/tenant/api/login_process.php',
|
||||
type: 'POST',
|
||||
data: { userid: userid, password: password },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
if(response.success) {
|
||||
alert('로그인 성공!');
|
||||
window.location.href = '/tenant/member/dashboard.php';
|
||||
} else {
|
||||
alert('로그인 실패: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('서버 오류가 발생했습니다.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
9
public/tenant/member/logout.php
Normal file
9
public/tenant/member/logout.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_unset(); // 모든 세션 변수 해제
|
||||
session_destroy(); // 세션 파기
|
||||
|
||||
// 로그인 페이지로 이동 (필요시 경로 수정)
|
||||
header("Location: /tenant/member/intro.php");
|
||||
exit;
|
||||
?>
|
||||
66
public/tenant/member/profile_edit.php
Normal file
66
public/tenant/member/profile_edit.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'member';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 샘플: 세션에서 값 읽기 (실제론 DB에서 조회)
|
||||
$userid = isset($_SESSION['userid']) ? $_SESSION['userid'] : 'user';
|
||||
$username = isset($_SESSION['username']) ? $_SESSION['username'] : '홍길동';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 75vh;">
|
||||
<div class="card shadow p-4" style="width: 400px;">
|
||||
<form id="profileEditForm" method="post" action="/tenant/api/profile_edit_process.php" autocomplete="off">
|
||||
<h3 class="text-center mb-4">내 정보 수정</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">아이디</label>
|
||||
<input type="text" class="form-control" name="userid" value="<?=htmlspecialchars($userid)?>" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">이름</label>
|
||||
<input type="text" class="form-control" id="username" name="username" value="<?=htmlspecialchars($username)?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">새 비밀번호</label>
|
||||
<input type="password" class="form-control" id="password" name="password" maxlength="30" placeholder="변경 시 입력">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password2" class="form-label">비밀번호 확인</label>
|
||||
<input type="password" class="form-control" id="password2" name="password2" maxlength="30" placeholder="변경 시 입력">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">수정하기</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#profileEditForm').on('submit', function(e){
|
||||
var username = $('#username').val().trim();
|
||||
var pw1 = $('#password').val();
|
||||
var pw2 = $('#password2').val();
|
||||
|
||||
if (username.length < 2) {
|
||||
alert('이름은 2글자 이상 입력하세요.');
|
||||
$('#username').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1 || pw2) {
|
||||
if (pw1.length < 4) {
|
||||
alert('비밀번호는 4글자 이상이어야 합니다.');
|
||||
$('#password').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1 !== pw2) {
|
||||
alert('비밀번호가 일치하지 않습니다.');
|
||||
$('#password2').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
}
|
||||
// 서버로 전송
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
66
public/tenant/member/register.php
Normal file
66
public/tenant/member/register.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'member';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 75vh;">
|
||||
<div class="card shadow p-4" style="width: 380px;">
|
||||
<form id="registerForm" method="post" action="/tenant/api/register_process.php" autocomplete="off">
|
||||
<h3 class="text-center mb-4">회원가입</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="userid" class="form-label">아이디</label>
|
||||
<input type="text" class="form-control" id="userid" name="userid" maxlength="20" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">이름</label>
|
||||
<input type="text" class="form-control" id="username" name="username" maxlength="20" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">비밀번호</label>
|
||||
<input type="password" class="form-control" id="password" name="password" maxlength="30" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password2" class="form-label">비밀번호 확인</label>
|
||||
<input type="password" class="form-control" id="password2" name="password2" maxlength="30" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">가입하기</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#registerForm').on('submit', function(e){
|
||||
// 클라이언트 밸리데이션
|
||||
var userid = $('#userid').val().trim();
|
||||
var username = $('#username').val().trim();
|
||||
var pw1 = $('#password').val();
|
||||
var pw2 = $('#password2').val();
|
||||
|
||||
if (userid.length < 4) {
|
||||
alert('아이디는 4글자 이상이어야 합니다.');
|
||||
$('#userid').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (username.length < 2) {
|
||||
alert('이름은 2글자 이상 입력하세요.');
|
||||
$('#username').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1.length < 4) {
|
||||
alert('비밀번호는 4글자 이상이어야 합니다.');
|
||||
$('#password').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1 !== pw2) {
|
||||
alert('비밀번호가 일치하지 않습니다.');
|
||||
$('#password2').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
// 서버 전송
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
22
public/tenant/member/tenant_add_process.php
Normal file
22
public/tenant/member/tenant_add_process.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
include '../inc/config.php';
|
||||
|
||||
// 새 테넌트(회사명) 입력값 확인
|
||||
$new_tenant_name = isset($_POST['new_tenant_name']) ? trim($_POST['new_tenant_name']) : '';
|
||||
|
||||
if (!$new_tenant_name) {
|
||||
echo "<script>alert('회사명을 입력하세요.'); history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 실제로는 DB에 등록하고 ID를 받아야 함 (샘플로 랜덤값 부여)
|
||||
$new_tenant_id = rand(1000, 9999); // 실제는 DB의 AUTO_INCREMENT 등 사용
|
||||
|
||||
// 세션에 테넌트 정보 저장
|
||||
$_SESSION['tenant_id'] = $new_tenant_id;
|
||||
$_SESSION['tenant_name'] = $new_tenant_name;
|
||||
|
||||
// 대시보드로 이동
|
||||
header("Location: /tenant/member/dashboard.php");
|
||||
exit;
|
||||
?>
|
||||
38
public/tenant/member/tenant_select.php
Normal file
38
public/tenant/member/tenant_select.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'member';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 2. 샘플 테넌트 목록 (실제로는 DB 등에서 불러옴)
|
||||
$sample_tenants = [
|
||||
['id' => 101, 'name' => '삼성전자'],
|
||||
['id' => 102, 'name' => '카카오'],
|
||||
['id' => 103, 'name' => '네이버'],
|
||||
];
|
||||
|
||||
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width:500px; margin-top:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<h4 class="mb-3 text-center">테넌트(회사/기업) 선택</h4>
|
||||
<form method="post" action="tenant_select_process.php">
|
||||
<div class="mb-3">
|
||||
<label for="tenant_id" class="form-label">기존 테넌트 선택</label>
|
||||
<select class="form-select" id="tenant_id" name="tenant_id" required>
|
||||
<option value="">테넌트 선택...</option>
|
||||
<?php foreach($sample_tenants as $tenant): ?>
|
||||
<option value="<?=$tenant['id']?>"><?=$tenant['name']?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mb-3">선택</button>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">새 테넌트(회사/기업) 추가</label><br>
|
||||
<a href="/tenant/tenant/join.php" class="btn btn-success w-100">추가</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
18
public/tenant/member/tenant_select_process.php
Normal file
18
public/tenant/member/tenant_select_process.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
include '../inc/config.php';
|
||||
|
||||
// POST로 받은 테넌트 ID 확인
|
||||
$tenant_id = isset($_POST['tenant_id']) ? intval($_POST['tenant_id']) : 0;
|
||||
|
||||
if (!$tenant_id) {
|
||||
echo "<script>alert('테넌트를 선택하세요.'); history.back();</script>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// 세션에 테넌트 ID 저장
|
||||
$_SESSION['tenant_id'] = $tenant_id;
|
||||
|
||||
// 대시보드로 이동
|
||||
header("Location: /tenant/member/dashboard.php");
|
||||
exit;
|
||||
?>
|
||||
77
public/tenant/member/withdraw.php
Normal file
77
public/tenant/member/withdraw.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'member';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="card shadow p-4" style="width: 380px;">
|
||||
<form id="withdrawForm" method="post" action="/tenant/member/logout.php" autocomplete="off">
|
||||
<h3 class="text-center mb-4 text-danger">회원 탈퇴</h3>
|
||||
<p class="text-center mb-3">
|
||||
정말로 회원 탈퇴를 진행하시겠습니까?<br>
|
||||
<span class="text-danger fw-bold">탈퇴 시 모든 정보가 삭제됩니다.</span>
|
||||
</p>
|
||||
<div class="mb-3">
|
||||
<label for="reason" class="form-label">탈퇴 사유</label>
|
||||
<select class="form-select" id="reason" name="reason" required>
|
||||
<option value="">사유를 선택하세요</option>
|
||||
<option value="서비스 불만족">서비스 불만족</option>
|
||||
<option value="사용 빈도 낮음">사용 빈도 낮음</option>
|
||||
<option value="개인정보 우려">개인정보 우려</option>
|
||||
<option value="기타">기타 (직접 입력)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3" id="reasonEtcDiv" style="display:none;">
|
||||
<input type="text" class="form-control" id="reason_etc" name="reason_etc" maxlength="100" placeholder="탈퇴 사유를 입력하세요">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">비밀번호 확인</label>
|
||||
<input type="password" class="form-control" id="password" name="password" maxlength="30" required placeholder="비밀번호를 입력하세요">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-danger w-50">탈퇴</button>
|
||||
<a href="/tenant/member/dashboard.php" class="btn btn-secondary w-50">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// 탈퇴 사유 기타 선택시 입력 필드 노출
|
||||
$('#reason').on('change', function(){
|
||||
if ($(this).val() === '기타') {
|
||||
$('#reasonEtcDiv').show();
|
||||
$('#reason_etc').prop('required', true);
|
||||
} else {
|
||||
$('#reasonEtcDiv').hide();
|
||||
$('#reason_etc').prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 밸리데이션
|
||||
$('#withdrawForm').on('submit', function(e){
|
||||
var pw = $('#password').val();
|
||||
var reason = $('#reason').val();
|
||||
var reasonEtc = $('#reason_etc').val();
|
||||
|
||||
if (!reason) {
|
||||
alert('탈퇴 사유를 선택하세요.');
|
||||
$('#reason').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (reason === '기타' && (!reasonEtc || reasonEtc.trim().length < 2)) {
|
||||
alert('탈퇴 사유를 입력하세요.');
|
||||
$('#reason_etc').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (!pw || pw.length < 4) {
|
||||
alert('비밀번호를 올바르게 입력하세요.');
|
||||
$('#password').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
// 서버 전송
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/order/edit.php
Normal file
8
public/tenant/order/edit.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='order';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/order/manage.php
Normal file
8
public/tenant/order/manage.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='order';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/order/status.php
Normal file
8
public/tenant/order/status.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='order';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
532
public/tenant/permission/analyze.php
Normal file
532
public/tenant/permission/analyze.php
Normal file
@@ -0,0 +1,532 @@
|
||||
<?php
|
||||
// 권한 분석 — 선택한 메뉴/액션 기준으로 효과권한(EFFECTIVE) 분석
|
||||
$CURRENT_SECTION='permission';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
|
||||
<strong>권한 분석</strong>
|
||||
<select class="form-select form-select-sm" id="actionSelect" style="width:160px;">
|
||||
<option value="read">읽기(read)</option>
|
||||
<option value="create">쓰기(create)</option>
|
||||
<option value="update">수정(update)</option>
|
||||
<option value="delete">삭제(delete)</option>
|
||||
<option value="approve">결재(approve)</option>
|
||||
</select>
|
||||
<input class="form-control form-control-sm" id="keyword" placeholder="사용자/부서/역할 검색" style="width:260px;">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnRecalc">권한 재계산</button>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-success" id="btnExport">CSV 내보내기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<!-- 좌: 메뉴 트리 -->
|
||||
<div class="col-md-5">
|
||||
<div class="border rounded p-2">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div class="small text-muted">분석 메뉴</div>
|
||||
<div class="input-group input-group-sm" style="max-width:280px;">
|
||||
<input type="text" class="form-control" id="menuSearch" placeholder="메뉴/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnMenuSearch">검색</button>
|
||||
<button class="btn btn-outline-secondary" id="btnMenuReset">초기화</button>
|
||||
</div>
|
||||
<!-- (기존) 메뉴 검색 input-group 바로 뒤에 추가 -->
|
||||
<div class="form-check form-check-sm ms-2">
|
||||
<input class="form-check-input" type="checkbox" id="highlightAllowed">
|
||||
<label class="form-check-label small" for="highlightAllowed">접근가능</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="menuTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:60%;">메뉴</th>
|
||||
<th>코드</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- PermissionMenu로 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우: 분석 결과 -->
|
||||
<div class="col-md-7">
|
||||
<div class="border rounded p-2">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<div>
|
||||
<div class="small text-muted">선택 메뉴</div>
|
||||
<div id="selMenuTitle" class="fw-semibold">-</div>
|
||||
</div>
|
||||
<div class="small text-muted">
|
||||
결론 규칙: <code>ALLOW = 부서 OR 역할 OR (개인 ALLOW)</code> → <code>개인 DENY 최우선</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-pills mb-2" role="tablist">
|
||||
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabAllow">접근 가능</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabDeny">접근 불가</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabWhy">근거/상세</button></li>
|
||||
<!-- 탭 버튼들 (기존 3개 뒤에 추가) -->
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabUser">사용자 역추적</button></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<!-- 접근 가능 -->
|
||||
<div class="tab-pane fade show active" id="tabAllow">
|
||||
<div class="table-responsive" style="max-height:28vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="tblAllow">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:30%;">사용자</th><th>부서</th><th>역할</th><th>개인모드</th><th>최종</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 접근 불가 -->
|
||||
<div class="tab-pane fade" id="tabDeny">
|
||||
<div class="table-responsive" style="max-height:28vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="tblDeny">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:30%;">사용자</th><th>부서</th><th>역할</th><th>개인모드</th><th>사유</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 근거/상세 -->
|
||||
<div class="tab-pane fade" id="tabWhy">
|
||||
<div class="table-responsive" style="max-height:28vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="tblWhy">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:24%;">사용자</th><th>부서 허용</th><th>역할 허용</th><th>개인 허용</th><th>개인 DENY</th><th>최종</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="small text-muted">* “개인 DENY”가 하나라도 있으면 최종 DENY (최우선)</div>
|
||||
</div>
|
||||
<!-- 탭 콘텐츠 (아래 블록을 기존 tab-content 맨 아래에 추가) -->
|
||||
<div class="tab-pane fade" id="tabUser">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<select class="form-select form-select-sm" id="userTraceSelect" style="width:260px;">
|
||||
<!-- 샘플: USERS 배열과 동일하게 -->
|
||||
<option value="101">권혁성(kevin)</option>
|
||||
<option value="102">김슬기(sally)</option>
|
||||
<option value="103">이민수(minsu)</option>
|
||||
</select>
|
||||
<select class="form-select form-select-sm" id="userTraceAction" style="width:160px;">
|
||||
<option value="read">읽기(read)</option>
|
||||
<option value="create">쓰기(create)</option>
|
||||
<option value="update">수정(update)</option>
|
||||
<option value="delete">삭제(delete)</option>
|
||||
<option value="approve">결재(approve)</option>
|
||||
</select>
|
||||
<div class="form-check ms-2">
|
||||
<input class="form-check-input" type="checkbox" id="onlyAllow">
|
||||
<label class="form-check-label" for="onlyAllow">ALLOW만 보기</label>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnTraceExport">CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:50vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="tblUserTrace">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:40%;">메뉴</th>
|
||||
<th>코드</th>
|
||||
<th class="text-center">최종</th>
|
||||
<th class="text-center">부서</th>
|
||||
<th class="text-center">역할</th>
|
||||
<th class="text-center">개인 ALLOW</th>
|
||||
<th class="text-center">개인 DENY</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.menu-name { font-size:14px; font-weight:500; }
|
||||
.menu-url { color:#6c757d; font-size:12px;}
|
||||
.indent { font-family: ui-monospace, Menlo, Consolas, monospace; color:#6c757d; }
|
||||
#menuTable tbody tr.active { background: #eef3ff; }
|
||||
.badge-inherit{ background:#6c757d; }
|
||||
.badge-allow{ background:#0d6efd; }
|
||||
.badge-deny{ background:#dc3545; }
|
||||
/* ALLOW 행은 왼쪽 라인 + 은은한 배경, DENY는 살짝 흐리게 */
|
||||
#menuTable tbody tr.allow-row {
|
||||
background: #f3f8ff;
|
||||
border-left: 4px solid #0d6efd;
|
||||
}
|
||||
#menuTable tbody tr.deny-dim {
|
||||
opacity: .45;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="/tenant/assets/js/permission_menu.js"></script>
|
||||
<script>
|
||||
// ================== 샘플 데이터 (실전: AJAX로 교체) ==================
|
||||
const MENU_DATA = [
|
||||
{id:1,parent_id:null,title:'대시보드',code:'dashboard',path:'/tenant/member/dashboard.php',source:'system'},
|
||||
{id:2,parent_id:null,title:'수주',code:'order',path:null,source:'system'},
|
||||
{id:21,parent_id:2,title:'수주 관리',code:'order.manage',path:'/tenant/order/manage.php',source:'system'},
|
||||
{id:22,parent_id:2,title:'수주 등록/수정',code:'order.edit',path:'/tenant/order/edit.php',source:'system'},
|
||||
{id:3,parent_id:null,title:'생산',code:'production',path:null,source:'system'},
|
||||
{id:31,parent_id:3,title:'작업 지시',code:'production.wi',path:'/tenant/production/work_instruction.php',source:'workflow'},
|
||||
{id:32,parent_id:3,title:'스크린 작업',code:'production.screen',path:'/tenant/production/screen_work.php',source:'workflow'},
|
||||
{id:5,parent_id:null,title:'게시판',code:'board',path:null,source:'system'},
|
||||
{id:51,parent_id:5,title:'공지사항',code:'board.notice',path:'/tenant/board/notice_list.php',source:'board'},
|
||||
];
|
||||
const TENANT_USE = {dashboard:true, order:true,'order.manage':true,'order.edit':false,
|
||||
production:true,'production.wi':true,'production.screen':true,
|
||||
board:true,'board.notice':true};
|
||||
|
||||
// 조직/역할/유저 샘플
|
||||
const DEPARTMENTS = [{id:201,name:'개발팀'},{id:202,name:'영업팀'}];
|
||||
const ROLES = [{id:301,name:'최고관리자'},{id:302,name:'일반관리자'},{id:303,name:'직원'}];
|
||||
const USERS = [
|
||||
{id:101, name:'권혁성', uid:'kevin', dept_id:201, role_ids:[301,302]},
|
||||
{id:102, name:'김슬기', uid:'sally', dept_id:201, role_ids:[303]},
|
||||
{id:103, name:'이민수', uid:'minsu', dept_id:202, role_ids:[303]},
|
||||
];
|
||||
|
||||
// 부서/역할 권한 정의(샘플) — 실제는 DB에서 로드
|
||||
const DEPT_PERMS = {
|
||||
// dept_id: { code: {read,create,update,delete,approve} }
|
||||
201: { 'order.manage': {read:true,create:true,update:true,delete:false,approve:false},
|
||||
'production.wi':{read:true,create:true,update:false,delete:false,approve:false},
|
||||
'dashboard': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'board.notice': {read:true,create:false,update:false,delete:false,approve:false} },
|
||||
202: { 'order.manage': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'dashboard': {read:true,create:false,update:false,delete:false,approve:false} }
|
||||
};
|
||||
const ROLE_PERMS = {
|
||||
// role_id: { code: {...} }
|
||||
301: { 'order.manage':{read:true,create:true,update:true,delete:true,approve:true},
|
||||
'production': {read:true,create:false,update:false,delete:false,approve:false} },
|
||||
302: { 'production.wi':{read:true,create:true,update:true,delete:false,approve:false} },
|
||||
303: { 'board.notice':{read:true,create:true,update:true,delete:false,approve:false} }
|
||||
};
|
||||
|
||||
// 개인 예외(샘플)
|
||||
// 모드: 사용자별 전역 모드 (INHERIT/ALLOW/DENY)
|
||||
const USER_MODE = { 101:'ALLOW', 102:'INHERIT', 103:'DENY' };
|
||||
// 개인 허용(ALLOW 모드일 때만 의미) — user_id: { code: {...} }
|
||||
const USER_PERMS = {
|
||||
101: { 'production.screen': {read:true,create:true,update:false,delete:false,approve:false} }
|
||||
};
|
||||
// 개인 DENY(선택사항: 메뉴별 차단) — user_id: Set(menu_code)
|
||||
const USER_DENY = {
|
||||
102: new Set(['order.manage']) // 예: 상속으로 되더라도 이 메뉴만 차단
|
||||
};
|
||||
|
||||
// ================== 공통 트리 세팅 + 좌측 메뉴 렌더 ==================
|
||||
PermissionMenu.setData(MENU_DATA, TENANT_USE);
|
||||
|
||||
function renderMenuList(){
|
||||
const rows = PermissionMenu.buildRows((node)=>{
|
||||
return `<td class="text-muted">${node.code}</td>`;
|
||||
});
|
||||
document.querySelector('#menuTable tbody').innerHTML = rows;
|
||||
}
|
||||
renderMenuList();
|
||||
|
||||
let selectedMenuCode = null;
|
||||
function selectFirstMenuIfNeeded(){
|
||||
if (!selectedMenuCode){
|
||||
const first = document.querySelector('#menuTable tbody tr');
|
||||
if (first){ first.classList.add('active'); selectedMenuCode = first.dataset.code; updateSelectedInfo(); runAnalyze(); }
|
||||
}
|
||||
}
|
||||
function updateSelectedInfo(){
|
||||
const tr = document.querySelector('#menuTable tbody tr.active');
|
||||
const title = tr ? tr.querySelector('td:first-child').innerText.trim() : '-';
|
||||
document.querySelector('#selMenuTitle').innerText = title;
|
||||
}
|
||||
|
||||
// 메뉴 선택
|
||||
document.querySelector('#menuTable').addEventListener('click', (e)=>{
|
||||
const tr = e.target.closest('tr'); if(!tr) return;
|
||||
document.querySelectorAll('#menuTable tbody tr').forEach(x=> x.classList.remove('active'));
|
||||
tr.classList.add('active');
|
||||
selectedMenuCode = tr.dataset.code;
|
||||
updateSelectedInfo();
|
||||
runAnalyze();
|
||||
});
|
||||
|
||||
// 메뉴 검색
|
||||
document.querySelector('#btnMenuSearch').addEventListener('click', ()=>{
|
||||
const q = document.querySelector('#menuSearch').value.trim().toLowerCase();
|
||||
document.querySelectorAll('#menuTable tbody tr').forEach(tr=>{
|
||||
const name = tr.querySelector('td:first-child').innerText.toLowerCase();
|
||||
const code = tr.dataset.code.toLowerCase();
|
||||
tr.style.display = (q==='' || name.includes(q) || code.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
document.querySelector('#btnMenuReset').addEventListener('click', ()=>{
|
||||
document.querySelector('#menuSearch').value=''; document.querySelectorAll('#menuTable tbody tr').forEach(tr=> tr.style.display='');
|
||||
});
|
||||
|
||||
// ================== 분석 로직 ==================
|
||||
function hasDeptAllow(userId, code, action){
|
||||
const user = USERS.find(u=>u.id==userId); if(!user) return false;
|
||||
const map = DEPT_PERMS[user.dept_id]||{};
|
||||
return !!(map[code] && map[code][action]);
|
||||
}
|
||||
function hasRoleAllow(userId, code, action){
|
||||
const user = USERS.find(u=>u.id==userId); if(!user) return false;
|
||||
return user.role_ids.some(rid => !!(ROLE_PERMS[rid] && ROLE_PERMS[rid][code] && ROLE_PERMS[rid][code][action]));
|
||||
}
|
||||
function hasUserAllow(userId, code, action){
|
||||
const mode = USER_MODE[userId] || 'INHERIT';
|
||||
if (mode!=='ALLOW') return false;
|
||||
const up = USER_PERMS[userId]||{};
|
||||
return !!(up[code] && up[code][action]);
|
||||
}
|
||||
function hasUserDeny(userId, code){
|
||||
// 전역 DENY 우선
|
||||
if ((USER_MODE[userId]||'INHERIT')==='DENY') return true;
|
||||
// 메뉴별 DENY
|
||||
return !!(USER_DENY[userId] && USER_DENY[userId].has(code));
|
||||
}
|
||||
function effective(userId, code, action){
|
||||
// 개인 DENY 최우선
|
||||
if (hasUserDeny(userId, code)) return {allow:false, reason:'개인 DENY'};
|
||||
const allow = hasDeptAllow(userId, code, action) || hasRoleAllow(userId, code, action) || hasUserAllow(userId, code, action);
|
||||
return {allow, reason: allow?'ALLOW':'NO MATCH'};
|
||||
}
|
||||
|
||||
function runAnalyze(){
|
||||
if (!selectedMenuCode){ selectFirstMenuIfNeeded(); return; }
|
||||
const action = document.querySelector('#actionSelect').value;
|
||||
const kw = document.querySelector('#keyword').value.trim().toLowerCase();
|
||||
|
||||
const rowsAllow=[], rowsDeny=[], rowsWhy=[];
|
||||
USERS.forEach(u=>{
|
||||
const name = `${u.name}(${u.uid})`;
|
||||
if (kw && !name.toLowerCase().includes(kw)) return;
|
||||
|
||||
const dept = (DEPARTMENTS.find(d=>d.id===u.dept_id)||{}).name || '-';
|
||||
const roleNames = u.role_ids.map(id => (ROLES.find(r=>r.id===id)||{}).name).filter(Boolean).join(', ') || '-';
|
||||
const mode = USER_MODE[u.id] || 'INHERIT';
|
||||
|
||||
const deptOk = hasDeptAllow(u.id, selectedMenuCode, action);
|
||||
const roleOk = hasRoleAllow(u.id, selectedMenuCode, action);
|
||||
const userOk = hasUserAllow(u.id, selectedMenuCode, action);
|
||||
const uDeny = hasUserDeny(u.id, selectedMenuCode);
|
||||
|
||||
const eff = effective(u.id, selectedMenuCode, action);
|
||||
|
||||
const modeBadge = mode==='INHERIT' ? '<span class="badge badge-inherit">INHERIT</span>' :
|
||||
mode==='ALLOW' ? '<span class="badge badge-allow">ALLOW</span>' :
|
||||
'<span class="badge badge-deny">DENY</span>';
|
||||
|
||||
const finalBadge = eff.allow ? '<span class="badge bg-primary">ALLOW</span>' :
|
||||
'<span class="badge bg-danger">DENY</span>';
|
||||
|
||||
// WHY 표
|
||||
rowsWhy.push(`<tr>
|
||||
<td>${name}</td>
|
||||
<td>${deptOk?'Y':''}</td>
|
||||
<td>${roleOk?'Y':''}</td>
|
||||
<td>${userOk?'Y':''}</td>
|
||||
<td>${uDeny?'Y':''}</td>
|
||||
<td>${eff.allow? 'ALLOW':'DENY'}</td>
|
||||
</tr>`);
|
||||
|
||||
if (eff.allow){
|
||||
rowsAllow.push(`<tr>
|
||||
<td>${name}</td><td>${dept}</td><td>${roleNames}</td><td>${modeBadge}</td><td>${finalBadge}</td>
|
||||
</tr>`);
|
||||
} else {
|
||||
const reason = uDeny ? '개인 DENY' : '권한 없음';
|
||||
rowsDeny.push(`<tr>
|
||||
<td>${name}</td><td>${dept}</td><td>${roleNames}</td><td>${modeBadge}</td><td>${reason}</td>
|
||||
</tr>`);
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#tblAllow tbody').innerHTML = rowsAllow.join('') || `<tr><td colspan="5" class="text-center text-muted">해당 없음</td></tr>`;
|
||||
document.querySelector('#tblDeny tbody').innerHTML = rowsDeny.join('') || `<tr><td colspan="5" class="text-center text-muted">해당 없음</td></tr>`;
|
||||
document.querySelector('#tblWhy tbody').innerHTML = rowsWhy.join('') || `<tr><td colspan="6" class="text-center text-muted">해당 없음</td></tr>`;
|
||||
}
|
||||
|
||||
// 액션/검색/재계산
|
||||
document.querySelector('#actionSelect').addEventListener('change', runAnalyze);
|
||||
document.querySelector('#keyword').addEventListener('keyup', (e)=>{ if(e.key==='Enter') runAnalyze(); });
|
||||
document.querySelector('#btnRecalc').addEventListener('click', runAnalyze);
|
||||
|
||||
// CSV 내보내기(접근 가능 탭 기준 + WHY 탭 함께)
|
||||
document.querySelector('#btnExport').addEventListener('click', ()=>{
|
||||
const rows = [['tab','user','dept','roles','mode','final','dept_allow','role_allow','user_allow','user_deny']];
|
||||
USERS.forEach(u=>{
|
||||
const dept = (DEPARTMENTS.find(d=>d.id===u.dept_id)||{}).name || '-';
|
||||
const roleNames = u.role_ids.map(id => (ROLES.find(r=>r.id===id)||{}).name).filter(Boolean).join('|') || '-';
|
||||
const mode = USER_MODE[u.id] || 'INHERIT';
|
||||
const a = document.querySelector('#actionSelect').value;
|
||||
const code = selectedMenuCode || '';
|
||||
const deptOk = hasDeptAllow(u.id, code, a);
|
||||
const roleOk = hasRoleAllow(u.id, code, a);
|
||||
const userOk = hasUserAllow(u.id, code, a);
|
||||
const uDeny = hasUserDeny(u.id, code);
|
||||
const eff = effective(u.id, code, a);
|
||||
rows.push(['result', `${u.name}(${u.uid})`, dept, roleNames, mode, eff.allow?'ALLOW':'DENY', deptOk?'Y':'', roleOk?'Y':'', userOk?'Y':'', uDeny?'Y':'']);
|
||||
});
|
||||
const csv = rows.map(r=> r.map(x=> `"${String(x).replace(/"/g,'""')}"`).join(',')).join('\n');
|
||||
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = 'permission_analysis.csv'; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// 최초 선택/분석
|
||||
selectFirstMenuIfNeeded();
|
||||
|
||||
// ---------- 사용자 역추적 ----------
|
||||
function traceEffectiveForUser(userId, code, action){
|
||||
// anlyze 탭에서 쓰던 로직 재사용
|
||||
const deptOk = hasDeptAllow(userId, code, action);
|
||||
const roleOk = hasRoleAllow(userId, code, action);
|
||||
const userOk = hasUserAllow(userId, code, action);
|
||||
const uDeny = hasUserDeny(userId, code);
|
||||
const allow = !uDeny && (deptOk || roleOk || userOk);
|
||||
return {allow, deptOk, roleOk, userOk, uDeny};
|
||||
}
|
||||
|
||||
function renderUserTrace(){
|
||||
const userId = parseInt(document.getElementById('userTraceSelect').value,10);
|
||||
const action = document.getElementById('userTraceAction').value;
|
||||
const onlyAllow = document.getElementById('onlyAllow').checked;
|
||||
|
||||
const rows = [];
|
||||
// PermissionMenu.activeCodes() 순회해서 메뉴별 결과 뽑기
|
||||
const { maps } = PermissionMenu;
|
||||
const codes = PermissionMenu.activeCodes();
|
||||
const { byCode } = maps();
|
||||
|
||||
codes.forEach(code=>{
|
||||
const node = byCode[code];
|
||||
if (!node) return;
|
||||
const r = traceEffectiveForUser(userId, code, action);
|
||||
if (onlyAllow && !r.allow) return;
|
||||
|
||||
const finalBadge = r.allow ? '<span class="badge bg-primary">ALLOW</span>'
|
||||
: '<span class="badge bg-danger">DENY</span>';
|
||||
rows.push(`
|
||||
<tr>
|
||||
<td><span class="menu-name">${node.title}</span>
|
||||
${node.source==='workflow' ? '<span class="badge bg-warning text-dark ms-1">workflow</span>':''}
|
||||
${node.source==='board' ? '<span class="badge bg-success ms-1">board</span>':''}
|
||||
${node.path? `<span class="menu-url ms-2 text-muted">${node.path}</span>`:''}
|
||||
</td>
|
||||
<td class="text-muted">${node.code}</td>
|
||||
<td class="text-center">${finalBadge}</td>
|
||||
<td class="text-center">${r.deptOk?'Y':''}</td>
|
||||
<td class="text-center">${r.roleOk?'Y':''}</td>
|
||||
<td class="text-center">${r.userOk?'Y':''}</td>
|
||||
<td class="text-center">${r.uDeny?'Y':''}</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
const tbody = document.querySelector('#tblUserTrace tbody');
|
||||
tbody.innerHTML = rows.join('') || `<tr><td colspan="7" class="text-center text-muted">해당 없음</td></tr>`;
|
||||
}
|
||||
|
||||
// 이벤트 바인딩
|
||||
['userTraceSelect','userTraceAction','onlyAllow'].forEach(id=>{
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('change', renderUserTrace);
|
||||
});
|
||||
|
||||
// CSV 내보내기
|
||||
document.getElementById('btnTraceExport').addEventListener('click', ()=>{
|
||||
const userId = parseInt(document.getElementById('userTraceSelect').value,10);
|
||||
const action = document.getElementById('userTraceAction').value;
|
||||
|
||||
const user = USERS.find(u=>u.id===userId);
|
||||
const rows = [['menu','code','final','dept','role','user_allow','user_deny','action',`user:${user?.name||''}(${user?.uid||''})`]];
|
||||
|
||||
const { maps } = PermissionMenu; const { byCode } = maps();
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
const node = byCode[code]; if (!node) return;
|
||||
const r = traceEffectiveForUser(userId, code, action);
|
||||
rows.push([
|
||||
node.title, code, r.allow?'ALLOW':'DENY',
|
||||
r.deptOk?'Y':'', r.roleOk?'Y':'', r.userOk?'Y':'', r.uDeny?'Y':'', action
|
||||
]);
|
||||
});
|
||||
|
||||
const csv = rows.map(r=> r.map(x=>`"${String(x).replace(/"/g,'""')}"`).join(',')).join('\n');
|
||||
const blob = new Blob([csv], {type:'text/csv;charset=utf-8;'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = 'user_trace.csv'; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// 처음 들어오면 한 번 렌더
|
||||
renderUserTrace();
|
||||
|
||||
|
||||
// 좌측 트리에 허용 하이라이트 적용
|
||||
function updateMenuHighlight() {
|
||||
const chk = document.getElementById('highlightAllowed');
|
||||
const userSel = document.getElementById('userTraceSelect');
|
||||
const actSel = document.getElementById('userTraceAction');
|
||||
if (!chk || !userSel || !actSel) return;
|
||||
|
||||
const on = chk.checked;
|
||||
const userId = parseInt(userSel.value, 10);
|
||||
const action = actSel.value;
|
||||
|
||||
document.querySelectorAll('#menuTable tbody tr').forEach(tr=>{
|
||||
tr.classList.remove('allow-row','deny-dim');
|
||||
if (!on) return; // 꺼져 있으면 리셋만
|
||||
|
||||
const code = tr.dataset.code;
|
||||
// analyze 탭에서 쓰던 판단식 재사용
|
||||
const r = effective(userId, code, action); // {allow:boolean}
|
||||
if (r.allow) tr.classList.add('allow-row');
|
||||
else tr.classList.add('deny-dim');
|
||||
});
|
||||
}
|
||||
|
||||
// 탭/선택 변경 시 갱신
|
||||
['highlightAllowed','userTraceSelect','userTraceAction'].forEach(id=>{
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('change', updateMenuHighlight);
|
||||
});
|
||||
|
||||
// 최초 렌더/재렌더 후에도 호출
|
||||
document.addEventListener('DOMContentLoaded', updateMenuHighlight);
|
||||
|
||||
// 좌측 메뉴 다시 그릴 때도 호출 (이미 있는 렌더 뒤쪽에 한 줄만 추가)
|
||||
const _origRenderMenuList = renderMenuList;
|
||||
renderMenuList = function() {
|
||||
_origRenderMenuList();
|
||||
updateMenuHighlight();
|
||||
};
|
||||
|
||||
// 사용자 역추적 테이블 재계산 후에도 한 번 더 (이미 만든 함수 끝에 한 줄 추가)
|
||||
const _origRenderUserTrace = renderUserTrace;
|
||||
renderUserTrace = function() {
|
||||
_origRenderUserTrace();
|
||||
updateMenuHighlight();
|
||||
};
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
70
public/tenant/permission/approver.php
Normal file
70
public/tenant/permission/approver.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php $CURRENT_SECTION='permission'; include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:1100px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<strong>결제(결재) 권자 설정</strong>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#ruleModal">규칙 추가</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr><th>규칙명</th><th>금액 구간</th><th>구성원</th><th style="width:140px;">관리</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>일반 결재</td>
|
||||
<td>0 ~ 1,000만원</td>
|
||||
<td><span class="badge bg-secondary">개발팀</span> <span class="badge bg-info text-dark">일반관리자</span> <span class="badge bg-dark">kevin</span></td>
|
||||
<td><button class="btn btn-sm btn-outline-secondary">편집</button> <button class="btn btn-sm btn-outline-danger">삭제</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 규칙 추가 모달 -->
|
||||
<div class="modal fade" id="ruleModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg"><div class="modal-content">
|
||||
<div class="modal-header"><h5 class="modal-title">결제 규칙 추가</h5><button class="btn-close" data-bs-dismiss="modal"></button></div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="form-label">규칙명</label><input class="form-control" id="ruleName"></div>
|
||||
<div class="col-md-3"><label class="form-label">최소금액</label><input class="form-control" id="minAmt" placeholder="원"></div>
|
||||
<div class="col-md-3"><label class="form-label">최대금액</label><input class="form-control" id="maxAmt" placeholder="원"></div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">구성원 추가</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select" id="memberType" style="max-width:160px;">
|
||||
<option value="department">부서</option>
|
||||
<option value="role">역할</option>
|
||||
<option value="user">개인</option>
|
||||
</select>
|
||||
<input class="form-control" id="memberKeyword" placeholder="이름/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnFindMember">검색</button>
|
||||
<button class="btn btn-outline-primary" id="btnAddMember">추가</button>
|
||||
</div>
|
||||
<div class="mt-2" id="memberChips"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button class="btn btn-primary" id="btnSaveRule">저장</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#btnSaveRule').on('click', ()=>{ alert('저장(샘플) — /api/permission/approval_rule_save.php'); $('#ruleModal').modal('hide'); });
|
||||
$('#btnAddMember').on('click', ()=>{
|
||||
const type = $('#memberType').val();
|
||||
const kw = $('#memberKeyword').val().trim() || (type==='department'?'개발팀': type==='role'?'일반관리자':'kevin');
|
||||
$('#memberChips').append(`<span class="badge bg-secondary me-1">${type}:${kw}</span>`);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
52
public/tenant/permission/audit.php
Normal file
52
public/tenant/permission/audit.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php $CURRENT_SECTION='permission'; include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:1100px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header"><strong>권한 분석</strong></div>
|
||||
<div class="card-body">
|
||||
<ul class="nav nav-tabs" id="auditTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#byMenu" type="button" role="tab">메뉴 기준</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#byUser" type="button" role="tab">사용자 기준</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content border border-top-0 p-3">
|
||||
<div class="tab-pane fade show active" id="byMenu" role="tabpanel">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<select class="form-select" style="max-width:360px;">
|
||||
<option>수주 / 수주관리</option>
|
||||
<option>생산 / 작업지시</option>
|
||||
</select>
|
||||
<button class="btn btn-primary">조회</button>
|
||||
</div>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr><th>유저</th><th>부서</th><th>역할</th><th>최종 읽기</th><th>최종 쓰기</th><th>DENY 여부</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>권혁성</td><td>개발팀</td><td>일반관리자</td><td>○</td><td>×</td><td>-</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="byUser" role="tabpanel">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<input class="form-control" placeholder="이름/아이디">
|
||||
<button class="btn btn-primary">조회</button>
|
||||
</div>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr><th>메뉴</th><th>최종 읽기</th><th>최종 쓰기</th><th>DENY 여부</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>수주 / 수주관리</td><td>○</td><td>×</td><td>-</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
253
public/tenant/permission/department.php
Normal file
253
public/tenant/permission/department.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
// 부서 권한 설정 (트리형) — 메뉴관리에서 활성화된 메뉴만 대상
|
||||
$CURRENT_SECTION='permission';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
|
||||
<strong>부서 권한 설정 (트리)</strong>
|
||||
<select class="form-select form-select-sm" id="deptSelect" style="width:240px;">
|
||||
<option value="1">경영지원팀</option>
|
||||
<option value="2">영업팀</option>
|
||||
<option value="3" selected>개발팀</option>
|
||||
</select>
|
||||
<div class="ms-auto d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnAllAllow">전체 허용</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="btnAllDeny">전체 금지</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnClear">초기화</button>
|
||||
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<div class="input-group" style="max-width:420px;">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
||||
<button class="btn btn-outline-secondary" id="btnResetSearch">초기화</button>
|
||||
</div>
|
||||
<div class="small text-muted ms-auto">
|
||||
* 부모 설정은 하위로 전파됩니다. 자식이 섞이면 부모 칸은 흐릿 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="permTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:320px;">메뉴</th>
|
||||
<th class="text-center" style="width:90px;">
|
||||
읽기<br><input type="checkbox" id="hdr_read">
|
||||
</th>
|
||||
<th class="text-center" style="width:90px;">
|
||||
쓰기<br><input type="checkbox" id="hdr_create">
|
||||
</th>
|
||||
<th class="text-center" style="width:90px;">
|
||||
수정<br><input type="checkbox" id="hdr_update">
|
||||
</th>
|
||||
<th class="text-center" style="width:90px;">
|
||||
삭제<br><input type="checkbox" id="hdr_delete">
|
||||
</th>
|
||||
<th class="text-center" style="width:90px;">
|
||||
결재<br><input type="checkbox" id="hdr_approve">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- PermissionMenu가 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 메뉴관리 페이지와 동일한 크기/톤 */
|
||||
.menu-name { font-size:14px; font-weight:500; }
|
||||
.menu-url { color:#6c757d; font-size:12px;}
|
||||
.indent { font-family: ui-monospace, Menlo, Consolas, monospace; color:#6c757d; }
|
||||
</style>
|
||||
|
||||
<!-- 공통 트리 모듈 로드 -->
|
||||
<script src="/tenant/assets/js/permission_menu.js"></script>
|
||||
|
||||
<script>
|
||||
// ====== 샘플 데이터(실전에서는 AJAX 로드하세요) ======
|
||||
const MENU_DATA = [
|
||||
{id:1, parent_id:null, title:'대시보드', code:'dashboard', path:'/tenant/member/dashboard.php', source:'system'},
|
||||
{id:2, parent_id:null, title:'수주', code:'order', path:null, source:'system'},
|
||||
{id:21, parent_id:2, title:'수주 관리', code:'order.manage', path:'/tenant/order/manage.php', source:'system'},
|
||||
{id:22, parent_id:2, title:'수주 등록/수정', code:'order.edit', path:'/tenant/order/edit.php', source:'system'},
|
||||
|
||||
{id:3, parent_id:null, title:'생산', code:'production', path:null, source:'system'},
|
||||
{id:31, parent_id:3, title:'작업 지시', code:'production.wi', path:'/tenant/production/work_instruction.php', source:'workflow'},
|
||||
{id:32, parent_id:3, title:'스크린 작업', code:'production.screen', path:'/tenant/production/screen_work.php', source:'workflow'},
|
||||
|
||||
{id:4, parent_id:null, title:'자재', code:'material', path:null, source:'system'},
|
||||
{id:41, parent_id:4, title:'수입 검사 대장', code:'material.insp', path:'/tenant/material/inspection_list.php', source:'workflow'},
|
||||
|
||||
{id:5, parent_id:null, title:'게시판', code:'board', path:null, source:'system'},
|
||||
{id:51, parent_id:5, title:'공지사항', code:'board.notice', path:'/tenant/board/notice_list.php', source:'board'},
|
||||
{id:52, parent_id:5, title:'Q&A', code:'board.qna', path:'/tenant/board/qna_list.php', source:'board'},
|
||||
];
|
||||
|
||||
// 테넌트에서 “메뉴 사용 설정”으로 켜둔 메뉴만 권한 대상
|
||||
const TENANT_USE = {
|
||||
'dashboard': true,
|
||||
'order': true, 'order.manage': true, 'order.edit': false,
|
||||
'production': true, 'production.wi': true, 'production.screen': true,
|
||||
'material': true, 'material.insp': false,
|
||||
'board': true, 'board.notice': true, 'board.qna': false
|
||||
};
|
||||
|
||||
// 현재 선택 부서의 권한 상태 (페이지 state)
|
||||
// 실제 구현: deptSelect 변경 시 AJAX 로드해서 채워넣기
|
||||
const DEPT_PERMS = {}; // code: {read,create,update,delete,approve}
|
||||
|
||||
// 공통 트리 데이터 주입
|
||||
PermissionMenu.setData(MENU_DATA, TENANT_USE);
|
||||
|
||||
// 행 렌더
|
||||
function renderRows(){
|
||||
const rows = PermissionMenu.buildRows((node, depth) => {
|
||||
const p = (DEPT_PERMS[node.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
return ['read','create','update','delete','approve'].map(perm =>
|
||||
`<td class="text-center">
|
||||
<input type="checkbox" class="perm" data-perm="${perm}" ${p[perm]?'checked':''}>
|
||||
</td>`
|
||||
).join('');
|
||||
});
|
||||
document.querySelector('#permTable tbody').innerHTML = rows;
|
||||
applyIndeterminate();
|
||||
}
|
||||
|
||||
// 전파/요약
|
||||
function setChildrenPerm(code, perm, on){
|
||||
PermissionMenu.childrenOf(code).forEach(ch=>{
|
||||
(DEPT_PERMS[ch.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
DEPT_PERMS[ch.code][perm] = on;
|
||||
setChildrenPerm(ch.code, perm, on);
|
||||
});
|
||||
}
|
||||
function updateParentPerms(code, perm){
|
||||
const parent = PermissionMenu.parentOf(code);
|
||||
if (!parent) return;
|
||||
const kids = PermissionMenu.childrenOf(parent.code);
|
||||
const states = kids.map(k => !!(DEPT_PERMS[k.code] && DEPT_PERMS[k.code][perm]));
|
||||
const allOn = states.every(Boolean);
|
||||
const allOff = states.every(s=>!s);
|
||||
(DEPT_PERMS[parent.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
DEPT_PERMS[parent.code][perm] = allOn ? true : (allOff ? false : true);
|
||||
updateParentPerms(parent.code, perm);
|
||||
}
|
||||
function applyIndeterminate(){
|
||||
// 행별 indeterminate
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
||||
const code = tr.dataset.code;
|
||||
const kids = PermissionMenu.childrenOf(code);
|
||||
tr.querySelectorAll('input.perm').forEach(chk=>{
|
||||
chk.indeterminate = false;
|
||||
if (kids.length){
|
||||
const perm = chk.dataset.perm;
|
||||
const onCnt = kids.filter(k => !!(DEPT_PERMS[k.code] && DEPT_PERMS[k.code][perm])).length;
|
||||
if (onCnt>0 && onCnt<kids.length) chk.indeterminate = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
// 헤더 요약
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if(!hdr) return;
|
||||
const codes = PermissionMenu.activeCodes();
|
||||
const vals = codes.map(code => !!(DEPT_PERMS[code] && DEPT_PERMS[code][perm]));
|
||||
const onCnt = vals.filter(Boolean).length;
|
||||
hdr.indeterminate = (onCnt>0 && onCnt<vals.length);
|
||||
hdr.checked = (onCnt===vals.length);
|
||||
});
|
||||
}
|
||||
|
||||
// 이벤트
|
||||
document.addEventListener('change', e=>{
|
||||
const el = e.target;
|
||||
if (!el.classList.contains('perm')) return;
|
||||
const tr = el.closest('tr');
|
||||
const code = tr.dataset.code;
|
||||
const perm = el.dataset.perm;
|
||||
(DEPT_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
DEPT_PERMS[code][perm] = el.checked;
|
||||
setChildrenPerm(code, perm, el.checked);
|
||||
updateParentPerms(code, perm);
|
||||
renderRows();
|
||||
});
|
||||
|
||||
// 헤더 전체 허용/금지(권한별)
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if (!hdr) return;
|
||||
hdr.addEventListener('change', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
(DEPT_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
DEPT_PERMS[code][perm] = hdr.checked;
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
});
|
||||
|
||||
// 검색
|
||||
document.querySelector('#btnSearch').addEventListener('click', ()=>{
|
||||
const q = document.querySelector('#searchInput').value.trim().toLowerCase();
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
||||
const name = tr.querySelector('td:first-child').innerText.toLowerCase();
|
||||
const code = tr.dataset.code.toLowerCase();
|
||||
tr.style.display = (q==='' || name.includes(q) || code.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
document.querySelector('#btnResetSearch').addEventListener('click', ()=>{
|
||||
document.querySelector('#searchInput').value='';
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=> tr.style.display='');
|
||||
});
|
||||
|
||||
// 전체 허용/금지/초기화
|
||||
document.querySelector('#btnAllAllow').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
DEPT_PERMS[code] = {read:true,create:true,update:true,delete:true,approve:true};
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
document.querySelector('#btnAllDeny').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
DEPT_PERMS[code] = {read:false,create:false,update:false,delete:false,approve:false};
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
document.querySelector('#btnClear').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
DEPT_PERMS[code] = {read:false,create:false,update:false,delete:false,approve:false};
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
|
||||
// 부서 변경 → 서버에서 권한 로드 (샘플)
|
||||
document.querySelector('#deptSelect').addEventListener('change', function(){
|
||||
const id = this.value;
|
||||
alert('부서 권한 로드(샘플): GET /api/permission/department_get.php?dept_id='+id);
|
||||
// 실제: AJAX로 DEPT_PERMS 갱신 후 renderRows();
|
||||
});
|
||||
|
||||
// 저장
|
||||
document.querySelector('#btnSave').addEventListener('click', ()=>{
|
||||
const deptId = document.querySelector('#deptSelect').value;
|
||||
const payload = {};
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
payload[code] = DEPT_PERMS[code] || {read:false,create:false,update:false,delete:false,approve:false};
|
||||
});
|
||||
console.log('SAVE department permissions', {deptId, perms: payload});
|
||||
alert('저장(샘플): POST /api/permission/department_save.php');
|
||||
// $.post('/api/permission/department_save.php', { dept_id:deptId, data: JSON.stringify(payload) })
|
||||
});
|
||||
|
||||
renderRows();
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
189
public/tenant/permission/role.php
Normal file
189
public/tenant/permission/role.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
// 역할 권한 설정 (트리형)
|
||||
$CURRENT_SECTION='permission';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
|
||||
<strong>역할 권한 설정 (트리)</strong>
|
||||
<select class="form-select form-select-sm" id="roleSelect" style="width:240px;">
|
||||
<option value="1">최고관리자</option>
|
||||
<option value="2">일반관리자</option>
|
||||
<option value="3" selected>일반직원</option>
|
||||
</select>
|
||||
<div class="ms-auto d-flex flex-wrap gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnAllAllow">전체 허용</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="btnAllDeny">전체 금지</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnClear">초기화</button>
|
||||
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<div class="input-group" style="max-width:420px;">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
||||
<button class="btn btn-outline-secondary" id="btnResetSearch">초기화</button>
|
||||
</div>
|
||||
<div class="small text-muted ms-auto">
|
||||
* 부모 설정은 하위로 전파됩니다. 자식이 섞이면 부모 칸은 흐릿 표시됩니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="permTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:320px;">메뉴</th>
|
||||
<th class="text-center" style="width:90px;">읽기<br><input type="checkbox" id="hdr_read"></th>
|
||||
<th class="text-center" style="width:90px;">쓰기<br><input type="checkbox" id="hdr_create"></th>
|
||||
<th class="text-center" style="width:90px;">수정<br><input type="checkbox" id="hdr_update"></th>
|
||||
<th class="text-center" style="width:90px;">삭제<br><input type="checkbox" id="hdr_delete"></th>
|
||||
<th class="text-center" style="width:90px;">결재<br><input type="checkbox" id="hdr_approve"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- rows --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.menu-name { font-size:14px; font-weight:500; }
|
||||
.menu-url { color:#6c757d; font-size:12px;}
|
||||
.indent { font-family: ui-monospace, Menlo, Consolas, monospace; color:#6c757d; }
|
||||
</style>
|
||||
|
||||
<script src="/tenant/assets/js/permission_menu.js"></script>
|
||||
<script>
|
||||
// ===== 샘플 데이터 (실전: AJAX) =====
|
||||
const MENU_DATA = [
|
||||
{id:1,parent_id:null,title:'대시보드',code:'dashboard',path:'/tenant/member/dashboard.php',source:'system'},
|
||||
{id:2,parent_id:null,title:'수주',code:'order',path:null,source:'system'},
|
||||
{id:21,parent_id:2,title:'수주 관리',code:'order.manage',path:'/tenant/order/manage.php',source:'system'},
|
||||
{id:22,parent_id:2,title:'수주 등록/수정',code:'order.edit',path:'/tenant/order/edit.php',source:'system'},
|
||||
{id:3,parent_id:null,title:'생산',code:'production',path:null,source:'system'},
|
||||
{id:31,parent_id:3,title:'작업 지시',code:'production.wi',path:'/tenant/production/work_instruction.php',source:'workflow'},
|
||||
{id:32,parent_id:3,title:'스크린 작업',code:'production.screen',path:'/tenant/production/screen_work.php',source:'workflow'},
|
||||
{id:5,parent_id:null,title:'게시판',code:'board',path:null,source:'system'},
|
||||
{id:51,parent_id:5,title:'공지사항',code:'board.notice',path:'/tenant/board/notice_list.php',source:'board'},
|
||||
];
|
||||
const TENANT_USE = {dashboard:true, order:true,'order.manage':true,'order.edit':false,
|
||||
production:true,'production.wi':true,'production.screen':true,
|
||||
board:true,'board.notice':true};
|
||||
|
||||
// 역할 권한 상태
|
||||
const ROLE_PERMS = {}; // code: {read,create,update,delete,approve}
|
||||
|
||||
PermissionMenu.setData(MENU_DATA, TENANT_USE);
|
||||
|
||||
function renderRows(){
|
||||
const rows = PermissionMenu.buildRows((node) => {
|
||||
const p = (ROLE_PERMS[node.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
return ['read','create','update','delete','approve'].map(perm =>
|
||||
`<td class="text-center"><input type="checkbox" class="perm" data-perm="${perm}" ${p[perm]?'checked':''}></td>`
|
||||
).join('');
|
||||
});
|
||||
document.querySelector('#permTable tbody').innerHTML = rows;
|
||||
applyIndeterminate();
|
||||
}
|
||||
|
||||
function setChildrenPerm(code, perm, on){
|
||||
PermissionMenu.childrenOf(code).forEach(ch=>{
|
||||
(ROLE_PERMS[ch.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
ROLE_PERMS[ch.code][perm] = on;
|
||||
setChildrenPerm(ch.code, perm, on);
|
||||
});
|
||||
}
|
||||
function updateParentPerms(code, perm){
|
||||
const parent = PermissionMenu.parentOf(code);
|
||||
if (!parent) return;
|
||||
const kids = PermissionMenu.childrenOf(parent.code);
|
||||
const states = kids.map(k => !!(ROLE_PERMS[k.code] && ROLE_PERMS[k.code][perm]));
|
||||
const allOn = states.every(Boolean), allOff = states.every(s=>!s);
|
||||
(ROLE_PERMS[parent.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
ROLE_PERMS[parent.code][perm] = allOn ? true : (allOff ? false : true);
|
||||
updateParentPerms(parent.code, perm);
|
||||
}
|
||||
function applyIndeterminate(){
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
||||
const code = tr.dataset.code, kids = PermissionMenu.childrenOf(code);
|
||||
tr.querySelectorAll('input.perm').forEach(chk=>{
|
||||
chk.indeterminate = false;
|
||||
if (kids.length){
|
||||
const perm = chk.dataset.perm;
|
||||
const onCnt = kids.filter(k => !!(ROLE_PERMS[k.code] && ROLE_PERMS[k.code][perm])).length;
|
||||
if (onCnt>0 && onCnt<kids.length) chk.indeterminate = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if(!hdr) return;
|
||||
const codes = PermissionMenu.activeCodes();
|
||||
const vals = codes.map(code => !!(ROLE_PERMS[code] && ROLE_PERMS[code][perm]));
|
||||
const onCnt = vals.filter(Boolean).length;
|
||||
hdr.indeterminate = (onCnt>0 && onCnt<vals.length);
|
||||
hdr.checked = (onCnt===vals.length);
|
||||
});
|
||||
}
|
||||
|
||||
// events
|
||||
document.addEventListener('change', e=>{
|
||||
if (!e.target.classList.contains('perm')) return;
|
||||
const tr = e.target.closest('tr'); const code = tr.dataset.code; const perm = e.target.dataset.perm;
|
||||
(ROLE_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
ROLE_PERMS[code][perm] = e.target.checked;
|
||||
setChildrenPerm(code, perm, e.target.checked);
|
||||
updateParentPerms(code, perm);
|
||||
renderRows();
|
||||
});
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if(!hdr) return;
|
||||
hdr.addEventListener('change', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
(ROLE_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
ROLE_PERMS[code][perm] = hdr.checked;
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
});
|
||||
// 검색/초기화
|
||||
document.querySelector('#btnSearch').addEventListener('click', ()=>{
|
||||
const q = document.querySelector('#searchInput').value.trim().toLowerCase();
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
||||
const name = tr.querySelector('td:first-child').innerText.toLowerCase();
|
||||
const code = tr.dataset.code.toLowerCase();
|
||||
tr.style.display = (q==='' || name.includes(q) || code.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
document.querySelector('#btnResetSearch').addEventListener('click', ()=>{
|
||||
document.querySelector('#searchInput').value=''; document.querySelectorAll('#permTable tbody tr').forEach(tr=> tr.style.display='');
|
||||
});
|
||||
// 일괄 버튼
|
||||
document.querySelector('#btnAllAllow').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{ ROLE_PERMS[code]={read:true,create:true,update:true,delete:true,approve:true}; });
|
||||
renderRows();
|
||||
});
|
||||
document.querySelector('#btnAllDeny').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{ ROLE_PERMS[code]={read:false,create:false,update:false,delete:false,approve:false}; });
|
||||
renderRows();
|
||||
});
|
||||
document.querySelector('#btnClear').addEventListener('click', ()=>{
|
||||
PermissionMenu.activeCodes().forEach(code=>{ ROLE_PERMS[code]={read:false,create:false,update:false,delete:false,approve:false}; });
|
||||
renderRows();
|
||||
});
|
||||
// 역할 변경(샘플)
|
||||
document.querySelector('#roleSelect').addEventListener('change', function(){
|
||||
alert('역할 권한 로드(샘플): GET /api/permission/role_get.php?role_id='+this.value);
|
||||
// 실제 구현: ROLE_PERMS 갱신 → renderRows();
|
||||
});
|
||||
|
||||
renderRows();
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
113
public/tenant/permission/structure.php
Normal file
113
public/tenant/permission/structure.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php $CURRENT_SECTION='permission'; include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">메뉴 트리</div>
|
||||
<div class="card-body" style="max-height:60vh; overflow:auto;">
|
||||
<div class="mb-2 d-flex gap-2">
|
||||
<input type="text" class="form-control" id="menuSearch" placeholder="메뉴명/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnMenuSearch">검색</button>
|
||||
</div>
|
||||
<ul id="menuTree" class="list-group small">
|
||||
<!-- 샘플 -->
|
||||
<li class="list-group-item">
|
||||
<a href="#" class="menu-node" data-id="1" data-code="dashboard">대시보드</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<a href="#" class="menu-node" data-id="2" data-code="order">수주</a>
|
||||
<ul class="mt-1">
|
||||
<li><a href="#" class="menu-node" data-id="21" data-code="order.manage">수주관리</a></li>
|
||||
<li><a href="#" class="menu-node" data-id="22" data-code="order.edit">수주 등록/수정</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- ... -->
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-footer d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnAddChild">하위메뉴 추가</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="btnDeleteMenu">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">메뉴 상세/권한 항목</div>
|
||||
<div class="card-body">
|
||||
<form id="menuForm">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">메뉴명</label>
|
||||
<input type="text" class="form-control" name="title" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">코드</label>
|
||||
<input type="text" class="form-control" name="code" required>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">경로(Path)</label>
|
||||
<input type="text" class="form-control" name="path" placeholder="/tenant/...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">정렬</label>
|
||||
<input type="number" class="form-control" name="sort_order" value="10">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="is_active" checked>
|
||||
<label class="form-check-label" for="is_active">활성화</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div>
|
||||
<label class="form-label">권한 항목</label>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" name="perm_read" id="perm_read"><label class="form-check-label" for="perm_read">읽기</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" name="perm_create" id="perm_create"><label class="form-check-label" for="perm_create">쓰기</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" name="perm_update" id="perm_update"><label class="form-check-label" for="perm_update">수정</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" name="perm_delete" id="perm_delete"><label class="form-check-label" for="perm_delete">삭제</label></div>
|
||||
<div class="form-check"><input class="form-check-input" type="checkbox" name="perm_approve" id="perm_approve"><label class="form-check-label" for="perm_approve">결재</label></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">저장</button>
|
||||
<button class="btn btn-outline-secondary" type="button" id="btnReset">초기화</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// 트리에서 노드 선택 → 상세 로드(샘플)
|
||||
$(document).on('click','.menu-node',function(e){
|
||||
e.preventDefault();
|
||||
$('.menu-node').removeClass('fw-bold');
|
||||
$(this).addClass('fw-bold');
|
||||
const code = $(this).data('code');
|
||||
// 샘플 바인딩
|
||||
$('#menuForm [name=title]').val($(this).text().trim());
|
||||
$('#menuForm [name=code]').val(code);
|
||||
$('#menuForm [name=path]').val('/tenant/'+code.replaceAll('.','/')+'.php');
|
||||
});
|
||||
|
||||
$('#menuForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
alert('저장(샘플) — 실제는 /api/permission/structure_save.php 로 POST');
|
||||
});
|
||||
|
||||
$('#btnAddChild').on('click', ()=>alert('하위메뉴 추가(샘플)'));
|
||||
$('#btnDeleteMenu').on('click', ()=>confirm('삭제하시겠습니까?') && alert('삭제(샘플)'));
|
||||
$('#btnReset').on('click', ()=>$('#menuForm')[0].reset());
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
301
public/tenant/permission/tenant_menu.php
Normal file
301
public/tenant/permission/tenant_menu.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php $CURRENT_SECTION='permission'; include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabUse" type="button">메뉴 사용 설정</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabWorkflow" type="button">작업공정 연동(읽기전용)</button></li>
|
||||
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabBoards" type="button">게시판 연동(읽기전용)</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content border border-top-0 p-3">
|
||||
|
||||
<!-- 메뉴 사용 설정 (작업공정/게시판 연동도 함께 표시) -->
|
||||
<div class="tab-pane fade show active" id="tabUse">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<div class="input-group" style="max-width:420px;">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드/출처 검색 (예: workflow, board)">
|
||||
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
||||
<button class="btn btn-outline-secondary" id="btnClear">초기화</button>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="btnAllOn">전체 허용</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="btnAllOff">전체 금지</button>
|
||||
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
||||
<table class="table table-hover align-middle small" id="menuTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:56px;">사용</th>
|
||||
<th>메뉴명</th>
|
||||
<th style="width:200px;">코드</th>
|
||||
<th style="width:260px;">경로</th>
|
||||
<!-- ▼ 신규: 출처 컬럼 -->
|
||||
<th style="width:120px;">출처</th>
|
||||
<th style="width:120px;">구독기본</th>
|
||||
<th style="width:120px;">강제고정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="form-text">
|
||||
* <b>출처</b>: system(일반), <span class="text-lowercase">workflow</span>(작업공정 연동), <span class="text-lowercase">board</span>(게시판 연동)<br>
|
||||
* <b>읽기전용</b> 항목은 직접 토글할 수 없으며, 상위 메뉴 ON/OFF에 따라 상태가 함께 변합니다.<br>
|
||||
* <b>강제고정</b>: 테넌트에서 끌 수 없음(최고관리자 정책).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업공정 연동(읽기전용) -->
|
||||
<div class="tab-pane fade" id="tabWorkflow">
|
||||
<div class="alert alert-info py-2 small">작업공정에서 생성/연동된 메뉴 목록입니다. 사용 여부는 상단 “메뉴 사용 설정”에서 구조 전체와 함께 확인하세요.</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="table-light"><tr><th>프로세스</th><th>연동 메뉴</th><th>메뉴 코드</th><th>경로</th></tr></thead>
|
||||
<tbody id="workflowRows"><!-- JS --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 게시판 연동(읽기전용) -->
|
||||
<div class="tab-pane fade" id="tabBoards">
|
||||
<div class="alert alert-info py-2 small">게시판 생성 시 연결된 메뉴 목록입니다.</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead class="table-light"><tr><th>게시판 코드</th><th>게시판 명</th><th>연동 메뉴</th><th>메뉴 코드</th></tr></thead>
|
||||
<tbody id="boardRows"><!-- JS --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===== 샘플 데이터 (실전에서는 AJAX 로드) =====
|
||||
// source: 'system' | 'workflow' | 'board'
|
||||
// readonly: true 이면 직접 토글 불가(체크박스 disabled)
|
||||
const MENU_DATA = [
|
||||
{id:1, parent_id:null, title:'대시보드', code:'dashboard', path:'/tenant/member/dashboard.php', forced:false, plan_default:true, source:'system', readonly:false},
|
||||
{id:2, parent_id:null, title:'수주', code:'order', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
||||
{id:21, parent_id:2, title:'수주 관리', code:'order.manage', path:'/tenant/order/manage.php', forced:false, plan_default:true, source:'system', readonly:false},
|
||||
{id:22, parent_id:2, title:'수주 등록/수정', code:'order.edit', path:'/tenant/order/edit.php', forced:false, plan_default:false, source:'system', readonly:false},
|
||||
|
||||
{id:3, parent_id:null, title:'생산', code:'production', path:null, forced:true, plan_default:true, source:'system', readonly:false},
|
||||
// ▼ 작업공정 연동 예시 (readonly)
|
||||
{id:31, parent_id:3, title:'작업 지시', code:'production.wi', path:'/tenant/production/work_instruction.php', forced:true, plan_default:true, source:'workflow', readonly:true},
|
||||
{id:32, parent_id:3, title:'스크린 작업', code:'production.screen', path:'/tenant/production/screen_work.php', forced:false, plan_default:true, source:'workflow', readonly:true},
|
||||
|
||||
{id:4, parent_id:null, title:'자재', code:'material', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
||||
// ▼ 작업공정/품질 연동 예시 (readonly)
|
||||
{id:41, parent_id:4, title:'수입 검사 대장', code:'material.insp', path:'/tenant/material/inspection_list.php', forced:false, plan_default:false, source:'workflow', readonly:true},
|
||||
|
||||
// ▼ 게시판 연동 예시 (readonly)
|
||||
{id:5, parent_id:null, title:'게시판', code:'board', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
||||
{id:51, parent_id:5, title:'공지사항', code:'board.notice', path:'/tenant/board/notice_list.php', forced:false, plan_default:true, source:'board', readonly:true},
|
||||
{id:52, parent_id:5, title:'Q&A', code:'board.qna', path:'/tenant/board/qna_list.php', forced:false, plan_default:false, source:'board', readonly:true},
|
||||
];
|
||||
|
||||
// 테넌트 현재 설정
|
||||
const TENANT_USE = {
|
||||
'dashboard': true,
|
||||
'order': true,
|
||||
'order.manage': true,
|
||||
'order.edit': false,
|
||||
'production': true,
|
||||
'production.wi': true,
|
||||
'production.screen': true,
|
||||
'material': true,
|
||||
'material.insp': false,
|
||||
'board': true,
|
||||
'board.notice': true,
|
||||
'board.qna': false,
|
||||
};
|
||||
|
||||
// ===== 유틸/트리 =====
|
||||
const byId = MENU_DATA.reduce((m,x)=> (m[x.id]=x,m),{});
|
||||
const childrenMap = MENU_DATA.reduce((m,x)=>{ (m[x.parent_id??0]??=([])).push(x); return m; },{});
|
||||
|
||||
function buildRows(parentId=null, depth=0, rows=[]){
|
||||
const list = childrenMap[parentId??0] || [];
|
||||
list.sort((a,b)=> (a.title > b.title ? 1 : -1));
|
||||
for (const node of list){
|
||||
const enabled = !!TENANT_USE[node.code];
|
||||
rows.push(renderRow(node, depth, enabled));
|
||||
buildRows(node.id, depth+1, rows);
|
||||
}
|
||||
return rows.join('');
|
||||
}
|
||||
|
||||
function badge(text, cls){ return `<span class="badge ${cls} ms-1">${text}</span>`; }
|
||||
|
||||
function renderRow(node, depth, enabled){
|
||||
const indent = ' '.repeat(depth*4) + (depth? '└ ' : '');
|
||||
const isReadOnly = !!node.readonly;
|
||||
const isForced = !!node.forced;
|
||||
|
||||
const disabledAttr = (isForced || isReadOnly) ? 'disabled' : '';
|
||||
const checkedAttr = (isForced ? 'checked' : (enabled ? 'checked' : ''));
|
||||
|
||||
const src = node.source || 'system';
|
||||
const sourceBadge =
|
||||
src==='workflow' ? badge('workflow','bg-warning text-dark') :
|
||||
src==='board' ? badge('board','bg-success') :
|
||||
badge('system','bg-secondary');
|
||||
|
||||
const forceBadge = isForced ? badge('강제','bg-secondary') : '';
|
||||
const roBadge = isReadOnly ? badge('읽기전용','bg-dark') : '';
|
||||
const planBadge = node.plan_default ? badge('플랜기본','bg-info text-dark') : '';
|
||||
|
||||
const path = node.path ? node.path : '-';
|
||||
|
||||
return `
|
||||
<tr data-id="${node.id}" data-code="${node.code}" data-parent="${node.parent_id??''}">
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="chk-use" ${checkedAttr} ${disabledAttr} title="${isReadOnly?'연동 항목은 직접 변경할 수 없습니다.':''}">
|
||||
</td>
|
||||
<td><span class="text-monospace">${indent}</span>${node.title}${forceBadge}${roBadge}</td>
|
||||
<td class="text-muted">${node.code}</td>
|
||||
<td class="text-muted">${path}</td>
|
||||
<td>${sourceBadge}</td>
|
||||
<td>${node.plan_default ? '기본 사용' : '-'}</td>
|
||||
<td>${isForced ? '강제 고정' : '-'}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function setChildren(code, on){
|
||||
const node = MENU_DATA.find(x=>x.code===code);
|
||||
if (!node) return;
|
||||
(childrenMap[node.id]||[]).forEach(ch=>{
|
||||
// 하위는 readonly여도 부모 변경에 따라 상태는 동기화 (직접토글만 막음)
|
||||
TENANT_USE[ch.code]=on;
|
||||
setChildren(ch.code, on);
|
||||
});
|
||||
}
|
||||
|
||||
function updateParents(code){
|
||||
const node = MENU_DATA.find(x=>x.code===code);
|
||||
if (!node || !node.parent_id) return;
|
||||
const parent = byId[node.parent_id];
|
||||
const kids = (childrenMap[parent.id]||[]);
|
||||
const states = kids.map(k=> !!TENANT_USE[k.code]);
|
||||
const allOn = states.every(Boolean);
|
||||
const allOff= states.every(s=>!s);
|
||||
if (!parent.forced){
|
||||
TENANT_USE[parent.code] = allOn ? true : (allOff ? false : true);
|
||||
}
|
||||
updateParents(parent.code);
|
||||
}
|
||||
|
||||
function applyIndeterminate(){
|
||||
$('#menuTable tbody tr').each(function(){
|
||||
const id = $(this).data('id');
|
||||
const code = $(this).data('code');
|
||||
const $chk = $(this).find('.chk-use')[0];
|
||||
if (!$chk) return;
|
||||
$chk.indeterminate = false;
|
||||
const kids = childrenMap[id]||[];
|
||||
if (kids.length){
|
||||
const onCnt = kids.filter(k=> TENANT_USE[k.code]).length;
|
||||
if (onCnt>0 && onCnt<kids.length){ $chk.indeterminate = true; }
|
||||
}
|
||||
// forced/readOnly 도구팁
|
||||
const node = byId[id];
|
||||
if (node.forced){ $chk.title = '강제 고정 메뉴입니다.'; }
|
||||
});
|
||||
}
|
||||
|
||||
function render(){
|
||||
$('#menuTable tbody').html( buildRows(null,0,[]) );
|
||||
// 체크박스 상태 반영
|
||||
$('#menuTable .chk-use').each(function(){
|
||||
const code = $(this).closest('tr').data('code');
|
||||
this.checked = !!TENANT_USE[code] || byId[$(this).closest('tr').data('id')].forced;
|
||||
});
|
||||
applyIndeterminate();
|
||||
|
||||
// 하단 탭(읽기전용 목록)도 같이 채움
|
||||
renderSubLists();
|
||||
}
|
||||
|
||||
function renderSubLists(){
|
||||
// 작업공정
|
||||
const wf = MENU_DATA.filter(x=>x.source==='workflow');
|
||||
$('#workflowRows').html(wf.map(x=>`
|
||||
<tr>
|
||||
<td>${x.title.includes('작업')? x.title.replace(/ 작업.*/,' 작업') : '프로세스'}</td>
|
||||
<td>${x.title}</td>
|
||||
<td class="text-muted">${x.code}</td>
|
||||
<td class="text-muted">${x.path||'-'}</td>
|
||||
</tr>`).join(''));
|
||||
|
||||
// 게시판
|
||||
const bd = MENU_DATA.filter(x=>x.source==='board');
|
||||
$('#boardRows').html(bd.map(x=>`
|
||||
<tr>
|
||||
<td class="text-muted">${x.code.split('.').pop().toUpperCase()}</td>
|
||||
<td>${x.title}</td>
|
||||
<td>${byId[x.parent_id]?.title||'-'}</td>
|
||||
<td class="text-muted">${x.code}</td>
|
||||
</tr>`).join(''));
|
||||
}
|
||||
|
||||
$(function(){
|
||||
render();
|
||||
|
||||
// 직접 토글
|
||||
$(document).on('change','.chk-use', function(){
|
||||
const $tr = $(this).closest('tr');
|
||||
const node = byId[$tr.data('id')];
|
||||
const code = node.code;
|
||||
|
||||
// 강제/읽기전용은 직접 토글 금지
|
||||
if (node.forced || node.readonly){
|
||||
this.checked = !!TENANT_USE[code]; // 원상복구
|
||||
return;
|
||||
}
|
||||
|
||||
TENANT_USE[code] = this.checked;
|
||||
setChildren(code, this.checked);
|
||||
updateParents(code);
|
||||
render();
|
||||
});
|
||||
|
||||
// 전체 허용/금지 (강제/readonly 제외)
|
||||
$('#btnAllOn').on('click', ()=>{
|
||||
MENU_DATA.forEach(n=>{ if(!n.forced && !n.readonly) TENANT_USE[n.code]=true; });
|
||||
render();
|
||||
});
|
||||
$('#btnAllOff').on('click', ()=>{
|
||||
MENU_DATA.forEach(n=>{ if(!n.forced && !n.readonly) TENANT_USE[n.code]=false; });
|
||||
render();
|
||||
});
|
||||
|
||||
// 저장
|
||||
$('#btnSave').on('click', ()=>{
|
||||
const payload = Object.fromEntries(MENU_DATA.map(n=> [n.code, !!TENANT_USE[n.code]]));
|
||||
console.log('SAVE', payload);
|
||||
alert('저장(샘플) — /api/permission/tenant_menu_save.php');
|
||||
});
|
||||
|
||||
// 검색: 메뉴명/코드/출처
|
||||
const doSearch = ()=>{
|
||||
const q = $('#searchInput').val().trim().toLowerCase();
|
||||
if (!q){ $('#menuTable tbody tr').show(); return; }
|
||||
$('#menuTable tbody tr').each(function(){
|
||||
const name = $(this).find('td:nth-child(2)').text().toLowerCase();
|
||||
const code = $(this).find('td:nth-child(3)').text().toLowerCase();
|
||||
const src = $(this).find('td:nth-child(5)').text().toLowerCase();
|
||||
$(this).toggle( name.includes(q) || code.includes(q) || src.includes(q) );
|
||||
});
|
||||
};
|
||||
$('#btnSearch').on('click', doSearch);
|
||||
$('#btnClear').on('click', ()=>{ $('#searchInput').val(''); $('#menuTable tbody tr').show(); });
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
226
public/tenant/permission/user.php
Normal file
226
public/tenant/permission/user.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
// 유저 권한 설정 (트리형 + 개인 예외 모드)
|
||||
$CURRENT_SECTION='permission';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
|
||||
<strong>유저 권한 설정 (개인 예외)</strong>
|
||||
<input class="form-control form-control-sm" style="width:220px;" id="userSearch" placeholder="이름/아이디 검색">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnUserFind">검색</button>
|
||||
<select class="form-select form-select-sm" id="userSelect" style="width:220px;">
|
||||
<option value="101">권혁성(kevin)</option>
|
||||
<option value="102">김슬기(sally)</option>
|
||||
</select>
|
||||
<div class="ms-auto small">
|
||||
예외 모드:
|
||||
<label class="ms-2 me-1"><input type="radio" name="mode" value="INHERIT" checked> INHERIT</label>
|
||||
<label class="me-1"><input type="radio" name="mode" value="ALLOW"> ALLOW</label>
|
||||
<label><input type="radio" name="mode" value="DENY"> DENY</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
<div class="input-group" style="max-width:420px;">
|
||||
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드 검색">
|
||||
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
||||
<button class="btn btn-outline-secondary" id="btnResetSearch">초기화</button>
|
||||
</div>
|
||||
<div class="small text-muted ms-auto">
|
||||
* INHERIT: 부서/역할 합집합만 반영(개인 변경 불가) / ALLOW: 개인 추가 허용 / DENY: 개인 차단(최우선)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
||||
<table class="table table-sm align-middle" id="permTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:320px;">메뉴</th>
|
||||
<th class="text-center" style="width:90px;">읽기<br><input type="checkbox" id="hdr_read"></th>
|
||||
<th class="text-center" style="width:90px;">쓰기<br><input type="checkbox" id="hdr_create"></th>
|
||||
<th class="text-center" style="width:90px;">수정<br><input type="checkbox" id="hdr_update"></th>
|
||||
<th class="text-center" style="width:90px;">삭제<br><input type="checkbox" id="hdr_delete"></th>
|
||||
<th class="text-center" style="width:90px;">결재<br><input type="checkbox" id="hdr_approve"></th>
|
||||
<th class="text-center" style="width:90px;">최종</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- rows --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 d-flex gap-2 justify-content-end">
|
||||
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.menu-name { font-size:14px; font-weight:500; }
|
||||
.menu-url { color:#6c757d; font-size:12px; }
|
||||
.indent { font-family: ui-monospace, Menlo, Consolas, monospace; color:#6c757d; }
|
||||
.badge-inherit{ background:#6c757d; }
|
||||
.badge-allow{ background:#0d6efd; }
|
||||
.badge-deny{ background:#dc3545; }
|
||||
</style>
|
||||
|
||||
<script src="/tenant/assets/js/permission_menu.js"></script>
|
||||
<script>
|
||||
// ===== 샘플 데이터 (실전: AJAX) =====
|
||||
const MENU_DATA = [
|
||||
{id:1,parent_id:null,title:'대시보드',code:'dashboard',path:'/tenant/member/dashboard.php',source:'system'},
|
||||
{id:2,parent_id:null,title:'수주',code:'order',path:null,source:'system'},
|
||||
{id:21,parent_id:2,title:'수주 관리',code:'order.manage',path:'/tenant/order/manage.php',source:'system'},
|
||||
{id:22,parent_id:2,title:'수주 등록/수정',code:'order.edit',path:'/tenant/order/edit.php',source:'system'},
|
||||
{id:3,parent_id:null,title:'생산',code:'production',path:null,source:'system'},
|
||||
{id:31,parent_id:3,title:'작업 지시',code:'production.wi',path:'/tenant/production/work_instruction.php',source:'workflow'},
|
||||
{id:32,parent_id:3,title:'스크린 작업',code:'production.screen',path:'/tenant/production/screen_work.php',source:'workflow'},
|
||||
{id:5,parent_id:null,title:'게시판',code:'board',path:null,source:'system'},
|
||||
{id:51,parent_id:5,title:'공지사항',code:'board.notice',path:'/tenant/board/notice_list.php',source:'board'},
|
||||
];
|
||||
const TENANT_USE = {dashboard:true, order:true,'order.manage':true,'order.edit':false,
|
||||
production:true,'production.wi':true,'production.screen':true,
|
||||
board:true,'board.notice':true};
|
||||
|
||||
// 상속 결과(부서+역할 합집합) — 실전에서는 서버에서 계산해서 내려주는 값
|
||||
const INHERITED = {
|
||||
// code: {read,create,update,delete,approve}
|
||||
'dashboard': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'order': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'order.manage': {read:true,create:true, update:true, delete:false,approve:false},
|
||||
'production': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'production.wi': {read:true,create:true, update:false,delete:false,approve:false},
|
||||
'production.screen': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'board': {read:true,create:false,update:false,delete:false,approve:false},
|
||||
'board.notice': {read:true,create:true, update:true, delete:false,approve:false},
|
||||
};
|
||||
|
||||
// 개인 예외 상태(페이지 state)
|
||||
let USER_MODE = 'INHERIT'; // INHERIT | ALLOW | DENY
|
||||
const USER_PERMS = {}; // code: {read,create,update,delete,approve} (ALLOW일 때만 의미)
|
||||
|
||||
PermissionMenu.setData(MENU_DATA, TENANT_USE);
|
||||
|
||||
function finalCell(code){
|
||||
const inh = INHERITED[code] || {read:false,create:false,update:false,delete:false,approve:false};
|
||||
if (USER_MODE === 'DENY') {
|
||||
return `<span class="badge badge-deny">DENY</span>`;
|
||||
}
|
||||
if (USER_MODE === 'INHERIT') {
|
||||
const any = Object.values(inh).some(Boolean);
|
||||
return `<span class="badge badge-inherit">${any?'상속 허용':'상속 없음'}</span>`;
|
||||
}
|
||||
// ALLOW 모드: 상속 OR 개인허용
|
||||
const up = USER_PERMS[code] || {};
|
||||
const merged = {
|
||||
read: !!(inh.read || up.read),
|
||||
create: !!(inh.create || up.create),
|
||||
update: !!(inh.update || up.update),
|
||||
delete: !!(inh.delete || up.delete),
|
||||
approve:!!(inh.approve|| up.approve),
|
||||
};
|
||||
const any = Object.values(merged).some(Boolean);
|
||||
return `<span class="badge badge-allow">${any?'허용됨':'없음'}</span>`;
|
||||
}
|
||||
|
||||
function renderRows(){
|
||||
const rows = PermissionMenu.buildRows((node) => {
|
||||
const inh = INHERITED[node.code] || {read:false,create:false,update:false,delete:false,approve:false};
|
||||
const up = (USER_PERMS[node.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
const disabled = (USER_MODE!=='ALLOW') ? 'disabled' : ''; // INHERIT/DENY 에선 비활성
|
||||
return ['read','create','update','delete','approve'].map(perm=>{
|
||||
const checked = (USER_MODE==='ALLOW') ? (up[perm]||false) : false;
|
||||
const title = (USER_MODE==='INHERIT') ? `상속값: ${inh[perm]?'허용':'-'}` :
|
||||
(USER_MODE==='DENY') ? '개인 DENY 상태' : '개인 허용값';
|
||||
return `<td class="text-center">
|
||||
<input type="checkbox" class="perm" data-perm="${perm}" ${checked?'checked':''} ${disabled} title="${title}">
|
||||
</td>`;
|
||||
}).join('') + `<td class="text-center">${finalCell(node.code)}</td>`;
|
||||
});
|
||||
document.querySelector('#permTable tbody').innerHTML = rows;
|
||||
applyIndeterminate();
|
||||
}
|
||||
|
||||
function applyIndeterminate(){
|
||||
// 헤더 요약(모드에 따라)
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if (!hdr) return;
|
||||
if (USER_MODE!=='ALLOW'){ // INHERIT/DENY: 헤더도 비활성
|
||||
hdr.indeterminate = false; hdr.checked = false; hdr.disabled = true;
|
||||
}else{
|
||||
hdr.disabled = false;
|
||||
const codes = PermissionMenu.activeCodes();
|
||||
const vals = codes.map(code => !!(USER_PERMS[code] && USER_PERMS[code][perm]));
|
||||
const onCnt = vals.filter(Boolean).length;
|
||||
hdr.indeterminate = (onCnt>0 && onCnt<vals.length);
|
||||
hdr.checked = (onCnt===vals.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 이벤트
|
||||
document.addEventListener('change', e=>{
|
||||
const el = e.target;
|
||||
if (el.name==='mode'){
|
||||
USER_MODE = el.value; renderRows(); return;
|
||||
}
|
||||
if (el.classList.contains('perm')){
|
||||
if (USER_MODE!=='ALLOW') return;
|
||||
const tr = el.closest('tr'); const code = tr.dataset.code; const perm = el.dataset.perm;
|
||||
(USER_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
USER_PERMS[code][perm] = el.checked;
|
||||
renderRows();
|
||||
}
|
||||
});
|
||||
['read','create','update','delete','approve'].forEach(perm=>{
|
||||
const hdr = document.querySelector('#hdr_'+perm);
|
||||
if(!hdr) return;
|
||||
hdr.addEventListener('change', ()=>{
|
||||
if (USER_MODE!=='ALLOW') return;
|
||||
PermissionMenu.activeCodes().forEach(code=>{
|
||||
(USER_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
||||
USER_PERMS[code][perm] = hdr.checked;
|
||||
});
|
||||
renderRows();
|
||||
});
|
||||
});
|
||||
|
||||
// 검색
|
||||
document.querySelector('#btnSearch').addEventListener('click', ()=>{
|
||||
const q = document.querySelector('#searchInput').value.trim().toLowerCase();
|
||||
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
||||
const name = tr.querySelector('td:first-child').innerText.toLowerCase();
|
||||
const code = tr.dataset.code.toLowerCase();
|
||||
tr.style.display = (q==='' || name.includes(q) || code.includes(q)) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
document.querySelector('#btnResetSearch').addEventListener('click', ()=>{
|
||||
document.querySelector('#searchInput').value=''; document.querySelectorAll('#permTable tbody tr').forEach(tr=> tr.style.display='');
|
||||
});
|
||||
|
||||
// 유저 선택/검색(샘플)
|
||||
document.querySelector('#btnUserFind').addEventListener('click', ()=>alert('유저 검색(샘플)'));
|
||||
document.querySelector('#userSelect').addEventListener('change', function(){
|
||||
alert('유저 권한/상속 로드(샘플): GET /api/permission/user_get.php?user_id='+this.value);
|
||||
// 실제: USER_MODE/USER_PERMS/INHERITED 로드 후 renderRows();
|
||||
});
|
||||
|
||||
// 저장
|
||||
document.querySelector('#btnSave').addEventListener('click', ()=>{
|
||||
const userId = document.querySelector('#userSelect').value;
|
||||
const payload = {
|
||||
user_id: userId,
|
||||
mode: USER_MODE, // INHERIT/ALLOW/DENY
|
||||
perms: USER_PERMS // ALLOW일 때만 의미; 서버에서 병합/적용
|
||||
};
|
||||
console.log('SAVE user permissions', payload);
|
||||
alert('저장(샘플): POST /api/permission/user_save.php');
|
||||
});
|
||||
|
||||
renderRows();
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
92
public/tenant/process/process_form.php
Normal file
92
public/tenant/process/process_form.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h2 class="m-0">공정 생성 / 수정</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/tenant/process/processes.php" class="btn btn-outline-secondary">취소</a>
|
||||
<button class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">공정명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" placeholder="예) 절곡">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" placeholder="선택 입력">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h4 class="mb-1">작업 리스트</h4>
|
||||
<p class="text-muted small mb-3">작업별로 시작/완료 피드백 필요 여부를 설정할 수 있습니다.</p>
|
||||
|
||||
<div id="task-list"></div>
|
||||
|
||||
<div class="text-end mt-2">
|
||||
<button id="btn-add-task" type="button" class="btn btn-outline-secondary">+ 작업 추가</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<template id="task-row-tpl">
|
||||
<div class="card task-row mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label mb-1">작업명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control task-name" placeholder="예) 절단작업">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input task-need-start" type="checkbox" id="">
|
||||
<label class="form-check-label">작업 시작 피드백 필요</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input task-need-end" type="checkbox" id="">
|
||||
<label class="form-check-label">작업 완료 피드백 필요</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<button type="button" class="btn btn-sm btn-danger btn-remove-task mt-4">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const list = document.getElementById('task-list');
|
||||
const tpl = document.getElementById('task-row-tpl').content;
|
||||
|
||||
function addRow(prefill){
|
||||
const node = document.importNode(tpl, true);
|
||||
if (prefill){
|
||||
if (prefill.name) node.querySelector('.task-name').value = prefill.name;
|
||||
if (prefill.needStart) node.querySelector('.task-need-start').checked = true;
|
||||
if (prefill.needEnd) node.querySelector('.task-need-end').checked = true;
|
||||
}
|
||||
node.querySelector('.btn-remove-task').addEventListener('click', e=>{
|
||||
e.target.closest('.task-row').remove();
|
||||
});
|
||||
list.appendChild(node);
|
||||
}
|
||||
|
||||
// seed
|
||||
addRow({name:'절단작업', needStart:true, needEnd:true});
|
||||
addRow({name:'미싱작업', needStart:false, needEnd:true});
|
||||
|
||||
document.getElementById('btn-add-task').addEventListener('click', ()=> addRow());
|
||||
})();
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
169
public/tenant/process/process_settings.php
Normal file
169
public/tenant/process/process_settings.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-1">
|
||||
<h2 class="m-0">공정: <strong>스크린</strong> 작업 지시 설정</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/tenant/process/processes.php" class="btn btn-outline-secondary">공정 목록</a>
|
||||
<button class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small">※ 이 화면은 실제 작업 지시 생성 시 노출될 <b>동적 필드</b>와 <b>투입 자재</b> 템플릿을 정의합니다.</p>
|
||||
|
||||
<ul class="nav nav-tabs" id="setTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#fields" type="button" role="tab">작업 지시용 필드 설정</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#items" type="button" role="tab">투입 품목/자재 설정</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content pt-3">
|
||||
<!-- 필드 설정 -->
|
||||
<div class="tab-pane fade show active" id="fields" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>필드 목록</strong>
|
||||
<button id="btn-add-field" type="button" class="btn btn-sm btn-outline-secondary">+ 필드 추가</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="fields-table" class="table table-bordered table-striped m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:16%;">필드명</th>
|
||||
<th style="width:12%;">유형</th>
|
||||
<th style="width:10%;">필수</th>
|
||||
<th>기본값/계산식</th>
|
||||
<th style="width:14%;">유효성</th>
|
||||
<th>설명</th>
|
||||
<th style="width:8%;">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-muted small">
|
||||
유형 예: 텍스트, 숫자, 숫자(mm), 날짜, 선택(셀렉트), 체크박스 등<br>
|
||||
유효성 예: text, number, integer, decimal, regex 등
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 품목/자재 설정 -->
|
||||
<div class="tab-pane fade" id="items" role="tabpanel">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>투입 품목/자재</strong>
|
||||
<button id="btn-add-item" type="button" class="btn btn-sm btn-outline-secondary">+ 품목 추가</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="items-table" class="table table-bordered table-striped m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:20%;">품목명</th>
|
||||
<th style="width:14%;">자재 유형</th>
|
||||
<th style="width:16%;">서브 자재 포함</th>
|
||||
<th>설명</th>
|
||||
<th style="width:8%;">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer text-muted small">
|
||||
자재 유형 예: 주 자재, 서브 자재<br>
|
||||
서브 자재 포함 여부가 O이면, 자식 자재를 후속 단계에서 매핑(계층화)합니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template id="field-row-tpl">
|
||||
<tr>
|
||||
<td><input type="text" class="form-control form-control-sm" placeholder="필드명"></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm">
|
||||
<option>텍스트</option><option>숫자</option><option>숫자(mm)</option>
|
||||
<option>날짜</option><option>선택(셀렉트)</option><option>체크박스</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input class="form-check-input required-chk" type="checkbox">
|
||||
</td>
|
||||
<td><input type="text" class="form-control form-control-sm" placeholder="예: qty * set_count 또는 고정값"></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm">
|
||||
<option>text</option><option>number</option><option>integer</option>
|
||||
<option>decimal</option><option>regex</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><input type="text" class="form-control form-control-sm" placeholder="참고 설명"></td>
|
||||
<td class="text-center"><button type="button" class="btn btn-sm btn-danger btn-remove-field">삭제</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template id="item-row-tpl">
|
||||
<tr>
|
||||
<td><input type="text" class="form-control form-control-sm" placeholder="품목명"></td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm">
|
||||
<option>주 자재</option><option>서브 자재</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="text-center"><input class="form-check-input has-sub" type="checkbox"></td>
|
||||
<td><input type="text" class="form-control form-control-sm" placeholder="설명"></td>
|
||||
<td class="text-center"><button type="button" class="btn btn-sm btn-danger btn-remove-item">삭제</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const seedFields = [
|
||||
{name:'일련번호', type:'텍스트', required:true, def:'', valid:'text', desc:'작업 식별번호'},
|
||||
{name:'층', type:'숫자', required:false, def:'', valid:'integer', desc:'작업 층수'},
|
||||
{name:'호수', type:'숫자', required:false, def:'', valid:'integer', desc:'작업 호수'},
|
||||
{name:'제작 가로사이즈', type:'숫자(mm)', required:true, def:'', valid:'decimal', desc:'가로 길이(mm)'},
|
||||
{name:'제작 세로사이즈', type:'숫자(mm)', required:true, def:'', valid:'decimal', desc:'세로 길이(mm)'},
|
||||
{name:'매수', type:'숫자', required:true, def:'자동 계산식 있음', valid:'integer', desc:'주문 매수'}
|
||||
];
|
||||
const seedItems = [
|
||||
{name:'절곡판', type:'주 자재', hasSub:true, desc:'주요 작업 재료'},
|
||||
{name:'조인트바', type:'서브 자재', hasSub:false, desc:'절곡판에 부속되는 자재'}
|
||||
];
|
||||
|
||||
function addFieldRow(p){
|
||||
const tpl = document.getElementById('field-row-tpl').content;
|
||||
const node = document.importNode(tpl,true);
|
||||
const tds = node.querySelectorAll('td');
|
||||
tds[0].querySelector('input').value = p?.name || '';
|
||||
tds[1].querySelector('select').value = p?.type || '텍스트';
|
||||
tds[2].querySelector('.required-chk').checked = p?.required || false;
|
||||
tds[3].querySelector('input').value = p?.def || '';
|
||||
tds[4].querySelector('select').value = p?.valid || 'text';
|
||||
tds[5].querySelector('input').value = p?.desc || '';
|
||||
node.querySelector('.btn-remove-field').addEventListener('click', e=> e.target.closest('tr').remove());
|
||||
document.querySelector('#fields-table tbody').appendChild(node);
|
||||
}
|
||||
function addItemRow(p){
|
||||
const tpl = document.getElementById('item-row-tpl').content;
|
||||
const node = document.importNode(tpl,true);
|
||||
const tds = node.querySelectorAll('td');
|
||||
tds[0].querySelector('input').value = p?.name || '';
|
||||
tds[1].querySelector('select').value = p?.type || '주 자재';
|
||||
tds[2].querySelector('.has-sub').checked = p?.hasSub || false;
|
||||
tds[3].querySelector('input').value = p?.desc || '';
|
||||
node.querySelector('.btn-remove-item').addEventListener('click', e=> e.target.closest('tr').remove());
|
||||
document.querySelector('#items-table tbody').appendChild(node);
|
||||
}
|
||||
|
||||
seedFields.forEach(addFieldRow);
|
||||
seedItems.forEach(addItemRow);
|
||||
document.getElementById('btn-add-field').addEventListener('click', ()=>addFieldRow());
|
||||
document.getElementById('btn-add-item').addEventListener('click', ()=>addItemRow());
|
||||
})();
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
92
public/tenant/process/processes.php
Normal file
92
public/tenant/process/processes.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
$CURRENT_SECTION = 'process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h2 class="m-0">공정 관리</h2>
|
||||
<a class="btn btn-primary" href="/tenant/process/process_form.php">+ 공정 생성</a>
|
||||
</div>
|
||||
|
||||
<!-- 검색 -->
|
||||
<form class="row g-2 align-items-end mb-3" role="search">
|
||||
<div class="col-auto">
|
||||
<label for="q" class="form-label">공정명</label>
|
||||
<input type="text" id="q" class="form-control" placeholder="공정명 검색">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="button" class="btn btn-outline-secondary">검색</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 리스트 -->
|
||||
<div class="border rounded p-2">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle m-0 tbl-proc">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:18%;">공정명</th>
|
||||
<th>설명</th>
|
||||
<th style="width:10%; text-align:center;">작업 수</th>
|
||||
<th style="width:14%;">등록일</th>
|
||||
<th style="width:10%;">수정</th>
|
||||
<th style="width:14%;">작업지시 설정</th>
|
||||
<th style="width:8%;">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="/tenant/process/tasks.php">스크린</a></td>
|
||||
<td>스크린 작업 공정</td>
|
||||
<td class="text-center">4</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/process_form.php">수정</a></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/tenant/process/process_settings.php">작업지시 설정</a>
|
||||
</td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/tenant/process/tasks.php">슬랫</a></td>
|
||||
<td>슬랫 작업 공정</td>
|
||||
<td class="text-center">3</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/process_form.php">수정</a></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/tenant/process/process_settings.php">작업지시 설정</a>
|
||||
</td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="/tenant/process/tasks.php">절곡</a></td>
|
||||
<td>절곡 작업 공정</td>
|
||||
<td class="text-center">4</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/process_form.php">수정</a></td>
|
||||
<td>
|
||||
<a class="btn btn-sm btn-outline-primary" href="/tenant/process/process_settings.php">작업지시 설정</a>
|
||||
</td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 스크린 작업지시현황과 동일한 컴팩트 테이블 톤 */
|
||||
.tbl-proc th{
|
||||
vertical-align: middle; text-align:center;
|
||||
white-space: nowrap; padding:.55rem .6rem;
|
||||
}
|
||||
.tbl-proc td{ vertical-align: middle; padding:.55rem .6rem; }
|
||||
.tbl-proc td:nth-child(1),
|
||||
.tbl-proc td:nth-child(2),
|
||||
.tbl-proc td:nth-child(4){ white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
40
public/tenant/process/task_form.php
Normal file
40
public/tenant/process/task_form.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h2 class="m-0">작업 생성 / 수정</h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="/tenant/process/tasks.php" class="btn btn-outline-secondary">취소</a>
|
||||
<button class="btn btn-primary">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">작업명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" placeholder="예) 포장">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="needStart">
|
||||
<label class="form-check-label" for="needStart">작업 시작 피드백 필요</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="needEnd">
|
||||
<label class="form-check-label" for="needEnd">작업 완료 피드백 필요</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" placeholder="선택 입력">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
55
public/tenant/process/tasks.php
Normal file
55
public/tenant/process/tasks.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h2 class="m-0">작업 관리 <small class="text-muted">(공정명: 스크린)</small></h2>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="/tenant/process/processes.php">공정 목록</a>
|
||||
<a class="btn btn-primary" href="/tenant/process/task_form.php">+ 작업 생성</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>작업명</th>
|
||||
<th style="width:14%;">작업 시작 피드백</th>
|
||||
<th style="width:14%;">작업 완료 피드백</th>
|
||||
<th style="width:14%;">등록일</th>
|
||||
<th style="width:8%;">수정</th>
|
||||
<th style="width:8%;">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>절단작업</td>
|
||||
<td>사용</td>
|
||||
<td>사용</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/task_form.php">수정</a></td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>미싱작업</td>
|
||||
<td>미사용</td>
|
||||
<td>사용</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/task_form.php">수정</a></td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>포장</td>
|
||||
<td>사용</td>
|
||||
<td>미사용</td>
|
||||
<td>2025-08-07</td>
|
||||
<td><a class="btn btn-sm btn-info" href="/tenant/process/task_form.php">수정</a></td>
|
||||
<td><button class="btn btn-sm btn-danger">삭제</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
289
public/tenant/product/bending.php
Normal file
289
public/tenant/product/bending.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
<form id="bendForm">
|
||||
<!-- 기본정보 -->
|
||||
<details open class="mb-3">
|
||||
<summary class="fw-semibold">기본정보 입력</summary>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">분류</label>
|
||||
<select class="form-select" id="bfCategory">
|
||||
<option>스크린</option><option>철재</option><option>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">품목명</label>
|
||||
<input class="form-control" id="bfName" placeholder="품목명">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">단위</label>
|
||||
<select class="form-select" id="bfUnit"><option>EA</option><option>mm</option></select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">검색태그</label>
|
||||
<input class="form-control" id="bfTags" placeholder="쉼표로 구분">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 고정 규격 -->
|
||||
<details open class="mb-3">
|
||||
<summary class="fw-semibold">고정 규격정보 입력</summary>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">폭</label>
|
||||
<input class="form-control" id="fxWidth" type="number" min="0" placeholder="폭(mm)">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">규격(가로×세로)</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="fxW" type="number" min="0" placeholder="가로">
|
||||
<span class="input-group-text">×</span>
|
||||
<input class="form-control" id="fxH" type="number" min="0" placeholder="세로">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">재질</label>
|
||||
<select class="form-select" id="fxMaterial">
|
||||
<option>선택</option><option>EGI</option><option>SUS</option><option>AL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">레이일폭</label>
|
||||
<input class="form-control" id="fxRail" type="number" min="0" placeholder="mm">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 기타정보 (+) -->
|
||||
<details class="mb-3">
|
||||
<summary class="fw-semibold d-flex align-items-center">기타정보 입력
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-2" id="btnAddMeta">+</button>
|
||||
</summary>
|
||||
<div id="metaRows" class="mt-2"></div>
|
||||
</details>
|
||||
|
||||
<!-- 절곡 이미지/스텝 -->
|
||||
<details open class="mb-4">
|
||||
<summary class="fw-semibold">절곡 이미지 & 스텝</summary>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<!-- 스텝 테이블 -->
|
||||
<div class="col-md-7">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:60px;">번호</th>
|
||||
<th>입력 길이</th>
|
||||
<th>연신율(%)</th>
|
||||
<th>연신 후</th>
|
||||
<th>합계</th>
|
||||
<th style="width:90px;">A각</th>
|
||||
<th style="width:110px;">옵션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stepBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnAddCol">마지막 열 추가</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnDelCol">마지막 열 삭제</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" id="btnClear">모든칸 비우기</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 캔버스 -->
|
||||
<div class="col-md-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong>도면 캔버스</strong>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="file" id="bgImage" accept="image/*" class="form-control form-control-sm">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnDrawLine">선</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnDrawRect">사각</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnClearCanvas">지우기</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded p-1">
|
||||
<canvas id="cv" width="420" height="300" style="display:block; background:#fff;"></canvas>
|
||||
</div>
|
||||
<small class="text-muted">이미지를 올리면 배경으로 표시됩니다. 간단한 선/사각 도구 제공.</small>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-outline-secondary" type="button" id="btnPreview">미리보기</button>
|
||||
<button class="btn btn-primary" type="submit">저장</button>
|
||||
</div>
|
||||
|
||||
<textarea id="preview" class="form-control mt-3" rows="8" style="display:none;"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.meta-row .btn{ --bs-btn-padding-y:.15rem; --bs-btn-padding-x:.4rem; --bs-btn-font-size:.75rem; }
|
||||
td input[type=number]{ width: 90px; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
// ===== 기타정보 행 =====
|
||||
function metaRowTpl(key='', val='', unit=''){
|
||||
return `<div class="row g-2 align-items-center meta-row mb-2">
|
||||
<div class="col-4"><input class="form-control form-control-sm meta-key" placeholder="규격"></div>
|
||||
<div class="col-5"><input class="form-control form-control-sm meta-val" placeholder="값"></div>
|
||||
<div class="col-2"><input class="form-control form-control-sm meta-unit" placeholder="단위"></div>
|
||||
<div class="col-1 text-end"><button type="button" class="btn btn-outline-danger btn-sm btnMetaDel">–</button></div>
|
||||
</div>`;
|
||||
}
|
||||
$('#btnAddMeta').on('click', ()=> $('#metaRows').append(metaRowTpl()));
|
||||
$('#metaRows').on('click','.btnMetaDel', function(){ $(this).closest('.meta-row').remove(); });
|
||||
|
||||
// ===== 절곡 스텝 테이블 =====
|
||||
let COLS = 2; // 기본 2열
|
||||
function drawTable(){
|
||||
const rows=[];
|
||||
const headers=['번호','입력','연신율','연신후','합계','A각','옵션']; // 설명용
|
||||
for(let r=0;r<7;r++){
|
||||
const cells=[];
|
||||
if (r===0){ // 번호
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-control form-control-sm step-no" type="number" min="1" value="${c+1}">`);
|
||||
}else if(r===1){ // 입력
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-control form-control-sm step-in" type="number" min="0" value="0">`);
|
||||
}else if(r===2){ // 연신율
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-control form-control-sm step-strain" type="number" step="0.01" value="0">`);
|
||||
}else if(r===3){ // 연신율계산 후 (= 입력 × (1 + 연신율/100))
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-control form-control-sm step-after" type="number" readonly value="0">`);
|
||||
}else if(r===4){ // 합계 (누적)
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-control form-control-sm step-sum" type="number" readonly value="0">`);
|
||||
}else if(r===5){ // 음영
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-check-input step-dark" type="checkbox">`);
|
||||
}else if(r===6){ // A각 표시
|
||||
for(let c=0;c<COLS;c++) cells.push(`<input class="form-check-input step-a" type="checkbox">`);
|
||||
}
|
||||
rows.push(`<tr>${r===0?'<th>번호</th>': r===1?'<th>입력</th>': r===2?'<th>연신율(%)</th>': r===3?'<th>연신 후</th>': r===4?'<th>합계</th>': r===5?'<th>음영</th>':'<th>A각 표시</th>'}
|
||||
${cells.map(td=>`<td>${td}</td>`).join('')}
|
||||
</tr>`);
|
||||
}
|
||||
$('#stepBody').html(rows.join(''));
|
||||
recalc();
|
||||
}
|
||||
function recalc(){
|
||||
let running = 0;
|
||||
$('#stepBody tr').each(function(idx,tr){
|
||||
if (idx<3){ /* 입력·연신율 줄 계산은 아래에서 */ }
|
||||
if (idx===3 || idx===4){ // 연신후 / 합계 재계산
|
||||
const row = $(tr);
|
||||
row.find('td').each(function(col){
|
||||
const $in = $('#stepBody .step-in').eq(col);
|
||||
const $st = $('#stepBody .step-strain').eq(col);
|
||||
const after = (+$in.val()||0) * (1 + (+$st.val()||0)/100);
|
||||
if (idx===3){
|
||||
row.find('.step-after').eq(col).val(after.toFixed(2));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 합계(누적)
|
||||
const afters = $('#stepBody .step-after');
|
||||
const sums = $('#stepBody .step-sum');
|
||||
let acc=0;
|
||||
afters.each(function(i){
|
||||
acc += +($(this).val()||0);
|
||||
sums.eq(i).val(acc.toFixed(2));
|
||||
});
|
||||
}
|
||||
$('#stepBody').on('input', '.step-in, .step-strain', recalc);
|
||||
|
||||
$('#btnAddCol').on('click', ()=>{ COLS++; drawTable(); });
|
||||
$('#btnDelCol').on('click', ()=>{ COLS=Math.max(1,COLS-1); drawTable(); });
|
||||
$('#btnClear').on('click', ()=>{
|
||||
$('#stepBody input[type=number]').val(0);
|
||||
$('#stepBody input[type=checkbox]').prop('checked',false);
|
||||
recalc();
|
||||
});
|
||||
|
||||
// ===== 캔버스 (아주 간단한 드로잉) =====
|
||||
const cv = document.getElementById('cv');
|
||||
const ctx = cv.getContext('2d');
|
||||
let bgImg = null;
|
||||
function clearCanvas(){
|
||||
ctx.clearRect(0,0,cv.width,cv.height);
|
||||
if (bgImg){ ctx.drawImage(bgImg, 0,0, cv.width, cv.height); }
|
||||
}
|
||||
$('#bgImage').on('change', function(){
|
||||
const f=this.files[0]; if(!f) return;
|
||||
const img=new Image();
|
||||
img.onload=function(){ bgImg=img; clearCanvas(); };
|
||||
img.src=URL.createObjectURL(f);
|
||||
});
|
||||
|
||||
let mode='line', drawing=false, sx=0, sy=0;
|
||||
$('#btnDrawLine').on('click', ()=> mode='line');
|
||||
$('#btnDrawRect').on('click', ()=> mode='rect');
|
||||
$('#btnClearCanvas').on('click', ()=>{ bgImg=null; clearCanvas(); });
|
||||
|
||||
cv.addEventListener('mousedown', e=>{ drawing=true; const r=cv.getBoundingClientRect(); sx=e.clientX-r.left; sy=e.clientY-r.top; });
|
||||
cv.addEventListener('mouseup', e=>{
|
||||
if(!drawing) return; drawing=false;
|
||||
const r=cv.getBoundingClientRect(); const x=e.clientX-r.left, y=e.clientY-r.top;
|
||||
ctx.strokeStyle='#333'; ctx.lineWidth=2; clearCanvas();
|
||||
if(mode==='line'){ ctx.beginPath(); ctx.moveTo(sx,sy); ctx.lineTo(x,y); ctx.stroke(); }
|
||||
else{ const w=x-sx, h=y-sy; ctx.strokeRect(sx,sy,w,h); }
|
||||
});
|
||||
|
||||
// ===== 미리보기/저장 =====
|
||||
function collect(){
|
||||
// 메타
|
||||
const metas=[]; $('#metaRows .meta-row').each(function(){
|
||||
metas.push({
|
||||
key: $(this).find('.meta-key').val().trim(),
|
||||
value: $(this).find('.meta-val').val().trim(),
|
||||
unit: $(this).find('.meta-unit').val().trim(),
|
||||
});
|
||||
});
|
||||
// 스텝
|
||||
const no = $('#stepBody .step-no').map((i,e)=>+e.value||0).get();
|
||||
const inp = $('#stepBody .step-in').map((i,e)=>+e.value||0).get();
|
||||
const str = $('#stepBody .step-strain').map((i,e)=>+e.value||0).get();
|
||||
const aft = $('#stepBody .step-after').map((i,e)=>+e.value||0).get();
|
||||
const sum = $('#stepBody .step-sum').map((i,e)=>+e.value||0).get();
|
||||
const dark = $('#stepBody .step-dark').map((i,e)=>e.checked).get();
|
||||
const a = $('#stepBody .step-a').map((i,e)=>e.checked).get();
|
||||
const steps = no.map((_,i)=>({ no:no[i], length:inp[i], strain:str[i], after:aft[i], acc:sum[i], shade:dark[i], a_mark:a[i] }));
|
||||
|
||||
// 캔버스 스냅샷
|
||||
const snapshot = cv.toDataURL('image/png');
|
||||
|
||||
return {
|
||||
base:{ category:$('#bfCategory').val(), name:$('#bfName').val(), unit:$('#bfUnit').val(), tags:$('#bfTags').val() },
|
||||
fixed:{ width:+$('#fxWidth').val()||0, w:+$('#fxW').val()||0, h:+$('#fxH').val()||0, material:$('#fxMaterial').val(), rail:+$('#fxRail').val()||0 },
|
||||
metas, steps, cols: COLS, snapshot
|
||||
};
|
||||
}
|
||||
|
||||
$('#btnPreview').on('click', ()=>{
|
||||
const txt = JSON.stringify(collect(), null, 2);
|
||||
$('#preview').val(txt).show().focus();
|
||||
});
|
||||
|
||||
$('#bendForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const payload = collect();
|
||||
console.log('[BENDING SAVE]', payload);
|
||||
alert('절곡 저장(모의). 콘솔을 확인하세요.');
|
||||
// 실제는 $.post('/tenant/api/product/save_bending.php', payload)
|
||||
});
|
||||
|
||||
// 초기
|
||||
$('#metaRows').append(metaRowTpl());
|
||||
drawTable();
|
||||
clearCanvas();
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/product/bom_combined.php
Normal file
8
public/tenant/product/bom_combined.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
295
public/tenant/product/bom_editor.php
Normal file
295
public/tenant/product/bom_editor.php
Normal file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='item'; // 상단 네비 하이라이트 용도
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
|
||||
<!-- 상단 탭 -->
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/material/list.php">자재 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/product/model_list.php">모델 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="/tenant/product/bom_editor.php">BOM 관리</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- 좌측: 모델 + 자재리스트 -->
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<strong>제품(모델) 선택</strong>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="modelFilter" class="form-select form-select-sm" style="width:120px;">
|
||||
<option value="">전체</option>
|
||||
<option value="인정">인정</option>
|
||||
<option value="비인정">비인정</option>
|
||||
</select>
|
||||
<input id="modelSearch" class="form-control form-control-sm" placeholder="검색">
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group" id="modelList" style="max-height:240px; overflow:auto;"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>자재/부품(해당 제품용)</strong></div>
|
||||
<div class="card-body p-0">
|
||||
<div style="max-height:340px; overflow:auto;">
|
||||
<table class="table table-sm m-0">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:60px;">분류</th><th>이름</th><th style="width:70px;">단위</th></tr>
|
||||
</thead>
|
||||
<tbody id="materialList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 트리 + 요약 -->
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<strong>편집 트리</strong>
|
||||
<small class="text-muted ms-2">(루트=선택한 제품)</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnExpand">모두 펼치기</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="btnCollapse">모두 닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="treeRoot"></div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<div class="small text-muted">
|
||||
<span class="me-3">합계수량(샘플): <b id="sumQty">0</b></span>
|
||||
<span class="me-3">노드수: <b id="sumNodes">0</b></span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="btnPreview">JSON 미리보기</button>
|
||||
<button class="btn btn-primary btn-sm" id="btnSave">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea id="jsonPreview" class="form-control" rows="6" style="display:none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.node { border:1px solid #e5e7eb; border-radius:10px; padding:.6rem .75rem; margin-bottom:.5rem; }
|
||||
.node-header{display:flex; align-items:center; gap:.5rem}
|
||||
.node-children{margin-left:1.25rem; margin-top:.5rem}
|
||||
.badge-type{font-size:.7rem}
|
||||
.node-tools .btn{ --bs-btn-padding-y: .1rem; --bs-btn-padding-x: .35rem; --bs-btn-font-size: .75rem; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
// ===== 샘플 데이터 =====
|
||||
const MODELS = [
|
||||
{id:201, name:'KSS01', status:'인정'},
|
||||
{id:202, name:'KSS02', status:'비인정'},
|
||||
{id:203, name:'KSE01', status:'인정'},
|
||||
{id:204, name:'KWE01', status:'비인정'},
|
||||
];
|
||||
const MATERIALS = [
|
||||
{id:1, type:'material', category:'자재', name:'EGI 1.5T', unit:'EA'},
|
||||
{id:2, type:'material', category:'자재', name:'SUS 1.2T', unit:'EA'},
|
||||
{id:3, type:'part', category:'부품', name:'가이드레일', unit:'M'},
|
||||
{id:4, type:'part', category:'부품', name:'모터', unit:'EA'},
|
||||
];
|
||||
|
||||
// 트리 데이터 예시(모델별)
|
||||
const TREE_BY_MODEL = {
|
||||
201: { id:'root-201', type:'bom', name:'KSS01', qty:1, unit:'EA', note:'',
|
||||
children:[
|
||||
{id:'b-10',type:'bom', name:'셔터박스', qty:1, unit:'EA', note:'', children:[
|
||||
{id:'m-1', type:'material', ref:1, name:'EGI 1.5T', qty:2, unit:'EA', note:''}
|
||||
]},
|
||||
{id:'p-3', type:'part', ref:3, name:'가이드레일', qty:3, unit:'M', note:''}
|
||||
]
|
||||
},
|
||||
202: { id:'root-202', type:'bom', name:'KSS02', qty:1, unit:'EA', note:'', children:[] },
|
||||
203: { id:'root-203', type:'bom', name:'KSE01', qty:1, unit:'EA', note:'', children:[] },
|
||||
204: { id:'root-204', type:'bom', name:'KWE01', qty:1, unit:'EA', note:'', children:[] },
|
||||
};
|
||||
|
||||
// ===== 좌측 패널 렌더 =====
|
||||
function renderModels(){
|
||||
const f = $('#modelFilter').val();
|
||||
const kw = ($('#modelSearch').val()||'').toLowerCase();
|
||||
const list = MODELS.filter(m=>{
|
||||
const ok1 = !f || m.status===f;
|
||||
const ok2 = !kw || m.name.toLowerCase().includes(kw);
|
||||
return ok1 && ok2;
|
||||
});
|
||||
const html = list.map(m =>
|
||||
`<a href="#" class="list-group-item list-group-item-action model-item" data-id="${m.id}">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span>${m.name}</span>
|
||||
<span class="badge ${m.status==='인정'?'bg-primary':'bg-secondary'}">${m.status}</span>
|
||||
</div>
|
||||
</a>`
|
||||
).join('');
|
||||
$('#modelList').html(html);
|
||||
}
|
||||
function renderMaterials(){
|
||||
const rows = MATERIALS.map(x=> `<tr>
|
||||
<td>${x.category}</td><td>${x.name}</td><td>${x.unit}</td></tr>`).join('');
|
||||
$('#materialList').html(rows);
|
||||
}
|
||||
|
||||
// ===== 트리 렌더 =====
|
||||
function nodeHeader(n){
|
||||
const label = n.type==='bom' ? n.name
|
||||
: (n.name || (n.type==='material'?'(자재)':'(부품)'));
|
||||
const badge = n.type==='bom' ? 'BOM' : (n.type==='material'?'MAT':'PART');
|
||||
const refInfo = n.ref ? `<span class="text-muted small ms-1">#${n.ref}</span>` : '';
|
||||
return `
|
||||
<div class="node-header">
|
||||
<span class="badge rounded-pill bg-info-subtle text-dark border badge-type">${badge}</span>
|
||||
<strong>${label}</strong>${refInfo}
|
||||
<div class="ms-auto node-tools">
|
||||
<button class="btn btn-light btn-sm btnAdd" data-kind="bom" title="BOM 추가">+B</button>
|
||||
<button class="btn btn-light btn-sm btnAdd" data-kind="material" title="자재 추가">+M</button>
|
||||
<button class="btn btn-light btn-sm btnAdd" data-kind="part" title="부품 추가">+P</button>
|
||||
<button class="btn btn-outline-danger btn-sm btnDel" title="삭제">–</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-3"><input class="form-control form-control-sm inpQty" type="number" min="0" value="${n.qty||1}" placeholder="수량"></div>
|
||||
<div class="col-2"><input class="form-control form-control-sm inpUnit" value="${n.unit||''}" placeholder="단위"></div>
|
||||
<div class="col"><input class="form-control form-control-sm inpNote" value="${n.note||''}" placeholder="메모"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
function renderNode(n){
|
||||
const hasChildren = Array.isArray(n.children) && n.children.length;
|
||||
const childHtml = hasChildren ? n.children.map(ch => renderNode(ch)).join('') : '';
|
||||
return `
|
||||
<div class="node" data-id="${n.id}">
|
||||
${nodeHeader(n)}
|
||||
<div class="node-children">${childHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
function bindNodeEvents(rootEl, data){
|
||||
// 입력 변경 반영
|
||||
rootEl.find('.inpQty').on('input', function(){
|
||||
const id = $(this).closest('.node').data('id');
|
||||
const node = findNodeById(data, id);
|
||||
node.qty = +this.value || 0; computeSummary(data);
|
||||
});
|
||||
rootEl.find('.inpUnit').on('input', function(){
|
||||
const id = $(this).closest('.node').data('id');
|
||||
findNodeById(data, id).unit = this.value;
|
||||
});
|
||||
rootEl.find('.inpNote').on('input', function(){
|
||||
const id = $(this).closest('.node').data('id');
|
||||
findNodeById(data, id).note = this.value;
|
||||
});
|
||||
|
||||
// 추가/삭제
|
||||
rootEl.find('.btnAdd').on('click', function(){
|
||||
const kind = $(this).data('kind'); // bom/material/part
|
||||
const id = $(this).closest('.node').data('id');
|
||||
const parent = findNodeById(data, id);
|
||||
parent.children = parent.children || [];
|
||||
const newId = 'n-' + Math.random().toString(36).slice(2,8);
|
||||
const base = {id:newId, type:kind, qty:1, unit:'EA', note:'', children:[]};
|
||||
if (kind!=='bom'){
|
||||
// 참조 선택(샘플: 첫 항목 고정 or 프롬프트)
|
||||
const ref = prompt(kind.toUpperCase()+' 참조 ID(예: 1):', '1');
|
||||
base.ref = parseInt(ref,10)||1;
|
||||
const refItem = MATERIALS.find(m=>m.id===base.ref) || {name:kind.toUpperCase()};
|
||||
base.name = refItem.name;
|
||||
base.unit = refItem.unit||'EA';
|
||||
} else {
|
||||
base.name = prompt('BOM 이름:', '서브어셈블리') || 'BOM';
|
||||
}
|
||||
parent.children.push(base);
|
||||
drawTree(data);
|
||||
});
|
||||
rootEl.find('.btnDel').on('click', function(){
|
||||
const id = $(this).closest('.node').data('id');
|
||||
if (!confirm('삭제할까요?')) return;
|
||||
removeNode(data, id);
|
||||
drawTree(data);
|
||||
});
|
||||
}
|
||||
function findNodeById(node, id){
|
||||
if (node.id===id) return node;
|
||||
if (!node.children) return null;
|
||||
for (const ch of node.children){
|
||||
const f = findNodeById(ch, id);
|
||||
if (f) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function removeNode(node, id){
|
||||
if (!node.children) return false;
|
||||
const idx = node.children.findIndex(c=>c.id===id);
|
||||
if (idx>=0){ node.children.splice(idx,1); return true; }
|
||||
return node.children.some(c=>removeNode(c,id));
|
||||
}
|
||||
|
||||
function computeSummary(root){
|
||||
let nodes=0, qty=0;
|
||||
(function walk(n){
|
||||
nodes++; qty += +(n.qty||0);
|
||||
(n.children||[]).forEach(walk);
|
||||
})(root);
|
||||
$('#sumNodes').text(nodes);
|
||||
$('#sumQty').text(qty);
|
||||
}
|
||||
|
||||
// 그리기
|
||||
let CURRENT_MODEL_ID = MODELS[0].id;
|
||||
let CURRENT_TREE = JSON.parse(JSON.stringify(TREE_BY_MODEL[CURRENT_MODEL_ID]));
|
||||
function drawTree(data){
|
||||
$('#treeRoot').html( renderNode(data) );
|
||||
bindNodeEvents($('#treeRoot'), data);
|
||||
computeSummary(data);
|
||||
}
|
||||
|
||||
// ===== 이벤트 =====
|
||||
$('#modelFilter, #modelSearch').on('input', renderModels);
|
||||
$('#modelList').on('click', '.model-item', function(e){
|
||||
e.preventDefault();
|
||||
const id = parseInt($(this).data('id'),10);
|
||||
CURRENT_MODEL_ID = id;
|
||||
CURRENT_TREE = JSON.parse(JSON.stringify(TREE_BY_MODEL[id] || {id:'root-'+id,type:'bom',name:'NEW',qty:1,unit:'EA',children:[]}));
|
||||
drawTree(CURRENT_TREE);
|
||||
$('#modelList .model-item').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
});
|
||||
|
||||
$('#btnExpand').on('click', ()=> $('#treeRoot .node-children').show());
|
||||
$('#btnCollapse').on('click', ()=> $('#treeRoot .node-children').hide());
|
||||
|
||||
$('#btnPreview').on('click', ()=>{
|
||||
const txt = JSON.stringify(CURRENT_TREE, null, 2);
|
||||
$('#jsonPreview').val(txt).show().focus();
|
||||
});
|
||||
|
||||
$('#btnSave').on('click', ()=>{
|
||||
const payload = {
|
||||
model_id: CURRENT_MODEL_ID,
|
||||
tree: CURRENT_TREE
|
||||
};
|
||||
console.log('[SAVE] payload', payload);
|
||||
alert('저장(모의). 콘솔을 확인하세요.');
|
||||
// 실제는 $.post('/tenant/api/product/save_bom.php', payload)
|
||||
});
|
||||
|
||||
// 초기 렌더
|
||||
renderModels(); renderMaterials();
|
||||
$('#modelList .model-item').first().addClass('active');
|
||||
drawTree(CURRENT_TREE);
|
||||
})(jQuery);
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/product/code_lot.php
Normal file
8
public/tenant/product/code_lot.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
310
public/tenant/product/model_list.php
Normal file
310
public/tenant/product/model_list.php
Normal file
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
// 모델관리
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:24px;">
|
||||
|
||||
<!-- 탭 -->
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/material/list.php">자재 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link active" href="/tenant/product/model_list.php">모델 관리</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/tenant/product/bom_editor.php">BOM 관리</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- 툴바 -->
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="text-muted">분류</label>
|
||||
<select class="form-select form-select-sm" id="filterCategory" style="width:160px;">
|
||||
<option value="">전체</option>
|
||||
<option value="스크린">스크린</option>
|
||||
<option value="철재">철재</option>
|
||||
<option value="유리">유리</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="max-width:340px;">
|
||||
<input type="text" class="form-control form-control-sm" id="keyword" placeholder="모델명/로트코드 검색">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="btnSearch">Search</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="text-muted">표시</label>
|
||||
<select id="pageSize" class="form-select form-select-sm" style="width:80px;">
|
||||
<option>10</option><option selected>20</option><option>30</option><option>50</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="ms-auto small text-muted" id="totalInfo">총 0건</div>
|
||||
<button class="btn btn-primary btn-sm" id="btnOpenCreate">신규등록</button>
|
||||
</div>
|
||||
|
||||
<!-- 리스트 -->
|
||||
<div class="table-responsive border rounded">
|
||||
<table class="table table-hover table-striped align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:70px;">NO</th>
|
||||
<th style="width:140px;">분류</th>
|
||||
<th>모델명</th>
|
||||
<th style="width:140px;">로트 코드</th>
|
||||
<th style="width:140px;">등록일</th>
|
||||
<th style="width:160px;" class="text-center">수정/삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="listBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 페이징 -->
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
<nav id="pagerNav" aria-label="Page navigation"></nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모달 (부트스트랩 + 폴백) -->
|
||||
<div class="modal fade" id="modelModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<form class="modal-content needs-validation" id="modelForm" novalidate>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modelModalTitle">모델 등록</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기" id="btnCloseX"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="mdlId">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">분류 <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="mdlCategory" required>
|
||||
<option value="">선택</option>
|
||||
<option>스크린</option>
|
||||
<option>철재</option>
|
||||
<option>유리</option>
|
||||
</select>
|
||||
<div class="invalid-feedback">분류를 선택하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">모델명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="mdlName" required>
|
||||
<div class="invalid-feedback">모델명을 입력하세요.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">로트 코드</label>
|
||||
<input type="text" class="form-control" id="mdlLot" placeholder="예: SA, WE">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">내용</label>
|
||||
<input type="text" class="form-control" id="mdlDesc" placeholder="간단 설명">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="me-auto small text-muted" id="modalHint"></div>
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal" id="btnClose">닫기</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSubmit">등록</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 토스트 -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index:1080">
|
||||
<div id="snack" class="toast align-items-center text-bg-primary border-0" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="snackMsg">Saved</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table > :not(caption) > * > * { vertical-align: middle; }
|
||||
/* 부트스트랩 JS 없는 경우의 간단 폴백 */
|
||||
.modal.fallback-show { display:block; background:rgba(0,0,0,.45); }
|
||||
.modal.fallback-show .modal-dialog {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// ===== 샘플 데이터 =====
|
||||
function buildSample(count=64){
|
||||
const cats=['스크린','철재','유리'], lots=['SS','SA','SE','WE','TS','TE','DS'];
|
||||
const arr=[]; for(let i=1;i<=count;i++){
|
||||
arr.push({
|
||||
id:i,
|
||||
category: cats[i%cats.length],
|
||||
name: (i%7===0)? `${cats[i%cats.length]} 비인정` : `${cats[i%cats.length]} K${String(i).padStart(2,'0')}`,
|
||||
lot: lots[i%lots.length],
|
||||
created_at: new Date(Date.now()-86400000*i).toISOString().slice(0,10),
|
||||
desc:''
|
||||
});
|
||||
} return arr;
|
||||
}
|
||||
let MODELS = buildSample(97);
|
||||
|
||||
// ===== 상태/유틸 =====
|
||||
let PAGE=1, SIZE=20;
|
||||
const $=s=>document.querySelector(s), $$=s=>Array.from(document.querySelectorAll(s));
|
||||
const hasBS = ()=> !!window.bootstrap;
|
||||
const snack = ()=> hasBS() ? new bootstrap.Toast('#snack') : { show(){ /* no-op */ } };
|
||||
|
||||
function getFiltered(){
|
||||
const cat=$('#filterCategory').value.trim();
|
||||
const kw=$('#keyword').value.trim().toLowerCase();
|
||||
return MODELS.filter(m=>{
|
||||
const okCat=!cat || m.category===cat;
|
||||
const hay=(m.name+' '+(m.lot||'')).toLowerCase();
|
||||
const okKw=!kw || hay.includes(kw);
|
||||
return okCat && okKw;
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 렌더 =====
|
||||
function renderList(){
|
||||
const data=getFiltered();
|
||||
const total=data.length;
|
||||
const pages=Math.max(1, Math.ceil(total/SIZE));
|
||||
if(PAGE>pages) PAGE=pages;
|
||||
|
||||
const start=(PAGE-1)*SIZE, end=start+SIZE;
|
||||
$('#listBody').innerHTML = data.slice(start,end).map((m,i)=>`
|
||||
<tr data-id="${m.id}">
|
||||
<td>${start+i+1}</td>
|
||||
<td>${m.category}</td>
|
||||
<td>${m.name}</td>
|
||||
<td>${m.lot||''}</td>
|
||||
<td>${m.created_at}</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-outline-primary me-1 btnEdit">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btnDel">삭제</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('') || `<tr><td colspan="6" class="text-center text-muted py-4">데이터가 없습니다.</td></tr>`;
|
||||
|
||||
$('#totalInfo').textContent=`총 ${total.toLocaleString()}건 · ${total?(start+1):0}-${Math.min(end,total)} 표시`;
|
||||
|
||||
const win=7; let sp=Math.max(1,PAGE-Math.floor(win/2)), ep=Math.min(pages, sp+win-1); sp=Math.max(1,ep-win+1);
|
||||
let html=`<ul class="pagination pagination-sm m-0">`;
|
||||
html+=`<li class="page-item ${PAGE===1?'disabled':''}"><a class="page-link" href="#" data-go="first">«</a></li>`;
|
||||
html+=`<li class="page-item ${PAGE===1?'disabled':''}"><a class="page-link" href="#" data-go="${PAGE-1}">‹</a></li>`;
|
||||
for(let p=sp;p<=ep;p++){ html+=`<li class="page-item ${p===PAGE?'active':''}"><a class="page-link" href="#" data-go="${p}">${p}</a></li>`; }
|
||||
html+=`<li class="page-item ${PAGE===pages?'disabled':''}"><a class="page-link" href="#" data-go="${PAGE+1}">›</a></li>`;
|
||||
html+=`<li class="page-item ${PAGE===pages?'disabled':''}"><a class="page-link" href="#" data-go="last">»</a></li>`;
|
||||
html+=`</ul>`; $('#pagerNav').innerHTML=html;
|
||||
}
|
||||
|
||||
// ===== 모달(부트스트랩/폴백) =====
|
||||
let mdlModal = null;
|
||||
function openModal(){
|
||||
try{
|
||||
if (hasBS()){
|
||||
mdlModal = mdlModal || new bootstrap.Modal('#modelModal');
|
||||
mdlModal.show(); return;
|
||||
}
|
||||
}catch(_){}
|
||||
// 폴백
|
||||
const m = $('#modelModal');
|
||||
m.classList.add('fallback-show','show');
|
||||
}
|
||||
function closeModal(){
|
||||
if (hasBS() && mdlModal){ mdlModal.hide(); return; }
|
||||
const m = $('#modelModal');
|
||||
m.classList.remove('fallback-show','show');
|
||||
}
|
||||
$('#btnClose').addEventListener('click', closeModal);
|
||||
$('#btnCloseX').addEventListener('click', closeModal);
|
||||
|
||||
// ===== 이벤트 =====
|
||||
// 신규등록
|
||||
$('#btnOpenCreate').addEventListener('click', ()=>{
|
||||
$('#modelModalTitle').textContent='모델 등록';
|
||||
$('#btnSubmit').textContent='등록';
|
||||
$('#modelForm').reset();
|
||||
$('#mdlId').value='';
|
||||
$('#modalHint').textContent='※ 필수 입력: 분류, 모델명';
|
||||
openModal();
|
||||
});
|
||||
|
||||
// 저장(등록/수정)
|
||||
$('#modelForm').addEventListener('submit', e=>{
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
e.currentTarget.classList.add('was-validated');
|
||||
if(!e.currentTarget.checkValidity()) return;
|
||||
|
||||
const dto={
|
||||
id: $('#mdlId').value ? parseInt($('#mdlId').value,10) : null,
|
||||
category: $('#mdlCategory').value,
|
||||
name: $('#mdlName').value.trim(),
|
||||
lot: $('#mdlLot').value.trim(),
|
||||
desc: $('#mdlDesc').value.trim(),
|
||||
created_at: new Date().toISOString().slice(0,10)
|
||||
};
|
||||
|
||||
if(dto.id){
|
||||
const idx=MODELS.findIndex(x=>x.id===dto.id);
|
||||
if(idx>=0) MODELS[idx]={...MODELS[idx], ...dto};
|
||||
$('#snackMsg').textContent='수정되었습니다.'; snack().show();
|
||||
}else{
|
||||
dto.id=(MODELS.reduce((mx,m)=>Math.max(mx,m.id),0)||0)+1;
|
||||
MODELS.unshift(dto);
|
||||
$('#snackMsg').textContent='등록되었습니다.'; snack().show();
|
||||
}
|
||||
closeModal();
|
||||
PAGE=1; renderList();
|
||||
});
|
||||
|
||||
// 수정/삭제/페이징
|
||||
document.addEventListener('click', e=>{
|
||||
const a=e.target.closest('#pagerNav a');
|
||||
if (a){ e.preventDefault();
|
||||
const pages=Math.max(1, Math.ceil(getFiltered().length/SIZE));
|
||||
const go=a.dataset.go;
|
||||
if(go==='first') PAGE=1;
|
||||
else if(go==='last') PAGE=pages;
|
||||
else PAGE=Math.min(Math.max(parseInt(go,10)||1,1), pages);
|
||||
renderList();
|
||||
return;
|
||||
}
|
||||
if (e.target.classList.contains('btnEdit')){
|
||||
const id=parseInt(e.target.closest('tr').dataset.id,10);
|
||||
const m=MODELS.find(x=>x.id===id); if(!m) return;
|
||||
$('#modelModalTitle').textContent='모델 수정';
|
||||
$('#btnSubmit').textContent='수정';
|
||||
$('#mdlId').value=m.id; $('#mdlCategory').value=m.category; $('#mdlName').value=m.name;
|
||||
$('#mdlLot').value=m.lot||''; $('#mdlDesc').value=m.desc||'';
|
||||
$('#modalHint').textContent='필드 수정 후 [수정] 클릭';
|
||||
openModal();
|
||||
}
|
||||
if (e.target.classList.contains('btnDel')){
|
||||
const id=parseInt(e.target.closest('tr').dataset.id,10);
|
||||
if(!confirm('삭제하시겠습니까?')) return;
|
||||
MODELS = MODELS.filter(x=>x.id!==id);
|
||||
$('#snackMsg').textContent='삭제되었습니다.'; snack().show();
|
||||
renderList();
|
||||
}
|
||||
});
|
||||
|
||||
// 검색/필터/표시개수
|
||||
$('#btnSearch').addEventListener('click', ()=>{ PAGE=1; renderList(); });
|
||||
$('#keyword').addEventListener('keydown', e=>{ if(e.key==='Enter'){ PAGE=1; renderList(); }});
|
||||
$('#filterCategory').addEventListener('change', ()=>{ PAGE=1; renderList(); });
|
||||
$('#pageSize').addEventListener('change', function(){ SIZE=parseInt(this.value,10)||20; PAGE=1; renderList(); });
|
||||
|
||||
// 최초
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const ps=$('#pageSize'); if(ps) SIZE=parseInt(ps.value,10)||20;
|
||||
renderList();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Bootstrap JS (있으면 사용, 없어도 폴백으로 동작) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/product/product_price.php
Normal file
8
public/tenant/product/product_price.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='item';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/production/bend_work.php
Normal file
8
public/tenant/production/bend_work.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
101
public/tenant/production/mid_inspection.php
Normal file
101
public/tenant/production/mid_inspection.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
// 모달 바디로 Ajax 로드되는 "중간검사서" 전용 페이지(본문 조각)
|
||||
$title = $_POST['sheet_title'] ?? '중간 검사성적서';
|
||||
$rowsJson = $_POST['rows'] ?? '[]';
|
||||
$rows = json_decode($rowsJson, true) ?: [];
|
||||
?>
|
||||
<div class="p-3">
|
||||
|
||||
<!-- 문서 헤더 -->
|
||||
<div class="border rounded mb-3">
|
||||
<div class="p-2 bg-light d-flex justify-content-between align-items-center">
|
||||
<div class="fw-bold"><?= htmlspecialchars($title) ?></div>
|
||||
<div class="small text-muted">문서번호: KD-QP-01-006</div>
|
||||
</div>
|
||||
<div class="p-3 small">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3"><span class="text-muted">품명</span><br><strong>스크린</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">제품 LOT NO</span><br><strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">검사일자</span><br><strong>연도-월-일</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">검사자</span><br><strong>자동입력</strong></div>
|
||||
</div>
|
||||
<div class="row g-2 mt-2">
|
||||
<div class="col-md-3"><span class="text-muted">현장명</span><br><strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">규격</span><br><strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">결재</span><br>
|
||||
<div class="d-flex gap-2">
|
||||
<span class="badge text-bg-secondary">작성</span>
|
||||
<span class="badge text-bg-secondary">검토</span>
|
||||
<span class="badge text-bg-secondary">승인</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 검사표 -->
|
||||
<div class="border rounded">
|
||||
<div class="p-2 border-bottom bg-light fw-semibold">중간검사 결과</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:70px;">일련</th>
|
||||
<th style="width:140px;">원자재 로트</th>
|
||||
<th style="width:150px;">자재명</th>
|
||||
<th style="width:120px;">길이(mm)</th>
|
||||
<th style="width:120px;">높이(mm)</th>
|
||||
<th style="width:220px;">판정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="miTbody">
|
||||
<?php foreach($rows as $i=>$r): ?>
|
||||
<tr data-idx="<?= $i ?>">
|
||||
<td><?= htmlspecialchars($r['id'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($r['rawl ot'] ?? $r['rawlot'] ?? '') ?></td>
|
||||
<td><?= htmlspecialchars($r['item'] ?? '') ?></td>
|
||||
<td><input type="number" class="form-control form-control-sm mi-len" placeholder="mm"></td>
|
||||
<td><input type="number" class="form-control form-control-sm mi-hei" placeholder="mm"></td>
|
||||
<td>
|
||||
<div class="d-flex gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="mi_<?= $i ?>" value="합격">
|
||||
<label class="form-check-label">합격</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="mi_<?= $i ?>" value="불합격">
|
||||
<label class="form-check-label">불합격</label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-2 small text-muted">※ 측정값은 기록용(저장 대상 아님), 판정만 상위 화면에 반영됩니다.</div>
|
||||
</div>
|
||||
|
||||
<!-- 하단 버튼 -->
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-primary" id="btnApplyMid">적용</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// 적용 → 부모에 결과 전달
|
||||
document.getElementById('btnApplyMid').addEventListener('click', function(){
|
||||
const out = [];
|
||||
document.querySelectorAll('#miTbody tr').forEach(tr=>{
|
||||
const idx = +tr.getAttribute('data-idx');
|
||||
const res = tr.querySelector('input[type=radio]:checked');
|
||||
out.push({ idx, result: res ? res.value : null });
|
||||
});
|
||||
// 부모에 커스텀 이벤트 전달
|
||||
document.dispatchEvent(new CustomEvent('mid:apply', { detail: out }));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
201
public/tenant/production/process_detail.php
Normal file
201
public/tenant/production/process_detail.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
// 모달 내부 Ajax 본문 조각 (header/footer 없음)
|
||||
$jobId = $_POST['job_id'] ?? ($_GET['job_id'] ?? '');
|
||||
$cust = $_POST['cust'] ?? ($_GET['cust'] ?? '');
|
||||
$orderNo = $_POST['order_no'] ?? ($_GET['order_no'] ?? '');
|
||||
?>
|
||||
<div class="p-3">
|
||||
|
||||
<!-- 0) 작업 기본정보 (텍스트 표기) -->
|
||||
<div class="border rounded mb-3">
|
||||
<div class="p-2 border-bottom bg-light fw-semibold">작업 기본정보</div>
|
||||
<div class="p-3">
|
||||
<div class="row g-2 small">
|
||||
<div class="col-md-3"><span class="text-muted">작업코드</span> : <strong><?= htmlspecialchars($jobId) ?></strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">수주번호</span> : <strong><?= htmlspecialchars($orderNo) ?></strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">고객명</span> : <strong><?= htmlspecialchars($cust) ?></strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">현장명</span> : <strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">수주계약일</span> : <strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">제품명</span> : <strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">출고요청일</span> : <strong>자동입력</strong></div>
|
||||
<div class="col-md-3"><span class="text-muted">비고</span> : <strong>-</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 1) 작업 상세 -->
|
||||
<div class="border rounded">
|
||||
<!-- 상단 바: 전체 일괄 체크 -->
|
||||
<div class="d-flex align-items-center justify-content-between p-2 border-bottom bg-light">
|
||||
<div class="fw-semibold">작업 상세정보</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark" id="btnCheckAll">전체 일괄 체크</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle m-0" id="procTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:70px;">일련</th>
|
||||
<th style="width:140px;">원자재 로트</th>
|
||||
<th style="width:60px;">층</th>
|
||||
<th style="width:90px;">부호</th>
|
||||
<th style="width:120px;">자재명</th>
|
||||
<th style="width:90px;">가로</th>
|
||||
<th style="width:90px;">세로</th>
|
||||
<th style="width:160px;">제단사항</th>
|
||||
|
||||
<!-- 각 공정 버튼을 해당 칼럼 라인에 정렬 -->
|
||||
<th style="width:110px; text-align:center;">
|
||||
<div class="mb-1">절단</div>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary w-100" id="btnAllCut">전체</button>
|
||||
</th>
|
||||
<th style="width:110px; text-align:center;">
|
||||
<div class="mb-1">미싱</div>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary w-100" id="btnAllSew">전체</button>
|
||||
</th>
|
||||
<th style="width:130px; text-align:center;">
|
||||
<div class="mb-1">중간검사</div>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary w-100" id="btnOpenMid">열기</button>
|
||||
</th>
|
||||
<th style="width:110px; text-align:center;">
|
||||
<div class="mb-1">포장</div>
|
||||
<button type="button" class="btn btn-xs btn-outline-primary w-100" id="btnAllPack">전체</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="procBody"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 하단 버튼 -->
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSaveProcess">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 중간검사서 모달(본문은 별도 파일로 로드) -->
|
||||
<div class="modal fade" id="midInspectModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body p-0" id="midBody"><!-- /tenant/production/mid_inspection.php 로드 --></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#procTable th, #procTable td { vertical-align: middle; }
|
||||
.cell-center { text-align:center; }
|
||||
.chip-pass { background:#d1e7dd; color:#0f5132; border-radius:12px; padding:.1rem .5rem; font-size:.8rem; }
|
||||
.chip-fail { background:#ffdede; color:#a10000; border-radius:12px; padding:.1rem .5rem; font-size:.8rem; }
|
||||
.btn-xs { --bs-btn-padding-y:.15rem; --bs-btn-padding-x:.35rem; --bs-btn-font-size:.75rem; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
// ---- 샘플 데이터 ----
|
||||
const rows = [
|
||||
{ id:1, rawlot:'250729-01', floor:1, unit:'FSS-01', item:'실리카', w:6960, h:3050, cutnote:'1220*2장 800*1장',
|
||||
cut:false, sew:false, mid:null, pack:false },
|
||||
{ id:2, rawlot:'250729-01', floor:1, unit:'FSS-02', item:'실리카', w:5160, h:3050, cutnote:'1220*2장 800*1장',
|
||||
cut:false, sew:false, mid:null, pack:false },
|
||||
{ id:3, rawlot:'', floor:'', unit:'', item:'', w:'', h:'', cutnote:'',
|
||||
cut:false, sew:false, mid:null, pack:false },
|
||||
{ id:4, rawlot:'', floor:'', unit:'', item:'', w:'', h:'', cutnote:'',
|
||||
cut:false, sew:false, mid:null, pack:false },
|
||||
];
|
||||
|
||||
function midBadge(v){
|
||||
if(v==null || v==='') return '';
|
||||
return (v==='합격')
|
||||
? '<span class="chip-pass">합격</span>'
|
||||
: '<span class="chip-fail">불합격</span>';
|
||||
}
|
||||
|
||||
function renderMain(){
|
||||
const html = rows.map((r,i)=>`
|
||||
<tr data-idx="${i}">
|
||||
<td>${r.id||''}</td>
|
||||
<td>${r.rawlot||''}</td>
|
||||
<td>${r.floor||''}</td>
|
||||
<td>${r.unit||''}</td>
|
||||
<td>${r.item||''}</td>
|
||||
<td>${r.w||''}</td>
|
||||
<td>${r.h||''}</td>
|
||||
<td>${r.cutnote||''}</td>
|
||||
|
||||
<td class="cell-center">
|
||||
<input class="form-check-input form-check-input-sm chk-cut" type="checkbox" ${r.cut?'checked':''}>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<input class="form-check-input form-check-input-sm chk-sew" type="checkbox" ${r.sew?'checked':''}>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<div class="mid-badge">${midBadge(r.mid)}</div>
|
||||
</td>
|
||||
<td class="cell-center">
|
||||
<input class="form-check-input form-check-input-sm chk-pack" type="checkbox" ${r.pack?'checked':''}>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#procBody').html(html);
|
||||
}
|
||||
|
||||
// ---- 전체 일괄 체크/해제 ----
|
||||
function toggleAllProcesses(){
|
||||
const anyOff = rows.some(r=>!(r.cut && r.sew && r.pack));
|
||||
rows.forEach(r=>{ r.cut=anyOff; r.sew=anyOff; r.pack=anyOff; });
|
||||
renderMain();
|
||||
}
|
||||
$('#btnCheckAll').on('click', toggleAllProcesses);
|
||||
|
||||
// ---- 공정별 일괄 토글 ----
|
||||
function toggleColumn(key){
|
||||
const anyOff = rows.some(r=>!r[key]);
|
||||
rows.forEach(r=>{ r[key]=anyOff; });
|
||||
renderMain();
|
||||
}
|
||||
$('#btnAllCut').on('click', ()=>toggleColumn('cut'));
|
||||
$('#btnAllSew').on('click', ()=>toggleColumn('sew'));
|
||||
$('#btnAllPack').on('click', ()=>toggleColumn('pack'));
|
||||
|
||||
// 개별 체크
|
||||
$(document).on('change', '.chk-cut', function(){ rows[$(this).closest('tr').data('idx')].cut = this.checked; });
|
||||
$(document).on('change', '.chk-sew', function(){ rows[$(this).closest('tr').data('idx')].sew = this.checked; });
|
||||
$(document).on('change', '.chk-pack', function(){ rows[$(this).closest('tr').data('idx')].pack = this.checked; });
|
||||
|
||||
// ---- 중간검사서 열기: 별도 페이지 로드 ----
|
||||
$('#btnOpenMid').on('click', function(){
|
||||
$('#midBody').html('<div class="p-4 text-center text-muted">로딩 중…</div>');
|
||||
$('#midBody').load('/tenant/production/mid_inspection.php', {
|
||||
sheet_title: '스크린-중간 검사성적서',
|
||||
rows: JSON.stringify(rows)
|
||||
}, function(){
|
||||
new bootstrap.Modal('#midInspectModal').show();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- 중간검사서 → 결과 적용 (커스텀 이벤트 수신) ----
|
||||
document.addEventListener('mid:apply', function(e){
|
||||
const results = e.detail || [];
|
||||
// results: [{idx:0, result:'합격'|'불합격'}, ...]
|
||||
results.forEach(r=>{
|
||||
if(rows[r.idx]) rows[r.idx].mid = r.result || null;
|
||||
});
|
||||
renderMain();
|
||||
const m = bootstrap.Modal.getInstance(document.getElementById('midInspectModal'));
|
||||
m && m.hide();
|
||||
});
|
||||
|
||||
// 저장(모의)
|
||||
$('#btnSaveProcess').on('click', function(){
|
||||
const payload = { job_id: <?= json_encode($jobId) ?>, order_no: <?= json_encode($orderNo) ?>, rows };
|
||||
console.log('[PROCESS SAVE]', payload);
|
||||
alert('저장(모의). 콘솔을 확인하세요.');
|
||||
});
|
||||
|
||||
renderMain();
|
||||
})(jQuery);
|
||||
</script>
|
||||
275
public/tenant/production/screen_progress.php
Normal file
275
public/tenant/production/screen_progress.php
Normal file
@@ -0,0 +1,275 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
$CURRENT_SECTION='process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:18px;">
|
||||
|
||||
<!-- 탭 (좌측) + 검색 (우측) -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<ul class="nav nav-pills gap-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tenant/production/screen_work.php">작업지시현황</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/tenant/production/screen_progress.php">공정진행현황</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 기본 검색폼 (키워드 + 작업자 + 버튼) -->
|
||||
<form id="searchForm" class="d-flex gap-2">
|
||||
<div class="search-form d-flex gap-2">
|
||||
<input type="text" id="keyword" class="form-control" placeholder="현장명/발주처/제품명 등">
|
||||
<input type="text" id="worker" class="form-control" placeholder="작업자">
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-2">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle m-0" id="jobTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:64px;">NO</th>
|
||||
<th style="width:90px;">작업순위</th>
|
||||
<th style="width:120px;">수주계약일</th>
|
||||
<th style="width:140px;">발주처</th>
|
||||
<th>현장명</th>
|
||||
<th style="width:110px;">제품명</th>
|
||||
<th style="width:90px;">수량(틀)</th>
|
||||
<th style="width:130px;">출고요청일</th>
|
||||
<th style="width:150px;">작업자</th> <!-- 추가 -->
|
||||
<th style="width:140px;">공정진행현황</th> <!-- 라벨 변경 -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobTbody"><!-- JS로 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="d-flex justify-content-center py-2" id="pager"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업지시 모달 (본문 Ajax 로드 그대로 사용) -->
|
||||
<div class="modal fade" id="workInstructionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<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 p-0" id="wiBody"><!-- /tenant/production/work_instruction.php 로드 --></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모바일 카드 뷰 -->
|
||||
<div id="jobCards" class="mt-2"></div>
|
||||
|
||||
<style>
|
||||
/* 헤더/셀 간격 + 한 줄 유지 */
|
||||
#jobTable th{ vertical-align:middle; text-align:center; white-space:nowrap; padding:.55rem .6rem; }
|
||||
#jobTable td{ vertical-align:middle; padding:.55rem .6rem; }
|
||||
|
||||
/* 정렬: 번호/순위/수량/마지막 2개 */
|
||||
#jobTable td:nth-child(1),
|
||||
#jobTable td:nth-child(2),
|
||||
#jobTable td:nth-child(7),
|
||||
#jobTable td:nth-last-child(1),
|
||||
#jobTable td:nth-last-child(2){ text-align:center; }
|
||||
|
||||
/* 현장명: 고정폭 + 말줄임 */
|
||||
#jobTable td:nth-child(5){
|
||||
max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 상태 뱃지 */
|
||||
.badge-state{ font-weight:600; }
|
||||
.badge-wait{ background:#fff1a8; color:#111; } /* 대기 */
|
||||
.badge-miss{ background:#86b7fe; color:#052c65; } /* 미싱 */
|
||||
.badge-check{ background:#d1e7dd; color:#0f5132; } /* 중간검사 */
|
||||
.badge-pack{ background:#cfe2ff; color:#052c65; } /* 포장 */
|
||||
.badge-done{ background:#adb5bd; }
|
||||
.badge-cancel{ background:#ffb3b3; }
|
||||
|
||||
/* 대기 행 하이라이트(다른 행은 hover 때만) */
|
||||
.row-waiting{ background:#fff8cc; }
|
||||
|
||||
/* 페이지네이션 */
|
||||
.pagination .page-link{ padding:.35rem .6rem; }
|
||||
.pagination .page-item.active .page-link{ background:#2c4a85; border-color:#2c4a85; }
|
||||
|
||||
/* 모바일: 테이블 → 카드 */
|
||||
@media (max-width: 767.98px){
|
||||
.table-responsive{ display:none; }
|
||||
#jobCards{ display:block; }
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
#jobCards{ display:none; }
|
||||
}
|
||||
|
||||
.job-card .title{ font-weight:700; }
|
||||
.job-card .meta{ font-size:.9rem; color:#555; }
|
||||
.job-card.waiting{ background:#fff8cc; }
|
||||
|
||||
.search-form .btn{
|
||||
height:38px; line-height:1.5; padding:0 16px; white-space:nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
|
||||
// ------- 샘플 데이터 (공정단계 + 작업자 포함) -------
|
||||
const rows = [
|
||||
{no:13, pri:0, od:'2025-07-25', buyer:'명보에스티', site:'일산 동국대병원 3층', prod:'KSS01', qty:10, ship:'2025-08-01', workers:['이강민','김소연'], stage:'대기'},
|
||||
{no:12, pri:0, od:'2025-07-29', buyer:'명보에스티', site:'일산 동국대병원 2층', prod:'KSS01', qty:10, ship:'2025-08-01', workers:['김소연'], stage:'미싱'},
|
||||
{no:11, pri:1, od:'2025-07-31', buyer:'이에스디', site:'용인 기흥', prod:'KSS02', qty:2, ship:'2025-08-03', workers:['홍길동'], stage:'중간검사'},
|
||||
{no:10, pri:2, od:'2025-06-24', buyer:'대성산업', site:'횡덕중학교', prod:'KSS02', qty:9, ship:'2025-08-03', workers:['김철수'], stage:'포장'},
|
||||
{no:9, pri:3, od:'2025-07-14', buyer:'이엘도어', site:'파주아르디움 2층', prod:'KSS01', qty:5, ship:'2025-08-03', workers:['박지훈'], stage:'포장'},
|
||||
{no:8, pri:4, od:'2025-07-26', buyer:'주월기업', site:'여의도IFC 1층', prod:'KSS01', qty:5, ship:'2025-08-05', workers:['이강민','유영수'], stage:'중간검사'},
|
||||
{no:7, pri:5, od:'2025-08-01', buyer:'명보에스티', site:'일산 동국대병원 1층', prod:'KSS01', qty:20, ship:'2025-08-05', workers:['김소연'], stage:'대기'},
|
||||
{no:6, pri:6, od:'2025-08-01', buyer:'주일기업', site:'여의도IFC 2층', prod:'KSS01', qty:5, ship:'2025-08-05', workers:['홍길동'], stage:'중간검사'},
|
||||
{no:5, pri:7, od:'2025-08-01', buyer:'주일기업', site:'여의도IFC 3층', prod:'KSS01', qty:5, ship:'2025-08-05', workers:['김철수'], stage:'미싱'},
|
||||
{no:4, pri:3, od:'2025-08-01', buyer:'이엘도어', site:'파주아르디움 1층', prod:'KSS01', qty:5, ship:'2025-08-07', workers:['박지훈'], stage:'완료'},
|
||||
{no:3, pri:3, od:'2025-08-03', buyer:'한국특수', site:'인천염림물류창고', prod:'KSS01', qty:5, ship:'2025-08-07', workers:['이강민'], stage:'완료'},
|
||||
{no:2, pri:2, od:'2025-08-04', buyer:'명보에스티', site:'평택', prod:'KSS02', qty:1, ship:'2025-08-10', workers:['김소연'], stage:'완료'},
|
||||
{no:1, pri:1, od:'2025-08-04', buyer:'명보에스티', site:'장수', prod:'KSS02', qty:2, ship:'2025-08-10', workers:['홍길동'], stage:'완료'},
|
||||
];
|
||||
|
||||
const pageSize = 10;
|
||||
let curPage = 1;
|
||||
let filtered = [...rows];
|
||||
|
||||
function stageBadge(stage){
|
||||
const cls = {
|
||||
'대기':'badge-wait',
|
||||
'미싱':'badge-miss',
|
||||
'중간검사':'badge-check',
|
||||
'포장':'badge-pack',
|
||||
'완료':'badge-done',
|
||||
'취소':'badge-cancel'
|
||||
}[stage] || 'bg-secondary';
|
||||
return `<span class="badge badge-state ${cls}">${stage}</span>`;
|
||||
}
|
||||
|
||||
// PC 테이블 렌더
|
||||
function renderTable(pageItems){
|
||||
const tbody = pageItems.map(r=>`
|
||||
<tr class="row-open ${r.stage==='대기'?'row-waiting':''}" data-no="${r.no}">
|
||||
<td>${r.no}</td>
|
||||
<td>${r.pri}</td>
|
||||
<td>${r.od}</td>
|
||||
<td>${r.buyer}</td>
|
||||
<td title="${r.site}">${r.site}</td>
|
||||
<td>${r.prod}</td>
|
||||
<td>${r.qty}</td>
|
||||
<td>${r.ship}</td>
|
||||
<td>${(r.workers||[]).join(', ')}</td>
|
||||
<td>${stageBadge(r.stage)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#jobTbody').html(tbody);
|
||||
}
|
||||
|
||||
// 모바일 카드 렌더
|
||||
function renderCards(pageItems){
|
||||
const cards = pageItems.map(r=>`
|
||||
<div class="card mb-2 job-card ${r.stage==='대기'?'waiting':''}" data-no="${r.no}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="title">${r.site}</div>
|
||||
<div>${stageBadge(r.stage)}</div>
|
||||
</div>
|
||||
<div class="meta mt-1">
|
||||
<span>NO ${r.no}</span> · <span>순위 ${r.pri}</span> · <span>${r.prod}</span> · <span>${r.qty}틀</span>
|
||||
</div>
|
||||
<div class="meta">발주처 ${r.buyer} / 출고요청일 ${r.ship}</div>
|
||||
<div class="meta">작업자 ${(r.workers||[]).join(', ')}</div>
|
||||
<div class="mt-2 text-end">
|
||||
<button class="btn btn-sm btn-outline-primary btn-open-wi">작업 지시</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
$('#jobCards').html(cards);
|
||||
}
|
||||
|
||||
// 페이지네이션 렌더
|
||||
function renderPager(totalPage){
|
||||
let html = `
|
||||
<nav aria-label="pagination">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item ${curPage===1?'disabled':''}">
|
||||
<a class="page-link" href="#" data-page="prev">«</a>
|
||||
</li>`;
|
||||
for(let p=1;p<=totalPage;p++){
|
||||
html += `
|
||||
<li class="page-item ${p===curPage?'active':''}">
|
||||
<a class="page-link" href="#" data-page="${p}">${p}</a>
|
||||
</li>`;
|
||||
}
|
||||
html += `
|
||||
<li class="page-item ${curPage===totalPage?'disabled':''}">
|
||||
<a class="page-link" href="#" data-page="next">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>`;
|
||||
$('#pager').html(html);
|
||||
}
|
||||
|
||||
function render(){
|
||||
const start = (curPage-1)*pageSize;
|
||||
const pageItems = filtered.slice(start, start+pageSize);
|
||||
renderTable(pageItems);
|
||||
renderCards(pageItems);
|
||||
renderPager(Math.max(1, Math.ceil(filtered.length / pageSize)));
|
||||
}
|
||||
|
||||
// 검색
|
||||
$('#searchForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const kw = $('#keyword').val().trim().toLowerCase();
|
||||
const wk = $('#worker').val().trim().toLowerCase();
|
||||
filtered = rows.filter(r=>{
|
||||
const s = [r.buyer,r.site,r.prod,r.od,r.stage,String(r.no)].join(' ').toLowerCase();
|
||||
const w = (r.workers||[]).join(',').toLowerCase();
|
||||
const inKW = !kw || s.includes(kw);
|
||||
const inWK = !wk || w.includes(wk);
|
||||
return inKW && inWK;
|
||||
});
|
||||
curPage = 1; render();
|
||||
});
|
||||
|
||||
// 페이지네이션 클릭
|
||||
$(document).on('click', '.pagination .page-link', function(e){
|
||||
e.preventDefault();
|
||||
const val = $(this).data('page');
|
||||
const total = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
if(val==='prev' && curPage>1) curPage--;
|
||||
else if(val==='next' && curPage<total) curPage++;
|
||||
else if(/^\d+$/.test(val)) curPage = parseInt(val,10);
|
||||
render();
|
||||
});
|
||||
|
||||
// 행/카드 클릭 → 작업지시 모달 로드(동일 흐름 유지)
|
||||
$(document).on('click', 'tr.row-open, .btn-open-wi', function(){
|
||||
const $el = $(this).closest('[data-no]');
|
||||
const no = $el.data('no');
|
||||
const r = rows.find(x=>x.no==no);
|
||||
$('#wiBody').html('<div class="p-4 text-center text-muted">로딩 중…</div>');
|
||||
$('#wiBody').load('/tenant/production/process_detail.php', {
|
||||
job_id: 'SCR-'+String(no).padStart(3,'0'),
|
||||
cust: r.buyer,
|
||||
order_no: r.od.replaceAll('-','')+'-'+String(no).padStart(2,'0')
|
||||
}, function(){
|
||||
new bootstrap.Modal('#workInstructionModal').show();
|
||||
});
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
281
public/tenant/production/screen_work.php
Normal file
281
public/tenant/production/screen_work.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<!-- SAM RULES: include=../inc/header.php; base=/tenant; width=1280; js=jQuery+BS5 -->
|
||||
<?php
|
||||
$CURRENT_SECTION='process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
<div class="container" style="max-width:1280px; margin-top:18px;">
|
||||
|
||||
<!-- 탭 (좌측) + 검색 (우측) -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<ul class="nav nav-pills gap-2">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/tenant/production/screen_work.php">작업지시현황</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/tenant/production/screen_progress.php">공정진행현황</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 기본 검색폼 (키워드 + 작업자 + 버튼) -->
|
||||
<form id="searchForm" class="d-flex gap-2">
|
||||
<div class="search-form d-flex gap-2">
|
||||
<input type="text" class="form-control" placeholder="현장명/발주처/제품명 등">
|
||||
<input type="text" class="form-control" placeholder="작업자">
|
||||
<button type="submit" class="btn btn-primary">검색</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-2">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle m-0" id="jobTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:64px;">NO</th>
|
||||
<th style="width:90px;">작업순위</th>
|
||||
<th style="width:120px;">수주계약일</th>
|
||||
<th style="width:140px;">발주처</th>
|
||||
<th>현장명</th>
|
||||
<th style="width:110px;">제품명</th>
|
||||
<th style="width:90px;">수량(틀)</th>
|
||||
<th style="width:130px;">출고요청일</th>
|
||||
<th style="width:120px;">작업지시현황</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobTbody">
|
||||
<!-- JS로 렌더 -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 페이지네이션 -->
|
||||
<div class="d-flex justify-content-center py-2" id="pager"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업지시 모달 (본문 Ajax 로드) -->
|
||||
<div class="modal fade" id="workInstructionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
||||
<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 p-0" id="wiBody">
|
||||
<!-- /tenant/production/work_instruction.php 로드 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 모바일 카드 뷰 -->
|
||||
<div id="jobCards" class="mt-2"></div>
|
||||
|
||||
<style>
|
||||
/* 헤더/셀 간격 줄이기 + 헤더 한 줄 유지 */
|
||||
#jobTable th{
|
||||
vertical-align: middle; text-align:center;
|
||||
white-space: nowrap; padding:.55rem .6rem;
|
||||
}
|
||||
#jobTable td{ vertical-align: middle; padding:.55rem .6rem; }
|
||||
|
||||
/* 정렬: 번호/순위/수량/상태 */
|
||||
#jobTable td:nth-child(1),
|
||||
#jobTable td:nth-child(2),
|
||||
#jobTable td:nth-child(7),
|
||||
#jobTable td:nth-child(9){ text-align:center; }
|
||||
|
||||
/* 현장명: 고정폭 + 말줄임 */
|
||||
#jobTable td:nth-child(5){
|
||||
max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 상태 뱃지 (필요 시 유지) */
|
||||
.badge-state { font-weight:600; }
|
||||
.badge-wait { background:#fff1a8; color:#111; }
|
||||
.badge-run { background:#7fb3ff; }
|
||||
.badge-rcv { background:#d1e7dd; color:#0f5132;}
|
||||
.badge-done { background:#adb5bd; }
|
||||
.badge-cancel { background:#ffb3b3; }
|
||||
|
||||
/* '대기' 행 하이라이트 (나머지는 hover시에만) */
|
||||
.row-waiting { background:#fff8cc; }
|
||||
|
||||
/* 페이지네이션 - 얇고 최신 느낌 */
|
||||
.pagination .page-link{ padding:.35rem .6rem; }
|
||||
.pagination .page-item.active .page-link{
|
||||
background:#2c4a85; border-color:#2c4a85;
|
||||
}
|
||||
|
||||
/* 모바일: 테이블 숨기고 카드형 노출 */
|
||||
@media (max-width: 767.98px){
|
||||
.table-responsive{ display:none; }
|
||||
#jobCards{ display:block; }
|
||||
}
|
||||
@media (min-width: 768px){
|
||||
#jobCards{ display:none; }
|
||||
}
|
||||
|
||||
/* 카드형 아이템 */
|
||||
.job-card .title{ font-weight:700; }
|
||||
.job-card .meta{ font-size:.9rem; color:#555; }
|
||||
.job-card.waiting{ background:#fff8cc; }
|
||||
|
||||
.search-form .btn {
|
||||
height: 38px; /* input 높이와 동일하게 */
|
||||
line-height: 1.5; /* 세로 중앙 정렬 */
|
||||
padding: 0 16px; /* 좌우 여백만 */
|
||||
white-space: nowrap; /* 줄바꿈 방지 */
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// ------- 샘플 데이터 (작업자 필드 추가) -------
|
||||
const rows = [
|
||||
{no:13, pri:0, od:'2025-07-25', buyer:'명보에스티', site:'일산 동국대병원 3층', prod:'KSS01', qty:10, ship:'2025-08-01', state:'대기', worker:'이강민'},
|
||||
{no:12, pri:0, od:'2025-07-29', buyer:'명보에스티', site:'일산 동국대병원 2층', prod:'KSS01', qty:10, ship:'2025-08-01', state:'대기', worker:'김소연'},
|
||||
{no:11, pri:1, od:'2025-07-31', buyer:'이에스디', site:'용인 기흥', prod:'KSS02', qty:2, ship:'2025-08-03', state:'작업중', worker:'홍길동'},
|
||||
{no:10, pri:2, od:'2025-06-24', buyer:'대성산업', site:'횡덕중학교', prod:'KSS02', qty:9, ship:'2025-08-03', state:'작업중', worker:'김철수'},
|
||||
{no:9, pri:3, od:'2025-07-14', buyer:'이엘도어', site:'파주아르디움 2층', prod:'KSS01', qty:5, ship:'2025-08-03', state:'작업중', worker:'박지훈'},
|
||||
{no:8, pri:4, od:'2025-07-26', buyer:'주월기업', site:'여의도IFC 1층', prod:'KSS01', qty:5, ship:'2025-08-05', state:'작업중', worker:'이강민'},
|
||||
{no:7, pri:5, od:'2025-08-01', buyer:'명보에스티', site:'일산 동국대병원 1층', prod:'KSS01', qty:20, ship:'2025-08-05', state:'작업취소', worker:'김소연'},
|
||||
{no:6, pri:6, od:'2025-08-01', buyer:'주일기업', site:'여의도IFC 2층', prod:'KSS01', qty:5, ship:'2025-08-05', state:'접수', worker:'홍길동'},
|
||||
{no:5, pri:7, od:'2025-08-01', buyer:'주일기업', site:'여의도IFC 3층', prod:'KSS01', qty:5, ship:'2025-08-05', state:'접수', worker:'김철수'},
|
||||
{no:4, pri:3, od:'2025-08-01', buyer:'이엘도어', site:'파주아르디움 1층', prod:'KSS01', qty:5, ship:'2025-08-07', state:'완료', worker:'박지훈'},
|
||||
{no:3, pri:3, od:'2025-08-03', buyer:'한국특수', site:'인천염림물류창고', prod:'KSS01', qty:5, ship:'2025-08-07', state:'완료', worker:'이강민'},
|
||||
{no:2, pri:2, od:'2025-08-04', buyer:'명보에스티', site:'평택', prod:'KSS02', qty:1, ship:'2025-08-10', state:'완료', worker:'김소연'},
|
||||
{no:1, pri:1, od:'2025-08-04', buyer:'명보에스티', site:'장수', prod:'KSS02', qty:2, ship:'2025-08-10', state:'완료', worker:'홍길동'},
|
||||
];
|
||||
|
||||
const pageSize = 10;
|
||||
let curPage = 1;
|
||||
let filtered = [...rows];
|
||||
|
||||
function badge(state){
|
||||
switch(state){
|
||||
case '대기': return '<span class="badge badge-state badge-wait">대기</span>';
|
||||
case '작업중': return '<span class="badge badge-state badge-run">작업중</span>';
|
||||
case '접수': return '<span class="badge badge-state badge-rcv">접수</span>';
|
||||
case '완료': return '<span class="badge badge-state badge-done">완료</span>';
|
||||
case '작업취소': return '<span class="badge badge-state badge-cancel">작업취소</span>';
|
||||
default: return state;
|
||||
}
|
||||
}
|
||||
|
||||
// PC 테이블 렌더
|
||||
function renderTable(pageItems){
|
||||
const tbody = pageItems.map(r=>`
|
||||
<tr class="row-open ${r.state==='대기'?'row-waiting':''}" data-no="${r.no}">
|
||||
<td>${r.no}</td>
|
||||
<td>${r.pri}</td>
|
||||
<td>${r.od}</td>
|
||||
<td>${r.buyer}</td>
|
||||
<td title="${r.site}">${r.site}</td>
|
||||
<td>${r.prod}</td>
|
||||
<td>${r.qty}</td>
|
||||
<td>${r.ship}</td>
|
||||
<td>${badge(r.state)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#jobTbody').html(tbody);
|
||||
}
|
||||
|
||||
// 모바일 카드 렌더
|
||||
function renderCards(pageItems){
|
||||
const cards = pageItems.map(r=>`
|
||||
<div class="card mb-2 job-card ${r.state==='대기'?'waiting':''}" data-no="${r.no}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="title">${r.site}</div>
|
||||
<div>${badge(r.state)}</div>
|
||||
</div>
|
||||
<div class="meta mt-1">
|
||||
<span>NO ${r.no}</span> · <span>순위 ${r.pri}</span> · <span>${r.prod}</span> · <span>${r.qty}틀</span>
|
||||
</div>
|
||||
<div class="meta">발주처 ${r.buyer} / 출고요청일 ${r.ship}</div>
|
||||
<div class="mt-2 text-end">
|
||||
<button class="btn btn-sm btn-outline-primary btn-open-wi">작업 지시</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
$('#jobCards').html(cards);
|
||||
}
|
||||
|
||||
// 페이지네이션 렌더 (부트스트랩)
|
||||
function renderPager(totalPage){
|
||||
let html = `
|
||||
<nav aria-label="pagination">
|
||||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||||
<li class="page-item ${curPage===1?'disabled':''}">
|
||||
<a class="page-link" href="#" data-page="prev">«</a>
|
||||
</li>`;
|
||||
for(let p=1;p<=totalPage;p++){
|
||||
html += `
|
||||
<li class="page-item ${p===curPage?'active':''}">
|
||||
<a class="page-link" href="#" data-page="${p}">${p}</a>
|
||||
</li>`;
|
||||
}
|
||||
html += `
|
||||
<li class="page-item ${curPage===totalPage?'disabled':''}">
|
||||
<a class="page-link" href="#" data-page="next">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>`;
|
||||
$('#pager').html(html);
|
||||
}
|
||||
|
||||
function render(){
|
||||
const start = (curPage-1)*pageSize;
|
||||
const pageItems = filtered.slice(start, start+pageSize);
|
||||
renderTable(pageItems);
|
||||
renderCards(pageItems);
|
||||
renderPager(Math.max(1, Math.ceil(filtered.length / pageSize)));
|
||||
}
|
||||
|
||||
// 검색 (버튼 submit)
|
||||
$('#searchForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const kw = $('#keyword').val().trim().toLowerCase();
|
||||
const wk = $('#worker').val().trim().toLowerCase();
|
||||
filtered = rows.filter(r=>{
|
||||
const inKW = !kw || [r.buyer,r.site,r.prod,r.state,r.od,String(r.no)].some(v=>String(v).toLowerCase().includes(kw));
|
||||
const inWK = !wk || String(r.worker||'').toLowerCase().includes(wk);
|
||||
return inKW && inWK;
|
||||
});
|
||||
curPage = 1; render();
|
||||
});
|
||||
|
||||
// 페이지네이션 클릭
|
||||
$(document).on('click', '.pagination .page-link', function(e){
|
||||
e.preventDefault();
|
||||
const val = $(this).data('page');
|
||||
const total = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
if(val==='prev' && curPage>1) curPage--;
|
||||
else if(val==='next' && curPage<total) curPage++;
|
||||
else if(typeof val==='number' || /^\d+$/.test(val)) curPage = parseInt(val,10);
|
||||
render();
|
||||
});
|
||||
|
||||
// 행/카드 클릭 → 작업지시 모달 로드
|
||||
$(document).on('click', 'tr.row-open, .btn-open-wi', function(){
|
||||
// 테이블 행이면 data-no에서, 카드 버튼이면 부모 카드에서 가져옴
|
||||
const $el = $(this).closest('[data-no]');
|
||||
const no = $el.data('no');
|
||||
const r = rows.find(x=>x.no==no);
|
||||
$('#wiBody').html('<div class="p-4 text-center text-muted">로딩 중…</div>');
|
||||
$('#wiBody').load('/tenant/production/work_instruction.php', {
|
||||
job_id: 'SCR-'+String(no).padStart(3,'0'),
|
||||
cust: r.buyer,
|
||||
order_no: r.od.replaceAll('-','')+'-'+String(no).padStart(2,'0')
|
||||
}, function(){
|
||||
new bootstrap.Modal('#workInstructionModal').show();
|
||||
});
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/production/slat_work.php
Normal file
8
public/tenant/production/slat_work.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='process';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
406
public/tenant/production/work_instruction.php
Normal file
406
public/tenant/production/work_instruction.php
Normal file
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
// SAM MODAL BODY (NO header/footer)
|
||||
// Params: job_id, cust, order_no (POST/GET)
|
||||
$jobId = $_POST['job_id'] ?? ($_GET['job_id'] ?? '');
|
||||
$cust = $_POST['cust'] ?? ($_GET['cust'] ?? '');
|
||||
$orderNo = $_POST['order_no'] ?? ($_GET['order_no'] ?? '');
|
||||
?>
|
||||
<div class="p-3">
|
||||
|
||||
<!-- ROW 1 : 기본정보(좌 2/3) + 우선 작업순위(우 1/3) -->
|
||||
<div class="row g-3 mb-3">
|
||||
<!-- 작업 기본정보 -->
|
||||
<div class="col-lg-8">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="fw-bold mb-2">① 작업 기본정보</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">수주계약일</label>
|
||||
<input class="form-control form-control-sm" value="자동입력" readonly>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">발주처</label>
|
||||
<input class="form-control form-control-sm" value="<?= htmlspecialchars($cust) ?: '자동입력' ?>" readonly>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">현장명</label>
|
||||
<input class="form-control form-control-sm" value="자동입력" readonly>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">출고요청일</label>
|
||||
<input class="form-control form-control-sm" value="자동입력" readonly>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">배송방식</label>
|
||||
<input class="form-control form-control-sm" value="자동입력" readonly>
|
||||
</div>
|
||||
<div class="col-6 col-md-4">
|
||||
<label class="form-label mb-0 small text-muted">제품명</label>
|
||||
<input class="form-control form-control-sm" value="자동입력" readonly>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-0 small text-muted">비고</label>
|
||||
<input class="form-control form-control-sm" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우선 작업순위 -->
|
||||
<div class="col-lg-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="fw-bold mb-2">우선 작업순위</div>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-5 text-end small text-muted">현재순위</div>
|
||||
<div class="col-7"><input type="number" class="form-control form-control-sm" value="0" readonly></div>
|
||||
<div class="col-5 text-end small text-muted">설정순위</div>
|
||||
<div class="col-7">
|
||||
<select class="form-select form-select-sm" id="setPriority">
|
||||
<?php for($i=1;$i<=10;$i++) echo "<option>$i</option>"; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW 2 : 메인 자재(전체폭) + 일괄적용 버튼(이 박스 안) -->
|
||||
<div class="border rounded p-3 mb-3">
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<div class="fw-bold flex-grow-1">② 재고확인 및 자재투입</div>
|
||||
<div class="d-flex gap-2">
|
||||
<input type="text" class="form-control form-control-sm" id="bulkLot" placeholder="LOT 번호 입력/선택값" style="width:200px;">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="btnOpenLotForBulk">LOT 선택</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" id="btnApplyLotMain">로트번호 일괄적용</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:360px; overflow:auto;">
|
||||
<table class="table table-sm align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:42px;"><input type="checkbox" id="chkAll"></th>
|
||||
<th style="width:70px;">일련</th>
|
||||
<th style="width:60px;">층</th>
|
||||
<th style="width:80px;">부호</th>
|
||||
<th>품목명</th>
|
||||
<th style="width:110px;">가로</th>
|
||||
<th style="width:110px;">세로</th>
|
||||
<th style="width:90px;">매수</th>
|
||||
<th style="width:110px;">조인트바</th>
|
||||
<th style="width:160px;">입고 LOT NO.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wiRows"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ROW 3 : 좌(내화실/부자재, 다건) / 우(작업자 배치) -->
|
||||
<div class="row g-3">
|
||||
<!-- ③ 부자재 (고정 목록, 추가/삭제 없음) -->
|
||||
<div class="col-lg-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="fw-bold mb-2">③ 부자재</div>
|
||||
|
||||
<div class="table-responsive" style="max-height:260px; overflow:auto;">
|
||||
<table class="table table-sm align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>항목명</th>
|
||||
<th style="width:160px;">LOT NO.</th>
|
||||
<th style="width:90px;">수량</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subRows"><!-- JS --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 작업자 배치 -->
|
||||
<div class="col-lg-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="fw-bold mb-2">④ 작업자 배치</div>
|
||||
|
||||
<div class="row g-2 align-items-center mb-2">
|
||||
<div class="col-4 col-md-3 text-end small text-muted">부서 선택</div>
|
||||
<div class="col-8 col-md-5">
|
||||
<select id="deptSelect" class="form-select form-select-sm"></select>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 mt-2 mt-md-0">
|
||||
<input type="text" id="workerSearch" class="form-control form-control-sm" placeholder="작업자 검색">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<div class="border rounded" style="height:220px; overflow:auto;">
|
||||
<table class="table table-sm m-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:42px;"></th><th>이름</th><th class="text-muted" style="width:120px;">직책</th></tr>
|
||||
</thead>
|
||||
<tbody id="workerList"><!-- JS --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<div class="small text-muted mb-1">선택됨</div>
|
||||
<div id="chosenWrap" class="d-flex flex-wrap gap-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 하단 버튼 -->
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSaveWI">저장</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LOT 선택 서브 모달 (메인/부자재에서 공용) -->
|
||||
<div class="modal fade" id="lotSelectModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||
<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="table-responsive border rounded">
|
||||
<table class="table table-hover align-middle m-0">
|
||||
<thead class="table-light">
|
||||
<tr><th style="width:160px;">로트번호</th><th>자재명</th><th style="width:100px;">재고</th><th style="width:80px;">선택</th></tr>
|
||||
</thead>
|
||||
<tbody id="lotRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<small class="text-muted">선택 즉시 대상 입력란에 반영됩니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table th, .table td { vertical-align: middle; }
|
||||
#wiRows .lot-input { min-width: 120px; }
|
||||
#chosenWrap .badge { font-size:.85rem; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function($){
|
||||
/* ===== 샘플 데이터 ===== */
|
||||
const rows = [
|
||||
{no:1, floor:1, sign:'FSS-01', item:'실리카', w:696, h:3050, qty:47, joint:50, lot:'', checked:false},
|
||||
{no:2, floor:1, sign:'', item:'실리카', w:700, h:3100, qty:40, joint:45, lot:'', checked:false},
|
||||
{no:3, floor:2, sign:'', item:'실리카', w:650, h:3000, qty:38, joint:42, lot:'', checked:false},
|
||||
];
|
||||
const lots = [
|
||||
{lot:'250626-01', mat:'실리카원단-WY-SC780', stock:800},
|
||||
{lot:'250617-01', mat:'실리카원단-WY-SC780', stock:5069},
|
||||
{lot:'250513-02', mat:'실리카원단-WY-SC780', stock:4625},
|
||||
{lot:'250411-05', mat:'실리카원단-WY-SC780', stock:738},
|
||||
];
|
||||
const DEPTS = {
|
||||
'생산1팀': [
|
||||
{id:'u01', name:'김동실', role:'반장'},
|
||||
{id:'u02', name:'원준서', role:'사원'},
|
||||
{id:'u03', name:'유영수', role:'사원'},
|
||||
{id:'u04', name:'배천석', role:'사원'},
|
||||
],
|
||||
'생산2팀': [
|
||||
{id:'u11', name:'박서준', role:'주임'},
|
||||
{id:'u12', name:'최다연', role:'사원'},
|
||||
{id:'u13', name:'한민수', role:'사원'},
|
||||
]
|
||||
};
|
||||
|
||||
/* ===== 렌더: 메인 자재 ===== */
|
||||
function renderMain(){
|
||||
const html = rows.map((r,i)=>`
|
||||
<tr data-idx="${i}">
|
||||
<td><input type="checkbox" class="row-check" ${r.checked?'checked':''}></td>
|
||||
<td>${r.no}</td>
|
||||
<td>${r.floor}</td>
|
||||
<td>${r.sign||''}</td>
|
||||
<td>${r.item}</td>
|
||||
<td><input type="number" class="form-control form-control-sm wi-w" value="${r.w}"></td>
|
||||
<td><input type="number" class="form-control form-control-sm wi-h" value="${r.h}"></td>
|
||||
<td><input type="number" class="form-control form-control-sm wi-qty" value="${r.qty}"></td>
|
||||
<td><input type="number" class="form-control form-control-sm wi-joint" value="${r.joint}"></td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control lot-input wi-lot" value="${r.lot}" placeholder="선택">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#wiRows').html(html);
|
||||
}
|
||||
|
||||
/* ===== 렌더: LOT 목록 ===== */
|
||||
function renderLots(){
|
||||
$('#lotRows').html(
|
||||
lots.map(l=>`
|
||||
<tr>
|
||||
<td>${l.lot}</td>
|
||||
<td>${l.mat}</td>
|
||||
<td>${l.stock.toLocaleString()}</td>
|
||||
<td><button type="button" class="btn btn-sm btn-primary btn-choose-lot" data-lot="${l.lot}">선택</button></td>
|
||||
</tr>`).join('')
|
||||
);
|
||||
}
|
||||
|
||||
/* ===== 렌더: 부자재(내화실) ===== */
|
||||
// 부자재 고정 목록 (예: 제품별 프리셋)
|
||||
let subItems = [
|
||||
{ id: 's1', name: '내화실', lot: '', qty: 1 },
|
||||
{ id: 's2', name: '실리콘(내열)', lot: '', qty: 2 }
|
||||
];
|
||||
|
||||
function addSubRow(){ subItems.push({id:'s'+(subSeed++), name:'', lot:'', qty:1}); renderSub(); }
|
||||
function renderSub(){
|
||||
$('#subRows').html(
|
||||
subItems.map(s => `
|
||||
<tr data-sid="${s.id}">
|
||||
<td><input type="text" class="form-control form-control-sm sub-name" value="${s.name}" readonly></td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm sub-lot" value="${s.lot}" placeholder="LOT 선택">
|
||||
</td>
|
||||
<td><input type="number" class="form-control form-control-sm sub-qty" value="${s.qty}" min="1"></td>
|
||||
</tr>
|
||||
`).join('')
|
||||
);
|
||||
}
|
||||
|
||||
/* ===== 체크박스 전체/부분 ===== */
|
||||
$(document).on('change', '#chkAll', function(){
|
||||
const on = $(this).is(':checked'); rows.forEach(r=>r.checked=on); renderMain(); this.indeterminate=false;
|
||||
});
|
||||
$(document).on('change', '.row-check', function(){
|
||||
const idx = +$(this).closest('tr').data('idx'); rows[idx].checked = $(this).is(':checked');
|
||||
const total=rows.length, sel=rows.filter(r=>r.checked).length, all=document.getElementById('chkAll');
|
||||
if(sel===0){ all.checked=false; all.indeterminate=false; }
|
||||
else if(sel===total){ all.checked=true; all.indeterminate=false; }
|
||||
else{ all.checked=false; all.indeterminate=true; }
|
||||
});
|
||||
|
||||
/* ===== 값 반영 ===== */
|
||||
$(document).on('input', '.wi-w, .wi-h, .wi-qty, .wi-joint, .wi-lot', function(){
|
||||
const i = +$(this).closest('tr').data('idx');
|
||||
rows[i].w = +$(this).closest('tr').find('.wi-w').val()||0;
|
||||
rows[i].h = +$(this).closest('tr').find('.wi-h').val()||0;
|
||||
rows[i].qty = +$(this).closest('tr').find('.wi-qty').val()||0;
|
||||
rows[i].joint = +$(this).closest('tr').find('.wi-joint').val()||0;
|
||||
rows[i].lot = $(this).closest('tr').find('.wi-lot').val().trim();
|
||||
});
|
||||
|
||||
$(document).on('input', '.sub-name, .sub-lot, .sub-qty', function(){
|
||||
const sid = $(this).closest('tr').data('sid');
|
||||
const s = subItems.find(x=>x.id===sid);
|
||||
s.name = $(this).closest('tr').find('.sub-name').val().trim();
|
||||
s.lot = $(this).closest('tr').find('.sub-lot').val().trim();
|
||||
s.qty = +($(this).closest('tr').find('.sub-qty').val())||1;
|
||||
});
|
||||
|
||||
/* ===== LOT 모달 호출/선택 (메인/부자재 공용) ===== */
|
||||
let lotTarget = {mode:null, rowIdx:null, sid:null}; // mode: 'main' | 'sub' | 'bulk'
|
||||
function openLotModal(){ renderLots(); new bootstrap.Modal('#lotSelectModal').show(); }
|
||||
// 부자재 LOT 입력 클릭 시 모달 오픈
|
||||
$(document).on('focus click', '.sub-lot', function(){
|
||||
lotTarget = { mode: 'sub', sid: $(this).closest('tr').data('sid') };
|
||||
openLotModal();
|
||||
});
|
||||
|
||||
$(document).on('click', '.btn-open-lot', function(){
|
||||
lotTarget.mode = $(this).data('mode'); // main | sub
|
||||
if(lotTarget.mode==='main') lotTarget.rowIdx = +$(this).closest('tr').data('idx');
|
||||
if(lotTarget.mode==='sub') lotTarget.sid = $(this).closest('tr').data('sid');
|
||||
openLotModal();
|
||||
});
|
||||
|
||||
$('#btnOpenLotForBulk').on('click', function(){ lotTarget = {mode:'bulk'}; openLotModal(); });
|
||||
|
||||
$(document).on('click', '.btn-choose-lot', function(){
|
||||
const lot = $(this).data('lot');
|
||||
if(lotTarget.mode==='main' && lotTarget.rowIdx!=null){
|
||||
rows[lotTarget.rowIdx].lot = lot; renderMain();
|
||||
}else if(lotTarget.mode==='sub' && lotTarget.sid){
|
||||
const s = subItems.find(x=>x.id===lotTarget.sid); if(s){ s.lot = lot; renderSub(); }
|
||||
}else if(lotTarget.mode==='bulk'){
|
||||
$('#bulkLot').val(lot);
|
||||
}
|
||||
const m = bootstrap.Modal.getInstance(document.getElementById('lotSelectModal')); m && m.hide();
|
||||
});
|
||||
|
||||
// 메인 자재 일괄적용(체크된 행들만)
|
||||
$('#btnApplyLotMain').on('click', function(){
|
||||
const lot = $('#bulkLot').val().trim();
|
||||
if(!lot) return alert('일괄 적용할 LOT 번호를 입력/선택하세요.');
|
||||
const targets = rows.filter(r=>r.checked);
|
||||
if(!targets.length) return alert('적용할 행(체크)을 선택하세요.');
|
||||
rows.forEach(r=>{ if(r.checked) r.lot = lot; });
|
||||
renderMain();
|
||||
});
|
||||
|
||||
/* ===== 작업자 배치(부서 선택 → 체크) ===== */
|
||||
let currentDept=null, chosen=new Map(); // id -> user
|
||||
function fillDept(){ const $s=$('#deptSelect').empty(); Object.keys(DEPTS).forEach((k,i)=>$s.append(`<option ${i? '':'selected'}>${k}</option>`)); currentDept=$s.val(); }
|
||||
function renderWorkers(){
|
||||
const q=($('#workerSearch').val()||'').trim().toLowerCase();
|
||||
const list=(DEPTS[currentDept]||[]).filter(u=>u.name.toLowerCase().includes(q)||u.role.toLowerCase().includes(q));
|
||||
$('#workerList').html(list.map(u=>`
|
||||
<tr>
|
||||
<td><input type="checkbox" class="chk-worker" data-id="${u.id}" ${chosen.has(u.id)?'checked':''}></td>
|
||||
<td>${u.name}</td><td class="text-muted">${u.role}</td>
|
||||
</tr>`).join(''));
|
||||
renderChosen();
|
||||
}
|
||||
function renderChosen(){
|
||||
const $w=$('#chosenWrap').empty();
|
||||
if(!chosen.size) return $w.append('<span class="text-muted small">선택된 작업자가 없습니다.</span>');
|
||||
chosen.forEach(u=>$w.append(
|
||||
`<span class="badge bg-secondary-subtle border text-body d-flex align-items-center gap-1">
|
||||
${u.name}<span class="text-muted small">(${u.role})</span>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-1 btn-pill-remove" data-id="${u.id}">×</button>
|
||||
</span>`
|
||||
));
|
||||
}
|
||||
$('#deptSelect').on('change', function(){ currentDept=this.value; renderWorkers(); });
|
||||
$('#workerSearch').on('input', renderWorkers);
|
||||
$(document).on('change','.chk-worker',function(){
|
||||
const id=$(this).data('id'); const u=Object.values(DEPTS).flat().find(x=>x.id===id);
|
||||
if($(this).is(':checked')) chosen.set(id,u); else chosen.delete(id); renderChosen();
|
||||
});
|
||||
$(document).on('click','.btn-pill-remove',function(){ chosen.delete($(this).data('id')); renderWorkers(); });
|
||||
|
||||
/* ===== 부자재 행 추가/삭제 ===== */
|
||||
$('#btnAddSub').on('click', addSubRow);
|
||||
$(document).on('click','.btn-del-sub',function(){
|
||||
const sid=$(this).closest('tr').data('sid'); subItems=subItems.filter(x=>x.id!==sid); renderSub();
|
||||
});
|
||||
|
||||
/* ===== 저장(모의) ===== */
|
||||
$('#btnSaveWI').on('click', function(){
|
||||
const payload = {
|
||||
job_id: <?= json_encode($jobId) ?>,
|
||||
order_no: <?= json_encode($orderNo) ?>,
|
||||
priority: $('#setPriority').val(),
|
||||
main_rows: rows,
|
||||
sub_items: subItems,
|
||||
workers: Array.from(chosen.values())
|
||||
};
|
||||
console.log('[WORK INSTRUCTION SAVE]', payload);
|
||||
alert('저장(모의). 콘솔을 확인하세요.');
|
||||
// 실제: $.post('/tenant/api/production/save_work_instruction.php', payload)
|
||||
});
|
||||
|
||||
/* 초기 구동 */
|
||||
renderMain(); renderSub(); fillDept(); renderWorkers();
|
||||
|
||||
})(jQuery);
|
||||
</script>
|
||||
8
public/tenant/quality/docs.php
Normal file
8
public/tenant/quality/docs.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='quality';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/quality/record.php
Normal file
8
public/tenant/quality/record.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='quality';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/quality/request.php
Normal file
8
public/tenant/quality/request.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='quality';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/quality/schedule.php
Normal file
8
public/tenant/quality/schedule.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='quality';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/shipment/monthly_schedule.php
Normal file
8
public/tenant/shipment/monthly_schedule.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='shipment';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/shipment/status.php
Normal file
8
public/tenant/shipment/status.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='shipment';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/subscription/list.php
Normal file
8
public/tenant/subscription/list.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='subscription';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/subscription/tenant_list.php
Normal file
8
public/tenant/subscription/tenant_list.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='subscription';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/subscription/tenant_product_list.php
Normal file
8
public/tenant/subscription/tenant_product_list.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='subscription';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
346
public/tenant/tenant/department_list.php
Normal file
346
public/tenant/tenant/department_list.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<!-- 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>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-secondary" id="btnToggleAll">전체 접기</button>
|
||||
<button class="btn btn-primary" id="btnOpenAddRoot">+ 부서 등록</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle text-center" id="deptTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:80px;">#</th>
|
||||
<th class="text-start">조직(부서/팀/파트)</th>
|
||||
<th>설명</th>
|
||||
<th style="width:200px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="deptTbody"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<div class="modal fade" id="deptAddModal" 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="deptAddForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="parent_id" id="add_parent_id" value="">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 조직</label>
|
||||
<select class="form-select" id="add_parent_select">
|
||||
<!-- JS로 계층 옵션 렌더 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">부서명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="dept_name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="dept_desc" maxlength="100">
|
||||
</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="deptEditModal" 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="deptEditForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="dept_id" id="edit_dept_id">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">상위 조직</label>
|
||||
<select class="form-select" id="edit_parent_select">
|
||||
<!-- JS로 계층 옵션 렌더 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">부서명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="dept_name" id="edit_dept_name" maxlength="50" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="dept_desc" id="edit_dept_desc" maxlength="100">
|
||||
</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="deptDeleteModal" 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_dept_id">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" data-bs-dismiss="modal" type="button">취소</button>
|
||||
<button class="btn btn-danger" id="btnConfirmDelete" type="button">삭제</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* 트리 인덴트 표현 */
|
||||
#deptTbody tr[data-level] td.name-cell {
|
||||
--indent: calc( (var(--level, 0) * 18px) );
|
||||
padding-left: calc(var(--indent) + .25rem) !important;
|
||||
}
|
||||
.caret {
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
width:1.15rem; height:1.15rem; border-radius:4px; border:1px solid #cbd3e1;
|
||||
background:#f6f8fc; cursor:pointer; margin-right:.35rem; font-size:.8rem;
|
||||
user-select:none;
|
||||
}
|
||||
.caret[aria-expanded="false"]::after { content:"+"; }
|
||||
.caret[aria-expanded="true"]::after { content:"–"; }
|
||||
.badge-node { background:#e9f0fb; color:#2c4a85; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// ------- 샘플 데이터 (id, parent_id, name, desc) -------
|
||||
let SEQ = 7;
|
||||
const nodes = [
|
||||
{id:1, parent_id:0, name:'경영지원본부', desc:'인사/총무/회계'},
|
||||
{id:2, parent_id:1, name:'인사팀', desc:'인사 기획/운영'},
|
||||
{id:3, parent_id:1, name:'총무팀', desc:'총무/자산/구매'},
|
||||
{id:4, parent_id:0, name:'영업본부', desc:'고객/채널'},
|
||||
{id:5, parent_id:4, name:'영업1팀', desc:'대기업 영업'},
|
||||
{id:6, parent_id:0, name:'생산본부', desc:'제조/품질'},
|
||||
{id:7, parent_id:6, name:'생산팀', desc:'현장 생산 관리'},
|
||||
];
|
||||
|
||||
// 확장 상태 기억
|
||||
const expanded = new Set(); // 펼친 id
|
||||
|
||||
function childrenOf(pid){
|
||||
return nodes.filter(n => n.parent_id === pid);
|
||||
}
|
||||
function hasChildren(id){
|
||||
return nodes.some(n => n.parent_id === id);
|
||||
}
|
||||
|
||||
// 계층 순회(전개)하여 플랫 렌더 데이터 구성
|
||||
function flatten(rootPid=0, level=0){
|
||||
const out = [];
|
||||
const ch = childrenOf(rootPid).sort((a,b)=>a.name.localeCompare(b.name,'ko'));
|
||||
for(const n of ch){
|
||||
out.push({...n, level});
|
||||
if(expanded.has(n.id)){
|
||||
out.push(...flatten(n.id, level+1));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function render(){
|
||||
const rows = flatten(0,0);
|
||||
const html = rows.map(r=>{
|
||||
const hasKid = hasChildren(r.id);
|
||||
const caret = hasKid
|
||||
? `<span class="caret" data-id="${r.id}" aria-expanded="${expanded.has(r.id)}"></span>`
|
||||
: `<span style="display:inline-block; width:1.15rem; margin-right:.35rem;"></span>`;
|
||||
return `
|
||||
<tr data-id="${r.id}" data-level="${r.level}" style="--level:${r.level}">
|
||||
<td>${r.id}</td>
|
||||
<td class="text-start name-cell">
|
||||
${caret}
|
||||
<span class="badge badge-node me-2">Lv.${r.level}</span>
|
||||
<strong>${escapeHtml(r.name)}</strong>
|
||||
</td>
|
||||
<td>${escapeHtml(r.desc||'')}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button class="btn btn-sm btn-outline-primary btn-add-child" data-id="${r.id}">하위등록</button>
|
||||
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${r.id}">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btn-del" data-id="${r.id}">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
$('#deptTbody').html(html);
|
||||
}
|
||||
|
||||
// 옵션용 계층 라벨 만들기
|
||||
function buildOptions(selectedParentId=null, excludeId=null){
|
||||
function walk(pid=0, level=0){
|
||||
const list = childrenOf(pid).sort((a,b)=>a.name.localeCompare(b.name,'ko'));
|
||||
let out = [];
|
||||
for(const n of list){
|
||||
if(excludeId && n.id===excludeId) continue; // 자기 자신은 상위로 못 올림
|
||||
const label = `${'— '.repeat(level)}${n.name}`;
|
||||
out.push(`<option value="${n.id}" ${selectedParentId==n.id?'selected':''}>${escapeHtml(label)}</option>`);
|
||||
out.push(...walk(n.id, level+1));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 최상위(루트) 선택
|
||||
return [`<option value="0" ${selectedParentId==0?'selected':''}>(최상위)</option>`, ...walk()].join('');
|
||||
}
|
||||
|
||||
// 유틸
|
||||
function escapeHtml(s){ return String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }
|
||||
function findNode(id){ return nodes.find(n=>n.id==id); }
|
||||
function removeBranch(id){
|
||||
// 해당 노드와 모든 하위 재귀 삭제
|
||||
const kids = childrenOf(id);
|
||||
for(const k of kids) removeBranch(k.id);
|
||||
const i = nodes.findIndex(n=>n.id==id);
|
||||
if(i>-1) nodes.splice(i,1);
|
||||
expanded.delete(id);
|
||||
}
|
||||
|
||||
// 펼침/접음
|
||||
$(document).on('click', '.caret', function(){
|
||||
const id = +$(this).data('id');
|
||||
if(expanded.has(id)) expanded.delete(id); else expanded.add(id);
|
||||
render();
|
||||
});
|
||||
|
||||
// 전체 접기/펼치기 토글
|
||||
$('#btnToggleAll').on('click', function(){
|
||||
const allIds = nodes.map(n=>n.id);
|
||||
const allExpanded = allIds.every(id => expanded.has(id) || !hasChildren(id));
|
||||
if(allExpanded){
|
||||
expanded.clear();
|
||||
$(this).text('전체 펼치기');
|
||||
}else{
|
||||
expanded.clear();
|
||||
allIds.forEach(id => { if(hasChildren(id)) expanded.add(id); });
|
||||
$(this).text('전체 접기');
|
||||
}
|
||||
render();
|
||||
});
|
||||
|
||||
// 루트 추가 버튼
|
||||
$('#btnOpenAddRoot').on('click', function(){
|
||||
$('#add_parent_id').val(0);
|
||||
$('#add_parent_select').html(buildOptions(0));
|
||||
$('#deptAddForm')[0].reset();
|
||||
new bootstrap.Modal('#deptAddModal').show();
|
||||
});
|
||||
|
||||
// 행: 하위등록
|
||||
$(document).on('click', '.btn-add-child', function(){
|
||||
const id = +$(this).data('id');
|
||||
$('#add_parent_id').val(id);
|
||||
$('#add_parent_select').html(buildOptions(id));
|
||||
$('#deptAddForm')[0].reset();
|
||||
// 부모 펼쳐 둠
|
||||
expanded.add(id);
|
||||
new bootstrap.Modal('#deptAddModal').show();
|
||||
});
|
||||
|
||||
// 등록 처리(프로토타입: 클라이언트만)
|
||||
$('#deptAddForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const pid = +$('#add_parent_select').val();
|
||||
const name = $(this).find('[name="dept_name"]').val().trim();
|
||||
const desc = $(this).find('[name="dept_desc"]').val().trim();
|
||||
if(name.length<2){ alert('부서명을 2글자 이상 입력하세요.'); return; }
|
||||
const id = ++SEQ;
|
||||
nodes.push({id, parent_id:pid, name, desc});
|
||||
expanded.add(pid);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('deptAddModal')).hide();
|
||||
|
||||
// 실제 서버 전송 예:
|
||||
// $.post('/tenant/tenant/department_add_process.php', {parent_id:pid, dept_name:name, dept_desc:desc})
|
||||
});
|
||||
|
||||
// 행: 수정 열기
|
||||
$(document).on('click', '.btn-edit', function(){
|
||||
const id = +$(this).data('id');
|
||||
const n = findNode(id);
|
||||
if(!n) return;
|
||||
$('#edit_dept_id').val(n.id);
|
||||
$('#edit_dept_name').val(n.name);
|
||||
$('#edit_dept_desc').val(n.desc||'');
|
||||
$('#edit_parent_select').html(buildOptions(n.parent_id, n.id)); // 자신 제외
|
||||
new bootstrap.Modal('#deptEditModal').show();
|
||||
});
|
||||
|
||||
// 수정 처리(프로토타입)
|
||||
$('#deptEditForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const id = +$('#edit_dept_id').val();
|
||||
const pid = +$('#edit_parent_select').val();
|
||||
const name = $('#edit_dept_name').val().trim();
|
||||
const desc = $('#edit_dept_desc').val().trim();
|
||||
if(name.length<2){ alert('부서명을 2글자 이상 입력하세요.'); return; }
|
||||
|
||||
const n = findNode(id);
|
||||
if(!n) return;
|
||||
n.name = name; n.desc = desc; n.parent_id = pid;
|
||||
expanded.add(pid);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('deptEditModal')).hide();
|
||||
|
||||
// 실제 서버 전송 예:
|
||||
// $.post('/tenant/tenant/department_edit_process.php', {dept_id:id, parent_id:pid, dept_name:name, dept_desc:desc})
|
||||
});
|
||||
|
||||
// 행: 삭제 열기
|
||||
$(document).on('click', '.btn-del', function(){
|
||||
const id = +$(this).data('id');
|
||||
$('#del_dept_id').val(id);
|
||||
new bootstrap.Modal('#deptDeleteModal').show();
|
||||
});
|
||||
|
||||
// 삭제 처리(프로토타입)
|
||||
$('#btnConfirmDelete').on('click', function(){
|
||||
const id = +$('#del_dept_id').val();
|
||||
removeBranch(id);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('deptDeleteModal')).hide();
|
||||
|
||||
// 실제 서버 전송 예:
|
||||
// location.href = '/tenant/tenant/department_delete.php?id='+id;
|
||||
});
|
||||
|
||||
// 초기: 1~2단계만 펼치기
|
||||
expanded.add(1); expanded.add(4); expanded.add(6);
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
108
public/tenant/tenant/edit.php
Normal file
108
public/tenant/tenant/edit.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 샘플: 세션 또는 DB에서 기존 데이터 조회 (실제로는 DB에서 가져와야 함)
|
||||
$tenant = [
|
||||
'company_name' => isset($_SESSION['company_name']) ? $_SESSION['company_name'] : '샘플주식회사',
|
||||
'code' => isset($_SESSION['tenant_code']) ? $_SESSION['tenant_code'] : 'samplecorp',
|
||||
'email' => isset($_SESSION['tenant_email']) ? $_SESSION['tenant_email'] : 'admin@sample.com',
|
||||
'phone' => isset($_SESSION['tenant_phone']) ? $_SESSION['tenant_phone'] : '02-1234-5678',
|
||||
'address' => isset($_SESSION['tenant_address']) ? $_SESSION['tenant_address'] : '서울시 강남구',
|
||||
'business_num' => isset($_SESSION['tenant_business_num']) ? $_SESSION['tenant_business_num'] : '1234567890',
|
||||
'corp_reg_no' => isset($_SESSION['tenant_corp_reg_no']) ? $_SESSION['tenant_corp_reg_no'] : '',
|
||||
'ceo_name' => isset($_SESSION['tenant_ceo_name']) ? $_SESSION['tenant_ceo_name'] : '홍길동',
|
||||
'homepage' => isset($_SESSION['tenant_homepage']) ? $_SESSION['tenant_homepage'] : '',
|
||||
'fax' => isset($_SESSION['tenant_fax']) ? $_SESSION['tenant_fax'] : '',
|
||||
'admin_memo' => isset($_SESSION['tenant_admin_memo']) ? $_SESSION['tenant_admin_memo'] : '',
|
||||
'max_users' => isset($_SESSION['tenant_max_users']) ? $_SESSION['tenant_max_users'] : 10,
|
||||
'logo' => isset($_SESSION['tenant_logo']) ? $_SESSION['tenant_logo'] : '',
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width:600px; margin-top:40px; margin-bottom:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<form id="tenantEditForm" method="post" action="/tenant/tenant/edit_process.php" enctype="multipart/form-data" autocomplete="off">
|
||||
<h3 class="mb-4 text-center">회사(테넌트) 정보 수정</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사/조직명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="company_name" maxlength="100" value="<?=htmlspecialchars($tenant['company_name'])?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사 코드 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="code" maxlength="50" value="<?=htmlspecialchars($tenant['code'])?>" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표 이메일</label>
|
||||
<input type="email" class="form-control" name="email" maxlength="80" value="<?=htmlspecialchars($tenant['email'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표 전화번호</label>
|
||||
<input type="text" class="form-control" name="phone" maxlength="20" value="<?=htmlspecialchars($tenant['phone'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">주소</label>
|
||||
<input type="text" class="form-control" name="address" maxlength="255" value="<?=htmlspecialchars($tenant['address'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">사업자등록번호</label>
|
||||
<input type="text" class="form-control" name="business_num" maxlength="12" value="<?=htmlspecialchars($tenant['business_num'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">법인등록번호</label>
|
||||
<input type="text" class="form-control" name="corp_reg_no" maxlength="13" value="<?=htmlspecialchars($tenant['corp_reg_no'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표자명</label>
|
||||
<input type="text" class="form-control" name="ceo_name" maxlength="50" value="<?=htmlspecialchars($tenant['ceo_name'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">홈페이지 주소</label>
|
||||
<input type="url" class="form-control" name="homepage" maxlength="255" value="<?=htmlspecialchars($tenant['homepage'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">팩스번호</label>
|
||||
<input type="text" class="form-control" name="fax" maxlength="30" value="<?=htmlspecialchars($tenant['fax'])?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사 로고</label>
|
||||
<?php if ($tenant['logo']): ?>
|
||||
<div class="mb-2"><img src="<?=htmlspecialchars($tenant['logo'])?>" alt="로고" style="height:40px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control" name="logo" accept="image/*">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">관리자 메모</label>
|
||||
<textarea class="form-control" name="admin_memo" maxlength="500" rows="2"><?=htmlspecialchars($tenant['admin_memo'])?></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">최대 사용자 수</label>
|
||||
<input type="number" class="form-control" name="max_users" min="1" max="999" value="<?=htmlspecialchars($tenant['max_users'])?>">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mt-2">수정하기</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#tenantEditForm').on('submit', function(e){
|
||||
var company = $('[name="company_name"]').val().trim();
|
||||
var biz = $('[name="business_num"]').val();
|
||||
if (company.length < 2) {
|
||||
alert('회사명을 2글자 이상 입력하세요.');
|
||||
$('[name="company_name"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (biz && !/^\d{10}$/.test(biz)) {
|
||||
alert('사업자등록번호는 10자리 숫자만 입력하세요.');
|
||||
$('[name="business_num"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
// 기타 밸리데이션 추가 가능
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
87
public/tenant/tenant/join.php
Normal file
87
public/tenant/tenant/join.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:600px; margin-top:40px; margin-bottom:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<form id="tenantJoinForm" method="post" action="/tenant/tenant/join_process.php" enctype="multipart/form-data" autocomplete="off">
|
||||
<h3 class="mb-4 text-center">회사(테넌트) 가입</h3>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사/조직명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="company_name" maxlength="100" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사 코드 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="code" maxlength="50" required placeholder="영문/숫자, 고유식별코드">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표 이메일</label>
|
||||
<input type="email" class="form-control" name="email" maxlength="80">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표 전화번호</label>
|
||||
<input type="text" class="form-control" name="phone" maxlength="20">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">주소</label>
|
||||
<input type="text" class="form-control" name="address" maxlength="255">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">사업자등록번호</label>
|
||||
<input type="text" class="form-control" name="business_num" maxlength="12" placeholder="10자리(숫자만)">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">법인등록번호</label>
|
||||
<input type="text" class="form-control" name="corp_reg_no" maxlength="13">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">대표자명</label>
|
||||
<input type="text" class="form-control" name="ceo_name" maxlength="50">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">홈페이지 주소</label>
|
||||
<input type="url" class="form-control" name="homepage" maxlength="255" placeholder="https://">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">팩스번호</label>
|
||||
<input type="text" class="form-control" name="fax" maxlength="30">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회사 로고</label>
|
||||
<input type="file" class="form-control" name="logo" accept="image/*">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">관리자 메모</label>
|
||||
<textarea class="form-control" name="admin_memo" maxlength="500" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">최대 사용자 수</label>
|
||||
<input type="number" class="form-control" name="max_users" min="1" max="999" value="10">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 mt-2">가입하기</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#tenantJoinForm').on('submit', function(e){
|
||||
var code = $('[name="code"]').val().trim();
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(code)) {
|
||||
alert('회사 코드는 영문, 숫자, -, _만 사용할 수 있습니다.');
|
||||
$('[name="code"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
var biz = $('[name="business_num"]').val();
|
||||
if (biz && !/^\d{10}$/.test(biz)) {
|
||||
alert('사업자등록번호는 10자리 숫자만 입력하세요.');
|
||||
$('[name="business_num"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
// 기타 밸리데이션 추가 가능
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
38
public/tenant/tenant/role_add.php
Normal file
38
public/tenant/tenant/role_add.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
<div class="container" style="max-width:500px; margin-top:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<h4 class="mb-3 text-center">역할 등록</h4>
|
||||
<form id="roleAddForm" method="post" action="/tenant/tenant/role_add_process.php" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">역할명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="role_name" maxlength="30" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="role_desc" maxlength="100">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-50">등록</button>
|
||||
<a href="/tenant/tenant/role_list.php" class="btn btn-secondary w-50">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#roleAddForm').on('submit', function(e){
|
||||
var name = $('[name="role_name"]').val().trim();
|
||||
if (name.length < 2) {
|
||||
alert('역할명을 2글자 이상 입력하세요.');
|
||||
$('[name="role_name"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
19
public/tenant/tenant/role_delete.php
Normal file
19
public/tenant/tenant/role_delete.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
include '../inc/config.php';
|
||||
|
||||
// 실제로는 ?id=...로 전달받아 삭제 처리
|
||||
$role_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
// 실제 삭제 쿼리는 이곳에
|
||||
// 예: DELETE FROM roles WHERE id = $role_id
|
||||
|
||||
// 삭제 완료 메시지 후 역할 리스트로 이동
|
||||
echo <<<HTML
|
||||
<script>
|
||||
alert('역할이 삭제되었습니다.');
|
||||
window.location.href = '/tenant/tenant/role_list.php';
|
||||
</script>
|
||||
HTML;
|
||||
|
||||
exit;
|
||||
?>
|
||||
56
public/tenant/tenant/role_edit.php
Normal file
56
public/tenant/tenant/role_edit.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php'; ?>
|
||||
|
||||
// 샘플 데이터
|
||||
$roles = [
|
||||
1 => ['id'=>1, 'name'=>'최고관리자', 'desc'=>'회사 전체 관리권한'],
|
||||
2 => ['id'=>2, 'name'=>'일반관리자', 'desc'=>'부서/사용자/업무 관리'],
|
||||
3 => ['id'=>3, 'name'=>'일반직원', 'desc'=>'일반 사용 권한'],
|
||||
];
|
||||
|
||||
$role_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
$role = isset($roles[$role_id]) ? $roles[$role_id] : null;
|
||||
|
||||
if (!$role) {
|
||||
echo '<div class="alert alert-danger mt-4 text-center">해당 역할을 찾을 수 없습니다.</div>';
|
||||
include '../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width:500px; margin-top:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<h4 class="mb-3 text-center">역할 정보 수정</h4>
|
||||
<form id="roleEditForm" method="post" action="/tenant/tenant/role_edit_process.php" autocomplete="off">
|
||||
<input type="hidden" name="role_id" value="<?= htmlspecialchars($role['id']) ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">역할명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="role_name" maxlength="30" value="<?= htmlspecialchars($role['name']) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="role_desc" maxlength="100" value="<?= htmlspecialchars($role['desc']) ?>">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-50">수정</button>
|
||||
<a href="/tenant/tenant/role_list.php" class="btn btn-secondary w-50">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#roleEditForm').on('submit', function(e){
|
||||
var name = $('[name="role_name"]').val().trim();
|
||||
if (name.length < 2) {
|
||||
alert('역할명을 2글자 이상 입력하세요.');
|
||||
$('[name="role_name"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
198
public/tenant/tenant/role_list.php
Normal file
198
public/tenant/tenant/role_list.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<!-- 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="roleTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:80px;">#</th>
|
||||
<th>역할명</th>
|
||||
<th>설명</th>
|
||||
<th style="width:180px;">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="roleTbody"><!-- JS 렌더 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 등록 모달 -->
|
||||
<div class="modal fade" id="roleAddModal" 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="roleAddForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">역할명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="role_name" maxlength="30" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="role_desc" maxlength="100">
|
||||
</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="roleEditModal" 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="roleEditForm" autocomplete="off">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="role_id" id="edit_role_id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">역할명 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="role_name" id="edit_role_name" maxlength="30" required>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" name="role_desc" id="edit_role_desc" maxlength="100">
|
||||
</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="roleDeleteModal" 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_role_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(){
|
||||
// ------- 샘플 데이터 -------
|
||||
let SEQ = 3;
|
||||
const roles = [
|
||||
{id:1, name:'최고관리자', desc:'회사 전체 관리권한'},
|
||||
{id:2, name:'일반관리자', desc:'부서/사용자/업무 관리'},
|
||||
{id:3, name:'일반직원', desc:'일반 사용 권한'}
|
||||
];
|
||||
|
||||
function escapeHtml(s){ return String(s??'').replace(/[&<>"']/g, m=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[m])); }
|
||||
|
||||
function render(){
|
||||
const html = roles.map(r=>`
|
||||
<tr data-id="${r.id}">
|
||||
<td>${r.id}</td>
|
||||
<td>${escapeHtml(r.name)}</td>
|
||||
<td>${escapeHtml(r.desc||'')}</td>
|
||||
<td>
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button class="btn btn-sm btn-outline-secondary btn-edit" data-id="${r.id}">수정</button>
|
||||
<button class="btn btn-sm btn-outline-danger btn-del" data-id="${r.id}">삭제</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
$('#roleTbody').html(html);
|
||||
}
|
||||
|
||||
// 등록
|
||||
$('#btnOpenAdd').on('click', function(){
|
||||
$('#roleAddForm')[0].reset();
|
||||
new bootstrap.Modal('#roleAddModal').show();
|
||||
});
|
||||
$('#roleAddForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const name = $(this).find('[name="role_name"]').val().trim();
|
||||
const desc = $(this).find('[name="role_desc"]').val().trim();
|
||||
if(name.length<2){ alert('역할명을 2글자 이상 입력하세요.'); return; }
|
||||
const id = ++SEQ;
|
||||
roles.push({id, name, desc});
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('roleAddModal')).hide();
|
||||
|
||||
// 실제:
|
||||
// $.post('/tenant/tenant/role_add_process.php', {role_name:name, role_desc:desc}).done(()=>location.reload());
|
||||
});
|
||||
|
||||
// 수정
|
||||
$(document).on('click', '.btn-edit', function(){
|
||||
const id = +$(this).data('id');
|
||||
const r = roles.find(x=>x.id===id);
|
||||
if(!r) return;
|
||||
$('#edit_role_id').val(r.id);
|
||||
$('#edit_role_name').val(r.name);
|
||||
$('#edit_role_desc').val(r.desc||'');
|
||||
new bootstrap.Modal('#roleEditModal').show();
|
||||
});
|
||||
$('#roleEditForm').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
const id = +$('#edit_role_id').val();
|
||||
const name = $('#edit_role_name').val().trim();
|
||||
const desc = $('#edit_role_desc').val().trim();
|
||||
if(name.length<2){ alert('역할명을 2글자 이상 입력하세요.'); return; }
|
||||
const r = roles.find(x=>x.id===id);
|
||||
if(!r) return;
|
||||
r.name = name; r.desc = desc;
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('roleEditModal')).hide();
|
||||
|
||||
// 실제:
|
||||
// $.post('/tenant/tenant/role_edit_process.php', {role_id:id, role_name:name, role_desc:desc}).done(()=>location.reload());
|
||||
});
|
||||
|
||||
// 삭제
|
||||
$(document).on('click', '.btn-del', function(){
|
||||
const id = +$(this).data('id');
|
||||
$('#del_role_id').val(id);
|
||||
new bootstrap.Modal('#roleDeleteModal').show();
|
||||
});
|
||||
$('#btnConfirmDelete').on('click', function(){
|
||||
const id = +$('#del_role_id').val();
|
||||
const idx = roles.findIndex(x=>x.id===id);
|
||||
if(idx>-1) roles.splice(idx,1);
|
||||
render();
|
||||
bootstrap.Modal.getInstance(document.getElementById('roleDeleteModal')).hide();
|
||||
|
||||
// 실제:
|
||||
// location.href = '/tenant/tenant/role_delete.php?id='+id;
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
8
public/tenant/tenant/subscribe.php
Normal file
8
public/tenant/tenant/subscribe.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
$CURRENT_SECTION='tenant';
|
||||
include '../inc/header.php';
|
||||
?>
|
||||
|
||||
화면 준비중입니다.
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
124
public/tenant/tenant/user_edit.php
Normal file
124
public/tenant/tenant/user_edit.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 실제 환경에서는 ?id=...로 받아서 DB에서 해당 유저 정보 조회
|
||||
// 샘플 데이터
|
||||
$allowed_options = ['사번', '계좌번호']; // 이 테넌트(회사)에서 허용된 옵션
|
||||
|
||||
$users = [
|
||||
1 => [
|
||||
'id'=>1,
|
||||
'user_id'=>'kevin',
|
||||
'name'=>'권혁성',
|
||||
'email'=>'kevin@sample.com',
|
||||
'phone'=>'010-1111-2222',
|
||||
'options'=>json_encode(['사번'=>'A001','계좌번호'=>'111-2222-3333']),
|
||||
'profile_photo_path' => '',
|
||||
],
|
||||
2 => [
|
||||
'id'=>2,
|
||||
'user_id'=>'sally',
|
||||
'name'=>'김슬기',
|
||||
'email'=>'sally@sample.com',
|
||||
'phone'=>'010-3333-4444',
|
||||
'options'=>json_encode(['사번'=>'A002','계좌번호'=>'222-3333-4444']),
|
||||
'profile_photo_path' => '',
|
||||
],
|
||||
];
|
||||
|
||||
$user_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
$user = isset($users[$user_id]) ? $users[$user_id] : null;
|
||||
|
||||
if (!$user) {
|
||||
echo '<div class="alert alert-danger mt-4 text-center">해당 회원을 찾을 수 없습니다.</div>';
|
||||
include '../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
$user_options = json_decode($user['options'], true);
|
||||
?>
|
||||
|
||||
<div class="container" style="max-width:800px; margin-top:40px;">
|
||||
<div class="card shadow p-4">
|
||||
<h4 class="mb-3 text-center">회원 정보 수정</h4>
|
||||
<form id="userEditForm" method="post" action="/tenant/tenant/user_edit_process.php" enctype="multipart/form-data" autocomplete="off">
|
||||
<input type="hidden" name="user_id" value="<?= htmlspecialchars($user['id']) ?>">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">회원 아이디</label>
|
||||
<input type="text" class="form-control" name="user_id_display" value="<?= htmlspecialchars($user['user_id']) ?>" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">이름 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="name" maxlength="100" value="<?= htmlspecialchars($user['name']) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">이메일 <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" name="email" maxlength="100" value="<?= htmlspecialchars($user['email']) ?>" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">전화번호</label>
|
||||
<input type="text" class="form-control" name="phone" maxlength="30" value="<?= htmlspecialchars($user['phone']) ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">프로필 사진</label>
|
||||
<?php if ($user['profile_photo_path']): ?>
|
||||
<div class="mb-2"><img src="<?= htmlspecialchars($user['profile_photo_path']) ?>" alt="프로필" style="height:40px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control" name="profile_photo" accept="image/*">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">비밀번호 변경</label>
|
||||
<input type="password" class="form-control" name="password" maxlength="30" placeholder="변경 시 입력">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">비밀번호 확인</label>
|
||||
<input type="password" class="form-control" name="password2" maxlength="30" placeholder="변경 시 입력">
|
||||
</div>
|
||||
<?php foreach($allowed_options as $opt): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= htmlspecialchars($opt) ?></label>
|
||||
<input type="text" class="form-control" name="option_<?= urlencode($opt) ?>" value="<?= isset($user_options[$opt]) ? htmlspecialchars($user_options[$opt]) : '' ?>">
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary w-50">수정</button>
|
||||
<a href="/tenant/tenant/user_list.php" class="btn btn-secondary w-50">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#userEditForm').on('submit', function(e){
|
||||
var name = $('[name="name"]').val().trim();
|
||||
var email = $('[name="email"]').val().trim();
|
||||
var pw1 = $('[name="password"]').val();
|
||||
var pw2 = $('[name="password2"]').val();
|
||||
if (name.length < 2) {
|
||||
alert('이름은 2글자 이상 입력하세요.');
|
||||
$('[name="name"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||
alert('이메일을 올바르게 입력하세요.');
|
||||
$('[name="email"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1 || pw2) {
|
||||
if (pw1.length < 4) {
|
||||
alert('비밀번호는 4글자 이상이어야 합니다.');
|
||||
$('[name="password"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
if (pw1 !== pw2) {
|
||||
alert('비밀번호가 일치하지 않습니다.');
|
||||
$('[name="password2"]').focus();
|
||||
e.preventDefault(); return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
347
public/tenant/tenant/user_list.php
Normal file
347
public/tenant/tenant/user_list.php
Normal file
@@ -0,0 +1,347 @@
|
||||
<!-- 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'; ?>
|
||||
20
public/tenant/tenant/user_unmap.php
Normal file
20
public/tenant/tenant/user_unmap.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
include '../inc/config.php';
|
||||
|
||||
// 실제로는 ?id=...로 받아서 매핑 해제 처리
|
||||
$user_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||
|
||||
// 실제 매핑 해제 쿼리는 이곳에
|
||||
// 예: DELETE FROM tenant_user_map WHERE user_id = $user_id AND tenant_id = 현재회사ID
|
||||
// 또는 users 테이블에서 deleted_at 세팅 등
|
||||
|
||||
// 샘플: 삭제 완료 메시지 후 유저 리스트로 이동
|
||||
echo <<<HTML
|
||||
<script>
|
||||
alert('회사의 회원 연결이 해제되었습니다.');
|
||||
window.location.href = '/tenant/tenant/user_list.php';
|
||||
</script>
|
||||
HTML;
|
||||
|
||||
exit;
|
||||
?>
|
||||
111
public/tenant/tenant/withdraw.php
Normal file
111
public/tenant/tenant/withdraw.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
$CURRENT_SECTION = 'tenant';
|
||||
include '../inc/header.php';
|
||||
|
||||
// 최고관리자 권한 체크(예시)
|
||||
// 실제 환경에서는 $_SESSION['user_role'] 또는 tenant 관리자 여부 등으로 확인
|
||||
if (!isset($_SESSION['user_role']) || $_SESSION['user_role'] !== 'tenant_admin') {
|
||||
echo '<div class="alert alert-danger mt-4 text-center">회사 관리자만 회사 탈퇴가 가능합니다.</div>';
|
||||
include '../inc/footer.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
$tenant_name = isset($_SESSION['tenant_name']) ? htmlspecialchars($_SESSION['tenant_name']) : '이 회사';
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="card shadow p-4" style="width: 420px;">
|
||||
<form id="tenantWithdrawForm" method="post" action="/tenant/tenant/withdraw_process.php" autocomplete="off">
|
||||
<h3 class="text-center mb-4 text-danger">회사(테넌트) 해지</h3>
|
||||
<div class="alert alert-danger small text-center mb-4 fw-bold">
|
||||
<b>※ 반드시 확인해주세요!</b><br>
|
||||
<span class="d-block mt-2 mb-1">
|
||||
· <b><?= $tenant_name ?></b>의 모든 데이터, 계정, 파일이 <u>영구 삭제</u>됩니다.<br>
|
||||
· 결제 중인 서비스/구독도 즉시 중단 및 해지됩니다.<br>
|
||||
· <u>복구는 불가</u>합니다. 탈퇴 후에는 어떤 이유로도 데이터/계정 복구가 불가합니다.<br>
|
||||
· 탈퇴 즉시 소속 모든 유저가 로그아웃/접근 차단됩니다.<br>
|
||||
</span>
|
||||
<span class="text-danger">
|
||||
이 작업은 최고관리자만 수행할 수 있습니다.
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="checkbox" id="agree" name="agree" required>
|
||||
<label for="agree" class="form-label ms-1">
|
||||
위 내용을 모두 이해했으며, 회사 해지에 동의합니다.
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="reason" class="form-label">해지 사유</label>
|
||||
<select class="form-select" id="reason" name="reason" required>
|
||||
<option value="">사유를 선택하세요</option>
|
||||
<option value="서비스 불만족">서비스 불만족</option>
|
||||
<option value="요금 부담">요금 부담</option>
|
||||
<option value="재가입 예정">재가입 예정</option>
|
||||
<option value="기타">기타 (직접 입력)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3" id="reasonEtcDiv" style="display:none;">
|
||||
<input type="text" class="form-control" id="reason_etc" name="reason_etc" maxlength="100" placeholder="해지 사유를 입력하세요">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="form-label">관리자 비밀번호 확인</label>
|
||||
<input type="password" class="form-control" id="password" name="password" maxlength="30" required placeholder="비밀번호를 입력하세요">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-danger w-50" id="withdrawBtn">탈퇴</button>
|
||||
<a href="/tenant/tenant/edit.php" class="btn btn-secondary w-50">취소</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
// 해지 사유 기타 선택시 입력란 노출
|
||||
$('#reason').on('change', function(){
|
||||
if ($(this).val() === '기타') {
|
||||
$('#reasonEtcDiv').show();
|
||||
$('#reason_etc').prop('required', true);
|
||||
} else {
|
||||
$('#reasonEtcDiv').hide();
|
||||
$('#reason_etc').prop('required', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 이중 확인 및 밸리데이션
|
||||
$('#withdrawBtn').on('click', function(){
|
||||
if (!$('#agree').is(':checked')) {
|
||||
alert('동의 체크가 필요합니다.');
|
||||
$('#agree').focus();
|
||||
return false;
|
||||
}
|
||||
var reason = $('#reason').val();
|
||||
var reasonEtc = $('#reason_etc').val();
|
||||
var pw = $('#password').val();
|
||||
|
||||
if (!reason) {
|
||||
alert('해지 사유를 선택하세요.');
|
||||
$('#reason').focus();
|
||||
return false;
|
||||
}
|
||||
if (reason === '기타' && (!reasonEtc || reasonEtc.trim().length < 2)) {
|
||||
alert('해지 사유를 입력하세요.');
|
||||
$('#reason_etc').focus();
|
||||
return false;
|
||||
}
|
||||
if (!pw || pw.length < 4) {
|
||||
alert('비밀번호를 올바르게 입력하세요.');
|
||||
$('#password').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 이중 확인 팝업
|
||||
if (confirm("정말로 회사를 완전히 탈퇴(해지)하시겠습니까?\n이 작업은 되돌릴 수 없습니다.")) {
|
||||
$('#tenantWithdrawForm').submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include '../inc/footer.php'; ?>
|
||||
37
public/tenant/tenant_base.html
Normal file
37
public/tenant/tenant_base.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>사용자 페이지</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { background-color: #dfe6f0; }
|
||||
header, footer { background-color: #2c4a85; color: white; padding: 1rem; }
|
||||
nav a { color: white; margin-right: 1rem; }
|
||||
nav a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>MyCompany 사용자 페이지</h1>
|
||||
<nav>
|
||||
<a href="/dashboard.html">대시보드</a>
|
||||
<a href="/profile.html">내 정보</a>
|
||||
<a href="/subscription.html">구독관리</a>
|
||||
<a href="/support.html">고객지원</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container my-4">
|
||||
<!-- 개별 페이지 내용 여기 삽입 -->
|
||||
</main>
|
||||
|
||||
<footer class="text-center">
|
||||
© 2025 MyCompany. All rights reserved.
|
||||
</footer>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
public/tenant/test.html
Normal file
27
public/tenant/test.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Highmaps Only Test</title>
|
||||
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
||||
<script src="https://code.highcharts.com/maps/highmaps.js"></script>
|
||||
<script src="https://code.highcharts.com/mapdata/custom/world.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mapContainer" style="height: 500px; min-width: 310px; max-width: 800px; margin: 0 auto;"></div>
|
||||
<script>
|
||||
$(function() {
|
||||
Highcharts.mapChart('mapContainer', {
|
||||
chart: { map: 'custom/world' },
|
||||
title: { text: 'World Map Example' },
|
||||
series: [{
|
||||
data: [['kr', 45], ['us', 28], ['de', 15]],
|
||||
name: 'Revenue',
|
||||
states: { hover: { color: '#BADA55' } },
|
||||
dataLabels: { enabled: true, format: '{point.name}' }
|
||||
}]
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user