- [CUSTOMER] Install 요청, 청소 전, 청소 후 사진도 보이도록 수정. multi image 보이도록 수정.
- [MAP] Container 교체 요청 시 사진 multi upload & preview.
- [INSTALL WAIT LIST] Install request 완료 사진 multi upload & preview.
- [INSTALL ORDER] Install 완료 사진 multi upload & preview. 조회 조건 바뀌어도 View Mode 유지되도록 수정.
This commit is contained in:
Hyojin Ahn 2026-05-05 10:22:40 -04:00
parent 62342d3454
commit c48c0a97da
7 changed files with 311 additions and 262 deletions

View File

@ -264,11 +264,10 @@ trait InstallAPI {
while ($img = mysqli_fetch_assoc($ri)) { while ($img = mysqli_fetch_assoc($ri)) {
$imgPath = $img['i_filepath'] . $img['i_filename'];
$photos[] = [ $photos[] = [
'i_uid' => (int)$img['i_uid'], 'i_uid' => (int)$img['i_uid'],
'imgPath' => $imgPath 'i_filepath' => $img['i_filepath'],
'i_filename' => $img['i_filename']
]; ];
} }
@ -1708,20 +1707,19 @@ trait InstallAPI {
$fail('select_di', 'daily_install not found'); $fail('select_di', 'daily_install not found');
} }
if(!isset($_FILES['photo']) || $_FILES['photo']['error'] !== 0){ if(!isset($_FILES['photos'])){
$fail('file', 'no file'); $fail('file', 'no files');
} }
$file = $_FILES['photo'];
$customer_uid = intval($di['di_customer_uid']); $customer_uid = intval($di['di_customer_uid']);
$ok = $this->save_customer_image_single( $ok = $this->save_customer_image_multi(
$customer_uid, $customer_uid,
$member_uid, $member_uid,
$i_type, $i_type,
$di_uid, // i_sourceuid = di_uid $di_uid,
$note, $note,
$file $_FILES['photos']
); );
if(!$ok){ if(!$ok){
@ -1732,7 +1730,7 @@ trait InstallAPI {
exit; exit;
} }
private function save_customer_image_single($i_customeruid, $i_memberuid, $i_type, $i_sourceuid, $i_note, $file){ private function save_customer_image_multi($i_customeruid, $i_memberuid, $i_type, $i_sourceuid, $i_note, $files){
$conn = $GLOBALS['conn']; $conn = $GLOBALS['conn'];
@ -1744,36 +1742,48 @@ trait InstallAPI {
$i_noteEsc = mysqli_real_escape_string($conn, trim($i_note)); $i_noteEsc = mysqli_real_escape_string($conn, trim($i_note));
$i_createddate = date("YmdHis"); $i_createddate = date("YmdHis");
// 확장자 체크(최소 보안)
$allowed = ['jpg','jpeg','png','webp']; $allowed = ['jpg','jpeg','png','webp'];
$ext = strtolower(pathinfo($file['name'] ?? '', PATHINFO_EXTENSION));
if(!$ext || !in_array($ext, $allowed)){
return false;
}
// 작업 단위 1장 유지: (customeruid + type, sourceuid 는 제외) // 기존 row 비활성화
$qry_update = " $qry_update = "
UPDATE tbl_customer_image UPDATE tbl_customer_image
SET i_status = 'I' SET i_status = 'I'
WHERE i_customeruid = '{$i_customeruid}' WHERE i_customeruid = '{$i_customeruid}'
AND i_type = '{$i_type}' AND i_type = '{$i_type}'
AND i_status = 'A' AND i_status = 'A'
"; ";
if(qry($qry_update) === false) return false; if(qry($qry_update) === false) return false;
// 저장 폴더 // 폴더
$upload_folder = getenv("DOCUMENT_ROOT")."/upload/customer_image/".$i_customeruid; $upload_folder = getenv("DOCUMENT_ROOT")."/upload/customer_image/".$i_customeruid;
if(!is_dir($upload_folder)){ if(!is_dir($upload_folder)){
mkdir($upload_folder, 0777, true); mkdir($upload_folder, 0777, true);
} }
$newFileName = "IMG_".$i_customeruid."_".time().rand(1000,9999)."_0.jpg"; $savedFiles = [];
$savePath = $upload_folder . "/" . $newFileName;
if (!$this->compressAndResizeImage($file['tmp_name'], $savePath, $ext, 1200, 75)) { foreach ($files['name'] as $i => $name) {
return false;
if ($files['error'][$i] !== 0) continue;
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
if(!$ext || !in_array($ext, $allowed)) continue;
$tmp = $files['tmp_name'][$i];
$newFileName = "IMG_".$i_customeruid."_".time().rand(1000,9999)."_".$i.".jpg";
$savePath = $upload_folder . "/" . $newFileName;
if (!$this->compressAndResizeImage($tmp, $savePath, $ext, 1200, 75)) {
continue;
}
$savedFiles[] = $newFileName;
} }
if(empty($savedFiles)) return false;
$fileNameString = implode(',', $savedFiles);
$dbFilePath = "/upload/customer_image/".$i_customeruid."/"; $dbFilePath = "/upload/customer_image/".$i_customeruid."/";
$sqlIns = " $sqlIns = "
@ -1794,7 +1804,7 @@ trait InstallAPI {
'{$i_memberuid}', '{$i_memberuid}',
'{$i_createddate}', '{$i_createddate}',
'{$i_type}', '{$i_type}',
'{$newFileName}', '{$fileNameString}',
'{$dbFilePath}', '{$dbFilePath}',
'A', 'A',
'{$i_noteEsc}', '{$i_noteEsc}',

View File

@ -2048,137 +2048,116 @@ if( $total_count < 1 ) {
<p style="clear: both;"></p> <p style="clear: both;"></p>
<?php <?php
// 이미지 가져오기 // 이미지 가져오기
$qry_img = " $qry_img = "
SELECT * SELECT *
FROM tbl_customer_image tci FROM tbl_customer_image tci
LEFT OUTER JOIN tbl_member tm ON tci.i_createdby = tm.m_uid LEFT OUTER JOIN tbl_member tm ON tci.i_createdby = tm.m_uid
WHERE tci.i_customeruid = '$c_uid' WHERE tci.i_customeruid = '$c_uid'
AND tci.i_status = 'A' AND tci.i_status = 'A'
ORDER BY tci.i_uid DESC ORDER BY tci.i_uid DESC
"; ";
$rt_img = $jdb->nQuery($qry_img, "image list error"); $rt_img = $jdb->nQuery($qry_img, "image list error");
// 분류 배열 function getImageTypeLabel($type) {
$install = []; switch ($type) {
$container = []; case 'install':
return 'Install Image';
case 'install_order':
return 'Container Request Image';
case 'clean_before':
return 'Before Clean';
case 'clean_after':
return 'After Clean';
default:
return ucfirst(str_replace('_', ' ', $type));
}
}
while ($img = mysqli_fetch_array($rt_img, MYSQLI_ASSOC)) { if (mysqli_num_rows($rt_img) == 0) {
if ($img['i_type'] == "install") $install[] = $img;
else $container[] = $img;
}
if (count($install) + count($container) == 0) {
echo "<p>No images found.</p>"; echo "<p>No images found.</p>";
} else { } else {
echo '<div style="display:flex; flex-wrap:wrap; gap:16px;">';
// =========================== while ($img = mysqli_fetch_array($rt_img, MYSQLI_ASSOC)) {
// INSTALL
// ===========================
if (count($install) > 0) {
echo "<h4 style='margin-top:20px;'>Install</h4>";
foreach ($install as $img) { echo '<div style="
width:220px;
border:1px solid #ddd;
border-radius:8px;
padding:10px;
background:#fafafa;
position:relative;
">';
echo '<div style=" echo '
border:1px solid #ddd; <button
padding:15px; class="btn btn-sm btn-danger btn-delete-image"
border-radius:8px; data-uid="'.htmlspecialchars($img['i_uid']).'"
margin-bottom:20px; style="
background:#fafafa; position:absolute;
">'; top:6px;
right:6px;
padding:2px 6px;
font-size:11px;
"
title="Delete image"
>
<i class="bi bi-x"></i>
</button>
';
echo '<div style="display:flex; gap:15px; flex-wrap:wrap;">'; // 이미지 영역 (위)
echo '<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;">';
$files = explode(",", $img['i_filename']); $files = explode(",", $img['i_filename']);
foreach ($files as $f) { foreach ($files as $f) {
$imgPath = $img['i_filepath'] . trim($f); $f = trim($f);
echo ' if (!$f) continue;
<div style="width:150px; text-align:center;">
<img src="'.$imgPath.'"
class="preview-img"
data-img="'.$imgPath.'"
data-filename="'.$f.'"
data-uid="'.$img['i_uid'].'"
data-customeruid="'.$c_uid.'"
data-type="'.$img['i_type'].'"
style="width:100%; height:120px;
object-fit:cover; border-radius:5px;
cursor:pointer; border:1px solid #ccc;">
</div>
';
}
echo '</div>'; $imgPath = $img['i_filepath'] . $f;
echo '<div style="margin-top:10px; font-size:14px; line-height:20px;"> echo '<img
<b>Note:</b> '.nl2br($img['i_note']).'<br>'; src="'.htmlspecialchars($imgPath).'"
echo "<b>Created:</b> " . date("Y-m-d H:i:s", strtotime($img['i_createddate'])).'<br>'; class="preview-img"
echo "<b>Created by:</b> " . $img['m_firstname']. ' ' . $img['m_lastname']. ' (' . $img['m_initial']. ')'; data-img="'.htmlspecialchars($imgPath).'"
echo '</div></div>'; style="
} width:100px;
height:80px;
object-fit:cover;
border-radius:5px;
cursor:pointer;
border:1px solid #ccc;
"
>';
}
echo '</div>';
// 설명 영역 (아래)
echo '<div style="font-size:12px; line-height:1.4;">';
echo '
<div style="margin-bottom:5px;">
<span class="badge bg-secondary" style="font-size:11px;">
'.htmlspecialchars(getImageTypeLabel($img['i_type'])).'
</span>
</div>
<div><b>Note:</b> '.nl2br(htmlspecialchars($img['i_note'])).'</div>
<div><b>Created:</b> '.date("Y-m-d H:i", strtotime($img['i_createddate'])).'</div>
<div><b>By:</b> '
. htmlspecialchars($img['m_firstname'] ?? '')
. ' '
. htmlspecialchars($img['m_lastname'] ?? '')
.'</div>
';
echo '</div>';
echo '</div>'; // card end
} }
// =========================== echo '</div>'; // wrapper end
// CONTAINER }
// ===========================
// if (count($container) > 0) {
// echo "<h4 style='margin-top:30px;'>Container</h4>";
// foreach ($container as $img) {
// echo '<div style="
// border:1px solid #ddd;
// padding:15px;
// border-radius:8px;
// margin-bottom:20px;
// background:#fafafa;
// ">';
// echo '<div style="display:flex; gap:15px; flex-wrap:wrap;">';
// $files = explode(",", $img['i_filename']);
// foreach ($files as $f) {
// $imgPath = $img['i_filepath'] . trim($f);
// echo '
// <div style="width:150px; text-align:center;">
// <img src="'.$imgPath.'"
// class="preview-img"
// data-img="'.$imgPath.'"
// data-filename="'.$f.'"
// data-uid="'.$img['i_uid'].'"
// data-customeruid="'.$c_uid.'"
// data-type="'.$img['i_type'].'"
// style="width:100%; height:120px;
// object-fit:cover; border-radius:5px;
// cursor:pointer; border:1px solid #ccc;">
// </div>
// ';
// }
// echo '</div>';
// echo '<div style="margin-top:10px; font-size:14px; line-height:20px;">
// <b>Note:</b> '.nl2br($img['i_note']).'<br>';
// echo "<b>Created:</b> " . date("Y-m-d H:i:s", strtotime($img['i_createddate'])).'<br>';
// echo '
// </div>
// <button onclick="openEditModal(
// \''.$img['i_uid'].'\',
// \''.htmlspecialchars($img['i_note'], ENT_QUOTES).'\',
// \''.$c_uid.'\',
// \''.htmlspecialchars($img['i_filename'], ENT_QUOTES).'\',
// \''.$img['i_filepath'].'\',
// \''.$img['i_type'].'\'
// )"
// style="margin-top:10px; padding:4px 10px; background:#007bff; color:white; border:none; border-radius:4px;">
// Edit
// </button>';
// echo '</div>';
// }
// }
}
?> ?>
</div> </div>
@ -2211,7 +2190,7 @@ if( $total_count < 1 ) {
<form id="deleteImageForm" method="POST" action="/lib/user_process.php"> <form id="deleteImageForm" method="POST" action="/lib/user_process.php">
<input type="hidden" name="actionStr" value="DELETEIMAGE"> <input type="hidden" name="actionStr" value="DELETEIMAGE">
<input type="hidden" name="i_uid" id="delete_i_uid"> <input type="hidden" name="i_uid" id="delete_i_uid">
<input type="hidden" name="i_customeruid" id="delete_i_customeruid"> <input type="hidden" name="i_customeruid" id="delete_i_customeruid" value="<?=$c_uid?>">
<input type="hidden" name="fileName" id="delete_fileName"> <input type="hidden" name="fileName" id="delete_fileName">
<input type="hidden" name="goStr" value="<?=$goStr?>"> <input type="hidden" name="goStr" value="<?=$goStr?>">
</form> </form>
@ -2414,22 +2393,48 @@ document.addEventListener("DOMContentLoaded", function () {
// Delete image // Delete image
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const deleteBtn = document.getElementById("btnDeleteImage"); const modalEl = document.getElementById("deleteConfirmModal");
const confirmModalObj = new bootstrap.Modal(document.getElementById("deleteConfirmModal")); if (!modalEl) return;
if (deleteBtn) { const confirmModalObj = new bootstrap.Modal(modalEl);
deleteBtn.addEventListener("click", function () {
confirmModalObj.show();
});
}
document.getElementById("confirmDeleteBtn").addEventListener("click", function () { // 삭제 버튼 클릭
const uid = document.getElementById("currentImageUid").value; document.addEventListener("click", function (e) {
const btn = e.target.closest(".btn-delete-image");
if (!btn) return;
const uid = btn.dataset.uid;
if (!uid) return;
// 현재 선택된 이미지 UID 저장
const uidInput = document.getElementById("currentImageUid");
if (uidInput) {
uidInput.value = uid;
}
confirmModalObj.show();
});
// confirm 버튼 클릭
const confirmBtn = document.getElementById("confirmDeleteBtn");
if (confirmBtn) {
confirmBtn.addEventListener("click", function () {
const uid = document.getElementById("currentImageUid")?.value;
if (!uid) return; if (!uid) return;
document.getElementById("delete_i_uid").value = uid; const formInput = document.getElementById("delete_i_uid");
document.getElementById("deleteImageForm").submit(); if (formInput) {
}); formInput.value = uid;
}
const form = document.getElementById("deleteImageForm");
if (form) {
form.submit();
}
});
}
}); });
function openEditModal(uid, note, customeruid, filesString, filepath, type) { function openEditModal(uid, note, customeruid, filesString, filepath, type) {

View File

@ -2,6 +2,7 @@
$selectedDate = $_GET['date'] ?? date('Y-m-d'); $selectedDate = $_GET['date'] ?? date('Y-m-d');
$canViewAllDrivers = ($_SESSION['ss_LEVEL'] <= 5 || $_SESSION['ss_GROUP'] == 1); $canViewAllDrivers = ($_SESSION['ss_LEVEL'] <= 5 || $_SESSION['ss_GROUP'] == 1);
$selectedDriver = $_GET['driver_uid'] ?? ($canViewAllDrivers ? 0 : $_SESSION['ss_DRUID']); $selectedDriver = $_GET['driver_uid'] ?? ($canViewAllDrivers ? 0 : $_SESSION['ss_DRUID']);
$view_mode = strtolower($_GET['view_mode'] ?? 'list');
$isAdmin = $canViewAllDrivers; $isAdmin = $canViewAllDrivers;
@ -40,6 +41,7 @@ $sql_order = "
SELECT 1 SELECT 1
FROM tbl_customer_image ci FROM tbl_customer_image ci
WHERE ci.i_sourceuid = di.di_uid WHERE ci.i_sourceuid = di.di_uid
and ci.i_type in ('install','clean_before','clean_after')
LIMIT 1 LIMIT 1
) AS has_image ) AS has_image
FROM tbl_daily_install di FROM tbl_daily_install di
@ -105,11 +107,10 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
// filepath + filename split // filepath + filename split
$files = array_filter(array_map('trim', explode(',', $img['i_filename'] ?? ''))); $files = array_filter(array_map('trim', explode(',', $img['i_filename'] ?? '')));
$first = $files ? $files[0] : '';
$imgPath = ''; $images = [];
if ($first !== '') { foreach ($files as $f) {
$imgPath = ($img['i_filepath'] ?? '') . $first; $images[] = ($img['i_filepath'] ?? '') . $f;
} }
echo json_encode([ echo json_encode([
@ -120,8 +121,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
'i_sourceuid' => $img['i_sourceuid'], 'i_sourceuid' => $img['i_sourceuid'],
'i_note' => $img['i_note'], 'i_note' => $img['i_note'],
'i_createddate' => $img['i_createddate'], 'i_createddate' => $img['i_createddate'],
'imgPath' => $imgPath, 'images' => $images // ⭐ 핵심
'filename' => $first
] ]
]); ]);
exit; exit;
@ -204,11 +204,11 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
<!-- TOGGLE --> <!-- TOGGLE -->
<div class="radio-segment d-flex" style="width:100%;"> <div class="radio-segment d-flex" style="width:100%;">
<label class="w-50 text-center"> <label class="w-50 text-center">
<input type="radio" name="view_mode" value="list" checked> <input type="radio" name="view_mode" value="list" <?= $view_mode === 'list' ? 'checked' : '' ?>>
<span>LIST</span> <span>LIST</span>
</label> </label>
<label class="w-50 text-center"> <label class="w-50 text-center">
<input type="radio" name="view_mode" value="map"> <input type="radio" name="view_mode" value="map" <?= $view_mode === 'map' ? 'checked' : '' ?>>
<span>MAP</span> <span>MAP</span>
</label> </label>
</div> </div>
@ -348,7 +348,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
<div id="wm_before_photo_list" class="photo-body"></div> <div id="wm_before_photo_list" class="photo-body"></div>
<input type="file" class="form-control form-control-sm mt-2" id="wm_photos_before" accept="image/*"> <input type="file" class="form-control form-control-sm mt-2" id="wm_photos_before" accept="image/*" multiple>
</div> </div>
</div> </div>
@ -359,7 +359,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
<div id="wm_after_photo_list" class="photo-body"></div> <div id="wm_after_photo_list" class="photo-body"></div>
<input type="file" class="form-control form-control-sm mt-2" id="wm_photos_after" accept="image/*"> <input type="file" class="form-control form-control-sm mt-2" id="wm_photos_after" accept="image/*" multiple>
</div> </div>
</div> </div>
@ -380,6 +380,7 @@ if (isset($_GET['ajax']) && $_GET['ajax'] === 'work_images') {
<!-- =============================== --> <!-- =============================== -->
<!-- IMAGE MODAL --> <!-- IMAGE MODAL -->
<!-- =============================== --> <!-- =============================== -->
<script src="/js/customer_image.js?v=<?=time()?>"></script>
<div class="modal fade" id="customerImageModal" tabindex="-1"> <div class="modal fade" id="customerImageModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-scrollable"> <div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
@ -669,20 +670,19 @@ function initWorkModal(){
const workType = selectedRow.di_work_type; const workType = selectedRow.di_work_type;
const afterFileInput = document.getElementById('wm_photos_after'); const afterFileInput = document.getElementById('wm_photos_after');
const beforeFileInput = document.getElementById('wm_photos_before'); const beforeFileInput = document.getElementById('wm_photos_before');
const afterFile = afterFileInput?.files?.[0] || null; const afterFiles = Array.from(afterFileInput?.files || []);
const beforeFile = beforeFileInput?.files?.[0] || null; const beforeFiles = Array.from(beforeFileInput?.files || []);
let uploads = []; let uploads = [];
// AFTER // AFTER
if(afterFile){ if(afterFiles.length){
const type = (workType === 'CLEAN') ? 'clean_after' : 'install'; const type = (workType === 'CLEAN') ? 'clean_after' : 'install';
uploads.push(uploadPhotoPromise(selectedRow, afterFile, type)); uploads.push(uploadPhotoPromise(selectedRow, afterFiles, type));
} }
// BEFORE (CLEAN만) if(workType === 'CLEAN' && beforeFiles.length){
if(workType === 'CLEAN' && beforeFile){ uploads.push(uploadPhotoPromise(selectedRow, beforeFiles, 'clean_before'));
uploads.push(uploadPhotoPromise(selectedRow, beforeFile, 'clean_before'));
} }
if(uploads.length > 0){ if(uploads.length > 0){
@ -1053,26 +1053,24 @@ function renderAfterPhoto(photo){
const wrap = document.getElementById('wm_after_photo_list'); const wrap = document.getElementById('wm_after_photo_list');
if(!wrap) return; if(!wrap) return;
if(!photo || !photo.imgPath){ if(!photo || !photo.images || !photo.images.length){
wrap.innerHTML = `<div class="small text-muted">No after photo.</div>`; wrap.innerHTML = `<div class="small text-muted">No after photo.</div>`;
return; return;
} }
const url = photo.imgPath;
wrap.innerHTML = ` wrap.innerHTML = `
<div style="width:100%;"> <div style="display:flex; gap:8px; flex-wrap:wrap;">
<img src="${escapeHtml(url)}" ${photo.images.map(url => `
style="width:100%; height:160px; object-fit:cover; border-radius:8px; border:1px solid #ddd; cursor:pointer;" <img src="${escapeHtml(url)}"
class="after-photo-thumb" style="width:100px; height:80px; object-fit:cover; border-radius:6px; border:1px solid #ddd; cursor:pointer;"
data-img-url="${escapeHtml(url)}"> class="after-photo-thumb"
data-img-url="${escapeHtml(url)}">
`).join('')}
</div> </div>
`; `;
// 클릭 이벤트 연결 wrap.querySelectorAll('.after-photo-thumb').forEach(img => {
const imgEl = wrap.querySelector('.after-photo-thumb'); img.addEventListener('click', function(){
if(imgEl){
imgEl.addEventListener('click', function(){
const modalEl = document.getElementById('customerImageModal'); const modalEl = document.getElementById('customerImageModal');
const body = document.getElementById('customerImageModalBody'); const body = document.getElementById('customerImageModalBody');
@ -1081,15 +1079,14 @@ function renderAfterPhoto(photo){
body.innerHTML = ` body.innerHTML = `
<div class="text-center"> <div class="text-center">
<img src="${escapeHtml(url)}" <img src="${this.dataset.imgUrl}"
style="max-width:100%; max-height:75vh; border-radius:10px;"> style="max-width:100%; max-height:75vh; border-radius:10px;">
</div> </div>
`; `;
const modal = new bootstrap.Modal(modalEl); new bootstrap.Modal(modalEl).show();
modal.show();
}); });
} });
} }
function renderBeforePhoto(photo){ function renderBeforePhoto(photo){
@ -1097,19 +1094,40 @@ function renderBeforePhoto(photo){
const wrap = document.getElementById('wm_before_photo_list'); const wrap = document.getElementById('wm_before_photo_list');
if(!wrap) return; if(!wrap) return;
if(!photo || !photo.imgPath){ if(!photo || !photo.images || !photo.images.length){
wrap.innerHTML = `<div class="small text-muted">No before photo.</div>`; wrap.innerHTML = `<div class="small text-muted">No before photo.</div>`;
return; return;
} }
const url = photo.imgPath;
wrap.innerHTML = ` wrap.innerHTML = `
<div style="width:100%;"> <div style="display:flex; gap:8px; flex-wrap:wrap;">
<img src="${escapeHtml(url)}" ${photo.images.map(url => `
style="width:100%; height:160px; object-fit:cover; border-radius:8px; border:1px solid #ddd; cursor:pointer;"> <img src="${escapeHtml(url)}"
style="width:100px; height:80px; object-fit:cover; border-radius:6px; border:1px solid #ddd; cursor:pointer;"
class="before-photo-thumb"
data-img-url="${escapeHtml(url)}">
`).join('')}
</div> </div>
`; `;
wrap.querySelectorAll('.before-photo-thumb').forEach(img => {
img.addEventListener('click', function(){
const modalEl = document.getElementById('customerImageModal');
const body = document.getElementById('customerImageModalBody');
if(!modalEl || !body) return;
body.innerHTML = `
<div class="text-center">
<img src="${this.dataset.imgUrl}"
style="max-width:100%; max-height:75vh; border-radius:10px;">
</div>
`;
new bootstrap.Modal(modalEl).show();
});
});
} }
function resetPhotoBlocks(){ function resetPhotoBlocks(){
@ -1159,6 +1177,7 @@ function openCustomerImages(customerUid, iType = 'install_order') {
.then(res => res.text()) .then(res => res.text())
.then(html => { .then(html => {
body.innerHTML = html; body.innerHTML = html;
initCustomerImageSlider(body);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@ -1238,7 +1257,7 @@ function togglePhotoSection(workType){
} }
} }
function uploadPhotoPromise(row, file, type){ function uploadPhotoPromise(row, files, type){
return new Promise((resolve) => { return new Promise((resolve) => {
@ -1250,7 +1269,9 @@ function uploadPhotoPromise(row, file, type){
fd.append('member_uid', memberUid); fd.append('member_uid', memberUid);
fd.append('note', (document.getElementById('wm_comment').value || '').trim()); fd.append('note', (document.getElementById('wm_comment').value || '').trim());
fd.append('i_type', type); fd.append('i_type', type);
fd.append('photo', file); files.forEach(file => {
fd.append('photos[]', file);
});
fetch('/assets/internal_api.php?func=upload_after_photo', { fetch('/assets/internal_api.php?func=upload_after_photo', {
method: 'POST', method: 'POST',
@ -1855,12 +1876,14 @@ function reloadPage(){
const date = document.getElementById('installDate').value; const date = document.getElementById('installDate').value;
const driver = document.getElementById('driverSelect').value; const driver = document.getElementById('driverSelect').value;
const viewMode = document.querySelector('input[name="view_mode"]:checked')?.value || 'list';
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.set('view', 'install_order'); url.searchParams.set('view', 'install_order');
url.searchParams.set('date', date); url.searchParams.set('date', date);
url.searchParams.set('driver_uid', driver); url.searchParams.set('driver_uid', driver);
url.searchParams.set('view_mode', viewMode);
window.location.href = url.toString(); window.location.href = url.toString();
} }
@ -2115,6 +2138,22 @@ function showPopupMessage(message, type = 'success', duration = 1200) {
/* =============================== /* ===============================
TOGGLE TOGGLE
=================================*/ =================================*/
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
const viewMode = params.get('view_mode') || 'list';
const targetRadio = document.querySelector(`input[name="view_mode"][value="${viewMode}"]`);
if (targetRadio) {
targetRadio.checked = true;
// change 강제 실행
targetRadio.dispatchEvent(new Event('change'));
}
});
document.querySelectorAll('input[name="view_mode"]').forEach(radio => { document.querySelectorAll('input[name="view_mode"]').forEach(radio => {
radio.addEventListener('change', function () { radio.addEventListener('change', function () {
document.getElementById('mapSection') document.getElementById('mapSection')

View File

@ -1562,6 +1562,7 @@ function formatPhone($phone) {
di_customer_name: w.iw_customer_name, di_customer_name: w.iw_customer_name,
di_address: w.iw_address, di_address: w.iw_address,
di_city: w.iw_city, di_city: w.iw_city,
di_postal: w.iw_postal,
di_lat: w.iw_lat, di_lat: w.iw_lat,
di_lng: w.iw_lng, di_lng: w.iw_lng,
di_work_type: w.iw_work_type, di_work_type: w.iw_work_type,
@ -1576,7 +1577,7 @@ function formatPhone($phone) {
// lat/lng 없으면 여기서 geocode // lat/lng 없으면 여기서 geocode
if (!row.di_lat || !row.di_lng) { if (!row.di_lat || !row.di_lng) {
const fullAddress = [w.iw_address, w.iw_city].filter(Boolean).join(', '); const fullAddress = [w.iw_address, w.iw_city, w.iw_postal].filter(Boolean).join(', ');
if (fullAddress) { if (fullAddress) {
geocoder.geocode({ address: fullAddress }, (results, status) => { geocoder.geocode({ address: fullAddress }, (results, status) => {

View File

@ -107,11 +107,9 @@
<!-- New --> <!-- New -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.js"></script>
<script src="/js/customer_image.js?v=20260213"></script>
<div class="loading-overlay">Loading...</div>
<div class="loading-overlay">Loading...</div>
<div id="popupMessage"></div> <div id="popupMessage"></div>
@ -799,6 +797,7 @@ function popup(){
</div> </div>
<!-- image modal --> <!-- image modal -->
<script src="/js/customer_image.js?v=<?=time()?>"></script>
<div class="modal fade top map" id="customerImageModal" tabindex="-1" role="dialog"> <div class="modal fade top map" id="customerImageModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl modal-dialog-scrollable"> <div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
@ -2514,6 +2513,7 @@ function popup(){
const outer = document.getElementById('simpleInstallPhotoWrap'); const outer = document.getElementById('simpleInstallPhotoWrap');
if (!wrap || !outer) return; if (!wrap || !outer) return;
wrap.innerHTML = ''; wrap.innerHTML = '';
if (!photos || photos.length === 0) { if (!photos || photos.length === 0) {
@ -2522,20 +2522,35 @@ function popup(){
} }
outer.style.display = 'block'; outer.style.display = 'block';
photos.forEach(photo => { photos.forEach(photo => {
if (!photo.imgPath) return; if (!photo.i_filename) return;
const url = photo.imgPath; const basePath = (photo.i_filepath || '').trim();
const div = document.createElement('div');
div.style.width = '120px';
div.innerHTML = ` const filenames = photo.i_filename
<img src="${url}" .split(',')
.map(f => f.trim())
.filter(Boolean);
filenames.forEach(name => {
const fullUrl = basePath.endsWith('/')
? basePath + name
: basePath + '/' + name;
const div = document.createElement('div');
div.style.width = '120px';
div.innerHTML = `
<img src="${fullUrl}"
style="width:100%; height:100px; object-fit:cover; border-radius:8px; border:1px solid #ddd;"> style="width:100%; height:100px; object-fit:cover; border-radius:8px; border:1px solid #ddd;">
`; `;
wrap.appendChild(div);
});
wrap.appendChild(div);
}); });
} }
@ -2784,6 +2799,7 @@ function popup(){
.then(res => res.text()) .then(res => res.text())
.then(html => { .then(html => {
body.innerHTML = html; body.innerHTML = html;
initCustomerImageSlider(body);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);

View File

@ -18,7 +18,7 @@ $sqDriver = qry("SELECT
FROM FROM
tbl_member tbl_member
WHERE WHERE
m_level = 9 AND m_status = 'A' ".$qrySTR." m_level in (8,9) AND m_status = 'A' ".$qrySTR." AND m_gid = 0
ORDER BY ORDER BY
m_initial ASC "); m_initial ASC ");
while($rstDriver = fetch_array($sqDriver)){ while($rstDriver = fetch_array($sqDriver)){
@ -56,12 +56,10 @@ while($rstDriver = fetch_array($sqDriver)){
<label>Created By</label> <label>Created By</label>
<select name="i_createdby" id="driver" class="custom-select-map-orderby" required> <select name="i_createdby" id="driver" class="custom-select-map-orderby" required>
<option value="">Select</option> <option value="<?=$_SESSION['ss_UID']?>">Select</option>
<?php <?php
$defaultValue = 43; // E.X
foreach($drivers as $driver) { foreach($drivers as $driver) {
$selected = ($driver['id'] == $defaultValue) ? 'selected' : ''; echo '<option value="'.$driver['id'].'">'.$driver['name'].'</option>';
echo '<option value="'.$driver['id'].'" '.$selected.'>'.$driver['name'].'</option>';
} }
?> ?>
</select> </select>
@ -70,15 +68,17 @@ while($rstDriver = fetch_array($sqDriver)){
<label>Type</label> <label>Type</label>
<select name="i_type" id="i_type" required> <select name="i_type" id="i_type" required>
<option value="install">Install</option> <option value="install">Install</option>
<option value="install_order">Install Request</option>
<option value="clean_before">Before Clean</option>
<option value="clean_after">After Clean</option>
<!-- <option value="container">Container</option> --> <!-- <option value="container">Container</option> -->
</select> </select>
<label>Image</label> <label>Image</label>
<p style="font-size:12px; color:#666; margin-top:4px;"> 기존 이미지가 있을 경우 삭제됩니다.</p> <p style="font-size:12px; color:#666; margin-top:4px;"> Will replace the same type images</p>
<p style="font-size:12px; color:#666; margin-top:-6px;"> Install 이미지는 1개만 업로드 가능합니다.</p> <input type="file" id="uploadFileInput" name="upload_file[]" accept="image/*" multiple required>
<input type="file" id="uploadFileInput" name="upload_file[]" accept="image/*" required>
<img id="previewImg" class="preview-img"> <div id="previewList" style="display:flex; gap:8px; flex-wrap:wrap; margin-top:12px;"></div>
<label>Note</label> <label>Note</label>
<textarea name="i_note" rows="3"></textarea> <textarea name="i_note" rows="3"></textarea>
@ -105,19 +105,28 @@ document.addEventListener("DOMContentLoaded", function () {
// 이미지 preview // 이미지 preview
const input = document.getElementById("uploadFileInput"); const input = document.getElementById("uploadFileInput");
const preview = document.getElementById("previewImg"); const previewList = document.getElementById("previewList");
if (input) { if (input) {
input.addEventListener("change", function () { input.addEventListener("change", function () {
const file = this.files[0]; previewList.innerHTML = "";
if (!file) return;
const reader = new FileReader(); Array.from(this.files).forEach(file => {
reader.onload = function () { if (!file.type.startsWith("image/")) return;
preview.src = reader.result;
preview.style.display = "block"; const reader = new FileReader();
}; reader.onload = function () {
reader.readAsDataURL(file); const img = document.createElement("img");
img.src = reader.result;
img.style.width = "90px";
img.style.height = "70px";
img.style.objectFit = "cover";
img.style.border = "1px solid #ccc";
img.style.borderRadius = "6px";
previewList.appendChild(img);
};
reader.readAsDataURL(file);
});
}); });
} }

View File

@ -455,7 +455,7 @@ if ($actionStr == "DELETENOTE" && $mode == "delete") {
// ADD IMAGE (Customer Detail - Image Upload) // ADD IMAGE (Customer Detail - Image Upload)
////////////////////////////////////////////// //////////////////////////////////////////////
if ($actionStr == "ADDIMAGE") { if ($actionStr == "ADDIMAGE") {
$singleTypes = ['install', 'container', 'install_order']; $singleTypes = ['install', 'install_order', 'clean_before', 'clean_after'];
$i_note = isset($_POST['i_note']) ? trim($_POST['i_note']) : ""; $i_note = isset($_POST['i_note']) ? trim($_POST['i_note']) : "";
$i_createddate = date("YmdHis"); $i_createddate = date("YmdHis");
@ -478,37 +478,6 @@ if ($actionStr == "ADDIMAGE") {
$jdb->nQuery($qry_update, "update error"); $jdb->nQuery($qry_update, "update error");
} }
// 이미지 최대 6개 제한 체크
if ($i_type == "container") {
// 기존 이미지 수 조회
$qry_cnt = "
SELECT i_filename
FROM tbl_customer_image
WHERE i_customeruid = '$i_customeruid'
AND i_type = 'container'
AND i_status = 'A'
ORDER BY i_uid DESC LIMIT 1
";
$rt_cnt = $jdb->fQuery($qry_cnt, "error");
$existingCount = 0;
if (!empty($rt_cnt['i_filename'])) {
$existingFiles = explode(",", $rt_cnt['i_filename']);
$existingCount = count($existingFiles);
}
// 이번에 업로드할 파일 개수
$newCount = is_array($_FILES['upload_file']['name'])
? count($_FILES['upload_file']['name'])
: 1;
if ($existingCount + $newCount > 6) {
$msg = "Container 이미지는 최대 6개까지 업로드할 수 있습니다.";
$func->modalMsg($msg, "/index_intranet.php?view=customer_detail&mode=update&c_uid=$i_customeruid&$goStr");
exit();
}
}
// 업로드 파일 체크 // 업로드 파일 체크
if (!isset($_FILES['upload_file'])) { if (!isset($_FILES['upload_file'])) {
$msg = "Upload failed. Please try again."; $msg = "Upload failed. Please try again.";
@ -592,7 +561,7 @@ if ($actionStr == "ADDIMAGE") {
$values = array( $values = array(
$i_customeruid, $i_customeruid,
$i_memberuid, $i_memberuid,
$i_memberuid, $i_createdby,
$i_createddate, $i_createddate,
$i_type, $i_type,
$fileNameString, $fileNameString,