라이믹스 스킨 제작 완전 가이드

라이믹스 스킨 제작 완전 가이드

XE 스킨 제작 매뉴얼을 라이믹스 환경에 맞게 업데이트한 종합 가이드입니다.

📋 목차

  1. 스킨 제작 개요
  2. 스킨 제작의 기초
  3. 레이아웃 스킨 만들기
  4. 게시판 스킨 만들기
  5. 라이믹스 업데이트 사항

1. 스킨 제작 개요

1.1 라이믹스 스킨이란

스킨은 라이믹스로 생성한 데이터를 사용자 화면에 표현하는 양식입니다. 모듈, 레이아웃, 위젯에는 스킨이 하나 이상 있습니다.

스킨의 저장 위치:
- 모듈 스킨: /modules/{모듈명}/skins/{스킨명}/
- 레이아웃 스킨: /layouts/{레이아웃명}/
- 위젯 스킨: /widgets/{위젯명}/skins/{스킨명}/

1.2 스킨 제작에 필요한 기술

HTML5 (HyperText Markup Language)
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>라이믹스 사이트</title>
</head>
<body>
    <main>{$content}</main>
</body>
</html>
CSS3 (Cascading Style Sheets)
/* 반응형 디자인 */
@media (max-width: 768px) {
    .container {
        padding: 10px;
    }
}

/* Flexbox 레이아웃 */
.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
자바스크립트와 jQuery

라이믹스는 여전히 jQuery를 내장하고 있어 쉽게 활용할 수 있습니다.

jQuery(document).ready(function($) {
    $('.menu-toggle').on('click', function() {
        $('.nav-menu').toggleClass('active');
    });
});
라이믹스 템플릿 문법

XE 템플릿 문법과 호환되며, 추가 기능이 제공됩니다.


2. 스킨 제작의 기초

2.1 HTML5 기준

2.1.1 시맨틱 HTML 사용
<header>
    <nav>
        <ul>
            <li><a href="/">홈</a></li>
            <li><a href="/about">소개</a></li>
        </ul>
    </nav>
</header>

<main>
    <article>
        <h1>게시글 제목</h1>
        <section>내용</section>
    </article>
</main>

<footer>
    <p>&copy; 2024 My Site</p>
</footer>
2.1.2 접근성 고려
<!-- ARIA 레이블 사용 -->
<button aria-label="메뉴 열기" class="menu-toggle">
    <span class="sr-only">메뉴</span>
    ☰
</button>

<!-- 스킵 네비게이션 -->
<a href="#main-content" class="skip-to-content">본문 바로가기</a>

2.2 현대적 CSS

2.2.1 CSS Grid와 Flexbox
/* Grid 레이아웃 */
.layout {
    display: grid;
    grid-template-areas: 
        "header header"
        "sidebar main"
        "footer footer";
    grid-template-columns: 250px 1fr;
    min-height: 100vh;
}

.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.footer { grid-area: footer; }

/* Flexbox 네비게이션 */
.nav {
    display: flex;
    gap: 1rem;
}
2.2.2 CSS 변수 활용
:root {
    --primary-color: #007bff;
    --secondary-color: #6c757d;
    --border-radius: 0.375rem;
    --spacing-unit: 1rem;
}

.btn {
    background-color: var(--primary-color);
    border-radius: var(--border-radius);
    padding: var(--spacing-unit);
}

2.3 라이믹스 템플릿 문법

2.3.1 기본 변수
<!-- 사용자 정보 -->
<div cond="$is_logged">
    안녕하세요, {$logged_info->nick_name}님!
</div>

<!-- 모듈 정보 -->
<h1>{$module_info->browser_title}</h1>
<p>{$module_info->description}</p>
2.3.2 조건문 (향상된 문법)
<!-- 기존 XE 문법 -->
<!--@if($is_logged)-->
    <p>로그인 상태</p>
<!--@end-->

<!-- 라이믹스 권장 문법 -->
<p cond="$is_logged">로그인 상태</p>

<!-- 복합 조건 -->
<div cond="$is_logged && $logged_info->is_admin == 'Y'">
    관리자 메뉴
</div>
2.3.3 반복문
<!-- 메뉴 출력 -->
<nav>
    <ul>
        <li loop="$menu_list=>$menu" class="active"|cond="$menu['selected']">
            <a href="{$menu['href']}">{$menu['text']}</a>
        </li>
    </ul>
</nav>

<!-- 게시물 목록 -->
<div class="post-grid">
    <article loop="$document_list=>$document" class="post-card">
        <h2><a href="{getUrl('document_srl', $document->document_srl)}">{$document->getTitle()}</a></h2>
        <p>{cut_str($document->getContentText(), 100, '...')}</p>
    </article>
</div>
2.3.4 파일 참조 (라이믹스 스타일)
<!-- CSS 파일 -->
<load target="./css/layout.css" />
<load target="./css/responsive.css" media="screen" />

<!-- JS 파일 -->
<load target="./js/common.js" />
<load target="./js/mobile.js" type="body" />

<!-- 조건부 로딩 -->
<load target="./css/ie-fix.css" targetie="lte IE 9" />

3. 레이아웃 스킨 만들기

3.1 레이아웃 스킨 구조

3.1.1 디렉토리 구조
/layouts/my_layout/
├── conf/
│   └── info.xml
├── css/
│   ├── layout.css
│   └── responsive.css
├── js/
│   └── layout.js
├── images/
│   └── logo.png
├── layout.html
└── thumbnail.png
3.1.2 info.xml 작성
<?xml version="1.0" encoding="UTF-8"?>
<layout version="0.2">
    <title xml:lang="ko">모던 레이아웃</title>
    <title xml:lang="en">Modern Layout</title>

    <description xml:lang="ko">라이믹스용 현대적 반응형 레이아웃</description>
    <description xml:lang="en">Modern responsive layout for Rhymix</description>

    <version>2.0.0</version>
    <date>2024-01-01</date>

    <author email_address="developer@example.com">
        <name xml:lang="ko">개발자</name>
        <name xml:lang="en">Developer</name>
    </author>

    <license>MIT</license>

    <!-- 메뉴 정의 -->
    <menus>
        <menu name="main_menu" maxdepth="3" default="true">
            <title xml:lang="ko">주 메뉴</title>
            <title xml:lang="en">Main Menu</title>
        </menu>
        <menu name="footer_menu" maxdepth="1">
            <title xml:lang="ko">푸터 메뉴</title>
            <title xml:lang="en">Footer Menu</title>
        </menu>
    </menus>

    <!-- 설정 변수 -->
    <extra_vars>
        <var name="color_scheme" type="select">
            <title xml:lang="ko">색상 테마</title>
            <description xml:lang="ko">사이트의 기본 색상 테마를 선택하세요</description>
            <options value="blue">블루</options>
            <options value="green">그린</options>
            <options value="purple">퍼플</options>
        </var>

        <var name="layout_width" type="select">
            <title xml:lang="ko">레이아웃 너비</title>
            <options value="fluid">유동형</options>
            <options value="fixed">고정형</options>
        </var>

        <var name="show_sidebar" type="select">
            <title xml:lang="ko">사이드바 표시</title>
            <options value="Y">표시</options>
            <options value="N">숨김</options>
        </var>
    </extra_vars>
</layout>

3.2 현대적 레이아웃 HTML

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- SEO 메타태그 -->
    <meta name="description" content="{$module_info->description}">
    <meta name="keywords" content="{$module_info->keywords}">

    <!-- 오픈그래프 -->
    <meta property="og:title" content="{$module_info->browser_title}">
    <meta property="og:description" content="{$module_info->description}">
    <meta property="og:type" content="website">
    <meta property="og:url" content="{$current_url}">

    <title>{$module_info->browser_title}</title>

    <!-- CSS 로드 -->
    <load target="./css/layout.css" />
    <load target="./css/theme-{$layout_info->color_scheme|default:'blue'}.css" />
    <load target="./css/responsive.css" />

    <!-- JS 로드 -->
    <load target="./js/layout.js" type="body" />
</head>
<body class="layout-{$layout_info->layout_width|default:'fluid'} theme-{$layout_info->color_scheme|default:'blue'}">
    <!-- 스킵 네비게이션 -->
    <a href="#main-content" class="skip-to-content">본문 바로가기</a>

    <!-- 헤더 -->
    <header class="site-header" role="banner">
        <div class="container">
            <!-- 로고 -->
            <div class="site-branding">
                <h1 class="site-title">
                    <a href="{getUrl()}">{$module_info->browser_title}</a>
                </h1>
                <p class="site-description" cond="$module_info->description">
                    {$module_info->description}
                </p>
            </div>

            <!-- 주 메뉴 -->
            <nav class="main-navigation" role="navigation" aria-label="주 메뉴">
                <button class="menu-toggle" aria-controls="primary-menu" aria-expanded="false">
                    <span class="sr-only">메뉴</span>
                    <span class="hamburger"></span>
                </button>

                <ul class="menu" id="primary-menu">
                    <li loop="$main_menu->list=>$menu1" class="menu-item {$menu1['class']}">
                        <a href="{$menu1['href']}" 
                           target="_blank"|cond="$menu1['open_window']=='Y'"
                           class="menu-link">
                            {$menu1['text']}
                        </a>

                        <!-- 서브메뉴 -->
                        <ul cond="$menu1['list']" class="sub-menu">
                            <li loop="$menu1['list']=>$menu2" class="sub-menu-item {$menu2['class']}">
                                <a href="{$menu2['href']}" 
                                   target="_blank"|cond="$menu2['open_window']=='Y'"
                                   class="sub-menu-link">
                                    {$menu2['text']}
                                </a>
                            </li>
                        </ul>
                    </li>
                </ul>
            </nav>

            <!-- 사용자 메뉴 -->
            <div class="user-menu">
                <!-- 검색 -->
                <form action="{getUrl()}" method="get" class="search-form" role="search">
                    <input type="hidden" name="mid" value="{$mid}">
                    <input type="hidden" name="act" value="IS">
                    <label class="sr-only" for="search-input">검색</label>
                    <input type="search" 
                           id="search-input"
                           name="is_keyword" 
                           value="{$is_keyword}" 
                           placeholder="검색어 입력"
                           class="search-input">
                    <button type="submit" class="search-submit" aria-label="검색 실행">
                        🔍
                    </button>
                </form>

                <!-- 로그인 정보 -->
                <div class="account-info">
                    <div cond="$is_logged" class="logged-in">
                        <a href="{getUrl('act','dispMemberInfo')}" class="user-profile">
                            <img cond="$logged_info->profile_image" 
                                 src="{$logged_info->profile_image}" 
                                 alt="{$logged_info->nick_name} 프로필"
                                 class="profile-image">
                            <span class="user-name">{$logged_info->nick_name}</span>
                        </a>
                        <a href="{getUrl('act','dispMemberLogout')}" class="logout-link">로그아웃</a>
                    </div>

                    <div cond="!$is_logged" class="logged-out">
                        <a href="{getUrl('act','dispMemberLoginForm')}" class="login-link">로그인</a>
                        <a href="{getUrl('act','dispMemberSignUpForm')}" class="signup-link">회원가입</a>
                    </div>
                </div>
            </div>
        </div>
    </header>

    <!-- 메인 콘텐츠 -->
    <div class="site-content">
        <div class="container">
            <main id="main-content" class="content-area" role="main">
                {$content}
            </main>

            <!-- 사이드바 -->
            <aside cond="$layout_info->show_sidebar != 'N'" class="sidebar" role="complementary">
                <!-- 위젯 영역 -->
                <div class="widget-area">
                    <!-- 최근 게시물 위젯 -->
                    <div class="widget recent-posts">
                        <h3 class="widget-title">최근 게시물</h3>
                        <div class="widget-content">
                            <!-- 위젯 내용 -->
                        </div>
                    </div>

                    <!-- 인기 태그 위젯 -->
                    <div class="widget popular-tags">
                        <h3 class="widget-title">인기 태그</h3>
                        <div class="widget-content">
                            <!-- 위젯 내용 -->
                        </div>
                    </div>
                </div>
            </aside>
        </div>
    </div>

    <!-- 푸터 -->
    <footer class="site-footer" role="contentinfo">
        <div class="container">
            <!-- 푸터 메뉴 -->
            <nav class="footer-navigation" cond="$footer_menu->list">
                <ul class="footer-menu">
                    <li loop="$footer_menu->list=>$menu" class="footer-menu-item">
                        <a href="{$menu['href']}" class="footer-menu-link">
                            {$menu['text']}
                        </a>
                    </li>
                </ul>
            </nav>

            <!-- 저작권 -->
            <div class="site-info">
                <p>&copy; {date('Y')} {$module_info->browser_title}. All rights reserved.</p>
                <p>Powered by <a href="https://rhymix.org">Rhymix</a></p>
            </div>
        </div>
    </footer>
</body>
</html>

3.3 반응형 CSS

/* CSS 변수 */
:root {
    --primary-color: #007bff;
    --secondary-color: #6c757d;
    --success-color: #28a745;
    --warning-color: #ffc107;
    --danger-color: #dc3545;

    --font-size-base: 1rem;
    --font-size-lg: 1.25rem;
    --font-size-sm: 0.875rem;

    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 3rem;

    --border-radius: 0.375rem;
    --box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);

    --container-max-width: 1200px;
    --header-height: 70px;
}

/* 기본 스타일 */
* {
    box-sizing: border-box;
}

body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    line-height: 1.6;
    color: #333;
}

.container {
    max-width: var(--container-max-width);
    margin: 0 auto;
    padding: 0 var(--spacing-md);
}

/* 스킵 네비게이션 */
.skip-to-content {
    position: absolute;
    top: -40px;
    left: 6px;
    background: var(--primary-color);
    color: white;
    padding: 8px;
    text-decoration: none;
    border-radius: var(--border-radius);
    z-index: 1000;
}

.skip-to-content:focus {
    top: 6px;
}

/* 헤더 */
.site-header {
    background: white;
    box-shadow: var(--box-shadow);
    position: sticky;
    top: 0;
    z-index: 100;
    height: var(--header-height);
}

.site-header .container {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 100%;
}

.site-branding {
    flex: 0 0 auto;
}

.site-title {
    margin: 0;
    font-size: var(--font-size-lg);
}

.site-title a {
    text-decoration: none;
    color: var(--primary-color);
}

/* 네비게이션 */
.main-navigation {
    flex: 1;
    margin: 0 var(--spacing-lg);
}

.menu-toggle {
    display: none;
    background: none;
    border: none;
    cursor: pointer;
    padding: var(--spacing-sm);
}

.menu {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
    gap: var(--spacing-md);
}

.menu-item {
    position: relative;
}

.menu-link {
    display: block;
    padding: var(--spacing-sm) var(--spacing-md);
    text-decoration: none;
    color: #333;
    border-radius: var(--border-radius);
    transition: background-color 0.3s ease;
}

.menu-link:hover,
.menu-item.active .menu-link {
    background-color: var(--primary-color);
    color: white;
}

/* 서브메뉴 */
.sub-menu {
    position: absolute;
    top: 100%;
    left: 0;
    background: white;
    box-shadow: var(--box-shadow);
    border-radius: var(--border-radius);
    min-width: 200px;
    list-style: none;
    margin: 0;
    padding: var(--spacing-sm);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: all 0.3s ease;
}

.menu-item:hover .sub-menu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* 사용자 메뉴 */
.user-menu {
    display: flex;
    align-items: center;
    gap: var(--spacing-md);
}

.search-form {
    display: flex;
    align-items: center;
    background: #f8f9fa;
    border-radius: var(--border-radius);
    padding: var(--spacing-xs);
}

.search-input {
    border: none;
    background: none;
    padding: var(--spacing-sm);
    outline: none;
    width: 200px;
}

.search-submit {
    background: none;
    border: none;
    cursor: pointer;
    padding: var(--spacing-sm);
}

/* 메인 콘텐츠 */
.site-content {
    min-height: calc(100vh - var(--header-height) - 100px);
    padding: var(--spacing-lg) 0;
}

.site-content .container {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: var(--spacing-xl);
}

.layout-fluid .site-content .container {
    max-width: 100%;
}

/* 사이드바 숨김 시 */
.site-content .container:not(:has(.sidebar)) {
    grid-template-columns: 1fr;
}

/* 위젯 */
.widget {
    background: white;
    border-radius: var(--border-radius);
    box-shadow: var(--box-shadow);
    padding: var(--spacing-lg);
    margin-bottom: var(--spacing-lg);
}

.widget-title {
    margin: 0 0 var(--spacing-md) 0;
    font-size: var(--font-size-lg);
    color: var(--primary-color);
}

/* 푸터 */
.site-footer {
    background: #f8f9fa;
    padding: var(--spacing-xl) 0;
    border-top: 1px solid #dee2e6;
}

.footer-menu {
    display: flex;
    list-style: none;
    margin: 0 0 var(--spacing-lg) 0;
    padding: 0;
    gap: var(--spacing-lg);
    justify-content: center;
}

.footer-menu-link {
    text-decoration: none;
    color: var(--secondary-color);
}

.site-info {
    text-align: center;
    color: var(--secondary-color);
    font-size: var(--font-size-sm);
}

/* 반응형 디자인 */
@media (max-width: 768px) {
    .site-header .container {
        flex-wrap: wrap;
    }

    .menu-toggle {
        display: block;
    }

    .main-navigation {
        flex: 1 1 100%;
        order: 3;
    }

    .menu {
        display: none;
        flex-direction: column;
        width: 100%;
        background: white;
        box-shadow: var(--box-shadow);
        border-radius: var(--border-radius);
        padding: var(--spacing-md);
        margin-top: var(--spacing-md);
    }

    .menu.active {
        display: flex;
    }

    .sub-menu {
        position: static;
        opacity: 1;
        visibility: visible;
        transform: none;
        box-shadow: none;
        background: #f8f9fa;
        margin-top: var(--spacing-sm);
    }

    .site-content .container {
        grid-template-columns: 1fr;
    }

    .sidebar {
        order: -1;
    }

    .search-input {
        width: 150px;
    }

    .footer-menu {
        flex-direction: column;
        align-items: center;
        gap: var(--spacing-sm);
    }
}

@media (max-width: 480px) {
    .container {
        padding: 0 var(--spacing-sm);
    }

    .user-menu {
        flex-direction: column;
        align-items: stretch;
        gap: var(--spacing-sm);
    }

    .search-form {
        width: 100%;
    }

    .search-input {
        width: 100%;
    }
}

/* 테마별 색상 */
.theme-blue {
    --primary-color: #007bff;
}

.theme-green {
    --primary-color: #28a745;
}

.theme-purple {
    --primary-color: #6f42c1;
}

/* 접근성 */
.sr-only {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

/* 포커스 스타일 */
:focus {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
}

a:focus,
button:focus {
    outline-color: var(--primary-color);
}

4. 게시판 스킨 만들기

4.1 라이믹스 게시판 스킨 구조

4.1.1 필수 파일 목록
/modules/board/skins/modern_board/
├── conf/
│   └── skin.xml
├── css/
│   ├── board.css
│   └── responsive.css
├── js/
│   └── board.js
├── images/
│   └── icons/
├── list.html
├── view.html
├── write_form.html
├── comment_form.html
├── delete_form.html
├── delete_comment_form.html
├── input_password_form.html
├── message.html
├── _header.html
└── _footer.html
4.1.2 skin.xml 작성
<?xml version="1.0" encoding="UTF-8"?>
<skin version="0.2">
    <title xml:lang="ko">모던 게시판</title>
    <title xml:lang="en">Modern Board</title>

    <description xml:lang="ko">라이믹스용 현대적 게시판 스킨</description>
    <description xml:lang="en">Modern board skin for Rhymix</description>

    <version>2.0.0</version>
    <date>2024-01-01</date>

    <author email_address="developer@example.com">
        <name xml:lang="ko">개발자</name>
        <name xml:lang="en">Developer</name>
    </author>

    <license>MIT</license>

    <extra_vars>
        <var name="board_style" type="select">
            <title xml:lang="ko">게시판 스타일</title>
            <description xml:lang="ko">게시판의 표시 스타일을 선택하세요</description>
            <options value="list">목록형</options>
            <options value="card">카드형</options>
            <options value="gallery">갤러리형</options>
        </var>

        <var name="show_category" type="select">
            <title xml:lang="ko">카테고리 표시</title>
            <options value="Y">표시</options>
            <options value="N">숨김</options>
        </var>

        <var name="posts_per_page" type="text">
            <title xml:lang="ko">페이지당 게시물 수</title>
            <description xml:lang="ko">한 페이지에 표시할 게시물 수 (기본값: 20)</description>
        </var>

        <var name="thumbnail_width" type="text">
            <title xml:lang="ko">썸네일 너비</title>
            <description xml:lang="ko">갤러리형일 때 썸네일 너비 (기본값: 200)</description>
        </var>
    </extra_vars>
</skin>

4.2 현대적 게시판 목록 (list.html)

<load target="./css/board.css" />
<load target="./css/responsive.css" />
<load target="./js/board.js" type="body" />

<div class="modern-board board-style-{$skin_info->board_style|default:'list'}">
    <!-- 게시판 헤더 -->
    <div class="board-header" cond="$module_info->title || $module_info->description">
        <div class="board-title-area">
            <h1 class="board-title">
                <a href="{getUrl('', 'mid', $mid)}">{$module_info->title}</a>
            </h1>
            <p cond="$module_info->description" class="board-description">
                {$module_info->description}
            </p>
        </div>

        <!-- 게시판 통계 -->
        <div class="board-stats">
            <div class="stat-item">
                <span class="stat-label">전체 게시물</span>
                <span class="stat-value">{number_format($total_count)}</span>
            </div>
            <div class="stat-item" cond="$page > 1">
                <span class="stat-label">현재 페이지</span>
                <span class="stat-value">{$page}/{$page_navigation->last_page}</span>
            </div>
        </div>
    </div>

    <!-- 카테고리 및 필터 -->
    <div class="board-filters" cond="$category_list || $grant->list">
        <!-- 카테고리 -->
        <div cond="$category_list && $skin_info->show_category != 'N'" class="category-filter">
            <button type="button" class="category-btn {!$category ? 'active' : ''}" 
                    onclick="location.href='{getUrl('category', '')}'">
                전체
            </button>
            <button loop="$category_list => $category_item" 
                    type="button" 
                    class="category-btn {$category == $category_item->category_srl ? 'active' : ''}"
                    onclick="location.href='{getUrl('category', $category_item->category_srl)}'">
                {$category_item->title}
                <span class="category-count">({number_format($category_item->count)})</span>
            </button>
        </div>

        <!-- 정렬 옵션 -->
        <div class="sort-options">
            <label for="sort-select" class="sr-only">정렬 방식</label>
            <select id="sort-select" onchange="changeSortOrder(this.value)" class="sort-select">
                <option value="regdate" {$order_type == 'regdate' ? 'selected' : ''}>최신순</option>
                <option value="title" {$order_type == 'title' ? 'selected' : ''}>제목순</option>
                <option value="readed_count" {$order_type == 'readed_count' ? 'selected' : ''}>조회순</option>
                <option value="voted_count" {$order_type == 'voted_count' ? 'selected' : ''}>추천순</option>
                <option value="comment_count" {$order_type == 'comment_count' ? 'selected' : ''}>댓글순</option>
            </select>
        </div>

        <!-- 보기 방식 선택 -->
        <div class="view-toggle">
            <button type="button" 
                    class="view-btn {$skin_info->board_style == 'list' ? 'active' : ''}"
                    onclick="changeViewStyle('list')"
                    aria-label="목록형 보기">
                📄
            </button>
            <button type="button" 
                    class="view-btn {$skin_info->board_style == 'card' ? 'active' : ''}"
                    onclick="changeViewStyle('card')"
                    aria-label="카드형 보기">
                📋
            </button>
            <button type="button" 
                    class="view-btn {$skin_info->board_style == 'gallery' ? 'active' : ''}"
                    onclick="changeViewStyle('gallery')"
                    aria-label="갤러리형 보기">
                🖼️
            </button>
        </div>
    </div>

    <!-- 게시물이 없을 때 -->
    <div cond="!$document_list && !$notice_list" class="empty-board">
        <div class="empty-icon">📝</div>
        <h3>등록된 게시물이 없습니다</h3>
        <p>첫 번째 게시물을 작성해보세요!</p>
        <a cond="$grant->write_document" 
           href="{getUrl('act', 'dispBoardWrite')}" 
           class="btn btn-primary">
            글쓰기
        </a>
    </div>

    <!-- 공지사항 -->
    <div cond="$notice_list" class="notice-list">
        <h2 class="notice-title">📌 공지사항</h2>
        <div class="notice-items">
            <article loop="$notice_list => $document" class="notice-item">
                <div class="post-category" cond="$document->get('category_srl')">
                    {$category_list[$document->get('category_srl')]->title}
                </div>
                <h3 class="post-title">
                    <a href="{getUrl('document_srl', $document->document_srl)}">
                        {$document->getTitle()}
                        <span cond="$document->getCommentCount()" class="comment-count">
                            [{$document->getCommentCount()}]
                        </span>
                    </a>
                </h3>
                <div class="post-meta">
                    <span class="author">{$document->getNickName()}</span>
                    <time class="date" datetime="{$document->getRegdate('c')}">
                        {$document->getRegdate('Y.m.d')}
                    </time>
                    <span class="views">조회 {number_format($document->get('readed_count'))}</span>
                </div>
            </article>
        </div>
    </div>

    <!-- 일반 게시물 목록 -->
    <div cond="$document_list" class="document-list">
        <!-- 목록형 -->
        <div cond="$skin_info->board_style == 'list'" class="list-view">
            <div class="list-header">
                <div class="col-number">번호</div>
                <div class="col-category" cond="$category_list && $skin_info->show_category != 'N'">분류</div>
                <div class="col-title">제목</div>
                <div class="col-author">작성자</div>
                <div class="col-date">작성일</div>
                <div class="col-views">조회</div>
            </div>

            <article loop="$document_list => $no => $document" class="list-item">
                <div class="col-number">{$no}</div>
                <div class="col-category" cond="$category_list && $skin_info->show_category != 'N'">
                    <span cond="$document->get('category_srl')" class="category-tag">
                        {$category_list[$document->get('category_srl')]->title}
                    </span>
                </div>
                <div class="col-title">
                    <h3>
                        <a href="{getUrl('document_srl', $document->document_srl)}">
                            {$document->getTitle()}
                            <span cond="$document->getCommentCount()" class="comment-count">
                                [{$document->getCommentCount()}]
                            </span>
                        </a>
                    </h3>
                    <div cond="$document->getThumbnail()" class="post-thumbnail">
                        <img src="{$document->getThumbnail(50, 50)}" alt="" loading="lazy">
                    </div>
                </div>
                <div class="col-author">
                    <span class="author-name">{$document->getNickName()}</span>
                    <img cond="$document->getProfileImage()" 
                         src="{$document->getProfileImage()}" 
                         alt="{$document->getNickName()} 프로필"
                         class="author-avatar">
                </div>
                <div class="col-date">
                    <time datetime="{$document->getRegdate('c')}">
                        {$document->getRegdate('Y.m.d')}
                    </time>
                </div>
                <div class="col-views">{number_format($document->get('readed_count'))}</div>
            </article>
        </div>

        <!-- 카드형 -->
        <div cond="$skin_info->board_style == 'card'" class="card-view">
            <article loop="$document_list => $document" class="card-item">
                <div cond="$document->getThumbnail()" class="card-thumbnail">
                    <img src="{$document->getThumbnail(300, 200)}" alt="" loading="lazy">
                </div>

                <div class="card-content">
                    <div class="card-header">
                        <div cond="$document->get('category_srl')" class="card-category">
                            {$category_list[$document->get('category_srl')]->title}
                        </div>
                        <time class="card-date" datetime="{$document->getRegdate('c')}">
                            {$document->getRegdate('Y.m.d')}
                        </time>
                    </div>

                    <h3 class="card-title">
                        <a href="{getUrl('document_srl', $document->document_srl)}">
                            {$document->getTitle()}
                        </a>
                    </h3>

                    <p cond="$document->getContentText()" class="card-excerpt">
                        {cut_str(strip_tags($document->getContentText()), 100, '...')}
                    </p>

                    <div class="card-footer">
                        <div class="card-author">
                            <img cond="$document->getProfileImage()" 
                                 src="{$document->getProfileImage()}" 
                                 alt="{$document->getNickName()} 프로필"
                                 class="author-avatar">
                            <span class="author-name">{$document->getNickName()}</span>
                        </div>

                        <div class="card-stats">
                            <span class="views">👁 {number_format($document->get('readed_count'))}</span>
                            <span cond="$document->getCommentCount()" class="comments">
                                💬 {$document->getCommentCount()}
                            </span>
                            <span cond="$document->get('voted_count')" class="likes">
                                ❤️ {$document->get('voted_count')}
                            </span>
                        </div>
                    </div>
                </div>
            </article>
        </div>

        <!-- 갤러리형 -->
        <div cond="$skin_info->board_style == 'gallery'" class="gallery-view">
            <article loop="$document_list => $document" class="gallery-item">
                <a href="{getUrl('document_srl', $document->document_srl)}" class="gallery-link">
                    <div class="gallery-thumbnail">
                        <img cond="$document->getThumbnail()" 
                             src="{$document->getThumbnail($skin_info->thumbnail_width|default:200, $skin_info->thumbnail_width|default:200)}" 
                             alt="{$document->getTitle()}"
                             loading="lazy">
                        <div cond="!$document->getThumbnail()" class="no-image">
                            🖼️
                        </div>

                        <!-- 오버레이 정보 -->
                        <div class="gallery-overlay">
                            <h3 class="gallery-title">{cut_str($document->getTitle(), 30, '...')}</h3>
                            <div class="gallery-stats">
                                <span>👁 {$document->get('readed_count')}</span>
                                <span cond="$document->getCommentCount()">💬 {$document->getCommentCount()}</span>
                            </div>
                        </div>
                    </div>
                </a>
            </article>
        </div>
    </div>

    <!-- 페이지네이션 -->
    <nav cond="$page_navigation" class="pagination" aria-label="페이지 네비게이션">
        <a cond="$page_navigation->first_page < $page" 
           href="{getUrl('page', $page_navigation->first_page)}" 
           class="page-link page-first"
           aria-label="첫 페이지">
            ⏮️
        </a>

        <a cond="$page_navigation->prev_page" 
           href="{getUrl('page', $page_navigation->prev_page)}" 
           class="page-link page-prev"
           aria-label="이전 페이지">
            ◀️
        </a>

        <span loop="$page_navigation->page_list => $page_no" class="page-item">
            <strong cond="$page == $page_no" class="page-current" aria-current="page">
                {$page_no}
            </strong>
            <a cond="$page != $page_no" 
               href="{getUrl('page', $page_no)}" 
               class="page-link">
                {$page_no}
            </a>
        </span>

        <a cond="$page_navigation->next_page" 
           href="{getUrl('page', $page_navigation->next_page)}" 
           class="page-link page-next"
           aria-label="다음 페이지">
            ▶️
        </a>

        <a cond="$page_navigation->last_page > $page" 
           href="{getUrl('page', $page_navigation->last_page)}" 
           class="page-link page-last"
           aria-label="마지막 페이지">
            ⏭️
        </a>
    </nav>

    <!-- 하단 액션 -->
    <div class="board-actions">
        <div class="board-search">
            <form action="{getUrl()}" method="get" class="search-form" role="search">
                <input type="hidden" name="mid" value="{$mid}">
                <input type="hidden" name="category" value="{$category}">

                <label for="search-target" class="sr-only">검색 대상</label>
                <select id="search-target" name="search_target" class="search-target">
                    <option value="title_content" {$search_target == 'title_content' ? 'selected' : ''}>
                        제목+내용
                    </option>
                    <option value="title" {$search_target == 'title' ? 'selected' : ''}>
                        제목
                    </option>
                    <option value="content" {$search_target == 'content' ? 'selected' : ''}>
                        내용
                    </option>
                    <option value="nick_name" {$search_target == 'nick_name' ? 'selected' : ''}>
                        작성자
                    </option>
                    <option value="tags" {$search_target == 'tags' ? 'selected' : ''}>
                        태그
                    </option>
                </select>

                <label for="search-keyword" class="sr-only">검색어</label>
                <input type="search" 
                       id="search-keyword"
                       name="search_keyword" 
                       value="{htmlspecialchars($search_keyword)}" 
                       placeholder="검색어를 입력하세요"
                       class="search-input">

                <button type="submit" class="search-submit" aria-label="검색 실행">
                    🔍
                </button>
            </form>
        </div>

        <div class="write-actions">
            <a cond="$grant->write_document" 
               href="{getUrl('act', 'dispBoardWrite')}" 
               class="btn btn-primary write-btn">
                ✏️ 글쓰기
            </a>
        </div>
    </div>
</div>

<script>
// 정렬 변경
function changeSortOrder(orderType) {
    location.href = getUrl('order_type', orderType, 'page', '');
}

// 보기 방식 변경
function changeViewStyle(style) {
    // AJAX로 스킨 설정 변경 또는 쿠키 저장
    document.cookie = 'board_style=' + style + '; path=/';
    location.reload();
}

// URL 생성 함수 (기존 파라미터 유지)
function getUrl(key, value, ...args) {
    const url = new URL(location.href);
    url.searchParams.set(key, value);

    // 추가 파라미터 설정
    for (let i = 0; i < args.length; i += 2) {
        if (args[i + 1] === '') {
            url.searchParams.delete(args[i]);
        } else {
            url.searchParams.set(args[i], args[i + 1]);
        }
    }

    return url.toString();
}
</script>

5. 라이믹스 업데이트 사항

5.1 XE 대비 개선점

5.1.1 성능 향상
  • PHP 8+ 지원: 더 빠른 실행 속도
  • 템플릿 캐싱: 컴파일된 템플릿 캐시로 성능 향상
  • 최적화된 쿼리: 데이터베이스 성능 개선
5.1.2 보안 강화
  • CSRF 토큰: 자동 CSRF 보호
  • XSS 방지: 개선된 입력 필터링
  • SQL 인젝션 방지: 강화된 쿼리 보안
5.1.3 현대적 웹 표준
  • HTML5 완전 지원: 시맨틱 마크업
  • CSS3 활용: Flexbox, Grid 레이아웃
  • 반응형 웹: 모바일 퍼스트 접근

5.2 마이그레이션 가이드

5.2.1 XE 스킨 호환성
<!-- XE 1.x 문법 (여전히 지원됨) -->
<!--@if($is_logged)-->
    <p>환영합니다!</p>
<!--@end-->

<!-- 라이믹스 권장 문법 -->
<p cond="$is_logged">환영합니다!</p>
5.2.2 새로운 템플릿 기능
<!-- 조건부 클래스 -->
<div class="menu-item {$selected ? 'active' : ''}">메뉴</div>

<!-- 필터 함수 -->
<p>{$content|strip_tags|cut_str:100}</p>

<!-- 기본값 설정 -->
<h1>{$title|default:'제목 없음'}</h1>

<!-- 배열 접근 -->
<span>{$data['key']}</span>
5.2.3 개선된 보안 기능
<!-- 자동 CSRF 토큰 -->
<form action="./" method="post">
    {$security_token}
    <!-- 폼 내용 -->
</form>

<!-- XSS 방지 -->
<p>{htmlspecialchars($user_input)}</p>

<!-- 안전한 URL 생성 -->
<a href="{getUrl('mid', $mid, 'document_srl', $document_srl)}">링크</a>

5.3 개발 도구

5.3.1 디버깅 기능
// 개발 모드에서 템플릿 디버깅
{@debug($variable)}

// 성능 프로파일링
{@profile_start('template_section')}
<!-- 템플릿 코드 -->
{@profile_end('template_section')}
5.3.2 개발자 도구
# 라이믹스 CLI 도구 (가상의 예시)
rhymix make:skin board my_board_skin
rhymix make:layout my_layout
rhymix cache:clear
rhymix optimize:templates

참고 자료

공식 문서

웹 표준

접근성


라이선스: MIT
최종 수정: 2024년 1월 1일