고급 레이아웃 기법¶
라이믹스 레이아웃 개발의 고급 기법들을 학습합니다.
삼항연산자 사용¶
PHP 삼항연산자 활용¶
<!-- XE 레이아웃에서의 삼항연산자 사용방법(php) -->
{@
// 기본 삼항연산자
$display_mode = $is_mobile ? 'mobile' : 'desktop';
// 중첩 삼항연산자
$user_level = $logged_info ? ($logged_info->is_admin == 'Y' ? 'admin' : 'member') : 'guest';
// null 병합 연산자 (PHP 7+)
$site_title = $layout_info->site_title ?? '기본 사이트명';
// 조건부 변수 할당
$menu_class = ($module_info->module_srl == $layout_info->index_module_srl) ? 'home-menu' : 'sub-menu';
}
<!-- HTML 내에서 사용 -->
<div class="{$is_mobile ? 'mobile-layout' : 'desktop-layout'}">
<!-- 조건부 속성 -->
<a href="{$val['url']}"
class="menu-link {$val['selected'] ? 'active' : ''}"
target="{$val['open_window'] == 'Y' ? '_blank' : '_self'}">
{$val['text']}
</a>
<!-- 조건부 텍스트 -->
<span class="user-status">
{$logged_info ? '환영합니다, ' . $logged_info->nick_name . '님' : '로그인하세요'}
</span>
<!-- 복잡한 조건 -->
{@
$button_text = $oDocument->isExists() ?
($oDocument->isGranted() ? '수정' : '읽기전용') :
'새 글 작성';
}
<button type="button">{$button_text}</button>
</div>
switch문 활용¶
다중 조건 처리¶
<!-- 레이아웃 switch문 -->
{@
// 모듈별 레이아웃 설정
switch($module_info->module) {
case 'board':
$layout_type = 'board-layout';
$sidebar_position = 'right';
break;
case 'page':
$layout_type = 'page-layout';
$sidebar_position = 'none';
break;
case 'shop':
case 'marketplace':
$layout_type = 'shop-layout';
$sidebar_position = 'left';
break;
default:
$layout_type = 'default-layout';
$sidebar_position = 'right';
}
// 페이지별 클래스
switch($act) {
case 'dispBoardWrite':
case 'dispBoardModify':
$page_class = 'write-page';
break;
case 'dispMemberLoginForm':
case 'dispMemberSignUpForm':
$page_class = 'auth-page';
break;
case 'dispMemberInfo':
case 'dispMemberModifyInfo':
$page_class = 'mypage';
break;
default:
$page_class = 'normal-page';
}
}
<body class="{$layout_type} {$page_class} sidebar-{$sidebar_position}">
<!-- 레이아웃 구조 -->
<div class="layout-wrapper">
<!-- 사이드바 위치에 따른 배치 -->
<!--@switch($sidebar_position)-->
<!--@case('left')-->
<aside class="sidebar sidebar-left">
<include target="sidebar.html" />
</aside>
<main class="content">
{$content}
</main>
<!--@break-->
<!--@case('right')-->
<main class="content">
{$content}
</main>
<aside class="sidebar sidebar-right">
<include target="sidebar.html" />
</aside>
<!--@break-->
<!--@case('both')-->
<aside class="sidebar sidebar-left">
<include target="sidebar-left.html" />
</aside>
<main class="content">
{$content}
</main>
<aside class="sidebar sidebar-right">
<include target="sidebar-right.html" />
</aside>
<!--@break-->
<!--@default-->
<main class="content content-full">
{$content}
</main>
<!--@endswitch-->
</div>
</body>
동적 레이아웃 변경¶
메인/서브 레이아웃 분기¶
<!-- 하나의 레이아웃에서 메인과 서브의 넓이 다르게 적용하기 -->
{@
// 메인 페이지 체크
$is_main = ($module_info->module_srl == $layout_info->index_module_srl) || ($act == 'dispPageIndex' && !$mid);
// 전체 너비 페이지
$full_width_acts = array(
'dispMemberLoginForm',
'dispMemberSignUpForm',
'dispBoardWrite',
'dispPageAdminContentModify'
);
$is_full_width = in_array($act, $full_width_acts);
// 레이아웃 설정
if($is_main) {
$container_class = 'container-main';
$content_width = '100%';
$show_sidebar = false;
} elseif($is_full_width) {
$container_class = 'container-full';
$content_width = '100%';
$show_sidebar = false;
} else {
$container_class = 'container-sub';
$content_width = '70%';
$show_sidebar = true;
}
}
<div class="layout-container {$container_class}">
<!-- 메인 페이지 전용 영역 -->
<div cond="$is_main" class="main-hero">
<div class="hero-slider">
<!-- 메인 슬라이더 -->
</div>
<div class="main-features">
<!-- 메인 기능 소개 -->
</div>
</div>
<!-- 공통 콘텐츠 영역 -->
<div class="content-wrapper" style="width: {$content_width}">
<main class="main-content">
{$content}
</main>
<!-- 조건부 사이드바 -->
<aside cond="$show_sidebar" class="sidebar">
<include target="sidebar.html" />
</aside>
</div>
</div>
<style>
/* 메인 페이지 스타일 */
.container-main {
max-width: 1400px;
}
.container-main .hero-slider {
height: 500px;
margin-bottom: 50px;
}
/* 서브 페이지 스타일 */
.container-sub {
max-width: 1200px;
}
.container-sub .content-wrapper {
display: flex;
gap: 30px;
}
.container-sub .main-content {
flex: 1;
}
.container-sub .sidebar {
width: 300px;
flex-shrink: 0;
}
/* 전체 너비 페이지 */
.container-full {
max-width: 100%;
padding: 0;
}
/* 반응형 처리 */
@media (max-width: 768px) {
.content-wrapper {
width: 100% !important;
flex-direction: column;
}
.sidebar {
width: 100% !important;
}
}
</style>
헤더 상태 변화¶
스크롤에 따른 헤더 변경¶
<!-- 헤더 - 스크롤 내리면 상태 변화주기 -->
<header class="site-header" id="header">
<div class="header-inner">
<!-- 로고 -->
<div class="logo">
<a href="{getUrl('')}">
<img src="/layouts/my_layout/img/logo.png" alt="{$layout_info->site_title}" class="logo-default" />
<img src="/layouts/my_layout/img/logo-small.png" alt="{$layout_info->site_title}" class="logo-small" />
</a>
</div>
<!-- 네비게이션 -->
<nav class="main-nav">
<ul class="nav-list">
<li loop="$main_menu->list=>$key,$val">
<a href="{$val['url']}">{$val['text']}</a>
</li>
</ul>
</nav>
<!-- 검색/사용자 메뉴 -->
<div class="header-tools">
<button type="button" class="search-btn">
<i class="xi-search"></i>
</button>
<div class="user-menu">
<!-- 사용자 메뉴 -->
</div>
</div>
</div>
<!-- 프로그레스 바 -->
<div class="scroll-progress"></div>
</header>
<style>
/* 헤더 기본 스타일 */
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
z-index: 1000;
height: 80px;
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 30px;
max-width: 1200px;
margin: 0 auto;
}
/* 로고 전환 */
.logo-default {
display: block;
height: 50px;
transition: opacity 0.3s;
}
.logo-small {
display: none;
height: 30px;
}
/* 스크롤 시 헤더 변화 */
.site-header.scrolled {
height: 60px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.site-header.scrolled .logo-default {
display: none;
}
.site-header.scrolled .logo-small {
display: block;
}
.site-header.scrolled .nav-list {
font-size: 14px;
}
/* 헤더 숨김/표시 */
.site-header.hide {
transform: translateY(-100%);
}
/* 스크롤 프로그레스 바 */
.scroll-progress {
position: absolute;
bottom: 0;
left: 0;
height: 3px;
background: #007bff;
transition: width 0.1s;
width: 0%;
}
/* 컨텐츠 여백 */
body {
padding-top: 80px;
}
</style>
<script>
(function() {
var header = document.getElementById('header');
var scrollProgress = header.querySelector('.scroll-progress');
var lastScrollTop = 0;
var scrollThreshold = 100;
var hideThreshold = 300;
function updateHeader() {
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
var docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
var scrollPercent = (scrollTop / docHeight) * 100;
// 스크롤 프로그레스 업데이트
scrollProgress.style.width = scrollPercent + '%';
// 스크롤 상태에 따른 헤더 변화
if(scrollTop > scrollThreshold) {
header.classList.add('scrolled');
} else {
header.classList.remove('scrolled');
}
// 스크롤 방향에 따른 헤더 숨김/표시
if(scrollTop > hideThreshold) {
if(scrollTop > lastScrollTop) {
// 아래로 스크롤
header.classList.add('hide');
} else {
// 위로 스크롤
header.classList.remove('hide');
}
} else {
header.classList.remove('hide');
}
lastScrollTop = scrollTop;
}
// 스크롤 이벤트 (throttle 적용)
var scrollTimer;
window.addEventListener('scroll', function() {
if(scrollTimer) {
window.cancelAnimationFrame(scrollTimer);
}
scrollTimer = window.requestAnimationFrame(function() {
updateHeader();
});
});
// 초기 실행
updateHeader();
})();
</script>
고급 메뉴 시스템¶
메가 메뉴 구현¶
<!-- 고급 메가 메뉴 -->
<nav class="mega-menu">
<ul class="menu-list">
<li loop="$main_menu->list=>$key,$val" class="menu-item {$val['list'] ? 'has-mega' : ''}">
<a href="{$val['url']}" class="menu-link">
{$val['text']}
<i cond="$val['list']" class="xi-angle-down"></i>
</a>
<!-- 메가 메뉴 드롭다운 -->
<div cond="$val['list']" class="mega-dropdown">
<div class="mega-content">
<!-- 서브메뉴 컬럼 -->
<div class="mega-column">
<h3>{$val['text']} 하위메뉴</h3>
<ul class="sub-menu">
<li loop="$val['list']=>$key2,$val2">
<a href="{$val2['url']}">
<i class="xi-arrow-right"></i>
{$val2['text']}
</a>
</li>
</ul>
</div>
<!-- 추가 콘텐츠 -->
<div class="mega-column">
<h3>인기 게시물</h3>
{@
// 해당 메뉴의 인기 게시물 가져오기
$menu_module_srl = $val['module_srl'];
if($menu_module_srl) {
$args = new stdClass();
$args->module_srl = $menu_module_srl;
$args->list_count = 5;
$args->sort_index = 'readed_count';
$args->order_type = 'desc';
$output = executeQuery('document.getDocumentList', $args);
$popular_docs = $output->data;
}
}
<ul class="popular-list">
<li loop="$popular_docs=>$doc">
<a href="{getUrl('', 'document_srl', $doc->document_srl)}">
{$doc->getTitle()}
</a>
</li>
</ul>
</div>
<!-- 배너/프로모션 -->
<div class="mega-column mega-promo">
<img src="/layouts/my_layout/img/promo-{$key}.jpg" alt="프로모션" />
<h3>특별 이벤트</h3>
<p>이번 달 특별 할인 이벤트를 확인하세요!</p>
<a href="#" class="btn-promo">자세히 보기</a>
</div>
</div>
</div>
</li>
</ul>
</nav>
<style>
.mega-menu {
position: relative;
}
.menu-list {
display: flex;
list-style: none;
margin: 0;
padding: 0;
}
.menu-item {
position: static;
}
.menu-link {
display: block;
padding: 20px;
text-decoration: none;
color: #333;
transition: color 0.3s;
}
.menu-item:hover .menu-link {
color: #007bff;
}
/* 메가 드롭다운 */
.mega-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
}
.menu-item:hover .mega-dropdown {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.mega-content {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
padding: 40px;
max-width: 1200px;
margin: 0 auto;
}
.mega-column h3 {
margin-bottom: 15px;
color: #333;
}
.mega-promo {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.mega-promo img {
width: 100%;
border-radius: 8px;
margin-bottom: 15px;
}
</style>
포인트/레벨 시스템¶
사용자 포인트 및 레벨 표시¶
<!-- 포인트 레벨 및 아이콘 출력하기 및 경험치 출력 -->
{@
if($logged_info) {
// 포인트 모듈 설정
$point_config = getModel('point')->getModuleConfig();
// 현재 포인트와 레벨
$current_point = $logged_info->point;
$current_level = getModel('point')->getLevel($current_point, $point_config->level_step);
// 다음 레벨까지 필요한 포인트
$next_level_point = $point_config->level_step[$current_level + 1];
$prev_level_point = $point_config->level_step[$current_level];
$need_point = $next_level_point - $current_point;
// 경험치 퍼센트
$level_progress = (($current_point - $prev_level_point) / ($next_level_point - $prev_level_point)) * 100;
// 레벨 아이콘
$level_icon = getModel('point')->getLevelIcon($current_level);
}
}
<!-- 포인트/레벨 표시 위젯 -->
<div cond="$logged_info" class="point-level-widget">
<div class="user-info">
<div class="level-icon">
<img src="{$level_icon}" alt="Level {$current_level}" cond="$level_icon" />
<span class="level-number" cond="!$level_icon">Lv.{$current_level}</span>
</div>
<div class="user-details">
<h4>{$logged_info->nick_name}</h4>
<div class="point-info">
<i class="xi-coin"></i>
<span>{number_format($current_point)}P</span>
</div>
</div>
</div>
<!-- 레벨 진행도 -->
<div class="level-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: {$level_progress}%"></div>
</div>
<div class="progress-info">
<span>Lv.{$current_level}</span>
<span>{round($level_progress)}%</span>
<span>Lv.{$current_level + 1}</span>
</div>
</div>
<!-- 다음 레벨까지 -->
<div class="next-level-info">
다음 레벨까지 <strong>{number_format($need_point)}P</strong> 필요
</div>
<!-- 포인트 내역 -->
<div class="point-history">
<a href="{getUrl('act', 'dispPointHistory')}">
포인트 내역 보기 <i class="xi-angle-right"></i>
</a>
</div>
</div>
<style>
.point-level-widget {
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.level-icon {
width: 60px;
height: 60px;
margin-right: 15px;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
border-radius: 50%;
}
.level-icon img {
max-width: 100%;
max-height: 100%;
}
.level-number {
font-size: 20px;
font-weight: bold;
color: #007bff;
}
.progress-bar {
height: 10px;
background: #e9ecef;
border-radius: 5px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007bff, #0056b3);
transition: width 0.3s;
}
.progress-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #666;
}
.next-level-info {
text-align: center;
margin: 15px 0;
color: #666;
}
.next-level-info strong {
color: #007bff;
}
/* 포인트 애니메이션 */
@keyframes pointGain {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
color: #28a745;
}
100% {
transform: scale(1);
}
}
.point-animation {
animation: pointGain 0.5s ease;
}
</style>
<script>
// 포인트 획득 시 애니메이션
function animatePointGain(points) {
var $pointElement = $('.point-info span');
var currentPoints = parseInt($pointElement.text().replace(/[^0-9]/g, ''));
var newPoints = currentPoints + points;
// 애니메이션 클래스 추가
$pointElement.addClass('point-animation');
// 숫자 카운트업 애니메이션
$({count: currentPoints}).animate({count: newPoints}, {
duration: 1000,
easing: 'swing',
step: function() {
$pointElement.text(Math.floor(this.count).toLocaleString() + 'P');
},
complete: function() {
$pointElement.text(newPoints.toLocaleString() + 'P');
$pointElement.removeClass('point-animation');
// 진행도 업데이트
updateLevelProgress();
}
});
// 획득 포인트 표시
showPointNotification('+' + points + 'P');
}
function showPointNotification(message) {
var $notification = $('<div class="point-notification">' + message + '</div>');
$('body').append($notification);
$notification.fadeIn(300).delay(2000).fadeOut(300, function() {
$(this).remove();
});
}
</script>
모범 사례¶
- 성능 최적화: requestAnimationFrame 사용
- 접근성: ARIA 속성과 키보드 네비게이션
- 반응형: 모든 기능이 모바일에서도 작동
- 점진적 향상: JavaScript 없이도 기본 기능 제공
- 유지보수: 모듈화된 코드 구조
다음 단계¶
고급 레이아웃 기법을 마스터했다면, 애드온 개발로 진행하세요.