diff --git a/salesmanagement/api/get_performance.php b/salesmanagement/api/get_performance.php
new file mode 100644
index 0000000..79bc630
--- /dev/null
+++ b/salesmanagement/api/get_performance.php
@@ -0,0 +1,215 @@
+prepare("SELECT * FROM sales_member WHERE member_id = 'sales' LIMIT 1");
+ $stmt->execute();
+ $testUser = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($testUser) {
+ $currentUser = $testUser;
+ $userId = $currentUser['id'];
+ $userRole = $currentUser['role'];
+ } else {
+ echo json_encode(['success' => false, 'error' => '로그인이 필요합니다.']);
+ exit;
+ }
+ } catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => '로그인이 필요합니다.']);
+ exit;
+ }
+} else {
+ $currentUser = $_SESSION['sales_user'];
+ $userId = $currentUser['id'];
+ $userRole = $currentUser['role'];
+}
+
+// 기간 필터링 (기본값: 당월)
+$startDate = $_GET['startDate'] ?? date('Y-m-01');
+$endDate = $_GET['endDate'] ?? date('Y-m-t');
+
+try {
+ $pdo = db_connect();
+
+ // 1. 조직도 트리 생성 함수 (재귀)
+ function buildOrgTree($pdo, $parentId, $depth, $startDate, $endDate, $targetUserId) {
+ // 이 멤버의 정보 가져오기
+ $stmt = $pdo->prepare("SELECT id, name, role FROM sales_member WHERE id = ? AND is_active = 1");
+ $stmt->execute([$parentId]);
+ $member = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$member) return null;
+
+ // 이 멤버의 직접 실적 가져오기
+ $stmt = $pdo->prepare("SELECT id, customer_name as customer, contract_date as contractDate, amount FROM sales_record WHERE member_id = ? AND status = 'completed' AND contract_date BETWEEN ? AND ?");
+ $stmt->execute([$parentId, $startDate, $endDate]);
+ $directContracts = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $directSales = 0;
+ foreach ($directContracts as $c) {
+ $directSales += $c['amount'];
+ }
+
+ // 하위 멤버들 가져오기
+ $stmt = $pdo->prepare("SELECT id FROM sales_member WHERE parent_id = ? AND is_active = 1");
+ $stmt->execute([$parentId]);
+ $childrenIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ $children = [];
+ $totalSales = $directSales;
+ $totalContractCount = count($directContracts);
+
+ foreach ($childrenIds as $childId) {
+ $childNode = buildOrgTree($pdo, $childId, $depth + 1, $startDate, $endDate, $targetUserId);
+ if ($childNode) {
+ $children[] = $childNode;
+ $totalSales += $childNode['totalSales_subtree'];
+ $totalContractCount += $childNode['contractCount_subtree'];
+ }
+ }
+
+ // 수당 계산 (targetUserId 기준)
+ // targetUserId == parentId 이면 본인의 직접 판매 (20%)
+ // targetUserId 가 parentId의 부모이면 (depth=1) 관리자 수당 (5%)
+ // targetUserId 가 parentId의 조부모이면 (depth=2) 교육자 수당 (3%)
+
+ $commission = 0;
+ if ($parentId == $targetUserId) {
+ $commission = $directSales * 0.20;
+ } else if ($depth == 1) {
+ $commission = $directSales * 0.05;
+ } else if ($depth == 2) {
+ $commission = $directSales * 0.03;
+ }
+
+ return [
+ 'id' => 'node_' . $member['id'],
+ 'real_id' => $member['id'],
+ 'name' => $member['name'],
+ 'role' => $member['role'],
+ 'depth' => $depth,
+ 'isDirect' => ($parentId == $targetUserId),
+ 'directSales' => $directSales,
+ 'totalSales' => $totalSales,
+ 'contractCount' => $totalContractCount,
+ 'commission' => $commission,
+ 'contracts' => $directContracts,
+ 'children' => $children
+ ];
+ }
+
+ // 대상 사용자 결정
+ $rootUserId = $userId;
+ if ($userRole === 'operator' && isset($_GET['target_id'])) {
+ $rootUserId = $_GET['target_id'];
+ }
+
+ // 트리 구축 (본인 노드)
+ $rootNode = buildOrgTree($pdo, $rootUserId, 0, $startDate, $endDate, $rootUserId);
+
+ // 프론트엔드 형식에 맞게 내 직접 판매 노드를 children의 첫번째로 삽입
+ if ($rootNode) {
+ $directNode = [
+ 'id' => 'root-direct',
+ 'name' => '내 직접 판매',
+ 'depth' => 0,
+ 'role' => '판매자',
+ 'isDirect' => true,
+ 'totalSales' => $rootNode['directSales'],
+ 'contractCount' => count($rootNode['contracts']),
+ 'commission' => $rootNode['directSales'] * 0.20,
+ 'contracts' => $rootNode['contracts'],
+ 'children' => []
+ ];
+
+ // Root node's children should include this direct node + actual children
+ $actualChildren = $rootNode['children'];
+ $rootNode['children'] = array_merge([$directNode], $actualChildren);
+
+ // 중요: Root 노드 자체의 수당은 하위 '내 직접 판매'에서 합산되므로 0으로 설정하거나 isDirect를 false로 변경
+ $rootNode['isDirect'] = false;
+ $rootNode['commission'] = 0;
+ }
+
+ // 전체 누적 실적 계산 (전체 기간)
+ function calculateTotalStats($pdo, $parentId, $targetUserId, $depth) {
+ $stmt = $pdo->prepare("SELECT amount FROM sales_record WHERE member_id = ? AND status = 'completed'");
+ $stmt->execute([$parentId]);
+ $amounts = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ $directSales = array_sum($amounts);
+ $count = count($amounts);
+
+ $commission = 0;
+ if ($parentId == $targetUserId) $commission = $directSales * 0.20;
+ else if ($depth == 1) $commission = $directSales * 0.05;
+ else if ($depth == 2) $commission = $directSales * 0.03;
+
+ $stats = [
+ 'totalSales' => $directSales,
+ 'totalCommission' => $commission,
+ 'totalCount' => $count
+ ];
+
+ $stmt = $pdo->prepare("SELECT id FROM sales_member WHERE parent_id = ? AND is_active = 1");
+ $stmt->execute([$parentId]);
+ $children = $stmt->fetchAll(PDO::FETCH_COLUMN);
+
+ foreach ($children as $childId) {
+ if ($depth < 2) { // 수당은 2단계 하위까지만
+ $childStats = calculateTotalStats($pdo, $childId, $targetUserId, $depth + 1);
+ $stats['totalSales'] += $childStats['totalSales'];
+ $stats['totalCommission'] += $childStats['totalCommission'];
+ $stats['totalCount'] += $childStats['totalCount'];
+ } else {
+ // 실적만 합산
+ $childStats = calculateTotalStats($pdo, $childId, $targetUserId, $depth + 1);
+ $stats['totalSales'] += $childStats['totalSales'];
+ $stats['totalCount'] += $childStats['totalCount'];
+ }
+ }
+
+ return $stats;
+ }
+
+ $totalStats = calculateTotalStats($pdo, $rootUserId, $rootUserId, 0);
+
+ // 역할별 수당 합계 계산 (현재 기간 트리 기반)
+ function summarizeCommissions($node, &$summary) {
+ if ($node['isDirect']) {
+ $summary['direct'] += $node['commission'];
+ } else if ($node['depth'] == 1) {
+ $summary['manager'] += $node['commission'];
+ } else if ($node['depth'] == 2) {
+ $summary['educator'] += $node['commission'];
+ }
+
+ foreach ($node['children'] as $child) {
+ summarizeCommissions($child, $summary);
+ }
+ }
+
+ $commissionSummary = ['direct' => 0, 'manager' => 0, 'educator' => 0];
+ if ($rootNode) {
+ summarizeCommissions($rootNode, $commissionSummary);
+ }
+
+ echo json_encode([
+ 'success' => true,
+ 'total_stats' => $totalStats,
+ 'period_stats' => [
+ 'startDate' => $startDate,
+ 'endDate' => $endDate,
+ 'commission_summary' => $commissionSummary,
+ 'total_period_commission' => array_sum($commissionSummary)
+ ],
+ 'org_tree' => $rootNode
+ ], JSON_UNESCAPED_UNICODE);
+
+} catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/salesmanagement/api/init_db.php b/salesmanagement/api/init_db.php
new file mode 100644
index 0000000..859d37f
--- /dev/null
+++ b/salesmanagement/api/init_db.php
@@ -0,0 +1,93 @@
+exec($sql);
+
+ // 2. sales_record 테이블 생성
+ $sql_record = "
+ CREATE TABLE IF NOT EXISTS `sales_record` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `member_id` int(11) NOT NULL COMMENT '영업 담당자 ID (sales_member.id)',
+ `customer_name` varchar(100) NOT NULL COMMENT '고객사 성명/사명',
+ `contract_date` date NOT NULL COMMENT '계약일 (가입비 완료일 기준)',
+ `amount` decimal(15, 2) NOT NULL DEFAULT 0.00 COMMENT '가입비 금액',
+ `status` varchar(20) DEFAULT 'completed' COMMENT '상태 (pending, completed, cancelled)',
+ `remarks` text DEFAULT NULL COMMENT '비고',
+ `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `idx_member_id` (`member_id`),
+ KEY `idx_contract_date` (`contract_date`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='영업 실적(가입비) 기록 테이블';
+ ";
+ $pdo->exec($sql_record);
+
+ // 기본 운영자 계정 생성
+ $check = $pdo->prepare("SELECT id FROM sales_member WHERE member_id = 'admin'");
+ $check->execute();
+ if (!$check->fetch()) {
+ $stmt = $pdo->prepare("INSERT INTO sales_member (member_id, password, name, role) VALUES ('admin', 'admin', '운영자', 'operator')");
+ $stmt->execute();
+ }
+
+ // 기본 영업관리자 계정 생성
+ $check = $pdo->prepare("SELECT id FROM sales_member WHERE member_id = 'sales'");
+ $check->execute();
+ if (!$check->fetch()) {
+ $stmt = $pdo->prepare("INSERT INTO sales_member (member_id, password, name, role) VALUES ('sales', 'sales', '영업관리자', 'sales_admin')");
+ $stmt->execute();
+ }
+ $sales_id = $pdo->lastInsertId() ?: 2;
+
+ // 기본 매니저 계정 생성 (영업관리자 하위)
+ $check = $pdo->prepare("SELECT id FROM sales_member WHERE member_id = 'manager'");
+ $check->execute();
+ if (!$check->fetch()) {
+ $stmt = $pdo->prepare("INSERT INTO sales_member (member_id, password, name, role, parent_id) VALUES ('manager', 'manager', '일반매니저', 'manager', ?)");
+ $stmt->execute([$sales_id]);
+ }
+ $manager_id = $pdo->lastInsertId() ?: 3;
+
+ // 샘플 실적 데이터 (데이터가 없을 때만 입력)
+ $check = $pdo->prepare("SELECT id FROM sales_record LIMIT 1");
+ $check->execute();
+ if (!$check->fetch()) {
+ $stmt = $pdo->prepare("INSERT INTO sales_record (member_id, customer_name, contract_date, amount) VALUES (?, ?, ?, ?)");
+
+ // 영업관리자(sales) 직접 실적
+ $stmt->execute([$sales_id, '스타트업 A', '2024-12-01', 25000000]);
+ $stmt->execute([$sales_id, '벤처기업 B', '2024-12-10', 30000000]);
+
+ // 매니저(manager) 실적 (sales의 1차 하위)
+ $stmt->execute([$manager_id, '매뉴팩처링 C', '2024-12-15', 20000000]);
+ $stmt->execute([$manager_id, '서비스 D', '2024-12-20', 15000000]);
+ }
+
+ echo "Success: Database initialized with sales_member and sales_record tables.";
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage();
+}
+?>
diff --git a/salesmanagement/api/sales_members.php b/salesmanagement/api/sales_members.php
new file mode 100644
index 0000000..faae792
--- /dev/null
+++ b/salesmanagement/api/sales_members.php
@@ -0,0 +1,215 @@
+exec("
+ CREATE TABLE IF NOT EXISTS `sales_member` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `member_id` varchar(50) NOT NULL COMMENT '로그인 ID',
+ `password` varchar(255) NOT NULL COMMENT '비밀번호',
+ `name` varchar(100) NOT NULL COMMENT '성명',
+ `phone` varchar(20) DEFAULT NULL COMMENT '전화번호',
+ `email` varchar(100) DEFAULT NULL COMMENT '이메일',
+ `parent_id` int(11) DEFAULT NULL COMMENT '상위 관리자 ID',
+ `role` varchar(20) DEFAULT 'manager' COMMENT '역할 (operator, manager)',
+ `remarks` text DEFAULT NULL COMMENT '비고',
+ `is_active` tinyint(1) DEFAULT 1 COMMENT '활성화 여부',
+ `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
+ `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `idx_member_id` (`member_id`),
+ KEY `idx_parent_id` (`parent_id`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+ ");
+
+ switch ($method) {
+ case 'GET':
+ if ($action === 'check_session') {
+ if (isset($_SESSION['sales_user'])) {
+ echo json_encode(['success' => true, 'user' => $_SESSION['sales_user']]);
+ } else {
+ echo json_encode(['success' => false]);
+ }
+ } elseif ($action === 'list') {
+ if (!isset($_SESSION['sales_user'])) throw new Exception("로그인이 필요합니다.");
+ $currentUser = $_SESSION['sales_user'];
+
+ if ($currentUser['role'] === 'operator') {
+ // 운영자는 상위/하위 상관없이 모든 활동중인 멤버 조회 (본인 제외)
+ $stmt = $pdo->prepare("SELECT id, member_id, name, phone, email, role, parent_id, remarks, created_at FROM sales_member WHERE is_active = 1 AND role != 'operator' ORDER BY name ASC");
+ $stmt->execute();
+ } else {
+ // 특정 관리자의 하위 멤버 목록 조회
+ $parent_id = $_GET['parent_id'] ?? $currentUser['id'];
+ $stmt = $pdo->prepare("SELECT id, member_id, name, phone, email, role, parent_id, remarks, created_at FROM sales_member WHERE parent_id = ? AND is_active = 1 ORDER BY name ASC");
+ $stmt->execute([$parent_id]);
+ }
+
+ $members = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ echo json_encode(['success' => true, 'data' => $members]);
+ }
+ break;
+
+ case 'POST':
+ $data = json_decode(file_get_contents('php://input'), true);
+
+ if ($action === 'login') {
+ $member_id = $data['member_id'] ?? '';
+ $password = $data['password'] ?? '';
+
+ $stmt = $pdo->prepare("SELECT * FROM sales_member WHERE member_id = ? AND is_active = 1");
+ $stmt->execute([$member_id]);
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($user && $password === $user['password']) {
+ $_SESSION['sales_user'] = $user;
+ echo json_encode(['success' => true, 'user' => $user]);
+ } else {
+ // 수동 초기화용 (개발용: plain text)
+ if ($member_id === 'admin' && $password === 'admin') {
+ $pdo->prepare("INSERT IGNORE INTO sales_member (member_id, password, name, role) VALUES ('admin', 'admin', '운영자', 'operator')")->execute();
+ $stmt = $pdo->prepare("SELECT * FROM sales_member WHERE member_id = 'admin'");
+ $stmt->execute();
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
+ $_SESSION['sales_user'] = $user;
+ echo json_encode(['success' => true, 'user' => $user, 'message' => 'Admin reset successful. Logged in.']);
+ return;
+ }
+ if ($member_id === 'sales' && $password === 'sales') {
+ $pdo->prepare("INSERT IGNORE INTO sales_member (member_id, password, name, role) VALUES ('sales', 'sales', '영업관리자', 'sales_admin')")->execute();
+ $stmt = $pdo->prepare("SELECT * FROM sales_member WHERE member_id = 'sales'");
+ $stmt->execute();
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
+ $_SESSION['sales_user'] = $user;
+ echo json_encode(['success' => true, 'user' => $user, 'message' => 'Sales admin reset successful. Logged in.']);
+ return;
+ }
+ if ($member_id === 'manager' && $password === 'manager') {
+ $pdo->prepare("INSERT IGNORE INTO sales_member (member_id, password, name, role) VALUES ('manager', 'manager', '일반매니저', 'manager')")->execute();
+ $stmt = $pdo->prepare("SELECT * FROM sales_member WHERE member_id = 'manager'");
+ $stmt->execute();
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
+ $_SESSION['sales_user'] = $user;
+ echo json_encode(['success' => true, 'user' => $user, 'message' => 'Manager reset successful. Logged in.']);
+ return;
+ }
+ throw new Exception("아이디 또는 비밀번호가 일치하지 않습니다.");
+ }
+ } elseif ($action === 'logout') {
+ session_destroy();
+ echo json_encode(['success' => true]);
+ } elseif ($action === 'create') {
+ if (!isset($_SESSION['sales_user'])) throw new Exception("로그인이 필요합니다.");
+ $currentUser = $_SESSION['sales_user'];
+
+ $member_id = $data['member_id'] ?? '';
+ $password = $data['password'] ?? '1234';
+ $name = $data['name'] ?? '';
+ $phone = $data['phone'] ?? '';
+ $email = $data['email'] ?? '';
+ $remarks = $data['remarks'] ?? '';
+
+ // 운영자는 parent_id와 role을 직접 지정 가능
+ if ($currentUser['role'] === 'operator') {
+ $parent_id = $data['parent_id'] ?: null;
+ $role = $data['role'] ?? 'manager';
+ } else {
+ $parent_id = $currentUser['id'];
+ $role = 'manager';
+ }
+
+ // 중복 체크
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM sales_member WHERE member_id = ?");
+ $stmt->execute([$member_id]);
+ if ($stmt->fetchColumn() > 0) throw new Exception("이미 존재하는 아이디입니다.");
+
+ $stmt = $pdo->prepare("INSERT INTO sales_member (member_id, password, name, phone, email, parent_id, role, remarks) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
+ $stmt->execute([$member_id, $password, $name, $phone, $email, $parent_id, $role, $remarks]);
+
+ echo json_encode(['success' => true, 'message' => '등록되었습니다.']);
+ } elseif ($action === 'check_id') {
+ $member_id = $data['member_id'] ?? '';
+ if (!$member_id) throw new Exception("아이디를 입력해주세요.");
+
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM sales_member WHERE member_id = ?");
+ $stmt->execute([$member_id]);
+ $exists = $stmt->fetchColumn() > 0;
+
+ echo json_encode([
+ 'success' => true,
+ 'exists' => $exists,
+ 'message' => $exists ? '이미 사용 중인 아이디입니다.' : '사용 가능한 아이디입니다.'
+ ]);
+ } elseif ($action === 'delete') {
+ $id = $data['id'] ?? $_GET['id'] ?? null;
+ if (!$id) throw new Exception("ID가 누락되었습니다.");
+
+ $stmt = $pdo->prepare("UPDATE sales_member SET is_active = 0 WHERE id = ?");
+ $stmt->execute([$id]);
+
+ echo json_encode(['success' => true, 'message' => '삭제되었습니다.']);
+ return;
+ }
+ break;
+
+ case 'PUT':
+ if (!isset($_SESSION['sales_user'])) throw new Exception("로그인이 필요합니다.");
+ $currentUser = $_SESSION['sales_user'];
+
+ $data = json_decode(file_get_contents('php://input'), true);
+ $id = $data['id'] ?? null;
+ if (!$id) throw new Exception("ID가 누락되었습니다.");
+
+ $updates = [];
+ $params = [];
+
+ $fields = ['name', 'phone', 'email', 'remarks'];
+ if ($currentUser['role'] === 'operator') {
+ $fields[] = 'role';
+ $fields[] = 'parent_id';
+ }
+
+ foreach ($fields as $field) {
+ if (isset($data[$field])) {
+ $updates[] = "$field = ?";
+ $params[] = ($field === 'parent_id' && $data[$field] === '') ? null : $data[$field];
+ }
+ }
+
+ if (isset($data['password']) && !empty($data['password'])) {
+ $updates[] = "password = ?";
+ $params[] = $data['password'];
+ }
+
+ if (empty($updates)) throw new Exception("수정할 내용이 없습니다.");
+
+ $params[] = $id;
+ $stmt = $pdo->prepare("UPDATE sales_member SET " . implode(", ", $updates) . " WHERE id = ?");
+ $stmt->execute($params);
+
+ echo json_encode(['success' => true, 'message' => '수정되었습니다.']);
+ break;
+
+ case 'DELETE':
+ $id = $_GET['id'] ?? null;
+ if (!$id) throw new Exception("ID가 누락되었습니다.");
+
+ $stmt = $pdo->prepare("UPDATE sales_member SET is_active = 0 WHERE id = ?");
+ $stmt->execute([$id]);
+
+ echo json_encode(['success' => true, 'message' => '삭제되었습니다.']);
+ break;
+ }
+
+} catch (Exception $e) {
+ echo json_encode(['success' => false, 'error' => $e->getMessage()]);
+}
diff --git a/salesmanagement/index.php b/salesmanagement/index.php
index 41e0041..e83fc85 100644
--- a/salesmanagement/index.php
+++ b/salesmanagement/index.php
@@ -55,22 +55,20 @@
if (window.lucide && ref.current) {
const i = document.createElement('i');
i.setAttribute('data-lucide', name);
- // Combine className and size helpers if needed, but existing code uses w-4 h-4 etc.
- // We just pass className through.
if (className) i.className = className;
-
ref.current.innerHTML = '';
ref.current.appendChild(i);
window.lucide.createIcons({ root: ref.current });
}
}, [name, className]);
- return ;
+ // Ensure icon itself doesn't eat clicks if no specific handler
+ return ;
};
// --- Components ---
// 1. Header Component
- const Header = ({ companyInfo, onOpenHelp, selectedRole, onRoleChange }) => {
+ const Header = ({ companyInfo, onOpenHelp, selectedRole, onRoleChange, currentUser, onLogout }) => {
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const profileMenuRef = React.useRef(null);
@@ -93,37 +91,64 @@
if (!companyInfo) return
;
- const roles = ['운영자', '영업관리'];
+ const roles = ['운영자', '영업관리', '매니저'];
return (
-
-
영업관리
+
+
+
+
+
+ SAM 영업관리
+
+
+ {currentUser && (
+
+
+
+ {currentUser.name} ({currentUser.member_id})
+
+
+ )}
+
-
+
- 홈으로
+ 홈으로
-