first commit 2

This commit is contained in:
hmw1001
2026-06-11 18:47:38 +09:00
parent c768729ce6
commit 6f534e33a6
11095 changed files with 1595758 additions and 0 deletions
@@ -0,0 +1,270 @@
/*
* ==========================================================================
* 💡 베이스 모듈(Base Module) 전용 스타일시트
* ==========================================================================
*/
/* --- 1. 섹션 기본 스타일 --- */
.item-section {
width: 100%;
padding: 80px 0;
background-color: #ffffff;
}
/* --- 2. 섹션 헤더 (제목, 부제) --- */
.section-header {
text-align: center;
margin-bottom: 50px;
}
.section-header .subtitle {
font-size: 16px;
font-weight: 700;
color: #0056b3;
margin-bottom: 10px;
display: block;
}
.section-header h2 {
font-size: 36px;
font-weight: 900;
color: #25282B;
margin-bottom: 15px;
line-height: 1.4;
}
.section-header p {
font-size: 16px;
line-height: 1.7;
color: #666;
max-width: 600px;
margin: 0 auto;
}
/* --- 3. 아이템 카드 그리드 --- */
.item-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
/* --- 4. 개별 아이템 카드 --- */
.item-card {
background-color: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.item-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
/* --- 5. 카드 내부 요소 --- */
.item-image {
width: 100%;
aspect-ratio: 4 / 3;
overflow: hidden;
}
.item-image.is-patent {
padding: 20px;
background-color: #f0f0f0;
}
.item-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
}
.item-card:hover .item-image img {
transform: scale(1.05);
}
.item-info {
padding: 25px;
}
.item-info h3 {
font-size: 1.2rem;
font-weight: 700;
margin-bottom: 10px;
color: #333;
}
.item-info p {
font-size: 0.95rem;
color: #666;
line-height: 1.6;
}
/* --- 9. 스크롤 애니메이션 (공용) --- */
.reveal-up, .reveal-fade {
opacity: 0;
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
}
.reveal-up { transform: translateY(40px); }
.reveal-fade { transform: scale(0.95); }
.reveal-up.is-revealed, .reveal-fade.is-revealed {
opacity: 1;
transform: none;
}
/* --- 10. [모듈 전용] 이미지 팝업 모달 스타일 (개선) --- */
.image-modal {
display: none;
position: fixed;
z-index: 1050;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.85);
justify-content: center;
align-items: center;
padding: 40px 20px;
opacity: 0;
transition: opacity 0.3s ease;
}
.image-modal.is-active {
display: flex;
opacity: 1;
}
.modal-content {
position: relative;
background-color: #fff;
margin: auto;
padding: 0;
border-radius: 8px;
width: auto;
max-width: 100%;
box-shadow: 0 5px 15px rgba(0,0,0,.5);
animation: modal-slide-down 0.4s ease-out;
display: flex;
flex-direction: column;
}
@keyframes modal-slide-down {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-content .modal-image {
display: block;
max-width: 90vw;
max-height: 80vh;
object-fit: contain;
width: auto;
height: auto;
}
.modal-info {
padding: 20px 25px;
max-width: 800px; /* 텍스트 영역의 최대 너비는 제한 */
width: 100%;
}
.modal-info .modal-title {
font-size: 1.5rem;
font-weight: 700;
color: #333;
margin: 0 0 10px 0;
}
.modal-info .modal-desc {
font-size: 1rem;
color: #666;
line-height: 1.6;
margin: 0;
}
.close-btn {
color: #fff;
position: absolute;
top: 15px;
right: 25px;
font-size: 35px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
z-index: 10;
}
.close-btn:hover,
.close-btn:focus {
color: #ccc;
text-decoration: none;
}
/* --- 11. [개선] 가독성 향상을 위한 줄바꿈 처리 --- */
.section-header h2,
.section-header p,
.product-info-trend h3,
.product-info-trend p,
.item-info h3,
.item-info p {
word-break: keep-all;
overflow-wrap: break-word;
}
/* --- 12. [개선] 카드 설명 텍스트 잘림 방지 및 높이 고정 --- */
.product-info-trend p,
.item-info p {
height: 3.04rem;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.6;
}
/* --- 13. [최종 수정] 모듈 스크롤바 완전 숨김 처리 --- */
/* 1. 기본 상태: 스크롤바를 완전히 투명하게 만듭니다. */
.image-modal {
/* Firefox: 스크롤바 색상을 양쪽 모두 투명하게 설정 */
scrollbar-color: transparent transparent;
scrollbar-width: thin;
transition: scrollbar-color 0.3s ease;
}
.image-modal::-webkit-scrollbar {
width: 10px;
}
.image-modal::-webkit-scrollbar-track {
background: transparent;
}
.image-modal::-webkit-scrollbar-thumb {
/* Webkit: 스크롤바 막대를 투명하게 */
background-color: transparent;
border-radius: 10px;
border: 3px solid transparent;
background-clip: padding-box;
transition: background-color 0.3s ease;
}
/* 2. :hover 상태: 마우스를 올리면 스크롤바가 나타납니다. */
.image-modal:hover {
/* Firefox: 스크롤바 색상을 보이게 변경 */
scrollbar-color: rgba(0, 0, 0, 0.4) transparent;
}
.image-modal:hover::-webkit-scrollbar-thumb {
/* Webkit: 스크롤바 막대 색상을 보이게 변경 */
background-color: rgba(0, 0, 0, 0.4);
}
@@ -0,0 +1,78 @@
// TODO: 'initBaseModule'을 고유한 함수 이름으로 변경하세요. (예: initNewsModule)
function initBaseModule(moduleId) {
const moduleElement = document.getElementById(moduleId);
if (!moduleElement || moduleElement.classList.contains('initialized')) {
return;
}
const section = moduleElement.querySelector('.base-section');
const itemGrid = moduleElement.querySelector('.item-grid');
const modal = moduleElement.querySelector('.image-modal');
if (!section || !itemGrid || !modal) {
return;
}
function renderGrid() {
const itemsJson = section.dataset.items;
let itemsData;
try {
itemsData = JSON.parse(itemsJson);
} catch (e) {
itemGrid.innerHTML = '<p>데이터를 불러오는 데 실패했습니다.</p>';
return;
}
if (!itemsData || itemsData.length === 0) {
itemGrid.innerHTML = '<p>표시할 항목이 없습니다.</p>';
return;
}
const itemCardsHTML = itemsData.map(item => `
<div class="item-card js-modal-trigger" data-title="${item.title}" data-desc="${item.description}" data-img="${item.image}">
<div class="item-image">
<img src="${item.image}" alt="${item.title}">
</div>
<div class="item-info">
<h3>${item.title}</h3>
<p>${item.description}</p>
</div>
</div>
`).join('');
itemGrid.innerHTML = itemCardsHTML;
}
function setupModalEvents() {
const modalImage = modal.querySelector('.modal-image');
const modalTitle = modal.querySelector('.modal-title');
const modalDesc = modal.querySelector('.modal-desc');
const closeBtn = modal.querySelector('.close-btn');
function closeModal() {
modal.classList.remove('is-active');
}
itemGrid.addEventListener('click', function(event) {
const card = event.target.closest('.js-modal-trigger');
if (card) {
modalImage.src = card.dataset.img;
modalTitle.textContent = card.dataset.title;
modalDesc.textContent = card.dataset.desc;
modal.classList.add('is-active');
}
});
closeBtn.addEventListener('click', closeModal);
modal.addEventListener('click', (e) => (e.target === modal) && closeModal());
document.addEventListener('keydown', (e) => (e.key === 'Escape' && modal.classList.contains('is-active')) && closeModal());
}
renderGrid();
setupModalEvents();
moduleElement.classList.add('initialized');
}
// TODO: 'initBaseModule'을 고유한 함수 이름으로 변경하세요.
window.initBaseModule = initBaseModule;
@@ -0,0 +1,102 @@
<?php
if (!defined('_GNUBOARD_')) exit;
// =================================================================================
// 💡 [사용법] 이 파일을 복사하여 새 모듈을 만들 때 아래 TODO 항목들을 수정하세요.
// =================================================================================
// TODO: 'board_name'을 실제 게시판 테이블명으로 변경하세요. (예: 'gallery', 'news' 등)
$bo_table_name = 'board_name';
// TODO: 가져올 게시물 수를 변경할 수 있습니다.
$limit = 3;
// --- 데이터 처리 ---
$module_data = array();
$sql = " SELECT wr_id, wr_subject, wr_content FROM {$g5['write_prefix']}{$bo_table_name} WHERE wr_is_comment = 0 ORDER BY wr_num, wr_reply LIMIT {$limit} ";
$result = sql_query($sql);
for ($i=0; $row=sql_fetch_array($result); $i++) {
$files = get_file($bo_table_name, $row['wr_id']);
$image_url = (isset($files[0]['path']) && isset($files[0]['file'])) ? $files[0]['path'].'/'.$files[0]['file'] : G5_THEME_URL.'/img/no_image.png';
$module_data[] = array(
'id' => $row['wr_id'],
'title' => get_text($row['wr_subject']),
'description' => get_text(cut_str(strip_tags($row['wr_content']), 100)),
'image' => $image_url
);
}
$module_json = json_encode($module_data, JSON_UNESCAPED_UNICODE);
// CSS와 JS 파일의 버전을 파일 수정 시간으로 자동 갱신
$module_css_path = G5_THEME_PATH.'/rb.custom/_base_module/module.css';
$module_js_path = G5_THEME_PATH.'/rb.custom/_base_module/module.js';
$module_css_ver = file_exists($module_css_path) ? filemtime($module_css_path) : G5_CSS_VER;
$module_js_ver = file_exists($module_js_path) ? filemtime($module_js_path) : G5_JS_VER;
// 이 모듈만의 고유 ID를 생성합니다.
$module_id = 'base_module_'.uniqid();
?>
<!-- 모듈의 가장 바깥 요소에 고유 ID를 부여합니다. -->
<div id="<?php echo $module_id; ?>" class="base-module">
<section class="item-section" data-items='<?php echo htmlspecialchars($module_json, ENT_QUOTES, 'UTF-8'); ?>'>
<div class="container">
<div class="section-header">
<!-- TODO: 모듈의 제목과 설명을 수정하세요. -->
<span class="subtitle">Module Subtitle</span>
<h2>모듈 제목</h2>
<p>이곳에 모듈에 대한 간단한 설명을 입력하세요.</p>
</div>
<div class="item-grid">
<!-- JS로 아이템 카드가 생성될 영역 -->
</div>
</div>
</section>
<!-- 모듈 내부에 모달 HTML을 포함시킵니다. -->
<div class="image-modal">
<div class="modal-content">
<span class="close-btn">&times;</span>
<img class="modal-image" src="" alt="이미지">
<div class="modal-info">
<h3 class="modal-title"></h3>
<p class="modal-desc"></p>
</div>
</div>
</div>
</div>
<!-- 이 모듈에 필요한 CSS 파일을 불러옵니다. -->
<link rel="stylesheet" href="<?php echo G5_THEME_URL; ?>/rb.custom/_item_module/module.css?ver=<?php echo $module_css_ver; ?>">
<script>
(function() {
const currentModuleId = '<?php echo $module_id; ?>';
// TODO: 'inititemModule'을 고유한 함수 이름으로 변경하세요. (예: initNewsModule)
const initFunctionName = 'inititemModule';
// TODO: 'item-module-script'를 고유한 스크립트 ID로 변경하세요. (예: news-module-script)
const scriptId = 'item-module-script';
if (document.getElementById(scriptId)) {
if (typeof window[initFunctionName] === 'function') {
window[initFunctionName](currentModuleId);
}
return;
}
const script = document.createElement('script');
script.id = scriptId;
// TODO: JS 파일 경로를 실제 모듈 경로로 수정하세요.
script.src = '<?php echo G5_THEME_URL; ?>/rb.custom/_base_module/module.js?ver=<?php echo $module_js_ver; ?>';
script.async = true;
script.onload = () => {
if (typeof window[initFunctionName] === 'function') {
window[initFunctionName](currentModuleId);
}
};
document.head.appendChild(script);
})();
</script>