- [MAP] 목표량 대비 현재 픽업량 표시. 목표량 달성 시 폭죽. POI 클릭 팝업 제거. Reload 시 선택한 날짜 / 드라이버 복원.
- [INSTALL] POI 클릭 팝업 제거
- [ORDER] UCO Target 양 집계. 데이터 저장을 위해 테이블 tbl_daily_uco_target 생성. Target 양 상한선과 예외 로직을 위해 tbl_driver 에 컬럼 추가.
This commit is contained in:
Hyojin Ahn 2026-07-13 16:06:20 -04:00
parent ec79355bae
commit 0521aee0bc
7 changed files with 334 additions and 35 deletions

View File

@ -105,6 +105,28 @@
border: 1px solid #007bff; border: 1px solid #007bff;
} }
/* Pickup Qty 게이지: 타겟 대비 채움 비율 표시 */
.btn-gauge {
text-shadow:
-1px -1px 0 #007bff,
1px -1px 0 #007bff,
-1px 1px 0 #007bff,
1px 1px 0 #007bff,
0 0 2px #007bff;
background-clip: padding-box;
}
.btn-gauge .gauge-pct {
font-size: 12px;
opacity: 0.9;
}
.btn-gauge .gauge-sub {
display: block;
font-size: 11px;
font-weight: 400;
letter-spacing: 0;
opacity: 0.95;
}
.btn-red-map { .btn-red-map {
margin-top: 5px; margin-top: 5px;
width: 100%; width: 100%;

View File

@ -1768,7 +1768,8 @@ class API extends CONF {
$_POST['route_date'] = str_replace("-","",$_POST['route_date']); $_POST['route_date'] = str_replace("-","",$_POST['route_date']);
$sqQuantity = qry("SELECT $sqQuantity = qry("SELECT
SUM(d_quantity) as pickupquantity SUM(d_quantity) as pickupquantity,
COUNT(*) as pickupcnt
FROM FROM
tbl_daily tbl_daily
WHERE WHERE
@ -1778,9 +1779,24 @@ class API extends CONF {
$rstQuantity = fetch_array($sqQuantity); $rstQuantity = fetch_array($sqQuantity);
$curPickupQty = number_format((int)$rstQuantity['pickupquantity']); $curPickupQty = number_format((int)$rstQuantity['pickupquantity']);
$curVisitCnt = (int)$rstQuantity['pickupcnt'];
$sqTarget = qry("SELECT
dut_target_qty,
dut_target_visit_cnt
FROM
tbl_daily_uco_target
WHERE
dut_date = '".$_POST['route_date']."'
AND dut_druid = '".$_POST['driver']."' ");
$rstTarget = fetch_array($sqTarget);
$curTargetQty = number_format($rstTarget ? (int)$rstTarget['dut_target_qty'] : 0);
$curTargetVisitCnt = $rstTarget ? (int)$rstTarget['dut_target_visit_cnt'] : 0;
$this->response($this->json(array("curPickupQty"=>$curPickupQty)), 200); $this->response($this->json(array("curPickupQty"=>$curPickupQty, "curTargetQty"=>$curTargetQty, "curVisitCnt"=>$curVisitCnt, "curTargetVisitCnt"=>$curTargetVisitCnt)), 200);
} catch(Exception $e) { } catch(Exception $e) {
$error = array($e->getMessage()); $error = array($e->getMessage());
$this->response($this->json($error), 417); $this->response($this->json($error), 417);

View File

@ -1945,7 +1945,8 @@ async function initMap(){
center: { lat: 43.7, lng: -79.4 }, center: { lat: 43.7, lng: -79.4 },
zoom: 11, zoom: 11,
mapId: "4504f8b37365c3d0", // waitlist와 동일 mapId: "4504f8b37365c3d0", // waitlist와 동일
disableDefaultUI: true // Map/Satellite 제거 disableDefaultUI: true, // Map/Satellite 제거
clickableIcons: false // POI 클릭 팝업 제거
} }
); );

View File

@ -1233,7 +1233,8 @@ $categoryLabels = [
center: { lat:43.65, lng:-79.38 }, center: { lat:43.65, lng:-79.38 },
zoom: 10, zoom: 10,
mapId: "4504f8b37365c3d0", mapId: "4504f8b37365c3d0",
disableDefaultUI: true disableDefaultUI: true,
clickableIcons: false
}); });
// 새로고침 전에 작업하던 날짜/드라이버 복원 (loadDailyInstallPoints 호출 전) // 새로고침 전에 작업하던 날짜/드라이버 복원 (loadDailyInstallPoints 호출 전)

View File

@ -73,7 +73,8 @@
$orderdateSTR = str_replace('-', "", $orderdateSTR); $orderdateSTR = str_replace('-', "", $orderdateSTR);
$sqQuantity = qry("SELECT $sqQuantity = qry("SELECT
SUM(d_quantity) as pickupquantity SUM(d_quantity) as pickupquantity,
COUNT(*) as pickupcnt
FROM FROM
tbl_daily tbl_daily
WHERE WHERE
@ -83,6 +84,21 @@
$rstQuantity = fetch_array($sqQuantity); $rstQuantity = fetch_array($sqQuantity);
$curPickupQty = (int)$rstQuantity['pickupquantity']; $curPickupQty = (int)$rstQuantity['pickupquantity'];
$curVisitCnt = (int)$rstQuantity['pickupcnt'];
$sqTarget = qry("SELECT
dut_target_qty,
dut_target_visit_cnt
FROM
tbl_daily_uco_target
WHERE
dut_date = '".$orderdateSTR."'
AND dut_druid = '".$_SESSION['ss_DRUID']."' ");
$rstTarget = fetch_array($sqTarget);
$curTargetQty = $rstTarget ? (int)$rstTarget['dut_target_qty'] : 0;
$curTargetVisitCnt = $rstTarget ? (int)$rstTarget['dut_target_visit_cnt'] : 0;
} }
@ -121,7 +137,7 @@
<? //if (($_SESSION['ss_LEVEL'] == 9) && ($_SESSION['ss_UID'] != "")) { ?> <? //if (($_SESSION['ss_LEVEL'] == 9) && ($_SESSION['ss_UID'] != "")) { ?>
<? if (($_SESSION['ss_UID'] != "")) { ?> <? if (($_SESSION['ss_UID'] != "")) { ?>
<tr> <tr>
<td><button class="btn-blue-map" type="button">Pickup Qty: <span id="pickupquantity"><?=$curPickupQty?></span>L</button></td> <td><button id="pickupQtyBtn" class="btn-blue-map btn-gauge" type="button"><span id="pickupquantity"><?=$curPickupQty?></span> / <span id="targetquantity"><?=$curTargetQty?></span>L<span class="gauge-sub"><span id="pickuppercent" class="gauge-pct"></span><span id="pickupvisit"><?=$curVisitCnt?></span> / <span id="targetvisit"><?=$curTargetVisitCnt?></span> visits</span></button></td>
</tr> </tr>
<? } ?> <? } ?>
@ -1088,6 +1104,7 @@ function popup(){
/*Redraw marker when search option changed*/ /*Redraw marker when search option changed*/
jQuery("#orderdate").change(function(e){ jQuery("#orderdate").change(function(e){
try { sessionStorage.setItem('map_orderdate', jQuery(this).val()); } catch(err) {}
jQuery('.qty').prop('checked', false); jQuery('.qty').prop('checked', false);
jQuery("#search-keyword").val(""); jQuery("#search-keyword").val("");
drawPoint(); drawPoint();
@ -1106,6 +1123,7 @@ function popup(){
}); });
jQuery("#driver").change(function(e){ jQuery("#driver").change(function(e){
try { sessionStorage.setItem('map_driver', jQuery(this).val()); } catch(err) {}
jQuery('.qty').prop('checked', false); jQuery('.qty').prop('checked', false);
jQuery("#search-keyword").val(""); jQuery("#search-keyword").val("");
drawPoint(); drawPoint();
@ -1122,6 +1140,22 @@ function popup(){
}); });
// 선택한 날짜/드라이버 복원 (F5·INIT 리로드 후 유지)
(function(){
var changed = false;
try {
var savedDate = sessionStorage.getItem('map_orderdate');
if (savedDate) { jQuery('#orderdate').val(savedDate); changed = true; }
<? if ($_SESSION['ss_LEVEL'] != 9) { ?>
var savedDriver = sessionStorage.getItem('map_driver');
if (savedDriver !== null && jQuery('#driver option[value="'+savedDriver+'"]').length) {
jQuery('#driver').val(savedDriver); changed = true;
}
<? } ?>
} catch(err) {}
if (changed) jQuery('#driver').trigger('change');
})();
jQuery("#search-keyword").keyup(function(e){ jQuery("#search-keyword").keyup(function(e){
//if(jQuery(e.target).val().length > 0) { //if(jQuery(e.target).val().length > 0) {
drawPoint(); drawPoint();
@ -1480,9 +1514,178 @@ function popup(){
} }
var rstInqPickupQty = function(json) { var rstInqPickupQty = function(json) {
jQuery("#targetquantity").text(json.curTargetQty);
jQuery("#pickupquantity").text(json.curPickupQty); jQuery("#pickupquantity").text(json.curPickupQty);
jQuery("#pickupvisit").text(json.curVisitCnt);
jQuery("#targetvisit").text(json.curTargetVisitCnt);
setPickupGauge(json.curPickupQty, json.curTargetQty);
} }
// Pickup Qty 버튼을 타겟 대비 비율만큼 파란색으로 채움
function setPickupGauge(pickup, target){
var p = parseInt(String(pickup).replace(/[^0-9]/g, ''), 10) || 0;
var t = parseInt(String(target).replace(/[^0-9]/g, ''), 10) || 0;
var pct = (t > 0) ? Math.round(p / t * 100) : 0; // 실제 퍼센트 (초과 시 100+ 그대로)
var ratio = Math.min(100, pct); // 채움 비율 (100% 상한)
jQuery("#pickupQtyBtn").css(
"background",
"linear-gradient(to right, #007bff 0%, #007bff " + ratio + "%, #a9cdff " + ratio + "%, #a9cdff 100%)"
);
jQuery("#pickuppercent").text(t > 0 ? pct + "% · " : "");
// 타겟 100% 도달 시 폭죽 (도달하는 순간 1회, 미만으로 떨어지면 재무장)
var reached = (t > 0 && pct >= 100);
if (reached && !window._pickupGoalReached) {
launchFireworks();
}
window._pickupGoalReached = reached;
}
// 미리 만든 글로우 스프라이트 캐시 (shadowBlur 대체 → 모바일에서 빠름)
var _fwSprites = {};
function fwSprite(color){
if (_fwSprites[color]) return _fwSprites[color];
var R = 16;
var s = document.createElement('canvas');
s.width = s.height = R * 2;
var g = s.getContext('2d');
var grad = g.createRadialGradient(R, R, 0, R, R, R);
grad.addColorStop(0, color);
grad.addColorStop(0.35, color);
grad.addColorStop(1, 'rgba(0,0,0,0)');
g.fillStyle = grad;
g.beginPath(); g.arc(R, R, R, 0, Math.PI*2); g.fill();
_fwSprites[color] = s;
return s;
}
// 폭죽 애니메이션 (canvas, 자체 제거) - 로켓 발사 + 글로우(스프라이트)
function launchFireworks(){
// 모션 최소화 설정이면 생략
if (window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var canvas = document.createElement('canvas');
canvas.style.cssText = 'position:fixed;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var W = canvas.width = window.innerWidth;
var H = canvas.height = window.innerHeight;
// 저사양(터치/작은 화면) 감지 → 개수 감소
var lowPower = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || Math.min(W, H) < 760;
var NUM_ROCKETS = lowPower ? 7 : 12;
var P_MIN = lowPower ? 28 : 60;
var P_RANGE = lowPower ? 26 : 50;
var LAUNCH_GAP = lowPower ? 300 : 230;
var colors = ['#ff3b3b','#ffd23f','#3bd16f','#3b9dff','#c14bff','#ff8c1a','#ff5fa2','#ffffff'];
var rockets = [];
var particles = [];
function rand(a, b){ return a + Math.random()*(b - a); }
function pick(){ return colors[Math.floor(Math.random()*colors.length)]; }
function launchRocket(){
rockets.push({
x: rand(W*0.15, W*0.85),
y: H + 10,
vy: -rand(9, 13),
targetY: rand(H*0.10, H*0.42),
color: pick()
});
}
function explode(x, y, color){
var n = P_MIN + Math.floor(Math.random()*P_RANGE);
var base = rand(3, 7);
for(var i=0;i<n;i++){
var angle = Math.random()*Math.PI*2;
var speed = base * (0.3 + Math.random());
particles.push({
x:x, y:y,
vx:Math.cos(angle)*speed,
vy:Math.sin(angle)*speed,
life:1,
decay: rand(0.008, 0.018),
size: rand(1.5, 3.5),
color: Math.random() < 0.18 ? '#ffffff' : color
});
}
}
// 중앙 배너 팝업
var banner = document.createElement('div');
banner.textContent = '🎉 TARGET 100%! 🎉';
banner.style.cssText = 'position:fixed;left:50%;top:24%;transform:translate(-50%,-50%);'
+ 'font-size:38px;font-weight:800;color:#fff;text-shadow:0 2px 12px rgba(0,0,0,0.6),0 0 22px #ffd23f;'
+ 'z-index:100000;pointer-events:none;white-space:nowrap;';
document.body.appendChild(banner);
if (banner.animate) {
banner.animate([
{ opacity:0, transform:'translate(-50%,-50%) scale(0.5)' },
{ opacity:1, transform:'translate(-50%,-50%) scale(1.15)', offset:0.2 },
{ opacity:1, transform:'translate(-50%,-50%) scale(1)', offset:0.8 },
{ opacity:0, transform:'translate(-50%,-50%) scale(1.25)' }
], { duration:3200, easing:'ease-out' }).onfinish = function(){ banner.remove(); };
} else {
setTimeout(function(){ banner.remove(); }, 3200);
}
// 순차 발사
for(var s=0;s<NUM_ROCKETS;s++){
(function(delay){ setTimeout(launchRocket, delay); })(s*LAUNCH_GAP);
}
var start = performance.now();
var DURATION = 3500;
function frame(now){
var elapsed = now - start;
ctx.clearRect(0, 0, W, H);
ctx.globalCompositeOperation = 'lighter'; // 겹칠수록 밝게 → 글로우
// 로켓
for(var i=rockets.length-1;i>=0;i--){
var r = rockets[i];
r.y += r.vy;
var rs = fwSprite(r.color);
ctx.globalAlpha = 1;
ctx.drawImage(rs, r.x-7, r.y-7, 14, 14);
ctx.globalAlpha = 0.35;
ctx.drawImage(rs, r.x-5, r.y+4, 10, 10); // 꼬리
if(r.y <= r.targetY){ explode(r.x, r.y, r.color); rockets.splice(i,1); }
}
// 파티클
for(var j=particles.length-1;j>=0;j--){
var p = particles[j];
p.vy += 0.04; // 중력
p.vx *= 0.985; p.vy *= 0.985;
p.x += p.vx; p.y += p.vy;
p.life -= p.decay;
if(p.life<=0){ particles.splice(j,1); continue; }
var d = p.size * (0.6 + p.life*0.6) * 3; // 스프라이트 표시 크기
ctx.globalAlpha = Math.max(0, p.life);
ctx.drawImage(fwSprite(p.color), p.x - d/2, p.y - d/2, d, d);
}
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
if(elapsed < DURATION || rockets.length > 0 || particles.length > 0){
requestAnimationFrame(frame);
} else if(canvas.parentNode){
canvas.parentNode.removeChild(canvas);
}
}
requestAnimationFrame(frame);
}
jQuery(function(){
setPickupGauge(jQuery("#pickupquantity").text(), jQuery("#targetquantity").text());
});
var rstSaveInput = function(json) { var rstSaveInput = function(json) {
$('#map-modal-input').modal('hide'); // Bootstrap 정상 닫기 시도 $('#map-modal-input').modal('hide'); // Bootstrap 정상 닫기 시도
$('#map-modal-input').removeClass('show in').css('display', 'none').attr('aria-hidden', 'true'); // 남아 있을 수 있는 클래스 제거 $('#map-modal-input').removeClass('show in').css('display', 'none').attr('aria-hidden', 'true'); // 남아 있을 수 있는 클래스 제거
@ -2388,7 +2591,8 @@ function popup(){
center, center,
mapId: "4504f8b37365c3d0", mapId: "4504f8b37365c3d0",
gestureHandling: 'greedy', gestureHandling: 'greedy',
disableDefaultUI: true disableDefaultUI: true,
clickableIcons: false
}); });
/* /*

View File

@ -290,7 +290,8 @@ async function initMap(){
}, },
zoom: 11, zoom: 11,
mapId: "4504f8b37365c3d0", mapId: "4504f8b37365c3d0",
disableDefaultUI: true disableDefaultUI: true,
clickableIcons: false
} }
); );

View File

@ -54,17 +54,21 @@ echo "[TARGET DATE][$getDate]{$ENT}";
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
$qry = " $qry = "
SELECT SELECT
d_druid AS driveruid, d.d_druid AS driveruid,
COALESCE(SUM(d_estquantity), 0) AS target_qty, COALESCE(SUM(d.d_estquantity), 0) AS target_qty,
COUNT(*) AS target_visit_cnt COUNT(*) AS target_visit_cnt,
FROM tbl_daily dr.dr_target_limit AS target_limit,
WHERE d_orderdate = '{$getDate}' dr.dr_trip_qty AS trip_qty,
AND d_jobtype = 'UCO' dr.dr_trip2_from AS trip2_from
AND d_status <> 'D' FROM tbl_daily d
AND d_druid IS NOT NULL LEFT JOIN tbl_driver dr ON d.d_druid = dr.dr_uid
AND d_druid <> '' WHERE d.d_orderdate = '{$getDate}'
AND d_driveruid <> d_createruid AND d.d_jobtype = 'UCO'
GROUP BY d_druid AND d.d_status <> 'D'
AND d.d_druid IS NOT NULL
AND d.d_druid <> ''
AND d.d_driveruid <> d.d_createruid
GROUP BY d.d_druid, dr.dr_target_limit, dr.dr_trip_qty, dr.dr_trip2_from
"; ";
echo "[DAILY AGG QUERY][$qry]{$ENT}"; echo "[DAILY AGG QUERY][$qry]{$ENT}";
@ -75,12 +79,45 @@ $cnt = 0;
while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) { while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
// 목표량 = 예측 합계. 드라이버별 조정(라이센스 상한 / 트립 모델) 적용
$rawQty = (float)$row['target_qty'];
$targetQty = $rawQty;
$limit = $row['target_limit']; // 최종 상한 T2 (NULL=무제한)
$tripQty = $row['trip_qty']; // 1트립 용량 T1
$trip2From = $row['trip2_from']; // 2트립 시작 임계 TH
$hasLimit = ($limit !== null && $limit !== '');
$hasTrip = ($tripQty !== null && $tripQty !== '' && $trip2From !== null && $trip2From !== '');
if ($hasTrip) {
// 단계형(2트립) 모델: T1=1트립 용량, TH=2트립 시작 임계, T2=최종 상한
$T1 = (float)$tripQty;
$TH = (float)$trip2From;
$T2 = $hasLimit ? (float)$limit : INF;
if ($rawQty <= $T1) {
$targetQty = $rawQty; // 1트립 이내 → 그대로
} elseif ($rawQty <= $TH) {
$targetQty = $T1; // 애매 구간 → 1트립으로 조정
} elseif ($rawQty <= $T2) {
$targetQty = $rawQty; // 2트립 진입 → 그대로
} else {
$targetQty = $T2; // 2트립 상한
}
} elseif ($hasLimit) {
// 단순 상한
if ($rawQty > (float)$limit) {
$targetQty = (float)$limit;
}
}
$payload = array( $payload = array(
"action" => "TARGET", "action" => "TARGET",
"after" => array( "after" => array(
"u_date" => $getDate, "u_date" => $getDate,
"u_druid" => $row['driveruid'], "u_druid" => $row['driveruid'],
"u_target_qty" => (float)$row['target_qty'], "u_target_qty" => $targetQty,
"u_target_visit_cnt" => (int)$row['target_visit_cnt'], "u_target_visit_cnt" => (int)$row['target_visit_cnt'],
"u_synced_at" => $syncedAt, "u_synced_at" => $syncedAt,
"u_createruid" => "SYSTEM", "u_createruid" => "SYSTEM",
@ -100,7 +137,24 @@ while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
$jdb->nQuery($qry_ins, "uco outbox insert error"); $jdb->nQuery($qry_ins, "uco outbox insert error");
echo "[QUEUED][driver:{$row['driveruid']}][qty:{$row['target_qty']}][visit:{$row['target_visit_cnt']}]{$ENT}"; ///////////////////////////////////////////////////
// 로컬 스냅샷 저장 (map.php 조회용) - UNIQUE(dut_date, dut_druid) UPSERT
///////////////////////////////////////////////////
$qry_snap = "
INSERT INTO tbl_daily_uco_target
(dut_date, dut_druid, dut_target_qty, dut_target_visit_cnt, dut_synced_at, dut_createruid)
VALUES
('{$getDate}', '{$row['driveruid']}', " . $targetQty . ", " . (int)$row['target_visit_cnt'] . ", '{$syncedAt}', 'SYSTEM')
ON DUPLICATE KEY UPDATE
dut_target_qty = VALUES(dut_target_qty),
dut_target_visit_cnt = VALUES(dut_target_visit_cnt),
dut_synced_at = VALUES(dut_synced_at),
dut_createruid = VALUES(dut_createruid)
";
$jdb->nQuery($qry_snap, "uco daily target snapshot upsert error");
echo "[QUEUED][driver:{$row['driveruid']}][qty:{$targetQty}][raw:{$rawQty}][limit:{$limit}][T1:{$tripQty}][TH:{$trip2From}][visit:{$row['target_visit_cnt']}]{$ENT}";
$cnt++; $cnt++;
} }