아파트단지 상세 파일업로드 추가

This commit is contained in:
yangsh
2025-12-26 09:28:39 +09:00
parent 6b7e8ad386
commit db8e33f10d
6 changed files with 1114 additions and 82 deletions

View File

@@ -42,6 +42,31 @@
background-color: #ff0000 !important;
color: #fff !important;
}
.dropzone {
min-height: 260px;
background-color: #fafbfc;
}
.dropzone:hover {
background-color: #f4f6f8;
}
#myDropzone {
position: relative;
padding-top: 110px;
/* 메시지 영역 높이만큼 */
}
#uploadModal .modal-dialog {
max-width: 1140px;
/* modal-xl */
}
#uploadModal .modal-content {
height: 450px;
max-height: 450px;
}
</style>
<h1>아파트단지 DB구축 상세</h1>
@@ -85,12 +110,11 @@
<td>
<div class="row g-2 align-items-center">
<div class="col-md-9">
<!-- ✅ 원본: value="" <?= $apt['memo'] ?> -> HTML 속성 깨짐 -->
<input class="form-control" type="text" id="memo"
value="<?= esc($apt['memo'] ?? '') ?>" />
</div>
<div class="col-md-3 text-end">
<button type="button" class="btn btn-outline-light"
<button type="button" class="btn btn-outline-focus"
onclick="saveMemo('<?= esc($apt['rcpt_no'] ?? '') ?>')">저장</button>
</div>
</div>
@@ -155,7 +179,7 @@
</td>
<td style="text-align:center">
<button type="button" class="btn btn-sm btn-outline-light"
<button type="button" class="btn btn-sm btn-outline-focus"
onclick="saveKeeper('<?= esc($apt['rcpt_no'] ?? '') ?>')">저장</button>
</td>
</tr>
@@ -227,7 +251,7 @@
<h5 class="mb-2">단지 특이사항</h5>
<textarea name="note" id="note" class="form-control mb-2"
style="height: 220px;"><?= esc($apt['note'] ?? '') ?></textarea>
style="height: 220px;resize: none;"><?= esc($apt['note'] ?? '') ?></textarea>
<div class="d-flex align-items-center">
<div class="ms-auto">
@@ -279,10 +303,21 @@
<tr>
<th rowspan="2" style="text-align: center;">동영상</th>
<td rowspan="2" style="text-align: center;">
<img src="/plugin/img/photo.gif" alt="비디오" style="padding: 3px;"><br>
<button class="btn btn-sm btn-outline-success" type="button" id="btn_upload_video">
<i class="fa fa-fw" aria-hidden="true" title="Copy to use file-image-o"></i> 파일
</button>
<div class="d-flex flex-column align-items-center gap-2">
<?php if (!empty($apt['vdo_path'])): ?>
<button class="btn btn-sm btn-danger" type="button" id="btn_preview_video"
data-video-src="<?= esc($apt['vdo_path']) ?>"
onclick="fn_preview('<?= esc($apt['vdo_path']) ?>', 'vdo')">
<i class="fa fa-play-circle me-1"></i> 동영상확인
</button>
<?php else: ?>
<img src="/plugin/img/photo.gif" alt="비디오" style="padding: 3px;"><br>
<?php endif; ?>
<button class="btn btn-sm btn-outline-success" type="button" id="btn_upload_video">
<i class="fa fa-fw" aria-hidden="true" title="Copy to use file-image-o"></i> 파일
</button>
</div>
</td>
<th style="text-align: center;">동영상 촬영불가</th>
<td style="text-align: center;">
@@ -352,17 +387,181 @@
</td>
<th style="text-align:center">사진 일괄삭제</th>
<td style="text-align:center">
<button type="button" class="btn btn-sm btn-outline-danger">삭제</button>
<button type="button" class="btn btn-sm btn-outline-danger"
onclick="fn_pho_remove(<?= $apt['rcpt_no'] ?>, 'all');">삭제</button>
</td>
<th style="text-align:center">단지정보 작성완료</th>
<td style="text-align:center">
<button type="button" class="btn btn-sm btn-outline-light">저장</button>
<button type="button" class="btn btn-sm btn-outline-focus"
onclick="fn_write_complete(<?= $apt['rcpt_no'] ?>);">저장</button>
</td>
</tr>
</table>
</div>
</div>
<?php
// 카테고리 없는 이미지만 추출
$arrPho = [];
foreach ($image as $key => $val) {
if (empty($val['pho_cate2'])) {
$arrPho[] = $val;
unset($image[$key]);
}
}
if (!empty($arrPho)):
?>
<style>
/* 썸네일 카드 */
.pho-grid {
display: grid;
gap: 12px;
}
@media (min-width: 576px) {
.pho-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 992px) {
.pho-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.pho-item {
border: 1px solid #e6e9ef;
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
}
.pho-thumb {
position: relative;
aspect-ratio: 4 / 3;
background: #f6f7f9;
}
.pho-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.pho-check {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, .92);
border: 1px solid rgba(0, 0, 0, .06);
padding: 6px 8px;
border-radius: 999px;
display: inline-flex;
align-items: center;
gap: 6px;
}
.pho-meta {
padding: 10px 12px;
border-top: 1px solid #f0f2f6;
}
.pho-title {
font-size: 13px;
font-weight: 600;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pho-sub {
font-size: 12px;
color: #6c757d;
margin: 4px 0 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<div class="main-card mb-3 card">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-2">
<h5 class="card-title mb-0">카테고리 미지정 이미지</h5>
<div class="d-flex align-items-center gap-2">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="chkAllPho">
<label class="form-check-label" for="chkAllPho">전체선택</label>
</div>
<span class="badge bg-secondary"><?= count($arrPho) ?>건</span>
</div>
</div>
<div class="pho-grid mt-3">
<?php foreach ($arrPho as $row):
$link = (string) ($row['file_path'] ?? '') . (string) ($row['filenm_up'] ?? '');
$thumb = (string) ($row['thumb_path'] ?? '') . (string) ($row['thumb_nm'] ?? '');
$fileNm = $row['filenm'] ?? '';
if (($row['cloud_upload_yn'] ?? '') === 'Y') {
$link = NCLOUD_OBJECT_STORAGE_URL . $link;
$thumb = NCLOUD_OBJECT_STORAGE_URL . $thumb;
}
$phoNo = $row['pho_no'] ?? '';
?>
<div class="pho-item">
<div class="pho-thumb">
<a onclick="fn_preview('<?= esc($link) ?>')" rel="lightbox">
<img src="<?= esc($thumb) ?>" alt="<?= esc($fileNm) ?>" loading="lazy">
</a>
<div class="pho-check">
<input class="form-check-input m-0" type="checkbox" name="phoNo[]"
value="<?= esc($phoNo) ?>" id="phoNo_<?= esc($phoNo) ?>">
<label class="small mb-0" for="phoNo_<?= esc($phoNo) ?>">선택</label>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="card-footer d-flex justify-content-end align-items-center gap-3">
<div class="d-flex justify-content-end align-items-center gap-1 flex-wrap">
<select class="form-select form-select-sm w-auto" id="pho_cate1">
<option value="">- 대분류 -</option>
<?php if (!empty($code1)): ?>
<?php foreach ($code1 as $c): ?>
<option value="<?= $c['cd'] ?>"><?= $c['cd_nm'] ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
<select class="form-select form-select-sm w-auto" id="pho_cate2">
<option value="">- 소분류 -</option>
</select>
<button class="btn btn-sm btn-primary" onclick="fn_pho_save(<?= $apt['rcpt_no'] ?>);">
저장
</button>
<button class="btn btn-sm btn-danger" onclick="fn_pho_remove(<?= $apt['rcpt_no'] ?>, 'select');">
삭제
</button>
</div>
</div>
</div>
<?php endif; ?>
<div class="main-card mb-3 card">
<div class="card-body">
<h5 class="card-title">단지 사진 및 설명 정보</h5>
@@ -493,7 +692,7 @@
</tr>
<tr>
<td style="border: 0;">
<a href="<?= esc($link) ?>" rel="lightbox">
<a onclick="fn_preview('<?= esc($link) ?>')" rel="lightbox">
<img src="<?= esc($thumb) ?>" style="<?= esc($borderStyle) ?>" />
</a>
</td>
@@ -635,7 +834,7 @@
<!-- ✅ textarea 안에 PHP 로직 제거 -->
<textarea class="form-control" name="pho_explain"
id="pho_explain_<?= esc($c2cd) ?>"
style="width:350px;height:90px;"><?= esc($phoExplain) ?></textarea>
style="width:350px;height:90px;resize: none;"><?= esc($phoExplain) ?></textarea>
</td>
<td style="border: 0; padding: 0;">
<!-- <span id="count_<?= esc($c2cd) ?>" style="font-weight:bold">0</span> -->
@@ -671,7 +870,7 @@
</div>
<?php
if (in_array(session('usr_level'), array('1', '2', '70'))): ?>
<div class="card-footer card-footer d-flex justify-content-center">
<div class="card-footer d-flex justify-content-center">
<button class="mb-2 me-2 btn btn-success"
onclick="ajax_saveCheck('<?= $apt['rcpt_no'] ?>','<?= $apt['hscp_no'] ?>');">검수</button>
<button class="mb-2 me-2 btn btn-warning"
@@ -734,24 +933,31 @@
<input type="hidden" name="rcpt_no" value="<?= $apt['rcpt_no'] ?>">
<input type="hidden" name="upload_type" value="photo">
<div class="form-group">
<div class="row">
<!-- 전체 업로드 -->
<div class="col-md-2">
<button class="btn btn-block btn-outline-success" type="button" id="btnUpload">
<i class="pe-7s-up-arrow"></i> 전체업로드 </button>
</div>
<!-- 버튼 툴바 -->
<div class="d-flex justify-content-end gap-2 mb-3" style="padding: 16px 10px 0 0;">
<button type="button" class="btn btn-primary" id="uploadPick">
<i class="pe-7s-up-arrow"></i> 파일선택
</button>
<!-- 업로드 취소 -->
<div class="col-md-2">
<button class="btn btn-block btn-outline-danger" type="button" id="btnRemove">
<i class="pe-7s-less"></i> 업로드취소 </button>
</div>
<button type="button" class="btn btn-success" id="btnUpload">
<i class="pe-7s-up-arrow"></i> 파일업로드
</button>
</div>
<button type="button" class="btn btn-danger" id="btnRemove">
<i class="pe-7s-less"></i> 업로드취소
</button>
</div>
<div id="myDropzone" class="form-group dropzone">
<!-- Dropzone 영역 -->
<div id="myDropzone" class="dropzone border rounded-3 p-4"
style="max-height: 400px;overflow-y: scroll;">
<div class="dz-message dz-message-fixed needsclick text-center">
<i class="pe-7s-upload mb-2" style="font-size:42px;"></i><br>
<strong class="fs-6">파일을 드래그하거나 클릭해서 추가하세요</strong><br>
<small class="text-muted">
사진 여러 장 가능 / 동영상은 1개만
</small>
</div>
</div>
</form>
@@ -773,6 +979,25 @@
</div>
</div>
</div>
<div class="modal" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">미리보기</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<img id="imgPreview" src="" alt="미리보기" width="100%" height="500px">
<video id="vdoPreview" controls playsinline preload="metadata"
style="display: none;height: 500px;width: 100%;">
<source id="videoSource" src="" type="video/mp4">
브라우저가 video 태그를 지원하지 않습니다.
</video>
</div>
</div>
</div>
</div>
<?= $this->endSection() ?>
<style>
@@ -788,6 +1013,8 @@
const teamArr = <?= json_encode($team ?? [], JSON_UNESCAPED_UNICODE); ?>;
const userArr = <?= json_encode($user ?? [], JSON_UNESCAPED_UNICODE); ?>;
const code2Arr = <?= json_encode($code2 ?? [], JSON_UNESCAPED_UNICODE); ?>;
var map, marker;
Dropzone.autoDiscover = false;
@@ -836,8 +1063,8 @@
map = new naver.maps.Map('mapArea', {
center: new naver.maps.LatLng(lat, lng),
useStyleMap: true,
zoom: 15,
minZoom: 1,
zoom: 17,
minZoom: 10,
mapTypeControl: true,
mapTypeControlOptions: {
style: naver.maps.MapTypeControlStyle.BUTTON,
@@ -898,7 +1125,7 @@
uploadMultiple: true,
parallelUploads: 20,
maxFilesize: 100,
acceptedFiles: '.jpeg,.jpg,.JPEG,.JPG,.mpg,.MP4',
acceptedFiles: '.jpeg,.jpg,.JPEG,.JPG,.webp,.mpg,.MP4',
addRemoveLinks: true,
dictRemoveFile: "삭제",
dictDefaultMessage: "파일을 여기에 드래그하거나 클릭해서 추가하세요",
@@ -949,6 +1176,7 @@
}
});
let isFormDataAppended = false;
dz.on("sending", function (file, xhr, formData) {
if (isFormDataAppended) return;
@@ -959,12 +1187,30 @@
isFormDataAppended = true;
});
dz.on("completeMultiple", function () {
dz.on("queuecomplete", function () {
location.reload();
});
dz.on("successmultiple", function () {
isFormDataAppended = false;
});
dz.on("errormultiple", function () {
isFormDataAppended = false;
});
dz.on("canceledmultiple", function () {
isFormDataAppended = false;
});
dz.on("processingmultiple", function () { });
// 업로드파일 선택
$("#uploadPick").on("click", function () {
isFormDataAppended = false;
dz.hiddenFileInput.click();
});
$("#btnUpload").on("click", function () {
dz.processQueue(); // 업로드 실행
isFormDataAppended = false;
});
$("#btnRemove").on("click", function () {
@@ -979,6 +1225,49 @@
dz.removeFile(file);
});
});
// 체크박스 전체선택
$("#chkAllPho").on("click", function () {
const isChecked = $(this).is(":checked");
$("input[name='phoNo[]']").prop("checked", isChecked);
});
$("input[name='phoNo[]']").on("change", function () {
const total = $("input[name='phoNo[]']").length;
const checked = $("input[name='phoNo[]']:checked").length;
$("#chkAllPho").prop("checked", total === checked);
});
// 카테고리 onchange
$("#pho_cate1").on("change", function (e) {
const val = e.target.value;
var str = "";
str = "<option value=''>-소분류-</option>";
$.getJSON("/article/apt/cateJson?pho_cate1=" + val, function (result) {
var total = result.length;
for (var i = 0; i < total; i++) {
var cateNm = result[i].cd_nm;
if (total == 1) {
str += "<option value=\"" + result[i].cd + "\" selected>" + cateNm + "</option>";
} else {
str += "<option value=\"" + result[i].cd + "\">" + cateNm + "</option>";
}
}
$("#pho_cate2").html(str);
});
});
});
@@ -1443,6 +1732,148 @@
}
// 사진 카테고리 저장
function fn_pho_save(rcpt_no) {
const code1 = $("#pho_cate1").val();
const code2 = $("#pho_cate2").val();
if (code1 === "") {
Swal.fire({
title: "대분류를 선택해주세요.",
icon: "warning",
});
return;
}
if (code2 === "") {
Swal.fire({
title: "소분류를 선택해주세요.",
icon: "warning",
});
return;
}
var phoNos = $('input[name="phoNo[]"]:checked')
.map(function () { return this.value; })
.get();
if (phoNos.length === 0) {
Swal.fire({
title: "사진을 선택해주세요.",
icon: "warning",
});
return;
}
data = {
'rcpt_no': rcpt_no,
'phoNo': phoNos,
'code1': code1,
'code2': code2,
};
swal.fire({
text: "저장 하시겠습니까?",
type: "warning",
showCancelButton: true,
confirmButtonText: "예",
cancelButtonText: "아니오",
closeOnConfirm: false,
closeOnCancel: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
}).then((result) => {
if (result.isConfirmed) {
callAjax("/article/apt/savePhoCate", data, fn_result);
}
});
}
// 사진 일괄삭제
function fn_pho_remove(rcpt_no, type) {
var data = new Array();
if (type === "all") {
data = {
'type': type,
'rcpt_no': rcpt_no
};
} else if (type === "select") {
var phoNos = $('input[name="phoNo[]"]:checked')
.map(function () { return this.value; })
.get();
if (phoNos.length === 0) {
Swal.fire({
title: "삭제할 사진을 선택해주세요.",
icon: "warning",
});
return;
}
data = {
'type': type,
phoNo: phoNos,
};
}
if (data === null) {
Swal.fire({
title: "데이터 누락",
icon: "warning",
});
return;
}
var title = "삭제 하시겠습니까?"
swal.fire({
text: title,
type: "warning",
showCancelButton: true,
confirmButtonText: "예",
cancelButtonText: "아니오",
closeOnConfirm: false,
closeOnCancel: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
}).then((result) => {
if (result.isConfirmed) {
callAjax("/article/apt/reqRemovePho", data, fn_result);
}
});
}
// 단지정보 작성완료
function fn_write_complete(rcpt_no) {
swal.fire({
text: "저장하시겠습니까?",
type: "warning",
showCancelButton: true,
confirmButtonText: "예",
cancelButtonText: "아니오",
closeOnConfirm: false,
closeOnCancel: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
}).then((result) => {
if (result.isConfirmed) {
var data = {
'rcpt_no': rcpt_no
};
callAjax("/article/apt/saveWriteComplete", data, fn_result);
}
});
}
// 검수 저장
function ajax_saveCheck(rcpt_no, hscp_no) {
var stat = '<?php echo $apt['apt_step'] ?>';
@@ -1567,6 +1998,11 @@
icon: "success",
draggable: true
})
setTimeout(() => {
location.reload();
}, 1000);
} else {
Swal.fire({
title: result.msg,
@@ -1609,6 +2045,42 @@
$("#mapModal").modal("show");
}
// 이미지 프리뷰
function fn_preview(src, type = 'img') {
const $img = $('#imgPreview');
const $video = $('#vdoPreview');
const video = document.getElementById('vdoPreview');
const source = document.getElementById('videoSource');
if (type === 'vdo') {
// 이미지 숨김
$img.hide().attr('src', '');
// video source 세팅
source.src = '<?= NCLOUD_OBJECT_STORAGE_URL ?>' + src;
// video 표시 + 로드
$video.show();
video.load();
$('#previewTitle').text('동영상 미리보기');
} else {
// video 정지 및 초기화
video.pause();
source.src = '';
video.load();
$video.hide();
// 이미지 표시
$img.attr('src', src).show();
$('#previewTitle').text('이미지 미리보기');
}
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
}
</script>
<?= $this->endSection() ?>