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}) + +
+ )}
+
- + - 홈으로 + 홈으로 - + + {currentUser && ( + + )} +
{isProfileMenuOpen && ( -
-
-
역할 선택
-
{selectedRole}
+
+
+
접속 모드 변경
+
{selectedRole}
{roles.map((role) => ( @@ -152,145 +179,187 @@ }; // Operator View Component - const OperatorView = () => { + const OperatorView = ({ currentUser }) => { const [selectedManager, setSelectedManager] = useState(null); const [managers, setManagers] = useState([]); + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingMember, setEditingMember] = useState(null); + const [detailModalUser, setDetailModalUser] = useState(null); + const [isIdChecked, setIsIdChecked] = useState(false); + const [isIdChecking, setIsIdChecking] = useState(false); + const [idCheckMessage, setIdCheckMessage] = useState(''); + const [deleteConfirmMember, setDeleteConfirmMember] = useState(null); - // 영업담당 데이터 생성 - useEffect(() => { - const generateManagerData = () => { - const managerNames = ['김철수', '이영희', '박민수', '정수진', '최동욱']; - const getRandomSales = () => Math.floor(Math.random() * 50000000) + 30000000; - const getRandomContracts = () => Math.floor(Math.random() * 15) + 5; - - // 랜덤 날짜 생성 (최근 12개월 내) - const getRandomDate = () => { - const now = new Date(); - const monthsAgo = Math.floor(Math.random() * 12); - const date = new Date(now.getFullYear(), now.getMonth() - monthsAgo, Math.floor(Math.random() * 28) + 1); - return date.toISOString().split('T')[0]; - }; - - // 계약 생성 - const generateContracts = (count) => { - return Array.from({ length: count }, () => ({ - id: `contract-${Date.now()}-${Math.random()}`, - customer: `고객사 ${String.fromCharCode(65 + Math.floor(Math.random() * 26))}`, - contractDate: getRandomDate(), - amount: getRandomSales() - })); - }; - - return managerNames.map((name, idx) => { - // 각 역할별 계약 생성 - const directContracts = generateContracts(Math.floor(Math.random() * 5) + 2); // 직접 판매 - const managerContracts = generateContracts(Math.floor(Math.random() * 8) + 3); // 1차 하위 - const educatorContracts = generateContracts(Math.floor(Math.random() * 5) + 1); // 2차 하위 - - // 역할 구분 추가 - directContracts.forEach(c => c.role = 'direct'); // 판매자 - managerContracts.forEach(c => c.role = 'manager'); // 관리자 - educatorContracts.forEach(c => c.role = 'educator'); // 메뉴제작 협업자 - - const allContracts = [...directContracts, ...managerContracts, ...educatorContracts]; - - // 역할별 총 매출 - const directSales = directContracts.reduce((sum, c) => sum + c.amount, 0); - const managerSales = managerContracts.reduce((sum, c) => sum + c.amount, 0); - const educatorSales = educatorContracts.reduce((sum, c) => sum + c.amount, 0); - const totalSales = directSales + managerSales + educatorSales; - - // 역할별 수당 - const directCommission = directSales * 0.20; - const managerCommission = managerSales * 0.05; - const educatorCommission = 0; // 메뉴제작 협업수당: 운영팀 별도 산정 - const totalCommission = directCommission + managerCommission + educatorCommission; - - // 당월 필터링 - const now = new Date(); - const currentMonth = now.getMonth(); - const currentYear = now.getFullYear(); - const currentMonthContracts = allContracts.filter(c => { - const d = new Date(c.contractDate); - return d.getMonth() === currentMonth && d.getFullYear() === currentYear; - }); - - const monthlyDirectSales = currentMonthContracts.filter(c => c.role === 'direct').reduce((sum, c) => sum + c.amount, 0); - const monthlyManagerSales = currentMonthContracts.filter(c => c.role === 'manager').reduce((sum, c) => sum + c.amount, 0); - const monthlyEducatorSales = currentMonthContracts.filter(c => c.role === 'educator').reduce((sum, c) => sum + c.amount, 0); - const monthlySales = monthlyDirectSales + monthlyManagerSales + monthlyEducatorSales; - const monthlyCommission = (monthlyDirectSales * 0.20) + (monthlyManagerSales * 0.05); // 메뉴제작 협업수당 제외 - - // 지난달 필터링 - const lastMonth = currentMonth === 0 ? 11 : currentMonth - 1; - const lastMonthYear = currentMonth === 0 ? currentYear - 1 : currentYear; - const lastMonthContracts = allContracts.filter(c => { - const d = new Date(c.contractDate); - return d.getMonth() === lastMonth && d.getFullYear() === lastMonthYear; - }); - - const lastMonthDirectSales = lastMonthContracts.filter(c => c.role === 'direct').reduce((sum, c) => sum + c.amount, 0); - const lastMonthManagerSales = lastMonthContracts.filter(c => c.role === 'manager').reduce((sum, c) => sum + c.amount, 0); - const lastMonthEducatorSales = lastMonthContracts.filter(c => c.role === 'educator').reduce((sum, c) => sum + c.amount, 0); - const lastMonthSales = lastMonthDirectSales + lastMonthManagerSales + lastMonthEducatorSales; - const lastMonthCommission = (lastMonthDirectSales * 0.20) + (lastMonthManagerSales * 0.05); // 메뉴제작 협업수당 제외 - - return { - id: idx + 1, - name: name, - contractCount: allContracts.length, - monthlyContractCount: currentMonthContracts.length, - lastMonthContractCount: lastMonthContracts.length, - totalSales, - directSales, - managerSales, - educatorSales, - totalCommission, - directCommission, - managerCommission, - educatorCommission, - monthlySales, - monthlyCommission, - lastMonthSales, - lastMonthCommission, - contracts: allContracts - }; - }); - }; - - setManagers(generateManagerData()); - }, []); - - // 통계 계산 - const totalStats = managers.reduce((acc, manager) => ({ - contractCount: acc.contractCount + manager.contractCount, - monthlyContractCount: acc.monthlyContractCount + manager.monthlyContractCount, - lastMonthContractCount: acc.lastMonthContractCount + manager.lastMonthContractCount, - totalSales: acc.totalSales + manager.totalSales, - totalCommission: acc.totalCommission + manager.totalCommission, - monthlySales: acc.monthlySales + manager.monthlySales, - monthlyCommission: acc.monthlyCommission + manager.monthlyCommission, - lastMonthSales: acc.lastMonthSales + manager.lastMonthSales, - lastMonthCommission: acc.lastMonthCommission + manager.lastMonthCommission - }), { - contractCount: 0, - monthlyContractCount: 0, - lastMonthContractCount: 0, - totalSales: 0, - totalCommission: 0, - monthlySales: 0, - monthlyCommission: 0, - lastMonthSales: 0, - lastMonthCommission: 0 + const [formData, setFormData] = useState({ + member_id: '', + password: '', + name: '', + phone: '', + email: '', + role: 'manager', + parent_id: '', + remarks: '' }); + useEffect(() => { + fetchMembers(); + }, []); + + const fetchMembers = async () => { + setLoading(true); + try { + const res = await fetch(`api/sales_members.php?action=list`); + const result = await res.json(); + if (result.success) setMembers(result.data); + } catch (err) { + console.error('Fetch error:', err); + } finally { + setLoading(false); + } + }; + + const handleOpenAdd = () => { + setEditingMember(null); + setFormData({ member_id: '', password: '', name: '', phone: '', email: '', role: 'manager', parent_id: '', remarks: '' }); + setIsIdChecked(false); + setIdCheckMessage(''); + setIsModalOpen(true); + }; + + const handleCheckId = async () => { + if (!formData.member_id) { + setIdCheckMessage('아이디를 입력해주세요.'); + return; + } + setIsIdChecking(true); + setIdCheckMessage(''); + try { + const res = await fetch(`api/sales_members.php?action=check_id`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ member_id: formData.member_id }) + }); + const result = await res.json(); + if (result.success) { + if (result.exists) { + setIdCheckMessage('이미 사용 중인 아이디입니다.'); + setIsIdChecked(false); + } else { + setIdCheckMessage('사용 가능한 아이디입니다.'); + setIsIdChecked(true); + } + } else { + setIdCheckMessage(result.error || '확인 실패'); + } + } catch (err) { + setIdCheckMessage('오류가 발생했습니다.'); + } finally { + setIsIdChecking(false); + } + }; + + const handleOpenEdit = (member) => { + setEditingMember(member); + setFormData({ + member_id: member.member_id, + password: '', + name: member.name, + phone: member.phone || '', + email: member.email || '', + role: member.role || 'manager', + parent_id: member.parent_id || '', + remarks: member.remarks || '' + }); + setIsModalOpen(true); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const action = editingMember ? 'update' : 'create'; + const method = editingMember ? 'PUT' : 'POST'; + + try { + const res = await fetch(`api/sales_members.php${action === 'create' ? '?action=create' : ''}`, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + id: editingMember?.id + }) + }); + const result = await res.json(); + if (result.success) { + alert(result.message); + setIsModalOpen(false); + fetchMembers(); + } else { + alert(result.error); + } + } catch (err) { + alert('저장 중 오류가 발생했습니다.'); + } + }; + + const handleMemberDelete = (member) => { + console.log('[OperatorView] handleDelete triggered for:', member.name, member.id); + setDeleteConfirmMember(member); + }; + + const executeDelete = async () => { + const member = deleteConfirmMember; + if (!member) return; + + try { + const res = await fetch(`api/sales_members.php?action=delete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: member.id }) + }); + const result = await res.json(); + + if (result.success) { + alert(result.message || '삭제되었습니다.'); + setDeleteConfirmMember(null); + fetchMembers(); + } else { + alert(result.error || '삭제에 실패했습니다.'); + } + } catch (err) { + console.error('Delete error exception:', err); + alert('삭제 중 오류가 발생했습니다: ' + err.message); + } + }; + + // 통계 계산 (실제 데이터 기반) + const totalStats = { + contractCount: 0, // 나중에 실적 API와 연동 필요 + totalSales: 0, + totalCommission: 0, + monthlyContractCount: 0, + monthlySales: 0, + monthlyCommission: 0, + lastMonthCommission: 0 + }; + const formatCurrency = (val) => new Intl.NumberFormat('ko-KR', { style: 'currency', currency: 'KRW' }).format(val); - - return ( -
-

영업담당 관리

+
+
+
+

영업 전체 관리

+

플랫폼의 모든 영업 관리자와 매니저를 총괄 관리합니다.

+
+ +
{/* 통계 카드 */}
@@ -307,10 +376,8 @@
{totalStats.contractCount}건
전체 계약 건수
-
setSelectedManager(null)} - > + +

이번달 건수

@@ -320,10 +387,8 @@
{totalStats.monthlyContractCount}건
이번달 신규 계약
-
setSelectedManager(null)} - > + +

총 가입비

@@ -333,10 +398,8 @@
{formatCurrency(totalStats.totalSales)}
전체 누적 가입비
-
setSelectedManager(null)} - > + +

총 수당 지급

@@ -346,10 +409,8 @@
{formatCurrency(totalStats.totalCommission)}
전체 누적 수당
-
setSelectedManager(null)} - > + +

이번달 수당

@@ -359,10 +420,8 @@
{formatCurrency(totalStats.monthlyCommission)}
이번달 지급 예정
-
setSelectedManager(null)} - > + +

지난달 수당

@@ -374,192 +433,381 @@
- {/* 영업담당 리스트 또는 세부 리스트 */} - {selectedManager ? ( -
-
-

{selectedManager.name} 세부 수당 리스트

-
- -
- {/* 역할별 수당 요약 */} -
-
-
- 판매자 - 20% -
-
{formatCurrency(selectedManager.directCommission)}
-
-
-
-
- 관리자 수당 - 5% -
-
{formatCurrency(selectedManager.managerCommission)}
-
-
-
-
- 메뉴제작 협업수당 - 별도 -
-
운영팀 산정
-
-
-
+
+
+ + + + + + + + + + + + + + {loading ? ( + + ) : members.length === 0 ? ( + + ) : members.map(m => ( + + + + + + + + + + ))} + +
성명아이디역할상위 관리자연락처가입일관리
데이터 로딩 중...
등록된 영업 인력이 없습니다.
{m.name}{m.member_id} + m.role === 'sales_admin' && setDetailModalUser(m)} + className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase transition-all ${ + m.role === 'sales_admin' ? 'bg-indigo-100 text-indigo-700 cursor-pointer hover:bg-indigo-200 ring-1 ring-indigo-200' : + m.role === 'manager' ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-700' + }`} + title={m.role === 'sales_admin' ? "하위 멤버 보기" : ""} + > + {m.role === 'sales_admin' ? '영업관리' : m.role === 'manager' ? '매니저' : m.role} + + + {m.parent_id ? (() => { + const parent = members.find(p => p.id == m.parent_id); + return ( + + + {parent ? `${parent.name} (${parent.member_id})` : m.parent_id} + + ); + })() : '-'} + {m.phone || '-'}{m.created_at?.split(' ')[0]} +
+ + +
+
+
+ - {/* 계약 목록 */} -
-
- - + {/* Member CRUD Modal */} + {isModalOpen && ( +
+
+
+
+

+
+ +
+ {editingMember ? '정보 수정' : '신규 회원 등록'} +

+ +
+ +
+
+
+ + setFormData({...formData, name: e.target.value})} + className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all font-bold" + placeholder="홍길동" + /> +
+
+ +
+ { + setFormData({...formData, member_id: e.target.value}); + setIsIdChecked(false); + setIdCheckMessage(''); + }} + className={`w-full px-4 py-3 bg-slate-50 border ${isIdChecked ? 'border-emerald-500 bg-emerald-50' : 'border-slate-200'} rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all font-mono disabled:opacity-50`} + placeholder="sales_01" + /> + {!editingMember && ( + + )} +
+ {!editingMember && idCheckMessage && ( +

+ {idCheckMessage} +

+ )} +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + setFormData({...formData, password: e.target.value})} + className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all" + placeholder={editingMember ? "********" : "비밀번호 입력"} + /> +
+ +
+
+ + setFormData({...formData, phone: e.target.value})} + className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all font-medium" + placeholder="010-0000-0000" + /> +
+
+ + setFormData({...formData, email: e.target.value})} + className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all font-medium" + placeholder="example@email.com" + /> +
+
+
+ +
+ + +
+ +
+
+ )} + + {/* Manager Detail Modal (하위 멤버 목록) */} + {detailModalUser && ( +
+
+
+
+

+
+ +
+ {detailModalUser.name} ({detailModalUser.member_id}) 하위 멤버 +

+

이 영업관리자에게 소속된 모든 매니저 목록입니다.

+
+ +
+
+
+ - - - - - - + + + + + - {selectedManager.contracts && selectedManager.contracts.map((contract, idx) => { - const getRoleName = (role) => { - if (role === 'direct') return '판매자'; - if (role === 'manager') return '관리자'; - if (role === 'educator') return '메뉴제작 협업자'; - return '-'; - }; - - const getRoleColor = (role) => { - if (role === 'direct') return 'bg-green-100 text-green-800'; - if (role === 'manager') return 'bg-purple-100 text-purple-800'; - if (role === 'educator') return 'bg-orange-100 text-orange-800'; - return 'bg-slate-100 text-slate-800'; - }; - - const getCommissionRate = (role) => { - if (role === 'direct') return 0.20; - if (role === 'manager') return 0.05; - if (role === 'educator') return 0; - return 0; - }; - - const commission = contract.amount * getCommissionRate(contract.role); - - return ( - - - - - - - - - ); - })} - - + {members.filter(m => m.parent_id == detailModalUser.id).length === 0 ? ( - - - + - + ) : ( + members.filter(m => m.parent_id == detailModalUser.id).map(m => ( + + + + + + + + )) + )} +
번호고객사계약일역할가입비수당성명아이디역할연락처작업
{idx + 1}{contract.customer}{contract.contractDate} - - {getRoleName(contract.role)} ({getCommissionRate(contract.role) * 100}%) - - {formatCurrency(contract.amount)} - {contract.role === 'educator' ? '운영팀 산정' : formatCurrency(commission)} -
합계{formatCurrency(selectedManager.totalSales)}{formatCurrency(selectedManager.totalCommission)} + 하위 멤버가 없습니다. +
{m.name}{m.member_id} + + 매니저 + + {m.phone || '-'} +
+ + +
+
-
-
-
- ) : ( -
-

영업담당 목록

-
- {managers.map((manager) => ( -
setSelectedManager(manager)} - className="bg-white rounded-card p-6 shadow-sm border border-slate-100 hover:shadow-md transition-shadow cursor-pointer" +
+
- ))} + 닫기 + +
)} {/* 아이템 설정 카드 (운영자 전용) */} -
-

- - 아이템 가격 설정 +
+

+
+ +
+ 베이직 요금 및 수당 설정

+ + {/* Delete Confirmation Modal */} + {deleteConfirmMember && ( +
+
+
+
+ +
+

인력 삭제 확인

+

+ 정말 '{deleteConfirmMember.name}({deleteConfirmMember.member_id})' 인력을 삭제하시겠습니까? +

+ + {members.some(m => m.parent_id == deleteConfirmMember.id) && ( +
+ +
+

주의 사항

+

+ 이 인력은 하위 담당자가 있습니다. 삭제 시 상위 관리자 정보가 유실될 수 있습니다. +

+
+
+ )} +
+
+ + +
+
+
+ )}

); }; @@ -898,9 +1146,12 @@ ); }; - // ... (StatCard component remains same) ... - const StatCard = ({ title, value, subtext, icon }) => ( -
+ // 2. StatCard Component (Move up to be reusable) + const StatCard = ({ title, value, subtext, icon, onClick }) => ( +

{title}

@@ -912,6 +1163,443 @@
); + // --- NEW: Login View Component --- + const LoginView = ({ onLoginSuccess, selectedRole }) => { + const [memberId, setMemberId] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (selectedRole === '운영자') { setMemberId('admin'); setPassword('admin'); } + else if (selectedRole === '영업관리') { setMemberId('sales'); setPassword('sales'); } + else if (selectedRole === '매니저') { setMemberId('manager'); setPassword('manager'); } + else { setMemberId(''); setPassword(''); } + }, [selectedRole]); + + const handleLogin = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + try { + const res = await fetch('api/sales_members.php?action=login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ member_id: memberId, password }) + }); + const data = await res.json(); + if (data.success) { + if (data.message && !data.user) { + alert(data.message); // Admin/Manager creation notice (requiring manual re-login) + } else { + if (data.message) console.log(data.message); + onLoginSuccess(data.user); + } + } else { + setError(data.error || '로그인에 실패했습니다.'); + } + } catch (err) { + setError('서버 통신 오류가 발생했습니다.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ +
+

영업관리 로그인

+

[{selectedRole}] 접근 권한이 필요합니다.

+
+ +
+
+ + setMemberId(e.target.value)} + className="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" + placeholder="아이디를 입력하세요" + /> +
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-2 bg-slate-50 border border-slate-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all" + placeholder="비밀번호를 입력하세요" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +
+

+ 테스트 계정:
+ 운영자: admin / admin
+ 영업관리: sales / sales
+ 매니저: manager / manager +

+
+
+
+ ); + }; + + // --- NEW: Manager Management View (CRUD) --- + const ManagerManagementView = ({ currentUser }) => { + const [members, setMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingMember, setEditingMember] = useState(null); + const [isIdChecked, setIsIdChecked] = useState(false); + const [isIdChecking, setIsIdChecking] = useState(false); + const [idCheckMessage, setIdCheckMessage] = useState(''); + const [deleteConfirmMember, setDeleteConfirmMember] = useState(null); + + // Form states + const [formData, setFormData] = useState({ + member_id: '', + password: '', + name: '', + phone: '', + email: '', + remarks: '' + }); + + useEffect(() => { + if (currentUser) { + fetchMembers(); + } + }, [currentUser]); + + const fetchMembers = async () => { + if (!currentUser) return; + setLoading(true); + try { + const res = await fetch(`api/sales_members.php?action=list&parent_id=${currentUser.id}`); + const data = await res.json(); + if (data.success) setMembers(data.data); + } catch (err) { + console.error('Fetch error:', err); + } finally { + setLoading(false); + } + }; + + const handleOpenAdd = () => { + setEditingMember(null); + setFormData({ member_id: '', password: '', name: '', phone: '', email: '', remarks: '' }); + setIsIdChecked(false); + setIdCheckMessage(''); + setIsModalOpen(true); + }; + + const handleCheckId = async () => { + if (!formData.member_id) { + setIdCheckMessage('아이디를 입력해주세요.'); + return; + } + setIsIdChecking(true); + setIdCheckMessage(''); + try { + const res = await fetch(`api/sales_members.php?action=check_id`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ member_id: formData.member_id }) + }); + const result = await res.json(); + if (result.success) { + if (result.exists) { + setIdCheckMessage('이미 사용 중인 아이디입니다.'); + setIsIdChecked(false); + } else { + setIdCheckMessage('사용 가능한 아이디입니다.'); + setIsIdChecked(true); + } + } else { + setIdCheckMessage(result.error || '확인 실패'); + } + } catch (err) { + setIdCheckMessage('오류가 발생했습니다.'); + } finally { + setIsIdChecking(false); + } + }; + + const handleOpenEdit = (member) => { + setEditingMember(member); + setFormData({ + member_id: member.member_id, + password: '', // 비밀번호는 비워둠 (변경 시만 입력) + name: member.name, + phone: member.phone || '', + email: member.email || '', + remarks: member.remarks || '' + }); + setIsIdChecked(true); // 수정 시에는 이미 존재하므로 체크 완료 상태로 설정 + setIsModalOpen(true); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const action = editingMember ? 'update' : 'create'; + const method = editingMember ? 'PUT' : 'POST'; + + try { + const res = await fetch(`api/sales_members.php${action === 'create' ? '?action=create' : ''}`, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + id: editingMember?.id + }) + }); + const result = await res.json(); + if (result.success) { + alert(result.message); + setIsModalOpen(false); + fetchMembers(); + } else { + alert(result.error); + } + } catch (err) { + alert('저장 중 오류가 발생했습니다.'); + } + }; + + const handleDelete = (member) => { + console.log('[ManagerManagementView] handleDelete triggered for:', member.name, member.id); + setDeleteConfirmMember(member); + }; + + const executeDelete = async () => { + const member = deleteConfirmMember; + if (!member) return; + + try { + const res = await fetch(`api/sales_members.php?action=delete`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: member.id }) + }); + const result = await res.json(); + + if (result.success) { + alert(result.message || '삭제되었습니다.'); + setDeleteConfirmMember(null); + fetchMembers(); + } else { + alert(result.error || '삭제에 실패했습니다.'); + } + } catch (err) { + console.error('Delete error:', err); + alert('삭제 중 오류가 발생했습니다.'); + } + }; + + return ( +
+
+
+

내 하위 담당자 관리

+

내가 직접 영입하여 관리하는 팀원들의 정보를 관리합니다.

+
+ +
+ +
+ + + + + + + + + + + + + + {loading ? ( + + ) : members.length === 0 ? ( + + ) : members.map(m => ( + + + + + + + + + + ))} + +
성명아이디연락처이메일비고등록일관리
데이터 로딩 중...
등록된 담당자가 없습니다.
{m.name}{m.member_id}{m.phone || '-'}{m.email || '-'}{m.remarks || '-'}{m.created_at?.split(' ')[0]} +
+ + +
+
+
+ + {/* CRUD Modal */} + {isModalOpen && ( +
+
+
+
+

+ + {editingMember ? '담당자 정보 수정' : '신규 담당자 등록'} +

+ +
+ +
+
+
+ + setFormData({...formData, name: e.target.value})} + className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" + /> +
+
+ +
+ { + setFormData({...formData, member_id: e.target.value}); + setIsIdChecked(false); + setIdCheckMessage(''); + }} + className={`w-full px-3 py-2 border ${isIdChecked ? 'border-emerald-500 bg-emerald-50' : 'border-slate-200'} rounded-lg outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-slate-50 font-mono`} + /> + {!editingMember && ( + + )} +
+ {!editingMember && idCheckMessage && ( +

{idCheckMessage}

+ )} +
+
+
+ + setFormData({...formData, password: e.target.value})} + className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" + placeholder={editingMember ? "********" : "비밀번호 입력"} + /> +
+
+ + setFormData({...formData, phone: e.target.value})} + className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" + placeholder="010-0000-0000" + /> +
+
+ + setFormData({...formData, email: e.target.value})} + className="w-full px-3 py-2 border border-slate-200 rounded-lg outline-none focus:ring-2 focus:ring-blue-500" + placeholder="example@email.com" + /> +
+
+ +