고급 레이아웃 기법

고급 레이아웃 기법

라이믹스 레이아웃 개발의 고급 기법들을 학습합니다.

삼항연산자 사용

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>

모범 사례

  1. 성능 최적화: requestAnimationFrame 사용
  2. 접근성: ARIA 속성과 키보드 네비게이션
  3. 반응형: 모든 기능이 모바일에서도 작동
  4. 점진적 향상: JavaScript 없이도 기본 기능 제공
  5. 유지보수: 모듈화된 코드 구조

다음 단계

고급 레이아웃 기법을 마스터했다면, 애드온 개발로 진행하세요.