diff --git a/public_html/assets/api/v1/orders.php b/public_html/assets/api/v1/orders.php
index 864e1de..4bfc7d7 100644
--- a/public_html/assets/api/v1/orders.php
+++ b/public_html/assets/api/v1/orders.php
@@ -26,6 +26,258 @@ validateApiKey();
$pdo = getDB();
+// ── Forecast Cache 분기 ───────────────────────
+if (($_GET['type'] ?? '') === 'forecast') {
+
+ header('Content-Type: application/json');
+ header('Access-Control-Allow-Origin: *');
+ header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
+
+ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(204); exit;
+ }
+
+ $pdo = getDB();
+ $date = $_GET['date'] ?? date('Ymd');
+
+ $pdo->exec("CREATE TABLE IF NOT EXISTS tbl_forecast_cache (
+ fc_date VARCHAR(8) NOT NULL PRIMARY KEY,
+ fc_data TEXT NOT NULL,
+ fc_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
+ )");
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $body = json_decode(file_get_contents('php://input'), true);
+ if (!$body) { echo json_encode(['success' => false, 'error' => 'Invalid JSON']); exit; }
+ $st = $pdo->prepare("
+ INSERT INTO tbl_forecast_cache (fc_date, fc_data)
+ VALUES (:d, :data)
+ ON DUPLICATE KEY UPDATE fc_data = VALUES(fc_data), fc_updated = NOW()
+ ");
+ $st->execute([':d' => $date, ':data' => json_encode($body)]);
+ echo json_encode(['success' => true, 'date' => $date]);
+ } else {
+ $st = $pdo->prepare("SELECT fc_data, fc_updated FROM tbl_forecast_cache WHERE fc_date = :d");
+ $st->execute([':d' => $date]);
+ $row = $st->fetch(PDO::FETCH_ASSOC);
+ echo json_encode([
+ 'success' => true,
+ 'date' => $date,
+ 'saved_at' => $row ? $row['fc_updated'] : null,
+ 'data' => $row ? json_decode($row['fc_data'], true) : null,
+ ]);
+ }
+ exit;
+}
+
+// ── CS Dashboard 분기 ─────────────────────────
+if (($_GET['type'] ?? '') === 'cs') {
+
+ $dateFrom = $_GET['date_from'] ?? date('Ymd', strtotime('-30 days'));
+ $dateTo = $_GET['date_to'] ?? date('Ymd');
+ $status = $_GET['status'] ?? 'all';
+ $today = date('Ymd');
+
+ // 1. SUMMARY KPIs
+ $summary = [];
+ $st = $pdo->prepare("SELECT COUNT(*) FROM tbl_request WHERE r_status='A' AND SUBSTR(r_createddate,1,8) BETWEEN :f AND :t");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $summary['total_pending'] = (int)$st->fetchColumn();
+
+ $st = $pdo->prepare("SELECT COUNT(*) FROM tbl_request WHERE SUBSTR(r_createddate,1,8) = :d");
+ $st->execute([':d' => $today]);
+ $summary['today_new'] = (int)$st->fetchColumn();
+
+ $st = $pdo->prepare("SELECT COUNT(*) FROM tbl_request WHERE r_status='F' AND SUBSTR(r_createddate,1,8) = :d");
+ $st->execute([':d' => $today]);
+ $summary['today_completed'] = (int)$st->fetchColumn();
+
+ $st = $pdo->prepare("SELECT MIN(r_requestdate) FROM tbl_request WHERE r_status='A' AND SUBSTR(r_createddate,1,8) BETWEEN :f AND :t");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $oldest = $st->fetchColumn();
+ $summary['oldest_pending_date'] = $oldest ?: null;
+ if ($oldest && strlen($oldest) === 8) {
+ $oDate = mktime(0,0,0,(int)substr($oldest,4,2),(int)substr($oldest,6,2),(int)substr($oldest,0,4));
+ $summary['oldest_pending_days'] = (int)floor((time() - $oDate) / 86400);
+ } else {
+ $summary['oldest_pending_days'] = 0;
+ }
+
+ $st = $pdo->prepare("SELECT COUNT(*) FROM tbl_request WHERE SUBSTR(r_createddate,1,8) BETWEEN :f AND :t");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $summary['period_total'] = (int)$st->fetchColumn();
+
+ $st = $pdo->prepare("SELECT COUNT(*) FROM tbl_request WHERE r_status='F' AND SUBSTR(r_createddate,1,8) BETWEEN :f AND :t");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $summary['period_completed'] = (int)$st->fetchColumn();
+
+ // 2a. ALL PENDING (날짜 필터 없음 — 전체 미처리)
+ $st = $pdo->query("
+ SELECT r.r_uid AS id, r.r_requestdate AS request_date, r.r_accountno AS account,
+ r.r_name AS customer_name, r.r_status AS status, r.r_note AS note,
+ r.r_createddate AS created_at,
+ COALESCE(c.c_area, '') AS area,
+ COALESCE(c.c_city, '') AS city,
+ COALESCE(c.c_phone, '') AS phone,
+ COALESCE(m.m_initial, '') AS driver,
+ COALESCE(NULLIF(m2.m_initial,''), m2.m_firstname, '?') AS staff
+ FROM tbl_request r
+ LEFT JOIN tbl_customer c ON r.r_customeruid = c.c_uid
+ LEFT JOIN tbl_member m ON r.r_driveruid = m.m_uid
+ LEFT JOIN tbl_member m2 ON r.r_memberuid = m2.m_uid
+ WHERE r.r_status = 'A'
+ ORDER BY r.r_requestdate DESC, r.r_uid DESC
+ LIMIT 20000
+ ");
+ $allPending = $st->fetchAll(PDO::FETCH_ASSOC);
+ foreach ($allPending as &$row) {
+ $d = $row['request_date'];
+ $row['request_date_fmt'] = strlen($d) === 8
+ ? substr($d,0,4).'-'.substr($d,4,2).'-'.substr($d,6,2) : $d;
+ $oDate = mktime(0,0,0,(int)substr($d,4,2),(int)substr($d,6,2),(int)substr($d,0,4));
+ $row['days_pending'] = (int)floor((time() - $oDate) / 86400);
+ }
+ unset($row);
+
+ // 2b. PERIOD REQUESTS (기간 필터 — 전체 요청 탭용)
+ $where = ["SUBSTR(r.r_createddate,1,8) BETWEEN :date_from AND :date_to"];
+ $params = [':date_from' => $dateFrom, ':date_to' => $dateTo];
+ $sql = "
+ SELECT r.r_uid AS id, r.r_requestdate AS request_date, r.r_accountno AS account,
+ r.r_name AS customer_name, r.r_status AS status, r.r_note AS note,
+ r.r_createddate AS created_at,
+ COALESCE(c.c_area, '') AS area,
+ COALESCE(c.c_city, '') AS city,
+ COALESCE(c.c_phone, '') AS phone,
+ COALESCE(m.m_initial, '') AS driver,
+ COALESCE(NULLIF(m2.m_initial,''), m2.m_firstname, '?') AS staff
+ FROM tbl_request r
+ LEFT JOIN tbl_customer c ON r.r_customeruid = c.c_uid
+ LEFT JOIN tbl_member m ON r.r_driveruid = m.m_uid
+ LEFT JOIN tbl_member m2 ON r.r_memberuid = m2.m_uid
+ WHERE " . implode(" AND ", $where) . "
+ ORDER BY r.r_requestdate DESC, r.r_uid DESC
+ LIMIT 500
+ ";
+ $st = $pdo->prepare($sql);
+ $st->execute($params);
+ $requests = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($requests as &$row) {
+ $d = $row['request_date'];
+ $row['request_date_fmt'] = strlen($d) === 8
+ ? substr($d,0,4).'-'.substr($d,4,2).'-'.substr($d,6,2) : $d;
+ if ($row['status'] === 'A' && strlen($d) === 8) {
+ $oDate = mktime(0,0,0,(int)substr($d,4,2),(int)substr($d,6,2),(int)substr($d,0,4));
+ $row['days_pending'] = (int)floor((time() - $oDate) / 86400);
+ } else {
+ $row['days_pending'] = 0;
+ }
+ }
+ unset($row);
+
+ // 3. STAFF BREAKDOWN (접수자별 통계)
+ $st = $pdo->prepare("
+ SELECT
+ COALESCE(NULLIF(m2.m_initial,''), m2.m_firstname, '?') AS staff,
+ COUNT(*) AS total,
+ SUM(CASE WHEN r.r_status='A' THEN 1 ELSE 0 END) AS pending,
+ SUM(CASE WHEN r.r_status='F' THEN 1 ELSE 0 END) AS completed
+ FROM tbl_request r
+ LEFT JOIN tbl_member m2 ON r.r_memberuid = m2.m_uid
+ WHERE SUBSTR(r.r_createddate,1,8) BETWEEN :f AND :t
+ GROUP BY r.r_memberuid, m2.m_initial
+ ORDER BY total DESC
+ ");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $byStaff = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ // 4. DRIVER BREAKDOWN
+ $st = $pdo->prepare("
+ SELECT COALESCE(m.m_initial, '?') AS driver,
+ COUNT(*) AS total,
+ SUM(CASE WHEN r.r_status='A' THEN 1 ELSE 0 END) AS pending,
+ SUM(CASE WHEN r.r_status='F' THEN 1 ELSE 0 END) AS completed
+ FROM tbl_request r
+ LEFT JOIN tbl_member m ON r.r_driveruid = m.m_uid
+ WHERE r.r_requestdate BETWEEN :f AND :t
+ GROUP BY r.r_driveruid, m.m_initial
+ ORDER BY pending DESC, total DESC
+ ");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $byDriver = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ // 4. NOTES (type A + D)
+ $st = $pdo->prepare("
+ SELECT n.n_uid AS id, n.n_type AS type, n.n_note AS note,
+ n.n_createddate AS created_at,
+ COALESCE(c.c_name, '') AS customer_name,
+ COALESCE(c.c_accountno, '') AS account,
+ COALESCE(c.c_area, '') AS area,
+ COALESCE(c.c_city, '') AS city,
+ COALESCE(CONCAT(m.m_firstname,' ',m.m_lastname), '') AS author
+ FROM tbl_note n
+ LEFT JOIN tbl_customer c ON n.n_customeruid = c.c_uid
+ LEFT JOIN tbl_member m ON n.n_memberuid = m.m_uid
+ WHERE n.n_status = 'A' AND n.n_type IN ('A','D')
+ AND SUBSTR(n.n_createddate,1,8) BETWEEN :f AND :t
+ ORDER BY n.n_createddate DESC
+ LIMIT 300
+ ");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $notes = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ foreach ($notes as &$n) {
+ $d = substr($n['created_at'], 0, 8);
+ $n['date_fmt'] = strlen($d) === 8
+ ? substr($d,0,4).'-'.substr($d,4,2).'-'.substr($d,6,2) : $d;
+ }
+ unset($n);
+
+ // 5. AREA BREAKDOWN
+ $st = $pdo->prepare("
+ SELECT COALESCE(NULLIF(c.c_area,''), c.c_city, '기타') AS area,
+ COUNT(*) AS total
+ FROM tbl_request r
+ LEFT JOIN tbl_customer c ON r.r_customeruid = c.c_uid
+ WHERE SUBSTR(r.r_createddate,1,8) BETWEEN :f AND :t
+ GROUP BY area
+ ORDER BY total DESC
+ ");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $byArea = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ // 6. DAILY BREAKDOWN (접수일 기준)
+ $st = $pdo->prepare("
+ SELECT SUBSTR(r_createddate,1,8) AS date,
+ COUNT(*) AS total,
+ SUM(CASE WHEN r_status='F' THEN 1 ELSE 0 END) AS completed,
+ SUM(CASE WHEN r_status='A' THEN 1 ELSE 0 END) AS pending
+ FROM tbl_request
+ WHERE SUBSTR(r_createddate,1,8) BETWEEN :f AND :t
+ GROUP BY SUBSTR(r_createddate,1,8)
+ ORDER BY SUBSTR(r_createddate,1,8) ASC
+ ");
+ $st->execute([':f' => $dateFrom, ':t' => $dateTo]);
+ $byDay = $st->fetchAll(PDO::FETCH_ASSOC);
+
+ jsonResponse([
+ 'success' => true,
+ 'date_from' => $dateFrom,
+ 'date_to' => $dateTo,
+ 'summary' => $summary,
+ 'all_pending' => $allPending,
+ 'requests' => $requests,
+ 'by_driver' => $byDriver,
+ 'by_staff' => $byStaff,
+ 'by_area' => $byArea,
+ 'by_day' => $byDay,
+ 'notes' => $notes,
+ ]);
+ exit;
+}
+// ─────────────────────────────────────────────
+
// 날짜 파라미터 (DB 형식: YYYYMMDD varchar)
$dateFrom = $_GET['date_from'] ?? date('Ymd');
$dateTo = $_GET['date_to'] ?? date('Ymd');
@@ -76,8 +328,9 @@ $sql = "
h.h_comment AS comment
FROM tbl_daily d
- INNER JOIN tbl_member m
- ON (d.d_druid = m.m_currentdriver)
+ INNER JOIN (
+ SELECT DISTINCT m_currentdriver FROM tbl_member WHERE m_currentdriver IS NOT NULL
+ ) m ON (d.d_druid = m.m_currentdriver)
LEFT JOIN tbl_memberhis h
ON (d.d_druid = h.h_druid AND d.d_orderdate = h.h_date)
WHERE d.d_orderdate BETWEEN :date_from AND :date_to