채택완료

안내창 틀이 깨집니다.

https://shooter.dothome.co.kr/bbs/board.php?bo_table=ARCHIVE&wr_id=7 

1777265337_PmOvLxuqNq.webp

1777265367_X1ppSyfvwD.webp이렇게 나와야하는데 수정해도 틀이 틀어져요...

처음엔 잘나왔다가 소스를 조금 수정했더니 충돌나는건지 자꾸 창이 길게 나옵니다.

view.skin.php 소스
Copy
<?php
if (!defined("_GNUBOARD_")) exit; // 개별 페이지 접근 차단
include_once(G5_LIB_PATH.'/thumbnail.lib.php');

// 0. 강력한 브라우저 캐시 방지
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");

// 1. 세션 키 설정
$ss_down_key = 'ss_downloaded_' . $bo_table . '_' . $wr_id;
$ss_name = 'ss_view_password_' . $bo_table . '_' . $wr_id;

// 2. [개선] 실시간 DB 상태를 확인하여 차단 해제 (관리자 제외)
if (!$is_admin && get_session($ss_down_key)) {
    // 현재 게시물에서 'private' 처리된 파일이 아직 남아있는지 실시간 확인
    $sql = " select count(*) as cnt from {$g5['board_file_table']}
             where bo_table = '$bo_table'
             and wr_id = '$wr_id'
             and bf_content = 'private' ";
    $row = sql_fetch($sql);

    // 만약 관리자가 모든 사진을 다시 공개로 바꿨다면 (private인 파일이 0개라면)
    if ((int)$row['cnt'] === 0) {
        // 차단 세션을 제거하여 재접속 허용
        set_session($ss_down_key, '');
    } else {
        // 아직 비공개(다운로드 완료) 상태인 사진이 있다면 계속 차단
        echo "<script>alert('보안 정책상 이미 다운로드한 앨범에는 다시 접근할 수 없습니다.'); location.replace('".G5_URL."');</script>";
        exit;
    }
}

// --- [공통] 사진 상태 변경 및 다운로드 완료 세션 처리 ---
if (isset($_POST['action']) && $_POST['action'] == 'toggle_privacy') {
    $bf_no = (int)$_POST['bf_no'];
    $target_status = $_POST['status'];
    $val = ($target_status == 'private') ? 'private' : '';

    if (!$is_admin && $target_status == 'private') {
        set_session($ss_down_key, true);
    }

    sql_query(" update {$g5['board_file_table']} set bf_content = '$val' where bo_table = '$bo_table' and wr_id = '$wr_id' and bf_no = '$bf_no' ");
    exit;
}

// [관리자] 선택 삭제 처리
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, '');
    header("Location: " . G5_URL);
    exit;
}

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

if (isset($_POST['view_pwd'])) {
    if (!empty($view['wr_1']) && $_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; inset: 0; background: #f8f9fa; display: flex; align-items: center; justify-content: center; z-index: 10000; font-family: 'Pretendard', sans-serif; }
        .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; }
        .pwd-box h2 { margin-bottom: 10px; font-size: 18px; font-weight: 700; }
        .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; }
    </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="Password" required autofocus><button type="submit" class="pwd-submit">확인</button></form></div></div>
<?php return; }

// --- 파일 데이터 처리 ---
$all_valid_files = array();
if (isset($view['file']) && is_array($view['file'])) {
    foreach ($view['file'] as $i => $file) {
        if (isset($file['view']) && $file['view']) {
            $is_file_private = ($file['bf_content'] == 'private');
            if (!$is_admin && $is_file_private) continue;
            $file['orig_idx'] = $i;
            $file['is_private'] = $is_file_private;
            $all_valid_files[] = $file;
        }
    }
}
?>

<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>
/* 펌 방지 및 기본 폰트 */
body { -webkit-user-select: none !important; user-select: none !important; -webkit-touch-callout: none !important; margin: 0; font-family: 'Pretendard', sans-serif; background: #fff; }
img { -webkit-user-drag: none !important; pointer-events: none !important; }

/* 레이아웃 */
.view-top-bar { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid #eee; position: sticky; top: 0; background: #fff; z-index: 1001; }
.view-top-bar h1 { font-size: 18px; margin: 0; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 50%; }
.top-right-btns { display: flex; gap: 5px; align-items: center; }
.top-btn { font-size: 20px; color: #444; background: none; border: 0; cursor: pointer; padding: 8px; text-decoration: none; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: 0.2s; }
.top-btn:hover { background: #f5f5f5; }
.top-btn.active-mode { color: #2563eb; background: #f0f4ff; }

.gallery-container { padding: 15px; max-width: 1400px; margin: 0 auto; }
.masonry-grid { column-count: 4; column-gap: 12px; }
.grid-item { display: inline-block; width: 100%; margin-bottom: 12px; break-inside: avoid; position: relative; border-radius: 8px; overflow: hidden; background: #f0f0f0; }
@media (max-width: 768px) { .masonry-grid { column-count: 2 !important; } .view-top-bar h1 { font-size: 16px; } }

.img-wrapper { position: relative; border: 4px solid transparent; transition: 0.2s; }
.img-wrapper.selected { border-color: #2563eb; }
.img-wrapper img { width: 100%; display: block; height: auto; }
.click-layer { position: absolute; inset: 0; z-index: 10; cursor: pointer; }

/* 관리자 전용 스타일 */
.private-item { opacity: 0.5; filter: grayscale(1); }
.admin-status-badge { position: absolute; top: 8px; right: 8px; z-index: 20; background: rgba(255,255,255,0.95); width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; border: none; box-shadow: 0 2px 5px rgba(0,0,0,0.15); pointer-events: auto !important; }


/* 보안 모달 */
.security-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 20000; align-items: center; justify-content: center; padding: 20px; }
.security-modal.active { display: flex; }
.modal-content { background: #fff; width: 100%; max-width: 450px; border-radius: 20px; overflow: hidden; animation: modalShow 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
@keyframes modalShow { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
.modal-header { padding: 30px 20px 10px; text-align: center; }
.modal-header i { color: #2563eb; font-size: 32px; margin-bottom: 15px; display: block; }
.modal-header h3 { margin: 0; font-size: 20px; font-weight: 800; }
.modal-body { padding: 10px 25px; }
.check-list { list-style: none; padding: 15px 0; margin: 0; }
.check-item { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 15px; cursor: pointer; padding: 12px; border-radius: 10px; background: #f8fafc; border: 1px solid transparent; transition: 0.2s; }
.check-item.checked { border-color: #cbd5e1; background: #f1f5f9; }
.check-text { font-size: 14px; color: #475569; line-height: 1.4; word-break: keep-all; }
.check-box-ui { width: 20px; height: 20px; border: 2px solid #cbd5e1; border-radius: 5px; flex-shrink: 0; position: relative; background: #fff; transition: 0.2s; }
.check-item.checked .check-box-ui { background: #2563eb; border-color: #2563eb; }
.check-item.checked .check-box-ui::after { content: '✓'; color: #fff; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 12px; }
.modal-footer { padding: 10px 25px 30px; }
.btn-confirm-dw { width: 100%; padding: 18px; background: #e2e8f0; color: #94a3b8; border: none; border-radius: 12px; font-size: 16px; font-weight: 700; cursor: not-allowed; transition: 0.3s; }
.btn-confirm-dw.ready { background: #2563eb; color: #fff; cursor: pointer; box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2); }

.pswp__button--custom { background: none !important; color: #fff; font-size: 20px !important; line-height: 44px; text-align: center; width: 44px; float: right; opacity: 0.8; }
</style>

<script>
window.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('keydown', e => {
    if (e.ctrlKey && (e.keyCode === 67 || e.keyCode === 86 || e.keyCode === 85 || e.keyCode === 83 || e.keyCode === 73)) e.preventDefault();
    if (e.keyCode === 123) e.preventDefault();
});
</script>

<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">
            <h1><?php echo get_text($view['wr_subject']); ?></h1>
            <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-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" style="display:none; color:#ef4444;" title="선택삭제"><i class="fa-solid fa-trash-can"></i></button>
                <button type="button" 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 class="masonry-grid">
                <?php foreach ($all_valid_files as $idx => $file) {
                    preg_match('/src="([^"]+)"/', $file['view'], $match);
                    $img_url = $match[1];
                ?>
                <div class="grid-item <?php echo $file['is_private'] ? 'private-item' : ''; ?>">
                    <?php if ($is_admin) { ?>
                    <button type="button" class="admin-status-badge" onclick="adminTogglePrivacy(<?php echo $file['orig_idx']; ?>, '<?php echo $file['is_private'] ? 'public' : 'private'; ?>')" style="color:<?php echo $file['is_private'] ? '#ef4444' : '#2563eb'; ?>"><i class="fa-solid <?php echo $file['is_private'] ? 'fa-eye-slash' : 'fa-eye'; ?>"></i></button>
                    <?php } ?>
                    <div class="click-layer" onclick="handleAction(<?php echo $idx; ?>, this, event);"></div>
                    <div class="img-wrapper">
                        <img src="<?php echo $img_url; ?>" class="gallery-img" data-bfno="<?php echo $file['orig_idx']; ?>">
                        <input type="checkbox" name="chk_file_idx[]" value="<?php echo $file['orig_idx']; ?>" 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 } ?>
            </div>
        </div>
    </form>
</div>

<div id="dw_modal" class="security-modal">
    <div class="modal-content">
        <div class="modal-header"><i class="fa-solid fa-shield-halved"></i><h3>사진 다운로드 보안 동의</h3></div>
        <div class="modal-body">
            <div class="check-list">
                <div class="check-item" onclick="toggleCheck(this)"><div class="check-text">본 사진의 다운로드는 1회만 제공되며, 본 사진은 비공개 및 완료 즉시 앨범 재접속이 영구 차단됩니다.</div><div class="check-box-ui"></div></div>
                <div class="check-item" onclick="toggleCheck(this)"><div class="check-text">무단 배포 및 상업적 이용 금지에 동의하며, 위반 시 법적 책임을 질 수 있음을 확인합니다.</div><div class="check-box-ui"></div></div>
            </div>
        </div>
        <div class="modal-footer"><button type="button" id="btn_dw_start" class="btn-confirm-dw" onclick="executeDownload()">동의 및 다운로드</button></div>
    </div>
</div>

<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true" style="z-index:9999;">
    <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="닫기 (Esc)"></button>
                <button class="pswp__button pswp__button--fs" title="전체화면"></button>
                <button class="pswp__button pswp__button--zoom" title="확대/축소"></button>
                <button class="pswp__button pswp__button--custom" id="btn-pswp-dw" title="다운로드"><i class="fa-solid fa-download"></i></button>
                <button class="pswp__button pswp__button--custom" id="btn-pswp-play" title="슬라이드쇼"><i class="fa-solid fa-play"></i></button>
                <div class="pswp__preloader"><div class="pswp__preloader__icn"><div class="pswp__preloader__cut"><div class="pswp__preloader__donut"></div></div></div></div>
            </div>
            <button class="pswp__button pswp__button--arrow--left" title="이전"></button>
            <button class="pswp__button pswp__button--arrow--right" title="다음"></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 src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>

<script>
let gallery = null;
let playTimer = null;
let isSelectMode = false;
const isAdmin = <?php echo $is_admin ? 'true' : 'false'; ?>;

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

function deleteSelected() {
    const checked = document.querySelectorAll('.file-chk:checked').length;
    if (checked === 0) return alert("삭제할 사진을 선택하세요.");
    if (confirm("선택한 " + checked + "장의 사진을 삭제하시겠습니까?")) document.getElementById('fview').submit();
}

function toggleSelectAll() {
    if(!isSelectMode) toggleSelectMode();
    const all = document.querySelectorAll('.img-wrapper');
    const isNowAll = document.querySelectorAll('.img-wrapper.selected').length === all.length;
    all.forEach(el => {
        el.classList.toggle('selected', !isNowAll);
        el.querySelectorAll('input[type="checkbox"]').forEach(chk => chk.checked = !isNowAll);
    });
}

function handleAction(index, layer, e) {
    if (isAdmin && isSelectMode) {
        const wrapper = layer.nextElementSibling;
        const chk = wrapper.querySelector('.file-chk');
        chk.checked = !chk.checked;
        wrapper.classList.toggle('selected', chk.checked);
    } else {
        openPhotoSwipe(index);
    }
}

async function adminTogglePrivacy(bf_no, status) {
    if(!confirm("사진의 공개 상태를 변경하시겠습니까?")) return;
    const formData = new FormData();
    formData.append('action', 'toggle_privacy'); formData.append('bf_no', bf_no); formData.append('status', status);
    await fetch(window.location.href, { method: 'POST', body: formData });
    location.reload();
}

function toggleCheck(el) {
    el.classList.toggle('checked');
    const allChecked = document.querySelectorAll('.check-item.checked').length === 2;
    document.getElementById('btn_dw_start').classList.toggle('ready', allChecked);
}

function openPhotoSwipe(index) {
    const pswpElement = document.querySelectorAll('.pswp')[0];
    const items = [];

    // 이미지 사이즈 동적 계산 로직 추가
    document.querySelectorAll('.gallery-img').forEach(img => {
        // 자연 크기(naturalWidth)가 없으면 화면 비율에 맞는 기본값 설정
        const w = img.naturalWidth || 1200;
        const h = img.naturalHeight || 1600;
        items.push({
            src: img.src,
            w: w,
            h: h,
            bf_no: img.getAttribute('data-bfno'),
            msrc: img.src
        });
    });

    const options = {
        index: index,
        history: false,
        bgOpacity: 0.95,
        showHideOpacity: true,
        shareEl: false,
        fullscreenEl: true,
        zoomEl: true,
        // 사진이 화면에 꽉 차게 나오도록 스케일 모드 조정
        getThumbBoundsFn: function(index) {
            const thumbnail = document.querySelectorAll('.gallery-img')[index];
            const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
            const rect = thumbnail.getBoundingClientRect();
            return {x:rect.left, y:rect.top + pageYScroll, w:rect.width};
        }
    };

    gallery = new PhotoSwipe(pswpElement, PhotoSwipeUI_Default, items, options);

    // 다운로드 버튼 이벤트
    document.getElementById('btn-pswp-dw').onclick = (e) => {
        e.stopPropagation();
        if (isAdmin) {
            const a = document.createElement('a');
            a.href = gallery.currItem.src;
            a.download = 'admin_download.jpg';
            a.click();
        } else {
            document.getElementById('dw_modal').classList.add('active');
        }
    };

    // 슬라이드쇼 재생 이벤트
    document.getElementById('btn-pswp-play').onclick = function(e) {
        e.stopPropagation();
        const icon = this.querySelector('i');
        if (!playTimer) {
            playTimer = setInterval(() => { gallery.next(); }, 3500);
            icon.className = 'fa-solid fa-pause';
        } else {
            clearInterval(playTimer); playTimer = null;
            icon.className = 'fa-solid fa-play';
        }
    };

    gallery.listen('close', () => { if(playTimer) { clearInterval(playTimer); playTimer = null; } });
    gallery.init();
}

async function executeDownload() {
    if (!document.getElementById('btn_dw_start').classList.contains('ready')) return;
    const bf_no = gallery.currItem.bf_no;
    const url = gallery.currItem.src;

    const formData = new FormData();
    formData.append('action', 'toggle_privacy');
    formData.append('bf_no', bf_no);
    formData.append('status', 'private');

    await fetch(window.location.href, { method: 'POST', body: formData });

    const a = document.createElement('a');
    a.href = url; a.download = 'shooter_photo.jpg'; a.click();

    alert("다운로드가 완료되었습니다.\n보안 정책에 따라 앨범 접근이 차단됩니다.");
    location.replace("<?php echo G5_URL; ?>");
}

async function downloadZipBrowser() {
    const selected = document.querySelectorAll('.url-chk:checked');
    if (selected.length === 0) return alert("압축할 사진을 선택해주세요.");

    const zip = new JSZip();
    const total = selected.length;

    for (let i = 0; i < total; i++) {
        try {
            const resp = await fetch(selected[i].value);
            const blob = await resp.blob();
            zip.file(`photo_${i+1}.jpg`, blob);
        } catch (e) {
            console.error("파일 로드 실패:", selected[i].value);
        }
    }

    zip.generateAsync({type:"blob"}).then(c => {
        saveAs(c, "album_archive.zip");
    });
}
</script>

 
|

답변 1개 / 댓글 4개

채택된 답변
+20 포인트
height 을 주되지 안을까요?

답변에 대한 댓글 4개

원래 height auto로 줬었는데 갑자기 이상해졌어요..
가로도 아니고 새로인데 그냥 정해도 상관없다 생각 되네요
max-height 설정 및 .modal. header ,i , h3 , body 전체수정해서 처음부터 다시 수정했더니 잡혔네요...
.modal-header { padding: 30px 20px 10px; text-align: center; }
.modal-header i { color: #2563eb; font-size: 32px; margin-bottom: 15px; display: block; }
.modal-header h3 { margin: 0; font-size: 20px; font-weight: 800; }
.modal-body { padding: 10px 25px; }

수정후
.modal-header { display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; padding: 40px 20px 20px !important; height: auto !important; position: relative !important; }
.modal-header i { position: static !important; display: block !important; font-size: 45px !important; color: #2563eb; margin-bottom: 15px !important; }
.modal-header h3 { position: static !important; display: block !important; margin: 0 !important; font-size: 22px !important; font-weight: 800; color: #000; line-height: 1.2 !important; }
.modal-body { position: static !important; padding: 10px 25px !important; }
파이팅

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