미채택 완료

업로드할때 추가로 사진을 업로드하면 기존에 사진이 지워지는 현상이 나타나요.

업로드할때 추가로 사진을 업로드하면 기존에 사진이 지워지고 추가한 파일만 업로드 됩니다.
최대 20개밖에 추가가 안돼고있어요

1774749301_cO8msBiqWb.webp
view.skin.php 코드
Copy
<?php
if (!defined("_GNUBOARD_")) exit;
include_once(G5_LIB_PATH.'/thumbnail.lib.php');

// 1. 비밀번호 및 세션 관리
$ss_name = 'ss_view_password_' . $bo_table . '_' . $wr_id;

if ($is_admin && isset($_POST['btn_delete_selected'])) {
    $del_files = $_POST['chk_file_idx'];
    if (is_array($del_files)) {
        foreach ($del_files as $f_idx) {
            $f_idx = (int)$f_idx;
            sql_query(" delete from {$g5['board_file_table']} where bo_table = '$bo_table' and wr_id = '$wr_id' and bf_no = '$f_idx' ");
        }
    }
    goto_url("./board.php?bo_table=$bo_table&wr_id=$wr_id");
}

if (isset($_GET['unlock']) && $_GET['unlock'] == 'y') {
    set_session($ss_name, '');
    goto_url(get_pretty_url($bo_table));
}

$has_password = !empty($view['wr_1']);
$is_unlocked = ($is_admin || get_session($ss_name) || !$has_password) ? true : false;

if (isset($_POST['view_pwd'])) {
    if ($has_password && $_POST['view_pwd'] === $view['wr_1']) {
        set_session($ss_name, true);
        $is_unlocked = true;
    } else {
        echo "<script>alert('비밀번호가 일치하지 않습니다.');</script>";
    }
}

if (!$is_unlocked) {
?>
    <style>
        .pwd-lock-screen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #f8f9fa; display: flex; align-items: center; justify-content: center; z-index: 10000; }
        .pwd-box { background: #fff; padding: 50px 30px; border-radius: 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.08); text-align: center; width: 90%; max-width: 400px; font-family: 'Pretendard', sans-serif; }
        .pwd-box h2 { margin: 0 0 10px 0; font-size: 18px; font-weight: 700; color: #333; line-height: 1.4; word-break: keep-all; }
        .pwd-input { width: 100%; padding: 18px; border: 1px solid #e2e8f0; border-radius: 12px; margin-bottom: 15px; text-align: center; font-size: 16px; outline: none; box-sizing: border-box; }
        .pwd-submit { width: 100%; padding: 18px; background: #000; color: #fff; border: none; border-radius: 12px; cursor: pointer; font-size: 17px; font-weight: 600; }
        .back-link { display: inline-block; margin-top: 25px; color: #888; text-decoration: none; font-size: 14px; border-bottom: 1px solid #ddd; }
    </style>
    <div class="pwd-lock-screen">
        <div class="pwd-box">
            <h2><?php echo get_text($view['wr_subject']); ?></h2>
            <p>비밀번호를 입력해 주세요.</p>
            <form method="post" autocomplete="off">
                <input type="password" name="view_pwd" class="pwd-input" placeholder="비밀번호" required autofocus>
                <button type="submit" class="pwd-submit">확인</button>
            </form>
            <a href="<?php echo $list_href; ?>" class="back-link">목록으로 돌아가기</a>
        </div>
    </div>
<?php
    return;
}
$files = (isset($view['file']) && is_array($view['file'])) ? $view['file'] : array();
?>

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/default-skin/default-skin.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">

<style>
/* 1. 펌 방지 CSS (모바일 롱탭 메뉴 차단 포함) */
body {
    -webkit-user-select: none !important;
    -moz-user-select: none !important;
    -ms-user-select: none !important;
    user-select: none !important;
    -webkit-touch-callout: none !important; /* 모바일 롱탭 방지 */
    margin: 0; padding: 0; background: #fff;
}
img {
    -webkit-user-drag: none !important;
    pointer-events: auto;
}
.pswp__img {
    -webkit-user-select: none !important;
    -webkit-touch-callout: none !important;
    -webkit-user-drag: none !important;
}

/* 2. 상단 바 및 꽉 찬 갤러리 디자인 */
#res_view_skin { font-family: 'Pretendard', sans-serif; width: 100%; }
.view-top-bar {
    display: flex; justify-content: space-between; align-items: center;
    padding: 15px 20px; border-bottom: 1px solid #f2f2f2;
    position: sticky; top: 0; background: rgba(255,255,255,0.95); z-index: 1001;
    backdrop-filter: blur(10px);
}
.top-left h1 { font-size: 18px; margin: 0; font-weight: 600; color: #111; }
.top-right-btns { display: flex; align-items: center; gap: 8px; }
.top-btn { font-size: 20px; color: #333; background: none; border: 0; cursor: pointer; padding: 5px; display: flex; align-items: center; text-decoration: none; }
.top-btn.active-mode { color: #2563eb; }
.btn-delete-multi { color: #ef4444 !important; display: none; }

.gallery-container { padding: 4px; max-width: 100%; }
.masonry-grid { column-count: 4; column-gap: 4px; width: 100%; }

/* 해상도별 배치 */
@media (min-width: 1600px) { .masonry-grid { column-count: 5; } }
@media (max-width: 1024px) { .masonry-grid { column-count: 3; } }
@media (max-width: 768px) {
    .masonry-grid { column-count: 2; column-gap: 3px; }
    .gallery-container { padding: 3px; }
}

.grid-item { display: inline-block; width: 100%; margin-bottom: 4px; break-inside: avoid; position: relative; }
.img-wrapper { position: relative; border: 0px solid transparent; transition: all 0.2s; }
.img-wrapper img { width: 100% !important; height: auto !important; display: block; }
.img-wrapper.selected { outline: 4px solid #2563eb; outline-offset: -4px; z-index: 5; }
.click-layer { position: absolute; inset: 0; z-index: 10; cursor: pointer; }

/* PhotoSwipe 커스텀 */
.pswp__button--play, .pswp__button--download-custom { background: none !important; color: #fff; font-size: 18px; line-height: 44px; text-align: center; width: 44px; float: right; cursor: pointer; }
</style>

<div id="res_view_skin">
    <form name="fview" id="fview" method="post">
        <input type="hidden" name="btn_delete_selected" value="1">
        <div class="view-top-bar">
            <div class="top-left"><h1><?php echo get_text($view['wr_subject']); ?></h1></div>
            <div class="top-right-btns">
                <?php if ($is_admin) { ?>
                <a href="<?php echo G5_BBS_URL; ?>/write.php?w=u&bo_table=<?php echo $bo_table; ?>&wr_id=<?php echo $wr_id; ?>" class="top-btn" title="사진 추가"><i class="fa-solid fa-square-plus"></i></a>
                <button type="button" id="toggle-select-btn" onclick="toggleSelectMode();" class="top-btn" title="선택모드"><i class="fa-solid fa-square-check"></i></button>
                <button type="button" id="delete-selected-btn" onclick="deleteSelected();" class="top-btn btn-delete-multi" title="선택삭제"><i class="fa-solid fa-trash-can"></i></button>
                <button type="button" id="all-select-btn" onclick="toggleSelectAll();" class="top-btn" title="전체선택"><i class="fa-solid fa-list-check"></i></button>
                <button type="button" onclick="downloadZipBrowser();" class="top-btn" title="압축다운로드"><i class="fa-solid fa-file-zipper" style="color:#2563eb;"></i></button>
                <?php } ?>
                <a href="?bo_table=<?php echo $bo_table; ?>&wr_id=<?php echo $wr_id; ?>&unlock=y" class="top-btn" title="잠금 후 나가기"><i class="fa-solid fa-right-from-bracket"></i></a>
            </div>
        </div>

        <div class="gallery-container">
            <div id="gallery" class="masonry-grid">
                <?php
                $idx = 0;
                foreach ($files as $i => $file) {
                    if (isset($file['view']) && $file['view']) {
                        preg_match('/src="([^"]+)"/', $file['view'], $match);
                        $img_url = $match[1];
                ?>
                <div class="grid-item">
                    <div class="click-layer" onclick="handleAction(<?php echo $idx; ?>, this);"></div>
                    <div class="img-wrapper">
                        <img src="<?php echo $img_url; ?>" class="gallery-img" loading="lazy">
                        <input type="checkbox" name="chk_file_idx[]" value="<?php echo $i; ?>" class="file-chk" style="display:none;">
                        <input type="checkbox" name="chk_file_url[]" value="<?php echo $img_url; ?>" class="url-chk" style="display:none;">
                    </div>
                </div>
                <?php $idx++; } } ?>
            </div>
        </div>
    </form>
</div>

<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
    <div class="pswp__bg"></div>
    <div class="pswp__scroll-wrap">
        <div class="pswp__container"><div class="pswp__item"></div><div class="pswp__item"></div><div class="pswp__item"></div></div>
        <div class="pswp__ui pswp__ui--hidden">
            <div class="pswp__top-bar">
                <div class="pswp__counter"></div>
                <button class="pswp__button pswp__button--close" title="Close"></button>
                <button class="pswp__button pswp__button--download-custom" id="pswp-down-btn"><i class="fa-solid fa-download"></i></button>
                <button class="pswp__button pswp__button--play" id="pswp-play-btn"><i class="fa-solid fa-play"></i></button>
                <button class="pswp__button pswp__button--fs" title="Fullscreen"></button>
                <button class="pswp__button pswp__button--zoom" title="Zoom"></button>
                <div class="pswp__preloader"><div class="pswp__preloader__icn"><div class="psloader__cut"><div class="pswp__preloader__donut"></div></div></div></div>
            </div>
            <button class="pswp__button pswp__button--arrow--left"></button>
            <button class="pswp__button pswp__button--arrow--right"></button>
            <div class="pswp__caption"><div class="pswp__caption__center"></div></div>
        </div>
    </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/photoswipe/4.1.3/photoswipe-ui-default.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>

<script>
const isAdmin = <?php echo $is_admin ? 'true' : 'false'; ?>;
const DOWNLOAD_KEY = 'downloaded_<?php echo $bo_table."_".$wr_id; ?>';
let isSelectMode = false;
let gallery, playTimer = null;

// --- 핵심: 펌 방지 스크립트 추가 ---
(function() {
    if (isAdmin) return; // 관리자는 제한 해제

    // 1. 우클릭 방지
    document.addEventListener('contextmenu', function(e) { e.preventDefault(); }, false);

    // 2. 드래그 및 선택 방지
    document.addEventListener('selectstart', function(e) { e.preventDefault(); }, false);
    document.addEventListener('dragstart', function(e) { if (e.target.tagName === 'IMG') e.preventDefault(); }, false);

    // 3. 개발자 도구 및 소스보기 단축키 방지
    document.addEventListener('keydown', function(e) {
        if (
            e.keyCode === 123 || // F12
            (e.ctrlKey && e.shiftKey && e.keyCode === 73) || // Ctrl+Shift+I
            (e.ctrlKey && e.keyCode === 85) || // Ctrl+U
            (e.ctrlKey && e.keyCode === 83) || // Ctrl+S
            (e.ctrlKey && e.keyCode === 80)    // Ctrl+P
        ) {
            e.preventDefault();
            return false;
        }
    });
})();

function toggleSelectMode() {
    isSelectMode = !isSelectMode;
    document.getElementById('toggle-select-btn').classList.toggle('active-mode');
    document.getElementById('delete-selected-btn').style.display = isSelectMode ? 'inline-block' : 'none';
}

function handleAction(index, el) {
    if (isSelectMode) {
        const wrapper = el.nextElementSibling;
        const chks = wrapper.querySelectorAll('input[type="checkbox"]');
        chks.forEach(c => c.checked = !c.checked);
        wrapper.classList.toggle('selected', chks[0].checked);
    } else {
        openPhotoSwipe(index);
    }
}

function openPhotoSwipe(index) {
    const pswpElement = document.querySelectorAll('.pswp')[0];
    const items = [];
    document.querySelectorAll('.gallery-img').forEach(img => {
        items.push({ src: img.src, w: img.naturalWidth || 1200, h: img.naturalHeight || 1600 });
    });

    gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, { index: index, bgOpacity: 0.95 });

    document.getElementById('pswp-down-btn').onclick = () => {
        if (!isAdmin && localStorage.getItem(DOWNLOAD_KEY)) {
            alert("이미 다운로드하셨습니다. (게시물당 1회 제한)"); return;
        }
        if (!confirm("이미지를 다운로드하시겠습니까?")) return;
        const a = document.createElement('a');
        a.href = gallery.currItem.src;
        a.download = 'img_' + Date.now() + '.jpg';
        a.click();
        if (!isAdmin) localStorage.setItem(DOWNLOAD_KEY, "true");
    };

    document.getElementById('pswp-play-btn').onclick = function() {
        if (!playTimer) {
            playTimer = setInterval(() => { gallery.next(); }, 3000);
            this.innerHTML = '<i class="fa-solid fa-pause"></i>';
        } else {
            clearInterval(playTimer); playTimer = null;
            this.innerHTML = '<i class="fa-solid fa-play"></i>';
        }
    };
    gallery.listen('close', () => { clearInterval(playTimer); playTimer = null; });
    gallery.init();
}

function deleteSelected() { if(confirm("삭제하시겠습니까?")) document.getElementById('fview').submit(); }

function toggleSelectAll() {
    if(!isSelectMode) toggleSelectMode();
    const wrappers = document.querySelectorAll('.img-wrapper');
    const state = !wrappers[0].querySelector('.file-chk').checked;
    wrappers.forEach(w => {
        w.querySelectorAll('input[type="checkbox"]').forEach(c => c.checked = state);
        w.classList.toggle('selected', state);
    });
}

async function downloadZipBrowser() {
    if (!isAdmin) return;
    const urls = Array.from(document.querySelectorAll('.url-chk:checked')).map(el => el.value);
    if(!urls.length) return alert('이미지를 선택해주세요.');
    const zip = new JSZip();
    for(let i=0; i<urls.length; i++) {
        const res = await fetch(urls[i]);
        const blob = await res.blob();
        zip.file(`image_${i+1}.jpg`, blob);
    }
    zip.generateAsync({type:"blob"}).then(c => {
        const a = document.createElement('a');
        a.href = URL.createObjectURL(c);
        a.download = "archive.zip";
        a.click();
    });
}
</script>


write.skin.php 코드
Copy
<?php
if (!defined('_GNUBOARD_')) exit;

// 1. 관리자만 접근 가능하도록 제한
if (!$is_admin) {
    alert('관리자만 작성할 수 있습니다.', G5_URL);
}

// 수정 모드일 때 기존 비밀번호 설정 여부 확인
$is_pw_set = (isset($write['wr_1']) && !empty($write['wr_1'])) ? true : false;
?>
<link rel="stylesheet" href="<?php echo $board_skin_url; ?>/style.css">

<section id="bo_w">
    <form name="fwrite" id="fwrite" action="<?php echo $action_url ?>" onsubmit="return fwrite_submit(this);" method="post" enctype="multipart/form-data" autocomplete="off">
        <input type="hidden" name="w" value="<?php echo $w ?>">
        <input type="hidden" name="bo_table" value="<?php echo $bo_table ?>">
        <input type="hidden" name="wr_id" value="<?php echo $wr_id ?>">
        <input type="hidden" name="wr_content" id="wr_content" value="Gallery Images">

        <?php if ($is_category) { ?>
        <div class="write_div">
            <select name="ca_name" id="ca_name" required class="frm_input full_width category_select">
                <option value="">분류를 선택하세요</option>
                <?php echo $category_option ?>
            </select>
        </div>
        <?php } ?>

        <div class="write_div">
            <input type="text" name="wr_subject" value="<?php echo $subject ?>" id="wr_subject" required class="frm_input full_width" placeholder="앨범 제목을 입력하세요">
        </div>

        <div class="write_div pass_setup">
            <label for="set_pw_check" class="pw_label">
                <input type="checkbox" id="set_pw_check" onclick="togglePassword(this)" <?php echo $is_pw_set ? 'checked' : ''; ?>> 비밀번호를 사용하시겠습니까?
            </label>
            <div id="pw_input_area" style="display:<?php echo $is_pw_set ? 'block' : 'none'; ?>; margin-top:10px;">
                <input type="password" name="wr_1" value="<?php echo isset($write['wr_1']) ? $write['wr_1'] : ''; ?>" id="wr_1" class="frm_input full_width" placeholder="사용할 비밀번호 입력">
            </div>
        </div>

        <div class="write_div">
            <div class="file_upload_wrapper" onclick="document.getElementById('bf_file_multi').click();">
                <div class="upload_msg">
                    <i class="fa fa-camera fa-3x" style="color:#eee; margin-bottom:10px;"></i><br>
                    클릭하여 사진들을 한꺼번에 선택하세요.<br>
                    <span id="file_status" style="font-size:12px; color:#999;">(여러 장 동시 선택 가능)</span>
                </div>
                <input type="file" name="bf_file[]" id="bf_file_multi" multiple style="display:none;" accept="image/*">
            </div>
        </div>

        <div class="btn_confirm">
            <a href="./board.php?bo_table=<?php echo $bo_table ?>" class="btn_cancel">취소</a>
            <input type="submit" value="작성 완료" id="btn_submit" class="btn_submit">
        </div>
    </form>
</section>

<script>
function togglePassword(chk) {
    const area = document.getElementById('pw_input_area');
    const input = document.getElementById('wr_1');
    if (chk.checked) {
        area.style.display = 'block';
        input.focus();
    } else {
        area.style.display = 'none';
        input.value = ''; // 체크 해제 시 입력값 초기화 (일반 게시물로 전환)
    }
}

document.getElementById('bf_file_multi').onchange = function() {
    const files = this.files;
    if(files.length > 0) {
        document.getElementById('file_status').innerHTML = "<strong>" + files.length + "</strong>장의 사진이 선택되었습니다.";
        document.getElementById('file_status').style.color = "#2563eb";
    }
};

function fwrite_submit(f) {
    if (f.wr_subject.value.trim() === "") {
        alert("제목을 입력하세요.");
        f.wr_subject.focus();
        return false;
    }

    const pwCheck = document.getElementById('set_pw_check');
    if (pwCheck.checked && f.wr_1.value.trim() === "") {
        alert("비밀번호를 입력해주세요.");
        f.wr_1.focus();
        return false;
    }
    return true;
}
</script>
<style>
/* CSS 스타일은 제공해주신 원본과 동일하게 유지됩니다 */
#bo_w { max-width: 600px; margin: 60px auto; padding: 20px; font-family: 'Pretendard', sans-serif; }
.full_width { width: 100%; padding: 15px; border: 1px solid #ddd; font-size: 16px; margin-bottom: 12px; box-sizing: border-box; border-radius: 8px; outline: none; }
.category_select { height: 52px !important; padding: 0 15px !important; line-height: normal !important; background-color: #fcfcfc; border: 1px solid #2563eb; appearance: auto; }
.pass_setup { margin-bottom: 12px; padding: 10px; border: 1px solid #eee; border-radius: 8px; background: #fafafa; }
.pw_label { cursor: pointer; font-size: 14px; color: #555; display: flex; align-items: center; gap: 8px; }
.file_upload_wrapper { border: 2px dashed #ddd; background: #f9f9f9; padding: 40px 20px; text-align: center; cursor: pointer; border-radius: 12px; }
.btn_confirm { margin-top: 40px; display: flex; gap: 10px; }
.btn_submit { flex: 2; background: #2563eb; color: #fff; border: 0; padding: 15px; font-size: 16px; font-weight: bold; border-radius: 8px; cursor: pointer; }
.btn_cancel { flex: 1; background: #eee; color: #666; padding: 15px; text-decoration: none; border-radius: 8px; text-align: center; }
</style>

답변 2개 / 댓글 4개

2026-03-29 (일) 12:03:11
안녕하세요.
저는 그누보드5 소스를 안본지 몇년이 지났는데요.
원본소스에서 업로드시 기존 파일을 삭제하는 부분이 있을 것 같은데요.
원본소스는 건드리지 않는 것이 좋습니다.

원본 소스를 수정하지 않으려면,

스킨폴더에
write_update.head.skin.php

이 파일에 업로드 파일이 존재하면, 
기존파일을 게시글 번호와 함께 복사하는 구문이 필요할 것으로 같습니다.

답변에 대한 댓글 1개

댓글 달아주셔서 감사합니다. 소스에서 해결을 아무리 해볼려고 해봐도 안되어서 sql 까지 건드려 해결했습니다.
write.skin.php 

상단 추가

$file_count = 0;
if ($w == 'u') {
    $file_count = (int)$write['wr_file']; 
}

기존 bf_file[] 이름을 상황에 따라 바꾸도록 수정합니다.

<div class="write_div">
    <div class="file_upload_wrapper" onclick="document.getElementById('bf_file_multi').click();">
        <div class="upload_msg">
            <i class="fa fa-camera fa-3x" style="color:#eee; margin-bottom:10px;"></i><br>
            클릭하여 사진들을 추가하세요.<br>
            <span id="file_status" style="font-size:12px; color:#999;">
                <?php if($w == 'u') echo "현재 " . $file_count . "장의 사진이 저장되어 있습니다."; ?>
            </span>
        </div>
        <!-- name 부분을 수정 모드일 때 인덱스를 지정하여 덮어쓰기 방지 -->
        <input type="file" name="bf_file[<?php echo $file_count; ?>]" id="bf_file_multi" multiple style="display:none;" accept="image/*">
    </div>
</div>

파일을 선택했을 때 name 속성을 한 번 더 확실히 잡아주면 좋습니다.

document.getElementById('bf_file_multi').onchange = function() {
    const files = this.files;
    if(files.length > 0) {
        // 기존 파일 개수가 있으면 그 다음 번호부터 이름 부여 (브라우저에 따라 다를 수 있음)
        // 기본적으로 위 HTML의 name="bf_file[<?php echo $file_count; ?>]" 만으로도 
        // 그누보드는 해당 번호 이후부터 새 파일로 인식합니다.
        
        document.getElementById('file_status').innerHTML = "<strong>" + files.length + "</strong>장의 사진이 새로 선택되었습니다.";
        document.getElementById('file_status').style.color = "#2563eb";
    }
};

혹 모르니 설정(갯수, 용량) 부분도 확인해 보시기 바랍니다. 
테스트 해본 코드가 아니니 참고 정도만 해 주세요.

질문을 수정/추가 하셨네요. 혹 모르니 아래 내용도 참고해 보시기 바랍니다. 

<?php
if (!defined('_GNUBOARD_')) exit;

// 기존 파일 중 가장 높은 번호 찾기 (수정 모드일 때)
$next_idx = 0;
if ($w == 'u') {
    $row = sql_fetch(" select max(bf_no) as max_no from {$g5['board_file_table']} where bo_table = '$bo_table' and wr_id = '$wr_id' ");
    if (isset($row['max_no'])) {
        $next_idx = (int)$row['max_no'] + 1;
    }
}
?>

<!-- name="bf_file[]" 대신 아래처럼 인덱스를 지정합니다 -->
<input type="file" name="bf_file[<?php echo $next_idx; ?>]" id="bf_file_multi" multiple style="display:none;" accept="image/*">


관리자 접속 게시판 관리 > 수정 > 업로드 갯수 수정
파일 업로드 용량도 확인 ....

답변에 대한 댓글 3개

댓글 달아주셔서 감사합니다. 소스에서 해결을 아무리 해볼려고 해봐도 안되어서 sql 까지 건드려 해결했습니다.
그래도 해결 하셨다니 다행 입니다. 좋은 오후 보내세요.
참고로 php.ini 파일에 max_file_uploads = 20 지정 되어있어서 그럴수도 있어요

답변을 작성하려면 로그인이 필요합니다.