From 5ac19523c1d0ff83bf1010e9e4f5e213e9d7e183 Mon Sep 17 00:00:00 2001 From: dsyoon Date: Mon, 13 Jul 2026 09:07:10 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20NCue=20=EB=A1=9C=EA=B3=A0=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=9C=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 썸네일에서 서버의 실제 이미지 파일을 안전하게 삭제하고 갤러리 상태를 동기화한다. Co-authored-by: Cursor --- api/delete_image.php | 52 +++++++++++++++++++++++ css/galleriffic-2.css | 26 ++++++++++++ index.php | 3 +- js/bestpic.js | 98 +++++++++++++++++++++++++++++++++++++++---- js/uploader.js | 24 ++++------- 5 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 api/delete_image.php diff --git a/api/delete_image.php b/api/delete_image.php new file mode 100644 index 0000000..b753aae --- /dev/null +++ b/api/delete_image.php @@ -0,0 +1,52 @@ + false, 'error' => 'Method not allowed']); + exit; +} + +$filename = isset($_POST['filename']) ? (string)$_POST['filename'] : ''; +$filename = basename($filename); + +if ($filename === '' || $filename === '.' || $filename === '..') { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Invalid filename']); + exit; +} + +$allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp']; +$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); +if (!in_array($ext, $allowedExt, true)) { + http_response_code(400); + echo json_encode(['ok' => false, 'error' => 'Unsupported file type']); + exit; +} + +$imgDir = realpath(__DIR__ . '/../img'); +if ($imgDir === false || !is_dir($imgDir)) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'img directory not found']); + exit; +} + +$path = $imgDir . DIRECTORY_SEPARATOR . $filename; +$realPath = realpath($path); + +if ($realPath === false || !is_file($realPath) || dirname($realPath) !== $imgDir) { + http_response_code(404); + echo json_encode(['ok' => false, 'error' => 'File not found']); + exit; +} + +if (!@unlink($realPath)) { + http_response_code(500); + echo json_encode(['ok' => false, 'error' => 'Failed to delete file (check permissions)']); + exit; +} + +echo json_encode(['ok' => true, 'filename' => $filename], JSON_UNESCAPED_UNICODE); diff --git a/css/galleriffic-2.css b/css/galleriffic-2.css index 9a9058b..53e4f47 100644 --- a/css/galleriffic-2.css +++ b/css/galleriffic-2.css @@ -275,6 +275,32 @@ ul.thumbs li { padding: 0; margin: 5px 10px 5px 0; list-style: none; + position: relative; +} +.thumb-delete { + position: absolute; + top: 0; + right: 0; + z-index: 10; + width: 18px; + height: 18px; + padding: 0; + border: none; + border-radius: 0; + background: rgba(128, 0, 200, 0.9); + color: #fff; + font-size: 14px; + font-weight: bold; + line-height: 16px; + text-align: center; + cursor: pointer; + opacity: 0.85; +} +.thumb-delete:hover, +.thumb-delete:focus { + opacity: 1; + background: rgba(100, 0, 160, 1); + outline: none; } a.thumb { padding: 2px; diff --git a/index.php b/index.php index 35ee73c..1797bc4 100644 --- a/index.php +++ b/index.php @@ -33,8 +33,7 @@ dreamgirl_require_login_page();
-
-

NCue

+ diff --git a/js/bestpic.js b/js/bestpic.js index 646fae2..96c4201 100644 --- a/js/bestpic.js +++ b/js/bestpic.js @@ -2,6 +2,95 @@ var refreshIntervalId = null; // Prefer same-origin relative paths so HTTPS doesn't break due to mixed-content / upgrade rules. var imgurl = "img/"; +function buildThumbLi(filename) { + var imgfile = imgurl + encodeURIComponent(filename); + return ( + "
  • " + + " " + + " " + + " " + filename.replace(/" + + " " + + "
    " + + "
  • " + ); +} + +function removeFromImageList(filename) { + if (!window.DREAMGIRL_IMAGES || !window.DREAMGIRL_IMAGES.length) return; + var idx = window.DREAMGIRL_IMAGES.indexOf(filename); + if (idx >= 0) window.DREAMGIRL_IMAGES.splice(idx, 1); +} + +function deleteImageFile(filename, onSuccess, onError) { + var fd = new FormData(); + fd.append('filename', filename); + + fetch('api/delete_image.php', { + method: 'POST', + body: fd, + credentials: 'same-origin' + }) + .then(function (r) { return r.json(); }) + .then(function (data) { + if (!data || !data.ok) { + throw new Error((data && data.error) ? data.error : '삭제 실패'); + } + if (typeof onSuccess === 'function') onSuccess(data); + }) + .catch(function (err) { + if (typeof onError === 'function') { + onError(err); + } else { + alert('에러: ' + (err && err.message ? err.message : '삭제 실패')); + } + }); +} + +function bindThumbDeleteHandler() { + $('#thumbs').off('click.thumbDelete', '.thumb-delete'); + $('#thumbs').on('click.thumbDelete', '.thumb-delete', function (e) { + e.preventDefault(); + e.stopPropagation(); + + var $li = $(this).closest('li'); + var filename = $li.attr('data-filename'); + if (!filename) return; + + if (!window.confirm('이 이미지를 삭제하시겠습니까?\n' + filename)) return; + + var $btn = $(this); + $btn.prop('disabled', true); + + deleteImageFile(filename, function () { + removeFromImageList(filename); + + if (window.dreamgirlGallery) { + var gallery = window.dreamgirlGallery; + var index = $li.index(); + var wasCurrent = gallery.currentImage && gallery.currentImage.index === index; + + if (typeof gallery.removeImageByIndex === 'function') { + gallery.removeImageByIndex(index); + } else { + $li.remove(); + } + + if (wasCurrent && gallery.data && gallery.data.length) { + var nextIndex = Math.min(index, gallery.data.length - 1); + if (typeof gallery.gotoIndex === 'function') { + gallery.gotoIndex(nextIndex); + } + } + } else { + $li.remove(); + } + }, function (err) { + $btn.prop('disabled', false); + alert('에러: ' + (err && err.message ? err.message : '삭제 실패')); + }); + }); +} + function getApiUrl() { // Use scheme-relative URL so it matches http/https of the current page. // NOTE: if the API does not support https, consider proxying it via Apache and pointing to a same-origin endpoint. @@ -87,17 +176,12 @@ function onLoad() { for (var i=0; i"; - text += " girl" + (i+1) + ""; - text += " "; - text += "
    "; - text += ""; + text += buildThumbLi(fname); } text += ""; $("#thumbs-list").html(text); initGallery(); + bindThumbDeleteHandler(); } function tryLoadFromLocalListApi() { diff --git a/js/uploader.js b/js/uploader.js index 564d8cd..31d439b 100644 --- a/js/uploader.js +++ b/js/uploader.js @@ -10,19 +10,6 @@ return (Math.round(n * 10) / 10) + ' ' + units[i]; } - function buildThumbLi(filename) { - var imgUrl = 'img/' + encodeURIComponent(filename); - var title = filename; - return ( - "
  • " + - " " + - " " + title + "" + - " " + - "
    " + - "
  • " - ); - } - function setStatus(text) { var el = qs('upload-status'); if (el) el.textContent = text || ''; @@ -112,12 +99,15 @@ // Add immediately to current gallery var filename = data.filename; - if (window.dreamgirlGallery && typeof window.dreamgirlGallery.appendImage === 'function') { - window.dreamgirlGallery.appendImage(buildThumbLi(filename)); - } else { + var thumbHtml = (typeof buildThumbLi === 'function') + ? buildThumbLi(filename) + : ''; + if (thumbHtml && window.dreamgirlGallery && typeof window.dreamgirlGallery.appendImage === 'function') { + window.dreamgirlGallery.appendImage(thumbHtml); + } else if (thumbHtml) { // fallback: append to DOM var ul = document.querySelector('#thumbs ul.thumbs'); - if (ul) ul.insertAdjacentHTML('beforeend', buildThumbLi(filename)); + if (ul) ul.insertAdjacentHTML('beforeend', thumbHtml); } // keep in-memory list (doesn't persist; list_images.php handles persistence)