https://moonhouse.co.kr/xetip/588063
위의 글에서 적용된 폴딩 기능에서 좀 더 발전한 옵션을 주는 벙법입니다.
<div style="float:left; border-top:1px solid #ddd; width:100%">
<!--@if($mi->default_style=='list')--><!--@else--><table class="bd_lst bd_tb_lst bd_tb common_notice"><!--@end-->
<tr loop="$notice_list=>$no,$document" class="notice">
<td $list_config['no']><span class="sbtn_mini sbtn-orange">{$lang->notice}</span></td>
<td scope="col" class="m_no" style="color:{$category_list[$document->get('category_srl')]->color}"|cond="$category_list[$document->get('category_srl')]->color!='transparent'"><span>{$category_list[$document->get('category_srl')]->title}</span></td>
<td class="title">
<!--// 공지사항 일반 출력 -->
<a cond="$mi->notice_view_slide ==''" href="{getUrl('document_srl',$document->document_srl, 'listStyle', $listStyle, 'cpage','')}">
<strong><include cond="$mi->notice_view_slide ==''" target="title.html" /></strong>
</a>
<!--// 공지사항 슬라이드 출력 -->
<details cond="!$mi->notice_view_slide ==''" class="accordion">
<summary class="notice_fold">
<strong><include target="title.html" /></strong>
</summary>
<div class="slide-content">
<p style="margin-top:10px">
<span>{$document->getContent(false)}</span>
<div style="text-align:right; float:left; padding-bottom:5px; width:100%">
<div style="float:right">
<a href="{getUrl('document_srl',$document->document_srl, 'listStyle', $listStyle, 'cpage','')}" class="btn"><span>공지글 보기</span></a>
<!--@if($grant->write_comment && $oDocument->isEnableComment() )-->
<a href="{getUrl('', 'mid', $mid, 'act','dispBoardWriteComment','document_srl',$document->document_srl)}" class="btn"><span>{$lang->cmd_comment_registration}</span></a>
<!--@end-->
<!--@if($document->isEditable())-->
<!--@if(!( !$grant->manager && $document->getCommentCount()))-->
<a href="{getUrl('', 'mid', $mid, 'act','dispBoardWrite','document_srl',$document->document_srl,'comment_srl','')}" class="btn"><span>{$lang->cmd_modify}</span></a>
<!--@end-->
<!--@if(!( !$grant->manager && $oDocument->getCommentCount()))-->
<a href="{getUrl('', 'mid', $mid, 'act','dispBoardDelete','document_srl',$document->document_srl,'comment_srl','')}" class="btn"><span>{$lang->cmd_delete}</span></a>
<!--@end-->
<!--@end-->
</div>
</div>
</p>
<!-- 내용글 하단 감지용 마커 추가 -->
<div class="scroll-end-marker"></div>
</div>
<style>
summary.notice_fold::-webkit-details-marker { display: none; }
summary.notice_fold { display: block; }
summary.notice_fold { position: relative; padding-left: 1.2em; cursor: pointer; }
summary.notice_fold::before { content: ""; position: absolute; border-top: 0.5em solid transparent; border-bottom: 0.5em solid transparent; border-left: 0.75em solid yellowgreen; top: 0.2em; left: 0; transition: .25s transform; margin-top:2px; }
details[open] summary:before { transform: rotateZ(90deg); }
details[open] > summary::-webkit-details-marker { transform:rotate3d(0, 0, 1, 180deg);}
.slide-content { max-height:0; overflow:hidden; transition:max-height 0.5s ease-out, opacity 0.5s ease-out; opacity:0; }
details[open] .slide-content { opacity:1; }
.scroll-end-marker { height:1px; width:100%; }
</style>
</details>
</td>
<td cond="$list_config['nick_name']"><a href="#popup_menu_area" class="member_{$document->get('member_srl')}" onclick="return false">{$document->getNickName()}</a></td>
<td cond="$list_config['regdate']">{$document->getRegdate('Y.m.d')}</td>
<td cond="$list_config['last_update']">{getTimeGap($document->get('last_update'), "y.m.d")}</td>
<td cond="$list_config['readed_count'] && $val->idx==-1">{$document->get('readed_count')>0?$document->get('readed_count'):'0'}</td>
<td cond="$grant->manager" class="check m_no"><input type="checkbox" name="cart" value="{$document->document_srl}" class="iCheck" title="Check This Article" onclick="doAddDocumentCart(this)" checked="checked"|cond="$document->isCarted()" /></td>
</tr>
<!--@if($mi->default_style=='list')--><!--@else--></table><!--@end-->
</div>
<script cond="!$mi->notice_view_slide ==''" >
document.addEventListener('DOMContentLoaded', function() {
const detailsElements = document.querySelectorAll('details.accordion');
const closeTimers = new Map(); // 각 details 요소별로 타이머 관리
// Intersection Observer 설정 (긴 글용)
const observerOptions = {
root: null, // 뷰포트를 기준으로 관찰
rootMargin: '0px',
threshold: 0.1 // 마커가 뷰포트의 10% 이상 보일 때 감지
};
const scrollEndObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
const details = entry.target.closest('details.accordion');
if (!details || !details.open) return;
const slideContent = details.querySelector('.slide-content');
// "긴 글"로 판단된 경우에만 Intersection Observer 로직 적용
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (slideContent && slideContent.scrollHeight > viewportHeight * 0.8) {
if (entry.isIntersecting) {
// 마커가 뷰포트에 들어왔을 때 (내용의 끝까지 스크롤됨)
if (closeTimers.has(details)) {
clearTimeout(closeTimers.get(details));
}
// 3초 후에 닫히도록 타이머 설정
const timerId = setTimeout(() => {
if (details.open) {
details.open = false;
const slideContent = details.querySelector('.slide-content');
if (slideContent) {
slideContent.style.maxHeight = '0';
slideContent.style.opacity = '0';
}
closeTimers.delete(details);
setTimeout(() => {
const detailsTop = details.getBoundingClientRect().top;
window.scrollTo({
top: window.pageYOffset + detailsTop - 200,
behavior: 'smooth',
});
}, 300);
}
}, 3000); // 3초 = 3000ms
closeTimers.set(details, timerId);
} else {
// 마커가 뷰포트를 벗어나면 (다시 위로 스크롤하면) 타이머 취소
if (closeTimers.has(details)) {
clearTimeout(closeTimers.get(details));
closeTimers.delete(details);
}
}
}
});
}, observerOptions);
let currentOpenDetails = null;
let lastScrollDirection = 0; // 0: 정지, 1: 아래, -1: 위
// window 스크롤 이벤트 리스너 (짧은 글용)
window.addEventListener('wheel', function(e) {
if (currentOpenDetails && currentOpenDetails.open) {
const slideContent = currentOpenDetails.querySelector('.slide-content');
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
// "짧은 글"로 판단된 경우에만 wheel 로직 적용
if (slideContent && slideContent.scrollHeight <= viewportHeight * 0.8) {
// 스크롤 방향 감지
const newScrollDirection = e.deltaY > 0 ? 1 : (e.deltaY < 0 ? -1 : 0);
// 스크롤 방향이 아래쪽일 때
if (newScrollDirection === 1) {
// 기존 타이머가 있다면 취소 (새로운 스크롤 이벤트 발생 시)
if (closeTimers.has(currentOpenDetails)) {
clearTimeout(closeTimers.get(currentOpenDetails));
}
// 1초 후에 닫히도록 타이머 설정
const timerId = setTimeout(() => {
if (currentOpenDetails && currentOpenDetails.open) { // 타이머 만료 시에도 열려있는지 확인
currentOpenDetails.open = false;
if (slideContent) {
slideContent.style.maxHeight = '0';
slideContent.style.opacity = '0';
}
closeTimers.delete(currentOpenDetails);
setTimeout(() => {
const detailsTop = currentOpenDetails.getBoundingClientRect().top;
window.scrollTo({
top: window.pageYOffset + detailsTop - 200,
behavior: 'smooth',
});
}, 300);
currentOpenDetails = null; // 닫혔으므로 초기화
}
}, 1000); // 1초 = 1000ms
closeTimers.set(currentOpenDetails, timerId);
} else if (newScrollDirection === -1) { // 스크롤 방향이 위쪽일 때
// 위로 스크롤하면 타이머 취소
if (closeTimers.has(currentOpenDetails)) {
clearTimeout(closeTimers.get(currentOpenDetails));
closeTimers.delete(currentOpenDetails);
}
}
}
}
});
detailsElements.forEach(details => {
const slideContent = details.querySelector('.slide-content');
const scrollEndMarker = details.querySelector('.scroll-end-marker');
details.addEventListener('toggle', function() {
// details가 닫힐 때 타이머 및 currentOpenDetails 정리
if (!this.open) {
if (closeTimers.has(this)) {
clearTimeout(closeTimers.get(this));
closeTimers.delete(this);
}
if (currentOpenDetails === this) {
currentOpenDetails = null;
}
// 닫힐 때 Intersection Observer도 해제
if (scrollEndMarker) {
scrollEndObserver.unobserve(scrollEndMarker);
}
}
if (this.open) {
currentOpenDetails = this;
// 다른 details 닫기
detailsElements.forEach(otherDetails => {
if (otherDetails !== this && otherDetails.open) {
if (closeTimers.has(otherDetails)) {
clearTimeout(closeTimers.get(otherDetails));
closeTimers.delete(otherDetails);
}
otherDetails.open = false;
const otherSlideContent = otherDetails.querySelector('.slide-content');
if (otherSlideContent) {
otherSlideContent.style.maxHeight = '0';
otherSlideContent.style.opacity = '0';
}
// 다른 details가 닫힐 때 해당 Observer도 해제
const otherMarker = otherDetails.querySelector('.scroll-end-marker');
if (otherMarker) {
scrollEndObserver.unobserve(otherMarker);
}
}
});
// 슬라이딩 효과
if (slideContent) {
slideContent.style.maxHeight = 'none';
const scrollHeight = slideContent.scrollHeight;
slideContent.style.maxHeight = '0';
requestAnimationFrame(() => {
slideContent.style.maxHeight = scrollHeight + 'px';
slideContent.style.opacity = '1';
// details가 열린 후, 내용의 길이를 판단하여 Observer 등록
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
if (scrollHeight > viewportHeight * 0.8) { // 긴 글인 경우
if (scrollEndMarker) {
scrollEndObserver.observe(scrollEndMarker);
}
}
});
}
// 열린 details로 스크롤 이동
requestAnimationFrame(() => {
const detailsTop = this.getBoundingClientRect().top;
window.scrollTo({
top: window.pageYOffset + detailsTop - 200,
behavior: 'smooth',
});
});
} else {
// details가 닫힐 때
if (slideContent) {
slideContent.style.maxHeight = '0';
slideContent.style.opacity = '0';
}
}
});
});
});
</script>
위의 스크립트는 공지글의 내용이 길어서 한 화면을 넘어서면 가장 아래줄에 도달시 3초가 지나면 자동으로 닫히고,
내용글이 짧아서 화면을 벗어나지 않으면 내용이 닫히지 않고, 마우스 스크롤시 1초가 지나면 닫히게 만든 스크립트 입니다.
시간은 자신에게 맞게끔 조절하면 됩니다.
별 기능이 아닌데 스크립트가 길다고 여겨지면 적용 안 하면 됩니다.(딴지 걸기 없기 ^.^)
위의 코드는 앨런 A.I가 만들어 줬습니다.
Tip이 도움이 되었다면 댓글과 평가 부탁합니다.
Tip에 대한 궁금한 점은 댓글로 남겨 주시면 성심껏 답변 드립니다. 
