카테고리 없음

Thymeleaf 가이드 - #5. 반복처리

shaprimanDev 2025. 8. 25. 11:51
반응형

5장. 표준 속성 (Standard Attributes)

5.1 텍스트와 HTML 출력 (th:text, th:utext)

th:text - 안전한 텍스트 출력

th:text는 가장 기본적인 Thymeleaf 속성으로, 텍스트를 안전하게 출력합니다. HTML 특수 문자들이 자동으로 이스케이프됩니다.

<div class="text-examples">
    <!-- 기본 텍스트 출력 -->
    <h1 th:text="${pageTitle}">기본 페이지 제목</h1>
    <p th:text="${user.name}">사용자 이름</p>

    <!-- HTML 특수문자 이스케이프 -->
    <p th:text="${userInput}">사용자 입력: &lt;script&gt;alert('XSS')&lt;/script&gt;</p>

    <!-- 숫자와 날짜 출력 -->
    <p>나이: <span th:text="${user.age}">25</span>세</p>
    <p>가입일: <span th:text="${#dates.format(user.joinDate, 'yyyy-MM-dd')}">2024-01-15</span></p>

    <!-- null 값 처리 -->
    <p th:text="${user.nickname ?: '닉네임 없음'}">닉네임</p>
    <p th:text="${user.bio}">자기소개가 없습니다.</p>

    <!-- 조건부 텍스트 -->
    <span th:text="${user.isOnline()} ? '온라인' : '오프라인'">상태</span>
    <span th:text="${order.isPaid()} ? '결제완료' : '미결제'">결제상태</span>
</div>

th:utext - HTML 내용 출력

th:utext는 HTML을 해석하여 출력합니다. 신뢰할 수 있는 HTML 내용에만 사용해야 합니다.

<div class="utext-examples">
    <!-- 에디터에서 작성된 HTML 내용 -->
    <div class="article-content" th:utext="${article.htmlContent}">
        <p>기본 <strong>HTML</strong> 내용입니다.</p>
    </div>

    <!-- 마크다운을 HTML로 변환한 내용 -->
    <div class="comment-body" th:utext="${comment.renderedContent}">
        <p>렌더링된 댓글 내용</p>
    </div>

    <!-- 이메일 템플릿 -->
    <div class="email-body" th:utext="${emailTemplate.body}">
        <h2>안녕하세요!</h2>
        <p>이메일 <em>내용</em>입니다.</p>
    </div>

    <!-- 조건부 HTML 출력 -->
    <div th:utext="${user.isVip()} ? '<span class=&quot;vip-badge&quot;>VIP</span>' : ''"></div>

    <!-- 국제화된 HTML 메시지 -->
    <div th:utext="#{message.welcome.html(${user.name})}">
        환영합니다, <strong>사용자</strong>님!
    </div>
</div>

th:textth:utext 보안 고려사항

<div class="security-examples">
    <!-- 안전한 사용 - 사용자 입력은 th:text 사용 -->
    <div class="user-comment">
        <h4 th:text="${comment.author.name}">작성자</h4>
        <p th:text="${comment.text}">댓글 내용</p>
        <small th:text="${#dates.format(comment.createdAt, 'yyyy-MM-dd HH:mm')}">작성일</small>
    </div>

    <!-- 위험한 사용 - 사용자 입력을 th:utext로 출력하면 XSS 위험 -->
    <!-- <div th:utext="${userInput}"></div> --> <!-- 절대 하지 말 것! -->

    <!-- 안전한 HTML 출력 - 서버에서 검증된 내용만 -->
    <div class="admin-notice" th:if="${adminNotice}" th:utext="${adminNotice.content}">
        <p>관리자 공지사항입니다.</p>
    </div>

    <!-- HTML 태그 제거 후 출력 -->
    <p th:text="${#strings.unescapeHtml(#strings.escapeJava(userBio))}">
        HTML 태그가 제거된 자기소개
    </p>

    <!-- 조건부 HTML 이스케이프 -->
    <div th:with="isAdminContent=${currentUser.isAdmin()}">
        <div th:if="${isAdminContent}" th:utext="${content}">관리자 전용 HTML</div>
        <div th:unless="${isAdminContent}" th:text="${content}">일반 텍스트</div>
    </div>
</div>

5.2 속성 설정 (th:attr, th:attrappend, th:attrprepend)

th:attr - 일반적인 속성 설정

<div class="attr-examples">
    <!-- 단일 속성 설정 -->
    <img th:attr="src=@{/images/users/{id}.jpg(id=${user.id})}, alt=${user.name}">

    <!-- 여러 속성 동시 설정 -->
    <a th:attr="href=@{/user/{id}(id=${user.id})}, 
                title=${user.name} + ' 프로필', 
                class=${user.isOnline()} ? 'user-link online' : 'user-link offline'">
        <span th:text="${user.name}">사용자명</span>
    </a>

    <!-- 조건부 속성 설정 -->
    <input type="text" 
           th:attr="placeholder=${#strings.isEmpty(user.name)} ? '이름을 입력하세요' : ${user.name},
                   maxlength=${user.isPremium()} ? '200' : '100'">

    <!-- 데이터 속성 설정 -->
    <div th:attr="data-user-id=${user.id},
                  data-role=${user.role},
                  data-premium=${user.isPremium()},
                  data-last-seen=${#dates.format(user.lastSeen, 'yyyy-MM-dd')}">
        사용자 정보
    </div>

    <!-- 동적 ID와 클래스 -->
    <div th:attr="id='user-' + ${user.id},
                  class='card ' + ${user.role} + ' ' + (${user.isActive()} ? 'active' : 'inactive')">
        동적 속성
    </div>
</div>

th:attrappend - 속성 값 추가

<div class="attrappend-examples">
    <!-- 클래스 추가 -->
    <div class="card" th:attrappend="class=${user.isVip()} ? ' vip-card' : ''">
        기본 카드에 VIP 클래스 추가
    </div>

    <div class="user-profile" 
         th:attrappend="class=${user.isOnline()} ? ' online' : ' offline'">
        온라인 상태 클래스 추가
    </div>

    <!-- 스타일 추가 -->
    <div style="padding: 10px;" 
         th:attrappend="style=${user.customColor} ? '; background-color: ' + ${user.customColor} : ''">
        사용자 정의 색상 추가
    </div>

    <!-- 여러 조건부 클래스 추가 -->
    <div class="post"
         th:attrappend="class=${post.isPinned()} ? ' pinned' : '',
                       class=${post.isHot()} ? ' hot' : '',
                       class=${post.hasImage()} ? ' with-image' : ''">
        게시글 상태 클래스들 추가
    </div>

    <!-- 데이터 속성 추가 -->
    <button class="action-btn" data-action="like"
            th:attrappend="data-count=' ' + ${post.likeCount},
                          data-user-liked=${currentUser.hasLiked(post)} ? ' user-liked' : ''">
        좋아요
    </button>
</div>

th:attrprepend - 속성 값 앞에 추가

<div class="attrprepend-examples">
    <!-- 클래스 앞에 추가 -->
    <div class="content" th:attrprepend="class=${user.role} + ' '">
        역할별 클래스를 앞에 추가
    </div>

    <!-- 경로 앞에 추가 -->
    <img src="default.jpg" 
         th:attrprepend="src=${user.avatarPath} ? ${user.avatarPath} + '/' : '/images/'">

    <!-- ID 접두사 추가 -->
    <div id="content" th:attrprepend="id=${pageType} + '-'">
        페이지 타입별 ID 접두사
    </div>

    <!-- 조건부 접두사 -->
    <span class="badge" 
          th:attrprepend="class=${notification.isUrgent()} ? 'urgent-' : 'normal-'">
        알림 배지
    </span>
</div>

특정 속성 직접 설정

<div class="specific-attr-examples">
    <!-- 자주 사용되는 속성들의 직접 설정 -->

    <!-- th:href -->
    <a th:href="@{/user/{id}(id=${user.id})}">프로필 보기</a>
    <a th:href="@{/products(category=${category.id}, page=${currentPage})}">상품 목록</a>

    <!-- th:src -->
    <img th:src="@{/images/products/{id}.jpg(id=${product.id})}" 
         th:alt="${product.name}">

    <!-- th:value -->
    <input type="text" th:value="${user.name}" name="name">
    <input type="hidden" th:value="${csrfToken}" name="_token">

    <!-- th:id와 th:class -->
    <div th:id="'section-' + ${section.id}" 
         th:class="'content-section ' + ${section.type}">
        섹션 내용
    </div>

    <!-- th:title과 th:alt -->
    <img th:src="@{/images/help.png}" 
         th:alt="#{help.icon.alt}" 
         th:title="#{help.icon.title}">

    <!-- th:style -->
    <div th:style="'width: ' + ${progress} + '%; background-color: ' + ${progressColor}">
        진행률 바
    </div>

    <!-- th:disabled와 th:readonly -->
    <input type="text" th:value="${user.email}" 
           th:readonly="${not user.canEditEmail()}">
    <button th:disabled="${not user.canPerformAction()}" 
            th:text="#{action.submit}">제출</button>

    <!-- th:checked와 th:selected -->
    <input type="checkbox" th:checked="${user.receiveNewsletter}">
    <select name="country">
        <option th:each="country : ${countries}"
                th:value="${country.code}"
                th:text="${country.name}"
                th:selected="${country.code == user.country}">국가</option>
    </select>
</div>

5.3 조건부 렌더링 (th:if, th:unless, th:switch)

th:ifth:unless - 기본 조건부 렌더링

<div class="conditional-rendering">
    <!-- 기본 조건부 표시 -->
    <div th:if="${user != null}">
        <h3>로그인 사용자 정보</h3>
        <p th:text="${user.name}">사용자명</p>
    </div>

    <div th:unless="${user != null}">
        <p><a th:href="@{/login}">로그인하세요</a></p>
    </div>

    <!-- 불리언 조건 -->
    <div th:if="${user.isActive()}">
        <span class="status active">활성 계정</span>
    </div>

    <div th:unless="${user.isActive()}">
        <span class="status inactive">비활성 계정</span>
        <p>계정을 활성화하려면 이메일을 확인하세요.</p>
    </div>

    <!-- 숫자 비교 -->
    <div th:if="${user.age >= 18}">
        <p>성인 인증이 완료되었습니다.</p>
        <button>성인 콘텐츠 보기</button>
    </div>

    <div th:if="${user.score >= 100}">
        <div class="achievement">
            <h4>🏆 100점 달성!</h4>
            <p>축하합니다!</p>
        </div>
    </div>

    <!-- 문자열 조건 -->
    <div th:if="${user.role == 'admin'}">
        <nav class="admin-nav">
            <a th:href="@{/admin}">관리자 패널</a>
            <a th:href="@{/admin/users}">사용자 관리</a>
        </nav>
    </div>

    <div th:unless="${#strings.isEmpty(user.bio)}">
        <div class="user-bio">
            <h4>자기소개</h4>
            <p th:text="${user.bio}">자기소개</p>
        </div>
    </div>
</div>

복합 조건과 논리 연산

<div class="complex-conditions">
    <!-- AND 조건 -->
    <div th:if="${user.isActive() and user.isVerified()}">
        <div class="full-access">
            <p>모든 기능을 사용할 수 있습니다.</p>
        </div>
    </div>

    <!-- OR 조건 -->
    <div th:if="${user.isPremium() or user.score >= 90}">
        <div class="premium-features">
            <h4>프리미엄 기능</h4>
            <ul>
                <li>광고 제거</li>
                <li>고급 통계</li>
                <li>우선 지원</li>
            </ul>
        </div>
    </div>

    <!-- NOT 조건 -->
    <div th:if="${not user.hasCompletedProfile()}">
        <div class="profile-incomplete-warning">
            <p>프로필을 완성해주세요!</p>
            <a th:href="@{/profile/edit}">프로필 편집</a>
        </div>
    </div>

    <!-- 복잡한 조건 -->
    <div th:if="${(user.isActive() and user.isVerified()) or user.isAdmin()}">
        <button>고급 설정</button>
    </div>

    <!-- 컬렉션 조건 -->
    <div th:if="${not #lists.isEmpty(user.orders)}">
        <div class="order-history">
            <h4>주문 내역</h4>
            <p>총 <span th:text="${#lists.size(user.orders)}">0</span>개의 주문</p>
        </div>
    </div>

    <div th:unless="${#lists.isEmpty(notifications)}">
        <div class="notifications">
            <span class="badge" th:text="${#lists.size(notifications)}">5</span>
            새 알림
        </div>
    </div>
</div>

th:switchth:case - 다중 분기

<div class="switch-examples">
    <!-- 기본 switch 문 -->
    <div th:switch="${user.role}">
        <div th:case="'admin'">
            <h3>관리자</h3>
            <p>시스템 전체를 관리할 수 있습니다.</p>
            <a th:href="@{/admin}" class="btn btn-danger">관리자 패널</a>
        </div>

        <div th:case="'moderator'">
            <h3>운영자</h3>
            <p>콘텐츠를 관리할 수 있습니다.</p>
            <a th:href="@{/moderation}" class="btn btn-warning">운영 도구</a>
        </div>

        <div th:case="'premium'">
            <h3>프리미엄 사용자</h3>
            <p>모든 프리미엄 기능을 이용할 수 있습니다.</p>
            <span class="badge badge-gold">PREMIUM</span>
        </div>

        <div th:case="*">
            <h3>일반 사용자</h3>
            <p>기본 기능을 이용할 수 있습니다.</p>
            <a th:href="@{/upgrade}" class="btn btn-primary">업그레이드</a>
        </div>
    </div>

    <!-- 숫자 기반 switch -->
    <div class="grade-display" th:switch="${student.grade}">
        <div th:case="'A'">
            <span class="grade excellent">A학점</span>
            <p>우수한 성적입니다! 🎉</p>
        </div>
        <div th:case="'B'">
            <span class="grade good">B학점</span>
            <p>양호한 성적입니다.</p>
        </div>
        <div th:case="'C'">
            <span class="grade average">C학점</span>
            <p>보통 성적입니다.</p>
        </div>
        <div th:case="*">
            <span class="grade poor">재수강 필요</span>
            <p>더 노력이 필요합니다.</p>
        </div>
    </div>

    <!-- 주문 상태별 처리 -->
    <div class="order-status" th:switch="${order.status}">
        <div th:case="'PENDING'">
            <span class="status pending">⏳ 처리 대기중</span>
            <p>주문이 접수되었습니다.</p>
            <button th:onclick="|cancelOrder(${order.id})|">취소</button>
        </div>

        <div th:case="'PROCESSING'">
            <span class="status processing">⚙️ 처리중</span>
            <p>주문을 준비하고 있습니다.</p>
        </div>

        <div th:case="'SHIPPED'">
            <span class="status shipped">🚚 배송중</span>
            <p>상품이 배송 중입니다.</p>
            <p>운송장번호: <code th:text="${order.trackingNumber}">123456789</code></p>
        </div>

        <div th:case="'DELIVERED'">
            <span class="status delivered">✅ 배송완료</span>
            <p>배송이 완료되었습니다.</p>
            <button th:onclick="|reviewOrder(${order.id})|">리뷰 작성</button>
        </div>

        <div th:case="'CANCELLED'">
            <span class="status cancelled">❌ 취소됨</span>
            <p>주문이 취소되었습니다.</p>
            <p>환불은 3-5일 소요됩니다.</p>
        </div>

        <div th:case="*">
            <span class="status unknown">❓ 상태 불명</span>
            <p>주문 상태를 확인할 수 없습니다.</p>
        </div>
    </div>

    <!-- 계절별 메시지 -->
    <div th:switch="${#dates.month(#dates.createNow())}">
        <div th:case="12" th:case="1" th:case="2">
            <div class="season winter">
                ❄️ 겨울입니다. 따뜻하게 입으세요!
            </div>
        </div>
        <div th:case="3" th:case="4" th:case="5">
            <div class="season spring">
                🌸 봄입니다. 꽃구경 어떠세요?
            </div>
        </div>
        <div th:case="6" th:case="7" th:case="8">
            <div class="season summer">
                ☀️ 여름입니다. 시원한 음료 어떠세요?
            </div>
        </div>
        <div th:case="*">
            <div class="season autumn">
                🍂 가을입니다. 단풍구경 어떠세요?
            </div>
        </div>
    </div>
</div>

조건부 CSS 클래스와 스타일

<div class="conditional-styling">
    <!-- 조건부 클래스 적용 -->
    <div th:class="${user.isOnline()} ? 'user-card online' : 'user-card offline'">
        <span th:text="${user.name}">사용자명</span>
        <span th:if="${user.isOnline()}" class="status-indicator">🟢</span>
        <span th:unless="${user.isOnline()}" class="status-indicator">⚫</span>
    </div>

    <!-- 여러 조건부 클래스 -->
    <article th:class="'post ' + 
                      (${post.isPinned()} ? 'pinned ' : '') + 
                      (${post.isHot()} ? 'hot ' : '') + 
                      (${post.hasImages()} ? 'with-images ' : '') +
                      ${post.category}">
        <h3 th:text="${post.title}">게시글 제목</h3>
    </article>

    <!-- 조건부 스타일 -->
    <div class="progress-bar">
        <div th:style="'width: ' + ${task.progress} + '%; ' + 
                       'background-color: ' + (${task.progress >= 100} ? 'green' : 
                                              ${task.progress >= 70} ? 'orange' : 'red')">
        </div>
    </div>

    <!-- 테마별 스타일 -->
    <div th:style="${user.theme == 'dark'} ? 'background: #333; color: white;' : 
                   (${user.theme == 'blue'} ? 'background: #e3f2fd; color: #1565c0;' : 
                    'background: white; color: black;')">
        사용자 정의 테마
    </div>
</div>

 

반응형